Documentation
¶
Overview ¶
Package event holds the CLI-agnostic event abstraction. Every supported CLI (claude, codex, gemini) emits its own stream-json flavor; this package normalizes those into a single AgentEvent type so the rest of the agents pipeline (state machine, store, pool) doesn't have to care which backend produced the line.
Files:
- types.go — AgentEvent + EventType
- parser.go — Parser interface
- claude.go — ClaudeParser implementation (phase 2 scope)
Codex and Gemini parsers land in phase 6.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type AgentEvent ¶
type AgentEvent struct {
Type EventType
Text string // TextDelta / Thinking / ToolResult body
ToolName string // ToolUse: tool identifier (e.g. "Bash")
ToolInput string // ToolUse: JSON-encoded arguments before exec
ToolUseID string // ToolUse + ToolResult: correlation ID to pair call with result
IsError bool // ToolResult: true when the tool returned an error
SessionID string // SessionStart: CLI session ID (or first event for Claude)
ErrorMsg string // Error: short reason
Raw string // verbatim source line
}
AgentEvent is the normalized event passed downstream of every parser. Fields are populated based on Type — only Type and Raw are always set.
Raw holds the verbatim JSON line so raw.jsonl in the session folder can mirror the upstream stream byte-for-byte (debug view).
type ClaudeParser ¶
type ClaudeParser struct {
// contains filtered or unexported fields
}
ClaudeParser parses the Claude CLI `--output-format stream-json` stream when claude is run as `claude -p --verbose --input-format stream-json --output-format stream-json` (the long-lived headless mode used by ClaudeSpawner).
Wire shape per turn:
- {"type":"system","subtype":"hook_started", ...} // optional, skip
- {"type":"system","subtype":"hook_response", ...} // optional, skip
- {"type":"system","subtype":"init", "session_id":"...", ...}
- {"type":"assistant","message":{"content":[ {"type":"text","text":"..."}, {"type":"tool_use","id":"t1","name":"Bash","input":{}} ]}}
- {"type":"user","message":{"content":[ {"type":"tool_result","tool_use_id":"t1","content":"..."} ]}} // tool result wrapped as user msg
- {"type":"result","subtype":"success","is_error":false,"result":"..."}
- ... process stays alive, next turn starts at step 3 again
Concurrency: not safe for concurrent use. One parser per subprocess.
func NewClaudeParser ¶
func NewClaudeParser() *ClaudeParser
NewClaudeParser returns a fresh parser ready to consume Claude stream-json lines.
func (*ClaudeParser) Parse ¶
func (p *ClaudeParser) Parse(line string) (AgentEvent, error)
Parse decodes one line and returns the normalized event. Empty / ws- only lines yield (Unknown, nil) — caller can stream stdout without filtering.
A single claude line frequently carries multiple semantic events (e.g. one assistant message with both text and tool_use blocks). We can't return more than one event per call, so we collapse: text content wins over tool_use for the headline event, but we still surface the tool via subsequent calls? No — claude emits one block type per assistant frame in practice; if both ever co-occur, the raw line is preserved so downstream consumers can re-parse.
func (*ClaudeParser) SessionID ¶
func (p *ClaudeParser) SessionID() string
SessionID returns the captured CLI session ID, or "" if no `system init` event has been seen yet.
type CodexParser ¶ added in v0.13.4
type CodexParser struct {
// contains filtered or unexported fields
}
CodexParser parses the `codex exec --json` newline-delimited JSON stream.
Actual wire shape from codex 0.129+ --json:
{"type":"thread.started","thread_id":"<uuid>"}
{"type":"turn.started"}
{"type":"item.created","item":{"id":"...","type":"function_call","name":"...","call_id":"..."}}
{"type":"item.updated","item":{"id":"...","type":"agent_message","text":"partial..."}} ← streaming snapshot
{"type":"item.completed","item":{"id":"...","type":"agent_message","text":"full text"}}
{"type":"turn.completed","usage":{...}}
{"type":"error","message":"..."}
Session ends when process exits. thread_id is used as session ID for --resume.
Streaming semantics:
- item.updated for agent_message carries the FULL text so far (snapshot, not chunk). We diff against the last-seen text per item.id and emit only the appended tail as a TextDelta so consumers (FE, store) append naturally without dedup work.
- item.completed for agent_message carries the final full text. We emit only the remaining tail (if any) — usually empty because item.updated already streamed everything.
Concurrency: not safe for concurrent use. One parser per subprocess.
func NewCodexParser ¶ added in v0.13.4
func NewCodexParser() *CodexParser
NewCodexParser returns a fresh parser ready to consume codex --json lines.
func (*CodexParser) Parse ¶ added in v0.13.4
func (p *CodexParser) Parse(line string) (AgentEvent, error)
Parse decodes one codex --json line into an AgentEvent. Blank/whitespace lines return (Unknown, nil).
type EventType ¶
type EventType int
EventType is the normalized event taxonomy used across all CLIs.
`Thinking` is optional — only Claude exposes thinking deltas; other parsers may never emit it. Consumers must not rely on Thinking arriving before TextDelta.
const ( // Unknown is the zero value, used for parser output that the // caller can safely skip (e.g. control frames, keepalives). Unknown EventType = iota // SessionStart fires once per spawn, carrying the CLI's session ID // so wick can persist it for `--resume`. SessionStart // Thinking is a chain-of-thought delta (Claude only). UI may show // it in the raw view; conversation.jsonl skips it. Thinking // TextDelta is one chunk of streamed assistant output. Consumers // concatenate the .Text fields until Done to get the full reply. TextDelta // ToolUse fires when the CLI is about to invoke a tool (Bash, // edit, ...). ToolName + ToolInput are populated; the wick command // gate keys off this in phase 3. ToolUse // ToolResult fires after a tool finishes. Body is in .Text. ToolResult // Done marks end-of-turn — subprocess is idle until next input. Done // Error indicates the CLI emitted an error event (not a parse // failure — those are returned via Parser.Parse error). Error )
type Parser ¶
type Parser interface {
Parse(line string) (AgentEvent, error)
}
Parser turns one CLI stdout line into an AgentEvent. Implementations are stateful when the CLI's stream-json grammar requires it (e.g. Claude emits content_block_start then a sequence of content_block_delta for the same block — the parser tracks "what kind of block am I in" across calls).
Parse returns:
- (AgentEvent{Type: Unknown}, nil) → line is parseable but uninteresting
- (event, nil) → caller forwards the event
- (_, err) → line is malformed; caller logs and skips
A blank line is always (Unknown, nil) — never an error — so naive scanners can hand every line to Parse without filtering.