event

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package event defines the typed event stream the agent emits as it runs a turn, and the Sink it emits to. It decouples "what happened" (the model produced reasoning, a tool was dispatched, a turn used N tokens) from "how to show it" (ANSI scrollback in a terminal, a card in a webview).

The agent depends only on Sink; each frontend implements one. The chat TUI renders events to its scrollback; a headless run renders them to plain ANSI on stdout; a future GUI/serve transport forwards them to a webview or websocket. This replaces the old io.Writer contract, where the agent wrote pre-formatted ANSI and the consumer had to re-derive structure by matching line prefixes — fragile, and lossy for any frontend richer than a terminal.

itemadapter.go — upgrade spec 4-1 Step 1: dual-track event emission. This sink wrapper sits between the agent and the real sink, translating every legacy Kind event into the ItemEvent form and emitting BOTH. Old sinks see exactly what they saw before (zero breaking); new consumers (mobile bridge, future remote) can listen for Kind == Item and render directly.

The mapping is lossless — every field the desktop reducer reads from the flat events is carried in the structured Item payload.

items.go — upgrade spec 4-1 Step 1: the item-based event model that will eventually replace the flat Kind enum. In Step 1 the agent emits BOTH the old Kind events (for every existing sink) and ItemEvents (for new consumers: mobile bridge, future remote). The dual-track is zero-breaking: old sinks simply ignore ItemEvents on the wire.

An "item" is one thing on the timeline the user sees — one user message, one tool call card, one reasoning block, one notice. Items have a stable ID and a three-phase lifecycle:

item_started    → the item exists (card appears)
item_delta      → incremental content (streaming text, partial patch)
item_completed  → the item is final (card settles)

This maps 1:1 to how the desktop reducer already works internally; the ItemEvent form makes that structure explicit on the wire so any frontend (desktop, mobile, remote) can render without translating from 18 flat event kinds.

snapshot.go — upgrade spec 4-2: periodic item snapshots that let a frontend restore any-length sessions instantly (replacing the 100-turn present sidecar cap). A snapshot is a full item list at a known revision; recovery replays deltas after it.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RecordReadinessAudit

func RecordReadinessAudit(s Sink, a evidence.ReadinessAudit)

RecordReadinessAudit forwards a readiness audit receipt to sinks that opt in.

Types

type AgentMessageItem added in v0.2.0

type AgentMessageItem struct {
	Text      string `json:"text,omitempty"`
	Reasoning string `json:"reasoning,omitempty"`
}

AgentMessageItem is the payload for ItemAgentMessage.

type Approval

type Approval struct {
	ID      string
	Tool    string
	Subject string
	// Decisions (spec 5-7) are the selectable options; nil = default 3.
	Decisions []Decision
	// Args is the raw JSON arguments of the call being approved, so a card can
	// show specifics itself (the bash command, target paths). Empty for
	// synthetic approvals (plan/replan) that have no tool args.
	Args string
	// Changes carries the previewed per-file changes for writer tools (via the
	// tool's Previewer, computed at request time — before the permission
	// decision). Nil for tools that can't describe their change.
	Changes []FileChange
}

Approval identifies a pending tool-call approval for an ApprovalRequest event. ID correlates the request with the controller's Approve(ID, …) reply.

type ApprovalItem added in v0.2.0

type ApprovalItem struct {
	Tool    string       `json:"tool"`
	Subject string       `json:"subject"`
	Changes []FileChange `json:"changes,omitempty"`
}

ApprovalItem is the payload for ItemApproval.

type Ask

type Ask struct {
	ID        string
	Questions []AskQuestion
}

Ask carries an AskRequest: a batch of questions and the ID that correlates the controller's AnswerQuestion(ID, …) reply.

type AskAnswer

type AskAnswer struct {
	QuestionID string
	Selected   []string
}

AskAnswer is the user's reply to one AskQuestion: the chosen option label(s) (a free-typed answer is carried as a single Selected entry).

type AskOption

type AskOption struct {
	Label       string
	Description string // optional one-line explanation shown under the label
}

AskOption is one choice the user can pick for an AskQuestion.

type AskQuestion

type AskQuestion struct {
	ID      string // stable per-question id, so answers correlate back
	Header  string // short label (the tab title)
	Prompt  string // the question text
	Options []AskOption
	Multi   bool // allow selecting more than one option
}

AskQuestion is one structured question the `ask` tool puts to the user.

type Attachment

