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 ¶
- type ApprovalNeededPayload
- type BubbleUp
- type CompactingEndPayload
- type CompactingPayload
- type ErrorPayload
- type Event
- type IterLimitPayload
- type Kind
- type ModeChangedPayload
- type Multi
- type QuestionItem
- type QuestionNeededPayload
- type QuestionOption
- type RunEndPayload
- type RunResumePayload
- type RunStartPayload
- type Sink
- type SinkFunc
- type StoreUpdatePayload
- type TextPayload
- type ToolUseResultPayload
- type ToolUseStartPayload
- type TurnPayload
- type UsagePayload
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ApprovalNeededPayload ¶
type ApprovalNeededPayload struct {
// RequestID is the Broker correlation key; the TUI passes it back to
// Broker.Respond when forwarding the user's choice.
RequestID string
// ToolName is the wire name of the tool whose call is being gated.
ToolName string
// ToolInput is the raw JSON the LLM passed to the tool.
ToolInput json.RawMessage
// InputDescription is the model-supplied `description` field from
// ToolInput; "" when the tool's input has no such field.
InputDescription string
// Mode is the permission mode active when the gate fired.
Mode string
// Reason is the gate's explanation for asking (e.g. "matches dangerous prefix").
Reason string
// RiskHint is non-empty for Bash (the classifier's risk label);
// empty for other tools.
RiskHint string
// Matched is the rule fragment that triggered the prompt, if any.
Matched string
// PlanContent is non-empty only for ExitPlanMode — carries the
// markdown plan body so the approval overlay can render it inline.
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 ¶
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.
type CompactingEndPayload ¶
type CompactingEndPayload struct {
// Type matches the prior CompactingPayload.Type — "micro" or "full".
Type string
// OK reports success or failure; false means the TUI should swap
// the spinner for an error line rather than removing it silently.
OK bool
// BriefTokens is the size of the full-compact brief; updated UIs use
// this to replace the percent estimate they painted at start.
BriefTokens int
// Err is the failure reason when OK is false; empty otherwise.
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 is "micro" (elide old tool results) or "full" (summarise).
Type string
// UsageRatio is the input/budget ratio that triggered the compaction.
UsageRatio float64
}
CompactingPayload reports the start of a session compaction. Type is "micro" or "full"; UsageRatio is the input/output token ratio that triggered the compaction.
type ErrorPayload ¶
type ErrorPayload struct {
// Stage tags where in the loop the failure occurred:
// "llm" | "tool:<name>" | "loop".
Stage string
// Err is the underlying Go error. Use Message for the rendered
// string form when you only need text (covers the nil case too).
Err error
// Message is err.Error() captured at emit time, or "" when Err is nil.
// Convenience for consumers that don't want to nil-check + stringify
// (UIs, JSON serialisers).
Message string
}
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).
func (Event) Payload ¶
Payload returns the payload pointer matching e.Kind, or nil when the event Kind has no associated payload (KindIdle, KindRunCancelled, KindTurnStart/End in some emitters, etc.). Consumers can switch on the returned type instead of remembering which of the 20+ pointer fields on Event corresponds to each Kind:
switch p := e.Payload().(type) {
case *event.TextPayload:
render(p.Text)
case *event.ToolUseStartPayload:
renderToolCall(p.Name, p.Input)
}
The direct field access (e.Text, e.ToolUseStart, …) stays available for callers that already do a Kind switch — Payload is purely an ergonomics layer.
Example ¶
ExampleEvent_Payload demonstrates the Phase 19a Payload() helper — type-switch on the returned pointer instead of remembering which Event.* field goes with each Kind.
package main
import (
"fmt"
"github.com/johnny1110/evva/pkg/event"
)
func main() {
events := []event.Event{
{Kind: event.KindText, Text: &event.TextPayload{Text: "done"}},
{Kind: event.KindToolUseStart, ToolUseStart: &event.ToolUseStartPayload{Name: "read"}},
{Kind: event.KindRunEnd, RunEnd: &event.RunEndPayload{Iters: 3}},
}
for _, e := range events {
switch p := e.Payload().(type) {
case *event.TextPayload:
fmt.Println("text:", p.Text)
case *event.ToolUseStartPayload:
fmt.Println("tool:", p.Name)
case *event.RunEndPayload:
fmt.Println("done in", p.Iters, "iters")
}
}
}
Output: text: done tool: read done in 3 iters
type IterLimitPayload ¶
type IterLimitPayload struct {
// Iters is the iteration count the loop hit before pausing — matches
// RunEndPayload.Iters naming so callers can read iteration counts
// from either payload without remembering two field names.
Iters 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 ( // KindIdle marks the agent as inactive — no Run in flight. Useful for // status-bar widgets that want a "ready" indicator distinct from "running". KindIdle Kind = "idle" // KindRunStart fires once at the top of every Agent.Run invocation; // payload carries the user prompt that kicked off the run. KindRunStart Kind = "run_start" // KindRunResume fires when Agent.Continue resumes after an iter-limit // pause; payload carries the message index the resume picks up from. KindRunResume Kind = "run_resume" // KindRunEnd fires once per Run — terminal, win or lose; payload // carries the final iteration count and assistant text/thinking. KindRunEnd Kind = "run_end" // KindRunCancelled fires when context cancellation tore the run down // mid-flight (Ctrl-C, deadline, etc.). No payload. KindRunCancelled Kind = "run_cancelled" // KindIterLimit fires when the loop hits Agent.maxIters and pauses // without finishing. The caller may invoke Agent.Continue to resume. KindIterLimit Kind = "iter_limit" // KindTurnStart / KindTurnEnd bracket one iteration of the loop. Payload // carries the iteration index so subscribers can scope sub-events to a turn. KindTurnStart Kind = "turn_start" KindTurnEnd Kind = "turn_end" // KindDrainingInfo signals the agent is folding deferred information // from subagents or background bash into the parent context. Cosmetic // — useful for a status bar "draining…" hint. KindDrainingInfo = "draining_info" // KindThinking carries the assistant's reasoning text as one full // block (buffered providers). Streaming providers emit KindThinkingChunk // deltas instead, then skip the final KindThinking to avoid duplication. KindThinking Kind = "thinking" // KindText carries the assistant's final text as one full block // (buffered providers). Streaming providers emit KindTextChunk deltas // instead, then skip the final KindText. KindText Kind = "text" // 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 fires at tool-dispatch time, carrying the tool name // + raw JSON input. The matching KindToolUseResult follows when the // tool returns. KindToolUseStart Kind = "tool_use_start" // KindToolUseResult fires when a tool returns. Pairs with the prior // KindToolUseStart by ToolID. 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 fires when the agent starts a session compaction // (micro or full). Pairs with KindCompactingEnd. KindCompacting Kind = "compacting" // KindCompactingEnd pairs with KindCompacting; OK reports success or // failure so the TUI can swap the spinner for the right final block. KindCompactingEnd Kind = "compacting_end" // KindError reports a Go-level failure that aborted the loop. Tool // errors surfaced via Result.IsError do NOT produce this event — // they flow through KindToolUseResult so the model can recover. 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 reports per-turn token usage plus the running session // total after the turn is folded in. KindUsage Kind = "usage" // 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 is the mode that was active before the change. Empty on the
// very first initialization.
PrevMode string
// Mode is the new mode the agent has transitioned into.
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.
Example ¶
ExampleMulti shows how to fan one event to several sinks — useful for wiring a TUI plus a structured log at the same time.
package main
import (
"fmt"
"github.com/johnny1110/evva/pkg/event"
)
func main() {
var tuiTexts, logTexts []string
tui := event.SinkFunc(func(e event.Event) {
if e.Text != nil {
tuiTexts = append(tuiTexts, e.Text.Text)
}
})
logger := event.SinkFunc(func(e event.Event) {
if e.Text != nil {
logTexts = append(logTexts, e.Text.Text)
}
})
fanout := event.Multi{Sinks: []event.Sink{tui, logger}}
fanout.Emit(event.Event{Kind: event.KindText, Text: &event.TextPayload{Text: "broadcast"}})
fmt.Println("tui:", tuiTexts)
fmt.Println("log:", logTexts)
}
Output: tui: [broadcast] log: [broadcast]
type QuestionItem ¶
type QuestionItem struct {
// Question is the prompt body.
Question string
// Header is the short chip label (max 12 chars in the canonical UI).
Header string
// MultiSelect controls whether the user may pick more than one option.
MultiSelect bool
// Options are the offered choices.
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 is the question.Broker correlation key; the TUI passes
// it back via Controller.RespondQuestion.
RequestID string
// AgentID is the agent that invoked AskUserQuestion (relevant when
// subagent question routing lands).
AgentID string
// Questions are the items rendered in the overlay.
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 is the choice text shown to the user.
Label string
// Description is the optional explanation rendered alongside.
Description string
// Preview is the optional code/diagram block shown in side-by-side mode.
Preview string
}
QuestionOption mirrors question.Option for the same reason.
type RunEndPayload ¶
type RunEndPayload struct {
// Iters is the number of iterations the loop consumed before ending.
Iters int
// Content is the assistant's final text for the Run.
Content string
// Thinking is the assistant's reasoning text (if any).
Thinking string
}
RunEndPayload carries the final state of a completed Run.
type RunResumePayload ¶
type RunResumePayload struct {
// FromMessageIndex is the position in the session transcript where
// Continue picked up.
FromMessageIndex int
}
RunResumePayload carries the message index Agent.Continue resumed from after an iter-limit pause.
type RunStartPayload ¶
type RunStartPayload struct {
// Prompt is the user message that opened this Run.
Prompt string
}
RunStartPayload carries the user prompt that kicked off a Run.
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).
Example ¶
ExampleSinkFunc shows the function-shaped sink pattern. The smallest possible implementation of event.Sink — wrap any func(Event) and pass it to agent.WithSink.
package main
import (
"fmt"
"github.com/johnny1110/evva/pkg/event"
)
func main() {
sink := event.SinkFunc(func(e event.Event) {
if e.Kind == event.KindText && e.Text != nil {
fmt.Println("assistant:", e.Text.Text)
}
})
sink.Emit(event.Event{Kind: event.KindText, Text: &event.TextPayload{Text: "hi"}})
}
Output: assistant: hi
type StoreUpdatePayload ¶
type StoreUpdatePayload struct {
// Domain identifies the emitting store ("task", "subagent", …).
Domain string
// Op is the verb: "created" / "updated" / "removed" / "phase" / "done" / "crushed".
Op string
// ID is the store-local identifier (task ID, subagent ID, …).
ID string
// Payload is the store's domain-typed snapshot; consumers type-assert
// based on Domain.
Payload any
// Time is the emit timestamp.
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 is the assistant text content (a full block, or one streaming
// delta when the event Kind is KindTextChunk / KindThinkingChunk).
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 correlates this result with the prior ToolUseStart.
ToolID string
// Content is the LLM-facing text summary the tool produced.
Content string
// IsError is true when the tool itself returned an error result.
// Distinct from a Go-level failure (which surfaces via KindError).
IsError bool
// Metadata is an optional tool-specific structured payload (e.g. a
// *fs.FileDiff for write/edit). UIs type-assert to render rich views.
Metadata any
// ContentBlocks carries multimodal output (text + images). Empty for
// text-only tool results.
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 is the tool's wire name (matches tools.ToolName).
Name string
// Input is the raw JSON the LLM passed to the tool — UIs typically
// summarise one field rather than dumping the whole blob.
Input json.RawMessage
// ToolID correlates this dispatch with its eventual ToolUseResult.
ToolID string
}
ToolUseStartPayload reports a tool dispatch.
type TurnPayload ¶
type TurnPayload struct {
// Iteration is the zero-based loop iteration index.
Iteration int
}
TurnPayload carries the iteration index a TurnStart/TurnEnd event refers to.
type UsagePayload ¶
type UsagePayload struct {
// Turn is usage for the just-completed LLM call.
Turn llm.Usage
// Cumulative is the running session total after Turn is folded in.
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.