event

package
v0.14.21 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 3, 2026 License: MIT Imports: 4 Imported by: 0

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:

  1. {"type":"system","subtype":"hook_started", ...} // optional, skip
  2. {"type":"system","subtype":"hook_response", ...} // optional, skip
  3. {"type":"system","subtype":"init", "session_id":"...", ...}
  4. {"type":"assistant","message":{"content":[ {"type":"text","text":"..."}, {"type":"tool_use","id":"t1","name":"Bash","input":{}} ]}}
  5. {"type":"user","message":{"content":[ {"type":"tool_result","tool_use_id":"t1","content":"..."} ]}} // tool result wrapped as user msg
  6. {"type":"result","subtype":"success","is_error":false,"result":"..."}
  7. ... 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
)

func (EventType) String

func (t EventType) String() string

String makes log lines readable. Not used for serialization.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL