subagents

package
v0.24.4 Latest Latest
Warning

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

Go to latest
Published: Oct 9, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package subagents: the process-wide active-subagent counter and the terminal display-prefix builder (SP-141 phase 4, increment 1). The counter is read by the CLI status footer via GetActiveSubagents; the prefix shapes the "[persona]" / "[persona:taskID]" terminal labels. pkg/agent forwards both so existing call sites are unchanged.

Package subagents: subagent lifecycle-event emission (SP-141 phase 4, increment 2). PublishLifecycleEventWithCost emits the subagent_activity event (queued/started/completed/cancelled) on the shared EventBus and mirrors it to the runlog. It is pure over the EventBus + runlog — no *Agent state — so it lives here; pkg/agent keeps the thin *SubagentRunner method wrappers that resolve the shared EventBus.

Package subagents: subagent execution-timeout resolution (SP-141 phase 4, increment 2). ResolveSubagentTimeout picks the effective timeout (explicit > SPROUT_TOOL_TIMEOUT floor > persona tier default), EnvSubagentTimeout reads the env override, and IsOrchestratorPersona identifies the orchestrator persona (the longer tier) via the config catalog. The subagent construction that calls this cannot leave pkg/agent (it writes unexported *Agent fields), so pkg/agent keeps thin *SubagentRunner method wrappers that resolve the shared config manager and delegate here.

Package subagents: the subagent data types (SP-141 phase 4, increment 1). The wire/result types (SubagentReturn, SubagentResult, SubagentStatus, FileChange, SubagentOptions, SubagentTask, ...) and their pure helpers live here, out of the god package. The execution machinery (SubagentRunner and its methods) stays in the pkg/agent parent until the runner seam lands; pkg/agent re-exports these types as aliases so the existing call sites are unchanged.

Index

Constants

View Source
const DefaultSubagentTimeout = 30 * time.Minute

DefaultSubagentTimeout bounds a subagent run when the caller didn't set an explicit timeout. Generous enough for locally-hosted models (which may be slower than cloud APIs) while still bounding the worst case.

View Source
const OrchestratorSubagentTimeout = time.Hour

OrchestratorSubagentTimeout bounds the orchestrator persona when it is itself spawned as a subagent. It delegates to nested subagents and drives multi-phase workflows, so a full hour avoids cutting it short mid-delegation.

View Source
const ProgressLogCap = 50

ProgressLogCap bounds the per-run progress log. Beyond this, the buffer becomes head-trimmed (oldest entries dropped) so the LLM always sees the most recent activity.

Variables

This section is empty.

Functions

func AppendSubagentPreamble

func AppendSubagentPreamble(prompt string) string

appendSubagentPreamble appends the shared subagent operating rules to a persona system prompt. Idempotent: if the marker is already present (e.g. a user-configured prompt that inlined it), the prompt is returned unchanged.

func BuildSubagentPrefix

func BuildSubagentPrefix(persona, taskID string) string

buildSubagentPrefix returns the terminal prefix for a subagent based on persona and taskID. For single subagents (taskID starting with "subagent-"), returns "[{persona}]". For parallel subagents (other taskIDs), returns "[{persona}:{taskID}]".

func CompactCount

func CompactCount(n int) string

compactCount renders 12500 → "12.5k", 1_500_000 → "1.5M".

func DecrementActiveSubagents

func DecrementActiveSubagents()

DecrementActiveSubagents lowers the active-subagent counter when a subagent finishes (success, error, cancel — any terminal state).

func EnvSubagentTimeout

func EnvSubagentTimeout() time.Duration

EnvSubagentTimeout reads SPROUT_TOOL_TIMEOUT (seconds). Returns 0 when unset or unparseable so callers fall back to the persona defaults.

func GetActiveSubagents

func GetActiveSubagents() int

GetActiveSubagents returns the current number of running subagents.

func IncrementActiveSubagents

func IncrementActiveSubagents()

IncrementActiveSubagents bumps the active-subagent counter; paired with DecrementActiveSubagents under a defer in the spawner.

func IsOrchestratorPersona

func IsOrchestratorPersona(cm *configuration.Manager, persona string) bool

IsOrchestratorPersona reports whether the given persona string resolves to the canonical orchestrator persona (by ID or alias) via the config catalog. A nil manager or config returns false so callers fall back to defaults.

func IsOutputComplete

func IsOutputComplete(r *SubagentResult) bool

