@agentuity/runtime 2.0.11 → 3.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (415) hide show
  1. package/dist/index.d.ts +37 -65
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +59 -61
  4. package/dist/index.js.map +1 -1
  5. package/package.json +9 -38
  6. package/src/index.ts +58 -259
  7. package/AGENTS.md +0 -116
  8. package/dist/_config.d.ts +0 -100
  9. package/dist/_config.d.ts.map +0 -1
  10. package/dist/_config.js +0 -147
  11. package/dist/_config.js.map +0 -1
  12. package/dist/_context.d.ts +0 -80
  13. package/dist/_context.d.ts.map +0 -1
  14. package/dist/_context.js +0 -160
  15. package/dist/_context.js.map +0 -1
  16. package/dist/_events.d.ts +0 -64
  17. package/dist/_events.d.ts.map +0 -1
  18. package/dist/_events.js +0 -92
  19. package/dist/_events.js.map +0 -1
  20. package/dist/_globals.d.ts +0 -58
  21. package/dist/_globals.d.ts.map +0 -1
  22. package/dist/_globals.js +0 -71
  23. package/dist/_globals.js.map +0 -1
  24. package/dist/_idle.d.ts +0 -7
  25. package/dist/_idle.d.ts.map +0 -1
  26. package/dist/_idle.js +0 -10
  27. package/dist/_idle.js.map +0 -1
  28. package/dist/_metadata.d.ts +0 -117
  29. package/dist/_metadata.d.ts.map +0 -1
  30. package/dist/_metadata.js +0 -268
  31. package/dist/_metadata.js.map +0 -1
  32. package/dist/_process-protection.d.ts +0 -27
  33. package/dist/_process-protection.d.ts.map +0 -1
  34. package/dist/_process-protection.js +0 -56
  35. package/dist/_process-protection.js.map +0 -1
  36. package/dist/_server.d.ts +0 -50
  37. package/dist/_server.d.ts.map +0 -1
  38. package/dist/_server.js +0 -89
  39. package/dist/_server.js.map +0 -1
  40. package/dist/_services.d.ts +0 -25
  41. package/dist/_services.d.ts.map +0 -1
  42. package/dist/_services.js +0 -286
  43. package/dist/_services.js.map +0 -1
  44. package/dist/_standalone.d.ts +0 -212
  45. package/dist/_standalone.d.ts.map +0 -1
  46. package/dist/_standalone.js +0 -556
  47. package/dist/_standalone.js.map +0 -1
  48. package/dist/_tokens.d.ts +0 -12
  49. package/dist/_tokens.d.ts.map +0 -1
  50. package/dist/_tokens.js +0 -97
  51. package/dist/_tokens.js.map +0 -1
  52. package/dist/_util.d.ts +0 -16
  53. package/dist/_util.d.ts.map +0 -1
  54. package/dist/_util.js +0 -54
  55. package/dist/_util.js.map +0 -1
  56. package/dist/_validation.d.ts +0 -89
  57. package/dist/_validation.d.ts.map +0 -1
  58. package/dist/_validation.js +0 -29
  59. package/dist/_validation.js.map +0 -1
  60. package/dist/_waituntil.d.ts +0 -32
  61. package/dist/_waituntil.d.ts.map +0 -1
  62. package/dist/_waituntil.js +0 -156
  63. package/dist/_waituntil.js.map +0 -1
  64. package/dist/agent.d.ts +0 -1262
  65. package/dist/agent.d.ts.map +0 -1
  66. package/dist/agent.js +0 -981
  67. package/dist/agent.js.map +0 -1
  68. package/dist/app.d.ts +0 -514
  69. package/dist/app.d.ts.map +0 -1
  70. package/dist/app.js +0 -228
  71. package/dist/app.js.map +0 -1
  72. package/dist/bootstrap.d.ts +0 -44
  73. package/dist/bootstrap.d.ts.map +0 -1
  74. package/dist/bootstrap.js +0 -259
  75. package/dist/bootstrap.js.map +0 -1
  76. package/dist/bun-s3-patch.d.ts +0 -37
  77. package/dist/bun-s3-patch.d.ts.map +0 -1
  78. package/dist/bun-s3-patch.js +0 -142
  79. package/dist/bun-s3-patch.js.map +0 -1
  80. package/dist/cors.d.ts +0 -42
  81. package/dist/cors.d.ts.map +0 -1
  82. package/dist/cors.js +0 -117
  83. package/dist/cors.js.map +0 -1
  84. package/dist/dev-patches/aisdk.d.ts +0 -17
  85. package/dist/dev-patches/aisdk.d.ts.map +0 -1
  86. package/dist/dev-patches/aisdk.js +0 -160
  87. package/dist/dev-patches/aisdk.js.map +0 -1
  88. package/dist/dev-patches/gateway.d.ts +0 -16
  89. package/dist/dev-patches/gateway.d.ts.map +0 -1
  90. package/dist/dev-patches/gateway.js +0 -54
  91. package/dist/dev-patches/gateway.js.map +0 -1
  92. package/dist/dev-patches/index.d.ts +0 -21
  93. package/dist/dev-patches/index.d.ts.map +0 -1
  94. package/dist/dev-patches/index.js +0 -33
  95. package/dist/dev-patches/index.js.map +0 -1
  96. package/dist/dev-patches/otel-llm.d.ts +0 -12
  97. package/dist/dev-patches/otel-llm.d.ts.map +0 -1
  98. package/dist/dev-patches/otel-llm.js +0 -352
  99. package/dist/dev-patches/otel-llm.js.map +0 -1
  100. package/dist/devmode.d.ts +0 -3
  101. package/dist/devmode.d.ts.map +0 -1
  102. package/dist/devmode.js +0 -167
  103. package/dist/devmode.js.map +0 -1
  104. package/dist/eval.d.ts +0 -91
  105. package/dist/eval.d.ts.map +0 -1
  106. package/dist/eval.js +0 -16
  107. package/dist/eval.js.map +0 -1
  108. package/dist/handlers/_route-meta.d.ts +0 -22
  109. package/dist/handlers/_route-meta.d.ts.map +0 -1
  110. package/dist/handlers/_route-meta.js +0 -25
  111. package/dist/handlers/_route-meta.js.map +0 -1
  112. package/dist/handlers/cron.d.ts +0 -73
  113. package/dist/handlers/cron.d.ts.map +0 -1
  114. package/dist/handlers/cron.js +0 -43
  115. package/dist/handlers/cron.js.map +0 -1
  116. package/dist/handlers/index.d.ts +0 -6
  117. package/dist/handlers/index.d.ts.map +0 -1
  118. package/dist/handlers/index.js +0 -6
  119. package/dist/handlers/index.js.map +0 -1
  120. package/dist/handlers/sse.d.ts +0 -163
  121. package/dist/handlers/sse.d.ts.map +0 -1
  122. package/dist/handlers/sse.js +0 -175
  123. package/dist/handlers/sse.js.map +0 -1
  124. package/dist/handlers/stream.d.ts +0 -52
  125. package/dist/handlers/stream.d.ts.map +0 -1
  126. package/dist/handlers/stream.js +0 -108
  127. package/dist/handlers/stream.js.map +0 -1
  128. package/dist/handlers/webrtc.d.ts +0 -49
  129. package/dist/handlers/webrtc.d.ts.map +0 -1
  130. package/dist/handlers/webrtc.js +0 -109
  131. package/dist/handlers/webrtc.js.map +0 -1
  132. package/dist/handlers/websocket.d.ts +0 -88
  133. package/dist/handlers/websocket.d.ts.map +0 -1
  134. package/dist/handlers/websocket.js +0 -161
  135. package/dist/handlers/websocket.js.map +0 -1
  136. package/dist/logger/console.d.ts +0 -70
  137. package/dist/logger/console.d.ts.map +0 -1
  138. package/dist/logger/console.js +0 -278
  139. package/dist/logger/console.js.map +0 -1
  140. package/dist/logger/index.d.ts +0 -3
  141. package/dist/logger/index.d.ts.map +0 -1
  142. package/dist/logger/index.js +0 -3
  143. package/dist/logger/index.js.map +0 -1
  144. package/dist/logger/internal.d.ts +0 -79
  145. package/dist/logger/internal.d.ts.map +0 -1
  146. package/dist/logger/internal.js +0 -133
  147. package/dist/logger/internal.js.map +0 -1
  148. package/dist/logger/logger.d.ts +0 -41
  149. package/dist/logger/logger.d.ts.map +0 -1
  150. package/dist/logger/logger.js +0 -2
  151. package/dist/logger/logger.js.map +0 -1
  152. package/dist/logger/user.d.ts +0 -8
  153. package/dist/logger/user.d.ts.map +0 -1
  154. package/dist/logger/user.js +0 -7
  155. package/dist/logger/user.js.map +0 -1
  156. package/dist/logger/util.d.ts +0 -11
  157. package/dist/logger/util.d.ts.map +0 -1
  158. package/dist/logger/util.js +0 -77
  159. package/dist/logger/util.js.map +0 -1
  160. package/dist/middleware.d.ts +0 -105
  161. package/dist/middleware.d.ts.map +0 -1
  162. package/dist/middleware.js +0 -763
  163. package/dist/middleware.js.map +0 -1
  164. package/dist/otel/config.d.ts +0 -19
  165. package/dist/otel/config.d.ts.map +0 -1
  166. package/dist/otel/config.js +0 -26
  167. package/dist/otel/config.js.map +0 -1
  168. package/dist/otel/console.d.ts +0 -33
  169. package/dist/otel/console.d.ts.map +0 -1
  170. package/dist/otel/console.js +0 -86
  171. package/dist/otel/console.js.map +0 -1
  172. package/dist/otel/exporters/index.d.ts +0 -4
  173. package/dist/otel/exporters/index.d.ts.map +0 -1
  174. package/dist/otel/exporters/index.js +0 -4
  175. package/dist/otel/exporters/index.js.map +0 -1
  176. package/dist/otel/exporters/jsonl-log-exporter.d.ts +0 -36
  177. package/dist/otel/exporters/jsonl-log-exporter.d.ts.map +0 -1
  178. package/dist/otel/exporters/jsonl-log-exporter.js +0 -103
  179. package/dist/otel/exporters/jsonl-log-exporter.js.map +0 -1
  180. package/dist/otel/exporters/jsonl-metric-exporter.d.ts +0 -40
  181. package/dist/otel/exporters/jsonl-metric-exporter.d.ts.map +0 -1
  182. package/dist/otel/exporters/jsonl-metric-exporter.js +0 -104
  183. package/dist/otel/exporters/jsonl-metric-exporter.js.map +0 -1
  184. package/dist/otel/exporters/jsonl-trace-exporter.d.ts +0 -36
  185. package/dist/otel/exporters/jsonl-trace-exporter.d.ts.map +0 -1
  186. package/dist/otel/exporters/jsonl-trace-exporter.js +0 -111
  187. package/dist/otel/exporters/jsonl-trace-exporter.js.map +0 -1
  188. package/dist/otel/fetch.d.ts +0 -12
  189. package/dist/otel/fetch.d.ts.map +0 -1
  190. package/dist/otel/fetch.js +0 -82
  191. package/dist/otel/fetch.js.map +0 -1
  192. package/dist/otel/http.d.ts +0 -16
  193. package/dist/otel/http.d.ts.map +0 -1
  194. package/dist/otel/http.js +0 -44
  195. package/dist/otel/http.js.map +0 -1
  196. package/dist/otel/logger.d.ts +0 -37
  197. package/dist/otel/logger.d.ts.map +0 -1
  198. package/dist/otel/logger.js +0 -265
  199. package/dist/otel/logger.js.map +0 -1
  200. package/dist/otel/otel.d.ts +0 -68
  201. package/dist/otel/otel.d.ts.map +0 -1
  202. package/dist/otel/otel.js +0 -245
  203. package/dist/otel/otel.js.map +0 -1
  204. package/dist/otel/tracestate.d.ts +0 -44
  205. package/dist/otel/tracestate.d.ts.map +0 -1
  206. package/dist/otel/tracestate.js +0 -84
  207. package/dist/otel/tracestate.js.map +0 -1
  208. package/dist/router.d.ts +0 -66
  209. package/dist/router.d.ts.map +0 -1
  210. package/dist/router.js +0 -44
  211. package/dist/router.js.map +0 -1
  212. package/dist/services/evalrun/composite.d.ts +0 -21
  213. package/dist/services/evalrun/composite.d.ts.map +0 -1
  214. package/dist/services/evalrun/composite.js +0 -26
  215. package/dist/services/evalrun/composite.js.map +0 -1
  216. package/dist/services/evalrun/http.d.ts +0 -24
  217. package/dist/services/evalrun/http.d.ts.map +0 -1
  218. package/dist/services/evalrun/http.js +0 -115
  219. package/dist/services/evalrun/http.js.map +0 -1
  220. package/dist/services/evalrun/index.d.ts +0 -5
  221. package/dist/services/evalrun/index.d.ts.map +0 -1
  222. package/dist/services/evalrun/index.js +0 -5
  223. package/dist/services/evalrun/index.js.map +0 -1
  224. package/dist/services/evalrun/json.d.ts +0 -21
  225. package/dist/services/evalrun/json.d.ts.map +0 -1
  226. package/dist/services/evalrun/json.js +0 -38
  227. package/dist/services/evalrun/json.js.map +0 -1
  228. package/dist/services/evalrun/local.d.ts +0 -19
  229. package/dist/services/evalrun/local.d.ts.map +0 -1
  230. package/dist/services/evalrun/local.js +0 -22
  231. package/dist/services/evalrun/local.js.map +0 -1
  232. package/dist/services/local/_db.d.ts +0 -4
  233. package/dist/services/local/_db.d.ts.map +0 -1
  234. package/dist/services/local/_db.js +0 -281
  235. package/dist/services/local/_db.js.map +0 -1
  236. package/dist/services/local/_router.d.ts +0 -3
  237. package/dist/services/local/_router.d.ts.map +0 -1
  238. package/dist/services/local/_router.js +0 -28
  239. package/dist/services/local/_router.js.map +0 -1
  240. package/dist/services/local/_util.d.ts +0 -18
  241. package/dist/services/local/_util.d.ts.map +0 -1
  242. package/dist/services/local/_util.js +0 -44
  243. package/dist/services/local/_util.js.map +0 -1
  244. package/dist/services/local/email.d.ts +0 -24
  245. package/dist/services/local/email.d.ts.map +0 -1
  246. package/dist/services/local/email.js +0 -58
  247. package/dist/services/local/email.js.map +0 -1
  248. package/dist/services/local/index.d.ts +0 -10
  249. package/dist/services/local/index.d.ts.map +0 -1
  250. package/dist/services/local/index.js +0 -10
  251. package/dist/services/local/index.js.map +0 -1
  252. package/dist/services/local/keyvalue.d.ts +0 -17
  253. package/dist/services/local/keyvalue.d.ts.map +0 -1
  254. package/dist/services/local/keyvalue.js +0 -133
  255. package/dist/services/local/keyvalue.js.map +0 -1
  256. package/dist/services/local/queue.d.ts +0 -10
  257. package/dist/services/local/queue.d.ts.map +0 -1
  258. package/dist/services/local/queue.js +0 -96
  259. package/dist/services/local/queue.js.map +0 -1
  260. package/dist/services/local/stream.d.ts +0 -12
  261. package/dist/services/local/stream.d.ts.map +0 -1
  262. package/dist/services/local/stream.js +0 -266
  263. package/dist/services/local/stream.js.map +0 -1
  264. package/dist/services/local/task.d.ts +0 -55
  265. package/dist/services/local/task.d.ts.map +0 -1
  266. package/dist/services/local/task.js +0 -1248
  267. package/dist/services/local/task.js.map +0 -1
  268. package/dist/services/local/vector.d.ts +0 -17
  269. package/dist/services/local/vector.d.ts.map +0 -1
  270. package/dist/services/local/vector.js +0 -303
  271. package/dist/services/local/vector.js.map +0 -1
  272. package/dist/services/sandbox/http.d.ts +0 -23
  273. package/dist/services/sandbox/http.d.ts.map +0 -1
  274. package/dist/services/sandbox/http.js +0 -327
  275. package/dist/services/sandbox/http.js.map +0 -1
  276. package/dist/services/sandbox/index.d.ts +0 -2
  277. package/dist/services/sandbox/index.d.ts.map +0 -1
  278. package/dist/services/sandbox/index.js +0 -2
  279. package/dist/services/sandbox/index.js.map +0 -1
  280. package/dist/services/session/composite.d.ts +0 -21
  281. package/dist/services/session/composite.d.ts.map +0 -1
  282. package/dist/services/session/composite.js +0 -26
  283. package/dist/services/session/composite.js.map +0 -1
  284. package/dist/services/session/http.d.ts +0 -34
  285. package/dist/services/session/http.d.ts.map +0 -1
  286. package/dist/services/session/http.js +0 -124
  287. package/dist/services/session/http.js.map +0 -1
  288. package/dist/services/session/index.d.ts +0 -5
  289. package/dist/services/session/index.d.ts.map +0 -1
  290. package/dist/services/session/index.js +0 -5
  291. package/dist/services/session/index.js.map +0 -1
  292. package/dist/services/session/json.d.ts +0 -22
  293. package/dist/services/session/json.d.ts.map +0 -1
  294. package/dist/services/session/json.js +0 -35
  295. package/dist/services/session/json.js.map +0 -1
  296. package/dist/services/session/local.d.ts +0 -19
  297. package/dist/services/session/local.d.ts.map +0 -1
  298. package/dist/services/session/local.js +0 -23
  299. package/dist/services/session/local.js.map +0 -1
  300. package/dist/services/thread/local.d.ts +0 -20
  301. package/dist/services/thread/local.d.ts.map +0 -1
  302. package/dist/services/thread/local.js +0 -158
  303. package/dist/services/thread/local.js.map +0 -1
  304. package/dist/session.d.ts +0 -734
  305. package/dist/session.d.ts.map +0 -1
  306. package/dist/session.js +0 -1140
  307. package/dist/session.js.map +0 -1
  308. package/dist/signature.d.ts +0 -22
  309. package/dist/signature.d.ts.map +0 -1
  310. package/dist/signature.js +0 -63
  311. package/dist/signature.js.map +0 -1
  312. package/dist/validator.d.ts +0 -142
  313. package/dist/validator.d.ts.map +0 -1
  314. package/dist/validator.js +0 -149
  315. package/dist/validator.js.map +0 -1
  316. package/dist/version-check.d.ts +0 -20
  317. package/dist/version-check.d.ts.map +0 -1
  318. package/dist/version-check.js +0 -157
  319. package/dist/version-check.js.map +0 -1
  320. package/dist/web.d.ts +0 -8
  321. package/dist/web.d.ts.map +0 -1
  322. package/dist/web.js +0 -67
  323. package/dist/web.js.map +0 -1
  324. package/dist/webrtc-signaling.d.ts +0 -80
  325. package/dist/webrtc-signaling.d.ts.map +0 -1
  326. package/dist/webrtc-signaling.js +0 -237
  327. package/dist/webrtc-signaling.js.map +0 -1
  328. package/dist/workbench.d.ts +0 -17
  329. package/dist/workbench.d.ts.map +0 -1
  330. package/dist/workbench.js +0 -605
  331. package/dist/workbench.js.map +0 -1
  332. package/src/_config.ts +0 -163
  333. package/src/_context.ts +0 -240
  334. package/src/_events.ts +0 -142
  335. package/src/_globals.ts +0 -92
  336. package/src/_idle.ts +0 -10
  337. package/src/_metadata.ts +0 -407
  338. package/src/_process-protection.ts +0 -71
  339. package/src/_server.ts +0 -109
  340. package/src/_services.ts +0 -379
  341. package/src/_standalone.ts +0 -710
  342. package/src/_tokens.ts +0 -114
  343. package/src/_util.ts +0 -62
  344. package/src/_validation.ts +0 -119
  345. package/src/_waituntil.ts +0 -188
  346. package/src/agent.ts +0 -2739
  347. package/src/app.ts +0 -769
  348. package/src/bootstrap.ts +0 -321
  349. package/src/bun-s3-patch.ts +0 -224
  350. package/src/cors.ts +0 -137
  351. package/src/dev-patches/aisdk.ts +0 -169
  352. package/src/dev-patches/gateway.ts +0 -68
  353. package/src/dev-patches/index.ts +0 -37
  354. package/src/dev-patches/otel-llm.ts +0 -405
  355. package/src/devmode.ts +0 -171
  356. package/src/eval.ts +0 -109
  357. package/src/globals.d.ts +0 -28
  358. package/src/handlers/_route-meta.ts +0 -33
  359. package/src/handlers/cron.ts +0 -141
  360. package/src/handlers/index.ts +0 -18
  361. package/src/handlers/sse.ts +0 -358
  362. package/src/handlers/stream.ts +0 -121
  363. package/src/handlers/webrtc.ts +0 -125
  364. package/src/handlers/websocket.ts +0 -203
  365. package/src/logger/console.ts +0 -323
  366. package/src/logger/index.ts +0 -2
  367. package/src/logger/internal.ts +0 -165
  368. package/src/logger/logger.ts +0 -44
  369. package/src/logger/user.ts +0 -15
  370. package/src/logger/util.ts +0 -80
  371. package/src/middleware.ts +0 -1095
  372. package/src/otel/config.ts +0 -47
  373. package/src/otel/console.ts +0 -91
  374. package/src/otel/exporters/README.md +0 -217
  375. package/src/otel/exporters/index.ts +0 -3
  376. package/src/otel/exporters/jsonl-log-exporter.ts +0 -113
  377. package/src/otel/exporters/jsonl-metric-exporter.ts +0 -120
  378. package/src/otel/exporters/jsonl-trace-exporter.ts +0 -121
  379. package/src/otel/fetch.ts +0 -105
  380. package/src/otel/http.ts +0 -53
  381. package/src/otel/logger.ts +0 -293
  382. package/src/otel/otel.ts +0 -354
  383. package/src/otel/tracestate.ts +0 -108
  384. package/src/router.ts +0 -75
  385. package/src/services/evalrun/composite.ts +0 -34
  386. package/src/services/evalrun/http.ts +0 -167
  387. package/src/services/evalrun/index.ts +0 -4
  388. package/src/services/evalrun/json.ts +0 -46
  389. package/src/services/evalrun/local.ts +0 -28
  390. package/src/services/local/README.md +0 -1576
  391. package/src/services/local/_db.ts +0 -353
  392. package/src/services/local/_router.ts +0 -40
  393. package/src/services/local/_util.ts +0 -55
  394. package/src/services/local/email.ts +0 -91
  395. package/src/services/local/index.ts +0 -9
  396. package/src/services/local/keyvalue.ts +0 -174
  397. package/src/services/local/queue.ts +0 -145
  398. package/src/services/local/stream.ts +0 -358
  399. package/src/services/local/task.ts +0 -1711
  400. package/src/services/local/vector.ts +0 -438
  401. package/src/services/sandbox/http.ts +0 -522
  402. package/src/services/sandbox/index.ts +0 -1
  403. package/src/services/session/composite.ts +0 -33
  404. package/src/services/session/http.ts +0 -167
  405. package/src/services/session/index.ts +0 -4
  406. package/src/services/session/json.ts +0 -42
  407. package/src/services/session/local.ts +0 -33
  408. package/src/services/thread/local.ts +0 -199
  409. package/src/session.ts +0 -1960
  410. package/src/signature.ts +0 -82
  411. package/src/validator.ts +0 -283
  412. package/src/version-check.ts +0 -184
  413. package/src/web.ts +0 -76
  414. package/src/webrtc-signaling.ts +0 -288
  415. package/src/workbench.ts +0 -725
