@raybase/core 0.0.1 → 0.3.3

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 (435) hide show
  1. package/LICENSE +21 -0
  2. package/dist/.admin-build/AdminApp.css +2 -0
  3. package/dist/.admin-build/AdminApp.js +77 -0
  4. package/dist/.admin-docs-build/AdminDocsApp.css +2 -0
  5. package/dist/.admin-docs-build/AdminDocsApp.js +45 -0
  6. package/dist/actions.d.ts +347 -0
  7. package/dist/actions.d.ts.map +1 -0
  8. package/dist/actions.js +661 -0
  9. package/dist/actions.js.map +1 -0
  10. package/dist/admin-assets/admin.7738f50720a8ed7b.css +2 -0
  11. package/dist/admin-assets/admin.8678cb61db34dc81.js +77 -0
  12. package/dist/admin-boot.d.ts +94 -0
  13. package/dist/admin-boot.d.ts.map +1 -0
  14. package/dist/admin-boot.js +139 -0
  15. package/dist/admin-boot.js.map +1 -0
  16. package/dist/admin-docs-content.d.ts +95 -0
  17. package/dist/admin-docs-content.d.ts.map +1 -0
  18. package/dist/admin-docs-content.js +463 -0
  19. package/dist/admin-docs-content.js.map +1 -0
  20. package/dist/admin-docs-ui.d.ts +11 -0
  21. package/dist/admin-docs-ui.d.ts.map +1 -0
  22. package/dist/admin-docs-ui.js +23 -0
  23. package/dist/admin-docs-ui.js.map +1 -0
  24. package/dist/admin-kit.d.ts +29 -0
  25. package/dist/admin-kit.js +1557 -0
  26. package/dist/admin-role.d.ts +8 -0
  27. package/dist/admin-role.d.ts.map +1 -0
  28. package/dist/admin-role.js +22 -0
  29. package/dist/admin-role.js.map +1 -0
  30. package/dist/admin-ui.d.ts +3 -0
  31. package/dist/admin-ui.d.ts.map +1 -0
  32. package/dist/admin-ui.js +16 -0
  33. package/dist/admin-ui.js.map +1 -0
  34. package/dist/agent-ai-chat.d.ts +127 -0
  35. package/dist/agent-ai-chat.d.ts.map +1 -0
  36. package/dist/agent-ai-chat.js +5975 -0
  37. package/dist/agent-ai-chat.js.map +1 -0
  38. package/dist/agent-chapter-archive.d.ts +214 -0
  39. package/dist/agent-chapter-archive.d.ts.map +1 -0
  40. package/dist/agent-chapter-archive.js +696 -0
  41. package/dist/agent-chapter-archive.js.map +1 -0
  42. package/dist/agent-chapter-search.d.ts +116 -0
  43. package/dist/agent-chapter-search.d.ts.map +1 -0
  44. package/dist/agent-chapter-search.js +605 -0
  45. package/dist/agent-chapter-search.js.map +1 -0
  46. package/dist/agent-chapter-tools.d.ts +45 -0
  47. package/dist/agent-chapter-tools.d.ts.map +1 -0
  48. package/dist/agent-chapter-tools.js +344 -0
  49. package/dist/agent-chapter-tools.js.map +1 -0
  50. package/dist/agent-chapters.d.ts +228 -0
  51. package/dist/agent-chapters.d.ts.map +1 -0
  52. package/dist/agent-chapters.js +737 -0
  53. package/dist/agent-chapters.js.map +1 -0
  54. package/dist/agent-data-parts.d.ts +167 -0
  55. package/dist/agent-data-parts.d.ts.map +1 -0
  56. package/dist/agent-data-parts.js +68 -0
  57. package/dist/agent-data-parts.js.map +1 -0
  58. package/dist/agent-errors.d.ts +41 -0
  59. package/dist/agent-errors.d.ts.map +1 -0
  60. package/dist/agent-errors.js +191 -0
  61. package/dist/agent-errors.js.map +1 -0
  62. package/dist/agent-follow-up-store.d.ts +273 -0
  63. package/dist/agent-follow-up-store.d.ts.map +1 -0
  64. package/dist/agent-follow-up-store.js +703 -0
  65. package/dist/agent-follow-up-store.js.map +1 -0
  66. package/dist/agent-follow-ups.d.ts +314 -0
  67. package/dist/agent-follow-ups.d.ts.map +1 -0
  68. package/dist/agent-follow-ups.js +840 -0
  69. package/dist/agent-follow-ups.js.map +1 -0
  70. package/dist/agent-memory.d.ts +323 -0
  71. package/dist/agent-memory.d.ts.map +1 -0
  72. package/dist/agent-memory.js +1270 -0
  73. package/dist/agent-memory.js.map +1 -0
  74. package/dist/agent-message-parts.d.ts +58 -0
  75. package/dist/agent-message-parts.d.ts.map +1 -0
  76. package/dist/agent-message-parts.js +163 -0
  77. package/dist/agent-message-parts.js.map +1 -0
  78. package/dist/agent-remote-mcp-credentials.d.ts +154 -0
  79. package/dist/agent-remote-mcp-credentials.d.ts.map +1 -0
  80. package/dist/agent-remote-mcp-credentials.js +287 -0
  81. package/dist/agent-remote-mcp-credentials.js.map +1 -0
  82. package/dist/agent-remote-mcp-crypto.d.ts +32 -0
  83. package/dist/agent-remote-mcp-crypto.d.ts.map +1 -0
  84. package/dist/agent-remote-mcp-crypto.js +101 -0
  85. package/dist/agent-remote-mcp-crypto.js.map +1 -0
  86. package/dist/agent-remote-mcp-install.d.ts +299 -0
  87. package/dist/agent-remote-mcp-install.d.ts.map +1 -0
  88. package/dist/agent-remote-mcp-install.js +785 -0
  89. package/dist/agent-remote-mcp-install.js.map +1 -0
  90. package/dist/agent-remote-mcp-oauth.d.ts +73 -0
  91. package/dist/agent-remote-mcp-oauth.d.ts.map +1 -0
  92. package/dist/agent-remote-mcp-oauth.js +344 -0
  93. package/dist/agent-remote-mcp-oauth.js.map +1 -0
  94. package/dist/agent-remote-mcp-transport.d.ts +46 -0
  95. package/dist/agent-remote-mcp-transport.d.ts.map +1 -0
  96. package/dist/agent-remote-mcp-transport.js +225 -0
  97. package/dist/agent-remote-mcp-transport.js.map +1 -0
  98. package/dist/agent-remote-mcp.d.ts +147 -0
  99. package/dist/agent-remote-mcp.d.ts.map +1 -0
  100. package/dist/agent-remote-mcp.js +681 -0
  101. package/dist/agent-remote-mcp.js.map +1 -0
  102. package/dist/agent-room-redaction.d.ts +66 -0
  103. package/dist/agent-room-redaction.d.ts.map +1 -0
  104. package/dist/agent-room-redaction.js +264 -0
  105. package/dist/agent-room-redaction.js.map +1 -0
  106. package/dist/agent-rooms.d.ts +209 -0
  107. package/dist/agent-rooms.d.ts.map +1 -0
  108. package/dist/agent-rooms.js +244 -0
  109. package/dist/agent-rooms.js.map +1 -0
  110. package/dist/agent-runtime.d.ts +541 -0
  111. package/dist/agent-runtime.d.ts.map +1 -0
  112. package/dist/agent-runtime.js +4260 -0
  113. package/dist/agent-runtime.js.map +1 -0
  114. package/dist/agent-skill-archive.d.ts +20 -0
  115. package/dist/agent-skill-archive.d.ts.map +1 -0
  116. package/dist/agent-skill-archive.js +316 -0
  117. package/dist/agent-skill-archive.js.map +1 -0
  118. package/dist/agent-skill-discovery.d.ts +96 -0
  119. package/dist/agent-skill-discovery.d.ts.map +1 -0
  120. package/dist/agent-skill-discovery.js +464 -0
  121. package/dist/agent-skill-discovery.js.map +1 -0
  122. package/dist/agent-skill-format.d.ts +57 -0
  123. package/dist/agent-skill-format.d.ts.map +1 -0
  124. package/dist/agent-skill-format.js +238 -0
  125. package/dist/agent-skill-format.js.map +1 -0
  126. package/dist/agent-skill-import.d.ts +64 -0
  127. package/dist/agent-skill-import.d.ts.map +1 -0
  128. package/dist/agent-skill-import.js +295 -0
  129. package/dist/agent-skill-import.js.map +1 -0
  130. package/dist/agent-skill-publish.d.ts +51 -0
  131. package/dist/agent-skill-publish.d.ts.map +1 -0
  132. package/dist/agent-skill-publish.js +136 -0
  133. package/dist/agent-skill-publish.js.map +1 -0
  134. package/dist/agent-skill-store.d.ts +252 -0
  135. package/dist/agent-skill-store.d.ts.map +1 -0
  136. package/dist/agent-skill-store.js +514 -0
  137. package/dist/agent-skill-store.js.map +1 -0
  138. package/dist/agent-skills.d.ts +85 -0
  139. package/dist/agent-skills.d.ts.map +1 -0
  140. package/dist/agent-skills.js +291 -0
  141. package/dist/agent-skills.js.map +1 -0
  142. package/dist/agent-transcript-pager.d.ts +77 -0
  143. package/dist/agent-transcript-pager.d.ts.map +1 -0
  144. package/dist/agent-transcript-pager.js +210 -0
  145. package/dist/agent-transcript-pager.js.map +1 -0
  146. package/dist/agent-user-data-parts.d.ts +20 -0
  147. package/dist/agent-user-data-parts.d.ts.map +1 -0
  148. package/dist/agent-user-data-parts.js +161 -0
  149. package/dist/agent-user-data-parts.js.map +1 -0
  150. package/dist/agent-user-transcript.d.ts +46 -0
  151. package/dist/agent-user-transcript.d.ts.map +1 -0
  152. package/dist/agent-user-transcript.js +106 -0
  153. package/dist/agent-user-transcript.js.map +1 -0
  154. package/dist/agent-voice-notes.d.ts +15 -0
  155. package/dist/agent-voice-notes.d.ts.map +1 -0
  156. package/dist/agent-voice-notes.js +17 -0
  157. package/dist/agent-voice-notes.js.map +1 -0
  158. package/dist/agent-wakes.d.ts +213 -0
  159. package/dist/agent-wakes.d.ts.map +1 -0
  160. package/dist/agent-wakes.js +534 -0
  161. package/dist/agent-wakes.js.map +1 -0
  162. package/dist/agent.d.ts +1780 -0
  163. package/dist/agent.d.ts.map +1 -0
  164. package/dist/agent.js +3123 -0
  165. package/dist/agent.js.map +1 -0
  166. package/dist/ai.d.ts +583 -0
  167. package/dist/ai.d.ts.map +1 -0
  168. package/dist/ai.js +1858 -0
  169. package/dist/ai.js.map +1 -0
  170. package/dist/app-manifest.d.ts +255 -0
  171. package/dist/app-manifest.d.ts.map +1 -0
  172. package/dist/app-manifest.js +372 -0
  173. package/dist/app-manifest.js.map +1 -0
  174. package/dist/app-users.d.ts +30 -0
  175. package/dist/app-users.d.ts.map +1 -0
  176. package/dist/app-users.js +91 -0
  177. package/dist/app-users.js.map +1 -0
  178. package/dist/audit-archive.d.ts +85 -0
  179. package/dist/audit-archive.d.ts.map +1 -0
  180. package/dist/audit-archive.js +178 -0
  181. package/dist/audit-archive.js.map +1 -0
  182. package/dist/audit.d.ts +54 -0
  183. package/dist/audit.d.ts.map +1 -0
  184. package/dist/audit.js +95 -0
  185. package/dist/audit.js.map +1 -0
  186. package/dist/auth-logger.d.ts +28 -0
  187. package/dist/auth-logger.d.ts.map +1 -0
  188. package/dist/auth-logger.js +53 -0
  189. package/dist/auth-logger.js.map +1 -0
  190. package/dist/auth-password-secret.d.ts +59 -0
  191. package/dist/auth-password-secret.d.ts.map +1 -0
  192. package/dist/auth-password-secret.js +409 -0
  193. package/dist/auth-password-secret.js.map +1 -0
  194. package/dist/auth-schema.d.ts +120 -0
  195. package/dist/auth-schema.d.ts.map +1 -0
  196. package/dist/auth-schema.js +285 -0
  197. package/dist/auth-schema.js.map +1 -0
  198. package/dist/auth.d.ts +321 -0
  199. package/dist/auth.d.ts.map +1 -0
  200. package/dist/auth.js +1562 -0
  201. package/dist/auth.js.map +1 -0
  202. package/dist/backup-archive.d.ts +257 -0
  203. package/dist/backup-archive.d.ts.map +1 -0
  204. package/dist/backup-archive.js +2068 -0
  205. package/dist/backup-archive.js.map +1 -0
  206. package/dist/backup-crypto.d.ts +313 -0
  207. package/dist/backup-crypto.d.ts.map +1 -0
  208. package/dist/backup-crypto.js +693 -0
  209. package/dist/backup-crypto.js.map +1 -0
  210. package/dist/backup-format.d.ts +200 -0
  211. package/dist/backup-format.d.ts.map +1 -0
  212. package/dist/backup-format.js +1247 -0
  213. package/dist/backup-format.js.map +1 -0
  214. package/dist/cli.d.ts +9 -0
  215. package/dist/cli.d.ts.map +1 -0
  216. package/dist/cli.js +15 -0
  217. package/dist/cli.js.map +1 -0
  218. package/dist/clock.d.ts +35 -0
  219. package/dist/clock.d.ts.map +1 -0
  220. package/dist/clock.js +83 -0
  221. package/dist/clock.js.map +1 -0
  222. package/dist/data-client.d.ts +187 -0
  223. package/dist/data-client.d.ts.map +1 -0
  224. package/dist/data-client.js +590 -0
  225. package/dist/data-client.js.map +1 -0
  226. package/dist/data-secrets.d.ts +164 -0
  227. package/dist/data-secrets.d.ts.map +1 -0
  228. package/dist/data-secrets.js +383 -0
  229. package/dist/data-secrets.js.map +1 -0
  230. package/dist/db.d.ts +453 -0
  231. package/dist/db.d.ts.map +1 -0
  232. package/dist/db.js +324 -0
  233. package/dist/db.js.map +1 -0
  234. package/dist/dev-activity.d.ts +48 -0
  235. package/dist/dev-activity.d.ts.map +1 -0
  236. package/dist/dev-activity.js +50 -0
  237. package/dist/dev-activity.js.map +1 -0
  238. package/dist/do.d.ts +429 -0
  239. package/dist/do.d.ts.map +1 -0
  240. package/dist/do.js +4526 -0
  241. package/dist/do.js.map +1 -0
  242. package/dist/email.d.ts +137 -0
  243. package/dist/email.d.ts.map +1 -0
  244. package/dist/email.js +527 -0
  245. package/dist/email.js.map +1 -0
  246. package/dist/generated/admin-assets.d.ts +16 -0
  247. package/dist/generated/admin-assets.d.ts.map +1 -0
  248. package/dist/generated/admin-assets.js +16 -0
  249. package/dist/generated/admin-assets.js.map +1 -0
  250. package/dist/generated/admin-docs-assets.d.ts +3 -0
  251. package/dist/generated/admin-docs-assets.d.ts.map +1 -0
  252. package/dist/generated/admin-docs-assets.js +3 -0
  253. package/dist/generated/admin-docs-assets.js.map +1 -0
  254. package/dist/handoff-consumer.d.ts +79 -0
  255. package/dist/handoff-consumer.d.ts.map +1 -0
  256. package/dist/handoff-consumer.js +300 -0
  257. package/dist/handoff-consumer.js.map +1 -0
  258. package/dist/handoff.d.ts +70 -0
  259. package/dist/handoff.d.ts.map +1 -0
  260. package/dist/handoff.js +228 -0
  261. package/dist/handoff.js.map +1 -0
  262. package/dist/host-auth.d.ts +47 -0
  263. package/dist/host-auth.d.ts.map +1 -0
  264. package/dist/host-auth.js +59 -0
  265. package/dist/host-auth.js.map +1 -0
  266. package/dist/host-user.d.ts +15 -0
  267. package/dist/host-user.d.ts.map +1 -0
  268. package/dist/host-user.js +23 -0
  269. package/dist/host-user.js.map +1 -0
  270. package/dist/html.d.ts +35 -0
  271. package/dist/html.d.ts.map +1 -0
  272. package/dist/html.js +634 -0
  273. package/dist/html.js.map +1 -0
  274. package/dist/http-error.d.ts +13 -0
  275. package/dist/http-error.d.ts.map +1 -0
  276. package/dist/http-error.js +15 -0
  277. package/dist/http-error.js.map +1 -0
  278. package/dist/imports.d.ts +219 -0
  279. package/dist/imports.d.ts.map +1 -0
  280. package/dist/imports.js +571 -0
  281. package/dist/imports.js.map +1 -0
  282. package/dist/index.d.ts +78 -0
  283. package/dist/index.d.ts.map +1 -0
  284. package/dist/index.js +76 -0
  285. package/dist/index.js.map +1 -0
  286. package/dist/inference-tape.d.ts +64 -0
  287. package/dist/inference-tape.d.ts.map +1 -0
  288. package/dist/inference-tape.js +278 -0
  289. package/dist/inference-tape.js.map +1 -0
  290. package/dist/internal.d.ts +15 -0
  291. package/dist/internal.d.ts.map +1 -0
  292. package/dist/internal.js +48 -0
  293. package/dist/internal.js.map +1 -0
  294. package/dist/jobs.d.ts +796 -0
  295. package/dist/jobs.d.ts.map +1 -0
  296. package/dist/jobs.js +538 -0
  297. package/dist/jobs.js.map +1 -0
  298. package/dist/json-schema.d.ts +91 -0
  299. package/dist/json-schema.d.ts.map +1 -0
  300. package/dist/json-schema.js +884 -0
  301. package/dist/json-schema.js.map +1 -0
  302. package/dist/knowledge.d.ts +33 -0
  303. package/dist/knowledge.d.ts.map +1 -0
  304. package/dist/knowledge.js +348 -0
  305. package/dist/knowledge.js.map +1 -0
  306. package/dist/local-dev-chrome.d.ts +29 -0
  307. package/dist/local-dev-chrome.d.ts.map +1 -0
  308. package/dist/local-dev-chrome.js +514 -0
  309. package/dist/local-dev-chrome.js.map +1 -0
  310. package/dist/local-dev-personas.d.ts +15 -0
  311. package/dist/local-dev-personas.d.ts.map +1 -0
  312. package/dist/local-dev-personas.js +46 -0
  313. package/dist/local-dev-personas.js.map +1 -0
  314. package/dist/mcp.d.ts +567 -0
  315. package/dist/mcp.d.ts.map +1 -0
  316. package/dist/mcp.js +2480 -0
  317. package/dist/mcp.js.map +1 -0
  318. package/dist/modules.d.ts +145 -0
  319. package/dist/modules.d.ts.map +1 -0
  320. package/dist/modules.js +164 -0
  321. package/dist/modules.js.map +1 -0
  322. package/dist/notification-client.d.ts +94 -0
  323. package/dist/notification-client.d.ts.map +1 -0
  324. package/dist/notification-client.js +127 -0
  325. package/dist/notification-client.js.map +1 -0
  326. package/dist/notifications.d.ts +447 -0
  327. package/dist/notifications.d.ts.map +1 -0
  328. package/dist/notifications.js +1625 -0
  329. package/dist/notifications.js.map +1 -0
  330. package/dist/operator-data-query.d.ts +117 -0
  331. package/dist/operator-data-query.d.ts.map +1 -0
  332. package/dist/operator-data-query.js +431 -0
  333. package/dist/operator-data-query.js.map +1 -0
  334. package/dist/operator-mcp.d.ts +63 -0
  335. package/dist/operator-mcp.d.ts.map +1 -0
  336. package/dist/operator-mcp.js +2828 -0
  337. package/dist/operator-mcp.js.map +1 -0
  338. package/dist/outbound-https.d.ts +24 -0
  339. package/dist/outbound-https.d.ts.map +1 -0
  340. package/dist/outbound-https.js +172 -0
  341. package/dist/outbound-https.js.map +1 -0
  342. package/dist/pitr.d.ts +60 -0
  343. package/dist/pitr.d.ts.map +1 -0
  344. package/dist/pitr.js +94 -0
  345. package/dist/pitr.js.map +1 -0
  346. package/dist/platform.d.ts +199 -0
  347. package/dist/platform.d.ts.map +1 -0
  348. package/dist/platform.js +196 -0
  349. package/dist/platform.js.map +1 -0
  350. package/dist/policies.d.ts +115 -0
  351. package/dist/policies.d.ts.map +1 -0
  352. package/dist/policies.js +158 -0
  353. package/dist/policies.js.map +1 -0
  354. package/dist/principal.d.ts +143 -0
  355. package/dist/principal.d.ts.map +1 -0
  356. package/dist/principal.js +174 -0
  357. package/dist/principal.js.map +1 -0
  358. package/dist/realtime.d.ts +280 -0
  359. package/dist/realtime.d.ts.map +1 -0
  360. package/dist/realtime.js +578 -0
  361. package/dist/realtime.js.map +1 -0
  362. package/dist/retrieval.d.ts +263 -0
  363. package/dist/retrieval.d.ts.map +1 -0
  364. package/dist/retrieval.js +602 -0
  365. package/dist/retrieval.js.map +1 -0
  366. package/dist/runtime-error-client.d.ts +10 -0
  367. package/dist/runtime-error-client.d.ts.map +1 -0
  368. package/dist/runtime-error-client.js +70 -0
  369. package/dist/runtime-error-client.js.map +1 -0
  370. package/dist/runtime-errors.d.ts +180 -0
  371. package/dist/runtime-errors.d.ts.map +1 -0
  372. package/dist/runtime-errors.js +572 -0
  373. package/dist/runtime-errors.js.map +1 -0
  374. package/dist/schema.d.ts +434 -0
  375. package/dist/schema.d.ts.map +1 -0
  376. package/dist/schema.js +232 -0
  377. package/dist/schema.js.map +1 -0
  378. package/dist/search.d.ts +216 -0
  379. package/dist/search.d.ts.map +1 -0
  380. package/dist/search.js +785 -0
  381. package/dist/search.js.map +1 -0
  382. package/dist/settings.d.ts +191 -0
  383. package/dist/settings.d.ts.map +1 -0
  384. package/dist/settings.js +478 -0
  385. package/dist/settings.js.map +1 -0
  386. package/dist/sql.d.ts +7 -0
  387. package/dist/sql.d.ts.map +1 -0
  388. package/dist/sql.js +30 -0
  389. package/dist/sql.js.map +1 -0
  390. package/dist/storage-client.d.ts +69 -0
  391. package/dist/storage-client.d.ts.map +1 -0
  392. package/dist/storage-client.js +469 -0
  393. package/dist/storage-client.js.map +1 -0
  394. package/dist/storage.d.ts +21 -0
  395. package/dist/storage.d.ts.map +1 -0
  396. package/dist/storage.js +82 -0
  397. package/dist/storage.js.map +1 -0
  398. package/dist/subject-domains.d.ts +15 -0
  399. package/dist/subject-domains.d.ts.map +1 -0
  400. package/dist/subject-domains.js +38 -0
  401. package/dist/subject-domains.js.map +1 -0
  402. package/dist/support.d.ts +132 -0
  403. package/dist/support.d.ts.map +1 -0
  404. package/dist/support.js +483 -0
  405. package/dist/support.js.map +1 -0
  406. package/dist/system-tables.d.ts +12 -0
  407. package/dist/system-tables.d.ts.map +1 -0
  408. package/dist/system-tables.js +225 -0
  409. package/dist/system-tables.js.map +1 -0
  410. package/dist/test.d.ts +57 -0
  411. package/dist/test.d.ts.map +1 -0
  412. package/dist/test.js +454 -0
  413. package/dist/test.js.map +1 -0
  414. package/dist/vite.d.ts +82 -0
  415. package/dist/vite.d.ts.map +1 -0
  416. package/dist/vite.js +82 -0
  417. package/dist/vite.js.map +1 -0
  418. package/dist/webpush-crypto.d.ts +39 -0
  419. package/dist/webpush-crypto.d.ts.map +1 -0
  420. package/dist/webpush-crypto.js +213 -0
  421. package/dist/webpush-crypto.js.map +1 -0
  422. package/dist/webpush-private.d.ts +11 -0
  423. package/dist/webpush-private.d.ts.map +1 -0
  424. package/dist/webpush-private.js +11 -0
  425. package/dist/webpush-private.js.map +1 -0
  426. package/dist/webpush.d.ts +86 -0
  427. package/dist/webpush.d.ts.map +1 -0
  428. package/dist/webpush.js +375 -0
  429. package/dist/webpush.js.map +1 -0
  430. package/dist/worker.d.ts +185 -0
  431. package/dist/worker.d.ts.map +1 -0
  432. package/dist/worker.js +13021 -0
  433. package/dist/worker.js.map +1 -0
  434. package/package.json +161 -8
  435. package/README.md +0 -3