type Attachment struct {
	Path string `json:"path"` // repo-relative, under .fairpeer/attachments/
	Kind string `json:"kind"` // "image"
}

Attachment is a file (e.g. a generated image) a tool produced alongside its text output, so a frontend can render it directly under the tool card without relying on the model to echo the path into its reply.

type CacheDiagnostics

type CacheDiagnostics struct {
	PrefixHash          string
	PrefixChanged       bool
	PrefixChangeReasons []string // "system", "tools", "log_rewrite"
	SystemHash          string
	ToolsHash           string
	LogRewriteVersion   int
	ToolSchemaTokens    int
	CacheMissTokens     int
	CacheHitTokens      int
}

CacheDiagnostics describes whether and why the cacheable prefix changed since the last turn. It rides on the Usage event so every frontend can show cache-churn attribution.

type Collab

type Collab struct {
	RunID     string
	TeamID    string
	TeamName  string
	Task      string
	Mode      string
	Rounds    [][]CollabAnswer
	Synthesis string
	CreatedAt int64 // unix ms
}

Collab is the payload of an ExpertCollab event: everything a frontend needs to render a finished collaboration as an expandable card — provenance (team, task, mode), the per-round expert answers, the synthesis, and when it ran. Mirrors experts.CollabRecord (the persisted form) field-for-field.

type CollabAnswer

type CollabAnswer struct {
	ExpertName string
	Text       string
}

CollabAnswer is one expert's answer within one round of a multi-model collaboration. ExpertName is the contributing team member; Text is its answer. Mirrors experts.ExpertAnswer at the event boundary so the event package doesn't depend on experts.

type Compaction

type Compaction struct {
	Trigger  string // "auto" | "manual"
	Messages int    // Done: how many messages were folded into the summary
	Summary  string // Done: the briefing the agent keeps relying on
	Archive  string // Done: path the dropped originals were archived to ("" if none)
}

Compaction carries a context-compaction pass for the CompactionStarted / CompactionDone events. On CompactionStarted only Trigger is set. On CompactionDone, Messages/Summary/Archive are filled in (an aborted pass leaves Summary empty). Trigger is "auto" (the prompt reached the window threshold) or "manual" (the user ran /compact).

type CompactionItem added in v0.2.0

type CompactionItem struct {
	Trigger  string `json:"trigger"`
	Messages int    `json:"messages,omitempty"`
	Summary  string `json:"summary,omitempty"`
}

CompactionItem is the payload for ItemCompaction.

type Decision added in v0.2.0

type Decision struct {
	Label        string        `json:"label"`
	Scope        string        `json:"scope"` // once | session | always | path | host
	Restrictions *Restrictions `json:"restrictions,omitempty"`
}

Decision is one option the user can pick on an approval card (spec 5-7). The default 3 (once/session/always) stay unchanged; fine-grained options appear only when the tool carries path or network restrictions.

type Event

type Event struct {
	Kind             Kind
	Text             string            // Reasoning / Text / Message / Notice / Phase
	Reasoning        string            // Message: the full reasoning chain
	Tool             Tool              // ToolDispatch / ToolResult
	Usage            *provider.Usage   // Usage
	Pricing          *provider.Pricing // Usage: for cost display (nil = omit cost)
	CacheDiagnostics *CacheDiagnostics // Usage: cache-churn attribution (nil = N/A)
	// SessionHit/SessionMiss carry cumulative cache tokens across the whole
	// session (Usage events only), so a frontend can show the aggregate hit-rate
	// — which doesn't crater on a short turn or after compaction — alongside
	// Usage's single-turn numbers.
	SessionHit   int        // Usage: cumulative cache-hit prompt tokens this session
	SessionMiss  int        // Usage: cumulative cache-miss prompt tokens this session
	Level        Level      // Notice
	Approval     Approval   // ApprovalRequest
	Ask          Ask        // AskRequest
	Err          error      // TurnDone: non-nil on failure
	Compaction   Compaction // Compaction
	RetryAttempt int        // Retrying: 1-based attempt about to be made
	RetryMax     int        // Retrying: total attempts before giving up
	RetryAfterMs int64      // Retrying: backoff delay before the attempt (0 = immediate)
	Collab       Collab     // ExpertCollab
	// Item carries the item-model payload (4-1 dual-track). Non-nil only on
	// the synthetic "Item" Kind; old sinks skip it naturally.
	Item *ItemEvent
}

Event is one increment in a turn's event stream. Read the field(s) documented for Kind; the others are zero.

type FileChange added in v0.2.0

