MusePi

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


Agent definition shape

Task agents normalize into AgentDefinition (src/task/types.ts):

Parsing comes from frontmatter via parseAgentFields() (src/discovery/helpers.ts):

Role-backed custom agents

musepi discovers user agents from ~/.musepi/agent/agents/*.md and project agents from .musepi/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:

~/.musepi/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 ~/.musepi/agent/config.yml:

modelRoles:
  review: openai/gpt-5.4: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:

{
  "context": "Review the current change in this repository.",
  "tasks": [
    { "agent": "reviewer", "task": "Report concrete correctness findings." }
  ]
}

/model’s Roles view can assign and persist custom role mappings such as review, fast, and good. Changing only the active or default session selection does not remap those roles.

Watch running agents

After dispatch, press Alt+A to open Agent Hub. Its live roster shows each task agent’s status, current activity, model, age, and usage. Select an agent to read its transcript and steer it directly; parked agents can be revived from the same view.

vibe_spawn tier routing

vibe_spawn maps fast to bundled sonic and good to bundled task. Both resolve through task.agentModelOverrides before their bundled agent model defaults (src/vibe/runtime.ts, src/task/agents.ts).

Route these tiers through roles by keeping aliases in task.agentModelOverrides and concrete selectors only in modelRoles:

task:
  agentModelOverrides:
    sonic: "@fast_worker"
    task: "@good_worker"
modelRoles:
  fast_worker: openai/gpt-5-mini
  good_worker: openai/gpt-5.4:high

The vibe_spawn cli remains fast or good; update modelRoles to change the worker model.

Bundled agents

Bundled agents are embedded at build time (src/task/agents.ts) using text imports.

EMBEDDED_AGENT_DEFS defines:

Loading path:

  1. loadBundledAgents() parses embedded markdown with parseAgent(..., "bundled", "fatal")
  2. results are cached in-memory (bundledAgentsCache)
  3. 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 = ".musepi" filters both dir lists).

Discovery inputs

  1. Nearest project .musepi agents dir from findAllNearestProjectConfigDirs("agents", cwd) (filtered to .musepi; first hit only)
  2. User .musepi agents dir from getConfigDirs("agents", { project: false }) (filtered to .musepi; first hit only)
  3. Claude plugin roots (listClaudePluginRoots(home, cwd)) with agents/ subdirs — only when isProviderEnabled("claude-plugins"); project-scope plugins sort before user-scope
  4. Bundled agents (loadBundledAgents())

Actual source order

  1. project .musepi/agents
  2. user ~/.musepi/agent/agents
  3. plugin agents/ dirs (project-scope first, then user-scope)
  4. bundled agents last

Merge and collision rules

Discovery uses first-wins dedup by exact agent.name:

Implications:

Invalid/missing agent file behavior

Per directory (loadAgentsFromDir):

Frontmatter failure behavior comes from parseFrontmatter:

Net effect: one bad custom agent file does not abort discovery of other files.

Agent lookup and selection

Lookup is exact-name linear search:

In spawn execution (TaskTool.#executeSync#runSpawn):

  1. agents are rediscovered at execution time (discoverAgents(this.session.cwd))
  2. requested params.agent is resolved through getAgent
  3. 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():

  1. task.agentModelOverrides[agentName]
  2. agent frontmatter model
  3. the parent session model fallback

Runtime output schema precedence is:

  1. the task item’s explicit outputSchema
  2. agent frontmatter output
  3. 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:

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():

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):

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:

The same effectiveAgent is used for subprocess launch, model/thinking overrides, and output-schema selection.