@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.
Files changed (38) hide show
  1. package/dist/membrane.d.ts +37 -0
  2. package/dist/membrane.d.ts.map +1 -1
  3. package/dist/membrane.js +554 -0
  4. package/dist/membrane.js.map +1 -1
  5. package/dist/providers/mock.d.ts +8 -0
  6. package/dist/providers/mock.d.ts.map +1 -1
  7. package/dist/providers/mock.js +39 -2
  8. package/dist/providers/mock.js.map +1 -1
  9. package/dist/providers/openai-compatible.d.ts.map +1 -1
  10. package/dist/providers/openai-compatible.js +7 -2
  11. package/dist/providers/openai-compatible.js.map +1 -1
  12. package/dist/providers/openai.d.ts.map +1 -1
  13. package/dist/providers/openai.js +7 -2
  14. package/dist/providers/openai.js.map +1 -1
  15. package/dist/providers/openrouter.d.ts.map +1 -1
  16. package/dist/providers/openrouter.js +7 -2
  17. package/dist/providers/openrouter.js.map +1 -1
  18. package/dist/types/index.d.ts +2 -0
  19. package/dist/types/index.d.ts.map +1 -1
  20. package/dist/types/index.js +1 -0
  21. package/dist/types/index.js.map +1 -1
  22. package/dist/types/yielding-stream.d.ts +167 -0
  23. package/dist/types/yielding-stream.d.ts.map +1 -0
  24. package/dist/types/yielding-stream.js +34 -0
  25. package/dist/types/yielding-stream.js.map +1 -0
  26. package/dist/yielding-stream.d.ts +60 -0
  27. package/dist/yielding-stream.d.ts.map +1 -0
  28. package/dist/yielding-stream.js +204 -0
  29. package/dist/yielding-stream.js.map +1 -0
  30. package/package.json +1 -1
  31. package/src/membrane.ts +691 -1
  32. package/src/providers/mock.ts +47 -2
  33. package/src/providers/openai-compatible.ts +10 -4
  34. package/src/providers/openai.ts +10 -4
  35. package/src/providers/openrouter.ts +10 -4
  36. package/src/types/index.ts +23 -0
  37. package/src/types/yielding-stream.ts +228 -0
  38. 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
@@ -1043,7 +1050,7 @@ export class Membrane {
1043
1050
  private async streamOnce(
1044
1051
  request: any,
1045
1052
  callbacks: { onChunk: (chunk: string) => void; onContentBlock?: (index: number, block: unknown) => void },
1046
- options: { signal?: AbortSignal; onRequest?: (rawRequest: unknown) => void }
1053
+ options: { signal?: AbortSignal; timeoutMs?: number; onRequest?: (rawRequest: unknown) => void }
1047
1054
  ) {
1048
1055
  return await this.adapter.stream(request, callbacks, options);
1049
1056
  }
@@ -1446,4 +1453,687 @@ export class Membrane {
1446
1453
  toolResults: toolResults.length > 0 ? toolResults : undefined,
1447
1454
  };
1448
1455
  }
1456
+
1457
+ // ============================================================================
1458
+ // Yielding Stream API
1459
+ // ============================================================================
1460
+
1461
+ /**
1462
+ * Stream inference with yielding control for tool execution.
1463
+ *
1464
+ * Unlike `stream()` which uses callbacks for tool execution, this method
1465
+ * returns an async iterator that yields control back to the caller when
1466
+ * tool calls are detected. The caller provides results via `provideToolResults()`.
1467
+ *
1468
+ * @example
1469
+ * ```typescript
1470
+ * const stream = membrane.streamYielding(request, options);
1471
+ *
1472
+ * for await (const event of stream) {
1473
+ * switch (event.type) {
1474
+ * case 'tokens':
1475
+ * process.stdout.write(event.content);
1476
+ * break;
1477
+ * case 'tool-calls':
1478
+ * const results = await executeTools(event.calls);
1479
+ * stream.provideToolResults(results);
1480
+ * break;
1481
+ * case 'complete':
1482
+ * console.log('Done:', event.response);
1483
+ * break;
1484
+ * }
1485
+ * }
1486
+ * ```
1487
+ */
1488
+ streamYielding(
1489
+ request: NormalizedRequest,
1490
+ options: YieldingStreamOptions = {}
1491
+ ): YieldingStream {
1492
+ const toolMode = this.resolveToolMode(request);
1493
+
1494
+ // Create the yielding stream with the appropriate inference runner
1495
+ const runInference = toolMode === 'native'
1496
+ ? (stream: YieldingStreamImpl) => this.runNativeToolsYielding(request, options, stream)
1497
+ : (stream: YieldingStreamImpl) => this.runXmlToolsYielding(request, options, stream);
1498
+
1499
+ return new YieldingStreamImpl(options, runInference);
1500
+ }
1501
+
1502
+ /**
1503
+ * Run XML-based tool execution with yielding stream.
1504
+ */
1505
+ private async runXmlToolsYielding(
1506
+ request: NormalizedRequest,
1507
+ options: YieldingStreamOptions,
1508
+ stream: YieldingStreamImpl
1509
+ ): Promise<void> {
1510
+ const startTime = Date.now();
1511
+ const {
1512
+ maxToolDepth = 10,
1513
+ emitTokens = true,
1514
+ emitBlocks = true,
1515
+ emitUsage = true,
1516
+ } = options;
1517
+
1518
+ // Initialize parser from formatter for format-specific tracking
1519
+ const formatter = this.formatter;
1520
+ const parser = formatter.createStreamParser();
1521
+ let toolDepth = 0;
1522
+ let totalUsage: BasicUsage = { inputTokens: 0, outputTokens: 0 };
1523
+ const contentBlocks: ContentBlock[] = [];
1524
+ let lastStopReason: StopReason = 'end_turn';
1525
+ let rawRequest: unknown;
1526
+ let rawResponse: unknown;
1527
+
1528
+ // Track executed tool calls and results
1529
+ const executedToolCalls: ToolCall[] = [];
1530
+ const executedToolResults: ToolResult[] = [];
1531
+
1532
+ // Transform initial request using the formatter
1533
+ let { providerRequest, prefillResult } = this.transformRequest(request, formatter);
1534
+
1535
+ // Initialize parser with prefill content
1536
+ let initialPrefillLength = 0;
1537
+ let initialBlockType: 'thinking' | 'tool_call' | 'tool_result' | null = null;
1538
+ if (prefillResult.assistantPrefill) {
1539
+ parser.push(prefillResult.assistantPrefill);
1540
+ initialPrefillLength = prefillResult.assistantPrefill.length;
1541
+ if (parser.isInsideBlock()) {
1542
+ const blockType = parser.getCurrentBlockType();
1543
+ if (blockType === 'thinking' || blockType === 'tool_call' || blockType === 'tool_result') {
1544
+ initialBlockType = blockType;
1545
+ }
1546
+ }
1547
+ }
1548
+
1549
+ try {
1550
+ // Tool execution loop
1551
+ while (toolDepth <= maxToolDepth) {
1552
+ // Check for cancellation
1553
+ if (stream.isCancelled) {
1554
+ const fullAccumulated = parser.getAccumulated();
1555
+ const newContent = fullAccumulated.slice(initialPrefillLength);
1556
+ stream.emit({
1557
+ type: 'aborted',
1558
+ reason: 'user',
1559
+ partialContent: parseAccumulatedIntoBlocks(newContent).blocks,
1560
+ rawAssistantText: newContent,
1561
+ toolCalls: executedToolCalls,
1562
+ toolResults: executedToolResults,
1563
+ });
1564
+ return;
1565
+ }
1566
+
1567
+ // Track if we manually detected a stop sequence
1568
+ let detectedStopSequence: string | null = null;
1569
+ let truncatedAccumulated: string | null = null;
1570
+ const checkFromIndex = parser.getAccumulated().length;
1571
+
1572
+ // Stream from provider
1573
+ const streamResult = await this.streamOnce(
1574
+ providerRequest,
1575
+ {
1576
+ onChunk: (chunk) => {
1577
+ if (detectedStopSequence || stream.isCancelled) {
1578
+ return;
1579
+ }
1580
+
1581
+ // Process chunk with enriched streaming API
1582
+ const { emissions } = parser.processChunk(chunk);
1583
+
1584
+ // Check for stop sequences only in NEW content
1585
+ const accumulated = parser.getAccumulated();
1586
+ const newContent = accumulated.slice(checkFromIndex);
1587
+
1588
+ for (const stopSeq of prefillResult.stopSequences) {
1589
+ const idx = newContent.indexOf(stopSeq);
1590
+ if (idx !== -1) {
1591
+ const absoluteIdx = checkFromIndex + idx;
1592
+ detectedStopSequence = stopSeq;
1593
+ truncatedAccumulated = accumulated.slice(0, absoluteIdx);
1594
+
1595
+ // Emit only the portion up to stop sequence
1596
+ const alreadyEmitted = accumulated.length - chunk.length;
1597
+ if (emitTokens && absoluteIdx > alreadyEmitted) {
1598
+ const truncatedChunk = accumulated.slice(alreadyEmitted, absoluteIdx);
1599
+ const meta: ChunkMeta = {
1600
+ type: parser.getCurrentBlockType(),
1601
+ visible: parser.getCurrentBlockType() === 'text',
1602
+ blockIndex: 0,
1603
+ };
1604
+ stream.emit({ type: 'tokens', content: truncatedChunk, meta });
1605
+ }
1606
+ return;
1607
+ }
1608
+ }
1609
+
1610
+ // Emit in correct interleaved order
1611
+ for (const emission of emissions) {
1612
+ if (emission.kind === 'blockEvent') {
1613
+ if (emitBlocks) {
1614
+ stream.emit({ type: 'block', event: emission.event });
1615
+ }
1616
+ } else {
1617
+ if (emitTokens) {
1618
+ stream.emit({ type: 'tokens', content: emission.text, meta: emission.meta });
1619
+ }
1620
+ }
1621
+ }
1622
+ },
1623
+ onContentBlock: undefined,
1624
+ },
1625
+ {
1626
+ signal: stream.signal,
1627
+ timeoutMs: options.timeoutMs,
1628
+ onRequest: (req: unknown) => { rawRequest = req; },
1629
+ }
1630
+ );
1631
+
1632
+ // If we detected stop sequence manually, fix up the parser and result
1633
+ if (detectedStopSequence && truncatedAccumulated !== null) {
1634
+ parser.reset();
1635
+ parser.push(truncatedAccumulated);
1636
+ streamResult.stopReason = 'stop_sequence';
1637
+ streamResult.stopSequence = detectedStopSequence;
1638
+ }
1639
+
1640
+ rawResponse = streamResult.raw;
1641
+ lastStopReason = this.mapStopReason(streamResult.stopReason);
1642
+
1643
+ // Accumulate usage
1644
+ totalUsage.inputTokens += streamResult.usage.inputTokens;
1645
+ totalUsage.outputTokens += streamResult.usage.outputTokens;
1646
+ if (emitUsage) {
1647
+ stream.emit({ type: 'usage', usage: { ...totalUsage } });
1648
+ }
1649
+
1650
+ // Flush the parser
1651
+ const flushResult = parser.flush();
1652
+ for (const emission of flushResult.emissions) {
1653
+ if (emission.kind === 'blockEvent' && emitBlocks) {
1654
+ stream.emit({ type: 'block', event: emission.event });
1655
+ }
1656
+ }
1657
+
1658
+ // Check for tool calls
1659
+ if (streamResult.stopSequence === '</function_calls>') {
1660
+ const closeTag = '</function_calls>';
1661
+ parser.push(closeTag);
1662
+
1663
+ const parsed = parseToolCalls(parser.getAccumulated());
1664
+
1665
+ if (parsed && parsed.calls.length > 0) {
1666
+ // Emit block events for each tool call
1667
+ if (emitBlocks) {
1668
+ for (const call of parsed.calls) {
1669
+ const toolCallBlockIndex = parser.getBlockIndex();
1670
+ stream.emit({
1671
+ type: 'block',
1672
+ event: {
1673
+ event: 'block_start',
1674
+ index: toolCallBlockIndex,
1675
+ block: { type: 'tool_call' },
1676
+ },
1677
+ });
1678
+ stream.emit({
1679
+ type: 'block',
1680
+ event: {
1681
+ event: 'block_complete',
1682
+ index: toolCallBlockIndex,
1683
+ block: {
1684
+ type: 'tool_call',
1685
+ toolId: call.id,
1686
+ toolName: call.name,
1687
+ input: call.input,
1688
+ },
1689
+ },
1690
+ });
1691
+ parser.incrementBlockIndex();
1692
+ }
1693
+ }
1694
+
1695
+ // Track the tool calls
1696
+ executedToolCalls.push(...parsed.calls);
1697
+
1698
+ // Build tool context
1699
+ const context: ToolContext = {
1700
+ rawText: parsed.fullMatch,
1701
+ preamble: parsed.beforeText,
1702
+ depth: toolDepth,
1703
+ previousResults: executedToolResults,
1704
+ accumulated: parser.getAccumulated(),
1705
+ };
1706
+
1707
+ // Yield control for tool execution
1708
+ const toolCallsEvent: ToolCallsEvent = {
1709
+ type: 'tool-calls',
1710
+ calls: parsed.calls,
1711
+ context,
1712
+ };
1713
+
1714
+ const results = await stream.requestToolExecution(toolCallsEvent);
1715
+
1716
+ // Track the tool results
1717
+ executedToolResults.push(...results);
1718
+
1719
+ // Check if results contain images
1720
+ if (hasImageInToolResults(results)) {
1721
+ const splitContent = formatToolResultsForSplitTurn(results);
1722
+
1723
+ // Emit block events for tool results
1724
+ if (emitBlocks) {
1725
+ stream.emit({
1726
+ type: 'block',
1727
+ event: {
1728
+ event: 'block_start',
1729
+ index: parser.getBlockIndex(),
1730
+ block: { type: 'tool_result' },
1731
+ },
1732
+ });
1733
+ }
1734
+
1735
+ parser.push(splitContent.beforeImageXml);
1736
+
1737
+ // Emit tool result content
1738
+ for (const result of results) {
1739
+ const resultContent = typeof result.content === 'string'
1740
+ ? result.content
1741
+ : JSON.stringify(result.content);
1742
+
1743
+ if (emitTokens) {
1744
+ const toolResultMeta: ChunkMeta = {
1745
+ type: 'tool_result',
1746
+ visible: false,
1747
+ blockIndex: parser.getBlockIndex(),
1748
+ toolId: result.toolUseId,
1749
+ };
1750
+ stream.emit({ type: 'tokens', content: resultContent, meta: toolResultMeta });
1751
+ }
1752
+
1753
+ if (emitBlocks) {
1754
+ stream.emit({
1755
+ type: 'block',
1756
+ event: {
1757
+ event: 'block_complete',
1758
+ index: parser.getBlockIndex(),
1759
+ block: {
1760
+ type: 'tool_result',
1761
+ toolId: result.toolUseId,
1762
+ content: resultContent,
1763
+ isError: result.isError,
1764
+ },
1765
+ },
1766
+ });
1767
+ }
1768
+ parser.incrementBlockIndex();
1769
+ }
1770
+
1771
+ let afterImageXml = splitContent.afterImageXml;
1772
+ if (request.config.thinking?.enabled) {
1773
+ afterImageXml += '\n<thinking>';
1774
+ }
1775
+
1776
+ providerRequest = this.buildContinuationRequestWithImages(
1777
+ request,
1778
+ prefillResult,
1779
+ parser.getAccumulated(),
1780
+ splitContent.images,
1781
+ afterImageXml
1782
+ );
1783
+
1784
+ parser.push(afterImageXml);
1785
+ prefillResult.assistantPrefill = parser.getAccumulated();
1786
+ parser.resetForNewIteration();
1787
+ } else {
1788
+ // Standard path: no images
1789
+ const resultsXml = formatToolResults(results);
1790
+
1791
+ if (emitBlocks) {
1792
+ stream.emit({
1793
+ type: 'block',
1794
+ event: {
1795
+ event: 'block_start',
1796
+ index: parser.getBlockIndex(),
1797
+ block: { type: 'tool_result' },
1798
+ },
1799
+ });
1800
+ }
1801
+
1802
+ parser.push(resultsXml);
1803
+
1804
+ for (const result of results) {
1805
+ const resultContent = typeof result.content === 'string'
1806
+ ? result.content
1807
+ : JSON.stringify(result.content);
1808
+
1809
+ if (emitTokens) {
1810
+ const toolResultMeta: ChunkMeta = {
1811
+ type: 'tool_result',
1812
+ visible: false,
1813
+ blockIndex: parser.getBlockIndex(),
1814
+ toolId: result.toolUseId,
1815
+ };
1816
+ stream.emit({ type: 'tokens', content: resultContent, meta: toolResultMeta });
1817
+ }
1818
+
1819
+ if (emitBlocks) {
1820
+ stream.emit({
1821
+ type: 'block',
1822
+ event: {
1823
+ event: 'block_complete',
1824
+ index: parser.getBlockIndex(),
1825
+ block: {
1826
+ type: 'tool_result',
1827
+ toolId: result.toolUseId,
1828
+ content: resultContent,
1829
+ isError: result.isError,
1830
+ },
1831
+ },
1832
+ });
1833
+ }
1834
+ parser.incrementBlockIndex();
1835
+ }
1836
+
1837
+ if (request.config.thinking?.enabled) {
1838
+ parser.push('\n<thinking>');
1839
+ }
1840
+
1841
+ prefillResult.assistantPrefill = parser.getAccumulated();
1842
+ providerRequest = this.buildContinuationRequest(
1843
+ request,
1844
+ prefillResult,
1845
+ parser.getAccumulated()
1846
+ );
1847
+ }
1848
+
1849
+ parser.resetForNewIteration();
1850
+ toolDepth++;
1851
+ continue;
1852
+ }
1853
+ }
1854
+
1855
+ // Check for false-positive stop (unclosed block)
1856
+ if (lastStopReason === 'stop_sequence' && parser.isInsideBlock()) {
1857
+ if (streamResult.stopSequence) {
1858
+ parser.push(streamResult.stopSequence);
1859
+ if (emitTokens) {
1860
+ const meta: ChunkMeta = {
1861
+ type: parser.getCurrentBlockType(),
1862
+ visible: parser.getCurrentBlockType() === 'text',
1863
+ blockIndex: 0,
1864
+ };
1865
+ stream.emit({ type: 'tokens', content: streamResult.stopSequence, meta });
1866
+ }
1867
+ }
1868
+
1869
+ toolDepth++;
1870
+ if (toolDepth > maxToolDepth) {
1871
+ break;
1872
+ }
1873
+ prefillResult.assistantPrefill = parser.getAccumulated();
1874
+ providerRequest = this.buildContinuationRequest(
1875
+ request,
1876
+ prefillResult,
1877
+ parser.getAccumulated()
1878
+ );
1879
+ parser.resetForNewIteration();
1880
+ continue;
1881
+ }
1882
+
1883
+ // No more tools, we're done
1884
+ break;
1885
+ }
1886
+
1887
+ // Build final response
1888
+ const fullAccumulated = parser.getAccumulated();
1889
+ const newContent = fullAccumulated.slice(initialPrefillLength);
1890
+
1891
+ const response = this.buildFinalResponse(
1892
+ newContent,
1893
+ contentBlocks,
1894
+ lastStopReason,
1895
+ totalUsage,
1896
+ request,
1897
+ prefillResult,
1898
+ startTime,
1899
+ 1,
1900
+ rawRequest,
1901
+ rawResponse,
1902
+ executedToolCalls,
1903
+ executedToolResults,
1904
+ initialBlockType
1905
+ );
1906
+
1907
+ stream.emit({ type: 'complete', response });
1908
+ } catch (error) {
1909
+ if (this.isAbortError(error)) {
1910
+ const fullAccumulated = parser.getAccumulated();
1911
+ const newContent = fullAccumulated.slice(initialPrefillLength);
1912
+ stream.emit({
1913
+ type: 'aborted',
1914
+ reason: 'user',
1915
+ partialContent: parseAccumulatedIntoBlocks(newContent).blocks,
1916
+ rawAssistantText: newContent,
1917
+ toolCalls: executedToolCalls,
1918
+ toolResults: executedToolResults,
1919
+ });
1920
+ } else {
1921
+ throw error;
1922
+ }
1923
+ }
1924
+ }
1925
+
1926
+ /**
1927
+ * Run native tool execution with yielding stream.
1928
+ */
1929
+ private async runNativeToolsYielding(
1930
+ request: NormalizedRequest,
1931
+ options: YieldingStreamOptions,
1932
+ stream: YieldingStreamImpl
1933
+ ): Promise<void> {
1934
+ const startTime = Date.now();
1935
+ const {
1936
+ maxToolDepth = 10,
1937
+ emitTokens = true,
1938
+ emitUsage = true,
1939
+ } = options;
1940
+
1941
+ let toolDepth = 0;
1942
+ let totalUsage: BasicUsage = { inputTokens: 0, outputTokens: 0 };
1943
+ let lastStopReason: StopReason = 'end_turn';
1944
+ let rawRequest: unknown;
1945
+ let rawResponse: unknown;
1946
+
1947
+ let allTextAccumulated = '';
1948
+ const executedToolCalls: ToolCall[] = [];
1949
+ const executedToolResults: ToolResult[] = [];
1950
+
1951
+ let messages = [...request.messages];
1952
+ let allContentBlocks: ContentBlock[] = [];
1953
+
1954
+ try {
1955
+ // Tool execution loop
1956
+ while (toolDepth <= maxToolDepth) {
1957
+ // Check for cancellation
1958
+ if (stream.isCancelled) {
1959
+ stream.emit({
1960
+ type: 'aborted',
1961
+ reason: 'user',
1962
+ rawAssistantText: allTextAccumulated,
1963
+ toolCalls: executedToolCalls,
1964
+ toolResults: executedToolResults,
1965
+ });
1966
+ return;
1967
+ }
1968
+
1969
+ // Build provider request with native tools
1970
+ const providerRequest = this.buildNativeToolRequest(request, messages);
1971
+
1972
+ // Stream from provider
1973
+ let textAccumulated = '';
1974
+ let blockIndex = 0;
1975
+ const streamResult = await this.streamOnce(
1976
+ providerRequest,
1977
+ {
1978
+ onChunk: (chunk) => {
1979
+ if (stream.isCancelled) return;
1980
+
1981
+ textAccumulated += chunk;
1982
+ allTextAccumulated += chunk;
1983
+
1984
+ if (emitTokens) {
1985
+ const meta: ChunkMeta = {
1986
+ type: 'text',
1987
+ visible: true,
1988
+ blockIndex,
1989
+ };
1990
+ stream.emit({ type: 'tokens', content: chunk, meta });
1991
+ }
1992
+ },
1993
+ onContentBlock: undefined,
1994
+ },
1995
+ {
1996
+ signal: stream.signal,
1997
+ timeoutMs: options.timeoutMs,
1998
+ onRequest: (req: unknown) => { rawRequest = req; },
1999
+ }
2000
+ );
2001
+
2002
+ rawResponse = streamResult.raw;
2003
+ lastStopReason = this.mapStopReason(streamResult.stopReason);
2004
+
2005
+ // Accumulate usage
2006
+ totalUsage.inputTokens += streamResult.usage.inputTokens;
2007
+ totalUsage.outputTokens += streamResult.usage.outputTokens;
2008
+ if (emitUsage) {
2009
+ stream.emit({ type: 'usage', usage: { ...totalUsage } });
2010
+ }
2011
+
2012
+ // Parse content blocks from response
2013
+ const responseBlocks = this.parseProviderContent(streamResult.content);
2014
+ allContentBlocks.push(...responseBlocks);
2015
+
2016
+ // Check for tool_use blocks
2017
+ const toolUseBlocks = responseBlocks.filter(
2018
+ (b): b is ContentBlock & { type: 'tool_use' } => b.type === 'tool_use'
2019
+ );
2020
+
2021
+ if (toolUseBlocks.length > 0 && lastStopReason === 'tool_use') {
2022
+ // Convert to normalized ToolCall[]
2023
+ const toolCalls: ToolCall[] = toolUseBlocks.map(block => ({
2024
+ id: block.id,
2025
+ name: block.name,
2026
+ input: block.input as Record<string, unknown>,
2027
+ }));
2028
+
2029
+ // Track tool calls
2030
+ executedToolCalls.push(...toolCalls);
2031
+
2032
+ // Build tool context
2033
+ const context: ToolContext = {
2034
+ rawText: JSON.stringify(toolUseBlocks),
2035
+ preamble: textAccumulated,
2036
+ depth: toolDepth,
2037
+ previousResults: executedToolResults,
2038
+ accumulated: allTextAccumulated,
2039
+ };
2040
+
2041
+ // Yield control for tool execution
2042
+ const toolCallsEvent: ToolCallsEvent = {
2043
+ type: 'tool-calls',
2044
+ calls: toolCalls,
2045
+ context,
2046
+ };
2047
+
2048
+ const results = await stream.requestToolExecution(toolCallsEvent);
2049
+
2050
+ // Track tool results
2051
+ executedToolResults.push(...results);
2052
+
2053
+ // Add tool results to content blocks
2054
+ for (const result of results) {
2055
+ allContentBlocks.push({
2056
+ type: 'tool_result',
2057
+ toolUseId: result.toolUseId,
2058
+ content: result.content,
2059
+ isError: result.isError,
2060
+ });
2061
+ }
2062
+
2063
+ // Add messages for next iteration
2064
+ messages.push({
2065
+ participant: 'Claude',
2066
+ content: responseBlocks,
2067
+ });
2068
+
2069
+ messages.push({
2070
+ participant: 'User',
2071
+ content: results.map(r => ({
2072
+ type: 'tool_result' as const,
2073
+ toolUseId: r.toolUseId,
2074
+ content: r.content,
2075
+ isError: r.isError,
2076
+ })),
2077
+ });
2078
+
2079
+ toolDepth++;
2080
+ continue;
2081
+ }
2082
+
2083
+ // No more tools, we're done
2084
+ break;
2085
+ }
2086
+
2087
+ const durationMs = Date.now() - startTime;
2088
+
2089
+ const response: NormalizedResponse = {
2090
+ content: allContentBlocks,
2091
+ rawAssistantText: allTextAccumulated,
2092
+ toolCalls: executedToolCalls,
2093
+ toolResults: executedToolResults,
2094
+ stopReason: lastStopReason,
2095
+ usage: totalUsage,
2096
+ details: {
2097
+ stop: {
2098
+ reason: lastStopReason,
2099
+ wasTruncated: lastStopReason === 'max_tokens',
2100
+ },
2101
+ usage: { ...totalUsage },
2102
+ timing: {
2103
+ totalDurationMs: durationMs,
2104
+ attempts: 1,
2105
+ },
2106
+ model: {
2107
+ requested: request.config.model,
2108
+ actual: request.config.model,
2109
+ provider: this.adapter.name,
2110
+ },
2111
+ cache: {
2112
+ markersInRequest: 0,
2113
+ tokensCreated: 0,
2114
+ tokensRead: 0,
2115
+ hitRatio: 0,
2116
+ },
2117
+ },
2118
+ raw: {
2119
+ request: rawRequest,
2120
+ response: rawResponse,
2121
+ },
2122
+ };
2123
+
2124
+ stream.emit({ type: 'complete', response });
2125
+ } catch (error) {
2126
+ if (this.isAbortError(error)) {
2127
+ stream.emit({
2128
+ type: 'aborted',
2129
+ reason: 'user',
2130
+ rawAssistantText: allTextAccumulated,
2131
+ toolCalls: executedToolCalls,
2132
+ toolResults: executedToolResults,
2133
+ });
2134
+ } else {
2135
+ throw error;
2136
+ }
2137
+ }
2138
+ }
1449
2139
  }