@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.mjs CHANGED
@@ -1,9 +1,79 @@
1
- // src/convert-to-browser-ai-messages.ts
2
- import {
3
- UnsupportedFunctionalityError
4
- } from "@ai-sdk/provider";
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
- // src/tool-calling/format-tool-results.ts
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
- textContent = textContent.replace(/\n{2,}/g, "\n");
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";