@desplega.ai/agent-swarm 1.158.0 → 1.159.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 (272) hide show
  1. package/dist/{acp-adapter-xaq4n6pc.js → acp-adapter-qhch0n2v.js} +1 -1
  2. package/dist/{actions-p9hz5yat.js → actions-dtsh1mfb.js} +33 -29
  3. package/dist/{anthropic-messages-51pz26qe.js → anthropic-messages-f2h06k64.js} +8 -3
  4. package/dist/{app-g26wdcz6.js → app-y1b9srva.js} +10 -9
  5. package/dist/{assistant-wydfqkb3.js → assistant-31hwy5xj.js} +34 -30
  6. package/dist/{azure-openai-responses-8gksqtmz.js → azure-openai-responses-0k1mfax5.js} +12 -13
  7. package/dist/{boot-reembed-se8zmdcp.js → boot-reembed-48d2qpkt.js} +11 -11
  8. package/dist/{boot-reembed-g5ckg9yb.js → boot-reembed-w0bryhkr.js} +12 -12
  9. package/dist/{boot-scrub-logs-wfxqan61.js → boot-scrub-logs-t65wbrn7.js} +9 -8
  10. package/dist/{boot-seeds-s3q01xj3.js → boot-seeds-h7c32hvm.js} +12 -11
  11. package/dist/{claude-adapter-tv2x4f36.js → claude-adapter-1t6611ne.js} +26 -21
  12. package/dist/{claude-managed-adapter-41b2s7fd.js → claude-managed-adapter-werp659j.js} +4 -2
  13. package/dist/{claude-managed-setup-60g8aazx.js → claude-managed-setup-y6rdgbc1.js} +11 -2
  14. package/dist/claude-sdk-session-zhkyk8yw.js +29319 -0
  15. package/dist/{cli-vj6h9kc2.js → cli-0m7wct0d.js} +59 -1
  16. package/dist/{cli-wscdvcqz.js → cli-0n2e4x49.js} +1 -1
  17. package/dist/cli-0xqde53b.js +176 -0
  18. package/dist/{cli-xberxa1r.js → cli-10ym38qc.js} +2 -2
  19. package/dist/{cli-5b6b62tz.js → cli-13hw4z91.js} +1 -1
  20. package/dist/{cli-rqz8qbw7.js → cli-20035trh.js} +50 -18
  21. package/dist/{cli-k0g1sjzm.js → cli-31nkw3g3.js} +3 -3
  22. package/dist/cli-3gvrr8vj.js +1422 -0
  23. package/dist/{cli-c07r9a2m.js → cli-48527fpw.js} +9 -8
  24. package/dist/{cli-24k806sc.js → cli-4aj25szq.js} +2 -2
  25. package/dist/{cli-7gtvvpbh.js → cli-4tvgz99m.js} +7 -7
  26. package/dist/cli-503ya656.js +455 -0
  27. package/dist/{cli-cw9zck8j.js → cli-5f2hq128.js} +8 -4
  28. package/dist/{cli-s8eet254.js → cli-68bmme5p.js} +11078 -27833
  29. package/dist/{cli-ftfvggzx.js → cli-79aq9cgb.js} +794 -563
  30. package/dist/cli-7cxw8b60.js +14910 -0
  31. package/dist/cli-7kyk9jr3.js +22 -0
  32. package/dist/cli-7zttzetj.js +324 -0
  33. package/dist/{cli-y95wp3tg.js → cli-96qd5psa.js} +1 -1
  34. package/dist/{cli-g8djp6qe.js → cli-9yg65c1j.js} +22 -0
  35. package/dist/{cli-3gmcwfyx.js → cli-ardpzhjm.js} +5 -5
  36. package/dist/{cli-7s3syk22.js → cli-b6thtzac.js} +5 -5
  37. package/dist/{cli-00w2c66p.js → cli-bgv70kym.js} +13 -2
  38. package/dist/{cli-j55n64b6.js → cli-bv0a38v9.js} +163 -44
  39. package/dist/cli-ca2w7zv7.js +115 -0
  40. package/dist/cli-cpvsaqsw.js +333 -0
  41. package/dist/cli-cygyqj3b.js +754 -0
  42. package/dist/{cli-x24pshtq.js → cli-dvjpkkjd.js} +6 -6
  43. package/dist/{cli-5d10vd2m.js → cli-e0ndxhtk.js} +4 -4
  44. package/dist/{cli-4198qc9v.js → cli-e4xnc0qp.js} +15 -3
  45. package/dist/{cli-rjehvrts.js → cli-eg7cdg68.js} +8 -8
  46. package/dist/cli-ejpygnqz.js +3815 -0
  47. package/dist/{cli-fgqxwrpv.js → cli-fjtp3463.js} +1631 -127
  48. package/dist/{cli-zt1227k1.js → cli-fkqhbg8g.js} +5 -3
  49. package/dist/{cli-47m6dkd3.js → cli-g9df2m37.js} +409 -168
  50. package/dist/cli-gzyh0j0g.js +8455 -0
  51. package/dist/cli-hgqfttfc.js +183 -0
  52. package/dist/cli-hj65wsts.js +17755 -0
  53. package/dist/cli-j9z8mfx3.js +476 -0
  54. package/dist/{cli-aehm1rbq.js → cli-kc3dp84d.js} +1135 -61
  55. package/dist/{cli-h38x0qhg.js → cli-kdnehe0t.js} +4 -3
  56. package/dist/{cli-20cj37vt.js → cli-m4excttz.js} +136 -2216
  57. package/dist/{cli-8qmme6vd.js → cli-mg24hkt4.js} +2 -2
  58. package/dist/{cli-xhaeg2nc.js → cli-mzkqpxee.js} +1 -1
  59. package/dist/{cli-q67y4wzm.js → cli-phsaj4xq.js} +24 -2
  60. package/dist/{cli-pw9rj5t9.js → cli-pqetb83f.js} +1 -176
  61. package/dist/{cli-rj19hc34.js → cli-qjxy52w7.js} +1 -1
  62. package/dist/{cli-53sz8gkz.js → cli-qv4jv8bx.js} +1 -1
  63. package/dist/cli-qvnnx4ct.js +1051 -0
  64. package/dist/cli-rb9m6rpe.js +4980 -0
  65. package/dist/{cli-tt9k8r35.js → cli-rfezr056.js} +476 -909
  66. package/dist/{cli-ggm92wcn.js → cli-rgjprct3.js} +1 -0
  67. package/dist/{cli-9httdpyq.js → cli-rrqmqr1s.js} +1 -1
  68. package/dist/cli-rykfmgwg.js +102 -0
  69. package/dist/{cli-eetrvv91.js → cli-t3p77qk1.js} +1 -1
  70. package/dist/{cli-68w09sjw.js → cli-vn0qv9cj.js} +25 -88
  71. package/dist/{cli-987hg0aa.js → cli-vwbbaecp.js} +2 -2
  72. package/dist/cli-wd9hz40d.js +69 -0
  73. package/dist/{cli-j57qkrpq.js → cli-wgjx0q33.js} +13 -13
  74. package/dist/cli-xgpf00sk.js +2149 -0
  75. package/dist/{cli-mcnhgnn1.js → cli-ykk5jaax.js} +184 -26
  76. package/dist/{cli-aky8tctm.js → cli-z9x0sh1t.js} +3 -3
  77. package/dist/{cli-1hkrch3k.js → cli-zw65mab6.js} +5 -5
  78. package/dist/cli.js +38 -29
  79. package/dist/cloudflare-workers-ai-system-one-1sg586qv.js +46 -0
  80. package/dist/{codex-adapter-cnm8wk11.js → codex-adapter-jqfqqa21.js} +21 -14
  81. package/dist/{codex-hook-9hptmdhj.js → codex-hook-5eys3dya.js} +2 -2
  82. package/dist/{codex-session-runner-gfg547zj.js → codex-session-runner-zd341dsm.js} +21 -14
  83. package/dist/{commands-e3bn0p75.js → commands-vfsysz4s.js} +9 -8
  84. package/dist/{db-03v9kzr2.js → db-s3znhvbn.js} +15 -10
  85. package/dist/{dispatcher-nrmg8yht.js → dispatcher-5xn0c7rf.js} +32 -28
  86. package/dist/{dsh-adapter-k03nfqa6.js → dsh-adapter-894mgfvs.js} +3 -3
  87. package/dist/{e2b-93x1k0j5.js → e2b-ez07znam.js} +4 -3
  88. package/dist/execute-71ysgv35.js +965 -0
  89. package/dist/{google-generative-ai-bxmv50nc.js → google-generative-ai-1nfcdphp.js} +5 -4
  90. package/dist/{google-vertex-809af514.js → google-vertex-1tqcd7b1.js} +5 -4
  91. package/dist/{handlers-aqcedbyb.js → handlers-5w4jwycr.js} +35 -31
  92. package/dist/{hook-7vkskkre.js → hook-853bxrr6.js} +16 -13
  93. package/dist/{hook-ybvmrmhq.js → hook-z0hbxj85.js} +16 -13
  94. package/dist/{http-2eqa8t24.js → http-akc3wy3m.js} +600 -123
  95. package/dist/{index-f6cryxwe.js → index-4a5njnm1.js} +2 -59
  96. package/dist/{index-a3vn96n8.js → index-5sz16ksh.js} +187 -47
  97. package/dist/{index-c6zsv1bd.js → index-bjmjh9h4.js} +11 -11
  98. package/dist/{index-chgdkq36.js → index-cd151qkq.js} +32 -28
  99. package/dist/{index-x71bvq0p.js → index-k661dzzr.js} +33 -29
  100. package/dist/{index-1yks3qcr.js → index-ttpbhnjg.js} +32 -28
  101. package/dist/{keepalive-vtkf5n99.js → keepalive-8p9rpvz5.js} +11 -10
  102. package/dist/{lead-h3sp357x.js → lead-jp69z5h3.js} +49 -45
  103. package/dist/llama-cpp-classify-t2tccams.js +335 -0
  104. package/dist/{maintenance-jzeqtk3q.js → maintenance-fwx4ev0k.js} +12 -12
  105. package/dist/{mistral-conversations-zx0kn554.js → mistral-conversations-bhvwqcnk.js} +14 -17
  106. package/dist/{oauth-refresh-sweep-qp3dzmdp.js → oauth-refresh-sweep-cwcq1ctx.js} +10 -9
  107. package/dist/{onboard-tmsp0qbz.js → onboard-2b4q0t84.js} +6 -5
  108. package/dist/{openai-codex-responses-4vjqzvjp.js → openai-codex-responses-nekthgks.js} +21 -8
  109. package/dist/{openai-completions-9hjrqbw8.js → openai-completions-hxtzwj0s.js} +8 -11
  110. package/dist/{openai-responses-7b1pn0xt.js → openai-responses-w4r118w7.js} +22 -17
  111. package/dist/{opencode-adapter-x6x2da1z.js → opencode-adapter-zz7xvjj6.js} +6 -4
  112. package/dist/{openrouter-images-9f9ek4rw.js → openrouter-images-b6sqqjgn.js} +4 -6
  113. package/dist/{otel-impl-9mz73q65.js → otel-impl-g8rs4vwr.js} +1 -1
  114. package/dist/{pi-messages-txpjj39q.js → pi-messages-vxdt8v7j.js} +2 -1
  115. package/dist/{pi-mono-adapter-m6dvv0dz.js → pi-mono-adapter-n0me28wt.js} +29 -17
  116. package/dist/pricing-refresh-0v77385v.js +39 -0
  117. package/dist/{rbac-roles-c9wqz83x.js → rbac-roles-krjyazbj.js} +10 -9
  118. package/dist/{render-v2-gryrh9h8.js → render-v2-3wjsp9aw.js} +10 -9
  119. package/dist/runtime-ktjysacx.js +24 -0
  120. package/dist/{setup-q9720bt6.js → setup-yeqryke6.js} +6 -5
  121. package/dist/typesafe-system-one-96960jxp.js +27 -0
  122. package/dist/{virtual-modules-3htpgn8x.js → virtual-modules-dzdeq1sf.js} +21 -13
  123. package/dist/{work-objects-b76x7deb.js → work-objects-3pakpkw0.js} +35 -31
  124. package/dist/{worker-6vrxbv41.js → worker-85testgj.js} +49 -45
  125. package/openapi.json +863 -14
  126. package/package.json +9 -8
  127. package/src/be/db/agents.ts +20 -0
  128. package/src/be/db/tasks/human-free.ts +442 -0
  129. package/src/be/db/tasks/read.ts +56 -1
  130. package/src/be/db/tasks/write.ts +107 -11
  131. package/src/be/db.ts +89 -165
  132. package/src/be/harness-model-support.ts +182 -0
  133. package/src/be/human-free-drain.ts +67 -0
  134. package/src/be/migrations/182_task_human_free_flag.sql +63 -0
  135. package/src/be/migrations/183_human_free_reclassify_queue.sql +25 -0
  136. package/src/be/migrations/184_model_catalog.sql +120 -0
  137. package/src/be/model-catalog-store.ts +644 -0
  138. package/src/be/model-tier-keys.ts +31 -0
  139. package/src/be/model-tier-resolution.ts +489 -0
  140. package/src/be/model-validation.ts +112 -0
  141. package/src/be/models-catalog.ts +61 -7
  142. package/src/be/modelsdev-cache.json +82 -0
  143. package/src/be/modelsdev-cache.ts +2 -0
  144. package/src/be/pricing-refresh.ts +154 -12
  145. package/src/be/scripts/typecheck.ts +15 -0
  146. package/src/be/seed-scripts/catalog/task-failure-audit.ts +61 -9
  147. package/src/be/swarm-config-guard.ts +6 -0
  148. package/src/cli.tsx +10 -0
  149. package/src/commands/claude-managed-setup.ts +3 -1
  150. package/src/commands/runner.ts +76 -2
  151. package/src/extensions/contract-types.generated.ts +1 -1
  152. package/src/heartbeat/heartbeat.ts +64 -15
  153. package/src/heartbeat/templates.ts +2 -2
  154. package/src/http/agents.ts +49 -1
  155. package/src/http/index.ts +8 -0
  156. package/src/http/models-catalog.ts +357 -8
  157. package/src/http/poll.ts +45 -2
  158. package/src/http/schedules.ts +36 -6
  159. package/src/http/session-data.ts +2 -5
  160. package/src/http/stats.ts +6 -8
  161. package/src/http/tasks.ts +26 -1
  162. package/src/http/usage-cache.ts +56 -15
  163. package/src/http/workflows.ts +35 -8
  164. package/src/prompts/session-templates.ts +1 -0
  165. package/src/providers/claude-managed-models.ts +47 -161
  166. package/src/providers/codex-adapter.ts +4 -4
  167. package/src/providers/codex-models.ts +65 -198
  168. package/src/providers/reasoning-effort.ts +44 -140
  169. package/src/rbac/legacy-policy.ts +25 -0
  170. package/src/rbac/permissions.ts +10 -0
  171. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  172. package/src/scripts-runtime/types/stdlib.d.ts +15 -0
  173. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
  174. package/src/scripts-runtime/types/swarm-sdk.d.ts +15 -0
  175. package/src/server.ts +6 -0
  176. package/src/slack/message-text.ts +56 -2
  177. package/src/slack/thread-buffer.ts +10 -2
  178. package/src/tests/agents-list-model-display.test.ts +4 -6
  179. package/src/tests/base-prompt.test.ts +3 -0
  180. package/src/tests/claude-adapter.test.ts +10 -0
  181. package/src/tests/claude-managed-adapter.test.ts +12 -18
  182. package/src/tests/codex-adapter.test.ts +2 -1
  183. package/src/tests/codex-gpt6-release.test.ts +66 -5
  184. package/src/tests/context-window.test.ts +7 -5
  185. package/src/tests/db-extraction-contract.test.ts +1 -0
  186. package/src/tests/db-task-read-contract.test.ts +5 -1
  187. package/src/tests/db-task-write-contract.test.ts +4 -1
  188. package/src/tests/docs-index-footprint.test.js +3 -3
  189. package/src/tests/explicit-model-validation.test.ts +332 -0
  190. package/src/tests/harness-model-support.test.ts +374 -0
  191. package/src/tests/heartbeat-checklist-config.test.ts +71 -0
  192. package/src/tests/heartbeat-checklist.test.ts +4 -2
  193. package/src/tests/heartbeat-orphan-resume.test.ts +48 -0
  194. package/src/tests/human-free-flag.test.ts +279 -0
  195. package/src/tests/human-free-oracle.ts +57 -0
  196. package/src/tests/human-free-reclassify.test.ts +669 -0
  197. package/src/tests/migration-046-budgets.test.ts +3 -3
  198. package/src/tests/model-catalog-migration.test.ts +134 -0
  199. package/src/tests/model-catalog-table.test.ts +249 -0
  200. package/src/tests/model-catalog-write-access.test.ts +387 -0
  201. package/src/tests/model-tier-resolution.test.ts +519 -0
  202. package/src/tests/models-catalog.test.ts +1 -1
  203. package/src/tests/multi-runtime-enabled-stats.test.ts +65 -1
  204. package/src/tests/package-publish.test.ts +137 -36
  205. package/src/tests/pricing-routes.test.ts +2 -2
  206. package/src/tests/rbac-engine.test.ts +42 -2
  207. package/src/tests/reasoning-effort.test.ts +109 -0
  208. package/src/tests/refresh-modelsdev-carryforward.test.ts +5 -5
  209. package/src/tests/runtime-model-catalog.test.ts +86 -0
  210. package/src/tests/script-connections.test.ts +5 -0
  211. package/src/tests/seed-scripts.test.ts +89 -0
  212. package/src/tests/session-costs.test.ts +14 -11
  213. package/src/tests/slack-message-text.test.ts +101 -0
  214. package/src/tests/slack-thread-buffer-context.test.ts +93 -0
  215. package/src/tests/task-cascade-fail.test.ts +148 -2
  216. package/src/tests/tool-annotations.test.ts +1 -1
  217. package/src/tests/usage-cache.test.ts +166 -0
  218. package/src/tests/workflow-executors.test.ts +5 -2
  219. package/src/tests/workflow-system-one-decision-review.test.ts +1021 -0
  220. package/src/tests/workflow-system-one-decision-save-warnings.test.ts +365 -0
  221. package/src/tests/workflow-system-one-decision.test.ts +2063 -0
  222. package/src/tools/model-catalog/index.ts +137 -0
  223. package/src/tools/schedules/create-schedule.ts +16 -1
  224. package/src/tools/schedules/patch-schedule.ts +17 -0
  225. package/src/tools/schedules/update-schedule.ts +17 -0
  226. package/src/tools/send-task.ts +14 -0
  227. package/src/tools/task-action.ts +12 -0
  228. package/src/tools/tool-config.ts +3 -1
  229. package/src/tools/workflows/create-workflow.ts +9 -2
  230. package/src/tools/workflows/patch-workflow-node.ts +17 -7
  231. package/src/tools/workflows/patch-workflow.ts +9 -2
  232. package/src/tools/workflows/update-workflow.ts +9 -2
  233. package/src/types.ts +30 -1
  234. package/src/utils/context-window.ts +47 -53
  235. package/src/utils/harness-cli-version.ts +115 -0
  236. package/src/utils/harness-model-error.ts +33 -0
  237. package/src/utils/runtime-model-catalog.ts +137 -0
  238. package/src/workflows/approval-resolution.ts +39 -0
  239. package/src/workflows/definition.ts +7 -0
  240. package/src/workflows/engine.ts +69 -1
  241. package/src/workflows/executors/agent-task.ts +9 -0
  242. package/src/workflows/executors/base.ts +53 -0
  243. package/src/workflows/executors/human-in-the-loop.ts +8 -8
  244. package/src/workflows/executors/index.ts +1 -0
  245. package/src/workflows/executors/registry.ts +2 -0
  246. package/src/workflows/executors/system-one-decision.ts +1232 -0
  247. package/src/workflows/executors/system-one-providers.ts +301 -0
  248. package/src/workflows/executors/workflow-llm.ts +13 -1
  249. package/src/workflows/readiness.ts +86 -0
  250. package/src/workflows/recovery.ts +12 -12
  251. package/src/workflows/resume.ts +23 -9
  252. package/templates/schedules/daily-blocker-digest/content.md +4 -5
  253. package/templates/skills/agent-browser/SKILL.md +6 -5
  254. package/templates/skills/agent-browser/content.md +6 -5
  255. package/templates/skills/heartbeat-runbook/config.json +1 -1
  256. package/templates/skills/heartbeat-runbook/content.md +2 -2
  257. package/templates/skills/swarm-scripts/SKILL.md +1 -1
  258. package/templates/skills/swarm-scripts/content.md +1 -1
  259. package/templates/skills/workflow-iterate/content.md +81 -1
  260. package/templates/workflows/claude-code-changelog-watch/content.md +2 -0
  261. package/templates/workflows/pr-review-status-sweep/content.md +2 -0
  262. package/dist/claude-sdk-session-t442qz6f.js +0 -39108
  263. package/dist/cli-0xjkj1j7.js +0 -61
  264. package/dist/cli-65hjmev5.js +0 -1141
  265. package/dist/cli-aw1jwp1r.js +0 -14075
  266. package/dist/cli-h967nb6r.js +0 -6603
  267. package/dist/cli-vapyk8sw.js +0 -20
  268. package/dist/cli-ws6rqycm.js +0 -247
  269. package/dist/pricing-refresh-v1cbr9te.js +0 -27
  270. package/src/providers/modelsdev-reasoning.json +0 -6453
  271. package/dist/{codex-login-x3jk4etn.js → codex-login-he9vrn90.js} +3 -3
  272. package/dist/{index-f24115ge.js → index-2wsdsnmm.js} +3 -3
