@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,1654 @@
1
+ /**
2
+ * Storage and infrastructure ports — `docs/02-core-and-persistence.md`.
3
+ *
4
+ * Small interfaces rather than one database adapter. Every read and write receives
5
+ * `tenantId` explicitly: `findById(id)` is forbidden, `findById({ tenantId, id })` is
6
+ * the shape. Ports never import adapters.
7
+ *
8
+ * Method bodies are intentionally sparse at this stage — each port is filled in as its
9
+ * phase in `docs/08-migration-plan.md` lands, against the shared conformance suite.
10
+ */
11
+ import type { Page, PageRequest, TenantScope } from "../core/context.js";
12
+ import type { Message, QuestionAnswer } from "../core/content-parts.js";
13
+ import type { AgentManifest } from "../agents/index.js";
14
+ import type { PlatformError } from "../core/errors.js";
15
+ import type { AgentId, ApprovalGrantId, ArtifactId, ArtifactVersionId, BlobRef, ConversationId, FileId, InteractionId, MessageId, PrincipalId, RunId, TenantId } from "../core/ids.js";
16
+ import type { ApprovalDecision, ApprovalGrant, PendingApproval, PendingQuestion } from "../hitl/index.js";
17
+ import type { Run, RunCheckpoint, RunStatus } from "../runtime/index.js";
18
+ import type { UsageEvent } from "../usage/index.js";
19
+ import type { SkillCatalogEntry, SkillVersion } from "../skills/index.js";
20
+ export type Conversation = {
21
+ readonly id: ConversationId;
22
+ readonly tenantId: TenantId;
23
+ readonly title: string;
24
+ /** Optimistic-concurrency version; incremented on every write. */
25
+ readonly version: number;
26
+ readonly archivedAt?: string;
27
+ /** Set by soft-delete; `findById`/`list` hide soft-deleted rows. */
28
+ readonly deletedAt?: string;
29
+ readonly createdAt: string;
30
+ readonly updatedAt: string;
31
+ };
32
+ export type ConversationPatch = {
33
+ readonly title?: string;
34
+ /** `null` un-archives; a timestamp archives. */
35
+ readonly archivedAt?: string | null;
36
+ };
37
+ /**
38
+ * The exemplar store. **Every store follows this shape** (docs/02 mandatory method behavior):
39
+ * an explicit `{ tenantId }` on every call (bare `findById(id)` is a type error), cursor
40
+ * pagination on lists, `expectedVersion` optimistic concurrency on updates, and soft-delete
41
+ * semantics. Adapters are verified by the shared conformance harness.
42
+ */
43
+ export interface ConversationStore {
44
+ create(input: TenantScope & {
45
+ id: ConversationId;
46
+ title: string;
47
+ }): Promise<Conversation>;
48
+ findById(input: TenantScope & {
49
+ id: ConversationId;
50
+ }): Promise<Conversation | null>;
51
+ list(input: TenantScope & PageRequest): Promise<Page<Conversation>>;
52
+ update(input: TenantScope & {
53
+ id: ConversationId;
54
+ expectedVersion: number;
55
+ patch: ConversationPatch;
56
+ }): Promise<Conversation>;
57
+ softDelete(input: TenantScope & {
58
+ id: ConversationId;
59
+ }): Promise<void>;
60
+ }
61
+ /**
62
+ * Cross-run working memory for a thread (`docs/13-sessions-and-threads.md`). Bounded, versioned,
63
+ * written by the runtime/tools — never raw model output. **Frozen v1.**
64
+ */
65
+ export type SessionState = {
66
+ readonly conversationId: ConversationId;
67
+ readonly version: number;
68
+ readonly data: Readonly<Record<string, unknown>>;
69
+ readonly updatedAt: string;
70
+ };
71
+ /**
72
+ * The ceiling on session state, defined here rather than in an adapter (#97). It is a domain limit —
73
+ * session state is bounded working memory, not a document store — and every adapter must enforce the
74
+ * same one. It previously lived in `adapters/memory/sessions.ts`, which made two adapters agreeing on
75
+ * it a coincidence rather than a property.
76
+ */
77
+ export declare const DEFAULT_SESSION_STATE_MAX_BYTES: number;
78
+ export interface SessionStateStore {
79
+ get(input: TenantScope & {
80
+ conversationId: ConversationId;
81
+ }): Promise<SessionState | null>;
82
+ /** Optimistic concurrency: rejects when `expectedVersion` is stale. */
83
+ put(input: TenantScope & {
84
+ conversationId: ConversationId;
85
+ expectedVersion: number;
86
+ data: Readonly<Record<string, unknown>>;
87
+ }): Promise<SessionState>;
88
+ }
89
+ export type AgentVersionPolicy = "pinned" | "latest";
90
+ /**
91
+ * Binds a thread to the agent that owns it (`docs/13`). A resumed thread runs the same agent — and,
92
+ * when pinned, the same version — that produced its earlier turns, so continuation is deterministic.
93
+ * Kept as its own record rather than bloating the frozen `Conversation`.
94
+ */
95
+ export type ConversationBinding = {
96
+ readonly conversationId: ConversationId;
97
+ readonly agentId: AgentId;
98
+ readonly agentVersionPolicy: AgentVersionPolicy;
99
+ /** Recorded when the policy is `pinned`. */
100
+ readonly agentVersion?: number;
101
+ };
102
+ export interface ConversationBindingStore {
103
+ bind(input: TenantScope & ConversationBinding): Promise<void>;
104
+ get(input: TenantScope & {
105
+ conversationId: ConversationId;
106
+ }): Promise<ConversationBinding | null>;
107
+ }
108
+ /**
109
+ * Per-conversation run serialization (`docs/13` → Run ordering). At most one `Running` run per
110
+ * conversation; further runs queue FIFO by enqueue time so session-state and message order are
111
+ * deterministic. Backed by `DistributedLockStore` semantics in production; in-memory for tests.
112
+ */
113
+ export interface ConversationRunCoordinator {
114
+ /**
115
+ * Atomically claim the conversation for `runId`, or enqueue it FIFO if one is already active. This
116
+ * MUST be atomic (no claim→enqueue gap) so a run can never slip past into an idle-but-unclaimed
117
+ * slot and strand itself. The primary entry point for starting a run.
118
+ */
119
+ claimOrEnqueue(input: TenantScope & {
120
+ conversationId: ConversationId;
121
+ runId: RunId;
122
+ }): Promise<{
123
+ status: "started" | "queued";
124
+ position: number;
125
+ }>;
126
+ /**
127
+ * Atomically release `runId` (if it holds the slot) and promote the next queued run. MUST be atomic
128
+ * (no release→dequeue→claim gap) so two runs can never both become active. Returns the promoted run
129
+ * to dispatch, or null when the backlog is empty. The primary entry point on run terminal.
130
+ */
131
+ releaseAndPromote(input: TenantScope & {
132
+ conversationId: ConversationId;
133
+ runId: RunId;
134
+ }): Promise<RunId | null>;
135
+ active(input: TenantScope & {
136
+ conversationId: ConversationId;
137
+ }): Promise<RunId | null>;
138
+ depth(input: TenantScope & {
139
+ conversationId: ConversationId;
140
+ }): Promise<number>;
141
+ }
142
+ /** Compacted older history (`docs/13`). Recent turns stay verbatim; this is versioned. */
143
+ export type ThreadSummary = {
144
+ readonly conversationId: ConversationId;
145
+ readonly version: number;
146
+ readonly summary: string;
147
+ readonly coversUpToMessageId: MessageId;
148
+ readonly createdAt: string;
149
+ };
150
+ export interface ThreadSummaryStore {
151
+ latest(input: TenantScope & {
152
+ conversationId: ConversationId;
153
+ }): Promise<ThreadSummary | null>;
154
+ append(input: TenantScope & {
155
+ conversationId: ConversationId;
156
+ summary: string;
157
+ coversUpToMessageId: MessageId;
158
+ }): Promise<ThreadSummary>;
159
+ }
160
+ export type NewRun = {
161
+ readonly id: RunId;
162
+ /**
163
+ * The conversation this run belongs to, or **absent for a run that belongs to none** — #198.
164
+ *
165
+ * Required until now, which meant a triggered automation — a webhook, a schedule, a flow step — had to invent
166
+ * a conversation id to exist at all. That is #164 exactly: `Run` carried no principal, so every host
167
+ * fabricated one, the shipped example used `"example-worker"`, and every per-person figure silently became a
168
+ * machine's. Nothing failed. A fabricated conversation id would do the same to every conversation-scoped
169
+ * query, and it would look like data.
170
+ *
171
+ * Absent is a *fact about the run*, not a missing field: the conversation-scoped capabilities — history,
172
+ * compaction, thread summaries — are then **unavailable** rather than operating on nothing. Asking for them is
173
+ * a programming error and says so.
174
+ */
175
+ readonly conversationId?: ConversationId;
176
+ readonly agentId: AgentId;
177
+ readonly agentVersion: number;
178
+ /**
179
+ * Who this run is for, and with what roles — #164.
180
+ *
181
+ * Optional on the *type* only so existing callers keep compiling; supplying it is what lets a durable worker
182
+ * rebuild the caller's identity instead of inventing one. A run carried a tenant and nothing else, so
183
+ * `buildContext(run)` had no principal to return and every host fabricated one — the shipped example used
184
+ * `"example-worker"` with `roleIds: ["editor"]`, which attributed every person's memory to one identity and
185
+ * ran a `viewer`'s admitted run with editor rights.
186
+ *
187
+ * Recorded at admission, from the authenticated caller. Never from anything the model produced.
188
+ */
189
+ readonly principalId?: PrincipalId;
190
+ readonly roleIds?: readonly string[];
191
+ /**
192
+ * What this run was asked to do — #202.
193
+ *
194
+ * Added because a **conversation-less** run had nowhere to carry its request. #198 made a run able to exist
195
+ * without a conversation, and a triggered automation or a flow's agent step is exactly that — but the only
196
+ * place a prompt could live was a `Message`, and `Message.conversationId` is required. So the shape said a run
197
+ * needs no conversation while the storage said its input does.
198
+ *
199
+ * A property of the run rather than a message, which is also the more honest model: a webhook's payload and a
200
+ * flow step's brief are *the run's input*, not something somebody said in a thread. A run inside a conversation
201
+ * still reads its history; this is for the runs that have none.
202
+ */
203
+ readonly input?: unknown;
204
+ /**
205
+ * Per-run ceilings, overriding the agent manifest's — #202 AC-3.
206
+ *
207
+ * A flow derives these from what it has left to spend, re-derived per step, so a member cannot outspend the
208
+ * team. Without them the child would take the manifest's limits, which are a property of the *agent* and
209
+ * therefore the same for every member of every team — and a team's budget would bound nothing.
210
+ *
211
+ * Partial on purpose: a caller narrowing one dimension should not have to restate the rest.
212
+ */
213
+ readonly limits?: {
214
+ readonly maxSteps?: number;
215
+ readonly costCeilingMinorUnits?: number;
216
+ readonly wallClockTimeoutMs?: number;
217
+ };
218
+ };
219
+ /**
220
+ * The run's durable lifecycle store. Beyond read/claim it owns lease keepalive, guarded status
221
+ * transitions, durable cancellation and stale-lease reaping — the primitives the durable worker
222
+ * (`createDurableWorker`) needs for crash recovery without duplicate work. Verified by the shared
223
+ * `runStoreConformance` harness so every adapter agrees on claim/lease/transition semantics.
224
+ */
225
+ export interface RunStore {
226
+ create(input: TenantScope & NewRun): Promise<Run>;
227
+ findById(input: TenantScope & {
228
+ id: RunId;
229
+ }): Promise<Run | null>;
230
+ /**
231
+ * Atomic lease-based claim. Succeeds for a `queued` run, or for a `running` run whose lease has
232
+ * expired (crash recovery). Returns null when another worker holds a live lease or the run is
233
+ * terminal — so two workers never process one run.
234
+ */
235
+ claim(input: TenantScope & {
236
+ id: RunId;
237
+ workerId: string;
238
+ leaseMs: number;
239
+ now: string;
240
+ }): Promise<Run | null>;
241
+ /** Extend the lease. Returns false when the claim was lost (reaped/stolen) so the worker aborts. */
242
+ keepalive(input: TenantScope & {
243
+ id: RunId;
244
+ workerId: string;
245
+ leaseMs: number;
246
+ now: string;
247
+ }): Promise<boolean>;
248
+ /** Guarded transition by the claiming worker; rejects moves absent from `RUN_TRANSITIONS`. */
249
+ transition(input: TenantScope & {
250
+ id: RunId;
251
+ workerId: string;
252
+ to: RunStatus;
253
+ now: string;
254
+ error?: PlatformError;
255
+ }): Promise<Run>;
256
+ /** Persist a cancellation request. The owning worker observes it and stops cooperatively. */
257
+ requestCancel(input: TenantScope & {
258
+ id: RunId;
259
+ now: string;
260
+ }): Promise<Run | null>;
261
+ /**
262
+ * Maintenance sweep for runs whose lease expired — recovery candidates. Cross-tenant by design
263
+ * (a background reaper has no tenant); each returned run carries its own `tenantId` for re-claim.
264
+ */
265
+ reapExpired(input: {
266
+ now: string;
267
+ limit: number;
268
+ }): Promise<readonly Run[]>;
269
+ }
270
+ export interface MessageStore {
271
+ findById(input: TenantScope & {
272
+ id: MessageId;
273
+ }): Promise<Message | null>;
274
+ /**
275
+ * A page of a conversation's messages.
276
+ *
277
+ * `newestFirst` exists because the port could only answer "the oldest N" — #167. That is the wrong question for
278
+ * a chat application, whose two real queries are "show me the start" (oldest) and "what does the model need to
279
+ * see" (newest). Without it, a host asking for 100 messages of a 2000-message conversation gets turns 1–100
280
+ * and none of the recent ones: the assistant forgets everything that just happened, and the longer the
281
+ * conversation the worse it gets. Reaching the tail by paging is O(n) round trips for the query made on every
282
+ * single turn.
283
+ *
284
+ * Items come back in the requested order, so a caller rendering oldest-first reverses a newest-first page. The
285
+ * keyset cursor flips with it, so paging is still stable under concurrent inserts in both directions.
286
+ */
287
+ listByConversation(input: TenantScope & PageRequest & {
288
+ conversationId: ConversationId;
289
+ newestFirst?: boolean;
290
+ }): Promise<Page<Message>>;
291
+ /**
292
+ * Record a message — #157.
293
+ *
294
+ * The port was read-only, and both Postgres and memory adapters carried an `append` documented as a "test-only
295
+ * affordance". So there was no *supported* way for an application to record what the user said, and every host
296
+ * had to reach past the port with a cast or write raw SQL. The engine reads history from here, so something has
297
+ * to write to it.
298
+ *
299
+ * **Insert-only, and idempotent on the id.** A message is immutable once written: editing one would rewrite
300
+ * history a client has already streamed and a model has already been shown. So there is deliberately no update
301
+ * and no delete, and a repeat of the same id is a no-op rather than an error — a retried request must not fail
302
+ * and must not duplicate.
303
+ */
304
+ append(input: TenantScope & {
305
+ message: Message;
306
+ }): Promise<void>;
307
+ }
308
+ export interface AgentStore {
309
+ findByVersion(input: TenantScope & {
310
+ agentId: string;
311
+ version: number;
312
+ }): Promise<AgentManifest | null>;
313
+ }
314
+ export interface SkillStore {
315
+ listCatalog(input: TenantScope): Promise<readonly SkillCatalogEntry[]>;
316
+ findVersion(input: TenantScope & {
317
+ name: string;
318
+ version: number;
319
+ }): Promise<SkillVersion | null>;
320
+ }
321
+ /**
322
+ * Durable human-in-the-loop interactions (`docs/04` → Questions & Approvals). Pending questions and
323
+ * approvals survive restart/deploy. `answer`/`decide` are idempotent — the first call resolves the
324
+ * interaction and reports `alreadyResolved: false`; a duplicate reports `true` and changes nothing,
325
+ * so a continuation is queued exactly once.
326
+ */
327
+ export interface InteractionStore {
328
+ createQuestion(input: TenantScope & {
329
+ question: PendingQuestion;
330
+ }): Promise<void>;
331
+ findPendingQuestion(input: TenantScope & {
332
+ runId: RunId;
333
+ }): Promise<PendingQuestion | null>;
334
+ /**
335
+ * Record answers.
336
+ *
337
+ * A value may be a **string or an array of strings** (#155): a multi-select question has several answers, and
338
+ * `QuestionSpec.multiple` exists to say so. Widened rather than encoded — comma-joining would make an answer
339
+ * containing a comma indistinguishable from two answers.
340
+ */
341
+ answerQuestion(input: TenantScope & {
342
+ interactionId: InteractionId;
343
+ answers: Readonly<Record<string, QuestionAnswer>>;
344
+ at: string;
345
+ }): Promise<{
346
+ question: PendingQuestion;
347
+ alreadyResolved: boolean;
348
+ }>;
349
+ /**
350
+ * The run's answered question — what a resumed run must tell the model — #163.
351
+ *
352
+ * The approval side has had `findDecidedApproval` from the start; questions had nothing, so a resumed run
353
+ * could not learn that its question had been answered. The observable result was a loop: the person picked
354
+ * an option, the run resumed, the model had no idea and asked the same question again.
355
+ *
356
+ * **No claim, unlike an approval.** A claim exists to make an external write happen exactly once; this
357
+ * produces a line of history, which is idempotent — and scoping to the run already bounds it, since the next
358
+ * turn is a different run. Adding a claim would mean a recovered run rebuilt its history *without* the answer,
359
+ * which is the bug again.
360
+ */
361
+ findAnsweredQuestion(input: TenantScope & {
362
+ runId: RunId;
363
+ }): Promise<PendingQuestion | null>;
364
+ createApproval(input: TenantScope & {
365
+ approval: PendingApproval;
366
+ }): Promise<void>;
367
+ findPendingApproval(input: TenantScope & {
368
+ runId: RunId;
369
+ }): Promise<PendingApproval | null>;
370
+ decideApproval(input: TenantScope & {
371
+ interactionId: InteractionId;
372
+ decision: ApprovalDecision;
373
+ at: string;
374
+ }): Promise<{
375
+ approval: PendingApproval;
376
+ alreadyResolved: boolean;
377
+ }>;
378
+ /** One approval by id — how a one-time authorization is verified against what was stored. */
379
+ findApproval(input: TenantScope & {
380
+ interactionId: InteractionId;
381
+ }): Promise<PendingApproval | null>;
382
+ /**
383
+ * The run's decided-but-unclaimed approval: what a resumed run must execute. Decided and claimed
384
+ * are separate states because the decision is the human's and the claim is the runtime's — without
385
+ * the second, nothing distinguishes "approved, waiting to run" from "approved, already run".
386
+ */
387
+ findDecidedApproval(input: TenantScope & {
388
+ runId: RunId;
389
+ }): Promise<PendingApproval | null>;
390
+ /**
391
+ * Claim the single execution a decided approval authorizes — the mechanism behind `allow-once`.
392
+ *
393
+ * A compare-and-set, not a read-then-write: two workers racing a resumed run must see exactly one
394
+ * `claimed: true`, because the loser executing anyway is a duplicate publish. `claimed` is false
395
+ * for an approval that is already claimed *and* for one nobody has decided, so an undecided
396
+ * interaction can never be turned into permission.
397
+ */
398
+ claimApproval(input: TenantScope & {
399
+ interactionId: InteractionId;
400
+ at: string;
401
+ }): Promise<{
402
+ approval: PendingApproval;
403
+ claimed: boolean;
404
+ }>;
405
+ }
406
+ /** Standing approval grants from `allow-conversation` / `allow-always` (`docs/04` → Approvals). */
407
+ export interface ApprovalGrantStore {
408
+ grant(input: TenantScope & {
409
+ grant: ApprovalGrant;
410
+ }): Promise<void>;
411
+ /**
412
+ * An active (unrevoked, unexpired) grant matching the tool name or category, or null. A
413
+ * conversation-scoped grant matches only when `conversationId` is supplied and equal — so an
414
+ * `allow-conversation` grant never leaks to another conversation or tenant-wide.
415
+ */
416
+ findActive(input: TenantScope & {
417
+ toolNameOrCategory: string;
418
+ now: string;
419
+ conversationId?: string;
420
+ }): Promise<ApprovalGrant | null>;
421
+ revoke(input: TenantScope & {
422
+ grantId: ApprovalGrantId;
423
+ at: string;
424
+ }): Promise<void>;
425
+ }
426
+ export interface CheckpointStore {
427
+ latest(input: TenantScope & {
428
+ runId: RunId;
429
+ }): Promise<RunCheckpoint | null>;
430
+ /** Overwrite the run's checkpoint. Monotonic: a save with a lower sequence is ignored. */
431
+ save(input: TenantScope & {
432
+ checkpoint: RunCheckpoint;
433
+ }): Promise<void>;
434
+ }
435
+ /** Aggregated usage across a set of events — the basis of ceiling checks and rollups. */
436
+ export type UsageTotals = {
437
+ readonly inputTokens: number;
438
+ readonly outputTokens: number;
439
+ readonly cachedInputTokens: number;
440
+ readonly reasoningTokens: number;
441
+ readonly costMinorUnits: number;
442
+ readonly eventCount: number;
443
+ };
444
+ /**
445
+ * Rollup granularities (#139).
446
+ *
447
+ * Hour, day, week and month, and nothing finer than an hour. A minute bucket multiplies the row count
448
+ * sixty-fold to answer a question nobody asks.
449
+ *
450
+ * Week and month were added by #175, because they are the periods a *person's* allowance is actually expressed
451
+ * in — "500k tokens a month" is a plan, "500k tokens a day" is not. They cannot be derived by summing days
452
+ * either: a month is 28 to 31 days and a week crosses month boundaries, so a caller summing day buckets has to
453
+ * reimplement calendar arithmetic, and two callers doing it differently is two answers to what a month cost.
454
+ *
455
+ * A week starts **Monday**, in UTC, following ISO 8601 — chosen rather than defaulted, because a Sunday start
456
+ * would split a working week across two buckets and every weekly figure would describe half of one week and half
457
+ * of another.
458
+ */
459
+ export declare const ROLLUP_PERIODS: readonly ["hour", "day", "week", "month"];
460
+ export type RollupPeriod = (typeof ROLLUP_PERIODS)[number];
461
+ /**
462
+ * The span a limit is measured over — #181.
463
+ *
464
+ * A union, not an optional field beside `period`, because the two kinds are read differently and confusing them
465
+ * produces a wrong number rather than an error. A calendar window is a bucket the rollups already hold; a
466
+ * rolling window has no bucket and has to be summed from the ledger. An optional `minutes` next to a required
467
+ * `period` would let a caller set both, and something would have to decide which one meant it.
468
+ *
469
+ * **A rolling window slides; it does not reset.** "No more than X in any five hours" — the window is always the
470
+ * last `minutes` up to now. That is a deliberate choice over the other reading, an *anchored session* that
471
+ * starts on first use and hard-resets after five hours, and the reason is that an anchored window's boundary is
472
+ * state: to know which session a spend belongs to you must know when the current one began, which cannot be
473
+ * derived from the records without walking history forward from the first one ever. Storing the anchor would
474
+ * make the boundary a row that can be wrong, stale or missing, and the refusal message quotes it to people.
475
+ *
476
+ * The sliding reading needs no state, cannot drift, and is strictly harder to game — you cannot wait out a
477
+ * boundary and spend twice the allowance across it. What it gives up is a single clean "resets at": headroom
478
+ * returns gradually as records age out, so the honest statement is *when the oldest one leaves*, which is what
479
+ * `earliestAt` is for.
480
+ */
481
+ export type QuotaWindow = {
482
+ readonly kind: "calendar";
483
+ readonly period: RollupPeriod;
484
+ }
485
+ /** `minutes` so an admin can express 5 hours, 90 minutes or 2 days without a new type. */
486
+ | {
487
+ readonly kind: "rolling";
488
+ readonly minutes: number;
489
+ };
490
+ /**
491
+ * One tenant's consumption in one bucket.
492
+ *
493
+ * `bucketStart` is the ISO instant the bucket opens, truncated to the period — so a bucket is identified by
494
+ * its start rather than by a range, and two writers computing "the hour containing T" agree by construction.
495
+ */
496
+ export type UsageRollup = UsageTotals & {
497
+ readonly period: RollupPeriod;
498
+ readonly bucketStart: string;
499
+ readonly currency: string;
500
+ /**
501
+ * Whose bucket this is — absent for the tenant's own total (#175).
502
+ *
503
+ * On the row rather than only in the query, because a caller holding a mixed list otherwise cannot tell a
504
+ * tenant total from one person's, and adding them together double-counts everything.
505
+ */
506
+ readonly principalId?: PrincipalId;
507
+ /** When this bucket was last recomputed. For spotting a rollup job that has stopped running. */
508
+ readonly computedAt: string;
509
+ };
510
+ /**
511
+ * Aggregated consumption, derived from the ledger — never a second source of truth.
512
+ *
513
+ * **Rollups are recomputed, not accumulated.** `rebuild` reads a bucket's raw events and *replaces* the row.
514
+ * That makes idempotency structural rather than bookkept: re-running a bucket produces the same numbers, and
515
+ * two workers racing the same bucket write the same value. The alternative — accumulating deltas with a set of
516
+ * applied event keys — needs that set to be durable, unbounded and exactly right forever, and any gap in it is
517
+ * silent double counting or silent loss.
518
+ *
519
+ * The cost is one scan per bucket in the *job*, not in the query. That is what a rollup job is for; AC-1 is
520
+ * about the read path.
521
+ */
522
+ export interface UsageRollupStore {
523
+ /**
524
+ * Recompute one bucket from the ledger and replace the row.
525
+ *
526
+ * Returns the row it wrote, so a caller can act on the figures without a second read.
527
+ */
528
+ /**
529
+ * The store stamps `computedAt` itself, from its own clock.
530
+ *
531
+ * Not the caller's, and this is not a convenience: staleness is *defined* by comparing a bucket's
532
+ * `computedAt` against when its newest event was recorded, and both of those are the store's timestamps. A
533
+ * caller-supplied value is a caller-supplied answer to "is this rollup current" — and a caller whose clock
534
+ * runs slow, or which passes a fixed value, would mark a bucket permanently stale or permanently fresh.
535
+ * Found by a conformance fixture passing a constant and every bucket coming back stale.
536
+ */
537
+ rebuild(input: TenantScope & {
538
+ period: RollupPeriod;
539
+ bucketStart: string;
540
+ principalId?: PrincipalId;
541
+ }): Promise<UsageRollup>;
542
+ /**
543
+ * One bucket, or null when nothing has been recorded in it.
544
+ *
545
+ * `principalId` omitted reads the **tenant** row; supplied reads that person's — #175. They are the same
546
+ * measurement at two grains, stored as a nullable dimension on one table rather than in two tables, because
547
+ * two rebuild paths is two chances to disagree and the day they disagree is the day an invoice is wrong.
548
+ *
549
+ * A per-principal quota needs this: without it, a limit on one person is checked against everybody's usage.
550
+ */
551
+ get(input: TenantScope & {
552
+ period: RollupPeriod;
553
+ bucketStart: string;
554
+ principalId?: PrincipalId;
555
+ }): Promise<UsageRollup | null>;
556
+ /**
557
+ * A range of buckets, for a chart — the read path AC-1 is about.
558
+ *
559
+ * `from` inclusive, `to` exclusive, so adjacent ranges tile without overlapping and a caller cannot
560
+ * double-count a boundary bucket by asking for two ranges.
561
+ */
562
+ list(input: TenantScope & PageRequest & {
563
+ period: RollupPeriod;
564
+ from: string;
565
+ to: string;
566
+ principalId?: PrincipalId;
567
+ }): Promise<Page<UsageRollup>>;
568
+ /**
569
+ * The sum across a range, without returning the buckets.
570
+ *
571
+ * Separate from `list` because a quota check needs one number and a chart needs many, and making the quota
572
+ * check page through buckets would put the read path's cost on the admission path.
573
+ */
574
+ sum(input: TenantScope & {
575
+ period: RollupPeriod;
576
+ from: string;
577
+ to: string;
578
+ principalId?: PrincipalId;
579
+ }): Promise<UsageTotals>;
580
+ /**
581
+ * Buckets that have never been computed, or were computed before the newest event in them.
582
+ *
583
+ * The rollup job's work list, derived from the ledger so an interrupted job resumes by asking again — the
584
+ * same shape `listStaleSources` uses for re-indexing, and for the same reason.
585
+ */
586
+ listStaleBuckets(input: TenantScope & PageRequest & {
587
+ period: RollupPeriod;
588
+ since: string;
589
+ }): Promise<Page<{
590
+ readonly period: RollupPeriod;
591
+ readonly bucketStart: string;
592
+ /**
593
+ * The principal whose bucket is stale, or undefined for the tenant's own — #175.
594
+ *
595
+ * Returned rather than left for the job to enumerate, because the job cannot know which principals were
596
+ * active in a bucket without reading the ledger, which is the scan a rollup exists to avoid. The store
597
+ * already has to look at the ledger to decide staleness, so it is the only place that knows cheaply.
598
+ */
599
+ readonly principalId?: PrincipalId;
600
+ }>>;
601
+ }
602
+ /**
603
+ * Admin-configured spend limits — #175.
604
+ *
605
+ * `principalId` absent is the **tenant default**; present overrides it for one person. One store rather than
606
+ * two, because "the default" and "an override" are the same kind of fact and resolution is then a single query
607
+ * ordered by specificity rather than two queries and a merge.
608
+ *
609
+ * Every limit is optional and an omitted one is **unbounded**, not zero — matching `QuotaLimits`, and for the
610
+ * reason stated there: a misconfigured quota that blocks everything is an outage, and one that blocks nothing is
611
+ * a bill that the rollups make visible.
612
+ */
613
+ /**
614
+ * A window as one storable string, and back — #181.
615
+ *
616
+ * `usage_limits` keys its unique indexes on this single column, so the two kinds of window have to share one
617
+ * value space. A calendar window is its own period; a rolling one is `rolling:<minutes>`. Both spellings are
618
+ * pinned by the table's CHECK constraint, so a value that would not round-trip cannot be stored in the first
619
+ * place.
620
+ *
621
+ * The codec lives here, beside the type, rather than in each adapter: two adapters spelling a key differently is
622
+ * two stores that cannot read each other's rows, and conformance would pass because each is self-consistent.
623
+ */
624
+ export declare const windowKey: (window: QuotaWindow) => string;
625
+ /**
626
+ * `null` for anything this does not recognise — a row written by a newer version, or by hand.
627
+ *
628
+ * Null rather than a thrown error or a calendar fallback. A fallback would silently enforce the wrong window,
629
+ * which for a spend limit means either refusing people wrongly or charging them wrongly; null makes the caller
630
+ * decide, and every caller here treats it as "no limit I can honour", which fails towards *not* pretending.
631
+ */
632
+ export declare const parseWindowKey: (key: string) => QuotaWindow | null;
633
+ export type UsageLimitRecord = {
634
+ readonly tenantId: TenantId;
635
+ readonly principalId?: PrincipalId;
636
+ /**
637
+ * The span this allowance covers — widened from `period: RollupPeriod` by #181, so an admin can set a rolling
638
+ * window as easily as a calendar one.
639
+ */
640
+ readonly window: QuotaWindow;
641
+ /**
642
+ * The model this allowance covers, or absent for **any** model — #182.
643
+ *
644
+ * A separate limit rather than a component of one: "500 of Opus in any five hours" and "10,000 a month
645
+ * overall" are two allowances that both bind, not one allowance with two fields.
646
+ */
647
+ readonly modelId?: string;
648
+ readonly costMinorUnits?: number;
649
+ readonly inputTokens?: number;
650
+ readonly outputTokens?: number;
651
+ readonly warnAt?: number;
652
+ readonly updatedAt: string;
653
+ /** Who set it. A spend limit is the kind of setting somebody eventually has to explain. */
654
+ readonly updatedBy?: string;
655
+ };
656
+ export interface UsageLimitStore {
657
+ /**
658
+ * Set or replace one limit. Keyed on `(tenant, principal, window)`, so an admin editing a limit updates it
659
+ * rather than accumulating rows that a resolver would then have to disambiguate.
660
+ */
661
+ put(input: TenantScope & {
662
+ limit: UsageLimitRecord;
663
+ }): Promise<UsageLimitRecord>;
664
+ /**
665
+ * The limit that applies to this principal in this period — **most specific wins**.
666
+ *
667
+ * A principal's own row if there is one, else the tenant default, else null for unbounded. Resolved in the
668
+ * store rather than by the caller because "most specific wins" is a rule, and a rule implemented at two call
669
+ * sites is a rule with two behaviours.
670
+ */
671
+ resolve(input: TenantScope & {
672
+ principalId?: PrincipalId;
673
+ modelId?: string;
674
+ window: QuotaWindow;
675
+ }): Promise<UsageLimitRecord | null>;
676
+ /**
677
+ * **Every** limit that applies to this principal on this model — #182.
678
+ *
679
+ * Not one limit. A person can be subject to a five-hour cap, a monthly cap and a per-model cap at once, and
680
+ * all three bind: that is what "limits" means everywhere they are used in practice. Returning the single
681
+ * most-specific one, as `resolve` does, meant a workspace-wide Opus cap was silently ignored for anybody who
682
+ * also had a personal overall limit.
683
+ *
684
+ * The rule is **override within a scope, coexist across scopes**. One row per `(window, model)`, preferring
685
+ * the principal's own row over the tenant default for that same scope — so a personal limit replaces the
686
+ * workspace's for the same window and model, and does not replace one for a *different* window or model.
687
+ *
688
+ * A model-scoped row applies only when `modelId` matches. With no `modelId` given, model-scoped rows do not
689
+ * apply at all: an unknown model cannot be checked against a per-model allowance, and guessing would either
690
+ * refuse the wrong work or let it through.
691
+ *
692
+ * Implemented in the store, like `resolve`, because it is a rule — and a rule implemented at two call sites is
693
+ * a rule with two behaviours. Conformance holds the adapters to the same one.
694
+ */
695
+ applicable(input: TenantScope & {
696
+ principalId?: PrincipalId;
697
+ modelId?: string;
698
+ }): Promise<readonly UsageLimitRecord[]>;
699
+ /** Every configured limit for a tenant, for an admin screen. */
700
+ list(input: TenantScope): Promise<readonly UsageLimitRecord[]>;
701
+ /** Remove one. Absent afterwards means "inherit the tenant default", not "zero". */
702
+ remove(input: TenantScope & {
703
+ principalId?: PrincipalId;
704
+ modelId?: string;
705
+ window: QuotaWindow;
706
+ }): Promise<void>;
707
+ }
708
+ /**
709
+ * Append-only usage ledger (`docs/12`). Events are never edited or deleted; corrections are new
710
+ * compensating events. Rollups are derived from events, never a second source of truth. Appends are
711
+ * idempotent on `(runId, stepId)` (or `id`) so a recovered run never double-counts.
712
+ */
713
+ export interface UsageStore {
714
+ append(input: TenantScope & {
715
+ event: UsageEvent;
716
+ }): Promise<void>;
717
+ listByRun(input: TenantScope & PageRequest & {
718
+ runId: RunId;
719
+ }): Promise<Page<UsageEvent>>;
720
+ /**
721
+ * Consumption grouped by model or by conversation, over a bounded range (#140).
722
+ *
723
+ * From the **ledger**, not the rollups, and that is a deliberate trade rather than an oversight. Rollups are
724
+ * keyed on `(tenant, period, bucket)`; adding a model and a conversation dimension would multiply the row
725
+ * count by the product of both cardinalities to serve a panel nobody opens per second. A breakdown over one
726
+ * period is a bounded scan served by the `(tenant_id, occurred_at)` index — where the *headline* total, which
727
+ * is read on every page load and every quota check, comes from a rollup.
728
+ *
729
+ * `limit` because a tenant can have thousands of conversations and a breakdown of all of them is not a
730
+ * breakdown. Returned largest-first so the truncation drops what matters least.
731
+ */
732
+ breakdown(input: TenantScope & {
733
+ from: string;
734
+ to: string;
735
+ /**
736
+ * `principal` added by #175 — "who spent this" was unanswerable because the dimension did not exist.
737
+ *
738
+ * Its cardinality is bounded by the people in a tenant rather than by their activity, which is what makes
739
+ * it a cheaper breakdown than `conversation` rather than a more expensive one.
740
+ */
741
+ by: "model" | "conversation" | "principal";
742
+ limit: number;
743
+ }): Promise<readonly {
744
+ readonly key: string;
745
+ readonly totals: UsageTotals;
746
+ }[]>;
747
+ /**
748
+ * Totals over an exact half-open interval `[from, to)`, and the earliest record inside it — #181.
749
+ *
750
+ * The rollups cannot answer this. They are keyed on `(tenant, period, bucket)`, and a rolling window has no
751
+ * bucket: a five-hour allowance that began at 09:37 spans parts of six hourly buckets, and summing those
752
+ * over-counts at both edges by whatever fell outside the window. An approximate spend limit is worse than
753
+ * none, because the number is used to refuse people.
754
+ *
755
+ * So this reads the **ledger**, over the same `(tenant_id, occurred_at)` index `breakdown` uses, and the scan
756
+ * is bounded by the window rather than by history. `breakdown` was not reused because its `limit` truncates —
757
+ * fine for a panel, wrong for a total that decides admission.
758
+ *
759
+ * `earliestAt` is `null` when nothing was spent in the window, and otherwise the `occurredAt` of the oldest
760
+ * record in it. A sliding window never "resets", so this is what makes a *true* statement possible about when
761
+ * headroom returns: at `earliestAt + length` that record leaves the window. Deriving it here rather than in
762
+ * the caller keeps it consistent with the totals it came from — computed from two queries, they could disagree
763
+ * about a record that arrived between them.
764
+ *
765
+ * `modelId` scopes it to one model (#182). `principalId` scopes it to one person; **absent means the whole
766
+ * tenant**, not "unknown principal" — a rolling tenant limit has to see everyone's spend.
767
+ */
768
+ totalsBetween(input: TenantScope & {
769
+ readonly from: string;
770
+ readonly to: string;
771
+ readonly principalId?: PrincipalId;
772
+ readonly modelId?: string;
773
+ }): Promise<{
774
+ readonly totals: UsageTotals;
775
+ readonly earliestAt: string | null;
776
+ }>;
777
+ /** Running totals, optionally scoped to a run or conversation — used by `reserve()` and rollups. */
778
+ totals(input: TenantScope & {
779
+ runId?: RunId;
780
+ conversationId?: ConversationId;
781
+ }): Promise<UsageTotals>;
782
+ }
783
+ /**
784
+ * Evaluation results, per release — REQ-032 (#141).
785
+ *
786
+ * Durable because the point is *comparison*: a score with nothing to compare it against cannot gate a release,
787
+ * and "quality went down" is only a statement if last release's number still exists. Aggregates are stored
788
+ * alongside per-case results rather than derived on read, because a per-dimension breakdown of a thousand cases
789
+ * is a read every release does and an aggregate nobody can recompute once a case is retired from the dataset.
790
+ */
791
+ export type EvalVerdict = {
792
+ readonly pass: boolean;
793
+ /** 0–1. Binary graders report 0 or 1; a partial-credit grader may report between. */
794
+ readonly score: number;
795
+ /** Why. Shown in the report, so a regression names its cause rather than only its case id. */
796
+ readonly reason: string;
797
+ /**
798
+ * What producing this verdict cost, in integer minor units.
799
+ *
800
+ * On the verdict because the cost is a property of *this grading* — a cache hit and a fresh model call produce
801
+ * the same verdict at different prices, and a harness that could not tell them apart could not report the
802
+ * gate's expense. Absent means free, which is every deterministic grader.
803
+ */
804
+ readonly costMinorUnits?: number;
805
+ };
806
+ /**
807
+ * One case's result, with everything needed to reproduce it.
808
+ *
809
+ * The grader id and version, and for a judged case the model and prompt version, are stored **on the result**.
810
+ * A run that recorded only scores could not answer "why did this change" — a prompt edit and a model upgrade
811
+ * look identical in the numbers, and both look like a quality change.
812
+ */
813
+ export type EvalCaseResult = {
814
+ readonly caseId: string;
815
+ readonly dimension: string;
816
+ readonly expectKind: string;
817
+ readonly verdict: EvalVerdict;
818
+ readonly graderId: string;
819
+ readonly graderVersion: string;
820
+ /** Present only for a judged case. Absent means graded by code, which is also the cheap path. */
821
+ readonly modelId?: string;
822
+ readonly promptVersion?: string;
823
+ /** Integer minor units. Zero for a deterministic grader, which is asserted rather than assumed. */
824
+ readonly costMinorUnits: number;
825
+ };
826
+ export type EvalDimensionSummary = {
827
+ readonly dimension: string;
828
+ readonly total: number;
829
+ readonly passed: number;
830
+ readonly meanScore: number;
831
+ };
832
+ export type EvalRun = {
833
+ readonly id: string;
834
+ /** The release this scored. The axis every comparison is along. */
835
+ readonly release: string;
836
+ readonly startedAt: string;
837
+ readonly finishedAt?: string;
838
+ readonly total: number;
839
+ readonly passed: number;
840
+ readonly meanScore: number;
841
+ readonly byDimension: readonly EvalDimensionSummary[];
842
+ /** What the whole gate cost. Recorded so the gate's own expense is known rather than assumed cheap. */
843
+ readonly costMinorUnits: number;
844
+ /** Every grader version in the run, so two runs can be compared *knowingly*. */
845
+ readonly graderVersions: Readonly<Record<string, string>>;
846
+ };
847
+ export interface EvaluationStore {
848
+ /** Opens a run. Separate from completing it so an interrupted run is visibly unfinished rather than absent. */
849
+ startRun(input: TenantScope & {
850
+ id: string;
851
+ release: string;
852
+ startedAt: string;
853
+ }): Promise<EvalRun>;
854
+ /**
855
+ * Records one case's result.
856
+ *
857
+ * Idempotent on `(runId, caseId)`: a resumed run must not double-count a case it already scored, and a case
858
+ * cannot be scored twice in one run because a run scores each case once by construction.
859
+ */
860
+ recordCase(input: TenantScope & {
861
+ runId: string;
862
+ result: EvalCaseResult;
863
+ }): Promise<{
864
+ readonly recorded: boolean;
865
+ }>;
866
+ /** Closes a run, computing and storing its aggregates from the recorded cases. */
867
+ completeRun(input: TenantScope & {
868
+ runId: string;
869
+ finishedAt: string;
870
+ graderVersions: Readonly<Record<string, string>>;
871
+ }): Promise<EvalRun>;
872
+ get(input: TenantScope & {
873
+ runId: string;
874
+ }): Promise<EvalRun | null>;
875
+ /** The most recent *completed* run for a release, or across releases when none is named. */
876
+ latest(input: TenantScope & {
877
+ release?: string;
878
+ }): Promise<EvalRun | null>;
879
+ /** Runs newest first, for a release history. */
880
+ list(input: TenantScope & PageRequest): Promise<Page<EvalRun>>;
881
+ /** A run's per-case results, which is what a comparison between two runs reads. */
882
+ listCaseResults(input: TenantScope & PageRequest & {
883
+ runId: string;
884
+ }): Promise<Page<EvalCaseResult>>;
885
+ }
886
+ /**
887
+ * What an artifact is.
888
+ *
889
+ * Markdown and structured content first, per `docs/05` → Document writing. Rendered formats — PDF, DOCX — are
890
+ * *exports* of an artifact rather than kinds of one, which is why they are absent here: an artifact exported
891
+ * twice is one artifact, and making PDF a kind would make it two things that drift.
892
+ */
893
+ export declare const ARTIFACT_KINDS: readonly ["markdown", "html", "json", "csv", "code", "diagram"];
894
+ export type ArtifactKind = (typeof ARTIFACT_KINDS)[number];
895
+ /**
896
+ * Where a version came from — AC-3.
897
+ *
898
+ * Required on every version, not optional. A reader asking "where did this number come from" is the whole
899
+ * reason artifacts exist as first-class objects, and provenance that *can* be absent is provenance that will
900
+ * be, on the version someone eventually asks about.
901
+ */
902
+ export type ArtifactProvenance = {
903
+ /** The run that produced it. Absent for an artifact a person created directly. */
904
+ readonly runId?: RunId;
905
+ /** The tool or agent that produced it, by name. */
906
+ readonly producedBy: string;
907
+ /**
908
+ * The inputs it was produced from.
909
+ *
910
+ * Stored as JSON, and deliberately the *normalised* input rather than the model's prose request: a
911
+ * regeneration that produced a different result should be explicable by comparing these, and free text
912
+ * does not compare.
913
+ */
914
+ readonly inputs: Readonly<Record<string, unknown>>;
915
+ /** Attachments the content was derived from, so a document's figures trace back to their source. */
916
+ readonly sourceFileIds?: readonly FileId[];
917
+ };
918
+ /**
919
+ * One version's content, by reference — AC-5.
920
+ *
921
+ * `contentRef` points into `BlobStore`, which stores JSON, which is what markdown-and-structured-content is
922
+ * once it is a value. The row holds no content: an artifact is the thing a user exports, so it grows without
923
+ * limit, and a table row is the wrong place for something unbounded — the same reasoning that kept file bytes
924
+ * out of `files` in #129.
925
+ */
926
+ export type ArtifactVersion = {
927
+ readonly id: ArtifactVersionId;
928
+ readonly artifactId: ArtifactId;
929
+ /** 1-based and contiguous. A gap would make "the previous version" ambiguous. */
930
+ readonly version: number;
931
+ readonly contentRef: BlobRef;
932
+ readonly byteSize: number;
933
+ readonly checksum?: string;
934
+ readonly provenance: ArtifactProvenance;
935
+ readonly createdBy: PrincipalId;
936
+ readonly createdAt: string;
937
+ };
938
+ export type Artifact = {
939
+ readonly id: ArtifactId;
940
+ /**
941
+ * The conversation that owns it.
942
+ *
943
+ * Ownership, not association: AC-4 follows from it. Entitlement to an artifact *is* entitlement to its
944
+ * conversation, exactly as #129 established for files, so there is no second permission model to keep in
945
+ * step with the first.
946
+ */
947
+ readonly conversationId: ConversationId;
948
+ readonly kind: ArtifactKind;
949
+ /** As the user or agent named it. Display only. */
950
+ readonly name: string;
951
+ /** The highest version that exists. The default a reader gets when they do not ask for one. */
952
+ readonly latestVersion: number;
953
+ readonly createdAt: string;
954
+ readonly updatedAt: string;
955
+ /** Soft delete, so a shared link resolves to "deleted" rather than to nothing. */
956
+ readonly deletedAt?: string;
957
+ };
958
+ /**
959
+ * A rendered export of one artifact version (#134).
960
+ *
961
+ * **Not an artifact version.** #133's versions are versions of the *content*; a PDF is a rendering of one.
962
+ * Making a render a new version would bump `latestVersion` for a reason unrelated to the content, and
963
+ * "the newest version" would stop meaning "the newest thing the assistant wrote". The issue's wording said
964
+ * artifact version; this is the deviation, and the reason.
965
+ *
966
+ * Keyed on `(artifactId, version, format)`, which is what makes "re-downloaded without re-rendering" a
967
+ * constraint rather than a cache someone remembers to check.
968
+ */
969
+ export declare const EXPORT_FORMATS: readonly ["pdf", "markdown"];
970
+ export type ExportFormat = (typeof EXPORT_FORMATS)[number];
971
+ export declare const EXPORT_STATES: readonly ["pending", "rendering", "rendered", "failed"];
972
+ export type ExportState = (typeof EXPORT_STATES)[number];
973
+ export type ArtifactExport = {
974
+ readonly id: string;
975
+ readonly artifactId: ArtifactId;
976
+ /** The content version this rendered. An export is of a *version*, never of "the artifact". */
977
+ readonly version: number;
978
+ readonly format: ExportFormat;
979
+ readonly state: ExportState;
980
+ /**
981
+ * The rendered bytes, as a file.
982
+ *
983
+ * A `FileId` rather than a `BlobRef`: `BlobStore` holds JSON and a PDF is bytes, and going through the file
984
+ * ports means the export inherits #129's entitlement check and short-lived signed URLs rather than needing
985
+ * a second mediated-download path.
986
+ */
987
+ readonly fileId?: FileId;
988
+ readonly byteSize?: number;
989
+ readonly checksum?: string;
990
+ readonly failureReason?: string;
991
+ readonly failureMessage?: string;
992
+ readonly requestedBy: PrincipalId;
993
+ readonly createdAt: string;
994
+ readonly renderedAt?: string;
995
+ };
996
+ export interface ArtifactExportStore {
997
+ /**
998
+ * Claims the export slot for `(artifactId, version, format)`.
999
+ *
1000
+ * Returns the existing row when there is one, so a second request for the same export is a *read* rather
1001
+ * than a second render. `claimed: false` means someone else has it — which is the answer that stops two
1002
+ * workers rendering the same PDF.
1003
+ */
1004
+ claim(input: TenantScope & {
1005
+ export: Omit<ArtifactExport, "state" | "fileId" | "byteSize" | "checksum" | "failureReason" | "failureMessage" | "renderedAt">;
1006
+ }): Promise<{
1007
+ readonly claimed: boolean;
1008
+ readonly export: ArtifactExport;
1009
+ }>;
1010
+ /** Records the outcome. Idempotent, because a worker retrying after a crash cannot know what it wrote. */
1011
+ complete(input: TenantScope & {
1012
+ id: string;
1013
+ state: Extract<ExportState, "rendered" | "failed">;
1014
+ fileId?: FileId;
1015
+ byteSize?: number;
1016
+ checksum?: string;
1017
+ failureReason?: string;
1018
+ failureMessage?: string;
1019
+ at: string;
1020
+ }): Promise<{
1021
+ readonly recorded: boolean;
1022
+ }>;
1023
+ get(input: TenantScope & {
1024
+ id: string;
1025
+ }): Promise<ArtifactExport | null>;
1026
+ /** The export for a specific version and format, which is the cache lookup. */
1027
+ find(input: TenantScope & {
1028
+ artifactId: ArtifactId;
1029
+ version: number;
1030
+ format: ExportFormat;
1031
+ }): Promise<ArtifactExport | null>;
1032
+ listByArtifact(input: TenantScope & PageRequest & {
1033
+ artifactId: ArtifactId;
1034
+ }): Promise<Page<ArtifactExport>>;
1035
+ }
1036
+ export interface ArtifactStore {
1037
+ /** Creates the artifact and its version 1 together: an artifact with no version is not a thing. */
1038
+ create(input: TenantScope & {
1039
+ artifact: Omit<Artifact, "latestVersion" | "updatedAt" | "deletedAt">;
1040
+ version: Omit<ArtifactVersion, "artifactId" | "version">;
1041
+ }): Promise<Artifact>;
1042
+ /**
1043
+ * Adds the next version — AC-2.
1044
+ *
1045
+ * `expectedLatestVersion` makes this a compare-and-set. Without it two concurrent regenerations both become
1046
+ * version 2: one silently replaces the other, and "earlier versions remain resolvable" stops being true for
1047
+ * the one that lost. Reported rather than thrown, because losing that race is an ordinary outcome.
1048
+ */
1049
+ addVersion(input: TenantScope & {
1050
+ id: ArtifactId;
1051
+ expectedLatestVersion: number;
1052
+ version: Omit<ArtifactVersion, "artifactId" | "version">;
1053
+ }): Promise<{
1054
+ readonly added: boolean;
1055
+ readonly version?: number;
1056
+ }>;
1057
+ /** `null` for another tenant's artifact as well as an absent one — the two must be indistinguishable. */
1058
+ get(input: TenantScope & {
1059
+ id: ArtifactId;
1060
+ }): Promise<Artifact | null>;
1061
+ /** A specific version, or the latest when `version` is omitted. */
1062
+ getVersion(input: TenantScope & {
1063
+ id: ArtifactId;
1064
+ version?: number;
1065
+ }): Promise<ArtifactVersion | null>;
1066
+ /** Live artifacts of a conversation, newest cursor last. */
1067
+ listByConversation(input: TenantScope & PageRequest & {
1068
+ conversationId: ConversationId;
1069
+ }): Promise<Page<Artifact>>;
1070
+ /** Every version, oldest first — the history a restore reads. */
1071
+ listVersions(input: TenantScope & PageRequest & {
1072
+ id: ArtifactId;
1073
+ }): Promise<Page<ArtifactVersion>>;
1074
+ /** Soft delete. Versions are kept: the row is what makes a shared link resolve to "deleted". */
1075
+ softDelete(input: TenantScope & {
1076
+ id: ArtifactId;
1077
+ at: string;
1078
+ }): Promise<{
1079
+ readonly deleted: boolean;
1080
+ }>;
1081
+ }
1082
+ /**
1083
+ * Where a file is in its lifecycle.
1084
+ *
1085
+ * `pending` exists because an upload is two writes — metadata, then bytes — and the window between them is
1086
+ * real. A file stuck in `pending` is metadata with no bytes, which is one of the two orphan directions
1087
+ * reconciliation looks for.
1088
+ *
1089
+ * `deleting` is the same window in reverse: the metadata is gone from the user's view and the bytes are
1090
+ * not yet gone from storage. Deleting them in one transaction is not available — object storage does not
1091
+ * join a database transaction — so the intermediate state is named rather than pretended away.
1092
+ */
1093
+ export declare const FILE_STATES: readonly ["pending", "stored", "deleting", "deleted"];
1094
+ export type FileState = (typeof FILE_STATES)[number];
1095
+ export type FileMetadata = {
1096
+ readonly id: FileId;
1097
+ /**
1098
+ * The conversation that owns it.
1099
+ *
1100
+ * Ownership rather than association: AC-3 and AC-4 of #129 both follow from it — entitlement to the file
1101
+ * *is* entitlement to the conversation, and deleting the conversation is what schedules the bytes. A file
1102
+ * with no owner would need its own permission model, which is a second one to keep in step.
1103
+ */
1104
+ readonly conversationId: ConversationId;
1105
+ /** As the user named it. Display only — never used to address the bytes. */
1106
+ readonly filename: string;
1107
+ readonly mediaType: string;
1108
+ readonly byteSize: number;
1109
+ /**
1110
+ * How the content store addresses the bytes. Opaque here.
1111
+ *
1112
+ * Deliberately not derived from the filename or the id: a key a caller can *construct* is a key a caller
1113
+ * can guess, and `sanitizeMediaRefs`' comment in ShareFlow is the cautionary tale — its workspace-prefix
1114
+ * check was "the ONLY thing standing between a forged path and a signed URL to another tenant's private
1115
+ * object".
1116
+ */
1117
+ readonly contentKey: string;
1118
+ /** Of the bytes as stored, so a read-back can be checked rather than assumed. */
1119
+ readonly checksum?: string;
1120
+ readonly state: FileState;
1121
+ /** Extraction outcome (#131). Absent for a file nothing has tried to extract yet. */
1122
+ readonly extraction?: FileExtraction;
1123
+ readonly uploadedBy: PrincipalId;
1124
+ readonly createdAt: string;
1125
+ /** Soft delete. A row is kept so a reference to it resolves to "deleted" rather than to nothing. */
1126
+ readonly deletedAt?: string;
1127
+ };
1128
+ /**
1129
+ * What a file records about its derived text (#131).
1130
+ *
1131
+ * Declared here rather than imported from `documents/`: a store port that depended on the extraction pipeline
1132
+ * would make the pipeline a prerequisite for storing a file, and `persistence` is the layer nothing above it
1133
+ * gets to reach into.
1134
+ *
1135
+ * Separate from the file's own `state` on purpose. A file is perfectly `stored` while its extraction has
1136
+ * `failed`, and conflating the two would make an unreadable document look like a lost upload.
1137
+ */
1138
+ /**
1139
+ * Below this, a recognised extraction is flagged rather than presented as certain (#132).
1140
+ *
1141
+ * Here rather than in `documents/` because two layers interpret the same field and neither may import the
1142
+ * other: `documents/vision.ts` sets the flag, and `files/context.ts` marks the reference line so a model
1143
+ * choosing between attachments knows before it reads any of them. A copy in each is a copy that drifts, and
1144
+ * the port that declares `confidence` is the right place for the number that gives it meaning.
1145
+ *
1146
+ * 0.7 because that is roughly where OCR stops being "a few wrong characters" and becomes "wrong words" — and
1147
+ * a wrong word is worse than a gap, because the sentence still reads.
1148
+ */
1149
+ export declare const LOW_CONFIDENCE_THRESHOLD = 0.7;
1150
+ export type FileExtraction = {
1151
+ readonly state: "pending" | "running" | "extracted" | "failed" | "skipped";
1152
+ /** Where the extracted document lives. `BlobStore` holds JSON, which is exactly what it is. */
1153
+ readonly ref?: BlobRef;
1154
+ readonly failureReason?: string;
1155
+ readonly failureMessage?: string;
1156
+ readonly pageCount?: number;
1157
+ readonly blockCount?: number;
1158
+ readonly truncated?: boolean;
1159
+ /**
1160
+ * OCR/vision confidence, 0–1 (#132).
1161
+ *
1162
+ * On the record as well as inside the extracted document, so a listing can flag a low-confidence extraction
1163
+ * without fetching the blob to find out. Absent means the extraction was not probabilistic — a PDF's text
1164
+ * layer is read, not recognised — which is a different fact from "confidence unknown".
1165
+ */
1166
+ readonly confidence?: number;
1167
+ readonly at?: string;
1168
+ };
1169
+ export interface FileMetadataStore {
1170
+ create(input: TenantScope & {
1171
+ file: FileMetadata;
1172
+ }): Promise<void>;
1173
+ /** `null` for another tenant's file as well as an absent one — the two must be indistinguishable. */
1174
+ get(input: TenantScope & {
1175
+ id: FileId;
1176
+ }): Promise<FileMetadata | null>;
1177
+ /** Live files only: soft-deleted rows are excluded, since this is what a user sees. */
1178
+ listByConversation(input: TenantScope & PageRequest & {
1179
+ conversationId: ConversationId;
1180
+ }): Promise<Page<FileMetadata>>;
1181
+ /**
1182
+ * Advance the lifecycle, with the state it must currently be in.
1183
+ *
1184
+ * Compare-and-set rather than a blind write: two workers finishing the same upload, or a delete racing a
1185
+ * completion, must not leave a file `stored` after its bytes were scheduled for removal. Returns whether
1186
+ * this call was the one that moved it.
1187
+ */
1188
+ transition(input: TenantScope & {
1189
+ id: FileId;
1190
+ from: FileState;
1191
+ to: FileState;
1192
+ at: string;
1193
+ checksum?: string;
1194
+ }): Promise<{
1195
+ readonly moved: boolean;
1196
+ }>;
1197
+ /**
1198
+ * Record the outcome of extraction (#131).
1199
+ *
1200
+ * Idempotent and deliberately **not** a compare-and-set, unlike `transition`. A retried extraction writing
1201
+ * the same outcome twice is harmless, and requiring the caller to state the previous extraction state would
1202
+ * make a retry after a crash impossible — the crash is exactly why it does not know what that state was.
1203
+ *
1204
+ * `recorded: false` means the file is gone, which a retry after a conversation delete will legitimately
1205
+ * hit; reported rather than thrown, because it is an ordinary race and not a fault.
1206
+ */
1207
+ recordExtraction(input: TenantScope & {
1208
+ id: FileId;
1209
+ extraction: FileExtraction;
1210
+ }): Promise<{
1211
+ readonly recorded: boolean;
1212
+ }>;
1213
+ /**
1214
+ * Files in a given extraction state, oldest first.
1215
+ *
1216
+ * The reconciliation of *extraction*, distinct from `listByState`'s reconciliation of bytes: a file stuck in
1217
+ * `running` is a worker that died mid-parse, and nothing else would ever notice.
1218
+ */
1219
+ listByExtractionState(input: TenantScope & PageRequest & {
1220
+ state: FileExtraction["state"];
1221
+ olderThan: string;
1222
+ }): Promise<Page<FileMetadata>>;
1223
+ /**
1224
+ * Mark every live file of a conversation for byte deletion.
1225
+ *
1226
+ * One call rather than a list-then-loop, because a file uploaded between the list and the loop would be
1227
+ * missed — and it would be missed silently, leaving bytes for a conversation that no longer exists.
1228
+ */
1229
+ scheduleConversationDeletion(input: TenantScope & {
1230
+ conversationId: ConversationId;
1231
+ at: string;
1232
+ }): Promise<{
1233
+ readonly scheduled: number;
1234
+ }>;
1235
+ /**
1236
+ * Files in a state longer than they should be — reconciliation's input.
1237
+ *
1238
+ * `olderThan` rather than "all in this state", because a file that entered `pending` a second ago is an
1239
+ * upload in progress and a file that entered it yesterday is an orphan. Without the threshold the job
1240
+ * would report every upload happening while it ran.
1241
+ */
1242
+ listByState(input: TenantScope & PageRequest & {
1243
+ state: FileState;
1244
+ olderThan: string;
1245
+ }): Promise<Page<FileMetadata>>;
1246
+ }
1247
+ /** What the content store recorded about the bytes it accepted. */
1248
+ export type StoredContent = {
1249
+ readonly contentKey: string;
1250
+ /** As actually written, which may differ from what the caller declared — see `putFile`. */
1251
+ readonly byteSize: number;
1252
+ readonly checksum: string;
1253
+ };
1254
+ /** One object as the content store sees it. Used only by reconciliation. */
1255
+ export type StoredObject = {
1256
+ readonly contentKey: string;
1257
+ readonly byteSize: number;
1258
+ };
1259
+ /**
1260
+ * The bytes.
1261
+ *
1262
+ * Separate from `FileMetadataStore` because object storage cannot join a database transaction, and
1263
+ * pretending otherwise is what produces orphans. Every method is tenant-scoped: a `contentKey` from one
1264
+ * tenant must never resolve another's object, and the key alone must not be sufficient.
1265
+ */
1266
+ export interface FileContentStore {
1267
+ /**
1268
+ * Write bytes and report what was written.
1269
+ *
1270
+ * **`maxBytes` is enforced while reading, not checked beforehand.** A declared size is a claim; the cap is
1271
+ * the defence. An adapter must stop consuming and discard the partial object when the cap is passed —
1272
+ * reading to the end and then refusing is a denial of service that happens to return an error.
1273
+ */
1274
+ putFile(input: TenantScope & {
1275
+ contentKey: string;
1276
+ mediaType: string;
1277
+ bytes: AsyncIterable<Uint8Array>;
1278
+ maxBytes: number;
1279
+ }): Promise<StoredContent>;
1280
+ /** `null` when absent, or when the key belongs to another tenant. */
1281
+ readFile(input: TenantScope & {
1282
+ contentKey: string;
1283
+ }): Promise<AsyncIterable<Uint8Array> | null>;
1284
+ /**
1285
+ * A short-lived authorised URL, or `null` when this adapter proxies reads instead.
1286
+ *
1287
+ * `expiresInSeconds` is **required**, and there is deliberately no method that returns a durable URL —
1288
+ * that is AC-6 of #129 made structural rather than left as a rule an adapter has to remember. An adapter
1289
+ * with no signing mechanism returns `null` and the caller streams through `readFile`.
1290
+ */
1291
+ signedUrl(input: TenantScope & {
1292
+ contentKey: string;
1293
+ expiresInSeconds: number;
1294
+ }): Promise<string | null>;
1295
+ /** Idempotent: deleting an absent object is a no-op, because a retried sweep must not fail. */
1296
+ deleteFile(input: TenantScope & {
1297
+ contentKey: string;
1298
+ }): Promise<void>;
1299
+ /**
1300
+ * Objects this tenant has stored.
1301
+ *
1302
+ * For reconciliation's second direction — bytes with no metadata — which is the one that costs money
1303
+ * silently and which the metadata store cannot see at all.
1304
+ */
1305
+ listObjects(input: TenantScope & PageRequest & {
1306
+ prefix?: string;
1307
+ }): Promise<Page<StoredObject>>;
1308
+ }
1309
+ /**
1310
+ * Knowledge chunks and the vector index — REQ-029 (#135).
1311
+ *
1312
+ * Two ports rather than one, because they answer different questions and can be backed by different systems:
1313
+ * `KnowledgeStore` owns the *rows* (content, provenance, which embedding produced them) and `VectorIndex` owns
1314
+ * the *similarity search*. A deployment on pgvector satisfies both with one table; a deployment on a dedicated
1315
+ * vector database satisfies them with two systems, and nothing above this layer changes.
1316
+ *
1317
+ * **The authorisation subject is on the chunk row.** That is the single most important decision here. The SPEC
1318
+ * says filtering happens *inside* the query, and the reason is precise: filtering after retrieval leaks through
1319
+ * result counts. Ask for ten chunks, get three back, and you have learned that seven exist that you may not
1320
+ * see — and with a few queries, roughly what they are about. So `authSubject` travels with the chunk and every
1321
+ * search takes it as a required filter.
1322
+ */
1323
+ /**
1324
+ * The embedding width every adapter stores.
1325
+ *
1326
+ * On the port, not in an adapter, for the reason `DEFAULT_SESSION_STATE_MAX_BYTES` is (#97): a vector column has
1327
+ * one width and a vector index cannot span widths, so "every adapter agrees on the size" must be a property
1328
+ * rather than a coincidence. The reference adapter accepted 768 while pgvector refused it, which is exactly the
1329
+ * laxness that turns a production write failure into a passing test.
1330
+ *
1331
+ * 1536 is OpenAI's `text-embedding-3-small` and `-large` at its default reduction, and Cohere's v3 — the sizes
1332
+ * a deployment is most likely to have. Changing it is a **migration**, not a re-index, which is why
1333
+ * `EmbeddingModelRef` carries `dimensions` and a mismatch is refused rather than queued for re-embedding.
1334
+ */
1335
+ export declare const EMBEDDING_DIMENSIONS = 1536;
1336
+ export declare const KNOWLEDGE_SOURCE_TYPES: readonly ["file", "artifact", "message", "external"];
1337
+ export type KnowledgeSourceType = (typeof KNOWLEDGE_SOURCE_TYPES)[number];
1338
+ /**
1339
+ * Which embedding produced a vector.
1340
+ *
1341
+ * Recorded per chunk, not per deployment. A model change must be *detectable* — and it is detectable only if
1342
+ * an old chunk still says what produced it. A single global "current model" setting cannot tell you which rows
1343
+ * are stale, which makes incremental re-indexing impossible.
1344
+ */
1345
+ export type EmbeddingModelRef = {
1346
+ readonly modelId: string;
1347
+ /** Bumped when the same model id starts producing different vectors. Providers do this silently. */
1348
+ readonly version: string;
1349
+ readonly dimensions: number;
1350
+ };
1351
+ export type KnowledgeChunk = {
1352
+ readonly id: string;
1353
+ readonly sourceType: KnowledgeSourceType;
1354
+ /** The file, artifact or message this came from. */
1355
+ readonly sourceId: string;
1356
+ /** Position within the source, so neighbouring chunks can be fetched to widen context. */
1357
+ readonly chunkIndex: number;
1358
+ readonly content: string;
1359
+ readonly tokenCount: number;
1360
+ /**
1361
+ * Who may see this chunk, as an opaque subject the authorization engine understands.
1362
+ *
1363
+ * A conversation id for an attachment, a workspace id for shared knowledge. Opaque here on purpose: this
1364
+ * port must not know the permission model, only that a search is scoped to a set of subjects.
1365
+ */
1366
+ readonly authSubject: string;
1367
+ readonly embeddingModel: EmbeddingModelRef;
1368
+ /** Where in the source this came from, for a citation that resolves. */
1369
+ readonly locator?: string;
1370
+ readonly createdAt: string;
1371
+ };
1372
+ /** A chunk plus its vector, for writing. Read paths never return the vector — nobody above needs it. */
1373
+ export type KnowledgeChunkWithEmbedding = KnowledgeChunk & {
1374
+ readonly embedding: readonly number[];
1375
+ };
1376
+ export interface KnowledgeStore {
1377
+ /**
1378
+ * Replaces a source's chunks with a new set, atomically from a reader's point of view.
1379
+ *
1380
+ * Replace rather than append, because re-indexing a changed document must not leave its old chunks
1381
+ * searchable — a stale chunk is a citation pointing at text that is no longer there. The whole source at
1382
+ * once, because a partial replacement is a document that is half old and half new, and no reader can tell.
1383
+ */
1384
+ replaceSource(input: TenantScope & {
1385
+ sourceType: KnowledgeSourceType;
1386
+ sourceId: string;
1387
+ chunks: readonly KnowledgeChunkWithEmbedding[];
1388
+ }): Promise<{
1389
+ readonly written: number;
1390
+ readonly removed: number;
1391
+ }>;
1392
+ /** A source's chunks in order, for reading around a hit or for re-indexing. */
1393
+ listBySource(input: TenantScope & PageRequest & {
1394
+ sourceType: KnowledgeSourceType;
1395
+ sourceId: string;
1396
+ }): Promise<Page<KnowledgeChunk>>;
1397
+ get(input: TenantScope & {
1398
+ id: string;
1399
+ }): Promise<KnowledgeChunk | null>;
1400
+ /** Removes a source's chunks. For a deleted document: its content must stop being searchable. */
1401
+ deleteSource(input: TenantScope & {
1402
+ sourceType: KnowledgeSourceType;
1403
+ sourceId: string;
1404
+ }): Promise<{
1405
+ readonly removed: number;
1406
+ }>;
1407
+ /**
1408
+ * Sources whose chunks were embedded by anything other than `current` — AC-5.
1409
+ *
1410
+ * The whole basis of incremental re-indexing: the work list is derived from what is *stored*, so an
1411
+ * interrupted re-index resumes by asking again rather than by remembering where it was.
1412
+ */
1413
+ listStaleSources(input: TenantScope & PageRequest & {
1414
+ current: EmbeddingModelRef;
1415
+ }): Promise<Page<{
1416
+ readonly sourceType: KnowledgeSourceType;
1417
+ readonly sourceId: string;
1418
+ readonly chunkCount: number;
1419
+ }>>;
1420
+ }
1421
+ export type VectorSearchHit = {
1422
+ readonly chunk: KnowledgeChunk;
1423
+ /**
1424
+ * 0–1, higher is closer. Normalised so a caller never has to know the metric.
1425
+ *
1426
+ * **0.5 means unrelated**, not "no match". The mapping is `(cosine + 1) / 2`, so orthogonal vectors — two
1427
+ * texts with nothing in common — score exactly 0.5, and only actively *opposite* vectors score below it.
1428
+ * That is why a vector index with no `minScore` returns every chunk it is asked for: there is no such thing
1429
+ * as a non-match, only a distant one. Any caller that needs "found nothing" must supply a floor above 0.5,
1430
+ * which is what `SEMANTIC_RELEVANCE_FLOOR` is for.
1431
+ */
1432
+ readonly score: number;
1433
+ };
1434
+ /**
1435
+ * The floor above which a vector hit is worth having (#136).
1436
+ *
1437
+ * 0.5 is orthogonal — see `VectorSearchHit.score` — so anything at or below it shares nothing with the query.
1438
+ * 0.55 is comfortably above that and still admits a weak-but-real match. Without a floor, retrieval always
1439
+ * returns *something*, and a model handed the least-bad chunk cites it: this constant is what makes an honest
1440
+ * empty result possible at all.
1441
+ *
1442
+ * Deliberately absolute, unlike the fusion floor which is relative to the best hit. They answer different
1443
+ * questions — "is this hit any good" and "is this hit much worse than the best" — and one instrument cannot do
1444
+ * both: a relative floor can never reject a result set that is uniformly poor, because something is always the
1445
+ * best of it.
1446
+ */
1447
+ export declare const SEMANTIC_RELEVANCE_FLOOR = 0.55;
1448
+ export interface VectorIndex {
1449
+ /**
1450
+ * Nearest neighbours, filtered by permission **inside** the query.
1451
+ *
1452
+ * `authSubjects` is required and not optional. An optional filter is a filter someone omits, and the day it
1453
+ * is omitted every tenant member can retrieve every chunk. An empty array means "no subjects", which
1454
+ * correctly returns nothing rather than everything.
1455
+ */
1456
+ search(input: TenantScope & {
1457
+ embedding: readonly number[];
1458
+ authSubjects: readonly string[];
1459
+ limit: number;
1460
+ /** Below this, a hit is not worth returning. Prevents a query with no good answer returning noise. */
1461
+ minScore?: number;
1462
+ sourceTypes?: readonly KnowledgeSourceType[];
1463
+ }): Promise<readonly VectorSearchHit[]>;
1464
+ }
1465
+ /**
1466
+ * Words a keyword query drops (#136).
1467
+ *
1468
+ * On the port, like `EMBEDDING_DIMENSIONS`, because both adapters need the same list and a copy in each is a
1469
+ * copy that drifts.
1470
+ *
1471
+ * **Why the query and not the index.** Postgres's `simple` text configuration is chosen deliberately over
1472
+ * `english`: `english` stems, and stemming is exactly what destroys `ERR-4021` and `Q3-2026` — the terms
1473
+ * keyword retrieval exists to find. But `simple` also keeps stopwords, so `was the site down` matches whichever
1474
+ * document says `the` most often. Found by measuring hybrid against semantic-only, where a decoy sharing only
1475
+ * `was` and `the` outranked the document that actually answered the question.
1476
+ *
1477
+ * Stripping them from the *query* keeps identifiers intact in the index while removing the terms that carry no
1478
+ * retrieval signal. Small and English-only, which is honest: a deployment in another language needs its own
1479
+ * list, and pretending otherwise would be worse than the list being visibly incomplete.
1480
+ */
1481
+ export declare const KEYWORD_STOPWORDS: ReadonlySet<string>;
1482
+ /**
1483
+ * A keyword query with its stopwords removed.
1484
+ *
1485
+ * Returns the empty string when nothing survives, which callers treat as "no query" — a search for `the` is a
1486
+ * search for nothing, and returning every document would be the worst possible answer.
1487
+ */
1488
+ export declare const stripStopwords: (query: string) => string;
1489
+ export type KeywordSearchHit = {
1490
+ readonly chunk: KnowledgeChunk;
1491
+ /**
1492
+ * 0–1, higher is a better lexical match.
1493
+ *
1494
+ * Normalised so a caller fusing this with a vector score does not have to know that one is a cosine
1495
+ * similarity and the other a `ts_rank_cd` — two unbounded, incomparable scales fused directly would let
1496
+ * whichever happened to be larger dominate.
1497
+ */
1498
+ readonly score: number;
1499
+ };
1500
+ /**
1501
+ * Exact-term retrieval over the same chunks the vector index searches (#136).
1502
+ *
1503
+ * The same rows on purpose: semantic and keyword retrieval share **one permission-filtered source of truth**,
1504
+ * so a chunk cannot be visible to one signal and invisible to the other. Two indexes over two copies would
1505
+ * eventually disagree about what exists, and the disagreement would be a permission gap.
1506
+ *
1507
+ * It exists because semantic search misses what it was never trained on — a product code, an error number, a
1508
+ * campaign identifier. An embedding of `ERR-4021` is an embedding of a string that looks like other strings.
1509
+ */
1510
+ export interface KeywordIndex {
1511
+ /**
1512
+ * Lexical search, filtered by permission **inside** the query.
1513
+ *
1514
+ * `authSubjects` is required for the same reason it is on `VectorIndex`: an optional filter is one someone
1515
+ * omits, and an excluded chunk must not merely be absent from the results — it must never have been a
1516
+ * candidate, or its presence still shows in a count.
1517
+ */
1518
+ search(input: TenantScope & {
1519
+ /** The user's terms, as typed. Parsed by the adapter, never interpolated into SQL. */
1520
+ query: string;
1521
+ authSubjects: readonly string[];
1522
+ limit: number;
1523
+ minScore?: number;
1524
+ sourceTypes?: readonly KnowledgeSourceType[];
1525
+ }): Promise<readonly KeywordSearchHit[]>;
1526
+ }
1527
+ /**
1528
+ * Content-addressable blob storage for spilled tool output (`docs/03` → Tool results). A large
1529
+ * result is offloaded here and referenced by an authorized `BlobRef`, read back via
1530
+ * `read_tool_output`. Tenant-scoped so a ref from one tenant can never resolve another's bytes.
1531
+ */
1532
+ export interface BlobStore {
1533
+ put(input: TenantScope & {
1534
+ value: unknown;
1535
+ }): Promise<BlobRef>;
1536
+ get(input: TenantScope & {
1537
+ ref: BlobRef;
1538
+ }): Promise<unknown | null>;
1539
+ }
1540
+ /**
1541
+ * Neutral unit of work, so a transaction-dependent workflow does not hardcode a
1542
+ * database. Adapters that cannot provide transactions must not advertise the
1543
+ * capability — startup validation fails loudly instead of degrading silently.
1544
+ */
1545
+ export interface UnitOfWork {
1546
+ run<T>(fn: () => Promise<T>): Promise<T>;
1547
+ }
1548
+ export declare const ADAPTER_CAPABILITIES: readonly ["transactions", "row-level-security", "full-text-search", "vector-search", "realtime", "distributed-locking", "durable-jobs"];
1549
+ export type AdapterCapability = (typeof ADAPTER_CAPABILITIES)[number];
1550
+ export interface CapabilityAware {
1551
+ capabilities(): readonly AdapterCapability[];
1552
+ }
1553
+ /**
1554
+ * Stored flow and team definitions, **immutable per version**.
1555
+ *
1556
+ * `put` writes a new version rather than overwriting one, and `get` takes a version. That is not
1557
+ * over-engineering: an execution pins the version it started with and reads it for its whole life, so a
1558
+ * definition that could change under a running execution would change the shape of an automation halfway
1559
+ * through — and the person who edited step 4 has no idea an execution is sitting at step 3.
1560
+ *
1561
+ * `latest` exists for *starting* something new, which is the only moment "the current version" is the right
1562
+ * question.
1563
+ */
1564
+ export interface FlowDefinitionStore {
1565
+ /** Refuses to overwrite an existing (id, version) — a version that changed is not a version. */
1566
+ put(input: TenantScope & {
1567
+ definition: StoredFlowDefinition;
1568
+ }): Promise<void>;
1569
+ get(input: TenantScope & {
1570
+ flowId: string;
1571
+ version: number;
1572
+ }): Promise<StoredFlowDefinition | null>;
1573
+ latest(input: TenantScope & {
1574
+ flowId: string;
1575
+ }): Promise<StoredFlowDefinition | null>;
1576
+ list(input: TenantScope & PageRequest): Promise<Page<StoredFlowDefinition>>;
1577
+ }
1578
+ /**
1579
+ * The definition as stored: the shape is the flows layer's, kept opaque here.
1580
+ *
1581
+ * `unknown` rather than an import, deliberately — `persistence` is a ports layer and importing `flows` would make
1582
+ * the storage contract depend on the interpreter's types. An adapter stores and returns JSON; the flows layer
1583
+ * parses it. The same reasoning as `SessionState.data`.
1584
+ */
1585
+ export type StoredFlowDefinition = {
1586
+ readonly flowId: string;
1587
+ readonly version: number;
1588
+ readonly name: string;
1589
+ readonly kind: "flow" | "team";
1590
+ readonly definition: unknown;
1591
+ readonly createdAt: string;
1592
+ readonly createdBy?: PrincipalId;
1593
+ };
1594
+ /**
1595
+ * A running or finished flow execution.
1596
+ *
1597
+ * `save` is a full write of the execution document rather than a patch, because the interpreter produces a whole
1598
+ * next state and a patch would need the two to agree about which fields it touched — a second contract, kept in
1599
+ * agreement by hand.
1600
+ */
1601
+ export interface FlowExecutionStore {
1602
+ create(input: TenantScope & {
1603
+ execution: StoredFlowExecution;
1604
+ }): Promise<void>;
1605
+ /**
1606
+ * Overwrite the execution.
1607
+ *
1608
+ * **Monotonic on `steps`**, like `CheckpointStore.save`: a save carrying fewer completed steps than the stored
1609
+ * one is ignored. Two workers that both picked up the same execution would otherwise let the slower one move it
1610
+ * backwards, and a flow that goes backwards re-performs external writes.
1611
+ */
1612
+ save(input: TenantScope & {
1613
+ execution: StoredFlowExecution;
1614
+ }): Promise<void>;
1615
+ get(input: TenantScope & {
1616
+ executionId: string;
1617
+ }): Promise<StoredFlowExecution | null>;
1618
+ /** Executions parked on a signal, so a delivered signal can find what was waiting for it. */
1619
+ waitingOnSignal(input: TenantScope & {
1620
+ signal: string;
1621
+ limit?: number;
1622
+ }): Promise<readonly StoredFlowExecution[]>;
1623
+ /**
1624
+ * The execution parked on a child run — #202.
1625
+ *
1626
+ * One at most: a run belongs to a single flow step. Returning `null` rather than a list says so, and a caller
1627
+ * that got a list would have to decide what two parents for one child means.
1628
+ */
1629
+ waitingOnRun(input: TenantScope & {
1630
+ runId: RunId;
1631
+ }): Promise<StoredFlowExecution | null>;
1632
+ /** For the inspector: what has this flow been doing? #187 AC-7. */
1633
+ listByFlow(input: TenantScope & PageRequest & {
1634
+ flowId: string;
1635
+ }): Promise<Page<StoredFlowExecution>>;
1636
+ }
1637
+ export type StoredFlowExecution = {
1638
+ readonly id: string;
1639
+ readonly flowId: string;
1640
+ readonly flowVersion: number;
1641
+ readonly runId: RunId;
1642
+ readonly status: string;
1643
+ readonly currentStep: string | null;
1644
+ /** How many steps have completed. The monotonic guard reads this. */
1645
+ readonly steps: number;
1646
+ /** The whole execution document, as the flows layer wrote it. */
1647
+ readonly execution: unknown;
1648
+ readonly waitingSignal?: string;
1649
+ /** The child run this execution is parked on — #202. Indexed, because a settled run has to find its parent. */
1650
+ readonly waitingRunId?: RunId;
1651
+ readonly startedAt: string;
1652
+ readonly finishedAt?: string;
1653
+ };
1654
+ //# sourceMappingURL=index.d.ts.map