266 lines
13 KiB
Markdown
266 lines
13 KiB
Markdown
# 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 `<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](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:
|
|
|
|
```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 "<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](../../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 <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](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`.
|