type FileChange struct {
	Path    string
	Kind    string
	Added   int
	Removed int
	Diff    string
}

FileChange is one file within a previewed multi-file change (the Approval payload). Kind mirrors diff.Kind: "create" | "modify" | "delete".

type FileDiff

type FileDiff struct {
	Diff    string
	Added   int
	Removed int
}

FileDiff is a previewed change carried on a writer tool's full ToolDispatch and on its ApprovalRequest, so a frontend can render +/- lines before the call runs. Diff is the unified diff (empty for read-only tools, binary files, or no-op changes); Added/Removed are its line tallies.

type FuncSink

type FuncSink func(Event)

FuncSink adapts a plain function to a Sink.

func (FuncSink) Emit

func (f FuncSink) Emit(e Event)

Emit calls the wrapped function.

type ItemAdapter added in v0.2.0

type ItemAdapter struct {
	// contains filtered or unexported fields
}

ItemAdapter wraps a Sink and emits ItemEvents alongside legacy kinds.

func NewItemAdapter added in v0.2.0

func NewItemAdapter(inner Sink) *ItemAdapter

NewItemAdapter creates the dual-track wrapper.

func (*ItemAdapter) Emit added in v0.2.0

func (a *ItemAdapter) Emit(e Event)

Emit implements Sink: forward the legacy event AND emit the Item form.

func (*ItemAdapter) RecordReadinessAudit added in v0.2.0

func (a *ItemAdapter) RecordReadinessAudit(audit evidence.ReadinessAudit)

RecordReadinessAudit forwards the optional sink capability so sinks that implement ReadinessAuditSink keep receiving audits through the adapter.

func (*ItemAdapter) Reset added in v0.2.0

func (a *ItemAdapter) Reset()

Reset clears per-turn state (call at TurnStarted).

type ItemEvent added in v0.2.0

type ItemEvent struct {
	Phase    ItemPhaseTransition `json:"phase"`
	ItemID   string              `json:"item_id"`
	ItemKind ItemKind            `json:"item_kind"`
	// Delta carries the incremental text for ItemDelta (streaming).
	Delta string `json:"delta,omitempty"`
	// Item carries the full item payload on ItemStarted/ItemCompleted.
	// Structure depends on ItemKind; consumers switch on ItemKind.
	Item json.RawMessage `json:"item,omitempty"`
}

ItemEvent is the wire form of a timeline item transition. It rides on Event as a JSON payload, so existing sinks that don't know about it can skip it without any code change.

type ItemKind added in v0.2.0

type ItemKind string

ItemKind identifies what a timeline item IS (not what happened to it).

const (
	ItemUserMessage  ItemKind = "user_message"
	ItemAgentMessage ItemKind = "agent_message"
	ItemReasoning    ItemKind = "reasoning"
	ItemToolCall     ItemKind = "tool_call"
	ItemApproval     ItemKind = "approval"
	ItemCompaction   ItemKind = "compaction"
	ItemNotice       ItemKind = "notice"
	ItemTurnSummary  ItemKind = "turn_summary"
	ItemPhase        ItemKind = "phase"
)

type ItemPhaseTransition added in v0.2.0

type ItemPhaseTransition string

ItemPhaseTransition identifies which lifecycle phase an ItemEvent carries.

const (
	ItemStarted   ItemPhaseTransition = "item_started"
	ItemDelta     ItemPhaseTransition = "item_delta"
	ItemCompleted ItemPhaseTransition = "item_completed"
)

type Kind

type Kind int

Kind tags an Event. Read the field(s) documented for that kind.

