@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/src/membrane.ts CHANGED
@@ -44,8 +44,15 @@ import {
44
44
  } from './utils/tool-parser.js';
45
45
  import { IncrementalXmlParser, type ProcessChunkResult } from './utils/stream-parser.js';
46
46
  import type { ChunkMeta, BlockEvent } from './types/streaming.js';
47
+ import type {
48
+ YieldingStream,
49
+ YieldingStreamOptions,
50
+ StreamEvent,
51
+ ToolCallsEvent,
52
+ } from './types/yielding-stream.js';
47
53
  import type { PrefillFormatter, StreamParser } from './formatters/types.js';
48
54
  import { AnthropicXmlFormatter } from './formatters/anthropic-xml.js';
55
+ import { YieldingStreamImpl } from './yielding-stream.js';
49
56
 
50
57
  // ============================================================================
51
58
  // Membrane Class
@@ -243,6 +250,10 @@ export class Membrane {
243
250
  const executedToolCalls: ToolCall[] = [];
244
251
  const executedToolResults: ToolResult[] = [];
245
252
 
253
+ // Track non-text content blocks from provider (e.g., generated_image from Gemini)
254
+ // These can't be handled by the text-based XML parser, so we capture and append them
255
+ const extraContentBlocks: ContentBlock[] = [];
256
+
246
257
  // Transform initial request using the formatter
247
258
  let { providerRequest, prefillResult } = this.transformRequest(request, formatter);
248
259
 
@@ -345,6 +356,20 @@ export class Membrane {
345
356
  streamResult.stopSequence = detectedStopSequence;
346
357
  }
347
358
 
359
+ // Capture non-text content blocks from provider response (e.g., generated_image from Gemini)
360
+ // The XML parser only handles text — binary content blocks need to be preserved separately
361
+ if (Array.isArray(streamResult.content)) {
362
+ for (const block of streamResult.content) {
363
+ if (block.type === 'generated_image') {
364
+ extraContentBlocks.push({
365
+ type: 'generated_image',
366
+ data: (block as any).data,
367
+ mimeType: (block as any).mimeType,
368
+ } as ContentBlock);
369
+ }
370
+ }
371
+ }
372
+
348
373
  rawResponse = streamResult.raw;
349
374
 
350
375
  // Call onResponse callback with raw response from API
@@ -598,7 +623,7 @@ export class Membrane {
598
623
  const fullAccumulated = parser.getAccumulated();
599
624
  const newContent = fullAccumulated.slice(initialPrefillLength);
600
625
 
601
- return this.buildFinalResponse(
626
+ const response = this.buildFinalResponse(
602
627
  newContent,
603
628
  contentBlocks,
604
629
  lastStopReason,
@@ -613,6 +638,13 @@ export class Membrane {
613
638
  executedToolResults,
614
639
  initialBlockType
615
640
  );
641
+
642
+ // Append non-text content blocks (e.g., generated_image) that the XML parser can't handle
643
+ if (extraContentBlocks.length > 0) {
644
+ response.content.push(...extraContentBlocks);
645
+ }
646
+
647
+ return response;
616
648
  } catch (error) {
617
649
  // Check if this is an abort error
618
650
  if (this.isAbortError(error)) {
@@ -968,6 +1000,12 @@ export class Membrane {
968
1000
  thinking: item.thinking,
969
1001
  signature: item.signature,
970
1002
  });
1003
+ } else if (item.type === 'generated_image') {
1004
+ blocks.push({
1005
+ type: 'generated_image',
1006
+ data: item.data,
1007
+ mimeType: item.mimeType,
1008
+ });
971
1009
  }
972
1010
  }
973
1011
  return blocks;
@@ -1043,7 +1081,7 @@ export class Membrane {
1043
1081
  private async streamOnce(
1044
1082
  request: any,
1045
1083
  callbacks: { onChunk: (chunk: string) => void; onContentBlock?: (index: number, block: unknown) => void },
1046
- options: { signal?: AbortSignal; onRequest?: (rawRequest: unknown) => void }
1084
+ options: { signal?: AbortSignal; timeoutMs?: number; onRequest?: (rawRequest: unknown) => void }
1047
1085
  ) {
1048
1086
  return await this.adapter.stream(request, callbacks, options);
1049
1087
  }
@@ -1203,6 +1241,12 @@ export class Membrane {
1203
1241
  thinking: block.thinking,
1204
1242
  signature: block.signature,
1205
1243
  });
1244
+ } else if (block.type === 'generated_image') {
1245
+ content.push({
1246
+ type: 'generated_image',
1247
+ data: block.data,
1248
+ mimeType: block.mimeType,
1249
+ });
1206
1250
  }
1207
1251
  }
