fix(web): dispatch ConsumeResult::ToolCalls in WebUI path#7
fix(web): dispatch ConsumeResult::ToolCalls in WebUI path#7Scooter-DeJean wants to merge 1 commit intoMettaMazza:mainfrom
Conversation
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.
🔴 REJECTED — Governance Violations (3)Violation 1: §3.1 — Missing tests
Required: Add tests covering at minimum:
Violation 2: §1.1 / R11 — File lengthThis PR pushes Required: Split Violation 3: R10 — Missing module doc comment
Required: Add a The bug diagnosis is correct — the |
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_budgetdoesn't belong in the helper — has been applied (call removed; budget enforcement is intentionally deferred to the chain, mirroringplatform_ingest.rs:192-215).What this fixes
stream_consumer::consume_streamreturnsConsumeResult::ToolCalls(vec)fortool_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:377andws_l1.rs:203only matched the singularToolCallvariant, 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()modelgemma-4-31B-it-Q4_K_M.ggufwould trip this.The fix
Adds a
ConsumeResult::ToolCalls(calls)arm in bothws.rs(turn-1 dispatch) andws_l1.rs(chain-iteration dispatch), sharing a newdispatch_leading_tool_callshelper inws_l1.rs. The helper executes all but the last call inline (sendingtool_executing/tool_completedUI events and appending messages), returns the last call so the caller can drive the L1 chain on it. ReturnsNoneon emptyVec(stream_consumer invariant violation), surfaced explicitly by callers viatracing::error!and a WS error event — no silent fall-through.Governance alignment
tracing::error!with context, WS error events surfacedunwrap, no silent fallback, single concernTest note
ws.rsandws_l1.rshave effectively no test scaffolding (ws.rshas onlytest_ws_module_compiles;ws_l1.rshas none). Building a mockableWS sink + providerinfra 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:267andws_stream.rs:115/:169have the sameConsumeResult::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.