@lleverage-ai/agent-sdk 0.0.3 → 0.0.5

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 (71) hide show
  1. package/README.md +87 -0
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +594 -70
  4. package/dist/agent.js.map +1 -1
  5. package/dist/hooks.d.ts +28 -1
  6. package/dist/hooks.d.ts.map +1 -1
  7. package/dist/hooks.js +40 -0
  8. package/dist/hooks.js.map +1 -1
  9. package/dist/index.d.ts +4 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +3 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/middleware/apply.d.ts.map +1 -1
  14. package/dist/middleware/apply.js +8 -0
  15. package/dist/middleware/apply.js.map +1 -1
  16. package/dist/middleware/context.d.ts.map +1 -1
  17. package/dist/middleware/context.js +11 -0
  18. package/dist/middleware/context.js.map +1 -1
  19. package/dist/middleware/types.d.ts +8 -0
  20. package/dist/middleware/types.d.ts.map +1 -1
  21. package/dist/plugins/agent-teams/coordinator.d.ts +46 -0
  22. package/dist/plugins/agent-teams/coordinator.d.ts.map +1 -0
  23. package/dist/plugins/agent-teams/coordinator.js +255 -0
  24. package/dist/plugins/agent-teams/coordinator.js.map +1 -0
  25. package/dist/plugins/agent-teams/hooks.d.ts +29 -0
  26. package/dist/plugins/agent-teams/hooks.d.ts.map +1 -0
  27. package/dist/plugins/agent-teams/hooks.js +29 -0
  28. package/dist/plugins/agent-teams/hooks.js.map +1 -0
  29. package/dist/plugins/agent-teams/index.d.ts +59 -0
  30. package/dist/plugins/agent-teams/index.d.ts.map +1 -0
  31. package/dist/plugins/agent-teams/index.js +313 -0
  32. package/dist/plugins/agent-teams/index.js.map +1 -0
  33. package/dist/plugins/agent-teams/mermaid.d.ts +32 -0
  34. package/dist/plugins/agent-teams/mermaid.d.ts.map +1 -0
  35. package/dist/plugins/agent-teams/mermaid.js +66 -0
  36. package/dist/plugins/agent-teams/mermaid.js.map +1 -0
  37. package/dist/plugins/agent-teams/session-runner.d.ts +92 -0
  38. package/dist/plugins/agent-teams/session-runner.d.ts.map +1 -0
  39. package/dist/plugins/agent-teams/session-runner.js +166 -0
  40. package/dist/plugins/agent-teams/session-runner.js.map +1 -0
  41. package/dist/plugins/agent-teams/tools.d.ts +41 -0
  42. package/dist/plugins/agent-teams/tools.d.ts.map +1 -0
  43. package/dist/plugins/agent-teams/tools.js +289 -0
  44. package/dist/plugins/agent-teams/tools.js.map +1 -0
  45. package/dist/plugins/agent-teams/types.d.ts +164 -0
  46. package/dist/plugins/agent-teams/types.d.ts.map +1 -0
  47. package/dist/plugins/agent-teams/types.js +7 -0
  48. package/dist/plugins/agent-teams/types.js.map +1 -0
  49. package/dist/plugins.d.ts.map +1 -1
  50. package/dist/plugins.js +1 -0
  51. package/dist/plugins.js.map +1 -1
  52. package/dist/presets/production.d.ts.map +1 -1
  53. package/dist/presets/production.js +7 -7
  54. package/dist/presets/production.js.map +1 -1
  55. package/dist/task-manager.d.ts +15 -0
  56. package/dist/task-manager.d.ts.map +1 -1
  57. package/dist/task-manager.js +36 -0
  58. package/dist/task-manager.js.map +1 -1
  59. package/dist/testing/mock-agent.d.ts.map +1 -1
  60. package/dist/testing/mock-agent.js +6 -0
  61. package/dist/testing/mock-agent.js.map +1 -1
  62. package/dist/testing/recorder.d.ts.map +1 -1
  63. package/dist/testing/recorder.js +6 -0
  64. package/dist/testing/recorder.js.map +1 -1
  65. package/dist/tools/task.d.ts.map +1 -1
  66. package/dist/tools/task.js +6 -2
  67. package/dist/tools/task.js.map +1 -1
  68. package/dist/types.d.ts +103 -3
  69. package/dist/types.d.ts.map +1 -1
  70. package/dist/types.js.map +1 -1
  71. package/package.json +1 -1
package/dist/agent.js CHANGED
@@ -42,6 +42,46 @@ class InterruptSignal extends Error {
42
42
  function isInterruptSignal(error) {
43
43
  return error instanceof InterruptSignal;
44
44
  }
45
+ /**
46
+ * Outermost tool wrapper that intercepts flow-control signals.
47
+ *
48
+ * When a tool throws `InterruptSignal`, this wrapper catches it before the AI
49
+ * SDK can, stores it in the shared `signalState`, and returns a placeholder
50
+ * string. Combined with a custom `stopWhen` condition, this cleanly stops
51
+ * generation and allows `generate()` to inspect `signalState` in the normal
52
+ * return path (not the catch block).
53
+ *
54
+ * @internal
55
+ */
56
+ function wrapToolsWithSignalCatching(tools, signalState) {
57
+ const wrapped = {};
58
+ for (const [name, toolDef] of Object.entries(tools)) {
59
+ if (!toolDef.execute) {
60
+ wrapped[name] = toolDef;
61
+ continue;
62
+ }
63
+ const originalExecute = toolDef.execute;
64
+ wrapped[name] = {
65
+ ...toolDef,
66
+ execute: async (input, options) => {
67
+ try {
68
+ return await originalExecute.call(toolDef, input, options);
69
+ }
70
+ catch (error) {
71
+ if (isInterruptSignal(error)) {
72
+ if (signalState.interrupt) {
73
+ throw error; // Already have a signal — let AI SDK handle this one
74
+ }
75
+ signalState.interrupt = error;
76
+ return "[Interrupt requested]";
77
+ }
78
+ throw error;
79
+ }
80
+ },
81
+ };
82
+ }
83
+ return wrapped;
84
+ }
45
85
  /**
46
86
  * File edit tool names that get auto-approved in acceptEdits mode.
47
87
  * @internal
@@ -457,17 +497,21 @@ function wrapToolsWithHooks(tools, hookRegistration, agent, sessionId) {
457
497
  return output;
458
498
  }
459
499
  catch (error) {
460
- // Invoke PostToolUseFailure hooks
461
- if (hookRegistration?.PostToolUseFailure?.length) {
462
- const failureInput = {
463
- hook_event_name: "PostToolUseFailure",
464
- session_id: sessionId,
465
- cwd: process.cwd(),
466
- tool_name: name,
467
- tool_input: input,
468
- error: error instanceof Error ? error : new Error(String(error)),
469
- };
470
- await invokeMatchingHooks(hookRegistration.PostToolUseFailure, name, failureInput, toolUseId, agent);
500
+ // Skip PostToolUseFailure for flow-control signals — these are not
501
+ // actual failures but intentional control flow (interrupt).
502
+ if (!isInterruptSignal(error)) {
503
+ // Invoke PostToolUseFailure hooks
504
+ if (hookRegistration?.PostToolUseFailure?.length) {
505
+ const failureInput = {
506
+ hook_event_name: "PostToolUseFailure",
507
+ session_id: sessionId,
508
+ cwd: process.cwd(),
509
+ tool_name: name,
510
+ tool_input: input,
511
+ error: error instanceof Error ? error : new Error(String(error)),
512
+ };
513
+ await invokeMatchingHooks(hookRegistration.PostToolUseFailure, name, failureInput, toolUseId, agent);
514
+ }
471
515
  }
472
516
  throw error;
473
517
  }
@@ -541,7 +585,8 @@ export function createAgent(options) {
541
585
  // Process middleware to get hooks (middleware hooks come before explicit hooks)
542
586
  const middleware = options.middleware ?? [];
543
587
  const middlewareHooks = applyMiddleware(middleware);
544
- const mergedHooks = mergeHooks(middlewareHooks, options.hooks);
588
+ const pluginHooks = (options.plugins ?? []).filter((p) => p.hooks).map((p) => p.hooks);
589
+ const mergedHooks = mergeHooks(middlewareHooks, ...pluginHooks, options.hooks);
545
590
  // Create options with merged hooks for all hook lookups
546
591
  const effectiveHooks = mergedHooks;
547
592
  // Permission mode (mutable for setPermissionMode)
@@ -572,6 +617,18 @@ export function createAgent(options) {
572
617
  }
573
618
  // Initialize task manager for background task tracking
574
619
  const taskManager = new TaskManager();
620
+ // Background task completion options
621
+ const waitForBackgroundTasks = options.waitForBackgroundTasks ?? true;
622
+ const formatTaskCompletion = options.formatTaskCompletion ??
623
+ ((task) => {
624
+ const command = task.metadata?.command ?? "unknown command";
625
+ return `[Background task completed: ${task.id}]\nCommand: ${command}\nOutput:\n${task.result ?? "(no output)"}`;
626
+ });
627
+ const formatTaskFailure = options.formatTaskFailure ??
628
+ ((task) => {
629
+ const command = task.metadata?.command ?? "unknown command";
630
+ return `[Background task failed: ${task.id}]\nCommand: ${command}\nError: ${task.error ?? "Unknown error"}`;
631
+ });
575
632
  // Determine plugin loading mode
576
633
  // Track whether it was explicitly set to distinguish from default
577
634
  const explicitPluginLoading = options.pluginLoading !== undefined;
@@ -828,6 +885,7 @@ export function createAgent(options) {
828
885
  // only advertises tools the agent will actually expose
829
886
  const filteredTools = filterToolsByAllowed((() => {
830
887
  const allTools = { ...coreTools };
888
+ Object.assign(allTools, runtimeTools);
831
889
  Object.assign(allTools, mcpManager.getToolSet());
832
890
  if (toolRegistry) {
833
891
  Object.assign(allTools, toolRegistry.getLoadedTools());
@@ -878,10 +936,14 @@ export function createAgent(options) {
878
936
  // Build prompt using prompt builder
879
937
  return promptBuilder.build(context);
880
938
  };
881
- // Helper to get current active tools (core + MCP + dynamically loaded from registry)
939
+ // Runtime tools added/removed dynamically by plugins at runtime
940
+ const runtimeTools = {};
941
+ // Helper to get current active tools (core + runtime + MCP + dynamically loaded from registry)
882
942
  const getActiveToolSet = (threadId) => {
883
943
  // Start with core tools
884
944
  const allTools = { ...coreTools };
945
+ // Add runtime tools (added by plugins at runtime)
946
+ Object.assign(allTools, runtimeTools);
885
947
  // Add MCP tools from plugin registrations
886
948
  const mcpTools = mcpManager.getToolSet();
887
949
  Object.assign(allTools, mcpTools);
@@ -910,6 +972,8 @@ export function createAgent(options) {
910
972
  const getActiveToolSetWithStreaming = (streamingContext, threadId, step) => {
911
973
  // Start with core tools
912
974
  const allTools = { ...coreTools };
975
+ // Add runtime tools (added by plugins at runtime)
976
+ Object.assign(allTools, runtimeTools);
913
977
  // Process plugins - invoke function-based tools with streaming context
914
978
  for (const plugin of options.plugins ?? []) {
915
979
  if (plugin.tools) {
@@ -1222,6 +1286,34 @@ export function createAgent(options) {
1222
1286
  usage: step.usage,
1223
1287
  }));
1224
1288
  }
1289
+ /**
1290
+ * Get the next actionable task prompt from the background task queue.
1291
+ *
1292
+ * Waits for a task to reach terminal state, skips already-consumed and killed
1293
+ * tasks, formats the result as a prompt, removes the task, and returns it.
1294
+ * Returns null when no more tasks need processing.
1295
+ */
1296
+ async function getNextTaskPrompt() {
1297
+ while (taskManager.hasActiveTasks() || taskManager.hasTerminalTasks()) {
1298
+ const completedTask = await taskManager.waitForNextCompletion();
1299
+ // Dedup: skip if already consumed via task_output tool
1300
+ if (!taskManager.getTask(completedTask.id)) {
1301
+ continue;
1302
+ }
1303
+ // Skip killed tasks (user already knows)
1304
+ if (completedTask.status === "killed") {
1305
+ taskManager.removeTask(completedTask.id);
1306
+ continue;
1307
+ }
1308
+ // Format as follow-up prompt
1309
+ const prompt = completedTask.status === "completed"
1310
+ ? formatTaskCompletion(completedTask)
1311
+ : formatTaskFailure(completedTask);
1312
+ taskManager.removeTask(completedTask.id);
1313
+ return prompt;
1314
+ }
1315
+ return null;
1316
+ }
1225
1317
  const agent = {
1226
1318
  id,
1227
1319
  options,
@@ -1251,9 +1343,17 @@ export function createAgent(options) {
1251
1343
  lastBuiltMessages = messages;
1252
1344
  const maxSteps = options.maxSteps ?? 10;
1253
1345
  const startStep = checkpoint?.step ?? 0;
1346
+ // Shared signal state: flow-control signals (interrupt) thrown by tools
1347
+ // are caught by the outermost wrapper and stored here. A custom
1348
+ // stopWhen condition stops generation after the current step completes.
1349
+ const signalState = {};
1254
1350
  // Build initial params - use active tools (core + dynamically loaded + task)
1255
- // Apply hooks AFTER adding task tool so task tool is also wrapped
1256
- const activeTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSet(effectiveGenOptions.threadId)), effectiveGenOptions.threadId);
1351
+ // Apply hooks AFTER adding task tool so task tool is also wrapped.
1352
+ // Then wrap with signal catching as the outermost layer so that
1353
+ // InterruptSignal is intercepted before the AI SDK can catch it and
1354
+ // convert it to a tool-error result.
1355
+ const hookedTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSet(effectiveGenOptions.threadId)), effectiveGenOptions.threadId);
1356
+ const activeTools = wrapToolsWithSignalCatching(hookedTools, signalState);
1257
1357
  // Build prompt context and generate system prompt
1258
1358
  const promptContext = buildPromptContext(messages, effectiveGenOptions.threadId);
1259
1359
  const systemPrompt = getSystemPrompt(promptContext);
@@ -1268,6 +1368,9 @@ export function createAgent(options) {
1268
1368
  providerOptions: effectiveGenOptions.providerOptions,
1269
1369
  headers: effectiveGenOptions.headers,
1270
1370
  };
1371
+ // Stop condition: stop when an interrupt signal was caught, OR when
1372
+ // the step count reaches maxSteps (whichever comes first).
1373
+ const signalStopCondition = () => signalState.interrupt != null;
1271
1374
  // Execute generation
1272
1375
  const response = await generateText({
1273
1376
  model: retryState.currentModel,
@@ -1278,13 +1381,62 @@ export function createAgent(options) {
1278
1381
  temperature: initialParams.temperature,
1279
1382
  stopSequences: initialParams.stopSequences,
1280
1383
  abortSignal: initialParams.abortSignal,
1281
- stopWhen: stepCountIs(maxSteps),
1384
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
1282
1385
  // Passthrough AI SDK options
1283
1386
  output: effectiveGenOptions.output,
1284
1387
  // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
1285
1388
  providerOptions: initialParams.providerOptions,
1286
1389
  headers: initialParams.headers,
1287
1390
  });
1391
+ // Check for intercepted interrupt signal (cooperative path)
1392
+ if (signalState.interrupt) {
1393
+ const interrupt = signalState.interrupt.interrupt;
1394
+ // Save checkpoint with messages AND the pending interrupt.
1395
+ // The normal completion path saves messages at the end of generate(),
1396
+ // but when interrupted we return early. Without saving here, resume()
1397
+ // cannot find the checkpoint (or finds one without messages/interrupt).
1398
+ if (effectiveGenOptions.threadId && options.checkpointer) {
1399
+ const checkpointThreadId = forkedSessionId ?? effectiveGenOptions.threadId;
1400
+ const finalMessages = [
1401
+ ...messages,
1402
+ { role: "assistant", content: response.text },
1403
+ ];
1404
+ const savedCheckpoint = await saveCheckpoint(checkpointThreadId, finalMessages, startStep + response.steps.length);
1405
+ if (savedCheckpoint) {
1406
+ const withInterrupt = updateCheckpoint(savedCheckpoint, {
1407
+ pendingInterrupt: interrupt,
1408
+ });
1409
+ await options.checkpointer.save(withInterrupt);
1410
+ threadCheckpoints.set(checkpointThreadId, withInterrupt);
1411
+ }
1412
+ }
1413
+ // Emit InterruptRequested hook
1414
+ const interruptRequestedHooks = effectiveHooks?.InterruptRequested ?? [];
1415
+ if (interruptRequestedHooks.length > 0) {
1416
+ const hookInput = {
1417
+ hook_event_name: "InterruptRequested",
1418
+ session_id: effectiveGenOptions.threadId ?? "default",
1419
+ cwd: process.cwd(),
1420
+ interrupt_id: interrupt.id,
1421
+ interrupt_type: interrupt.type,
1422
+ tool_call_id: interrupt.toolCallId,
1423
+ tool_name: interrupt.toolName,
1424
+ request: interrupt.request,
1425
+ };
1426
+ await invokeHooksWithTimeout(interruptRequestedHooks, hookInput, null, agent);
1427
+ }
1428
+ // Return interrupted result with partial results from the response
1429
+ const interruptedResult = {
1430
+ status: "interrupted",
1431
+ interrupt,
1432
+ partial: {
1433
+ text: response.text,
1434
+ steps: mapSteps(response.steps),
1435
+ usage: response.usage,
1436
+ },
1437
+ };
1438
+ return interruptedResult;
1439
+ }
1288
1440
  // Only access output if an output schema was provided
1289
1441
  // (accessing response.output throws AI_NoOutputGeneratedError otherwise)
1290
1442
  let output;
@@ -1342,20 +1494,54 @@ export function createAgent(options) {
1342
1494
  finalResult = updatedResult;
1343
1495
  }
1344
1496
  }
1345
- return finalResult;
1497
+ // --- Background task completion loop ---
1498
+ if (!waitForBackgroundTasks) {
1499
+ return finalResult;
1500
+ }
1501
+ // When checkpointing is active, the checkpoint already contains the
1502
+ // full conversation history (saved above). Passing explicit messages
1503
+ // would cause buildMessages() to load checkpoint messages AND append
1504
+ // the same messages again, causing duplication.
1505
+ const hasCheckpointing = !!(effectiveGenOptions.threadId && options.checkpointer);
1506
+ let lastResult = finalResult;
1507
+ let runningMessages = hasCheckpointing
1508
+ ? []
1509
+ : [...messages, { role: "assistant", content: finalResult.text }];
1510
+ let followUpPrompt = await getNextTaskPrompt();
1511
+ while (followUpPrompt !== null) {
1512
+ lastResult = await agent.generate({
1513
+ ...genOptions,
1514
+ prompt: followUpPrompt,
1515
+ messages: hasCheckpointing ? undefined : runningMessages,
1516
+ });
1517
+ if (lastResult.status === "interrupted") {
1518
+ return lastResult;
1519
+ }
1520
+ if (!hasCheckpointing) {
1521
+ runningMessages = [
1522
+ ...runningMessages,
1523
+ { role: "user", content: followUpPrompt },
1524
+ { role: "assistant", content: lastResult.text },
1525
+ ];
1526
+ }
1527
+ followUpPrompt = await getNextTaskPrompt();
1528
+ }
1529
+ return lastResult;
1346
1530
  }
1347
1531
  catch (error) {
1348
1532
  // Check if this is an InterruptSignal (new interrupt system)
1349
1533
  if (isInterruptSignal(error)) {
1350
1534
  const interrupt = error.interrupt;
1351
- // Save the interrupt to checkpoint
1535
+ // Save checkpoint with messages AND the pending interrupt (catch-block path).
1536
+ // lastBuiltMessages holds the messages built before generateText was called.
1352
1537
  if (effectiveGenOptions.threadId && options.checkpointer) {
1353
- const checkpoint = await options.checkpointer.load(effectiveGenOptions.threadId);
1354
- if (checkpoint) {
1355
- const updatedCheckpoint = updateCheckpoint(checkpoint, {
1538
+ const savedCheckpoint = await saveCheckpoint(effectiveGenOptions.threadId, lastBuiltMessages ?? [], 0);
1539
+ if (savedCheckpoint) {
1540
+ const withInterrupt = updateCheckpoint(savedCheckpoint, {
1356
1541
  pendingInterrupt: interrupt,
1357
1542
  });
1358
- await options.checkpointer.save(updatedCheckpoint);
1543
+ await options.checkpointer.save(withInterrupt);
1544
+ threadCheckpoints.set(effectiveGenOptions.threadId, withInterrupt);
1359
1545
  }
1360
1546
  }
1361
1547
  // Emit InterruptRequested hook
@@ -1521,9 +1707,13 @@ export function createAgent(options) {
1521
1707
  const { messages, checkpoint } = await buildMessages(effectiveGenOptions);
1522
1708
  const maxSteps = options.maxSteps ?? 10;
1523
1709
  const startStep = checkpoint?.step ?? 0;
1710
+ // Signal state for cooperative signal catching in streaming mode
1711
+ const signalState = {};
1524
1712
  // Build initial params - use active tools (core + dynamically loaded + task)
1525
- // Apply hooks AFTER adding task tool so task tool is also wrapped
1526
- const activeTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSet(effectiveGenOptions.threadId)), effectiveGenOptions.threadId);
1713
+ // Apply hooks AFTER adding task tool so task tool is also wrapped.
1714
+ // Then wrap with signal catching as the outermost layer.
1715
+ const hookedTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSet(effectiveGenOptions.threadId)), effectiveGenOptions.threadId);
1716
+ const activeTools = wrapToolsWithSignalCatching(hookedTools, signalState);
1527
1717
  // Build prompt context and generate system prompt
1528
1718
  const promptContext = buildPromptContext(messages, effectiveGenOptions.threadId);
1529
1719
  const systemPrompt = getSystemPrompt(promptContext);
@@ -1538,6 +1728,9 @@ export function createAgent(options) {
1538
1728
  providerOptions: effectiveGenOptions.providerOptions,
1539
1729
  headers: effectiveGenOptions.headers,
1540
1730
  };
1731
+ // Stop condition: stop when an interrupt signal was caught, OR when
1732
+ // the step count reaches maxSteps.
1733
+ const signalStopCondition = () => signalState.interrupt != null;
1541
1734
  // Execute stream
1542
1735
  const response = streamText({
1543
1736
  model: retryState.currentModel,
@@ -1548,7 +1741,7 @@ export function createAgent(options) {
1548
1741
  temperature: initialParams.temperature,
1549
1742
  stopSequences: initialParams.stopSequences,
1550
1743
  abortSignal: initialParams.abortSignal,
1551
- stopWhen: stepCountIs(maxSteps),
1744
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
1552
1745
  // Passthrough AI SDK options
1553
1746
  output: genOptions.output,
1554
1747
  // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
@@ -1632,7 +1825,36 @@ export function createAgent(options) {
1632
1825
  await invokeHooksWithTimeout(postGenerateHooks, postGenerateInput, null, agent);
1633
1826
  // Note: updatedResult is not applied for streaming since the stream has already been sent
1634
1827
  }
1635
- // Success - break out of retry loop
1828
+ // --- Background task completion loop ---
1829
+ if (!waitForBackgroundTasks || signalState.interrupt) {
1830
+ return;
1831
+ }
1832
+ const hasCheckpointing = !!(effectiveGenOptions.threadId && options.checkpointer);
1833
+ let currentMessages = hasCheckpointing
1834
+ ? []
1835
+ : [...messages, { role: "assistant", content: text }];
1836
+ let followUpPrompt = await getNextTaskPrompt();
1837
+ while (followUpPrompt !== null) {
1838
+ const followUpGen = agent.stream({
1839
+ ...genOptions,
1840
+ prompt: followUpPrompt,
1841
+ messages: hasCheckpointing ? undefined : currentMessages,
1842
+ });
1843
+ let followUpText = "";
1844
+ for await (const part of followUpGen) {
1845
+ yield part;
1846
+ if (part.type === "text-delta")
1847
+ followUpText += part.text;
1848
+ }
1849
+ if (!hasCheckpointing) {
1850
+ currentMessages = [
1851
+ ...currentMessages,
1852
+ { role: "user", content: followUpPrompt },
1853
+ { role: "assistant", content: followUpText },
1854
+ ];
1855
+ }
1856
+ followUpPrompt = await getNextTaskPrompt();
1857
+ }
1636
1858
  return;
1637
1859
  }
1638
1860
  catch (error) {
@@ -1682,9 +1904,13 @@ export function createAgent(options) {
1682
1904
  const { messages, checkpoint } = await buildMessages(effectiveGenOptions);
1683
1905
  const maxSteps = options.maxSteps ?? 10;
1684
1906
  const startStep = checkpoint?.step ?? 0;
1907
+ // Signal state for cooperative signal catching in streaming mode
1908
+ const signalState = {};
1685
1909
  // Build initial params - use active tools (core + dynamically loaded + task)
1686
- // Apply hooks AFTER adding task tool so task tool is also wrapped
1687
- const activeTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSet(effectiveGenOptions.threadId)), effectiveGenOptions.threadId);
1910
+ // Apply hooks AFTER adding task tool so task tool is also wrapped.
1911
+ // Then wrap with signal catching as the outermost layer.
1912
+ const hookedTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSet(effectiveGenOptions.threadId)), effectiveGenOptions.threadId);
1913
+ const activeTools = wrapToolsWithSignalCatching(hookedTools, signalState);
1688
1914
  // Build prompt context and generate system prompt
1689
1915
  const promptContext = buildPromptContext(messages, effectiveGenOptions.threadId);
1690
1916
  const systemPrompt = getSystemPrompt(promptContext);
@@ -1699,11 +1925,18 @@ export function createAgent(options) {
1699
1925
  providerOptions: effectiveGenOptions.providerOptions,
1700
1926
  headers: effectiveGenOptions.headers,
1701
1927
  };
1928
+ // Capture currentModel for use in the callback closure
1929
+ const modelToUse = retryState.currentModel;
1930
+ // Stop condition: stop when an interrupt signal was caught, OR when
1931
+ // the step count reaches maxSteps.
1932
+ const signalStopCondition = () => signalState.interrupt != null;
1702
1933
  // Track step count for incremental checkpointing
1703
1934
  let currentStepCount = 0;
1704
- // Execute stream
1935
+ // Execute streamText OUTSIDE createUIMessageStream so errors propagate
1936
+ // to the retry loop (if streamText throws synchronously on creation,
1937
+ // e.g. rate limit, the catch block handles retry/fallback).
1705
1938
  const result = streamText({
1706
- model: retryState.currentModel,
1939
+ model: modelToUse,
1707
1940
  system: initialParams.system,
1708
1941
  messages: initialParams.messages,
1709
1942
  tools: initialParams.tools,
@@ -1711,7 +1944,7 @@ export function createAgent(options) {
1711
1944
  temperature: initialParams.temperature,
1712
1945
  stopSequences: initialParams.stopSequences,
1713
1946
  abortSignal: initialParams.abortSignal,
1714
- stopWhen: stepCountIs(maxSteps),
1947
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
1715
1948
  // Passthrough AI SDK options
1716
1949
  output: effectiveGenOptions.output,
1717
1950
  // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
@@ -1774,10 +2007,93 @@ export function createAgent(options) {
1774
2007
  }
1775
2008
  },
1776
2009
  });
1777
- // Return AI SDK compatible response for use with useChat
1778
- // Note: Not passing originalMessages since ModelMessage[] != UIMessage[]
1779
- // The AI SDK will reconstruct messages from the stream
1780
- return result.toUIMessageStreamResponse();
2010
+ // Use createUIMessageStream to control stream lifecycle for background task follow-ups
2011
+ const stream = createUIMessageStream({
2012
+ execute: async ({ writer }) => {
2013
+ // Merge initial generation into the stream
2014
+ writer.merge(result.toUIMessageStream());
2015
+ // Wait for initial generation to complete to get final text
2016
+ const text = await result.text;
2017
+ // --- Background task completion loop ---
2018
+ if (waitForBackgroundTasks) {
2019
+ // Track accumulated steps for checkpoint saves
2020
+ const initialSteps = await result.steps;
2021
+ let accumulatedStepCount = initialSteps.length;
2022
+ let currentMessages = [
2023
+ ...messages,
2024
+ { role: "assistant", content: text },
2025
+ ];
2026
+ let followUpPrompt = await getNextTaskPrompt();
2027
+ while (followUpPrompt !== null) {
2028
+ // Stream follow-up generation into the same writer
2029
+ const followUpResult = streamText({
2030
+ model: modelToUse,
2031
+ system: initialParams.system,
2032
+ messages: [
2033
+ ...currentMessages,
2034
+ { role: "user", content: followUpPrompt },
2035
+ ],
2036
+ tools: initialParams.tools,
2037
+ maxOutputTokens: initialParams.maxTokens,
2038
+ temperature: initialParams.temperature,
2039
+ stopSequences: initialParams.stopSequences,
2040
+ abortSignal: initialParams.abortSignal,
2041
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
2042
+ // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
2043
+ providerOptions: initialParams.providerOptions,
2044
+ headers: initialParams.headers,
2045
+ });
2046
+ writer.merge(followUpResult.toUIMessageStream());
2047
+ const followUpText = await followUpResult.text;
2048
+ currentMessages = [
2049
+ ...currentMessages,
2050
+ { role: "user", content: followUpPrompt },
2051
+ { role: "assistant", content: followUpText },
2052
+ ];
2053
+ // --- Post-completion bookkeeping for follow-ups ---
2054
+ const followUpSteps = await followUpResult.steps;
2055
+ accumulatedStepCount += followUpSteps.length;
2056
+ // Checkpoint save
2057
+ if (effectiveGenOptions.threadId && options.checkpointer) {
2058
+ await saveCheckpoint(effectiveGenOptions.threadId, currentMessages, startStep + accumulatedStepCount);
2059
+ }
2060
+ // Context manager update
2061
+ const followUpUsage = await followUpResult.usage;
2062
+ if (options.contextManager?.updateUsage && followUpUsage) {
2063
+ options.contextManager.updateUsage({
2064
+ inputTokens: followUpUsage.inputTokens,
2065
+ outputTokens: followUpUsage.outputTokens,
2066
+ totalTokens: followUpUsage.totalTokens,
2067
+ });
2068
+ }
2069
+ // PostGenerate hooks
2070
+ const followUpPostGenerateHooks = effectiveHooks?.PostGenerate ?? [];
2071
+ if (followUpPostGenerateHooks.length > 0) {
2072
+ const followUpFinishReason = await followUpResult.finishReason;
2073
+ const followUpHookResult = {
2074
+ status: "complete",
2075
+ text: followUpText,
2076
+ usage: followUpUsage,
2077
+ finishReason: followUpFinishReason,
2078
+ output: undefined,
2079
+ steps: mapSteps(followUpSteps),
2080
+ };
2081
+ const followUpPostGenerateInput = {
2082
+ hook_event_name: "PostGenerate",
2083
+ session_id: effectiveGenOptions.threadId ?? "default",
2084
+ cwd: process.cwd(),
2085
+ options: effectiveGenOptions,
2086
+ result: followUpHookResult,
2087
+ };
2088
+ await invokeHooksWithTimeout(followUpPostGenerateHooks, followUpPostGenerateInput, null, agent);
2089
+ }
2090
+ followUpPrompt = await getNextTaskPrompt();
2091
+ }
2092
+ }
2093
+ },
2094
+ });
2095
+ // Convert the stream to a Response
2096
+ return createUIMessageStreamResponse({ stream });
1781
2097
  }
1782
2098
  catch (error) {
1783
2099
  // Normalize error to AgentError
@@ -1816,9 +2132,13 @@ export function createAgent(options) {
1816
2132
  const { messages, checkpoint } = await buildMessages(effectiveGenOptions);
1817
2133
  const maxSteps = options.maxSteps ?? 10;
1818
2134
  const startStep = checkpoint?.step ?? 0;
2135
+ // Signal state for cooperative signal catching in streaming mode
2136
+ const signalState = {};
1819
2137
  // Build initial params - use active tools (core + dynamically loaded + task)
1820
- // Apply hooks AFTER adding task tool so task tool is also wrapped
1821
- const activeTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSet(effectiveGenOptions.threadId)), effectiveGenOptions.threadId);
2138
+ // Apply hooks AFTER adding task tool so task tool is also wrapped.
2139
+ // Then wrap with signal catching as the outermost layer.
2140
+ const hookedTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSet(effectiveGenOptions.threadId)), effectiveGenOptions.threadId);
2141
+ const activeTools = wrapToolsWithSignalCatching(hookedTools, signalState);
1822
2142
  // Build prompt context and generate system prompt
