fix(acp): name the video tool the way an ACP client offers it (#2513) - #2571
Merged
Conversation
An ACP agent asked to inspect a screen recording reported that "the advertised video_frames reader isn't exposed in this session's tool set" and read the file with shell tooling instead (#2513). The tool was exposed. `video_frames` is registered unconditionally, is in `BRIDGE_TOOL_NAMES`, and survives the draft-2020-12 conversion `bridgedTools` filters on — checked directly rather than assumed. What the agent could not find was the *name*: the bridge mounts Copse's tools as an MCP server, so the client renames them. `acp-bridge-name.ts` documents the three observed shapes — `copse-video_frames` (Cursor), `mcp__copse__video_frames` (Claude), `mcp.copse.video_frames` (Codex). The steering block said "Use the `video_frames` tool", which is only ever literally true under the native loop. So the block now tells the model to match on the name's *ending* and shows a namespaced example, and — the part that would have contained the damage either way — says to speak up when no such tool is offered rather than reach for the shell. That second half is not new thinking: the archive block beside it has carried exactly those sentences since an ACP run "quietly went off grepping", and its test says so. The video block simply never got them. The archive block gets the ending/example hint too. Its hedge ("if you have a `read_archive` tool") reads as false to an agent whose only match is `mcp__copse__read_archive`, so the hedge was sending agents to ask for a tool they already had — the same defect, one line up. `packages/agent` cannot import from `src/main` by design, so the example name is a literal there and could drift from the real server name. `attachment-steering-names.test.ts` is the seam: it lives in `src/`, where both sides are importable, and asserts the example is a string the bridge's own `matchesBridgedToolName` accepts and that it carries `BRIDGE_MCP_SERVER_NAME` — so renaming the server fails a test instead of silently leaving stale copy in front of every agent. Verified: 6 new tests, all 6 fail against the unfixed preambles. 8738 tests, 8728 pass, 10 skipped, 0 fail; typecheck, lint, format, dead-code, oracle, e2e-syntax and demo:site all clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hpk1gEma9LhMUuvr2TwUj6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2513 (P2,
area:acp).The report, and what was actually wrong
An ACP agent asked to inspect a screen recording said "the advertised
video_framesreader isn't exposed in this session's tool set" and read the file with shell tooling instead.The tool was exposed. I checked rather than assumed, and the issue's premise ("an ACP session does not receive it") is wrong:
video_framesis registered unconditionally (registry-bootstrap.ts) — the per-turn withholding inparentToolsis native-loop only.BRIDGE_TOOL_NAMES(acp-native-bridge.ts), and was already at370cb70, the commit the issue was triaged against.bridgedTools()filters on — I ran the real conversion to confirm, becausetoMcpTools()drops unconvertible schemas with nothing but aconsole.warn, which would have been a plausible culprit.What the agent could not find was the name. The bridge mounts Copse's tools as an MCP server, so the client renames them.
acp-bridge-name.tsalready documents the three observed shapes:copse-video_framesmcp__copse__video_framesmcp.copse.video_framesThe steering block said "Use the
video_framestool" — only ever literally true under the native loop. This is the same class of miss as #1659, where^copsefailed to match Claude'smcp__-prefixed titles.The fix
The video block now tells the model to match on the name's ending and shows a namespaced example, and says to speak up when no such tool is offered rather than reach for the shell.
That second half is not new thinking. The archive block sitting directly beside it already carries exactly those sentences, added after an ACP run "quietly went off grepping" — its test says so in as many words. The video block simply never got them. Had it, the reported turn would have ended in a question instead of a video read with shell commands, which cannot decode a video and left the user believing it had been watched.
The archive block gets the ending/example hint too, because it has the same defect one line up: its hedge "if you have a
read_archivetool" reads as false to an agent whose only matching entry ismcp__copse__read_archive. So the hedge that was meant to prevent silent failure was instead sending agents to ask for a tool they already had. Same root cause, adjacent constant — fixing one and not the other would leave the bug in place under a different name.The drift seam
packages/agentcannot import fromsrc/mainby design (headless-contract.tsis explicit about staying free of main-process code), andbuild-text-with-attachments.tshas no imports at all. So the example name has to be a literal there, and could drift from the real server name.src/main/services/acp/attachment-steering-names.test.tsis the seam. It lives insrc/, where both sides are importable, and asserts the example is a string the bridge's ownmatchesBridgedToolNameaccepts — not merely a plausible-looking one — and that it carriesBRIDGE_MCP_SERVER_NAME. Rename the server and a test fails, rather than stale copy sitting in front of every agent.Verification
node scripts/run-tests.mts(whole repo)tsc --noEmit(node + web)pnpm run lint/oxfmt --check .check:dead-code/check:oracle/check:e2e-syntax/demo:site:checkSix new tests: the video block tells a model without the tool to speak up rather than shell out; both blocks name the namespaced form; and three seam tests binding the examples to the live bridge name.
Two existing assertions changed.
emits one steering preamble + a line per {video,archive}counted raw occurrences of the tool name and expected exactly one; the preamble now names each tool three times deliberately. They count the preamble's own opening line instead, which is what "one preamble however many attachments" actually means — the property the test is named for is preserved, not weakened.Reaching a full local run needs an uncommitted shim over node-pty's native loader, reverted before committing.
One note for the reviewer: this fixes the instruction. If you would rather the block never name a client-specific example at all, the alternative is to drop the example and keep only "look for a tool whose name ends in
video_frames" — less actionable for the model, but it would let the seam test go away.🤖 Generated with Claude Code
https://claude.ai/code/session_01Hpk1gEma9LhMUuvr2TwUj6
Generated by Claude Code