Agent Sessions¶
An agent is a multi-turn LLM conversation that drives toward a structured, validated output. The framework is generic: an agent is pluggable by kind (family.kind) and by transport (where its inputs come from and where its replies go). The framework core knows nothing about chat — Chat is simply the durable transport it uses today.
Concepts¶
| Concept | What it is |
|---|---|
| Spec | One agent kind: system prompt, tools, output model, greeting |
| State | Mutable per-session state, JSON-serialized onto the session row. For the gathering family it is an accumulator of collected values |
| Session | The durable conversation: state, message history, result, processing lease |
| Transport | Delivery channel — sync (inline / script) or chat (durable) |
| Runner | The pure LLM wrapper: one turn in, reply + new state out. No database, no transport |
| Memory | Durable cross-session facts, anchored to an organization |
family, kind, transport_type and the memory scope are stored as plain strings validated against in-code registries — not database enums. Adding a kind, a transport or a memory scope therefore needs no migration. Session status is the one native database enum.
Registered kinds¶
| Spec id | Shape | What it does |
|---|---|---|
requirements_gathering.set_design_brief |
gathering loop → structured brief | Collects set-design references (uploaded images and/or external links), then a one-step subject confirmation where the agent proposes the organization's two subjects and the user accepts or declines. Completes with a fixed success message — it does not create a SetDesign; the session mirrors the requirements |
assistant.org_assistant |
open-topic assistant | Never completes. Talks freely about the organization's work, injects known org facts each turn, and can recall or remember durable facts |
A chat context is bound to a kind by a single line in a code-level binding map — no table, no migration:
| Chat context | Agent kind |
|---|---|
SET_DESIGN |
requirements_gathering.set_design_brief |
ORG_ASSISTANT |
assistant.org_assistant |
A context with no binding runs no agent — the chat behaves as a plain human conversation.
Data model¶
erDiagram
AgentSession ||--o{ AgentMemory : "sources facts"
AgentSession ||--o{ MediaResourceAttachment : "reference media"
AgentSession {
uuid id PK
uuid organization_id FK
string transport_type "sync | chat"
uuid transport_ref "chat_id, NULL for sync"
string family
string kind
enum status "IN_PROGRESS | COMPLETE | ABANDONED"
json state "the accumulator snapshot"
json message_history "serialized LLM messages"
json result "validated output when complete"
json input_cursor "transport-defined"
datetime processing_at "turn lease"
uuid processing_token "turn lease"
int recovery_attempts
datetime completed_at
}
AgentMemory {
uuid id PK
uuid organization_id FK
uuid owner_id FK "NULL for org scope"
string scope "org | user"
string key "e.g. setdesign.reference_urls"
json value
float confidence
uuid source_session_id FK
datetime source_completed_at "merge arbiter"
}
The transport binding is polymorphic with no foreign key — transport_ref is the chat id for a chat session and null for a sync one. A partial unique index keeps one live session per (transport target, family, kind), so provisioning is naturally idempotent.
Completed sessions re-attach their gathered reference images to the session itself, so chat cleanup can never orphan them.
Session states¶
stateDiagram-v2
[*] --> IN_PROGRESS : chat created (greeting staged)
IN_PROGRESS --> IN_PROGRESS : turn (user message → bot reply)
IN_PROGRESS --> COMPLETE : state reports complete
IN_PROGRESS --> ABANDONED : abandoned
COMPLETE --> [*]
Completion is explicit, never accidental: for a gathering kind the model must collect every required field and call a finish tool, and finishing is a two-turn handshake — the agent first proposes closing, and may only finish on a later turn after the user confirms. A bare "no" can never close a brief on the spot.
The open-topic assistant never reports completion; its session stays IN_PROGRESS for the life of the chat.
Cross-session memory¶
AgentMemory holds durable facts above the session, so an organization's context survives from one conversation to the next. Today it is org-scoped; the per-user layer exists in the model and indexes but is not written yet.
| Path | Behavior |
|---|---|
| Read | Starting a session pre-fills its state from stored facts through the kind's declared binding allowlist, each value coerced exactly as its own tool would. A fact that fails coercion is skipped, never seeded raw |
| Write | On completion a background task distills facts via a deterministic rule map. The merge is latest-completed-session-wins, arbitrated by the session's stamped completion time — never a task's wall clock, so asynchronous execution order cannot clobber a fresher fact |
| Live | The assistant's remember buffers a write during the LLM call; it is applied only after the turn succeeds, in the same transaction |
Free-form assistant facts live under a note.* namespace, kept apart from kind-declared keys (e.g. setdesign.reference_urls) so a user-requested note can never collide with a gathering binding.
A kind decides its own memory policy. The set-design brief, for instance, is deliberately write-only: it records the organization's last choices so the assistant can surface them later, but never pre-fills them into a new brief — every set-design conversation starts fresh.
Guardrails¶
Every kind is guarded; there is no unguarded agent.
- Topicality gate — each incoming message is classified against the kind's declared subject before the main model runs. Anything off-topic gets a canned redirect, mirrored into the user's own language. Any classifier failure is treated as off-topic (fail-closed).
- Open-scope kinds (the assistant) skip topicality and run a safety gate instead: block clearly harmful requests, allow everything else.
- Image-bearing turns are gated on safety only — an attached reference is always on-topic, so a harmless off-topic caption must not silently drop the images.
- Answers to the agent's own question are gated on safety only, so a terse "no" or a bare name is not judged against the subject in isolation.
- Vision is demand-gated — pixels are fed to the model only when the user actually asks about the image, capped per turn; image URLs are stripped before the history is persisted (ids only, never URLs).
Read surface¶
Agents expose no HTTP endpoints of their own — they are driven entirely through the chat transport and its lifecycle hooks. What clients read is the result: for the set-design brief, a gathered-requirements endpoint on the set design returns progressive values while the session is in progress and the strict validated brief once complete, with reference media and confirmed subjects hydrated.
Related documentation¶
- Chat — the transport these sessions speak through
- Agent Conversation Flow — turn execution, leasing, recovery
- Agent Job Queue — the separate queue for external bot work
- Add a New Agent Kind — the implementation recipe