decolua/9router · error
Kiro tool name changed between fragments
Error message
Kiro tool name changed between fragments
What it means
Kiro streams a single tool call as multiple toolUseEvent fragments keyed by toolUseId. When a fragment arrives for an id already registered, the executor requires the name to match the name recorded when the tool was first seen; a mismatch means two different logical tool calls are sharing one id, which would silently merge their inputs into one corrupt call. The executor throws to preserve integrity of the emitted tool_calls.
Source
Thrown at open-sse/executors/kiro.js:848
for (const value of values) {
const name = typeof value?.name === "string" ? value.name.trim() : "";
if (!name) throw new Error("Kiro toolUseEvent is missing a tool name");
let id;
if (value.toolUseId == null) {
id = `call_${created}_${state.tools.size + 1}`;
} else if (typeof value.toolUseId !== "string" || !value.toolUseId.trim()) {
throw new Error("Kiro toolUseEvent has an invalid toolUseId");
} else {
id = value.toolUseId;
}
let tool = state.tools.get(id);
if (!tool) {
tool = { id, name };
state.tools.set(id, tool);
state.bufferedToolBytes += encoder.encode(id).byteLength + encoder.encode(name).byteLength + 32;
assertToolBufferBound();
} else if (tool.name !== name) {
throw new Error("Kiro tool name changed between fragments");
}
appendToolInput(tool, value.input);
}
} else if (eventType === "messageStopEvent") {
state.explicitStop = true;
const reason = normalizeStopReason(
event.payload?.stopReason ?? event.payload?.stop_reason
) || (state.sawToolUse ? "tool_use" : "end_turn");
const merged = mergeStopReason(state.stopReason, reason);
if (merged !== state.stopReason) state.terminalProvenance = "message_stop_event";
state.stopReason = merged;
} else if (eventType === "metadataEvent" || eventType === "MetadataEvent") {
const metadata = event.payload?.metadataEvent || event.payload?.metadata || event.payload;
const reason = normalizeStopReason(metadata?.stopReason ?? metadata?.stop_reason);
if (reason) {
state.explicitStop = true;
const merged = mergeStopReason(state.stopReason, reason);
if (merged !== state.stopReason) state.terminalProvenance = "metadata_stop_reason";View on GitHub (pinned to 90b52e06ff)
Solutions
- Retry the request once — mid-stream id/name inconsistency is usually a transient upstream fault
- Log both the existing tool.name and the incoming fragment name with the shared id to identify whether it's an id collision or a genuine rename
- If names differ only by case/whitespace, normalize names before comparison (trim/lowercase) if upstream is inconsistent
- If the upstream genuinely reuses ids across distinct calls, namespace ids per call sequence instead of throwing
- Check for collisions between synthesized ids (call_<created>_<n>) and real upstream ids; prefix synthesized ids distinctly
Example fix
// before: throw on any name change for a known id
} else if (tool.name !== name) {
throw new Error('Kiro tool name changed between fragments');
}
// after: treat the mismatch as a new call by minting a fresh id
} else if (tool.name !== name) {
const freshId = `${tool.id}_r${state.tools.size}`;
tool = { id: freshId, name };
state.tools.set(freshId, tool);
console.warn(`[Kiro] toolUseId ${id} reused with different name, split into ${freshId}`);
} Defensive patterns
Strategy: retry
Validate before calling
// on the client side, validate that returned tool_call ids are unique before executing
const ids = res.choices[0].message.tool_calls?.map(tc => tc.id) ?? [];
if (new Set(ids).size !== ids.length) console.warn('duplicate tool_call ids returned'); Type guard
function fragmentsConsistent(events) {
const byId = new Map();
for (const ev of events.filter(e => e.type === 'toolUseEvent')) {
const prev = byId.get(ev.toolUseId);
if (prev && prev.name !== ev.name) return false;
byId.set(ev.toolUseId, ev);
}
return true;
} Try / catch
try {
return await streamKiro(req);
} catch (e) {
if (/tool name changed between fragments/.test(e.message) && attempts < 2) return retry(req);
throw e;
} Prevention
- Retry once on fragment inconsistency before failing the client turn
- If the upstream omits toolUseId, ensure synthesized ids are namespaced so they cannot collide with real upstream ids
- Limit aggressive parallel tool-call prompting (parallel tool use) with models known to reuse ids sloppily
- Log old vs new name on mismatch to distinguish upstream id collision from genuine rename before changing handling
When it happens
Trigger: Two toolUseEvent streams reuse the same toolUseId but carry different 'name' values — e.g. upstream id-collision, a synthesized id (call_<created>_<n>) colliding because toolUseId was absent and fragment order shifted, or an upstream bug renaming the tool mid-call.
Common situations: Upstream generating duplicate toolUseIds within one turn; models emitting parallel tool calls with sloppy id allocation; protocol changes splitting name/input differently across fragments; state.toolUseId synthesis (call_created_N) colliding with a real upstream id of the same form.
Related errors
- Kiro toolUseEvent is empty
- Kiro toolUseEvent is missing a tool name
- Kiro toolUseEvent has an invalid toolUseId
- Kiro tool_use stop reason did not include a complete tool ca
- AWS EventStream frame is shorter than 16 bytes
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/2640f04f0611b47b.
Report an issue: GitHub.