@animalabs/membrane 0.5.24 → 0.5.26

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 (42) hide show
  1. package/dist/membrane.d.ts +37 -0
  2. package/dist/membrane.d.ts.map +1 -1
  3. package/dist/membrane.js +590 -1
  4. package/dist/membrane.js.map +1 -1
  5. package/dist/providers/gemini.d.ts.map +1 -1
  6. package/dist/providers/gemini.js +9 -2
  7. package/dist/providers/gemini.js.map +1 -1
  8. package/dist/providers/mock.d.ts +8 -0
  9. package/dist/providers/mock.d.ts.map +1 -1
  10. package/dist/providers/mock.js +39 -2
  11. package/dist/providers/mock.js.map +1 -1
  12. package/dist/providers/openai-compatible.d.ts.map +1 -1
  13. package/dist/providers/openai-compatible.js +5 -1
  14. package/dist/providers/openai-compatible.js.map +1 -1
  15. package/dist/providers/openai.d.ts.map +1 -1
  16. package/dist/providers/openai.js +5 -1
  17. package/dist/providers/openai.js.map +1 -1
  18. package/dist/providers/openrouter.d.ts.map +1 -1
  19. package/dist/providers/openrouter.js +5 -1
  20. package/dist/providers/openrouter.js.map +1 -1
  21. package/dist/types/index.d.ts +2 -0
  22. package/dist/types/index.d.ts.map +1 -1
  23. package/dist/types/index.js +1 -0
  24. package/dist/types/index.js.map +1 -1
  25. package/dist/types/yielding-stream.d.ts +167 -0
  26. package/dist/types/yielding-stream.d.ts.map +1 -0
  27. package/dist/types/yielding-stream.js +34 -0
  28. package/dist/types/yielding-stream.js.map +1 -0
  29. package/dist/yielding-stream.d.ts +60 -0
  30. package/dist/yielding-stream.d.ts.map +1 -0
  31. package/dist/yielding-stream.js +204 -0
  32. package/dist/yielding-stream.js.map +1 -0
  33. package/package.json +1 -1
  34. package/src/membrane.ts +729 -2
  35. package/src/providers/gemini.ts +11 -2
  36. package/src/providers/mock.ts +47 -2
  37. package/src/providers/openai-compatible.ts +8 -3
  38. package/src/providers/openai.ts +8 -3
  39. package/src/providers/openrouter.ts +8 -3
  40. package/src/types/index.ts +23 -0
  41. package/src/types/yielding-stream.ts +228 -0
  42. package/src/yielding-stream.ts +271 -0
