@bitkyc08/opencodex 2.52.0-preview.20260912 → 2.53.0-preview.20260913
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/index-BBOZWGB6.css +1 -0
- package/gui/dist/assets/index-D7ynYo2K.js +128 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/Cargo.lock +130 -0
- package/native/remote-workspace-helper/Cargo.toml +24 -0
- package/native/remote-workspace-helper/src/main.rs +49 -0
- package/native/remote-workspace-helper/src/protocol.rs +246 -0
- package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
- package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
- package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
- package/package.json +6 -1
- package/src/adapters/anthropic-image-normalize.ts +30 -2
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/base.ts +8 -2
- package/src/adapters/cursor/cursor-errors.ts +12 -0
- package/src/adapters/cursor/thread-continuity.ts +93 -0
- package/src/adapters/cursor.ts +104 -73
- package/src/adapters/devin/cloud-direct/chat.ts +312 -23
- package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
- package/src/adapters/devin/live-models.ts +70 -3
- package/src/adapters/devin.ts +281 -21
- package/src/adapters/google-wire-compiler.ts +14 -6
- package/src/adapters/google.ts +22 -8
- package/src/adapters/kiro/adapter.ts +316 -0
- package/src/adapters/kiro/conversation.ts +136 -0
- package/src/adapters/kiro/payload.ts +432 -0
- package/src/adapters/kiro/reasoning.ts +56 -0
- package/src/adapters/kiro/stream.ts +1153 -0
- package/src/adapters/kiro/usage.ts +223 -0
- package/src/adapters/kiro/wire.ts +76 -0
- package/src/adapters/kiro.ts +8 -2319
- package/src/adapters/mimo-free.ts +1 -1
- package/src/adapters/openai-chat-images.ts +101 -0
- package/src/adapters/openai-chat.ts +201 -181
- package/src/adapters/openai-responses.ts +92 -224
- package/src/adapters/registry.ts +0 -7
- package/src/adapters/run-turn-queue.ts +13 -6
- package/src/bridge.ts +14 -15
- package/src/chat/inbound.ts +29 -4
- package/src/chat/outbound.ts +145 -107
- package/src/claude/desktop-profile.ts +4 -6
- package/src/cli/account-api.ts +14 -0
- package/src/cli/account-extended.ts +1 -1
- package/src/cli/account-history.ts +60 -0
- package/src/cli/account-main.ts +80 -0
- package/src/cli/account.ts +11 -3
- package/src/cli/capabilities.ts +113 -0
- package/src/cli/catalog.ts +109 -0
- package/src/cli/dispatch.ts +9 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +2 -2
- package/src/cli/observe.ts +28 -1
- package/src/cli/opencode.ts +42 -8
- package/src/cli/provider-runtime.ts +11 -1
- package/src/cli/provider.ts +22 -2
- package/src/cli/registry.ts +21 -0
- package/src/cli/remote-workspace.ts +154 -0
- package/src/cli/status.ts +39 -7
- package/src/cli/usage-report.ts +14 -2
- package/src/client/hub-client.ts +34 -0
- package/src/client/hub-state.ts +9 -1
- package/src/codex/account-store.ts +78 -0
- package/src/codex/auth-api.ts +81 -54
- package/src/codex/auth-context.ts +45 -16
- package/src/codex/catalog/effort.ts +1 -1
- package/src/codex/catalog/metadata.ts +3 -6
- package/src/codex/catalog/native-models.ts +4 -4
- package/src/codex/catalog/parsing.ts +2 -20
- package/src/codex/catalog/provider-fetch.ts +10 -1
- package/src/codex/catalog/remote.ts +233 -0
- package/src/codex/catalog/sync.ts +403 -35
- package/src/codex/convergence.ts +1 -1
- package/src/codex/history-manifest.ts +36 -0
- package/src/codex/history-provider.ts +32 -5
- package/src/codex/inject.ts +9 -0
- package/src/codex/main-account.ts +113 -0
- package/src/codex/main-device-reauth-api.ts +89 -0
- package/src/codex/main-device-reauth.ts +217 -0
- package/src/codex/native-residue.ts +9 -2
- package/src/codex/quota-auto-refresh.ts +3 -2
- package/src/codex/quota-capacity.ts +98 -0
- package/src/codex/quota-history.ts +160 -0
- package/src/codex/quota-types.ts +8 -0
- package/src/codex/quota.ts +118 -91
- package/src/codex/refresh.ts +2 -1
- package/src/codex/routing.ts +90 -17
- package/src/codex/sync.ts +33 -4
- package/src/combos/request.ts +19 -1
- package/src/config/multi-agent-surface.ts +61 -0
- package/src/config/provider-validation.ts +176 -0
- package/src/config.ts +213 -11
- package/src/generated/compatibility-version.json +436 -168
- package/src/images/loop.ts +119 -36
- package/src/lib/admission.ts +12 -6
- package/src/lib/redact.ts +7 -0
- package/src/lib/translator-budget.ts +4 -3
- package/src/lib/windows-atomic-replace.ts +1 -0
- package/src/lib/windows-elevation.ts +1 -1
- package/src/oauth/chatgpt-device.ts +62 -5
- package/src/oauth/devin/cli-import.ts +130 -0
- package/src/oauth/devin.ts +63 -8
- package/src/oauth/index.ts +29 -14
- package/src/oauth/kiro.ts +18 -6
- package/src/oauth/login-cli.ts +9 -1
- package/src/oauth/meta-muse-device.ts +464 -0
- package/src/oauth/meta-muse.ts +123 -32
- package/src/oauth/pool-kernel.ts +9 -0
- package/src/oauth/pool-settings-capability.ts +2 -2
- package/src/oauth/store.ts +57 -0
- package/src/oauth/types.ts +31 -0
- package/src/providers/derive.ts +13 -3
- package/src/providers/devin-cli-authmode-migration.ts +57 -35
- package/src/providers/devin-provider-merge-migration.ts +240 -0
- package/src/providers/muse-key-quota.ts +117 -0
- package/src/providers/muse-subscription-usage.ts +14 -2
- package/src/providers/openai-sidecar.ts +25 -3
- package/src/providers/opencode-zen-rate-limit.ts +58 -0
- package/src/providers/provider-id-rewrite.ts +20 -5
- package/src/providers/quota-types.ts +12 -0
- package/src/providers/quota.ts +143 -102
- package/src/providers/reasoning-metadata.ts +543 -0
- package/src/providers/registry.ts +80 -49
- package/src/reasoning-effort.ts +26 -2
- package/src/remote/hub-usage.ts +32 -0
- package/src/remote-control/index.ts +192 -41
- package/src/remote-control/workspace-activation.ts +9 -0
- package/src/remote-control/workspace-agent-connection.ts +366 -0
- package/src/remote-control/workspace-claude-runtime.ts +243 -0
- package/src/remote-control/workspace-codex-runtime.ts +531 -0
- package/src/remote-control/workspace-codex-sandbox.ts +115 -0
- package/src/remote-control/workspace-command-runner.ts +748 -0
- package/src/remote-control/workspace-coordinator.ts +231 -0
- package/src/remote-control/workspace-device.ts +585 -0
- package/src/remote-control/workspace-executable.ts +43 -0
- package/src/remote-control/workspace-executor.ts +397 -0
- package/src/remote-control/workspace-hub.ts +519 -0
- package/src/remote-control/workspace-pi-runtime.ts +382 -0
- package/src/remote-control/workspace-process.ts +129 -0
- package/src/remote-control/workspace-rpc.ts +304 -0
- package/src/remote-control/workspace-runtime.ts +60 -0
- package/src/remote-control/workspace-secret-store.ts +39 -0
- package/src/remote-control/workspace-sessions.ts +799 -0
- package/src/remote-control/workspace-tool-bridge.ts +192 -0
- package/src/responses/code-mode-helper-compat.ts +22 -3
- package/src/responses/hosted-tool-policy.ts +0 -1
- package/src/responses/muse-tool-name-alias.ts +379 -0
- package/src/responses/plaintext-v2-agent-messages.ts +902 -0
- package/src/router.ts +7 -0
- package/src/routing/compatibility/behavior.ts +0 -1
- package/src/server/audio-client.ts +64 -0
- package/src/server/audio-dictation.ts +91 -0
- package/src/server/audio-live.ts +185 -0
- package/src/server/audio-transcriptions.ts +183 -0
- package/src/server/audio-upstream.ts +153 -0
- package/src/server/auth-cors.ts +61 -2
- package/src/server/chat-completions.ts +1 -1
- package/src/server/chat-native-sse.ts +92 -48
- package/src/server/chat-native.ts +37 -15
- package/src/server/hub-usage.ts +57 -0
- package/src/server/images.ts +4 -0
- package/src/server/index.ts +722 -57
- package/src/server/lifecycle.ts +5 -6
- package/src/server/live-call-bindings.ts +60 -0
- package/src/server/live.ts +12 -1
- package/src/server/management/agent-settings-routes.ts +25 -4
- package/src/server/management/api-access.ts +37 -0
- package/src/server/management/api-key-usage.ts +7 -2
- package/src/server/management/config-routes.ts +1 -18
- package/src/server/management/context.ts +15 -0
- package/src/server/management/logs-usage-routes.ts +2 -0
- package/src/server/management/oauth-account-routes.ts +39 -12
- package/src/server/management/provider-routes.ts +125 -2
- package/src/server/management/remote-workspace-routes.ts +140 -0
- package/src/server/management/route-registry.ts +15 -0
- package/src/server/management/usage-aggregate-cache.ts +14 -15
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management-api.ts +23 -0
- package/src/server/ports.ts +17 -0
- package/src/server/relay-eager.ts +4 -1
- package/src/server/relay.ts +70 -10
- package/src/server/request-decompress.ts +6 -3
- package/src/server/responses/agent-task-recovery.ts +25 -32
- package/src/server/responses/codex-auth-error.ts +11 -0
- package/src/server/responses/codex-ws-exchange.ts +52 -3
- package/src/server/responses/codex-ws-wire.ts +55 -0
- package/src/server/responses/compact.ts +9 -1
- package/src/server/responses/core.ts +337 -73
- package/src/server/responses/encrypted-payload.ts +45 -2
- package/src/server/responses/ws-upstream.ts +4 -1
- package/src/server/responses-self-named-namespace-scrub.ts +1 -3
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/search.ts +3 -0
- package/src/server/sse-payload-rewrite.ts +136 -51
- package/src/server/ws-bridge.ts +35 -1
- package/src/service/cli.ts +372 -0
- package/src/service/diagnostics.ts +340 -0
- package/src/service/guards.ts +303 -0
- package/src/service/health.ts +222 -0
- package/src/service/launchd.ts +853 -0
- package/src/service/orchestration.ts +617 -0
- package/src/service/repair.ts +334 -0
- package/src/service/state.ts +363 -0
- package/src/service/systemd.ts +229 -0
- package/src/service/windows-ops.ts +690 -0
- package/src/service/windows-scheduler.ts +769 -0
- package/src/service/windows-taskxml.ts +613 -0
- package/src/service.ts +22 -5550
- package/src/storage/cleanup/db.ts +258 -0
- package/src/storage/cleanup/execute.ts +358 -0
- package/src/storage/cleanup/paths.ts +189 -0
- package/src/storage/cleanup/pending.ts +140 -0
- package/src/storage/cleanup/preview.ts +292 -0
- package/src/storage/cleanup/reconcile.ts +347 -0
- package/src/storage/cleanup/restore.ts +932 -0
- package/src/storage/cleanup/satellite.ts +474 -0
- package/src/storage/cleanup/staging.ts +129 -0
- package/src/storage/cleanup/types.ts +98 -0
- package/src/storage/cleanup.ts +49 -3127
- package/src/types/accounts.ts +2 -0
- package/src/types/config.ts +13 -12
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +17 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +127 -0
- package/src/usage/log.ts +58 -1
- package/src/vision/eligibility.ts +13 -2
- package/src/web-search/loop.ts +56 -3
- package/gui/dist/assets/index-D_t6sCWs.js +0 -115
- package/gui/dist/assets/index-EdoPnm9_.css +0 -1
- package/src/adapters/devin-cli/acp.ts +0 -204
- package/src/adapters/devin-cli/adapter.ts +0 -345
- package/src/adapters/devin-cli/binary.ts +0 -69
- package/src/adapters/devin-cli/models.ts +0 -57
- package/src/oauth/devin-cli.ts +0 -149
- package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
|
@@ -0,0 +1,1153 @@
|
|
|
1
|
+
import { decodeEventStream } from "../../lib/eventstream-decoder";
|
|
2
|
+
import { debugProviderDiagnostic } from "../../lib/debug";
|
|
3
|
+
import {
|
|
4
|
+
isTranslatorBudgetExceededError,
|
|
5
|
+
type TranslatorBudget,
|
|
6
|
+
} from "../../lib/translator-budget";
|
|
7
|
+
import type { AdapterEvent, OcxUsage } from "../../types";
|
|
8
|
+
import { recordKiroCalibration, rekeyKiroCalibration } from "../kiro-calibration";
|
|
9
|
+
import { KIRO_COMPLETION_TOOL_NAME, type KiroCompletionMode } from "../kiro-constants";
|
|
10
|
+
import {
|
|
11
|
+
classifyKiroEventError,
|
|
12
|
+
classifyKiroHttpError,
|
|
13
|
+
classifyKiroStreamError,
|
|
14
|
+
safeKiroErrorMessage,
|
|
15
|
+
type KiroErrorClassification,
|
|
16
|
+
} from "../kiro-errors";
|
|
17
|
+
import { parseKiroEvent } from "../kiro-events";
|
|
18
|
+
import { noteKiroTransientThrottle } from "../kiro-retry";
|
|
19
|
+
import { KiroThinkingParser } from "../kiro-thinking";
|
|
20
|
+
import { isCompleteKiroToolInput, kiroTruncationErrorMessage } from "../kiro-truncation";
|
|
21
|
+
import { isValidKiroConversationId } from "../kiro-wire";
|
|
22
|
+
import { estimateKiroTokens, kiroUpstreamContextWindow } from "./usage";
|
|
23
|
+
|
|
24
|
+
// Stream parsing (shared by parseStream + parseResponse)
|
|
25
|
+
// CodeWhisperer GenerateAssistantResponse ALWAYS returns an AWS eventstream body (there is no
|
|
26
|
+
// non-streaming wire mode), so the streaming bridge and non-streaming Responses path decode the
|
|
27
|
+
// same way — parseResponse just collects what parseStream yields.
|
|
28
|
+
interface KiroAttemptParseResult {
|
|
29
|
+
terminal?: AdapterEvent;
|
|
30
|
+
needsFallback?: boolean;
|
|
31
|
+
usage?: OcxUsage;
|
|
32
|
+
providerState?: { kiro: { conversationId: string } };
|
|
33
|
+
assistantText: string;
|
|
34
|
+
sawReasoning: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface KiroAttemptResult extends KiroAttemptParseResult {
|
|
38
|
+
releaseRetained(): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface KiroAttemptRetention {
|
|
42
|
+
trackReplacement(previousBytes: number, nextBytes: number): void;
|
|
43
|
+
retainEvent(event: AdapterEvent, bytes: number): void;
|
|
44
|
+
releaseEvent(event: AdapterEvent): void;
|
|
45
|
+
releaseAll(): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createKiroAttemptRetention(budget: TranslatorBudget): KiroAttemptRetention {
|
|
49
|
+
let retainedBytes = 0;
|
|
50
|
+
const eventBytes = new Map<AdapterEvent, number>();
|
|
51
|
+
return {
|
|
52
|
+
trackReplacement(previousBytes, nextBytes) {
|
|
53
|
+
retainedBytes = Math.max(0, retainedBytes - previousBytes) + nextBytes;
|
|
54
|
+
},
|
|
55
|
+
retainEvent(event, bytes) {
|
|
56
|
+
retainedBytes += bytes;
|
|
57
|
+
eventBytes.set(event, bytes);
|
|
58
|
+
},
|
|
59
|
+
releaseEvent(event) {
|
|
60
|
+
const bytes = eventBytes.get(event);
|
|
61
|
+
if (bytes === undefined) return;
|
|
62
|
+
eventBytes.delete(event);
|
|
63
|
+
retainedBytes = Math.max(0, retainedBytes - bytes);
|
|
64
|
+
budget.releaseRetained(bytes, { kind: "retained_collectors" });
|
|
65
|
+
},
|
|
66
|
+
releaseAll() {
|
|
67
|
+
if (retainedBytes > 0) budget.releaseRetained(retainedBytes, { kind: "retained_collectors" });
|
|
68
|
+
retainedBytes = 0;
|
|
69
|
+
eventBytes.clear();
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface KiroFallbackAttempt {
|
|
75
|
+
response: Response;
|
|
76
|
+
inputTokens: number;
|
|
77
|
+
contextInputEstimate: number;
|
|
78
|
+
nameMap: Map<string, string>;
|
|
79
|
+
conversationId: string;
|
|
80
|
+
releaseRequestBody?: () => void;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function appendedUtf8Bytes(previous: string, previousBytes: number, fragment: string): number {
|
|
84
|
+
let nextBytes = previousBytes + Buffer.byteLength(fragment);
|
|
85
|
+
const previousLast = previous.charCodeAt(previous.length - 1);
|
|
86
|
+
const fragmentFirst = fragment.charCodeAt(0);
|
|
87
|
+
if (previousLast >= 0xd800 && previousLast <= 0xdbff
|
|
88
|
+
&& fragmentFirst >= 0xdc00 && fragmentFirst <= 0xdfff) {
|
|
89
|
+
nextBytes -= 2;
|
|
90
|
+
}
|
|
91
|
+
return nextBytes;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Exact UTF-8 size JSON.stringify() will use for a string, without materializing that copy. */
|
|
95
|
+
export function jsonStringSerializedUtf8Bytes(value: string): number {
|
|
96
|
+
let bytes = 2; // Opening and closing quotes.
|
|
97
|
+
for (let index = 0; index < value.length; index++) {
|
|
98
|
+
const code = value.charCodeAt(index);
|
|
99
|
+
if (code === 0x22 || code === 0x5c) {
|
|
100
|
+
bytes += 2;
|
|
101
|
+
} else if (code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d) {
|
|
102
|
+
bytes += 2;
|
|
103
|
+
} else if (code < 0x20) {
|
|
104
|
+
bytes += 6;
|
|
105
|
+
} else if (code <= 0x7f) {
|
|
106
|
+
bytes += 1;
|
|
107
|
+
} else if (code <= 0x7ff) {
|
|
108
|
+
bytes += 2;
|
|
109
|
+
} else if (code >= 0xd800 && code <= 0xdbff) {
|
|
110
|
+
const next = value.charCodeAt(index + 1);
|
|
111
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
112
|
+
bytes += 4;
|
|
113
|
+
index++;
|
|
114
|
+
} else {
|
|
115
|
+
bytes += 6;
|
|
116
|
+
}
|
|
117
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
118
|
+
bytes += 6;
|
|
119
|
+
} else {
|
|
120
|
+
bytes += 3;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return bytes;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface KiroContextWindowState {
|
|
127
|
+
value?: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type KiroFallbackFactory = (
|
|
131
|
+
conversationId: string | undefined,
|
|
132
|
+
assistantText: string,
|
|
133
|
+
sawReasoning: boolean,
|
|
134
|
+
budget: TranslatorBudget,
|
|
135
|
+
) => Promise<KiroFallbackAttempt>;
|
|
136
|
+
|
|
137
|
+
function mergeKiroUsage(
|
|
138
|
+
first: OcxUsage | undefined,
|
|
139
|
+
second: OcxUsage | undefined,
|
|
140
|
+
preserveFirstContextGrowth = false,
|
|
141
|
+
): OcxUsage | undefined {
|
|
142
|
+
if (!first) return second;
|
|
143
|
+
if (!second) return first;
|
|
144
|
+
const sumOptional = (key: keyof OcxUsage): number | undefined => {
|
|
145
|
+
const a = first[key];
|
|
146
|
+
const b = second[key];
|
|
147
|
+
return typeof a === "number" || typeof b === "number"
|
|
148
|
+
? (typeof a === "number" ? a : 0) + (typeof b === "number" ? b : 0)
|
|
149
|
+
: undefined;
|
|
150
|
+
};
|
|
151
|
+
const totalTokens = typeof first.totalTokens === "number" && typeof second.totalTokens === "number"
|
|
152
|
+
? first.totalTokens + second.totalTokens
|
|
153
|
+
: undefined;
|
|
154
|
+
const carriedContextTotal = preserveFirstContextGrowth && typeof first.contextTotalTokens === "number"
|
|
155
|
+
? first.contextTotalTokens + second.outputTokens
|
|
156
|
+
: undefined;
|
|
157
|
+
const combinedOutputTokens = first.outputTokens + second.outputTokens;
|
|
158
|
+
return {
|
|
159
|
+
inputTokens: first.inputTokens + second.inputTokens,
|
|
160
|
+
outputTokens: combinedOutputTokens,
|
|
161
|
+
...(typeof first.contextTotalTokens === "number" || typeof second.contextTotalTokens === "number"
|
|
162
|
+
? {
|
|
163
|
+
contextTotalTokens: Math.max(
|
|
164
|
+
first.contextTotalTokens ?? 0,
|
|
165
|
+
second.contextTotalTokens ?? 0,
|
|
166
|
+
carriedContextTotal ?? 0,
|
|
167
|
+
combinedOutputTokens,
|
|
168
|
+
),
|
|
169
|
+
}
|
|
170
|
+
: {}),
|
|
171
|
+
...(totalTokens !== undefined ? { totalTokens } : {}),
|
|
172
|
+
...(sumOptional("cachedInputTokens") !== undefined ? { cachedInputTokens: sumOptional("cachedInputTokens") } : {}),
|
|
173
|
+
...(sumOptional("cacheReadInputTokens") !== undefined ? { cacheReadInputTokens: sumOptional("cacheReadInputTokens") } : {}),
|
|
174
|
+
...(sumOptional("cacheCreationInputTokens") !== undefined ? { cacheCreationInputTokens: sumOptional("cacheCreationInputTokens") } : {}),
|
|
175
|
+
...(sumOptional("reasoningOutputTokens") !== undefined ? { reasoningOutputTokens: sumOptional("reasoningOutputTokens") } : {}),
|
|
176
|
+
...(first.estimated || second.estimated ? { estimated: true } : {}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function retryableKiroIncomplete(
|
|
181
|
+
reason: string,
|
|
182
|
+
message: string,
|
|
183
|
+
usage: OcxUsage,
|
|
184
|
+
providerState: { kiro: { conversationId: string } } | undefined,
|
|
185
|
+
retryable = true,
|
|
186
|
+
): AdapterEvent {
|
|
187
|
+
return {
|
|
188
|
+
type: "incomplete",
|
|
189
|
+
reason,
|
|
190
|
+
message,
|
|
191
|
+
usage,
|
|
192
|
+
retryable,
|
|
193
|
+
endTurn: false,
|
|
194
|
+
...(providerState ? { providerState } : {}),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Catch-path retryability for #519: only transport/socket failures with no emitted output
|
|
200
|
+
* are replay-safe. Malformed event payloads (`invalid Kiro …`) and any post-output failure
|
|
201
|
+
* stay terminal — same spirit as cursor's emittedOutput gate.
|
|
202
|
+
*/
|
|
203
|
+
export function isRetryableKiroStreamCatchError(err: unknown, emittedOutput: boolean): boolean {
|
|
204
|
+
if (emittedOutput) return false;
|
|
205
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
206
|
+
if (/^invalid Kiro\b/i.test(message)) return false;
|
|
207
|
+
// Include Smithy/eventstream truncation (`eventstream: truncated message at end of stream`):
|
|
208
|
+
// partial frame + clean EOF with zero output is the same replay-safe class as a socket close.
|
|
209
|
+
return /socket connection was closed|connection(?: was)? closed unexpectedly|ECONNRESET|EPIPE|UND_ERR_|fetch failed|decoder failed|premature close|other side closed|unexpected EOF|network connection lost|terminated|truncated message at end of stream|eventstream:\s*truncated/i
|
|
210
|
+
.test(message);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Native clean-stop reason eligible for bounded private-completion validation. */
|
|
214
|
+
const KIRO_END_TURN_STOP_REASON = "END_TURN";
|
|
215
|
+
|
|
216
|
+
async function* parseKiroAttempt(
|
|
217
|
+
response: Response,
|
|
218
|
+
budget: TranslatorBudget,
|
|
219
|
+
mode: KiroCompletionMode,
|
|
220
|
+
modelId: string | undefined,
|
|
221
|
+
inputTokens: number,
|
|
222
|
+
contextWindowState: KiroContextWindowState,
|
|
223
|
+
nameMap: Map<string, string> | undefined,
|
|
224
|
+
conversationId: string | undefined,
|
|
225
|
+
contextInputEstimate?: number,
|
|
226
|
+
/** True when an earlier attempt already flushed visible content to the client (#520). */
|
|
227
|
+
priorEmittedOutput = false,
|
|
228
|
+
): AsyncGenerator<AdapterEvent, KiroAttemptResult> {
|
|
229
|
+
// `required` mode holds staged commentary until a real tool call or terminal metadata identifies
|
|
230
|
+
// the attempt boundary. Anything the inner parser leaves behind is flushed before the terminal.
|
|
231
|
+
const deferred: AdapterEvent[] = [];
|
|
232
|
+
const retention = createKiroAttemptRetention(budget);
|
|
233
|
+
// Shared box: the inner parser stages its calibration observation here on the completion path,
|
|
234
|
+
// and this wrapper decides whether the attempt was terminal enough to commit it. A box rather
|
|
235
|
+
// than a return field because the completion path has a dozen terminal returns and threading a
|
|
236
|
+
// field through every one of them is exactly the kind of edit that misses one.
|
|
237
|
+
const attemptCalibration: { value?: { conversationId: string; estimated: number; charged: number } } = {};
|
|
238
|
+
const attempt = parseKiroAttemptEvents(
|
|
239
|
+
response,
|
|
240
|
+
budget,
|
|
241
|
+
mode,
|
|
242
|
+
modelId,
|
|
243
|
+
inputTokens,
|
|
244
|
+
contextWindowState,
|
|
245
|
+
nameMap,
|
|
246
|
+
conversationId,
|
|
247
|
+
deferred,
|
|
248
|
+
retention,
|
|
249
|
+
attemptCalibration,
|
|
250
|
+
contextInputEstimate,
|
|
251
|
+
priorEmittedOutput,
|
|
252
|
+
);
|
|
253
|
+
let handedOff = false;
|
|
254
|
+
try {
|
|
255
|
+
const result = yield* attempt;
|
|
256
|
+
// A staged observation only counts when this attempt is the LAST one for the user turn. An
|
|
257
|
+
// attempt that asks for the bounded fallback streams again against a rebuilt payload, so
|
|
258
|
+
// committing here would move the factor twice for one turn and score the second observation
|
|
259
|
+
// against a payload the first had already inflated.
|
|
260
|
+
const staged = attemptCalibration.value;
|
|
261
|
+
attemptCalibration.value = undefined;
|
|
262
|
+
if (staged && !result.needsFallback) {
|
|
263
|
+
recordKiroCalibration(staged.conversationId, staged.estimated, staged.charged);
|
|
264
|
+
}
|
|
265
|
+
for (const event of deferred.splice(0)) {
|
|
266
|
+
try { yield event; } finally { retention.releaseEvent(event); }
|
|
267
|
+
}
|
|
268
|
+
handedOff = true;
|
|
269
|
+
return { ...result, releaseRetained: () => retention.releaseAll() };
|
|
270
|
+
} finally {
|
|
271
|
+
if (!handedOff) retention.releaseAll();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function* parseKiroAttemptEvents(
|
|
276
|
+
response: Response,
|
|
277
|
+
budget: TranslatorBudget,
|
|
278
|
+
mode: KiroCompletionMode,
|
|
279
|
+
modelId: string | undefined,
|
|
280
|
+
inputTokens: number,
|
|
281
|
+
contextWindowState: KiroContextWindowState,
|
|
282
|
+
nameMap: Map<string, string> | undefined,
|
|
283
|
+
conversationId: string | undefined,
|
|
284
|
+
deferred: AdapterEvent[],
|
|
285
|
+
retention: KiroAttemptRetention,
|
|
286
|
+
attemptCalibration: { value?: { conversationId: string; estimated: number; charged: number } },
|
|
287
|
+
contextInputEstimate?: number,
|
|
288
|
+
priorEmittedOutput = false,
|
|
289
|
+
): AsyncGenerator<AdapterEvent, KiroAttemptParseResult> {
|
|
290
|
+
const emptyResult = (): KiroAttemptParseResult => ({ assistantText: "", sawReasoning: false });
|
|
291
|
+
// Every early return below is a failure path that stages nothing; only the completion path
|
|
292
|
+
// writes `attemptCalibration`, and the wrapper decides whether to commit it.
|
|
293
|
+
if (!response.body) {
|
|
294
|
+
return {
|
|
295
|
+
...emptyResult(),
|
|
296
|
+
terminal: { type: "error", message: "Kiro response has no body", status: 502, errorType: "upstream_error" },
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let open: { id: string; name: string; chunks: string[]; completion: boolean } | null = null;
|
|
301
|
+
let openCallId: string | undefined;
|
|
302
|
+
const closeOpenCall = () => {
|
|
303
|
+
if (!openCallId) return;
|
|
304
|
+
budget.closeCall(openCallId);
|
|
305
|
+
openCallId = undefined;
|
|
306
|
+
};
|
|
307
|
+
let outputChars = "";
|
|
308
|
+
let outputCharsBytes = 0;
|
|
309
|
+
let contextUsagePercentage: number | undefined;
|
|
310
|
+
let returnedConversationId = conversationId;
|
|
311
|
+
let assistantText = "";
|
|
312
|
+
let assistantTextBytes = 0;
|
|
313
|
+
let sawText = false;
|
|
314
|
+
let sawReasoning = false;
|
|
315
|
+
let sawRealTool = false;
|
|
316
|
+
let completionAnswer: string | undefined;
|
|
317
|
+
let completionCalls = 0;
|
|
318
|
+
let authoritativeUsage: OcxUsage | undefined;
|
|
319
|
+
let stopReason: string | undefined;
|
|
320
|
+
const fallbackEvents: AdapterEvent[] = [];
|
|
321
|
+
const thinking = new KiroThinkingParser(budget);
|
|
322
|
+
|
|
323
|
+
const retainedEventBytes = (event: AdapterEvent): number => Buffer.byteLength(JSON.stringify(event));
|
|
324
|
+
const retainEvent = (event: AdapterEvent): void => {
|
|
325
|
+
const bytes = retainedEventBytes(event);
|
|
326
|
+
budget.chargeRetained(bytes, { kind: "retained_collectors" });
|
|
327
|
+
retention.retainEvent(event, bytes);
|
|
328
|
+
};
|
|
329
|
+
const emitRetained = async function* (events: Iterable<AdapterEvent>): AsyncGenerator<AdapterEvent> {
|
|
330
|
+
for (const event of events) {
|
|
331
|
+
try { yield event; } finally { retention.releaseEvent(event); }
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
// A valid private completion answer supersedes the progress prose staged during the SAME
|
|
335
|
+
// inference: Kiro emits answer-like text and then calls the completion tool, so releasing both
|
|
336
|
+
// makes the bridge close the commentary message and open a second one with near-identical text
|
|
337
|
+
// (#2819 follow-up). Consume the collection instead — drop the redundant text, keep every
|
|
338
|
+
// non-text event, and release retention either way.
|
|
339
|
+
//
|
|
340
|
+
// This is deliberately the ONLY suppression site. The outer drain in `parseKiroAttempt` is also
|
|
341
|
+
// the leftover flush for early terminal returns (stream, protocol, and provider failures), so
|
|
342
|
+
// teaching it to discard text would hide the only commentary a failed turn ever produced.
|
|
343
|
+
// Splicing here leaves that drain empty on the completion path and untouched everywhere else.
|
|
344
|
+
const consumeSupersededByCompletion = async function* (
|
|
345
|
+
events: AdapterEvent[],
|
|
346
|
+
): AsyncGenerator<AdapterEvent> {
|
|
347
|
+
for (const event of events.splice(0)) {
|
|
348
|
+
try {
|
|
349
|
+
if (event.type !== "text_delta") yield event;
|
|
350
|
+
} finally {
|
|
351
|
+
retention.releaseEvent(event);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const providerState = (): { kiro: { conversationId: string } } | undefined =>
|
|
357
|
+
returnedConversationId ? { kiro: { conversationId: returnedConversationId } } : undefined;
|
|
358
|
+
|
|
359
|
+
const contextUsageTotalFloor = (): number | undefined => {
|
|
360
|
+
if (contextUsagePercentage === undefined || !contextWindowState.value) return undefined;
|
|
361
|
+
const floor = Math.ceil(contextWindowState.value * Math.min(contextUsagePercentage, 100) / 100);
|
|
362
|
+
return Number.isFinite(floor) && floor > 0 ? floor : undefined;
|
|
363
|
+
};
|
|
364
|
+
const usage = (): OcxUsage => {
|
|
365
|
+
const base = authoritativeUsage ?? {
|
|
366
|
+
inputTokens,
|
|
367
|
+
outputTokens: estimateKiroTokens(outputChars, modelId),
|
|
368
|
+
estimated: true,
|
|
369
|
+
};
|
|
370
|
+
const estimatedContextTotal = contextInputEstimate !== undefined
|
|
371
|
+
? contextInputEstimate + base.outputTokens
|
|
372
|
+
: undefined;
|
|
373
|
+
const authoritativeTurnTotal = base.inputTokens + base.outputTokens;
|
|
374
|
+
const contextTotal = Math.max(
|
|
375
|
+
estimatedContextTotal ?? 0,
|
|
376
|
+
contextUsageTotalFloor() ?? 0,
|
|
377
|
+
authoritativeTurnTotal,
|
|
378
|
+
);
|
|
379
|
+
return contextTotal > 0 ? { ...base, contextTotalTokens: contextTotal } : base;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const classifiedTerminal = (failure: KiroErrorClassification): AdapterEvent => {
|
|
383
|
+
// Upstream exception/error frames can arrive after commentary was already staged (and will be
|
|
384
|
+
// flushed before this terminal is yielded). Replaying after that content would duplicate it.
|
|
385
|
+
const emittedOutput = priorEmittedOutput
|
|
386
|
+
|| sawText
|
|
387
|
+
|| sawReasoning
|
|
388
|
+
|| sawRealTool
|
|
389
|
+
|| assistantText.length > 0
|
|
390
|
+
|| deferred.length > 0
|
|
391
|
+
|| completionAnswer !== undefined
|
|
392
|
+
|| completionCalls > 0
|
|
393
|
+
|| open !== null
|
|
394
|
+
|| fallbackEvents.length > 0;
|
|
395
|
+
if (failure.status === 429 && failure.retryable) noteKiroTransientThrottle();
|
|
396
|
+
return {
|
|
397
|
+
type: "error",
|
|
398
|
+
message: failure.message,
|
|
399
|
+
status: failure.status,
|
|
400
|
+
errorType: failure.errorType,
|
|
401
|
+
code: failure.code,
|
|
402
|
+
retryable: emittedOutput ? false : failure.retryable,
|
|
403
|
+
usage: usage(),
|
|
404
|
+
};
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const protocolTerminal = (message: string, malformedCompletion = false): AdapterEvent => {
|
|
408
|
+
if (mode === "text_fallback" && malformedCompletion) {
|
|
409
|
+
return retryableKiroIncomplete(
|
|
410
|
+
"malformed_kiro_completion",
|
|
411
|
+
message,
|
|
412
|
+
usage(),
|
|
413
|
+
providerState(),
|
|
414
|
+
// First-attempt progress was already flushed before this bounded fallback (#520).
|
|
415
|
+
!priorEmittedOutput,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
type: "error",
|
|
420
|
+
message,
|
|
421
|
+
status: 502,
|
|
422
|
+
errorType: "upstream_error",
|
|
423
|
+
code: malformedCompletion ? "invalid_kiro_completion" : "kiro_stream_protocol_error",
|
|
424
|
+
retryable: false,
|
|
425
|
+
usage: usage(),
|
|
426
|
+
};
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const classifyTool = (
|
|
430
|
+
tool: { id: string; name: string; chunks: string[]; completion: boolean },
|
|
431
|
+
): AdapterEvent | undefined => {
|
|
432
|
+
if (tool.name !== KIRO_COMPLETION_TOOL_NAME) {
|
|
433
|
+
tool.completion = false;
|
|
434
|
+
return completionAnswer !== undefined || completionCalls > 0
|
|
435
|
+
? protocolTerminal("Kiro returned a real tool call alongside a private final answer")
|
|
436
|
+
: undefined;
|
|
437
|
+
}
|
|
438
|
+
if (mode === "disabled") {
|
|
439
|
+
return protocolTerminal("Kiro returned the reserved private final-answer tool while explicit completion was disabled");
|
|
440
|
+
}
|
|
441
|
+
tool.completion = true;
|
|
442
|
+
if (completionAnswer !== undefined || completionCalls > 0) {
|
|
443
|
+
return protocolTerminal("Kiro returned more than one private final-answer tool call", true);
|
|
444
|
+
}
|
|
445
|
+
if (sawRealTool) {
|
|
446
|
+
return protocolTerminal("Kiro returned a private final answer alongside a real tool call");
|
|
447
|
+
}
|
|
448
|
+
return undefined;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const beginTool = (
|
|
452
|
+
id: string,
|
|
453
|
+
name: string,
|
|
454
|
+
): { tool?: { id: string; name: string; chunks: string[]; completion: boolean }; terminal?: AdapterEvent } => {
|
|
455
|
+
const next = { id, name, chunks: [], completion: false };
|
|
456
|
+
const terminal = classifyTool(next);
|
|
457
|
+
return terminal ? { terminal } : { tool: next };
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// In `required` mode Kiro's stop reason only arrives on the terminal metadata event, so staged
|
|
461
|
+
// commentary is held until either a real tool call proves the turn continues (flush as
|
|
462
|
+
// commentary) or the stream ends (relabel as the final answer when END_TURN says so). A heartbeat
|
|
463
|
+
// stands in for each held event so the bridge's stall watchdog stays armed.
|
|
464
|
+
const defer = (event: AdapterEvent): AdapterEvent[] => {
|
|
465
|
+
if (sawRealTool) return [...deferred.splice(0), event];
|
|
466
|
+
if (event.type !== "text_delta" && deferred.length === 0) return [event];
|
|
467
|
+
deferred.push(event);
|
|
468
|
+
retainEvent(event);
|
|
469
|
+
return [{ type: "heartbeat" }];
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const stage = (event: AdapterEvent): AdapterEvent[] => {
|
|
473
|
+
if (event.type === "text_delta") {
|
|
474
|
+
const nextAssistantTextBytes = appendedUtf8Bytes(assistantText, assistantTextBytes, event.text);
|
|
475
|
+
const assistantReservation = budget.reserveTransient(nextAssistantTextBytes, { kind: "retained_collectors" });
|
|
476
|
+
assistantText += event.text;
|
|
477
|
+
assistantReservation.commitRetained();
|
|
478
|
+
budget.releaseRetained(assistantTextBytes, { kind: "retained_collectors" });
|
|
479
|
+
retention.trackReplacement(assistantTextBytes, nextAssistantTextBytes);
|
|
480
|
+
assistantTextBytes = nextAssistantTextBytes;
|
|
481
|
+
if (event.text.trim()) sawText = true;
|
|
482
|
+
const nextOutputCharsBytes = appendedUtf8Bytes(outputChars, outputCharsBytes, event.text);
|
|
483
|
+
const outputReservation = budget.reserveTransient(nextOutputCharsBytes, { kind: "retained_collectors" });
|
|
484
|
+
outputChars += event.text;
|
|
485
|
+
outputReservation.commitRetained();
|
|
486
|
+
budget.releaseRetained(outputCharsBytes, { kind: "retained_collectors" });
|
|
487
|
+
retention.trackReplacement(outputCharsBytes, nextOutputCharsBytes);
|
|
488
|
+
outputCharsBytes = nextOutputCharsBytes;
|
|
489
|
+
const phased = mode === "disabled"
|
|
490
|
+
? event
|
|
491
|
+
: { ...event, phase: "commentary" as const };
|
|
492
|
+
if (mode === "text_fallback") {
|
|
493
|
+
fallbackEvents.push(phased);
|
|
494
|
+
retainEvent(phased);
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
return mode === "required" ? defer(phased) : [phased];
|
|
498
|
+
}
|
|
499
|
+
if (event.type === "reasoning_raw_delta" || event.type === "thinking_delta") {
|
|
500
|
+
const text = event.type === "reasoning_raw_delta" ? event.text : event.thinking;
|
|
501
|
+
if (text.trim()) sawReasoning = true;
|
|
502
|
+
const nextOutputCharsBytes = appendedUtf8Bytes(outputChars, outputCharsBytes, text);
|
|
503
|
+
const reasoningReservation = budget.reserveTransient(nextOutputCharsBytes, { kind: "retained_collectors" });
|
|
504
|
+
outputChars += text;
|
|
505
|
+
reasoningReservation.commitRetained();
|
|
506
|
+
budget.releaseRetained(outputCharsBytes, { kind: "retained_collectors" });
|
|
507
|
+
retention.trackReplacement(outputCharsBytes, nextOutputCharsBytes);
|
|
508
|
+
outputCharsBytes = nextOutputCharsBytes;
|
|
509
|
+
}
|
|
510
|
+
if (mode === "text_fallback" && event.type !== "heartbeat") {
|
|
511
|
+
fallbackEvents.push(event);
|
|
512
|
+
retainEvent(event);
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
515
|
+
return mode === "required" ? defer(event) : [event];
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const parseCompletion = (chunks: string[]): string | Error => {
|
|
519
|
+
const raw = chunks.join("").trim();
|
|
520
|
+
let value: unknown;
|
|
521
|
+
try {
|
|
522
|
+
value = JSON.parse(raw || "{}");
|
|
523
|
+
} catch {
|
|
524
|
+
return new Error("Kiro returned invalid JSON for the private final-answer tool");
|
|
525
|
+
}
|
|
526
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
527
|
+
return new Error("Kiro returned a non-object value for the private final-answer tool");
|
|
528
|
+
}
|
|
529
|
+
const answer = (value as { answer?: unknown }).answer;
|
|
530
|
+
if (typeof answer !== "string" || !answer.trim()) {
|
|
531
|
+
return new Error("Kiro returned an empty final answer");
|
|
532
|
+
}
|
|
533
|
+
return answer;
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const flushOpen = (): { events: AdapterEvent[]; terminal?: AdapterEvent } => {
|
|
537
|
+
if (!open) return { events: [] };
|
|
538
|
+
const tool = open;
|
|
539
|
+
open = null;
|
|
540
|
+
closeOpenCall();
|
|
541
|
+
const input = tool.chunks.join("");
|
|
542
|
+
if (!isCompleteKiroToolInput(input)) {
|
|
543
|
+
return { events: [], terminal: protocolTerminal(kiroTruncationErrorMessage("incomplete tool input JSON"), tool.completion) };
|
|
544
|
+
}
|
|
545
|
+
if (tool.completion) {
|
|
546
|
+
completionCalls++;
|
|
547
|
+
if (completionCalls > 1) {
|
|
548
|
+
return { events: [], terminal: protocolTerminal("Kiro returned more than one private final-answer tool call", true) };
|
|
549
|
+
}
|
|
550
|
+
if (sawRealTool) {
|
|
551
|
+
return { events: [], terminal: protocolTerminal("Kiro returned a private final answer alongside a real tool call") };
|
|
552
|
+
}
|
|
553
|
+
const answer = parseCompletion(tool.chunks);
|
|
554
|
+
if (answer instanceof Error) return { events: [], terminal: protocolTerminal(answer.message, true) };
|
|
555
|
+
completionAnswer = answer;
|
|
556
|
+
return { events: [] };
|
|
557
|
+
}
|
|
558
|
+
if (completionAnswer !== undefined || completionCalls > 0) {
|
|
559
|
+
return { events: [], terminal: protocolTerminal("Kiro returned a real tool call alongside a private final answer") };
|
|
560
|
+
}
|
|
561
|
+
sawRealTool = true;
|
|
562
|
+
const restored = nameMap?.get(tool.name) ?? tool.name;
|
|
563
|
+
return {
|
|
564
|
+
events: [
|
|
565
|
+
{ type: "tool_call_start", id: tool.id, name: restored },
|
|
566
|
+
...tool.chunks.filter(Boolean).map(argumentsChunk => ({ type: "tool_call_delta", arguments: argumentsChunk }) as AdapterEvent),
|
|
567
|
+
{ type: "tool_call_end" },
|
|
568
|
+
],
|
|
569
|
+
};
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
try {
|
|
573
|
+
for await (const msg of decodeEventStream(response.body)) {
|
|
574
|
+
const mt = msg.headers[":message-type"];
|
|
575
|
+
if (mt === "exception" || mt === "error") {
|
|
576
|
+
open = null;
|
|
577
|
+
return {
|
|
578
|
+
assistantText,
|
|
579
|
+
sawReasoning,
|
|
580
|
+
terminal: classifiedTerminal(classifyKiroStreamError(msg.headers, new TextDecoder().decode(msg.payload))),
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
if (mt !== "event") {
|
|
584
|
+
open = null;
|
|
585
|
+
return {
|
|
586
|
+
assistantText,
|
|
587
|
+
sawReasoning,
|
|
588
|
+
terminal: protocolTerminal(`Kiro response protocol error: unsupported Smithy message type ${JSON.stringify(mt ?? "missing")}`),
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
const eventType = msg.headers[":event-type"];
|
|
592
|
+
if (!eventType) {
|
|
593
|
+
open = null;
|
|
594
|
+
return { assistantText, sawReasoning, terminal: protocolTerminal("Kiro response protocol error: event is missing :event-type") };
|
|
595
|
+
}
|
|
596
|
+
const ev = parseKiroEvent(eventType, msg.payload);
|
|
597
|
+
if (!ev) continue;
|
|
598
|
+
switch (ev.type) {
|
|
599
|
+
case "metadata":
|
|
600
|
+
if (ev.usage) authoritativeUsage = ev.usage;
|
|
601
|
+
if (ev.contextUsagePercentage !== undefined && ev.contextUsagePercentage > 0) {
|
|
602
|
+
contextUsagePercentage = ev.contextUsagePercentage;
|
|
603
|
+
}
|
|
604
|
+
if (ev.stopReason !== undefined) stopReason = ev.stopReason;
|
|
605
|
+
break;
|
|
606
|
+
case "message_metadata":
|
|
607
|
+
if (isValidKiroConversationId(ev.conversationId)) {
|
|
608
|
+
// Kiro can answer under a different conversation id than the request was built with.
|
|
609
|
+
// Carry the calibration entry across so the record below finds its own raw estimate
|
|
610
|
+
// instead of silently falling back to the already-corrected value.
|
|
611
|
+
rekeyKiroCalibration(returnedConversationId, ev.conversationId);
|
|
612
|
+
returnedConversationId = ev.conversationId;
|
|
613
|
+
}
|
|
614
|
+
break;
|
|
615
|
+
case "content":
|
|
616
|
+
if (ev.modelId) {
|
|
617
|
+
contextWindowState.value = kiroUpstreamContextWindow(ev.modelId) ?? contextWindowState.value;
|
|
618
|
+
}
|
|
619
|
+
if (open) {
|
|
620
|
+
open = null;
|
|
621
|
+
return { assistantText, sawReasoning, terminal: protocolTerminal(kiroTruncationErrorMessage("content arrived before tool stop")) };
|
|
622
|
+
}
|
|
623
|
+
if (ev.data) {
|
|
624
|
+
for (const contentEvent of thinking.feed(ev.data)) {
|
|
625
|
+
yield* emitRetained(stage(contentEvent));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
break;
|
|
629
|
+
case "reasoning":
|
|
630
|
+
for (const contentEvent of thinking.flush()) {
|
|
631
|
+
yield* emitRetained(stage(contentEvent));
|
|
632
|
+
}
|
|
633
|
+
if (ev.data) {
|
|
634
|
+
yield* emitRetained(stage({ type: "reasoning_raw_delta", text: ev.data }));
|
|
635
|
+
}
|
|
636
|
+
if (ev.redactedContent) {
|
|
637
|
+
yield* emitRetained(stage({ type: "kiro_redacted_reasoning", data: ev.redactedContent }));
|
|
638
|
+
}
|
|
639
|
+
break;
|
|
640
|
+
case "context_usage":
|
|
641
|
+
if (ev.contextUsagePercentage > 0) contextUsagePercentage = ev.contextUsagePercentage;
|
|
642
|
+
break;
|
|
643
|
+
case "tool": {
|
|
644
|
+
for (const contentEvent of thinking.flush()) {
|
|
645
|
+
yield* emitRetained(stage(contentEvent));
|
|
646
|
+
}
|
|
647
|
+
if (!open) {
|
|
648
|
+
if (ev.stop === true) {
|
|
649
|
+
return { assistantText, sawReasoning, terminal: protocolTerminal("Kiro response protocol error: tool stop received without an open tool call") };
|
|
650
|
+
}
|
|
651
|
+
if (!ev.toolUseId || !ev.name) {
|
|
652
|
+
return { assistantText, sawReasoning, terminal: protocolTerminal("Kiro response protocol error: new tool event is missing toolUseId or name") };
|
|
653
|
+
}
|
|
654
|
+
const started = beginTool(ev.toolUseId, ev.name);
|
|
655
|
+
if (started.terminal) return { assistantText, sawReasoning, terminal: started.terminal };
|
|
656
|
+
open = started.tool!;
|
|
657
|
+
budget.openCall(open.id);
|
|
658
|
+
openCallId = open.id;
|
|
659
|
+
} else if (
|
|
660
|
+
(ev.toolUseId && ev.toolUseId !== open.id)
|
|
661
|
+
|| (ev.name && open.name !== "unknown" && ev.name !== open.name)
|
|
662
|
+
) {
|
|
663
|
+
closeOpenCall();
|
|
664
|
+
open = null;
|
|
665
|
+
return { assistantText, sawReasoning, terminal: protocolTerminal(kiroTruncationErrorMessage("tool input changed identity before stop")) };
|
|
666
|
+
}
|
|
667
|
+
if (open && open.name === "unknown" && ev.name) {
|
|
668
|
+
open.name = ev.name;
|
|
669
|
+
const terminal = classifyTool(open);
|
|
670
|
+
if (terminal) {
|
|
671
|
+
open = null;
|
|
672
|
+
return { assistantText, sawReasoning, terminal };
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (open && ev.input !== undefined) {
|
|
676
|
+
const previousCallBytes = open.chunks.reduce((total, chunk) => total + Buffer.byteLength(chunk), 0);
|
|
677
|
+
const nextCallBytes = previousCallBytes + Buffer.byteLength(ev.input);
|
|
678
|
+
const callReservation = budget.reserveTransient(nextCallBytes, { kind: "tool_args", callId: open.id });
|
|
679
|
+
open.chunks.push(ev.input);
|
|
680
|
+
callReservation.commitRetained();
|
|
681
|
+
budget.releaseRetained(previousCallBytes, { kind: "tool_args", callId: open.id });
|
|
682
|
+
const nextOutputCharsBytes = appendedUtf8Bytes(outputChars, outputCharsBytes, ev.input);
|
|
683
|
+
const toolOutputReservation = budget.reserveTransient(nextOutputCharsBytes, { kind: "retained_collectors" });
|
|
684
|
+
outputChars += ev.input;
|
|
685
|
+
toolOutputReservation.commitRetained();
|
|
686
|
+
budget.releaseRetained(outputCharsBytes, { kind: "retained_collectors" });
|
|
687
|
+
retention.trackReplacement(outputCharsBytes, nextOutputCharsBytes);
|
|
688
|
+
outputCharsBytes = nextOutputCharsBytes;
|
|
689
|
+
}
|
|
690
|
+
if (ev.stop === true) {
|
|
691
|
+
const flushed = flushOpen();
|
|
692
|
+
if (flushed.terminal) return { assistantText, sawReasoning, terminal: flushed.terminal };
|
|
693
|
+
for (const event of flushed.events) {
|
|
694
|
+
yield* emitRetained(stage(event));
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
yield { type: "heartbeat" };
|
|
698
|
+
}
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
case "invalid_state":
|
|
702
|
+
open = null;
|
|
703
|
+
return { assistantText, sawReasoning, terminal: classifiedTerminal(classifyKiroEventError(undefined, ev.message ?? "Kiro entered an invalid state")) };
|
|
704
|
+
case "error":
|
|
705
|
+
open = null;
|
|
706
|
+
return { assistantText, sawReasoning, terminal: classifiedTerminal(classifyKiroEventError(ev.reason, ev.message)) };
|
|
707
|
+
case "truncation":
|
|
708
|
+
open = null;
|
|
709
|
+
return { assistantText, sawReasoning, terminal: protocolTerminal(kiroTruncationErrorMessage(ev.data)) };
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
for (const contentEvent of thinking.flush()) {
|
|
714
|
+
yield* emitRetained(stage(contentEvent));
|
|
715
|
+
}
|
|
716
|
+
if (open) {
|
|
717
|
+
const input = open.chunks.join("");
|
|
718
|
+
if (!isCompleteKiroToolInput(input)) {
|
|
719
|
+
const privateTool = open.completion;
|
|
720
|
+
open = null;
|
|
721
|
+
return {
|
|
722
|
+
assistantText,
|
|
723
|
+
sawReasoning,
|
|
724
|
+
terminal: protocolTerminal(kiroTruncationErrorMessage("stream ended before tool stop"), privateTool),
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
const flushed = flushOpen();
|
|
728
|
+
if (flushed.terminal) return { assistantText, sawReasoning, terminal: flushed.terminal };
|
|
729
|
+
for (const event of flushed.events) {
|
|
730
|
+
yield* emitRetained(stage(event));
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const finalUsage = usage();
|
|
735
|
+
const finalProviderState = providerState();
|
|
736
|
+
if (contextUsagePercentage !== undefined) {
|
|
737
|
+
debugProviderDiagnostic("kiro", "context_usage", {
|
|
738
|
+
contextUsagePercentage,
|
|
739
|
+
...(contextWindowState.value ? { upstreamContextWindow: contextWindowState.value } : {}),
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
// Upstream just told us what this payload cost. The ratio between that and our pre-request
|
|
743
|
+
// estimate is this conversation's own measured error, and it is the only feedback the
|
|
744
|
+
// estimator ever receives.
|
|
745
|
+
//
|
|
746
|
+
// Staged, not recorded. An attempt that sets `needsFallback` is not over: the adapter rebuilds
|
|
747
|
+
// the payload and streams a second time for the SAME user turn. Learning here would apply the
|
|
748
|
+
// fresh factor to that rebuild and then learn again from it, so one turn would move the factor
|
|
749
|
+
// twice and the second observation would score a payload the first had already inflated. Only
|
|
750
|
+
// the outer parser knows whether an attempt is terminal, so it commits.
|
|
751
|
+
//
|
|
752
|
+
// Subtract the output first. `contextUsageTotalFloor` is the absolute context size AFTER the
|
|
753
|
+
// response (`OcxUsage.contextTotalTokens`, types/request.ts), while `contextInputEstimate`
|
|
754
|
+
// covers the request payload alone. Dividing one by the other would charge generated tokens to
|
|
755
|
+
// prompt-tokenization error, so a short prompt answered at length would learn a large factor
|
|
756
|
+
// and inflate every later request in that conversation — the premature compaction this work
|
|
757
|
+
// exists to prevent.
|
|
758
|
+
const chargedTotal = contextUsageTotalFloor();
|
|
759
|
+
if (chargedTotal !== undefined && contextInputEstimate !== undefined) {
|
|
760
|
+
const chargedInput = chargedTotal - finalUsage.outputTokens;
|
|
761
|
+
if (chargedInput > 0 && returnedConversationId) {
|
|
762
|
+
attemptCalibration.value = { conversationId: returnedConversationId, estimated: contextInputEstimate, charged: chargedInput };
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
// Native stop metadata proves that this inference ended, but it does not prove that ordinary
|
|
766
|
+
// text is a final answer. Kiro has emitted END_TURN for progress prose, so tool-enabled turns
|
|
767
|
+
// still require the private completion call to distinguish commentary from completion (#531).
|
|
768
|
+
const normalizedStopReason = stopReason?.trim().toUpperCase();
|
|
769
|
+
const nativeCompletionStop = (normalizedStopReason === KIRO_END_TURN_STOP_REASON
|
|
770
|
+
|| normalizedStopReason === "STOP_SEQUENCE")
|
|
771
|
+
&& sawText
|
|
772
|
+
&& !sawRealTool
|
|
773
|
+
&& completionAnswer === undefined
|
|
774
|
+
&& completionCalls === 0;
|
|
775
|
+
|
|
776
|
+
debugProviderDiagnostic("kiro", "attempt_complete", {
|
|
777
|
+
mode,
|
|
778
|
+
sawText,
|
|
779
|
+
sawReasoning,
|
|
780
|
+
sawRealTool,
|
|
781
|
+
completionCalls,
|
|
782
|
+
nativeCompletionStop,
|
|
783
|
+
...(stopReason !== undefined ? { stopReason } : {}),
|
|
784
|
+
assistantChars: assistantText.length,
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
if (mode === "required") {
|
|
788
|
+
// A valid completion answer makes this inference's staged prose redundant; anything else
|
|
789
|
+
// still flushes exactly as before (bounded fallback, explicit stops, real tool calls).
|
|
790
|
+
if (completionAnswer !== undefined) yield* consumeSupersededByCompletion(deferred);
|
|
791
|
+
else yield* emitRetained(deferred.splice(0));
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (mode === "text_fallback") {
|
|
795
|
+
if (completionAnswer !== undefined) {
|
|
796
|
+
yield* consumeSupersededByCompletion(fallbackEvents);
|
|
797
|
+
yield { type: "text_delta", text: completionAnswer, phase: "final_answer" };
|
|
798
|
+
return {
|
|
799
|
+
assistantText,
|
|
800
|
+
sawReasoning,
|
|
801
|
+
terminal: { type: "done", usage: finalUsage, endTurn: true, ...(finalProviderState ? { providerState: finalProviderState } : {}) },
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
if (sawRealTool) {
|
|
805
|
+
yield* emitRetained(fallbackEvents);
|
|
806
|
+
return {
|
|
807
|
+
assistantText,
|
|
808
|
+
sawReasoning,
|
|
809
|
+
terminal: { type: "done", usage: finalUsage, endTurn: false, ...(finalProviderState ? { providerState: finalProviderState } : {}) },
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
if (sawText) {
|
|
813
|
+
for (const event of fallbackEvents) {
|
|
814
|
+
try {
|
|
815
|
+
if (event.type !== "text_delta") yield event;
|
|
816
|
+
else yield { ...event, phase: "final_answer" };
|
|
817
|
+
} finally {
|
|
818
|
+
retention.releaseEvent(event);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
assistantText,
|
|
823
|
+
sawReasoning,
|
|
824
|
+
terminal: { type: "done", usage: finalUsage, endTurn: true, ...(finalProviderState ? { providerState: finalProviderState } : {}) },
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
yield* emitRetained(fallbackEvents);
|
|
828
|
+
return {
|
|
829
|
+
assistantText,
|
|
830
|
+
sawReasoning,
|
|
831
|
+
terminal: retryableKiroIncomplete(
|
|
832
|
+
sawReasoning ? "reasoning_only_kiro_fallback" : "empty_kiro_fallback",
|
|
833
|
+
sawReasoning
|
|
834
|
+
? "Kiro produced reasoning but no final answer on its bounded completion retry"
|
|
835
|
+
: "Kiro produced no final answer on its bounded completion retry",
|
|
836
|
+
finalUsage,
|
|
837
|
+
finalProviderState,
|
|
838
|
+
// First-attempt progress was already flushed before this bounded fallback (#520).
|
|
839
|
+
!priorEmittedOutput,
|
|
840
|
+
),
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (completionAnswer !== undefined) {
|
|
845
|
+
yield { type: "text_delta", text: completionAnswer, phase: "final_answer" };
|
|
846
|
+
return {
|
|
847
|
+
assistantText,
|
|
848
|
+
sawReasoning,
|
|
849
|
+
terminal: { type: "done", usage: finalUsage, endTurn: true, ...(finalProviderState ? { providerState: finalProviderState } : {}) },
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
if (sawRealTool) {
|
|
853
|
+
return {
|
|
854
|
+
assistantText,
|
|
855
|
+
sawReasoning,
|
|
856
|
+
terminal: { type: "done", usage: finalUsage, endTurn: false, ...(finalProviderState ? { providerState: finalProviderState } : {}) },
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
if (mode === "required" && nativeCompletionStop) {
|
|
860
|
+
return {
|
|
861
|
+
assistantText,
|
|
862
|
+
sawReasoning,
|
|
863
|
+
needsFallback: true,
|
|
864
|
+
usage: finalUsage,
|
|
865
|
+
providerState: finalProviderState,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// An explicit non-completion stop reason has already terminated this inference. Converting it into
|
|
870
|
+
// another model request would hide truncation behind a second paid call, and for context
|
|
871
|
+
// exhaustion it would resubmit a request that cannot fit. Only a MISSING stop reason falls
|
|
872
|
+
// through to the bounded compatibility fallback below.
|
|
873
|
+
//
|
|
874
|
+
// END_TURN and STOP_SEQUENCE with text take the bounded validation path above; reaching here
|
|
875
|
+
// with either means the turn produced no replayable text.
|
|
876
|
+
if (mode === "required" && normalizedStopReason !== undefined) {
|
|
877
|
+
const providerStateField = finalProviderState ? { providerState: finalProviderState } : {};
|
|
878
|
+
const incomplete = (reason: string, retryable: boolean) => ({
|
|
879
|
+
assistantText,
|
|
880
|
+
sawReasoning,
|
|
881
|
+
terminal: {
|
|
882
|
+
type: "incomplete" as const,
|
|
883
|
+
reason,
|
|
884
|
+
message: `Kiro stopped with ${normalizedStopReason} before an explicit final answer`,
|
|
885
|
+
usage: finalUsage,
|
|
886
|
+
retryable,
|
|
887
|
+
endTurn: false,
|
|
888
|
+
...providerStateField,
|
|
889
|
+
},
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
if (normalizedStopReason === "MODEL_CONTEXT_WINDOW_EXCEEDED") {
|
|
893
|
+
// Reuse the existing context-length contract (kiro-errors.ts) instead of inventing an
|
|
894
|
+
// incomplete reason: an unrecognized incomplete becomes a retryable 529 in Claude
|
|
895
|
+
// outbound, and `max_output_tokens` would make responses/state.ts cache this partial
|
|
896
|
+
// for continuation replay. Both invite a retry that cannot succeed.
|
|
897
|
+
return {
|
|
898
|
+
assistantText,
|
|
899
|
+
sawReasoning,
|
|
900
|
+
terminal: {
|
|
901
|
+
type: "error" as const,
|
|
902
|
+
message: "Kiro stopped because the model context window was exhausted",
|
|
903
|
+
status: 400,
|
|
904
|
+
errorType: "invalid_request_error",
|
|
905
|
+
code: "context_length_exceeded",
|
|
906
|
+
retryable: false,
|
|
907
|
+
usage: finalUsage,
|
|
908
|
+
},
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
if (normalizedStopReason === "MAX_TOKENS") return incomplete("max_output_tokens", true);
|
|
912
|
+
if (normalizedStopReason === "CONTENT_FILTERED" || normalizedStopReason === "GUARDRAIL_INTERVENED") {
|
|
913
|
+
return incomplete("content_filter", false);
|
|
914
|
+
}
|
|
915
|
+
if (normalizedStopReason === "MALFORMED_TOOL_USE") return incomplete("kiro_malformed_tool_use", false);
|
|
916
|
+
if (normalizedStopReason === "MALFORMED_MODEL_OUTPUT") return incomplete("kiro_malformed_model_output", false);
|
|
917
|
+
// TOOL_USE here means Kiro claimed a tool call it never emitted.
|
|
918
|
+
if (normalizedStopReason === "TOOL_USE") return incomplete("kiro_tool_use_without_call", false);
|
|
919
|
+
if (normalizedStopReason === KIRO_END_TURN_STOP_REASON || normalizedStopReason === "STOP_SEQUENCE") {
|
|
920
|
+
return incomplete(`kiro_${normalizedStopReason.toLowerCase()}_without_text`, false);
|
|
921
|
+
}
|
|
922
|
+
return incomplete(`kiro_${normalizedStopReason.toLowerCase() || "unknown_stop"}`, false);
|
|
923
|
+
}
|
|
924
|
+
// Kiro text has no trustworthy final/progress marker. When completion is required, ordinary
|
|
925
|
+
// text and reasoning remain unfinished until the one bounded fallback validates the turn.
|
|
926
|
+
if (mode === "required" && (sawText || sawReasoning)) {
|
|
927
|
+
return { assistantText, sawReasoning, needsFallback: true, usage: finalUsage, providerState: finalProviderState };
|
|
928
|
+
}
|
|
929
|
+
if (!sawText && !sawReasoning) {
|
|
930
|
+
return {
|
|
931
|
+
assistantText,
|
|
932
|
+
sawReasoning,
|
|
933
|
+
terminal: retryableKiroIncomplete(
|
|
934
|
+
"empty_kiro_stream",
|
|
935
|
+
"Kiro returned a successful but empty response stream",
|
|
936
|
+
finalUsage,
|
|
937
|
+
finalProviderState,
|
|
938
|
+
),
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
return {
|
|
942
|
+
assistantText,
|
|
943
|
+
sawReasoning,
|
|
944
|
+
terminal: {
|
|
945
|
+
type: "done",
|
|
946
|
+
usage: finalUsage,
|
|
947
|
+
endTurn: mode === "disabled" ? sawText : false,
|
|
948
|
+
...(finalProviderState ? { providerState: finalProviderState } : {}),
|
|
949
|
+
},
|
|
950
|
+
};
|
|
951
|
+
} catch (err) {
|
|
952
|
+
if (isTranslatorBudgetExceededError(err)) {
|
|
953
|
+
closeOpenCall();
|
|
954
|
+
return {
|
|
955
|
+
assistantText,
|
|
956
|
+
sawReasoning,
|
|
957
|
+
terminal: {
|
|
958
|
+
type: "error",
|
|
959
|
+
status: 502,
|
|
960
|
+
errorType: "upstream_error",
|
|
961
|
+
code: "translation_buffer_limit",
|
|
962
|
+
message: "upstream translation buffer exceeded the safe limit",
|
|
963
|
+
},
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
// Mid-stream socket closes after response.created / heartbeats only must stay retryable:
|
|
967
|
+
// nothing was relayed to the client, so a string-body replay is safe (see #519 / cursor's
|
|
968
|
+
// emittedOutput gate). Once any assistant text, reasoning, tool, or deferred content exists
|
|
969
|
+
// — including content flushed by a prior attempt before a bounded fallback — fail closed;
|
|
970
|
+
// the client may already have partial output. Protocol parse throws stay non-retryable even
|
|
971
|
+
// with zero output.
|
|
972
|
+
const emittedOutput = priorEmittedOutput
|
|
973
|
+
|| sawText
|
|
974
|
+
|| sawReasoning
|
|
975
|
+
|| sawRealTool
|
|
976
|
+
|| assistantText.length > 0
|
|
977
|
+
|| deferred.length > 0
|
|
978
|
+
|| completionAnswer !== undefined
|
|
979
|
+
|| completionCalls > 0
|
|
980
|
+
|| open !== null
|
|
981
|
+
|| fallbackEvents.length > 0;
|
|
982
|
+
return {
|
|
983
|
+
assistantText,
|
|
984
|
+
sawReasoning,
|
|
985
|
+
terminal: {
|
|
986
|
+
type: "error",
|
|
987
|
+
message: safeKiroErrorMessage({}, err instanceof Error ? err.message : String(err)),
|
|
988
|
+
status: 502,
|
|
989
|
+
errorType: "server_error",
|
|
990
|
+
code: "kiro_stream_protocol_error",
|
|
991
|
+
retryable: isRetryableKiroStreamCatchError(err, emittedOutput),
|
|
992
|
+
usage: usage(),
|
|
993
|
+
},
|
|
994
|
+
};
|
|
995
|
+
} finally {
|
|
996
|
+
thinking.dispose();
|
|
997
|
+
closeOpenCall();
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
export async function* parseKiroStream(
|
|
1002
|
+
response: Response,
|
|
1003
|
+
budget: TranslatorBudget,
|
|
1004
|
+
modelId?: string,
|
|
1005
|
+
inputTokens = 0,
|
|
1006
|
+
contextWindow?: number,
|
|
1007
|
+
nameMap?: Map<string, string>,
|
|
1008
|
+
conversationId?: string,
|
|
1009
|
+
completionMode: KiroCompletionMode = "disabled",
|
|
1010
|
+
fallbackFactory?: KiroFallbackFactory,
|
|
1011
|
+
contextInputEstimate?: number,
|
|
1012
|
+
): AsyncGenerator<AdapterEvent> {
|
|
1013
|
+
const contextWindowState: KiroContextWindowState = { value: contextWindow };
|
|
1014
|
+
const firstResult = yield* parseKiroAttempt(
|
|
1015
|
+
response,
|
|
1016
|
+
budget,
|
|
1017
|
+
completionMode,
|
|
1018
|
+
modelId,
|
|
1019
|
+
inputTokens,
|
|
1020
|
+
contextWindowState,
|
|
1021
|
+
nameMap,
|
|
1022
|
+
conversationId,
|
|
1023
|
+
contextInputEstimate,
|
|
1024
|
+
false,
|
|
1025
|
+
);
|
|
1026
|
+
try {
|
|
1027
|
+
if (!firstResult.needsFallback) {
|
|
1028
|
+
if (firstResult.terminal) yield firstResult.terminal;
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
if (!fallbackFactory) {
|
|
1032
|
+
yield retryableKiroIncomplete(
|
|
1033
|
+
"uncompleted_kiro_response",
|
|
1034
|
+
"Kiro produced progress without an explicit final answer and no bounded retry transport was available",
|
|
1035
|
+
firstResult.usage ?? { inputTokens, outputTokens: 0, estimated: true },
|
|
1036
|
+
firstResult.providerState,
|
|
1037
|
+
);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
yield { type: "heartbeat" };
|
|
1042
|
+
// First attempt already flushed deferred progress before this point. Gate fallback
|
|
1043
|
+
// setup/HTTP failures the same way as the second-stream catch so a replay cannot
|
|
1044
|
+
// duplicate visible commentary (#520).
|
|
1045
|
+
const priorEmittedOutput = Boolean(firstResult.assistantText.trim()) || firstResult.sawReasoning;
|
|
1046
|
+
let firstAssistantText = firstResult.assistantText;
|
|
1047
|
+
const firstHadAssistantText = firstAssistantText.length > 0;
|
|
1048
|
+
let fallback: KiroFallbackAttempt;
|
|
1049
|
+
try {
|
|
1050
|
+
fallback = await fallbackFactory(
|
|
1051
|
+
firstResult.providerState?.kiro.conversationId ?? conversationId,
|
|
1052
|
+
firstAssistantText,
|
|
1053
|
+
firstResult.sawReasoning,
|
|
1054
|
+
budget,
|
|
1055
|
+
);
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
firstAssistantText = "";
|
|
1058
|
+
firstResult.assistantText = "";
|
|
1059
|
+
firstResult.releaseRetained();
|
|
1060
|
+
if (isTranslatorBudgetExceededError(err)) {
|
|
1061
|
+
yield {
|
|
1062
|
+
type: "error",
|
|
1063
|
+
message: "upstream translation buffer exceeded the safe limit",
|
|
1064
|
+
status: 502,
|
|
1065
|
+
errorType: "upstream_error",
|
|
1066
|
+
code: "translation_buffer_limit",
|
|
1067
|
+
usage: firstResult.usage,
|
|
1068
|
+
};
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
yield {
|
|
1072
|
+
type: "error",
|
|
1073
|
+
message: safeKiroErrorMessage({}, err instanceof Error ? err.message : String(err)),
|
|
1074
|
+
status: err instanceof Error && err.name === "TimeoutError" ? 504 : 502,
|
|
1075
|
+
errorType: "upstream_error",
|
|
1076
|
+
retryable: !priorEmittedOutput,
|
|
1077
|
+
usage: firstResult.usage,
|
|
1078
|
+
};
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
// The factory has finished using the live first-attempt alias and has retained its own retry
|
|
1082
|
+
// serialization through the fetch boundary. The discarded parser collectors can now release
|
|
1083
|
+
// before the second attempt begins on the same turn budget.
|
|
1084
|
+
firstAssistantText = "";
|
|
1085
|
+
firstResult.assistantText = "";
|
|
1086
|
+
firstResult.releaseRetained();
|
|
1087
|
+
fallback.releaseRequestBody?.();
|
|
1088
|
+
if (!fallback.response.ok) {
|
|
1089
|
+
const payload = await fallback.response.text().catch(() => "");
|
|
1090
|
+
const failure = classifyKiroHttpError(fallback.response.status, fallback.response.headers, payload);
|
|
1091
|
+
yield {
|
|
1092
|
+
type: "error",
|
|
1093
|
+
message: failure.message,
|
|
1094
|
+
status: failure.status,
|
|
1095
|
+
errorType: failure.errorType,
|
|
1096
|
+
code: failure.code,
|
|
1097
|
+
retryable: priorEmittedOutput ? false : failure.retryable,
|
|
1098
|
+
usage: firstResult.usage,
|
|
1099
|
+
};
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const secondResult = yield* parseKiroAttempt(
|
|
1104
|
+
fallback.response,
|
|
1105
|
+
budget,
|
|
1106
|
+
"text_fallback",
|
|
1107
|
+
modelId,
|
|
1108
|
+
fallback.inputTokens,
|
|
1109
|
+
contextWindowState,
|
|
1110
|
+
fallback.nameMap,
|
|
1111
|
+
fallback.conversationId,
|
|
1112
|
+
fallback.contextInputEstimate,
|
|
1113
|
+
// First attempt already flushed deferred progress to the client before this fallback.
|
|
1114
|
+
// A zero-output transport failure here must stay non-retryable to avoid duplicating that text.
|
|
1115
|
+
priorEmittedOutput,
|
|
1116
|
+
);
|
|
1117
|
+
try {
|
|
1118
|
+
if (!secondResult.terminal) {
|
|
1119
|
+
yield retryableKiroIncomplete(
|
|
1120
|
+
"empty_kiro_fallback",
|
|
1121
|
+
"Kiro's bounded completion retry ended without a terminal result",
|
|
1122
|
+
mergeKiroUsage(firstResult.usage, secondResult.usage, firstHadAssistantText)
|
|
1123
|
+
?? { inputTokens, outputTokens: 0, estimated: true },
|
|
1124
|
+
secondResult.providerState ?? firstResult.providerState,
|
|
1125
|
+
!priorEmittedOutput,
|
|
1126
|
+
);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
if (secondResult.terminal.type === "done" || secondResult.terminal.type === "incomplete") {
|
|
1130
|
+
yield {
|
|
1131
|
+
...secondResult.terminal,
|
|
1132
|
+
// Belt-and-suspenders: never advertise a replay-safe incomplete after flushed progress.
|
|
1133
|
+
...(secondResult.terminal.type === "incomplete" && priorEmittedOutput
|
|
1134
|
+
? { retryable: false as const }
|
|
1135
|
+
: {}),
|
|
1136
|
+
usage: mergeKiroUsage(firstResult.usage, secondResult.terminal.usage, firstHadAssistantText),
|
|
1137
|
+
providerState: secondResult.terminal.providerState ?? firstResult.providerState,
|
|
1138
|
+
};
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
yield {
|
|
1142
|
+
...secondResult.terminal,
|
|
1143
|
+
...(secondResult.terminal.type === "error"
|
|
1144
|
+
? { usage: mergeKiroUsage(firstResult.usage, secondResult.terminal.usage, firstHadAssistantText) }
|
|
1145
|
+
: {}),
|
|
1146
|
+
};
|
|
1147
|
+
} finally {
|
|
1148
|
+
secondResult.releaseRetained();
|
|
1149
|
+
}
|
|
1150
|
+
} finally {
|
|
1151
|
+
firstResult.releaseRetained();
|
|
1152
|
+
}
|
|
1153
|
+
}
|