@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
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { cloudAnswerCommand } from '../authored-human.js';
|
|
2
|
+
import type { CloudHumanWait } from '../cloud-human-wait.js';
|
|
3
|
+
import type { CloudRunState } from '../cloud-run-record.js';
|
|
4
|
+
import { redact } from '../redact.js';
|
|
5
|
+
import { safe } from './cloud-format.js';
|
|
6
|
+
|
|
7
|
+
export function scrubCloudHumanWait(wait: CloudHumanWait | undefined, env = process.env): CloudHumanWait | undefined {
|
|
8
|
+
if (!wait) return undefined;
|
|
9
|
+
return {
|
|
10
|
+
...wait,
|
|
11
|
+
...(wait.recipient === undefined ? {} : { recipient: { ...wait.recipient, target: safe(redact(wait.recipient.target, env)) } }),
|
|
12
|
+
...(wait.question === undefined ? {} : { question: safe(redact(wait.question, env)) }),
|
|
13
|
+
...(wait.to === undefined ? {} : { to: safe(redact(wait.to, env)) }),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function cloudParkLines(state: Extract<CloudRunState, { status: 'needs_human' }>, env = process.env): string[] {
|
|
18
|
+
const wait = scrubCloudHumanWait(state.humanWait, env);
|
|
19
|
+
return [
|
|
20
|
+
`NEEDS_HUMAN ${state.runId} completionReason: needs_human`,
|
|
21
|
+
...(wait ? [`Waiting for ${wait.to ?? '(recipient unavailable)'} to answer ${wait.waitId}: ${JSON.stringify(wait.question ?? '(question unavailable)')}`] : []),
|
|
22
|
+
`Answer with: ${cloudAnswerCommand(state.runId)}`,
|
|
23
|
+
];
|
|
24
|
+
}
|
package/src/cli/cloud-live.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// owns for the verb's duration, and nothing here installs a process handler.
|
|
22
22
|
// Aborting stops the observation, not the hosted run — the refusal says so.
|
|
23
23
|
|
|
24
|
+
import { cloudParkLines } from './cloud-human.js';
|
|
24
25
|
import { canonicalize } from '../canonical.js';
|
|
25
26
|
import {
|
|
26
27
|
getCloudRunDetailLive, getCloudRunLog, getCloudRunSteps,
|
|
@@ -56,7 +57,8 @@ function delayFor(failures: number, interval: number): number {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/** The hosted `flows run --wait` mapping: only an attested success is 0. */
|
|
59
|
-
function exitFor(state: CloudRunState): 0 | 1 {
|
|
60
|
+
function exitFor(state: CloudRunState): 0 | 1 | 3 {
|
|
61
|
+
if (state.status === 'needs_human') return 3;
|
|
60
62
|
return state.status === 'completed' ? 0 : 1;
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -99,7 +101,7 @@ export async function runCloudStatusWatch(
|
|
|
99
101
|
args: { runId?: string; json: boolean },
|
|
100
102
|
io: CliIo,
|
|
101
103
|
options: CloudLiveOptions = {},
|
|
102
|
-
): Promise<0 | 1 | 2> {
|
|
104
|
+
): Promise<0 | 1 | 2 | 3> {
|
|
103
105
|
const env = options.env ?? process.env;
|
|
104
106
|
const clock = options.now ?? Date.now;
|
|
105
107
|
const signal = options.signal;
|
|
@@ -132,12 +134,15 @@ export async function runCloudStatusWatch(
|
|
|
132
134
|
}
|
|
133
135
|
failures = 0;
|
|
134
136
|
|
|
135
|
-
const run = scrubRun(live.
|
|
137
|
+
const run = scrubRun(live.state.status === 'needs_human'
|
|
138
|
+
? { ...live.detail, status: 'needs_human' } : live.detail, env);
|
|
136
139
|
const scrubbed = scrubSteps(steps, env);
|
|
137
140
|
const terminal = !isCloudRunActive(live.state.status);
|
|
138
141
|
// Rendered before the abort check and emitted after it, in one call: an
|
|
139
142
|
// interrupt between two lines of a frame would leave a torn page.
|
|
140
|
-
const
|
|
143
|
+
const lines = renderCloudStatus(run, scrubbed, clock());
|
|
144
|
+
if (live.state.status === 'needs_human') lines.push(...cloudParkLines(live.state, env));
|
|
145
|
+
const page = args.json ? '' : `${clearSequence(io)}${lines.join('\n')}`;
|
|
141
146
|
if (isAborted(signal)) return aborted('watching', runId, args.json, io);
|
|
142
147
|
if (!args.json) io.stdout(page);
|
|
143
148
|
if (terminal) {
|
|
@@ -265,7 +270,7 @@ export async function runCloudLogsFollow(
|
|
|
265
270
|
args: { runId: string; step: string | undefined; json: boolean },
|
|
266
271
|
io: CliIo,
|
|
267
272
|
options: CloudLiveOptions = {},
|
|
268
|
-
): Promise<0 | 1 | 2> {
|
|
273
|
+
): Promise<0 | 1 | 2 | 3> {
|
|
269
274
|
const env = options.env ?? process.env;
|
|
270
275
|
const signal = options.signal;
|
|
271
276
|
const interval = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { readFile } from 'node:fs/promises';
|
|
26
26
|
import type { CliIo } from '../cli.js';
|
|
27
27
|
import { canonicalize } from '../canonical.js';
|
|
28
|
-
import {
|
|
28
|
+
import { resolveCloudConnection, CloudFlowError } from '../cloud-http.js';
|
|
29
29
|
import { recordMirroredRun, readMirroredRun } from '../cloud-mirror-ledger.js';
|
|
30
30
|
import { createRunMirror, readJournalEvents, type RunMirror } from '../cloud-mirror.js';
|
|
31
31
|
import { MirrorClient, registerLocalRun, type MirrorRunSource } from '../cloud-mirror-transport.js';
|
|
@@ -231,7 +231,8 @@ export function mirrorSourceFromJournal(dataDir: string): (runId: string) => Pro
|
|
|
231
231
|
// the deployment this invocation will actually register with, so a run
|
|
232
232
|
// mirrored to staging never claims to continue an id that means something
|
|
233
233
|
// else in production.
|
|
234
|
-
const
|
|
234
|
+
const { baseUrl } = await resolveCloudConnection({});
|
|
235
|
+
const resumedFromRunId = await readMirroredRun(dataDir, runId, baseUrl)
|
|
235
236
|
.catch(() => undefined);
|
|
236
237
|
// Through the retrying reader: a resume reads this journal while the
|
|
237
238
|
// daemon is writing to it, and a single `journal_busy` used to abandon
|
package/src/cli/cloud-run.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { cloudAnswerCommand } from '../authored-human.js';
|
|
2
|
+
import { scrubCloudHumanWait, cloudParkLines } from './cloud-human.js';
|
|
1
3
|
import { CloudFlowError } from '../cloud-http.js';
|
|
2
4
|
import { runInCloud, waitForCloudFlowRun, type RunInCloudOptions } from '../cloud-run.js';
|
|
3
5
|
import { DirectInputError, isAuthoredFlowPath, parseDirectInput } from '../direct-input.js';
|
|
@@ -17,7 +19,7 @@ export async function runCloudCli(
|
|
|
17
19
|
* handler is installed here.
|
|
18
20
|
*/
|
|
19
21
|
signal: AbortSignal,
|
|
20
|
-
): Promise<0 | 1 | 2> {
|
|
22
|
+
): Promise<0 | 1 | 2 | 3> {
|
|
21
23
|
let runId: string | undefined;
|
|
22
24
|
// Flips at the run submission. An interruption before it — during prepare,
|
|
23
25
|
// packing or upload — admitted nothing and is safe to retry; only an
|
|
@@ -65,6 +67,13 @@ export async function runCloudCli(
|
|
|
65
67
|
return 0;
|
|
66
68
|
}
|
|
67
69
|
const run = await waitForCloudFlowRun(receipt.runId, { signal });
|
|
70
|
+
if (run.status === 'needs_human') {
|
|
71
|
+
const humanWait = scrubCloudHumanWait(run.humanWait);
|
|
72
|
+
const next = cloudAnswerCommand(run.runId);
|
|
73
|
+
if (json) io.stdout(JSON.stringify({ ok: false, ...receipt, ...run, humanWait, next }));
|
|
74
|
+
else for (const line of cloudParkLines(run)) io.stdout(line);
|
|
75
|
+
return 3;
|
|
76
|
+
}
|
|
68
77
|
const ok = run.status === 'completed';
|
|
69
78
|
if (json) io.stdout(JSON.stringify({ ok, ...receipt, ...run }));
|
|
70
79
|
else io.stdout(`${run.status.toUpperCase()} ${run.runId} completionReason: ${'completionReason' in run ? run.completionReason : 'unavailable'}`);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import type { RunExecution } from './run.js';
|
|
3
|
+
|
|
4
|
+
export const DETACH_RECORD_ENV = 'FLOWS_DETACH_RECORD';
|
|
5
|
+
|
|
6
|
+
/** A receipt/index, never run state: the journal remains authoritative. */
|
|
7
|
+
export interface DetachedRecord {
|
|
8
|
+
phase: 'started' | 'finished' | 'refused';
|
|
9
|
+
pid: number;
|
|
10
|
+
runId?: string;
|
|
11
|
+
observerUrl?: string;
|
|
12
|
+
execution?: RunExecution;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function readDetachedRecord(path: string): DetachedRecord | undefined {
|
|
16
|
+
let text: string;
|
|
17
|
+
try { text = readFileSync(path, 'utf8'); }
|
|
18
|
+
catch (error) {
|
|
19
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
const record = JSON.parse(text) as DetachedRecord;
|
|
23
|
+
if (!['started', 'finished', 'refused'].includes(record.phase)
|
|
24
|
+
|| !Number.isSafeInteger(record.pid) || record.pid <= 0
|
|
25
|
+
|| (record.phase === 'started' && typeof record.runId !== 'string')
|
|
26
|
+
|| (record.phase !== 'started' && record.execution === undefined)) {
|
|
27
|
+
throw new Error('Invalid detached receipt');
|
|
28
|
+
}
|
|
29
|
+
return record;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DetachedReceipt {
|
|
33
|
+
/** The daemon admitted the run: the only signal that makes a detach succeed. */
|
|
34
|
+
started(run: { runId: string }): void;
|
|
35
|
+
observerUrl(url: string): void;
|
|
36
|
+
finished(execution: RunExecution): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Only the standalone child consumes this marker, before authored code loads. */
|
|
40
|
+
export function takeDetachedReceipt(env: NodeJS.ProcessEnv = process.env): DetachedReceipt | undefined {
|
|
41
|
+
const path = env[DETACH_RECORD_ENV];
|
|
42
|
+
delete env[DETACH_RECORD_ENV];
|
|
43
|
+
if (path === undefined) return undefined;
|
|
44
|
+
let runId: string | undefined;
|
|
45
|
+
let observerUrl: string | undefined;
|
|
46
|
+
let execution: RunExecution | undefined;
|
|
47
|
+
const publish = (): void => {
|
|
48
|
+
const record: DetachedRecord = {
|
|
49
|
+
pid: process.pid,
|
|
50
|
+
// Never admitted means the parent reports this execution as-is.
|
|
51
|
+
phase: execution === undefined ? 'started' : runId === undefined ? 'refused' : 'finished',
|
|
52
|
+
...(runId === undefined ? {} : { runId }),
|
|
53
|
+
...(observerUrl === undefined ? {} : { observerUrl }),
|
|
54
|
+
...(execution === undefined ? {} : { execution }),
|
|
55
|
+
};
|
|
56
|
+
if (runId === undefined && execution === undefined) return;
|
|
57
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
58
|
+
writeFileSync(temporary, JSON.stringify(record), { mode: 0o600 });
|
|
59
|
+
renameSync(temporary, path);
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
started(run) { runId ??= run.runId; publish(); },
|
|
63
|
+
observerUrl(url) { observerUrl = url; publish(); },
|
|
64
|
+
finished(result) {
|
|
65
|
+
// Admission is explicit. A failure report can name the requested run
|
|
66
|
+
// (e.g. resume's pre-admission source check) without having admitted it.
|
|
67
|
+
execution = result;
|
|
68
|
+
publish();
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { closeSync, mkdirSync, mkdtempSync, openSync, readSync, fstatSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
6
|
+
import type { CliIo } from '../cli.js';
|
|
7
|
+
import { LOCAL_AGENT_ENV_FD } from '../local-agent-environment.js';
|
|
8
|
+
import { shellWord } from '../shell-word.js';
|
|
9
|
+
import { DETACH_RECORD_ENV, readDetachedRecord } from './detached-record.js';
|
|
10
|
+
import type { RunExecution } from './run.js';
|
|
11
|
+
|
|
12
|
+
// Preflight may compile authored code or fetch a bundle; it runs only in the child.
|
|
13
|
+
export const DETACHED_START_TIMEOUT_MS = 60_000;
|
|
14
|
+
// Observation is optional. Never wait for it for the duration of the run.
|
|
15
|
+
export const DETACHED_OBSERVER_GRACE_MS = 2_000;
|
|
16
|
+
const POLL_MS = 50;
|
|
17
|
+
|
|
18
|
+
export interface DetachedHandle {
|
|
19
|
+
detached: true;
|
|
20
|
+
runId: string;
|
|
21
|
+
pid: number;
|
|
22
|
+
logPath: string;
|
|
23
|
+
recordPath: string;
|
|
24
|
+
observerUrl?: string;
|
|
25
|
+
follow: string;
|
|
26
|
+
notice: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type DetachedResult = { handle: DetachedHandle } | { execution: RunExecution };
|
|
30
|
+
|
|
31
|
+
export function detachedArgv(args: readonly string[]): string[] {
|
|
32
|
+
return args.filter(argument => argument !== '--detach');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function startDetachedRun(
|
|
36
|
+
args: readonly string[],
|
|
37
|
+
options: { command: 'run' | 'resume'; dataDir: string; noObserverLink: boolean; selfCommand?: readonly string[] },
|
|
38
|
+
): Promise<DetachedResult> {
|
|
39
|
+
const failure = (message: string, refusal = false): DetachedResult => ({ execution: {
|
|
40
|
+
exitCode: refusal ? 2 : 1,
|
|
41
|
+
report: { ok: false, command: options.command, resolutions: [], diagnostics: [refusal
|
|
42
|
+
? { severity: 'refusal', kind: 'invalid_invocation', message }
|
|
43
|
+
: { severity: 'failure', kind: 'protocol_error', message }] },
|
|
44
|
+
} });
|
|
45
|
+
// This descriptor is deliberately not inherited. Silently losing its provider
|
|
46
|
+
// credentials would admit a run that cannot execute its steps.
|
|
47
|
+
if (process.env[LOCAL_AGENT_ENV_FD] !== undefined) {
|
|
48
|
+
return failure(`detach_environment_fd_unsupported: --detach cannot forward ${LOCAL_AGENT_ENV_FD}; use foreground execution.`, true);
|
|
49
|
+
}
|
|
50
|
+
let logPath: string | undefined;
|
|
51
|
+
let child: ReturnType<typeof spawn> | undefined;
|
|
52
|
+
try {
|
|
53
|
+
const directory = join(resolve(options.dataDir), 'detached');
|
|
54
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
55
|
+
const handleDir = mkdtempSync(join(directory, 'run-'));
|
|
56
|
+
logPath = join(handleDir, 'run.log');
|
|
57
|
+
const recordPath = join(handleDir, 'record.json');
|
|
58
|
+
const log = openSync(logPath, 'a', 0o600);
|
|
59
|
+
let spawnError: Error | undefined;
|
|
60
|
+
let exited = false;
|
|
61
|
+
try {
|
|
62
|
+
const [executable, ...prefix] = options.selfCommand ?? defaultSelfCommand();
|
|
63
|
+
child = spawn(executable!, [...prefix, ...detachedArgv(args)], {
|
|
64
|
+
detached: true, stdio: ['ignore', log, log], cwd: process.cwd(),
|
|
65
|
+
env: { ...process.env, [DETACH_RECORD_ENV]: recordPath },
|
|
66
|
+
});
|
|
67
|
+
child.once('error', error => { spawnError = error; });
|
|
68
|
+
child.once('exit', () => { exited = true; });
|
|
69
|
+
child.unref();
|
|
70
|
+
} finally { closeSync(log); }
|
|
71
|
+
const deadline = Date.now() + DETACHED_START_TIMEOUT_MS;
|
|
72
|
+
let observerDeadline: number | undefined;
|
|
73
|
+
for (;;) {
|
|
74
|
+
const record = readDetachedRecord(recordPath);
|
|
75
|
+
if (record?.phase === 'refused') return { execution: record.execution! };
|
|
76
|
+
if (record?.runId !== undefined) {
|
|
77
|
+
observerDeadline ??= Date.now() + DETACHED_OBSERVER_GRACE_MS;
|
|
78
|
+
if (options.noObserverLink || record.observerUrl !== undefined
|
|
79
|
+
|| exited || Date.now() >= observerDeadline) {
|
|
80
|
+
return { handle: {
|
|
81
|
+
detached: true, runId: record.runId, pid: record.pid, logPath, recordPath,
|
|
82
|
+
...(record.observerUrl === undefined ? {} : { observerUrl: record.observerUrl }),
|
|
83
|
+
follow: `flows status ${shellWord(record.runId)} --data-dir ${shellWord(resolve(options.dataDir))}`,
|
|
84
|
+
notice: 'Started in background; the run may later fail or park. Check status and the log for any park remedy.',
|
|
85
|
+
} };
|
|
86
|
+
}
|
|
87
|
+
} else if (spawnError !== undefined || exited) {
|
|
88
|
+
return failure(`detached_start_failed: ${spawnError?.message ?? 'child exited before publishing a run id'}. Log: ${logPath}\n${logTail(logPath)}`);
|
|
89
|
+
} else if (Date.now() >= deadline) {
|
|
90
|
+
// Signal only this child, never a pre-existing daemon. The agent reaper
|
|
91
|
+
// owns provider-tree cleanup. A timeout is not a run cancellation.
|
|
92
|
+
child.kill('SIGTERM');
|
|
93
|
+
return failure(`detached_start_timeout: no receipt within ${DETACHED_START_TIMEOUT_MS}ms; sent SIGTERM to child. Inspect ${logPath} before retrying; a journal may already exist.\n${logTail(logPath)}`);
|
|
94
|
+
}
|
|
95
|
+
await sleep(POLL_MS);
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
child?.kill('SIGTERM');
|
|
99
|
+
return failure(`detached_start_failed: ${error instanceof Error ? error.message : String(error)}${logPath === undefined ? '' : `. Log: ${logPath}`}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* cli-watch.ts's re-exec precedent supports both source and built entries. A
|
|
105
|
+
* compiled executable is its own entry: its module path is virtual and would
|
|
106
|
+
* reach the CLI as an extra positional, so it passes `selfCommand` instead.
|
|
107
|
+
*/
|
|
108
|
+
function defaultSelfCommand(): string[] {
|
|
109
|
+
const entry = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? '../cli.ts' : '../cli.js', import.meta.url));
|
|
110
|
+
return [process.execPath, ...process.execArgv, entry];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function emitDetachedHandle(handle: DetachedHandle, json: boolean, io: CliIo): void {
|
|
114
|
+
if (json) { io.stdout(JSON.stringify(handle)); return; }
|
|
115
|
+
io.stdout(`DETACHED ${handle.runId} pid ${handle.pid}`);
|
|
116
|
+
io.stdout(`Log: ${handle.logPath}`);
|
|
117
|
+
io.stdout(`Receipt: ${handle.recordPath}`);
|
|
118
|
+
if (handle.observerUrl !== undefined) io.stdout(`Observer: ${handle.observerUrl}`);
|
|
119
|
+
io.stdout(`Follow: ${handle.follow}`);
|
|
120
|
+
io.stdout(handle.notice);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function logTail(path: string): string {
|
|
124
|
+
const fd = openSync(path, 'r');
|
|
125
|
+
try {
|
|
126
|
+
const size = fstatSync(fd).size;
|
|
127
|
+
const buffer = Buffer.alloc(Math.min(size, 4096));
|
|
128
|
+
const count = readSync(fd, buffer, 0, buffer.length, Math.max(0, size - buffer.length));
|
|
129
|
+
return buffer.subarray(0, count).toString('utf8');
|
|
130
|
+
} finally { closeSync(fd); }
|
|
131
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface FoldOptions {
|
|
2
|
+
/** Collapse repeated warnings only when the caller opts in. */
|
|
3
|
+
fold?: boolean;
|
|
4
|
+
explain?: boolean;
|
|
5
|
+
/** Every step in the spec, including llm and agent steps. */
|
|
6
|
+
totalSteps?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Presentation only: retain the original diagnostics for machine consumers. */
|
|
10
|
+
export function foldUnprovableEffects<D extends {
|
|
11
|
+
severity: string; kind: string; message: string; stepId?: string;
|
|
12
|
+
}>(diagnostics: readonly D[], options: FoldOptions = {}): readonly (D | {
|
|
13
|
+
severity: 'warning'; kind: 'unprovable_effects'; message: string;
|
|
14
|
+
})[] {
|
|
15
|
+
if (options.fold !== true || options.explain) return diagnostics;
|
|
16
|
+
const warnings = diagnostics.filter(d => d.kind === 'unprovable_effects');
|
|
17
|
+
const count = new Set(warnings.flatMap(d => d.stepId === undefined ? [] : [d.stepId])).size
|
|
18
|
+
+ warnings.filter(d => d.stepId === undefined).length;
|
|
19
|
+
if (count < 2) return diagnostics;
|
|
20
|
+
const denominator = options.totalSteps === undefined ? '' : ` of ${options.totalSteps}`;
|
|
21
|
+
return [
|
|
22
|
+
...diagnostics.filter(d => d.kind !== 'unprovable_effects'),
|
|
23
|
+
{
|
|
24
|
+
severity: 'warning', kind: 'unprovable_effects',
|
|
25
|
+
message: `${count}${denominator} steps have effects that cannot be proven before execution (rerun with --explain-warnings to list them).`,
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
}
|
package/src/cli/direct-run.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { completedResultUnreadableReport, daemonUnresponsiveReport, isReadInterruption } from './journal-timeout.js';
|
|
2
|
+
import { FLOW_READ_BUDGET_MS } from '../journal-read-policy.js';
|
|
1
3
|
import { onWorkerFailure } from '../worker-lease.js';
|
|
2
4
|
import { McpStepError } from '../authored-mcp.js';
|
|
3
5
|
import { randomUUID } from 'node:crypto';
|
|
@@ -25,6 +27,7 @@ import {
|
|
|
25
27
|
connect,
|
|
26
28
|
emptyReport,
|
|
27
29
|
fromCheckReport,
|
|
30
|
+
memoryWriteFailure,
|
|
28
31
|
protocolFailure,
|
|
29
32
|
suspendedExecution,
|
|
30
33
|
socketFor,
|
|
@@ -67,7 +70,8 @@ export async function runDirectFlow(
|
|
|
67
70
|
}
|
|
68
71
|
const socketPath = socketFor(dataDir);
|
|
69
72
|
const base: RunReport = { ...emptyReport('run'), path };
|
|
70
|
-
const client = new JournalClient(socketPath
|
|
73
|
+
const client = new JournalClient(socketPath, { readBudgetMs: FLOW_READ_BUDGET_MS,
|
|
74
|
+
...(options.signal === undefined ? {} : { readSignal: options.signal }) });
|
|
71
75
|
const connected = await connect(client, 'run', dataDir, base, options);
|
|
72
76
|
if (connected !== undefined) return connected;
|
|
73
77
|
|
|
@@ -105,6 +109,7 @@ export async function runDirectFlow(
|
|
|
105
109
|
lifecycle: {
|
|
106
110
|
onProgress: options.onProgress,
|
|
107
111
|
...(options.onRunStarted !== undefined ? { onRunStarted: options.onRunStarted } : {}),
|
|
112
|
+
...(options.onRunReceipt !== undefined ? { onRunReceipt: options.onRunReceipt } : {}),
|
|
108
113
|
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
109
114
|
...(options.onWait !== undefined ? { onWait: options.onWait } : {}),
|
|
110
115
|
},
|
|
@@ -212,6 +217,14 @@ export async function runDirectFlow(
|
|
|
212
217
|
if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed')) {
|
|
213
218
|
return authoredStepFailure('run', base, socketPath, error);
|
|
214
219
|
}
|
|
220
|
+
if (error instanceof AuthoredFlowExecutionError && error.code === 'result_unreadable') {
|
|
221
|
+
return completedResultUnreadableReport('run', base, socketPath, error);
|
|
222
|
+
}
|
|
223
|
+
if (error instanceof AuthoredFlowExecutionError) {
|
|
224
|
+
const memoryFailure = memoryWriteFailure('run', base, socketPath, error, base.rootRunId);
|
|
225
|
+
if (memoryFailure !== undefined) return memoryFailure;
|
|
226
|
+
}
|
|
227
|
+
if (isReadInterruption(error)) return daemonUnresponsiveReport('run', base, socketPath, error, base.rootRunId, options, dataDir);
|
|
215
228
|
if (error instanceof AuthoredFlowExecutionError && error.code === 'root_lease_lost') {
|
|
216
229
|
return rootLeaseLostReport('run', base, socketPath, error);
|
|
217
230
|
}
|
package/src/cli/eval.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import type { CliIo } from '../cli.js';
|
|
3
|
+
import { CloudFlowError } from '../cloud-http.js';
|
|
4
|
+
import {
|
|
5
|
+
evaluateFlow, FlowEvalError, loadFlowEvalSuite,
|
|
6
|
+
type FlowEvalExecutor, type FlowEvalReport,
|
|
7
|
+
} from '../flow-eval.js';
|
|
8
|
+
import { cloudFlowEvalExecutor } from '../flow-eval-cloud.js';
|
|
9
|
+
import { localFlowEvalExecutor } from '../flow-eval-local.js';
|
|
10
|
+
|
|
11
|
+
export interface EvalArgs {
|
|
12
|
+
command: 'eval';
|
|
13
|
+
value: string;
|
|
14
|
+
cases: string;
|
|
15
|
+
json: boolean;
|
|
16
|
+
cloud: boolean;
|
|
17
|
+
dataDir: string;
|
|
18
|
+
localAgent: boolean;
|
|
19
|
+
concurrency?: number;
|
|
20
|
+
report?: string;
|
|
21
|
+
baseline?: string;
|
|
22
|
+
expectVersion?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseEvalArgs(args: readonly string[]): EvalArgs | undefined {
|
|
26
|
+
const flags = new Set<string>();
|
|
27
|
+
const values = new Map<string, string>();
|
|
28
|
+
const positionals: string[] = [];
|
|
29
|
+
const valued = ['--cases', '--data-dir', '--concurrency', '--report', '--baseline', '--expect-version'];
|
|
30
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
31
|
+
const argument = args[index]!;
|
|
32
|
+
if (argument === '--json' || argument === '--cloud' || argument === '--local-agent') {
|
|
33
|
+
if (flags.has(argument)) return undefined;
|
|
34
|
+
flags.add(argument);
|
|
35
|
+
} else if (valued.includes(argument)) {
|
|
36
|
+
const value = args[++index];
|
|
37
|
+
if (value === undefined || value.length === 0 || value.startsWith('-') || values.has(argument)) return undefined;
|
|
38
|
+
values.set(argument, value);
|
|
39
|
+
} else if (argument.startsWith('-')) {
|
|
40
|
+
return undefined;
|
|
41
|
+
} else {
|
|
42
|
+
positionals.push(argument);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const cases = values.get('--cases');
|
|
46
|
+
if (positionals.length !== 1 || cases === undefined) return undefined;
|
|
47
|
+
// Local-only options have no meaning against Cloud; refuse rather than ignore.
|
|
48
|
+
if (flags.has('--cloud') && (flags.has('--local-agent') || values.has('--data-dir'))) return undefined;
|
|
49
|
+
let concurrency: number | undefined;
|
|
50
|
+
if (values.has('--concurrency')) {
|
|
51
|
+
concurrency = Number(values.get('--concurrency'));
|
|
52
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 64) return undefined;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
command: 'eval',
|
|
56
|
+
value: positionals[0]!,
|
|
57
|
+
cases,
|
|
58
|
+
json: flags.has('--json'),
|
|
59
|
+
cloud: flags.has('--cloud'),
|
|
60
|
+
localAgent: flags.has('--local-agent'),
|
|
61
|
+
dataDir: values.get('--data-dir') ?? '.relayflowd',
|
|
62
|
+
...(concurrency === undefined ? {} : { concurrency }),
|
|
63
|
+
...(values.has('--report') ? { report: values.get('--report')! } : {}),
|
|
64
|
+
...(values.has('--baseline') ? { baseline: values.get('--baseline')! } : {}),
|
|
65
|
+
...(values.has('--expect-version') ? { expectVersion: values.get('--expect-version')! } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* `flows eval`: re-execute the flow against every case and print the report.
|
|
71
|
+
* Exit 0 when the gate passes, 1 when it fails, 2 when the evaluation was refused.
|
|
72
|
+
*/
|
|
73
|
+
export async function runEvalCli(
|
|
74
|
+
args: EvalArgs,
|
|
75
|
+
io: CliIo,
|
|
76
|
+
signal: AbortSignal,
|
|
77
|
+
executor?: FlowEvalExecutor,
|
|
78
|
+
): Promise<0 | 1 | 2> {
|
|
79
|
+
let report: FlowEvalReport;
|
|
80
|
+
try {
|
|
81
|
+
const suite = await loadFlowEvalSuite(args.cases);
|
|
82
|
+
let baseline: unknown;
|
|
83
|
+
if (args.baseline !== undefined) {
|
|
84
|
+
try {
|
|
85
|
+
baseline = JSON.parse(await readFile(args.baseline, 'utf8'));
|
|
86
|
+
} catch {
|
|
87
|
+
throw new FlowEvalError('invalid_baseline', `Baseline report "${args.baseline}" is not readable JSON.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
report = await evaluateFlow({
|
|
91
|
+
flow: { path: args.value },
|
|
92
|
+
suite,
|
|
93
|
+
executor: executor ?? (args.cloud
|
|
94
|
+
? cloudFlowEvalExecutor({ signal })
|
|
95
|
+
: localFlowEvalExecutor({ dataDir: args.dataDir, localAgent: args.localAgent })),
|
|
96
|
+
executorName: executor !== undefined ? 'custom' : args.cloud ? 'cloud' : 'local',
|
|
97
|
+
signal,
|
|
98
|
+
...(args.concurrency === undefined ? {} : { concurrency: args.concurrency }),
|
|
99
|
+
...(args.expectVersion === undefined ? {} : { expectVersion: args.expectVersion }),
|
|
100
|
+
...(baseline === undefined ? {} : { baseline }),
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (signal.aborted) {
|
|
104
|
+
io.stderr('CANCELED evaluation interrupted; runs already started are not stopped by this command.');
|
|
105
|
+
return 2;
|
|
106
|
+
}
|
|
107
|
+
const kind = error instanceof FlowEvalError || error instanceof CloudFlowError ? error.code : 'eval_failed';
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
return refuse(args, io, kind, message);
|
|
110
|
+
}
|
|
111
|
+
if (args.report !== undefined) {
|
|
112
|
+
try {
|
|
113
|
+
await writeFile(args.report, `${JSON.stringify(report, null, 2)}\n`);
|
|
114
|
+
} catch {
|
|
115
|
+
return refuse(args, io, 'report_unwritable', `Could not write the report to "${args.report}".`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (args.json) {
|
|
119
|
+
io.stdout(JSON.stringify(report));
|
|
120
|
+
} else {
|
|
121
|
+
for (const result of report.cases) {
|
|
122
|
+
const cost = result.costUsd === null ? 'cost=?' : `cost=$${result.costUsd}`;
|
|
123
|
+
io.stdout(`${result.outcome.toUpperCase()} ${result.id} latency=${result.latencyMs}ms ${cost}`
|
|
124
|
+
+ (result.runId === undefined ? '' : ` run=${result.runId}`));
|
|
125
|
+
for (const failure of result.failures) io.stdout(` - ${failure}`);
|
|
126
|
+
}
|
|
127
|
+
const { summary } = report;
|
|
128
|
+
io.stdout(`${summary.passed}/${summary.total} passed, ${summary.failed} failed, ${summary.errored} errored; `
|
|
129
|
+
+ `p50=${summary.latencyMs.p50}ms p95=${summary.latencyMs.p95}ms `
|
|
130
|
+
+ `cost=${summary.totalCostUsd === null ? '?' : `$${summary.totalCostUsd}`}`);
|
|
131
|
+
io.stdout(`flow ${report.flow.version} suite ${report.suite.sha256}`);
|
|
132
|
+
io.stdout(report.gate.pass ? 'GATE PASS' : `GATE FAIL: ${report.gate.reasons.join('; ')}`);
|
|
133
|
+
}
|
|
134
|
+
return report.gate.pass ? 0 : 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function refuse(args: EvalArgs, io: CliIo, kind: string, message: string): 2 {
|
|
138
|
+
if (args.json) io.stdout(JSON.stringify({ ok: false, diagnostics: [{ severity: 'refusal', kind, message }] }));
|
|
139
|
+
else io.stderr(`REFUSED [${kind}] ${message}`);
|
|
140
|
+
return 2;
|
|
141
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { probeSocket } from '../daemon-connection.js';
|
|
2
|
+
import { AuthoredFlowExecutionError } from '../authored-flow-error.js';
|
|
3
|
+
import { resumeCommand } from '../authored-human.js';
|
|
4
|
+
import { isReadInterruptionError, JournalReadInterruptedError } from '../journal-read-policy.js';
|
|
5
|
+
import type { CheckReport } from './check.js';
|
|
6
|
+
import type { RunExecution, RunReport, RunLifecycleOptions, RunCommand } from './run.js';
|
|
7
|
+
|
|
8
|
+
export function isReadInterruption(error: unknown): boolean {
|
|
9
|
+
return isReadInterruptionError(error)
|
|
10
|
+
|| (error instanceof AuthoredFlowExecutionError && error.code === 'daemon_unresponsive');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A completed run whose stored result could not be read back. The run is
|
|
15
|
+
* finished — nothing to resume — but its authored verdict is unknown, so the
|
|
16
|
+
* report claims neither success nor a reason, and says how to read it later.
|
|
17
|
+
*/
|
|
18
|
+
export function completedResultUnreadableReport(command: RunCommand, base: CheckReport | RunReport,
|
|
19
|
+
socketPath: string, error: AuthoredFlowExecutionError): RunExecution {
|
|
20
|
+
const runId = error.rootRunId ?? error.runId;
|
|
21
|
+
return { exitCode: 1, report: {
|
|
22
|
+
...base, command, ok: false, socketPath, status: 'completed',
|
|
23
|
+
...(runId === undefined ? {} : { runId, rootRunId: runId }),
|
|
24
|
+
diagnostics: [...base.diagnostics, { severity: 'warning', kind: 'result_unreadable',
|
|
25
|
+
message: `${error.message}. The run is finished; its verdict is unconfirmed`
|
|
26
|
+
+ (runId === undefined ? '.' : ` — read it with \`flows status ${runId}\` once relayflowd answers.`) }],
|
|
27
|
+
} as RunExecution['report'] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function daemonUnresponsiveReport(command: RunCommand,
|
|
31
|
+
base: CheckReport | RunReport, socketPath: string, error: unknown, fallbackRunId?: string,
|
|
32
|
+
options: RunLifecycleOptions = {}, dataDir?: string): Promise<RunExecution> {
|
|
33
|
+
const runId = error instanceof AuthoredFlowExecutionError ? error.rootRunId ?? fallbackRunId : fallbackRunId;
|
|
34
|
+
const reachable = (await probeSocket(socketPath)).reachable;
|
|
35
|
+
const kind = reachable ? 'daemon_unresponsive' : 'daemon_unreachable';
|
|
36
|
+
const detail = error instanceof JournalReadInterruptedError
|
|
37
|
+
? (reachable ? 'relayflowd answers a fresh connection but the run read session kept being interrupted.'
|
|
38
|
+
: 'relayflowd did not answer a fresh connection after the run read session was interrupted; it may be unreachable or delayed by CPU load.')
|
|
39
|
+
: reachable ? 'relayflowd answers a fresh connection but the run read timed out; CPU load may be delaying it.'
|
|
40
|
+
: 'relayflowd did not answer a fresh connection; it may be unreachable or delayed by CPU load.';
|
|
41
|
+
return { exitCode: 1, report: {
|
|
42
|
+
...base, command, ok: false, socketPath, status: 'running',
|
|
43
|
+
...(runId === undefined ? {} : { runId, rootRunId: runId }),
|
|
44
|
+
diagnostics: [...base.diagnostics, { severity: 'failure', kind,
|
|
45
|
+
message: `${detail} ${error instanceof Error ? error.message : String(error)}`
|
|
46
|
+
+ (runId === undefined ? '' : ` The run remains resumable. Continue with: ${resumeCommand(runId, dataDir ?? options.dataDir, options.localAgent === true)}.`),
|
|
47
|
+
}],
|
|
48
|
+
} };
|
|
49
|
+
}
|
|
@@ -66,6 +66,8 @@ export function authoredInput(argument: string | undefined): AuthoredInput {
|
|
|
66
66
|
*/
|
|
67
67
|
const PINS = 'Declared workspace or stream surfaces require a worker that holds their pins.';
|
|
68
68
|
|
|
69
|
+
const DETACH = ' --local-agent binds the worker to this terminal. Add --detach to run in the background.';
|
|
70
|
+
|
|
69
71
|
/**
|
|
70
72
|
* The remedy clause appended to a worker-park diagnostic, with a leading
|
|
71
73
|
* space, or `''` when there is nothing to add.
|
|
@@ -82,11 +84,11 @@ export function localAgentRemedy(remedy: LocalAgentRemedy): string {
|
|
|
82
84
|
+ ` ${PINS} The local agent worker holds only its own stream.`;
|
|
83
85
|
case 'spec-run':
|
|
84
86
|
return ` To start a new run with a local agent worker: ${
|
|
85
|
-
newRunCommand(remedy.path, undefined, remedy.dataDir)}. ${PINS}`;
|
|
87
|
+
newRunCommand(remedy.path, undefined, remedy.dataDir)}. ${PINS}${DETACH}`;
|
|
86
88
|
case 'spec-resume':
|
|
87
89
|
// This run is resumable, so the remedy is this run — not a new one.
|
|
88
90
|
return ` To continue this run with a local agent worker: ${
|
|
89
|
-
resumeCommand(remedy.runId, remedy.dataDir, true)}. ${PINS}`;
|
|
91
|
+
resumeCommand(remedy.runId, remedy.dataDir, true)}. ${PINS}${DETACH}`;
|
|
90
92
|
case 'authored-run':
|
|
91
93
|
return authoredRunRemedy(remedy);
|
|
92
94
|
}
|
|
@@ -103,7 +105,7 @@ export function localAgentRemedy(remedy: LocalAgentRemedy): string {
|
|
|
103
105
|
function authoredRunRemedy(remedy: Extract<LocalAgentRemedy, { kind: 'authored-run' }>): string {
|
|
104
106
|
if (remedy.input.kind === 'inline') {
|
|
105
107
|
return ` To start a new run with a local agent worker: ${
|
|
106
|
-
newRunCommand(remedy.path, remedy.input.argument, remedy.dataDir)}. ${PINS}`;
|
|
108
|
+
newRunCommand(remedy.path, remedy.input.argument, remedy.dataDir)}. ${PINS}${DETACH}`;
|
|
107
109
|
}
|
|
108
110
|
const requirement = ' A local agent worker is admitted at run start, so this run needs a new one.'
|
|
109
111
|
+ ` Starting one needs --local-agent, the flow path ${shellQuote(remedy.path)}, and the same`
|
|
@@ -115,7 +117,7 @@ function authoredRunRemedy(remedy: Extract<LocalAgentRemedy, { kind: 'authored-r
|
|
|
115
117
|
// printed `--input '<json>'` would fail before the CLI ran at all.
|
|
116
118
|
: `the journal recorded ${remedy.input.bytes} bytes of input, more than a shell`
|
|
117
119
|
+ ' can carry in one argument, so pass the input file this run was started from.')
|
|
118
|
-
+ ` ${PINS}`;
|
|
120
|
+
+ ` ${PINS}${DETACH}`;
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
/**
|
|
@@ -36,6 +36,7 @@ export interface ObserverSession {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export interface ObserverSessionDeps {
|
|
39
|
+
onObserverUrl?: (url: string) => void;
|
|
39
40
|
fetch?: ProjectionFetch;
|
|
40
41
|
mint?: (options: MintObserverOptions) => Promise<MintOutcome>;
|
|
41
42
|
}
|
|
@@ -72,7 +73,12 @@ export function createObserverSession(
|
|
|
72
73
|
diagnostic: message => io.stderr(`[observer] ${message}`),
|
|
73
74
|
}, run);
|
|
74
75
|
void mintFor(run.runId).then(outcome => {
|
|
75
|
-
if (outcome.observerUrl !== undefined)
|
|
76
|
+
if (outcome.observerUrl !== undefined) {
|
|
77
|
+
io.stderr(`Observer: ${outcome.observerUrl}`);
|
|
78
|
+
try { deps.onObserverUrl?.(outcome.observerUrl); } catch (error) {
|
|
79
|
+
io.stderr(`[observer] could not publish observer URL: ${error instanceof Error ? error.message : String(error)}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
76
82
|
});
|
|
77
83
|
return projection;
|
|
78
84
|
};
|