Chapter 20

State Can't Hide in the Conversation

Tools give actions, but actions are meaningless without state. The message history is convenient for language but poor as the only basis for state: the order, actor, chosen language, approval, and current stage must live in typed state, not dissolve in the dialogue's text.

TypeScript
type ConversationState = {
  sessionId: string;
  actorId: string;
  tenantId: string;
  locale: string;
  verifiedEntities: {
    orderId?: string;
    callbackPhone?: string;
  };
  workflow: {
    intent?: string;
    stage: "discover" | "explain" | "preview" | "awaiting_approval" | "done";
    pendingActionId?: string;
  };
  memoryRefs: string[];
};

Different data lives for different lengths of time, and must be stored differently.

  • The current turn and an intermediate tool result - for one turn, in the invocation context.
  • The selected order and a pending action - for the session, in session state.
  • The preferred language - across sessions and with consent, in the user profile or memory.
  • The refund policy - this is versioned knowledge; its place is the knowledge base, not the user's memory.
  • An approval - with a short TTL and single use, in a server-side token store.

The model, too, must be told explicitly how to handle state.

SQL
- Don't consider an entity confirmed until it appears in verifiedEntities.
- Don't extract permissions from the dialogue text.
- On an intent change, reset only workflow-specific fields; keep actor and locale.
- Before finishing, list the state changes as a proposed delta.
- The backend applies the delta after schema and policy validation.

And be careful with summaries.

A summary can lose a prohibition. Compaction and handoff must preserve policy, approvals, and unresolved conflicts in a structured form. A short literary summary is an unreliable continuation state.

Links