core

package
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Sep 11, 2026 License: MIT Imports: 10 Imported by: 0

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

View Source
const (
	ColorDim    = "2"
	ColorRed    = "31"
	ColorGreen  = "32"
	ColorYellow = "33"
	ColorCyan   = "36"
)

ANSI SGR codes used across the UI.

Variables

View Source
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.

View Source
var Bus = &eventBus{}

Bus is the process-wide event Bus.

View Source
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.

View Source
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

func ANSIForRole(r Role) string

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

func ConfinePath(base, p string) (string, error)

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 Dedup

func Dedup(ss []string) []string

Dedup returns ss with duplicate strings removed, preserving first-seen order.

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 EmitBusy

func EmitBusy(working bool)

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 EmitError

func EmitError(text string)

func EmitLine

func EmitLine(text string)

func EmitLineC

func EmitLineC(color, text string)

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 EmitStats

func EmitStats(text string, meta map[string]string)

func EmitStatus

func EmitStatus(text string)

func EmitThinking

func EmitThinking(text string)

func EmitToken

func EmitToken(text string)

func EmitToolCall

func EmitToolCall(tool, argsPreview string)

func EmitToolCallInline

func EmitToolCallInline(tool, argsPreview string)

func EmitToolDone

func EmitToolDone(tool, summary string)

func EmitUser

func EmitUser(text string)

func LogSafe added in v0.4.0

func LogSafe(s string) string

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

func ParseTestFailures(out string) string

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 RelPaths

func RelPaths(root string, abs []string) []string

RelPaths converts absolute paths to paths relative to root (best-effort).

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

func Tint(code, s string) string

Tint wraps s in the given ANSI SGR code (e.g. ColorRed), or returns s unchanged when color is disabled.

func VetCommand

func VetCommand(cmdStr string) error

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.

Jump to

Keyboard shortcuts

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