@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.js CHANGED
@@ -28,10 +28,82 @@ __export(index_exports, {
28
28
  });
29
29
  module.exports = __toCommonJS(index_exports);
30
30
 
31
- // src/convert-to-browser-ai-messages.ts
32
- var import_provider = require("@ai-sdk/provider");
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}
74
+
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.
33
86
 
34
- // src/tool-calling/format-tool-results.ts
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);
@@ -246,151 +676,27 @@ function convertToBrowserAIMessages(prompt) {
246
676
  break;
247
677
  }
248
678
  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
- }
368
- }
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
- textContent = textContent.replace(/\n{2,}/g, "\n");
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) {
@@ -482,49 +788,6 @@ function getExpectedInputs(prompt) {
482
788
  }
483
789
  return Array.from(inputs).map((type) => ({ type }));
484
790
  }
485
- function prependSystemPromptToMessages(messages, systemPrompt) {
486
- if (!systemPrompt.trim()) {
487
- return messages;
488
- }
489
- const prompts = messages.map((message) => ({ ...message }));
490
- const firstUserIndex = prompts.findIndex(
491
- (message) => message.role === "user"
492
- );
493
- if (firstUserIndex !== -1) {
494
- const firstUserMessage = prompts[firstUserIndex];
495
- if (Array.isArray(firstUserMessage.content)) {
496
- const content = firstUserMessage.content.slice();
497
- content.unshift({
498
- type: "text",
499
- value: `${systemPrompt}
500
-
501
- `
502
- });
503
- prompts[firstUserIndex] = {
504
- ...firstUserMessage,
505
- content
506
- };
507
- } else if (typeof firstUserMessage.content === "string") {
508
- prompts[firstUserIndex] = {
509
- ...firstUserMessage,
510
- content: `${systemPrompt}
511
-
512
- ${firstUserMessage.content}`
513
- };
514
- }
515
- } else {
516
- prompts.unshift({
517
- role: "user",
518
- content: systemPrompt
519
- });
520
- }
521
- return prompts;
522
- }
523
-
524
- // src/utils/tool-utils.ts
525
- function isFunctionTool(tool) {
526
- return tool.type === "function";
527
- }
528
791
 
529
792
  // src/models/session-manager.ts
530
793
  var import_provider2 = require("@ai-sdk/provider");
@@ -696,258 +959,6 @@ var SessionManager = class {
696
959
  }
697
960
  };
698
961
 
