- Home
- Documentation
- runtime-tools
- Task Agent Discovery and Selection
Task Agent Discovery and Selection
Task Agent Discovery and Selection
Section titled “Task Agent Discovery and Selection”This document describes how the task subsystem discovers agent definitions, merges multiple sources, and resolves a requested agent at execution time.
It covers runtime behavior as implemented today, including precedence, invalid-definition handling, and spawn/depth constraints that can make an agent effectively unavailable.
Implementation files
Section titled “Implementation files”src/task/discovery.tssrc/task/agents.tssrc/task/types.tssrc/task/index.tssrc/task/commands.tssrc/prompts/agents/task.mdsrc/prompts/tools/task.mdsrc/discovery/helpers.tssrc/config.tssrc/task/executor.ts
Agent definition shape
Section titled “Agent definition shape”Task agents normalize into AgentDefinition (src/task/types.ts):
name,description,systemPrompt(required for a valid loaded agent)- optional
tools,spawns,model,thinkingLevel,output source:"bundled" | "user" | "project"- optional
filePath
Parsing comes from frontmatter via parseAgentFields() (src/discovery/helpers.ts):
- missing
nameordescription=> invalid (null), caller treats as parse failure toolsaccepts CSV or array; if provided,submit_resultis auto-addedspawnsaccepts*, CSV, or array- backward-compat behavior: if
spawnsmissing buttoolsincludestask,spawnsbecomes* outputis passed through as opaque schema data
Bundled agents
Section titled “Bundled agents”Bundled agents are embedded at build time (src/task/agents.ts) using text imports.
EMBEDDED_AGENT_DEFS defines:
explore,plan,designer,reviewerfrom prompt filestaskandquick_taskfrom sharedtask.mdbody plus injected frontmatter
Loading path:
loadBundledAgents()parses embedded markdown withparseAgent(..., "bundled", "fatal")- results are cached in-memory (
bundledAgentsCache) clearBundledAgentsCache()is test-only cache reset
Because bundled parsing uses level: "fatal", malformed bundled frontmatter throws and can fail discovery entirely.
Filesystem and plugin discovery
Section titled “Filesystem and plugin discovery”discoverAgents(cwd, home) (src/task/discovery.ts) merges agents from multiple places before appending bundled definitions.
Discovery inputs
Section titled “Discovery inputs”- User config agent dirs from
getConfigDirs("agents", { project: false }) - Nearest project agent dirs from
findAllNearestProjectConfigDirs("agents", cwd) - Claude plugin roots (
listClaudePluginRoots(home)) withagents/subdirs - Bundled agents (
loadBundledAgents())
Actual source order
Section titled “Actual source order”Source-family order comes from getConfigDirs("", { project: false }), which is derived from priorityList in src/config.ts:
.xcsh.claude.codex.gemini
For each source family, discovery order is:
- nearest project dir for that source (if found)
- user dir for that source
After all source-family dirs, plugin agents/ dirs are appended (project-scope plugins first, then user-scope).
Bundled agents are appended last.
Important caveat: stale comments vs current code
Section titled “Important caveat: stale comments vs current code”discovery.ts header comments still mention .pi and do not mention .codex/.gemini. Actual runtime order is driven by src/config.ts and currently uses .xcsh, .claude, .codex, .gemini.
Merge and collision rules
Section titled “Merge and collision rules”Discovery uses first-wins dedup by exact agent.name:
- A
Set<string>tracks seen names. - Loaded agents are flattened in directory order and kept only if name unseen.
- Bundled agents are filtered against the same set and only added if still unseen.
Implications:
- Project overrides user for same source family.
- Higher-priority source family overrides lower (
.xcshbefore.claude, etc.). - Non-bundled agents override bundled agents with the same name.
- Name matching is case-sensitive (
Taskandtaskare distinct). - Within one directory, markdown files are read in lexicographic filename order before dedup.
Invalid/missing agent file behavior
Section titled “Invalid/missing agent file behavior”Per directory (loadAgentsFromDir):
- unreadable/missing directory: treated as empty (
readdir(...).catch(() => [])) - file read or parse failure: warning logged, file skipped
- parse path uses
parseAgent(..., level: "warn")
Frontmatter failure behavior comes from parseFrontmatter:
- parse error at
warnlevel logs warning - parser falls back to a simple
key: valueline parser - if required fields are still missing,
parseAgentFieldsfails, thenAgentParsingErroris thrown and caught by caller (file skipped)
Net effect: one bad custom agent file does not abort discovery of other files.
Agent lookup and selection
Section titled “Agent lookup and selection”Lookup is exact-name linear search:
getAgent(agents, name)=>agents.find(a => a.name === name)
In task execution (TaskTool.execute):
- agents are rediscovered at call time (
discoverAgents(this.session.cwd)) - requested
params.agentis resolved throughgetAgent - missing agent returns immediate tool response:
Unknown agent "...". Available: ...- no subprocess runs
Description vs execution-time discovery
Section titled “Description vs execution-time discovery”TaskTool.create() builds the tool description from discovery results at initialization time (buildDescription).
execute() rediscoveres agents again. So the runtime set can differ from what was listed in the earlier tool description if agent files changed mid-session.
Structured-output guardrails and schema precedence
Section titled “Structured-output guardrails and schema precedence”Runtime output schema precedence in TaskTool.execute:
- agent frontmatter
output - task call
params.schema - parent session
outputSchema
(effectiveOutputSchema = effectiveAgent.output ?? outputSchema ?? this.session.outputSchema)
Prompt-time guardrail text in src/prompts/tools/task.md warns about mismatch behavior for structured-output agents (explore, reviewer): output-format instructions in prose can conflict with built-in schema and produce null outputs.
This is guidance, not hard runtime validation logic in discoverAgents.
Command discovery interaction
Section titled “Command discovery interaction”src/task/commands.ts is parallel infrastructure for workflow commands (not agent definitions), but it follows the same overall pattern:
- discover from capability providers first
- deduplicate by name with first-wins
- append bundled commands if still unseen
- exact-name lookup via
getCommand
In src/task/index.ts, command helpers are re-exported with agent discovery helpers. Agent discovery itself does not depend on command discovery at runtime.
Availability constraints beyond discovery
Section titled “Availability constraints beyond discovery”An agent can be discoverable but still unavailable to run because of execution guardrails.
Parent spawn policy
Section titled “Parent spawn policy”TaskTool.execute checks session.getSessionSpawns():
"*"=> allow any""=> deny all- CSV list => allow only listed names
If denied: immediate Cannot spawn '...'. Allowed: ... response.
Blocked self-recursion env guard
Section titled “Blocked self-recursion env guard”PI_BLOCKED_AGENT is read at tool construction. If request matches, execution is rejected with recursion-prevention message.
Recursion-depth gating (task tool availability inside child sessions)
Section titled “Recursion-depth gating (task tool availability inside child sessions)”In runSubprocess (src/task/executor.ts):
- depth computed from
taskDepth task.maxRecursionDepthcontrols cutoff- when at max depth:
tasktool is removed from child tool list- child
spawnsenv is set to empty
So deeper levels cannot spawn further tasks even if the agent definition includes spawns.
Plan mode caveat (current implementation)
Section titled “Plan mode caveat (current implementation)”TaskTool.execute computes an effectiveAgent for plan mode (prepends plan-mode prompt, forces read-only tool subset, clears spawns), but runSubprocess is called with agent rather than effectiveAgent.
Current effect:
- model override / thinking level / output schema are derived from
effectiveAgent - system prompt and tool/spawn restrictions from
effectiveAgentare not passed through in this call path
This is an implementation caveat worth knowing when reading plan-mode behavior expectations.