@@ -0,0 +1,271 @@
1
+ /**
2
+ * YieldingStream implementation
3
+ *
4
+ * Provides an async iterator interface for streaming inference that yields
5
+ * control back to the caller for tool execution.
6
+ */
7
+
8
+ import type {
9
+ StreamEvent,
10
+ YieldingStream,
11
+ YieldingStreamOptions,
12
+ ToolCallsEvent,
13
+ } from './types/yielding-stream.js';
14
+ import type { ToolResult } from './types/tools.js';
15
+
16
+ // ============================================================================
17
+ // Internal State Types
18
+ // ============================================================================
19
+
20
+ type StreamState =
21
+ | { status: 'idle' }
22
+ | { status: 'streaming' }
23
+ | { status: 'waiting_for_tools'; pendingCallIds: string[] }
24
+ | { status: 'done' }
25
+ | { status: 'error'; error: Error };
26
+
27
+ interface PendingToolResults {
28
+ resolve: (results: ToolResult[]) => void;
29
+ reject: (error: Error) => void;
30
+ }
31
+
32
+ // ============================================================================
33
+ // YieldingStreamImpl
34
+ // ============================================================================
35
+
36
+ /**
37
+ * Implementation of the YieldingStream interface.
38
+ *
39
+ * This class manages:
40
+ * - An event queue for yielding events to the consumer
41
+ * - A promise-based handshake for tool results
42
+ * - Cancellation via AbortController
43
+ * - State tracking for debugging and validation
44
+ */
45
+ export class YieldingStreamImpl implements YieldingStream {
46
+ private state: StreamState = { status: 'idle' };
47
+ private eventQueue: StreamEvent[] = [];
48
+ private pendingToolResults: PendingToolResults | null = null;
49
+ private abortController: AbortController;
50
+ private _toolDepth = 0;
51
+
52
+ // Promise/resolver for the async iterator to wait on new events
53
+ private eventWaiter: {
54
+ resolve: () => void;
55
+ promise: Promise<void>;
56
+ } | null = null;
57
+
58
+ // Flag indicating the stream producer is done
59
+ private producerDone = false;
60
+
61
+ constructor(
62
+ private readonly options: YieldingStreamOptions,
63
+ private readonly runInference: (stream: YieldingStreamImpl) => Promise<void>
64
+ ) {
65
+ this.abortController = new AbortController();
66
+
67
+ // Link external signal if provided
68
+ if (options.signal) {
69
+ options.signal.addEventListener('abort', () => {
70
+ this.cancel();
71
+ });
72
+ }
73
+ }
74
+
75
+ // ============================================================================
76
+ // Public Interface
77
+ // ============================================================================
78
+
79
+ get isWaitingForTools(): boolean {
80
+ return this.state.status === 'waiting_for_tools';
81
+ }
82
+
83
+ get pendingToolCallIds(): string[] {
84
+ if (this.state.status === 'waiting_for_tools') {
85
+ return this.state.pendingCallIds;
86
+ }
87
+ return [];
88
+ }
89
+
90
+ get toolDepth(): number {
91
+ return this._toolDepth;
92
+ }
93
+
94
+ /**
95
+ * Get the abort signal for use in internal operations.
96
+ */
97
+ get signal(): AbortSignal {
98
+ return this.abortController.signal;
99
+ }
100
+
101
+ provideToolResults(results: ToolResult[]): void {
102
+ if (this.state.status !== 'waiting_for_tools') {
103
+ throw new Error(
104
+ `Cannot provide tool results: stream is not waiting for tools (status: ${this.state.status})`
105
+ );
106
+ }
107
+
108
+ if (!this.pendingToolResults) {
109
+ throw new Error('Internal error: no pending tool results promise');
110
+ }
111
+
112
+ // Validate that results match pending call IDs
113
+ const pendingIds = new Set(this.state.pendingCallIds);
114
+ const providedIds = new Set(results.map((r) => r.toolUseId));
115
+
116
+ for (const id of pendingIds) {
117
+ if (!providedIds.has(id)) {
118
+ throw new Error(`Missing tool result for call ID: ${id}`);
119
+ }
120
+ }
121
+
122
+ // Resolve the promise and transition state
123
+ this.pendingToolResults.resolve(results);
124
+ this.pendingToolResults = null;
125
+ this.state = { status: 'streaming' };
126
+ this._toolDepth++;
127
+ }
128
+
129
+ cancel(): void {
130
+ if (this.state.status === 'done' || this.state.status === 'error') {
131
+ return; // Already terminated
132
+ }
133
+
134
+ this.abortController.abort();
135
+
136
+ // If waiting for tools, reject the pending promise
137
+ if (this.pendingToolResults) {
138
+ this.pendingToolResults.reject(new Error('Stream cancelled'));
139
+ this.pendingToolResults = null;
140
+ }
141
+
142
+ // Emit aborted event and wake the iterator so it can deliver it
143
+ this.emit({ type: 'aborted', reason: 'user' });
144
+ this.producerDone = true;
145
+ this.state = { status: 'done' };
146
+ }
147
+
148
+ // ============================================================================
149
+ // Async Iterator Implementation
150
+ // ============================================================================
151
+
152
+ [Symbol.asyncIterator](): AsyncIterator<StreamEvent> {
153
+ // Start the inference loop when iteration begins
154
+ this.startInference();
155
+
156
+ return {
157
+ next: async (): Promise<IteratorResult<StreamEvent>> => {
158
+ while (true) {
159
+ // Check for queued events
160
+ const event = this.eventQueue.shift();
161
+ if (event) {
162
+ // Check if this is a terminal event
163
+ if (
164
+ event.type === 'complete' ||
165
+ event.type === 'error' ||
166
+ event.type === 'aborted'
167
+ ) {
168
+ this.state = { status: 'done' };
169
+ }
170
+ return { value: event, done: false };
171
+ }
172
+
173
+ // If producer is done and queue is empty, we're done
174
+ if (this.producerDone) {
175
+ return { value: undefined as unknown as StreamEvent, done: true };
176
+ }
177
+
178
+ // Wait for more events
179
+ await this.waitForEvent();
180
+ }
181
+ },
182
+ };
183
+ }
184
+
185
+ // ============================================================================
186
+ // Internal Methods (called by the inference loop)
187
+ // ============================================================================
188
+
189
+ /**
190
+ * Push an event to be yielded to the consumer.
191
+ */
192
+ emit(event: StreamEvent): void {
193
+ this.eventQueue.push(event);
194
+ this.notifyEventWaiter();
195
+ }
196
+
197
+ /**
198
+ * Request tool execution and wait for results.
199
+ * Called by the inference loop when tool calls are detected.
200
+ */
201
+ async requestToolExecution(event: ToolCallsEvent): Promise<ToolResult[]> {
202
+ // Emit the tool calls event
203
+ this.emit(event);
204
+
205
+ // Transition to waiting state
206
+ this.state = {
207
+ status: 'waiting_for_tools',
208
+ pendingCallIds: event.calls.map((c) => c.id),
209
+ };
210
+
211
+ // Create a promise that will be resolved by provideToolResults()
212
+ return new Promise<ToolResult[]>((resolve, reject) => {
213
+ this.pendingToolResults = { resolve, reject };
214
+ });
215
+ }
216
+
217
+ /**
218
+ * Mark the producer as done (inference loop finished).
219
+ */
220
+ markDone(): void {
221
+ this.producerDone = true;
222
+ this.notifyEventWaiter();
223
+ }
224
+
225
+ /**
226
+ * Check if the stream has been cancelled.
227
+ */
228
+ get isCancelled(): boolean {
229
+ return this.abortController.signal.aborted;
230
+ }
231
+
232
+ // ============================================================================
233
+ // Private Helpers
234
+ // ============================================================================
235
+
236
+ private startInference(): void {
237
+ if (this.state.status !== 'idle') {
238
+ return; // Already started
239
+ }
240
+
241
+ this.state = { status: 'streaming' };
242
+
243
+ // Run the inference loop in the background
244
+ this.runInference(this)
245
+ .then(() => {
246
+ this.markDone();
247
+ })
248
+ .catch((error) => {
249
+ this.emit({ type: 'error', error });
250
+ this.markDone();
251
+ });
252
+ }
253
+
254
+ private waitForEvent(): Promise<void> {
255
+ if (!this.eventWaiter) {
256
+ let resolve: () => void;
257
+ const promise = new Promise<void>((r) => {
258
+ resolve = r;
259
+ });
260
+ this.eventWaiter = { resolve: resolve!, promise };
261
+ }
262
+ return this.eventWaiter.promise;
263
+ }
264
+
265
+ private notifyEventWaiter(): void {
266
+ if (this.eventWaiter) {
267
+ this.eventWaiter.resolve();
268
+ this.eventWaiter = null;
269
+ }
270
+ }
271
+ }