event

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: May 21, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package event defines the event stream the agent emits while running.

The Event envelope is a discriminated union — every event has a Kind and exactly one non-nil typed payload field. This keeps consumer code type-safe (no interface{} assertions, no reflection) while still allowing one Sink to receive every kind of event the agent might emit.

State-change events from backing stores (task list, subagent panel, future notes/todos/...) all flow through a single KindStoreUpdate so adding a new panel never requires a new event kind. The store's domain identifier (see internal/observable.Change.Domain) selects how the consumer renders the row.

Sinks (see sink.go) are the consumer side. A TUI, a structured logger, and a JSON-over-websocket bridge can each implement Sink and subscribe independently of one another — the agent doesn't know about them.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ApprovalNeededPayload

type ApprovalNeededPayload struct {
	RequestID   string
	ToolName    string
	ToolInput   json.RawMessage
	Mode        string
	Reason      string
	RiskHint    string
	Matched     string
	PlanContent string
}

ApprovalNeededPayload is the wire shape of a pending permission prompt. The TUI receives one of these per blocked tool call. Carries every piece of context the user needs to decide: the tool name, the raw input (UI summarises), the active mode, and the gate's reason for asking.

RequestID is the Broker's correlation key — the TUI uses it when calling Broker.Respond. RiskHint is non-empty for Bash; other tools see "". PlanContent is non-empty only for ExitPlanMode — carries the markdown plan body so the approval overlay can render it inline.

type BubbleUp

type BubbleUp struct {
	Parent   Sink
	ParentID string
}

BubbleUp wraps a parent's Sink so a subagent's events appear in the parent's stream with ParentID set to the parent's AgentID. The TUI uses this tagging to route nested events into a subagent panel.

BubbleUp does NOT change the subagent's own AgentID — only ParentID. The hierarchy is always exactly two layers (subagents cannot spawn subagents), so a single rewrite at the boundary is enough.

func (BubbleUp) Emit

func (b BubbleUp) Emit(e Event)

Emit rewrites the event's ParentID and forwards.

type CompactingEndPayload

type CompactingEndPayload struct {
	Type        string
	OK          bool
	BriefTokens int
	Err         string
}

CompactingEndPayload reports the outcome of a compaction the TUI was painting. OK=false marks the failure path so the transcript can swap the spinner block for a short error line instead of just removing it. BriefTokens carries the size of the full-compact brief so callers that already painted a percent can update the figure on completion.

type CompactingPayload

type CompactingPayload struct {
	Type       string
	UsageRatio float64
}

type ErrorPayload

type ErrorPayload struct {
	Stage string // "llm" | "tool:<name>" | "loop"
	Err   error
}

ErrorPayload reports a Go-level failure that aborted the loop. Tool errors surfaced as Result.IsError do NOT produce this event — they flow through ToolUseResult so the model can recover.

type Event

type Event struct {
	Kind     Kind
	AgentID  string
	ParentID string
	Time     time.Time

	RunStart       *RunStartPayload       `json:",omitempty"`
	RunResume      *RunResumePayload      `json:",omitempty"`
	RunEnd         *RunEndPayload         `json:",omitempty"`
	IterLimit      *IterLimitPayload      `json:",omitempty"`
	Turn           *TurnPayload           `json:",omitempty"`
	Thinking       *TextPayload           `json:",omitempty"`
	Text           *TextPayload           `json:",omitempty"`
	ToolUseStart   *ToolUseStartPayload   `json:",omitempty"`
	ToolUseResult  *ToolUseResultPayload  `json:",omitempty"`
	ApprovalNeeded *ApprovalNeededPayload `json:",omitempty"`
	QuestionNeeded *QuestionNeededPayload `json:",omitempty"`
	Error          *ErrorPayload          `json:",omitempty"`
	StoreUpdate    *StoreUpdatePayload    `json:",omitempty"`
	Usage          *UsagePayload          `json:",omitempty"`
	Compacting     *CompactingPayload     `json:",omitempty"`
	CompactingEnd  *CompactingEndPayload  `json:",omitempty"`
	ModeChanged    *ModeChangedPayload    `json:",omitempty"`
}

