Documentation
¶
Overview ¶
Package subagents provides foreground and background subagent spawning for task delegation. A subagent is a fully independent agent loop with its own LLM client, tool registry, and message history. It inherits the parent's config, persona, grimoire, and permissions but runs in isolation. Subagents may run in the foreground (blocking) or transition to background after a configurable threshold (#30), delivering their result via a channel.
Index ¶
- Variables
- func MergeWorktree(repo string, wt *Worktree) error
- func RemoveWorktree(repo string, wt *Worktree) error
- type DAGEntry
- type Finding
- type Mailbox
- type Manager
- func (m *Manager) Close()
- func (m *Manager) GetRun(id string) (*SubagentRun, bool)
- func (m *Manager) Kill(selector string) bool
- func (m *Manager) ListRuns() []*SubagentRun
- func (m *Manager) Resume(ctx context.Context, checkpointID string, turnCb TurnCallback) (*SubagentRun, error)
- func (m *Manager) Spawn(ctx context.Context, goal string, workspace string, turnCb ...TurnCallback) (*SubagentRun, error)
- func (m *Manager) SpawnAsync(ctx context.Context, goal, workspace string, opts SpawnOptions) (<-chan *SubagentRun, *SubagentRun)
- func (m *Manager) SpawnWithOptions(ctx context.Context, goal string, workspace string, opts SpawnOptions) (*SubagentRun, error)
- func (m *Manager) UseParent(p loop.Nester, warn func(string))
- type Message
- type PostMessageTool
- func (t *PostMessageTool) Description() string
- func (t *PostMessageTool) Execute(_ context.Context, input map[string]any, _ chan<- tools.ProgressEvent) (tools.ToolResult, error)
- func (t *PostMessageTool) InterruptBehavior() tools.InterruptBehavior
- func (t *PostMessageTool) IsConcurrencySafe(input map[string]any) bool
- func (t *PostMessageTool) IsReadOnly() bool
- func (t *PostMessageTool) Name() string
- func (t *PostMessageTool) Parameters() json.RawMessage
- func (t *PostMessageTool) ValidateInput(input map[string]any) error
- type Profile
- type Result
- type SpawnAgentTool
- func (t *SpawnAgentTool) Description() string
- func (t *SpawnAgentTool) Execute(ctx context.Context, input map[string]any, progress chan<- tools.ProgressEvent) (tools.ToolResult, error)
- func (t *SpawnAgentTool) InterruptBehavior() tools.InterruptBehavior
- func (t *SpawnAgentTool) IsConcurrencySafe(input map[string]any) bool
- func (t *SpawnAgentTool) IsReadOnly() bool
- func (t *SpawnAgentTool) Name() string
- func (t *SpawnAgentTool) Parameters() json.RawMessage
- func (t *SpawnAgentTool) Timeout() time.Duration
- func (t *SpawnAgentTool) ValidateInput(input map[string]any) error
- type SpawnOptions
- type SubagentRun
- type SubmitResultTool
- func (t *SubmitResultTool) Description() string
- func (t *SubmitResultTool) Execute(_ context.Context, input map[string]any, _ chan<- tools.ProgressEvent) (tools.ToolResult, error)
- func (t *SubmitResultTool) InterruptBehavior() tools.InterruptBehavior
- func (t *SubmitResultTool) IsConcurrencySafe(map[string]any) bool
- func (t *SubmitResultTool) IsReadOnly() bool
- func (t *SubmitResultTool) Name() string
- func (t *SubmitResultTool) Parameters() json.RawMessage
- func (t *SubmitResultTool) ValidateInput(map[string]any) error
- type TurnCallback
- type Type
- type Worktree
Constants ¶
This section is empty.
Variables ¶
var StaggerDelay = 500 * time.Millisecond
StaggerDelay is the configurable pause between concurrent subagent launches to avoid hitting provider rate limits. Zero means no delay. Default 500ms — safe for xAI's grok-build-0.1 at 6 concurrent agents.
Functions ¶
func MergeWorktree ¶
MergeWorktree merges the worktree's branch back into the repo's current branch. Returns an error on conflict so the caller can surface it.
func RemoveWorktree ¶
RemoveWorktree force-removes the worktree dir and deletes its branch.
Types ¶
type DAGEntry ¶
type DAGEntry struct {
Run *SubagentRun
Goal string // the full goal including persona overrides
Workspace string
TurnCb TurnCallback
MaxTurns int
IsolateWorktree bool
ResultCh chan *SubagentRun // result sent here when complete
// contains filtered or unexported fields
}
DAGEntry is a queued subagent waiting for dependencies to clear.
type Finding ¶
type Finding struct {
Title string `json:"title"`
Detail string `json:"detail,omitempty"`
Severity string `json:"severity,omitempty"`
File string `json:"file,omitempty"`
Line int `json:"line,omitempty"`
}
Finding is one thing a typed subagent found.
type Mailbox ¶
type Mailbox struct {
// contains filtered or unexported fields
}
Mailbox is a concurrency-safe per-address message store keyed by an agent's address (its element name). Drain is destructive.
type Manager ¶
type Manager struct {
// OnBackgroundComplete is an optional callback invoked when a backgrounded
// subagent finishes. The TUI may set this to surface a completion notice.
// It is called from a goroutine; implementations must be goroutine-safe.
// nil = no notification (default; non-TUI callers are unaffected).
OnBackgroundComplete func(*SubagentRun)
// contains filtered or unexported fields
}
Manager handles subagent lifecycle and execution.
func NewManager ¶
NewManager creates a subagent manager. Pass isChild=true when the manager itself is running inside a subagent to block recursive spawning.
func (*Manager) Close ¶
func (m *Manager) Close()
Close releases the subagents' shared environment (one UseParent supplied stays open: its owner closes it). Warnings stop first, so none reaches the sink after Close, even from the environment's own shutdown. Every in-flight run — running, background, or waiting in the DAG queue — is cancelled next: a background run is detached (context.Background(), so only /agents kill or Close stops it) and otherwise keeps a reference on the shared Env forever, so the owner's own Close (MCP.Stop, code graph Close) never runs (F2e M1). Close waits up to closeRunWait for cancelled runs to finish and release that reference before returning, so a caller that closes its own Env right after Close sees the shared resources actually freed. A spawn after Close fails cleanly.
func (*Manager) GetRun ¶
func (m *Manager) GetRun(id string) (*SubagentRun, bool)
GetRun returns a subagent run by ID.
func (*Manager) Kill ¶
Kill cancels a specific in-flight subagent (task 6ffb5a7c). The selector may be the run id, the task id, OR the element/display name the user sees in /agents (e.g. "mizu" or "water") — users refer to agents by the name on screen, not the internal id (#d15ac448). Returns true if a cancellable run was found. The run's context is cancelled (which the runtime honors — task 349f1f14) and its status is marked failed so ListRuns reflects the kill immediately. A run whose cancel is gone (it finished and was merged or discarded) is left untouched and returns false.
func (*Manager) ListRuns ¶
func (m *Manager) ListRuns() []*SubagentRun
ListRuns returns all subagent runs, most recent first.
func (*Manager) Resume ¶
func (m *Manager) Resume(ctx context.Context, checkpointID string, turnCb TurnCallback) (*SubagentRun, error)
Resume continues a previously-failed subagent from its last checkpoint. checkpointID is the RunID returned by RunGoal and stored in SubagentRun.CheckpointID. It constructs a runner with the same options as executeSubagent so the resumed run shares the same workspace, turn limits, and callback wiring.
func (*Manager) Spawn ¶
func (m *Manager) Spawn(ctx context.Context, goal string, workspace string, turnCb ...TurnCallback) (*SubagentRun, error)
Spawn creates and runs a subagent with the given goal. It blocks until the subagent completes or the context is cancelled.
func (*Manager) SpawnAsync ¶
func (m *Manager) SpawnAsync(ctx context.Context, goal, workspace string, opts SpawnOptions) (<-chan *SubagentRun, *SubagentRun)
SpawnAsync starts a subagent in a goroutine and returns a buffered channel that receives the final *SubagentRun when it completes, plus the live run handle (status "running"). The channel has capacity 1 so the exec goroutine never blocks or leaks if the caller abandons the channel. The caller may wait on the channel or ignore it (e.g., after transitioning to background).
SpawnAsync does NOT handle DAG dependencies; use SpawnWithOptions for that.
func (*Manager) SpawnWithOptions ¶
func (m *Manager) SpawnWithOptions(ctx context.Context, goal string, workspace string, opts SpawnOptions) (*SubagentRun, error)
SpawnWithOptions creates and runs a subagent with full options including DAG dependencies. If depends_on contains task IDs that haven't completed yet, the subagent blocks in "waiting" state until all dependencies clear, then auto-starts.
When opts.BackgroundAfter > 0, the call blocks up to that duration waiting for the subagent to finish. If the subagent is still running at the threshold, it transitions to "background" status and SpawnWithOptions returns the live (background) run immediately so the parent can resume. The result is later delivered via a goroutine that updates the run state and invokes m.OnBackgroundComplete (if set).
Default behavior (BackgroundAfter == 0) is unchanged: fully blocking.
func (*Manager) UseParent ¶
UseParent makes every subagent nest under p and report its own setup and hook warnings to warn. The chat passes its Env (2.0 F2e): subagents share its MCP clients, hooks (and its session_id) and code graph instead of starting their own. The manager never closes p; its owner does. Call it before the first spawn.
type PostMessageTool ¶
type PostMessageTool struct {
// contains filtered or unexported fields
}
PostMessageTool is a built-in tool that allows a subagent to post a message to another subagent's mailbox. The recipient is identified by its element name (e.g. "fire", "water"). Messages are delivered at spawn time when the recipient agent next starts (spawn-time injection; live mid-run injection is a follow-up, see issue #31).
func NewPostMessageTool ¶
func NewPostMessageTool(manager *Manager, from string) *PostMessageTool
NewPostMessageTool creates a post_message tool backed by the given manager. from should be the element name of the agent that holds this tool instance, or "parent" when the tool is held by the top-level orchestrator.
func (*PostMessageTool) Description ¶
func (t *PostMessageTool) Description() string
func (*PostMessageTool) Execute ¶
func (t *PostMessageTool) Execute(_ context.Context, input map[string]any, _ chan<- tools.ProgressEvent) (tools.ToolResult, error)
func (*PostMessageTool) InterruptBehavior ¶
func (t *PostMessageTool) InterruptBehavior() tools.InterruptBehavior
func (*PostMessageTool) IsConcurrencySafe ¶
func (t *PostMessageTool) IsConcurrencySafe(input map[string]any) bool
func (*PostMessageTool) IsReadOnly ¶
func (t *PostMessageTool) IsReadOnly() bool
func (*PostMessageTool) Name ¶
func (t *PostMessageTool) Name() string
func (*PostMessageTool) Parameters ¶
func (t *PostMessageTool) Parameters() json.RawMessage
func (*PostMessageTool) ValidateInput ¶
func (t *PostMessageTool) ValidateInput(input map[string]any) error
type Profile ¶
type Profile struct {
Allow func(tools.Tool) bool // nil: every tool
PersonaLevel prompts.PersonaLevel
Model string
}
Profile is what a Type sets on a subagent's run.
type Result ¶
type Result struct {
Summary string `json:"summary"`
Findings []Finding `json:"findings"`
Files []string `json:"files"`
Warning string `json:"warning,omitempty"`
}
Result is what a typed subagent hands its parent: {summary, findings[], files[]}, plus a warning when the run never called submit_result.
type SpawnAgentTool ¶
type SpawnAgentTool struct {
// contains filtered or unexported fields
}
SpawnAgentTool is a built-in tool that allows the model to spawn subagents for task delegation. The subagent runs in the foreground (blocks until complete) and returns its final result.
func NewSpawnAgentTool ¶
func NewSpawnAgentTool(manager *Manager) *SpawnAgentTool
NewSpawnAgentTool creates a new spawn_agent tool backed by the given manager.
func (*SpawnAgentTool) Description ¶
func (t *SpawnAgentTool) Description() string
func (*SpawnAgentTool) Execute ¶
func (t *SpawnAgentTool) Execute(ctx context.Context, input map[string]any, progress chan<- tools.ProgressEvent) (tools.ToolResult, error)
func (*SpawnAgentTool) InterruptBehavior ¶
func (t *SpawnAgentTool) InterruptBehavior() tools.InterruptBehavior
func (*SpawnAgentTool) IsConcurrencySafe ¶
func (t *SpawnAgentTool) IsConcurrencySafe(input map[string]any) bool
func (*SpawnAgentTool) IsReadOnly ¶
func (t *SpawnAgentTool) IsReadOnly() bool
func (*SpawnAgentTool) Name ¶
func (t *SpawnAgentTool) Name() string
func (*SpawnAgentTool) Parameters ¶
func (t *SpawnAgentTool) Parameters() json.RawMessage
func (*SpawnAgentTool) Timeout ¶
func (t *SpawnAgentTool) Timeout() time.Duration
Timeout gives a subagent 10 minutes; it manages its own turn limits.
func (*SpawnAgentTool) ValidateInput ¶
func (t *SpawnAgentTool) ValidateInput(input map[string]any) error
type SpawnOptions ¶
type SpawnOptions struct {
TaskID string // user-assigned task ID for DAG references
DependsOn []string // task IDs that must complete before this starts
TurnCb TurnCallback // nested progress callback
MaxTurns int // 0 = default (20)
IsolateWorktree bool // run this subagent in its own git worktree (#32)
BackgroundAfter time.Duration // >0: auto-transition to background if the subagent runs longer than this (#30). 0 = always foreground (unchanged).
Sliders *config.SliderConfig // persona override for the subagent's voice modulation; nil = slider.json
Type Type // subagent type (2.0 W4e); "" = untyped
}
SpawnOptions holds optional parameters for Spawn.
type SubagentRun ¶
type SubagentRun struct {
ID string `json:"id"`
TaskID string `json:"task_id,omitempty"` // user-assigned ID for DAG dependencies
Name string `json:"name"` // display name (e.g., "火 hi")
Element string `json:"element"` // english element (e.g., "fire")
Goal string `json:"goal"`
Workspace string `json:"workspace"`
Status string `json:"status"` // "waiting", "running", "background", "completed", "failed"
DependsOn []string `json:"depends_on,omitempty"` // task_ids that must complete first
Result string `json:"result"`
Error string `json:"error,omitempty"`
StartedAt time.Time `json:"started_at"`
EndedAt time.Time `json:"ended_at,omitempty"`
Turns int `json:"turns"`
CheckpointID string `json:"checkpoint_id,omitempty"` // run id to resume from on failure
// Type is the subagent type spawn_agent asked for (2.0 W4e); "" for a
// run spawned without one (Manager.Spawn), which keeps the untyped
// behaviour.
Type Type `json:"type,omitempty"`
// Summary is a typed run's result summary (Result holds its JSON), for
// /agents. Empty for an untyped run.
Summary string `json:"summary,omitempty"`
// contains filtered or unexported fields
}
SubagentRun tracks the state and result of a spawned subagent.
type SubmitResultTool ¶
type SubmitResultTool struct {
// contains filtered or unexported fields
}
SubmitResultTool records a typed subagent's result (2.0 W4e). It only records, so it is read-only: every type keeps it and it never prompts.
func NewSubmitResultTool ¶
func NewSubmitResultTool(h *resultHolder) *SubmitResultTool
NewSubmitResultTool returns a submit_result tool that stores into h.
func (*SubmitResultTool) Description ¶
func (t *SubmitResultTool) Description() string
func (*SubmitResultTool) Execute ¶
func (t *SubmitResultTool) Execute(_ context.Context, input map[string]any, _ chan<- tools.ProgressEvent) (tools.ToolResult, error)
func (*SubmitResultTool) InterruptBehavior ¶
func (t *SubmitResultTool) InterruptBehavior() tools.InterruptBehavior
func (*SubmitResultTool) IsConcurrencySafe ¶
func (t *SubmitResultTool) IsConcurrencySafe(map[string]any) bool
func (*SubmitResultTool) IsReadOnly ¶
func (t *SubmitResultTool) IsReadOnly() bool
func (*SubmitResultTool) Name ¶
func (t *SubmitResultTool) Name() string
func (*SubmitResultTool) Parameters ¶
func (t *SubmitResultTool) Parameters() json.RawMessage
func (*SubmitResultTool) ValidateInput ¶
func (t *SubmitResultTool) ValidateInput(map[string]any) error
ValidateInput accepts everything: Execute validates, so the model gets every problem back as a tool result it can fix.
type TurnCallback ¶
TurnCallback is called on each subagent turn so the parent can display nested progress. turn is 1-indexed, toolName is the tool being called on this turn (empty if the turn is a text response).
type Type ¶
type Type string
Type is a subagent type (2.0 W4e): it picks the subagent's tools, model and whether its persona is composed.
const ( // TypeExplore investigates: read-only tools, the off persona, the small // model. TypeExplore Type = "explore" // TypeGeneral does the work: every tool, the persona, the agent model. TypeGeneral Type = "general" // TypeReview reviews code: read and code-graph tools, the off persona, // the agent model. TypeReview Type = "review" )