@gajae-code/ai 0.13.2 → 0.14.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 (81) hide show
  1. package/CHANGELOG.md +61 -2
  2. package/dist/types/auth-broker/client.d.ts +9 -1
  3. package/dist/types/auth-broker/redact.d.ts +7 -0
  4. package/dist/types/auth-broker/remote-store.d.ts +50 -9
  5. package/dist/types/auth-broker/types.d.ts +14 -0
  6. package/dist/types/auth-broker/wire-schemas.d.ts +25 -0
  7. package/dist/types/auth-storage.d.ts +200 -6
  8. package/dist/types/core.d.ts +1 -0
  9. package/dist/types/model-cache.d.ts +4 -1
  10. package/dist/types/model-manager.d.ts +11 -0
  11. package/dist/types/provider-models/openai-compat.d.ts +5 -0
  12. package/dist/types/provider-models/special.d.ts +3 -0
  13. package/dist/types/providers/anthropic.d.ts +31 -0
  14. package/dist/types/providers/cursor.d.ts +9 -1
  15. package/dist/types/providers/kiro-codewhisperer.d.ts +8 -0
  16. package/dist/types/providers/mock.d.ts +8 -0
  17. package/dist/types/providers/register-builtins.d.ts +1 -0
  18. package/dist/types/providers/transform-messages.d.ts +18 -0
  19. package/dist/types/types.d.ts +34 -8
  20. package/dist/types/usage/grok-cli.d.ts +5 -0
  21. package/dist/types/usage.d.ts +6 -0
  22. package/dist/types/utils/discovery/openai-compatible.d.ts +5 -0
  23. package/dist/types/utils/event-stream.d.ts +4 -2
  24. package/dist/types/utils/fallback-transport.d.ts +10 -0
  25. package/dist/types/utils/http-inspector.d.ts +1 -0
  26. package/dist/types/utils/idle-iterator.d.ts +13 -1
  27. package/dist/types/utils/json-parse.d.ts +19 -0
  28. package/dist/types/utils/oauth/callback-server.d.ts +13 -0
  29. package/dist/types/utils/oauth/kiro.d.ts +71 -0
  30. package/dist/types/utils/oauth/types.d.ts +1 -1
  31. package/dist/types/utils/parse-bind.d.ts +8 -5
  32. package/dist/types/utils/tool-call-healing.d.ts +7 -0
  33. package/dist/types/utils/tool-choice-capability.d.ts +11 -0
  34. package/package.json +3 -2
  35. package/src/auth-broker/client.ts +30 -0
  36. package/src/auth-broker/redact.ts +15 -0
  37. package/src/auth-broker/refresher.ts +4 -2
  38. package/src/auth-broker/remote-store.ts +693 -70
  39. package/src/auth-broker/server.ts +57 -12
  40. package/src/auth-broker/types.ts +16 -0
  41. package/src/auth-broker/wire-schemas.ts +21 -0
  42. package/src/auth-gateway/server.ts +84 -19
  43. package/src/auth-storage.ts +985 -41
  44. package/src/core.ts +1 -0
  45. package/src/model-cache.ts +23 -4
  46. package/src/model-manager.ts +70 -11
  47. package/src/model-thinking.ts +45 -1
  48. package/src/models.json +9604 -1932
  49. package/src/openai-completions-compat.ts +2 -1
  50. package/src/provider-models/descriptors.ts +7 -1
  51. package/src/provider-models/openai-compat.ts +52 -28
  52. package/src/provider-models/special.ts +12 -0
  53. package/src/providers/amazon-bedrock.ts +2 -1
  54. package/src/providers/anthropic.ts +831 -27
  55. package/src/providers/cursor.ts +83 -3
  56. package/src/providers/kiro-codewhisperer.ts +572 -0
  57. package/src/providers/mock.ts +15 -2
  58. package/src/providers/ollama.ts +9 -2
  59. package/src/providers/openai-codex-responses.ts +16 -9
  60. package/src/providers/openai-completions.ts +6 -1
  61. package/src/providers/openai-responses-shared.ts +180 -18
  62. package/src/providers/register-builtins.ts +24 -2
  63. package/src/providers/transform-messages.ts +64 -1
  64. package/src/stream.ts +25 -2
  65. package/src/types.ts +36 -7
  66. package/src/usage/grok-cli.ts +86 -1
  67. package/src/usage.ts +7 -0
  68. package/src/utils/discovery/openai-compatible.ts +89 -4
  69. package/src/utils/event-stream.ts +11 -2
  70. package/src/utils/fallback-transport.ts +44 -2
  71. package/src/utils/http-inspector.ts +1 -0
  72. package/src/utils/idle-iterator.ts +29 -6
  73. package/src/utils/json-parse.ts +80 -0
  74. package/src/utils/oauth/callback-server.ts +31 -1
  75. package/src/utils/oauth/index.ts +14 -1
  76. package/src/utils/oauth/kiro.ts +448 -0
  77. package/src/utils/oauth/synthetic.ts +2 -3
  78. package/src/utils/oauth/types.ts +1 -0
  79. package/src/utils/parse-bind.ts +27 -0
  80. package/src/utils/tool-call-healing.ts +13 -2
  81. package/src/utils/tool-choice-capability.ts +386 -6
