@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,510 @@
1
+ /**
2
+ * Rollup buckets and quota enforcement (#139).
3
+ *
4
+ * Phase 5 gave usage a recording hook and #100 made it durable. Nothing aggregated it and nothing enforced a
5
+ * limit, so one customer's consumption was unbounded.
6
+ *
7
+ * Four decisions carry this module.
8
+ *
9
+ * **Buckets are identified by their start, truncated to the period.** Two writers asking "which bucket does T
10
+ * belong to" must agree, and they do because truncation is a pure function of T rather than a range someone
11
+ * chooses.
12
+ *
13
+ * **The quota check happens at admission, before any provider call.** AC-2's wording is "before work starts",
14
+ * and the reason is what the alternative costs: a limit enforced mid-run leaves a half-written answer, a
15
+ * partial charge, and a user who has to guess whether to retry. Refusing admission is a complete outcome.
16
+ *
17
+ * **The warning fires below the limit, not at it.** A customer told at 100% is told when work is already
18
+ * failing. The threshold is a fraction so it scales with the limit rather than being a constant that is
19
+ * meaningless at one plan size and useless at another.
20
+ *
21
+ * **Enforcement reads a rollup, not the ledger.** Admission is on the hot path of every message; a check that
22
+ * scanned raw events would make the platform slower in exact proportion to how much it had been used.
23
+ */
24
+ import { AgentPlatformError } from "../core/errors.js";
25
+ import { windowKey } from "../persistence/index.js";
26
+ /** Zero, as a total. Named because "no usage" appears in several places and an object literal invites drift. */
27
+ export const NO_USAGE = {
28
+ inputTokens: 0,
29
+ outputTokens: 0,
30
+ cachedInputTokens: 0,
31
+ reasoningTokens: 0,
32
+ costMinorUnits: 0,
33
+ eventCount: 0,
34
+ };
35
+ /**
36
+ * The instant a period's bucket opens, for a given moment.
37
+ *
38
+ * Truncation in UTC, deliberately. A tenant-local day would make a bucket's identity depend on a timezone
39
+ * setting that can change, and a rollup already written under the old offset would silently belong to a
40
+ * different day than one written after — so "yesterday" would double-count an hour or lose one. Presenting
41
+ * totals in local time is a display concern; *storing* them in one is a correctness bug.
42
+ */
43
+ export const bucketStartFor = (period, at) => {
44
+ const d = new Date(at);
45
+ if (Number.isNaN(d.getTime()))
46
+ throw new AgentPlatformError({
47
+ code: "invalid_input",
48
+ message: `not a timestamp: ${JSON.stringify(at)}`,
49
+ retryable: false,
50
+ });
51
+ /**
52
+ * Week and month are calendar truncations, not fixed spans — #175.
53
+ *
54
+ * A month is 28 to 31 days and a week crosses month boundaries, so neither can be expressed as a multiple of
55
+ * milliseconds from an epoch. Getting that wrong drifts: buckets that start mid-day, and a "month" that slowly
56
+ * detaches from the calendar.
57
+ *
58
+ * The week starts **Monday**, per ISO 8601. `getUTCDay()` returns 0 for Sunday, so the offset is
59
+ * `(day + 6) % 7` — the arithmetic that turns a Sunday-based index into a Monday-based one, and the reason this
60
+ * is written out rather than inlined.
61
+ */
62
+ if (period === "month") {
63
+ return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1)).toISOString();
64
+ }
65
+ if (period === "week") {
66
+ const mondayOffset = (d.getUTCDay() + 6) % 7;
67
+ return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - mondayOffset)).toISOString();
68
+ }
69
+ const truncated = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), period === "hour" ? d.getUTCHours() : 0));
70
+ return truncated.toISOString();
71
+ };
72
+ /** The bucket after this one. For tiling a range without arithmetic at the call site. */
73
+ export const nextBucket = (period, bucketStart) => {
74
+ const d = new Date(bucketStart);
75
+ /**
76
+ * A month advances by **calendar** month, not by 30 days — #175.
77
+ *
78
+ * `Date.UTC(y, m + 1, 1)` handles the December rollover and the varying length without a special case, where
79
+ * adding a fixed span would put February's next bucket on the 2nd or 3rd of March and every subsequent bucket
80
+ * further adrift.
81
+ */
82
+ if (period === "month") {
83
+ return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1)).toISOString();
84
+ }
85
+ // A week *is* exactly seven days, and unlike a month it has no calendar irregularity to respect — DST does not
86
+ // exist in UTC, which is why the buckets are stored in UTC in the first place.
87
+ const ms = period === "hour" ? 3_600_000 : period === "week" ? 7 * 86_400_000 : 86_400_000;
88
+ return new Date(d.getTime() + ms).toISOString();
89
+ };
90
+ /** Every bucket start covering `[from, to)`, in order. */
91
+ export const bucketsBetween = (period, from, to) => {
92
+ const out = [];
93
+ let cursor = bucketStartFor(period, from);
94
+ const end = new Date(to).getTime();
95
+ // Bounded so a bad range cannot loop forever: a year of hours is the most anyone charts at that resolution,
96
+ // and a caller wanting more is asking the wrong question of the wrong period.
97
+ for (let guard = 0; guard < 9000 && new Date(cursor).getTime() < end; guard += 1) {
98
+ out.push(cursor);
99
+ cursor = nextBucket(period, cursor);
100
+ }
101
+ return out;
102
+ };
103
+ /**
104
+ * What a tenant may consume in a period.
105
+ *
106
+ * Every field optional, and an omitted field is *unbounded* rather than zero. That direction is deliberate: a
107
+ * misconfigured quota that blocks everything is an outage, and a misconfigured quota that blocks nothing is a
108
+ * bill — and the bill is visible in the rollups this module also provides, whereas the outage is only visible
109
+ * to the customer it is happening to.
110
+ */
111
+ /** How the window reads in a sentence: "your 5,000 spend limit for **the day** / **any 5 hours**". */
112
+ export const describeWindow = (window) => window.kind === "calendar"
113
+ ? `the ${window.period}`
114
+ : window.minutes % 60 === 0
115
+ ? `any ${window.minutes / 60} hour${window.minutes === 60 ? "" : "s"}`
116
+ : `any ${window.minutes} minutes`;
117
+ export const DEFAULT_WARN_AT = 0.8;
118
+ /** Which limit was hit. Separate values because the sentence a user reads differs. */
119
+ export const QUOTA_DIMENSIONS = ["cost", "input-tokens", "output-tokens"];
120
+ const DIMENSION_LABEL = {
121
+ cost: "spend",
122
+ "input-tokens": "input tokens",
123
+ "output-tokens": "output tokens",
124
+ };
125
+ export const createQuotaGuard = (deps) => {
126
+ const clock = deps.clock ?? (() => new Date().toISOString());
127
+ const log = deps.log ?? (() => { });
128
+ /**
129
+ * Who the message is about — #175.
130
+ *
131
+ * Every message said "This workspace", which was true while every limit was a tenant's and became a lie the
132
+ * moment one could belong to a person: someone refused for their own overspend was told the workspace was out,
133
+ * so the obvious next step is asking a colleague to stop working. The sentence a person reads has to name the
134
+ * thing that actually ran out.
135
+ */
136
+ const subject = (limits) => (limits.principalId === undefined ? "This workspace" : "You");
137
+ const verb = (limits) => (limits.principalId === undefined ? "has" : "have");
138
+ const possessive = (limits) => (limits.principalId === undefined ? "its" : "your");
139
+ /**
140
+ * " on claude-opus-5", or nothing at all — #182.
141
+ *
142
+ * Part of the sentence rather than appended after it, so the limit reads as being *for that model* instead of
143
+ * as a general limit with a note. Somebody whose Opus allowance is spent can still work on a cheaper model,
144
+ * and a message that does not say which model turns a narrow limit into an apparent outage.
145
+ */
146
+ const scopeOf = (limits) => limits.modelId === undefined ? "" : ` on ${limits.modelId}`;
147
+ /**
148
+ * Usage for the window, and when there will be room again — #181.
149
+ *
150
+ * One function for both kinds, because everything downstream (the checks, the refusal, the warnings) must not
151
+ * care which it got. The two arms differ in exactly the way the two windows differ:
152
+ *
153
+ * - **Calendar** reads the rollup. Admission is on the hot path of every message, and a check that scanned raw
154
+ * events would make the platform slower in proportion to how much it had been used. The reset is arithmetic:
155
+ * the next bucket boundary.
156
+ * - **Rolling** reads the ledger over `[at - minutes, at)`, because there is no bucket to read. The scan is
157
+ * bounded by the window, not by history. Nothing "resets", so the sentence says when the oldest record in
158
+ * the window ages out — the soonest anything changes — and never promises a clean slate.
159
+ *
160
+ * A rolling window with nothing in it still has to answer: `earliestAt` is null then, and the sentence is
161
+ * omitted rather than invented. That case is reachable — a limit of zero refuses on an empty window.
162
+ */
163
+ const read = async (context, limits, at) => {
164
+ /**
165
+ * A **model-scoped** limit always reads the ledger, whichever kind of window it has — #182.
166
+ *
167
+ * The rollups have no model dimension, and adding one would multiply their row count by the number of models
168
+ * a tenant uses to serve a check that a bounded index scan already answers — the same trade `breakdown`
169
+ * documents. For a calendar window the interval is exactly `[bucketStart, nextBucket)`, so the number is
170
+ * exact either way; only the source differs.
171
+ */
172
+ if (limits.window.kind === "calendar" && limits.modelId === undefined) {
173
+ const bucketStart = bucketStartFor(limits.window.period, at);
174
+ const rollup = await deps.rollups.get({
175
+ tenantId: context.tenantId,
176
+ period: limits.window.period,
177
+ bucketStart,
178
+ // The grain the limit is expressed at — #175. A per-person limit must read that person's bucket, or the
179
+ // first busy colleague exhausts an allowance the refused person has not touched.
180
+ ...(limits.principalId === undefined ? {} : { principalId: limits.principalId }),
181
+ });
182
+ const resetsAt = nextBucket(limits.window.period, bucketStart);
183
+ return { usage: rollup ?? NO_USAGE, relief: { sentence: `It resets at ${resetsAt}.`, at: resetsAt } };
184
+ }
185
+ // Everything else needs the ledger, which the rollup store cannot give — hence this dependency, and hence
186
+ // it being required only once such a limit is actually configured.
187
+ if (deps.usage === undefined)
188
+ throw new Error("this quota window needs a UsageStore: rollups are keyed on calendar buckets with no model dimension, " +
189
+ "so a rolling window or a per-model limit cannot be answered from them. Pass `usage` to " +
190
+ "createQuotaGuard, or configure an unscoped calendar limit.");
191
+ // A calendar window scoped to a model still has calendar bounds; only a rolling one is measured back from
192
+ // now. Computing the bounds here keeps the two cases one code path with one set of filters.
193
+ const bounds = limits.window.kind === "calendar"
194
+ ? (() => {
195
+ const bucketStart = bucketStartFor(limits.window.period, at);
196
+ const resetsAt = nextBucket(limits.window.period, bucketStart);
197
+ return { from: bucketStart, to: at, calendarResetsAt: resetsAt };
198
+ })()
199
+ : {
200
+ from: new Date(new Date(at).getTime() - limits.window.minutes * 60_000).toISOString(),
201
+ to: at,
202
+ calendarResetsAt: undefined,
203
+ };
204
+ const { totals, earliestAt } = await deps.usage.totalsBetween({
205
+ tenantId: context.tenantId,
206
+ from: bounds.from,
207
+ to: bounds.to,
208
+ ...(limits.principalId === undefined ? {} : { principalId: limits.principalId }),
209
+ ...(limits.modelId === undefined ? {} : { modelId: limits.modelId }),
210
+ });
211
+ // A calendar window does reset, even when its usage came from the ledger — so it says so, and says the true
212
+ // boundary rather than the sliding-window sentence.
213
+ if (bounds.calendarResetsAt !== undefined)
214
+ return {
215
+ usage: totals,
216
+ relief: { sentence: `It resets at ${bounds.calendarResetsAt}.`, at: bounds.calendarResetsAt },
217
+ };
218
+ if (earliestAt === null)
219
+ // No relief time to give, and none invented. `at` as the retry target is the honest answer: there is
220
+ // nothing to wait for, so anything that changes must be the limit itself.
221
+ return { usage: totals, relief: { sentence: "", at } };
222
+ const relievesAt = new Date(new Date(earliestAt).getTime() + (limits.window.kind === "rolling" ? limits.window.minutes : 0) * 60_000).toISOString();
223
+ return {
224
+ usage: totals,
225
+ relief: {
226
+ // Deliberately not "it resets": a sliding window frees up gradually, and the oldest record leaving is
227
+ // the first moment any of it does. Saying "resets" would promise the whole allowance back.
228
+ sentence: `The oldest of it falls outside the window at ${relievesAt}.`,
229
+ at: relievesAt,
230
+ },
231
+ };
232
+ };
233
+ return {
234
+ /**
235
+ * Decide whether a run may start — AC-2.
236
+ *
237
+ * Reads the current period's rollup, not the ledger: admission is on the hot path of every message, and a
238
+ * check that scanned raw events would make the platform slower in proportion to how much it had been used.
239
+ */
240
+ // `about`, not `subject` — `subject()` below is the helper that decides "You" versus "This workspace",
241
+ // and shadowing it here made every refusal message try to call a plain object.
242
+ async admit(context, about = {}) {
243
+ const at = about.at ?? clock();
244
+ const applicable = await deps.resolveLimits(context, about);
245
+ // Nothing configured is unbounded, not zero. A misconfigured quota that blocks everything is an outage;
246
+ // one that blocks nothing is a bill, and the bill is visible in these very rollups.
247
+ if (applicable.length === 0)
248
+ return { admitted: true, usage: NO_USAGE, warnings: [] };
249
+ /**
250
+ * **Every** applicable limit is checked, not the most specific one — #182.
251
+ *
252
+ * This resolved a single limit, which meant a workspace-wide cap on an expensive model was silently
253
+ * ignored for anybody who also had a personal overall limit: the personal one was "more specific", so the
254
+ * model cap was never read. But they are not competing answers to one question — they are two allowances,
255
+ * and both bind. That is what a limit means everywhere it is used in practice.
256
+ *
257
+ * Ordered shortest-span first by the resolver, so the limit a person is refused by is the one that stops
258
+ * them soonest, which is also the one whose reset time is nearest and therefore most useful to hear.
259
+ */
260
+ const evaluated = [];
261
+ for (const limits of applicable) {
262
+ const { usage, relief } = await read(context, limits, at);
263
+ evaluated.push({
264
+ limits,
265
+ usage,
266
+ relief,
267
+ checks: [
268
+ { dimension: "cost", ...(limits.costMinorUnits === undefined ? {} : { limit: limits.costMinorUnits }), used: usage.costMinorUnits },
269
+ { dimension: "input-tokens", ...(limits.inputTokens === undefined ? {} : { limit: limits.inputTokens }), used: usage.inputTokens },
270
+ { dimension: "output-tokens", ...(limits.outputTokens === undefined ? {} : { limit: limits.outputTokens }), used: usage.outputTokens },
271
+ ],
272
+ });
273
+ }
274
+ // Refusals first, across every limit and every dimension, before any warning is emitted. Warning and then
275
+ // refusing would tell somebody they are approaching a limit they have already passed.
276
+ for (const { limits, relief, checks } of evaluated) {
277
+ for (const check of checks) {
278
+ if (check.limit === undefined)
279
+ continue;
280
+ if (check.used >= check.limit) {
281
+ const refusal = {
282
+ admitted: false,
283
+ dimension: check.dimension,
284
+ limit: check.limit,
285
+ used: check.used,
286
+ // The model, when the limit has one. "You have run out" reads as an account-wide stop, and
287
+ // somebody whose Opus allowance is spent can still work on a cheaper model — so not saying which
288
+ // model turns a small limit into an apparent outage.
289
+ ...(limits.modelId === undefined ? {} : { modelId: limits.modelId }),
290
+ // Actionable: names the dimension, the figure, the limit and when there will be room again.
291
+ // "Quota exceeded" leaves a user with nothing to do.
292
+ message: `${subject(limits)} ${verb(limits)} used ${check.used} of ${possessive(limits)} ${check.limit} ` +
293
+ `${DIMENSION_LABEL[check.dimension]} limit${scopeOf(limits)} for ${describeWindow(limits.window)}. ` +
294
+ `${relief.sentence}`,
295
+ retryAfter: relief.at,
296
+ };
297
+ try {
298
+ await deps.observer?.onRefusal?.(context, refusal);
299
+ }
300
+ catch (error) {
301
+ // A refusal must not depend on an observer succeeding: the point is to stop work, and a broken
302
+ // notification is not a reason to let it through.
303
+ log("quota refusal observer failed", { error });
304
+ }
305
+ return refusal;
306
+ }
307
+ }
308
+ }
309
+ const warnings = [];
310
+ for (const { limits, checks } of evaluated) {
311
+ const warnAt = limits.warnAt ?? DEFAULT_WARN_AT;
312
+ for (const check of checks) {
313
+ if (check.limit === undefined || check.limit === 0)
314
+ continue;
315
+ const fraction = check.used / check.limit;
316
+ if (fraction < warnAt)
317
+ continue;
318
+ warnings.push({
319
+ dimension: check.dimension,
320
+ limit: check.limit,
321
+ used: check.used,
322
+ fraction,
323
+ ...(limits.modelId === undefined ? {} : { modelId: limits.modelId }),
324
+ message: `${subject(limits)} ${verb(limits)} used ${Math.round(fraction * 100)}% of ${possessive(limits)} ` +
325
+ `${DIMENSION_LABEL[check.dimension]} limit${scopeOf(limits)} for ${describeWindow(limits.window)}.`,
326
+ });
327
+ }
328
+ }
329
+ for (const warning of warnings) {
330
+ try {
331
+ await deps.observer?.onWarning(context, warning);
332
+ }
333
+ catch (error) {
334
+ // Logged, not thrown. A failed warning must not refuse a run that is inside its limit — that would
335
+ // turn a notification outage into a service outage.
336
+ log("quota warning observer failed", { error });
337
+ }
338
+ }
339
+ /**
340
+ * The usage reported alongside an admission is the **first** limit's, which is the shortest span.
341
+ *
342
+ * There is no single "usage" once several limits apply, and inventing a sum across windows would be a
343
+ * number that means nothing. The shortest span is the one a caller rendering a single figure wants, and
344
+ * `explain` gives all of them to a caller that wants more.
345
+ */
346
+ return { admitted: true, usage: evaluated[0]?.usage ?? NO_USAGE, warnings };
347
+ },
348
+ /**
349
+ * The limits in force for this context — an empty list for unlimited.
350
+ *
351
+ * Exposed so a UI can render "you have used X of Y" without a second source for Y — a panel that took its
352
+ * limit from configuration while enforcement took it from here would eventually disagree, and the version a
353
+ * user sees would be the wrong one.
354
+ */
355
+ async limits(context, about = {}) {
356
+ return deps.resolveLimits(context, about);
357
+ },
358
+ /**
359
+ * Every limit with its usage and its reset — #183.
360
+ *
361
+ * A limit nobody can see is a limit that surprises people, and once several apply at once "how much have I
362
+ * got left" stops being answerable by reading one number. This is the same `read` the refusal path uses, so
363
+ * a panel cannot disagree with enforcement about either the figure or the reset time — the failure that a
364
+ * second implementation of "how full is it" always eventually produces.
365
+ *
366
+ * Ordered as the resolver ordered them, shortest span first, which puts the limit most likely to stop you at
367
+ * the top without the caller having to sort by anything.
368
+ */
369
+ async explain(context, about = {}) {
370
+ const at = about.at ?? clock();
371
+ const applicable = await deps.resolveLimits(context, about);
372
+ const explained = [];
373
+ for (const limits of applicable) {
374
+ const { usage, relief } = await read(context, limits, at);
375
+ explained.push({
376
+ window: describeWindow(limits.window),
377
+ ...(limits.modelId === undefined ? {} : { modelId: limits.modelId }),
378
+ // Whose allowance it is, so a personal limit is distinguishable from the workspace's without the
379
+ // caller re-deriving it from the presence of a field.
380
+ scope: limits.principalId === undefined ? "workspace" : "personal",
381
+ resetsAt: relief.at,
382
+ resetNote: relief.sentence,
383
+ dimensions: [
384
+ ["cost", limits.costMinorUnits, usage.costMinorUnits],
385
+ ["input-tokens", limits.inputTokens, usage.inputTokens],
386
+ ["output-tokens", limits.outputTokens, usage.outputTokens],
387
+ ]
388
+ // Only the bounded dimensions. An unbounded one has nothing to report, and rendering it as
389
+ // "0 of null" is how a panel starts looking broken.
390
+ .filter(([, limit]) => limit !== undefined)
391
+ .map(([dimension, limit, used]) => ({
392
+ dimension,
393
+ limit: limit,
394
+ used,
395
+ // Computed here, not by the caller: two implementations of a fraction eventually round differently
396
+ // and the bar disagrees with the number beside it.
397
+ fraction: limit === 0 ? 1 : used / limit,
398
+ })),
399
+ });
400
+ }
401
+ return explained;
402
+ },
403
+ /** Throws the refusal, for a caller that would rather not branch. Same decision, different ergonomics. */
404
+ async assertAdmitted(context, about = {}) {
405
+ const decision = await this.admit(context, about);
406
+ if (!decision.admitted)
407
+ throw new AgentPlatformError({
408
+ code: "budget_exceeded",
409
+ message: decision.message,
410
+ // Retryable: the limit resets. A caller that treats this as permanent would give up on a workspace
411
+ // that is fine again in an hour.
412
+ retryable: true,
413
+ /**
414
+ * **When** it resets — #175.
415
+ *
416
+ * "Retryable" without a time is not actionable: a caller can only guess, and an HTTP surface has no
417
+ * `retry-after` to send. A client reading zero, or defaulting to immediately, retries straight back
418
+ * into the same refusal.
419
+ *
420
+ * In `details` because that field is the redacted, user-safe context — and a bucket boundary is not a
421
+ * secret. The dimension and figures are here too, so a client can say *which* limit without parsing
422
+ * the sentence.
423
+ */
424
+ details: {
425
+ retryAfter: decision.retryAfter,
426
+ dimension: decision.dimension,
427
+ limit: decision.limit,
428
+ used: decision.used,
429
+ },
430
+ });
431
+ return decision;
432
+ },
433
+ };
434
+ };
435
+ /**
436
+ * `resolveLimits` backed by the admin-configured store — #175.
437
+ *
438
+ * The guard already took `resolveLimits` as a function so limits could change without a redeploy. What was
439
+ * missing was anything to resolve them *from*: every deployment had to hardcode them, which is not a
440
+ * configuration.
441
+ *
442
+ * **Per-person first, tenant default second, unbounded last.** The store decides which row applies; this decides
443
+ * what to do when a person has no override — and it deliberately does *not* fall back to checking the tenant
444
+ * default against the person's own usage. That would compare a tenant-sized allowance to one person's spend, so
445
+ * nobody would ever hit it and the limit would silently do nothing.
446
+ *
447
+ * So the resolved limit carries the grain it was configured at, and the guard reads the matching rollup. A limit
448
+ * and the usage it is compared against have to be the same shape, and this is the one place that can guarantee
449
+ * it.
450
+ */
451
+ export const createStoredLimitResolver = (deps) => {
452
+ return async (context, about = {}) => {
453
+ /**
454
+ * Every applicable limit, from the store, shortest span first — #181, #182.
455
+ *
456
+ * Two things this deliberately does not do. It does not walk a hardcoded list of periods: that was how a
457
+ * stored `rolling:300` could be read back from the API and never enforced, because the resolver never asked
458
+ * for it. And it does not pick one: a person subject to a five-hour cap, a monthly cap and an Opus cap is
459
+ * subject to all three, and choosing the "most specific" left the others configured and unenforced.
460
+ *
461
+ * `applicable` does the override-within-a-scope selection in the store, where the rule has one
462
+ * implementation per adapter and conformance holds them to the same behaviour.
463
+ */
464
+ const records = await deps.limits.applicable({
465
+ tenantId: context.tenantId,
466
+ ...(context.principalId === undefined ? {} : { principalId: context.principalId }),
467
+ // The model of the run being admitted. Absent means model-scoped limits cannot apply — see `applicable`.
468
+ ...(about.modelId === undefined ? {} : { modelId: about.modelId }),
469
+ });
470
+ return records
471
+ .map((record) => ({
472
+ window: record.window,
473
+ ...(record.principalId === undefined ? {} : { principalId: record.principalId }),
474
+ ...(record.modelId === undefined ? {} : { modelId: record.modelId }),
475
+ ...(record.costMinorUnits === undefined ? {} : { costMinorUnits: record.costMinorUnits }),
476
+ ...(record.inputTokens === undefined ? {} : { inputTokens: record.inputTokens }),
477
+ ...(record.outputTokens === undefined ? {} : { outputTokens: record.outputTokens }),
478
+ ...(record.warnAt === undefined ? {} : { warnAt: record.warnAt }),
479
+ }))
480
+ // Shortest span first, so the limit that stops someone soonest is the one they are told about, and its
481
+ // reset — the nearest one — is the one they can act on.
482
+ .sort((a, b) => spanMinutes(a.window) - spanMinutes(b.window));
483
+ };
484
+ };
485
+ /**
486
+ * A window's length in minutes, for **ordering only**.
487
+ *
488
+ * A month is 28 to 31 days, so this is approximate by construction — and that is fine for deciding which of two
489
+ * limits to check first, and would not be fine for deciding whether someone is over one. Nothing computes usage
490
+ * from this; `bucketStartFor` and `nextBucket` do the real calendar arithmetic.
491
+ */
492
+ const spanMinutes = (window) => window.kind === "rolling"
493
+ ? window.minutes
494
+ : window.period === "hour"
495
+ ? 60
496
+ : window.period === "day"
497
+ ? 1_440
498
+ : window.period === "week"
499
+ ? 10_080
500
+ : 43_200;
501
+ /**
502
+ * The order periods are considered in, shortest first.
503
+ *
504
+ * Shortest first because a shorter window is the tighter constraint in practice: someone with a monthly
505
+ * allowance who has burned it in a day is stopped by the daily limit a day earlier, and being stopped early is
506
+ * recoverable where a surprise at month end is not. It is a default, and a deployment that disagrees passes its
507
+ * own order.
508
+ */
509
+ export const PERIOD_PRECEDENCE = ["hour", "day", "week", "month"];
510
+ //# sourceMappingURL=quota.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Usage recorder — `docs/12-usage-and-accounting.md`.
3
+ *
4
+ * `record` appends a durable `UsageEvent` for each realized step (append-only; corrections are new
5
+ * compensating events, never edits). `reserve` is the pre-flight ceiling check: it computes the
6
+ * estimated cost from live pricing and refuses a call that would push the run past its cost or token
7
+ * ceiling, so a run fails clearly rather than overspending. Cost is always integer minor units with
8
+ * an explicit currency.
9
+ */
10
+ import type { ExecutionContext } from "../core/context.js";
11
+ import { AgentPlatformError } from "../core/errors.js";
12
+ import type { ModelRegistry } from "../models/index.js";
13
+ import type { UsageStore } from "../persistence/index.js";
14
+ import type { PricingResolver, TokenCounter, UsageCeiling, UsageRecorder } from "./index.js";
15
+ /** A crude but provider-agnostic default: ~4 characters per token. Replace with a real tokenizer. */
16
+ export declare const heuristicTokenCounter: TokenCounter;
17
+ /** Pricing resolver backed by the model registry — resolves rates from the catalog at call time. */
18
+ export declare const createRegistryPricingResolver: (registry: ModelRegistry) => PricingResolver;
19
+ export declare const createUsageRecorder: (config: {
20
+ readonly store: UsageStore;
21
+ readonly pricing: PricingResolver;
22
+ /** Per-run ceiling from the agent's `ExecutionLimits`. Undefined ⇒ the run is unbounded. */
23
+ readonly resolveCeiling?: (context: ExecutionContext) => Promise<UsageCeiling | undefined> | UsageCeiling | undefined;
24
+ readonly clock?: () => string;
25
+ readonly idFactory?: () => string;
26
+ }) => UsageRecorder;
27
+ /** Thrown by callers when `reserve()` denies a call, so the run fails with a clear budget error. */
28
+ export declare const budgetExceeded: (detail: string) => AgentPlatformError;
29
+ //# sourceMappingURL=recorder.d.ts.map
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Usage recorder — `docs/12-usage-and-accounting.md`.
3
+ *
4
+ * `record` appends a durable `UsageEvent` for each realized step (append-only; corrections are new
5
+ * compensating events, never edits). `reserve` is the pre-flight ceiling check: it computes the
6
+ * estimated cost from live pricing and refuses a call that would push the run past its cost or token
7
+ * ceiling, so a run fails clearly rather than overspending. Cost is always integer minor units with
8
+ * an explicit currency.
9
+ */
10
+ import { estimateTokens } from "../core/tokens.js";
11
+ import { AgentPlatformError } from "../core/errors.js";
12
+ // The pricing module, not the models barrel: the barrel re-exports `streaming.js`, which imports the AI
13
+ // SDK, and this recorder is reached from the in-memory adapters (#199).
14
+ import { computeModelCostMinorUnits } from "../models/pricing.js";
15
+ /** A crude but provider-agnostic default: ~4 characters per token. Replace with a real tokenizer. */
16
+ export const heuristicTokenCounter = {
17
+ estimate: (text) => estimateTokens(text),
18
+ };
19
+ /** Pricing resolver backed by the model registry — resolves rates from the catalog at call time. */
20
+ export const createRegistryPricingResolver = (registry) => ({
21
+ resolve: (modelId) => registry.list().find((m) => m.modelId === modelId)?.pricing ?? null,
22
+ });
23
+ /**
24
+ * A usage record id that survives a restart — #174.
25
+ *
26
+ * This was `usage-${counter += 1}` over a **module-level** counter, which is per-process. `usage_records` has
27
+ * `PRIMARY KEY (tenant_id, id)`, so restarting a worker reset the counter to 1 and the next record collided with
28
+ * the `usage-1` already in the table: `duplicate key value violates unique constraint "usage_records_pkey"`, and
29
+ * the run failed. Two workers in one deployment collided the same way without restarting at all.
30
+ *
31
+ * Invisible to every test, because a test starts with a fresh schema *and* a fresh module — the counter and the
32
+ * table are always in step. It only breaks against a database that outlives the process, which is the only kind
33
+ * a deployment has.
34
+ *
35
+ * The dedupe index on `(tenant_id, dedupe_key)` is the *intended* idempotency mechanism and it works: a recovered
36
+ * run re-recording a step it already logged is a no-op. That is a different thing from two distinct events
37
+ * colliding on a surrogate id, which is why this is fixed by making the id unique rather than by adding another
38
+ * `ON CONFLICT` — swallowing a PK collision would silently drop a real record, and the dropped one would be
39
+ * revenue.
40
+ */
41
+ const defaultIdFactory = () => `usage-${crypto.randomUUID()}`;
42
+ export const createUsageRecorder = (config) => {
43
+ const clock = config.clock ?? (() => new Date().toISOString());
44
+ const idFactory = config.idFactory ?? defaultIdFactory;
45
+ const estimatedCost = (estimate) => {
46
+ const pricing = config.pricing.resolve(estimate.modelId);
47
+ if (!pricing)
48
+ return 0; // unknown model → cannot price; ceiling check falls back to token limits
49
+ return computeModelCostMinorUnits(pricing, {
50
+ inputTokens: estimate.inputTokens,
51
+ outputTokens: estimate.maxOutputTokens,
52
+ });
53
+ };
54
+ return {
55
+ async record(context, event) {
56
+ // Identity comes from the trusted context, never from the event payload — tenant *and* principal (#175).
57
+ // A caller able to name either could bill someone else's budget.
58
+ await config.store.append({
59
+ tenantId: context.tenantId,
60
+ event: {
61
+ ...event,
62
+ id: idFactory(),
63
+ tenantId: context.tenantId,
64
+ principalId: context.principalId,
65
+ occurredAt: clock(),
66
+ },
67
+ });
68
+ },
69
+ async reserve(context, estimate) {
70
+ // Best-effort pre-flight: a read-then-decide, so two concurrent reserves can both pass and then
71
+ // both spend — the authoritative bound is the append-only ledger checked here plus the run's
72
+ // limits. If a model has no pricing, its estimated cost is 0; pair a cost ceiling with a token
73
+ // ceiling to bound unknown-priced models.
74
+ const id = idFactory();
75
+ const ceiling = await config.resolveCeiling?.(context);
76
+ if (!ceiling)
77
+ return { id, withinCeiling: true };
78
+ const runId = context.runId;
79
+ const spent = runId
80
+ ? await config.store.totals({ tenantId: context.tenantId, runId })
81
+ : { inputTokens: 0, outputTokens: 0, cachedInputTokens: 0, reasoningTokens: 0, costMinorUnits: 0, eventCount: 0 };
82
+ const overCost = ceiling.costMinorUnits !== undefined &&
83
+ spent.costMinorUnits + estimatedCost(estimate) > ceiling.costMinorUnits;
84
+ const overInput = ceiling.inputTokens !== undefined && spent.inputTokens + estimate.inputTokens > ceiling.inputTokens;
85
+ const overOutput = ceiling.outputTokens !== undefined && spent.outputTokens + estimate.maxOutputTokens > ceiling.outputTokens;
86
+ return { id, withinCeiling: !(overCost || overInput || overOutput) };
87
+ },
88
+ };
89
+ };
90
+ /** Thrown by callers when `reserve()` denies a call, so the run fails with a clear budget error. */
91
+ export const budgetExceeded = (detail) => new AgentPlatformError({
92
+ code: "budget_exceeded",
93
+ message: `Run would exceed its usage ceiling: ${detail}`,
94
+ retryable: false,
95
+ });
96
+ //# sourceMappingURL=recorder.js.map