The typed turn event: one schema that every interface renders.
A conversation publishes events on the PubSub topic "conversation:<id>" as
{:conversation_event, conversation_id, wire_map}. Before this module each producer built an
untyped map and each consumer (Gateway, RPC server, TUI) read it again in its own way. Now a
producer builds an event with one function per type (the only sanctioned construction site, each
with a @spec), and a consumer reads it through a renderer (SuperPi.Event.Renderer).
There is one struct, not one per type. The wire form is a plain map anyway, and a renderer
matches on :type.
Wire form
to_wire/1 gives the flat map that consumers have always read, plus "v" (schema version, 1)
and "turn_id" (the id of the turn task, or nil outside a turn). Payload keys stay at the top
level, not under "data", so a consumer that matches %{"type" => "text_delta", "delta" => d}
keeps working. Both additions are purely additive.
Types
| Type | Payload keys |
|---|---|
turn_started | seq |
turn_completed | stop_reason, result, seq |
turn_error | error, seq |
turn_cancelled | seq |
text_delta | index, delta |
thinking_delta | index, delta |
toolcall_delta | index, delta, partial_args, and id and name when the provider gave them |
message_completed | role, content, usage |
tool_started | tool_call_id, name, arguments, overwritten_keys |
tool_blocked | tool_call_id, name, reason |
tool_completed | tool_call_id, name, is_error, output |
permission_request | request_id, tool_call_id, tool_name, arguments, reason, risk, options |
ui_request | request_id and the request map (for example kind, questions) |
ui_resolved | request_id, outcome |
state_changed | status |
mode_changed | mode, max_mode |
stream_reset | retry_in_ms |
compaction_completed | head_entry_id, summary |
usage_updated | input_tokens, output_tokens, total_input_tokens, total_output_tokens |
plan_updated | entries (todo items with id, text, completed) |
notice | level, message |
watch_started | seq |
subagent_started | child_conversation_id, role, task |
subagent_finished | child_conversation_id, status, summary |
stop_reason of turn_completed is "end_turn". permission_request options carry ACP
PermissionOptionKind values in kind: allow_once, allow_always, reject_once, reject_always.
Summary
Functions
Wraps a map that an extension passed to ctx.emit_event into an event. The map must have a
"type". Keys that belong to the envelope are taken from the arguments, not the map.
Reads a wire map back into an event. A map from an older producer (no "v", no "turn_id")
reads as version 1 with no turn.
The four permission options, with ACP PermissionOptionKind in kind.
A tool call waits for a human decision. request_id answers through SuperPi.UI.Broker.respond/2
with the option_id of one of options.
Publishes an event on the conversation topic.
The wire map of an event: its payload at the top level, plus the envelope keys.
PubSub topic of a conversation.
The event types.
Types
Functions
Wraps a map that an extension passed to ctx.emit_event into an event. The map must have a
"type". Keys that belong to the envelope are taken from the arguments, not the map.
Reads a wire map back into an event. A map from an older producer (no "v", no "turn_id")
reads as version 1 with no turn.
@spec permission_options() :: [map()]
The four permission options, with ACP PermissionOptionKind in kind.
A tool call waits for a human decision. request_id answers through SuperPi.UI.Broker.respond/2
with the option_id of one of options.
@spec publish(t()) :: :ok
Publishes an event on the conversation topic.
The wire map of an event: its payload at the top level, plus the envelope keys.
PubSub topic of a conversation.
@spec types() :: [String.t()]
The event types.