Skip to content

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 keytransport_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.