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