1208
1252
  } else if (typeof providerResponse.content === 'string') {
@@ -1446,4 +1490,687 @@ export class Membrane {
1446
1490
  toolResults: toolResults.length > 0 ? toolResults : undefined,
1447
1491
  };
1448
1492
  }
1493
+
1494
+ // ============================================================================
1495
+ // Yielding Stream API
1496
+ // ============================================================================
1497
+
1498
+ /**
1499
+ * Stream inference with yielding control for tool execution.
1500
+ *
1501
+ * Unlike `stream()` which uses callbacks for tool execution, this method
1502
+ * returns an async iterator that yields control back to the caller when
1503
+ * tool calls are detected. The caller provides results via `provideToolResults()`.
1504
+ *
1505
+ * @example
1506
+ * ```typescript
1507
+ * const stream = membrane.streamYielding(request, options);
1508
+ *
1509
+ * for await (const event of stream) {
1510
+ * switch (event.type) {
1511
+ * case 'tokens':
1512
+ * process.stdout.write(event.content);
1513
+ * break;
1514
+ * case 'tool-calls':
1515
+ * const results = await executeTools(event.calls);
1516
+ * stream.provideToolResults(results);
1517
+ * break;
1518
+ * case 'complete':
1519
+ * console.log('Done:', event.response);
1520
+ * break;
1521
+ * }
1522
+ * }
1523
+ * ```
1524
+ */
1525
+ streamYielding(
1526
+ request: NormalizedRequest,
1527
+ options: YieldingStreamOptions = {}
1528
+ ): YieldingStream {
1529
+ const toolMode = this.resolveToolMode(request);
1530
+
1531
+ // Create the yielding stream with the appropriate inference runner
1532
+ const runInference = toolMode === 'native'
1533
+ ? (stream: YieldingStreamImpl) => this.runNativeToolsYielding(request, options, stream)
1534
+ : (stream: YieldingStreamImpl) => this.runXmlToolsYielding(request, options, stream);
1535
+
1536
+ return new YieldingStreamImpl(options, runInference);
1537
+ }
1538
+
1539
+ /**
1540
+ * Run XML-based tool execution with yielding stream.
1541
+ */
1542
+ private async runXmlToolsYielding(
1543
+ request: NormalizedRequest,
1544
+ options: YieldingStreamOptions,
1545
+ stream: YieldingStreamImpl
1546
+ ): Promise<void> {
1547
+ const startTime = Date.now();
1548
+ const {
1549
+ maxToolDepth = 10,
1550
+ emitTokens = true,
1551
+ emitBlocks = true,
1552
+ emitUsage = true,
1553
+ } = options;
1554
+
1555
+ // Initialize parser from formatter for format-specific tracking
1556
+ const formatter = this.formatter;
1557
+ const parser = formatter.createStreamParser();
1558
+ let toolDepth = 0;
1559
+ let totalUsage: BasicUsage = { inputTokens: 0, outputTokens: 0 };
1560
+ const contentBlocks: ContentBlock[] = [];
1561
+ let lastStopReason: StopReason = 'end_turn';
1562
+ let rawRequest: unknown;
1563
+ let rawResponse: unknown;
1564
+
1565
+ // Track executed tool calls and results
1566
+ const executedToolCalls: ToolCall[] = [];
1567
+ const executedToolResults: ToolResult[] = [];
1568
+
1569
+ // Transform initial request using the formatter
1570
+ let { providerRequest, prefillResult } = this.transformRequest(request, formatter);
1571
+
1572
+ // Initialize parser with prefill content
1573
+ let initialPrefillLength = 0;
1574
+ let initialBlockType: 'thinking' | 'tool_call' | 'tool_result' | null = null;
1575
+ if (prefillResult.assistantPrefill) {
1576
+ parser.push(prefillResult.assistantPrefill);
1577
+ initialPrefillLength = prefillResult.assistantPrefill.length;
1578
+ if (parser.isInsideBlock()) {
1579
+ const blockType = parser.getCurrentBlockType();
1580
+ if (blockType === 'thinking' || blockType === 'tool_call' || blockType === 'tool_result') {
1581
+ initialBlockType = blockType;
1582
+ }
1583
+ }
1584
+ }
1585
+
1586
+ try {
1587
+ // Tool execution loop
1588
+ while (toolDepth <= maxToolDepth) {
1589
+ // Check for cancellation
1590
+ if (stream.isCancelled) {
1591
+ const fullAccumulated = parser.getAccumulated();
1592
+ const newContent = fullAccumulated.slice(initialPrefillLength);
1593
+ stream.emit({
1594
+ type: 'aborted',
1595
+ reason: 'user',
1596
+ partialContent: parseAccumulatedIntoBlocks(newContent).blocks,
1597
+ rawAssistantText: newContent,
1598
+ toolCalls: executedToolCalls,
1599
+ toolResults: executedToolResults,
1600
+ });
1601
+ return;
1602
+ }
1603
+
1604
+ // Track if we manually detected a stop sequence
1605
+ let detectedStopSequence: string | null = null;
1606
+ let truncatedAccumulated: string | null = null;
1607
+ const checkFromIndex = parser.getAccumulated().length;
1608
+
1609
+ // Stream from provider
1610
+ const streamResult = await this.streamOnce(
1611
+ providerRequest,
1612
+ {
1613
+ onChunk: (chunk) => {
1614
+ if (detectedStopSequence || stream.isCancelled) {
1615
+ return;
1616
+ }
1617
+
1618
+ // Process chunk with enriched streaming API
1619
+ const { emissions } = parser.processChunk(chunk);
1620
+
1621
+ // Check for stop sequences only in NEW content
1622
+ const accumulated = parser.getAccumulated();
1623
+ const newContent = accumulated.slice(checkFromIndex);
1624
+
1625
+ for (const stopSeq of prefillResult.stopSequences) {
1626
+ const idx = newContent.indexOf(stopSeq);
1627
+ if (idx !== -1) {
1628
+ const absoluteIdx = checkFromIndex + idx;
1629
+ detectedStopSequence = stopSeq;
1630
+ truncatedAccumulated = accumulated.slice(0, absoluteIdx);
1631
+
1632
+ // Emit only the portion up to stop sequence
1633
+ const alreadyEmitted = accumulated.length - chunk.length;
1634
+ if (emitTokens && absoluteIdx > alreadyEmitted) {
1635
+ const truncatedChunk = accumulated.slice(alreadyEmitted, absoluteIdx);
1636
+ const meta: ChunkMeta = {
1637
+ type: parser.getCurrentBlockType(),
1638
+ visible: parser.getCurrentBlockType() === 'text',
1639
+ blockIndex: 0,
1640
+ };
1641
+ stream.emit({ type: 'tokens', content: truncatedChunk, meta });
1642
+ }
1643
+ return;
1644
+ }
1645
+ }
1646
+
1647
+ // Emit in correct interleaved order
1648
+ for (const emission of emissions) {
1649
+ if (emission.kind === 'blockEvent') {
1650
+ if (emitBlocks) {
1651
+ stream.emit({ type: 'block', event: emission.event });
1652
+ }
1653
+ } else {
1654
+ if (emitTokens) {
1655
+ stream.emit({ type: 'tokens', content: emission.text, meta: emission.meta });
1656
+ }
1657
+ }
1658
+ }
1659
+ },
1660
+ onContentBlock: undefined,
1661
+ },
1662
+ {
1663
+ signal: stream.signal,
1664
+ timeoutMs: options.timeoutMs,
1665
+ onRequest: (req: unknown) => { rawRequest = req; },
1666
+ }
1667
+ );
1668
+
1669
+ // If we detected stop sequence manually, fix up the parser and result
1670
+ if (detectedStopSequence && truncatedAccumulated !== null) {
1671
+ parser.reset();
1672
+ parser.push(truncatedAccumulated);
1673
+ streamResult.stopReason = 'stop_sequence';
1674
+ streamResult.stopSequence = detectedStopSequence;
1675
+ }
1676
+
1677
+ rawResponse = streamResult.raw;
1678
+ lastStopReason = this.mapStopReason(streamResult.stopReason);
1679
+
1680
+ // Accumulate usage
1681
+ totalUsage.inputTokens += streamResult.usage.inputTokens;
1682
+ totalUsage.outputTokens += streamResult.usage.outputTokens;
1683
+ if (emitUsage) {
1684
+ stream.emit({ type: 'usage', usage: { ...totalUsage } });
1685
+ }
1686
+
1687
+ // Flush the parser
1688
+ const flushResult = parser.flush();
1689
+ for (const emission of flushResult.emissions) {
1690
+ if (emission.kind === 'blockEvent' && emitBlocks) {
1691
+ stream.emit({ type: 'block', event: emission.event });
1692
+ }
1693
+ }
1694
+
1695
+ // Check for tool calls
1696
+ if (streamResult.stopSequence === '</function_calls>') {
1697
+ const closeTag = '</function_calls>';
1698
+ parser.push(closeTag);
1699
+
1700
+ const parsed = parseToolCalls(parser.getAccumulated());
1701
+
1702
+ if (parsed && parsed.calls.length > 0) {
1703
+ // Emit block events for each tool call
1704
+ if (emitBlocks) {
1705
+ for (const call of parsed.calls) {
1706
+ const toolCallBlockIndex = parser.getBlockIndex();
1707
+ stream.emit({
1708
+ type: 'block',
1709
+ event: {
1710
+ event: 'block_start',
1711
+ index: toolCallBlockIndex,
1712
+ block: { type: 'tool_call' },
1713
+ },
1714
+ });
1715
+ stream.emit({
1716
+ type: 'block',
1717
+ event: {
1718
+ event: 'block_complete',
1719
+ index: toolCallBlockIndex,
1720
+ block: {
1721
+ type: 'tool_call',
1722
+ toolId: call.id,
1723
+ toolName: call.name,
1724
+ input: call.input,
1725
+ },
1726
+ },
1727
+ });
1728
+ parser.incrementBlockIndex();
1729
+ }
1730
+ }
1731
+
1732
+ // Track the tool calls
1733
+ executedToolCalls.push(...parsed.calls);
1734
+
1735
+ // Build tool context
1736
+ const context: ToolContext = {
1737
+ rawText: parsed.fullMatch,
1738
+ preamble: parsed.beforeText,
1739
+ depth: toolDepth,
1740
+ previousResults: executedToolResults,
1741
+ accumulated: parser.getAccumulated(),
1742
+ };
1743
+
1744
+ // Yield control for tool execution
1745
+ const toolCallsEvent: ToolCallsEvent = {
1746
+ type: 'tool-calls',
1747
+ calls: parsed.calls,
1748
+ context,
1749
+ };
1750
+
1751
+ const results = await stream.requestToolExecution(toolCallsEvent);
1752
+
1753
+ // Track the tool results
1754
+ executedToolResults.push(...results);
1755
+
1756
+ // Check if results contain images
1757
+ if (hasImageInToolResults(results)) {
1758
+ const splitContent = formatToolResultsForSplitTurn(results);
1759
+
1760
+ // Emit block events for tool results
1761
+ if (emitBlocks) {
1762
+ stream.emit({
1763
+ type: 'block',
1764
+ event: {
1765
+ event: 'block_start',
1766
+ index: parser.getBlockIndex(),
1767
+ block: { type: 'tool_result' },
1768
+ },
1769
+ });
1770
+ }
1771
+
1772
+ parser.push(splitContent.beforeImageXml);
1773
+
1774
+ // Emit tool result content
1775
+ for (const result of results) {
1776
+ const resultContent = typeof result.content === 'string'
1777
+ ? result.content
1778
+ : JSON.stringify(result.content);
1779
+
1780
+ if (emitTokens) {
1781
+ const toolResultMeta: ChunkMeta = {
1782
+ type: 'tool_result',
1783
+ visible: false,
1784
+ blockIndex: parser.getBlockIndex(),
1785
+ toolId: result.toolUseId,
1786
+ };
1787
+ stream.emit({ type: 'tokens', content: resultContent, meta: toolResultMeta });
1788
+ }
1789
+
1790
+ if (emitBlocks) {
1791
+ stream.emit({
1792
+ type: 'block',
1793
+ event: {
1794
+ event: 'block_complete',
1795
+ index: parser.getBlockIndex(),
1796
+ block: {
1797
+ type: 'tool_result',
1798
+ toolId: result.toolUseId,
1799
+ content: resultContent,
1800
+ isError: result.isError,
1801
+ },
1802
+ },
1803
+ });
1804
+ }
1805
+ parser.incrementBlockIndex();
1806
+ }
1807
+
1808
+ let afterImageXml = splitContent.afterImageXml;
1809
+ if (request.config.thinking?.enabled) {
1810
+ afterImageXml += '\n<thinking>';
1811
+ }
1812
+
1813
+ providerRequest = this.buildContinuationRequestWithImages(
1814
+ request,
1815
+ prefillResult,
1816
+ parser.getAccumulated(),
1817
+ splitContent.images,
1818
+ afterImageXml
1819
+ );
1820
+
1821
+ parser.push(afterImageXml);
1822
+ prefillResult.assistantPrefill = parser.getAccumulated();
1823
+ parser.resetForNewIteration();
1824
+ } else {
1825
+ // Standard path: no images
1826
+ const resultsXml = formatToolResults(results);
1827
+
1828
+ if (emitBlocks) {
1829
+ stream.emit({
1830
+ type: 'block',
1831
+ event: {
1832
+ event: 'block_start',
1833
+ index: parser.getBlockIndex(),
1834
+ block: { type: 'tool_result' },
1835
+ },
1836
+ });
1837
+ }
1838
+
1839
+ parser.push(resultsXml);
1840
+
1841
+ for (const result of results) {
1842
+ const resultContent = typeof result.content === 'string'
1843
+ ? result.content
1844
+ : JSON.stringify(result.content);
1845
+
1846
+ if (emitTokens) {
1847
+ const toolResultMeta: ChunkMeta = {
1848
+ type: 'tool_result',
1849
+ visible: false,
1850
+ blockIndex: parser.getBlockIndex(),
1851
+ toolId: result.toolUseId,
1852
+ };
1853
+ stream.emit({ type: 'tokens', content: resultContent, meta: toolResultMeta });
1854
+ }
1855
+
1856
+ if (emitBlocks) {
1857
+ stream.emit({
1858
+ type: 'block',
1859
+ event: {
1860
+ event: 'block_complete',
1861
+ index: parser.getBlockIndex(),
1862
+ block: {
1863
+ type: 'tool_result',
1864
+ toolId: result.toolUseId,
1865
+ content: resultContent,
1866
+ isError: result.isError,
1867
+ },
1868
+ },
1869
+ });
1870
+ }
1871
+ parser.incrementBlockIndex();
1872
+ }
1873
+
1874
+ if (request.config.thinking?.enabled) {
1875
+ parser.push('\n<thinking>');
1876
+ }
1877
+
1878
+ prefillResult.assistantPrefill = parser.getAccumulated();
1879
+ providerRequest = this.buildContinuationRequest(
1880
+ request,
1881
+ prefillResult,
1882
+ parser.getAccumulated()
1883
+ );
1884
+ }
1885
+
1886
+ parser.resetForNewIteration();
1887
+ toolDepth++;
1888
+ continue;
1889
+ }
1890
+ }
1891
+
1892
+ // Check for false-positive stop (unclosed block)
1893
+ if (lastStopReason === 'stop_sequence' && parser.isInsideBlock()) {
1894
+ if (streamResult.stopSequence) {
1895
+ parser.push(streamResult.stopSequence);
1896
+ if (emitTokens) {
1897
+ const meta: ChunkMeta = {
1898
+ type: parser.getCurrentBlockType(),
1899
+ visible: parser.getCurrentBlockType() === 'text',
1900
+ blockIndex: 0,
1901
+ };
1902
+ stream.emit({ type: 'tokens', content: streamResult.stopSequence, meta });
1903
+ }
1904
+ }
1905
+
1906
+ toolDepth++;
1907
+ if (toolDepth > maxToolDepth) {
1908
+ break;
1909
+ }
1910
+ prefillResult.assistantPrefill = parser.getAccumulated();
1911
+ providerRequest = this.buildContinuationRequest(
1912
+ request,
1913
+ prefillResult,
1914
+ parser.getAccumulated()
1915
+ );
1916
+ parser.resetForNewIteration();
1917
+ continue;
1918
+ }
1919
+
1920
+ // No more tools, we're done
1921
+ break;
1922
+ }
1923
+
1924
+ // Build final response
1925
+ const fullAccumulated = parser.getAccumulated();
1926
+ const newContent = fullAccumulated.slice(initialPrefillLength);
1927
+
1928
+ const response = this.buildFinalResponse(
1929
+ newContent,
1930
+ contentBlocks,
1931
+ lastStopReason,
1932
+ totalUsage,
1933
+ request,
1934
+ prefillResult,
1935
+ startTime,
1936
+ 1,
1937
+ rawRequest,
1938
+ rawResponse,
1939
+ executedToolCalls,
1940
+ executedToolResults,
1941
+ initialBlockType
1942
+ );
1943
+
1944
+ stream.emit({ type: 'complete', response });
1945
+ } catch (error) {
1946
+ if (this.isAbortError(error)) {
1947
+ const fullAccumulated = parser.getAccumulated();
1948
+ const newContent = fullAccumulated.slice(initialPrefillLength);
1949
+ stream.emit({
1950
+ type: 'aborted',
1951
+ reason: 'user',
1952
+ partialContent: parseAccumulatedIntoBlocks(newContent).blocks,
1953
+ rawAssistantText: newContent,
1954
+ toolCalls: executedToolCalls,
1955
+ toolResults: executedToolResults,
1956
+ });
1957
+ } else {
1958
+ throw error;
1959
+ }
1960
+ }
1961
+ }
1962
+
1963
+ /**
1964
+ * Run native tool execution with yielding stream.
1965
+ */
1966
+ private async runNativeToolsYielding(
1967
+ request: NormalizedRequest,
1968
+ options: YieldingStreamOptions,
1969
+ stream: YieldingStreamImpl
1970
+ ): Promise<void> {
1971
+ const startTime = Date.now();
1972
+ const {
1973
+ maxToolDepth = 10,
1974
+ emitTokens = true,
1975
+ emitUsage = true,
1976
+ } = options;
1977
+
1978
+ let toolDepth = 0;
1979
+ let totalUsage: BasicUsage = { inputTokens: 0, outputTokens: 0 };
1980
+ let lastStopReason: StopReason = 'end_turn';
1981
+ let rawRequest: unknown;
1982
+ let rawResponse: unknown;
1983
+
1984
+ let allTextAccumulated = '';
1985
+ const executedToolCalls: ToolCall[] = [];
1986
+ const executedToolResults: ToolResult[] = [];
1987
+
1988
+ let messages = [...request.messages];
1989
+ let allContentBlocks: ContentBlock[] = [];
1990
+
1991
+ try {
1992
+ // Tool execution loop
1993
+ while (toolDepth <= maxToolDepth) {
1994
+ // Check for cancellation
1995
+ if (stream.isCancelled) {
1996
+ stream.emit({
1997
+ type: 'aborted',
1998
+ reason: 'user',
1999
+ rawAssistantText: allTextAccumulated,
2000
+ toolCalls: executedToolCalls,
2001
+ toolResults: executedToolResults,
2002
+ });
2003
+ return;
2004
+ }
2005
+
2006
+ // Build provider request with native tools
2007
+ const providerRequest = this.buildNativeToolRequest(request, messages);
2008
+
2009
+ // Stream from provider
2010
+ let textAccumulated = '';
2011
+ let blockIndex = 0;
2012
+ const streamResult = await this.streamOnce(
2013
+ providerRequest,
2014
+ {
2015
+ onChunk: (chunk) => {
2016
+ if (stream.isCancelled) return;
2017
+
2018
+ textAccumulated += chunk;
2019
+ allTextAccumulated += chunk;
2020
+
2021
+ if (emitTokens) {
2022
+ const meta: ChunkMeta = {
2023
+ type: 'text',
2024
+ visible: true,
2025
+ blockIndex,
2026
+ };
2027
+ stream.emit({ type: 'tokens', content: chunk, meta });
2028
+ }
2029
+ },
2030
+ onContentBlock: undefined,
2031
+ },
2032
+ {
2033
+ signal: stream.signal,
2034
+ timeoutMs: options.timeoutMs,
2035
+ onRequest: (req: unknown) => { rawRequest = req; },
2036
+ }
2037
+ );
2038
+
2039
+ rawResponse = streamResult.raw;
2040
+ lastStopReason = this.mapStopReason(streamResult.stopReason);
2041
+
2042
+ // Accumulate usage
2043
+ totalUsage.inputTokens += streamResult.usage.inputTokens;
2044
+ totalUsage.outputTokens += streamResult.usage.outputTokens;
2045
+ if (emitUsage) {
2046
+ stream.emit({ type: 'usage', usage: { ...totalUsage } });
2047
+ }
2048
+
2049
+ // Parse content blocks from response
2050
+ const responseBlocks = this.parseProviderContent(streamResult.content);
2051
+ allContentBlocks.push(...responseBlocks);
2052
+
2053
+ // Check for tool_use blocks
2054
+ const toolUseBlocks = responseBlocks.filter(
2055
+ (b): b is ContentBlock & { type: 'tool_use' } => b.type === 'tool_use'
2056
+ );
2057
+
2058
+ if (toolUseBlocks.length > 0 && lastStopReason === 'tool_use') {
2059
+ // Convert to normalized ToolCall[]
2060
+ const toolCalls: ToolCall[] = toolUseBlocks.map(block => ({
2061
+ id: block.id,
2062
+ name: block.name,
2063
+ input: block.input as Record<string, unknown>,
2064
+ }));
2065
+
2066
+ // Track tool calls
2067
+ executedToolCalls.push(...toolCalls);
2068
+
2069
+ // Build tool context
2070
+ const context: ToolContext = {
2071
+ rawText: JSON.stringify(toolUseBlocks),
2072
+ preamble: textAccumulated,
2073
+ depth: toolDepth,
2074
+ previousResults: executedToolResults,
2075
+ accumulated: allTextAccumulated,
2076
+ };
2077
+
2078
+ // Yield control for tool execution
2079
+ const toolCallsEvent: ToolCallsEvent = {
2080
+ type: 'tool-calls',
2081
+ calls: toolCalls,
2082
+ context,
2083
+ };
2084
+
2085
+ const results = await stream.requestToolExecution(toolCallsEvent);
2086
+
2087
+ // Track tool results
2088
+ executedToolResults.push(...results);
2089
+
2090
+ // Add tool results to content blocks
2091
+ for (const result of results) {
2092
+ allContentBlocks.push({
2093
+ type: 'tool_result',
2094
+ toolUseId: result.toolUseId,
2095
+ content: result.content,
2096
+ isError: result.isError,
2097
+ });
2098
+ }
2099
+
2100
+ // Add messages for next iteration
2101
+ messages.push({
2102
+ participant: 'Claude',
2103
+ content: responseBlocks,
2104
+ });
2105
+
2106
+ messages.push({
2107
+ participant: 'User',
2108
+ content: results.map(r => ({
2109
+ type: 'tool_result' as const,
2110
+ toolUseId: r.toolUseId,
2111
+ content: r.content,
2112
+ isError: r.isError,
2113
+ })),
2114
+ });
2115
+
2116
+ toolDepth++;
2117
+ continue;
2118
+ }
2119
+
2120
+ // No more tools, we're done
2121
+ break;
2122
+ }
2123
+
2124
+ const durationMs = Date.now() - startTime;
2125
+
2126
+ const response: NormalizedResponse = {
2127
+ content: allContentBlocks,
2128
+ rawAssistantText: allTextAccumulated,
2129
+ toolCalls: executedToolCalls,
2130
+ toolResults: executedToolResults,
2131
+ stopReason: lastStopReason,
2132
+ usage: totalUsage,
2133
+ details: {
2134
+ stop: {
2135
+ reason: lastStopReason,
2136
+ wasTruncated: lastStopReason === 'max_tokens',
2137
+ },
2138
+ usage: { ...totalUsage },
2139
+ timing: {
2140
+ totalDurationMs: durationMs,
2141
+ attempts: 1,
2142
+ },
2143
+ model: {
2144
+ requested: request.config.model,
2145
+ actual: request.config.model,
2146
+ provider: this.adapter.name,
2147
+ },
2148
+ cache: {
2149
+ markersInRequest: 0,
2150
+ tokensCreated: 0,
2151
+ tokensRead: 0,
2152
+ hitRatio: 0,
2153
+ },
2154
+ },
2155
+ raw: {
2156
+ request: rawRequest,
2157
+ response: rawResponse,
2158
+ },
2159
+ };
2160
+
2161
+ stream.emit({ type: 'complete', response });
2162
+ } catch (error) {
2163
+ if (this.isAbortError(error)) {
2164
+ stream.emit({
2165
+ type: 'aborted',
2166
+ reason: 'user',
2167
+ rawAssistantText: allTextAccumulated,
2168
+ toolCalls: executedToolCalls,
2169
+ toolResults: executedToolResults,
2170
+ });
2171
+ } else {
2172
+ throw error;
2173
+ }
2174
+ }
2175
+ }
1449
2176
  }