const (
	// TurnStarted marks the start of one top-level Run (one user turn). Sinks
	// reset any per-turn rendering state on it. Carries no payload.
	TurnStarted Kind = iota
	// Reasoning is a thinking-mode reasoning delta (Text). Streamed before the
	// visible answer; sinks typically render it muted under a "thinking" header.
	Reasoning
	// Text is an answer-text delta (Text).
	Text
	// Message marks the assistant turn's text as complete: Text holds the full
	// answer and Reasoning the full chain-of-thought (both already streamed via
	// the deltas above). A sink may use it to re-render the streamed raw text as
	// styled markdown; a plain sink can ignore it.
	Message
	// ToolDispatch announces a tool call is about to run (Tool: ID/Name/Args/ReadOnly).
	ToolDispatch
	// ToolResult reports a finished tool call (Tool: Output/Err/Truncated set).
	ToolResult
	// ToolArgsDelta streams a tool call's raw argument fragment while the model
	// is still generating it (Tool: ID/Name; Text: the fragment) — apply_patch
	// previews render a live diff from these. Transient: not persisted.
	ToolArgsDelta
	// Usage carries per-turn token telemetry (Usage; Pricing optional, for cost).
	Usage
	// Notice is an out-of-band message — a warning, truncation, block, or
	// compaction notice (Level + Text).
	Notice
	// Phase marks a coordinator boundary, e.g. planner→executor handoff (Text =
	// label such as "planning").
	Phase
	// ApprovalRequest asks the frontend to approve a pending tool call
	// (Approval: ID/Tool/Subject). The run blocks until the controller's
	// Approve(ID, …) resolves it; a frontend shows a prompt and answers.
	ApprovalRequest
	// AskRequest asks the frontend to put one or more structured multiple-choice
	// questions to the user (Ask: ID + Questions). The run blocks until the
	// controller's AnswerQuestion(ID, …) resolves it. Powers the `ask` tool.
	AskRequest
	// TurnDone marks the end of one top-level Run (Err non-nil on failure;
	// nil also for a user cancellation, which is not an error). Always the
	// last event of a turn.
	TurnDone
	// CompactionStarted marks the start of a context-compaction pass (Compaction
	// payload: Trigger). A frontend shows a "compacting…" placeholder while the
	// summarizer runs; CompactionDone replaces it. Mirrors ToolDispatch/ToolResult.
	CompactionStarted
	// CompactionDone reports a finished compaction pass (Compaction payload:
	// Trigger/Messages/Summary/Archive). An aborted pass emits this with an empty
	// Summary so the placeholder still resolves. Replaces the older plain Notice
	// so a sink can render a distinct, expandable card.
	CompactionDone
	// ToolProgress streams a chunk of a still-running tool's combined output
	// (Tool: ID + Output = the new chunk). Emitted between ToolDispatch and
	// ToolResult for long tools like bash so a frontend can show live progress.
	// Appended last to keep the Kind values before it wire-stable.
	ToolProgress
	// MCPSurfaceReady fires once per server when its background-loaded surface
	// (prompts or resources) finishes after startup. Lets UIs refresh /mcp
	// status without polling. Text carries "<server>: <surface> ready (<count>
	// items)". Appended last to keep the Kind values before it wire-stable.
	MCPSurfaceReady
	// Retrying fires before each backoff sleep while the provider re-attempts the
	// connection+header phase after a transient failure (RetryAttempt of RetryMax).
	// A frontend shows a transient "retrying (n/m)" indicator that the next stream
	// event — or TurnDone — clears. Appended last to keep the Kind values before
	// it wire-stable.
	Retrying
	// Steer fires when a mid-turn steer message is consumed from the queue and
	// injected as a user message. Text carries the raw steer content (without the
	// wrapper prefix), so a frontend can display it to the user as confirmation.
	// Frontends use Steer to know a queued message has been delivered.
	Steer
	// Paused fires when the agent suspends itself between steps (a pause
	// request from the controller/frontend, e.g. "pause after the current
	// step"). State is preserved; Resumed follows when the run continues.
	// Appended last to keep the Kind values before it wire-stable.
	Paused
	// Resumed fires when a paused agent run continues. Clears a Paused
	// indicator in the frontend. Appended last to keep the Kind values before
	// it wire-stable.
	Resumed
	// ExpertCollab carries a finished expert-team collaboration record so the
	// frontend renders an expandable card (per-round expert answers + the
	// synthesis). Collab payload. Appended last to keep the Kind values before
	// it wire-stable.
	ExpertCollab
	// Item is the synthetic kind for the 4-1 item model (dual-track with
	// the flat kinds above). Item payload carries the structured form.
	Item
)

type Level

type Level int

Level classifies a Notice so sinks can style or filter it.

const (
	LevelInfo Level = iota
	LevelWarn
)

type NoticeItem added in v0.2.0

type NoticeItem struct {
	Level string `json:"level"` // info | warn
	Text  string `json:"text"`
}

NoticeItem is the payload for ItemNotice.

type Profile

type Profile struct {
	Model  string
	Effort string
}

Profile carries the subagent model/effort resolved for this call.

type ReadinessAuditSink

type ReadinessAuditSink interface {
	RecordReadinessAudit(evidence.ReadinessAudit)
}

ReadinessAuditSink is an optional sink capability. Sinks that do not care about readiness audit receipts can implement only Sink and will ignore them.

