@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,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { ModelInput, Message, AssistantMessage, ModelOutput, PartialMessageContent } from "../model";
|
|
3
|
+
* @import { AnthropicChatCompletion, AnthropicMessage, AnthropicToolDefinition, AnthropicModelConfig, AnthropicAssistantMessage, AnthropicStreamEvent, AnthropicAssistantMessageContent, AnthropicChatCompletionUsage, AnthropicRequestInput } from "./anthropic";
|
|
4
|
+
* @import { ToolDefinition } from "../tool";
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { styleText } from "node:util";
|
|
8
|
+
import { Sha256 } from "@aws-crypto/sha256-js";
|
|
9
|
+
import { fromIni } from "@aws-sdk/credential-providers";
|
|
10
|
+
import { HttpRequest } from "@smithy/protocol-http";
|
|
11
|
+
import { SignatureV4 } from "@smithy/signature-v4";
|
|
12
|
+
import { noThrow } from "../utils/noThrow.mjs";
|
|
13
|
+
import { readBedrockStreamEvents } from "./platform/bedrock.mjs";
|
|
14
|
+
import { getGoogleCloudAccessToken } from "./platform/googleCloud.mjs";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {import("../modelDefinition").PlatformConfig} platformConfig
|
|
18
|
+
* @param {AnthropicModelConfig} modelConfig
|
|
19
|
+
* @param {ModelInput} input
|
|
20
|
+
* @param {number} [retryCount]
|
|
21
|
+
* @returns {Promise<ModelOutput | Error>}
|
|
22
|
+
*/
|
|
23
|
+
export async function callAnthropicModel(
|
|
24
|
+
platformConfig,
|
|
25
|
+
modelConfig,
|
|
26
|
+
input,
|
|
27
|
+
retryCount = 0,
|
|
28
|
+
) {
|
|
29
|
+
return await noThrow(async () => {
|
|
30
|
+
const messages = convertGenericMessageToAnthropicFormat(input.messages);
|
|
31
|
+
const cacheEnabledMessages = enableContextCaching(messages);
|
|
32
|
+
const tools = convertGenericToolDefinitionToAnthropicFormat(
|
|
33
|
+
input.tools || [],
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const url = (() => {
|
|
37
|
+
const baseURL = platformConfig.baseURL;
|
|
38
|
+
|
|
39
|
+
switch (platformConfig.name) {
|
|
40
|
+
case "anthropic":
|
|
41
|
+
return `${baseURL}/v1/messages`;
|
|
42
|
+
case "bedrock":
|
|
43
|
+
return `${baseURL}/model/${modelConfig.model}/invoke-with-response-stream`;
|
|
44
|
+
case "vertex-ai":
|
|
45
|
+
return `${baseURL}/publishers/anthropic/models/${modelConfig.model}:streamRawPredict`;
|
|
46
|
+
default:
|
|
47
|
+
throw new Error(`Unsupported platform: ${platformConfig.name}`);
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
|
|
51
|
+
/** @type {Record<string,string>} */
|
|
52
|
+
const headers = await (async () => {
|
|
53
|
+
switch (platformConfig.name) {
|
|
54
|
+
case "anthropic":
|
|
55
|
+
return {
|
|
56
|
+
...platformConfig.customHeaders,
|
|
57
|
+
"anthropic-version": "2023-06-01",
|
|
58
|
+
"x-api-key": `${platformConfig.apiKey}`,
|
|
59
|
+
};
|
|
60
|
+
case "bedrock":
|
|
61
|
+
return platformConfig.customHeaders ?? {};
|
|
62
|
+
case "vertex-ai":
|
|
63
|
+
return {
|
|
64
|
+
...platformConfig.customHeaders,
|
|
65
|
+
Authorization: `Bearer ${await getGoogleCloudAccessToken()}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
|
|
70
|
+
const { model: _, ...modelConfigWithoutName } = modelConfig;
|
|
71
|
+
const platformRequest = (() => {
|
|
72
|
+
switch (platformConfig.name) {
|
|
73
|
+
case "anthropic":
|
|
74
|
+
return {
|
|
75
|
+
...modelConfig,
|
|
76
|
+
stream: true,
|
|
77
|
+
};
|
|
78
|
+
case "bedrock":
|
|
79
|
+
return {
|
|
80
|
+
anthropic_version: "bedrock-2023-05-31",
|
|
81
|
+
...modelConfigWithoutName,
|
|
82
|
+
};
|
|
83
|
+
case "vertex-ai":
|
|
84
|
+
return {
|
|
85
|
+
anthropic_version: "vertex-2023-10-16",
|
|
86
|
+
stream: true,
|
|
87
|
+
...modelConfigWithoutName,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
|
|
92
|
+
/** @type {AnthropicRequestInput} */
|
|
93
|
+
const request = {
|
|
94
|
+
...platformRequest,
|
|
95
|
+
system: messages
|
|
96
|
+
.filter((m) => m.role === "system")
|
|
97
|
+
.flatMap((m) => m.content),
|
|
98
|
+
messages: cacheEnabledMessages.filter((m) => m.role !== "system"),
|
|
99
|
+
tools: tools.length ? tools : undefined,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const runFetchDefault = async () =>
|
|
103
|
+
fetch(url, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: {
|
|
106
|
+
...headers,
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify(request),
|
|
110
|
+
signal: AbortSignal.timeout(4 * 60 * 1000),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// bedrock + sso profile
|
|
114
|
+
const runFetchForBedrock = async () => {
|
|
115
|
+
const region =
|
|
116
|
+
url.match(/bedrock-runtime\.([\w-]+)\.amazonaws\.com/)?.[1] ?? "";
|
|
117
|
+
const urlParsed = new URL(url);
|
|
118
|
+
const { hostname, pathname } = urlParsed;
|
|
119
|
+
|
|
120
|
+
const signer = new SignatureV4({
|
|
121
|
+
credentials: fromIni({
|
|
122
|
+
profile:
|
|
123
|
+
platformConfig.name === "bedrock" ? platformConfig.awsProfile : "",
|
|
124
|
+
}),
|
|
125
|
+
region,
|
|
126
|
+
service: "bedrock",
|
|
127
|
+
sha256: Sha256,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const req = new HttpRequest({
|
|
131
|
+
protocol: "https:",
|
|
132
|
+
method: "POST",
|
|
133
|
+
hostname,
|
|
134
|
+
path: pathname,
|
|
135
|
+
headers: {
|
|
136
|
+
host: hostname,
|
|
137
|
+
"Content-Type": "application/json",
|
|
138
|
+
},
|
|
139
|
+
body: JSON.stringify(request),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const signed = await signer.sign(req);
|
|
143
|
+
|
|
144
|
+
return fetch(url, {
|
|
145
|
+
method: signed.method,
|
|
146
|
+
headers: signed.headers,
|
|
147
|
+
body: signed.body,
|
|
148
|
+
signal: AbortSignal.timeout(4 * 60 * 1000),
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const runFetch =
|
|
153
|
+
platformConfig.name === "bedrock" ? runFetchForBedrock : runFetchDefault;
|
|
154
|
+
|
|
155
|
+
const response = await runFetch();
|
|
156
|
+
|
|
157
|
+
if (response.status === 429 || response.status >= 500) {
|
|
158
|
+
const interval = Math.min(2 * 2 ** retryCount, 16);
|
|
159
|
+
console.error(
|
|
160
|
+
styleText(
|
|
161
|
+
"yellow",
|
|
162
|
+
`Anthropic rate limit exceeded. Retry in ${interval} seconds...`,
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
166
|
+
return callAnthropicModel(
|
|
167
|
+
platformConfig,
|
|
168
|
+
modelConfig,
|
|
169
|
+
input,
|
|
170
|
+
retryCount + 1,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (response.status !== 200) {
|
|
175
|
+
return new Error(
|
|
176
|
+
`Failed to call Anthropic model: status=${response.status}, body=${await response.text()}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!response.body) {
|
|
181
|
+
throw new Error("Response body is empty");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const reader = response.body.getReader();
|
|
185
|
+
const eventStreamReader =
|
|
186
|
+
platformConfig.name === "bedrock"
|
|
187
|
+
? /** @type {typeof readAnthropicStreamEvents} */ (
|
|
188
|
+
readBedrockStreamEvents
|
|
189
|
+
)
|
|
190
|
+
: readAnthropicStreamEvents;
|
|
191
|
+
|
|
192
|
+
/** @type {AnthropicStreamEvent[]} */
|
|
193
|
+
const events = [];
|
|
194
|
+
/** @type {PartialMessageContent | undefined} */
|
|
195
|
+
let previousPartialContent;
|
|
196
|
+
for await (const event of eventStreamReader(reader)) {
|
|
197
|
+
events.push(event);
|
|
198
|
+
|
|
199
|
+
const partialContent = convertAnthropicStreamEventToAgentPartialContent(
|
|
200
|
+
event,
|
|
201
|
+
previousPartialContent,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
if (partialContent) {
|
|
205
|
+
previousPartialContent = partialContent;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (input.onPartialMessageContent && partialContent) {
|
|
209
|
+
input.onPartialMessageContent(partialContent);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** @type {AnthropicChatCompletion} */
|
|
214
|
+
const chatCompletion = convertAnthropicStreamEventsToChatCompletion(events);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
message: convertAnthropicAssistantMessageToGenericFormat(chatCompletion),
|
|
218
|
+
providerTokenUsage: chatCompletion.usage,
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {Message[]} genericMessages
|
|
225
|
+
* @returns {AnthropicMessage[]}
|
|
226
|
+
*/
|
|
227
|
+
function convertGenericMessageToAnthropicFormat(genericMessages) {
|
|
228
|
+
/** @type {AnthropicMessage[]} */
|
|
229
|
+
const anthropicMessages = [];
|
|
230
|
+
for (const genericMessage of genericMessages) {
|
|
231
|
+
switch (genericMessage.role) {
|
|
232
|
+
case "system": {
|
|
233
|
+
anthropicMessages.push({
|
|
234
|
+
role: "system",
|
|
235
|
+
content: genericMessage.content.map((part) => {
|
|
236
|
+
if (part.type === "text") {
|
|
237
|
+
return { type: "text", text: part.text };
|
|
238
|
+
}
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Unsupported content part: ${JSON.stringify(part)}`,
|
|
241
|
+
);
|
|
242
|
+
}),
|
|
243
|
+
});
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
case "user": {
|
|
247
|
+
anthropicMessages.push({
|
|
248
|
+
role: "user",
|
|
249
|
+
content: genericMessage.content.map((part) => {
|
|
250
|
+
if (part.type === "text") {
|
|
251
|
+
return { type: "text", text: part.text };
|
|
252
|
+
}
|
|
253
|
+
if (part.type === "image") {
|
|
254
|
+
return {
|
|
255
|
+
type: "image",
|
|
256
|
+
source: {
|
|
257
|
+
type: "base64",
|
|
258
|
+
media_type: part.mimeType,
|
|
259
|
+
data: part.data,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
if (part.type === "tool_result") {
|
|
264
|
+
return {
|
|
265
|
+
type: "tool_result",
|
|
266
|
+
tool_use_id: part.toolUseId,
|
|
267
|
+
content: part.content.map((contentPart) => {
|
|
268
|
+
switch (contentPart.type) {
|
|
269
|
+
case "text":
|
|
270
|
+
return { type: "text", text: contentPart.text };
|
|
271
|
+
case "image":
|
|
272
|
+
return {
|
|
273
|
+
type: "image",
|
|
274
|
+
source: {
|
|
275
|
+
type: "base64",
|
|
276
|
+
media_type: contentPart.mimeType,
|
|
277
|
+
data: contentPart.data,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
default:
|
|
281
|
+
throw new Error(
|
|
282
|
+
`Unsupported content part: ${JSON.stringify(contentPart)}`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}),
|
|
286
|
+
is_error: part.isError,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
throw new Error(
|
|
290
|
+
`Unsupported content part: ${JSON.stringify(part)}`,
|
|
291
|
+
);
|
|
292
|
+
}),
|
|
293
|
+
});
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
case "assistant": {
|
|
297
|
+
anthropicMessages.push({
|
|
298
|
+
role: "assistant",
|
|
299
|
+
content: genericMessage.content.map((part) => {
|
|
300
|
+
if (part.type === "thinking") {
|
|
301
|
+
const signature = /** @type {string} */ (
|
|
302
|
+
part.provider?.fields?.signature
|
|
303
|
+
);
|
|
304
|
+
return {
|
|
305
|
+
type: "thinking",
|
|
306
|
+
thinking: part.thinking,
|
|
307
|
+
signature,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
if (part.type === "redacted_thinking") {
|
|
311
|
+
const data = /** @type {string} */ (part.provider?.fields?.data);
|
|
312
|
+
return {
|
|
313
|
+
type: "redacted_thinking",
|
|
314
|
+
data,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (part.type === "text") {
|
|
318
|
+
return { type: "text", text: part.text };
|
|
319
|
+
}
|
|
320
|
+
if (part.type === "tool_use") {
|
|
321
|
+
return {
|
|
322
|
+
type: "tool_use",
|
|
323
|
+
id: part.toolUseId,
|
|
324
|
+
name: part.toolName,
|
|
325
|
+
input: part.input,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
throw new Error(`Unknown message part type: ${part}`);
|
|
329
|
+
}),
|
|
330
|
+
});
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return anthropicMessages;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* @param {ToolDefinition[]} genericToolDefs
|
|
341
|
+
* @returns {AnthropicToolDefinition[]}
|
|
342
|
+
*/
|
|
343
|
+
function convertGenericToolDefinitionToAnthropicFormat(genericToolDefs) {
|
|
344
|
+
/** @type {AnthropicToolDefinition[]} */
|
|
345
|
+
const anthropicToolDefs = [];
|
|
346
|
+
for (const tool of genericToolDefs) {
|
|
347
|
+
anthropicToolDefs.push({
|
|
348
|
+
name: tool.name,
|
|
349
|
+
description: tool.description,
|
|
350
|
+
input_schema: tool.inputSchema,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return anthropicToolDefs;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* @param {AnthropicAssistantMessage} anthropicAssistantMessage
|
|
358
|
+
* @returns {AssistantMessage}
|
|
359
|
+
*/
|
|
360
|
+
function convertAnthropicAssistantMessageToGenericFormat(
|
|
361
|
+
anthropicAssistantMessage,
|
|
362
|
+
) {
|
|
363
|
+
/** @type {AssistantMessage["content"]} */
|
|
364
|
+
const content = [];
|
|
365
|
+
for (const part of anthropicAssistantMessage.content) {
|
|
366
|
+
if (part.type === "thinking") {
|
|
367
|
+
content.push({
|
|
368
|
+
type: "thinking",
|
|
369
|
+
thinking: part.thinking,
|
|
370
|
+
provider: { fields: { signature: part.signature } },
|
|
371
|
+
});
|
|
372
|
+
} else if (part.type === "redacted_thinking") {
|
|
373
|
+
content.push({
|
|
374
|
+
type: "redacted_thinking",
|
|
375
|
+
provider: { fields: { data: part.data } },
|
|
376
|
+
});
|
|
377
|
+
} else if (part.type === "text") {
|
|
378
|
+
content.push({ type: "text", text: part.text });
|
|
379
|
+
} else if (part.type === "tool_use") {
|
|
380
|
+
content.push({
|
|
381
|
+
type: "tool_use",
|
|
382
|
+
toolUseId: part.id,
|
|
383
|
+
toolName: part.name,
|
|
384
|
+
input: part.input,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
role: "assistant",
|
|
391
|
+
content,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* @param {AnthropicStreamEvent[]} events
|
|
397
|
+
* @returns {AnthropicChatCompletion}
|
|
398
|
+
*/
|
|
399
|
+
function convertAnthropicStreamEventsToChatCompletion(events) {
|
|
400
|
+
/** @type {Partial<AnthropicChatCompletion>} */
|
|
401
|
+
let chatCompletion = {};
|
|
402
|
+
/** @type {string[]} */
|
|
403
|
+
const toolUseInputJsonBuffer = [];
|
|
404
|
+
for (const event of events) {
|
|
405
|
+
if (event.type === "message_start") {
|
|
406
|
+
chatCompletion = Object.assign(chatCompletion, event.message);
|
|
407
|
+
} else if (event.type === "message_delta") {
|
|
408
|
+
Object.assign(chatCompletion, event.delta);
|
|
409
|
+
if (event.usage?.output_tokens) {
|
|
410
|
+
const usage = /** @type {AnthropicChatCompletionUsage} */ (
|
|
411
|
+
chatCompletion.usage || {}
|
|
412
|
+
);
|
|
413
|
+
usage.output_tokens += event.usage.output_tokens;
|
|
414
|
+
chatCompletion.usage = usage;
|
|
415
|
+
}
|
|
416
|
+
} else if (event.type === "content_block_start") {
|
|
417
|
+
chatCompletion.content = chatCompletion.content || [];
|
|
418
|
+
chatCompletion.content.push(
|
|
419
|
+
/** @type {AnthropicAssistantMessageContent} */ (event.content_block),
|
|
420
|
+
);
|
|
421
|
+
} else if (event.type === "content_block_delta") {
|
|
422
|
+
const lastContentPart = chatCompletion.content?.at(-1);
|
|
423
|
+
if (lastContentPart) {
|
|
424
|
+
switch (event.delta.type) {
|
|
425
|
+
case "text_delta": {
|
|
426
|
+
if (lastContentPart.type === "text") {
|
|
427
|
+
lastContentPart.text = lastContentPart.text + event.delta.text;
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
case "thinking_delta": {
|
|
432
|
+
if (lastContentPart.type === "thinking") {
|
|
433
|
+
lastContentPart.thinking =
|
|
434
|
+
lastContentPart.thinking + event.delta.thinking;
|
|
435
|
+
}
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
case "signature_delta": {
|
|
439
|
+
if (lastContentPart.type === "thinking") {
|
|
440
|
+
lastContentPart.signature = event.delta.signature;
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
case "input_json_delta": {
|
|
445
|
+
if (lastContentPart.type === "tool_use") {
|
|
446
|
+
toolUseInputJsonBuffer.push(event.delta.partial_json);
|
|
447
|
+
}
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
} else {
|
|
452
|
+
console.error(
|
|
453
|
+
`Received content block delta without a content block: ${JSON.stringify(event)}`,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
} else if (event.type === "content_block_stop") {
|
|
457
|
+
const lastContentPart = chatCompletion.content?.at(-1);
|
|
458
|
+
if (lastContentPart?.type === "tool_use") {
|
|
459
|
+
lastContentPart.input = JSON.parse(
|
|
460
|
+
toolUseInputJsonBuffer.join("") || "{}",
|
|
461
|
+
);
|
|
462
|
+
toolUseInputJsonBuffer.length = 0;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return /** @type {AnthropicChatCompletion} */ (chatCompletion);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* @param {AnthropicStreamEvent} event
|
|
472
|
+
* @param {PartialMessageContent | undefined} previousPartialContent
|
|
473
|
+
* @returns {PartialMessageContent | undefined}
|
|
474
|
+
*/
|
|
475
|
+
function convertAnthropicStreamEventToAgentPartialContent(
|
|
476
|
+
event,
|
|
477
|
+
previousPartialContent,
|
|
478
|
+
) {
|
|
479
|
+
switch (event.type) {
|
|
480
|
+
case "content_block_start":
|
|
481
|
+
return {
|
|
482
|
+
type: event.content_block.type,
|
|
483
|
+
position: "start",
|
|
484
|
+
};
|
|
485
|
+
case "content_block_delta":
|
|
486
|
+
switch (event.delta.type) {
|
|
487
|
+
case "text_delta":
|
|
488
|
+
return {
|
|
489
|
+
type: "text",
|
|
490
|
+
content: event.delta.text,
|
|
491
|
+
position: "delta",
|
|
492
|
+
};
|
|
493
|
+
case "thinking_delta":
|
|
494
|
+
return {
|
|
495
|
+
type: "thinking",
|
|
496
|
+
content: event.delta.thinking,
|
|
497
|
+
position: "delta",
|
|
498
|
+
};
|
|
499
|
+
case "input_json_delta":
|
|
500
|
+
return {
|
|
501
|
+
type: "tool_use",
|
|
502
|
+
content: event.delta.partial_json,
|
|
503
|
+
position: "delta",
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
break;
|
|
507
|
+
case "content_block_stop":
|
|
508
|
+
return {
|
|
509
|
+
type: previousPartialContent?.type || "unknown",
|
|
510
|
+
position: "stop",
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* @param {AnthropicMessage[]} messages
|
|
517
|
+
* @returns {AnthropicMessage[]}
|
|
518
|
+
*/
|
|
519
|
+
function enableContextCaching(messages) {
|
|
520
|
+
/** @type {number[]} */
|
|
521
|
+
const userMessageIndices = [];
|
|
522
|
+
for (let i = 0; i < messages.length; i++) {
|
|
523
|
+
if (messages[i].role === "user") {
|
|
524
|
+
userMessageIndices.push(i);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const cacheTargetIndices = [
|
|
528
|
+
// last user message
|
|
529
|
+
userMessageIndices.at(-1),
|
|
530
|
+
// second last user message
|
|
531
|
+
userMessageIndices.at(-2),
|
|
532
|
+
].filter((index) => index !== undefined);
|
|
533
|
+
|
|
534
|
+
const contextCachingEnabledMessages = messages.map((message, index) => {
|
|
535
|
+
if (
|
|
536
|
+
(index === 0 && message.role === "system") ||
|
|
537
|
+
cacheTargetIndices.includes(index)
|
|
538
|
+
) {
|
|
539
|
+
return {
|
|
540
|
+
...message,
|
|
541
|
+
content: message.content.map((part, partIndex) =>
|
|
542
|
+
partIndex === message.content.length - 1
|
|
543
|
+
? { ...part, cache_control: { type: "ephemeral" } }
|
|
544
|
+
: part,
|
|
545
|
+
),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
return message;
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
return /** @type {AnthropicMessage[]} */ (contextCachingEnabledMessages);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* @param {ReadableStreamDefaultReader<Uint8Array>} reader
|
|
556
|
+
*/
|
|
557
|
+
async function* readAnthropicStreamEvents(reader) {
|
|
558
|
+
let buffer = new Uint8Array();
|
|
559
|
+
|
|
560
|
+
while (true) {
|
|
561
|
+
const { done, value } = await reader.read();
|
|
562
|
+
if (done) {
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const nextBuffer = new Uint8Array(buffer.length + value.length);
|
|
567
|
+
nextBuffer.set(buffer);
|
|
568
|
+
nextBuffer.set(value, buffer.length);
|
|
569
|
+
buffer = nextBuffer;
|
|
570
|
+
|
|
571
|
+
const lineFeed = "\n".charCodeAt(0);
|
|
572
|
+
const eventEndIndices = [];
|
|
573
|
+
for (let i = 0; i < buffer.length - 1; i++) {
|
|
574
|
+
if (buffer[i] === lineFeed && buffer[i + 1] === lineFeed) {
|
|
575
|
+
eventEndIndices.push(i);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
for (let i = 0; i < eventEndIndices.length; i++) {
|
|
580
|
+
const eventStartIndex = i === 0 ? 0 : eventEndIndices[i - 1] + 2;
|
|
581
|
+
const eventEndIndex = eventEndIndices[i];
|
|
582
|
+
const event = buffer.slice(eventStartIndex, eventEndIndex);
|
|
583
|
+
const decodedEvent = new TextDecoder().decode(event);
|
|
584
|
+
const data = decodedEvent.split("\n").at(-1);
|
|
585
|
+
if (data?.startsWith("data: ")) {
|
|
586
|
+
/** @type {AnthropicStreamEvent} */
|
|
587
|
+
const parsedData = JSON.parse(data.slice("data: ".length));
|
|
588
|
+
yield parsedData;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (eventEndIndices.length) {
|
|
593
|
+
buffer = buffer.slice(eventEndIndices[eventEndIndices.length - 1] + 2);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|