@@ -27,7 +27,7 @@ import type {
27
27
  } from "../types";
28
28
  import { normalizeSystemPrompts } from "../utils";
29
29
  import { AssistantMessageEventStream } from "../utils/event-stream";
30
- import { parseStreamingJson } from "../utils/json-parse";
30
+ import { findUnnecessaryUnicodeEscape, parseStreamingJson } from "../utils/json-parse";
31
31
  import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
32
32
  import { flattenToolRootCombinators, toolWireSchema } from "../utils/schema";
33
33
  import { CURSOR_COMPOSER_EDIT_DISCIPLINE_PROMPT, isComposerHarnessModel } from "./composer-discipline";
@@ -564,6 +564,9 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
564
564
  if (state.currentToolCall) {
565
565
  const idx = output.content.indexOf(state.currentToolCall);
566
566
  state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall.partialJson);
567
+ if (findUnnecessaryUnicodeEscape(state.currentToolCall.partialJson ?? "")) {
568
+ state.currentToolCall.escapedNonAsciiArguments = true;
569
+ }
567
570
  delete (state.currentToolCall as any).partialJson;
568
571
  delete (state.currentToolCall as any).index;
569
572
  stream.push({
@@ -1913,7 +1916,75 @@ function cursorNativeToolName(kindKey: string): string {
1913
1916
  // do not otherwise handle (everything except mcpToolCall / updateTodosToolCall), so
1914
1917
  // without this they are silently dropped and never render. Build a generic toolCall
1915
1918
  // block from whichever *ToolCall field is set so the call (and its result) is shown.
1916
- function buildNativeToolCallBlock(
1919
+
1920
+ /** Hard node budget for one native-payload conversion; bounds hostile or cyclic graphs. */
1921
+ const CURSOR_JSON_SAFE_MAX_NODES = 10_000;
1922
+ const CURSOR_JSON_SAFE_MAX_DEPTH = 100;
1923
+
1924
+ /**
1925
+ * Total conversion of a Cursor protobuf payload into plain JSON-safe data.
1926
+ *
1927
+ * protobuf-es v2 messages are plain objects, but they carry `$typeName`
1928
+ * markers, `bigint` fields (e.g. `fileSize`, `durationMs`, `timestampMs`,
1929
+ * `fileOutputThresholdBytes`), and `Uint8Array` blobs. None of those may leak
1930
+ * into assistant message content: toolCall `arguments` are staged into managed
1931
+ * snapshots, persisted to the JSONL transcript, and replayed to providers —
1932
+ * all of which require `JSON.stringify`-safe values. Attaching the raw payload
1933
+ * is exactly the local-snapshot producer defect class behind issue #4578.
1934
+ *
1935
+ * Rules: `$typeName` is stripped, safe-range bigints become numbers (decimal
1936
+ * strings beyond `Number.MAX_SAFE_INTEGER`), byte arrays become base64
1937
+ * strings, dates become ISO strings, functions/symbols are dropped, cycles
1938
+ * and over-depth values collapse to null, and containers stop accepting
1939
+ * entries once the shared node budget is exhausted.
1940
+ */
1941
+ function cursorJsonSafeValue(value: unknown, path?: Set<object>, budget?: { remaining: number }, depth = 0): unknown {
1942
+ const seen = path ?? new Set<object>();
1943
+ const nodes = budget ?? { remaining: CURSOR_JSON_SAFE_MAX_NODES };
1944
+ if (nodes.remaining-- <= 0) return null;
1945
+ if (depth >= CURSOR_JSON_SAFE_MAX_DEPTH) return null;
1946
+ if (typeof value === "bigint") {
1947
+ return value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= BigInt(-Number.MAX_SAFE_INTEGER)
1948
+ ? Number(value)
1949
+ : value.toString();
1950
+ }
1951
+ if (typeof value === "function" || typeof value === "symbol" || value === undefined) return null;
1952
+ if (typeof value === "number" && !Number.isFinite(value)) return null;
1953
+ if (value === null || typeof value !== "object") return value;
1954
+ if (seen.has(value)) return null;
1955
+ if (value instanceof Uint8Array)
1956
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("base64");
1957
+ if (value instanceof Date) return Number.isFinite(value.getTime()) ? value.toISOString() : null;
1958
+ seen.add(value);
1959
+ try {
1960
+ if (Array.isArray(value)) {
1961
+ const array: unknown[] = [];
1962
+ for (const entry of value) {
1963
+ if (nodes.remaining <= 0) break;
1964
+ array.push(cursorJsonSafeValue(entry, seen, nodes, depth + 1));
1965
+ }
1966
+ return array;
1967
+ }
1968
+ const record: Record<string, unknown> = {};
1969
+ for (const [key, entry] of Object.entries(value)) {
1970
+ if (key === "$typeName") continue;
1971
+ if (nodes.remaining <= 0) break;
1972
+ record[key] = cursorJsonSafeValue(entry, seen, nodes, depth + 1);
1973
+ }
1974
+ return record;
1975
+ } catch {
1976
+ return null;
1977
+ } finally {
1978
+ seen.delete(value);
1979
+ }
1980
+ }
1981
+
1982
+ /** Exported for direct regression coverage of the JSON-safety boundary. */
1983
+ export function cursorJsonSafeValueForTest(value: unknown): unknown {
1984
+ return cursorJsonSafeValue(value);
1985
+ }
1986
+
1987
+ export function buildNativeToolCallBlock(
1917
1988
  toolCall: Record<string, unknown>,
1918
1989
  callId: string,
1919
1990
  index: number,
@@ -1922,11 +1993,20 @@ function buildNativeToolCallBlock(
1922
1993
  if (!/ToolCall$/.test(key) || !payload || typeof payload !== "object") continue;
1923
1994
  if (key === "mcpToolCall" || key === "updateTodosToolCall") continue;
1924
1995
  const args = (payload as { args?: unknown }).args;
1996
+ const hasObjectArgs = args !== null && typeof args === "object";
1997
+ const convertedArgs = hasObjectArgs ? cursorJsonSafeValue(args) : undefined;
1998
+ const safeArguments =
1999
+ convertedArgs !== undefined &&
2000
+ convertedArgs !== null &&
2001
+ typeof convertedArgs === "object" &&
2002
+ !Array.isArray(convertedArgs)
2003
+ ? (convertedArgs as Record<string, unknown>)
2004
+ : { raw: hasObjectArgs ? convertedArgs : cursorJsonSafeValue(payload) };
1925
2005
  return {
1926
2006
  type: "toolCall",
1927
2007
  id: callId,
1928
2008
  name: cursorNativeToolName(key),
1929
- arguments: args && typeof args === "object" ? (args as Record<string, unknown>) : { raw: payload },
2009
+ arguments: safeArguments,
1930
2010
  index,
1931
2011
  kind: "native",
1932
2012
  };
@@ -0,0 +1,572 @@
1
+ /**
2
+ * Kiro / Amazon Q Developer / CodeWhisperer streaming transport.
3
+ *
4
+ * Talks directly to the CodeWhisperer streaming service over HTTPS using
5
+ * a bearer token from AWS SSO OIDC. The response is an
6
+ * `application/vnd.amazon.eventstream`, decoded by the shared
7
+ * `decodeEventStream` primitive from `aws-eventstream.ts`.
8
+ *
9
+ * Clean-room: derived from published Amazon service model shapes
10
+ * (aws-toolkit-vscode CodeWhisperer streaming + codewhispererruntime-2022-11-11),
11
+ * not from any AGPL reference implementation.
12
+ */
13
+ import { $credentialEnv, $env, extractHttpStatusFromError } from "@gajae-code/utils";
14
+ import type {
15
+ Api,
16
+ AssistantMessage,
17
+ Context,
18
+ Model,
19
+ StreamFunction,
20
+ StreamOptions,
21
+ TextContent,
22
+ Tool,
23
+ ToolCall,
24
+ ToolResultMessage,
25
+ } from "../types";
26
+ import { AssistantMessageEventStream } from "../utils/event-stream";
27
+ import { transportFailureFacts } from "../utils/fallback-transport";
28
+ import { withHttpStatus } from "../utils/http-inspector";
29
+ import { decodeEventStream } from "./aws-eventstream";
30
+
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+ // Provider options
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+
35
+ export interface KiroCodeWhispererOptions extends StreamOptions {
36
+ /** AWS region for the CodeWhisperer streaming endpoint. */
37
+ region?: string;
38
+ /** Profile ARN for enterprise IAM Identity Center accounts. */
39
+ profileArn?: string;
40
+ }
41
+
42
+ // ─────────────────────────────────────────────────────────────────────────────
43
+ // CodeWhisperer streaming wire types
44
+ // ─────────────────────────────────────────────────────────────────────────────
45
+
46
+ interface WireToolSpec {
47
+ toolSpecification: {
48
+ name: string;
49
+ description?: string;
50
+ inputSchema: { json: unknown };
51
+ };
52
+ }
53
+
54
+ interface WireToolResult {
55
+ toolResultMessage: {
56
+ content: string;
57
+ toolUseId: string;
58
+ status?: "success" | "error";
59
+ };
60
+ }
61
+
62
+ interface WireUserMessage {
63
+ userInputMessage: {
64
+ content: string;
65
+ userInputMessageContext?: {
66
+ tools?: { tools: WireToolSpec[] };
67
+ toolResults?: { toolResults: WireToolResult[][] };
68
+ editorStateContext?: Record<string, unknown>;
69
+ };
70
+ origin?: string;
71
+ };
72
+ }
73
+
74
+ interface WireAssistantMessage {
75
+ assistantResponseMessage: {
76
+ content: string;
77
+ };
78
+ }
79
+
80
+ type WireHistoryMessage = WireUserMessage | WireAssistantMessage;
81
+
82
+ interface ConversationState {
83
+ chatTriggerType: "MANUAL";
84
+ currentMessage: WireUserMessage;
85
+ history?: WireHistoryMessage[];
86
+ profileArn?: string;
87
+ customizationArn?: string;
88
+ }
89
+
90
+ interface GenerateAssistantResponseRequest {
91
+ conversationState: ConversationState;
92
+ }
93
+
94
+ // ─────────────────────────────────────────────────────────────────────────────
95
+ // Response eventstream types (ChatResponseStream union members)
96
+ // ─────────────────────────────────────────────────────────────────────────────
97
+
98
+ interface AssistantResponseEvent {
99
+ assistantResponseEvent?: {
100
+ content?: string;
101
+ };
102
+ }
103
+
104
+ interface ToolUseEventPayload {
105
+ toolUseEvent?: {
106
+ toolUseId?: string;
107
+ name?: string;
108
+ input?: unknown;
109
+ stop?: { stopReason?: string };
110
+ };
111
+ }
112
+
113
+ interface MessageMetadataEvent {
114
+ messageMetadataEvent?: {
115
+ conversationId?: string;
116
+ utteranceId?: string;
117
+ };
118
+ }
119
+
120
+ interface ErrorPayload {
121
+ error?: {
122
+ message?: string;
123
+ code?: string;
124
+ };
125
+ }
126
+
127
+ // ─────────────────────────────────────────────────────────────────────────────
128
+ // Constants
129
+ // ─────────────────────────────────────────────────────────────────────────────
130
+
131
+ const DEFAULT_REGION = "us-east-1";
132
+ const STREAMING_SERVICE_NAME = "amazoncodewhispererstreamingservice";
133
+
134
+ type Block = (TextContent | ToolCall) & { index?: number; partialJson?: string };
135
+
136
+ // ─────────────────────────────────────────────────────────────────────────────
137
+ // Stream function
138
+ // ─────────────────────────────────────────────────────────────────────────────
139
+
140
+ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"> = (
141
+ model: Model<"kiro-codewhisperer-stream">,
142
+ context: Context,
143
+ options: KiroCodeWhispererOptions,
144
+ ): AssistantMessageEventStream => {
145
+ const stream = new AssistantMessageEventStream();
146
+
147
+ (async () => {
148
+ const startTime = Date.now();
149
+ let firstTokenTime: number | undefined;
150
+
151
+ const output: AssistantMessage = {
152
+ role: "assistant",
153
+ content: [],
154
+ api: "kiro-codewhisperer-stream" as Api,
155
+ provider: model.provider,
156
+ model: model.id,
157
+ usage: {
158
+ input: 0,
159
+ output: 0,
160
+ cacheRead: 0,
161
+ cacheWrite: 0,
162
+ totalTokens: 0,
163
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
164
+ },
165
+ stopReason: "stop",
166
+ timestamp: Date.now(),
167
+ };
168
+
169
+ const blocks = output.content as Block[];
170
+ const region = options.region || $env.KIRO_REGION || $env.AWS_REGION || $env.AWS_DEFAULT_REGION || DEFAULT_REGION;
171
+
172
+ try {
173
+ // Resolve bearer token
174
+ const bearerToken = resolveBearerToken(options.apiKey);
175
+ if (!bearerToken) {
176
+ throw new Error(
177
+ "No Kiro credentials found. Run 'gjc auth-broker login kiro' to authenticate via AWS Builder ID, or set AWS_BEARER_TOKEN_KIRO.",
178
+ );
179
+ }
180
+
181
+ // Build request
182
+ const conversationState = buildConversationState(context, model, options);
183
+ const requestBody: GenerateAssistantResponseRequest = {
184
+ conversationState,
185
+ };
186
+
187
+ options?.onPayload?.(requestBody, model, options?.attemptScope);
188
+
189
+ const host = `${STREAMING_SERVICE_NAME}.${region}.amazonaws.com`;
190
+ const url = `https://${host}/`;
191
+
192
+ const bodyText = JSON.stringify(requestBody);
193
+ const body = new TextEncoder().encode(bodyText);
194
+ const requestHeaders: Record<string, string> = {
195
+ "content-type": "application/json",
196
+ accept: "application/vnd.amazon.eventstream",
197
+ authorization: `Bearer ${bearerToken}`,
198
+ "amzn-X-amz-target": "AmazonCodeWhispererService.GenerateAssistantResponse",
199
+ };
200
+
201
+ if (options.profileArn) {
202
+ requestHeaders["x-amzn-codewhisperer-proflearn"] = options.profileArn;
203
+ }
204
+
205
+ // Merge user-provided headers
206
+ if (options.headers) {
207
+ Object.assign(requestHeaders, options.headers);
208
+ }
209
+
210
+ const response = await fetch(url, {
211
+ method: "POST",
212
+ headers: requestHeaders,
213
+ body,
214
+ signal: options.signal,
215
+ });
216
+
217
+ if (!response.ok) {
218
+ const errBody = await response.text().catch(() => "");
219
+ throw withHttpStatus(
220
+ new Error(`Kiro CodeWhisperer HTTP ${response.status}: ${errBody.slice(0, 1000)}`),
221
+ response.status,
222
+ );
223
+ }
224
+
225
+ if (!response.body) throw new Error("Kiro CodeWhisperer response has no body");
226
+
227
+ // Decode eventstream
228
+ for await (const message of decodeEventStream(response.body)) {
229
+ if (options.signal?.aborted) break;
230
+
231
+ const messageType = message.headers[":message-type"];
232
+ const eventType = message.headers[":event-type"];
233
+
234
+ if (messageType === "exception") {
235
+ const exceptionType = message.headers[":exception-type"] || "Exception";
236
+ const payload = safeParsePayload(message.payload) as { message?: string } | undefined;
237
+ const errorMessage = payload?.message || new TextDecoder().decode(message.payload);
238
+ const status = exceptionType === "ValidationException" ? 400 : 0;
239
+ const err = new Error(`${exceptionType}: ${errorMessage}`);
240
+ throw status ? withHttpStatus(err, status) : err;
241
+ }
242
+
243
+ if (messageType === "error") {
244
+ const code = message.headers[":error-code"] || "UnknownError";
245
+ const errorMessage = message.headers[":error-message"] || new TextDecoder().decode(message.payload);
246
+ throw new Error(`${code}: ${errorMessage}`);
247
+ }
248
+
249
+ if (messageType !== "event") continue;
250
+
251
+ const payload = safeParsePayload(message.payload);
252
+ if (!payload) continue;
253
+
254
+ switch (eventType) {
255
+ case "assistantResponseEvent": {
256
+ const ev = payload as AssistantResponseEvent;
257
+ const content = ev.assistantResponseEvent?.content;
258
+ if (content) {
259
+ if (!firstTokenTime) firstTokenTime = Date.now();
260
+ if (blocks.length === 0) {
261
+ stream.push({ type: "start", partial: output });
262
+ }
263
+ handleTextDelta(content, blocks, output, stream);
264
+ }
265
+ break;
266
+ }
267
+ case "toolUseEvent": {
268
+ const ev = payload as ToolUseEventPayload;
269
+ if (!firstTokenTime) firstTokenTime = Date.now();
270
+ if (blocks.length === 0) {
271
+ stream.push({ type: "start", partial: output });
272
+ }
273
+ handleToolUseEvent(ev, blocks, output, stream);
274
+ break;
275
+ }
276
+ case "messageMetadataEvent": {
277
+ const ev = payload as MessageMetadataEvent;
278
+ if (ev.messageMetadataEvent?.conversationId) {
279
+ output.responseId = ev.messageMetadataEvent.conversationId;
280
+ }
281
+ break;
282
+ }
283
+ case "codeReferenceEvent":
284
+ case "supplementaryWebLinksEvent":
285
+ case "followupPromptEvent":
286
+ case "dryRunSucceedEvent":
287
+ case "citationEvent":
288
+ case "intentsEvent":
289
+ case "interactionComponentsEvent":
290
+ case "invalidStateEvent":
291
+ // Known but unhandled events — ignore gracefully
292
+ break;
293
+ default:
294
+ // Unknown event types — ignore (forward compatibility)
295
+ break;
296
+ }
297
+
298
+ const errorPayload = payload as ErrorPayload;
299
+ if (errorPayload.error?.message) {
300
+ throw new Error(`Kiro CodeWhisperer stream error: ${errorPayload.error.message}`);
301
+ }
302
+ }
303
+
304
+ if (options.signal?.aborted) throw new Error("Request was aborted");
305
+
306
+ // Finalize blocks
307
+ for (const block of blocks) {
308
+ delete block.index;
309
+ delete block.partialJson;
310
+ }
311
+
312
+ // Determine stop reason
313
+ const hasToolCall = blocks.some(b => b.type === "toolCall");
314
+ output.stopReason = hasToolCall ? "toolUse" : "stop";
315
+
316
+ output.duration = Date.now() - startTime;
317
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
318
+ stream.push({ type: "done", reason: output.stopReason as "stop" | "length" | "toolUse", message: output });
319
+ stream.end();
320
+ } catch (error) {
321
+ for (const block of output.content) {
322
+ delete (block as Block).index;
323
+ delete (block as Block).partialJson;
324
+ }
325
+ output.stopReason = options.signal?.aborted ? "aborted" : "error";
326
+ output.errorStatus = extractHttpStatusFromError(error);
327
+ output.transportFailure = transportFailureFacts(error);
328
+ const baseMessage = error instanceof Error ? error.message : JSON.stringify(error);
329
+ output.errorMessage = baseMessage;
330
+ output.duration = Date.now() - startTime;
331
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
332
+ stream.push({ type: "error", reason: output.stopReason, error: output });
333
+ stream.end();
334
+ }
335
+ })();
336
+
337
+ return stream;
338
+ };
339
+
340
+ // ─────────────────────────────────────────────────────────────────────────────
341
+ // Request building
342
+ // ─────────────────────────────────────────────────────────────────────────────
343
+
344
+ function buildConversationState(
345
+ context: Context,
346
+ _model: Model<"kiro-codewhisperer-stream">,
347
+ options: KiroCodeWhispererOptions,
348
+ ): ConversationState {
349
+ const messages = context.messages;
350
+ if (messages.length === 0) {
351
+ throw new Error("Kiro CodeWhisperer requires at least one message");
352
+ }
353
+
354
+ // Build history from all messages except the last
355
+ const history: WireHistoryMessage[] = [];
356
+ const systemPrompt = context.systemPrompt?.join("\n") ?? "";
357
+
358
+ for (let i = 0; i < messages.length - 1; i++) {
359
+ const msg = messages[i];
360
+ history.push(convertToWireMessage(msg, i === 0 ? systemPrompt : undefined));
361
+ }
362
+
363
+ // Convert the last message as currentMessage
364
+ const lastMsg = messages[messages.length - 1];
365
+ const currentMessage = convertToWireUserMessage(lastMsg, systemPrompt);
366
+
367
+ // Add tools to the current message context
368
+ if (context.tools && context.tools.length > 0) {
369
+ if (!currentMessage.userInputMessage.userInputMessageContext) {
370
+ currentMessage.userInputMessage.userInputMessageContext = {};
371
+ }
372
+ currentMessage.userInputMessage.userInputMessageContext.tools = {
373
+ tools: convertTools(context.tools),
374
+ };
375
+ }
376
+
377
+ return {
378
+ chatTriggerType: "MANUAL",
379
+ currentMessage,
380
+ history: history.length > 0 ? history : undefined,
381
+ profileArn: options.profileArn,
382
+ };
383
+ }
384
+
385
+ function convertToWireMessage(msg: Context["messages"][number], systemPrompt?: string): WireHistoryMessage {
386
+ if (msg.role === "user") {
387
+ return convertToWireUserMessage(msg, systemPrompt);
388
+ }
389
+ if (msg.role === "toolResult") {
390
+ return convertToWireUserMessage(msg, systemPrompt);
391
+ }
392
+ // assistant → assistant response
393
+ const textParts: string[] = [];
394
+ for (const block of msg.content) {
395
+ if (typeof block === "string") {
396
+ textParts.push(block);
397
+ } else if (block.type === "text") {
398
+ textParts.push(block.text);
399
+ } else if (block.type === "toolCall") {
400
+ textParts.push(JSON.stringify({ toolUseId: block.id, name: block.name, input: block.arguments }));
401
+ }
402
+ }
403
+ return {
404
+ assistantResponseMessage: {
405
+ content: textParts.join("\n") || "",
406
+ },
407
+ };
408
+ }
409
+
410
+ function convertToWireUserMessage(msg: Context["messages"][number], systemPrompt?: string): WireUserMessage {
411
+ let content = extractTextContent(msg);
412
+ if (systemPrompt) {
413
+ content = `${systemPrompt}\n\n${content}`;
414
+ }
415
+
416
+ const userMsg: WireUserMessage = {
417
+ userInputMessage: {
418
+ content,
419
+ },
420
+ };
421
+
422
+ // Handle tool results
423
+ if (msg.role === "toolResult") {
424
+ const toolResultMsg = msg as ToolResultMessage;
425
+ const toolResults = (toolResultMsg.content ?? []).map(detail => ({
426
+ toolResultMessage: {
427
+ content: detail.type === "text" ? detail.text : "",
428
+ toolUseId: toolResultMsg.toolCallId,
429
+ status: toolResultMsg.isError ? ("error" as const) : ("success" as const),
430
+ },
431
+ }));
432
+ if (toolResults.length > 0) {
433
+ if (!userMsg.userInputMessage.userInputMessageContext) {
434
+ userMsg.userInputMessage.userInputMessageContext = {};
435
+ }
436
+ userMsg.userInputMessage.userInputMessageContext.toolResults = { toolResults: [toolResults] };
437
+ }
438
+ }
439
+
440
+ return userMsg;
441
+ }
442
+
443
+ function extractTextContent(msg: Context["messages"][number]): string {
444
+ if (typeof msg.content === "string") return msg.content;
445
+ if (Array.isArray(msg.content)) {
446
+ return msg.content
447
+ .map(block => {
448
+ if (typeof block === "string") return block;
449
+ if (block.type === "text") return block.text;
450
+ if (block.type === "image") return ""; // Images not supported in text field
451
+ return "";
452
+ })
453
+ .join("");
454
+ }
455
+ return "";
456
+ }
457
+
458
+ function convertTools(tools: Tool[]): WireToolSpec[] {
459
+ return tools.map(tool => ({
460
+ toolSpecification: {
461
+ name: tool.name,
462
+ description: tool.description ?? "",
463
+ inputSchema: {
464
+ json: tool.parameters ?? {},
465
+ },
466
+ },
467
+ }));
468
+ }
469
+
470
+ // ─────────────────────────────────────────────────────────────────────────────
471
+ // Event handling
472
+ // ─────────────────────────────────────────────────────────────────────────────
473
+
474
+ function handleTextDelta(
475
+ delta: string,
476
+ blocks: Block[],
477
+ output: AssistantMessage,
478
+ stream: AssistantMessageEventStream,
479
+ ): void {
480
+ // Find or create the last text block
481
+ let lastBlock = blocks[blocks.length - 1];
482
+ if (lastBlock?.type !== "text") {
483
+ const newBlock: Block = { type: "text", text: "", index: blocks.length };
484
+ blocks.push(newBlock);
485
+ lastBlock = newBlock;
486
+ stream.push({ type: "text_start", contentIndex: newBlock.index!, partial: output });
487
+ }
488
+ lastBlock.text += delta;
489
+ stream.push({ type: "text_delta", contentIndex: lastBlock.index!, delta, partial: output });
490
+ }
491
+
492
+ function handleToolUseEvent(
493
+ ev: ToolUseEventPayload,
494
+ blocks: Block[],
495
+ output: AssistantMessage,
496
+ stream: AssistantMessageEventStream,
497
+ ): void {
498
+ const toolEvent = ev.toolUseEvent;
499
+ if (!toolEvent) return;
500
+
501
+ const toolUseId = toolEvent.toolUseId ?? "";
502
+ const name = toolEvent.name ?? "";
503
+
504
+ // If input is provided as a complete object, emit toolcall_end
505
+ if (toolEvent.input !== undefined && toolEvent.input !== null) {
506
+ const inputStr = typeof toolEvent.input === "string" ? toolEvent.input : JSON.stringify(toolEvent.input);
507
+ const toolCall: ToolCall = {
508
+ type: "toolCall",
509
+ id: toolUseId,
510
+ name,
511
+ arguments: safeParseJson(inputStr) as Record<string, any>,
512
+ };
513
+
514
+ const newBlock: Block = { ...toolCall, index: blocks.length };
515
+ blocks.push(newBlock);
516
+ stream.push({ type: "toolcall_end", contentIndex: newBlock.index!, toolCall, partial: output });
517
+ return;
518
+ }
519
+
520
+ // Otherwise, accumulate partial input (if the service streams it in chunks)
521
+ // The published model does not document chunked tool input for CodeWhisperer,
522
+ // so this path handles the case defensively but expects complete input per event.
523
+ const toolCall: ToolCall = {
524
+ type: "toolCall",
525
+ id: toolUseId,
526
+ name,
527
+ arguments: {},
528
+ };
529
+
530
+ const newBlock: Block = { ...toolCall, index: blocks.length };
531
+ blocks.push(newBlock);
532
+ stream.push({ type: "toolcall_end", contentIndex: newBlock.index!, toolCall, partial: output });
533
+ }
534
+
535
+ // ─────────────────────────────────────────────────────────────────────────────
536
+ // Helpers
537
+ // ─────────────────────────────────────────────────────────────────────────────
538
+
539
+ function resolveBearerToken(apiKey: string | undefined): string | undefined {
540
+ if (!apiKey) {
541
+ return $credentialEnv("AWS_BEARER_TOKEN_KIRO") ?? undefined;
542
+ }
543
+
544
+ // Structured API key (from getOAuthApiKey) contains the access token as JSON
545
+ try {
546
+ const parsed = JSON.parse(apiKey) as { token?: string };
547
+ if (parsed.token) return parsed.token;
548
+ } catch {
549
+ // Plain bearer token
550
+ }
551
+
552
+ return apiKey;
553
+ }
554
+
555
+ function safeParsePayload(payload: Uint8Array): unknown {
556
+ if (payload.length === 0) return {};
557
+ try {
558
+ const text = new TextDecoder().decode(payload);
559
+ return JSON.parse(text);
560
+ } catch {
561
+ return undefined;
562
+ }
563
+ }
564
+
565
+ function safeParseJson(str: string): unknown {
566
+ if (!str) return {};
567
+ try {
568
+ return JSON.parse(str);
569
+ } catch {
570
+ return str;
571
+ }
572
+ }