Skip to content

fix(streaming): forward tool args on execution-end event#477

Open
larock22 wants to merge 1 commit into
masterfrom
fix/tool-execution-end-args-passthrough
Open

fix(streaming): forward tool args on execution-end event#477
larock22 wants to merge 1 commit into
masterfrom
fix/tool-execution-end-args-passthrough

Conversation

@larock22

@larock22 larock22 commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

What

Forward the tool-call arguments on the streaming tool-execution-end handler
instead of hardcoding an empty dict.

 self.tool_result_callback(
     tool_name,
     status,
-    {},
+    event_obj.args or {},
     event_obj.result,
     duration_ms,
 )

Why

For completed tools, _handle_stream_tool_execution_end passed {} as the args
to tool_result_callback. Per-tool renderers that derive their panel display
from args therefore had nothing to work with. The most visible symptom: the
read_file result panel builds its filename/path solely from
args["filepath"], so with empty args it rendered the literal string
"unknown" in the tool-output bar for every successful read.

Safety / compatibility

event_obj.args or {} is None-safe. On a tinyagent build where
ToolExecutionEndEvent does not carry args, event_obj.args is None and the
expression yields {} — identical to today's behaviour, no regression. Once
tinyagent populates the field, the real filepath flows through automatically with
no further change here.

Dependency

The end-to-end fix needs tinyagent to expose args on ToolExecutionEndEvent
(it currently only lives on the Start/Update events). Tracked upstream in
alchemiststudiosDOTai/tinyAgent#43. This PR is the consumer half and is safe to
merge independently ahead of that.

Updated streaming tool execution end handling so tool_result_callback now receives event_obj.args or {} instead of a hardcoded empty dict.

This preserves state needed by per-tool renderers and message/tool-call output (for example, allowing completed read_file calls to surface their filepath instead of "unknown"), while remaining safe when ToolExecutionEndEvent.args is unavailable.

Type safety is unchanged functionally, but the new None-fallback avoids regressions on builds that have not yet adopted the upstream event field.

The tool result callback hardcoded an empty args dict for completed tools,
so renderers that derive their display from args (e.g. the read_file panel's
filename/path) fell back to the literal "unknown". Forward event_obj.args
instead. It is None-safe and harmlessly yields {} on tinyagent builds that
do not yet populate the field, and lights up automatically once they do.

