@iinm/plain-agent 1.0.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 (79) hide show
  1. package/.config/agents.library/code-simplifier.md +5 -0
  2. package/.config/agents.library/qa-engineer.md +74 -0
  3. package/.config/agents.library/software-architect.md +278 -0
  4. package/.config/agents.predefined/worker.md +3 -0
  5. package/.config/config.predefined.json +825 -0
  6. package/.config/prompts.library/code-review.md +8 -0
  7. package/.config/prompts.library/feature-dev.md +6 -0
  8. package/.config/prompts.predefined/shortcuts/commit-by-user.md +9 -0
  9. package/.config/prompts.predefined/shortcuts/commit.md +10 -0
  10. package/.config/prompts.predefined/shortcuts/general-question.md +6 -0
  11. package/LICENSE +21 -0
  12. package/README.md +624 -0
  13. package/bin/plain +3 -0
  14. package/bin/plain-interrupt +6 -0
  15. package/bin/plain-notify-desktop +19 -0
  16. package/bin/plain-notify-terminal-bell +3 -0
  17. package/package.json +57 -0
  18. package/sandbox/bin/plain-sandbox +972 -0
  19. package/src/agent.d.ts +48 -0
  20. package/src/agent.mjs +159 -0
  21. package/src/agentLoop.mjs +369 -0
  22. package/src/agentState.mjs +41 -0
  23. package/src/cliArgs.mjs +45 -0
  24. package/src/cliFormatter.mjs +217 -0
  25. package/src/cliInteractive.mjs +739 -0
  26. package/src/config.d.ts +48 -0
  27. package/src/config.mjs +168 -0
  28. package/src/context/consumeInterruptMessage.mjs +30 -0
  29. package/src/context/loadAgentRoles.mjs +272 -0
  30. package/src/context/loadPrompts.mjs +312 -0
  31. package/src/context/loadUserMessageContext.mjs +147 -0
  32. package/src/env.mjs +46 -0
  33. package/src/main.mjs +202 -0
  34. package/src/mcp.mjs +202 -0
  35. package/src/model.d.ts +109 -0
  36. package/src/modelCaller.mjs +29 -0
  37. package/src/modelDefinition.d.ts +73 -0
  38. package/src/prompt.mjs +128 -0
  39. package/src/providers/anthropic.d.ts +248 -0
  40. package/src/providers/anthropic.mjs +596 -0
  41. package/src/providers/gemini.d.ts +208 -0
  42. package/src/providers/gemini.mjs +752 -0
  43. package/src/providers/openai.d.ts +281 -0
  44. package/src/providers/openai.mjs +551 -0
  45. package/src/providers/openaiCompatible.d.ts +147 -0
  46. package/src/providers/openaiCompatible.mjs +658 -0
  47. package/src/providers/platform/azure.mjs +42 -0
  48. package/src/providers/platform/bedrock.mjs +74 -0
  49. package/src/providers/platform/googleCloud.mjs +34 -0
  50. package/src/subagent.mjs +247 -0
  51. package/src/tmpfile.mjs +27 -0
  52. package/src/tool.d.ts +74 -0
  53. package/src/toolExecutor.mjs +236 -0
  54. package/src/toolInputValidator.mjs +183 -0
  55. package/src/toolUseApprover.mjs +98 -0
  56. package/src/tools/askGoogle.mjs +135 -0
  57. package/src/tools/delegateToSubagent.d.ts +4 -0
  58. package/src/tools/delegateToSubagent.mjs +48 -0
  59. package/src/tools/execCommand.d.ts +22 -0
  60. package/src/tools/execCommand.mjs +200 -0
  61. package/src/tools/fetchWebPage.mjs +96 -0
  62. package/src/tools/patchFile.d.ts +4 -0
  63. package/src/tools/patchFile.mjs +96 -0
  64. package/src/tools/reportAsSubagent.d.ts +3 -0
  65. package/src/tools/reportAsSubagent.mjs +44 -0
  66. package/src/tools/tavilySearch.d.ts +6 -0
  67. package/src/tools/tavilySearch.mjs +57 -0
  68. package/src/tools/tmuxCommand.d.ts +14 -0
  69. package/src/tools/tmuxCommand.mjs +194 -0
  70. package/src/tools/writeFile.d.ts +4 -0
  71. package/src/tools/writeFile.mjs +56 -0
  72. package/src/utils/evalJSONConfig.mjs +48 -0
  73. package/src/utils/matchValue.d.ts +6 -0
  74. package/src/utils/matchValue.mjs +40 -0
  75. package/src/utils/noThrow.mjs +31 -0
  76. package/src/utils/notify.mjs +28 -0
  77. package/src/utils/parseFileRange.mjs +18 -0
  78. package/src/utils/readFileRange.mjs +33 -0
  79. package/src/utils/retryOnError.mjs +41 -0
