bus

package
v0.26.0 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: MIT Imports: 27 Imported by: 0

Documentation

Overview

Package bus provides a typed event bus for decoupling agent, TUI, and serve layers.

Events are fire-and-forget (async fan-out to subscribers). Commands are synchronous request→response (one handler per type). Queries are synchronous read-only requests (one handler per type).

Top-level event/command/query payloads must be non-nil value structs. Nested fields may contain pointers, slices, and maps — subscribers must treat all payloads as read-only (no mutation after publish).

Index

Constants

View Source
const (
	// LiveToolPhaseGenerating: the model is still streaming this call's
	// arguments; nothing is executing yet.
	LiveToolPhaseGenerating = "generating"
	// LiveToolPhaseRunning: the tool started executing and has not ended.
	LiveToolPhaseRunning = "running"
)

Live tool-call phases. The values match the status clients render for a live tool row, so a snapshot entry restores exactly the row the live events would have produced.

View Source
const UserShellMaxOutput = 50 * 1024

UserShellMaxOutput caps captured shell-escape output (head+tail, combined stdout+stderr) at 50KB. This is user-triggered output, not model output, so no disk spill is used — it is simply truncated.

View Source
const UserShellTimeout = 5 * time.Minute

UserShellTimeout bounds how long a "!" / "!!" shell escape may run before being killed. Shared by TUI and web so behaviour matches.

Variables

View Source
var (
	// ErrPermissionDecisionSnapshotMismatch means the pending request changed,
	// was replaced, or was resolved after a caller reviewed its snapshot.
	// It intentionally has no request details: those may be sensitive tool args.
	ErrPermissionDecisionSnapshotMismatch    = errors.New("permission decision snapshot no longer matches")
	ErrPermissionDecisionSnapshotUnavailable = errors.New("permission decision snapshot unavailable")
)
View Source
var ErrClosed = errors.New("bus: closed")

ErrClosed is returned by Execute/Query when the bus has been closed.

View Source
var ErrInvalidTransition = errors.New("invalid state transition")

ErrInvalidTransition is returned (wrapped) by StateMachine.Transition when the requested transition is not allowed from the current state. Callers use errors.Is to detect "the session was busy" precisely, instead of matching the error text.

View Source
var ErrManualVerifyGoalActive = errors.New("cannot verify while goal mode is active; stop it first with /goal stop")

ErrManualVerifyGoalActive is returned when a user tries to run /verify while goal mode owns the maker→verification lifecycle.

View Source
var ErrNoHandler = errors.New("bus: no handler registered for this type")

ErrNoHandler is returned by Execute/Query when no handler is registered for the type.

View Source
var ErrSessionBusy = errors.New("session is busy")

ErrSessionBusy is returned by a bus command that requires the session to be idle (e.g. RunManualVerify) when a run is in flight or a permission is pending.

View Source
var ErrSteerQueueFull = errors.New("steer queue full")

ErrSteerQueueFull is returned by the SteerAgent command when the agent's steer queue is at capacity, so callers surface a rejection instead of confirming a message that would never be delivered.

View Source
var ErrVerifyRunning = errors.New("verify already running")

ErrVerifyRunning is returned by RunManualVerify when a manual verify is already in progress for the session.

Functions

func Bridge

func Bridge(sctx *SessionContext, subscriber AgentSubscriber) func()

Bridge subscribes to an agent's event emitter and publishes typed bus events. Returns an unsubscribe function. Call it when the session is destroyed.

func QueryTyped

func QueryTyped[Q any, R any](b EventBus, q Q) (R, error)

QueryTyped is a type-safe wrapper around Query that avoids manual type assertions.

msgs, err := bus.QueryTyped[GetMessages, []core.AgentMessage](b, GetMessages{})

func RegisterHandlers

func RegisterHandlers(sctx *SessionContext)

RegisterHandlers registers command and query handlers for a session on its bus. Call once after creating a SessionContext.

func RegisterPersistenceReactor

func RegisterPersistenceReactor(b EventBus, sctx *SessionContext, p SessionPersister)

RegisterPersistenceReactor subscribes to state-changing events and auto-saves. Saves are serialized through a mutex to prevent concurrent Snapshot calls. If the persister implements TreePersister and the session has a tree, it saves tree entries instead of flat messages.

func RequireManualVerifyAllowed

func RequireManualVerifyAllowed(b EventBus) error

RequireManualVerifyAllowed applies the shared manual-verification policy. Goal mode owns verification between its iterations, including the idle interval while it builds evidence and asks its verifier. A manual verify in that interval could run against the same workspace concurrently, so both UI frontends must reject it until goal mode has ended. Querying the runtime rather than relying on presentation state also makes reconnects safe.

func TranslateAgentEvent

func TranslateAgentEvent(sid string, gen uint64, e core.AgentEvent, taskStore *tasks.Store) []any

TranslateAgentEvent translates a single core.AgentEvent into 0..n typed bus events. It is a pure function (no publishing) so it can be reused both by the session Bridge and by the subagent event sink (namespaced per jobID).

taskStore may be nil; when nil, the TasksUpdated side event for the "tasks" tool is skipped (used by callers, e.g. subagent children, that have no meaningful task store).

Note: this does NOT apply SessionContext.SteerFilter — callers that care about filtering steer events (the session Bridge) must do so themselves before/around calling this function.

Types

type AbortAndRecall added in v0.26.0

type AbortAndRecall struct {
	SessionID       string
	RunGen          uint64
	DiscardedSteers *[]core.SteerItem
}

AbortAndRecall cancels a running agent and returns the queued items it atomically removed. Interactive clients use those IDs to restore only messages that were truly not delivered.

type AbortRun

type AbortRun struct{ SessionID string }

AbortRun cancels a running agent.

type AddAllowedPath

type AddAllowedPath struct {
	SessionID string
	Path      string
}

AddAllowedPath adds a directory to allowed paths.

type AddPermissionRule

type AddPermissionRule struct {
	SessionID    string
	PermissionID string
	Rule         string
}

AddPermissionRule adds a natural-language rule to auto-mode while a permission request is pending. The request stays open — the user can still approve/deny. This is NOT "always allow this request".

type AgentController

type AgentController interface {
	// Commands
	Abort()
	Steer(it core.SteerItem) bool
	CancelSteer() []core.SteerItem
	DrainSteers() []core.SteerItem
	DrainUntilBarrier() []core.SteerItem
	PushSteersFront(items []core.SteerItem)
	PeekQueueHead() (core.SteerItem, bool)
	PopQueueBarrier(id string) bool
	SendItems(ctx context.Context, items []core.SteerItem, msgIDs []string, announce func()) ([]core.AgentMessage, []string, error)
	SetModel(provider core.Provider, model core.Model) error
	SetThinkingLevel(level string) error
	SetSystemPrompt(prompt string) error
	SetCompactAt(tokens int) error
	SetMaxBudget(v float64) error
	Reset() error
	Compact(ctx context.Context, focus string) (*core.CompactionPayload, error)
	Send(ctx context.Context, prompt string) ([]core.AgentMessage, error)
	SendWithMsgID(ctx context.Context, prompt, msgID string) ([]core.AgentMessage, error)
	SendWithCustom(ctx context.Context, prompt string, custom map[string]any) ([]core.AgentMessage, error)
	SendWithContent(ctx context.Context, content []core.Content) ([]core.AgentMessage, error)
	SendWithContentMsgID(ctx context.Context, content []core.Content, msgID string) ([]core.AgentMessage, error)
	// SendWithContentAnnounced is SendWithContentMsgID that also announces the
	// appended user message (core.AgentEventUserMessage) from the append point,
	// so the announcement can never race a concurrent history snapshot.
	SendWithContentAnnounced(ctx context.Context, content []core.Content, msgID string) ([]core.AgentMessage, error)
	AppendMessage(msg core.AgentMessage) error
	SetPermissionCheck(fn func(ctx context.Context, name string, args map[string]any) *core.ToolCallDecision) error
	LoadState(msgs []core.AgentMessage, compactionEpoch int) error

	// Queries
	Messages() []core.AgentMessage
	Model() core.Model
	SystemPrompt() string
	ThinkingLevel() string
	CompactAt() int
	CompactAtFloor() int
	MaxBudget() float64
	CompactionEpoch() int
	IsRunning() bool
	PendingSteers() []core.SteerItem
	QueueLen() int
	NativeDocBytesUndelivered() int64
	ReserveNativeDocBytes(n int64)
	ReleaseNativeDocBytes(n int64)
}

AgentController is the command surface of an agent session.

type AgentEnded

type AgentEnded struct {
	SessionID string
	RunGen    uint64
	Messages  []core.AgentMessage
}

AgentEnded is published when the agent loop finishes normally.

type AgentError

type AgentError struct {
	SessionID string
	RunGen    uint64
	Err       error
}

AgentError is published when the agent loop exits with an error.

type AgentStarted

type AgentStarted struct {
	SessionID string
	RunGen    uint64
}

AgentStarted is published when the agent loop begins.

type AgentSubscriber

type AgentSubscriber interface {
	Subscribe(fn func(core.AgentEvent)) func()
}

AgentSubscriber allows subscribing to agent events.

type AppendToConversation

type AppendToConversation struct {
	SessionID string
	Message   core.AgentMessage
}

AppendToConversation adds a message to the conversation without running the agent.

type ApprovalManager

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

ApprovalManager manages pending permission and ask_user requests. Lives in SessionContext. Handlers delegate to it for resolve/lifecycle.

Lock discipline: validate/extract under mu, then send responses and publish events OUTSIDE the lock to prevent deadlock/reentrancy.

