Documentation
¶
Overview ¶
Event Bus — the shared output spine.
Historically every part of the agent wrote straight to stdout with fmt.Println. That works for a REPL but makes alternate front-ends (a TUI, a web dashboard) impossible without duplicating output plumbing. The Bus inverts it: code EMITS typed events, and any number of SUBSCRIBERS render them however they like. stdout is just the first subscriber; the TUI and dashboard become additional ones, reading the same stream.
Migration is incremental and safe. This step introduces the Bus and makes stdout a subscriber, but does NOT rewrite the 250+ existing fmt.Println sites — those keep printing directly for now. New structured emits (tokens, tool calls, stats) go through the Bus; the plain-text sites get migrated file-by-file in later steps. During the transition the terminal output is identical because the stdout subscriber mirrors what the direct prints already do. Nothing breaks; the Bus simply becomes available.
Index ¶
- Constants
- Variables
- func ANSIForRole(r Role) string
- func ConfinePath(base, p string) (string, error)
- func Dedup(ss []string) []string
- func DrainBrowserSubmission() string
- func EmitAssistant(text string)
- func EmitBusy(working bool)
- func EmitClear(key string)
- func EmitDiff(text string)
- func EmitError(text string)
- func EmitLine(text string)
- func EmitLineC(color, text string)
- func EmitOverwrite(key, text, color string)
- func EmitStats(text string, meta map[string]string)
- func EmitStatus(text string)
- func EmitThinking(text string)
- func EmitToken(text string)
- func EmitToolCall(tool, argsPreview string)
- func EmitToolCallInline(tool, argsPreview string)
- func EmitToolDone(tool, summary string)
- func EmitUser(text string)
- func LogSafe(s string) string
- func NewBus() *eventBus
- func ParseTestFailures(out string) string
- func RelPaths(root string, abs []string) []string
- func TermWidth() int
- func Tint(code, s string) string
- func VetCommand(cmdStr string) error
- type Event
- type EventKind
- type EventStyle
- type Message
- type Role
- type Subscriber
- type SubscriberFunc
- type ToolCall
Constants ¶
const ( ColorDim = "2" ColorRed = "31" ColorGreen = "32" ColorYellow = "33" ColorCyan = "36" )
ANSI SGR codes used across the UI.
Variables ¶
var BrowserSubmissions = make(chan string, 16)
BrowserSubmissions carries prompts submitted from the web dashboard into the agent loop. It lives in core so the web package (producer) and the engine / tui (consumers) can share it without importing each other — the same neutral role the event bus plays.
var Bus = &eventBus{}
Bus is the process-wide event Bus.
var EmbedModel string
EmbedModel is the embedding model id (config "embed_model"); "" = disabled. Set by the engine from config, read by the code_search tool for semantic (meaning-based) code search over the working directory.
var UseColor = func() bool { if os.Getenv("NO_COLOR") != "" { return false } fi, err := os.Stdout.Stat() return err == nil && fi.Mode()&os.ModeCharDevice != 0 }()
UseColor reports whether ANSI styling should be emitted: only when stdout is a real terminal and NO_COLOR is unset. Computed once at startup.
Functions ¶
func ANSIForRole ¶ added in v0.2.0
ANSIForRole maps a semantic Role to the terminal color the CLI renders it in. This is the ANSI half of the palette; the TUI keeps a parallel lipgloss map and the dashboard a parallel CSS class set, but all three key off the SAME Role, so a tool trace is "dim" everywhere and an error is "error" everywhere — color consistency by construction, not coincidence. RolePlain and RoleStatus return "" (terminal default), matching the CLI's prior look.
func ConfinePath ¶
ConfinePath resolves p (relative to base if not absolute) and verifies the result stays within base. It returns the cleaned absolute path, or an error if the path escapes base via "..", symlink-style tricks, or an absolute path pointing elsewhere. Use this before any file operation whose path derives from user- or model-supplied input.
func DrainBrowserSubmission ¶
func DrainBrowserSubmission() string
DrainBrowserSubmission returns a queued browser prompt if one is waiting, or "" immediately if none. Non-blocking.
func EmitAssistant ¶
func EmitAssistant(text string)
func EmitClear ¶ added in v0.4.0
func EmitClear(key string)
EmitClear removes the in-place line for key (e.g. a finished spinner) — subscribers that rendered it erase the line entirely rather than leaving stale text behind.
func EmitDiff ¶
func EmitDiff(text string)
EmitDiff emits pre-formatted, already-styled output (diffs, edit traces) that must NOT be re-rendered as markdown or word-wrapped. The TUI prints it verbatim in-viewport instead of letting raw fmt.Print corrupt the bubbletea alt-screen.
func EmitOverwrite ¶ added in v0.4.0
func EmitOverwrite(key, text, color string)
EmitOverwrite emits (or updates) an in-place line identified by key — repeated calls with the same key replace that line rather than adding a new one. Use for spinners, progress counters, and other status text that changes rapidly and shouldn't scroll the transcript. color is an optional tint hint (see Event.Color), empty for none.
func EmitStatus ¶
func EmitStatus(text string)
func EmitThinking ¶
func EmitThinking(text string)
func EmitToolCall ¶
func EmitToolCall(tool, argsPreview string)
func EmitToolCallInline ¶
func EmitToolCallInline(tool, argsPreview string)
func EmitToolDone ¶
func EmitToolDone(tool, summary string)
func LogSafe ¶ added in v0.4.0
LogSafe neutralizes a user-controlled value before it is written to a log or terminal line. It strips the characters that let a caller forge or corrupt log output — CR and LF (which fabricate new log lines) and other C0 control characters and DEL (which can move the cursor, inject ANSI escapes, or hide text). Ordinary printable text, including Unicode, is passed through unchanged.
Use this on any value that originates from user input (a typed path, a slash-command argument, a query) at the point it is interpolated into a log/print call. It addresses CWE-117 (log injection); it is NOT a shell or path sanitizer — see VetCommand and ConfinePath for those.
Implementation note: CodeQL's go/log-injection query has a built-in sanitizer pattern (ReplaceSanitizer) that recognizes an expression equivalent to strings.ReplaceAll(s, "\r", ...) or strings.ReplaceAll(s, "\n", ...) as neutralizing a value. It does NOT recognize strings.Map, and Go is not yet in the set of languages CodeQL model packs support (so the custom barrier model in .github/codeql/extensions/ is inert for this query too). Routing CR/LF removal through strings.ReplaceAll — after strings.Map strips the other C0 controls and DEL — lets CodeQL confirm every call site is sanitized without relying on either mechanism.
func NewBus ¶
func NewBus() *eventBus
NewBus returns a fresh, empty event bus. Tests use it to exercise subscribe/emit in isolation from the process-global Bus.
func ParseTestFailures ¶
parseTestFailures distills `go test` (or build) output to the actionable parts: which tests failed and the file:line + message of each assertion. Returns "" when nothing failed (caller keeps the raw output for context).
func TermWidth ¶
func TermWidth() int
TermWidth returns the terminal's column width, trying stdout/stderr/stdin, then $COLUMNS, then a conservative default of 80.
func Tint ¶
Tint wraps s in the given ANSI SGR code (e.g. ColorRed), or returns s unchanged when color is disabled.
func VetCommand ¶
VetCommand returns a non-nil error if cmdStr matches a catastrophic, irreversible-damage pattern that must be blocked regardless of approval. It intentionally does NOT try to be a general command sanitizer — running arbitrary shell commands is the tool's purpose, and the approval gate is the primary control. This is only a last-resort guard against a handful of system-destroying mistakes.
Types ¶
type Event ¶
type Event struct {
Kind EventKind
Text string
// Optional structured payload for richer subscribers (TUI/dashboard).
// The stdout subscriber mostly uses Text.
Tool string // tool name (EvToolCall/EvToolDone)
Meta map[string]string // arbitrary extras (stats fields, args preview)
Color string // suggested color hint for the line (maps to term colors)
Time time.Time
// Key identifies the in-place line an EvOverwrite event updates or
// clears (see EvOverwrite). Unused by other kinds.
Key string
}
Event is one thing that happened, timestamped. Fields beyond Kind/Text are optional and kind-specific; subscribers read what they need.
type EventKind ¶
type EventKind string
EventKind classifies a Bus event so subscribers can render selectively.
const ( EvLine EventKind = "line" // a plain status/trace line (the fmt.Println replacement) EvToken EventKind = "token" // a streamed assistant content token EvThinking EventKind = "thinking" // reasoning progress (token count / label) EvToolCall EventKind = "tool_call" // a tool invocation (name + short args) EvToolDone EventKind = "tool_done" // a tool result summary EvUser EventKind = "user" // the user's submitted message EvAssistant EventKind = "assistant" // a completed assistant message EvStats EventKind = "stats" // a stats/budget snapshot update EvStatus EventKind = "status" // connection/mode status (mcp connected, model, etc.) EvError EventKind = "error" // an error line EvBusy EventKind = "busy" // turn lifecycle: Text is "1" (working) or "0" (idle) EvApproval EventKind = "approval" // a tool/command awaits user approval (web mode) // EvOverwrite is an in-place update to a line identified by Event.Key, // rather than a new discrete line — the spinner, a progress counter, or // any other "this line keeps changing" status. A subscriber that // understands Key re-renders that ONE line in place (terminal: erase + // reprint the current line; TUI/dashboard: update the existing widget // instead of appending a new one). Meta["clear"]=="1" means "remove the // line for this Key entirely" (e.g. the spinner finished). A subscriber // that doesn't implement in-place rendering may safely treat this like // EvLine and append it — degraded (a scrolling trail instead of one // updating line) but never broken, and a clear with no matching prior // line is simply a no-op. EvOverwrite EventKind = "overwrite" )
type EventStyle ¶ added in v0.2.0
type EventStyle struct {
Glyph string // leading glyph, e.g. "⚙" — empty for content that shouldn't be decorated
Role Role
Indent int // leading spaces before the glyph
}
EventStyle is the canonical presentation for an event kind: the glyph that prefixes it, its semantic role, and how deeply it's indented (in spaces at the CLI; renderers may map indent to their own layout).
func StyleFor ¶ added in v0.2.0
func StyleFor(k EventKind) EventStyle
StyleFor returns the canonical presentation for a kind. Unknown kinds get a plain default, so a newly added kind renders sanely everywhere until it's given an explicit entry.
func (EventStyle) Pad ¶ added in v0.2.0
func (s EventStyle) Pad() string
Pad returns the leading indentation as spaces.
type Message ¶
type Message struct {
Role string `json:"role"` // system | user | assistant | tool
Content string `json:"content"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"` // set on role=tool replies
}
Message is one turn in an OpenAI-compatible chat conversation. It is the central data type passed between the model client, session store, and UI.
type Role ¶ added in v0.2.0
type Role string
Role is the semantic color/emphasis category for an event, independent of the concrete palette a given renderer uses (ANSI code, lipgloss style, or CSS class). Each renderer maps Role → its own styling once, centrally.
const ( RolePlain Role = "plain" // default text RoleDim Role = "dim" // de-emphasized (tool traces, stats) RoleError Role = "error" // errors RoleStatus Role = "status" // connection/mode status RoleAssistant Role = "assistant" // assistant content RoleUser Role = "user" // the user's own message RoleAccent Role = "accent" // highlighted (thinking, approvals) )
type Subscriber ¶
type Subscriber interface {
OnEvent(Event)
}
Subscriber receives every event. Implementations must be non-blocking or buffer internally — the emit path holds a lock briefly and must not stall.
type SubscriberFunc ¶
type SubscriberFunc func(Event)
SubscriberFunc adapts a plain function to Subscriber.
func (SubscriberFunc) OnEvent ¶
func (f SubscriberFunc) OnEvent(e Event)
type ToolCall ¶
type ToolCall struct {
ID string `json:"id"`
Type string `json:"type"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
}
ToolCall is a single tool invocation requested by the model. Arguments is a JSON *string* (OpenAI-compatible), unlike Ollama's pre-parsed map.