Depends on tinyagent exposing `args` on ToolExecutionEndEvent
(alchemiststudiosDOTai/tinyAgent#43).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6012a993-673c-4c4e-94c2-f0f068a199f1

📥 Commits

Reviewing files that changed from the base of the PR and between 56f3dba and 4b2b8b3.

📒 Files selected for processing (1)
  • src/tunacode/core/agents/agent_components/agent_streaming.py
📜 Recent review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: pre-commit
⚠️ CI failures not shown inline (2)

GitHub Actions: Empty Directory Check / 0_Check for Empty_Only-init Directories.txt: fix(streaming): forward tool args on execution-end event

Conclusion: failure

View job details

##[group]Run EXIT_CODE=0
 �[36;1mEXIT_CODE=0�[0m
 �[36;1mSRC_DIR="src"�[0m
 �[36;1m�[0m
 �[36;1mwhile IFS= read -r dir; do�[0m
 �[36;1m  file_count=$(find "$dir" -maxdepth 1 -type f | wc -l)�[0m
 �[36;1m  subdir_count=$(find "$dir" -maxdepth 1 -mindepth 1 -type d -not -name "__pycache__" | wc -l)�[0m
 �[36;1m�[0m
 �[36;1m  if [ "$file_count" -eq 0 ] && [ "$subdir_count" -eq 0 ]; then�[0m
 �[36;1m    echo "::error::Empty directory: $dir"�[0m

GitHub Actions: Empty Directory Check / Check for Empty_Only-init Directories: fix(streaming): forward tool args on execution-end event

Conclusion: failure

View job details

##[group]Run EXIT_CODE=0
 �[36;1mEXIT_CODE=0�[0m
 �[36;1mSRC_DIR="src"�[0m
 �[36;1m�[0m
 �[36;1mwhile IFS= read -r dir; do�[0m
 �[36;1m  file_count=$(find "$dir" -maxdepth 1 -type f | wc -l)�[0m
 �[36;1m  subdir_count=$(find "$dir" -maxdepth 1 -mindepth 1 -type d -not -name "__pycache__" | wc -l)�[0m
 �[36;1m�[0m
 �[36;1m  if [ "$file_count" -eq 0 ] && [ "$subdir_count" -eq 0 ]; then�[0m
 �[36;1m    echo "::error::Empty directory: $dir"�[0m
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (Custom checks)

**/*.py: Enforce dependency direction: ui → core → tools → utils/types. Flag violations where core/ imports from ui/, tools/ imports from ui/, tools/ imports from core/, or types/ imports from anything except stdlib/typing
In agent/orchestration code, verify exception handlers clean up state: flag except (UserAbortError, CancelledError) patterns that exist without corresponding cleanup like _remove_dangling_tool_calls() or state rollback. State mutations (messages.append, session modifications) followed by await/function calls that could raise must be cleaned up in except blocks
Check for shallow copy followed by nested dict mutation: flag .copy() on dicts that contain nested dicts/lists. Use deepcopy() instead or use dictionary spreading syntax to avoid mutating original objects
Enforce error handling principle: 'Fail fast, fail loud. No silent fallbacks.' Flag except: pass, except Exception: pass, empty except blocks, catching broad exceptions without re-raising or logging, and returning None/[]/{} sentinel values instead of raising exceptions for invalid inputs

**/*.py: Preserve architectural order rules; do not add new imports across forbidden layers defined in the dependency layer enforcement test
Follow allowed import direction: ui -> core -> tools, with shared-layer imports from utils, types, configuration, constants, exceptions
Prefer existing patterns; mirror command/test naming used in nearby code

Files:

  • src/tunacode/core/agents/agent_components/agent_streaming.py
src/tunacode/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Do not add TunaCode-owned message-contract wrappers around tinyagent message types; use tinyagent models directly in memory and keep dict payloads at real boundaries

Files:

  • src/tunacode/core/agents/agent_components/agent_streaming.py
src/tunacode/**/

📄 CodeRabbit inference engine (AGENTS.md)

Avoid adding empty directories or __init__.py-only directories

Files:

  • src/tunacode/core/agents/agent_components/agent_streaming.py
src/tunacode/core/agents/**/*.py

⚙️ CodeRabbit configuration file

src/tunacode/core/agents/**/*.py: CRITICAL REVIEW AREA - Most bugs in last 2 months originated here.

Check for these specific bug patterns:

  1. DANGLING TOOL CALLS (PRs #283, #257, #246):

    • Every exception handler in agent loops MUST clean up state
    • If messages are mutated before an await, the except block MUST handle cleanup
    • Look for: except (UserAbortError, asyncio.CancelledError) without cleanup
    • Pattern: _remove_dangling_tool_calls() should be called on abort
  2. FROZEN DATACLASS MUTATION (PR #279):

    • Canonical messages are frozen dataclasses
    • NEVER use setattr() on message objects
    • MUST use dataclasses.replace() to create new instances
    • Look for: setattr(message, ...) or message.field = ...
  3. SHALLOW COPY CORRUPTION (PR #289):

    • .copy() does NOT deep copy nested dicts
    • Mutating a shallow copy pollutes the original
    • Look for: config.copy() followed by mutation of nested keys
    • Fix: Either use deepcopy() or don't mutate at all
  4. GATE 6 - Exception Paths Are First-Class:

    • Every try block: what state is dirty if exception occurs?
    • Every except block: does it restore valid state?
    • Message lists MUST have matching tool calls and returns

Files:

  • src/tunacode/core/agents/agent_components/agent_streaming.py
🔇 Additional comments (1)
src/tunacode/core/agents/agent_components/agent_streaming.py (1)

230-236: 🎯 Functional Correctness

Check ToolExecutionEndEvent.args in tiny-agent-os 1.2.28

event_obj.args or {} only works if args is a real attribute on ToolExecutionEndEvent; if this release still omits the field, this line raises AttributeError instead of falling back to {}.


📝 Walkthrough

Walkthrough

The _handle_stream_tool_execution_end method now forwards event_obj.args or {} to tool_result_callback instead of always passing an empty dict, allowing actual tool arguments to reach the callback when available.

Changes

Tool Execution Callback Args Fix

Layer / File(s) Summary
Forward event args to callback
src/tunacode/core/agents/agent_components/agent_streaming.py
tool_result_callback now receives event_obj.args or {} instead of a hardcoded empty dict, while tool name/status/result/duration handling is unchanged.

Estimated code review effort: 1 (Trivial) | ~3 minutes

Possibly related issues

  • ToolExecutionEndEvent is missing args field (present on Start/Update events) tinyAgent#43: Addresses ToolExecutionEndEvent.args presence at the event source, which this PR's callback change now consumes — flag as a dependency: if the upstream event doesn't reliably populate args, this fix is a no-op or silently passes stale/empty data.
  • alchemiststudiosDOTai/tinyAgent#42: Directly tied to the same ToolExecutionEndEvent.args field this change relies on; verify the field's type/default contract matches the event_obj.args or {} fallback logic here to avoid masking upstream bugs (e.g., None vs missing attribute vs empty dict semantics differ).

Note: this is a one-line diff, but the fallback event_obj.args or {} silently swallows falsy-but-valid args (e.g., an empty dict passed intentionally is indistinguishable from None). If CLAUDE.md has a gate on explicit None-checks for event payloads rather than truthy-fallback (common in this codebase's defensive-coding guidance), this should be event_obj.args if event_obj.args is not None else {} instead.

🚥 Pre-merge checks | ✅ 9
✅ Passed checks (9 passed)
Check name Status Explanation
Title check ✅ Passed The title uses a valid fix conventional-commit prefix and accurately describes the streaming tool-args change.
Description check ✅ Passed The description covers what, why, safety, and dependency, so it is substantially complete despite missing template checklist sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Noshallowcopymutation ✅ Passed No .copy()-then-nested-mutation pattern exists in the touched streaming handler; the PR only forwards event_obj.args or {}.
Exceptionpathcleanup ✅ Passed PASS: agent_streaming.py cleans up in its CancelledError/Exception handler via _handle_interrupted_stream_cleanup(); this PR only forwards args and doesn’t alter exception paths.
Dependencydirection ✅ Passed PASS: Gate 2 holds—this core file imports stdlib/tinyagent/core/utils/types only; no ui/ or tools/ imports, and no types/ module changes.
Nosilentfailures ✅ Passed PASS: The diff only swaps {} for event_obj.args or {}; no added empty excepts, swallowed exceptions, or silent return None paths in the touched handler.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant