@lleverage-ai/agent-sdk 0.0.3 → 0.0.4

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 +441 -36
  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,53 @@ 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 the interrupt to checkpoint
1395
+ if (effectiveGenOptions.threadId && options.checkpointer) {
1396
+ const checkpoint = await options.checkpointer.load(effectiveGenOptions.threadId);
1397
+ if (checkpoint) {
1398
+ const updatedCheckpoint = updateCheckpoint(checkpoint, {
1399
+ pendingInterrupt: interrupt,
1400
+ });
1401
+ await options.checkpointer.save(updatedCheckpoint);
1402
+ }
1403
+ }
1404
+ // Emit InterruptRequested hook
1405
+ const interruptRequestedHooks = effectiveHooks?.InterruptRequested ?? [];
1406
+ if (interruptRequestedHooks.length > 0) {
1407
+ const hookInput = {
1408
+ hook_event_name: "InterruptRequested",
1409
+ session_id: effectiveGenOptions.threadId ?? "default",
1410
+ cwd: process.cwd(),
1411
+ interrupt_id: interrupt.id,
1412
+ interrupt_type: interrupt.type,
1413
+ tool_call_id: interrupt.toolCallId,
1414
+ tool_name: interrupt.toolName,
1415
+ request: interrupt.request,
1416
+ };
1417
+ await invokeHooksWithTimeout(interruptRequestedHooks, hookInput, null, agent);
1418
+ }
1419
+ // Return interrupted result with partial results from the response
1420
+ const interruptedResult = {
1421
+ status: "interrupted",
1422
+ interrupt,
1423
+ partial: {
1424
+ text: response.text,
1425
+ steps: mapSteps(response.steps),
1426
+ usage: response.usage,
1427
+ },
1428
+ };
1429
+ return interruptedResult;
1430
+ }
1288
1431
  // Only access output if an output schema was provided
1289
1432
  // (accessing response.output throws AI_NoOutputGeneratedError otherwise)
1290
1433
  let output;
@@ -1342,7 +1485,39 @@ export function createAgent(options) {
1342
1485
  finalResult = updatedResult;
1343
1486
  }
1344
1487
  }
1345
- return finalResult;
1488
+ // --- Background task completion loop ---
1489
+ if (!waitForBackgroundTasks) {
1490
+ return finalResult;
1491
+ }
1492
+ // When checkpointing is active, the checkpoint already contains the
1493
+ // full conversation history (saved above). Passing explicit messages
1494
+ // would cause buildMessages() to load checkpoint messages AND append
1495
+ // the same messages again, causing duplication.
1496
+ const hasCheckpointing = !!(effectiveGenOptions.threadId && options.checkpointer);
1497
+ let lastResult = finalResult;
1498
+ let runningMessages = hasCheckpointing
1499
+ ? []
1500
+ : [...messages, { role: "assistant", content: finalResult.text }];
1501
+ let followUpPrompt = await getNextTaskPrompt();
1502
+ while (followUpPrompt !== null) {
1503
+ lastResult = await agent.generate({
1504
+ ...genOptions,
1505
+ prompt: followUpPrompt,
1506
+ messages: hasCheckpointing ? undefined : runningMessages,
1507
+ });
1508
+ if (lastResult.status === "interrupted") {
1509
+ return lastResult;
1510
+ }
1511
+ if (!hasCheckpointing) {
1512
+ runningMessages = [
1513
+ ...runningMessages,
1514
+ { role: "user", content: followUpPrompt },
1515
+ { role: "assistant", content: lastResult.text },
1516
+ ];
1517
+ }
1518
+ followUpPrompt = await getNextTaskPrompt();
1519
+ }
1520
+ return lastResult;
1346
1521
  }
1347
1522
  catch (error) {
1348
1523
  // Check if this is an InterruptSignal (new interrupt system)
@@ -1521,9 +1696,13 @@ export function createAgent(options) {
1521
1696
  const { messages, checkpoint } = await buildMessages(effectiveGenOptions);
1522
1697
  const maxSteps = options.maxSteps ?? 10;
1523
1698
  const startStep = checkpoint?.step ?? 0;
1699
+ // Signal state for cooperative signal catching in streaming mode
1700
+ const signalState = {};
1524
1701
  // 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);
1702
+ // Apply hooks AFTER adding task tool so task tool is also wrapped.
1703
+ // Then wrap with signal catching as the outermost layer.
1704
+ const hookedTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSet(effectiveGenOptions.threadId)), effectiveGenOptions.threadId);
1705
+ const activeTools = wrapToolsWithSignalCatching(hookedTools, signalState);
1527
1706
  // Build prompt context and generate system prompt