Response channels: both permission.Gate.askUser and askuser.Bridge create response channels with buffer size 1. The select{default:} pattern is therefore safe — the send always succeeds because the buffer guarantees space for exactly one response (the contract).

func NewApprovalManager

func NewApprovalManager(bus EventBus, state *StateMachine, sid string) *ApprovalManager

NewApprovalManager creates an ApprovalManager.

func (*ApprovalManager) ClearPending

func (am *ApprovalManager) ClearPending(gen uint64)

ClearPending auto-denies and removes still-pending permission/ask requests orphaned by the ended run, publishing Resolved events so no stale modal survives. Called when a run ends: a normal resolve already removed its entry before the run finished, so this only fires for approvals orphaned by an abort — which would otherwise reappear on every reconnect via PendingInfo.

gen is the generation of the ended run. Only requests from that run or an earlier one (RunGen <= gen) are cleared: if the user immediately re-sent a prompt, a newer run may already have a live approval, and a delayed RunEnded of the old run must not auto-deny it.

func (*ApprovalManager) PendingInfo

func (am *ApprovalManager) PendingInfo() PendingApprovalInfo

PendingInfo returns the current pending approval state.

func (*ApprovalManager) PendingPermissionDecisionSnapshot

func (am *ApprovalManager) PendingPermissionDecisionSnapshot() (PermissionDecisionSnapshot, error)

PendingPermissionDecisionSnapshot returns the exact identity of the sole current permission request. A Pulse decision cannot choose among multiple requests, because a human review of "the pending permission" would then be ambiguous.

func (*ApprovalManager) ResolveAskUser

func (am *ApprovalManager) ResolveAskUser(id string, answers []string) error

ResolveAskUser resolves a pending ask_user request.

func (*ApprovalManager) ResolvePermission

func (am *ApprovalManager) ResolvePermission(id string, approved bool, feedback, allow string) error

ResolvePermission resolves a pending permission request.

func (*ApprovalManager) ResolvePermissionExact

func (am *ApprovalManager) ResolvePermissionExact(snapshot PermissionDecisionSnapshot, approved bool) error

ResolvePermissionExact resolves only the request represented by snapshot. Identity validation and removal happen while the approval map is locked, so a legacy UI resolution or a new run cannot turn a reviewed Pulse decision into a decision for another request. This one-off primitive intentionally sends no feedback, allow pattern, or other agent-visible text.

func (*ApprovalManager) StartAskBridge

func (am *ApprovalManager) StartAskBridge(sessionCtx context.Context, bridge *askuser.Bridge)

StartAskBridge starts reading askBridge.Prompts(). Call once at session creation.

func (*ApprovalManager) StartPermissionBridge

func (am *ApprovalManager) StartPermissionBridge(sessionCtx context.Context, gate *permission.Gate)

StartPermissionBridge starts reading gate.Requests() and publishing PermissionRequested events. Call when a Gate is created.

func (*ApprovalManager) Stop

func (am *ApprovalManager) Stop()

Stop stops all bridges. Called by runtime.Close().

func (*ApprovalManager) StopPermissionBridge

func (am *ApprovalManager) StopPermissionBridge()

StopPermissionBridge stops the bridge and auto-denies all pending permissions. Used by SetPermissionMode(yolo) and runtime Close.

func (*ApprovalManager) ValidatePending

func (am *ApprovalManager) ValidatePending(id string) error

ValidatePending checks that a permission request is currently pending and not resolved.

type AskQuestion

type AskQuestion struct {
	Text    string   `json:"question"`
	Options []string `json:"options,omitempty"`
}

AskQuestion is a user-facing question with optional predefined answers.

type AskUserRequested

type AskUserRequested struct {
	SessionID string
	ID        string
	Questions []AskQuestion
}

AskUserRequested is published when the agent needs user input.

type AskUserResolved

type AskUserResolved struct {
	SessionID string
	ID        string
}

AskUserResolved is published when a pending ask-user prompt is answered.

type AutoVerifyEnded

type AutoVerifyEnded struct {
	SessionID string
	AllPass   bool
	Summary   string // formatted failure text, empty if all pass
	Err       error  // config/execution error
}

AutoVerifyEnded is published when auto-verify completes.

type AutoVerifyStarted

type AutoVerifyStarted struct {
	SessionID string
	Dir       string // empty when verifying the session's own directory
	Manual    bool   // true for /verify, false for auto-verify after an edit
}

AutoVerifyStarted is published when verification begins: automatically after an edit run, or manually via /verify. Dir names the directory being verified when it is not the session's own, so a multi-repo run says which checkout it is checking instead of leaving the user guessing.

type BashCompleted

type BashCompleted struct {
	SessionID    string
	JobID        string
	OwnerAgentID string
	Command      string
	Status       string
	Text         string
}

BashCompleted is published when an async background bash job finishes and its result is reinjected into the agent's conversation. Distinct from BashJobEnded (which drives the tray): BashCompleted carries the formatted notification text the agent (and chat UI) sees.

type BashJobEnded

type BashJobEnded struct {
	SessionID    string
	JobID        string
	OwnerAgentID string
	Status       string
	Output       string
}

BashJobEnded finalizes a background bash command with full bounded output.

type BashJobOutput

type BashJobOutput struct {
	SessionID    string
	JobID        string
	OwnerAgentID string
	Delta        string
}

BashJobOutput carries streamed background command output. It is lossy; the final BashJobEnded Output is authoritative.

type BashJobSettled

type BashJobSettled struct {
	SessionID string
	JobID     string
}

BashJobSettled is published after an async bash completion has either been reinjected or consumed by bash_wait. It is internal lifecycle bookkeeping so quiescence cannot observe a finished job before its follow-up is scheduled.

type BashJobSnapshot

type BashJobSnapshot struct {
	JobID        string
	OwnerAgentID string
	Command      string
	CWD          string
	Status       string
	Output       string
}

BashJobSnapshot is the transport-safe snapshot of one background bash job.

type BashJobStarted

type BashJobStarted struct {
	SessionID    string
	JobID        string
	OwnerAgentID string
	Command      string
	CWD          string
}

BashJobStarted announces a session-scoped background bash command.

type BranchPoint

type BranchPoint struct {
	EntryID       string `json:"entry_id"`
	Label         string `json:"label"` // first line of message
	Role          string `json:"role"`  // user/assistant
	Timestamp     int64  `json:"timestamp"`
	BranchCount   int    `json:"branch_count"` // number of children
	IsCurrentPath bool   `json:"is_current_path"`
}

BranchPoint describes a possible branch target in the conversation.

type BranchTo

type BranchTo struct{ EntryID string }

BranchTo moves the session tree leaf to the given entry ID, starting a new branch. The agent state is rehydrated from the new branch context. Returns error if the agent is running or the target is invalid.

type CancelBashJob

type CancelBashJob struct {
	SessionID string
	JobID     string
}

CancelBashJob cancels a session-scoped background bash job. Background bash is explicit-only: a synchronous bash call cannot be safely promoted after launch because its tool result and shell-state semantics are already bound to the foreground turn; cancel and relaunch it with async:true instead.

type CancelSteer

type CancelSteer struct {
	SessionID string
}

CancelSteer drops steer messages still queued (not yet delivered) for the running agent. Pairs with the TUI pulling queued steers back for editing.

type ClearSession

type ClearSession struct{ SessionID string }

ClearSession resets the conversation (agent.Reset).

type CommandDequeued

type CommandDequeued struct {
	SessionID string
	ID        string
	Raw       string
	Executed  bool
	// Err is non-empty when the barrier left the queue because its execution
	// failed permanently (e.g. a bad queued /goal objective); Executed is then
	// false. Frontends can surface it. A transient failure (lost run slot) does
	// NOT dequeue and emits no event — the pump retries at the next idle point.
	Err string
}

CommandDequeued is published when a queued command barrier leaves the queue, AFTER its execution: either because it was executed (Executed=true) or because its execution failed permanently (Executed=false, Err set) — a transient failure (lost run slot) neither dequeues nor emits an event, it is retried. It can also be published when the barrier was pulled back / canceled. Frontends clear the matching queued chip by ID. The command's own execution still emits its usual events (CommandExecuted, CompactionStarted/Ended, ConfigChanged, …).

type CommandExecuted

type CommandExecuted struct {
	SessionID string
	Command   string
	Messages  []core.AgentMessage // non-nil for /compact
}

CommandExecuted is published when a slash command is executed.

type CommandQueued

type CommandQueued struct {
	SessionID string
	ID        string
	Raw       string
}

CommandQueued is published when a slash command is enqueued as a barrier in the unified queue rail (issued while the session was busy). Frontends render an optimistic queued chip for it, distinct from a queued message chip, keyed by ID. Raw is the normalized command line for display (e.g. "/compact").

type CompactSession

type CompactSession struct {
	SessionID string
	Focus     string
}

CompactSession triggers manual compaction. Focus, when non-empty, is a one-shot instruction from `/compact <focus>` forwarded to the summarizer to tell it what to keep in the foreground; it is not persisted on the session.

type CompactionEnded

type CompactionEnded struct {
	SessionID         string
	RunGen            uint64
	Payload           *core.CompactionPayload
	Err               error
	CostIncludedInRun bool // true when this payload's usage is already included in RunEnded.Cost
}

CompactionEnded is published when context compaction finishes.

type CompactionStarted

type CompactionStarted struct {
	SessionID string
	RunGen    uint64
}

CompactionStarted is published when context compaction begins.

type ConfigChanged

