@iinm/plain-agent 1.8.4 → 1.8.6

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 (85) hide show
  1. package/bin/plain +1 -1
  2. package/package.json +8 -9
  3. package/sandbox/bin/plain-sandbox +13 -0
  4. package/src/agent.d.ts +52 -0
  5. package/src/agent.mjs +204 -0
  6. package/src/agentLoop.mjs +419 -0
  7. package/src/agentState.mjs +41 -0
  8. package/src/claudeCodePlugin.mjs +164 -0
  9. package/src/cliArgs.mjs +175 -0
  10. package/src/cliBatch.mjs +147 -0
  11. package/src/cliCommands.mjs +283 -0
  12. package/src/cliCompleter.mjs +227 -0
  13. package/src/cliCost.mjs +309 -0
  14. package/src/cliFormatter.mjs +518 -0
  15. package/src/cliInteractive.mjs +533 -0
  16. package/src/cliInterruptTransform.mjs +51 -0
  17. package/src/cliMuteTransform.mjs +26 -0
  18. package/src/cliPasteTransform.mjs +183 -0
  19. package/src/config.d.ts +36 -0
  20. package/src/config.mjs +197 -0
  21. package/src/context/loadAgentRoles.mjs +267 -0
  22. package/src/context/loadPrompts.mjs +303 -0
  23. package/src/context/loadUserMessageContext.mjs +147 -0
  24. package/src/costTracker.mjs +210 -0
  25. package/src/env.mjs +44 -0
  26. package/src/main.mjs +281 -0
  27. package/src/mcpClient.mjs +351 -0
  28. package/src/mcpIntegration.mjs +160 -0
  29. package/src/model.d.ts +109 -0
  30. package/src/modelCaller.mjs +32 -0
  31. package/src/modelDefinition.d.ts +92 -0
  32. package/src/prompt.mjs +138 -0
  33. package/src/providers/anthropic.d.ts +248 -0
  34. package/src/providers/anthropic.mjs +587 -0
  35. package/src/providers/bedrock.d.ts +249 -0
  36. package/src/providers/bedrock.mjs +700 -0
  37. package/src/providers/gemini.d.ts +208 -0
  38. package/src/providers/gemini.mjs +754 -0
  39. package/src/providers/openai.d.ts +281 -0
  40. package/src/providers/openai.mjs +544 -0
  41. package/src/providers/openaiCompatible.d.ts +147 -0
  42. package/src/providers/openaiCompatible.mjs +652 -0
  43. package/src/providers/platform/awsSigV4.mjs +184 -0
  44. package/src/providers/platform/azure.mjs +42 -0
  45. package/src/providers/platform/bedrock.mjs +78 -0
  46. package/src/providers/platform/googleCloud.mjs +34 -0
  47. package/src/subagent.mjs +265 -0
  48. package/src/tmpfile.mjs +27 -0
  49. package/src/tool.d.ts +74 -0
  50. package/src/toolExecutor.mjs +236 -0
  51. package/src/toolInputValidator.mjs +183 -0
  52. package/src/toolUseApprover.mjs +99 -0
  53. package/src/tools/askURL.mjs +209 -0
  54. package/src/tools/askWeb.mjs +208 -0
  55. package/src/tools/compactContext.d.ts +4 -0
  56. package/src/tools/compactContext.mjs +87 -0
  57. package/src/tools/execCommand.d.ts +22 -0
  58. package/src/tools/execCommand.mjs +200 -0
  59. package/src/tools/patchFile.d.ts +4 -0
  60. package/src/tools/patchFile.mjs +133 -0
  61. package/src/tools/switchToMainAgent.d.ts +3 -0
  62. package/src/tools/switchToMainAgent.mjs +43 -0
  63. package/src/tools/switchToSubagent.d.ts +4 -0
  64. package/src/tools/switchToSubagent.mjs +59 -0
  65. package/src/tools/tmuxCommand.d.ts +14 -0
  66. package/src/tools/tmuxCommand.mjs +194 -0
  67. package/src/tools/writeFile.d.ts +4 -0
  68. package/src/tools/writeFile.mjs +56 -0
  69. package/src/usageStore.mjs +167 -0
  70. package/src/utils/evalJSONConfig.mjs +72 -0
  71. package/src/utils/matchValue.d.ts +6 -0
  72. package/src/utils/matchValue.mjs +40 -0
  73. package/src/utils/noThrow.mjs +31 -0
  74. package/src/utils/notify.mjs +29 -0
  75. package/src/utils/parseFileRange.mjs +18 -0
  76. package/src/utils/parseFrontmatter.mjs +19 -0
  77. package/src/utils/readFileRange.mjs +33 -0
  78. package/src/utils/retryOnError.mjs +41 -0
  79. package/src/voiceInput.mjs +61 -0
  80. package/src/voiceInputGemini.mjs +105 -0
  81. package/src/voiceInputOpenAI.mjs +104 -0
  82. package/src/voiceInputSession.mjs +543 -0
  83. package/src/voiceToggleKey.mjs +62 -0
  84. package/dist/main.mjs +0 -473
  85. package/dist/main.mjs.map +0 -7
