@animalabs/membrane 0.5.73 → 0.5.75

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.
@@ -21,6 +21,11 @@ import {
21
21
  abortError,
22
22
  } from '../types/index.js';
23
23
  import { createCombinedSignal } from './utils.js';
24
+ import {
25
+ INTERLEAVED_THINKING_BETA,
26
+ needsInterleavedThinkingBeta,
27
+ thinkingEnabled,
28
+ } from './anthropic.js';
24
29
 
25
30
  // ============================================================================
26
31
  // Adapter Configuration
@@ -64,6 +69,8 @@ interface BedrockMessageRequest {
64
69
  stop_sequences?: string[];
65
70
  tools?: unknown[];
66
71
  thinking?: { type: 'enabled'; budget_tokens: number };
72
+ /** Beta flags. On bedrock-runtime these ride in the body, not a header. */
73
+ anthropic_beta?: string[];
67
74
  }
68
75
 
69
76
  interface BedrockMessageResponse {
@@ -407,6 +414,17 @@ export class BedrockAdapter implements ProviderAdapter {
407
414
  Object.assign(params, rest);
408
415
  }
409
416
 
417
+ // Interleaved thinking on pre-4.6 Claude 4: same gate as the Anthropic
418
+ // adapter, but bedrock-runtime takes betas as the `anthropic_beta` body
419
+ // field rather than an HTTP header. Runs after the extra-assign so a
420
+ // consumer-supplied anthropic_beta is merged (Set-deduped), not clobbered.
421
+ // The gate only matches Claude 4 <4.6 ids, so legacy 3.x models (which
422
+ // reject unknown beta flags) never receive the field.
423
+ if (thinkingEnabled(request) && needsInterleavedThinkingBeta(request.model)) {
424
+ const existing = Array.isArray(params.anthropic_beta) ? params.anthropic_beta : [];
425
+ params.anthropic_beta = [...new Set([...existing, INTERLEAVED_THINKING_BETA])];
426
+ }
427
+
410
428
  return params;
411
429
  }
412
430
 
@@ -632,6 +650,15 @@ export class BedrockAdapter implements ProviderAdapter {
632
650
  if (contentBlocks[currentBlockIndex]) {
633
651
  contentBlocks[currentBlockIndex]!.signature = (contentBlocks[currentBlockIndex]!.signature ?? '') + (eventData.delta as any).signature;
634
652
  }
653
+ } else if (eventData.delta?.type === 'input_json_delta' && (eventData.delta as any).partial_json !== undefined) {
654
+ // Tool-call arguments stream as input_json_delta fragments.
655
+ // Dropping these (pre-2026-07-21 behavior) gutted every
656
+ // tool_use block: stop_reason said tool_use but the block
657
+ // lost its input — and then its id/name in final assembly.
658
+ const blk = contentBlocks[currentBlockIndex] as (typeof contentBlocks)[number] & { partialJson?: string };
659
+ if (blk) {
660
+ blk.partialJson = (blk.partialJson ?? '') + (eventData.delta as any).partial_json;
661
+ }
635
662
  }
