-
Notifications
You must be signed in to change notification settings - Fork 975
fix(kiro): let a blocking question be the final answer #3031
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
939c942
docs(devlog): record why the Kiro terminal-wording fix failed and wha…
lidge-jun d36a6b5
fix(kiro): let a blocking question be the final answer so the model s…
lidge-jun 833cf2c
fix(kiro): cover information and clarification, not just decisions
lidge-jun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
130 changes: 130 additions & 0 deletions
130
devlog/_plan/260831_kiro_pause_path_and_answer_shape/000_research.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| # Kiro post-answer tool calls, round two: why the prompt fix failed | ||
|
|
||
| The user reported the same defect a second time, with a hard pair of | ||
| requirements: never recur, and do not regress anything. | ||
|
|
||
| ## The first fix is live and did not work | ||
|
|
||
| PR #3012 (`f5a625cf3`) appended terminal semantics to both injected surfaces. | ||
| That code was serving when the defect recurred: | ||
|
|
||
| | Fact | Value | | ||
| | --- | --- | | ||
| | Serving proxy | PID 55727, port 10100, version 2.37.0 | | ||
| | Proxy start | 09:23:37 | | ||
| | Adapter file mtime | 01:28 (same day, before the start) | | ||
| | `f5a625cf3` ancestry | ancestor of the serving HEAD | | ||
| | Failure | 09:27 | | ||
|
|
||
| So prompt wording is not a sufficient mechanism for this defect. That is the | ||
| finding that governs this unit. | ||
|
|
||
| ## What actually happened | ||
|
|
||
| A 1454-char answer-shaped message and the next `exec` call came out of ONE | ||
| inference, 4ms apart (message `00:27:23.114`, call `00:27:23.118`), with | ||
| `sendCount: 1` and no second upstream request. This was never "the model keeps | ||
| working after the turn ended" — there was no second turn to keep working in. | ||
|
|
||
| The message text is the proof. It ends: | ||
|
|
||
| > ...이 호출 경계 동작은 문서에 아직 정확히 안 들어가 있습니다. 넣어둘까요.레퍼런스에 | ||
| > 있는 "scope persists" 서술이 실측과 어긋나니 그 부분부터 고치겠습니다. | ||
|
|
||
| A permission question, glued with no separator to a sentence that overrides the | ||
| question and proceeds. Two emissions merged into one message: the model asked | ||
| the user something and then answered itself, inside one inference. | ||
|
|
||
| The adapter behaved exactly as designed. `defer()` holds staged text, a real | ||
| tool call proves the turn continues, and the staged prose is released as | ||
| `commentary`. | ||
|
|
||
| ## Root cause: there is no sanctioned way to pause | ||
|
|
||
| In `required` mode the model has three expressible moves and none of them is | ||
| "I have a question, hold for the user": | ||
|
|
||
| - Ordinary prose does not end the turn, by explicit contract. | ||
| - The completion tool means "the task is fully complete and no more work or tool | ||
| calls are needed" — which a pending question is not. | ||
| - A real tool call continues the turn. | ||
|
|
||
| `KIRO_COMPLETION_RETRY_MESSAGE` closes the last door explicitly: *"Do not ask | ||
| the user for another task or emit another progress-only message."* | ||
|
|
||
| So a model that wants to ask has no explicitly endorsed blocking-question move. | ||
| Pausing is mechanically expressible — the ask tool is advertised and works — but | ||
| nothing in the injected contract endorses it, and the one instruction the model | ||
| sees at the moment it fails to complete reads as a ban on asking. Continuing to | ||
| work is the only move the contract actively endorses, which is what it did. | ||
|
|
||
| ## The regression boundary, measured | ||
|
|
||
| Across 644 Kiro rollouts, same-inference prose (>=600 chars) followed by a real | ||
| tool call occurs 26 times. Only 4 have a tail that asks the user something. The | ||
| other 22 are ordinary progress narration and must keep working unchanged. | ||
|
|
||
| Prose length cannot separate them: | ||
|
|
||
| - 4 question-tailed: 1329, 1454, 1697, 1938 chars. | ||
| - 22 legitimate: 608 ... 3141 chars. | ||
|
|
||
| The ranges overlap completely. Any length or volume threshold that catches the | ||
| four also catches legitimate narration, and any threshold that spares the 22 | ||
| also spares the defect. There is no structural discriminator for the general | ||
| prose-plus-tool shape, which an independent read-only design audit confirmed | ||
| from the code: at `flushOpen` the adapter knows only that a non-completion tool | ||
| was emitted, its restored identity, and its arguments. `stopReason` cannot help | ||
| either — Kiro emits `END_TURN` for progress prose, and the adapter deliberately | ||
| ignores it when a real tool exists. | ||
|
|
||
| ## Rejected candidates | ||
|
|
||
| **Gate the answer-shaped prose.** Rejected: it requires a text heuristic to tell | ||
| the 4 from the 22, and the measurement above shows no non-heuristic signal | ||
| exists. A regex would decide whether the user sees their agent's work. | ||
|
|
||
| **Discard prose when a real tool follows** (what `consumeSupersededByCompletion` | ||
| does on the completion path). Rejected: on the real-tool path no replacement | ||
| answer ever arrives, so all 22 legitimate progress messages would silently | ||
| vanish rather than arrive later. | ||
|
|
||
| **Isolate the ask tool** (relay `request_user_input`, fail closed on any later | ||
| real tool from the same inference). Rejected as the primary mechanism: measured | ||
| 8 `request_user_input` calls across 644 rollouts, and the | ||
| ask-then-another-tool shape occurs **0** times. It guards a case that does not | ||
| happen. Shipping a guard for an unreachable path with a test that cannot fail is | ||
| the exact trap this unit's predecessor already fell into once. | ||
|
|
||
| ## What the evidence does support | ||
|
|
||
| `request_user_input` already terminates a Kiro turn cleanly. Measured three | ||
| times in the failing session itself: staged prose, then the ask call, then the | ||
| turn yields and the human answers. The pause path exists and works — the model | ||
| simply did not take it, because the contract never told it to and the retry | ||
| message reads as a ban on asking. | ||
|
|
||
| The completion channel is also content-agnostic: a completion answer yields | ||
| `final_answer` and `done(endTurn: true)` regardless of what the answer says. A | ||
| question delivered there ends the turn correctly today. | ||
|
|
||
| So the gap is not enforcement, it is expressibility. The contract describes two | ||
| states (still working, fully done) for a model that has three (still working, | ||
| done, blocked on the user). The fix is to make the third state expressible and | ||
| to stop forbidding it. | ||
|
|
||
| ## Honest statement of limits | ||
|
|
||
| This unit cannot make the defect impossible without model compliance, and it | ||
| will not claim otherwise. Deterministic prevention would need a structural | ||
| upstream signal that does not exist: Kiro accepts only automatic or no tool | ||
| choice, so the proxy cannot force a typed progress/pause/complete protocol. The | ||
| bad and good event streams are observationally equivalent at the adapter. | ||
|
|
||
| What this unit can do is remove the contradiction that made the defect the | ||
| model's *only* endorsed move: make the blocked-on-user state expressible, and | ||
| stop forbidding it. That is one change and it is testable. It introduces no | ||
| adapter-side regression for the 22 — the non-regression test guards their | ||
| structural output byte for byte — but it is a prompt change, so it can still | ||
| shift what the model chooses to emit. Adapter behavior is pinned; model | ||
| behavior is influenced, not fixed. |
144 changes: 144 additions & 0 deletions
144
devlog/_plan/260831_kiro_pause_path_and_answer_shape/010_wp2_pause_path.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| # wp2 — make the blocked-on-user state expressible | ||
|
|
||
| One change, in the injected contract, carried on every surface that describes | ||
| when to complete. It removes the contradiction that made continuing to work the | ||
| model's only endorsed move. No adapter enforcement is added, because no | ||
| non-heuristic signal exists to enforce on. | ||
|
|
||
| ## A blocking question is a valid final answer | ||
|
|
||
| The pause state rides the channel that already terminates correctly. A | ||
| completion answer yields `final_answer` and `done(endTurn: true)` | ||
| **regardless of what the answer says** — verified in `kiro.ts` at the | ||
| completion terminal. So a question delivered there ends the turn today, with no | ||
| new event type, no new phase, and no change to `endTurn` semantics for real | ||
| tools. | ||
|
|
||
| `KIRO_COMPLETION_INSTRUCTIONS` therefore gains one clause: when the model needs | ||
| a decision from the user before it can continue, that question IS its final | ||
| answer — deliver it through the completion tool and stop, instead of writing the | ||
| question as prose and then answering itself. | ||
|
|
||
| The completion tool's own schema carries the same clause. That description is the | ||
| surface the model reads while CHOOSING a tool, and it admitted only "fully | ||
| complete"; a model holding a blocking question reads the tool as unavailable and | ||
| keeps working, which is the defect. Round one changed only the terminality half of | ||
| this description and left the eligibility half narrow, which is part of why prose | ||
| alone did not move the outcome. The `answer` property description is widened the | ||
| same way, since it is what tells the model what may go in the field. | ||
|
|
||
| This is deliberately NOT conditioned on an ask tool being present. An earlier | ||
| draft keyed it on `request_user_input` appearing in the emitted catalog, which | ||
| would have named a tool that is absent from most turns and contradicted the | ||
| nudge's rule that instruction-only names are not callable (the failure mode | ||
| `0325a5afd` fixed for the catalog nudge). The completion tool is always present | ||
| when this instruction is emitted — that is the condition under which the | ||
| instruction exists at all — so the clause can never name something uncallable. | ||
|
|
||
| `KIRO_COMPLETION_RETRY_MESSAGE` currently ends with *"Do not ask the user for | ||
| another task or emit another progress-only message."* In context that forbids | ||
| soliciting NEW work, but it reads as a blanket ban on asking, and it is the one | ||
| instruction the model sees at exactly the moment it failed to complete. It is | ||
| narrowed to keep forbidding a request for another task while making a blocking | ||
| question an explicitly acceptable final answer. | ||
|
|
||
| Both sites are compared by constant, never by inline literal | ||
| (`currentUim.content !== KIRO_COMPLETION_RETRY_MESSAGE`), and both pinning | ||
| tests assert against the constant, so editing the text cannot desynchronize the | ||
| equality checks that keep the bounded retry from stacking. | ||
|
|
||
| This depends on model compliance and is labeled as such. It is the reason this | ||
| unit does not promise impossibility: the tests prove the contract is delivered, | ||
| not that the model obeys it. | ||
|
|
||
| ## Ask-tool isolation: considered and dropped | ||
|
|
||
| An earlier draft added a `flushOpen` invariant: once a turn emitted the ask | ||
| tool, a later real tool call from the same inference would fail closed. It is not | ||
| being implemented, for three independent reasons. | ||
|
|
||
| It is unreachable. Measured across 644 Kiro rollouts: 8 `request_user_input` | ||
| calls, and the ask-then-another-real-tool shape occurs **0** times. | ||
|
|
||
| It cannot be keyed safely. `nameMap` aliases every REQUESTED tool before | ||
| admission, and admission can omit tools, so `nameMap` membership does not prove | ||
| a tool was emitted. Doing it correctly means threading an explicit emitted-ask | ||
| identity through `buildRequest`, the fallback state, and `parseKiroStream`. | ||
|
|
||
| It can false-positive. `OcxTool` carries no "asks the user" marker, and the | ||
| parser reduces ordinary function tools to the same generic shape, so an unrelated | ||
| bare function named `request_user_input` is indistinguishable from Codex's ask | ||
| tool. Claiming it "cannot affect any ordinary tool" was an overclaim. | ||
|
|
||
| Three costs and a measured zero benefit. The previous unit already shipped and | ||
| then reverted a guard for an unreachable path; this one declines it before the | ||
| commit instead of after. | ||
|
|
||
| ## What is NOT changed, and why | ||
|
|
||
| No prose-shape gate. No length threshold. No discard of staged commentary. The | ||
| measurement in `000_research.md` shows the 4 defect cases (1329-1938 chars) and | ||
| the 22 legitimate ones (608-3141) overlap completely, so every such rule is a | ||
| coin flip on whether the user sees their agent's work. | ||
|
|
||
| No change to `src/router.ts`, `src/server/lifecycle.ts`, or | ||
| `src/server/responses/core.ts`: the Lab core boundary is unrelated and | ||
| `tests/core-lab-boundary.test.ts` guards it. No change to the shared | ||
| `tool-catalog-nudge` sentence or its pinned test. | ||
|
|
||
| ## Tests | ||
|
|
||
| **Red-first, contract shape:** every emitted completion contract carries the | ||
| pause clause, and the retry message permits a blocking question as a final | ||
| answer. Driven red against the current strings before the edit. | ||
|
|
||
| **Non-regression, the 22:** staged progress prose followed by `exec` still | ||
| yields the prose as `commentary` and `done(endTurn: false)` with the tool call | ||
| relayed, byte-identical to today, parameterized across the measured lengths | ||
| 608 / 1329 / 1454 / 1938 / 3141. This is the test that protects the user's | ||
| "회귀없도록" requirement, and it must pass unchanged both before and after the | ||
| diff. | ||
|
|
||
| ## Budget truncation: measured unreachable, pinned by test | ||
|
|
||
| The completion contract is charged last against | ||
| `MAX_KIRO_INJECTED_INSTRUCTION_CHARS` (16384), so in principle a large enough | ||
| injected context could slice the pause clause mid-sentence. Measured against a | ||
| hostile catalog (80 requested tools, 64-character names, 6000-character | ||
| descriptions): | ||
|
|
||
| | Charged item | Chars | | ||
| | --- | --- | | ||
| | Omission notice | 922 | | ||
| | Catalog nudge | 2432 | | ||
| | Total before the contract | 3354 | | ||
| | Headroom | 13030 | | ||
| | Contract | 696 | | ||
|
|
||
| Both charged inputs are structurally capped — the notice names at most 12 tools, | ||
| the nudge is bounded by the 48-tool and 64-character limits — and the caller's | ||
| system prompt is not charged to this budget at all. So no reachable input | ||
| approaches the slice boundary. | ||
|
|
||
| A reservation guard is therefore dead code, and the previous unit already proved | ||
| its test would be vacuous: it passed with the guard removed. Instead this unit | ||
| pins the property that makes truncation unreachable, at BOTH hostile extremes, | ||
| asserting the contract arrives COMPLETE including its closing pause clause: | ||
|
|
||
| - **Largest nudge:** `MAX_KIRO_TOOL_COUNT` admitted tools with unique 64-character | ||
| names and one-character descriptions. Every admitted tool is named in the nudge, | ||
| so this maximizes the charged nudge. Nothing is omitted, which the test asserts | ||
| so it cannot pass while silently charging less than it claims. | ||
| - **Notice plus nudge:** oversized descriptions on twice the tool limit, which | ||
| forces admission to omit and charges the omission notice on top of the nudge. | ||
| The test asserts the notice is actually present for the same reason. | ||
|
|
||
| The 6000-character-description shape alone would not have been enough: it reduces | ||
| the admitted count and therefore shrinks the nudge, so it never probes the | ||
| largest charged input. Both tests fail if a future change lets the notice or the | ||
| nudge grow without bound. A reservation's test could not detect its own removal. | ||
|
|
||
| ## Verification | ||
|
|
||
| `bun run typecheck`, the focused Kiro suites, and `bun run privacy:scan`. The | ||
| full local suite is excluded by explicit user instruction; CI covers it. | ||
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This work package records its implementation and terminal verification in the same commit, but it is added under
devlog/_plan, which advertises it as still open. Move the unit todevlog/_finnow that the outcome and code are present in public history so the repository’s planning state remains accurate.AGENTS.md reference: AGENTS.md:L83-L86
Useful? React with 👍 / 👎.