mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 12:40:32 +00:00
## Summary A native subagent child session gets its model only from what the child reports. The update is a `config_option_update` with the child's session id and a single `model` select option. This follows the subagents RFD, [Child configuration](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/docs/rfds/subagents.mdx#child-configuration) (see also agentclientprotocol/agent-client-protocol#2482). ## Relation to #1417 #1417 reports a child's model from its responses, in the `rfd` form only, with the raw model id as the name. This PR keeps that source and extends it: - The `air` form gets the same update. - History replay sends each child's model from its transcript. - The value maps to an entry of the root session's `model` option, so a client can compare the child's model with the session's. There is no model at spawn. The adapter cannot tell the model that Claude Code picks for a new child, for example with `CLAUDE_CODE_SUBAGENT_MODEL_FORCE`, or when an alias runs on the parent's exact model. So the first update comes with the child's first real response (`message.model`, not `<synthetic>`). A new update goes only when the entry changes. The configuration hooks of #1417 (`PostModelSwitch` and the others) now also run for `air` clients. A hook names the SDK agent id, so in the `air` form its options go to the agent's current generation session (`<agent id>:generation:<n>`). In the `air` form, no configuration goes after a child's end. ## Notification ```json { "sessionId": "<child session id>", "update": { "sessionUpdate": "config_option_update", "configOptions": [{ "id": "model", "name": "Model", "category": "model", "type": "select", "currentValue": "opus[1m]", "options": [{ "value": "opus[1m]", "name": "Opus 4.6 (1M context)" }] }] } } ``` - The client cannot change a child's configuration, so the select offers only the current value. - No field is added to `subagent_spawned` or `subagent_update`. ## Matching a model id to an entry Best first: 1. the root session's current entry, when it runs the same model up to a context hint, such as `opus[1m]` or `default` for `claude-opus-4-6`; 2. the listed entry whose value or resolved model is the id; 3. a listed entry that runs the same model up to a context hint. When several entries run the model, a concrete entry comes before `default`, unless `default` is the current entry. A model id is compared as it is, so a dated or provider id (`claude-haiku-4-5-20251001`, `eu.anthropic.claude-sonnet-5`) matches only the same id: `ModelInfo.resolvedModel` is the wire id that a response reports. A model that matches no entry goes as its full id, with its display name when the session's model list has one. A `PostModelSwitch` hook of a child uses the same matching. ## Replay - A child without responses in its transcript gets no model. - `rfd` form: the replay sends a child's model right after its announcement, from its first response, and again when a later response changes it. During a load, the session's model list can be unknown, so the replay can send the full id. After the load response, a child's model goes again only when the list maps it to another entry. - `air` form: the replay ends every child, and nothing may follow the end. So each child's latest response model goes once the session creation ended, just before the child's end, as the entry of the session's model list. ## Validation - `npm run build` - `npm run check` - `npm run test:run`, three runs: 3315 passed, 30 skipped. - `src/tests/acp-scenarios.test.ts`, 30 runs in 6 parallel workers: no failure. New tests cover: - no model before the child's first response, in both forms; - an update only when the entry changes; - no update to an ended `air` child, also with an update in flight at the end; - a hook's options under the current `air` generation; - matching for `opus[1m]` and `default`, concrete entries before `default`, listed entries, dated and provider ids, and unlisted ids; - the `PostModelSwitch` entry, in both forms; - an `air` load replay that maps a child's model only once the session exists; - `rfd` replay with an unlisted full id, and no duplicate update after the load response. The scenario snapshots for both forms and v2 are updated.