IsOutputComplete returns true when a subagent's result represents substantive, actionable output rather than an empty or suspiciously brief response. A result is complete only when ALL of these hold:

  • No error
  • Not cancelled, budget-exceeded, or truncated
  • Trimmed output is at least outputCompleteThreshold bytes

The zero-value SubagentResult (Error=nil, Output="") returns false because the length check fails — this is the safe default for early-return paths that never explicitly set OutputComplete.

func PrintParallelSubagentStart

func PrintParallelSubagentStart(count int, provider, model string)

printParallelSubagentStart announces a batch of parallel subagents.

func PrintSubagentDone

func PrintSubagentDone(persona string, res *SubagentResult)

printSubagentDone announces completion with a compact stat summary so the user gets closure on a delegation (files touched, tokens, cost, tools, time) rather than the run finishing silently. Severity glyph reflects outcome.

func PrintSubagentStart

func PrintSubagentStart(persona, provider, model string)

printSubagentStart announces a delegated subagent run with a consistent action glyph instead of the old literal "[~] Spawning subagent" prefix.

func PublishLifecycleEvent

func PublishLifecycleEvent(bus *events.EventBus, parentCallID, taskID, persona, status, reason string, tokensUsed int, elapsedMs int64)

PublishLifecycleEvent emits a subagent_activity event with a status field describing the lifecycle transition. The event is only published when the EventBus is available.

func PublishLifecycleEventWithCost

func PublishLifecycleEventWithCost(bus *events.EventBus, parentCallID, taskID, persona, status, reason string, tokensUsed int, elapsedMs int64, cost float64)

PublishLifecycleEventWithCost is the extended form used when the runner has the per-subagent cost in hand (typically at "completed"/"cancelled"). Kept as a separate entry point so the existing call sites that only have the lifecycle transition remain a one-liner; the original signature is preserved.

func ResolveSubagentTimeout

func ResolveSubagentTimeout(cm *configuration.Manager, opts SubagentOptions) time.Duration

ResolveSubagentTimeout returns the effective execution timeout for a subagent run: an explicit caller-set timeout always wins; otherwise the orchestrator persona (by canonical ID or alias) gets a full hour, a persona with a time budget gets that budget plus the wrap-up grace, and every other persona gets the 30-minute default. Alias resolution goes through the config catalog so it stays in sync with the persona definitions.

Automation workflows can raise both defaults via SPROUT_TOOL_TIMEOUT (seconds) — `sprout automate run` sets it from the workflow's subagent_timeout_seconds field. The env value applies as a floor-multiple: subagents get the value directly when it exceeds their tier default; orchestrators get it when it exceeds the orchestrator default. (Implementation: see EnvSubagentTimeout below.)

cm is the shared config manager; nil (no shared state, or no config manager) falls back to the persona defaults rather than panicking.

Types

type FileChange

type FileChange struct {
	Path string `json:"path"`
	Op   string `json:"op"` // "created" | "modified" | "deleted"
}

FileChange is a single tracked write/edit/delete from a subagent run.

type ProgressEntry

type ProgressEntry struct {
	OffsetMS int64  `json:"offset_ms"`
	Phase    string `json:"phase"`
	Message  string `json:"message"`
}

ProgressEntry is the envelope-facing form of SubagentProgressEntry, kept separately from the runner-internal type so the runner struct can change without affecting the wire shape.

type SharedState

type SharedState struct {
	EventBus      *events.EventBus
	TodoManager   *tools.TodoManager
	ConfigManager *configuration.Manager
	WorkspaceRoot string
}

SharedState holds resources shared between parent and subagents

type SubagentError

type SubagentError struct {
	Status SubagentStatus
	Reason string
}

SubagentError is an in-process error value carrying both the terminal Status and a free-form Reason. Returned alongside the JSON envelope when callers in Go-land want to switch on the failure mode without re-parsing the result JSON.

func (*SubagentError) Error

func (e *SubagentError) Error() string

type SubagentMetrics

type SubagentMetrics struct {
	Active            int64 // Currently executing subagents
	Queued            int64 // Waiting for semaphore slot
	Completed         int64 // Successfully completed
	Failed            int64 // Completed with error
	Cancelled         int64 // Cancelled (parent ctx or budget)
	TotalQueuedWaitMS int64 // Cumulative milliseconds spent waiting in queue
}

SubagentMetrics tracks operational metrics for the subagent runner.

type SubagentOptions

