@animalabs/membrane 0.5.78 → 0.5.80

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 (58) hide show
  1. package/dist/cache-keepalive.d.ts +115 -0
  2. package/dist/cache-keepalive.d.ts.map +1 -0
  3. package/dist/cache-keepalive.js +0 -0
  4. package/dist/cache-keepalive.js.map +1 -0
  5. package/dist/cache-keepalive.test.d.ts +2 -0
  6. package/dist/cache-keepalive.test.d.ts.map +1 -0
  7. package/dist/cache-keepalive.test.js +206 -0
  8. package/dist/cache-keepalive.test.js.map +1 -0
  9. package/dist/floating-cache-marker.test.d.ts +2 -0
  10. package/dist/floating-cache-marker.test.d.ts.map +1 -0
  11. package/dist/floating-cache-marker.test.js +242 -0
  12. package/dist/floating-cache-marker.test.js.map +1 -0
  13. package/dist/formatters/anthropic-xml.d.ts.map +1 -1
  14. package/dist/formatters/anthropic-xml.js +30 -2
  15. package/dist/formatters/anthropic-xml.js.map +1 -1
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/membrane.d.ts +7 -1
  21. package/dist/membrane.d.ts.map +1 -1
  22. package/dist/membrane.js +148 -5
  23. package/dist/membrane.js.map +1 -1
  24. package/dist/providers/anthropic.d.ts +20 -0
  25. package/dist/providers/anthropic.d.ts.map +1 -1
  26. package/dist/providers/anthropic.js +23 -3
  27. package/dist/providers/anthropic.js.map +1 -1
  28. package/dist/providers/bedrock.d.ts.map +1 -1
  29. package/dist/providers/bedrock.js +31 -2
  30. package/dist/providers/bedrock.js.map +1 -1
  31. package/dist/types/config.d.ts +5 -0
  32. package/dist/types/config.d.ts.map +1 -1
  33. package/dist/types/config.js.map +1 -1
  34. package/dist/types/content.d.ts +5 -0
  35. package/dist/types/content.d.ts.map +1 -1
  36. package/dist/types/content.js.map +1 -1
  37. package/dist/types/request.d.ts +13 -0
  38. package/dist/types/request.d.ts.map +1 -1
  39. package/dist/types/tools.d.ts +19 -0
  40. package/dist/types/tools.d.ts.map +1 -1
  41. package/dist/utils/tool-parser.d.ts +16 -2
  42. package/dist/utils/tool-parser.d.ts.map +1 -1
  43. package/dist/utils/tool-parser.js +143 -35
  44. package/dist/utils/tool-parser.js.map +1 -1
  45. package/package.json +3 -2
  46. package/src/cache-keepalive.test.ts +244 -0
  47. package/src/cache-keepalive.ts +385 -0
  48. package/src/floating-cache-marker.test.ts +261 -0
  49. package/src/formatters/anthropic-xml.ts +30 -2
  50. package/src/index.ts +13 -0
  51. package/src/membrane.ts +143 -5
  52. package/src/providers/anthropic.ts +46 -3
  53. package/src/providers/bedrock.ts +32 -1
  54. package/src/types/config.ts +6 -0
  55. package/src/types/content.ts +5 -0
  56. package/src/types/request.ts +14 -0
  57. package/src/types/tools.ts +20 -0
  58. package/src/utils/tool-parser.ts +147 -36