type ConfigChanged struct {
	SessionID string
	Model     string
	// Provider is the new model's provider (e.g. "anthropic"), set alongside
	// Model on a model switch. Empty when this event doesn't carry a model
	// change (e.g. a thinking-level or permission-mode-only update).
	Provider       string
	Thinking       string
	PermissionMode string
	PathScope      string
	// CompactAt is the new soft compaction threshold in tokens, set only on a
	// threshold change. A pointer because 0 is itself a meaningful value ("use
	// the model window"), so nil is the only way to say "unchanged" here.
	CompactAt *int
	// ContextWindow is the new model's input window in tokens, set only on a
	// model switch. It travels with the switch because it is the denominator
	// every context reading is measured against — a client that kept the old
	// one would report percentages against a window the session no longer has.
	ContextWindow int
}

ConfigChanged is published when session configuration changes (model, thinking, etc).

type ContextUpdated

type ContextUpdated struct {
	SessionID string
	Percent   int
}

ContextUpdated is published when the context window usage percentage changes.

type ContinueRefining

type ContinueRefining struct{ SessionID string }

ContinueRefining transitions from reviewing → planning (continue refining).

type EnterGoal

type EnterGoal struct {
	SessionID     string
	Objective     string
	CompactAt     int           // soft compaction threshold in tokens; 0 = leave unchanged
	VerifierSpec  string        // model spec for the verifier; "" = default (haiku)
	MaxIterations int           // 0 = unlimited
	MaxStalled    int           // 0 = default
	Timeout       time.Duration // 0 = no wall-clock deadline
	VerifyTimeout time.Duration // 0 = default verifier run timeout
	VerifyOneShot bool          // use the legacy tool-less one-shot verifier
	TotalBudget   float64       // cumulative USD ceiling; 0 = derive from per-run MaxBudget
	StatePath     string        // "" = default (.moa/goal/STATE.md)
	WorkDir       string        // "" = session CWD; relative paths resolve against the session CWD
}

EnterGoal starts an autonomous maker→verifier loop toward Objective. The handler lowers the compaction threshold (CompactAt), injects the goal directive into the system prompt, and kicks the first iteration.

type EnterPlanMode

type EnterPlanMode struct{ SessionID string }

EnterPlanMode enters planning mode (creates plan file).

type EventBus

type EventBus interface {
	// Publish fans out an event to all subscribers of that type.
	// No-op after Close. Panics on nil event.
	Publish(event any)

	// Subscribe registers a handler for events of a specific type.
	// handler must be func(T) where T is a concrete struct (not pointer).
	// Returns an unsubscribe function (idempotent, non-blocking, safe to call
	// from within the handler itself).
	// Returns a no-op unsubscribe if bus is already closed.
	// Panics on invalid signature or pointer type.
	Subscribe(handler any) func()

	// SubscribeAll registers a handler that receives ALL events regardless of type.
	// The handler receives events in publication order within a single goroutine,
	// guaranteeing ordering. Events are delivered to SubscribeAll handlers BEFORE
	// typed subscribers.
	// Returns an unsubscribe function (idempotent, non-blocking).
	// Returns a no-op unsubscribe if bus is already closed.
	SubscribeAll(handler func(any)) func()

	// SubscribeAllSeq is the sequenced counterpart of SubscribeAll. Sequence
	// numbers are monotonically increasing within this bus and identify a
	// publication boundary; gaps are valid when consumers drop lossy events.
	SubscribeAllSeq(handler func(seq uint64, event any)) func()

	// LastSeq returns the most recently accepted publication sequence.
	LastSeq() uint64

	// Execute dispatches a command to its registered handler synchronously.
	// Returns ErrNoHandler if none registered, ErrClosed if bus is closed.
	// Recovers handler panics and returns them as wrapped errors.
	// Panics on nil command.
	Execute(command any) error

	// Query dispatches a query to its registered handler synchronously.
	// Returns (nil, ErrNoHandler) if none registered, (nil, ErrClosed) if closed.
	// Recovers handler panics and returns them as wrapped errors.
	// Panics on nil query.
	Query(query any) (any, error)

	// OnCommand registers a handler for a specific command type.
	// handler must be func(T) error where T is a concrete struct (not pointer).
	// Panics on invalid signature, pointer type, or duplicate registration.
	OnCommand(handler any)

	// OnQuery registers a handler for a specific query type.
	// handler must be func(T) (R, error) where T is a concrete struct (not pointer).
	// Panics on invalid signature, pointer type, or duplicate registration.
	OnQuery(handler any)

	// Drain waits for all in-flight event handlers to finish, or until timeout.
	Drain(timeout time.Duration)

	// Close marks the bus as closed. Idempotent.
	// New Publish calls become no-ops; Execute/Query return ErrClosed.
	// Subscriber goroutines drain remaining queued events and exit.
	Close()
}

EventBus mediates typed events, commands, and queries between components.

Events are async (fan-out to subscribers via buffered channels). Commands and queries are synchronous (one handler per type).

Top-level event/command/query payloads must be non-nil value structs. Nested fields may contain pointers, slices, and maps — subscribers must treat all payloads as read-only (no mutation after publish).

type ExitGoal

type ExitGoal struct{ SessionID string }

ExitGoal stops goal mode (removes the directive and restores compaction).

type ExitPlanMode

type ExitPlanMode struct{ SessionID string }

ExitPlanMode exits planning mode.

type FinishPlanReview

type FinishPlanReview struct{ SessionID string }

FinishPlanReview completes the review phase and transitions to ready.

type GetBashJobs

type GetBashJobs struct{ SessionID string }

GetBashJobs returns active/recent background bash jobs for reconnecting UIs.

type GetBranchPoints

type GetBranchPoints struct{ SessionID string }

GetBranchPoints returns branch points for the branch picker UI. Handler returns: []BranchPoint

type GetCompactAt

type GetCompactAt struct{ SessionID string }

GetCompactAt returns the soft compaction threshold in tokens (0 = the default window-based behavior). Handler returns: int

type GetCompactAtFloor

type GetCompactAtFloor struct{ SessionID string }

GetCompactAtFloor returns the lowest compaction threshold the engine honors, in tokens. A UI offering the threshold needs it to bound its own control: below it the engine raises the value, so anything lower would be a promise it breaks. Handler returns: int

type GetCompacting

type GetCompacting struct{ SessionID string }

GetCompacting reports whether a compaction is currently in progress, so a reconnect snapshot can restore (or clear) the compacting spinner. Handler returns: bool

type GetCompactionEpoch

type GetCompactionEpoch struct{ SessionID string }

GetCompactionEpoch returns the current compaction epoch counter. Handler returns: int

type GetContextUsage

type GetContextUsage struct{ SessionID string }

GetContextUsage returns the context window usage as a percentage (0-100). Handler returns: int

type GetDisplayMessages

type GetDisplayMessages struct{ SessionID string }

GetDisplayMessages returns the full message history for display (from tree). Unlike GetMessages, this includes pre-compaction messages. Handler returns: []core.AgentMessage

type GetGoal

type GetGoal struct{ SessionID string }

GetGoal returns the current goal-mode state. Handler returns: GoalInfo

type GetMessages

type GetMessages struct{ SessionID string }

GetMessages returns the current conversation messages. Handler returns: []core.AgentMessage

type GetModel

type GetModel struct{ SessionID string }

GetModel returns the current model configuration. Handler returns: core.Model

type GetPathPolicy

type GetPathPolicy struct{ SessionID string }

GetPathPolicy returns the current path policy state. Handler returns: PathPolicyInfo

type GetPendingApproval

type GetPendingApproval struct{ SessionID string }

GetPendingApproval returns pending permission/ask info for WS init data. Handler returns: PendingApprovalInfo

type GetPendingSteers

type GetPendingSteers struct{ SessionID string }

GetPendingSteers returns the authoritative queue of steer messages not yet delivered, so a reconnect snapshot can restore the queued-message chips. Handler returns: []core.SteerItem

type GetPermissionDecisionSnapshot

type GetPermissionDecisionSnapshot struct{ SessionID string }

GetPermissionDecisionSnapshot returns the safe exact identity of the one current pending permission. It fails when there are none or more than one.

type GetPermissionInfo

type GetPermissionInfo struct{ SessionID string }

GetPermissionInfo returns detailed permission info (mode, patterns, rules). Handler returns: PermissionInfo

type GetPermissionMode

type GetPermissionMode struct{ SessionID string }

GetPermissionMode returns the current permission mode (yolo/ask/auto). Handler returns: string

type GetPlanMode

type GetPlanMode struct{ SessionID string }

GetPlanMode returns the current plan mode and plan file path. Handler returns: PlanModeInfo

type GetQueueLen

type GetQueueLen struct{ SessionID string }

GetQueueLen returns the number of items in the unified queue rail (steers and command barriers not yet delivered/executed). The serve layer uses it to decide whether a /send starts a run directly (idle and empty queue) or must be enqueued as a steer to preserve strict send order. Handler returns: int

type GetRunGeneration added in v0.26.0

type GetRunGeneration struct{ SessionID string }

GetRunGeneration returns the generation of the current or most recently settled run. A stop command binds to this value so it cannot abort a newer run that starts while the request is in flight. Handler returns: uint64

type GetRunTokens

type GetRunTokens struct{ SessionID string }

GetRunTokens returns the current run's estimated logical input/output traffic. Handler returns: RunTokens

type GetSessionCost

type GetSessionCost struct{ SessionID string }

GetSessionCost returns the accumulated session cost in USD (main run + subagents). Handler returns: float64

type GetSessionError

type GetSessionError struct{ SessionID string }

GetSessionError returns the last error message from the state machine. Handler returns: string

