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