@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/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
|
-
|
|
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
|