@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.
- package/README.md +87 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +594 -70
- package/dist/agent.js.map +1 -1
- package/dist/hooks.d.ts +28 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +40 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/apply.d.ts.map +1 -1
- package/dist/middleware/apply.js +8 -0
- package/dist/middleware/apply.js.map +1 -1
- package/dist/middleware/context.d.ts.map +1 -1
- package/dist/middleware/context.js +11 -0
- package/dist/middleware/context.js.map +1 -1
- package/dist/middleware/types.d.ts +8 -0
- package/dist/middleware/types.d.ts.map +1 -1
- package/dist/plugins/agent-teams/coordinator.d.ts +46 -0
- package/dist/plugins/agent-teams/coordinator.d.ts.map +1 -0
- package/dist/plugins/agent-teams/coordinator.js +255 -0
- package/dist/plugins/agent-teams/coordinator.js.map +1 -0
- package/dist/plugins/agent-teams/hooks.d.ts +29 -0
- package/dist/plugins/agent-teams/hooks.d.ts.map +1 -0
- package/dist/plugins/agent-teams/hooks.js +29 -0
- package/dist/plugins/agent-teams/hooks.js.map +1 -0
- package/dist/plugins/agent-teams/index.d.ts +59 -0
- package/dist/plugins/agent-teams/index.d.ts.map +1 -0
- package/dist/plugins/agent-teams/index.js +313 -0
- package/dist/plugins/agent-teams/index.js.map +1 -0
- package/dist/plugins/agent-teams/mermaid.d.ts +32 -0
- package/dist/plugins/agent-teams/mermaid.d.ts.map +1 -0
- package/dist/plugins/agent-teams/mermaid.js +66 -0
- package/dist/plugins/agent-teams/mermaid.js.map +1 -0
- package/dist/plugins/agent-teams/session-runner.d.ts +92 -0
- package/dist/plugins/agent-teams/session-runner.d.ts.map +1 -0
- package/dist/plugins/agent-teams/session-runner.js +166 -0
- package/dist/plugins/agent-teams/session-runner.js.map +1 -0
- package/dist/plugins/agent-teams/tools.d.ts +41 -0
- package/dist/plugins/agent-teams/tools.d.ts.map +1 -0
- package/dist/plugins/agent-teams/tools.js +289 -0
- package/dist/plugins/agent-teams/tools.js.map +1 -0
- package/dist/plugins/agent-teams/types.d.ts +164 -0
- package/dist/plugins/agent-teams/types.d.ts.map +1 -0
- package/dist/plugins/agent-teams/types.js +7 -0
- package/dist/plugins/agent-teams/types.js.map +1 -0
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +1 -0
- package/dist/plugins.js.map +1 -1
- package/dist/presets/production.d.ts.map +1 -1
- package/dist/presets/production.js +7 -7
- package/dist/presets/production.js.map +1 -1
- package/dist/task-manager.d.ts +15 -0
- package/dist/task-manager.d.ts.map +1 -1
- package/dist/task-manager.js +36 -0
- package/dist/task-manager.js.map +1 -1
- package/dist/testing/mock-agent.d.ts.map +1 -1
- package/dist/testing/mock-agent.js +6 -0
- package/dist/testing/mock-agent.js.map +1 -1
- package/dist/testing/recorder.d.ts.map +1 -1
- package/dist/testing/recorder.js +6 -0
- package/dist/testing/recorder.js.map +1 -1
- package/dist/tools/task.d.ts.map +1 -1
- package/dist/tools/task.js +6 -2
- package/dist/tools/task.js.map +1 -1
- package/dist/types.d.ts +103 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- 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
|
-
//
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1354
|
-
if (
|
|
1355
|
-
const
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
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:
|
|
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.
|
|
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
|
|
2245
|
-
//
|
|
2246
|
-
//
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
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
|