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