@@ -4,7 +4,10 @@
4
4
  * This module is safe for both API and worker code — it has NO database imports.
5
5
  *
6
6
  * Phase 4 + Phase 9 of the cost-tracking plan:
7
- * - `getContextWindowSize` now resolves shortnames, family-versioned ids
7
+ * - Model-catalog phase 4: windows come from the model catalog (no
8
+ * per-model table). A new model gets the right window as soon as it
9
+ * lands in `model_catalog`.
10
+ * - `getContextWindowSize` resolves shortnames, family-versioned ids
8
11
  * (`claude-sonnet-4-6`), AND dated full ids (`claude-sonnet-4-6-20251004`)
9
12
  * by stripping the trailing date suffix. Previously the dated form fell
10
13
  * to the 200k default — wildly wrong for sonnet/opus 4.x.
@@ -17,6 +20,9 @@
17
20
  * deprecated; new code should use `computeContextUsedUnified`.
18
21
  */
19
22
 
23
+ import { buildClaudeShortnameMap } from "@desplega/model-catalog";
24
+ import { runtimeCatalogModel, runtimeCatalogSection } from "./runtime-model-catalog";
25
+
20
26
  /**
21
27
  * Phase 9: stamp this onto every `context_usage` event the adapter emits.
22
28
  * Callers that compute their own number for legacy reasons (e.g. pi-mono
@@ -25,43 +31,6 @@
25
31
  */
