Skip to content

fix(streaming): fail loudly when a [DONE]-less stream cuts tool-call arguments (#184) - #188

Open
Fahad090NP wants to merge 3 commits into
ltmoerdani:mainfrom
Fahad090NP:fix/issue-184-incomplete-toolcall-guard
Open

fix(streaming): fail loudly when a [DONE]-less stream cuts tool-call arguments (#184)#188
Fahad090NP wants to merge 3 commits into
ltmoerdani:mainfrom
Fahad090NP:fix/issue-184-incomplete-toolcall-guard

Conversation

@Fahad090NP

@Fahad090NP Fahad090NP commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #184

PEACE BE UPON YOU

Summary

Follow-up to #187 (merged), reported in #184: GPT 5.6 luna with tool calling ends the Responses stream without [DONE]/finish_reason after 257 KB / 93 events. #187 made such streams return successfully — but for a tool-calling stream that was cut mid-arguments, that success flushes a tool call whose truncated JSON parseToolInput silently coerces to {}. The tool then executes with corrupted/empty input, and nothing tells the user.

Fix

Behavior matrix

Stream end Before this PR After
No [DONE], text-only content delivered success (#187) unchanged
No [DONE], tool calls with complete JSON args success, tools execute unchanged
No [DONE], tool-call args truncated mid-JSON success, tool executes with {} input ❌ truncation error, user resends ✅

Verification

  • npm run lint all 7 checks green; 5 new unit tests for hasCompletePendingCalls.
  • 6 files changed, +100/−1.

Fi Amanillah

@ltmoerdani

Copy link
Copy Markdown
Owner

The direction here is right and the tests are solid @Fahad090NP. traced it through the code though and found a gap that stops the main claim from landing.

the finally in responses.ts still flushes the truncated call.

when your new workIncomplete path throws in engine.ts, execution falls into the finally block, which calls extractor.flushRemainingToolCalls(...). that goes to flushToolCalls()parseToolInput('{"path":') → coerces to {} → emits the LanguageModelToolCallPart to VS Code via reportProgressPart before the error surfaces.

so the row in your behavior matrix that says "truncation error, user resends ✅" doesn't hold. both things happen: the corrupted tool call gets emitted, and the error popup shows on top of it. the tool still executes with {} input, same as before the fix, just noisier.

the fix is small: guard the flush. either check hasCompletePendingToolCalls() inside flushRemainingToolCalls() and skip when it's false, or skip the flush call in responses.ts when the engine threw truncation.

second thing, smaller: hasCompletePendingCalls() treats an empty arguments string as incomplete on the assumption that OpenAI-style streams always send at least one args delta. worth double-checking that against a real capture, a tool that legitimately takes no parameters on a stream that ends without [DONE] would now throw a truncation error for nothing. narrow case, but if the gateway genuinely never sends args for no-param tools, we'd want to only treat non-empty-but-invalid JSON as truncated.

one housekeeping note: repo convention is an entry under docs/issues/ for issue fixes like this, would be great to add one in the same PR.

happy to merge once the flush guard is in.

Fahad090NP added a commit to Fahad090NP/opencode-copilot-chat that referenced this pull request Aug 24, 2026
… as complete (review ltmoerdani#188)

- flushRemainingToolCalls skips flushing when pending calls are incomplete
  (empty/truncated JSON) — previously the finally-block emitted a corrupted
  LanguageModelToolCallPart before the truncation error surfaced
- hasCompletePendingCalls: empty arguments string now counts as COMPLETE —
  gateways legitimately send no args delta for no-param tools; Copilot's own
  loop normalizes arguments === '' to '{}' (toolCallingLoop.ts:1909). Only
  non-empty invalid JSON counts as truncated
- docs/issues/80 entry per repo convention
@Fahad090NP

Copy link
Copy Markdown
Contributor Author

All three points addressed:

1. Flush guard — fixed. flushRemainingToolCalls() now checks hasCompletePendingCalls() first: when pending calls are incomplete it logs [warn] dropping N incomplete tool call(s) from a truncated stream (arguments cut mid-JSON), clears them, and emits nothing. This guards every path that reaches the flush — the adapter finally blocks included — so the corrupted tool call is never emitted, with or without the error popup.

2. Empty-args assumption — you were right to push on it, and I verified against a real reference. Copilot's own tool-calling loop (extensions/copilot/src/extension/intents/node/toolCallingLoop.ts:1909 in the vscode-main source) explicitly normalizes arguments === '' ? '{}' : call.arguments — i.e. gateways legitimately send no arguments delta for no-parameter tools, and empty args means complete. hasCompletePendingCalls() now treats empty args as complete; only non-empty-but-invalid JSON counts as truncated. Test updated to match.

3. Housekeeping — done. docs/issues/80-20260823-issue184-incomplete-toolcall-guard.md added in this PR, plus CHANGELOG and devlog entries.

Full gate green (all 7 checks + 6 accumulator tests). Fi Amanillah

…arguments (ltmoerdani#184)

ltmoerdani#187 made content-delivered streams without [DONE]/finish_reason return
successfully (Muse Spark). But for tool-calling streams that means a stream
cut mid-arguments flushes a tool call whose truncated JSON silently becomes
{} input via parseToolInput — the tool executes with corrupted input.

- ToolCallAccumulator.hasCompletePendingCalls(): every named pending call
  must carry non-empty, parseable JSON-object arguments
- OpenAiResponseExtractor.hasCompletePendingToolCalls() exposes it
- engine: hasCompletePendingWork option — when it returns false on an
  unterminated stream, throw the truncation error instead of returning
  success (responses adapter wired; other transports unaffected)
- 5 new unit tests
… as complete (review ltmoerdani#188)

- flushRemainingToolCalls skips flushing when pending calls are incomplete
  (empty/truncated JSON) — previously the finally-block emitted a corrupted
  LanguageModelToolCallPart before the truncation error surfaced
- hasCompletePendingCalls: empty arguments string now counts as COMPLETE —
  gateways legitimately send no args delta for no-param tools; Copilot's own
  loop normalizes arguments === '' to '{}' (toolCallingLoop.ts:1909). Only
  non-empty invalid JSON counts as truncated
- docs/issues/80 entry per repo convention
@Fahad090NP
Fahad090NP force-pushed the fix/issue-184-incomplete-toolcall-guard branch from b4d9ae3 to 49a3d22 Compare August 24, 2026 02:53
@Fahad090NP

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (picked up #189's merge + the docs-sync commit) — conflicts resolved, all 7 lint checks green. Fi Amanillah

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.

[BUG] GPT 5.6 luna model with tool calling fails

2 participants