type GetSessionState

type GetSessionState struct{ SessionID string }

GetSessionState returns the current session state (idle/running/permission/ask). Handler returns: string

type GetSubagents

type GetSubagents struct{ SessionID string }

GetSubagents returns a snapshot of currently live subagent jobs (running or cancelling), plus terminal children that still own a retained background bash job, including their accumulated transcript. Used to populate the agent tray and reconnect clients mid-run. Bus itself does not know about pkg/subagent — the handler is registered by the frontend (serve/TUI) that owns the *subagent.Jobs handle. Handler returns: []SubagentSnapshot

type GetTasks

type GetTasks struct{ SessionID string }

GetTasks returns the current task list. Handler returns: []tasks.Task

type GetThinkingLevel

type GetThinkingLevel struct{ SessionID string }

GetThinkingLevel returns the current thinking level string. Handler returns: string

type GetUndeliveredNativeBytes

type GetUndeliveredNativeBytes struct{ SessionID string }

GetUndeliveredNativeBytes returns the decoded native document/image bytes that are accepted into the session (queued steers plus any drained batch in flight to history) but not yet visible in history. The serve quota check adds it to the history total so concurrent sends can't collectively exceed the per-session native-content budget through the async delivery window. Handler returns: int64

type GoalChanged

type GoalChanged struct {
	SessionID string
	Active    bool
	Objective string
	WorkDir   string
	Iteration int
	Stalled   int
}

GoalChanged is published when goal mode activates or deactivates.

type GoalEnded

type GoalEnded struct {
	SessionID string
	Reason    string
}

GoalEnded is published when the loop stops (objective met or a backstop hit).

type GoalInfo

type GoalInfo struct {
	Active        bool
	Objective     string
	WorkDir       string
	Iteration     int
	Stalled       int
	MaxIterations int
	MaxStalled    int
	Verifying     bool // a verifier run is currently in flight
}

GoalInfo is the result of GetGoal.

type GoalIterationEnded

type GoalIterationEnded struct {
	SessionID string
	Iteration int
	Satisfied bool
	Feedback  string
	// Err is set when the iteration ended because the verifier was unavailable
	// (a transient infrastructure failure that survived retries), as opposed to
	// a genuine "not satisfied" verdict. The loop pauses in that case.
	Err error
}

GoalIterationEnded is published after the verifier judges an iteration.

type GoalVerifyEnded

type GoalVerifyEnded struct {
	SessionID string
	Iteration int
	Verifying bool
}

GoalVerifyEnded is published when the verifier finishes an iteration (with a verdict, an error, or because it was cancelled). Verifying carries the aggregate state AFTER this verify finished: if another verification is still running (verifications overlapped), it stays true so the UI keeps its indicator on instead of clearing it prematurely.

type GoalVerifyStarted

type GoalVerifyStarted struct {
	SessionID string
	Iteration int
}

GoalVerifyStarted is published when the verifier begins judging an iteration. The verifier can take minutes (it reads the plan and inspects the repo), so the UI shows a "verifying…" indicator instead of an apparent idle gap.

type HandoffReady added in v0.25.0

type HandoffReady struct {
	SessionID string
	Prompt    string
	ModelSpec string
	Thinking  string
}

HandoffReady carries an ephemeral handoff brief to the frontend that owns session creation. It is never persisted in the source session.

type HandoffSession added in v0.25.0

type HandoffSession struct {
	SessionID string
	Options   handoff.Options
}

HandoffSession generates an ephemeral brief from this conversation. Its frontend creates and starts the destination session when HandoffReady arrives.

type HandoffSettled added in v0.25.0

type HandoffSettled struct {
	SessionID string
	Cancelled bool
	Err       error
}

HandoffSettled is the terminal outcome of an internal handoff run. Unlike RunEnded it is specific to /handoff, so existing run-success consumers keep their established cancellation semantics.

type LiveToolCall added in v0.23.0

type LiveToolCall struct {
	ToolCallID string
	ToolName   string
	Args       map[string]any
	// Phase is LiveToolPhaseGenerating or LiveToolPhaseRunning.
	Phase string
	// StartedAt anchors the client's elapsed timer to the moment the call first
	// appeared, so a reconnect resumes the count instead of restarting it.
	StartedAt time.Time
}

LiveToolCall is one tool call that exists but has not finished. It is the tool-call counterpart of StreamingAggregate: while a call streams its arguments or executes, it is in no message history (a call lands in history when its assistant message closes, its result when the tool ends), so a client that (re)opens the session mid-call would otherwise render a nameless "Calling" row — or nothing at all for a two-minute bash. Captured atomically with the sequence cut via SessionContext.SnapshotInFlightWithCut.

type LocalBus

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

LocalBus is an in-process EventBus implementation. Create with NewLocalBus; zero value is NOT usable.

func NewLocalBus

func NewLocalBus() *LocalBus

NewLocalBus creates a ready-to-use LocalBus.

func (*LocalBus) Close

func (b *LocalBus) Close()

Close implements EventBus.

func (*LocalBus) Drain

func (b *LocalBus) Drain(timeout time.Duration)

Drain implements EventBus.

func (*LocalBus) Execute

func (b *LocalBus) Execute(command any) (retErr error)

Execute implements EventBus.

func (*LocalBus) LastSeq

func (b *LocalBus) LastSeq() uint64

LastSeq implements EventBus.LastSeq.

func (*LocalBus) OnCommand

func (b *LocalBus) OnCommand(handler any)

OnCommand implements EventBus.

func (*LocalBus) OnQuery

func (b *LocalBus) OnQuery(handler any)

OnQuery implements EventBus.

func (*LocalBus) Publish

func (b *LocalBus) Publish(event any)

Publish implements EventBus.

func (*LocalBus) Query

func (b *LocalBus) Query(query any) (retResult any, retErr error)

Query implements EventBus.

func (*LocalBus) Subscribe

func (b *LocalBus) Subscribe(handler any) func()

Subscribe implements EventBus.

func (*LocalBus) SubscribeAll

func (b *LocalBus) SubscribeAll(handler func(any)) func()

SubscribeAll implements EventBus.

func (*LocalBus) SubscribeAllSeq

func (b *LocalBus) SubscribeAllSeq(handler func(uint64, any)) func()

SubscribeAllSeq implements EventBus.SubscribeAllSeq.

type MCPChanged

type MCPChanged struct {
	SessionID string
	Total     int
	Ready     int
	Disabled  int
	Unhealthy int
	Pending   int
}

MCPChanged is published when a session's MCP servers change: a server connects, exits, is restarted, or is enabled/disabled. It carries the rolled up counts for the status-line indicator so clients can recolor without a round-trip; an open panel re-fetches the full per-server detail on receipt. Counts mirror serve.MCPSummary but stay primitive so the bus keeps no dependency on the serve layer.

type MarkTaskDone

type MarkTaskDone struct {
	SessionID string
	TaskID    int
}

MarkTaskDone marks a task as done.

type MessageEnded

type MessageEnded struct {
	SessionID string
	RunGen    uint64
	Message   core.AgentMessage
	FullText  string
}

MessageEnded is published when an assistant message finishes streaming.

type MessageStarted

type MessageStarted struct {
	SessionID string
	RunGen    uint64
	Message   core.AgentMessage
}

MessageStarted is published when a new assistant message begins streaming.

type MsgIDInUse added in v0.21.0

type MsgIDInUse struct {
	SessionID string
	MsgID     string
}

MsgIDInUse reports whether a message with this ID is already part of the session's history. Ingress paths that accept a client-supplied message ID use it to refuse an identity that is already taken: reusing a historical ID would make every client dedup the new message away and hide it until a reload. Handler returns: bool

type PathPolicyInfo

type PathPolicyInfo struct {
	WorkspaceRoot string
	Scope         string
	AllowedPaths  []string
}

PathPolicyInfo is the result of GetPathPolicy.

type PendingApprovalInfo

type PendingApprovalInfo struct {
	Permission *PendingPermissionInfo `json:"permission,omitempty"`
	Ask        *PendingAskInfo        `json:"ask,omitempty"`
}

PendingApprovalInfo is returned by GetPendingApproval for WS init data.

type PendingAsk

type PendingAsk struct {
	ID        string
	Questions []AskQuestion
	RunGen    uint64
	// contains filtered or unexported fields
}

PendingAsk tracks a single pending ask_user request.

type PendingAskInfo

type PendingAskInfo struct {
	ID        string        `json:"id"`
	Questions []AskQuestion `json:"questions"`
}

PendingAskInfo describes a pending ask_user request.

type PendingPermission

type PendingPermission struct {
	ID           string
	ToolName     string
	Args         map[string]any
	AllowPattern string
	RunGen       uint64
	// contains filtered or unexported fields
}

PendingPermission tracks a single pending permission request.

type PendingPermissionInfo

type PendingPermissionInfo struct {
	ID           string         `json:"id"`
	ToolName     string         `json:"tool_name"`
	Args         map[string]any `json:"args"`
	AllowPattern string         `json:"allow_pattern"`
}

PendingPermissionInfo describes a pending permission request.

type PermissionDecisionSnapshot

type PermissionDecisionSnapshot struct {
	PermissionID       string
	ToolName           string
	AllowPatternDigest string
	ArgsDigest         string
	RunGen             uint64
}

PermissionDecisionSnapshot is the non-sensitive, exact identity of one pending permission request. It deliberately excludes raw Args and the raw allow pattern: callers can bind them by digest without exposing or persisting tool arguments outside the approval manager.

type PermissionInfo

type PermissionInfo struct {
	Mode          string
	AllowPatterns []string
	Rules         []string
}