package/dist/agent.js ADDED
@@ -0,0 +1,3123 @@
1
+ import { jsonSchema, tool } from "ai";
2
+ import { agentPrincipal } from "./principal.js";
3
+ import { actionInputJsonSchema, agentActionRegistrations, executeAction, restoreStrictActionInput, unsupportedRaybaseAgentInvocationRuntime } from "./actions.js";
4
+ import { authSecret, createRaybaseAuthContextRuntime, unsupportedRaybaseAuthContextRuntime } from "./auth.js";
5
+ import { agentActionToolName, agentSkillActionBindings, agentSkillRegistrations, agentSkillSummaries, validateAgentSkillsForAgent } from "./agent-skills.js";
6
+ import { agentRemoteMcpBindingSummaries, agentRemoteMcpToolBindings, validateAgentRemoteMcpBindings } from "./agent-remote-mcp.js";
7
+ import { agentMemoryMaintenanceRuns, agentMemorySpaces, raybaseAgentMemorySystemTables, raybaseAgentMemoryToolNames, validateAgentMemoryForAgent } from "./agent-memory.js";
8
+ import { raybaseAgentSkillSystemTables, raybaseAgentSkillToolNames, validateAgentSkillStoreForAgent } from "./agent-skill-store.js";
9
+ import { raybaseAgentWakeSystemTables, raybaseAgentWakeToolNames, validateAgentWakesForAgent } from "./agent-wakes.js";
10
+ import { raybaseAgentFollowUpSystemTables, validateAgentFollowUpsForAgent } from "./agent-follow-ups.js";
11
+ import { findAgentRoomMember, raybaseAgentRoomSystemTables, setAgentRoomMemberPinned, validateAgentRoomsForAgent } from "./agent-rooms.js";
12
+ import { buildAgentChapterRehydrateSeed, defaultAgentRunEvidenceTtlMs, loadLastAgentChapter, minAgentRunEvidenceTtlMs, normalizeAgentRetentionConfig, purgeAgentChaptersForConversation, raybaseAgentChapterSystemTables, validateAgentRetentionForAgent } from "./agent-chapters.js";
13
+ import { ensureAgentChapterSearch } from "./agent-chapter-search.js";
14
+ import { raybaseAgentChapterArchiveSystemTables } from "./agent-chapter-archive.js";
15
+ import { normalizeAgentVoiceNotesConfig } from "./agent-voice-notes.js";
16
+ import { validateAgentUserDataPartsConfig } from "./agent-user-data-parts.js";
17
+ export { isAgentUserDataPartType, isReservedAgentDataPartType, maxAgentUserDataBytesPerMessage, maxAgentUserDataPartBytes, maxAgentUserDataPartsPerMessage } from "./agent-user-data-parts.js";
18
+ import { parseSearchQuery } from "./search.js";
19
+ import { storageHardMaxSizeBytes } from "./storage.js";
20
+ import { estimateTokensFromChars, normalizeAiDefaults, normalizeAiLimits, recordAiActionEvent } from "./ai.js";
21
+ import { sha256Hex, stableJson, validateRaybaseName } from "./internal.js";
22
+ import { validateJsonSchema } from "./json-schema.js";
23
+ import { nextCronRun, validateCron } from "./jobs.js";
24
+ import { id, integer, longText, raybaseSystemTable, text } from "./schema.js";
25
+ import { setting } from "./settings.js";
26
+ import { nowIso, q, randomId } from "./sql.js";
27
+ import { appNowMs, wallNowIso, wallNowMs } from "./clock.js";
28
+ export const defaultAgentCompaction = {
29
+ enabled: true,
30
+ keepRecentMessages: 16,
31
+ triggerRatio: 0.7,
32
+ minSummaryOutputTokens: 800,
33
+ summarySpanRatio: 0.12,
34
+ maxSummaryOutputTokens: 4_000
35
+ };
36
+ /** Default cap for custom prompt bands. The built-in head has no default cap. */
37
+ export const defaultAgentPromptBandMaxChars = 16_000;
38
+ export const defaultAgentDeliveryMaxReplacementChars = 32_000;
39
+ export const maxAgentDeliveryReplacementChars = 128_000;
40
+ export const defaultAgentAttachmentMediaTypes = [
41
+ "image/jpeg",
42
+ "image/png",
43
+ "image/webp",
44
+ "image/gif"
45
+ ];
46
+ export const defaultAgentAttachmentMaxPerMessage = 4;
47
+ export const defaultAgentAttachmentMaxBytesPerFile = 8 * 1024 * 1024;
48
+ export const defaultAgentAttachmentRecentMessages = 8;
49
+ export const raybaseAiChatHarness = {
50
+ runtime: "ai-chat",
51
+ durableObjectBinding: "RAYBASE_AI_CHAT_AGENT",
52
+ triggerType: "chat",
53
+ instanceStrategy: "conversation"
54
+ };
55
+ export const raybaseAgentInternalReadPath = "/__raybase_agent_read";
56
+ export const raybaseAgentInternalUserReadPath = "/__raybase_agent_user_read";
57
+ export const raybaseAgentInternalCleanupPath = "/__raybase_agent_cleanup";
58
+ export const raybaseAgentInternalUserPurgePath = "/__raybase_agent_user_purge";
59
+ export const raybaseAgentInternalRehydratePath = "/__raybase_agent_rehydrate";
60
+ export const raybaseAgentInternalRoomNoticePath = "/__raybase_agent_room_notice";
61
+ export const raybaseAgentInternalRoomActivityPath = "/__raybase_agent_room_activity";
62
+ export const raybaseAgentInternalRoomMembershipPath = "/__raybase_agent_room_membership";
63
+ export const raybaseAgentInternalRoomRedactPath = "/__raybase_agent_room_redact";
64
+ export const raybaseAgentSystemTables = [
65
+ ...raybaseAgentChapterSystemTables,
66
+ ...raybaseAgentChapterArchiveSystemTables,
67
+ ...raybaseAgentRoomSystemTables,
68
+ raybaseSystemTable("_agent_prompt_bands", {
69
+ id: id(),
70
+ agentName: text({ required: true }),
71
+ conversationId: text({ required: true }),
72
+ userId: text(),
73
+ bandName: text({ required: true }),
74
+ epochKey: text({ required: true }),
75
+ included: integer({ required: true }),
76
+ content: longText(),
77
+ totalChars: integer(),
78
+ truncatedAt: integer(),
79
+ updatedAt: text({ required: true })
80
+ }, {
81
+ indexes: [
82
+ ["agentName", "conversationId"],
83
+ ["userId"],
84
+ ["conversationId", "bandName"]
85
+ ]
86
+ }),
87
+ raybaseSystemTable("_agent_instances", {
88
+ id: id(),
89
+ conversationId: text({ required: true, unique: true }),
90
+ agentName: text({ required: true }),
91
+ agentTitle: text({ required: true }),
92
+ agentOwner: text(),
93
+ moduleId: text(),
94
+ runtime: text({ required: true }),
95
+ triggerType: text({ required: true }),
96
+ instanceKey: text({ required: true, unique: true }),
97
+ principalKind: text({ required: true }),
98
+ userId: text(),
99
+ publicScopeHash: text(),
100
+ organizationId: text(),
101
+ route: text(),
102
+ surface: text({ required: true }),
103
+ status: text({ required: true }),
104
+ cleanupRequestedAt: text(),
105
+ hotStorageDeletedAt: text(),
106
+ deletedAt: text(),
107
+ retentionReason: text(),
108
+ pinnedAt: text(),
109
+ createdAt: text({ required: true }),
110
+ updatedAt: text({ required: true }),
111
+ lastActiveAt: text({ required: true }),
112
+ expiresAt: text(),
113
+ lastRunId: text(),
114
+ lastError: longText(),
115
+ metadataJson: longText(),
116
+ entryKind: text(),
117
+ entryJson: longText(),
118
+ kind: text(),
119
+ roomMembershipRev: integer(),
120
+ lastReadAt: text()
121
+ }, {
122
+ indexes: [
123
+ ["agentName", "conversationId"],
124
+ ["principalKind", "userId", "updatedAt"],
125
+ ["principalKind", "publicScopeHash", "updatedAt"],
126
+ ["status", "expiresAt"],
127
+ ["instanceKey"]
128
+ ]
129
+ }),
130
+ raybaseSystemTable("_agent_runs", {
131
+ runId: id(),
132
+ conversationId: text({ required: true }),
133
+ agentName: text({ required: true }),
134
+ requestId: text({ required: true }),
135
+ triggerType: text({ required: true }),
136
+ inputMessageId: text(),
137
+ status: text({ required: true }),
138
+ startedAt: text({ required: true }),
139
+ /**
140
+ * The last moment this run was known to be making progress.
141
+ *
142
+ * `startedAt` cannot answer "is it still alive": it never moves, so a
143
+ * turn working steadily for six minutes and a turn whose writer died in
144
+ * its first second look identical to any reader outside the isolate. The
145
+ * staleness sweep read `startedAt`, could not tell those apart, and said
146
+ * so in its own comments — then failed live runs anyway (found 2026-08-25:
147
+ * a member watched "Birch ran into a problem while answering" arrive 24
148
+ * seconds before the same run delivered its answer).
149
+ *
150
+ * It is touched per model step rather than on a timer, and that is the
151
+ * point: a wedged turn stops beating and is still reaped, which is what
152
+ * the horizon is for. Null on rows written before the column existed, and
153
+ * every predicate coalesces to `startedAt` so those behave exactly as
154
+ * they used to.
155
+ */
156
+ heartbeatAt: text(),
157
+ finishedAt: text(),
158
+ error: longText(),
159
+ usageEventId: text(),
160
+ auditEventId: text(),
161
+ metadataJson: longText()
162
+ }, { indexes: [["conversationId", "startedAt"], ["agentName", "startedAt"], ["status", "startedAt"], ["requestId"]] }),
163
+ raybaseSystemTable("_agent_requests", {
164
+ id: id(),
165
+ conversationId: text({ required: true }),
166
+ requestKey: text({ required: true }),
167
+ messageId: text({ required: true }),
168
+ kind: text(),
169
+ authorUserId: text(),
170
+ disposition: text(),
171
+ runId: text(),
172
+ seq: integer(),
173
+ createdAt: text({ required: true }),
174
+ updatedAt: text({ required: true })
175
+ }, {
176
+ indexes: [
177
+ { columns: ["conversationId", "requestKey"], unique: true },
178
+ ["conversationId", "createdAt"],
179
+ ["runId"]
180
+ ]
181
+ }),
182
+ raybaseSystemTable("_agent_limit_windows", {
183
+ id: id(),
184
+ scope: text({ required: true }),
185
+ scopeId: text({ required: true }),
186
+ windowStart: text({ required: true }),
187
+ windowEnd: text({ required: true }),
188
+ requestCount: integer({ required: true }),
189
+ estimatedOutputTokens: integer({ required: true }),
190
+ updatedAt: text({ required: true })
191
+ }, { indexes: [{ columns: ["scope", "scopeId", "windowStart"], unique: true }, ["windowEnd"]] }),
192
+ raybaseSystemTable("_agent_schedules", {
193
+ id: id(),
194
+ agentName: text({ required: true }),
195
+ triggerIndex: integer({ required: true }),
196
+ triggerKey: text({ required: true }),
197
+ inputJson: longText(),
198
+ cron: text({ required: true }),
199
+ timezone: text(),
200
+ enabled: integer({ required: true }),
201
+ userMessage: longText(),
202
+ cronOverride: text(),
203
+ timezoneOverride: text(),
204
+ enabledOverride: integer(),
205
+ inputJsonOverride: longText(),
206
+ inputJsonOverrideEnabled: integer(),
207
+ userMessageOverride: longText(),
208
+ lastRunAt: text(),
209
+ nextRunAt: text(),
210
+ service: text(),
211
+ dispatchGeneration: integer(),
212
+ dispatchAttempts: integer(),
213
+ dispatchLeaseExpiresAt: text(),
214
+ lastDispatchAt: text(),
215
+ lastDispatchError: longText(),
216
+ createdAt: text({ required: true }),
217
+ updatedAt: text({ required: true })
218
+ }, {
219
+ indexes: [
220
+ ["agentName", "triggerIndex"],
221
+ ["enabled", "nextRunAt"],
222
+ ["enabledOverride", "nextRunAt"],
223
+ ["triggerKey"]
224
+ ]
225
+ }),
226
+ raybaseSystemTable("_agent_trigger_events", {
227
+ id: id(),
228
+ agentName: text({ required: true }),
229
+ triggerType: text({ required: true }),
230
+ triggerKey: text({ required: true }),
231
+ sourceTable: text(),
232
+ sourceOp: text(),
233
+ rowId: text(),
234
+ inputJson: longText(),
235
+ idempotencyKey: text({ unique: true }),
236
+ status: text({ required: true }),
237
+ runId: text(),
238
+ error: longText(),
239
+ createdAt: text({ required: true }),
240
+ updatedAt: text({ required: true })
241
+ }, {
242
+ indexes: [
243
+ ["agentName", "createdAt"],
244
+ ["triggerType", "status"],
245
+ ["sourceTable", "sourceOp", "createdAt"],
246
+ ["runId"]
247
+ ]
248
+ }),
249
+ ...raybaseAgentWakeSystemTables,
250
+ ...raybaseAgentFollowUpSystemTables,
251
+ ...raybaseAgentMemorySystemTables,
252
+ ...raybaseAgentSkillSystemTables
253
+ ];
254
+ export const defaultAgentRunHistoryLimit = 20;
255
+ export const maxAgentRunHistoryLimit = 100;
256
+ const agentNamePattern = /^[A-Za-z][A-Za-z0-9_.:-]{0,127}$/;
257
+ const encoder = new TextEncoder();
258
+ const decoder = new TextDecoder();
259
+ const defaultConversationTtlSeconds = 60 * 60 * 4;
260
+ const websocketBootstrapTtlSeconds = 30;
261
+ const staleDeletingRetrySeconds = 10 * 60;
262
+ const retryableCleanupStatuses = new Set(["active", "cleanupRequested", "cleanupError"]);
263
+ const dataTriggerEventPattern = /^([A-Za-z][A-Za-z0-9_:-]*)\.(create|update|delete)$/;
264
+ const agentEntryKindPattern = /^[a-z][a-z0-9-]{0,63}$/;
265
+ export const maxAgentEntryPayloadBytes = 4 * 1024;
266
+ export const maxAgentEntryKickoffChars = 4 * 1024;
267
+ // Composed-turn and entry-kickoff daily caps ship with no default: unset means
268
+ // unlimited. Both are runaway-loop guards in quota clothing, and the runtime
269
+ // already serializes one turn per conversation and bounds loop steps per turn.
270
+ // Apps that want a cap declare limits.composedPerDay / limits.entryKickoffPerDay
271
+ // and get exactly the enforcement below.
272
+ export function defineAgent(definition) {
273
+ validateAgentName(definition.name);
274
+ const runtime = definition.runtime;
275
+ if (runtime !== "ai-chat")
276
+ throw new Error(`Unsupported Raybase agent runtime: ${String(runtime)}`);
277
+ const unexpected = definition;
278
+ if (unexpected.scope !== undefined || unexpected.instance !== undefined) {
279
+ throw new Error("Raybase agent scope and instance fields are internal and cannot be declared by apps");
280
+ }
281
+ if (definition.prompt && definition.system !== undefined) {
282
+ throw new Error("Raybase agents cannot declare both system and prompt; use prompt.default for configurable prompt defaults");
283
+ }
284
+ if (definition.prompt?.configurable === true && !definition.prompt.default.trim()) {
285
+ throw new Error("Configurable Raybase agent prompts must provide a non-empty default prompt");
286
+ }
287
+ validateAgentConversationTitles(definition);
288
+ validateAgentSkillsForAgent(definition);
289
+ validateAgentSkillStoreForAgent(definition);
290
+ validateAgentRemoteMcpBindings(definition);
291
+ validateAgentMemoryForAgent(definition);
292
+ validateAgentWakesForAgent(definition);
293
+ validateAgentFollowUpsForAgent(definition);
294
+ validateAgentRetentionForAgent(definition);
295
+ validateAgentAllRuntimeToolNames(definition);
296
+ validateAgentCompaction(definition.compaction);
297
+ if (definition.limits?.composedPerDay !== undefined
298
+ && (!Number.isInteger(definition.limits.composedPerDay) || definition.limits.composedPerDay < 1)) {
299
+ throw new Error(`Raybase agent ${definition.name} limits.composedPerDay must be a positive integer`);
300
+ }
301
+ if (definition.limits?.entryKickoffPerDay !== undefined
302
+ && (!Number.isInteger(definition.limits.entryKickoffPerDay) || definition.limits.entryKickoffPerDay < 1)) {
303
+ throw new Error(`Raybase agent ${definition.name} limits.entryKickoffPerDay must be a positive integer`);
304
+ }
305
+ if (definition.limits?.loopReserveChars !== undefined
306
+ && (!Number.isInteger(definition.limits.loopReserveChars) || definition.limits.loopReserveChars < 0)) {
307
+ throw new Error(`Raybase agent ${definition.name} limits.loopReserveChars must be a non-negative integer`);
308
+ }
309
+ if (definition.compose?.renderContext !== undefined && typeof definition.compose.renderContext !== "boolean") {
310
+ throw new Error(`Raybase agent ${definition.name} compose.renderContext must be a boolean`);
311
+ }
312
+ if (definition.timeSentinel !== undefined && typeof definition.timeSentinel !== "boolean") {
313
+ throw new Error(`Raybase agent ${definition.name} timeSentinel must be a boolean`);
314
+ }
315
+ validateAgentExtensionPoints(definition);
316
+ const scopes = normalizeAgentScopes(definition.scopes, definition.name);
317
+ const triggers = normalizeAgentTriggers(definition.triggers, agentMemoryMaintenanceRuns(definition));
318
+ const normalized = { ...definition, runtime, triggers, ...(scopes ? { scopes } : {}) };
319
+ validateAgentEntriesForAgent(normalized);
320
+ validateAgentRoomsForAgent(normalized);
321
+ return normalized;
322
+ }
323
+ function normalizeAgentScopes(scopes, agentName) {
324
+ if (scopes === undefined)
325
+ return undefined;
326
+ if (!Array.isArray(scopes) || scopes.some((scope) => typeof scope !== "string" || !scope.trim())) {
327
+ throw new Error(`Raybase agent ${agentName} scopes must be an array of non-empty strings`);
328
+ }
329
+ return [...new Set(scopes.map((scope) => scope.trim()))];
330
+ }
331
+ function validateAgentExtensionPoints(agent) {
332
+ if (agent.turnState) {
333
+ if (typeof agent.turnState.load !== "function")
334
+ throw new Error(`Raybase agent ${agent.name} turnState.load must be a function`);
335
+ validateAgentHookTimeout(agent.name, "turnState", agent.turnState.timeoutMs);
336
+ validateAgentHookFailure(agent.name, "turnState", agent.turnState.onFailure);
337
+ const seenServicePrincipals = new Set();
338
+ for (const service of agent.turnState.servicePrincipals ?? []) {
339
+ if (!/^[A-Za-z][A-Za-z0-9_.:-]{0,127}$/.test(service)) {
340
+ throw new Error(`Raybase agent ${agent.name} turnState.servicePrincipals contains invalid service name ${String(service)}`);
341
+ }
342
+ if (seenServicePrincipals.has(service)) {
343
+ throw new Error(`Raybase agent ${agent.name} turnState.servicePrincipals declares ${service} more than once`);
344
+ }
345
+ seenServicePrincipals.add(service);
346
+ }
347
+ }
348
+ if (agent.promptHead !== undefined
349
+ && (!agent.promptHead
350
+ || typeof agent.promptHead !== "object"
351
+ || Array.isArray(agent.promptHead)
352
+ || (agent.promptHead.epoch !== "chapter" && agent.promptHead.epoch !== "turn"))) {
353
+ throw new Error(`Raybase agent ${agent.name} promptHead.epoch must be chapter or turn`);
354
+ }
355
+ if (agent.promptHead?.maxChars !== undefined
356
+ && (!Number.isInteger(agent.promptHead.maxChars) || agent.promptHead.maxChars < 1)) {
357
+ throw new Error(`Raybase agent ${agent.name} promptHead.maxChars must be a positive integer`);
358
+ }
359
+ if (agent.promptHead?.onOverflow !== undefined
360
+ && agent.promptHead.onOverflow !== "truncate"
361
+ && agent.promptHead.onOverflow !== "error") {
362
+ throw new Error(`Raybase agent ${agent.name} promptHead.onOverflow must be truncate or error`);
363
+ }
364
+ validateNamedAgentHooks(agent.name, "prompt band", agent.promptBands, ["chapter", "turn"]);
365
+ validateNamedAgentHooks(agent.name, "sentinel", agent.sentinels);
366
+ if (agent.delivery) {
367
+ if (agent.delivery.mode !== undefined && typeof agent.delivery.mode !== "function") {
368
+ throw new Error(`Raybase agent ${agent.name} delivery.mode must be a function`);
369
+ }
370
+ if (agent.delivery.gate !== undefined && typeof agent.delivery.gate !== "function") {
371
+ throw new Error(`Raybase agent ${agent.name} delivery.gate must be a function`);
372
+ }
373
+ if (agent.delivery.maxRetries !== undefined
374
+ && (!Number.isInteger(agent.delivery.maxRetries) || agent.delivery.maxRetries < 0 || agent.delivery.maxRetries > 2)) {
375
+ throw new Error(`Raybase agent ${agent.name} delivery.maxRetries must be an integer between 0 and 2`);
376
+ }
377
+ if (agent.delivery.maxReplacementChars !== undefined
378
+ && (!Number.isInteger(agent.delivery.maxReplacementChars)
379
+ || agent.delivery.maxReplacementChars < 1
380
+ || agent.delivery.maxReplacementChars > maxAgentDeliveryReplacementChars)) {
381
+ throw new Error(`Raybase agent ${agent.name} delivery.maxReplacementChars must be a positive integer no greater than ${maxAgentDeliveryReplacementChars}`);
382
+ }
383
+ if (agent.delivery.fallback !== undefined && agent.delivery.fallback !== "deliver" && agent.delivery.fallback !== "drop") {
384
+ throw new Error(`Raybase agent ${agent.name} delivery.fallback must be deliver or drop`);
385
+ }
386
+ if (agent.delivery.silenceSentinel !== undefined) {
387
+ const sentinel = agent.delivery.silenceSentinel;
388
+ for (const field of ["text", "silence"]) {
389
+ const value = sentinel?.[field];
390
+ if (typeof value !== "string" || value.length === 0 || value.length > 64 || value.trim() !== value) {
391
+ throw new Error(`Raybase agent ${agent.name} delivery.silenceSentinel.${field} must be a trimmed string of 1 to 64 characters`);
392
+ }
393
+ }
394
+ }
395
+ validateAgentHookTimeout(agent.name, "delivery", agent.delivery.timeoutMs);
396
+ validateAgentHookFailure(agent.name, "delivery", agent.delivery.onFailure);
397
+ }
398
+ validateAgentConversationsConfig(agent);
399
+ if (agent.chapterClose) {
400
+ for (const key of ["prompt"]) {
401
+ const value = agent.chapterClose[key];
402
+ if (value !== undefined && (typeof value !== "string" || !value.trim())) {
403
+ throw new Error(`Raybase agent ${agent.name} chapterClose.${key} must be a non-empty string`);
404
+ }
405
+ }
406
+ }
407
+ }
408
+ function validateAgentConversationsConfig(agent) {
409
+ const conversations = agent.conversations;
410
+ if (conversations === undefined)
411
+ return;
412
+ if (!conversations || typeof conversations !== "object" || Array.isArray(conversations)) {
413
+ throw new Error(`Raybase agent ${agent.name} conversations must be an object`);
414
+ }
415
+ if (conversations.eligibility !== undefined && typeof conversations.eligibility !== "function") {
416
+ throw new Error(`Raybase agent ${agent.name} conversations.eligibility must be a function`);
417
+ }
418
+ if (conversations.onCommit !== undefined && typeof conversations.onCommit !== "function") {
419
+ throw new Error(`Raybase agent ${agent.name} conversations.onCommit must be a function`);
420
+ }
421
+ if (conversations.onSilence !== undefined && typeof conversations.onSilence !== "function") {
422
+ throw new Error(`Raybase agent ${agent.name} conversations.onSilence must be a function`);
423
+ }
424
+ if (conversations.onRead !== undefined && typeof conversations.onRead !== "function") {
425
+ throw new Error(`Raybase agent ${agent.name} conversations.onRead must be a function`);
426
+ }
427
+ if (conversations.onMessageMutation !== undefined && typeof conversations.onMessageMutation !== "function") {
428
+ throw new Error(`Raybase agent ${agent.name} conversations.onMessageMutation must be a function`);
429
+ }
430
+ validateAgentHookTimeout(agent.name, "conversations.eligibility", conversations.eligibilityTimeoutMs);
431
+ validateAgentHookFailure(agent.name, "conversations.eligibility", conversations.onEligibilityFailure);
432
+ const messages = conversations.messages;
433
+ if (messages === undefined)
434
+ return;
435
+ if (!messages || typeof messages !== "object" || Array.isArray(messages)) {
436
+ throw new Error(`Raybase agent ${agent.name} conversations.messages must be an object`);
437
+ }
438
+ for (const key of ["edit", "remove"]) {
439
+ const config = messages[key];
440
+ if (config === undefined)
441
+ continue;
442
+ if (!config
443
+ || typeof config !== "object"
444
+ || Array.isArray(config)
445
+ || !Number.isInteger(config.windowMs)
446
+ || config.windowMs < 1) {
447
+ throw new Error(`Raybase agent ${agent.name} conversations.messages.${key}.windowMs must be a positive integer`);
448
+ }
449
+ }
450
+ if (messages.reactions !== undefined
451
+ && (!messages.reactions
452
+ || typeof messages.reactions !== "object"
453
+ || Array.isArray(messages.reactions)
454
+ || messages.reactions.enabled !== true
455
+ || (messages.reactions.agentTool !== undefined && typeof messages.reactions.agentTool !== "boolean"))) {
456
+ throw new Error(`Raybase agent ${agent.name} conversations.messages.reactions must enable reactions and may set agentTool`);
457
+ }
458
+ const attachments = messages.attachments;
459
+ if (attachments !== undefined) {
460
+ if (!attachments
461
+ || typeof attachments !== "object"
462
+ || Array.isArray(attachments)
463
+ || attachments.enabled !== true
464
+ || typeof attachments.bucket !== "string"
465
+ || !attachments.bucket.trim()) {
466
+ throw new Error(`Raybase agent ${agent.name} conversations.messages.attachments must enable attachments and name a bucket`);
467
+ }
468
+ validateOptionalPositiveInteger(agent.name, "maxPerMessage", attachments.maxPerMessage);
469
+ validateOptionalPositiveInteger(agent.name, "maxBytesPerFile", attachments.maxBytesPerFile);
470
+ if ((attachments.maxBytesPerFile ?? 0) > storageHardMaxSizeBytes) {
471
+ throw new Error(`Raybase agent ${agent.name} conversations.messages.attachments.maxBytesPerFile exceeds the storage hard cap`);
472
+ }
473
+ if (attachments.mediaTypes !== undefined) {
474
+ if (!Array.isArray(attachments.mediaTypes)
475
+ || !attachments.mediaTypes.length
476
+ || attachments.mediaTypes.some((value) => typeof value !== "string" || !/^image\/[A-Za-z0-9.+-]+$/.test(value))) {
477
+ throw new Error(`Raybase agent ${agent.name} conversations.messages.attachments.mediaTypes must contain image media types`);
478
+ }
479
+ }
480
+ const recentMessages = attachments.resolveForModel?.recentMessages;
481
+ if (attachments.resolveForModel !== undefined
482
+ && (!attachments.resolveForModel
483
+ || typeof attachments.resolveForModel !== "object"
484
+ || Array.isArray(attachments.resolveForModel)
485
+ || (recentMessages !== undefined && (!Number.isInteger(recentMessages) || recentMessages < 0)))) {
486
+ throw new Error(`Raybase agent ${agent.name} conversations.messages.attachments.resolveForModel.recentMessages must be a non-negative integer`);
487
+ }
488
+ }
489
+ const voiceNotes = messages.voiceNotes;
490
+ if (voiceNotes !== undefined) {
491
+ if (!voiceNotes
492
+ || typeof voiceNotes !== "object"
493
+ || Array.isArray(voiceNotes)
494
+ || voiceNotes.enabled !== true) {
495
+ throw new Error(`Raybase agent ${agent.name} conversations.messages.voiceNotes must enable voice notes`);
496
+ }
497
+ if (!attachments?.enabled) {
498
+ throw new Error(`Raybase agent ${agent.name} conversations.messages.voiceNotes requires attachments to be enabled`);
499
+ }
500
+ validateOptionalPositiveInteger(agent.name, "voiceNotes.maxSeconds", voiceNotes.maxSeconds);
501
+ }
502
+ validateAgentUserDataPartsConfig(agent.name, messages.userDataParts);
503
+ }
504
+ function validateOptionalPositiveInteger(agentName, key, value) {
505
+ if (value !== undefined && (!Number.isInteger(value) || value < 1)) {
506
+ const path = key.startsWith("voiceNotes.") ? key : `attachments.${key}`;
507
+ throw new Error(`Raybase agent ${agentName} conversations.messages.${path} must be a positive integer`);
508
+ }
509
+ }
510
+ export function agentConversationAttachmentsConfig(agent) {
511
+ const config = agent.conversations?.messages?.attachments;
512
+ if (!config)
513
+ return null;
514
+ return {
515
+ enabled: true,
516
+ bucket: config.bucket.trim(),
517
+ maxPerMessage: config.maxPerMessage ?? defaultAgentAttachmentMaxPerMessage,
518
+ maxBytesPerFile: config.maxBytesPerFile ?? defaultAgentAttachmentMaxBytesPerFile,
519
+ mediaTypes: [...(config.mediaTypes ?? defaultAgentAttachmentMediaTypes)],
520
+ resolveForModel: {
521
+ recentMessages: config.resolveForModel?.recentMessages ?? defaultAgentAttachmentRecentMessages
522
+ }
523
+ };
524
+ }
525
+ export function agentConversationVoiceNotesConfig(agent) {
526
+ const attachments = agentConversationAttachmentsConfig(agent);
527
+ if (!attachments)
528
+ return null;
529
+ return normalizeAgentVoiceNotesConfig(agent.conversations?.messages?.voiceNotes, attachments.maxBytesPerFile);
530
+ }
531
+ export function validateAgentAttachmentStorage(agents, storage) {
532
+ for (const agent of Object.values(agents)) {
533
+ const attachments = agentConversationAttachmentsConfig(agent);
534
+ if (!attachments)
535
+ continue;
536
+ if (!storage?.buckets[attachments.bucket]) {
537
+ throw new Error(`Raybase agent ${agent.name} attachment bucket ${attachments.bucket} is not configured in storage`);
538
+ }
539
+ const voiceNotes = agentConversationVoiceNotesConfig(agent);
540
+ const acceptedMediaTypes = storage.buckets[attachments.bucket]?.accept;
541
+ if (voiceNotes && acceptedMediaTypes && !acceptedMediaTypes.includes("audio/wav")) {
542
+ throw new Error(`Raybase agent ${agent.name} voice note bucket ${attachments.bucket} must accept audio/wav`);
543
+ }
544
+ }
545
+ }
546
+ export function agentConversationEligibilityConfig(agent, conversationKind) {
547
+ const conversations = agent.conversations;
548
+ if (conversationKind !== "room")
549
+ return conversations ?? {};
550
+ return {
551
+ eligibility: agent.rooms?.eligibility ?? conversations?.eligibility,
552
+ eligibilityTimeoutMs: agent.rooms?.eligibilityTimeoutMs ?? conversations?.eligibilityTimeoutMs,
553
+ onEligibilityFailure: agent.rooms?.onEligibilityFailure ?? conversations?.onEligibilityFailure
554
+ };
555
+ }
556
+ function validateNamedAgentHooks(agentName, label, hooks, epochs) {
557
+ if (hooks === undefined)
558
+ return;
559
+ if (!Array.isArray(hooks))
560
+ throw new Error(`Raybase agent ${agentName} ${label}s must be an array`);
561
+ const seen = new Set();
562
+ for (const [index, hook] of hooks.entries()) {
563
+ if (!hook || typeof hook !== "object")
564
+ throw new Error(`Raybase agent ${agentName} ${label} at index ${index} must be an object`);
565
+ validateRaybaseName("agent", hook.name);
566
+ if (seen.has(hook.name))
567
+ throw new Error(`Raybase agent ${agentName} declares duplicate ${label} ${hook.name}`);
568
+ seen.add(hook.name);
569
+ if (typeof hook.render !== "function")
570
+ throw new Error(`Raybase agent ${agentName} ${label} ${hook.name} render must be a function`);
571
+ if (hook.when !== undefined && typeof hook.when !== "function") {
572
+ throw new Error(`Raybase agent ${agentName} ${label} ${hook.name} when must be a function`);
573
+ }
574
+ if (epochs && hook.epoch !== undefined && !epochs.includes(String(hook.epoch))) {
575
+ throw new Error(`Raybase agent ${agentName} ${label} ${hook.name} has invalid epoch ${String(hook.epoch)}`);
576
+ }
577
+ if (hook.maxChars !== undefined && (!Number.isInteger(hook.maxChars) || hook.maxChars < 1)) {
578
+ throw new Error(`Raybase agent ${agentName} ${label} ${hook.name} maxChars must be a positive integer`);
579
+ }
580
+ if (hook.onOverflow !== undefined && hook.onOverflow !== "truncate" && hook.onOverflow !== "error") {
581
+ throw new Error(`Raybase agent ${agentName} ${label} ${hook.name} onOverflow must be truncate or error`);
582
+ }
583
+ if (hook.trust !== undefined && (label !== "sentinel" || hook.trust !== "app" && hook.trust !== "data")) {
584
+ throw new Error(`Raybase agent ${agentName} ${label} ${hook.name} trust must be app or data`);
585
+ }
586
+ }
587
+ }
588
+ function validateAgentHookTimeout(agentName, label, timeoutMs) {
589
+ if (timeoutMs !== undefined && (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000)) {
590
+ throw new Error(`Raybase agent ${agentName} ${label}.timeoutMs must be between 1 and 60000`);
591
+ }
592
+ }
593
+ function validateAgentHookFailure(agentName, label, failure) {
594
+ if (failure !== undefined && failure !== "fail-turn" && failure !== "continue-empty") {
595
+ throw new Error(`Raybase agent ${agentName} ${label}.onFailure must be fail-turn or continue-empty`);
596
+ }
597
+ }
598
+ export function validateAgentEntriesForAgent(agent) {
599
+ const entries = agent.entries;
600
+ if (entries === undefined)
601
+ return;
602
+ if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
603
+ throw new Error(`Raybase agent ${agent.name} entries must be an object`);
604
+ }
605
+ if (!agent.triggers?.some((trigger) => trigger.type === "chat")) {
606
+ throw new Error(`Raybase agent ${agent.name} declares entries but has no chat trigger`);
607
+ }
608
+ for (const [kind, definition] of Object.entries(entries)) {
609
+ if (!agentEntryKindPattern.test(kind)) {
610
+ throw new Error(`Raybase agent ${agent.name} entry kind "${kind}" must be a lowercase slug`);
611
+ }
612
+ if (!definition || typeof definition !== "object" || Array.isArray(definition)) {
613
+ throw new Error(`Raybase agent ${agent.name} entry ${kind} must be an object`);
614
+ }
615
+ if (typeof definition.description !== "string" || !definition.description.trim()) {
616
+ throw new Error(`Raybase agent ${agent.name} entry ${kind} requires a description`);
617
+ }
618
+ const source = definition.source ?? "inline";
619
+ if (source !== "inline" && source !== "notification") {
620
+ throw new Error(`Raybase agent ${agent.name} entry ${kind} has an invalid source`);
621
+ }
622
+ if (definition.payloadSchema !== undefined)
623
+ validateAgentEntrySchema(agent.name, kind, definition.payloadSchema);
624
+ if (definition.userMessage !== undefined) {
625
+ if (typeof definition.userMessage !== "string" || !definition.userMessage.trim()) {
626
+ throw new Error(`Raybase agent ${agent.name} entry ${kind} has an invalid userMessage`);
627
+ }
628
+ if (definition.userMessage.length > maxAgentEntryKickoffChars) {
629
+ throw new Error(`Raybase agent ${agent.name} entry ${kind} userMessage exceeds ${maxAgentEntryKickoffChars} characters`);
630
+ }
631
+ }
632
+ const audiences = definition.audience ?? ["signed-in"];
633
+ if (!Array.isArray(audiences) || audiences.length === 0 || audiences.some((audience) => audience !== "signed-in" && audience !== "public")) {
634
+ throw new Error(`Raybase agent ${agent.name} entry ${kind} has an invalid audience`);
635
+ }
636
+ if (definition.userMessage && audiences.includes("public")) {
637
+ throw new Error(`Raybase agent ${agent.name} entry ${kind} cannot declare a kickoff userMessage for a public audience`);
638
+ }
639
+ }
640
+ }
641
+ function validateAgentEntrySchema(agentName, kind, schema) {
642
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
643
+ throw new Error(`Raybase agent ${agentName} entry ${kind} payloadSchema must be a JSON Schema object`);
644
+ }
645
+ const allowedTypes = new Set(["object", "array", "string", "number", "integer", "boolean", "null"]);
646
+ const visit = (node, path) => {
647
+ if (!node || typeof node !== "object" || Array.isArray(node))
648
+ throw new Error(`${path} must be a JSON Schema object`);
649
+ if (node.type !== undefined && (typeof node.type !== "string" || !allowedTypes.has(node.type)))
650
+ throw new Error(`${path}.type is unsupported`);
651
+ if (node.properties !== undefined) {
652
+ if (!node.properties || typeof node.properties !== "object" || Array.isArray(node.properties))
653
+ throw new Error(`${path}.properties must be an object`);
654
+ for (const [key, child] of Object.entries(node.properties))
655
+ visit(child, `${path}.properties.${key}`);
656
+ }
657
+ if (node.items !== undefined)
658
+ visit(node.items, `${path}.items`);
659
+ };
660
+ visit(schema, `Raybase agent ${agentName} entry ${kind} payloadSchema`);
661
+ }
662
+ export function agentEntryDefinition(agent, kind) {
663
+ const definition = agent.entries?.[kind];
664
+ if (!definition)
665
+ throw new AgentHttpError(`Unknown agent entry kind: ${kind}`, 400);
666
+ return definition;
667
+ }
668
+ export function agentEntryAudiences(definition) {
669
+ return uniqueAudiences(definition.audience ?? ["signed-in"]);
670
+ }
671
+ export function validateInlineAgentEntryPayload(definition, payload) {
672
+ let serialized;
673
+ try {
674
+ serialized = stableJson(payload ?? null);
675
+ }
676
+ catch {
677
+ throw new AgentHttpError("Agent entry payload must be JSON serializable", 400);
678
+ }
679
+ if (encoder.encode(serialized).byteLength > maxAgentEntryPayloadBytes) {
680
+ throw new AgentHttpError(`Agent entry payload exceeds ${maxAgentEntryPayloadBytes} bytes`, 400);
681
+ }
682
+ if (!definition.payloadSchema)
683
+ return payload;
684
+ try {
685
+ return validateJsonSchema(definition.payloadSchema, payload, "entry.payload");
686
+ }
687
+ catch (error) {
688
+ throw new AgentHttpError(error instanceof Error ? error.message : "Agent entry payload is invalid", 400);
689
+ }
690
+ }
691
+ export function agentEntryFromInstance(instance) {
692
+ if (!instance.entryKind || !instance.entryJson)
693
+ return undefined;
694
+ try {
695
+ const parsed = JSON.parse(instance.entryJson);
696
+ if (parsed?.kind !== instance.entryKind || (parsed.source !== "inline" && parsed.source !== "notification"))
697
+ return undefined;
698
+ return parsed;
699
+ }
700
+ catch {
701
+ return undefined;
702
+ }
703
+ }
704
+ function validateAgentConversationTitles(agent) {
705
+ const config = agent.conversationTitles;
706
+ if (config === undefined || config === "auto")
707
+ return;
708
+ if (config && typeof config === "object" && !Array.isArray(config)) {
709
+ if (config.model === undefined || (typeof config.model === "string" && config.model.trim()))
710
+ return;
711
+ throw new Error(`Raybase agent ${agent.name} conversationTitles.model must be a non-empty string`);
712
+ }
713
+ throw new Error(`Raybase agent ${agent.name} conversationTitles must be "auto" or an object like { model }`);
714
+ }
715
+ export function agentConversationTitlesConfig(agent) {
716
+ const config = agent.conversationTitles;
717
+ if (config === undefined)
718
+ return null;
719
+ if (config === "auto")
720
+ return { model: null };
721
+ return { model: config.model?.trim() || null };
722
+ }
723
+ function validateAgentCompaction(compaction) {
724
+ if (compaction === undefined)
725
+ return;
726
+ if (!compaction || typeof compaction !== "object" || Array.isArray(compaction)) {
727
+ throw new Error("Raybase agent compaction must be an object");
728
+ }
729
+ if (compaction.enabled !== undefined && typeof compaction.enabled !== "boolean") {
730
+ throw new Error("Raybase agent compaction enabled must be a boolean");
731
+ }
732
+ if (compaction.model !== undefined && (typeof compaction.model !== "string" || !compaction.model.trim())) {
733
+ throw new Error("Raybase agent compaction model must be a non-empty string");
734
+ }
735
+ if (compaction.keepRecentMessages !== undefined && (!Number.isInteger(compaction.keepRecentMessages) || compaction.keepRecentMessages < 1)) {
736
+ throw new Error("Raybase agent compaction keepRecentMessages must be an integer of at least 1");
737
+ }
738
+ if (compaction.triggerRatio !== undefined && (typeof compaction.triggerRatio !== "number" || !(compaction.triggerRatio > 0) || compaction.triggerRatio > 1)) {
739
+ throw new Error("Raybase agent compaction triggerRatio must be a number greater than 0 and at most 1");
740
+ }
741
+ if (compaction.minSummaryOutputTokens !== undefined && (!Number.isInteger(compaction.minSummaryOutputTokens) || compaction.minSummaryOutputTokens < 1)) {
742
+ throw new Error("Raybase agent compaction minSummaryOutputTokens must be an integer of at least 1");
743
+ }
744
+ if (compaction.summarySpanRatio !== undefined && (typeof compaction.summarySpanRatio !== "number" || !(compaction.summarySpanRatio > 0) || compaction.summarySpanRatio > 1)) {
745
+ throw new Error("Raybase agent compaction summarySpanRatio must be a number greater than 0 and at most 1");
746
+ }
747
+ if (compaction.maxSummaryOutputTokens !== undefined && (!Number.isInteger(compaction.maxSummaryOutputTokens) || compaction.maxSummaryOutputTokens < 1)) {
748
+ throw new Error("Raybase agent compaction maxSummaryOutputTokens must be an integer of at least 1");
749
+ }
750
+ // Only checked when both are pinned: a lone maxSummaryOutputTokens pin below
751
+ // the default floor is the pre-proportional flat budget and must stay valid.
752
+ if (compaction.minSummaryOutputTokens !== undefined
753
+ && compaction.maxSummaryOutputTokens !== undefined
754
+ && compaction.minSummaryOutputTokens > compaction.maxSummaryOutputTokens) {
755
+ throw new Error("Raybase agent compaction minSummaryOutputTokens must not exceed maxSummaryOutputTokens");
756
+ }
757
+ }
758
+ function validateAgentAllRuntimeToolNames(agent) {
759
+ const seen = new Map();
760
+ const add = (toolName, source) => {
761
+ const existing = seen.get(toolName);
762
+ if (existing)
763
+ throw new Error(`Raybase agent ${agent.name} has duplicate runtime tool name "${toolName}" from ${existing} and ${source}`);
764
+ seen.set(toolName, source);
765
+ };
766
+ // Uniqueness always covers the full registered set; per-turn offer gates
767
+ // never relax it.
768
+ for (const { action } of agentActionRegistrations(agent.actions))
769
+ add(agentActionToolName(action.name), `direct action ${action.name}`);
770
+ for (const binding of agentSkillActionBindings(agent))
771
+ add(binding.modelToolName, `skill ${binding.skill.name} action ${binding.action.name}`);
772
+ const skillRegistrations = agentSkillRegistrations(agent.skills, agent.name);
773
+ if (skillRegistrations.some(({ policy, mode, skill }) => Boolean(skill.guidance) && (Boolean(policy) || mode !== "include" && mode !== "excluded")) || agent.skillStore?.enabled === true) {
774
+ add("load_agent_skill", "built-in skill loader");
775
+ }
776
+ if (skillRegistrations.some(({ policy, mode, skill }) => (Boolean(policy) || mode !== "excluded") && (Boolean(skill.files?.length) || Boolean(skill.guidance && skill.guidance.length > (skill.maxChars ?? (skill.ephemeral ? 4_000 : 8_000))))) || agent.skillStore?.enabled === true) {
777
+ add("read_skill", "built-in skill reader");
778
+ }
779
+ for (const binding of agentRemoteMcpToolBindings(agent))
780
+ add(binding.modelToolName, `remote MCP ${binding.server.name}/${binding.toolName}`);
781
+ if (agentMemorySpaces(agent).length) {
782
+ for (const toolName of raybaseAgentMemoryToolNames)
783
+ add(toolName, "built-in memory tool");
784
+ }
785
+ if (agent.skillStore?.enabled === true) {
786
+ const access = agent.skillStore.agentAccess ?? {};
787
+ for (const name of raybaseAgentSkillToolNames) {
788
+ const mutating = name === "skill_write" ? access.create : name === "skill_edit" ? access.edit : name === "skill_delete" ? access.delete : name === "skill_set_enabled" ? access.setEnabled : name.startsWith("skill_import_") ? Boolean(access.importSources?.length) : true;
789
+ if (mutating)
790
+ add(name, "built-in database skill tool");
791
+ }
792
+ }
793
+ if (agent.wakes?.enabled === true) {
794
+ for (const toolName of raybaseAgentWakeToolNames)
795
+ add(toolName, "built-in wake tool");
796
+ }
797
+ if (agent.followUps?.enabled === true) {
798
+ for (const toolName of ["follow_up", "list_follow_ups", "close_follow_up"])
799
+ add(toolName, "built-in follow-up tool");
800
+ }
801
+ if (agent.conversations?.messages?.reactions?.agentTool === true) {
802
+ add("react", "built-in conversation reaction tool");
803
+ }
804
+ }
805
+ function normalizeAgentTriggers(triggers, maintenanceRuns = []) {
806
+ const declared = triggers?.length ? triggers : [{ type: "chat" }];
807
+ const existingMaintenance = new Set();
808
+ for (const [index, trigger] of declared.entries()) {
809
+ if (trigger?.type === "schedule" && trigger.maintenance !== undefined) {
810
+ const run = maintenanceRuns.find((candidate) => candidate.name === trigger.maintenance);
811
+ if (!run
812
+ || trigger.cron !== run.schedule.cron
813
+ || (trigger.timezone ?? undefined) !== run.schedule.timezone
814
+ || trigger.userMessage !== run.instruction) {
815
+ throw new Error(`Raybase agent schedule trigger at index ${index} cannot declare maintenance directly; use memory.maintenance`);
816
+ }
817
+ existingMaintenance.add(trigger.maintenance);
818
+ }
819
+ }
820
+ const normalized = [
821
+ ...declared,
822
+ ...maintenanceRuns.filter((run) => !existingMaintenance.has(run.name)).map((run) => ({
823
+ type: "schedule",
824
+ cron: run.schedule.cron,
825
+ ...(run.schedule.timezone ? { timezone: run.schedule.timezone } : {}),
826
+ userMessage: run.instruction,
827
+ maintenance: run.name
828
+ }))
829
+ ];
830
+ const seen = new Set();
831
+ return normalized.map((trigger, index) => {
832
+ if (!trigger || typeof trigger !== "object")
833
+ throw new Error(`Raybase agent trigger at index ${index} must be an object`);
834
+ let key;
835
+ if (trigger.type === "chat") {
836
+ key = "chat";
837
+ }
838
+ else if (trigger.type === "schedule") {
839
+ if (typeof trigger.cron !== "string" || !trigger.cron.trim())
840
+ throw new Error(`Raybase agent schedule trigger at index ${index} requires cron`);
841
+ validateCron(trigger.cron);
842
+ if (trigger.timezone !== undefined && typeof trigger.timezone !== "string")
843
+ throw new Error(`Raybase agent schedule trigger at index ${index} has invalid timezone`);
844
+ if (trigger.userMessage !== undefined) {
845
+ if (typeof trigger.userMessage !== "string" || !trigger.userMessage.trim()) {
846
+ throw new Error(`Raybase agent schedule trigger at index ${index} has invalid userMessage`);
847
+ }
848
+ trigger = { ...trigger, userMessage: trigger.userMessage.trim() };
849
+ }
850
+ key = trigger.maintenance
851
+ ? `maintenance:${trigger.maintenance}`
852
+ : `schedule:${trigger.cron}:${trigger.timezone ?? "UTC"}:${stableJson(trigger.input ?? null)}`;
853
+ }
854
+ else if (trigger.type === "data") {
855
+ if (typeof trigger.event !== "string" || !dataTriggerEventPattern.test(trigger.event)) {
856
+ throw new Error(`Raybase agent data trigger at index ${index} must use table.create, table.update, or table.delete`);
857
+ }
858
+ key = `data:${trigger.event}:${trigger.includeAdmin === true}:${stableJson(trigger.input ?? null)}`;
859
+ }
860
+ else {
861
+ throw new Error(`Unsupported Raybase agent trigger type: ${String(trigger.type)}`);
862
+ }
863
+ if (seen.has(key))
864
+ throw new Error(`Duplicate Raybase agent trigger: ${key}`);
865
+ seen.add(key);
866
+ return trigger;
867
+ });
868
+ }
869
+ export function validateAgentTriggers(agents, schema) {
870
+ const tableNames = new Set(Object.values(schema.tables).map((table) => table.name));
871
+ for (const agent of Object.values(defineAgents(agents ?? {}))) {
872
+ agent.triggers.forEach((trigger, index) => {
873
+ if (trigger.type !== "data")
874
+ return;
875
+ const parsed = parseAgentDataTriggerEvent(trigger.event);
876
+ if (!tableNames.has(parsed.tableName)) {
877
+ throw new Error(`Raybase agent ${agent.name} data trigger ${index} references unknown table ${parsed.tableName}`);
878
+ }
879
+ });
880
+ }
881
+ }
882
+ export function parseAgentDataTriggerEvent(event) {
883
+ const match = event.match(dataTriggerEventPattern);
884
+ if (!match)
885
+ throw new Error(`Invalid Raybase agent data trigger event: ${event}`);
886
+ return { tableName: match[1], op: match[2] };
887
+ }
888
+ export function defineAgents(agents) {
889
+ const seen = new Set();
890
+ const normalized = {};
891
+ for (const [key, agent] of Object.entries(agents)) {
892
+ const name = agent.name || key;
893
+ validateAgentName(name);
894
+ if (seen.has(name))
895
+ throw new Error(`Duplicate Raybase agent name: ${name}`);
896
+ seen.add(name);
897
+ normalized[key] = defineAgent({ ...agent, name });
898
+ }
899
+ return normalized;
900
+ }
901
+ export function validateAgentName(name) {
902
+ if (!agentNamePattern.test(name)) {
903
+ throw new Error(`Invalid Raybase agent name "${name}". Use letters, numbers, "_", "-", ".", or ":".`);
904
+ }
905
+ }
906
+ export function agentSummaries(agents) {
907
+ return Object.values(defineAgents(agents ?? {})).map((agent) => ({
908
+ name: agent.name,
909
+ title: agent.title ?? agent.name,
910
+ description: agent.description ?? "",
911
+ runtime: agent.runtime,
912
+ triggers: agent.triggers,
913
+ model: agent.model ?? null,
914
+ limits: agent.limits ?? null,
915
+ scopes: agent.scopes ?? [],
916
+ owner: agent.owner ?? null,
917
+ actions: agentActionRegistrations(agent.actions).map(({ action }) => ({ name: action.name, description: action.description ?? "" })),
918
+ skills: agentSkillSummaries(agent.skills),
919
+ skillStore: agent.skillStore ?? null,
920
+ remoteMcpBindings: agentRemoteMcpBindingSummaries([agent]),
921
+ memorySpaces: agentMemorySpaces(agent),
922
+ rooms: agent.rooms?.enabled === true,
923
+ entries: Object.entries(agent.entries ?? {}).map(([kind, definition]) => ({
924
+ kind,
925
+ description: definition.description,
926
+ source: definition.source ?? "inline",
927
+ audience: agentEntryAudiences(definition),
928
+ hasKickoff: Boolean(definition.userMessage),
929
+ payloadSchema: definition.payloadSchema ?? null
930
+ })),
931
+ runtimeControl: {
932
+ configurable: agent.runtimeControl?.configurable === true,
933
+ defaultEnabled: agent.runtimeControl?.defaultEnabled !== false
934
+ },
935
+ prompt: agent.prompt
936
+ ? {
937
+ configurable: agent.prompt.configurable === true,
938
+ renderer: agent.prompt.renderer ?? "liquid",
939
+ default: agent.prompt.default
940
+ }
941
+ : null
942
+ }));
943
+ }
944
+ export function agentFor(agents, name) {
945
+ const registry = defineAgents(agents ?? {});
946
+ const agent = Object.values(registry).find((candidate) => candidate.name === name || candidate.name.toLowerCase() === name.toLowerCase());
947
+ if (!agent)
948
+ throw new AgentHttpError(`Unknown agent: ${name}`, 404);
949
+ return agent;
950
+ }
951
+ export class AgentHttpError extends Error {
952
+ status;
953
+ retryable;
954
+ constructor(message, status, retryable = false) {
955
+ super(message);
956
+ this.status = status;
957
+ this.retryable = retryable;
958
+ }
959
+ }
960
+ export function agentAudienceKinds(audience, surface) {
961
+ const configured = audience ?? "signed-in";
962
+ if (Array.isArray(configured))
963
+ return uniqueAudiences(configured);
964
+ if (typeof configured === "string")
965
+ return [configured];
966
+ const value = configured[surface] ?? configured.default ?? configured.app ?? "signed-in";
967
+ return Array.isArray(value) ? uniqueAudiences(value) : [value];
968
+ }
969
+ export function agentAudienceAllows(audience, surface, kind) {
970
+ return agentAudienceKinds(audience, surface).includes(kind);
971
+ }
972
+ export function agentRequestSurface(request, surfaces = []) {
973
+ const url = new URL(request.url);
974
+ const referer = request.headers.get("referer");
975
+ const requestedRoute = request.headers.get("x-raybase-agent-route");
976
+ const requestedSurface = request.headers.get("x-raybase-agent-surface");
977
+ let route = url.pathname || "/";
978
+ let origin = request.headers.get("origin") ?? undefined;
979
+ if (requestedRoute?.trim()) {
980
+ route = requestedRoute.trim();
981
+ }
982
+ else if (referer) {
983
+ try {
984
+ const ref = new URL(referer);
985
+ if (ref.origin === url.origin) {
986
+ route = ref.pathname || "/";
987
+ origin = ref.origin;
988
+ }
989
+ }
990
+ catch {
991
+ // Keep the API route when Referer is malformed.
992
+ }
993
+ }
994
+ const normalizedRoute = normalizeRoute(route);
995
+ const inferredSurface = surfaces.find((entry) => {
996
+ const prefix = normalizeRoute(entry.pathPrefix);
997
+ return normalizedRoute === prefix || normalizedRoute.startsWith(`${prefix.replace(/\/$/, "")}/`);
998
+ })?.surface ?? "app";
999
+ const surface = requestedSurface?.trim() || inferredSurface;
1000
+ return { surface, route: normalizedRoute, origin };
1001
+ }
1002
+ export async function resolveAgentAccess(input) {
1003
+ const requestContext = agentRequestSurface(input.request, input.surfaces);
1004
+ const allowed = agentAudienceKinds(input.agent.audience, requestContext.surface);
1005
+ const requestedKind = input.principalKind;
1006
+ const organizationId = input.organizationId ?? principalOrganizationId(input.user);
1007
+ if (requestedKind === "user" || (!requestedKind && allowed.includes("signed-in") && input.user)) {
1008
+ if (!allowed.includes("signed-in"))
1009
+ throw new AgentHttpError("Agent surface does not allow signed-in access", 403);
1010
+ if (!input.user)
1011
+ throw new AgentHttpError("Authentication required", 401);
1012
+ return {
1013
+ audience: "signed-in",
1014
+ principal: {
1015
+ kind: "user",
1016
+ user: input.user,
1017
+ userId: input.user.id,
1018
+ membershipId: optionalUserField(input.user, "membershipId"),
1019
+ organizationId: organizationId ?? undefined,
1020
+ roles: input.user.appRoles
1021
+ },
1022
+ surface: requestContext.surface,
1023
+ route: requestContext.route,
1024
+ origin: requestContext.origin,
1025
+ organizationId
1026
+ };
1027
+ }
1028
+ if (requestedKind === "public" || (!requestedKind && allowed.includes("public"))) {
1029
+ if (!allowed.includes("public"))
1030
+ throw new AgentHttpError("Agent surface does not allow public access", 403);
1031
+ return {
1032
+ audience: "public",
1033
+ principal: await publicAgentPrincipal(input.request, organizationId, requestContext),
1034
+ surface: requestContext.surface,
1035
+ route: requestContext.route,
1036
+ origin: requestContext.origin,
1037
+ organizationId
1038
+ };
1039
+ }
1040
+ throw new AgentHttpError("Authentication required", 401);
1041
+ }
1042
+ export function publicAgentUser(principal) {
1043
+ return {
1044
+ id: `public:${principal.id}`,
1045
+ email: "public@raybase.local",
1046
+ name: "Public visitor",
1047
+ operatorRole: null,
1048
+ appRoles: []
1049
+ };
1050
+ }
1051
+ export function agentInstanceKey(agentName, conversationId) {
1052
+ return `raybase-agent:${agentName}:${conversationId}`;
1053
+ }
1054
+ export function agentExpiresAt(ttlSeconds = defaultConversationTtlSeconds) {
1055
+ if (ttlSeconds === null)
1056
+ return null;
1057
+ return new Date(wallNowMs() + ttlSeconds * 1000).toISOString();
1058
+ }
1059
+ export async function createOrResumeAgentInstance(input) {
1060
+ const now = nowIso();
1061
+ const retention = normalizeAgentRetentionConfig(input.agent.retention);
1062
+ const isUserPrincipal = input.access.principal.kind === "user";
1063
+ const keepHot = isUserPrincipal && retention.transcripts === "keep-hot";
1064
+ const defaultTtlSeconds = isUserPrincipal ? retention.hotTtlMs / 1000 : retention.publicHotTtlMs / 1000;
1065
+ const expiresAt = agentExpiresAt(keepHot ? null : input.ttlSeconds ?? defaultTtlSeconds);
1066
+ if (input.conversationId) {
1067
+ const existing = await findAgentInstance(input.control, input.agent.name, input.conversationId);
1068
+ if (!existing)
1069
+ throw new AgentHttpError("Agent conversation not found", 404);
1070
+ if (existing.retentionReason === "user-deleted")
1071
+ throw new AgentHttpError("Agent conversation has been deleted", 410);
1072
+ await assertAgentConversationAccess({ control: input.control, instance: existing, access: input.access });
1073
+ if (existing.status === "deleted")
1074
+ throw new AgentHttpError("Agent conversation has been deleted", 410);
1075
+ if (existing.status === "deleting" || existing.status === "cleanupRequested") {
1076
+ throw new AgentHttpError("Agent conversation cleanup is in progress", 409);
1077
+ }
1078
+ if (existing.status === "cleanupError") {
1079
+ throw new AgentHttpError("Agent conversation cleanup must complete before it can resume", 503);
1080
+ }
1081
+ if (input.entry && stableJson(input.entry) !== (existing.entryJson ?? "")) {
1082
+ throw new AgentHttpError("Agent conversation entry context conflicts with the existing snapshot", 409);
1083
+ }
1084
+ const metadataJson = agentInstanceMetadataJson({
1085
+ current: existing.metadataJson,
1086
+ metadata: input.metadata,
1087
+ title: input.title
1088
+ });
1089
+ if (existing.status === "archived") {
1090
+ const lastChapter = await loadLastAgentChapter(input.control, existing.conversationId);
1091
+ if (!lastChapter) {
1092
+ throw new AgentHttpError("Archived conversation context is unavailable; retry later", 503);
1093
+ }
1094
+ if (!input.rehydrateArchived) {
1095
+ throw new AgentHttpError("Agent conversation rehydration is unavailable", 503);
1096
+ }
1097
+ const synopsis = buildAgentChapterRehydrateSeed(lastChapter);
1098
+ try {
1099
+ await input.rehydrateArchived(existing, synopsis);
1100
+ }
1101
+ catch (error) {
1102
+ throw new AgentHttpError(`Agent conversation rehydration failed: ${error instanceof Error ? error.message : String(error)}`, 503);
1103
+ }
1104
+ await input.control.run(`UPDATE ${q("_agent_instances")}
1105
+ SET "status" = 'active', "cleanupRequestedAt" = NULL, "deletedAt" = NULL, "retentionReason" = NULL,
1106
+ "updatedAt" = ?, "lastActiveAt" = ?, "expiresAt" = ?, "route" = ?, "surface" = ?,
1107
+ "metadataJson" = ?, "lastError" = NULL
1108
+ WHERE "id" = ? AND "status" = 'archived'`, [
1109
+ now,
1110
+ now,
1111
+ expiresAt,
1112
+ input.access.route,
1113
+ input.access.surface,
1114
+ metadataJson,
1115
+ existing.id
1116
+ ]);
1117
+ const activated = await findAgentInstance(input.control, input.agent.name, input.conversationId);
1118
+ if (activated?.status === "active")
1119
+ return activated;
1120
+ if (activated?.status === "archived") {
1121
+ throw new AgentHttpError("Agent conversation rehydration activation did not complete", 503);
1122
+ }
1123
+ throw new AgentHttpError("Agent conversation changed while rehydrating", 409);
1124
+ }
1125
+ await input.control.run(`UPDATE ${q("_agent_instances")}
1126
+ SET "status" = 'active', "cleanupRequestedAt" = NULL, "deletedAt" = NULL, "updatedAt" = ?, "lastActiveAt" = ?,
1127
+ "expiresAt" = ?, "route" = ?, "surface" = ?, "metadataJson" = ?, "lastError" = NULL
1128
+ WHERE "id" = ? AND "status" = 'active'
1129
+ AND COALESCE("retentionReason", '') <> 'user-deleted'`, [
1130
+ now,
1131
+ now,
1132
+ expiresAt,
1133
+ input.access.route,
1134
+ input.access.surface,
1135
+ metadataJson,
1136
+ existing.id
1137
+ ]);
1138
+ const refreshed = await findAgentInstance(input.control, input.agent.name, input.conversationId);
1139
+ if (!refreshed)
1140
+ throw new AgentHttpError("Agent conversation not found", 404);
1141
+ if (refreshed.retentionReason === "user-deleted" || refreshed.status === "deleted") {
1142
+ throw new AgentHttpError("Agent conversation has been deleted", 410);
1143
+ }
1144
+ if (refreshed.status === "cleanupRequested" || refreshed.status === "deleting" || refreshed.status === "archived") {
1145
+ throw new AgentHttpError("Agent conversation changed while resuming; retry", 409);
1146
+ }
1147
+ if (refreshed.status === "cleanupError") {
1148
+ throw new AgentHttpError("Agent conversation cleanup must complete before it can resume", 503);
1149
+ }
1150
+ return refreshed;
1151
+ }
1152
+ const conversationId = randomId("agent_conv");
1153
+ const idValue = randomId("agent_inst");
1154
+ const row = {
1155
+ id: idValue,
1156
+ conversationId,
1157
+ agentName: input.agent.name,
1158
+ agentTitle: input.agent.title ?? input.agent.name,
1159
+ agentOwner: input.agent.owner?.packageName ?? input.agent.owner?.moduleId ?? null,
1160
+ moduleId: input.agent.owner?.moduleId ?? null,
1161
+ runtime: input.agent.runtime,
1162
+ triggerType: "chat",
1163
+ instanceKey: agentInstanceKey(input.agent.name, conversationId),
1164
+ principalKind: input.access.principal.kind,
1165
+ userId: input.access.principal.kind === "user" ? input.access.principal.user.id : null,
1166
+ publicScopeHash: input.access.principal.kind === "public" ? input.access.principal.id : null,
1167
+ organizationId: input.access.organizationId ?? null,
1168
+ route: input.access.route,
1169
+ surface: input.access.surface,
1170
+ status: "active",
1171
+ cleanupRequestedAt: null,
1172
+ hotStorageDeletedAt: null,
1173
+ deletedAt: null,
1174
+ retentionReason: null,
1175
+ pinnedAt: null,
1176
+ createdAt: now,
1177
+ updatedAt: now,
1178
+ lastActiveAt: now,
1179
+ expiresAt,
1180
+ lastRunId: null,
1181
+ lastError: null,
1182
+ metadataJson: agentInstanceMetadataJson({
1183
+ metadata: input.metadata,
1184
+ title: input.title
1185
+ }),
1186
+ entryKind: input.entry?.kind ?? null,
1187
+ entryJson: input.entry ? stableJson(input.entry) : null,
1188
+ kind: null,
1189
+ roomMembershipRev: null
1190
+ };
1191
+ await input.control.run(`INSERT INTO ${q("_agent_instances")}
1192
+ ("id", "conversationId", "agentName", "agentTitle", "agentOwner", "moduleId", "runtime", "triggerType", "instanceKey",
1193
+ "principalKind", "userId", "publicScopeHash", "organizationId", "route", "surface", "status", "cleanupRequestedAt",
1194
+ "hotStorageDeletedAt", "deletedAt", "retentionReason", "pinnedAt", "createdAt", "updatedAt", "lastActiveAt", "expiresAt", "lastRunId", "lastError", "metadataJson", "entryKind", "entryJson", "kind", "roomMembershipRev")
1195
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1196
+ row.id,
1197
+ row.conversationId,
1198
+ row.agentName,
1199
+ row.agentTitle,
1200
+ row.agentOwner,
1201
+ row.moduleId,
1202
+ row.runtime,
1203
+ row.triggerType,
1204
+ row.instanceKey,
1205
+ row.principalKind,
1206
+ row.userId,
1207
+ row.publicScopeHash,
1208
+ row.organizationId,
1209
+ row.route,
1210
+ row.surface,
1211
+ row.status,
1212
+ row.cleanupRequestedAt,
1213
+ row.hotStorageDeletedAt,
1214
+ row.deletedAt,
1215
+ row.retentionReason,
1216
+ row.pinnedAt,
1217
+ row.createdAt,
1218
+ row.updatedAt,
1219
+ row.lastActiveAt,
1220
+ row.expiresAt,
1221
+ row.lastRunId,
1222
+ row.lastError,
1223
+ row.metadataJson,
1224
+ row.entryKind,
1225
+ row.entryJson,
1226
+ row.kind ?? null,
1227
+ row.roomMembershipRev ?? null
1228
+ ]);
1229
+ return row;
1230
+ }
1231
+ export function buildAgentRoomInstance(input) {
1232
+ if (!input.agent.rooms?.enabled)
1233
+ throw new AgentHttpError(`Agent ${input.agent.name} does not enable rooms`, 400);
1234
+ if (!input.agent.triggers.some((trigger) => trigger.type === "chat")) {
1235
+ throw new AgentHttpError(`Agent ${input.agent.name} does not declare a chat trigger`, 400);
1236
+ }
1237
+ const now = input.now ?? nowIso();
1238
+ const retention = normalizeAgentRetentionConfig(input.agent.retention);
1239
+ const expiresAt = agentExpiresAt(retention.transcripts === "keep-hot" ? null : retention.hotTtlMs / 1000);
1240
+ const row = {
1241
+ id: randomId("agent_inst"),
1242
+ conversationId: input.conversationId,
1243
+ agentName: input.agent.name,
1244
+ agentTitle: input.agent.title ?? input.agent.name,
1245
+ agentOwner: input.agent.owner?.packageName ?? input.agent.owner?.moduleId ?? null,
1246
+ moduleId: input.agent.owner?.moduleId ?? null,
1247
+ runtime: input.agent.runtime,
1248
+ triggerType: "chat",
1249
+ instanceKey: agentInstanceKey(input.agent.name, input.conversationId),
1250
+ principalKind: "user",
1251
+ userId: input.ownerUserId,
1252
+ publicScopeHash: null,
1253
+ organizationId: input.organizationId ?? null,
1254
+ route: input.route ?? null,
1255
+ surface: input.surface ?? "app",
1256
+ status: "active",
1257
+ cleanupRequestedAt: null,
1258
+ hotStorageDeletedAt: null,
1259
+ deletedAt: null,
1260
+ retentionReason: null,
1261
+ pinnedAt: null,
1262
+ createdAt: now,
1263
+ updatedAt: now,
1264
+ lastActiveAt: now,
1265
+ expiresAt,
1266
+ lastRunId: null,
1267
+ lastError: null,
1268
+ metadataJson: agentInstanceMetadataJson({ title: input.title }),
1269
+ entryKind: null,
1270
+ entryJson: null,
1271
+ kind: "room",
1272
+ roomMembershipRev: 1
1273
+ };
1274
+ return row;
1275
+ }
1276
+ export function agentRoomInstanceInsertStatement(row) {
1277
+ return {
1278
+ sql: `INSERT INTO ${q("_agent_instances")}
1279
+ ("id", "conversationId", "agentName", "agentTitle", "agentOwner", "moduleId", "runtime", "triggerType", "instanceKey",
1280
+ "principalKind", "userId", "publicScopeHash", "organizationId", "route", "surface", "status", "cleanupRequestedAt",
1281
+ "hotStorageDeletedAt", "deletedAt", "retentionReason", "pinnedAt", "createdAt", "updatedAt", "lastActiveAt", "expiresAt", "lastRunId", "lastError", "metadataJson", "entryKind", "entryJson", "kind", "roomMembershipRev")
1282
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1283
+ params: [
1284
+ row.id, row.conversationId, row.agentName, row.agentTitle, row.agentOwner, row.moduleId,
1285
+ row.runtime, row.triggerType, row.instanceKey, row.principalKind, row.userId, row.publicScopeHash,
1286
+ row.organizationId, row.route, row.surface, row.status, row.cleanupRequestedAt, row.hotStorageDeletedAt,
1287
+ row.deletedAt, row.retentionReason, row.pinnedAt, row.createdAt, row.updatedAt, row.lastActiveAt,
1288
+ row.expiresAt, row.lastRunId, row.lastError, row.metadataJson, row.entryKind, row.entryJson, row.kind ?? null,
1289
+ row.roomMembershipRev ?? null
1290
+ ]
1291
+ };
1292
+ }
1293
+ export async function createAgentRoomInstance(input) {
1294
+ const row = buildAgentRoomInstance(input);
1295
+ const statement = agentRoomInstanceInsertStatement(row);
1296
+ await input.control.run(statement.sql, statement.params);
1297
+ return row;
1298
+ }
1299
+ export async function findAgentInstance(control, agentName, conversationId) {
1300
+ return control.first(`SELECT * FROM ${q("_agent_instances")} WHERE "agentName" = ? AND "conversationId" = ? LIMIT 1`, [agentName, conversationId]);
1301
+ }
1302
+ export async function listAgentConversations(control, input) {
1303
+ const limit = boundedAgentConversationPage(input.limit, 20, 1, 100);
1304
+ const offset = boundedAgentConversationPage(input.offset, 0, 0, 10_000);
1305
+ const now = input.now ?? nowIso();
1306
+ const membershipUserId = input.access.principal.kind === "user" ? input.access.principal.user.id : "";
1307
+ const commonParams = [membershipUserId, input.agentName, now];
1308
+ const principalColumn = input.access.principal.kind === "user" ? "userId" : "publicScopeHash";
1309
+ const principalId = input.access.principal.kind === "user" ? input.access.principal.user.id : input.access.principal.id;
1310
+ const query = input.q?.trim();
1311
+ const searchQuery = query ? parseSearchQuery(query) : null;
1312
+ const titleQuery = searchQuery?.terms.join(" ") || query;
1313
+ const hasNormalizedTitleQuery = Boolean(query && titleQuery && titleQuery !== query);
1314
+ if (searchQuery)
1315
+ await ensureAgentChapterSearch(control);
1316
+ const statusPredicate = input.includeArchived && input.access.principal.kind === "user"
1317
+ ? `i."status" IN ('active', 'archived')`
1318
+ : `i."status" = 'active'`;
1319
+ const searchPredicate = query
1320
+ ? `AND (
1321
+ LOWER(COALESCE(json_extract(i."metadataJson", '$.title'), '')) LIKE LOWER(?)
1322
+ ${hasNormalizedTitleQuery
1323
+ ? `OR LOWER(COALESCE(json_extract(i."metadataJson", '$.title'), '')) LIKE LOWER(?)`
1324
+ : ""}
1325
+ ${searchQuery ? `OR EXISTS (
1326
+ SELECT 1 FROM ${q("_agent_chapter_fts")} f
1327
+ JOIN ${q("_agent_chapters")} c ON c."id" = f."chapterId"
1328
+ WHERE f."conversationId" = i."conversationId"
1329
+ AND c."agentName" = i."agentName"
1330
+ AND c."userId" = i."userId"
1331
+ AND ${q("_agent_chapter_fts")} MATCH ?
1332
+ )` : ""}
1333
+ )`
1334
+ : "";
1335
+ const rows = await control.all(`SELECT i.*, r."status" AS "lastRunStatus", m."pinnedAt" AS "roomMemberPinnedAt", m."lastReadAt" AS "roomMemberLastReadAt"
1336
+ FROM ${q("_agent_instances")} i
1337
+ LEFT JOIN ${q("_agent_runs")} r ON r."runId" = i."lastRunId"
1338
+ LEFT JOIN ${q("_agent_room_members")} m
1339
+ ON i."kind" = 'room' AND m."conversationId" = i."conversationId" AND m."userId" = ?
1340
+ WHERE i."agentName" = ?
1341
+ AND i."runtime" = 'ai-chat'
1342
+ AND i."triggerType" = 'chat'
1343
+ AND ${statusPredicate}
1344
+ AND (i."status" = 'archived' OR i."expiresAt" IS NULL OR i."expiresAt" > ?)
1345
+ AND i."surface" = ?
1346
+ AND COALESCE(i."organizationId", '') = COALESCE(?, '')
1347
+ AND (
1348
+ (COALESCE(i."kind", '') <> 'room' AND i."principalKind" = ? AND i.${q(principalColumn)} = ?)
1349
+ OR (i."kind" = 'room' AND ? = 'user' AND m."status" = 'active')
1350
+ )
1351
+ ${searchPredicate}
1352
+ ORDER BY CASE WHEN COALESCE(m."pinnedAt", i."pinnedAt") IS NULL THEN 1 ELSE 0 END,
1353
+ COALESCE(m."pinnedAt", i."pinnedAt") DESC, i."lastActiveAt" DESC, i."createdAt" DESC
1354
+ LIMIT ? OFFSET ?`, [
1355
+ ...commonParams,
1356
+ input.access.surface,
1357
+ input.access.organizationId ?? null,
1358
+ input.access.principal.kind,
1359
+ principalId,
1360
+ input.access.principal.kind,
1361
+ ...(query
1362
+ ? [
1363
+ `%${query}%`,
1364
+ ...(hasNormalizedTitleQuery ? [`%${titleQuery}%`] : []),
1365
+ ...(searchQuery ? [searchQuery.match] : [])
1366
+ ]
1367
+ : []),
1368
+ limit,
1369
+ offset
1370
+ ]);
1371
+ return rows.map((row) => agentConversationSummaryFromRow({
1372
+ ...row,
1373
+ pinnedAt: row.kind === "room" ? row.roomMemberPinnedAt : row.pinnedAt,
1374
+ lastReadAt: row.kind === "room" ? row.roomMemberLastReadAt : row.lastReadAt ?? null
1375
+ }));
1376
+ }
1377
+ export async function loadAgentConversationForAccess(control, input) {
1378
+ const instance = await loadAgentInstance(control, input.agentName, input.conversationId);
1379
+ await assertAgentConversationAccess({ control, instance, access: input.access });
1380
+ return instance;
1381
+ }
1382
+ export async function requestAgentConversationDeletion(control, input) {
1383
+ const instance = await findAgentInstance(control, input.agentName, input.conversationId);
1384
+ if (!instance)
1385
+ throw new AgentHttpError("Agent conversation not found", 404);
1386
+ await assertAgentConversationAccess({ control, instance, access: input.access, require: "owner" });
1387
+ const now = input.now ?? nowIso();
1388
+ await control.run(`UPDATE ${q("_agent_instances")}
1389
+ SET "status" = CASE
1390
+ WHEN "status" = 'deleted' THEN 'deleted'
1391
+ WHEN "status" = 'deleting' THEN 'deleting'
1392
+ ELSE 'cleanupRequested'
1393
+ END,
1394
+ "cleanupRequestedAt" = CASE
1395
+ WHEN "status" = 'deleted' THEN "cleanupRequestedAt"
1396
+ ELSE COALESCE("cleanupRequestedAt", ?)
1397
+ END,
1398
+ "deletedAt" = CASE
1399
+ WHEN "status" = 'deleted' THEN COALESCE("deletedAt", ?)
1400
+ ELSE "deletedAt"
1401
+ END,
1402
+ "retentionReason" = 'user-deleted',
1403
+ "expiresAt" = ?,
1404
+ "updatedAt" = ?,
1405
+ "lastError" = NULL
1406
+ WHERE "id" = ?`, [now, now, now, now, instance.id]);
1407
+ const claimed = await findAgentInstance(control, input.agentName, input.conversationId);
1408
+ if (!claimed)
1409
+ throw new AgentHttpError("Agent conversation not found", 404);
1410
+ if (claimed.retentionReason !== "user-deleted") {
1411
+ throw new AgentHttpError("Agent conversation deletion could not be secured", 409);
1412
+ }
1413
+ if (claimed.status !== "deleted") {
1414
+ try {
1415
+ await input.deleteHotStorage(claimed);
1416
+ }
1417
+ catch (error) {
1418
+ throw new AgentHttpError(`Agent conversation hot-storage deletion failed: ${error instanceof Error ? error.message : String(error)}`, 503);
1419
+ }
1420
+ }
1421
+ const completed = await findAgentInstance(control, input.agentName, input.conversationId);
1422
+ if (!completed)
1423
+ throw new AgentHttpError("Agent conversation not found", 404);
1424
+ if (completed.status !== "deleted" || completed.retentionReason !== "user-deleted") {
1425
+ throw new AgentHttpError("Agent conversation deletion has not completed", 503);
1426
+ }
1427
+ await purgeAgentChaptersForConversation(control, {
1428
+ agentName: completed.agentName,
1429
+ conversationId: completed.conversationId
1430
+ });
1431
+ return { agentName: instance.agentName, conversationId: instance.conversationId, status: "deleted" };
1432
+ }
1433
+ export async function loadAgentInstance(control, agentName, conversationId) {
1434
+ const row = await findAgentInstance(control, agentName, conversationId);
1435
+ if (!row)
1436
+ throw new AgentHttpError("Agent conversation not found", 404);
1437
+ if (row.status !== "active")
1438
+ throw new AgentHttpError("Agent conversation is not active", 410);
1439
+ if (row.expiresAt && new Date(row.expiresAt).getTime() <= wallNowMs())
1440
+ throw new AgentHttpError("Agent conversation expired", 410);
1441
+ return row;
1442
+ }
1443
+ function agentConversationSummaryFromRow(row) {
1444
+ return {
1445
+ agentName: row.agentName,
1446
+ conversationId: row.conversationId,
1447
+ runtime: row.runtime,
1448
+ triggerType: "chat",
1449
+ title: agentConversationTitle(row.metadataJson),
1450
+ agentTitle: row.agentTitle,
1451
+ route: row.route,
1452
+ surface: row.surface,
1453
+ status: row.status,
1454
+ createdAt: row.createdAt,
1455
+ updatedAt: row.updatedAt,
1456
+ lastActiveAt: row.lastActiveAt,
1457
+ expiresAt: row.expiresAt,
1458
+ pinnedAt: row.pinnedAt,
1459
+ archived: row.status === "archived",
1460
+ lastRunId: row.lastRunId,
1461
+ lastRunStatus: row.lastRunStatus ?? null,
1462
+ entryKind: row.entryKind,
1463
+ kind: row.kind ?? null,
1464
+ lastReadAt: row.lastReadAt ?? null
1465
+ };
1466
+ }
1467
+ // Title semantics: undefined keeps the stored title, null clears it, and a
1468
+ // string replaces it (a string that cleans to nothing keeps the stored title
1469
+ // rather than silently clearing). Resuming a conversation without a title
1470
+ // must never wipe one that a client or the runtime already stored.
1471
+ export function agentInstanceMetadataJson(input) {
1472
+ if (input.metadata === undefined && input.title === undefined)
1473
+ return input.current ?? null;
1474
+ const currentTitle = cleanAgentConversationTitle(parseAgentMetadata(input.current).title);
1475
+ const metadata = input.metadata === undefined
1476
+ ? parseAgentMetadata(input.current)
1477
+ : normalizeAgentMetadata(input.metadata);
1478
+ const title = input.title === null
1479
+ ? undefined
1480
+ : input.title !== undefined
1481
+ ? cleanAgentConversationTitle(input.title) ?? currentTitle
1482
+ : currentTitle ?? cleanAgentConversationTitle(metadata.title);
1483
+ delete metadata.title;
1484
+ if (title)
1485
+ metadata.title = title;
1486
+ return Object.keys(metadata).length ? stableJson(metadata) : null;
1487
+ }
1488
+ export function agentConversationTitle(metadataJson) {
1489
+ const title = cleanAgentConversationTitle(parseAgentMetadata(metadataJson).title);
1490
+ return title ?? null;
1491
+ }
1492
+ export async function updateAgentConversationTitle(control, instance, title, now) {
1493
+ const metadataJson = agentInstanceMetadataJson({ current: instance.metadataJson, title });
1494
+ await control.run(`UPDATE ${q("_agent_instances")} SET "metadataJson" = ?, "updatedAt" = ? WHERE "id" = ?`, [metadataJson, now ?? nowIso(), instance.id]);
1495
+ return agentConversationTitle(metadataJson);
1496
+ }
1497
+ export async function updateAgentConversationPinned(control, instance, pinned, now) {
1498
+ const timestamp = now ?? nowIso();
1499
+ const pinnedAt = pinned ? timestamp : null;
1500
+ await control.run(`UPDATE ${q("_agent_instances")} SET "pinnedAt" = ?, "updatedAt" = ? WHERE "id" = ?`, [pinnedAt, timestamp, instance.id]);
1501
+ return pinnedAt;
1502
+ }
1503
+ export async function markAgentDirectConversationRead(control, instance, now) {
1504
+ if (instance.kind === "room")
1505
+ throw new AgentHttpError("Agent conversation is a room", 400);
1506
+ const lastReadAt = now ?? nowIso();
1507
+ await control.run(`UPDATE ${q("_agent_instances")}
1508
+ SET "lastReadAt" = CASE
1509
+ WHEN "lastReadAt" IS NULL OR "lastReadAt" < ? THEN ?
1510
+ ELSE "lastReadAt"
1511
+ END,
1512
+ "updatedAt" = CASE
1513
+ WHEN "lastReadAt" IS NULL OR "lastReadAt" < ? THEN ?
1514
+ ELSE "updatedAt"
1515
+ END
1516
+ WHERE "id" = ?`, [lastReadAt, lastReadAt, lastReadAt, lastReadAt, instance.id]);
1517
+ const row = await control.first(`SELECT "lastReadAt" FROM ${q("_agent_instances")} WHERE "id" = ? LIMIT 1`, [instance.id]);
1518
+ if (!row?.lastReadAt)
1519
+ throw new AgentHttpError("Agent conversation read cursor could not be advanced", 500);
1520
+ return row.lastReadAt;
1521
+ }
1522
+ // Dedicated rename path: unlike the conversations POST resume, this never
1523
+ // flips status back to active, bumps lastActiveAt, or extends expiresAt.
1524
+ export async function renameAgentConversation(control, input) {
1525
+ if (input.title !== null && typeof input.title !== "string") {
1526
+ throw new AgentHttpError("Conversation title must be a string, or null to clear it", 400);
1527
+ }
1528
+ if (typeof input.title === "string" && !cleanAgentConversationTitle(input.title)) {
1529
+ throw new AgentHttpError("Conversation title must not be blank", 400);
1530
+ }
1531
+ const instance = await findAgentInstance(control, input.agentName, input.conversationId);
1532
+ if (!instance)
1533
+ throw new AgentHttpError("Agent conversation not found", 404);
1534
+ await assertAgentConversationAccess({ control, instance, access: input.access, require: "owner" });
1535
+ if (instance.status !== "active" && instance.status !== "archived") {
1536
+ throw new AgentHttpError("Agent conversation cannot be renamed in its current state", 409);
1537
+ }
1538
+ const title = await updateAgentConversationTitle(control, instance, input.title, input.now);
1539
+ return { agentName: instance.agentName, conversationId: instance.conversationId, title };
1540
+ }
1541
+ export async function pinAgentConversation(control, input) {
1542
+ if (typeof input.pinned !== "boolean")
1543
+ throw new AgentHttpError("Conversation pinned must be a boolean", 400);
1544
+ if (input.access.principal.kind !== "user")
1545
+ throw new AgentHttpError("Public conversations cannot be pinned", 403);
1546
+ const instance = await findAgentInstance(control, input.agentName, input.conversationId);
1547
+ if (!instance)
1548
+ throw new AgentHttpError("Agent conversation not found", 404);
1549
+ const accessResult = await assertAgentConversationAccess({ control, instance, access: input.access });
1550
+ if (instance.status !== "active" && instance.status !== "archived") {
1551
+ throw new AgentHttpError("Agent conversation cannot be pinned in its current state", 409);
1552
+ }
1553
+ const pinnedAt = instance.kind === "room"
1554
+ ? await setAgentRoomMemberPinned({
1555
+ control,
1556
+ conversationId: instance.conversationId,
1557
+ userId: accessResult.membership.userId,
1558
+ pinned: input.pinned,
1559
+ now: input.now
1560
+ })
1561
+ : await updateAgentConversationPinned(control, instance, input.pinned, input.now);
1562
+ return {
1563
+ agentName: instance.agentName,
1564
+ conversationId: instance.conversationId,
1565
+ pinned: Boolean(pinnedAt),
1566
+ pinnedAt
1567
+ };
1568
+ }
1569
+ function parseAgentMetadata(value) {
1570
+ if (!value)
1571
+ return {};
1572
+ try {
1573
+ return normalizeAgentMetadata(JSON.parse(value));
1574
+ }
1575
+ catch {
1576
+ return {};
1577
+ }
1578
+ }
1579
+ function normalizeAgentMetadata(value) {
1580
+ return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
1581
+ }
1582
+ export function cleanAgentConversationTitle(value) {
1583
+ if (typeof value !== "string")
1584
+ return undefined;
1585
+ const title = value.trim().replace(/\s+/g, " ");
1586
+ return title ? title.slice(0, 120) : undefined;
1587
+ }
1588
+ function boundedAgentConversationPage(value, fallback, min, max) {
1589
+ const numeric = Number(value ?? fallback);
1590
+ if (!Number.isFinite(numeric))
1591
+ return fallback;
1592
+ return Math.max(min, Math.min(max, Math.floor(numeric)));
1593
+ }
1594
+ export async function createAgentRun(input) {
1595
+ const now = nowIso();
1596
+ const expiresAt = input.ttlSeconds === undefined
1597
+ ? input.instance.expiresAt === null ? null : agentExpiresAt()
1598
+ : agentExpiresAt(input.ttlSeconds);
1599
+ const run = {
1600
+ runId: input.runId || randomId("agent_run"),
1601
+ conversationId: input.instance.conversationId,
1602
+ agentName: input.instance.agentName,
1603
+ requestId: input.requestId,
1604
+ triggerType: input.triggerType ?? input.instance.triggerType,
1605
+ inputMessageId: input.inputMessageId ?? null,
1606
+ status: "queued",
1607
+ startedAt: now,
1608
+ heartbeatAt: now,
1609
+ finishedAt: null,
1610
+ error: null,
1611
+ usageEventId: null,
1612
+ auditEventId: null,
1613
+ metadataJson: input.metadata === undefined ? null : stableJson(input.metadata)
1614
+ };
1615
+ await input.control.run(`INSERT INTO ${q("_agent_runs")}
1616
+ ("runId", "conversationId", "agentName", "requestId", "triggerType", "inputMessageId", "status", "startedAt", "heartbeatAt", "finishedAt",
1617
+ "error", "usageEventId", "auditEventId", "metadataJson")
1618
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1619
+ run.runId,
1620
+ run.conversationId,
1621
+ run.agentName,
1622
+ run.requestId,
1623
+ run.triggerType,
1624
+ run.inputMessageId,
1625
+ run.status,
1626
+ run.startedAt,
1627
+ run.heartbeatAt,
1628
+ run.finishedAt,
1629
+ run.error,
1630
+ run.usageEventId,
1631
+ run.auditEventId,
1632
+ run.metadataJson
1633
+ ]);
1634
+ await input.control.run(`UPDATE ${q("_agent_instances")}
1635
+ SET "lastRunId" = ?, "lastActiveAt" = ?, "expiresAt" = ?, "updatedAt" = ?, "lastError" = NULL
1636
+ WHERE "id" = ?`, [run.runId, now, expiresAt, now, input.instance.id]);
1637
+ return run;
1638
+ }
1639
+ export function agentEnabledSettingKey(agentName) {
1640
+ return `raybase.agent.${agentName}.enabled`;
1641
+ }
1642
+ export function agentPromptSettingKey(agentName) {
1643
+ return `raybase.agent.${agentName}.prompt`;
1644
+ }
1645
+ export const raybaseAgentTimezoneSettingKey = "raybase.agent.timezone";
1646
+ export function agentGeneratedSettingDefinitions(agents) {
1647
+ const definitions = {};
1648
+ const resolvedAgents = Object.values(defineAgents(agents ?? {}));
1649
+ if (resolvedAgents.length > 0) {
1650
+ definitions[raybaseAgentTimezoneSettingKey] = setting.string({
1651
+ scope: "user",
1652
+ group: "Agents",
1653
+ label: "Timezone",
1654
+ description: "IANA timezone used for per-turn Agent time context.",
1655
+ default: "UTC",
1656
+ validate: (value) => {
1657
+ try {
1658
+ new Intl.DateTimeFormat("en", { timeZone: value }).format();
1659
+ return true;
1660
+ }
1661
+ catch {
1662
+ return "Timezone must be a valid IANA timezone such as Europe/Paris.";
1663
+ }
1664
+ }
1665
+ });
1666
+ }
1667
+ for (const agent of resolvedAgents) {
1668
+ const group = `Agents / ${agent.title ?? agent.name}`;
1669
+ if (agent.runtimeControl?.configurable === true) {
1670
+ definitions[agentEnabledSettingKey(agent.name)] = setting.boolean({
1671
+ scope: "app",
1672
+ group,
1673
+ label: "Enabled",
1674
+ description: "Controls admission for new chat, scheduled, data-triggered, direct, and wake runs. In-flight work is not cancelled.",
1675
+ default: agent.runtimeControl.defaultEnabled !== false,
1676
+ admin: { hidden: true }
1677
+ });
1678
+ }
1679
+ if (agent.prompt?.configurable === true) {
1680
+ definitions[agentPromptSettingKey(agent.name)] = setting.string({
1681
+ scope: "app",
1682
+ group,
1683
+ label: "Prompt",
1684
+ description: "Operator override for this agent prompt. Resetting removes the stored override and returns to the code-defined default.",
1685
+ default: agent.prompt.default,
1686
+ admin: { hidden: true, multiline: true },
1687
+ validate: (value) => value.trim() ? true : "Prompt override must not be blank"
1688
+ });
1689
+ }
1690
+ }
1691
+ return definitions;
1692
+ }
1693
+ export async function isAgentRuntimeEnabled(settings, agent) {
1694
+ if (agent.runtimeControl?.configurable !== true)
1695
+ return agent.runtimeControl?.defaultEnabled !== false;
1696
+ try {
1697
+ return Boolean(await settings?.get(agentEnabledSettingKey(agent.name)) ?? (agent.runtimeControl.defaultEnabled !== false));
1698
+ }
1699
+ catch {
1700
+ return agent.runtimeControl.defaultEnabled !== false;
1701
+ }
1702
+ }
1703
+ export function agentScheduleId(agentName, triggerIndex, trigger) {
1704
+ if (trigger?.maintenance)
1705
+ return `static:${agentName}:maintenance:${trigger.maintenance}`;
1706
+ return `static:${agentName}:${triggerIndex}`;
1707
+ }
1708
+ export function agentTriggerKey(agentName, triggerIndex, trigger) {
1709
+ if (trigger.type === "chat")
1710
+ return `chat:${agentName}`;
1711
+ if (trigger.type === "schedule") {
1712
+ return trigger.maintenance
1713
+ ? `schedule:${agentName}:maintenance:${trigger.maintenance}`
1714
+ : `schedule:${agentName}:${triggerIndex}:${trigger.cron}:${trigger.timezone ?? "UTC"}`;
1715
+ }
1716
+ return `data:${agentName}:${triggerIndex}:${trigger.event}`;
1717
+ }
1718
+ function hasOwn(object, key) {
1719
+ return Object.prototype.hasOwnProperty.call(object, key);
1720
+ }
1721
+ function normalizeScheduleUserMessage(value, label = "Schedule user message") {
1722
+ if (value === undefined || value === null)
1723
+ return null;
1724
+ if (typeof value !== "string")
1725
+ throw new Error(`${label} must be a string`);
1726
+ const trimmed = value.trim();
1727
+ if (!trimmed)
1728
+ throw new Error(`${label} must not be blank`);
1729
+ return trimmed;
1730
+ }
1731
+ function normalizeScheduleTimezoneOverride(value) {
1732
+ if (value === undefined || value === null)
1733
+ return null;
1734
+ if (typeof value !== "string")
1735
+ throw new Error("Schedule timezone override must be a string");
1736
+ const trimmed = value.trim();
1737
+ return trimmed ? trimmed : null;
1738
+ }
1739
+ function normalizeScheduleCronOverride(value) {
1740
+ if (typeof value !== "string" || !value.trim())
1741
+ throw new Error("Schedule cron override must be a non-empty string");
1742
+ const cron = value.trim();
1743
+ validateCron(cron);
1744
+ return cron;
1745
+ }
1746
+ function serializeScheduleJson(value) {
1747
+ const serialized = JSON.stringify(value);
1748
+ if (serialized === undefined)
1749
+ throw new Error("Schedule input override must be JSON-serializable");
1750
+ return stableJson(JSON.parse(serialized));
1751
+ }
1752
+ function parseScheduleJson(value, fallback) {
1753
+ if (value === null || value === undefined)
1754
+ return fallback;
1755
+ try {
1756
+ return JSON.parse(value);
1757
+ }
1758
+ catch {
1759
+ return fallback;
1760
+ }
1761
+ }
1762
+ function normalizeOverrideEnabled(value) {
1763
+ return value === 0 || value === 1 ? value : null;
1764
+ }
1765
+ export function parseAgentScheduleSourceInput(row) {
1766
+ return parseScheduleJson(row.inputJson, {});
1767
+ }
1768
+ export function parseAgentScheduleInput(row) {
1769
+ if (row.inputJsonOverrideEnabled === 1)
1770
+ return parseScheduleJson(row.inputJsonOverride, null);
1771
+ return parseAgentScheduleSourceInput(row);
1772
+ }
1773
+ export function agentScheduleOverrideState(row) {
1774
+ return {
1775
+ cron: row.cronOverride !== null && row.cronOverride !== undefined,
1776
+ timezone: row.timezoneOverride !== null && row.timezoneOverride !== undefined,
1777
+ enabled: normalizeOverrideEnabled(row.enabledOverride) !== null,
1778
+ input: row.inputJsonOverrideEnabled === 1,
1779
+ userMessage: row.userMessageOverride !== null && row.userMessageOverride !== undefined
1780
+ };
1781
+ }
1782
+ export function effectiveAgentSchedule(row) {
1783
+ const enabledOverride = normalizeOverrideEnabled(row.enabledOverride);
1784
+ const userMessage = row.userMessageOverride ?? row.userMessage ?? null;
1785
+ return {
1786
+ id: row.id,
1787
+ agentName: row.agentName,
1788
+ triggerIndex: row.triggerIndex,
1789
+ triggerKey: row.triggerKey,
1790
+ cron: row.cronOverride ?? row.cron,
1791
+ timezone: row.timezoneOverride ?? row.timezone ?? null,
1792
+ enabled: (enabledOverride ?? row.enabled) === 1,
1793
+ input: parseAgentScheduleInput(row),
1794
+ userMessage: userMessage?.trim() || undefined
1795
+ };
1796
+ }
1797
+ export function agentScheduleRuntimeTrigger(row) {
1798
+ const effective = effectiveAgentSchedule(row);
1799
+ return {
1800
+ type: "schedule",
1801
+ cron: effective.cron,
1802
+ timezone: effective.timezone ?? undefined,
1803
+ input: effective.input,
1804
+ enabled: effective.enabled,
1805
+ userMessage: effective.userMessage
1806
+ };
1807
+ }
1808
+ export function agentScheduleAdminSnapshot(row) {
1809
+ const overrideEnabled = agentScheduleOverrideState(row);
1810
+ const effective = effectiveAgentSchedule(row);
1811
+ const sourceInput = parseAgentScheduleSourceInput(row);
1812
+ return {
1813
+ id: row.id,
1814
+ agentName: row.agentName,
1815
+ triggerIndex: row.triggerIndex,
1816
+ triggerKey: row.triggerKey,
1817
+ source: {
1818
+ cron: row.cron,
1819
+ timezone: row.timezone ?? null,
1820
+ enabled: row.enabled === 1,
1821
+ input: sourceInput,
1822
+ userMessage: row.userMessage ?? null
1823
+ },
1824
+ overrides: {
1825
+ cron: overrideEnabled.cron ? row.cronOverride : null,
1826
+ timezone: overrideEnabled.timezone ? row.timezoneOverride : null,
1827
+ enabled: overrideEnabled.enabled ? normalizeOverrideEnabled(row.enabledOverride) === 1 : null,
1828
+ input: overrideEnabled.input ? parseScheduleJson(row.inputJsonOverride, null) : null,
1829
+ userMessage: overrideEnabled.userMessage ? row.userMessageOverride : null
1830
+ },
1831
+ overrideEnabled,
1832
+ effective: {
1833
+ cron: effective.cron,
1834
+ timezone: effective.timezone,
1835
+ enabled: effective.enabled,
1836
+ input: effective.input,
1837
+ userMessage: effective.userMessage ?? null
1838
+ },
1839
+ lastRunAt: row.lastRunAt,
1840
+ nextRunAt: row.nextRunAt,
1841
+ service: row.service,
1842
+ createdAt: row.createdAt,
1843
+ updatedAt: row.updatedAt
1844
+ };
1845
+ }
1846
+ export function scheduleNextRunFrom(now, cron, timezone) {
1847
+ validateCron(cron);
1848
+ return nextCronRun(cron, now, timezone ?? undefined);
1849
+ }
1850
+ function nextRunAfterScheduleChange(input) {
1851
+ if (!input.before)
1852
+ return input.fallbackNextRunAt ?? scheduleNextRunFrom(input.now, input.after.cron, input.after.timezone);
1853
+ const timingChanged = input.before.cron !== input.after.cron || (input.before.timezone ?? null) !== (input.after.timezone ?? null);
1854
+ const reenabled = !input.before.enabled && input.after.enabled;
1855
+ if (timingChanged || reenabled)
1856
+ return scheduleNextRunFrom(input.now, input.after.cron, input.after.timezone);
1857
+ return input.currentNextRunAt;
1858
+ }
1859
+ async function findAgentSchedule(control, idValue) {
1860
+ return control.first(`SELECT * FROM ${q("_agent_schedules")} WHERE "id" = ? LIMIT 1`, [idValue]);
1861
+ }
1862
+ export async function loadAgentSchedule(control, idValue) {
1863
+ const row = await findAgentSchedule(control, idValue);
1864
+ if (!row)
1865
+ throw new Error(`Agent schedule not found: ${idValue}`);
1866
+ return row;
1867
+ }
1868
+ export async function listAgentSchedulesForAgent(control, agentName) {
1869
+ return control.all(`SELECT * FROM ${q("_agent_schedules")} WHERE "agentName" = ? ORDER BY "triggerIndex" ASC`, [agentName]);
1870
+ }
1871
+ function applyReset(row, field) {
1872
+ if (field === "cron")
1873
+ row.cronOverride = null;
1874
+ if (field === "timezone")
1875
+ row.timezoneOverride = null;
1876
+ if (field === "enabled")
1877
+ row.enabledOverride = null;
1878
+ if (field === "input") {
1879
+ row.inputJsonOverride = null;
1880
+ row.inputJsonOverrideEnabled = null;
1881
+ }
1882
+ if (field === "userMessage")
1883
+ row.userMessageOverride = null;
1884
+ }
1885
+ function normalizeResetFields(fields) {
1886
+ if (fields === undefined || fields === null)
1887
+ return [];
1888
+ if (!Array.isArray(fields))
1889
+ throw new Error("Schedule reset must be an array");
1890
+ const allowed = new Set(["cron", "timezone", "enabled", "input", "userMessage"]);
1891
+ return fields.map((field) => {
1892
+ if (typeof field !== "string" || !allowed.has(field))
1893
+ throw new Error(`Unknown schedule override field: ${String(field)}`);
1894
+ return field;
1895
+ });
1896
+ }
1897
+ async function persistScheduleOverrideRow(control, row) {
1898
+ await control.run(`UPDATE ${q("_agent_schedules")}
1899
+ SET "cronOverride" = ?, "timezoneOverride" = ?, "enabledOverride" = ?,
1900
+ "inputJsonOverride" = ?, "inputJsonOverrideEnabled" = ?, "userMessageOverride" = ?,
1901
+ "nextRunAt" = ?, "dispatchGeneration" = ?, "dispatchAttempts" = ?,
1902
+ "dispatchLeaseExpiresAt" = ?, "lastDispatchAt" = ?, "lastDispatchError" = ?, "updatedAt" = ?
1903
+ WHERE "id" = ?`, [
1904
+ row.cronOverride,
1905
+ row.timezoneOverride,
1906
+ row.enabledOverride,
1907
+ row.inputJsonOverride,
1908
+ row.inputJsonOverrideEnabled,
1909
+ row.userMessageOverride,
1910
+ row.nextRunAt,
1911
+ row.dispatchGeneration,
1912
+ row.dispatchAttempts,
1913
+ row.dispatchLeaseExpiresAt,
1914
+ row.lastDispatchAt,
1915
+ row.lastDispatchError,
1916
+ row.updatedAt,
1917
+ row.id
1918
+ ]);
1919
+ return loadAgentSchedule(control, row.id);
1920
+ }
1921
+ export async function setAgentScheduleOverrides(control, input) {
1922
+ const current = await loadAgentSchedule(control, input.id);
1923
+ const beforeEffective = effectiveAgentSchedule(current);
1924
+ const before = agentScheduleAdminSnapshot(current);
1925
+ const next = { ...current };
1926
+ const changedFields = new Set();
1927
+ for (const field of normalizeResetFields(input.reset)) {
1928
+ applyReset(next, field);
1929
+ changedFields.add(field);
1930
+ }
1931
+ const overrides = input.overrides && typeof input.overrides === "object" && !Array.isArray(input.overrides)
1932
+ ? input.overrides
1933
+ : {};
1934
+ const allowed = new Set(["cron", "timezone", "enabled", "input", "userMessage"]);
1935
+ for (const key of Object.keys(overrides)) {
1936
+ if (!allowed.has(key))
1937
+ throw new Error(`Unknown schedule override field: ${key}`);
1938
+ }
1939
+ if (hasOwn(overrides, "cron")) {
1940
+ next.cronOverride = normalizeScheduleCronOverride(overrides.cron);
1941
+ changedFields.add("cron");
1942
+ }
1943
+ if (hasOwn(overrides, "timezone")) {
1944
+ next.timezoneOverride = normalizeScheduleTimezoneOverride(overrides.timezone);
1945
+ changedFields.add("timezone");
1946
+ }
1947
+ if (hasOwn(overrides, "enabled")) {
1948
+ if (typeof overrides.enabled !== "boolean")
1949
+ throw new Error("Schedule enabled override must be boolean");
1950
+ next.enabledOverride = overrides.enabled ? 1 : 0;
1951
+ changedFields.add("enabled");
1952
+ }
1953
+ if (hasOwn(overrides, "input")) {
1954
+ next.inputJsonOverride = serializeScheduleJson(overrides.input);
1955
+ next.inputJsonOverrideEnabled = 1;
1956
+ changedFields.add("input");
1957
+ }
1958
+ if (hasOwn(overrides, "userMessage")) {
1959
+ next.userMessageOverride = normalizeScheduleUserMessage(overrides.userMessage, "Schedule user message override");
1960
+ changedFields.add("userMessage");
1961
+ }
1962
+ const now = input.now ?? nowIso();
1963
+ const afterEffective = effectiveAgentSchedule(next);
1964
+ next.nextRunAt = nextRunAfterScheduleChange({
1965
+ before: beforeEffective,
1966
+ after: afterEffective,
1967
+ currentNextRunAt: current.nextRunAt,
1968
+ now
1969
+ });
1970
+ if (stableJson(beforeEffective) !== stableJson(afterEffective) || next.nextRunAt !== current.nextRunAt) {
1971
+ next.dispatchGeneration = Number(current.dispatchGeneration ?? 1) + 1;
1972
+ next.dispatchAttempts = 0;
1973
+ next.dispatchLeaseExpiresAt = null;
1974
+ next.lastDispatchAt = null;
1975
+ next.lastDispatchError = null;
1976
+ }
1977
+ next.updatedAt = now;
1978
+ const row = await persistScheduleOverrideRow(control, next);
1979
+ return {
1980
+ row,
1981
+ before,
1982
+ after: agentScheduleAdminSnapshot(row),
1983
+ changedFields: [...changedFields]
1984
+ };
1985
+ }
1986
+ export async function resetAgentScheduleOverrides(control, input) {
1987
+ const current = await loadAgentSchedule(control, input.id);
1988
+ const beforeEffective = effectiveAgentSchedule(current);
1989
+ const before = agentScheduleAdminSnapshot(current);
1990
+ const next = { ...current };
1991
+ const fields = input.fields?.length ? input.fields : ["cron", "timezone", "enabled", "input", "userMessage"];
1992
+ for (const field of fields)
1993
+ applyReset(next, field);
1994
+ const now = input.now ?? nowIso();
1995
+ const afterEffective = effectiveAgentSchedule(next);
1996
+ next.nextRunAt = nextRunAfterScheduleChange({
1997
+ before: beforeEffective,
1998
+ after: afterEffective,
1999
+ currentNextRunAt: current.nextRunAt,
2000
+ now
2001
+ });
2002
+ if (stableJson(beforeEffective) !== stableJson(afterEffective) || next.nextRunAt !== current.nextRunAt) {
2003
+ next.dispatchGeneration = Number(current.dispatchGeneration ?? 1) + 1;
2004
+ next.dispatchAttempts = 0;
2005
+ next.dispatchLeaseExpiresAt = null;
2006
+ next.lastDispatchAt = null;
2007
+ next.lastDispatchError = null;
2008
+ }
2009
+ next.updatedAt = now;
2010
+ const row = await persistScheduleOverrideRow(control, next);
2011
+ return {
2012
+ row,
2013
+ before,
2014
+ after: agentScheduleAdminSnapshot(row),
2015
+ changedFields: fields
2016
+ };
2017
+ }
2018
+ export async function upsertAgentSchedule(control, input) {
2019
+ const now = input.now ?? nowIso();
2020
+ const idValue = agentScheduleId(input.agentName, input.triggerIndex, input.trigger);
2021
+ const existing = await findAgentSchedule(control, idValue);
2022
+ const row = {
2023
+ id: idValue,
2024
+ agentName: input.agentName,
2025
+ triggerIndex: input.triggerIndex,
2026
+ triggerKey: agentTriggerKey(input.agentName, input.triggerIndex, input.trigger),
2027
+ inputJson: stableJson(input.trigger.input ?? {}),
2028
+ cron: input.trigger.cron,
2029
+ timezone: input.trigger.timezone ?? null,
2030
+ enabled: input.trigger.enabled === false ? 0 : 1,
2031
+ userMessage: normalizeScheduleUserMessage(input.trigger.userMessage, "Schedule userMessage"),
2032
+ cronOverride: existing?.cronOverride ?? null,
2033
+ timezoneOverride: existing?.timezoneOverride ?? null,
2034
+ enabledOverride: normalizeOverrideEnabled(existing?.enabledOverride),
2035
+ inputJsonOverride: existing?.inputJsonOverride ?? null,
2036
+ inputJsonOverrideEnabled: existing?.inputJsonOverrideEnabled === 1 ? 1 : null,
2037
+ userMessageOverride: existing?.userMessageOverride ?? null,
2038
+ lastRunAt: existing?.lastRunAt ?? null,
2039
+ nextRunAt: null,
2040
+ service: input.service ?? "scheduler",
2041
+ dispatchGeneration: existing?.dispatchGeneration ?? 1,
2042
+ dispatchAttempts: existing?.dispatchAttempts ?? 0,
2043
+ dispatchLeaseExpiresAt: existing?.dispatchLeaseExpiresAt ?? null,
2044
+ lastDispatchAt: existing?.lastDispatchAt ?? null,
2045
+ lastDispatchError: existing?.lastDispatchError ?? null,
2046
+ createdAt: existing?.createdAt ?? now,
2047
+ updatedAt: now
2048
+ };
2049
+ row.nextRunAt = nextRunAfterScheduleChange({
2050
+ before: existing ? effectiveAgentSchedule(existing) : null,
2051
+ after: effectiveAgentSchedule(row),
2052
+ currentNextRunAt: existing?.nextRunAt ?? null,
2053
+ fallbackNextRunAt: input.nextRunAt,
2054
+ now
2055
+ });
2056
+ const definitionChanged = Boolean(existing) && (existing.agentName !== row.agentName
2057
+ || existing.triggerIndex !== row.triggerIndex
2058
+ || existing.triggerKey !== row.triggerKey
2059
+ || existing.inputJson !== row.inputJson
2060
+ || existing.cron !== row.cron
2061
+ || existing.timezone !== row.timezone
2062
+ || existing.enabled !== row.enabled
2063
+ || existing.userMessage !== row.userMessage
2064
+ || existing.nextRunAt !== row.nextRunAt);
2065
+ if (definitionChanged) {
2066
+ row.dispatchGeneration = Number(existing?.dispatchGeneration ?? 1) + 1;
2067
+ row.dispatchAttempts = 0;
2068
+ row.dispatchLeaseExpiresAt = null;
2069
+ row.lastDispatchAt = null;
2070
+ row.lastDispatchError = null;
2071
+ }
2072
+ await control.run(`INSERT INTO ${q("_agent_schedules")}
2073
+ ("id", "agentName", "triggerIndex", "triggerKey", "inputJson", "cron", "timezone", "enabled", "userMessage",
2074
+ "cronOverride", "timezoneOverride", "enabledOverride", "inputJsonOverride", "inputJsonOverrideEnabled", "userMessageOverride",
2075
+ "lastRunAt", "nextRunAt", "service", "dispatchGeneration", "dispatchAttempts", "dispatchLeaseExpiresAt",
2076
+ "lastDispatchAt", "lastDispatchError", "createdAt", "updatedAt")
2077
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2078
+ ON CONFLICT("id") DO UPDATE SET
2079
+ "agentName" = excluded."agentName",
2080
+ "triggerIndex" = excluded."triggerIndex",
2081
+ "triggerKey" = excluded."triggerKey",
2082
+ "inputJson" = excluded."inputJson",
2083
+ "cron" = excluded."cron",
2084
+ "timezone" = excluded."timezone",
2085
+ "enabled" = excluded."enabled",
2086
+ "userMessage" = excluded."userMessage",
2087
+ "lastRunAt" = excluded."lastRunAt",
2088
+ "nextRunAt" = excluded."nextRunAt",
2089
+ "service" = excluded."service",
2090
+ "dispatchGeneration" = excluded."dispatchGeneration",
2091
+ "dispatchAttempts" = excluded."dispatchAttempts",
2092
+ "dispatchLeaseExpiresAt" = excluded."dispatchLeaseExpiresAt",
2093
+ "lastDispatchAt" = excluded."lastDispatchAt",
2094
+ "lastDispatchError" = excluded."lastDispatchError",
2095
+ "updatedAt" = excluded."updatedAt"`, [
2096
+ row.id,
2097
+ row.agentName,
2098
+ row.triggerIndex,
2099
+ row.triggerKey,
2100
+ row.inputJson,
2101
+ row.cron,
2102
+ row.timezone,
2103
+ row.enabled,
2104
+ row.userMessage,
2105
+ row.cronOverride,
2106
+ row.timezoneOverride,
2107
+ row.enabledOverride,
2108
+ row.inputJsonOverride,
2109
+ row.inputJsonOverrideEnabled,
2110
+ row.userMessageOverride,
2111
+ row.lastRunAt,
2112
+ row.nextRunAt,
2113
+ row.service,
2114
+ row.dispatchGeneration,
2115
+ row.dispatchAttempts,
2116
+ row.dispatchLeaseExpiresAt,
2117
+ row.lastDispatchAt,
2118
+ row.lastDispatchError,
2119
+ row.createdAt,
2120
+ row.updatedAt
2121
+ ]);
2122
+ return (await control.first(`SELECT * FROM ${q("_agent_schedules")} WHERE "id" = ?`, [row.id])) ?? row;
2123
+ }
2124
+ export async function listDueAgentSchedules(control, input) {
2125
+ const limit = Math.max(1, Math.min(500, input.limit ?? 100));
2126
+ return control.all(`SELECT * FROM ${q("_agent_schedules")}
2127
+ WHERE (("enabledOverride" IS NULL AND "enabled" = 1) OR "enabledOverride" = 1)
2128
+ AND "nextRunAt" IS NOT NULL AND "nextRunAt" <= ?
2129
+ ORDER BY "nextRunAt" ASC
2130
+ LIMIT ?`, [input.now, limit]);
2131
+ }
2132
+ export const agentScheduleMaxDispatchAttempts = 5;
2133
+ export async function claimDueAgentSchedules(control, input) {
2134
+ const limit = Math.max(1, Math.min(500, input.limit ?? 100));
2135
+ const maxAttempts = Math.max(1, input.maxAttempts ?? agentScheduleMaxDispatchAttempts);
2136
+ await control.run(`UPDATE ${q("_agent_schedules")} SET "enabled" = 0, "enabledOverride" = 0,
2137
+ "lastDispatchError" = 'dispatch-attempts-exhausted', "dispatchLeaseExpiresAt" = NULL, "updatedAt" = ?
2138
+ WHERE (("enabledOverride" IS NULL AND "enabled" = 1) OR "enabledOverride" = 1)
2139
+ AND "nextRunAt" IS NOT NULL AND "nextRunAt" <= ? AND COALESCE("dispatchAttempts", 0) >= ?
2140
+ AND ("dispatchLeaseExpiresAt" IS NULL OR "dispatchLeaseExpiresAt" <= ?)`, [input.now, input.now, maxAttempts, input.wallNow]);
2141
+ const due = await control.all(`SELECT * FROM ${q("_agent_schedules")}
2142
+ WHERE (("enabledOverride" IS NULL AND "enabled" = 1) OR "enabledOverride" = 1)
2143
+ AND "nextRunAt" IS NOT NULL AND "nextRunAt" <= ? AND COALESCE("dispatchAttempts", 0) < ?
2144
+ AND ("dispatchLeaseExpiresAt" IS NULL OR "dispatchLeaseExpiresAt" <= ?)
2145
+ ORDER BY "nextRunAt" ASC LIMIT ?`, [input.now, maxAttempts, input.wallNow, limit]);
2146
+ const claimed = [];
2147
+ for (const row of due) {
2148
+ if (!row.nextRunAt)
2149
+ continue;
2150
+ const generation = Number(row.dispatchGeneration ?? 1);
2151
+ const attempts = Number(row.dispatchAttempts ?? 0);
2152
+ await control.run(`UPDATE ${q("_agent_schedules")} SET "dispatchGeneration" = ?, "dispatchAttempts" = ?,
2153
+ "dispatchLeaseExpiresAt" = ?, "lastDispatchAt" = ?, "lastDispatchError" = NULL, "updatedAt" = ?
2154
+ WHERE "id" = ? AND COALESCE("dispatchGeneration", 1) = ? AND COALESCE("dispatchAttempts", 0) = ?
2155
+ AND "nextRunAt" = ? AND ("dispatchLeaseExpiresAt" IS NULL OR "dispatchLeaseExpiresAt" <= ?)`, [generation, attempts + 1, input.leaseExpiresAt, input.wallNow, input.now, row.id, generation, attempts, row.nextRunAt, input.wallNow]);
2156
+ const current = await control.first(`SELECT * FROM ${q("_agent_schedules")} WHERE "id" = ? LIMIT 1`, [row.id]);
2157
+ if (current?.nextRunAt === row.nextRunAt
2158
+ && Number(current.dispatchGeneration ?? 1) === generation
2159
+ && Number(current.dispatchAttempts ?? 0) === attempts + 1
2160
+ && current.dispatchLeaseExpiresAt === input.leaseExpiresAt) {
2161
+ claimed.push({ id: current.id, agentName: current.agentName, generation, bookedAt: current.nextRunAt });
2162
+ }
2163
+ }
2164
+ return claimed;
2165
+ }
2166
+ export async function loadClaimedAgentSchedule(control, input) {
2167
+ return control.first(`SELECT * FROM ${q("_agent_schedules")} WHERE "id" = ? AND "nextRunAt" = ?
2168
+ AND COALESCE("dispatchGeneration", 1) = ? LIMIT 1`, [input.id, input.bookedAt, input.generation]);
2169
+ }
2170
+ export async function releaseAgentScheduleDispatch(control, input) {
2171
+ await control.run(`UPDATE ${q("_agent_schedules")} SET "dispatchLeaseExpiresAt" = NULL, "lastDispatchError" = ?, "updatedAt" = ?
2172
+ WHERE "id" = ? AND "nextRunAt" = ? AND COALESCE("dispatchGeneration", 1) = ?`, [input.error, input.now ?? nowIso(), input.id, input.bookedAt, input.generation]);
2173
+ }
2174
+ export async function advanceAgentSchedule(control, input) {
2175
+ await control.run(`UPDATE ${q("_agent_schedules")} SET "lastRunAt" = ?, "nextRunAt" = ?,
2176
+ "dispatchGeneration" = COALESCE("dispatchGeneration", 1) + 1, "dispatchAttempts" = 0,
2177
+ "dispatchLeaseExpiresAt" = NULL, "lastDispatchAt" = NULL, "lastDispatchError" = NULL, "updatedAt" = ?
2178
+ WHERE "id" = ? ${input.generation === undefined ? "" : `AND COALESCE("dispatchGeneration", 1) = ? AND "nextRunAt" = ?`}`, [input.lastRunAt, input.nextRunAt, input.now ?? nowIso(), input.id,
2179
+ ...(input.generation === undefined ? [] : [input.generation, input.lastRunAt])]);
2180
+ }
2181
+ export async function createAgentTriggerEvent(control, input) {
2182
+ const now = input.now ?? wallNowIso();
2183
+ const row = {
2184
+ id: randomId("agent_evt"),
2185
+ agentName: input.agentName,
2186
+ triggerType: input.triggerType,
2187
+ triggerKey: input.triggerKey,
2188
+ sourceTable: input.sourceTable ?? null,
2189
+ sourceOp: input.sourceOp ?? null,
2190
+ rowId: input.rowId ?? null,
2191
+ inputJson: input.input === undefined ? null : stableJson(input.input),
2192
+ idempotencyKey: input.idempotencyKey ?? null,
2193
+ status: input.status ?? "queued",
2194
+ runId: null,
2195
+ error: input.error ?? null,
2196
+ createdAt: now,
2197
+ updatedAt: now
2198
+ };
2199
+ await control.run(`INSERT INTO ${q("_agent_trigger_events")}
2200
+ ("id", "agentName", "triggerType", "triggerKey", "sourceTable", "sourceOp", "rowId", "inputJson", "idempotencyKey", "status", "runId", "error", "createdAt", "updatedAt")
2201
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2202
+ row.id,
2203
+ row.agentName,
2204
+ row.triggerType,
2205
+ row.triggerKey,
2206
+ row.sourceTable,
2207
+ row.sourceOp,
2208
+ row.rowId,
2209
+ row.inputJson,
2210
+ row.idempotencyKey,
2211
+ row.status,
2212
+ row.runId,
2213
+ row.error,
2214
+ row.createdAt,
2215
+ row.updatedAt
2216
+ ]);
2217
+ return row;
2218
+ }
2219
+ async function markAgentTriggerEvent(control, eventId, input) {
2220
+ await control.run(`UPDATE ${q("_agent_trigger_events")} SET "status" = ?, "runId" = COALESCE(?, "runId"), "error" = ?, "updatedAt" = ? WHERE "id" = ?`, [input.status, input.runId ?? null, input.error ?? null, input.now ?? wallNowIso(), eventId]);
2221
+ }
2222
+ export const markAgentTriggerEventQueued = (control, eventId, input = {}) => markAgentTriggerEvent(control, eventId, { ...input, status: "queued" });
2223
+ export const markAgentTriggerEventRunning = (control, eventId, input = {}) => markAgentTriggerEvent(control, eventId, { ...input, status: "running" });
2224
+ export const markAgentTriggerEventCompleted = (control, eventId, input = {}) => markAgentTriggerEvent(control, eventId, { ...input, status: "completed" });
2225
+ export const markAgentTriggerEventFailed = (control, eventId, input = {}) => markAgentTriggerEvent(control, eventId, { ...input, status: "failed" });
2226
+ export const markAgentTriggerEventSkipped = (control, eventId, input = {}) => markAgentTriggerEvent(control, eventId, { ...input, status: "skipped" });
2227
+ export async function loadAgentTriggerEvent(control, eventId) {
2228
+ const row = await control.first(`SELECT * FROM ${q("_agent_trigger_events")} WHERE "id" = ? LIMIT 1`, [eventId]);
2229
+ if (!row)
2230
+ throw new AgentHttpError("Agent trigger event not found", 404);
2231
+ return row;
2232
+ }
2233
+ export async function findAgentTriggerEventByIdempotencyKey(control, idempotencyKey) {
2234
+ return control.first(`SELECT * FROM ${q("_agent_trigger_events")} WHERE "idempotencyKey" = ? LIMIT 1`, [idempotencyKey]);
2235
+ }
2236
+ export function directAgentTriggerKey(agentName) {
2237
+ return `direct:${agentName}`;
2238
+ }
2239
+ function directBindingFingerprintFromInput(inputJson) {
2240
+ if (!inputJson)
2241
+ return "new";
2242
+ try {
2243
+ const input = JSON.parse(inputJson);
2244
+ const conversation = input.conversation;
2245
+ if (!conversation || typeof conversation !== "object")
2246
+ return "new";
2247
+ const record = conversation;
2248
+ // Compare the derived id, not the raw external thread key, so replay checks
2249
+ // do not depend on provider ids being used as storage identifiers.
2250
+ if (record.mode === "thread" && typeof record.conversationId === "string") {
2251
+ return `thread:${record.conversationId}`;
2252
+ }
2253
+ if (record.mode === "chat" && typeof record.conversationId === "string") {
2254
+ return `chat:${record.conversationId}`;
2255
+ }
2256
+ return "new";
2257
+ }
2258
+ catch {
2259
+ return "new";
2260
+ }
2261
+ }
2262
+ export async function findAgentRun(control, runId) {
2263
+ return control.first(`SELECT * FROM ${q("_agent_runs")} WHERE "runId" = ? LIMIT 1`, [runId]);
2264
+ }
2265
+ export async function loadAgentRun(control, runId) {
2266
+ const row = await findAgentRun(control, runId);
2267
+ if (!row)
2268
+ throw new AgentHttpError("Agent run not found", 404);
2269
+ return row;
2270
+ }
2271
+ export async function createOrLoadDirectAgentInvocationRecord(input) {
2272
+ const triggerKey = directAgentTriggerKey(input.agent.name);
2273
+ if (input.idempotencyKey !== undefined && input.idempotencyKey !== null) {
2274
+ const existing = await findAgentTriggerEventByIdempotencyKey(input.control, input.idempotencyKey);
2275
+ if (existing) {
2276
+ if (existing.agentName !== input.agent.name || existing.triggerType !== "direct" || existing.triggerKey !== triggerKey) {
2277
+ throw new Error(`Agent idempotency key conflict for ${input.agent.name}`);
2278
+ }
2279
+ const existingFingerprint = directBindingFingerprintFromInput(existing.inputJson);
2280
+ if ((input.bindingFingerprint ?? "new") !== existingFingerprint) {
2281
+ throw new Error(`Agent idempotency key conflict for ${input.agent.name}: conversation binding changed`);
2282
+ }
2283
+ return {
2284
+ created: false,
2285
+ event: existing,
2286
+ run: existing.runId ? await findAgentRun(input.control, existing.runId) : null
2287
+ };
2288
+ }
2289
+ }
2290
+ const event = await createAgentTriggerEvent(input.control, {
2291
+ agentName: input.agent.name,
2292
+ triggerType: "direct",
2293
+ triggerKey,
2294
+ input: input.invocationInput,
2295
+ idempotencyKey: input.idempotencyKey ?? null
2296
+ });
2297
+ const run = await createAgentAutomationRun({
2298
+ control: input.control,
2299
+ agent: input.agent,
2300
+ triggerType: "direct",
2301
+ eventId: event.id,
2302
+ requestId: input.requestId,
2303
+ metadata: input.metadata,
2304
+ conversationId: input.conversationId
2305
+ });
2306
+ await markAgentTriggerEventQueued(input.control, event.id, { runId: run.runId });
2307
+ return {
2308
+ created: true,
2309
+ event: { ...event, runId: run.runId },
2310
+ run
2311
+ };
2312
+ }
2313
+ export async function createAgentAutomationRun(input) {
2314
+ const now = nowIso();
2315
+ const run = {
2316
+ runId: randomId("agent_run"),
2317
+ conversationId: input.conversationId ?? (input.triggerType === "direct" ? `agent_direct:${input.eventId}` : `agent_auto:${input.eventId}`),
2318
+ agentName: input.agent.name,
2319
+ requestId: input.requestId ?? randomId("agent_req"),
2320
+ triggerType: input.triggerType,
2321
+ inputMessageId: null,
2322
+ status: "queued",
2323
+ startedAt: now,
2324
+ heartbeatAt: now,
2325
+ finishedAt: null,
2326
+ error: null,
2327
+ usageEventId: null,
2328
+ auditEventId: null,
2329
+ metadataJson: input.metadata === undefined ? null : stableJson(input.metadata)
2330
+ };
2331
+ await input.control.run(`INSERT INTO ${q("_agent_runs")}
2332
+ ("runId", "conversationId", "agentName", "requestId", "triggerType", "inputMessageId", "status", "startedAt", "heartbeatAt", "finishedAt",
2333
+ "error", "usageEventId", "auditEventId", "metadataJson")
2334
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2335
+ run.runId,
2336
+ run.conversationId,
2337
+ run.agentName,
2338
+ run.requestId,
2339
+ run.triggerType,
2340
+ run.inputMessageId,
2341
+ run.status,
2342
+ run.startedAt,
2343
+ run.heartbeatAt,
2344
+ run.finishedAt,
2345
+ run.error,
2346
+ run.usageEventId,
2347
+ run.auditEventId,
2348
+ run.metadataJson
2349
+ ]);
2350
+ return run;
2351
+ }
2352
+ // A crashed isolate can leave a running run with no writer left to finish it;
2353
+ // past this age it no longer blocks a retry. Queue wait is deliberately
2354
+ // governed by a separate, much longer horizon below: work that has not
2355
+ // started must not spend its execution budget while waiting for capacity.
2356
+ export const activeAgentRunStaleSeconds = 5 * 60;
2357
+ export const queuedAgentRunStaleSeconds = 24 * 60 * 60;
2358
+ export async function findActiveAgentRun(control, input) {
2359
+ const now = input.now ?? nowIso();
2360
+ const nowMs = new Date(now).getTime();
2361
+ const runningStaleBefore = new Date(nowMs - activeAgentRunStaleSeconds * 1000).toISOString();
2362
+ const queuedStaleBefore = new Date(nowMs - queuedAgentRunStaleSeconds * 1000).toISOString();
2363
+ return control.first(`SELECT * FROM ${q("_agent_runs")}
2364
+ WHERE "agentName" = ? AND "conversationId" = ?
2365
+ AND (("status" = 'running' AND COALESCE("heartbeatAt", "startedAt") > ?) OR ("status" = 'queued' AND "startedAt" > ?))
2366
+ ORDER BY "startedAt" DESC LIMIT 1`, [input.agentName, input.conversationId, runningStaleBefore, queuedStaleBefore]);
2367
+ }
2368
+ /**
2369
+ * A run whose writer is known to be gone rather than merely late.
2370
+ *
2371
+ * The staleness sweep does not write this and must not: from a horizon it
2372
+ * cannot tell a restart from a turn that simply ran long, and claiming a
2373
+ * restart sends debugging after reloads that never happened. The conversation
2374
+ * object can tell, because it knows which runs the isolate it is running in
2375
+ * actually holds, so it settles its own orphans under this label the moment it
2376
+ * next reconciles (`isOrphanedRun` in agent-ai-chat.ts). Rows written by
2377
+ * earlier builds carry it too, which is why isReapedAgentRunError still
2378
+ * matches it.
2379
+ */
2380
+ export const interruptedAgentRunError = "Interrupted by restart or deployment";
2381
+ const staleAgentRunErrorPrefix = "Timed out: no completion within";
2382
+ const staleQueuedAgentRunErrorPrefix = "Timed out: did not start within";
2383
+ /**
2384
+ * What the execution sweep actually knows: the run was still running when it
2385
+ * passed the horizon. Names the configured horizon so a reader can tell
2386
+ * "this turn was too slow" from "something killed the isolate".
2387
+ */
2388
+ export function staleAgentRunError(staleSeconds = activeAgentRunStaleSeconds) {
2389
+ return `${staleAgentRunErrorPrefix} the ${staleSeconds}s agent run staleness horizon`;
2390
+ }
2391
+ /** Truthful terminal label for work that remained queued without starting. */
2392
+ export function staleQueuedAgentRunError(staleSeconds = queuedAgentRunStaleSeconds) {
2393
+ return `${staleQueuedAgentRunErrorPrefix} the ${staleSeconds}s agent queue-wait horizon`;
2394
+ }
2395
+ /**
2396
+ * True for any run the staleness sweep marked failed, under either current
2397
+ * label or the legacy one. Retry, redispatch and coverage checks
2398
+ * treat such a run as "no live writer" rather than as a real turn outcome, so
2399
+ * they must match both.
2400
+ */
2401
+ export function isReapedAgentRunError(error) {
2402
+ if (!error)
2403
+ return false;
2404
+ return error === interruptedAgentRunError
2405
+ || error.startsWith(staleAgentRunErrorPrefix)
2406
+ || error.startsWith(staleQueuedAgentRunErrorPrefix);
2407
+ }
2408
+ /**
2409
+ * Terminal bookkeeping for runs no writer is left to finish. A deploy or
2410
+ * crash kills the writer between run creation and the completed/failed
2411
+ * update, so the row sits at running with finishedAt null forever and runs
2412
+ * evidence reports a turn that is not executing.
2413
+ *
2414
+ * A running row is judged on `heartbeatAt`, not on when it started (2026-08-25).
2415
+ * On `startedAt` this swept up **live** turns: a turn is not late because it is
2416
+ * long, and a nine-round tool loop passes five minutes without anything being
2417
+ * wrong with it. What that cost was not a stray row — it was a member reading
2418
+ * "Birch ran into a problem while answering" twenty-four seconds before the
2419
+ * same run delivered its answer, because the sweep's terminal write is what the
2420
+ * conversation reports. The heartbeat is touched per model step, so a wedged
2421
+ * turn stops beating and is still reaped on time, which is the whole point of
2422
+ * having a horizon. Rows written before the column coalesce to `startedAt` and
2423
+ * are swept exactly as they were.
2424
+ *
2425
+ * A queued row keeps its own queue-wait horizon against `startedAt` and its own
2426
+ * label, because its execution has not begun and there is nothing to beat.
2427
+ * Status and timestamp predicates are re-checked in each UPDATE so adoption
2428
+ * cannot be reaped using its former queue clock.
2429
+ */
2430
+ export async function sweepStaleAgentRuns(control, input = {}) {
2431
+ const now = input.now ?? nowIso();
2432
+ const nowMs = new Date(now).getTime();
2433
+ const runningStaleBefore = new Date(nowMs - activeAgentRunStaleSeconds * 1000).toISOString();
2434
+ const queuedStaleBefore = new Date(nowMs - queuedAgentRunStaleSeconds * 1000).toISOString();
2435
+ const limit = Math.max(1, Math.min(500, input.limit ?? 100));
2436
+ const stale = await control.all(`SELECT "runId", "status" FROM ${q("_agent_runs")}
2437
+ WHERE ("status" = 'running' AND COALESCE("heartbeatAt", "startedAt") <= ?)
2438
+ OR ("status" = 'queued' AND "startedAt" <= ?)
2439
+ ORDER BY "startedAt" ASC LIMIT ?`, [runningStaleBefore, queuedStaleBefore, limit]);
2440
+ if (stale.length === 0)
2441
+ return [];
2442
+ const runningIds = stale.filter((row) => row.status === "running").map((row) => row.runId);
2443
+ const queuedIds = stale.filter((row) => row.status === "queued").map((row) => row.runId);
2444
+ if (runningIds.length) {
2445
+ await control.run(`UPDATE ${q("_agent_runs")}
2446
+ SET "status" = 'failed', "error" = ?, "finishedAt" = ?
2447
+ WHERE "runId" IN (${runningIds.map(() => "?").join(", ")})
2448
+ AND "status" = 'running' AND COALESCE("heartbeatAt", "startedAt") <= ?`, [staleAgentRunError(), now, ...runningIds, runningStaleBefore]);
2449
+ }
2450
+ if (queuedIds.length) {
2451
+ await control.run(`UPDATE ${q("_agent_runs")}
2452
+ SET "status" = 'failed', "error" = ?, "finishedAt" = ?
2453
+ WHERE "runId" IN (${queuedIds.map(() => "?").join(", ")})
2454
+ AND "status" = 'queued' AND "startedAt" <= ?`, [staleQueuedAgentRunError(), now, ...queuedIds, queuedStaleBefore]);
2455
+ }
2456
+ return stale.map((row) => row.runId);
2457
+ }
2458
+ /**
2459
+ * Deletes poll/idempotency evidence after each owning Agent's configured
2460
+ * evidence window. Request replay and outcome polling are guaranteed only
2461
+ * while their receipt and run evidence remain inside that window.
2462
+ */
2463
+ export async function sweepAgentRunEvidence(control, input = {}) {
2464
+ const nowMs = new Date(input.now ?? nowIso()).getTime();
2465
+ const limit = Math.max(1, Math.min(2_000, input.limit ?? 500));
2466
+ const ttlForAgent = input.runEvidenceTtlMsForAgent
2467
+ ?? (() => defaultAgentRunEvidenceTtlMs);
2468
+ const pageSize = Math.min(500, Math.max(100, limit));
2469
+ const earliestPossibleExpiry = new Date(nowMs - minAgentRunEvidenceTtlMs).toISOString();
2470
+ const runIds = [];
2471
+ let runCursor = null;
2472
+ while (runIds.length < limit) {
2473
+ const terminalRuns = await control.all(`SELECT "runId", "agentName", "finishedAt"
2474
+ FROM ${q("_agent_runs")}
2475
+ WHERE "status" IN ('completed', 'cancelled', 'failed', 'skipped')
2476
+ AND "finishedAt" IS NOT NULL
2477
+ AND "finishedAt" < ?
2478
+ ${runCursor ? `AND ("finishedAt" > ? OR ("finishedAt" = ? AND "runId" > ?))` : ""}
2479
+ ORDER BY "finishedAt" ASC, "runId" ASC
2480
+ LIMIT ?`, runCursor
2481
+ ? [earliestPossibleExpiry, runCursor.finishedAt, runCursor.finishedAt, runCursor.runId, pageSize]
2482
+ : [earliestPossibleExpiry, pageSize]);
2483
+ for (const row of terminalRuns) {
2484
+ const finishedAtMs = row.finishedAt ? new Date(row.finishedAt).getTime() : Number.NaN;
2485
+ const ttlMs = ttlForAgent(row.agentName);
2486
+ if (Number.isFinite(finishedAtMs) && Number.isFinite(ttlMs) && nowMs - finishedAtMs > ttlMs) {
2487
+ runIds.push(row.runId);
2488
+ if (runIds.length === limit)
2489
+ break;
2490
+ }
2491
+ }
2492
+ const last = terminalRuns.at(-1);
2493
+ if (!last?.finishedAt || terminalRuns.length < pageSize || runIds.length === limit)
2494
+ break;
2495
+ runCursor = { finishedAt: last.finishedAt, runId: last.runId };
2496
+ }
2497
+ for (let offset = 0; offset < runIds.length; offset += 50) {
2498
+ const batch = runIds.slice(offset, offset + 50);
2499
+ await control.run(`DELETE FROM ${q("_agent_runs")} WHERE "runId" IN (${batch.map(() => "?").join(", ")})`, batch);
2500
+ }
2501
+ const requestIds = [];
2502
+ let receiptCursor = null;
2503
+ while (requestIds.length < limit) {
2504
+ const receipts = await control.all(`SELECT r."id", r."createdAt", i."agentName"
2505
+ FROM ${q("_agent_requests")} r
2506
+ LEFT JOIN ${q("_agent_instances")} i ON i."conversationId" = r."conversationId"
2507
+ WHERE r."createdAt" < ?
2508
+ ${receiptCursor ? `AND (r."createdAt" > ? OR (r."createdAt" = ? AND r."id" > ?))` : ""}
2509
+ ORDER BY r."createdAt" ASC, r."id" ASC
2510
+ LIMIT ?`, receiptCursor
2511
+ ? [earliestPossibleExpiry, receiptCursor.createdAt, receiptCursor.createdAt, receiptCursor.id, pageSize]
2512
+ : [earliestPossibleExpiry, pageSize]);
2513
+ for (const row of receipts) {
2514
+ const createdAtMs = new Date(row.createdAt).getTime();
2515
+ const ttlMs = row.agentName
2516
+ ? ttlForAgent(row.agentName)
2517
+ : defaultAgentRunEvidenceTtlMs;
2518
+ if (Number.isFinite(createdAtMs) && Number.isFinite(ttlMs) && nowMs - createdAtMs > ttlMs) {
2519
+ requestIds.push(row.id);
2520
+ if (requestIds.length === limit)
2521
+ break;
2522
+ }
2523
+ }
2524
+ const last = receipts.at(-1);
2525
+ if (!last || receipts.length < pageSize || requestIds.length === limit)
2526
+ break;
2527
+ receiptCursor = { createdAt: last.createdAt, id: last.id };
2528
+ }
2529
+ for (let offset = 0; offset < requestIds.length; offset += 50) {
2530
+ const batch = requestIds.slice(offset, offset + 50);
2531
+ await control.run(`DELETE FROM ${q("_agent_requests")} WHERE "id" IN (${batch.map(() => "?").join(", ")})`, batch);
2532
+ }
2533
+ return { runIds, requestIds };
2534
+ }
2535
+ /**
2536
+ * Mark a run as still working, without touching anything else about it.
2537
+ *
2538
+ * One column, one row, no read first — it runs once per model step inside a
2539
+ * turn that is already doing far more expensive things, and it must never be
2540
+ * the reason a turn fails. A run that has already reached a terminal status is
2541
+ * left alone by the status predicate rather than by a check here, so a beat
2542
+ * that lands in the teardown race cannot resurrect a finished row.
2543
+ */
2544
+ export async function touchAgentRunHeartbeat(control, runId, now = nowIso()) {
2545
+ await control.run(`UPDATE ${q("_agent_runs")} SET "heartbeatAt" = ? WHERE "runId" = ? AND "status" = 'running'`, [now, runId]);
2546
+ }
2547
+ export async function updateAgentRun(control, runId, input) {
2548
+ const current = await control.first(`SELECT * FROM ${q("_agent_runs")} WHERE "runId" = ? LIMIT 1`, [runId]);
2549
+ if (!current)
2550
+ throw new AgentHttpError("Agent run not found", 404);
2551
+ const status = input.status ?? current.status;
2552
+ const startedAt = input.startedAt
2553
+ ?? (current.status === "queued" && status === "running" ? nowIso() : current.startedAt);
2554
+ const finishedAt = input.finishedAt !== undefined
2555
+ ? input.finishedAt
2556
+ : status === "completed" || status === "cancelled" || status === "failed" || status === "skipped"
2557
+ ? nowIso()
2558
+ : current.finishedAt;
2559
+ // Execution starting is itself a sign of life, and the queue wait before it
2560
+ // is not: a row that sat queued for an hour must not begin its running life
2561
+ // already an hour stale. Every other update leaves the mark where the turn's
2562
+ // own steps put it.
2563
+ const heartbeatAt = startedAt !== current.startedAt ? startedAt : current.heartbeatAt;
2564
+ await control.run(`UPDATE ${q("_agent_runs")}
2565
+ SET "status" = ?, "startedAt" = ?, "heartbeatAt" = ?, "error" = ?, "finishedAt" = ?, "metadataJson" = ?
2566
+ WHERE "runId" = ?`, [
2567
+ status,
2568
+ startedAt,
2569
+ heartbeatAt,
2570
+ input.error !== undefined ? input.error : current.error,
2571
+ finishedAt,
2572
+ input.metadata !== undefined ? stableJson(input.metadata) : current.metadataJson,
2573
+ runId
2574
+ ]);
2575
+ }
2576
+ export async function touchAgentInstance(control, instanceId, input = {}) {
2577
+ const now = nowIso();
2578
+ await control.run(`UPDATE ${q("_agent_instances")}
2579
+ SET "lastActiveAt" = ?, "expiresAt" = ?, "updatedAt" = ?, "lastError" = ?
2580
+ WHERE "id" = ?`, [now, agentExpiresAt(input.ttlSeconds), now, input.lastError ?? null, instanceId]);
2581
+ }
2582
+ export async function listExpiredAgentInstances(control, input = {}) {
2583
+ const now = input.now ?? nowIso();
2584
+ const staleDeletingBefore = new Date(new Date(now).getTime() - staleDeletingRetrySeconds * 1000).toISOString();
2585
+ const limit = Math.max(1, Math.min(500, input.limit ?? 50));
2586
+ return control.all(`SELECT * FROM ${q("_agent_instances")}
2587
+ WHERE "runtime" = 'ai-chat'
2588
+ AND "triggerType" = 'chat'
2589
+ AND (
2590
+ ("expiresAt" IS NOT NULL AND "expiresAt" <= ? AND "status" IN ('active', 'cleanupRequested', 'cleanupError'))
2591
+ OR ("status" = 'deleting' AND "updatedAt" <= ?)
2592
+ )
2593
+ ORDER BY "expiresAt" ASC
2594
+ LIMIT ?`, [now, staleDeletingBefore, limit]);
2595
+ }
2596
+ export async function markAgentCleanupRequested(control, instanceId, input = {}) {
2597
+ const now = input.now ?? nowIso();
2598
+ await control.run(`UPDATE ${q("_agent_instances")}
2599
+ SET "status" = CASE WHEN "status" = 'active' THEN 'cleanupRequested' ELSE "status" END,
2600
+ "cleanupRequestedAt" = COALESCE("cleanupRequestedAt", ?),
2601
+ "retentionReason" = COALESCE("retentionReason", ?),
2602
+ "lastError" = ?,
2603
+ "updatedAt" = ?
2604
+ WHERE "id" = ? AND "status" IN ('active', 'cleanupRequested', 'cleanupError')`, [now, input.reason ?? null, input.error ?? null, now, instanceId]);
2605
+ }
2606
+ export async function beginAgentHotCleanup(control, input) {
2607
+ const now = input.now ?? nowIso();
2608
+ const staleDeletingBefore = new Date(new Date(now).getTime() - staleDeletingRetrySeconds * 1000).toISOString();
2609
+ const current = await control.first(`SELECT * FROM ${q("_agent_instances")} WHERE "id" = ? LIMIT 1`, [input.instance.id]);
2610
+ if (!current)
2611
+ throw new AgentHttpError("Agent conversation not found", 404);
2612
+ if (current.status === "deleted" || current.status === "archived")
2613
+ return { status: "terminal", instance: current };
2614
+ if (current.status === "deleting" && current.updatedAt > staleDeletingBefore)
2615
+ return { status: "busy", instance: current };
2616
+ if (current.status !== "deleting" && !retryableCleanupStatuses.has(current.status))
2617
+ return { status: "busy", instance: current };
2618
+ if (current.status !== "deleting" && (current.expiresAt === null || new Date(current.expiresAt).getTime() > new Date(now).getTime())) {
2619
+ await control.run(`UPDATE ${q("_agent_instances")}
2620
+ SET "status" = 'active', "lastError" = NULL, "updatedAt" = ?
2621
+ WHERE "id" = ? AND "status" IN ('cleanupRequested', 'cleanupError')`, [now, current.id]);
2622
+ const rescheduled = await control.first(`SELECT * FROM ${q("_agent_instances")} WHERE "id" = ? LIMIT 1`, [current.id]);
2623
+ return { status: "reschedule", instance: rescheduled ?? current };
2624
+ }
2625
+ await control.run(`UPDATE ${q("_agent_instances")}
2626
+ SET "status" = 'deleting',
2627
+ "cleanupRequestedAt" = COALESCE("cleanupRequestedAt", ?),
2628
+ "retentionReason" = COALESCE("retentionReason", ?),
2629
+ "lastError" = NULL,
2630
+ "updatedAt" = ?
2631
+ WHERE "id" = ?
2632
+ AND (
2633
+ ("status" IN ('active', 'cleanupRequested', 'cleanupError') AND "expiresAt" IS NOT NULL AND "expiresAt" <= ?)
2634
+ OR ("status" = 'deleting' AND "updatedAt" <= ?)
2635
+ )`, [now, input.reason ?? null, now, current.id, now, staleDeletingBefore]);
2636
+ const next = await control.first(`SELECT * FROM ${q("_agent_instances")} WHERE "id" = ? LIMIT 1`, [current.id]);
2637
+ if (!next)
2638
+ throw new AgentHttpError("Agent conversation not found", 404);
2639
+ if (next.status !== "deleting") {
2640
+ if (next.status === "deleted" || next.status === "archived")
2641
+ return { status: "terminal", instance: next };
2642
+ if (next.expiresAt === null || new Date(next.expiresAt).getTime() > new Date(now).getTime() || next.status === "active")
2643
+ return { status: "reschedule", instance: next };
2644
+ return { status: "busy", instance: next };
2645
+ }
2646
+ return { status: "delete", instance: next };
2647
+ }
2648
+ export async function completeAgentHotCleanup(control, instanceId, input = {}) {
2649
+ const now = input.now ?? nowIso();
2650
+ const outcome = input.outcome ?? "deleted";
2651
+ if (outcome === "archived") {
2652
+ await control.run(`UPDATE ${q("_agent_instances")}
2653
+ SET "status" = 'archived', "deletedAt" = NULL,
2654
+ "updatedAt" = ?, "lastError" = NULL
2655
+ WHERE "id" = ? AND "status" = 'deleting'
2656
+ AND COALESCE("retentionReason", '') <> 'user-deleted'`, [now, instanceId]);
2657
+ return;
2658
+ }
2659
+ await control.run(`UPDATE ${q("_agent_instances")}
2660
+ SET "status" = 'deleted', "deletedAt" = COALESCE("deletedAt", ?),
2661
+ "updatedAt" = ?, "lastError" = NULL
2662
+ WHERE "id" = ? AND "status" = 'deleting'
2663
+ AND NOT EXISTS (
2664
+ SELECT 1 FROM ${q("_agent_chapter_purges")} p
2665
+ WHERE p."scopeKind" = 'conversation'
2666
+ AND p."scopeId" = ${q("_agent_instances")}."conversationId"
2667
+ )`, [now, now, instanceId]);
2668
+ }
2669
+ export async function completeAgentUserErasureAfterArtifactDrain(control, userId, input = {}) {
2670
+ const pendingArtifacts = await control.first(`SELECT COUNT(*) AS "count" FROM ${q("_agent_chapter_purges")}
2671
+ WHERE "scopeKind" = 'user' AND "scopeId" = ?`, [userId]);
2672
+ if (Number(pendingArtifacts?.count ?? 0) > 0)
2673
+ return false;
2674
+ const pendingRoomRedactions = await countPendingAgentRoomMemberRedactions(control, userId);
2675
+ if (pendingRoomRedactions > 0)
2676
+ return false;
2677
+ const unverifiedHotStorage = await control.first(`SELECT COUNT(*) AS "count" FROM ${q("_agent_instances")}
2678
+ WHERE "runtime" = 'ai-chat' AND "triggerType" = 'chat'
2679
+ AND "userId" = ? AND "status" <> 'deleted'
2680
+ AND "hotStorageDeletedAt" IS NULL`, [userId]);
2681
+ if (Number(unverifiedHotStorage?.count ?? 0) > 0)
2682
+ return false;
2683
+ const now = input.now ?? nowIso();
2684
+ await control.run(`UPDATE ${q("_agent_instances")}
2685
+ SET "status" = 'deleted',
2686
+ "deletedAt" = COALESCE("deletedAt", ?),
2687
+ "lastError" = NULL,
2688
+ "updatedAt" = ?
2689
+ WHERE "runtime" = 'ai-chat' AND "triggerType" = 'chat'
2690
+ AND "userId" = ? AND "status" <> 'deleted'
2691
+ AND "retentionReason" = 'user-deleted'
2692
+ AND "hotStorageDeletedAt" IS NOT NULL
2693
+ AND NOT EXISTS (
2694
+ SELECT 1 FROM ${q("_agent_chapter_purges")} p
2695
+ WHERE p."scopeKind" = 'user'
2696
+ AND p."scopeId" = ${q("_agent_instances")}."userId"
2697
+ )`, [now, now, userId]);
2698
+ const remaining = await control.first(`SELECT COUNT(*) AS "count" FROM ${q("_agent_instances")}
2699
+ WHERE "runtime" = 'ai-chat' AND "triggerType" = 'chat'
2700
+ AND "userId" = ? AND "status" <> 'deleted'`, [userId]);
2701
+ return Number(remaining?.count ?? 0) === 0;
2702
+ }
2703
+ export async function countPendingAgentRoomMemberRedactions(control, userId) {
2704
+ const pending = await control.first(`SELECT COUNT(*) AS "count" FROM ${q("_agent_chapter_purges")}
2705
+ WHERE "scopeKind" = 'room-member'
2706
+ AND substr("scopeId", -(length(?) + 1)) = ':' || ?`, [userId, userId]);
2707
+ return Number(pending?.count ?? 0);
2708
+ }
2709
+ export async function failAgentHotCleanup(control, instanceId, input) {
2710
+ const now = input.now ?? nowIso();
2711
+ await control.run(`UPDATE ${q("_agent_instances")}
2712
+ SET "status" = 'cleanupError', "lastError" = ?, "updatedAt" = ?
2713
+ WHERE "id" = ? AND "status" IN ('cleanupRequested', 'deleting', 'cleanupError')`, [input.error, now, instanceId]);
2714
+ }
2715
+ export async function claimAgentGuideLimits(input) {
2716
+ const limits = input.agent.limits;
2717
+ if (!limits)
2718
+ return;
2719
+ if (input.instance.principalKind === "public") {
2720
+ const publicScope = input.instance.publicScopeHash ?? "public";
2721
+ if (limits.publicIpHourlyRequests || limits.publicIpHourlyOutputTokens) {
2722
+ await claimAgentLimitWindow({
2723
+ control: input.control,
2724
+ scope: "public-ip-hourly",
2725
+ scopeId: publicScope,
2726
+ durationMs: 60 * 60 * 1000,
2727
+ requestLimit: limits.publicIpHourlyRequests,
2728
+ outputLimit: limits.publicIpHourlyOutputTokens,
2729
+ estimatedOutputTokens: input.estimatedOutputTokens
2730
+ });
2731
+ }
2732
+ if (limits.publicDailyRequests || limits.publicDailyOutputTokens) {
2733
+ await claimAgentLimitWindow({
2734
+ control: input.control,
2735
+ scope: "public-daily",
2736
+ scopeId: publicScope,
2737
+ durationMs: 24 * 60 * 60 * 1000,
2738
+ requestLimit: limits.publicDailyRequests,
2739
+ outputLimit: limits.publicDailyOutputTokens,
2740
+ estimatedOutputTokens: input.estimatedOutputTokens
2741
+ });
2742
+ }
2743
+ }
2744
+ else if (limits.signedInDailyRequests) {
2745
+ await claimAgentLimitWindow({
2746
+ control: input.control,
2747
+ scope: "signed-in-daily",
2748
+ scopeId: input.instance.userId ?? "user",
2749
+ durationMs: 24 * 60 * 60 * 1000,
2750
+ requestLimit: limits.signedInDailyRequests,
2751
+ estimatedOutputTokens: 0
2752
+ });
2753
+ }
2754
+ }
2755
+ /** `limit` unset means unlimited; the window row is still counted so usage stays visible. */
2756
+ export async function claimAgentEntryKickoffLimit(control, instance, limit) {
2757
+ if (instance.principalKind !== "user" || !instance.userId) {
2758
+ throw new AgentHttpError("Agent entry kickoff requires a signed-in user", 403);
2759
+ }
2760
+ await claimAgentLimitWindow({
2761
+ control,
2762
+ scope: "entry-kickoff",
2763
+ scopeId: instance.userId,
2764
+ durationMs: 24 * 60 * 60 * 1000,
2765
+ requestLimit: limit,
2766
+ estimatedOutputTokens: 0
2767
+ });
2768
+ }
2769
+ /** `limit` unset means unlimited; the window row is still counted so usage stays visible. */
2770
+ export async function claimAgentComposedLimit(control, instance, limit) {
2771
+ if (instance.principalKind !== "user" || !instance.userId) {
2772
+ throw new AgentHttpError("Composed Agent turns require a user conversation", 403);
2773
+ }
2774
+ await claimAgentLimitWindow({
2775
+ control,
2776
+ scope: "composed-daily",
2777
+ scopeId: instance.conversationId,
2778
+ durationMs: 24 * 60 * 60 * 1000,
2779
+ requestLimit: limit,
2780
+ estimatedOutputTokens: 0
2781
+ });
2782
+ }
2783
+ export async function signAgentToken(secret, claims) {
2784
+ const now = Math.floor(wallNowMs() / 1000);
2785
+ const payload = {
2786
+ ...claims,
2787
+ iat: now,
2788
+ exp: now + Math.max(1, Math.floor(claims.ttlSeconds)),
2789
+ nonce: randomId("agent_nonce")
2790
+ };
2791
+ const body = base64url(encoder.encode(JSON.stringify(payload)));
2792
+ const signature = await hmac(secret, body);
2793
+ return `${body}.${signature}`;
2794
+ }
2795
+ export async function verifyAgentToken(secret, token, purpose) {
2796
+ const [body, signature] = token.split(".");
2797
+ // Names the shape, not the verdict: this branch is reached by strings that
2798
+ // are not tokens at all (an absent mint interpolated as "undefined", a
2799
+ // truncated copy/paste), and "Invalid agent token" reads as a refusal of a
2800
+ // token that was actually issued.
2801
+ if (!body || !signature)
2802
+ throw new AgentHttpError("Invalid agent token format", 401);
2803
+ const expected = await hmac(secret, body);
2804
+ if (!timingSafeEqual(signature, expected))
2805
+ throw new AgentHttpError("Invalid agent token signature", 401);
2806
+ let claims;
2807
+ try {
2808
+ claims = JSON.parse(decoder.decode(fromBase64url(body)));
2809
+ }
2810
+ catch {
2811
+ throw new AgentHttpError("Invalid agent token payload", 401);
2812
+ }
2813
+ if (claims.v !== 1 || claims.aud !== "raybase-agent-chat" || claims.purpose !== purpose)
2814
+ throw new AgentHttpError("Invalid agent token claims", 401);
2815
+ if (claims.exp <= Math.floor(wallNowMs() / 1000))
2816
+ throw new AgentHttpError("Agent token expired", 401);
2817
+ return claims;
2818
+ }
2819
+ export function agentTokenClaims(input) {
2820
+ return {
2821
+ v: 1,
2822
+ purpose: input.purpose,
2823
+ aud: "raybase-agent-chat",
2824
+ agentName: input.instance.agentName,
2825
+ conversationId: input.instance.conversationId,
2826
+ runtime: "ai-chat",
2827
+ triggerType: "chat",
2828
+ principalKind: input.instance.principalKind,
2829
+ userId: input.userId ?? input.instance.userId ?? undefined,
2830
+ publicScopeHash: input.instance.publicScopeHash ?? undefined,
2831
+ organizationId: input.instance.organizationId ?? undefined,
2832
+ route: input.instance.route ?? undefined,
2833
+ surface: input.instance.surface,
2834
+ socketMode: input.socketMode,
2835
+ ttlSeconds: input.ttlSeconds ?? (input.purpose === "agent-websocket" ? websocketBootstrapTtlSeconds : defaultConversationTtlSeconds)
2836
+ };
2837
+ }
2838
+ export function assertAgentTokenMatchesInstance(claims, instance) {
2839
+ if (claims.agentName !== instance.agentName || claims.conversationId !== instance.conversationId || claims.runtime !== instance.runtime) {
2840
+ throw new AgentHttpError("Agent token does not match conversation", 401);
2841
+ }
2842
+ if (claims.principalKind !== instance.principalKind)
2843
+ throw new AgentHttpError("Agent token principal mismatch", 401);
2844
+ if (instance.kind !== "room") {
2845
+ if ((claims.userId ?? null) !== (instance.userId ?? null))
2846
+ throw new AgentHttpError("Agent token user mismatch", 401);
2847
+ if ((claims.publicScopeHash ?? null) !== (instance.publicScopeHash ?? null))
2848
+ throw new AgentHttpError("Agent token public scope mismatch", 401);
2849
+ }
2850
+ else if (!claims.userId) {
2851
+ throw new AgentHttpError("Agent token user mismatch", 401);
2852
+ }
2853
+ if ((claims.organizationId ?? null) !== (instance.organizationId ?? null))
2854
+ throw new AgentHttpError("Agent token organization mismatch", 401);
2855
+ }
2856
+ export async function assertAgentTokenConversationAccess(control, claims, instance) {
2857
+ assertAgentTokenMatchesInstance(claims, instance);
2858
+ if (instance.kind !== "room")
2859
+ return { membership: null };
2860
+ const membership = await findAgentRoomMember(control, instance.conversationId, claims.userId);
2861
+ if (!membership || membership.status !== "active") {
2862
+ throw new AgentHttpError("Agent room member mismatch", 403);
2863
+ }
2864
+ return { membership };
2865
+ }
2866
+ export function extractBearerToken(request) {
2867
+ const authorization = request.headers.get("authorization") ?? "";
2868
+ const match = /^Bearer\s+(.+)$/i.exec(authorization.trim());
2869
+ return match?.[1] ?? null;
2870
+ }
2871
+ export function raybaseActionToAiTool(action, contextFactory) {
2872
+ return tool({
2873
+ description: action.description,
2874
+ inputSchema: jsonSchema(actionInputJsonSchema(action)),
2875
+ execute: async (input, options = {}) => {
2876
+ const toolCallId = typeof options?.toolCallId === "string" ? options.toolCallId : randomId("tool_call");
2877
+ const ctx = await contextFactory({ toolCallId });
2878
+ return executeRaybaseAgentAction(action, input, ctx);
2879
+ }
2880
+ });
2881
+ }
2882
+ export async function executeRaybaseAgentAction(action, rawInput,
2883
+ // Keep raw control on this wrapper only; action handlers receive it as
2884
+ // ctx.control through executeAction().
2885
+ ctx) {
2886
+ // The action sees the agent as its principal. Authority (policies, audience,
2887
+ // data access) still comes from the run's own user or visitor, which the
2888
+ // agent principal carries as onBehalfOf; what changes is attribution.
2889
+ const principal = agentPrincipal({
2890
+ agent: ctx.agentName,
2891
+ onBehalfOf: ctx.principal,
2892
+ scopes: ctx.agentScopes,
2893
+ organizationId: ctx.organizationId ?? null,
2894
+ runId: ctx.runId,
2895
+ conversationId: ctx.conversationId
2896
+ });
2897
+ // Tool arguments arrive in the strict wire form a provider will carry. The
2898
+ // action's own schema is the one that decides, so the padding comes off
2899
+ // first; see restoreStrictActionInput.
2900
+ return executeAction(action, restoreStrictActionInput(action, rawInput), {
2901
+ request: ctx.request,
2902
+ env: ctx.env,
2903
+ user: ctx.user,
2904
+ principal,
2905
+ db: ctx.db,
2906
+ auth: ctx.control
2907
+ ? createRaybaseAuthContextRuntime({
2908
+ db: ctx.control,
2909
+ secret: authSecret(ctx.env),
2910
+ config: ctx.config,
2911
+ sessionAuthenticated: false,
2912
+ principal,
2913
+ request: ctx.request,
2914
+ audit: ctx.audit
2915
+ })
2916
+ : unsupportedRaybaseAuthContextRuntime,
2917
+ ai: ctx.ai,
2918
+ settings: ctx.settings,
2919
+ notifications: ctx.notifications,
2920
+ jobs: ctx.jobs,
2921
+ agentName: ctx.agentName,
2922
+ agentInstanceId: ctx.agentInstanceId,
2923
+ trigger: ctx.trigger,
2924
+ conversationId: ctx.conversationId,
2925
+ ...(ctx.conversations ? { conversations: ctx.conversations } : {}),
2926
+ runId: ctx.runId,
2927
+ requestId: ctx.requestId,
2928
+ toolCallId: ctx.toolCallId,
2929
+ route: ctx.route,
2930
+ surface: ctx.surface,
2931
+ organizationId: ctx.organizationId,
2932
+ audit: ctx.audit,
2933
+ agents: ctx.agents ?? unsupportedRaybaseAgentInvocationRuntime,
2934
+ control: ctx.control
2935
+ }, {
2936
+ surface: "agent",
2937
+ requirePolicy: ctx.principal.kind === "user",
2938
+ onAccounting: async (event) => {
2939
+ if (!ctx.control)
2940
+ return;
2941
+ await recordAiActionEvent(ctx.control, {
2942
+ userId: event.status === "denied" && ctx.principal.kind === "user" ? ctx.principal.user.id : actionEventUserId(ctx.principal),
2943
+ agent: ctx.agentName,
2944
+ conversationId: ctx.conversationId,
2945
+ actionName: event.actionName,
2946
+ actionInput: event.actionInput,
2947
+ status: event.status,
2948
+ error: event.error
2949
+ });
2950
+ }
2951
+ });
2952
+ }
2953
+ export function agentLimits(config, agent) {
2954
+ const defaults = normalizeAiDefaults(config);
2955
+ const aiLimits = normalizeAiLimits(config);
2956
+ return {
2957
+ maxInputChars: agent.limits?.maxInputChars ?? defaults.maxInputChars,
2958
+ maxOutputTokens: agent.limits?.maxOutputTokens ?? defaults.maxOutputTokens,
2959
+ maxToolCallsPerTurn: agent.limits?.maxToolCallsPerTurn ?? aiLimits.maxToolCallsPerTurn,
2960
+ maxLoopStepsPerTurn: agent.limits?.maxLoopStepsPerTurn ?? aiLimits.maxLoopStepsPerTurn,
2961
+ maxConversationMessages: agent.limits?.maxConversationMessages ?? aiLimits.maxConversationMessages,
2962
+ loopReserveChars: agent.limits?.loopReserveChars,
2963
+ publicIpHourlyRequests: agent.limits?.publicIpHourlyRequests,
2964
+ publicIpHourlyOutputTokens: agent.limits?.publicIpHourlyOutputTokens,
2965
+ publicDailyRequests: agent.limits?.publicDailyRequests ?? aiLimits.perPublicDailyRequests,
2966
+ publicDailyOutputTokens: agent.limits?.publicDailyOutputTokens ?? aiLimits.perPublicDailyOutputTokens,
2967
+ signedInDailyRequests: agent.limits?.signedInDailyRequests ?? aiLimits.perUserDailyRequests,
2968
+ composedPerDay: agent.limits?.composedPerDay,
2969
+ entryKickoffPerDay: agent.limits?.entryKickoffPerDay
2970
+ };
2971
+ }
2972
+ export function agentCompaction(agent) {
2973
+ return {
2974
+ enabled: agent.compaction?.enabled ?? defaultAgentCompaction.enabled,
2975
+ model: agent.compaction?.model,
2976
+ keepRecentMessages: agent.compaction?.keepRecentMessages ?? defaultAgentCompaction.keepRecentMessages,
2977
+ triggerRatio: agent.compaction?.triggerRatio ?? defaultAgentCompaction.triggerRatio,
2978
+ minSummaryOutputTokens: agent.compaction?.minSummaryOutputTokens ?? defaultAgentCompaction.minSummaryOutputTokens,
2979
+ summarySpanRatio: agent.compaction?.summarySpanRatio ?? defaultAgentCompaction.summarySpanRatio,
2980
+ maxSummaryOutputTokens: agent.compaction?.maxSummaryOutputTokens ?? defaultAgentCompaction.maxSummaryOutputTokens
2981
+ };
2982
+ }
2983
+ /**
2984
+ * Output-token budget for one chapter-close summarization call, proportional
2985
+ * to the span being summarized:
2986
+ * clamp(minSummaryOutputTokens, round(summarySpanRatio × spanChars/4),
2987
+ * maxSummaryOutputTokens). `spanChars` is the total content length of the
2988
+ * messages actually being closed, so a three-message exchange keeps the floor
2989
+ * while a twenty-turn working session gets room for its settled facts.
2990
+ * The ceiling wins over the floor when a pinned `maxSummaryOutputTokens`
2991
+ * sits below the (defaulted) floor: an app that pinned the old flat budget
2992
+ * keeps exactly its pre-proportional summaries.
2993
+ */
2994
+ export function agentSummaryBudgetTokens(compaction, spanChars) {
2995
+ const min = compaction?.minSummaryOutputTokens ?? defaultAgentCompaction.minSummaryOutputTokens;
2996
+ const ratio = compaction?.summarySpanRatio ?? defaultAgentCompaction.summarySpanRatio;
2997
+ const max = compaction?.maxSummaryOutputTokens ?? defaultAgentCompaction.maxSummaryOutputTokens;
2998
+ const proportional = Math.round(ratio * estimateTokensFromChars(spanChars));
2999
+ return Math.min(max, Math.max(min, proportional));
3000
+ }
3001
+ export function agentModel(config, agent, runtimeDefaultModel) {
3002
+ return agent.model ?? runtimeDefaultModel ?? normalizeAiDefaults(config).model;
3003
+ }
3004
+ export function actionEventUserId(principal) {
3005
+ return principal.kind === "user" ? principal.user.id : principal.id;
3006
+ }
3007
+ async function claimAgentLimitWindow(input) {
3008
+ const now = appNowMs();
3009
+ const windowStartMs = Math.floor(now / input.durationMs) * input.durationMs;
3010
+ const windowStart = new Date(windowStartMs).toISOString();
3011
+ const windowEnd = new Date(windowStartMs + input.durationMs).toISOString();
3012
+ const row = await input.control.first(`SELECT "requestCount", "estimatedOutputTokens" FROM ${q("_agent_limit_windows")}
3013
+ WHERE "scope" = ? AND "scopeId" = ? AND "windowStart" = ? LIMIT 1`, [input.scope, input.scopeId, windowStart]);
3014
+ const nextRequests = Number(row?.requestCount ?? 0) + 1;
3015
+ const nextOutput = Number(row?.estimatedOutputTokens ?? 0) + Math.max(0, input.estimatedOutputTokens);
3016
+ if (input.requestLimit !== undefined && nextRequests > input.requestLimit) {
3017
+ throw new AgentHttpError(`Agent request limit exceeded for ${input.scope}`, 429);
3018
+ }
3019
+ if (input.outputLimit !== undefined && nextOutput > input.outputLimit) {
3020
+ throw new AgentHttpError(`Agent output token limit exceeded for ${input.scope}`, 429);
3021
+ }
3022
+ if (row) {
3023
+ await input.control.run(`UPDATE ${q("_agent_limit_windows")}
3024
+ SET "requestCount" = ?, "estimatedOutputTokens" = ?, "updatedAt" = ?
3025
+ WHERE "scope" = ? AND "scopeId" = ? AND "windowStart" = ?`, [nextRequests, nextOutput, nowIso(), input.scope, input.scopeId, windowStart]);
3026
+ }
3027
+ else {
3028
+ await input.control.run(`INSERT INTO ${q("_agent_limit_windows")}
3029
+ ("id", "scope", "scopeId", "windowStart", "windowEnd", "requestCount", "estimatedOutputTokens", "updatedAt")
3030
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [randomId("agent_limit"), input.scope, input.scopeId, windowStart, windowEnd, nextRequests, nextOutput, nowIso()]);
3031
+ }
3032
+ }
3033
+ export async function assertAgentConversationAccess(input) {
3034
+ if (input.instance.kind !== "room") {
3035
+ assertAgentInstanceAccess(input.instance, input.access);
3036
+ return { membership: null };
3037
+ }
3038
+ if (input.access.principal.kind !== "user") {
3039
+ throw new AgentHttpError("Agent room member mismatch", 403);
3040
+ }
3041
+ const membership = await findAgentRoomMember(input.control, input.instance.conversationId, input.access.principal.user.id);
3042
+ if (!membership || membership.status !== "active") {
3043
+ throw new AgentHttpError("Agent room member mismatch", 403);
3044
+ }
3045
+ if (input.require === "owner" && membership.role !== "owner") {
3046
+ throw new AgentHttpError("Agent conversation owner mismatch", 403);
3047
+ }
3048
+ return { membership };
3049
+ }
3050
+ function assertAgentInstanceAccess(instance, access) {
3051
+ if (instance.principalKind !== access.principal.kind)
3052
+ throw new AgentHttpError("Agent conversation principal mismatch", 403);
3053
+ if (access.principal.kind === "user" && instance.userId !== access.principal.user.id)
3054
+ throw new AgentHttpError("Agent conversation user mismatch", 403);
3055
+ if (access.principal.kind === "public" && instance.publicScopeHash !== access.principal.id)
3056
+ throw new AgentHttpError("Agent conversation public scope mismatch", 403);
3057
+ }
3058
+ function uniqueAudiences(values) {
3059
+ return Array.from(new Set(values));
3060
+ }
3061
+ function normalizeRoute(path) {
3062
+ const value = path.startsWith("/") ? path : `/${path}`;
3063
+ return value.replace(/\/{2,}/g, "/") || "/";
3064
+ }
3065
+ function optionalUserField(user, field) {
3066
+ if (!user || typeof user !== "object")
3067
+ return undefined;
3068
+ const value = user[field];
3069
+ return typeof value === "string" && value ? value : undefined;
3070
+ }
3071
+ function principalOrganizationId(user) {
3072
+ return optionalUserField(user, "organizationId") ?? null;
3073
+ }
3074
+ async function publicAgentPrincipal(request, organizationId, requestContext) {
3075
+ const ip = request.headers.get("cf-connecting-ip") ?? request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "local";
3076
+ const userAgent = request.headers.get("user-agent") ?? "unknown";
3077
+ const sessionHash = await sha256Hex(`${ip}:${userAgent}:${requestContext.surface}`);
3078
+ return {
3079
+ kind: "public",
3080
+ id: sessionHash.slice(0, 64),
3081
+ sessionHash: sessionHash.slice(0, 64),
3082
+ organizationId: organizationId ?? undefined,
3083
+ surface: requestContext.surface,
3084
+ route: requestContext.route,
3085
+ origin: requestContext.origin,
3086
+ ipHash: (await sha256Hex(ip)).slice(0, 64),
3087
+ userAgentHash: (await sha256Hex(userAgent)).slice(0, 64)
3088
+ };
3089
+ }
3090
+ function base64url(bytes) {
3091
+ let binary = "";
3092
+ for (const byte of bytes)
3093
+ binary += String.fromCharCode(byte);
3094
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
3095
+ }
3096
+ function fromBase64url(value) {
3097
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
3098
+ const binary = atob(padded);
3099
+ const bytes = new Uint8Array(binary.length);
3100
+ for (let index = 0; index < binary.length; index += 1)
3101
+ bytes[index] = binary.charCodeAt(index);
3102
+ return bytes;
3103
+ }
3104
+ async function hmac(secret, data) {
3105
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
3106
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
3107
+ return base64url(new Uint8Array(signature));
3108
+ }
3109
+ function timingSafeEqual(left, right) {
3110
+ const a = encoder.encode(left);
3111
+ const b = encoder.encode(right);
3112
+ if (a.length !== b.length)
3113
+ return false;
3114
+ let result = 0;
3115
+ for (let index = 0; index < a.length; index += 1) {
3116
+ result |= (a[index] ?? 0) ^ (b[index] ?? 0);
3117
+ }
3118
+ return result === 0;
3119
+ }
3120
+ export function jsonSchemaObject(schema) {
3121
+ return schema;
3122
+ }
3123
+ //# sourceMappingURL=agent.js.map