@desplega.ai/agent-swarm 1.164.1 → 1.166.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 (328) hide show
  1. package/README.md +1 -1
  2. package/dist/acp-adapter-yzfxn8vj.js +23 -0
  3. package/dist/{actions-hp2h32dx.js → actions-qz3m692w.js} +24 -28
  4. package/dist/{amp-adapter-zfd7djyx.js → amp-adapter-j1xbmsqz.js} +7 -7
  5. package/dist/{app-w2d26h4f.js → app-43vrdmv8.js} +9 -10
  6. package/dist/{assistant-45pc4en7.js → assistant-ertffmka.js} +25 -29
  7. package/dist/{boot-reembed-k1q2s4fc.js → boot-reembed-998jtpdg.js} +12 -15
  8. package/dist/{boot-reembed-6rhcpsjf.js → boot-reembed-s7k7c6v2.js} +13 -15
  9. package/dist/boot-scrub-sweep-nepsk4t1.js +211 -0
  10. package/dist/{boot-seeds-ex93edx7.js → boot-seeds-vvzs1zhk.js} +12 -12
  11. package/dist/{claude-adapter-hcxd0szm.js → claude-adapter-2zvmekrp.js} +15 -17
  12. package/dist/{claude-managed-adapter-fz7xps2r.js → claude-managed-adapter-s49wbk8t.js} +5 -5
  13. package/dist/{claude-managed-setup-xx6f15h9.js → claude-managed-setup-cvhqq8qn.js} +4 -4
  14. package/dist/{claude-sdk-session-6nb9raem.js → claude-sdk-session-kkbxtw5q.js} +2624 -2606
  15. package/dist/{cli-etbrt8x1.js → cli-0nhdwxd5.js} +4 -4
  16. package/dist/{cli-jeefgppq.js → cli-0qjcsxgz.js} +7 -7
  17. package/dist/{cli-6behs2bt.js → cli-32bca7nr.js} +1 -1
  18. package/dist/{cli-gt8v96qx.js → cli-3ddybbbx.js} +4 -4
  19. package/dist/{acp-adapter-rxvek4ym.js → cli-3qn4r334.js} +317 -35
  20. package/dist/{cli-1d3w4xm3.js → cli-4r1sjfc4.js} +14 -6
  21. package/dist/{cli-691f2rxm.js → cli-66zh26sp.js} +68 -16
  22. package/dist/{cli-et8wdyqh.js → cli-68r1j4sg.js} +9 -9
  23. package/dist/{cli-xb7kyanp.js → cli-8kgvmkak.js} +3 -3
  24. package/dist/{cli-fdpkw8vk.js → cli-8t229cmr.js} +8 -8
  25. package/dist/{cli-gc54qc0p.js → cli-aqcey741.js} +1 -1
  26. package/dist/{cli-4q4cmr4t.js → cli-cpfmqz3n.js} +8 -8
  27. package/dist/{cli-a9bw0xxt.js → cli-df56tqdw.js} +997 -408
  28. package/dist/{cli-n7h1kqpf.js → cli-dmtrxqhg.js} +6 -1
  29. package/dist/{cli-xfb7333f.js → cli-dw2q9f7g.js} +5 -5
  30. package/dist/{cli-4drstbb9.js → cli-ecknp7p1.js} +81 -23
  31. package/dist/{cli-v0pvvhfq.js → cli-efnd2kce.js} +36 -1
  32. package/dist/{cli-1wetf34q.js → cli-f2n17zhp.js} +1 -1
  33. package/dist/{cli-2c8eky5y.js → cli-feh3dkah.js} +1 -1
  34. package/dist/cli-fn697bnt.js +126 -0
  35. package/dist/{cli-v5pr2fgt.js → cli-gt8azyet.js} +3 -3
  36. package/dist/{cli-dbszf3rw.js → cli-h3x613ze.js} +4 -4
  37. package/dist/{cli-nwhb8y2e.js → cli-hf5s9pgf.js} +1 -1
  38. package/dist/cli-j2jbr8af.js +516 -0
  39. package/dist/{cli-agjzafns.js → cli-jhm6hzjn.js} +593 -10
  40. package/dist/{cli-j0tmc908.js → cli-jvj6a0cv.js} +484 -477
  41. package/dist/{cli-xxz89c2n.js → cli-mb75wpxf.js} +1 -1
  42. package/dist/{cli-07s39g1n.js → cli-mv2520nm.js} +1 -1
  43. package/dist/{cli-v90vfwyb.js → cli-mwegs4af.js} +5 -5
  44. package/dist/{cli-bz1gbac9.js → cli-p4ztb80h.js} +2 -2
  45. package/dist/{cli-1wd1n2ya.js → cli-rfphkzh5.js} +1 -1
  46. package/dist/{cli-976r1zqx.js → cli-rggdq8wq.js} +5 -5
  47. package/dist/{cli-3ajw5v5v.js → cli-rsy933yz.js} +6 -6
  48. package/dist/{cli-yj790e19.js → cli-tadfgd0t.js} +8 -1
  49. package/dist/{cli-cn53e7vc.js → cli-ttbsse3f.js} +6 -1
  50. package/dist/{cli-dyqsrqcf.js → cli-v2a8x45d.js} +4 -4
  51. package/dist/{cli-wbvnn6tk.js → cli-v6vv7dy8.js} +91 -9
  52. package/dist/{cli-z2ap2c0c.js → cli-vzc73wet.js} +90 -55
  53. package/dist/cli-wt7ccy4r.js +2654 -0
  54. package/dist/{cli-4cn19jvn.js → cli-x7kpr72a.js} +3 -3
  55. package/dist/{cli-003785v9.js → cli-xvc3q9br.js} +1 -1
  56. package/dist/{cli-v0e7jpr5.js → cli-xvdgasgy.js} +13 -4
  57. package/dist/{cli-asqeg4ve.js → cli-y59h2pk9.js} +2 -2
  58. package/dist/{cli-0wn3ypqv.js → cli-yshaazs1.js} +14121 -3478
  59. package/dist/{cli-wg8yjqtw.js → cli-ysnyhws9.js} +16 -3
  60. package/dist/cli-zp67qg5x.js +92 -0
  61. package/dist/{cli-g3qgrzck.js → cli-zsh093hk.js} +71 -3
  62. package/dist/{cli-ma3zvh39.js → cli-zy7vwsne.js} +6066 -5829
  63. package/dist/cli.js +79 -26
  64. package/dist/{codex-adapter-pcxdn10v.js → codex-adapter-dw1wxqt8.js} +10 -10
  65. package/dist/{codex-hook-b1fb7c2k.js → codex-hook-d2rkvf39.js} +1 -1
  66. package/dist/{codex-login-059nm145.js → codex-login-terbdsqq.js} +2 -2
  67. package/dist/{codex-session-runner-xzwwddp8.js → codex-session-runner-3cgb8y1g.js} +10 -10
  68. package/dist/{commands-98at8mkn.js → commands-mhxam6z2.js} +9 -9
  69. package/dist/{cursor-adapter-dkg6qnkp.js → cursor-adapter-2qda3st7.js} +6 -6
  70. package/dist/{db-01g8wtvw.js → db-sjks418k.js} +11 -9
  71. package/dist/{devin-adapter-qmttt2fz.js → devin-adapter-bffjj5n4.js} +2 -2
  72. package/dist/{dispatcher-nn2k1wxd.js → dispatcher-1es759rn.js} +22 -26
  73. package/dist/{dsh-adapter-w4d4vwdq.js → dsh-adapter-bek4cyw9.js} +6 -6
  74. package/dist/{e2b-9e4g2hvt.js → e2b-nd50eccg.js} +1 -1
  75. package/dist/{event-bus-z7v2t26e.js → event-bus-vnfthafg.js} +2 -2
  76. package/dist/grok-adapter-ar9cw9jg.js +29 -0
  77. package/dist/{handlers-b0kqsbg8.js → handlers-1gkbhktr.js} +25 -29
  78. package/dist/{hook-sap7v1aj.js → hook-ds2zxq9v.js} +16 -18
  79. package/dist/{hook-pz50gqep.js → hook-vxa3tcsy.js} +10 -12
  80. package/dist/{http-ncr8rmd0.js → http-3h21z9md.js} +679 -227
  81. package/dist/{index-c4aqcyz1.js → index-9b65j1m5.js} +11 -11
  82. package/dist/{index-r46s01nq.js → index-bn6cy8ps.js} +22 -26
  83. package/dist/{index-38kqdt3e.js → index-dkfxpkmm.js} +23 -27
  84. package/dist/{index-630kj5vd.js → index-f8xnqrye.js} +55 -43
  85. package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
  86. package/dist/{index-9nrj7797.js → index-n99fycq7.js} +22 -26
  87. package/dist/{keepalive-31613r2h.js → keepalive-3xspa9jw.js} +14 -11
  88. package/dist/{lead-mx59tr5s.js → lead-6qphc124.js} +43 -43
  89. package/dist/{maintenance-55ff39ca.js → maintenance-0teem1bb.js} +10 -12
  90. package/dist/{node-f0ykebbv.js → node-9sg7ajby.js} +25 -7
  91. package/dist/{oauth-refresh-sweep-anma954w.js → oauth-refresh-sweep-sh9q25xp.js} +13 -10
  92. package/dist/{onboard-b2fy8ftb.js → onboard-54qmnqxe.js} +4 -3
  93. package/dist/{opencode-adapter-p1s92tmy.js → opencode-adapter-wjfa2wvd.js} +6 -6
  94. package/dist/{otel-impl-9brvt7xv.js → otel-impl-ycqzp2a1.js} +2 -2
  95. package/dist/{pi-mono-adapter-kz4rrwvd.js → pi-mono-adapter-e77e9zst.js} +6 -6
  96. package/dist/{pricing-refresh-7w8mbmva.js → pricing-refresh-atnkngqs.js} +10 -10
  97. package/dist/{rbac-roles-ejp7qwvb.js → rbac-roles-9pjphh9q.js} +10 -10
  98. package/dist/{rbac-roles-nhk4r7bc.js → rbac-roles-j78wdksm.js} +10 -10
  99. package/dist/{render-v2-2gzt8skq.js → render-v2-pxjs49fx.js} +9 -10
  100. package/dist/{setup-5et8t412.js → setup-ymej0xtx.js} +2 -2
  101. package/dist/{work-objects-tb0er7qa.js → work-objects-3nh4ymzb.js} +26 -30
  102. package/dist/{worker-62cv075d.js → worker-qa9fkz5s.js} +43 -43
  103. package/dist/{x-vbxb31jr.js → x-ma4gnfgy.js} +2 -2
  104. package/openapi.json +457 -31
  105. package/package.json +14 -6
  106. package/plugin/commands/create-pr.md +7 -1
  107. package/plugin/pi-skills/create-pr/SKILL.md +7 -1
  108. package/src/agentmail/app.ts +5 -4
  109. package/src/be/automation-preflight.ts +11 -9
  110. package/src/be/boot-scrub-sweep.ts +302 -0
  111. package/src/be/connector-codes.ts +81 -0
  112. package/src/be/crypto/key-bootstrap.ts +1 -1
  113. package/src/be/db/approvals.ts +15 -1
  114. package/src/be/db/codex-oauth-device-flows.ts +40 -0
  115. package/src/be/db/config-secrets.ts +31 -0
  116. package/src/be/db/runtime.ts +4 -0
  117. package/src/be/db/script-runs.ts +15 -0
  118. package/src/be/db/tasks/write.ts +1 -1
  119. package/src/be/db-queries/mcp-oauth.ts +5 -0
  120. package/src/be/db-queries/oauth.ts +77 -0
  121. package/src/be/db.ts +162 -59
  122. package/src/be/events.ts +17 -1
  123. package/src/be/memory/index-content.ts +2 -2
  124. package/src/be/memory/providers/sqlite-store.ts +63 -11
  125. package/src/be/memory/types.ts +14 -0
  126. package/src/be/migrations/197_connector_codes.sql +23 -0
  127. package/src/be/migrations/198_permission_audit_guest.sql +33 -0
  128. package/src/be/migrations/200_backfill_auto_memory_expiry.sql +20 -0
  129. package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
  130. package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
  131. package/src/be/model-tier-resolution.ts +2 -1
  132. package/src/be/model-validation.ts +1 -1
  133. package/src/be/pricing-normalize.ts +3 -0
  134. package/src/be/rbac-audit.ts +2 -1
  135. package/src/be/scripts/db.ts +28 -4
  136. package/src/be/scripts/source-secrets.ts +37 -0
  137. package/src/be/scripts/typecheck.ts +23 -2
  138. package/src/be/scrub-json.ts +32 -0
  139. package/src/be/sealed-json.ts +59 -0
  140. package/src/be/secret-registry.ts +131 -0
  141. package/src/be/seed-pricing.ts +14 -0
  142. package/src/be/users.ts +7 -1
  143. package/src/be/workflow-replay.ts +77 -0
  144. package/src/cli.tsx +7 -0
  145. package/src/commands/provider-credentials.ts +46 -8
  146. package/src/commands/runner.ts +2 -10
  147. package/src/extensions/contract-types.generated.ts +1 -1
  148. package/src/github/app.ts +4 -4
  149. package/src/heartbeat/heartbeat.ts +20 -8
  150. package/src/hooks/pr-body-guard.ts +1 -1
  151. package/src/http/agents.ts +1 -0
  152. package/src/http/approval-requests.ts +1 -0
  153. package/src/http/apps.ts +3 -0
  154. package/src/http/auth.ts +4 -1
  155. package/src/http/codex-oauth-device.ts +8 -10
  156. package/src/http/codex-oauth-keep-warm.ts +2 -2
  157. package/src/http/config-values.ts +59 -0
  158. package/src/http/config.ts +5 -0
  159. package/src/http/db-query-bounded.ts +3 -0
  160. package/src/http/db-query-shared.ts +107 -15
  161. package/src/http/db-query.ts +10 -2
  162. package/src/http/extensions.ts +1 -0
  163. package/src/http/index.ts +23 -6
  164. package/src/http/integrations.ts +1 -38
  165. package/src/http/ip-rate-limit.ts +82 -0
  166. package/src/http/kv.ts +38 -4
  167. package/src/http/mcp-servers.ts +1 -0
  168. package/src/http/memory.ts +9 -0
  169. package/src/http/models-catalog.ts +2 -0
  170. package/src/http/operator-actor.ts +4 -0
  171. package/src/http/page-proxy.ts +22 -0
  172. package/src/http/pages.ts +4 -2
  173. package/src/http/realtime.ts +1 -1
  174. package/src/http/request-principal.ts +14 -1
  175. package/src/http/script-connections.ts +8 -0
  176. package/src/http/script-runs.ts +12 -4
  177. package/src/http/scripts.ts +15 -0
  178. package/src/http/session-cost-recompute.ts +3 -2
  179. package/src/http/tasks.ts +9 -0
  180. package/src/http/users.ts +226 -0
  181. package/src/http/workflows.ts +3 -2
  182. package/src/http.ts +1 -0
  183. package/src/integrations/kapso/client.ts +3 -1
  184. package/src/jira/app.ts +2 -2
  185. package/src/kv-view.ts +324 -0
  186. package/src/linear/app.ts +2 -2
  187. package/src/oauth/mcp-wrapper.ts +1 -1
  188. package/src/prompts/memories.ts +42 -1
  189. package/src/providers/acp-adapter.ts +85 -24
  190. package/src/providers/acp-target-catalog.ts +54 -1
  191. package/src/providers/acp-targets.ts +358 -4
  192. package/src/providers/amp-adapter.ts +1 -1
  193. package/src/providers/cursor-adapter.ts +1 -1
  194. package/src/providers/grok-adapter.ts +186 -0
  195. package/src/providers/index.ts +5 -1
  196. package/src/providers/pricing-sources.md +6 -0
  197. package/src/providers/reasoning-effort.ts +17 -1
  198. package/src/providers/types.ts +2 -1
  199. package/src/rbac/can.ts +4 -3
  200. package/src/rbac/legacy-policy.ts +3 -3
  201. package/src/rbac/types.ts +6 -0
  202. package/src/script-workflows/supervisor.ts +11 -2
  203. package/src/scripts-runtime/eval-harness.ts +2 -12
  204. package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
  205. package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
  206. package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
  207. package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
  208. package/src/scripts-runtime/executors/quickjs.ts +196 -0
  209. package/src/scripts-runtime/executors/registry.ts +2 -0
  210. package/src/scripts-runtime/executors/sourcemap.ts +79 -0
  211. package/src/scripts-runtime/script-error-hints.ts +14 -0
  212. package/src/scripts-runtime/swarm-sdk.ts +9 -2
  213. package/src/scripts-runtime/types/stdlib.d.ts +27 -8
  214. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
  215. package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -8
  216. package/src/server-user.ts +2 -20
  217. package/src/server.ts +3 -1
  218. package/src/slack/app.ts +4 -0
  219. package/src/slack/config.ts +3 -1
  220. package/src/slack/egress-scrub.ts +87 -0
  221. package/src/tests/acp-adapter.test.ts +191 -2
  222. package/src/tests/boot-scrub-sweep.test.ts +390 -0
  223. package/src/tests/budget-user-scope.test.ts +7 -21
  224. package/src/tests/check-pr-body.test.ts +149 -6
  225. package/src/tests/check-text-columns.test.ts +145 -0
  226. package/src/tests/claude-adapter-binary.test.ts +1 -1
  227. package/src/tests/claude-sdk-process.fixture.ts +13 -2
  228. package/src/tests/codex-oauth-device.test.ts +23 -37
  229. package/src/tests/codex-oauth-keep-warm.test.ts +29 -0
  230. package/src/tests/console-scrub.test.ts +141 -0
  231. package/src/tests/credential-check.test.ts +90 -0
  232. package/src/tests/db-query-denylist.test.ts +202 -0
  233. package/src/tests/db-task-read-contract.test.ts +1 -0
  234. package/src/tests/db-task-write-contract.test.ts +1 -0
  235. package/src/tests/docs-index-footprint.test.js +3 -3
  236. package/src/tests/entrypoint-acp-target-binary.test.ts +44 -1
  237. package/src/tests/events-scrub.test.ts +88 -0
  238. package/src/tests/explicit-model-validation.test.ts +81 -0
  239. package/src/tests/extensions-pre-task.test.ts +33 -1
  240. package/src/tests/fixtures/grok/acp-updates-recorded.jsonl +53 -0
  241. package/src/tests/fixtures/grok/prompt-response-meta.json +36 -0
  242. package/src/tests/fixtures/secret-corpus.ts +320 -0
  243. package/src/tests/grok-adapter.test.ts +647 -0
  244. package/src/tests/heartbeat-checklist.test.ts +44 -0
  245. package/src/tests/integration-disable-flags.test.ts +156 -0
  246. package/src/tests/kv-http.test.ts +34 -0
  247. package/src/tests/kv-tool.test.ts +274 -1
  248. package/src/tests/kv-view.test.ts +147 -0
  249. package/src/tests/mcp-user-route.test.ts +257 -15
  250. package/src/tests/memory-injection-outcome.test.ts +143 -0
  251. package/src/tests/model-tier-resolution.test.ts +76 -3
  252. package/src/tests/page-guest-session.test.ts +424 -0
  253. package/src/tests/page-proxy.test.ts +1 -1
  254. package/src/tests/page-viewer-auth.test.ts +22 -1
  255. package/src/tests/pr-body-leaks.test.ts +157 -2
  256. package/src/tests/preload.ts +10 -0
  257. package/src/tests/provider-registration.test.ts +3 -1
  258. package/src/tests/rbac-audit.test.ts +32 -0
  259. package/src/tests/rbac-mcp-admission-e2e.test.ts +4 -62
  260. package/src/tests/script-connections.test.ts +9 -0
  261. package/src/tests/script-executor-conformance.test.ts +3 -0
  262. package/src/tests/script-executor-quickjs.test.ts +442 -0
  263. package/src/tests/script-executor-registry.test.ts +6 -1
  264. package/src/tests/script-runs-http.test.ts +126 -1
  265. package/src/tests/script-runs-scrub.test.ts +275 -0
  266. package/src/tests/scripts-http.test.ts +82 -1
  267. package/src/tests/scripts-mcp-e2e.test.ts +42 -6
  268. package/src/tests/scripts-runtime.test.ts +37 -0
  269. package/src/tests/scrubbed-text-12e.test.ts +289 -0
  270. package/src/tests/sealed-key-guard.test.ts +166 -0
  271. package/src/tests/secret-canary-sweep.test.ts +958 -0
  272. package/src/tests/secret-registry.test.ts +355 -0
  273. package/src/tests/secret-rules.test.ts +230 -0
  274. package/src/tests/secret-scrubber.test.ts +317 -1
  275. package/src/tests/session-costs-golden.test.ts +17 -0
  276. package/src/tests/slack-egress-scrub.test.ts +216 -0
  277. package/src/tests/swarm-tool-result-gate.test.ts +143 -30
  278. package/src/tests/task-tools-ctx.test.ts +3 -0
  279. package/src/tests/user-token-routes.test.ts +342 -3
  280. package/src/tests/workflow-replay.test.ts +518 -0
  281. package/src/tests/workflow-scrub.test.ts +242 -0
  282. package/src/tests/workflow-system-one-decision-providers.test.ts +737 -0
  283. package/src/tools/db-query.ts +1 -1
  284. package/src/tools/inject-learning.ts +1 -1
  285. package/src/tools/kv/kv-get.ts +136 -6
  286. package/src/tools/send-task.ts +61 -17
  287. package/src/tools/utils.ts +186 -43
  288. package/src/tools/workflows/get-workflow-run.ts +3 -2
  289. package/src/types.ts +16 -1
  290. package/src/utils/console-scrub-install.ts +5 -0
  291. package/src/utils/console-scrub.ts +57 -0
  292. package/src/utils/credentials.ts +8 -0
  293. package/src/utils/page-session.ts +6 -2
  294. package/src/utils/pr-body-leaks.ts +149 -10
  295. package/src/utils/request-auth-context.ts +2 -0
  296. package/src/utils/secret-rules.generated.ts +2055 -0
  297. package/src/utils/secret-scrubber.ts +662 -36
  298. package/src/workflows/engine.ts +39 -4
  299. package/src/workflows/executors/notify.ts +6 -1
  300. package/src/workflows/executors/system-one-decision.ts +31 -12
  301. package/src/workflows/executors/system-one-openai-wire.ts +168 -0
  302. package/src/workflows/executors/system-one-providers.ts +112 -11
  303. package/src/workflows/executors/system-one-wire.ts +79 -0
  304. package/src/workflows/recovery.ts +10 -1
  305. package/src/workflows/resume.ts +62 -3
  306. package/src/workflows/retry-poller.ts +3 -0
  307. package/templates/ai-toolbox.manifest.json +2 -2
  308. package/templates/skills/agent-browser/SKILL.md +5 -5
  309. package/templates/skills/agent-browser/config.json +1 -1
  310. package/templates/skills/agent-browser/content.md +4 -4
  311. package/templates/skills/artifacts/content.md +17 -7
  312. package/templates/skills/jira-interaction/config.json +1 -1
  313. package/templates/skills/jira-interaction/content.md +56 -205
  314. package/templates/skills/linear-interaction/config.json +1 -1
  315. package/templates/skills/linear-interaction/content.md +31 -50
  316. package/templates/skills/pages/content.md +8 -4
  317. package/templates/skills/qa/content.md +2 -2
  318. package/templates/skills/workflow-iterate/content.md +4 -4
  319. package/tsconfig.json +3 -3
  320. package/dist/boot-scrub-logs-t8xg7wd7.js +0 -99
  321. package/dist/cli-0a7r5331.js +0 -168
  322. package/dist/cli-93rxw9yd.js +0 -951
  323. package/dist/cli-cvk6yzw5.js +0 -40
  324. package/dist/cli-dgnj0zcc.js +0 -40
  325. package/dist/cli-qt5wzq6v.js +0 -256
  326. package/dist/cli-v7fqehqd.js +0 -9281
  327. package/dist/cli-vbr859rz.js +0 -572
  328. package/src/be/boot-scrub-logs.ts +0 -132
