Skip to content

fix(web): dispatch ConsumeResult::ToolCalls in WebUI path#7

Open
Scooter-DeJean wants to merge 1 commit intoMettaMazza:mainfrom
Scooter-DeJean:fix/multi-tool-call-dispatch
Open

fix(web): dispatch ConsumeResult::ToolCalls in WebUI path#7
Scooter-DeJean wants to merge 1 commit intoMettaMazza:mainfrom
Scooter-DeJean:fix/multi-tool-call-dispatch

Conversation

@Scooter-DeJean
Copy link
Copy Markdown
Contributor

Pre-reviewed via the #dev-general submission earlier today (the bug-patch markdown). Your verification confirmed the bug is real, the diagnosis is correct, and the fix shape is right. The one revision you flagged — enforce_context_budget doesn't belong in the helper — has been applied (call removed; budget enforcement is intentionally deferred to the chain, mirroring platform_ingest.rs:192-215).

What this fixes

stream_consumer::consume_stream returns ConsumeResult::ToolCalls(vec) for tool_calls.len() > 1 (stream_consumer.rs:345). The platform/* handlers (platform_ingest.rs:192, platform_exec.rs:268, platform_reinfer.rs:60) all match this variant. ws.rs:377 and ws_l1.rs:203 only matched the singular ToolCall variant, with the plural case falling through to _ => {} — silently dropping the turn.

Symptom: model emits a response with ≥2 tool calls, SSE [DONE] arrives with tool_calls=N has_content=true, then nothing. No observer audit, no tool execution, no UI text. Reasoning trace renders (it streamed live) but the response body never lands. Black hole.

Hits any model that parallelizes tool calls (Qwen3.5/3.6, Gemma 4, Llama 3.2/3.3, recent Mistral with tools). The default LlamaCppConfig::default() model gemma-4-31B-it-Q4_K_M.gguf would trip this.

The fix

Adds a ConsumeResult::ToolCalls(calls) arm in both ws.rs (turn-1 dispatch) and ws_l1.rs (chain-iteration dispatch), sharing a new dispatch_leading_tool_calls helper in ws_l1.rs. The helper executes all but the last call inline (sending tool_executing / tool_completed UI events and appending messages), returns the last call so the caller can drive the L1 chain on it. Returns None on empty Vec (stream_consumer invariant violation), surfaced explicitly by callers via tracing::error! and a WS error event — no silent fall-through.

Governance alignment

  • §2.4 No silent fallbacks — explicit handling for the empty-Vec case
  • §2.6 Clean error handling — tracing::error! with context, WS error events surfaced
  • §8.1 Root cause — addresses the actual missing dispatch arm, not a symptom
  • §10.2 One concern per PR — single change: dispatch the multi-tool-call variant in WebUI path
  • §11 R3/R4/R7 — no unwrap, no silent fallback, single concern
  • §13.4 (your new §) — N/A; no networking changes

Test note

ws.rs and ws_l1.rs have effectively no test scaffolding (ws.rs has only test_ws_module_compiles; ws_l1.rs has none). Building a mockable WS sink + provider infra is larger than the fix. Per your #dev-general feedback, this gap is acceptable for this PR; happy to bootstrap test infra as a separate follow-up if you want.

Cousin sites flagged

While in here, platform_stream.rs:267 and ws_stream.rs:115/:169 have the same ConsumeResult::ToolCall-only matching pattern. Confirmed by your observer's verification. Per §10.2 these are not in this PR's scope; will file as a separate report after this lands.

Independent of the three mesh PRs (#4, #5, #6) — no blocker between them, can land in either order.

stream_consumer returns ConsumeResult::ToolCalls(vec) when the model
emits >=2 tool calls in one inference turn. ws.rs and ws_l1.rs only
matched the singular ConsumeResult::ToolCall variant; the plural case
fell through to `_ => {}` and silently dropped the turn. The platform/*
handlers (platform_ingest.rs:192, platform_exec.rs:268,
platform_reinfer.rs:60) handle both variants correctly already — this
brings the WebUI dispatch in line with the canonical pattern.

Symptoms: model emits a response with >=2 tool calls, SSE [DONE]
arrives with `tool_calls=N has_content=true`, and then nothing — no
observer audit, no tool execution, no UI text. The reasoning trace
renders (it streamed live) but the response body never lands. Black
hole. Hits any model that parallelizes tool calls (Qwen3.5/3.6, Gemma
4, Llama 3.2/3.3, recent Mistral with tools).

Fix: add a ConsumeResult::ToolCalls(calls) arm in both ws.rs (turn-1
dispatch) and ws_l1.rs (chain-iteration dispatch), sharing a new
`dispatch_leading_tool_calls` helper in ws_l1.rs. The helper executes
all but the last call inline (sending tool_executing /
tool_completed UI events and appending messages), returns the last
call so the caller can drive the L1 chain on it. Returns None on empty
Vec (stream_consumer invariant violation), surfaced explicitly by
callers via tracing::error! and a WS error event — no silent
fall-through.

Per the #dev-general review, enforce_context_budget is intentionally
deferred to the chain (which calls it after each tool result
iteration), mirroring platform_ingest.rs:192-215 which also doesn't
enforce budget during leading-call dispatch.

Cousin sites at platform_stream.rs and ws_stream.rs have the same
pattern; flagged separately as a follow-up report.
@MettaMazza
Copy link
Copy Markdown
Owner

🔴 REJECTED — Governance Violations (3)

Violation 1: §3.1 — Missing tests

pub async fn dispatch_leading_tool_calls() is a new public function with zero tests. §3.1 mandates: "Unit tests for every public function."

Required: Add tests covering at minimum:

  • Empty Vec input → returns None
  • Single-element Vec → returns that element as Some, no leading dispatch
  • Multi-element Vec → dispatches N-1 leading calls, returns the Nth

Violation 2: §1.1 / R11 — File length

This PR pushes src/web/ws.rs from 563 → 587 lines. §1.1 limit is 500. §11 R11 is an immediate rejection trigger.

Required: Split ws.rs as part of this PR. The file must be under 500 lines after merge.

Violation 3: R10 — Missing module doc comment

src/web/ws_l1.rs has no //! module doc comment. This PR adds 71 lines to it. R10 rejects PRs with missing doc comments on modules being modified.

Required: Add a //! doc comment to ws_l1.rs.


The bug diagnosis is correct — the _ => {} silent drop is a real V4-class incident and the fix logic is sound. The code quality is not the issue. The structural compliance is. Fix the three items above and resubmit.

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.

2 participants