decolua/9router · error
Kiro tool_use stop reason did not include a complete tool ca
Error message
Kiro tool_use stop reason did not include a complete tool call
What it means
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.
Source
Thrown at open-sse/executors/kiro.js:769
tool_calls: [{ index, function: { arguments: serializedInput } }]
});
// Tool arguments are billed output like any other completion bytes. They
// were never added to totalContentLength, so the /4 estimator in finish()
// reported OUT 0 -- or the Math.max floor of 1 -- for every turn whose
// entire answer was a tool call.
state.totalContentLength += tool.name.length + serializedInput.length;
state.hasToolCalls = true;
}
state.tools.clear();
state.bufferedToolBytes = 0;
// A declared tool turn that emitted no usable call is only fatal when the
// turn produced nothing else. Throwing unconditionally here escaped
// emitTools() with provenance "invalid_tool_call", which the integrity gate
// re-derived into a repair retry -- discarding text the client had already
// been promised.
if (state.stopReason === "tool_use" && !state.hasToolCalls &&
!state.hasText && !state.hasReasoning && !state.hasCode) {
throw new Error("Kiro tool_use stop reason did not include a complete tool call");
}
};
const processEvent = (event, controller) => {
const messageType = event.headers[":message-type"];
if (messageType === "error" || messageType === "exception") {
fail(
controller,
"upstream_eventstream_error",
"kiro_upstream_eventstream_error",
event.payload?.message || `Kiro upstream sent an EventStream ${messageType}`,
{ transport_state: "upstream_error" }
);
return false;
}
const eventType = event.headers[":event-type"] || "";
const eventCountKey = KIRO_EVENT_TYPES.has(eventType) ? eventType : "other";
eventCounts[eventCountKey] = (eventCounts[eventCountKey] || 0) + 1;View on GitHub (pinned to 90b52e06ff)
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
Example fix
// before: unconditional reliance on the declared stop reason
if (res.stop_reason === 'tool_use' && !res.choices[0].message.tool_calls) throw new Error('no tool call');
// after: tolerate empty declared-tool turns when other content exists, retry otherwise
if (res.stop_reason === 'tool_use' && !res.choices[0].message.tool_calls) {
if (res.choices[0].message.content) return res; // text was still produced
return retryOnce(req); // transient upstream inconsistency
} Defensive patterns
Strategy: retry
Validate before calling
// before calling, ensure tool schemas are well-formed so Kiro gets valid definitions
function validateToolDefs(tools) {
if (!Array.isArray(tools)) return false;
return tools.every(t => t && typeof t.function?.name === 'string' && t.function.name.trim() &&
t.function.parameters?.type === 'object');
}
if (!validateToolDefs(request.tools)) console.warn('malformed tool defs increase empty tool_use risk'); Type guard
function producedUsableToolCall(res) {
const msg = res?.choices?.[0]?.message;
return Array.isArray(msg?.tool_calls) && msg.tool_calls.length > 0 &&
msg.tool_calls.every(tc => typeof tc?.function?.name === 'string' && tc.function.name.trim());
} Try / catch
try {
const res = await client.chat.completions.create(req);
if (res.choices[0].finish_reason === 'tool_calls' && !producedUsableToolCall(res)) {
if (res.choices[0].message.content) return res; // accept text-only turn
throw new EmptyToolUseError();
}
return res;
} catch (e) {
if (attempts < 2) return withRetry(req, attempts + 1);
throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Kiro toolUseEvent is empty
- Kiro toolUseEvent is missing a tool name
- Kiro toolUseEvent has an invalid toolUseId
- Kiro tool name changed between fragments
- Kiro tool input changed fragment type
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/5a2dea572b132c36.
Report an issue: GitHub.