@desplega.ai/agent-swarm 1.167.0 → 1.169.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 (184) hide show
  1. package/dist/{acp-adapter-zpvy6321.js → acp-adapter-m47zfx4a.js} +2 -2
  2. package/dist/{actions-fq2zrwtf.js → actions-ws7k3tvq.js} +25 -19
  3. package/dist/{amp-adapter-5gbk7501.js → amp-adapter-nvv8k8j5.js} +1 -1
  4. package/dist/{app-nctetn1e.js → app-80w6qspx.js} +5 -5
  5. package/dist/{assistant-0aq8gjvj.js → assistant-jtq439kw.js} +29 -20
  6. package/dist/{boot-reembed-1ze5853n.js → boot-reembed-3nc0aaed.js} +4 -4
  7. package/dist/{boot-reembed-fmp5ww69.js → boot-reembed-feda8wn8.js} +4 -4
  8. package/dist/{boot-scrub-sweep-cbp8xd4e.js → boot-scrub-sweep-grsa2er0.js} +5 -5
  9. package/dist/{boot-seeds-22tx8a53.js → boot-seeds-gcfgq44f.js} +7 -7
  10. package/dist/{claude-adapter-0jhgeh1d.js → claude-adapter-pjv2q7t8.js} +7 -7
  11. package/dist/{claude-managed-adapter-tzwxy803.js → claude-managed-adapter-0wa8se82.js} +2 -2
  12. package/dist/{claude-managed-setup-nrhfx5b0.js → claude-managed-setup-zxa9p9tx.js} +1 -1
  13. package/dist/{claude-sdk-session-wh77rh83.js → claude-sdk-session-c9j6k9kx.js} +8772 -8663
  14. package/dist/{cli-c2tpw9ck.js → cli-3ry5gx4f.js} +1 -1
  15. package/dist/{cli-wydz3feg.js → cli-61tbnjpf.js} +214 -2472
  16. package/dist/{cli-cexctgn5.js → cli-7vz49g7h.js} +101 -34
  17. package/dist/{cli-6qxmjpem.js → cli-8xjst8q2.js} +34 -22
  18. package/dist/{cli-tx2zvn23.js → cli-91r17hb1.js} +3 -3
  19. package/dist/{cli-577bxkf6.js → cli-a8vps458.js} +182 -2208
  20. package/dist/{cli-tc5zec7v.js → cli-b7y8113e.js} +1 -1
  21. package/dist/{cli-69egj654.js → cli-bqrs5ycj.js} +1 -1
  22. package/dist/{cli-cf2vgn9d.js → cli-dv76vq5d.js} +30 -28
  23. package/dist/{cli-g5erf0zt.js → cli-e2prcx5m.js} +31 -0
  24. package/dist/{cli-c8r99akr.js → cli-e5qk3m2a.js} +209 -31
  25. package/dist/{cli-z1dqhc9y.js → cli-eqfbsxsn.js} +3 -3
  26. package/dist/{cli-kw0jcfje.js → cli-fyeft04q.js} +1 -1
  27. package/dist/{cli-5qkcdv8c.js → cli-gmdajfw5.js} +3 -3
  28. package/dist/{cli-ypgba1a8.js → cli-jkfxpg1b.js} +1 -1
  29. package/dist/{cli-mxvnnv5q.js → cli-jv36y8wz.js} +1 -1
  30. package/dist/{cli-m7vmeepg.js → cli-km8559dm.js} +6 -6
  31. package/dist/{cli-ykt8f7va.js → cli-kp3vfknv.js} +1 -1
  32. package/dist/{cli-ymmc1zxg.js → cli-m23r88re.js} +8 -5
  33. package/dist/{cli-v1pwxreq.js → cli-n4z21pt8.js} +1 -1
  34. package/dist/{cli-mxry91sa.js → cli-qg2zhjfc.js} +73 -4
  35. package/dist/{cli-1yrcfa5s.js → cli-rbr91whd.js} +4 -4
  36. package/dist/{cli-mkdsy1nq.js → cli-rg1c8ccw.js} +93 -7
  37. package/dist/cli-rh44xx46.js +2659 -0
  38. package/dist/cli-s0k2j7q9.js +2084 -0
  39. package/dist/{cli-ty4bzkqr.js → cli-sajr098b.js} +1 -1
  40. package/dist/{cli-6vfz44sr.js → cli-swrz3mj8.js} +1 -1
  41. package/dist/{cli-p8rt8spp.js → cli-vejds37t.js} +2 -2
  42. package/dist/{cli-jbzbr36n.js → cli-w6xa5nf5.js} +1 -1
  43. package/dist/{cli-9q3wt23f.js → cli-x6z90nq9.js} +1 -1
  44. package/dist/{cli-qzmzwegq.js → cli-ysnkvdx6.js} +102 -9
  45. package/dist/{cli-y0kjpfm9.js → cli-yvxfaem2.js} +2 -2
  46. package/dist/{cli-pkj1w46r.js → cli-z48ggfyq.js} +1 -1
  47. package/dist/{cli-6bpvv8h4.js → cli-zzev32jw.js} +1 -1
  48. package/dist/cli.js +20 -17
  49. package/dist/{codex-adapter-cgesnjkr.js → codex-adapter-0kpg09ca.js} +3 -3
  50. package/dist/{codex-hook-15dgc4cb.js → codex-hook-295jpbfa.js} +2 -2
  51. package/dist/{codex-session-runner-8kd7ddhr.js → codex-session-runner-8n672hxh.js} +3 -3
  52. package/dist/{commands-h6zaaxym.js → commands-mhmg5n0x.js} +4 -4
  53. package/dist/{cursor-adapter-v8mm6bp5.js → cursor-adapter-wn8n5zzk.js} +2 -2
  54. package/dist/{db-qg3myn53.js → db-wa6s1mwv.js} +4 -4
  55. package/dist/{dispatcher-7vnwj9qw.js → dispatcher-nhs2pkr8.js} +16 -14
  56. package/dist/{dsh-adapter-tz6hfp5r.js → dsh-adapter-5423p70m.js} +1 -1
  57. package/dist/{grok-adapter-jdt2dz2s.js → grok-adapter-28pkdt80.js} +3 -3
  58. package/dist/{handlers-2qtkpncr.js → handlers-5rtf9nng.js} +19 -17
  59. package/dist/{hook-51yfcgsv.js → hook-krjfkpzy.js} +4 -4
  60. package/dist/{hook-qnbdgwvd.js → hook-qkm73zk6.js} +5 -5
  61. package/dist/{http-jsmtag08.js → http-a397xcn4.js} +4845 -3484
  62. package/dist/{index-2t9kesn1.js → index-ac0v8v9d.js} +4 -4
  63. package/dist/{index-ekywyv2v.js → index-kqre45fr.js} +16 -14
  64. package/dist/{index-bt8wkv1w.js → index-rtxg15ah.js} +17 -15
  65. package/dist/{index-t4g06w93.js → index-vvsq7v45.js} +26 -23
  66. package/dist/{index-cx1sb9bq.js → index-ytdhtdry.js} +50 -11
  67. package/dist/{keepalive-4aezx4bj.js → keepalive-psfh8ysx.js} +6 -6
  68. package/dist/{lead-r44bkpm5.js → lead-m6rppjn7.js} +29 -27
  69. package/dist/{maintenance-grz5jrzh.js → maintenance-41f837ke.js} +4 -4
  70. package/dist/{node-qm9eb6z0.js → node-fkajaq96.js} +1 -1
  71. package/dist/{oauth-refresh-sweep-wm4ctjfj.js → oauth-refresh-sweep-6hr55hm8.js} +5 -5
  72. package/dist/{onboard-v60zgqf0.js → onboard-ktrtd7cz.js} +2 -2
  73. package/dist/{otel-impl-ghh4dyf8.js → otel-impl-dq4f2k3m.js} +1 -1
  74. package/dist/{pi-mono-adapter-0eg8kw2c.js → pi-mono-adapter-e5v9w0hv.js} +3 -2
  75. package/dist/{pricing-refresh-mjrqvnvw.js → pricing-refresh-kmk7j1ra.js} +6 -6
  76. package/dist/{rbac-roles-fd5tskj9.js → rbac-roles-9w1dkkxd.js} +4 -4
  77. package/dist/{rbac-roles-axv4d74x.js → rbac-roles-9ykx9tc5.js} +5 -5
  78. package/dist/{render-v2-k50j33qd.js → render-v2-ycqm53np.js} +5 -5
  79. package/dist/resume-cq2pkp8q.js +70 -0
  80. package/dist/{setup-4syh0pd2.js → setup-0dcmdvbn.js} +2 -2
  81. package/dist/{virtual-modules-ftr9kwe8.js → virtual-modules-1x9k4jcs.js} +1 -1
  82. package/dist/{work-objects-jcdbg8rr.js → work-objects-x01x2fh8.js} +19 -17
  83. package/dist/{worker-x8j8c9sx.js → worker-py1ssb1h.js} +29 -27
  84. package/openapi.json +15622 -15042
  85. package/package.json +8 -5
  86. package/src/apps/store.ts +1 -0
  87. package/src/artifact-sdk/feedback-overlay.ts +528 -0
  88. package/src/be/db/workflow-runs.ts +95 -0
  89. package/src/be/db-queries/slack-inbound.ts +489 -0
  90. package/src/be/db.ts +9 -2
  91. package/src/be/extensions/assets.ts +1 -1
  92. package/src/be/migrations/203_workflow_run_parent_step.sql +10 -0
  93. package/src/be/migrations/204_slack_inbound_receipts.sql +51 -0
  94. package/src/be/script-connections.ts +41 -7
  95. package/src/be/scripts/run-saved.ts +40 -20
  96. package/src/be/swarm-config-guard.ts +4 -0
  97. package/src/extensions/catalog.generated.json +17 -0
  98. package/src/extensions/catalog.ts +1 -1
  99. package/src/extensions/dispatcher.ts +3 -3
  100. package/src/heartbeat/heartbeat.ts +152 -2
  101. package/src/heartbeat/templates.ts +16 -0
  102. package/src/http/all-routes.ts +1 -0
  103. package/src/http/index.ts +2 -0
  104. package/src/http/integrations.ts +3 -1
  105. package/src/http/mcp-bridge.ts +1 -0
  106. package/src/http/pages-public.ts +7 -2
  107. package/src/http/pages.ts +178 -0
  108. package/src/http/script-connections.ts +35 -0
  109. package/src/http/slack-inbound.ts +107 -0
  110. package/src/http/tasks.ts +45 -27
  111. package/src/http/users.ts +1 -1
  112. package/src/http/webhooks.ts +20 -20
  113. package/src/prompts/session-templates.ts +32 -0
  114. package/src/providers/acp-targets.ts +1 -1
  115. package/src/providers/amp-adapter.ts +2 -2
  116. package/src/providers/claude-adapter.ts +6 -0
  117. package/src/providers/claude-background-keepalive.ts +100 -0
  118. package/src/providers/claude-managed-adapter.ts +2 -11
  119. package/src/providers/claude-sdk-session.ts +7 -1
  120. package/src/providers/dsh-adapter.ts +1 -1
  121. package/src/providers/opencode-adapter.ts +1 -2
  122. package/src/providers/pi-mono-adapter.ts +3 -3
  123. package/src/realtime/browser.generated.txt +8 -8
  124. package/src/realtime/browser.ts +3 -3
  125. package/src/scheduler/scheduler.ts +82 -13
  126. package/src/scripts-runtime/executors/quickjs-pool-config.ts +26 -0
  127. package/src/scripts-runtime/executors/quickjs.ts +9 -2
  128. package/src/scripts-runtime/executors/registry.ts +1 -1
  129. package/src/server.ts +1 -0
  130. package/src/slack/actions.ts +6 -2
  131. package/src/slack/app.ts +5 -0
  132. package/src/slack/assistant.ts +15 -5
  133. package/src/slack/egress-scrub.ts +3 -2
  134. package/src/slack/handlers.ts +36 -18
  135. package/src/slack/inbound-dispatch.ts +549 -0
  136. package/src/slack/inbound-files.ts +2 -0
  137. package/src/telemetry-context.ts +1 -0
  138. package/src/tests/claude-background-keepalive-wiring.test.ts +220 -0
  139. package/src/tests/claude-background-keepalive.fixture.ts +162 -0
  140. package/src/tests/claude-background-keepalive.test.ts +139 -0
  141. package/src/tests/codex-oauth-device.test.ts +1 -1
  142. package/src/tests/connection-embedded-auth.test.ts +51 -0
  143. package/src/tests/docs-index-footprint.test.js +3 -3
  144. package/src/tests/dsh-adapter.test.ts +1 -1
  145. package/src/tests/extensions-catalog.test.ts +1 -0
  146. package/src/tests/extensions-example-slack-reply-gate.test.ts +756 -0
  147. package/src/tests/heartbeat-checklist.test.ts +243 -0
  148. package/src/tests/pages-feedback.test.ts +218 -0
  149. package/src/tests/schedule-target-type.test.ts +136 -0
  150. package/src/tests/script-connections-http.test.ts +45 -0
  151. package/src/tests/script-connections-mcp.test.ts +41 -0
  152. package/src/tests/script-connections.test.ts +2 -0
  153. package/src/tests/script-executor-quickjs-pool.test.ts +106 -0
  154. package/src/tests/script-executor-registry.test.ts +56 -0
  155. package/src/tests/send-task-self-send.test.ts +107 -0
  156. package/src/tests/slack-inbound-diagnostics-auth.test.ts +93 -0
  157. package/src/tests/slack-inbound-receipts.test.ts +684 -0
  158. package/src/tests/slack-session-status.test.ts +4 -2
  159. package/src/tests/ui-e2e-ingest-payload.test.ts +5 -1
  160. package/src/tests/user-token-routes.test.ts +1 -1
  161. package/src/tests/workflow-executors.test.ts +3 -2
  162. package/src/tests/workflow-sub-workflow.test.ts +363 -0
  163. package/src/tools/script-connections/tool.ts +35 -3
  164. package/src/tools/send-task.ts +9 -1
  165. package/src/utils/internal-ai/complete-structured.ts +6 -3
  166. package/src/utils/runtime-model-catalog.ts +1 -1
  167. package/src/workflows/engine.ts +17 -1
  168. package/src/workflows/executors/agent-task.ts +2 -2
  169. package/src/workflows/executors/human-in-the-loop.ts +3 -3
  170. package/src/workflows/executors/registry.ts +2 -0
  171. package/src/workflows/executors/script.ts +1 -1
  172. package/src/workflows/executors/sub-workflow.ts +135 -0
  173. package/src/workflows/executors/wait.ts +3 -3
  174. package/src/workflows/recovery.ts +11 -1
  175. package/src/workflows/resume.ts +77 -0
  176. package/src/workflows/wait-filter.ts +5 -7
  177. package/templates/extensions/README.md +1 -0
  178. package/templates/extensions/slack-reply-gate/README.md +98 -0
  179. package/templates/extensions/slack-reply-gate/hooks.ts +582 -0
  180. package/templates/extensions/slack-reply-gate/manifest.json +8 -0
  181. package/templates/skills/heartbeat-runbook/content.md +3 -1
  182. package/templates/skills/pages/content.md +25 -0
  183. package/templates/skills/swarm-scripts/SKILL.md +10 -0
  184. package/templates/skills/swarm-scripts/content.md +10 -0
