subagents

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jun 10, 2026 License: MIT Imports: 18 Imported by: 0

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

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

func ProjectAgentsDir(cwd string) string

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

func TranscriptDirFor(cwd string) (string, error)

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

func UserAgentsDir() (string, error)

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

func (r *Registry) ActiveCount() int

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

func (r *Registry) ActiveForegroundCount() int

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

func (r *Registry) Add(t *Task)

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

func (r *Registry) AppendActivity(id, line string)

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

func (r *Registry) Cancel(id string) bool

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

func (r *Registry) CancelAll() int

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

func (r *Registry) FindByPrefix(prefix string) (*Task, bool)

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

func (r *Registry) Get(id string) (*Task, bool)

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

func (r *Registry) List() []Task

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

func (r *Registry) SetContextUsage(id string, tokens, window int)

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

func (r *Registry) SetToolCalls(id string, n int)

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

func (r *Registry) WaitFor(id string) <-chan struct{}

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.

func (Task) Duration

func (t Task) Duration() time.Duration

Duration returns how long the task ran. For still-running tasks, it returns the time since Started.

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`.

Jump to

Keyboard shortcuts

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