@@ -188,40 +188,94 @@ export function endsWithPartialToolBlock(text: string): boolean {
188
188
  // ============================================================================
189
189
 
190
190
  /**
191
- * Format tool results as XML for injection.
192
- * Handles both string content and structured content blocks (with images).
191
+ * Structural tags of the XML tool convention. Result content containing any
192
+ * of these must be escaped or it would desync the document/stream parser;
193
+ * everything else rides raw (legacy convention — full escapeXml put `"`
194
+ * entities in front of the model, which Claude-3-era models then reproduce
195
+ * in their own output).
196
+ */
197
+ const STRUCTURAL_TAG_RE =
198
+ /<\/?(?:antml:)?(?:function_calls|function_results|invoke|result|stdout|error|tool_name)\b/;
199
+
200
+ function renderResultContentString(result: ToolResult): string {
201
+ if (typeof result.content === 'string') {
202
+ return result.content;
203
+ }
204
+ const parts: string[] = [];
205
+ for (const block of result.content) {
206
+ if (block.type === 'text') {
207
+ parts.push(block.text);
208
+ } else if (block.type === 'image') {
209
+ // For XML mode, we can't embed images directly
210
+ // Add a note about the image for the model
211
+ const sizeKb = Math.round((block.source.data.length * 0.75) / 1024);
212
+ parts.push(`[Image: ${block.source.mediaType}, ~${sizeKb}KB]`);
213
+ }
214
+ }
215
+ return parts.join('\n');
216
+ }
217
+
218
+ /**
219
+ * Format tool results as XML for injection — the LEGACY Anthropic tool
220
+ * convention Claude-3-era models were trained on:
221
+ *
222
+ * <function_results>
223
+ * <result>
224
+ * <tool_name>NAME</tool_name>
225
+ * <stdout>
226
+ * content
227
+ * </stdout>
228
+ * </result>
229
+ * </function_results>
230
+ *
231
+ * Errors render as <error>…</error> inside <function_results>. No
232
+ * tool_use_id attributes on the wire (a Messages-API concept — the
233
+ * store keeps the linkage on the blocks); content rides raw unless it
234
+ * contains structural tags (then escaped, see STRUCTURAL_TAG_RE).
193
235
  */
194
236
  export function formatToolResults(results: ToolResult[]): string {
195
237
  const parts: string[] = ['<function_results>'];
196
238
 
197
239
  for (const result of results) {
198
- const tagName = result.isError ? 'error' : 'result';
199
- parts.push(`<${tagName} tool_use_id="${result.toolUseId}">`);
200
-
201
- // Handle both string and array content
202
- if (typeof result.content === 'string') {
203
- parts.push(escapeXml(result.content));
204
- } else if (Array.isArray(result.content)) {
205
- // Structured content blocks
206
- for (const block of result.content) {
207
- if (block.type === 'text') {
208
- parts.push(escapeXml(block.text));
209
- } else if (block.type === 'image') {
210
- // For XML mode, we can't embed images directly
211
- // Add a note about the image for the model
212
- const sizeKb = Math.round((block.source.data.length * 0.75) / 1024);
213
- parts.push(`[Image: ${block.source.mediaType}, ~${sizeKb}KB]`);
214
- }
240
+ if (result.isError) {
241
+ parts.push('<error>');
242
+ parts.push(guardResultContent(renderResultContentString(result)));
243
+ parts.push('</error>');
244
+ } else {
245
+ parts.push('<result>');
246
+ if (result.toolName) {
247
+ parts.push(`<tool_name>${result.toolName}</tool_name>`);
215
248
  }
249
+ parts.push('<stdout>');
250
+ parts.push(guardResultContent(renderResultContentString(result)));
251
+ parts.push('</stdout>');
252
+ parts.push('</result>');
216
253
  }
217
-
218
- parts.push(`</${tagName}>`);
219
254
  }
220
255
 
221
256
  parts.push('</function_results>');
222
257
  return parts.join('\n');
223
258
  }
224
259
 
260
+ /** Escape result content only when it would desync the structural parse. */
261
+ function guardResultContent(s: string): string {
262
+ return STRUCTURAL_TAG_RE.test(s) ? escapeXml(s) : s;
263
+ }
264
+
265
+ /** Opening XML of one result, up to where its content begins. */
266
+ function resultOpenXml(result: ToolResult): string {
267
+ if (result.isError) return '<error>\n';
268
+ let xml = '<result>\n';
269
+ if (result.toolName) xml += `<tool_name>${result.toolName}</tool_name>\n`;
270
+ xml += '<stdout>\n';
271
+ return xml;
272
+ }
273
+
274
+ /** Closing XML of one result, after its content. */
275
+ function resultCloseXml(result: ToolResult): string {
276
+ return result.isError ? '\n</error>\n' : '\n</stdout>\n</result>\n';
277
+ }
278
+
225
279
  /**
226
280
  * Format a single tool result
227
281
  */
@@ -292,6 +346,13 @@ const FUNCTION_RESULTS_BLOCK_REGEX = /<(antml:)?function_results>([\s\S]*?)<\/(a
292
346
  const RESULT_REGEX = /<result\s+tool_use_id="([^"]+)">([\s\S]*?)<\/result>/g;
293
347
  const ERROR_REGEX = /<error\s+tool_use_id="([^"]+)">([\s\S]*?)<\/error>/g;
294
348
 