type Restrictions added in v0.2.0

type Restrictions struct {
	AllowedPaths []string `json:"allowed_paths,omitempty"` // glob patterns
	AllowedHosts []string `json:"allowed_hosts,omitempty"` // host[:port]
}

Restrictions narrows a grant to specific paths or hosts (spec 5-7).

type SessionSnapshot added in v0.2.0

type SessionSnapshot struct {
	SessionID string      `json:"session_id"`
	Revision  int         `json:"revision"`
	Items     []ItemEvent `json:"items"`
	CreatedAt time.Time   `json:"created_at"`
}

SessionSnapshot is the persisted item state at a point in time.

func LoadSnapshot added in v0.2.0

func LoadSnapshot(sessionDir string) *SessionSnapshot

LoadSnapshot reads a snapshot from disk (nil when absent/corrupt).

type Sink

type Sink interface {
	Emit(Event)
}

Sink consumes a turn's events. The agent calls Emit serially from its run loop (tool execution may fan out across goroutines, but emission does not), so an implementation need not be safe for concurrent Emit. Emit must not block indefinitely — a channel-backed sink should be buffered or drained by a live reader.

var Discard Sink = FuncSink(func(Event) {})

Discard is a Sink that drops every event. Useful in tests and for runs that only care about the final session state.

func Sync

func Sync(s Sink) Sink

Sync wraps a Sink so concurrent Emit calls are serialized. The base Sink contract assumes serial emission — the agent's run loop emits one event at a time. Background jobs (internal/jobs) emit from their own goroutines, which can overlap a running turn's emission; wrapping the session sink once in Sync keeps the serial-Emit invariant every sink relies on (an SSE writer, a webview EventsEmit, a TUI channel) without each having to lock. A nil sink yields Discard.

type SnapshotWriter added in v0.2.0

type SnapshotWriter struct {
	// contains filtered or unexported fields
}

SnapshotWriter accumulates ItemEvents and periodically flushes a snapshot.

func NewSnapshotWriter added in v0.2.0

func NewSnapshotWriter(sessionDir, sessionID string) *SnapshotWriter

NewSnapshotWriter creates a writer for one session directory.

func (*SnapshotWriter) Flush added in v0.2.0

func (w *SnapshotWriter) Flush()

Flush writes the current snapshot to disk atomically.

func (*SnapshotWriter) Record added in v0.2.0

func (w *SnapshotWriter) Record(e ItemEvent)

Record appends one ItemEvent and triggers a flush when thresholds hit.

type Tool

type Tool struct {
	ID         string
	Name       string
	Args       string
	Output     string // ToolResult: the result text fed to the model
	Err        string // ToolResult: non-empty when the call failed or was blocked
	ReadOnly   bool
	Truncated  bool  // ToolResult: Output was head+tailed before display/model
	DurationMs int64 // ToolResult: wall-clock execution time in milliseconds
	// Partial marks an early ToolDispatch emitted when a call begins (ID/Name set,
	// Args still streaming) so a frontend can show the card immediately; a second,
	// full ToolDispatch (Partial false, Args set) follows when the call completes.
	Partial bool
	// ParentID, when set, is the ID of the tool call that spawned this one — a
	// sub-agent's calls carry the parent `task` call's ID so a frontend can nest
	// them under it. Empty for top-level calls.
	ParentID string
	// Attachments carries files the tool generated (e.g. image_generate pictures
	// saved under .fairpeer/attachments/), parsed from the result text so the
	// frontend can display them regardless of what the model writes back.
	Attachments []Attachment
	FileDiff
	Profile *Profile // ToolDispatch: subagent model/effort (set for task/skill calls)
}

Tool describes a tool call for ToolDispatch / ToolResult events. On dispatch only ID/Name/Args/ReadOnly are set; on result Output/Err/Truncated are filled in. Args is the raw JSON arguments — a sink compacts it for display.

type ToolCallItem added in v0.2.0

type ToolCallItem struct {
	Name       string    `json:"name"`
	Args       string    `json:"args,omitempty"`
	Output     string    `json:"output,omitempty"`
	Err        string    `json:"err,omitempty"`
	FileDiff   *FileDiff `json:"file_diff,omitempty"`
	ReadOnly   bool      `json:"read_only"`
	DurationMs int64     `json:"duration_ms,omitempty"`
	Status     string    `json:"status"` // running | done | error | stopped
}

ToolCallItem is the payload for ItemToolCall.

Jump to

Keyboard shortcuts

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