636
663
  } else if (eventData.type === 'content_block_stop') {
637
664
  // Mirror the Anthropic adapter: fire onContentBlock a second
@@ -640,6 +667,13 @@ export class BedrockAdapter implements ProviderAdapter {
640
667
  // on the dual-fire convention (e.g. membrane's native
641
668
  // yielding path) silently drops block_complete on Bedrock.
642
669
  const blockIdx = (eventData as { index?: number }).index ?? currentBlockIndex;
670
+ // Finalize tool_use blocks: parse accumulated argument JSON
671
+ // into `input` before the block_complete fire, so consumers
672
+ // of the dual-fire convention see a complete tool call.
673
+ const stoppedBlk = contentBlocks[blockIdx] as ((typeof contentBlocks)[number] & { partialJson?: string; input?: unknown }) | undefined;
674
+ if (stoppedBlk?.type === 'tool_use' && stoppedBlk.partialJson !== undefined) {
675
+ try { stoppedBlk.input = JSON.parse(stoppedBlk.partialJson || '{}'); } catch { /* keep prior input */ }
676
+ }
643
677
  callbacks.onContentBlock?.(blockIdx, contentBlocks[blockIdx]);
644
678
  } else if (eventData.type === 'message_delta') {
645
679
  if (eventData.usage) {
@@ -687,6 +721,17 @@ export class BedrockAdapter implements ProviderAdapter {
687
721
  // Pass through verbatim — carries the encrypted `data` payload
688
722
  return { ...b } as unknown as { type: 'text'; text?: string };
689
723
  }
724
+ if (b.type === 'tool_use') {
725
+ // Preserve id/name and the input accumulated from input_json_delta
726
+ // fragments — the generic text mapping below would strip them and
727
+ // parseResponse would then drop the block (requires id && name).
728
+ const tb = b as { type: string; id?: string; name?: string; input?: unknown; partialJson?: string };
729
+ let input: unknown = tb.input ?? {};
730
+ if (tb.partialJson) {
731
+ try { input = JSON.parse(tb.partialJson); } catch { /* keep prior input */ }
732
+ }
733
+ return { type: 'tool_use', id: tb.id, name: tb.name, input } as unknown as { type: 'text'; text?: string };
734
+ }
690
735
  return { type: b.type as 'text', text: b.text };
691
736
  }),
692
737
  model: modelId,
@@ -136,6 +136,14 @@ export interface MembraneConfig {
136
136
  */
137
137
  maxParticipantsForStop?: number;
138
138
 
139
+ /**
140
+ * Default for request.promptCaching when the request doesn't set it.
141
+ * Historical default is true (Anthropic API). Set false for transports
142
+ * that reject cache_control (e.g. Bedrock legacy Claude models, which 400
143
+ * with "your request did not allow prompt caching").
144
+ */
145
+ defaultPromptCaching?: boolean;
146
+
139
147
  /**
140
148
  * Prefill formatter for message serialization and response parsing.
141
149
  * Controls how messages are formatted for the API and how responses are parsed.
@@ -110,6 +110,17 @@ export interface ToolUseContent {
110
110
  id: string;
111
111
  name: string;
112
112
  input: Record<string, unknown>;
113
+ /**
114
+ * Verbatim document text this call was parsed from in prefill/XML mode:
115
+ * the full `<function_calls>…</function_calls>` block, shared by every
116
+ * invoke parsed from that block. Prefill formatters replay it exactly
117
+ * instead of synthesizing a rendering — in prefill mode the context IS
118
+ * the document the agent authored, and a paraphrase of its own action
119
+ * both corrupts the record and teaches the model a syntax the parser
120
+ * does not accept (membrane#36). Analogous to `signature` on a thinking
121
+ * block. Absent on native-tools blocks and on legacy stored blocks.
122
+ */
123
+ rawXml?: string;
113
124
  /** See {@link TextContent.rawItem}. */
114
125
  rawItem?: unknown;
115
126
  }
@@ -119,6 +130,13 @@ export interface ToolResultContent {
119
130
  toolUseId: string;
120
131
  content: string | ContentBlock[];
121
132
  isError?: boolean;
133
+ /**
134
+ * Verbatim document text this result was parsed from in prefill/XML mode:
135
+ * the full `<function_results>…</function_results>` block as the harness
136
+ * originally placed it in the document, shared by every result parsed
137
+ * from that block. Replayed exactly on the prefill path (membrane#36).
138
+ */
139
+ rawXml?: string;
122
140
  /** See {@link TextContent.rawItem}. */
123
141
  rawItem?: unknown;
124
142
  }
@@ -239,6 +239,12 @@ export interface ProviderRequestOptions {
239
239
  timeoutMs?: number;
240
240
  /** Abort if no SSE event arrives within this many ms (default: 120000) */
241
241
  idleTimeoutMs?: number;
242
+ /**
243
+ * Deadline for the FIRST stream event (TTFT). Large contexts on a cache
244
+ * miss legitimately take minutes before message_start while the SDK
245
+ * swallows ping keepalives (default: max(idleTimeoutMs, 600000)).
246
+ */
247
+ firstEventTimeoutMs?: number;
242
248
  /** Called with the raw API request body right before fetch */
243
249
  onRequest?: (rawRequest: unknown) => void;
244
250
  /**
@@ -350,6 +350,10 @@ export function parseAccumulatedIntoBlocks(
350
350
  let funcMatch: RegExpExecArray | null;
351
351
  while ((funcMatch = FUNCTION_BLOCK_WITH_CONTENT_REGEX.exec(processedText)) !== null) {
352
352
  const innerContent = funcMatch[2] ?? '';
353
+ // Verbatim document text of the whole block — carried on each parsed
354
+ // tool_use so prefill replay reproduces the generation exactly instead
355
+ // of synthesizing a paraphrase (membrane#36).
356
+ const rawXml = funcMatch[0];
353
357
  const blockToolCalls: ContentBlock[] = [];
354
358
 
355
359
  // Parse invoke tags in this block
@@ -378,6 +382,7 @@ export function parseAccumulatedIntoBlocks(
378
382
  id,
379
383
  name: toolName,
380
384
  input,
385
+ rawXml,
381
386
  });
382
387
  }
383
388
 
@@ -394,6 +399,7 @@ export function parseAccumulatedIntoBlocks(
394
399
  id,
395
400
  name: toolName,
396
401
  input: {},
402
+ rawXml,
397
403
  });
398
404
  }
399
405
 
@@ -411,6 +417,9 @@ export function parseAccumulatedIntoBlocks(
411
417
  let resultsMatch: RegExpExecArray | null;
412
418
  while ((resultsMatch = FUNCTION_RESULTS_BLOCK_REGEX.exec(processedText)) !== null) {
413
419
  const innerContent = resultsMatch[2] ?? '';
420
+ // Verbatim document text the harness placed — carried for exact replay
421
+ // on the prefill path (membrane#36).
422
+ const rawXml = resultsMatch[0];
414
423
  const blockResults: ContentBlock[] = [];
415
424
 
416
425
  // Parse result tags
@@ -426,6 +435,7 @@ export function parseAccumulatedIntoBlocks(
426
435
  toolUseId,
427
436
  content,
428
437
  isError: false,
438
+ rawXml,
429
439
  });
430
440
  }
431
441
 
@@ -442,6 +452,7 @@ export function parseAccumulatedIntoBlocks(
442
452
  toolUseId,
443
453
  content,
444
454
  isError: true,
455
+ rawXml,
445
456
  });
446
457
  }
447
458