1823
2143
  const promptContext = buildPromptContext(messages, effectiveGenOptions.threadId);
1824
2144
  const systemPrompt = getSystemPrompt(promptContext);
@@ -1835,6 +2155,9 @@ export function createAgent(options) {
1835
2155
  };
1836
2156
  // Track step count for incremental checkpointing
1837
2157
  let currentStepCount = 0;
2158
+ // Stop condition: stop when an interrupt signal was caught, OR when
2159
+ // the step count reaches maxSteps.
2160
+ const signalStopCondition = () => signalState.interrupt != null;
1838
2161
  // Execute stream
1839
2162
  const result = streamText({
1840
2163
  model: retryState.currentModel,
@@ -1845,7 +2168,7 @@ export function createAgent(options) {
1845
2168
  temperature: initialParams.temperature,
1846
2169
  stopSequences: initialParams.stopSequences,
1847
2170
  abortSignal: initialParams.abortSignal,
1848
- stopWhen: stepCountIs(maxSteps),
2171
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
1849
2172
  // Passthrough AI SDK options
1850
2173
  output: effectiveGenOptions.output,
1851
2174
  // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
@@ -1968,9 +2291,13 @@ export function createAgent(options) {
1968
2291
  }
1969
2292
  // Create streaming context for tools
1970
2293
  const streamingContext = { writer };
2294
+ // Signal state for cooperative signal catching in streaming mode
2295
+ const signalState = {};
1971
2296
  // Build tools with streaming context and task tool
1972
- // Apply hooks AFTER adding task tool so task tool is also wrapped
1973
- const streamingTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSetWithStreaming(streamingContext, effectiveGenOptions.threadId), streamingContext), effectiveGenOptions.threadId);
2297
+ // Apply hooks AFTER adding task tool so task tool is also wrapped.
2298
+ // Then wrap with signal catching as the outermost layer.
2299
+ const hookedStreamingTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSetWithStreaming(streamingContext, effectiveGenOptions.threadId), streamingContext), effectiveGenOptions.threadId);
2300
+ const streamingTools = wrapToolsWithSignalCatching(hookedStreamingTools, signalState);
1974
2301
  // Build prompt context and generate system prompt
