Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/adapters/kiro/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,10 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
toolNameMap = built.nameMap;
conversationId = built.conversationId;
completionMode = built.completionMode;
requestSnapshot = structuredClone(parsed);
// Runtime callbacks belong to the original request, not the retry payload.
requestSnapshot = structuredClone(Object.fromEntries(
Object.entries(parsed).filter(([, value]) => typeof value !== "function"),
)) as OcxParsedRequest;
firstRequestBodyBytes = Buffer.byteLength(built.request.body);
requestAbortSignal = incoming?.abortSignal;
return built.request;
Expand Down
1 change: 1 addition & 0 deletions structure/providers/kiro.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ budget until emitted, discarded, or cancelled; replay collectors are released af
retry construction. Native `END_TURN` and `STOP_SEQUENCE` alone do not distinguish
progress from an answer and therefore still require validation. Normal private completion
and real tool calls need no completion retry.
The retry snapshots cloneable request fields without top-level runtime callbacks, leaving the original request intact.

A turn can carry a real tool call and the private completion call together, in either order.
The turn then continues through the real tools: the parser drops the completion call, ends the
Expand Down
22 changes: 18 additions & 4 deletions tests/providers/kiro/kiro-single-final.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,19 @@ function response(frames: Uint8Array[]): Response {
}
afterEach(resetKiroThrottleStateForTests);

async function run(first: Uint8Array[], retry: Uint8Array[], buffered = false) {
async function run(first: Uint8Array[], retry: Uint8Array[], buffered = false, input = structuredClone(parsed)) {
const adapter = createKiroAdapter(provider);
const budget = createTranslatorBudget();
const sends: number[] = [];
const events: AdapterEvent[] = [];
let visibleAtRetry: AdapterEvent[] = [];
let physicalRequests = 0;
const bodies: string[] = [];
try {
const request = await adapter.buildRequest(structuredClone(parsed));
const request = await adapter.buildRequest(input);
const upstream = await adapter.fetchResponse!(request, {
executor: (async () => {
executor: (async (_url, init) => {
bodies.push(String(init?.body));
if (++physicalRequests === 1) return response(first);
visibleAtRetry = events.filter(event => event.type === "text_delta");
return response(retry);
Expand All @@ -59,13 +61,25 @@ async function run(first: Uint8Array[], retry: Uint8Array[], buffered = false) {
else for await (const event of adapter.parseStream(upstream, budget)) events.push(event);
if (buffered) for (const event of events) releaseTranslatedEvent(event, budget);
expect(budget.snapshot().currentBytes).toBe(0);
return { events, sends, physicalRequests, visibleAtRetry };
return { events, sends, physicalRequests, visibleAtRetry, bodies };
} finally {
budget.dispose();
}
}

describe("Kiro single final answer (#6270)", () => {
test("advisor guard survives request building and completion retry", async () => {
const guard: NonNullable<OcxParsedRequest["_advisorGuard"]> = async function* () {};
const input = { ...structuredClone(parsed), _advisorGuard: guard };
const answer = "The workspace is ready.";
const { events, physicalRequests, bodies } = await run([text(answer)], [text(answer)], false, input);
expect(input._advisorGuard).toBe(guard);
expect(physicalRequests).toBe(2);
expect(bodies[1]).toContain("Inspect the workspace.");
expect(bodies[1]).toContain("claude-opus-5.5");
expect(events.at(-1)).toMatchObject({ type: "done", endTurn: true });
});

for (const buffered of [false, true]) {
test.each(["END_TURN", "STOP_SEQUENCE", undefined])(
`plain text ending is held through validation (stop=%s, buffered=${buffered})`,
Expand Down
Loading