@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
package/dist/membrane.js CHANGED
@@ -6,6 +6,7 @@
6
6
  import { DEFAULT_RETRY_CONFIG, MembraneError, classifyError, } from './types/index.js';
7
7
  import { parseToolCalls, formatToolResults, parseAccumulatedIntoBlocks, hasImageInToolResults, formatToolResultsForSplitTurn, } from './utils/tool-parser.js';
8
8
  import { AnthropicXmlFormatter } from './formatters/anthropic-xml.js';
9
+ import { YieldingStreamImpl } from './yielding-stream.js';
9
10
  // ============================================================================
10
11
  // Membrane Class
11
12
  // ============================================================================
@@ -147,6 +148,9 @@ export class Membrane {
147
148
  // Track executed tool calls and results
148
149
  const executedToolCalls = [];
149
150
  const executedToolResults = [];
151
+ // Track non-text content blocks from provider (e.g., generated_image from Gemini)
152
+ // These can't be handled by the text-based XML parser, so we capture and append them
153
+ const extraContentBlocks = [];
150
154
  // Transform initial request using the formatter
151
155
  let { providerRequest, prefillResult } = this.transformRequest(request, formatter);
152
156
  // Initialize parser with prefill content so it knows about any open tags
@@ -234,6 +238,19 @@ export class Membrane {
234
238
  streamResult.stopReason = 'stop_sequence';
235
239
  streamResult.stopSequence = detectedStopSequence;
236
240
  }
241
+ // Capture non-text content blocks from provider response (e.g., generated_image from Gemini)
242
+ // The XML parser only handles text — binary content blocks need to be preserved separately
243
+ if (Array.isArray(streamResult.content)) {
244
+ for (const block of streamResult.content) {
245
+ if (block.type === 'generated_image') {
246
+ extraContentBlocks.push({
247
+ type: 'generated_image',
248
+ data: block.data,
249
+ mimeType: block.mimeType,
250
+ });
251
+ }
252
+ }
253
+ }
237
254
  rawResponse = streamResult.raw;
238
255
  // Call onResponse callback with raw response from API
239
256
  onResponse?.(rawResponse);
@@ -440,8 +457,13 @@ export class Membrane {
440
457
  // The full accumulated text is still available in raw.response
441
458
  const fullAccumulated = parser.getAccumulated();
442
459
  const newContent = fullAccumulated.slice(initialPrefillLength);
443
- return this.buildFinalResponse(newContent, contentBlocks, lastStopReason, totalUsage, request, prefillResult, startTime, 1, // attempts
460
+ const response = this.buildFinalResponse(newContent, contentBlocks, lastStopReason, totalUsage, request, prefillResult, startTime, 1, // attempts
444
461
  rawRequest, rawResponse, executedToolCalls, executedToolResults, initialBlockType);
462
+ // Append non-text content blocks (e.g., generated_image) that the XML parser can't handle
463
+ if (extraContentBlocks.length > 0) {
464
+ response.content.push(...extraContentBlocks);
465
+ }
466
+ return response;
445
467
  }
446
468
  catch (error) {
447
469
  // Check if this is an abort error
@@ -732,6 +754,13 @@ export class Membrane {
732
754
  signature: item.signature,
733
755
  });
734
756
  }
757
+ else if (item.type === 'generated_image') {
758
+ blocks.push({
759
+ type: 'generated_image',
760
+ data: item.data,
761
+ mimeType: item.mimeType,
762
+ });
763
+ }
735
764
  }
736
765
  return blocks;
737
766
  }
@@ -918,6 +947,13 @@ export class Membrane {
918
947
  signature: block.signature,
919
948
  });
920
949
  }
950
+ else if (block.type === 'generated_image') {
951
+ content.push({
952
+ type: 'generated_image',
953
+ data: block.data,
954
+ mimeType: block.mimeType,
955
+ });
956
+ }
921
957
  }
