@desplega.ai/agent-swarm 2.4.2 → 2.4.4

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 (183) hide show
  1. package/dist/{acp-adapter-dv7hmx4r.js → acp-adapter-thq04adh.js} +4 -5
  2. package/dist/{actions-51nc1gny.js → actions-j6ywh6x5.js} +18 -18
  3. package/dist/{amp-adapter-abp5tdx8.js → amp-adapter-k8b5xmj8.js} +3 -3
  4. package/dist/{app-9h14qvr6.js → app-rb7vc02s.js} +3 -3
  5. package/dist/{assistant-wv1gz3mt.js → assistant-ftta1zjf.js} +20 -20
  6. package/dist/{boot-reembed-6yn53ms9.js → boot-reembed-srp6v6af.js} +1 -1
  7. package/dist/{boot-reembed-7fcka4z5.js → boot-reembed-xns7h5h9.js} +1 -1
  8. package/dist/{boot-scrub-sweep-pm1580ax.js → boot-scrub-sweep-3qqrh2mz.js} +3 -3
  9. package/dist/{boot-seeds-g5hfc1x8.js → boot-seeds-9ks8hce7.js} +4 -4
  10. package/dist/{claude-adapter-gf3kxhf0.js → claude-adapter-k5ksyres.js} +4 -4
  11. package/dist/{claude-managed-setup-h00hsgtz.js → claude-managed-setup-n5a5eqks.js} +1 -1
  12. package/dist/{claude-sdk-session-s2k1qd8y.js → claude-sdk-session-2j3j4kwn.js} +9822 -9749
  13. package/dist/{cli-pkht5hap.js → cli-1d70crsy.js} +2 -2
  14. package/dist/{cli-r99kc9yq.js → cli-1ephgwnq.js} +3 -3
  15. package/dist/{cli-7225gwj7.js → cli-1m17dc9d.js} +1 -1
  16. package/dist/{cli-bcj753sq.js → cli-2v045xyf.js} +3 -3
  17. package/dist/{cli-h38rjv87.js → cli-2y91wnbh.js} +12 -7
  18. package/dist/{cli-sv5gb0yb.js → cli-2zqpkvtt.js} +1 -1
  19. package/dist/{cli-xbzvet96.js → cli-36ckw0wx.js} +5 -4
  20. package/dist/{cli-8sxqsbbq.js → cli-37yb7307.js} +1 -1
  21. package/dist/{cli-hyfw2j1e.js → cli-3cgngyd8.js} +11 -11
  22. package/dist/{cli-a4gvwttf.js → cli-44159k28.js} +2 -2
  23. package/dist/{cli-7awf1fp8.js → cli-4dg55wsk.js} +3 -3
  24. package/dist/{cli-mhywgrrz.js → cli-4sydckyj.js} +1 -1
  25. package/dist/{cli-y2x52gs2.js → cli-51y09c6p.js} +20 -17
  26. package/dist/{cli-ahtpy9kf.js → cli-55qmbp6z.js} +10 -5
  27. package/dist/{cli-pw7v69vp.js → cli-625tb7aq.js} +1 -1
  28. package/dist/{cli-x5mq1fyq.js → cli-74c1wpm0.js} +3 -3
  29. package/dist/{cli-b1dfr1m4.js → cli-7a6p4e65.js} +1 -1
  30. package/dist/{cli-hqhzg1qq.js → cli-82d3nkc3.js} +1 -1
  31. package/dist/{cli-nmqdc2t3.js → cli-9fhkkpj5.js} +78 -12
  32. package/dist/{cli-h93c9vb3.js → cli-amyhm3gj.js} +1 -1
  33. package/dist/{cli-0qvtffe9.js → cli-b8k7epj1.js} +6 -6
  34. package/dist/{cli-1pa3rz69.js → cli-ba8ne1es.js} +6 -1
  35. package/dist/{cli-g0hgtz4p.js → cli-bnz0r3kc.js} +3 -3
  36. package/dist/{cli-0hyhdkdk.js → cli-cebvbc9t.js} +4 -1
  37. package/dist/{cli-efaqv1c7.js → cli-cynvc9fj.js} +35 -7
  38. package/dist/{cli-8zwe9mpk.js → cli-dqb4n629.js} +10 -10
  39. package/dist/{cli-7a91w22y.js → cli-e8xyjvfc.js} +1 -1
  40. package/dist/{cli-s6vr7r2d.js → cli-esxerzg7.js} +1 -1
  41. package/dist/{cli-w9g4sz81.js → cli-gf2850q7.js} +1 -1
  42. package/dist/{cli-10hhg0km.js → cli-gqvh8d8a.js} +7 -7
  43. package/dist/{cli-t30yj95q.js → cli-hj3debgw.js} +1 -1
  44. package/dist/{cli-fpmsqq59.js → cli-nc35pxb7.js} +3 -3
  45. package/dist/{cli-d3qqdy4c.js → cli-p20r8w42.js} +16 -14
  46. package/dist/{cli-shvfedvm.js → cli-pccfmtb5.js} +5 -5
  47. package/dist/{cli-02qm7src.js → cli-q6r7t299.js} +1037 -299
  48. package/dist/{cli-2wag8973.js → cli-qeaq3j6t.js} +1 -1
  49. package/dist/{cli-nn47d7e8.js → cli-qkhc5aqy.js} +15 -15
  50. package/dist/{cli-faypxz7q.js → cli-t7cyef5g.js} +4 -4
  51. package/dist/{cli-z4wd2ftv.js → cli-te6b5wvg.js} +149 -184
  52. package/dist/{cli-5rk36sjf.js → cli-v3yr2gbg.js} +119 -44
  53. package/dist/{cli-239yjk27.js → cli-vdm20wej.js} +5 -5
  54. package/dist/{cli-zfg9enh9.js → cli-vfha21c9.js} +6 -1
  55. package/dist/{cli-rdyw3mct.js → cli-vq97g1jq.js} +6 -5
  56. package/dist/{cli-n85dqg6d.js → cli-w6nadkh4.js} +4 -4
  57. package/dist/{cli-99jez72r.js → cli-w9etnnxk.js} +6 -1
  58. package/dist/{cli-saw9a6f6.js → cli-xc27fwzg.js} +2 -2
  59. package/dist/{cli-ftyypbg5.js → cli-xj0re5tm.js} +1 -1
  60. package/dist/{cli-fyzjmseb.js → cli-zrk6jxv8.js} +1 -1
  61. package/dist/{cli-y8n5k515.js → cli-zrwpctp4.js} +28 -28
  62. package/dist/cli.js +44 -19
  63. package/dist/{codex-adapter-ttygw382.js → codex-adapter-968xwfy7.js} +2 -2
  64. package/dist/{codex-hook-x7nejyrr.js → codex-hook-e2he7aca.js} +2 -2
  65. package/dist/{codex-session-runner-60b58vv3.js → codex-session-runner-ht4z99fa.js} +2 -2
  66. package/dist/{commands-edmdwa4x.js → commands-pcymaeg9.js} +3 -3
  67. package/dist/{controller-m263fg43.js → controller-fpjyk46r.js} +3 -3
  68. package/dist/{controllers-connect-5fym8sw1.js → controllers-connect-mwzx1fh6.js} +19 -19
  69. package/dist/{cursor-adapter-cz6nm6mj.js → cursor-adapter-cp4s2e2f.js} +2 -2
  70. package/dist/{db-2y0mgstc.js → db-sgd12bt9.js} +3 -3
  71. package/dist/{db-nn0hcmx5.js → db-xsjg9jqq.js} +2 -2
  72. package/dist/{dispatcher-0a9wwjcx.js → dispatcher-x0jfw2c9.js} +17 -17
  73. package/dist/{dsh-adapter-3n3net33.js → dsh-adapter-jrq7ma7h.js} +2 -2
  74. package/dist/{e2b-thdcxtjd.js → e2b-16d6ch39.js} +1 -1
  75. package/dist/{e2b-7am6tmq6.js → e2b-zn0b4w5w.js} +1 -1
  76. package/dist/{external-channel-zhvt95n3.js → external-channel-8njj4h3b.js} +2 -2
  77. package/dist/{grok-adapter-qnr1hp2y.js → grok-adapter-kkr97v2f.js} +5 -6
  78. package/dist/handlers-0pvee9nw.js +39 -0
  79. package/dist/{heartbeat-gz97r7ac.js → heartbeat-wd03zhn0.js} +20 -20
  80. package/dist/{hook-1mmdgjqs.js → hook-f0vghj42.js} +10 -5
  81. package/dist/{hook-x8rthca4.js → hook-xzj2ts2p.js} +2 -2
  82. package/dist/{http-r8f85xr9.js → http-4hwxv62p.js} +261 -123
  83. package/dist/{index-va3y63es.js → index-6srwvd5b.js} +1 -2
  84. package/dist/{index-y7rbcrc1.js → index-97q4xn03.js} +20 -11
  85. package/dist/{index-s98037v0.js → index-cbxkmfc5.js} +1 -2
  86. package/dist/{index-vzvfqzfj.js → index-gdh8jrdm.js} +17 -17
  87. package/dist/{index-aex0eba9.js → index-hg0x2cdv.js} +17 -17
  88. package/dist/index-thm0y6a8.js +41 -0
  89. package/dist/{index-smxyk04s.js → index-ww2vznkp.js} +1 -1
  90. package/dist/{keepalive-64c0f34c.js → keepalive-hrh7xnr9.js} +3 -3
  91. package/dist/{lead-b1da0f97.js → lead-ee8vazz2.js} +30 -31
  92. package/dist/{maintenance-mnxpar12.js → maintenance-e8wnwnqx.js} +1 -1
  93. package/dist/{oauth-refresh-sweep-36hs2v4h.js → oauth-refresh-sweep-zm0yj4sj.js} +2 -2
  94. package/dist/{onboard-xes8de61.js → onboard-dtwcq603.js} +3 -3
  95. package/dist/{opencode-adapter-3s7ax02e.js → opencode-adapter-x4tqzhkh.js} +3 -3
  96. package/dist/{otel-impl-fzc57kcq.js → otel-impl-w3fbhpn0.js} +1 -1
  97. package/dist/{pi-mono-adapter-ytp7gz8b.js → pi-mono-adapter-mcfkn9vf.js} +3 -3
  98. package/dist/{pricing-refresh-1nek2w7b.js → pricing-refresh-j38dehfs.js} +3 -3
  99. package/dist/{rbac-roles-ttyfaep2.js → rbac-roles-ae8yttq4.js} +3 -3
  100. package/dist/{rbac-roles-sqefb5hw.js → rbac-roles-ejxx9b35.js} +2 -2
  101. package/dist/{render-v2-vb527jwn.js → render-v2-n16n0phe.js} +3 -3
  102. package/dist/{resume-jcdzg1bn.js → resume-z0fgjc38.js} +5 -5
  103. package/dist/{session-access-zsp4wsp0.js → session-access-cv3mq6mc.js} +6 -4
  104. package/dist/{setup-1nb2zqd3.js → setup-mafhj3yt.js} +1 -1
  105. package/dist/{work-objects-61w08fwr.js → work-objects-8b86mgjp.js} +21 -21
  106. package/dist/{worker-c1e68hpa.js → worker-16s75hm5.js} +30 -31
  107. package/dist/worker-token-xf11yyds.js +136 -0
  108. package/openapi.json +295 -12
  109. package/package.json +5 -5
  110. package/src/apps/action-runner.ts +4 -0
  111. package/src/apps/catalog.generated.json +3 -0
  112. package/src/apps/page-validator.ts +8 -2
  113. package/src/be/db.ts +7 -4
  114. package/src/be/migrations/231_slack_tasks_last_updated_index.sql +5 -0
  115. package/src/be/migrations/232_agent_session_tokens.sql +41 -0
  116. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +10 -1
  117. package/src/be/swarm-config-guard.ts +16 -0
  118. package/src/be/users.ts +169 -12
  119. package/src/cli.tsx +24 -0
  120. package/src/commands/worker-token.ts +164 -0
  121. package/src/github/handlers.ts +6 -5
  122. package/src/hooks/hook.ts +2 -1
  123. package/src/http/apps.ts +43 -5
  124. package/src/http/codex-oauth-keep-warm.ts +27 -9
  125. package/src/http/config.ts +5 -3
  126. package/src/http/core.ts +2 -0
  127. package/src/http/session-access.ts +51 -2
  128. package/src/http/sessions.ts +249 -10
  129. package/src/http/tasks.ts +4 -32
  130. package/src/providers/acp-adapter.ts +33 -13
  131. package/src/providers/acp-swarm-events.ts +1 -1
  132. package/src/providers/amp-adapter.ts +1 -1
  133. package/src/providers/codex-adapter.ts +4 -4
  134. package/src/providers/codex-oauth/storage.ts +1 -1
  135. package/src/providers/opencode-adapter.ts +2 -2
  136. package/src/providers/pi-mono-extension.ts +2 -1
  137. package/src/rbac/legacy-policy.ts +3 -1
  138. package/src/rbac/permissions.ts +4 -0
  139. package/src/tests/acp-adapter.test.ts +10 -23
  140. package/src/tests/acp-fake-agent.ts +36 -0
  141. package/src/tests/acp-swarm-events.test.ts +1 -1
  142. package/src/tests/agent-session-tokens.test.ts +452 -0
  143. package/src/tests/apps-action-parent-task.test.ts +295 -0
  144. package/src/tests/claude-stop-hook.test.ts +47 -0
  145. package/src/tests/codex-adapter.test.ts +42 -0
  146. package/src/tests/codex-oauth-keep-warm.test.ts +85 -1
  147. package/src/tests/config-session-auth.test.ts +14 -0
  148. package/src/tests/docs-index-footprint.test.js +2 -2
  149. package/src/tests/env-flag.test.ts +15 -1
  150. package/src/tests/github-handlers-cancel-config.test.ts +29 -8
  151. package/src/tests/github-handlers-inline-comments.test.ts +9 -2
  152. package/src/tests/grok-adapter.test.ts +3 -3
  153. package/src/tests/heartbeat-jev-gate.test.ts +23 -0
  154. package/src/tests/migration-232-agent-session-tokens.test.ts +67 -0
  155. package/src/tests/rbac-engine.test.ts +1 -0
  156. package/src/tests/runtime-child-token-cleanup.test.ts +3 -3
  157. package/src/tests/script-connections.test.ts +4 -0
  158. package/src/tests/session-route-posture.test.ts +2 -0
  159. package/src/tests/skill-script-refs.test.ts +63 -0
  160. package/src/tests/slack-render-v2-query-plans.test.ts +147 -0
  161. package/src/tests/worker-token-cli.test.ts +76 -0
  162. package/src/tests/workflow-executors.test.ts +4 -2
  163. package/src/tests/workflow-system-one-extraction.test.ts +805 -0
  164. package/src/types.ts +2 -1
  165. package/src/utils/env-flag.ts +9 -0
  166. package/src/utils/request-auth-context.ts +2 -1
  167. package/src/workflows/definition.ts +7 -1
  168. package/src/workflows/engine.ts +18 -12
  169. package/src/workflows/executors/index.ts +1 -0
  170. package/src/workflows/executors/registry.ts +2 -0
  171. package/src/workflows/executors/system-one-decision.ts +56 -255
  172. package/src/workflows/executors/system-one-extract-wire.ts +57 -0
  173. package/src/workflows/executors/system-one-extraction.ts +830 -0
  174. package/src/workflows/executors/system-one-providers.ts +277 -110
  175. package/src/workflows/executors/system-one-transport.ts +264 -0
  176. package/templates/official/lead/HEARTBEAT.md +1 -1
  177. package/templates/skills/apps/SKILL.md +4 -1
  178. package/templates/skills/apps/content.md +4 -1
  179. package/templates/skills/code-quality/content.md +9 -14
  180. package/templates/skills/heartbeat-runbook/content.md +3 -3
  181. package/dist/cli-v1sjhn52.js +0 -7
  182. package/dist/handlers-dkcarkd0.js +0 -39
  183. package/dist/index-5jecgp3n.js +0 -41
