@animalabs/membrane 0.1.0

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 (131) hide show
  1. package/dist/context/index.d.ts +10 -0
  2. package/dist/context/index.d.ts.map +1 -0
  3. package/dist/context/index.js +9 -0
  4. package/dist/context/index.js.map +1 -0
  5. package/dist/context/process.d.ts +22 -0
  6. package/dist/context/process.d.ts.map +1 -0
  7. package/dist/context/process.js +369 -0
  8. package/dist/context/process.js.map +1 -0
  9. package/dist/context/types.d.ts +118 -0
  10. package/dist/context/types.d.ts.map +1 -0
  11. package/dist/context/types.js +60 -0
  12. package/dist/context/types.js.map +1 -0
  13. package/dist/index.d.ts +12 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +18 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/membrane.d.ts +96 -0
  18. package/dist/membrane.d.ts.map +1 -0
  19. package/dist/membrane.js +893 -0
  20. package/dist/membrane.js.map +1 -0
  21. package/dist/providers/anthropic.d.ts +36 -0
  22. package/dist/providers/anthropic.d.ts.map +1 -0
  23. package/dist/providers/anthropic.js +265 -0
  24. package/dist/providers/anthropic.js.map +1 -0
  25. package/dist/providers/index.d.ts +8 -0
  26. package/dist/providers/index.d.ts.map +1 -0
  27. package/dist/providers/index.js +8 -0
  28. package/dist/providers/index.js.map +1 -0
  29. package/dist/providers/openai-compatible.d.ts +74 -0
  30. package/dist/providers/openai-compatible.d.ts.map +1 -0
  31. package/dist/providers/openai-compatible.js +412 -0
  32. package/dist/providers/openai-compatible.js.map +1 -0
  33. package/dist/providers/openai.d.ts +69 -0
  34. package/dist/providers/openai.d.ts.map +1 -0
  35. package/dist/providers/openai.js +455 -0
  36. package/dist/providers/openai.js.map +1 -0
  37. package/dist/providers/openrouter.d.ts +76 -0
  38. package/dist/providers/openrouter.d.ts.map +1 -0
  39. package/dist/providers/openrouter.js +492 -0
  40. package/dist/providers/openrouter.js.map +1 -0
  41. package/dist/transforms/chat.d.ts +52 -0
  42. package/dist/transforms/chat.d.ts.map +1 -0
  43. package/dist/transforms/chat.js +136 -0
  44. package/dist/transforms/chat.js.map +1 -0
  45. package/dist/transforms/index.d.ts +6 -0
  46. package/dist/transforms/index.d.ts.map +1 -0
  47. package/dist/transforms/index.js +6 -0
  48. package/dist/transforms/index.js.map +1 -0
  49. package/dist/transforms/prefill.d.ts +89 -0
  50. package/dist/transforms/prefill.d.ts.map +1 -0
  51. package/dist/transforms/prefill.js +401 -0
  52. package/dist/transforms/prefill.js.map +1 -0
  53. package/dist/types/config.d.ts +103 -0
  54. package/dist/types/config.d.ts.map +1 -0
  55. package/dist/types/config.js +21 -0
  56. package/dist/types/config.js.map +1 -0
  57. package/dist/types/content.d.ts +81 -0
  58. package/dist/types/content.d.ts.map +1 -0
  59. package/dist/types/content.js +40 -0
  60. package/dist/types/content.js.map +1 -0
  61. package/dist/types/errors.d.ts +42 -0
  62. package/dist/types/errors.d.ts.map +1 -0
  63. package/dist/types/errors.js +208 -0
  64. package/dist/types/errors.js.map +1 -0
  65. package/dist/types/index.d.ts +18 -0
  66. package/dist/types/index.d.ts.map +1 -0
  67. package/dist/types/index.js +9 -0
  68. package/dist/types/index.js.map +1 -0
  69. package/dist/types/message.d.ts +46 -0
  70. package/dist/types/message.d.ts.map +1 -0
  71. package/dist/types/message.js +38 -0
  72. package/dist/types/message.js.map +1 -0
  73. package/dist/types/provider.d.ts +155 -0
  74. package/dist/types/provider.d.ts.map +1 -0
  75. package/dist/types/provider.js +5 -0
  76. package/dist/types/provider.js.map +1 -0
  77. package/dist/types/request.d.ts +78 -0
  78. package/dist/types/request.d.ts.map +1 -0
  79. package/dist/types/request.js +5 -0
  80. package/dist/types/request.js.map +1 -0
  81. package/dist/types/response.d.ts +131 -0
  82. package/dist/types/response.d.ts.map +1 -0
  83. package/dist/types/response.js +7 -0
  84. package/dist/types/response.js.map +1 -0
  85. package/dist/types/streaming.d.ts +164 -0
  86. package/dist/types/streaming.d.ts.map +1 -0
  87. package/dist/types/streaming.js +5 -0
  88. package/dist/types/streaming.js.map +1 -0
  89. package/dist/types/tools.d.ts +71 -0
  90. package/dist/types/tools.d.ts.map +1 -0
  91. package/dist/types/tools.js +5 -0
  92. package/dist/types/tools.js.map +1 -0
  93. package/dist/utils/index.d.ts +5 -0
  94. package/dist/utils/index.d.ts.map +1 -0
  95. package/dist/utils/index.js +5 -0
  96. package/dist/utils/index.js.map +1 -0
  97. package/dist/utils/stream-parser.d.ts +53 -0
  98. package/dist/utils/stream-parser.d.ts.map +1 -0
  99. package/dist/utils/stream-parser.js +359 -0
  100. package/dist/utils/stream-parser.js.map +1 -0
  101. package/dist/utils/tool-parser.d.ts +130 -0
  102. package/dist/utils/tool-parser.d.ts.map +1 -0
  103. package/dist/utils/tool-parser.js +571 -0
  104. package/dist/utils/tool-parser.js.map +1 -0
  105. package/package.json +37 -0
  106. package/src/context/index.ts +24 -0
  107. package/src/context/process.ts +520 -0
  108. package/src/context/types.ts +231 -0
  109. package/src/index.ts +23 -0
  110. package/src/membrane.ts +1174 -0
  111. package/src/providers/anthropic.ts +340 -0
  112. package/src/providers/index.ts +31 -0
  113. package/src/providers/openai-compatible.ts +570 -0
  114. package/src/providers/openai.ts +625 -0
  115. package/src/providers/openrouter.ts +662 -0
  116. package/src/transforms/chat.ts +212 -0
  117. package/src/transforms/index.ts +22 -0
  118. package/src/transforms/prefill.ts +585 -0
  119. package/src/types/config.ts +172 -0
  120. package/src/types/content.ts +181 -0
  121. package/src/types/errors.ts +277 -0
  122. package/src/types/index.ts +154 -0
  123. package/src/types/message.ts +89 -0
  124. package/src/types/provider.ts +249 -0
  125. package/src/types/request.ts +131 -0
  126. package/src/types/response.ts +223 -0
  127. package/src/types/streaming.ts +231 -0
  128. package/src/types/tools.ts +92 -0
  129. package/src/utils/index.ts +15 -0
  130. package/src/utils/stream-parser.ts +440 -0
  131. package/src/utils/tool-parser.ts +715 -0