Event is the envelope. Exactly one of the *Payload fields is non-nil per event, matched to Kind. Consumers should switch on Kind and read the corresponding field directly — type-safe access, no reflection.

AgentID identifies the emitter. ParentID is empty for the root agent and equal to the root's AgentID for subagent events (the hierarchy is always exactly two layers — subagents cannot spawn subagents).

type IterLimitPayload

type IterLimitPayload struct {
	Reached int
}

IterLimitPayload is emitted when the loop hits Agent.maxIters. The UI should prompt the user (e.g. "press Enter to keep going") and call Agent.Continue to resume; the loop is paused, not failed.

type Kind

type Kind string

Kind tags every event. New kinds are added by extending this list and the matching payload field on Event.

const (
	KindRunStart     Kind = "run_start"
	KindRunResume    Kind = "run_resume"
	KindRunEnd       Kind = "run_end"
	KindRunCancelled Kind = "run_cancelled"
	KindIterLimit    Kind = "iter_limit" // paused — caller may Continue

	KindTurnStart Kind = "turn_start"
	KindTurnEnd   Kind = "turn_end"

	KindDrainingInfo      = "draining_info" // agent is draining info from subagent or bg bash
	KindThinking     Kind = "thinking"      // assistant reasoning text (whole block; buffered providers)
	KindText         Kind = "text"          // assistant final text (whole block; buffered providers)

	// KindTextChunk and KindThinkingChunk are emitted by the streaming
	// path. Each carries an incremental delta in TextPayload.Text; the
	// UI accumulates consecutive chunks of the same kind into one logical
	// block. Reset on KindTurnEnd. Streaming agents emit chunks only —
	// the final KindText / KindThinking is skipped to avoid duplication.
	KindTextChunk     Kind = "text_chunk"
	KindThinkingChunk Kind = "thinking_chunk"

	KindToolUseStart  Kind = "tool_use_start"
	KindToolUseResult Kind = "tool_use_result"

	// KindApprovalNeeded is emitted when the permission gate decides a tool
	// call must be approved by the user. The TUI subscribes, opens an
	// approval overlay, and calls Broker.Respond with the user's decision.
	// The blocked tool goroutine sleeps in Broker.Request until the answer
	// arrives (or the context is cancelled).
	KindApprovalNeeded Kind = "approval_needed"

	// KindQuestionNeeded is emitted when the AskUserQuestion tool is invoked.
	// The TUI subscribes, opens a question overlay, and calls
	// Controller.RespondQuestion with the user's answers. The blocked tool
	// goroutine sleeps in question.Broker.Request until the answer arrives.
	KindQuestionNeeded Kind = "question_needed"

	KindCompacting    Kind = "compacting"
	KindCompactingEnd Kind = "compacting_end" // pair to KindCompacting; TUI removes the inflight block

	KindError Kind = "error"

	// KindStoreUpdate carries every state change emitted by an
	// observable.Store registered on the agent's ToolState. The consumer
	// switches on StoreUpdatePayload.Domain to decide how to render.
	KindStoreUpdate Kind = "store_update"

	KindUsage Kind = "usage" // per-turn token usage report

	// KindModeChanged fires whenever the agent's permission mode changes
	// — Shift+Tab cycle, EnterPlanMode / ExitPlanMode tool calls, or a
	// SwitchProfile that resets the mode. Lets the TUI sync the status-
	// bar indicator without having to poll Agent.PermissionMode each
	// render. Emitted only by the root agent; subagent mode changes
	// stay internal.
	KindModeChanged Kind = "mode_changed"
)

type ModeChangedPayload

type ModeChangedPayload struct {
	PrevMode string
	Mode     string
}

ModeChangedPayload reports a permission-mode transition. PrevMode is the mode that was active before the change (empty on the very first initialization); Mode is the new mode. Both are the wire string form (permission.Mode is type-aliased to string for the same reason).

type Multi

type Multi struct {
	Sinks []Sink
}

