ns-kiro-core 0.2.3 → 0.3.1
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/dist/blocks.d.ts +4 -0
- package/dist/blocks.js +4 -0
- package/dist/blocks.js.map +1 -1
- package/dist/cache-estimator.d.ts +11 -0
- package/dist/cache-estimator.js +61 -0
- package/dist/cache-estimator.js.map +1 -0
- package/dist/endpoints.d.ts +8 -0
- package/dist/endpoints.js +14 -0
- package/dist/endpoints.js.map +1 -1
- package/dist/errors.d.ts +39 -0
- package/dist/errors.js +82 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-parser.d.ts +127 -26
- package/dist/event-parser.js +256 -48
- package/dist/event-parser.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/kiro-cli.d.ts +5 -0
- package/dist/kiro-cli.js +15 -0
- package/dist/kiro-cli.js.map +1 -1
- package/dist/models.js +4 -1
- package/dist/models.js.map +1 -1
- package/dist/response-assembler.d.ts +29 -3
- package/dist/response-assembler.js +125 -37
- package/dist/response-assembler.js.map +1 -1
- package/dist/response-stream.d.ts +16 -1
- package/dist/response-stream.js +116 -17
- package/dist/response-stream.js.map +1 -1
- package/dist/stream.d.ts +7 -0
- package/dist/stream.js +249 -19
- package/dist/stream.js.map +1 -1
- package/dist/token-type.d.ts +3 -3
- package/dist/token-type.js +20 -13
- package/dist/token-type.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/usage-tracking.d.ts +34 -0
- package/dist/usage-tracking.js +88 -0
- package/dist/usage-tracking.js.map +1 -0
- package/package.json +1 -1
package/dist/response-stream.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// ABOUTME: Owns framing and the stall timeouts; block assembly lives elsewhere.
|
|
3
3
|
import { UniversalEventStreamMarshaller } from "@smithy/core/event-streams";
|
|
4
4
|
import { debugEnabled, debugLog } from "./debug.js";
|
|
5
|
-
import { parseKiroEvent } from "./event-parser.js";
|
|
5
|
+
import { parseKiroEvent, parseKiroExceptionFrame } from "./event-parser.js";
|
|
6
6
|
/** Longest gap between two frames before the response is treated as stalled. */
|
|
7
7
|
export const IDLE_TIMEOUT = 300_000;
|
|
8
8
|
const eventStreamMarshaller = new UniversalEventStreamMarshaller({
|
|
@@ -19,11 +19,18 @@ const eventStreamMarshaller = new UniversalEventStreamMarshaller({
|
|
|
19
19
|
* A Kiro `error` frame ends the stream and is not yielded — it carries no
|
|
20
20
|
* content, and every caller treats it as the same retryable failure as a
|
|
21
21
|
* protocol error.
|
|
22
|
+
*
|
|
23
|
+
* A caller abort throws out of `frames` instead of reporting through
|
|
24
|
+
* `outcome`: the interrupt is not a retryable stream condition, and surfacing
|
|
25
|
+
* it as one would burn the retry budget on a cancelled turn. The throw
|
|
26
|
+
* propagates through the consuming generator to the host adapter, which maps
|
|
27
|
+
* it to an `aborted` stop reason.
|
|
22
28
|
*/
|
|
23
29
|
export function readKiroEventStream(body, options) {
|
|
24
30
|
const outcome = { firstTokenTimedOut: false, idleTimedOut: false, error: null };
|
|
25
31
|
const idleTimeoutMs = options.idleTimeoutMs ?? IDLE_TIMEOUT;
|
|
26
32
|
const bodyReader = body.getReader();
|
|
33
|
+
const callerSignal = options.signal;
|
|
27
34
|
async function* frames() {
|
|
28
35
|
let idleTimer = null;
|
|
29
36
|
const resetIdle = () => {
|
|
@@ -34,6 +41,17 @@ export function readKiroEventStream(body, options) {
|
|
|
34
41
|
void bodyReader.cancel().catch(() => { });
|
|
35
42
|
}, idleTimeoutMs);
|
|
36
43
|
};
|
|
44
|
+
// Cancel the body read as soon as the caller aborts (e.g. user presses
|
|
45
|
+
// Esc mid-stream). Without this, the read loop below keeps consuming the
|
|
46
|
+
// event stream until the server finishes the response, which makes an
|
|
47
|
+
// interrupt appear to hang for the remainder of the generation.
|
|
48
|
+
const onCallerStreamAbort = () => {
|
|
49
|
+
void bodyReader.cancel().catch(() => { });
|
|
50
|
+
};
|
|
51
|
+
if (callerSignal?.aborted)
|
|
52
|
+
onCallerStreamAbort();
|
|
53
|
+
else
|
|
54
|
+
callerSignal?.addEventListener("abort", onCallerStreamAbort, { once: true });
|
|
37
55
|
// Smithy's marshaller handles chunk reassembly, CRC validation, protocol
|
|
38
56
|
// error/exception detection, and payload deserialization.
|
|
39
57
|
const bodyIterable = {
|
|
@@ -57,6 +75,50 @@ export function readKiroEventStream(body, options) {
|
|
|
57
75
|
if (!entry)
|
|
58
76
|
throw new Error("Received an empty event stream message");
|
|
59
77
|
const [key, msg] = entry;
|
|
78
|
+
// The four error members of ChatResponseStream target `@error` shapes,
|
|
79
|
+
// so the service frames them as `:message-type: exception`. The
|
|
80
|
+
// marshaller keys those by `:exception-type` and throws whatever this
|
|
81
|
+
// callback returns, so returning the bare payload would discard the
|
|
82
|
+
// modeled class. Return an Error carrying the parsed detail instead.
|
|
83
|
+
if (msg.headers[":message-type"]?.value === "exception") {
|
|
84
|
+
// Parsed defensively, and BEFORE the shared parse below: an exception
|
|
85
|
+
// body that is empty or not JSON would otherwise throw a SyntaxError
|
|
86
|
+
// out of this deserializer, and the caller would report
|
|
87
|
+
// "Unexpected end of JSON input" with the modeled class gone — the
|
|
88
|
+
// exact loss this routing removes. The class lives in the header, so
|
|
89
|
+
// it survives a body we cannot read. The same-service client's own
|
|
90
|
+
// bridge takes this position too (sse-middleware.ts: "Non-JSON body:
|
|
91
|
+
// still throw a typed exception with a fallback message").
|
|
92
|
+
let parsedException = {};
|
|
93
|
+
try {
|
|
94
|
+
const decoded = JSON.parse(utf8Decoder.decode(msg.body));
|
|
95
|
+
if (decoded && typeof decoded === "object")
|
|
96
|
+
parsedException = decoded;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Header-only classification below.
|
|
100
|
+
}
|
|
101
|
+
// An unmodeled member (a fifth error added server-side, or `$unknown`)
|
|
102
|
+
// still arrives keyed by `:exception-type`. Smithy's own fail-open path
|
|
103
|
+
// is unreachable here — it only triggers when the deserializer returns
|
|
104
|
+
// a `$unknown` property, which this one never does — so without a
|
|
105
|
+
// fallback the marshaller would throw the bare parsed body and the
|
|
106
|
+
// member name would be lost in exactly the way this routing exists to
|
|
107
|
+
// prevent. Synthesize the same typed shape with `kind: "unknown"`.
|
|
108
|
+
const data = parseKiroExceptionFrame(key, parsedException) ?? {
|
|
109
|
+
error: key,
|
|
110
|
+
kind: "unknown",
|
|
111
|
+
...(typeof parsedException.message === "string" ? { message: parsedException.message } : {}),
|
|
112
|
+
...(typeof parsedException.reason === "string" ? { reason: parsedException.reason } : {}),
|
|
113
|
+
...(typeof parsedException.retryAfterMilliseconds === "number"
|
|
114
|
+
? { retryAfterMilliseconds: parsedException.retryAfterMilliseconds }
|
|
115
|
+
: {}),
|
|
116
|
+
};
|
|
117
|
+
const error = new Error(data.message ? `${data.error}: ${data.message}` : data.error);
|
|
118
|
+
error.name = data.error;
|
|
119
|
+
error.kiroError = data;
|
|
120
|
+
return { [key]: error };
|
|
121
|
+
}
|
|
60
122
|
const parsed = JSON.parse(utf8Decoder.decode(msg.body));
|
|
61
123
|
return { [key]: parsed };
|
|
62
124
|
});
|
|
@@ -65,30 +127,54 @@ export function readKiroEventStream(body, options) {
|
|
|
65
127
|
const FIRST_TOKEN_SENTINEL = Symbol("firstTokenTimeout");
|
|
66
128
|
try {
|
|
67
129
|
while (true) {
|
|
130
|
+
if (callerSignal?.aborted) {
|
|
131
|
+
// Surface the abort instead of treating the cancelled read as a
|
|
132
|
+
// retryable stream error; the adapter maps this to a stopReason of
|
|
133
|
+
// "aborted".
|
|
134
|
+
throw callerSignal.reason ?? new Error("Request aborted");
|
|
135
|
+
}
|
|
68
136
|
let iterResult;
|
|
69
137
|
try {
|
|
70
138
|
if (!gotFirstToken) {
|
|
71
139
|
const readPromise = iterator.next();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
140
|
+
let firstTokenTimer;
|
|
141
|
+
try {
|
|
142
|
+
const result = await Promise.race([
|
|
143
|
+
readPromise,
|
|
144
|
+
new Promise((resolve) => {
|
|
145
|
+
firstTokenTimer = setTimeout(() => resolve(FIRST_TOKEN_SENTINEL), options.firstTokenTimeoutMs);
|
|
146
|
+
}),
|
|
147
|
+
]);
|
|
148
|
+
if (result === FIRST_TOKEN_SENTINEL) {
|
|
149
|
+
readPromise.catch(() => { }); // suppress dangling rejection
|
|
150
|
+
void bodyReader.cancel().catch(() => { });
|
|
151
|
+
outcome.firstTokenTimedOut = true;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
iterResult = result;
|
|
155
|
+
gotFirstToken = true;
|
|
156
|
+
resetIdle();
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
// The losing timeout branch of the race must not keep a ref'd
|
|
160
|
+
// timer alive until it fires: an uncleared 90 s handle holds the
|
|
161
|
+
// Node event loop open long after a print-mode caller has
|
|
162
|
+
// finished its turn (upstream #154).
|
|
163
|
+
if (firstTokenTimer !== undefined)
|
|
164
|
+
clearTimeout(firstTokenTimer);
|
|
81
165
|
}
|
|
82
|
-
iterResult = result;
|
|
83
|
-
gotFirstToken = true;
|
|
84
|
-
resetIdle();
|
|
85
166
|
}
|
|
86
167
|
else {
|
|
87
168
|
iterResult = await iterator.next();
|
|
88
169
|
}
|
|
89
170
|
}
|
|
90
171
|
catch (e) {
|
|
91
|
-
// Smithy throws on `:message-type` error/exception headers.
|
|
172
|
+
// Smithy throws on `:message-type` error/exception headers. A modeled
|
|
173
|
+
// exception frame arrives here as the Error built in the
|
|
174
|
+
// deserializer above, with its parsed detail attached.
|
|
175
|
+
const kiroError = e?.kiroError;
|
|
176
|
+
if (kiroError)
|
|
177
|
+
outcome.errorData = kiroError;
|
|
92
178
|
outcome.error =
|
|
93
179
|
e instanceof Error
|
|
94
180
|
? e.message
|
|
@@ -99,23 +185,36 @@ export function readKiroEventStream(body, options) {
|
|
|
99
185
|
if (done)
|
|
100
186
|
return;
|
|
101
187
|
resetIdle();
|
|
102
|
-
|
|
103
|
-
|
|
188
|
+
// The marshaller keys each frame by its modeled `ChatResponseStream`
|
|
189
|
+
// union member (from the `:event-type` header). Route on that key
|
|
190
|
+
// instead of guessing the member from which fields are populated.
|
|
191
|
+
const frameEntry = Object.entries(value)[0];
|
|
192
|
+
if (!frameEntry)
|
|
193
|
+
continue;
|
|
194
|
+
const [frameKey, framePayload] = frameEntry;
|
|
195
|
+
const event = parseKiroEvent(frameKey, (framePayload ?? {}));
|
|
104
196
|
if (!event)
|
|
105
197
|
continue;
|
|
198
|
+
if (event.type === "ignored") {
|
|
199
|
+
if (debugEnabled())
|
|
200
|
+
debugLog("stream.events.ignored", [event.data.key]);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
106
203
|
if (debugEnabled())
|
|
107
204
|
debugLog("stream.events", [event]);
|
|
108
205
|
if (event.type === "error") {
|
|
109
206
|
outcome.error = event.data.message ? `${event.data.error}: ${event.data.message}` : event.data.error;
|
|
207
|
+
outcome.errorData = event.data;
|
|
110
208
|
void bodyReader.cancel().catch(() => { });
|
|
111
209
|
return;
|
|
112
210
|
}
|
|
113
|
-
yield { event, payload };
|
|
211
|
+
yield { event, payload: framePayload };
|
|
114
212
|
}
|
|
115
213
|
}
|
|
116
214
|
finally {
|
|
117
215
|
if (idleTimer)
|
|
118
216
|
clearTimeout(idleTimer);
|
|
217
|
+
callerSignal?.removeEventListener("abort", onCallerStreamAbort);
|
|
119
218
|
}
|
|
120
219
|
}
|
|
121
220
|
return { frames: frames(), outcome };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"response-stream.js","sourceRoot":"","sources":["../src/response-stream.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,gFAAgF;AAEhF,OAAO,EAAE,8BAA8B,EAAE,MAAM,4BAA4B,CAAC;AAE5E,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,
|
|
1
|
+
{"version":3,"file":"response-stream.js","sourceRoot":"","sources":["../src/response-stream.ts"],"names":[],"mappings":"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"}
|
package/dist/stream.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type KiroModel } from "./models.js";
|
|
2
2
|
import type { KiroEffort, KiroMessage, KiroStreamEvent, KiroTool } from "./types.js";
|
|
3
|
+
import { type KiroUsageTracking } from "./usage-tracking.js";
|
|
3
4
|
/** One model call, fully assembled by the host adapter. */
|
|
4
5
|
export interface KiroStreamRequest {
|
|
5
6
|
model: KiroModel;
|
|
@@ -21,6 +22,12 @@ export interface KiroStreamRequest {
|
|
|
21
22
|
* behaviour instead of retrying mid-response.
|
|
22
23
|
*/
|
|
23
24
|
canDiscardEmittedBlocks?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Opt-in usage estimates (credit→USD value, cache-read estimation). Absent
|
|
27
|
+
* means disabled; the core never reads a settings file on its own — the host
|
|
28
|
+
* resolves the policy and passes it here.
|
|
29
|
+
*/
|
|
30
|
+
usageTracking?: KiroUsageTracking;
|
|
24
31
|
}
|
|
25
32
|
/** Reset profile resolution state — exported for stream tests. */
|
|
26
33
|
export declare function resetProfileArnCache(resolved?: boolean): void;
|
package/dist/stream.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// ABOUTME: Core streaming integration for Kiro API requests and responses.
|
|
2
2
|
// ABOUTME: Handles request building, retry logic, event parsing, and token counting.
|
|
3
|
-
import {
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { applyCacheEstimate } from "./cache-estimator.js";
|
|
5
|
+
import { debugEnabled, debugLog, formatSafeError, redactSensitiveText } from "./debug.js";
|
|
4
6
|
import { buildKiroAdditionalModelRequestFields, clampKiroEffort, getKiroEffortConfig } from "./effort.js";
|
|
5
|
-
import { getKiroEndpoints } from "./endpoints.js";
|
|
7
|
+
import { getKiroEndpoints, getKiroRegionFromProfileArn } from "./endpoints.js";
|
|
8
|
+
import { extractKiroReasonCode, KiroApiError, parseRetryAfterMs } from "./errors.js";
|
|
6
9
|
import { getKiroCliCredentials, getKiroCliCredentialsAllowExpired, refreshViaKiroCli } from "./kiro-cli.js";
|
|
7
10
|
import { invalidateKiroProfileArn, KiroManagementHttpError, resetKiroProfileArnCache, resolveKiroProfileArn, } from "./management.js";
|
|
8
11
|
import { isCacheStale, resolveKiroModel, updateKiroModelsCache } from "./models.js";
|
|
@@ -13,6 +16,116 @@ import { readKiroEventStream } from "./response-stream.js";
|
|
|
13
16
|
import { capacityRetryConfig, exponentialBackoff, extractKiroReason, firstTokenTimeoutForModel, isCapacityError, isNonRetryableBodyError, isTooBigError, KIRO_REASON_CODES, MAX_RETRY_DELAY, resolveRequestRateRetryDelay, retryConfig, } from "./retry.js";
|
|
14
17
|
import { kiroTokenTypeHeaders } from "./token-type.js";
|
|
15
18
|
import { abortableDelay, createResponseHeaderDeadline, logCapacityEvent } from "./transport.js";
|
|
19
|
+
import { estimateKiroCreditCost, KIRO_USAGE_TRACKING_DISABLED } from "./usage-tracking.js";
|
|
20
|
+
/**
|
|
21
|
+
* Pluralise an observed-attempt count for a diagnostic. The count is what was
|
|
22
|
+
* actually seen, not the configured retry budget: the two diverge whenever a
|
|
23
|
+
* 403 refresh, a timeout or a mid-stream error already spent part of the shared
|
|
24
|
+
* budget, and a diagnostic that exists to explain a silent failure must not
|
|
25
|
+
* itself assert something that did not happen.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately not worded as "consecutive": the degenerate attempts need not be
|
|
28
|
+
* adjacent. A 403 credential refresh or a mid-stream error can land between two
|
|
29
|
+
* of them and spend the same shared budget, so an unqualified count is the only
|
|
30
|
+
* claim the counter can actually support.
|
|
31
|
+
*/
|
|
32
|
+
function describeAttempts(count) {
|
|
33
|
+
return count === 1 ? "1 attempt" : `${count} attempts`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Cap for wire-derived echo text quoted into a persisted `errorMessage`. The
|
|
37
|
+
* echo pattern `/^\s*(continue|\.+)\s*$/i` admits an arbitrarily long run of
|
|
38
|
+
* dots, and this string is written into the stream's terminal diagnostic.
|
|
39
|
+
* Matches the 200-char cap used for raw tool input in the assembler's parse
|
|
40
|
+
* warning. Tool-name collections use their own whole-value policy in
|
|
41
|
+
* `describeDroppedToolNames`; they are never sliced into partial identities.
|
|
42
|
+
*/
|
|
43
|
+
const DIAGNOSTIC_QUOTE_LIMIT = 200;
|
|
44
|
+
/**
|
|
45
|
+
* INVARIANT: no unbounded integer may be interpolated into a persisted
|
|
46
|
+
* `errorMessage`. Consumers classify that string by pattern-matching its text,
|
|
47
|
+
* and retryable-error predicates in the wild match bare `429|500|502|503|504`
|
|
48
|
+
* with NO word boundary. So a `(5000 chars total)` annotation makes a
|
|
49
|
+
* diagnostic that says "terminal, do not retry" read as a transient HTTP 500
|
|
50
|
+
* and get suppressed — precisely the silent failure these diagnostics exist to
|
|
51
|
+
* defeat, reintroduced by the diagnostic itself.
|
|
52
|
+
*
|
|
53
|
+
* Hence the truncation marker carries no length: the exact length goes to
|
|
54
|
+
* `console.warn`, which no classifier reads. The only integer these diagnostics
|
|
55
|
+
* interpolate is the observed-attempt count, bounded by `maxRetries + 1` = 4.
|
|
56
|
+
*
|
|
57
|
+
* Wire-derived tool names can carry the same trigger text, so they are encoded
|
|
58
|
+
* before entering this diagnostic. See `encodeToolNameForDiagnostic`.
|
|
59
|
+
*/
|
|
60
|
+
function clampForDiagnostic(text) {
|
|
61
|
+
return text.length <= DIAGNOSTIC_QUOTE_LIMIT ? text : `${text.slice(0, DIAGNOSTIC_QUOTE_LIMIT)}… (truncated)`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Encode untrusted bytes without letting their text change how a consumer
|
|
65
|
+
* classifies the surrounding error. Each byte is represented by two letters,
|
|
66
|
+
* A through P, for its high and low nibbles. That alphabet contains no digits
|
|
67
|
+
* and cannot spell any alternative in a retryable-error predicate.
|
|
68
|
+
*/
|
|
69
|
+
function encodeBytesForDiagnostic(bytes) {
|
|
70
|
+
let encoded = "";
|
|
71
|
+
for (const byte of bytes) {
|
|
72
|
+
encoded += String.fromCharCode(65 + (byte >> 4), 65 + (byte & 0x0f));
|
|
73
|
+
}
|
|
74
|
+
return encoded;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Encode one tool-name identity reversibly from its UTF-16 code units. String
|
|
78
|
+
* names retain their exact value. A malformed non-string wire name is prefixed
|
|
79
|
+
* with its runtime type and JSON representation, so it stays distinguishable
|
|
80
|
+
* from a legitimate string with the same rendered text.
|
|
81
|
+
*
|
|
82
|
+
* Using `TextEncoder` here would replace an unpaired surrogate with U+FFFD,
|
|
83
|
+
* corrupting the only persisted identity of a dropped call; JSON permits that
|
|
84
|
+
* escaped shape and the event parser carries it through as a JavaScript string.
|
|
85
|
+
* Quoting any identity verbatim is unsafe: values such as `set_timeout` and
|
|
86
|
+
* `http500_probe` make a terminal diagnostic look transient to consumers.
|
|
87
|
+
*/
|
|
88
|
+
function encodeToolNameForDiagnostic(name) {
|
|
89
|
+
const identity = typeof name === "string" ? name : `${typeof name}:${JSON.stringify(name)}`;
|
|
90
|
+
let encoded = "";
|
|
91
|
+
for (let i = 0; i < identity.length; i++) {
|
|
92
|
+
const codeUnit = identity.charCodeAt(i);
|
|
93
|
+
encoded += String.fromCharCode(65 + (codeUnit >> 12), 65 + ((codeUnit >> 8) & 0x0f), 65 + ((codeUnit >> 4) & 0x0f), 65 + (codeUnit & 0x0f));
|
|
94
|
+
}
|
|
95
|
+
return encoded;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Describe the complete dropped-name set without unbounded output or partial
|
|
99
|
+
* identities. A set that fits is reversible name by name. If the complete set
|
|
100
|
+
* would exceed the diagnostic limit, replace all names with one SHA-256
|
|
101
|
+
* fingerprint. The explicit marker means no valid-looking name prefix can be
|
|
102
|
+
* mistaken for the whole identity, while the fingerprint still lets two
|
|
103
|
+
* records be compared exactly.
|
|
104
|
+
*/
|
|
105
|
+
function describeDroppedToolNames(names) {
|
|
106
|
+
const encoded = names.map((name) => `A-P:${encodeToolNameForDiagnostic(name)}`).join(", ");
|
|
107
|
+
if (encoded.length <= DIAGNOSTIC_QUOTE_LIMIT)
|
|
108
|
+
return encoded;
|
|
109
|
+
const digest = createHash("sha256").update(JSON.stringify(names)).digest();
|
|
110
|
+
return `A-P-DIGEST:${encodeBytesForDiagnostic(digest)} (tool identities fingerprinted)`;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* What the turn's blocks look like, for the exhausted-empty-response
|
|
114
|
+
* diagnostic. "No text and no tool calls" does NOT imply empty content: a
|
|
115
|
+
* reasoning turn that emits only `thinkingText` and then ends is degenerate by
|
|
116
|
+
* that test while a thinking block still exists, and a `ThinkingTagParser`
|
|
117
|
+
* turn can leave a zero-length text block behind. Claiming `empty content`
|
|
118
|
+
* there would assert something not observed.
|
|
119
|
+
*
|
|
120
|
+
* Block TYPES only, never a count: a count is an unbounded integer, which the
|
|
121
|
+
* invariant above forbids.
|
|
122
|
+
*/
|
|
123
|
+
function describeReturnedContent(kinds) {
|
|
124
|
+
const unique = [...new Set(kinds)].sort();
|
|
125
|
+
if (unique.length === 0)
|
|
126
|
+
return "returning empty content";
|
|
127
|
+
return `returning only ${unique.join(" and ")} content`;
|
|
128
|
+
}
|
|
16
129
|
let skipProfileResolutionForTests = false;
|
|
17
130
|
const TEST_PROFILE_ARN = "arn:aws:codewhisperer:us-east-1:000000000000:profile/test";
|
|
18
131
|
/** Reset profile resolution state — exported for stream tests. */
|
|
@@ -37,7 +150,6 @@ export async function* streamKiro(request) {
|
|
|
37
150
|
}
|
|
38
151
|
let accessToken = initialAccessToken;
|
|
39
152
|
const region = model.region ?? "us-east-1";
|
|
40
|
-
const endpoint = new URL("generateAssistantResponse", getKiroEndpoints(region).runtime).toString();
|
|
41
153
|
let managementAuth = { accessToken, region };
|
|
42
154
|
const cliCreds = getKiroCliCredentials() ?? getKiroCliCredentialsAllowExpired();
|
|
43
155
|
const cliProfileArn = cliCreds?.access === accessToken ? cliCreds.profileArn : undefined;
|
|
@@ -65,10 +177,17 @@ export async function* streamKiro(request) {
|
|
|
65
177
|
freshCreds.profileArn ||
|
|
66
178
|
(skipProfileResolutionForTests ? TEST_PROFILE_ARN : await resolveKiroProfileArn(managementAuth));
|
|
67
179
|
}
|
|
180
|
+
// ListAvailableProfiles probes across regions (#104, #131), so an SSO login
|
|
181
|
+
// in one region can legitimately resolve a profile owned by another. The
|
|
182
|
+
// runtime host and the catalog have to follow the profile: sending a
|
|
183
|
+
// cross-region profile ARN to the runtime API fails the whole request with
|
|
184
|
+
// a generic `Improperly formed request.`.
|
|
185
|
+
let runtimeRegion = getKiroRegionFromProfileArn(profileArn) ?? region;
|
|
186
|
+
let endpoint = new URL("generateAssistantResponse", getKiroEndpoints(runtimeRegion).runtime).toString();
|
|
68
187
|
// Refresh the catalog in the background when it has gone stale.
|
|
69
|
-
if (!process.env.VITEST && isCacheStale(
|
|
70
|
-
updateKiroModelsCache(accessToken,
|
|
71
|
-
console.warn(`[kiro-core] Failed to refresh Kiro model catalog in ${
|
|
188
|
+
if (!process.env.VITEST && isCacheStale(runtimeRegion)) {
|
|
189
|
+
updateKiroModelsCache(accessToken, runtimeRegion, profileArn).catch((error) => {
|
|
190
|
+
console.warn(`[kiro-core] Failed to refresh Kiro model catalog in ${runtimeRegion}: ${formatSafeError(error)}`);
|
|
72
191
|
});
|
|
73
192
|
}
|
|
74
193
|
const kiroModelId = resolveKiroModel(model.id, model.kiroModelId);
|
|
@@ -117,8 +236,30 @@ export async function* streamKiro(request) {
|
|
|
117
236
|
systemPrompt = `<thinking_mode>enabled</thinking_mode><max_thinking_length>${budget}</max_thinking_length>${systemPrompt ? `\n${systemPrompt}` : ""}`;
|
|
118
237
|
}
|
|
119
238
|
const assembler = new KiroResponseAssembler(model, thinkingEnabled);
|
|
239
|
+
const usageTracking = request.usageTracking ?? KIRO_USAGE_TRACKING_DISABLED;
|
|
120
240
|
let retryCount = 0;
|
|
121
241
|
const maxRetries = 3;
|
|
242
|
+
/** Degenerate attempts, counted BY SHAPE. Both are counted separately from
|
|
243
|
+
* `retryCount`, which is the shared retry budget also spent by 403 credential
|
|
244
|
+
* refreshes, idle/first-token timeouts and mid-stream errors — so
|
|
245
|
+
* `maxRetries + 1` is NOT the number of empty attempts, and reporting it as
|
|
246
|
+
* such overstates what was observed.
|
|
247
|
+
*
|
|
248
|
+
* Split rather than pooled because the two shapes are not interchangeable and
|
|
249
|
+
* the exhaustion diagnostic is worded from the LAST attempt's shape only. The
|
|
250
|
+
* model can echo on one attempt and return nothing on the next; a single
|
|
251
|
+
* pooled counter would then make "returned no text ... on 4 attempts" out of
|
|
252
|
+
* three empty attempts and one that did carry text, or claim four echoes from
|
|
253
|
+
* one. Each diagnostic reports its own shape's count and, when the other shape
|
|
254
|
+
* also occurred, names it separately. */
|
|
255
|
+
let emptyAttempts = 0;
|
|
256
|
+
let echoAttempts = 0;
|
|
257
|
+
// Cumulative provider-internal retry tallies reported on KiroApiError.
|
|
258
|
+
// `retryCount` cannot stand in for either: it is also consumed by stream
|
|
259
|
+
// errors, idle/first-token timeouts, and empty-response retries, and
|
|
260
|
+
// `capacityRetryCount` resets on every outer iteration.
|
|
261
|
+
let credentialRefreshTotal = 0;
|
|
262
|
+
let capacityRetryTotal = 0;
|
|
122
263
|
const conversationId = request.sessionId ?? crypto.randomUUID();
|
|
123
264
|
requestLoop: while (retryCount <= maxRetries) {
|
|
124
265
|
if (signal?.aborted)
|
|
@@ -210,6 +351,7 @@ export async function* streamKiro(request) {
|
|
|
210
351
|
// Retry transient capacity errors with longer backoff.
|
|
211
352
|
if (isCapacityError(errText) && capacityRetryCount < capacityRetryConfig.maxRetries) {
|
|
212
353
|
capacityRetryCount++;
|
|
354
|
+
capacityRetryTotal++;
|
|
213
355
|
const delayMs = exponentialBackoff(capacityRetryCount - 1, capacityRetryConfig.baseDelayMs, 30_000);
|
|
214
356
|
logCapacityEvent(`INSUFFICIENT_MODEL_CAPACITY — retrying in ${delayMs}ms (${capacityRetryCount}/${capacityRetryConfig.maxRetries})`);
|
|
215
357
|
await abortableDelay(delayMs, signal);
|
|
@@ -237,6 +379,7 @@ export async function* streamKiro(request) {
|
|
|
237
379
|
}
|
|
238
380
|
if (response.status === 403 && !isCapacityError(errText) && retryCount < maxRetries) {
|
|
239
381
|
retryCount++;
|
|
382
|
+
credentialRefreshTotal++;
|
|
240
383
|
// Re-read the shared store first in case another process already
|
|
241
384
|
// rotated the token. If it still contains the rejected token, force
|
|
242
385
|
// kiro-cli to refresh before retrying runtime.
|
|
@@ -263,19 +406,35 @@ export async function* streamKiro(request) {
|
|
|
263
406
|
freshCreds?.profileArn ||
|
|
264
407
|
inheritedDesktopProfileArn ||
|
|
265
408
|
(skipProfileResolutionForTests ? TEST_PROFILE_ARN : await resolveKiroProfileArn(managementAuth));
|
|
409
|
+
// A replacement credential can carry a profile in another region,
|
|
410
|
+
// so re-pin the runtime host before retrying.
|
|
411
|
+
runtimeRegion = getKiroRegionFromProfileArn(profileArn) ?? region;
|
|
412
|
+
endpoint = new URL("generateAssistantResponse", getKiroEndpoints(runtimeRegion).runtime).toString();
|
|
266
413
|
await abortableDelay(exponentialBackoff(retryCount - 1, 500, MAX_RETRY_DELAY), signal);
|
|
267
414
|
break; // break inner loop, continue outer loop
|
|
268
415
|
}
|
|
269
416
|
// Known quota/capacity body markers must not be re-read by a host's own
|
|
270
|
-
// outer auto-retry as a generic retryable 429.
|
|
417
|
+
// outer auto-retry as a generic retryable 429. This covers both hard
|
|
418
|
+
// quota (MONTHLY_REQUEST_COUNT) and exhausted capacity retries
|
|
419
|
+
// (INSUFFICIENT_MODEL_CAPACITY).
|
|
420
|
+
//
|
|
421
|
+
// The three throws below carry identical `message` text to what this
|
|
422
|
+
// provider has always emitted — host adapters and downstream consumers
|
|
423
|
+
// string-match it. KiroApiError adds the classification as typed fields
|
|
424
|
+
// alongside that text; it never changes it.
|
|
425
|
+
const errorMeta = {
|
|
426
|
+
reasonCode: extractKiroReasonCode(errText),
|
|
427
|
+
retryAfterMs: parseRetryAfterMs(response.headers),
|
|
428
|
+
providerAttempts: { credentialRefresh: credentialRefreshTotal, capacity: capacityRetryTotal },
|
|
429
|
+
};
|
|
271
430
|
if (isNonRetryableBodyError(errText) || isCapacityError(errText)) {
|
|
272
|
-
throw new
|
|
431
|
+
throw new KiroApiError(`Kiro API error: ${errText || safeStatusText}`, response.status, errorMeta.reasonCode, errorMeta.retryAfterMs, errorMeta.providerAttempts);
|
|
273
432
|
}
|
|
274
433
|
// Phrase overflow so a host's context-overflow detector recognizes it.
|
|
275
434
|
if (isTooBigError(response.status, errText)) {
|
|
276
|
-
throw new
|
|
435
|
+
throw new KiroApiError(`Kiro API error: context_length_exceeded (${response.status} ${errText})`, response.status, errorMeta.reasonCode, errorMeta.retryAfterMs, errorMeta.providerAttempts);
|
|
277
436
|
}
|
|
278
|
-
throw new
|
|
437
|
+
throw new KiroApiError(`Kiro API error: ${response.status} ${safeStatusText} ${errText}`, response.status, errorMeta.reasonCode, errorMeta.retryAfterMs, errorMeta.providerAttempts);
|
|
279
438
|
}
|
|
280
439
|
break; // success, break inner loop
|
|
281
440
|
}
|
|
@@ -291,6 +450,7 @@ export async function* streamKiro(request) {
|
|
|
291
450
|
assembler.beginAttempt();
|
|
292
451
|
const { frames, outcome } = readKiroEventStream(response.body, {
|
|
293
452
|
firstTokenTimeoutMs: model.firstTokenTimeout ?? firstTokenTimeoutForModel(model.id),
|
|
453
|
+
signal,
|
|
294
454
|
});
|
|
295
455
|
for await (const frame of frames) {
|
|
296
456
|
assembler.handle(frame);
|
|
@@ -301,6 +461,23 @@ export async function* streamKiro(request) {
|
|
|
301
461
|
// Timed out or received an error mid-stream: retry with backoff.
|
|
302
462
|
if (retryCount < maxRetries) {
|
|
303
463
|
retryCount++;
|
|
464
|
+
if (outcome.errorData && debugEnabled()) {
|
|
465
|
+
debugLog("stream.error.typed", [outcome.errorData]);
|
|
466
|
+
}
|
|
467
|
+
// The assembler outlives the retry loop, so anything the aborted
|
|
468
|
+
// attempt already emitted survives into the next one. A typed error
|
|
469
|
+
// frame (throttling/validation/serviceUnavailable) can arrive after
|
|
470
|
+
// partial text, which would otherwise concatenate the abandoned prefix
|
|
471
|
+
// onto the retried response. The degenerate-response retry below
|
|
472
|
+
// discards for the same reason. The usage figures are cleared by
|
|
473
|
+
// `beginAttempt` at the next loop top.
|
|
474
|
+
//
|
|
475
|
+
// The event protocol has no retraction event, so deltas already pushed
|
|
476
|
+
// for the abandoned attempt cannot be withdrawn. The signals a
|
|
477
|
+
// consumer does get are the `reset` event and the fresh `start`
|
|
478
|
+
// emitted for the retried attempt.
|
|
479
|
+
assembler.discard();
|
|
480
|
+
yield* assembler.takeEvents();
|
|
304
481
|
await abortableDelay(exponentialBackoff(retryCount - 1, 1000, MAX_RETRY_DELAY), signal);
|
|
305
482
|
continue;
|
|
306
483
|
}
|
|
@@ -320,7 +497,13 @@ export async function* streamKiro(request) {
|
|
|
320
497
|
// When tool calls *were* present but all got dropped (empty/unparseable
|
|
321
498
|
// input), don't retry — the API did respond, it just sent malformed tool
|
|
322
499
|
// calls. Retrying would likely produce the same result.
|
|
323
|
-
|
|
500
|
+
const degenerate = summary.isEmpty || summary.isEchoLoop;
|
|
501
|
+
if (summary.isEchoLoop)
|
|
502
|
+
echoAttempts++;
|
|
503
|
+
else if (degenerate)
|
|
504
|
+
emptyAttempts++;
|
|
505
|
+
let errorMessage;
|
|
506
|
+
if (degenerate) {
|
|
324
507
|
// Retrying an echo loop means unsaying text already delivered, which only
|
|
325
508
|
// a host that can discard emitted blocks may do. Elsewhere, go straight to
|
|
326
509
|
// the terminal behaviour: strip the echo so the agent loop does not read
|
|
@@ -334,22 +517,69 @@ export async function* streamKiro(request) {
|
|
|
334
517
|
await abortableDelay(exponentialBackoff(retryCount - 1, 1000, MAX_RETRY_DELAY), signal);
|
|
335
518
|
continue;
|
|
336
519
|
}
|
|
520
|
+
// Retries are spent (or the host cannot discard an echo) and the turn
|
|
521
|
+
// still carries nothing usable. The stopReason stays in the existing
|
|
522
|
+
// union — a new member would break every consumer — so the only channel
|
|
523
|
+
// that can say a turn failed while it still looks successful is the
|
|
524
|
+
// `errorMessage` carried on the terminal `done` event. Without it these
|
|
525
|
+
// turns are indistinguishable from an ordinary completion.
|
|
526
|
+
//
|
|
527
|
+
// Deliberately NOT worded as a transient/transport failure: this is
|
|
528
|
+
// terminal, so consumer retry classifiers must not match it and hand it
|
|
529
|
+
// another doomed attempt.
|
|
337
530
|
if (summary.isEchoLoop) {
|
|
531
|
+
// Strip the echo text to prevent the agent loop from interpreting
|
|
532
|
+
// "Continue" as a continuation signal.
|
|
338
533
|
assembler.stripEcho();
|
|
339
|
-
|
|
534
|
+
const alsoEmpty = emptyAttempts > 0 ? ` (plus ${describeAttempts(emptyAttempts)} with no text at all)` : "";
|
|
535
|
+
console.warn(`[kiro-core] Echo loop persisted across ${describeAttempts(echoAttempts)}${alsoEmpty} — stripping "Continue" response (${summary.responseText.length} chars)`);
|
|
536
|
+
errorMessage = `Kiro model echoed its own continuation prompt (${JSON.stringify(clampForDiagnostic(summary.responseText))}) on ${describeAttempts(echoAttempts)}${alsoEmpty} and emitted no tool calls; retry budget exhausted, text stripped, stopReason:"${summary.stopReason}"`;
|
|
340
537
|
}
|
|
341
538
|
else {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
// not actually guarantee.
|
|
346
|
-
console.warn(`[kiro-core] Empty response after ${maxRetries} retries — giving up on this turn`);
|
|
539
|
+
const alsoEchoed = echoAttempts > 0 ? ` (plus ${describeAttempts(echoAttempts)} that echoed the continuation prompt)` : "";
|
|
540
|
+
console.warn(`[kiro-core] Empty response on ${describeAttempts(emptyAttempts)}${alsoEchoed}, retry budget exhausted — returning stopReason:"${summary.stopReason}" to avoid agent loop stall`);
|
|
541
|
+
errorMessage = `Kiro returned no text and no tool calls on ${describeAttempts(emptyAttempts)}${alsoEchoed}; retry budget exhausted, ${describeReturnedContent(assembler.contentKinds())} with stopReason:"${summary.stopReason}"`;
|
|
347
542
|
}
|
|
348
543
|
}
|
|
349
|
-
|
|
544
|
+
// A tool call the model DID make never reached the host: its arguments
|
|
545
|
+
// would not parse, so the assembler dropped it. Nothing else records this
|
|
546
|
+
// — `sawAnyToolCalls` is already true, which is exactly what suppresses the
|
|
547
|
+
// empty-response retry above and the text-dialect fallback — and the block
|
|
548
|
+
// stream simply lacks the call. Unlike the two exhaustion cases, this one
|
|
549
|
+
// is unrecoverable downstream: the call is gone before the events are
|
|
550
|
+
// persisted.
|
|
551
|
+
if (summary.droppedToolCalls.length > 0) {
|
|
552
|
+
const names = describeDroppedToolNames(summary.droppedToolCalls);
|
|
553
|
+
// The reversible names or whole-set fingerprint identify the drops, so
|
|
554
|
+
// the count is not printed: it is unbounded (a turn may carry any
|
|
555
|
+
// number of malformed calls) and unbounded or wire-controlled text
|
|
556
|
+
// here can collide with a consumer's retryable-error pattern.
|
|
557
|
+
const one = summary.droppedToolCalls.length === 1;
|
|
558
|
+
const dropDiagnostic = `Kiro sent ${one ? "a tool call" : "tool calls"} with unparseable arguments (${names}); ${one ? "it was" : "they were"} dropped and never reached the agent, stopReason:"${summary.stopReason}"`;
|
|
559
|
+
// Concatenation is defensive: today the two diagnostics are mutually
|
|
560
|
+
// exclusive, because any drop sets `sawAnyToolCalls` and `degenerate`
|
|
561
|
+
// requires `!sawAnyToolCalls`. Kept so that loosening either predicate
|
|
562
|
+
// appends rather than silently overwriting an exhaustion diagnostic.
|
|
563
|
+
errorMessage = errorMessage ? `${errorMessage}. ${dropDiagnostic}` : dropDiagnostic;
|
|
564
|
+
}
|
|
565
|
+
const { stopReason, usage, wireUsage, metering } = assembler.complete();
|
|
350
566
|
yield* assembler.takeEvents();
|
|
567
|
+
if (!errorMessage) {
|
|
568
|
+
const estimatedRead = applyCacheEstimate(conversationId, usage, wireUsage, usageTracking);
|
|
569
|
+
if (estimatedRead > 0) {
|
|
570
|
+
debugLog("usage.estimate", {
|
|
571
|
+
conversationId,
|
|
572
|
+
estimatedRead,
|
|
573
|
+
input: usage.input,
|
|
574
|
+
cacheRead: usage.cacheRead,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
const estimatedCost = estimateKiroCreditCost(usageTracking, metering);
|
|
578
|
+
if (estimatedCost !== undefined)
|
|
579
|
+
usage.cost.total = estimatedCost;
|
|
580
|
+
}
|
|
351
581
|
yield { type: "usage", usage };
|
|
352
|
-
yield { type: "done", stopReason };
|
|
582
|
+
yield { type: "done", stopReason, ...(errorMessage ? { errorMessage } : {}) };
|
|
353
583
|
return;
|
|
354
584
|
}
|
|
355
585
|
}
|