{"record":{"id":"5a2dea572b132c36","repo":"decolua/9router","slug":"kiro-tool-use-stop-reason-did-not-include-a-comple","errorCode":null,"errorMessage":"Kiro tool_use stop reason did not include a complete tool call","messagePattern":"Kiro tool_use stop reason did not include a complete tool call","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"open-sse/executors/kiro.js","lineNumber":769,"sourceCode":"          tool_calls: [{ index, function: { arguments: serializedInput } }]\n        });\n        // Tool arguments are billed output like any other completion bytes. They\n        // were never added to totalContentLength, so the /4 estimator in finish()\n        // reported OUT 0 -- or the Math.max floor of 1 -- for every turn whose\n        // entire answer was a tool call.\n        state.totalContentLength += tool.name.length + serializedInput.length;\n        state.hasToolCalls = true;\n      }\n      state.tools.clear();\n      state.bufferedToolBytes = 0;\n      // A declared tool turn that emitted no usable call is only fatal when the\n      // turn produced nothing else. Throwing unconditionally here escaped\n      // emitTools() with provenance \"invalid_tool_call\", which the integrity gate\n      // re-derived into a repair retry -- discarding text the client had already\n      // been promised.\n      if (state.stopReason === \"tool_use\" && !state.hasToolCalls &&\n          !state.hasText && !state.hasReasoning && !state.hasCode) {\n        throw new Error(\"Kiro tool_use stop reason did not include a complete tool call\");\n      }\n    };\n    const processEvent = (event, controller) => {\n      const messageType = event.headers[\":message-type\"];\n      if (messageType === \"error\" || messageType === \"exception\") {\n        fail(\n          controller,\n          \"upstream_eventstream_error\",\n          \"kiro_upstream_eventstream_error\",\n          event.payload?.message || `Kiro upstream sent an EventStream ${messageType}`,\n          { transport_state: \"upstream_error\" }\n        );\n        return false;\n      }\n\n      const eventType = event.headers[\":event-type\"] || \"\";\n      const eventCountKey = KIRO_EVENT_TYPES.has(eventType) ? eventType : \"other\";\n      eventCounts[eventCountKey] = (eventCounts[eventCountKey] || 0) + 1;","sourceCodeStart":751,"sourceCodeEnd":787,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/open-sse/executors/kiro.js#L751-L787","documentation":"KiroExecutor validates that a turn which Kiro terminated with stop reason 'tool_use' actually produced at least one usable tool call (or text/reasoning/code). When emitTools() finishes and the turn declared a tool_use stop but no tool call survived validation (all dropped as unusable) and the turn emitted no other content, this error is thrown. It guards against silently returning an empty completion for a turn the upstream claimed was a tool-use turn, which previously escaped as an 'invalid_tool_call' integrity-gate retry that discarded already-streamed client text.","triggerScenarios":"Upstream Kiro sent a messageStopEvent/metadata with stopReason 'tool_use' but: (a) every toolUseEvent failed validation and was dropped in emitTools (bad input JSON, missing nested tool_call fields), (b) no toolUseEvent arrived at all despite the declared stop reason, or (c) tool_use was merged into the stop reason while only empty content fragments were emitted.","commonSituations":"Kiro upstream protocol changes or model quirks emitting a tool_use stop without matching toolUseEvents; tool input fragments that fail parsedToolInput validation (truncated/invalid JSON arguments); all calls being MCP 'tool_call' payloads missing name/arguments; flaky upstream responses after context truncation.","solutions":["Retry the request once — this is usually a transient upstream inconsistency where the model declared tool_use but never streamed a usable call","Inspect console '[Kiro] dropping unusable tool call' lines emitted just before the error to see which validation failed and why the calls were dropped","Check the request's tool definitions — malformed or oversized tool schemas increase the chance of Kiro emitting truncated/invalid tool input that gets dropped","Verify the Kiro translator route (prefer direct claude:kiro / openai:kiro pairs over the lossy OpenAI double-hop) isn't mangling tool definitions","If reproducible, capture the raw EventStream and file against the upstream/model version — a persistent empty tool_use stop is an upstream bug"],"exampleFix":"// before: unconditional reliance on the declared stop reason\nif (res.stop_reason === 'tool_use' && !res.choices[0].message.tool_calls) throw new Error('no tool call');\n// after: tolerate empty declared-tool turns when other content exists, retry otherwise\nif (res.stop_reason === 'tool_use' && !res.choices[0].message.tool_calls) {\n  if (res.choices[0].message.content) return res; // text was still produced\n  return retryOnce(req); // transient upstream inconsistency\n}","handlingStrategy":"retry","validationCode":"// before calling, ensure tool schemas are well-formed so Kiro gets valid definitions\nfunction validateToolDefs(tools) {\n  if (!Array.isArray(tools)) return false;\n  return tools.every(t => t && typeof t.function?.name === 'string' && t.function.name.trim() &&\n    t.function.parameters?.type === 'object');\n}\nif (!validateToolDefs(request.tools)) console.warn('malformed tool defs increase empty tool_use risk');","typeGuard":"function producedUsableToolCall(res) {\n  const msg = res?.choices?.[0]?.message;\n  return Array.isArray(msg?.tool_calls) && msg.tool_calls.length > 0 &&\n    msg.tool_calls.every(tc => typeof tc?.function?.name === 'string' && tc.function.name.trim());\n}","tryCatchPattern":"try {\n  const res = await client.chat.completions.create(req);\n  if (res.choices[0].finish_reason === 'tool_calls' && !producedUsableToolCall(res)) {\n    if (res.choices[0].message.content) return res; // accept text-only turn\n    throw new EmptyToolUseError();\n  }\n  return res;\n} catch (e) {\n  if (attempts < 2) return withRetry(req, attempts + 1);\n  throw e;\n}","preventionTips":["Retry tool_use turns once automatically before surfacing errors to the user","Keep tool JSON schemas small and valid — oversized schemas raise truncation/drop risk","Prefer direct translator routes for Kiro rather than the lossy OpenAI double-hop","Monitor '[Kiro] dropping unusable tool call' logs to catch systematic tool-input corruption early"],"tags":["kiro","tool-use","streaming","upstream-protocol","sse"],"backgroundTag":"invalid-tool-call","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}