@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.
- package/README.md +87 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +441 -36
- 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,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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|