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) AppendActivity(id, line string)
- func (r *Registry) AttachCancel(id string, cancel context.CancelFunc)
- func (r *Registry) Cancel(id string) bool
- func (r *Registry) CancelAll() int
- func (r *Registry) FindByPrefix(prefix string) (*Task, bool)
- func (r *Registry) Get(id string) (*Task, bool)
- func (r *Registry) List() []Task
- func (r *Registry) MarkDone(id string, status TaskStatus, result string, errored bool, tokensUsed int)
- func (r *Registry) SetContextUsage(id string, tokens, window int)
- func (r *Registry) SetToolCalls(id string, n int)
- func (r *Registry) WaitFor(id string) <-chan struct{}
- type Task
- 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) 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) 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) 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) 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) 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) 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
TokensUsed int
ToolCalls int // count of ToolStart events from the child
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
// 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
// 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.
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`.