@animalabs/membrane 0.1.0

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.
Files changed (131) hide show
  1. package/dist/context/index.d.ts +10 -0
  2. package/dist/context/index.d.ts.map +1 -0
  3. package/dist/context/index.js +9 -0
  4. package/dist/context/index.js.map +1 -0
  5. package/dist/context/process.d.ts +22 -0
  6. package/dist/context/process.d.ts.map +1 -0
  7. package/dist/context/process.js +369 -0
  8. package/dist/context/process.js.map +1 -0
  9. package/dist/context/types.d.ts +118 -0
  10. package/dist/context/types.d.ts.map +1 -0
  11. package/dist/context/types.js +60 -0
  12. package/dist/context/types.js.map +1 -0
  13. package/dist/index.d.ts +12 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +18 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/membrane.d.ts +96 -0
  18. package/dist/membrane.d.ts.map +1 -0
  19. package/dist/membrane.js +893 -0
  20. package/dist/membrane.js.map +1 -0
  21. package/dist/providers/anthropic.d.ts +36 -0
  22. package/dist/providers/anthropic.d.ts.map +1 -0
  23. package/dist/providers/anthropic.js +265 -0
  24. package/dist/providers/anthropic.js.map +1 -0
  25. package/dist/providers/index.d.ts +8 -0
  26. package/dist/providers/index.d.ts.map +1 -0
  27. package/dist/providers/index.js +8 -0
  28. package/dist/providers/index.js.map +1 -0
  29. package/dist/providers/openai-compatible.d.ts +74 -0
  30. package/dist/providers/openai-compatible.d.ts.map +1 -0
  31. package/dist/providers/openai-compatible.js +412 -0
  32. package/dist/providers/openai-compatible.js.map +1 -0
  33. package/dist/providers/openai.d.ts +69 -0
  34. package/dist/providers/openai.d.ts.map +1 -0
  35. package/dist/providers/openai.js +455 -0
  36. package/dist/providers/openai.js.map +1 -0
  37. package/dist/providers/openrouter.d.ts +76 -0
  38. package/dist/providers/openrouter.d.ts.map +1 -0
  39. package/dist/providers/openrouter.js +492 -0
  40. package/dist/providers/openrouter.js.map +1 -0
  41. package/dist/transforms/chat.d.ts +52 -0
  42. package/dist/transforms/chat.d.ts.map +1 -0
  43. package/dist/transforms/chat.js +136 -0
  44. package/dist/transforms/chat.js.map +1 -0
  45. package/dist/transforms/index.d.ts +6 -0
  46. package/dist/transforms/index.d.ts.map +1 -0
  47. package/dist/transforms/index.js +6 -0
  48. package/dist/transforms/index.js.map +1 -0
  49. package/dist/transforms/prefill.d.ts +89 -0
  50. package/dist/transforms/prefill.d.ts.map +1 -0
  51. package/dist/transforms/prefill.js +401 -0
  52. package/dist/transforms/prefill.js.map +1 -0
  53. package/dist/types/config.d.ts +103 -0
  54. package/dist/types/config.d.ts.map +1 -0
  55. package/dist/types/config.js +21 -0
  56. package/dist/types/config.js.map +1 -0
  57. package/dist/types/content.d.ts +81 -0
  58. package/dist/types/content.d.ts.map +1 -0
  59. package/dist/types/content.js +40 -0
  60. package/dist/types/content.js.map +1 -0
  61. package/dist/types/errors.d.ts +42 -0
  62. package/dist/types/errors.d.ts.map +1 -0
  63. package/dist/types/errors.js +208 -0
  64. package/dist/types/errors.js.map +1 -0
  65. package/dist/types/index.d.ts +18 -0
  66. package/dist/types/index.d.ts.map +1 -0
  67. package/dist/types/index.js +9 -0
  68. package/dist/types/index.js.map +1 -0
  69. package/dist/types/message.d.ts +46 -0
  70. package/dist/types/message.d.ts.map +1 -0
  71. package/dist/types/message.js +38 -0
  72. package/dist/types/message.js.map +1 -0
  73. package/dist/types/provider.d.ts +155 -0
  74. package/dist/types/provider.d.ts.map +1 -0
  75. package/dist/types/provider.js +5 -0
  76. package/dist/types/provider.js.map +1 -0
  77. package/dist/types/request.d.ts +78 -0
  78. package/dist/types/request.d.ts.map +1 -0
  79. package/dist/types/request.js +5 -0
  80. package/dist/types/request.js.map +1 -0
  81. package/dist/types/response.d.ts +131 -0
  82. package/dist/types/response.d.ts.map +1 -0
  83. package/dist/types/response.js +7 -0
  84. package/dist/types/response.js.map +1 -0
  85. package/dist/types/streaming.d.ts +164 -0
  86. package/dist/types/streaming.d.ts.map +1 -0
  87. package/dist/types/streaming.js +5 -0
  88. package/dist/types/streaming.js.map +1 -0
  89. package/dist/types/tools.d.ts +71 -0
  90. package/dist/types/tools.d.ts.map +1 -0
  91. package/dist/types/tools.js +5 -0
  92. package/dist/types/tools.js.map +1 -0
  93. package/dist/utils/index.d.ts +5 -0
  94. package/dist/utils/index.d.ts.map +1 -0
  95. package/dist/utils/index.js +5 -0
  96. package/dist/utils/index.js.map +1 -0
  97. package/dist/utils/stream-parser.d.ts +53 -0
  98. package/dist/utils/stream-parser.d.ts.map +1 -0
  99. package/dist/utils/stream-parser.js +359 -0
  100. package/dist/utils/stream-parser.js.map +1 -0
  101. package/dist/utils/tool-parser.d.ts +130 -0
  102. package/dist/utils/tool-parser.d.ts.map +1 -0
  103. package/dist/utils/tool-parser.js +571 -0
  104. package/dist/utils/tool-parser.js.map +1 -0
  105. package/package.json +37 -0
  106. package/src/context/index.ts +24 -0
  107. package/src/context/process.ts +520 -0
  108. package/src/context/types.ts +231 -0
  109. package/src/index.ts +23 -0
  110. package/src/membrane.ts +1174 -0
  111. package/src/providers/anthropic.ts +340 -0
  112. package/src/providers/index.ts +31 -0
  113. package/src/providers/openai-compatible.ts +570 -0
  114. package/src/providers/openai.ts +625 -0
  115. package/src/providers/openrouter.ts +662 -0
  116. package/src/transforms/chat.ts +212 -0
  117. package/src/transforms/index.ts +22 -0
  118. package/src/transforms/prefill.ts +585 -0
  119. package/src/types/config.ts +172 -0
  120. package/src/types/content.ts +181 -0
  121. package/src/types/errors.ts +277 -0
  122. package/src/types/index.ts +154 -0
  123. package/src/types/message.ts +89 -0
  124. package/src/types/provider.ts +249 -0
  125. package/src/types/request.ts +131 -0
  126. package/src/types/response.ts +223 -0
  127. package/src/types/streaming.ts +231 -0
  128. package/src/types/tools.ts +92 -0
  129. package/src/utils/index.ts +15 -0
  130. package/src/utils/stream-parser.ts +440 -0
  131. package/src/utils/tool-parser.ts +715 -0
