@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.
- package/.config/agents.library/code-simplifier.md +5 -0
- package/.config/agents.library/qa-engineer.md +74 -0
- package/.config/agents.library/software-architect.md +278 -0
- package/.config/agents.predefined/worker.md +3 -0
- package/.config/config.predefined.json +825 -0
- package/.config/prompts.library/code-review.md +8 -0
- package/.config/prompts.library/feature-dev.md +6 -0
- package/.config/prompts.predefined/shortcuts/commit-by-user.md +9 -0
- package/.config/prompts.predefined/shortcuts/commit.md +10 -0
- package/.config/prompts.predefined/shortcuts/general-question.md +6 -0
- package/LICENSE +21 -0
- package/README.md +624 -0
- package/bin/plain +3 -0
- package/bin/plain-interrupt +6 -0
- package/bin/plain-notify-desktop +19 -0
- package/bin/plain-notify-terminal-bell +3 -0
- package/package.json +57 -0
- package/sandbox/bin/plain-sandbox +972 -0
- package/src/agent.d.ts +48 -0
- package/src/agent.mjs +159 -0
- package/src/agentLoop.mjs +369 -0
- package/src/agentState.mjs +41 -0
- package/src/cliArgs.mjs +45 -0
- package/src/cliFormatter.mjs +217 -0
- package/src/cliInteractive.mjs +739 -0
- package/src/config.d.ts +48 -0
- package/src/config.mjs +168 -0
- package/src/context/consumeInterruptMessage.mjs +30 -0
- package/src/context/loadAgentRoles.mjs +272 -0
- package/src/context/loadPrompts.mjs +312 -0
- package/src/context/loadUserMessageContext.mjs +147 -0
- package/src/env.mjs +46 -0
- package/src/main.mjs +202 -0
- package/src/mcp.mjs +202 -0
- package/src/model.d.ts +109 -0
- package/src/modelCaller.mjs +29 -0
- package/src/modelDefinition.d.ts +73 -0
- package/src/prompt.mjs +128 -0
- package/src/providers/anthropic.d.ts +248 -0
- package/src/providers/anthropic.mjs +596 -0
- package/src/providers/gemini.d.ts +208 -0
- package/src/providers/gemini.mjs +752 -0
- package/src/providers/openai.d.ts +281 -0
- package/src/providers/openai.mjs +551 -0
- package/src/providers/openaiCompatible.d.ts +147 -0
- package/src/providers/openaiCompatible.mjs +658 -0
- package/src/providers/platform/azure.mjs +42 -0
- package/src/providers/platform/bedrock.mjs +74 -0
- package/src/providers/platform/googleCloud.mjs +34 -0
- package/src/subagent.mjs +247 -0
- package/src/tmpfile.mjs +27 -0
- package/src/tool.d.ts +74 -0
- package/src/toolExecutor.mjs +236 -0
- package/src/toolInputValidator.mjs +183 -0
- package/src/toolUseApprover.mjs +98 -0
- package/src/tools/askGoogle.mjs +135 -0
- package/src/tools/delegateToSubagent.d.ts +4 -0
- package/src/tools/delegateToSubagent.mjs +48 -0
- package/src/tools/execCommand.d.ts +22 -0
- package/src/tools/execCommand.mjs +200 -0
- package/src/tools/fetchWebPage.mjs +96 -0
- package/src/tools/patchFile.d.ts +4 -0
- package/src/tools/patchFile.mjs +96 -0
- package/src/tools/reportAsSubagent.d.ts +3 -0
- package/src/tools/reportAsSubagent.mjs +44 -0
- package/src/tools/tavilySearch.d.ts +6 -0
- package/src/tools/tavilySearch.mjs +57 -0
- package/src/tools/tmuxCommand.d.ts +14 -0
- package/src/tools/tmuxCommand.mjs +194 -0
- package/src/tools/writeFile.d.ts +4 -0
- package/src/tools/writeFile.mjs +56 -0
- package/src/utils/evalJSONConfig.mjs +48 -0
- package/src/utils/matchValue.d.ts +6 -0
- package/src/utils/matchValue.mjs +40 -0
- package/src/utils/noThrow.mjs +31 -0
- package/src/utils/notify.mjs +28 -0
- package/src/utils/parseFileRange.mjs +18 -0
- package/src/utils/readFileRange.mjs +33 -0
- 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
|
+
}
|