@lleverage-ai/agent-sdk 0.0.1
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/LICENSE +21 -0
- package/README.md +2321 -0
- package/dist/agent.d.ts +52 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +2122 -0
- package/dist/agent.js.map +1 -0
- package/dist/backend.d.ts +378 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +71 -0
- package/dist/backend.js.map +1 -0
- package/dist/backends/composite.d.ts +258 -0
- package/dist/backends/composite.d.ts.map +1 -0
- package/dist/backends/composite.js +437 -0
- package/dist/backends/composite.js.map +1 -0
- package/dist/backends/filesystem.d.ts +268 -0
- package/dist/backends/filesystem.d.ts.map +1 -0
- package/dist/backends/filesystem.js +623 -0
- package/dist/backends/filesystem.js.map +1 -0
- package/dist/backends/index.d.ts +14 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/index.js +14 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/persistent.d.ts +312 -0
- package/dist/backends/persistent.d.ts.map +1 -0
- package/dist/backends/persistent.js +519 -0
- package/dist/backends/persistent.js.map +1 -0
- package/dist/backends/sandbox.d.ts +315 -0
- package/dist/backends/sandbox.d.ts.map +1 -0
- package/dist/backends/sandbox.js +490 -0
- package/dist/backends/sandbox.js.map +1 -0
- package/dist/backends/state.d.ts +225 -0
- package/dist/backends/state.d.ts.map +1 -0
- package/dist/backends/state.js +396 -0
- package/dist/backends/state.js.map +1 -0
- package/dist/checkpointer/file-saver.d.ts +182 -0
- package/dist/checkpointer/file-saver.d.ts.map +1 -0
- package/dist/checkpointer/file-saver.js +298 -0
- package/dist/checkpointer/file-saver.js.map +1 -0
- package/dist/checkpointer/index.d.ts +40 -0
- package/dist/checkpointer/index.d.ts.map +1 -0
- package/dist/checkpointer/index.js +40 -0
- package/dist/checkpointer/index.js.map +1 -0
- package/dist/checkpointer/kv-saver.d.ts +142 -0
- package/dist/checkpointer/kv-saver.d.ts.map +1 -0
- package/dist/checkpointer/kv-saver.js +176 -0
- package/dist/checkpointer/kv-saver.js.map +1 -0
- package/dist/checkpointer/memory-saver.d.ts +158 -0
- package/dist/checkpointer/memory-saver.d.ts.map +1 -0
- package/dist/checkpointer/memory-saver.js +222 -0
- package/dist/checkpointer/memory-saver.js.map +1 -0
- package/dist/checkpointer/types.d.ts +353 -0
- package/dist/checkpointer/types.d.ts.map +1 -0
- package/dist/checkpointer/types.js +159 -0
- package/dist/checkpointer/types.js.map +1 -0
- package/dist/context-manager.d.ts +627 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +1039 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/context.d.ts +57 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +76 -0
- package/dist/context.js.map +1 -0
- package/dist/errors/index.d.ts +611 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +1023 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/generation-helpers.d.ts +126 -0
- package/dist/generation-helpers.d.ts.map +1 -0
- package/dist/generation-helpers.js +181 -0
- package/dist/generation-helpers.js.map +1 -0
- package/dist/hooks/audit.d.ts +210 -0
- package/dist/hooks/audit.d.ts.map +1 -0
- package/dist/hooks/audit.js +305 -0
- package/dist/hooks/audit.js.map +1 -0
- package/dist/hooks/cache.d.ts +180 -0
- package/dist/hooks/cache.d.ts.map +1 -0
- package/dist/hooks/cache.js +273 -0
- package/dist/hooks/cache.js.map +1 -0
- package/dist/hooks/guardrails.d.ts +145 -0
- package/dist/hooks/guardrails.d.ts.map +1 -0
- package/dist/hooks/guardrails.js +326 -0
- package/dist/hooks/guardrails.js.map +1 -0
- package/dist/hooks/index.d.ts +18 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +32 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/logging.d.ts +193 -0
- package/dist/hooks/logging.d.ts.map +1 -0
- package/dist/hooks/logging.js +345 -0
- package/dist/hooks/logging.js.map +1 -0
- package/dist/hooks/parallel-guardrails.d.ts +268 -0
- package/dist/hooks/parallel-guardrails.d.ts.map +1 -0
- package/dist/hooks/parallel-guardrails.js +416 -0
- package/dist/hooks/parallel-guardrails.js.map +1 -0
- package/dist/hooks/rate-limit.d.ts +305 -0
- package/dist/hooks/rate-limit.d.ts.map +1 -0
- package/dist/hooks/rate-limit.js +372 -0
- package/dist/hooks/rate-limit.js.map +1 -0
- package/dist/hooks/retry.d.ts +144 -0
- package/dist/hooks/retry.d.ts.map +1 -0
- package/dist/hooks/retry.js +210 -0
- package/dist/hooks/retry.js.map +1 -0
- package/dist/hooks/secrets.d.ts +174 -0
- package/dist/hooks/secrets.d.ts.map +1 -0
- package/dist/hooks/secrets.js +306 -0
- package/dist/hooks/secrets.js.map +1 -0
- package/dist/hooks.d.ts +229 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +352 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +182 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/env.d.ts +25 -0
- package/dist/mcp/env.d.ts.map +1 -0
- package/dist/mcp/env.js +18 -0
- package/dist/mcp/env.js.map +1 -0
- package/dist/mcp/index.d.ts +16 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +17 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/manager.d.ts +184 -0
- package/dist/mcp/manager.d.ts.map +1 -0
- package/dist/mcp/manager.js +446 -0
- package/dist/mcp/manager.js.map +1 -0
- package/dist/mcp/types.d.ts +58 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +7 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/mcp/validation.d.ts +119 -0
- package/dist/mcp/validation.d.ts.map +1 -0
- package/dist/mcp/validation.js +407 -0
- package/dist/mcp/validation.js.map +1 -0
- package/dist/mcp/virtual-server.d.ts +78 -0
- package/dist/mcp/virtual-server.d.ts.map +1 -0
- package/dist/mcp/virtual-server.js +137 -0
- package/dist/mcp/virtual-server.js.map +1 -0
- package/dist/memory/filesystem-store.d.ts +217 -0
- package/dist/memory/filesystem-store.d.ts.map +1 -0
- package/dist/memory/filesystem-store.js +343 -0
- package/dist/memory/filesystem-store.js.map +1 -0
- package/dist/memory/index.d.ts +46 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +46 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/loader.d.ts +396 -0
- package/dist/memory/loader.d.ts.map +1 -0
- package/dist/memory/loader.js +419 -0
- package/dist/memory/loader.js.map +1 -0
- package/dist/memory/permissions.d.ts +282 -0
- package/dist/memory/permissions.d.ts.map +1 -0
- package/dist/memory/permissions.js +297 -0
- package/dist/memory/permissions.js.map +1 -0
- package/dist/memory/rules.d.ts +249 -0
- package/dist/memory/rules.d.ts.map +1 -0
- package/dist/memory/rules.js +362 -0
- package/dist/memory/rules.js.map +1 -0
- package/dist/memory/store.d.ts +286 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +263 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/middleware/apply.d.ts +73 -0
- package/dist/middleware/apply.d.ts.map +1 -0
- package/dist/middleware/apply.js +219 -0
- package/dist/middleware/apply.js.map +1 -0
- package/dist/middleware/context.d.ts +33 -0
- package/dist/middleware/context.d.ts.map +1 -0
- package/dist/middleware/context.js +176 -0
- package/dist/middleware/context.js.map +1 -0
- package/dist/middleware/index.d.ts +31 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +32 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/logging.d.ts +137 -0
- package/dist/middleware/logging.d.ts.map +1 -0
- package/dist/middleware/logging.js +374 -0
- package/dist/middleware/logging.js.map +1 -0
- package/dist/middleware/types.d.ts +183 -0
- package/dist/middleware/types.d.ts.map +1 -0
- package/dist/middleware/types.js +11 -0
- package/dist/middleware/types.js.map +1 -0
- package/dist/observability/events.d.ts +183 -0
- package/dist/observability/events.d.ts.map +1 -0
- package/dist/observability/events.js +305 -0
- package/dist/observability/events.js.map +1 -0
- package/dist/observability/index.d.ts +55 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +87 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/logger.d.ts +318 -0
- package/dist/observability/logger.d.ts.map +1 -0
- package/dist/observability/logger.js +436 -0
- package/dist/observability/logger.js.map +1 -0
- package/dist/observability/metrics.d.ts +341 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +490 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/preset.d.ts +161 -0
- package/dist/observability/preset.d.ts.map +1 -0
- package/dist/observability/preset.js +133 -0
- package/dist/observability/preset.js.map +1 -0
- package/dist/observability/streaming.d.ts +113 -0
- package/dist/observability/streaming.d.ts.map +1 -0
- package/dist/observability/streaming.js +114 -0
- package/dist/observability/streaming.js.map +1 -0
- package/dist/observability/tracing.d.ts +378 -0
- package/dist/observability/tracing.d.ts.map +1 -0
- package/dist/observability/tracing.js +539 -0
- package/dist/observability/tracing.js.map +1 -0
- package/dist/plugins.d.ts +55 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +63 -0
- package/dist/plugins.js.map +1 -0
- package/dist/presets/index.d.ts +7 -0
- package/dist/presets/index.d.ts.map +1 -0
- package/dist/presets/index.js +7 -0
- package/dist/presets/index.js.map +1 -0
- package/dist/presets/production.d.ts +262 -0
- package/dist/presets/production.d.ts.map +1 -0
- package/dist/presets/production.js +295 -0
- package/dist/presets/production.js.map +1 -0
- package/dist/security/index.d.ts +179 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +323 -0
- package/dist/security/index.js.map +1 -0
- package/dist/subagents/advanced.d.ts +413 -0
- package/dist/subagents/advanced.d.ts.map +1 -0
- package/dist/subagents/advanced.js +396 -0
- package/dist/subagents/advanced.js.map +1 -0
- package/dist/subagents/index.d.ts +14 -0
- package/dist/subagents/index.d.ts.map +1 -0
- package/dist/subagents/index.js +15 -0
- package/dist/subagents/index.js.map +1 -0
- package/dist/subagents.d.ts +73 -0
- package/dist/subagents.d.ts.map +1 -0
- package/dist/subagents.js +213 -0
- package/dist/subagents.js.map +1 -0
- package/dist/task-store/file-store.d.ts +76 -0
- package/dist/task-store/file-store.d.ts.map +1 -0
- package/dist/task-store/file-store.js +190 -0
- package/dist/task-store/file-store.js.map +1 -0
- package/dist/task-store/index.d.ts +11 -0
- package/dist/task-store/index.d.ts.map +1 -0
- package/dist/task-store/index.js +10 -0
- package/dist/task-store/index.js.map +1 -0
- package/dist/task-store/kv-store.d.ts +140 -0
- package/dist/task-store/kv-store.d.ts.map +1 -0
- package/dist/task-store/kv-store.js +169 -0
- package/dist/task-store/kv-store.js.map +1 -0
- package/dist/task-store/memory-store.d.ts +66 -0
- package/dist/task-store/memory-store.d.ts.map +1 -0
- package/dist/task-store/memory-store.js +125 -0
- package/dist/task-store/memory-store.js.map +1 -0
- package/dist/task-store/types.d.ts +235 -0
- package/dist/task-store/types.d.ts.map +1 -0
- package/dist/task-store/types.js +110 -0
- package/dist/task-store/types.js.map +1 -0
- package/dist/testing/assertions.d.ts +401 -0
- package/dist/testing/assertions.d.ts.map +1 -0
- package/dist/testing/assertions.js +630 -0
- package/dist/testing/assertions.js.map +1 -0
- package/dist/testing/index.d.ts +343 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +360 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-agent.d.ts +214 -0
- package/dist/testing/mock-agent.d.ts.map +1 -0
- package/dist/testing/mock-agent.js +448 -0
- package/dist/testing/mock-agent.js.map +1 -0
- package/dist/testing/recorder.d.ts +288 -0
- package/dist/testing/recorder.d.ts.map +1 -0
- package/dist/testing/recorder.js +499 -0
- package/dist/testing/recorder.js.map +1 -0
- package/dist/tools/execute.d.ts +104 -0
- package/dist/tools/execute.d.ts.map +1 -0
- package/dist/tools/execute.js +191 -0
- package/dist/tools/execute.js.map +1 -0
- package/dist/tools/factory.d.ts +260 -0
- package/dist/tools/factory.d.ts.map +1 -0
- package/dist/tools/factory.js +241 -0
- package/dist/tools/factory.js.map +1 -0
- package/dist/tools/filesystem.d.ts +215 -0
- package/dist/tools/filesystem.d.ts.map +1 -0
- package/dist/tools/filesystem.js +311 -0
- package/dist/tools/filesystem.js.map +1 -0
- package/dist/tools/index.d.ts +33 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +33 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/search.d.ts +59 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +94 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/skills.d.ts +354 -0
- package/dist/tools/skills.d.ts.map +1 -0
- package/dist/tools/skills.js +413 -0
- package/dist/tools/skills.js.map +1 -0
- package/dist/tools/task.d.ts +272 -0
- package/dist/tools/task.d.ts.map +1 -0
- package/dist/tools/task.js +521 -0
- package/dist/tools/task.js.map +1 -0
- package/dist/tools/todos.d.ts +131 -0
- package/dist/tools/todos.d.ts.map +1 -0
- package/dist/tools/todos.js +120 -0
- package/dist/tools/todos.js.map +1 -0
- package/dist/tools/tool-registry.d.ts +424 -0
- package/dist/tools/tool-registry.d.ts.map +1 -0
- package/dist/tools/tool-registry.js +607 -0
- package/dist/tools/tool-registry.js.map +1 -0
- package/dist/tools/user-interaction.d.ts +116 -0
- package/dist/tools/user-interaction.d.ts.map +1 -0
- package/dist/tools/user-interaction.js +147 -0
- package/dist/tools/user-interaction.js.map +1 -0
- package/dist/tools/utils.d.ts +124 -0
- package/dist/tools/utils.d.ts.map +1 -0
- package/dist/tools/utils.js +189 -0
- package/dist/tools/utils.js.map +1 -0
- package/dist/tools.d.ts +74 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +73 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +2421 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +55 -0
- package/dist/types.js.map +1 -0
- package/package.json +81 -0
package/dist/agent.js
ADDED
|
@@ -0,0 +1,2122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core agent implementation.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { createUIMessageStream, createUIMessageStreamResponse, generateText, stepCountIs, streamText, } from "ai";
|
|
7
|
+
import { isSandboxBackend } from "./backend.js";
|
|
8
|
+
import { CommandBlockedError } from "./backends/sandbox.js";
|
|
9
|
+
import { createAgentState, StateBackend } from "./backends/state.js";
|
|
10
|
+
import { createCheckpoint, createInterrupt, isApprovalInterrupt, updateCheckpoint, } from "./checkpointer/types.js";
|
|
11
|
+
import { CheckpointError, ToolExecutionError, ToolPermissionDeniedError, } from "./errors/index.js";
|
|
12
|
+
import { createRetryLoopState, handleGenerationError, invokePreGenerateHooks, normalizeError, updateRetryLoopState, waitForRetryDelay, } from "./generation-helpers.js";
|
|
13
|
+
import { aggregatePermissionDecisions, extractUpdatedInput, extractUpdatedResult, invokeHooksWithTimeout, invokeMatchingHooks, } from "./hooks.js";
|
|
14
|
+
import { MCPManager } from "./mcp/manager.js";
|
|
15
|
+
import { applyMiddleware, mergeHooks, setupMiddleware } from "./middleware/index.js";
|
|
16
|
+
import { ACCEPT_EDITS_BLOCKED_PATTERNS } from "./security/index.js";
|
|
17
|
+
import { coreToolsToToolSet, createCoreTools, createSearchToolsTool, createTaskTool, } from "./tools/factory.js";
|
|
18
|
+
import { createUseToolsTool, ToolRegistry } from "./tools/tool-registry.js";
|
|
19
|
+
let agentIdCounter = 0;
|
|
20
|
+
/**
|
|
21
|
+
* Internal signal for interrupt flow control.
|
|
22
|
+
*
|
|
23
|
+
* This is thrown when a tool requires user approval or an interrupt is requested.
|
|
24
|
+
* It's caught by the generate() function and converted to an interrupted result.
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
class InterruptSignal extends Error {
|
|
29
|
+
interrupt;
|
|
30
|
+
constructor(interrupt) {
|
|
31
|
+
super(`Interrupt: ${interrupt.type}`);
|
|
32
|
+
this.name = "InterruptSignal";
|
|
33
|
+
this.interrupt = interrupt;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check if an error is an InterruptSignal.
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
function isInterruptSignal(error) {
|
|
41
|
+
return error instanceof InterruptSignal;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* File edit tool names that get auto-approved in acceptEdits mode.
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
const FILE_EDIT_TOOLS = new Set([
|
|
48
|
+
"write",
|
|
49
|
+
"edit",
|
|
50
|
+
// Bash commands that perform file operations (if we ever add them)
|
|
51
|
+
// For now, bash is not auto-approved even in acceptEdits mode
|
|
52
|
+
]);
|
|
53
|
+
/**
|
|
54
|
+
* Wraps a sandbox backend to add additional blocked command patterns.
|
|
55
|
+
* This creates a proxy that intercepts execute() calls and validates
|
|
56
|
+
* commands against the additional patterns before delegating.
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
function wrapSandboxWithBlockedPatterns(sandbox, additionalPatterns) {
|
|
60
|
+
// Create a proxy that intercepts execute() calls
|
|
61
|
+
return new Proxy(sandbox, {
|
|
62
|
+
get(target, prop, receiver) {
|
|
63
|
+
if (prop === "execute") {
|
|
64
|
+
return async (command) => {
|
|
65
|
+
// Check additional patterns before delegating
|
|
66
|
+
for (const pattern of additionalPatterns) {
|
|
67
|
+
if (pattern.test(command)) {
|
|
68
|
+
throw new CommandBlockedError(command, "Command blocked by acceptEdits shell file operation safety");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Delegate to original execute
|
|
72
|
+
return target.execute(command);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// For all other properties, delegate to target
|
|
76
|
+
return Reflect.get(target, prop, receiver);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Determines if an error should trigger fallback to an alternative model.
|
|
82
|
+
* @internal
|
|
83
|
+
*/
|
|
84
|
+
function shouldUseFallback(error) {
|
|
85
|
+
// Check error code for known fallback-triggering conditions
|
|
86
|
+
if (error.code === "RATE_LIMIT_ERROR" || error.code === "TIMEOUT_ERROR") {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
// Check for model unavailability or service errors
|
|
90
|
+
const message = error.message.toLowerCase();
|
|
91
|
+
const causeMessage = error.cause?.message?.toLowerCase() ?? "";
|
|
92
|
+
if (error.code === "MODEL_ERROR" ||
|
|
93
|
+
error.code === "UNKNOWN_ERROR" ||
|
|
94
|
+
error.code === "AGENT_ERROR") {
|
|
95
|
+
// Check both the AgentError message and the original error message
|
|
96
|
+
if (message.includes("unavailable") ||
|
|
97
|
+
message.includes("503") ||
|
|
98
|
+
message.includes("service unavailable") ||
|
|
99
|
+
causeMessage.includes("unavailable") ||
|
|
100
|
+
causeMessage.includes("503") ||
|
|
101
|
+
causeMessage.includes("service unavailable")) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Determines if an error is related to context length/token limits.
|
|
109
|
+
* @internal
|
|
110
|
+
*/
|
|
111
|
+
function isContextLengthError(error) {
|
|
112
|
+
const message = error.message.toLowerCase();
|
|
113
|
+
const causeMessage = error.cause?.message?.toLowerCase() ?? "";
|
|
114
|
+
// Check for common context length error patterns
|
|
115
|
+
const contextErrorPatterns = [
|
|
116
|
+
"context length",
|
|
117
|
+
"context_length",
|
|
118
|
+
"token limit",
|
|
119
|
+
"maximum context",
|
|
120
|
+
"too long",
|
|
121
|
+
"exceeds",
|
|
122
|
+
"max tokens",
|
|
123
|
+
"context size",
|
|
124
|
+
];
|
|
125
|
+
return contextErrorPatterns.some((pattern) => message.includes(pattern) || causeMessage.includes(pattern));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Check if a tool should be allowed based on permission mode.
|
|
129
|
+
* Returns "allow" or "deny" for definitive decisions, or undefined to defer to canUseTool callback.
|
|
130
|
+
* @internal
|
|
131
|
+
*/
|
|
132
|
+
function checkPermissionMode(toolName, mode) {
|
|
133
|
+
switch (mode) {
|
|
134
|
+
case "plan":
|
|
135
|
+
// Block all tool execution in plan mode
|
|
136
|
+
return "deny";
|
|
137
|
+
case "bypassPermissions":
|
|
138
|
+
// Allow all tools (dangerous - use only for testing/demos)
|
|
139
|
+
return "allow";
|
|
140
|
+
case "acceptEdits":
|
|
141
|
+
// Auto-approve file edit operations
|
|
142
|
+
return FILE_EDIT_TOOLS.has(toolName) ? "allow" : undefined;
|
|
143
|
+
default:
|
|
144
|
+
// Defer to canUseTool callback
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Wrap tools with permission mode checking and canUseTool callback.
|
|
150
|
+
* @internal
|
|
151
|
+
*/
|
|
152
|
+
function wrapToolsWithPermissionMode(tools, getPermissionMode, canUseTool, approvalState) {
|
|
153
|
+
const wrapped = {};
|
|
154
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
155
|
+
const originalExecute = tool.execute;
|
|
156
|
+
if (!originalExecute) {
|
|
157
|
+
// Skip tools without execute function
|
|
158
|
+
wrapped[name] = tool;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
// Create needsApproval function that bridges canUseTool to AI SDK's approval flow
|
|
162
|
+
// This allows the AI SDK to handle approval UI natively when canUseTool returns "ask"
|
|
163
|
+
const needsApproval = canUseTool
|
|
164
|
+
? async (input) => {
|
|
165
|
+
const mode = getPermissionMode();
|
|
166
|
+
const modeDecision = checkPermissionMode(name, mode);
|
|
167
|
+
// If permission mode denies, don't show approval UI (execute will throw)
|
|
168
|
+
if (modeDecision === "deny") {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
// If permission mode allows, no approval needed
|
|
172
|
+
if (modeDecision === "allow") {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
// Defer to canUseTool callback
|
|
176
|
+
const decision = await canUseTool(name, input);
|
|
177
|
+
return decision === "ask";
|
|
178
|
+
}
|
|
179
|
+
: tool.needsApproval; // Preserve original needsApproval if no canUseTool
|
|
180
|
+
wrapped[name] = {
|
|
181
|
+
...tool,
|
|
182
|
+
needsApproval,
|
|
183
|
+
execute: async (input, options) => {
|
|
184
|
+
const mode = getPermissionMode();
|
|
185
|
+
const modeDecision = checkPermissionMode(name, mode);
|
|
186
|
+
// Create the interrupt function for tool execution
|
|
187
|
+
const toolCallId = options?.toolCallId ?? `call_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
188
|
+
const threadId = approvalState?.threadId ?? "unknown";
|
|
189
|
+
const step = approvalState?.step ?? 0;
|
|
190
|
+
const interrupt = async (request, interruptOptions) => {
|
|
191
|
+
const interruptType = interruptOptions?.type ?? "custom";
|
|
192
|
+
const interruptId = `int_${toolCallId}`;
|
|
193
|
+
// Check if we have a pending response for this interrupt
|
|
194
|
+
if (approvalState?.pendingResponses.has(interruptId)) {
|
|
195
|
+
const response = approvalState.pendingResponses.get(interruptId);
|
|
196
|
+
// Clear the response after use
|
|
197
|
+
approvalState.pendingResponses.delete(interruptId);
|
|
198
|
+
return response;
|
|
199
|
+
}
|
|
200
|
+
// No response yet - create and throw an interrupt
|
|
201
|
+
if (!approvalState?.checkpointSaver) {
|
|
202
|
+
throw new ToolExecutionError(`Tool "${name}" called interrupt() but no checkpointer is configured`, {
|
|
203
|
+
toolName: name,
|
|
204
|
+
toolInput: input,
|
|
205
|
+
metadata: { interruptType, request },
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const interruptData = createInterrupt({
|
|
209
|
+
id: interruptId,
|
|
210
|
+
threadId,
|
|
211
|
+
type: interruptType,
|
|
212
|
+
toolCallId,
|
|
213
|
+
toolName: name,
|
|
214
|
+
request,
|
|
215
|
+
step,
|
|
216
|
+
});
|
|
217
|
+
throw new InterruptSignal(interruptData);
|
|
218
|
+
};
|
|
219
|
+
// Create extended options with the interrupt function
|
|
220
|
+
const extendedOptions = {
|
|
221
|
+
...options,
|
|
222
|
+
interrupt,
|
|
223
|
+
};
|
|
224
|
+
// If permission mode gives a definitive answer, use it
|
|
225
|
+
if (modeDecision === "allow") {
|
|
226
|
+
// Execute the original tool
|
|
227
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
|
|
228
|
+
return originalExecute.call(tool, input, extendedOptions);
|
|
229
|
+
}
|
|
230
|
+
if (modeDecision === "deny") {
|
|
231
|
+
// Denied by permission mode
|
|
232
|
+
const errorMessage = mode === "plan"
|
|
233
|
+
? `Tool "${name}" is blocked in plan mode (planning/analysis only)`
|
|
234
|
+
: `Tool "${name}" requires permission approval`;
|
|
235
|
+
throw new ToolExecutionError(errorMessage, {
|
|
236
|
+
toolName: name,
|
|
237
|
+
toolInput: input,
|
|
238
|
+
metadata: { permissionMode: mode },
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
// Permission mode deferred to canUseTool callback
|
|
242
|
+
if (canUseTool) {
|
|
243
|
+
const callbackDecision = await canUseTool(name, input);
|
|
244
|
+
if (callbackDecision === "allow") {
|
|
245
|
+
// Execute the original tool
|
|
246
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
|
|
247
|
+
return originalExecute.call(tool, input, extendedOptions);
|
|
248
|
+
}
|
|
249
|
+
if (callbackDecision === "deny") {
|
|
250
|
+
throw new ToolExecutionError(`Tool "${name}" was denied by canUseTool callback`, {
|
|
251
|
+
toolName: name,
|
|
252
|
+
toolInput: input,
|
|
253
|
+
metadata: { permissionMode: mode, decision: callbackDecision },
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (callbackDecision === "ask") {
|
|
257
|
+
// When canUseTool returns "ask", we need to determine the execution path:
|
|
258
|
+
// 1. AI SDK streaming: needsApproval (set above) triggers approval UI,
|
|
259
|
+
// and execute() is only called after user approves
|
|
260
|
+
// 2. Direct tool execution: execute() is called directly without AI SDK,
|
|
261
|
+
// so we need to throw an error to require approval
|
|
262
|
+
const toolUseId = options?.toolCallId;
|
|
263
|
+
if (toolUseId && approvalState) {
|
|
264
|
+
// Check for explicit denial in pending responses
|
|
265
|
+
const pendingResponse = approvalState.pendingResponses.get(toolUseId);
|
|
266
|
+
if (pendingResponse !== undefined) {
|
|
267
|
+
const response = pendingResponse;
|
|
268
|
+
if (response.approved === false) {
|
|
269
|
+
throw new ToolExecutionError(`Tool "${name}" was denied by user${response.reason ? `: ${response.reason}` : ""}`, {
|
|
270
|
+
toolName: name,
|
|
271
|
+
toolInput: input,
|
|
272
|
+
metadata: {
|
|
273
|
+
permissionMode: mode,
|
|
274
|
+
decision: "deny",
|
|
275
|
+
toolUseId,
|
|
276
|
+
reason: response.reason,
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
// Has approval response - user approved, continue to execution
|
|
281
|
+
if (response.approved === true) {
|
|
282
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
|
|
283
|
+
return originalExecute.call(tool, input, extendedOptions);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Check legacy approval system
|
|
287
|
+
const decision = approvalState.approvalDecisions.get(toolUseId);
|
|
288
|
+
if (decision === false) {
|
|
289
|
+
throw new ToolExecutionError(`Tool "${name}" was denied by user`, {
|
|
290
|
+
toolName: name,
|
|
291
|
+
toolInput: input,
|
|
292
|
+
metadata: {
|
|
293
|
+
permissionMode: mode,
|
|
294
|
+
decision: "deny",
|
|
295
|
+
toolUseId,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
if (decision === true) {
|
|
300
|
+
// Explicitly approved via legacy system
|
|
301
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
|
|
302
|
+
return originalExecute.call(tool, input, extendedOptions);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// No approval decision exists yet.
|
|
306
|
+
// For AI SDK streaming, this path shouldn't be reached because
|
|
307
|
+
// needsApproval returned true and AI SDK won't call execute.
|
|
308
|
+
// For direct calls without AI SDK, we throw an error.
|
|
309
|
+
throw new ToolExecutionError(`Tool "${name}" requires user approval but no checkpointer is configured`, {
|
|
310
|
+
toolName: name,
|
|
311
|
+
toolInput: input,
|
|
312
|
+
metadata: {
|
|
313
|
+
permissionMode: mode,
|
|
314
|
+
decision: "ask",
|
|
315
|
+
reason: "Direct tool call requires approval - use AI SDK streaming for approval UI",
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// No canUseTool callback - default to allow in default mode
|
|
321
|
+
// This preserves backward compatibility where tools work by default
|
|
322
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
|
|
323
|
+
return originalExecute.call(tool, input, extendedOptions);
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
return wrapped;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Wraps tools to emit PreToolUse/PostToolUse/PostToolUseFailure hooks.
|
|
331
|
+
*
|
|
332
|
+
* This enables observability hooks (logging, metrics, tracing) and guardrails
|
|
333
|
+
* (rate-limiting, audit, permission checks) to fire during tool execution.
|
|
334
|
+
*
|
|
335
|
+
* @param tools - The tools to wrap
|
|
336
|
+
* @param hookRegistration - The hook registration containing tool hook matchers
|
|
337
|
+
* @param agent - The agent instance
|
|
338
|
+
* @param sessionId - The session ID for hook input
|
|
339
|
+
* @returns Wrapped tools that emit hooks
|
|
340
|
+
*
|
|
341
|
+
* @internal
|
|
342
|
+
*/
|
|
343
|
+
function wrapToolsWithHooks(tools, hookRegistration, agent, sessionId) {
|
|
344
|
+
// If no tool hooks are registered, return tools unchanged
|
|
345
|
+
if (!hookRegistration?.PreToolUse?.length &&
|
|
346
|
+
!hookRegistration?.PostToolUse?.length &&
|
|
347
|
+
!hookRegistration?.PostToolUseFailure?.length) {
|
|
348
|
+
return tools;
|
|
349
|
+
}
|
|
350
|
+
const wrapped = {};
|
|
351
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
352
|
+
if (!tool.execute) {
|
|
353
|
+
// Tool has no execute function (e.g., client-side only tool)
|
|
354
|
+
wrapped[name] = tool;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const originalExecute = tool.execute;
|
|
358
|
+
wrapped[name] = {
|
|
359
|
+
...tool,
|
|
360
|
+
execute: async (input, options) => {
|
|
361
|
+
const toolUseId = options?.toolCallId ?? `tool-${Date.now()}`;
|
|
362
|
+
// Create PreToolUse input
|
|
363
|
+
const preToolUseInput = {
|
|
364
|
+
hook_event_name: "PreToolUse",
|
|
365
|
+
session_id: sessionId,
|
|
366
|
+
cwd: process.cwd(),
|
|
367
|
+
tool_name: name,
|
|
368
|
+
tool_input: input,
|
|
369
|
+
};
|
|
370
|
+
// Invoke PreToolUse hooks
|
|
371
|
+
if (hookRegistration?.PreToolUse?.length) {
|
|
372
|
+
const preHookOutputs = await invokeMatchingHooks(hookRegistration.PreToolUse, name, preToolUseInput, toolUseId, agent);
|
|
373
|
+
// Check permission decisions
|
|
374
|
+
const permissionDecision = aggregatePermissionDecisions(preHookOutputs);
|
|
375
|
+
if (permissionDecision === "deny") {
|
|
376
|
+
// Find the reason from hook outputs
|
|
377
|
+
const reason = preHookOutputs.find((o) => o.hookSpecificOutput?.permissionDecisionReason)?.hookSpecificOutput?.permissionDecisionReason;
|
|
378
|
+
const error = new ToolPermissionDeniedError(`Tool '${name}' execution denied by hook`, {
|
|
379
|
+
toolName: name,
|
|
380
|
+
toolInput: input,
|
|
381
|
+
reason,
|
|
382
|
+
});
|
|
383
|
+
// Emit PostToolUseFailure for denied tools
|
|
384
|
+
if (hookRegistration?.PostToolUseFailure?.length) {
|
|
385
|
+
const failureInput = {
|
|
386
|
+
hook_event_name: "PostToolUseFailure",
|
|
387
|
+
session_id: sessionId,
|
|
388
|
+
cwd: process.cwd(),
|
|
389
|
+
tool_name: name,
|
|
390
|
+
tool_input: input,
|
|
391
|
+
error,
|
|
392
|
+
};
|
|
393
|
+
await invokeMatchingHooks(hookRegistration.PostToolUseFailure, name, failureInput, toolUseId, agent);
|
|
394
|
+
}
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
397
|
+
// Check for input transformation
|
|
398
|
+
const updatedInput = extractUpdatedInput(preHookOutputs);
|
|
399
|
+
if (updatedInput !== undefined) {
|
|
400
|
+
input = updatedInput;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
// Execute the original tool with (potentially modified) input
|
|
405
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
|
|
406
|
+
const output = await originalExecute.call(tool, input, options);
|
|
407
|
+
// Invoke PostToolUse hooks
|
|
408
|
+
if (hookRegistration?.PostToolUse?.length) {
|
|
409
|
+
const postToolUseInput = {
|
|
410
|
+
hook_event_name: "PostToolUse",
|
|
411
|
+
session_id: sessionId,
|
|
412
|
+
cwd: process.cwd(),
|
|
413
|
+
tool_name: name,
|
|
414
|
+
tool_input: input,
|
|
415
|
+
tool_response: output,
|
|
416
|
+
};
|
|
417
|
+
const postHookOutputs = await invokeMatchingHooks(hookRegistration.PostToolUse, name, postToolUseInput, toolUseId, agent);
|
|
418
|
+
// Check for output transformation
|
|
419
|
+
const updatedResult = extractUpdatedResult(postHookOutputs);
|
|
420
|
+
if (updatedResult !== undefined) {
|
|
421
|
+
return updatedResult;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return output;
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
// Invoke PostToolUseFailure hooks
|
|
428
|
+
if (hookRegistration?.PostToolUseFailure?.length) {
|
|
429
|
+
const failureInput = {
|
|
430
|
+
hook_event_name: "PostToolUseFailure",
|
|
431
|
+
session_id: sessionId,
|
|
432
|
+
cwd: process.cwd(),
|
|
433
|
+
tool_name: name,
|
|
434
|
+
tool_input: input,
|
|
435
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
436
|
+
};
|
|
437
|
+
await invokeMatchingHooks(hookRegistration.PostToolUseFailure, name, failureInput, toolUseId, agent);
|
|
438
|
+
}
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return wrapped;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Check if a value is a backend factory function.
|
|
448
|
+
*/
|
|
449
|
+
function isBackendFactory(value) {
|
|
450
|
+
return typeof value === "function";
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Creates a new agent instance with the specified configuration.
|
|
454
|
+
*
|
|
455
|
+
* Agents are the main abstraction for interacting with AI models. They combine
|
|
456
|
+
* a language model with tools, plugins, and hooks to create intelligent assistants.
|
|
457
|
+
*
|
|
458
|
+
* @param options - Configuration options for the agent
|
|
459
|
+
* @returns A configured agent instance
|
|
460
|
+
*
|
|
461
|
+
* @example
|
|
462
|
+
* ```typescript
|
|
463
|
+
* import { createAgent } from "@lleverage-ai/agent-sdk";
|
|
464
|
+
* import { anthropic } from "@ai-sdk/anthropic";
|
|
465
|
+
* import { tool } from "ai";
|
|
466
|
+
* import { z } from "zod";
|
|
467
|
+
*
|
|
468
|
+
* const agent = createAgent({
|
|
469
|
+
* model: anthropic("claude-sonnet-4-20250514"),
|
|
470
|
+
* systemPrompt: "You are a helpful assistant.",
|
|
471
|
+
* tools: {
|
|
472
|
+
* weather: tool({
|
|
473
|
+
* description: "Get weather for a city",
|
|
474
|
+
* inputSchema: z.object({ city: z.string() }),
|
|
475
|
+
* execute: async ({ city }) => `Weather in ${city}: sunny`,
|
|
476
|
+
* }),
|
|
477
|
+
* },
|
|
478
|
+
* });
|
|
479
|
+
*
|
|
480
|
+
* const result = await agent.generate({
|
|
481
|
+
* prompt: "What's the weather in Tokyo?",
|
|
482
|
+
* });
|
|
483
|
+
* ```
|
|
484
|
+
*
|
|
485
|
+
* @example
|
|
486
|
+
* ```typescript
|
|
487
|
+
* // Use in a Next.js API route with useChat
|
|
488
|
+
* export async function POST(req: Request) {
|
|
489
|
+
* const { messages } = await req.json();
|
|
490
|
+
* return agent.streamResponse({ messages });
|
|
491
|
+
* }
|
|
492
|
+
* ```
|
|
493
|
+
*
|
|
494
|
+
* @category Agent
|
|
495
|
+
*/
|
|
496
|
+
export function createAgent(options) {
|
|
497
|
+
const id = `agent-${++agentIdCounter}`;
|
|
498
|
+
// Process middleware to get hooks (middleware hooks come before explicit hooks)
|
|
499
|
+
const middleware = options.middleware ?? [];
|
|
500
|
+
const middlewareHooks = applyMiddleware(middleware);
|
|
501
|
+
const mergedHooks = mergeHooks(middlewareHooks, options.hooks);
|
|
502
|
+
// Create options with merged hooks for all hook lookups
|
|
503
|
+
const effectiveHooks = mergedHooks;
|
|
504
|
+
// Permission mode (mutable for setPermissionMode)
|
|
505
|
+
let permissionMode = options.permissionMode ?? "default";
|
|
506
|
+
// Store approval decisions in-memory (keyed by toolUseId)
|
|
507
|
+
// In production, this could be persisted via checkpointer
|
|
508
|
+
const approvalDecisions = new Map();
|
|
509
|
+
// Store pending interrupt responses (keyed by interrupt ID or tool call ID)
|
|
510
|
+
// Used by the new interrupt/resume system
|
|
511
|
+
const pendingResponses = new Map();
|
|
512
|
+
// Initialize agent state (shared with backend if using factory)
|
|
513
|
+
const state = createAgentState();
|
|
514
|
+
// Initialize backend - default to StateBackend if not provided
|
|
515
|
+
let backend;
|
|
516
|
+
if (options.backend) {
|
|
517
|
+
if (isBackendFactory(options.backend)) {
|
|
518
|
+
// Factory function - create backend with shared state
|
|
519
|
+
backend = options.backend(state);
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
// Direct backend instance
|
|
523
|
+
backend = options.backend;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
// Default: StateBackend with shared state
|
|
528
|
+
backend = new StateBackend(state);
|
|
529
|
+
}
|
|
530
|
+
// Determine plugin loading mode
|
|
531
|
+
const pluginLoadingMode = options.pluginLoading ?? "eager";
|
|
532
|
+
const preloadPlugins = new Set(options.preloadPlugins ?? []);
|
|
533
|
+
// Initialize tool registry for lazy/explicit loading modes
|
|
534
|
+
const toolRegistry = pluginLoadingMode !== "eager"
|
|
535
|
+
? new ToolRegistry({
|
|
536
|
+
onToolRegistered: async (input) => {
|
|
537
|
+
const hooks = effectiveHooks?.ToolRegistered ?? [];
|
|
538
|
+
if (hooks.length === 0)
|
|
539
|
+
return;
|
|
540
|
+
const hookInput = {
|
|
541
|
+
hook_event_name: "ToolRegistered",
|
|
542
|
+
session_id: "default",
|
|
543
|
+
cwd: process.cwd(),
|
|
544
|
+
tool_name: input.tool_name,
|
|
545
|
+
description: input.description,
|
|
546
|
+
source: input.source,
|
|
547
|
+
};
|
|
548
|
+
await invokeHooksWithTimeout(hooks, hookInput, null, agent);
|
|
549
|
+
},
|
|
550
|
+
onToolLoadError: async (input) => {
|
|
551
|
+
const hooks = effectiveHooks?.ToolLoadError ?? [];
|
|
552
|
+
if (hooks.length === 0)
|
|
553
|
+
return;
|
|
554
|
+
const hookInput = {
|
|
555
|
+
hook_event_name: "ToolLoadError",
|
|
556
|
+
session_id: "default",
|
|
557
|
+
cwd: process.cwd(),
|
|
558
|
+
tool_name: input.tool_name,
|
|
559
|
+
error: input.error,
|
|
560
|
+
source: input.source,
|
|
561
|
+
};
|
|
562
|
+
await invokeHooksWithTimeout(hooks, hookInput, null, agent);
|
|
563
|
+
},
|
|
564
|
+
})
|
|
565
|
+
: undefined;
|
|
566
|
+
// Collect skills from options and plugins
|
|
567
|
+
const skills = [...(options.skills ?? [])];
|
|
568
|
+
// Initialize MCP manager for unified plugin tool handling
|
|
569
|
+
// Note: The callbacks reference `agent` which is defined later, but they
|
|
570
|
+
// won't be called until MCP connections happen in initPromise, by which
|
|
571
|
+
// time `agent` is already defined.
|
|
572
|
+
const mcpManager = new MCPManager({
|
|
573
|
+
onConnectionFailed: async (input) => {
|
|
574
|
+
const hooks = effectiveHooks?.MCPConnectionFailed ?? [];
|
|
575
|
+
if (hooks.length === 0)
|
|
576
|
+
return;
|
|
577
|
+
const hookInput = {
|
|
578
|
+
hook_event_name: "MCPConnectionFailed",
|
|
579
|
+
session_id: "default",
|
|
580
|
+
cwd: process.cwd(),
|
|
581
|
+
server_name: input.server_name,
|
|
582
|
+
config: input.config,
|
|
583
|
+
error: input.error,
|
|
584
|
+
};
|
|
585
|
+
await invokeHooksWithTimeout(hooks, hookInput, null, agent);
|
|
586
|
+
},
|
|
587
|
+
onConnectionRestored: async (input) => {
|
|
588
|
+
const hooks = effectiveHooks?.MCPConnectionRestored ?? [];
|
|
589
|
+
if (hooks.length === 0)
|
|
590
|
+
return;
|
|
591
|
+
const hookInput = {
|
|
592
|
+
hook_event_name: "MCPConnectionRestored",
|
|
593
|
+
session_id: "default",
|
|
594
|
+
cwd: process.cwd(),
|
|
595
|
+
server_name: input.server_name,
|
|
596
|
+
tool_count: input.tool_count,
|
|
597
|
+
};
|
|
598
|
+
await invokeHooksWithTimeout(hooks, hookInput, null, agent);
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
// Determine sandbox backend if available
|
|
602
|
+
let sandbox = isSandboxBackend(backend) ? backend : undefined;
|
|
603
|
+
// Apply acceptEdits shell file operation blocking
|
|
604
|
+
// When permissionMode is "acceptEdits" and a sandbox is available,
|
|
605
|
+
// automatically block shell-based file operations unless explicitly disabled
|
|
606
|
+
if (permissionMode === "acceptEdits" && sandbox) {
|
|
607
|
+
const blockShellFileOps = options.blockShellFileOps ?? true;
|
|
608
|
+
if (blockShellFileOps) {
|
|
609
|
+
// Wrap the sandbox to block shell file operations
|
|
610
|
+
sandbox = wrapSandboxWithBlockedPatterns(sandbox, ACCEPT_EDITS_BLOCKED_PATTERNS);
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
// User explicitly disabled shell blocking - log a warning
|
|
614
|
+
console.warn("[agent-sdk] Warning: blockShellFileOps is disabled in acceptEdits mode. " +
|
|
615
|
+
"Shell commands like 'echo > file', 'rm', 'mv', etc. can bypass file edit permissions. " +
|
|
616
|
+
"This is not recommended for production use.");
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// Tool search configuration
|
|
620
|
+
const toolSearchConfig = options.toolSearch ?? {};
|
|
621
|
+
const toolSearchEnabled = toolSearchConfig.enabled ?? "auto";
|
|
622
|
+
const toolSearchThreshold = toolSearchConfig.threshold ?? 20;
|
|
623
|
+
const toolSearchMaxResults = toolSearchConfig.maxResults ?? 10;
|
|
624
|
+
// Track whether deferred loading is active
|
|
625
|
+
let deferredLoadingActive = false;
|
|
626
|
+
// Count total plugin tools for threshold calculation and collect plugin skills.
|
|
627
|
+
// Note: Function-based (streaming) tools are not counted since we don't know
|
|
628
|
+
// their count until they're invoked with a streaming context.
|
|
629
|
+
// IMPORTANT: Plugin skills must be collected BEFORE createCoreTools() is called
|
|
630
|
+
// so the skill tool includes them in progressive disclosure.
|
|
631
|
+
let totalPluginToolCount = 0;
|
|
632
|
+
for (const plugin of options.plugins ?? []) {
|
|
633
|
+
if (plugin.tools && typeof plugin.tools !== "function") {
|
|
634
|
+
totalPluginToolCount += Object.keys(plugin.tools).length;
|
|
635
|
+
}
|
|
636
|
+
// Collect plugin skills early so they're available for skill tool creation
|
|
637
|
+
if (plugin.skills) {
|
|
638
|
+
skills.push(...plugin.skills);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// Determine if we should use deferred loading based on tool search settings
|
|
642
|
+
// Note: "auto" mode enables deferred loading when tool count exceeds threshold
|
|
643
|
+
if (toolSearchEnabled === "always") {
|
|
644
|
+
deferredLoadingActive = true;
|
|
645
|
+
}
|
|
646
|
+
else if (toolSearchEnabled === "auto" && totalPluginToolCount > toolSearchThreshold) {
|
|
647
|
+
deferredLoadingActive = true;
|
|
648
|
+
}
|
|
649
|
+
// Auto-create core tools (unless user provides explicit tools)
|
|
650
|
+
// Note: search_tools is created separately with proper configuration
|
|
651
|
+
const autoCreatedCoreTools = createCoreTools({
|
|
652
|
+
backend,
|
|
653
|
+
state,
|
|
654
|
+
sandbox,
|
|
655
|
+
mcpManager: deferredLoadingActive ? undefined : mcpManager, // Only pass if not deferred
|
|
656
|
+
disabled: options.disabledCoreTools,
|
|
657
|
+
skills,
|
|
658
|
+
});
|
|
659
|
+
// Start with auto-created core tools, then overlay user-provided tools
|
|
660
|
+
const coreTools = {
|
|
661
|
+
...coreToolsToToolSet(autoCreatedCoreTools),
|
|
662
|
+
...(options.tools ?? {}),
|
|
663
|
+
};
|
|
664
|
+
// Process plugins based on loading mode and deferred loading
|
|
665
|
+
// Note: Plugin skills are collected earlier (before createCoreTools) so
|
|
666
|
+
// the skill tool can include them in progressive disclosure.
|
|
667
|
+
for (const plugin of options.plugins ?? []) {
|
|
668
|
+
// Handle tools via MCP manager for unified interface
|
|
669
|
+
// Note: Function-based (streaming) tools are handled separately in
|
|
670
|
+
// getActiveToolSetWithStreaming() and are not registered here
|
|
671
|
+
if (plugin.tools && typeof plugin.tools !== "function") {
|
|
672
|
+
const shouldPreload = preloadPlugins.has(plugin.name);
|
|
673
|
+
if (pluginLoadingMode === "lazy" && toolRegistry) {
|
|
674
|
+
// Lazy mode: register with registry for on-demand loading
|
|
675
|
+
toolRegistry.registerPlugin(plugin.name, plugin.tools);
|
|
676
|
+
}
|
|
677
|
+
else if (deferredLoadingActive && !shouldPreload) {
|
|
678
|
+
// Deferred loading: register tools but don't load them initially
|
|
679
|
+
mcpManager.registerPluginTools(plugin.name, plugin.tools, {
|
|
680
|
+
autoLoad: false,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
else if (pluginLoadingMode === "eager" || shouldPreload) {
|
|
684
|
+
// Eager mode or preloaded: register and load immediately
|
|
685
|
+
mcpManager.registerPluginTools(plugin.name, plugin.tools, {
|
|
686
|
+
autoLoad: true,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
// explicit mode: don't register, user must do it manually
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
// Create search_tools with load capability when deferred loading is active
|
|
693
|
+
if (deferredLoadingActive && !options.disabledCoreTools?.includes("search_tools")) {
|
|
694
|
+
coreTools.search_tools = createSearchToolsTool({
|
|
695
|
+
manager: mcpManager,
|
|
696
|
+
maxResults: toolSearchMaxResults,
|
|
697
|
+
enableLoad: true,
|
|
698
|
+
onToolsLoaded: (toolNames) => {
|
|
699
|
+
// Tools are now loaded in MCPManager and will be included in getActiveToolSet()
|
|
700
|
+
// This callback can be used for logging/notifications
|
|
701
|
+
},
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
// Add use_tools meta-tool in lazy mode
|
|
705
|
+
if (pluginLoadingMode === "lazy" && toolRegistry) {
|
|
706
|
+
coreTools.use_tools = createUseToolsTool({ registry: toolRegistry });
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Filter a tool set by the allowedTools and disallowedTools restrictions.
|
|
710
|
+
* If neither is set, returns all tools.
|
|
711
|
+
*
|
|
712
|
+
* Priority: disallowedTools takes precedence over allowedTools.
|
|
713
|
+
* If a tool is in both lists, it is blocked.
|
|
714
|
+
*/
|
|
715
|
+
const filterToolsByAllowed = (toolSet) => {
|
|
716
|
+
const allowed = options.allowedTools;
|
|
717
|
+
const disallowed = options.disallowedTools;
|
|
718
|
+
// If neither restriction is set, return all tools
|
|
719
|
+
if ((!allowed || allowed.length === 0) && (!disallowed || disallowed.length === 0)) {
|
|
720
|
+
return toolSet;
|
|
721
|
+
}
|
|
722
|
+
const allowedSet = allowed ? new Set(allowed) : null;
|
|
723
|
+
const disallowedSet = disallowed ? new Set(disallowed) : null;
|
|
724
|
+
const filtered = {};
|
|
725
|
+
for (const [name, tool] of Object.entries(toolSet)) {
|
|
726
|
+
// If disallowedTools is set and tool is in it, skip
|
|
727
|
+
if (disallowedSet?.has(name)) {
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
// If allowedTools is set, only include if in the list
|
|
731
|
+
if (allowedSet) {
|
|
732
|
+
if (allowedSet.has(name)) {
|
|
733
|
+
filtered[name] = tool;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
// No allowedTools restriction, include if not disallowed
|
|
738
|
+
filtered[name] = tool;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return filtered;
|
|
742
|
+
};
|
|
743
|
+
// Helper to get current active tools (core + MCP + dynamically loaded from registry)
|
|
744
|
+
const getActiveToolSet = (threadId) => {
|
|
745
|
+
// Start with core tools
|
|
746
|
+
const allTools = { ...coreTools };
|
|
747
|
+
// Add MCP tools from plugin registrations
|
|
748
|
+
const mcpTools = mcpManager.getToolSet();
|
|
749
|
+
Object.assign(allTools, mcpTools);
|
|
750
|
+
// Add dynamically loaded tools from registry (lazy mode)
|
|
751
|
+
if (toolRegistry) {
|
|
752
|
+
Object.assign(allTools, toolRegistry.getLoadedTools());
|
|
753
|
+
}
|
|
754
|
+
// Apply allowedTools filtering
|
|
755
|
+
const filtered = filterToolsByAllowed(allTools);
|
|
756
|
+
// Apply permission mode wrapping with canUseTool callback and approval state
|
|
757
|
+
const withPermissions = wrapToolsWithPermissionMode(filtered, () => permissionMode, options.canUseTool, {
|
|
758
|
+
approvalDecisions,
|
|
759
|
+
pendingResponses,
|
|
760
|
+
checkpointSaver: options.checkpointer,
|
|
761
|
+
threadId,
|
|
762
|
+
});
|
|
763
|
+
// Note: Tool hooks are NOT applied here - they are applied at usage sites
|
|
764
|
+
// AFTER the task tool is added via addTaskToolIfConfigured. This ensures
|
|
765
|
+
// the task tool is also wrapped with hooks for logging/metrics.
|
|
766
|
+
return withPermissions;
|
|
767
|
+
};
|
|
768
|
+
/**
|
|
769
|
+
* Rebuild tools with streaming context for plugins with function-based tools.
|
|
770
|
+
* This enables tools to stream custom data to the client via ctx.writer.write().
|
|
771
|
+
*/
|
|
772
|
+
const getActiveToolSetWithStreaming = (streamingContext, threadId, step) => {
|
|
773
|
+
// Start with core tools
|
|
774
|
+
const allTools = { ...coreTools };
|
|
775
|
+
// Process plugins - invoke function-based tools with streaming context
|
|
776
|
+
for (const plugin of options.plugins ?? []) {
|
|
777
|
+
if (plugin.tools) {
|
|
778
|
+
if (typeof plugin.tools === "function") {
|
|
779
|
+
// Streaming-aware tools: invoke factory with context
|
|
780
|
+
const streamingTools = plugin.tools(streamingContext);
|
|
781
|
+
Object.assign(allTools, streamingTools);
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
// Static tools: use as-is
|
|
785
|
+
Object.assign(allTools, plugin.tools);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
// Add MCP tools from plugin registrations
|
|
790
|
+
const mcpTools = mcpManager.getToolSet();
|
|
791
|
+
Object.assign(allTools, mcpTools);
|
|
792
|
+
// Add dynamically loaded tools from registry (lazy mode)
|
|
793
|
+
if (toolRegistry) {
|
|
794
|
+
Object.assign(allTools, toolRegistry.getLoadedTools());
|
|
795
|
+
}
|
|
796
|
+
// Apply allowedTools filtering
|
|
797
|
+
const filtered = filterToolsByAllowed(allTools);
|
|
798
|
+
// Apply permission mode wrapping with canUseTool callback and approval state
|
|
799
|
+
const withPermissions = wrapToolsWithPermissionMode(filtered, () => permissionMode, options.canUseTool, {
|
|
800
|
+
approvalDecisions,
|
|
801
|
+
pendingResponses,
|
|
802
|
+
checkpointSaver: options.checkpointer,
|
|
803
|
+
threadId,
|
|
804
|
+
step,
|
|
805
|
+
});
|
|
806
|
+
// Note: Tool hooks are NOT applied here - they are applied at usage sites
|
|
807
|
+
// AFTER the task tool is added via addTaskToolIfConfigured. This ensures
|
|
808
|
+
// the task tool is also wrapped with hooks for logging/metrics.
|
|
809
|
+
return withPermissions;
|
|
810
|
+
};
|
|
811
|
+
/**
|
|
812
|
+
* Wraps all tools (including task tool) with hooks.
|
|
813
|
+
* Call this AFTER addTaskToolIfConfigured to ensure task tool is also wrapped.
|
|
814
|
+
*/
|
|
815
|
+
const applyToolHooks = (tools, threadId) => {
|
|
816
|
+
return wrapToolsWithHooks(tools, effectiveHooks, agent, threadId ?? "default");
|
|
817
|
+
};
|
|
818
|
+
/**
|
|
819
|
+
* Adds the task tool to a toolset when subagents are configured.
|
|
820
|
+
*
|
|
821
|
+
* This enables the agent to delegate work to specialized subagents via the
|
|
822
|
+
* task tool. The streaming context is only passed when using streamDataResponse(),
|
|
823
|
+
* allowing streaming subagents to write to the parent's data stream.
|
|
824
|
+
*
|
|
825
|
+
* @param tools - The base toolset to augment
|
|
826
|
+
* @param streamingContext - Optional streaming context for streaming subagents
|
|
827
|
+
* @returns The toolset with task tool added (if subagents configured)
|
|
828
|
+
*/
|
|
829
|
+
const addTaskToolIfConfigured = (tools, streamingContext) => {
|
|
830
|
+
// Skip if no subagents configured
|
|
831
|
+
if (!options.subagents || options.subagents.length === 0) {
|
|
832
|
+
return tools;
|
|
833
|
+
}
|
|
834
|
+
// Respect disabledCoreTools setting
|
|
835
|
+
if (options.disabledCoreTools?.includes("task")) {
|
|
836
|
+
return tools;
|
|
837
|
+
}
|
|
838
|
+
return {
|
|
839
|
+
...tools,
|
|
840
|
+
task: createTaskTool({
|
|
841
|
+
subagents: options.subagents,
|
|
842
|
+
defaultModel: options.model,
|
|
843
|
+
parentAgent: agent,
|
|
844
|
+
// Only pass streaming context when provided (streamDataResponse)
|
|
845
|
+
streamingContext,
|
|
846
|
+
}),
|
|
847
|
+
};
|
|
848
|
+
};
|
|
849
|
+
// Track current checkpoint state per thread
|
|
850
|
+
const threadCheckpoints = new Map();
|
|
851
|
+
/**
|
|
852
|
+
* Load checkpoint for a thread if checkpointer is configured.
|
|
853
|
+
* Returns the loaded checkpoint or undefined.
|
|
854
|
+
*/
|
|
855
|
+
async function loadCheckpoint(threadId) {
|
|
856
|
+
if (!options.checkpointer) {
|
|
857
|
+
return undefined;
|
|
858
|
+
}
|
|
859
|
+
// Check if we already have it cached
|
|
860
|
+
const cached = threadCheckpoints.get(threadId);
|
|
861
|
+
if (cached) {
|
|
862
|
+
return cached;
|
|
863
|
+
}
|
|
864
|
+
try {
|
|
865
|
+
// Load from checkpointer
|
|
866
|
+
const checkpoint = await options.checkpointer.load(threadId);
|
|
867
|
+
if (checkpoint) {
|
|
868
|
+
threadCheckpoints.set(threadId, checkpoint);
|
|
869
|
+
// Restore agent state from checkpoint
|
|
870
|
+
state.todos = [...checkpoint.state.todos];
|
|
871
|
+
state.files = { ...checkpoint.state.files };
|
|
872
|
+
}
|
|
873
|
+
return checkpoint;
|
|
874
|
+
}
|
|
875
|
+
catch (error) {
|
|
876
|
+
// Wrap checkpoint load errors with CheckpointError
|
|
877
|
+
throw new CheckpointError(`Failed to load checkpoint for thread ${threadId}`, {
|
|
878
|
+
operation: "load",
|
|
879
|
+
threadId,
|
|
880
|
+
cause: error instanceof Error ? error : undefined,
|
|
881
|
+
metadata: { threadId },
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Save checkpoint for a thread if checkpointer is configured.
|
|
887
|
+
*/
|
|
888
|
+
async function saveCheckpoint(threadId, messages, step) {
|
|
889
|
+
if (!options.checkpointer) {
|
|
890
|
+
return undefined;
|
|
891
|
+
}
|
|
892
|
+
const existingCheckpoint = threadCheckpoints.get(threadId);
|
|
893
|
+
let checkpoint;
|
|
894
|
+
if (existingCheckpoint) {
|
|
895
|
+
// Update existing checkpoint
|
|
896
|
+
checkpoint = updateCheckpoint(existingCheckpoint, {
|
|
897
|
+
messages,
|
|
898
|
+
step,
|
|
899
|
+
state: {
|
|
900
|
+
todos: [...state.todos],
|
|
901
|
+
files: { ...state.files },
|
|
902
|
+
},
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
906
|
+
// Create new checkpoint
|
|
907
|
+
checkpoint = createCheckpoint({
|
|
908
|
+
threadId,
|
|
909
|
+
messages,
|
|
910
|
+
step,
|
|
911
|
+
state: {
|
|
912
|
+
todos: [...state.todos],
|
|
913
|
+
files: { ...state.files },
|
|
914
|
+
},
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
try {
|
|
918
|
+
// Save to checkpointer
|
|
919
|
+
await options.checkpointer.save(checkpoint);
|
|
920
|
+
threadCheckpoints.set(threadId, checkpoint);
|
|
921
|
+
return checkpoint;
|
|
922
|
+
}
|
|
923
|
+
catch (error) {
|
|
924
|
+
// Wrap checkpoint save errors with CheckpointError
|
|
925
|
+
throw new CheckpointError(`Failed to save checkpoint for thread ${threadId}`, {
|
|
926
|
+
operation: "save",
|
|
927
|
+
threadId,
|
|
928
|
+
cause: error instanceof Error ? error : undefined,
|
|
929
|
+
metadata: { threadId, step },
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Fork an existing checkpoint to a new thread ID.
|
|
935
|
+
* Copies all checkpoint data including messages and state.
|
|
936
|
+
*/
|
|
937
|
+
async function forkCheckpoint(sourceThreadId, targetThreadId) {
|
|
938
|
+
if (!options.checkpointer) {
|
|
939
|
+
return undefined;
|
|
940
|
+
}
|
|
941
|
+
// Load the source checkpoint
|
|
942
|
+
const sourceCheckpoint = await loadCheckpoint(sourceThreadId);
|
|
943
|
+
if (!sourceCheckpoint) {
|
|
944
|
+
return undefined;
|
|
945
|
+
}
|
|
946
|
+
// Create a new checkpoint with the target threadId
|
|
947
|
+
const forkedCheckpoint = createCheckpoint({
|
|
948
|
+
threadId: targetThreadId,
|
|
949
|
+
messages: [...sourceCheckpoint.messages],
|
|
950
|
+
step: sourceCheckpoint.step,
|
|
951
|
+
state: {
|
|
952
|
+
todos: [...sourceCheckpoint.state.todos],
|
|
953
|
+
files: { ...sourceCheckpoint.state.files },
|
|
954
|
+
},
|
|
955
|
+
});
|
|
956
|
+
try {
|
|
957
|
+
// Save the forked checkpoint
|
|
958
|
+
await options.checkpointer.save(forkedCheckpoint);
|
|
959
|
+
threadCheckpoints.set(targetThreadId, forkedCheckpoint);
|
|
960
|
+
return forkedCheckpoint;
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
throw new CheckpointError(`Failed to fork checkpoint from ${sourceThreadId} to ${targetThreadId}`, {
|
|
964
|
+
operation: "fork",
|
|
965
|
+
threadId: targetThreadId,
|
|
966
|
+
cause: error instanceof Error ? error : undefined,
|
|
967
|
+
metadata: { sourceThreadId, targetThreadId },
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Build the messages array for AI SDK from GenerateOptions.
|
|
973
|
+
* If a checkpoint exists for the threadId, prepends checkpoint messages.
|
|
974
|
+
* If forkSession is specified, creates a new session from the source.
|
|
975
|
+
* If contextManager is provided, applies automatic compaction if needed.
|
|
976
|
+
*/
|
|
977
|
+
async function buildMessages(genOptions) {
|
|
978
|
+
const messages = [];
|
|
979
|
+
let checkpoint;
|
|
980
|
+
let forkedSessionId;
|
|
981
|
+
// Handle session forking
|
|
982
|
+
if (genOptions.forkSession && genOptions.threadId) {
|
|
983
|
+
forkedSessionId = genOptions.forkSession;
|
|
984
|
+
checkpoint = await forkCheckpoint(genOptions.threadId, forkedSessionId);
|
|
985
|
+
if (checkpoint) {
|
|
986
|
+
// Prepend forked checkpoint messages
|
|
987
|
+
messages.push(...checkpoint.messages);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
else if (genOptions.threadId) {
|
|
991
|
+
// Normal checkpoint loading
|
|
992
|
+
checkpoint = await loadCheckpoint(genOptions.threadId);
|
|
993
|
+
if (checkpoint) {
|
|
994
|
+
// Prepend checkpoint messages
|
|
995
|
+
messages.push(...checkpoint.messages);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
// Add conversation history if provided
|
|
999
|
+
if (genOptions.messages) {
|
|
1000
|
+
messages.push(...genOptions.messages);
|
|
1001
|
+
}
|
|
1002
|
+
// Add user prompt if provided
|
|
1003
|
+
if (genOptions.prompt) {
|
|
1004
|
+
messages.push({ role: "user", content: genOptions.prompt });
|
|
1005
|
+
}
|
|
1006
|
+
// Apply context compaction if contextManager is configured
|
|
1007
|
+
// Skip compaction if _skipCompaction flag is set (used during summary generation)
|
|
1008
|
+
if (options.contextManager && !genOptions._skipCompaction) {
|
|
1009
|
+
const contextManager = options.contextManager;
|
|
1010
|
+
// Check if compaction is needed
|
|
1011
|
+
const { trigger, reason } = contextManager.shouldCompact(messages);
|
|
1012
|
+
if (trigger && reason) {
|
|
1013
|
+
// Calculate token count before compaction
|
|
1014
|
+
const tokensBefore = contextManager.tokenCounter.countMessages(messages);
|
|
1015
|
+
const messagesBefore = messages.length;
|
|
1016
|
+
// Emit PreCompact hook
|
|
1017
|
+
const preCompactHooks = effectiveHooks?.PreCompact ?? [];
|
|
1018
|
+
if (preCompactHooks.length > 0) {
|
|
1019
|
+
const preCompactInput = {
|
|
1020
|
+
hook_event_name: "PreCompact",
|
|
1021
|
+
session_id: genOptions.threadId ?? "default",
|
|
1022
|
+
cwd: process.cwd(),
|
|
1023
|
+
message_count: messagesBefore,
|
|
1024
|
+
tokens_before: tokensBefore,
|
|
1025
|
+
};
|
|
1026
|
+
await invokeHooksWithTimeout(preCompactHooks, preCompactInput, null, agent);
|
|
1027
|
+
}
|
|
1028
|
+
// Perform compaction
|
|
1029
|
+
const compactionResult = await contextManager.compact(messages, agent, reason);
|
|
1030
|
+
// Replace messages with compacted version
|
|
1031
|
+
messages.length = 0;
|
|
1032
|
+
messages.push(...compactionResult.newMessages);
|
|
1033
|
+
// Emit PostCompact hook with metrics
|
|
1034
|
+
const postCompactHooks = effectiveHooks?.PostCompact ?? [];
|
|
1035
|
+
if (postCompactHooks.length > 0) {
|
|
1036
|
+
const postCompactInput = {
|
|
1037
|
+
hook_event_name: "PostCompact",
|
|
1038
|
+
session_id: genOptions.threadId ?? "default",
|
|
1039
|
+
cwd: process.cwd(),
|
|
1040
|
+
messages_before: compactionResult.messagesBefore,
|
|
1041
|
+
messages_after: compactionResult.messagesAfter,
|
|
1042
|
+
tokens_before: compactionResult.tokensBefore,
|
|
1043
|
+
tokens_after: compactionResult.tokensAfter,
|
|
1044
|
+
tokens_saved: compactionResult.tokensBefore - compactionResult.tokensAfter,
|
|
1045
|
+
};
|
|
1046
|
+
await invokeHooksWithTimeout(postCompactHooks, postCompactInput, null, agent);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return { messages, checkpoint, forkedSessionId };
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Map AI SDK steps to our GenerateStep format.
|
|
1054
|
+
*/
|
|
1055
|
+
function mapSteps(steps) {
|
|
1056
|
+
return steps.map((step) => ({
|
|
1057
|
+
text: step.text,
|
|
1058
|
+
toolCalls: step.toolCalls.map((tc) => ({
|
|
1059
|
+
toolCallId: tc.toolCallId,
|
|
1060
|
+
toolName: tc.toolName,
|
|
1061
|
+
input: tc.input,
|
|
1062
|
+
})),
|
|
1063
|
+
toolResults: step.toolResults.map((tr) => ({
|
|
1064
|
+
toolCallId: tr.toolCallId,
|
|
1065
|
+
toolName: tr.toolName,
|
|
1066
|
+
output: tr.output,
|
|
1067
|
+
})),
|
|
1068
|
+
finishReason: step.finishReason,
|
|
1069
|
+
usage: step.usage,
|
|
1070
|
+
}));
|
|
1071
|
+
}
|
|
1072
|
+
const agent = {
|
|
1073
|
+
id,
|
|
1074
|
+
options,
|
|
1075
|
+
backend,
|
|
1076
|
+
state,
|
|
1077
|
+
getSkills() {
|
|
1078
|
+
return [...skills];
|
|
1079
|
+
},
|
|
1080
|
+
async generate(genOptions) {
|
|
1081
|
+
// Invoke unified PreGenerate hooks
|
|
1082
|
+
const preGenerateHooks = effectiveHooks?.PreGenerate ?? [];
|
|
1083
|
+
const preGenResult = await invokePreGenerateHooks(preGenerateHooks, genOptions, agent);
|
|
1084
|
+
// Check for cache short-circuit via respondWith
|
|
1085
|
+
if (preGenResult.cachedResult !== undefined) {
|
|
1086
|
+
return preGenResult.cachedResult;
|
|
1087
|
+
}
|
|
1088
|
+
let effectiveGenOptions = preGenResult.effectiveOptions;
|
|
1089
|
+
// Initialize retry loop state
|
|
1090
|
+
const retryState = createRetryLoopState(options.model);
|
|
1091
|
+
// Track messages for emergency compaction (accessible in catch block)
|
|
1092
|
+
let lastBuiltMessages = [];
|
|
1093
|
+
while (retryState.retryAttempt <= retryState.maxRetries) {
|
|
1094
|
+
try {
|
|
1095
|
+
const { messages, checkpoint, forkedSessionId } = await buildMessages(effectiveGenOptions);
|
|
1096
|
+
// Store for potential emergency compaction in catch block
|
|
1097
|
+
lastBuiltMessages = messages;
|
|
1098
|
+
const maxSteps = options.maxSteps ?? 10;
|
|
1099
|
+
const startStep = checkpoint?.step ?? 0;
|
|
1100
|
+
// Build initial params - use active tools (core + dynamically loaded + task)
|
|
1101
|
+
// Apply hooks AFTER adding task tool so task tool is also wrapped
|
|
1102
|
+
const activeTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSet(effectiveGenOptions.threadId)), effectiveGenOptions.threadId);
|
|
1103
|
+
const initialParams = {
|
|
1104
|
+
system: options.systemPrompt,
|
|
1105
|
+
messages,
|
|
1106
|
+
tools: activeTools,
|
|
1107
|
+
maxTokens: effectiveGenOptions.maxTokens,
|
|
1108
|
+
temperature: effectiveGenOptions.temperature,
|
|
1109
|
+
stopSequences: effectiveGenOptions.stopSequences,
|
|
1110
|
+
abortSignal: effectiveGenOptions.signal,
|
|
1111
|
+
providerOptions: effectiveGenOptions.providerOptions,
|
|
1112
|
+
headers: effectiveGenOptions.headers,
|
|
1113
|
+
};
|
|
1114
|
+
// Execute generation
|
|
1115
|
+
const response = await generateText({
|
|
1116
|
+
model: retryState.currentModel,
|
|
1117
|
+
system: initialParams.system,
|
|
1118
|
+
messages: initialParams.messages,
|
|
1119
|
+
tools: initialParams.tools,
|
|
1120
|
+
maxOutputTokens: initialParams.maxTokens,
|
|
1121
|
+
temperature: initialParams.temperature,
|
|
1122
|
+
stopSequences: initialParams.stopSequences,
|
|
1123
|
+
abortSignal: initialParams.abortSignal,
|
|
1124
|
+
stopWhen: stepCountIs(maxSteps),
|
|
1125
|
+
// Passthrough AI SDK options
|
|
1126
|
+
output: effectiveGenOptions.output,
|
|
1127
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
|
|
1128
|
+
providerOptions: initialParams.providerOptions,
|
|
1129
|
+
headers: initialParams.headers,
|
|
1130
|
+
});
|
|
1131
|
+
// Only access output if an output schema was provided
|
|
1132
|
+
// (accessing response.output throws AI_NoOutputGeneratedError otherwise)
|
|
1133
|
+
let output;
|
|
1134
|
+
if (effectiveGenOptions.output) {
|
|
1135
|
+
try {
|
|
1136
|
+
output = response.output;
|
|
1137
|
+
}
|
|
1138
|
+
catch {
|
|
1139
|
+
// No structured output was generated
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const result = {
|
|
1143
|
+
status: "complete",
|
|
1144
|
+
text: response.text,
|
|
1145
|
+
usage: response.usage,
|
|
1146
|
+
finishReason: response.finishReason,
|
|
1147
|
+
output,
|
|
1148
|
+
steps: mapSteps(response.steps),
|
|
1149
|
+
forkedSessionId,
|
|
1150
|
+
};
|
|
1151
|
+
// Update context manager with actual usage if available
|
|
1152
|
+
if (options.contextManager?.updateUsage && response.usage) {
|
|
1153
|
+
options.contextManager.updateUsage({
|
|
1154
|
+
inputTokens: response.usage.inputTokens,
|
|
1155
|
+
outputTokens: response.usage.outputTokens,
|
|
1156
|
+
totalTokens: response.usage.totalTokens,
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
// Save checkpoint - use forked session ID if forking, otherwise use original threadId
|
|
1160
|
+
const checkpointThreadId = forkedSessionId ?? effectiveGenOptions.threadId;
|
|
1161
|
+
if (checkpointThreadId && options.checkpointer) {
|
|
1162
|
+
// Build final messages including the assistant response
|
|
1163
|
+
const finalMessages = [
|
|
1164
|
+
...messages,
|
|
1165
|
+
{ role: "assistant", content: response.text },
|
|
1166
|
+
];
|
|
1167
|
+
await saveCheckpoint(checkpointThreadId, finalMessages, startStep + response.steps.length);
|
|
1168
|
+
}
|
|
1169
|
+
// Invoke unified PostGenerate hooks
|
|
1170
|
+
const postGenerateHooks = effectiveHooks?.PostGenerate ?? [];
|
|
1171
|
+
let finalResult = result;
|
|
1172
|
+
if (postGenerateHooks.length > 0) {
|
|
1173
|
+
const postGenerateInput = {
|
|
1174
|
+
hook_event_name: "PostGenerate",
|
|
1175
|
+
session_id: effectiveGenOptions.threadId ?? "default",
|
|
1176
|
+
cwd: process.cwd(),
|
|
1177
|
+
options: effectiveGenOptions,
|
|
1178
|
+
result,
|
|
1179
|
+
};
|
|
1180
|
+
const hookOutputs = await invokeHooksWithTimeout(postGenerateHooks, postGenerateInput, null, agent);
|
|
1181
|
+
// Apply output transformation via updatedResult
|
|
1182
|
+
// Note: Hooks can only return complete results, not interrupted ones
|
|
1183
|
+
const updatedResult = extractUpdatedResult(hookOutputs);
|
|
1184
|
+
if (updatedResult !== undefined) {
|
|
1185
|
+
finalResult = updatedResult;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return finalResult;
|
|
1189
|
+
}
|
|
1190
|
+
catch (error) {
|
|
1191
|
+
// Check if this is an InterruptSignal (new interrupt system)
|
|
1192
|
+
if (isInterruptSignal(error)) {
|
|
1193
|
+
const interrupt = error.interrupt;
|
|
1194
|
+
// Save the interrupt to checkpoint
|
|
1195
|
+
if (effectiveGenOptions.threadId && options.checkpointer) {
|
|
1196
|
+
const checkpoint = await options.checkpointer.load(effectiveGenOptions.threadId);
|
|
1197
|
+
if (checkpoint) {
|
|
1198
|
+
const updatedCheckpoint = updateCheckpoint(checkpoint, {
|
|
1199
|
+
pendingInterrupt: interrupt,
|
|
1200
|
+
});
|
|
1201
|
+
await options.checkpointer.save(updatedCheckpoint);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
// Emit InterruptRequested hook
|
|
1205
|
+
const interruptRequestedHooks = effectiveHooks?.InterruptRequested ?? [];
|
|
1206
|
+
if (interruptRequestedHooks.length > 0) {
|
|
1207
|
+
const hookInput = {
|
|
1208
|
+
hook_event_name: "InterruptRequested",
|
|
1209
|
+
session_id: effectiveGenOptions.threadId ?? "default",
|
|
1210
|
+
cwd: process.cwd(),
|
|
1211
|
+
interrupt_id: interrupt.id,
|
|
1212
|
+
interrupt_type: interrupt.type,
|
|
1213
|
+
tool_call_id: interrupt.toolCallId,
|
|
1214
|
+
tool_name: interrupt.toolName,
|
|
1215
|
+
request: interrupt.request,
|
|
1216
|
+
};
|
|
1217
|
+
await invokeHooksWithTimeout(interruptRequestedHooks, hookInput, null, agent);
|
|
1218
|
+
}
|
|
1219
|
+
// Return interrupted result
|
|
1220
|
+
const interruptedResult = {
|
|
1221
|
+
status: "interrupted",
|
|
1222
|
+
interrupt,
|
|
1223
|
+
partial: {
|
|
1224
|
+
text: "",
|
|
1225
|
+
steps: [],
|
|
1226
|
+
usage: undefined,
|
|
1227
|
+
},
|
|
1228
|
+
};
|
|
1229
|
+
return interruptedResult;
|
|
1230
|
+
}
|
|
1231
|
+
// Normalize error to AgentError
|
|
1232
|
+
const normalizedError = normalizeError(error, "Generation failed", effectiveGenOptions.threadId);
|
|
1233
|
+
// Check for context length error and attempt emergency compaction if enabled
|
|
1234
|
+
// Note: Only attempt this ONCE to avoid infinite loops
|
|
1235
|
+
if (options.contextManager?.policy.enableErrorFallback &&
|
|
1236
|
+
retryState.retryAttempt === 0 && // Only on first error, not on retry
|
|
1237
|
+
isContextLengthError(normalizedError)) {
|
|
1238
|
+
// Emergency compaction - try to recover
|
|
1239
|
+
// We'll compact and save to checkpoint, then retry
|
|
1240
|
+
try {
|
|
1241
|
+
// Get current messages from checkpoint if available, or use the messages from the current call
|
|
1242
|
+
let messagesToCompact = [];
|
|
1243
|
+
if (effectiveGenOptions.threadId && options.checkpointer) {
|
|
1244
|
+
const checkpoint = await options.checkpointer.load(effectiveGenOptions.threadId);
|
|
1245
|
+
if (checkpoint && checkpoint.messages.length > 0) {
|
|
1246
|
+
messagesToCompact = checkpoint.messages;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
// Fall back to messages from the current call if checkpoint is empty
|
|
1250
|
+
if (messagesToCompact.length === 0 && lastBuiltMessages.length > 0) {
|
|
1251
|
+
messagesToCompact = lastBuiltMessages;
|
|
1252
|
+
}
|
|
1253
|
+
// If we have messages to compact, do emergency compaction
|
|
1254
|
+
if (messagesToCompact.length > 0) {
|
|
1255
|
+
const compactionResult = await options.contextManager.compact(messagesToCompact, agent, "error_fallback");
|
|
1256
|
+
// Save compacted state to checkpoint and clear original messages
|
|
1257
|
+
// to prevent duplication on retry
|
|
1258
|
+
if (effectiveGenOptions.threadId && options.checkpointer) {
|
|
1259
|
+
const existingCheckpoint = await options.checkpointer.load(effectiveGenOptions.threadId);
|
|
1260
|
+
if (existingCheckpoint) {
|
|
1261
|
+
const updatedCheckpoint = updateCheckpoint(existingCheckpoint, {
|
|
1262
|
+
messages: compactionResult.newMessages,
|
|
1263
|
+
});
|
|
1264
|
+
await options.checkpointer.save(updatedCheckpoint);
|
|
1265
|
+
// Update cache
|
|
1266
|
+
threadCheckpoints.set(effectiveGenOptions.threadId, updatedCheckpoint);
|
|
1267
|
+
}
|
|
1268
|
+
else {
|
|
1269
|
+
// Create a new checkpoint with compacted messages
|
|
1270
|
+
const newCheckpoint = createCheckpoint({
|
|
1271
|
+
threadId: effectiveGenOptions.threadId,
|
|
1272
|
+
messages: compactionResult.newMessages,
|
|
1273
|
+
step: 0,
|
|
1274
|
+
state: {
|
|
1275
|
+
todos: [...state.todos],
|
|
1276
|
+
files: { ...state.files },
|
|
1277
|
+
},
|
|
1278
|
+
});
|
|
1279
|
+
await options.checkpointer.save(newCheckpoint);
|
|
1280
|
+
threadCheckpoints.set(effectiveGenOptions.threadId, newCheckpoint);
|
|
1281
|
+
}
|
|
1282
|
+
// Clear messages from effectiveGenOptions to prevent duplication
|
|
1283
|
+
// The retry will use checkpoint messages only
|
|
1284
|
+
effectiveGenOptions = {
|
|
1285
|
+
...effectiveGenOptions,
|
|
1286
|
+
messages: undefined,
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
retryState.retryAttempt++;
|
|
1290
|
+
// Retry immediately with compacted context
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
catch (_compactionError) {
|
|
1295
|
+
// If compaction itself fails, don't retry - fall through to normal error handling
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
// Handle error with PostGenerateFailure hooks and fallback logic
|
|
1299
|
+
const postGenerateFailureHooks = effectiveHooks?.PostGenerateFailure ?? [];
|
|
1300
|
+
const errorDecision = await handleGenerationError(normalizedError, postGenerateFailureHooks, effectiveGenOptions, agent, retryState, options.fallbackModel, shouldUseFallback);
|
|
1301
|
+
if (errorDecision.shouldRetry) {
|
|
1302
|
+
// Update retry state
|
|
1303
|
+
Object.assign(retryState, updateRetryLoopState(retryState, errorDecision));
|
|
1304
|
+
// Wait for the specified delay before retrying
|
|
1305
|
+
await waitForRetryDelay(errorDecision.retryDelayMs);
|
|
1306
|
+
// Continue to next iteration of retry loop
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
// No retry requested or max retries exceeded - throw the normalized error
|
|
1310
|
+
throw normalizedError;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
// This should never be reached, but TypeScript needs it for type safety
|
|
1314
|
+
throw new Error("Unexpected: retry loop exited without return or throw");
|
|
1315
|
+
},
|
|
1316
|
+
async *stream(genOptions) {
|
|
1317
|
+
// Invoke unified PreGenerate hooks
|
|
1318
|
+
const preGenerateHooks = effectiveHooks?.PreGenerate ?? [];
|
|
1319
|
+
const preGenResult = await invokePreGenerateHooks(preGenerateHooks, genOptions, agent);
|
|
1320
|
+
// Check for cache short-circuit via respondWith
|
|
1321
|
+
// For streaming, we convert the cached GenerateResult into StreamParts
|
|
1322
|
+
if (preGenResult.cachedResult !== undefined) {
|
|
1323
|
+
const cachedResult = preGenResult.cachedResult;
|
|
1324
|
+
// Only process complete results (interrupted results can't be cached)
|
|
1325
|
+
if (cachedResult.status === "complete") {
|
|
1326
|
+
// Yield cached result as stream parts
|
|
1327
|
+
// First, yield text as a single text-delta
|
|
1328
|
+
if (cachedResult.text) {
|
|
1329
|
+
yield { type: "text-delta", text: cachedResult.text };
|
|
1330
|
+
}
|
|
1331
|
+
// Yield tool calls and results from steps
|
|
1332
|
+
for (const step of cachedResult.steps ?? []) {
|
|
1333
|
+
for (const toolCall of step.toolCalls ?? []) {
|
|
1334
|
+
yield {
|
|
1335
|
+
type: "tool-call",
|
|
1336
|
+
toolCallId: toolCall.toolCallId,
|
|
1337
|
+
toolName: toolCall.toolName,
|
|
1338
|
+
input: toolCall.input,
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
for (const toolResult of step.toolResults ?? []) {
|
|
1342
|
+
yield {
|
|
1343
|
+
type: "tool-result",
|
|
1344
|
+
toolCallId: toolResult.toolCallId,
|
|
1345
|
+
toolName: toolResult.toolName,
|
|
1346
|
+
output: toolResult.output,
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
// Finally yield finish
|
|
1351
|
+
yield {
|
|
1352
|
+
type: "finish",
|
|
1353
|
+
finishReason: cachedResult.finishReason,
|
|
1354
|
+
usage: cachedResult.usage,
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
const effectiveGenOptions = preGenResult.effectiveOptions;
|
|
1360
|
+
// Initialize retry loop state
|
|
1361
|
+
const retryState = createRetryLoopState(options.model);
|
|
1362
|
+
while (retryState.retryAttempt <= retryState.maxRetries) {
|
|
1363
|
+
try {
|
|
1364
|
+
const { messages, checkpoint } = await buildMessages(effectiveGenOptions);
|
|
1365
|
+
const maxSteps = options.maxSteps ?? 10;
|
|
1366
|
+
const startStep = checkpoint?.step ?? 0;
|
|
1367
|
+
// Build initial params - use active tools (core + dynamically loaded + task)
|
|
1368
|
+
// Apply hooks AFTER adding task tool so task tool is also wrapped
|
|
1369
|
+
const activeTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSet(effectiveGenOptions.threadId)), effectiveGenOptions.threadId);
|
|
1370
|
+
const initialParams = {
|
|
1371
|
+
system: options.systemPrompt,
|
|
1372
|
+
messages,
|
|
1373
|
+
tools: activeTools,
|
|
1374
|
+
maxTokens: effectiveGenOptions.maxTokens,
|
|
1375
|
+
temperature: effectiveGenOptions.temperature,
|
|
1376
|
+
stopSequences: effectiveGenOptions.stopSequences,
|
|
1377
|
+
abortSignal: effectiveGenOptions.signal,
|
|
1378
|
+
providerOptions: effectiveGenOptions.providerOptions,
|
|
1379
|
+
headers: effectiveGenOptions.headers,
|
|
1380
|
+
};
|
|
1381
|
+
// Execute stream
|
|
1382
|
+
const response = streamText({
|
|
1383
|
+
model: retryState.currentModel,
|
|
1384
|
+
system: initialParams.system,
|
|
1385
|
+
messages: initialParams.messages,
|
|
1386
|
+
tools: initialParams.tools,
|
|
1387
|
+
maxOutputTokens: initialParams.maxTokens,
|
|
1388
|
+
temperature: initialParams.temperature,
|
|
1389
|
+
stopSequences: initialParams.stopSequences,
|
|
1390
|
+
abortSignal: initialParams.abortSignal,
|
|
1391
|
+
stopWhen: stepCountIs(maxSteps),
|
|
1392
|
+
// Passthrough AI SDK options
|
|
1393
|
+
output: genOptions.output,
|
|
1394
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
|
|
1395
|
+
providerOptions: initialParams.providerOptions,
|
|
1396
|
+
headers: initialParams.headers,
|
|
1397
|
+
});
|
|
1398
|
+
for await (const part of response.fullStream) {
|
|
1399
|
+
if (part.type === "text-delta") {
|
|
1400
|
+
yield { type: "text-delta", text: part.text };
|
|
1401
|
+
}
|
|
1402
|
+
else if (part.type === "tool-call") {
|
|
1403
|
+
yield {
|
|
1404
|
+
type: "tool-call",
|
|
1405
|
+
toolCallId: part.toolCallId,
|
|
1406
|
+
toolName: part.toolName,
|
|
1407
|
+
input: part.input,
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
else if (part.type === "tool-result") {
|
|
1411
|
+
yield {
|
|
1412
|
+
type: "tool-result",
|
|
1413
|
+
toolCallId: part.toolCallId,
|
|
1414
|
+
toolName: part.toolName,
|
|
1415
|
+
output: part.output,
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
else if (part.type === "finish") {
|
|
1419
|
+
yield {
|
|
1420
|
+
type: "finish",
|
|
1421
|
+
finishReason: part.finishReason,
|
|
1422
|
+
usage: part.totalUsage,
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
else if (part.type === "error") {
|
|
1426
|
+
yield { type: "error", error: part.error };
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
// Get final result for hooks - need to await all properties
|
|
1430
|
+
const [text, usage, finishReason, steps] = await Promise.all([
|
|
1431
|
+
response.text,
|
|
1432
|
+
response.usage,
|
|
1433
|
+
response.finishReason,
|
|
1434
|
+
response.steps,
|
|
1435
|
+
]);
|
|
1436
|
+
// Only access output if an output schema was provided
|
|
1437
|
+
let output;
|
|
1438
|
+
if (genOptions.output) {
|
|
1439
|
+
try {
|
|
1440
|
+
output = await response.output;
|
|
1441
|
+
}
|
|
1442
|
+
catch {
|
|
1443
|
+
// No structured output was generated
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
const result = {
|
|
1447
|
+
status: "complete",
|
|
1448
|
+
text,
|
|
1449
|
+
usage,
|
|
1450
|
+
finishReason: finishReason,
|
|
1451
|
+
output,
|
|
1452
|
+
steps: mapSteps(steps),
|
|
1453
|
+
};
|
|
1454
|
+
// Save checkpoint if threadId is provided
|
|
1455
|
+
if (effectiveGenOptions.threadId && options.checkpointer) {
|
|
1456
|
+
const finalMessages = [
|
|
1457
|
+
...messages,
|
|
1458
|
+
{ role: "assistant", content: text },
|
|
1459
|
+
];
|
|
1460
|
+
await saveCheckpoint(effectiveGenOptions.threadId, finalMessages, startStep + steps.length);
|
|
1461
|
+
}
|
|
1462
|
+
// Invoke unified PostGenerate hooks
|
|
1463
|
+
const postGenerateHooks = effectiveHooks?.PostGenerate ?? [];
|
|
1464
|
+
if (postGenerateHooks.length > 0) {
|
|
1465
|
+
const postGenerateInput = {
|
|
1466
|
+
hook_event_name: "PostGenerate",
|
|
1467
|
+
session_id: effectiveGenOptions.threadId ?? "default",
|
|
1468
|
+
cwd: process.cwd(),
|
|
1469
|
+
options: effectiveGenOptions,
|
|
1470
|
+
result,
|
|
1471
|
+
};
|
|
1472
|
+
await invokeHooksWithTimeout(postGenerateHooks, postGenerateInput, null, agent);
|
|
1473
|
+
// Note: updatedResult is not applied for streaming since the stream has already been sent
|
|
1474
|
+
}
|
|
1475
|
+
// Success - break out of retry loop
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
catch (error) {
|
|
1479
|
+
// Normalize error to AgentError
|
|
1480
|
+
const normalizedError = normalizeError(error, "Stream generation failed", effectiveGenOptions.threadId);
|
|
1481
|
+
// Handle error with PostGenerateFailure hooks and fallback logic
|
|
1482
|
+
const postGenerateFailureHooks = effectiveHooks?.PostGenerateFailure ?? [];
|
|
1483
|
+
const errorDecision = await handleGenerationError(normalizedError, postGenerateFailureHooks, effectiveGenOptions, agent, retryState, options.fallbackModel, shouldUseFallback);
|
|
1484
|
+
if (errorDecision.shouldRetry) {
|
|
1485
|
+
// Update retry state
|
|
1486
|
+
Object.assign(retryState, updateRetryLoopState(retryState, errorDecision));
|
|
1487
|
+
// Wait for the specified delay before retrying
|
|
1488
|
+
await waitForRetryDelay(errorDecision.retryDelayMs);
|
|
1489
|
+
// Continue to next iteration of retry loop
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
// No retry requested or max retries exceeded - throw the normalized error
|
|
1493
|
+
throw normalizedError;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
// This should never be reached, but TypeScript needs it for type safety
|
|
1497
|
+
throw new Error("Unexpected: retry loop exited without return or throw");
|
|
1498
|
+
},
|
|
1499
|
+
async streamResponse(genOptions) {
|
|
1500
|
+
// Invoke unified PreGenerate hooks
|
|
1501
|
+
const preGenerateHooks = effectiveHooks?.PreGenerate ?? [];
|
|
1502
|
+
const preGenResult = await invokePreGenerateHooks(preGenerateHooks, genOptions, agent);
|
|
1503
|
+
// Check for cache short-circuit via respondWith
|
|
1504
|
+
// For streaming response, create a simple text response from the cached result
|
|
1505
|
+
if (preGenResult.cachedResult !== undefined) {
|
|
1506
|
+
const cachedResult = preGenResult.cachedResult;
|
|
1507
|
+
// For cached results, return a simple Response with the cached text
|
|
1508
|
+
// This is compatible with useChat and provides immediate delivery
|
|
1509
|
+
// Only complete results can be cached
|
|
1510
|
+
const text = cachedResult.status === "complete" ? cachedResult.text : "";
|
|
1511
|
+
return new Response(text, {
|
|
1512
|
+
headers: {
|
|
1513
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
1514
|
+
},
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
const effectiveGenOptions = preGenResult.effectiveOptions;
|
|
1518
|
+
// Initialize retry loop state
|
|
1519
|
+
const retryState = createRetryLoopState(options.model);
|
|
1520
|
+
while (retryState.retryAttempt <= retryState.maxRetries) {
|
|
1521
|
+
try {
|
|
1522
|
+
const { messages, checkpoint } = await buildMessages(effectiveGenOptions);
|
|
1523
|
+
const maxSteps = options.maxSteps ?? 10;
|
|
1524
|
+
const startStep = checkpoint?.step ?? 0;
|
|
1525
|
+
// Build initial params - use active tools (core + dynamically loaded + task)
|
|
1526
|
+
// Apply hooks AFTER adding task tool so task tool is also wrapped
|
|
1527
|
+
const activeTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSet(effectiveGenOptions.threadId)), effectiveGenOptions.threadId);
|
|
1528
|
+
const initialParams = {
|
|
1529
|
+
system: options.systemPrompt,
|
|
1530
|
+
messages,
|
|
1531
|
+
tools: activeTools,
|
|
1532
|
+
maxTokens: effectiveGenOptions.maxTokens,
|
|
1533
|
+
temperature: effectiveGenOptions.temperature,
|
|
1534
|
+
stopSequences: effectiveGenOptions.stopSequences,
|
|
1535
|
+
abortSignal: effectiveGenOptions.signal,
|
|
1536
|
+
providerOptions: effectiveGenOptions.providerOptions,
|
|
1537
|
+
headers: effectiveGenOptions.headers,
|
|
1538
|
+
};
|
|
1539
|
+
// Track step count for incremental checkpointing
|
|
1540
|
+
let currentStepCount = 0;
|
|
1541
|
+
// Execute stream
|
|
1542
|
+
const result = streamText({
|
|
1543
|
+
model: retryState.currentModel,
|
|
1544
|
+
system: initialParams.system,
|
|
1545
|
+
messages: initialParams.messages,
|
|
1546
|
+
tools: initialParams.tools,
|
|
1547
|
+
maxOutputTokens: initialParams.maxTokens,
|
|
1548
|
+
temperature: initialParams.temperature,
|
|
1549
|
+
stopSequences: initialParams.stopSequences,
|
|
1550
|
+
abortSignal: initialParams.abortSignal,
|
|
1551
|
+
stopWhen: stepCountIs(maxSteps),
|
|
1552
|
+
// Passthrough AI SDK options
|
|
1553
|
+
output: effectiveGenOptions.output,
|
|
1554
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
|
|
1555
|
+
providerOptions: initialParams.providerOptions,
|
|
1556
|
+
headers: initialParams.headers,
|
|
1557
|
+
// Incremental checkpointing: save after each step if enabled
|
|
1558
|
+
onStepFinish: effectiveGenOptions.checkpointAfterToolCall
|
|
1559
|
+
? async (stepResult) => {
|
|
1560
|
+
if (effectiveGenOptions.threadId && options.checkpointer) {
|
|
1561
|
+
currentStepCount++;
|
|
1562
|
+
// Build messages including this step's results
|
|
1563
|
+
const stepMessages = [
|
|
1564
|
+
...initialParams.messages,
|
|
1565
|
+
{
|
|
1566
|
+
role: "assistant",
|
|
1567
|
+
content: stepResult.text,
|
|
1568
|
+
},
|
|
1569
|
+
];
|
|
1570
|
+
await saveCheckpoint(effectiveGenOptions.threadId, stepMessages, startStep + currentStepCount);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
: undefined,
|
|
1574
|
+
// Save checkpoint and emit PostGenerate hook after completion
|
|
1575
|
+
onFinish: async (finishResult) => {
|
|
1576
|
+
// Update context manager with actual usage if available
|
|
1577
|
+
if (options.contextManager?.updateUsage && finishResult.usage) {
|
|
1578
|
+
options.contextManager.updateUsage({
|
|
1579
|
+
inputTokens: finishResult.usage.inputTokens,
|
|
1580
|
+
outputTokens: finishResult.usage.outputTokens,
|
|
1581
|
+
totalTokens: finishResult.usage.totalTokens,
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
if (effectiveGenOptions.threadId && options.checkpointer) {
|
|
1585
|
+
const finalMessages = [
|
|
1586
|
+
...initialParams.messages,
|
|
1587
|
+
{ role: "assistant", content: finishResult.text },
|
|
1588
|
+
];
|
|
1589
|
+
await saveCheckpoint(effectiveGenOptions.threadId, finalMessages, startStep + finishResult.steps.length);
|
|
1590
|
+
}
|
|
1591
|
+
// Invoke unified PostGenerate hooks
|
|
1592
|
+
const hookResult = {
|
|
1593
|
+
status: "complete",
|
|
1594
|
+
text: finishResult.text,
|
|
1595
|
+
usage: finishResult.usage,
|
|
1596
|
+
finishReason: finishResult.finishReason,
|
|
1597
|
+
output: undefined,
|
|
1598
|
+
steps: mapSteps(finishResult.steps),
|
|
1599
|
+
};
|
|
1600
|
+
const postGenerateHooks = effectiveHooks?.PostGenerate ?? [];
|
|
1601
|
+
if (postGenerateHooks.length > 0) {
|
|
1602
|
+
const postGenerateInput = {
|
|
1603
|
+
hook_event_name: "PostGenerate",
|
|
1604
|
+
session_id: effectiveGenOptions.threadId ?? "default",
|
|
1605
|
+
cwd: process.cwd(),
|
|
1606
|
+
options: effectiveGenOptions,
|
|
1607
|
+
result: hookResult,
|
|
1608
|
+
};
|
|
1609
|
+
await invokeHooksWithTimeout(postGenerateHooks, postGenerateInput, null, agent);
|
|
1610
|
+
// Note: updatedResult is not applied for streaming since the stream has already been sent
|
|
1611
|
+
}
|
|
1612
|
+
},
|
|
1613
|
+
});
|
|
1614
|
+
// Return AI SDK compatible response for use with useChat
|
|
1615
|
+
// Note: Not passing originalMessages since ModelMessage[] != UIMessage[]
|
|
1616
|
+
// The AI SDK will reconstruct messages from the stream
|
|
1617
|
+
return result.toUIMessageStreamResponse();
|
|
1618
|
+
}
|
|
1619
|
+
catch (error) {
|
|
1620
|
+
// Normalize error to AgentError
|
|
1621
|
+
const normalizedError = normalizeError(error, "Stream generation failed", effectiveGenOptions.threadId);
|
|
1622
|
+
// Handle error with PostGenerateFailure hooks and fallback logic
|
|
1623
|
+
const postGenerateFailureHooks = effectiveHooks?.PostGenerateFailure ?? [];
|
|
1624
|
+
const errorDecision = await handleGenerationError(normalizedError, postGenerateFailureHooks, effectiveGenOptions, agent, retryState, options.fallbackModel, shouldUseFallback);
|
|
1625
|
+
if (errorDecision.shouldRetry) {
|
|
1626
|
+
// Update retry state
|
|
1627
|
+
Object.assign(retryState, updateRetryLoopState(retryState, errorDecision));
|
|
1628
|
+
// Wait for the specified delay before retrying
|
|
1629
|
+
await waitForRetryDelay(errorDecision.retryDelayMs);
|
|
1630
|
+
// Continue to next iteration of retry loop
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1633
|
+
// No retry requested or max retries exceeded - throw the normalized error
|
|
1634
|
+
throw normalizedError;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
// This should never be reached, but TypeScript needs it for type safety
|
|
1638
|
+
throw new Error("Unexpected: retry loop exited without return or throw");
|
|
1639
|
+
},
|
|
1640
|
+
async streamRaw(genOptions) {
|
|
1641
|
+
// Invoke unified PreGenerate hooks
|
|
1642
|
+
// Note: respondWith cache short-circuit is NOT supported for streamRaw()
|
|
1643
|
+
// because it returns the raw AI SDK streamText result which cannot be mocked.
|
|
1644
|
+
// Use stream(), streamResponse(), or streamDataResponse() for caching support.
|
|
1645
|
+
const preGenerateHooks = effectiveHooks?.PreGenerate ?? [];
|
|
1646
|
+
const preGenResult = await invokePreGenerateHooks(preGenerateHooks, genOptions, agent);
|
|
1647
|
+
// Input transformation is applied even though respondWith is not supported
|
|
1648
|
+
const effectiveGenOptions = preGenResult.effectiveOptions;
|
|
1649
|
+
// Initialize retry loop state
|
|
1650
|
+
const retryState = createRetryLoopState(options.model);
|
|
1651
|
+
while (retryState.retryAttempt <= retryState.maxRetries) {
|
|
1652
|
+
try {
|
|
1653
|
+
const { messages, checkpoint } = await buildMessages(effectiveGenOptions);
|
|
1654
|
+
const maxSteps = options.maxSteps ?? 10;
|
|
1655
|
+
const startStep = checkpoint?.step ?? 0;
|
|
1656
|
+
// Build initial params - use active tools (core + dynamically loaded + task)
|
|
1657
|
+
// Apply hooks AFTER adding task tool so task tool is also wrapped
|
|
1658
|
+
const activeTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSet(effectiveGenOptions.threadId)), effectiveGenOptions.threadId);
|
|
1659
|
+
const initialParams = {
|
|
1660
|
+
system: options.systemPrompt,
|
|
1661
|
+
messages,
|
|
1662
|
+
tools: activeTools,
|
|
1663
|
+
maxTokens: effectiveGenOptions.maxTokens,
|
|
1664
|
+
temperature: effectiveGenOptions.temperature,
|
|
1665
|
+
stopSequences: effectiveGenOptions.stopSequences,
|
|
1666
|
+
abortSignal: effectiveGenOptions.signal,
|
|
1667
|
+
providerOptions: effectiveGenOptions.providerOptions,
|
|
1668
|
+
headers: effectiveGenOptions.headers,
|
|
1669
|
+
};
|
|
1670
|
+
// Track step count for incremental checkpointing
|
|
1671
|
+
let currentStepCount = 0;
|
|
1672
|
+
// Execute stream
|
|
1673
|
+
const result = streamText({
|
|
1674
|
+
model: retryState.currentModel,
|
|
1675
|
+
system: initialParams.system,
|
|
1676
|
+
messages: initialParams.messages,
|
|
1677
|
+
tools: initialParams.tools,
|
|
1678
|
+
maxOutputTokens: initialParams.maxTokens,
|
|
1679
|
+
temperature: initialParams.temperature,
|
|
1680
|
+
stopSequences: initialParams.stopSequences,
|
|
1681
|
+
abortSignal: initialParams.abortSignal,
|
|
1682
|
+
stopWhen: stepCountIs(maxSteps),
|
|
1683
|
+
// Passthrough AI SDK options
|
|
1684
|
+
output: effectiveGenOptions.output,
|
|
1685
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
|
|
1686
|
+
providerOptions: initialParams.providerOptions,
|
|
1687
|
+
headers: initialParams.headers,
|
|
1688
|
+
// Incremental checkpointing: save after each step if enabled
|
|
1689
|
+
onStepFinish: effectiveGenOptions.checkpointAfterToolCall
|
|
1690
|
+
? async (stepResult) => {
|
|
1691
|
+
if (effectiveGenOptions.threadId && options.checkpointer) {
|
|
1692
|
+
currentStepCount++;
|
|
1693
|
+
// Build messages including this step's results
|
|
1694
|
+
const stepMessages = [
|
|
1695
|
+
...initialParams.messages,
|
|
1696
|
+
{
|
|
1697
|
+
role: "assistant",
|
|
1698
|
+
content: stepResult.text,
|
|
1699
|
+
},
|
|
1700
|
+
];
|
|
1701
|
+
await saveCheckpoint(effectiveGenOptions.threadId, stepMessages, startStep + currentStepCount);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
: undefined,
|
|
1705
|
+
// Save checkpoint and invoke unified PostGenerate hook after completion
|
|
1706
|
+
onFinish: async (finishResult) => {
|
|
1707
|
+
// Update context manager with actual usage if available
|
|
1708
|
+
if (options.contextManager?.updateUsage && finishResult.usage) {
|
|
1709
|
+
options.contextManager.updateUsage({
|
|
1710
|
+
inputTokens: finishResult.usage.inputTokens,
|
|
1711
|
+
outputTokens: finishResult.usage.outputTokens,
|
|
1712
|
+
totalTokens: finishResult.usage.totalTokens,
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
if (effectiveGenOptions.threadId && options.checkpointer) {
|
|
1716
|
+
const finalMessages = [
|
|
1717
|
+
...initialParams.messages,
|
|
1718
|
+
{ role: "assistant", content: finishResult.text },
|
|
1719
|
+
];
|
|
1720
|
+
await saveCheckpoint(effectiveGenOptions.threadId, finalMessages, startStep + finishResult.steps.length);
|
|
1721
|
+
}
|
|
1722
|
+
// Invoke unified PostGenerate hooks
|
|
1723
|
+
const hookResult = {
|
|
1724
|
+
status: "complete",
|
|
1725
|
+
text: finishResult.text,
|
|
1726
|
+
usage: finishResult.usage,
|
|
1727
|
+
finishReason: finishResult.finishReason,
|
|
1728
|
+
output: undefined,
|
|
1729
|
+
steps: mapSteps(finishResult.steps),
|
|
1730
|
+
};
|
|
1731
|
+
const postGenerateHooks = effectiveHooks?.PostGenerate ?? [];
|
|
1732
|
+
if (postGenerateHooks.length > 0) {
|
|
1733
|
+
const postGenerateInput = {
|
|
1734
|
+
hook_event_name: "PostGenerate",
|
|
1735
|
+
session_id: effectiveGenOptions.threadId ?? "default",
|
|
1736
|
+
cwd: process.cwd(),
|
|
1737
|
+
options: effectiveGenOptions,
|
|
1738
|
+
result: hookResult,
|
|
1739
|
+
};
|
|
1740
|
+
await invokeHooksWithTimeout(postGenerateHooks, postGenerateInput, null, agent);
|
|
1741
|
+
// Note: updatedResult is not applied for streaming since the stream has already been sent
|
|
1742
|
+
}
|
|
1743
|
+
},
|
|
1744
|
+
});
|
|
1745
|
+
return result;
|
|
1746
|
+
}
|
|
1747
|
+
catch (error) {
|
|
1748
|
+
// Normalize error to AgentError
|
|
1749
|
+
const normalizedError = normalizeError(error, "Stream generation failed", effectiveGenOptions.threadId);
|
|
1750
|
+
// Handle error with PostGenerateFailure hooks and fallback logic
|
|
1751
|
+
const postGenerateFailureHooks = effectiveHooks?.PostGenerateFailure ?? [];
|
|
1752
|
+
const errorDecision = await handleGenerationError(normalizedError, postGenerateFailureHooks, effectiveGenOptions, agent, retryState, options.fallbackModel, shouldUseFallback);
|
|
1753
|
+
if (errorDecision.shouldRetry) {
|
|
1754
|
+
// Update retry state
|
|
1755
|
+
Object.assign(retryState, updateRetryLoopState(retryState, errorDecision));
|
|
1756
|
+
// Wait for the specified delay before retrying
|
|
1757
|
+
await waitForRetryDelay(errorDecision.retryDelayMs);
|
|
1758
|
+
// Continue to next iteration of retry loop
|
|
1759
|
+
continue;
|
|
1760
|
+
}
|
|
1761
|
+
// No retry requested or max retries exceeded - throw the normalized error
|
|
1762
|
+
throw normalizedError;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
// This should never be reached, but TypeScript needs it for type safety
|
|
1766
|
+
throw new Error("Unexpected: retry loop exited without return or throw");
|
|
1767
|
+
},
|
|
1768
|
+
async streamDataResponse(genOptions) {
|
|
1769
|
+
// Invoke unified PreGenerate hooks
|
|
1770
|
+
const preGenerateHooks = effectiveHooks?.PreGenerate ?? [];
|
|
1771
|
+
const preGenResult = await invokePreGenerateHooks(preGenerateHooks, genOptions, agent);
|
|
1772
|
+
// Check for cache short-circuit via respondWith
|
|
1773
|
+
// For data stream response, create a simple text response from the cached result
|
|
1774
|
+
if (preGenResult.cachedResult !== undefined) {
|
|
1775
|
+
const cachedResult = preGenResult.cachedResult;
|
|
1776
|
+
// For cached results, return a simple Response with the cached text
|
|
1777
|
+
// This is compatible with useChat and provides immediate delivery
|
|
1778
|
+
// Only complete results can be cached
|
|
1779
|
+
const text = cachedResult.status === "complete" ? cachedResult.text : "";
|
|
1780
|
+
return new Response(text, {
|
|
1781
|
+
headers: {
|
|
1782
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
1783
|
+
},
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
const effectiveGenOptions = preGenResult.effectiveOptions;
|
|
1787
|
+
// Initialize retry loop state
|
|
1788
|
+
const retryState = createRetryLoopState(options.model);
|
|
1789
|
+
while (retryState.retryAttempt <= retryState.maxRetries) {
|
|
1790
|
+
try {
|
|
1791
|
+
const { messages, checkpoint } = await buildMessages(effectiveGenOptions);
|
|
1792
|
+
const maxSteps = options.maxSteps ?? 10;
|
|
1793
|
+
const startStep = checkpoint?.step ?? 0;
|
|
1794
|
+
// Capture currentModel for use in the callback closure
|
|
1795
|
+
const modelToUse = retryState.currentModel;
|
|
1796
|
+
// Create a UI message stream that tools can write to
|
|
1797
|
+
const stream = createUIMessageStream({
|
|
1798
|
+
execute: async ({ writer }) => {
|
|
1799
|
+
// Notify caller that writer is ready (for log streaming setup)
|
|
1800
|
+
if (effectiveGenOptions.onStreamWriterReady) {
|
|
1801
|
+
effectiveGenOptions.onStreamWriterReady(writer);
|
|
1802
|
+
}
|
|
1803
|
+
// Create streaming context for tools
|
|
1804
|
+
const streamingContext = { writer };
|
|
1805
|
+
// Build tools with streaming context and task tool
|
|
1806
|
+
// Apply hooks AFTER adding task tool so task tool is also wrapped
|
|
1807
|
+
const streamingTools = applyToolHooks(addTaskToolIfConfigured(getActiveToolSetWithStreaming(streamingContext, effectiveGenOptions.threadId), streamingContext), effectiveGenOptions.threadId);
|
|
1808
|
+
// Build initial params with streaming-aware tools
|
|
1809
|
+
const initialParams = {
|
|
1810
|
+
system: options.systemPrompt,
|
|
1811
|
+
messages,
|
|
1812
|
+
tools: streamingTools,
|
|
1813
|
+
maxTokens: effectiveGenOptions.maxTokens,
|
|
1814
|
+
temperature: effectiveGenOptions.temperature,
|
|
1815
|
+
stopSequences: effectiveGenOptions.stopSequences,
|
|
1816
|
+
abortSignal: effectiveGenOptions.signal,
|
|
1817
|
+
providerOptions: effectiveGenOptions.providerOptions,
|
|
1818
|
+
headers: effectiveGenOptions.headers,
|
|
1819
|
+
};
|
|
1820
|
+
// Track step count for incremental checkpointing
|
|
1821
|
+
let currentStepCount = 0;
|
|
1822
|
+
// Execute stream
|
|
1823
|
+
const result = streamText({
|
|
1824
|
+
model: modelToUse,
|
|
1825
|
+
system: initialParams.system,
|
|
1826
|
+
messages: initialParams.messages,
|
|
1827
|
+
tools: initialParams.tools,
|
|
1828
|
+
maxOutputTokens: initialParams.maxTokens,
|
|
1829
|
+
temperature: initialParams.temperature,
|
|
1830
|
+
stopSequences: initialParams.stopSequences,
|
|
1831
|
+
abortSignal: initialParams.abortSignal,
|
|
1832
|
+
stopWhen: stepCountIs(maxSteps),
|
|
1833
|
+
// Passthrough AI SDK options
|
|
1834
|
+
output: effectiveGenOptions.output,
|
|
1835
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type cast needed for AI SDK compatibility
|
|
1836
|
+
providerOptions: initialParams.providerOptions,
|
|
1837
|
+
headers: initialParams.headers,
|
|
1838
|
+
// Incremental checkpointing: save after each step if enabled
|
|
1839
|
+
onStepFinish: effectiveGenOptions.checkpointAfterToolCall
|
|
1840
|
+
? async (stepResult) => {
|
|
1841
|
+
if (effectiveGenOptions.threadId && options.checkpointer) {
|
|
1842
|
+
currentStepCount++;
|
|
1843
|
+
// Build messages including this step's results
|
|
1844
|
+
const stepMessages = [
|
|
1845
|
+
...initialParams.messages,
|
|
1846
|
+
{
|
|
1847
|
+
role: "assistant",
|
|
1848
|
+
content: stepResult.text,
|
|
1849
|
+
},
|
|
1850
|
+
];
|
|
1851
|
+
await saveCheckpoint(effectiveGenOptions.threadId, stepMessages, startStep + currentStepCount);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
: undefined,
|
|
1855
|
+
// Save checkpoint and invoke unified PostGenerate hook after completion
|
|
1856
|
+
onFinish: async (finishResult) => {
|
|
1857
|
+
// Update context manager with actual usage if available
|
|
1858
|
+
if (options.contextManager?.updateUsage && finishResult.usage) {
|
|
1859
|
+
options.contextManager.updateUsage({
|
|
1860
|
+
inputTokens: finishResult.usage.inputTokens,
|
|
1861
|
+
outputTokens: finishResult.usage.outputTokens,
|
|
1862
|
+
totalTokens: finishResult.usage.totalTokens,
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
if (effectiveGenOptions.threadId && options.checkpointer) {
|
|
1866
|
+
const finalMessages = [
|
|
1867
|
+
...initialParams.messages,
|
|
1868
|
+
{
|
|
1869
|
+
role: "assistant",
|
|
1870
|
+
content: finishResult.text,
|
|
1871
|
+
},
|
|
1872
|
+
];
|
|
1873
|
+
await saveCheckpoint(effectiveGenOptions.threadId, finalMessages, startStep + finishResult.steps.length);
|
|
1874
|
+
}
|
|
1875
|
+
// Invoke unified PostGenerate hooks
|
|
1876
|
+
const hookResult = {
|
|
1877
|
+
status: "complete",
|
|
1878
|
+
text: finishResult.text,
|
|
1879
|
+
usage: finishResult.usage,
|
|
1880
|
+
finishReason: finishResult.finishReason,
|
|
1881
|
+
output: undefined,
|
|
1882
|
+
steps: mapSteps(finishResult.steps),
|
|
1883
|
+
};
|
|
1884
|
+
const postGenerateHooks = effectiveHooks?.PostGenerate ?? [];
|
|
1885
|
+
if (postGenerateHooks.length > 0) {
|
|
1886
|
+
const postGenerateInput = {
|
|
1887
|
+
hook_event_name: "PostGenerate",
|
|
1888
|
+
session_id: effectiveGenOptions.threadId ?? "default",
|
|
1889
|
+
cwd: process.cwd(),
|
|
1890
|
+
options: effectiveGenOptions,
|
|
1891
|
+
result: hookResult,
|
|
1892
|
+
};
|
|
1893
|
+
await invokeHooksWithTimeout(postGenerateHooks, postGenerateInput, null, agent);
|
|
1894
|
+
// Note: updatedResult is not applied for streaming since the stream has already been sent
|
|
1895
|
+
}
|
|
1896
|
+
},
|
|
1897
|
+
});
|
|
1898
|
+
// Merge the streamText output into the UI message stream
|
|
1899
|
+
writer.merge(result.toUIMessageStream());
|
|
1900
|
+
},
|
|
1901
|
+
});
|
|
1902
|
+
// Convert the stream to a Response
|
|
1903
|
+
return createUIMessageStreamResponse({ stream });
|
|
1904
|
+
}
|
|
1905
|
+
catch (error) {
|
|
1906
|
+
// Normalize error to AgentError
|
|
1907
|
+
const normalizedError = normalizeError(error, "Stream generation failed", effectiveGenOptions.threadId);
|
|
1908
|
+
// Handle error with PostGenerateFailure hooks and fallback logic
|
|
1909
|
+
const postGenerateFailureHooks = effectiveHooks?.PostGenerateFailure ?? [];
|
|
1910
|
+
const errorDecision = await handleGenerationError(normalizedError, postGenerateFailureHooks, effectiveGenOptions, agent, retryState, options.fallbackModel, shouldUseFallback);
|
|
1911
|
+
if (errorDecision.shouldRetry) {
|
|
1912
|
+
// Update retry state
|
|
1913
|
+
Object.assign(retryState, updateRetryLoopState(retryState, errorDecision));
|
|
1914
|
+
// Wait for the specified delay before retrying
|
|
1915
|
+
await waitForRetryDelay(errorDecision.retryDelayMs);
|
|
1916
|
+
// Continue to next iteration of retry loop
|
|
1917
|
+
continue;
|
|
1918
|
+
}
|
|
1919
|
+
// No retry requested or max retries exceeded - throw the normalized error
|
|
1920
|
+
throw normalizedError;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
// This should never be reached, but TypeScript needs it for type safety
|
|
1924
|
+
throw new Error("Unexpected: retry loop exited without return or throw");
|
|
1925
|
+
},
|
|
1926
|
+
getActiveTools() {
|
|
1927
|
+
return getActiveToolSet();
|
|
1928
|
+
},
|
|
1929
|
+
loadTools(toolNames) {
|
|
1930
|
+
if (!toolRegistry) {
|
|
1931
|
+
// No registry in eager mode - all tools already loaded
|
|
1932
|
+
return { loaded: [], notFound: toolNames };
|
|
1933
|
+
}
|
|
1934
|
+
const result = toolRegistry.load(toolNames);
|
|
1935
|
+
return {
|
|
1936
|
+
loaded: result.loaded,
|
|
1937
|
+
notFound: result.notFound,
|
|
1938
|
+
};
|
|
1939
|
+
},
|
|
1940
|
+
setPermissionMode(mode) {
|
|
1941
|
+
permissionMode = mode;
|
|
1942
|
+
},
|
|
1943
|
+
async getInterrupt(threadId) {
|
|
1944
|
+
if (!options.checkpointer) {
|
|
1945
|
+
return undefined;
|
|
1946
|
+
}
|
|
1947
|
+
const checkpoint = await options.checkpointer.load(threadId);
|
|
1948
|
+
return checkpoint?.pendingInterrupt;
|
|
1949
|
+
},
|
|
1950
|
+
async resume(threadId, interruptId, response, genOptions) {
|
|
1951
|
+
if (!options.checkpointer) {
|
|
1952
|
+
throw new Error("Cannot resume: checkpointer is required");
|
|
1953
|
+
}
|
|
1954
|
+
const checkpoint = await options.checkpointer.load(threadId);
|
|
1955
|
+
if (!checkpoint) {
|
|
1956
|
+
throw new Error(`Cannot resume: no checkpoint found for thread ${threadId}`);
|
|
1957
|
+
}
|
|
1958
|
+
const interrupt = checkpoint.pendingInterrupt;
|
|
1959
|
+
if (!interrupt) {
|
|
1960
|
+
throw new Error(`Cannot resume: no pending interrupt found for thread ${threadId}`);
|
|
1961
|
+
}
|
|
1962
|
+
if (interrupt.id !== interruptId) {
|
|
1963
|
+
throw new Error(`Cannot resume: interrupt ID mismatch. Expected ${interrupt.id}, got ${interruptId}`);
|
|
1964
|
+
}
|
|
1965
|
+
// Store the response for the tool wrapper to use
|
|
1966
|
+
const toolCallId = interrupt.toolCallId;
|
|
1967
|
+
if (toolCallId) {
|
|
1968
|
+
pendingResponses.set(toolCallId, response);
|
|
1969
|
+
}
|
|
1970
|
+
// Emit InterruptResolved hook
|
|
1971
|
+
const interruptResolvedHooks = effectiveHooks?.InterruptResolved ?? [];
|
|
1972
|
+
if (interruptResolvedHooks.length > 0) {
|
|
1973
|
+
const isApproval = isApprovalInterrupt(interrupt);
|
|
1974
|
+
const approvalResponse = isApproval ? response : undefined;
|
|
1975
|
+
const hookInput = {
|
|
1976
|
+
hook_event_name: "InterruptResolved",
|
|
1977
|
+
session_id: threadId,
|
|
1978
|
+
cwd: process.cwd(),
|
|
1979
|
+
interrupt_id: interrupt.id,
|
|
1980
|
+
interrupt_type: interrupt.type,
|
|
1981
|
+
tool_call_id: interrupt.toolCallId,
|
|
1982
|
+
tool_name: interrupt.toolName,
|
|
1983
|
+
response,
|
|
1984
|
+
approved: approvalResponse?.approved,
|
|
1985
|
+
};
|
|
1986
|
+
await invokeHooksWithTimeout(interruptResolvedHooks, hookInput, null, agent);
|
|
1987
|
+
}
|
|
1988
|
+
// Handle approval interrupt
|
|
1989
|
+
if (isApprovalInterrupt(interrupt)) {
|
|
1990
|
+
const approvalResponse = response;
|
|
1991
|
+
// For backward compatibility, also store in approvalDecisions
|
|
1992
|
+
approvalDecisions.set(interrupt.toolCallId, approvalResponse.approved);
|
|
1993
|
+
// Build the assistant message with the tool call
|
|
1994
|
+
const assistantMessage = {
|
|
1995
|
+
role: "assistant",
|
|
1996
|
+
content: [
|
|
1997
|
+
{
|
|
1998
|
+
type: "tool-call",
|
|
1999
|
+
toolCallId: interrupt.toolCallId,
|
|
2000
|
+
toolName: interrupt.toolName,
|
|
2001
|
+
input: interrupt.request.args,
|
|
2002
|
+
},
|
|
2003
|
+
],
|
|
2004
|
+
};
|
|
2005
|
+
let toolResultOutput;
|
|
2006
|
+
if (approvalResponse.approved) {
|
|
2007
|
+
// Approved: Execute the tool deterministically
|
|
2008
|
+
const unwrappedTools = { ...coreTools };
|
|
2009
|
+
for (const plugin of options.plugins ?? []) {
|
|
2010
|
+
if (plugin.tools && typeof plugin.tools !== "function") {
|
|
2011
|
+
Object.assign(unwrappedTools, plugin.tools);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
const mcpTools = mcpManager.getToolSet();
|
|
2015
|
+
Object.assign(unwrappedTools, mcpTools);
|
|
2016
|
+
const tool = unwrappedTools[interrupt.toolName];
|
|
2017
|
+
if (!tool || !tool.execute) {
|
|
2018
|
+
throw new Error(`Cannot resume: tool "${interrupt.toolName}" not found or has no execute function`);
|
|
2019
|
+
}
|
|
2020
|
+
try {
|
|
2021
|
+
toolResultOutput = await tool.execute(interrupt.request.args, {
|
|
2022
|
+
toolCallId: interrupt.toolCallId,
|
|
2023
|
+
messages: checkpoint.messages,
|
|
2024
|
+
abortSignal: genOptions?.signal,
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
catch (error) {
|
|
2028
|
+
toolResultOutput = {
|
|
2029
|
+
error: true,
|
|
2030
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
else {
|
|
2035
|
+
// Denied: Create a synthetic denial result
|
|
2036
|
+
toolResultOutput = {
|
|
2037
|
+
denied: true,
|
|
2038
|
+
message: `Tool "${interrupt.toolName}" was denied by user${approvalResponse.reason ? `: ${approvalResponse.reason}` : ""}`,
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
// Build the tool result message
|
|
2042
|
+
const toolResultMessage = {
|
|
2043
|
+
role: "tool",
|
|
2044
|
+
content: [
|
|
2045
|
+
{
|
|
2046
|
+
type: "tool-result",
|
|
2047
|
+
toolCallId: interrupt.toolCallId,
|
|
2048
|
+
toolName: interrupt.toolName,
|
|
2049
|
+
output: toolResultOutput,
|
|
2050
|
+
},
|
|
2051
|
+
],
|
|
2052
|
+
};
|
|
2053
|
+
// Update checkpoint with the tool call and result messages, clear interrupt
|
|
2054
|
+
const updatedMessages = [
|
|
2055
|
+
...checkpoint.messages,
|
|
2056
|
+
assistantMessage,
|
|
2057
|
+
toolResultMessage,
|
|
2058
|
+
];
|
|
2059
|
+
const updatedCheckpoint = updateCheckpoint(checkpoint, {
|
|
2060
|
+
messages: updatedMessages,
|
|
2061
|
+
pendingInterrupt: undefined,
|
|
2062
|
+
step: checkpoint.step + 1,
|
|
2063
|
+
});
|
|
2064
|
+
await options.checkpointer.save(updatedCheckpoint);
|
|
2065
|
+
// Clean up the response from our maps
|
|
2066
|
+
pendingResponses.delete(interrupt.toolCallId);
|
|
2067
|
+
approvalDecisions.delete(interrupt.toolCallId);
|
|
2068
|
+
// Continue generation from the updated checkpoint
|
|
2069
|
+
return agent.generate({
|
|
2070
|
+
threadId,
|
|
2071
|
+
...genOptions,
|
|
2072
|
+
prompt: undefined,
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
// For custom interrupts, the tool's interrupt() call will receive the response
|
|
2076
|
+
// Store it and re-run generation - the tool will get the response
|
|
2077
|
+
// Clear the interrupt from checkpoint before continuing
|
|
2078
|
+
const updatedCheckpoint = updateCheckpoint(checkpoint, {
|
|
2079
|
+
pendingInterrupt: undefined,
|
|
2080
|
+
});
|
|
2081
|
+
await options.checkpointer.save(updatedCheckpoint);
|
|
2082
|
+
// Continue generation - the wrapped tool will pick up the response from pendingResponses
|
|
2083
|
+
return agent.generate({
|
|
2084
|
+
threadId,
|
|
2085
|
+
...genOptions,
|
|
2086
|
+
prompt: undefined,
|
|
2087
|
+
});
|
|
2088
|
+
},
|
|
2089
|
+
// Initialize the ready promise
|
|
2090
|
+
ready: Promise.resolve(),
|
|
2091
|
+
};
|
|
2092
|
+
// Initialize plugins and middleware asynchronously (including MCP server connections)
|
|
2093
|
+
const initPromise = (async () => {
|
|
2094
|
+
// Setup middleware first
|
|
2095
|
+
await setupMiddleware(middleware);
|
|
2096
|
+
for (const plugin of options.plugins ?? []) {
|
|
2097
|
+
// Connect to MCP server if configured
|
|
2098
|
+
if (plugin.mcpServer) {
|
|
2099
|
+
try {
|
|
2100
|
+
await mcpManager.connectServer(plugin.name, plugin.mcpServer);
|
|
2101
|
+
}
|
|
2102
|
+
catch (error) {
|
|
2103
|
+
// Log error with full details - MCP connection failures are common
|
|
2104
|
+
// issues that are hard to debug when silently swallowed
|
|
2105
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2106
|
+
console.warn(`[Agent SDK] MCP server connection failed for plugin '${plugin.name}':\n` +
|
|
2107
|
+
` Error: ${errorMessage}\n` +
|
|
2108
|
+
` Server: ${JSON.stringify(plugin.mcpServer)}\n` +
|
|
2109
|
+
` The agent will continue without this plugin's MCP tools.`);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
// Run plugin setup
|
|
2113
|
+
if (plugin.setup) {
|
|
2114
|
+
await plugin.setup(agent);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
})();
|
|
2118
|
+
// Replace the ready promise with the actual initialization
|
|
2119
|
+
agent.ready = initPromise;
|
|
2120
|
+
return agent;
|
|
2121
|
+
}
|
|
2122
|
+
//# sourceMappingURL=agent.js.map
|