@retinue/agentkit 0.1.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 (405) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +310 -0
  3. package/dist/adapters/bullmq/consumer.d.ts +33 -0
  4. package/dist/adapters/bullmq/consumer.js +41 -0
  5. package/dist/adapters/bullmq/dispatcher.d.ts +74 -0
  6. package/dist/adapters/bullmq/dispatcher.js +160 -0
  7. package/dist/adapters/bullmq/export.d.ts +31 -0
  8. package/dist/adapters/bullmq/export.js +53 -0
  9. package/dist/adapters/bullmq/extraction.d.ts +42 -0
  10. package/dist/adapters/bullmq/extraction.js +63 -0
  11. package/dist/adapters/bullmq/index.d.ts +13 -0
  12. package/dist/adapters/bullmq/index.js +13 -0
  13. package/dist/adapters/bullmq/lock.d.ts +77 -0
  14. package/dist/adapters/bullmq/lock.js +126 -0
  15. package/dist/adapters/bullmq/queue.d.ts +50 -0
  16. package/dist/adapters/bullmq/queue.js +81 -0
  17. package/dist/adapters/memory/artifact-exports.d.ts +11 -0
  18. package/dist/adapters/memory/artifact-exports.js +102 -0
  19. package/dist/adapters/memory/artifacts.d.ts +15 -0
  20. package/dist/adapters/memory/artifacts.js +134 -0
  21. package/dist/adapters/memory/blobs.d.ts +7 -0
  22. package/dist/adapters/memory/blobs.js +27 -0
  23. package/dist/adapters/memory/evaluation.d.ts +18 -0
  24. package/dist/adapters/memory/evaluation.js +148 -0
  25. package/dist/adapters/memory/files.d.ts +27 -0
  26. package/dist/adapters/memory/files.js +0 -0
  27. package/dist/adapters/memory/flows.d.ts +16 -0
  28. package/dist/adapters/memory/flows.js +117 -0
  29. package/dist/adapters/memory/hitl.d.ts +9 -0
  30. package/dist/adapters/memory/hitl.js +130 -0
  31. package/dist/adapters/memory/idempotency.d.ts +13 -0
  32. package/dist/adapters/memory/idempotency.js +32 -0
  33. package/dist/adapters/memory/index.d.ts +39 -0
  34. package/dist/adapters/memory/index.js +107 -0
  35. package/dist/adapters/memory/knowledge.d.ts +43 -0
  36. package/dist/adapters/memory/knowledge.js +248 -0
  37. package/dist/adapters/memory/mcp.d.ts +9 -0
  38. package/dist/adapters/memory/mcp.js +37 -0
  39. package/dist/adapters/memory/message-store.d.ts +17 -0
  40. package/dist/adapters/memory/message-store.js +70 -0
  41. package/dist/adapters/memory/principal-memory.d.ts +7 -0
  42. package/dist/adapters/memory/principal-memory.js +83 -0
  43. package/dist/adapters/memory/runtime.d.ts +29 -0
  44. package/dist/adapters/memory/runtime.js +0 -0
  45. package/dist/adapters/memory/sessions.d.ts +29 -0
  46. package/dist/adapters/memory/sessions.js +0 -0
  47. package/dist/adapters/memory/skills.d.ts +10 -0
  48. package/dist/adapters/memory/skills.js +41 -0
  49. package/dist/adapters/memory/thread-summary.d.ts +7 -0
  50. package/dist/adapters/memory/thread-summary.js +29 -0
  51. package/dist/adapters/memory/usage-limits.d.ts +13 -0
  52. package/dist/adapters/memory/usage-limits.js +72 -0
  53. package/dist/adapters/memory/usage.d.ts +16 -0
  54. package/dist/adapters/memory/usage.js +279 -0
  55. package/dist/adapters/otel/index.d.ts +111 -0
  56. package/dist/adapters/otel/index.js +133 -0
  57. package/dist/adapters/postgres/artifact-exports.d.ts +12 -0
  58. package/dist/adapters/postgres/artifact-exports.js +117 -0
  59. package/dist/adapters/postgres/artifacts.d.ts +16 -0
  60. package/dist/adapters/postgres/artifacts.js +172 -0
  61. package/dist/adapters/postgres/checkpoint-store.d.ts +16 -0
  62. package/dist/adapters/postgres/checkpoint-store.js +34 -0
  63. package/dist/adapters/postgres/config.d.ts +15 -0
  64. package/dist/adapters/postgres/config.js +187 -0
  65. package/dist/adapters/postgres/conversation-store.d.ts +4 -0
  66. package/dist/adapters/postgres/conversation-store.js +82 -0
  67. package/dist/adapters/postgres/evaluation.d.ts +17 -0
  68. package/dist/adapters/postgres/evaluation.js +193 -0
  69. package/dist/adapters/postgres/file-content.d.ts +30 -0
  70. package/dist/adapters/postgres/file-content.js +111 -0
  71. package/dist/adapters/postgres/files.d.ts +19 -0
  72. package/dist/adapters/postgres/files.js +209 -0
  73. package/dist/adapters/postgres/flows.d.ts +20 -0
  74. package/dist/adapters/postgres/flows.js +206 -0
  75. package/dist/adapters/postgres/hitl.d.ts +5 -0
  76. package/dist/adapters/postgres/hitl.js +247 -0
  77. package/dist/adapters/postgres/index.d.ts +35 -0
  78. package/dist/adapters/postgres/index.js +35 -0
  79. package/dist/adapters/postgres/knowledge.d.ts +48 -0
  80. package/dist/adapters/postgres/knowledge.js +255 -0
  81. package/dist/adapters/postgres/memory.d.ts +14 -0
  82. package/dist/adapters/postgres/memory.js +194 -0
  83. package/dist/adapters/postgres/message-store.d.ts +11 -0
  84. package/dist/adapters/postgres/message-store.js +145 -0
  85. package/dist/adapters/postgres/migrations.d.ts +69 -0
  86. package/dist/adapters/postgres/migrations.js +1594 -0
  87. package/dist/adapters/postgres/pg-executor.d.ts +19 -0
  88. package/dist/adapters/postgres/pg-executor.js +32 -0
  89. package/dist/adapters/postgres/retention.d.ts +26 -0
  90. package/dist/adapters/postgres/retention.js +59 -0
  91. package/dist/adapters/postgres/rollups.d.ts +17 -0
  92. package/dist/adapters/postgres/rollups.js +267 -0
  93. package/dist/adapters/postgres/run-coordinator.d.ts +5 -0
  94. package/dist/adapters/postgres/run-coordinator.js +98 -0
  95. package/dist/adapters/postgres/run-event-log.d.ts +26 -0
  96. package/dist/adapters/postgres/run-event-log.js +30 -0
  97. package/dist/adapters/postgres/run-store.d.ts +4 -0
  98. package/dist/adapters/postgres/run-store.js +199 -0
  99. package/dist/adapters/postgres/schema.d.ts +39 -0
  100. package/dist/adapters/postgres/schema.js +70 -0
  101. package/dist/adapters/postgres/session-state.d.ts +7 -0
  102. package/dist/adapters/postgres/session-state.js +99 -0
  103. package/dist/adapters/postgres/sql.d.ts +8 -0
  104. package/dist/adapters/postgres/sql.js +2 -0
  105. package/dist/adapters/postgres/transaction.d.ts +37 -0
  106. package/dist/adapters/postgres/transaction.js +93 -0
  107. package/dist/adapters/postgres/unit-of-work.d.ts +18 -0
  108. package/dist/adapters/postgres/unit-of-work.js +8 -0
  109. package/dist/adapters/postgres/usage-limits.d.ts +15 -0
  110. package/dist/adapters/postgres/usage-limits.js +136 -0
  111. package/dist/adapters/postgres/usage.d.ts +15 -0
  112. package/dist/adapters/postgres/usage.js +226 -0
  113. package/dist/adapters/redis/index.d.ts +9 -0
  114. package/dist/adapters/redis/index.js +9 -0
  115. package/dist/adapters/redis/realtime.d.ts +74 -0
  116. package/dist/adapters/redis/realtime.js +112 -0
  117. package/dist/adapters/supabase/index.d.ts +88 -0
  118. package/dist/adapters/supabase/index.js +84 -0
  119. package/dist/adapters/supabase/realtime.d.ts +30 -0
  120. package/dist/adapters/supabase/realtime.js +53 -0
  121. package/dist/adapters/supabase/rls.d.ts +99 -0
  122. package/dist/adapters/supabase/rls.js +216 -0
  123. package/dist/adapters/supabase/storage.d.ts +50 -0
  124. package/dist/adapters/supabase/storage.js +207 -0
  125. package/dist/agents/agent.d.ts +66 -0
  126. package/dist/agents/agent.js +209 -0
  127. package/dist/agents/define.d.ts +21 -0
  128. package/dist/agents/define.js +63 -0
  129. package/dist/agents/engine.d.ts +98 -0
  130. package/dist/agents/engine.js +462 -0
  131. package/dist/agents/index.d.ts +50 -0
  132. package/dist/agents/index.js +17 -0
  133. package/dist/artifacts/index.d.ts +114 -0
  134. package/dist/artifacts/index.js +219 -0
  135. package/dist/authorization/index.d.ts +76 -0
  136. package/dist/authorization/index.js +76 -0
  137. package/dist/capabilities/index.d.ts +120 -0
  138. package/dist/capabilities/index.js +167 -0
  139. package/dist/capabilities/runtime.d.ts +89 -0
  140. package/dist/capabilities/runtime.js +84 -0
  141. package/dist/citations/index.d.ts +161 -0
  142. package/dist/citations/index.js +182 -0
  143. package/dist/context/assembler.d.ts +82 -0
  144. package/dist/context/assembler.js +129 -0
  145. package/dist/context/compaction.d.ts +45 -0
  146. package/dist/context/compaction.js +55 -0
  147. package/dist/context/index.d.ts +75 -0
  148. package/dist/context/index.js +17 -0
  149. package/dist/core/content-parts.d.ts +194 -0
  150. package/dist/core/content-parts.js +23 -0
  151. package/dist/core/context.d.ts +51 -0
  152. package/dist/core/context.js +9 -0
  153. package/dist/core/env.d.ts +25 -0
  154. package/dist/core/env.js +41 -0
  155. package/dist/core/errors.d.ts +30 -0
  156. package/dist/core/errors.js +65 -0
  157. package/dist/core/events.d.ts +139 -0
  158. package/dist/core/events.js +99 -0
  159. package/dist/core/ids.d.ts +52 -0
  160. package/dist/core/ids.js +13 -0
  161. package/dist/core/index.d.ts +9 -0
  162. package/dist/core/index.js +9 -0
  163. package/dist/core/tokens.d.ts +22 -0
  164. package/dist/core/tokens.js +22 -0
  165. package/dist/core/validation.d.ts +34 -0
  166. package/dist/core/validation.js +176 -0
  167. package/dist/documents/extraction.d.ts +121 -0
  168. package/dist/documents/extraction.js +293 -0
  169. package/dist/documents/index.d.ts +199 -0
  170. package/dist/documents/index.js +65 -0
  171. package/dist/documents/parsers/pdf.d.ts +47 -0
  172. package/dist/documents/parsers/pdf.js +508 -0
  173. package/dist/documents/parsers/text.d.ts +59 -0
  174. package/dist/documents/parsers/text.js +325 -0
  175. package/dist/documents/read-tool.d.ts +52 -0
  176. package/dist/documents/read-tool.js +109 -0
  177. package/dist/documents/render.d.ts +29 -0
  178. package/dist/documents/render.js +59 -0
  179. package/dist/documents/vision.d.ts +159 -0
  180. package/dist/documents/vision.js +214 -0
  181. package/dist/entries/adapters-bullmq.d.ts +8 -0
  182. package/dist/entries/adapters-bullmq.js +8 -0
  183. package/dist/entries/adapters-otel.d.ts +13 -0
  184. package/dist/entries/adapters-otel.js +13 -0
  185. package/dist/entries/adapters-postgres.d.ts +10 -0
  186. package/dist/entries/adapters-postgres.js +10 -0
  187. package/dist/entries/adapters-redis.d.ts +3 -0
  188. package/dist/entries/adapters-redis.js +3 -0
  189. package/dist/entries/context.d.ts +20 -0
  190. package/dist/entries/context.js +20 -0
  191. package/dist/entries/flows.d.ts +15 -0
  192. package/dist/entries/flows.js +15 -0
  193. package/dist/entries/hitl.d.ts +10 -0
  194. package/dist/entries/hitl.js +10 -0
  195. package/dist/entries/knowledge.d.ts +18 -0
  196. package/dist/entries/knowledge.js +19 -0
  197. package/dist/entries/mcp.d.ts +10 -0
  198. package/dist/entries/mcp.js +10 -0
  199. package/dist/entries/observability.d.ts +14 -0
  200. package/dist/entries/observability.js +16 -0
  201. package/dist/entries/persistence.d.ts +11 -0
  202. package/dist/entries/persistence.js +11 -0
  203. package/dist/entries/providers.d.ts +14 -0
  204. package/dist/entries/providers.js +14 -0
  205. package/dist/entries/runtime.d.ts +13 -0
  206. package/dist/entries/runtime.js +13 -0
  207. package/dist/entries/server.d.ts +24 -0
  208. package/dist/entries/server.js +24 -0
  209. package/dist/entries/tools.d.ts +21 -0
  210. package/dist/entries/tools.js +21 -0
  211. package/dist/entries/usage.d.ts +10 -0
  212. package/dist/entries/usage.js +10 -0
  213. package/dist/evaluation/gate.d.ts +168 -0
  214. package/dist/evaluation/gate.js +180 -0
  215. package/dist/evaluation/graders.d.ts +125 -0
  216. package/dist/evaluation/graders.js +203 -0
  217. package/dist/evaluation/index.d.ts +120 -0
  218. package/dist/evaluation/index.js +183 -0
  219. package/dist/evaluation/judge.d.ts +75 -0
  220. package/dist/evaluation/judge.js +111 -0
  221. package/dist/export/index.d.ts +162 -0
  222. package/dist/export/index.js +363 -0
  223. package/dist/export/markdown.d.ts +19 -0
  224. package/dist/export/markdown.js +29 -0
  225. package/dist/export/pdf.d.ts +73 -0
  226. package/dist/export/pdf.js +407 -0
  227. package/dist/files/context.d.ts +97 -0
  228. package/dist/files/context.js +185 -0
  229. package/dist/files/index.d.ts +210 -0
  230. package/dist/files/index.js +338 -0
  231. package/dist/files/read-tool.d.ts +81 -0
  232. package/dist/files/read-tool.js +163 -0
  233. package/dist/files/turn-parts.d.ts +96 -0
  234. package/dist/files/turn-parts.js +171 -0
  235. package/dist/flows/index.d.ts +270 -0
  236. package/dist/flows/index.js +62 -0
  237. package/dist/flows/interpreter.d.ts +146 -0
  238. package/dist/flows/interpreter.js +426 -0
  239. package/dist/flows/runner.d.ts +145 -0
  240. package/dist/flows/runner.js +270 -0
  241. package/dist/graphql/index.d.ts +8 -0
  242. package/dist/graphql/index.js +8 -0
  243. package/dist/graphql/resolvers.d.ts +237 -0
  244. package/dist/graphql/resolvers.js +253 -0
  245. package/dist/graphql/schema.d.ts +11 -0
  246. package/dist/graphql/schema.js +258 -0
  247. package/dist/graphql/sse.d.ts +77 -0
  248. package/dist/graphql/sse.js +100 -0
  249. package/dist/hitl/approved-execution.d.ts +127 -0
  250. package/dist/hitl/approved-execution.js +177 -0
  251. package/dist/hitl/index.d.ts +79 -0
  252. package/dist/hitl/index.js +12 -0
  253. package/dist/hitl/service.d.ts +221 -0
  254. package/dist/hitl/service.js +268 -0
  255. package/dist/idempotency/index.d.ts +70 -0
  256. package/dist/idempotency/index.js +59 -0
  257. package/dist/index.d.ts +103 -0
  258. package/dist/index.js +60 -0
  259. package/dist/knowledge/chunking.d.ts +57 -0
  260. package/dist/knowledge/chunking.js +158 -0
  261. package/dist/knowledge/index.d.ts +119 -0
  262. package/dist/knowledge/index.js +166 -0
  263. package/dist/knowledge/retrieval.d.ts +146 -0
  264. package/dist/knowledge/retrieval.js +170 -0
  265. package/dist/loadtest/harness.d.ts +168 -0
  266. package/dist/loadtest/harness.js +507 -0
  267. package/dist/loadtest/index.d.ts +13 -0
  268. package/dist/loadtest/index.js +13 -0
  269. package/dist/loadtest/injection.d.ts +89 -0
  270. package/dist/loadtest/injection.js +147 -0
  271. package/dist/loadtest/metrics.d.ts +197 -0
  272. package/dist/loadtest/metrics.js +160 -0
  273. package/dist/loadtest/runbooks.d.ts +28 -0
  274. package/dist/loadtest/runbooks.js +159 -0
  275. package/dist/loadtest/scenario.d.ts +104 -0
  276. package/dist/loadtest/scenario.js +208 -0
  277. package/dist/mcp/egress.d.ts +53 -0
  278. package/dist/mcp/egress.js +115 -0
  279. package/dist/mcp/index.d.ts +93 -0
  280. package/dist/mcp/index.js +33 -0
  281. package/dist/mcp/provider.d.ts +62 -0
  282. package/dist/mcp/provider.js +0 -0
  283. package/dist/models/index.d.ts +98 -0
  284. package/dist/models/index.js +74 -0
  285. package/dist/models/pricing.d.ts +24 -0
  286. package/dist/models/pricing.js +37 -0
  287. package/dist/models/provider-factory.d.ts +31 -0
  288. package/dist/models/provider-factory.js +67 -0
  289. package/dist/models/streaming.d.ts +145 -0
  290. package/dist/models/streaming.js +272 -0
  291. package/dist/models/vision.d.ts +38 -0
  292. package/dist/models/vision.js +62 -0
  293. package/dist/persistence/index.d.ts +1654 -0
  294. package/dist/persistence/index.js +226 -0
  295. package/dist/principal-memory/index.d.ts +106 -0
  296. package/dist/principal-memory/index.js +89 -0
  297. package/dist/retention/index.d.ts +89 -0
  298. package/dist/retention/index.js +70 -0
  299. package/dist/runtime/checkpoint.d.ts +37 -0
  300. package/dist/runtime/checkpoint.js +22 -0
  301. package/dist/runtime/index.d.ts +118 -0
  302. package/dist/runtime/index.js +69 -0
  303. package/dist/runtime/retry.d.ts +95 -0
  304. package/dist/runtime/retry.js +126 -0
  305. package/dist/runtime/serialization.d.ts +85 -0
  306. package/dist/runtime/serialization.js +95 -0
  307. package/dist/runtime/streaming.d.ts +54 -0
  308. package/dist/runtime/streaming.js +115 -0
  309. package/dist/runtime/worker.d.ts +130 -0
  310. package/dist/runtime/worker.js +405 -0
  311. package/dist/security/checklist.d.ts +53 -0
  312. package/dist/security/checklist.js +204 -0
  313. package/dist/security/findings.d.ts +56 -0
  314. package/dist/security/findings.js +168 -0
  315. package/dist/security/index.d.ts +14 -0
  316. package/dist/security/index.js +14 -0
  317. package/dist/security/prompt-safety.d.ts +100 -0
  318. package/dist/security/prompt-safety.js +133 -0
  319. package/dist/server/boot.d.ts +32 -0
  320. package/dist/server/boot.js +36 -0
  321. package/dist/server/cli-worker.d.ts +37 -0
  322. package/dist/server/cli-worker.js +151 -0
  323. package/dist/server/cli.d.ts +27 -0
  324. package/dist/server/cli.js +74 -0
  325. package/dist/server/config.d.ts +42 -0
  326. package/dist/server/config.js +127 -0
  327. package/dist/server/health.d.ts +59 -0
  328. package/dist/server/health.js +90 -0
  329. package/dist/server/host.d.ts +39 -0
  330. package/dist/server/host.js +124 -0
  331. package/dist/server/index.d.ts +15 -0
  332. package/dist/server/index.js +15 -0
  333. package/dist/server/main.d.ts +16 -0
  334. package/dist/server/main.js +31 -0
  335. package/dist/server/sse-route.d.ts +21 -0
  336. package/dist/server/sse-route.js +282 -0
  337. package/dist/skills/index.d.ts +67 -0
  338. package/dist/skills/index.js +31 -0
  339. package/dist/skills/resolver.d.ts +54 -0
  340. package/dist/skills/resolver.js +121 -0
  341. package/dist/teams/index.d.ts +93 -0
  342. package/dist/teams/index.js +207 -0
  343. package/dist/telemetry/index.d.ts +157 -0
  344. package/dist/telemetry/index.js +71 -0
  345. package/dist/telemetry/instrument.d.ts +108 -0
  346. package/dist/telemetry/instrument.js +232 -0
  347. package/dist/telemetry/log-events.d.ts +17 -0
  348. package/dist/telemetry/log-events.js +58 -0
  349. package/dist/telemetry/metrics.d.ts +123 -0
  350. package/dist/telemetry/metrics.js +135 -0
  351. package/dist/telemetry/noop.d.ts +39 -0
  352. package/dist/telemetry/noop.js +143 -0
  353. package/dist/telemetry/redaction.d.ts +64 -0
  354. package/dist/telemetry/redaction.js +153 -0
  355. package/dist/telemetry/spans.d.ts +56 -0
  356. package/dist/telemetry/spans.js +78 -0
  357. package/dist/telemetry/trace-context.d.ts +55 -0
  358. package/dist/telemetry/trace-context.js +60 -0
  359. package/dist/toolkit/compute.d.ts +53 -0
  360. package/dist/toolkit/compute.js +152 -0
  361. package/dist/toolkit/data.d.ts +98 -0
  362. package/dist/toolkit/data.js +235 -0
  363. package/dist/toolkit/http.d.ts +113 -0
  364. package/dist/toolkit/http.js +205 -0
  365. package/dist/toolkit/index.d.ts +21 -0
  366. package/dist/toolkit/index.js +17 -0
  367. package/dist/toolkit/web.d.ts +107 -0
  368. package/dist/toolkit/web.js +147 -0
  369. package/dist/tools/define.d.ts +25 -0
  370. package/dist/tools/define.js +45 -0
  371. package/dist/tools/delegating.d.ts +132 -0
  372. package/dist/tools/delegating.js +211 -0
  373. package/dist/tools/index.d.ts +129 -0
  374. package/dist/tools/index.js +33 -0
  375. package/dist/tools/library/compute.d.ts +11 -0
  376. package/dist/tools/library/compute.js +46 -0
  377. package/dist/tools/library/data.d.ts +16 -0
  378. package/dist/tools/library/data.js +92 -0
  379. package/dist/tools/library/http.d.ts +28 -0
  380. package/dist/tools/library/http.js +71 -0
  381. package/dist/tools/library/index.d.ts +97 -0
  382. package/dist/tools/library/index.js +134 -0
  383. package/dist/tools/library/knowledge.d.ts +39 -0
  384. package/dist/tools/library/knowledge.js +58 -0
  385. package/dist/tools/library/web.d.ts +19 -0
  386. package/dist/tools/library/web.js +65 -0
  387. package/dist/tools/meta-tools.d.ts +19 -0
  388. package/dist/tools/meta-tools.js +36 -0
  389. package/dist/tools/registry.d.ts +146 -0
  390. package/dist/tools/registry.js +291 -0
  391. package/dist/usage/index.d.ts +105 -0
  392. package/dist/usage/index.js +20 -0
  393. package/dist/usage/quota.d.ts +258 -0
  394. package/dist/usage/quota.js +510 -0
  395. package/dist/usage/recorder.d.ts +29 -0
  396. package/dist/usage/recorder.js +96 -0
  397. package/dist/usage/rollups.d.ts +121 -0
  398. package/dist/usage/rollups.js +157 -0
  399. package/dist/worker/export.d.ts +57 -0
  400. package/dist/worker/export.js +81 -0
  401. package/dist/worker/extraction.d.ts +57 -0
  402. package/dist/worker/extraction.js +84 -0
  403. package/dist/worker/main.d.ts +103 -0
  404. package/dist/worker/main.js +159 -0
  405. package/package.json +187 -0
