@animalabs/membrane 0.5.23 → 0.5.25
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/membrane.d.ts +37 -0
- package/dist/membrane.d.ts.map +1 -1
- package/dist/membrane.js +554 -0
- package/dist/membrane.js.map +1 -1
- package/dist/providers/mock.d.ts +8 -0
- package/dist/providers/mock.d.ts.map +1 -1
- package/dist/providers/mock.js +39 -2
- package/dist/providers/mock.js.map +1 -1
- package/dist/providers/openai-compatible.d.ts.map +1 -1
- package/dist/providers/openai-compatible.js +7 -2
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +7 -2
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts.map +1 -1
- package/dist/providers/openrouter.js +7 -2
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/yielding-stream.d.ts +167 -0
- package/dist/types/yielding-stream.d.ts.map +1 -0
- package/dist/types/yielding-stream.js +34 -0
- package/dist/types/yielding-stream.js.map +1 -0
- package/dist/yielding-stream.d.ts +60 -0
- package/dist/yielding-stream.d.ts.map +1 -0
- package/dist/yielding-stream.js +204 -0
- package/dist/yielding-stream.js.map +1 -0
- package/package.json +1 -1
- package/src/membrane.ts +691 -1
- package/src/providers/mock.ts +47 -2
- package/src/providers/openai-compatible.ts +10 -4
- package/src/providers/openai.ts +10 -4
- package/src/providers/openrouter.ts +10 -4
- package/src/types/index.ts +23 -0
- package/src/types/yielding-stream.ts +228 -0
- package/src/yielding-stream.ts +271 -0
package/src/providers/mock.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
ProviderResponse,
|
|
13
13
|
StreamCallbacks,
|
|
14
14
|
} from '../types/provider.js';
|
|
15
|
+
import { abortError } from '../types/errors.js';
|
|
15
16
|
|
|
16
17
|
export interface MockAdapterConfig {
|
|
17
18
|
/** Default response text when no specific response is configured */
|
|
@@ -157,12 +158,15 @@ export class MockAdapter implements ProviderAdapter {
|
|
|
157
158
|
request: ProviderRequest,
|
|
158
159
|
options?: ProviderRequestOptions
|
|
159
160
|
): Promise<ProviderResponse> {
|
|
161
|
+
// Check for abort before starting
|
|
162
|
+
this.checkAbort(options?.signal);
|
|
163
|
+
|
|
160
164
|
// Call onRequest callback if provided
|
|
161
165
|
options?.onRequest?.(request);
|
|
162
166
|
|
|
163
167
|
// Simulate processing delay
|
|
164
168
|
if (this.config.completeDelayMs > 0) {
|
|
165
|
-
await this.
|
|
169
|
+
await this.abortableSleep(this.config.completeDelayMs, options?.signal);
|
|
166
170
|
}
|
|
167
171
|
|
|
168
172
|
const responseText = this.getResponse(request);
|
|
@@ -185,20 +189,28 @@ export class MockAdapter implements ProviderAdapter {
|
|
|
185
189
|
callbacks: StreamCallbacks,
|
|
186
190
|
options?: ProviderRequestOptions
|
|
187
191
|
): Promise<ProviderResponse> {
|
|
192
|
+
// Check for abort before starting
|
|
193
|
+
this.checkAbort(options?.signal);
|
|
194
|
+
|
|
188
195
|
// Call onRequest callback if provided
|
|
189
196
|
options?.onRequest?.(request);
|
|
190
197
|
|
|
191
198
|
const responseText = this.getResponse(request);
|
|
199
|
+
let streamedText = '';
|
|
192
200
|
|
|
193
201
|
// Stream the response in chunks
|
|
194
202
|
let offset = 0;
|
|
195
203
|
while (offset < responseText.length) {
|
|
204
|
+
// Check for abort before each chunk
|
|
205
|
+
this.checkAbort(options?.signal);
|
|
206
|
+
|
|
196
207
|
const chunk = responseText.slice(offset, offset + this.config.streamChunkSize);
|
|
197
208
|
callbacks.onChunk(chunk);
|
|
209
|
+
streamedText += chunk;
|
|
198
210
|
offset += this.config.streamChunkSize;
|
|
199
211
|
|
|
200
212
|
if (offset < responseText.length && this.config.streamChunkDelayMs > 0) {
|
|
201
|
-
await this.
|
|
213
|
+
await this.abortableSleep(this.config.streamChunkDelayMs, options?.signal);
|
|
202
214
|
}
|
|
203
215
|
}
|
|
204
216
|
|
|
@@ -215,6 +227,39 @@ export class MockAdapter implements ProviderAdapter {
|
|
|
215
227
|
};
|
|
216
228
|
}
|
|
217
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Check if the abort signal is set and throw if so.
|
|
232
|
+
*/
|
|
233
|
+
private checkAbort(signal?: AbortSignal): void {
|
|
234
|
+
if (signal?.aborted) {
|
|
235
|
+
throw abortError('Request aborted');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Sleep that can be interrupted by an abort signal.
|
|
241
|
+
*/
|
|
242
|
+
private abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
if (signal?.aborted) {
|
|
245
|
+
reject(abortError('Request aborted'));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const onAbort = () => {
|
|
250
|
+
clearTimeout(timeout);
|
|
251
|
+
reject(abortError('Request aborted'));
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const timeout = setTimeout(() => {
|
|
255
|
+
signal?.removeEventListener('abort', onAbort);
|
|
256
|
+
resolve();
|
|
257
|
+
}, ms);
|
|
258
|
+
|
|
259
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
218
263
|
private sleep(ms: number): Promise<void> {
|
|
219
264
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
220
265
|
}
|
|
@@ -291,8 +291,9 @@ export class OpenAICompatibleAdapter implements ProviderAdapter {
|
|
|
291
291
|
params.frequency_penalty = request.frequencyPenalty;
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
// OpenAI-compatible APIs may limit stop sequences (OpenAI: 4) — truncate to be safe
|
|
294
295
|
if (request.stopSequences && request.stopSequences.length > 0) {
|
|
295
|
-
params.stop = request.stopSequences;
|
|
296
|
+
params.stop = request.stopSequences.slice(0, 4);
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
if (request.tools && request.tools.length > 0) {
|
|
@@ -349,16 +350,21 @@ export class OpenAICompatibleAdapter implements ProviderAdapter {
|
|
|
349
350
|
return toolResults;
|
|
350
351
|
}
|
|
351
352
|
|
|
353
|
+
// Skip messages with no usable content (image-only, embed-only messages)
|
|
354
|
+
if (textParts.length === 0 && toolCalls.length === 0) {
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
|
|
352
358
|
// Otherwise build normal message
|
|
353
359
|
const result: OpenAIMessage = {
|
|
354
360
|
role: msg.role,
|
|
355
|
-
content: textParts.join('\n')
|
|
361
|
+
content: textParts.length > 0 ? textParts.join('\n') : null,
|
|
356
362
|
};
|
|
357
|
-
|
|
363
|
+
|
|
358
364
|
if (toolCalls.length > 0) {
|
|
359
365
|
result.tool_calls = toolCalls;
|
|
360
366
|
}
|
|
361
|
-
|
|
367
|
+
|
|
362
368
|
return [result];
|
|
363
369
|
}
|
|
364
370
|
|
package/src/providers/openai.ts
CHANGED
|
@@ -390,8 +390,9 @@ export class OpenAIAdapter implements ProviderAdapter {
|
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
// Reasoning models (o1, o3, o4) don't support stop sequences
|
|
393
|
+
// OpenAI limits stop sequences to 4 — truncate to fit
|
|
393
394
|
if (request.stopSequences && request.stopSequences.length > 0 && !noStopSupport(model)) {
|
|
394
|
-
params.stop = request.stopSequences;
|
|
395
|
+
params.stop = request.stopSequences.slice(0, 4);
|
|
395
396
|
}
|
|
396
397
|
|
|
397
398
|
if (request.tools && request.tools.length > 0) {
|
|
@@ -448,16 +449,21 @@ export class OpenAIAdapter implements ProviderAdapter {
|
|
|
448
449
|
return toolResults;
|
|
449
450
|
}
|
|
450
451
|
|
|
452
|
+
// Skip messages with no usable content (image-only, embed-only messages)
|
|
453
|
+
if (textParts.length === 0 && toolCalls.length === 0) {
|
|
454
|
+
return [];
|
|
455
|
+
}
|
|
456
|
+
|
|
451
457
|
// Otherwise build normal message
|
|
452
458
|
const result: OpenAIMessage = {
|
|
453
459
|
role: msg.role,
|
|
454
|
-
content: textParts.join('\n')
|
|
460
|
+
content: textParts.length > 0 ? textParts.join('\n') : null,
|
|
455
461
|
};
|
|
456
|
-
|
|
462
|
+
|
|
457
463
|
if (toolCalls.length > 0) {
|
|
458
464
|
result.tool_calls = toolCalls;
|
|
459
465
|
}
|
|
460
|
-
|
|
466
|
+
|
|
461
467
|
return [result];
|
|
462
468
|
}
|
|
463
469
|
|
|
@@ -313,8 +313,9 @@ export class OpenRouterAdapter implements ProviderAdapter {
|
|
|
313
313
|
params.frequency_penalty = request.frequencyPenalty;
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
+
// OpenAI-compatible APIs may limit stop sequences (OpenAI: 4) — truncate to be safe
|
|
316
317
|
if (request.stopSequences && request.stopSequences.length > 0) {
|
|
317
|
-
params.stop = request.stopSequences;
|
|
318
|
+
params.stop = request.stopSequences.slice(0, 4);
|
|
318
319
|
}
|
|
319
320
|
|
|
320
321
|
if (request.tools && request.tools.length > 0) {
|
|
@@ -406,17 +407,22 @@ export class OpenRouterAdapter implements ProviderAdapter {
|
|
|
406
407
|
return toolResults;
|
|
407
408
|
}
|
|
408
409
|
|
|
410
|
+
// Skip messages with no usable content (image-only, embed-only messages)
|
|
411
|
+
if (textParts.length === 0 && toolCalls.length === 0) {
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
|
|
409
415
|
// Otherwise build normal message
|
|
410
416
|
const result: OpenRouterMessage = {
|
|
411
417
|
role: msg.role,
|
|
412
418
|
// Use content blocks array if caching is in use, otherwise concatenate text
|
|
413
|
-
content: hasCache ? contentBlocks : (textParts.join('\n')
|
|
419
|
+
content: hasCache ? contentBlocks : (textParts.length > 0 ? textParts.join('\n') : null),
|
|
414
420
|
};
|
|
415
|
-
|
|
421
|
+
|
|
416
422
|
if (toolCalls.length > 0) {
|
|
417
423
|
result.tool_calls = toolCalls;
|
|
418
424
|
}
|
|
419
|
-
|
|
425
|
+
|
|
420
426
|
return [result];
|
|
421
427
|
}
|
|
422
428
|
|
package/src/types/index.ts
CHANGED
|
@@ -120,6 +120,29 @@ export type {
|
|
|
120
120
|
BlockDelta,
|
|
121
121
|
} from './streaming.js';
|
|
122
122
|
|
|
123
|
+
// Yielding Stream
|
|
124
|
+
export type {
|
|
125
|
+
TokensEvent,
|
|
126
|
+
StreamBlockEvent,
|
|
127
|
+
ToolCallsEvent,
|
|
128
|
+
UsageEvent,
|
|
129
|
+
CompleteEvent,
|
|
130
|
+
ErrorEvent,
|
|
131
|
+
AbortedEvent,
|
|
132
|
+
StreamEvent,
|
|
133
|
+
YieldingStream,
|
|
134
|
+
YieldingStreamOptions,
|
|
135
|
+
} from './yielding-stream.js';
|
|
136
|
+
|
|
137
|
+
export {
|
|
138
|
+
isTokensEvent,
|
|
139
|
+
isToolCallsEvent,
|
|
140
|
+
isCompleteEvent,
|
|
141
|
+
isErrorEvent,
|
|
142
|
+
isAbortedEvent,
|
|
143
|
+
isTerminalEvent,
|
|
144
|
+
} from './yielding-stream.js';
|
|
145
|
+
|
|
123
146
|
// Errors
|
|
124
147
|
export type {
|
|
125
148
|
MembraneErrorType,
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yielding stream types for membrane
|
|
3
|
+
*
|
|
4
|
+
* This module defines the interface for a streaming API that yields control
|
|
5
|
+
* back to the caller when tool calls are detected, rather than handling them
|
|
6
|
+
* internally via callbacks.
|
|
7
|
+
*
|
|
8
|
+
* @see agent-framework/docs/yielding-stream-architecture.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ContentBlock } from './content.js';
|
|
12
|
+
import type { ToolCall, ToolResult, ToolContext } from './tools.js';
|
|
13
|
+
import type { BasicUsage, NormalizedResponse, StopReason } from './response.js';
|
|
14
|
+
import type { ChunkMeta, BlockEvent } from './streaming.js';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Stream Events
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Token/chunk event - raw text as it arrives from the LLM.
|
|
22
|
+
*/
|
|
23
|
+
export interface TokensEvent {
|
|
24
|
+
type: 'tokens';
|
|
25
|
+
content: string;
|
|
26
|
+
meta: ChunkMeta;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Block event - structural block start/complete notifications.
|
|
31
|
+
*/
|
|
32
|
+
export interface StreamBlockEvent {
|
|
33
|
+
type: 'block';
|
|
34
|
+
event: BlockEvent;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Tool calls event - LLM has requested tool execution.
|
|
39
|
+
* The stream pauses here until results are provided via provideToolResults().
|
|
40
|
+
*/
|
|
41
|
+
export interface ToolCallsEvent {
|
|
42
|
+
type: 'tool-calls';
|
|
43
|
+
calls: ToolCall[];
|
|
44
|
+
context: ToolContext;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Usage update event - token counts updated.
|
|
49
|
+
*/
|
|
50
|
+
export interface UsageEvent {
|
|
51
|
+
type: 'usage';
|
|
52
|
+
usage: BasicUsage;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Complete event - inference cycle finished successfully.
|
|
57
|
+
*/
|
|
58
|
+
export interface CompleteEvent {
|
|
59
|
+
type: 'complete';
|
|
60
|
+
response: NormalizedResponse;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Error event - something went wrong.
|
|
65
|
+
*/
|
|
66
|
+
export interface ErrorEvent {
|
|
67
|
+
type: 'error';
|
|
68
|
+
error: Error;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Aborted event - stream was cancelled.
|
|
73
|
+
*/
|
|
74
|
+
export interface AbortedEvent {
|
|
75
|
+
type: 'aborted';
|
|
76
|
+
reason: 'user' | 'timeout' | 'error';
|
|
77
|
+
partialContent?: ContentBlock[];
|
|
78
|
+
rawAssistantText?: string;
|
|
79
|
+
toolCalls?: ToolCall[];
|
|
80
|
+
toolResults?: ToolResult[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Union of all stream events.
|
|
85
|
+
*/
|
|
86
|
+
export type StreamEvent =
|
|
87
|
+
| TokensEvent
|
|
88
|
+
| StreamBlockEvent
|
|
89
|
+
| ToolCallsEvent
|
|
90
|
+
| UsageEvent
|
|
91
|
+
| CompleteEvent
|
|
92
|
+
| ErrorEvent
|
|
93
|
+
| AbortedEvent;
|
|
94
|
+
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// Yielding Stream Interface
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A streaming inference that yields control to the caller for tool execution.
|
|
101
|
+
*
|
|
102
|
+
* Usage:
|
|
103
|
+
* ```typescript
|
|
104
|
+
* const stream = membrane.streamYielding(request, options);
|
|
105
|
+
*
|
|
106
|
+
* for await (const event of stream) {
|
|
107
|
+
* switch (event.type) {
|
|
108
|
+
* case 'tokens':
|
|
109
|
+
* process.stdout.write(event.content);
|
|
110
|
+
* break;
|
|
111
|
+
* case 'tool-calls':
|
|
112
|
+
* const results = await executeTools(event.calls);
|
|
113
|
+
* stream.provideToolResults(results);
|
|
114
|
+
* break;
|
|
115
|
+
* case 'complete':
|
|
116
|
+
* console.log('Done:', event.response);
|
|
117
|
+
* break;
|
|
118
|
+
* case 'error':
|
|
119
|
+
* console.error('Error:', event.error);
|
|
120
|
+
* break;
|
|
121
|
+
* }
|
|
122
|
+
* }
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export interface YieldingStream extends AsyncIterable<StreamEvent> {
|
|
126
|
+
/**
|
|
127
|
+
* Provide tool results after receiving a 'tool-calls' event.
|
|
128
|
+
* The stream will resume and continue generating.
|
|
129
|
+
*
|
|
130
|
+
* @param results - Results for the tool calls (must match call IDs)
|
|
131
|
+
* @throws Error if called when not waiting for tool results
|
|
132
|
+
*/
|
|
133
|
+
provideToolResults(results: ToolResult[]): void;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Cancel the stream. Any in-flight requests will be aborted.
|
|
137
|
+
* The iterator will yield an 'aborted' event and then complete.
|
|
138
|
+
*/
|
|
139
|
+
cancel(): void;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check if the stream is currently waiting for tool results.
|
|
143
|
+
*/
|
|
144
|
+
readonly isWaitingForTools: boolean;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get the IDs of tool calls we're waiting for results for.
|
|
148
|
+
* Empty if not waiting for tools.
|
|
149
|
+
*/
|
|
150
|
+
readonly pendingToolCallIds: string[];
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Current tool execution depth (0 = first inference, 1 = after first tool round, etc.)
|
|
154
|
+
*/
|
|
155
|
+
readonly toolDepth: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============================================================================
|
|
159
|
+
// Yielding Stream Options
|
|
160
|
+
// ============================================================================
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Options for streamYielding().
|
|
164
|
+
* Simpler than StreamOptions since tool execution is handled externally.
|
|
165
|
+
*/
|
|
166
|
+
export interface YieldingStreamOptions {
|
|
167
|
+
/** Abort signal for cancellation */
|
|
168
|
+
signal?: AbortSignal;
|
|
169
|
+
|
|
170
|
+
/** Request timeout (per API call, not total) */
|
|
171
|
+
timeoutMs?: number;
|
|
172
|
+
|
|
173
|
+
/** Request ID for correlation/logging */
|
|
174
|
+
requestId?: string;
|
|
175
|
+
|
|
176
|
+
/** Maximum tool execution depth (default: 10) */
|
|
177
|
+
maxToolDepth?: number;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Whether to emit 'tokens' events.
|
|
181
|
+
* Set to false if you only care about tool calls and final response.
|
|
182
|
+
* Default: true
|
|
183
|
+
*/
|
|
184
|
+
emitTokens?: boolean;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Whether to emit 'block' events.
|
|
188
|
+
* Default: true
|
|
189
|
+
*/
|
|
190
|
+
emitBlocks?: boolean;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Whether to emit 'usage' events.
|
|
194
|
+
* Default: true
|
|
195
|
+
*/
|
|
196
|
+
emitUsage?: boolean;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ============================================================================
|
|
200
|
+
// Type Guards
|
|
201
|
+
// ============================================================================
|
|
202
|
+
|
|
203
|
+
export function isTokensEvent(event: StreamEvent): event is TokensEvent {
|
|
204
|
+
return event.type === 'tokens';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function isToolCallsEvent(event: StreamEvent): event is ToolCallsEvent {
|
|
208
|
+
return event.type === 'tool-calls';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function isCompleteEvent(event: StreamEvent): event is CompleteEvent {
|
|
212
|
+
return event.type === 'complete';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function isErrorEvent(event: StreamEvent): event is ErrorEvent {
|
|
216
|
+
return event.type === 'error';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function isAbortedEvent(event: StreamEvent): event is AbortedEvent {
|
|
220
|
+
return event.type === 'aborted';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check if the stream has terminated (complete, error, or aborted).
|
|
225
|
+
*/
|
|
226
|
+
export function isTerminalEvent(event: StreamEvent): event is CompleteEvent | ErrorEvent | AbortedEvent {
|
|
227
|
+
return event.type === 'complete' || event.type === 'error' || event.type === 'aborted';
|
|
228
|
+
}
|