@browser-ai/core 2.0.2 → 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.
56
+
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()}
5
64
 
6
- // src/tool-calling/format-tool-results.ts
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);
@@ -218,151 +648,27 @@ function convertToBrowserAIMessages(prompt) {
218
648
  break;
219
649
  }
220
650
  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
- }
340
- }
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) {
@@ -454,49 +760,6 @@ function getExpectedInputs(prompt) {
454
760
  }
455
761
  return Array.from(inputs).map((type) => ({ type }));
456
762
  }
457
- function prependSystemPromptToMessages(messages, systemPrompt) {
458
- if (!systemPrompt.trim()) {
459
- return messages;
460
- }
461
- const prompts = messages.map((message) => ({ ...message }));
462
- const firstUserIndex = prompts.findIndex(
463
- (message) => message.role === "user"
464
- );
465
- if (firstUserIndex !== -1) {
466
- const firstUserMessage = prompts[firstUserIndex];
467
- if (Array.isArray(firstUserMessage.content)) {
468
- const content = firstUserMessage.content.slice();
469
- content.unshift({
470
- type: "text",
471
- value: `${systemPrompt}
472
-
473
- `
474
- });
475
- prompts[firstUserIndex] = {
476
- ...firstUserMessage,
477
- content
478
- };
479
- } else if (typeof firstUserMessage.content === "string") {
480
- prompts[firstUserIndex] = {
481
- ...firstUserMessage,
482
- content: `${systemPrompt}
483
-
484
- ${firstUserMessage.content}`
485
- };
486
- }
487
- } else {
488
- prompts.unshift({
489
- role: "user",
490
- content: systemPrompt
491
- });
492
- }
493
- return prompts;
494
- }
495
-
496
- // src/utils/tool-utils.ts
497
- function isFunctionTool(tool) {
498
- return tool.type === "function";
499
- }
500
763
 
501
764
  // src/models/session-manager.ts
502
765
  import { LoadSettingError } from "@ai-sdk/provider";
@@ -668,258 +931,6 @@ var SessionManager = class {
668
931
  }
669
932
  };
670
933
 
