fix(stream): emit a tool_call for zero-argument tools so their card is never lost - #86
Merged
Merged
Conversation
…s never lost `_tool_args_ready` holds a tool card back while the model is still streaming the call's arguments. It cannot tell "not written yet" from "there is nothing to write", so a tool declared without parameters keeps empty args forever and never clears the gate — no `tool_call` event is emitted for the whole run. The result then arrives orphaned. The frontend, finding no card for that tool_id, fell back to "bind it to the last still-running card", which with parallel tool calls files the output under a sibling tool: the call vanishes from the tool list entirely and the sibling briefly renders the wrong output. The persisted log entry was equally degraded (no display name, no args, no content_offset), which also knocked the whole message off the offset-ordered history replay. Backend: at the tool_result stage, synthesize the missing `tool_call` when the tool_id never produced a card — in both streaming paths. Frontend: stop the blind "last running card" fallback for results that carry a tool_id which matched nothing; such a result belongs to a card that was never created, so claiming an unrelated one only hides the real call.
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.
Problem
A tool declared with no parameters (its arguments are always
{}) never showed up in the chat's tool list, even though it ran and its output reached the answer. With tools running in parallel, a sibling tool's card also briefly rendered the missing tool's output.Root cause
_tool_args_ready(orchestration/tool_payloads.py) exists to hold a tool card back while the model is still streaming the call's arguments — the first chunks arrive with incomplete args. It cannot distinguish "not written yet" from "there is nothing to write":A zero-argument tool keeps empty args forever, so it never clears the gate and
astream_chat_workflowemits notool_callevent for it at all.Its
tool_resultthen arrives orphaned, and two things go wrong downstream:hooks/chatStream.ts) —findToolCallIndexfell back to "bind this result to the last still-running card". Under parallel tool calls that is an unrelated sibling: the sibling's card gets overwritten with the wrong output (until its own result lands and overwrites it back), and the real call never appears.attach_tool_resultappends a bare log entry with no display name, no args and nocontent_offset.buildHistorySegmentsrequires every entry to carry an offset, so one such entry drops the entire message off the offset-ordered history replay.Fix
tool_resultstage, synthesize thetool_callthat was never emitted when the tool_id never produced a card, in both streaming paths (astream_chat_workflowand_astream_subagent_direct). The card keeps the tool's normal display name;update_plan, which deliberately renders no card, is still skipped beforehand. Because the synthesized call goes through the regular log path, the persisted entry regains its display name, args andcontent_offset.Tests
New
tests/orchestration/test_missing_tool_call_synthesis.pycovers the gate precondition, synthesis for an unseen tool_id, idempotency once the id is claimed, the skill-load display name, the raw-name fallback, and that the persisted log entry is complete while a sibling card does not absorb the orphan result.