1528
1707
  const promptContext = buildPromptContext(messages, effectiveGenOptions.threadId);
1529
1708
  const systemPrompt = getSystemPrompt(promptContext);
@@ -1538,6 +1717,9 @@ export function createAgent(options) {
1538
1717
  providerOptions: effectiveGenOptions.providerOptions,
1539
1718
  headers: effectiveGenOptions.headers,
1540
1719
  };
1720
+ // Stop condition: stop when an interrupt signal was caught, OR when
1721
+ // the step count reaches maxSteps.
1722
+ const signalStopCondition = () => signalState.interrupt != null;
1541
1723
  // Execute stream
1542
1724
  const response = streamText({
1543
1725
  model: retryState.currentModel,
@@ -1548,7 +1730,7 @@ export function createAgent(options) {
1548
1730
  temperature: initialParams.temperature,
1549
1731
  stopSequences: initialParams.stopSequences,
1550
1732
  abortSignal: initialParams.abortSignal,
1551
- stopWhen: stepCountIs(maxSteps),
1733
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
1552
1734
  // Passthrough AI SDK options
1553
1735
  output: genOptions.output,
1554
1736
  // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
@@ -1632,7 +1814,36 @@ export function createAgent(options) {
1632
1814
  await invokeHooksWithTimeout(postGenerateHooks, postGenerateInput, null, agent);
1633
1815
  // Note: updatedResult is not applied for streaming since the stream has already been sent
1634
1816
  }
1635
- // Success - break out of retry loop
1817
+ // --- Background task completion loop ---
1818
+ if (!waitForBackgroundTasks || signalState.interrupt) {
1819
+ return;
1820
+ }
1821
+ const hasCheckpointing = !!(effectiveGenOptions.threadId && options.checkpointer);
1822
+ let currentMessages = hasCheckpointing
1823
+ ? []
1824
+ : [...messages, { role: "assistant", content: text }];
1825
+ let followUpPrompt = await getNextTaskPrompt();
1826
+ while (followUpPrompt !== null) {
1827
+ const followUpGen = agent.stream({
1828
+ ...genOptions,
1829
+ prompt: followUpPrompt,
1830
+ messages: hasCheckpointing ? undefined : currentMessages,
1831
+ });
1832
+ let followUpText = "";
1833
+ for await (const part of followUpGen) {
1834
+ yield part;
1835
+ if (part.type === "text-delta")
1836
+ followUpText += part.text;
1837
+ }
1838
+ if (!hasCheckpointing) {
1839
+ currentMessages = [
1840
+ ...currentMessages,
1841
+ { role: "user", content: followUpPrompt },
1842
+ { role: "assistant", content: followUpText },
1843
+ ];
1844
+ }
1845
+ followUpPrompt = await getNextTaskPrompt();
1846
+ }
1636
1847
  return;
1637
1848
  }
1638
1849
  catch (error) {
@@ -1682,9 +1893,13 @@ export function createAgent(options) {
1682
1893
  const { messages, checkpoint } = await buildMessages(effectiveGenOptions);
1683
1894
  const maxSteps = options.maxSteps ?? 10;
1684
1895
  const startStep = checkpoint?.step ?? 0;
1896
+ // Signal state for cooperative signal catching in streaming mode
1897
+ const signalState = {};
1685
1898
  // 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);
1899
+ // Apply hooks AFTER adding task tool so task tool is also wrapped.
1900
+ // Then wrap with signal catching as the outermost layer.
1901
+ const hookedTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSet(effectiveGenOptions.threadId)), effectiveGenOptions.threadId);
1902
+ const activeTools = wrapToolsWithSignalCatching(hookedTools, signalState);
1688
1903
  // Build prompt context and generate system prompt
1689
1904
  const promptContext = buildPromptContext(messages, effectiveGenOptions.threadId);
1690
1905
  const systemPrompt = getSystemPrompt(promptContext);
@@ -1699,11 +1914,18 @@ export function createAgent(options) {
1699
1914
  providerOptions: effectiveGenOptions.providerOptions,
1700
1915
  headers: effectiveGenOptions.headers,
1701
1916
  };
1917
+ // Capture currentModel for use in the callback closure
1918
+ const modelToUse = retryState.currentModel;
1919
+ // Stop condition: stop when an interrupt signal was caught, OR when
1920
+ // the step count reaches maxSteps.
1921
+ const signalStopCondition = () => signalState.interrupt != null;
1702
1922
  // Track step count for incremental checkpointing
1703
1923
  let currentStepCount = 0;
1704
- // Execute stream
1924
+ // Execute streamText OUTSIDE createUIMessageStream so errors propagate
1925
+ // to the retry loop (if streamText throws synchronously on creation,
1926
+ // e.g. rate limit, the catch block handles retry/fallback).
1705
1927
  const result = streamText({
1706
- model: retryState.currentModel,
1928
+ model: modelToUse,
1707
1929
  system: initialParams.system,
1708
1930
  messages: initialParams.messages,
1709
1931
  tools: initialParams.tools,
@@ -1711,7 +1933,7 @@ export function createAgent(options) {
1711
1933
  temperature: initialParams.temperature,
1712
1934
  stopSequences: initialParams.stopSequences,
1713
1935
  abortSignal: initialParams.abortSignal,
1714
- stopWhen: stepCountIs(maxSteps),
1936
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
1715
1937
  // Passthrough AI SDK options
1716
1938
  output: effectiveGenOptions.output,
1717
1939
  // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
@@ -1774,10 +1996,93 @@ export function createAgent(options) {
1774
1996
  }
1775
1997
  },
1776
1998
  });
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();
1999
+ // Use createUIMessageStream to control stream lifecycle for background task follow-ups
2000
+ const stream = createUIMessageStream({
2001
+ execute: async ({ writer }) => {
2002
+ // Merge initial generation into the stream
2003
+ writer.merge(result.toUIMessageStream());
2004
+ // Wait for initial generation to complete to get final text
2005
+ const text = await result.text;
2006
+ // --- Background task completion loop ---
2007
+ if (waitForBackgroundTasks) {
2008
+ // Track accumulated steps for checkpoint saves
2009
+ const initialSteps = await result.steps;
2010
+ let accumulatedStepCount = initialSteps.length;
2011
+ let currentMessages = [
2012
+ ...messages,
2013
+ { role: "assistant", content: text },
2014
+ ];
2015
+ let followUpPrompt = await getNextTaskPrompt();
2016
+ while (followUpPrompt !== null) {
2017
+ // Stream follow-up generation into the same writer
2018
+ const followUpResult = streamText({
2019
+ model: modelToUse,
2020
+ system: initialParams.system,
2021
+ messages: [
2022
+ ...currentMessages,
2023
+ { role: "user", content: followUpPrompt },
2024
+ ],
2025
+ tools: initialParams.tools,
2026
+ maxOutputTokens: initialParams.maxTokens,
2027
+ temperature: initialParams.temperature,
2028
+ stopSequences: initialParams.stopSequences,
2029
+ abortSignal: initialParams.abortSignal,
2030
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
2031
+ // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
2032
+ providerOptions: initialParams.providerOptions,
2033
+ headers: initialParams.headers,
2034
+ });
2035
+ writer.merge(followUpResult.toUIMessageStream());
2036
+ const followUpText = await followUpResult.text;
2037
+ currentMessages = [
2038
+ ...currentMessages,
2039
+ { role: "user", content: followUpPrompt },
2040
+ { role: "assistant", content: followUpText },
2041
+ ];
2042
+ // --- Post-completion bookkeeping for follow-ups ---
2043
+ const followUpSteps = await followUpResult.steps;
2044
+ accumulatedStepCount += followUpSteps.length;
2045
+ // Checkpoint save
2046
+ if (effectiveGenOptions.threadId && options.checkpointer) {
2047
+ await saveCheckpoint(effectiveGenOptions.threadId, currentMessages, startStep + accumulatedStepCount);
2048
+ }
2049
+ // Context manager update
2050
+ const followUpUsage = await followUpResult.usage;
2051
+ if (options.contextManager?.updateUsage && followUpUsage) {
2052
+ options.contextManager.updateUsage({
2053
+ inputTokens: followUpUsage.inputTokens,
2054
+ outputTokens: followUpUsage.outputTokens,
2055
+ totalTokens: followUpUsage.totalTokens,
2056
+ });
2057
+ }
2058
+ // PostGenerate hooks
2059
+ const followUpPostGenerateHooks = effectiveHooks?.PostGenerate ?? [];
2060
+ if (followUpPostGenerateHooks.length > 0) {
2061
+ const followUpFinishReason = await followUpResult.finishReason;
2062
+ const followUpHookResult = {
2063
+ status: "complete",
2064
+ text: followUpText,
2065
+ usage: followUpUsage,
2066
+ finishReason: followUpFinishReason,
2067
+ output: undefined,
2068
+ steps: mapSteps(followUpSteps),
2069
+ };
2070
+ const followUpPostGenerateInput = {
2071
+ hook_event_name: "PostGenerate",
2072
+ session_id: effectiveGenOptions.threadId ?? "default",
2073
+ cwd: process.cwd(),
2074
+ options: effectiveGenOptions,
2075
+ result: followUpHookResult,
2076
+ };
2077
+ await invokeHooksWithTimeout(followUpPostGenerateHooks, followUpPostGenerateInput, null, agent);
2078
+ }
2079
+ followUpPrompt = await getNextTaskPrompt();
2080
+ }
2081
+ }
2082
+ },
2083
+ });
2084
+ // Convert the stream to a Response
2085
+ return createUIMessageStreamResponse({ stream });
1781
2086
  }
1782
2087
  catch (error) {
1783
2088
  // Normalize error to AgentError
@@ -1816,9 +2121,13 @@ export function createAgent(options) {
1816
2121
  const { messages, checkpoint } = await buildMessages(effectiveGenOptions);
1817
2122
  const maxSteps = options.maxSteps ?? 10;
1818
2123
  const startStep = checkpoint?.step ?? 0;
2124
+ // Signal state for cooperative signal catching in streaming mode
2125
+ const signalState = {};
1819
2126
  // 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);
2127
+ // Apply hooks AFTER adding task tool so task tool is also wrapped.
2128
+ // Then wrap with signal catching as the outermost layer.
2129
+ const hookedTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSet(effectiveGenOptions.threadId)), effectiveGenOptions.threadId);
2130
+ const activeTools = wrapToolsWithSignalCatching(hookedTools, signalState);
1822
2131
  // Build prompt context and generate system prompt
1823
2132
  const promptContext = buildPromptContext(messages, effectiveGenOptions.threadId);
1824
2133
  const systemPrompt = getSystemPrompt(promptContext);
@@ -1835,6 +2144,9 @@ export function createAgent(options) {
1835
2144
  };
1836
2145
  // Track step count for incremental checkpointing
1837
2146
  let currentStepCount = 0;
2147
+ // Stop condition: stop when an interrupt signal was caught, OR when
2148
+ // the step count reaches maxSteps.
2149
+ const signalStopCondition = () => signalState.interrupt != null;
1838
2150
  // Execute stream
1839
2151
  const result = streamText({
1840
2152
  model: retryState.currentModel,
@@ -1845,7 +2157,7 @@ export function createAgent(options) {
1845
2157
  temperature: initialParams.temperature,
1846
2158
  stopSequences: initialParams.stopSequences,
1847
2159
  abortSignal: initialParams.abortSignal,
1848
- stopWhen: stepCountIs(maxSteps),
2160
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
1849
2161
  // Passthrough AI SDK options
1850
2162
  output: effectiveGenOptions.output,
1851
2163
  // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
@@ -1968,9 +2280,13 @@ export function createAgent(options) {
1968
2280
  }
1969
2281
  // Create streaming context for tools
1970
2282
  const streamingContext = { writer };
2283
+ // Signal state for cooperative signal catching in streaming mode
2284
+ const signalState = {};
1971
2285
  // 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);
2286
+ // Apply hooks AFTER adding task tool so task tool is also wrapped.
2287
+ // Then wrap with signal catching as the outermost layer.
2288
+ const hookedStreamingTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSetWithStreaming(streamingContext, effectiveGenOptions.threadId), streamingContext), effectiveGenOptions.threadId);
2289
+ const streamingTools = wrapToolsWithSignalCatching(hookedStreamingTools, signalState);
1974
2290
  // Build prompt context and generate system prompt
1975
2291
  const promptContext = buildPromptContext(messages, effectiveGenOptions.threadId);
1976
2292
  const systemPrompt = getSystemPrompt(promptContext);
@@ -1988,6 +2304,9 @@ export function createAgent(options) {
1988
2304
  };
1989
2305
  // Track step count for incremental checkpointing
1990
2306
  let currentStepCount = 0;
2307
+ // Stop condition: stop when a flow-control signal was caught, OR when
2308
+ // the step count reaches maxSteps.
2309
+ const signalStopCondition = () => signalState.interrupt != null;
1991
2310
  // Execute stream
1992
2311
  const result = streamText({
1993
2312
  model: modelToUse,
@@ -1998,7 +2317,7 @@ export function createAgent(options) {
1998
2317
  temperature: initialParams.temperature,
1999
2318
  stopSequences: initialParams.stopSequences,
2000
2319
  abortSignal: initialParams.abortSignal,
2001
- stopWhen: stepCountIs(maxSteps),
2320
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
2002
2321
  // Passthrough AI SDK options
2003
2322
  output: effectiveGenOptions.output,
2004
2323
  // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
@@ -2066,6 +2385,84 @@ export function createAgent(options) {
2066
2385
  });
2067
2386
  // Merge the streamText output into the UI message stream
2068
2387
  writer.merge(result.toUIMessageStream());
2388
+ // Wait for initial generation to complete to get final text
2389
+ const text = await result.text;
2390
+ // --- Background task completion loop ---
2391
+ if (waitForBackgroundTasks) {
2392
+ // Track accumulated steps for checkpoint saves
2393
+ const initialSteps = await result.steps;
2394
+ let accumulatedStepCount = initialSteps.length;
2395
+ let currentMessages = [
2396
+ ...messages,
2397
+ { role: "assistant", content: text },
2398
+ ];
2399
+ let followUpPrompt = await getNextTaskPrompt();
2400
+ while (followUpPrompt !== null) {
2401
+ // Stream follow-up generation into the same writer
2402
+ const followUpResult = streamText({
2403
+ model: modelToUse,
2404
+ system: initialParams.system,
2405
+ messages: [
2406
+ ...currentMessages,
2407
+ { role: "user", content: followUpPrompt },
2408
+ ],
2409
+ tools: initialParams.tools,
2410
+ maxOutputTokens: initialParams.maxTokens,
2411
+ temperature: initialParams.temperature,
2412
+ stopSequences: initialParams.stopSequences,
2413
+ abortSignal: initialParams.abortSignal,
2414
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
2415
+ // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
2416
+ providerOptions: initialParams.providerOptions,
2417
+ headers: initialParams.headers,
2418
+ });
2419
+ writer.merge(followUpResult.toUIMessageStream());
2420
+ const followUpText = await followUpResult.text;
2421
+ currentMessages = [
2422
+ ...currentMessages,
2423
+ { role: "user", content: followUpPrompt },
2424
+ { role: "assistant", content: followUpText },
2425
+ ];
2426
+ // --- Post-completion bookkeeping for follow-ups ---
2427
+ const followUpSteps = await followUpResult.steps;
2428
+ accumulatedStepCount += followUpSteps.length;
2429
+ // Checkpoint save
2430
+ if (effectiveGenOptions.threadId && options.checkpointer) {
2431
+ await saveCheckpoint(effectiveGenOptions.threadId, currentMessages, startStep + accumulatedStepCount);
2432
+ }
2433
+ // Context manager update
2434
+ const followUpUsage = await followUpResult.usage;
2435
+ if (options.contextManager?.updateUsage && followUpUsage) {
2436
+ options.contextManager.updateUsage({
2437
+ inputTokens: followUpUsage.inputTokens,
2438
+ outputTokens: followUpUsage.outputTokens,
2439
+ totalTokens: followUpUsage.totalTokens,
2440
+ });
2441
+ }
2442
+ // PostGenerate hooks
2443
+ const followUpPostGenerateHooks = effectiveHooks?.PostGenerate ?? [];
2444
+ if (followUpPostGenerateHooks.length > 0) {
2445
+ const followUpFinishReason = await followUpResult.finishReason;
2446
+ const followUpHookResult = {
2447
+ status: "complete",
2448
+ text: followUpText,
2449
+ usage: followUpUsage,
2450
+ finishReason: followUpFinishReason,
2451
+ output: undefined,
2452
+ steps: mapSteps(followUpSteps),
2453
+ };
2454
+ const followUpPostGenerateInput = {
2455
+ hook_event_name: "PostGenerate",
2456
+ session_id: effectiveGenOptions.threadId ?? "default",
2457
+ cwd: process.cwd(),
2458
+ options: effectiveGenOptions,
2459
+ result: followUpHookResult,
2460
+ };
2461
+ await invokeHooksWithTimeout(followUpPostGenerateHooks, followUpPostGenerateInput, null, agent);
2462
+ }
2463
+ followUpPrompt = await getNextTaskPrompt();
2464
+ }
2465
+ }
2069
2466
  },
2070
2467
  });
2071
2468
  // Convert the stream to a Response
@@ -2095,6 +2492,14 @@ export function createAgent(options) {
2095
2492
  getActiveTools() {
2096
2493
  return getActiveToolSet();
2097
2494
  },
2495
+ addRuntimeTools(tools) {
2496
+ Object.assign(runtimeTools, tools);
2497
+ },
2498
+ removeRuntimeTools(toolNames) {
2499
+ for (const name of toolNames) {
2500
+ delete runtimeTools[name];
2501
+ }
2502
+ },
2098
2503
  loadTools(toolNames) {
2099
2504
  if (!toolRegistry) {
2100
2505
  // No registry in eager mode - all tools already loaded