@@ -0,0 +1,2084 @@
1
+ import {
2
+ DEFAULT_MODEL,
3
+ resolveCredential
4
+ } from "./cli-7mhy3ths.js";
5
+ import {
6
+ getSlackApp
7
+ } from "./cli-e5qk3m2a.js";
8
+ import {
9
+ claimApprovalCancellationNotification,
10
+ completeApprovalCancellationNotificationClaim,
11
+ createTaskExtended,
12
+ getTaskById,
13
+ init_db1 as init_db,
14
+ releaseApprovalCancellationNotificationClaim
15
+ } from "./cli-rg1c8ccw.js";
16
+ import {
17
+ TERMINAL_TASK_STATUSES,
18
+ init_types
19
+ } from "./cli-xvdgasgy.js";
20
+ import {
21
+ init_resolver,
22
+ resolveTemplate
23
+ } from "./cli-5c2j5yvx.js";
24
+ import {
25
+ DEFAULT_OPENROUTER_BASE_URL,
26
+ getOpenRouterAttributionHeaders,
27
+ getOpenRouterBaseUrl,
28
+ init_openrouter_base_url
29
+ } from "./cli-a35hzdve.js";
30
+ import {
31
+ getAppUrl,
32
+ init_constants
33
+ } from "./cli-gd4favx9.js";
34
+ import {
35
+ init_zod
36
+ } from "./cli-sd0mxa3c.js";
37
+ import {
38
+ exports_external
39
+ } from "./cli-w5e1d1vv.js";
40
+ import {
41
+ init_secret_scrubber,
42
+ scrubSecrets
43
+ } from "./cli-wt7ccy4r.js";
44
+ import {
45
+ __require
46
+ } from "./cli-bc05qbss.js";
47
+
48
+ // src/workflows/approval-notifications.ts
49
+ init_db();
50
+ init_resolver();
51
+ init_types();
52
+ init_secret_scrubber();
53
+ async function postApprovalCancellationUpdates(approvals, reason, client = getSlackApp()?.client) {
54
+ if (!client)
55
+ return;
56
+ const text = `This approval is no longer actionable: ${reason}`;
57
+ for (const approval of approvals) {
58
+ const channels = approval.notificationChannels;
59
+ const slackThreads = (channels ?? []).filter((notification) => notification.channel === "slack" && notification.messageTs);
60
+ if (slackThreads.length === 0)
61
+ continue;
62
+ for (const notification of slackThreads) {
63
+ if (notification.channel !== "slack" || !notification.messageTs)
64
+ continue;
65
+ const notificationKey = `${notification.target}:${notification.messageTs}`;
66
+ const claim = await claimApprovalCancellationNotification(approval.id, notificationKey);
67
+ if (!claim)
68
+ continue;
69
+ try {
70
+ await client.chat.postMessage({
71
+ channel: notification.target,
72
+ thread_ts: notification.messageTs,
73
+ text,
74
+ unfurl_links: false,
75
+ unfurl_media: false
76
+ });
77
+ await completeApprovalCancellationNotificationClaim(approval.id, notificationKey, claim.leaseToken);
78
+ } catch (error) {
79
+ await releaseApprovalCancellationNotificationClaim(approval.id, notificationKey, claim.leaseToken);
80
+ console.error(`[HITL] Failed to post cancellation update for approval ${approval.id}:`, scrubSecrets(error instanceof Error ? error.message : String(error)));
81
+ }
82
+ }
83
+ }
84
+ }
85
+ async function createApprovalFollowUpTask(request, templateEventType) {
86
+ if (request.workflowRunId || !request.sourceTaskId)
87
+ return false;
88
+ const sourceTask = await getTaskById(request.sourceTaskId);
89
+ if (!sourceTask)
90
+ return false;
91
+ if (templateEventType === "hitl.timeout" && TERMINAL_TASK_STATUSES.includes(sourceTask.status)) {
92
+ return false;
93
+ }
94
+ const { text: taskText } = templateEventType === "hitl.follow_up" ? resolveTemplate("hitl.follow_up", {
95
+ request_id: request.id,
96
+ title: request.title,
97
+ status: request.status,
98
+ responses: formatResponses(request.questions, request.responses ?? {})
99
+ }) : resolveTemplate("hitl.timeout", {
100
+ request_id: request.id,
101
+ title: request.title,
102
+ expires_at: request.expiresAt ?? "",
103
+ reason: request.resolutionReason ?? ""
104
+ });
105
+ await createTaskExtended(taskText, {
106
+ agentId: sourceTask.agentId,
107
+ routingReason: sourceTask.agentId ? "continuity" : undefined,
108
+ routingSource: sourceTask.agentId ? "engine_default" : undefined,
109
+ parentTaskId: request.sourceTaskId,
110
+ source: "system",
111
+ taskType: "hitl-follow-up",
112
+ tags: ["hitl", "follow-up"],
113
+ slackChannelId: sourceTask.slackChannelId ?? undefined,
114
+ slackThreadTs: sourceTask.slackThreadTs ?? undefined,
115
+ slackUserId: sourceTask.slackUserId ?? undefined
116
+ });
117
+ return true;
118
+ }
119
+ function formatResponses(questions, responses) {
120
+ return questions.map((q) => {
121
+ const answer = responses[q.id];
122
+ let answerText;
123
+ if (answer == null) {
124
+ answerText = "(no answer)";
125
+ } else if (q.type === "approval") {
126
+ const a = answer;
127
+ answerText = a.approved ? "Approved" : "Rejected";
128
+ if (a.comment)
129
+ answerText += ` — ${a.comment}`;
130
+ } else if (typeof answer === "object") {
131
+ answerText = JSON.stringify(answer);
132
+ } else {
133
+ answerText = String(answer);
134
+ }
135
+ return `- ${q.label}: ${answerText}`;
136
+ }).join(`
137
+ `);
138
+ }
139
+
140
+ // src/workflows/executors/system-one-decision.ts
141
+ init_zod();
142
+ init_openrouter_base_url();
143
+ init_secret_scrubber();
144
+
145
+ // src/workflows/executors/base.ts
146
+ class BaseExecutor {
147
+ deps;
148
+ retryPolicy;
149
+ constructor(deps) {
150
+ this.deps = deps;
151
+ }
152
+ async checkReadiness(_nodes) {
153
+ return [];
154
+ }
155
+ resolveApproval(_parkedOutput, _approval) {
156
+ return null;
157
+ }
158
+ async run(input) {
159
+ const configResult = this.configSchema.safeParse(input.config);
160
+ if (!configResult.success) {
161
+ return {
162
+ status: "failed",
163
+ error: `Input validation failed: ${configResult.error.message}`
164
+ };
165
+ }
166
+ let result;
167
+ try {
168
+ result = await this.execute(configResult.data, input.context, input.meta);
169
+ } catch (err) {
170
+ return {
171
+ status: "failed",
172
+ error: `Executor threw: ${err instanceof Error ? err.message : String(err)}`
173
+ };
174
+ }
175
+ if (result.status === "success" && result.output !== undefined) {
176
+ const outputResult = this.outputSchema.safeParse(result.output);
177
+ if (!outputResult.success) {
178
+ return {
179
+ status: "failed",
180
+ error: `Output validation failed: ${outputResult.error.message}`
181
+ };
182
+ }
183
+ }
184
+ return result;
185
+ }
186
+ }
187
+
188
+ // src/workflows/executors/human-in-the-loop.ts
189
+ init_zod();
190
+ init_constants();
191
+ init_secret_scrubber();
192
+ var SelectOptionSchema = exports_external.object({
193
+ value: exports_external.string(),
194
+ label: exports_external.string(),
195
+ description: exports_external.string().optional()
196
+ });
197
+ var QuestionSchema = exports_external.discriminatedUnion("type", [
198
+ exports_external.object({
199
+ id: exports_external.string(),
200
+ type: exports_external.literal("approval"),
201
+ label: exports_external.string(),
202
+ required: exports_external.boolean().default(true),
203
+ description: exports_external.string().optional()
204
+ }),
205
+ exports_external.object({
206
+ id: exports_external.string(),
207
+ type: exports_external.literal("text"),
208
+ label: exports_external.string(),
209
+ required: exports_external.boolean().default(true),
210
+ description: exports_external.string().optional(),
211
+ placeholder: exports_external.string().optional(),
212
+ multiline: exports_external.boolean().optional()
213
+ }),
214
+ exports_external.object({
215
+ id: exports_external.string(),
216
+ type: exports_external.literal("single-select"),
217
+ label: exports_external.string(),
218
+ required: exports_external.boolean().default(true),
219
+ description: exports_external.string().optional(),
220
+ options: exports_external.array(SelectOptionSchema)
221
+ }),
222
+ exports_external.object({
223
+ id: exports_external.string(),
224
+ type: exports_external.literal("multi-select"),
225
+ label: exports_external.string(),
226
+ required: exports_external.boolean().default(true),
227
+ description: exports_external.string().optional(),
228
+ options: exports_external.array(SelectOptionSchema),
229
+ minSelections: exports_external.number().int().min(0).optional(),
230
+ maxSelections: exports_external.number().int().min(1).optional()
231
+ }),
232
+ exports_external.object({
233
+ id: exports_external.string(),
234
+ type: exports_external.literal("boolean"),
235
+ label: exports_external.string(),
236
+ required: exports_external.boolean().default(true),
237
+ description: exports_external.string().optional(),
238
+ defaultValue: exports_external.boolean().optional()
239
+ })
240
+ ]);
241
+ var ApproverConfigSchema = exports_external.object({
242
+ users: exports_external.array(exports_external.string()).optional(),
243
+ roles: exports_external.array(exports_external.string()).optional(),
244
+ policy: exports_external.union([exports_external.literal("any"), exports_external.literal("all"), exports_external.object({ min: exports_external.number().int().min(1) })])
245
+ });
246
+ var NotificationConfigSchema = exports_external.object({
247
+ channel: exports_external.enum(["slack", "email"]),
248
+ target: exports_external.string()
249
+ });
250
+ var HITLTimeoutSchema = exports_external.object({
251
+ seconds: exports_external.number().int().min(1),
252
+ action: exports_external.literal("reject")
253
+ });
254
+ var DYNAMIC_QUESTIONS_TOKEN_RE = /^\{\{[^}]+\}\}$/;
255
+ var MAX_HITL_QUESTIONS = 100;
256
+ var SLACK_SECTION_TEXT_LIMIT = 3000;
257
+ var MAX_REPORTED_ISSUES = 5;
258
+ var HITLConfigSchema = exports_external.object({
259
+ title: exports_external.string(),
260
+ questions: exports_external.union([
261
+ exports_external.array(QuestionSchema).min(1),
262
+ exports_external.string().regex(DYNAMIC_QUESTIONS_TOKEN_RE, "questions must be an array or one exact {{interpolation}} token")
263
+ ]),
264
+ approvers: ApproverConfigSchema,
265
+ timeout: HITLTimeoutSchema.optional(),
266
+ notifications: exports_external.array(NotificationConfigSchema).optional()
267
+ });
268
+ var HITLOutputSchema = exports_external.object({
269
+ requestId: exports_external.string().uuid(),
270
+ status: exports_external.string(),
271
+ responses: exports_external.record(exports_external.string(), exports_external.unknown()).nullable()
272
+ });
273
+ function resolveHitlQuestions(value) {
274
+ const fail = (reason) => ({
275
+ ok: false,
276
+ error: `human-in-the-loop questions ${reason}`
277
+ });
278
+ if (typeof value === "string") {
279
+ return fail(value.trim() === "" ? "resolved to an empty value: the {{token}} did not resolve (check the node's inputs mapping and the upstream output)" : "resolved to a string; they must resolve to an array of question objects (use one exact {{token}} pointing at an array)");
280
+ }
281
+ if (!Array.isArray(value)) {
282
+ return fail(`must resolve to an array of question objects, got ${value === null ? "null" : typeof value}`);
283
+ }
284
+ if (value.length === 0) {
285
+ return fail("resolved to an empty array; refusing to create an approval card with no questions");
286
+ }
287
+ if (value.length > MAX_HITL_QUESTIONS) {
288
+ return fail(`resolved to ${value.length} questions, over the limit of ${MAX_HITL_QUESTIONS} per card; split or group them upstream`);
289
+ }
290
+ const issues = [];
291
+ const questions = [];
292
+ const seenIds = new Set;
293
+ value.forEach((raw, index) => {
294
+ const parsed = QuestionSchema.safeParse(raw);
295
+ if (!parsed.success) {
296
+ for (const issue of parsed.error.issues) {
297
+ const path = issue.path.length > 0 ? `.${issue.path.map(String).join(".")}` : "";
298
+ issues.push(`[${index}]${path}: ${issue.message}`);
299
+ }
300
+ return;
301
+ }
302
+ const question = parsed.data;
303
+ if (question.id.trim() === "") {
304
+ issues.push(`[${index}].id: must be a non-empty string`);
305
+ } else if (seenIds.has(question.id)) {
306
+ issues.push(`[${index}].id: duplicate id "${question.id}" (responses are keyed by id)`);
307
+ }
308
+ seenIds.add(question.id);
309
+ if ((question.type === "single-select" || question.type === "multi-select") && question.options.length === 0) {
310
+ issues.push(`[${index}].options: a ${question.type} question needs at least one option`);
311
+ }
312
+ questions.push(question);
313
+ });
314
+ if (issues.length > 0) {
315
+ const shown = issues.slice(0, MAX_REPORTED_ISSUES).join("; ");
316
+ const more = issues.length > MAX_REPORTED_ISSUES ? `; and ${issues.length - MAX_REPORTED_ISSUES} more issue(s)` : "";
317
+ return fail(`are invalid: ${shown}${more}`);
318
+ }
319
+ return { ok: true, questions };
320
+ }
321
+ function escapeSlackMrkdwn(text) {
322
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
323
+ }
324
+ function buildSlackQuestionsSummary(questions, timeoutText = "") {
325
+ const header = `*Questions:*
326
+ `;
327
+ const lines = questions.map((q) => `• ${escapeSlackMrkdwn(q.label.replace(/\s+/g, " ").trim())}`);
328
+ const fits = (shown, omitted) => {
329
+ const tail = omitted > 0 ? `
330
+ _…and ${omitted} more. Answer all of them on the dashboard._` : "";
331
+ const text = `${header}${shown.join(`
332
+ `)}${tail}${timeoutText}`;
333
+ return text.length <= SLACK_SECTION_TEXT_LIMIT ? text : null;
334
+ };
335
+ const full = fits(lines, 0);
336
+ if (full)
337
+ return full;
338
+ for (let count = lines.length - 1;count >= 0; count--) {
339
+ const text = fits(lines.slice(0, count), lines.length - count);
340
+ if (text)
341
+ return text;
342
+ }
343
+ return `${header}_${questions.length} questions. Answer them on the dashboard._`;
344
+ }
345
+
346
+ class HumanInTheLoopExecutor extends BaseExecutor {
347
+ type = "human-in-the-loop";
348
+ mode = "async";
349
+ configSchema = HITLConfigSchema;
350
+ outputSchema = HITLOutputSchema;
351
+ async run(input) {
352
+ const resolved = resolveHitlQuestions(input.config?.questions);
353
+ if (!resolved.ok)
354
+ return { status: "failed", error: resolved.error };
355
+ return super.run({ ...input, config: { ...input.config, questions: resolved.questions } });
356
+ }
357
+ async execute(config, _context, meta) {
358
+ const { db } = this.deps;
359
+ const existing = await db.getApprovalRequestByStepId(meta.stepId);
360
+ if (existing) {
361
+ if (existing.status !== "pending") {
362
+ if (existing.status === "cancelled") {
363
+ return {
364
+ status: "failed",
365
+ error: `Approval request was cancelled: ${existing.resolutionReason ?? "workflow run cancelled"}`
366
+ };
367
+ }
368
+ const nextPort = existing.status === "timeout" ? "timeout" : existing.status === "rejected" ? "rejected" : "approved";
369
+ return {
370
+ status: "success",
371
+ output: {
372
+ requestId: existing.id,
373
+ status: existing.status,
374
+ responses: existing.responses
375
+ },
376
+ nextPort
377
+ };
378
+ }
379
+ return {
380
+ status: "success",
381
+ async: true,
382
+ waitFor: "approval.resolved",
383
+ correlationId: existing.id
384
+ };
385
+ }
386
+ if (!Array.isArray(config.questions)) {
387
+ return { status: "failed", error: "human-in-the-loop questions did not resolve to an array" };
388
+ }
389
+ const requestId = crypto.randomUUID();
390
+ const created = await db.createApprovalRequest({
391
+ id: requestId,
392
+ title: config.title,
393
+ questions: config.questions,
394
+ approvers: config.approvers,
395
+ workflowRunId: meta.runId,
396
+ workflowRunStepId: meta.stepId,
397
+ timeoutSeconds: config.timeout?.seconds,
398
+ notificationChannels: config.notifications,
399
+ createdBy: meta.requestedByUserId,
400
+ requireActionableWorkflow: true
401
+ });
402
+ if (created.status === "cancelled") {
403
+ return {
404
+ status: "success",
405
+ async: true,
406
+ waitFor: "approval.resolved",
407
+ correlationId: requestId
408
+ };
409
+ }
410
+ if (config.notifications?.length) {
411
+ this.dispatchNotifications(requestId, config, db).catch((err) => {
412
+ console.error("[HITL] Unexpected error dispatching notifications:", scrubSecrets(err instanceof Error ? err.message : String(err)));
413
+ });
414
+ }
415
+ return {
416
+ status: "success",
417
+ async: true,
418
+ waitFor: "approval.resolved",
419
+ correlationId: requestId
420
+ };
421
+ }
422
+ async dispatchNotifications(requestId, config, db) {
423
+ if (!config.notifications?.length)
424
+ return;
425
+ const approvalUrl = `${getAppUrl()}/approval-requests/${requestId}`;
426
+ const updatedChannels = [...config.notifications];
427
+ let updated = false;
428
+ for (let i = 0;i < config.notifications.length; i++) {
429
+ const notification = config.notifications[i];
430
+ if (notification.channel === "email") {
431
+ console.warn(`[HITL] Email notifications not yet supported (target: ${notification.target})`);
432
+ continue;
433
+ }
434
+ if (notification.channel === "slack") {
435
+ try {
436
+ const { getSlackApp: getSlackApp2 } = await import("./app-80w6qspx.js");
437
+ const slackApp = getSlackApp2();
438
+ if (!slackApp) {
439
+ console.warn("[HITL] Slack not initialized — cannot send notification");
440
+ continue;
441
+ }
442
+ const questions = Array.isArray(config.questions) ? config.questions : [];
443
+ const questionsText = buildSlackQuestionsSummary(questions);
444
+ const timeoutCaption = config.timeout ? [
445
+ {
446
+ type: "context",
447
+ elements: [
448
+ {
449
+ type: "mrkdwn",
450
+ text: `⏱ Timeout: ${formatTimeout(config.timeout.seconds)} — times out if not responded`
451
+ }
452
+ ]
453
+ }
454
+ ] : [];
455
+ const blocks = [
456
+ {
457
+ type: "section",
458
+ text: {
459
+ type: "mrkdwn",
460
+ text: `\uD83D\uDD14 *Approval Required: ${config.title}*`
461
+ }
462
+ },
463
+ {
464
+ type: "section",
465
+ text: {
466
+ type: "mrkdwn",
467
+ text: questionsText
468
+ }
469
+ },
470
+ {
471
+ type: "actions",
472
+ elements: [
473
+ {
474
+ type: "button",
475
+ text: { type: "plain_text", text: "Review & Respond" },
476
+ url: approvalUrl,
477
+ style: "primary"
478
+ }
479
+ ]
480
+ },
481
+ ...timeoutCaption
482
+ ];
483
+ const result = await slackApp.client.chat.postMessage({
484
+ channel: notification.target,
485
+ text: `Approval Required: ${config.title} — ${approvalUrl}`,
486
+ unfurl_links: false,
487
+ unfurl_media: false,
488
+ blocks
489
+ });
490
+ if (result.ts) {
491
+ updatedChannels[i] = { ...notification, messageTs: result.ts };
492
+ updated = true;
493
+ }
494
+ console.log(`[HITL] Slack notification sent to ${notification.target} for request ${requestId}`);
495
+ } catch (err) {
496
+ console.error(`[HITL] Failed to send Slack notification to ${notification.target}:`, scrubSecrets(err instanceof Error ? err.message : String(err)));
497
+ }
498
+ }
499
+ }
500
+ if (updated) {
501
+ try {
502
+ await db.updateApprovalRequestNotifications(requestId, updatedChannels);
503
+ const latest = await db.getApprovalRequestById(requestId);
504
+ if (latest?.status === "cancelled") {
505
+ await postApprovalCancellationUpdates([latest], latest.resolutionReason ?? "Workflow run cancelled");
506
+ }
507
+ } catch (err) {
508
+ console.error("[HITL] Failed to update notification channels in DB:", scrubSecrets(err instanceof Error ? err.message : String(err)));
509
+ }
510
+ }
511
+ }
512
+ }
513
+ function formatTimeout(seconds) {
514
+ if (seconds < 60)
515
+ return `${seconds}s`;
516
+ if (seconds < 3600)
517
+ return `${Math.round(seconds / 60)}m`;
518
+ const hours = Math.floor(seconds / 3600);
519
+ const mins = Math.round(seconds % 3600 / 60);
520
+ return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
521
+ }
522
+
523
+ // src/workflows/executors/system-one-providers.ts
524
+ init_openrouter_base_url();
525
+
526
+ // src/workflows/executors/system-one-wire.ts
527
+ class SystemOneWireError extends Error {
528
+ }
529
+ var VALIDATION_PREFIX = "SystemOne response failed validation: ";
530
+ function wireContractError(message) {
531
+ throw new SystemOneWireError(`${VALIDATION_PREFIX}${message}`);
532
+ }
533
+ function isRecord(value) {
534
+ return typeof value === "object" && value !== null && !Array.isArray(value);
535
+ }
536
+ var SYSTEM_ONE_WIRE = {
537
+ buildRequest: (config, model) => ({
538
+ state: config.state,
539
+ ...model === undefined ? {} : { model },
540
+ questions: config.questions
541
+ }),
542
+ toSystemOne: (body) => body
543
+ };
544
+ var CLOUDFLARE_WIRE = {
545
+ buildRequest: SYSTEM_ONE_WIRE.buildRequest,
546
+ toSystemOne(body) {
547
+ if (!isRecord(body))
548
+ wireContractError("response body must be a JSON object");
549
+ if (body.success !== true) {
550
+ const code = cloudflareErrorCode(body);
551
+ throw new SystemOneWireError(`Cloudflare Workers AI reported a failed run${code ? ` (${code})` : ""}. No decision was made.`);
552
+ }
553
+ if (!isRecord(body.result))
554
+ wireContractError("Cloudflare response is missing its result");
555
+ return body.result;
556
+ }
557
+ };
558
+ var ERROR_CODE_RE = /^[A-Za-z0-9._:-]{1,64}$/;
559
+ function cloudflareErrorCode(body) {
560
+ const first = Array.isArray(body.errors) ? body.errors[0] : undefined;
561
+ const code = isRecord(first) ? first.code : undefined;
562
+ if (typeof code === "number" && Number.isSafeInteger(code))
563
+ return String(code);
564
+ return typeof code === "string" && ERROR_CODE_RE.test(code) ? code : undefined;
565
+ }
566
+
567
+ // src/workflows/executors/system-one-openai-wire.ts
568
+ function entryText(entry) {
569
+ if (entry === undefined || entry === null)
570
+ return;
571
+ return typeof entry === "string" ? entry : JSON.stringify(entry);
572
+ }
573
+ function openaiInput(state) {
574
+ if (typeof state === "string")
575
+ return state;
576
+ if (Array.isArray(state)) {
577
+ return [
578
+ {
579
+ role: "user",
580
+ content: state.map((text) => ({ type: "input_text", text: String(text) }))
581
+ }
582
+ ];
583
+ }
584
+ return JSON.stringify(state);
585
+ }
586
+ function openaiQuestion(name, question) {
587
+ const instructions = entryText(question.instructions) ?? "";
588
+ switch (question.type) {
589
+ case "noul": {
590
+ const whenTrue = entryText(question.criteria?.true);
591
+ const whenFalse = entryText(question.criteria?.false);
592
+ const parts = [instructions];
593
+ if (whenTrue)
594
+ parts.push(`True when: ${whenTrue}`);
595
+ if (whenFalse)
596
+ parts.push(`False when: ${whenFalse}`);
597
+ return { type: "predicate", name, instructions: parts.join(`
598
+
599
+ `) };
600
+ }
601
+ case "choice":
602
+ return {
603
+ type: "choice",
604
+ name,
605
+ instructions,
606
+ choices: Object.entries(question.criteria).map(([value, entry]) => {
607
+ const description = entryText(entry);
608
+ return description === undefined ? { value } : { value, description };
609
+ })
610
+ };
611
+ case "score":
612
+ return {
613
+ type: "score",
614
+ name,
615
+ instructions,
616
+ levels: question.criteria.map((entry, index) => ({
617
+ label: entryText(entry) ?? String(index)
618
+ }))
619
+ };
620
+ }
621
+ }
622
+ function probabilityMap(value, at) {
623
+ if (!Array.isArray(value))
624
+ return value;
625
+ const out = {};
626
+ for (const item of value) {
627
+ if (!isRecord(item) || typeof item.value !== "string" && typeof item.value !== "number") {
628
+ wireContractError(`${at} entries must carry a string or number value`);
629
+ }
630
+ const key = String(item.value);
631
+ if (Object.hasOwn(out, key))
632
+ wireContractError(`${at} lists an option twice`);
633
+ out[key] = item.probability;
634
+ }
635
+ return out;
636
+ }
637
+ function toSystemOneAnswer(id, question, raw) {
638
+ const at = `answer "${id}".probabilities`;
639
+ switch (raw.type) {
640
+ case "predicate":
641
+ return { type: "noul", noul: raw.probability };
642
+ case "choice":
643
+ return {
644
+ type: "choice",
645
+ choice: raw.choice,
646
+ probabilities: probabilityMap(raw.probabilities, at),
647
+ confidence: raw.confidence
648
+ };
649
+ case "score":
650
+ return {
651
+ type: "score",
652
+ score: raw.score,
653
+ probabilities: probabilityMap(raw.probabilities, at),
654
+ legend: question.type === "score" ? Object.fromEntries(question.criteria.map((entry, index) => [String(index), entry])) : undefined,
655
+ confidence: raw.confidence
656
+ };
657
+ default:
658
+ return raw;
659
+ }
660
+ }
661
+ var OPENAI_DECISIONS_WIRE = {
662
+ buildRequest: (config, model) => ({
663
+ ...model === undefined ? {} : { model },
664
+ input: openaiInput(config.state),
665
+ questions: Object.entries(config.questions).map(([name, question]) => openaiQuestion(name, question))
666
+ }),
667
+ toSystemOne(body, { questions }) {
668
+ if (!isRecord(body))
669
+ wireContractError("response body must be a JSON object");
670
+ if (!Array.isArray(body.answers))
671
+ wireContractError("response is missing the answers array");
672
+ const byName = new Map;
673
+ for (const answer of body.answers) {
674
+ if (!isRecord(answer) || typeof answer.name !== "string") {
675
+ wireContractError("every answer must be an object naming its question");
676
+ }
677
+ if (!Object.hasOwn(questions, answer.name)) {
678
+ wireContractError("response contains an answer for an undeclared question");
679
+ }
680
+ if (byName.has(answer.name)) {
681
+ wireContractError(`response answers question "${answer.name}" twice`);
682
+ }
683
+ byName.set(answer.name, answer);
684
+ }
685
+ const refused = [...byName.values()].filter((answer) => answer.type === "refusal").map((answer) => `"${answer.name}"`);
686
+ if (refused.length > 0) {
687
+ throw new SystemOneWireError(`OpenAI refused question${refused.length > 1 ? "s" : ""} ${refused.join(", ")}; no decision was made`);
688
+ }
689
+ const answers = {};
690
+ for (const [id, answer] of byName) {
691
+ const question = questions[id];
692
+ if (question)
693
+ answers[id] = toSystemOneAnswer(id, question, answer);
694
+ }
695
+ const usage = isRecord(body.usage) ? { input_tokens: body.usage.input_tokens, output_tokens: body.usage.output_tokens } : body.usage;
696
+ return { model: body.model, answers, usage };
697
+ }
698
+ };
699
+
700
+ // src/workflows/executors/workflow-llm.ts
701
+ init_openrouter_base_url();
702
+ async function resolveWorkflowLlmConfig(requestedModel, env = process.env, options = {}) {
703
+ const credential = await resolveCredential({ env });
704
+ if (!credential) {
705
+ throw new Error("No workflow LLM credential found. Set OPENROUTER_API_KEY or OPENAI_API_KEY.");
706
+ }
707
+ if (options.requireKind && credential.kind !== options.requireKind) {
708
+ throw new Error(`No ${options.requireKind} credential found for this workflow LLM call.`);
709
+ }
710
+ if (credential.kind !== "openrouter" && credential.kind !== "openai") {
711
+ throw new Error(`Workflow LLM nodes do not support the resolved ${credential.kind} credential yet. Set OPENROUTER_API_KEY or OPENAI_API_KEY.`);
712
+ }
713
+ const providerPrefix = `${credential.kind}/`;
714
+ const model = requestedModel ?? DEFAULT_MODEL[credential.kind];
715
+ const baseURL = credential.kind === "openrouter" ? getOpenRouterBaseUrl(env) : undefined;
716
+ return {
717
+ apiKey: credential.apiKey,
718
+ baseURL,
719
+ headers: baseURL ? getOpenRouterAttributionHeaders(baseURL, env) : {},
720
+ model: model.startsWith(providerPrefix) ? model.slice(providerPrefix.length) : model
721
+ };
722
+ }
723
+
724
+ // src/workflows/executors/system-one-providers.ts
725
+ var TYPESAFE_KEY = "TYPESAFE_API_KEY";
726
+ var OPENROUTER_KEY = "OPENROUTER_API_KEY";
727
+ var LAYA_KEY = "LAYA_API_KEY";
728
+ var LAYA_URL_KEY = "LAYA_URL";
729
+ var OPENAI_DECISIONS_KEY = "OPENAI_DECISIONS_API_KEY";
730
+ var CLOUDFLARE_TOKEN_KEY = "CLOUDFLARE_API_TOKEN";
731
+ var CLOUDFLARE_ACCOUNT_KEY = "CLOUDFLARE_ACCOUNT_ID";
732
+ var CLOUDFLARE_ACCOUNT_RE = /^[0-9a-f]{32}$/;
733
+ async function readGlobalConfig(db, key) {
734
+ const rows = await db.getSwarmConfigs({ scope: "global", key });
735
+ return rows.find((row) => row.scope === "global" && row.key === key)?.value;
736
+ }
737
+ var SYSTEM_ONE_PROVIDERS = {
738
+ typesafe: {
739
+ label: "TypeSafe",
740
+ keyName: TYPESAFE_KEY,
741
+ endpoint: "https://api.typesafe.ai/v1/systemone",
742
+ defaultModel: "jev-latest",
743
+ confidenceField: "confidence",
744
+ readKey: ({ db }) => readGlobalConfig(db, TYPESAFE_KEY)
745
+ },
746
+ openrouter: {
747
+ label: "OpenRouter",
748
+ keyName: OPENROUTER_KEY,
749
+ endpoint: "https://openrouter.ai/api/alpha/decisions",
750
+ defaultModel: "~typesafe/jev-latest",
751
+ confidenceField: "confidence",
752
+ deploymentProblem(env) {
753
+ return getOpenRouterBaseUrl(env) === DEFAULT_OPENROUTER_BASE_URL ? null : 'OPENROUTER_BASE_URL points at a gateway. SystemOne provider "openrouter" calls openrouter.ai directly and is not supported through a gateway; unset OPENROUTER_BASE_URL or use provider "typesafe".';
754
+ },
755
+ async readKey({ env }) {
756
+ try {
757
+ return (await resolveWorkflowLlmConfig(undefined, env, { requireKind: "openrouter" })).apiKey;
758
+ } catch {
759
+ return null;
760
+ }
761
+ }
762
+ },
763
+ laya: {
764
+ label: "laya",
765
+ keyName: LAYA_KEY,
766
+ endpoint: { configKey: LAYA_URL_KEY, path: "/v1/systemone" },
767
+ confidenceField: "answer_confidence",
768
+ readKey: ({ db }) => readGlobalConfig(db, LAYA_KEY)
769
+ },
770
+ openai: {
771
+ label: "OpenAI",
772
+ keyName: OPENAI_DECISIONS_KEY,
773
+ endpoint: "https://api.openai.com/v1/decisions",
774
+ defaultModel: "gpt-6-luna",
775
+ confidenceField: "confidence",
776
+ wire: OPENAI_DECISIONS_WIRE,
777
+ readKey: ({ db }) => readGlobalConfig(db, OPENAI_DECISIONS_KEY)
778
+ },
779
+ cloudflare: {
780
+ label: "Cloudflare Workers AI",
781
+ keyName: CLOUDFLARE_TOKEN_KEY,
782
+ endpoint: {
783
+ configKey: CLOUDFLARE_ACCOUNT_KEY,
784
+ describe: "the 32-character id of a Cloudflare account",
785
+ build: (accountId, model) => {
786
+ const id = accountId.trim();
787
+ return CLOUDFLARE_ACCOUNT_RE.test(id) ? `https://api.cloudflare.com/client/v4/accounts/${id}/ai/run/@cf/cloudflare/${model}` : null;
788
+ }
789
+ },
790
+ defaultModel: "clef",
791
+ allowedModels: ["clef", "clef-flash"],
792
+ limits: { maxQuestions: 64 },
793
+ confidenceField: "confidence",
794
+ wire: CLOUDFLARE_WIRE,
795
+ readKey: ({ db }) => readGlobalConfig(db, CLOUDFLARE_TOKEN_KEY)
796
+ }
797
+ };
798
+ var SYSTEM_ONE_PROVIDER_IDS = Object.keys(SYSTEM_ONE_PROVIDERS);
799
+ var SYSTEM_ONE_DEFAULT_PROVIDER = "typesafe";
800
+ function isSystemOneProviderId(value) {
801
+ return typeof value === "string" && Object.hasOwn(SYSTEM_ONE_PROVIDERS, value);
802
+ }
803
+ function systemOneProviderOf(config) {
804
+ if (config.provider === undefined)
805
+ return SYSTEM_ONE_DEFAULT_PROVIDER;
806
+ return isSystemOneProviderId(config.provider) ? config.provider : undefined;
807
+ }
808
+ var SET_IT = (keyName) => `Set ${keyName} as a global secret on the Secrets page (Settings > Secrets).`;
809
+ var SET_URL = (configKey) => `Set ${configKey} as a global config value (set-config with scope global).`;
810
+ function systemOneKeyMissingMessage(id) {
811
+ const { label, keyName } = SYSTEM_ONE_PROVIDERS[id];
812
+ return `${keyName} is not configured. SystemOne provider "${id}" needs a working ${label} API key. ${SET_IT(keyName)}`;
813
+ }
814
+ function systemOneKeyUnreadableMessage(id) {
815
+ return `Could not read ${SYSTEM_ONE_PROVIDERS[id].keyName} from swarm config`;
816
+ }
817
+ function systemOneKeyMalformedMessage(id) {
818
+ return `${SYSTEM_ONE_PROVIDERS[id].keyName} is not a valid bearer token value. ${SET_IT(SYSTEM_ONE_PROVIDERS[id].keyName)}`;
819
+ }
820
+ function systemOneKeyRejectedMessage(id, status, code) {
821
+ const { label, keyName } = SYSTEM_ONE_PROVIDERS[id];
822
+ const detail = code ? ` (${code})` : "";
823
+ return `${keyName} was rejected by ${label} (HTTP ${status}${detail}). ` + `The key is invalid, revoked, or lacks access to the model. Replace it on the Secrets page (Settings > Secrets). No decision was made.`;
824
+ }
825
+ function urlKeyOf(provider) {
826
+ return typeof provider.endpoint === "string" ? null : provider.endpoint.configKey;
827
+ }
828
+ function isBuiltEndpoint(endpoint) {
829
+ return typeof endpoint !== "string" && "build" in endpoint;
830
+ }
831
+ function urlDescriptionOf(provider) {
832
+ return isBuiltEndpoint(provider.endpoint) ? provider.endpoint.describe : `the URL of a ${provider.label} server`;
833
+ }
834
+ function systemOneUrlMissingMessage(id) {
835
+ const provider = SYSTEM_ONE_PROVIDERS[id];
836
+ const configKey = urlKeyOf(provider) ?? "";
837
+ return `${configKey} is not configured. SystemOne provider "${id}" needs ${urlDescriptionOf(provider)}. ${SET_URL(configKey)}`;
838
+ }
839
+ function systemOneUrlUnreadableMessage(id) {
840
+ return `Could not read ${urlKeyOf(SYSTEM_ONE_PROVIDERS[id]) ?? ""} from swarm config`;
841
+ }
842
+ function systemOneUrlInvalidMessage(id) {
843
+ const provider = SYSTEM_ONE_PROVIDERS[id];
844
+ const configKey = urlKeyOf(provider) ?? "";
845
+ if (isBuiltEndpoint(provider.endpoint)) {
846
+ return `${configKey} is not ${provider.endpoint.describe}. ${SET_URL(configKey)}`;
847
+ }
848
+ 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)}`;
849
+ }
850
+ function systemOneModelProblem(id, model) {
851
+ const { allowedModels } = SYSTEM_ONE_PROVIDERS[id];
852
+ if (!allowedModels || model !== undefined && allowedModels.includes(model))
853
+ return null;
854
+ return `SystemOne provider "${id}" serves only the models ${allowedModels.join(", ")}; set model to one of them or leave it unset.`;
855
+ }
856
+ function systemOneQuestionLimitProblem(id, count) {
857
+ const max = SYSTEM_ONE_PROVIDERS[id].limits?.maxQuestions;
858
+ return max !== undefined && count > max ? `SystemOne provider "${id}" takes at most ${max} questions per node; this node has ${count}.` : null;
859
+ }
860
+ function hasWhitespaceOrControl(value) {
861
+ for (const char of value) {
862
+ const code = char.charCodeAt(0);
863
+ if (code <= 32 || code === 127)
864
+ return true;
865
+ }
866
+ return false;
867
+ }
868
+ var LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
869
+ function systemOneEndpointFromBase(base, path) {
870
+ let url;
871
+ try {
872
+ url = new URL(base.trim());
873
+ } catch {
874
+ return null;
875
+ }
876
+ const secure = url.protocol === "https:";
877
+ const loopback = url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
878
+ if (!secure && !loopback)
879
+ return null;
880
+ if (url.username || url.password || url.search || url.hash)
881
+ return null;
882
+ return `${url.origin}${url.pathname.replace(/\/+$/, "")}${path}`;
883
+ }
884
+ async function resolveSystemOneTarget(id, ctx, overrides = {}) {
885
+ const provider = SYSTEM_ONE_PROVIDERS[id];
886
+ const deploymentProblem = provider.deploymentProblem?.(ctx.env);
887
+ if (deploymentProblem)
888
+ return { ok: false, error: deploymentProblem };
889
+ const errors = [];
890
+ let endpoint;
891
+ if (typeof provider.endpoint === "string") {
892
+ endpoint = provider.endpoint;
893
+ } else {
894
+ const { configKey } = provider.endpoint;
895
+ let base;
896
+ let readable = true;
897
+ try {
898
+ base = await (overrides.serverUrl ? overrides.serverUrl(id) : readGlobalConfig(ctx.db, configKey));
899
+ } catch {
900
+ readable = false;
901
+ errors.push(systemOneUrlUnreadableMessage(id));
902
+ }
903
+ if (readable) {
904
+ if (!base || base.trim() === "") {
905
+ errors.push(systemOneUrlMissingMessage(id));
906
+ } else {
907
+ const resolved = isBuiltEndpoint(provider.endpoint) ? provider.endpoint.build(base, overrides.model ?? provider.defaultModel ?? "") : systemOneEndpointFromBase(base, provider.endpoint.path);
908
+ if (resolved)
909
+ endpoint = resolved;
910
+ else
911
+ errors.push(systemOneUrlInvalidMessage(id));
912
+ }
913
+ }
914
+ }
915
+ let apiKey;
916
+ let keyProblem;
917
+ try {
918
+ apiKey = await (overrides.apiKey ? overrides.apiKey(id) : provider.readKey(ctx));
919
+ } catch {
920
+ keyProblem = systemOneKeyUnreadableMessage(id);
921
+ }
922
+ if (!keyProblem) {
923
+ if (!apiKey || apiKey.trim() === "")
924
+ keyProblem = systemOneKeyMissingMessage(id);
925
+ else if (hasWhitespaceOrControl(apiKey))
926
+ keyProblem = systemOneKeyMalformedMessage(id);
927
+ }
928
+ if (keyProblem)
929
+ errors.push(keyProblem);
930
+ if (errors.length > 0 || !endpoint || !apiKey)
931
+ return { ok: false, error: errors.join(" ") };
932
+ return { ok: true, apiKey, endpoint };
933
+ }
934
+
935
+ // src/workflows/executors/system-one-decision.ts
936
+ var SYSTEM_ONE_DECISION_NODE_TYPE = "system-one-decision";
937
+ var SYSTEM_ONE_ENDPOINT = SYSTEM_ONE_PROVIDERS.typesafe.endpoint;
938
+ var SYSTEM_ONE_API_KEY_CONFIG_KEY = SYSTEM_ONE_PROVIDERS.typesafe.keyName;
939
+ var SYSTEM_ONE_DEFAULT_TIMEOUT_MS = 30000;
940
+ var SYSTEM_ONE_MIN_TIMEOUT_MS = 1000;
941
+ var SYSTEM_ONE_MAX_TIMEOUT_MS = 300000;
942
+ var SYSTEM_ONE_DEFAULT_MAX_RETRIES = 2;
943
+ var SYSTEM_ONE_MAX_RETRIES_LIMIT = 3;
944
+ var CHOICE_MIN_OPTIONS = 2;
945
+ var CHOICE_MAX_OPTIONS = 255;
946
+ var SCORE_MIN_LEVELS = 2;
947
+ var SCORE_MAX_LEVELS = 10;
948
+ var DEADLINE_MARGIN_MS = 250;
949
+ var BACKOFF_BASE_MS = 500;
950
+ var BACKOFF_CAP_MS = 8000;
951
+ var DISTRIBUTION_TOLERANCE = 0.02;
952
+ var MAX_RESPONSE_CHARS = 1048576;
953
+ var MAX_ERROR_BODY_CHARS = 65536;
954
+ var REVIEW_PORTS = ["approved", "rejected", "timeout"];
955
+ var ENGINE_RETRY_DEFAULT = 3;
956
+ var SystemOneEntrySchema = exports_external.union([
957
+ exports_external.string(),
958
+ exports_external.null(),
959
+ exports_external.array(exports_external.unknown()),
960
+ exports_external.record(exports_external.string(), exports_external.unknown())
961
+ ]);
962
+ var SystemOneInstructionsSchema = SystemOneEntrySchema.refine((value) => value !== null && !(typeof value === "string" && value.trim() === ""), { message: "instructions must not be null or blank" });
963
+ var SystemOneStateSchema = exports_external.union([
964
+ exports_external.string().min(1, "state must not be empty"),
965
+ exports_external.array(exports_external.string()).min(1, "state must not be an empty array"),
966
+ exports_external.record(exports_external.string(), exports_external.unknown())
967
+ ]);
968
+ var NoulQuestionSchema = exports_external.strictObject({
969
+ type: exports_external.literal("noul"),
970
+ instructions: SystemOneInstructionsSchema,
971
+ criteria: exports_external.strictObject({ true: SystemOneEntrySchema.optional(), false: SystemOneEntrySchema.optional() }).refine((criteria) => criteria.true !== undefined || criteria.false !== undefined, {
972
+ message: "noul criteria must describe at least one of true / false"
973
+ }).optional()
974
+ });
975
+ var ChoiceQuestionSchema = exports_external.strictObject({
976
+ type: exports_external.literal("choice"),
977
+ instructions: SystemOneInstructionsSchema,
978
+ criteria: exports_external.record(exports_external.string().min(1), SystemOneEntrySchema).refine((criteria) => {
979
+ const count = Object.keys(criteria).length;
980
+ return count >= CHOICE_MIN_OPTIONS && count <= CHOICE_MAX_OPTIONS;
981
+ }, {
982
+ message: `choice criteria must define ${CHOICE_MIN_OPTIONS}-${CHOICE_MAX_OPTIONS} options`
983
+ })
984
+ });
985
+ var ScoreQuestionSchema = exports_external.strictObject({
986
+ type: exports_external.literal("score"),
987
+ instructions: SystemOneInstructionsSchema,
988
+ criteria: exports_external.array(SystemOneEntrySchema).min(SCORE_MIN_LEVELS).max(SCORE_MAX_LEVELS)
989
+ });
990
+ var SystemOneQuestionSchema = exports_external.discriminatedUnion("type", [
991
+ NoulQuestionSchema,
992
+ ChoiceQuestionSchema,
993
+ ScoreQuestionSchema
994
+ ]);
995
+ var Unit = exports_external.number().min(0).max(1);
996
+ var QUESTION_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
997
+ var RESERVED_QUESTION_IDS = new Set([
998
+ ...Object.getOwnPropertyNames(Object.prototype),
999
+ "prototype"
1000
+ ]);
1001
+ var SystemOnePrimitiveTypeSchema = exports_external.enum(["noul", "choice", "score"]);
1002
+ var HumanReviewSchema = exports_external.strictObject({
1003
+ band: exports_external.strictObject({ min: Unit, max: Unit }).refine((band) => band.min <= band.max, { message: "band.min must not exceed band.max" }),
1004
+ approvers: ApproverConfigSchema,
1005
+ title: exports_external.string().min(1).optional(),
1006
+ timeout: HITLTimeoutSchema.optional(),
1007
+ notifications: exports_external.array(NotificationConfigSchema).optional()
1008
+ });
1009
+ var SystemOneDecisionConfigSchema = exports_external.strictObject({
1010
+ provider: exports_external.enum(SYSTEM_ONE_PROVIDER_IDS).default(SYSTEM_ONE_DEFAULT_PROVIDER),
1011
+ model: exports_external.string().min(1).optional(),
1012
+ state: SystemOneStateSchema,
1013
+ timeoutMs: exports_external.number().int().min(SYSTEM_ONE_MIN_TIMEOUT_MS).max(SYSTEM_ONE_MAX_TIMEOUT_MS).default(SYSTEM_ONE_DEFAULT_TIMEOUT_MS),
1014
+ maxRetries: exports_external.number().int().min(0).max(SYSTEM_ONE_MAX_RETRIES_LIMIT).default(SYSTEM_ONE_DEFAULT_MAX_RETRIES),
1015
+ questions: exports_external.record(exports_external.string().regex(QUESTION_ID_RE, "question ids must start with a letter or underscore and use only letters, digits, _ or -"), SystemOneQuestionSchema),
1016
+ returns: exports_external.record(exports_external.string(), exports_external.strictObject({ type: SystemOnePrimitiveTypeSchema })),
1017
+ humanReview: HumanReviewSchema.optional()
1018
+ }).superRefine((config, ctx) => {
1019
+ const questionIds = Object.keys(config.questions);
1020
+ if (questionIds.length === 0) {
1021
+ ctx.addIssue({
1022
+ code: "custom",
1023
+ path: ["questions"],
1024
+ message: "system-one-decision needs at least one question"
1025
+ });
1026
+ }
1027
+ for (const id of questionIds) {
1028
+ if (RESERVED_QUESTION_IDS.has(id)) {
1029
+ ctx.addIssue({
1030
+ code: "custom",
1031
+ path: ["questions", id],
1032
+ message: `"${id}" is reserved and cannot be a question id`
1033
+ });
1034
+ }
1035
+ const declared = Object.hasOwn(config.returns, id) ? config.returns[id] : undefined;
1036
+ if (!declared) {
1037
+ ctx.addIssue({
1038
+ code: "custom",
1039
+ path: ["returns"],
1040
+ message: `returns must declare question "${id}"`
1041
+ });
1042
+ } else if (declared.type !== config.questions[id]?.type) {
1043
+ ctx.addIssue({
1044
+ code: "custom",
1045
+ path: ["returns", id, "type"],
1046
+ message: `returns.${id}.type is "${declared.type}" but question "${id}" is "${config.questions[id]?.type}"`
1047
+ });
1048
+ }
1049
+ }
1050
+ for (const id of Object.keys(config.returns)) {
1051
+ if (!Object.hasOwn(config.questions, id)) {
1052
+ ctx.addIssue({
1053
+ code: "custom",
1054
+ path: ["returns", id],
1055
+ message: `returns declares "${id}" but no question has that id`
1056
+ });
1057
+ }
1058
+ }
1059
+ const modelProblem = systemOneModelProblem(config.provider, config.model ?? SYSTEM_ONE_PROVIDERS[config.provider].defaultModel);
1060
+ if (modelProblem)
1061
+ ctx.addIssue({ code: "custom", path: ["model"], message: modelProblem });
1062
+ const limitProblem = systemOneQuestionLimitProblem(config.provider, questionIds.length);
1063
+ if (limitProblem)
1064
+ ctx.addIssue({ code: "custom", path: ["questions"], message: limitProblem });
1065
+ });
1066
+ var NoulAnswerSchema = exports_external.strictObject({ type: exports_external.literal("noul"), noul: Unit });
1067
+ var ChoiceAnswerSchema = exports_external.strictObject({
1068
+ type: exports_external.literal("choice"),
1069
+ choice: exports_external.string(),
1070
+ probabilities: exports_external.record(exports_external.string(), Unit),
1071
+ confidence: Unit
1072
+ });
1073
+ var ScoreAnswerSchema = exports_external.strictObject({
1074
+ type: exports_external.literal("score"),
1075
+ score: exports_external.number(),
1076
+ legend: exports_external.record(exports_external.string(), SystemOneEntrySchema),
1077
+ probabilities: exports_external.record(exports_external.string(), Unit),
1078
+ confidence: Unit
1079
+ });
1080
+ var SystemOneAnswerSchema = exports_external.discriminatedUnion("type", [
1081
+ NoulAnswerSchema,
1082
+ ChoiceAnswerSchema,
1083
+ ScoreAnswerSchema
1084
+ ]);
1085
+ var ReviewQuestionSchema = exports_external.strictObject({
1086
+ confidence: Unit,
1087
+ inBand: exports_external.boolean(),
1088
+ decidedBy: exports_external.enum(["model", "human"]),
1089
+ modelAnswer: exports_external.union([exports_external.string(), exports_external.number()]).optional()
1090
+ });
1091
+ var ReviewSchema = exports_external.strictObject({
1092
+ status: exports_external.enum(["not_required", "pending", "approved", "rejected", "timeout"]),
1093
+ approvalRequestId: exports_external.string().optional(),
1094
+ reason: exports_external.string().optional(),
1095
+ questions: exports_external.record(exports_external.string(), ReviewQuestionSchema)
1096
+ });
1097
+ var SystemOneDecisionOutputSchema = exports_external.object({
1098
+ model: exports_external.string().min(1),
1099
+ answers: exports_external.record(exports_external.string(), SystemOneAnswerSchema),
1100
+ usage: exports_external.object({
1101
+ input_tokens: exports_external.number().int().min(0),
1102
+ output_tokens: exports_external.number().int().min(0)
1103
+ }),
1104
+ requestId: exports_external.string().optional(),
1105
+ routing: exports_external.strictObject({ model: exports_external.string().min(1) }).optional(),
1106
+ review: ReviewSchema.optional()
1107
+ });
1108
+ function systemOneRetryViolations(node) {
1109
+ if (node.type !== SYSTEM_ONE_DECISION_NODE_TYPE)
1110
+ return [];
1111
+ const violations = [];
1112
+ const attempts = (retry) => retry ? retry.maxRetries ?? ENGINE_RETRY_DEFAULT : 0;
1113
+ if (attempts(node.retry) > 0) {
1114
+ violations.push(`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`);
1115
+ }
1116
+ if (attempts(node.validation?.retry) > 0) {
1117
+ violations.push(`Node "${node.id}" (system-one-decision) must not set validation.retry.maxRetries > 0: a validation-driven retry would send another paid request`);
1118
+ }
1119
+ return violations;
1120
+ }
1121
+ function systemOneStaticShapeViolations(node) {
1122
+ if (node.type !== SYSTEM_ONE_DECISION_NODE_TYPE)
1123
+ return [];
1124
+ const violations = [];
1125
+ for (const field of ["questions", "returns"]) {
1126
+ const value = node.config[field];
1127
+ if (value !== undefined && (typeof value !== "object" || value === null || Array.isArray(value))) {
1128
+ violations.push(`Node "${node.id}" (system-one-decision) config.${field} must be an object: question ids and return types are static, only state and descriptions may use {{tokens}}`);
1129
+ }
1130
+ }
1131
+ const provider = node.config.provider;
1132
+ if (typeof provider === "string" && provider.includes("{{")) {
1133
+ violations.push(`Node "${node.id}" (system-one-decision) config.provider must be a literal (${SYSTEM_ONE_PROVIDER_IDS.join(", ")}), not a {{token}}: the provider decides which credential is checked before the run starts`);
1134
+ }
1135
+ if (isRecord2(node.config.humanReview) && node.next != null) {
1136
+ if (typeof node.next === "string" || Array.isArray(node.next)) {
1137
+ violations.push(`Node "${node.id}" (system-one-decision) sets humanReview, so next must map output ports (${REVIEW_PORTS.join(", ")}): a single or list next would run the same successors after a rejection or a timeout`);
1138
+ } else {
1139
+ const ports = Object.keys(node.next);
1140
+ const unknown = ports.filter((port) => !REVIEW_PORTS.includes(port));
1141
+ if (unknown.length > 0) {
1142
+ violations.push(`Node "${node.id}" (system-one-decision) sets humanReview, so next ports must be ${REVIEW_PORTS.join(", ")}; got ${unknown.map((port) => `"${port}"`).join(", ")}`);
1143
+ }
1144
+ if (!ports.includes("approved")) {
1145
+ violations.push(`Node "${node.id}" (system-one-decision) sets humanReview, so next must define the "approved" port: it carries every answer that is accepted, by the model or by a person`);
1146
+ }
1147
+ }
1148
+ }
1149
+ return violations;
1150
+ }
1151
+ function systemOneUnresolvedError(nodeId, tokens) {
1152
+ const rendered = [...new Set(tokens)].map((token) => `{{${token}}}`).join(", ");
1153
+ return `SystemOne Decision node "${nodeId}" has unresolved interpolation token(s): ${rendered}. ` + "No request was sent. Declare the value in the node's inputs mapping or fix the path.";
1154
+ }
1155
+
1156
+ class SystemOneContractError extends Error {
1157
+ }
1158
+ function fail(message) {
1159
+ throw new SystemOneContractError(message);
1160
+ }
1161
+ function isRecord2(value) {
1162
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1163
+ }
1164
+ function unit(value, what) {
1165
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
1166
+ fail(`${what} must be a finite number between 0 and 1`);
1167
+ }
1168
+ return value;
1169
+ }
1170
+ function distribution(value, keys, what) {
1171
+ if (!isRecord2(value))
1172
+ fail(`${what} must be an object`);
1173
+ const actual = Object.keys(value);
1174
+ const missing = keys.filter((key) => !Object.hasOwn(value, key));
1175
+ const extra = actual.filter((key) => !keys.includes(key));
1176
+ if (missing.length > 0 || extra.length > 0) {
1177
+ fail(`${what} keys must be exactly the declared ${keys.length} option(s)` + (missing.length > 0 ? `; missing ${missing.length}` : "") + (extra.length > 0 ? `; ${extra.length} unexpected` : ""));
1178
+ }
1179
+ const out = {};
1180
+ let sum = 0;
1181
+ for (const key of keys) {
1182
+ const p = unit(value[key], `${what}["${key}"]`);
1183
+ out[key] = p;
1184
+ sum += p;
1185
+ }
1186
+ if (Math.abs(sum - 1) > DISTRIBUTION_TOLERANCE) {
1187
+ fail(`${what} must sum to 1 (within ${DISTRIBUTION_TOLERANCE})`);
1188
+ }
1189
+ return out;
1190
+ }
1191
+ function validateAnswer(id, question, raw, confidenceField) {
1192
+ const at = `answer "${id}"`;
1193
+ if (!isRecord2(raw))
1194
+ fail(`${at} must be an object`);
1195
+ if (raw.type !== question.type) {
1196
+ fail(`${at} has type ${JSON.stringify(raw.type)} but the question declares "${question.type}"`);
1197
+ }
1198
+ switch (question.type) {
1199
+ case "noul":
1200
+ return { type: "noul", noul: unit(raw.noul, `${at}.noul`) };
1201
+ case "choice": {
1202
+ const options = Object.keys(question.criteria);
1203
+ if (typeof raw.choice !== "string" || !Object.hasOwn(question.criteria, raw.choice)) {
1204
+ fail(`${at}.choice is not one of the declared options`);
1205
+ }
1206
+ return {
1207
+ type: "choice",
1208
+ choice: raw.choice,
1209
+ probabilities: distribution(raw.probabilities, options, `${at}.probabilities`),
1210
+ confidence: unit(raw[confidenceField], `${at}.${confidenceField}`)
1211
+ };
1212
+ }
1213
+ case "score": {
1214
+ const indices = question.criteria.map((_, index) => String(index));
1215
+ const top = indices.length - 1;
1216
+ if (typeof raw.score !== "number" || !Number.isFinite(raw.score) || raw.score < 0 || raw.score > top) {
1217
+ fail(`${at}.score must be a finite number between 0 and ${top}`);
1218
+ }
1219
+ if (!isRecord2(raw.legend))
1220
+ fail(`${at}.legend must be an object`);
1221
+ const legend = {};
1222
+ for (const key of indices) {
1223
+ if (!Object.hasOwn(raw.legend, key))
1224
+ fail(`${at}.legend is missing level ${key}`);
1225
+ const entry = SystemOneEntrySchema.safeParse(raw.legend[key]);
1226
+ if (!entry.success)
1227
+ fail(`${at}.legend["${key}"] must be a string, object, array, or null`);
1228
+ legend[key] = entry.data;
1229
+ }
1230
+ return {
1231
+ type: "score",
1232
+ score: raw.score,
1233
+ legend,
1234
+ probabilities: distribution(raw.probabilities, indices, `${at}.probabilities`),
1235
+ confidence: unit(raw[confidenceField], `${at}.${confidenceField}`)
1236
+ };
1237
+ }
1238
+ }
1239
+ }
1240
+ var MAX_ROUTING_MODEL_CHARS = 128;
1241
+ function readRoutingModel(routing) {
1242
+ if (!isRecord2(routing))
1243
+ return;
1244
+ const { model } = routing;
1245
+ return typeof model === "string" && model !== "" && model.length <= MAX_ROUTING_MODEL_CHARS ? model : undefined;
1246
+ }
1247
+ function validateSystemOneResponse(questions, body, requestId, confidenceField = "confidence") {
1248
+ if (!isRecord2(body))
1249
+ fail("response body must be a JSON object");
1250
+ if (typeof body.model !== "string" || body.model === "") {
1251
+ fail("response is missing the model that produced the answers");
1252
+ }
1253
+ if (!isRecord2(body.answers))
1254
+ fail("response is missing the answers object");
1255
+ const usage = exports_external.object({ input_tokens: exports_external.number().int().min(0), output_tokens: exports_external.number().int().min(0) }).safeParse(body.usage);
1256
+ if (!usage.success)
1257
+ fail("response usage must report non-negative integer input_tokens and output_tokens");
1258
+ const declared = Object.keys(questions);
1259
+ const unexpected = Object.keys(body.answers).filter((id) => !Object.hasOwn(questions, id));
1260
+ if (unexpected.length > 0)
1261
+ fail(`response contains ${unexpected.length} answer(s) for undeclared questions`);
1262
+ const answers = {};
1263
+ for (const id of declared) {
1264
+ if (!Object.hasOwn(body.answers, id))
1265
+ fail(`response is missing the answer for question "${id}"`);
1266
+ const question = questions[id];
1267
+ if (!question)
1268
+ fail(`question "${id}" is not defined`);
1269
+ answers[id] = validateAnswer(id, question, body.answers[id], confidenceField);
1270
+ }
1271
+ if (declared.length === 0)
1272
+ fail("a system-one-decision call needs at least one question");
1273
+ const routingModel = readRoutingModel(body.routing);
1274
+ return {
1275
+ model: body.model,
1276
+ answers,
1277
+ usage: usage.data,
1278
+ ...requestId ? { requestId } : {},
1279
+ ...routingModel ? { routing: { model: routingModel } } : {}
1280
+ };
1281
+ }
1282
+ var REVIEW_CONFIRM_ID = "$confirm";
1283
+ var REVIEW_STATE_CHARS = 2000;
1284
+ var REVIEW_LABEL_CHARS = 160;
1285
+ var REVIEW_OPTION_CHARS = 200;
1286
+ var round6 = (value) => Math.round(value * 1e6) / 1e6;
1287
+ function answerConfidence(answer) {
1288
+ return answer.type === "noul" ? round6(Math.max(answer.noul, 1 - answer.noul)) : answer.confidence;
1289
+ }
1290
+ function clip(text, max) {
1291
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
1292
+ }
1293
+ function showEntry(entry, max) {
1294
+ return clip(typeof entry === "string" ? entry : JSON.stringify(entry) ?? "", max);
1295
+ }
1296
+ function modelAnswerOf(answer) {
1297
+ return answer.type === "choice" ? answer.choice : answer.type === "score" ? answer.score : answer.noul;
1298
+ }
1299
+ function buildReviewCard(nodeId, config, review, parked) {
1300
+ const state = scrubSecrets(typeof config.state === "string" ? config.state : JSON.stringify(config.state) ?? "");
1301
+ const questions = [
1302
+ {
1303
+ id: REVIEW_CONFIRM_ID,
1304
+ type: "approval",
1305
+ label: "Accept these answers and continue?",
1306
+ required: true,
1307
+ description: `Model ${parked.model}. Input: ${clip(state, REVIEW_STATE_CHARS)}`
1308
+ }
1309
+ ];
1310
+ for (const [id, info] of Object.entries(parked.review?.questions ?? {})) {
1311
+ if (!info.inBand)
1312
+ continue;
1313
+ const question = config.questions[id];
1314
+ const answer = parked.answers[id];
1315
+ if (!question || !answer)
1316
+ continue;
1317
+ const label = `${id}: ${showEntry(question.instructions, REVIEW_LABEL_CHARS)}`;
1318
+ const said = `The model answered ${JSON.stringify(modelAnswerOf(answer))} at confidence ${info.confidence}.`;
1319
+ if (question.type === "choice" && answer.type === "choice") {
1320
+ questions.push({
1321
+ id,
1322
+ type: "single-select",
1323
+ label,
1324
+ required: false,
1325
+ description: `${said} Leave empty to confirm it, or pick another option.`,
1326
+ options: Object.entries(question.criteria).map(([value, entry]) => ({
1327
+ value,
1328
+ label: value,
1329
+ ...typeof entry === "string" && entry ? { description: clip(entry, REVIEW_OPTION_CHARS) } : {}
1330
+ }))
1331
+ });
1332
+ } else if (question.type === "score" && answer.type === "score") {
1333
+ questions.push({
1334
+ id,
1335
+ type: "single-select",
1336
+ label,
1337
+ required: false,
1338
+ description: `${said} Leave empty to confirm it, or pick a level.`,
1339
+ options: question.criteria.map((entry, level) => ({
1340
+ value: String(level),
1341
+ label: `${level}: ${showEntry(entry, REVIEW_OPTION_CHARS)}`
1342
+ }))
1343
+ });
1344
+ } else if (question.type === "noul" && answer.type === "noul") {
1345
+ questions.push({
1346
+ id,
1347
+ type: "boolean",
1348
+ label,
1349
+ required: false,
1350
+ defaultValue: answer.noul >= 0.5,
1351
+ description: `${said} The switch starts on the side the model took.`
1352
+ });
1353
+ }
1354
+ }
1355
+ return {
1356
+ title: review.title ?? `Review decision: ${nodeId}`,
1357
+ questions,
1358
+ approvers: review.approvers,
1359
+ ...review.timeout ? { timeout: review.timeout } : {},
1360
+ ...review.notifications ? { notifications: review.notifications } : {}
1361
+ };
1362
+ }
1363
+ function readHumanAnswer(answer, response) {
1364
+ if (response === undefined || response === null || response === "")
1365
+ return { kind: "confirm" };
1366
+ switch (answer.type) {
1367
+ case "choice":
1368
+ if (typeof response !== "string" || !Object.hasOwn(answer.probabilities, response)) {
1369
+ return { kind: "invalid" };
1370
+ }
1371
+ return response === answer.choice ? { kind: "confirm" } : { kind: "override", answer: { ...answer, choice: response } };
1372
+ case "score": {
1373
+ if (typeof response !== "string" || !Object.hasOwn(answer.legend, response)) {
1374
+ return { kind: "invalid" };
1375
+ }
1376
+ const level = Number(response);
1377
+ return level === answer.score ? { kind: "confirm" } : { kind: "override", answer: { ...answer, score: level } };
1378
+ }
1379
+ case "noul":
1380
+ if (typeof response !== "boolean")
1381
+ return { kind: "invalid" };
1382
+ return response === answer.noul >= 0.5 ? { kind: "confirm" } : { kind: "override", answer: { ...answer, noul: response ? 1 : 0 } };
1383
+ }
1384
+ }
1385
+ function resolveReviewedDecision(parkedOutput, approval) {
1386
+ const parked = SystemOneDecisionOutputSchema.safeParse(parkedOutput);
1387
+ if (!parked.success || parked.data.review?.status !== "pending")
1388
+ return null;
1389
+ const decision = parked.data;
1390
+ const parkedQuestions = decision.review?.questions ?? {};
1391
+ const finish = (status, answers2, questions2, reason) => ({
1392
+ output: {
1393
+ ...decision,
1394
+ answers: answers2,
1395
+ review: {
1396
+ status,
1397
+ approvalRequestId: approval.requestId,
1398
+ ...reason ? { reason } : {},
1399
+ questions: questions2
1400
+ }
1401
+ },
1402
+ nextPort: status
1403
+ });
1404
+ if (approval.status !== "approved") {
1405
+ return finish(approval.status, decision.answers, parkedQuestions);
1406
+ }
1407
+ const answers = { ...decision.answers };
1408
+ const questions = { ...parkedQuestions };
1409
+ for (const [id, info] of Object.entries(parkedQuestions)) {
1410
+ const modelAnswer = decision.answers[id];
1411
+ if (!info.inBand || !modelAnswer)
1412
+ continue;
1413
+ const human = readHumanAnswer(modelAnswer, approval.responses?.[id]);
1414
+ if (human.kind === "invalid") {
1415
+ return finish("rejected", decision.answers, parkedQuestions, `The response for "${id}" is not one of its options`);
1416
+ }
1417
+ if (human.kind === "override")
1418
+ answers[id] = human.answer;
1419
+ questions[id] = { ...info, decidedBy: "human", modelAnswer: modelAnswerOf(modelAnswer) };
1420
+ }
1421
+ return finish("approved", answers, questions);
1422
+ }
1423
+ var REQUEST_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
1424
+ var ERROR_CODE_RE2 = /^[A-Za-z0-9._:-]{1,64}$/;
1425
+ function readRequestId(headers) {
1426
+ const value = headers.get("x-request-id") ?? headers.get("request-id") ?? headers.get("cf-ray");
1427
+ return value && REQUEST_ID_RE.test(value) ? value : undefined;
1428
+ }
1429
+ function parseRetryAfter(value, now) {
1430
+ if (!value)
1431
+ return;
1432
+ const trimmed = value.trim();
1433
+ if (/^\d+(\.\d+)?$/.test(trimmed))
1434
+ return Math.round(Number(trimmed) * 1000);
1435
+ const at = Date.parse(trimmed);
1436
+ return Number.isNaN(at) ? undefined : Math.max(0, at - now);
1437
+ }
1438
+ function readErrorCode(text) {
1439
+ if (text.length === 0 || text.length > MAX_ERROR_BODY_CHARS)
1440
+ return;
1441
+ let parsed;
1442
+ try {
1443
+ parsed = JSON.parse(text);
1444
+ } catch {
1445
+ return;
1446
+ }
1447
+ if (!isRecord2(parsed))
1448
+ return;
1449
+ const nested = isRecord2(parsed.error) ? parsed.error : undefined;
1450
+ const detail = isRecord2(parsed.detail) ? parsed.detail : undefined;
1451
+ for (const candidate of [
1452
+ nested?.code,
1453
+ nested?.type,
1454
+ detail?.error_type,
1455
+ parsed.code,
1456
+ parsed.type
1457
+ ]) {
1458
+ if (typeof candidate === "string" && ERROR_CODE_RE2.test(candidate))
1459
+ return candidate;
1460
+ }
1461
+ return cloudflareErrorCode(parsed);
1462
+ }
1463
+ function defaultSleep(ms, signal) {
1464
+ return new Promise((resolve) => {
1465
+ if (signal.aborted)
1466
+ return resolve();
1467
+ const timer = setTimeout(done, ms);
1468
+ function done() {
1469
+ clearTimeout(timer);
1470
+ signal.removeEventListener("abort", done);
1471
+ resolve();
1472
+ }
1473
+ signal.addEventListener("abort", done, { once: true });
1474
+ });
1475
+ }
1476
+
1477
+ class SystemOneDecisionExecutor extends BaseExecutor {
1478
+ options;
1479
+ type = SYSTEM_ONE_DECISION_NODE_TYPE;
1480
+ mode = "async";
1481
+ configSchema = SystemOneDecisionConfigSchema;
1482
+ outputSchema = SystemOneDecisionOutputSchema;
1483
+ constructor(deps, options = {}) {
1484
+ super(deps);
1485
+ this.options = options;
1486
+ }
1487
+ providerContext() {
1488
+ return { db: this.deps.db, env: this.options.env ?? process.env };
1489
+ }
1490
+ resolveTarget(id, model) {
1491
+ return resolveSystemOneTarget(id, this.providerContext(), {
1492
+ apiKey: this.options.getApiKey,
1493
+ serverUrl: this.options.getServerUrl,
1494
+ model
1495
+ });
1496
+ }
1497
+ async checkReadiness(nodes) {
1498
+ const nodeIdsByProvider = new Map;
1499
+ for (const node of nodes) {
1500
+ const id = systemOneProviderOf(node.config);
1501
+ if (id)
1502
+ nodeIdsByProvider.set(id, [...nodeIdsByProvider.get(id) ?? [], node.id]);
1503
+ }
1504
+ const problems = [];
1505
+ for (const [id, nodeIds] of nodeIdsByProvider) {
1506
+ const target = await this.resolveTarget(id);
1507
+ if (!target.ok)
1508
+ problems.push({ nodeIds, message: target.error });
1509
+ }
1510
+ return problems;
1511
+ }
1512
+ async execute(config, context, meta) {
1513
+ if (config.humanReview) {
1514
+ const existing = await this.deps.db.getApprovalRequestByStepId(meta.stepId);
1515
+ if (existing) {
1516
+ return this.handOffToReviewer(config, await this.readParked(meta.stepId), context, meta);
1517
+ }
1518
+ }
1519
+ const providerId = config.provider;
1520
+ const provider = SYSTEM_ONE_PROVIDERS[providerId];
1521
+ const model = config.model ?? provider.defaultModel;
1522
+ const modelProblem = systemOneModelProblem(providerId, model);
1523
+ if (modelProblem)
1524
+ return { status: "failed", error: modelProblem };
1525
+ const target = await this.resolveTarget(providerId, model);
1526
+ if (!target.ok)
1527
+ return { status: "failed", error: target.error };
1528
+ const { apiKey, endpoint } = target;
1529
+ const wire = provider.wire ?? SYSTEM_ONE_WIRE;
1530
+ const body = JSON.stringify(wire.buildRequest(config, model));
1531
+ const scrub = (message) => message.split(apiKey).join("[REDACTED]");
1532
+ const sent = await this.send(providerId, endpoint, apiKey, body, config.timeoutMs, config.maxRetries);
1533
+ if (!sent.ok)
1534
+ return { status: "failed", error: scrub(sent.error) };
1535
+ let parsed;
1536
+ try {
1537
+ parsed = JSON.parse(sent.text);
1538
+ } catch {
1539
+ return { status: "failed", error: "SystemOne response was not valid JSON" };
1540
+ }
1541
+ let decision;
1542
+ try {
1543
+ decision = validateSystemOneResponse(config.questions, wire.toSystemOne(parsed, { questions: config.questions, model }), sent.requestId, provider.confidenceField);
1544
+ } catch (err) {
1545
+ if (err instanceof SystemOneWireError) {
1546
+ const withId = sent.requestId ? `${err.message} [request ${sent.requestId}]` : err.message;
1547
+ return { status: "failed", error: scrub(withId) };
1548
+ }
1549
+ if (err instanceof SystemOneContractError) {
1550
+ return {
1551
+ status: "failed",
1552
+ error: scrub(`SystemOne response failed validation: ${err.message}`)
1553
+ };
1554
+ }
1555
+ throw err;
1556
+ }
1557
+ if (!config.humanReview)
1558
+ return { status: "success", output: decision };
1559
+ return this.reviewDecision(config, config.humanReview, decision, context, meta);
1560
+ }
1561
+ async reviewDecision(config, review, decision, context, meta) {
1562
+ const questions = {};
1563
+ for (const [id, answer] of Object.entries(decision.answers)) {
1564
+ const confidence = answerConfidence(answer);
1565
+ questions[id] = {
1566
+ confidence,
1567
+ inBand: confidence >= review.band.min && confidence <= review.band.max,
1568
+ decidedBy: "model"
1569
+ };
1570
+ }
1571
+ if (!Object.values(questions).some((question) => question.inBand)) {
1572
+ return {
1573
+ status: "success",
1574
+ output: { ...decision, review: { status: "not_required", questions } },
1575
+ nextPort: "approved"
1576
+ };
1577
+ }
1578
+ const parked = {
1579
+ ...decision,
1580
+ review: { status: "pending", questions }
1581
+ };
1582
+ await this.deps.db.updateWorkflowRunStep(meta.stepId, { output: parked });
1583
+ return this.handOffToReviewer(config, parked, context, meta);
1584
+ }
1585
+ async readParked(stepId) {
1586
+ const step = await this.deps.db.getWorkflowRunStep(stepId);
1587
+ const parked = SystemOneDecisionOutputSchema.safeParse(step?.output);
1588
+ return parked.success && parked.data.review?.status === "pending" ? parked.data : null;
1589
+ }
1590
+ async handOffToReviewer(config, parked, context, meta) {
1591
+ if (!parked || !config.humanReview) {
1592
+ return {
1593
+ status: "failed",
1594
+ error: "The decision this step parked with is missing, so its approval cannot be resumed. Run the node again."
1595
+ };
1596
+ }
1597
+ const result = await new HumanInTheLoopExecutor(this.deps).run({
1598
+ config: buildReviewCard(meta.nodeId, config, config.humanReview, parked),
1599
+ context,
1600
+ meta
1601
+ });
1602
+ if (result.status === "failed")
1603
+ return { status: "failed", error: result.error };
1604
+ if ("async" in result)
1605
+ return result;
1606
+ const answered = result.output;
1607
+ const outcome = answered ? this.resolveApproval(parked, {
1608
+ requestId: answered.requestId,
1609
+ status: answered.status,
1610
+ responses: answered.responses
1611
+ }) : null;
1612
+ if (!outcome) {
1613
+ return {
1614
+ status: "failed",
1615
+ error: "The approval was answered but its result could not be applied"
1616
+ };
1617
+ }
1618
+ return {
1619
+ status: "success",
1620
+ output: outcome.output,
1621
+ nextPort: outcome.nextPort
1622
+ };
1623
+ }
1624
+ resolveApproval(parkedOutput, approval) {
1625
+ return resolveReviewedDecision(parkedOutput, approval);
1626
+ }
1627
+ async send(providerId, endpoint, apiKey, body, timeoutMs, maxRetries) {
1628
+ const doFetch = this.options.fetch ?? fetch;
1629
+ const sleep = this.options.sleep ?? defaultSleep;
1630
+ const now = this.options.now ?? Date.now;
1631
+ const random = this.options.random ?? Math.random;
1632
+ const deadlineAt = now() + timeoutMs - DEADLINE_MARGIN_MS;
1633
+ const controller = new AbortController;
1634
+ const timer = setTimeout(() => controller.abort(), Math.max(0, deadlineAt - now()));
1635
+ const timedOut = () => ({
1636
+ ok: false,
1637
+ error: `SystemOne request exceeded its ${timeoutMs}ms time budget`
1638
+ });
1639
+ try {
1640
+ for (let attempt = 0;; attempt++) {
1641
+ const result = await this.attemptOnce(doFetch, providerId, endpoint, apiKey, body, controller.signal, now);
1642
+ if (result.kind === "ok") {
1643
+ return { ok: true, text: result.text, requestId: result.requestId };
1644
+ }
1645
+ if (result.kind === "timeout" || controller.signal.aborted)
1646
+ return timedOut();
1647
+ const attempts = attempt + 1;
1648
+ const suffix = attempts > 1 ? ` after ${attempts} attempts` : "";
1649
+ if (!result.retryable || attempt >= maxRetries) {
1650
+ return { ok: false, error: `${result.message}${suffix}` };
1651
+ }
1652
+ const backoff = Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** attempt);
1653
+ const delay = result.retryAfterMs ?? Math.round(backoff * (0.5 + random() * 0.5));
1654
+ if (delay >= deadlineAt - now()) {
1655
+ return {
1656
+ ok: false,
1657
+ error: `${result.message}${suffix}; the ${delay}ms retry delay does not fit the ${timeoutMs}ms time budget`
1658
+ };
1659
+ }
1660
+ await sleep(delay, controller.signal);
1661
+ if (controller.signal.aborted)
1662
+ return timedOut();
1663
+ }
1664
+ } finally {
1665
+ clearTimeout(timer);
1666
+ }
1667
+ }
1668
+ async attemptOnce(doFetch, providerId, endpoint, apiKey, body, signal, now) {
1669
+ let res;
1670
+ try {
1671
+ res = await doFetch(endpoint, {
1672
+ method: "POST",
1673
+ headers: {
1674
+ ...getOpenRouterAttributionHeaders(endpoint, this.providerContext().env),
1675
+ "content-type": "application/json",
1676
+ accept: "application/json",
1677
+ authorization: `Bearer ${apiKey}`
1678
+ },
1679
+ body,
1680
+ redirect: "manual",
1681
+ signal
1682
+ });
1683
+ } catch (err) {
1684
+ if (signal.aborted)
1685
+ return { kind: "timeout" };
1686
+ const code2 = err?.code;
1687
+ const detail2 = typeof code2 === "string" && /^[A-Z0-9_]{3,40}$/.test(code2) ? ` (${code2})` : "";
1688
+ return {
1689
+ kind: "fail",
1690
+ message: `SystemOne request failed: network error${detail2}`,
1691
+ retryable: true
1692
+ };
1693
+ }
1694
+ const requestId = readRequestId(res.headers);
1695
+ const withId = (message) => requestId ? `${message} [request ${requestId}]` : message;
1696
+ if (res.status >= 200 && res.status < 300) {
1697
+ let text;
1698
+ try {
1699
+ text = await res.text();
1700
+ } catch {
1701
+ if (signal.aborted)
1702
+ return { kind: "timeout" };
1703
+ return {
1704
+ kind: "fail",
1705
+ message: withId("SystemOne response body could not be read"),
1706
+ retryable: true
1707
+ };
1708
+ }
1709
+ if (text.length > MAX_RESPONSE_CHARS) {
1710
+ return {
1711
+ kind: "fail",
1712
+ message: withId("SystemOne response exceeded the size limit"),
1713
+ retryable: false
1714
+ };
1715
+ }
1716
+ return { kind: "ok", text, requestId };
1717
+ }
1718
+ if (res.status >= 300 && res.status < 400) {
1719
+ return {
1720
+ kind: "fail",
1721
+ message: withId(`SystemOne request was redirected (HTTP ${res.status}); redirects are refused`),
1722
+ retryable: false
1723
+ };
1724
+ }
1725
+ let errorText = "";
1726
+ try {
1727
+ errorText = await res.text();
1728
+ } catch {
1729
+ if (signal.aborted)
1730
+ return { kind: "timeout" };
1731
+ }
1732
+ const code = readErrorCode(errorText);
1733
+ if (res.status === 401 || res.status === 403) {
1734
+ return {
1735
+ kind: "fail",
1736
+ message: withId(systemOneKeyRejectedMessage(providerId, res.status, code)),
1737
+ retryable: false
1738
+ };
1739
+ }
1740
+ const detail = code ? ` (${code})` : "";
1741
+ const retryable = res.status === 408 || res.status === 429 || res.status >= 500;
1742
+ return {
1743
+ kind: "fail",
1744
+ message: withId(`SystemOne API returned HTTP ${res.status}${detail}`),
1745
+ retryable,
1746
+ retryAfterMs: retryable ? parseRetryAfter(res.headers.get("retry-after"), now()) : undefined
1747
+ };
1748
+ }
1749
+ }
1750
+
1751
+ // src/workflows/definition.ts
1752
+ function getNextTargets(next) {
1753
+ if (typeof next === "string")
1754
+ return [next];
1755
+ if (Array.isArray(next))
1756
+ return next;
1757
+ return Object.values(next);
1758
+ }
1759
+ function generateEdges(def) {
1760
+ const edges = [];
1761
+ for (const node of def.nodes) {
1762
+ if (!node.next)
1763
+ continue;
1764
+ if (typeof node.next === "string") {
1765
+ edges.push({
1766
+ id: `${node.id}→${node.next}`,
1767
+ source: node.id,
1768
+ target: node.next,
1769
+ sourcePort: "default"
1770
+ });
1771
+ } else if (Array.isArray(node.next)) {
1772
+ for (const targetId of node.next) {
1773
+ edges.push({
1774
+ id: `${node.id}→${targetId}`,
1775
+ source: node.id,
1776
+ target: targetId,
1777
+ sourcePort: "default"
1778
+ });
1779
+ }
1780
+ } else {
1781
+ for (const [port, targetId] of Object.entries(node.next)) {
1782
+ edges.push({
1783
+ id: `${node.id}→${targetId}:${port}`,
1784
+ source: node.id,
1785
+ target: targetId,
1786
+ sourcePort: port
1787
+ });
1788
+ }
1789
+ }
1790
+ }
1791
+ return edges;
1792
+ }
1793
+ function findEntryNodes(def) {
1794
+ const targets = new Set;
1795
+ for (const node of def.nodes) {
1796
+ if (!node.next)
1797
+ continue;
1798
+ for (const targetId of getNextTargets(node.next)) {
1799
+ targets.add(targetId);
1800
+ }
1801
+ }
1802
+ return def.nodes.filter((n) => !targets.has(n.id));
1803
+ }
1804
+ function resolveValidationPort(next, executorPort, validationPassed) {
1805
+ if (validationPassed === undefined)
1806
+ return executorPort;
1807
+ if (!next || typeof next !== "object" || Array.isArray(next))
1808
+ return executorPort;
1809
+ if (executorPort && Object.hasOwn(next, executorPort))
1810
+ return executorPort;
1811
+ return validationPassed ? "pass" : "fail";
1812
+ }
1813
+ function getSuccessors(def, nodeId, port) {
1814
+ const node = def.nodes.find((n) => n.id === nodeId);
1815
+ if (!node?.next)
1816
+ return [];
1817
+ const targetIds = [];
1818
+ if (typeof node.next === "string") {
1819
+ targetIds.push(node.next);
1820
+ } else if (Array.isArray(node.next)) {
1821
+ targetIds.push(...node.next);
1822
+ } else {
1823
+ if (port) {
1824
+ const targetId = node.next[port];
1825
+ if (targetId)
1826
+ targetIds.push(targetId);
1827
+ } else {
1828
+ targetIds.push(...Object.values(node.next));
1829
+ }
1830
+ }
1831
+ return targetIds.map((id) => def.nodes.find((n) => n.id === id)).filter((n) => n != null);
1832
+ }
1833
+ function isUpstream(def, sourceId, targetId) {
1834
+ const reverseDeps = new Map;
1835
+ for (const node of def.nodes) {
1836
+ if (!node.next)
1837
+ continue;
1838
+ for (const target of getNextTargets(node.next)) {
1839
+ if (!reverseDeps.has(target))
1840
+ reverseDeps.set(target, []);
1841
+ reverseDeps.get(target).push(node.id);
1842
+ }
1843
+ }
1844
+ const visited = new Set;
1845
+ const queue = [targetId];
1846
+ while (queue.length > 0) {
1847
+ const current = queue.shift();
1848
+ const preds = reverseDeps.get(current) || [];
1849
+ for (const pred of preds) {
1850
+ if (visited.has(pred))
1851
+ continue;
1852
+ visited.add(pred);
1853
+ if (pred === sourceId)
1854
+ return true;
1855
+ queue.push(pred);
1856
+ }
1857
+ }
1858
+ return false;
1859
+ }
1860
+ function containsInterpolationToken(value) {
1861
+ if (typeof value === "string")
1862
+ return value.includes("{{") && value.includes("}}");
1863
+ if (Array.isArray(value))
1864
+ return value.some(containsInterpolationToken);
1865
+ if (value && typeof value === "object") {
1866
+ return Object.values(value).some(containsInterpolationToken);
1867
+ }
1868
+ return false;
1869
+ }
1870
+ function definitionNodeIds(def) {
1871
+ return new Set(def.nodes.map((node) => node.id));
1872
+ }
1873
+ function validateDefinition(def, registry, options = {}) {
1874
+ const errors = [];
1875
+ const nodeIds = new Set(def.nodes.map((n) => n.id));
1876
+ for (const node of def.nodes) {
1877
+ if (node.id.includes("#") && !options.legacyNodeIds?.has(node.id)) {
1878
+ errors.push(`Node "${node.id}" contains reserved character "#"`);
1879
+ }
1880
+ if (node.type === "human-in-the-loop" && typeof node.config.questions === "string" && !/^\{\{[^}]+\}\}$/.test(node.config.questions.trim())) {
1881
+ errors.push(`Node "${node.id}" human-in-the-loop config.questions must be an array or one exact {{interpolation}} token`);
1882
+ }
1883
+ errors.push(...systemOneStaticShapeViolations(node), ...systemOneRetryViolations(node));
1884
+ if (node.type === "foreach") {
1885
+ validateForeachNode(node, errors);
1886
+ if (node.id.includes("#")) {
1887
+ errors.push(`Node "${node.id}" cannot be a foreach: its id contains "#", which is reserved for synthetic child ids`);
1888
+ }
1889
+ if (isUpstream(def, node.id, node.id)) {
1890
+ errors.push("foreach inside a loop is not supported in v1");
1891
+ }
1892
+ const collidingSibling = def.nodes.find((other) => other.id !== node.id && other.id.startsWith(`${node.id}#`));
1893
+ if (collidingSibling) {
1894
+ errors.push(`Node "${node.id}" cannot be a foreach: node "${collidingSibling.id}" collides with its synthetic child id space ("${node.id}#…")`);
1895
+ }
1896
+ }
1897
+ }
1898
+ for (const node of def.nodes) {
1899
+ if (!node.next)
1900
+ continue;
1901
+ if (typeof node.next === "string") {
1902
+ if (!nodeIds.has(node.next)) {
1903
+ errors.push(`Node "${node.id}" references non-existent next target "${node.next}"`);
1904
+ }
1905
+ } else if (Array.isArray(node.next)) {
1906
+ for (const targetId of node.next) {
1907
+ if (!nodeIds.has(targetId)) {
1908
+ errors.push(`Node "${node.id}" fan-out references non-existent target "${targetId}"`);
1909
+ }
1910
+ }
1911
+ } else {
1912
+ for (const [port, targetId] of Object.entries(node.next)) {
1913
+ if (!nodeIds.has(targetId)) {
1914
+ errors.push(`Node "${node.id}" port "${port}" references non-existent target "${targetId}"`);
1915
+ }
1916
+ }
1917
+ }
1918
+ }
1919
+ const entryNodes = findEntryNodes(def);
1920
+ if (entryNodes.length === 0) {
1921
+ errors.push("No entry node found (every node is a target of some other node)");
1922
+ } else if (entryNodes.length > 1) {
1923
+ const ids = entryNodes.map((n) => `"${n.id}"`).join(", ");
1924
+ errors.push(`Multiple entry nodes found: ${ids} (expected exactly one)`);
1925
+ }
1926
+ if (entryNodes.length === 1) {
1927
+ const reachable = new Set;
1928
+ const queue = [entryNodes[0].id];
1929
+ while (queue.length > 0) {
1930
+ const current = queue.shift();
1931
+ if (reachable.has(current))
1932
+ continue;
1933
+ reachable.add(current);
1934
+ const node = def.nodes.find((n) => n.id === current);
1935
+ if (!node?.next)
1936
+ continue;
1937
+ for (const targetId of getNextTargets(node.next)) {
1938
+ queue.push(targetId);
1939
+ }
1940
+ }
1941
+ for (const node of def.nodes) {
1942
+ if (!reachable.has(node.id)) {
1943
+ errors.push(`Node "${node.id}" is unreachable from the entry node`);
1944
+ }
1945
+ }
1946
+ }
1947
+ if (registry) {
1948
+ for (const node of def.nodes) {
1949
+ if (!registry.has(node.type)) {
1950
+ errors.push(`Node "${node.id}" uses unregistered executor type "${node.type}"`);
1951
+ continue;
1952
+ }
1953
+ reportStaticConfigIssues(node, registry.get(node.type).configSchema, node.config, errors);
1954
+ if (node.type === "foreach" && registry.has("agent-task")) {
1955
+ const body = node.config.body;
1956
+ const bodyConfig = typeof body === "object" && body !== null && !Array.isArray(body) ? body.config : undefined;
1957
+ if (typeof bodyConfig === "object" && bodyConfig !== null && !Array.isArray(bodyConfig)) {
1958
+ reportStaticConfigIssues(node, registry.get("agent-task").configSchema, bodyConfig, errors, "config.body.config");
1959
+ }
1960
+ }
1961
+ }
1962
+ }
1963
+ for (const node of def.nodes) {
1964
+ if (!node.inputs)
1965
+ continue;
1966
+ for (const [localName, sourcePath] of Object.entries(node.inputs)) {
1967
+ const [sourceNodeId] = sourcePath.split(".");
1968
+ if (!sourceNodeId)
1969
+ continue;
1970
+ if (sourceNodeId === "trigger" || sourceNodeId === "input" || sourceNodeId === "run") {
1971
+ continue;
1972
+ }
1973
+ if (!nodeIds.has(sourceNodeId)) {
1974
+ errors.push(`Node "${node.id}" input "${localName}" references non-existent node "${sourceNodeId}"`);
1975
+ continue;
1976
+ }
1977
+ if (!isUpstream(def, sourceNodeId, node.id)) {
1978
+ errors.push(`Node "${node.id}" input "${localName}" references "${sourceNodeId}" which is not upstream`);
1979
+ }
1980
+ }
1981
+ }
1982
+ return { valid: errors.length === 0, errors };
1983
+ }
1984
+ function reportStaticConfigIssues(node, schema, config, errors, pathPrefix = "config") {
1985
+ const result = schema.safeParse(config);
1986
+ if (result.success)
1987
+ return;
1988
+ for (const rawIssue of result.error?.issues ?? []) {
1989
+ const issue = rawIssue;
1990
+ const issuePath = issue.path.map(String);
1991
+ const path = [pathPrefix, ...issuePath].join(".");
1992
+ let value = config;
1993
+ for (const segment of issue.path) {
1994
+ if (value === null || typeof value !== "object") {
1995
+ value = undefined;
1996
+ break;
1997
+ }
1998
+ value = value[segment];
1999
+ }
2000
+ if (containsInterpolationToken(value))
2001
+ continue;
2002
+ const renderedValue = value === undefined ? "undefined" : JSON.stringify(value);
2003
+ errors.push(`Node "${node.id}" (${node.type}) ${path} has invalid value ${renderedValue}: ${issue.message}`);
2004
+ }
2005
+ }
2006
+ function validateForeachNode(node, errors) {
2007
+ const config = node.config;
2008
+ if (Object.hasOwn(config, "concurrency")) {
2009
+ errors.push(`Node "${node.id}" foreach concurrency is not supported in v1`);
2010
+ }
2011
+ if (node.outputSchema !== undefined || node.validation !== undefined) {
2012
+ errors.push(`Node "${node.id}" foreach node-level outputSchema/validation is not supported in v1`);
2013
+ }
2014
+ const over = config.over;
2015
+ const isInterpolatedArray = typeof over === "string" && /^\{\{[^}]+\}\}$/.test(over.trim());
2016
+ if (!Array.isArray(over) && !isInterpolatedArray) {
2017
+ errors.push(`Node "${node.id}" foreach config.over must be an array or one exact {{interpolation}} token`);
2018
+ }
2019
+ if (typeof config.itemKey !== "string" || config.itemKey.length === 0) {
2020
+ errors.push(`Node "${node.id}" foreach config.itemKey must be a non-empty string`);
2021
+ }
2022
+ const body = config.body;
2023
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
2024
+ errors.push(`Node "${node.id}" foreach config.body must be an object`);
2025
+ return;
2026
+ }
2027
+ const bodyRecord = body;
2028
+ if (bodyRecord.type !== "agent-task") {
2029
+ errors.push(`Node "${node.id}" foreach config.body.type must be "agent-task" in v1`);
2030
+ }
2031
+ if (typeof bodyRecord.config !== "object" || bodyRecord.config === null || Array.isArray(bodyRecord.config)) {
2032
+ errors.push(`Node "${node.id}" foreach config.body.config must be an object`);
2033
+ return;
2034
+ }
2035
+ const bodyConfig = bodyRecord.config;
2036
+ if (typeof bodyConfig.template !== "string") {
2037
+ errors.push(`Node "${node.id}" foreach agent-task body.config.template must be a string`);
2038
+ }
2039
+ }
2040
+ function applyDefinitionPatch(def, patch) {
2041
+ const errors = [];
2042
+ let nodes = [...def.nodes];
2043
+ if (patch.delete?.length) {
2044
+ const missing = patch.delete.filter((id) => !nodes.some((n) => n.id === id));
2045
+ if (missing.length > 0) {
2046
+ errors.push(`Cannot delete non-existent nodes: ${missing.join(", ")}`);
2047
+ }
2048
+ const toDelete = new Set(patch.delete);
2049
+ nodes = nodes.filter((n) => !toDelete.has(n.id));
2050
+ }
2051
+ if (patch.create?.length) {
2052
+ const existingIds = new Set(nodes.map((n) => n.id));
2053
+ for (const newNode of patch.create) {
2054
+ if (existingIds.has(newNode.id)) {
2055
+ errors.push(`Cannot create node with duplicate ID: "${newNode.id}"`);
2056
+ continue;
2057
+ }
2058
+ nodes.push(newNode);
2059
+ existingIds.add(newNode.id);
2060
+ }
2061
+ }
2062
+ if (patch.update?.length) {
2063
+ for (const { nodeId, node: partial } of patch.update) {
2064
+ const idx = nodes.findIndex((n) => n.id === nodeId);
2065
+ if (idx === -1) {
2066
+ errors.push(`Cannot update non-existent node: "${nodeId}"`);
2067
+ continue;
2068
+ }
2069
+ const defined = {};
2070
+ for (const [k, v] of Object.entries(partial)) {
2071
+ if (v !== undefined)
2072
+ defined[k] = v;
2073
+ }
2074
+ nodes[idx] = { ...nodes[idx], ...defined, id: nodeId };
2075
+ }
2076
+ }
2077
+ const patchedDef = { ...def, nodes };
2078
+ if (patch.onNodeFailure !== undefined) {
2079
+ patchedDef.onNodeFailure = patch.onNodeFailure;
2080
+ }
2081
+ return { definition: patchedDef, errors };
2082
+ }
2083
+
2084
+ export { BaseExecutor, postApprovalCancellationUpdates, createApprovalFollowUpTask, HumanInTheLoopExecutor, resolveWorkflowLlmConfig, SYSTEM_ONE_DECISION_NODE_TYPE, systemOneRetryViolations, systemOneUnresolvedError, SystemOneDecisionExecutor, getNextTargets, generateEdges, findEntryNodes, resolveValidationPort, getSuccessors, definitionNodeIds, validateDefinition, applyDefinitionPatch };