Repository navigation
cursor: decode the native exec frame and stop reading tool calls as text - #179
HectorBernstorff wants to merge 3 commits into
Conversation
…text The Cursor provider never produced a tool_use, so every turn that needed a tool ended after one reply (issue raine#166). Two field numbers were wrong and the frame that asks the client to run a tool was never decoded at all. - AgentServerMessage.exec_server_message is tag 2, not 3 (tag 3 is conversation_checkpoint_update). The struct also declared a notes_session_id at tag 1, where the wire carries a varint id, so an exec payload was dropped whole. - InteractionUpdate is 1 text_delta, 4 thinking_delta, 14 turn_ended. The old mapping read tag 2 (tool_call_started) as the model's text - which is why a tool call reached the client as the literal string "call_...\nfc_..." - and tag 3 (tool_call_completed) as usage. With the exec frame decoded, read_args, write_args and shell_stream_args map onto the Claude tools that fulfil them and the stream pauses with stop_reason=tool_use. No pending tool is stored on purpose: the client's tool_result travels back in the next request, where render_cursor_prompt already turns it into a <tool_result> block for the upstream. Field numbers come from the protobuf descriptors inside the installed Cursor Agent bundle (index.js, 2026.10.01-e373342); the frames were captured from the live upstream. Verified with the crate test suite (55 passed; two new tests cover the exec decode and the text leak) and end to end through Claude Code, where Read, Write and Bash all complete a real loop. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Thanks for the fix and the live testing. I had an AI agent investigate the PR. Here's its assessment:
|
…context Addresses the review on the first version: - the run stays open across requests; each exec (read, write, shell) is answered on the same AgentService/Run stream with an ExecClientMessage that echoes id and exec_id, instead of starting a new run with rendered results (which made the agent re-issue the call it had just made); - WriteArgs.file_bytes (tag 5) is honoured, shell working_directory/timeout and read offset/limit are carried into the Claude Code tool input; - no pending-tool state is left behind: entries are taken on resume and swept when abandoned. Also: - one pooled reqwest client instead of one per request, with an HTTP/2 keepalive interval (the timeout alone is inert in hyper); - input tokens come from the checkpoint's token_details.used_tokens (the context of the latest step); turn_ended sums every internal step and showed a multiple of the real context on runs with tool calls. cargo test, fmt and clippy are clean for the cursor provider. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Pushed f07965c. It answers each exec on the original Run stream (ExecClientMessage echoing id/exec_id), honours WriteArgs.file_bytes, carries shell working_directory/timeout and read offset/limit, and leaves no pending state behind. It also uses one pooled client with an HTTP/2 keepalive interval, and takes input tokens from the checkpoint's token_details.used_tokens, because turn_ended sums every internal step. cargo test, fmt and clippy are clean for the cursor provider. |
- The prompt listed the client's tools but never said how to call them, so
the agent tried MCP tools on its own channel and the server answered "Tool
not available". The <tools> section now opens with the calling protocol:
built-in tools for files and shell, and the <tool_use name>{json}</tool_use>
form the bridge already parses for everything else.
- Read results reach Cursor as file text. Claude Code's Read answers in cat -n
form, and the agent copied the line numbers back into files it wrote.
- tool_reference blocks (tool search) render as "Tool loaded".
cargo test, fmt and clippy are clean for the cursor provider.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Pushed dbb4229. The prompt section now states the calling protocol for tools that are not the agent's own (without it the agent tried MCP tools on its own channel and got "Tool not available"). Read results reach Cursor as file text rather than cat -n output, and tool_reference blocks render as "Tool loaded". |
Fixes #166.
The Cursor provider never emitted a
tool_use, so any turn that needed a tool ended after one reply: the client saw the model's tool call as literal text (call_...\nfc_...) and the upstream's request to run the tool was dropped.Root cause
Two field numbers in the hand-rolled protobuf decoder are wrong, and the frame that carries the tool request was never decoded at all:
AgentServerMessage.exec_server_messageconversation_checkpoint_update)InteractionUpdate.text_deltatool_call_started)InteractionUpdate.thinking_deltaInteractionUpdate.turn_endedtool_call_completed)AgentServerMessagealso declarednotes_session_idat tag 1, where the wire carries a varint id, so exec payloads failed to decode and the frames were skipped whole.Two consequences, both visible in #166:
tool_call_started(tag 2) was rendered as the model's text, and the native exec frame — where Cursor asks the client to run Read, Write or a shell command — was never read.The field numbers are not guessed. They come from the protobuf descriptors shipped inside the installed Cursor Agent bundle (
~/.local/share/cursor-agent/versions/<version>/index.js), and the frames were captured from the live upstream.The fix
ExecServerMessage(id,exec_id,read_args,write_args,shell_args/shell_stream_args) into a newCursorStreamEvent::Exec.Read,Write,Bash) and pause the stream withstop_reason: tool_use.tool_resultcomes back inside the next request, whererender_cursor_promptalready renders it as a<tool_result>block for the upstream, so the loop keeps going.Verification
cargo test: 55 passed, 0 failed. Two new tests cover this directly:decodes_native_read_exec(the real frame shape) andtool_call_started_is_not_text(the text leak from Cursor: tool calls come back as plain text, so Claude Code turns end after one reply #166).Notes
allowed_tool_names, streaming, session id) is reused, so the bridge only engages when the client actually advertised those tools.contentsbeforecontent) and corrects itself on the following turn, and it probes with aReadbefore editing a file.🤖 Generated with Claude Code