1975
2302
  const promptContext = buildPromptContext(messages, effectiveGenOptions.threadId);
1976
2303
  const systemPrompt = getSystemPrompt(promptContext);
@@ -1988,6 +2315,9 @@ export function createAgent(options) {
1988
2315
  };
1989
2316
  // Track step count for incremental checkpointing
1990
2317
  let currentStepCount = 0;
2318
+ // Stop condition: stop when a flow-control signal was caught, OR when
2319
+ // the step count reaches maxSteps.
2320
+ const signalStopCondition = () => signalState.interrupt != null;
1991
2321
  // Execute stream
1992
2322
  const result = streamText({
1993
2323
  model: modelToUse,
@@ -1998,7 +2328,7 @@ export function createAgent(options) {
1998
2328
  temperature: initialParams.temperature,
1999
2329
  stopSequences: initialParams.stopSequences,
2000
2330
  abortSignal: initialParams.abortSignal,
2001
- stopWhen: stepCountIs(maxSteps),
2331
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
2002
2332
  // Passthrough AI SDK options
2003
2333
  output: effectiveGenOptions.output,
2004
2334
  // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
@@ -2066,6 +2396,84 @@ export function createAgent(options) {
2066
2396
  });
2067
2397
  // Merge the streamText output into the UI message stream
2068
2398
  writer.merge(result.toUIMessageStream());
2399
+ // Wait for initial generation to complete to get final text
2400
+ const text = await result.text;
2401
+ // --- Background task completion loop ---
2402
+ if (waitForBackgroundTasks) {
2403
+ // Track accumulated steps for checkpoint saves
2404
+ const initialSteps = await result.steps;
2405
+ let accumulatedStepCount = initialSteps.length;
2406
+ let currentMessages = [
2407
+ ...messages,
2408
+ { role: "assistant", content: text },
2409
+ ];
2410
+ let followUpPrompt = await getNextTaskPrompt();
2411
+ while (followUpPrompt !== null) {
2412
+ // Stream follow-up generation into the same writer
2413
+ const followUpResult = streamText({
2414
+ model: modelToUse,
2415
+ system: initialParams.system,
2416
+ messages: [
2417
+ ...currentMessages,
2418
+ { role: "user", content: followUpPrompt },
2419
+ ],
2420
+ tools: initialParams.tools,
2421
+ maxOutputTokens: initialParams.maxTokens,
2422
+ temperature: initialParams.temperature,
2423
+ stopSequences: initialParams.stopSequences,
2424
+ abortSignal: initialParams.abortSignal,
2425
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
2426
+ // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
2427
+ providerOptions: initialParams.providerOptions,
2428
+ headers: initialParams.headers,
2429
+ });
2430
+ writer.merge(followUpResult.toUIMessageStream());
2431
+ const followUpText = await followUpResult.text;
2432
+ currentMessages = [
2433
+ ...currentMessages,
2434
+ { role: "user", content: followUpPrompt },
2435
+ { role: "assistant", content: followUpText },
2436
+ ];
2437
+ // --- Post-completion bookkeeping for follow-ups ---
2438
+ const followUpSteps = await followUpResult.steps;
2439
+ accumulatedStepCount += followUpSteps.length;
2440
+ // Checkpoint save
2441
+ if (effectiveGenOptions.threadId && options.checkpointer) {
2442
+ await saveCheckpoint(effectiveGenOptions.threadId, currentMessages, startStep + accumulatedStepCount);
2443
+ }
2444
+ // Context manager update
2445
+ const followUpUsage = await followUpResult.usage;
2446
+ if (options.contextManager?.updateUsage && followUpUsage) {
2447
+ options.contextManager.updateUsage({
2448
+ inputTokens: followUpUsage.inputTokens,
2449
+ outputTokens: followUpUsage.outputTokens,
2450
+ totalTokens: followUpUsage.totalTokens,
2451
+ });
2452
+ }
2453
+ // PostGenerate hooks
2454
+ const followUpPostGenerateHooks = effectiveHooks?.PostGenerate ?? [];
2455
+ if (followUpPostGenerateHooks.length > 0) {
2456
+ const followUpFinishReason = await followUpResult.finishReason;
2457
+ const followUpHookResult = {
2458
+ status: "complete",
2459
+ text: followUpText,
2460
+ usage: followUpUsage,
2461
+ finishReason: followUpFinishReason,
2462
+ output: undefined,
2463
+ steps: mapSteps(followUpSteps),
2464
+ };
2465
+ const followUpPostGenerateInput = {
2466
+ hook_event_name: "PostGenerate",
2467
+ session_id: effectiveGenOptions.threadId ?? "default",
2468
+ cwd: process.cwd(),
2469
+ options: effectiveGenOptions,
2470
+ result: followUpHookResult,
2471
+ };
2472
+ await invokeHooksWithTimeout(followUpPostGenerateHooks, followUpPostGenerateInput, null, agent);
2473
+ }
2474
+ followUpPrompt = await getNextTaskPrompt();
2475
+ }
2476
+ }
2069
2477
  },
2070
2478
  });
