Documentation
¶
Overview ¶
Package subagents owns typed-subagent configuration, the per-session task registry for background runs, and the discovery + parsing of agent definition files (`.yottacode/agents/*.md` and `~/.yottacode/agents/*.md`). The agent loop itself imports this package to look up `Agent` tool invocations; nothing here imports the agent package, which keeps the dependency direction clean.
Index ¶
- func NewTaskID() string
- func ProjectAgentsDir(cwd string) string
- func TranscriptDirFor(cwd string) (string, error)
- func UserAgentsDir() (string, error)
- type AgentConfig
- type LoadResult
- type Registry
- func (r *Registry) ActiveCount() int
- func (r *Registry) ActiveForegroundCount() int
- func (r *Registry) Add(t *Task)
- func (r *Registry) AddUsage(id string, u *adapter.Usage)
- func (r *Registry) AppendActivity(id, line string)
- func (r *Registry) AttachCancel(id string, cancel context.CancelFunc)
- func (r *Registry) BatchActiveCount(batchID string) int
- func (r *Registry) Cancel(id string) bool
- func (r *Registry) CancelAll() int
- func (r *Registry) CancelBatch(batchID string) int
- func (r *Registry) CommittingCount() int
- func (r *Registry) Export() []TaskRecord
- func (r *Registry) FindByPrefix(prefix string) (*Task, bool)
- func (r *Registry) Get(id string) (*Task, bool)
- func (r *Registry) Import(records []TaskRecord)
- func (r *Registry) IncrementCompactionCount(id string)
- func (r *Registry) List() []Task
- func (r *Registry) MarkDone(id string, status TaskStatus, result string, errored bool, tokensUsed int)
- func (r *Registry) SetCommitting(id string, committing bool)
- func (r *Registry) SetContextUsage(id string, tokens, window int)
- func (r *Registry) SetToolCalls(id string, n int)
- func (r *Registry) TotalTokensUsed() int
- func (r *Registry) TryReserve(t *Task, max int, countForegroundOnly bool) bool
- func (r *Registry) TryReserveBatch(tasks []*Task, max int, countForegroundOnly bool) bool
- func (r *Registry) WaitFor(id string) <-chan struct{}
- type Task
- type TaskRecord
- type TaskStatus
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NewTaskID ¶
func NewTaskID() string
NewTaskID returns a short hex-encoded random ID suitable for a transcript filename and for prefix-matching by /subagents stop. 8 bytes → 16 hex chars; collisions within a single session are vanishingly unlikely.
func ProjectAgentsDir ¶
ProjectAgentsDir returns the per-project agents dir: <cwd>/.yottacode/agents. Project-local definitions are checked in here so a team can ship a repo-specific `Explore`/`Plan` flavor alongside the codebase. Project wins on name collision with the user-scope dir.
func TranscriptDirFor ¶
TranscriptDirFor resolves where subagent run transcripts get persisted: <project memory dir>/subagents/ — i.e. ~/.yottacode/memory/projects/<slug>/subagents/. Transcripts nest inside the project's memory dir so every per-project artifact is discoverable from one `ls ~/.yottacode/memory` tree; the memory loader skips subdirectories, so transcript .md files never load as memories. Root resolution (incl. the $YOTTACODE_HOME override) is owned by memory.ProjectMemoryDir.
func UserAgentsDir ¶
UserAgentsDir returns the global agents dir: $YOTTACODE_HOME/agents (when the env var is set) or ~/.yottacode/agents otherwise — the shared ychome.Dir resolution, so all global state lives under the same root regardless of override.
Types ¶
type AgentConfig ¶
type AgentConfig struct {
Name string // subagent_type the parent uses in the Agent tool call
Description string // shown to the parent model as part of the Agent tool schema
Tools []string // optional tool allowlist; ["*"] or nil means "inherit all parent tools (minus Agent)"
Model string // optional adapter model override; empty means inherit parent's
Prompt string // markdown body — used as the child's system prompt
Background bool // when true, dispatches default to background unless the caller opts in to foreground
Source string // "builtin" | "global" | "project" — diagnostics only
SourcePath string // absolute path of the source file (empty for builtins)
}
AgentConfig is one typed-subagent definition. The body of the source file becomes the child's system prompt; the frontmatter declares metadata + an optional tool allowlist + an optional model override.
func Find ¶
func Find(configs []AgentConfig, name string) *AgentConfig
Find returns the named config or nil. Case-sensitive: agent names are validated against agentNamePattern so casing is part of identity.
func LoadBuiltins ¶
func LoadBuiltins() []AgentConfig
LoadBuiltins parses every embedded *.md file under builtins/ and returns the resulting AgentConfig set. The Source is always "builtin" so warnings/diagnostics can distinguish embedded vs disk-loaded definitions. Parse errors here would be ship-blocking (the embedded files are part of the binary), so we panic — the test suite catches any bad frontmatter before release.
func ParseAgentFile ¶
func ParseAgentFile(data []byte) (AgentConfig, error)
ParseAgentFile parses one agents/*.md file into an AgentConfig. Returns an error when the frontmatter is missing or required fields are blank — the caller is expected to surface the error with the file path so the user can fix it.
Frontmatter format (YAML-ish, deliberately tolerant of hand-edits; matches the approach internal/memory uses):
--- name: Explore description: Fast read-only search agent for locating code. tools: [read_file, grep, glob] # optional; "*" or absent = inherit model: claude-haiku-4-5 # optional --- <markdown body — used as the child's system prompt>
func (AgentConfig) ToolAllowed ¶
func (c AgentConfig) ToolAllowed(name string) bool
ToolAllowed reports whether the named tool should be exposed to the child registry. Returns true when Tools is nil/empty (inherit all) or when the name is in the allowlist. Caller is still responsible for the unconditional Agent/exit_plan_mode exclusion — this method only encodes the per-config allowlist semantics.
type LoadResult ¶
type LoadResult struct {
Configs []AgentConfig
Warnings []string
}
LoadResult is what LoadAll returns: a deduplicated, ordered slice of configs (project > global > builtin precedence) plus a slice of human-readable warnings the caller can surface to the user. Warnings are non-fatal — a single malformed file shouldn't block startup.
func LoadAll ¶
func LoadAll(cwd string, validToolNames map[string]bool) (LoadResult, error)
LoadAll resolves agent definitions from all three sources and merges them with project > global > builtin precedence. validToolNames is the set of names the live tool registry exposes — any agent that references a tool outside this set emits a warning and that name is silently dropped from the allowlist. Pass nil to skip allowlist validation entirely (used by tests that don't care).
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry tracks active and historical subagent tasks for the current session. Mutex-guarded so the agent goroutine, the TUI render path, and slash commands can all touch it safely. Snapshots returned by List() are deep-enough copies that callers can hold them indefinitely.
waiters maps task id → list of channels signaled (closed) when the task transitions out of Running via MarkDone. The get_subagent_result tool registers a channel here when called with wait_seconds > 0, then blocks on it instead of polling. Channels are closed (not sent on) so multiple waiters per task work naturally — closing broadcasts to every receiver.
func NewRegistry ¶
func NewRegistry() *Registry
NewRegistry returns an empty Registry ready for concurrent use.
func (*Registry) ActiveCount ¶
ActiveCount returns the number of tasks currently in TaskRunning. Used by the TUI to render a status-bar indicator and to enforce the background concurrency cap.
func (*Registry) ActiveForegroundCount ¶ added in v0.3.0
ActiveForegroundCount returns the number of foreground tasks currently in TaskRunning. Used by AgentTool.Execute to enforce the foreground concurrency cap when the parent fans out multiple Agent calls in one assistant message (now parallel-safe).
func (*Registry) Add ¶
Add inserts a task. The ID is expected to be unique within the session; Add overwrites silently rather than failing so callers don't need to bother with conflict handling for short hex suffixes.
func (*Registry) AddUsage ¶ added in v0.4.0
AddUsage accumulates one assistant turn's exact provider-reported token usage onto the task's running Usage total. Called each turn from the runner as it forwards the child loop's AssistantMessage event — mirroring what the main TUI loop does with session.AddUsage for the parent thread, so subagent spend is captured with the same fidelity. Nil-safe (a turn whose adapter reported no usage is a no-op) and a no-op for unknown ids.
func (*Registry) AppendActivity ¶
AppendActivity adds a short status string to the task's ring buffer. Oldest entries drop when the ring fills.
func (*Registry) AttachCancel ¶
func (r *Registry) AttachCancel(id string, cancel context.CancelFunc)
AttachCancel records the cancel func so /subagents stop has a way to interrupt the child loop. Must be called before the goroutine starts; later AttachCancel calls overwrite.
func (*Registry) BatchActiveCount ¶ added in v0.4.0
BatchActiveCount returns how many tasks in the named dispatch batch are still TaskRunning. The TUI reads this to decide whether a batch's background completions are ready to wake the model: workers finish at staggered times, and waking once per worker would burn a turn each. Zero means the batch has fully drained (or the id is unknown). An empty batchID counts nothing — non-dispatch tasks carry no BatchID and must never be batch-gated.
func (*Registry) Cancel ¶
Cancel invokes the task's cancel func (if any) so the underlying agent.Turn goroutine returns at the next context check. Marks CanceledByUser=true so the runner's outcome message can attribute the cancellation to /subagents stop rather than to a parent-turn cancellation or a context deadline. The MarkDone(TaskCanceled) follow-up is the responsibility of the goroutine itself when it observes the canceled context.
func (*Registry) CancelAll ¶ added in v0.3.0
CancelAll invokes every running task's cancel func, signaling all in-flight subagents — foreground AND detached background workers — to stop at their next context check. Used on session shutdown so background workers (which run on context.Background() to survive the parent turn) don't leak their goroutines and provider SSE streams past TUI exit. Returns the number of tasks signaled. Like Cancel, it does not mark the tasks done — each goroutine does that when it observes the canceled context; callers that need to wait for the drain can poll ActiveCount.
func (*Registry) CancelBatch ¶ added in v0.4.0
CancelBatch cancels every running member of one dispatch batch, leaving tasks outside it untouched. A batch is up to 8 workers, and stopping one by one via Cancel means finding and typing each id while the rest keep burning tokens. Returns the number signaled. Same contract as Cancel: the goroutines mark themselves done when they observe the canceled context. An empty batchID cancels nothing — batch-less tasks must never be swept up.
func (*Registry) CommittingCount ¶ added in v0.4.0
CommittingCount returns how many running tasks are mid-commit. Session teardown polls this to extend its drain only while real work is at risk, rather than making every quit pay a longer worst-case wait.
func (*Registry) Export ¶ added in v0.4.0
func (r *Registry) Export() []TaskRecord
Export returns a serializable snapshot of every task for persistence in the session file, oldest-first. Taken under the read lock so it's safe to call alongside live registry mutation.
func (*Registry) FindByPrefix ¶
FindByPrefix is a typing-friendly accessor for /subagents stop: it returns the task whose ID starts with the given prefix. Returns (nil, false) when there's no match OR multiple matches (caller renders the disambiguation message). Matches Get's snapshot semantics.
func (*Registry) Get ¶
Get returns a snapshot of the named task or (nil, false). The returned pointer references a copy, so callers can read its fields without holding the registry lock — but mutating it has no effect on the stored task.
func (*Registry) Import ¶ added in v0.4.0
func (r *Registry) Import(records []TaskRecord)
Import rehydrates persisted task records as HISTORICAL entries so a prior session's task-ids resolve (get_subagent_result, /subagents) after a restart. A record still Running when its session ended is unattachable — it's marked errored-orphaned so it reads as "didn't finish" rather than a phantom live task. Existing tasks with the same id are not overwritten, and the records don't count toward this session's concurrency cap (terminal) or token budget (historical). Typically called once at startup on an empty registry.
func (*Registry) IncrementCompactionCount ¶ added in v0.4.0
IncrementCompactionCount records one successful firing of the child's own in-loop compaction (agent.ContextCompacted with Err == nil) — the runner calls this from its child-event loop alongside the existing activity-line rendering, so /usage can show a subagent's own compaction history instead of it being visible only as a transient activity string. A no-op for unknown ids.
func (*Registry) List ¶
List returns a snapshot of every task, newest-first. Callers can retain the slice without locking.
func (*Registry) MarkDone ¶
func (r *Registry) MarkDone(id string, status TaskStatus, result string, errored bool, tokensUsed int)
MarkDone updates the task's terminal state and signals every waiter blocked on WaitFor. Idempotent: calling MarkDone twice keeps the first finish time + status and overwrites only the Result/Errored fields if the caller passes different values. The cancel func is cleared so a later Cancel() call is a no-op rather than crashing.
Waiters are signaled by *closing* their channels (not sending on them), which fans out to every receiver and makes the channels reusable on multiple `<-ch` receives. Both the running→terminal transition AND a no-op MarkDone on an already-finished task close any registered waiters — defensive against late waiters that registered just before MarkDone ran.
func (*Registry) SetCommitting ¶ added in v0.4.0
SetCommitting marks (or clears) a task's commit-in-flight window. See Task.Committing — session teardown consults it before giving up on a drain.
func (*Registry) SetContextUsage ¶ added in v0.3.0
SetContextUsage records the subagent's current context size + window so the live dock can render a fill bar. Called each iteration from the runner as it forwards the child loop's ContextUsage event.
func (*Registry) SetToolCalls ¶
SetToolCalls records the final tool-call count for a task. Called by the runner just before MarkDone so the card / list can render an accurate ACTS value without inspecting the activities ring (which is bounded and may have dropped entries).
func (*Registry) TotalTokensUsed ¶ added in v0.4.0
TotalTokensUsed sums the estimated TokensUsed across every task in the session (running tasks contribute 0 until MarkDone records their estimate). The Agent tool reads this to enforce a session-wide subagent token budget — a backstop against unbounded fan-out spend that the per-wave concurrency cap can't bound. O(n) over the task set, called once per spawn; n is small.
func (*Registry) TryReserve ¶ added in v0.4.0
TryReserve atomically inserts t only if the relevant Running count is still below max, doing the count AND the insert under a single lock — so concurrent reservations (e.g. N parallel Agent calls in one assistant message) cannot each pass a separate check-then-Add and overshoot the cap. Returns false WITHOUT inserting when the class is already at/over max.
countForegroundOnly selects the class: true counts only foreground Running tasks (the foreground cap); false counts ALL Running tasks (the background cap, which bounds total concurrency — matching the historical ActiveCount()-based check). t.Status should be TaskRunning.
func (*Registry) TryReserveBatch ¶ added in v0.4.0
TryReserveBatch is the all-or-nothing variant of TryReserve for a group of tasks that must be admitted together: it counts the Running class ONCE and inserts every task, or inserts none. Dispatch needs this because it spawns N workers from a single call — a per-task TryReserve loop could admit some and reject the rest, leaving a half-built batch whose worktrees are already on disk, and a check-then-Add (count here, insert in the spawned goroutines) reopens the very race TryReserve exists to close.
countForegroundOnly selects the class, same as TryReserve. Every task's Status should be TaskRunning. Returns false WITHOUT inserting anything when the batch would push the class past max.
func (*Registry) WaitFor ¶
WaitFor returns a channel that is closed when the task transitions out of Running (via MarkDone). The channel is closed (never sent on) so multiple goroutines can wait simultaneously and unblock together. If the task is already terminal at call time, the returned channel is closed immediately so the caller's select case fires on the first iteration — no special-case needed at the call site.
Returns a closed channel for unknown task ids too (rather than nil), so a typo doesn't deadlock the caller. The caller should re-read the task state after the wait returns to decide what "no longer running" actually means.
type Task ¶
type Task struct {
ID string
AgentType string
Prompt string
Started time.Time
Finished time.Time
Status TaskStatus
Result string
Errored bool
Background bool
// NotifyOnDone records that the spawner asked to be re-prompted with
// this task's result when it finishes (the Agent tool's
// notify_on_done). The TUI reads it to decide whether a background
// completion should wake the model with the result, vs. stay silent
// until fetched. Always false for foreground tasks.
NotifyOnDone bool
TokensUsed int
// Usage is the exact, provider-reported token tally accumulated across
// the child's assistant turns (input/output/cache/reasoning). Unlike
// TokensUsed — a single ~4-chars/token estimate stamped at MarkDone and
// used only for the session budget backstop — Usage is updated live each
// turn from the child loop's AssistantMessage event, and is the number
// /usage folds into the session total. Zero (IsZero) for providers that
// don't report usage; readers fall back to the TokensUsed estimate there.
Usage adapter.Usage
ToolCalls int // count of ToolStart events from the child
CompactionCount int // number of times the child's own in-loop compaction fired (agent.ContextCompacted, Err == nil)
Model string // model the child ran on when task-routed; "" = inherited the parent's model
TranscriptPath string
// Branch / Worktree are set for dispatch write-subtasks that run in
// their own git worktree+branch. Empty for ordinary (shared-cwd)
// subagents. The integrate tool merges Branch into the integration
// branch; the TUI shows it per task.
Branch string
Worktree string
// Base is the commit SHA the dispatch worktree branched from. The
// session-exit sweep uses it to decide whether Branch ever gained
// commits (base..HEAD) before reclaiming an empty worktree. Empty for
// non-worktree subagents.
Base string
// BatchID groups the children of one dispatch call so the TUI can
// render them together and the parent can refer to the batch. Empty
// for standalone Agent dispatches.
BatchID string
// Committing marks the window where a dispatch write-worker is staging and
// committing its worktree to Branch. That work deliberately runs on a
// cancellation-detached context so a just-finished worker still saves its
// output — which means canceling the session does NOT stop it, and killing
// the process mid-commit can abandon a half-written index (a stale
// index.lock the user has to clear by hand in a worktree they never knew
// existed). Session teardown reads this to keep waiting while a commit is
// genuinely in flight, instead of abandoning it on a flat deadline.
Committing bool
// CtxTokens / CtxWindow track the subagent's live context usage,
// updated each iteration from the child loop's ContextUsage event.
// CtxWindow is 0 until the first update (and for child models whose
// window couldn't be resolved); the dock shows a fill bar only when
// CtxWindow > 0.
CtxTokens int
CtxWindow int
// CanceledByUser is set when /subagents stop fires for this task.
// Lets the runner distinguish "user explicitly stopped me" from
// "parent turn was canceled" / "context deadline" in the outcome
// message. Reading is safe under the registry lock; runChild
// snapshots via Get().
CanceledByUser bool
Activities []string
// LastActivityAt is when the most recent activity was appended. Lets
// the dock and get_subagent_result distinguish a subagent making steady
// progress from one wedged on a single tool call: a long gap since the
// last activity on a still-Running task is the stall signal. Zero until
// the first activity.
LastActivityAt time.Time
// Historical marks a task rehydrated from a prior session (via Import):
// a resolvable record of past work, not live this-session activity. The
// session-token budget (TotalTokensUsed) skips these, and the TUI's
// dropped-completion reconciliation skips them too, so resuming a session
// neither starts with a drained budget nor re-banners/re-wakes on old
// background results.
Historical bool
// contains filtered or unexported fields
}
Task is one subagent run, foreground or background. The fields are snapshot-friendly (no live channels) so List() can return a copy without holding the registry lock while the caller renders.
func (Task) Duration ¶
Duration returns how long the task ran. For still-running tasks, it returns the time since Started.
func (Task) UsageTokens ¶ added in v0.4.0
UsageTokens returns the exact provider-reported total this subagent consumed (input + output + cache read + cache write), matching the "total tokens" basis /usage uses so every surface tells the same story. Returns 0 when the provider never reported usage (Usage.IsZero) — callers fall back to the ~4-char/token estimate then.
type TaskRecord ¶ added in v0.4.0
type TaskRecord struct {
ID string `json:"id"`
AgentType string `json:"agent_type"`
Prompt string `json:"prompt,omitempty"`
Status TaskStatus `json:"status"`
Result string `json:"result,omitempty"`
Errored bool `json:"errored,omitempty"`
Background bool `json:"background,omitempty"`
NotifyOnDone bool `json:"notify_on_done,omitempty"`
TranscriptPath string `json:"transcript_path,omitempty"`
Started time.Time `json:"started,omitzero"`
Finished time.Time `json:"finished,omitzero"`
TokensUsed int `json:"tokens_used,omitempty"`
Usage adapter.Usage `json:"usage,omitzero"`
ToolCalls int `json:"tool_calls,omitempty"`
CompactionCount int `json:"compaction_count,omitempty"`
Model string `json:"model,omitempty"`
Branch string `json:"branch,omitempty"`
Worktree string `json:"worktree,omitempty"`
Base string `json:"base,omitempty"`
BatchID string `json:"batch_id,omitempty"`
}
TaskRecord is the JSON-serializable summary of a Task persisted in the session file so subagent task-ids survive a restart. The live cancel func, the activity ring, and live-context gauges are intentionally dropped — a rehydrated task is a historical record, not a re-attachable run. The Worktree/Base/Branch fields are kept so a startup sweep can reclaim a crashed session's empty dispatch worktrees.
type TaskStatus ¶
type TaskStatus int
TaskStatus is the lifecycle phase of a subagent run. The four terminal states are completed (clean TurnDone), errored (loop error or model returned an error message), canceled (parent invoked task.Cancel via /subagents stop), and iterCapped (hit MaxIterations without a final reply — surfaced as a soft error).
const ( TaskRunning TaskStatus = iota TaskCompleted TaskErrored TaskCanceled TaskIterCapped )
func (TaskStatus) String ¶
func (s TaskStatus) String() string
String renders the status as a short label suitable for `/subagents list`.