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
28 changes: 27 additions & 1 deletion devlog/_plan/260906_release_244_followups/030_kiro_results.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Adjacent Kiro result coalescing

Depends on task-input; class C3. Fix #3734 from recorded Codex code-mode output shape, never by spending live Kiro quota.
Depends on task-input; class C4 for protocol identity. Fix #3734 from recorded Codex code-mode output shape, never by spending live Kiro quota.

## Diff-level change map
- MODIFY src/adapters/kiro.ts pushUser/turn-construction helper: when adding results in immediately adjacent parsed messages, tracked separately from collapsed user turns, combine only adjacent results with identical normalized toolUseId. Append content in exact input order and propagate error if any constituent is error. Preserve images via the adapter's supported representation; ensure no image is dropped or reordered relative to supported content semantics.
Expand All @@ -19,3 +19,29 @@ No Kiro account/OAuth/quota changes, no aggressive malformed-history healing, no

## Source follow-up folded at roadmap lock
Track adjacency in original message iteration; reset on every non-toolResult message including user/developer/assistant, even if pushUser collapses it into one user turn. Retain Kiro images on the current user image list as the existing wire format requires; do not promise unsupported text/image interleaving in the wire. Preserve Co-authored-by: Yrlan <71253160+yrlan-montagnier@users.noreply.github.com>. Local log metadata contains old Kiro activity and is not a current live reproduction.

## Kiro-cycle P refresh on parent b24ed35a
Parent #3743 is verified and ready, still open as this branch base; fixture prerequisite #3745 is merged. Issue #3734 remains open without an author PR. kiroPayloadMessages currently returns parsed.context.messages unchanged, so tracking adjacency at the top of its loop observes original Ocx message barriers even when a reasoning-only assistant is later skipped or user/developer turns collapse.

Concrete source edits in src/adapters/kiro.ts only: priorCalls values retain rawId alongside wireName; validate each result against that exact raw id after normalizing for wire lookup. This rejects different raw ids sharing a replacement/truncation result without banning legitimate paired non-wire ids. Track adjacentRawToolResultId, reset it for every non-toolResult before any early continue; for matching adjacent raw id and last user turn/last wire result, append text content and images, set status error if any constituent isError. Otherwise retain pushUser and final conversation validation. No global dedup, cross-turn merge or normalizer change.

MODIFY tests/providers/kiro/kiro-adapter.test.ts only for regressions: parse a real Codex custom_call plus three adjacent custom outputs (optionally preceded by the parent external task input), assert one ordered result; error remains sticky and images survive including image-only later output; single-result control; A/B/A and user/developer/assistant/reasoning-only barriers reject. Raw-id controls cover pipe/underscore, whitespace, truncation and case mismatches; exact raw pairs still normalize and merge. Keep every orphan/encrypted and catalog test. No new test/layout files.

MODIFY docs-site/src/content/docs/reference/adapters.md Kiro section and structure/04_transports-and-sidecars.md with this bounded contract. Preserve Co-authored-by: Yrlan <71253160+yrlan-montagnier@users.noreply.github.com>. Resolve roadmap review thread PRRT_kwDOS-0Gi86fozIF only after the raw identity fix is verified.

Local evidence limit: saved Kiro conversation data and OCX diagnostic artifacts were inspected for field shapes only; no current Codex multi-output Kiro trace was available. No raw message, id or credential was emitted, and no live Kiro request was made. Synthetic CI fixtures are protocol regression evidence, not a field-success claim.

Dispatch: main owns adapter/docs; bounded worker owns only kiro-adapter.test.ts. Independent A/C reviewers inspect raw identity, original-message adjacency, error/image propagation and unchanged encrypted rejection. Full runtime CI is remote only, including existing Kiro image/adapter tests; live Kiro is forbidden.

