11 KiB
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
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
Task agents normalize into AgentDefinition (src/task/types.ts):
name,description,systemPrompt(required for a valid loaded agent)- optional
tools,spawns,model,thinkingLevel,output,blocking,autoloadSkills,readSummarize,prewalk 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,yieldis auto-addedspawnsaccepts*, CSV, or array- backward-compat behavior: if
spawnsmissing buttoolsincludestask,spawnsbecomes* outputis passed through as opaque schema dataread-summarize: false(parsed asreadSummarize) forces the subagent'sreadtool to return verbatim file content instead of structural summaries —runSubprocessapplies it as aread.summarize.enabled: falseoverride on the subagent's isolated settings (src/task/executor.ts).scoutandlibrarianship with it disabled. Defaults to enabled when the field is absent.prewalk: truestarts the subagent on its resolved model and hands off to the default prewalk target (thesmolrole) at its first edit/write, exactly like the session-level--prewalk; a string value (e.g.prewalk: "@smol"orprewalk: "openai/gpt-5-mini") picks a custom target. Thetask.agentPrewalksettings record (agent name →"on"/"off"/ pattern, toggled per agent from/agentswithP) overrides the frontmatter. Resolution happens inrunSubprocess(src/task/executor.ts); an unresolvable target or a target equal to the starting model skips the hand-off instead of failing the spawn.
Role-backed custom agents
OMP discovers user agents from ~/.omp/agent/agents/*.md and project agents from .omp/agents/*.md.
Give the agent a role alias in frontmatter, then dispatch it by name. For model routing, task dispatch sets only agent; it does not set a worker model:
~/.omp/agent/agents/reviewer.md:
---
name: reviewer
description: Review a change for correctness.
model: "@review"
---
Review the assigned change and report concrete findings.
Set the role mapping in ~/.omp/agent/config.yml:
modelRoles:
review: spark/minimax-m3:high
@review resolves through modelRoles.review. Each modelRoles.<role> value stores a concrete model selector and may append a thinking suffix such as :high (src/config/model-resolver.ts). Changing that mapping affects subsequent task resolutions without editing agent definitions.
For a dispatch, set the agent name and task:
{"tasks":[{"agent":"reviewer","task":"Review the current change and report concrete findings."}]}
/model changes the current or default session selection. It is not the worker-role configuration mechanism; edit modelRoles instead.
Bundled agents
Bundled agents are embedded at build time (src/task/agents.ts) using text imports.
EMBEDDED_AGENT_DEFS defines:
scout,designer,reviewer,librarianfrom prompt filestaskandsonicfrom sharedtask.mdbody plus injected frontmatter; no bundled agent setsprewalk— the generictaskagent's hand-off is armed by thetask.prewalksetting (default off), or per agent via/agents/task.agentPrewalk/ user agent 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
discoverAgents(cwd, home) (src/task/discovery.ts) merges agents from OMP-native roots and Claude plugin roots before appending bundled definitions. Cross-harness roots such as .claude/agents, .codex/agents, and .gemini/agents are intentionally skipped — their frontmatter schema is not the OMP task-agent contract (TASK_AGENT_CONFIG_SOURCE = ".omp" filters both dir lists).
Discovery inputs
- Nearest project
.ompagents dir fromfindAllNearestProjectConfigDirs("agents", cwd)(filtered to.omp; first hit only) - User
.ompagents dir fromgetConfigDirs("agents", { project: false })(filtered to.omp; first hit only) - Claude plugin roots (
listClaudePluginRoots(home, cwd)) withagents/subdirs — only whenisProviderEnabled("claude-plugins"); project-scope plugins sort before user-scope - Bundled agents (
loadBundledAgents())
Actual source order
- project
.omp/agents - user
~/.omp/agent/agents - plugin
agents/dirs (project-scope first, then user-scope) - bundled agents last
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
.ompoverrides user.omp. - 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
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
Lookup is exact-name linear search:
getAgent(agents, name)=>agents.find(a => a.name === name)
In spawn execution (TaskTool.#executeSync → #runSpawn):
- agents are rediscovered at execution 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
TaskTool.create() builds the tool description from discovery results at initialization time. #executeSync rediscovers agents, so the runtime set can differ from what was listed in the earlier tool description if agent files changed mid-session. The async entry path still uses the initialization-time list to decide whether an agent is marked blocking before scheduling.
Model and structured-output precedence
Runtime model precedence is resolved by resolveEffectiveSubagentPolicy():
task.agentModelOverrides[agentName]- agent frontmatter
model - the parent session model fallback
Runtime output schema precedence is:
- the task item's explicit
outputSchema - agent frontmatter
output - parent session
outputSchema
The task item's optional schemaMode overrides the parent session mode; the default is permissive.
The model-facing prompt (src/prompts/tools/task.md) no longer carries the old structured-output mismatch warning; it tags read-only agents and warns against offloading reasoning to scout/sonic instead.
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
An agent can be discoverable but still unavailable to run because of execution guardrails.
Disabled-agent settings
TaskTool.#executeSync checks task.disabledAgents after resolving the agent. If the requested name is disabled, execution returns an immediate error listing enabled alternatives when available.
Parent spawn policy
TaskTool.#executeSync 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
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)
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 behavior
When parent plan mode is enabled, TaskTool.#runSpawn builds an effectiveAgent before launching subprocesses:
- prepends the plan-mode subagent system prompt
- restricts tools to
read,search,find,lsp, andweb_search, plusast_grepwhen the agent's own tool list declares it (PLAN_MODE_AGENT_TOOL_ALLOWLIST) - clears child spawns
- clears
prewalk(read-only exploration must not receive the prewalk plan/implement nudges)
The same effectiveAgent is used for subprocess launch, model/thinking overrides, and output-schema selection.