@lleverage-ai/agent-sdk 0.0.7 → 0.0.9
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/agent.d.ts.map +1 -1
- package/dist/agent.js +327 -246
- package/dist/agent.js.map +1 -1
- package/dist/testing/mock-agent.d.ts.map +1 -1
- package/dist/testing/mock-agent.js +7 -0
- package/dist/testing/mock-agent.js.map +1 -1
- package/dist/testing/recorder.d.ts.map +1 -1
- package/dist/testing/recorder.js +3 -0
- package/dist/testing/recorder.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +9 -5
package/dist/agent.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA4DH,OAAO,KAAK,EACV,KAAK,EACL,YAAY,EAwBb,MAAM,YAAY,CAAC;AA6pBpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,KAAK,
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA4DH,OAAO,KAAK,EACV,KAAK,EACL,YAAY,EAwBb,MAAM,YAAY,CAAC;AA6pBpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,KAAK,CAiyFxD"}
|
package/dist/agent.js
CHANGED
|
@@ -1286,6 +1286,250 @@ export function createAgent(options) {
|
|
|
1286
1286
|
}
|
|
1287
1287
|
return null;
|
|
1288
1288
|
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Collect all tools (core + static plugin tools + MCP tools) for
|
|
1291
|
+
* deterministic tool execution during resume. This is the unwrapped set
|
|
1292
|
+
* — no permission mode, hooks, or signal-catching wrappers applied.
|
|
1293
|
+
*/
|
|
1294
|
+
function collectAllTools() {
|
|
1295
|
+
const allTools = { ...coreTools };
|
|
1296
|
+
for (const plugin of options.plugins ?? []) {
|
|
1297
|
+
if (plugin.tools && typeof plugin.tools !== "function") {
|
|
1298
|
+
Object.assign(allTools, plugin.tools);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
Object.assign(allTools, mcpManager.getToolSet());
|
|
1302
|
+
return allTools;
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Shared logic for resume() and resumeDataResponse().
|
|
1306
|
+
*
|
|
1307
|
+
* Validates the checkpoint/interrupt, stores the user response, emits hooks,
|
|
1308
|
+
* executes the interrupted tool (approval or custom), updates the checkpoint,
|
|
1309
|
+
* and returns a discriminated outcome so the caller can decide how to continue.
|
|
1310
|
+
*/
|
|
1311
|
+
async function executeResumeCore(threadId, interruptId, response, genOptions) {
|
|
1312
|
+
if (!options.checkpointer) {
|
|
1313
|
+
throw new Error("Cannot resume: checkpointer is required");
|
|
1314
|
+
}
|
|
1315
|
+
const checkpoint = await options.checkpointer.load(threadId);
|
|
1316
|
+
if (!checkpoint) {
|
|
1317
|
+
throw new Error(`Cannot resume: no checkpoint found for thread ${threadId}`);
|
|
1318
|
+
}
|
|
1319
|
+
const interrupt = checkpoint.pendingInterrupt;
|
|
1320
|
+
if (!interrupt) {
|
|
1321
|
+
throw new Error(`Cannot resume: no pending interrupt found for thread ${threadId}`);
|
|
1322
|
+
}
|
|
1323
|
+
if (interrupt.id !== interruptId) {
|
|
1324
|
+
throw new Error(`Cannot resume: interrupt ID mismatch. Expected ${interrupt.id}, got ${interruptId}`);
|
|
1325
|
+
}
|
|
1326
|
+
// Store the response keyed by interrupt ID (format: "int_<toolCallId>").
|
|
1327
|
+
// The interrupt() function in the tool wrapper looks up responses using
|
|
1328
|
+
// this exact key format, so we must use interrupt.id — NOT the raw
|
|
1329
|
+
// toolCallId which would never match.
|
|
1330
|
+
pendingResponses.set(interrupt.id, response);
|
|
1331
|
+
// Emit InterruptResolved hook
|
|
1332
|
+
const interruptResolvedHooks = effectiveHooks?.InterruptResolved ?? [];
|
|
1333
|
+
if (interruptResolvedHooks.length > 0) {
|
|
1334
|
+
const isApproval = isApprovalInterrupt(interrupt);
|
|
1335
|
+
const approvalResponse = isApproval ? response : undefined;
|
|
1336
|
+
const hookInput = {
|
|
1337
|
+
hook_event_name: "InterruptResolved",
|
|
1338
|
+
session_id: threadId,
|
|
1339
|
+
cwd: process.cwd(),
|
|
1340
|
+
interrupt_id: interrupt.id,
|
|
1341
|
+
interrupt_type: interrupt.type,
|
|
1342
|
+
tool_call_id: interrupt.toolCallId,
|
|
1343
|
+
tool_name: interrupt.toolName,
|
|
1344
|
+
response,
|
|
1345
|
+
approved: approvalResponse?.approved,
|
|
1346
|
+
};
|
|
1347
|
+
await invokeHooksWithTimeout(interruptResolvedHooks, hookInput, null, agent);
|
|
1348
|
+
}
|
|
1349
|
+
// Handle approval interrupt
|
|
1350
|
+
if (isApprovalInterrupt(interrupt)) {
|
|
1351
|
+
const approvalResponse = response;
|
|
1352
|
+
// For backward compatibility, also store in approvalDecisions
|
|
1353
|
+
approvalDecisions.set(interrupt.toolCallId, approvalResponse.approved);
|
|
1354
|
+
// Build the assistant message with the tool call
|
|
1355
|
+
const assistantMessage = {
|
|
1356
|
+
role: "assistant",
|
|
1357
|
+
content: [
|
|
1358
|
+
{
|
|
1359
|
+
type: "tool-call",
|
|
1360
|
+
toolCallId: interrupt.toolCallId,
|
|
1361
|
+
toolName: interrupt.toolName,
|
|
1362
|
+
input: interrupt.request.args,
|
|
1363
|
+
},
|
|
1364
|
+
],
|
|
1365
|
+
};
|
|
1366
|
+
let toolResultOutput;
|
|
1367
|
+
if (approvalResponse.approved) {
|
|
1368
|
+
// Approved: Execute the tool deterministically
|
|
1369
|
+
const unwrappedTools = collectAllTools();
|
|
1370
|
+
const tool = unwrappedTools[interrupt.toolName];
|
|
1371
|
+
if (!tool || !tool.execute) {
|
|
1372
|
+
throw new Error(`Cannot resume: tool "${interrupt.toolName}" not found or has no execute function`);
|
|
1373
|
+
}
|
|
1374
|
+
try {
|
|
1375
|
+
toolResultOutput = await tool.execute(interrupt.request.args, {
|
|
1376
|
+
toolCallId: interrupt.toolCallId,
|
|
1377
|
+
messages: checkpoint.messages,
|
|
1378
|
+
abortSignal: genOptions?.signal,
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
catch (error) {
|
|
1382
|
+
toolResultOutput = `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
else {
|
|
1386
|
+
// Denied: Create a synthetic denial result
|
|
1387
|
+
toolResultOutput = `Tool "${interrupt.toolName}" was denied by user${approvalResponse.reason ? `: ${approvalResponse.reason}` : ""}`;
|
|
1388
|
+
}
|
|
1389
|
+
// Build the tool result message with proper ToolResultOutput format.
|
|
1390
|
+
// The AI SDK validates messages against modelMessageSchema which
|
|
1391
|
+
// requires output to be { type: 'text', value } or { type: 'json', value }.
|
|
1392
|
+
const approvalOutput = typeof toolResultOutput === "string"
|
|
1393
|
+
? { type: "text", value: toolResultOutput }
|
|
1394
|
+
: { type: "json", value: toolResultOutput };
|
|
1395
|
+
const toolResultMessage = {
|
|
1396
|
+
role: "tool",
|
|
1397
|
+
content: [
|
|
1398
|
+
{
|
|
1399
|
+
type: "tool-result",
|
|
1400
|
+
toolCallId: interrupt.toolCallId,
|
|
1401
|
+
toolName: interrupt.toolName,
|
|
1402
|
+
output: approvalOutput,
|
|
1403
|
+
},
|
|
1404
|
+
],
|
|
1405
|
+
};
|
|
1406
|
+
// Update checkpoint with the tool call and result messages, clear interrupt
|
|
1407
|
+
const updatedMessages = [
|
|
1408
|
+
...checkpoint.messages,
|
|
1409
|
+
assistantMessage,
|
|
1410
|
+
toolResultMessage,
|
|
1411
|
+
];
|
|
1412
|
+
const updatedCheckpoint = updateCheckpoint(checkpoint, {
|
|
1413
|
+
messages: updatedMessages,
|
|
1414
|
+
pendingInterrupt: undefined,
|
|
1415
|
+
step: checkpoint.step + 1,
|
|
1416
|
+
});
|
|
1417
|
+
await options.checkpointer.save(updatedCheckpoint);
|
|
1418
|
+
threadCheckpoints.set(threadId, updatedCheckpoint);
|
|
1419
|
+
// Clean up the response from our maps
|
|
1420
|
+
pendingResponses.delete(interrupt.id);
|
|
1421
|
+
approvalDecisions.delete(interrupt.toolCallId);
|
|
1422
|
+
return { type: "continue", threadId, genOptions };
|
|
1423
|
+
}
|
|
1424
|
+
// For custom interrupts (e.g. ask_user), manually execute the tool so
|
|
1425
|
+
// that its interrupt() call receives the stored response deterministically.
|
|
1426
|
+
// Re-running generate() would rely on the model re-calling the same tool,
|
|
1427
|
+
// but tool call IDs change each generation so pendingResponses would never
|
|
1428
|
+
// be matched.
|
|
1429
|
+
const customToolCallId = interrupt.toolCallId;
|
|
1430
|
+
const customToolName = interrupt.toolName;
|
|
1431
|
+
if (!customToolCallId || !customToolName) {
|
|
1432
|
+
throw new Error("Cannot resume custom interrupt: missing toolCallId or toolName on interrupt");
|
|
1433
|
+
}
|
|
1434
|
+
// Build the assistant message with the original tool call
|
|
1435
|
+
const customAssistantMessage = {
|
|
1436
|
+
role: "assistant",
|
|
1437
|
+
content: [
|
|
1438
|
+
{
|
|
1439
|
+
type: "tool-call",
|
|
1440
|
+
toolCallId: customToolCallId,
|
|
1441
|
+
toolName: customToolName,
|
|
1442
|
+
input: interrupt.request,
|
|
1443
|
+
},
|
|
1444
|
+
],
|
|
1445
|
+
};
|
|
1446
|
+
// Collect all tools
|
|
1447
|
+
const customTools = collectAllTools();
|
|
1448
|
+
const customTool = customTools[customToolName];
|
|
1449
|
+
if (!customTool || !customTool.execute) {
|
|
1450
|
+
throw new Error(`Cannot resume: tool "${customToolName}" not found or has no execute function`);
|
|
1451
|
+
}
|
|
1452
|
+
let customToolResult;
|
|
1453
|
+
try {
|
|
1454
|
+
// Execute the tool, providing an interrupt function that returns
|
|
1455
|
+
// the stored user response. This mirrors what happens inside the
|
|
1456
|
+
// permission-mode tool wrapper when pendingResponses has a match.
|
|
1457
|
+
customToolResult = await customTool.execute(interrupt.request, {
|
|
1458
|
+
toolCallId: customToolCallId,
|
|
1459
|
+
messages: checkpoint.messages,
|
|
1460
|
+
abortSignal: genOptions?.signal,
|
|
1461
|
+
interrupt: async (request) => {
|
|
1462
|
+
// First call: return the stored user response (mirrors the
|
|
1463
|
+
// permission-mode wrapper when pendingResponses has a match).
|
|
1464
|
+
if (pendingResponses.has(interrupt.id)) {
|
|
1465
|
+
const stored = pendingResponses.get(interrupt.id);
|
|
1466
|
+
pendingResponses.delete(interrupt.id);
|
|
1467
|
+
return stored;
|
|
1468
|
+
}
|
|
1469
|
+
// Subsequent calls: no stored response — throw InterruptSignal
|
|
1470
|
+
// so the tool can pause again (e.g. multi-step wizards).
|
|
1471
|
+
const newInterruptData = createInterrupt({
|
|
1472
|
+
id: `int_${customToolCallId}`,
|
|
1473
|
+
threadId,
|
|
1474
|
+
type: "custom",
|
|
1475
|
+
toolCallId: customToolCallId,
|
|
1476
|
+
toolName: customToolName,
|
|
1477
|
+
request,
|
|
1478
|
+
step: checkpoint.step,
|
|
1479
|
+
});
|
|
1480
|
+
throw new InterruptSignal(newInterruptData);
|
|
1481
|
+
},
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
catch (executeError) {
|
|
1485
|
+
if (isInterruptSignal(executeError)) {
|
|
1486
|
+
// Tool threw another interrupt — persist it and return re-interrupted
|
|
1487
|
+
const newInterrupt = executeError.interrupt;
|
|
1488
|
+
const reInterruptCheckpoint = updateCheckpoint(checkpoint, {
|
|
1489
|
+
pendingInterrupt: newInterrupt,
|
|
1490
|
+
});
|
|
1491
|
+
await options.checkpointer.save(reInterruptCheckpoint);
|
|
1492
|
+
threadCheckpoints.set(threadId, reInterruptCheckpoint);
|
|
1493
|
+
return {
|
|
1494
|
+
type: "re-interrupted",
|
|
1495
|
+
interrupt: newInterrupt,
|
|
1496
|
+
checkpoint: reInterruptCheckpoint,
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
customToolResult = `Tool execution failed: ${executeError instanceof Error ? executeError.message : String(executeError)}`;
|
|
1500
|
+
}
|
|
1501
|
+
// Build tool result message with proper ToolResultOutput format.
|
|
1502
|
+
const customOutput = typeof customToolResult === "string"
|
|
1503
|
+
? { type: "text", value: customToolResult }
|
|
1504
|
+
: { type: "json", value: customToolResult };
|
|
1505
|
+
const customToolResultMessage = {
|
|
1506
|
+
role: "tool",
|
|
1507
|
+
content: [
|
|
1508
|
+
{
|
|
1509
|
+
type: "tool-result",
|
|
1510
|
+
toolCallId: customToolCallId,
|
|
1511
|
+
toolName: customToolName,
|
|
1512
|
+
output: customOutput,
|
|
1513
|
+
},
|
|
1514
|
+
],
|
|
1515
|
+
};
|
|
1516
|
+
// Update checkpoint with tool call + result, clear interrupt
|
|
1517
|
+
const customUpdatedMessages = [
|
|
1518
|
+
...checkpoint.messages,
|
|
1519
|
+
customAssistantMessage,
|
|
1520
|
+
customToolResultMessage,
|
|
1521
|
+
];
|
|
1522
|
+
const customUpdatedCheckpoint = updateCheckpoint(checkpoint, {
|
|
1523
|
+
messages: customUpdatedMessages,
|
|
1524
|
+
pendingInterrupt: undefined,
|
|
1525
|
+
step: checkpoint.step + 1,
|
|
1526
|
+
});
|
|
1527
|
+
await options.checkpointer.save(customUpdatedCheckpoint);
|
|
1528
|
+
threadCheckpoints.set(threadId, customUpdatedCheckpoint);
|
|
1529
|
+
// Clean up
|
|
1530
|
+
pendingResponses.delete(interrupt.id);
|
|
1531
|
+
return { type: "continue", threadId, genOptions };
|
|
1532
|
+
}
|
|
1289
1533
|
const agent = {
|
|
1290
1534
|
id,
|
|
1291
1535
|
options,
|
|
@@ -1685,9 +1929,10 @@ export function createAgent(options) {
|
|
|
1685
1929
|
const retryState = createRetryLoopState(options.model);
|
|
1686
1930
|
while (retryState.retryAttempt <= retryState.maxRetries) {
|
|
1687
1931
|
try {
|
|
1688
|
-
const { messages, checkpoint } = await buildMessages(effectiveGenOptions);
|
|
1932
|
+
const { messages, checkpoint, forkedSessionId } = await buildMessages(effectiveGenOptions);
|
|
1689
1933
|
const maxSteps = options.maxSteps ?? 10;
|
|
1690
1934
|
const startStep = checkpoint?.step ?? 0;
|
|
1935
|
+
const checkpointThreadId = forkedSessionId ?? effectiveGenOptions.threadId;
|
|
1691
1936
|
// Signal state for cooperative signal catching in streaming mode
|
|
1692
1937
|
const signalState = {};
|
|
1693
1938
|
// Build initial params - use active tools (core + dynamically loaded + task)
|
|
@@ -1786,12 +2031,39 @@ export function createAgent(options) {
|
|
|
1786
2031
|
steps: mapSteps(steps),
|
|
1787
2032
|
};
|
|
1788
2033
|
// Save checkpoint if threadId is provided
|
|
1789
|
-
if (
|
|
2034
|
+
if (checkpointThreadId && options.checkpointer) {
|
|
1790
2035
|
const finalMessages = [
|
|
1791
2036
|
...messages,
|
|
1792
2037
|
...(text ? [{ role: "assistant", content: text }] : []),
|
|
1793
2038
|
];
|
|
1794
|
-
await saveCheckpoint(
|
|
2039
|
+
await saveCheckpoint(checkpointThreadId, finalMessages, startStep + steps.length);
|
|
2040
|
+
}
|
|
2041
|
+
// Save pending interrupt to checkpoint (mirrors generate() pattern)
|
|
2042
|
+
if (signalState.interrupt && checkpointThreadId && options.checkpointer) {
|
|
2043
|
+
const interrupt = signalState.interrupt.interrupt;
|
|
2044
|
+
const savedCheckpoint = threadCheckpoints.get(checkpointThreadId);
|
|
2045
|
+
if (savedCheckpoint) {
|
|
2046
|
+
const withInterrupt = updateCheckpoint(savedCheckpoint, {
|
|
2047
|
+
pendingInterrupt: interrupt,
|
|
2048
|
+
});
|
|
2049
|
+
await options.checkpointer.save(withInterrupt);
|
|
2050
|
+
threadCheckpoints.set(checkpointThreadId, withInterrupt);
|
|
2051
|
+
}
|
|
2052
|
+
// Emit InterruptRequested hook
|
|
2053
|
+
const interruptRequestedHooks = effectiveHooks?.InterruptRequested ?? [];
|
|
2054
|
+
if (interruptRequestedHooks.length > 0) {
|
|
2055
|
+
const hookInput = {
|
|
2056
|
+
hook_event_name: "InterruptRequested",
|
|
2057
|
+
session_id: effectiveGenOptions.threadId ?? "default",
|
|
2058
|
+
cwd: process.cwd(),
|
|
2059
|
+
interrupt_id: interrupt.id,
|
|
2060
|
+
interrupt_type: interrupt.type,
|
|
2061
|
+
tool_call_id: interrupt.toolCallId,
|
|
2062
|
+
tool_name: interrupt.toolName,
|
|
2063
|
+
request: interrupt.request,
|
|
2064
|
+
};
|
|
2065
|
+
await invokeHooksWithTimeout(interruptRequestedHooks, hookInput, null, agent);
|
|
2066
|
+
}
|
|
1795
2067
|
}
|
|
1796
2068
|
// Invoke unified PostGenerate hooks
|
|
1797
2069
|
const postGenerateHooks = effectiveHooks?.PostGenerate ?? [];
|
|
@@ -2383,8 +2655,35 @@ export function createAgent(options) {
|
|
|
2383
2655
|
writer.merge(result.toUIMessageStream());
|
|
2384
2656
|
// Wait for initial generation to complete to get final text
|
|
2385
2657
|
const text = await result.text;
|
|
2658
|
+
// Save pending interrupt to checkpoint (mirrors stream() pattern)
|
|
2659
|
+
if (signalState.interrupt && effectiveGenOptions.threadId && options.checkpointer) {
|
|
2660
|
+
const interrupt = signalState.interrupt.interrupt;
|
|
2661
|
+
const savedCheckpoint = threadCheckpoints.get(effectiveGenOptions.threadId);
|
|
2662
|
+
if (savedCheckpoint) {
|
|
2663
|
+
const withInterrupt = updateCheckpoint(savedCheckpoint, {
|
|
2664
|
+
pendingInterrupt: interrupt,
|
|
2665
|
+
});
|
|
2666
|
+
await options.checkpointer.save(withInterrupt);
|
|
2667
|
+
threadCheckpoints.set(effectiveGenOptions.threadId, withInterrupt);
|
|
2668
|
+
}
|
|
2669
|
+
// Emit InterruptRequested hook
|
|
2670
|
+
const interruptRequestedHooks = effectiveHooks?.InterruptRequested ?? [];
|
|
2671
|
+
if (interruptRequestedHooks.length > 0) {
|
|
2672
|
+
const hookInput = {
|
|
2673
|
+
hook_event_name: "InterruptRequested",
|
|
2674
|
+
session_id: effectiveGenOptions.threadId ?? "default",
|
|
2675
|
+
cwd: process.cwd(),
|
|
2676
|
+
interrupt_id: interrupt.id,
|
|
2677
|
+
interrupt_type: interrupt.type,
|
|
2678
|
+
tool_call_id: interrupt.toolCallId,
|
|
2679
|
+
tool_name: interrupt.toolName,
|
|
2680
|
+
request: interrupt.request,
|
|
2681
|
+
};
|
|
2682
|
+
await invokeHooksWithTimeout(interruptRequestedHooks, hookInput, null, agent);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2386
2685
|
// --- Background task completion loop ---
|
|
2387
|
-
if (waitForBackgroundTasks) {
|
|
2686
|
+
if (waitForBackgroundTasks && !signalState.interrupt) {
|
|
2388
2687
|
// Track accumulated steps for checkpoint saves
|
|
2389
2688
|
const initialSteps = await result.steps;
|
|
2390
2689
|
let accumulatedStepCount = initialSteps.length;
|
|
@@ -2509,251 +2808,33 @@ export function createAgent(options) {
|
|
|
2509
2808
|
return checkpoint?.pendingInterrupt;
|
|
2510
2809
|
},
|
|
2511
2810
|
async resume(threadId, interruptId, response, genOptions) {
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
}
|
|
2519
|
-
const interrupt = checkpoint.pendingInterrupt;
|
|
2520
|
-
if (!interrupt) {
|
|
2521
|
-
throw new Error(`Cannot resume: no pending interrupt found for thread ${threadId}`);
|
|
2522
|
-
}
|
|
2523
|
-
if (interrupt.id !== interruptId) {
|
|
2524
|
-
throw new Error(`Cannot resume: interrupt ID mismatch. Expected ${interrupt.id}, got ${interruptId}`);
|
|
2525
|
-
}
|
|
2526
|
-
// Store the response keyed by interrupt ID (format: "int_<toolCallId>").
|
|
2527
|
-
// The interrupt() function in the tool wrapper looks up responses using
|
|
2528
|
-
// this exact key format, so we must use interrupt.id — NOT the raw
|
|
2529
|
-
// toolCallId which would never match.
|
|
2530
|
-
pendingResponses.set(interrupt.id, response);
|
|
2531
|
-
// Emit InterruptResolved hook
|
|
2532
|
-
const interruptResolvedHooks = effectiveHooks?.InterruptResolved ?? [];
|
|
2533
|
-
if (interruptResolvedHooks.length > 0) {
|
|
2534
|
-
const isApproval = isApprovalInterrupt(interrupt);
|
|
2535
|
-
const approvalResponse = isApproval ? response : undefined;
|
|
2536
|
-
const hookInput = {
|
|
2537
|
-
hook_event_name: "InterruptResolved",
|
|
2538
|
-
session_id: threadId,
|
|
2539
|
-
cwd: process.cwd(),
|
|
2540
|
-
interrupt_id: interrupt.id,
|
|
2541
|
-
interrupt_type: interrupt.type,
|
|
2542
|
-
tool_call_id: interrupt.toolCallId,
|
|
2543
|
-
tool_name: interrupt.toolName,
|
|
2544
|
-
response,
|
|
2545
|
-
approved: approvalResponse?.approved,
|
|
2546
|
-
};
|
|
2547
|
-
await invokeHooksWithTimeout(interruptResolvedHooks, hookInput, null, agent);
|
|
2548
|
-
}
|
|
2549
|
-
// Handle approval interrupt
|
|
2550
|
-
if (isApprovalInterrupt(interrupt)) {
|
|
2551
|
-
const approvalResponse = response;
|
|
2552
|
-
// For backward compatibility, also store in approvalDecisions
|
|
2553
|
-
approvalDecisions.set(interrupt.toolCallId, approvalResponse.approved);
|
|
2554
|
-
// Build the assistant message with the tool call
|
|
2555
|
-
const assistantMessage = {
|
|
2556
|
-
role: "assistant",
|
|
2557
|
-
content: [
|
|
2558
|
-
{
|
|
2559
|
-
type: "tool-call",
|
|
2560
|
-
toolCallId: interrupt.toolCallId,
|
|
2561
|
-
toolName: interrupt.toolName,
|
|
2562
|
-
input: interrupt.request.args,
|
|
2563
|
-
},
|
|
2564
|
-
],
|
|
2565
|
-
};
|
|
2566
|
-
let toolResultOutput;
|
|
2567
|
-
if (approvalResponse.approved) {
|
|
2568
|
-
// Approved: Execute the tool deterministically
|
|
2569
|
-
const unwrappedTools = { ...coreTools };
|
|
2570
|
-
for (const plugin of options.plugins ?? []) {
|
|
2571
|
-
if (plugin.tools && typeof plugin.tools !== "function") {
|
|
2572
|
-
Object.assign(unwrappedTools, plugin.tools);
|
|
2573
|
-
}
|
|
2574
|
-
}
|
|
2575
|
-
const mcpTools = mcpManager.getToolSet();
|
|
2576
|
-
Object.assign(unwrappedTools, mcpTools);
|
|
2577
|
-
const tool = unwrappedTools[interrupt.toolName];
|
|
2578
|
-
if (!tool || !tool.execute) {
|
|
2579
|
-
throw new Error(`Cannot resume: tool "${interrupt.toolName}" not found or has no execute function`);
|
|
2580
|
-
}
|
|
2581
|
-
try {
|
|
2582
|
-
toolResultOutput = await tool.execute(interrupt.request.args, {
|
|
2583
|
-
toolCallId: interrupt.toolCallId,
|
|
2584
|
-
messages: checkpoint.messages,
|
|
2585
|
-
abortSignal: genOptions?.signal,
|
|
2586
|
-
});
|
|
2587
|
-
}
|
|
2588
|
-
catch (error) {
|
|
2589
|
-
toolResultOutput = `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
2590
|
-
}
|
|
2591
|
-
}
|
|
2592
|
-
else {
|
|
2593
|
-
// Denied: Create a synthetic denial result
|
|
2594
|
-
toolResultOutput = `Tool "${interrupt.toolName}" was denied by user${approvalResponse.reason ? `: ${approvalResponse.reason}` : ""}`;
|
|
2595
|
-
}
|
|
2596
|
-
// Build the tool result message with proper ToolResultOutput format.
|
|
2597
|
-
// The AI SDK validates messages against modelMessageSchema which
|
|
2598
|
-
// requires output to be { type: 'text', value } or { type: 'json', value }.
|
|
2599
|
-
const approvalOutput = typeof toolResultOutput === "string"
|
|
2600
|
-
? { type: "text", value: toolResultOutput }
|
|
2601
|
-
: { type: "json", value: toolResultOutput };
|
|
2602
|
-
const toolResultMessage = {
|
|
2603
|
-
role: "tool",
|
|
2604
|
-
content: [
|
|
2605
|
-
{
|
|
2606
|
-
type: "tool-result",
|
|
2607
|
-
toolCallId: interrupt.toolCallId,
|
|
2608
|
-
toolName: interrupt.toolName,
|
|
2609
|
-
output: approvalOutput,
|
|
2610
|
-
},
|
|
2611
|
-
],
|
|
2811
|
+
const outcome = await executeResumeCore(threadId, interruptId, response, genOptions);
|
|
2812
|
+
if (outcome.type === "re-interrupted") {
|
|
2813
|
+
return {
|
|
2814
|
+
status: "interrupted",
|
|
2815
|
+
interrupt: outcome.interrupt,
|
|
2816
|
+
partial: undefined,
|
|
2612
2817
|
};
|
|
2613
|
-
// Update checkpoint with the tool call and result messages, clear interrupt
|
|
2614
|
-
const updatedMessages = [
|
|
2615
|
-
...checkpoint.messages,
|
|
2616
|
-
assistantMessage,
|
|
2617
|
-
toolResultMessage,
|
|
2618
|
-
];
|
|
2619
|
-
const updatedCheckpoint = updateCheckpoint(checkpoint, {
|
|
2620
|
-
messages: updatedMessages,
|
|
2621
|
-
pendingInterrupt: undefined,
|
|
2622
|
-
step: checkpoint.step + 1,
|
|
2623
|
-
});
|
|
2624
|
-
await options.checkpointer.save(updatedCheckpoint);
|
|
2625
|
-
// Clean up the response from our maps
|
|
2626
|
-
pendingResponses.delete(interrupt.id);
|
|
2627
|
-
approvalDecisions.delete(interrupt.toolCallId);
|
|
2628
|
-
// Continue generation from the updated checkpoint
|
|
2629
|
-
return agent.generate({
|
|
2630
|
-
threadId,
|
|
2631
|
-
...genOptions,
|
|
2632
|
-
prompt: undefined,
|
|
2633
|
-
});
|
|
2634
2818
|
}
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
//
|
|
2647
|
-
|
|
2648
|
-
role: "assistant",
|
|
2649
|
-
content: [
|
|
2650
|
-
{
|
|
2651
|
-
type: "tool-call",
|
|
2652
|
-
toolCallId: customToolCallId,
|
|
2653
|
-
toolName: customToolName,
|
|
2654
|
-
input: interrupt.request,
|
|
2655
|
-
},
|
|
2656
|
-
],
|
|
2657
|
-
};
|
|
2658
|
-
// Collect all tools (same pattern as the approval path above)
|
|
2659
|
-
const customTools = { ...coreTools };
|
|
2660
|
-
for (const plugin of options.plugins ?? []) {
|
|
2661
|
-
if (plugin.tools && typeof plugin.tools !== "function") {
|
|
2662
|
-
Object.assign(customTools, plugin.tools);
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
|
-
const customMcpTools = mcpManager.getToolSet();
|
|
2666
|
-
Object.assign(customTools, customMcpTools);
|
|
2667
|
-
const customTool = customTools[customToolName];
|
|
2668
|
-
if (!customTool || !customTool.execute) {
|
|
2669
|
-
throw new Error(`Cannot resume: tool "${customToolName}" not found or has no execute function`);
|
|
2670
|
-
}
|
|
2671
|
-
let customToolResult;
|
|
2672
|
-
try {
|
|
2673
|
-
// Execute the tool, providing an interrupt function that returns
|
|
2674
|
-
// the stored user response. This mirrors what happens inside the
|
|
2675
|
-
// permission-mode tool wrapper when pendingResponses has a match.
|
|
2676
|
-
customToolResult = await customTool.execute(interrupt.request, {
|
|
2677
|
-
toolCallId: customToolCallId,
|
|
2678
|
-
messages: checkpoint.messages,
|
|
2679
|
-
abortSignal: genOptions?.signal,
|
|
2680
|
-
interrupt: async (request) => {
|
|
2681
|
-
// First call: return the stored user response (mirrors the
|
|
2682
|
-
// permission-mode wrapper when pendingResponses has a match).
|
|
2683
|
-
if (pendingResponses.has(interrupt.id)) {
|
|
2684
|
-
const stored = pendingResponses.get(interrupt.id);
|
|
2685
|
-
pendingResponses.delete(interrupt.id);
|
|
2686
|
-
return stored;
|
|
2687
|
-
}
|
|
2688
|
-
// Subsequent calls: no stored response — throw InterruptSignal
|
|
2689
|
-
// so the tool can pause again (e.g. multi-step wizards).
|
|
2690
|
-
const newInterruptData = createInterrupt({
|
|
2691
|
-
id: `int_${customToolCallId}`,
|
|
2692
|
-
threadId,
|
|
2693
|
-
type: "custom",
|
|
2694
|
-
toolCallId: customToolCallId,
|
|
2695
|
-
toolName: customToolName,
|
|
2696
|
-
request,
|
|
2697
|
-
step: checkpoint.step,
|
|
2698
|
-
});
|
|
2699
|
-
throw new InterruptSignal(newInterruptData);
|
|
2700
|
-
},
|
|
2701
|
-
});
|
|
2702
|
-
}
|
|
2703
|
-
catch (executeError) {
|
|
2704
|
-
if (isInterruptSignal(executeError)) {
|
|
2705
|
-
// Tool threw another interrupt — persist it and return interrupted
|
|
2706
|
-
const newInterrupt = executeError.interrupt;
|
|
2707
|
-
const reInterruptCheckpoint = updateCheckpoint(checkpoint, {
|
|
2708
|
-
pendingInterrupt: newInterrupt,
|
|
2709
|
-
});
|
|
2710
|
-
await options.checkpointer.save(reInterruptCheckpoint);
|
|
2711
|
-
threadCheckpoints.set(threadId, reInterruptCheckpoint);
|
|
2712
|
-
return {
|
|
2713
|
-
status: "interrupted",
|
|
2714
|
-
interrupt: newInterrupt,
|
|
2715
|
-
partial: undefined,
|
|
2716
|
-
};
|
|
2717
|
-
}
|
|
2718
|
-
customToolResult = `Tool execution failed: ${executeError instanceof Error ? executeError.message : String(executeError)}`;
|
|
2719
|
-
}
|
|
2720
|
-
// Build tool result message with proper ToolResultOutput format.
|
|
2721
|
-
const customOutput = typeof customToolResult === "string"
|
|
2722
|
-
? { type: "text", value: customToolResult }
|
|
2723
|
-
: { type: "json", value: customToolResult };
|
|
2724
|
-
const customToolResultMessage = {
|
|
2725
|
-
role: "tool",
|
|
2726
|
-
content: [
|
|
2727
|
-
{
|
|
2728
|
-
type: "tool-result",
|
|
2729
|
-
toolCallId: customToolCallId,
|
|
2730
|
-
toolName: customToolName,
|
|
2731
|
-
output: customOutput,
|
|
2732
|
-
},
|
|
2733
|
-
],
|
|
2734
|
-
};
|
|
2735
|
-
// Update checkpoint with tool call + result, clear interrupt
|
|
2736
|
-
const customUpdatedMessages = [
|
|
2737
|
-
...checkpoint.messages,
|
|
2738
|
-
customAssistantMessage,
|
|
2739
|
-
customToolResultMessage,
|
|
2740
|
-
];
|
|
2741
|
-
const customUpdatedCheckpoint = updateCheckpoint(checkpoint, {
|
|
2742
|
-
messages: customUpdatedMessages,
|
|
2743
|
-
pendingInterrupt: undefined,
|
|
2744
|
-
step: checkpoint.step + 1,
|
|
2745
|
-
});
|
|
2746
|
-
await options.checkpointer.save(customUpdatedCheckpoint);
|
|
2747
|
-
threadCheckpoints.set(threadId, customUpdatedCheckpoint);
|
|
2748
|
-
// Clean up
|
|
2749
|
-
pendingResponses.delete(interrupt.id);
|
|
2750
|
-
// Continue generation from the updated checkpoint
|
|
2751
|
-
return agent.generate({
|
|
2752
|
-
threadId,
|
|
2753
|
-
...genOptions,
|
|
2754
|
-
prompt: undefined,
|
|
2755
|
-
});
|
|
2819
|
+
return agent.generate({
|
|
2820
|
+
threadId: outcome.threadId,
|
|
2821
|
+
...outcome.genOptions,
|
|
2822
|
+
prompt: undefined,
|
|
2823
|
+
});
|
|
2824
|
+
},
|
|
2825
|
+
async resumeDataResponse(threadId, interruptId, response, genOptions) {
|
|
2826
|
+
const outcome = await executeResumeCore(threadId, interruptId, response, genOptions);
|
|
2827
|
+
if (outcome.type === "re-interrupted") {
|
|
2828
|
+
// Return an empty response — the client already has the interrupt widget.
|
|
2829
|
+
// The new interrupt is persisted to the checkpoint and retrievable
|
|
2830
|
+
// via getInterrupt().
|
|
2831
|
+
return new Response(null, { status: 204 });
|
|
2756
2832
|
}
|
|
2833
|
+
return agent.streamDataResponse({
|
|
2834
|
+
threadId: outcome.threadId,
|
|
2835
|
+
...outcome.genOptions,
|
|
2836
|
+
prompt: undefined,
|
|
2837
|
+
});
|
|
2757
2838
|
},
|
|
2758
2839
|
async dispose() {
|
|
2759
2840
|
// Kill all running background tasks
|