Documentation
¶
Overview ¶
Package daemon is the unified abstraction over every long-running background unit the agent can spawn — bash run_in_background tasks, async subagents, monitor streams, and future kinds (remote_agent, in_process_teammate, local_workflow, dream). It replaces the per-kind stores (BgTaskStore, MonitorTaskStore, SpawnGroup) and the per-kind tools (task_list / task_stop / task_output) with one DaemonState + one Daemon interface + three daemon_* tools.
See docs/design/daemon-design.md for the full RFC.
Index ¶
- Constants
- Variables
- func GenerateID(kind DaemonKind) string
- func IsTerminal(s DaemonStatus) bool
- func Names() []tools.ToolName
- type Daemon
- type DaemonKind
- type DaemonMetadata
- type DaemonSnapshot
- type DaemonState
- func (s *DaemonState) Domain() string
- func (s *DaemonState) DrainSignals() []Signal
- func (s *DaemonState) Emit(sig Signal)
- func (s *DaemonState) Evict(id string)
- func (s *DaemonState) Get(id string) (Daemon, bool)
- func (s *DaemonState) HasPending() bool
- func (s *DaemonState) Len() int
- func (s *DaemonState) Register(d Daemon)
- func (s *DaemonState) Snapshot() []DaemonSnapshot
- func (s *DaemonState) SnapshotByKind(k DaemonKind) []DaemonSnapshot
- func (s *DaemonState) Stop(ctx context.Context, id string) (DaemonSnapshot, error)
- type DaemonStatus
- type Event
- type Lifecycle
- type ListTool
- type LocalAgentMeta
- type LocalBashMeta
- type MonitorMeta
- type OutputTool
- type Signal
- type StopTool
Constants ¶
const ( OpAdded = "added" // Register OpRemoved = "removed" // Evict OpEvent = "event" // Signal.Event variant )
Observable Op values DaemonState emits. Subscribers switch on these. Lifecycle transitions emit the status string directly ("running" / "completed" / "failed" / "killed") so subscribers can match a single case per terminal state.
const Domain = "daemons"
Domain is the observable.Change.Domain value DaemonState emits. The TUI strips and any other subscriber match this string to route renders.
Variables ¶
var ( ErrDaemonNotFound = errors.New("daemon not found") ErrAlreadyTerminal = errors.New("daemon already terminal") )
Errors returned by Stop. Tools type-assert to disambiguate response.
Functions ¶
func GenerateID ¶
func GenerateID(kind DaemonKind) string
GenerateID returns a wire-stable id: kind prefix + 8 base-36 chars. 36^8 ≈ 2.8 trillion combinations — sufficient for the lifetime of any session. Unknown kinds fall back to 'x' so callers don't need a nil check.
func IsTerminal ¶
func IsTerminal(s DaemonStatus) bool
IsTerminal reports whether the status is one a daemon never leaves. Terminal entries are evicted from DaemonState after their Lifecycle signal is drained.
Types ¶
type Daemon ¶
type Daemon interface {
Snapshot() DaemonSnapshot
Kill(ctx context.Context) error
Output() string
}
Daemon is the polymorphic contract every kind of background unit satisfies. Three methods, one for each concern:
- Snapshot — read state without exposing internals.
- Kill — cooperative cancellation (kind-specific: ctx cancel for bash/monitor, abort signal for agent, ...).
- Output — kind-specific formatted text for daemon_output.
Implementations live next to their owning tool: bashDaemon in pkg/tools/shell, monitorDaemon in pkg/tools/monitor, agentDaemon in internal/agent. Each implementation owns its own goroutine and calls DaemonState.Emit on lifecycle transitions / stream lines.
type DaemonKind ¶
type DaemonKind string
DaemonKind identifies the variant of one daemon. Used for ID prefix allocation, filtering in daemon_list, and rendering hints in the TUI.
New kinds plug in by adding one constant here, one metadata struct in snapshot.go, and one file implementing Daemon. Tools / drain / store do not change.
const ( // Implemented today. KindLocalBash DaemonKind = "local_bash" KindLocalAgent DaemonKind = "local_agent" KindMonitor DaemonKind = "monitor" // Reserved — enum entries only, no Daemon impl yet. Listed here so the // ID prefix table stays exhaustive and new kinds land as one-file diffs. KindRemoteAgent DaemonKind = "remote_agent" KindInProcessTeammate DaemonKind = "in_process_teammate" KindLocalWorkflow DaemonKind = "local_workflow" KindDream DaemonKind = "dream" )
type DaemonMetadata ¶
type DaemonMetadata interface {
// contains filtered or unexported methods
}
DaemonMetadata is the marker interface every kind-specific payload implements. Renderers (TUI strips, daemon_output, daemon_list) switch on DaemonSnapshot.Kind and type-assert to the concrete struct.
type DaemonSnapshot ¶
type DaemonSnapshot struct {
ID string
Kind DaemonKind
Status DaemonStatus
Description string
AgentID string // spawning agent's id; used by TUI to label rows by owner
StartedAt time.Time
EndedAt time.Time // zero until terminal
Metadata DaemonMetadata
}
DaemonSnapshot is the immutable view of one daemon's state at a point in time. Stores hand out snapshots by value so observers don't race the goroutine holding the live struct.
Metadata carries kind-specific payload; renderers type-assert it on Kind. New kinds add a struct implementing DaemonMetadata and place it on the snapshot — tools / drain / store stay untouched.
type DaemonState ¶
type DaemonState struct {
*observable.Observable
// contains filtered or unexported fields
}
DaemonState is the single source of truth for one agent's daemons. Replaces the previous trio of BgTaskStore, MonitorTaskStore, and SpawnGroup. Holds:
- daemons map[id]Daemon — for lookup, kill, output.
- signals []Signal — unified queue (Lifecycle + Event), drained at each agent loop iter start.
Embeds *observable.Observable so TUI strips receive per-change notifications with domain="daemons".
func NewState ¶
func NewState(notify func()) *DaemonState
NewState constructs an empty DaemonState. notify is the agent's signal-pump wake function — pass nil in tests that don't need wake semantics.
func (*DaemonState) Domain ¶
func (s *DaemonState) Domain() string
Domain implements observable.Store.
func (*DaemonState) DrainSignals ¶
func (s *DaemonState) DrainSignals() []Signal
DrainSignals pulls every queued signal and clears the queue. Called by the agent loop at iter start; the drain helper folds them into the conversation as <system-reminder> blocks.
func (*DaemonState) Emit ¶
func (s *DaemonState) Emit(sig Signal)
Emit is the entry point for daemon goroutines. Pushes the signal onto the queue, fans an observable Change for the matching Op, and fires the agent's wake-up function so an idle loop boots.
The queue is the durable backstop — even if the wake-up is dropped (channel full), the next iter's drain still picks it up. This ordering invariant (queue first, then notify) is what lets the agent loop's CAS-based wake be lossy without losing daemon results.
func (*DaemonState) Evict ¶
func (s *DaemonState) Evict(id string)
Evict removes a daemon from the catalog. Emits a "removed" Change so strips can drop the row. Called by the agent loop's drain after a terminal Lifecycle signal has been folded into the conversation. No-op when the id is unknown.
func (*DaemonState) Get ¶
func (s *DaemonState) Get(id string) (Daemon, bool)
Get returns the daemon for id. ok=false when unknown.
func (*DaemonState) HasPending ¶
func (s *DaemonState) HasPending() bool
HasPending reports whether any undrained signals remain. The agent loop checks this before releasing the run flag on a terminal turn — pending signals force one more iteration so the model sees the result.
func (*DaemonState) Len ¶
func (s *DaemonState) Len() int
Len returns the number of registered daemons (running + terminal). Cheap; used by HasDaemonState-style accessors and by tests.
func (*DaemonState) Register ¶
func (s *DaemonState) Register(d Daemon)
Register inserts d into the catalog. Emits an "added" observable.Change so TUI strips can render the new chip immediately. No-op when d is nil or its snapshot has an empty id.
Idempotent: a second Register of the same id overwrites silently — daemons own their own transition timing, the store does not enforce it.
func (*DaemonState) Snapshot ¶
func (s *DaemonState) Snapshot() []DaemonSnapshot
Snapshot returns every daemon's snapshot sorted by StartedAt. Used by daemon_list and TUI strips that need a static view.
func (*DaemonState) SnapshotByKind ¶
func (s *DaemonState) SnapshotByKind(k DaemonKind) []DaemonSnapshot
SnapshotByKind returns only the daemons matching the given kind, sorted by StartedAt. Used by daemon_list's kind filter and per-kind TUI strips.
func (*DaemonState) Stop ¶
func (s *DaemonState) Stop(ctx context.Context, id string) (DaemonSnapshot, error)
Stop is daemon_stop's entry point. Looks up id, asserts it's not already terminal, then calls daemon.Kill(ctx). The daemon's own goroutine is responsible for the subsequent terminal Lifecycle emission — Stop does not synthesise it.
Returns the pre-kill snapshot. Errors: ErrDaemonNotFound, ErrAlreadyTerminal, or any error from Kill.
type DaemonStatus ¶
type DaemonStatus string
DaemonStatus is the lifecycle state of one daemon.
Transitions:
Pending → Running (rare — typically a daemon is Running on Register) Running → Completed (clean exit / agent reported success) Running → Failed (non-zero exit / agent crashed) Running → Killed (daemon_stop / root ctx cancel)
Terminal statuses (Completed / Failed / Killed) trigger a Lifecycle Signal that flows into the agent loop's drain, after which the entry is evicted from DaemonState.
const ( StatusPending DaemonStatus = "pending" StatusRunning DaemonStatus = "running" StatusCompleted DaemonStatus = "completed" StatusFailed DaemonStatus = "failed" StatusKilled DaemonStatus = "killed" )
type Event ¶
Event is the variant fired per stream line by daemons that stream (monitors). Closing marks the final event before the daemon's terminal Lifecycle — drain renders it as a distinct phrasing so the model knows no more events are coming.
type Lifecycle ¶
type Lifecycle struct {
Status DaemonStatus
}
Lifecycle is the variant fired when a daemon enters a terminal status.
type ListTool ¶
type ListTool struct {
// contains filtered or unexported fields
}
ListTool enumerates daemons in the agent's DaemonState. Optional kind filter; by default omits terminal entries since the drain evicts them shortly after lifecycle emission.
func NewList ¶
func NewList(state *DaemonState) *ListTool
NewList constructs the tool. state may be nil — Execute reports a clear error in that case so the model gets a useful message instead of a panic.
func (*ListTool) Description ¶
func (*ListTool) Schema ¶
func (t *ListTool) Schema() json.RawMessage
type LocalAgentMeta ¶
type LocalAgentMeta struct {
AgentType string // "general-purpose" / "explore" / "plan" / ...
Prompt string
Async bool
Phase string
Summary string
Err string
}
LocalAgentMeta is the payload for KindLocalAgent snapshots.
Phase is the fine-grained sub-state ("thinking", "executing", "draining", ...) the child agent broadcasts as it works — orthogonal to the coarse DaemonStatus (running / completed / failed / killed). The TUI subagent strip renders both: DaemonStatus picks the chip color, Phase drives the inline glyph.
Summary is populated on terminal Lifecycle with the child agent's final response. Err is populated when the child crashed or was killed.
type LocalBashMeta ¶
LocalBashMeta is the payload for KindLocalBash snapshots.
Output is the captured stdout+stderr tail, capped at the size the bash goroutine enforces (64 KiB). ExitCode is nil while running; set on the terminal Lifecycle.
type MonitorMeta ¶
MonitorMeta is the payload for KindMonitor snapshots.
RecentLines is a ring buffer tail used by daemon_output; events have already been drained into the conversation by the time daemon_output reads, so we keep a small in-memory tail rather than asking the agent to scroll back.
type OutputTool ¶
type OutputTool struct {
// contains filtered or unexported fields
}
OutputTool returns the kind-specific formatted output for one daemon.
func (*OutputTool) Description ¶
func (t *OutputTool) Description() string
func (*OutputTool) Execute ¶
func (t *OutputTool) Execute(_ context.Context, logger *slog.Logger, raw json.RawMessage) (tools.Result, error)
func (*OutputTool) Name ¶
func (t *OutputTool) Name() string
func (*OutputTool) Schema ¶
func (t *OutputTool) Schema() json.RawMessage
type Signal ¶
type Signal struct {
DaemonID string
Kind DaemonKind
At time.Time
Snapshot DaemonSnapshot
Lifecycle *Lifecycle
Event *Event
}
Signal is the unit DaemonState's queue carries from per-daemon goroutines to the agent loop's drain. Exactly one of Lifecycle / Event is non-nil.
Lifecycle fires once per terminal transition (Completed / Failed / Killed) and triggers eviction. Event fires per stream line — used by monitors; kinds that don't stream (bash, local_agent) only emit Lifecycle.
Snapshot is captured at signal-emission time so drain readers see a consistent view even if the daemon is mutating concurrently.
func NewEventSignal ¶
NewEventSignal constructs an Event signal carrying one stream line. Closing=true on the final event before terminal lifecycle.
func NewLifecycleSignal ¶
func NewLifecycleSignal(d Daemon, status DaemonStatus) Signal
NewLifecycleSignal constructs a Lifecycle signal carrying the daemon's current snapshot. Daemons call this from their goroutine right before state.Emit at terminal transition time.
func (Signal) IsLifecycle ¶
IsLifecycle reports whether this is a lifecycle transition signal.