Report what ACP agents actually send: real exit codes, streamed output, args and results - #2211
Closed
SawyerHood wants to merge 1 commit into
Closed
Report what ACP agents actually send: real exit codes, streamed output, args and results#2211SawyerHood wants to merge 1 commit into
SawyerHood wants to merge 1 commit into
Conversation
…t, args and results The ACP bridge parsed rawInput/rawOutput and then emitted neither, synthesized every exit code from the ACP status, suppressed the in-progress updates that carry a running command's output, and never used the permission request's ToolCallUpdate. Spike findings M1, M2, M3, M5, M6, M7. - M1 the exit code and output are the agent's own (Cursor exitCode/stdout/ stderr, grok exit_code/output_for_prompt/signal/timed_out); a call whose agent reported no exit code now closes without one. - M2 an in-progress update on a running command emits command.outputSnapshot; grok's cumulative stdout with tail resets is the snapshot contract. - M3 rawInput -> tool.args, rawOutput -> tool.result, the failure text -> tool.error, with the data-URL scrub kept and a 64 KiB payload cap. - M5 the permission request merges into the call it describes (by id, else the single in-flight call of its kind); its content text is the approval's reason and detail. The row never changes shape mid-flight. - M6 relative locations/rawInput paths resolve against the session cwd, and target_file joins the path fields. - M7 the programmatic tool name (unstable name, else a per-agent dialect) names the row, and the dialect supplies grok's kind at open so its command rows no longer open generic and close as a second item. Co-Authored-By: Claude <noreply@anthropic.com>
SawyerHood
force-pushed
the
ws2b-acp-2-richer-tool-reporting
branch
from
August 21, 2026 16:31
1fa9666 to
f0026d3
Compare
This was referenced Aug 21, 2026
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.
Stacked on #2198.
What was wrong
The ACP bridge parsed the fields that carry a tool call's real result and then emitted none of them. Findings M1, M2, M3, M5, M6 and M7 of the ACP tool-reporting spike, all confirmed on live wire logs from cursor-agent 2026.08.11 and grok 1.0.5:
delta-translation.tswroteexitCode: status === "failed" ? 1 : 0and put the wholerawOutputenvelope inaggregatedOutput. In the committedacp-cursor/web-searchrecording Cursor reports{"exitCode":1,"stdout":"","stderr":"[eval]:1 …"}under an ACP status ofcompleted; bb showed exit 0, a successful row, and{"exitCode":1,…}as the output text.in_progressupdates. The bridge answered those withsuppressedUnhandled+onlyIfNoTurn, so with a turn open they were dropped and with no turn open they becameprovider/unhandled.rawInput/rawOutputnever reached the row. Both agents send them on every call; the grammar hastool.args/tool.result.ToolCallUpdatewas ignored by the merge cache. Cursor's fetch ask names the URL (Fetch https://nodejs.org/dist/index.json) under a different tool-call id (web_fetch_0) than the call, and its content text ("Not in allowlist: node") is the only statement of why it asks.locations[].pathis absolute; grok sendsREADME.md. grok also names its read argumenttarget_file, which was not in the path-field list.tool_callcarries nokind(it rides_meta["x.ai/tool"]), so a command opened as a generic tool and closed as a secondcommandExecutionitem.What changed
plugins/provider-acp, no grammar change and no daemon wire change.extractAcpCommandResultreads Cursor'sexitCode/stdout/stderrand grok'sexit_code/output_for_prompt/signal/timed_out; a timeout or terminating signal is noted after the output. An agent that reports no exit code leaves the row without one instead of a fabricated0.in_progressupdate on a row that opened as a command emitscommand.outputSnapshot; the assembler diffs consecutive snapshots, so grok's cumulative stdout with tail-window resets lands as output deltas.rawInput→tool.args,rawOutput→tool.result, and the output text →tool.erroron a failed call. The inline-image data-URL scrub now runs over every string in the payload, and a payload past 64 KiB is replaced by its rendered text with the same truncation marker the server writes.notePermissionToolCallmerges the ask into the call it describes: same id, else the single in-flight call of its kind. The merge is additive and never re-shapes an open row — a row that opens as one kind and settles as another is two rows in the timeline, so a generic row takes the ask's headline instead of its shape. A call that already has a core shape is left alone (ACP file-write approvals present as command approvals carrying a bare directory path #1719). The ask'scontenttext becomes the approval'sreasonand the banner'spresentation.detail.locations[].path, diff paths andrawInputpaths resolve against the session cwd;target_filejoinspath/filePath/file_path. The session cwd stays off the command item — ACP never says where a command ran.name, else a new per-agent dialect (src/dialect.ts), keyed by the launch executable, that reads_meta["x.ai/tool"]for grok. The dialect also supplies the kind at open, so grok's command opens as a command.src/dialect.tsis the seed of layer 3's dialect module: vendor side channels are read there, never in the shared wire schema.How I verified
Live ACP probe, re-run today against both installed agents (the spike's 120-line client; opencode/gemini/hermes are not installed on this machine). Same log replayed through
main's translator and this branch's:grok agent stdio, prompt "Runls, then read README.md":cursor-agent acp, same prompt — the command's output was the rawOutput blob, now it is the real stdout:M2 against production traffic. The 83
provider/unhandledrows the spike found (all groktool_call_updatein_progresswith cumulative stdout, exported read-only) replayed with their command row open:G11 — honest result: the 83 prod rows do NOT drop. All 83 arrived after a
provider/errorhad already settled that turn (threadthr_zvyf9ypnv6, sequences 3436-3518,turn/completedat 3435), so there is no row for them to land on andprovider/unhandledremains the correct record. What changes is the in-turn case above, where the same updates used to be dropped silently. Total ACPprovider/unhandledin the production DB is 155 rows across all 25 grok threads (83tool_call_update+ 37agent_message_chunk+ 35agent_thought_chunk), all post-turn. The G11 gate proper —unhandled/grammarDropsinrecordings/row-counts.json— is unchanged: every acp-cursor cell stays atunhandled: 0,turn-toolsstays at 1 grammar drop.Conformance.
pnpm exec turbo run typecheck test --filter=bb-plugin-provider-acp: 16 files, 221 tests. Scripted conformance (12 rules) and recorded conformance (8 acp-cursor cells) both pass.A2 parity.
pnpm --filter @bb/provider-parity rerecord --provider acp-cursorre-wrote the 8 changedbridge→runtime.current.ndjsonlanes;UPDATE_PARITY_ROW_COUNTS=1re-pinned nothing (event and row counts are unchanged in every cell).pnpm exec turbo run test --filter=@bb/provider-parity --force: 43/43.Two-checkout A2 against a
mainworktree:acp-cursor#2211)codexclaude-codeThe allowlisted classes are exactly the intended ones:
item/arguments(M3),item/result(M3),item/aggregatedOutput(M1),item/exitCode(M1), and their row projections including onechildren/*/statuswhere theexitCode: 1command now projects as an error row. The permission-merge headline change ("Web Fetch"→"Fetch https://nodejs.org/dist/index.json","MCP: tool"→"bb-bridge-AskUserQuestion: AskUserQuestion") needs no entry of its own:#2179's/*/item/presentationentry already covers it for those cells.turbo.jsongainspassThroughEnv: ["UPDATE_PARITY_ROW_COUNTS", "UPDATE_TRANSCRIPT_EXPECTATIONS"]ontest. Both are documented re-pin knobs and turbo's strict env was stripping them, so the documented command silently did nothing.Known limit
Cursor's
fetchcall cannot become a semanticwebFetchitem: the URL only exists in the permission ask, which arrives after the row is open, and adopting a new shape mid-flight projects as two rows. The row keeps itsfetchtool kind and shows the URL as its headline. Giving it the real item kind needs either a deferred open or a projection that folds a re-opened item id — out of scope here.