@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.
Files changed (327) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2321 -0
  3. package/dist/agent.d.ts +52 -0
  4. package/dist/agent.d.ts.map +1 -0
  5. package/dist/agent.js +2122 -0
  6. package/dist/agent.js.map +1 -0
  7. package/dist/backend.d.ts +378 -0
  8. package/dist/backend.d.ts.map +1 -0
  9. package/dist/backend.js +71 -0
  10. package/dist/backend.js.map +1 -0
  11. package/dist/backends/composite.d.ts +258 -0
  12. package/dist/backends/composite.d.ts.map +1 -0
  13. package/dist/backends/composite.js +437 -0
  14. package/dist/backends/composite.js.map +1 -0
  15. package/dist/backends/filesystem.d.ts +268 -0
  16. package/dist/backends/filesystem.d.ts.map +1 -0
  17. package/dist/backends/filesystem.js +623 -0
  18. package/dist/backends/filesystem.js.map +1 -0
  19. package/dist/backends/index.d.ts +14 -0
  20. package/dist/backends/index.d.ts.map +1 -0
  21. package/dist/backends/index.js +14 -0
  22. package/dist/backends/index.js.map +1 -0
  23. package/dist/backends/persistent.d.ts +312 -0
  24. package/dist/backends/persistent.d.ts.map +1 -0
  25. package/dist/backends/persistent.js +519 -0
  26. package/dist/backends/persistent.js.map +1 -0
  27. package/dist/backends/sandbox.d.ts +315 -0
  28. package/dist/backends/sandbox.d.ts.map +1 -0
  29. package/dist/backends/sandbox.js +490 -0
  30. package/dist/backends/sandbox.js.map +1 -0
  31. package/dist/backends/state.d.ts +225 -0
  32. package/dist/backends/state.d.ts.map +1 -0
  33. package/dist/backends/state.js +396 -0
  34. package/dist/backends/state.js.map +1 -0
  35. package/dist/checkpointer/file-saver.d.ts +182 -0
  36. package/dist/checkpointer/file-saver.d.ts.map +1 -0
  37. package/dist/checkpointer/file-saver.js +298 -0
  38. package/dist/checkpointer/file-saver.js.map +1 -0
  39. package/dist/checkpointer/index.d.ts +40 -0
  40. package/dist/checkpointer/index.d.ts.map +1 -0
  41. package/dist/checkpointer/index.js +40 -0
  42. package/dist/checkpointer/index.js.map +1 -0
  43. package/dist/checkpointer/kv-saver.d.ts +142 -0
  44. package/dist/checkpointer/kv-saver.d.ts.map +1 -0
  45. package/dist/checkpointer/kv-saver.js +176 -0
  46. package/dist/checkpointer/kv-saver.js.map +1 -0
  47. package/dist/checkpointer/memory-saver.d.ts +158 -0
  48. package/dist/checkpointer/memory-saver.d.ts.map +1 -0
  49. package/dist/checkpointer/memory-saver.js +222 -0
  50. package/dist/checkpointer/memory-saver.js.map +1 -0
  51. package/dist/checkpointer/types.d.ts +353 -0
  52. package/dist/checkpointer/types.d.ts.map +1 -0
  53. package/dist/checkpointer/types.js +159 -0
  54. package/dist/checkpointer/types.js.map +1 -0
  55. package/dist/context-manager.d.ts +627 -0
  56. package/dist/context-manager.d.ts.map +1 -0
  57. package/dist/context-manager.js +1039 -0
  58. package/dist/context-manager.js.map +1 -0
  59. package/dist/context.d.ts +57 -0
  60. package/dist/context.d.ts.map +1 -0
  61. package/dist/context.js +76 -0
  62. package/dist/context.js.map +1 -0
  63. package/dist/errors/index.d.ts +611 -0
  64. package/dist/errors/index.d.ts.map +1 -0
  65. package/dist/errors/index.js +1023 -0
  66. package/dist/errors/index.js.map +1 -0
  67. package/dist/generation-helpers.d.ts +126 -0
  68. package/dist/generation-helpers.d.ts.map +1 -0
  69. package/dist/generation-helpers.js +181 -0
  70. package/dist/generation-helpers.js.map +1 -0
  71. package/dist/hooks/audit.d.ts +210 -0
  72. package/dist/hooks/audit.d.ts.map +1 -0
  73. package/dist/hooks/audit.js +305 -0
  74. package/dist/hooks/audit.js.map +1 -0
  75. package/dist/hooks/cache.d.ts +180 -0
  76. package/dist/hooks/cache.d.ts.map +1 -0
  77. package/dist/hooks/cache.js +273 -0
  78. package/dist/hooks/cache.js.map +1 -0
  79. package/dist/hooks/guardrails.d.ts +145 -0
  80. package/dist/hooks/guardrails.d.ts.map +1 -0
  81. package/dist/hooks/guardrails.js +326 -0
  82. package/dist/hooks/guardrails.js.map +1 -0
  83. package/dist/hooks/index.d.ts +18 -0
  84. package/dist/hooks/index.d.ts.map +1 -0
  85. package/dist/hooks/index.js +32 -0
  86. package/dist/hooks/index.js.map +1 -0
  87. package/dist/hooks/logging.d.ts +193 -0
  88. package/dist/hooks/logging.d.ts.map +1 -0
  89. package/dist/hooks/logging.js +345 -0
  90. package/dist/hooks/logging.js.map +1 -0
  91. package/dist/hooks/parallel-guardrails.d.ts +268 -0
  92. package/dist/hooks/parallel-guardrails.d.ts.map +1 -0
  93. package/dist/hooks/parallel-guardrails.js +416 -0
  94. package/dist/hooks/parallel-guardrails.js.map +1 -0
  95. package/dist/hooks/rate-limit.d.ts +305 -0
  96. package/dist/hooks/rate-limit.d.ts.map +1 -0
  97. package/dist/hooks/rate-limit.js +372 -0
  98. package/dist/hooks/rate-limit.js.map +1 -0
  99. package/dist/hooks/retry.d.ts +144 -0
  100. package/dist/hooks/retry.d.ts.map +1 -0
  101. package/dist/hooks/retry.js +210 -0
  102. package/dist/hooks/retry.js.map +1 -0
  103. package/dist/hooks/secrets.d.ts +174 -0
  104. package/dist/hooks/secrets.d.ts.map +1 -0
  105. package/dist/hooks/secrets.js +306 -0
  106. package/dist/hooks/secrets.js.map +1 -0
  107. package/dist/hooks.d.ts +229 -0
  108. package/dist/hooks.d.ts.map +1 -0
  109. package/dist/hooks.js +352 -0
  110. package/dist/hooks.js.map +1 -0
  111. package/dist/index.d.ts +97 -0
  112. package/dist/index.d.ts.map +1 -0
  113. package/dist/index.js +182 -0
  114. package/dist/index.js.map +1 -0
  115. package/dist/mcp/env.d.ts +25 -0
  116. package/dist/mcp/env.d.ts.map +1 -0
  117. package/dist/mcp/env.js +18 -0
  118. package/dist/mcp/env.js.map +1 -0
  119. package/dist/mcp/index.d.ts +16 -0
  120. package/dist/mcp/index.d.ts.map +1 -0
  121. package/dist/mcp/index.js +17 -0
  122. package/dist/mcp/index.js.map +1 -0
  123. package/dist/mcp/manager.d.ts +184 -0
  124. package/dist/mcp/manager.d.ts.map +1 -0
  125. package/dist/mcp/manager.js +446 -0
  126. package/dist/mcp/manager.js.map +1 -0
  127. package/dist/mcp/types.d.ts +58 -0
  128. package/dist/mcp/types.d.ts.map +1 -0
  129. package/dist/mcp/types.js +7 -0
  130. package/dist/mcp/types.js.map +1 -0
  131. package/dist/mcp/validation.d.ts +119 -0
  132. package/dist/mcp/validation.d.ts.map +1 -0
  133. package/dist/mcp/validation.js +407 -0
  134. package/dist/mcp/validation.js.map +1 -0
  135. package/dist/mcp/virtual-server.d.ts +78 -0
  136. package/dist/mcp/virtual-server.d.ts.map +1 -0
  137. package/dist/mcp/virtual-server.js +137 -0
  138. package/dist/mcp/virtual-server.js.map +1 -0
  139. package/dist/memory/filesystem-store.d.ts +217 -0
  140. package/dist/memory/filesystem-store.d.ts.map +1 -0
  141. package/dist/memory/filesystem-store.js +343 -0
  142. package/dist/memory/filesystem-store.js.map +1 -0
  143. package/dist/memory/index.d.ts +46 -0
  144. package/dist/memory/index.d.ts.map +1 -0
  145. package/dist/memory/index.js +46 -0
  146. package/dist/memory/index.js.map +1 -0
  147. package/dist/memory/loader.d.ts +396 -0
  148. package/dist/memory/loader.d.ts.map +1 -0
  149. package/dist/memory/loader.js +419 -0
  150. package/dist/memory/loader.js.map +1 -0
  151. package/dist/memory/permissions.d.ts +282 -0
  152. package/dist/memory/permissions.d.ts.map +1 -0
  153. package/dist/memory/permissions.js +297 -0
  154. package/dist/memory/permissions.js.map +1 -0
  155. package/dist/memory/rules.d.ts +249 -0
  156. package/dist/memory/rules.d.ts.map +1 -0
  157. package/dist/memory/rules.js +362 -0
  158. package/dist/memory/rules.js.map +1 -0
  159. package/dist/memory/store.d.ts +286 -0
  160. package/dist/memory/store.d.ts.map +1 -0
  161. package/dist/memory/store.js +263 -0
  162. package/dist/memory/store.js.map +1 -0
  163. package/dist/middleware/apply.d.ts +73 -0
  164. package/dist/middleware/apply.d.ts.map +1 -0
  165. package/dist/middleware/apply.js +219 -0
  166. package/dist/middleware/apply.js.map +1 -0
  167. package/dist/middleware/context.d.ts +33 -0
  168. package/dist/middleware/context.d.ts.map +1 -0
  169. package/dist/middleware/context.js +176 -0
  170. package/dist/middleware/context.js.map +1 -0
  171. package/dist/middleware/index.d.ts +31 -0
  172. package/dist/middleware/index.d.ts.map +1 -0
  173. package/dist/middleware/index.js +32 -0
  174. package/dist/middleware/index.js.map +1 -0
  175. package/dist/middleware/logging.d.ts +137 -0
  176. package/dist/middleware/logging.d.ts.map +1 -0
  177. package/dist/middleware/logging.js +374 -0
  178. package/dist/middleware/logging.js.map +1 -0
  179. package/dist/middleware/types.d.ts +183 -0
  180. package/dist/middleware/types.d.ts.map +1 -0
  181. package/dist/middleware/types.js +11 -0
  182. package/dist/middleware/types.js.map +1 -0
  183. package/dist/observability/events.d.ts +183 -0
  184. package/dist/observability/events.d.ts.map +1 -0
  185. package/dist/observability/events.js +305 -0
  186. package/dist/observability/events.js.map +1 -0
  187. package/dist/observability/index.d.ts +55 -0
  188. package/dist/observability/index.d.ts.map +1 -0
  189. package/dist/observability/index.js +87 -0
  190. package/dist/observability/index.js.map +1 -0
  191. package/dist/observability/logger.d.ts +318 -0
  192. package/dist/observability/logger.d.ts.map +1 -0
  193. package/dist/observability/logger.js +436 -0
  194. package/dist/observability/logger.js.map +1 -0
  195. package/dist/observability/metrics.d.ts +341 -0
  196. package/dist/observability/metrics.d.ts.map +1 -0
  197. package/dist/observability/metrics.js +490 -0
  198. package/dist/observability/metrics.js.map +1 -0
  199. package/dist/observability/preset.d.ts +161 -0
  200. package/dist/observability/preset.d.ts.map +1 -0
  201. package/dist/observability/preset.js +133 -0
  202. package/dist/observability/preset.js.map +1 -0
  203. package/dist/observability/streaming.d.ts +113 -0
  204. package/dist/observability/streaming.d.ts.map +1 -0
  205. package/dist/observability/streaming.js +114 -0
  206. package/dist/observability/streaming.js.map +1 -0
  207. package/dist/observability/tracing.d.ts +378 -0
  208. package/dist/observability/tracing.d.ts.map +1 -0
  209. package/dist/observability/tracing.js +539 -0
  210. package/dist/observability/tracing.js.map +1 -0
  211. package/dist/plugins.d.ts +55 -0
  212. package/dist/plugins.d.ts.map +1 -0
  213. package/dist/plugins.js +63 -0
  214. package/dist/plugins.js.map +1 -0
  215. package/dist/presets/index.d.ts +7 -0
  216. package/dist/presets/index.d.ts.map +1 -0
  217. package/dist/presets/index.js +7 -0
  218. package/dist/presets/index.js.map +1 -0
  219. package/dist/presets/production.d.ts +262 -0
  220. package/dist/presets/production.d.ts.map +1 -0
  221. package/dist/presets/production.js +295 -0
  222. package/dist/presets/production.js.map +1 -0
  223. package/dist/security/index.d.ts +179 -0
  224. package/dist/security/index.d.ts.map +1 -0
  225. package/dist/security/index.js +323 -0
  226. package/dist/security/index.js.map +1 -0
  227. package/dist/subagents/advanced.d.ts +413 -0
  228. package/dist/subagents/advanced.d.ts.map +1 -0
  229. package/dist/subagents/advanced.js +396 -0
  230. package/dist/subagents/advanced.js.map +1 -0
  231. package/dist/subagents/index.d.ts +14 -0
  232. package/dist/subagents/index.d.ts.map +1 -0
  233. package/dist/subagents/index.js +15 -0
  234. package/dist/subagents/index.js.map +1 -0
  235. package/dist/subagents.d.ts +73 -0
  236. package/dist/subagents.d.ts.map +1 -0
  237. package/dist/subagents.js +213 -0
  238. package/dist/subagents.js.map +1 -0
  239. package/dist/task-store/file-store.d.ts +76 -0
  240. package/dist/task-store/file-store.d.ts.map +1 -0
  241. package/dist/task-store/file-store.js +190 -0
  242. package/dist/task-store/file-store.js.map +1 -0
  243. package/dist/task-store/index.d.ts +11 -0
  244. package/dist/task-store/index.d.ts.map +1 -0
  245. package/dist/task-store/index.js +10 -0
  246. package/dist/task-store/index.js.map +1 -0
  247. package/dist/task-store/kv-store.d.ts +140 -0
  248. package/dist/task-store/kv-store.d.ts.map +1 -0
  249. package/dist/task-store/kv-store.js +169 -0
  250. package/dist/task-store/kv-store.js.map +1 -0
  251. package/dist/task-store/memory-store.d.ts +66 -0
  252. package/dist/task-store/memory-store.d.ts.map +1 -0
  253. package/dist/task-store/memory-store.js +125 -0
  254. package/dist/task-store/memory-store.js.map +1 -0
  255. package/dist/task-store/types.d.ts +235 -0
  256. package/dist/task-store/types.d.ts.map +1 -0
  257. package/dist/task-store/types.js +110 -0
  258. package/dist/task-store/types.js.map +1 -0
  259. package/dist/testing/assertions.d.ts +401 -0
  260. package/dist/testing/assertions.d.ts.map +1 -0
  261. package/dist/testing/assertions.js +630 -0
  262. package/dist/testing/assertions.js.map +1 -0
  263. package/dist/testing/index.d.ts +343 -0
  264. package/dist/testing/index.d.ts.map +1 -0
  265. package/dist/testing/index.js +360 -0
  266. package/dist/testing/index.js.map +1 -0
  267. package/dist/testing/mock-agent.d.ts +214 -0
  268. package/dist/testing/mock-agent.d.ts.map +1 -0
  269. package/dist/testing/mock-agent.js +448 -0
  270. package/dist/testing/mock-agent.js.map +1 -0
  271. package/dist/testing/recorder.d.ts +288 -0
  272. package/dist/testing/recorder.d.ts.map +1 -0
  273. package/dist/testing/recorder.js +499 -0
  274. package/dist/testing/recorder.js.map +1 -0
  275. package/dist/tools/execute.d.ts +104 -0
  276. package/dist/tools/execute.d.ts.map +1 -0
  277. package/dist/tools/execute.js +191 -0
  278. package/dist/tools/execute.js.map +1 -0
  279. package/dist/tools/factory.d.ts +260 -0
  280. package/dist/tools/factory.d.ts.map +1 -0
  281. package/dist/tools/factory.js +241 -0
  282. package/dist/tools/factory.js.map +1 -0
  283. package/dist/tools/filesystem.d.ts +215 -0
  284. package/dist/tools/filesystem.d.ts.map +1 -0
  285. package/dist/tools/filesystem.js +311 -0
  286. package/dist/tools/filesystem.js.map +1 -0
  287. package/dist/tools/index.d.ts +33 -0
  288. package/dist/tools/index.d.ts.map +1 -0
  289. package/dist/tools/index.js +33 -0
  290. package/dist/tools/index.js.map +1 -0
  291. package/dist/tools/search.d.ts +59 -0
  292. package/dist/tools/search.d.ts.map +1 -0
  293. package/dist/tools/search.js +94 -0
  294. package/dist/tools/search.js.map +1 -0
  295. package/dist/tools/skills.d.ts +354 -0
  296. package/dist/tools/skills.d.ts.map +1 -0
  297. package/dist/tools/skills.js +413 -0
  298. package/dist/tools/skills.js.map +1 -0
  299. package/dist/tools/task.d.ts +272 -0
  300. package/dist/tools/task.d.ts.map +1 -0
  301. package/dist/tools/task.js +521 -0
  302. package/dist/tools/task.js.map +1 -0
  303. package/dist/tools/todos.d.ts +131 -0
  304. package/dist/tools/todos.d.ts.map +1 -0
  305. package/dist/tools/todos.js +120 -0
  306. package/dist/tools/todos.js.map +1 -0
  307. package/dist/tools/tool-registry.d.ts +424 -0
  308. package/dist/tools/tool-registry.d.ts.map +1 -0
  309. package/dist/tools/tool-registry.js +607 -0
  310. package/dist/tools/tool-registry.js.map +1 -0
  311. package/dist/tools/user-interaction.d.ts +116 -0
  312. package/dist/tools/user-interaction.d.ts.map +1 -0
  313. package/dist/tools/user-interaction.js +147 -0
  314. package/dist/tools/user-interaction.js.map +1 -0
  315. package/dist/tools/utils.d.ts +124 -0
  316. package/dist/tools/utils.d.ts.map +1 -0
  317. package/dist/tools/utils.js +189 -0
  318. package/dist/tools/utils.js.map +1 -0
  319. package/dist/tools.d.ts +74 -0
  320. package/dist/tools.d.ts.map +1 -0
  321. package/dist/tools.js +73 -0
  322. package/dist/tools.js.map +1 -0
  323. package/dist/types.d.ts +2421 -0
  324. package/dist/types.d.ts.map +1 -0
  325. package/dist/types.js +55 -0
  326. package/dist/types.js.map +1 -0
  327. 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