@@ -0,0 +1,136 @@
1
+ /**
2
+ * PostgreSQL `UsageLimitStore` — admin-configured spend limits (#175).
3
+ *
4
+ * `principal_id IS NULL` is the tenant default; a non-null row overrides it for one person. One table rather than
5
+ * two, because "the default" and "an override" are the same kind of fact — and resolution is then a single query
6
+ * ordered by specificity rather than two queries and a merge in application code.
7
+ *
8
+ * Every limit column is nullable and NULL means **unbounded**, not zero. That direction matches `QuotaLimits` and
9
+ * is the whole reason it is stated twice: a misconfigured quota that blocks everything is an outage, and one that
10
+ * blocks nothing is a bill the rollups make visible. An outage is only visible to the customer it happens to.
11
+ */
12
+ import { parseWindowKey, windowKey } from "../../persistence/index.js";
13
+ const iso = (v) => (v instanceof Date ? v.toISOString() : new Date(v).toISOString());
14
+ const num = (v) => (v === null ? undefined : Number(v));
15
+ const toLimit = (r) => ({
16
+ tenantId: r.tenant_id,
17
+ ...(r.principal_id === null ? {} : { principalId: r.principal_id }),
18
+ ...(r.model_id === null ? {} : { modelId: r.model_id }),
19
+ /**
20
+ * A row whose key this version cannot parse is **not** silently treated as some default window — it throws,
21
+ * naming the value. The alternative is enforcing an allowance over a span nobody configured, in whichever
22
+ * direction the fallback happened to point.
23
+ */
24
+ window: (() => {
25
+ const parsed = parseWindowKey(r.window_key);
26
+ if (parsed === null)
27
+ throw new Error(`usage_limits row has an unrecognised window key ${JSON.stringify(r.window_key)}`);
28
+ return parsed;
29
+ })(),
30
+ // Spread conditionally rather than assigning `undefined`, so an absent limit is absent from the object and
31
+ // `"costMinorUnits" in limit` answers "is this bounded" without a second convention.
32
+ ...(num(r.cost_minor_units) === undefined ? {} : { costMinorUnits: num(r.cost_minor_units) }),
33
+ ...(num(r.input_tokens) === undefined ? {} : { inputTokens: num(r.input_tokens) }),
34
+ ...(num(r.output_tokens) === undefined ? {} : { outputTokens: num(r.output_tokens) }),
35
+ ...(r.warn_at === null ? {} : { warnAt: r.warn_at }),
36
+ updatedAt: iso(r.updated_at),
37
+ ...(r.updated_by === null ? {} : { updatedBy: r.updated_by }),
38
+ });
39
+ const COLUMNS = `tenant_id, principal_id, model_id, window_key, cost_minor_units, input_tokens,
40
+ output_tokens, warn_at, updated_at, updated_by`;
41
+ export const createPostgresUsageLimitStore = (sql) => ({
42
+ async put({ tenantId, limit }) {
43
+ /**
44
+ * One statement, one conflict target — #182.
45
+ *
46
+ * This branched on grain, because uniqueness was two *partial* unique indexes (a NULL cannot participate in
47
+ * a normal unique constraint) and `ON CONFLICT` has to name the matching one. Adding the model dimension
48
+ * would have made that four indexes and a 2x2 branch — four chances to name the wrong one, and naming the
49
+ * wrong one is how #177's `error: dup` reached a user.
50
+ *
51
+ * Migration 0025 replaced them with a single expression index over `COALESCE(col, '')`, so both nullable
52
+ * dimensions normalise into one key. The `::text` casts are load-bearing: the target has to match the
53
+ * index's expression exactly, or Postgres cannot find it and the upsert fails at runtime.
54
+ */
55
+ const rows = await sql.query(`INSERT INTO usage_limits
56
+ (tenant_id, principal_id, model_id, window_key, cost_minor_units, input_tokens, output_tokens, warn_at,
57
+ updated_at, updated_by)
58
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, now(), $9)
59
+ ON CONFLICT (tenant_id, COALESCE(principal_id, ''::text), COALESCE(model_id, ''::text), window_key)
60
+ DO UPDATE SET
61
+ cost_minor_units = EXCLUDED.cost_minor_units,
62
+ input_tokens = EXCLUDED.input_tokens,
63
+ output_tokens = EXCLUDED.output_tokens,
64
+ warn_at = EXCLUDED.warn_at,
65
+ updated_at = EXCLUDED.updated_at,
66
+ updated_by = EXCLUDED.updated_by
67
+ RETURNING ${COLUMNS}`, [
68
+ tenantId,
69
+ limit.principalId ?? null,
70
+ limit.modelId ?? null,
71
+ windowKey(limit.window),
72
+ // `?? null` and not `?? 0`: an omitted limit is unbounded. Writing 0 would refuse every run, which is
73
+ // the outage direction.
74
+ limit.costMinorUnits ?? null,
75
+ limit.inputTokens ?? null,
76
+ limit.outputTokens ?? null,
77
+ limit.warnAt ?? null,
78
+ limit.updatedBy ?? null,
79
+ ]);
80
+ return toLimit(rows[0]);
81
+ },
82
+ async resolve({ tenantId, principalId, modelId, window }) {
83
+ /**
84
+ * Most specific wins, decided by the database rather than by the caller.
85
+ *
86
+ * `ORDER BY principal_id NULLS LAST` puts the principal's own row first when it exists and falls back to the
87
+ * tenant default otherwise. One query, one round trip, and — the actual point — one implementation: "most
88
+ * specific wins" is a rule, and a rule implemented at two call sites is a rule with two behaviours.
89
+ */
90
+ const rows = await sql.query(`SELECT ${COLUMNS} FROM usage_limits
91
+ WHERE tenant_id = $1 AND window_key = $2
92
+ AND (principal_id IS NULL OR principal_id = $3::text)
93
+ -- IS NOT DISTINCT FROM, so asking for the unscoped limit finds the unscoped row, not nothing.
94
+ AND model_id IS NOT DISTINCT FROM $4::text
95
+ ORDER BY principal_id NULLS LAST
96
+ LIMIT 1`, [tenantId, windowKey(window), principalId ?? null, modelId ?? null]);
97
+ return rows[0] === undefined ? null : toLimit(rows[0]);
98
+ },
99
+ async applicable({ tenantId, principalId, modelId }) {
100
+ /**
101
+ * Every limit in force, one per `(window, model)` scope — #182.
102
+ *
103
+ * `DISTINCT ON` with the scope as its key and `principal_id NULLS LAST` in the ordering is the whole rule:
104
+ * for each scope Postgres keeps the first row, and the ordering puts the principal's own row first when it
105
+ * exists. Override within a scope, coexist across scopes, in one query rather than a read-then-choose that
106
+ * could see a limit change between the two halves.
107
+ *
108
+ * `model_id IS NULL OR model_id = $3` and *not* `IS NOT DISTINCT FROM`: an unscoped row applies to every
109
+ * model, so it must come back whatever the current model is. A model-scoped row applies only to its own
110
+ * model, and when `$3` is NULL that comparison is never true — which is the intended answer, because an
111
+ * unknown model cannot be checked against a per-model allowance.
112
+ */
113
+ const rows = await sql.query(`SELECT DISTINCT ON (COALESCE(model_id, ''), window_key) ${COLUMNS}
114
+ FROM usage_limits
115
+ WHERE tenant_id = $1
116
+ AND (principal_id IS NULL OR principal_id = $2::text)
117
+ AND (model_id IS NULL OR model_id = $3::text)
118
+ ORDER BY COALESCE(model_id, ''), window_key, principal_id NULLS LAST`, [tenantId, principalId ?? null, modelId ?? null]);
119
+ return rows.map(toLimit);
120
+ },
121
+ async list({ tenantId }) {
122
+ const rows = await sql.query(
123
+ // The tenant default first, then principals alphabetically — a stable order, so an admin screen does not
124
+ // reshuffle between refreshes.
125
+ `SELECT ${COLUMNS} FROM usage_limits WHERE tenant_id = $1
126
+ ORDER BY principal_id NULLS FIRST, model_id NULLS FIRST, window_key`, [tenantId]);
127
+ return rows.map(toLimit);
128
+ },
129
+ async remove({ tenantId, principalId, modelId, window }) {
130
+ // `IS NOT DISTINCT FROM`, so a NULL parameter removes the tenant default rather than matching nothing.
131
+ await sql.query(`DELETE FROM usage_limits
132
+ WHERE tenant_id = $1 AND window_key = $2 AND principal_id IS NOT DISTINCT FROM $3::text
133
+ AND model_id IS NOT DISTINCT FROM $4::text`, [tenantId, windowKey(window), principalId ?? null, modelId ?? null]);
134
+ },
135
+ });
136
+ //# sourceMappingURL=usage-limits.js.map
@@ -0,0 +1,15 @@
1
+ import type { IdempotencyStore } from "../../idempotency/index.js";
2
+ import type { UsageStore } from "../../persistence/index.js";
3
+ import type { SqlExecutor } from "./sql.js";
4
+ export declare const createPostgresUsageStore: (sql: SqlExecutor) => UsageStore;
5
+ /**
6
+ * The replay guard. `firstSeen` is `false` on a hit, matching the reference adapter and the type's own
7
+ * docstring — *"True on the first execution; false when the stored result is returned"* — because a
8
+ * stored value means the call already ran.
9
+ *
10
+ * No expiry column: `put` takes no TTL and the port has no prune method, so nothing could populate
11
+ * one, and an always-NULL `expires_at` would read as a retention policy that does not exist. Pruning
12
+ * is an operational query over `created_at`; see the open question on #100 for the policy.
13
+ */
14
+ export declare const createPostgresIdempotencyStore: (sql: SqlExecutor) => IdempotencyStore;
15
+ //# sourceMappingURL=usage.d.ts.map
@@ -0,0 +1,226 @@
1
+ import { usageDedupeKey } from "../../usage/index.js";
2
+ const iso = (v) => (v instanceof Date ? v.toISOString() : new Date(v).toISOString());
3
+ /** `bigint` and `numeric` come back as strings from node-postgres; `integer` as a number. */
4
+ const int = (v) => (v === null || v === undefined ? 0 : Number(v));
5
+ const json = (value) => (typeof value === "string" ? JSON.parse(value) : value);
6
+ const toEvent = (r) => ({
7
+ id: r.id,
8
+ tenantId: r.tenant_id,
9
+ // #175. Omitted when null: a record from before the column existed has an unknown principal, and that is a
10
+ // fact rather than something to fill in.
11
+ ...(r.principal_id === null || r.principal_id === undefined
12
+ ? {}
13
+ : { principalId: r.principal_id }),
14
+ runId: r.run_id,
15
+ ...(r.conversation_id === null ? {} : { conversationId: r.conversation_id }),
16
+ ...(r.step_id === null ? {} : { stepId: r.step_id }),
17
+ ...(r.tool_call_id === null ? {} : { toolCallId: r.tool_call_id }),
18
+ modelId: r.model_id,
19
+ inputTokens: int(r.input_tokens),
20
+ outputTokens: int(r.output_tokens),
21
+ cachedInputTokens: int(r.cached_input_tokens),
22
+ ...(r.reasoning_tokens === null ? {} : { reasoningTokens: int(r.reasoning_tokens) }),
23
+ // Null stays absent: "not counted" and "none" are different facts about a row (#185).
24
+ ...(r.image_count === null || r.image_count === undefined ? {} : { imageCount: int(r.image_count) }),
25
+ ...(r.audio_seconds === null || r.audio_seconds === undefined ? {} : { audioSeconds: int(r.audio_seconds) }),
26
+ costMinorUnits: int(r.cost_minor_units),
27
+ currency: r.currency,
28
+ occurredAt: iso(r.occurred_at),
29
+ });
30
+ const USAGE_COLUMNS = `id, tenant_id, principal_id, run_id, conversation_id, step_id, tool_call_id, model_id,
31
+ input_tokens, output_tokens, cached_input_tokens, reasoning_tokens,
32
+ image_count, audio_seconds,
33
+ cost_minor_units, currency, occurred_at`;
34
+ export const createPostgresUsageStore = (sql) => ({
35
+ async append({ tenantId, event }) {
36
+ // The conflict target is the dedupe key, not the id: a recovered run may re-record the same step
37
+ // under a fresh event id, and that must still be a no-op. Keying only on id would let it through.
38
+ await sql.query(`INSERT INTO usage_records
39
+ (tenant_id, id, dedupe_key, principal_id, run_id, conversation_id, step_id, tool_call_id, model_id,
40
+ input_tokens, output_tokens, cached_input_tokens, reasoning_tokens,
41
+ image_count, audio_seconds,
42
+ cost_minor_units, currency, occurred_at)
43
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18::timestamptz)
44
+ ON CONFLICT (tenant_id, dedupe_key) DO NOTHING`, [
45
+ tenantId,
46
+ event.id,
47
+ usageDedupeKey(event),
48
+ event.principalId ?? null,
49
+ event.runId,
50
+ event.conversationId ?? null,
51
+ event.stepId ?? null,
52
+ event.toolCallId ?? null,
53
+ event.modelId,
54
+ event.inputTokens,
55
+ event.outputTokens,
56
+ event.cachedInputTokens,
57
+ event.reasoningTokens ?? null,
58
+ event.imageCount ?? null,
59
+ event.audioSeconds ?? null,
60
+ event.costMinorUnits,
61
+ event.currency,
62
+ event.occurredAt,
63
+ ]);
64
+ },
65
+ async listByRun({ tenantId, runId, limit, cursor }) {
66
+ // Keyset paging on (occurred_at, id) — the same shape as messages (#96) and for the same reason:
67
+ // an OFFSET would skip or repeat rows when a concurrent append lands mid-page, and usage appends
68
+ // continuously while a run is executing.
69
+ const rows = await sql.query(`WITH anchor AS (
70
+ SELECT occurred_at, id FROM usage_records WHERE tenant_id = $1 AND id = $3
71
+ )
72
+ SELECT ${USAGE_COLUMNS}, occurred_at AS cursor_occurred_at
73
+ FROM usage_records
74
+ WHERE tenant_id = $1
75
+ AND run_id = $2
76
+ AND ($3::text IS NULL
77
+ OR (occurred_at, id) > ((SELECT occurred_at FROM anchor), (SELECT id FROM anchor)))
78
+ ORDER BY occurred_at, id
79
+ LIMIT $4`, [tenantId, runId, cursor ?? null, limit + 1]);
80
+ const hasMore = rows.length > limit;
81
+ const items = (hasMore ? rows.slice(0, limit) : rows).map(toEvent);
82
+ const last = items[items.length - 1];
83
+ const page = hasMore && last ? { items, nextCursor: last.id } : { items };
84
+ return page;
85
+ },
86
+ async breakdown({ tenantId, from, to, by, limit }) {
87
+ /**
88
+ * The grouping column is chosen from a closed union, never interpolated from user input — every arm is a
89
+ * literal in this file.
90
+ *
91
+ * `principal` is `COALESCE(principal_id, '')` for the same reason `conversation` is: rows written before the
92
+ * column existed have NULL, and a NULL group key would be dropped by the client rather than shown as
93
+ * "unattributed" — which is what it honestly is.
94
+ */
95
+ const column = by === "model"
96
+ ? "model_id"
97
+ : by === "principal"
98
+ ? "COALESCE(principal_id, '')"
99
+ : "COALESCE(conversation_id, '')";
100
+ const rows = await sql.query(`SELECT ${column} AS key,
101
+ COALESCE(SUM(input_tokens), 0) AS input_tokens,
102
+ COALESCE(SUM(output_tokens), 0) AS output_tokens,
103
+ COALESCE(SUM(cached_input_tokens), 0) AS cached_input_tokens,
104
+ COALESCE(SUM(COALESCE(reasoning_tokens, 0)), 0) AS reasoning_tokens,
105
+ COALESCE(SUM(cost_minor_units), 0) AS cost_minor_units,
106
+ COUNT(*) AS event_count
107
+ FROM usage_records
108
+ WHERE tenant_id = $1
109
+ -- Half-open, like every other range here, so adjacent periods tile without a boundary event
110
+ -- appearing in both.
111
+ AND occurred_at >= $2::timestamptz AND occurred_at < $3::timestamptz
112
+ GROUP BY 1
113
+ -- Largest cost first so a LIMIT drops what matters least; key breaks the tie so the order is stable
114
+ -- and a rendered breakdown does not reshuffle between refreshes.
115
+ ORDER BY cost_minor_units DESC, key
116
+ LIMIT $4`, [tenantId, from, to, limit]);
117
+ return rows.map((r) => ({
118
+ key: r.key,
119
+ totals: {
120
+ inputTokens: int(r.input_tokens),
121
+ outputTokens: int(r.output_tokens),
122
+ cachedInputTokens: int(r.cached_input_tokens),
123
+ reasoningTokens: int(r.reasoning_tokens),
124
+ costMinorUnits: int(r.cost_minor_units),
125
+ eventCount: int(r.event_count),
126
+ },
127
+ }));
128
+ },
129
+ async totalsBetween({ tenantId, from, to, principalId, modelId }) {
130
+ /**
131
+ * One query for the totals **and** the earliest timestamp — #181.
132
+ *
133
+ * Two queries could disagree: a record arriving between them would be in one and not the other, and the
134
+ * refusal message would then name a reset time derived from a set of records different to the total it
135
+ * refused on.
136
+ *
137
+ * Aggregated in the database, like `totals`, because this runs at admission on every message.
138
+ *
139
+ * The optional filters are `($n IS NULL OR col = $n)` rather than a built-up WHERE clause: the parameter
140
+ * list stays fixed, so there is one query plan and no string assembly anywhere near user input. Note the
141
+ * asymmetry that matters — an absent `principalId` means **every** principal, not the rows whose principal
142
+ * is NULL, which is why this is a guard on the parameter and not `col IS NULL`.
143
+ */
144
+ const rows = await sql.query(`SELECT COALESCE(SUM(input_tokens), 0) AS input_tokens,
145
+ COALESCE(SUM(output_tokens), 0) AS output_tokens,
146
+ COALESCE(SUM(cached_input_tokens), 0) AS cached_input_tokens,
147
+ COALESCE(SUM(COALESCE(reasoning_tokens, 0)), 0) AS reasoning_tokens,
148
+ COALESCE(SUM(cost_minor_units), 0) AS cost_minor_units,
149
+ COUNT(*) AS event_count,
150
+ -- Not COALESCEd: null is the answer when the window is empty, and a substituted date would be a
151
+ -- reset time for a window nothing was spent in.
152
+ MIN(occurred_at) AS earliest_at
153
+ FROM usage_records
154
+ WHERE tenant_id = $1
155
+ -- Half-open, so adjacent windows tile without a boundary event counting twice.
156
+ AND occurred_at >= $2::timestamptz AND occurred_at < $3::timestamptz
157
+ AND ($4::text IS NULL OR principal_id = $4::text)
158
+ AND ($5::text IS NULL OR model_id = $5::text)`, [tenantId, from, to, principalId ?? null, modelId ?? null]);
159
+ const row = rows[0];
160
+ const totals = {
161
+ inputTokens: int(row?.input_tokens),
162
+ outputTokens: int(row?.output_tokens),
163
+ cachedInputTokens: int(row?.cached_input_tokens),
164
+ reasoningTokens: int(row?.reasoning_tokens),
165
+ costMinorUnits: int(row?.cost_minor_units),
166
+ eventCount: int(row?.event_count),
167
+ };
168
+ const earliest = row?.earliest_at ?? null;
169
+ return {
170
+ totals,
171
+ // Normalised to the same ISO form every other timestamp in this codebase uses. `pg` hands back a `Date`
172
+ // for `timestamptz`, and a `Date` compared against an ISO string is a comparison that silently fails.
173
+ earliestAt: earliest === null ? null : new Date(earliest).toISOString(),
174
+ };
175
+ },
176
+ async totals({ tenantId, runId, conversationId }) {
177
+ // Aggregated in the database rather than by fetching every row: totals feed `reserve()`, which
178
+ // runs before each provider call, and a run can accumulate thousands of usage records.
179
+ const rows = await sql.query(`SELECT COALESCE(SUM(input_tokens), 0) AS input_tokens,
180
+ COALESCE(SUM(output_tokens), 0) AS output_tokens,
181
+ COALESCE(SUM(cached_input_tokens), 0) AS cached_input_tokens,
182
+ COALESCE(SUM(reasoning_tokens), 0) AS reasoning_tokens,
183
+ COALESCE(SUM(cost_minor_units), 0) AS cost_minor_units,
184
+ COUNT(*) AS event_count
185
+ FROM usage_records
186
+ WHERE tenant_id = $1
187
+ AND ($2::text IS NULL OR run_id = $2::text)
188
+ AND ($3::text IS NULL OR conversation_id = $3::text)`, [tenantId, runId ?? null, conversationId ?? null]);
189
+ const row = rows[0];
190
+ const totals = {
191
+ inputTokens: int(row?.input_tokens),
192
+ outputTokens: int(row?.output_tokens),
193
+ cachedInputTokens: int(row?.cached_input_tokens),
194
+ reasoningTokens: int(row?.reasoning_tokens),
195
+ costMinorUnits: int(row?.cost_minor_units),
196
+ eventCount: int(row?.event_count),
197
+ };
198
+ return totals;
199
+ },
200
+ });
201
+ /**
202
+ * The replay guard. `firstSeen` is `false` on a hit, matching the reference adapter and the type's own
203
+ * docstring — *"True on the first execution; false when the stored result is returned"* — because a
204
+ * stored value means the call already ran.
205
+ *
206
+ * No expiry column: `put` takes no TTL and the port has no prune method, so nothing could populate
207
+ * one, and an always-NULL `expires_at` would read as a retention policy that does not exist. Pruning
208
+ * is an operational query over `created_at`; see the open question on #100 for the policy.
209
+ */
210
+ export const createPostgresIdempotencyStore = (sql) => ({
211
+ async get({ tenantId, key }) {
212
+ const rows = await sql.query(`SELECT result FROM idempotency_keys WHERE tenant_id = $1 AND key = $2`, [tenantId, key]);
213
+ const row = rows[0];
214
+ if (!row)
215
+ return null;
216
+ return { key, firstSeen: false, result: json(row.result) };
217
+ },
218
+ async put({ tenantId, key, result }) {
219
+ // Last write wins on a repeat, matching the reference adapter's `Map.set`. The first result is
220
+ // what callers read back, because a caller that already got a stored result does not call `put`.
221
+ await sql.query(`INSERT INTO idempotency_keys (tenant_id, key, result, created_at)
222
+ VALUES ($1, $2, $3::jsonb, now())
223
+ ON CONFLICT (tenant_id, key) DO UPDATE SET result = excluded.result`, [tenantId, key, JSON.stringify(result ?? null)]);
224
+ },
225
+ });
226
+ //# sourceMappingURL=usage.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Redis adapters that are not the queue.
3
+ *
4
+ * `adapters/bullmq` owns the queue and its lock; this directory is for the other things Redis is the natural fit
5
+ * for. Kept separate so a deployment reading the tree can see that "we use Redis for the queue" and "we use Redis
6
+ * for realtime" are two decisions, not one.
7
+ */
8
+ export * from "./realtime.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Redis adapters that are not the queue.
3
+ *
4
+ * `adapters/bullmq` owns the queue and its lock; this directory is for the other things Redis is the natural fit
5
+ * for. Kept separate so a deployment reading the tree can see that "we use Redis for the queue" and "we use Redis
6
+ * for realtime" are two decisions, not one.
7
+ */
8
+ export * from "./realtime.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Redis pub/sub realtime — the cross-process half that was missing (#161).
3
+ *
4
+ * Everything needed for streaming already existed: a durable event log, resumable SSE frames, the projection
5
+ * fold, and both ports. What did not exist was a `RealtimePublisher`/`LiveEventSource` pair that works when the
6
+ * publisher and the subscriber are **different processes** — which they always are, since the whole point of the
7
+ * durable runtime is that the API host and the worker share only Postgres and the queue.
8
+ *
9
+ * The consequence was that no client ever saw a token while a run was in progress. The in-memory bus connects a
10
+ * publisher and a subscriber inside one process, and the worker CLI published to a hard-coded no-op; so the SSE
11
+ * endpoint replayed the durable log and then waited forever on a channel nothing wrote to. It looked like a
12
+ * working system with no streaming rather than a broken one.
13
+ *
14
+ * **Why pub/sub is the right primitive here, despite being at-most-once.** The durable log is the source of
15
+ * truth and the stream resumes from a sequence, so a dropped live message costs *latency*, not correctness — a
16
+ * reconnect fills the gap. The live channel only has to be fast. Reaching for a Redis Stream instead would buy
17
+ * replay this design already has, at the cost of trimming policy and consumer-group bookkeeping.
18
+ *
19
+ * Redis is already a required configuration value and already carries the queue, so this adds no dependency and
20
+ * no new operational surface.
21
+ */
22
+ import type { RealtimePublisher } from "../../core/events.js";
23
+ import type { LiveEventSource } from "../../runtime/streaming.js";
24
+ /**
25
+ * The publish side, narrowed to one method.
26
+ *
27
+ * An interface rather than an `ioredis` import so this file stays testable without a server and the package does
28
+ * not gain a hard dependency for a feature a deployment may not use — the same reasoning the Supabase adapter
29
+ * gives for its broadcaster.
30
+ */
31
+ export interface RedisPublishClient {
32
+ publish(channel: string, message: string): Promise<number> | number;
33
+ }
34
+ /**
35
+ * The subscribe side.
36
+ *
37
+ * **A subscriber connection cannot issue other commands**, which is why this is a separate interface from
38
+ * `RedisPublishClient` rather than one client doing both: in Redis, a connection in subscriber mode rejects
39
+ * everything except further subscribe/unsubscribe. A single shared client would work until the first publish
40
+ * from the same process and then fail confusingly.
41
+ */
42
+ export interface RedisSubscribeClient {
43
+ subscribe(channel: string): Promise<unknown>;
44
+ unsubscribe(channel: string): Promise<unknown>;
45
+ on(event: "message", listener: (channel: string, message: string) => void): unknown;
46
+ off?(event: "message", listener: (channel: string, message: string) => void): unknown;
47
+ }
48
+ /** Namespaced, so run events cannot collide with BullMQ's own keys or another app's channels. */
49
+ export declare const realtimeChannel: (channel: string) => string;
50
+ export declare const createRedisRealtimePublisher: (client: RedisPublishClient) => RealtimePublisher;
51
+ export type RedisLiveEventSourceOptions = {
52
+ /**
53
+ * How many events one subscriber may buffer before the oldest are dropped.
54
+ *
55
+ * A bound, because a subscriber that stops reading must not grow this process's memory — #144's `slow-consumer`
56
+ * mode is exactly that scenario. Dropping is safe: the durable log is the source of truth, so a client that
57
+ * fell behind reconnects and resumes from its sequence. An unbounded queue would trade a correctness-preserving
58
+ * gap for an out-of-memory kill.
59
+ */
60
+ readonly maxBuffered?: number;
61
+ /** Reported when a payload fails validation or the buffer overflows, so neither is silent. */
62
+ readonly onDropped?: (reason: "invalid-payload" | "buffer-overflow", channel: string) => void;
63
+ };
64
+ export declare const DEFAULT_MAX_BUFFERED = 512;
65
+ /**
66
+ * The receive side.
67
+ *
68
+ * The contract that matters is inherited from `LiveEventSource`: **buffering must begin at `subscribe()` time**,
69
+ * before the caller's first `next()`. `openRunEventStream` subscribes and *then* reads the durable log, and that
70
+ * ordering is the only thing stopping an event published during catch-up from being lost. So the Redis
71
+ * subscription and the listener are established synchronously inside `subscribe`, not lazily in the generator.
72
+ */
73
+ export declare const createRedisLiveEventSource: (client: RedisSubscribeClient, options?: RedisLiveEventSourceOptions) => LiveEventSource;
74
+ //# sourceMappingURL=realtime.d.ts.map
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Redis pub/sub realtime — the cross-process half that was missing (#161).
3
+ *
4
+ * Everything needed for streaming already existed: a durable event log, resumable SSE frames, the projection
5
+ * fold, and both ports. What did not exist was a `RealtimePublisher`/`LiveEventSource` pair that works when the
6
+ * publisher and the subscriber are **different processes** — which they always are, since the whole point of the
7
+ * durable runtime is that the API host and the worker share only Postgres and the queue.
8
+ *
9
+ * The consequence was that no client ever saw a token while a run was in progress. The in-memory bus connects a
10
+ * publisher and a subscriber inside one process, and the worker CLI published to a hard-coded no-op; so the SSE
11
+ * endpoint replayed the durable log and then waited forever on a channel nothing wrote to. It looked like a
12
+ * working system with no streaming rather than a broken one.
13
+ *
14
+ * **Why pub/sub is the right primitive here, despite being at-most-once.** The durable log is the source of
15
+ * truth and the stream resumes from a sequence, so a dropped live message costs *latency*, not correctness — a
16
+ * reconnect fills the gap. The live channel only has to be fast. Reaching for a Redis Stream instead would buy
17
+ * replay this design already has, at the cost of trimming policy and consumer-group bookkeeping.
18
+ *
19
+ * Redis is already a required configuration value and already carries the queue, so this adds no dependency and
20
+ * no new operational surface.
21
+ */
22
+ import { parseRunEvent } from "../../core/validation.js";
23
+ /** Namespaced, so run events cannot collide with BullMQ's own keys or another app's channels. */
24
+ export const realtimeChannel = (channel) => `agentkit:events:${channel}`;
25
+ export const createRedisRealtimePublisher = (client) => ({
26
+ async publish(channel, event) {
27
+ // Failure is deliberately *not* swallowed here. The worker decides what a failed publish means; silently
28
+ // dropping it inside the adapter would make a misconfigured Redis indistinguishable from a quiet run, which
29
+ // is precisely the bug this file exists to fix.
30
+ await client.publish(realtimeChannel(channel), JSON.stringify(event));
31
+ },
32
+ });
33
+ export const DEFAULT_MAX_BUFFERED = 512;
34
+ /**
35
+ * The receive side.
36
+ *
37
+ * The contract that matters is inherited from `LiveEventSource`: **buffering must begin at `subscribe()` time**,
38
+ * before the caller's first `next()`. `openRunEventStream` subscribes and *then* reads the durable log, and that
39
+ * ordering is the only thing stopping an event published during catch-up from being lost. So the Redis
40
+ * subscription and the listener are established synchronously inside `subscribe`, not lazily in the generator.
41
+ */
42
+ export const createRedisLiveEventSource = (client, options = {}) => {
43
+ const maxBuffered = options.maxBuffered ?? DEFAULT_MAX_BUFFERED;
44
+ return {
45
+ subscribe(channel) {
46
+ const topic = realtimeChannel(channel);
47
+ const queue = [];
48
+ let wake = null;
49
+ let closed = false;
50
+ const listener = (incoming, message) => {
51
+ if (incoming !== topic || closed)
52
+ return;
53
+ let event;
54
+ try {
55
+ // A pub/sub payload is network input. `openRunEventStream` feeds whatever the live source yields
56
+ // straight into the projection fold, so an unvalidated message would put corrupt — or attacker-shaped —
57
+ // data into a run's state. Parsed and dropped on failure rather than trusted for arriving on the right
58
+ // channel, exactly as the Supabase adapter does.
59
+ event = parseRunEvent(JSON.parse(message));
60
+ }
61
+ catch {
62
+ options.onDropped?.("invalid-payload", channel);
63
+ return;
64
+ }
65
+ if (queue.length >= maxBuffered) {
66
+ // Oldest first: the newest events are the ones a live view needs, and a client that fell behind is
67
+ // going to reconnect and replay from its sequence anyway.
68
+ queue.shift();
69
+ options.onDropped?.("buffer-overflow", channel);
70
+ }
71
+ queue.push(event);
72
+ wake?.();
73
+ };
74
+ // Established here, synchronously — see the note above. `subscribe` is awaited inside the generator's first
75
+ // pull, but the *listener* is attached now, so a message arriving between this line and the first `next()`
76
+ // is queued rather than missed.
77
+ client.on("message", listener);
78
+ const subscribed = Promise.resolve(client.subscribe(topic)).catch(() => undefined);
79
+ return {
80
+ async *[Symbol.asyncIterator]() {
81
+ await subscribed;
82
+ try {
83
+ for (;;) {
84
+ while (queue.length > 0) {
85
+ const event = queue.shift();
86
+ if (event !== undefined)
87
+ yield event;
88
+ }
89
+ if (closed)
90
+ return;
91
+ await new Promise((resolve) => {
92
+ wake = () => {
93
+ wake = null;
94
+ resolve();
95
+ };
96
+ });
97
+ }
98
+ }
99
+ finally {
100
+ // Unsubscribing on *any* exit — return, throw, or the consumer breaking out of its loop — because a
101
+ // leaked subscription is a listener that keeps buffering for a client that has gone. One per abandoned
102
+ // SSE connection is how a host runs out of memory a day later.
103
+ closed = true;
104
+ client.off?.("message", listener);
105
+ await Promise.resolve(client.unsubscribe(topic)).catch(() => undefined);
106
+ }
107
+ },
108
+ };
109
+ },
110
+ };
111
+ };
112
+ //# sourceMappingURL=realtime.js.map