PermissionInfo is the result of GetPermissionInfo.

type PermissionRequested

type PermissionRequested struct {
	SessionID    string
	ID           string
	ToolName     string
	Args         map[string]any
	AllowPattern string // glob pattern for "always allow"
}

PermissionRequested is published when a tool needs user approval.

type PermissionResolved

type PermissionResolved struct {
	SessionID string
	ID        string
}

PermissionResolved is published when a pending permission is resolved.

type PlanModeChanged

type PlanModeChanged struct {
	SessionID string
	Mode      string // "off", "planning", "ready", "executing", "reviewing"
	PlanFile  string
}

PlanModeChanged is published when the plan mode state transitions.

type PlanModeInfo

type PlanModeInfo struct {
	Mode                string
	PlanFile            string
	ReviewModelID       string // model ID for plan review
	ReviewModelName     string // display name of review model
	ReviewThinkingLevel string // thinking level for plan review
}

PlanModeInfo is the result of GetPlanMode.

type PrepareCompactSession

type PrepareCompactSession struct{ SessionID string }

PrepareCompactSession runs a short internal preparation turn then compacts without releasing the session slot between the two phases.

type PromoteSubagent

type PromoteSubagent struct {
	SessionID string
	JobID     string
}

PromoteSubagent flips a running synchronous subagent job to async, unblocking its parent's blocking tool call while the child keeps running in the background.

type QueueCommand

type QueueCommand struct {
	SessionID string
	ID        string
	Raw       string
}

QueueCommand enqueues a slash command as a BARRIER in the agent's unified queue rail. The command is not executed now: it stops the queue drain and is run at the next idle point (RunEnded) by the queue pump, in strict send order relative to surrounding steer messages. Raw is the normalized command line (leading slash optional, e.g. "/compact", "model sonnet"). Only commands with PolicyQueue should be enqueued this way; the caller classifies first.

type QueuePolicy

type QueuePolicy int

QueuePolicy classifies how a slash command behaves when it is issued while the session is BUSY (a run is in flight, or the agent is otherwise occupied). When the session is idle every command runs immediately regardless of policy; the policy only decides what happens to a command typed mid-run.

const (
	// PolicyInstant: the command is safe to run immediately even while a run is
	// in flight (it doesn't touch the live run's history or model). Frontends
	// execute it right away.
	PolicyInstant QueuePolicy = iota
	// PolicyQueue: the command must wait for the current run to finish, because
	// running it mid-flight would corrupt the run (e.g. compact rewrites
	// history, model/thinking strip model-specific thinking signatures). It is
	// enqueued as a barrier and executed at the next idle point, preserving
	// strict send order relative to surrounding messages.
	PolicyQueue
	// PolicyReject: the command cannot run while busy and cannot be meaningfully
	// deferred (it is a mode transition or a destructive rewind that only makes
	// sense against a settled conversation). Frontends refuse it with an error.
	PolicyReject
)

func ClassifyCommand

func ClassifyCommand(raw string) QueuePolicy

ClassifyCommand returns the queue policy for a raw slash command line issued while the session is busy. The input may include or omit the leading slash and may carry arguments (e.g. "/model sonnet", "goal ship it"). It never panics on malformed input: an empty or slash-only line is PolicyInstant.

goal is argument-dependent: "goal", "goal status", "goal stop" only read or tear down goal mode and run instantly; "goal <objective>" (or "goal start") launches a new run and must wait for the current one to finish (barrier).

func (QueuePolicy) String

func (p QueuePolicy) String() string

type RateLimitUpdated

type RateLimitUpdated struct {
	SessionID string
	RunGen    uint64
	RateLimit core.RateLimit
}

RateLimitUpdated is published after each assistant message with the provider's reported rate-limit state (plan-window utilization + whether the request drew on extra usage). Enables instant, per-session overage awareness without waiting for the account-global usage poll.

type RemoveAllowedPath

type RemoveAllowedPath struct {
	SessionID string
	Path      string
}

RemoveAllowedPath removes a directory from allowed paths.

type ResetTasks

type ResetTasks struct{ SessionID string }

ResetTasks clears all tasks.

type ResolveAskUser

type ResolveAskUser struct {
	SessionID string
	AskID     string
	Answers   []string
}

ResolveAskUser resolves a pending ask_user prompt.

type ResolvePermission

type ResolvePermission struct {
	SessionID    string
	PermissionID string
	Approved     bool
	Feedback     string
	AllowPattern string
}

ResolvePermission resolves a pending tool permission request.

type ResolvePermissionExact

type ResolvePermissionExact struct {
	SessionID string
	Snapshot  PermissionDecisionSnapshot
	Approved  bool
}

ResolvePermissionExact resolves a reviewed one-off permission only when the current pending request still exactly matches Snapshot. It has no allow, rule, or feedback field, so callers cannot create a permanent rule or inject text into the pending tool's result.

type RunEnded

type RunEnded struct {
	SessionID string
	RunGen    uint64
	FinalText string
	Err       error   // non-nil for real errors (not cancellation)
	HadEdits  bool    // true if edit/write/multiedit/apply_patch completed successfully
	Cost      float64 // USD cost of this run (0 if the model has no pricing)
}

RunEnded is published when a full agent run completes (may span multiple turns).

type RunManualVerify

type RunManualVerify struct {
	SessionID string
	Dir       string
}

RunManualVerify runs the project's verification checks (the /verify command) as a bus command that occupies the session state (idle→running→idle), so a queued /verify barrier keeps its position and can't race a concurrent run. It emits AutoVerifyStarted/Ended and returns an error describing a failure (ErrManualVerifyGoalActive when goal mode is active, ErrSessionBusy when a run is in flight, ErrVerifyRunning when one is already running, or a check failure). The serve/TUI /verify commands are routed through it in a later commit so both frontends share this state-occupying implementation.

Dir carries the optional directory of `/verify <dir>`; empty means the session's own. It has to travel with the command because a /verify typed mid-run is queued as raw text and replayed here — dropping the directory would silently verify the wrong repository.

type RunStarted

type RunStarted struct {
	SessionID string
	RunGen    uint64
}

RunStarted is published when a new agent run begins (after state transition). Frontends use RunGen to filter events belonging to the current run.

type RunTokens

type RunTokens struct {
	Up   int
	Down int
}

RunTokens is the result of GetRunTokens.

type RunTokensUpdated

type RunTokensUpdated struct {
	SessionID string
	RunGen    uint64
	Up        int
	Down      int
}

RunTokensUpdated carries the current run's estimated logical input and output traffic, excluding resent context and provider cache usage.

type RuntimeConfig

type RuntimeConfig struct {
	SessionID         string
	Ctx               context.Context
	Bus               EventBus // optional pre-created bus; if nil, a new LocalBus is created
	Agent             AgentController
	Subscriber        AgentSubscriber // nil = use Agent if it implements AgentSubscriber
	TaskStore         *tasks.Store
	Checkpoints       *checkpoint.Store
	SessionCheckpoint *sessioncheckpoint.Slot
	PlanMode          *planmode.PlanMode
	Goal              *goal.Goal
	Gate              *permission.Gate
	PathPolicy        *tool.PathPolicy
	AskBridge         *askuser.Bridge
	ProviderFactory   func(core.Model) (core.Provider, error)
	BaseSystemPrompt  string
	Persister         SessionPersister
	SteerFilter       func(text string) bool

	CWD        string // workspace directory
	AutoVerify bool   // run verify after edit runs

	// GateConfig preserves allow/deny/rules/headless config for gate reconstruction
	// when switching between permission modes at runtime.
	GateConfig permission.Config

	// InitialMessages/InitialCompactionEpoch load saved state into the agent
	// at construction time (before any handlers fire). Used by session restore.
	InitialMessages        []core.AgentMessage
	InitialCompactionEpoch int
	InitialMetadata        map[string]any

	// InitialEntries/InitialLeafID load a v2 session tree.
	// When set, the tree is reconstructed and agent state is derived from BuildContext.
	// InitialMessages is ignored when InitialEntries is set.
	InitialEntries []session.Entry
	InitialLeafID  string
}

RuntimeConfig holds all dependencies for creating a SessionRuntime.

type SendPrompt

type SendPrompt struct {
	SessionID string
	Text      string
	Custom    map[string]any
	// MsgID, when set, is used as the user message's stable identifier instead
	// of an auto-minted one, so a caller that later announces this prompt can
	// reference it by a shared MsgID for reconnect dedup. Ignored when Custom is
	// set. It is honored only if free: the handler claims the identity atomically
	// and re-mints a taken one, reporting the effective ID in AcceptedMsgID.
	MsgID string
	// AcceptedMsgID, when non-nil, receives the message ID the prompt was
	// actually accepted under, so a caller that supplied MsgID learns whether it
	// was re-minted and can reconcile its optimistic echo. Only written for the
	// direct-send path (a queued prompt becomes a steer and is identified by its
	// chip ID instead).
	AcceptedMsgID *string
	// SteerID, when set, is the identity to use if the prompt cannot start a run
	// (the queue rail is not empty) and is converted into a queued steer. It
	// never lands in AcceptedMsgID: the two rails have distinct identities.
	SteerID string
	// AcceptedSteerID, when non-nil, receives the chip ID the prompt was queued
	// under when it was converted into a steer. Exactly one of AcceptedMsgID /
	// AcceptedSteerID is written per accepted prompt, so a caller learns the
	// effective action ("send" vs "steer") from which one came back non-empty.
	AcceptedSteerID *string
}

