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:
~/.config/cap/agents/(user), or/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 @:
- a bot name or nickname,
- a provider label, then
molodetz, then a configured backend label, - a model id already known to the application (exact),
auto,- an exact agent name, or
agent:...whose first part is an exact agent name, - an existing file path,
- the fuzzy catalog (below),
- 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:
- 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.
head:tail(split at the first:):headis matched against the names of the ready agents with a floor ofCAP_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
tailis 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. Ifheadmatches 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 (sollama3:8bstill works as a model id).
- empty
- 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
CapSessionper (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.cancelfirst, 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-modelson 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.