@animalabs/membrane 0.5.72 → 0.5.73
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/cache-thinking-guard.test.d.ts +2 -0
- package/dist/cache-thinking-guard.test.d.ts.map +1 -0
- package/dist/cache-thinking-guard.test.js +32 -0
- package/dist/cache-thinking-guard.test.js.map +1 -0
- package/dist/formatters/native.d.ts +5 -0
- package/dist/formatters/native.d.ts.map +1 -1
- package/dist/formatters/native.js +1 -1
- package/dist/formatters/native.js.map +1 -1
- package/dist/membrane.d.ts.map +1 -1
- package/dist/membrane.js +51 -6
- package/dist/membrane.js.map +1 -1
- package/dist/providers/openai-compatible.d.ts.map +1 -1
- package/dist/providers/openai-compatible.js +18 -1
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +20 -1
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts.map +1 -1
- package/dist/providers/openrouter.js +18 -1
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/yielding-stream.d.ts +47 -1
- package/dist/types/yielding-stream.d.ts.map +1 -1
- package/dist/types/yielding-stream.js.map +1 -1
- package/dist/yielding-stream.d.ts +12 -3
- package/dist/yielding-stream.d.ts.map +1 -1
- package/dist/yielding-stream.js +21 -2
- package/dist/yielding-stream.js.map +1 -1
- package/package.json +1 -1
- package/src/cache-thinking-guard.test.ts +34 -0
- package/src/formatters/native.ts +1 -1
- package/src/membrane.ts +55 -6
- package/src/providers/openai-compatible.ts +18 -1
- package/src/providers/openai.ts +20 -1
- package/src/providers/openrouter.ts +18 -1
- package/src/types/index.ts +2 -0
- package/src/types/yielding-stream.ts +50 -1
- package/src/yielding-stream.ts +41 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yielding-stream.js","sourceRoot":"","sources":["../src/yielding-stream.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"yielding-stream.js","sourceRoot":"","sources":["../src/yielding-stream.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression: a cache breakpoint must never land on a thinking block.
|
|
3
|
+
*
|
|
4
|
+
* The API rejects `cache_control` on thinking / redacted_thinking with
|
|
5
|
+
* 400 "thinking.cache_control: Extra inputs are not permitted". The rule was
|
|
6
|
+
* fixed in NativeFormatter (2026-07-01) but NOT in Membrane.buildNativeToolRequest
|
|
7
|
+
* — the live Connectome path — so the 400 returned on 2026-07-14 the moment a
|
|
8
|
+
* breakpoint landed on a thinking-terminated turn. One shared helper now.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import { lastCacheableBlockIndex } from './formatters/native.js';
|
|
12
|
+
|
|
13
|
+
describe('lastCacheableBlockIndex', () => {
|
|
14
|
+
it('steps back past a trailing thinking block', () => {
|
|
15
|
+
expect(lastCacheableBlockIndex([
|
|
16
|
+
{ type: 'text', text: 'hi' },
|
|
17
|
+
{ type: 'thinking', thinking: '', signature: 'sig' },
|
|
18
|
+
])).toBe(0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('skips the breakpoint entirely for a thinking-only message', () => {
|
|
22
|
+
expect(lastCacheableBlockIndex([
|
|
23
|
+
{ type: 'thinking', thinking: '', signature: 'sig' },
|
|
24
|
+
{ type: 'redacted_thinking', data: 'x' },
|
|
25
|
+
])).toBe(-1);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('uses the last block when it is cacheable', () => {
|
|
29
|
+
expect(lastCacheableBlockIndex([
|
|
30
|
+
{ type: 'thinking', thinking: '', signature: 'sig' },
|
|
31
|
+
{ type: 'tool_use', name: 't', input: {} },
|
|
32
|
+
])).toBe(1);
|
|
33
|
+
});
|
|
34
|
+
});
|
package/src/formatters/native.ts
CHANGED
|
@@ -35,7 +35,7 @@ import { isAcceptedImageMediaType, strippedImagePlaceholder } from '../utils/ima
|
|
|
35
35
|
* rejects cache_control on thinking / redacted_thinking blocks, so a cache
|
|
36
36
|
* breakpoint must attach to the last NON-thinking block. Returns -1 when the
|
|
37
37
|
* message has only thinking blocks (the breakpoint is then skipped). */
|
|
38
|
-
function lastCacheableBlockIndex(blocks: Array<Record<string, unknown>>): number {
|
|
38
|
+
export function lastCacheableBlockIndex(blocks: Array<Record<string, unknown>>): number {
|
|
39
39
|
for (let k = blocks.length - 1; k >= 0; k--) {
|
|
40
40
|
const t = blocks[k]?.type as string | undefined;
|
|
41
41
|
if (t !== 'thinking' && t !== 'redacted_thinking') return k;
|
package/src/membrane.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
ToolMode,
|
|
27
27
|
ToolDefinition,
|
|
28
28
|
} from './types/index.js';
|
|
29
|
+
import { lastCacheableBlockIndex } from './formatters/native.js';
|
|
29
30
|
import {
|
|
30
31
|
DEFAULT_RETRY_CONFIG,
|
|
31
32
|
MembraneError,
|
|
@@ -1087,10 +1088,24 @@ export class Membrane {
|
|
|
1087
1088
|
}
|
|
1088
1089
|
}
|
|
1089
1090
|
|
|
1090
|
-
// Apply cache_control to last block of messages with
|
|
1091
|
+
// Apply cache_control to the last CACHEABLE block of messages with a
|
|
1092
|
+
// cacheBreakpoint. The API rejects cache_control on thinking /
|
|
1093
|
+
// redacted_thinking blocks (400 "thinking.cache_control: Extra inputs
|
|
1094
|
+
// are not permitted"), so a breakpoint landing on a thinking-terminated
|
|
1095
|
+
// message must step back to the last non-thinking block — and is skipped
|
|
1096
|
+
// entirely when the message is thinking-only.
|
|
1097
|
+
//
|
|
1098
|
+
// 2026-07-14: this is the THIRD request builder to need the rule. The
|
|
1099
|
+
// 2026-07-01 fix hardened NativeFormatter's two sites but not this one,
|
|
1100
|
+
// which is the live Connectome path (native tools + thinking) — so the
|
|
1101
|
+
// 400 came back the moment a breakpoint landed on a thinking-only turn.
|
|
1102
|
+
// The rule now lives in ONE exported helper that every builder calls.
|
|
1091
1103
|
if (msg.cacheBreakpoint && cacheControl && content.length > 0) {
|
|
1092
|
-
content
|
|
1093
|
-
|
|
1104
|
+
const bpIdx = lastCacheableBlockIndex(content as Array<Record<string, unknown>>);
|
|
1105
|
+
if (bpIdx >= 0) {
|
|
1106
|
+
content[bpIdx].cache_control = cacheControl;
|
|
1107
|
+
messageBreakpoints++;
|
|
1108
|
+
}
|
|
1094
1109
|
}
|
|
1095
1110
|
|
|
1096
1111
|
providerMessages.push({ role, content });
|
|
@@ -2001,6 +2016,8 @@ export class Membrane {
|
|
|
2001
2016
|
const formatter = this.formatter;
|
|
2002
2017
|
const parser = formatter.createStreamParser();
|
|
2003
2018
|
let toolDepth = 0;
|
|
2019
|
+
// Once-per-stream latch for the injectedMessages-unsupported warning.
|
|
2020
|
+
let warnedInjectionUnsupported = false;
|
|
2004
2021
|
let totalUsage: DetailedUsage = { inputTokens: 0, outputTokens: 0 };
|
|
2005
2022
|
const pricing = this.resolvePricing(request.config.model);
|
|
2006
2023
|
const contentBlocks: ContentBlock[] = [];
|
|
@@ -2224,7 +2241,21 @@ export class Membrane {
|
|
|
2224
2241
|
context,
|
|
2225
2242
|
};
|
|
2226
2243
|
|
|
2227
|
-
const results = await stream.requestToolExecution(toolCallsEvent);
|
|
2244
|
+
const { results, injectedMessages } = await stream.requestToolExecution(toolCallsEvent);
|
|
2245
|
+
|
|
2246
|
+
// Mid-turn injected messages are not supported on the XML prefill
|
|
2247
|
+
// path: the continuation is an assistant prefill over an XML
|
|
2248
|
+
// transcript, not a message array, so there is no user envelope
|
|
2249
|
+
// to append to. Warn (once per stream — long turns have many
|
|
2250
|
+
// rounds) rather than drop silently; the messages remain in the
|
|
2251
|
+
// caller's context window and reach the model on the next turn.
|
|
2252
|
+
if (injectedMessages && injectedMessages.length > 0 && !warnedInjectionUnsupported) {
|
|
2253
|
+
warnedInjectionUnsupported = true;
|
|
2254
|
+
console.warn(
|
|
2255
|
+
`[membrane] provideToolResults injectedMessages ignored: XML tool mode ` +
|
|
2256
|
+
`does not support mid-turn injection (delivered next turn instead)`
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2228
2259
|
|
|
2229
2260
|
// Track the tool results
|
|
2230
2261
|
executedToolResults.push(...results);
|
|
@@ -2647,7 +2678,7 @@ export class Membrane {
|
|
|
2647
2678
|
context,
|
|
2648
2679
|
};
|
|
2649
2680
|
|
|
2650
|
-
const results = await stream.requestToolExecution(toolCallsEvent);
|
|
2681
|
+
const { results, injectedMessages } = await stream.requestToolExecution(toolCallsEvent);
|
|
2651
2682
|
|
|
2652
2683
|
// Track tool results
|
|
2653
2684
|
executedToolResults.push(...results);
|
|
@@ -2665,13 +2696,14 @@ export class Membrane {
|
|
|
2665
2696
|
// Add messages for next iteration — use the request's participant names
|
|
2666
2697
|
const assistantName = request.assistantParticipant
|
|
2667
2698
|
?? this.config.assistantParticipant ?? 'Claude';
|
|
2699
|
+
const userName = assistantName === 'Claude' ? 'User' : 'user';
|
|
2668
2700
|
messages.push({
|
|
2669
2701
|
participant: assistantName,
|
|
2670
2702
|
content: responseBlocks,
|
|
2671
2703
|
});
|
|
2672
2704
|
|
|
2673
2705
|
messages.push({
|
|
2674
|
-
participant:
|
|
2706
|
+
participant: userName,
|
|
2675
2707
|
content: results.map(r => ({
|
|
2676
2708
|
type: 'tool_result' as const,
|
|
2677
2709
|
toolUseId: r.toolUseId,
|
|
@@ -2680,6 +2712,23 @@ export class Membrane {
|
|
|
2680
2712
|
})),
|
|
2681
2713
|
});
|
|
2682
2714
|
|
|
2715
|
+
// Mid-turn injection: messages that arrived while this round's
|
|
2716
|
+
// tools were executing, appended AFTER the tool_result envelope so
|
|
2717
|
+
// the next inference round sees them. Placed here (not co-mingled
|
|
2718
|
+
// with the results) so provider conversions that special-case
|
|
2719
|
+
// tool_result envelopes (e.g. ChatCompletions role:'tool') carry
|
|
2720
|
+
// them as ordinary user messages. Signed-thinking adjacency is
|
|
2721
|
+
// unaffected: the assistant turn above is round-tripped verbatim.
|
|
2722
|
+
if (injectedMessages) {
|
|
2723
|
+
for (const injected of injectedMessages) {
|
|
2724
|
+
messages.push({
|
|
2725
|
+
participant: injected.participant ?? userName,
|
|
2726
|
+
content: injected.content,
|
|
2727
|
+
...(injected.metadata ? { metadata: injected.metadata } : {}),
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2683
2732
|
toolDepth++;
|
|
2684
2733
|
continue;
|
|
2685
2734
|
}
|
|
@@ -409,8 +409,25 @@ export class OpenAICompatibleAdapter implements ProviderAdapter {
|
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
-
// If we have tool results,
|
|
412
|
+
// If we have tool results, emit them (possibly multiple) as
|
|
413
|
+
// role:'tool' messages — plus a FOLLOWING user message for any other
|
|
414
|
+
// content sharing the envelope (e.g. a mid-turn injected user message
|
|
415
|
+
// that mergeConsecutiveRoles folded into the tool_result envelope),
|
|
416
|
+
// instead of silently dropping it. Mirrors openai.ts / openrouter.ts.
|
|
413
417
|
if (toolResults.length > 0) {
|
|
418
|
+
const hasImagesInterloper = contentParts.some(p => p.type === 'image_url');
|
|
419
|
+
const interloperText = hasImagesInterloper
|
|
420
|
+
? ''
|
|
421
|
+
: contentParts.filter(p => p.type === 'text').map(p => p.text!).join('\n');
|
|
422
|
+
if (hasImagesInterloper || interloperText) {
|
|
423
|
+
return [
|
|
424
|
+
...toolResults,
|
|
425
|
+
{
|
|
426
|
+
role: 'user' as const,
|
|
427
|
+
content: hasImagesInterloper ? contentParts : interloperText,
|
|
428
|
+
},
|
|
429
|
+
];
|
|
430
|
+
}
|
|
414
431
|
return toolResults;
|
|
415
432
|
}
|
|
416
433
|
|
package/src/providers/openai.ts
CHANGED
|
@@ -472,8 +472,27 @@ export class OpenAIAdapter implements ProviderAdapter {
|
|
|
472
472
|
}
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
// If we have tool results,
|
|
475
|
+
// If we have tool results, emit them (possibly multiple) as
|
|
476
|
+
// role:'tool' messages. Any OTHER content sharing the envelope
|
|
477
|
+
// (e.g. a mid-turn user message injected alongside tool_results —
|
|
478
|
+
// mergeConsecutiveRoles folds the separately-pushed injected user
|
|
479
|
+
// message INTO the tool_result envelope) is emitted as a FOLLOWING
|
|
480
|
+
// user message rather than dropped — this used to silently swallow
|
|
481
|
+
// such text. Same fix in openrouter.ts / openai-compatible.ts.
|
|
476
482
|
if (toolResults.length > 0) {
|
|
483
|
+
const hasImagesInterloper = contentParts.some(p => p.type === 'image_url');
|
|
484
|
+
const interloperText = hasImagesInterloper
|
|
485
|
+
? '' // unused: array form carries the parts
|
|
486
|
+
: contentParts.filter(p => p.type === 'text').map(p => p.text!).join('\n');
|
|
487
|
+
if (hasImagesInterloper || interloperText) {
|
|
488
|
+
return [
|
|
489
|
+
...toolResults,
|
|
490
|
+
{
|
|
491
|
+
role: 'user' as const,
|
|
492
|
+
content: hasImagesInterloper ? contentParts : interloperText,
|
|
493
|
+
},
|
|
494
|
+
];
|
|
495
|
+
}
|
|
477
496
|
return toolResults;
|
|
478
497
|
}
|
|
479
498
|
|
|
@@ -514,8 +514,25 @@ export class OpenRouterAdapter implements ProviderAdapter {
|
|
|
514
514
|
}
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
-
// If we have tool results,
|
|
517
|
+
// If we have tool results, emit them (possibly multiple) as
|
|
518
|
+
// role:'tool' messages — plus a FOLLOWING user message for any other
|
|
519
|
+
// content sharing the envelope (e.g. a mid-turn injected user message
|
|
520
|
+
// that mergeConsecutiveRoles folded into the tool_result envelope),
|
|
521
|
+
// instead of silently dropping it. Mirrors openai.ts / openai-compatible.ts.
|
|
518
522
|
if (toolResults.length > 0) {
|
|
523
|
+
const interloperText = textParts.join('\n');
|
|
524
|
+
if (contentBlocks.length > 0) {
|
|
525
|
+
return [
|
|
526
|
+
...toolResults,
|
|
527
|
+
{ role: 'user' as const, content: contentBlocks },
|
|
528
|
+
];
|
|
529
|
+
}
|
|
530
|
+
if (interloperText) {
|
|
531
|
+
return [
|
|
532
|
+
...toolResults,
|
|
533
|
+
{ role: 'user' as const, content: interloperText },
|
|
534
|
+
];
|
|
535
|
+
}
|
|
519
536
|
return toolResults;
|
|
520
537
|
}
|
|
521
538
|
|
package/src/types/index.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { ContentBlock } from './content.js';
|
|
12
|
+
import type { NormalizedMessage } from './message.js';
|
|
12
13
|
import type { ToolCall, ToolResult, ToolContext } from './tools.js';
|
|
13
14
|
import type { DetailedUsage, NormalizedResponse, StopReason } from './response.js';
|
|
14
15
|
import type { ChunkMeta, BlockEvent } from './streaming.js';
|
|
@@ -122,15 +123,63 @@ export type StreamEvent =
|
|
|
122
123
|
* }
|
|
123
124
|
* ```
|
|
124
125
|
*/
|
|
126
|
+
/**
|
|
127
|
+
* A user-side message injected into the conversation between tool rounds.
|
|
128
|
+
*
|
|
129
|
+
* This is how a consumer lets the model see events that arrived while the
|
|
130
|
+
* turn was in flight (e.g. a chat reply landing mid-way through a long
|
|
131
|
+
* tool-using turn): pass it alongside the tool results and the next
|
|
132
|
+
* inference round's request includes it as a user message AFTER the
|
|
133
|
+
* tool_result envelope.
|
|
134
|
+
*
|
|
135
|
+
* Placement guarantee: injected messages always land after the round's
|
|
136
|
+
* tool_results (the wire normalizer additionally enforces results-first
|
|
137
|
+
* ordering inside a merged envelope), so they never break the
|
|
138
|
+
* tool_use → tool_result adjacency or signed-thinking constraints.
|
|
139
|
+
*
|
|
140
|
+
* Shape: NormalizedMessage minus cacheBreakpoint (breakpoints are the
|
|
141
|
+
* request compiler's concern), with `participant` optional — it defaults to
|
|
142
|
+
* the generic user participant. Non-assistant participants get the standard
|
|
143
|
+
* "Name: " text prefix when rendered to the provider. Content must be
|
|
144
|
+
* user-side blocks only (text/image); tool blocks are stripped with a
|
|
145
|
+
* warning. NOTE: a participant equal to the request's assistantParticipant
|
|
146
|
+
* would render as an ASSISTANT turn (a prefill) — callers should not inject
|
|
147
|
+
* messages named as the assistant.
|
|
148
|
+
*/
|
|
149
|
+
export type InjectedMessage =
|
|
150
|
+
Omit<NormalizedMessage, 'participant' | 'cacheBreakpoint'> & {
|
|
151
|
+
participant?: string;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Options for provideToolResults().
|
|
156
|
+
*/
|
|
157
|
+
export interface ProvideToolResultsOptions {
|
|
158
|
+
/**
|
|
159
|
+
* Messages that arrived while the turn was in flight, to be appended to
|
|
160
|
+
* the conversation after this round's tool_result envelope so the NEXT
|
|
161
|
+
* inference round sees them.
|
|
162
|
+
*
|
|
163
|
+
* Supported in native tool mode (Anthropic Messages, OpenAI Responses,
|
|
164
|
+
* OpenRouter). The XML prefill path currently ignores these (the
|
|
165
|
+
* continuation is an assistant prefill, not a message array) — callers
|
|
166
|
+
* on XML-mode models should deliver mid-turn events on the next turn
|
|
167
|
+
* instead.
|
|
168
|
+
*/
|
|
169
|
+
injectedMessages?: InjectedMessage[];
|
|
170
|
+
}
|
|
171
|
+
|
|
125
172
|
export interface YieldingStream extends AsyncIterable<StreamEvent> {
|
|
126
173
|
/**
|
|
127
174
|
* Provide tool results after receiving a 'tool-calls' event.
|
|
128
175
|
* The stream will resume and continue generating.
|
|
129
176
|
*
|
|
130
177
|
* @param results - Results for the tool calls (must match call IDs)
|
|
178
|
+
* @param options - Optionally inject mid-turn user messages into the
|
|
179
|
+
* next round (see ProvideToolResultsOptions.injectedMessages)
|
|
131
180
|
* @throws Error if called when not waiting for tool results
|
|
132
181
|
*/
|
|
133
|
-
provideToolResults(results: ToolResult[]): void;
|
|
182
|
+
provideToolResults(results: ToolResult[], options?: ProvideToolResultsOptions): void;
|
|
134
183
|
|
|
135
184
|
/**
|
|
136
185
|
* Cancel the stream. Any in-flight requests will be aborted.
|
package/src/yielding-stream.ts
CHANGED
|
@@ -10,9 +10,21 @@ import type {
|
|
|
10
10
|
YieldingStream,
|
|
11
11
|
YieldingStreamOptions,
|
|
12
12
|
ToolCallsEvent,
|
|
13
|
+
InjectedMessage,
|
|
14
|
+
ProvideToolResultsOptions,
|
|
13
15
|
} from './types/yielding-stream.js';
|
|
14
16
|
import type { ToolResult } from './types/tools.js';
|
|
15
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Payload handed from provideToolResults() to the inference loop awaiting
|
|
20
|
+
* requestToolExecution(): the round's tool results plus any mid-turn
|
|
21
|
+
* messages the consumer wants the next inference round to see.
|
|
22
|
+
*/
|
|
23
|
+
export interface ToolResultsPayload {
|
|
24
|
+
results: ToolResult[];
|
|
25
|
+
injectedMessages?: InjectedMessage[];
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
// ============================================================================
|
|
17
29
|
// Internal State Types
|
|
18
30
|
// ============================================================================
|
|
@@ -25,7 +37,7 @@ type StreamState =
|
|
|
25
37
|
| { status: 'error'; error: Error };
|
|
26
38
|
|
|
27
39
|
interface PendingToolResults {
|
|
28
|
-
resolve: (
|
|
40
|
+
resolve: (payload: ToolResultsPayload) => void;
|
|
29
41
|
reject: (error: Error) => void;
|
|
30
42
|
}
|
|
31
43
|
|
|
@@ -98,7 +110,7 @@ export class YieldingStreamImpl implements YieldingStream {
|
|
|
98
110
|
return this.abortController.signal;
|
|
99
111
|
}
|
|
100
112
|
|
|
101
|
-
provideToolResults(results: ToolResult[]): void {
|
|
113
|
+
provideToolResults(results: ToolResult[], options?: ProvideToolResultsOptions): void {
|
|
102
114
|
if (this.state.status !== 'waiting_for_tools') {
|
|
103
115
|
throw new Error(
|
|
104
116
|
`Cannot provide tool results: stream is not waiting for tools (status: ${this.state.status})`
|
|
@@ -119,8 +131,32 @@ export class YieldingStreamImpl implements YieldingStream {
|
|
|
119
131
|
}
|
|
120
132
|
}
|
|
121
133
|
|
|
134
|
+
// Injected messages must be user-side content only — tool blocks here
|
|
135
|
+
// would corrupt the tool-cycle structure the normalizer guarantees.
|
|
136
|
+
// Offending BLOCKS are stripped (loudly) rather than thrown on: a throw
|
|
137
|
+
// at this point would leave the stream parked in waiting_for_tools with
|
|
138
|
+
// an unresolvable promise — wedging the caller's whole turn over a
|
|
139
|
+
// notification is far worse than delivering it without its tool blocks.
|
|
140
|
+
const injectedMessages = options?.injectedMessages
|
|
141
|
+
?.map((m) => {
|
|
142
|
+
const clean = m.content.filter(
|
|
143
|
+
(block) => block.type !== 'tool_use' && block.type !== 'tool_result'
|
|
144
|
+
);
|
|
145
|
+
if (clean.length !== m.content.length) {
|
|
146
|
+
console.warn(
|
|
147
|
+
`[membrane] provideToolResults: stripped ${m.content.length - clean.length} ` +
|
|
148
|
+
`tool block(s) from an injected mid-turn message (user-side content only)`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return { ...m, content: clean };
|
|
152
|
+
})
|
|
153
|
+
.filter((m) => m.content.length > 0);
|
|
154
|
+
|
|
122
155
|
// Resolve the promise and transition state
|
|
123
|
-
this.pendingToolResults.resolve(
|
|
156
|
+
this.pendingToolResults.resolve({
|
|
157
|
+
results,
|
|
158
|
+
...(injectedMessages && injectedMessages.length > 0 ? { injectedMessages } : {}),
|
|
159
|
+
});
|
|
124
160
|
this.pendingToolResults = null;
|
|
125
161
|
this.state = { status: 'streaming' };
|
|
126
162
|
this._toolDepth++;
|
|
@@ -198,7 +234,7 @@ export class YieldingStreamImpl implements YieldingStream {
|
|
|
198
234
|
* Request tool execution and wait for results.
|
|
199
235
|
* Called by the inference loop when tool calls are detected.
|
|
200
236
|
*/
|
|
201
|
-
async requestToolExecution(event: ToolCallsEvent): Promise<
|
|
237
|
+
async requestToolExecution(event: ToolCallsEvent): Promise<ToolResultsPayload> {
|
|
202
238
|
// Emit the tool calls event
|
|
203
239
|
this.emit(event);
|
|
204
240
|
|
|
@@ -209,7 +245,7 @@ export class YieldingStreamImpl implements YieldingStream {
|
|
|
209
245
|
};
|
|
210
246
|
|
|
211
247
|
// Create a promise that will be resolved by provideToolResults()
|
|
212
|
-
return new Promise<
|
|
248
|
+
return new Promise<ToolResultsPayload>((resolve, reject) => {
|
|
213
249
|
this.pendingToolResults = { resolve, reject };
|
|
214
250
|
});
|
|
215
251
|
}
|