SendPrompt starts an agent run with a text prompt. If Custom is non-nil, SendWithCustom is used instead of Send.

type SendPromptWithContent

type SendPromptWithContent struct {
	SessionID string
	Content   []core.Content
	// MsgID, when set, is used as the user message's stable identifier instead
	// of an auto-minted one, so the live announcement of this prompt
	// (UserMessageAppended) shares an identity with the caller's optimistic
	// echo and with reconnect snapshots. Mirrors SendPrompt.MsgID, including the
	// atomic claim and re-mint.
	MsgID string
	// AcceptedMsgID mirrors SendPrompt.AcceptedMsgID.
	AcceptedMsgID *string
	// SteerID mirrors SendPrompt.SteerID.
	SteerID string
	// AcceptedSteerID mirrors SendPrompt.AcceptedSteerID.
	AcceptedSteerID *string
}

SendPromptWithContent starts an agent run with structured content (e.g. images).

type SessionContext

type SessionContext struct {
	SessionID  string
	SessionCtx context.Context // session lifetime context; cancelled on destroy
	Bus        EventBus
	Agent      AgentController
	State      *StateMachine    // may be nil for backward compat
	Approvals  *ApprovalManager // manages pending permissions/asks; may be nil
	Tree       *session.Tree    // session entry tree; may be nil during migration

	PlanMode          *planmode.PlanMode      // may be nil
	Goal              *goal.Goal              // may be nil
	TaskStore         *tasks.Store            // may be nil
	Checkpoints       *checkpoint.Store       // may be nil
	SessionCheckpoint *sessioncheckpoint.Slot // ephemeral pre-compaction state
	PathPolicy        *tool.PathPolicy        // may be nil
	AskBridge         *askuser.Bridge         // may be nil
	PersistNow        func() error            // synchronous checkpoint-safe save

	ProviderFactory  func(core.Model) (core.Provider, error)
	BaseSystemPrompt string

	// GateConfig is used to reconstruct a Gate when switching from yolo
	// to ask/auto. Preserves allow/deny patterns, rules, headless, etc.
	GateConfig permission.Config

	CWD        string // workspace directory for tools/verify
	AutoVerify bool   // run verify automatically after edit runs

	// SteerFilter returns false to suppress a steer event (e.g. subagent
	// completion text in serve). If nil, all steers are published.
	SteerFilter func(text string) bool

	// RunGenAtomic is the current run generation, readable without locks.
	// Stamped on agent-lifecycle events by the bridge. Written by startRun
	// (under runMu), read atomically by the bridge.
	RunGenAtomic atomic.Uint64
	// contains filtered or unexported fields
}

SessionContext holds all session-scoped dependencies needed by handlers and the agent event bridge. Created once per session.

Bus is per-session (not shared between sessions). The SessionID in events and commands is metadata for logging/serialization, not routing.

func (*SessionContext) Compacting

func (sctx *SessionContext) Compacting() bool

Compacting reports whether a compaction is currently in progress, so a reconnect snapshot can restore (or clear) the compacting spinner.

func (*SessionContext) GetGate

func (sctx *SessionContext) GetGate() *permission.Gate

GetGate returns the current permission gate (may be nil for yolo mode).

func (*SessionContext) GoalVerifying

func (sctx *SessionContext) GoalVerifying() bool

GoalVerifying reports whether a goal verifier is currently running, so a reconnect snapshot can restore the "verifying…" indicator.

func (*SessionContext) LiveTools added in v0.23.0

func (sctx *SessionContext) LiveTools() []LiveToolCall

LiveTools returns the tool calls currently generating arguments or executing.

func (*SessionContext) SetGate

func (sctx *SessionContext) SetGate(g *permission.Gate)

SetGate atomically replaces the permission gate.

func (*SessionContext) SnapshotInFlightWithCut added in v0.23.0

func (sctx *SessionContext) SnapshotInFlightWithCut() (StreamingAggregate, []LiveToolCall, uint64)

SnapshotInFlightWithCut atomically captures the in-flight streaming aggregate AND the live tool-call registry together with the current bus sequence, all under streamMu. bridgeEvent holds streamMu across the mutation AND the derived Bus.Publish, so this pairing gives a total order for state that is not idempotent under replay: a streamed delta is either already folded into the returned text AND at/below the returned cut, or absent AND published above it — never both. Without this atomicity a delta could be seeded into the reconnect snapshot and ALSO replayed live (seq > cut), double-rendering the partial reply.

The tool registry rides the same gate for the same reason: a client dedups a restored row by tool_call_id, but the pair (snapshot, replayed events) must still be consistent — a call must not be *absent* from the snapshot while its only announcing events (tool_call_start / tool_start) sit at/below the cut and are therefore never replayed, which would resurrect the nameless "Calling" row this registry exists to kill.

func (*SessionContext) StreamingAggregate

func (sctx *SessionContext) StreamingAggregate() (text, thinking, msgID string)

StreamingAggregate returns the in-flight partial assistant text/thinking and the current message ID, for a reconnect snapshot during generation. Empty strings mean nothing is streaming right now.

type SessionCostUpdated

type SessionCostUpdated struct {
	SessionID string
	TotalUSD  float64
	RunUSD    float64
}

SessionCostUpdated is published when the accumulated session cost changes: after each run (RunEnded) and after each subagent finishes (SubagentEnded), or when the running total is reset (clear / clean-context plan execution / session load). TotalUSD is the cumulative spend for the session; RunUSD is the delta that triggered this update (0 on a reset).

type SessionLoaded

type SessionLoaded struct {
	SessionID string
}

SessionLoaded is published once a persisted session has been fully restored into a long-lived runtime. No intermediate configuration events are emitted during restoration.

type SessionPersister

type SessionPersister interface {
	// Snapshot persists the current session state.
	// Called by the persistence reactor. Implementations must be safe for
	// sequential calls (reactor serializes, but calls may come from
	// different goroutines).
	Snapshot(messages []core.AgentMessage, epoch int, metadata map[string]any) error
}

SessionPersister abstracts session persistence.

type SessionRebinder

type SessionRebinder interface {
	RebindSession(sess *session.Session)
}

SessionRebinder is an optional SessionPersister capability: re-point it at a different session so subsequent snapshots write there. A single long-lived runtime (the TUI) uses it to switch sessions without re-registering the persistence reactor.

type SessionRestoreState

type SessionRestoreState struct {
	Model             core.Model
	HasModel          bool
	Thinking          string
	HasThinking       bool
	PermissionMode    permission.Mode
	HasPermissionMode bool
	Tasks             tasks.State
	HasTasks          bool
	Plan              planmode.State
	PathScope         string
	AllowedPaths      []string
	HasPathPolicy     bool
}

SessionRestoreState is the validated, typed runtime state decoded from a persisted session. It centralizes metadata parsing for in-place restores.

func NewSessionRestoreState

func NewSessionRestoreState(sess *session.Session) SessionRestoreState

NewSessionRestoreState decodes the metadata persisted on sess. Invalid values are treated as absent so callers fall back to runtime defaults.

type SessionRuntime

type SessionRuntime struct {
	ID    string
	Bus   EventBus
	State *StateMachine
	// contains filtered or unexported fields
}

SessionRuntime is a fully wired session: bus + state machine + bridge + handlers + persistence. Created via NewSessionRuntime.

func NewSessionRuntime

func NewSessionRuntime(cfg RuntimeConfig) (*SessionRuntime, error)

NewSessionRuntime creates a fully wired session runtime. Returns error if required config fields are missing.

func (*SessionRuntime) AttachPersister

func (r *SessionRuntime) AttachPersister(p SessionPersister)

AttachPersister registers a persistence reactor on this runtime. Must be called at most once — panics on double call.

func (*SessionRuntime) Close

func (r *SessionRuntime) Close()

Close tears down the runtime. Idempotent. Aborts any running agent, cancels the run context, stops approval bridges, unsubscribes from agent events, and closes the bus.

func (*SessionRuntime) Context

func (r *SessionRuntime) Context() *SessionContext

Context returns the SessionContext. For testing and advanced use.

func (*SessionRuntime) DoIfQuiescent

func (r *SessionRuntime) DoIfQuiescent(fn func()) bool

