daemon

package
v0.2.9-alpha.2 Latest Latest
Warning

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

Go to latest
Published: May 24, 2026 License: MIT Imports: 12 Imported by: 0

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

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

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

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

func Names

func Names() []tools.ToolName

Names lists every tool name this package contributes. Profile constructors concat this into their DeferredTools list (see internal/agent/profiles.go).

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"
	KindLSP        DaemonKind = "lsp"

	// 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

type Event struct {
	Line    string
	Closing bool
}

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 LSPMeta

type LSPMeta struct {
	ServerName   string
	Command      string
	State        string // "running", "starting", "error", etc.
	ExitCode     *int
	RestartCount int
	MaxRestarts  int
}

LSPMeta is the payload for KindLSP snapshots.

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 (t *ListTool) Description() string

func (*ListTool) Execute

func (t *ListTool) Execute(_ context.Context, logger *slog.Logger, raw json.RawMessage) (tools.Result, error)

func (*ListTool) Name

func (t *ListTool) Name() string

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

type LocalBashMeta struct {
	Command  string
	ExitCode *int
	Output   string
}

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

type MonitorMeta struct {
	Command     string
	EventCount  int
	Persistent  bool
	RecentLines []string
}

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 NewOutput

func NewOutput(state *DaemonState) *OutputTool

NewOutput constructs the tool.

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

func NewEventSignal(d Daemon, line string, closing bool) Signal

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) IsEvent

func (s Signal) IsEvent() bool

IsEvent reports whether this is a stream-event signal.

func (Signal) IsLifecycle

func (s Signal) IsLifecycle() bool

IsLifecycle reports whether this is a lifecycle transition signal.

type StopTool

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

StopTool terminates one daemon by id. Idempotent on already-terminal ids.

func NewStop

func NewStop(state *DaemonState) *StopTool

NewStop constructs the tool.

func (*StopTool) Description

func (t *StopTool) Description() string

func (*StopTool) Execute

func (t *StopTool) Execute(ctx context.Context, logger *slog.Logger, raw json.RawMessage) (tools.Result, error)

func (*StopTool) Name

func (t *StopTool) Name() string

func (*StopTool) Schema

func (t *StopTool) Schema() json.RawMessage

Jump to

Keyboard shortcuts

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