@@ -0,0 +1,715 @@
1
+ /**
2
+ * Tool parsing utilities for XML-based tool calls
3
+ *
4
+ * Supports both plain and antml:-prefixed formats:
5
+ * <function_calls> or <function_calls>
6
+ * <invoke name="..."> or <invoke name="...">
7
+ * <parameter name="..."> or <parameter name="...">
8
+ *
9
+ * Also supports self-closing invoke tags:
10
+ * <invoke name="tool"/> or <invoke name="tool"/>
11
+ */
12
+
13
+ import type { ToolCall, ToolResult, ParsedToolCalls, ContentBlock, ToolResultContentBlock } from '../types/index.js';
14
+
15
+ // ============================================================================
16
+ // Tool Call Parsing
17
+ // ============================================================================
18
+
19
+ // Regex patterns supporting both plain and antml: prefix
20
+ // Pattern matches: <function_calls> or <function_calls>
21
+ const FUNCTION_CALLS_REGEX = /<(antml:)?function_calls>([\s\S]*?)<\/(antml:)?function_calls>/g;
22
+
23
+ // Full invoke tags with content
24
+ const INVOKE_REGEX_FULL = /<(antml:)?invoke\s+name="([^"]+)">([\s\S]*?)<\/(antml:)?invoke>/g;
25
+
26
+ // Self-closing invoke tags (no parameters)
27
+ const INVOKE_REGEX_SELF_CLOSE = /<(antml:)?invoke\s+name="([^"]+)"\s*\/>/g;
28
+
29
+ // Parameter tags
30
+ const PARAMETER_REGEX = /<(antml:)?parameter\s+name="([^"]+)">([\s\S]*?)<\/(antml:)?parameter>/g;
31
+
32
+ // Check for function_results following a block
33
+ const FUNCTION_RESULTS_START = /<(antml:)?function_results>/;
34
+
35
+ /**
36
+ * Parse tool calls from text containing XML function_calls blocks
37
+ *
38
+ * Uses "last-unexecuted-block" logic: finds the last function_calls block
39
+ * that doesn't have function_results immediately following it.
40
+ */
41
+ export function parseToolCalls(text: string): ParsedToolCalls | null {
42
+ // Reset regex
43
+ FUNCTION_CALLS_REGEX.lastIndex = 0;
44
+
45
+ // Find all function_calls blocks and pick the last unexecuted one
46
+ let blockMatch: RegExpExecArray | null = null;
47
+ let lastUnexecutedMatch: RegExpExecArray | null = null;
48
+
49
+ while ((blockMatch = FUNCTION_CALLS_REGEX.exec(text)) !== null) {
50
+ // Check if this block already has results after it
51
+ const afterPos = blockMatch.index + blockMatch[0].length;
52
+ const textAfter = text.slice(afterPos, afterPos + 100); // Check next 100 chars
53
+
54
+ if (!FUNCTION_RESULTS_START.test(textAfter.trimStart())) {
55
+ // This block hasn't been executed yet - store it
56
+ // Need to capture all properties since exec returns are reused
57
+ lastUnexecutedMatch = {
58
+ ...blockMatch,
59
+ index: blockMatch.index,
60
+ input: blockMatch.input,
61
+ } as RegExpExecArray;
62
+ }
63
+ }
64
+
65
+ if (!lastUnexecutedMatch) {
66
+ return null;
67
+ }
68
+
69
+ const fullMatch = lastUnexecutedMatch[0];
70
+ const innerContent = lastUnexecutedMatch[2] ?? ''; // Group 2 is content between tags
71
+ const matchIndex = lastUnexecutedMatch.index;
72
+
73
+ const beforeText = text.slice(0, matchIndex);
74
+ const afterText = text.slice(matchIndex + fullMatch.length);
75
+
76
+ const calls: ToolCall[] = [];
77
+
78
+ // Parse full invoke tags (with content and closing tag)
79
+ INVOKE_REGEX_FULL.lastIndex = 0;
80
+ let invokeMatch: RegExpExecArray | null;
81
+
82
+ while ((invokeMatch = INVOKE_REGEX_FULL.exec(innerContent)) !== null) {
83
+ const toolName = invokeMatch[2] ?? ''; // Group 2 is the name
84
+ const invokeContent = invokeMatch[3] ?? ''; // Group 3 is the content
85
+
86
+ // Parse parameters
87
+ const input: Record<string, unknown> = {};
88
+ PARAMETER_REGEX.lastIndex = 0;
89
+ let paramMatch: RegExpExecArray | null;
90
+
91
+ while ((paramMatch = PARAMETER_REGEX.exec(invokeContent)) !== null) {
92
+ const paramName = paramMatch[2] ?? ''; // Group 2 is the name
93
+ const paramValue = paramMatch[3] ?? ''; // Group 3 is the value
94
+
95
+ // Try to parse as JSON, fall back to string
96
+ try {
97
+ input[paramName] = JSON.parse(paramValue);
98
+ } catch {
99
+ input[paramName] = paramValue.trim();
100
+ }
101
+ }
102
+
103
+ calls.push({
104
+ id: generateToolId(),
105
+ name: toolName,
106
+ input,
107
+ });
108
+ }
109
+
110
+ // Parse self-closing invoke tags (no parameters)
111
+ INVOKE_REGEX_SELF_CLOSE.lastIndex = 0;
112
+ let selfCloseMatch: RegExpExecArray | null;
113
+
114
+ while ((selfCloseMatch = INVOKE_REGEX_SELF_CLOSE.exec(innerContent)) !== null) {
115
+ const toolName = selfCloseMatch[2] ?? ''; // Group 2 is the name
116
+
117
+ calls.push({
118
+ id: generateToolId(),
119
+ name: toolName,
120
+ input: {}, // No parameters for self-closing tag
121
+ });
122
+ }
123
+
124
+ return {
125
+ calls,
126
+ beforeText,
127
+ afterText,
128
+ fullMatch,
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Check if text contains an unclosed function_calls block
134
+ * Used for false-positive stop sequence detection
135
+ * Supports both plain and antml: prefixed tags
136
+ */
137
+ export function hasUnclosedToolBlock(text: string): boolean {
138
+ // Use regex that matches both plain and antml: prefixed tags
139
+ const openPattern = /<(antml:)?function_calls>/g;
140
+ const closePattern = /<\/(antml:)?function_calls>/g;
141
+
142
+ const openCount = (text.match(openPattern) || []).length;
143
+ const closeCount = (text.match(closePattern) || []).length;
144
+ return openCount > closeCount;
145
+ }
146
+
147
+ /**
148
+ * Check if text ends with a partial/unclosed tool block
149
+ * Supports both plain and antml: prefixed tags
150
+ */
151
+ export function endsWithPartialToolBlock(text: string): boolean {
152
+ // Check for partial opening tag (plain or antml:)
153
+ if (/<(antml:)?function_calls[^>]*$/.test(text)) return true;
154
+ if (/<(antml:)?invoke[^>]*$/.test(text)) return true;
155
+ if (/<(antml:)?parameter[^>]*$/.test(text)) return true;
156
+
157
+ // Check for unclosed block
158
+ return hasUnclosedToolBlock(text);
159
+ }
160
+
161
+ // ============================================================================
162
+ // Tool Result Formatting
163
+ // ============================================================================
164
+
165
+ /**
166
+ * Format tool results as XML for injection.
167
+ * Handles both string content and structured content blocks (with images).
168
+ */
169
+ export function formatToolResults(results: ToolResult[]): string {
170
+ const parts: string[] = ['<function_results>'];
171
+
172
+ for (const result of results) {
173
+ const tagName = result.isError ? 'error' : 'result';
174
+ parts.push(`<${tagName} tool_use_id="${result.toolUseId}">`);
175
+
176
+ // Handle both string and array content
177
+ if (typeof result.content === 'string') {
178
+ parts.push(escapeXml(result.content));
179
+ } else if (Array.isArray(result.content)) {
180
+ // Structured content blocks
181
+ for (const block of result.content) {
182
+ if (block.type === 'text') {
183
+ parts.push(escapeXml(block.text));
184
+ } else if (block.type === 'image') {
185
+ // For XML mode, we can't embed images directly
186
+ // Add a note about the image for the model
187
+ const sizeKb = Math.round((block.source.data.length * 0.75) / 1024);
188
+ parts.push(`[Image: ${block.source.mediaType}, ~${sizeKb}KB]`);
189
+ }
190
+ }
191
+ }
192
+
193
+ parts.push(`</${tagName}>`);
194
+ }
195
+
196
+ parts.push('</function_results>');
197
+ return parts.join('\n');
198
+ }
199
+
200
+ /**
201
+ * Format a single tool result
202
+ */
203
+ export function formatToolResult(result: ToolResult): string {
204
+ return formatToolResults([result]);
205
+ }
206
+
207
+ // ============================================================================
208
+ // Tool Definition Formatting (for system prompt injection)
209
+ // ============================================================================
210
+
211
+ export interface ToolDefinitionForPrompt {
212
+ name: string;
213
+ description: string;
214
+ parameters: Record<string, {
215
+ type: string;
216
+ description?: string;
217
+ required?: boolean;
218
+ enum?: string[];
219
+ }>;
220
+ }
221
+
222
+ /**
223
+ * Format tool definitions as XML for system prompt
224
+ */
225
+ export function formatToolDefinitions(tools: ToolDefinitionForPrompt[]): string {
226
+ const parts: string[] = ['<tools>'];
227
+
228
+ for (const tool of tools) {
229
+ parts.push(`<tool name="${escapeXml(tool.name)}">`);
230
+ parts.push(`<description>${escapeXml(tool.description)}</description>`);
231
+ parts.push('<parameters>');
232
+
233
+ for (const [paramName, param] of Object.entries(tool.parameters)) {
234
+ const attrs: string[] = [`name="${escapeXml(paramName)}"`, `type="${param.type}"`];
235
+ if (param.required) attrs.push('required="true"');
236
+ if (param.enum) attrs.push(`enum="${param.enum.join(',')}"`);
237
+
238
+ parts.push(`<parameter ${attrs.join(' ')}>`);
239
+ if (param.description) {
240
+ parts.push(escapeXml(param.description));
241
+ }
242
+ parts.push('</parameter>');
243
+ }
244
+
245
+ parts.push('</parameters>');
246
+ parts.push('</tool>');
247
+ }
248
+
249
+ parts.push('</tools>');
250
+ return parts.join('\n');
251
+ }
252
+
253
+ // ============================================================================
254
+ // Accumulated Text to ContentBlock[] Parsing
255
+ // ============================================================================
256
+
257
+ // Regex for matching thinking blocks (both plain and antml: prefixed)
258
+ const THINKING_BLOCK_REGEX = /<(antml:)?thinking>([\s\S]*?)<\/(antml:)?thinking>/g;
259
+
260
+ // Regex for matching function_calls blocks with their content
261
+ const FUNCTION_BLOCK_WITH_CONTENT_REGEX = /<(antml:)?function_calls>([\s\S]*?)<\/(antml:)?function_calls>/g;
262
+
263
+ // Regex for matching function_results blocks with their content
264
+ const FUNCTION_RESULTS_BLOCK_REGEX = /<(antml:)?function_results>([\s\S]*?)<\/(antml:)?function_results>/g;
265
+
266
+ // Regex for individual result/error within function_results
267
+ const RESULT_REGEX = /<result\s+tool_use_id="([^"]+)">([\s\S]*?)<\/result>/g;
268
+ const ERROR_REGEX = /<error\s+tool_use_id="([^"]+)">([\s\S]*?)<\/error>/g;
269
+
270
+ /**
271
+ * Parse accumulated assistant text into structured ContentBlock[].
272
+ * Extracts thinking blocks, tool calls, tool results, and plain text.
273
+ *
274
+ * @param text - The accumulated assistant output text
275
+ * @returns Array of ContentBlock in order of appearance
276
+ */
277
+ export function parseAccumulatedIntoBlocks(text: string): {
278
+ blocks: ContentBlock[];
279
+ toolCalls: ToolCall[];
280
+ toolResults: ToolResult[];
281
+ } {
282
+ const blocks: ContentBlock[] = [];
283
+ const toolCalls: ToolCall[] = [];
284
+ const toolResults: ToolResult[] = [];
285
+
286
+ // Track positions of all special blocks to extract plain text between them
287
+ type BlockPosition = {
288
+ start: number;
289
+ end: number;
290
+ block: ContentBlock | ContentBlock[];
291
+ };
292
+ const positions: BlockPosition[] = [];
293
+
294
+ // Find all thinking blocks
295
+ THINKING_BLOCK_REGEX.lastIndex = 0;
296
+ let thinkingMatch: RegExpExecArray | null;
297
+ while ((thinkingMatch = THINKING_BLOCK_REGEX.exec(text)) !== null) {
298
+ positions.push({
299
+ start: thinkingMatch.index,
300
+ end: thinkingMatch.index + thinkingMatch[0].length,
301
+ block: {
302
+ type: 'thinking',
303
+ thinking: thinkingMatch[2] ?? '',
304
+ },
305
+ });
306
+ }
307
+
308
+ // Find all function_calls blocks and parse their tool calls
309
+ FUNCTION_BLOCK_WITH_CONTENT_REGEX.lastIndex = 0;
310
+ let funcMatch: RegExpExecArray | null;
311
+ while ((funcMatch = FUNCTION_BLOCK_WITH_CONTENT_REGEX.exec(text)) !== null) {
312
+ const innerContent = funcMatch[2] ?? '';
313
+ const blockToolCalls: ContentBlock[] = [];
314
+
315
+ // Parse invoke tags in this block
316
+ INVOKE_REGEX_FULL.lastIndex = 0;
317
+ let invokeMatch: RegExpExecArray | null;
318
+ while ((invokeMatch = INVOKE_REGEX_FULL.exec(innerContent)) !== null) {
319
+ const toolName = invokeMatch[2] ?? '';
320
+ const invokeContent = invokeMatch[3] ?? '';
321
+ const input: Record<string, unknown> = {};
322
+
323
+ // Parse parameters
324
+ PARAMETER_REGEX.lastIndex = 0;
325
+ let paramMatch: RegExpExecArray | null;
326
+ while ((paramMatch = PARAMETER_REGEX.exec(invokeContent)) !== null) {
327
+ const paramName = paramMatch[2] ?? '';
328
+ const paramValue = paramMatch[3] ?? '';
329
+ try {
330
+ input[paramName] = JSON.parse(paramValue);
331
+ } catch {
332
+ input[paramName] = paramValue.trim();
333
+ }
334
+ }
335
+
336
+ const id = generateToolId();
337
+ const toolCall: ToolCall = { id, name: toolName, input };
338
+ toolCalls.push(toolCall);
339
+ blockToolCalls.push({
340
+ type: 'tool_use',
341
+ id,
342
+ name: toolName,
343
+ input,
344
+ });
345
+ }
346
+
347
+ // Parse self-closing invoke tags
348
+ INVOKE_REGEX_SELF_CLOSE.lastIndex = 0;
349
+ let selfCloseMatch: RegExpExecArray | null;
350
+ while ((selfCloseMatch = INVOKE_REGEX_SELF_CLOSE.exec(innerContent)) !== null) {
351
+ const toolName = selfCloseMatch[2] ?? '';
352
+ const id = generateToolId();
353
+ const toolCall: ToolCall = { id, name: toolName, input: {} };
354
+ toolCalls.push(toolCall);
355
+ blockToolCalls.push({
356
+ type: 'tool_use',
357
+ id,
358
+ name: toolName,
359
+ input: {},
360
+ });
361
+ }
362
+
363
+ if (blockToolCalls.length > 0) {
364
+ positions.push({
365
+ start: funcMatch.index,
366
+ end: funcMatch.index + funcMatch[0].length,
367
+ block: blockToolCalls,
368
+ });
369
+ }
370
+ }
371
+
372
+ // Find all function_results blocks and parse their results
373
+ FUNCTION_RESULTS_BLOCK_REGEX.lastIndex = 0;
374
+ let resultsMatch: RegExpExecArray | null;
375
+ while ((resultsMatch = FUNCTION_RESULTS_BLOCK_REGEX.exec(text)) !== null) {
376
+ const innerContent = resultsMatch[2] ?? '';
377
+ const blockResults: ContentBlock[] = [];
378
+
379
+ // Parse result tags
380
+ RESULT_REGEX.lastIndex = 0;
381
+ let resultMatch: RegExpExecArray | null;
382
+ while ((resultMatch = RESULT_REGEX.exec(innerContent)) !== null) {
383
+ const toolUseId = resultMatch[1] ?? '';
384
+ const content = unescapeXml(resultMatch[2] ?? '');
385
+ const result: ToolResult = { toolUseId, content, isError: false };
386
+ toolResults.push(result);
387
+ blockResults.push({
388
+ type: 'tool_result',
389
+ toolUseId,
390
+ content,
391
+ isError: false,
392
+ });
393
+ }
394
+
395
+ // Parse error tags
396
+ ERROR_REGEX.lastIndex = 0;
397
+ let errorMatch: RegExpExecArray | null;
398
+ while ((errorMatch = ERROR_REGEX.exec(innerContent)) !== null) {
399
+ const toolUseId = errorMatch[1] ?? '';
400
+ const content = unescapeXml(errorMatch[2] ?? '');
401
+ const result: ToolResult = { toolUseId, content, isError: true };
402
+ toolResults.push(result);
403
+ blockResults.push({
404
+ type: 'tool_result',
405
+ toolUseId,
406
+ content,
407
+ isError: true,
408
+ });
409
+ }
410
+
411
+ if (blockResults.length > 0) {
412
+ positions.push({
413
+ start: resultsMatch.index,
414
+ end: resultsMatch.index + resultsMatch[0].length,
415
+ block: blockResults,
416
+ });
417
+ }
418
+ }
419
+
420
+ // Sort positions by start index
421
+ positions.sort((a, b) => a.start - b.start);
422
+
423
+ // Build final blocks array, inserting text blocks between special blocks
424
+ let lastEnd = 0;
425
+ for (const pos of positions) {
426
+ // Add text block for content before this special block
427
+ if (pos.start > lastEnd) {
428
+ const textContent = text.slice(lastEnd, pos.start).trim();
429
+ if (textContent) {
430
+ blocks.push({ type: 'text', text: textContent });
431
+ }
432
+ }
433
+
434
+ // Add the special block(s)
435
+ if (Array.isArray(pos.block)) {
436
+ blocks.push(...pos.block);
437
+ } else {
438
+ blocks.push(pos.block);
439
+ }
440
+
441
+ lastEnd = pos.end;
442
+ }
443
+
444
+ // Add any remaining text after the last special block
445
+ if (lastEnd < text.length) {
446
+ const textContent = text.slice(lastEnd).trim();
447
+ if (textContent) {
448
+ blocks.push({ type: 'text', text: textContent });
449
+ }
450
+ }
451
+
452
+ // Handle case where there are no special blocks at all
453
+ if (positions.length === 0 && text.trim()) {
454
+ blocks.push({ type: 'text', text: text.trim() });
455
+ }
456
+
457
+ return { blocks, toolCalls, toolResults };
458
+ }
459
+
460
+ // ============================================================================
461
+ // Tool Instructions (for manual placement)
462
+ // ============================================================================
463
+
464
+ import type { ToolDefinition } from '../types/index.js';
465
+
466
+ // Assembled to avoid triggering stop sequences in model output
467
+ const FUNC_CALLS_OPEN = '<' + 'function_calls>';
468
+ const FUNC_CALLS_CLOSE = '</' + 'function_calls>';
469
+ const INVOKE_OPEN = '<' + 'invoke name="';
470
+ const INVOKE_CLOSE = '</' + 'invoke>';
471
+ const PARAM_OPEN = '<' + 'parameter name="';
472
+ const PARAM_CLOSE = '</' + 'parameter>';
473
+
474
+ /**
475
+ * Get tool instructions string for manual placement.
476
+ * Use this when you want to control where tool instructions appear
477
+ * (e.g., injected into conversation rather than system prompt).
478
+ *
479
+ * @param tools - Tool definitions
480
+ * @returns Complete instruction string with definitions and usage example
481
+ */
482
+ export function getToolInstructions(tools: ToolDefinition[]): string {
483
+ // Format definitions
484
+ const definitions = tools.map((tool) => {
485
+ const toolDef = {
486
+ description: tool.description,
487
+ name: tool.name,
488
+ parameters: tool.inputSchema,
489
+ };
490
+ return `<function>${JSON.stringify(toolDef)}</function>`;
491
+ });
492
+
493
+ // Build instruction with example
494
+ return `<functions>
495
+ ${definitions.join('\n')}
496
+ </functions>
497
+
498
+ When making function calls using tools that accept array or object parameters ensure those are structured using JSON. For example:
499
+ ${FUNC_CALLS_OPEN}
500
+ ${INVOKE_OPEN}example_tool">
501
+ ${PARAM_OPEN}parameter">[{"key": "value"}]${PARAM_CLOSE}
502
+ ${INVOKE_CLOSE}
503
+ ${FUNC_CALLS_CLOSE}`;
504
+ }
505
+
506
+ // ============================================================================
507
+ // Image Handling in Tool Results
508
+ // ============================================================================
509
+
510
+ /**
511
+ * Provider image block format (Anthropic-style)
512
+ */
513
+ export interface ProviderImageBlock {
514
+ type: 'image';
515
+ source: {
516
+ type: 'base64';
517
+ media_type: string;
518
+ data: string;
519
+ };
520
+ }
521
+
522
+ /**
523
+ * Check if any tool result contains image content
524
+ */
525
+ export function hasImageInToolResults(results: ToolResult[]): boolean {
526
+ for (const result of results) {
527
+ if (Array.isArray(result.content)) {
528
+ if (result.content.some(block => block.type === 'image')) {
529
+ return true;
530
+ }
531
+ }
532
+ }
533
+ return false;
534
+ }
535
+
536
+ /**
537
+ * Result of separating tool result content for split-turn injection.
538
+ *
539
+ * When tool results contain images in prefill mode, we need to:
540
+ * 1. Put text content in the assistant turn (as XML)
541
+ * 2. Extract images into a separate user turn
542
+ * 3. Continue assistant turn with closing XML
543
+ */
544
+ export interface SplitTurnContent {
545
+ /** XML up to and including text content, ending mid-result if images present */
546
+ beforeImageXml: string;
547
+
548
+ /** Images extracted from results (in provider format) */
549
+ images: ProviderImageBlock[];
550
+
551
+ /** Closing XML after images (closing result tags, function_results) */
552
+ afterImageXml: string;
553
+
554
+ /** Whether any images were found */
555
+ hasImages: boolean;
556
+ }
557
+
558
+ /**
559
+ * Format tool results for split-turn injection when images are present.
560
+ *
561
+ * This separates the XML into parts that go in the assistant turn (text)
562
+ * and the user turn (images), with continuation XML for the next assistant turn.
563
+ *
564
+ * Structure when images present:
565
+ * ```
566
+ * Assistant: <function_results>
567
+ * <result tool_use_id="...">
568
+ * text content here
569
+ * [END - mid XML]
570
+ *
571
+ * User: [image blocks]
572
+ *
573
+ * Assistant (prefill): </result>
574
+ * </function_results>
575
+ * ```
576
+ */
577
+ export function formatToolResultsForSplitTurn(results: ToolResult[]): SplitTurnContent {
578
+ const images: ProviderImageBlock[] = [];
579
+ let beforeImageXml = '<function_results>\n';
580
+ let afterImageXml = '';
581
+ let imageInsertionPoint = -1; // Index of result where we found images
582
+
583
+ for (let i = 0; i < results.length; i++) {
584
+ const result = results[i]!;
585
+ const tagName = result.isError ? 'error' : 'result';
586
+
587
+ // Check if this result has images
588
+ let resultHasImages = false;
589
+ let textParts: string[] = [];
590
+ let resultImages: ProviderImageBlock[] = [];
591
+
592
+ if (typeof result.content === 'string') {
593
+ textParts.push(escapeXml(result.content));
594
+ } else if (Array.isArray(result.content)) {
595
+ for (const block of result.content) {
596
+ if (block.type === 'text') {
597
+ textParts.push(escapeXml(block.text));
598
+ } else if (block.type === 'image') {
599
+ resultHasImages = true;
600
+ resultImages.push({
601
+ type: 'image',
602
+ source: {
603
+ type: 'base64',
604
+ media_type: block.source.mediaType,
605
+ data: block.source.data,
606
+ },
607
+ });
608
+ }
609
+ }
610
+ }
611
+
612
+ if (resultHasImages && imageInsertionPoint === -1) {
613
+ // First result with images - split here
614
+ imageInsertionPoint = i;
615
+ images.push(...resultImages);
616
+
617
+ // Add opening tag and text content (no closing tag yet)
618
+ beforeImageXml += `<${tagName} tool_use_id="${result.toolUseId}">\n`;
619
+ if (textParts.length > 0) {
620
+ beforeImageXml += textParts.join('\n');
621
+ }
622
+ // Note: Intentionally NOT adding closing tag - split happens here
623
+
624
+ // After image, we need to close this result and add remaining results
625
+ afterImageXml = `</${tagName}>\n`;
626
+
627
+ // Process remaining results into afterImageXml
628
+ for (let j = i + 1; j < results.length; j++) {
629
+ const remainingResult = results[j]!;
630
+ afterImageXml += formatSingleResultXml(remainingResult);
631
+ }
632
+ afterImageXml += '</function_results>';
633
+
634
+ // Stop processing - we've handled everything
635
+ break;
636
+ } else if (imageInsertionPoint === -1) {
637
+ // No images yet - add full result to beforeImageXml
638
+ beforeImageXml += `<${tagName} tool_use_id="${result.toolUseId}">\n`;
639
+ beforeImageXml += textParts.join('\n');
640
+ beforeImageXml += `\n</${tagName}>\n`;
641
+ }
642
+ }
643
+
644
+ // If no images were found, complete the XML normally
645
+ if (imageInsertionPoint === -1) {
646
+ beforeImageXml += '</function_results>';
647
+ return {
648
+ beforeImageXml,
649
+ images: [],
650
+ afterImageXml: '',
651
+ hasImages: false,
652
+ };
653
+ }
654
+
655
+ return {
656
+ beforeImageXml,
657
+ images,
658
+ afterImageXml,
659
+ hasImages: true,
660
+ };
661
+ }
662
+
663
+ /**
664
+ * Format a single tool result as complete XML
665
+ */
666
+ function formatSingleResultXml(result: ToolResult): string {
667
+ const tagName = result.isError ? 'error' : 'result';
668
+ let xml = `<${tagName} tool_use_id="${result.toolUseId}">\n`;
669
+
670
+ if (typeof result.content === 'string') {
671
+ xml += escapeXml(result.content);
672
+ } else if (Array.isArray(result.content)) {
673
+ for (const block of result.content) {
674
+ if (block.type === 'text') {
675
+ xml += escapeXml(block.text);
676
+ } else if (block.type === 'image') {
677
+ // For remaining results after split, images become text placeholders
678
+ const sizeKb = Math.round((block.source.data.length * 0.75) / 1024);
679
+ xml += `[Image: ${block.source.mediaType}, ~${sizeKb}KB]`;
680
+ }
681
+ }
682
+ }
683
+
684
+ xml += `\n</${tagName}>\n`;
685
+ return xml;
686
+ }
687
+
688
+ // ============================================================================
689
+ // Utilities
690
+ // ============================================================================
691
+
692
+ let toolIdCounter = 0;
693
+
694
+ function generateToolId(): string {
695
+ toolIdCounter++;
696
+ return `tool_${Date.now()}_${toolIdCounter}`;
697
+ }
698
+
699
+ function escapeXml(text: string): string {
700
+ return text
701
+ .replace(/&/g, '&amp;')
702
+ .replace(/</g, '&lt;')
703
+ .replace(/>/g, '&gt;')
704
+ .replace(/"/g, '&quot;')
705
+ .replace(/'/g, '&apos;');
706
+ }
707
+
708
+ export function unescapeXml(text: string): string {
709
+ return text
710
+ .replace(/&apos;/g, "'")
711
+ .replace(/&quot;/g, '"')
712
+ .replace(/&gt;/g, '>')
713
+ .replace(/&lt;/g, '<')
714
+ .replace(/&amp;/g, '&');
715
+ }