type SubagentOptions struct {
	Persona                string        // "coder", "tester", "debugger", etc.
	Model                  string        // optional model override
	Provider               string        // optional provider override
	SystemPrompt           string        // optional system prompt override
	MaxTokens              int           // token budget (0 = unlimited)
	Timeout                time.Duration // execution timeout; <=0 defaults to 30 minutes, 1 hour for the orchestrator persona (see runTask)
	WorkingDir             string        // optional: override workspace root (must be within $HOME)
	MaxConcurrentSubagents int           // max parallel subagents (0 = unlimited, default unlimited)
	FleetTokenBudget       int           // shared token budget across all parallel subagents (0 = unlimited)
	// Role is the role the subagent's usage is attributed to
	// (the role its model was resolved through; a configuration.Role*
	// constant). Set by the spawn handler from the same resolution that
	// picked the provider/model; empty → createSubagent attributes the
	// subagent to the coder role (the default subagent resolution).
	Role string
	// Quiet suppresses streaming the subagent's output to the terminal. Used
	// for background runs, whose output would otherwise interleave with the
	// primary agent's. Activity events are still published.
	Quiet bool
}

SubagentOptions configures an in-process subagent

type SubagentProgressEntry

type SubagentProgressEntry struct {
	OffsetMS int64  `json:"offset_ms"` // ms since subagent started
	Phase    string `json:"phase"`     // "spawn" | "output" | "complete"
	Message  string `json:"message"`
}

SubagentProgressEntry is one timeline entry from a subagent run. Kept minimal to avoid bloating the envelope the primary's LLM sees.

type SubagentResult

type SubagentResult struct {
	ID         string
	Output     string
	Error      error
	TokensUsed int
	Cost       float64
	// Role is the role the subagent's model was resolved
	// through. The parent rolls the subagent's usage up under this role so
	// its spend is attributed to the purpose it served (empty → the coder
	// fallback, matching subagentRole's default resolution).
	Role string
	// PromptTokens / CompletionTokens are the subagent's prompt/completion
	// token split. Carried so the parent's rollup attributes the subagent's
	// real token counts to its role instead of a zero-split aggregate.
	PromptTokens     int
	CompletionTokens int
	ToolCalls        int
	// Iterations is the assistant-turn count consumed by this subagent
	// run. Surfaced to the primary via SubagentRunMetrics.Iterations so
	// the model has visibility into how many LLM rounds a delegated task
	// burned.
	Iterations     int
	Elapsed        time.Duration
	Cancelled      bool
	BudgetExceeded bool // true if task was skipped because fleet budget was already exceeded before starting
	Truncated      bool // true if subagent was cut short due to fleet budget exceeded mid-run
	// OutputComplete signals whether the subagent produced a substantive
	// final response. false when the output is empty or suspiciously brief
	// (under 50 trimmed chars) despite a clean exit — the orchestrator can
	// use this to decide whether to retry, escalate, or accept. This is
	// distinct from Error/Cancelled/BudgetExceeded (all of which also set
	// it false): OutputComplete focuses specifically on "did the subagent
	// actually say something useful?"
	OutputComplete bool
	// FileChanges is the manifest of writes/edits this subagent performed,
	// captured via its own ChangeTracker. nil when tracking wasn't
	// initialized for this run.
	FileChanges []changes.TrackedFileChange
	// ProgressLog is a per-run timeline of notable subagent events
	// (spawn, output, complete). Surfaced to the primary's LLM via the
	// SubagentReturn envelope so the model can reason about *what* the
	// subagent did, not just the final assistant message. Capped to
	// ProgressLogCap entries.
	ProgressLog []SubagentProgressEntry
}

SubagentResult is the structured output from a subagent

type SubagentReturn