26
32
  export const CONTEXT_FORMULA = "input-cache-output" as const;
27
33
 
28
- const CONTEXT_WINDOW_DEFAULTS: Record<string, number> = {
29
- // Anthropic Fable / Mythos tier
30
- "claude-fable-5-1": 1_000_000,
31
- "claude-mythos-5-1": 1_000_000,
32
- "claude-opus-5-5": 1_000_000,
33
- "claude-opus-5": 1_000_000,
34
- "claude-fable-5": 1_000_000,
35
- "claude-mythos-5": 1_000_000,
36
- "claude-sonnet-5-5": 1_000_000,
37
- "claude-sonnet-5": 1_000_000,
38
- // Anthropic 4.x family
39
- "claude-opus-4-8": 1_000_000,
40
- "claude-opus-4-7": 1_000_000,
41
- "claude-opus-4-6": 1_000_000,
42
- "claude-opus-4-5": 1_000_000,
43
- "claude-opus-4-1": 200_000,
44
- "claude-opus-4-0": 200_000,
45
- "claude-sonnet-4-6": 1_000_000,
46
- "claude-sonnet-4-5": 1_000_000,
47
- "claude-sonnet-4-0": 200_000,
48
- "claude-haiku-4-5": 200_000,
49
- // Anthropic 3.x family (legacy)
50
- "claude-3-7-sonnet": 200_000,
51
- "claude-3-5-sonnet": 200_000,
52
- "claude-3-5-haiku": 200_000,
53
- "claude-3-opus": 200_000,
54
- "claude-3-sonnet": 200_000,
55
- "claude-3-haiku": 200_000,
56
- // Shortnames used by the local-CLI adapter and pi-mono OpenRouter mirror.
57
- fable: 1_000_000,
58
- mythos: 1_000_000,
59
- opus: 1_000_000,
60
- sonnet: 1_000_000,
61
- haiku: 200_000,
62
- default: 200_000,
63
- };
64
-
65
34
  const DEFAULT_CONTEXT_WINDOW = 200_000;