## Resumed P after verified guidance parent b7e67d84d
The separate task-guidance cycle is complete, parent3743 P1 is resolved and CI34014313740 is green. Its verified head was merged into this preserved Kiro branch before implementation. Prior Euler review is folded below and must be rechecked before B.

A contiguous group is finalized before any non-toolResult (including skipped reasoning-only assistant), before a different raw id, and after the loop. Track only local bookkeeping: rawId, reference to the fresh KiroToolResult, count, raw text parts and whether this group carried images; never put these fields on wire objects. A single-result group keeps the exact existing normalized text/fallback. For 2+ results, preserve ordered raw text parts except successful empty-exec wrappers, append images and keep any isError sticky. If the whole group has meaningful text, use those parts and remove any first-chunk empty fallback. Preserve whitespace text parts when meaningful text exists. If all text is empty, retain one existing fallback; use the neutral KIRO_EMPTY_TOOL_RESULT_MESSAGE when images or an error flag make an empty-success exec hint inappropriate. Failed exec wrappers are meaningful failure information and remain raw text in multi-result groups even when the incoming isError flag is false; preserve existing FAILED_EXEC_OUTPUT_MESSAGE for a single result. No new normalizer or message template.

Read evidence: normalizeEmptyExecToolResultText distinguishes EMPTY_EXEC_OUTPUT_MESSAGE from FAILED_EXEC_OUTPUT_MESSAGE, and failed wrappers can arrive with isError=false. The wire validator requires at least one nonblank text part for each result; finalize groups before that unchanged validator. Keep the encrypted-content throw ahead of every grouping branch, and enforce exact raw id for every result, not only on coalescing.

Additional regressions: later image-only/empty/success-empty wrapper does not inject placeholders into an already-populated result; initial empty then real text removes the empty hint; all-empty groups retain a valid nonblank result; multi-result failed wrapper retains its failure signal; later encrypted adjacent result still rejects; whitespace between meaningful chunks survives. Existing single empty/failed exec normalization tests must pass unchanged.

## Resumed A dispositions
Accept whitespace concern: collect a nonzero-length raw text part when trim is empty OR the shared normalizer did not classify it as EMPTY_EXEC_OUTPUT_MESSAGE. This preserves whitespace between/before actual text while discarding only true empty-success wrapper text; failed wrappers are never in that drop category. Finalization decides whether the aggregate has meaningful text.
Rebut the need for duplicated tool-name bookkeeping: create the first fresh wire result using the EXISTING normalizeEmptyExecToolResultText(text,{toolName,toolNamespace}) call before registering the group. A one-result group is never rewritten at finalization, so its exact precomputed fallback is retained; no normalization without identity occurs. Multi-result finalization replaces that initial content only with raw aggregate parts (or neutral empty text for image/error groups). Tests pin the existing single-result behavior and no bookkeeping keys on wire.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Adjacent Kiro result implementation

The adapter retains each original call ID beside its normalized wire ID and checks
that exact identity on every result. A local group tracks only adjacent results;
non-tool messages, another ID and end-of-input finalize it before the existing
conversation validator runs. The encrypted-result rejection still happens first.

Single results keep their precomputed, tool-identity-aware normalization. Multiple
results keep ordered raw text, real whitespace and failed-exec wrapper information,
while empty-success wrappers do not become extra messages. An initial empty hint
is replaced when later text exists. Images remain on the user turn with existing
limits, error status is sticky, and entirely text-empty image/error groups use one
neutral fallback. Group bookkeeping remains outside Kiro wire objects.

Regression coverage is added to the existing adapter test file, including the
parent task-input plus code-mode-output sequence, collision controls, barriers,
empty/failed wrappers and images. Yrlan's source contribution is attributed.
Proof is independent review and exact-head hosted CI; saved local metadata did not
contain a current multi-output reproduction and no live Kiro call is performed.
7 changes: 7 additions & 0 deletions docs-site/src/content/docs/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,13 @@ header and does not guarantee a provider cache hit.

