@animalabs/membrane 0.5.74 → 0.5.76
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/formatters/anthropic-xml.d.ts +21 -0
- package/dist/formatters/anthropic-xml.d.ts.map +1 -1
- package/dist/formatters/anthropic-xml.js +134 -6
- package/dist/formatters/anthropic-xml.js.map +1 -1
- package/dist/formatters/native.d.ts.map +1 -1
- package/dist/formatters/native.js +13 -0
- package/dist/formatters/native.js.map +1 -1
- package/dist/membrane.d.ts.map +1 -1
- package/dist/membrane.js +40 -8
- package/dist/membrane.js.map +1 -1
- package/dist/providers/anthropic.d.ts +33 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +80 -3
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/bedrock.d.ts.map +1 -1
- package/dist/providers/bedrock.js +23 -0
- package/dist/providers/bedrock.js.map +1 -1
- package/dist/types/content.d.ts +18 -0
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/content.js.map +1 -1
- package/dist/utils/tool-parser.d.ts.map +1 -1
- package/dist/utils/tool-parser.js +11 -0
- package/dist/utils/tool-parser.js.map +1 -1
- package/package.json +1 -1
- package/src/formatters/anthropic-xml.ts +142 -6
- package/src/formatters/native.ts +12 -0
- package/src/membrane.ts +40 -8
- package/src/providers/anthropic.ts +83 -3
- package/src/providers/bedrock.ts +30 -0
- package/src/types/content.ts +18 -0
- package/src/utils/tool-parser.ts +11 -0
package/src/membrane.ts
CHANGED
|
@@ -476,8 +476,11 @@ export class Membrane {
|
|
|
476
476
|
|
|
477
477
|
if (parsed && parsed.calls.length > 0) {
|
|
478
478
|
// Notify about pre-tool content
|
|
479
|
-
|
|
480
|
-
|
|
479
|
+
// Slice the seeded prefill off: beforeText starts with the whole
|
|
480
|
+
// flattened document in XML mode (see ToolContext note below).
|
|
481
|
+
const preToolNew = parsed.beforeText.slice(initialPrefillLength);
|
|
482
|
+
if (onPreToolContent && preToolNew.trim()) {
|
|
483
|
+
await onPreToolContent(preToolNew);
|
|
481
484
|
}
|
|
482
485
|
|
|
483
486
|
// Emit block events for each tool call
|
|
@@ -504,13 +507,24 @@ export class Membrane {
|
|
|
504
507
|
// Track the tool calls
|
|
505
508
|
executedToolCalls.push(...parsed.calls);
|
|
506
509
|
|
|
507
|
-
// Execute tools
|
|
510
|
+
// Execute tools.
|
|
511
|
+
// preamble/accumulated must expose the MODEL'S text only. The
|
|
512
|
+
// parser is seeded with the entire assistant prefill (the whole
|
|
513
|
+
// flattened document in XML mode), so parsed.beforeText starts
|
|
514
|
+
// with it — consumers that persist the preamble as "what the
|
|
515
|
+
// agent said this round" would otherwise write the full document
|
|
516
|
+
// back into the store as an assistant message (observed on Ash,
|
|
517
|
+
// 2026-07-26: a died-mid-rounds turn flushed a ~720k-char
|
|
518
|
+
// document echo into her message store as 62 sharded messages).
|
|
519
|
+
// The turn-END path already slices (newContent =
|
|
520
|
+
// fullAccumulated.slice(initialPrefillLength)); the tool-round
|
|
521
|
+
// path must match it.
|
|
508
522
|
const context: ToolContext = {
|
|
509
523
|
rawText: parsed.fullMatch,
|
|
510
|
-
preamble: parsed.beforeText,
|
|
524
|
+
preamble: parsed.beforeText.slice(initialPrefillLength),
|
|
511
525
|
depth: toolDepth,
|
|
512
526
|
previousResults: executedToolResults,
|
|
513
|
-
accumulated: parser.getAccumulated(),
|
|
527
|
+
accumulated: parser.getAccumulated().slice(initialPrefillLength),
|
|
514
528
|
};
|
|
515
529
|
|
|
516
530
|
const results = await onToolCalls(parsed.calls, context);
|
|
@@ -1033,6 +1047,13 @@ export class Membrane {
|
|
|
1033
1047
|
const textBlock: Record<string, unknown> = { type: 'text', text };
|
|
1034
1048
|
if ((block as any).cache_control) {
|
|
1035
1049
|
textBlock.cache_control = (block as any).cache_control;
|
|
1050
|
+
// A block-level passthrough occupies one of the 4 breakpoint slots
|
|
1051
|
+
// exactly like a marked message — count it, so the tools/system
|
|
1052
|
+
// fallback below doesn't stack more on top. (Imported/seeded
|
|
1053
|
+
// conversations can carry stale request-time cache_control on
|
|
1054
|
+
// stored blocks — first seen wedging Sill 2026-07-25: 3 cm markers
|
|
1055
|
+
// + 2 stale Arc-export blocks = 5 → hard 400 on every inference.)
|
|
1056
|
+
messageBreakpoints++;
|
|
1036
1057
|
}
|
|
1037
1058
|
content.push(textBlock);
|
|
1038
1059
|
} else if (block.type === 'tool_use') {
|
|
@@ -2225,13 +2246,24 @@ export class Membrane {
|
|
|
2225
2246
|
// Track the tool calls
|
|
2226
2247
|
executedToolCalls.push(...parsed.calls);
|
|
2227
2248
|
|
|
2228
|
-
// Build tool context
|
|
2249
|
+
// Build tool context.
|
|
2250
|
+
// preamble/accumulated must expose the MODEL'S text only: the
|
|
2251
|
+
// parser was seeded with the entire assistant prefill (the whole
|
|
2252
|
+
// flattened document in XML mode), so parsed.beforeText starts
|
|
2253
|
+
// with it. Consumers persist the preamble as "what the agent said
|
|
2254
|
+
// this round" — unsliced, a turn that dies mid-rounds flushes the
|
|
2255
|
+
// full document back into the agent's store as its own message
|
|
2256
|
+
// (observed on Ash 2026-07-26: ~720k-char document echo persisted
|
|
2257
|
+
// as 62 sharded assistant messages, doubling her store and
|
|
2258
|
+
// wedging every subsequent compile). The turn-END path already
|
|
2259
|
+
// slices (newContent = fullAccumulated.slice(initialPrefillLength));
|
|
2260
|
+
// the tool-round path must match it.
|
|
2229
2261
|
const context: ToolContext = {
|
|
2230
2262
|
rawText: parsed.fullMatch,
|
|
2231
|
-
preamble: parsed.beforeText,
|
|
2263
|
+
preamble: parsed.beforeText.slice(initialPrefillLength),
|
|
2232
2264
|
depth: toolDepth,
|
|
2233
2265
|
previousResults: executedToolResults,
|
|
2234
|
-
accumulated: parser.getAccumulated(),
|
|
2266
|
+
accumulated: parser.getAccumulated().slice(initialPrefillLength),
|
|
2235
2267
|
};
|
|
2236
2268
|
|
|
2237
2269
|
// Yield control for tool execution
|
|
@@ -68,6 +68,61 @@ function noTemperatureSupport(model: string): boolean {
|
|
|
68
68
|
return NO_TEMPERATURE_MODELS.some(prefix => model.startsWith(prefix));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** Beta flag for thinking blocks between tool calls on pre-4.6 Claude 4.
|
|
72
|
+
* Shared with the Bedrock adapter, where it rides in the request body
|
|
73
|
+
* (`anthropic_beta`) instead of an HTTP header. */
|
|
74
|
+
export const INTERLEAVED_THINKING_BETA = 'interleaved-thinking-2025-05-14';
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Interleaved thinking (thinking blocks between tool calls) is native from
|
|
78
|
+
* Opus/Sonnet 4.6 onward; earlier Claude 4 models only do it behind the
|
|
79
|
+
* `interleaved-thinking-2025-05-14` beta flag. Matches Claude 4 ids with a
|
|
80
|
+
* minor version below 6 — dated snapshots (claude-opus-4-1-20250805), bare
|
|
81
|
+
* bases (claude-opus-4), and date-only 4.0 ids (claude-opus-4-20250514) are
|
|
82
|
+
* all covered, as are gateway-prefixed ids ('anthropic/claude-opus-4-5').
|
|
83
|
+
* Claude 3.x never matches (no interleaved support, beta or not); 4.6+ and
|
|
84
|
+
* the 5-series need no flag.
|
|
85
|
+
*/
|
|
86
|
+
export function needsInterleavedThinkingBeta(model: string): boolean {
|
|
87
|
+
const m = /claude-(?:opus|sonnet|haiku)-4(?:-(\d+))?/.exec(model);
|
|
88
|
+
if (!m) return false;
|
|
89
|
+
const minor = m[1];
|
|
90
|
+
if (minor === undefined) return true; // bare 'claude-opus-4'
|
|
91
|
+
if (minor.length >= 8) return true; // 8-digit date right after the major = a 4.0 id
|
|
92
|
+
return parseInt(minor, 10) < 6;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Pull any anthropic-beta value out of a ClientOptions defaultHeaders bag,
|
|
96
|
+
* which the SDK types as record | entries-array | Headers. Case-insensitive
|
|
97
|
+
* on the key; non-string values (explicit null override, arrays) are treated
|
|
98
|
+
* as absent. */
|
|
99
|
+
function extractBetaHeader(headers: ClientOptions['defaultHeaders']): string | undefined {
|
|
100
|
+
if (!headers) return undefined;
|
|
101
|
+
if (typeof (headers as Headers).get === 'function') {
|
|
102
|
+
return (headers as Headers).get('anthropic-beta') ?? undefined;
|
|
103
|
+
}
|
|
104
|
+
const entries = Array.isArray(headers)
|
|
105
|
+
? headers
|
|
106
|
+
: Object.entries(headers as Record<string, unknown>);
|
|
107
|
+
for (const [key, value] of entries) {
|
|
108
|
+
if (String(key).toLowerCase() === 'anthropic-beta' && typeof value === 'string') {
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Resolve whether a thinking config is enabled on a request. Thinking can
|
|
116
|
+
* arrive top-level OR smuggled through `extra` (see the sampling gate in
|
|
117
|
+
* buildRequest) — both the sampling strip and the beta header must agree on
|
|
118
|
+
* one answer, so they share this resolver. Exported for the Bedrock adapter,
|
|
119
|
+
* which applies the same interleaved-thinking gate to its request body. */
|
|
120
|
+
export function thinkingEnabled(request: ProviderRequest): boolean {
|
|
121
|
+
const extraThinking = (request.extra as { thinking?: { type?: string } } | undefined)?.thinking;
|
|
122
|
+
const thinkingConfig = request.thinking ?? extraThinking;
|
|
123
|
+
return thinkingConfig !== undefined && thinkingConfig.type !== 'disabled';
|
|
124
|
+
}
|
|
125
|
+
|
|
71
126
|
// ============================================================================
|
|
72
127
|
// Adapter Configuration
|
|
73
128
|
// ============================================================================
|
|
@@ -101,12 +156,18 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
101
156
|
readonly name = 'anthropic';
|
|
102
157
|
private client: Anthropic;
|
|
103
158
|
private defaultMaxTokens: number;
|
|
159
|
+
/** Any anthropic-beta value from defaultHeaders (e.g. the oauth beta for
|
|
160
|
+
* subscription tokens). Per-request headers REPLACE same-key defaults in
|
|
161
|
+
* the SDK rather than merging, so when we add a per-request beta we must
|
|
162
|
+
* re-carry this one alongside it or auth breaks. */
|
|
163
|
+
private defaultBeta: string | undefined;
|
|
104
164
|
|
|
105
165
|
constructor(config: AnthropicAdapterConfig = {}) {
|
|
106
166
|
const clientOptions: ClientOptions = {
|
|
107
167
|
baseURL: config.baseURL,
|
|
108
168
|
defaultHeaders: config.defaultHeaders,
|
|
109
169
|
};
|
|
170
|
+
this.defaultBeta = extractBetaHeader(config.defaultHeaders);
|
|
110
171
|
|
|
111
172
|
if (config.authToken !== undefined) {
|
|
112
173
|
clientOptions.authToken = config.authToken;
|
|
@@ -134,6 +195,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
134
195
|
try {
|
|
135
196
|
const response = await this.client.messages.create(fullRequest, {
|
|
136
197
|
signal: options?.signal,
|
|
198
|
+
headers: this.betaHeaders(request),
|
|
137
199
|
});
|
|
138
200
|
|
|
139
201
|
return this.parseResponse(response, fullRequest);
|
|
@@ -198,6 +260,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
198
260
|
try {
|
|
199
261
|
const stream = await this.client.messages.stream(anthropicRequest, {
|
|
200
262
|
signal: idleAbort.signal,
|
|
263
|
+
headers: this.betaHeaders(request),
|
|
201
264
|
});
|
|
202
265
|
|
|
203
266
|
// Accumulate response metadata from SSE events directly, so we can
|
|
@@ -399,6 +462,25 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
399
462
|
}
|
|
400
463
|
}
|
|
401
464
|
|
|
465
|
+
/** Per-request headers for both create() and stream(): the interleaved-
|
|
466
|
+
* thinking beta when thinking is enabled on a pre-4.6 Claude 4 model.
|
|
467
|
+
* Any default anthropic-beta (oauth) is re-carried in the same header —
|
|
468
|
+
* the SDK replaces same-key defaults instead of merging, and the API
|
|
469
|
+
* accepts comma-separated betas. Undefined when nothing to add, so the
|
|
470
|
+
* defaults apply untouched. */
|
|
471
|
+
private betaHeaders(request: ProviderRequest): Record<string, string> | undefined {
|
|
472
|
+
if (!thinkingEnabled(request) || !needsInterleavedThinkingBeta(request.model)) {
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
// Set-join so a default that already carries the interleaved beta
|
|
476
|
+
// doesn't emit it twice (the API tolerates duplicates; this is hygiene).
|
|
477
|
+
const betas = new Set(
|
|
478
|
+
(this.defaultBeta ?? '').split(',').map((b) => b.trim()).filter(Boolean),
|
|
479
|
+
);
|
|
480
|
+
betas.add(INTERLEAVED_THINKING_BETA);
|
|
481
|
+
return { 'anthropic-beta': [...betas].join(',') };
|
|
482
|
+
}
|
|
483
|
+
|
|
402
484
|
private buildRequest(request: ProviderRequest): Anthropic.MessageCreateParams {
|
|
403
485
|
// Strip provider-specific fields (e.g., sourceUrl for Gemini) from image blocks
|
|
404
486
|
// before sending to Anthropic, which rejects extra inputs.
|
|
@@ -464,9 +546,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
464
546
|
// otherwise `extra: { thinking, temperature }` reproduces the exact 400
|
|
465
547
|
// this gate exists to prevent (same bug class as the extra-sampling bypass,
|
|
466
548
|
// one field over).
|
|
467
|
-
const
|
|
468
|
-
const thinkingConfig = request.thinking ?? extraThinking;
|
|
469
|
-
const thinkingOn = thinkingConfig !== undefined && thinkingConfig.type !== 'disabled';
|
|
549
|
+
const thinkingOn = thinkingEnabled(request);
|
|
470
550
|
|
|
471
551
|
if (request.temperature !== undefined && !stripSampling && !thinkingOn) {
|
|
472
552
|
params.temperature = request.temperature;
|
package/src/providers/bedrock.ts
CHANGED
|
@@ -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
|
|
|
@@ -499,6 +517,7 @@ export class BedrockAdapter implements ProviderAdapter {
|
|
|
499
517
|
let inputTokens = 0;
|
|
500
518
|
let outputTokens = 0;
|
|
501
519
|
let stopReason: string = 'end_turn';
|
|
520
|
+
let stopSequence: string | undefined;
|
|
502
521
|
let fullText = '';
|
|
503
522
|
|
|
504
523
|
const reader = response.body?.getReader();
|
|
@@ -664,6 +683,16 @@ export class BedrockAdapter implements ProviderAdapter {
|
|
|
664
683
|
if (eventData.delta?.stop_reason) {
|
|
665
684
|
stopReason = eventData.delta.stop_reason;
|
|
666
685
|
}
|
|
686
|
+
// WHICH stop sequence fired, not just that one did. Dropping
|
|
687
|
+
// this (pre-2026-07-26) broke prefill/XML tool use on
|
|
688
|
+
// Bedrock entirely: membrane's tool gate matches
|
|
689
|
+
// stopSequence === '</function_calls>', so calls were never
|
|
690
|
+
// parsed or executed, the close tag was never restored, and
|
|
691
|
+
// the turn continuation looped forever on the dangling
|
|
692
|
+
// block (~6 output tokens per full-prefill round).
|
|
693
|
+
if (eventData.delta?.stop_sequence) {
|
|
694
|
+
stopSequence = eventData.delta.stop_sequence;
|
|
695
|
+
}
|
|
667
696
|
}
|
|
668
697
|
}
|
|
669
698
|
} catch (e) {
|
|
@@ -718,6 +747,7 @@ export class BedrockAdapter implements ProviderAdapter {
|
|
|
718
747
|
}),
|
|
719
748
|
model: modelId,
|
|
720
749
|
stop_reason: stopReason as BedrockMessageResponse['stop_reason'],
|
|
750
|
+
stop_sequence: stopSequence ?? null,
|
|
721
751
|
usage: {
|
|
722
752
|
input_tokens: inputTokens,
|
|
723
753
|
output_tokens: outputTokens,
|
package/src/types/content.ts
CHANGED
|
@@ -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
|
}
|
package/src/utils/tool-parser.ts
CHANGED
|
@@ -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
|
|