66
35
 
67
36
  /**
@@ -69,30 +38,55 @@ const DEFAULT_CONTEXT_WINDOW = 200_000;
69
38
  * resolve to the same window as the family-versioned id.
70
39
  *
71
40
  * `claude-sonnet-4-6-20251004` → `claude-sonnet-4-6`
72
- * `claude-haiku-4-5-20251001` → `claude-haiku-4-5`
73
- *
74
- * Anthropic's dated full ids are always `${family}-${major}-${minor}-${YYYYMMDD}`,
75
- * so an 8-digit trailing date is a reliable signal.
76
41
  */
77
42
  function stripAnthropicDateSuffix(model: string): string {
78
43
  return model.replace(/-(\d{8})$/, "");
79
44
  }
80
45
 
46
+ let shortnameMap: { section: Record<string, unknown>; map: Record<string, string> } | null = null;
47
+
48
+ /** Claude CLI shortnames (`opus`, `sonnet`, ...) → newest catalog id. */
49
+ function resolveClaudeShortname(model: string): string | undefined {
50
+ const section = runtimeCatalogSection("anthropic");
51
+ if (!shortnameMap || shortnameMap.section !== section) {
52
+ shortnameMap = { section, map: buildClaudeShortnameMap(section) };
53
+ }
54
+ return shortnameMap.map[model.trim().toLowerCase()];
55
+ }
56
+
57
+ function catalogContext(provider: string, model: string): number | undefined {
58
+ const context = runtimeCatalogModel(provider, model)?.limit?.context;
59
+ return typeof context === "number" && context > 0 ? context : undefined;
60
+ }
61
+
62
+ /**
63
+ * Context window (tokens) for a model id, read from the model catalog
64
+ * (`src/utils/runtime-model-catalog.ts`: live `model_catalog` + overlay,
65
+ * vendored models.dev snapshot offline). Accepts Anthropic ids (dated or
66
+ * not), Claude CLI shortnames, OpenAI/Codex ids and `provider/model` ids.
67
+ * Unknown models get a conservative 200k so percent math stays finite.
68
+ */
81
69
  export function getContextWindowSize(model: string): number {
82
- // Fast path: exact match (shortname or family-versioned id).
83
- if (CONTEXT_WINDOW_DEFAULTS[model] !== undefined) {
84
- return CONTEXT_WINDOW_DEFAULTS[model];
70
+ if (!model) return DEFAULT_CONTEXT_WINDOW;
71
+ const slash = model.indexOf("/");
72
+ if (slash > 0) {
73
+ const scoped = catalogContext(model.slice(0, slash), model.slice(slash + 1));
74
+ if (scoped) return scoped;
75
+ }
76
+ for (const provider of ["anthropic", "openai"]) {
77
+ const exact = catalogContext(provider, model);
78
+ if (exact) return exact;
85
79
  }
86
- // Dated full id → strip suffix and retry.
87
80
  const stripped = stripAnthropicDateSuffix(model);
88
- if (stripped !== model && CONTEXT_WINDOW_DEFAULTS[stripped] !== undefined) {
89
- return CONTEXT_WINDOW_DEFAULTS[stripped];
81
+ if (stripped !== model) {
82
+ const dated = catalogContext("anthropic", stripped);
83
+ if (dated) return dated;
84
+ }
85
+ const shortname = resolveClaudeShortname(model);
86
+ if (shortname) {
87
+ const resolved = catalogContext("anthropic", shortname);
88
+ if (resolved) return resolved;
90
89
  }
91
- // OpenAI / GPT family — most reasoning models have 200k+; we keep this
92
- // conservative and let callers override via models.dev rates if they want.
93
- // Specific gpt-5.x context windows are >1M but the local-CLI adapter
94
- // generally doesn't surface those; the API recompute path uses the rate
95
- // table, not the window. The 200k default keeps the math safe.
96
90
  return DEFAULT_CONTEXT_WINDOW;
97
91
  }
98
92
 
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The worker's harness CLI version (`claude --version` / `codex --version`),
3
+ * reported on register and used to key harness_model_support rows. Worker-safe
4
+ * (no DB). Probed once per process; null when the CLI is missing or silent.
5
+ */
6
+ import { isUnknownModelError } from "./harness-model-error";
7
+ import { scrubSecrets } from "./secret-scrubber";
8
+
9
+ const CLI_BINARY: Record<string, string> = { claude: "claude", codex: "codex" };
10
+ const PROBE_TIMEOUT_MS = 10_000;
11
+ const VERSION_RE = /\d+\.\d+\.\d+(?:[-+][\w.]+)?/;
12
+
13
+ const probed = new Map<string, Promise<string | null>>();
14
+
15
+ export function parseCliVersion(output: string): string | null {
16
+ return VERSION_RE.exec(output)?.[0] ?? null;
17
+ }
18
+
19
+ export function probeHarnessCliVersion(harness: string): Promise<string | null> {
20
+ const binary = CLI_BINARY[harness];
21
+ if (!binary) return Promise.resolve(null);
22
+ let pending = probed.get(harness);
23
+ if (!pending) {
24
+ pending = (async () => {
25
+ try {
26
+ const proc = Bun.spawn([binary, "--version"], {
27
+ stdin: "ignore",
28
+ stdout: "pipe",
29
+ stderr: "pipe",
30
+ timeout: PROBE_TIMEOUT_MS,
31
+ killSignal: "SIGKILL",
32
+ });
33
+ const [stdout, exitCode] = await Promise.all([
34
+ new Response(proc.stdout).text(),
35
+ proc.exited,
36
+ ]);
37
+ return exitCode === 0 ? parseCliVersion(stdout) : null;
38
+ } catch {
39
+ return null;
40
+ }
41
+ })();
42
+ probed.set(harness, pending);
43
+ }
44
+ return pending;
45
+ }
46
+
47
+ /** Last probed version, without waiting; null until the probe finished. */
48
+ export async function cachedHarnessCliVersion(harness: string): Promise<string | null> {
49
+ return (await probed.get(harness)) ?? null;
50
+ }
51
+
52
+ const reported = new Set<string>();
53
+
54
+ /**
55
+ * Record a model's run outcome on this worker's CLI. `ok` is sent once per
56
+ * model per process; `unsupported` whenever the CLI rejects the id. Other
57
+ * failures say nothing about support and are not sent. Never throws.
58
+ */
59
+ export async function reportHarnessModelOutcome(opts: {
60
+ apiUrl: string;
61
+ apiKey?: string;
62
+ agentId: string;
63
+ harness: string;
64
+ model: string | undefined;
65
+ exitCode: number;
66
+ failureReason?: string;
67
+ fetchImpl?: typeof fetch;
68
+ }): Promise<void> {
69
+ if (!opts.model || !CLI_BINARY[opts.harness]) return;
70
+ const cliVersion = await cachedHarnessCliVersion(opts.harness);
71
+ if (!cliVersion) return;
72
+ const status =
73
+ opts.exitCode === 0
74
+ ? "ok"
75
+ : isUnknownModelError(opts.failureReason, opts.model)
76
+ ? "unsupported"
77
+ : null;
78
+ if (!status) return;
79
+ const key = `${opts.harness}|${cliVersion}|${opts.model}`;
80
+ if (status === "ok" && reported.has(key)) return;
81
+ try {
82
+ const headers: Record<string, string> = {
83
+ "Content-Type": "application/json",
84
+ "X-Agent-ID": opts.agentId,
85
+ };
86
+ if (opts.apiKey) headers.Authorization = `Bearer ${opts.apiKey}`;
87
+ const res = await (opts.fetchImpl ?? fetch)(
88
+ `${opts.apiUrl}/api/models-catalog/harness-support`,
89
+ {
90
+ method: "PUT",
91
+ headers,
92
+ body: JSON.stringify({
93
+ harness: opts.harness,
94
+ cliVersion,
95
+ modelId: opts.model,
96
+ status,
97
+ ...(status === "unsupported" && opts.failureReason
98
+ ? { error: scrubSecrets(opts.failureReason).slice(0, 2000) }
99
+ : {}),
100
+ }),
101
+ },
102
+ );
103
+ if (res.ok && status === "ok") reported.add(key);
104
+ } catch {
105
+ // Best effort: the next run reports again.
106
+ }
107
+ }
108
+
109
+ export function resetHarnessCliVersionForTests(seed?: Record<string, string | null>): void {
110
+ probed.clear();
111
+ reported.clear();
112
+ for (const [harness, version] of Object.entries(seed ?? {})) {
113
+ probed.set(harness, Promise.resolve(version));
114
+ }
115
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Recognizes a harness CLI rejecting a model id. Worker-safe (no DB).
3
+ *
4
+ * Claude Code: "There's an issue with the selected model (x). It may not exist
5
+ * or you may not have access to it", or the API's `not_found_error` "model: x".
6
+ * Codex: "The model `x` does not exist or you do not have access to it",
7
+ * "model is not supported", "unknown model".
8
+ *
9
+ * A model-rejection phrase can also appear in an unrelated failure ("invalid model output"),
10
+ * and a false positive marks the model unsupported for the whole (harness, CLI version). So
11
+ * when `modelId` is given, the rejection line must name that model.
12
+ */
13
+ const UNKNOWN_MODEL_PATTERNS: RegExp[] = [
14
+ /issue with the selected model/i,
15
+ /model[`'" ]*[\w.:/-]*[`'" ]*(?:does not exist|not found)/i,
16
+ /not_found_error[^\n]*model/i,
17
+ /model[^\n]{0,80}is not supported/i,
18
+ /unsupported model/i,
19
+ /unknown model/i,
20
+ /invalid model/i,
21
+ ];
22
+
23
+ export function isUnknownModelError(
24
+ text: string | null | undefined,
25
+ modelId?: string | null,
26
+ ): boolean {
27
+ if (!text) return false;
28
+ const id = modelId?.trim().toLowerCase();
29
+ return text.split("\n").some((line) => {
30
+ if (!UNKNOWN_MODEL_PATTERNS.some((re) => re.test(line))) return false;
31
+ return !id || line.toLowerCase().includes(id);
32
+ });
33
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Process-wide model catalog for code that runs on BOTH sides of the
3
+ * API/worker boundary (provider adapters, context-window math, reasoning
4
+ * gating). Model-catalog phase 4: this replaces the hand-maintained per-model
5
+ * tables (codex allowlist/windows/pricing, claude-managed allowlist/pricing,
6
+ * per-model context windows, the slim reasoning snapshot).
7
+ *
8
+ * Sources, highest first:
9
+ * 1. the live catalog: the API server's `model_catalog` + overlay rows.
10
+ * Workers pull it over HTTP (`GET /api/models-catalog`, see
11
+ * `refreshRuntimeModelCatalog`); the API process sets it directly after
12
+ * every catalog reload.
13
+ * 2. the vendored models.dev snapshot (`src/be/modelsdev-cache.json`) —
14
+ * offline fallback, and the only source for ids the live catalog has
15
+ * dropped (legacy models still named by old tasks and pricing rows).
16
+ *
17
+ * NO database imports (worker-safe). Every lookup is synchronous; the HTTP
18
+ * refresh is TTL-cached and never throws, so hot paths stay cheap.
19
+ */
20
+ import modelsDevSnapshot from "../be/modelsdev-cache.json";
21
+
22
+ export interface RuntimeCatalogModel {
23
+ id?: string;
24
+ name?: string;
25
+ cost?: { input?: number; output?: number; cache_read?: number; cache_write?: number };
26
+ limit?: { context?: number };
27
+ release_date?: string;
28
+ status?: string;
29
+ reasoning?: boolean;
30
+ reasoning_options?: { type?: string; values?: string[] }[];
31
+ }
32
+
33
+ export interface RuntimeCatalogProvider {
34
+ id?: string;
35
+ name?: string;
36
+ models?: Record<string, RuntimeCatalogModel>;
37
+ }
38
+
39
+ export type RuntimeCatalog = Record<string, RuntimeCatalogProvider | undefined>;
40
+
41
+ const SNAPSHOT = modelsDevSnapshot as unknown as RuntimeCatalog;
42
+
43
+ /** Default freshness window for the worker-side HTTP pull. */
44
+ export const RUNTIME_CATALOG_TTL_MS = 10 * 60_000;
45
+ const FETCH_TIMEOUT_MS = 5_000;
46
+
47
+ let live: RuntimeCatalog | null = null;
48
+ let liveAt = 0;
49
+ let liveSource: "live" | "snapshot" | null = null;
50
+ const mergedSections = new Map<string, Record<string, RuntimeCatalogModel>>();
51
+ let inFlight: Promise<boolean> | null = null;
52
+
53
+ /** Replace the live layer (API process after a reload; worker after a pull). */
54
+ export function setRuntimeModelCatalog(
55
+ providers: RuntimeCatalog,
56
+ at: number = Date.now(),
57
+ source: "live" | "snapshot" = "live",
58
+ ): void {
59
+ live = providers;
60
+ liveAt = at;
61
+ liveSource = source;
62
+ mergedSections.clear();
63
+ }
64
+
65
+ export function resetRuntimeModelCatalogForTests(): void {
66
+ live = null;
67
+ liveAt = 0;
68
+ liveSource = null;
69
+ mergedSections.clear();
70
+ inFlight = null;
71
+ }
72
+
73
+ /** Where the current live layer came from; null = snapshot only. */
74
+ export function runtimeModelCatalogSource(): "live" | "snapshot" | null {
75
+ return liveSource;
76
+ }
77
+
78
+ /** One provider section: snapshot models overlaid by the live catalog (live wins per id). */
79
+ export function runtimeCatalogSection(provider: string): Record<string, RuntimeCatalogModel> {
80
+ const cached = mergedSections.get(provider);
81
+ if (cached) return cached;
82
+ const merged: Record<string, RuntimeCatalogModel> = {
83
+ ...(SNAPSHOT[provider]?.models ?? {}),
84
+ ...(live?.[provider]?.models ?? {}),
85
+ };
86
+ mergedSections.set(provider, merged);
87
+ return merged;
88
+ }
89
+
90
+ export function runtimeCatalogModel(
91
+ provider: string,
92
+ modelId: string,
93
+ ): RuntimeCatalogModel | undefined {
94
+ return runtimeCatalogSection(provider)[modelId];
95
+ }
96
+
97
+ /**
98
+ * Pull the live catalog from the API server. TTL-cached (`maxAgeMs`),
99
+ * single-flight, never throws: returns false and keeps the previous layer
100
+ * when the server is unreachable or answers non-2xx.
101
+ */
102
+ export async function refreshRuntimeModelCatalog(opts: {
103
+ apiUrl: string;
104
+ apiKey?: string;
105
+ agentId?: string;
106
+ maxAgeMs?: number;
107
+ fetchImpl?: typeof fetch;
108
+ }): Promise<boolean> {
109
+ const maxAge = opts.maxAgeMs ?? RUNTIME_CATALOG_TTL_MS;
110
+ if (live && Date.now() - liveAt < maxAge) return true;
111
+ if (inFlight) return inFlight;
112
+ const doFetch = opts.fetchImpl ?? fetch;
113
+ inFlight = (async () => {
114
+ try {
115
+ const headers: Record<string, string> = {};
116
+ if (opts.apiKey) headers.Authorization = `Bearer ${opts.apiKey}`;
117
+ if (opts.agentId) headers["X-Agent-ID"] = opts.agentId;
118
+ const res = await doFetch(`${opts.apiUrl}/api/models-catalog`, {
119
+ headers,
120
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
121
+ });
122
+ if (!res.ok) return false;
123
+ const body = (await res.json()) as {
124
+ source?: "live" | "snapshot";
125
+ providers?: RuntimeCatalog;
126
+ };
127
+ if (!body || typeof body.providers !== "object" || body.providers === null) return false;
128
+ setRuntimeModelCatalog(body.providers, Date.now(), body.source ?? "live");
129
+ return true;
130
+ } catch {
131
+ return false;
132
+ } finally {
133
+ inFlight = null;
134
+ }
135
+ })();
136
+ return inFlight;
137
+ }
@@ -0,0 +1,39 @@
1
+ import type { WorkflowDefinition } from "../types";
2
+ import type { ApprovalOutcome, ResolvedApproval } from "./executors/base";
3
+ import type { ExecutorRegistry } from "./executors/registry";
4
+
5
+ /**
6
+ * Step output and output port for a step whose approval request resolved.
7
+ *
8
+ * Both resume paths (the live `approval.resolved` event and the recovery sweep)
9
+ * go through here so they cannot disagree. A `human-in-the-loop` step gets its
10
+ * own output on the port named after the status. An executor that parked on the
11
+ * same approval machinery (`system-one-decision` with `humanReview`) shapes its
12
+ * own output from what it stored before it parked.
13
+ */
14
+ export function shapeApprovalResolution(
15
+ registry: ExecutorRegistry,
16
+ definition: Pick<WorkflowDefinition, "nodes">,
17
+ nodeId: string,
18
+ parkedOutput: unknown,
19
+ approval: ResolvedApproval,
20
+ ): ApprovalOutcome {
21
+ const node = definition.nodes.find((candidate) => candidate.id === nodeId);
22
+ if (node && registry.has(node.type)) {
23
+ const shaped = registry.get(node.type).resolveApproval(parkedOutput, approval);
24
+ if (shaped) return shaped;
25
+ }
26
+ return {
27
+ output: {
28
+ requestId: approval.requestId,
29
+ status: approval.status,
30
+ responses: approval.responses,
31
+ },
32
+ nextPort:
33
+ approval.status === "timeout"
34
+ ? "timeout"
35
+ : approval.status === "rejected"
36
+ ? "rejected"
37
+ : "approved",
38
+ };
39
+ }
@@ -6,6 +6,10 @@ import type {
6
6
  WorkflowPatch,
7
7
  } from "../types";
8
8
  import type { ExecutorRegistry } from "./executors/registry";
9
+ import {
10
+ systemOneRetryViolations,
11
+ systemOneStaticShapeViolations,
12
+ } from "./executors/system-one-decision";
9
13
 
10
14
  /** Extract all target node IDs from a node's `next` field */
11
15
  export function getNextTargets(next: string | string[] | Record<string, string>): string[] {
@@ -206,6 +210,9 @@ export function validateDefinition(
206
210
  `Node "${node.id}" human-in-the-loop config.questions must be an array or one exact {{interpolation}} token`,
207
211
  );
208
212
  }
213
+ // A system-one-decision node's question ids and return types are static, and it never carries
214
+ // an engine retry policy (it retries transient transport errors itself).
215
+ errors.push(...systemOneStaticShapeViolations(node), ...systemOneRetryViolations(node));
209
216
  if (node.type === "foreach") {
210
217
  validateForeachNode(node, errors);
211
218
  // A legacy `#` id may stay editable as a NORMAL node, but never as a foreach
@@ -28,10 +28,16 @@ import { shouldSkipCooldown } from "./cooldown";
28
28
  import { findEntryNodes, getNextTargets, getSuccessors, resolveValidationPort } from "./definition";
29
29
  import type { AsyncExecutorResult } from "./executors/base";
30
30
  import type { ExecutorRegistry } from "./executors/registry";
31
+ import {
32
+ SYSTEM_ONE_DECISION_NODE_TYPE,
33
+ systemOneRetryViolations,
34
+ systemOneUnresolvedError,
35
+ } from "./executors/system-one-decision";
31
36
  import { FOREACH_TERMINAL_STEP_STATUSES, resolveForeachParent } from "./foreach-join";
32
37
  import { getSecretInputKeys, redactSecretsForStorage, resolveInputs } from "./input";
33
38
  import { validateJsonSchema } from "./json-schema-validator";
34
39
  import { getMaxWorkflowStepsPerRun } from "./limits";
40
+ import { findWorkflowReadinessProblems, formatReadinessRunError } from "./readiness";
35
41
  import { deepInterpolate } from "./template";
36
42
  import { runStepValidation, type ValidationRunResult } from "./validation";
37
43
 
@@ -149,6 +155,18 @@ export async function startWorkflowExecution(
149
155
  triggerType: options.triggerType ?? "manual",
150
156
  });
151
157
 
158
+ // An executor that cannot run (for example a system-one-decision node with no API key) fails
159
+ // the run here, before any node has side effects, instead of partway through.
160
+ const notReady = await findWorkflowReadinessProblems(workflow.definition, registry);
161
+ if (notReady.length > 0) {
162
+ await updateWorkflowRun(runId, {
163
+ status: "failed",
164
+ error: formatReadinessRunError(notReady),
165
+ finishedAt: new Date().toISOString(),
166
+ });
167
+ return runId;
168
+ }
169
+
152
170
  // Resolve inputs and merge into initial context
153
171
  const ctx: Record<string, unknown> = { trigger: triggerData };
154
172
  if (options.requestedByUserId) {
@@ -744,6 +762,15 @@ async function runClaimedStep(
744
762
  // 3. Get executor
745
763
  const executor = registry.get(node.type);
746
764
 
765
+ // 3a. A system-one-decision node retries transient transport errors itself (config.maxRetries).
766
+ // Engine retries are not status-aware, so a policy here would re-send rejected
767
+ // requests and multiply paid attempts. Fail before any request is built.
768
+ const systemOneRetryProblems = systemOneRetryViolations(node);
769
+ if (systemOneRetryProblems.length > 0) {
770
+ await checkpointStepFailure(runId, stepId, systemOneRetryProblems.join("; "), 0);
771
+ return { outcome: "failed", successors: [] };
772
+ }
773
+
747
774
  // 3b. Build local interpolation context from explicit inputs mapping
748
775
  const interpolationCtx = buildNodeInterpolationCtx(node, ctx);
749
776
 
@@ -765,6 +792,7 @@ async function runClaimedStep(
765
792
  value: interpolatedValue,
766
793
  unresolved,
767
794
  scriptBodyUnresolved,
795
+ strictUnresolved,
768
796
  } = interpolateNodeConfig(node, interpolationCtx);
769
797
  const interpolatedConfig = interpolatedValue as Record<string, unknown>;
770
798
  const executionCtx: Record<string, unknown> = { ...ctx, ...interpolationCtx };
@@ -775,6 +803,17 @@ async function runClaimedStep(
775
803
  return { outcome: "failed", successors: [] };
776
804
  }
777
805
 
806
+ // A paid, non-idempotent call must not go out with a blanked field.
807
+ if (strictUnresolved && strictUnresolved.length > 0) {
808
+ await checkpointStepFailure(
809
+ runId,
810
+ stepId,
811
+ systemOneUnresolvedError(node.id, strictUnresolved),
812
+ 0,
813
+ );
814
+ return { outcome: "failed", successors: [] };
815
+ }
816
+
778
817
  if (unresolved.length > 0) {
779
818
  console.warn(
780
819
  `[workflow] Step ${node.id}: unresolved interpolation tokens: ${unresolved.join(", ")}`,
@@ -1063,7 +1102,36 @@ function buildScriptBodyCtx(
1063
1102
  export function interpolateNodeConfig(
1064
1103
  node: Pick<WorkflowNode, "type" | "config" | "inputs">,
1065
1104
  interpolationCtx: Record<string, unknown>,
1066
- ): { value: unknown; unresolved: string[]; scriptBodyUnresolved?: string[] } {
1105
+ ): {
1106
+ value: unknown;
1107
+ unresolved: string[];
1108
+ scriptBodyUnresolved?: string[];
1109
+ /** Tokens that must resolve before dispatch; the caller fails the step when non-empty. */
1110
+ strictUnresolved?: string[];
1111
+ } {
1112
+ if (node.type === SYSTEM_ONE_DECISION_NODE_TYPE) {
1113
+ // `state` and `questions` keep a whole-token reference's JSON type (an object
1114
+ // stays an object; string interpolation would flatten it to JSON text). The
1115
+ // resolved value is data, never re-interpolated, so a literal `{{` inside a
1116
+ // lead's text is not read as another template. Every other field, and every
1117
+ // unresolved token anywhere in the config, is strict: no blank reaches the API.
1118
+ const { state, questions, ...rest } = node.config;
1119
+ const restResult = deepInterpolate(rest, interpolationCtx);
1120
+ const dynamic: Record<string, unknown> = {};
1121
+ if (Object.hasOwn(node.config, "state")) dynamic.state = state;
1122
+ if (Object.hasOwn(node.config, "questions")) dynamic.questions = questions;
1123
+ const dynamicResult = deepInterpolate(dynamic, interpolationCtx, { preserveRawTokens: true });
1124
+ const unresolved = [...restResult.unresolved, ...dynamicResult.unresolved];
1125
+ return {
1126
+ value: {
1127
+ ...(restResult.value as Record<string, unknown>),
1128
+ ...(dynamicResult.value as Record<string, unknown>),
1129
+ },
1130
+ unresolved,
1131
+ strictUnresolved: unresolved,
1132
+ };
1133
+ }
1134
+
1067
1135
  if (node.type === "foreach" && Object.hasOwn(node.config, "over")) {
1068
1136
  const { over, body, ...configWithoutOverAndBody } = node.config;
1069
1137
  const configResult = deepInterpolate(configWithoutOverAndBody, interpolationCtx);
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { explicitModelErrorForAgent } from "../../be/model-validation";
2
3
  import { applyPreTaskCreate } from "../../extensions/apply-task-create";
3
4
  import { workflowContextKey } from "../../tasks/context-key";
4
5
  import { TaskCreationBlockedError } from "../../tasks/errors";
@@ -30,6 +31,8 @@ export const AgentTaskConfigSchema = z.object({
30
31
  vcsRepo: z.string().min(1).optional(),
31
32
  model: z.string().min(1).optional(),
32
33
  modelTier: ModelTierSchema.optional(),
34
+ /** Accept a `model` the catalog does not list (a custom id). Without it an unknown id fails the node. */
35
+ allowCustomModel: z.boolean().optional(),
33
36
  effort: ReasoningEffortSchema.optional(),
34
37
  parentTaskId: z.string().uuid().optional(),
35
38
  requestedByUserId: z.string().optional(),
@@ -98,6 +101,12 @@ export class AgentTaskExecutor extends BaseExecutor<
98
101
  }
99
102
 
100
103
  // 3. Create the task (config is already deep-interpolated by the engine)
104
+ const modelError = await explicitModelErrorForAgent({
105
+ model: splitLegacyModelAlias({ model: config.model, modelTier: config.modelTier }).model,
106
+ allowCustomModel: config.allowCustomModel,
107
+ agentId: config.agentId,
108
+ });
109
+ if (modelError) return { status: "failed", error: modelError };
101
110
  const preCreate = await applyPreTaskCreate({
102
111
  description: config.template,
103
112
  options: {