# 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](cap.md); how the running agents are shown and supervised is in [orchestration.md](orchestration.md); the scoring is in [fuzzy.md](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](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 `.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](cap.md), `CapRegistry`): a `cap-agent.toml` next to the binary, then the user directory, then the system directory, then ` --cap-manifest` with a `CAP_PROBE_TIMEOUT` of 15 s. A minimal manifest: ```toml [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** ```toml [models] list = ["a", "b"] # strings, or tables with id / name / model default = "a" ``` **Live, by probe** (optional) ```toml [models] probe = ["myagent", "--cap-models"] # or, to use " --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](../../research/cap-protocol/conclusions/cap-model-handling-practice.md)); 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](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](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 ...` 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:`. ## 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: ` | 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 `; 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](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`.