Changeset 0.26.0 (#165)

This commit is contained in:
2026-03-10 13:38:01 +00:00
parent dbc1a97343
commit 400f7dd176
48 changed files with 4923 additions and 208 deletions

View File

@@ -1,4 +1,4 @@
# Architecture — Chat Switchboard v0.20
# Architecture — Chat Switchboard v0.26
## Deployment Modes
@@ -47,34 +47,42 @@ Three Docker images support different deployment scenarios:
6. **Channels as Execution Context**: Channels are the universal container for conversation state — messages, tool activity, notes, and artifacts all hang off a channel. Today channels are single-user direct chats. The architecture anticipates multi-participant channels (team members, anonymous visitors, AI personas) for workflow execution, without requiring changes to the message tree, tool framework, or streaming infrastructure.
## Workflow Architecture (Future — v0.25.0+)
## Workflow Architecture (v0.26.0 — Shipped)
The platform's existing primitives (teams, personas, channels, tools, notes)
compose into a workflow engine where the channel is the execution context
that moves through defined stages.
### Conceptual Model
### Data Model
```
Team
└─ Workflow (team-admin defined)
├─ name, description
├─ entry conditions (public link, team-internal, API trigger)
Stages[]
└─ Workflow (admin-defined, team-scoped or global)
├─ name, slug, description, branding (JSONB)
├─ entry_mode (authenticated | public_link)
is_active, version (auto-incremented on edit)
└─ Stages[] (ordered)
├─ persona_id (which AI drives this stage)
├─ assignment_team_id (who can be assigned)
├─ form_template (structured note schema, optional)
transitions[] (conditions → next stage)
├─ system_prompt (stage-specific override)
├─ form_template (JSONB — fields to collect)
history_mode (full | summary | fresh)
└─ assignment_team_id (who can be assigned)
Channel (workflow instance)
├─ workflow_id + current_stage
├─ workflow_id + workflow_version + current_stage
├─ stage_data (JSONB — accumulated form data across stages)
├─ workflow_status (active | completed | stale | cancelled)
├─ participants[]
│ ├─ anonymous visitor (mTLS fingerprint / session token)
│ ├─ anonymous visitor (session token)
│ ├─ AI persona (per-stage, from workflow definition)
│ └─ assigned team member (claimed or auto-routed)
│ └─ assigned team member (claimed from assignment queue)
├─ messages (existing tree structure)
─ notes (channel-scoped artifacts, intake forms)
└─ tool activity (existing execution framework)
─ notes (stage data persisted as channel-scoped notes)
WorkflowAssignment (queue entry)
├─ channel_id + stage + team_id
├─ assigned_to (nullable — claimed by team member)
└─ status (unassigned | claimed | completed)
```
### How Existing Primitives Map
@@ -84,33 +92,46 @@ Channel (workflow instance)
| **Persona** (system prompt + model) | Drives a workflow stage — the AI knows what to collect, when to route |
| **Team** (members + roles) | Owns the workflow definition; members are assignable to channels |
| **Channel** (messages + tree) | Execution instance of a workflow; full conversation history |
| **Notes + Tools** | Structured data collection; the persona's system prompt *is* the form definition, the note *is* the filled form |
| **Notes + Tools** | Structured data collection; `workflow_advance` tool triggers transitions, notes persist form data |
| **EventBus + WebSocket** | Real-time notifications for assignment, stage transitions, new messages |
| **Anonymous Sessions** (v0.24.3) | Visitors participate without user accounts |
| **DenyVisitor** predicate | Scopes tool availability per participant type |
### Design Constraints for Current Development
### Stage Transition Flow
These invariants keep the workflow path open without building it prematurely:
```
Visitor starts → Channel created (stage 0, persona bound)
Persona collects form data via conversation
Persona calls workflow_advance(data) → Stage notes created
│ │
▼ ▼
Next stage: new persona bound If assignment_team_id set:
(or workflow completes) → workflow_assignment created
→ team member claims
→ member + persona collaborate
```
- **Channels**: Don't assume single-owner. If touching channel queries, keep
room for `team_id`, `type` (beyond `direct`), and multi-participant access
patterns alongside `user_id`.
- **Notes**: User-scoped with `[[wikilink]]` bi-directional linking (v0.17.3).
The `note_links` junction table tracks directed edges between notes, with
`target_note_id` nullable for dangling links (unresolved references). Links
are extracted from content on save via regex, resolved by title match, and
re-resolved when new notes are created. The graph endpoint returns all nodes,
edges, and unresolved links for Canvas-based force-directed visualization.
Future channel-scoped notes (attached to a conversation) need a `channel_id`
FK option. `source_message_id` enables jump-to-source provenance from notes
back to the originating chat message.
- **Personas**: Already team-scoped. Don't couple to "user picks from dropdown"
— a workflow stage references a persona programmatically.
- **Tool ExecutionContext**: Already carries `UserID` + `ChannelID`. Will need
`TeamID` and `WorkflowID` — the struct is easily extended.
- **Auth**: mTLS anonymous users need identity to participate in channels.
Lightest version: a `participants` table that can reference a `user_id` or
an opaque session identifier (cert fingerprint). Don't assume every channel
participant has a row in `users`.
### Key Implementation Details
- **Form template injection.** When `channelType == "workflow"`, the completion
handler loads the current stage's `form_template` JSONB and injects a system
prompt telling the persona what fields to collect and when to call
`workflow_advance`.
- **Dialect-safe stage data merge.** `MergeWorkflowStageData()` reads existing
JSON from the `stage_data` column, merges in Go, writes back. No
Postgres-specific JSONB operators — works on both Postgres and SQLite.
- **Optimistic claim lock.** `UPDATE workflow_assignments SET assigned_to = $1
WHERE id = $2 AND status = 'unassigned'` — if rows_affected == 0, another
team member already claimed it.
- **Staleness sweep.** Background goroutine (1h tick) marks workflow instances
as `stale` when `last_activity_at` exceeds `WORKFLOW_STALE_HOURS`.
## Package Structure