922
958
  }
923
959
  else if (typeof providerResponse.content === 'string') {
@@ -1126,5 +1162,558 @@ export class Membrane {
1126
1162
  toolResults: toolResults.length > 0 ? toolResults : undefined,
1127
1163
  };
1128
1164
  }
1165
+ // ============================================================================
1166
+ // Yielding Stream API
1167
+ // ============================================================================
1168
+ /**
1169
+ * Stream inference with yielding control for tool execution.
1170
+ *
1171
+ * Unlike `stream()` which uses callbacks for tool execution, this method
1172
+ * returns an async iterator that yields control back to the caller when
1173
+ * tool calls are detected. The caller provides results via `provideToolResults()`.
1174
+ *
1175
+ * @example
1176
+ * ```typescript
1177
+ * const stream = membrane.streamYielding(request, options);
1178
+ *
1179
+ * for await (const event of stream) {
1180
+ * switch (event.type) {
1181
+ * case 'tokens':
1182
+ * process.stdout.write(event.content);
1183
+ * break;
1184
+ * case 'tool-calls':
1185
+ * const results = await executeTools(event.calls);
1186
+ * stream.provideToolResults(results);
1187
+ * break;
1188
+ * case 'complete':
1189
+ * console.log('Done:', event.response);
1190
+ * break;
1191
+ * }
1192
+ * }
1193
+ * ```
1194
+ */
1195
+ streamYielding(request, options = {}) {
1196
+ const toolMode = this.resolveToolMode(request);
1197
+ // Create the yielding stream with the appropriate inference runner
1198
+ const runInference = toolMode === 'native'
1199
+ ? (stream) => this.runNativeToolsYielding(request, options, stream)
1200
+ : (stream) => this.runXmlToolsYielding(request, options, stream);
1201
+ return new YieldingStreamImpl(options, runInference);
1202
+ }
1203
+ /**
1204
+ * Run XML-based tool execution with yielding stream.
1205
+ */
1206
+ async runXmlToolsYielding(request, options, stream) {
1207
+ const startTime = Date.now();
1208
+ const { maxToolDepth = 10, emitTokens = true, emitBlocks = true, emitUsage = true, } = options;
1209
+ // Initialize parser from formatter for format-specific tracking
1210
+ const formatter = this.formatter;
1211
+ const parser = formatter.createStreamParser();
1212
+ let toolDepth = 0;
1213
+ let totalUsage = { inputTokens: 0, outputTokens: 0 };
1214
+ const contentBlocks = [];
1215
+ let lastStopReason = 'end_turn';
1216
+ let rawRequest;
1217
+ let rawResponse;
1218
+ // Track executed tool calls and results
1219
+ const executedToolCalls = [];
1220
+ const executedToolResults = [];
1221
+ // Transform initial request using the formatter
1222
+ let { providerRequest, prefillResult } = this.transformRequest(request, formatter);
1223
+ // Initialize parser with prefill content
1224
+ let initialPrefillLength = 0;
1225
+ let initialBlockType = null;
1226
+ if (prefillResult.assistantPrefill) {
1227
+ parser.push(prefillResult.assistantPrefill);
1228
+ initialPrefillLength = prefillResult.assistantPrefill.length;
1229
+ if (parser.isInsideBlock()) {
1230
+ const blockType = parser.getCurrentBlockType();
1231
+ if (blockType === 'thinking' || blockType === 'tool_call' || blockType === 'tool_result') {
1232
+ initialBlockType = blockType;
1233
+ }
1234
+ }
1235
+ }
1236
+ try {
1237
+ // Tool execution loop
1238
+ while (toolDepth <= maxToolDepth) {
1239
+ // Check for cancellation
1240
+ if (stream.isCancelled) {
1241
+ const fullAccumulated = parser.getAccumulated();
1242
+ const newContent = fullAccumulated.slice(initialPrefillLength);
1243
+ stream.emit({
1244
+ type: 'aborted',
1245
+ reason: 'user',
1246
+ partialContent: parseAccumulatedIntoBlocks(newContent).blocks,
1247
+ rawAssistantText: newContent,
1248
+ toolCalls: executedToolCalls,
1249
+ toolResults: executedToolResults,
1250
+ });
1251
+ return;
1252
+ }
1253
+ // Track if we manually detected a stop sequence
1254
+ let detectedStopSequence = null;
1255
+ let truncatedAccumulated = null;
1256
+ const checkFromIndex = parser.getAccumulated().length;
1257
+ // Stream from provider
1258
+ const streamResult = await this.streamOnce(providerRequest, {
1259
+ onChunk: (chunk) => {
1260
+ if (detectedStopSequence || stream.isCancelled) {
1261
+ return;
1262
+ }
1263
+ // Process chunk with enriched streaming API
1264
+ const { emissions } = parser.processChunk(chunk);
1265
+ // Check for stop sequences only in NEW content
1266
+ const accumulated = parser.getAccumulated();
1267
+ const newContent = accumulated.slice(checkFromIndex);
1268
+ for (const stopSeq of prefillResult.stopSequences) {
1269
+ const idx = newContent.indexOf(stopSeq);
1270
+ if (idx !== -1) {
1271
+ const absoluteIdx = checkFromIndex + idx;
1272
+ detectedStopSequence = stopSeq;
1273
+ truncatedAccumulated = accumulated.slice(0, absoluteIdx);
1274
+ // Emit only the portion up to stop sequence
1275
+ const alreadyEmitted = accumulated.length - chunk.length;
1276
+ if (emitTokens && absoluteIdx > alreadyEmitted) {
1277
+ const truncatedChunk = accumulated.slice(alreadyEmitted, absoluteIdx);
1278
+ const meta = {
1279
+ type: parser.getCurrentBlockType(),
1280
+ visible: parser.getCurrentBlockType() === 'text',
1281
+ blockIndex: 0,
1282
+ };
1283
+ stream.emit({ type: 'tokens', content: truncatedChunk, meta });
1284
+ }
1285
+ return;
1286
+ }
1287
+ }
1288
+ // Emit in correct interleaved order
1289
+ for (const emission of emissions) {
1290
+ if (emission.kind === 'blockEvent') {
1291
+ if (emitBlocks) {
1292
+ stream.emit({ type: 'block', event: emission.event });
1293
+ }
1294
+ }
1295
+ else {
1296
+ if (emitTokens) {
1297
+ stream.emit({ type: 'tokens', content: emission.text, meta: emission.meta });
1298
+ }
1299
+ }
1300
+ }
1301
+ },
1302
+ onContentBlock: undefined,
1303
+ }, {
1304
+ signal: stream.signal,
1305
+ timeoutMs: options.timeoutMs,
1306
+ onRequest: (req) => { rawRequest = req; },
1307
+ });
1308
+ // If we detected stop sequence manually, fix up the parser and result
1309
+ if (detectedStopSequence && truncatedAccumulated !== null) {
1310
+ parser.reset();
1311
+ parser.push(truncatedAccumulated);
1312
+ streamResult.stopReason = 'stop_sequence';
1313
+ streamResult.stopSequence = detectedStopSequence;
1314
+ }
1315
+ rawResponse = streamResult.raw;
1316
+ lastStopReason = this.mapStopReason(streamResult.stopReason);
1317
+ // Accumulate usage
1318
+ totalUsage.inputTokens += streamResult.usage.inputTokens;
1319
+ totalUsage.outputTokens += streamResult.usage.outputTokens;
1320
+ if (emitUsage) {
1321
+ stream.emit({ type: 'usage', usage: { ...totalUsage } });
1322
+ }
1323
+ // Flush the parser
1324
+ const flushResult = parser.flush();
1325
+ for (const emission of flushResult.emissions) {
1326
+ if (emission.kind === 'blockEvent' && emitBlocks) {
1327
+ stream.emit({ type: 'block', event: emission.event });
1328
+ }
1329
+ }
1330
+ // Check for tool calls
1331
+ if (streamResult.stopSequence === '</function_calls>') {
1332
+ const closeTag = '</function_calls>';
1333
+ parser.push(closeTag);
1334
+ const parsed = parseToolCalls(parser.getAccumulated());
1335
+ if (parsed && parsed.calls.length > 0) {
1336
+ // Emit block events for each tool call
1337
+ if (emitBlocks) {
1338
+ for (const call of parsed.calls) {
1339
+ const toolCallBlockIndex = parser.getBlockIndex();
1340
+ stream.emit({
1341
+ type: 'block',
1342
+ event: {
1343
+ event: 'block_start',
1344
+ index: toolCallBlockIndex,
1345
+ block: { type: 'tool_call' },
1346
+ },
1347
+ });
1348
+ stream.emit({
1349
+ type: 'block',
1350
+ event: {
1351
+ event: 'block_complete',
1352
+ index: toolCallBlockIndex,
1353
+ block: {
1354
+ type: 'tool_call',
1355
+ toolId: call.id,
1356
+ toolName: call.name,
1357
+ input: call.input,
1358
+ },
1359
+ },
1360
+ });
1361
+ parser.incrementBlockIndex();
1362
+ }
1363
+ }
1364
+ // Track the tool calls
1365
+ executedToolCalls.push(...parsed.calls);
1366
+ // Build tool context
1367
+ const context = {
1368
+ rawText: parsed.fullMatch,
1369
+ preamble: parsed.beforeText,
1370
+ depth: toolDepth,
1371
+ previousResults: executedToolResults,
1372
+ accumulated: parser.getAccumulated(),
1373
+ };
1374
+ // Yield control for tool execution
1375
+ const toolCallsEvent = {
1376
+ type: 'tool-calls',
1377
+ calls: parsed.calls,
1378
+ context,
1379
+ };
1380
+ const results = await stream.requestToolExecution(toolCallsEvent);
1381
+ // Track the tool results
1382
+ executedToolResults.push(...results);
1383
+ // Check if results contain images
1384
+ if (hasImageInToolResults(results)) {
1385
+ const splitContent = formatToolResultsForSplitTurn(results);
1386
+ // Emit block events for tool results
1387
+ if (emitBlocks) {
1388
+ stream.emit({
1389
+ type: 'block',
1390
+ event: {
1391
+ event: 'block_start',
1392
+ index: parser.getBlockIndex(),
1393
+ block: { type: 'tool_result' },
1394
+ },
1395
+ });
1396
+ }
1397
+ parser.push(splitContent.beforeImageXml);
1398
+ // Emit tool result content
1399
+ for (const result of results) {
1400
+ const resultContent = typeof result.content === 'string'
1401
+ ? result.content
1402
+ : JSON.stringify(result.content);
1403
+ if (emitTokens) {
1404
+ const toolResultMeta = {
1405
+ type: 'tool_result',
1406
+ visible: false,
1407
+ blockIndex: parser.getBlockIndex(),
1408
+ toolId: result.toolUseId,
1409
+ };
1410
+ stream.emit({ type: 'tokens', content: resultContent, meta: toolResultMeta });
1411
+ }
1412
+ if (emitBlocks) {
1413
+ stream.emit({
1414
+ type: 'block',
1415
+ event: {
1416
+ event: 'block_complete',
1417
+ index: parser.getBlockIndex(),
1418
+ block: {
1419
+ type: 'tool_result',
1420
+ toolId: result.toolUseId,
1421
+ content: resultContent,
1422
+ isError: result.isError,
1423
+ },
1424
+ },
1425
+ });
1426
+ }
1427
+ parser.incrementBlockIndex();
1428
+ }
1429
+ let afterImageXml = splitContent.afterImageXml;
1430
+ if (request.config.thinking?.enabled) {
1431
+ afterImageXml += '\n<thinking>';
1432
+ }
1433
+ providerRequest = this.buildContinuationRequestWithImages(request, prefillResult, parser.getAccumulated(), splitContent.images, afterImageXml);
1434
+ parser.push(afterImageXml);
1435
+ prefillResult.assistantPrefill = parser.getAccumulated();
1436
+ parser.resetForNewIteration();
1437
+ }
1438
+ else {
1439
+ // Standard path: no images
1440
+ const resultsXml = formatToolResults(results);
1441
+ if (emitBlocks) {
1442
+ stream.emit({
1443
+ type: 'block',
1444
+ event: {
1445
+ event: 'block_start',
1446
+ index: parser.getBlockIndex(),
1447
+ block: { type: 'tool_result' },
1448
+ },
1449
+ });
1450
+ }
1451
+ parser.push(resultsXml);
1452
+ for (const result of results) {
1453
+ const resultContent = typeof result.content === 'string'
1454
+ ? result.content
1455
+ : JSON.stringify(result.content);
1456
+ if (emitTokens) {
1457
+ const toolResultMeta = {
1458
+ type: 'tool_result',
1459
+ visible: false,
1460
+ blockIndex: parser.getBlockIndex(),
1461
+ toolId: result.toolUseId,
1462
+ };
1463
+ stream.emit({ type: 'tokens', content: resultContent, meta: toolResultMeta });
1464
+ }
1465
+ if (emitBlocks) {
1466
+ stream.emit({
1467
+ type: 'block',
1468
+ event: {
1469
+ event: 'block_complete',
1470
+ index: parser.getBlockIndex(),
1471
+ block: {
1472
+ type: 'tool_result',
1473
+ toolId: result.toolUseId,
1474
+ content: resultContent,
1475
+ isError: result.isError,
1476
+ },
1477
+ },
1478
+ });
1479
+ }
1480
+ parser.incrementBlockIndex();
1481
+ }
1482
+ if (request.config.thinking?.enabled) {
1483
+ parser.push('\n<thinking>');
1484
+ }
1485
+ prefillResult.assistantPrefill = parser.getAccumulated();
1486
+ providerRequest = this.buildContinuationRequest(request, prefillResult, parser.getAccumulated());
1487
+ }
1488
+ parser.resetForNewIteration();
1489
+ toolDepth++;
1490
+ continue;
1491
+ }
1492
+ }
1493
+ // Check for false-positive stop (unclosed block)
1494
+ if (lastStopReason === 'stop_sequence' && parser.isInsideBlock()) {
1495
+ if (streamResult.stopSequence) {
1496
+ parser.push(streamResult.stopSequence);
1497
+ if (emitTokens) {
1498
+ const meta = {
1499
+ type: parser.getCurrentBlockType(),
1500
+ visible: parser.getCurrentBlockType() === 'text',
1501
+ blockIndex: 0,
1502
+ };
1503
+ stream.emit({ type: 'tokens', content: streamResult.stopSequence, meta });
1504
+ }
1505
+ }
1506
+ toolDepth++;
1507
+ if (toolDepth > maxToolDepth) {
1508
+ break;
1509
+ }
1510
+ prefillResult.assistantPrefill = parser.getAccumulated();
1511
+ providerRequest = this.buildContinuationRequest(request, prefillResult, parser.getAccumulated());
1512
+ parser.resetForNewIteration();
1513
+ continue;
1514
+ }
1515
+ // No more tools, we're done
1516
+ break;
1517
+ }
1518
+ // Build final response
1519
+ const fullAccumulated = parser.getAccumulated();
1520
+ const newContent = fullAccumulated.slice(initialPrefillLength);
1521
+ const response = this.buildFinalResponse(newContent, contentBlocks, lastStopReason, totalUsage, request, prefillResult, startTime, 1, rawRequest, rawResponse, executedToolCalls, executedToolResults, initialBlockType);
1522
+ stream.emit({ type: 'complete', response });
1523
+ }
1524
+ catch (error) {
1525
+ if (this.isAbortError(error)) {
1526
+ const fullAccumulated = parser.getAccumulated();
1527
+ const newContent = fullAccumulated.slice(initialPrefillLength);
1528
+ stream.emit({
1529
+ type: 'aborted',
1530
+ reason: 'user',
1531
+ partialContent: parseAccumulatedIntoBlocks(newContent).blocks,
1532
+ rawAssistantText: newContent,
1533
+ toolCalls: executedToolCalls,
1534
+ toolResults: executedToolResults,
1535
+ });
1536
+ }
1537
+ else {
1538
+ throw error;
1539
+ }
1540
+ }
1541
+ }
1542
+ /**
1543
+ * Run native tool execution with yielding stream.
1544
+ */
1545
+ async runNativeToolsYielding(request, options, stream) {
1546
+ const startTime = Date.now();
1547
+ const { maxToolDepth = 10, emitTokens = true, emitUsage = true, } = options;
1548
+ let toolDepth = 0;
1549
+ let totalUsage = { inputTokens: 0, outputTokens: 0 };
1550
+ let lastStopReason = 'end_turn';
1551
+ let rawRequest;
1552
+ let rawResponse;
1553
+ let allTextAccumulated = '';
1554
+ const executedToolCalls = [];
1555
+ const executedToolResults = [];
1556
+ let messages = [...request.messages];
1557
+ let allContentBlocks = [];
1558
+ try {
1559
+ // Tool execution loop
1560
+ while (toolDepth <= maxToolDepth) {
1561
+ // Check for cancellation
1562
+ if (stream.isCancelled) {
1563
+ stream.emit({
1564
+ type: 'aborted',
1565
+ reason: 'user',
1566
+ rawAssistantText: allTextAccumulated,
1567
+ toolCalls: executedToolCalls,
1568
+ toolResults: executedToolResults,
1569
+ });
1570
+ return;
1571
+ }
1572
+ // Build provider request with native tools
1573
+ const providerRequest = this.buildNativeToolRequest(request, messages);
1574
+ // Stream from provider
1575
+ let textAccumulated = '';
1576
+ let blockIndex = 0;
1577
+ const streamResult = await this.streamOnce(providerRequest, {
1578
+ onChunk: (chunk) => {
1579
+ if (stream.isCancelled)
1580
+ return;
1581
+ textAccumulated += chunk;
1582
+ allTextAccumulated += chunk;
1583
+ if (emitTokens) {
1584
+ const meta = {
1585
+ type: 'text',
1586
+ visible: true,
1587
+ blockIndex,
1588
+ };
1589
+ stream.emit({ type: 'tokens', content: chunk, meta });
1590
+ }
1591
+ },
1592
+ onContentBlock: undefined,
1593
+ }, {
1594
+ signal: stream.signal,
1595
+ timeoutMs: options.timeoutMs,
1596
+ onRequest: (req) => { rawRequest = req; },
1597
+ });
1598
+ rawResponse = streamResult.raw;
1599
+ lastStopReason = this.mapStopReason(streamResult.stopReason);
1600
+ // Accumulate usage
1601
+ totalUsage.inputTokens += streamResult.usage.inputTokens;
1602
+ totalUsage.outputTokens += streamResult.usage.outputTokens;
1603
+ if (emitUsage) {
1604
+ stream.emit({ type: 'usage', usage: { ...totalUsage } });
1605
+ }
1606
+ // Parse content blocks from response
1607
+ const responseBlocks = this.parseProviderContent(streamResult.content);
1608
+ allContentBlocks.push(...responseBlocks);
1609
+ // Check for tool_use blocks
1610
+ const toolUseBlocks = responseBlocks.filter((b) => b.type === 'tool_use');
1611
+ if (toolUseBlocks.length > 0 && lastStopReason === 'tool_use') {
1612
+ // Convert to normalized ToolCall[]
1613
+ const toolCalls = toolUseBlocks.map(block => ({
1614
+ id: block.id,
1615
+ name: block.name,
1616
+ input: block.input,
1617
+ }));
1618
+ // Track tool calls
1619
+ executedToolCalls.push(...toolCalls);
1620
+ // Build tool context
1621
+ const context = {
1622
+ rawText: JSON.stringify(toolUseBlocks),
1623
+ preamble: textAccumulated,
1624
+ depth: toolDepth,
1625
+ previousResults: executedToolResults,
1626
+ accumulated: allTextAccumulated,
1627
+ };
1628
+ // Yield control for tool execution
1629
+ const toolCallsEvent = {
1630
+ type: 'tool-calls',
1631
+ calls: toolCalls,
1632
+ context,
1633
+ };
1634
+ const results = await stream.requestToolExecution(toolCallsEvent);
1635
+ // Track tool results
1636
+ executedToolResults.push(...results);
1637
+ // Add tool results to content blocks
1638
+ for (const result of results) {
1639
+ allContentBlocks.push({
1640
+ type: 'tool_result',
1641
+ toolUseId: result.toolUseId,
1642
+ content: result.content,
1643
+ isError: result.isError,
1644
+ });
1645
+ }
1646
+ // Add messages for next iteration
1647
+ messages.push({
1648
+ participant: 'Claude',
1649
+ content: responseBlocks,
1650
+ });
1651
+ messages.push({
1652
+ participant: 'User',
1653
+ content: results.map(r => ({
1654
+ type: 'tool_result',
1655
+ toolUseId: r.toolUseId,
1656
+ content: r.content,
1657
+ isError: r.isError,
1658
+ })),
1659
+ });
1660
+ toolDepth++;
1661
+ continue;
1662
+ }
1663
+ // No more tools, we're done
1664
+ break;
1665
+ }
1666
+ const durationMs = Date.now() - startTime;
1667
+ const response = {
1668
+ content: allContentBlocks,
1669
+ rawAssistantText: allTextAccumulated,
1670
+ toolCalls: executedToolCalls,
1671
+ toolResults: executedToolResults,
1672
+ stopReason: lastStopReason,
1673
+ usage: totalUsage,
1674
+ details: {
1675
+ stop: {
1676
+ reason: lastStopReason,
1677
+ wasTruncated: lastStopReason === 'max_tokens',
1678
+ },
1679
+ usage: { ...totalUsage },
1680
+ timing: {
1681
+ totalDurationMs: durationMs,
1682
+ attempts: 1,
1683
+ },
1684
+ model: {
1685
+ requested: request.config.model,
1686
+ actual: request.config.model,
1687
+ provider: this.adapter.name,
1688
+ },
1689
+ cache: {
1690
+ markersInRequest: 0,
1691
+ tokensCreated: 0,
1692
+ tokensRead: 0,
1693
+ hitRatio: 0,
1694
+ },
1695
+ },
1696
+ raw: {
1697
+ request: rawRequest,
1698
+ response: rawResponse,
1699
+ },
1700
+ };
1701
+ stream.emit({ type: 'complete', response });
1702
+ }
1703
+ catch (error) {
1704
+ if (this.isAbortError(error)) {
1705
+ stream.emit({
1706
+ type: 'aborted',
1707
+ reason: 'user',
1708
+ rawAssistantText: allTextAccumulated,
1709
+ toolCalls: executedToolCalls,
1710
+ toolResults: executedToolResults,
1711
+ });
1712
+ }
1713
+ else {
1714
+ throw error;
1715
+ }
1716
+ }
1717
+ }
1129
1718
  }
1130
1719
  //# sourceMappingURL=membrane.js.map