@@ -1,17 +1,21 @@
1
1
  import { DEFAULT_OPENROUTER_BASE_URL, getOpenRouterBaseUrl } from "../../utils/openrouter-base-url";
2
2
  import type { ExecutorDependencies } from "./base";
3
+ import { CLOUDFLARE_EXTRACT_WIRE, type SystemOneExtractWire } from "./system-one-extract-wire";
3
4
  import { OPENAI_DECISIONS_WIRE } from "./system-one-openai-wire";
4
5
  import { CLOUDFLARE_WIRE, type SystemOneWire } from "./system-one-wire";
5
6
  import { resolveWorkflowLlmConfig } from "./workflow-llm";
6
7
 
7
8
  /**
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`.
9
+ * Hosts of System One models. One registry serves every capability: a host
10
+ * declares what it serves (`decide` for typed decisions: Jev, laya, OpenAI
11
+ * Decisions, Cloudflare Clef; `extract` for typed extraction: Cloudflare Clairic)
12
+ * and, per capability, its endpoint, models, limits, and wire. The credential, the
13
+ * deployment check, and every message are the host's, so a decision node and an
14
+ * extraction node on the same host fail with the same sentence.
15
+ *
16
+ * A node names a host by id (`config.provider`); the endpoint, header shape, and
17
+ * key name live here, never in a workflow definition. Adding a host, or a
18
+ * capability to one, is one entry in `SYSTEM_ONE_HOSTS`.
15
19
  *
16
20
  * Do not add a chat-completions route. OpenRouter's `typesafe/jev-router` is a
17
21
  * router that forwards a chat request to another model, and `~typesafe/jev-latest`
@@ -19,6 +23,8 @@ import { resolveWorkflowLlmConfig } from "./workflow-llm";
19
23
  * runs Jev itself.
20
24
  */