2071
2479
  // Convert the stream to a Response
@@ -2095,6 +2503,14 @@ export function createAgent(options) {
2095
2503
  getActiveTools() {
2096
2504
  return getActiveToolSet();
2097
2505
  },
2506
+ addRuntimeTools(tools) {
2507
+ Object.assign(runtimeTools, tools);
2508
+ },
2509
+ removeRuntimeTools(toolNames) {
2510
+ for (const name of toolNames) {
2511
+ delete runtimeTools[name];
2512
+ }
2513
+ },
2098
2514
  loadTools(toolNames) {
2099
2515
  if (!toolRegistry) {
2100
2516
  // No registry in eager mode - all tools already loaded
@@ -2131,11 +2547,11 @@ export function createAgent(options) {
2131
2547
  if (interrupt.id !== interruptId) {
2132
2548
  throw new Error(`Cannot resume: interrupt ID mismatch. Expected ${interrupt.id}, got ${interruptId}`);
2133
2549
  }
2134
- // Store the response for the tool wrapper to use
2135
- const toolCallId = interrupt.toolCallId;
2136
- if (toolCallId) {
2137
- pendingResponses.set(toolCallId, response);
2138
- }
2550
+ // Store the response keyed by interrupt ID (format: "int_<toolCallId>").
2551
+ // The interrupt() function in the tool wrapper looks up responses using
2552
+ // this exact key format, so we must use interrupt.id — NOT the raw
2553
+ // toolCallId which would never match.
2554
+ pendingResponses.set(interrupt.id, response);
2139
2555
  // Emit InterruptResolved hook
2140
2556
  const interruptResolvedHooks = effectiveHooks?.InterruptResolved ?? [];
2141
2557
  if (interruptResolvedHooks.length > 0) {
@@ -2194,20 +2610,19 @@ export function createAgent(options) {
2194
2610
  });
2195
2611
  }
2196
2612
  catch (error) {
2197
- toolResultOutput = {
2198
- error: true,
2199
- message: error instanceof Error ? error.message : String(error),
2200
- };
2613
+ toolResultOutput = `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`;
2201
2614
  }
2202
2615
  }
2203
2616
  else {
2204
2617
  // Denied: Create a synthetic denial result
2205
- toolResultOutput = {
2206
- denied: true,
2207
- message: `Tool "${interrupt.toolName}" was denied by user${approvalResponse.reason ? `: ${approvalResponse.reason}` : ""}`,
2208
- };
2618
+ toolResultOutput = `Tool "${interrupt.toolName}" was denied by user${approvalResponse.reason ? `: ${approvalResponse.reason}` : ""}`;
2209
2619
  }
2210
- // Build the tool result message
2620
+ // Build the tool result message with proper ToolResultOutput format.
2621
+ // The AI SDK validates messages against modelMessageSchema which
2622
+ // requires output to be { type: 'text', value } or { type: 'json', value }.
2623
+ const approvalOutput = typeof toolResultOutput === "string"
2624
+ ? { type: "text", value: toolResultOutput }
2625
+ : { type: "json", value: toolResultOutput };
2211
2626
  const toolResultMessage = {
2212
2627
  role: "tool",
2213
2628
  content: [
@@ -2215,7 +2630,7 @@ export function createAgent(options) {
2215
2630
  type: "tool-result",
2216
2631
  toolCallId: interrupt.toolCallId,
2217
2632
  toolName: interrupt.toolName,
2218
- output: toolResultOutput,
2633
+ output: approvalOutput,
2219
2634
  },
2220
2635
  ],
2221
2636
  };
@@ -2232,7 +2647,7 @@ export function createAgent(options) {
2232
2647
  });
2233
2648
  await options.checkpointer.save(updatedCheckpoint);
2234
2649
  // Clean up the response from our maps
2235
- pendingResponses.delete(interrupt.toolCallId);
2650
+ pendingResponses.delete(interrupt.id);
2236
2651
  approvalDecisions.delete(interrupt.toolCallId);
2237
2652
  // Continue generation from the updated checkpoint
2238
2653
  return agent.generate({
@@ -2241,19 +2656,128 @@ export function createAgent(options) {
2241
2656
  prompt: undefined,
2242
2657
  });
2243
2658
  }
2244
- // For custom interrupts, the tool's interrupt() call will receive the response
2245
- // Store it and re-run generation - the tool will get the response
2246
- // Clear the interrupt from checkpoint before continuing
2247
- const updatedCheckpoint = updateCheckpoint(checkpoint, {
2248
- pendingInterrupt: undefined,
2249
- });
2250
- await options.checkpointer.save(updatedCheckpoint);
2251
- // Continue generation - the wrapped tool will pick up the response from pendingResponses
2252
- return agent.generate({
2253
- threadId,
2254
- ...genOptions,
2255
- prompt: undefined,
2256
- });
2659
+ // For custom interrupts (e.g. ask_user), manually execute the tool so
2660
+ // that its interrupt() call receives the stored response deterministically.
2661
+ // Re-running generate() would rely on the model re-calling the same tool,
2662
+ // but tool call IDs change each generation so pendingResponses would never
2663
+ // be matched.
2664
+ {
2665
+ const customToolCallId = interrupt.toolCallId;
2666
+ const customToolName = interrupt.toolName;
2667
+ if (!customToolCallId || !customToolName) {
2668
+ throw new Error("Cannot resume custom interrupt: missing toolCallId or toolName on interrupt");
2669
+ }
2670
+ // Build the assistant message with the original tool call
2671
+ const customAssistantMessage = {
2672
+ role: "assistant",
2673
+ content: [
2674
+ {
2675
+ type: "tool-call",
2676
+ toolCallId: customToolCallId,
2677
+ toolName: customToolName,
2678
+ input: interrupt.request,
2679
+ },
2680
+ ],
2681
+ };
2682
+ // Collect all tools (same pattern as the approval path above)
2683
+ const customTools = { ...coreTools };
2684
+ for (const plugin of options.plugins ?? []) {
2685
+ if (plugin.tools && typeof plugin.tools !== "function") {
2686
+ Object.assign(customTools, plugin.tools);
2687
+ }
2688
+ }
2689
+ const customMcpTools = mcpManager.getToolSet();
2690
+ Object.assign(customTools, customMcpTools);
2691
+ const customTool = customTools[customToolName];
2692
+ if (!customTool || !customTool.execute) {
2693
+ throw new Error(`Cannot resume: tool "${customToolName}" not found or has no execute function`);
2694
+ }
2695
+ let customToolResult;
2696
+ try {
2697
+ // Execute the tool, providing an interrupt function that returns
2698
+ // the stored user response. This mirrors what happens inside the
2699
+ // permission-mode tool wrapper when pendingResponses has a match.
2700
+ customToolResult = await customTool.execute(interrupt.request, {
2701
+ toolCallId: customToolCallId,
2702
+ messages: checkpoint.messages,
2703
+ abortSignal: genOptions?.signal,
2704
+ interrupt: async (request) => {
2705
+ // First call: return the stored user response (mirrors the
2706
+ // permission-mode wrapper when pendingResponses has a match).
2707
+ if (pendingResponses.has(interrupt.id)) {
2708
+ const stored = pendingResponses.get(interrupt.id);
2709
+ pendingResponses.delete(interrupt.id);
2710
+ return stored;
2711
+ }
2712
+ // Subsequent calls: no stored response — throw InterruptSignal
2713
+ // so the tool can pause again (e.g. multi-step wizards).
2714
+ const newInterruptData = createInterrupt({
2715
+ id: `int_${customToolCallId}`,
2716
+ threadId,
2717
+ type: "custom",
2718
+ toolCallId: customToolCallId,
2719
+ toolName: customToolName,
2720
+ request,
2721
+ step: checkpoint.step,
2722
+ });
2723
+ throw new InterruptSignal(newInterruptData);
2724
+ },
2725
+ });
2726
+ }
2727
+ catch (executeError) {
2728
+ if (isInterruptSignal(executeError)) {
2729
+ // Tool threw another interrupt — persist it and return interrupted
2730
+ const newInterrupt = executeError.interrupt;
2731
+ const reInterruptCheckpoint = updateCheckpoint(checkpoint, {
2732
+ pendingInterrupt: newInterrupt,
2733
+ });
2734
+ await options.checkpointer.save(reInterruptCheckpoint);
2735
+ threadCheckpoints.set(threadId, reInterruptCheckpoint);
2736
+ return {
2737
+ status: "interrupted",
2738
+ interrupt: newInterrupt,
2739
+ partial: undefined,
2740
+ };
2741
+ }
2742
+ customToolResult = `Tool execution failed: ${executeError instanceof Error ? executeError.message : String(executeError)}`;
2743
+ }
2744
+ // Build tool result message with proper ToolResultOutput format.
2745
+ const customOutput = typeof customToolResult === "string"
2746
+ ? { type: "text", value: customToolResult }
2747
+ : { type: "json", value: customToolResult };
2748
+ const customToolResultMessage = {
2749
+ role: "tool",
2750
+ content: [
2751
+ {
2752
+ type: "tool-result",
2753
+ toolCallId: customToolCallId,
2754
+ toolName: customToolName,
2755
+ output: customOutput,
2756
+ },
2757
+ ],
2758
+ };
2759
+ // Update checkpoint with tool call + result, clear interrupt
2760
+ const customUpdatedMessages = [
2761
+ ...checkpoint.messages,
2762
+ customAssistantMessage,
2763
+ customToolResultMessage,
2764
+ ];
2765
+ const customUpdatedCheckpoint = updateCheckpoint(checkpoint, {
2766
+ messages: customUpdatedMessages,
2767
+ pendingInterrupt: undefined,
2768
+ step: checkpoint.step + 1,
2769
+ });
2770
+ await options.checkpointer.save(customUpdatedCheckpoint);
2771
+ threadCheckpoints.set(threadId, customUpdatedCheckpoint);
2772
+ // Clean up
2773
+ pendingResponses.delete(interrupt.id);
2774
+ // Continue generation from the updated checkpoint
2775
+ return agent.generate({
2776
+ threadId,
2777
+ ...genOptions,
2778
+ prompt: undefined,
2779
+ });
2780
+ }
2257
2781
  },
2258
2782
  async dispose() {
2259
2783
  // Kill all running background tasks