DoIfQuiescent runs fn atomically with respect to run-start if the session is quiescent, returning whether it ran. It holds the state lock across fn (via StateMachine.DoIfIdle) so a run cannot begin between the quiescence check and fn — closing the check-then-act race for a live tool-set mutation. Background work is also required to be absent; that part is a snapshot (background jobs don't flip a tool set mid-fn), but the run-start edge, which does, is serialized. fn must not call back into the state machine.

func (*SessionRuntime) Flush

func (r *SessionRuntime) Flush() error

Flush synchronously persists the current session state to disk, bypassing the async RunEnded→TreeSynced→save event chain. It first folds any not-yet-synced agent messages (the last or in-flight turn) into the tree, then snapshots through the attached persister. No-op if no persister is attached.

Used on server shutdown: the async chain may not drain before the process exits, which would lose a turn that finished moments before. Flush is idempotent and safe to call once activity has quiesced.

func (*SessionRuntime) IsQuiescent

func (r *SessionRuntime) IsQuiescent() bool

IsQuiescent reports, without blocking, whether the session is idle enough to mutate its live tool set: not running, not awaiting a permission decision, and with no background work pending. Callers that must defer a tool-set change (e.g. an MCP toggle) until it is safe use this to decide apply-now vs pending.

func (*SessionRuntime) LoadSession

func (r *SessionRuntime) LoadSession(sess *session.Session) error

LoadSession is retained for callers that used the original in-place API. New callers should use SwitchSession.

func (*SessionRuntime) RefreshBaseSystemPrompt

func (r *SessionRuntime) RefreshBaseSystemPrompt(base string)

RefreshBaseSystemPrompt sets a freshly built base system prompt and re-applies it, composing plan/goal fragments on top. Callers use it after the tool set changes at runtime (e.g. an MCP server is enabled or disabled) so the model is never told about a tool that is no longer registered. It must be called while the agent is not running; the MCP controller only reconciles at quiescence, which guarantees that.

func (*SessionRuntime) SwitchSession

func (r *SessionRuntime) SwitchSession(sess *session.Session) error

SwitchSession atomically restores sess into this long-lived runtime. It restores history, runtime metadata, the tree syncer, and cost before rebinding persistence; direct restoration intentionally emits no ConfigChanged events. The agent must be idle.

func (*SessionRuntime) SyncPlanMode

func (r *SessionRuntime) SyncPlanMode()

SyncPlanMode rebuilds the system prompt and publishes PlanModeChanged for the current plan mode state. Call after restoring plan mode from persisted metadata (RestoreState/ApplyRestoredState happen before SetOnChange is wired).

func (*SessionRuntime) WaitQuiescent

func (r *SessionRuntime) WaitQuiescent(ctx context.Context) bool

WaitQuiescent waits for the complete autonomous session chain to finish. Unlike WaitSettled, it does not return in the gap after a foreground run becomes idle while auto-verify, a goal verifier, or an asynchronous child job can still publish work (and potentially start another run). It includes background bash jobs because their final output is likewise delivered after the foreground turn.

A goal that is active but paused without work is quiescent. This makes the method usable for headless callers even when a goal stops on a verifier infrastructure failure and requires human intervention to resume.

func (*SessionRuntime) WaitSettled

func (r *SessionRuntime) WaitSettled(ctx context.Context) bool

WaitSettled blocks until the session leaves the active states (running or waiting on a permission) — meaning any in-flight run has observed its context's cancellation and transitioned to idle/error — or ctx is done.

It reads the state machine directly (the authoritative source) and is woken by StateChanged events rather than busy-polling. Returns true if the session settled, false if ctx expired while a run was still active. Used on shutdown so Flush snapshots a complete turn instead of a partial one.

type SessionState

type SessionState string

SessionState represents the state of a session.

const (
	StateIdle       SessionState = "idle"
	StateRunning    SessionState = "running"
	StatePermission SessionState = "permission"
	StateError      SessionState = "error"
)

type SetCompactAt

type SetCompactAt struct {
	SessionID string
	Tokens    int
}

SetCompactAt changes the soft compaction threshold in tokens, so the session compacts once context passes it instead of waiting for the full model window. 0 restores the default (window-based) behavior.

type SetPathScope

type SetPathScope struct {
	SessionID string
	Scope     string
}

SetPathScope changes workspace/unrestricted scope.

type SetPermissionMode

type SetPermissionMode struct {
	SessionID string
	Mode      string
}

SetPermissionMode changes the permission mode (yolo/ask/auto).

type SetThinking

type SetThinking struct {
	SessionID string
	Level     string
}

SetThinking changes the thinking level.

type StartPlanExecution

type StartPlanExecution struct {
	SessionID    string
	CleanContext bool
}

StartPlanExecution transitions from ready → executing. CleanContext controls whether the conversation is reset before execution.

type StartPlanReview

type StartPlanReview struct {
	SessionID string
}

StartPlanReview transitions from ready → reviewing. Review configuration (model, thinking) is handled by the TUI locally, not by the bus — the plan mode state machine only tracks the mode transition.

type StateChanged

type StateChanged struct {
	SessionID string
	State     string
	Error     string
}

StateChanged is published when the session state transitions (idle/running/etc).

type StateMachine

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

StateMachine manages session state with validated transitions. Thread-safe. Publishes StateChanged events on every transition.

func NewStateMachine

func NewStateMachine(bus EventBus, sessionID string) *StateMachine

NewStateMachine creates a new state machine starting in StateIdle.

func (*StateMachine) Current

func (sm *StateMachine) Current() SessionState

Current returns the current state.

func (*StateMachine) DoIfIdle

func (sm *StateMachine) DoIfIdle(fn func()) bool

DoIfIdle runs fn while holding the state lock, but only if the current state is one from which a run may start (idle or error — not running, not permission). It returns whether fn ran. Holding the lock across fn makes the check and fn atomic with respect to state transitions: a run cannot start while fn executes, so a caller that mutates the live tool set under it cannot race a run beginning. The startable set matches WaitQuiescent's notion of quiescence (which also treats StateError as settled), so a deferred caller that waits for quiescence and then calls this does not busy-spin. fn must not call back into the state machine (it would deadlock) and should be short.

func (*StateMachine) ForceState

func (sm *StateMachine) ForceState(s SessionState)

ForceState sets state without validation or events. For session restore only.

func (*StateMachine) LastError

func (sm *StateMachine) LastError() string

LastError returns the most recent error message. Empty if last transition was non-error or after a clear transition.

func (*StateMachine) MustTransition

func (sm *StateMachine) MustTransition(to SessionState)

MustTransition panics on invalid transitions. Use in code paths where the transition is guaranteed valid by construction.

func (*StateMachine) Transition

func (sm *StateMachine) Transition(to SessionState) error

Transition moves to a new state. Returns error if the transition is invalid. Publishes StateChanged on success. Clears lastError.

func (*StateMachine) TransitionWithError

func (sm *StateMachine) TransitionWithError(to SessionState, errMsg string) error

TransitionWithError moves to a new state with an optional error message. Returns error if the transition is invalid. Publishes StateChanged on success.

type SteerAgent

type SteerAgent struct {
	SessionID string
	ID        string
	Text      string
	// Content, when non-nil, carries the full payload of the steer (text plus
	// image/content blocks) so a mid-run message can include attachments. When
	// nil the steer is plain text carried in Text.
	Content []core.Content
	// Internal marks a system-generated steer (subagent/bash completion) so it
	// is delivered to the agent but excluded from the user-visible queue
	// snapshot. Its delivery event is separately suppressed via SteerFilter.
	Internal bool
}

SteerAgent injects a steering message into a running agent.

type Steered

type Steered struct {
	SessionID string
	RunGen    uint64
	ID        string
	MsgID     string
	Text      string
	Content   []core.Content
}

Steered is published when a steering message is injected into the agent. Content carries the injected message's blocks when the steer had attachments (Text always holds its plain text, which is what text-only consumers render); without it a queued message with an image would appear live as bare text and only grow its thumbnail after a reload.

type SteersCanceled

type SteersCanceled struct {
	SessionID     string
	AttachmentIDs []string
}

SteersCanceled is published when all queued (not yet delivered) steers are dropped, so every client of the shared session queue clears its chips.

type StreamingAggregate

type StreamingAggregate struct {
	Text     string
	Thinking string
	MsgID    string
}

StreamingAggregate is the in-flight partial assistant text/thinking and its message ID, surfaced in the reconnect snapshot so a reconnect during generation restores the whole streamed-so-far reply instead of only post-cut deltas. Captured atomically with the sequence cut via SessionContext.SnapshotInFlightWithCut. Empty Text and Thinking mean nothing is streaming right now.

type SubagentCompleted

type SubagentCompleted struct {
	SessionID string
	JobID     string
	Task      string
	Status    string
	Text      string
}

SubagentCompleted is published when a subagent job finishes.

type SubagentCountChanged

type SubagentCountChanged struct {
	SessionID string
	Count     int
}

SubagentCountChanged is published when the active subagent count changes.

type SubagentEnded

type SubagentEnded struct {
	SessionID string
	JobID     string
	Status    string
	Usage     *core.Usage
	CostUSD   float64
}

SubagentEnded announces a subagent's completion (completed/failed/cancelled) along with its aggregated usage and precomputed cost (using the CHILD model's pricing, since it may differ from the parent's).

type SubagentEvent

type SubagentEvent struct {
	SessionID string
	JobID     string
	Inner     any
}

SubagentEvent transports a single already-typed bus event from a subagent child, namespaced by JobID. Inner is a bus.TextDelta / bus.ToolExecStarted / etc, produced by TranslateAgentEvent — never a raw core.AgentEvent.

type SubagentSnapshot

type SubagentSnapshot struct {
	JobID string
	// OriginToolCallID identifies the parent model tool call that created this job.
	OriginToolCallID string
	Task             string
	Model            string
	Thinking         string
	Status           string
	Async            bool
	Messages         []core.AgentMessage
	// StartedAt is when the child began running, so a reconnecting client can
	// keep computing live elapsed time. Zero when unknown.
	StartedAt time.Time
	// Usage/CostUSD carry the child's accumulated usage/cost so far, so live
	// cost doesn't reset to zero after a reconnect. Usage is nil until the
	// child has closed at least one message.
	Usage   *core.Usage
	CostUSD float64
	// ContextPercent is how full the CHILD's own context window is (0-100), or
	// -1 when unknown, so a reconnecting client restores the child's reading
	// rather than showing the parent's or nothing at all.
	ContextPercent int
	// AccentIndex is the subagent's stable per-session creation ordinal (see
	// bus.SubagentStarted.AccentIndex), used by clients to derive a
	// deterministic accent color that survives reconnects.
	AccentIndex int
}

SubagentSnapshot describes one reconnect-visible subagent job, including its transcript so far. Result element type for GetSubagents.

type SubagentStarted

type SubagentStarted struct {
	SessionID string
	JobID     string
	// OriginToolCallID identifies the parent model tool call that created this job.
	OriginToolCallID string
	Task             string
	Model            string
	Thinking         string
	Async            bool
	// StartedAt is when the child agent began running, so live UIs can compute
	// elapsed time (now - StartedAt) and reconcile it after a reconnect. Zero
	// when the emitter did not record a start time.
	StartedAt time.Time
	// AccentIndex is the subagent's stable per-session creation ordinal
	// (0, 1, 2, ... assigned once, in creation order, never reused), used by
	// clients to derive a deterministic accent color that survives WS
	// reconnects instead of one derived from map iteration order.
	AccentIndex int
}

SubagentStarted announces the start of a subagent (sync or async).

type SubagentUsage

type SubagentUsage struct {
	SessionID string
	JobID     string
	Usage     *core.Usage
	CostUSD   float64
	// ContextPercent is how full the CHILD's own context window is (0-100), or
	// -1 when its model has no known window. It travels with usage because a
	// client zoomed into the child measures it against the child's window, not
	// the parent's: different transcript, often a different model.
	ContextPercent int
}

SubagentUsage carries a subagent's running aggregated usage/cost, published each time the child closes a message (its message_end). It lets live UIs show accumulated tokens/cost while the child is still running, before the terminal SubagentEnded. Cost is computed with the CHILD model's pricing, the same way SubagentEnded computes its final total, so the live value stays consistent with the final one. It is safe to drop under backpressure (lossy): each message_end re-sends the full accumulated total, and SubagentEnded is authoritative.

type SwitchModel

type SwitchModel struct {
	SessionID string
	ModelSpec string
}

SwitchModel changes the active model.

type TasksUpdated

type TasksUpdated struct {
	SessionID string
	Tasks     []tasks.Task
}

TasksUpdated is published when the task list changes.

type TextDelta

type TextDelta struct {
	SessionID string
	RunGen    uint64
	Delta     string
}

TextDelta is published for each text chunk streamed from the model.

type ThinkingDelta

type ThinkingDelta struct {
	SessionID string
	RunGen    uint64
	Delta     string
}

ThinkingDelta is published for each thinking/reasoning chunk from the model.

type ToolCallDelta

type ToolCallDelta struct {
	SessionID  string
	RunGen     uint64
	ToolCallID string
	Args       map[string]any // partially-parsed, monotonically non-regressing
}

ToolCallDelta is published with incrementally-parsed tool call arguments. High-frequency event — treated as lossy (same as TextDelta/ThinkingDelta).

type ToolCallStreaming

type ToolCallStreaming struct {
	SessionID  string
	RunGen     uint64
	ToolCallID string
	ToolName   string
}

ToolCallStreaming is published when the LLM starts generating a tool call. The tool block should appear in the UI immediately.

type ToolExecEnded

type ToolExecEnded struct {
	SessionID  string
	RunGen     uint64
	ToolCallID string
	ToolName   string
	Result     string
	IsError    bool
	Rejected   bool
}

ToolExecEnded is published when a tool call finishes.

type ToolExecStarted

type ToolExecStarted struct {
	SessionID  string
	RunGen     uint64
	ToolCallID string
	ToolName   string
	Args       map[string]any
}

ToolExecStarted is published when a tool call begins execution.

type ToolExecUpdate

type ToolExecUpdate struct {
	SessionID  string
	RunGen     uint64
	ToolCallID string
	Delta      string
}

ToolExecUpdate is published for streaming tool output.

type TreePersister

type TreePersister interface {
	SessionPersister
	// SnapshotTree persists the session tree entries and leaf.
	SnapshotTree(entries []session.Entry, leafID string, metadata map[string]any) error
}

TreePersister extends SessionPersister with tree-based persistence. Implementations that support v2 sessions should implement this interface.

type TreeSynced

type TreeSynced struct {
	SessionID string
}

TreeSynced is published by the TreeSyncer AFTER it has applied a tree mutation (message append, compaction entry, or clear/re-sync) in response to RunEnded / CompactionEnded / CommandExecuted. The tree-based persistence reactor subscribes to THIS instead of RunEnded so it never snapshots the tree before the latest turn has been appended (fixes a lost-last-turn race).

type TreeSyncer

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

TreeSyncer keeps a session.Tree in sync with agent message mutations. It subscribes to bus events and appends entries to the tree incrementally.

Sync strategy:

  • RunEnded: diff agent messages vs tree, append new entries
  • CompactionEnded: append CompactionEntry, adjust sync point
  • CommandExecuted("clear"): reset tree
  • CommandExecuted (other): re-sync to catch AppendToConversation etc.

func RegisterTreeSyncer

func RegisterTreeSyncer(b EventBus, sctx *SessionContext) *TreeSyncer

RegisterTreeSyncer creates a TreeSyncer and subscribes to bus events. The tree must already be set on sctx.Tree.

func (*TreeSyncer) DisplayMessages

func (ts *TreeSyncer) DisplayMessages() []core.AgentMessage

DisplayMessages returns the full display history: the messages already synced to the tree PLUS any agent messages appended since the last sync (the in-flight turn). The tree only gains a turn's messages after RunEnded, so mid-run it lags by exactly the current turn. Without the tail, a WS reconnect during a run rebuilds from a snapshot missing the just-sent user message and the streaming reply, making them vanish until the run ends.

func (*TreeSyncer) HasMsgID added in v0.21.0

func (ts *TreeSyncer) HasMsgID(msgID string) bool

HasMsgID reports whether this ID belongs to a message that exists anywhere in the session tree (any branch, not just the current path) or in the in-flight turn not synced to the tree yet. Unlike DisplayMessages, which projects the current branch, this is the uniqueness domain for message identities: a message the current branch does not show is still reachable by branching back to it, so its ID is taken.

func (*TreeSyncer) Reset

func (ts *TreeSyncer) Reset(tree *session.Tree, syncCount int)

Reset re-points the syncer at a new tree and sync baseline. Used when the runtime loads a different session in place (TUI session switch), where the cached tree pointer and lastSyncCount would otherwise still reference the previous session.

type TurnEnded

type TurnEnded struct {
	SessionID string
	RunGen    uint64
}

TurnEnded is published at the end of each agent turn.

type TurnStarted

type TurnStarted struct {
	SessionID string
	RunGen    uint64
}

TurnStarted is published at the start of each agent turn (LLM call).

type UndoLastChange

type UndoLastChange struct{ SessionID string }

UndoLastChange pops the last checkpoint and restores files.

type UserMessageAppended added in v0.21.0

type UserMessageAppended struct {
	SessionID string
	RunGen    uint64
	MsgID     string
	Text      string
	Content   []core.Content
}

UserMessageAppended is published when a user prompt is accepted and enters the conversation as a new run (SendPrompt / SendPromptWithContent), so every connected client renders it live instead of waiting for a reload. Mid-run messages are NOT reported here: they travel the queue rail and are announced by Steered on delivery. Internal prompts (goal loop, auto-verify, subagent / bash notifications) carry a Custom source and are excluded too — they already have their own live representation.

Text carries a plain-text prompt; Content carries the full block list of a structured send (attachments plus text). Exactly one of them is populated. MsgID is the stable identifier of the message that lands in history, so clients dedup it against an optimistic echo or a reconnect snapshot.

type UserShellDelivery

type UserShellDelivery string

UserShellDelivery describes how a completed shell escape's output was handed to the agent/conversation.

const (
	// UserShellDeliverySteer means the output was injected into a running
	// agent via SteerAgent (the agent was running or awaiting a permission
	// decision when the command finished).
	UserShellDeliverySteer UserShellDelivery = "steer"
	// UserShellDeliveryAppend means the output was appended to the
	// conversation directly (the agent was idle).
	UserShellDeliveryAppend UserShellDelivery = "append"
	// UserShellDeliveryNone means the output was not delivered anywhere
	// (silent "!!" while the agent was idle).
	UserShellDeliveryNone UserShellDelivery = "none"
)

type UserShellExecuted

type UserShellExecuted struct {
	SessionID string
	Command   string
	Output    string
	ExitCode  int
	TimedOut  bool
	Delivered UserShellDelivery
}

UserShellExecuted is published after a "!" / "!!" shell escape completes and its output has been delivered (or an attempt was made to deliver it). Both TUI and web frontends can render from this single event.

type UserShellResult

type UserShellResult struct {
	Command   string
	Output    string
	ExitCode  int
	TimedOut  bool
	Delivered UserShellDelivery
	// DeliveryErr is set if handing the result to the bus (SteerAgent /
	// AppendToConversation) failed. The shell command itself still ran and
	// Output/ExitCode are valid; callers must surface this, not swallow it.
	DeliveryErr error
}

UserShellResult is the outcome of RunUserShell.

func RunUserShell

func RunUserShell(ctx context.Context, sctx *SessionContext, command string, silent bool) UserShellResult

RunUserShell executes a user-triggered "!" / "!!" shell escape command against the session's working directory and delivers its output to the agent or conversation, matching the semantics of tool.RunShell (process group handling, timeout, head+tail output cap).

Delivery is decided from the session state *after* the command finishes, not when it was launched, to avoid racing a run that completes while the shell command is still executing:

  • agent busy (StateRunning or StatePermission — an approval prompt is still conceptually "busy") and not silent → SteerAgent.
  • agent busy and silent ("!!") → not delivered; don't interrupt a live run/approval with a background command's output.
  • agent idle (or errored) → AppendToConversation, tagged role "shell" when silent, "user" otherwise, so frontends can render distinctly.

The context passed in bounds cancellation (e.g. session shutdown); the timeout is applied internally regardless of ctx's own deadline.

Jump to

Keyboard shortcuts

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