AI agent state hooks (useKoderAgent / useKoderChat)
Framework-neutral state-machine contract for any AI chat / agent surface in a Koder product — message list, streaming state, append handlers, abort, retry, tool-call lifecycle. Companion to patterns/ai-chat-surface.kmd (which ratifies the 6 primitives); this spec ratifies the state machine driving them. Modeled after Ant Design X `useXAgent` / `useXChat`.
Pattern — AI agent state hooks
*tatus* v0.1.0 — Draft. Contract
only; perframework implementations (useKoderAgentfor React,KoderAgentControllerfor Flutter,KoderAgentViewModelfor Kotlin / Swift) live in the respective SDK backlogs.
R1 — Three roles, one machine
The state machine has three logical roles a consumer may attach to:
| Role | Responsibility |
|---|---|
| *gent* | Owns the transport: opens connection, streams tokens, surfaces lifecycle events. |
| *hat* | Owns the message list: append user message, append assistant streaming, mutate history, dispatch retry. |
| *ool host* | Owns tool |
A simple chatonly surface attaches Chat alone. An agentheavy surface (Kortex assist with tool use) attaches all three.
R2 — Required state shape
Every framework binding MUST expose at least:
| Field | Type | Notes |
|---|---|---|
messages |
Message[] |
Ordered history (user / assistant / system / tool) |
streaming |
boolean |
True between requestSend and streamEnd / error |
lastError |
Error | null |
Most recent transport / model error (cleared on requestSend) |
abortable |
boolean |
True iff a stream can be aborted now |
pendingToolCalls |
ToolCall[] |
Tool calls awaiting user permission per specs/ai-ui/permission-prompt.kmd |
R3 — Required actions
| Action | Effect |
|---|---|
send(text, attachments?) |
Append user message → start stream |
regenerate(messageId) |
Re-run from the prior turn; discards subsequent history |
edit(messageId, newText) |
Mutate user message → regenerate from that turn |
branch(messageId) |
Fork: create a new conversation from this turn (per specs/ai-ui/conversation-history.kmd) |
abort() |
Cancel in-flight stream; partial response retained in history with aborted: true |
clear() |
Reset to empty (does NOT delete conversation in storage; that's a separate delete() for explicit removal) |
R4 — Lifecycle events (emitted to subscribers)
onUserMessage → onStreamStart → onStreamToken (many) → onToolCall (optional) → onStreamEnd | onError | onAbort
Consumers MAY hook any subset. The state shape (R2) is the canonical view; events are for side effects (telemetry, sound, haptics).
R5 — Tool-call lifecycle (subset)
When AI emits a tool call:
pendingToolCallsgets the entry,streaming: false.- Consumer surfaces
permission-prompt.kmdUI. - On approve: framework binding dispatches to the registered tool;
on result,
messagesgains atoolentry;streaming: trueresumes. - On deny: append
toolentry withdenied: true;streaming: trueresumes (AI sees denial in context).
R6 — Multi-conversation
The hook is scoped per-conversation. Switching conversations (per specs/ai-ui/conversation-history.kmd) instantiates a new hook with its own state. Consumers maintain a list of hook instances or use a provider that supplies the active one.
R7 — Concurrency
- Only one
sendcan be in flight per hook instance. - A second
sendwhilestreaming: trueis rejected with a clearerror; consumers should disable the Sender button (
#066Sender primitive R3 morphs to Stop).
R8 — i18n + a11y
State is localeneutral; userfacing strings come from patterns/
ai-chat-surface.kmd + patterns/ai-feature-visual-language.kmd disclosure microcopy. Hooks MUST surface streaming state in a form the Bubble primitive can route to aria-live="polite".
Não-escopo
- Transport protocol (SSE / WebSocket / chunked HTTP) — separate
specs/ai-ui/transport.kmdif pursued. - Token counting / cost display — covered by
specs/ai-ui/cost-display.kmd. - Memory / RAG retrieval — separate spec.