671
- // src/streaming/tool-call-detector.ts
672
- var ToolCallFenceDetector = class {
673
- constructor() {
674
- this.FENCE_STARTS = ["```tool_call"];
675
- this.FENCE_END = "```";
676
- this.buffer = "";
677
- // Streaming state
678
- this.inFence = false;
679
- this.fenceStartBuffer = "";
680
- }
681
- // Accumulated fence content
682
- /**
683
- * Adds a chunk of text to the internal buffer
684
- *
685
- * @param chunk - Text chunk from the stream
686
- */
687
- addChunk(chunk) {
688
- this.buffer += chunk;
689
- }
690
- /**
691
- * Gets the current buffer content
692
- */
693
- getBuffer() {
694
- return this.buffer;
695
- }
696
- /**
697
- * Clears the internal buffer
698
- */
699
- clearBuffer() {
700
- this.buffer = "";
701
- }
702
- /**
703
- * Detects if there's a complete fence in the buffer
704
- *
705
- * This method:
706
- * 1. Searches for fence start markers
707
- * 2. If found, looks for closing fence
708
- * 3. Computes overlap for partial fences
709
- * 4. Returns safe text that can be emitted
710
- *
711
- * @returns Detection result with fence info and safe text
712
- */
713
- detectFence() {
714
- const { index: startIdx, prefix: matchedPrefix } = this.findFenceStart(
715
- this.buffer
716
- );
717
- if (startIdx === -1) {
718
- const overlap = this.computeOverlapLength(this.buffer, this.FENCE_STARTS);
719
- const safeTextLength = this.buffer.length - overlap;
720
- const prefixText2 = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
721
- const remaining = overlap > 0 ? this.buffer.slice(-overlap) : "";
722
- this.buffer = remaining;
723
- return {
724
- fence: null,
725
- prefixText: prefixText2,
726
- remainingText: "",
727
- overlapLength: overlap
728
- };
729
- }
730
- const prefixText = this.buffer.slice(0, startIdx);
731
- this.buffer = this.buffer.slice(startIdx);
732
- const prefixLength = matchedPrefix?.length ?? 0;
733
- const closingIdx = this.buffer.indexOf(this.FENCE_END, prefixLength);
734
- if (closingIdx === -1) {
735
- return {
736
- fence: null,
737
- prefixText,
738
- remainingText: "",
739
- overlapLength: 0
740
- };
741
- }
742
- const endPos = closingIdx + this.FENCE_END.length;
743
- const fence = this.buffer.slice(0, endPos);
744
- const remainingText = this.buffer.slice(endPos);
745
- this.buffer = "";
746
- return {
747
- fence,
748
- prefixText,
749
- remainingText,
750
- overlapLength: 0
751
- };
752
- }
753
- /**
754
- * Finds the first occurrence of any fence start marker
755
- *
756
- * @param text - Text to search in
757
- * @returns Index of first fence start and which prefix matched
758
- * @private
759
- */
760
- findFenceStart(text) {
761
- let bestIndex = -1;
762
- let matchedPrefix = null;
763
- for (const prefix of this.FENCE_STARTS) {
764
- const idx = text.indexOf(prefix);
765
- if (idx !== -1 && (bestIndex === -1 || idx < bestIndex)) {
766
- bestIndex = idx;
767
- matchedPrefix = prefix;
768
- }
769
- }
770
- return { index: bestIndex, prefix: matchedPrefix };
771
- }
772
- /**
773
- * Computes the maximum overlap between the end of text and the start of any prefix
774
- *
775
- * This is crucial for streaming: if the buffer ends with "``", we can't emit it
776
- * because the next chunk might be "`tool_call", completing a fence marker.
777
- *
778
- * @param text - Text to check for overlap
779
- * @param prefixes - List of prefixes to check against
780
- * @returns Length of the maximum overlap found
781
- *
782
- * @example
783
- * ```typescript
784
- * computeOverlapLength("hello ``", ["```tool_call"])
785
- * // Returns: 2 (because "``" matches start of "```tool_call")
786
- *
787
- * computeOverlapLength("hello `", ["```tool_call"])
788
- * // Returns: 1
789
- *
790
- * computeOverlapLength("hello world", ["```tool_call"])
791
- * // Returns: 0 (no overlap)
792
- * ```
793
- *
794
- * @private
795
- */
796
- computeOverlapLength(text, prefixes) {
797
- let overlap = 0;
798
- for (const prefix of prefixes) {
799
- const maxLength = Math.min(text.length, prefix.length - 1);
800
- for (let size = maxLength; size > 0; size -= 1) {
801
- if (prefix.startsWith(text.slice(-size))) {
802
- overlap = Math.max(overlap, size);
803
- break;
804
- }
805
- }
806
- }
807
- return overlap;
808
- }
809
- /**
810
- * Checks if the buffer currently contains any text
811
- */
812
- hasContent() {
813
- return this.buffer.length > 0;
814
- }
815
- /**
816
- * Gets the buffer size
817
- */
818
- getBufferSize() {
819
- return this.buffer.length;
820
- }
821
- /**
822
- * Detect and stream fence content in real-time for true incremental streaming
823
- *
824
- * This method is designed for streaming tool calls as they arrive:
825
- * 1. Detects when a fence starts and transitions to "inFence" state
826
- * 2. While inFence, emits safe content that won't conflict with fence end marker
827
- * 3. When fence ends, returns the complete fence for parsing
828
- *
829
- * @returns Streaming result with current state and safe content to emit
830
- */
831
- detectStreamingFence() {
832
- if (!this.inFence) {
833
- const { index: startIdx, prefix: matchedPrefix } = this.findFenceStart(
834
- this.buffer
835
- );
836
- if (startIdx === -1) {
837
- const overlap = this.computeOverlapLength(
838
- this.buffer,
839
- this.FENCE_STARTS
840
- );
841
- const safeTextLength = this.buffer.length - overlap;
842
- const safeContent = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
843
- this.buffer = this.buffer.slice(safeTextLength);
844
- return {
845
- inFence: false,
846
- safeContent,
847
- completeFence: null,
848
- textAfterFence: ""
849
- };
850
- }
851
- const prefixText = this.buffer.slice(0, startIdx);
852
- const fenceStartLength = matchedPrefix?.length ?? 0;
853
- this.buffer = this.buffer.slice(startIdx + fenceStartLength);
854
- if (this.buffer.startsWith("\n")) {
855
- this.buffer = this.buffer.slice(1);
856
- }
857
- this.inFence = true;
858
- this.fenceStartBuffer = "";
859
- return {
860
- inFence: true,
861
- safeContent: prefixText,
862
- // Emit any text before the fence
863
- completeFence: null,
864
- textAfterFence: ""
865
- };
866
- }
867
- const closingIdx = this.buffer.indexOf(this.FENCE_END);
868
- if (closingIdx === -1) {
869
- const overlap = this.computeOverlapLength(this.buffer, [this.FENCE_END]);
870
- const safeContentLength = this.buffer.length - overlap;
871
- if (safeContentLength > 0) {
872
- const safeContent = this.buffer.slice(0, safeContentLength);
873
- this.fenceStartBuffer += safeContent;
874
- this.buffer = this.buffer.slice(safeContentLength);
875
- return {
876
- inFence: true,
877
- safeContent,
878
- completeFence: null,
879
- textAfterFence: ""
880
- };
881
- }
882
- return {
883
- inFence: true,
884
- safeContent: "",
885
- completeFence: null,
886
- textAfterFence: ""
887
- };
888
- }
889
- const fenceContent = this.buffer.slice(0, closingIdx);
890
- this.fenceStartBuffer += fenceContent;
891
- const completeFence = `${this.FENCE_STARTS[0]}
892
- ${this.fenceStartBuffer}
893
- ${this.FENCE_END}`;
894
- const textAfterFence = this.buffer.slice(
895
- closingIdx + this.FENCE_END.length
896
- );
897
- this.inFence = false;
898
- this.fenceStartBuffer = "";
899
- this.buffer = textAfterFence;
900
- return {
901
- inFence: false,
902
- safeContent: fenceContent,
903
- // Emit the last bit of fence content
904
- completeFence,
905
- textAfterFence
906
- };
907
- }
908
- /**
909
- * Check if currently inside a fence
910
- */
911
- isInFence() {
912
- return this.inFence;
913
- }
914
- /**
915
- * Reset streaming state
916
- */
917
- resetStreamingState() {
918
- this.inFence = false;
919
- this.fenceStartBuffer = "";
920
- }
921
- };
922
-
923
934
  // src/browser-ai-language-model.ts