@@ -0,0 +1,752 @@
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(120 * 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
+ input:
208
+ content.usageMetadata.promptTokenCount -
209
+ (content.usageMetadata.cachedContentTokenCount ?? 0),
210
+ cached: content.usageMetadata.cachedContentTokenCount ?? 0,
211
+ output: content.usageMetadata.candidatesTokenCount ?? 0,
212
+ thought: content.usageMetadata.thoughtsTokenCount ?? 0,
213
+ total: content.usageMetadata.totalTokenCount,
214
+ };
215
+
216
+ const message = convertGeminiAssistantMessageToGenericFormat(content);
217
+ if (
218
+ message instanceof GeminiNoCandidateError ||
219
+ message instanceof GeminiMalformedFunctionCallError
220
+ ) {
221
+ const interval = Math.min(2 * 2 ** retryCount, 16);
222
+ console.error(
223
+ styleText(
224
+ "yellow",
225
+ `${message.name}: Retrying in ${interval} seconds...`,
226
+ ),
227
+ );
228
+ await new Promise((resolve) => setTimeout(resolve, interval * 1000));
229
+ return modelCaller(config, input, retryCount + 1);
230
+ }
231
+
232
+ // Create context cache for next request
233
+ if (
234
+ props.minCacheableTokenCount < content.usageMetadata.promptTokenCount
235
+ ) {
236
+ await updateCache({
237
+ contentsWithoutSystem: [
238
+ ...contentsWithoutSystem,
239
+ /** @type {GeminiModelContent} */ (
240
+ content.candidates?.at(0)?.content
241
+ ),
242
+ ],
243
+ systemInstruction,
244
+ tools,
245
+ toolConfig,
246
+ headers,
247
+ });
248
+ }
249
+
250
+ return {
251
+ message,
252
+ providerTokenUsage: tokenUsage,
253
+ };
254
+ });
255
+ }
256
+
257
+ /**
258
+ * @typedef {Object} UpdateCacheParams
259
+ * @property {(GeminiUserContent|GeminiModelContent|GeminiFunctionContent)[]} contentsWithoutSystem
260
+ * @property {GeminiSystemContent=} systemInstruction
261
+ * @property {GeminiToolDefinition[]=} tools
262
+ * @property {GeminiToolConfig=} toolConfig
263
+ * @property {Record<string,string>} headers
264
+ */
265
+
266
+ /**
267
+ * @param {UpdateCacheParams} params
268
+ */
269
+ async function updateCache({
270
+ contentsWithoutSystem,
271
+ systemInstruction,
272
+ tools,
273
+ toolConfig,
274
+ headers,
275
+ }) {
276
+ const modelPrefix =
277
+ platformConfig.name === "vertex-ai"
278
+ ? `${baseURL.match(/projects\/[^/]+\/locations\/[^/]+/)?.[0] || ""}/publishers/google/models`
279
+ : "models";
280
+
281
+ const url = `${baseURL}/cachedContents`;
282
+
283
+ /** @type {GeminiCreateCachedContentInput} */
284
+ const request = {
285
+ model: `${modelPrefix}/${modelConfig.model}`,
286
+ ttl: `${props.cacheTTL}s`,
287
+ system_instruction: systemInstruction,
288
+ contents: contentsWithoutSystem,
289
+ tools,
290
+ toolConfig,
291
+ };
292
+
293
+ await fetch(url, {
294
+ method: "POST",
295
+ headers: {
296
+ ...headers,
297
+ "Content-Type": "application/json",
298
+ },
299
+ body: JSON.stringify(request),
300
+ signal: AbortSignal.timeout(120 * 1000),
301
+ })
302
+ .then(async (response) => {
303
+ if (response.status !== 200) {
304
+ console.error(
305
+ styleText(
306
+ "yellow",
307
+ `Failed to create Gemini context cache: status=${response.status}, body=${await response.text()}`,
308
+ ),
309
+ );
310
+ } else {
311
+ /** @type {GeminiCachedContents} */
312
+ const cachedContents = await response.json();
313
+
314
+ // Delete old cache if previous cache is alive
315
+ if (state.cache && Date.now() < state.cache.expireTime.getTime()) {
316
+ fetch(
317
+ `${url}/${state.cache.name.replace(/.*cachedContents\//, "")}`,
318
+ {
319
+ method: "DELETE",
320
+ headers: {
321
+ ...headers,
322
+ "Content-Type": "application/json",
323
+ },
324
+ signal: AbortSignal.timeout(120 * 1000),
325
+ },
326
+ )
327
+ .then(async (response) => {
328
+ if (response.status !== 200) {
329
+ console.error(
330
+ styleText(
331
+ "yellow",
332
+ `Failed to delete Gemini context cache: status=${response.status}, body=${await response.text()}`,
333
+ ),
334
+ );
335
+ }
336
+ })
337
+ .catch((error) => {
338
+ console.error(
339
+ styleText(
340
+ "yellow",
341
+ `Failed to delete Gemini context cache: ${error}`,
342
+ ),
343
+ );
344
+ });
345
+ }
346
+
347
+ state.cache = {
348
+ name: cachedContents.name,
349
+ contentsLength: contentsWithoutSystem.length,
350
+ expireTime: new Date(cachedContents.expireTime),
351
+ };
352
+ }
353
+ })
354
+ .catch((error) => {
355
+ console.error(
356
+ styleText(
357
+ "yellow",
358
+ `Failed to create Gemini context cache: ${error}`,
359
+ ),
360
+ );
361
+ });
362
+ }
363
+
364
+ return modelCaller;
365
+ }
366
+
367
+ /**
368
+ * @param {Message[]} messages
369
+ * @returns {GeminiContent[]}
370
+ */
371
+ function convertGenericMessageToGeminiFormat(messages) {
372
+ /** @type {GeminiContent[]} */
373
+ const geminiContents = [];
374
+ for (const message of messages) {
375
+ switch (message.role) {
376
+ case "system": {
377
+ geminiContents.push({
378
+ role: "system",
379
+ parts: message.content.map((part) => ({
380
+ text: part.text,
381
+ })),
382
+ });
383
+ break;
384
+ }
385
+ case "user": {
386
+ const toolUseResults = message.content.filter(
387
+ (part) => part.type === "tool_result",
388
+ );
389
+ const userContentParts = message.content.filter(
390
+ (part) => part.type === "text" || part.type === "image",
391
+ );
392
+
393
+ if (toolUseResults.length) {
394
+ geminiContents.push({
395
+ role: "user",
396
+ parts: toolUseResults.map((toolResult) => ({
397
+ functionResponse: {
398
+ name: toolResult.toolName,
399
+ response: {
400
+ name: toolResult.toolName,
401
+ content: toolResult.content.map((part) => {
402
+ switch (part.type) {
403
+ case "text":
404
+ return { text: part.text };
405
+ case "image":
406
+ return {
407
+ inline_data: {
408
+ mime_type: part.mimeType,
409
+ data: part.data,
410
+ },
411
+ };
412
+ default:
413
+ throw new Error(
414
+ `Unsupported content part: ${JSON.stringify(part)}`,
415
+ );
416
+ }
417
+ }),
418
+ },
419
+ },
420
+ })),
421
+ });
422
+ }
423
+
424
+ if (userContentParts.length) {
425
+ geminiContents.push({
426
+ role: "user",
427
+ parts: userContentParts.map((part) => {
428
+ if (part.type === "text") {
429
+ return { text: part.text };
430
+ }
431
+ if (part.type === "image") {
432
+ return {
433
+ inline_data: {
434
+ mime_type: part.mimeType,
435
+ data: part.data,
436
+ },
437
+ };
438
+ }
439
+ throw new Error(
440
+ `Unsupported content part: ${JSON.stringify(part)}`,
441
+ );
442
+ }),
443
+ });
444
+ }
445
+
446
+ break;
447
+ }
448
+ case "assistant": {
449
+ /** @type {(GeminiContentPartText | GeminiContentPartFunctionCall)[]} */
450
+ const parts = [];
451
+ for (const part of message.content) {
452
+ if (part.type === "thinking") {
453
+ parts.push({
454
+ text: part.thinking,
455
+ thought: true,
456
+ ...(part.provider?.fields || {}),
457
+ });
458
+ } else if (part.type === "text") {
459
+ parts.push({
460
+ text: part.text,
461
+ ...(part.provider?.fields || {}),
462
+ });
463
+ } else if (part.type === "tool_use") {
464
+ parts.push({
465
+ functionCall: {
466
+ name: part.toolName,
467
+ args: part.input,
468
+ },
469
+ ...(part.provider?.fields || {}),
470
+ });
471
+ }
472
+ }
473
+ geminiContents.push({
474
+ role: "model",
475
+ parts,
476
+ });
477
+ break;
478
+ }
479
+ }
480
+ }
481
+
482
+ return geminiContents;
483
+ }
484
+
485
+ /**
486
+ * @param {ToolDefinition[]} tools
487
+ * @returns {GeminiToolDefinition[]}
488
+ */
489
+ function convertGenericToolDefinitionToGeminiFormat(tools) {
490
+ /** @type {GeminiToolDefinition["functionDeclarations"]} */
491
+ const functionDeclarations = [];
492
+ for (const tool of tools) {
493
+ functionDeclarations.push({
494
+ name: tool.name,
495
+ description: tool.description,
496
+ parametersJsonSchema: tool.inputSchema,
497
+ });
498
+ }
499
+
500
+ return [
501
+ {
502
+ functionDeclarations,
503
+ },
504
+ ];
505
+ }
506
+
507
+ /**
508
+ * @param {GeminiGeneratedContent} content
509
+ * @returns {AssistantMessage | GeminiNoCandidateError | GeminiMalformedFunctionCallError}
510
+ */
511
+ function convertGeminiAssistantMessageToGenericFormat(content) {
512
+ const candidate = content.candidates?.at(0);
513
+ if (!candidate) {
514
+ return new GeminiNoCandidateError(
515
+ `No candidates found: content=${JSON.stringify(content)}`,
516
+ );
517
+ }
518
+
519
+ if (candidate.finishReason === "MALFORMED_FUNCTION_CALL") {
520
+ return new GeminiMalformedFunctionCallError(
521
+ `Malformed function call: content=${JSON.stringify(content)}`,
522
+ );
523
+ }
524
+
525
+ /** @type {AssistantMessage["content"]} */
526
+ const assistantMessageContent = [];
527
+ for (const part of candidate.content.parts || []) {
528
+ if ("text" in part) {
529
+ if (part.thought) {
530
+ // thought summary
531
+ assistantMessageContent.push({
532
+ type: "thinking",
533
+ thinking: part.text,
534
+ });
535
+ } else {
536
+ assistantMessageContent.push({
537
+ type: "text",
538
+ text: part.text,
539
+ provider: part.thoughtSignature
540
+ ? { fields: { thoughtSignature: part.thoughtSignature } }
541
+ : undefined,
542
+ });
543
+ }
544
+ }
545
+ if ("functionCall" in part) {
546
+ assistantMessageContent.push({
547
+ type: "tool_use",
548
+ toolUseId: part.functionCall.name,
549
+ toolName: part.functionCall.name,
550
+ input: part.functionCall.args,
551
+ provider: part.thoughtSignature
552
+ ? { fields: { thoughtSignature: part.thoughtSignature } }
553
+ : undefined,
554
+ });
555
+ }
556
+ }
557
+
558
+ return {
559
+ role: "assistant",
560
+ content: assistantMessageContent,
561
+ };
562
+ }
563
+
564
+ /**
565
+ * @param {GeminiGeneratedContent[]} events
566
+ * @returns {GeminiGeneratedContent}
567
+ */
568
+ function convertGeminiStreamContentsToContent(events) {
569
+ const firstContent = events.at(0);
570
+ if (!firstContent) {
571
+ throw new Error("No content found");
572
+ }
573
+
574
+ /** @type {GeminiGeneratedContent} */
575
+ const mergedContent = {
576
+ ...firstContent,
577
+ // avoid side effects of mutating the original object
578
+ candidates: (firstContent.candidates || []).map((candidate) => ({
579
+ ...candidate,
580
+ content: {
581
+ ...candidate.content,
582
+ parts: [...(candidate.content.parts || [])],
583
+ },
584
+ })),
585
+ };
586
+
587
+ for (let i = 1; i < events.length; i++) {
588
+ const event = events[i];
589
+ if (event.candidates?.length) {
590
+ const candidate = event.candidates.at(0);
591
+ if (candidate?.content.parts?.length) {
592
+ mergedContent.candidates?.[0].content.parts?.push(
593
+ ...candidate.content.parts,
594
+ );
595
+ }
596
+ if (candidate?.finishReason && mergedContent.candidates?.[0]) {
597
+ mergedContent.candidates[0].finishReason = candidate.finishReason;
598
+ }
599
+ if (candidate?.finishMessage && mergedContent.candidates?.[0]) {
600
+ mergedContent.candidates[0].finishMessage = candidate.finishMessage;
601
+ }
602
+ }
603
+
604
+ if (event.usageMetadata.totalTokenCount) {
605
+ mergedContent.usageMetadata = event.usageMetadata;
606
+ }
607
+ }
608
+
609
+ return mergedContent;
610
+ }
611
+
612
+ /**
613
+ * @param {GeminiGeneratedContent} event
614
+ * @param {PartialMessageContent | undefined} previousPartialContent
615
+ * @returns {PartialMessageContent[]}
616
+ */
617
+ function convertGeminiStreamContentToAgentPartialContents(
618
+ event,
619
+ previousPartialContent,
620
+ ) {
621
+ const candiate = event.candidates?.at(0);
622
+ /** @type {PartialMessageContent[]} */
623
+ const partialMessageContents = [];
624
+ if (candiate?.content.parts?.length) {
625
+ /** @type {string | undefined} */
626
+ let previousPartType = previousPartialContent?.type;
627
+ for (const part of candiate.content.parts) {
628
+ const partType =
629
+ "text" in part ? (part.thought ? "thinking" : "text") : "tool_use";
630
+
631
+ if (previousPartType && previousPartType !== partType) {
632
+ partialMessageContents.push({
633
+ type: previousPartType,
634
+ position: "stop",
635
+ });
636
+ }
637
+
638
+ if (previousPartType !== partType) {
639
+ previousPartType = partType;
640
+ partialMessageContents.push({
641
+ type: partType,
642
+ position: "start",
643
+ });
644
+ }
645
+
646
+ if ("text" in part) {
647
+ if (part.thought) {
648
+ partialMessageContents.push({
649
+ type: "thinking",
650
+ content: part.text,
651
+ position: "delta",
652
+ });
653
+ } else {
654
+ partialMessageContents.push({
655
+ type: "text",
656
+ content: part.text,
657
+ position: "delta",
658
+ });
659
+ }
660
+ }
661
+
662
+ if ("functionCall" in part) {
663
+ if (previousPartialContent?.type === "tool_use") {
664
+ partialMessageContents.push({
665
+ type: "tool_use",
666
+ content: "\n",
667
+ position: "delta",
668
+ });
669
+ }
670
+ partialMessageContents.push({
671
+ type: "tool_use",
672
+ content: part.functionCall.name,
673
+ position: "delta",
674
+ });
675
+ }
676
+ }
677
+ }
678
+
679
+ return partialMessageContents;
680
+ }
681
+
682
+ class GeminiNoCandidateError extends Error {
683
+ /**
684
+ * @param {string} message
685
+ */
686
+ constructor(message) {
687
+ super(message);
688
+ this.name = "GeminiNoCandidateError";
689
+ }
690
+ }
691
+
692
+ class GeminiMalformedFunctionCallError extends Error {
693
+ /**
694
+ * @param {string} message
695
+ */
696
+ constructor(message) {
697
+ super(message);
698
+ this.name = "GeminiMalformedFunctionCallError";
699
+ }
700
+ }
701
+
702
+ /**
703
+ * @param {ReadableStreamDefaultReader<Uint8Array>} reader
704
+ * @returns {AsyncGenerator<GeminiGeneratedContent>}
705
+ */
706
+ async function* readGeminiStreamContents(reader) {
707
+ let buffer = new Uint8Array();
708
+
709
+ while (true) {
710
+ const { done, value } = await reader.read();
711
+ if (done) {
712
+ break;
713
+ }
714
+
715
+ const nextBuffer = new Uint8Array(buffer.length + value.length);
716
+ nextBuffer.set(buffer);
717
+ nextBuffer.set(value, buffer.length);
718
+ buffer = nextBuffer;
719
+
720
+ const carriageReturn = "\r".charCodeAt(0);
721
+ const lineFeed = "\n".charCodeAt(0);
722
+
723
+ const dataEndIndices = [];
724
+ for (let i = 0; i < buffer.length - 3; i++) {
725
+ if (
726
+ buffer[i] === carriageReturn &&
727
+ buffer[i + 1] === lineFeed &&
728
+ buffer[i + 2] === carriageReturn &&
729
+ buffer[i + 3] === lineFeed
730
+ ) {
731
+ dataEndIndices.push(i);
732
+ }
733
+ }
734
+
735
+ for (let i = 0; i < dataEndIndices.length; i++) {
736
+ const dataStartIndex = i === 0 ? 0 : dataEndIndices[i - 1] + 4;
737
+ const dataEndIndex = dataEndIndices[i];
738
+ const data = buffer.slice(dataStartIndex, dataEndIndex);
739
+ const decodedData = new TextDecoder().decode(data);
740
+
741
+ if (decodedData.startsWith("data: {")) {
742
+ /** @type {GeminiGeneratedContent} */
743
+ const parsedData = JSON.parse(decodedData.slice("data: ".length));
744
+ yield parsedData;
745
+ }
746
+ }
747
+
748
+ if (dataEndIndices.length) {
749
+ buffer = buffer.slice(dataEndIndices[dataEndIndices.length - 1] + 4);
750
+ }
751
+ }
752
+ }