ns-kiro-core 0.1.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.
Files changed (78) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +51 -0
  3. package/dist/blocks.d.ts +32 -0
  4. package/dist/blocks.js +83 -0
  5. package/dist/blocks.js.map +1 -0
  6. package/dist/bracket-tool-parser.d.ts +11 -0
  7. package/dist/bracket-tool-parser.js +78 -0
  8. package/dist/bracket-tool-parser.js.map +1 -0
  9. package/dist/cost.d.ts +3 -0
  10. package/dist/cost.js +10 -0
  11. package/dist/cost.js.map +1 -0
  12. package/dist/debug.d.ts +6 -0
  13. package/dist/debug.js +63 -0
  14. package/dist/debug.js.map +1 -0
  15. package/dist/effort.d.ts +36 -0
  16. package/dist/effort.js +138 -0
  17. package/dist/effort.js.map +1 -0
  18. package/dist/endpoints.d.ts +8 -0
  19. package/dist/endpoints.js +43 -0
  20. package/dist/endpoints.js.map +1 -0
  21. package/dist/event-parser.d.ts +49 -0
  22. package/dist/event-parser.js +56 -0
  23. package/dist/event-parser.js.map +1 -0
  24. package/dist/history-validator.d.ts +91 -0
  25. package/dist/history-validator.js +370 -0
  26. package/dist/history-validator.js.map +1 -0
  27. package/dist/history.d.ts +21 -0
  28. package/dist/history.js +130 -0
  29. package/dist/history.js.map +1 -0
  30. package/dist/index.d.ts +22 -0
  31. package/dist/index.js +28 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/invoke-tool-parser.d.ts +16 -0
  34. package/dist/invoke-tool-parser.js +152 -0
  35. package/dist/invoke-tool-parser.js.map +1 -0
  36. package/dist/kiro-cli.d.ts +31 -0
  37. package/dist/kiro-cli.js +308 -0
  38. package/dist/kiro-cli.js.map +1 -0
  39. package/dist/kiro-ide.d.ts +12 -0
  40. package/dist/kiro-ide.js +74 -0
  41. package/dist/kiro-ide.js.map +1 -0
  42. package/dist/management.d.ts +34 -0
  43. package/dist/management.js +226 -0
  44. package/dist/management.js.map +1 -0
  45. package/dist/models.d.ts +58 -0
  46. package/dist/models.js +462 -0
  47. package/dist/models.js.map +1 -0
  48. package/dist/oauth.d.ts +50 -0
  49. package/dist/oauth.js +295 -0
  50. package/dist/oauth.js.map +1 -0
  51. package/dist/retry.d.ts +74 -0
  52. package/dist/retry.js +156 -0
  53. package/dist/retry.js.map +1 -0
  54. package/dist/stream.d.ts +36 -0
  55. package/dist/stream.js +898 -0
  56. package/dist/stream.js.map +1 -0
  57. package/dist/thinking-parser.d.ts +21 -0
  58. package/dist/thinking-parser.js +150 -0
  59. package/dist/thinking-parser.js.map +1 -0
  60. package/dist/token-type.d.ts +18 -0
  61. package/dist/token-type.js +45 -0
  62. package/dist/token-type.js.map +1 -0
  63. package/dist/tokenizer.d.ts +1 -0
  64. package/dist/tokenizer.js +19 -0
  65. package/dist/tokenizer.js.map +1 -0
  66. package/dist/transform.d.ts +111 -0
  67. package/dist/transform.js +268 -0
  68. package/dist/transform.js.map +1 -0
  69. package/dist/truncation.d.ts +3 -0
  70. package/dist/truncation.js +12 -0
  71. package/dist/truncation.js.map +1 -0
  72. package/dist/types.d.ts +159 -0
  73. package/dist/types.js +4 -0
  74. package/dist/types.js.map +1 -0
  75. package/dist/usage.d.ts +86 -0
  76. package/dist/usage.js +85 -0
  77. package/dist/usage.js.map +1 -0
  78. package/package.json +31 -0