@@ -20,6 +20,7 @@ import {
20
20
  updateWorkflowRun,
21
21
  updateWorkflowRunStep,
22
22
  } from "../be/db";
23
+ import { WorkflowReplayStateError } from "../be/workflow-replay";
23
24
  import { telemetry } from "../telemetry";
24
25
  import type { Workflow, WorkflowDefinition, WorkflowNode, WorkflowRunStep } from "../types";
25
26
  import { checkpointStep, checkpointStepFailure, checkpointStepWaiting } from "./checkpoint";
@@ -226,6 +227,32 @@ const activeWalks = new Map<string, number>();
226
227
  // left behind is not mistaken for a live execution.
227
228
  const executingSteps = new Set<string>();
228
229
 
230
+ /**
231
+ * Fail closed when a run's sealed replay state cannot be opened. Resuming from
232
+ * the scrubbed copy would feed redaction markers to downstream nodes, so the
233
+ * run (and `stepId`, when one was claimed) fails with the error instead.
234
+ * Returns false, writing nothing, for any other error.
235
+ */
236
+ export async function failRunOnUnreadableReplay(
237
+ runId: string,
238
+ err: unknown,
239
+ stepId?: string,
240
+ ): Promise<boolean> {
241
+ if (!(err instanceof WorkflowReplayStateError)) return false;
242
+ const finishedAt = new Date().toISOString();
243
+ console.error(`[workflow] ${err.message}`);
244
+ if (stepId) {
245
+ await updateWorkflowRunStep(stepId, {
246
+ status: "failed",
247
+ error: err.message,
248
+ finishedAt,
249
+ nextRetryAt: null,
250
+ });
251
+ }
252
+ await updateWorkflowRun(runId, { status: "failed", error: err.message, finishedAt });
253
+ return true;
254
+ }
255
+
229
256
  export function isWorkflowRunActive(runId: string): boolean {
230
257
  return activeWalks.has(runId);
231
258
  }
