Repository navigation
fix(kiro): retry once when a malformed tool_call wrapper precedes any output - #16495
Merged
diegosouzapw merged 4 commits intoOct 15, 2026
Merged
diegosouzapw merged 4 commits into
diegosouzapw merged 4 commits into
Conversation
… output Kiro sometimes emits the MCP wrapper tool `tool_call` without input.name or input.arguments, which ended the agent turn with invalid_kiro_tool_call. Streaming responses now pass through an in-stream repair: chunks are forwarded as they arrive (the role-only start chunk still hands the stream off at once, #15260), and a malformed wrapper seen before any text, reasoning or tool output is suppressed, the upstream is cancelled and the request is re-sent once with a corrective instruction appended to the current user message (validation reason capped at 300 chars, tool arguments never echoed). The retry reuses the branded-gateway -> CodeWhisperer endpoint fallback. A failed retry (second malformed wrapper, non-OK status or network error) ends the stream with kiro_tool_call_repair_retry_failed. Non-streaming requests and wrappers that follow visible output keep the previous behavior. Gated by KIRO_TOOL_CALL_REPAIR_ENABLED (runtime flag, on by default). Co-authored-by: Edison42 <gn00742754@gmail.com> Inspired-by: decolua/9router#2688
Contributor
CI Coverage Report
Coverage artifact was not available for this run. |
…o repair identifier Condense a two-line comment so the new SAFE_PUBLIC_ERROR_IDENTIFIERS entry does not grow the frozen file, and drop the now-unneeded 1217->1218 rebaseline.
diegosouzapw
deleted the
fix/port-pr-2688-kiro-tool-call-wrapper-repair
branch
October 18, 2026 23:50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
tool_callwrapper (missinginput.name/input.arguments) before any visible output, OmniRoute now suppresses it and re-sends the request once with a corrective instruction, instead of ending the agent turn withinvalid_kiro_tool_call.KIRO_TOOL_CALL_REPAIR_ENABLED(on by default, toggleable via env or the dashboard/settings override like every other flag).Attribution
Thanks to @SemonCat for the original implementation.
Changes
open-sse/executors/kiro/toolCallRepair.ts(new leaf): in-stream repair wrapper,buildKiroToolCallRepairPayload(appends the instruction toconversationState.currentMessage.userInputMessage.content, validation reason capped at 300 chars, tool arguments never echoed), meaningful-chunk detection (role-only start chunks do not count as output), flag reader.open-sse/executors/kiro.ts: the endpoint loop (branded gateway → CodeWhisperer fallback) is now a localsend()reused by the retry;transformEventStreamToSSEacceptsonInvalidToolCall(suppress the error frame) andresponseId(the retry keeps the first attempt's chunk id); streaming responses are wrapped when the flag is on.open-sse/utils/error.ts+ fixture:kiro_tool_call_repair_retry_failedadded to the public error identifier vocabulary.featureFlagDefinitions.ts,docs/reference/FEATURE_FLAGS.md(+.i18n-state.jsonsource hash), flag-count tests 85 → 86.config/quality/file-size-baseline.json:error.ts1217 → 1218 (one vocabulary line, note added).eslint-suppressions.json: kiro.tsno-unused-vars3 → 2 (the refactor removed one pre-existing violation).Design notes (deviations from a header-holding gate):
execute()until the first output. Kiro hands the stream off on its first role-only chunk (fix(resilience): content-stall watchdog aborts live streams during reasoning/thinking phases (signature, encrypted & redacted deltas not counted as output) #15260, readiness/content-stall watchdogs depend on it), so holding headers would regress that contract and TTFT. Consequence: headers are already sent when the retry happens, so a non-OK retry (e.g. 429) ends the stream withkiro_tool_call_repair_retry_failed(HTTP <status>in the message, upstream body not echoed) instead of reaching account fallback. That is the same outcome as today (turn fails), never worse.invalid_kiro_tool_callas before. With inline<thinking>enabled this limits the repair to turns whose first output is the tool call.Test plan
node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/kiro-tool-call-repair.test.ts(isolatedDATA_DIR,env -u OMNIROUTE_API_KEY)ℹ tests 10 ℹ pass 5 ℹ fail 5(flag missing; malformed→valid did 1 fetch instead of 2; retry-failed / non-OK retry / cancel cases failed)ℹ tests 10 ℹ pass 10 ℹ fail 0— covers flag on (default) and flag off (KIRO_TOOL_CALL_REPAIR_ENABLED=false→ single attempt,invalid_kiro_tool_call), malformed→valid, malformed→malformed, malformed→429, text-then-malformed (no retry), early text release before upstream completes, client cancel, non-stream, payload builder.ℹ tests 312 ℹ pass 311 ℹ fail 1— the one failure (server-owned-tool-loop-flag.test.ts› pt-BR label ofSERVER_OWNED_TOOL_LOOP_ENABLED) reproduces with the base version of that test file and is unrelated (base-red).npm run typecheck:core→ exit 0npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files>→ 0 errors;npx prettier --check <changed files>→ cleannode scripts/check/check-file-size.mjs --base-ref origin/release/v3.8.52→ OKnpm run check:docs-all→ exit 0node scripts/check/check-pr-test-policy.mjs→ PASS;GITHUB_BASE_REF=release/v3.8.52 npm run check:test-masking→ OK (no weakening)