@@ -0,0 +1,754 @@
1
+ /**
2
+ * @import { ModelInput, Message, AssistantMessage, ModelOutput, PartialMessageContent, ProviderTokenUsage } from "../model";
3
+ * @import { GeminiCachedContents, GeminiContent, GeminiContentPartFunctionCall, GeminiContentPartText, GeminiCreateCachedContentInput as GeminiCreateCachedContentInput, GeminiFunctionContent, GeminiGenerateContentInput, GeminiGeneratedContent, GeminiModelConfig, GeminiModelContent, GeminiSystemContent, GeminiToolConfig, GeminiToolDefinition, GeminiUserContent } from "./gemini";
4
+ * @import { ToolDefinition } from "../tool";
5
+ */
6
+
7
+ import { styleText } from "node:util";
8
+ import { noThrow } from "../utils/noThrow.mjs";
9
+ import { getGoogleCloudAccessToken } from "./platform/googleCloud.mjs";
10
+
11
+ /**
12
+ * @callback GeminiModelCaller
13
+ * @param {GeminiModelConfig} config
14
+ * @param {ModelInput} input
15
+ * @param {number=} retryCount
16
+ * @returns {Promise<ModelOutput | Error>}
17
+ */
18
+
19
+ /**
20
+ * References:
21
+ * - https://ai.google.dev/gemini-api/docs/caching
22
+ * - https://ai.google.dev/api/caching
23
+ * @param {import("../modelDefinition").PlatformConfig} platformConfig
24
+ * @param {Pick<GeminiModelConfig, "model">} modelConfig
25
+ * @returns {GeminiModelCaller}
26
+ */
27
+ export function createCacheEnabledGeminiModelCaller(
28
+ platformConfig,
29
+ modelConfig,
30
+ ) {
31
+ const baseURL =
32
+ platformConfig.baseURL ||
33
+ "https://generativelanguage.googleapis.com/v1beta";
34
+
35
+ const props = {
36
+ cacheTTL: 2 * 60, // seconds
37
+ // https://ai.google.dev/gemini-api/docs/caching#considerations
38
+ minCacheableTokenCount: 2048,
39
+ };
40
+
41
+ /**
42
+ * @typedef {Object} CacheState
43
+ * @property {string} name
44
+ * @property {number} contentsLength - Length of contents without system
45
+ * @property {Date} expireTime
46
+ */
47
+
48
+ const state = {
49
+ /** @type {CacheState=} */
50
+ cache: undefined,
51
+ };
52
+
53
+ /** @type {GeminiModelCaller} */
54
+ async function modelCaller(config, input, retryCount = 0) {
55
+ return await noThrow(async () => {
56
+ const contents = convertGenericMessageToGeminiFormat(input.messages);
57
+ const tools = convertGenericToolDefinitionToGeminiFormat(
58
+ input.tools || [],
59
+ );
60
+ /** @type {GeminiToolConfig} */
61
+ const toolConfig = {
62
+ functionCallingConfig: {
63
+ // Workaround to prevent MALFORMED_FUNCTION_CALL issues with gemini-3-flash
64
+ mode: "VALIDATED",
65
+ },
66
+ };
67
+ const systemInstruction = contents.find((c) => c.role === "system");
68
+ const contentsWithoutSystem = contents.filter((c) => c.role !== "system");
69
+
70
+ // Clear cache if messages are cleared
71
+ if (contentsWithoutSystem.length <= (state.cache?.contentsLength ?? 0)) {
72
+ state.cache = undefined;
73
+ }
74
+
75
+ const url = (() => {
76
+ switch (platformConfig.name) {
77
+ case "gemini":
78
+ return `${baseURL}/models/${config.model}:streamGenerateContent?alt=sse`;
79
+ case "vertex-ai":
80
+ return `${baseURL}/publishers/google/models/${config.model}:streamGenerateContent?alt=sse`;
81
+ default:
82
+ throw new Error(`Unsupported platform: ${platformConfig.name}`);
83
+ }
84
+ })();
85
+
86
+ /** @type {Record<string,string>} */
87
+ const headers = await (async () => {
88
+ switch (platformConfig.name) {
89
+ case "gemini":
90
+ return {
91
+ ...platformConfig.customHeaders,
92
+ "x-goog-api-key": platformConfig.apiKey,
93
+ };
94
+ case "vertex-ai":
95
+ return {
96
+ ...platformConfig.customHeaders,
97
+ Authorization: `Bearer ${await getGoogleCloudAccessToken(platformConfig.account)}`,
98
+ };
99
+ }
100
+ })();
101
+
102
+ /** @type {Pick<GeminiGenerateContentInput, "generationConfig" | "safetySettings">} */
103
+ const baseRequest = {
104
+ // default
105
+ generationConfig: {
106
+ temperature: 0,
107
+ },
108
+ safetySettings: [
109
+ {
110
+ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
111
+ threshold: "BLOCK_NONE",
112
+ },
113
+ { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" },
114
+ { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" },
115
+ {
116
+ category: "HARM_CATEGORY_DANGEROUS_CONTENT",
117
+ threshold: "BLOCK_NONE",
118
+ },
119
+ ],
120
+ ...config.requestConfig,
121
+ };
122
+
123
+ /** @type {GeminiGenerateContentInput} */
124
+ const request =
125
+ state.cache && Date.now() < state.cache.expireTime.getTime()
126
+ ? {
127
+ ...baseRequest,
128
+ cachedContent: state.cache.name,
129
+ contents: contentsWithoutSystem.slice(state.cache.contentsLength),
130
+ }
131
+ : {
132
+ ...baseRequest,
133
+ system_instruction: systemInstruction,
134
+ contents: contentsWithoutSystem,
135
+ tools: tools.length ? tools : undefined,
136
+ toolConfig,
137
+ };
138
+
139
+ const response = await fetch(url, {
140
+ method: "POST",
141
+ headers: {
142
+ ...headers,
143
+ "Content-Type": "application/json",
144
+ },
145
+ body: JSON.stringify(request),
146
+ signal: AbortSignal.timeout(8 * 60 * 1000),
147
+ });
148
+
149
+ if (response.status === 429 || response.status >= 500) {
150
+ const interval = Math.min(2 * 2 ** retryCount, 16);
151
+ console.error(
152
+ styleText(
153
+ "yellow",
154
+ `Gemini rate limit exceeded. Retrying in ${interval} seconds...`,
155
+ ),
156
+ );
157
+ await new Promise((resolve) => setTimeout(resolve, interval * 1000));
158
+ return modelCaller(config, input, retryCount + 1);
159
+ }
160
+
161
+ if (response.status !== 200) {
162
+ return new Error(
163
+ `Failed to call Gemini model: status=${response.status}, body=${await response.text()}`,
164
+ );
165
+ }
166
+
167
+ if (!response.body) {
168
+ throw new Error("Response body is empty");
169
+ }
170
+
171
+ const reader = response.body.getReader();
172
+
173
+ /** @type {GeminiGeneratedContent[]} */
174
+ const streamContents = [];
175
+ /** @type {PartialMessageContent | undefined} */
176
+ let previousPartialContent;
177
+
178
+ for await (const streamContent of readGeminiStreamContents(reader)) {
179
+ streamContents.push(streamContent);
180
+
181
+ const partialContents =
182
+ convertGeminiStreamContentToAgentPartialContents(
183
+ streamContent,
184
+ previousPartialContent,
185
+ );
186
+ previousPartialContent = partialContents.at(-1);
187
+
188
+ if (input.onPartialMessageContent && partialContents.length) {
189
+ for (const partialContent of partialContents) {
190
+ input.onPartialMessageContent(partialContent);
191
+ }
192
+ }
193
+ }
194
+
195
+ if (input.onPartialMessageContent && previousPartialContent) {
196
+ input.onPartialMessageContent({
197
+ type: previousPartialContent.type,
198
+ position: "stop",
199
+ });
200
+ }
201
+
202
+ /** @type {GeminiGeneratedContent} */
203
+ const content = convertGeminiStreamContentsToContent(streamContents);
204
+
205
+ /** @type {ProviderTokenUsage} */
206
+ const tokenUsage = {
207
+ promptTokenCount: content.usageMetadata.promptTokenCount,
208
+ // nonCachedPromptTokenCount:
209
+ // content.usageMetadata.promptTokenCount -
210
+ // (content.usageMetadata.cachedContentTokenCount ?? 0),
211
+ cachedContentTokenCount:
212
+ content.usageMetadata.cachedContentTokenCount ?? 0,
213
+ candidatesTokenCount: content.usageMetadata.candidatesTokenCount ?? 0,
214
+ thoughtsTokenCount: content.usageMetadata.thoughtsTokenCount ?? 0,
215
+ totalTokenCount: content.usageMetadata.totalTokenCount,
216
+ };
217
+
218
+ const message = convertGeminiAssistantMessageToGenericFormat(content);
219
+ if (
220
+ message instanceof GeminiNoCandidateError ||
221
+ message instanceof GeminiMalformedFunctionCallError
222
+ ) {
223
+ const interval = Math.min(2 * 2 ** retryCount, 16);
224
+ console.error(
225
+ styleText(
226
+ "yellow",
227
+ `${message.name}: Retrying in ${interval} seconds...`,
228
+ ),
229
+ );
230
+ await new Promise((resolve) => setTimeout(resolve, interval * 1000));
231
+ return modelCaller(config, input, retryCount + 1);
232
+ }
233
+
234
+ // Create context cache for next request
235
+ if (
236
+ props.minCacheableTokenCount < content.usageMetadata.promptTokenCount
237
+ ) {
238
+ await updateCache({
239
+ contentsWithoutSystem: [
240
+ ...contentsWithoutSystem,
241
+ /** @type {GeminiModelContent} */ (
242
+ content.candidates?.at(0)?.content
243
+ ),
244
+ ],
245
+ systemInstruction,
246
+ tools,
247
+ toolConfig,
248
+ headers,
249
+ });
250
+ }
251
+
252
+ return {
253
+ message,
254
+ providerTokenUsage: tokenUsage,
255
+ };
256
+ });
257
+ }
258
+
259
+ /**
260
+ * @typedef {Object} UpdateCacheParams
261
+ * @property {(GeminiUserContent|GeminiModelContent|GeminiFunctionContent)[]} contentsWithoutSystem
262
+ * @property {GeminiSystemContent=} systemInstruction
263
+ * @property {GeminiToolDefinition[]=} tools
264
+ * @property {GeminiToolConfig=} toolConfig
265
+ * @property {Record<string,string>} headers
266
+ */
267
+
268
+ /**
269
+ * @param {UpdateCacheParams} params
270
+ */
271
+ async function updateCache({
272
+ contentsWithoutSystem,
273
+ systemInstruction,
274
+ tools,
275
+ toolConfig,
276
+ headers,
277
+ }) {
278
+ const modelPrefix =
279
+ platformConfig.name === "vertex-ai"
280
+ ? `${baseURL.match(/projects\/[^/]+\/locations\/[^/]+/)?.[0] || ""}/publishers/google/models`
281
+ : "models";
282
+
283
+ const url = `${baseURL}/cachedContents`;
284
+
285
+ /** @type {GeminiCreateCachedContentInput} */
286
+ const request = {
287
+ model: `${modelPrefix}/${modelConfig.model}`,
288
+ ttl: `${props.cacheTTL}s`,
289
+ system_instruction: systemInstruction,
290
+ contents: contentsWithoutSystem,
291
+ tools,
292
+ toolConfig,
293
+ };
294
+
295
+ await fetch(url, {
296
+ method: "POST",
297
+ headers: {
298
+ ...headers,
299
+ "Content-Type": "application/json",
300
+ },
301
+ body: JSON.stringify(request),
302
+ signal: AbortSignal.timeout(8 * 60 * 1000),
303
+ })
304
+ .then(async (response) => {
305
+ if (response.status !== 200) {
306
+ console.error(
307
+ styleText(
308
+ "yellow",
309
+ `Failed to create Gemini context cache: status=${response.status}, body=${await response.text()}`,
310
+ ),
311
+ );
312
+ } else {
313
+ /** @type {GeminiCachedContents} */
314
+ const cachedContents = await response.json();
315
+
316
+ // Delete old cache if previous cache is alive
317
+ if (state.cache && Date.now() < state.cache.expireTime.getTime()) {
318
+ fetch(
319
+ `${url}/${state.cache.name.replace(/.*cachedContents\//, "")}`,
320
+ {
321
+ method: "DELETE",
322
+ headers: {
323
+ ...headers,
324
+ "Content-Type": "application/json",
325
+ },
326
+ signal: AbortSignal.timeout(120 * 1000),
327
+ },
328
+ )
329
+ .then(async (response) => {
330
+ if (response.status !== 200) {
331
+ console.error(
332
+ styleText(
333
+ "yellow",
334
+ `Failed to delete Gemini context cache: status=${response.status}, body=${await response.text()}`,
335
+ ),
336
+ );
337
+ }
338
+ })
339
+ .catch((error) => {
340
+ console.error(
341
+ styleText(
342
+ "yellow",
343
+ `Failed to delete Gemini context cache: ${error}`,
344
+ ),
345
+ );
346
+ });
347
+ }
348
+
349
+ state.cache = {
350
+ name: cachedContents.name,
351
+ contentsLength: contentsWithoutSystem.length,
352
+ expireTime: new Date(cachedContents.expireTime),
353
+ };
354
+ }
355
+ })
356
+ .catch((error) => {
357
+ console.error(
358
+ styleText(
359
+ "yellow",
360
+ `Failed to create Gemini context cache: ${error}`,
361
+ ),
362
+ );
363
+ });
364
+ }
365
+
366
+ return modelCaller;
367
+ }
368
+
369
+ /**
370
+ * @param {Message[]} messages
371
+ * @returns {GeminiContent[]}
372
+ */
373
+ function convertGenericMessageToGeminiFormat(messages) {
374
+ /** @type {GeminiContent[]} */
375
+ const geminiContents = [];
376
+ for (const message of messages) {
377
+ switch (message.role) {
378
+ case "system": {
379
+ geminiContents.push({
380
+ role: "system",
381
+ parts: message.content.map((part) => ({
382
+ text: part.text,
383
+ })),
384
+ });
385
+ break;
386
+ }
387
+ case "user": {
388
+ const toolUseResults = message.content.filter(
389
+ (part) => part.type === "tool_result",
390
+ );
391
+ const userContentParts = message.content.filter(
392
+ (part) => part.type === "text" || part.type === "image",
393
+ );
394
+
395
+ if (toolUseResults.length) {
396
+ geminiContents.push({
397
+ role: "user",
398
+ parts: toolUseResults.map((toolResult) => ({
399
+ functionResponse: {
400
+ name: toolResult.toolName,
401
+ response: {
402
+ name: toolResult.toolName,
403
+ content: toolResult.content.map((part) => {
404
+ switch (part.type) {
405
+ case "text":
406
+ return { text: part.text };
407
+ case "image":
408
+ return {
409
+ inline_data: {
410
+ mime_type: part.mimeType,
411
+ data: part.data,
412
+ },
413
+ };
414
+ default:
415
+ throw new Error(
416
+ `Unsupported content part: ${JSON.stringify(part)}`,
417
+ );
418
+ }
419
+ }),
420
+ },
421
+ },
422
+ })),
423
+ });
424
+ }
425
+
426
+ if (userContentParts.length) {
427
+ geminiContents.push({
428
+ role: "user",
429
+ parts: userContentParts.map((part) => {
430
+ if (part.type === "text") {
431
+ return { text: part.text };
432
+ }
433
+ if (part.type === "image") {
434
+ return {
435
+ inline_data: {
436
+ mime_type: part.mimeType,
437
+ data: part.data,
438
+ },
439
+ };
440
+ }
441
+ throw new Error(
442
+ `Unsupported content part: ${JSON.stringify(part)}`,
443
+ );
444
+ }),
445
+ });
446
+ }
447
+
448
+ break;
449
+ }
450
+ case "assistant": {
451
+ /** @type {(GeminiContentPartText | GeminiContentPartFunctionCall)[]} */
452
+ const parts = [];
453
+ for (const part of message.content) {
454
+ if (part.type === "thinking") {
455
+ parts.push({
456
+ text: part.thinking,
457
+ thought: true,
458
+ ...(part.provider?.fields || {}),
459
+ });
460
+ } else if (part.type === "text") {
461
+ parts.push({
462
+ text: part.text,
463
+ ...(part.provider?.fields || {}),
464
+ });
465
+ } else if (part.type === "tool_use") {
466
+ parts.push({
467
+ functionCall: {
468
+ name: part.toolName,
469
+ args: part.input,
470
+ },
471
+ ...(part.provider?.fields || {}),
472
+ });
473
+ }
474
+ }
475
+ geminiContents.push({
476
+ role: "model",
477
+ parts,
478
+ });
479
+ break;
480
+ }
481
+ }
482
+ }
483
+
484
+ return geminiContents;
485
+ }
486
+
487
+ /**
488
+ * @param {ToolDefinition[]} tools
489
+ * @returns {GeminiToolDefinition[]}
490
+ */
491
+ function convertGenericToolDefinitionToGeminiFormat(tools) {
492
+ /** @type {GeminiToolDefinition["functionDeclarations"]} */
493
+ const functionDeclarations = [];
494
+ for (const tool of tools) {
495
+ functionDeclarations.push({
496
+ name: tool.name,
497
+ description: tool.description,
498
+ parametersJsonSchema: tool.inputSchema,
499
+ });
500
+ }
501
+
502
+ return [
503
+ {
504
+ functionDeclarations,
505
+ },
506
+ ];
507
+ }
508
+
509
+ /**
510
+ * @param {GeminiGeneratedContent} content
511
+ * @returns {AssistantMessage | GeminiNoCandidateError | GeminiMalformedFunctionCallError}
512
+ */
513
+ function convertGeminiAssistantMessageToGenericFormat(content) {
514
+ const candidate = content.candidates?.at(0);
515
+ if (!candidate) {
516
+ return new GeminiNoCandidateError(
517
+ `No candidates found: content=${JSON.stringify(content)}`,
518
+ );
519
+ }
520
+
521
+ if (candidate.finishReason === "MALFORMED_FUNCTION_CALL") {
522
+ return new GeminiMalformedFunctionCallError(
523
+ `Malformed function call: content=${JSON.stringify(content)}`,
524
+ );
525
+ }
526
+
527
+ /** @type {AssistantMessage["content"]} */
528
+ const assistantMessageContent = [];
529
+ for (const part of candidate.content.parts || []) {
530
+ if ("text" in part) {
531
+ if (part.thought) {
532
+ // thought summary
533
+ assistantMessageContent.push({
534
+ type: "thinking",
535
+ thinking: part.text,
536
+ });
537
+ } else {
538
+ assistantMessageContent.push({
539
+ type: "text",
540
+ text: part.text,
541
+ provider: part.thoughtSignature
542
+ ? { fields: { thoughtSignature: part.thoughtSignature } }
543
+ : undefined,
544
+ });
545
+ }
546
+ }
547
+ if ("functionCall" in part) {
548
+ assistantMessageContent.push({
549
+ type: "tool_use",
550
+ toolUseId: part.functionCall.name,
551
+ toolName: part.functionCall.name,
552
+ input: part.functionCall.args,
553
+ provider: part.thoughtSignature
554
+ ? { fields: { thoughtSignature: part.thoughtSignature } }
555
+ : undefined,
556
+ });
557
+ }
558
+ }
559
+
560
+ return {
561
+ role: "assistant",
562
+ content: assistantMessageContent,
563
+ };
564
+ }
565
+
566
+ /**
567
+ * @param {GeminiGeneratedContent[]} events
568
+ * @returns {GeminiGeneratedContent}
569
+ */
570
+ function convertGeminiStreamContentsToContent(events) {
571
+ const firstContent = events.at(0);
572
+ if (!firstContent) {
573
+ throw new Error("No content found");
574
+ }
575
+
576
+ /** @type {GeminiGeneratedContent} */
577
+ const mergedContent = {
578
+ ...firstContent,
579
+ // avoid side effects of mutating the original object
580
+ candidates: (firstContent.candidates || []).map((candidate) => ({
581
+ ...candidate,
582
+ content: {
583
+ ...candidate.content,
584
+ parts: [...(candidate.content.parts || [])],
585
+ },
586
+ })),
587
+ };
588
+
589
+ for (let i = 1; i < events.length; i++) {
590
+ const event = events[i];
591
+ if (event.candidates?.length) {
592
+ const candidate = event.candidates.at(0);
593
+ if (candidate?.content.parts?.length) {
594
+ mergedContent.candidates?.[0].content.parts?.push(
595
+ ...candidate.content.parts,
596
+ );
597
+ }
598
+ if (candidate?.finishReason && mergedContent.candidates?.[0]) {
599
+ mergedContent.candidates[0].finishReason = candidate.finishReason;
600
+ }
601
+ if (candidate?.finishMessage && mergedContent.candidates?.[0]) {
602
+ mergedContent.candidates[0].finishMessage = candidate.finishMessage;
603
+ }
604
+ }
605
+
606
+ if (event.usageMetadata.totalTokenCount) {
607
+ mergedContent.usageMetadata = event.usageMetadata;
608
+ }
609
+ }
610
+
611
+ return mergedContent;
612
+ }
613
+
614
+ /**
615
+ * @param {GeminiGeneratedContent} event
616
+ * @param {PartialMessageContent | undefined} previousPartialContent
617
+ * @returns {PartialMessageContent[]}
618
+ */
619
+ function convertGeminiStreamContentToAgentPartialContents(
620
+ event,
621
+ previousPartialContent,
622
+ ) {
623
+ const candiate = event.candidates?.at(0);
624
+ /** @type {PartialMessageContent[]} */
625
+ const partialMessageContents = [];
626
+ if (candiate?.content.parts?.length) {
627
+ /** @type {string | undefined} */
628
+ let previousPartType = previousPartialContent?.type;
629
+ for (const part of candiate.content.parts) {
630
+ const partType =
631
+ "text" in part ? (part.thought ? "thinking" : "text") : "tool_use";
632
+
633
+ if (previousPartType && previousPartType !== partType) {
634
+ partialMessageContents.push({
635
+ type: previousPartType,
636
+ position: "stop",
637
+ });
638
+ }
639
+
640
+ if (previousPartType !== partType) {
641
+ previousPartType = partType;
642
+ partialMessageContents.push({
643
+ type: partType,
644
+ position: "start",
645
+ });
646
+ }
647
+
648
+ if ("text" in part) {
649
+ if (part.thought) {
650
+ partialMessageContents.push({
651
+ type: "thinking",
652
+ content: part.text,
653
+ position: "delta",
654
+ });
655
+ } else {
656
+ partialMessageContents.push({
657
+ type: "text",
658
+ content: part.text,
659
+ position: "delta",
660
+ });
661
+ }
662
+ }
663
+
664
+ if ("functionCall" in part) {
665
+ if (previousPartialContent?.type === "tool_use") {
666
+ partialMessageContents.push({
667
+ type: "tool_use",
668
+ content: "\n",
669
+ position: "delta",
670
+ });
671
+ }
672
+ partialMessageContents.push({
673
+ type: "tool_use",
674
+ content: part.functionCall.name,
675
+ position: "delta",
676
+ });
677
+ }
678
+ }
679
+ }
680
+
681
+ return partialMessageContents;
682
+ }
683
+
684
+ class GeminiNoCandidateError extends Error {
685
+ /**
686
+ * @param {string} message
687
+ */
688
+ constructor(message) {
689
+ super(message);
690
+ this.name = "GeminiNoCandidateError";
691
+ }
692
+ }
693
+
694
+ class GeminiMalformedFunctionCallError extends Error {
695
+ /**
696
+ * @param {string} message
697
+ */
698
+ constructor(message) {
699
+ super(message);
700
+ this.name = "GeminiMalformedFunctionCallError";
701
+ }
702
+ }
703
+
704
+ /**
705
+ * @param {ReadableStreamDefaultReader<Uint8Array>} reader
706
+ * @returns {AsyncGenerator<GeminiGeneratedContent>}
707
+ */
708
+ async function* readGeminiStreamContents(reader) {
709
+ let buffer = new Uint8Array();
710
+
711
+ while (true) {
712
+ const { done, value } = await reader.read();
713
+ if (done) {
714
+ break;
715
+ }
716
+
717
+ const nextBuffer = new Uint8Array(buffer.length + value.length);
718
+ nextBuffer.set(buffer);
719
+ nextBuffer.set(value, buffer.length);
720
+ buffer = nextBuffer;
721
+
722
+ const carriageReturn = "\r".charCodeAt(0);
723
+ const lineFeed = "\n".charCodeAt(0);
724
+
725
+ const dataEndIndices = [];
726
+ for (let i = 0; i < buffer.length - 3; i++) {
727
+ if (
728
+ buffer[i] === carriageReturn &&
729
+ buffer[i + 1] === lineFeed &&
730
+ buffer[i + 2] === carriageReturn &&
731
+ buffer[i + 3] === lineFeed
732
+ ) {
733
+ dataEndIndices.push(i);
734
+ }
735
+ }
736
+
737
+ for (let i = 0; i < dataEndIndices.length; i++) {
738
+ const dataStartIndex = i === 0 ? 0 : dataEndIndices[i - 1] + 4;
739
+ const dataEndIndex = dataEndIndices[i];
740
+ const data = buffer.slice(dataStartIndex, dataEndIndex);
741
+ const decodedData = new TextDecoder().decode(data);
742
+
743
+ if (decodedData.startsWith("data: {")) {
744
+ /** @type {GeminiGeneratedContent} */
745
+ const parsedData = JSON.parse(decodedData.slice("data: ".length));
746
+ yield parsedData;
747
+ }
748
+ }
749
+
750
+ if (dataEndIndices.length) {
751
+ buffer = buffer.slice(dataEndIndices[dataEndIndices.length - 1] + 4);
752
+ }
753
+ }
754
+ }