Frontend provider protocol#
A frontend owns how the operator sees and types. It does not own the agent loop, policy, or the state backend. Wire traffic with the kernel uses the kernel callback channel exclusively for session lifecycle, input, state, metadata, transcript, and token deltas; the category service is only the standard triple.
Transport#
FrontendService exposes:
| RPC | Shape | Purpose |
|---|---|---|
GetCapabilities | unary | Slash commands, config schema, supported hook points |
Configure | unary | Apply agent.hcl provider config |
Describe | unary | Plugin identity from the running process |
There is no Attach. Capabilities MUST be cheaply re-queryable and MUST NOT require a network call.
Callback-channel RPCs a frontend uses#
Documented fully in kernel-callbacks.md; listed here as the frontend-facing contract:
| Concern | RPCs |
|---|---|
| Session lifecycle | CreateSession, AttachSession, ResumeSession, DetachSession, ListSessions |
| Input | SubmitInput (returns turn_id), InvokeSlashCommand, TriggerAction, Interrupt |
| Plan / interactive | ResolvePlanDecision, ResolveInteractive |
| State | GetSessionState |
| Metadata | ListMetadata, and (for frontend-owned blocks) PublishMetadata / RetractMetadata |
| Transcript | ReadEvents |
| Live updates | Subscribe with topics kernel.event.*, kernel.state, kernel.metadata |
| Token fast path | StreamDeltas (server-streaming; not on the bus) |
Four surfaces#
Input#
Operator input is a capability, not a region. The frontend collects content (text, pasted images as ContentBlocks) and calls SubmitInput. Correlation uses the returned turn_id plus the event stream. Slash commands and ActionNode activations are separate unaries (InvokeSlashCommand, TriggerAction) so they keep the no-model-turn plan/apply path.
State#
SessionState (session.v1) is a fixed schema: SessionInfo plus working directory, VCS summary, model, thinking/effort, context pressure, turn count, elapsed, total tokens. No extension point — that is what makes it renderable by a status bar, HTTP header, stdout line, or spoken sentence.
Per-session: every snapshot and bus payload names exactly one session_id. A frontend attached to several sessions holds one state per session.
Startup sequence: GetSessionState then Subscribe on topic kernel.state (payload carries session_id). Snapshot-then-subscribe cannot drop updates that were committed after the snapshot if the frontend also re-reads on mismatch; the kernel republishes on kernel.state whenever a watched field changes.
Metadata#
Formerly "sidebar": a keyed collection of MetadataBlock (metadata.v1) with a closed body oneof (KeyValue, Progress, Status, ItemList, Timer), Tone token scale (neutral/info/success/warning/danger), and Liveness (LIVE / DISCONNECTED).
- Plugin authors compose blocks via
pkg/metadatabuilders; frontends mapToneto their own vocabulary (never a wire color). PublishMetadataupserts; producer is server-derived.- Retraction and publisher exit flip
livenesstoDISCONNECTEDand republish — the kernel never deletes a block. - Snapshot:
ListMetadata; live: topickernel.metadata(payload is the block, includingsession_id).
Transcript#
Conversation content travels as durable events (ReadEvents / kernel.event.{kind}) with optional Render payloads decoded to RenderTree (render-tree.md). There is no placement region: transcript is the conversation stream; chrome is state/metadata.
Token fast path#
StreamDeltas delivers live TokenDeltas (session_id, target_id, text) on a dedicated server-streaming RPC on the callback channel — not the event bus (no topic matching, no shared queue). The kernel forwards each delta promptly and does not batch; coalescing is the frontend's decision. Deltas are live-only: replayed text arrives as finished renders, never as deltas. Per-stream FIFO only; outside determinism.md replay guarantees by construction.
Session lifecycle#
- CreateSession — new session; auto-attaches the caller. Optional profile, working_directory, initial_prompt.
- AttachSession — subscribe to an existing (possibly live) session; backfill via
ReadEvents+ListMetadata. - ResumeSession — attach a historical session.
COMPLETED/CANCELLEDMAY re-open toRUNNINGfor new turns; bound-exhausted orFAILEDattaches replay-only — subsequentSubmitInputis rejected withFRONTEND_ERROR_CATEGORY_SESSION_REPLAY_ONLY. - DetachSession — drop this frontend's subscription; other frontends and the session itself are unaffected.
- ListSessions — filtered summary list.
No frontend-triggered session deletion. Pruning is a kernel CLI / operator action only.
Plan and interactive resolution#
When policy evaluates a plan item as ASK, or an interactive-kind tool blocks for input, the frontend learns via bus/hooks/render (plan-ready / interactive prompt content) and answers with ResolvePlanDecision or ResolveInteractive.
- First-response-wins across multi-attach frontends for a given pending id.
ClientDecisionis allow/deny;PlanDecisionScopeis once/session/always (plan.v1). ALWAYS that cannot be persisted MUST be rejected, never silently downgraded.- Corrected input on a plan decision MUST be re-validated against the tool's input schema.
Multi-attach arbitration#
ClientEvent-era connection multiplexing is gone. Multiple frontends each hold their own callback connection. Input and decisions are processed in kernel arrival order per session; the only single-winner rule is first-response-wins on pending plan/interactive ids.
Error taxonomy#
Structured FrontendError / FrontendErrorCategory on gRPC status details for Configure and residual frontend-local failures. Session and input errors from callback RPCs use the same category vocabulary where applicable (SESSION_NOT_FOUND, SESSION_CREATE_FAILED, SESSION_REPLAY_ONLY, INVALID_REQUEST, …). Region-unsupported is retired with placement regions.
Slash commands#
Unchanged aggregation model: direct-invoke from slashcommand.v1 providers; prompt-expansion from any category's capability response. Registry delivery is via bus/session attach side channel as specified in the slashcommand docs — not via a bidi Attach stream.