[GH-ISSUE #196] bug: tool-call arguments are never captured — input_json_delta fragments are ignored #115

Closed
opened 2026-10-18 14:03:04 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @penradioGables on GitHub (Jul 28, 2026).
Original GitHub issue: https://github.com/open-webui/computer/issues/196

Summary

The Claude Code adapter reads a tool_use block's input at content_block_start, where it is always {}, and never handles the input_json_delta fragments that carry the actual arguments. The real arguments are discarded at ingest, so no downstream consumer — cptr's own UI, the gateway, the database — can ever show what a tool did.

Verified still present in v0.9.15 — cptr/utils/agents/claude_code.py is byte-identical between the v0.9.10 and v0.9.15 tags.

Symptom

Tool calls surface as a bare name with no detail. You can see that Bash ran; you can never see what command it ran.

Root cause

Anthropic streams a tool_use block's input as a series of input_json_delta fragments that arrive after content_block_start. The block observed at start time therefore always has input == {}.

_tool_update_from_claude_start reads block.get("input") at exactly that moment, the streaming loop never handles input_json_delta at all, and at content_block_stop the same stale dict is re-emitted (L185–201):

                    elif event_type == "content_block_start":
                        index, tool = _tool_update_from_claude_start(event)
                        if tool:
                            if index is not None:
                                tool_calls[index] = tool
                            yield tool
                    elif event_type == "content_block_stop":
                        index = event.get("index")
                        tool = tool_calls.get(index) if isinstance(index, int) else None
                        if tool:
                            yield AgentToolUpdate(
                                call_id=tool.call_id,
                                name=tool.name,
                                status="completed",
                                arguments=tool.arguments,
                                output="",
                            )

grep input_json_delta over the adapter returns nothing on either tag.

The non-streaming path has the same gap from the other direction: the AssistantMessage branch handles TextBlock and ThinkingBlock only, so ToolUseBlock — which does carry the complete input — is ignored there too.

Suggested fix

Accumulate partial_json per content-block index and json.loads it at content_block_stop, then merge into arguments. Worth merging in a way that cptr's own display key (title, see the related rendering issue) can't be shadowed by a tool argument that happens to share the name.

Happy to open a PR.

Originally created by @penradioGables on GitHub (Jul 28, 2026). Original GitHub issue: https://github.com/open-webui/computer/issues/196 ### Summary The Claude Code adapter reads a `tool_use` block's `input` at `content_block_start`, where it is always `{}`, and never handles the `input_json_delta` fragments that carry the actual arguments. The real arguments are discarded at ingest, so no downstream consumer — cptr's own UI, the gateway, the database — can ever show what a tool did. **Verified still present in v0.9.15** — `cptr/utils/agents/claude_code.py` is byte-identical between the `v0.9.10` and `v0.9.15` tags. ### Symptom Tool calls surface as a bare name with no detail. You can see that `Bash` ran; you can never see *what command it ran*. ### Root cause Anthropic streams a `tool_use` block's `input` as a series of `input_json_delta` fragments that arrive **after** `content_block_start`. The block observed at start time therefore always has `input == {}`. `_tool_update_from_claude_start` reads `block.get("input")` at exactly that moment, the streaming loop never handles `input_json_delta` at all, and at `content_block_stop` the same stale dict is re-emitted (L185–201): ```python elif event_type == "content_block_start": index, tool = _tool_update_from_claude_start(event) if tool: if index is not None: tool_calls[index] = tool yield tool elif event_type == "content_block_stop": index = event.get("index") tool = tool_calls.get(index) if isinstance(index, int) else None if tool: yield AgentToolUpdate( call_id=tool.call_id, name=tool.name, status="completed", arguments=tool.arguments, output="", ) ``` `grep input_json_delta` over the adapter returns nothing on either tag. The non-streaming path has the same gap from the other direction: the `AssistantMessage` branch handles `TextBlock` and `ThinkingBlock` only, so `ToolUseBlock` — which *does* carry the complete `input` — is ignored there too. ### Suggested fix Accumulate `partial_json` per content-block index and `json.loads` it at `content_block_stop`, then merge into `arguments`. Worth merging in a way that cptr's own display key (`title`, see the related rendering issue) can't be shadowed by a tool argument that happens to share the name. Happy to open a PR.
Author
Owner

@tjbck commented on GitHub (Jul 31, 2026):

Should be addressed!

<!-- gh-comment-id:5147957628 --> @tjbck commented on GitHub (Jul 31, 2026): Should be addressed!
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
open-webui/computer#115
No description provided.