@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.
- package/dist/membrane.d.ts +37 -0
- package/dist/membrane.d.ts.map +1 -1
- package/dist/membrane.js +590 -1
- package/dist/membrane.js.map +1 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +9 -2
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/mock.d.ts +8 -0
- package/dist/providers/mock.d.ts.map +1 -1
- package/dist/providers/mock.js +39 -2
- package/dist/providers/mock.js.map +1 -1
- package/dist/providers/openai-compatible.d.ts.map +1 -1
- package/dist/providers/openai-compatible.js +5 -1
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +5 -1
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts.map +1 -1
- package/dist/providers/openrouter.js +5 -1
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/yielding-stream.d.ts +167 -0
- package/dist/types/yielding-stream.d.ts.map +1 -0
- package/dist/types/yielding-stream.js +34 -0
- package/dist/types/yielding-stream.js.map +1 -0
- package/dist/yielding-stream.d.ts +60 -0
- package/dist/yielding-stream.d.ts.map +1 -0
- package/dist/yielding-stream.js +204 -0
- package/dist/yielding-stream.js.map +1 -0
- package/package.json +1 -1
- package/src/membrane.ts +729 -2
- package/src/providers/gemini.ts +11 -2
- package/src/providers/mock.ts +47 -2
- package/src/providers/openai-compatible.ts +8 -3
- package/src/providers/openai.ts +8 -3
- package/src/providers/openrouter.ts +8 -3
- package/src/types/index.ts +23 -0
- package/src/types/yielding-stream.ts +228 -0
- 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
|
-
|
|
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
|
}
|