@relayflows/sdk 2.0.42 → 2.0.43
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/adapters/base.d.ts +12 -1
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/base.js +11 -1
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +8 -4
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/codex.d.ts.map +1 -1
- package/dist/adapters/codex.js +15 -6
- package/dist/adapters/codex.js.map +1 -1
- package/dist/agent-resume.d.ts +8 -0
- package/dist/agent-resume.d.ts.map +1 -0
- package/dist/agent-resume.js +37 -0
- package/dist/agent-resume.js.map +1 -0
- package/dist/agent-transcript.d.ts.map +1 -1
- package/dist/agent-transcript.js +5 -3
- package/dist/agent-transcript.js.map +1 -1
- package/dist/authored-flow-error.d.ts +10 -2
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +16 -2
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-loader.d.ts +6 -0
- package/dist/authored-flow-loader.d.ts.map +1 -1
- package/dist/authored-flow-loader.js +18 -6
- package/dist/authored-flow-loader.js.map +1 -1
- package/dist/authored-helper-effect.js +3 -2
- package/dist/authored-helper-effect.js.map +1 -1
- package/dist/authored-human.d.ts +2 -0
- package/dist/authored-human.d.ts.map +1 -1
- package/dist/authored-human.js +4 -0
- package/dist/authored-human.js.map +1 -1
- package/dist/authored-memory.d.ts +47 -2
- package/dist/authored-memory.d.ts.map +1 -1
- package/dist/authored-memory.js +394 -15
- package/dist/authored-memory.js.map +1 -1
- package/dist/authored-node-entry.js +6 -1
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +6 -3
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-root-readback.d.ts +21 -0
- package/dist/authored-root-readback.d.ts.map +1 -0
- package/dist/authored-root-readback.js +90 -0
- package/dist/authored-root-readback.js.map +1 -0
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +53 -74
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-step-output.d.ts +2 -0
- package/dist/authored-step-output.d.ts.map +1 -1
- package/dist/authored-step-output.js +1 -0
- package/dist/authored-step-output.js.map +1 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +26 -4
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check-helper-body.d.ts +2 -2
- package/dist/cli/check-helper-body.d.ts.map +1 -1
- package/dist/cli/check-helper-body.js +7 -2
- package/dist/cli/check-helper-body.js.map +1 -1
- package/dist/cli/check-helper-surface.d.ts +8 -0
- package/dist/cli/check-helper-surface.d.ts.map +1 -0
- package/dist/cli/check-helper-surface.js +37 -0
- package/dist/cli/check-helper-surface.js.map +1 -0
- package/dist/cli/check-triggers.d.ts +4 -3
- package/dist/cli/check-triggers.d.ts.map +1 -1
- package/dist/cli/check-triggers.js +7 -3
- package/dist/cli/check-triggers.js.map +1 -1
- package/dist/cli/check.d.ts +7 -1
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +35 -8
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-answer.d.ts +9 -0
- package/dist/cli/cloud-answer.d.ts.map +1 -0
- package/dist/cli/cloud-answer.js +39 -0
- package/dist/cli/cloud-answer.js.map +1 -0
- package/dist/cli/cloud-connect-cli.d.ts +2 -1
- package/dist/cli/cloud-connect-cli.d.ts.map +1 -1
- package/dist/cli/cloud-connect-cli.js +10 -2
- package/dist/cli/cloud-connect-cli.js.map +1 -1
- package/dist/cli/cloud-human.d.ts +7 -0
- package/dist/cli/cloud-human.d.ts.map +1 -0
- package/dist/cli/cloud-human.js +22 -0
- package/dist/cli/cloud-human.js.map +1 -0
- package/dist/cli/cloud-live.d.ts +2 -2
- package/dist/cli/cloud-live.d.ts.map +1 -1
- package/dist/cli/cloud-live.js +9 -2
- package/dist/cli/cloud-live.js.map +1 -1
- package/dist/cli/cloud-mirror-session.d.ts.map +1 -1
- package/dist/cli/cloud-mirror-session.js +3 -2
- package/dist/cli/cloud-mirror-session.js.map +1 -1
- package/dist/cli/cloud-run.d.ts +1 -1
- package/dist/cli/cloud-run.d.ts.map +1 -1
- package/dist/cli/cloud-run.js +12 -0
- package/dist/cli/cloud-run.js.map +1 -1
- package/dist/cli/detached-record.d.ts +22 -0
- package/dist/cli/detached-record.d.ts.map +1 -0
- package/dist/cli/detached-record.js +57 -0
- package/dist/cli/detached-record.js.map +1 -0
- package/dist/cli/detached-run.d.ts +28 -0
- package/dist/cli/detached-run.d.ts.map +1 -0
- package/dist/cli/detached-run.js +122 -0
- package/dist/cli/detached-run.js.map +1 -0
- package/dist/cli/diagnostic-fold.d.ts +19 -0
- package/dist/cli/diagnostic-fold.d.ts.map +1 -0
- package/dist/cli/diagnostic-fold.js +19 -0
- package/dist/cli/diagnostic-fold.js.map +1 -0
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +16 -2
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/eval.d.ts +22 -0
- package/dist/cli/eval.d.ts.map +1 -0
- package/dist/cli/eval.js +131 -0
- package/dist/cli/eval.js.map +1 -0
- package/dist/cli/journal-timeout.d.ts +12 -0
- package/dist/cli/journal-timeout.d.ts.map +1 -0
- package/dist/cli/journal-timeout.js +42 -0
- package/dist/cli/journal-timeout.js.map +1 -0
- package/dist/cli/local-agent-remedy.d.ts.map +1 -1
- package/dist/cli/local-agent-remedy.js +5 -4
- package/dist/cli/local-agent-remedy.js.map +1 -1
- package/dist/cli/observer-session.d.ts +1 -0
- package/dist/cli/observer-session.d.ts.map +1 -1
- package/dist/cli/observer-session.js +8 -1
- package/dist/cli/observer-session.js.map +1 -1
- package/dist/cli/reuse.d.ts +8 -0
- package/dist/cli/reuse.d.ts.map +1 -1
- package/dist/cli/reuse.js +24 -0
- package/dist/cli/reuse.js.map +1 -1
- package/dist/cli/run.d.ts +8 -2
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +72 -56
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/running-step.d.ts +9 -0
- package/dist/cli/running-step.d.ts.map +1 -0
- package/dist/cli/running-step.js +156 -0
- package/dist/cli/running-step.js.map +1 -0
- package/dist/cli-adapter.d.ts +3 -1
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +6 -2
- package/dist/cli-adapter.js.map +1 -1
- package/dist/cli-commands.d.ts +77 -11
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +41 -7
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli-watch.d.ts +6 -2
- package/dist/cli-watch.d.ts.map +1 -1
- package/dist/cli-watch.js +7 -6
- package/dist/cli-watch.js.map +1 -1
- package/dist/cli.d.ts +22 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +154 -50
- package/dist/cli.js.map +1 -1
- package/dist/cloud-answer.d.ts +35 -0
- package/dist/cloud-answer.d.ts.map +1 -0
- package/dist/cloud-answer.js +194 -0
- package/dist/cloud-answer.js.map +1 -0
- package/dist/cloud-auth-store.d.ts +46 -0
- package/dist/cloud-auth-store.d.ts.map +1 -0
- package/dist/cloud-auth-store.js +171 -0
- package/dist/cloud-auth-store.js.map +1 -0
- package/dist/cloud-http.d.ts +7 -16
- package/dist/cloud-http.d.ts.map +1 -1
- package/dist/cloud-http.js +59 -40
- package/dist/cloud-http.js.map +1 -1
- package/dist/cloud-human-wait.d.ts +10 -0
- package/dist/cloud-human-wait.d.ts.map +1 -0
- package/dist/cloud-human-wait.js +17 -0
- package/dist/cloud-human-wait.js.map +1 -0
- package/dist/cloud-mirror-transport.d.ts.map +1 -1
- package/dist/cloud-mirror-transport.js +1 -0
- package/dist/cloud-mirror-transport.js.map +1 -1
- package/dist/cloud-run-record.d.ts +7 -2
- package/dist/cloud-run-record.d.ts.map +1 -1
- package/dist/cloud-run-record.js +13 -5
- package/dist/cloud-run-record.js.map +1 -1
- package/dist/cloud-run.d.ts +9 -0
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +36 -2
- package/dist/cloud-run.js.map +1 -1
- package/dist/communication/local.d.ts.map +1 -1
- package/dist/communication/local.js +6 -1
- package/dist/communication/local.js.map +1 -1
- package/dist/communication/worker.d.ts.map +1 -1
- package/dist/communication/worker.js +12 -2
- package/dist/communication/worker.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +5 -3
- package/dist/compile.js.map +1 -1
- package/dist/daemon-spec-validation.d.ts +31 -0
- package/dist/daemon-spec-validation.d.ts.map +1 -0
- package/dist/daemon-spec-validation.js +115 -0
- package/dist/daemon-spec-validation.js.map +1 -0
- package/dist/failure-kinds.d.ts +6 -4
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +17 -1
- package/dist/failure-kinds.js.map +1 -1
- package/dist/flow-eval-cloud.d.ts +25 -0
- package/dist/flow-eval-cloud.d.ts.map +1 -0
- package/dist/flow-eval-cloud.js +108 -0
- package/dist/flow-eval-cloud.js.map +1 -0
- package/dist/flow-eval-error.d.ts +7 -0
- package/dist/flow-eval-error.d.ts.map +1 -0
- package/dist/flow-eval-error.js +10 -0
- package/dist/flow-eval-error.js.map +1 -0
- package/dist/flow-eval-gate.d.ts +15 -0
- package/dist/flow-eval-gate.d.ts.map +1 -0
- package/dist/flow-eval-gate.js +102 -0
- package/dist/flow-eval-gate.js.map +1 -0
- package/dist/flow-eval-local.d.ts +54 -0
- package/dist/flow-eval-local.d.ts.map +1 -0
- package/dist/flow-eval-local.js +220 -0
- package/dist/flow-eval-local.js.map +1 -0
- package/dist/flow-eval-outcome.d.ts +22 -0
- package/dist/flow-eval-outcome.d.ts.map +1 -0
- package/dist/flow-eval-outcome.js +37 -0
- package/dist/flow-eval-outcome.js.map +1 -0
- package/dist/flow-eval-report.d.ts +106 -0
- package/dist/flow-eval-report.d.ts.map +1 -0
- package/dist/flow-eval-report.js +4 -0
- package/dist/flow-eval-report.js.map +1 -0
- package/dist/flow-eval-suite.d.ts +46 -0
- package/dist/flow-eval-suite.d.ts.map +1 -0
- package/dist/flow-eval-suite.js +144 -0
- package/dist/flow-eval-suite.js.map +1 -0
- package/dist/flow-eval-version.d.ts +59 -0
- package/dist/flow-eval-version.d.ts.map +1 -0
- package/dist/flow-eval-version.js +303 -0
- package/dist/flow-eval-version.js.map +1 -0
- package/dist/flow-eval.d.ts +41 -0
- package/dist/flow-eval.d.ts.map +1 -0
- package/dist/flow-eval.js +201 -0
- package/dist/flow-eval.js.map +1 -0
- package/dist/helper-operation-use.d.ts +18 -0
- package/dist/helper-operation-use.d.ts.map +1 -0
- package/dist/helper-operation-use.js +336 -0
- package/dist/helper-operation-use.js.map +1 -0
- package/dist/helper-preflight.d.ts.map +1 -1
- package/dist/helper-preflight.js +16 -2
- package/dist/helper-preflight.js.map +1 -1
- package/dist/helper-reference.d.ts +33 -0
- package/dist/helper-reference.d.ts.map +1 -1
- package/dist/helper-reference.js +5 -5
- package/dist/helper-reference.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/journal-budgeted-reads.d.ts +41 -0
- package/dist/journal-budgeted-reads.d.ts.map +1 -0
- package/dist/journal-budgeted-reads.js +142 -0
- package/dist/journal-budgeted-reads.js.map +1 -0
- package/dist/journal-client.d.ts +34 -29
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +45 -151
- package/dist/journal-client.js.map +1 -1
- package/dist/journal-connection.d.ts +42 -0
- package/dist/journal-connection.d.ts.map +1 -0
- package/dist/journal-connection.js +209 -0
- package/dist/journal-connection.js.map +1 -0
- package/dist/journal-read-policy.d.ts +38 -0
- package/dist/journal-read-policy.d.ts.map +1 -0
- package/dist/journal-read-policy.js +126 -0
- package/dist/journal-read-policy.js.map +1 -0
- package/dist/named-gate-lowering.d.ts.map +1 -1
- package/dist/named-gate-lowering.js +2 -6
- package/dist/named-gate-lowering.js.map +1 -1
- package/dist/re2js-embedded.d.ts +3 -0
- package/dist/re2js-embedded.d.ts.map +1 -0
- package/dist/re2js-embedded.js +4 -0
- package/dist/re2js-embedded.js.map +1 -0
- package/dist/spec.d.ts +10 -0
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/step-fields.d.ts +1 -1
- package/dist/step-fields.js +1 -1
- package/dist/step-fields.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +15 -0
- package/dist/validate.js.map +1 -1
- package/dist/worker-cli.d.ts +3 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +15 -5
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-lease.d.ts.map +1 -1
- package/dist/worker-lease.js +11 -3
- package/dist/worker-lease.js.map +1 -1
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/package.json +5 -3
- package/src/adapters/base.ts +16 -1
- package/src/adapters/claude.ts +8 -4
- package/src/adapters/codex.ts +15 -6
- package/src/agent-resume.ts +36 -0
- package/src/agent-transcript.ts +5 -3
- package/src/authored-flow-error.ts +9 -0
- package/src/authored-flow-executor.ts +18 -1
- package/src/authored-flow-loader.ts +27 -10
- package/src/authored-helper-effect.ts +3 -2
- package/src/authored-human.ts +5 -0
- package/src/authored-memory.ts +445 -15
- package/src/authored-node-entry.ts +6 -1
- package/src/authored-node-runner.ts +6 -3
- package/src/authored-root-readback.ts +100 -0
- package/src/authored-root.ts +58 -85
- package/src/authored-step-output.ts +3 -0
- package/src/authored-worker-step.ts +29 -4
- package/src/cli/check-helper-body.ts +9 -3
- package/src/cli/check-helper-surface.ts +43 -0
- package/src/cli/check-triggers.ts +9 -4
- package/src/cli/check.ts +37 -7
- package/src/cli/cloud-answer.ts +40 -0
- package/src/cli/cloud-connect-cli.ts +9 -2
- package/src/cli/cloud-human.ts +24 -0
- package/src/cli/cloud-live.ts +10 -5
- package/src/cli/cloud-mirror-session.ts +3 -2
- package/src/cli/cloud-run.ts +10 -1
- package/src/cli/detached-record.ts +71 -0
- package/src/cli/detached-run.ts +131 -0
- package/src/cli/diagnostic-fold.ts +28 -0
- package/src/cli/direct-run.ts +14 -1
- package/src/cli/eval.ts +141 -0
- package/src/cli/journal-timeout.ts +49 -0
- package/src/cli/local-agent-remedy.ts +6 -4
- package/src/cli/observer-session.ts +7 -1
- package/src/cli/reuse.ts +25 -0
- package/src/cli/run.ts +75 -67
- package/src/cli/running-step.ts +140 -0
- package/src/cli-adapter.ts +7 -1
- package/src/cli-commands.ts +43 -7
- package/src/cli-watch.ts +8 -5
- package/src/cli.ts +158 -51
- package/src/cloud-answer.ts +202 -0
- package/src/cloud-auth-store.ts +185 -0
- package/src/cloud-http.ts +61 -50
- package/src/cloud-human-wait.ts +23 -0
- package/src/cloud-mirror-transport.ts +1 -0
- package/src/cloud-run-record.ts +14 -5
- package/src/cloud-run.ts +46 -2
- package/src/communication/local.ts +6 -1
- package/src/communication/worker.ts +11 -3
- package/src/compile.ts +5 -3
- package/src/daemon-spec-validation.ts +126 -0
- package/src/failure-kinds.ts +17 -1
- package/src/flow-eval-cloud.ts +114 -0
- package/src/flow-eval-error.ts +10 -0
- package/src/flow-eval-gate.ts +114 -0
- package/src/flow-eval-local.ts +236 -0
- package/src/flow-eval-outcome.ts +43 -0
- package/src/flow-eval-report.ts +102 -0
- package/src/flow-eval-suite.ts +169 -0
- package/src/flow-eval-version.ts +316 -0
- package/src/flow-eval.ts +234 -0
- package/src/helper-operation-use.ts +285 -0
- package/src/helper-preflight.ts +17 -2
- package/src/helper-reference.ts +6 -6
- package/src/index.ts +15 -1
- package/src/journal-budgeted-reads.ts +134 -0
- package/src/journal-client.ts +59 -166
- package/src/journal-connection.ts +222 -0
- package/src/journal-read-policy.ts +123 -0
- package/src/named-gate-lowering.ts +2 -6
- package/src/re2js-embedded.ts +3 -0
- package/src/spec.ts +10 -0
- package/src/step-fields.ts +1 -1
- package/src/validate.ts +14 -0
- package/src/worker-cli.ts +14 -3
- package/src/worker-lease.ts +11 -3
- package/src/worker.ts +1 -1
package/src/cloud-run.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { snapshotJsonValue, type JsonValue } from './json-value.js';
|
|
|
10
10
|
import { loadAuthoredFlow, type SurfaceModuleAuthority } from './authored-flow-loader.js';
|
|
11
11
|
import { assertNoUseDependencies, collectExtensionSubmissions, type FlowExtensionSubmission } from './flow-extension-submit.js';
|
|
12
12
|
import {
|
|
13
|
-
CloudFlowError,
|
|
13
|
+
CloudFlowError, resolveCloudConnection, cloudFetch, cloudRequest, cloudRunId, isCloudRecord,
|
|
14
14
|
type CloudConnectionOptions,
|
|
15
15
|
} from './cloud-http.js';
|
|
16
16
|
import { cloudRunState, isCloudRunActive, type CloudRunState } from './cloud-run-record.js';
|
|
@@ -37,6 +37,15 @@ export interface RunInCloudOptions extends CloudConnectionOptions {
|
|
|
37
37
|
* admitted" from "admission unknown".
|
|
38
38
|
*/
|
|
39
39
|
onSubmit?: () => void;
|
|
40
|
+
/**
|
|
41
|
+
* Per-run environment variables for the hosted sandbox, e.g. a short-lived
|
|
42
|
+
* token for the caller's own API. Sent with this run's submission only: never
|
|
43
|
+
* part of {@link cloudSubmissionBody}, so a schedule never stores them, and
|
|
44
|
+
* never echoed in errors or the receipt. Names must match
|
|
45
|
+
* `^[A-Za-z_][A-Za-z0-9_]*$`; Cloud refuses names it owns (run identity,
|
|
46
|
+
* Relay/Relayfile auth, provider keys such as `ANTHROPIC_API_KEY`).
|
|
47
|
+
*/
|
|
48
|
+
envSecrets?: Readonly<Record<string, string>>;
|
|
40
49
|
}
|
|
41
50
|
export interface CloudRunReceipt {
|
|
42
51
|
runId: string;
|
|
@@ -68,6 +77,37 @@ export function cloudSubmissionBody(submission: CloudSubmission): Record<string,
|
|
|
68
77
|
|
|
69
78
|
export type { CloudRunState };
|
|
70
79
|
|
|
80
|
+
const ENV_SECRET_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate and copy per-run env secrets before any network call. Errors name
|
|
84
|
+
* the offending variable, never its value.
|
|
85
|
+
*/
|
|
86
|
+
function snapshotEnvSecrets(value: unknown): Record<string, string> | undefined {
|
|
87
|
+
if (value === undefined) return undefined;
|
|
88
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
89
|
+
throw new CloudFlowError('invalid_input', 'envSecrets must be an object mapping variable names to string values.');
|
|
90
|
+
}
|
|
91
|
+
// Null prototype: an own `__proto__` key (JSON.parse creates one) must not
|
|
92
|
+
// hit the inherited setter and vanish from the copy.
|
|
93
|
+
const copy: Record<string, string> = Object.create(null) as Record<string, string>;
|
|
94
|
+
for (const [name, secret] of Object.entries(value)) {
|
|
95
|
+
if (!ENV_SECRET_NAME.test(name)) {
|
|
96
|
+
throw new CloudFlowError('invalid_input', `envSecrets name "${name}" must match ^[A-Za-z_][A-Za-z0-9_]*$.`);
|
|
97
|
+
}
|
|
98
|
+
if (name === '__proto__') {
|
|
99
|
+
// Valid shell syntax, but Cloud's plain-object env handling cannot carry
|
|
100
|
+
// it and refuses it; fail here, by name, instead of losing it silently.
|
|
101
|
+
throw new CloudFlowError('invalid_input', 'envSecrets name "__proto__" cannot be delivered to a hosted run.');
|
|
102
|
+
}
|
|
103
|
+
if (typeof secret !== 'string') {
|
|
104
|
+
throw new CloudFlowError('invalid_input', `envSecrets value for "${name}" must be a string.`);
|
|
105
|
+
}
|
|
106
|
+
copy[name] = secret;
|
|
107
|
+
}
|
|
108
|
+
return Object.keys(copy).length === 0 ? undefined : copy;
|
|
109
|
+
}
|
|
110
|
+
|
|
71
111
|
/**
|
|
72
112
|
* Submit a declarative flow to Cloud's pinned v2 runtime. Compilation only
|
|
73
113
|
* happens here; Cloud owns provisioning and the Rust engine owns execution.
|
|
@@ -210,8 +250,11 @@ export async function runInCloud(
|
|
|
210
250
|
flow: CloudFlowSource,
|
|
211
251
|
options: RunInCloudOptions = {},
|
|
212
252
|
): Promise<CloudRunReceipt> {
|
|
213
|
-
|
|
253
|
+
// Validate locally first: resolving the connection may refresh a stored
|
|
254
|
+
// login, which posts the refresh token and rotates the credential file.
|
|
255
|
+
const envSecrets = snapshotEnvSecrets(options.envSecrets);
|
|
214
256
|
const submission = await prepareCloudSubmission(flow, options);
|
|
257
|
+
const { baseUrl } = await resolveCloudConnection(options);
|
|
215
258
|
const hash = submission.specHash;
|
|
216
259
|
// Sync before submission: `prepare` reserves the run ID and the upload lands
|
|
217
260
|
// under it, so the run request below names code Cloud already holds. A
|
|
@@ -239,6 +282,7 @@ export async function runInCloud(
|
|
|
239
282
|
...cloudSubmissionBody(submission),
|
|
240
283
|
...(options.workspaceId === undefined ? {} : { workspaceId: options.workspaceId }),
|
|
241
284
|
...(synced === undefined ? {} : { runId: synced.runId, s3CodeKey: synced.codeKey }),
|
|
285
|
+
...(envSecrets === undefined ? {} : { envSecrets }),
|
|
242
286
|
}) });
|
|
243
287
|
} catch (error) {
|
|
244
288
|
// Cloud accepts exactly one authored Surface (the one its sandbox runs).
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { onWorkerFailure } from '../worker-lease.js';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { FLOW_READ_BUDGET_MS } from '../journal-read-policy.js';
|
|
3
4
|
import { JournalClient } from '../journal-client.js';
|
|
4
5
|
import { AgentWorker } from '../worker.js';
|
|
5
6
|
import type { KernelRunSpec } from '../spec.js';
|
|
@@ -18,6 +19,9 @@ export async function attachCommunicationWorkers(spec: KernelRunSpec, socketPath
|
|
|
18
19
|
let failure: unknown;
|
|
19
20
|
const close = async () => {
|
|
20
21
|
const outcomes = await Promise.allSettled(workers.map(async ({ client, worker }) => {
|
|
22
|
+
// Stop budgeted reads first: draining the worker must not wait out a
|
|
23
|
+
// delayed history read on shutdown.
|
|
24
|
+
client.cancelReads();
|
|
21
25
|
try { await worker.close(); } finally { client.close(); }
|
|
22
26
|
}));
|
|
23
27
|
const rejected = outcomes.find(result => result.status === 'rejected');
|
|
@@ -28,7 +32,8 @@ export async function attachCommunicationWorkers(spec: KernelRunSpec, socketPath
|
|
|
28
32
|
if (step.type !== 'agent') continue;
|
|
29
33
|
requireCommunicationCli(step.cli ?? spec.cli);
|
|
30
34
|
if (step.surfaces?.workspace?.length) throw new Error('Local communication workers support stream surfaces only');
|
|
31
|
-
|
|
35
|
+
// History reads on a retried attempt meet the same CPU load as flow reads.
|
|
36
|
+
const client = new JournalClient(socketPath, { readBudgetMs: FLOW_READ_BUDGET_MS });
|
|
32
37
|
const worker = new AgentWorker(client, { workerId: `communication-${randomUUID()}`, dataDir, environment,
|
|
33
38
|
requiredStreams: [channelName(step.id, '$receipts')],
|
|
34
39
|
pins: { workspace: [], streams: step.surfaces?.streams?.map(({ stream }) => ({ stream, read_offset: 0 })) ?? [] } });
|
|
@@ -5,7 +5,8 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
5
5
|
import type { JournalClient } from '../journal-client.js';
|
|
6
6
|
import type { StepDispatchEvent } from '../protocol.js';
|
|
7
7
|
import type { ResolvedKernelAgentStep } from '../resolved-cli-identity.js';
|
|
8
|
-
import { withWorkerLease } from '../worker-lease.js';
|
|
8
|
+
import { isLeaseLost, withWorkerLease } from '../worker-lease.js';
|
|
9
|
+
import { isReadInterruptionError } from '../journal-read-policy.js';
|
|
9
10
|
import { workerInstruction } from '../worker-input.js';
|
|
10
11
|
import { resolveCliModel } from '../cli-adapter.js';
|
|
11
12
|
import { resolveAgentCwd } from '../agent-cwd.js';
|
|
@@ -26,9 +27,16 @@ export async function completeCommunicationDispatch(client: JournalClient, dispa
|
|
|
26
27
|
let output: unknown;
|
|
27
28
|
let completionReason: 'success' | 'worker_error' = 'success';
|
|
28
29
|
try {
|
|
29
|
-
output = await withWorkerLease(client, dispatch, signal =>
|
|
30
|
-
|
|
30
|
+
output = await withWorkerLease(client, dispatch, signal => {
|
|
31
|
+
// A lost lease stops this attempt's history reads instead of letting
|
|
32
|
+
// them run out the budget beside the kernel's retry.
|
|
33
|
+
const releaseReads = client.scopeReads?.(signal) ?? (() => {});
|
|
34
|
+
return run(client, dispatch, instruction, spec, dataDir, signal, runRoot, environment).finally(releaseReads);
|
|
35
|
+
});
|
|
31
36
|
} catch (error) {
|
|
37
|
+
// A lost lease or an unanswered read belongs to the kernel's sweep and
|
|
38
|
+
// retry: never complete the attempt here as a worker failure.
|
|
39
|
+
if (isLeaseLost(error) || isReadInterruptionError(error)) throw error;
|
|
32
40
|
completionReason = 'worker_error';
|
|
33
41
|
output = { error: error instanceof Error ? error.message : String(error) };
|
|
34
42
|
}
|
package/src/compile.ts
CHANGED
|
@@ -253,6 +253,7 @@ function compileStep(step: StepSpec): StepSpec {
|
|
|
253
253
|
...(s.cli !== undefined ? { cli: s.cli } : {}),
|
|
254
254
|
...(s.model !== undefined ? { model: s.model } : {}),
|
|
255
255
|
...(s.cwd !== undefined ? { cwd: s.cwd } : {}),
|
|
256
|
+
...(s.resume !== undefined ? { resume: s.resume } : {}),
|
|
256
257
|
...(s.transport !== undefined ? { transport: s.transport } : {}),
|
|
257
258
|
recoveryMode,
|
|
258
259
|
...(s.surfaces !== undefined ? { surfaces: s.surfaces } : {}),
|
|
@@ -464,7 +465,7 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
|
|
|
464
465
|
const unionKeys = [
|
|
465
466
|
'id', 'type', 'depends_on', 'max_iterations', 'retry', 'verification', 'memory', 'requirements', 'input',
|
|
466
467
|
'command', 'timeout_ms', 'lease_ms', 'on_non_zero', 'prompt', 'model', 'cli', 'instruction',
|
|
467
|
-
'cwd', 'recovery_mode', 'surfaces', 'permissions', 'transport', 'cli_identity',
|
|
468
|
+
'cwd', 'resume', 'recovery_mode', 'surfaces', 'permissions', 'transport', 'cli_identity',
|
|
468
469
|
] as const;
|
|
469
470
|
const step = requireKernelObject(value, unionKeys, at);
|
|
470
471
|
if (step['cli_identity'] !== undefined) {
|
|
@@ -479,7 +480,7 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
|
|
|
479
480
|
: type === 'llm'
|
|
480
481
|
? ['prompt', 'model', 'cli', 'cli_identity'] as const
|
|
481
482
|
: type === 'agent'
|
|
482
|
-
? ['timeout_ms', 'instruction', 'cli', 'model', 'recovery_mode', 'surfaces', 'permissions', 'cwd', 'transport', 'cli_identity'] as const
|
|
483
|
+
? ['timeout_ms', 'instruction', 'cli', 'model', 'recovery_mode', 'surfaces', 'permissions', 'cwd', 'resume', 'transport', 'cli_identity'] as const
|
|
483
484
|
: [];
|
|
484
485
|
assertKernelKeys(step, [...commonKeys, ...typeKeys], at);
|
|
485
486
|
const transportRetries = step['retry'] === undefined
|
|
@@ -516,7 +517,7 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
|
|
|
516
517
|
instruction: step['instruction'],
|
|
517
518
|
...(step['timeout_ms'] === undefined ? {} : { timeoutMs: step['timeout_ms'] }),
|
|
518
519
|
...(step['recovery_mode'] !== undefined ? { recoveryMode: step['recovery_mode'] } : {}),
|
|
519
|
-
...copyDefined(step, ['cli', 'model', 'surfaces', 'cwd', 'transport']),
|
|
520
|
+
...copyDefined(step, ['cli', 'model', 'surfaces', 'cwd', 'resume', 'transport']),
|
|
520
521
|
...(step['permissions'] !== undefined
|
|
521
522
|
? { permissions: kernelPermissionsToAuthoring(step['permissions'], `${at}.permissions`) }
|
|
522
523
|
: {}),
|
|
@@ -691,6 +692,7 @@ function toKernelStep(step: StepSpec, cliIdentity?: string): ResolvedKernelStepS
|
|
|
691
692
|
...(step.cli !== undefined ? { cli: step.cli } : {}),
|
|
692
693
|
...(step.model !== undefined ? { model: step.model } : {}),
|
|
693
694
|
...(step.cwd !== undefined ? { cwd: step.cwd } : {}),
|
|
695
|
+
...(step.resume !== undefined ? { resume: step.resume } : {}),
|
|
694
696
|
...(step.transport !== undefined ? { transport: step.transport } : {}),
|
|
695
697
|
recovery_mode: step.recoveryMode ?? 'reset',
|
|
696
698
|
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { canonicalize } from './canonical.js';
|
|
3
|
+
import { CompileError, toKernelSpec } from './compile.js';
|
|
4
|
+
import { PROTOCOL_VERSION } from './protocol.js';
|
|
5
|
+
import { RelayflowdNotFoundError, resolveRelayflowdBinary } from './relayflowd-path.js';
|
|
6
|
+
import type { FlowSpec } from './spec.js';
|
|
7
|
+
import type { CheckReport } from './cli/check.js';
|
|
8
|
+
|
|
9
|
+
export type DaemonValidationMode = 'auto' | 'required' | 'off';
|
|
10
|
+
type Unavailable = 'disabled' | 'not_reached' | 'authored_body' | 'relayflowd_not_found'
|
|
11
|
+
| 'relayflowd_bin_invalid' | 'no_verdict' | 'spawn_failed' | 'timed_out' | 'protocol_mismatch' | 'abnormal_exit';
|
|
12
|
+
export interface DaemonValidation {
|
|
13
|
+
mode: 'daemon' | 'local';
|
|
14
|
+
binary?: string;
|
|
15
|
+
protocol?: number;
|
|
16
|
+
specVersion?: string;
|
|
17
|
+
reason?: Unavailable;
|
|
18
|
+
}
|
|
19
|
+
export interface DaemonValidationDeps {
|
|
20
|
+
resolveBinary(): string;
|
|
21
|
+
run(binary: string, input: string): {
|
|
22
|
+
status: number | null; signal?: NodeJS.Signals | null; stdout: string; stderr: string; error?: Error;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
type Result = { validation: DaemonValidation; diagnostics: CheckReport['diagnostics'] };
|
|
26
|
+
// Cache discovery, never verdicts. Changed resolution inputs get a fresh lookup.
|
|
27
|
+
const binaries = new Map<string, string | Error>();
|
|
28
|
+
const defaults: DaemonValidationDeps = {
|
|
29
|
+
resolveBinary() {
|
|
30
|
+
const key = JSON.stringify([process.env['RELAYFLOWD_BIN'], process.env['PATH'], process.cwd(), process.argv[1]]);
|
|
31
|
+
let found = binaries.get(key);
|
|
32
|
+
if (found === undefined) {
|
|
33
|
+
try { found = resolveRelayflowdBinary(); }
|
|
34
|
+
catch (error) { found = error instanceof Error ? error : new Error(String(error)); }
|
|
35
|
+
binaries.set(key, found);
|
|
36
|
+
}
|
|
37
|
+
if (found instanceof Error) throw found;
|
|
38
|
+
return found;
|
|
39
|
+
},
|
|
40
|
+
run: (binary, input) => spawnSync(binary, ['validate-spec'], {
|
|
41
|
+
input, encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL', maxBuffer: 1024 * 1024,
|
|
42
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function unavailableValidation(
|
|
47
|
+
mode: DaemonValidationMode, reason: Unavailable, detail = '', binary?: string,
|
|
48
|
+
): Result {
|
|
49
|
+
const message = `${binary === undefined ? 'relayflowd' : `relayflowd at ${binary}`}: ${detail || reason.replaceAll('_', ' ')}. `
|
|
50
|
+
+ 'Daemon acceptance is unproven; an already attached daemon from another build is not checked.';
|
|
51
|
+
return {
|
|
52
|
+
validation: { mode: 'local', reason, ...(binary === undefined ? {} : { binary }) },
|
|
53
|
+
diagnostics: mode === 'required'
|
|
54
|
+
? [{ severity: 'refusal', kind: 'daemon_validation_unavailable', message }]
|
|
55
|
+
: reason === 'disabled' || reason === 'not_reached' || reason === 'relayflowd_not_found'
|
|
56
|
+
? [] : [{ severity: 'warning', kind: 'daemon_unvalidated', message }],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Same spec value run.start submits, in canonical form; no socket or run. */
|
|
61
|
+
export function validateSpecWithDaemon(
|
|
62
|
+
flow: FlowSpec, mode: DaemonValidationMode, deps: DaemonValidationDeps = defaults,
|
|
63
|
+
): Result {
|
|
64
|
+
if (mode === 'off') return unavailableValidation(mode, 'disabled');
|
|
65
|
+
let submitted: ReturnType<typeof toKernelSpec>;
|
|
66
|
+
let input: string;
|
|
67
|
+
try { submitted = toKernelSpec(flow); input = canonicalize(submitted); }
|
|
68
|
+
catch (error) {
|
|
69
|
+
// run refuses this lowering too: degrading to local would fail open.
|
|
70
|
+
const errors = error instanceof CompileError ? error.errors : [String(error)];
|
|
71
|
+
return { validation: { mode: 'local', reason: 'not_reached' },
|
|
72
|
+
diagnostics: [{ severity: 'refusal', kind: 'invalid_spec', message: errors.join('; '), errors }] };
|
|
73
|
+
}
|
|
74
|
+
let binary: string;
|
|
75
|
+
try { binary = deps.resolveBinary(); }
|
|
76
|
+
catch (error) {
|
|
77
|
+
const reason = error instanceof RelayflowdNotFoundError
|
|
78
|
+
? error.attempts[0]?.startsWith('RELAYFLOWD_BIN=') ? 'relayflowd_bin_invalid' : 'relayflowd_not_found'
|
|
79
|
+
: 'spawn_failed';
|
|
80
|
+
return unavailableValidation(mode, reason, String(error));
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const result = deps.run(binary, input);
|
|
84
|
+
if (result.error) return unavailableValidation(mode,
|
|
85
|
+
(result.error as NodeJS.ErrnoException).code === 'ETIMEDOUT' ? 'timed_out' : 'spawn_failed', result.error.message, binary);
|
|
86
|
+
let verdict: unknown;
|
|
87
|
+
try { verdict = JSON.parse(result.stdout); } catch { /* Old binaries have no envelope. */ }
|
|
88
|
+
if (!isVerdict(verdict)) return unavailableValidation(mode, 'no_verdict',
|
|
89
|
+
'no validation verdict (the binary may predate validate-spec)', binary);
|
|
90
|
+
if (verdict.protocol !== PROTOCOL_VERSION) return unavailableValidation(mode, 'protocol_mismatch',
|
|
91
|
+
`validator protocol ${verdict.protocol}, SDK protocol ${PROTOCOL_VERSION}`, binary);
|
|
92
|
+
const validation: DaemonValidation = { mode: 'daemon', binary,
|
|
93
|
+
protocol: verdict.protocol, specVersion: verdict.spec_version };
|
|
94
|
+
if (verdict.ok) {
|
|
95
|
+
// Acceptance is the envelope AND exit 0; a validator that fails or is
|
|
96
|
+
// killed after printing it has not completed validation.
|
|
97
|
+
if (result.status !== 0) return unavailableValidation(mode, 'abnormal_exit',
|
|
98
|
+
`validator ${result.status === null ? `was terminated by ${result.signal ?? 'a signal'}` : `exited ${result.status}`} after an accepting verdict`, binary);
|
|
99
|
+
return { validation, diagnostics: [] };
|
|
100
|
+
}
|
|
101
|
+
// A refusal fails closed whatever the exit status, so it is reported as-is.
|
|
102
|
+
const message = verdict.error.message;
|
|
103
|
+
const index = /steps\[(\d+)\]/.exec(message)?.[1];
|
|
104
|
+
const stepId = index === undefined ? undefined : submitted.steps[Number(index)]?.id;
|
|
105
|
+
const generated = stepId !== undefined && !flow.steps.some(step => step.id === stepId);
|
|
106
|
+
return { validation, diagnostics: [{ severity: 'refusal', kind: 'invalid_spec',
|
|
107
|
+
message: message + (generated ? `\nStep "${stepId}" is generated by this compiler's gate lowering, not declared in the flow; `
|
|
108
|
+
+ 'the installed relayflowd and this @relayflows/sdk disagree (SDK lowering bug). Report this rather than editing the flow.' : ''),
|
|
109
|
+
errors: [message], ...(stepId === undefined ? {} : { stepId }) }] };
|
|
110
|
+
} catch (error) { return unavailableValidation(mode, 'spawn_failed', String(error), binary); }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type Verdict = { protocol: number; spec_version: string } &
|
|
114
|
+
({ ok: true } | { ok: false; error: { code: 'invalid_spec'; message: string } });
|
|
115
|
+
function isVerdict(value: unknown): value is Verdict {
|
|
116
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
117
|
+
const v = value as Partial<Verdict>;
|
|
118
|
+
return Number.isInteger(v.protocol) && typeof v.spec_version === 'string'
|
|
119
|
+
&& (v.ok === true || (v.ok === false && v.error?.code === 'invalid_spec' && typeof v.error.message === 'string'));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function validationAnnotation(validation?: DaemonValidation): string {
|
|
123
|
+
return validation?.mode === 'daemon'
|
|
124
|
+
? ` [validated by relayflowd at ${validation.binary}, protocol ${validation.protocol}]`
|
|
125
|
+
: ` [local only: ${(validation?.reason ?? 'not_reached').replaceAll('_', ' ')}, so daemon acceptance is unproven]`;
|
|
126
|
+
}
|
package/src/failure-kinds.ts
CHANGED
|
@@ -48,7 +48,9 @@ export const CHECK_INPUT_FAILURE_KINDS = [
|
|
|
48
48
|
...SHARED_SPEC_FAILURE_KINDS,
|
|
49
49
|
] as const;
|
|
50
50
|
|
|
51
|
+
// run never enables daemon validation; fromCheckReport needs a mapping if that changes.
|
|
51
52
|
export const CHECK_FAILURE_KINDS = [
|
|
53
|
+
'daemon_validation_unavailable',
|
|
52
54
|
...CHECK_INPUT_FAILURE_KINDS,
|
|
53
55
|
...PREFLIGHT_ENVIRONMENT_FAILURE_KINDS,
|
|
54
56
|
] as const;
|
|
@@ -136,6 +138,14 @@ export const RUN_FAILURE_KINDS = [
|
|
|
136
138
|
'gate_failed',
|
|
137
139
|
/** `flows answer` named a wait the run is not asking: unknown, or already answered. */
|
|
138
140
|
'human_wait_unknown',
|
|
141
|
+
/** `f.memory.learn` was given a finding without non-empty string fields. */
|
|
142
|
+
'memory_finding_invalid',
|
|
143
|
+
/** `f.memory.learn` could not persist the finding to script memory. */
|
|
144
|
+
'memory_unwritable',
|
|
145
|
+
/** `f.memory.learn` ran without a durable root run to journal the finding in. */
|
|
146
|
+
'memory_unjournaled',
|
|
147
|
+
/** `f.memory` lost its database, or found a damaged learned finding, after the run started. */
|
|
148
|
+
'memory_unreachable',
|
|
139
149
|
/**
|
|
140
150
|
* `--local-agent` cannot be honoured for this invocation, so it is refused
|
|
141
151
|
* rather than accepted and ignored. A local agent worker is admitted at run
|
|
@@ -149,6 +159,7 @@ export const RUN_FAILURE_KINDS = [
|
|
|
149
159
|
* reach this process. The run is NOT failed (`status: running`): its
|
|
150
160
|
* completed steps are journaled and `flows resume <rootRunId>` continues it.
|
|
151
161
|
*/
|
|
162
|
+
'daemon_unresponsive',
|
|
152
163
|
'root_lease_lost',
|
|
153
164
|
] as const;
|
|
154
165
|
|
|
@@ -160,6 +171,9 @@ export const RUN_FAILURE_KINDS = [
|
|
|
160
171
|
*/
|
|
161
172
|
export const RUN_WARNING_KINDS = [
|
|
162
173
|
'connection_file_stale',
|
|
174
|
+
'reuse_summary_unavailable',
|
|
175
|
+
'result_unreadable',
|
|
176
|
+
'inspection_interrupted',
|
|
163
177
|
] as const;
|
|
164
178
|
|
|
165
179
|
/**
|
|
@@ -172,7 +186,9 @@ export const RUN_WARNING_KINDS = [
|
|
|
172
186
|
* daemon-free, can see none either — so it opts in, while `flows run`, `flows
|
|
173
187
|
* build` and SDK submissions do not (cli/check-worker-surface.ts).
|
|
174
188
|
*/
|
|
175
|
-
|
|
189
|
+
/** helper_credential_unresolved describes a local mount gap during inspection;
|
|
190
|
+
* Cloud validates workspace integrations at submit, and local execution refuses. */
|
|
191
|
+
export const CHECK_WARNING_KINDS = ['editor_schema_missing', 'daemon_unvalidated', 'agent_worker_unresolved', 'helper_credential_unresolved'] as const;
|
|
176
192
|
export type CheckWarningKind = (typeof CHECK_WARNING_KINDS)[number];
|
|
177
193
|
|
|
178
194
|
export type PreflightFailureKind = (typeof PREFLIGHT_FAILURE_KINDS)[number];
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// The Cloud executor for `evaluateFlow`: one fresh hosted run per case,
|
|
2
|
+
// waited on, with its verdict read from the hosted run report and its spend
|
|
3
|
+
// from the run's steps.
|
|
4
|
+
|
|
5
|
+
import { getCloudRunSteps, type CloudStep } from './cloud-read.js';
|
|
6
|
+
import { runInCloud, waitForCloudFlowRun, type RunInCloudOptions } from './cloud-run.js';
|
|
7
|
+
import { cloudRunState } from './cloud-run-record.js';
|
|
8
|
+
import { cloudRequest, isCloudRecord, type CloudConnectionOptions } from './cloud-http.js';
|
|
9
|
+
import { isAuthoredFlowPath } from './direct-input.js';
|
|
10
|
+
import type { FlowEvalExecutor, FlowEvalRun } from './flow-eval-report.js';
|
|
11
|
+
import { runReportOutcome } from './flow-eval-outcome.js';
|
|
12
|
+
import { flowEvalSources, isPathTarget, type FlowEvalTarget } from './flow-eval-version.js';
|
|
13
|
+
|
|
14
|
+
const MODULE = /\.(?:[mc]?[jt]s)$/u;
|
|
15
|
+
const EXTENSION_STORE = /(?:^|\/)\.flows\/plugins\//u;
|
|
16
|
+
/** Manifests and locks the version pins but Cloud does not need as imports. */
|
|
17
|
+
const PROJECT_METADATA = /(?:^|\/)(?:package(?:-lock)?|flows(?:\.lock)?)\.json$/u;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Cloud receives the entry file's source alone (cloud-run.ts): a relative
|
|
21
|
+
* import is not resolved by the hosted runner, even with `syncCode`. A flow
|
|
22
|
+
* whose version spans local modules would therefore fail only on Cloud, or run
|
|
23
|
+
* something other than what was judged. Refuse it before submitting anything.
|
|
24
|
+
*/
|
|
25
|
+
async function refuseLocalImports(flow: FlowEvalTarget): Promise<void> {
|
|
26
|
+
if (!isPathTarget(flow) || !MODULE.test(flow.path)) return;
|
|
27
|
+
const { files } = await flowEvalSources(flow);
|
|
28
|
+
const entry = files.find(file => !file.includes('/') && flow.path.endsWith(file));
|
|
29
|
+
// Locked extension payloads are not imports: Cloud receives them separately
|
|
30
|
+
// (`extensions` in the submission), so they never make a flow non-self-contained.
|
|
31
|
+
const modules = files.filter(file => (MODULE.test(file) || /\.json$/u.test(file)) && file !== entry
|
|
32
|
+
&& !EXTENSION_STORE.test(file) && !PROJECT_METADATA.test(file));
|
|
33
|
+
if (modules.length > 0) {
|
|
34
|
+
throw new Error(`Cloud evaluation runs the entry file alone, and this flow imports local modules (${modules.join(', ')}). `
|
|
35
|
+
+ 'Evaluate it locally, or inline those modules.');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CloudFlowEvalExecutorOptions extends CloudConnectionOptions {
|
|
40
|
+
workspaceId?: string;
|
|
41
|
+
syncCode?: RunInCloudOptions['syncCode'];
|
|
42
|
+
pollIntervalMs?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Execute each case as a fresh hosted run and wait for it. Needs a token with
|
|
47
|
+
* `workflow:invoke:write` and `runs:read`; inside a Cloud step that means an
|
|
48
|
+
* injected workspace token, since a run-scoped sandbox token reads only its own run.
|
|
49
|
+
*/
|
|
50
|
+
export function cloudFlowEvalExecutor(options: CloudFlowEvalExecutorOptions = {}): FlowEvalExecutor {
|
|
51
|
+
return async ({ flow, input, signal }) => {
|
|
52
|
+
await refuseLocalImports(flow);
|
|
53
|
+
const connection: CloudConnectionOptions = {
|
|
54
|
+
...(options.apiUrl === undefined ? {} : { apiUrl: options.apiUrl }),
|
|
55
|
+
...(options.token === undefined ? {} : { token: options.token }),
|
|
56
|
+
...(options.requestTimeoutMs === undefined ? {} : { requestTimeoutMs: options.requestTimeoutMs }),
|
|
57
|
+
...(signal === undefined ? {} : { signal }),
|
|
58
|
+
};
|
|
59
|
+
const receipt = await runInCloud(flow, {
|
|
60
|
+
...connection,
|
|
61
|
+
...(options.workspaceId === undefined ? {} : { workspaceId: options.workspaceId }),
|
|
62
|
+
...(options.syncCode === undefined ? {} : { syncCode: options.syncCode }),
|
|
63
|
+
// Cloud requires an explicit input for authored source; `{}` is the
|
|
64
|
+
// documented "no fields", matching the local executor's default.
|
|
65
|
+
...(input !== undefined ? { input } : 'path' in flow && isAuthoredFlowPath(flow.path) ? { input: {} } : {}),
|
|
66
|
+
});
|
|
67
|
+
await waitForCloudFlowRun(receipt.runId, {
|
|
68
|
+
...connection, ...(options.pollIntervalMs === undefined ? {} : { pollIntervalMs: options.pollIntervalMs }),
|
|
69
|
+
});
|
|
70
|
+
// Re-read the terminal record for its stored report: the run state alone
|
|
71
|
+
// carries the kernel's reason, which says `success` for a declined body
|
|
72
|
+
// and has no completion detail.
|
|
73
|
+
const record = await cloudRequest(`/api/v1/workflows/runs/${receipt.runId}`, connection);
|
|
74
|
+
let steps: CloudStep[] | undefined;
|
|
75
|
+
try {
|
|
76
|
+
steps = await getCloudRunSteps(receipt.runId, connection);
|
|
77
|
+
} catch {
|
|
78
|
+
// As locally: spend is evidence, not the verdict.
|
|
79
|
+
}
|
|
80
|
+
return { runId: receipt.runId, ...cloudRunOutcome(record, receipt.runId), ...cloudSpend(steps) };
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Map a terminal hosted run record onto the eval vocabulary: the validated run
|
|
86
|
+
* state decides parked/failed/canceled, and the stored report (the same
|
|
87
|
+
* RunReport a local run prints) supplies `declined` and the completion detail.
|
|
88
|
+
* Exported for tests.
|
|
89
|
+
*/
|
|
90
|
+
export function cloudRunOutcome(record: unknown, runId: string): Pick<FlowEvalRun, 'completionReason' | 'completionDetail'> {
|
|
91
|
+
const state = cloudRunState(record, runId);
|
|
92
|
+
const report = isCloudRecord(record) && isCloudRecord(record.result) ? record.result : {};
|
|
93
|
+
const mapped = runReportOutcome(report);
|
|
94
|
+
// A park with nothing attached to run it is the same fact on both sides.
|
|
95
|
+
if (mapped.completionReason === 'worker_unavailable') return mapped;
|
|
96
|
+
if (state.status === 'needs_human') return { ...mapped, completionReason: 'needs_human' };
|
|
97
|
+
if (!('completionReason' in state)) return { ...mapped, completionReason: state.status };
|
|
98
|
+
if (state.completionReason === 'success') return mapped.completionReason === 'declined' ? mapped : { ...mapped, completionReason: 'success' };
|
|
99
|
+
return { ...mapped, completionReason: state.completionReason };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Sum hosted step spend; any step with unknown cost makes the total unknown. Exported for tests. */
|
|
103
|
+
export function cloudSpend(steps: readonly CloudStep[] | undefined): Pick<FlowEvalRun, 'costUsd' | 'tokensIn' | 'tokensOut' | 'steps'> {
|
|
104
|
+
if (steps === undefined) return { costUsd: null, tokensIn: null, tokensOut: null };
|
|
105
|
+
const known = (values: Array<number | null>): number | null =>
|
|
106
|
+
values.some(v => v === null || !Number.isFinite(v) || v < 0) ? null : values.reduce<number>((sum, v) => sum + v!, 0);
|
|
107
|
+
const costed = steps.filter(step => step.step_type === 'agent' || step.step_type === 'llm' || step.cost_usd !== null);
|
|
108
|
+
return {
|
|
109
|
+
costUsd: known(costed.map(step => step.cost_usd)),
|
|
110
|
+
tokensIn: known(costed.map(step => step.tokens_in)),
|
|
111
|
+
tokensOut: known(costed.map(step => step.tokens_out)),
|
|
112
|
+
steps: steps.map(step => ({ id: step.step_name, status: step.status, durationMs: step.duration_ms, costUsd: step.cost_usd })),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type FlowEvalErrorCode =
|
|
2
|
+
| 'invalid_suite' | 'invalid_baseline' | 'version_mismatch' | 'suite_mismatch' | 'invalid_options' | 'unreadable_flow';
|
|
3
|
+
|
|
4
|
+
/** The evaluation itself is invalid. Case failures are verdicts in the report, never this. */
|
|
5
|
+
export class FlowEvalError extends Error {
|
|
6
|
+
constructor(readonly code: FlowEvalErrorCode, message: string) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'FlowEvalError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// The verdict of a flow evaluation: case results aggregated into a summary,
|
|
2
|
+
// and the summary judged against the suite's thresholds and a baseline.
|
|
3
|
+
|
|
4
|
+
import { FlowEvalError } from './flow-eval-error.js';
|
|
5
|
+
import type { FlowEvalCaseResult, FlowEvalOutcome, FlowEvalReport } from './flow-eval-report.js';
|
|
6
|
+
import { isRecord, type FlowEvalSuite } from './flow-eval-suite.js';
|
|
7
|
+
|
|
8
|
+
/** The part of a prior report a regression check reads, validated. */
|
|
9
|
+
export interface FlowEvalBaseline {
|
|
10
|
+
suiteSha256: string;
|
|
11
|
+
outcomes: ReadonlyMap<string, FlowEvalOutcome>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const OUTCOMES: readonly string[] = ['pass', 'fail', 'error'];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validate an untrusted prior report (parsed JSON) before any case runs, so a
|
|
18
|
+
* malformed baseline is refused before an evaluation spends anything.
|
|
19
|
+
*/
|
|
20
|
+
export function parseFlowEvalBaseline(value: unknown): FlowEvalBaseline {
|
|
21
|
+
const fail = (message: string): never => { throw new FlowEvalError('invalid_baseline', message); };
|
|
22
|
+
if (!isRecord(value) || value['kind'] !== 'flows.eval.report') fail('Baseline is not a flows eval report.');
|
|
23
|
+
const report = value as Record<string, unknown>;
|
|
24
|
+
const suite = report['suite'];
|
|
25
|
+
if (!isRecord(suite) || typeof suite['sha256'] !== 'string') fail('Baseline report names no suite sha256.');
|
|
26
|
+
if (!Array.isArray(report['cases'])) fail('Baseline report has no `cases` array.');
|
|
27
|
+
const outcomes = new Map<string, FlowEvalOutcome>();
|
|
28
|
+
for (const [index, entry] of (report['cases'] as unknown[]).entries()) {
|
|
29
|
+
if (!isRecord(entry) || typeof entry['id'] !== 'string' || !OUTCOMES.includes(entry['outcome'] as string)) {
|
|
30
|
+
fail(`Baseline cases[${index}] needs a string \`id\` and an \`outcome\` of pass, fail or error.`);
|
|
31
|
+
}
|
|
32
|
+
const item = entry as Record<string, unknown>;
|
|
33
|
+
if (outcomes.has(item['id'] as string)) fail(`Baseline case id "${item['id']}" is not unique.`);
|
|
34
|
+
outcomes.set(item['id'] as string, item['outcome'] as FlowEvalOutcome);
|
|
35
|
+
}
|
|
36
|
+
return { suiteSha256: (suite as Record<string, unknown>)['sha256'] as string, outcomes };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function summarizeFlowEval(results: readonly FlowEvalCaseResult[]): FlowEvalReport['summary'] {
|
|
40
|
+
const passed = results.filter(r => r.outcome === 'pass').length;
|
|
41
|
+
const failed = results.filter(r => r.outcome === 'fail').length;
|
|
42
|
+
const errored = results.filter(r => r.outcome === 'error').length;
|
|
43
|
+
const costs = results.map(r => r.costUsd);
|
|
44
|
+
const latencies = results.map(r => r.latencyMs).sort((a, b) => a - b);
|
|
45
|
+
return {
|
|
46
|
+
total: results.length, passed, failed, errored,
|
|
47
|
+
passRate: results.length === 0 ? 0 : passed / results.length,
|
|
48
|
+
totalCostUsd: costs.some(c => c === null) ? null : fromNanos(sumNanos(costs as number[])),
|
|
49
|
+
latencyMs: { p50: percentile(latencies, 0.5), p95: percentile(latencies, 0.95), max: latencies.at(-1) ?? 0 },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function decideFlowEvalGate(
|
|
54
|
+
suite: FlowEvalSuite,
|
|
55
|
+
results: readonly FlowEvalCaseResult[],
|
|
56
|
+
summary: FlowEvalReport['summary'],
|
|
57
|
+
baseline: FlowEvalBaseline | undefined,
|
|
58
|
+
): FlowEvalReport['gate'] {
|
|
59
|
+
const reasons: string[] = [];
|
|
60
|
+
const thresholds = suite.thresholds ?? {};
|
|
61
|
+
const minPassRate = thresholds.minPassRate ?? 1;
|
|
62
|
+
if (summary.passRate < minPassRate) {
|
|
63
|
+
reasons.push(`pass rate ${formatRate(summary.passRate)} is below ${formatRate(minPassRate)} (${summary.passed}/${summary.total})`);
|
|
64
|
+
}
|
|
65
|
+
// Fails closed regardless of minPassRate: an errored case was never judged,
|
|
66
|
+
// so a tolerance for quality misses must not absorb it.
|
|
67
|
+
if (summary.errored > 0) reasons.push(`${summary.errored} case(s) errored before a verdict`);
|
|
68
|
+
if (thresholds.maxTotalCostUsd !== undefined) {
|
|
69
|
+
// Decimal, not binary: costs and the ceiling are compared in whole
|
|
70
|
+
// nanodollars, so 0.1 + 0.2 meets a 0.3 ceiling exactly and a total a
|
|
71
|
+
// fraction of a cent over it still fails. No display rounding is involved.
|
|
72
|
+
const costs = results.map(r => r.costUsd);
|
|
73
|
+
if (costs.some(c => c === null)) reasons.push(`total cost is unknown; the suite caps it at $${thresholds.maxTotalCostUsd}`);
|
|
74
|
+
else {
|
|
75
|
+
const total = sumNanos(costs as number[]);
|
|
76
|
+
if (total > toNanos(thresholds.maxTotalCostUsd)) {
|
|
77
|
+
reasons.push(`total cost $${fromNanos(total)} exceeds $${thresholds.maxTotalCostUsd}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (thresholds.maxP95LatencyMs !== undefined && summary.latencyMs.p95 > thresholds.maxP95LatencyMs) {
|
|
82
|
+
reasons.push(`p95 latency ${summary.latencyMs.p95}ms exceeds ${thresholds.maxP95LatencyMs}ms`);
|
|
83
|
+
}
|
|
84
|
+
const regressions: string[] = [];
|
|
85
|
+
if (baseline !== undefined) {
|
|
86
|
+
for (const result of results) {
|
|
87
|
+
if (baseline.outcomes.get(result.id) === 'pass' && result.outcome !== 'pass') regressions.push(result.id);
|
|
88
|
+
}
|
|
89
|
+
if (regressions.length > 0) reasons.push(`${regressions.length} case(s) regressed against the baseline: ${regressions.join(', ')}`);
|
|
90
|
+
}
|
|
91
|
+
return { pass: reasons.length === 0, reasons, regressions };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function percentile(sorted: readonly number[], p: number): number {
|
|
95
|
+
if (sorted.length === 0) return 0;
|
|
96
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(p * sorted.length) - 1))]!;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Dollars as whole nanodollars: exact for any cost a run reports, far below a cent. */
|
|
100
|
+
function toNanos(usd: number): bigint {
|
|
101
|
+
return BigInt(Math.round(usd * 1e9));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sumNanos(costs: readonly number[]): bigint {
|
|
105
|
+
return costs.reduce((sum, cost) => sum + toNanos(cost), 0n);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function fromNanos(nanos: bigint): number {
|
|
109
|
+
return Number(nanos) / 1e9;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatRate(rate: number): string {
|
|
113
|
+
return `${Math.round(rate * 1000) / 10}%`;
|
|
114
|
+
}
|