Files
tai/taicpp/docs/cap-agents.md
T

13 KiB

CAP agents, their models, and @agent mentions

Code: src/cap/cap_model_catalog.* (discovery and resolution), src/cap/cap_mention.* (running a turn on an agent), Agent::resolve_tag, Agent::apply_mentions, Agent::run_agent in src/agent/agent.cpp. Tests: tests/cap_model_catalog_test.cpp (step capmcp.catalog). Research: research/cap-protocol/ (CAP, ACP, MCP sampling, delegation as a tool) and research/fuzzy-matching-selectors/. This page is the user guide and the reference for what exists today. The protocol itself (events, bindings, manifest, fleet) is in cap.md; how the running agents are shown and supervised is in orchestration.md; the scoring is in fuzzy.md.

Everything here is a taicpp extension of the Python app except the CAP protocol itself.

1. What you can do

In the interactive session, start a message with @ followed by an agent, an agent and a model, or just a model:

@claude refactor the parser                   run this turn on the claude agent
@claude:opus review the diff                  same, on its opus model
@opus review the diff                         a model name alone works when one agent offers it
@clde:opu review the diff                     typos are fine: the closest agent and model are picked
@claude                                       no text: pin the agent for the following messages
@auto                                         clear every pin (agent, provider, model)

The turn runs in a panel on the multiplexer screen (orchestration.md) and the answer is printed and filed in the conversation. If what was typed differs from what was picked, one line says so:

'tia:zne' resolved to tai:zen-free (fuzzy 0.67)

When nothing fits, the message names the closest candidate and lists every agent with its status, binary, manifest, binding and models (section 6).

2. Which agents exist

An agent is a CAP manifest, a TOML file named <agent>.toml in:

  1. ~/.config/cap/agents/ (user), or
  2. /usr/share/cap-agents/ (system).

The catalog lists the files of both directories (at most CAP_AGENT_MAX = 64, sorted by name) and checks each one before anything uses it:

Status Meaning
ready manifest parses, agent.binary is found on PATH and is executable
no_binary manifest is fine but the binary is missing or not executable
bad_manifest the file does not parse, a required field is missing, a pattern is outside the CAP subset, or the name inside differs from the file name
no_manifest the registry found nothing for that name (cap_manifest_missing)

Only ready agents take part in fuzzy picks. An agent that is not ready is mentioned only when nothing ready fits, and then the message says why it cannot be used.

Manifest resolution itself follows CAP (cap.md, CapRegistry): a cap-agent.toml next to the binary, then the user directory, then the system directory, then <binary> --cap-manifest with a CAP_PROBE_TIMEOUT of 15 s.

A minimal manifest:

[agent]
name = "myagent"
binary = "myagent"
profiles = ["coding"]

[startup]
command = ["myagent"]
ready_when = { pattern = "ready" }
model_arg = "--model"             # how a chosen model is passed to the agent

[pty]
cols = 120
rows = 40

[capabilities]
streaming_output = true

[models]
list = ["myagent-large", "myagent-small"]
default = "myagent-small"

startup.model_arg is CAP's own field: when a model is chosen, the driver appends model_arg model to the start command. If it is missing, the choice is kept but cannot reach the agent and a warning is recorded (CAP behavior, see the opencode manifest in the research archive).

3. Which models an agent has

CAP itself defines no model list (research/cap-protocol/cap-core-v1.md). taicpp adds two sources, merged (duplicates removed, at most CAP_MODEL_MAX = 500 per agent, the manifest list first):

Static, in the manifest

[models]
list = ["a", "b"]        # strings, or tables with id / name / model
default = "a"

Live, by probe (optional)

[models]
probe = ["myagent", "--cap-models"]
# or, to use "<binary> --cap-models":
[capabilities]
models = true

The probe runs without a terminal and without input, is limited to CAP_MODEL_PROBE_TIMEOUT = 5 s and CAP_MODEL_PROBE_MAX_BYTES = 1 MiB, and must exit 0. Accepted output:

["m1", "m2"]                                      a JSON array of ids
[{"id": "m1"}, {"name": "m2"}, {"model": "m3"}]   objects (id, then name, then model)
{"models": ["m1", "m2"], "default": "m1"}         an object with a default
m1\nm2\n# comment\n                               one id per line; blank lines and # lines are skipped

A probe is never trusted to be safe to run for every binary, which is why it only runs when the manifest asks for it. A failed probe (timeout, non-zero exit, no models in the output) never raises: the reason is kept on the agent and shown in the report, and the static list still applies.

Results are cached for CAP_CATALOG_TTL = 60 s and the agents are loaded in parallel, so a miss costs one probe round, not one per agent in sequence.

tai --cap-models is not implemented on tai's own side; tai's manifest declares no models. For ACP agents a live list exists in the protocol (a model config option at session setup) and is the recommended next source (research conclusion); it is not built.

4. How a mention is resolved

Order inside Agent::resolve_tag (first hit wins), for the word after @:

  1. a bot name or nickname,
  2. a provider label, then molodetz, then a configured backend label,
  3. a model id already known to the application (exact),
  4. auto,
  5. an exact agent name, or agent:... whose first part is an exact agent name,
  6. an existing file path,
  7. the fuzzy catalog (below),
  8. otherwise an unknown tag (the usual error, plus the agent report when agents exist).

Steps 1 to 6 never go through fuzzy matching, so a real bot, provider, model or file is never taken over by an agent. A file is checked before the fuzzy step so @README.md is never rerouted.

