@lleverage-ai/agent-sdk 0.0.2 → 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 (79) hide show
  1. package/README.md +141 -0
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +528 -41
  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 +7 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +6 -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/prompt-builder/components.d.ts +149 -0
  56. package/dist/prompt-builder/components.d.ts.map +1 -0
  57. package/dist/prompt-builder/components.js +252 -0
  58. package/dist/prompt-builder/components.js.map +1 -0
  59. package/dist/prompt-builder/index.d.ts +248 -0
  60. package/dist/prompt-builder/index.d.ts.map +1 -0
  61. package/dist/prompt-builder/index.js +165 -0
  62. package/dist/prompt-builder/index.js.map +1 -0
  63. package/dist/task-manager.d.ts +15 -0
  64. package/dist/task-manager.d.ts.map +1 -1
  65. package/dist/task-manager.js +36 -0
  66. package/dist/task-manager.js.map +1 -1
  67. package/dist/testing/mock-agent.d.ts.map +1 -1
  68. package/dist/testing/mock-agent.js +6 -0
  69. package/dist/testing/mock-agent.js.map +1 -1
  70. package/dist/testing/recorder.d.ts.map +1 -1
  71. package/dist/testing/recorder.js +6 -0
  72. package/dist/testing/recorder.js.map +1 -1
  73. package/dist/tools/task.d.ts.map +1 -1
  74. package/dist/tools/task.js +6 -2
  75. package/dist/tools/task.js.map +1 -1
  76. package/dist/types.d.ts +178 -3
  77. package/dist/types.d.ts.map +1 -1
  78. package/dist/types.js.map +1 -1
  79. package/package.json +1 -1
package/dist/agent.js CHANGED
@@ -13,6 +13,7 @@ import { createRetryLoopState, handleGenerationError, invokePreGenerateHooks, no
13
13
  import { aggregatePermissionDecisions, extractUpdatedInput, extractUpdatedResult, invokeHooksWithTimeout, invokeMatchingHooks, } from "./hooks.js";
14
14
  import { MCPManager } from "./mcp/manager.js";
15
15
  import { applyMiddleware, mergeHooks, setupMiddleware } from "./middleware/index.js";
16
+ import { createDefaultPromptBuilder } from "./prompt-builder/components.js";
16
17
  import { ACCEPT_EDITS_BLOCKED_PATTERNS } from "./security/index.js";
17
18
  import { TaskManager } from "./task-manager.js";
18
19
  import { coreToolsToToolSet, createCoreTools, createSearchToolsTool, createTaskOutputTool, createTaskTool, } from "./tools/factory.js";
@@ -41,6 +42,46 @@ class InterruptSignal extends Error {
41
42
  function isInterruptSignal(error) {
42
43
  return error instanceof InterruptSignal;
43
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
+ }
44
85
  /**
45
86
  * File edit tool names that get auto-approved in acceptEdits mode.
46
87
  * @internal
@@ -456,17 +497,21 @@ function wrapToolsWithHooks(tools, hookRegistration, agent, sessionId) {
456
497
  return output;
457
498
  }
458
499
  catch (error) {
459
- // Invoke PostToolUseFailure hooks
460
- if (hookRegistration?.PostToolUseFailure?.length) {
461
- const failureInput = {
462
- hook_event_name: "PostToolUseFailure",
463
- session_id: sessionId,
464
- cwd: process.cwd(),
465
- tool_name: name,
466
- tool_input: input,
467
- error: error instanceof Error ? error : new Error(String(error)),
468
- };
469
- 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
+ }
470
515
  }
471
516
  throw error;
472
517
  }
@@ -527,10 +572,21 @@ function isBackendFactory(value) {
527
572
  */
528
573
  export function createAgent(options) {
529
574
  const id = `agent-${++agentIdCounter}`;
575
+ // Validate mutually exclusive prompt options
576
+ if (options.systemPrompt !== undefined && options.promptBuilder) {
577
+ throw new Error("Cannot specify both systemPrompt and promptBuilder - they are mutually exclusive");
578
+ }
579
+ // Determine prompt mode
580
+ // - 'static': Use systemPrompt string directly
581
+ // - 'builder': Use PromptBuilder to generate dynamic prompts
582
+ const promptMode = options.systemPrompt !== undefined ? "static" : "builder";
583
+ // Get or create prompt builder
584
+ const promptBuilder = options.promptBuilder ?? (promptMode === "builder" ? createDefaultPromptBuilder() : undefined);
530
585
  // Process middleware to get hooks (middleware hooks come before explicit hooks)
531
586
  const middleware = options.middleware ?? [];
532
587
  const middlewareHooks = applyMiddleware(middleware);
533
- 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);
534
590
  // Create options with merged hooks for all hook lookups
535
591
  const effectiveHooks = mergedHooks;
536
592
  // Permission mode (mutable for setPermissionMode)
@@ -561,6 +617,18 @@ export function createAgent(options) {
561
617
  }
562
618
  // Initialize task manager for background task tracking
563
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
+ });
564
632
  // Determine plugin loading mode