@@ -0,0 +1,440 @@
1
+ /**
2
+ * Incremental XML parser for streaming
3
+ *
4
+ * Tracks nesting depth of XML blocks as tokens arrive, enabling:
5
+ * - False-positive stop sequence detection
6
+ * - Structured block events for UI
7
+ * - Enriched chunk metadata for TTS/display filtering
8
+ */
9
+
10
+ import type {
11
+ BlockEvent,
12
+ ChunkMeta,
13
+ MembraneBlock,
14
+ MembraneBlockType,
15
+ } from '../types/streaming.js';
16
+
17
+ // ============================================================================
18
+ // Result Types
19
+ // ============================================================================
20
+
21
+ export interface ProcessChunkResult {
22
+ content: Array<{ text: string; meta: ChunkMeta }>;
23
+ blockEvents: BlockEvent[];
24
+ }
25
+
26
+ // ============================================================================
27
+ // Parser State
28
+ // ============================================================================
29
+
30
+ interface ParserState {
31
+ functionCallsDepth: number;
32
+ functionResultsDepth: number;
33
+ thinkingDepth: number;
34
+ accumulated: string;
35
+ lastScanPos: number;
36
+ blockIndex: number;
37
+ currentBlockStarted: boolean;
38
+ currentBlockContent: string;
39
+ currentBlockType: MembraneBlockType;
40
+ tagBuffer: string;
41
+ toolCallState: {
42
+ inInvoke: boolean;
43
+ currentToolName: string;
44
+ currentToolId: string;
45
+ inParameter: boolean;
46
+ currentParamName: string;
47
+ paramContent: string;
48
+ allParams: Record<string, string>;
49
+ };
50
+ }
51
+
52
+ function createInitialState(): ParserState {
53
+ return {
54
+ functionCallsDepth: 0,
55
+ functionResultsDepth: 0,
56
+ thinkingDepth: 0,
57
+ accumulated: '',
58
+ lastScanPos: 0,
59
+ blockIndex: 0,
60
+ currentBlockStarted: false,
61
+ currentBlockContent: '',
62
+ currentBlockType: 'text',
63
+ tagBuffer: '',
64
+ toolCallState: {
65
+ inInvoke: false,
66
+ currentToolName: '',
67
+ currentToolId: '',
68
+ inParameter: false,
69
+ currentParamName: '',
70
+ paramContent: '',
71
+ allParams: {},
72
+ },
73
+ };
74
+ }
75
+
76
+ // For finding all membrane tags in one pass
77
+ const ALL_TAGS = /<\/?(?:antml:)?(?:function_calls|function_results|thinking)>/g;
78
+
79
+ // For matching complete membrane tags
80
+ const COMPLETE_MEMBRANE_TAG = /^<\/?(?:antml:)?(?:function_calls|function_results|thinking|invoke|parameter)(?:\s[^>]*)?>$/;
81
+
82
+ // Known membrane tag prefixes
83
+ const MEMBRANE_TAG_PREFIXES = [
84
+ '<thinking', '<\/thinking>',
85
+ '<function_calls', '<\/function_calls>',
86
+ '<function_results', '<\/function_results>',
87
+ '<invoke', '<\/invoke>',
88
+ '<parameter', '<\/parameter>',
89
+ '<function_calls', '<\/antml:function_calls>',
90
+ '<invoke', '<\/antml:invoke>',
91
+ ];
92
+
93
+ // ============================================================================
94
+ // Incremental XML Parser
95
+ // ============================================================================
96
+
97
+ export class IncrementalXmlParser {
98
+ private state: ParserState;
99
+
100
+ constructor() {
101
+ this.state = createInitialState();
102
+ }
103
+
104
+ push(chunk: string): BlockEvent[] {
105
+ this.state.accumulated += chunk;
106
+ return this.scan();
107
+ }
108
+
109
+ isInsideBlock(): boolean {
110
+ return (
111
+ this.state.functionCallsDepth > 0 ||
112
+ this.state.functionResultsDepth > 0 ||
113
+ this.state.thinkingDepth > 0
114
+ );
115
+ }
116
+
117
+ isInsideFunctionResults(): boolean {
118
+ return this.state.functionResultsDepth > 0;
119
+ }
120
+
121
+ isInsideFunctionCalls(): boolean {
122
+ return this.state.functionCallsDepth > 0;
123
+ }
124
+
125
+ getContext(): string {
126
+ const parts: string[] = [];
127
+ if (this.state.functionCallsDepth > 0) {
128
+ parts.push('function_calls(' + this.state.functionCallsDepth + ')');
129
+ }
130
+ if (this.state.functionResultsDepth > 0) {
131
+ parts.push('function_results(' + this.state.functionResultsDepth + ')');
132
+ }
133
+ if (this.state.thinkingDepth > 0) {
134
+ parts.push('thinking(' + this.state.thinkingDepth + ')');
135
+ }
136
+ return parts.length > 0 ? parts.join(' > ') : 'none';
137
+ }
138
+
139
+ getAccumulated(): string {
140
+ return this.state.accumulated;
141
+ }
142
+
143
+ getDepths(): { functionCalls: number; functionResults: number; thinking: number } {
144
+ return {
145
+ functionCalls: this.state.functionCallsDepth,
146
+ functionResults: this.state.functionResultsDepth,
147
+ thinking: this.state.thinkingDepth,
148
+ };
149
+ }
150
+
151
+ reset(): void {
152
+ this.state = createInitialState();
153
+ }
154
+
155
+ finish(): BlockEvent[] {
156
+ return this.flush().blockEvents;
157
+ }
158
+
159
+ // ============================================================================
160
+ // Enriched Streaming API
161
+ // ============================================================================
162
+
163
+ processChunk(chunk: string): ProcessChunkResult {
164
+ const content: Array<{ text: string; meta: ChunkMeta }> = [];
165
+ const blockEvents: BlockEvent[] = [];
166
+
167
+ // Also update accumulated and scan for depth tracking
168
+ this.state.accumulated += chunk;
169
+ this.scanForDepth();
170
+
171
+ let pos = 0;
172
+ while (pos < chunk.length) {
173
+ if (this.state.tagBuffer) {
174
+ const char = chunk[pos];
175
+ this.state.tagBuffer += char;
176
+ pos++;
177
+
178
+ if (this.isCompleteMembraneTag(this.state.tagBuffer)) {
179
+ const events = this.handleMembraneTag(this.state.tagBuffer);
180
+ blockEvents.push(...events);
181
+ this.state.tagBuffer = '';
182
+ } else if (this.cantBeMembraneTag(this.state.tagBuffer)) {
183
+ this.ensureBlockStarted(blockEvents);
184
+ content.push({
185
+ text: this.state.tagBuffer,
186
+ meta: this.getCurrentMeta()
187
+ });
188
+ this.state.currentBlockContent += this.state.tagBuffer;
189
+ this.state.tagBuffer = '';
190
+ }
191
+ } else {
192
+ const nextLt = chunk.indexOf('<', pos);
193
+ if (nextLt === -1) {
194
+ const text = chunk.slice(pos);
195
+ if (text) {
196
+ this.ensureBlockStarted(blockEvents);
197
+ content.push({ text, meta: this.getCurrentMeta() });
198
+ this.state.currentBlockContent += text;
199
+ }
200
+ break;
201
+ } else {
202
+ if (nextLt > pos) {
203
+ const text = chunk.slice(pos, nextLt);
204
+ this.ensureBlockStarted(blockEvents);
205
+ content.push({ text, meta: this.getCurrentMeta() });
206
+ this.state.currentBlockContent += text;
207
+ }
208
+ this.state.tagBuffer = '<';
209
+ pos = nextLt + 1;
210
+ }
211
+ }
212
+ }
213
+
214
+ return { content, blockEvents };
215
+ }
216
+
217
+ flush(): ProcessChunkResult {
218
+ const content: Array<{ text: string; meta: ChunkMeta }> = [];
219
+ const blockEvents: BlockEvent[] = [];
220
+
221
+ if (this.state.tagBuffer) {
222
+ this.ensureBlockStarted(blockEvents);
223
+ content.push({ text: this.state.tagBuffer, meta: this.getCurrentMeta() });
224
+ this.state.currentBlockContent += this.state.tagBuffer;
225
+ this.state.tagBuffer = '';
226
+ }
227
+
228
+ if (this.state.currentBlockStarted) {
229
+ blockEvents.push(this.makeBlockComplete());
230
+ }
231
+
232
+ return { content, blockEvents };
233
+ }
234
+
235
+ getCurrentBlockType(): MembraneBlockType {
236
+ if (this.state.thinkingDepth > 0) return 'thinking';
237
+ if (this.state.functionCallsDepth > 0) return 'tool_call';
238
+ if (this.state.functionResultsDepth > 0) return 'tool_result';
239
+ return 'text';
240
+ }
241
+
242
+ // ============================================================================
243
+ // Private Methods
244
+ // ============================================================================
245
+
246
+ private isCompleteMembraneTag(buffer: string): boolean {
247
+ if (!buffer.endsWith('>')) return false;
248
+ return COMPLETE_MEMBRANE_TAG.test(buffer);
249
+ }
250
+
251
+ private cantBeMembraneTag(buffer: string): boolean {
252
+ if (buffer.endsWith('>')) {
253
+ return !this.isCompleteMembraneTag(buffer);
254
+ }
255
+ for (const prefix of MEMBRANE_TAG_PREFIXES) {
256
+ if (prefix.startsWith(buffer) || buffer.startsWith(prefix.slice(0, buffer.length))) {
257
+ return false;
258
+ }
259
+ }
260
+ return true;
261
+ }
262
+
263
+ private handleMembraneTag(tag: string): BlockEvent[] {
264
+ const events: BlockEvent[] = [];
265
+ const isClosing = tag.startsWith('</');
266
+
267
+ if (tag.includes('thinking')) {
268
+ if (!isClosing) {
269
+ if (this.state.currentBlockStarted) {
270
+ events.push(this.makeBlockComplete());
271
+ }
272
+ this.state.thinkingDepth++;
273
+ this.state.currentBlockType = 'thinking';
274
+ events.push(this.makeBlockStart('thinking'));
275
+ } else {
276
+ events.push(this.makeBlockComplete());
277
+ this.state.thinkingDepth--;
278
+ this.state.currentBlockType = this.getCurrentBlockType();
279
+ }
280
+ } else if (tag.includes('function_calls')) {
281
+ if (!isClosing) {
282
+ if (this.state.currentBlockStarted) {
283
+ events.push(this.makeBlockComplete());
284
+ }
285
+ this.state.functionCallsDepth++;
286
+ this.state.currentBlockType = 'tool_call';
287
+ events.push(this.makeBlockStart('tool_call'));
288
+ } else {
289
+ events.push(this.makeBlockComplete());
290
+ this.state.functionCallsDepth--;
291
+ this.state.currentBlockType = this.getCurrentBlockType();
292
+ }
293
+ } else if (tag.includes('function_results')) {
294
+ if (!isClosing) {
295
+ if (this.state.currentBlockStarted) {
296
+ events.push(this.makeBlockComplete());
297
+ }
298
+ this.state.functionResultsDepth++;
299
+ this.state.currentBlockType = 'tool_result';
300
+ events.push(this.makeBlockStart('tool_result'));
301
+ } else {
302
+ events.push(this.makeBlockComplete());
303
+ this.state.functionResultsDepth--;
304
+ this.state.currentBlockType = this.getCurrentBlockType();
305
+ }
306
+ }
307
+
308
+ return events;
309
+ }
310
+
311
+ private ensureBlockStarted(events: BlockEvent[]): void {
312
+ if (!this.state.currentBlockStarted) {
313
+ events.push(this.makeBlockStart(this.state.currentBlockType));
314
+ }
315
+ }
316
+
317
+ private makeBlockStart(type: MembraneBlockType): BlockEvent {
318
+ this.state.currentBlockStarted = true;
319
+ this.state.currentBlockContent = '';
320
+ this.state.currentBlockType = type;
321
+ return {
322
+ event: 'block_start',
323
+ index: this.state.blockIndex,
324
+ block: { type }
325
+ };
326
+ }
327
+
328
+ private makeBlockComplete(): BlockEvent {
329
+ const block: MembraneBlock = {
330
+ type: this.state.currentBlockType,
331
+ content: this.state.currentBlockContent,
332
+ };
333
+
334
+ const event: BlockEvent = {
335
+ event: 'block_complete',
336
+ index: this.state.blockIndex,
337
+ block
338
+ };
339
+
340
+ this.state.blockIndex++;
341
+ this.state.currentBlockStarted = false;
342
+ this.state.currentBlockContent = '';
343
+
344
+ return event;
345
+ }
346
+
347
+ private getCurrentMeta(): ChunkMeta {
348
+ const type = this.getCurrentBlockType();
349
+ return {
350
+ type,
351
+ visible: type === 'text',
352
+ blockIndex: this.state.blockIndex,
353
+ depth: Math.max(
354
+ this.state.functionCallsDepth,
355
+ this.state.functionResultsDepth
356
+ ),
357
+ };
358
+ }
359
+
360
+ private scanForDepth(): void {
361
+ const text = this.state.accumulated;
362
+ const lookbackChars = 30;
363
+ const scanStart = Math.max(0, this.state.lastScanPos - lookbackChars);
364
+ const textToScan = text.slice(scanStart);
365
+
366
+ ALL_TAGS.lastIndex = 0;
367
+ let match: RegExpExecArray | null;
368
+
369
+ while ((match = ALL_TAGS.exec(textToScan)) !== null) {
370
+ const absolutePos = scanStart + match.index;
371
+ if (absolutePos < this.state.lastScanPos) continue;
372
+
373
+ const tag = match[0];
374
+ const isClosing = tag.startsWith('</');
375
+
376
+ if (tag.includes('function_calls')) {
377
+ if (isClosing) {
378
+ this.state.functionCallsDepth = Math.max(0, this.state.functionCallsDepth - 1);
379
+ } else {
380
+ this.state.functionCallsDepth++;
381
+ }
382
+ } else if (tag.includes('function_results')) {
383
+ if (isClosing) {
384
+ this.state.functionResultsDepth = Math.max(0, this.state.functionResultsDepth - 1);
385
+ } else {
386
+ this.state.functionResultsDepth++;
387
+ }
388
+ } else if (tag.includes('thinking')) {
389
+ if (isClosing) {
390
+ this.state.thinkingDepth = Math.max(0, this.state.thinkingDepth - 1);
391
+ } else {
392
+ this.state.thinkingDepth++;
393
+ }
394
+ }
395
+ }
396
+
397
+ const partialTagLen = this.findPartialTagAtEnd(text);
398
+ this.state.lastScanPos = text.length - partialTagLen;
399
+ }
400
+
401
+ private scan(): BlockEvent[] {
402
+ this.scanForDepth();
403
+ return [];
404
+ }
405
+
406
+ private findPartialTagAtEnd(text: string): number {
407
+ const tail = text.slice(-30);
408
+ const lastLt = tail.lastIndexOf('<');
409
+ if (lastLt === -1) return 0;
410
+ const afterLt = tail.slice(lastLt);
411
+ if (afterLt.includes('>')) return 0;
412
+ return afterLt.length;
413
+ }
414
+ }
415
+
416
+ // ============================================================================
417
+ // Utility Functions
418
+ // ============================================================================
419
+
420
+ export function hasUnclosedXmlBlock(text: string): boolean {
421
+ const parser = new IncrementalXmlParser();
422
+ parser.push(text);
423
+ return parser.isInsideBlock();
424
+ }
425
+
426
+ export function countTags(
427
+ text: string,
428
+ openPattern: RegExp,
429
+ closePattern: RegExp
430
+ ): { open: number; close: number; depth: number } {
431
+ openPattern.lastIndex = 0;
432
+ closePattern.lastIndex = 0;
433
+ const openMatches = text.match(openPattern) || [];
434
+ const closeMatches = text.match(closePattern) || [];
435
+ return {
436
+ open: openMatches.length,
437
+ close: closeMatches.length,
438
+ depth: openMatches.length - closeMatches.length,
439
+ };
440
+ }