699
- // src/streaming/tool-call-detector.ts
700
- var ToolCallFenceDetector = class {
701
- constructor() {
702
- this.FENCE_STARTS = ["```tool_call"];
703
- this.FENCE_END = "```";
704
- this.buffer = "";
705
- // Streaming state
706
- this.inFence = false;
707
- this.fenceStartBuffer = "";
708
- }
709
- // Accumulated fence content
710
- /**
711
- * Adds a chunk of text to the internal buffer
712
- *
713
- * @param chunk - Text chunk from the stream
714
- */
715
- addChunk(chunk) {
716
- this.buffer += chunk;
717
- }
718
- /**
719
- * Gets the current buffer content
720
- */
721
- getBuffer() {
722
- return this.buffer;
723
- }
724
- /**
725
- * Clears the internal buffer
726
- */
727
- clearBuffer() {
728
- this.buffer = "";
729
- }
730
- /**
731
- * Detects if there's a complete fence in the buffer
732
- *
733
- * This method:
734
- * 1. Searches for fence start markers
735
- * 2. If found, looks for closing fence
736
- * 3. Computes overlap for partial fences
737
- * 4. Returns safe text that can be emitted
738
- *
739
- * @returns Detection result with fence info and safe text
740
- */
741
- detectFence() {
742
- const { index: startIdx, prefix: matchedPrefix } = this.findFenceStart(
743
- this.buffer
744
- );
745
- if (startIdx === -1) {
746
- const overlap = this.computeOverlapLength(this.buffer, this.FENCE_STARTS);
747
- const safeTextLength = this.buffer.length - overlap;
748
- const prefixText2 = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
749
- const remaining = overlap > 0 ? this.buffer.slice(-overlap) : "";
750
- this.buffer = remaining;
751
- return {
752
- fence: null,
753
- prefixText: prefixText2,
754
- remainingText: "",
755
- overlapLength: overlap
756
- };
757
- }
758
- const prefixText = this.buffer.slice(0, startIdx);
759
- this.buffer = this.buffer.slice(startIdx);
760
- const prefixLength = matchedPrefix?.length ?? 0;
761
- const closingIdx = this.buffer.indexOf(this.FENCE_END, prefixLength);
762
- if (closingIdx === -1) {
763
- return {
764
- fence: null,
765
- prefixText,
766
- remainingText: "",
767
- overlapLength: 0
768
- };
769
- }
770
- const endPos = closingIdx + this.FENCE_END.length;
771
- const fence = this.buffer.slice(0, endPos);
772
- const remainingText = this.buffer.slice(endPos);
773
- this.buffer = "";
774
- return {
775
- fence,
776
- prefixText,
777
- remainingText,
778
- overlapLength: 0
779
- };
780
- }
781
- /**
782
- * Finds the first occurrence of any fence start marker
783
- *
784
- * @param text - Text to search in
785
- * @returns Index of first fence start and which prefix matched
786
- * @private
787
- */
788
- findFenceStart(text) {
789
- let bestIndex = -1;
790
- let matchedPrefix = null;
791
- for (const prefix of this.FENCE_STARTS) {
792
- const idx = text.indexOf(prefix);
793
- if (idx !== -1 && (bestIndex === -1 || idx < bestIndex)) {
794
- bestIndex = idx;
795
- matchedPrefix = prefix;
796
- }
797
- }
798
- return { index: bestIndex, prefix: matchedPrefix };
799
- }
800
- /**
801
- * Computes the maximum overlap between the end of text and the start of any prefix
802
- *
803
- * This is crucial for streaming: if the buffer ends with "``", we can't emit it
804
- * because the next chunk might be "`tool_call", completing a fence marker.
805
- *
806
- * @param text - Text to check for overlap
807
- * @param prefixes - List of prefixes to check against
808
- * @returns Length of the maximum overlap found
809
- *
810
- * @example
811
- * ```typescript
812
- * computeOverlapLength("hello ``", ["```tool_call"])
813
- * // Returns: 2 (because "``" matches start of "```tool_call")
814
- *
815
- * computeOverlapLength("hello `", ["```tool_call"])
816
- * // Returns: 1
817
- *
818
- * computeOverlapLength("hello world", ["```tool_call"])
819
- * // Returns: 0 (no overlap)
820
- * ```
821
- *
822
- * @private
823
- */
824
- computeOverlapLength(text, prefixes) {
825
- let overlap = 0;
826
- for (const prefix of prefixes) {
827
- const maxLength = Math.min(text.length, prefix.length - 1);
828
- for (let size = maxLength; size > 0; size -= 1) {
829
- if (prefix.startsWith(text.slice(-size))) {
830
- overlap = Math.max(overlap, size);
831
- break;
832
- }
833
- }
834
- }
835
- return overlap;
836
- }
837
- /**
838
- * Checks if the buffer currently contains any text
839
- */
840
- hasContent() {
841
- return this.buffer.length > 0;
842
- }
843
- /**
844
- * Gets the buffer size
845
- */
846
- getBufferSize() {
847
- return this.buffer.length;
848
- }
849
- /**
850
- * Detect and stream fence content in real-time for true incremental streaming
851
- *
852
- * This method is designed for streaming tool calls as they arrive:
853
- * 1. Detects when a fence starts and transitions to "inFence" state
854
- * 2. While inFence, emits safe content that won't conflict with fence end marker
855
- * 3. When fence ends, returns the complete fence for parsing
856
- *
857
- * @returns Streaming result with current state and safe content to emit
858
- */
859
- detectStreamingFence() {
860
- if (!this.inFence) {
861
- const { index: startIdx, prefix: matchedPrefix } = this.findFenceStart(
862
- this.buffer
863
- );
864
- if (startIdx === -1) {
865
- const overlap = this.computeOverlapLength(
866
- this.buffer,
867
- this.FENCE_STARTS
868
- );
869
- const safeTextLength = this.buffer.length - overlap;
870
- const safeContent = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
871
- this.buffer = this.buffer.slice(safeTextLength);
872
- return {
873
- inFence: false,
874
- safeContent,
875
- completeFence: null,
876
- textAfterFence: ""
877
- };
878
- }
879
- const prefixText = this.buffer.slice(0, startIdx);
880
- const fenceStartLength = matchedPrefix?.length ?? 0;
881
- this.buffer = this.buffer.slice(startIdx + fenceStartLength);
882
- if (this.buffer.startsWith("\n")) {
883
- this.buffer = this.buffer.slice(1);
884
- }
885
- this.inFence = true;
886
- this.fenceStartBuffer = "";
887
- return {
888
- inFence: true,
889
- safeContent: prefixText,
890
- // Emit any text before the fence
891
- completeFence: null,
892
- textAfterFence: ""
893
- };
894
- }
895
- const closingIdx = this.buffer.indexOf(this.FENCE_END);
896
- if (closingIdx === -1) {
897
- const overlap = this.computeOverlapLength(this.buffer, [this.FENCE_END]);
898
- const safeContentLength = this.buffer.length - overlap;
899
- if (safeContentLength > 0) {
900
- const safeContent = this.buffer.slice(0, safeContentLength);
901
- this.fenceStartBuffer += safeContent;
902
- this.buffer = this.buffer.slice(safeContentLength);
903
- return {
904
- inFence: true,
905
- safeContent,
906
- completeFence: null,
907
- textAfterFence: ""
908
- };
909
- }
910
- return {
911
- inFence: true,
912
- safeContent: "",
913
- completeFence: null,
914
- textAfterFence: ""
915
- };
916
- }
917
- const fenceContent = this.buffer.slice(0, closingIdx);
918
- this.fenceStartBuffer += fenceContent;
919
- const completeFence = `${this.FENCE_STARTS[0]}
920
- ${this.fenceStartBuffer}
921
- ${this.FENCE_END}`;
922
- const textAfterFence = this.buffer.slice(
923
- closingIdx + this.FENCE_END.length
924
- );
925
- this.inFence = false;
926
- this.fenceStartBuffer = "";
927
- this.buffer = textAfterFence;
928
- return {
929
- inFence: false,
930
- safeContent: fenceContent,
931
- // Emit the last bit of fence content
932
- completeFence,
933
- textAfterFence
934
- };
935
- }
936
- /**
937
- * Check if currently inside a fence
938
- */
939
- isInFence() {
940
- return this.inFence;
941
- }
942
- /**
943
- * Reset streaming state
944
- */
945
- resetStreamingState() {
946
- this.inFence = false;
947
- this.fenceStartBuffer = "";
948
- }
949
- };
950
-
951
962
  // src/browser-ai-language-model.ts