565
633
  // Track whether it was explicitly set to distinguish from default
566
634
  const explicitPluginLoading = options.pluginLoading !== undefined;
@@ -811,10 +879,71 @@ export function createAgent(options) {
811
879
  }
812
880
  return filtered;
813
881
  };
814
- // Helper to get current active tools (core + MCP + dynamically loaded from registry)
882
+ // Helper to build prompt context from current agent state
883
+ const buildPromptContext = (messages, threadId) => {
884
+ // Get filtered tools (respecting allowedTools/disallowedTools) so the prompt
885
+ // only advertises tools the agent will actually expose
886
+ const filteredTools = filterToolsByAllowed((() => {
887
+ const allTools = { ...coreTools };
888
+ Object.assign(allTools, runtimeTools);
889
+ Object.assign(allTools, mcpManager.getToolSet());
890
+ if (toolRegistry) {
891
+ Object.assign(allTools, toolRegistry.getLoadedTools());
892
+ }
893
+ return allTools;
894
+ })());
895
+ // Extract tool metadata for context
896
+ const toolsMetadata = Object.entries(filteredTools).map(([name, tool]) => ({
897
+ name,
898
+ description: tool.description ?? "",
899
+ }));
900
+ // Extract skills metadata from the skills array
901
+ const skillsMetadata = skills.map((skill) => ({
902
+ name: skill.name,
903
+ description: skill.description,
904
+ }));
905
+ // Extract plugins metadata
906
+ const pluginsMetadata = (options.plugins ?? []).map((plugin) => ({
907
+ name: plugin.name,
908
+ description: plugin.description ?? "",
909
+ }));
910
+ // Build backend info
911
+ const backendInfo = {
912
+ type: backend.constructor.name.toLowerCase().replace("backend", "") || "unknown",
913
+ hasExecuteCapability: hasExecuteCapability(backend),
914
+ rootDir: "rootDir" in backend ? backend.rootDir : undefined,
915
+ };
916
+ return {
917
+ tools: toolsMetadata.length > 0 ? toolsMetadata : undefined,
918
+ skills: skillsMetadata.length > 0 ? skillsMetadata : undefined,
919
+ plugins: pluginsMetadata.length > 0 ? pluginsMetadata : undefined,
920
+ backend: backendInfo,
921
+ state,
922
+ // Model ID extraction is not reliable across all LanguageModel types
923
+ // Users can access the full model via their custom context if needed
924
+ model: undefined,
925
+ maxSteps: options.maxSteps,
926
+ permissionMode,
927
+ currentMessages: messages,
928
+ threadId,
929
+ };
930
+ };
931
+ // Helper to get system prompt (either static or built from context)
932
+ const getSystemPrompt = (context) => {
933
+ if (promptMode === "static") {
934
+ return options.systemPrompt;
935
+ }
936
+ // Build prompt using prompt builder
937
+ return promptBuilder.build(context);
938
+ };
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)
815
942
  const getActiveToolSet = (threadId) => {
816
943
  // Start with core tools
817
944
  const allTools = { ...coreTools };
945
+ // Add runtime tools (added by plugins at runtime)
946
+ Object.assign(allTools, runtimeTools);
818
947
  // Add MCP tools from plugin registrations
819
948
  const mcpTools = mcpManager.getToolSet();
820
949
  Object.assign(allTools, mcpTools);
@@ -843,6 +972,8 @@ export function createAgent(options) {
843
972
  const getActiveToolSetWithStreaming = (streamingContext, threadId, step) => {
844
973
  // Start with core tools
845
974
  const allTools = { ...coreTools };
975
+ // Add runtime tools (added by plugins at runtime)
976
+ Object.assign(allTools, runtimeTools);
846
977
  // Process plugins - invoke function-based tools with streaming context
847
978
  for (const plugin of options.plugins ?? []) {
848
979
  if (plugin.tools) {
@@ -1155,6 +1286,34 @@ export function createAgent(options) {
1155
1286
  usage: step.usage,
1156
1287
  }));
1157
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
+ }
1158
1317
  const agent = {
1159
1318
  id,
1160
1319
  options,
@@ -1184,11 +1343,22 @@ export function createAgent(options) {
1184
1343
  lastBuiltMessages = messages;
1185
1344
  const maxSteps = options.maxSteps ?? 10;
1186
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 = {};
1187
1350
  // Build initial params - use active tools (core + dynamically loaded + task)
1188
- // Apply hooks AFTER adding task tool so task tool is also wrapped
1189
- 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);
1357
+ // Build prompt context and generate system prompt
1358
+ const promptContext = buildPromptContext(messages, effectiveGenOptions.threadId);
1359
+ const systemPrompt = getSystemPrompt(promptContext);
1190
1360
  const initialParams = {
1191
- system: options.systemPrompt,
1361
+ system: systemPrompt,
1192
1362
  messages,
1193
1363
  tools: activeTools,
1194
1364
  maxTokens: effectiveGenOptions.maxTokens,
@@ -1198,6 +1368,9 @@ export function createAgent(options) {
1198
1368
  providerOptions: effectiveGenOptions.providerOptions,
1199
1369
  headers: effectiveGenOptions.headers,
1200
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;
1201
1374
  // Execute generation
1202
1375
  const response = await generateText({
1203
1376
  model: retryState.currentModel,
@@ -1208,13 +1381,53 @@ export function createAgent(options) {
1208
1381
  temperature: initialParams.temperature,
1209
1382
  stopSequences: initialParams.stopSequences,
1210
1383
  abortSignal: initialParams.abortSignal,
1211
- stopWhen: stepCountIs(maxSteps),
1384
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
1212
1385
  // Passthrough AI SDK options
1213
1386
  output: effectiveGenOptions.output,
1214
1387
  // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
1215
1388
  providerOptions: initialParams.providerOptions,
1216
1389
  headers: initialParams.headers,
1217
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
+ }
1218
1431
  // Only access output if an output schema was provided
1219
1432
  // (accessing response.output throws AI_NoOutputGeneratedError otherwise)
1220
1433
  let output;
@@ -1272,7 +1485,39 @@ export function createAgent(options) {
1272
1485
  finalResult = updatedResult;
1273
1486
  }
1274
1487
  }
1275
- 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;
1276
1521
  }
1277
1522
  catch (error) {
1278
1523
  // Check if this is an InterruptSignal (new interrupt system)
@@ -1451,11 +1696,18 @@ export function createAgent(options) {
1451
1696
  const { messages, checkpoint } = await buildMessages(effectiveGenOptions);
1452
1697
  const maxSteps = options.maxSteps ?? 10;
1453
1698
  const startStep = checkpoint?.step ?? 0;
1699
+ // Signal state for cooperative signal catching in streaming mode
1700
+ const signalState = {};
1454
1701
  // Build initial params - use active tools (core + dynamically loaded + task)
1455
- // Apply hooks AFTER adding task tool so task tool is also wrapped
1456
- 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);
1706
+ // Build prompt context and generate system prompt
1707
+ const promptContext = buildPromptContext(messages, effectiveGenOptions.threadId);
1708
+ const systemPrompt = getSystemPrompt(promptContext);
1457
1709
  const initialParams = {
1458
- system: options.systemPrompt,
1710
+ system: systemPrompt,
1459
1711
  messages,
1460
1712
  tools: activeTools,
1461
1713
  maxTokens: effectiveGenOptions.maxTokens,
@@ -1465,6 +1717,9 @@ export function createAgent(options) {
1465
1717
  providerOptions: effectiveGenOptions.providerOptions,
1466
1718
  headers: effectiveGenOptions.headers,
1467
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;
1468
1723
  // Execute stream
1469
1724
  const response = streamText({
1470
1725
  model: retryState.currentModel,
@@ -1475,7 +1730,7 @@ export function createAgent(options) {
1475
1730
  temperature: initialParams.temperature,
1476
1731
  stopSequences: initialParams.stopSequences,
1477
1732
  abortSignal: initialParams.abortSignal,
1478
- stopWhen: stepCountIs(maxSteps),
1733
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
1479
1734
  // Passthrough AI SDK options
1480
1735
  output: genOptions.output,
1481
1736
  // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
@@ -1559,7 +1814,36 @@ export function createAgent(options) {
1559
1814
  await invokeHooksWithTimeout(postGenerateHooks, postGenerateInput, null, agent);
1560
1815
  // Note: updatedResult is not applied for streaming since the stream has already been sent
1561
1816
  }
1562
- // 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
+ }
1563
1847
  return;
1564
1848
  }
1565
1849
  catch (error) {
@@ -1609,11 +1893,18 @@ export function createAgent(options) {
1609
1893
  const { messages, checkpoint } = await buildMessages(effectiveGenOptions);
1610
1894
  const maxSteps = options.maxSteps ?? 10;
1611
1895
  const startStep = checkpoint?.step ?? 0;
1896
+ // Signal state for cooperative signal catching in streaming mode
1897
+ const signalState = {};
1612
1898
  // Build initial params - use active tools (core + dynamically loaded + task)
1613
- // Apply hooks AFTER adding task tool so task tool is also wrapped
1614
- 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);
1903
+ // Build prompt context and generate system prompt
1904
+ const promptContext = buildPromptContext(messages, effectiveGenOptions.threadId);
1905
+ const systemPrompt = getSystemPrompt(promptContext);
1615
1906
  const initialParams = {
1616
- system: options.systemPrompt,
1907
+ system: systemPrompt,
1617
1908
  messages,
1618
1909
  tools: activeTools,
1619
1910
  maxTokens: effectiveGenOptions.maxTokens,
@@ -1623,11 +1914,18 @@ export function createAgent(options) {
1623
1914
  providerOptions: effectiveGenOptions.providerOptions,
1624
1915
  headers: effectiveGenOptions.headers,
1625
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;
1626
1922
  // Track step count for incremental checkpointing
1627
1923
  let currentStepCount = 0;
1628
- // 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).
1629
1927
  const result = streamText({
1630
- model: retryState.currentModel,
1928
+ model: modelToUse,
1631
1929
  system: initialParams.system,
1632
1930
  messages: initialParams.messages,
1633
1931
  tools: initialParams.tools,
@@ -1635,7 +1933,7 @@ export function createAgent(options) {
1635
1933
  temperature: initialParams.temperature,
1636
1934
  stopSequences: initialParams.stopSequences,
1637
1935
  abortSignal: initialParams.abortSignal,
1638
- stopWhen: stepCountIs(maxSteps),
1936
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
1639
1937
  // Passthrough AI SDK options
1640
1938
  output: effectiveGenOptions.output,
1641
1939
  // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
@@ -1698,10 +1996,93 @@ export function createAgent(options) {
1698
1996
  }
1699
1997
  },
1700
1998
  });
1701
- // Return AI SDK compatible response for use with useChat
1702
- // Note: Not passing originalMessages since ModelMessage[] != UIMessage[]
1703
- // The AI SDK will reconstruct messages from the stream
1704
- 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 });
1705
2086
  }
1706
2087
  catch (error) {
1707
2088
  // Normalize error to AgentError
@@ -1740,11 +2121,18 @@ export function createAgent(options) {
1740
2121
  const { messages, checkpoint } = await buildMessages(effectiveGenOptions);
1741
2122
  const maxSteps = options.maxSteps ?? 10;
1742
2123
  const startStep = checkpoint?.step ?? 0;
2124
+ // Signal state for cooperative signal catching in streaming mode
2125
+ const signalState = {};
1743
2126
  // Build initial params - use active tools (core + dynamically loaded + task)
1744
- // Apply hooks AFTER adding task tool so task tool is also wrapped
1745
- 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);
2131
+ // Build prompt context and generate system prompt
2132
+ const promptContext = buildPromptContext(messages, effectiveGenOptions.threadId);
2133
+ const systemPrompt = getSystemPrompt(promptContext);
1746
2134
  const initialParams = {
1747
- system: options.systemPrompt,
2135
+ system: systemPrompt,
1748
2136
  messages,
1749
2137
  tools: activeTools,
1750
2138
  maxTokens: effectiveGenOptions.maxTokens,
@@ -1756,6 +2144,9 @@ export function createAgent(options) {
1756
2144
  };
1757
2145
  // Track step count for incremental checkpointing
1758
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;
1759
2150
  // Execute stream
1760
2151
  const result = streamText({
1761
2152
  model: retryState.currentModel,
@@ -1766,7 +2157,7 @@ export function createAgent(options) {
1766
2157
  temperature: initialParams.temperature,
1767
2158
  stopSequences: initialParams.stopSequences,
1768
2159
  abortSignal: initialParams.abortSignal,
1769
- stopWhen: stepCountIs(maxSteps),
2160
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
1770
2161
  // Passthrough AI SDK options
1771
2162
  output: effectiveGenOptions.output,
1772
2163
  // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
@@ -1889,12 +2280,19 @@ export function createAgent(options) {
1889
2280
  }
1890
2281
  // Create streaming context for tools
1891
2282
  const streamingContext = { writer };
2283
+ // Signal state for cooperative signal catching in streaming mode
2284
+ const signalState = {};
1892
2285
  // Build tools with streaming context and task tool
1893
- // Apply hooks AFTER adding task tool so task tool is also wrapped
1894
- 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);
2290
+ // Build prompt context and generate system prompt
2291
+ const promptContext = buildPromptContext(messages, effectiveGenOptions.threadId);
2292
+ const systemPrompt = getSystemPrompt(promptContext);
1895
2293
  // Build initial params with streaming-aware tools
1896
2294
  const initialParams = {
1897
- system: options.systemPrompt,
2295
+ system: systemPrompt,
1898
2296
  messages,
1899
2297
  tools: streamingTools,
1900
2298
  maxTokens: effectiveGenOptions.maxTokens,
@@ -1906,6 +2304,9 @@ export function createAgent(options) {
1906
2304
  };
1907
2305
  // Track step count for incremental checkpointing
1908
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;
1909
2310
  // Execute stream
1910
2311
  const result = streamText({
1911
2312
  model: modelToUse,
@@ -1916,7 +2317,7 @@ export function createAgent(options) {
1916
2317
  temperature: initialParams.temperature,
1917
2318
  stopSequences: initialParams.stopSequences,
1918
2319
  abortSignal: initialParams.abortSignal,
1919
- stopWhen: stepCountIs(maxSteps),
2320
+ stopWhen: [signalStopCondition, stepCountIs(maxSteps)],
1920
2321
  // Passthrough AI SDK options
1921
2322
  output: effectiveGenOptions.output,
1922
2323
  // biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
@@ -1984,6 +2385,84 @@ export function createAgent(options) {
1984
2385
  });
1985
2386
  // Merge the streamText output into the UI message stream
1986
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
+ }
1987
2466
  },
1988
2467
  });
1989
2468
  // Convert the stream to a Response
@@ -2013,6 +2492,14 @@ export function createAgent(options) {
2013
2492
  getActiveTools() {
2014
2493
  return getActiveToolSet();
2015
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
+ },
2016
2503
  loadTools(toolNames) {
2017
2504
  if (!toolRegistry) {
2018
2505
  // No registry in eager mode - all tools already loaded