@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,221 @@
1
+ /**
2
+ * Human-in-the-loop services — `docs/04-durable-runtime-and-hitl.md` → Questions & Approvals.
3
+ *
4
+ * `ask`/`request` persist a durable interaction (surviving restart/deploy) and pause the run into
5
+ * `waiting-for-question` / `waiting-for-approval`. `answer`/`decide` record the outcome idempotently
6
+ * and queue the continuation exactly once — a duplicate call is a safe no-op, so a run never resumes
7
+ * twice. An approval stores the exact normalized tool name + input. The `ApprovalGate` makes the
8
+ * approval unbypassable: a policy-classified tool cannot execute directly without either a standing
9
+ * grant or a one-time approval a human has decided and the runtime has claimed.
10
+ *
11
+ * **This file is the *what*, not the *when*.** Requesting, deciding and gating live here; the run path
12
+ * that raises an approval on a refusal and executes the stored input on resumption is
13
+ * `./approved-execution.ts`, and the engine calls it (`../agents/engine.ts`). That path did not
14
+ * exist until the loop was wired, and this docstring described a resumption nothing performed.
15
+ */
16
+ import type { ExecutionContext } from "../core/context.js";
17
+ import type { QuestionAnswer } from "../core/content-parts.js";
18
+ import type { ConversationId, InteractionId, RunId, TenantId } from "../core/ids.js";
19
+ import { AgentPlatformError } from "../core/errors.js";
20
+ import type { ApprovalGrantStore, InteractionStore, RunStore } from "../persistence/index.js";
21
+ import type { JobDispatcher } from "../runtime/index.js";
22
+ import type { ApprovalDecision, ApprovalGrant, PendingApproval, PendingQuestion } from "./index.js";
23
+ type Clock = () => string;
24
+ type IdFactory = () => string;
25
+ /**
26
+ * One question the agent asks a person.
27
+ *
28
+ * `options` + `multiple` + `allowOther` cover the three shapes a UI actually needs: pick one, pick several, or
29
+ * write your own — and any combination. Previously only `options` existed, so a client could not tell a choice
30
+ * from a hint and had to guess.
31
+ */
32
+ export type QuestionSpec = {
33
+ readonly key: string;
34
+ readonly prompt: string;
35
+ /** Suggested answers. Absent means free text only. */
36
+ readonly options?: readonly string[];
37
+ /** More than one may be chosen. A multi-select answer is an array. */
38
+ readonly multiple?: boolean;
39
+ /** An answer not on the list is accepted. Defaults to true when there are no options, false when there are. */
40
+ readonly allowOther?: boolean;
41
+ };
42
+ /** Re-exported from `core`, where it lives so the port and this service can share it without a cycle. */
43
+ export type { QuestionAnswer } from "../core/content-parts.js";
44
+ export declare const createQuestionService: (deps: {
45
+ readonly interactions: InteractionStore;
46
+ readonly dispatcher: JobDispatcher;
47
+ /**
48
+ * The run store, so answering can put the run back to `queued` (#144).
49
+ *
50
+ * Optional only so an existing caller keeps compiling; without it the resume enqueues a run the worker cannot
51
+ * claim, and the run waits forever. See `resumeRun` below.
52
+ */
53
+ readonly runs?: RunStore;
54
+ readonly clock?: Clock;
55
+ readonly idFactory?: IdFactory;
56
+ }) => {
57
+ /**
58
+ * Persist a pending question. The run is paused into `waiting-for-question` by the worker.
59
+ *
60
+ * That last sentence was aspirational until #163: the worker does pause on a `question.requested` event,
61
+ * but nothing emitted one, so a tool that called `ask` had its question stored and the run ran on to
62
+ * completion. See `questionPending` below for the signal that closes the loop.
63
+ */
64
+ ask(context: ExecutionContext, runId: RunId, questions: readonly QuestionSpec[]): Promise<PendingQuestion>;
65
+ /**
66
+ * The question a run is parked on, or null — the read side of `answer` (#163).
67
+ *
68
+ * The service had `ask` and `answer` and no way to *look at* what was asked, so the only client that could
69
+ * render a picker was one that had also raised the question and kept it in memory. A worker raises it and a
70
+ * browser renders it, which are different processes; the browser had the interaction id from the event and
71
+ * nothing else, and drew an empty text box.
72
+ */
73
+ pending(input: {
74
+ tenantId: TenantId;
75
+ runId: RunId;
76
+ }): Promise<PendingQuestion | null>;
77
+ /**
78
+ * The answer a resumed run has to tell the model about — #163.
79
+ *
80
+ * Returns null when nothing was asked, or when the answer was already part of an earlier turn's history:
81
+ * both mean "say nothing new". The engine calls this on resume, the way it calls `approvals.resume`.
82
+ */
83
+ answered(input: {
84
+ tenantId: TenantId;
85
+ runId: RunId;
86
+ }): Promise<PendingQuestion | null>;
87
+ /**
88
+ * Record answers and queue the continuation — exactly once. A second answer to the same
89
+ * interaction is a no-op and does NOT re-enqueue, so the run never resumes twice.
90
+ */
91
+ answer(input: TenantScopeInput & {
92
+ interactionId: InteractionId;
93
+ runId: RunId;
94
+ answers: Readonly<Record<string, QuestionAnswer>>;
95
+ }): Promise<{
96
+ resumed: boolean;
97
+ }>;
98
+ };
99
+ type TenantScopeInput = {
100
+ tenantId: TenantId;
101
+ };
102
+ export type ApprovalRequest = {
103
+ readonly toolName: string;
104
+ readonly normalizedInput: unknown;
105
+ readonly riskCategory: string;
106
+ readonly summary: string;
107
+ readonly estimatedCostMinorUnits?: number;
108
+ readonly expiresAt: string;
109
+ readonly idempotencyKey: string;
110
+ };
111
+ /**
112
+ * Decisions that authorize the approved call to run.
113
+ *
114
+ * A whitelist rather than `!== "deny"`: a decision added later — a deferral, an escalation — would
115
+ * read as permission under the negative form, which is the wrong direction to be wrong in.
116
+ */
117
+ export declare const ALLOW_DECISIONS: readonly ["allow-once", "allow-conversation", "allow-always"];
118
+ export declare const isAllowDecision: (decision: ApprovalDecision | undefined) => boolean;
119
+ export declare const createApprovalService: (deps: {
120
+ readonly interactions: InteractionStore;
121
+ readonly grants: ApprovalGrantStore;
122
+ readonly dispatcher: JobDispatcher;
123
+ /** As on `createQuestionService`: without it the resumed run is never claimable. See `resumeRun`. */
124
+ readonly runs?: RunStore;
125
+ readonly clock?: Clock;
126
+ readonly idFactory?: IdFactory;
127
+ }) => {
128
+ /** Persist a pending approval storing the exact normalized tool + input. Run pauses to waiting. */
129
+ request(context: ExecutionContext, runId: RunId, req: ApprovalRequest): Promise<PendingApproval>;
130
+ /**
131
+ * The approval a run is parked on, or null — the read side of `decide` (#163).
132
+ *
133
+ * Same reasoning as `pending` on the question service: the worker raises it, a browser renders it, and the
134
+ * browser had only the interaction id from the event. Without this the card could not name the tool it was
135
+ * asking about, and an approval prompt that says "run a tool?" is one people learn to click through.
136
+ */
137
+ pending(input: {
138
+ tenantId: TenantId;
139
+ runId: RunId;
140
+ }): Promise<PendingApproval | null>;
141
+ /**
142
+ * Record a decision (once), issue a standing grant for allow-conversation/allow-always, and queue
143
+ * the continuation exactly once. The resumed run executes the *stored* normalized input from the
144
+ * pending approval — never a regenerated one; see `./approved-execution.ts`.
145
+ *
146
+ * `allow-once` deliberately issues **no grant**. A grant is standing by definition, so minting one
147
+ * for a one-time decision would hand over authority the human did not give. Its single execution is
148
+ * claimed off the interaction instead (`InteractionStore.claimApproval`).
149
+ */
150
+ decide(input: TenantScopeInput & {
151
+ interactionId: InteractionId;
152
+ runId: RunId;
153
+ conversationId?: ConversationId;
154
+ decision: ApprovalDecision;
155
+ }): Promise<{
156
+ resumed: boolean;
157
+ grant?: ApprovalGrant;
158
+ }>;
159
+ };
160
+ /**
161
+ * A single approved execution, presented at the moment of the call.
162
+ *
163
+ * The ticket is just the interaction id, and it is **not** a credential — the gate below verifies it
164
+ * against the stored interaction rather than believing it. That is deliberate: the alternative to a
165
+ * ticket was issuing a grant for `allow-once`, and a grant is standing by definition, so a one-time
166
+ * decision would have silently become a permanent one.
167
+ *
168
+ * Per-call rather than carried on the `ExecutionContext`: a context is reused for every call in a
169
+ * turn, so an approval living on it would authorise all of them.
170
+ */
171
+ export type OneTimeApproval = {
172
+ readonly interactionId: InteractionId | string;
173
+ };
174
+ /**
175
+ * The gate that makes approval unbypassable. A policy-classified tool (`approvalPolicy` other than
176
+ * `never`, or an external/destructive effect under `policy`) may only execute directly when a
177
+ * standing grant covers it, or when the call presents a one-time approval a human has decided and the
178
+ * runtime has claimed.
179
+ *
180
+ * Both paths fail closed. A tool with no grant and no ticket is refused; a ticket presented with no
181
+ * `interactions` store to check it against is refused too, because an unwired dependency must never
182
+ * be the reason something was allowed.
183
+ */
184
+ export declare const createApprovalGate: (deps: {
185
+ readonly grants: ApprovalGrantStore;
186
+ /**
187
+ * Where one-time approvals are verified. Optional so a caller that only uses standing grants need
188
+ * not wire it — with it absent, every ticket is refused rather than trusted.
189
+ */
190
+ readonly interactions?: InteractionStore;
191
+ readonly clock?: Clock;
192
+ }) => {
193
+ isAllowed(context: ExecutionContext, tool: {
194
+ readonly name: string;
195
+ readonly category: string;
196
+ readonly approvalPolicy: "never" | "policy" | "always";
197
+ }, oneTime?: OneTimeApproval): Promise<boolean>;
198
+ };
199
+ export type ApprovalGate = ReturnType<typeof createApprovalGate>;
200
+ /**
201
+ * The signal a tool raises to park its run on a question — #163.
202
+ *
203
+ * A tool cannot pause a run by itself, and it must not block waiting for a person: a delegate that awaited a
204
+ * human reply would hold a worker slot for as long as someone takes to read, which is the entire thing the
205
+ * durable runtime exists to avoid. So the tool stores the question, throws this, and returns control.
206
+ *
207
+ * The engine recognises the `question_pending` code, tells the model the run is parked, and emits
208
+ * `question.requested` — the event the worker turns into `waiting-for-question`. Exactly the path an approval
209
+ * takes, which is the point: the two halves of "ask a human something" should not work differently.
210
+ *
211
+ * Thrown rather than returned because it has to survive the tool registry, which flattens a delegate's throw
212
+ * into a `ToolResult` error. A distinguishing code is what makes that flattening lossless.
213
+ */
214
+ export declare const questionPending: (question: {
215
+ readonly id: InteractionId;
216
+ }) => AgentPlatformError;
217
+ /** True when a refusal is a parked question rather than a failure. Used by the engine; exported for hosts. */
218
+ export declare const isQuestionPending: (error: {
219
+ readonly code: string;
220
+ }) => boolean;
221
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Human-in-the-loop services — `docs/04-durable-runtime-and-hitl.md` → Questions & Approvals.
3
+ *
4
+ * `ask`/`request` persist a durable interaction (surviving restart/deploy) and pause the run into
5
+ * `waiting-for-question` / `waiting-for-approval`. `answer`/`decide` record the outcome idempotently
6
+ * and queue the continuation exactly once — a duplicate call is a safe no-op, so a run never resumes
7
+ * twice. An approval stores the exact normalized tool name + input. The `ApprovalGate` makes the
8
+ * approval unbypassable: a policy-classified tool cannot execute directly without either a standing
9
+ * grant or a one-time approval a human has decided and the runtime has claimed.
10
+ *
11
+ * **This file is the *what*, not the *when*.** Requesting, deciding and gating live here; the run path
12
+ * that raises an approval on a refusal and executes the stored input on resumption is
13
+ * `./approved-execution.ts`, and the engine calls it (`../agents/engine.ts`). That path did not
14
+ * exist until the loop was wired, and this docstring described a resumption nothing performed.
15
+ */
16
+ import { asId } from "../core/ids.js";
17
+ import { AgentPlatformError } from "../core/errors.js";
18
+ const defaults = (clock, idFactory) => ({
19
+ clock: clock ?? (() => new Date().toISOString()),
20
+ newId: idFactory ?? (() => `int-${Math.round(Math.random() * 1e9)}`),
21
+ });
22
+ export const createQuestionService = (deps) => {
23
+ const { clock, newId } = defaults(deps.clock, deps.idFactory);
24
+ return {
25
+ /**
26
+ * Persist a pending question. The run is paused into `waiting-for-question` by the worker.
27
+ *
28
+ * That last sentence was aspirational until #163: the worker does pause on a `question.requested` event,
29
+ * but nothing emitted one, so a tool that called `ask` had its question stored and the run ran on to
30
+ * completion. See `questionPending` below for the signal that closes the loop.
31
+ */
32
+ async ask(context, runId, questions) {
33
+ const question = {
34
+ id: asId(newId()),
35
+ tenantId: context.tenantId,
36
+ runId,
37
+ questions,
38
+ createdAt: clock(),
39
+ };
40
+ await deps.interactions.createQuestion({ tenantId: context.tenantId, question });
41
+ return question;
42
+ },
43
+ /**
44
+ * The question a run is parked on, or null — the read side of `answer` (#163).
45
+ *
46
+ * The service had `ask` and `answer` and no way to *look at* what was asked, so the only client that could
47
+ * render a picker was one that had also raised the question and kept it in memory. A worker raises it and a
48
+ * browser renders it, which are different processes; the browser had the interaction id from the event and
49
+ * nothing else, and drew an empty text box.
50
+ */
51
+ async pending(input) {
52
+ return deps.interactions.findPendingQuestion({ tenantId: input.tenantId, runId: input.runId });
53
+ },
54
+ /**
55
+ * The answer a resumed run has to tell the model about — #163.
56
+ *
57
+ * Returns null when nothing was asked, or when the answer was already part of an earlier turn's history:
58
+ * both mean "say nothing new". The engine calls this on resume, the way it calls `approvals.resume`.
59
+ */
60
+ async answered(input) {
61
+ return deps.interactions.findAnsweredQuestion({ tenantId: input.tenantId, runId: input.runId });
62
+ },
63
+ /**
64
+ * Record answers and queue the continuation — exactly once. A second answer to the same
65
+ * interaction is a no-op and does NOT re-enqueue, so the run never resumes twice.
66
+ */
67
+ async answer(input) {
68
+ const { alreadyResolved } = await deps.interactions.answerQuestion({
69
+ tenantId: input.tenantId,
70
+ interactionId: input.interactionId,
71
+ answers: input.answers,
72
+ at: clock(),
73
+ });
74
+ if (alreadyResolved)
75
+ return { resumed: false };
76
+ await resumeRun(deps, { tenantId: input.tenantId, runId: input.runId, at: clock() });
77
+ return { resumed: true };
78
+ },
79
+ };
80
+ };
81
+ /**
82
+ * Put a paused run back on the queue — status *then* job.
83
+ *
84
+ * **A bug the load harness found (#144).** `decide` and `answer` used to enqueue and nothing else. But
85
+ * `RunStore.claim` accepts only `queued` or a `running` run with an expired lease, and pausing a run for a human
86
+ * leaves it in `waiting-for-approval` — so the enqueued job was handed to a worker, the claim matched no row, the
87
+ * run was skipped, and it waited forever. Sixty-five of a hundred and sixty runs in one load step, silently.
88
+ *
89
+ * It survived because the unit tests assert `resumed: true` and that a job was enqueued, which was exactly true
90
+ * and not the same as the run resuming. Only driving a real worker against a real store showed it.
91
+ *
92
+ * The transition comes **first**: enqueueing before it lets a worker pick the job up while the run is still
93
+ * paused, fail the claim, and drop the only job that would have resumed it.
94
+ *
95
+ * `workerId` is the actor's name rather than a worker's. `transition` guards on `claimedBy`, and pausing a run
96
+ * releases the claim — so the field is unconstrained here and the honest value is who is doing this.
97
+ */
98
+ const resumeRun = async (deps, input) => {
99
+ await deps.runs?.transition({
100
+ tenantId: input.tenantId,
101
+ id: input.runId,
102
+ workerId: "hitl",
103
+ to: "queued",
104
+ now: input.at,
105
+ });
106
+ await deps.dispatcher.enqueueRun({ tenantId: input.tenantId, runId: input.runId });
107
+ };
108
+ /**
109
+ * Decisions that authorize the approved call to run.
110
+ *
111
+ * A whitelist rather than `!== "deny"`: a decision added later — a deferral, an escalation — would
112
+ * read as permission under the negative form, which is the wrong direction to be wrong in.
113
+ */
114
+ export const ALLOW_DECISIONS = ["allow-once", "allow-conversation", "allow-always"];
115
+ export const isAllowDecision = (decision) => decision !== undefined && ALLOW_DECISIONS.includes(decision);
116
+ const grantScopeFor = (decision) => decision === "allow-conversation" ? "conversation" : decision === "allow-always" ? "tenant" : null;
117
+ export const createApprovalService = (deps) => {
118
+ const { clock, newId } = defaults(deps.clock, deps.idFactory);
119
+ return {
120
+ /** Persist a pending approval storing the exact normalized tool + input. Run pauses to waiting. */
121
+ async request(context, runId, req) {
122
+ const approval = {
123
+ id: asId(newId()),
124
+ tenantId: context.tenantId,
125
+ runId,
126
+ toolName: req.toolName,
127
+ normalizedInput: req.normalizedInput,
128
+ riskCategory: req.riskCategory,
129
+ summary: req.summary,
130
+ ...(req.estimatedCostMinorUnits === undefined ? {} : { estimatedCostMinorUnits: req.estimatedCostMinorUnits }),
131
+ expiresAt: req.expiresAt,
132
+ idempotencyKey: req.idempotencyKey,
133
+ };
134
+ await deps.interactions.createApproval({ tenantId: context.tenantId, approval });
135
+ return approval;
136
+ },
137
+ /**
138
+ * The approval a run is parked on, or null — the read side of `decide` (#163).
139
+ *
140
+ * Same reasoning as `pending` on the question service: the worker raises it, a browser renders it, and the
141
+ * browser had only the interaction id from the event. Without this the card could not name the tool it was
142
+ * asking about, and an approval prompt that says "run a tool?" is one people learn to click through.
143
+ */
144
+ async pending(input) {
145
+ return deps.interactions.findPendingApproval({ tenantId: input.tenantId, runId: input.runId });
146
+ },
147
+ /**
148
+ * Record a decision (once), issue a standing grant for allow-conversation/allow-always, and queue
149
+ * the continuation exactly once. The resumed run executes the *stored* normalized input from the
150
+ * pending approval — never a regenerated one; see `./approved-execution.ts`.
151
+ *
152
+ * `allow-once` deliberately issues **no grant**. A grant is standing by definition, so minting one
153
+ * for a one-time decision would hand over authority the human did not give. Its single execution is
154
+ * claimed off the interaction instead (`InteractionStore.claimApproval`).
155
+ */
156
+ async decide(input) {
157
+ const { approval, alreadyResolved } = await deps.interactions.decideApproval({
158
+ tenantId: input.tenantId,
159
+ interactionId: input.interactionId,
160
+ decision: input.decision,
161
+ at: clock(),
162
+ });
163
+ if (alreadyResolved)
164
+ return { resumed: false };
165
+ let grant;
166
+ const scope = grantScopeFor(input.decision);
167
+ // A conversation-scoped grant needs a conversationId; without one, skip the standing grant
168
+ // (the run still resumes once) rather than silently widening it to the whole tenant.
169
+ if (scope && !(scope === "conversation" && input.conversationId === undefined)) {
170
+ grant = {
171
+ id: asId(newId()),
172
+ tenantId: input.tenantId,
173
+ scope,
174
+ toolNameOrCategory: approval.toolName,
175
+ ...(scope === "conversation" && input.conversationId !== undefined ? { conversationId: input.conversationId } : {}),
176
+ grantedAt: clock(),
177
+ };
178
+ await deps.grants.grant({ tenantId: input.tenantId, grant });
179
+ }
180
+ // Both allow and deny queue a continuation; the resumed engine reads the decision and acts.
181
+ await resumeRun(deps, { tenantId: input.tenantId, runId: input.runId, at: clock() });
182
+ return grant ? { resumed: true, grant } : { resumed: true };
183
+ },
184
+ };
185
+ };
186
+ /**
187
+ * The gate that makes approval unbypassable. A policy-classified tool (`approvalPolicy` other than
188
+ * `never`, or an external/destructive effect under `policy`) may only execute directly when a
189
+ * standing grant covers it, or when the call presents a one-time approval a human has decided and the
190
+ * runtime has claimed.
191
+ *
192
+ * Both paths fail closed. A tool with no grant and no ticket is refused; a ticket presented with no
193
+ * `interactions` store to check it against is refused too, because an unwired dependency must never
194
+ * be the reason something was allowed.
195
+ */
196
+ export const createApprovalGate = (deps) => {
197
+ const clock = deps.clock ?? (() => new Date().toISOString());
198
+ /**
199
+ * Whether this ticket really authorises *this* call.
200
+ *
201
+ * Every clause is a way the loop could otherwise be widened, and each is checked against what was
202
+ * stored at request time rather than against anything the caller supplied:
203
+ *
204
+ * - the interaction exists in this tenant — a forged or foreign id authorises nothing;
205
+ * - it belongs to this run, so a ticket cannot be carried into another run;
206
+ * - the decision is an allow — a denial can never read as permission;
207
+ * - the tool is the one the human saw, so an approval for `publish` cannot run `delete`;
208
+ * - it has been claimed. The claim is the at-most-once counter (`InteractionStore.claimApproval`),
209
+ * and requiring it here is what keeps a merely *decided* approval from being executable by
210
+ * anything that has not first taken the single execution it grants.
211
+ */
212
+ const oneTimeAllows = async (context, tool, oneTime) => {
213
+ if (!deps.interactions)
214
+ return false;
215
+ const approval = await deps.interactions.findApproval({
216
+ tenantId: context.tenantId,
217
+ interactionId: asId(String(oneTime.interactionId)),
218
+ });
219
+ if (!approval)
220
+ return false;
221
+ if (approval.runId !== context.runId)
222
+ return false;
223
+ if (approval.toolName !== tool.name)
224
+ return false;
225
+ if (!isAllowDecision(approval.decision))
226
+ return false;
227
+ return approval.consumedAt !== undefined;
228
+ };
229
+ return {
230
+ async isAllowed(context, tool, oneTime) {
231
+ if (tool.approvalPolicy === "never")
232
+ return true;
233
+ const now = clock();
234
+ const conversationId = context.conversationId;
235
+ const scope = { tenantId: context.tenantId, now, ...(conversationId ? { conversationId } : {}) };
236
+ const byName = await deps.grants.findActive({ ...scope, toolNameOrCategory: tool.name });
237
+ if (byName)
238
+ return true;
239
+ const byCategory = await deps.grants.findActive({ ...scope, toolNameOrCategory: tool.category });
240
+ if (byCategory)
241
+ return true;
242
+ return oneTime === undefined ? false : oneTimeAllows(context, tool, oneTime);
243
+ },
244
+ };
245
+ };
246
+ /**
247
+ * The signal a tool raises to park its run on a question — #163.
248
+ *
249
+ * A tool cannot pause a run by itself, and it must not block waiting for a person: a delegate that awaited a
250
+ * human reply would hold a worker slot for as long as someone takes to read, which is the entire thing the
251
+ * durable runtime exists to avoid. So the tool stores the question, throws this, and returns control.
252
+ *
253
+ * The engine recognises the `question_pending` code, tells the model the run is parked, and emits
254
+ * `question.requested` — the event the worker turns into `waiting-for-question`. Exactly the path an approval
255
+ * takes, which is the point: the two halves of "ask a human something" should not work differently.
256
+ *
257
+ * Thrown rather than returned because it has to survive the tool registry, which flattens a delegate's throw
258
+ * into a `ToolResult` error. A distinguishing code is what makes that flattening lossless.
259
+ */
260
+ export const questionPending = (question) => new AgentPlatformError({
261
+ code: "question_pending",
262
+ message: "A question has been put to the person. The run is paused; do not retry or guess an answer.",
263
+ retryable: false,
264
+ details: { interactionId: question.id },
265
+ });
266
+ /** True when a refusal is a parked question rather than a failure. Used by the engine; exported for hosts. */
267
+ export const isQuestionPending = (error) => error.code === "question_pending";
268
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Idempotency contract — `docs/04-durable-runtime-and-hitl.md`. **Frozen v1.**
3
+ *
4
+ * Every external/destructive tool call carries a key derived from tenant + run + tool-call
5
+ * identity. A resumed or retried call returns the original result instead of repeating the side
6
+ * effect — the pairing that makes Claude-style retries safe.
7
+ */
8
+ import type { RunId, TenantId, ToolCallId } from "../core/ids.js";
9
+ declare const brand: unique symbol;
10
+ export type IdempotencyKey = string & {
11
+ readonly [brand]: "IdempotencyKey";
12
+ };
13
+ /**
14
+ * Pure and deterministic: the same (tenant, run, tool-call) always yields the same key, so a
15
+ * retry of the same logical call collides with its first attempt.
16
+ */
17
+ export declare const deriveIdempotencyKey: (input: {
18
+ readonly tenantId: TenantId;
19
+ readonly runId: RunId;
20
+ readonly toolCallId: ToolCallId;
21
+ }) => IdempotencyKey;
22
+ /**
23
+ * A stable string for a value.
24
+ *
25
+ * Object keys are sorted, because `{a, b}` and `{b, a}` are the same arguments and must not produce
26
+ * two keys — the whole point of a key is that an identical call collides with itself.
27
+ */
28
+ export declare const canonicalizeArgs: (value: unknown) => string;
29
+ /**
30
+ * The key for a call the runtime identifies by its **arguments** rather than by a provider tool-call
31
+ * id (`docs/04` → Approvals, Idempotency).
32
+ *
33
+ * A resumed approval has no tool-call id to derive from: the call it executes came off a stored
34
+ * interaction, not out of a model stream. Two properties matter, and they pull in opposite directions.
35
+ * It is **run-scoped**, so "publish this" today and the same call next week do not collide — a key
36
+ * shared across runs would return the first result and never publish the second. And it is
37
+ * **argument-derived**, so within one run the same logical call collides with itself: the model
38
+ * re-asking for a publish it already asked for finds the same approval and the same stored result,
39
+ * rather than a second approval and a second publish.
40
+ *
41
+ * Derive it from *normalised* arguments. A schema that lowercases a channel would otherwise give one
42
+ * logical call two keys, and the second would not see the first's result.
43
+ */
44
+ export declare const deriveCallIdempotencyKey: (input: {
45
+ readonly tenantId: TenantId;
46
+ readonly runId: RunId;
47
+ readonly toolName: string;
48
+ readonly args: unknown;
49
+ }) => IdempotencyKey;
50
+ /** Wraps a stored result so a repeated call can return it without re-executing. */
51
+ export type IdempotentResult<T> = {
52
+ readonly key: IdempotencyKey;
53
+ /** True on the first execution; false when the stored result is returned. */
54
+ readonly firstSeen: boolean;
55
+ readonly result: T;
56
+ };
57
+ /** Persists and returns prior results by key. Adapter lands with persistence (#17+). */
58
+ export interface IdempotencyStore {
59
+ get<T>(input: {
60
+ tenantId: TenantId;
61
+ key: IdempotencyKey;
62
+ }): Promise<IdempotentResult<T> | null>;
63
+ put<T>(input: {
64
+ tenantId: TenantId;
65
+ key: IdempotencyKey;
66
+ result: T;
67
+ }): Promise<void>;
68
+ }
69
+ export {};
70
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Idempotency contract — `docs/04-durable-runtime-and-hitl.md`. **Frozen v1.**
3
+ *
4
+ * Every external/destructive tool call carries a key derived from tenant + run + tool-call
5
+ * identity. A resumed or retried call returns the original result instead of repeating the side
6
+ * effect — the pairing that makes Claude-style retries safe.
7
+ */
8
+ const brand = Symbol("IdempotencyKey");
9
+ /**
10
+ * Pure and deterministic: the same (tenant, run, tool-call) always yields the same key, so a
11
+ * retry of the same logical call collides with its first attempt.
12
+ */
13
+ export const deriveIdempotencyKey = (input) => `${input.tenantId}:${input.runId}:${input.toolCallId}`;
14
+ /**
15
+ * A stable string for a value.
16
+ *
17
+ * Object keys are sorted, because `{a, b}` and `{b, a}` are the same arguments and must not produce
18
+ * two keys — the whole point of a key is that an identical call collides with itself.
19
+ */
20
+ export const canonicalizeArgs = (value) => {
21
+ if (value === null || typeof value !== "object")
22
+ return JSON.stringify(value) ?? "null";
23
+ if (Array.isArray(value))
24
+ return `[${value.map(canonicalizeArgs).join(",")}]`;
25
+ const entries = Object.entries(value)
26
+ .filter(([, v]) => v !== undefined)
27
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
28
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalizeArgs(v)}`).join(",")}}`;
29
+ };
30
+ /** FNV-1a over the canonical form. Bounds the key's length without needing a crypto dependency. */
31
+ const fnv1a = (text) => {
32
+ let hash = 0x811c9dc5;
33
+ for (let i = 0; i < text.length; i += 1) {
34
+ hash ^= text.charCodeAt(i);
35
+ hash = Math.imul(hash, 0x01000193) >>> 0;
36
+ }
37
+ return hash.toString(16).padStart(8, "0");
38
+ };
39
+ /**
40
+ * The key for a call the runtime identifies by its **arguments** rather than by a provider tool-call
41
+ * id (`docs/04` → Approvals, Idempotency).
42
+ *
43
+ * A resumed approval has no tool-call id to derive from: the call it executes came off a stored
44
+ * interaction, not out of a model stream. Two properties matter, and they pull in opposite directions.
45
+ * It is **run-scoped**, so "publish this" today and the same call next week do not collide — a key
46
+ * shared across runs would return the first result and never publish the second. And it is
47
+ * **argument-derived**, so within one run the same logical call collides with itself: the model
48
+ * re-asking for a publish it already asked for finds the same approval and the same stored result,
49
+ * rather than a second approval and a second publish.
50
+ *
51
+ * Derive it from *normalised* arguments. A schema that lowercases a channel would otherwise give one
52
+ * logical call two keys, and the second would not see the first's result.
53
+ */
54
+ export const deriveCallIdempotencyKey = (input) => deriveIdempotencyKey({
55
+ tenantId: input.tenantId,
56
+ runId: input.runId,
57
+ toolCallId: `${input.toolName}:${fnv1a(canonicalizeArgs(input.args))}`,
58
+ });
59
+ //# sourceMappingURL=index.js.map