package/src/session.ts DELETED
@@ -1,1960 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- /** biome-ignore-all lint/suspicious/noExplicitAny: anys are great */
3
- import type { Context } from 'hono';
4
- import { getSignedCookie, setSignedCookie } from 'hono/cookie';
5
- import { type Env, fireEvent } from './app';
6
- import type { AppState } from './index';
7
- import { getServiceUrls } from '@agentuity/server';
8
- import { internal } from './logger/internal';
9
- import { timingSafeEqual } from 'node:crypto';
10
-
11
- /**
12
- * Result of parsing serialized thread data.
13
- * @internal
14
- */
15
- export interface ParsedThreadData {
16
- flatStateJson?: string;
17
- metadata?: Record<string, unknown>;
18
- }
19
-
20
- /**
21
- * Parse serialized thread data, handling both old (flat state) and new ({ state, metadata }) formats.
22
- * @internal
23
- */
24
- export function parseThreadData(raw: string | undefined): ParsedThreadData {
25
- if (!raw) {
26
- return {};
27
- }
28
-
29
- try {
30
- const parsed = JSON.parse(raw);
31
- if (parsed && typeof parsed === 'object' && ('state' in parsed || 'metadata' in parsed)) {
32
- return {
33
- flatStateJson: parsed.state ? JSON.stringify(parsed.state) : undefined,
34
- metadata:
35
- parsed.metadata && typeof parsed.metadata === 'object' ? parsed.metadata : undefined,
36
- };
37
- }
38
- return { flatStateJson: raw };
39
- } catch {
40
- return { flatStateJson: raw };
41
- }
42
- }
43
-
44
- export type ThreadEventName = 'destroyed';
45
- export type SessionEventName = 'completed';
46
-
47
- /**
48
- * Represents a merge operation for thread state.
49
- * Used when state is modified without being loaded first.
50
- */
51
- export interface MergeOperation {
52
- op: 'set' | 'delete' | 'clear' | 'push';
53
- key?: string;
54
- value?: unknown;
55
- maxRecords?: number;
56
- }
57
-
58
- /**
59
- * Async thread state storage with lazy loading.
60
- *
61
- * State is only fetched from storage when first accessed via a read operation.
62
- * Write operations can be batched and sent as a merge command without loading.
63
- *
64
- * @example
65
- * ```typescript
66
- * // Read triggers lazy load
67
- * const count = await ctx.thread.state.get<number>('messageCount');
68
- *
69
- * // Write queues operation (may not trigger load)
70
- * await ctx.thread.state.set('messageCount', (count ?? 0) + 1);
71
- *
72
- * // Check state status
73
- * if (ctx.thread.state.dirty) {
74
- * console.log('State has pending changes');
75
- * }
76
- * ```
77
- */
78
- export interface ThreadState {
79
- /**
80
- * Whether state has been loaded from storage.
81
- * True when state has been fetched via a read operation.
82
- */
83
- readonly loaded: boolean;
84
-
85
- /**
86
- * Whether state has pending changes.
87
- * True when there are queued writes (pending-writes state) or
88
- * modifications after loading (loaded state with changes).
89
- */
90
- readonly dirty: boolean;
91
-
92
- /**
93
- * Get a value from thread state.
94
- * Triggers lazy load if state hasn't been fetched yet.
95
- */
96
- get<T = unknown>(key: string): Promise<T | undefined>;
97
-
98
- /**
99
- * Set a value in thread state.
100
- * If state hasn't been loaded, queues the operation for merge.
101
- */
102
- set<T = unknown>(key: string, value: T): Promise<void>;
103
-
104
- /**
105
- * Check if a key exists in thread state.
106
- * Triggers lazy load if state hasn't been fetched yet.
107
- */
108
- has(key: string): Promise<boolean>;
109
-
110
- /**
111
- * Delete a key from thread state.
112
- * If state hasn't been loaded, queues the operation for merge.
113
- */
114
- delete(key: string): Promise<void>;
115
-
116
- /**
117
- * Clear all thread state.
118
- * If state hasn't been loaded, queues a clear operation for merge.
119
- */
120
- clear(): Promise<void>;
121
-
122
- /**
123
- * Get all entries as key-value pairs.
124
- * Triggers lazy load if state hasn't been fetched yet.
125
- */
126
- entries<T = unknown>(): Promise<[string, T][]>;
127
-
128
- /**
129
- * Get all keys.
130
- * Triggers lazy load if state hasn't been fetched yet.
131
- */
132
- keys(): Promise<string[]>;
133
-
134
- /**
135
- * Get all values.
136
- * Triggers lazy load if state hasn't been fetched yet.
137
- */
138
- values<T = unknown>(): Promise<T[]>;
139
-
140
- /**
141
- * Get the number of entries in state.
142
- * Triggers lazy load if state hasn't been fetched yet.
143
- */
144
- size(): Promise<number>;
145
-
146
- /**
147
- * Push a value to an array in thread state.
148
- * If the key doesn't exist, creates a new array with the value.
149
- * If state hasn't been loaded, queues the operation for efficient merge.
150
- *
151
- * @param key - The key of the array to push to
152
- * @param value - The value to push
153
- * @param maxRecords - Optional maximum number of records to keep (sliding window)
154
- *
155
- * @example
156
- * ```typescript
157
- * // Efficiently append messages without loading entire array
158
- * await ctx.thread.state.push('messages', { role: 'user', content: 'Hello' });
159
- * await ctx.thread.state.push('messages', { role: 'assistant', content: 'Hi!' });
160
- *
161
- * // Keep only the last 100 messages
162
- * await ctx.thread.state.push('messages', newMessage, 100);
163
- * ```
164
- */
165
- push<T = unknown>(key: string, value: T, maxRecords?: number): Promise<void>;
166
- }
167
-
168
- type ThreadEventCallback<T extends Thread> = (
169
- eventName: 'destroyed',
170
- thread: T
171
- ) => Promise<void> | void;
172
-
173
- type SessionEventCallback<T extends Session> = (
174
- eventName: 'completed',
175
- session: T
176
- ) => Promise<void> | void;
177
-
178
- /**
179
- * Represents a conversation thread that persists across multiple sessions.
180
- * Threads maintain state and can contain multiple request-response sessions.
181
- *
182
- * Threads are automatically managed by the runtime and stored in cookies.
183
- * They expire after 1 hour of inactivity by default.
184
- *
185
- * @example
186
- * ```typescript
187
- * // Access thread in agent handler
188
- * const agent = createAgent('conversation', {
189
- * handler: async (ctx, input) => {
190
- * // Get thread ID
191
- * ctx.logger.info('Thread: %s', ctx.thread.id);
192
- *
193
- * // Store data in thread state (persists across sessions)
194
- * const count = await ctx.thread.state.get<number>('conversationCount') ?? 0;
195
- * await ctx.thread.state.set('conversationCount', count + 1);
196
- *
197
- * // Access metadata
198
- * const meta = await ctx.thread.getMetadata();
199
- * await ctx.thread.setMetadata({ ...meta, lastAccess: Date.now() });
200
- *
201
- * // Listen for thread destruction
202
- * ctx.thread.addEventListener('destroyed', (eventName, thread) => {
203
- * ctx.logger.info('Thread destroyed: %s', thread.id);
204
- * });
205
- *
206
- * return 'Response';
207
- * }
208
- * });
209
- * ```
210
- */
211
- export interface Thread {
212
- /**
213
- * Unique thread identifier (e.g., "thrd_a1b2c3d4...").
214
- * Stored in cookie and persists across requests.
215
- */
216
- id: string;
217
-
218
- /**
219
- * Thread-scoped state storage with async lazy-loading.
220
- * State is only fetched from storage when first accessed via a read operation.
221
- *
222
- * @example
223
- * ```typescript
224
- * // Read triggers lazy load
225
- * const count = await ctx.thread.state.get<number>('messageCount');
226
- * // Write may queue operation without loading
227
- * await ctx.thread.state.set('messageCount', (count ?? 0) + 1);
228
- * ```
229
- */
230
- state: ThreadState;
231
-
232
- /**
233
- * Get thread metadata (lazy-loaded).
234
- * Unlike state, metadata is stored unencrypted for efficient filtering.
235
- *
236
- * @example
237
- * ```typescript
238
- * const meta = await ctx.thread.getMetadata();
239
- * console.log(meta.userId);
240
- * ```
241
- */
242
- getMetadata(): Promise<Record<string, unknown>>;
243
-
244
- /**
245
- * Set thread metadata (full replace).
246
- *
247
- * @example
248
- * ```typescript
249
- * await ctx.thread.setMetadata({ userId: 'user123', department: 'sales' });
250
- * ```
251
- */
252
- setMetadata(metadata: Record<string, unknown>): Promise<void>;
253
-
254
- /**
255
- * Register an event listener for when the thread is destroyed.
256
- * Thread is destroyed when it expires or is manually destroyed.
257
- *
258
- * @param eventName - Must be 'destroyed'
259
- * @param callback - Function called when thread is destroyed
260
- *
261
- * @example
262
- * ```typescript
263
- * ctx.thread.addEventListener('destroyed', (eventName, thread) => {
264
- * ctx.logger.info('Cleaning up thread: %s', thread.id);
265
- * });
266
- * ```
267
- */
268
- addEventListener(
269
- eventName: 'destroyed',
270
- callback: (eventName: 'destroyed', thread: Thread) => Promise<void> | void
271
- ): void;
272
-
273
- /**
274
- * Remove a previously registered 'destroyed' event listener.
275
- *
276
- * @param eventName - Must be 'destroyed'
277
- * @param callback - The callback function to remove
278
- */
279
- removeEventListener(
280
- eventName: 'destroyed',
281
- callback: (eventName: 'destroyed', thread: Thread) => Promise<void> | void
282
- ): void;
283
-
284
- /**
285
- * Manually destroy the thread and clean up resources.
286
- * Fires the 'destroyed' event and removes thread from storage.
287
- *
288
- * @example
289
- * ```typescript
290
- * // Permanently delete the thread from storage
291
- * await ctx.thread.destroy();
292
- * ```
293
- */
294
- destroy(): Promise<void>;
295
-
296
- /**
297
- * Check if the thread has any data.
298
- * Returns true if thread state is empty (no data to save).
299
- * This is async because it may need to check lazy-loaded state.
300
- *
301
- * @example
302
- * ```typescript
303
- * if (await ctx.thread.empty()) {
304
- * // Thread has no data, won't be persisted
305
- * }
306
- * ```
307
- */
308
- empty(): Promise<boolean>;
309
- }
310
-
311
- /**
312
- * Represents a single request-response session within a thread.
313
- * Sessions are scoped to a single agent execution and its sub-agent calls.
314
- *
315
- * Each HTTP request creates a new session with a unique ID, but shares the same thread.
316
- *
317
- * @example
318
- * ```typescript
319
- * const agent = createAgent('request-handler', {
320
- * handler: async (ctx, input) => {
321
- * // Get session ID (unique per request)
322
- * ctx.logger.info('Session: %s', ctx.session.id);
323
- *
324
- * // Store data in session state (only for this request)
325
- * ctx.session.state.set('startTime', Date.now());
326
- *
327
- * // Access parent thread
328
- * ctx.logger.info('Thread: %s', ctx.session.thread.id);
329
- *
330
- * // Listen for session completion
331
- * ctx.session.addEventListener('completed', (eventName, session) => {
332
- * const duration = Date.now() - (session.state.get('startTime') as number);
333
- * ctx.logger.info('Session completed in %dms', duration);
334
- * });
335
- *
336
- * return 'Response';
337
- * }
338
- * });
339
- * ```
340
- */
341
- export interface Session {
342
- /**
343
- * Unique session identifier for this request.
344
- * Changes with each HTTP request, even within the same thread.
345
- */
346
- id: string;
347
-
348
- /**
349
- * The parent thread this session belongs to.
350
- * Multiple sessions can share the same thread.
351
- */
352
- thread: Thread;
353
-
354
- /**
355
- * Session-scoped state storage that only exists for this request.
356
- * Use this for temporary data that shouldn't persist across requests.
357
- *
358
- * @example
359
- * ```typescript
360
- * ctx.session.state.set('requestStartTime', Date.now());
361
- * ```
362
- */
363
- state: Map<string, unknown>;
364
-
365
- /**
366
- * Unencrypted metadata for filtering and querying sessions.
367
- * Unlike state, metadata is stored as-is in the database with GIN indexes
368
- * for efficient filtering. Initialized to empty object, only persisted if non-empty.
369
- *
370
- * @example
371
- * ```typescript
372
- * ctx.session.metadata.userId = 'user123';
373
- * ctx.session.metadata.requestType = 'chat';
374
- * ```
375
- */
376
- metadata: Record<string, unknown>;
377
-
378
- /**
379
- * Register an event listener for when the session completes.
380
- * Fired after the agent handler returns and response is sent.
381
- *
382
- * @param eventName - Must be 'completed'
383
- * @param callback - Function called when session completes
384
- *
385
- * @example
386
- * ```typescript
387
- * ctx.session.addEventListener('completed', (eventName, session) => {
388
- * ctx.logger.info('Session finished: %s', session.id);
389
- * });
390
- * ```
391
- */
392
- addEventListener(
393
- eventName: 'completed',
394
- callback: (eventName: 'completed', session: Session) => Promise<void> | void
395
- ): void;
396
-
397
- /**
398
- * Remove a previously registered 'completed' event listener.
399
- *
400
- * @param eventName - Must be 'completed'
401
- * @param callback - The callback function to remove
402
- */
403
- removeEventListener(
404
- eventName: 'completed',
405
- callback: (eventName: 'completed', session: Session) => Promise<void> | void
406
- ): void;
407
-
408
- /**
409
- * Return the session data as a serializable string or return undefined if not
410
- * data should be serialized.
411
- */
412
- serializeUserData(): string | undefined;
413
- }
414
-
415
- /**
416
- * Represent an interface for handling how thread ids are generated or restored.
417
- */
418
- export interface ThreadIDProvider {
419
- /**
420
- * A function that should return a thread id to be used for the incoming request.
421
- * The returning thread id must be globally unique and must start with the prefix
422
- * thrd_ such as `thrd_212c16896b974ffeb21a748f0eeba620`. The max length of the
423
- * string is 64 characters and the min length is 32 characters long
424
- * (including the prefix). The characters after the prefix must match the
425
- * regular expression [a-zA-Z0-9].
426
- *
427
- * @param appState - The app state from createApp setup function
428
- * @param ctx - Hono request context
429
- * @returns The thread id to use (can be async for signed cookies)
430
- */
431
- getThreadId(appState: AppState, ctx: Context<Env>): string | Promise<string>;
432
- }
433
-
434
- /**
435
- * Provider interface for managing thread lifecycle and persistence.
436
- * Implement this to customize how threads are stored and retrieved.
437
- *
438
- * The default implementation (DefaultThreadProvider) stores threads in-memory
439
- * with cookie-based identification and 1-hour expiration.
440
- *
441
- * Thread state is serialized using `getSerializedState()` which returns a JSON
442
- * envelope: `{ "state": {...}, "metadata": {...} }`. Use `parseThreadData()` to
443
- * correctly parse both old (flat) and new (envelope) formats on restore.
444
- *
445
- * @example
446
- * ```typescript
447
- * class RedisThreadProvider implements ThreadProvider {
448
- * private redis: Redis;
449
- *
450
- * async initialize(appState: AppState): Promise<void> {
451
- * this.redis = await connectRedis();
452
- * }
453
- *
454
- * async restore(ctx: Context<Env>): Promise<Thread> {
455
- * const threadId = ctx.req.header('x-thread-id') || getCookie(ctx, 'atid') || generateId('thrd');
456
- * const data = await this.redis.get(`thread:${threadId}`);
457
- *
458
- * // Parse stored data, handling both old and new formats
459
- * const { flatStateJson, metadata } = parseThreadData(data);
460
- * const thread = new DefaultThread(this, threadId, flatStateJson, metadata);
461
- *
462
- * // Populate state from parsed data
463
- * if (flatStateJson) {
464
- * const stateObj = JSON.parse(flatStateJson);
465
- * for (const [key, value] of Object.entries(stateObj)) {
466
- * thread.state.set(key, value);
467
- * }
468
- * }
469
- * return thread;
470
- * }
471
- *
472
- * async save(thread: Thread): Promise<void> {
473
- * if (thread instanceof DefaultThread && thread.isDirty()) {
474
- * await this.redis.setex(
475
- * `thread:${thread.id}`,
476
- * 3600,
477
- * thread.getSerializedState()
478
- * );
479
- * }
480
- * }
481
- *
482
- * async destroy(thread: Thread): Promise<void> {
483
- * await this.redis.del(`thread:${thread.id}`);
484
- * }
485
- * }
486
- *
487
- * // Use custom provider
488
- * const app = await createApp({
489
- * services: {
490
- * thread: new RedisThreadProvider()
491
- * }
492
- * });
493
- * ```
494
- */
495
- export interface ThreadProvider {
496
- /**
497
- * Initialize the provider when the app starts.
498
- * Use this to set up connections, start cleanup intervals, etc.
499
- *
500
- * @param appState - The app state from createApp setup function
501
- */
502
- initialize(appState: AppState): Promise<void>;
503
-
504
- /**
505
- * Set the provider to use for generating / restoring the thread id
506
- * on new requests. Overrides the built-in provider when set.
507
- *
508
- * @param provider - the provider implementation
509
- */
510
- setThreadIDProvider(provider: ThreadIDProvider): void;
511
-
512
- /**
513
- * Restore or create a thread from the HTTP request context.
514
- * Should check cookies for existing thread ID or create a new one.
515
- *
516
- * @param ctx - Hono request context
517
- * @returns The restored or newly created thread
518
- */
519
- restore(ctx: Context<Env>): Promise<Thread>;
520
-
521
- /**
522
- * Persist thread state to storage.
523
- * Called periodically to save thread data.
524
- *
525
- * @param thread - The thread to save
526
- */
527
- save(thread: Thread): Promise<void>;
528
-
529
- /**
530
- * Destroy a thread and clean up resources.
531
- * Should fire the 'destroyed' event and remove from storage.
532
- *
533
- * @param thread - The thread to destroy
534
- */
535
- destroy(thread: Thread): Promise<void>;
536
- }
537
-
538
- /**
539
- * Provider interface for managing session lifecycle and persistence.
540
- * Implement this to customize how sessions are stored and retrieved.
541
- *
542
- * The default implementation (DefaultSessionProvider) stores sessions in-memory
543
- * and automatically cleans them up after completion.
544
- *
545
- * @example
546
- * ```typescript
547
- * class PostgresSessionProvider implements SessionProvider {
548
- * private db: Database;
549
- *
550
- * async initialize(appState: AppState): Promise<void> {
551
- * this.db = appState.db;
552
- * }
553
- *
554
- * async restore(thread: Thread, sessionId: string): Promise<Session> {
555
- * const row = await this.db.query(
556
- * 'SELECT state FROM sessions WHERE id = $1',
557
- * [sessionId]
558
- * );
559
- * const session = new DefaultSession(thread, sessionId);
560
- * if (row) {
561
- * session.state = new Map(JSON.parse(row.state));
562
- * }
563
- * return session;
564
- * }
565
- *
566
- * async save(session: Session): Promise<void> {
567
- * await this.db.query(
568
- * 'INSERT INTO sessions (id, thread_id, state) VALUES ($1, $2, $3)',
569
- * [session.id, session.thread.id, JSON.stringify([...session.state])]
570
- * );
571
- * }
572
- * }
573
- *
574
- * // Use custom provider
575
- * const app = await createApp({
576
- * services: {
577
- * session: new PostgresSessionProvider()
578
- * }
579
- * });
580
- * ```
581
- */
582
- export interface SessionProvider {
583
- /**
584
- * Initialize the provider when the app starts.
585
- * Use this to set up database connections or other resources.
586
- *
587
- * @param appState - The app state from createApp setup function
588
- */
589
- initialize(appState: AppState): Promise<void>;
590
-
591
- /**
592
- * Restore or create a session for the given thread and session ID.
593
- * Should load existing session data or create a new session.
594
- *
595
- * @param thread - The parent thread for this session
596
- * @param sessionId - The unique session identifier
597
- * @returns The restored or newly created session
598
- */
599
- restore(thread: Thread, sessionId: string): Promise<Session>;
600
-
601
- /**
602
- * Persist session state and fire completion events.
603
- * Called after the agent handler completes.
604
- *
605
- * @param session - The session to save
606
- */
607
- save(session: Session): Promise<void>;
608
- }
609
-
610
- // WeakMap to store event listeners for Thread and Session instances
611
- const threadEventListeners = new WeakMap<
612
- Thread,
613
- Map<ThreadEventName, Set<ThreadEventCallback<any>>>
614
- >();
615
- const sessionEventListeners = new WeakMap<
616
- Session,
617
- Map<SessionEventName, Set<SessionEventCallback<any>>>
618
- >();
619
-
620
- // Helper to fire thread event listeners
621
- async function fireThreadEvent(thread: Thread, eventName: ThreadEventName): Promise<void> {
622
- const listeners = threadEventListeners.get(thread);
623
- if (!listeners) return;
624
-
625
- const callbacks = listeners.get(eventName);
626
- if (!callbacks || callbacks.size === 0) return;
627
-
628
- for (const callback of callbacks) {
629
- try {
630
- await (callback as any)(eventName, thread);
631
- } catch (error) {
632
- // Log but don't re-throw - event listener errors should not crash the server
633
- internal.error(`Error in thread event listener for '${eventName}':`, error);
634
- }
635
- }
636
- }
637
-
638
- // Helper to fire session event listeners
639
- async function fireSessionEvent(session: Session, eventName: SessionEventName): Promise<void> {
640
- const listeners = sessionEventListeners.get(session);
641
- if (!listeners) return;
642
-
643
- const callbacks = listeners.get(eventName);
644
- if (!callbacks || callbacks.size === 0) return;
645
-
646
- for (const callback of callbacks) {
647
- try {
648
- await (callback as any)(eventName, session);
649
- } catch (error) {
650
- // Log but don't re-throw - event listener errors should not crash the server
651
- internal.error(`Error in session event listener for '${eventName}':`, error);
652
- }
653
- }
654
- }
655
-
656
- // Generate thread or session ID
657
- export function generateId(prefix?: string): string {
658
- const arr = new Uint8Array(16);
659
- crypto.getRandomValues(arr);
660
- return `${prefix}${prefix ? '_' : ''}${arr.toHex()}`;
661
- }
662
-
663
- /**
664
- * Validates a thread ID against runtime constraints:
665
- * - Must start with 'thrd_'
666
- * - Must be at least 32 characters long (including prefix)
667
- * - Must be less than 64 characters long
668
- * - Must contain only [a-zA-Z0-9] after 'thrd_' prefix (no dashes for maximum randomness)
669
- */
670
- export function isValidThreadId(threadId: string): boolean {
671
- if (!threadId.startsWith('thrd_')) {
672
- return false;
673
- }
674
- if (threadId.length < 32 || threadId.length > 64) {
675
- return false;
676
- }
677
- const validThreadIdCharacters = /^[a-zA-Z0-9]+$/;
678
- if (!validThreadIdCharacters.test(threadId.substring(5))) {
679
- return false;
680
- }
681
- return true;
682
- }
683
-
684
- /**
685
- * Validates a thread ID and throws detailed error messages for debugging.
686
- * @param threadId The thread ID to validate
687
- * @throws Error with detailed message if validation fails
688
- */
689
- export function validateThreadIdOrThrow(threadId: string): void {
690
- if (!threadId) {
691
- throw new Error(`the ThreadIDProvider returned an empty thread id for getThreadId`);
692
- }
693
- if (!threadId.startsWith('thrd_')) {
694
- throw new Error(
695
- `the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must start with the prefix 'thrd_'.`
696
- );
697
- }
698
- if (threadId.length > 64) {
699
- throw new Error(
700
- `the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must be less than 64 characters long.`
701
- );
702
- }
703
- if (threadId.length < 32) {
704
- throw new Error(
705
- `the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must be at least 32 characters long.`
706
- );
707
- }
708
- const validThreadIdCharacters = /^[a-zA-Z0-9]+$/;
709
- if (!validThreadIdCharacters.test(threadId.substring(5))) {
710
- throw new Error(
711
- `the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must contain only characters that match the regular expression [a-zA-Z0-9].`
712
- );
713
- }
714
- }
715
-
716
- /**
717
- * Determines if the connection is secure (HTTPS) by checking the request protocol
718
- * and x-forwarded-proto header (for reverse proxy scenarios).
719
- * Defaults to false (HTTP) if unable to determine.
720
- */
721
- export function isSecureConnection(ctx: Context<Env>): boolean {
722
- // Check x-forwarded-proto header first (reverse proxy)
723
- const forwardedProto = ctx.req.header('x-forwarded-proto');
724
- if (forwardedProto) {
725
- return forwardedProto === 'https';
726
- }
727
-
728
- // Check the request URL protocol if available
729
- try {
730
- if (ctx.req.url) {
731
- const url = new URL(ctx.req.url);
732
- return url.protocol === 'https:';
733
- }
734
- } catch {
735
- // Fall through to default
736
- }
737
-
738
- // Default to HTTP (e.g., for localhost development)
739
- return false;
740
- }
741
-
742
- /**
743
- * Signs a thread ID using HMAC SHA-256 and returns it in the format: threadId;signature
744
- * Format: thrd_abc123;base64signature
745
- */
746
- export async function signThreadId(threadId: string, secret: string): Promise<string> {
747
- const hasher = new Bun.CryptoHasher('sha256', secret);
748
- hasher.update(threadId);
749
- const signatureBase64 = hasher.digest('base64');
750
-
751
- return `${threadId};${signatureBase64}`;
752
- }
753
-
754
- /**
755
- * Verifies a signed thread ID header and returns the thread ID if valid, or undefined if invalid.
756
- * Expected format: thrd_abc123;base64signature
757
- */
758
- export async function verifySignedThreadId(
759
- signedValue: string,
760
- secret: string
761
- ): Promise<string | undefined> {
762
- const parts = signedValue.split(';');
763
- if (parts.length !== 2) {
764
- return undefined;
765
- }
766
-
767
- const [threadId, providedSignature] = parts;
768
-
769
- // Validate both parts exist
770
- if (!threadId || !providedSignature) {
771
- return undefined;
772
- }
773
-
774
- // Validate thread ID format before verifying signature
775
- if (!isValidThreadId(threadId)) {
776
- return undefined;
777
- }
778
-
779
- // Re-sign the thread ID and compare signatures
780
- const expectedSigned = await signThreadId(threadId, secret);
781
- const expectedSignature = expectedSigned.split(';')[1];
782
-
783
- // Validate signature exists
784
- if (!expectedSignature) {
785
- return undefined;
786
- }
787
-
788
- // Constant-time comparison to prevent timing attacks
789
- // Check lengths match first (fail fast if different lengths)
790
- if (providedSignature.length !== expectedSignature.length) {
791
- return undefined;
792
- }
793
-
794
- try {
795
- // Convert to Buffers for constant-time comparison
796
- const providedBuffer = Buffer.from(providedSignature, 'base64');
797
- const expectedBuffer = Buffer.from(expectedSignature, 'base64');
798
-
799
- if (timingSafeEqual(providedBuffer, expectedBuffer)) {
800
- return threadId;
801
- }
802
- } catch {
803
- // Comparison failed or buffer conversion error
804
- return undefined;
805
- }
806
-
807
- return undefined;
808
- }
809
-
810
- /**
811
- * DefaultThreadIDProvider will look for an HTTP header `x-thread-id` first,
812
- * then fall back to a signed cookie named `atid`, and use that as the thread id.
813
- * If not found, generate a new one. Validates incoming thread IDs against
814
- * runtime constraints. Uses AGENTUITY_SDK_KEY for signing, falls back to 'agentuity'.
815
- */
816
- export class DefaultThreadIDProvider implements ThreadIDProvider {
817
- private getSecret(): string {
818
- return process.env.AGENTUITY_SDK_KEY || 'agentuity';
819
- }
820
-
821
- async getThreadId(_appState: AppState, ctx: Context<Env>): Promise<string> {
822
- let threadId: string | undefined;
823
- const secret = this.getSecret();
824
-
825
- // Check signed header first
826
- const headerValue = ctx.req.header('x-thread-id');
827
- if (headerValue) {
828
- const verifiedThreadId = await verifySignedThreadId(headerValue, secret);
829
- if (verifiedThreadId) {
830
- threadId = verifiedThreadId;
831
- }
832
- }
833
-
834
- // Fall back to signed cookie
835
- if (!threadId) {
836
- const cookieValue = await getSignedCookie(ctx, secret, 'atid');
837
- if (cookieValue && typeof cookieValue === 'string' && isValidThreadId(cookieValue)) {
838
- threadId = cookieValue;
839
- }
840
- }
841
-
842
- threadId = threadId || generateId('thrd');
843
-
844
- await setSignedCookie(ctx, 'atid', threadId, secret, {
845
- httpOnly: true,
846
- secure: isSecureConnection(ctx),
847
- sameSite: 'Lax',
848
- path: '/',
849
- maxAge: 604800, // 1 week in seconds
850
- });
851
-
852
- // Set signed header in response
853
- const signedHeader = await signThreadId(threadId, secret);
854
- ctx.header('x-thread-id', signedHeader);
855
- return threadId;
856
- }
857
- }
858
-
859
- type LazyStateStatus = 'idle' | 'pending-writes' | 'loaded';
860
-
861
- type RestoreFn = () => Promise<{ state: Map<string, unknown>; metadata: Record<string, unknown> }>;
862
-
863
- export class LazyThreadState implements ThreadState {
864
- #status: LazyStateStatus = 'idle';
865
- #state: Map<string, unknown> = new Map();
866
- #pendingOperations: MergeOperation[] = [];
867
- #initialStateJson: string | undefined;
868
- #restoreFn: RestoreFn;
869
- #loadingPromise: Promise<void> | null = null;
870
-
871
- constructor(restoreFn: RestoreFn) {
872
- this.#restoreFn = restoreFn;
873
- }
874
-
875
- get loaded(): boolean {
876
- return this.#status === 'loaded';
877
- }
878
-
879
- get dirty(): boolean {
880
- if (this.#status === 'pending-writes') {
881
- return this.#pendingOperations.length > 0;
882
- }
883
- if (this.#status === 'loaded') {
884
- const currentJson = JSON.stringify(Object.fromEntries(this.#state));
885
- return currentJson !== this.#initialStateJson;
886
- }
887
- return false;
888
- }
889
-
890
- private async ensureLoaded(): Promise<void> {
891
- if (this.#status === 'loaded') {
892
- return;
893
- }
894
-
895
- if (this.#loadingPromise) {
896
- await this.#loadingPromise;
897
- return;
898
- }
899
-
900
- this.#loadingPromise = (async () => {
901
- try {
902
- await this.doLoad();
903
- } finally {
904
- this.#loadingPromise = null;
905
- }
906
- })();
907
-
908
- await this.#loadingPromise;
909
- }
910
-
911
- private async doLoad(): Promise<void> {
912
- const { state } = await this.#restoreFn();
913
-
914
- // Initialize state from restored data
915
- this.#state = new Map(state);
916
- this.#initialStateJson = JSON.stringify(Object.fromEntries(this.#state));
917
-
918
- // Apply any pending operations
919
- for (const op of this.#pendingOperations) {
920
- switch (op.op) {
921
- case 'clear':
922
- this.#state.clear();
923
- break;
924
- case 'set':
925
- if (op.key !== undefined) {
926
- this.#state.set(op.key, op.value);
927
- }
928
- break;
929
- case 'delete':
930
- if (op.key !== undefined) {
931
- this.#state.delete(op.key);
932
- }
933
- break;
934
- case 'push':
935
- if (op.key !== undefined) {
936
- const existing = this.#state.get(op.key);
937
- if (Array.isArray(existing)) {
938
- existing.push(op.value);
939
- // Apply maxRecords limit
940
- if (op.maxRecords !== undefined && existing.length > op.maxRecords) {
941
- existing.splice(0, existing.length - op.maxRecords);
942
- }
943
- } else if (existing === undefined) {
944
- this.#state.set(op.key, [op.value]);
945
- }
946
- // If existing is non-array, silently skip (error would have been thrown if loaded)
947
- }
948
- break;
949
- }
950
- }
951
-
952
- this.#pendingOperations = [];
953
- this.#status = 'loaded';
954
- }
955
-
956
- async get<T = unknown>(key: string): Promise<T | undefined> {
957
- await this.ensureLoaded();
958
- return this.#state.get(key) as T | undefined;
959
- }
960
-
961
- async set<T = unknown>(key: string, value: T): Promise<void> {
962
- if (this.#status === 'loaded') {
963
- this.#state.set(key, value);
964
- } else {
965
- this.#pendingOperations.push({ op: 'set', key, value });
966
- if (this.#status === 'idle') {
967
- this.#status = 'pending-writes';
968
- }
969
- }
970
- }
971
-
972
- async has(key: string): Promise<boolean> {
973
- await this.ensureLoaded();
974
- return this.#state.has(key);
975
- }
976
-
977
- async delete(key: string): Promise<void> {
978
- if (this.#status === 'loaded') {
979
- this.#state.delete(key);
980
- } else {
981
- this.#pendingOperations.push({ op: 'delete', key });
982
- if (this.#status === 'idle') {
983
- this.#status = 'pending-writes';
984
- }
985
- }
986
- }
987
-
988
- async clear(): Promise<void> {
989
- if (this.#status === 'loaded') {
990
- this.#state.clear();
991
- } else {
992
- // Clear replaces all previous pending operations
993
- this.#pendingOperations = [{ op: 'clear' }];
994
- if (this.#status === 'idle') {
995
- this.#status = 'pending-writes';
996
- }
997
- }
998
- }
999
-
1000
- async entries<T = unknown>(): Promise<[string, T][]> {
1001
- await this.ensureLoaded();
1002
- return Array.from(this.#state.entries()) as [string, T][];
1003
- }
1004
-
1005
- async keys(): Promise<string[]> {
1006
- await this.ensureLoaded();
1007
- return Array.from(this.#state.keys());
1008
- }
1009
-
1010
- async values<T = unknown>(): Promise<T[]> {
1011
- await this.ensureLoaded();
1012
- return Array.from(this.#state.values()) as T[];
1013
- }
1014
-
1015
- async size(): Promise<number> {
1016
- await this.ensureLoaded();
1017
- return this.#state.size;
1018
- }
1019
-
1020
- async push<T = unknown>(key: string, value: T, maxRecords?: number): Promise<void> {
1021
- if (this.#status === 'loaded') {
1022
- // When loaded, push to local array
1023
- const existing = this.#state.get(key);
1024
- if (Array.isArray(existing)) {
1025
- existing.push(value);
1026
- // Apply maxRecords limit
1027
- if (maxRecords !== undefined && existing.length > maxRecords) {
1028
- existing.splice(0, existing.length - maxRecords);
1029
- }
1030
- } else if (existing === undefined) {
1031
- this.#state.set(key, [value]);
1032
- } else {
1033
- throw new Error(`Cannot push to non-array value at key "${key}"`);
1034
- }
1035
- } else {
1036
- // Queue push operation for merge
1037
- const op: MergeOperation = { op: 'push', key, value };
1038
- if (maxRecords !== undefined) {
1039
- op.maxRecords = maxRecords;
1040
- }
1041
- this.#pendingOperations.push(op);
1042
- if (this.#status === 'idle') {
1043
- this.#status = 'pending-writes';
1044
- }
1045
- }
1046
- }
1047
-
1048
- /**
1049
- * Get the current status for save logic
1050
- * @internal
1051
- */
1052
- getStatus(): LazyStateStatus {
1053
- return this.#status;
1054
- }
1055
-
1056
- /**
1057
- * Get pending operations for merge command
1058
- * @internal
1059
- */
1060
- getPendingOperations(): MergeOperation[] {
1061
- return [...this.#pendingOperations];
1062
- }
1063
-
1064
- /**
1065
- * Get serialized state for full save.
1066
- * Ensures state is loaded before serializing.
1067
- * @internal
1068
- */
1069
- async getSerializedState(): Promise<Record<string, unknown>> {
1070
- await this.ensureLoaded();
1071
- return Object.fromEntries(this.#state);
1072
- }
1073
- }
1074
-
1075
- export class DefaultThread implements Thread {
1076
- readonly id: string;
1077
- readonly state: LazyThreadState;
1078
- #metadata: Record<string, unknown> | null = null;
1079
- #metadataDirty = false;
1080
- #metadataLoadPromise: Promise<void> | null = null;
1081
- private provider: ThreadProvider;
1082
- #restoreFn: RestoreFn;
1083
- #restoredMetadata: Record<string, unknown> | undefined;
1084
-
1085
- constructor(
1086
- provider: ThreadProvider,
1087
- id: string,
1088
- restoreFn: RestoreFn,
1089
- initialMetadata?: Record<string, unknown>
1090
- ) {
1091
- this.provider = provider;
1092
- this.id = id;
1093
- this.#restoreFn = restoreFn;
1094
- this.#restoredMetadata = initialMetadata;
1095
- this.state = new LazyThreadState(restoreFn);
1096
- }
1097
-
1098
- private async ensureMetadataLoaded(): Promise<void> {
1099
- if (this.#metadata !== null) {
1100
- return;
1101
- }
1102
-
1103
- // If we have initial metadata from thread creation, use it
1104
- if (this.#restoredMetadata !== undefined) {
1105
- this.#metadata = this.#restoredMetadata;
1106
- return;
1107
- }
1108
-
1109
- if (this.#metadataLoadPromise) {
1110
- await this.#metadataLoadPromise;
1111
- return;
1112
- }
1113
-
1114
- this.#metadataLoadPromise = (async () => {
1115
- try {
1116
- await this.doLoadMetadata();
1117
- } finally {
1118
- this.#metadataLoadPromise = null;
1119
- }
1120
- })();
1121
-
1122
- await this.#metadataLoadPromise;
1123
- }
1124
-
1125
- private async doLoadMetadata(): Promise<void> {
1126
- const { metadata } = await this.#restoreFn();
1127
- this.#metadata = metadata;
1128
- }
1129
-
1130
- async getMetadata(): Promise<Record<string, unknown>> {
1131
- await this.ensureMetadataLoaded();
1132
- return { ...this.#metadata! };
1133
- }
1134
-
1135
- async setMetadata(metadata: Record<string, unknown>): Promise<void> {
1136
- this.#metadata = metadata;
1137
- this.#metadataDirty = true;
1138
- }
1139
-
1140
- addEventListener(eventName: ThreadEventName, callback: ThreadEventCallback<any>): void {
1141
- let listeners = threadEventListeners.get(this);
1142
- if (!listeners) {
1143
- listeners = new Map();
1144
- threadEventListeners.set(this, listeners);
1145
- }
1146
- let callbacks = listeners.get(eventName);
1147
- if (!callbacks) {
1148
- callbacks = new Set();
1149
- listeners.set(eventName, callbacks);
1150
- }
1151
- callbacks.add(callback);
1152
- }
1153
-
1154
- removeEventListener(eventName: ThreadEventName, callback: ThreadEventCallback<any>): void {
1155
- const listeners = threadEventListeners.get(this);
1156
- if (!listeners) return;
1157
- const callbacks = listeners.get(eventName);
1158
- if (!callbacks) return;
1159
- callbacks.delete(callback);
1160
- }
1161
-
1162
- async fireEvent(eventName: ThreadEventName): Promise<void> {
1163
- await fireThreadEvent(this, eventName);
1164
- }
1165
-
1166
- async destroy(): Promise<void> {
1167
- await this.provider.destroy(this);
1168
- }
1169
-
1170
- /**
1171
- * Check if thread has any data (state or metadata)
1172
- */
1173
- async empty(): Promise<boolean> {
1174
- const stateSize = await this.state.size();
1175
- // Check both loaded metadata and initial metadata from constructor
1176
- const meta = this.#metadata ?? this.#restoredMetadata ?? {};
1177
- return stateSize === 0 && Object.keys(meta).length === 0;
1178
- }
1179
-
1180
- /**
1181
- * Check if thread needs saving
1182
- * @internal
1183
- */
1184
- needsSave(): boolean {
1185
- return this.state.dirty || this.#metadataDirty;
1186
- }
1187
-
1188
- /**
1189
- * Get the save mode for this thread
1190
- * @internal
1191
- */
1192
- getSaveMode(): 'none' | 'merge' | 'full' {
1193
- const stateStatus = this.state.getStatus();
1194
-
1195
- if (stateStatus === 'idle' && !this.#metadataDirty) {
1196
- return 'none';
1197
- }
1198
-
1199
- if (stateStatus === 'pending-writes') {
1200
- return 'merge';
1201
- }
1202
-
1203
- if (stateStatus === 'loaded' && (this.state.dirty || this.#metadataDirty)) {
1204
- return 'full';
1205
- }
1206
-
1207
- // Only metadata was changed without loading state
1208
- if (this.#metadataDirty) {
1209
- return 'merge';
1210
- }
1211
-
1212
- return 'none';
1213
- }
1214
-
1215
- /**
1216
- * Get pending operations for merge command
1217
- * @internal
1218
- */
1219
- getPendingOperations(): MergeOperation[] {
1220
- return this.state.getPendingOperations();
1221
- }
1222
-
1223
- /**
1224
- * Get metadata for saving (returns null if not loaded/modified)
1225
- * @internal
1226
- */
1227
- getMetadataForSave(): Record<string, unknown> | undefined {
1228
- if (this.#metadataDirty && this.#metadata) {
1229
- return this.#metadata;
1230
- }
1231
- return undefined;
1232
- }
1233
-
1234
- /**
1235
- * Get serialized state for full save.
1236
- * Ensures state is loaded before serializing.
1237
- * @internal
1238
- */
1239
- async getSerializedState(): Promise<string> {
1240
- const state = await this.state.getSerializedState();
1241
- // Also ensure metadata is loaded
1242
- const meta = this.#metadata ?? this.#restoredMetadata ?? {};
1243
- const hasState = Object.keys(state).length > 0;
1244
- const hasMetadata = Object.keys(meta).length > 0;
1245
-
1246
- if (!hasState && !hasMetadata) {
1247
- return '';
1248
- }
1249
-
1250
- const data: { state?: Record<string, unknown>; metadata?: Record<string, unknown> } = {};
1251
-
1252
- if (hasState) {
1253
- data.state = state;
1254
- }
1255
-
1256
- if (hasMetadata) {
1257
- data.metadata = meta;
1258
- }
1259
-
1260
- return JSON.stringify(data);
1261
- }
1262
- }
1263
-
1264
- export class DefaultSession implements Session {
1265
- readonly id: string;
1266
- readonly thread: Thread;
1267
- readonly state: Map<string, unknown>;
1268
- metadata: Record<string, unknown>;
1269
-
1270
- constructor(thread: Thread, id: string, metadata?: Record<string, unknown>) {
1271
- this.id = id;
1272
- this.thread = thread;
1273
- this.state = new Map();
1274
- this.metadata = metadata || {};
1275
- }
1276
-
1277
- addEventListener(eventName: SessionEventName, callback: SessionEventCallback<any>): void {
1278
- let listeners = sessionEventListeners.get(this);
1279
- if (!listeners) {
1280
- listeners = new Map();
1281
- sessionEventListeners.set(this, listeners);
1282
- }
1283
- let callbacks = listeners.get(eventName);
1284
- if (!callbacks) {
1285
- callbacks = new Set();
1286
- listeners.set(eventName, callbacks);
1287
- }
1288
- callbacks.add(callback);
1289
- }
1290
-
1291
- removeEventListener(eventName: SessionEventName, callback: SessionEventCallback<any>): void {
1292
- const listeners = sessionEventListeners.get(this);
1293
- if (!listeners) return;
1294
- const callbacks = listeners.get(eventName);
1295
- if (!callbacks) return;
1296
- callbacks.delete(callback);
1297
- }
1298
-
1299
- async fireEvent(eventName: SessionEventName): Promise<void> {
1300
- await fireSessionEvent(this, eventName);
1301
- }
1302
-
1303
- /**
1304
- * Serialize session state to JSON string for persistence.
1305
- * Returns undefined if state is empty or exceeds 1MB limit.
1306
- * @internal
1307
- */
1308
- serializeUserData(): string | undefined {
1309
- if (this.state.size === 0) {
1310
- return undefined;
1311
- }
1312
-
1313
- try {
1314
- const obj = Object.fromEntries(this.state);
1315
- const json = JSON.stringify(obj);
1316
-
1317
- // Check 1MB limit (1,048,576 bytes)
1318
- const sizeInBytes = new TextEncoder().encode(json).length;
1319
- if (sizeInBytes > 1048576) {
1320
- console.error(
1321
- `Session ${this.id} user_data exceeds 1MB limit (${sizeInBytes} bytes), data will not be persisted`
1322
- );
1323
- return undefined;
1324
- }
1325
-
1326
- return json;
1327
- } catch (err) {
1328
- console.error(`Failed to serialize session ${this.id} user_data:`, err);
1329
- return undefined;
1330
- }
1331
- }
1332
- }
1333
-
1334
- /**
1335
- * WebSocket client for thread state persistence.
1336
- *
1337
- * **WARNING: This class is exported for testing purposes only and is subject to change
1338
- * without notice. Do not use this class directly in production code.**
1339
- *
1340
- * @internal
1341
- * @experimental
1342
- */
1343
- /**
1344
- * Configuration options for ThreadWebSocketClient
1345
- */
1346
- export interface ThreadWebSocketClientOptions {
1347
- /** Connection timeout in milliseconds (default: 10000) */
1348
- connectionTimeoutMs?: number;
1349
- /** Request timeout in milliseconds (default: 10000) */
1350
- requestTimeoutMs?: number;
1351
- /** Base delay for reconnection backoff in milliseconds (default: 1000) */
1352
- reconnectBaseDelayMs?: number;
1353
- /** Maximum delay for reconnection backoff in milliseconds (default: 30000) */
1354
- reconnectMaxDelayMs?: number;
1355
- /** Maximum number of reconnection attempts (default: 5) */
1356
- maxReconnectAttempts?: number;
1357
- }
1358
-
1359
- export class ThreadWebSocketClient {
1360
- private ws: WebSocket | null = null;
1361
- private authenticated = false;
1362
- private pendingRequests = new Map<
1363
- string,
1364
- { resolve: (data?: string) => void; reject: (err: Error) => void }
1365
- >();
1366
- private reconnectAttempts = 0;
1367
- private maxReconnectAttempts: number;
1368
- private apiKey: string;
1369
- private wsUrl: string;
1370
- private wsConnecting: Promise<void> | null = null;
1371
- private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
1372
- private isDisposed = false;
1373
- private initialConnectResolve: (() => void) | null = null;
1374
- private initialConnectReject: ((err: Error) => void) | null = null;
1375
- private connectionTimeoutMs: number;
1376
- private requestTimeoutMs: number;
1377
- private reconnectBaseDelayMs: number;
1378
- private reconnectMaxDelayMs: number;
1379
-
1380
- constructor(apiKey: string, wsUrl: string, options: ThreadWebSocketClientOptions = {}) {
1381
- this.apiKey = apiKey;
1382
- this.wsUrl = wsUrl;
1383
- this.connectionTimeoutMs = options.connectionTimeoutMs ?? 10_000;
1384
- this.requestTimeoutMs = options.requestTimeoutMs ?? 10_000;
1385
- this.reconnectBaseDelayMs = options.reconnectBaseDelayMs ?? 1_000;
1386
- this.reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? 30_000;
1387
- this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
1388
- }
1389
-
1390
- async connect(): Promise<void> {
1391
- return new Promise((resolve, reject) => {
1392
- // Store the initial connect promise callbacks if this is the first attempt
1393
- if (this.reconnectAttempts === 0) {
1394
- this.initialConnectResolve = resolve;
1395
- this.initialConnectReject = reject;
1396
- }
1397
-
1398
- // Set connection timeout
1399
- const connectionTimeout = setTimeout(() => {
1400
- this.cleanup();
1401
- const rejectFn = this.initialConnectReject || reject;
1402
- this.initialConnectResolve = null;
1403
- this.initialConnectReject = null;
1404
- rejectFn(new Error(`WebSocket connection timeout (${this.connectionTimeoutMs}ms)`));
1405
- }, this.connectionTimeoutMs);
1406
-
1407
- try {
1408
- this.ws = new WebSocket(this.wsUrl);
1409
-
1410
- this.ws.addEventListener('open', () => {
1411
- internal.info('WebSocket connected');
1412
- // Send authentication (do NOT clear timeout yet - wait for auth response)
1413
- this.ws?.send(JSON.stringify({ authorization: this.apiKey }));
1414
- });
1415
-
1416
- this.ws.addEventListener('message', (event: MessageEvent) => {
1417
- try {
1418
- const message = JSON.parse(event.data);
1419
-
1420
- // Handle auth response
1421
- if ('success' in message && !this.authenticated) {
1422
- clearTimeout(connectionTimeout);
1423
- if (message.success) {
1424
- this.authenticated = true;
1425
- this.reconnectAttempts = 0;
1426
-
1427
- // Resolve both the current promise and the initial connect promise
1428
- const resolveFn = this.initialConnectResolve || resolve;
1429
- this.initialConnectResolve = null;
1430
- this.initialConnectReject = null;
1431
- resolveFn();
1432
- } else {
1433
- const err = new Error(
1434
- `WebSocket authentication failed: ${message.error || 'Unknown error'}`
1435
- );
1436
- this.cleanup();
1437
- const rejectFn = this.initialConnectReject || reject;
1438
- this.initialConnectResolve = null;
1439
- this.initialConnectReject = null;
1440
- rejectFn(err);
1441
- }
1442
- return;
1443
- }
1444
-
1445
- // Handle action response
1446
- if ('id' in message && this.pendingRequests.has(message.id)) {
1447
- const pending = this.pendingRequests.get(message.id)!;
1448
- this.pendingRequests.delete(message.id);
1449
-
1450
- if (message.success) {
1451
- pending.resolve(message.data);
1452
- } else {
1453
- pending.reject(new Error(message.error || 'Request failed'));
1454
- }
1455
- }
1456
- } catch {
1457
- // Ignore parse errors
1458
- }
1459
- });
1460
-
1461
- this.ws.addEventListener('error', (_event: Event) => {
1462
- clearTimeout(connectionTimeout);
1463
- if (!this.authenticated) {
1464
- // Don't reject immediately if we'll attempt reconnection
1465
- if (this.reconnectAttempts >= this.maxReconnectAttempts || this.isDisposed) {
1466
- const rejectFn = this.initialConnectReject || reject;
1467
- this.initialConnectResolve = null;
1468
- this.initialConnectReject = null;
1469
- rejectFn(new Error(`WebSocket error`));
1470
- }
1471
- }
1472
- });
1473
-
1474
- this.ws.addEventListener('close', () => {
1475
- clearTimeout(connectionTimeout);
1476
- const wasAuthenticated = this.authenticated;
1477
- this.authenticated = false;
1478
-
1479
- // Reject all pending requests
1480
- for (const [id, pending] of this.pendingRequests) {
1481
- pending.reject(new Error('WebSocket connection closed'));
1482
- this.pendingRequests.delete(id);
1483
- }
1484
-
1485
- // Don't attempt reconnection if disposed
1486
- if (this.isDisposed) {
1487
- // Reject initial connect if still pending
1488
- if (!wasAuthenticated && this.initialConnectReject) {
1489
- this.initialConnectReject(new Error('WebSocket closed before authentication'));
1490
- this.initialConnectResolve = null;
1491
- this.initialConnectReject = null;
1492
- }
1493
- return;
1494
- }
1495
-
1496
- // Attempt reconnection if within retry limits (even if auth didn't complete)
1497
- // This handles server rollouts where connection closes before auth finishes
1498
- if (this.reconnectAttempts < this.maxReconnectAttempts) {
1499
- this.reconnectAttempts++;
1500
- const delay = Math.min(
1501
- this.reconnectBaseDelayMs * Math.pow(2, this.reconnectAttempts),
1502
- this.reconnectMaxDelayMs
1503
- );
1504
-
1505
- internal.info(
1506
- `WebSocket disconnected, attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`
1507
- );
1508
-
1509
- // Schedule reconnection with backoff delay
1510
- this.reconnectTimer = setTimeout(() => {
1511
- this.reconnectTimer = null;
1512
- // Create new connection promise for reconnection
1513
- this.wsConnecting = this.connect().catch(() => {
1514
- // Reconnection failed, reset
1515
- this.wsConnecting = null;
1516
- });
1517
- }, delay);
1518
- } else {
1519
- internal.error(
1520
- `WebSocket disconnected after ${this.reconnectAttempts} attempts, giving up`
1521
- );
1522
-
1523
- // Reject initial connect if still pending (all attempts exhausted)
1524
- if (!wasAuthenticated && this.initialConnectReject) {
1525
- this.initialConnectReject(
1526
- new Error(
1527
- `WebSocket closed before authentication after ${this.reconnectAttempts} attempts`
1528
- )
1529
- );
1530
- this.initialConnectResolve = null;
1531
- this.initialConnectReject = null;
1532
- }
1533
- }
1534
- });
1535
- } catch (err) {
1536
- clearTimeout(connectionTimeout);
1537
- const rejectFn = this.initialConnectReject || reject;
1538
- this.initialConnectResolve = null;
1539
- this.initialConnectReject = null;
1540
- rejectFn(err as Error);
1541
- }
1542
- });
1543
- }
1544
-
1545
- async restore(threadId: string): Promise<string | undefined> {
1546
- // Wait for connection/reconnection if in progress
1547
- if (this.wsConnecting) {
1548
- await this.wsConnecting;
1549
- }
1550
-
1551
- if (!this.authenticated || !this.ws) {
1552
- throw new Error('WebSocket not connected or authenticated');
1553
- }
1554
-
1555
- return new Promise((resolve, reject) => {
1556
- const requestId = crypto.randomUUID();
1557
- this.pendingRequests.set(requestId, { resolve, reject });
1558
-
1559
- const message = {
1560
- id: requestId,
1561
- action: 'restore',
1562
- data: { thread_id: threadId },
1563
- };
1564
-
1565
- this.ws!.send(JSON.stringify(message));
1566
-
1567
- // Timeout after configured duration
1568
- setTimeout(() => {
1569
- if (this.pendingRequests.has(requestId)) {
1570
- this.pendingRequests.delete(requestId);
1571
- reject(new Error('Request timeout'));
1572
- }
1573
- }, this.requestTimeoutMs);
1574
- });
1575
- }
1576
-
1577
- async save(
1578
- threadId: string,
1579
- userData: string,
1580
- threadMetadata?: Record<string, unknown>
1581
- ): Promise<void> {
1582
- // Wait for connection/reconnection if in progress
1583
- if (this.wsConnecting) {
1584
- await this.wsConnecting;
1585
- }
1586
-
1587
- if (!this.authenticated || !this.ws) {
1588
- throw new Error('WebSocket not connected or authenticated');
1589
- }
1590
-
1591
- // Check 1MB limit
1592
- const sizeInBytes = new TextEncoder().encode(userData).length;
1593
- if (sizeInBytes > 1048576) {
1594
- console.error(
1595
- `Thread ${threadId} user_data exceeds 1MB limit (${sizeInBytes} bytes), data will not be persisted`
1596
- );
1597
- return;
1598
- }
1599
-
1600
- return new Promise((resolve, reject) => {
1601
- const requestId = crypto.randomUUID();
1602
- this.pendingRequests.set(requestId, {
1603
- resolve: () => resolve(),
1604
- reject,
1605
- });
1606
-
1607
- const data: { thread_id: string; user_data: string; metadata?: Record<string, unknown> } =
1608
- {
1609
- thread_id: threadId,
1610
- user_data: userData,
1611
- };
1612
-
1613
- if (threadMetadata && Object.keys(threadMetadata).length > 0) {
1614
- data.metadata = threadMetadata;
1615
- }
1616
-
1617
- const message = {
1618
- id: requestId,
1619
- action: 'save',
1620
- data,
1621
- };
1622
-
1623
- this.ws!.send(JSON.stringify(message));
1624
-
1625
- // Timeout after configured duration
1626
- setTimeout(() => {
1627
- if (this.pendingRequests.has(requestId)) {
1628
- this.pendingRequests.delete(requestId);
1629
- reject(new Error('Request timeout'));
1630
- }
1631
- }, this.requestTimeoutMs);
1632
- });
1633
- }
1634
-
1635
- async delete(threadId: string): Promise<void> {
1636
- // Wait for connection/reconnection if in progress
1637
- if (this.wsConnecting) {
1638
- await this.wsConnecting;
1639
- }
1640
-
1641
- if (!this.authenticated || !this.ws) {
1642
- throw new Error('WebSocket not connected or authenticated');
1643
- }
1644
-
1645
- return new Promise((resolve, reject) => {
1646
- const requestId = crypto.randomUUID();
1647
- this.pendingRequests.set(requestId, {
1648
- resolve: () => resolve(),
1649
- reject,
1650
- });
1651
-
1652
- const message = {
1653
- id: requestId,
1654
- action: 'delete',
1655
- data: { thread_id: threadId },
1656
- };
1657
-
1658
- this.ws!.send(JSON.stringify(message));
1659
-
1660
- // Timeout after configured duration
1661
- setTimeout(() => {
1662
- if (this.pendingRequests.has(requestId)) {
1663
- this.pendingRequests.delete(requestId);
1664
- reject(new Error('Request timeout'));
1665
- }
1666
- }, this.requestTimeoutMs);
1667
- });
1668
- }
1669
-
1670
- async merge(
1671
- threadId: string,
1672
- operations: MergeOperation[],
1673
- metadata?: Record<string, unknown>
1674
- ): Promise<void> {
1675
- // Wait for connection/reconnection if in progress
1676
- if (this.wsConnecting) {
1677
- await this.wsConnecting;
1678
- }
1679
-
1680
- if (!this.authenticated || !this.ws) {
1681
- throw new Error('WebSocket not connected or authenticated');
1682
- }
1683
-
1684
- return new Promise((resolve, reject) => {
1685
- const requestId = crypto.randomUUID();
1686
- this.pendingRequests.set(requestId, {
1687
- resolve: () => resolve(),
1688
- reject,
1689
- });
1690
-
1691
- const data: {
1692
- thread_id: string;
1693
- operations: MergeOperation[];
1694
- metadata?: Record<string, unknown>;
1695
- } = {
1696
- thread_id: threadId,
1697
- operations,
1698
- };
1699
-
1700
- if (metadata && Object.keys(metadata).length > 0) {
1701
- data.metadata = metadata;
1702
- }
1703
-
1704
- const message = {
1705
- id: requestId,
1706
- action: 'merge',
1707
- data,
1708
- };
1709
-
1710
- this.ws!.send(JSON.stringify(message));
1711
-
1712
- // Timeout after configured duration
1713
- setTimeout(() => {
1714
- if (this.pendingRequests.has(requestId)) {
1715
- this.pendingRequests.delete(requestId);
1716
- reject(new Error('Request timeout'));
1717
- }
1718
- }, this.requestTimeoutMs);
1719
- });
1720
- }
1721
-
1722
- cleanup(): void {
1723
- // Mark as disposed to prevent new reconnection attempts
1724
- this.isDisposed = true;
1725
-
1726
- // Cancel any pending reconnection timer
1727
- if (this.reconnectTimer) {
1728
- clearTimeout(this.reconnectTimer);
1729
- this.reconnectTimer = null;
1730
- }
1731
-
1732
- if (this.ws) {
1733
- this.ws.close();
1734
- this.ws = null;
1735
- }
1736
- this.authenticated = false;
1737
- this.pendingRequests.clear();
1738
- this.reconnectAttempts = 0;
1739
- this.wsConnecting = null;
1740
- this.initialConnectResolve = null;
1741
- this.initialConnectReject = null;
1742
- }
1743
- }
1744
-
1745
- export class DefaultThreadProvider implements ThreadProvider {
1746
- private appState: AppState | null = null;
1747
- private wsClient: ThreadWebSocketClient | null = null;
1748
- private wsConnecting: Promise<void> | null = null;
1749
- private threadIDProvider: ThreadIDProvider | null = null;
1750
-
1751
- async initialize(appState: AppState): Promise<void> {
1752
- this.appState = appState;
1753
- this.threadIDProvider = new DefaultThreadIDProvider();
1754
-
1755
- // Initialize WebSocket connection for thread persistence (async, non-blocking)
1756
- const apiKey = process.env.AGENTUITY_SDK_KEY;
1757
- if (apiKey) {
1758
- const serviceUrls = getServiceUrls(process.env.AGENTUITY_REGION ?? 'usc');
1759
- const catalystUrl = serviceUrls.catalyst;
1760
- const wsUrl = new URL('/thread/ws', catalystUrl.replace(/^http/, 'ws'));
1761
- internal.debug('connecting to %s', wsUrl);
1762
-
1763
- this.wsClient = new ThreadWebSocketClient(apiKey, wsUrl.toString());
1764
- // Connect in background, don't block initialization
1765
- this.wsConnecting = this.wsClient
1766
- .connect()
1767
- .then(() => {
1768
- this.wsConnecting = null;
1769
- })
1770
- .catch((err) => {
1771
- internal.error('Failed to connect to thread WebSocket:', err);
1772
- this.wsClient = null;
1773
- this.wsConnecting = null;
1774
- });
1775
- }
1776
- }
1777
-
1778
- setThreadIDProvider(provider: ThreadIDProvider): void {
1779
- this.threadIDProvider = provider;
1780
- }
1781
-
1782
- async restore(ctx: Context<Env>): Promise<Thread> {
1783
- const threadId = await this.threadIDProvider!.getThreadId(this.appState!, ctx);
1784
- validateThreadIdOrThrow(threadId);
1785
- internal.info('[thread] creating lazy thread %s (no eager restore)', threadId);
1786
-
1787
- // Create a restore function that will be called lazily when state/metadata is accessed
1788
- const restoreFn = async (): Promise<{
1789
- state: Map<string, unknown>;
1790
- metadata: Record<string, unknown>;
1791
- }> => {
1792
- internal.info('[thread] lazy loading state for thread %s', threadId);
1793
-
1794
- // Wait for WebSocket connection if still connecting
1795
- if (this.wsConnecting) {
1796
- internal.info('[thread] waiting for WebSocket connection');
1797
- await this.wsConnecting;
1798
- }
1799
-
1800
- if (!this.wsClient) {
1801
- internal.info('[thread] no WebSocket client available, returning empty state');
1802
- return { state: new Map(), metadata: {} };
1803
- }
1804
-
1805
- try {
1806
- const restoredData = await this.wsClient.restore(threadId);
1807
- if (restoredData) {
1808
- internal.info('[thread] restored state: %d bytes', restoredData.length);
1809
- const { flatStateJson, metadata } = parseThreadData(restoredData);
1810
-
1811
- const state = new Map<string, unknown>();
1812
- if (flatStateJson) {
1813
- try {
1814
- const data = JSON.parse(flatStateJson);
1815
- for (const [key, value] of Object.entries(data)) {
1816
- state.set(key, value);
1817
- }
1818
- } catch {
1819
- internal.info('[thread] failed to parse state JSON');
1820
- }
1821
- }
1822
-
1823
- return { state, metadata: metadata || {} };
1824
- }
1825
- internal.info('[thread] no existing state found');
1826
- return { state: new Map(), metadata: {} };
1827
- } catch (err) {
1828
- internal.info('[thread] WebSocket restore failed: %s', err);
1829
- return { state: new Map(), metadata: {} };
1830
- }
1831
- };
1832
-
1833
- const thread = new DefaultThread(this, threadId, restoreFn);
1834
- await fireEvent('thread.created', thread);
1835
- return thread;
1836
- }
1837
-
1838
- async save(thread: Thread): Promise<void> {
1839
- if (thread instanceof DefaultThread) {
1840
- const saveMode = thread.getSaveMode();
1841
- internal.info(
1842
- '[thread] DefaultThreadProvider.save() - thread %s, saveMode: %s, hasWsClient: %s',
1843
- thread.id,
1844
- saveMode,
1845
- !!this.wsClient
1846
- );
1847
-
1848
- if (saveMode === 'none') {
1849
- internal.info('[thread] skipping save - no changes');
1850
- return;
1851
- }
1852
-
1853
- // Wait for WebSocket connection if still connecting
1854
- if (this.wsConnecting) {
1855
- internal.info('[thread] waiting for WebSocket connection');
1856
- await this.wsConnecting;
1857
- }
1858
-
1859
- if (!this.wsClient) {
1860
- internal.info('[thread] no WebSocket client available, skipping save');
1861
- return;
1862
- }
1863
-
1864
- try {
1865
- if (saveMode === 'merge') {
1866
- const operations = thread.getPendingOperations();
1867
- const metadata = thread.getMetadataForSave();
1868
- internal.info(
1869
- '[thread] sending merge command with %d operations',
1870
- operations.length
1871
- );
1872
- await this.wsClient.merge(thread.id, operations, metadata);
1873
- internal.info('[thread] WebSocket merge completed');
1874
- } else if (saveMode === 'full') {
1875
- const serialized = await thread.getSerializedState();
1876
- internal.info(
1877
- '[thread] saving to WebSocket, serialized length: %d',
1878
- serialized.length
1879
- );
1880
- const metadata = thread.getMetadataForSave();
1881
- await this.wsClient.save(thread.id, serialized, metadata);
1882
- internal.info('[thread] WebSocket save completed');
1883
- }
1884
- } catch (err) {
1885
- internal.info('[thread] WebSocket save/merge failed: %s', err);
1886
- // Don't throw - allow request to complete even if save fails
1887
- }
1888
- }
1889
- }
1890
-
1891
- async destroy(thread: Thread): Promise<void> {
1892
- if (thread instanceof DefaultThread) {
1893
- try {
1894
- // Wait for WebSocket connection if still connecting
1895
- if (this.wsConnecting) {
1896
- await this.wsConnecting;
1897
- }
1898
-
1899
- // Delete thread from remote storage
1900
- if (this.wsClient) {
1901
- try {
1902
- await this.wsClient.delete(thread.id);
1903
- } catch {
1904
- // Thread might not exist in remote storage if it was never persisted
1905
- // This is normal for ephemeral threads, so just log at debug level
1906
- internal.debug(
1907
- `Thread ${thread.id} not found in remote storage (already deleted or never persisted)`
1908
- );
1909
- // Continue with local cleanup even if remote delete fails
1910
- }
1911
- }
1912
-
1913
- await thread.fireEvent('destroyed');
1914
- await fireEvent('thread.destroyed', thread);
1915
- } finally {
1916
- threadEventListeners.delete(thread);
1917
- }
1918
- }
1919
- }
1920
- }
1921
-
1922
- export class DefaultSessionProvider implements SessionProvider {
1923
- private sessions = new Map<string, DefaultSession>();
1924
-
1925
- async initialize(_appState: AppState): Promise<void> {
1926
- // No initialization needed for in-memory provider
1927
- }
1928
-
1929
- async restore(thread: Thread, sessionId: string): Promise<Session> {
1930
- internal.info('[session] restoring session %s for thread %s', sessionId, thread.id);
1931
- let session = this.sessions.get(sessionId);
1932
- if (!session) {
1933
- session = new DefaultSession(thread, sessionId);
1934
- this.sessions.set(sessionId, session);
1935
- internal.info('[session] created new session, firing session.started');
1936
- await fireEvent('session.started', session);
1937
- } else {
1938
- internal.info('[session] found existing session');
1939
- }
1940
- return session;
1941
- }
1942
-
1943
- async save(session: Session): Promise<void> {
1944
- if (session instanceof DefaultSession) {
1945
- internal.info(
1946
- '[session] DefaultSessionProvider.save() - firing completed event for session %s',
1947
- session.id
1948
- );
1949
- try {
1950
- await session.fireEvent('completed');
1951
- internal.info('[session] session.fireEvent completed, firing app event');
1952
- await fireEvent('session.completed', session);
1953
- internal.info('[session] session.completed app event fired');
1954
- } finally {
1955
- this.sessions.delete(session.id);
1956
- sessionEventListeners.delete(session);
1957
- }
1958
- }
1959
- }
1960
- }