@rudderjs/ai 1.17.3 → 1.18.0

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 (377) hide show
  1. package/README.md +19 -1274
  2. package/dist/budget-orm/index.d.ts +1 -95
  3. package/dist/budget-orm/index.d.ts.map +1 -1
  4. package/dist/budget-orm/index.js +4 -176
  5. package/dist/budget-orm/index.js.map +1 -1
  6. package/dist/chat-mentions.d.ts +1 -58
  7. package/dist/chat-mentions.d.ts.map +1 -1
  8. package/dist/chat-mentions.js +4 -80
  9. package/dist/chat-mentions.js.map +1 -1
  10. package/dist/commands/ai-eval.d.ts +1 -92
  11. package/dist/commands/ai-eval.d.ts.map +1 -1
  12. package/dist/commands/ai-eval.js +4 -377
  13. package/dist/commands/ai-eval.js.map +1 -1
  14. package/dist/commands/make-agent.d.ts +1 -2
  15. package/dist/commands/make-agent.d.ts.map +1 -1
  16. package/dist/commands/make-agent.js +4 -22
  17. package/dist/commands/make-agent.js.map +1 -1
  18. package/dist/computer-use/index.d.ts +1 -52
  19. package/dist/computer-use/index.d.ts.map +1 -1
  20. package/dist/computer-use/index.js +4 -50
  21. package/dist/computer-use/index.js.map +1 -1
  22. package/dist/conversation-orm/index.d.ts +1 -108
  23. package/dist/conversation-orm/index.d.ts.map +1 -1
  24. package/dist/conversation-orm/index.js +4 -214
  25. package/dist/conversation-orm/index.js.map +1 -1
  26. package/dist/doctor.d.ts +1 -1
  27. package/dist/doctor.d.ts.map +1 -1
  28. package/dist/doctor.js +4 -65
  29. package/dist/doctor.js.map +1 -1
  30. package/dist/eval/index.d.ts +1 -270
  31. package/dist/eval/index.d.ts.map +1 -1
  32. package/dist/eval/index.js +4 -509
  33. package/dist/eval/index.js.map +1 -1
  34. package/dist/gateway/index.d.ts +1 -10
  35. package/dist/gateway/index.d.ts.map +1 -1
  36. package/dist/gateway/index.js +4 -10
  37. package/dist/gateway/index.js.map +1 -1
  38. package/dist/index.d.ts +1 -66
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +4 -78
  41. package/dist/index.js.map +1 -1
  42. package/dist/mcp/index.d.ts +1 -15
  43. package/dist/mcp/index.d.ts.map +1 -1
  44. package/dist/mcp/index.js +4 -14
  45. package/dist/mcp/index.js.map +1 -1
  46. package/dist/memory-embedding/index.d.ts +1 -120
  47. package/dist/memory-embedding/index.d.ts.map +1 -1
  48. package/dist/memory-embedding/index.js +4 -228
  49. package/dist/memory-embedding/index.js.map +1 -1
  50. package/dist/memory-orm/index.d.ts +1 -117
  51. package/dist/memory-orm/index.d.ts.map +1 -1
  52. package/dist/memory-orm/index.js +4 -186
  53. package/dist/memory-orm/index.js.map +1 -1
  54. package/dist/node/index.d.ts +1 -2
  55. package/dist/node/index.d.ts.map +1 -1
  56. package/dist/node/index.js +4 -2
  57. package/dist/node/index.js.map +1 -1
  58. package/dist/observers.d.ts +1 -129
  59. package/dist/observers.d.ts.map +1 -1
  60. package/dist/observers.js +4 -39
  61. package/dist/observers.js.map +1 -1
  62. package/dist/react/index.d.ts +1 -15
  63. package/dist/react/index.d.ts.map +1 -1
  64. package/dist/react/index.js +4 -15
  65. package/dist/react/index.js.map +1 -1
  66. package/dist/server/index.d.ts +1 -1
  67. package/dist/server/index.d.ts.map +1 -1
  68. package/dist/server/index.js +4 -1
  69. package/dist/server/index.js.map +1 -1
  70. package/package.json +9 -13
  71. package/boost/guidelines.md +0 -260
  72. package/boost/skills/ai-agents/SKILL.md +0 -240
  73. package/boost/skills/ai-tools/SKILL.md +0 -260
  74. package/dist/agent-run-store.d.ts +0 -161
  75. package/dist/agent-run-store.d.ts.map +0 -1
  76. package/dist/agent-run-store.js +0 -98
  77. package/dist/agent-run-store.js.map +0 -1
  78. package/dist/agent-sse.d.ts +0 -153
  79. package/dist/agent-sse.d.ts.map +0 -1
  80. package/dist/agent-sse.js +0 -282
  81. package/dist/agent-sse.js.map +0 -1
  82. package/dist/agent.d.ts +0 -508
  83. package/dist/agent.d.ts.map +0 -1
  84. package/dist/agent.js +0 -1538
  85. package/dist/agent.js.map +0 -1
  86. package/dist/attachment.d.ts +0 -31
  87. package/dist/attachment.d.ts.map +0 -1
  88. package/dist/attachment.js +0 -89
  89. package/dist/attachment.js.map +0 -1
  90. package/dist/audio.d.ts +0 -45
  91. package/dist/audio.d.ts.map +0 -1
  92. package/dist/audio.js +0 -93
  93. package/dist/audio.js.map +0 -1
  94. package/dist/base64.d.ts +0 -7
  95. package/dist/base64.d.ts.map +0 -1
  96. package/dist/base64.js +0 -39
  97. package/dist/base64.js.map +0 -1
  98. package/dist/budget/pricing.d.ts +0 -124
  99. package/dist/budget/pricing.d.ts.map +0 -1
  100. package/dist/budget/pricing.js +0 -175
  101. package/dist/budget/pricing.js.map +0 -1
  102. package/dist/budget/storage.d.ts +0 -104
  103. package/dist/budget/storage.d.ts.map +0 -1
  104. package/dist/budget/storage.js +0 -0
  105. package/dist/budget/storage.js.map +0 -1
  106. package/dist/budget/with-budget.d.ts +0 -119
  107. package/dist/budget/with-budget.d.ts.map +0 -1
  108. package/dist/budget/with-budget.js +0 -175
  109. package/dist/budget/with-budget.js.map +0 -1
  110. package/dist/cached-embedding.d.ts +0 -14
  111. package/dist/cached-embedding.d.ts.map +0 -1
  112. package/dist/cached-embedding.js +0 -44
  113. package/dist/cached-embedding.js.map +0 -1
  114. package/dist/computer-use/actions.d.ts +0 -214
  115. package/dist/computer-use/actions.d.ts.map +0 -1
  116. package/dist/computer-use/actions.js +0 -48
  117. package/dist/computer-use/actions.js.map +0 -1
  118. package/dist/computer-use/errors.d.ts +0 -57
  119. package/dist/computer-use/errors.d.ts.map +0 -1
  120. package/dist/computer-use/errors.js +0 -76
  121. package/dist/computer-use/errors.js.map +0 -1
  122. package/dist/computer-use/playwright.d.ts +0 -76
  123. package/dist/computer-use/playwright.d.ts.map +0 -1
  124. package/dist/computer-use/playwright.js +0 -270
  125. package/dist/computer-use/playwright.js.map +0 -1
  126. package/dist/computer-use/tool.d.ts +0 -154
  127. package/dist/computer-use/tool.d.ts.map +0 -1
  128. package/dist/computer-use/tool.js +0 -210
  129. package/dist/computer-use/tool.js.map +0 -1
  130. package/dist/continuation-validation.d.ts +0 -85
  131. package/dist/continuation-validation.d.ts.map +0 -1
  132. package/dist/continuation-validation.js +0 -166
  133. package/dist/continuation-validation.js.map +0 -1
  134. package/dist/conversation-persistence.d.ts +0 -46
  135. package/dist/conversation-persistence.d.ts.map +0 -1
  136. package/dist/conversation-persistence.js +0 -176
  137. package/dist/conversation-persistence.js.map +0 -1
  138. package/dist/conversation.d.ts +0 -11
  139. package/dist/conversation.d.ts.map +0 -1
  140. package/dist/conversation.js +0 -55
  141. package/dist/conversation.js.map +0 -1
  142. package/dist/eval/fixtures.d.ts +0 -65
  143. package/dist/eval/fixtures.d.ts.map +0 -1
  144. package/dist/eval/fixtures.js +0 -110
  145. package/dist/eval/fixtures.js.map +0 -1
  146. package/dist/eval/html-reporter.d.ts +0 -25
  147. package/dist/eval/html-reporter.d.ts.map +0 -1
  148. package/dist/eval/html-reporter.js +0 -209
  149. package/dist/eval/html-reporter.js.map +0 -1
  150. package/dist/eval/json-reporter.d.ts +0 -43
  151. package/dist/eval/json-reporter.d.ts.map +0 -1
  152. package/dist/eval/json-reporter.js +0 -40
  153. package/dist/eval/json-reporter.js.map +0 -1
  154. package/dist/facade.d.ts +0 -96
  155. package/dist/facade.d.ts.map +0 -1
  156. package/dist/facade.js +0 -146
  157. package/dist/facade.js.map +0 -1
  158. package/dist/fake.d.ts +0 -201
  159. package/dist/fake.d.ts.map +0 -1
  160. package/dist/fake.js +0 -428
  161. package/dist/fake.js.map +0 -1
  162. package/dist/file-search.d.ts +0 -168
  163. package/dist/file-search.d.ts.map +0 -1
  164. package/dist/file-search.js +0 -158
  165. package/dist/file-search.js.map +0 -1
  166. package/dist/files.d.ts +0 -27
  167. package/dist/files.d.ts.map +0 -1
  168. package/dist/files.js +0 -44
  169. package/dist/files.js.map +0 -1
  170. package/dist/gateway/http-gateway-adapter.d.ts +0 -94
  171. package/dist/gateway/http-gateway-adapter.d.ts.map +0 -1
  172. package/dist/gateway/http-gateway-adapter.js +0 -106
  173. package/dist/gateway/http-gateway-adapter.js.map +0 -1
  174. package/dist/gateway/sse.d.ts +0 -28
  175. package/dist/gateway/sse.d.ts.map +0 -1
  176. package/dist/gateway/sse.js +0 -78
  177. package/dist/gateway/sse.js.map +0 -1
  178. package/dist/handoff.d.ts +0 -95
  179. package/dist/handoff.d.ts.map +0 -1
  180. package/dist/handoff.js +0 -78
  181. package/dist/handoff.js.map +0 -1
  182. package/dist/handoffs-driver.d.ts +0 -58
  183. package/dist/handoffs-driver.d.ts.map +0 -1
  184. package/dist/handoffs-driver.js +0 -103
  185. package/dist/handoffs-driver.js.map +0 -1
  186. package/dist/image.d.ts +0 -40
  187. package/dist/image.d.ts.map +0 -1
  188. package/dist/image.js +0 -109
  189. package/dist/image.js.map +0 -1
  190. package/dist/mcp/client-tools.d.ts +0 -39
  191. package/dist/mcp/client-tools.d.ts.map +0 -1
  192. package/dist/mcp/client-tools.js +0 -147
  193. package/dist/mcp/client-tools.js.map +0 -1
  194. package/dist/mcp/server-from-agent.d.ts +0 -24
  195. package/dist/mcp/server-from-agent.d.ts.map +0 -1
  196. package/dist/mcp/server-from-agent.js +0 -113
  197. package/dist/mcp/server-from-agent.js.map +0 -1
  198. package/dist/mcp/types.d.ts +0 -64
  199. package/dist/mcp/types.d.ts.map +0 -1
  200. package/dist/mcp/types.js +0 -6
  201. package/dist/mcp/types.js.map +0 -1
  202. package/dist/memory-extract.d.ts +0 -60
  203. package/dist/memory-extract.d.ts.map +0 -1
  204. package/dist/memory-extract.js +0 -163
  205. package/dist/memory-extract.js.map +0 -1
  206. package/dist/memory-inject.d.ts +0 -39
  207. package/dist/memory-inject.d.ts.map +0 -1
  208. package/dist/memory-inject.js +0 -135
  209. package/dist/memory-inject.js.map +0 -1
  210. package/dist/memory.d.ts +0 -55
  211. package/dist/memory.d.ts.map +0 -1
  212. package/dist/memory.js +0 -132
  213. package/dist/memory.js.map +0 -1
  214. package/dist/middleware.d.ts +0 -18
  215. package/dist/middleware.d.ts.map +0 -1
  216. package/dist/middleware.js +0 -72
  217. package/dist/middleware.js.map +0 -1
  218. package/dist/node/attachment.d.ts +0 -6
  219. package/dist/node/attachment.d.ts.map +0 -1
  220. package/dist/node/attachment.js +0 -35
  221. package/dist/node/attachment.js.map +0 -1
  222. package/dist/node/transcription.d.ts +0 -4
  223. package/dist/node/transcription.d.ts.map +0 -1
  224. package/dist/node/transcription.js +0 -8
  225. package/dist/node/transcription.js.map +0 -1
  226. package/dist/output.d.ts +0 -22
  227. package/dist/output.d.ts.map +0 -1
  228. package/dist/output.js +0 -60
  229. package/dist/output.js.map +0 -1
  230. package/dist/provider-tools.d.ts +0 -87
  231. package/dist/provider-tools.d.ts.map +0 -1
  232. package/dist/provider-tools.js +0 -189
  233. package/dist/provider-tools.js.map +0 -1
  234. package/dist/providers/anthropic.d.ts +0 -24
  235. package/dist/providers/anthropic.d.ts.map +0 -1
  236. package/dist/providers/anthropic.js +0 -405
  237. package/dist/providers/anthropic.js.map +0 -1
  238. package/dist/providers/azure.d.ts +0 -13
  239. package/dist/providers/azure.d.ts.map +0 -1
  240. package/dist/providers/azure.js +0 -15
  241. package/dist/providers/azure.js.map +0 -1
  242. package/dist/providers/bedrock.d.ts +0 -75
  243. package/dist/providers/bedrock.d.ts.map +0 -1
  244. package/dist/providers/bedrock.js +0 -181
  245. package/dist/providers/bedrock.js.map +0 -1
  246. package/dist/providers/cohere.d.ts +0 -13
  247. package/dist/providers/cohere.d.ts.map +0 -1
  248. package/dist/providers/cohere.js +0 -87
  249. package/dist/providers/cohere.js.map +0 -1
  250. package/dist/providers/deepseek.d.ts +0 -12
  251. package/dist/providers/deepseek.d.ts.map +0 -1
  252. package/dist/providers/deepseek.js +0 -15
  253. package/dist/providers/deepseek.js.map +0 -1
  254. package/dist/providers/elevenlabs.d.ts +0 -98
  255. package/dist/providers/elevenlabs.d.ts.map +0 -1
  256. package/dist/providers/elevenlabs.js +0 -229
  257. package/dist/providers/elevenlabs.js.map +0 -1
  258. package/dist/providers/google-cache-registry.d.ts +0 -132
  259. package/dist/providers/google-cache-registry.d.ts.map +0 -1
  260. package/dist/providers/google-cache-registry.js +0 -209
  261. package/dist/providers/google-cache-registry.js.map +0 -1
  262. package/dist/providers/google.d.ts +0 -38
  263. package/dist/providers/google.d.ts.map +0 -1
  264. package/dist/providers/google.js +0 -903
  265. package/dist/providers/google.js.map +0 -1
  266. package/dist/providers/groq.d.ts +0 -12
  267. package/dist/providers/groq.d.ts.map +0 -1
  268. package/dist/providers/groq.js +0 -15
  269. package/dist/providers/groq.js.map +0 -1
  270. package/dist/providers/jina.d.ts +0 -13
  271. package/dist/providers/jina.d.ts.map +0 -1
  272. package/dist/providers/jina.js +0 -90
  273. package/dist/providers/jina.js.map +0 -1
  274. package/dist/providers/mistral.d.ts +0 -13
  275. package/dist/providers/mistral.d.ts.map +0 -1
  276. package/dist/providers/mistral.js +0 -46
  277. package/dist/providers/mistral.js.map +0 -1
  278. package/dist/providers/ollama.d.ts +0 -11
  279. package/dist/providers/ollama.d.ts.map +0 -1
  280. package/dist/providers/ollama.js +0 -15
  281. package/dist/providers/ollama.js.map +0 -1
  282. package/dist/providers/openai.d.ts +0 -79
  283. package/dist/providers/openai.d.ts.map +0 -1
  284. package/dist/providers/openai.js +0 -792
  285. package/dist/providers/openai.js.map +0 -1
  286. package/dist/providers/openrouter.d.ts +0 -43
  287. package/dist/providers/openrouter.d.ts.map +0 -1
  288. package/dist/providers/openrouter.js +0 -21
  289. package/dist/providers/openrouter.js.map +0 -1
  290. package/dist/providers/voyage.d.ts +0 -91
  291. package/dist/providers/voyage.d.ts.map +0 -1
  292. package/dist/providers/voyage.js +0 -166
  293. package/dist/providers/voyage.js.map +0 -1
  294. package/dist/providers/xai.d.ts +0 -12
  295. package/dist/providers/xai.d.ts.map +0 -1
  296. package/dist/providers/xai.js +0 -15
  297. package/dist/providers/xai.js.map +0 -1
  298. package/dist/queue-job.d.ts +0 -100
  299. package/dist/queue-job.d.ts.map +0 -1
  300. package/dist/queue-job.js +0 -185
  301. package/dist/queue-job.js.map +0 -1
  302. package/dist/react/agent-run.d.ts +0 -111
  303. package/dist/react/agent-run.d.ts.map +0 -1
  304. package/dist/react/agent-run.js +0 -107
  305. package/dist/react/agent-run.js.map +0 -1
  306. package/dist/react/useAgentRun.d.ts +0 -68
  307. package/dist/react/useAgentRun.d.ts.map +0 -1
  308. package/dist/react/useAgentRun.js +0 -125
  309. package/dist/react/useAgentRun.js.map +0 -1
  310. package/dist/registry.d.ts +0 -45
  311. package/dist/registry.d.ts.map +0 -1
  312. package/dist/registry.js +0 -131
  313. package/dist/registry.js.map +0 -1
  314. package/dist/rerank.d.ts +0 -20
  315. package/dist/rerank.d.ts.map +0 -1
  316. package/dist/rerank.js +0 -40
  317. package/dist/rerank.js.map +0 -1
  318. package/dist/resume-approval.d.ts +0 -30
  319. package/dist/resume-approval.d.ts.map +0 -1
  320. package/dist/resume-approval.js +0 -147
  321. package/dist/resume-approval.js.map +0 -1
  322. package/dist/sanitize-conversation.d.ts +0 -43
  323. package/dist/sanitize-conversation.d.ts.map +0 -1
  324. package/dist/sanitize-conversation.js +0 -85
  325. package/dist/sanitize-conversation.js.map +0 -1
  326. package/dist/scoped-tool.d.ts +0 -98
  327. package/dist/scoped-tool.d.ts.map +0 -1
  328. package/dist/scoped-tool.js +0 -174
  329. package/dist/scoped-tool.js.map +0 -1
  330. package/dist/server/provider.d.ts +0 -22
  331. package/dist/server/provider.d.ts.map +0 -1
  332. package/dist/server/provider.js +0 -194
  333. package/dist/server/provider.js.map +0 -1
  334. package/dist/similarity-search.d.ts +0 -163
  335. package/dist/similarity-search.d.ts.map +0 -1
  336. package/dist/similarity-search.js +0 -147
  337. package/dist/similarity-search.js.map +0 -1
  338. package/dist/sub-agent-run-store.d.ts +0 -157
  339. package/dist/sub-agent-run-store.d.ts.map +0 -1
  340. package/dist/sub-agent-run-store.js +0 -87
  341. package/dist/sub-agent-run-store.js.map +0 -1
  342. package/dist/tool-execution.d.ts +0 -16
  343. package/dist/tool-execution.d.ts.map +0 -1
  344. package/dist/tool-execution.js +0 -498
  345. package/dist/tool-execution.js.map +0 -1
  346. package/dist/tool-helpers.d.ts +0 -77
  347. package/dist/tool-helpers.d.ts.map +0 -1
  348. package/dist/tool-helpers.js +0 -117
  349. package/dist/tool-helpers.js.map +0 -1
  350. package/dist/tool.d.ts +0 -216
  351. package/dist/tool.d.ts.map +0 -1
  352. package/dist/tool.js +0 -175
  353. package/dist/tool.js.map +0 -1
  354. package/dist/transcription.d.ts +0 -42
  355. package/dist/transcription.d.ts.map +0 -1
  356. package/dist/transcription.js +0 -77
  357. package/dist/transcription.js.map +0 -1
  358. package/dist/types.d.ts +0 -1020
  359. package/dist/types.d.ts.map +0 -1
  360. package/dist/types.js +0 -2
  361. package/dist/types.js.map +0 -1
  362. package/dist/util/hash.d.ts +0 -11
  363. package/dist/util/hash.d.ts.map +0 -1
  364. package/dist/util/hash.js +0 -23
  365. package/dist/util/hash.js.map +0 -1
  366. package/dist/vector-stores/index.d.ts +0 -96
  367. package/dist/vector-stores/index.d.ts.map +0 -1
  368. package/dist/vector-stores/index.js +0 -153
  369. package/dist/vector-stores/index.js.map +0 -1
  370. package/dist/vercel-protocol.d.ts +0 -18
  371. package/dist/vercel-protocol.d.ts.map +0 -1
  372. package/dist/vercel-protocol.js +0 -75
  373. package/dist/vercel-protocol.js.map +0 -1
  374. package/dist/zod-to-json-schema.d.ts +0 -16
  375. package/dist/zod-to-json-schema.d.ts.map +0 -1
  376. package/dist/zod-to-json-schema.js +0 -17
  377. package/dist/zod-to-json-schema.js.map +0 -1