924
935
  function doesBrowserSupportBrowserAI() {
925
936
  return typeof LanguageModel !== "undefined";
@@ -1089,19 +1100,19 @@ var BrowserAIChatLanguageModel = class {
1089
1100
  expectedInputs,
1090
1101
  functionTools
1091
1102
  } = converted;
1092
- const session = await this.getSession(void 0, expectedInputs, void 0);
1093
- const systemPrompt = await buildJsonToolSystemPrompt(
1103
+ const systemPrompt = buildJsonToolSystemPrompt(
1094
1104
  systemMessage,
1095
1105
  functionTools,
1096
1106
  {
1097
1107
  allowParallelToolCalls: false
1098
1108
  }
1099
1109
  );
1100
- const promptMessages = prependSystemPromptToMessages(
1101
- messages,
1102
- systemPrompt
1110
+ const session = await this.getSession(
1111
+ void 0,
1112
+ expectedInputs,
1113
+ systemPrompt || void 0
1103
1114
  );
1104
- const rawResponse = await session.prompt(promptMessages, promptOptions);
1115
+ const rawResponse = await session.prompt(messages, promptOptions);
1105
1116
  const { toolCalls, textContent } = parseJsonFunctionCalls(rawResponse);
1106
1117
  if (toolCalls.length > 0) {
1107
1118
  const toolCallsToEmit = toolCalls.slice(0, 1);
@@ -1136,7 +1147,7 @@ var BrowserAIChatLanguageModel = class {
1136
1147
  reasoning: void 0
1137
1148
  }
1138
1149
  },
1139
- request: { body: { messages: promptMessages, options: promptOptions } },
1150
+ request: { body: { messages, options: promptOptions } },
1140
1151
  warnings
1141
1152
  };
1142
1153
  }
@@ -1162,7 +1173,7 @@ var BrowserAIChatLanguageModel = class {
1162
1173
  reasoning: void 0
1163
1174
  }
1164
1175
  },
1165
- request: { body: { messages: promptMessages, options: promptOptions } },
1176
+ request: { body: { messages, options: promptOptions } },
1166
1177
  warnings
1167
1178
  };
1168
1179
  }
@@ -1209,23 +1220,23 @@ var BrowserAIChatLanguageModel = class {
1209
1220
  expectedInputs,
1210
1221
  functionTools
1211
1222
  } = converted;
1212
- const session = await this.getSession(void 0, expectedInputs, void 0);
1213
- const systemPrompt = await buildJsonToolSystemPrompt(
1223
+ const systemPrompt = buildJsonToolSystemPrompt(
1214
1224
  systemMessage,
1215
1225
  functionTools,
1216
1226
  {
1217
1227
  allowParallelToolCalls: false
1218
1228
  }
1219
1229
  );
1220
- const promptMessages = prependSystemPromptToMessages(
1221
- messages,
1222
- systemPrompt
1230
+ const session = await this.getSession(
1231
+ void 0,
1232
+ expectedInputs,
1233
+ systemPrompt || void 0
1223
1234
  );
1224
1235
  const streamOptions = {
1225
1236
  ...promptOptions,
1226
1237
  signal: options.abortSignal
1227
1238
  };
1228
- const conversationHistory = [...promptMessages];
1239
+ const conversationHistory = [...messages];
1229
1240
  const textId = "text-0";
1230
1241
  const stream = new ReadableStream({
1231
1242
  start: async (controller) => {
@@ -1531,7 +1542,7 @@ var BrowserAIChatLanguageModel = class {
1531
1542
  });
1532
1543
  return {
1533
1544
  stream,
1534
- request: { body: { messages: promptMessages, options: promptOptions } }
1545
+ request: { body: { messages, options: promptOptions } }
1535
1546
  };
1536
1547
  }
1537
1548
  };