@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
package/src/types.ts CHANGED
@@ -2287,7 +2287,7 @@ export const WorkflowNodeSchema = z
2287
2287
  type: z
2288
2288
  .string()
2289
2289
  .describe(
2290
- "Executor type: 'agent-task', 'script', 'swarm-script', 'raw-llm', 'system-one-decision', 'validate', 'property-match'",
2290
+ "Executor type: 'agent-task', 'script', 'swarm-script', 'raw-llm', 'system-one-decision', 'system-one-extraction', 'validate', 'property-match'",
2291
2291
  ),
2292
2292
  label: z.string().optional().describe("Human-readable label for UI display"),
2293
2293
  config: z
@@ -2297,6 +2297,7 @@ export const WorkflowNodeSchema = z
2297
2297
  "For script: { runtime, script, args?, timeout? }. " +
2298
2298
  "For swarm-script: { scriptName, scope?, pinHash?, args?, fsMode?, timeoutMs? (1000-300000) }. " +
2299
2299
  "For system-one-decision (typed decisions, Jev by default): { provider? ('typesafe' default | 'openrouter' | 'laya'; a literal, never a {{token}}), state, questions: { <id>: { type: 'noul'|'choice'|'score', instructions, criteria? } }, returns: { <id>: { type } }, model? (provider-specific; unset = the provider's default, and laya has none so it sends no model and picks its own checkpoint), timeoutMs?, maxRetries? (0-3), humanReview? { band: { min, max } (0-1, inclusive), approvers: { users?, roles?, policy }, title?, timeout?, notifications? } }; each provider needs its own global secret (TYPESAFE_API_KEY, OPENROUTER_API_KEY, or LAYA_API_KEY; laya also needs the global config LAYA_URL), and a save warns and a run fails before any node executes when one is missing; system-one-decision nodes must not set retry or validation.retry. With humanReview, an answer whose confidence is inside the band waits for a person (human-in-the-loop approval), and next must map ports { approved, rejected?, timeout? }. " +
2300
+ "For system-one-extraction (typed extraction, Cloudflare Clairic by default): { provider? ('cloudflare'; a literal), state (text to read; may be empty when images are sent), fields: { <id>: { type: 'text'|'number'|'date'|'time'|'datetime'|'duration'|'list'|'estimate'|'forecast', instructions?, ...type options (number: unit?, integer?, decimals?; duration: unit? (seconds..years), integer?, decimals?; list: items ('text'|'number'|'date'); estimate: min, max, step?, scale?, unit?; forecast: min, max, horizon?, interval?, scale?, unit?; text/number/date/time/datetime/duration: null_threshold?) } }, images? (max 4 base64 data: URLs or { content_type, base64 }), referenceDate? (YYYY-MM-DD), model?, timeoutMs?, maxRetries? (0-3) }; output answers.<id> carries value (null when the input does not say), guess, p_null, confidence, span/spans, and for estimate/forecast median/p10/p90. Needs the global secret CLOUDFLARE_API_TOKEN (or CLOUDFLARE_API_KEY) and the global config CLOUDFLARE_ACCOUNT_ID; must not set retry or validation.retry. " +
2300
2301
  "Agent-task templates and ordinary config values support {{interpolation}} from the node's inputs context, including trigger and declared upstream aliases. " +
2301
2302
  "SECURITY: executable source for script/swarm-script nodes does not interpolate trigger.* or upstream node outputs; only input/workflow/swarm/run values are allowed in inline script source, and named swarm-script source is not workflow-interpolated. " +
2302
2303
  'Pass dynamic values through config.args instead (inline script receives them as argv, with runtime: "bash" running bash -c <script> ...args so args[0] is $0 and args[1] is $1; swarm-script receives its args object). ' +
@@ -53,3 +53,12 @@ export function isEnvFlagEnabled(
53
53
  ): boolean {
54
54
  return parseEnvFlag(env[key], defaultValue);
55
55
  }