package/dist/agent.js DELETED
@@ -1,1538 +0,0 @@
1
- import { z } from 'zod';
2
- import { AiRegistry } from './registry.js';
3
- import { pauseForApproval, pauseForClientTools, toolDefinition, toolToSchema } from './tool.js';
4
- import { attachmentsToContentParts, getMessageText } from './attachment.js';
5
- import { QueuedPromptBuilder } from './queue-job.js';
6
- import { resolveAutoPersistSpec, runWithPersistence, runWithPersistenceStreaming, } from './conversation-persistence.js';
7
- import { resolveRemembersSpec } from './memory.js';
8
- import { withMemoryInject } from './memory-inject.js';
9
- import { withMemoryExtract } from './memory-extract.js';
10
- import { runOnConfig, runOnChunk, runSequential, runOnUsage, runOnAbort, runOnError, } from './middleware.js';
11
- import { executeToolPhase } from './tool-execution.js';
12
- import { resumePendingToolCalls } from './resume-approval.js';
13
- import { buildHandoffChildOptions, driveHandoffs, MAX_HANDOFFS, mergeFinalHandoff, stripInternal, } from './handoffs-driver.js';
14
- // ─── AI Observer (lazy accessor) ─────────────────────────
15
- function _getAiObservers() {
16
- return globalThis['__rudderjs_ai_observers__'] ?? null;
17
- }
18
- function _buildObserverSteps(steps, modelString) {
19
- return steps.map((step, i) => ({
20
- iteration: i + 1,
21
- model: modelString,
22
- tokens: { prompt: step.usage.promptTokens, completion: step.usage.completionTokens, total: step.usage.totalTokens },
23
- finishReason: step.finishReason,
24
- toolCalls: step.toolCalls.map(tc => {
25
- const tr = step.toolResults.find(r => r.toolCallId === tc.id);
26
- return {
27
- id: tc.id,
28
- name: tc.name,
29
- args: tc.arguments,
30
- result: tr?.result,
31
- duration: tr?.duration ?? 0,
32
- needsApproval: false,
33
- };
34
- }),
35
- }));
36
- }
37
- // ─── Stop Condition Combinators ──────────────────────────
38
- /** Stop after N steps */
39
- export function stepCountIs(n) {
40
- return ({ steps }) => steps.length >= n;
41
- }
42
- /** Stop when a specific tool is called in the latest step */
43
- export function hasToolCall(toolName) {
44
- return ({ steps }) => {
45
- const last = steps[steps.length - 1];
46
- return last?.toolCalls.some(tc => tc.name === toolName) ?? false;
47
- };
48
- }
49
- // ─── Agent Base Class ────────────────────────────────────
50
- export class Agent {
51
- /** Model string (e.g. 'anthropic/claude-sonnet-4-5'). Defaults to registry default. */
52
- model() { return undefined; }
53
- /** Failover provider/model strings */
54
- failover() { return []; }
55
- /** Maximum iterations for the tool loop (default: 20) */
56
- maxSteps() { return 20; }
57
- /** Stop conditions — combine with array (OR logic) */
58
- stopWhen() {
59
- return stepCountIs(this.maxSteps());
60
- }
61
- /** Temperature (0-1) */
62
- temperature() { return undefined; }
63
- /** Max tokens for response */
64
- maxTokens() { return undefined; }
65
- /**
66
- * Declarative prompt-cache configuration.
67
- *
68
- * Override on a subclass to mark stable parts of the prompt as cacheable
69
- * — provider adapters translate to native primitives (Anthropic
70
- * `cache_control`, OpenAI `prompt_cache_key`, Google `cachedContent`)
71
- * so cache hits can save 50–90% on input tokens for long system prompts,
72
- * tool definitions, or stable conversation context.
73
- *
74
- * Returning `undefined` (the default) means no caching. Per-call override
75
- * via `agent.prompt(input, { cache: false })` disables caching for that
76
- * call; passing a {@link CacheableConfig} for `cache` replaces the agent
77
- * default for that call.
78
- *
79
- * @example
80
- * class SupportAgent extends Agent {
81
- * instructions() { return LONG_SYSTEM_PROMPT }
82
- * tools() { return [tool1, tool2, tool3] }
83
- * cacheable() {
84
- * return { instructions: true, tools: true }
85
- * }
86
- * }
87
- */
88
- cacheable() { return undefined; }
89
- /**
90
- * Opt into auto-persisted conversation behavior. Override on a subclass
91
- * to declare *which* user owns the thread and (optionally) which
92
- * specific thread, and the framework will load history before each
93
- * `prompt()`/`stream()` call and append the new turn after it — without
94
- * any caller having to remember `forUser()` / `continue()`.
95
- *
96
- * Returning `false` (the default) disables auto-persist; the agent runs
97
- * stateless. Returning a {@link ConversationalSpec} opts in:
98
- *
99
- * @example
100
- * class ChatAgent extends Agent {
101
- * conversational() {
102
- * return { user: Auth.user()?.id } // null user → falsy → opt-out
103
- * }
104
- * }
105
- *
106
- * await new ChatAgent().prompt('Hi') // auto-loads + auto-saves
107
- *
108
- * **Precedence (high → low):**
109
- * 1. Explicit `agent.forUser(id).prompt()` / `agent.continue(id).prompt()`
110
- * 2. Per-call `prompt(input, { conversation: false | {...} })`
111
- * 3. This method's return value
112
- *
113
- * Async returns are supported — useful when the user identity is fetched
114
- * from an async DI binding.
115
- */
116
- conversational() {
117
- return false;
118
- }
119
- /**
120
- * Opt this agent class into per-user memory beyond conversation history
121
- * (#A4). Returns a {@link RemembersSpec} naming the user whose memory
122
- * the agent reads/writes, and how injection / extraction should behave.
123
- * Returning `false` (the default) leaves the agent memory-stateless.
124
- *
125
- * Phase 1 wires the declaration + the per-call precedence chain so
126
- * apps and downstream phases (auto-inject middleware in Phase 2,
127
- * auto-extract middleware in Phase 3) can read a consistent spec.
128
- * Calling this method directly today produces no runtime behavior
129
- * unless application code reads it via `resolveRemembersSpec()`.
130
- *
131
- * **Precedence (high → low):**
132
- * 1. Per-call `prompt(input, { memory: false | {...} })`
133
- * 2. This method's return value
134
- *
135
- * Async returns are supported — useful when the user identity is fetched
136
- * from an async DI binding.
137
- *
138
- * @example
139
- * class SupportAgent extends Agent {
140
- * remembers() { return { user: ctx.user.id, inject: 'auto', tags: ['support'] } }
141
- * }
142
- */
143
- remembers() {
144
- return false;
145
- }
146
- /**
147
- * Default for `AgentPromptOptions.parallelTools`. When `true` (default),
148
- * multiple tool calls within a single step run their `execute()` functions
149
- * concurrently. Override on a subclass to flip the default for an agent
150
- * whose tools share non-idempotent state. Per-call options still win.
151
- */
152
- parallelTools() { return true; }
153
- /** Run the agent with a prompt (non-streaming) */
154
- async prompt(input, options) {
155
- // Memory auto-cascade — appends inject (Phase 2) + extract (Phase 3)
156
- // middlewares when `Agent.remembers()` opts in. Runs BEFORE
157
- // conversation persistence so the persisted history flows in
158
- // unchanged: inject only grows the system message; extract only
159
- // fires onFinish.
160
- const effOptions = await prepareOptionsWithMemoryAutoCascade(this, options);
161
- const spec = await resolveAutoPersistSpec(() => this.conversational(), effOptions?.conversation);
162
- if (spec) {
163
- return runWithPersistence(spec, this.constructor.name, resolveConversationStore, input, effOptions, (innerOptions) => runAgentLoop(this, input, innerOptions));
164
- }
165
- return runAgentLoop(this, input, effOptions);
166
- }
167
- /** Run the agent with a prompt (streaming) */
168
- stream(input, options) {
169
- return runStreamWithMaybeAutoPersist(this, input, options);
170
- }
171
- /** Queue the prompt for background execution */
172
- queue(input, options) {
173
- return new QueuedPromptBuilder(this, input, options);
174
- }
175
- /** Set the user scope for conversation persistence */
176
- forUser(userId) {
177
- return new ConversableAgent(this).forUser(userId);
178
- }
179
- /** Continue an existing conversation */
180
- continue(conversationId) {
181
- return new ConversableAgent(this).continue(conversationId);
182
- }
183
- asTool(options) {
184
- if (options.suspendable && !options.streaming) {
185
- throw new Error('[Rudder AI] asTool: `suspendable` requires `streaming: true` (or a projector). Silent suspend would leave the parent UI with no progress signal between sub-agent invocations.');
186
- }
187
- const schema = options.inputSchema ?? z.object({ prompt: z.string() });
188
- const promptOf = options.prompt ?? ((input) => input.prompt);
189
- const modelOutput = options.modelOutput ?? ((response) => response.text);
190
- if (!options.streaming) {
191
- // 1.2.0 zero-config path — single prompt() call, single AgentResponse out.
192
- return toolDefinition({
193
- name: options.name,
194
- description: options.description,
195
- inputSchema: schema,
196
- })
197
- .server((input) => this.prompt(promptOf(input)))
198
- .modelOutput(modelOutput);
199
- }
200
- const project = options.streaming === true ? defaultSubAgentProjector : options.streaming;
201
- const innerAgent = this; // eslint-disable-line @typescript-eslint/no-this-alias
202
- const agentName = options.name;
203
- const suspendable = options.suspendable;
204
- const generatorExecute = async function* (input) {
205
- const userPrompt = promptOf(input);
206
- yield { kind: 'agent_start', agentName };
207
- const streamOpts = suspendable
208
- ? { toolCallStreamingMode: 'stop-on-client-tool' }
209
- : undefined;
210
- const { stream, response } = innerAgent.stream(userPrompt, streamOpts);
211
- for await (const chunk of stream) {
212
- const update = project(chunk);
213
- if (update)
214
- yield update;
215
- }
216
- const result = await response;
217
- if (suspendable &&
218
- result.finishReason === 'client_tool_calls' &&
219
- result.pendingClientToolCalls?.length) {
220
- const subRunId = generateSubRunId();
221
- const snapshot = {
222
- messages: buildSubAgentSnapshotMessages(userPrompt, result),
223
- pendingToolCallIds: result.pendingClientToolCalls.map((tc) => tc.id),
224
- stepsSoFar: result.steps.length,
225
- tokensSoFar: result.usage?.totalTokens ?? 0,
226
- pauseKind: 'client_tool',
227
- };
228
- await suspendable.runStore.store(subRunId, snapshot);
229
- yield { kind: 'subagent_paused', subRunId, pendingToolCallIds: snapshot.pendingToolCallIds };
230
- yield pauseForClientTools(result.pendingClientToolCalls, subRunId);
231
- // Unreachable — the parent loop halts iteration after the pause chunk.
232
- return undefined;
233
- }
234
- if (suspendable &&
235
- result.finishReason === 'tool_approval_required' &&
236
- result.pendingApprovalToolCall) {
237
- const subRunId = generateSubRunId();
238
- const { toolCall: pendingCall, isClientTool } = result.pendingApprovalToolCall;
239
- const snapshot = {
240
- messages: buildSubAgentSnapshotMessages(userPrompt, result),
241
- pendingToolCallIds: [pendingCall.id],
242
- stepsSoFar: result.steps.length,
243
- tokensSoFar: result.usage?.totalTokens ?? 0,
244
- pauseKind: 'approval',
245
- pendingApprovalToolCall: { toolCall: pendingCall, isClientTool },
246
- };
247
- await suspendable.runStore.store(subRunId, snapshot);
248
- yield {
249
- kind: 'subagent_paused_approval',
250
- subRunId,
251
- toolCall: pendingCall,
252
- isClientTool,
253
- };
254
- yield pauseForApproval(pendingCall, isClientTool, subRunId);
255
- // Unreachable — the parent loop halts iteration after the pause chunk.
256
- return undefined;
257
- }
258
- yield {
259
- kind: 'agent_done',
260
- steps: result.steps.length,
261
- tokens: result.usage?.totalTokens ?? 0,
262
- };
263
- return result;
264
- };
265
- return toolDefinition({
266
- name: options.name,
267
- description: options.description,
268
- inputSchema: schema,
269
- })
270
- .server(generatorExecute)
271
- .modelOutput(modelOutput);
272
- }
273
- /**
274
- * Resume a sub-agent run that previously paused with either
275
- * `pauseForClientTools` (client-tool pause) or `pauseForApproval`
276
- * (approval pause), typically from {@link Agent.asTool} with
277
- * `suspendable: { runStore }` set. The snapshot's `pauseKind`
278
- * (default `'client_tool'`) selects the resume contract:
279
- *
280
- * - **`client_tool`** — `clientToolResults` must carry one entry per
281
- * id in the snapshot's `pendingToolCallIds`. Results are appended
282
- * to the inner-agent message history and the loop re-runs.
283
- * - **`approval`** — `approvedToolCallIds` and/or
284
- * `rejectedToolCallIds` must reference the single pending id.
285
- * `clientToolResults` must be empty; the loop re-runs with the
286
- * approval decision injected via `AgentPromptOptions`.
287
- *
288
- * Returns either a `'completed'` result (the inner agent finished),
289
- * a `'paused'` continuation pointing at a fresh `subRunId` for the
290
- * next round-trip, or stays `'paused'` if the inner loop hits another
291
- * gate. The resume can pause on a different kind than it started on
292
- * (e.g. an approval pause that, once approved, hits a client-tool
293
- * pause on the next step).
294
- *
295
- * @example Client-tool resume
296
- * const r = await Agent.resumeAsTool(subRunId, browserResults, { runStore, agent: subAgent })
297
- *
298
- * @example Approval resume
299
- * const r = await Agent.resumeAsTool(subRunId, [], {
300
- * runStore, agent: subAgent,
301
- * approvedToolCallIds: ['inner-call-id'],
302
- * })
303
- */
304
- static async resumeAsTool(subRunId, clientToolResults, options) {
305
- const snapshot = await options.runStore.consume(subRunId);
306
- if (!snapshot) {
307
- throw new Error(`[Rudder AI] resumeAsTool: subRunId "${subRunId}" expired or never existed.`);
308
- }
309
- const pauseKind = snapshot.pauseKind ?? 'client_tool';
310
- const pending = new Set(snapshot.pendingToolCallIds);
311
- let messages;
312
- const promptOpts = { toolCallStreamingMode: 'stop-on-client-tool' };
313
- if (pauseKind === 'client_tool') {
314
- // Forgery guard — every incoming tool-result id must be in the pending set.
315
- const seen = new Set();
316
- for (const r of clientToolResults) {
317
- if (!pending.has(r.toolCallId)) {
318
- throw new Error(`[Rudder AI] resumeAsTool: toolCallId "${r.toolCallId}" was not in the pending set.`);
319
- }
320
- if (seen.has(r.toolCallId)) {
321
- throw new Error(`[Rudder AI] resumeAsTool: duplicate result for toolCallId "${r.toolCallId}".`);
322
- }
323
- seen.add(r.toolCallId);
324
- }
325
- // Append client tool-result messages to the snapshot, in incoming order.
326
- messages = [...snapshot.messages];
327
- for (const r of clientToolResults) {
328
- messages.push({
329
- role: 'tool',
330
- content: typeof r.result === 'string' ? r.result : JSON.stringify(r.result),
331
- toolCallId: r.toolCallId,
332
- });
333
- }
334
- }
335
- else {
336
- // Approval-pause resume — clientToolResults must be empty; either an
337
- // approval or a rejection must be supplied for the pending id.
338
- if (clientToolResults.length > 0) {
339
- throw new Error('[Rudder AI] resumeAsTool: snapshot.pauseKind === "approval" but clientToolResults was non-empty. Pass `approvedToolCallIds` or `rejectedToolCallIds` instead.');
340
- }
341
- const approved = options.approvedToolCallIds ?? [];
342
- const rejected = options.rejectedToolCallIds ?? [];
343
- for (const id of approved) {
344
- if (!pending.has(id)) {
345
- throw new Error(`[Rudder AI] resumeAsTool: approvedToolCallId "${id}" was not in the pending set.`);
346
- }
347
- }
348
- for (const id of rejected) {
349
- if (!pending.has(id)) {
350
- throw new Error(`[Rudder AI] resumeAsTool: rejectedToolCallId "${id}" was not in the pending set.`);
351
- }
352
- }
353
- if (approved.length === 0 && rejected.length === 0) {
354
- throw new Error('[Rudder AI] resumeAsTool: snapshot.pauseKind === "approval" requires `approvedToolCallIds` or `rejectedToolCallIds`.');
355
- }
356
- messages = [...snapshot.messages];
357
- if (approved.length > 0)
358
- promptOpts.approvedToolCallIds = approved;
359
- if (rejected.length > 0)
360
- promptOpts.rejectedToolCallIds = rejected;
361
- }
362
- promptOpts.messages = messages;
363
- // Default path: non-streaming resume — one prompt() call, one response.
364
- // Opt-in streaming path: run the inner loop via stream() and forward each
365
- // projected chunk to onUpdate, so a host can keep a resumed sub-agent's
366
- // progress live across the round-trip. Either way `result` is the same
367
- // AgentResponse and the pause/completion partition below is unchanged.
368
- let result;
369
- if (options.streaming) {
370
- const project = options.streaming === true ? defaultSubAgentProjector : options.streaming;
371
- const { stream, response } = options.agent.stream('', promptOpts);
372
- const ctx = { originalSubRunId: subRunId, ...(options.key !== undefined ? { key: options.key } : {}) };
373
- for await (const chunk of stream) {
374
- const update = project(chunk, ctx);
375
- if (update && options.onUpdate)
376
- await options.onUpdate(update);
377
- }
378
- result = await response;
379
- }
380
- else {
381
- result = await options.agent.prompt('', promptOpts);
382
- }
383
- if (result.finishReason === 'client_tool_calls' &&
384
- result.pendingClientToolCalls?.length) {
385
- const newSubRunId = generateSubRunId();
386
- const newSnapshot = {
387
- messages: buildResumeSnapshotMessages(messages, result),
388
- pendingToolCallIds: result.pendingClientToolCalls.map((tc) => tc.id),
389
- stepsSoFar: snapshot.stepsSoFar + result.steps.length,
390
- tokensSoFar: snapshot.tokensSoFar + (result.usage?.totalTokens ?? 0),
391
- pauseKind: 'client_tool',
392
- ...(snapshot.meta !== undefined ? { meta: snapshot.meta } : {}),
393
- };
394
- await options.runStore.store(newSubRunId, newSnapshot);
395
- return {
396
- kind: 'paused',
397
- subRunId: newSubRunId,
398
- pauseKind: 'client_tool',
399
- pendingToolCallIds: newSnapshot.pendingToolCallIds,
400
- };
401
- }
402
- if (result.finishReason === 'tool_approval_required' &&
403
- result.pendingApprovalToolCall) {
404
- const newSubRunId = generateSubRunId();
405
- const { toolCall: pendingCall, isClientTool } = result.pendingApprovalToolCall;
406
- const newSnapshot = {
407
- messages: buildResumeSnapshotMessages(messages, result),
408
- pendingToolCallIds: [pendingCall.id],
409
- stepsSoFar: snapshot.stepsSoFar + result.steps.length,
410
- tokensSoFar: snapshot.tokensSoFar + (result.usage?.totalTokens ?? 0),
411
- pauseKind: 'approval',
412
- pendingApprovalToolCall: { toolCall: pendingCall, isClientTool },
413
- ...(snapshot.meta !== undefined ? { meta: snapshot.meta } : {}),
414
- };
415
- await options.runStore.store(newSubRunId, newSnapshot);
416
- return {
417
- kind: 'paused',
418
- subRunId: newSubRunId,
419
- pauseKind: 'approval',
420
- pendingToolCallIds: newSnapshot.pendingToolCallIds,
421
- toolCall: pendingCall,
422
- isClientTool,
423
- };
424
- }
425
- return { kind: 'completed', response: result };
426
- }
427
- /**
428
- * Resume MANY paused sub-agents in one call and aggregate their pending
429
- * tool calls into a single client round-trip.
430
- *
431
- * When an orchestrator dispatches several sub-agents in one parent turn
432
- * and more than one pauses on a client tool (or approval gate), the host
433
- * would otherwise loop over {@link Agent.resumeAsTool} by hand and stitch
434
- * the pending sets back together. This does that: each request resumes its
435
- * own `(subRunId, agent)` snapshot, and the result carries the combined
436
- * `completed` / `paused` / `errors` partition plus the flattened
437
- * `pendingToolCallIds` the host collects the next batch of results for.
438
- *
439
- * Re-entrant: feed the next round of `clientToolResults` / approvals back
440
- * in as a fresh batch keyed off each paused item's NEW `subRunId` until
441
- * `allCompleted` is `true`.
442
- *
443
- * @example
444
- * let batch = await Agent.resumeManyAsTool(
445
- * paused.map(p => ({ subRunId: p.subRunId, agent: p.agent, clientToolResults: results[p.subRunId] })),
446
- * { runStore },
447
- * )
448
- * // batch.pendingToolCallIds → gather the next round from the browser, repeat.
449
- */
450
- static async resumeManyAsTool(requests, options) {
451
- const onError = options.onError ?? 'capture';
452
- const concurrency = options.concurrency ?? 'parallel';
453
- const runOne = async (req) => {
454
- const base = { originalSubRunId: req.subRunId, ...(req.key !== undefined ? { key: req.key } : {}) };
455
- try {
456
- const opts = { runStore: options.runStore, agent: req.agent };
457
- if (req.approvedToolCallIds)
458
- opts.approvedToolCallIds = req.approvedToolCallIds;
459
- if (req.rejectedToolCallIds)
460
- opts.rejectedToolCallIds = req.rejectedToolCallIds;
461
- // Thread the shared projector + correlate each update back to this item.
462
- if (options.streaming !== undefined)
463
- opts.streaming = options.streaming;
464
- if (req.key !== undefined)
465
- opts.key = req.key;
466
- if (options.onUpdate) {
467
- const batchOnUpdate = options.onUpdate;
468
- opts.onUpdate = (update) => batchOnUpdate(update, base);
469
- }
470
- const r = await Agent.resumeAsTool(req.subRunId, req.clientToolResults ?? [], opts);
471
- if (r.kind === 'completed')
472
- return { ...base, kind: 'completed', response: r.response };
473
- return {
474
- ...base,
475
- kind: 'paused',
476
- subRunId: r.subRunId,
477
- pauseKind: r.pauseKind,
478
- pendingToolCallIds: r.pendingToolCallIds,
479
- ...(r.toolCall !== undefined ? { toolCall: r.toolCall } : {}),
480
- ...(r.isClientTool !== undefined ? { isClientTool: r.isClientTool } : {}),
481
- };
482
- }
483
- catch (err) {
484
- if (onError === 'throw')
485
- throw err;
486
- return { ...base, kind: 'error', error: err instanceof Error ? err : new Error(String(err)) };
487
- }
488
- };
489
- let results;
490
- if (concurrency === 'serial') {
491
- results = [];
492
- for (const req of requests)
493
- results.push(await runOne(req));
494
- }
495
- else {
496
- results = await Promise.all(requests.map(runOne));
497
- }
498
- const paused = results.filter((o) => o.kind === 'paused');
499
- const completed = results.filter((o) => o.kind === 'completed');
500
- const errors = results.filter((o) => o.kind === 'error');
501
- return {
502
- results,
503
- paused,
504
- completed,
505
- errors,
506
- pendingToolCallIds: paused.flatMap((p) => p.pendingToolCallIds),
507
- allCompleted: paused.length === 0 && errors.length === 0,
508
- };
509
- }
510
- }
511
- /**
512
- * Default projection from inner-agent stream chunks to {@link SubAgentUpdate}
513
- * events. Emits one `tool_call` per inner `tool-call` chunk and
514
- * `agent_pending_approval` per inner `pending-approval` chunk; everything
515
- * else is suppressed (the wrapping execute emits the `agent_start` /
516
- * `agent_done` bookends and the suspend paths emit `subagent_paused` /
517
- * `subagent_paused_approval`).
518
- *
519
- * Hosts wanting different cadence (e.g. surfacing `text-delta` previews
520
- * or per-step usage) pass `streaming: chunk => …` and own the discriminator.
521
- */
522
- function defaultSubAgentProjector(chunk) {
523
- if (chunk.type === 'tool-call' && chunk.toolCall?.name) {
524
- return {
525
- kind: 'tool_call',
526
- tool: chunk.toolCall.name,
527
- ...(chunk.toolCall.arguments ? { args: chunk.toolCall.arguments } : {}),
528
- };
529
- }
530
- if (chunk.type === 'pending-approval' && chunk.toolCall && chunk.toolCall.id && chunk.toolCall.name) {
531
- return {
532
- kind: 'agent_pending_approval',
533
- toolCall: chunk.toolCall,
534
- isClientTool: !!chunk.isClientTool,
535
- };
536
- }
537
- return null;
538
- }
539
- /**
540
- * Reconstruct the inner-agent message history at the point the loop
541
- * paused, so a subsequent {@link Agent.resumeAsTool} can rerun the loop
542
- * with the appended client tool results. The shape is `[user, …(message
543
- * + serverToolResults)*]` — system messages are omitted because the
544
- * `messages` mode of the agent loop prepends `system` itself.
545
- *
546
- * Each step's `message` includes ALL `toolCalls` (server + client).
547
- * Server-side `toolResults` are interleaved; client-side calls remain
548
- * unfulfilled until resume appends their results.
549
- */
550
- function buildSubAgentSnapshotMessages(userPrompt, response) {
551
- const out = [{ role: 'user', content: userPrompt }];
552
- for (const step of response.steps) {
553
- out.push(step.message);
554
- for (const tr of step.toolResults) {
555
- const resultStr = typeof tr.result === 'string' ? tr.result : JSON.stringify(tr.result);
556
- out.push({ role: 'tool', content: resultStr, toolCallId: tr.toolCallId });
557
- }
558
- }
559
- return out;
560
- }
561
- /**
562
- * Snapshot reconstruction for a resume-time pause. The `priorMessages`
563
- * already include the original user prompt + every step prior to the
564
- * resume call. Append the freshly-completed steps' messages and any
565
- * server-side tool results so the next resume sees the full history.
566
- */
567
- function buildResumeSnapshotMessages(priorMessages, response) {
568
- const out = [...priorMessages];
569
- for (const step of response.steps) {
570
- out.push(step.message);
571
- for (const tr of step.toolResults) {
572
- const resultStr = typeof tr.result === 'string' ? tr.result : JSON.stringify(tr.result);
573
- out.push({ role: 'tool', content: resultStr, toolCallId: tr.toolCallId });
574
- }
575
- }
576
- return out;
577
- }
578
- function generateSubRunId() {
579
- if (typeof globalThis.crypto?.randomUUID === 'function')
580
- return globalThis.crypto.randomUUID();
581
- return `sub-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
582
- }
583
- // ─── Conversable Agent (conversation persistence) ───────
584
- /**
585
- * Wraps an Agent to add conversation memory.
586
- * Created via `agent.forUser(id)` or `agent.continue(id)`.
587
- */
588
- export class ConversableAgent {
589
- agent;
590
- _userId;
591
- _conversationId;
592
- constructor(agent) {
593
- this.agent = agent;
594
- }
595
- forUser(userId) {
596
- this._userId = userId;
597
- return this;
598
- }
599
- continue(conversationId) {
600
- this._conversationId = conversationId;
601
- return this;
602
- }
603
- async prompt(input, options) {
604
- const spec = this.toSpec();
605
- return runWithPersistence(spec, this.agent.constructor.name, resolveConversationStore, input, options, (effOptions) => runAgentLoop(this.agent, input, effOptions)).then((r) => {
606
- // Track the resolved id back on the wrapper so a subsequent
607
- // `wrapper.prompt()` call resumes the same thread.
608
- if (r.conversationId)
609
- this._conversationId = r.conversationId;
610
- return r;
611
- });
612
- }
613
- stream(input, options) {
614
- const spec = this.toSpec();
615
- const persisted = runWithPersistenceStreaming(spec, this.agent.constructor.name, resolveConversationStore, input, options, (effOptions) => runAgentLoopStreaming(this.agent, input, effOptions));
616
- // Update the wrapper's id once the run completes.
617
- persisted.response.then((r) => { if (r.conversationId)
618
- this._conversationId = r.conversationId; }, () => { });
619
- return persisted;
620
- }
621
- /**
622
- * Translate the wrapper's explicit-form state (`forUser` / `continue`)
623
- * into a {@link ConversationalSpec}. The explicit chain bypasses the
624
- * agent's `conversational()` declaration entirely — `forUser` always
625
- * wins over class defaults.
626
- */
627
- toSpec() {
628
- if (this._conversationId)
629
- return { user: this._userId ?? '', id: this._conversationId };
630
- if (this._userId)
631
- return { user: this._userId };
632
- throw new Error('[Rudder AI] ConversableAgent requires forUser() or continue() to be called before prompt().');
633
- }
634
- }
635
- // ─── Anonymous Agent ─────────────────────────────────────
636
- class AnonymousAgent extends Agent {
637
- _instructions;
638
- _tools;
639
- _model;
640
- _middleware;
641
- constructor(options) {
642
- super();
643
- this._instructions = options.instructions;
644
- this._tools = options.tools ?? [];
645
- this._model = options.model;
646
- this._middleware = options.middleware ?? [];
647
- }
648
- instructions() { return this._instructions; }
649
- model() { return this._model; }
650
- tools() { return this._tools; }
651
- middleware() { return this._middleware; }
652
- }
653
- /**
654
- * Create an anonymous agent inline.
655
- *
656
- * @example
657
- * const response = await agent('You are helpful.').prompt('Hello')
658
- *
659
- * @example
660
- * const response = await agent({
661
- * instructions: 'You are a search assistant.',
662
- * tools: [searchTool],
663
- * model: 'anthropic/claude-sonnet-4-5',
664
- * }).prompt('Find users named John')
665
- */
666
- export function agent(instructionsOrOptions) {
667
- const options = typeof instructionsOrOptions === 'string'
668
- ? { instructions: instructionsOrOptions }
669
- : instructionsOrOptions;
670
- return new AnonymousAgent(options);
671
- }
672
- // ─── Helpers ─────────────────────────────────────────────
673
- // ─── Conversation Store Registry ────────────────────────
674
- let _conversationStore;
675
- /** Set the global conversation store (called by service provider or manually) */
676
- export function setConversationStore(store) {
677
- _conversationStore = store;
678
- }
679
- function resolveConversationStore() {
680
- return _conversationStore;
681
- }
682
- // ─── User Memory Registry (#A4) ──────────────────────────
683
- let _userMemory;
684
- /**
685
- * Set the global {@link UserMemory} (called by `AiProvider` from
686
- * `AiConfig.memory`, or manually for tests / standalone setups).
687
- * Phase 2/3 middleware reads it via `resolveUserMemory()` —
688
- * imported by the persistence layer the same way
689
- * `resolveConversationStore` is wired today.
690
- */
691
- export function setUserMemory(memory) {
692
- _userMemory = memory;
693
- }
694
- export function resolveUserMemory() {
695
- return _userMemory;
696
- }
697
- /**
698
- * Streaming counterpart of `Agent.prompt`'s auto-persist branch. The spec
699
- * resolution is async (since `conversational()` may return a Promise), so
700
- * we defer the decision into the outer wrapper that handles the inner
701
- * stream's setup the same way `runWithPersistenceStreaming` does for the
702
- * persisted path.
703
- */
704
- function runStreamWithMaybeAutoPersist(a, input, options) {
705
- // Synchronous fast path — most agents override neither `conversational()`
706
- // nor `remembers()`. Skip the async outer entirely when we can prove
707
- // both are no-ops, sparing a microtask boundary per streaming call.
708
- const declaredConv = a.conversational();
709
- const declaredMem = a.remembers();
710
- const isFast = ((options?.conversation === false ||
711
- (declaredConv === false && options?.conversation === undefined))
712
- && (options?.memory === false ||
713
- (declaredMem === false && options?.memory === undefined) ||
714
- options?.messages !== undefined));
715
- if (isFast) {
716
- return runAgentLoopStreaming(a, input, options);
717
- }
718
- // Async path — resolve memory + conversation specs, then dispatch.
719
- let resolveResp;
720
- let rejectResp;
721
- const responsePromise = new Promise((res, rej) => { resolveResp = res; rejectResp = rej; });
722
- async function* outer() {
723
- let effOptions;
724
- let spec;
725
- try {
726
- // Memory auto-cascade BEFORE conversation persistence — same
727
- // ordering as the non-streaming `Agent.prompt` path.
728
- effOptions = await prepareOptionsWithMemoryAutoCascade(a, options);
729
- spec = await resolveAutoPersistSpec(() => a.conversational(), effOptions?.conversation);
730
- }
731
- catch (err) {
732
- rejectResp(err);
733
- throw err;
734
- }
735
- if (!spec) {
736
- const inner = runAgentLoopStreaming(a, input, effOptions);
737
- try {
738
- for await (const chunk of inner.stream)
739
- yield chunk;
740
- }
741
- catch (err) {
742
- rejectResp(err);
743
- throw err;
744
- }
745
- try {
746
- const r = await inner.response;
747
- resolveResp(r);
748
- }
749
- catch (err) {
750
- rejectResp(err);
751
- throw err;
752
- }
753
- return;
754
- }
755
- const persisted = runWithPersistenceStreaming(spec, a.constructor.name, resolveConversationStore, input, effOptions, (innerOptions) => runAgentLoopStreaming(a, input, innerOptions));
756
- try {
757
- for await (const chunk of persisted.stream)
758
- yield chunk;
759
- }
760
- catch (err) {
761
- rejectResp(err);
762
- throw err;
763
- }
764
- try {
765
- const r = await persisted.response;
766
- resolveResp(r);
767
- }
768
- catch (err) {
769
- rejectResp(err);
770
- throw err;
771
- }
772
- }
773
- return { stream: outer(), response: responsePromise };
774
- }
775
- // ─── Helpers ─────────────────────────────────────────────
776
- function hasToolsMethod(a) {
777
- return 'tools' in a && typeof a.tools === 'function';
778
- }
779
- function getTools(a) {
780
- return hasToolsMethod(a) ? a.tools() : [];
781
- }
782
- /**
783
- * Internal symbol used to plumb auto-installed middlewares (today:
784
- * memory-inject; future: budget-tracker, etc.) through the public
785
- * `AgentPromptOptions` without polluting its surface. Resolution
786
- * happens at the `Agent.prompt` / `Agent.stream` boundary; the loop
787
- * just appends them to the user's `agent.middleware()` array.
788
- */
789
- const EXTRA_MIDDLEWARES = Symbol.for('rudderjs.ai.extraMiddlewares');
790
- function hasMiddlewareMethod(a) {
791
- return 'middleware' in a && typeof a.middleware === 'function';
792
- }
793
- function getMiddleware(a, options) {
794
- const own = hasMiddlewareMethod(a) ? a.middleware() : [];
795
- const extras = options?.[EXTRA_MIDDLEWARES] ?? [];
796
- return extras.length > 0 ? [...own, ...extras] : own;
797
- }
798
- /**
799
- * Resolve the effective `remembers()` spec and append the appropriate
800
- * memory middlewares (inject for Phase 2, extract for Phase 3) to the
801
- * options' hidden extras list. Skips entirely on:
802
- * - continuation calls (`options.messages` set) — the system message
803
- * was already augmented on the original `prompt()`, re-injecting
804
- * would duplicate the block on every tool round-trip; re-extracting
805
- * would also double-write the same facts on every round-trip.
806
- * - specs where neither `inject === 'auto'` nor `extract === 'auto'`
807
- * apply.
808
- *
809
- * Returns options unchanged when no auto-cascade is needed so the
810
- * downstream conversational/loop path sees the original reference.
811
- */
812
- async function prepareOptionsWithMemoryAutoCascade(a, options) {
813
- if (options?.messages)
814
- return options;
815
- const spec = await resolveRemembersSpec(() => a.remembers(), options?.memory);
816
- if (!spec)
817
- return options;
818
- const installed = [];
819
- if (spec.inject === 'auto')
820
- installed.push(withMemoryInject(spec));
821
- if (spec.extract === 'auto' && spec.extractWith)
822
- installed.push(withMemoryExtract(spec));
823
- if (installed.length === 0)
824
- return options;
825
- const current = options?.[EXTRA_MIDDLEWARES] ?? [];
826
- return {
827
- ...options,
828
- [EXTRA_MIDDLEWARES]: [...current, ...installed],
829
- };
830
- }
831
- function createMiddlewareContext(messages, model, tools, iteration) {
832
- const [provider] = AiRegistry.parseModelString(model);
833
- let aborted = false;
834
- let abortReason = '';
835
- return {
836
- requestId: typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : `req-${Date.now()}`,
837
- iteration,
838
- chunkIndex: 0,
839
- messages,
840
- model,
841
- provider,
842
- toolNames: tools.map(t => t.definition.name),
843
- abort(reason) {
844
- aborted = true;
845
- abortReason = reason ?? 'Aborted by middleware';
846
- },
847
- get _aborted() { return aborted; },
848
- get _abortReason() { return abortReason; },
849
- };
850
- }
851
- function buildUserMessage(input, attachments) {
852
- if (!attachments?.length)
853
- return { role: 'user', content: input };
854
- const parts = [
855
- { type: 'text', text: input },
856
- ...attachmentsToContentParts(attachments),
857
- ];
858
- return { role: 'user', content: parts };
859
- }
860
- function buildToolSchemas(tools) {
861
- return tools.filter(t => !t.definition.lazy).map(toolToSchema);
862
- }
863
- function buildToolMap(tools) {
864
- const map = new Map();
865
- for (const t of tools)
866
- map.set(t.definition.name, t);
867
- return map;
868
- }
869
- function addUsage(total, step) {
870
- total.promptTokens += step.promptTokens;
871
- total.completionTokens += step.completionTokens;
872
- total.totalTokens += step.totalTokens;
873
- }
874
- /**
875
- * Merge two `TokenUsage` snapshots emitted within a single step, taking the
876
- * MAX per field. Stream providers may emit usage in multiple chunks (e.g.
877
- * Anthropic's `message_start` carries promptTokens, `message_delta` carries
878
- * completionTokens) — a naive last-wins overwrite drops correct earlier
879
- * values when later chunks under-report. MAX is safe because every chunk is
880
- * a running snapshot, not a delta: token counts only ever grow within a step.
881
- *
882
- * @internal — exported for testing only.
883
- */
884
- export function mergeUsage(a, b) {
885
- return {
886
- promptTokens: Math.max(a.promptTokens, b.promptTokens),
887
- completionTokens: Math.max(a.completionTokens, b.completionTokens),
888
- totalTokens: Math.max(a.totalTokens, b.totalTokens),
889
- };
890
- }
891
- function buildMiddlewareConfig(messages, a) {
892
- const config = { messages };
893
- const temp = a.temperature();
894
- const maxTok = a.maxTokens();
895
- if (temp !== undefined)
896
- config.temperature = temp;
897
- if (maxTok !== undefined)
898
- config.maxTokens = maxTok;
899
- return config;
900
- }
901
- /**
902
- * Iterate the failover model list and invoke `call` against each provider
903
- * adapter until one succeeds. Mutates `loopCtx.failoverAttempts` so the
904
- * observer event reflects the real number of attempts. A caller-supplied
905
- * `AbortSignal` short-circuits — abort errors propagate immediately rather
906
- * than triggering the next failover model.
907
- */
908
- async function runFailover(loopCtx, currentModel, call) {
909
- const failoverModels = [currentModel, ...loopCtx.agent.failover().filter(m => m !== currentModel)];
910
- let lastError;
911
- for (const tryModel of failoverModels) {
912
- try {
913
- const adapter = AiRegistry.resolve(tryModel);
914
- const [, modelId] = AiRegistry.parseModelString(tryModel);
915
- const reqOptions = {
916
- model: modelId,
917
- messages: loopCtx.messages,
918
- tools: loopCtx.toolSchemas.length > 0 ? loopCtx.toolSchemas : undefined,
919
- temperature: loopCtx.agent.temperature(),
920
- maxTokens: loopCtx.agent.maxTokens(),
921
- signal: loopCtx.options?.signal,
922
- cache: resolveCacheMarkers(loopCtx.agent, loopCtx.options),
923
- };
924
- return await call(adapter, modelId, reqOptions);
925
- }
926
- catch (err) {
927
- // If the abort came from the caller, don't try the next failover
928
- // model — re-throw so `prompt()` / the stream rejects immediately.
929
- if (loopCtx.options?.signal?.aborted)
930
- throw loopCtx.options.signal.reason;
931
- lastError = err instanceof Error ? err : new Error(String(err));
932
- loopCtx.failoverAttempts++;
933
- if (tryModel === failoverModels[failoverModels.length - 1])
934
- throw lastError;
935
- }
936
- }
937
- throw lastError ?? new Error('No provider available');
938
- }
939
- /**
940
- * Merge agent-level `cacheable()` declaration with per-call override.
941
- *
942
- * - Per-call `cache: false` → returns `undefined` (caching disabled).
943
- * - Per-call `cache: { ... }` → replaces the agent default.
944
- * - Per-call omitted → uses `agent.cacheable()` unchanged.
945
- *
946
- * Returns `undefined` when no markers are set so the provider request
947
- * carries no `cache` field at all.
948
- */
949
- function resolveCacheMarkers(agent, options) {
950
- if (options && options.cache === false)
951
- return undefined;
952
- const perCall = options?.cache === false ? undefined : options?.cache;
953
- const config = perCall ?? agent.cacheable();
954
- if (!config)
955
- return undefined;
956
- const markers = {};
957
- if (config.instructions)
958
- markers.instructions = true;
959
- if (config.tools)
960
- markers.tools = true;
961
- if (config.messages !== undefined && config.messages > 0) {
962
- markers.messages = Math.floor(config.messages);
963
- }
964
- if (config.ttl)
965
- markers.ttl = config.ttl;
966
- // ttl alone with no region markers is meaningless — drop it.
967
- const hasRegion = markers.instructions || markers.tools || (markers.messages && markers.messages > 0);
968
- if (!hasRegion)
969
- return undefined;
970
- return markers;
971
- }
972
- /** Emit the `agent.failed` observer event from the shared loop state. */
973
- function emitObserverFailed(loopCtx, err, streaming) {
974
- const obs = _getAiObservers();
975
- if (!obs)
976
- return;
977
- const inputText = loopCtx.options?.messages ? '' : loopCtx.input;
978
- obs.emit({
979
- kind: 'agent.failed',
980
- agentName: loopCtx.agent.constructor.name,
981
- model: loopCtx.modelString,
982
- provider: loopCtx.providerName,
983
- input: inputText,
984
- output: '',
985
- steps: _buildObserverSteps(loopCtx.steps, loopCtx.modelString),
986
- tokens: {
987
- prompt: loopCtx.totalUsage.promptTokens,
988
- completion: loopCtx.totalUsage.completionTokens,
989
- total: loopCtx.totalUsage.totalTokens,
990
- },
991
- duration: Math.round(performance.now() - loopCtx.loopStart),
992
- finishReason: 'error',
993
- streaming,
994
- conversationId: null,
995
- failoverAttempts: loopCtx.failoverAttempts,
996
- error: err instanceof Error ? err.message : String(err),
997
- });
998
- }
999
- /**
1000
- * Emit the per-step `agent.step.completed` observer event after each
1001
- * iteration. Built from the SAME `_buildObserverSteps` mapping used by
1002
- * the terminal events so consumers see consistent shapes — they just see
1003
- * the latest step rather than the full array.
1004
- */
1005
- function emitObserverStepCompleted(loopCtx, iteration, streaming) {
1006
- const obs = _getAiObservers();
1007
- if (!obs)
1008
- return;
1009
- const justPushed = loopCtx.steps[loopCtx.steps.length - 1];
1010
- if (!justPushed)
1011
- return;
1012
- // Re-use _buildObserverSteps so the per-step shape matches the steps[]
1013
- // entries on the terminal events. Pass a single-element slice since we
1014
- // only need the latest step's mapping.
1015
- const built = _buildObserverSteps([justPushed], loopCtx.modelString);
1016
- const stepEvent = built[0];
1017
- if (!stepEvent)
1018
- return;
1019
- // Override iteration with the loop's iteration counter — _buildObserverSteps
1020
- // numbers from 1 within the array it sees, but we want the global step
1021
- // number across the whole run.
1022
- stepEvent.iteration = iteration + 1;
1023
- obs.emit({
1024
- kind: 'agent.step.completed',
1025
- agentName: loopCtx.agent.constructor.name,
1026
- model: loopCtx.modelString,
1027
- provider: loopCtx.providerName,
1028
- iteration: iteration + 1,
1029
- step: stepEvent,
1030
- tokens: {
1031
- prompt: loopCtx.totalUsage.promptTokens,
1032
- completion: loopCtx.totalUsage.completionTokens,
1033
- total: loopCtx.totalUsage.totalTokens,
1034
- },
1035
- duration: Math.round(performance.now() - loopCtx.loopStart),
1036
- streaming,
1037
- conversationId: null,
1038
- });
1039
- }
1040
- /** Emit the `agent.completed` observer event from the shared loop state. */
1041
- function emitObserverCompleted(loopCtx, result, streaming) {
1042
- const obs = _getAiObservers();
1043
- if (!obs)
1044
- return;
1045
- const inputText = loopCtx.options?.messages ? '' : loopCtx.input;
1046
- const lastStep = loopCtx.steps[loopCtx.steps.length - 1];
1047
- obs.emit({
1048
- kind: 'agent.completed',
1049
- agentName: loopCtx.agent.constructor.name,
1050
- model: loopCtx.modelString,
1051
- provider: loopCtx.providerName,
1052
- input: inputText,
1053
- output: result.text,
1054
- steps: _buildObserverSteps(loopCtx.steps, loopCtx.modelString),
1055
- tokens: {
1056
- prompt: loopCtx.totalUsage.promptTokens,
1057
- completion: loopCtx.totalUsage.completionTokens,
1058
- total: loopCtx.totalUsage.totalTokens,
1059
- },
1060
- duration: Math.round(performance.now() - loopCtx.loopStart),
1061
- finishReason: result.finishReason ?? lastStep?.finishReason ?? 'stop',
1062
- streaming,
1063
- conversationId: null,
1064
- failoverAttempts: loopCtx.failoverAttempts,
1065
- });
1066
- }
1067
- /** Build the final `AgentResponse` from accumulated loop state. */
1068
- function buildAgentResponse(loopCtx) {
1069
- const lastStep = loopCtx.steps[loopCtx.steps.length - 1];
1070
- const result = {
1071
- text: lastStep ? getMessageText(lastStep.message.content) : '',
1072
- steps: loopCtx.steps,
1073
- usage: loopCtx.totalUsage,
1074
- };
1075
- if (loopCtx.loopFinishReason)
1076
- result.finishReason = loopCtx.loopFinishReason;
1077
- if (loopCtx.pendingClientToolCalls.length > 0)
1078
- result.pendingClientToolCalls = loopCtx.pendingClientToolCalls;
1079
- if (loopCtx.pendingApprovalToolCall)
1080
- result.pendingApprovalToolCall = loopCtx.pendingApprovalToolCall;
1081
- if (loopCtx.resumedToolMessages.length > 0)
1082
- result.resumedToolMessages = loopCtx.resumedToolMessages;
1083
- // Internal — consumed by the handoff-aware wrapper, then stripped before
1084
- // surfacing to public callers.
1085
- if (loopCtx.pendingHandoff) {
1086
- result._pendingHandoff = loopCtx.pendingHandoff;
1087
- result._carriedMessages = loopCtx.messages;
1088
- }
1089
- return result;
1090
- }
1091
- /**
1092
- * Build the shared `LoopContext` for a `prompt()` / `stream()` call, run
1093
- * approval-resume, and fire `onConfig(init)` + `onStart`. After this returns,
1094
- * the iteration loop can run with the same setup regardless of streaming
1095
- * mode.
1096
- */
1097
- async function initializeLoop(a, input, options) {
1098
- // Honor caller-supplied AbortSignal as early as possible — if the signal
1099
- // is already aborted on entry, do no work at all.
1100
- options?.signal?.throwIfAborted();
1101
- const loopStart = performance.now();
1102
- const modelString = a.model() ?? AiRegistry.getDefault();
1103
- const [providerName] = AiRegistry.parseModelString(modelString);
1104
- const tools = getTools(a);
1105
- const middlewares = getMiddleware(a, options);
1106
- const toolSchemas = buildToolSchemas(tools);
1107
- const toolMap = buildToolMap(tools);
1108
- const messages = options?.messages
1109
- ? [{ role: 'system', content: a.instructions() }, ...options.messages]
1110
- : [
1111
- { role: 'system', content: a.instructions() },
1112
- ...(options?.history ?? []),
1113
- buildUserMessage(input, options?.attachments),
1114
- ];
1115
- const steps = [];
1116
- const stopConditions = normalizeStopConditions(a.stopWhen());
1117
- const totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
1118
- // Create middleware context (resume below mutates `messages`, captured by
1119
- // reference here, so order is safe).
1120
- const ctx = createMiddlewareContext(messages, modelString, tools, 0);
1121
- const loopCtx = {
1122
- agent: a,
1123
- input,
1124
- options,
1125
- modelString,
1126
- providerName,
1127
- tools,
1128
- toolMap,
1129
- toolSchemas,
1130
- middlewares,
1131
- loopStart,
1132
- ctx,
1133
- messages,
1134
- steps,
1135
- totalUsage,
1136
- pendingClientToolCalls: [],
1137
- pendingApprovalToolCall: undefined,
1138
- loopFinishReason: undefined,
1139
- stopForClientTools: false,
1140
- stopForApproval: false,
1141
- resumedToolMessages: [],
1142
- failoverAttempts: 0,
1143
- stopForHandoff: false,
1144
- };
1145
- // Resume server tools left pending by a previous approval round-trip.
1146
- {
1147
- const resume = await resumePendingToolCalls({ messages, toolMap, options });
1148
- loopCtx.resumedToolMessages = resume.resumed;
1149
- if (resume.approvalStillRequired) {
1150
- loopCtx.pendingApprovalToolCall = resume.approvalStillRequired;
1151
- loopCtx.loopFinishReason = 'tool_approval_required';
1152
- loopCtx.stopForApproval = true;
1153
- }
1154
- }
1155
- // onConfig — init phase
1156
- if (middlewares.length > 0) {
1157
- const configResult = runOnConfig(middlewares, ctx, buildMiddlewareConfig(messages, a), 'init');
1158
- if (configResult.messages)
1159
- messages.splice(0, messages.length, ...configResult.messages);
1160
- }
1161
- // onStart
1162
- if (middlewares.length > 0)
1163
- await runSequential(middlewares, 'onStart', ctx);
1164
- return { loopCtx, stopConditions };
1165
- }
1166
- /**
1167
- * Run the per-iteration prelude — caller-abort check, middleware-abort
1168
- * check, `onIteration`, `prepareStep`, `onConfig(beforeModel)`. Returns the
1169
- * resolved model for this step or `{ aborted: true }` if middleware
1170
- * cancelled the run (caller should `break`). Throws the abort reason if a
1171
- * caller-supplied AbortSignal fired between iterations.
1172
- */
1173
- async function runIterationPrelude(loopCtx, iteration) {
1174
- const { agent, options, ctx, middlewares, messages, modelString, steps } = loopCtx;
1175
- ctx.iteration = iteration;
1176
- // Reset the streaming chunk index for middlewares that key off it. Harmless
1177
- // in non-streaming mode where no chunks flow through `onChunk`.
1178
- ctx.chunkIndex = 0;
1179
- // Honor caller-supplied AbortSignal between iterations.
1180
- options?.signal?.throwIfAborted();
1181
- if (ctx._aborted) {
1182
- await runOnAbort(middlewares, ctx, ctx._abortReason);
1183
- return { aborted: true };
1184
- }
1185
- if (middlewares.length > 0)
1186
- await runSequential(middlewares, 'onIteration', ctx);
1187
- let currentModel = modelString;
1188
- if (agent.prepareStep) {
1189
- const prep = await agent.prepareStep({ stepNumber: iteration, steps, messages });
1190
- if (prep.model)
1191
- currentModel = prep.model;
1192
- if (prep.messages)
1193
- messages.splice(0, messages.length, ...prep.messages);
1194
- if (prep.system)
1195
- messages[0] = { role: 'system', content: prep.system };
1196
- }
1197
- if (middlewares.length > 0) {
1198
- const configResult = runOnConfig(middlewares, ctx, buildMiddlewareConfig(messages, agent), 'beforeModel');
1199
- if (configResult.messages)
1200
- messages.splice(0, messages.length, ...configResult.messages);
1201
- }
1202
- return { currentModel };
1203
- }
1204
- // ─── Agent Loop (non-streaming) ──────────────────────────
1205
- /**
1206
- * Public entry point for the non-streaming agent loop. Drives
1207
- * {@link runAgentLoopOnce} once, then — if the model called a {@link handoff}
1208
- * tool — constructs the target agent, carries the conversation forward, and
1209
- * recurses. Steps and usage from each hop are merged; the final `text` and
1210
- * `finishReason` come from the agent that produced the terminal answer.
1211
- * `handoffPath` records the chain of class names traversed.
1212
- */
1213
- async function runAgentLoop(a, input, options) {
1214
- const onceResult = await runAgentLoopOnce(a, input, options);
1215
- if (!onceResult._pendingHandoff) {
1216
- return stripInternal(onceResult);
1217
- }
1218
- const merged = await driveHandoffs(a.constructor.name, onceResult, onceResult._pendingHandoff, onceResult._carriedMessages ?? [], options, 0, runAgentLoopOnce);
1219
- return merged;
1220
- }
1221
- /**
1222
- * Streaming counterpart to {@link runAgentLoop}. Iterates handoffs and
1223
- * pivots the stream to the next agent each time the parent ends with a
1224
- * pending handoff. Chunks from every hop flow through the same returned
1225
- * `AsyncIterable`; the resolved `response` carries the merged final state.
1226
- */
1227
- function runAgentLoopStreaming(a, input, options) {
1228
- let resolveResponse;
1229
- let rejectResponse;
1230
- const responsePromise = new Promise((resolve, reject) => {
1231
- resolveResponse = resolve;
1232
- rejectResponse = reject;
1233
- });
1234
- async function* generateStream() {
1235
- let currentAgent = a;
1236
- let currentInput = input;
1237
- let currentOpts = options;
1238
- const mergedSteps = [];
1239
- const mergedUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
1240
- const handoffPath = [];
1241
- let finalResponse;
1242
- for (let hop = 0; hop <= MAX_HANDOFFS; hop++) {
1243
- const onceStream = runAgentLoopStreamingOnce(currentAgent, currentInput, currentOpts);
1244
- // Attach a no-op handler so a rejection from the inner response
1245
- // promise (e.g. caller-supplied AbortSignal firing mid-stream) is
1246
- // already observed by the time the `for await` re-throws — without
1247
- // this, Node logs an unhandledRejection between the stream's throw
1248
- // and our outer `withRejectOnError`'s catch.
1249
- onceStream.response.catch(() => { });
1250
- for await (const chunk of onceStream.stream)
1251
- yield chunk;
1252
- const r = await onceStream.response;
1253
- mergedSteps.push(...r.steps);
1254
- addUsage(mergedUsage, r.usage);
1255
- if (r._pendingHandoff && hop < MAX_HANDOFFS) {
1256
- handoffPath.push(currentAgent.constructor.name);
1257
- const ChildClass = r._pendingHandoff.spec.AgentClass;
1258
- currentAgent = new ChildClass();
1259
- currentInput = r._pendingHandoff.transitionMessage;
1260
- currentOpts = buildHandoffChildOptions(options, r._carriedMessages ?? []);
1261
- continue;
1262
- }
1263
- if (r._pendingHandoff) {
1264
- throw new Error(`[Rudder AI] Exceeded max handoffs (${MAX_HANDOFFS}). Likely a cycle between agents.`);
1265
- }
1266
- finalResponse = handoffPath.length === 0
1267
- ? stripInternal(r)
1268
- : mergeFinalHandoff(stripInternal(r), mergedSteps, mergedUsage, handoffPath, currentAgent.constructor.name);
1269
- break;
1270
- }
1271
- if (!finalResponse) {
1272
- throw new Error(`[Rudder AI] Exceeded max handoffs (${MAX_HANDOFFS}). Likely a cycle between agents.`);
1273
- }
1274
- resolveResponse(finalResponse);
1275
- }
1276
- async function* withRejectOnError() {
1277
- try {
1278
- yield* generateStream();
1279
- }
1280
- catch (err) {
1281
- rejectResponse(err);
1282
- throw err;
1283
- }
1284
- }
1285
- return {
1286
- stream: withRejectOnError(),
1287
- response: responsePromise,
1288
- };
1289
- }
1290
- async function runAgentLoopOnce(a, input, options) {
1291
- const { loopCtx, stopConditions } = await initializeLoop(a, input, options);
1292
- const { ctx, middlewares, messages, steps, totalUsage } = loopCtx;
1293
- try {
1294
- if (loopCtx.stopForApproval) {
1295
- // Approval is still required from the resume — skip the model loop.
1296
- }
1297
- else {
1298
- for (let iteration = 0; iteration < a.maxSteps(); iteration++) {
1299
- const prelude = await runIterationPrelude(loopCtx, iteration);
1300
- if ('aborted' in prelude)
1301
- break;
1302
- const { currentModel } = prelude;
1303
- const response = await runFailover(loopCtx, currentModel, (adapter, _, opts) => adapter.generate(opts));
1304
- addUsage(totalUsage, response.usage);
1305
- // onUsage
1306
- if (middlewares.length > 0)
1307
- await runOnUsage(middlewares, ctx, response.usage);
1308
- const toolCalls = response.message.toolCalls ?? [];
1309
- let toolResults = [];
1310
- if (toolCalls.length > 0) {
1311
- // Drain `executeToolPhase` to completion, discarding the streamed
1312
- // chunks — non-streaming callers don't surface them.
1313
- const phaseGen = executeToolPhase(loopCtx, toolCalls, response.message);
1314
- while (true) {
1315
- const next = await phaseGen.next();
1316
- if (next.done) {
1317
- toolResults = next.value;
1318
- break;
1319
- }
1320
- }
1321
- }
1322
- else {
1323
- messages.push(response.message);
1324
- }
1325
- const step = {
1326
- message: response.message,
1327
- toolCalls,
1328
- toolResults,
1329
- usage: response.usage,
1330
- finishReason: response.finishReason,
1331
- };
1332
- steps.push(step);
1333
- emitObserverStepCompleted(loopCtx, iteration, false);
1334
- if (loopCtx.stopForClientTools || loopCtx.stopForApproval || loopCtx.stopForHandoff)
1335
- break;
1336
- const shouldStop = stopConditions.some(cond => cond({ steps, iteration, lastMessage: response.message }));
1337
- if (shouldStop || response.finishReason !== 'tool_calls') {
1338
- break;
1339
- }
1340
- }
1341
- } // close `else` (skip-loop-when-resume-needs-approval)
1342
- }
1343
- catch (err) {
1344
- // onError
1345
- if (middlewares.length > 0)
1346
- await runOnError(middlewares, ctx, err);
1347
- emitObserverFailed(loopCtx, err, false);
1348
- throw err;
1349
- }
1350
- // onFinish
1351
- if (middlewares.length > 0)
1352
- await runSequential(middlewares, 'onFinish', ctx);
1353
- const result = buildAgentResponse(loopCtx);
1354
- emitObserverCompleted(loopCtx, result, false);
1355
- return result;
1356
- }
1357
- // ─── Agent Loop (streaming) ──────────────────────────────
1358
- function runAgentLoopStreamingOnce(a, input, options) {
1359
- let resolveResponse;
1360
- let rejectResponse;
1361
- const responsePromise = new Promise((resolve, reject) => {
1362
- resolveResponse = resolve;
1363
- rejectResponse = reject;
1364
- });
1365
- async function* generateStream() {
1366
- const { loopCtx, stopConditions } = await initializeLoop(a, input, options);
1367
- const { ctx, middlewares, messages, steps, totalUsage } = loopCtx;
1368
- try {
1369
- if (loopCtx.stopForApproval) {
1370
- // Resume detected unfulfilled approval — skip the model loop entirely.
1371
- }
1372
- else {
1373
- for (let iteration = 0; iteration < a.maxSteps(); iteration++) {
1374
- const prelude = await runIterationPrelude(loopCtx, iteration);
1375
- if ('aborted' in prelude)
1376
- break;
1377
- const { currentModel } = prelude;
1378
- const streamSource = await runFailover(loopCtx, currentModel, (adapter, _, opts) => adapter.stream(opts));
1379
- let text = '';
1380
- let currentToolCalls = [];
1381
- let stepUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
1382
- let finishReason = 'stop';
1383
- const partialToolCalls = new Map();
1384
- // Parallel-arg routing — OpenAI streams ≥2 tool calls interleaved by
1385
- // `index`. Without an index-keyed map the previous "pop last partial"
1386
- // attached `index=1`'s arg fragments to `index=0`'s partial (or vice
1387
- // versa), producing `{}` args or wrong args silently. Partials live in
1388
- // both maps by reference, so the final `JSON.parse` loop below
1389
- // continues to read from `partialToolCalls`.
1390
- const partialsByIndex = new Map();
1391
- for await (const chunk of streamSource) {
1392
- // onChunk — middleware can transform or drop chunks
1393
- let processedChunk = chunk;
1394
- if (middlewares.length > 0) {
1395
- processedChunk = runOnChunk(middlewares, ctx, chunk);
1396
- ctx.chunkIndex++;
1397
- }
1398
- if (processedChunk)
1399
- yield processedChunk;
1400
- // Always process the original chunk for state tracking
1401
- if (chunk.type === 'text-delta' && chunk.text) {
1402
- text += chunk.text;
1403
- }
1404
- else if (chunk.type === 'tool-call-delta' && chunk.toolCall?.id) {
1405
- const partial = {
1406
- id: chunk.toolCall.id,
1407
- name: chunk.toolCall.name ?? '',
1408
- argChunks: [],
1409
- };
1410
- partialToolCalls.set(chunk.toolCall.id, partial);
1411
- if (typeof chunk.toolCallIndex === 'number') {
1412
- partialsByIndex.set(chunk.toolCallIndex, partial);
1413
- }
1414
- }
1415
- else if (chunk.type === 'tool-call-delta' && chunk.text) {
1416
- // Route arg-delta to the matching partial by `toolCallIndex` when
1417
- // the adapter provides it (OpenAI). Fall back to the most recent
1418
- // partial only for adapters that don't track index — those don't
1419
- // currently stream parallel tool calls via arg-only deltas, so
1420
- // the legacy path is still safe.
1421
- const partial = typeof chunk.toolCallIndex === 'number'
1422
- ? partialsByIndex.get(chunk.toolCallIndex)
1423
- : Array.from(partialToolCalls.values()).pop();
1424
- if (partial)
1425
- partial.argChunks.push(chunk.text);
1426
- }
1427
- else if (chunk.type === 'tool-call' && chunk.toolCall) {
1428
- const tc = chunk.toolCall;
1429
- currentToolCalls.push(tc);
1430
- }
1431
- else if (chunk.type === 'usage' && chunk.usage) {
1432
- stepUsage = mergeUsage(stepUsage, chunk.usage);
1433
- }
1434
- else if (chunk.type === 'finish') {
1435
- if (chunk.usage)
1436
- stepUsage = mergeUsage(stepUsage, chunk.usage);
1437
- finishReason = chunk.finishReason ?? 'stop';
1438
- }
1439
- }
1440
- // Finalize partial tool calls
1441
- for (const [, partial] of partialToolCalls) {
1442
- try {
1443
- const args = JSON.parse(partial.argChunks.join(''));
1444
- currentToolCalls.push({ id: partial.id, name: partial.name, arguments: args });
1445
- }
1446
- catch {
1447
- currentToolCalls.push({ id: partial.id, name: partial.name, arguments: {} });
1448
- }
1449
- }
1450
- addUsage(totalUsage, stepUsage);
1451
- // onUsage
1452
- if (middlewares.length > 0)
1453
- await runOnUsage(middlewares, ctx, stepUsage);
1454
- let toolResults = [];
1455
- if (currentToolCalls.length > 0) {
1456
- const assistantMsg = { role: 'assistant', content: text, toolCalls: currentToolCalls };
1457
- // Forward chunks from the shared tool-phase generator straight
1458
- // through to the stream consumer.
1459
- const phaseGen = executeToolPhase(loopCtx, currentToolCalls, assistantMsg);
1460
- while (true) {
1461
- const next = await phaseGen.next();
1462
- if (next.done) {
1463
- toolResults = next.value;
1464
- break;
1465
- }
1466
- yield next.value;
1467
- }
1468
- }
1469
- else {
1470
- messages.push({ role: 'assistant', content: text });
1471
- }
1472
- const step = {
1473
- message: { role: 'assistant', content: text, ...(currentToolCalls.length > 0 ? { toolCalls: currentToolCalls } : {}) },
1474
- toolCalls: currentToolCalls,
1475
- toolResults,
1476
- usage: stepUsage,
1477
- finishReason,
1478
- };
1479
- steps.push(step);
1480
- emitObserverStepCompleted(loopCtx, iteration, true);
1481
- if (loopCtx.stopForClientTools || loopCtx.stopForApproval || loopCtx.stopForHandoff)
1482
- break;
1483
- const shouldStop = stopConditions.some(cond => cond({ steps, iteration, lastMessage: step.message }));
1484
- if (shouldStop || finishReason !== 'tool_calls')
1485
- break;
1486
- // Reset for next iteration
1487
- text = '';
1488
- currentToolCalls = [];
1489
- }
1490
- } // close `else` (skip-loop-when-resume-needs-approval)
1491
- }
1492
- catch (err) {
1493
- // onError
1494
- if (middlewares.length > 0)
1495
- await runOnError(middlewares, ctx, err);
1496
- emitObserverFailed(loopCtx, err, true);
1497
- throw err;
1498
- }
1499
- // onFinish
1500
- if (middlewares.length > 0)
1501
- await runSequential(middlewares, 'onFinish', ctx);
1502
- // Emit pending state to consumers via dedicated chunk types
1503
- if (loopCtx.pendingClientToolCalls.length > 0) {
1504
- yield { type: 'pending-client-tools', toolCalls: loopCtx.pendingClientToolCalls };
1505
- }
1506
- if (loopCtx.pendingApprovalToolCall) {
1507
- yield {
1508
- type: 'pending-approval',
1509
- toolCall: loopCtx.pendingApprovalToolCall.toolCall,
1510
- isClientTool: loopCtx.pendingApprovalToolCall.isClientTool,
1511
- };
1512
- }
1513
- const result = buildAgentResponse(loopCtx);
1514
- emitObserverCompleted(loopCtx, result, true);
1515
- resolveResponse(result);
1516
- }
1517
- // Outer wrapper: if `generateStream` throws (e.g. the caller's
1518
- // AbortSignal fired), reject the `response` promise with the same
1519
- // reason BEFORE re-throwing into the for-await consumer. Without this,
1520
- // `await response` would hang forever after a mid-stream abort.
1521
- async function* withRejectOnError() {
1522
- try {
1523
- yield* generateStream();
1524
- }
1525
- catch (err) {
1526
- rejectResponse(err);
1527
- throw err;
1528
- }
1529
- }
1530
- return {
1531
- stream: withRejectOnError(),
1532
- response: responsePromise,
1533
- };
1534
- }
1535
- function normalizeStopConditions(cond) {
1536
- return Array.isArray(cond) ? cond : [cond];
1537
- }
1538
- //# sourceMappingURL=agent.js.map