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