@browser-ai/core 2.0.3 → 2.0.4
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/dist/index.js +464 -415
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +466 -417
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,79 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
1
|
+
// ../shared/src/utils/tool-utils.ts
|
|
2
|
+
function isFunctionTool(tool) {
|
|
3
|
+
return tool.type === "function";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// ../shared/src/utils/warnings.ts
|
|
7
|
+
function createUnsupportedSettingWarning(feature, details) {
|
|
8
|
+
return {
|
|
9
|
+
type: "unsupported",
|
|
10
|
+
feature,
|
|
11
|
+
details
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function createUnsupportedToolWarning(tool, details) {
|
|
15
|
+
return {
|
|
16
|
+
type: "unsupported",
|
|
17
|
+
feature: `tool:${tool.name}`,
|
|
18
|
+
details
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ../shared/src/tool-calling/build-json-system-prompt.ts
|
|
23
|
+
function buildJsonToolSystemPrompt(originalSystemPrompt, tools, options) {
|
|
24
|
+
if (!tools || tools.length === 0) {
|
|
25
|
+
return originalSystemPrompt || "";
|
|
26
|
+
}
|
|
27
|
+
const parallelInstruction = "Only request one tool call at a time. Wait for tool results before asking for another tool.";
|
|
28
|
+
const toolSchemas = tools.map((tool) => {
|
|
29
|
+
const schema = getParameters(tool);
|
|
30
|
+
return {
|
|
31
|
+
name: tool.name,
|
|
32
|
+
description: tool.description ?? "No description provided.",
|
|
33
|
+
parameters: schema || { type: "object", properties: {} }
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
const toolsJson = JSON.stringify(toolSchemas, null, 2);
|
|
37
|
+
const instructionBody = `You are a helpful AI assistant with access to tools.
|
|
38
|
+
|
|
39
|
+
# Available Tools
|
|
40
|
+
${toolsJson}
|
|
41
|
+
|
|
42
|
+
# Tool Calling Instructions
|
|
43
|
+
${parallelInstruction}
|
|
44
|
+
|
|
45
|
+
To call a tool, output JSON in this exact format inside a \`\`\`tool_call code fence:
|
|
46
|
+
|
|
47
|
+
\`\`\`tool_call
|
|
48
|
+
{"name": "tool_name", "arguments": {"param1": "value1", "param2": "value2"}}
|
|
49
|
+
\`\`\`
|
|
50
|
+
|
|
51
|
+
Tool responses will be provided in \`\`\`tool_result fences. Each line contains JSON like:
|
|
52
|
+
\`\`\`tool_result
|
|
53
|
+
{"id": "call_123", "name": "tool_name", "result": {...}, "error": false}
|
|
54
|
+
\`\`\`
|
|
55
|
+
Use the \`result\` payload (and treat \`error\` as a boolean flag) when continuing the conversation.
|
|
5
56
|
|
|
6
|
-
|
|
57
|
+
Important:
|
|
58
|
+
- Use exact tool and parameter names from the schema above
|
|
59
|
+
- Arguments must be a valid JSON object matching the tool's parameters
|
|
60
|
+
- You can include brief reasoning before or after the tool call
|
|
61
|
+
- If no tool is needed, respond directly without tool_call fences`;
|
|
62
|
+
if (originalSystemPrompt?.trim()) {
|
|
63
|
+
return `${originalSystemPrompt.trim()}
|
|
64
|
+
|
|
65
|
+
${instructionBody}`;
|
|
66
|
+
}
|
|
67
|
+
return instructionBody;
|
|
68
|
+
}
|
|
69
|
+
function getParameters(tool) {
|
|
70
|
+
if ("parameters" in tool) {
|
|
71
|
+
return tool.parameters;
|
|
72
|
+
}
|
|
73
|
+
return tool.inputSchema;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ../shared/src/tool-calling/format-tool-results.ts
|
|
7
77
|
function buildResultPayload(result) {
|
|
8
78
|
const payload = {
|
|
9
79
|
name: result.toolName,
|
|
@@ -27,7 +97,367 @@ ${payloads.join("\n")}
|
|
|
27
97
|
\`\`\``;
|
|
28
98
|
}
|
|
29
99
|
|
|
100
|
+
// ../shared/src/tool-calling/parse-json-function-calls.ts
|
|
101
|
+
var DEFAULT_OPTIONS = {
|
|
102
|
+
supportXmlTags: true,
|
|
103
|
+
supportPythonStyle: true,
|
|
104
|
+
supportParametersField: true
|
|
105
|
+
};
|
|
106
|
+
function generateToolCallId() {
|
|
107
|
+
return `call_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
108
|
+
}
|
|
109
|
+
function buildRegex(options) {
|
|
110
|
+
const patterns = [];
|
|
111
|
+
patterns.push("```tool[_-]?call\\s*([\\s\\S]*?)```");
|
|
112
|
+
if (options.supportXmlTags) {
|
|
113
|
+
patterns.push("<tool_call>\\s*([\\s\\S]*?)\\s*</tool_call>");
|
|
114
|
+
}
|
|
115
|
+
if (options.supportPythonStyle) {
|
|
116
|
+
patterns.push("\\[(\\w+)\\(([^)]*)\\)\\]");
|
|
117
|
+
}
|
|
118
|
+
return new RegExp(patterns.join("|"), "gi");
|
|
119
|
+
}
|
|
120
|
+
function parseJsonFunctionCalls(response, options = DEFAULT_OPTIONS) {
|
|
121
|
+
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
122
|
+
const regex = buildRegex(mergedOptions);
|
|
123
|
+
const matches = Array.from(response.matchAll(regex));
|
|
124
|
+
regex.lastIndex = 0;
|
|
125
|
+
if (matches.length === 0) {
|
|
126
|
+
return { toolCalls: [], textContent: response };
|
|
127
|
+
}
|
|
128
|
+
const toolCalls = [];
|
|
129
|
+
let textContent = response;
|
|
130
|
+
for (const match of matches) {
|
|
131
|
+
const fullMatch = match[0];
|
|
132
|
+
textContent = textContent.replace(fullMatch, "");
|
|
133
|
+
try {
|
|
134
|
+
if (mergedOptions.supportPythonStyle && match[0].startsWith("[")) {
|
|
135
|
+
const pythonMatch = /\[(\w+)\(([^)]*)\)\]/.exec(match[0]);
|
|
136
|
+
if (pythonMatch) {
|
|
137
|
+
const [, funcName, pythonArgs] = pythonMatch;
|
|
138
|
+
const args = {};
|
|
139
|
+
if (pythonArgs && pythonArgs.trim()) {
|
|
140
|
+
const argPairs = pythonArgs.split(",").map((s) => s.trim());
|
|
141
|
+
for (const pair of argPairs) {
|
|
142
|
+
const equalIndex = pair.indexOf("=");
|
|
143
|
+
if (equalIndex > 0) {
|
|
144
|
+
const key = pair.substring(0, equalIndex).trim();
|
|
145
|
+
let value = pair.substring(equalIndex + 1).trim();
|
|
146
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
147
|
+
value = value.substring(1, value.length - 1);
|
|
148
|
+
}
|
|
149
|
+
args[key] = value;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
toolCalls.push({
|
|
154
|
+
type: "tool-call",
|
|
155
|
+
toolCallId: generateToolCallId(),
|
|
156
|
+
toolName: funcName,
|
|
157
|
+
args
|
|
158
|
+
});
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const innerContent = match[1] || match[2] || "";
|
|
163
|
+
const trimmed = innerContent.trim();
|
|
164
|
+
if (!trimmed) continue;
|
|
165
|
+
try {
|
|
166
|
+
const parsed = JSON.parse(trimmed);
|
|
167
|
+
const callsArray = Array.isArray(parsed) ? parsed : [parsed];
|
|
168
|
+
for (const call of callsArray) {
|
|
169
|
+
if (!call.name) continue;
|
|
170
|
+
let args = call.arguments || (mergedOptions.supportParametersField ? call.parameters : null) || {};
|
|
171
|
+
if (typeof args === "string") {
|
|
172
|
+
try {
|
|
173
|
+
args = JSON.parse(args);
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
toolCalls.push({
|
|
178
|
+
type: "tool-call",
|
|
179
|
+
toolCallId: call.id || generateToolCallId(),
|
|
180
|
+
toolName: call.name,
|
|
181
|
+
args
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
const lines = trimmed.split("\n").filter((line) => line.trim());
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
try {
|
|
188
|
+
const call = JSON.parse(line.trim());
|
|
189
|
+
if (!call.name) continue;
|
|
190
|
+
let args = call.arguments || (mergedOptions.supportParametersField ? call.parameters : null) || {};
|
|
191
|
+
if (typeof args === "string") {
|
|
192
|
+
try {
|
|
193
|
+
args = JSON.parse(args);
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
toolCalls.push({
|
|
198
|
+
type: "tool-call",
|
|
199
|
+
toolCallId: call.id || generateToolCallId(),
|
|
200
|
+
toolName: call.name,
|
|
201
|
+
args
|
|
202
|
+
});
|
|
203
|
+
} catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.warn("Failed to parse JSON tool call:", error);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
textContent = textContent.replace(/\n{2,}/g, "\n");
|
|
214
|
+
return { toolCalls, textContent: textContent.trim() };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ../shared/src/streaming/tool-call-detector.ts
|
|
218
|
+
var DEFAULT_FENCE_PATTERNS = [
|
|
219
|
+
{ start: "```tool_call", end: "```", reconstructStart: "```tool_call\n" },
|
|
220
|
+
{ start: "```tool-call", end: "```", reconstructStart: "```tool-call\n" }
|
|
221
|
+
];
|
|
222
|
+
var EXTENDED_FENCE_PATTERNS = [
|
|
223
|
+
...DEFAULT_FENCE_PATTERNS,
|
|
224
|
+
{
|
|
225
|
+
start: "<tool_call>",
|
|
226
|
+
end: "</tool_call>",
|
|
227
|
+
reconstructStart: "<tool_call>"
|
|
228
|
+
}
|
|
229
|
+
];
|
|
230
|
+
var ToolCallFenceDetector = class {
|
|
231
|
+
constructor(options = {}) {
|
|
232
|
+
this.pythonStyleRegex = /\[(\w+)\(/g;
|
|
233
|
+
this.buffer = "";
|
|
234
|
+
this.inFence = false;
|
|
235
|
+
this.fenceStartBuffer = "";
|
|
236
|
+
// Accumulated fence content
|
|
237
|
+
this.currentFencePattern = null;
|
|
238
|
+
this.fencePatterns = options.patterns ?? EXTENDED_FENCE_PATTERNS;
|
|
239
|
+
this.enablePythonStyle = options.enablePythonStyle ?? true;
|
|
240
|
+
this.fenceStarts = this.fencePatterns.map((p) => p.start);
|
|
241
|
+
}
|
|
242
|
+
addChunk(chunk) {
|
|
243
|
+
this.buffer += chunk;
|
|
244
|
+
}
|
|
245
|
+
getBuffer() {
|
|
246
|
+
return this.buffer;
|
|
247
|
+
}
|
|
248
|
+
clearBuffer() {
|
|
249
|
+
this.buffer = "";
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Detects if there's a complete fence in the buffer
|
|
253
|
+
* @returns Detection result with fence info and safe text
|
|
254
|
+
*/
|
|
255
|
+
detectFence() {
|
|
256
|
+
const {
|
|
257
|
+
index: startIdx,
|
|
258
|
+
prefix: matchedPrefix,
|
|
259
|
+
pattern
|
|
260
|
+
} = this.findFenceStart(this.buffer);
|
|
261
|
+
if (startIdx === -1) {
|
|
262
|
+
const overlap = this.computeOverlapLength(this.buffer, this.fenceStarts);
|
|
263
|
+
const safeTextLength = this.buffer.length - overlap;
|
|
264
|
+
const prefixText2 = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
|
|
265
|
+
const remaining = overlap > 0 ? this.buffer.slice(-overlap) : "";
|
|
266
|
+
this.buffer = remaining;
|
|
267
|
+
return {
|
|
268
|
+
fence: null,
|
|
269
|
+
prefixText: prefixText2,
|
|
270
|
+
remainingText: "",
|
|
271
|
+
overlapLength: overlap
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const prefixText = this.buffer.slice(0, startIdx);
|
|
275
|
+
this.buffer = this.buffer.slice(startIdx);
|
|
276
|
+
const prefixLength = matchedPrefix?.length ?? 0;
|
|
277
|
+
const fenceEnd = pattern?.end ?? "```";
|
|
278
|
+
const closingIdx = this.buffer.indexOf(fenceEnd, prefixLength);
|
|
279
|
+
if (closingIdx === -1) {
|
|
280
|
+
return {
|
|
281
|
+
fence: null,
|
|
282
|
+
prefixText,
|
|
283
|
+
remainingText: "",
|
|
284
|
+
overlapLength: 0
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const endPos = closingIdx + fenceEnd.length;
|
|
288
|
+
const fence = this.buffer.slice(0, endPos);
|
|
289
|
+
const remainingText = this.buffer.slice(endPos);
|
|
290
|
+
this.buffer = "";
|
|
291
|
+
return {
|
|
292
|
+
fence,
|
|
293
|
+
prefixText,
|
|
294
|
+
remainingText,
|
|
295
|
+
overlapLength: 0
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Finds the first occurrence of any fence start marker
|
|
300
|
+
*
|
|
301
|
+
* @param text - Text to search in
|
|
302
|
+
* @returns Index of first fence start and which pattern matched
|
|
303
|
+
* @private
|
|
304
|
+
*/
|
|
305
|
+
findFenceStart(text) {
|
|
306
|
+
let bestIndex = -1;
|
|
307
|
+
let matchedPrefix = null;
|
|
308
|
+
let matchedPattern = null;
|
|
309
|
+
for (const pattern of this.fencePatterns) {
|
|
310
|
+
const idx = text.indexOf(pattern.start);
|
|
311
|
+
if (idx !== -1 && (bestIndex === -1 || idx < bestIndex)) {
|
|
312
|
+
bestIndex = idx;
|
|
313
|
+
matchedPrefix = pattern.start;
|
|
314
|
+
matchedPattern = pattern;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (this.enablePythonStyle) {
|
|
318
|
+
this.pythonStyleRegex.lastIndex = 0;
|
|
319
|
+
const pythonMatch = this.pythonStyleRegex.exec(text);
|
|
320
|
+
if (pythonMatch && (bestIndex === -1 || pythonMatch.index < bestIndex)) {
|
|
321
|
+
bestIndex = pythonMatch.index;
|
|
322
|
+
matchedPrefix = pythonMatch[0];
|
|
323
|
+
matchedPattern = {
|
|
324
|
+
start: pythonMatch[0],
|
|
325
|
+
end: ")]",
|
|
326
|
+
reconstructStart: pythonMatch[0],
|
|
327
|
+
isRegex: true
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { index: bestIndex, prefix: matchedPrefix, pattern: matchedPattern };
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Computes the maximum overlap between the end of text and the start of any prefix
|
|
335
|
+
* @param text - Text to check for overlap
|
|
336
|
+
* @param prefixes - List of prefixes to check against
|
|
337
|
+
* @returns Length of the maximum overlap found
|
|
338
|
+
*/
|
|
339
|
+
computeOverlapLength(text, prefixes) {
|
|
340
|
+
let overlap = 0;
|
|
341
|
+
for (const prefix of prefixes) {
|
|
342
|
+
const maxLength = Math.min(text.length, prefix.length - 1);
|
|
343
|
+
for (let size = maxLength; size > 0; size -= 1) {
|
|
344
|
+
if (prefix.startsWith(text.slice(-size))) {
|
|
345
|
+
overlap = Math.max(overlap, size);
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return overlap;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Checks if the buffer currently contains any text
|
|
354
|
+
*/
|
|
355
|
+
hasContent() {
|
|
356
|
+
return this.buffer.length > 0;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Gets the buffer size
|
|
360
|
+
*/
|
|
361
|
+
getBufferSize() {
|
|
362
|
+
return this.buffer.length;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Detect and stream fence content in real-time for true incremental streaming
|
|
366
|
+
* @returns Streaming result with current state and safe content to emit
|
|
367
|
+
*/
|
|
368
|
+
detectStreamingFence() {
|
|
369
|
+
if (!this.inFence) {
|
|
370
|
+
const {
|
|
371
|
+
index: startIdx,
|
|
372
|
+
prefix: matchedPrefix,
|
|
373
|
+
pattern
|
|
374
|
+
} = this.findFenceStart(this.buffer);
|
|
375
|
+
if (startIdx === -1) {
|
|
376
|
+
const overlap = this.computeOverlapLength(
|
|
377
|
+
this.buffer,
|
|
378
|
+
this.fenceStarts
|
|
379
|
+
);
|
|
380
|
+
const safeTextLength = this.buffer.length - overlap;
|
|
381
|
+
const safeContent = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
|
|
382
|
+
this.buffer = this.buffer.slice(safeTextLength);
|
|
383
|
+
return {
|
|
384
|
+
inFence: false,
|
|
385
|
+
safeContent,
|
|
386
|
+
completeFence: null,
|
|
387
|
+
textAfterFence: ""
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const prefixText = this.buffer.slice(0, startIdx);
|
|
391
|
+
const fenceStartLength = matchedPrefix?.length ?? 0;
|
|
392
|
+
this.buffer = this.buffer.slice(startIdx + fenceStartLength);
|
|
393
|
+
if (pattern && pattern.start.startsWith("```") && this.buffer.startsWith("\n")) {
|
|
394
|
+
this.buffer = this.buffer.slice(1);
|
|
395
|
+
}
|
|
396
|
+
this.inFence = true;
|
|
397
|
+
this.fenceStartBuffer = "";
|
|
398
|
+
this.currentFencePattern = pattern;
|
|
399
|
+
return {
|
|
400
|
+
inFence: true,
|
|
401
|
+
safeContent: prefixText,
|
|
402
|
+
// Emit any text before the fence
|
|
403
|
+
completeFence: null,
|
|
404
|
+
textAfterFence: ""
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const fenceEnd = this.currentFencePattern?.end ?? "```";
|
|
408
|
+
const closingIdx = this.buffer.indexOf(fenceEnd);
|
|
409
|
+
if (closingIdx === -1) {
|
|
410
|
+
const overlap = this.computeOverlapLength(this.buffer, [fenceEnd]);
|
|
411
|
+
const safeContentLength = this.buffer.length - overlap;
|
|
412
|
+
if (safeContentLength > 0) {
|
|
413
|
+
const safeContent = this.buffer.slice(0, safeContentLength);
|
|
414
|
+
this.fenceStartBuffer += safeContent;
|
|
415
|
+
this.buffer = this.buffer.slice(safeContentLength);
|
|
416
|
+
return {
|
|
417
|
+
inFence: true,
|
|
418
|
+
safeContent,
|
|
419
|
+
completeFence: null,
|
|
420
|
+
textAfterFence: ""
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
inFence: true,
|
|
425
|
+
safeContent: "",
|
|
426
|
+
completeFence: null,
|
|
427
|
+
textAfterFence: ""
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const fenceContent = this.buffer.slice(0, closingIdx);
|
|
431
|
+
this.fenceStartBuffer += fenceContent;
|
|
432
|
+
const reconstructStart = this.currentFencePattern?.reconstructStart ?? "```tool_call\n";
|
|
433
|
+
const completeFence = `${reconstructStart}${this.fenceStartBuffer}${fenceEnd}`;
|
|
434
|
+
const textAfterFence = this.buffer.slice(closingIdx + fenceEnd.length);
|
|
435
|
+
this.inFence = false;
|
|
436
|
+
this.fenceStartBuffer = "";
|
|
437
|
+
this.currentFencePattern = null;
|
|
438
|
+
this.buffer = textAfterFence;
|
|
439
|
+
return {
|
|
440
|
+
inFence: false,
|
|
441
|
+
safeContent: fenceContent,
|
|
442
|
+
// Emit the last bit of fence content
|
|
443
|
+
completeFence,
|
|
444
|
+
textAfterFence
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
isInFence() {
|
|
448
|
+
return this.inFence;
|
|
449
|
+
}
|
|
450
|
+
resetStreamingState() {
|
|
451
|
+
this.inFence = false;
|
|
452
|
+
this.fenceStartBuffer = "";
|
|
453
|
+
this.currentFencePattern = null;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
|
|
30
457
|
// src/convert-to-browser-ai-messages.ts
|
|
458
|
+
import {
|
|
459
|
+
UnsupportedFunctionalityError
|
|
460
|
+
} from "@ai-sdk/provider";
|
|
31
461
|
function convertBase64ToUint8Array(base64) {
|
|
32
462
|
try {
|
|
33
463
|
const binaryString = atob(base64);
|
|
@@ -201,168 +631,44 @@ function convertToBrowserAIMessages(prompt) {
|
|
|
201
631
|
}
|
|
202
632
|
}
|
|
203
633
|
const toolCallJson = formatToolCallsJson(toolCallParts);
|
|
204
|
-
const contentSegments = [];
|
|
205
|
-
if (text.trim().length > 0) {
|
|
206
|
-
contentSegments.push(text);
|
|
207
|
-
} else if (text.length > 0) {
|
|
208
|
-
contentSegments.push(text);
|
|
209
|
-
}
|
|
210
|
-
if (toolCallJson) {
|
|
211
|
-
contentSegments.push(toolCallJson);
|
|
212
|
-
}
|
|
213
|
-
const content = contentSegments.length > 0 ? contentSegments.join("\n") : "";
|
|
214
|
-
messages.push({
|
|
215
|
-
role: "assistant",
|
|
216
|
-
content
|
|
217
|
-
});
|
|
218
|
-
break;
|
|
219
|
-
}
|
|
220
|
-
case "tool": {
|
|
221
|
-
const toolParts = message.content;
|
|
222
|
-
const results = toolParts.map(toToolResult);
|
|
223
|
-
const toolResultsJson = formatToolResults(results);
|
|
224
|
-
messages.push({
|
|
225
|
-
role: "user",
|
|
226
|
-
content: toolResultsJson
|
|
227
|
-
});
|
|
228
|
-
break;
|
|
229
|
-
}
|
|
230
|
-
default: {
|
|
231
|
-
const exhaustiveCheck = message;
|
|
232
|
-
throw new Error(
|
|
233
|
-
`Unsupported role: ${exhaustiveCheck.role ?? "unknown"}`
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
return { systemMessage, messages };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// src/tool-calling/build-json-system-prompt.ts
|
|
242
|
-
function buildJsonToolSystemPrompt(originalSystemPrompt, tools, options) {
|
|
243
|
-
if (!tools || tools.length === 0) {
|
|
244
|
-
return originalSystemPrompt || "";
|
|
245
|
-
}
|
|
246
|
-
const parallelInstruction = "Only request one tool call at a time. Wait for tool results before asking for another tool.";
|
|
247
|
-
const toolSchemas = tools.map((tool) => {
|
|
248
|
-
const schema = getParameters(tool);
|
|
249
|
-
return {
|
|
250
|
-
name: tool.name,
|
|
251
|
-
description: tool.description ?? "No description provided.",
|
|
252
|
-
parameters: schema || { type: "object", properties: {} }
|
|
253
|
-
};
|
|
254
|
-
});
|
|
255
|
-
const toolsJson = JSON.stringify(toolSchemas, null, 2);
|
|
256
|
-
const instructionBody = `You are a helpful AI assistant with access to tools.
|
|
257
|
-
|
|
258
|
-
# Available Tools
|
|
259
|
-
${toolsJson}
|
|
260
|
-
|
|
261
|
-
# Tool Calling Instructions
|
|
262
|
-
${parallelInstruction}
|
|
263
|
-
|
|
264
|
-
To call a tool, output JSON in this exact format inside a \`\`\`tool_call code fence:
|
|
265
|
-
|
|
266
|
-
\`\`\`tool_call
|
|
267
|
-
{"name": "tool_name", "arguments": {"param1": "value1", "param2": "value2"}}
|
|
268
|
-
\`\`\`
|
|
269
|
-
|
|
270
|
-
Tool responses will be provided in \`\`\`tool_result fences. Each line contains JSON like:
|
|
271
|
-
\`\`\`tool_result
|
|
272
|
-
{"id": "call_123", "name": "tool_name", "result": {...}, "error": false}
|
|
273
|
-
\`\`\`
|
|
274
|
-
Use the \`result\` payload (and treat \`error\` as a boolean flag) when continuing the conversation.
|
|
275
|
-
|
|
276
|
-
Important:
|
|
277
|
-
- Use exact tool and parameter names from the schema above
|
|
278
|
-
- Arguments must be a valid JSON object matching the tool's parameters
|
|
279
|
-
- You can include brief reasoning before or after the tool call
|
|
280
|
-
- If no tool is needed, respond directly without tool_call fences`;
|
|
281
|
-
if (originalSystemPrompt?.trim()) {
|
|
282
|
-
return `${originalSystemPrompt.trim()}
|
|
283
|
-
|
|
284
|
-
${instructionBody}`;
|
|
285
|
-
}
|
|
286
|
-
return instructionBody;
|
|
287
|
-
}
|
|
288
|
-
function getParameters(tool) {
|
|
289
|
-
if ("parameters" in tool) {
|
|
290
|
-
return tool.parameters;
|
|
291
|
-
}
|
|
292
|
-
return tool.inputSchema;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// src/tool-calling/parse-json-function-calls.ts
|
|
296
|
-
var JSON_TOOL_CALL_FENCE_REGEX = /```tool[_-]?call\s*([\s\S]*?)```/gi;
|
|
297
|
-
function generateToolCallId() {
|
|
298
|
-
return `call_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
299
|
-
}
|
|
300
|
-
function parseJsonFunctionCalls(response) {
|
|
301
|
-
const matches = Array.from(response.matchAll(JSON_TOOL_CALL_FENCE_REGEX));
|
|
302
|
-
JSON_TOOL_CALL_FENCE_REGEX.lastIndex = 0;
|
|
303
|
-
if (matches.length === 0) {
|
|
304
|
-
return { toolCalls: [], textContent: response };
|
|
305
|
-
}
|
|
306
|
-
const toolCalls = [];
|
|
307
|
-
let textContent = response;
|
|
308
|
-
for (const match of matches) {
|
|
309
|
-
const [fullFence, innerContent] = match;
|
|
310
|
-
textContent = textContent.replace(fullFence, "");
|
|
311
|
-
try {
|
|
312
|
-
const trimmed = innerContent.trim();
|
|
313
|
-
try {
|
|
314
|
-
const parsed = JSON.parse(trimmed);
|
|
315
|
-
const callsArray = Array.isArray(parsed) ? parsed : [parsed];
|
|
316
|
-
for (const call of callsArray) {
|
|
317
|
-
if (!call.name) continue;
|
|
318
|
-
toolCalls.push({
|
|
319
|
-
type: "tool-call",
|
|
320
|
-
toolCallId: call.id || generateToolCallId(),
|
|
321
|
-
toolName: call.name,
|
|
322
|
-
args: call.arguments || {}
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
} catch {
|
|
326
|
-
const lines = trimmed.split("\n").filter((line) => line.trim());
|
|
327
|
-
for (const line of lines) {
|
|
328
|
-
try {
|
|
329
|
-
const call = JSON.parse(line.trim());
|
|
330
|
-
if (!call.name) continue;
|
|
331
|
-
toolCalls.push({
|
|
332
|
-
type: "tool-call",
|
|
333
|
-
toolCallId: call.id || generateToolCallId(),
|
|
334
|
-
toolName: call.name,
|
|
335
|
-
args: call.arguments || {}
|
|
336
|
-
});
|
|
337
|
-
} catch {
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
634
|
+
const contentSegments = [];
|
|
635
|
+
if (text.trim().length > 0) {
|
|
636
|
+
contentSegments.push(text);
|
|
637
|
+
} else if (text.length > 0) {
|
|
638
|
+
contentSegments.push(text);
|
|
639
|
+
}
|
|
640
|
+
if (toolCallJson) {
|
|
641
|
+
contentSegments.push(toolCallJson);
|
|
340
642
|
}
|
|
643
|
+
const content = contentSegments.length > 0 ? contentSegments.join("\n") : "";
|
|
644
|
+
messages.push({
|
|
645
|
+
role: "assistant",
|
|
646
|
+
content
|
|
647
|
+
});
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
case "tool": {
|
|
651
|
+
const toolParts = message.content;
|
|
652
|
+
const results = toolParts.map(toToolResult);
|
|
653
|
+
const toolResultsJson = formatToolResults(results);
|
|
654
|
+
messages.push({
|
|
655
|
+
role: "user",
|
|
656
|
+
content: toolResultsJson
|
|
657
|
+
});
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
default: {
|
|
661
|
+
const exhaustiveCheck = message;
|
|
662
|
+
throw new Error(
|
|
663
|
+
`Unsupported role: ${exhaustiveCheck.role ?? "unknown"}`
|
|
664
|
+
);
|
|
341
665
|
}
|
|
342
|
-
} catch (error) {
|
|
343
|
-
console.warn("Failed to parse JSON tool call:", error);
|
|
344
|
-
continue;
|
|
345
666
|
}
|
|
346
667
|
}
|
|
347
|
-
|
|
348
|
-
return { toolCalls, textContent: textContent.trim() };
|
|
668
|
+
return { systemMessage, messages };
|
|
349
669
|
}
|
|
350
670
|
|
|
351
671
|
// src/utils/warnings.ts
|
|
352
|
-
function createUnsupportedSettingWarning(feature, details) {
|
|
353
|
-
return {
|
|
354
|
-
type: "unsupported",
|
|
355
|
-
feature,
|
|
356
|
-
details
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
function createUnsupportedToolWarning(tool, details) {
|
|
360
|
-
return {
|
|
361
|
-
type: "unsupported",
|
|
362
|
-
feature: `tool:${tool.name}`,
|
|
363
|
-
details
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
672
|
function gatherUnsupportedSettingWarnings(options) {
|
|
367
673
|
const warnings = [];
|
|
368
674
|
if (options.maxOutputTokens != null) {
|
|
@@ -455,11 +761,6 @@ function getExpectedInputs(prompt) {
|
|
|
455
761
|
return Array.from(inputs).map((type) => ({ type }));
|
|
456
762
|
}
|
|
457
763
|
|
|
458
|
-
// src/utils/tool-utils.ts
|
|
459
|
-
function isFunctionTool(tool) {
|
|
460
|
-
return tool.type === "function";
|
|
461
|
-
}
|
|
462
|
-
|
|
463
764
|
// src/models/session-manager.ts
|
|
464
765
|
import { LoadSettingError } from "@ai-sdk/provider";
|
|
465
766
|
var SessionManager = class {
|
|
@@ -630,258 +931,6 @@ var SessionManager = class {
|
|
|
630
931
|
}
|
|
631
932
|
};
|
|
632
933
|
|
|
633
|
-
// src/streaming/tool-call-detector.ts
|
|
634
|
-
var ToolCallFenceDetector = class {
|
|
635
|
-
constructor() {
|
|
636
|
-
this.FENCE_STARTS = ["```tool_call"];
|
|
637
|
-
this.FENCE_END = "```";
|
|
638
|
-
this.buffer = "";
|
|
639
|
-
// Streaming state
|
|
640
|
-
this.inFence = false;
|
|
641
|
-
this.fenceStartBuffer = "";
|
|
642
|
-
}
|
|
643
|
-
// Accumulated fence content
|
|
644
|
-
/**
|
|
645
|
-
* Adds a chunk of text to the internal buffer
|
|
646
|
-
*
|
|
647
|
-
* @param chunk - Text chunk from the stream
|
|
648
|
-
*/
|
|
649
|
-
addChunk(chunk) {
|
|
650
|
-
this.buffer += chunk;
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* Gets the current buffer content
|
|
654
|
-
*/
|
|
655
|
-
getBuffer() {
|
|
656
|
-
return this.buffer;
|
|
657
|
-
}
|
|
658
|
-
/**
|
|
659
|
-
* Clears the internal buffer
|
|
660
|
-
*/
|
|
661
|
-
clearBuffer() {
|
|
662
|
-
this.buffer = "";
|
|
663
|
-
}
|
|
664
|
-
/**
|
|
665
|
-
* Detects if there's a complete fence in the buffer
|
|
666
|
-
*
|
|
667
|
-
* This method:
|
|
668
|
-
* 1. Searches for fence start markers
|
|
669
|
-
* 2. If found, looks for closing fence
|
|
670
|
-
* 3. Computes overlap for partial fences
|
|
671
|
-
* 4. Returns safe text that can be emitted
|
|
672
|
-
*
|
|
673
|
-
* @returns Detection result with fence info and safe text
|
|
674
|
-
*/
|
|
675
|
-
detectFence() {
|
|
676
|
-
const { index: startIdx, prefix: matchedPrefix } = this.findFenceStart(
|
|
677
|
-
this.buffer
|
|
678
|
-
);
|
|
679
|
-
if (startIdx === -1) {
|
|
680
|
-
const overlap = this.computeOverlapLength(this.buffer, this.FENCE_STARTS);
|
|
681
|
-
const safeTextLength = this.buffer.length - overlap;
|
|
682
|
-
const prefixText2 = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
|
|
683
|
-
const remaining = overlap > 0 ? this.buffer.slice(-overlap) : "";
|
|
684
|
-
this.buffer = remaining;
|
|
685
|
-
return {
|
|
686
|
-
fence: null,
|
|
687
|
-
prefixText: prefixText2,
|
|
688
|
-
remainingText: "",
|
|
689
|
-
overlapLength: overlap
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
const prefixText = this.buffer.slice(0, startIdx);
|
|
693
|
-
this.buffer = this.buffer.slice(startIdx);
|
|
694
|
-
const prefixLength = matchedPrefix?.length ?? 0;
|
|
695
|
-
const closingIdx = this.buffer.indexOf(this.FENCE_END, prefixLength);
|
|
696
|
-
if (closingIdx === -1) {
|
|
697
|
-
return {
|
|
698
|
-
fence: null,
|
|
699
|
-
prefixText,
|
|
700
|
-
remainingText: "",
|
|
701
|
-
overlapLength: 0
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
const endPos = closingIdx + this.FENCE_END.length;
|
|
705
|
-
const fence = this.buffer.slice(0, endPos);
|
|
706
|
-
const remainingText = this.buffer.slice(endPos);
|
|
707
|
-
this.buffer = "";
|
|
708
|
-
return {
|
|
709
|
-
fence,
|
|
710
|
-
prefixText,
|
|
711
|
-
remainingText,
|
|
712
|
-
overlapLength: 0
|
|
713
|
-
};
|
|
714
|
-
}
|
|
715
|
-
/**
|
|
716
|
-
* Finds the first occurrence of any fence start marker
|
|
717
|
-
*
|
|
718
|
-
* @param text - Text to search in
|
|
719
|
-
* @returns Index of first fence start and which prefix matched
|
|
720
|
-
* @private
|
|
721
|
-
*/
|
|
722
|
-
findFenceStart(text) {
|
|
723
|
-
let bestIndex = -1;
|
|
724
|
-
let matchedPrefix = null;
|
|
725
|
-
for (const prefix of this.FENCE_STARTS) {
|
|
726
|
-
const idx = text.indexOf(prefix);
|
|
727
|
-
if (idx !== -1 && (bestIndex === -1 || idx < bestIndex)) {
|
|
728
|
-
bestIndex = idx;
|
|
729
|
-
matchedPrefix = prefix;
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
return { index: bestIndex, prefix: matchedPrefix };
|
|
733
|
-
}
|
|
734
|
-
/**
|
|
735
|
-
* Computes the maximum overlap between the end of text and the start of any prefix
|
|
736
|
-
*
|
|
737
|
-
* This is crucial for streaming: if the buffer ends with "``", we can't emit it
|
|
738
|
-
* because the next chunk might be "`tool_call", completing a fence marker.
|
|
739
|
-
*
|
|
740
|
-
* @param text - Text to check for overlap
|
|
741
|
-
* @param prefixes - List of prefixes to check against
|
|
742
|
-
* @returns Length of the maximum overlap found
|
|
743
|
-
*
|
|
744
|
-
* @example
|
|
745
|
-
* ```typescript
|
|
746
|
-
* computeOverlapLength("hello ``", ["```tool_call"])
|
|
747
|
-
* // Returns: 2 (because "``" matches start of "```tool_call")
|
|
748
|
-
*
|
|
749
|
-
* computeOverlapLength("hello `", ["```tool_call"])
|
|
750
|
-
* // Returns: 1
|
|
751
|
-
*
|
|
752
|
-
* computeOverlapLength("hello world", ["```tool_call"])
|
|
753
|
-
* // Returns: 0 (no overlap)
|
|
754
|
-
* ```
|
|
755
|
-
*
|
|
756
|
-
* @private
|
|
757
|
-
*/
|
|
758
|
-
computeOverlapLength(text, prefixes) {
|
|
759
|
-
let overlap = 0;
|
|
760
|
-
for (const prefix of prefixes) {
|
|
761
|
-
const maxLength = Math.min(text.length, prefix.length - 1);
|
|
762
|
-
for (let size = maxLength; size > 0; size -= 1) {
|
|
763
|
-
if (prefix.startsWith(text.slice(-size))) {
|
|
764
|
-
overlap = Math.max(overlap, size);
|
|
765
|
-
break;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
return overlap;
|
|
770
|
-
}
|
|
771
|
-
/**
|
|
772
|
-
* Checks if the buffer currently contains any text
|
|
773
|
-
*/
|
|
774
|
-
hasContent() {
|
|
775
|
-
return this.buffer.length > 0;
|
|
776
|
-
}
|
|
777
|
-
/**
|
|
778
|
-
* Gets the buffer size
|
|
779
|
-
*/
|
|
780
|
-
getBufferSize() {
|
|
781
|
-
return this.buffer.length;
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Detect and stream fence content in real-time for true incremental streaming
|
|
785
|
-
*
|
|
786
|
-
* This method is designed for streaming tool calls as they arrive:
|
|
787
|
-
* 1. Detects when a fence starts and transitions to "inFence" state
|
|
788
|
-
* 2. While inFence, emits safe content that won't conflict with fence end marker
|
|
789
|
-
* 3. When fence ends, returns the complete fence for parsing
|
|
790
|
-
*
|
|
791
|
-
* @returns Streaming result with current state and safe content to emit
|
|
792
|
-
*/
|
|
793
|
-
detectStreamingFence() {
|
|
794
|
-
if (!this.inFence) {
|
|
795
|
-
const { index: startIdx, prefix: matchedPrefix } = this.findFenceStart(
|
|
796
|
-
this.buffer
|
|
797
|
-
);
|
|
798
|
-
if (startIdx === -1) {
|
|
799
|
-
const overlap = this.computeOverlapLength(
|
|
800
|
-
this.buffer,
|
|
801
|
-
this.FENCE_STARTS
|
|
802
|
-
);
|
|
803
|
-
const safeTextLength = this.buffer.length - overlap;
|
|
804
|
-
const safeContent = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
|
|
805
|
-
this.buffer = this.buffer.slice(safeTextLength);
|
|
806
|
-
return {
|
|
807
|
-
inFence: false,
|
|
808
|
-
safeContent,
|
|
809
|
-
completeFence: null,
|
|
810
|
-
textAfterFence: ""
|
|
811
|
-
};
|
|
812
|
-
}
|
|
813
|
-
const prefixText = this.buffer.slice(0, startIdx);
|
|
814
|
-
const fenceStartLength = matchedPrefix?.length ?? 0;
|
|
815
|
-
this.buffer = this.buffer.slice(startIdx + fenceStartLength);
|
|
816
|
-
if (this.buffer.startsWith("\n")) {
|
|
817
|
-
this.buffer = this.buffer.slice(1);
|
|
818
|
-
}
|
|
819
|
-
this.inFence = true;
|
|
820
|
-
this.fenceStartBuffer = "";
|
|
821
|
-
return {
|
|
822
|
-
inFence: true,
|
|
823
|
-
safeContent: prefixText,
|
|
824
|
-
// Emit any text before the fence
|
|
825
|
-
completeFence: null,
|
|
826
|
-
textAfterFence: ""
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
const closingIdx = this.buffer.indexOf(this.FENCE_END);
|
|
830
|
-
if (closingIdx === -1) {
|
|
831
|
-
const overlap = this.computeOverlapLength(this.buffer, [this.FENCE_END]);
|
|
832
|
-
const safeContentLength = this.buffer.length - overlap;
|
|
833
|
-
if (safeContentLength > 0) {
|
|
834
|
-
const safeContent = this.buffer.slice(0, safeContentLength);
|
|
835
|
-
this.fenceStartBuffer += safeContent;
|
|
836
|
-
this.buffer = this.buffer.slice(safeContentLength);
|
|
837
|
-
return {
|
|
838
|
-
inFence: true,
|
|
839
|
-
safeContent,
|
|
840
|
-
completeFence: null,
|
|
841
|
-
textAfterFence: ""
|
|
842
|
-
};
|
|
843
|
-
}
|
|
844
|
-
return {
|
|
845
|
-
inFence: true,
|
|
846
|
-
safeContent: "",
|
|
847
|
-
completeFence: null,
|
|
848
|
-
textAfterFence: ""
|
|
849
|
-
};
|
|
850
|
-
}
|
|
851
|
-
const fenceContent = this.buffer.slice(0, closingIdx);
|
|
852
|
-
this.fenceStartBuffer += fenceContent;
|
|
853
|
-
const completeFence = `${this.FENCE_STARTS[0]}
|
|
854
|
-
${this.fenceStartBuffer}
|
|
855
|
-
${this.FENCE_END}`;
|
|
856
|
-
const textAfterFence = this.buffer.slice(
|
|
857
|
-
closingIdx + this.FENCE_END.length
|
|
858
|
-
);
|
|
859
|
-
this.inFence = false;
|
|
860
|
-
this.fenceStartBuffer = "";
|
|
861
|
-
this.buffer = textAfterFence;
|
|
862
|
-
return {
|
|
863
|
-
inFence: false,
|
|
864
|
-
safeContent: fenceContent,
|
|
865
|
-
// Emit the last bit of fence content
|
|
866
|
-
completeFence,
|
|
867
|
-
textAfterFence
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
/**
|
|
871
|
-
* Check if currently inside a fence
|
|
872
|
-
*/
|
|
873
|
-
isInFence() {
|
|
874
|
-
return this.inFence;
|
|
875
|
-
}
|
|
876
|
-
/**
|
|
877
|
-
* Reset streaming state
|
|
878
|
-
*/
|
|
879
|
-
resetStreamingState() {
|
|
880
|
-
this.inFence = false;
|
|
881
|
-
this.fenceStartBuffer = "";
|
|
882
|
-
}
|
|
883
|
-
};
|
|
884
|
-
|
|
885
934
|
// src/browser-ai-language-model.ts
|
|
886
935
|
function doesBrowserSupportBrowserAI() {
|
|
887
936
|
return typeof LanguageModel !== "undefined";
|