package/dist/stream.js ADDED
@@ -0,0 +1,898 @@
1
+ // ABOUTME: Core streaming integration for Kiro API requests and responses.
2
+ // ABOUTME: Handles request building, retry logic, event parsing, and token counting.
3
+ import { appendFile, mkdir } from "node:fs/promises";
4
+ import { homedir } from "node:os";
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";
12
+ 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
+ import { getKiroCliCredentials, getKiroCliCredentialsAllowExpired, refreshViaKiroCli } from "./kiro-cli.js";
18
+ import { invalidateKiroProfileArn, KiroManagementHttpError, resetKiroProfileArnCache, resolveKiroProfileArn, } from "./management.js";
19
+ import { isCacheStale, resolveKiroModel, updateKiroModelsCache } from "./models.js";
20
+ import { kiroAuthHeaders } from "./oauth.js";
21
+ 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
+ import { kiroTokenTypeHeaders } from "./token-type.js";
24
+ import { countTokens } from "./tokenizer.js";
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
+ }
92
+ let skipProfileResolutionForTests = false;
93
+ const TEST_PROFILE_ARN = "arn:aws:codewhisperer:us-east-1:000000000000:profile/test";
94
+ /** Reset profile resolution state — exported for stream tests. */
95
+ export function resetProfileArnCache(resolved = false) {
96
+ resetKiroProfileArnCache();
97
+ skipProfileResolutionForTests = resolved;
98
+ }
99
+ /**
100
+ * Stream one Kiro response as neutral events.
101
+ *
102
+ * Retries live inside this generator: transport timeouts, capacity pressure,
103
+ * request-rate windows, 403 credential rotation, and degenerate 200s all resend
104
+ * without the caller seeing a partial turn — except where the response had
105
+ * already been streamed out, which {@link KiroStreamRequest.canDiscardEmittedBlocks}
106
+ * governs.
107
+ */
108
+ export async function* streamKiro(request) {
109
+ 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
+ const initialAccessToken = request.accessToken;
119
+ if (!initialAccessToken) {
120
+ throw new Error("Kiro credentials not set. Run `kiro-cli login`, or set KIRO_API_KEY.");
121
+ }
122
+ let accessToken = initialAccessToken;
123
+ const region = model.region ?? "us-east-1";
124
+ const endpoint = new URL("generateAssistantResponse", getKiroEndpoints(region).runtime).toString();
125
+ let managementAuth = { accessToken, region };
126
+ const cliCreds = getKiroCliCredentials() ?? getKiroCliCredentialsAllowExpired();
127
+ const cliProfileArn = cliCreds?.access === accessToken ? cliCreds.profileArn : undefined;
128
+ const initialProfileArn = model.profileArn || request.profileArn || cliProfileArn;
129
+ let profileArn;
130
+ try {
131
+ profileArn =
132
+ initialProfileArn ||
133
+ (skipProfileResolutionForTests ? TEST_PROFILE_ARN : await resolveKiroProfileArn(managementAuth));
134
+ }
135
+ catch (error) {
136
+ if (!(error instanceof KiroManagementHttpError) || error.status !== 403)
137
+ throw error;
138
+ // The host may have captured an access token before kiro-cli rotated it.
139
+ // Re-read the shared store first, then force a refresh only when it still
140
+ // contains the rejected token. Profile discovery must succeed before the
141
+ // runtime request can be constructed.
142
+ const storedCreds = getKiroCliCredentials();
143
+ const freshCreds = storedCreds?.access && storedCreds.access !== accessToken ? storedCreds : refreshViaKiroCli();
144
+ if (!freshCreds?.access)
145
+ throw error;
146
+ accessToken = freshCreds.access;
147
+ managementAuth = { accessToken, region };
148
+ profileArn =
149
+ freshCreds.profileArn ||
150
+ (skipProfileResolutionForTests ? TEST_PROFILE_ARN : await resolveKiroProfileArn(managementAuth));
151
+ }
152
+ // Refresh the catalog in the background when it has gone stale.
153
+ if (!process.env.VITEST && isCacheStale(region)) {
154
+ updateKiroModelsCache(accessToken, region, profileArn).catch((error) => {
155
+ console.warn(`[kiro-core] Failed to refresh Kiro model catalog in ${region}: ${formatSafeError(error)}`);
156
+ });
157
+ }
158
+ const kiroModelId = resolveKiroModel(model.id, model.kiroModelId);
159
+ const effort = clampKiroEffort(model, request.effort);
160
+ const effortConfig = getKiroEffortConfig(model.additionalModelRequestFieldsSchema, kiroModelId);
161
+ const additionalModelRequestFields = buildKiroAdditionalModelRequestFields(model, kiroModelId, request.effort);
162
+ const thinkingEnabled = !!effort || model.reasoning;
163
+ debugLog("request.init", {
164
+ endpoint,
165
+ model: model.id,
166
+ kiroModelId,
167
+ contextWindow: model.contextWindow,
168
+ thinkingEnabled,
169
+ reasoning: effort,
170
+ messageCount: request.messages.length,
171
+ toolCount: request.tools?.length ?? 0,
172
+ hasSystemPrompt: !!request.systemPrompt,
173
+ profileArn,
174
+ sessionId: request.sessionId,
175
+ });
176
+ let systemPrompt = request.systemPrompt ?? "";
177
+ // Kiro's runtime endpoint honors structured effort but only exposes Claude's
178
+ // user-visible thinking stream when the legacy thinking markers are also
179
+ // present. Keep both controls: structured fields select effort, while these
180
+ // markers preserve the <thinking> content consumed by ThinkingTagParser.
181
+ if (thinkingEnabled && effortConfig?.field !== "reasoning") {
182
+ const budget = effort === "xhigh" || effort === "max" ? 50000 : effort === "high" ? 30000 : effort === "medium" ? 20000 : 10000;
183
+ systemPrompt = `<thinking_mode>enabled</thinking_mode><max_thinking_length>${budget}</max_thinking_length>${systemPrompt ? `\n${systemPrompt}` : ""}`;
184
+ }
185
+ let retryCount = 0;
186
+ const maxRetries = 3;
187
+ const conversationId = request.sessionId ?? crypto.randomUUID();
188
+ requestLoop: while (retryCount <= maxRetries) {
189
+ if (signal?.aborted)
190
+ throw signal.reason;
191
+ // Relocate a tool result that arrived behind a later assistant turn than
192
+ // the one that called it, before anything positional runs. Interleaved
193
+ // concurrent tool executions produce that shape, and `sanitizeHistory`
194
+ // pairs POSITIONALLY, so without this pass the displaced result's issuing
195
+ // assistant is dropped and the real tool output is discarded.
196
+ const normalized = relocateDisplacedToolResults(normalizeMessages(request.messages));
197
+ const { history: rawHistory, systemPrepended, currentMsgStartIdx, } = buildHistory(normalized, kiroModelId, systemPrompt);
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 } : {}),
393
+ profileArn,
394
+ agentMode: "vibe",
395
+ };
396
+ let response;
397
+ // Reset per outer iteration — each 403 retry gets a fresh capacity budget.
398
+ let capacityRetryCount = 0;
399
+ // Inner loop: retry capacity errors without consuming outer retry budget.
400
+ while (true) {
401
+ const mid = crypto.randomUUID().replace(/-/g, "");
402
+ const ua = `aws-sdk-rust/1.0.0 ua/2.1 os/other lang/rust api/codewhispererstreaming#1.28.3 m/E app/AmazonQ-For-CLI md/appVersion-1.28.3-${mid}`;
403
+ debugLog("request.send", {
404
+ attempt: retryCount,
405
+ capacityAttempt: capacityRetryCount,
406
+ // Wire values, not pre-repair ones: this line is what a reader
407
+ // correlates against a 400, so it must describe the bytes actually sent.
408
+ historyLen: wireHistory.length,
409
+ currentContentLen: wireContent.length,
410
+ hasImages: !!currentImages,
411
+ toolResultCount: wireUimc?.toolResults?.length ?? 0,
412
+ request: kiroRequest,
413
+ });
414
+ const responseHeaderDeadline = createResponseHeaderDeadline(signal, retryConfig.requestHeaderTimeoutMs);
415
+ let responseHeadersTimedOut = false;
416
+ try {
417
+ response = await fetch(endpoint, {
418
+ method: "POST",
419
+ headers: {
420
+ "Content-Type": "application/json",
421
+ Accept: "application/vnd.amazon.eventstream",
422
+ ...kiroAuthHeaders(accessToken),
423
+ ...kiroTokenTypeHeaders(accessToken),
424
+ "x-amzn-codewhisperer-optout": "true",
425
+ "amz-sdk-invocation-id": crypto.randomUUID(),
426
+ "amz-sdk-request": "attempt=1; max=1",
427
+ "x-amzn-kiro-agent-mode": "vibe",
428
+ "x-amz-user-agent": ua,
429
+ "user-agent": ua,
430
+ },
431
+ body: JSON.stringify(kiroRequest),
432
+ signal: responseHeaderDeadline.signal,
433
+ });
434
+ }
435
+ catch (error) {
436
+ if (!responseHeaderDeadline.didTimeout() || signal?.aborted)
437
+ throw error;
438
+ responseHeadersTimedOut = true;
439
+ }
440
+ finally {
441
+ responseHeaderDeadline.cleanup();
442
+ }
443
+ if (responseHeadersTimedOut) {
444
+ if (retryCount >= maxRetries)
445
+ throw new Error("Kiro API error: response headers timeout after max retries");
446
+ retryCount++;
447
+ await abortableDelay(exponentialBackoff(retryCount - 1, 1000, MAX_RETRY_DELAY), signal);
448
+ continue requestLoop;
449
+ }
450
+ if (!response.ok) {
451
+ let errText = "";
452
+ try {
453
+ errText = redactSensitiveText(await response.text());
454
+ }
455
+ catch {
456
+ errText = "";
457
+ }
458
+ const safeStatusText = redactSensitiveText(response.statusText);
459
+ const reasonCode = extractKiroReason(errText);
460
+ const isRequestRateExceeded = response.status === 429 &&
461
+ reasonCode === KIRO_REASON_CODES.USER_REQUEST_RATE_EXCEEDED &&
462
+ !isNonRetryableBodyError(errText) &&
463
+ !isCapacityError(errText);
464
+ debugLog("response.error", {
465
+ status: response.status,
466
+ statusText: safeStatusText,
467
+ ...(isRequestRateExceeded ? { reasonCode } : { body: errText }),
468
+ });
469
+ // Retry transient capacity errors with longer backoff.
470
+ if (isCapacityError(errText) && capacityRetryCount < capacityRetryConfig.maxRetries) {
471
+ capacityRetryCount++;
472
+ const delayMs = exponentialBackoff(capacityRetryCount - 1, capacityRetryConfig.baseDelayMs, 30_000);
473
+ logCapacityEvent(`INSUFFICIENT_MODEL_CAPACITY — retrying in ${delayMs}ms (${capacityRetryCount}/${capacityRetryConfig.maxRetries})`);
474
+ await abortableDelay(delayMs, signal);
475
+ continue;
476
+ }
477
+ if (isCapacityError(errText)) {
478
+ logCapacityEvent(`INSUFFICIENT_MODEL_CAPACITY — exhausted ${capacityRetryConfig.maxRetries} retries, giving up`);
479
+ }
480
+ if (isRequestRateExceeded) {
481
+ if (retryCount >= maxRetries) {
482
+ throw new Error(`Kiro API error: request window retry budget exhausted (${KIRO_REASON_CODES.USER_REQUEST_RATE_EXCEEDED})`);
483
+ }
484
+ retryCount++;
485
+ const retryDelay = resolveRequestRateRetryDelay(response.headers);
486
+ debugLog("request.rateWindowRetry", {
487
+ attempt: retryCount,
488
+ maxRetries,
489
+ delayMs: retryDelay.delayMs,
490
+ advertisedDelayMs: retryDelay.advertisedDelayMs,
491
+ capped: retryDelay.capped,
492
+ reasonCode,
493
+ });
494
+ await abortableDelay(retryDelay.delayMs, signal);
495
+ continue requestLoop;
496
+ }
497
+ if (response.status === 403 && !isCapacityError(errText) && retryCount < maxRetries) {
498
+ retryCount++;
499
+ // Re-read the shared store first in case another process already
500
+ // rotated the token. If it still contains the rejected token, force
501
+ // kiro-cli to refresh before retrying runtime.
502
+ invalidateKiroProfileArn(managementAuth);
503
+ const rejectedAccessToken = accessToken;
504
+ const rejectedProfileArn = profileArn;
505
+ const storedCreds = getKiroCliCredentials();
506
+ const rejectedCliCreds = storedCreds?.access === rejectedAccessToken
507
+ ? storedCreds
508
+ : cliCreds?.access === rejectedAccessToken
509
+ ? cliCreds
510
+ : undefined;
511
+ const freshCreds = storedCreds?.access && storedCreds.access !== rejectedAccessToken ? storedCreds : refreshViaKiroCli();
512
+ if (freshCreds?.access)
513
+ accessToken = freshCreds.access;
514
+ managementAuth = { accessToken, region };
515
+ // Social profiles may not be discoverable through management. Carry
516
+ // the profile used by the rejected request only across a confirmed
517
+ // desktop-to-desktop credential replacement.
518
+ const inheritedDesktopProfileArn = rejectedCliCreds?.authMethod === "desktop" && freshCreds?.authMethod === "desktop"
519
+ ? rejectedProfileArn
520
+ : undefined;
521
+ profileArn =
522
+ freshCreds?.profileArn ||
523
+ inheritedDesktopProfileArn ||
524
+ (skipProfileResolutionForTests ? TEST_PROFILE_ARN : await resolveKiroProfileArn(managementAuth));
525
+ await abortableDelay(exponentialBackoff(retryCount - 1, 500, MAX_RETRY_DELAY), signal);
526
+ break; // break inner loop, continue outer loop
527
+ }
528
+ // Known quota/capacity body markers must not be re-read by a host's own
529
+ // outer auto-retry as a generic retryable 429.
530
+ if (isNonRetryableBodyError(errText) || isCapacityError(errText)) {
531
+ throw new Error(`Kiro API error: ${errText || safeStatusText}`);
532
+ }
533
+ // Phrase overflow so a host's context-overflow detector recognizes it.
534
+ if (isTooBigError(response.status, errText)) {
535
+ throw new Error(`Kiro API error: context_length_exceeded (${response.status} ${errText})`);
536
+ }
537
+ throw new Error(`Kiro API error: ${response.status} ${safeStatusText} ${errText}`);
538
+ }
539
+ break; // success, break inner loop
540
+ }
541
+ if (capacityRetryCount > 0 && response.ok) {
542
+ logCapacityEvent(`INSUFFICIENT_MODEL_CAPACITY — succeeded after ${capacityRetryCount} retries`);
543
+ }
544
+ // 403 retry: continue outer loop
545
+ if (!response.ok)
546
+ continue;
547
+ yield { type: "start" };
548
+ if (!response.body)
549
+ throw new Error("No response body");
550
+ const bodyReader = response.body.getReader();
551
+ let totalContent = "";
552
+ let lastContentData = "";
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 };
641
+ });
642
+ const iterator = eventStream[Symbol.asyncIterator]();
643
+ while (true) {
644
+ let iterResult;
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;
762
+ }
763
+ yield* drain(pending);
764
+ if (idleTimer)
765
+ clearTimeout(idleTimer);
766
+ if (firstTokenTimedOut || idleCancelled || streamError) {
767
+ // Timed out or received an error mid-stream: retry with backoff.
768
+ if (retryCount < maxRetries) {
769
+ retryCount++;
770
+ await abortableDelay(exponentialBackoff(retryCount - 1, 1000, MAX_RETRY_DELAY), signal);
771
+ continue;
772
+ }
773
+ if (streamError)
774
+ throw new Error(`Kiro API stream error after max retries: ${streamError}`);
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
+ if (!sawAnyToolCalls && textBlockIndex !== null) {
795
+ let text = blocks.getText(textBlockIndex);
796
+ const recovered = [];
797
+ const bracketResult = parseBracketToolCalls(text);
798
+ if (bracketResult.toolCalls.length > 0) {
799
+ text = bracketResult.cleanedText;
800
+ recovered.push(...bracketResult.toolCalls);
801
+ }
802
+ const invokeResult = parseInvokeToolCalls(text);
803
+ if (invokeResult.toolCalls.length > 0) {
804
+ text = invokeResult.cleanedText;
805
+ recovered.push(...invokeResult.toolCalls);
806
+ }
807
+ if (recovered.length > 0) {
808
+ blocks.setText(textBlockIndex, text);
809
+ sawAnyToolCalls = true;
810
+ for (const call of recovered) {
811
+ if (emitToolCall({ toolUseId: call.toolUseId, name: call.name, input: JSON.stringify(call.arguments) })) {
812
+ emittedToolCalls++;
813
+ }
814
+ }
815
+ }
816
+ }
817
+ // Strip echo noise: when tool calls are present and the text content is just
818
+ // "." or a similar short echo from history padding, remove it. This prevents
819
+ // the echo from accumulating in conversation history and reinforcing the
820
+ // pattern in future turns.
821
+ if (emittedToolCalls > 0 && textBlockIndex !== null) {
822
+ if (/^\s*(\.+|continue)\s*$/i.test(blocks.getText(textBlockIndex)))
823
+ blocks.setText(textBlockIndex, "");
824
+ }
825
+ // Detect degenerate responses: the API returned 200 but produced no usable
826
+ // content at all — no text and no tool calls. This happens when the stream
827
+ // is truncated early or only a contextUsage event arrives.
828
+ //
829
+ // Also detect "Continue" echo loops, where the entire response is just
830
+ // "continue" with no tool calls, taught by synthetic history padding.
831
+ //
832
+ // When tool calls *were* present but all got dropped (empty/unparseable
833
+ // input), don't retry — the API did respond, it just sent malformed tool
834
+ // calls. Retrying would likely produce the same result.
835
+ const responseText = textBlockIndex === null ? "" : blocks.getText(textBlockIndex);
836
+ const hasText = responseText.length > 0;
837
+ const isEchoLoop = hasText && !sawAnyToolCalls && /^\s*(continue|\.+)\s*$/i.test(responseText);
838
+ if ((!hasText && !sawAnyToolCalls) || isEchoLoop) {
839
+ // Retrying an echo loop means unsaying text already delivered, which only
840
+ // a host that can discard emitted blocks may do. Elsewhere, go straight to
841
+ // the terminal behaviour: strip the echo so the agent loop does not read
842
+ // "Continue" as a continuation signal.
843
+ const mayRetry = retryCount < maxRetries && (!isEchoLoop || request.canDiscardEmittedBlocks === true);
844
+ if (mayRetry) {
845
+ retryCount++;
846
+ console.warn(`[kiro-core] ${isEchoLoop ? 'Echo loop detected (model responded with just "Continue")' : "Empty response (no text, no tool calls)"} — retrying (${retryCount}/${maxRetries})`);
847
+ blocks.reset();
848
+ textBlockIndex = null;
849
+ yield* drain(pending);
850
+ await abortableDelay(exponentialBackoff(retryCount - 1, 1000, MAX_RETRY_DELAY), signal);
851
+ continue;
852
+ }
853
+ if (isEchoLoop && textBlockIndex !== null) {
854
+ blocks.setText(textBlockIndex, "");
855
+ console.warn(`[kiro-core] Echo loop — stripping "Continue" response`);
856
+ }
857
+ else if (!hasText && !sawAnyToolCalls) {
858
+ console.warn(`[kiro-core] Empty response after ${maxRetries} retries — reporting a normal stop`);
859
+ }
860
+ }
861
+ if (textBlockIndex !== null)
862
+ blocks.endText(textBlockIndex);
863
+ // Kiro does not reliably emit per-response output token counts. When the
864
+ // `usage` event is missing or reports only `inputTokens`, fall back to a
865
+ // tiktoken estimate over everything the assistant emitted — text plus
866
+ // tool-call input JSON. Otherwise tool-call-only turns report 0 output
867
+ // tokens and break consumers that watch it.
868
+ if (usageEvent?.inputTokens !== undefined)
869
+ usage.input = usageEvent.inputTokens;
870
+ usage.output = usageEvent?.outputTokens ?? countTokens(totalContent);
871
+ usage.totalTokens = usage.input + usage.output;
872
+ usage.cost = calculateKiroCost(model.cost, usage);
873
+ // Use `emittedToolCalls`, not the count seen on the wire: a turn whose calls
874
+ // were all dropped for unparseable input must not report `toolUse`, because
875
+ // an empty turn with a tool-use stop stalls an agent loop waiting for
876
+ // results that will never arrive.
877
+ const stopReason = !receivedContextUsage && emittedToolCalls === 0 ? "length" : emittedToolCalls > 0 ? "toolUse" : "stop";
878
+ yield* drain(pending);
879
+ yield { type: "usage", usage };
880
+ yield { type: "done", stopReason };
881
+ debugLog("response.done", {
882
+ stopReason,
883
+ emittedToolCalls,
884
+ sawAnyToolCalls,
885
+ textLen: responseText.length,
886
+ usage,
887
+ });
888
+ return;
889
+ }
890
+ }
891
+ function* drain(pending) {
892
+ while (pending.length > 0) {
893
+ const event = pending.shift();
894
+ if (event)
895
+ yield event;
896
+ }
897
+ }
898
+ //# sourceMappingURL=stream.js.map