ns-kiro-core 0.1.2 → 0.2.0
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/cost.d.ts +6 -1
- package/dist/cost.js +9 -2
- package/dist/cost.js.map +1 -1
- package/dist/effort.js +10 -2
- package/dist/effort.js.map +1 -1
- package/dist/event-parser.d.ts +28 -4
- package/dist/event-parser.js +40 -1
- package/dist/event-parser.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/kiro-cli.d.ts +17 -0
- package/dist/kiro-cli.js +47 -1
- package/dist/kiro-cli.js.map +1 -1
- package/dist/models.d.ts +10 -2
- package/dist/models.js +70 -4
- package/dist/models.js.map +1 -1
- package/dist/request-builder.d.ts +49 -0
- package/dist/request-builder.js +229 -0
- package/dist/request-builder.js.map +1 -0
- package/dist/response-assembler.d.ts +89 -0
- package/dist/response-assembler.js +330 -0
- package/dist/response-assembler.js.map +1 -0
- package/dist/response-stream.d.ts +39 -0
- package/dist/response-stream.js +123 -0
- package/dist/response-stream.js.map +1 -0
- package/dist/stream.js +72 -621
- package/dist/stream.js.map +1 -1
- package/dist/transport.d.ts +24 -0
- package/dist/transport.js +80 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/package.json +6 -1
package/dist/stream.js
CHANGED
|
@@ -1,94 +1,18 @@
|
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { UniversalEventStreamMarshaller } from "@smithy/core/event-streams";
|
|
7
|
-
import { KiroBlockBuffer } from "./blocks.js";
|
|
8
|
-
import { parseBracketToolCalls } from "./bracket-tool-parser.js";
|
|
9
|
-
import { calculateKiroCost } from "./cost.js";
|
|
10
|
-
import { debugEnabled, debugLog, formatSafeError, redactSensitiveText } from "./debug.js";
|
|
11
|
-
import { buildKiroAdditionalModelRequestFields, clampKiroEffort, getKiroEffortConfig, } from "./effort.js";
|
|
3
|
+
import { debugLog, formatSafeError, redactSensitiveText } from "./debug.js";
|
|
4
|
+
import { buildKiroAdditionalModelRequestFields, clampKiroEffort, getKiroEffortConfig } from "./effort.js";
|
|
12
5
|
import { getKiroEndpoints } from "./endpoints.js";
|
|
13
|
-
import { parseKiroEvent } from "./event-parser.js";
|
|
14
|
-
import { addPlaceholderTools, assertHistoryWithinLimit, HISTORY_LIMIT, HISTORY_LIMIT_CONTEXT_WINDOW, prepareHistory, } from "./history.js";
|
|
15
|
-
import { isKiroToolStructureRule, kiroConversationEntries, repairKiroConversation } from "./history-validator.js";
|
|
16
|
-
import { parseInvokeToolCalls } from "./invoke-tool-parser.js";
|
|
17
6
|
import { getKiroCliCredentials, getKiroCliCredentialsAllowExpired, refreshViaKiroCli } from "./kiro-cli.js";
|
|
18
7
|
import { invalidateKiroProfileArn, KiroManagementHttpError, resetKiroProfileArnCache, resolveKiroProfileArn, } from "./management.js";
|
|
19
8
|
import { isCacheStale, resolveKiroModel, updateKiroModelsCache } from "./models.js";
|
|
20
9
|
import { kiroAuthHeaders } from "./oauth.js";
|
|
10
|
+
import { buildKiroRequest } from "./request-builder.js";
|
|
11
|
+
import { KiroResponseAssembler } from "./response-assembler.js";
|
|
12
|
+
import { readKiroEventStream } from "./response-stream.js";
|
|
21
13
|
import { capacityRetryConfig, exponentialBackoff, extractKiroReason, firstTokenTimeoutForModel, isCapacityError, isNonRetryableBodyError, isTooBigError, KIRO_REASON_CODES, MAX_RETRY_DELAY, resolveRequestRateRetryDelay, retryConfig, } from "./retry.js";
|
|
22
|
-
import { ThinkingTagParser } from "./thinking-parser.js";
|
|
23
14
|
import { kiroTokenTypeHeaders } from "./token-type.js";
|
|
24
|
-
import {
|
|
25
|
-
import { buildHistory, convertImagesToKiro, convertToolsToKiro, EMPTY_CONTENT_PLACEHOLDER, extractImages, getContentText, normalizeMessages, relocateDisplacedToolResults, sanitizeSurrogates, TOOL_RESULT_LIMIT, toKiroToolUseId, truncate, } from "./transform.js";
|
|
26
|
-
import { TRUNCATION_NOTICE, wasPreviousResponseTruncated } from "./truncation.js";
|
|
27
|
-
const CAPACITY_LOG_DIR = join(homedir(), ".ns-kiro-provider", "logs");
|
|
28
|
-
const CAPACITY_LOG_FILE = join(CAPACITY_LOG_DIR, "capacity-retries.log");
|
|
29
|
-
const eventStreamMarshaller = new UniversalEventStreamMarshaller({
|
|
30
|
-
utf8Encoder: (input) => new TextDecoder().decode(input),
|
|
31
|
-
utf8Decoder: (input) => new TextEncoder().encode(input),
|
|
32
|
-
});
|
|
33
|
-
let capacityLogDirCreated = false;
|
|
34
|
-
function logCapacityEvent(message) {
|
|
35
|
-
// Fire-and-forget async logging to avoid blocking the event loop
|
|
36
|
-
(async () => {
|
|
37
|
-
try {
|
|
38
|
-
if (!capacityLogDirCreated) {
|
|
39
|
-
await mkdir(CAPACITY_LOG_DIR, { recursive: true });
|
|
40
|
-
capacityLogDirCreated = true;
|
|
41
|
-
}
|
|
42
|
-
await appendFile(CAPACITY_LOG_FILE, `${new Date().toISOString()} ${message}\n`);
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
// best-effort logging, don't break the provider
|
|
46
|
-
}
|
|
47
|
-
})();
|
|
48
|
-
}
|
|
49
|
-
/** Delay that rejects early if the abort signal fires. */
|
|
50
|
-
function abortableDelay(ms, signal) {
|
|
51
|
-
if (signal?.aborted)
|
|
52
|
-
return Promise.reject(signal.reason);
|
|
53
|
-
return new Promise((resolve, reject) => {
|
|
54
|
-
const onAbort = () => {
|
|
55
|
-
clearTimeout(timer);
|
|
56
|
-
signal?.removeEventListener("abort", onAbort);
|
|
57
|
-
reject(signal?.reason);
|
|
58
|
-
};
|
|
59
|
-
const timer = setTimeout(() => {
|
|
60
|
-
signal?.removeEventListener("abort", onAbort);
|
|
61
|
-
resolve();
|
|
62
|
-
}, ms);
|
|
63
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
function createResponseHeaderDeadline(callerSignal, timeoutMs) {
|
|
67
|
-
const controller = new AbortController();
|
|
68
|
-
let timedOut = false;
|
|
69
|
-
const onCallerAbort = () => {
|
|
70
|
-
clearTimeout(timer);
|
|
71
|
-
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
72
|
-
controller.abort(callerSignal?.reason);
|
|
73
|
-
};
|
|
74
|
-
const timer = setTimeout(() => {
|
|
75
|
-
timedOut = true;
|
|
76
|
-
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
77
|
-
controller.abort(new DOMException("Kiro response headers timeout", "TimeoutError"));
|
|
78
|
-
}, timeoutMs);
|
|
79
|
-
if (callerSignal?.aborted)
|
|
80
|
-
onCallerAbort();
|
|
81
|
-
else
|
|
82
|
-
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
83
|
-
return {
|
|
84
|
-
signal: controller.signal,
|
|
85
|
-
didTimeout: () => timedOut,
|
|
86
|
-
cleanup: () => {
|
|
87
|
-
clearTimeout(timer);
|
|
88
|
-
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
}
|
|
15
|
+
import { abortableDelay, createResponseHeaderDeadline, logCapacityEvent } from "./transport.js";
|
|
92
16
|
let skipProfileResolutionForTests = false;
|
|
93
17
|
const TEST_PROFILE_ARN = "arn:aws:codewhisperer:us-east-1:000000000000:profile/test";
|
|
94
18
|
/** Reset profile resolution state — exported for stream tests. */
|
|
@@ -107,14 +31,6 @@ export function resetProfileArnCache(resolved = false) {
|
|
|
107
31
|
*/
|
|
108
32
|
export async function* streamKiro(request) {
|
|
109
33
|
const { model, signal } = request;
|
|
110
|
-
const pending = [];
|
|
111
|
-
const blocks = new KiroBlockBuffer((event) => pending.push(event));
|
|
112
|
-
const usage = {
|
|
113
|
-
input: 0,
|
|
114
|
-
output: 0,
|
|
115
|
-
totalTokens: 0,
|
|
116
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
117
|
-
};
|
|
118
34
|
const initialAccessToken = request.accessToken;
|
|
119
35
|
if (!initialAccessToken) {
|
|
120
36
|
throw new Error("Kiro credentials not set. Run `kiro-cli login`, or set KIRO_API_KEY.");
|
|
@@ -174,225 +90,50 @@ export async function* streamKiro(request) {
|
|
|
174
90
|
sessionId: request.sessionId,
|
|
175
91
|
});
|
|
176
92
|
let systemPrompt = request.systemPrompt ?? "";
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
93
|
+
// Legacy fallback for turning the thinking stream on, kept only where nothing
|
|
94
|
+
// better exists. When the request already carries the catalog's own `thinking`
|
|
95
|
+
// field, the markers are pure duplication: they restate in prose what the
|
|
96
|
+
// structured field states, and prepend an effort-dependent budget to the front
|
|
97
|
+
// of the system prompt for no gain.
|
|
98
|
+
//
|
|
99
|
+
// Verified 2026-09-06 against claude-sonnet-5 at effort `high`: dropping the
|
|
100
|
+
// markers left the thinking stream intact — one block, comparable length —
|
|
101
|
+
// in both arrangements.
|
|
102
|
+
//
|
|
103
|
+
// This does NOT buy back Kiro's server-side prompt cache. Measured the same
|
|
104
|
+
// day: a repeated prefix bills ~0.035 credits against ~0.066 for a fresh one,
|
|
105
|
+
// but changing effort misses even when the system prompt is byte-identical,
|
|
106
|
+
// and each effort then warms its own entry. The effort travels in
|
|
107
|
+
// `additionalModelRequestFields`, so it is part of the cache key no matter
|
|
108
|
+
// what the prompt says.
|
|
109
|
+
//
|
|
110
|
+
// Still emitted when Kiro offers no structured control: a model whose catalog
|
|
111
|
+
// entry carries no effort schema, or a Claude turn with no effort selected,
|
|
112
|
+
// has nothing else to switch thinking on with. Models keyed off `reasoning`
|
|
113
|
+
// (the GPT family) never wanted the markers at all.
|
|
114
|
+
const sendsThinkingField = !!additionalModelRequestFields && "thinking" in additionalModelRequestFields;
|
|
115
|
+
if (thinkingEnabled && effortConfig?.field !== "reasoning" && !sendsThinkingField) {
|
|
182
116
|
const budget = effort === "xhigh" || effort === "max" ? 50000 : effort === "high" ? 30000 : effort === "medium" ? 20000 : 10000;
|
|
183
117
|
systemPrompt = `<thinking_mode>enabled</thinking_mode><max_thinking_length>${budget}</max_thinking_length>${systemPrompt ? `\n${systemPrompt}` : ""}`;
|
|
184
118
|
}
|
|
119
|
+
const assembler = new KiroResponseAssembler(model, thinkingEnabled);
|
|
185
120
|
let retryCount = 0;
|
|
186
121
|
const maxRetries = 3;
|
|
187
122
|
const conversationId = request.sessionId ?? crypto.randomUUID();
|
|
188
123
|
requestLoop: while (retryCount <= maxRetries) {
|
|
189
124
|
if (signal?.aborted)
|
|
190
125
|
throw signal.reason;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// Preserve semantic context locally; the host owns lossy compaction.
|
|
199
|
-
const history = prepareHistory(rawHistory, model.input.includes("image"));
|
|
200
|
-
const dynamicHistoryLimit = Math.floor((model.contextWindow / HISTORY_LIMIT_CONTEXT_WINDOW) * HISTORY_LIMIT);
|
|
201
|
-
const currentMessages = normalized.slice(currentMsgStartIdx);
|
|
202
|
-
const firstMsg = currentMessages[0];
|
|
203
|
-
let currentContent = "";
|
|
204
|
-
const currentToolResults = [];
|
|
205
|
-
let currentImages;
|
|
206
|
-
if (firstMsg?.role === "assistant") {
|
|
207
|
-
let armContent = "";
|
|
208
|
-
const armToolUses = [];
|
|
209
|
-
for (const block of firstMsg.content) {
|
|
210
|
-
if (block.type === "text")
|
|
211
|
-
armContent += block.text;
|
|
212
|
-
// Reasoning is deliberately NOT serialized into the assistant text
|
|
213
|
-
// channel, matching `buildHistory`. Flattening it to
|
|
214
|
-
// `<thinking>...</thinking>` writes literal markup into the string the
|
|
215
|
-
// model reads back as its own prior speech.
|
|
216
|
-
else if (block.type === "toolCall") {
|
|
217
|
-
armToolUses.push({ name: block.name, toolUseId: toKiroToolUseId(block.id), input: block.arguments });
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (armContent || armToolUses.length > 0) {
|
|
221
|
-
const prevArm = history[history.length - 1]?.assistantResponseMessage;
|
|
222
|
-
if (history.length > 0 && !history[history.length - 1]?.userInputMessage && prevArm) {
|
|
223
|
-
// Merge into previous assistant message to maintain alternation
|
|
224
|
-
// without synthetic padding. Join only non-empty sides: a turn that
|
|
225
|
-
// carried only reasoning or only a tool call leaves `armContent`
|
|
226
|
-
// empty, and an unconditional separator would append a bare `\n\n`
|
|
227
|
-
// onto text the model actually produced.
|
|
228
|
-
prevArm.content =
|
|
229
|
-
prevArm.content && armContent ? `${prevArm.content}\n\n${armContent}` : prevArm.content || armContent;
|
|
230
|
-
if (armToolUses.length > 0)
|
|
231
|
-
prevArm.toolUses = [...(prevArm.toolUses || []), ...armToolUses];
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
history.push({
|
|
235
|
-
assistantResponseMessage: {
|
|
236
|
-
content: armContent,
|
|
237
|
-
...(armToolUses.length > 0 ? { toolUses: armToolUses } : {}),
|
|
238
|
-
},
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
const toolResultImages = [];
|
|
243
|
-
for (let i = 1; i < currentMessages.length; i++) {
|
|
244
|
-
const m = currentMessages[i];
|
|
245
|
-
if (m?.role !== "toolResult")
|
|
246
|
-
continue;
|
|
247
|
-
currentToolResults.push({
|
|
248
|
-
content: [{ text: truncate(getContentText(m), TOOL_RESULT_LIMIT) }],
|
|
249
|
-
status: m.isError ? "error" : "success",
|
|
250
|
-
toolUseId: toKiroToolUseId(m.toolCallId),
|
|
251
|
-
});
|
|
252
|
-
toolResultImages.push(...extractImages(m));
|
|
253
|
-
}
|
|
254
|
-
if (toolResultImages.length > 0)
|
|
255
|
-
currentImages = convertImagesToKiro(toolResultImages);
|
|
256
|
-
// A tool turn carries its payload in `userInputMessageContext.toolResults`,
|
|
257
|
-
// so it needs no text. Leaving this empty also leaves the fallback below
|
|
258
|
-
// free to fill in only genuinely payload-less turns.
|
|
259
|
-
currentContent = "";
|
|
260
|
-
}
|
|
261
|
-
else if (firstMsg?.role === "toolResult") {
|
|
262
|
-
const toolResultImages = [];
|
|
263
|
-
for (const m of currentMessages) {
|
|
264
|
-
if (m.role !== "toolResult")
|
|
265
|
-
continue;
|
|
266
|
-
currentToolResults.push({
|
|
267
|
-
content: [{ text: truncate(getContentText(m), TOOL_RESULT_LIMIT) }],
|
|
268
|
-
status: m.isError ? "error" : "success",
|
|
269
|
-
toolUseId: toKiroToolUseId(m.toolCallId),
|
|
270
|
-
});
|
|
271
|
-
toolResultImages.push(...extractImages(m));
|
|
272
|
-
}
|
|
273
|
-
if (toolResultImages.length > 0)
|
|
274
|
-
currentImages = convertImagesToKiro(toolResultImages);
|
|
275
|
-
// Empty by design — `toolResults` is this turn's payload.
|
|
276
|
-
currentContent = "";
|
|
277
|
-
}
|
|
278
|
-
else if (firstMsg?.role === "user") {
|
|
279
|
-
currentContent = getContentText(firstMsg);
|
|
280
|
-
if (systemPrompt && !systemPrepended)
|
|
281
|
-
currentContent = `${systemPrompt}\n\n${currentContent}`;
|
|
282
|
-
}
|
|
283
|
-
// Current assistant tool calls are outbound history too, so enforce the
|
|
284
|
-
// budget only after they have been appended.
|
|
285
|
-
assertHistoryWithinLimit(history, dynamicHistoryLimit);
|
|
286
|
-
if (wasPreviousResponseTruncated(request.messages)) {
|
|
287
|
-
currentContent = currentContent === "" ? TRUNCATION_NOTICE : `${TRUNCATION_NOTICE}\n\n${currentContent}`;
|
|
288
|
-
}
|
|
289
|
-
// Always synthesize placeholder specs for tool names referenced in history,
|
|
290
|
-
// even when the caller declares none. Without this, a call that inherits a
|
|
291
|
-
// tool-rich conversation but declares no current tools is rejected by Kiro
|
|
292
|
-
// as "Improperly formed request", because history references toolUses with
|
|
293
|
-
// no tool catalog.
|
|
294
|
-
let uimc;
|
|
295
|
-
const baseTools = request.tools?.length ? convertToolsToKiro(request.tools) : [];
|
|
296
|
-
const finalTools = history.length > 0 ? addPlaceholderTools(baseTools, history) : baseTools;
|
|
297
|
-
if (currentToolResults.length > 0 || finalTools.length > 0) {
|
|
298
|
-
uimc = {};
|
|
299
|
-
if (currentToolResults.length > 0)
|
|
300
|
-
uimc.toolResults = currentToolResults;
|
|
301
|
-
if (finalTools.length > 0)
|
|
302
|
-
uimc.tools = finalTools;
|
|
303
|
-
}
|
|
304
|
-
if (firstMsg?.role === "user") {
|
|
305
|
-
const imgs = extractImages(firstMsg);
|
|
306
|
-
if (imgs.length > 0)
|
|
307
|
-
currentImages = convertImagesToKiro(imgs);
|
|
308
|
-
}
|
|
309
|
-
// A turn with neither text nor tool results has no payload at all: an
|
|
310
|
-
// image-only user message, or an empty-text one. Send a neutral prompt so
|
|
311
|
-
// its attachments still reach the model.
|
|
312
|
-
//
|
|
313
|
-
// The `currentToolResults` guard is load-bearing. Without it this line
|
|
314
|
-
// refills every tool turn that deliberately left `currentContent` empty.
|
|
315
|
-
// Kiro's rule is content **or** tool results — see EMPTY_CONTENT_PLACEHOLDER.
|
|
316
|
-
if (currentContent === "" && currentToolResults.length === 0)
|
|
317
|
-
currentContent = EMPTY_CONTENT_PLACEHOLDER;
|
|
318
|
-
// Pre-send REPAIR against the rules first-party Kiro Agent enforces.
|
|
319
|
-
// `prepareHistory` covers the shapes this provider itself produces, but not
|
|
320
|
-
// every shape a caller can hand us: `sanitizeHistory` tests tool pairing by
|
|
321
|
-
// POSITION, so an assistant entry with `toolUses` survives whenever the next
|
|
322
|
-
// entry carries any `toolResults` at all, matching ids or not, and
|
|
323
|
-
// `injectSyntheticToolCalls` only rescues orphaned RESULTS. A mismatched
|
|
324
|
-
// pair — both partners present, paired with each other's counterpart —
|
|
325
|
-
// passes both passes untouched and is rejected on the wire with
|
|
326
|
-
// `400 TOOL_USE_RESULT_MISMATCH`.
|
|
327
|
-
//
|
|
328
|
-
// Repair runs on the WHOLE conversation and is split back afterwards.
|
|
329
|
-
// Repairing `history` alone would be wrong in the ordinary case: its last
|
|
330
|
-
// entry is normally the assistant whose `toolUses` this very request
|
|
331
|
-
// answers, so rule 4 would synthesize a FAILED result for a call whose real
|
|
332
|
-
// output is sitting in the current message.
|
|
333
|
-
const conversationEntries = kiroConversationEntries(history, {
|
|
334
|
-
content: currentContent,
|
|
335
|
-
modelId: kiroModelId,
|
|
336
|
-
origin: "KIRO_CLI",
|
|
337
|
-
...(uimc ? { userInputMessageContext: uimc } : {}),
|
|
338
|
-
});
|
|
339
|
-
const repair = repairKiroConversation(conversationEntries);
|
|
340
|
-
if (repair.diagnostics.length > 0) {
|
|
341
|
-
debugLog("request.invariants", { errors: repair.diagnostics, remaining: repair.remaining });
|
|
342
|
-
}
|
|
343
|
-
// Split back. Repair keeps the current message last in every case but total
|
|
344
|
-
// collapse, where a conversation that is *only* a bare tool-result carrier
|
|
345
|
-
// has no valid opening entry and step 1 consumes it.
|
|
346
|
-
const repairedCurrent = repair.entries[repair.entries.length - 1]?.userInputMessage;
|
|
347
|
-
// Read the repaired context EXACTLY, including when repair removed it. A
|
|
348
|
-
// `?? uimc` fallback would undo the repair in the one case that matters
|
|
349
|
-
// most: stripping every orphaned tool result leaves a turn with no context
|
|
350
|
-
// at all, and falling back would put the orphans — the shape the backend
|
|
351
|
-
// rejects — straight back onto the wire.
|
|
352
|
-
let wireHistory;
|
|
353
|
-
let wireContent;
|
|
354
|
-
let wireUimc;
|
|
355
|
-
if (repairedCurrent) {
|
|
356
|
-
wireHistory = repair.entries.slice(0, -1);
|
|
357
|
-
wireContent = repairedCurrent.content;
|
|
358
|
-
wireUimc = repairedCurrent.userInputMessageContext;
|
|
359
|
-
}
|
|
360
|
-
else {
|
|
361
|
-
// Collapsed. Apply what repair would have applied to a lone carrier: drop
|
|
362
|
-
// the results that answer nothing, keep any tool catalog, and give the
|
|
363
|
-
// empty turn the neutral prompt.
|
|
364
|
-
wireHistory = [];
|
|
365
|
-
wireContent = currentContent || EMPTY_CONTENT_PLACEHOLDER;
|
|
366
|
-
wireUimc = uimc?.tools?.length ? { tools: uimc.tools } : undefined;
|
|
367
|
-
}
|
|
368
|
-
if (repair.remaining.length > 0) {
|
|
369
|
-
const structural = repair.remaining.filter((e) => isKiroToolStructureRule(e.rule));
|
|
370
|
-
if (structural.length > 0) {
|
|
371
|
-
console.warn(`[kiro-core] outbound history still violates ${structural
|
|
372
|
-
.map((e) => `${e.rule}@${e.index}`)
|
|
373
|
-
.join(", ")} after repair — Kiro may reject this request`);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
const kiroRequest = {
|
|
377
|
-
conversationState: {
|
|
378
|
-
chatTriggerType: "MANUAL",
|
|
379
|
-
agentTaskType: "vibe",
|
|
380
|
-
conversationId,
|
|
381
|
-
currentMessage: {
|
|
382
|
-
userInputMessage: {
|
|
383
|
-
content: sanitizeSurrogates(wireContent),
|
|
384
|
-
modelId: kiroModelId,
|
|
385
|
-
origin: "KIRO_CLI",
|
|
386
|
-
...(currentImages ? { images: currentImages } : {}),
|
|
387
|
-
...(wireUimc ? { userInputMessageContext: wireUimc } : {}),
|
|
388
|
-
},
|
|
389
|
-
},
|
|
390
|
-
...(wireHistory.length > 0 ? { history: wireHistory } : {}),
|
|
391
|
-
},
|
|
392
|
-
...(additionalModelRequestFields ? { additionalModelRequestFields } : {}),
|
|
126
|
+
const built = buildKiroRequest({
|
|
127
|
+
messages: request.messages,
|
|
128
|
+
model,
|
|
129
|
+
kiroModelId,
|
|
130
|
+
systemPrompt,
|
|
131
|
+
tools: request.tools,
|
|
132
|
+
conversationId,
|
|
393
133
|
profileArn,
|
|
394
|
-
|
|
395
|
-
};
|
|
134
|
+
...(additionalModelRequestFields ? { additionalModelRequestFields } : {}),
|
|
135
|
+
});
|
|
136
|
+
const kiroRequest = built.request;
|
|
396
137
|
let response;
|
|
397
138
|
// Reset per outer iteration — each 403 retry gets a fresh capacity budget.
|
|
398
139
|
let capacityRetryCount = 0;
|
|
@@ -405,10 +146,10 @@ export async function* streamKiro(request) {
|
|
|
405
146
|
capacityAttempt: capacityRetryCount,
|
|
406
147
|
// Wire values, not pre-repair ones: this line is what a reader
|
|
407
148
|
// correlates against a 400, so it must describe the bytes actually sent.
|
|
408
|
-
historyLen:
|
|
409
|
-
currentContentLen:
|
|
410
|
-
hasImages:
|
|
411
|
-
toolResultCount:
|
|
149
|
+
historyLen: built.wireHistoryLength,
|
|
150
|
+
currentContentLen: built.wireContentLength,
|
|
151
|
+
hasImages: built.hasImages,
|
|
152
|
+
toolResultCount: built.toolResultCount,
|
|
412
153
|
request: kiroRequest,
|
|
413
154
|
});
|
|
414
155
|
const responseHeaderDeadline = createResponseHeaderDeadline(signal, retryConfig.requestHeaderTimeoutMs);
|
|
@@ -547,288 +288,28 @@ export async function* streamKiro(request) {
|
|
|
547
288
|
yield { type: "start" };
|
|
548
289
|
if (!response.body)
|
|
549
290
|
throw new Error("No response body");
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
let usageEvent = null;
|
|
554
|
-
let receivedContextUsage = false;
|
|
555
|
-
const thinkingParser = thinkingEnabled ? new ThinkingTagParser(blocks) : null;
|
|
556
|
-
let nativeThinkingBlockIndex = null;
|
|
557
|
-
let nativeThinkingEnded = false;
|
|
558
|
-
const ensureNativeThinkingBlock = () => {
|
|
559
|
-
if (nativeThinkingBlockIndex === null)
|
|
560
|
-
nativeThinkingBlockIndex = blocks.openThinking();
|
|
561
|
-
return nativeThinkingBlockIndex;
|
|
562
|
-
};
|
|
563
|
-
const endNativeThinking = (signature) => {
|
|
564
|
-
if (nativeThinkingBlockIndex === null || nativeThinkingEnded)
|
|
565
|
-
return;
|
|
566
|
-
nativeThinkingEnded = true;
|
|
567
|
-
blocks.endThinking(nativeThinkingBlockIndex, signature);
|
|
568
|
-
};
|
|
569
|
-
let textBlockIndex = null;
|
|
570
|
-
let emittedToolCalls = 0;
|
|
571
|
-
let sawAnyToolCalls = false;
|
|
572
|
-
let currentToolCall = null;
|
|
573
|
-
const emitToolCall = (state) => {
|
|
574
|
-
if (!state.input.trim()) {
|
|
575
|
-
// Kiro omits the input payload when the model calls a tool with no
|
|
576
|
-
// arguments (e.g. `mcp({})`). Treat empty input as an empty object
|
|
577
|
-
// rather than skipping — these are valid zero-arg calls, not truncations.
|
|
578
|
-
state.input = "{}";
|
|
579
|
-
}
|
|
580
|
-
let args;
|
|
581
|
-
try {
|
|
582
|
-
args = JSON.parse(state.input);
|
|
583
|
-
}
|
|
584
|
-
catch (e) {
|
|
585
|
-
console.warn(`[kiro-core] Failed to parse tool input for "${state.name}" (toolUseId: ${state.toolUseId}): ${formatSafeError(e)}. Raw input (${state.input.length} chars): ${redactSensitiveText(state.input.substring(0, 200))}`);
|
|
586
|
-
return false;
|
|
587
|
-
}
|
|
588
|
-
const index = blocks.reserve();
|
|
589
|
-
pending.push({ type: "tool_call_start", index, id: state.toolUseId, name: state.name });
|
|
590
|
-
pending.push({ type: "tool_call_delta", index, id: state.toolUseId, argumentsDelta: state.input });
|
|
591
|
-
pending.push({ type: "tool_call_end", index, id: state.toolUseId, name: state.name, arguments: args });
|
|
592
|
-
return true;
|
|
593
|
-
};
|
|
594
|
-
const flushToolCall = () => {
|
|
595
|
-
if (!currentToolCall)
|
|
596
|
-
return;
|
|
597
|
-
if (emitToolCall(currentToolCall))
|
|
598
|
-
emittedToolCalls++;
|
|
599
|
-
currentToolCall = null;
|
|
600
|
-
};
|
|
601
|
-
const IDLE_TIMEOUT = 300_000;
|
|
602
|
-
let idleTimer = null;
|
|
603
|
-
let idleCancelled = false;
|
|
604
|
-
const resetIdle = () => {
|
|
605
|
-
if (idleTimer)
|
|
606
|
-
clearTimeout(idleTimer);
|
|
607
|
-
idleTimer = setTimeout(() => {
|
|
608
|
-
idleCancelled = true;
|
|
609
|
-
void bodyReader.cancel().catch(() => { });
|
|
610
|
-
}, IDLE_TIMEOUT);
|
|
611
|
-
};
|
|
612
|
-
let gotFirstToken = false;
|
|
613
|
-
let firstTokenTimedOut = false;
|
|
614
|
-
let streamError = null;
|
|
615
|
-
const FIRST_TOKEN_SENTINEL = Symbol("firstTokenTimeout");
|
|
616
|
-
// Smithy's marshaller handles chunk reassembly, CRC validation, protocol
|
|
617
|
-
// error/exception detection, and payload deserialization.
|
|
618
|
-
const bodyIterable = {
|
|
619
|
-
async *[Symbol.asyncIterator]() {
|
|
620
|
-
try {
|
|
621
|
-
while (true) {
|
|
622
|
-
const { done, value } = await bodyReader.read();
|
|
623
|
-
if (done)
|
|
624
|
-
return;
|
|
625
|
-
yield value;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
finally {
|
|
629
|
-
bodyReader.releaseLock();
|
|
630
|
-
}
|
|
631
|
-
},
|
|
632
|
-
};
|
|
633
|
-
const utf8Decoder = new TextDecoder();
|
|
634
|
-
const eventStream = eventStreamMarshaller.deserialize(bodyIterable, async (event) => {
|
|
635
|
-
const entry = Object.entries(event)[0];
|
|
636
|
-
if (!entry)
|
|
637
|
-
throw new Error("Received an empty event stream message");
|
|
638
|
-
const [key, msg] = entry;
|
|
639
|
-
const parsed = JSON.parse(utf8Decoder.decode(msg.body));
|
|
640
|
-
return { [key]: parsed };
|
|
291
|
+
assembler.beginAttempt();
|
|
292
|
+
const { frames, outcome } = readKiroEventStream(response.body, {
|
|
293
|
+
firstTokenTimeoutMs: model.firstTokenTimeout ?? firstTokenTimeoutForModel(model.id),
|
|
641
294
|
});
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
try {
|
|
646
|
-
if (!gotFirstToken) {
|
|
647
|
-
const readPromise = iterator.next();
|
|
648
|
-
const result = await Promise.race([
|
|
649
|
-
readPromise,
|
|
650
|
-
new Promise((resolve) => setTimeout(() => resolve(FIRST_TOKEN_SENTINEL), model.firstTokenTimeout ?? firstTokenTimeoutForModel(model.id))),
|
|
651
|
-
]);
|
|
652
|
-
if (result === FIRST_TOKEN_SENTINEL) {
|
|
653
|
-
readPromise.catch(() => { }); // suppress dangling rejection
|
|
654
|
-
void bodyReader.cancel().catch(() => { });
|
|
655
|
-
firstTokenTimedOut = true;
|
|
656
|
-
break;
|
|
657
|
-
}
|
|
658
|
-
iterResult = result;
|
|
659
|
-
gotFirstToken = true;
|
|
660
|
-
resetIdle();
|
|
661
|
-
}
|
|
662
|
-
else {
|
|
663
|
-
iterResult = await iterator.next();
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
catch (e) {
|
|
667
|
-
// Smithy throws on `:message-type` error/exception headers.
|
|
668
|
-
streamError =
|
|
669
|
-
e instanceof Error
|
|
670
|
-
? e.message
|
|
671
|
-
: (typeof e === "object" && e !== null ? JSON.stringify(e) : String(e)) || "Unknown stream error";
|
|
672
|
-
break;
|
|
673
|
-
}
|
|
674
|
-
const { done, value } = iterResult;
|
|
675
|
-
if (done)
|
|
676
|
-
break;
|
|
677
|
-
resetIdle();
|
|
678
|
-
const eventPayload = Object.values(value)[0];
|
|
679
|
-
const event = parseKiroEvent(eventPayload);
|
|
680
|
-
if (!event)
|
|
681
|
-
continue;
|
|
682
|
-
if (debugEnabled())
|
|
683
|
-
debugLog("stream.events", [event]);
|
|
684
|
-
switch (event.type) {
|
|
685
|
-
case "contextUsage": {
|
|
686
|
-
const pct = event.data.contextUsagePercentage;
|
|
687
|
-
usage.input = Math.round((pct / 100) * model.contextWindow);
|
|
688
|
-
usage.contextPercent = pct;
|
|
689
|
-
receivedContextUsage = true;
|
|
690
|
-
break;
|
|
691
|
-
}
|
|
692
|
-
case "thinkingText": {
|
|
693
|
-
if (!thinkingEnabled)
|
|
694
|
-
break;
|
|
695
|
-
blocks.appendThinking(ensureNativeThinkingBlock(), event.data);
|
|
696
|
-
totalContent += event.data;
|
|
697
|
-
break;
|
|
698
|
-
}
|
|
699
|
-
case "thinkingSignature": {
|
|
700
|
-
if (!thinkingEnabled)
|
|
701
|
-
break;
|
|
702
|
-
ensureNativeThinkingBlock();
|
|
703
|
-
endNativeThinking(event.data);
|
|
704
|
-
break;
|
|
705
|
-
}
|
|
706
|
-
case "content": {
|
|
707
|
-
endNativeThinking();
|
|
708
|
-
if (event.data === lastContentData)
|
|
709
|
-
continue;
|
|
710
|
-
lastContentData = event.data;
|
|
711
|
-
totalContent += event.data;
|
|
712
|
-
if (thinkingParser) {
|
|
713
|
-
thinkingParser.processChunk(event.data);
|
|
714
|
-
}
|
|
715
|
-
else {
|
|
716
|
-
if (textBlockIndex === null)
|
|
717
|
-
textBlockIndex = blocks.openText();
|
|
718
|
-
blocks.appendText(textBlockIndex, event.data);
|
|
719
|
-
}
|
|
720
|
-
break;
|
|
721
|
-
}
|
|
722
|
-
case "toolUse": {
|
|
723
|
-
const tc = event.data;
|
|
724
|
-
sawAnyToolCalls = true;
|
|
725
|
-
if (!currentToolCall || currentToolCall.toolUseId !== tc.toolUseId) {
|
|
726
|
-
flushToolCall();
|
|
727
|
-
currentToolCall = { toolUseId: tc.toolUseId, name: tc.name, input: "" };
|
|
728
|
-
}
|
|
729
|
-
currentToolCall.input += tc.input || "";
|
|
730
|
-
if (tc.input)
|
|
731
|
-
totalContent += tc.input;
|
|
732
|
-
if (tc.stop)
|
|
733
|
-
flushToolCall();
|
|
734
|
-
break;
|
|
735
|
-
}
|
|
736
|
-
case "toolUseInput": {
|
|
737
|
-
if (currentToolCall)
|
|
738
|
-
currentToolCall.input += event.data.input || "";
|
|
739
|
-
if (event.data.input)
|
|
740
|
-
totalContent += event.data.input;
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
|
-
case "toolUseStop": {
|
|
744
|
-
if (event.data.stop)
|
|
745
|
-
flushToolCall();
|
|
746
|
-
break;
|
|
747
|
-
}
|
|
748
|
-
case "usage": {
|
|
749
|
-
usageEvent = event.data;
|
|
750
|
-
break;
|
|
751
|
-
}
|
|
752
|
-
case "error": {
|
|
753
|
-
streamError = event.data.message ? `${event.data.error}: ${event.data.message}` : event.data.error;
|
|
754
|
-
void bodyReader.cancel().catch(() => { });
|
|
755
|
-
break;
|
|
756
|
-
}
|
|
757
|
-
// followupPrompt events are intentionally ignored
|
|
758
|
-
}
|
|
759
|
-
yield* drain(pending);
|
|
760
|
-
if (streamError)
|
|
761
|
-
break;
|
|
295
|
+
for await (const frame of frames) {
|
|
296
|
+
assembler.handle(frame);
|
|
297
|
+
yield* assembler.takeEvents();
|
|
762
298
|
}
|
|
763
|
-
yield*
|
|
764
|
-
if (
|
|
765
|
-
clearTimeout(idleTimer);
|
|
766
|
-
if (firstTokenTimedOut || idleCancelled || streamError) {
|
|
299
|
+
yield* assembler.takeEvents();
|
|
300
|
+
if (outcome.firstTokenTimedOut || outcome.idleTimedOut || outcome.error) {
|
|
767
301
|
// Timed out or received an error mid-stream: retry with backoff.
|
|
768
302
|
if (retryCount < maxRetries) {
|
|
769
303
|
retryCount++;
|
|
770
304
|
await abortableDelay(exponentialBackoff(retryCount - 1, 1000, MAX_RETRY_DELAY), signal);
|
|
771
305
|
continue;
|
|
772
306
|
}
|
|
773
|
-
if (
|
|
774
|
-
throw new Error(`Kiro API stream error after max retries: ${
|
|
775
|
-
throw new Error(`Kiro API error: ${firstTokenTimedOut ? "first token" : "idle"} timeout after max retries`);
|
|
776
|
-
}
|
|
777
|
-
if (currentToolCall && emitToolCall(currentToolCall))
|
|
778
|
-
emittedToolCalls++;
|
|
779
|
-
currentToolCall = null;
|
|
780
|
-
endNativeThinking();
|
|
781
|
-
if (thinkingParser) {
|
|
782
|
-
thinkingParser.finalize();
|
|
783
|
-
textBlockIndex = thinkingParser.getTextBlockIndex();
|
|
784
|
-
}
|
|
785
|
-
yield* drain(pending);
|
|
786
|
-
// Fallback: extract text-dialect tool calls from content if no native tool
|
|
787
|
-
// calls arrived. Two dialects are recovered at this seam:
|
|
788
|
-
// 1. Kiro's own `[Called name with args: {...}]` bracket form.
|
|
789
|
-
// 2. Anthropic's `<invoke name="..."><parameter .../></invoke>` XML form,
|
|
790
|
-
// which opus-class models emit as plain text at high context.
|
|
791
|
-
// Without this, the turn ends `stop` with zero tool calls — the agent loop
|
|
792
|
-
// sees a finished answer and an unattended session stalls with no error
|
|
793
|
-
// recorded anywhere.
|
|
794
|
-
//
|
|
795
|
-
// Models that emit native tool-use events opt out via
|
|
796
|
-
// `recoverTextToolCalls: false`. For them this pass has nothing to rescue
|
|
797
|
-
// and everything to break: prose that merely *quotes* the syntax — a model
|
|
798
|
-
// explaining how a tool is called — would be lifted into a real call the
|
|
799
|
-
// model never made. Absent means recover, so a model the catalog says
|
|
800
|
-
// nothing about keeps the fallback.
|
|
801
|
-
if (model.recoverTextToolCalls !== false && !sawAnyToolCalls && textBlockIndex !== null) {
|
|
802
|
-
let text = blocks.getText(textBlockIndex);
|
|
803
|
-
const recovered = [];
|
|
804
|
-
const bracketResult = parseBracketToolCalls(text);
|
|
805
|
-
if (bracketResult.toolCalls.length > 0) {
|
|
806
|
-
text = bracketResult.cleanedText;
|
|
807
|
-
recovered.push(...bracketResult.toolCalls);
|
|
808
|
-
}
|
|
809
|
-
const invokeResult = parseInvokeToolCalls(text);
|
|
810
|
-
if (invokeResult.toolCalls.length > 0) {
|
|
811
|
-
text = invokeResult.cleanedText;
|
|
812
|
-
recovered.push(...invokeResult.toolCalls);
|
|
813
|
-
}
|
|
814
|
-
if (recovered.length > 0) {
|
|
815
|
-
blocks.setText(textBlockIndex, text);
|
|
816
|
-
sawAnyToolCalls = true;
|
|
817
|
-
for (const call of recovered) {
|
|
818
|
-
if (emitToolCall({ toolUseId: call.toolUseId, name: call.name, input: JSON.stringify(call.arguments) })) {
|
|
819
|
-
emittedToolCalls++;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
// Strip echo noise: when tool calls are present and the text content is just
|
|
825
|
-
// "." or a similar short echo from history padding, remove it. This prevents
|
|
826
|
-
// the echo from accumulating in conversation history and reinforcing the
|
|
827
|
-
// pattern in future turns.
|
|
828
|
-
if (emittedToolCalls > 0 && textBlockIndex !== null) {
|
|
829
|
-
if (/^\s*(\.+|continue)\s*$/i.test(blocks.getText(textBlockIndex)))
|
|
830
|
-
blocks.setText(textBlockIndex, "");
|
|
307
|
+
if (outcome.error)
|
|
308
|
+
throw new Error(`Kiro API stream error after max retries: ${outcome.error}`);
|
|
309
|
+
throw new Error(`Kiro API error: ${outcome.firstTokenTimedOut ? "first token" : "idle"} timeout after max retries`);
|
|
831
310
|
}
|
|
311
|
+
const summary = assembler.endTurn();
|
|
312
|
+
yield* assembler.takeEvents();
|
|
832
313
|
// Detect degenerate responses: the API returned 200 but produced no usable
|
|
833
314
|
// content at all — no text and no tool calls. This happens when the stream
|
|
834
315
|
// is truncated early or only a contextUsage event arrives.
|
|
@@ -839,67 +320,37 @@ export async function* streamKiro(request) {
|
|
|
839
320
|
// When tool calls *were* present but all got dropped (empty/unparseable
|
|
840
321
|
// input), don't retry — the API did respond, it just sent malformed tool
|
|
841
322
|
// calls. Retrying would likely produce the same result.
|
|
842
|
-
|
|
843
|
-
const hasText = responseText.length > 0;
|
|
844
|
-
const isEchoLoop = hasText && !sawAnyToolCalls && /^\s*(continue|\.+)\s*$/i.test(responseText);
|
|
845
|
-
if ((!hasText && !sawAnyToolCalls) || isEchoLoop) {
|
|
323
|
+
if (summary.isEmpty || summary.isEchoLoop) {
|
|
846
324
|
// Retrying an echo loop means unsaying text already delivered, which only
|
|
847
325
|
// a host that can discard emitted blocks may do. Elsewhere, go straight to
|
|
848
326
|
// the terminal behaviour: strip the echo so the agent loop does not read
|
|
849
327
|
// "Continue" as a continuation signal.
|
|
850
|
-
const mayRetry = retryCount < maxRetries && (!isEchoLoop || request.canDiscardEmittedBlocks === true);
|
|
328
|
+
const mayRetry = retryCount < maxRetries && (!summary.isEchoLoop || request.canDiscardEmittedBlocks === true);
|
|
851
329
|
if (mayRetry) {
|
|
852
330
|
retryCount++;
|
|
853
|
-
console.warn(`[kiro-core] ${isEchoLoop ? 'Echo loop detected (model responded with just "Continue")' : "Empty response (no text, no tool calls)"} — retrying (${retryCount}/${maxRetries})`);
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
yield* drain(pending);
|
|
331
|
+
console.warn(`[kiro-core] ${summary.isEchoLoop ? 'Echo loop detected (model responded with just "Continue")' : "Empty response (no text, no tool calls)"} — retrying (${retryCount}/${maxRetries})`);
|
|
332
|
+
assembler.discard();
|
|
333
|
+
yield* assembler.takeEvents();
|
|
857
334
|
await abortableDelay(exponentialBackoff(retryCount - 1, 1000, MAX_RETRY_DELAY), signal);
|
|
858
335
|
continue;
|
|
859
336
|
}
|
|
860
|
-
if (isEchoLoop
|
|
861
|
-
|
|
337
|
+
if (summary.isEchoLoop) {
|
|
338
|
+
assembler.stripEcho();
|
|
862
339
|
console.warn(`[kiro-core] Echo loop — stripping "Continue" response`);
|
|
863
340
|
}
|
|
864
|
-
else
|
|
865
|
-
|
|
341
|
+
else {
|
|
342
|
+
// The stop reason is still decided below, and an empty turn that never
|
|
343
|
+
// carried a contextUsage frame is reported as `length` rather than
|
|
344
|
+
// `stop` — say so, instead of promising a normal stop this branch does
|
|
345
|
+
// not actually guarantee.
|
|
346
|
+
console.warn(`[kiro-core] Empty response after ${maxRetries} retries — giving up on this turn`);
|
|
866
347
|
}
|
|
867
348
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
// Kiro does not reliably emit per-response output token counts. When the
|
|
871
|
-
// `usage` event is missing or reports only `inputTokens`, fall back to a
|
|
872
|
-
// tiktoken estimate over everything the assistant emitted — text plus
|
|
873
|
-
// tool-call input JSON. Otherwise tool-call-only turns report 0 output
|
|
874
|
-
// tokens and break consumers that watch it.
|
|
875
|
-
if (usageEvent?.inputTokens !== undefined)
|
|
876
|
-
usage.input = usageEvent.inputTokens;
|
|
877
|
-
usage.output = usageEvent?.outputTokens ?? countTokens(totalContent);
|
|
878
|
-
usage.totalTokens = usage.input + usage.output;
|
|
879
|
-
usage.cost = calculateKiroCost(model.cost, usage);
|
|
880
|
-
// Use `emittedToolCalls`, not the count seen on the wire: a turn whose calls
|
|
881
|
-
// were all dropped for unparseable input must not report `toolUse`, because
|
|
882
|
-
// an empty turn with a tool-use stop stalls an agent loop waiting for
|
|
883
|
-
// results that will never arrive.
|
|
884
|
-
const stopReason = !receivedContextUsage && emittedToolCalls === 0 ? "length" : emittedToolCalls > 0 ? "toolUse" : "stop";
|
|
885
|
-
yield* drain(pending);
|
|
349
|
+
const { stopReason, usage } = assembler.complete();
|
|
350
|
+
yield* assembler.takeEvents();
|
|
886
351
|
yield { type: "usage", usage };
|
|
887
352
|
yield { type: "done", stopReason };
|
|
888
|
-
debugLog("response.done", {
|
|
889
|
-
stopReason,
|
|
890
|
-
emittedToolCalls,
|
|
891
|
-
sawAnyToolCalls,
|
|
892
|
-
textLen: responseText.length,
|
|
893
|
-
usage,
|
|
894
|
-
});
|
|
895
353
|
return;
|
|
896
354
|
}
|
|
897
355
|
}
|
|
898
|
-
function* drain(pending) {
|
|
899
|
-
while (pending.length > 0) {
|
|
900
|
-
const event = pending.shift();
|
|
901
|
-
if (event)
|
|
902
|
-
yield event;
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
356
|
//# sourceMappingURL=stream.js.map
|