- Builds Kiro `conversationState`, maps Codex tools and tool results, and sends image blocks supported
by the Kiro wire.
- Coalesces adjacent outputs from the same original tool call into one Kiro result. Text remains
ordered, images retain the existing per-message limits, and any error flag remains set. User,
developer, assistant or another tool's output ends the group. Distinct original IDs that map
to the same normalized Kiro ID are rejected.
- Combined outputs keep real text and failure information without inserting an empty-output hint
for a later blank chunk. A single result keeps its existing normalization; an entirely text-empty
group receives one fallback, with neutral wording when images or an error flag are present.
- Treats a client `parallel_tool_calls: true` value as permission rather than a wire requirement.
Kiro remains serialized: the routed catalog advertises no parallel-tool capability and the
adapter sends no parallel-control field upstream, but ordinary Codex tool turns are not rejected
Expand Down
62 changes: 54 additions & 8 deletions src/adapters/kiro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import { extractKiroImages, normalizeKiroImages, type KiroImage } from "./kiro-i
import { sniffImageDimensions } from "./anthropic-image-guard";
import { fetchKiroWithRetry, noteKiroTransientThrottle } from "./kiro-retry";
import { convertKiroToolContext } from "./kiro-tools";
import { normalizeEmptyExecToolResultText } from "./exec-tool-result-normalize";
import { EMPTY_EXEC_OUTPUT_MESSAGE, normalizeEmptyExecToolResultText } from "./exec-tool-result-normalize";
import { identifyRoutedModel } from "./identity";
import { buildNonOpenAIToolCatalogNudgeFromNames, isBareShellBridgeTool, isCodexCodeModeExecTool } from "./tool-catalog-nudge";
import {
Expand Down Expand Up @@ -663,7 +663,7 @@ export function buildKiroPayload(
}
const systemPrefix = systemParts.length > 0 ? `${systemParts.join("\n\n")}\n\n` : "";
const turns: KiroTurn[] = [];
const priorCalls = new Map<string, { wireName: string }>();
const priorCalls = new Map<string, { wireName: string; rawId: string }>();
const pushUser = (content: string, images: KiroImage[] = [], toolResults: KiroToolResult[] = []): void => {
const last = turns.at(-1);
if (last?.kind === "user") {
Expand Down Expand Up @@ -695,7 +695,27 @@ export function buildKiroPayload(
}
};

let adjacentResult: {
rawId: string;
result: KiroToolResult;
texts: string[];
count: number;
hasImages: boolean;
} | undefined;
const finishAdjacentResult = (): void => {
if (adjacentResult && adjacentResult.count > 1) {
if (adjacentResult.texts.some(text => text.trim())) {
adjacentResult.result.content = adjacentResult.texts.map(text => ({ text }));
} else if (adjacentResult.hasImages || adjacentResult.result.status === "error") {
adjacentResult.result.content = [{ text: KIRO_EMPTY_TOOL_RESULT_MESSAGE }];
}
}
adjacentResult = undefined;
};

for (const msg of kiroPayloadMessages(parsed)) {
// Original-message adjacency matters even when a turn is collapsed or skipped below.
if (msg.role !== "toolResult") finishAdjacentResult();
if (msg.role === "user" || msg.role === "developer") {
const text = userContentText((msg as { content: string | OcxContentPart[] }).content);
const images = extractKiroImages((msg as { content: string | OcxContentPart[] }).content);
Expand All @@ -714,7 +734,7 @@ export function buildKiroPayload(
if (priorCalls.has(toolUseId)) throw new Error(`Kiro history contains duplicate tool call id ${JSON.stringify(tc.id)}`);
const wireName = namespacedToolName(tc.namespace, tc.name);
const name = registry.alias(wireName);
priorCalls.set(toolUseId, { wireName });
priorCalls.set(toolUseId, { wireName, rawId: tc.id });
return { name, input: (tc.arguments ?? {}) as Record<string, unknown>, toolUseId };
});
if (!text && toolUses.length === 0) {
Expand All @@ -735,26 +755,52 @@ export function buildKiroPayload(
// the task instead of calling text()/notify(). Checked before `text.trim()` because the
// wrapper form ("Script completed\nWall time ...\nOutput:\n") is non-blank and would
// otherwise pass through as if it were real output.
const resultText = normalizeEmptyExecToolResultText(text, {
const normalizedExecText = normalizeEmptyExecToolResultText(text, {
toolName: tr.toolName,
toolNamespace: tr.toolNamespace,
}) ?? (text.trim() ? text : KIRO_EMPTY_TOOL_RESULT_MESSAGE);
});
const resultText = normalizedExecText ?? (text.trim() ? text : KIRO_EMPTY_TOOL_RESULT_MESSAGE);
const images = extractKiroImages(tr.content);
const toolUseId = normalizeToolId(tr.toolCallId);
if (!priorCalls.has(toolUseId)) {
const call = priorCalls.get(toolUseId);
if (!call || call.rawId !== tr.toolCallId) {
throw new Error(`Kiro history contains an orphaned tool result for call ${JSON.stringify(tr.toolCallId)}`);
}
// Keep real whitespace and failed wrappers, but no empty-success wrapper boilerplate.
const rawGroupText = text.length > 0 && (!text.trim() || normalizedExecText !== EMPTY_EXEC_OUTPUT_MESSAGE)
? text : undefined;
const last = turns.at(-1);
if (
adjacentResult?.rawId === tr.toolCallId
&& last?.kind === "user"
&& last.toolResults.at(-1) === adjacentResult.result
) {
adjacentResult.count += 1;
adjacentResult.hasImages ||= images.length > 0;
if (rawGroupText !== undefined) adjacentResult.texts.push(rawGroupText);
last.images.push(...images);
if (tr.isError) adjacentResult.result.status = "error";
continue;
}
finishAdjacentResult();
// Carrier text is a placeholder for an OTHERWISE EMPTY tool-result turn, not a prefix.
// Passing it here would push proxy filler AHEAD of a human instruction that Claude Code
// sends in the same turn (mid-turn steering / queued_command, issue #543), burying the
// newest user intent behind boilerplate. Backfill below only when nothing else speaks.
pushUser("", images, [{
const result: KiroToolResult = {
content: [{ text: resultText }],
status: tr.isError ? "error" : "success",
toolUseId,
}]);
};
pushUser("", images, [result]);
adjacentResult = {
rawId: tr.toolCallId, result,
texts: rawGroupText === undefined ? [] : [rawGroupText],
count: 1, hasImages: images.length > 0,
};
}
}
finishAdjacentResult();

if (turns.length === 0 || turns[0].kind === "assistant") {
turns.unshift({ kind: "user", content: KIRO_CONTINUATION_MESSAGE, images: [], toolResults: [] });
Expand Down
8 changes: 8 additions & 0 deletions structure/04_transports-and-sidecars.md
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,14 @@ with seam heartbeats between bounded units. None of these clocks is a total gene

## Reasoning and tool-result compatibility

Kiro groups only consecutive original-message tool results whose raw call ID exactly matches
the originating call. Its wire-ID map retains the original ID privately so replacement or
truncation collisions cannot join unrelated results. Every non-tool message ends the group,
including a reasoning-only assistant omitted from the Kiro turns. Group finalization preserves
single-result normalization, ordered meaningful raw text and whitespace in multi-result output,
failure text, image order and sticky error status. Empty hints are applied once for an entirely
text-empty group, not once per chunk; local grouping state never enters the wire payload.

`src/responses/task-input.ts` recognizes complete external Codex task-input envelopes
before translated Responses adapters: `function_call_output`, no `call_id` property,
nonblank `id`/`name`/`namespace`, and fully representable nonempty text/image output.
Expand Down
Loading
Loading