@@ -267,6 +294,8 @@ export async function walkGraph(
267
294
  const release = holdWorkflowRun(runId);
268
295
  try {
269
296
  await walkGraphOwned(def, runId, ctx, startNodes, registry, workflowId, secretKeys, options);
297
+ } catch (err) {
298
+ if (!(await failRunOnUnreadableReplay(runId, err))) throw err;
270
299
  } finally {
271
300
  release();
272
301
  }
@@ -374,10 +403,11 @@ async function walkGraphOwned(
374
403
  const results = await Promise.all(
375
404
  pendingNodes.map((node) =>
376
405
  executeStep(def, runId, ctx, node, registry, workflowId, secretKeys, options).catch(
377
- (_err): StepResult => ({
378
- outcome: "failed",
379
- successors: [],
380
- }),
406
+ async (err): Promise<StepResult> => {
407
+ // The run-status check below then stops the walk.
408
+ await failRunOnUnreadableReplay(runId, err);
409
+ return { outcome: "failed", successors: [] };
410
+ },
381
411
  ),
382
412
  ),
383
413
  );
@@ -742,6 +772,11 @@ async function executeStep(
742
772
  workflowId,
743
773
  options,
744
774
  );
775
+ } catch (err) {
776
+ if (await failRunOnUnreadableReplay(runId, err, dedup.stepId)) {
777
+ return { outcome: "failed", successors: [] };
778
+ }
779
+ throw err;
745
780
  } finally {
746
781
  executingSteps.delete(dedup.stepId);
747
782
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import type { ExecutorMeta } from "../../types";
3
+ import { scrubSecrets } from "../../utils/secret-scrubber";
3
4
  import { BaseExecutor, type ExecutorResult } from "./base";
4
5
 
5
6
  // ─── Schemas ────────────────────────────────────────────────
@@ -32,7 +33,11 @@ export class NotifyExecutor extends BaseExecutor<
32
33
  context: Readonly<Record<string, unknown>>,
33
34
  _meta: ExecutorMeta,
34
35
  ): Promise<ExecutorResult<z.infer<typeof NotifyOutputSchema>>> {
35
- const message = this.deps.interpolate(config.template, context as Record<string, unknown>);
36
+ // Scrub once: the message reaches Slack, the swarm channel, a log line and
37
+ // the step output.
38
+ const message = scrubSecrets(
39
+ this.deps.interpolate(config.template, context as Record<string, unknown>),
40
+ );
36
41
 
37
42
  switch (config.channel) {
38
43
  case "swarm": {
@@ -24,8 +24,11 @@ import {
24
24
  type SystemOneProvider,
25
25
  type SystemOneProviderId,
26
26
  systemOneKeyRejectedMessage,
27
+ systemOneModelProblem,
27
28
  systemOneProviderOf,
29
+ systemOneQuestionLimitProblem,
28
30
  } from "./system-one-providers";
31
+ import { cloudflareErrorCode, SYSTEM_ONE_WIRE, SystemOneWireError } from "./system-one-wire";
29
32
 
30
33
  // ─── Constants ──────────────────────────────────────────────
31
34
 
@@ -226,6 +229,15 @@ export const SystemOneDecisionConfigSchema = z
226
229
  });
227
230
  }
228
231
  }
