@desplega.ai/agent-swarm 2.4.3 → 2.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{acp-adapter-1885dpr7.js → acp-adapter-thq04adh.js} +1 -2
- package/dist/{actions-pn76n32q.js → actions-j6ywh6x5.js} +17 -17
- package/dist/{amp-adapter-af84rx1d.js → amp-adapter-k8b5xmj8.js} +2 -2
- package/dist/{app-6sv8mhzm.js → app-rb7vc02s.js} +3 -3
- package/dist/{assistant-sc2tf825.js → assistant-ftta1zjf.js} +18 -18
- package/dist/{boot-reembed-y3m0ydzj.js → boot-reembed-srp6v6af.js} +1 -1
- package/dist/{boot-reembed-gvxjs2py.js → boot-reembed-xns7h5h9.js} +1 -1
- package/dist/{boot-scrub-sweep-1j9mk05f.js → boot-scrub-sweep-3qqrh2mz.js} +2 -2
- package/dist/{boot-seeds-37sp626b.js → boot-seeds-9ks8hce7.js} +4 -4
- package/dist/{claude-adapter-rp0jtnwr.js → claude-adapter-k5ksyres.js} +2 -2
- package/dist/{claude-managed-setup-rdt248k7.js → claude-managed-setup-n5a5eqks.js} +1 -1
- package/dist/{claude-sdk-session-53rcw5we.js → claude-sdk-session-2j3j4kwn.js} +2 -2
- package/dist/{cli-v87dr6e3.js → cli-1d70crsy.js} +1 -1
- package/dist/{cli-1ma335fz.js → cli-1ephgwnq.js} +2 -2
- package/dist/{cli-4yef8ghv.js → cli-2v045xyf.js} +3 -3
- package/dist/{cli-73ezfhha.js → cli-2y91wnbh.js} +10 -5
- package/dist/{cli-1eszr343.js → cli-2zqpkvtt.js} +1 -1
- package/dist/{cli-e4d78xsv.js → cli-37yb7307.js} +1 -1
- package/dist/{cli-8y99kfey.js → cli-3cgngyd8.js} +9 -9
- package/dist/{cli-wk28hzqn.js → cli-44159k28.js} +1 -1
- package/dist/{cli-qp2m4btv.js → cli-4dg55wsk.js} +2 -2
- package/dist/{cli-0dxxwfkt.js → cli-51y09c6p.js} +17 -14
- package/dist/{cli-aa3tb29r.js → cli-55qmbp6z.js} +1 -1
- package/dist/{cli-mzkwkbdy.js → cli-82d3nkc3.js} +1 -1
- package/dist/{cli-sds5zzs8.js → cli-9fhkkpj5.js} +78 -12
- package/dist/{cli-vq9gqarq.js → cli-amyhm3gj.js} +1 -1
- package/dist/{cli-bxrqvpg5.js → cli-b8k7epj1.js} +5 -5
- package/dist/{cli-8vverzf7.js → cli-ba8ne1es.js} +5 -0
- package/dist/{cli-58vwn100.js → cli-bnz0r3kc.js} +1 -1
- package/dist/{cli-6eyncg0y.js → cli-cynvc9fj.js} +34 -6
- package/dist/{cli-f0p0f6ja.js → cli-dqb4n629.js} +7 -7
- package/dist/{cli-pygdsgh8.js → cli-esxerzg7.js} +1 -1
- package/dist/{cli-dm8p2dpf.js → cli-gqvh8d8a.js} +6 -6
- package/dist/{cli-p0qerd2r.js → cli-hj3debgw.js} +1 -1
- package/dist/{cli-836ehxmt.js → cli-nc35pxb7.js} +2 -2
- package/dist/{cli-vcgscaan.js → cli-p20r8w42.js} +8 -6
- package/dist/{cli-1xsraagq.js → cli-pccfmtb5.js} +4 -4
- package/dist/{cli-s9rr4gr2.js → cli-q6r7t299.js} +1036 -298
- package/dist/{cli-tkf4x6ha.js → cli-qkhc5aqy.js} +11 -11
- package/dist/{cli-1n9gdg56.js → cli-t7cyef5g.js} +4 -4
- package/dist/{cli-038fga5k.js → cli-te6b5wvg.js} +148 -183
- package/dist/{cli-kgnkyabk.js → cli-v3yr2gbg.js} +109 -35
- package/dist/{cli-h0mgxann.js → cli-vdm20wej.js} +2 -2
- package/dist/{cli-zvbb4n0e.js → cli-vq97g1jq.js} +2 -2
- package/dist/{cli-gx0j2khs.js → cli-w6nadkh4.js} +3 -3
- package/dist/{cli-99jez72r.js → cli-w9etnnxk.js} +6 -1
- package/dist/{cli-npfe9t1s.js → cli-xc27fwzg.js} +2 -2
- package/dist/{cli-6aen3ezb.js → cli-xj0re5tm.js} +1 -1
- package/dist/{cli-aypbg9r2.js → cli-zrk6jxv8.js} +1 -1
- package/dist/{cli-1012xb7j.js → cli-zrwpctp4.js} +20 -20
- package/dist/cli.js +41 -16
- package/dist/{codex-adapter-2zrmab9b.js → codex-adapter-968xwfy7.js} +1 -1
- package/dist/{codex-session-runner-kbkz62vc.js → codex-session-runner-ht4z99fa.js} +1 -1
- package/dist/{commands-hd574th6.js → commands-pcymaeg9.js} +2 -2
- package/dist/{controller-pz5mcfg4.js → controller-fpjyk46r.js} +2 -2
- package/dist/{controllers-connect-5rgq2hfk.js → controllers-connect-mwzx1fh6.js} +18 -18
- package/dist/{cursor-adapter-mgaprq0j.js → cursor-adapter-cp4s2e2f.js} +1 -1
- package/dist/{db-h06dxp05.js → db-sgd12bt9.js} +3 -3
- package/dist/{db-6nf1207q.js → db-xsjg9jqq.js} +1 -1
- package/dist/{dispatcher-zthpy4m1.js → dispatcher-x0jfw2c9.js} +16 -16
- package/dist/{dsh-adapter-64dhw0j7.js → dsh-adapter-jrq7ma7h.js} +1 -1
- package/dist/{external-channel-x01t7ke6.js → external-channel-8njj4h3b.js} +2 -2
- package/dist/{grok-adapter-h21jv594.js → grok-adapter-kkr97v2f.js} +2 -3
- package/dist/handlers-0pvee9nw.js +39 -0
- package/dist/{heartbeat-zzfksckz.js → heartbeat-wd03zhn0.js} +19 -19
- package/dist/{hook-z4zhzj1f.js → hook-f0vghj42.js} +1 -1
- package/dist/{hook-3xfqhh2s.js → hook-xzj2ts2p.js} +1 -1
- package/dist/{http-q813tmya.js → http-4hwxv62p.js} +255 -117
- package/dist/{index-va3y63es.js → index-6srwvd5b.js} +1 -2
- package/dist/{index-220tk9bw.js → index-97q4xn03.js} +19 -10
- package/dist/{index-s98037v0.js → index-cbxkmfc5.js} +1 -2
- package/dist/{index-m0a4ydqj.js → index-gdh8jrdm.js} +16 -16
- package/dist/{index-wbbs14c5.js → index-hg0x2cdv.js} +16 -16
- package/dist/{index-d0g18bax.js → index-thm0y6a8.js} +19 -19
- package/dist/{index-x8xbyn1t.js → index-ww2vznkp.js} +1 -1
- package/dist/{keepalive-y5t1my7m.js → keepalive-hrh7xnr9.js} +3 -3
- package/dist/{lead-2xwtq66b.js → lead-ee8vazz2.js} +26 -27
- package/dist/{maintenance-ybfsaedv.js → maintenance-e8wnwnqx.js} +1 -1
- package/dist/{oauth-refresh-sweep-909jmn12.js → oauth-refresh-sweep-zm0yj4sj.js} +2 -2
- package/dist/{onboard-xn6fkkys.js → onboard-dtwcq603.js} +2 -2
- package/dist/{otel-impl-e6mn0z13.js → otel-impl-w3fbhpn0.js} +1 -1
- package/dist/{pricing-refresh-ygd362ep.js → pricing-refresh-j38dehfs.js} +3 -3
- package/dist/{rbac-roles-c9axvfnz.js → rbac-roles-ae8yttq4.js} +2 -2
- package/dist/{rbac-roles-mb470p4e.js → rbac-roles-ejxx9b35.js} +2 -2
- package/dist/{render-v2-g0306nh1.js → render-v2-n16n0phe.js} +3 -3
- package/dist/{resume-v315hnq6.js → resume-z0fgjc38.js} +5 -5
- package/dist/{session-access-2zg285a1.js → session-access-cv3mq6mc.js} +6 -4
- package/dist/{setup-a4mtf02k.js → setup-mafhj3yt.js} +1 -1
- package/dist/{work-objects-9fr0fgtn.js → work-objects-8b86mgjp.js} +19 -19
- package/dist/{worker-qae22jeb.js → worker-16s75hm5.js} +26 -27
- package/dist/worker-token-xf11yyds.js +136 -0
- package/openapi.json +295 -12
- package/package.json +4 -4
- package/src/apps/action-runner.ts +4 -0
- package/src/apps/catalog.generated.json +3 -0
- package/src/apps/page-validator.ts +8 -2
- package/src/be/migrations/232_agent_session_tokens.sql +41 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +10 -1
- package/src/be/swarm-config-guard.ts +16 -0
- package/src/be/users.ts +169 -12
- package/src/cli.tsx +24 -0
- package/src/commands/worker-token.ts +164 -0
- package/src/http/apps.ts +43 -5
- package/src/http/codex-oauth-keep-warm.ts +27 -9
- package/src/http/config.ts +5 -3
- package/src/http/core.ts +2 -0
- package/src/http/session-access.ts +51 -2
- package/src/http/sessions.ts +249 -10
- package/src/http/tasks.ts +4 -32
- package/src/providers/acp-adapter.ts +33 -13
- package/src/providers/acp-swarm-events.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-oauth/storage.ts +1 -1
- package/src/rbac/legacy-policy.ts +3 -1
- package/src/rbac/permissions.ts +4 -0
- package/src/tests/acp-adapter.test.ts +10 -23
- package/src/tests/acp-fake-agent.ts +36 -0
- package/src/tests/acp-swarm-events.test.ts +1 -1
- package/src/tests/agent-session-tokens.test.ts +452 -0
- package/src/tests/apps-action-parent-task.test.ts +295 -0
- package/src/tests/codex-oauth-keep-warm.test.ts +85 -1
- package/src/tests/config-session-auth.test.ts +14 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/grok-adapter.test.ts +3 -3
- package/src/tests/heartbeat-jev-gate.test.ts +23 -0
- package/src/tests/migration-232-agent-session-tokens.test.ts +67 -0
- package/src/tests/rbac-engine.test.ts +1 -0
- package/src/tests/runtime-child-token-cleanup.test.ts +3 -3
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/session-route-posture.test.ts +2 -0
- package/src/tests/worker-token-cli.test.ts +76 -0
- package/src/tests/workflow-executors.test.ts +4 -2
- package/src/tests/workflow-system-one-extraction.test.ts +805 -0
- package/src/types.ts +2 -1
- package/src/utils/request-auth-context.ts +2 -1
- package/src/workflows/definition.ts +7 -1
- package/src/workflows/engine.ts +18 -12
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/system-one-decision.ts +56 -255
- package/src/workflows/executors/system-one-extract-wire.ts +57 -0
- package/src/workflows/executors/system-one-extraction.ts +830 -0
- package/src/workflows/executors/system-one-providers.ts +277 -110
- package/src/workflows/executors/system-one-transport.ts +264 -0
- package/templates/official/lead/HEARTBEAT.md +1 -1
- package/templates/skills/apps/SKILL.md +3 -1
- package/templates/skills/apps/content.md +3 -1
- package/templates/skills/heartbeat-runbook/content.md +3 -3
- package/dist/cli-v1sjhn52.js +0 -7
- package/dist/handlers-2qjw434w.js +0 -39
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
completeApprovalCancellationNotificationClaim2,
|
|
6
6
|
releaseApprovalCancellationNotificationClaim2,
|
|
7
7
|
init_db3
|
|
8
|
-
} from "./cli-
|
|
8
|
+
} from "./cli-p20r8w42.js";
|
|
9
9
|
import {
|
|
10
10
|
getSlackApp2
|
|
11
|
-
} from "./cli-
|
|
11
|
+
} from "./cli-qkhc5aqy.js";
|
|
12
12
|
import {
|
|
13
13
|
DEFAULT_MODEL,
|
|
14
14
|
resolveCredential
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
TERMINAL_TASK_STATUSES,
|
|
18
18
|
init_types
|
|
19
|
-
} from "./cli-
|
|
19
|
+
} from "./cli-nc35pxb7.js";
|
|
20
20
|
import {
|
|
21
21
|
selectCredential,
|
|
22
22
|
init_credentials
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
strictObject2,
|
|
46
46
|
union2,
|
|
47
47
|
discriminatedUnion,
|
|
48
|
+
tuple,
|
|
48
49
|
record2,
|
|
49
50
|
_enum2,
|
|
50
51
|
literal2
|
|
@@ -66,7 +67,6 @@ import {
|
|
|
66
67
|
|
|
67
68
|
// src/workflows/executors/system-one-decision.ts
|
|
68
69
|
init_zod();
|
|
69
|
-
init_openrouter_base_url();
|
|
70
70
|
init_secret_scrubber();
|
|
71
71
|
|
|
72
72
|
// src/workflows/executors/base.ts
|
|
@@ -454,7 +454,7 @@ class HumanInTheLoopExecutor extends BaseExecutor {
|
|
|
454
454
|
}
|
|
455
455
|
if (notification.channel === "slack") {
|
|
456
456
|
try {
|
|
457
|
-
const { getSlackApp } = await import("./app-
|
|
457
|
+
const { getSlackApp } = await import("./app-rb7vc02s.js");
|
|
458
458
|
const slackApp = getSlackApp();
|
|
459
459
|
if (!slackApp) {
|
|
460
460
|
console.warn("[HITL] Slack not initialized — cannot send notification");
|
|
@@ -541,6 +541,9 @@ function formatTimeout(seconds) {
|
|
|
541
541
|
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
|
542
542
|
}
|
|
543
543
|
|
|
544
|
+
// src/workflows/executors/system-one-extraction.ts
|
|
545
|
+
init_zod();
|
|
546
|
+
|
|
544
547
|
// src/workflows/executors/system-one-providers.ts
|
|
545
548
|
init_openrouter_base_url();
|
|
546
549
|
|
|
@@ -585,6 +588,35 @@ function cloudflareErrorCode(body) {
|
|
|
585
588
|
return typeof code === "string" && ERROR_CODE_RE.test(code) ? code : undefined;
|
|
586
589
|
}
|
|
587
590
|
|
|
591
|
+
// src/workflows/executors/system-one-extract-wire.ts
|
|
592
|
+
function extractionStateText(state) {
|
|
593
|
+
return typeof state === "string" ? state : JSON.stringify(state);
|
|
594
|
+
}
|
|
595
|
+
var CLAIRIC_WIRE = {
|
|
596
|
+
buildRequest: (config, model) => ({
|
|
597
|
+
...model === undefined ? {} : { model },
|
|
598
|
+
state: extractionStateText(config.state),
|
|
599
|
+
fields: config.fields,
|
|
600
|
+
...config.images?.length ? { images: config.images } : {},
|
|
601
|
+
...config.referenceDate ? { reference_date: config.referenceDate } : {}
|
|
602
|
+
}),
|
|
603
|
+
toExtraction: (body) => body
|
|
604
|
+
};
|
|
605
|
+
var CLOUDFLARE_EXTRACT_WIRE = {
|
|
606
|
+
buildRequest: CLAIRIC_WIRE.buildRequest,
|
|
607
|
+
toExtraction(body) {
|
|
608
|
+
if (!isRecord(body))
|
|
609
|
+
wireContractError("response body must be a JSON object");
|
|
610
|
+
if (body.success !== true) {
|
|
611
|
+
const code = cloudflareErrorCode(body);
|
|
612
|
+
throw new SystemOneWireError(`Cloudflare Workers AI reported a failed run${code ? ` (${code})` : ""}. Nothing was extracted.`);
|
|
613
|
+
}
|
|
614
|
+
if (!isRecord(body.result))
|
|
615
|
+
wireContractError("Cloudflare response is missing its result");
|
|
616
|
+
return body.result;
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
|
|
588
620
|
// src/workflows/executors/system-one-openai-wire.ts
|
|
589
621
|
function entryText(entry) {
|
|
590
622
|
if (entry === undefined || entry === null)
|
|
@@ -764,27 +796,43 @@ var LAYA_KEY = "LAYA_API_KEY";
|
|
|
764
796
|
var LAYA_URL_KEY = "LAYA_URL";
|
|
765
797
|
var OPENAI_DECISIONS_KEY = "OPENAI_DECISIONS_API_KEY";
|
|
766
798
|
var CLOUDFLARE_TOKEN_KEY = "CLOUDFLARE_API_TOKEN";
|
|
799
|
+
var CLOUDFLARE_KEY_FALLBACK = "CLOUDFLARE_API_KEY";
|
|
767
800
|
var CLOUDFLARE_ACCOUNT_KEY = "CLOUDFLARE_ACCOUNT_ID";
|
|
768
801
|
var CLOUDFLARE_ACCOUNT_RE = /^[0-9a-f]{32}$/;
|
|
769
802
|
async function readGlobalConfig(db, key) {
|
|
770
803
|
const rows = await db.getSwarmConfigs({ scope: "global", key });
|
|
771
804
|
return rows.find((row) => row.scope === "global" && row.key === key)?.value;
|
|
772
805
|
}
|
|
773
|
-
|
|
806
|
+
async function readFirstGlobalConfig(db, keys) {
|
|
807
|
+
for (const keyName of keys) {
|
|
808
|
+
const value = await readGlobalConfig(db, keyName);
|
|
809
|
+
if (value && value.trim() !== "")
|
|
810
|
+
return { value, keyName };
|
|
811
|
+
}
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
var CLOUDFLARE_ENDPOINT = {
|
|
815
|
+
configKey: CLOUDFLARE_ACCOUNT_KEY,
|
|
816
|
+
describe: "the 32-character id of a Cloudflare account",
|
|
817
|
+
build: (accountId, model) => {
|
|
818
|
+
const id = accountId.trim();
|
|
819
|
+
return CLOUDFLARE_ACCOUNT_RE.test(id) ? `https://api.cloudflare.com/client/v4/accounts/${id}/ai/run/@cf/cloudflare/${model}` : null;
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
var SYSTEM_ONE_HOSTS = {
|
|
774
823
|
typesafe: {
|
|
775
824
|
label: "TypeSafe",
|
|
776
825
|
keyName: TYPESAFE_KEY,
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
826
|
+
readKey: ({ db }) => readGlobalConfig(db, TYPESAFE_KEY),
|
|
827
|
+
decide: {
|
|
828
|
+
endpoint: "https://api.typesafe.ai/v1/systemone",
|
|
829
|
+
defaultModel: "jev-latest",
|
|
830
|
+
confidenceField: "confidence"
|
|
831
|
+
}
|
|
781
832
|
},
|
|
782
833
|
openrouter: {
|
|
783
834
|
label: "OpenRouter",
|
|
784
835
|
keyName: OPENROUTER_KEY,
|
|
785
|
-
endpoint: "https://openrouter.ai/api/alpha/decisions",
|
|
786
|
-
defaultModel: "~typesafe/jev-latest",
|
|
787
|
-
confidenceField: "confidence",
|
|
788
836
|
deploymentProblem(env) {
|
|
789
837
|
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".';
|
|
790
838
|
},
|
|
@@ -794,97 +842,140 @@ var SYSTEM_ONE_PROVIDERS = {
|
|
|
794
842
|
} catch {
|
|
795
843
|
return null;
|
|
796
844
|
}
|
|
845
|
+
},
|
|
846
|
+
decide: {
|
|
847
|
+
endpoint: "https://openrouter.ai/api/alpha/decisions",
|
|
848
|
+
defaultModel: "~typesafe/jev-latest",
|
|
849
|
+
confidenceField: "confidence"
|
|
797
850
|
}
|
|
798
851
|
},
|
|
799
852
|
laya: {
|
|
800
853
|
label: "laya",
|
|
801
854
|
keyName: LAYA_KEY,
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
855
|
+
readKey: ({ db }) => readGlobalConfig(db, LAYA_KEY),
|
|
856
|
+
decide: {
|
|
857
|
+
endpoint: { configKey: LAYA_URL_KEY, path: "/v1/systemone" },
|
|
858
|
+
confidenceField: "answer_confidence"
|
|
859
|
+
}
|
|
805
860
|
},
|
|
806
861
|
openai: {
|
|
807
862
|
label: "OpenAI",
|
|
808
863
|
keyName: OPENAI_DECISIONS_KEY,
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
864
|
+
readKey: ({ db }) => readGlobalConfig(db, OPENAI_DECISIONS_KEY),
|
|
865
|
+
decide: {
|
|
866
|
+
endpoint: "https://api.openai.com/v1/decisions",
|
|
867
|
+
defaultModel: "gpt-6-luna",
|
|
868
|
+
confidenceField: "confidence",
|
|
869
|
+
wire: OPENAI_DECISIONS_WIRE
|
|
870
|
+
}
|
|
814
871
|
},
|
|
815
872
|
cloudflare: {
|
|
816
873
|
label: "Cloudflare Workers AI",
|
|
817
874
|
keyName: CLOUDFLARE_TOKEN_KEY,
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
875
|
+
fallbackKeyNames: [CLOUDFLARE_KEY_FALLBACK],
|
|
876
|
+
readKey: ({ db }) => readFirstGlobalConfig(db, [CLOUDFLARE_TOKEN_KEY, CLOUDFLARE_KEY_FALLBACK]),
|
|
877
|
+
decide: {
|
|
878
|
+
endpoint: CLOUDFLARE_ENDPOINT,
|
|
879
|
+
defaultModel: "clef",
|
|
880
|
+
allowedModels: ["clef", "clef-flash"],
|
|
881
|
+
limits: { maxQuestions: 64 },
|
|
882
|
+
confidenceField: "confidence",
|
|
883
|
+
wire: CLOUDFLARE_WIRE
|
|
825
884
|
},
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
885
|
+
extract: {
|
|
886
|
+
endpoint: CLOUDFLARE_ENDPOINT,
|
|
887
|
+
defaultModel: "clairic",
|
|
888
|
+
allowedModels: ["clairic"],
|
|
889
|
+
limits: { maxFields: 64, maxImages: 4 },
|
|
890
|
+
wire: CLOUDFLARE_EXTRACT_WIRE
|
|
891
|
+
}
|
|
832
892
|
}
|
|
833
893
|
};
|
|
834
|
-
|
|
894
|
+
function hostsServing(capability) {
|
|
895
|
+
const ids = Object.keys(SYSTEM_ONE_HOSTS).filter((id) => SYSTEM_ONE_HOSTS[id][capability] !== undefined);
|
|
896
|
+
return ids;
|
|
897
|
+
}
|
|
898
|
+
var SYSTEM_ONE_PROVIDERS = Object.fromEntries(hostsServing("decide").map((id) => {
|
|
899
|
+
const { decide, extract: _extract, ...host } = SYSTEM_ONE_HOSTS[id];
|
|
900
|
+
return [id, { ...host, ...decide }];
|
|
901
|
+
}));
|
|
902
|
+
var SYSTEM_ONE_PROVIDER_IDS = hostsServing("decide");
|
|
903
|
+
var SYSTEM_ONE_EXTRACT_PROVIDER_IDS = hostsServing("extract");
|
|
835
904
|
var SYSTEM_ONE_DEFAULT_PROVIDER = "typesafe";
|
|
905
|
+
var SYSTEM_ONE_EXTRACT_DEFAULT_PROVIDER = "cloudflare";
|
|
836
906
|
function isSystemOneProviderId(value) {
|
|
837
|
-
return typeof value === "string" &&
|
|
907
|
+
return typeof value === "string" && SYSTEM_ONE_PROVIDER_IDS.includes(value);
|
|
908
|
+
}
|
|
909
|
+
function isSystemOneExtractProviderId(value) {
|
|
910
|
+
return typeof value === "string" && SYSTEM_ONE_EXTRACT_PROVIDER_IDS.includes(value);
|
|
838
911
|
}
|
|
839
912
|
function systemOneProviderOf(config) {
|
|
840
913
|
if (config.provider === undefined)
|
|
841
914
|
return SYSTEM_ONE_DEFAULT_PROVIDER;
|
|
842
915
|
return isSystemOneProviderId(config.provider) ? config.provider : undefined;
|
|
843
916
|
}
|
|
917
|
+
function systemOneExtractProviderOf(config) {
|
|
918
|
+
if (config.provider === undefined)
|
|
919
|
+
return SYSTEM_ONE_EXTRACT_DEFAULT_PROVIDER;
|
|
920
|
+
return isSystemOneExtractProviderId(config.provider) ? config.provider : undefined;
|
|
921
|
+
}
|
|
922
|
+
function serviceOf(id, capability) {
|
|
923
|
+
const service = SYSTEM_ONE_HOSTS[id][capability];
|
|
924
|
+
if (!service)
|
|
925
|
+
throw new Error(`SystemOne provider "${id}" does not serve ${capability}`);
|
|
926
|
+
return service;
|
|
927
|
+
}
|
|
928
|
+
var NOTHING_PRODUCED = {
|
|
929
|
+
decide: "No decision was made.",
|
|
930
|
+
extract: "Nothing was extracted."
|
|
931
|
+
};
|
|
844
932
|
var SET_IT = (keyName) => `Set ${keyName} as a global secret on the Secrets page (Settings > Secrets).`;
|
|
845
933
|
var SET_URL = (configKey) => `Set ${configKey} as a global config value (set-config with scope global).`;
|
|
846
934
|
function systemOneKeyMissingMessage(id) {
|
|
847
|
-
const { label, keyName } =
|
|
848
|
-
|
|
935
|
+
const { label, keyName, fallbackKeyNames } = SYSTEM_ONE_HOSTS[id];
|
|
936
|
+
const also = fallbackKeyNames?.length ? ` (${fallbackKeyNames.join(", ")} is read when it is unset, and is not configured either)` : "";
|
|
937
|
+
return `${keyName} is not configured${also}. SystemOne provider "${id}" needs a working ${label} API key. ${SET_IT(keyName)}`;
|
|
849
938
|
}
|
|
850
939
|
function systemOneKeyUnreadableMessage(id) {
|
|
851
|
-
return `Could not read ${
|
|
940
|
+
return `Could not read ${SYSTEM_ONE_HOSTS[id].keyName} from swarm config`;
|
|
852
941
|
}
|
|
853
|
-
function systemOneKeyMalformedMessage(id) {
|
|
854
|
-
|
|
942
|
+
function systemOneKeyMalformedMessage(id, keyName) {
|
|
943
|
+
const name = keyName ?? SYSTEM_ONE_HOSTS[id].keyName;
|
|
944
|
+
return `${name} is not a valid bearer token value. ${SET_IT(name)}`;
|
|
855
945
|
}
|
|
856
|
-
function systemOneKeyRejectedMessage(id, status, code) {
|
|
857
|
-
const { label
|
|
946
|
+
function systemOneKeyRejectedMessage(id, status, code, options = {}) {
|
|
947
|
+
const { label } = SYSTEM_ONE_HOSTS[id];
|
|
948
|
+
const keyName = options.keyName ?? SYSTEM_ONE_HOSTS[id].keyName;
|
|
858
949
|
const detail = code ? ` (${code})` : "";
|
|
859
|
-
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).
|
|
950
|
+
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). ${NOTHING_PRODUCED[options.capability ?? "decide"]}`;
|
|
860
951
|
}
|
|
861
|
-
function urlKeyOf(
|
|
862
|
-
return typeof
|
|
952
|
+
function urlKeyOf(endpoint) {
|
|
953
|
+
return typeof endpoint === "string" ? null : endpoint.configKey;
|
|
863
954
|
}
|
|
864
955
|
function isBuiltEndpoint(endpoint) {
|
|
865
956
|
return typeof endpoint !== "string" && "build" in endpoint;
|
|
866
957
|
}
|
|
867
|
-
function urlDescriptionOf(
|
|
868
|
-
return isBuiltEndpoint(
|
|
958
|
+
function urlDescriptionOf(id, endpoint) {
|
|
959
|
+
return isBuiltEndpoint(endpoint) ? endpoint.describe : `the URL of a ${SYSTEM_ONE_HOSTS[id].label} server`;
|
|
869
960
|
}
|
|
870
|
-
function systemOneUrlMissingMessage(id) {
|
|
871
|
-
const
|
|
872
|
-
const configKey = urlKeyOf(
|
|
873
|
-
return `${configKey} is not configured. SystemOne provider "${id}" needs ${urlDescriptionOf(
|
|
961
|
+
function systemOneUrlMissingMessage(id, capability = "decide") {
|
|
962
|
+
const { endpoint } = serviceOf(id, capability);
|
|
963
|
+
const configKey = urlKeyOf(endpoint) ?? "";
|
|
964
|
+
return `${configKey} is not configured. SystemOne provider "${id}" needs ${urlDescriptionOf(id, endpoint)}. ${SET_URL(configKey)}`;
|
|
874
965
|
}
|
|
875
|
-
function systemOneUrlUnreadableMessage(id) {
|
|
876
|
-
return `Could not read ${urlKeyOf(
|
|
966
|
+
function systemOneUrlUnreadableMessage(id, capability = "decide") {
|
|
967
|
+
return `Could not read ${urlKeyOf(serviceOf(id, capability).endpoint) ?? ""} from swarm config`;
|
|
877
968
|
}
|
|
878
|
-
function systemOneUrlInvalidMessage(id) {
|
|
879
|
-
const
|
|
880
|
-
const configKey = urlKeyOf(
|
|
881
|
-
if (isBuiltEndpoint(
|
|
882
|
-
return `${configKey} is not ${
|
|
969
|
+
function systemOneUrlInvalidMessage(id, capability = "decide") {
|
|
970
|
+
const { endpoint } = serviceOf(id, capability);
|
|
971
|
+
const configKey = urlKeyOf(endpoint) ?? "";
|
|
972
|
+
if (isBuiltEndpoint(endpoint)) {
|
|
973
|
+
return `${configKey} is not ${endpoint.describe}. ${SET_URL(configKey)}`;
|
|
883
974
|
}
|
|
884
|
-
return `${configKey} is not a usable ${
|
|
975
|
+
return `${configKey} is not a usable ${SYSTEM_ONE_HOSTS[id].label} server URL. It must be an https URL (http only for localhost) with no credentials, query, or fragment. ${SET_URL(configKey)}`;
|
|
885
976
|
}
|
|
886
|
-
function systemOneModelProblem(id, model) {
|
|
887
|
-
const { allowedModels } =
|
|
977
|
+
function systemOneModelProblem(id, model, capability = "decide") {
|
|
978
|
+
const { allowedModels } = serviceOf(id, capability);
|
|
888
979
|
if (!allowedModels || model !== undefined && allowedModels.includes(model))
|
|
889
980
|
return null;
|
|
890
981
|
return `SystemOne provider "${id}" serves only the models ${allowedModels.join(", ")}; set model to one of them or leave it unset.`;
|
|
@@ -918,75 +1009,879 @@ function systemOneEndpointFromBase(base, path) {
|
|
|
918
1009
|
return `${url.origin}${url.pathname.replace(/\/+$/, "")}${path}`;
|
|
919
1010
|
}
|
|
920
1011
|
async function resolveSystemOneTarget(id, ctx, overrides = {}) {
|
|
921
|
-
const
|
|
922
|
-
const
|
|
1012
|
+
const host = SYSTEM_ONE_HOSTS[id];
|
|
1013
|
+
const capability = overrides.capability ?? "decide";
|
|
1014
|
+
const service = serviceOf(id, capability);
|
|
1015
|
+
const deploymentProblem = host.deploymentProblem?.(ctx.env);
|
|
923
1016
|
if (deploymentProblem)
|
|
924
1017
|
return { ok: false, error: deploymentProblem };
|
|
925
1018
|
const errors = [];
|
|
926
1019
|
let endpoint;
|
|
927
|
-
if (typeof
|
|
928
|
-
endpoint =
|
|
1020
|
+
if (typeof service.endpoint === "string") {
|
|
1021
|
+
endpoint = service.endpoint;
|
|
929
1022
|
} else {
|
|
930
|
-
const { configKey } =
|
|
1023
|
+
const { configKey } = service.endpoint;
|
|
931
1024
|
let base;
|
|
932
1025
|
let readable = true;
|
|
933
1026
|
try {
|
|
934
1027
|
base = await (overrides.serverUrl ? overrides.serverUrl(id) : readGlobalConfig(ctx.db, configKey));
|
|
935
1028
|
} catch {
|
|
936
1029
|
readable = false;
|
|
937
|
-
errors.push(systemOneUrlUnreadableMessage(id));
|
|
1030
|
+
errors.push(systemOneUrlUnreadableMessage(id, capability));
|
|
938
1031
|
}
|
|
939
1032
|
if (readable) {
|
|
940
1033
|
if (!base || base.trim() === "") {
|
|
941
|
-
errors.push(systemOneUrlMissingMessage(id));
|
|
1034
|
+
errors.push(systemOneUrlMissingMessage(id, capability));
|
|
942
1035
|
} else {
|
|
943
|
-
const resolved = isBuiltEndpoint(
|
|
1036
|
+
const resolved = isBuiltEndpoint(service.endpoint) ? service.endpoint.build(base, overrides.model ?? service.defaultModel ?? "") : systemOneEndpointFromBase(base, service.endpoint.path);
|
|
944
1037
|
if (resolved)
|
|
945
1038
|
endpoint = resolved;
|
|
946
1039
|
else
|
|
947
|
-
errors.push(systemOneUrlInvalidMessage(id));
|
|
1040
|
+
errors.push(systemOneUrlInvalidMessage(id, capability));
|
|
948
1041
|
}
|
|
949
1042
|
}
|
|
950
1043
|
}
|
|
951
|
-
let
|
|
1044
|
+
let read;
|
|
952
1045
|
let keyProblem;
|
|
953
1046
|
try {
|
|
954
|
-
|
|
1047
|
+
read = await (overrides.apiKey ? overrides.apiKey(id) : host.readKey(ctx));
|
|
955
1048
|
} catch {
|
|
956
1049
|
keyProblem = systemOneKeyUnreadableMessage(id);
|
|
957
1050
|
}
|
|
1051
|
+
const apiKey = typeof read === "string" ? read : read?.value;
|
|
1052
|
+
const keyName = typeof read === "object" && read?.keyName || host.keyName;
|
|
958
1053
|
if (!keyProblem) {
|
|
959
1054
|
if (!apiKey || apiKey.trim() === "")
|
|
960
1055
|
keyProblem = systemOneKeyMissingMessage(id);
|
|
961
1056
|
else if (hasWhitespaceOrControl(apiKey))
|
|
962
|
-
keyProblem = systemOneKeyMalformedMessage(id);
|
|
1057
|
+
keyProblem = systemOneKeyMalformedMessage(id, keyName);
|
|
963
1058
|
}
|
|
964
1059
|
if (keyProblem)
|
|
965
1060
|
errors.push(keyProblem);
|
|
966
1061
|
if (errors.length > 0 || !endpoint || !apiKey)
|
|
967
1062
|
return { ok: false, error: errors.join(" ") };
|
|
968
|
-
return { ok: true, apiKey, endpoint };
|
|
1063
|
+
return { ok: true, apiKey, endpoint, keyName };
|
|
969
1064
|
}
|
|
970
1065
|
|
|
971
|
-
// src/workflows/executors/system-one-
|
|
972
|
-
|
|
973
|
-
var SYSTEM_ONE_ENDPOINT = SYSTEM_ONE_PROVIDERS.typesafe.endpoint;
|
|
974
|
-
var SYSTEM_ONE_API_KEY_CONFIG_KEY = SYSTEM_ONE_PROVIDERS.typesafe.keyName;
|
|
1066
|
+
// src/workflows/executors/system-one-transport.ts
|
|
1067
|
+
init_openrouter_base_url();
|
|
975
1068
|
var SYSTEM_ONE_DEFAULT_TIMEOUT_MS = 30000;
|
|
976
1069
|
var SYSTEM_ONE_MIN_TIMEOUT_MS = 1000;
|
|
977
1070
|
var SYSTEM_ONE_MAX_TIMEOUT_MS = 300000;
|
|
978
1071
|
var SYSTEM_ONE_DEFAULT_MAX_RETRIES = 2;
|
|
979
1072
|
var SYSTEM_ONE_MAX_RETRIES_LIMIT = 3;
|
|
980
|
-
var CHOICE_MIN_OPTIONS = 2;
|
|
981
|
-
var CHOICE_MAX_OPTIONS = 255;
|
|
982
|
-
var SCORE_MIN_LEVELS = 2;
|
|
983
|
-
var SCORE_MAX_LEVELS = 10;
|
|
984
1073
|
var DEADLINE_MARGIN_MS = 250;
|
|
985
1074
|
var BACKOFF_BASE_MS = 500;
|
|
986
1075
|
var BACKOFF_CAP_MS = 8000;
|
|
987
|
-
var DISTRIBUTION_TOLERANCE = 0.02;
|
|
988
1076
|
var MAX_RESPONSE_CHARS = 1048576;
|
|
989
1077
|
var MAX_ERROR_BODY_CHARS = 65536;
|
|
1078
|
+
var REQUEST_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
1079
|
+
var ERROR_CODE_RE2 = /^[A-Za-z0-9._:-]{1,64}$/;
|
|
1080
|
+
function readRequestId(headers) {
|
|
1081
|
+
const value = headers.get("x-request-id") ?? headers.get("request-id") ?? headers.get("cf-ray");
|
|
1082
|
+
return value && REQUEST_ID_RE.test(value) ? value : undefined;
|
|
1083
|
+
}
|
|
1084
|
+
function parseRetryAfter(value, now) {
|
|
1085
|
+
if (!value)
|
|
1086
|
+
return;
|
|
1087
|
+
const trimmed = value.trim();
|
|
1088
|
+
if (/^\d+(\.\d+)?$/.test(trimmed))
|
|
1089
|
+
return Math.round(Number(trimmed) * 1000);
|
|
1090
|
+
const at = Date.parse(trimmed);
|
|
1091
|
+
return Number.isNaN(at) ? undefined : Math.max(0, at - now);
|
|
1092
|
+
}
|
|
1093
|
+
function readErrorCode(text) {
|
|
1094
|
+
if (text.length === 0 || text.length > MAX_ERROR_BODY_CHARS)
|
|
1095
|
+
return;
|
|
1096
|
+
let parsed;
|
|
1097
|
+
try {
|
|
1098
|
+
parsed = JSON.parse(text);
|
|
1099
|
+
} catch {
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (!isRecord(parsed))
|
|
1103
|
+
return;
|
|
1104
|
+
const nested = isRecord(parsed.error) ? parsed.error : undefined;
|
|
1105
|
+
const detail = isRecord(parsed.detail) ? parsed.detail : undefined;
|
|
1106
|
+
for (const candidate of [
|
|
1107
|
+
nested?.code,
|
|
1108
|
+
nested?.type,
|
|
1109
|
+
detail?.error_type,
|
|
1110
|
+
parsed.code,
|
|
1111
|
+
parsed.type
|
|
1112
|
+
]) {
|
|
1113
|
+
if (typeof candidate === "string" && ERROR_CODE_RE2.test(candidate))
|
|
1114
|
+
return candidate;
|
|
1115
|
+
}
|
|
1116
|
+
return cloudflareErrorCode(parsed);
|
|
1117
|
+
}
|
|
1118
|
+
function defaultSleep(ms, signal) {
|
|
1119
|
+
return new Promise((resolve) => {
|
|
1120
|
+
if (signal.aborted)
|
|
1121
|
+
return resolve();
|
|
1122
|
+
const timer = setTimeout(done, ms);
|
|
1123
|
+
function done() {
|
|
1124
|
+
clearTimeout(timer);
|
|
1125
|
+
signal.removeEventListener("abort", done);
|
|
1126
|
+
resolve();
|
|
1127
|
+
}
|
|
1128
|
+
signal.addEventListener("abort", done, { once: true });
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
async function sendSystemOneRequest(request, options = {}) {
|
|
1132
|
+
const { timeoutMs, maxRetries } = request;
|
|
1133
|
+
const doFetch = options.fetch ?? fetch;
|
|
1134
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
1135
|
+
const now = options.now ?? Date.now;
|
|
1136
|
+
const random = options.random ?? Math.random;
|
|
1137
|
+
const deadlineAt = now() + timeoutMs - DEADLINE_MARGIN_MS;
|
|
1138
|
+
const controller = new AbortController;
|
|
1139
|
+
const timer = setTimeout(() => controller.abort(), Math.max(0, deadlineAt - now()));
|
|
1140
|
+
const timedOut = () => ({
|
|
1141
|
+
ok: false,
|
|
1142
|
+
error: `SystemOne request exceeded its ${timeoutMs}ms time budget`
|
|
1143
|
+
});
|
|
1144
|
+
try {
|
|
1145
|
+
for (let attempt = 0;; attempt++) {
|
|
1146
|
+
const result = await attemptOnce(doFetch, request, controller.signal, now);
|
|
1147
|
+
if (result.kind === "ok") {
|
|
1148
|
+
return { ok: true, text: result.text, requestId: result.requestId };
|
|
1149
|
+
}
|
|
1150
|
+
if (result.kind === "timeout" || controller.signal.aborted)
|
|
1151
|
+
return timedOut();
|
|
1152
|
+
const attempts = attempt + 1;
|
|
1153
|
+
const suffix = attempts > 1 ? ` after ${attempts} attempts` : "";
|
|
1154
|
+
if (!result.retryable || attempt >= maxRetries) {
|
|
1155
|
+
return { ok: false, error: `${result.message}${suffix}` };
|
|
1156
|
+
}
|
|
1157
|
+
const backoff = Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** attempt);
|
|
1158
|
+
const delay = result.retryAfterMs ?? Math.round(backoff * (0.5 + random() * 0.5));
|
|
1159
|
+
if (delay >= deadlineAt - now()) {
|
|
1160
|
+
return {
|
|
1161
|
+
ok: false,
|
|
1162
|
+
error: `${result.message}${suffix}; the ${delay}ms retry delay does not fit the ${timeoutMs}ms time budget`
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
await sleep(delay, controller.signal);
|
|
1166
|
+
if (controller.signal.aborted)
|
|
1167
|
+
return timedOut();
|
|
1168
|
+
}
|
|
1169
|
+
} finally {
|
|
1170
|
+
clearTimeout(timer);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
async function attemptOnce(doFetch, request, signal, now) {
|
|
1174
|
+
const { endpoint, apiKey, body } = request;
|
|
1175
|
+
let res;
|
|
1176
|
+
try {
|
|
1177
|
+
res = await doFetch(endpoint, {
|
|
1178
|
+
method: "POST",
|
|
1179
|
+
headers: {
|
|
1180
|
+
...getOpenRouterAttributionHeaders(endpoint, request.env),
|
|
1181
|
+
"content-type": "application/json",
|
|
1182
|
+
accept: "application/json",
|
|
1183
|
+
authorization: `Bearer ${apiKey}`
|
|
1184
|
+
},
|
|
1185
|
+
body,
|
|
1186
|
+
redirect: "manual",
|
|
1187
|
+
signal
|
|
1188
|
+
});
|
|
1189
|
+
} catch (err) {
|
|
1190
|
+
if (signal.aborted)
|
|
1191
|
+
return { kind: "timeout" };
|
|
1192
|
+
const code = err?.code;
|
|
1193
|
+
const detail = typeof code === "string" && /^[A-Z0-9_]{3,40}$/.test(code) ? ` (${code})` : "";
|
|
1194
|
+
return {
|
|
1195
|
+
kind: "fail",
|
|
1196
|
+
message: `SystemOne request failed: network error${detail}`,
|
|
1197
|
+
retryable: true
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
const requestId = readRequestId(res.headers);
|
|
1201
|
+
const withId = (message) => requestId ? `${message} [request ${requestId}]` : message;
|
|
1202
|
+
if (res.status >= 200 && res.status < 300) {
|
|
1203
|
+
let text;
|
|
1204
|
+
try {
|
|
1205
|
+
text = await res.text();
|
|
1206
|
+
} catch {
|
|
1207
|
+
if (signal.aborted)
|
|
1208
|
+
return { kind: "timeout" };
|
|
1209
|
+
return {
|
|
1210
|
+
kind: "fail",
|
|
1211
|
+
message: withId("SystemOne response body could not be read"),
|
|
1212
|
+
retryable: true
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
if (text.length > MAX_RESPONSE_CHARS) {
|
|
1216
|
+
return {
|
|
1217
|
+
kind: "fail",
|
|
1218
|
+
message: withId("SystemOne response exceeded the size limit"),
|
|
1219
|
+
retryable: false
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
return { kind: "ok", text, requestId };
|
|
1223
|
+
}
|
|
1224
|
+
if (res.status >= 300 && res.status < 400) {
|
|
1225
|
+
return {
|
|
1226
|
+
kind: "fail",
|
|
1227
|
+
message: withId(`SystemOne request was redirected (HTTP ${res.status}); redirects are refused`),
|
|
1228
|
+
retryable: false
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
let errorText = "";
|
|
1232
|
+
try {
|
|
1233
|
+
errorText = await res.text();
|
|
1234
|
+
} catch {
|
|
1235
|
+
if (signal.aborted)
|
|
1236
|
+
return { kind: "timeout" };
|
|
1237
|
+
}
|
|
1238
|
+
const code = readErrorCode(errorText);
|
|
1239
|
+
if (res.status === 401 || res.status === 403) {
|
|
1240
|
+
return {
|
|
1241
|
+
kind: "fail",
|
|
1242
|
+
message: withId(systemOneKeyRejectedMessage(request.providerId, res.status, code, {
|
|
1243
|
+
keyName: request.keyName,
|
|
1244
|
+
capability: request.capability
|
|
1245
|
+
})),
|
|
1246
|
+
retryable: false
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
const detail = code ? ` (${code})` : "";
|
|
1250
|
+
const retryable = res.status === 408 || res.status === 429 || res.status >= 500;
|
|
1251
|
+
return {
|
|
1252
|
+
kind: "fail",
|
|
1253
|
+
message: withId(`SystemOne API returned HTTP ${res.status}${detail}`),
|
|
1254
|
+
retryable,
|
|
1255
|
+
retryAfterMs: retryable ? parseRetryAfter(res.headers.get("retry-after"), now()) : undefined
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// src/workflows/executors/system-one-extraction.ts
|
|
1260
|
+
var SYSTEM_ONE_EXTRACTION_NODE_TYPE = "system-one-extraction";
|
|
1261
|
+
var EXTRACTION_FIELD_TYPES = [
|
|
1262
|
+
"text",
|
|
1263
|
+
"number",
|
|
1264
|
+
"date",
|
|
1265
|
+
"time",
|
|
1266
|
+
"datetime",
|
|
1267
|
+
"duration",
|
|
1268
|
+
"list",
|
|
1269
|
+
"estimate",
|
|
1270
|
+
"forecast"
|
|
1271
|
+
];
|
|
1272
|
+
var DURATION_UNITS = ["seconds", "minutes", "hours", "days", "weeks", "months", "years"];
|
|
1273
|
+
var LIST_ITEMS = ["text", "number", "date"];
|
|
1274
|
+
var FORECAST_INTERVALS = [
|
|
1275
|
+
"step",
|
|
1276
|
+
"minute",
|
|
1277
|
+
"hour",
|
|
1278
|
+
"day",
|
|
1279
|
+
"week",
|
|
1280
|
+
"month",
|
|
1281
|
+
"quarter",
|
|
1282
|
+
"year"
|
|
1283
|
+
];
|
|
1284
|
+
var FIELD_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
|
|
1285
|
+
var FIELD_ID_MESSAGE = "field ids must start with a letter or underscore, use only letters, digits, _ or -, and be at most 64 characters";
|
|
1286
|
+
var RESERVED_FIELD_IDS = new Set([...Object.getOwnPropertyNames(Object.prototype), "prototype"]);
|
|
1287
|
+
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1288
|
+
var MAX_IMAGES = 4;
|
|
1289
|
+
var MAX_IMAGE_BYTES = 4 * 1024 * 1024;
|
|
1290
|
+
var MAX_IMAGES_TOTAL_BYTES = 8 * 1024 * 1024;
|
|
1291
|
+
var IMAGE_TYPES = ["image/png", "image/jpeg", "image/webp"];
|
|
1292
|
+
var DATA_URL_RE = /^data:(image\/(?:png|jpeg|webp));base64,([A-Za-z0-9+/=\s]+)$/i;
|
|
1293
|
+
var Instructions = string2().trim().min(1, "instructions must not be blank");
|
|
1294
|
+
var NullThreshold = number2().gt(0, "null_threshold must be above 0").max(1, "null_threshold must be at most 1");
|
|
1295
|
+
var Decimals = number2().int().min(0).max(6);
|
|
1296
|
+
var Unit = string2().trim().min(1, "unit must not be blank");
|
|
1297
|
+
var Scale = _enum2(["linear", "log"]);
|
|
1298
|
+
var Bound = union2([
|
|
1299
|
+
number2(),
|
|
1300
|
+
string2().regex(ISO_DATE_RE, "must be a number or an ISO date (YYYY-MM-DD)")
|
|
1301
|
+
]);
|
|
1302
|
+
var plain = { instructions: Instructions.optional(), null_threshold: NullThreshold.optional() };
|
|
1303
|
+
var TextField = strictObject2({ type: literal2("text"), ...plain });
|
|
1304
|
+
var NumberField = strictObject2({
|
|
1305
|
+
type: literal2("number"),
|
|
1306
|
+
...plain,
|
|
1307
|
+
unit: Unit.optional(),
|
|
1308
|
+
integer: boolean().optional(),
|
|
1309
|
+
decimals: Decimals.optional()
|
|
1310
|
+
});
|
|
1311
|
+
var DateField = strictObject2({ type: literal2("date"), ...plain });
|
|
1312
|
+
var TimeField = strictObject2({ type: literal2("time"), ...plain });
|
|
1313
|
+
var DatetimeField = strictObject2({ type: literal2("datetime"), ...plain });
|
|
1314
|
+
var DurationField = strictObject2({
|
|
1315
|
+
type: literal2("duration"),
|
|
1316
|
+
...plain,
|
|
1317
|
+
unit: _enum2(DURATION_UNITS).optional(),
|
|
1318
|
+
integer: boolean().optional(),
|
|
1319
|
+
decimals: Decimals.optional()
|
|
1320
|
+
});
|
|
1321
|
+
var ListField = strictObject2({
|
|
1322
|
+
type: literal2("list"),
|
|
1323
|
+
instructions: Instructions.optional(),
|
|
1324
|
+
items: _enum2(LIST_ITEMS),
|
|
1325
|
+
decimals: Decimals.optional()
|
|
1326
|
+
});
|
|
1327
|
+
var EstimateField = strictObject2({
|
|
1328
|
+
type: literal2("estimate"),
|
|
1329
|
+
instructions: Instructions.optional(),
|
|
1330
|
+
unit: Unit.optional(),
|
|
1331
|
+
min: Bound,
|
|
1332
|
+
max: Bound,
|
|
1333
|
+
step: union2([number2().positive(), _enum2(["month", "year"])]).optional(),
|
|
1334
|
+
scale: Scale.optional()
|
|
1335
|
+
});
|
|
1336
|
+
var ForecastField = strictObject2({
|
|
1337
|
+
type: literal2("forecast"),
|
|
1338
|
+
instructions: Instructions.optional(),
|
|
1339
|
+
unit: Unit.optional(),
|
|
1340
|
+
min: number2(),
|
|
1341
|
+
max: number2(),
|
|
1342
|
+
horizon: number2().int().min(1).max(64).optional(),
|
|
1343
|
+
interval: _enum2(FORECAST_INTERVALS).optional(),
|
|
1344
|
+
scale: Scale.optional()
|
|
1345
|
+
});
|
|
1346
|
+
var SystemOneExtractionFieldSchema = discriminatedUnion("type", [
|
|
1347
|
+
TextField,
|
|
1348
|
+
NumberField,
|
|
1349
|
+
DateField,
|
|
1350
|
+
TimeField,
|
|
1351
|
+
DatetimeField,
|
|
1352
|
+
DurationField,
|
|
1353
|
+
ListField,
|
|
1354
|
+
EstimateField,
|
|
1355
|
+
ForecastField
|
|
1356
|
+
]).superRefine((field, ctx) => {
|
|
1357
|
+
if ("integer" in field && field.integer && field.decimals !== undefined && field.decimals > 0) {
|
|
1358
|
+
ctx.addIssue({
|
|
1359
|
+
code: "custom",
|
|
1360
|
+
path: ["decimals"],
|
|
1361
|
+
message: "an integer field cannot also set decimals above 0"
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
if (field.type === "list" && field.decimals !== undefined && field.items !== "number") {
|
|
1365
|
+
ctx.addIssue({
|
|
1366
|
+
code: "custom",
|
|
1367
|
+
path: ["decimals"],
|
|
1368
|
+
message: 'decimals applies only to a list of items "number"'
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
if (field.type === "estimate" || field.type === "forecast") {
|
|
1372
|
+
const { min, max } = field;
|
|
1373
|
+
if (typeof min !== typeof max) {
|
|
1374
|
+
ctx.addIssue({
|
|
1375
|
+
code: "custom",
|
|
1376
|
+
path: ["max"],
|
|
1377
|
+
message: "min and max must both be numbers or both be ISO dates"
|
|
1378
|
+
});
|
|
1379
|
+
} else if (min >= max) {
|
|
1380
|
+
ctx.addIssue({ code: "custom", path: ["max"], message: "max must be greater than min" });
|
|
1381
|
+
}
|
|
1382
|
+
if (field.scale === "log" && typeof min === "number" && min <= 0) {
|
|
1383
|
+
ctx.addIssue({
|
|
1384
|
+
code: "custom",
|
|
1385
|
+
path: ["min"],
|
|
1386
|
+
message: 'a "log" scale needs min above 0'
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
if (field.type === "estimate" && typeof field.step === "string" && typeof field.min === "number") {
|
|
1390
|
+
ctx.addIssue({
|
|
1391
|
+
code: "custom",
|
|
1392
|
+
path: ["step"],
|
|
1393
|
+
message: `step "${field.step}" applies only to a date estimate (ISO date min and max)`
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
var ImageSchema = union2([
|
|
1399
|
+
string2().regex(DATA_URL_RE, "an image must be a base64 data: URL of a PNG, JPEG or WebP"),
|
|
1400
|
+
strictObject2({ content_type: _enum2(IMAGE_TYPES), base64: string2().min(1) })
|
|
1401
|
+
]);
|
|
1402
|
+
function imageBytes(image) {
|
|
1403
|
+
const base64 = (typeof image === "string" ? DATA_URL_RE.exec(image)?.[2] ?? "" : image.base64).replace(/\s+/g, "");
|
|
1404
|
+
const padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
|
|
1405
|
+
return Math.floor(base64.length * 3 / 4) - padding;
|
|
1406
|
+
}
|
|
1407
|
+
var SystemOneExtractionConfigSchema = strictObject2({
|
|
1408
|
+
provider: _enum2(SYSTEM_ONE_EXTRACT_PROVIDER_IDS).default(SYSTEM_ONE_EXTRACT_DEFAULT_PROVIDER),
|
|
1409
|
+
model: string2().min(1).optional(),
|
|
1410
|
+
state: union2([string2(), array2(unknown()), record2(string2(), unknown())]).default(""),
|
|
1411
|
+
images: array2(ImageSchema).max(MAX_IMAGES).optional(),
|
|
1412
|
+
referenceDate: string2().regex(ISO_DATE_RE, "referenceDate must be an ISO date (YYYY-MM-DD)").optional(),
|
|
1413
|
+
timeoutMs: number2().int().min(SYSTEM_ONE_MIN_TIMEOUT_MS).max(SYSTEM_ONE_MAX_TIMEOUT_MS).default(SYSTEM_ONE_DEFAULT_TIMEOUT_MS),
|
|
1414
|
+
maxRetries: number2().int().min(0).max(SYSTEM_ONE_MAX_RETRIES_LIMIT).default(SYSTEM_ONE_DEFAULT_MAX_RETRIES),
|
|
1415
|
+
fields: record2(string2().regex(FIELD_ID_RE, FIELD_ID_MESSAGE), SystemOneExtractionFieldSchema)
|
|
1416
|
+
}).superRefine((config, ctx) => {
|
|
1417
|
+
const fieldIds = Object.keys(config.fields);
|
|
1418
|
+
if (fieldIds.length === 0) {
|
|
1419
|
+
ctx.addIssue({
|
|
1420
|
+
code: "custom",
|
|
1421
|
+
path: ["fields"],
|
|
1422
|
+
message: "system-one-extraction needs at least one field"
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
for (const id of fieldIds) {
|
|
1426
|
+
if (RESERVED_FIELD_IDS.has(id)) {
|
|
1427
|
+
ctx.addIssue({
|
|
1428
|
+
code: "custom",
|
|
1429
|
+
path: ["fields", id],
|
|
1430
|
+
message: `"${id}" is reserved and cannot be a field id`
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
const stateText = typeof config.state === "string" ? config.state : "nonempty";
|
|
1435
|
+
if (stateText.trim() === "" && !config.images?.length) {
|
|
1436
|
+
ctx.addIssue({
|
|
1437
|
+
code: "custom",
|
|
1438
|
+
path: ["state"],
|
|
1439
|
+
message: "state must not be empty unless images are sent"
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
const images = config.images ?? [];
|
|
1443
|
+
let total = 0;
|
|
1444
|
+
images.forEach((image, index) => {
|
|
1445
|
+
const bytes = imageBytes(image);
|
|
1446
|
+
total += bytes;
|
|
1447
|
+
if (bytes > MAX_IMAGE_BYTES) {
|
|
1448
|
+
ctx.addIssue({
|
|
1449
|
+
code: "custom",
|
|
1450
|
+
path: ["images", index],
|
|
1451
|
+
message: "an image must be at most 4 MiB decoded"
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
if (total > MAX_IMAGES_TOTAL_BYTES) {
|
|
1456
|
+
ctx.addIssue({
|
|
1457
|
+
code: "custom",
|
|
1458
|
+
path: ["images"],
|
|
1459
|
+
message: "images must total at most 8 MiB decoded"
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
const host = SYSTEM_ONE_HOSTS[config.provider];
|
|
1463
|
+
const service = host.extract;
|
|
1464
|
+
const modelProblem = systemOneModelProblem(config.provider, config.model ?? service?.defaultModel, "extract");
|
|
1465
|
+
if (modelProblem)
|
|
1466
|
+
ctx.addIssue({ code: "custom", path: ["model"], message: modelProblem });
|
|
1467
|
+
const maxFields = service?.limits?.maxFields;
|
|
1468
|
+
if (maxFields !== undefined && fieldIds.length > maxFields) {
|
|
1469
|
+
ctx.addIssue({
|
|
1470
|
+
code: "custom",
|
|
1471
|
+
path: ["fields"],
|
|
1472
|
+
message: `SystemOne provider "${config.provider}" takes at most ${maxFields} fields per node; this node has ${fieldIds.length}.`
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
const maxImages = service?.limits?.maxImages;
|
|
1476
|
+
if (maxImages !== undefined && images.length > maxImages) {
|
|
1477
|
+
ctx.addIssue({
|
|
1478
|
+
code: "custom",
|
|
1479
|
+
path: ["images"],
|
|
1480
|
+
message: `SystemOne provider "${config.provider}" takes at most ${maxImages} images per node.`
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
var UnitInterval = number2().min(0).max(1);
|
|
1485
|
+
var Span = tuple([number2().int().min(0), number2().int().min(0)]);
|
|
1486
|
+
var Point = union2([number2(), string2()]);
|
|
1487
|
+
var SystemOneExtractionAnswerSchema = strictObject2({
|
|
1488
|
+
type: _enum2(EXTRACTION_FIELD_TYPES),
|
|
1489
|
+
value: unknown(),
|
|
1490
|
+
guess: unknown().optional(),
|
|
1491
|
+
p_null: UnitInterval.optional(),
|
|
1492
|
+
confidence: UnitInterval.optional(),
|
|
1493
|
+
span: Span.optional(),
|
|
1494
|
+
spans: array2(Span).optional(),
|
|
1495
|
+
unit: string2().optional(),
|
|
1496
|
+
mean: union2([Point, array2(Point)]).optional(),
|
|
1497
|
+
median: union2([Point, array2(Point)]).optional(),
|
|
1498
|
+
p10: union2([Point, array2(Point)]).optional(),
|
|
1499
|
+
p90: union2([Point, array2(Point)]).optional(),
|
|
1500
|
+
p_below: UnitInterval.optional(),
|
|
1501
|
+
p_above: UnitInterval.optional()
|
|
1502
|
+
});
|
|
1503
|
+
var SystemOneExtractionOutputSchema = object2({
|
|
1504
|
+
model: string2().min(1),
|
|
1505
|
+
answers: record2(string2(), SystemOneExtractionAnswerSchema),
|
|
1506
|
+
usage: object2({
|
|
1507
|
+
input_tokens: number2().int().min(0),
|
|
1508
|
+
output_tokens: number2().int().min(0)
|
|
1509
|
+
}),
|
|
1510
|
+
truncated: boolean().optional(),
|
|
1511
|
+
requestId: string2().optional()
|
|
1512
|
+
});
|
|
1513
|
+
function hasToken(value) {
|
|
1514
|
+
if (typeof value === "string")
|
|
1515
|
+
return value.includes("{{") && value.includes("}}");
|
|
1516
|
+
if (Array.isArray(value))
|
|
1517
|
+
return value.some(hasToken);
|
|
1518
|
+
if (isRecord(value))
|
|
1519
|
+
return Object.values(value).some(hasToken);
|
|
1520
|
+
return false;
|
|
1521
|
+
}
|
|
1522
|
+
function templatedFieldViolations(nodeId, fields) {
|
|
1523
|
+
const violations = [];
|
|
1524
|
+
for (const [id, spec] of Object.entries(fields)) {
|
|
1525
|
+
if (!hasToken(spec))
|
|
1526
|
+
continue;
|
|
1527
|
+
const at = `Node "${nodeId}" (system-one-extraction) config.fields.${id}`;
|
|
1528
|
+
if (!FIELD_ID_RE.test(id)) {
|
|
1529
|
+
violations.push(`${at}: ${FIELD_ID_MESSAGE}`);
|
|
1530
|
+
} else if (RESERVED_FIELD_IDS.has(id)) {
|
|
1531
|
+
violations.push(`${at}: "${id}" is reserved and cannot be a field id`);
|
|
1532
|
+
}
|
|
1533
|
+
if (!isRecord(spec)) {
|
|
1534
|
+
violations.push(`${at} must be an object: field types and options are static, only instructions may use {{tokens}}`);
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
const tokened = Object.keys(spec).filter((key) => key !== "instructions" && hasToken(spec[key]));
|
|
1538
|
+
for (const key of tokened) {
|
|
1539
|
+
violations.push(`${at}.${key} must be a literal: only instructions may use {{tokens}}`);
|
|
1540
|
+
}
|
|
1541
|
+
if (tokened.length > 0)
|
|
1542
|
+
continue;
|
|
1543
|
+
if (typeof spec.instructions !== "string") {
|
|
1544
|
+
violations.push(`${at}.instructions must be a string to use {{tokens}}`);
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
const parsed = SystemOneExtractionFieldSchema.safeParse({ ...spec, instructions: "x" });
|
|
1548
|
+
if (parsed.success)
|
|
1549
|
+
continue;
|
|
1550
|
+
for (const issue of parsed.error.issues) {
|
|
1551
|
+
const path = issue.path.length > 0 ? `${at}.${issue.path.map(String).join(".")}` : at;
|
|
1552
|
+
violations.push(`${path}: ${issue.message}`);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
return violations;
|
|
1556
|
+
}
|
|
1557
|
+
var STAND_IN_IMAGE = "data:image/png;base64,AAAA";
|
|
1558
|
+
function standInConfig(config) {
|
|
1559
|
+
const known = new Set(Object.keys(SystemOneExtractionConfigSchema.shape));
|
|
1560
|
+
const out = {};
|
|
1561
|
+
for (const [key, value] of Object.entries(config)) {
|
|
1562
|
+
if (!hasToken(value) || key === "fields" || !known.has(key)) {
|
|
1563
|
+
out[key] = value;
|
|
1564
|
+
} else if (key === "state") {
|
|
1565
|
+
out.state = typeof value === "string" ? "x" : value;
|
|
1566
|
+
} else if (key === "images" && Array.isArray(value)) {
|
|
1567
|
+
out.images = value.map((image) => hasToken(image) ? STAND_IN_IMAGE : image);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
if (isRecord(config.fields)) {
|
|
1571
|
+
out.fields = Object.fromEntries(Object.entries(config.fields).map(([id, spec]) => [
|
|
1572
|
+
id,
|
|
1573
|
+
isRecord(spec) && hasToken(spec) ? { ...spec, instructions: "x" } : spec
|
|
1574
|
+
]));
|
|
1575
|
+
}
|
|
1576
|
+
return out;
|
|
1577
|
+
}
|
|
1578
|
+
function templatedConfigViolations(nodeId, config) {
|
|
1579
|
+
if (!hasToken(config))
|
|
1580
|
+
return [];
|
|
1581
|
+
const parsed = SystemOneExtractionConfigSchema.safeParse(standInConfig(config));
|
|
1582
|
+
if (parsed.success)
|
|
1583
|
+
return [];
|
|
1584
|
+
const violations = [];
|
|
1585
|
+
for (const issue of parsed.error.issues) {
|
|
1586
|
+
let value = config;
|
|
1587
|
+
for (const segment of issue.path) {
|
|
1588
|
+
value = isRecord(value) || Array.isArray(value) ? value[segment] : undefined;
|
|
1589
|
+
}
|
|
1590
|
+
if (!hasToken(value) || typeof value === "string")
|
|
1591
|
+
continue;
|
|
1592
|
+
const path = ["config", ...issue.path.map(String)].join(".");
|
|
1593
|
+
violations.push(`Node "${nodeId}" (system-one-extraction) ${path}: ${issue.message}`);
|
|
1594
|
+
}
|
|
1595
|
+
return violations;
|
|
1596
|
+
}
|
|
1597
|
+
function systemOneExtractionStaticShapeViolations(node) {
|
|
1598
|
+
if (node.type !== SYSTEM_ONE_EXTRACTION_NODE_TYPE)
|
|
1599
|
+
return [];
|
|
1600
|
+
const violations = [];
|
|
1601
|
+
const fields = node.config.fields;
|
|
1602
|
+
if (fields !== undefined && (typeof fields !== "object" || fields === null || Array.isArray(fields))) {
|
|
1603
|
+
violations.push(`Node "${node.id}" (system-one-extraction) config.fields must be an object: field ids and types are static, only state, images, and instructions may use {{tokens}}`);
|
|
1604
|
+
} else if (fields !== undefined) {
|
|
1605
|
+
violations.push(...templatedFieldViolations(node.id, fields));
|
|
1606
|
+
}
|
|
1607
|
+
const provider = node.config.provider;
|
|
1608
|
+
if (typeof provider === "string" && provider.includes("{{")) {
|
|
1609
|
+
violations.push(`Node "${node.id}" (system-one-extraction) config.provider must be a literal (${SYSTEM_ONE_EXTRACT_PROVIDER_IDS.join(", ")}), not a {{token}}: the provider decides which credential is checked before the run starts`);
|
|
1610
|
+
}
|
|
1611
|
+
if (violations.length === 0) {
|
|
1612
|
+
violations.push(...templatedConfigViolations(node.id, node.config));
|
|
1613
|
+
}
|
|
1614
|
+
return violations;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
class ExtractionContractError extends Error {
|
|
1618
|
+
}
|
|
1619
|
+
function fail(message) {
|
|
1620
|
+
throw new ExtractionContractError(message);
|
|
1621
|
+
}
|
|
1622
|
+
function unitOrUndefined(value, what) {
|
|
1623
|
+
if (value === undefined)
|
|
1624
|
+
return;
|
|
1625
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
|
|
1626
|
+
fail(`${what} must be a finite number between 0 and 1`);
|
|
1627
|
+
}
|
|
1628
|
+
return value;
|
|
1629
|
+
}
|
|
1630
|
+
function isFiniteNumber(value) {
|
|
1631
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1632
|
+
}
|
|
1633
|
+
function isSpan(value) {
|
|
1634
|
+
return Array.isArray(value) && value.length === 2 && value.every((n) => Number.isInteger(n) && n >= 0) && value[0] <= value[1];
|
|
1635
|
+
}
|
|
1636
|
+
function isPoint(value, dates) {
|
|
1637
|
+
return dates ? typeof value === "string" : isFiniteNumber(value);
|
|
1638
|
+
}
|
|
1639
|
+
function checkScalar(at, field, value) {
|
|
1640
|
+
if (value === null)
|
|
1641
|
+
return;
|
|
1642
|
+
switch (field.type) {
|
|
1643
|
+
case "text":
|
|
1644
|
+
case "date":
|
|
1645
|
+
case "time":
|
|
1646
|
+
case "datetime":
|
|
1647
|
+
if (typeof value !== "string")
|
|
1648
|
+
fail(`${at}.value must be a string or null`);
|
|
1649
|
+
return;
|
|
1650
|
+
case "number":
|
|
1651
|
+
case "duration":
|
|
1652
|
+
if (!isFiniteNumber(value))
|
|
1653
|
+
fail(`${at}.value must be a finite number or null`);
|
|
1654
|
+
if (field.integer && !Number.isInteger(value))
|
|
1655
|
+
fail(`${at}.value must be an integer`);
|
|
1656
|
+
return;
|
|
1657
|
+
default:
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
function validateAnswer(id, field, raw) {
|
|
1662
|
+
const at = `answer "${id}"`;
|
|
1663
|
+
if (!isRecord(raw))
|
|
1664
|
+
fail(`${at} must be an object`);
|
|
1665
|
+
if (raw.type !== field.type) {
|
|
1666
|
+
fail(`${at} has type ${JSON.stringify(raw.type)} but the field declares "${field.type}"`);
|
|
1667
|
+
}
|
|
1668
|
+
if (!Object.hasOwn(raw, "value"))
|
|
1669
|
+
fail(`${at} is missing its value`);
|
|
1670
|
+
const answer = { type: field.type, value: raw.value };
|
|
1671
|
+
switch (field.type) {
|
|
1672
|
+
case "list": {
|
|
1673
|
+
if (!Array.isArray(raw.value))
|
|
1674
|
+
fail(`${at}.value must be a list`);
|
|
1675
|
+
const ok = raw.value.every((item) => field.items === "number" ? isFiniteNumber(item) : typeof item === "string");
|
|
1676
|
+
if (!ok)
|
|
1677
|
+
fail(`${at}.value items must all be ${field.items === "number" ? "numbers" : "strings"}`);
|
|
1678
|
+
if (raw.spans !== undefined) {
|
|
1679
|
+
if (!Array.isArray(raw.spans) || !raw.spans.every(isSpan)) {
|
|
1680
|
+
fail(`${at}.spans must be a list of [start, end] offsets`);
|
|
1681
|
+
}
|
|
1682
|
+
answer.spans = raw.spans;
|
|
1683
|
+
}
|
|
1684
|
+
break;
|
|
1685
|
+
}
|
|
1686
|
+
case "estimate": {
|
|
1687
|
+
const dates = typeof field.min === "string";
|
|
1688
|
+
if (raw.value !== null && !isPoint(raw.value, dates)) {
|
|
1689
|
+
fail(`${at}.value must be ${dates ? "an ISO date" : "a finite number"} or null`);
|
|
1690
|
+
}
|
|
1691
|
+
for (const key of ["median", "p10", "p90"]) {
|
|
1692
|
+
if (!isPoint(raw[key], dates))
|
|
1693
|
+
fail(`${at}.${key} must be ${dates ? "an ISO date" : "a finite number"}`);
|
|
1694
|
+
answer[key] = raw[key];
|
|
1695
|
+
}
|
|
1696
|
+
if (raw.mean !== undefined && isPoint(raw.mean, dates))
|
|
1697
|
+
answer.mean = raw.mean;
|
|
1698
|
+
break;
|
|
1699
|
+
}
|
|
1700
|
+
case "forecast": {
|
|
1701
|
+
const steps = (value) => Array.isArray(value) && value.every(isFiniteNumber);
|
|
1702
|
+
if (!steps(raw.value))
|
|
1703
|
+
fail(`${at}.value must be a list of numbers, one per step`);
|
|
1704
|
+
const length = raw.value.length;
|
|
1705
|
+
if (field.horizon !== undefined && length !== field.horizon) {
|
|
1706
|
+
fail(`${at}.value must have ${field.horizon} step(s), got ${length}`);
|
|
1707
|
+
}
|
|
1708
|
+
for (const key of ["median", "p10", "p90"]) {
|
|
1709
|
+
if (!steps(raw[key]) || raw[key].length !== length) {
|
|
1710
|
+
fail(`${at}.${key} must be a list of ${length} number(s)`);
|
|
1711
|
+
}
|
|
1712
|
+
answer[key] = raw[key];
|
|
1713
|
+
}
|
|
1714
|
+
break;
|
|
1715
|
+
}
|
|
1716
|
+
default:
|
|
1717
|
+
checkScalar(at, field, raw.value);
|
|
1718
|
+
if (raw.span !== undefined) {
|
|
1719
|
+
if (!isSpan(raw.span))
|
|
1720
|
+
fail(`${at}.span must be [start, end] offsets`);
|
|
1721
|
+
answer.span = raw.span;
|
|
1722
|
+
}
|
|
1723
|
+
if (raw.guess !== undefined && raw.guess !== null) {
|
|
1724
|
+
checkScalar(`${at}.guess`, field, raw.guess);
|
|
1725
|
+
answer.guess = raw.guess;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
const pNull = unitOrUndefined(raw.p_null, `${at}.p_null`);
|
|
1729
|
+
if (pNull !== undefined)
|
|
1730
|
+
answer.p_null = pNull;
|
|
1731
|
+
const confidence = unitOrUndefined(raw.confidence, `${at}.confidence`);
|
|
1732
|
+
if (confidence !== undefined)
|
|
1733
|
+
answer.confidence = confidence;
|
|
1734
|
+
if (field.type === "estimate") {
|
|
1735
|
+
const below = unitOrUndefined(raw.p_below, `${at}.p_below`);
|
|
1736
|
+
const above = unitOrUndefined(raw.p_above, `${at}.p_above`);
|
|
1737
|
+
if (below !== undefined)
|
|
1738
|
+
answer.p_below = below;
|
|
1739
|
+
if (above !== undefined)
|
|
1740
|
+
answer.p_above = above;
|
|
1741
|
+
}
|
|
1742
|
+
if (typeof raw.unit === "string" && raw.unit !== "")
|
|
1743
|
+
answer.unit = raw.unit;
|
|
1744
|
+
return answer;
|
|
1745
|
+
}
|
|
1746
|
+
function validateSystemOneExtraction(fields, body, requestId) {
|
|
1747
|
+
if (!isRecord(body))
|
|
1748
|
+
fail("response body must be a JSON object");
|
|
1749
|
+
if (typeof body.model !== "string" || body.model === "") {
|
|
1750
|
+
fail("response is missing the model that produced the answers");
|
|
1751
|
+
}
|
|
1752
|
+
if (!isRecord(body.answers))
|
|
1753
|
+
fail("response is missing the answers object");
|
|
1754
|
+
const usage = object2({ input_tokens: number2().int().min(0), output_tokens: number2().int().min(0) }).safeParse(body.usage);
|
|
1755
|
+
if (!usage.success) {
|
|
1756
|
+
fail("response usage must report non-negative integer input_tokens and output_tokens");
|
|
1757
|
+
}
|
|
1758
|
+
const declared = Object.keys(fields);
|
|
1759
|
+
if (declared.length === 0)
|
|
1760
|
+
fail("a system-one-extraction call needs at least one field");
|
|
1761
|
+
const unexpected = Object.keys(body.answers).filter((id) => !Object.hasOwn(fields, id));
|
|
1762
|
+
if (unexpected.length > 0) {
|
|
1763
|
+
fail(`response contains ${unexpected.length} answer(s) for undeclared fields`);
|
|
1764
|
+
}
|
|
1765
|
+
const answers = {};
|
|
1766
|
+
for (const id of declared) {
|
|
1767
|
+
if (!Object.hasOwn(body.answers, id))
|
|
1768
|
+
fail(`response is missing the answer for field "${id}"`);
|
|
1769
|
+
const field = fields[id];
|
|
1770
|
+
if (!field)
|
|
1771
|
+
fail(`field "${id}" is not defined`);
|
|
1772
|
+
answers[id] = validateAnswer(id, field, body.answers[id]);
|
|
1773
|
+
}
|
|
1774
|
+
return {
|
|
1775
|
+
model: body.model,
|
|
1776
|
+
answers,
|
|
1777
|
+
usage: usage.data,
|
|
1778
|
+
...typeof body.truncated === "boolean" ? { truncated: body.truncated } : {},
|
|
1779
|
+
...requestId ? { requestId } : {}
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
class SystemOneExtractionExecutor extends BaseExecutor {
|
|
1784
|
+
options;
|
|
1785
|
+
type = SYSTEM_ONE_EXTRACTION_NODE_TYPE;
|
|
1786
|
+
mode = "instant";
|
|
1787
|
+
configSchema = SystemOneExtractionConfigSchema;
|
|
1788
|
+
outputSchema = SystemOneExtractionOutputSchema;
|
|
1789
|
+
constructor(deps, options = {}) {
|
|
1790
|
+
super(deps);
|
|
1791
|
+
this.options = options;
|
|
1792
|
+
}
|
|
1793
|
+
providerContext() {
|
|
1794
|
+
return { db: this.deps.db, env: this.options.env ?? process.env };
|
|
1795
|
+
}
|
|
1796
|
+
resolveTarget(id, model) {
|
|
1797
|
+
const { getApiKey, getServerUrl } = this.options;
|
|
1798
|
+
return resolveSystemOneTarget(id, this.providerContext(), {
|
|
1799
|
+
apiKey: getApiKey ? () => getApiKey(id) : undefined,
|
|
1800
|
+
serverUrl: getServerUrl ? () => getServerUrl(id) : undefined,
|
|
1801
|
+
model,
|
|
1802
|
+
capability: "extract"
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
async checkReadiness(nodes) {
|
|
1806
|
+
const nodeIdsByProvider = new Map;
|
|
1807
|
+
for (const node of nodes) {
|
|
1808
|
+
const id = systemOneExtractProviderOf(node.config);
|
|
1809
|
+
if (id)
|
|
1810
|
+
nodeIdsByProvider.set(id, [...nodeIdsByProvider.get(id) ?? [], node.id]);
|
|
1811
|
+
}
|
|
1812
|
+
const problems = [];
|
|
1813
|
+
for (const [id, nodeIds] of nodeIdsByProvider) {
|
|
1814
|
+
const target = await this.resolveTarget(id);
|
|
1815
|
+
if (!target.ok)
|
|
1816
|
+
problems.push({ nodeIds, message: target.error });
|
|
1817
|
+
}
|
|
1818
|
+
return problems;
|
|
1819
|
+
}
|
|
1820
|
+
async execute(config, _context, _meta) {
|
|
1821
|
+
const extracted = await this.extract(config);
|
|
1822
|
+
return extracted.ok ? { status: "success", output: extracted.output } : { status: "failed", error: extracted.error };
|
|
1823
|
+
}
|
|
1824
|
+
async extract(config) {
|
|
1825
|
+
const providerId = config.provider;
|
|
1826
|
+
const service = SYSTEM_ONE_HOSTS[providerId].extract;
|
|
1827
|
+
if (!service) {
|
|
1828
|
+
return { ok: false, error: `SystemOne provider "${providerId}" does not serve extraction` };
|
|
1829
|
+
}
|
|
1830
|
+
const model = config.model ?? service.defaultModel;
|
|
1831
|
+
const modelProblem = systemOneModelProblem(providerId, model, "extract");
|
|
1832
|
+
if (modelProblem)
|
|
1833
|
+
return { ok: false, error: modelProblem };
|
|
1834
|
+
const target = await this.resolveTarget(providerId, model);
|
|
1835
|
+
if (!target.ok)
|
|
1836
|
+
return { ok: false, error: target.error };
|
|
1837
|
+
const { apiKey, endpoint, keyName } = target;
|
|
1838
|
+
const body = JSON.stringify(service.wire.buildRequest(config, model));
|
|
1839
|
+
const scrub = (message) => message.split(apiKey).join("[REDACTED]");
|
|
1840
|
+
const sent = await sendSystemOneRequest({
|
|
1841
|
+
providerId,
|
|
1842
|
+
capability: "extract",
|
|
1843
|
+
endpoint,
|
|
1844
|
+
apiKey,
|
|
1845
|
+
keyName,
|
|
1846
|
+
body,
|
|
1847
|
+
timeoutMs: config.timeoutMs,
|
|
1848
|
+
maxRetries: config.maxRetries,
|
|
1849
|
+
env: this.providerContext().env
|
|
1850
|
+
}, this.options);
|
|
1851
|
+
if (!sent.ok)
|
|
1852
|
+
return { ok: false, error: scrub(sent.error) };
|
|
1853
|
+
let parsed;
|
|
1854
|
+
try {
|
|
1855
|
+
parsed = JSON.parse(sent.text);
|
|
1856
|
+
} catch {
|
|
1857
|
+
return { ok: false, error: "SystemOne response was not valid JSON" };
|
|
1858
|
+
}
|
|
1859
|
+
try {
|
|
1860
|
+
return {
|
|
1861
|
+
ok: true,
|
|
1862
|
+
output: validateSystemOneExtraction(config.fields, service.wire.toExtraction(parsed), sent.requestId)
|
|
1863
|
+
};
|
|
1864
|
+
} catch (err) {
|
|
1865
|
+
const withId = (message) => sent.requestId ? `${message} [request ${sent.requestId}]` : message;
|
|
1866
|
+
if (err instanceof SystemOneWireError)
|
|
1867
|
+
return { ok: false, error: scrub(withId(err.message)) };
|
|
1868
|
+
if (err instanceof ExtractionContractError) {
|
|
1869
|
+
return { ok: false, error: scrub(withId(`${VALIDATION_PREFIX}${err.message}`)) };
|
|
1870
|
+
}
|
|
1871
|
+
throw err;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// src/workflows/executors/system-one-decision.ts
|
|
1877
|
+
var SYSTEM_ONE_DECISION_NODE_TYPE = "system-one-decision";
|
|
1878
|
+
var SYSTEM_ONE_ENDPOINT = SYSTEM_ONE_PROVIDERS.typesafe.endpoint;
|
|
1879
|
+
var SYSTEM_ONE_API_KEY_CONFIG_KEY = SYSTEM_ONE_PROVIDERS.typesafe.keyName;
|
|
1880
|
+
var CHOICE_MIN_OPTIONS = 2;
|
|
1881
|
+
var CHOICE_MAX_OPTIONS = 255;
|
|
1882
|
+
var SCORE_MIN_LEVELS = 2;
|
|
1883
|
+
var SCORE_MAX_LEVELS = 10;
|
|
1884
|
+
var DISTRIBUTION_TOLERANCE = 0.02;
|
|
990
1885
|
var REVIEW_PORTS = ["approved", "rejected", "timeout"];
|
|
991
1886
|
var ENGINE_RETRY_DEFAULT = 3;
|
|
992
1887
|
var SystemOneEntrySchema = union2([
|
|
@@ -1028,7 +1923,7 @@ var SystemOneQuestionSchema = discriminatedUnion("type", [
|
|
|
1028
1923
|
ChoiceQuestionSchema,
|
|
1029
1924
|
ScoreQuestionSchema
|
|
1030
1925
|
]);
|
|
1031
|
-
var
|
|
1926
|
+
var Unit2 = number2().min(0).max(1);
|
|
1032
1927
|
var QUESTION_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
1033
1928
|
var RESERVED_QUESTION_IDS = new Set([
|
|
1034
1929
|
...Object.getOwnPropertyNames(Object.prototype),
|
|
@@ -1036,7 +1931,7 @@ var RESERVED_QUESTION_IDS = new Set([
|
|
|
1036
1931
|
]);
|
|
1037
1932
|
var SystemOnePrimitiveTypeSchema = _enum2(["noul", "choice", "score"]);
|
|
1038
1933
|
var HumanReviewSchema = strictObject2({
|
|
1039
|
-
band: strictObject2({ min:
|
|
1934
|
+
band: strictObject2({ min: Unit2, max: Unit2 }).refine((band) => band.min <= band.max, { message: "band.min must not exceed band.max" }),
|
|
1040
1935
|
approvers: ApproverConfigSchema,
|
|
1041
1936
|
title: string2().min(1).optional(),
|
|
1042
1937
|
timeout: HITLTimeoutSchema.optional(),
|
|
@@ -1099,19 +1994,19 @@ var SystemOneDecisionConfigSchema = strictObject2({
|
|
|
1099
1994
|
if (limitProblem)
|
|
1100
1995
|
ctx.addIssue({ code: "custom", path: ["questions"], message: limitProblem });
|
|
1101
1996
|
});
|
|
1102
|
-
var NoulAnswerSchema = strictObject2({ type: literal2("noul"), noul:
|
|
1997
|
+
var NoulAnswerSchema = strictObject2({ type: literal2("noul"), noul: Unit2 });
|
|
1103
1998
|
var ChoiceAnswerSchema = strictObject2({
|
|
1104
1999
|
type: literal2("choice"),
|
|
1105
2000
|
choice: string2(),
|
|
1106
|
-
probabilities: record2(string2(),
|
|
1107
|
-
confidence:
|
|
2001
|
+
probabilities: record2(string2(), Unit2),
|
|
2002
|
+
confidence: Unit2
|
|
1108
2003
|
});
|
|
1109
2004
|
var ScoreAnswerSchema = strictObject2({
|
|
1110
2005
|
type: literal2("score"),
|
|
1111
2006
|
score: number2(),
|
|
1112
2007
|
legend: record2(string2(), SystemOneEntrySchema),
|
|
1113
|
-
probabilities: record2(string2(),
|
|
1114
|
-
confidence:
|
|
2008
|
+
probabilities: record2(string2(), Unit2),
|
|
2009
|
+
confidence: Unit2
|
|
1115
2010
|
});
|
|
1116
2011
|
var SystemOneAnswerSchema = discriminatedUnion("type", [
|
|
1117
2012
|
NoulAnswerSchema,
|
|
@@ -1119,7 +2014,7 @@ var SystemOneAnswerSchema = discriminatedUnion("type", [
|
|
|
1119
2014
|
ScoreAnswerSchema
|
|
1120
2015
|
]);
|
|
1121
2016
|
var ReviewQuestionSchema = strictObject2({
|
|
1122
|
-
confidence:
|
|
2017
|
+
confidence: Unit2,
|
|
1123
2018
|
inBand: boolean(),
|
|
1124
2019
|
decidedBy: _enum2(["model", "human"]),
|
|
1125
2020
|
modelAnswer: union2([string2(), number2()]).optional()
|
|
@@ -1141,16 +2036,23 @@ var SystemOneDecisionOutputSchema = object2({
|
|
|
1141
2036
|
routing: strictObject2({ model: string2().min(1) }).optional(),
|
|
1142
2037
|
review: ReviewSchema.optional()
|
|
1143
2038
|
});
|
|
2039
|
+
var SYSTEM_ONE_NODE_LABELS = {
|
|
2040
|
+
[SYSTEM_ONE_DECISION_NODE_TYPE]: "SystemOne Decision",
|
|
2041
|
+
[SYSTEM_ONE_EXTRACTION_NODE_TYPE]: "SystemOne Extraction"
|
|
2042
|
+
};
|
|
2043
|
+
function isSystemOneNodeType(type) {
|
|
2044
|
+
return Object.hasOwn(SYSTEM_ONE_NODE_LABELS, type);
|
|
2045
|
+
}
|
|
1144
2046
|
function systemOneRetryViolations(node) {
|
|
1145
|
-
if (node.type
|
|
2047
|
+
if (!isSystemOneNodeType(node.type))
|
|
1146
2048
|
return [];
|
|
1147
2049
|
const violations = [];
|
|
1148
2050
|
const attempts = (retry) => retry ? retry.maxRetries ?? ENGINE_RETRY_DEFAULT : 0;
|
|
1149
2051
|
if (attempts(node.retry) > 0) {
|
|
1150
|
-
violations.push(`Node "${node.id}" (
|
|
2052
|
+
violations.push(`Node "${node.id}" (${node.type}) must not set retry.maxRetries > 0: the ${node.type} executor retries transient transport errors itself (config.maxRetries), and engine retries would re-send rejected requests`);
|
|
1151
2053
|
}
|
|
1152
2054
|
if (attempts(node.validation?.retry) > 0) {
|
|
1153
|
-
violations.push(`Node "${node.id}" (
|
|
2055
|
+
violations.push(`Node "${node.id}" (${node.type}) must not set validation.retry.maxRetries > 0: a validation-driven retry would send another paid request`);
|
|
1154
2056
|
}
|
|
1155
2057
|
return violations;
|
|
1156
2058
|
}
|
|
@@ -1184,14 +2086,15 @@ function systemOneStaticShapeViolations(node) {
|
|
|
1184
2086
|
}
|
|
1185
2087
|
return violations;
|
|
1186
2088
|
}
|
|
1187
|
-
function systemOneUnresolvedError(nodeId, tokens) {
|
|
2089
|
+
function systemOneUnresolvedError(nodeId, tokens, nodeType = SYSTEM_ONE_DECISION_NODE_TYPE) {
|
|
1188
2090
|
const rendered = [...new Set(tokens)].map((token) => `{{${token}}}`).join(", ");
|
|
1189
|
-
|
|
2091
|
+
const label = SYSTEM_ONE_NODE_LABELS[nodeType] ?? SYSTEM_ONE_NODE_LABELS[SYSTEM_ONE_DECISION_NODE_TYPE];
|
|
2092
|
+
return `${label} 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.";
|
|
1190
2093
|
}
|
|
1191
2094
|
|
|
1192
2095
|
class SystemOneContractError extends Error {
|
|
1193
2096
|
}
|
|
1194
|
-
function
|
|
2097
|
+
function fail2(message) {
|
|
1195
2098
|
throw new SystemOneContractError(message);
|
|
1196
2099
|
}
|
|
1197
2100
|
function isRecord2(value) {
|
|
@@ -1199,18 +2102,18 @@ function isRecord2(value) {
|
|
|
1199
2102
|
}
|
|
1200
2103
|
function unit(value, what) {
|
|
1201
2104
|
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
|
|
1202
|
-
|
|
2105
|
+
fail2(`${what} must be a finite number between 0 and 1`);
|
|
1203
2106
|
}
|
|
1204
2107
|
return value;
|
|
1205
2108
|
}
|
|
1206
2109
|
function distribution(value, keys, what) {
|
|
1207
2110
|
if (!isRecord2(value))
|
|
1208
|
-
|
|
2111
|
+
fail2(`${what} must be an object`);
|
|
1209
2112
|
const actual = Object.keys(value);
|
|
1210
2113
|
const missing = keys.filter((key) => !Object.hasOwn(value, key));
|
|
1211
2114
|
const extra = actual.filter((key) => !keys.includes(key));
|
|
1212
2115
|
if (missing.length > 0 || extra.length > 0) {
|
|
1213
|
-
|
|
2116
|
+
fail2(`${what} keys must be exactly the declared ${keys.length} option(s)` + (missing.length > 0 ? `; missing ${missing.length}` : "") + (extra.length > 0 ? `; ${extra.length} unexpected` : ""));
|
|
1214
2117
|
}
|
|
1215
2118
|
const out = {};
|
|
1216
2119
|
let sum = 0;
|
|
@@ -1220,16 +2123,16 @@ function distribution(value, keys, what) {
|
|
|
1220
2123
|
sum += p;
|
|
1221
2124
|
}
|
|
1222
2125
|
if (Math.abs(sum - 1) > DISTRIBUTION_TOLERANCE) {
|
|
1223
|
-
|
|
2126
|
+
fail2(`${what} must sum to 1 (within ${DISTRIBUTION_TOLERANCE})`);
|
|
1224
2127
|
}
|
|
1225
2128
|
return out;
|
|
1226
2129
|
}
|
|
1227
|
-
function
|
|
2130
|
+
function validateAnswer2(id, question, raw, confidenceField) {
|
|
1228
2131
|
const at = `answer "${id}"`;
|
|
1229
2132
|
if (!isRecord2(raw))
|
|
1230
|
-
|
|
2133
|
+
fail2(`${at} must be an object`);
|
|
1231
2134
|
if (raw.type !== question.type) {
|
|
1232
|
-
|
|
2135
|
+
fail2(`${at} has type ${JSON.stringify(raw.type)} but the question declares "${question.type}"`);
|
|
1233
2136
|
}
|
|
1234
2137
|
switch (question.type) {
|
|
1235
2138
|
case "noul":
|
|
@@ -1237,7 +2140,7 @@ function validateAnswer(id, question, raw, confidenceField) {
|
|
|
1237
2140
|
case "choice": {
|
|
1238
2141
|
const options = Object.keys(question.criteria);
|
|
1239
2142
|
if (typeof raw.choice !== "string" || !Object.hasOwn(question.criteria, raw.choice)) {
|
|
1240
|
-
|
|
2143
|
+
fail2(`${at}.choice is not one of the declared options`);
|
|
1241
2144
|
}
|
|
1242
2145
|
return {
|
|
1243
2146
|
type: "choice",
|
|
@@ -1250,17 +2153,17 @@ function validateAnswer(id, question, raw, confidenceField) {
|
|
|
1250
2153
|
const indices = question.criteria.map((_, index) => String(index));
|
|
1251
2154
|
const top = indices.length - 1;
|
|
1252
2155
|
if (typeof raw.score !== "number" || !Number.isFinite(raw.score) || raw.score < 0 || raw.score > top) {
|
|
1253
|
-
|
|
2156
|
+
fail2(`${at}.score must be a finite number between 0 and ${top}`);
|
|
1254
2157
|
}
|
|
1255
2158
|
if (!isRecord2(raw.legend))
|
|
1256
|
-
|
|
2159
|
+
fail2(`${at}.legend must be an object`);
|
|
1257
2160
|
const legend = {};
|
|
1258
2161
|
for (const key of indices) {
|
|
1259
2162
|
if (!Object.hasOwn(raw.legend, key))
|
|
1260
|
-
|
|
2163
|
+
fail2(`${at}.legend is missing level ${key}`);
|
|
1261
2164
|
const entry = SystemOneEntrySchema.safeParse(raw.legend[key]);
|
|
1262
2165
|
if (!entry.success)
|
|
1263
|
-
|
|
2166
|
+
fail2(`${at}.legend["${key}"] must be a string, object, array, or null`);
|
|
1264
2167
|
legend[key] = entry.data;
|
|
1265
2168
|
}
|
|
1266
2169
|
return {
|
|
@@ -1282,30 +2185,30 @@ function readRoutingModel(routing) {
|
|
|
1282
2185
|
}
|
|
1283
2186
|
function validateSystemOneResponse(questions, body, requestId, confidenceField = "confidence") {
|
|
1284
2187
|
if (!isRecord2(body))
|
|
1285
|
-
|
|
2188
|
+
fail2("response body must be a JSON object");
|
|
1286
2189
|
if (typeof body.model !== "string" || body.model === "") {
|
|
1287
|
-
|
|
2190
|
+
fail2("response is missing the model that produced the answers");
|
|
1288
2191
|
}
|
|
1289
2192
|
if (!isRecord2(body.answers))
|
|
1290
|
-
|
|
2193
|
+
fail2("response is missing the answers object");
|
|
1291
2194
|
const usage = object2({ input_tokens: number2().int().min(0), output_tokens: number2().int().min(0) }).safeParse(body.usage);
|
|
1292
2195
|
if (!usage.success)
|
|
1293
|
-
|
|
2196
|
+
fail2("response usage must report non-negative integer input_tokens and output_tokens");
|
|
1294
2197
|
const declared = Object.keys(questions);
|
|
1295
2198
|
const unexpected = Object.keys(body.answers).filter((id) => !Object.hasOwn(questions, id));
|
|
1296
2199
|
if (unexpected.length > 0)
|
|
1297
|
-
|
|
2200
|
+
fail2(`response contains ${unexpected.length} answer(s) for undeclared questions`);
|
|
1298
2201
|
const answers = {};
|
|
1299
2202
|
for (const id of declared) {
|
|
1300
2203
|
if (!Object.hasOwn(body.answers, id))
|
|
1301
|
-
|
|
2204
|
+
fail2(`response is missing the answer for question "${id}"`);
|
|
1302
2205
|
const question = questions[id];
|
|
1303
2206
|
if (!question)
|
|
1304
|
-
|
|
1305
|
-
answers[id] =
|
|
2207
|
+
fail2(`question "${id}" is not defined`);
|
|
2208
|
+
answers[id] = validateAnswer2(id, question, body.answers[id], confidenceField);
|
|
1306
2209
|
}
|
|
1307
2210
|
if (declared.length === 0)
|
|
1308
|
-
|
|
2211
|
+
fail2("a system-one-decision call needs at least one question");
|
|
1309
2212
|
const routingModel = readRoutingModel(body.routing);
|
|
1310
2213
|
return {
|
|
1311
2214
|
model: body.model,
|
|
@@ -1456,59 +2359,6 @@ function resolveReviewedDecision(parkedOutput, approval) {
|
|
|
1456
2359
|
}
|
|
1457
2360
|
return finish("approved", answers, questions);
|
|
1458
2361
|
}
|
|
1459
|
-
var REQUEST_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
1460
|
-
var ERROR_CODE_RE2 = /^[A-Za-z0-9._:-]{1,64}$/;
|
|
1461
|
-
function readRequestId(headers) {
|
|
1462
|
-
const value = headers.get("x-request-id") ?? headers.get("request-id") ?? headers.get("cf-ray");
|
|
1463
|
-
return value && REQUEST_ID_RE.test(value) ? value : undefined;
|
|
1464
|
-
}
|
|
1465
|
-
function parseRetryAfter(value, now) {
|
|
1466
|
-
if (!value)
|
|
1467
|
-
return;
|
|
1468
|
-
const trimmed = value.trim();
|
|
1469
|
-
if (/^\d+(\.\d+)?$/.test(trimmed))
|
|
1470
|
-
return Math.round(Number(trimmed) * 1000);
|
|
1471
|
-
const at = Date.parse(trimmed);
|
|
1472
|
-
return Number.isNaN(at) ? undefined : Math.max(0, at - now);
|
|
1473
|
-
}
|
|
1474
|
-
function readErrorCode(text) {
|
|
1475
|
-
if (text.length === 0 || text.length > MAX_ERROR_BODY_CHARS)
|
|
1476
|
-
return;
|
|
1477
|
-
let parsed;
|
|
1478
|
-
try {
|
|
1479
|
-
parsed = JSON.parse(text);
|
|
1480
|
-
} catch {
|
|
1481
|
-
return;
|
|
1482
|
-
}
|
|
1483
|
-
if (!isRecord2(parsed))
|
|
1484
|
-
return;
|
|
1485
|
-
const nested = isRecord2(parsed.error) ? parsed.error : undefined;
|
|
1486
|
-
const detail = isRecord2(parsed.detail) ? parsed.detail : undefined;
|
|
1487
|
-
for (const candidate of [
|
|
1488
|
-
nested?.code,
|
|
1489
|
-
nested?.type,
|
|
1490
|
-
detail?.error_type,
|
|
1491
|
-
parsed.code,
|
|
1492
|
-
parsed.type
|
|
1493
|
-
]) {
|
|
1494
|
-
if (typeof candidate === "string" && ERROR_CODE_RE2.test(candidate))
|
|
1495
|
-
return candidate;
|
|
1496
|
-
}
|
|
1497
|
-
return cloudflareErrorCode(parsed);
|
|
1498
|
-
}
|
|
1499
|
-
function defaultSleep(ms, signal) {
|
|
1500
|
-
return new Promise((resolve) => {
|
|
1501
|
-
if (signal.aborted)
|
|
1502
|
-
return resolve();
|
|
1503
|
-
const timer = setTimeout(done, ms);
|
|
1504
|
-
function done() {
|
|
1505
|
-
clearTimeout(timer);
|
|
1506
|
-
signal.removeEventListener("abort", done);
|
|
1507
|
-
resolve();
|
|
1508
|
-
}
|
|
1509
|
-
signal.addEventListener("abort", done, { once: true });
|
|
1510
|
-
});
|
|
1511
|
-
}
|
|
1512
2362
|
|
|
1513
2363
|
class SystemOneDecisionExecutor extends BaseExecutor {
|
|
1514
2364
|
options;
|
|
@@ -1569,11 +2419,21 @@ class SystemOneDecisionExecutor extends BaseExecutor {
|
|
|
1569
2419
|
const target = await this.resolveTarget(providerId, model);
|
|
1570
2420
|
if (!target.ok)
|
|
1571
2421
|
return { ok: false, error: target.error };
|
|
1572
|
-
const { apiKey, endpoint } = target;
|
|
2422
|
+
const { apiKey, endpoint, keyName } = target;
|
|
1573
2423
|
const wire = provider.wire ?? SYSTEM_ONE_WIRE;
|
|
1574
2424
|
const body = JSON.stringify(wire.buildRequest(config, model));
|
|
1575
2425
|
const scrub = (message) => message.split(apiKey).join("[REDACTED]");
|
|
1576
|
-
const sent = await
|
|
2426
|
+
const sent = await sendSystemOneRequest({
|
|
2427
|
+
providerId,
|
|
2428
|
+
capability: "decide",
|
|
2429
|
+
endpoint,
|
|
2430
|
+
apiKey,
|
|
2431
|
+
keyName,
|
|
2432
|
+
body,
|
|
2433
|
+
timeoutMs: config.timeoutMs,
|
|
2434
|
+
maxRetries: config.maxRetries,
|
|
2435
|
+
env: this.providerContext().env
|
|
2436
|
+
}, this.options);
|
|
1577
2437
|
if (!sent.ok)
|
|
1578
2438
|
return { ok: false, error: scrub(sent.error) };
|
|
1579
2439
|
let parsed;
|
|
@@ -1663,128 +2523,6 @@ class SystemOneDecisionExecutor extends BaseExecutor {
|
|
|
1663
2523
|
resolveApproval(parkedOutput, approval) {
|
|
1664
2524
|
return resolveReviewedDecision(parkedOutput, approval);
|
|
1665
2525
|
}
|
|
1666
|
-
async send(providerId, endpoint, apiKey, body, timeoutMs, maxRetries) {
|
|
1667
|
-
const doFetch = this.options.fetch ?? fetch;
|
|
1668
|
-
const sleep = this.options.sleep ?? defaultSleep;
|
|
1669
|
-
const now = this.options.now ?? Date.now;
|
|
1670
|
-
const random = this.options.random ?? Math.random;
|
|
1671
|
-
const deadlineAt = now() + timeoutMs - DEADLINE_MARGIN_MS;
|
|
1672
|
-
const controller = new AbortController;
|
|
1673
|
-
const timer = setTimeout(() => controller.abort(), Math.max(0, deadlineAt - now()));
|
|
1674
|
-
const timedOut = () => ({
|
|
1675
|
-
ok: false,
|
|
1676
|
-
error: `SystemOne request exceeded its ${timeoutMs}ms time budget`
|
|
1677
|
-
});
|
|
1678
|
-
try {
|
|
1679
|
-
for (let attempt = 0;; attempt++) {
|
|
1680
|
-
const result = await this.attemptOnce(doFetch, providerId, endpoint, apiKey, body, controller.signal, now);
|
|
1681
|
-
if (result.kind === "ok") {
|
|
1682
|
-
return { ok: true, text: result.text, requestId: result.requestId };
|
|
1683
|
-
}
|
|
1684
|
-
if (result.kind === "timeout" || controller.signal.aborted)
|
|
1685
|
-
return timedOut();
|
|
1686
|
-
const attempts = attempt + 1;
|
|
1687
|
-
const suffix = attempts > 1 ? ` after ${attempts} attempts` : "";
|
|
1688
|
-
if (!result.retryable || attempt >= maxRetries) {
|
|
1689
|
-
return { ok: false, error: `${result.message}${suffix}` };
|
|
1690
|
-
}
|
|
1691
|
-
const backoff = Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** attempt);
|
|
1692
|
-
const delay = result.retryAfterMs ?? Math.round(backoff * (0.5 + random() * 0.5));
|
|
1693
|
-
if (delay >= deadlineAt - now()) {
|
|
1694
|
-
return {
|
|
1695
|
-
ok: false,
|
|
1696
|
-
error: `${result.message}${suffix}; the ${delay}ms retry delay does not fit the ${timeoutMs}ms time budget`
|
|
1697
|
-
};
|
|
1698
|
-
}
|
|
1699
|
-
await sleep(delay, controller.signal);
|
|
1700
|
-
if (controller.signal.aborted)
|
|
1701
|
-
return timedOut();
|
|
1702
|
-
}
|
|
1703
|
-
} finally {
|
|
1704
|
-
clearTimeout(timer);
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
async attemptOnce(doFetch, providerId, endpoint, apiKey, body, signal, now) {
|
|
1708
|
-
let res;
|
|
1709
|
-
try {
|
|
1710
|
-
res = await doFetch(endpoint, {
|
|
1711
|
-
method: "POST",
|
|
1712
|
-
headers: {
|
|
1713
|
-
...getOpenRouterAttributionHeaders(endpoint, this.providerContext().env),
|
|
1714
|
-
"content-type": "application/json",
|
|
1715
|
-
accept: "application/json",
|
|
1716
|
-
authorization: `Bearer ${apiKey}`
|
|
1717
|
-
},
|
|
1718
|
-
body,
|
|
1719
|
-
redirect: "manual",
|
|
1720
|
-
signal
|
|
1721
|
-
});
|
|
1722
|
-
} catch (err) {
|
|
1723
|
-
if (signal.aborted)
|
|
1724
|
-
return { kind: "timeout" };
|
|
1725
|
-
const code = err?.code;
|
|
1726
|
-
const detail = typeof code === "string" && /^[A-Z0-9_]{3,40}$/.test(code) ? ` (${code})` : "";
|
|
1727
|
-
return {
|
|
1728
|
-
kind: "fail",
|
|
1729
|
-
message: `SystemOne request failed: network error${detail}`,
|
|
1730
|
-
retryable: true
|
|
1731
|
-
};
|
|
1732
|
-
}
|
|
1733
|
-
const requestId = readRequestId(res.headers);
|
|
1734
|
-
const withId = (message) => requestId ? `${message} [request ${requestId}]` : message;
|
|
1735
|
-
if (res.status >= 200 && res.status < 300) {
|
|
1736
|
-
let text;
|
|
1737
|
-
try {
|
|
1738
|
-
text = await res.text();
|
|
1739
|
-
} catch {
|
|
1740
|
-
if (signal.aborted)
|
|
1741
|
-
return { kind: "timeout" };
|
|
1742
|
-
return {
|
|
1743
|
-
kind: "fail",
|
|
1744
|
-
message: withId("SystemOne response body could not be read"),
|
|
1745
|
-
retryable: true
|
|
1746
|
-
};
|
|
1747
|
-
}
|
|
1748
|
-
if (text.length > MAX_RESPONSE_CHARS) {
|
|
1749
|
-
return {
|
|
1750
|
-
kind: "fail",
|
|
1751
|
-
message: withId("SystemOne response exceeded the size limit"),
|
|
1752
|
-
retryable: false
|
|
1753
|
-
};
|
|
1754
|
-
}
|
|
1755
|
-
return { kind: "ok", text, requestId };
|
|
1756
|
-
}
|
|
1757
|
-
if (res.status >= 300 && res.status < 400) {
|
|
1758
|
-
return {
|
|
1759
|
-
kind: "fail",
|
|
1760
|
-
message: withId(`SystemOne request was redirected (HTTP ${res.status}); redirects are refused`),
|
|
1761
|
-
retryable: false
|
|
1762
|
-
};
|
|
1763
|
-
}
|
|
1764
|
-
let errorText = "";
|
|
1765
|
-
try {
|
|
1766
|
-
errorText = await res.text();
|
|
1767
|
-
} catch {
|
|
1768
|
-
if (signal.aborted)
|
|
1769
|
-
return { kind: "timeout" };
|
|
1770
|
-
}
|
|
1771
|
-
const code = readErrorCode(errorText);
|
|
1772
|
-
if (res.status === 401 || res.status === 403) {
|
|
1773
|
-
return {
|
|
1774
|
-
kind: "fail",
|
|
1775
|
-
message: withId(systemOneKeyRejectedMessage(providerId, res.status, code)),
|
|
1776
|
-
retryable: false
|
|
1777
|
-
};
|
|
1778
|
-
}
|
|
1779
|
-
const detail = code ? ` (${code})` : "";
|
|
1780
|
-
const retryable = res.status === 408 || res.status === 429 || res.status >= 500;
|
|
1781
|
-
return {
|
|
1782
|
-
kind: "fail",
|
|
1783
|
-
message: withId(`SystemOne API returned HTTP ${res.status}${detail}`),
|
|
1784
|
-
retryable,
|
|
1785
|
-
retryAfterMs: retryable ? parseRetryAfter(res.headers.get("retry-after"), now()) : undefined
|
|
1786
|
-
};
|
|
1787
|
-
}
|
|
1788
2526
|
}
|
|
1789
2527
|
|
|
1790
2528
|
// src/workflows/definition.ts
|
|
@@ -1919,7 +2657,7 @@ function validateDefinition(def, registry, options = {}) {
|
|
|
1919
2657
|
if (node.type === "human-in-the-loop" && typeof node.config.questions === "string" && !/^\{\{[^}]+\}\}$/.test(node.config.questions.trim())) {
|
|
1920
2658
|
errors.push(`Node "${node.id}" human-in-the-loop config.questions must be an array or one exact {{interpolation}} token`);
|
|
1921
2659
|
}
|
|
1922
|
-
errors.push(...systemOneStaticShapeViolations(node), ...systemOneRetryViolations(node));
|
|
2660
|
+
errors.push(...systemOneStaticShapeViolations(node), ...systemOneExtractionStaticShapeViolations(node), ...systemOneRetryViolations(node));
|
|
1923
2661
|
if (node.type === "foreach") {
|
|
1924
2662
|
validateForeachNode(node, errors);
|
|
1925
2663
|
if (node.id.includes("#")) {
|
|
@@ -2120,4 +2858,4 @@ function applyDefinitionPatch(def, patch) {
|
|
|
2120
2858
|
return { definition: patchedDef, errors };
|
|
2121
2859
|
}
|
|
2122
2860
|
|
|
2123
|
-
export { BaseExecutor, postApprovalCancellationUpdates, createApprovalFollowUpTask, HumanInTheLoopExecutor, resolveWorkflowLlmConfig, SYSTEM_ONE_PROVIDER_IDS, SYSTEM_ONE_DECISION_NODE_TYPE, SystemOneDecisionConfigSchema, systemOneRetryViolations, systemOneUnresolvedError, SystemOneDecisionExecutor, getNextTargets, generateEdges, findEntryNodes, resolveValidationPort, getSuccessors, definitionNodeIds, validateDefinition, applyDefinitionPatch };
|
|
2861
|
+
export { BaseExecutor, postApprovalCancellationUpdates, createApprovalFollowUpTask, HumanInTheLoopExecutor, resolveWorkflowLlmConfig, SYSTEM_ONE_PROVIDER_IDS, SystemOneExtractionExecutor, SYSTEM_ONE_DECISION_NODE_TYPE, SystemOneDecisionConfigSchema, isSystemOneNodeType, systemOneRetryViolations, systemOneUnresolvedError, SystemOneDecisionExecutor, getNextTargets, generateEdges, findEntryNodes, resolveValidationPort, getSuccessors, definitionNodeIds, validateDefinition, applyDefinitionPatch };
|