The fuzzy catalog (CapModelCatalog::resolve) works on the text as typed:

  1. Exact: an agent whose name equals the text (case-insensitive), or any ready agent's model id equal to the text. A matching agent that is not ready fails here with its reason.
  2. head:tail (split at the first :): head is matched against the names of the ready agents with a floor of CAP_FUZZY_FLOOR = 0.3. If one is found:
    • empty tail: that agent, its default model;
    • the agent has no model list: the tail is passed through unchecked, with a note saying so;
    • otherwise tail is matched against that agent's models with no floor, because the agent already narrows it down. The reported score is the lower of the two. If head matches no ready agent but matches one that is not ready, the message says it is unusable; if it matches nothing, the whole text is tried as step 3 (so llama3:8b still works as a model id).
  3. Everything: the whole text is ranked against every ready agent name and every model of every ready agent together. The best score at or above the floor wins; ties go to the shorter id, then to list order, and never raise an error. If no ready candidate reaches the floor, agents that are not ready are considered, then the request fails with the closest candidate named.

The scoring is fuzzy.md. A pick that differs from the typed text sets a note ('x' resolved to y (fuzzy 0.xx)), printed before the turn runs.

5. Running the turn

Agent::apply_mentions reads leading @ tags (several can be combined, for example a bot and a model). For an agent tag:

Situation Result
agent plus text the text runs once on that agent (Agent::run_agent)
agent only, no text the agent (and model) is pinned: Agent::agent_pin; every following plain message goes to it
@auto clears the pin together with the provider and model pins
agent tag together with a bot tag the agent takes the turn; the bot tag does not switch bots in that message

CapMention::ask does the work:

  • one CapSession per (agent, model, working directory) is created on first use and kept for later turns, so an agent keeps its own context between mentions;
  • every turn opens a panel and feeds it from the session's cap.* events (text, thoughts, tool calls, plan, usage, errors), whatever the binding (orchestration.md);
  • the turn is limited to CAP_TURN_TIMEOUT = 600 s; a panel idle for 300 s or closed by the user cancels the turn: cap.cancel first, and the session is closed after a 5 s grace if the agent ignores it;
  • the answer comes back as text, or as a single line starting cap agent <name> ... when the turn timed out, was cancelled, ended without an answer, or failed to start; a start failure also appends the agent report.

The answer then takes the same path every large tool result takes: above SPILL_LIMIT (6000 characters) it is stored as a record and the conversation receives the head, the tail and the record id (spill_or_truncate); record_read pages the rest. The exchange is logged and filed in the session history with backend cap:<agent>.

6. Errors and the agent report

Every failure is printed with the reason first. Selection failures:

Message Meaning
empty agent mention nothing after the @
agent 'x' is not usable: <why> the agent exists but its binary or manifest is broken
no model of agent 'x' resembles 'y' the agent has a model list and nothing in it relates to y
no agent or model resembles 'x', closest 'y' scored 0.12 (floor 0.30) nothing reached the floor

After the reason, the report lists every agent:

cap agents (2), use @agent or @agent:model:
  claude [ready] binding=stream_json binary=claude manifest=/home/me/.config/cap/agents/claude.toml
    models (3, from manifest): claude-opus-4-5, claude-sonnet-4-5, claude-haiku-4-5
    default: claude-sonnet-4-5
  codex [no_binary] binding=pty binary=codex manifest=/usr/share/cap-agents/codex.toml
    problem: binary 'codex' is not on PATH or not executable
    models: none known (any id is passed through)

Per agent it shows the status, the binding the driver would use, the binary, where the manifest came from, the problem if any, the models with their source (manifest, probe, manifest+probe), the default, and the probe error if the probe failed. An unknown @tag gets the same report appended, but only when at least one agent is installed (with none, the message is byte-identical to the Python app's).

7. Limits

Constant Value Where
CAP_AGENT_MAX 64 agents listed
CAP_MODEL_MAX 500 models per agent
CAP_MODEL_PROBE_TIMEOUT 5 s one probe
CAP_MODEL_PROBE_MAX_BYTES 1 MiB probe output kept
CAP_CATALOG_TTL 60 s cache lifetime of an agent entry
CAP_FUZZY_FLOOR 0.3 minimum score for a pick across agents and models
CAP_TURN_TIMEOUT 600 s one turn

8. Troubleshooting

Symptom Check
@name says unknown bot no manifest named name.toml in either directory, or no bot; the message lists both
the agent is listed no_binary which <binary>; the manifest's agent.binary is what is looked up
bad_manifest the report prints the parser's message with line and column
a model is not found although it exists the probe failed or timed out (see the probe: line); run the probe command by hand
the wrong model is picked use more of its name, or the exact id; the note shows what was picked and its score
an agent's panel shows nothing for a long time some agents emit their transcript at the end of the turn (tai over --cap-speak)

9. Verification

tests/cap_model_catalog_test.cpp covers discovery, statuses, the manifest list, the probe formats and failures, caching, exact and fuzzy resolution of all three forms, pass-through for agents without a list, and the error text with the report. The CAP protocol layer is covered by capmcp.unit, capmcp.wire_diff and capmcp.cli_diff (cap.md). A live check with a stream-json agent was run: @tia:zne ... resolved to tai:zen-free and the turn ran.

10. Gaps

  • No completion of agents and models after @ in the line editor or the chat scene.
  • No --cap-models on tai's own side; no ACP live model discovery.
  • Model switching inside a running session is not defined by CAP and is not done: a different model is a different session.
  • The 0.3 floor is a judgment, not a measurement; the constant is in cap_model_catalog.h.