Multi fans out one event to many sinks in declared order. A slow sink blocks subsequent ones (and the agent loop). This is intentional — backpressure beats event loss.

func (Multi) Emit

func (m Multi) Emit(e Event)

Emit forwards to every contained sink, skipping nil entries.

type QuestionItem

type QuestionItem struct {
	Question    string
	Header      string
	MultiSelect bool
	Options     []QuestionOption
}

QuestionItem mirrors question.Question for the event layer so event.go does not import internal/question (which would create a cycle through toolset → tools/ux → question → event).

type QuestionNeededPayload

type QuestionNeededPayload struct {
	RequestID string
	AgentID   string
	Questions []QuestionItem
}

QuestionNeededPayload is the wire shape of a pending question prompt. The TUI receives one of these when AskUserQuestion is invoked. RequestID is the question.Broker's correlation key used when calling Controller.RespondQuestion.

type QuestionOption

type QuestionOption struct {
	Label       string
	Description string
	Preview     string
}

QuestionOption mirrors question.Option for the same reason.

type RunEndPayload

type RunEndPayload struct {
	Iters    int
	Content  string
	Thinking string
}

type RunResumePayload

type RunResumePayload struct {
	FromMessageIndex int
}

type RunStartPayload

type RunStartPayload struct {
	Prompt string
}

type Sink

type Sink interface {
	Emit(Event)
}

Sink consumes events emitted by an agent's run loop.

Concurrency: an agent serializes calls into Sink.Emit internally — even when tools dispatch in parallel, individual Emit calls are mutex-guarded so events from one agent arrive one at a time. Sinks shared across multiple agents (e.g. a global logger, or a parent sink reached through BubbleUp) must still handle concurrent Emit calls themselves.

Emit should be fast — slow sinks block the agent loop. Sinks needing network or disk I/O should buffer internally (channel, ring buffer) and process asynchronously.

var Discard Sink = discard{}

Discard is the no-op sink. Use as the default for tests / silent CLI runs where the caller doesn't subscribe to events.

type SinkFunc

type SinkFunc func(Event)

SinkFunc adapts an ordinary function to the Sink interface — convenient for one-off consumers (tests, quick CLI prints).

func (SinkFunc) Emit

func (f SinkFunc) Emit(e Event)

Emit calls the wrapped function.

type StoreUpdatePayload

type StoreUpdatePayload struct {
	Domain  string
	Op      string
	ID      string
	Payload any
	Time    time.Time
}

StoreUpdatePayload is the bridge between observable.Change and the event stream. Domain names the emitting store ("task", "subagent", ...); Op is the verb ("created" / "updated" / "removed" / "phase" / "done" / "crushed"); Payload is the store's domain-typed snapshot, switched on by Domain at the consumer.

type TextPayload

type TextPayload struct {
	Text string
}

TextPayload carries an opaque text chunk — used for both Thinking and Text events. With streaming completions this becomes a stream of chunks; today it carries the full block.

type ToolUseResultPayload

type ToolUseResultPayload struct {
	ToolID        string
	Content       string
	IsError       bool
	Metadata      any
	ContentBlocks []tools.ContentBlock
}

ToolUseResultPayload reports the outcome of a single tool call.

Metadata is an optional tool-specific structured payload (e.g. a *fs.FileDiff for write_file / edit_file). Carried opaquely through this layer; the UI type-asserts. Never sent to the LLM — Content alone is the model-facing summary.

type ToolUseStartPayload

type ToolUseStartPayload struct {
	Name   string
	Input  json.RawMessage
	ToolID string
}

type TurnPayload

type TurnPayload struct {
	Iteration int
}

type UsagePayload

type UsagePayload struct {
	Turn       llm.Usage
	Cumulative llm.Usage
}

UsagePayload reports token usage for the LLM call that just completed. Turn is the just-completed call; Cumulative is the running session total after Turn has been folded in. The TUI typically shows both — Turn for the latest cost spike, Cumulative for the session budget.

Jump to

Keyboard shortcuts

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