type SubagentReturn struct {
	// Output is the subagent's final assistant message (was: "stdout").
	Output string `json:"stdout"`
	// Stderr carries the subagent's terminal error message if any.
	Stderr string `json:"stderr"`
	// ExitCode is "0" on success, "1" otherwise. Kept as string for
	// shape-compat with the legacy resultMap.
	ExitCode string `json:"exit_code"`
	// Completed is "true" on natural completion, "false" if cancelled.
	Completed string `json:"completed"`
	// TimedOut is "true" if the run hit its timeout.
	TimedOut string `json:"timed_out"`
	// BudgetExceeded is "true" if the run hit its token budget.
	BudgetExceeded string `json:"budget_exceeded"`
	// ElapsedSeconds is the wall-clock duration, formatted "%.1f".
	ElapsedSeconds string `json:"elapsed_seconds"`
	// TokensUsed is the rolled-up token count (string for shape-compat).
	TokensUsed string `json:"tokens_used"`
	// Cost is the rolled-up dollar cost (string for shape-compat).
	Cost string `json:"cost"`
	// ToolCallCount is the number of tool calls the subagent made.
	ToolCallCount string `json:"tool_calls"`
	// Summary is JSON-stringified human-readable highlights (file ops,
	// build/test status, errors). Kept for shape-compat.
	Summary string `json:"summary,omitempty"`
	// ContextUsed is "true" / "false" reflecting whether the subagent
	// received the parent's context bundle.
	ContextUsed string `json:"context_used,omitempty"`
	// FilesUsed is the parent-provided files-of-interest list.
	FilesUsed string `json:"files_used,omitempty"`
	// WorkingDir is the directory the subagent executed under.
	WorkingDir string `json:"working_dir,omitempty"`

	// Status is the terminal state. Always populated, even on success.
	Status SubagentStatus `json:"status"`
	// ErrorReason carries free-form context when Status != completed.
	ErrorReason string `json:"error_reason,omitempty"`
	// FilesModified is the change-tracker-sourced manifest. nil when
	// change tracking is disabled (caller treats nil as "not reported").
	FilesModified []FileChange `json:"files_modified,omitempty"`
	// Metrics is the structured token/cost rollup. Mirror of the
	// TokensUsed/Cost/ToolCallCount string fields above for callers
	// that prefer typed access.
	Metrics SubagentRunMetrics `json:"metrics"`
	// ProgressLog is a capped timeline of subagent activity events
	// (spawn / output / complete) so the primary's LLM can reason about
	// what the subagent actually did, not just its final assistant
	// message. nil when no events were captured.
	ProgressLog []ProgressEntry `json:"progress_log,omitempty"`
}

SubagentReturn is the typed envelope a subagent tool call returns to the primary's LLM. It marshals to JSON with backward-compatible keys for the old map[string]string shape so existing LLM behavior keeps working, plus new typed fields (status, files_modified, metrics) for callers that want them.

func (*SubagentReturn) MarshalJSONIndent

func (r *SubagentReturn) MarshalJSONIndent() (string, error)

MarshalJSONIndent renders the envelope as a 2-space-indented JSON string for the tool result.

type SubagentRunMetrics

type SubagentRunMetrics struct {
	TokensUsed int     `json:"tokens_used"`
	Cost       float64 `json:"cost"`
	ToolCalls  int     `json:"tool_calls"`
	Iterations int     `json:"iterations"`
}

SubagentRunMetrics is the structured token/cost accounting for a subagent run. Sourced directly from SubagentResult (subagent_runner.go), not by regex-scraping stdout. Iterations is the assistant-turn count, exposed so the primary's LLM can reason about how much budget a delegated task burned.

type SubagentStatus

type SubagentStatus string

SubagentStatus enumerates terminal states of a subagent run. Replaces the legacy SUBAGENT_SECURITY_ERROR / SUBAGENT_TOKEN_BUDGET_EXCEEDED / SUBAGENT_FAILED sentinel string prefixes — those literals are retained in the human-readable Output so any LLM behavior keyed on the legacy shape still works, but in-process callers should switch to Status.

const (
	SubagentStatusCompleted       SubagentStatus = "completed"
	SubagentStatusCancelled       SubagentStatus = "cancelled"
	SubagentStatusTimedOut        SubagentStatus = "timed_out"
	SubagentStatusBudgetExceeded  SubagentStatus = "budget_exceeded"
	SubagentStatusSecurityBlocked SubagentStatus = "security_blocked"
	SubagentStatusFailed          SubagentStatus = "failed"
)

type SubagentTask

type SubagentTask struct {
	ID         string
	Prompt     string
	Model      string
	Provider   string
	Persona    string
	WorkingDir string // optional: override workspace root
	// SystemPrompt is the resolved persona system prompt; empty falls back to
	// SubagentOptions.SystemPrompt.
	SystemPrompt string
	// Role is the role this task's subagent usage is
	// attributed to (the role its model was resolved through). Empty → the
	// coder role (createSubagent's fallback). Set by the caller that
	// resolved the model (e.g. the reviewer path sets "reviewer").
	Role string
}

SubagentTask represents a single parallel subagent task

Jump to

Keyboard shortcuts

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