232
+ // Provider limits. A {{token}} model is skipped at save and checked again at run,
233
+ // where the interpolated config is parsed by this same schema.
234
+ const modelProblem = systemOneModelProblem(
235
+ config.provider,
236
+ config.model ?? (SYSTEM_ONE_PROVIDERS[config.provider] as SystemOneProvider).defaultModel,
237
+ );
238
+ if (modelProblem) ctx.addIssue({ code: "custom", path: ["model"], message: modelProblem });
239
+ const limitProblem = systemOneQuestionLimitProblem(config.provider, questionIds.length);
240
+ if (limitProblem) ctx.addIssue({ code: "custom", path: ["questions"], message: limitProblem });
229
241
  });
230
242
 
231
243
  export type SystemOneDecisionConfig = z.infer<typeof SystemOneDecisionConfigSchema>;
@@ -805,7 +817,8 @@ const REQUEST_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
805
817
  const ERROR_CODE_RE = /^[A-Za-z0-9._:-]{1,64}$/;
806
818
 
807
819
  function readRequestId(headers: Headers): string | undefined {
808
- const value = headers.get("x-request-id") ?? headers.get("request-id");
820
+ // Cloudflare marks every response with `cf-ray` and sends no request id.
821
+ const value = headers.get("x-request-id") ?? headers.get("request-id") ?? headers.get("cf-ray");
809
822
  return value && REQUEST_ID_RE.test(value) ? value : undefined;
810
823
  }