952
963
  function doesBrowserSupportBrowserAI() {
953
964
  return typeof LanguageModel !== "undefined";
@@ -1117,19 +1128,19 @@ var BrowserAIChatLanguageModel = class {
1117
1128
  expectedInputs,
1118
1129
  functionTools
1119
1130
  } = converted;
1120
- const session = await this.getSession(void 0, expectedInputs, void 0);
1121
- const systemPrompt = await buildJsonToolSystemPrompt(
1131
+ const systemPrompt = buildJsonToolSystemPrompt(
1122
1132
  systemMessage,
1123
1133
  functionTools,
1124
1134
  {
1125
1135
  allowParallelToolCalls: false
1126
1136
  }
1127
1137
  );
1128
- const promptMessages = prependSystemPromptToMessages(
1129
- messages,
1130
- systemPrompt
1138
+ const session = await this.getSession(
1139
+ void 0,
1140
+ expectedInputs,
1141
+ systemPrompt || void 0
1131
1142
  );
1132
- const rawResponse = await session.prompt(promptMessages, promptOptions);
1143
+ const rawResponse = await session.prompt(messages, promptOptions);
1133
1144
  const { toolCalls, textContent } = parseJsonFunctionCalls(rawResponse);
1134
1145
  if (toolCalls.length > 0) {
1135
1146
  const toolCallsToEmit = toolCalls.slice(0, 1);
@@ -1164,7 +1175,7 @@ var BrowserAIChatLanguageModel = class {
1164
1175
  reasoning: void 0
1165
1176
  }
1166
1177
  },
1167
- request: { body: { messages: promptMessages, options: promptOptions } },
1178
+ request: { body: { messages, options: promptOptions } },
1168
1179
  warnings
1169
1180
  };
1170
1181
  }
@@ -1190,7 +1201,7 @@ var BrowserAIChatLanguageModel = class {
1190
1201
  reasoning: void 0
1191
1202
  }
1192
1203
  },
1193
- request: { body: { messages: promptMessages, options: promptOptions } },
1204
+ request: { body: { messages, options: promptOptions } },
1194
1205
  warnings
1195
1206
  };
1196
1207
  }
@@ -1237,23 +1248,23 @@ var BrowserAIChatLanguageModel = class {
1237
1248
  expectedInputs,
1238
1249
  functionTools
1239
1250
  } = converted;
1240
- const session = await this.getSession(void 0, expectedInputs, void 0);
1241
- const systemPrompt = await buildJsonToolSystemPrompt(
1251
+ const systemPrompt = buildJsonToolSystemPrompt(
1242
1252
  systemMessage,
1243
1253
  functionTools,
1244
1254
  {
1245
1255
  allowParallelToolCalls: false
1246
1256
  }
1247
1257
  );
1248
- const promptMessages = prependSystemPromptToMessages(
1249
- messages,
1250
- systemPrompt
1258
+ const session = await this.getSession(
1259
+ void 0,
1260
+ expectedInputs,
1261
+ systemPrompt || void 0
1251
1262
  );
1252
1263
  const streamOptions = {
1253
1264
  ...promptOptions,
1254
1265
  signal: options.abortSignal
1255
1266
  };
1256
- const conversationHistory = [...promptMessages];
1267
+ const conversationHistory = [...messages];
1257
1268
  const textId = "text-0";
1258
1269
  const stream = new ReadableStream({
1259
1270
  start: async (controller) => {
@@ -1559,7 +1570,7 @@ var BrowserAIChatLanguageModel = class {
1559
1570
  });
1560
1571
  return {
1561
1572
  stream,
1562
- request: { body: { messages: promptMessages, options: promptOptions } }
1573
+ request: { body: { messages, options: promptOptions } }
1563
1574
  };
1564
1575
  }
1565
1576
  };