349
+ // Legacy Anthropic convention — no ids on the wire; results pair
350
+ // positionally with the preceding unmatched tool calls in document order
351
+ // (optionally disambiguated by <tool_name>).
352
+ const LEGACY_RESULT_REGEX =
353
+ /<result>\s*(?:<tool_name>([\s\S]*?)<\/tool_name>\s*)?<stdout>\n?([\s\S]*?)\n?<\/stdout>\s*<\/result>/g;
354
+ const LEGACY_ERROR_REGEX = /<error>\n?([\s\S]*?)\n?<\/error>/g;
355
+
295
356
  /**
296
357
  * Parse accumulated assistant text into structured ContentBlock[].
297
358
  * Extracts thinking blocks, tool calls, tool results, and plain text.
@@ -331,6 +392,21 @@ export function parseAccumulatedIntoBlocks(
331
392
  };
332
393
  const positions: BlockPosition[] = [];
333
394
 
395
+ // Call sites in document order, for pairing legacy-shaped results
396
+ // (no tool_use_id on the wire) with the calls they answer.
397
+ const callSites: Array<{ id: string; name: string; pos: number }> = [];
398
+ const pairedCallIds = new Set<string>();
399
+ const claimCall = (beforePos: number, name?: string): string => {
400
+ for (const site of callSites) {
401
+ if (site.pos >= beforePos) break;
402
+ if (pairedCallIds.has(site.id)) continue;
403
+ if (name && site.name !== name) continue;
404
+ pairedCallIds.add(site.id);
405
+ return site.id;
406
+ }
407
+ return generateToolId();
408
+ };
409
+
334
410
  // Find all thinking blocks
335
411
  THINKING_BLOCK_REGEX.lastIndex = 0;
336
412
  let thinkingMatch: RegExpExecArray | null;
@@ -377,6 +453,7 @@ export function parseAccumulatedIntoBlocks(
377
453
  const id = generateToolId();
378
454
  const toolCall: ToolCall = { id, name: toolName, input };
379
455
  toolCalls.push(toolCall);
456
+ callSites.push({ id, name: toolName, pos: funcMatch.index });
380
457
  blockToolCalls.push({
381
458
  type: 'tool_use',
382
459
  id,
@@ -394,6 +471,7 @@ export function parseAccumulatedIntoBlocks(
394
471
  const id = generateToolId();
395
472
  const toolCall: ToolCall = { id, name: toolName, input: {} };
396
473
  toolCalls.push(toolCall);
474
+ callSites.push({ id, name: toolName, pos: funcMatch.index });
397
475
  blockToolCalls.push({
398
476
  type: 'tool_use',
399
477
  id,
@@ -428,6 +506,7 @@ export function parseAccumulatedIntoBlocks(
428
506
  while ((resultMatch = RESULT_REGEX.exec(innerContent)) !== null) {
429
507
  const toolUseId = resultMatch[1] ?? '';
430
508
  const content = unescapeXml(resultMatch[2] ?? '');
509
+ pairedCallIds.add(toolUseId);
431
510
  const result: ToolResult = { toolUseId, content, isError: false };
432
511
  toolResults.push(result);
433
512
  blockResults.push({
@@ -445,6 +524,7 @@ export function parseAccumulatedIntoBlocks(
445
524
  while ((errorMatch = ERROR_REGEX.exec(innerContent)) !== null) {
446
525
  const toolUseId = errorMatch[1] ?? '';
447
526
  const content = unescapeXml(errorMatch[2] ?? '');
527
+ pairedCallIds.add(toolUseId);
448
528
  const result: ToolResult = { toolUseId, content, isError: true };
449
529
  toolResults.push(result);
450
530
  blockResults.push({
@@ -456,6 +536,39 @@ export function parseAccumulatedIntoBlocks(
456
536
  });
457
537
  }
458
538
 
539
+ // Legacy-shaped results/errors (no ids on the wire): pair positionally
540
+ // with the preceding unclaimed calls, disambiguated by <tool_name>.
541
+ LEGACY_RESULT_REGEX.lastIndex = 0;
542
+ let legacyResultMatch: RegExpExecArray | null;
543
+ while ((legacyResultMatch = LEGACY_RESULT_REGEX.exec(innerContent)) !== null) {
544
+ const toolName = legacyResultMatch[1]?.trim() || undefined;
545
+ const content = unescapeXml(legacyResultMatch[2] ?? '');
546
+ const toolUseId = claimCall(resultsMatch.index, toolName);
547
+ toolResults.push({ toolUseId, toolName, content, isError: false });
548
+ blockResults.push({
549
+ type: 'tool_result',
550
+ toolUseId,
551
+ toolName,
552
+ content,
553
+ isError: false,
554
+ rawXml,
555
+ });
556
+ }
557
+ LEGACY_ERROR_REGEX.lastIndex = 0;
558
+ let legacyErrorMatch: RegExpExecArray | null;
559
+ while ((legacyErrorMatch = LEGACY_ERROR_REGEX.exec(innerContent)) !== null) {
560
+ const content = unescapeXml(legacyErrorMatch[1] ?? '');
561
+ const toolUseId = claimCall(resultsMatch.index);
562
+ toolResults.push({ toolUseId, content, isError: true });
563
+ blockResults.push({
564
+ type: 'tool_result',
565
+ toolUseId,
566
+ content,
567
+ isError: true,
568
+ rawXml,
569
+ });
570
+ }
571
+
459
572
  if (blockResults.length > 0) {
460
573
  positions.push({
461
574
  start: resultsMatch.index,
@@ -628,7 +741,6 @@ export function formatToolResultsForSplitTurn(results: ToolResult[]): SplitTurnC
628
741
 
629
742
  for (let i = 0; i < results.length; i++) {
630
743
  const result = results[i]!;
631
- const tagName = result.isError ? 'error' : 'result';
632
744
 
633
745
  // Check if this result has images
634
746
  let resultHasImages = false;
@@ -636,14 +748,14 @@ export function formatToolResultsForSplitTurn(results: ToolResult[]): SplitTurnC
636
748
  let resultImages: ProviderImageBlock[] = [];
637
749
 
638
750
  if (typeof result.content === 'string') {
639
- textParts.push(escapeXml(result.content));
751
+ textParts.push(guardResultContent(result.content));
640
752
  } else if (Array.isArray(result.content)) {
641
753
  for (const block of result.content) {
642
754
  if (block.type === 'text') {
643
- textParts.push(escapeXml(block.text));
755
+ textParts.push(guardResultContent(block.text));
644
756
  } else if (block.type === 'image') {
645
757
  if (!isAcceptedImageMediaType(block.source.mediaType)) {
646
- textParts.push(escapeXml(strippedImagePlaceholder(block.source.mediaType).text));
758
+ textParts.push(strippedImagePlaceholder(block.source.mediaType).text);
647
759
  } else {
648
760
  resultHasImages = true;
649
761
  resultImages.push({
@@ -664,15 +776,15 @@ export function formatToolResultsForSplitTurn(results: ToolResult[]): SplitTurnC
664
776
  imageInsertionPoint = i;
665
777
  images.push(...resultImages);
666
778
 
667
- // Add opening tag and text content (no closing tag yet)
668
- beforeImageXml += `<${tagName} tool_use_id="${result.toolUseId}">\n`;
779
+ // Add opening tags and text content (no closing tags yet)
780
+ beforeImageXml += resultOpenXml(result);
669
781
  if (textParts.length > 0) {
670
782
  beforeImageXml += textParts.join('\n');
671
783
  }
672
- // Note: Intentionally NOT adding closing tag - split happens here
784
+ // Note: Intentionally NOT adding closing tags - split happens here
673
785
 
674
786
  // After image, we need to close this result and add remaining results
675
- afterImageXml = `</${tagName}>\n`;
787
+ afterImageXml = resultCloseXml(result);
676
788
 
677
789
  // Process remaining results into afterImageXml
678
790
  for (let j = i + 1; j < results.length; j++) {
@@ -685,9 +797,9 @@ export function formatToolResultsForSplitTurn(results: ToolResult[]): SplitTurnC
685
797
  break;
686
798
  } else if (imageInsertionPoint === -1) {
687
799
  // No images yet - add full result to beforeImageXml
688
- beforeImageXml += `<${tagName} tool_use_id="${result.toolUseId}">\n`;
800
+ beforeImageXml += resultOpenXml(result);
689
801
  beforeImageXml += textParts.join('\n');
690
- beforeImageXml += `\n</${tagName}>\n`;
802
+ beforeImageXml += resultCloseXml(result);
691
803
  }
692
804
  }
693
805
 
@@ -714,15 +826,14 @@ export function formatToolResultsForSplitTurn(results: ToolResult[]): SplitTurnC
714
826
  * Format a single tool result as complete XML
715
827
  */
716
828
  function formatSingleResultXml(result: ToolResult): string {
717
- const tagName = result.isError ? 'error' : 'result';
718
- let xml = `<${tagName} tool_use_id="${result.toolUseId}">\n`;
829
+ let xml = resultOpenXml(result);
719
830
 
720
831
  if (typeof result.content === 'string') {
721
- xml += escapeXml(result.content);
832
+ xml += guardResultContent(result.content);
722
833
  } else if (Array.isArray(result.content)) {
723
834
  for (const block of result.content) {
724
835
  if (block.type === 'text') {
725
- xml += escapeXml(block.text);
836
+ xml += guardResultContent(block.text);
726
837
  } else if (block.type === 'image') {
727
838
  // For remaining results after split, images become text placeholders
728
839
  const sizeKb = Math.round((block.source.data.length * 0.75) / 1024);
@@ -731,7 +842,7 @@ function formatSingleResultXml(result: ToolResult): string {
731
842
  }
732
843
  }
733
844
 
734
- xml += `\n</${tagName}>\n`;
845
+ xml += resultCloseXml(result);
735
846
  return xml;
736
847
  }
737
848