56
+
57
+ /**
58
+ * Whether the session-end summary is turned off via `SKIP_SESSION_SUMMARY`.
59
+ * Every harness (claude Stop hook, pi, opencode, codex) gates on this, so
60
+ * `1`/`true` skip and unset/empty/`0`/`false` keep the summary everywhere.
61
+ */
62
+ export function isSessionSummarySkipped(env: NodeJS.ProcessEnv = process.env): boolean {
63
+ return isEnvFlagEnabled("SKIP_SESSION_SUMMARY", false, env);
64
+ }
@@ -8,9 +8,10 @@ export type HttpRequestAuth = (
8
8
  /** Password-page viewer with no user identity. Least privilege: every gate denies it. */
9
9
  | { kind: "guest" }
10
10
  | { kind: "controller"; controllerId: string; tokenId: string; expiresAt: string }
11
- /** Ephemeral token bound to an ACP task or a worker runtime (aseph_ prefix). */
11
+ /** Session token bound to an ACP task, a worker runtime, or only its agent (aseph_ prefix). */
12
12
  | {
13
13
  kind: "agent";
14
+ tokenKind: "task" | "runtime" | "agent";
14
15
  agentId: string;
15
16
  taskId: string | null;
16
17
  runtimeId: string | null;
@@ -10,6 +10,7 @@ import {
10
10
  systemOneRetryViolations,
11
11
  systemOneStaticShapeViolations,
12
12
  } from "./executors/system-one-decision";
13
+ import { systemOneExtractionStaticShapeViolations } from "./executors/system-one-extraction";
13
14
 
14
15
  /** Extract all target node IDs from a node's `next` field */
15
16
  export function getNextTargets(next: string | string[] | Record<string, string>): string[] {
@@ -212,7 +213,12 @@ export function validateDefinition(
212
213
  }
213
214
  // A system-one-decision node's question ids and return types are static, and it never carries
214
215
  // an engine retry policy (it retries transient transport errors itself).
215
- errors.push(...systemOneStaticShapeViolations(node), ...systemOneRetryViolations(node));
216
+ // A system-one-extraction node's field ids and types are static in the same way.
217
+ errors.push(
218
+ ...systemOneStaticShapeViolations(node),
219
+ ...systemOneExtractionStaticShapeViolations(node),
220
+ ...systemOneRetryViolations(node),
221
+ );
216
222
  if (node.type === "foreach") {
217
223
  validateForeachNode(node, errors);
218
224
  // A legacy `#` id may stay editable as a NORMAL node, but never as a foreach
@@ -30,7 +30,7 @@ import { findEntryNodes, getNextTargets, getSuccessors, resolveValidationPort }
30
30
  import type { AsyncExecutorResult } from "./executors/base";
31
31
  import type { ExecutorRegistry } from "./executors/registry";
32
32
  import {
33
- SYSTEM_ONE_DECISION_NODE_TYPE,
33
+ isSystemOneNodeType,
34
34
  systemOneRetryViolations,
35
35
  systemOneUnresolvedError,
36
36
  } from "./executors/system-one-decision";
@@ -854,7 +854,7 @@ async function runClaimedStep(
854
854
  await checkpointStepFailure(
855
855
  runId,
856
856
  stepId,
857
- systemOneUnresolvedError(node.id, strictUnresolved),
857
+ systemOneUnresolvedError(node.id, strictUnresolved, node.type),
858
858
  0,
859
859
  );
860
860
  return { outcome: "failed", successors: [] };
@@ -1150,6 +1150,9 @@ function buildScriptBodyCtx(
1150
1150
  return safe;
1151
1151
  }
1152
1152
 
1153
+ /** System One config fields whose whole-token references keep their JSON type. */
1154
+ const SYSTEM_ONE_DATA_FIELDS = new Set(["state", "questions", "fields", "images"]);
1155
+
1153
1156
  export function interpolateNodeConfig(
1154
1157
  node: Pick<WorkflowNode, "type" | "config" | "inputs">,
1155
1158
  interpolationCtx: Record<string, unknown>,
@@ -1160,17 +1163,20 @@ export function interpolateNodeConfig(
1160
1163
  /** Tokens that must resolve before dispatch; the caller fails the step when non-empty. */
1161
1164
  strictUnresolved?: string[];
1162
1165
  } {
1163
- if (node.type === SYSTEM_ONE_DECISION_NODE_TYPE) {
1164
- // `state` and `questions` keep a whole-token reference's JSON type (an object
1165
- // stays an object; string interpolation would flatten it to JSON text). The
1166
- // resolved value is data, never re-interpolated, so a literal `{{` inside a
1167
- // lead's text is not read as another template. Every other field, and every
1168
- // unresolved token anywhere in the config, is strict: no blank reaches the API.
1169
- const { state, questions, ...rest } = node.config;
1170
- const restResult = deepInterpolate(rest, interpolationCtx);
1166
+ if (isSystemOneNodeType(node.type)) {
1167
+ // `state` and the question / field specs (and extraction `images`) keep a
1168
+ // whole-token reference's JSON type (an object stays an object; string
1169
+ // interpolation would flatten it to JSON text). The resolved value is data,
1170
+ // never re-interpolated, so a literal `{{` inside a lead's text is not read as
1171
+ // another template. Every other field, and every unresolved token anywhere in
1172
+ // the config, is strict: no blank reaches the API.
1173
+ const rest: Record<string, unknown> = {};
1171
1174
  const dynamic: Record<string, unknown> = {};
1172
- if (Object.hasOwn(node.config, "state")) dynamic.state = state;
1173
- if (Object.hasOwn(node.config, "questions")) dynamic.questions = questions;
1175
+ for (const [key, value] of Object.entries(node.config)) {
1176
+ if (SYSTEM_ONE_DATA_FIELDS.has(key)) dynamic[key] = value;
1177
+ else rest[key] = value;
1178
+ }
1179
+ const restResult = deepInterpolate(rest, interpolationCtx);
1174
1180
  const dynamicResult = deepInterpolate(dynamic, interpolationCtx, { preserveRawTokens: true });
1175
1181
  const unresolved = [...restResult.unresolved, ...dynamicResult.unresolved];
1176
1182
  return {
@@ -14,6 +14,7 @@ export { createExecutorRegistry, ExecutorRegistry } from "./registry";
14
14
  export { ScriptExecutor } from "./script";
15
15
  export { SwarmScriptExecutor } from "./swarm-script";
16
16
  export { SystemOneDecisionExecutor } from "./system-one-decision";
17
+ export { SystemOneExtractionExecutor } from "./system-one-extraction";
17
18
  export { ValidateExecutor } from "./validate";
18
19
  export { VcsExecutor } from "./vcs";
19
20
  export { WaitExecutor } from "./wait";
@@ -11,6 +11,7 @@ import { ScriptExecutor } from "./script";
11
11
  import { SubWorkflowExecutor } from "./sub-workflow";
12
12
  import { SwarmScriptExecutor } from "./swarm-script";
13
13
  import { SystemOneDecisionExecutor } from "./system-one-decision";
14
+ import { SystemOneExtractionExecutor } from "./system-one-extraction";
14
15
  import { ValidateExecutor } from "./validate";
15
16
  import { VcsExecutor } from "./vcs";
16
17
  import { WaitExecutor } from "./wait";
@@ -72,6 +73,7 @@ export function createExecutorRegistry(deps: ExecutorDependencies): ExecutorRegi
72
73
  registry.register(new NotifyExecutor(deps));
73
74
  registry.register(new RawLlmExecutor(deps));
74
75
  registry.register(new SystemOneDecisionExecutor(deps));
76
+ registry.register(new SystemOneExtractionExecutor(deps));
75
77
  registry.register(new ScriptExecutor(deps));
76
78
  registry.register(new SwarmScriptExecutor(deps));
77
79
  registry.register(new VcsExecutor(deps));
@@ -1,6 +1,5 @@
1
1
  import { z } from "zod";
2
2
  import type { ExecutorMeta } from "../../types";
3
- import { getOpenRouterAttributionHeaders } from "../../utils/openrouter-base-url";
4
3
  import { scrubSecrets } from "../../utils/secret-scrubber";
5
4
  import {
6
5
  type ApprovalOutcome,
@@ -17,6 +16,7 @@ import {
17
16
  HumanInTheLoopExecutor,
18
17
  NotificationConfigSchema,
19
18
  } from "./human-in-the-loop";
19
+ import { SYSTEM_ONE_EXTRACTION_NODE_TYPE } from "./system-one-extraction";
20
20
  import {
21
21
  resolveSystemOneTarget,
22
22
  SYSTEM_ONE_DEFAULT_PROVIDER,
@@ -24,12 +24,20 @@ import {
24
24
  SYSTEM_ONE_PROVIDERS,
25
25
  type SystemOneProvider,
26
26
  type SystemOneProviderId,
27
- systemOneKeyRejectedMessage,
28
27
  systemOneModelProblem,
29
28
  systemOneProviderOf,
30
29
  systemOneQuestionLimitProblem,
31
30
  } from "./system-one-providers";
32
- import { cloudflareErrorCode, SYSTEM_ONE_WIRE, SystemOneWireError } from "./system-one-wire";
31
+ import {
32
+ SYSTEM_ONE_DEFAULT_MAX_RETRIES,
33
+ SYSTEM_ONE_DEFAULT_TIMEOUT_MS,
34
+ SYSTEM_ONE_MAX_RETRIES_LIMIT,
35
+ SYSTEM_ONE_MAX_TIMEOUT_MS,
36
+ SYSTEM_ONE_MIN_TIMEOUT_MS,
37
+ type SystemOneTransportOptions,
38
+ sendSystemOneRequest,
39
+ } from "./system-one-transport";
40
+ import { SYSTEM_ONE_WIRE, SystemOneWireError } from "./system-one-wire";
33
41
 
34
42
  // ─── Constants ──────────────────────────────────────────────
35
43
 
@@ -43,11 +51,13 @@ export const SYSTEM_ONE_DECISION_NODE_TYPE = "system-one-decision";
43
51
  export const SYSTEM_ONE_ENDPOINT = SYSTEM_ONE_PROVIDERS.typesafe.endpoint;
44
52
  export const SYSTEM_ONE_API_KEY_CONFIG_KEY = SYSTEM_ONE_PROVIDERS.typesafe.keyName;
45
53
 
46
- export const SYSTEM_ONE_DEFAULT_TIMEOUT_MS = 30_000;
47
- export const SYSTEM_ONE_MIN_TIMEOUT_MS = 1_000;
48
- export const SYSTEM_ONE_MAX_TIMEOUT_MS = 300_000;
49
- export const SYSTEM_ONE_DEFAULT_MAX_RETRIES = 2;
50
- export const SYSTEM_ONE_MAX_RETRIES_LIMIT = 3;
54
+ export {
55
+ SYSTEM_ONE_DEFAULT_MAX_RETRIES,
56
+ SYSTEM_ONE_DEFAULT_TIMEOUT_MS,
57
+ SYSTEM_ONE_MAX_RETRIES_LIMIT,
58
+ SYSTEM_ONE_MAX_TIMEOUT_MS,
59
+ SYSTEM_ONE_MIN_TIMEOUT_MS,
60
+ };
51
61
 
52
62
  /** Choice: 2-255 options, Score: 2-10 levels (TypeSafe primitive limits; 2 is our authoring floor). */
53
63
  const CHOICE_MIN_OPTIONS = 2;
@@ -55,14 +65,8 @@ const CHOICE_MAX_OPTIONS = 255;
55
65
  const SCORE_MIN_LEVELS = 2;
56
66
  const SCORE_MAX_LEVELS = 10;
57
67
 
58
- /** Room left before the engine watchdog fires so the executor can report its own failure. */
59
- const DEADLINE_MARGIN_MS = 250;
60
- const BACKOFF_BASE_MS = 500;
61
- const BACKOFF_CAP_MS = 8_000;
62
68
  /** A returned distribution must sum to 1 within this tolerance. It is checked, never normalized. */
63
69
  const DISTRIBUTION_TOLERANCE = 0.02;
64
- const MAX_RESPONSE_CHARS = 1_048_576;
65
- const MAX_ERROR_BODY_CHARS = 65_536;
66
70
 
67
71
  /** Output ports of a node that sets `humanReview`, the same names `human-in-the-loop` uses. */
68
72
  const REVIEW_PORTS = ["approved", "rejected", "timeout"] as const;
@@ -316,8 +320,18 @@ interface RetryShape {
316
320
  maxRetries?: number;
317
321
  }
318
322
 
323
+ /** Node types that call a System One host: paid, non-idempotent, with their own transient retries. */
324
+ const SYSTEM_ONE_NODE_LABELS: Record<string, string> = {
325
+ [SYSTEM_ONE_DECISION_NODE_TYPE]: "SystemOne Decision",
326
+ [SYSTEM_ONE_EXTRACTION_NODE_TYPE]: "SystemOne Extraction",
327
+ };
328
+
329
+ export function isSystemOneNodeType(type: string): boolean {
330
+ return Object.hasOwn(SYSTEM_ONE_NODE_LABELS, type);
331
+ }
332
+
319
333
  /**
320
- * Why a `system-one-decision` node may not carry an engine retry policy.
334
+ * Why a `system-one-decision` (or `system-one-extraction`) node may not carry an engine retry policy.
321
335
  *
322
336
  * The executor owns its transient retries (`config.maxRetries`). Engine retries
323
337
  * are not status-aware, so a node-level or validation retry would re-send 401 / 422
@@ -329,18 +343,18 @@ export function systemOneRetryViolations(node: {
329
343
  retry?: RetryShape | null;
330
344
  validation?: { retry?: RetryShape | null } | null;
331
345
  }): string[] {
332
- if (node.type !== SYSTEM_ONE_DECISION_NODE_TYPE) return [];
346
+ if (!isSystemOneNodeType(node.type)) return [];
333
347
  const violations: string[] = [];
334
348
  const attempts = (retry: RetryShape | null | undefined) =>
335
349
  retry ? (retry.maxRetries ?? ENGINE_RETRY_DEFAULT) : 0;
336
350
  if (attempts(node.retry) > 0) {
337
351
  violations.push(
338
- `Node "${node.id}" (system-one-decision) must not set retry.maxRetries > 0: the system-one-decision executor retries transient transport errors itself (config.maxRetries), and engine retries would re-send rejected requests`,
352
+ `Node "${node.id}" (${node.type}) must not set retry.maxRetries > 0: the ${node.type} executor retries transient transport errors itself (config.maxRetries), and engine retries would re-send rejected requests`,
339
353
  );
340
354
  }
341
355
  if (attempts(node.validation?.retry) > 0) {
342
356
  violations.push(
343
- `Node "${node.id}" (system-one-decision) must not set validation.retry.maxRetries > 0: a validation-driven retry would send another paid request`,
357
+ `Node "${node.id}" (${node.type}) must not set validation.retry.maxRetries > 0: a validation-driven retry would send another paid request`,
344
358
  );
345
359
  }
346
360
  return violations;
@@ -397,11 +411,17 @@ export function systemOneStaticShapeViolations(node: {
397
411
  return violations;
398
412
  }
399
413
 
400
- /** The engine fails a system-one-decision step before dispatch when any config token is unresolved. */
401
- export function systemOneUnresolvedError(nodeId: string, tokens: string[]): string {
414
+ /** The engine fails a System One step before dispatch when any config token is unresolved. */
415
+ export function systemOneUnresolvedError(
416
+ nodeId: string,
417
+ tokens: string[],
418
+ nodeType: string = SYSTEM_ONE_DECISION_NODE_TYPE,
419
+ ): string {
402
420
  const rendered = [...new Set(tokens)].map((token) => `{{${token}}}`).join(", ");
421
+ const label =
422
+ SYSTEM_ONE_NODE_LABELS[nodeType] ?? SYSTEM_ONE_NODE_LABELS[SYSTEM_ONE_DECISION_NODE_TYPE];
403
423
  return (
404
- `SystemOne Decision node "${nodeId}" has unresolved interpolation token(s): ${rendered}. ` +
424
+ `${label} node "${nodeId}" has unresolved interpolation token(s): ${rendered}. ` +
405
425
  "No request was sent. Declare the value in the node's inputs mapping or fix the path."
406
426
  );
407
427
  }
@@ -795,79 +815,13 @@ export function resolveReviewedDecision(
795
815
 
796
816
  // ─── Transport ──────────────────────────────────────────────
797
817
 
798
- export interface SystemOneDecisionExecutorOptions {
799
- /** Transport. Tests inject a fake; production uses the global `fetch`. */
800
- fetch?: typeof fetch;
818
+ export interface SystemOneDecisionExecutorOptions extends SystemOneTransportOptions {
801
819
  /** Credential lookup for any provider. Defaults to each provider's own source (see `SYSTEM_ONE_PROVIDERS`). */
802
820
  getApiKey?: (provider: SystemOneProviderId) => Promise<string | null | undefined>;
803
821
  /** Base-URL lookup for a provider with no fixed host (`laya`). Defaults to its global config value. */
804
822
  getServerUrl?: (provider: SystemOneProviderId) => Promise<string | null | undefined>;
805
823
  /** Environment the OpenRouter credential is resolved from. Defaults to `process.env`. */
806
824
  env?: NodeJS.ProcessEnv;
807
- sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
808
- now?: () => number;
809
- random?: () => number;
810
- }
811
-
812
- type Attempt =
813
- | { kind: "ok"; text: string; requestId?: string }
814
- | { kind: "timeout" }
815
- | { kind: "fail"; message: string; retryable: boolean; retryAfterMs?: number };
816
-
817
- const REQUEST_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
818
- const ERROR_CODE_RE = /^[A-Za-z0-9._:-]{1,64}$/;
819
-
820
- function readRequestId(headers: Headers): string | undefined {
821
- // Cloudflare marks every response with `cf-ray` and sends no request id.
822
- const value = headers.get("x-request-id") ?? headers.get("request-id") ?? headers.get("cf-ray");
823
- return value && REQUEST_ID_RE.test(value) ? value : undefined;
824
- }
825
-
826
- /** Retry-After as delta-seconds or an HTTP date, in ms. Undefined when absent or unparseable. */
827
- function parseRetryAfter(value: string | null, now: number): number | undefined {
828
- if (!value) return undefined;
829
- const trimmed = value.trim();
830
- if (/^\d+(\.\d+)?$/.test(trimmed)) return Math.round(Number(trimmed) * 1000);
831
- const at = Date.parse(trimmed);
832
- return Number.isNaN(at) ? undefined : Math.max(0, at - now);
833
- }
834
-
835
- /** Only a short identifier from the provider's error envelope. Never its message or echoed request. */
836
- function readErrorCode(text: string): string | undefined {
837
- if (text.length === 0 || text.length > MAX_ERROR_BODY_CHARS) return undefined;
838
- let parsed: unknown;
839
- try {
840
- parsed = JSON.parse(text);
841
- } catch {
842
- return undefined;
843
- }
844
- if (!isRecord(parsed)) return undefined;
845
- const nested = isRecord(parsed.error) ? parsed.error : undefined;
846
- const detail = isRecord(parsed.detail) ? parsed.detail : undefined;
847
- for (const candidate of [
848
- nested?.code,
849
- nested?.type,
850
- detail?.error_type,
851
- parsed.code,
852
- parsed.type,
853
- ]) {
854
- if (typeof candidate === "string" && ERROR_CODE_RE.test(candidate)) return candidate;
855
- }
856
- // Cloudflare's envelope: `{ success: false, errors: [{ code: 10000, message }] }`.
857
- return cloudflareErrorCode(parsed);
858
- }
859
-
860
- function defaultSleep(ms: number, signal: AbortSignal): Promise<void> {
861
- return new Promise((resolve) => {
862
- if (signal.aborted) return resolve();
863
- const timer = setTimeout(done, ms);
864
- function done() {
865
- clearTimeout(timer);
866
- signal.removeEventListener("abort", done);
867
- resolve();
868
- }
869
- signal.addEventListener("abort", done, { once: true });
870
- });
871
825
  }
872
826
 
873
827
  // ─── Executor ───────────────────────────────────────────────
@@ -959,19 +913,25 @@ export class SystemOneDecisionExecutor extends BaseExecutor<
959
913
  if (modelProblem) return { ok: false, error: modelProblem };
960
914
  const target = await this.resolveTarget(providerId, model);
961
915
  if (!target.ok) return { ok: false, error: target.error };
962
- const { apiKey, endpoint } = target;
916
+ const { apiKey, endpoint, keyName } = target;
963
917
 
964
918
  const wire = provider.wire ?? SYSTEM_ONE_WIRE;
965
919
  const body = JSON.stringify(wire.buildRequest(config, model));
966
920
 
967
921
  const scrub = (message: string) => message.split(apiKey).join("[REDACTED]");
968
- const sent = await this.send(
969
- providerId,
970
- endpoint,
971
- apiKey,
972
- body,
973
- config.timeoutMs,
974
- config.maxRetries,
922
+ const sent = await sendSystemOneRequest(
923
+ {
924
+ providerId,
925
+ capability: "decide",
926
+ endpoint,
927
+ apiKey,
928
+ keyName,
929
+ body,
930
+ timeoutMs: config.timeoutMs,
931
+ maxRetries: config.maxRetries,
932
+ env: this.providerContext().env,
933
+ },
934
+ this.options,
975
935
  );
976
936
  if (!sent.ok) return { ok: false, error: scrub(sent.error) };
977
937
 
@@ -1101,163 +1061,4 @@ export class SystemOneDecisionExecutor extends BaseExecutor<
1101
1061
  ): ApprovalOutcome | null {
1102
1062
  return resolveReviewedDecision(parkedOutput, approval);
1103
1063
  }
1104
-
1105
- /**
1106
- * POST once, retrying only transient failures inside one time budget.
1107
- * Retries: connection errors, HTTP 408 / 429 / 5xx (incl. 529). Never: 401, 422,
1108
- * other 4xx, redirects, or a bad success body (the caller validates that).
1109
- */
1110
- private async send(
1111
- providerId: SystemOneProviderId,
1112
- endpoint: string,
1113
- apiKey: string,
1114
- body: string,
1115
- timeoutMs: number,
1116
- maxRetries: number,
1117
- ): Promise<{ ok: true; text: string; requestId?: string } | { ok: false; error: string }> {
1118
- const doFetch = this.options.fetch ?? fetch;
1119
- const sleep = this.options.sleep ?? defaultSleep;
1120
- const now = this.options.now ?? Date.now;
1121
- const random = this.options.random ?? Math.random;
1122
-
1123
- const deadlineAt = now() + timeoutMs - DEADLINE_MARGIN_MS;
1124
- const controller = new AbortController();
1125
- const timer = setTimeout(() => controller.abort(), Math.max(0, deadlineAt - now()));
1126
- const timedOut = () =>
1127
- ({
1128
- ok: false,
1129
- error: `SystemOne request exceeded its ${timeoutMs}ms time budget`,
1130
- }) as const;
1131
-
1132
- try {
1133
- for (let attempt = 0; ; attempt++) {
1134
- const result = await this.attemptOnce(
1135
- doFetch,
1136
- providerId,
1137
- endpoint,
1138
- apiKey,
1139
- body,
1140
- controller.signal,
1141
- now,
1142
- );
1143
- if (result.kind === "ok") {
1144
- return { ok: true, text: result.text, requestId: result.requestId };
1145
- }
1146
- if (result.kind === "timeout" || controller.signal.aborted) return timedOut();
1147
-
1148
- const attempts = attempt + 1;
1149
- const suffix = attempts > 1 ? ` after ${attempts} attempts` : "";
1150
- if (!result.retryable || attempt >= maxRetries) {
1151
- return { ok: false, error: `${result.message}${suffix}` };
1152
- }
1153
-
1154
- const backoff = Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** attempt);
1155
- const delay = result.retryAfterMs ?? Math.round(backoff * (0.5 + random() * 0.5));
1156
- if (delay >= deadlineAt - now()) {
1157
- return {
1158
- ok: false,
1159
- error: `${result.message}${suffix}; the ${delay}ms retry delay does not fit the ${timeoutMs}ms time budget`,
1160
- };
1161
- }
1162
- await sleep(delay, controller.signal);
1163
- if (controller.signal.aborted) return timedOut();
1164
- }
1165
- } finally {
1166
- clearTimeout(timer);
1167
- }
1168
- }
1169
-
1170
- private async attemptOnce(
1171
- doFetch: typeof fetch,
1172
- providerId: SystemOneProviderId,
1173
- endpoint: string,
1174
- apiKey: string,
1175
- body: string,
1176
- signal: AbortSignal,
1177
- now: () => number,
1178
- ): Promise<Attempt> {
1179
- let res: Response;
1180
- try {
1181
- res = await doFetch(endpoint, {
1182
- method: "POST",
1183
- headers: {
1184
- ...getOpenRouterAttributionHeaders(endpoint, this.providerContext().env),
1185
- "content-type": "application/json",
1186
- accept: "application/json",
1187
- authorization: `Bearer ${apiKey}`,
1188
- },
1189
- body,
1190
- redirect: "manual",
1191
- signal,
1192
- });
1193
- } catch (err) {
1194
- if (signal.aborted) return { kind: "timeout" };
1195
- const code = (err as { code?: unknown })?.code;
1196
- const detail = typeof code === "string" && /^[A-Z0-9_]{3,40}$/.test(code) ? ` (${code})` : "";
1197
- return {
1198
- kind: "fail",
1199
- message: `SystemOne request failed: network error${detail}`,
1200
- retryable: true,
1201
- };
1202
- }
1203
-
1204
- const requestId = readRequestId(res.headers);
1205
- const withId = (message: string) => (requestId ? `${message} [request ${requestId}]` : message);
1206
-
1207
- if (res.status >= 200 && res.status < 300) {
1208
- let text: string;
1209
- try {
1210
- text = await res.text();
1211
- } catch {
1212
- if (signal.aborted) return { kind: "timeout" };
1213
- return {
1214
- kind: "fail",
1215
- message: withId("SystemOne response body could not be read"),
1216
- retryable: true,
1217
- };
1218
- }
1219
- if (text.length > MAX_RESPONSE_CHARS) {
1220
- return {
1221
- kind: "fail",
1222
- message: withId("SystemOne response exceeded the size limit"),
1223
- retryable: false,
1224
- };
1225
- }
1226
- return { kind: "ok", text, requestId };
1227
- }
1228
-
1229
- if (res.status >= 300 && res.status < 400) {
1230
- return {
1231
- kind: "fail",
1232
- message: withId(
1233
- `SystemOne request was redirected (HTTP ${res.status}); redirects are refused`,
1234
- ),
1235
- retryable: false,
1236
- };
1237
- }
1238
-
1239
- let errorText = "";
1240
- try {
1241
- errorText = await res.text();
1242
- } catch {
1243
- if (signal.aborted) return { kind: "timeout" };
1244
- }
1245
- const code = readErrorCode(errorText);
1246
- // The host answered but does not accept the key: name it, never retry.
1247
- if (res.status === 401 || res.status === 403) {
1248
- return {
1249
- kind: "fail",
1250
- message: withId(systemOneKeyRejectedMessage(providerId, res.status, code)),
1251
- retryable: false,
1252
- };
1253
- }
1254
- const detail = code ? ` (${code})` : "";
1255
- const retryable = res.status === 408 || res.status === 429 || res.status >= 500;
1256
- return {
1257
- kind: "fail",
1258
- message: withId(`SystemOne API returned HTTP ${res.status}${detail}`),
1259
- retryable,
1260
- retryAfterMs: retryable ? parseRetryAfter(res.headers.get("retry-after"), now()) : undefined,
1261
- };
1262
- }
1263
1064
  }
@@ -0,0 +1,57 @@
1
+ import type { SystemOneExtractionConfig } from "./system-one-extraction";
2
+ import {
3
+ cloudflareErrorCode,
4
+ isRecord,
5
+ SystemOneWireError,
6
+ wireContractError,
7
+ } from "./system-one-wire";
8
+
9
+ /**
10
+ * How one host's extraction request and response map onto the `system-one-extraction`
11
+ * contract: Clairic's `{ model, state, fields, images?, reference_date? }` request and
12
+ * `{ model, answers, truncated, usage }` body. A host with another shape supplies a
13
+ * wire that builds its body and returns that shape, which
14
+ * `validateSystemOneExtraction` then checks.
15
+ */
16
+ export interface SystemOneExtractWire {
17
+ /** Request body for one call. */
18
+ buildRequest(config: SystemOneExtractionConfig, model: string | undefined): unknown;
19
+ /**
20
+ * Turn a parsed success body into the Clairic `{ model, answers, usage }` shape, or
21
+ * throw a `SystemOneWireError`. The result is still validated afterwards.
22
+ */
23
+ toExtraction(body: unknown): unknown;
24
+ }
25
+
26
+ /** Clairic reads one text; structured state is sent as JSON text, which spans then index into. */
27
+ export function extractionStateText(state: SystemOneExtractionConfig["state"]): string {
28
+ return typeof state === "string" ? state : JSON.stringify(state);
29
+ }
30
+
31
+ /** The Clairic request as is: field specs already use Clairic's option names. */
32
+ export const CLAIRIC_WIRE: SystemOneExtractWire = {
33
+ buildRequest: (config, model) => ({
34
+ ...(model === undefined ? {} : { model }),
35
+ state: extractionStateText(config.state),
36
+ fields: config.fields,
37
+ ...(config.images?.length ? { images: config.images } : {}),
38
+ ...(config.referenceDate ? { reference_date: config.referenceDate } : {}),
39
+ }),
40
+ toExtraction: (body) => body,
41
+ };
42
+
43
+ /** Cloudflare Workers AI: the Clairic request, the response wrapped in `{ result, success, errors }`. */
44
+ export const CLOUDFLARE_EXTRACT_WIRE: SystemOneExtractWire = {
45
+ buildRequest: CLAIRIC_WIRE.buildRequest,
46
+ toExtraction(body) {
47
+ if (!isRecord(body)) wireContractError("response body must be a JSON object");
48
+ if (body.success !== true) {
49
+ const code = cloudflareErrorCode(body);
50
+ throw new SystemOneWireError(
51
+ `Cloudflare Workers AI reported a failed run${code ? ` (${code})` : ""}. Nothing was extracted.`,
52
+ );
53
+ }
54
+ if (!isRecord(body.result)) wireContractError("Cloudflare response is missing its result");
55
+ return body.result;
56
+ },
57
+ };