21
25
 
26
+ export type SystemOneCapability = "decide" | "extract";
27
+
22
28
  export interface SystemOneProviderContext {
23
29
  db: ExecutorDependencies["db"];
24
30
  env: NodeJS.ProcessEnv;
@@ -48,13 +54,18 @@ export interface SystemOneBuiltEndpoint {
48
54
  readonly build: (value: string, model: string) => string | null;
49
55
  }
50
56
 
51
- export interface SystemOneProvider {
52
- /** Name used in messages, e.g. "TypeSafe". */
53
- readonly label: string;
54
- /** The single credential this host needs: a global secret name. */
55
- readonly keyName: string;
57
+ export type SystemOneEndpoint = string | SystemOneConfiguredEndpoint | SystemOneBuiltEndpoint;
58
+
59
+ /** A credential read from config, with the name it was read under. */
60
+ export interface SystemOneCredential {
61
+ value: string;
62
+ keyName: string;
63
+ }
64
+
65
+ /** What every capability of a host declares. */
66
+ interface SystemOneServiceBase {
56
67
  /** 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;
68
+ readonly endpoint: SystemOneEndpoint;
58
69
  /**
59
70
  * Model used when the node sets none. Absent means no `model` is sent and the
60
71
  * host picks, for a host that ignores a model id it does not know.
@@ -62,6 +73,9 @@ export interface SystemOneProvider {
62
73
  readonly defaultModel?: string;
63
74
  /** When set, the only model ids the node may use (checked at save and at run). */
64
75
  readonly allowedModels?: readonly string[];
76
+ }
77
+
78
+ export interface SystemOneDecideService extends SystemOneServiceBase {
65
79
  /** Host limits stricter than the node's own. */
66
80
  readonly limits?: { readonly maxQuestions?: number };
67
81
  /** Request and response translation. Absent means the SystemOne wire as is. */
@@ -71,18 +85,48 @@ export interface SystemOneProvider {
71
85
  * the output reports and what a `humanReview` band is tested against.
72
86
  */
73
87
  readonly confidenceField: "confidence" | "answer_confidence";
88
+ }
89
+
90
+ export interface SystemOneExtractService extends SystemOneServiceBase {
91
+ /** Host limits stricter than the node's own. */
92
+ readonly limits?: { readonly maxFields?: number; readonly maxImages?: number };
93
+ /** Request and response translation. */
94
+ readonly wire: SystemOneExtractWire;
95
+ }
96
+
97
+ export interface SystemOneHost {
98
+ /** Name used in messages, e.g. "TypeSafe". */
99
+ readonly label: string;
100
+ /** The credential this host needs: a global secret name, the one messages tell an author to set. */
101
+ readonly keyName: string;
102
+ /** Other secret names read, in order, when `keyName` is unset. Messages mention them. */
103
+ readonly fallbackKeyNames?: readonly string[];
74
104
  /** A deployment setting that makes this host unusable, or null. */
75
105
  readonly deploymentProblem?: (env: NodeJS.ProcessEnv) => string | null;
76
- /** The credential value, or null/blank when absent. May throw; callers never relay the cause. */
77
- readonly readKey: (ctx: SystemOneProviderContext) => Promise<string | null | undefined>;
106
+ /**
107
+ * The credential value (a string is read under `keyName`), or null/blank when
108
+ * absent. May throw; callers never relay the cause.
109
+ */
110
+ readonly readKey: (
111
+ ctx: SystemOneProviderContext,
112
+ ) => Promise<SystemOneCredential | string | null | undefined>;
113
+ readonly decide?: SystemOneDecideService;
114
+ readonly extract?: SystemOneExtractService;
78
115
  }
79
116
 
117
+ /**
118
+ * A decision host as one flat record: the host's credential and messages plus its
119
+ * `decide` service. `system-one-decision` reads this shape.
120
+ */
121
+ export type SystemOneProvider = Omit<SystemOneHost, "decide" | "extract"> & SystemOneDecideService;
122
+
80
123
  const TYPESAFE_KEY = "TYPESAFE_API_KEY";
81
124
  const OPENROUTER_KEY = "OPENROUTER_API_KEY";
82
125
  const LAYA_KEY = "LAYA_API_KEY";
83
126
  const LAYA_URL_KEY = "LAYA_URL";
84
127
  const OPENAI_DECISIONS_KEY = "OPENAI_DECISIONS_API_KEY";
85
128
  const CLOUDFLARE_TOKEN_KEY = "CLOUDFLARE_API_TOKEN";
129
+ const CLOUDFLARE_KEY_FALLBACK = "CLOUDFLARE_API_KEY";
86
130
  const CLOUDFLARE_ACCOUNT_KEY = "CLOUDFLARE_ACCOUNT_ID";
87
131
  const CLOUDFLARE_ACCOUNT_RE = /^[0-9a-f]{32}$/;
88
132
 
@@ -95,21 +139,44 @@ async function readGlobalConfig(
95
139
  return rows.find((row) => row.scope === "global" && row.key === key)?.value;
96
140
  }
97
141
 
98
- export const SYSTEM_ONE_PROVIDERS = {
142
+ /** The first of `keys` with a non-blank global value, with the key it came from. */
143
+ async function readFirstGlobalConfig(
144
+ db: ExecutorDependencies["db"],
145
+ keys: readonly string[],
146
+ ): Promise<SystemOneCredential | undefined> {
147
+ for (const keyName of keys) {
148
+ const value = await readGlobalConfig(db, keyName);
149
+ if (value && value.trim() !== "") return { value, keyName };
150
+ }
151
+ return undefined;
152
+ }
153
+
154
+ /** Every Workers AI model is reached at the same account-scoped route; only the model differs. */
155
+ const CLOUDFLARE_ENDPOINT: SystemOneBuiltEndpoint = {
156
+ configKey: CLOUDFLARE_ACCOUNT_KEY,
157
+ describe: "the 32-character id of a Cloudflare account",
158
+ build: (accountId, model) => {
159
+ const id = accountId.trim();
160
+ return CLOUDFLARE_ACCOUNT_RE.test(id)
161
+ ? `https://api.cloudflare.com/client/v4/accounts/${id}/ai/run/@cf/cloudflare/${model}`
162
+ : null;
163
+ },
164
+ };
165
+
166
+ export const SYSTEM_ONE_HOSTS = {
99
167
  typesafe: {
100
168
  label: "TypeSafe",
101
169
  keyName: TYPESAFE_KEY,
102
- endpoint: "https://api.typesafe.ai/v1/systemone",
103
- defaultModel: "jev-latest",
104
- confidenceField: "confidence",
105
170
  readKey: ({ db }) => readGlobalConfig(db, TYPESAFE_KEY),
171
+ decide: {
172
+ endpoint: "https://api.typesafe.ai/v1/systemone",
173
+ defaultModel: "jev-latest",
174
+ confidenceField: "confidence",
175
+ },
106
176
  },
107
177
  openrouter: {
108
178
  label: "OpenRouter",
109
179
  keyName: OPENROUTER_KEY,
110
- endpoint: "https://openrouter.ai/api/alpha/decisions",
111
- defaultModel: "~typesafe/jev-latest",
112
- confidenceField: "confidence",
113
180
  deploymentProblem(env) {
114
181
  // A gateway URL means OPENROUTER_API_KEY may be a gateway token, and the
115
182
  // decisions API is a sibling of /v1 that a chat gateway need not forward.
@@ -127,65 +194,114 @@ export const SYSTEM_ONE_PROVIDERS = {
127
194
  return null;
128
195
  }
129
196
  },
197
+ decide: {
198
+ endpoint: "https://openrouter.ai/api/alpha/decisions",
199
+ defaultModel: "~typesafe/jev-latest",
200
+ confidenceField: "confidence",
201
+ },
130
202
  },
131
203
  laya: {
132
204
  label: "laya",
133
205
  keyName: LAYA_KEY,
134
- endpoint: { configKey: LAYA_URL_KEY, path: "/v1/systemone" },
135
- // No default model. laya answers an id it does not know (`jev-latest` included) by
136
- // routing on the text instead, with HTTP 200, so a default would be sent as if it meant something.
137
- // `confidence` on a choice or score is entropy-based (0.47 top probability reads
138
- // ~0.15). `answer_confidence` is the top probability, the number a `noul` band
139
- // already uses, so one band means the same thing on every question type.
140
- confidenceField: "answer_confidence",
141
206
  readKey: ({ db }) => readGlobalConfig(db, LAYA_KEY),
207
+ decide: {
208
+ endpoint: { configKey: LAYA_URL_KEY, path: "/v1/systemone" },
209
+ // No default model. laya answers an id it does not know (`jev-latest` included) by
210
+ // routing on the text instead, with HTTP 200, so a default would be sent as if it meant something.
211
+ // `confidence` on a choice or score is entropy-based (0.47 top probability reads
212
+ // ~0.15). `answer_confidence` is the top probability, the number a `noul` band
213
+ // already uses, so one band means the same thing on every question type.
214
+ confidenceField: "answer_confidence",
215
+ },
142
216
  },
143
217
  openai: {
144
218
  label: "OpenAI",
145
219
  // Not OPENAI_API_KEY: a global row of that name reaches every worker's env and
146
220
  // can change harness auth. This key serves this node only, and needs Decisions access.
147
221
  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
222
  readKey: ({ db }) => readGlobalConfig(db, OPENAI_DECISIONS_KEY),
223
+ decide: {
224
+ endpoint: "https://api.openai.com/v1/decisions",
225
+ defaultModel: "gpt-6-luna",
226
+ // OpenAI's own field; it is not the top probability.
227
+ confidenceField: "confidence",
228
+ wire: OPENAI_DECISIONS_WIRE,
229
+ },
154
230
  },
155
231
  cloudflare: {
156
232
  label: "Cloudflare Workers AI",
157
233
  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
- },
234
+ // A Workers AI API token is a bearer token whatever the secret is called; a
235
+ // deployment that stored it as CLOUDFLARE_API_KEY works without a rename.
236
+ fallbackKeyNames: [CLOUDFLARE_KEY_FALLBACK],
237
+ readKey: ({ db }) => readFirstGlobalConfig(db, [CLOUDFLARE_TOKEN_KEY, CLOUDFLARE_KEY_FALLBACK]),
238
+ decide: {
239
+ endpoint: CLOUDFLARE_ENDPOINT,
240
+ defaultModel: "clef",
241
+ // The model is part of the URL, so only these ids are accepted.
242
+ allowedModels: ["clef", "clef-flash"],
243
+ limits: { maxQuestions: 64 },
244
+ confidenceField: "confidence",
245
+ wire: CLOUDFLARE_WIRE,
246
+ },
247
+ extract: {
248
+ endpoint: CLOUDFLARE_ENDPOINT,
249
+ defaultModel: "clairic",
250
+ allowedModels: ["clairic"],
251
+ limits: { maxFields: 64, maxImages: 4 },
252
+ wire: CLOUDFLARE_EXTRACT_WIRE,
167
253
  },
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
254
  },
176
- } as const satisfies Record<string, SystemOneProvider>;
255
+ } as const satisfies Record<string, SystemOneHost>;
256
+
257
+ export type SystemOneHostId = keyof typeof SYSTEM_ONE_HOSTS;
177
258
 
178
- export type SystemOneProviderId = keyof typeof SYSTEM_ONE_PROVIDERS;
259
+ type HostsServing<C extends SystemOneCapability> = {
260
+ [K in SystemOneHostId]: (typeof SYSTEM_ONE_HOSTS)[K] extends Record<C, object> ? K : never;
261
+ }[SystemOneHostId];
262
+
263
+ /** A host that serves typed decisions. */
264
+ export type SystemOneProviderId = HostsServing<"decide">;
265
+ /** A host that serves typed extraction. */
266
+ export type SystemOneExtractProviderId = HostsServing<"extract">;
267
+
268
+ function hostsServing<C extends SystemOneCapability>(
269
+ capability: C,
270
+ ): [HostsServing<C>, ...HostsServing<C>[]] {
271
+ const ids = (Object.keys(SYSTEM_ONE_HOSTS) as SystemOneHostId[]).filter(
272
+ (id) => (SYSTEM_ONE_HOSTS[id] as SystemOneHost)[capability] !== undefined,
273
+ );
274
+ return ids as [HostsServing<C>, ...HostsServing<C>[]];
275
+ }
179
276
 
180
- export const SYSTEM_ONE_PROVIDER_IDS = Object.keys(SYSTEM_ONE_PROVIDERS) as [
181
- SystemOneProviderId,
182
- ...SystemOneProviderId[],
183
- ];
277
+ /** The decision view of every host that decides: the host's shared fields plus its `decide` service. */
278
+ export const SYSTEM_ONE_PROVIDERS = Object.fromEntries(
279
+ hostsServing("decide").map((id) => {
280
+ const { decide, extract: _extract, ...host } = SYSTEM_ONE_HOSTS[id] as SystemOneHost;
281
+ return [id, { ...host, ...decide }];
282
+ }),
283
+ ) as {
284
+ [K in SystemOneProviderId]: Omit<(typeof SYSTEM_ONE_HOSTS)[K], "decide" | "extract"> &
285
+ (typeof SYSTEM_ONE_HOSTS)[K]["decide"];
286
+ };
287
+
288
+ export const SYSTEM_ONE_PROVIDER_IDS = hostsServing("decide");
289
+ export const SYSTEM_ONE_EXTRACT_PROVIDER_IDS = hostsServing("extract");
184
290
 
185
291
  export const SYSTEM_ONE_DEFAULT_PROVIDER: SystemOneProviderId = "typesafe";
292
+ export const SYSTEM_ONE_EXTRACT_DEFAULT_PROVIDER: SystemOneExtractProviderId = "cloudflare";
186
293
 
187
294
  export function isSystemOneProviderId(value: unknown): value is SystemOneProviderId {
188
- return typeof value === "string" && Object.hasOwn(SYSTEM_ONE_PROVIDERS, value);
295
+ return (
296
+ typeof value === "string" && (SYSTEM_ONE_PROVIDER_IDS as readonly string[]).includes(value)
297
+ );
298
+ }
299
+
300
+ export function isSystemOneExtractProviderId(value: unknown): value is SystemOneExtractProviderId {
301
+ return (
302
+ typeof value === "string" &&
303
+ (SYSTEM_ONE_EXTRACT_PROVIDER_IDS as readonly string[]).includes(value)
304
+ );
189
305
  }
190
306
 
191
307
  /** Provider a raw node config selects. Undefined when the field is present but not a known id. */
@@ -196,89 +312,128 @@ export function systemOneProviderOf(
196
312
  return isSystemOneProviderId(config.provider) ? config.provider : undefined;
197
313
  }
198
314
 
315
+ /** Extraction provider a raw node config selects. Undefined when present but not a known id. */
316
+ export function systemOneExtractProviderOf(
317
+ config: Record<string, unknown>,
318
+ ): SystemOneExtractProviderId | undefined {
319
+ if (config.provider === undefined) return SYSTEM_ONE_EXTRACT_DEFAULT_PROVIDER;
320
+ return isSystemOneExtractProviderId(config.provider) ? config.provider : undefined;
321
+ }
322
+
323
+ /** The service a host runs for one capability. Callers pass an id that serves it. */
324
+ function serviceOf(id: SystemOneHostId, capability: SystemOneCapability): SystemOneServiceBase {
325
+ const service = (SYSTEM_ONE_HOSTS[id] as SystemOneHost)[capability];
326
+ if (!service) throw new Error(`SystemOne provider "${id}" does not serve ${capability}`);
327
+ return service;
328
+ }
329
+
199
330
  // ─── Messages ───────────────────────────────────────────────
200
331
  //
201
332
  // Every message names the exact credential and where to set it. They are shared by
202
333
  // the save-time warning, the run-start stop, and the step itself, so an author
203
334
  // sees the same sentence wherever the problem surfaces.
204
335
 
336
+ /** What a call that failed did not produce, for the end of a message. */
337
+ const NOTHING_PRODUCED: Record<SystemOneCapability, string> = {
338
+ decide: "No decision was made.",
339
+ extract: "Nothing was extracted.",
340
+ };
341
+
342
+ export function systemOneNothingProduced(capability: SystemOneCapability): string {
343
+ return NOTHING_PRODUCED[capability];
344
+ }
345
+
205
346
  const SET_IT = (keyName: string) =>
206
347
  `Set ${keyName} as a global secret on the Secrets page (Settings > Secrets).`;
207
348
 
208
349
  const SET_URL = (configKey: string) =>
209
350
  `Set ${configKey} as a global config value (set-config with scope global).`;
210
351
 
211
- export function systemOneKeyMissingMessage(id: SystemOneProviderId): string {
212
- const { label, keyName } = SYSTEM_ONE_PROVIDERS[id];
213
- return `${keyName} is not configured. SystemOne provider "${id}" needs a working ${label} API key. ${SET_IT(keyName)}`;
352
+ export function systemOneKeyMissingMessage(id: SystemOneHostId): string {
353
+ const { label, keyName, fallbackKeyNames }: SystemOneHost = SYSTEM_ONE_HOSTS[id];
354
+ const also = fallbackKeyNames?.length
355
+ ? ` (${fallbackKeyNames.join(", ")} is read when it is unset, and is not configured either)`
356
+ : "";
357
+ return `${keyName} is not configured${also}. SystemOne provider "${id}" needs a working ${label} API key. ${SET_IT(keyName)}`;
214
358
  }
215
359
 
216
- export function systemOneKeyUnreadableMessage(id: SystemOneProviderId): string {
360
+ export function systemOneKeyUnreadableMessage(id: SystemOneHostId): string {
217
361
  // The underlying error can name a config row id; keep the message generic.
218
- return `Could not read ${SYSTEM_ONE_PROVIDERS[id].keyName} from swarm config`;
362
+ return `Could not read ${SYSTEM_ONE_HOSTS[id].keyName} from swarm config`;
219
363
  }
220
364
 
221
- export function systemOneKeyMalformedMessage(id: SystemOneProviderId): string {
222
- return `${SYSTEM_ONE_PROVIDERS[id].keyName} is not a valid bearer token value. ${SET_IT(SYSTEM_ONE_PROVIDERS[id].keyName)}`;
365
+ export function systemOneKeyMalformedMessage(id: SystemOneHostId, keyName?: string): string {
366
+ const name = keyName ?? SYSTEM_ONE_HOSTS[id].keyName;
367
+ return `${name} is not a valid bearer token value. ${SET_IT(name)}`;
223
368
  }
224
369
 
225
370
  /** A 401 or 403 from the host: the key exists but the host does not accept it. */
226
371
  export function systemOneKeyRejectedMessage(
227
- id: SystemOneProviderId,
372
+ id: SystemOneHostId,
228
373
  status: number,
229
374
  code?: string,
375
+ options: { keyName?: string; capability?: SystemOneCapability } = {},
230
376
  ): string {
231
- const { label, keyName } = SYSTEM_ONE_PROVIDERS[id];
377
+ const { label }: SystemOneHost = SYSTEM_ONE_HOSTS[id];
378
+ const keyName = options.keyName ?? SYSTEM_ONE_HOSTS[id].keyName;
232
379
  const detail = code ? ` (${code})` : "";
233
380
  return (
234
381
  `${keyName} was rejected by ${label} (HTTP ${status}${detail}). ` +
235
- `The key is invalid, revoked, or lacks access to the model. Replace it on the Secrets page (Settings > Secrets). No decision was made.`
382
+ `The key is invalid, revoked, or lacks access to the model. Replace it on the Secrets page (Settings > Secrets). ${NOTHING_PRODUCED[options.capability ?? "decide"]}`
236
383
  );
237
384
  }
238
385
 
239
- function urlKeyOf(provider: SystemOneProvider): string | null {
240
- return typeof provider.endpoint === "string" ? null : provider.endpoint.configKey;
386
+ function urlKeyOf(endpoint: SystemOneEndpoint): string | null {
387
+ return typeof endpoint === "string" ? null : endpoint.configKey;
241
388
  }
242
389
 
243
- function isBuiltEndpoint(
244
- endpoint: SystemOneProvider["endpoint"],
245
- ): endpoint is SystemOneBuiltEndpoint {
390
+ function isBuiltEndpoint(endpoint: SystemOneEndpoint): endpoint is SystemOneBuiltEndpoint {
246
391
  return typeof endpoint !== "string" && "build" in endpoint;
247
392
  }
248
393
 
249
394
  /** 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`;
395
+ function urlDescriptionOf(id: SystemOneHostId, endpoint: SystemOneEndpoint): string {
396
+ return isBuiltEndpoint(endpoint)
397
+ ? endpoint.describe
398
+ : `the URL of a ${SYSTEM_ONE_HOSTS[id].label} server`;
254
399
  }
255
400
 
256
- export function systemOneUrlMissingMessage(id: SystemOneProviderId): string {
257
- const provider: SystemOneProvider = SYSTEM_ONE_PROVIDERS[id];
258
- const configKey = urlKeyOf(provider) ?? "";
259
- return `${configKey} is not configured. SystemOne provider "${id}" needs ${urlDescriptionOf(provider)}. ${SET_URL(configKey)}`;
401
+ export function systemOneUrlMissingMessage(
402
+ id: SystemOneHostId,
403
+ capability: SystemOneCapability = "decide",
404
+ ): string {
405
+ const { endpoint } = serviceOf(id, capability);
406
+ const configKey = urlKeyOf(endpoint) ?? "";
407
+ return `${configKey} is not configured. SystemOne provider "${id}" needs ${urlDescriptionOf(id, endpoint)}. ${SET_URL(configKey)}`;
260
408
  }
261
409
 
262
- export function systemOneUrlUnreadableMessage(id: SystemOneProviderId): string {
263
- return `Could not read ${urlKeyOf(SYSTEM_ONE_PROVIDERS[id]) ?? ""} from swarm config`;
410
+ export function systemOneUrlUnreadableMessage(
411
+ id: SystemOneHostId,
412
+ capability: SystemOneCapability = "decide",
413
+ ): string {
414
+ return `Could not read ${urlKeyOf(serviceOf(id, capability).endpoint) ?? ""} from swarm config`;
264
415
  }
265
416
 
266
- export function systemOneUrlInvalidMessage(id: SystemOneProviderId): string {
267
- const provider: SystemOneProvider = SYSTEM_ONE_PROVIDERS[id];
268
- const configKey = urlKeyOf(provider) ?? "";
417
+ export function systemOneUrlInvalidMessage(
418
+ id: SystemOneHostId,
419
+ capability: SystemOneCapability = "decide",
420
+ ): string {
421
+ const { endpoint } = serviceOf(id, capability);
422
+ const configKey = urlKeyOf(endpoint) ?? "";
269
423
  // 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)}`;
424
+ if (isBuiltEndpoint(endpoint)) {
425
+ return `${configKey} is not ${endpoint.describe}. ${SET_URL(configKey)}`;
272
426
  }
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)}`;
427
+ return `${configKey} is not a usable ${SYSTEM_ONE_HOSTS[id].label} server URL. It must be an https URL (http only for localhost) with no credentials, query, or fragment. ${SET_URL(configKey)}`;
274
428
  }
275
429
 
276
430
  /** Why a provider cannot use this model, or null. The value is not echoed: it may come from run input. */
277
431
  export function systemOneModelProblem(
278
- id: SystemOneProviderId,
432
+ id: SystemOneHostId,
279
433
  model: string | undefined,
434
+ capability: SystemOneCapability = "decide",
280
435
  ): string | null {
281
- const { allowedModels }: SystemOneProvider = SYSTEM_ONE_PROVIDERS[id];
436
+ const { allowedModels } = serviceOf(id, capability);
282
437
  if (!allowedModels || (model !== undefined && allowedModels.includes(model))) return null;
283
438
  return `SystemOne provider "${id}" serves only the models ${allowedModels.join(", ")}; set model to one of them or leave it unset.`;
284
439
  }
@@ -327,16 +482,24 @@ export function systemOneEndpointFromBase(base: string, path: string): string |
327
482
  }
328
483
 
329
484
  export type SystemOneTarget =
330
- | { ok: true; apiKey: string; endpoint: string }
485
+ | {
486
+ ok: true;
487
+ apiKey: string;
488
+ endpoint: string;
489
+ /** The secret name the key was read under, for a rejection message. */
490
+ keyName: string;
491
+ }
331
492
  | { ok: false; error: string };
332
493
 
333
494
  export interface SystemOneTargetOverrides {
334
495
  /** Replaces the credential lookup. */
335
- apiKey?: (id: SystemOneProviderId) => Promise<string | null | undefined>;
496
+ apiKey?: (id: SystemOneHostId) => Promise<string | null | undefined>;
336
497
  /** Replaces the lookup of a configured server's base URL (or account id). */
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. */
498
+ serverUrl?: (id: SystemOneHostId) => Promise<string | null | undefined>;
499
+ /** The model the call uses, for a host whose URL names it. Defaults to the service's default. */
339
500
  model?: string;
501
+ /** Which of the host's services is reached. Defaults to `decide`. */
502
+ capability?: SystemOneCapability;
340
503
  }
341
504
 
342
505
  /**
@@ -346,21 +509,23 @@ export interface SystemOneTargetOverrides {
346
509
  * fakes); the deployment check and the blank / malformed / URL checks still apply.
347
510
  */
348
511
  export async function resolveSystemOneTarget(
349
- id: SystemOneProviderId,
512
+ id: SystemOneHostId,
350
513
  ctx: SystemOneProviderContext,
351
514
  overrides: SystemOneTargetOverrides = {},
352
515
  ): Promise<SystemOneTarget> {
353
- const provider: SystemOneProvider = SYSTEM_ONE_PROVIDERS[id];
354
- const deploymentProblem = provider.deploymentProblem?.(ctx.env);
516
+ const host: SystemOneHost = SYSTEM_ONE_HOSTS[id];
517
+ const capability = overrides.capability ?? "decide";
518
+ const service = serviceOf(id, capability);
519
+ const deploymentProblem = host.deploymentProblem?.(ctx.env);
355
520
  if (deploymentProblem) return { ok: false, error: deploymentProblem };
356
521
 
357
522
  const errors: string[] = [];
358
523
 
359
524
  let endpoint: string | undefined;
360
- if (typeof provider.endpoint === "string") {
361
- endpoint = provider.endpoint;
525
+ if (typeof service.endpoint === "string") {
526
+ endpoint = service.endpoint;
362
527
  } else {
363
- const { configKey } = provider.endpoint;
528
+ const { configKey } = service.endpoint;
364
529
  let base: string | null | undefined;
365
530
  let readable = true;
366
531
  try {
@@ -369,34 +534,36 @@ export async function resolveSystemOneTarget(
369
534
  : readGlobalConfig(ctx.db, configKey));
370
535
  } catch {
371
536
  readable = false;
372
- errors.push(systemOneUrlUnreadableMessage(id));
537
+ errors.push(systemOneUrlUnreadableMessage(id, capability));
373
538
  }
374
539
  if (readable) {
375
540
  if (!base || base.trim() === "") {
376
- errors.push(systemOneUrlMissingMessage(id));
541
+ errors.push(systemOneUrlMissingMessage(id, capability));
377
542
  } else {
378
- const resolved = isBuiltEndpoint(provider.endpoint)
379
- ? provider.endpoint.build(base, overrides.model ?? provider.defaultModel ?? "")
380
- : systemOneEndpointFromBase(base, provider.endpoint.path);
543
+ const resolved = isBuiltEndpoint(service.endpoint)
544
+ ? service.endpoint.build(base, overrides.model ?? service.defaultModel ?? "")
545
+ : systemOneEndpointFromBase(base, service.endpoint.path);
381
546
  if (resolved) endpoint = resolved;
382
- else errors.push(systemOneUrlInvalidMessage(id));
547
+ else errors.push(systemOneUrlInvalidMessage(id, capability));
383
548
  }
384
549
  }
385
550
  }
386
551
 
387
- let apiKey: string | null | undefined;
552
+ let read: SystemOneCredential | string | null | undefined;
388
553
  let keyProblem: string | undefined;
389
554
  try {
390
- apiKey = await (overrides.apiKey ? overrides.apiKey(id) : provider.readKey(ctx));
555
+ read = await (overrides.apiKey ? overrides.apiKey(id) : host.readKey(ctx));
391
556
  } catch {
392
557
  keyProblem = systemOneKeyUnreadableMessage(id);
393
558
  }
559
+ const apiKey = typeof read === "string" ? read : read?.value;
560
+ const keyName = (typeof read === "object" && read?.keyName) || host.keyName;
394
561
  if (!keyProblem) {
395
562
  if (!apiKey || apiKey.trim() === "") keyProblem = systemOneKeyMissingMessage(id);
396
- else if (hasWhitespaceOrControl(apiKey)) keyProblem = systemOneKeyMalformedMessage(id);
563
+ else if (hasWhitespaceOrControl(apiKey)) keyProblem = systemOneKeyMalformedMessage(id, keyName);
397
564
  }
398
565
  if (keyProblem) errors.push(keyProblem);
399
566
 
400
567
  if (errors.length > 0 || !endpoint || !apiKey) return { ok: false, error: errors.join(" ") };
401
- return { ok: true, apiKey, endpoint };
568
+ return { ok: true, apiKey, endpoint, keyName };
402
569
  }