811
824
 
@@ -839,7 +852,8 @@ function readErrorCode(text: string): string | undefined {
839
852
  ]) {
840
853
  if (typeof candidate === "string" && ERROR_CODE_RE.test(candidate)) return candidate;
841
854
  }
842
- return undefined;
855
+ // Cloudflare's envelope: `{ success: false, errors: [{ code: 10000, message }] }`.
856
+ return cloudflareErrorCode(parsed);
843
857
  }
844
858
 
845
859
  function defaultSleep(ms: number, signal: AbortSignal): Promise<void> {
@@ -878,10 +892,11 @@ export class SystemOneDecisionExecutor extends BaseExecutor<
878
892
  return { db: this.deps.db, env: this.options.env ?? process.env };
879
893
  }
880
894
 
881
- private resolveTarget(id: SystemOneProviderId) {
895
+ private resolveTarget(id: SystemOneProviderId, model?: string) {
882
896
  return resolveSystemOneTarget(id, this.providerContext(), {
883
897
  apiKey: this.options.getApiKey,
884
898
  serverUrl: this.options.getServerUrl,
899
+ model,
885
900
  });
886
901
  }
887
902
 
@@ -922,17 +937,17 @@ export class SystemOneDecisionExecutor extends BaseExecutor<
922
937
 
923
938
  const providerId = config.provider;
924
939
  const provider: SystemOneProvider = SYSTEM_ONE_PROVIDERS[providerId];
925
- const target = await this.resolveTarget(providerId);
926
- if (!target.ok) return { status: "failed", error: target.error };
927
- const { apiKey, endpoint } = target;
928
940
 
929
941
  // A provider with no default sends no `model` when the node sets none.
930
942
  const model = config.model ?? provider.defaultModel;
931
- const body = JSON.stringify({
932
- state: config.state,
933
- ...(model === undefined ? {} : { model }),
934
- questions: config.questions,
935
- });
943
+ const modelProblem = systemOneModelProblem(providerId, model);
944
+ if (modelProblem) return { status: "failed", error: modelProblem };
945
+ const target = await this.resolveTarget(providerId, model);
946
+ if (!target.ok) return { status: "failed", error: target.error };
947
+ const { apiKey, endpoint } = target;
948
+
949
+ const wire = provider.wire ?? SYSTEM_ONE_WIRE;
950
+ const body = JSON.stringify(wire.buildRequest(config, model));
936
951
 
937
952
  const scrub = (message: string) => message.split(apiKey).join("[REDACTED]");
938
953
  const sent = await this.send(
@@ -955,11 +970,15 @@ export class SystemOneDecisionExecutor extends BaseExecutor<
955
970
  try {
956
971
  decision = validateSystemOneResponse(
957
972
  config.questions,
958
- parsed,
973
+ wire.toSystemOne(parsed, { questions: config.questions, model }),
959
974
  sent.requestId,
960
975
  provider.confidenceField,
961
976
  );
962
977
  } catch (err) {
978
+ if (err instanceof SystemOneWireError) {
979
+ const withId = sent.requestId ? `${err.message} [request ${sent.requestId}]` : err.message;
980
+ return { status: "failed", error: scrub(withId) };
981
+ }
963
982
  if (err instanceof SystemOneContractError) {
964
983
  return {
965
984
  status: "failed",
@@ -0,0 +1,168 @@
1
+ import type { SystemOneQuestion } from "./system-one-decision";
2
+ import {
3
+ isRecord,
4
+ type SystemOneWire,
5
+ SystemOneWireError,
6
+ wireContractError,
7
+ } from "./system-one-wire";
8
+
9
+ /**
10
+ * The OpenAI Decisions API (`POST /v1/decisions`, public beta) as a SystemOne host.
11
+ *
12
+ * Request: `{ model, input, questions: [...] }`. Questions are an array named by
13
+ * the node's question ids: `noul` becomes `predicate`, `choice` lists `choices`,
14
+ * `score` lists `levels`. Response: `answers` is an array in question order, each
15
+ * echoing `name`; choice and score probabilities are arrays of `{ value, probability }`.
16
+ * Both directions are translated here so the rest of the node sees SystemOne.
17
+ */
18
+
19
+ type Entry = string | null | unknown[] | Record<string, unknown>;
20
+
21
+ /** A criterion as text: a string as is, structure as JSON, null as nothing. */
22
+ function entryText(entry: Entry | undefined): string | undefined {
23
+ if (entry === undefined || entry === null) return undefined;
24
+ return typeof entry === "string" ? entry : JSON.stringify(entry);
25
+ }
26
+
27
+ function openaiInput(state: unknown): unknown {
28
+ if (typeof state === "string") return state;
29
+ if (Array.isArray(state)) {
30
+ return [
31
+ {
32
+ role: "user",
33
+ content: state.map((text) => ({ type: "input_text", text: String(text) })),
34
+ },
35
+ ];
36
+ }
37
+ // The API reads text and images only, so structured state is sent as JSON text.
38
+ return JSON.stringify(state);
39
+ }
40
+
41
+ function openaiQuestion(name: string, question: SystemOneQuestion): Record<string, unknown> {
42
+ const instructions = entryText(question.instructions) ?? "";
43
+ switch (question.type) {
44
+ case "noul": {
45
+ // A predicate has no true/false descriptions; they become part of the instructions.
46
+ const whenTrue = entryText(question.criteria?.true);
47
+ const whenFalse = entryText(question.criteria?.false);
48
+ const parts = [instructions];
49
+ if (whenTrue) parts.push(`True when: ${whenTrue}`);
50
+ if (whenFalse) parts.push(`False when: ${whenFalse}`);
51
+ return { type: "predicate", name, instructions: parts.join("\n\n") };
52
+ }
53
+ case "choice":
54
+ return {
55
+ type: "choice",
56
+ name,
57
+ instructions,
58
+ choices: Object.entries(question.criteria).map(([value, entry]) => {
59
+ const description = entryText(entry);
60
+ return description === undefined ? { value } : { value, description };
61
+ }),
62
+ };
63
+ case "score":
64
+ return {
65
+ type: "score",
66
+ name,
67
+ instructions,
68
+ // `label` is required, so a level with no description is named by its index.
69
+ levels: question.criteria.map((entry, index) => ({
70
+ label: entryText(entry) ?? String(index),
71
+ })),
72
+ };
73
+ }
74
+ }
75
+
76
+ /** `[{ value, probability }]` as `{ [value]: probability }`. Anything else is left for the validator. */
77
+ function probabilityMap(value: unknown, at: string): unknown {
78
+ if (!Array.isArray(value)) return value;
79
+ const out: Record<string, unknown> = {};
80
+ for (const item of value) {
81
+ if (!isRecord(item) || (typeof item.value !== "string" && typeof item.value !== "number")) {
82
+ wireContractError(`${at} entries must carry a string or number value`);
83
+ }
84
+ const key = String(item.value);
85
+ if (Object.hasOwn(out, key)) wireContractError(`${at} lists an option twice`);
86
+ out[key] = item.probability;
87
+ }
88
+ return out;
89
+ }
90
+
91
+ function toSystemOneAnswer(id: string, question: SystemOneQuestion, raw: Record<string, unknown>) {
92
+ const at = `answer "${id}".probabilities`;
93
+ switch (raw.type) {
94
+ case "predicate":
95
+ return { type: "noul", noul: raw.probability };
96
+ case "choice":
97
+ return {
98
+ type: "choice",
99
+ choice: raw.choice,
100
+ probabilities: probabilityMap(raw.probabilities, at),
101
+ confidence: raw.confidence,
102
+ };
103
+ case "score":
104
+ return {
105
+ type: "score",
106
+ score: raw.score,
107
+ probabilities: probabilityMap(raw.probabilities, at),
108
+ // OpenAI does not echo the levels; the legend is the node's own criteria.
109
+ legend:
110
+ question.type === "score"
111
+ ? Object.fromEntries(question.criteria.map((entry, index) => [String(index), entry]))
112
+ : undefined,
113
+ confidence: raw.confidence,
114
+ };
115
+ default:
116
+ // The validator reports the type mismatch against the declared question.
117
+ return raw;
118
+ }
119
+ }
120
+
121
+ export const OPENAI_DECISIONS_WIRE: SystemOneWire = {
122
+ buildRequest: (config, model) => ({
123
+ ...(model === undefined ? {} : { model }),
124
+ input: openaiInput(config.state),
125
+ questions: Object.entries(config.questions).map(([name, question]) =>
126
+ openaiQuestion(name, question),
127
+ ),
128
+ }),
129
+
130
+ toSystemOne(body, { questions }) {
131
+ if (!isRecord(body)) wireContractError("response body must be a JSON object");
132
+ if (!Array.isArray(body.answers)) wireContractError("response is missing the answers array");
133
+
134
+ const byName = new Map<string, Record<string, unknown>>();
135
+ for (const answer of body.answers) {
136
+ if (!isRecord(answer) || typeof answer.name !== "string") {
137
+ wireContractError("every answer must be an object naming its question");
138
+ }
139
+ if (!Object.hasOwn(questions, answer.name)) {
140
+ wireContractError("response contains an answer for an undeclared question");
141
+ }
142
+ if (byName.has(answer.name)) {
143
+ wireContractError(`response answers question "${answer.name}" twice`);
144
+ }
145
+ byName.set(answer.name, answer);
146
+ }
147
+
148
+ // A refusal is not an answer of the declared type, so no decision is made.
149
+ const refused = [...byName.values()]
150
+ .filter((answer) => answer.type === "refusal")
151
+ .map((answer) => `"${answer.name}"`);
152
+ if (refused.length > 0) {
153
+ throw new SystemOneWireError(
154
+ `OpenAI refused question${refused.length > 1 ? "s" : ""} ${refused.join(", ")}; no decision was made`,
155
+ );
156
+ }
157
+
158
+ const answers: Record<string, unknown> = {};
159
+ for (const [id, answer] of byName) {
160
+ const question = questions[id];
161
+ if (question) answers[id] = toSystemOneAnswer(id, question, answer);
162
+ }
163
+ const usage = isRecord(body.usage)
164
+ ? { input_tokens: body.usage.input_tokens, output_tokens: body.usage.output_tokens }
165
+ : body.usage;
166
+ return { model: body.model, answers, usage };
167
+ },
168
+ };
@@ -1,13 +1,17 @@
1
1
  import { DEFAULT_OPENROUTER_BASE_URL, getOpenRouterBaseUrl } from "../../utils/openrouter-base-url";
2
2
  import type { ExecutorDependencies } from "./base";
3
+ import { OPENAI_DECISIONS_WIRE } from "./system-one-openai-wire";
4
+ import { CLOUDFLARE_WIRE, type SystemOneWire } from "./system-one-wire";
3
5
  import { resolveWorkflowLlmConfig } from "./workflow-llm";
4
6
 
5
7
  /**
6
- * Hosts that serve typed decisions (Jev, laya). A `system-one-decision` node names one by id
7
- * (`config.provider`); the endpoint, header shape, and key name live here, never
8
- * in a workflow definition. Every host takes the same `{ state, model, questions }`
9
- * request and returns the same `{ model, answers, usage }` body, so the answer
10
- * contract in `system-one-decision.ts` is shared. Adding a host is one entry in `SYSTEM_ONE_PROVIDERS`.
8
+ * Hosts that serve typed decisions (Jev, laya, OpenAI Decisions, Cloudflare Clef). A
9
+ * `system-one-decision` node names one by id (`config.provider`); the endpoint, header
10
+ * shape, and key name live here, never in a workflow definition. Most hosts take the
11
+ * SystemOne `{ state, model, questions }` request and return its `{ model, answers, usage }`
12
+ * body; a host that does not supplies a `wire` that translates both ways, so the
13
+ * answer contract in `system-one-decision.ts` is shared. Adding a host is one entry
14
+ * in `SYSTEM_ONE_PROVIDERS`.
11
15
  *
12
16
  * Do not add a chat-completions route. OpenRouter's `typesafe/jev-router` is a
13
17
  * router that forwards a chat request to another model, and `~typesafe/jev-latest`
@@ -31,18 +35,37 @@ export interface SystemOneConfiguredEndpoint {
31
35
  readonly path: string;
32
36
  }
33
37
 
38
+ /**
39
+ * A host whose URL is built from a global config value and the model, e.g. an
40
+ * account id in the path. The value is checked before it reaches the URL.
41
+ */
42
+ export interface SystemOneBuiltEndpoint {
43
+ /** Global config key holding the value, e.g. `CLOUDFLARE_ACCOUNT_ID`. */
44
+ readonly configKey: string;
45
+ /** What the value is, for messages: "the account id of a Cloudflare account". */
46
+ readonly describe: string;
47
+ /** Request URL, or null when the value is not usable. `model` is already allowlisted. */
48
+ readonly build: (value: string, model: string) => string | null;
49
+ }
50
+
34
51
  export interface SystemOneProvider {
35
52
  /** Name used in messages, e.g. "TypeSafe". */
36
53
  readonly label: string;
37
54
  /** The single credential this host needs: a global secret name. */
38
55
  readonly keyName: string;
39
- /** A fixed URL, or the global config key that names the server. */
40
- readonly endpoint: string | SystemOneConfiguredEndpoint;
56
+ /** A fixed URL, the global config key that names the server, or a URL built from config and model. */
57
+ readonly endpoint: string | SystemOneConfiguredEndpoint | SystemOneBuiltEndpoint;
41
58
  /**
42
59
  * Model used when the node sets none. Absent means no `model` is sent and the
43
60
  * host picks, for a host that ignores a model id it does not know.
44
61
  */
45
62
  readonly defaultModel?: string;
63
+ /** When set, the only model ids the node may use (checked at save and at run). */
64
+ readonly allowedModels?: readonly string[];
65
+ /** Host limits stricter than the node's own. */
66
+ readonly limits?: { readonly maxQuestions?: number };
67
+ /** Request and response translation. Absent means the SystemOne wire as is. */
68
+ readonly wire?: SystemOneWire;
46
69
  /**
47
70
  * The field of a `choice` or `score` answer that is the answer's confidence: what
48
71
  * the output reports and what a `humanReview` band is tested against.
@@ -58,6 +81,10 @@ const TYPESAFE_KEY = "TYPESAFE_API_KEY";
58
81
  const OPENROUTER_KEY = "OPENROUTER_API_KEY";
59
82
  const LAYA_KEY = "LAYA_API_KEY";
60
83
  const LAYA_URL_KEY = "LAYA_URL";
84
+ const OPENAI_DECISIONS_KEY = "OPENAI_DECISIONS_API_KEY";
85
+ const CLOUDFLARE_TOKEN_KEY = "CLOUDFLARE_API_TOKEN";
86
+ const CLOUDFLARE_ACCOUNT_KEY = "CLOUDFLARE_ACCOUNT_ID";
87
+ const CLOUDFLARE_ACCOUNT_RE = /^[0-9a-f]{32}$/;
61
88
 
62
89
  /** Filter by key and scope in SQL so no other config row is read or decrypted. */
63
90
  async function readGlobalConfig(
@@ -113,6 +140,39 @@ export const SYSTEM_ONE_PROVIDERS = {
113
140
  confidenceField: "answer_confidence",
114
141
  readKey: ({ db }) => readGlobalConfig(db, LAYA_KEY),
115
142
  },
143
+ openai: {
144
+ label: "OpenAI",
145
+ // Not OPENAI_API_KEY: a global row of that name reaches every worker's env and
146
+ // can change harness auth. This key serves this node only, and needs Decisions access.
147
+ keyName: OPENAI_DECISIONS_KEY,
148
+ endpoint: "https://api.openai.com/v1/decisions",
149
+ defaultModel: "gpt-6-luna",
150
+ // OpenAI's own field; it is not the top probability.
151
+ confidenceField: "confidence",
152
+ wire: OPENAI_DECISIONS_WIRE,
153
+ readKey: ({ db }) => readGlobalConfig(db, OPENAI_DECISIONS_KEY),
154
+ },
155
+ cloudflare: {
156
+ label: "Cloudflare Workers AI",
157
+ keyName: CLOUDFLARE_TOKEN_KEY,
158
+ endpoint: {
159
+ configKey: CLOUDFLARE_ACCOUNT_KEY,
160
+ describe: "the 32-character id of a Cloudflare account",
161
+ build: (accountId, model) => {
162
+ const id = accountId.trim();
163
+ return CLOUDFLARE_ACCOUNT_RE.test(id)
164
+ ? `https://api.cloudflare.com/client/v4/accounts/${id}/ai/run/@cf/cloudflare/${model}`
165
+ : null;
166
+ },
167
+ },
168
+ defaultModel: "clef",
169
+ // The model is part of the URL, so only these ids are accepted.
170
+ allowedModels: ["clef", "clef-flash"],
171
+ limits: { maxQuestions: 64 },
172
+ confidenceField: "confidence",
173
+ wire: CLOUDFLARE_WIRE,
174
+ readKey: ({ db }) => readGlobalConfig(db, CLOUDFLARE_TOKEN_KEY),
175
+ },
116
176
  } as const satisfies Record<string, SystemOneProvider>;
117
177
 
118
178
  export type SystemOneProviderId = keyof typeof SYSTEM_ONE_PROVIDERS;
@@ -180,10 +240,23 @@ function urlKeyOf(provider: SystemOneProvider): string | null {
180
240
  return typeof provider.endpoint === "string" ? null : provider.endpoint.configKey;
181
241
  }
182
242
 
243
+ function isBuiltEndpoint(
244
+ endpoint: SystemOneProvider["endpoint"],
245
+ ): endpoint is SystemOneBuiltEndpoint {
246
+ return typeof endpoint !== "string" && "build" in endpoint;
247
+ }
248
+
249
+ /** What a provider's config value names, for messages. */
250
+ function urlDescriptionOf(provider: SystemOneProvider): string {
251
+ return isBuiltEndpoint(provider.endpoint)
252
+ ? provider.endpoint.describe
253
+ : `the URL of a ${provider.label} server`;
254
+ }
255
+
183
256
  export function systemOneUrlMissingMessage(id: SystemOneProviderId): string {
184
257
  const provider: SystemOneProvider = SYSTEM_ONE_PROVIDERS[id];
185
258
  const configKey = urlKeyOf(provider) ?? "";
186
- return `${configKey} is not configured. SystemOne provider "${id}" needs the URL of a ${provider.label} server. ${SET_URL(configKey)}`;
259
+ return `${configKey} is not configured. SystemOne provider "${id}" needs ${urlDescriptionOf(provider)}. ${SET_URL(configKey)}`;
187
260
  }
188
261
 
189
262
  export function systemOneUrlUnreadableMessage(id: SystemOneProviderId): string {
@@ -194,9 +267,33 @@ export function systemOneUrlInvalidMessage(id: SystemOneProviderId): string {
194
267
  const provider: SystemOneProvider = SYSTEM_ONE_PROVIDERS[id];
195
268
  const configKey = urlKeyOf(provider) ?? "";
196
269
  // The value is not echoed: a URL can carry a credential.
270
+ if (isBuiltEndpoint(provider.endpoint)) {
271
+ return `${configKey} is not ${provider.endpoint.describe}. ${SET_URL(configKey)}`;
272
+ }
197
273
  return `${configKey} is not a usable ${provider.label} server URL. It must be an https URL (http only for localhost) with no credentials, query, or fragment. ${SET_URL(configKey)}`;
198
274
  }
199
275
 
276
+ /** Why a provider cannot use this model, or null. The value is not echoed: it may come from run input. */
277
+ export function systemOneModelProblem(
278
+ id: SystemOneProviderId,
279
+ model: string | undefined,
280
+ ): string | null {
281
+ const { allowedModels }: SystemOneProvider = SYSTEM_ONE_PROVIDERS[id];
282
+ if (!allowedModels || (model !== undefined && allowedModels.includes(model))) return null;
283
+ return `SystemOne provider "${id}" serves only the models ${allowedModels.join(", ")}; set model to one of them or leave it unset.`;
284
+ }
285
+
286
+ /** Why a provider cannot take this many questions, or null. */
287
+ export function systemOneQuestionLimitProblem(
288
+ id: SystemOneProviderId,
289
+ count: number,
290
+ ): string | null {
291
+ const max = (SYSTEM_ONE_PROVIDERS[id] as SystemOneProvider).limits?.maxQuestions;
292
+ return max !== undefined && count > max
293
+ ? `SystemOne provider "${id}" takes at most ${max} questions per node; this node has ${count}.`
294
+ : null;
295
+ }
296
+
200
297
  // ─── Credential and endpoint ────────────────────────────────
201
298
 
202
299
  /** A bearer token is one visible token; anything else would break or split the header. */
@@ -236,8 +333,10 @@ export type SystemOneTarget =
236
333
  export interface SystemOneTargetOverrides {
237
334
  /** Replaces the credential lookup. */
238
335
  apiKey?: (id: SystemOneProviderId) => Promise<string | null | undefined>;
239
- /** Replaces the lookup of a configured server's base URL. */
336
+ /** Replaces the lookup of a configured server's base URL (or account id). */
240
337
  serverUrl?: (id: SystemOneProviderId) => Promise<string | null | undefined>;
338
+ /** The model the call uses, for a host whose URL names it. Defaults to the provider's default. */
339
+ model?: string;
241
340
  }
242
341
 
243
342
  /**
@@ -261,7 +360,7 @@ export async function resolveSystemOneTarget(
261
360
  if (typeof provider.endpoint === "string") {
262
361
  endpoint = provider.endpoint;
263
362
  } else {
264
- const { configKey, path } = provider.endpoint;
363
+ const { configKey } = provider.endpoint;
265
364
  let base: string | null | undefined;
266
365
  let readable = true;
267
366
  try {
@@ -276,7 +375,9 @@ export async function resolveSystemOneTarget(
276
375
  if (!base || base.trim() === "") {
277
376
  errors.push(systemOneUrlMissingMessage(id));
278
377
  } else {
279
- const resolved = systemOneEndpointFromBase(base, path);
378
+ const resolved = isBuiltEndpoint(provider.endpoint)
379
+ ? provider.endpoint.build(base, overrides.model ?? provider.defaultModel ?? "")
380
+ : systemOneEndpointFromBase(base, provider.endpoint.path);
280
381
  if (resolved) endpoint = resolved;
281
382
  else errors.push(systemOneUrlInvalidMessage(id));
282
383
  }
@@ -0,0 +1,79 @@
1
+ import type { SystemOneDecisionConfig, SystemOneQuestion } from "./system-one-decision";
2
+
3
+ /**
4
+ * How one provider's request and response map onto the SystemOne contract. Most
5
+ * hosts speak it natively (`SYSTEM_ONE_WIRE`). A host with another shape supplies
6
+ * a wire that builds its body and turns its success body back into the SystemOne
7
+ * shape, which `validateSystemOneResponse` then checks like any other.
8
+ */
9
+ export interface SystemOneWire {
10
+ /** Request body for one call. */
11
+ buildRequest(config: SystemOneDecisionConfig, model: string | undefined): unknown;
12
+ /**
13
+ * Turn a parsed success body into the SystemOne `{ model, answers, usage }` shape,
14
+ * or throw a `SystemOneWireError`. The result is still validated afterwards.
15
+ */
16
+ toSystemOne(body: unknown, call: SystemOneWireCall): unknown;
17
+ }
18
+
19
+ export interface SystemOneWireCall {
20
+ questions: Record<string, SystemOneQuestion>;
21
+ /** The model the request named, when one was sent. */
22
+ model: string | undefined;
23
+ }
24
+
25
+ /**
26
+ * A success body the wire cannot turn into a decision. `message` is the whole step
27
+ * error: a shape problem carries the validation prefix, a refusal does not.
28
+ */
29
+ export class SystemOneWireError extends Error {}
30
+
31
+ export const VALIDATION_PREFIX = "SystemOne response failed validation: ";
32
+
33
+ export function wireContractError(message: string): never {
34
+ throw new SystemOneWireError(`${VALIDATION_PREFIX}${message}`);
35
+ }
36
+
37
+ export function isRecord(value: unknown): value is Record<string, unknown> {
38
+ return typeof value === "object" && value !== null && !Array.isArray(value);
39
+ }
40
+
41
+ /** The SystemOne request as is. The response needs no translation. */
42
+ export const SYSTEM_ONE_WIRE: SystemOneWire = {
43
+ buildRequest: (config, model) => ({
44
+ state: config.state,
45
+ ...(model === undefined ? {} : { model }),
46
+ questions: config.questions,
47
+ }),
48
+ toSystemOne: (body) => body,
49
+ };
50
+
51
+ /**
52
+ * Cloudflare Workers AI. The request is the SystemOne request (Clef is SystemOne
53
+ * compatible); the response is wrapped in Cloudflare's `{ result, success, errors }`
54
+ * envelope.
55
+ */
56
+ export const CLOUDFLARE_WIRE: SystemOneWire = {
57
+ buildRequest: SYSTEM_ONE_WIRE.buildRequest,
58
+ toSystemOne(body) {
59
+ if (!isRecord(body)) wireContractError("response body must be a JSON object");
60
+ if (body.success !== true) {
61
+ const code = cloudflareErrorCode(body);
62
+ throw new SystemOneWireError(
63
+ `Cloudflare Workers AI reported a failed run${code ? ` (${code})` : ""}. No decision was made.`,
64
+ );
65
+ }
66
+ if (!isRecord(body.result)) wireContractError("Cloudflare response is missing its result");
67
+ return body.result;
68
+ },
69
+ };
70
+
71
+ const ERROR_CODE_RE = /^[A-Za-z0-9._:-]{1,64}$/;
72
+
73
+ /** `errors[0].code` of a Cloudflare envelope, a number or a short string, as text. */
74
+ export function cloudflareErrorCode(body: Record<string, unknown>): string | undefined {
75
+ const first = Array.isArray(body.errors) ? body.errors[0] : undefined;
76
+ const code = isRecord(first) ? first.code : undefined;
77
+ if (typeof code === "number" && Number.isSafeInteger(code)) return String(code);
78
+ return typeof code === "string" && ERROR_CODE_RE.test(code) ? code : undefined;
79
+ }