@relayflows/sdk 2.0.29 → 2.0.31
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/agent-artifacts.d.ts.map +1 -1
- package/dist/agent-artifacts.js +2 -9
- package/dist/agent-artifacts.js.map +1 -1
- package/dist/agent-cwd.d.ts +69 -0
- package/dist/agent-cwd.d.ts.map +1 -0
- package/dist/agent-cwd.js +140 -0
- package/dist/agent-cwd.js.map +1 -0
- package/dist/artifact-scan-policy.d.ts +35 -0
- package/dist/artifact-scan-policy.d.ts.map +1 -0
- package/dist/artifact-scan-policy.js +49 -0
- package/dist/artifact-scan-policy.js.map +1 -0
- package/dist/authored-completion-record.d.ts +35 -0
- package/dist/authored-completion-record.d.ts.map +1 -0
- package/dist/authored-completion-record.js +92 -0
- package/dist/authored-completion-record.js.map +1 -0
- package/dist/authored-completion.d.ts +99 -0
- package/dist/authored-completion.d.ts.map +1 -0
- package/dist/authored-completion.js +184 -0
- package/dist/authored-completion.js.map +1 -0
- package/dist/authored-flow-error.d.ts +13 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js +12 -0
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts +7 -30
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +64 -48
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-human.d.ts.map +1 -1
- package/dist/authored-human.js +1 -3
- package/dist/authored-human.js.map +1 -1
- package/dist/authored-node-entry.js +4 -1
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts +9 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +92 -17
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-root.d.ts +5 -0
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +20 -2
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-step-output.d.ts +35 -10
- package/dist/authored-step-output.d.ts.map +1 -1
- package/dist/authored-step-output.js +108 -40
- package/dist/authored-step-output.js.map +1 -1
- package/dist/authored-verdict.d.ts +28 -0
- package/dist/authored-verdict.d.ts.map +1 -0
- package/dist/authored-verdict.js +79 -0
- package/dist/authored-verdict.js.map +1 -0
- package/dist/authored-worker-step.d.ts +5 -2
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +48 -11
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/bundle-extensions.d.ts.map +1 -1
- package/dist/bundle-extensions.js +4 -7
- package/dist/bundle-extensions.js.map +1 -1
- package/dist/bundle.d.ts.map +1 -1
- package/dist/bundle.js +41 -7
- package/dist/bundle.js.map +1 -1
- package/dist/canonical.d.ts.map +1 -1
- package/dist/canonical.js +18 -6
- package/dist/canonical.js.map +1 -1
- package/dist/cli/check-worker-surface.d.ts +27 -0
- package/dist/cli/check-worker-surface.d.ts.map +1 -0
- package/dist/cli/check-worker-surface.js +63 -0
- package/dist/cli/check-worker-surface.js.map +1 -0
- package/dist/cli/check.d.ts +19 -2
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +12 -4
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-format.d.ts +9 -0
- package/dist/cli/cloud-format.d.ts.map +1 -0
- package/dist/cli/cloud-format.js +77 -0
- package/dist/cli/cloud-format.js.map +1 -0
- package/dist/cli/cloud-live.d.ts +39 -0
- package/dist/cli/cloud-live.d.ts.map +1 -0
- package/dist/cli/cloud-live.js +325 -0
- package/dist/cli/cloud-live.js.map +1 -0
- package/dist/cli/cloud-read.d.ts +3 -7
- package/dist/cli/cloud-read.d.ts.map +1 -1
- package/dist/cli/cloud-read.js +14 -266
- package/dist/cli/cloud-read.js.map +1 -1
- package/dist/cli/cloud-refusal.d.ts +39 -0
- package/dist/cli/cloud-refusal.d.ts.map +1 -0
- package/dist/cli/cloud-refusal.js +95 -0
- package/dist/cli/cloud-refusal.js.map +1 -0
- package/dist/cli/cloud-status-view.d.ts +7 -0
- package/dist/cli/cloud-status-view.d.ts.map +1 -0
- package/dist/cli/cloud-status-view.js +214 -0
- package/dist/cli/cloud-status-view.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/hn-monitor.d.ts +0 -13
- package/dist/cli/hn-monitor.d.ts.map +1 -1
- package/dist/cli/hn-monitor.js +2 -1
- package/dist/cli/hn-monitor.js.map +1 -1
- package/dist/cli/local-agent-remedy.d.ts +80 -0
- package/dist/cli/local-agent-remedy.d.ts.map +1 -0
- package/dist/cli/local-agent-remedy.js +116 -0
- package/dist/cli/local-agent-remedy.js.map +1 -0
- package/dist/cli/observer-session.d.ts +41 -0
- package/dist/cli/observer-session.d.ts.map +1 -0
- package/dist/cli/observer-session.js +78 -0
- package/dist/cli/observer-session.js.map +1 -0
- package/dist/cli/run.d.ts +30 -2
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +186 -24
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/status.d.ts +9 -0
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +31 -2
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/step-evidence.d.ts +90 -0
- package/dist/cli/step-evidence.d.ts.map +1 -0
- package/dist/cli/step-evidence.js +259 -0
- package/dist/cli/step-evidence.js.map +1 -0
- package/dist/cli/step-failure.d.ts +14 -4
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +41 -76
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cli-commands.d.ts +6 -0
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +2 -0
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +10 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +53 -42
- package/dist/cli.js.map +1 -1
- package/dist/cloud-read.d.ts +15 -0
- package/dist/cloud-read.d.ts.map +1 -1
- package/dist/cloud-read.js +20 -1
- package/dist/cloud-read.js.map +1 -1
- package/dist/cloud-run-record.d.ts +27 -0
- package/dist/cloud-run-record.d.ts.map +1 -0
- package/dist/cloud-run-record.js +56 -0
- package/dist/cloud-run-record.js.map +1 -0
- package/dist/cloud-run.d.ts +2 -9
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +3 -20
- package/dist/cloud-run.js.map +1 -1
- package/dist/communication/local.d.ts.map +1 -1
- package/dist/communication/local.js +2 -1
- package/dist/communication/local.js.map +1 -1
- package/dist/communication/worker.d.ts +1 -1
- package/dist/communication/worker.d.ts.map +1 -1
- package/dist/communication/worker.js +13 -5
- package/dist/communication/worker.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +28 -6
- package/dist/compile.js.map +1 -1
- package/dist/direct-input.d.ts.map +1 -1
- package/dist/direct-input.js +21 -1
- package/dist/direct-input.js.map +1 -1
- package/dist/failure-kinds.d.ts +96 -6
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +29 -2
- package/dist/failure-kinds.js.map +1 -1
- package/dist/flow-extension-compat.d.ts.map +1 -1
- package/dist/flow-extension-compat.js +26 -8
- package/dist/flow-extension-compat.js.map +1 -1
- package/dist/flow-extension-loader.d.ts +7 -0
- package/dist/flow-extension-loader.d.ts.map +1 -1
- package/dist/flow-extension-loader.js +48 -22
- package/dist/flow-extension-loader.js.map +1 -1
- package/dist/flow-requirements.d.ts.map +1 -1
- package/dist/flow-requirements.js +24 -9
- package/dist/flow-requirements.js.map +1 -1
- package/dist/fs-descriptor.d.ts +17 -0
- package/dist/fs-descriptor.d.ts.map +1 -0
- package/dist/fs-descriptor.js +96 -0
- package/dist/fs-descriptor.js.map +1 -0
- package/dist/gate-contract.d.ts +7 -0
- package/dist/gate-contract.d.ts.map +1 -1
- package/dist/gate-contract.js +6 -1
- package/dist/gate-contract.js.map +1 -1
- package/dist/helper-preflight.d.ts.map +1 -1
- package/dist/helper-preflight.js +10 -3
- package/dist/helper-preflight.js.map +1 -1
- package/dist/helper-reference.d.ts +25 -0
- package/dist/helper-reference.d.ts.map +1 -0
- package/dist/helper-reference.js +129 -0
- package/dist/helper-reference.js.map +1 -0
- package/dist/hosted-base-snapshot.d.ts +25 -0
- package/dist/hosted-base-snapshot.d.ts.map +1 -0
- package/dist/hosted-base-snapshot.js +278 -0
- package/dist/hosted-base-snapshot.js.map +1 -0
- package/dist/hosted-extension-declarations.d.ts +10 -0
- package/dist/hosted-extension-declarations.d.ts.map +1 -0
- package/dist/hosted-extension-declarations.js +272 -0
- package/dist/hosted-extension-declarations.js.map +1 -0
- package/dist/hosted-extension-isolation.d.ts +82 -0
- package/dist/hosted-extension-isolation.d.ts.map +1 -0
- package/dist/hosted-extension-isolation.js +322 -0
- package/dist/hosted-extension-isolation.js.map +1 -0
- package/dist/hosted-extension-manifest.d.ts +9 -0
- package/dist/hosted-extension-manifest.d.ts.map +1 -0
- package/dist/hosted-extension-manifest.js +235 -0
- package/dist/hosted-extension-manifest.js.map +1 -0
- package/dist/hosted-extension-protocol.d.ts +16 -0
- package/dist/hosted-extension-protocol.d.ts.map +1 -0
- package/dist/hosted-extension-protocol.js +224 -0
- package/dist/hosted-extension-protocol.js.map +1 -0
- package/dist/hosted-extension-runtime.d.ts +30 -0
- package/dist/hosted-extension-runtime.d.ts.map +1 -0
- package/dist/hosted-extension-runtime.js +167 -0
- package/dist/hosted-extension-runtime.js.map +1 -0
- package/dist/hosted-extension-sandbox-source.d.ts +9 -0
- package/dist/hosted-extension-sandbox-source.d.ts.map +1 -0
- package/dist/hosted-extension-sandbox-source.js +95 -0
- package/dist/hosted-extension-sandbox-source.js.map +1 -0
- package/dist/hosted-extension-sandbox.d.ts +39 -0
- package/dist/hosted-extension-sandbox.d.ts.map +1 -0
- package/dist/hosted-extension-sandbox.js +418 -0
- package/dist/hosted-extension-sandbox.js.map +1 -0
- package/dist/hosted-project.d.ts +3 -0
- package/dist/hosted-project.d.ts.map +1 -0
- package/dist/hosted-project.js +19 -0
- package/dist/hosted-project.js.map +1 -0
- package/dist/hosted-promise-safety.d.ts +12 -0
- package/dist/hosted-promise-safety.d.ts.map +1 -0
- package/dist/hosted-promise-safety.js +63 -0
- package/dist/hosted-promise-safety.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/intrinsic-array.d.ts +3 -0
- package/dist/intrinsic-array.d.ts.map +1 -0
- package/dist/intrinsic-array.js +14 -0
- package/dist/intrinsic-array.js.map +1 -0
- package/dist/journal-client.d.ts +3 -1
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +17 -2
- package/dist/journal-client.js.map +1 -1
- package/dist/journal-projection.d.ts +16 -0
- package/dist/journal-projection.d.ts.map +1 -0
- package/dist/journal-projection.js +108 -0
- package/dist/journal-projection.js.map +1 -0
- package/dist/json-value.d.ts +6 -1
- package/dist/json-value.d.ts.map +1 -1
- package/dist/json-value.js +155 -36
- package/dist/json-value.js.map +1 -1
- package/dist/llm-worker.d.ts +9 -0
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +16 -2
- package/dist/llm-worker.js.map +1 -1
- package/dist/local-agent.d.ts +1 -1
- package/dist/local-agent.d.ts.map +1 -1
- package/dist/local-agent.js +4 -3
- package/dist/local-agent.js.map +1 -1
- package/dist/named-gate-lowering.js +35 -5
- package/dist/named-gate-lowering.js.map +1 -1
- package/dist/named-gate-preflight.d.ts +27 -0
- package/dist/named-gate-preflight.d.ts.map +1 -0
- package/dist/named-gate-preflight.js +47 -0
- package/dist/named-gate-preflight.js.map +1 -0
- package/dist/named-gates.d.ts +1 -1
- package/dist/named-gates.d.ts.map +1 -1
- package/dist/named-gates.js +3 -0
- package/dist/named-gates.js.map +1 -1
- package/dist/observer-link.d.ts +7 -0
- package/dist/observer-link.d.ts.map +1 -1
- package/dist/observer-link.js +3 -1
- package/dist/observer-link.js.map +1 -1
- package/dist/plugin-store.d.ts +8 -2
- package/dist/plugin-store.d.ts.map +1 -1
- package/dist/plugin-store.js +233 -56
- package/dist/plugin-store.js.map +1 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +7 -0
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/redact.d.ts +42 -0
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +128 -0
- package/dist/redact.js.map +1 -1
- package/dist/run-projection.d.ts +83 -0
- package/dist/run-projection.d.ts.map +1 -0
- package/dist/run-projection.js +199 -0
- package/dist/run-projection.js.map +1 -0
- package/dist/semver-range.d.ts.map +1 -1
- package/dist/semver-range.js +27 -15
- package/dist/semver-range.js.map +1 -1
- package/dist/shell-word.d.ts +19 -0
- package/dist/shell-word.d.ts.map +1 -0
- package/dist/shell-word.js +23 -0
- package/dist/shell-word.js.map +1 -0
- package/dist/spec.d.ts +52 -3
- 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/steps-green.d.ts +27 -0
- package/dist/steps-green.d.ts.map +1 -0
- package/dist/steps-green.js +164 -0
- package/dist/steps-green.js.map +1 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +39 -8
- package/dist/validate.js.map +1 -1
- package/dist/worker-lease.d.ts +8 -1
- package/dist/worker-lease.d.ts.map +1 -1
- package/dist/worker-lease.js +34 -4
- package/dist/worker-lease.js.map +1 -1
- package/dist/worker.d.ts +8 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +17 -9
- package/dist/worker.js.map +1 -1
- package/package.json +3 -7
- package/src/agent-artifacts.ts +2 -10
- package/src/agent-cwd.ts +139 -0
- package/src/artifact-scan-policy.ts +51 -0
- package/src/authored-completion-record.ts +119 -0
- package/src/authored-completion.ts +203 -0
- package/src/authored-flow-error.ts +13 -1
- package/src/authored-flow-executor.ts +101 -59
- package/src/authored-human.ts +1 -4
- package/src/authored-node-entry.ts +4 -1
- package/src/authored-node-runner.ts +96 -22
- package/src/authored-root.ts +20 -2
- package/src/authored-step-output.ts +139 -42
- package/src/authored-verdict.ts +87 -0
- package/src/authored-worker-step.ts +60 -12
- package/src/bundle-extensions.ts +6 -5
- package/src/bundle.ts +52 -7
- package/src/canonical.ts +20 -6
- package/src/cli/check-worker-surface.ts +66 -0
- package/src/cli/check.ts +35 -4
- package/src/cli/cloud-format.ts +82 -0
- package/src/cli/cloud-live.ts +366 -0
- package/src/cli/cloud-read.ts +13 -282
- package/src/cli/cloud-refusal.ts +113 -0
- package/src/cli/cloud-status-view.ts +213 -0
- package/src/cli/direct-run.ts +19 -2
- package/src/cli/hn-monitor.ts +2 -1
- package/src/cli/local-agent-remedy.ts +160 -0
- package/src/cli/observer-session.ts +107 -0
- package/src/cli/run.ts +233 -28
- package/src/cli/status.ts +59 -3
- package/src/cli/step-evidence.ts +310 -0
- package/src/cli/step-failure.ts +57 -81
- package/src/cli-commands.ts +2 -0
- package/src/cli.ts +64 -44
- package/src/cloud-read.ts +25 -1
- package/src/cloud-run-record.ts +64 -0
- package/src/cloud-run.ts +5 -24
- package/src/communication/local.ts +2 -1
- package/src/communication/worker.ts +13 -5
- package/src/compile.ts +28 -6
- package/src/direct-input.ts +22 -1
- package/src/failure-kinds.ts +104 -2
- package/src/flow-extension-compat.ts +32 -7
- package/src/flow-extension-loader.ts +56 -21
- package/src/flow-requirements.ts +24 -9
- package/src/fs-descriptor.ts +127 -0
- package/src/gate-contract.ts +13 -1
- package/src/helper-preflight.ts +10 -3
- package/src/helper-reference.ts +130 -0
- package/src/hosted-base-snapshot.ts +351 -0
- package/src/hosted-extension-declarations.ts +387 -0
- package/src/hosted-extension-isolation.ts +457 -0
- package/src/hosted-extension-manifest.ts +249 -0
- package/src/hosted-extension-protocol.ts +262 -0
- package/src/hosted-extension-runtime.ts +266 -0
- package/src/hosted-extension-sandbox-source.ts +94 -0
- package/src/hosted-extension-sandbox.ts +489 -0
- package/src/hosted-project.ts +18 -0
- package/src/hosted-promise-safety.ts +68 -0
- package/src/index.ts +20 -0
- package/src/intrinsic-array.ts +14 -0
- package/src/journal-client.ts +19 -2
- package/src/journal-projection.ts +106 -0
- package/src/json-value.ts +184 -32
- package/src/llm-worker.ts +16 -2
- package/src/local-agent.ts +4 -2
- package/src/named-gate-lowering.ts +35 -5
- package/src/named-gate-preflight.ts +47 -0
- package/src/named-gates.ts +3 -0
- package/src/observer-link.ts +10 -1
- package/src/plugin-store.ts +323 -56
- package/src/preflight.ts +7 -0
- package/src/protocol.ts +2 -0
- package/src/redact.ts +111 -0
- package/src/run-projection.ts +247 -0
- package/src/semver-range.ts +39 -14
- package/src/shell-word.ts +24 -0
- package/src/spec.ts +54 -3
- package/src/step-fields.ts +1 -1
- package/src/steps-green.ts +168 -0
- package/src/validate.ts +35 -8
- package/src/worker-lease.ts +35 -5
- package/src/worker.ts +27 -11
|
@@ -8,12 +8,14 @@ import { isAbsolute, join } from 'node:path';
|
|
|
8
8
|
import type { Readable } from 'node:stream';
|
|
9
9
|
import type { AuthoredRootMetadata } from './authored-root.js';
|
|
10
10
|
import type { AuthoredExecutionRuntime, AuthoredFlowExecutionResult, ExecuteAuthoredFlowOptions } from './authored-flow-executor.js';
|
|
11
|
-
import { completionMarker, isLoweredCompletion } from './authored-
|
|
11
|
+
import { completionMarker, isDurableCompletionDetail, isLoweredCompletion } from './authored-completion.js';
|
|
12
12
|
import {
|
|
13
13
|
AuthoredFlowExecutionError, AuthoredHumanParked,
|
|
14
14
|
type AuthoredFlowExecutionErrorCode, type AuthoredHumanWait,
|
|
15
15
|
} from './authored-flow-error.js';
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
AttemptEvidenceComparison, ParkCause, StepAttemptFailure, StepFailedDetails,
|
|
18
|
+
} from './failure-kinds.js';
|
|
17
19
|
import { HUMAN_WAIT_ID } from './authored-human.js';
|
|
18
20
|
import { assertAuthoredPromiseHooks } from './authored-runtime-capability.js';
|
|
19
21
|
|
|
@@ -137,13 +139,18 @@ export async function runAuthoredInNode(
|
|
|
137
139
|
else if (message.type === 'wait') options.onWait?.(message.event);
|
|
138
140
|
else if (message.type === 'result') result = { ...message.result, executionRuntime: runtime };
|
|
139
141
|
else if (message.type === 'error') {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
if (message.code === 'human_parked' && isHumanWaitFrame(message.wait) && message.runId === rootRunId) {
|
|
143
|
+
failure = new AuthoredHumanParked(message.wait, rootRunId);
|
|
144
|
+
} else if (typeof message.code === 'string') {
|
|
145
|
+
const authored = new AuthoredFlowExecutionError(message.code as AuthoredFlowExecutionErrorCode,
|
|
146
|
+
message.message, message.completionReason, message.runId,
|
|
147
|
+
stepFailedFrame(message.details));
|
|
148
|
+
// Validated, not trusted, like `details`: an unrecognised cause
|
|
149
|
+
// is dropped so the boundary says nothing about workers rather
|
|
150
|
+
// than acting on a value this frame could have invented.
|
|
151
|
+
authored.parkCause = parkCauseFrame(message.parkCause);
|
|
152
|
+
failure = authored;
|
|
153
|
+
} else failure = new Error(message.message);
|
|
147
154
|
} else throw new Error('unknown authored runtime message');
|
|
148
155
|
} catch (error) { stop(error instanceof Error ? error : new Error('invalid authored runtime message')); }
|
|
149
156
|
}
|
|
@@ -174,6 +181,17 @@ function isHumanWaitFrame(value: unknown): value is AuthoredHumanWait {
|
|
|
174
181
|
&& typeof wait.to === 'string' && wait.to !== '';
|
|
175
182
|
}
|
|
176
183
|
|
|
184
|
+
/**
|
|
185
|
+
* A park cause arriving over IPC, accepted only as one of the two values the
|
|
186
|
+
* type admits. Anything else — absent, misspelled, a different type — becomes
|
|
187
|
+
* `undefined`, which the remedy formatter reads as "unestablished" and answers
|
|
188
|
+
* with silence. Guessing `worker_unavailable` here would let a malformed frame
|
|
189
|
+
* put "attach a worker" on a park a worker cannot clear.
|
|
190
|
+
*/
|
|
191
|
+
export function parkCauseFrame(value: unknown): ParkCause | undefined {
|
|
192
|
+
return value === 'worker_unavailable' || value === 'needs_human' ? value : undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
177
195
|
/**
|
|
178
196
|
* Step evidence arriving over IPC, reduced to the fields `StepFailedDetails`
|
|
179
197
|
* declares and the types it declares them as. The child is the same pinned
|
|
@@ -184,26 +202,76 @@ function isHumanWaitFrame(value: unknown): value is AuthoredHumanWait {
|
|
|
184
202
|
* evidence frame would replace the answer with a worse one.
|
|
185
203
|
*/
|
|
186
204
|
export function stepFailedFrame(value: unknown): StepFailedDetails | undefined {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const text = (key: string): string | undefined =>
|
|
190
|
-
typeof frame[key] === 'string' ? (frame[key] as string).slice(0, 8192) : undefined;
|
|
191
|
-
const count = (key: string): number | undefined =>
|
|
192
|
-
typeof frame[key] === 'number' && Number.isSafeInteger(frame[key]) ? frame[key] as number : undefined;
|
|
205
|
+
const frame = frameRecord(value);
|
|
206
|
+
if (frame === undefined) return undefined;
|
|
193
207
|
const details: StepFailedDetails = {};
|
|
194
208
|
for (const [key, parsed] of [
|
|
195
|
-
['stepId',
|
|
196
|
-
['completionReason',
|
|
197
|
-
['maxIterations',
|
|
198
|
-
['stdoutTail',
|
|
199
|
-
['detail',
|
|
200
|
-
['
|
|
209
|
+
['stepId', frameText(frame, 'stepId')], ['stepType', frameText(frame, 'stepType')],
|
|
210
|
+
['completionReason', frameText(frame, 'completionReason')], ['attempt', frameCount(frame, 'attempt')],
|
|
211
|
+
['maxIterations', frameCount(frame, 'maxIterations')], ['exitCode', frameCount(frame, 'exitCode')],
|
|
212
|
+
['stdoutTail', frameText(frame, 'stdoutTail')], ['stderrTail', frameText(frame, 'stderrTail')],
|
|
213
|
+
['detail', frameText(frame, 'detail')], ['transcriptPath', frameText(frame, 'transcriptPath')],
|
|
214
|
+
['attempts', attemptFrames(frame['attempts'])],
|
|
215
|
+
['attemptEvidence', comparisonFrame(frame['attemptEvidence'])],
|
|
216
|
+
['hint', frameText(frame, 'hint')], ['journalPath', frameText(frame, 'journalPath')],
|
|
201
217
|
] as const) {
|
|
202
218
|
if (parsed !== undefined) (details as Record<string, unknown>)[key] = parsed;
|
|
203
219
|
}
|
|
204
220
|
return Object.keys(details).length === 0 ? undefined : details;
|
|
205
221
|
}
|
|
206
222
|
|
|
223
|
+
/**
|
|
224
|
+
* The attempt history, reduced element by element on the same terms as the
|
|
225
|
+
* frame that carries it. An element that is not an object contributes nothing
|
|
226
|
+
* rather than voiding the whole history: a report naming three of four
|
|
227
|
+
* attempts is still the first attempt's error, which is the fact the terminal
|
|
228
|
+
* scalars cannot supply.
|
|
229
|
+
*/
|
|
230
|
+
function attemptFrames(value: unknown): StepAttemptFailure[] | undefined {
|
|
231
|
+
if (!Array.isArray(value)) return undefined;
|
|
232
|
+
const attempts: StepAttemptFailure[] = [];
|
|
233
|
+
for (const element of value) {
|
|
234
|
+
const source = frameRecord(element);
|
|
235
|
+
if (source === undefined) continue;
|
|
236
|
+
const attempt: StepAttemptFailure = {};
|
|
237
|
+
for (const [key, parsed] of [
|
|
238
|
+
['attempt', frameCount(source, 'attempt')],
|
|
239
|
+
['completionReason', frameText(source, 'completionReason')],
|
|
240
|
+
['disposition', frameText(source, 'disposition')],
|
|
241
|
+
['exitCode', frameCount(source, 'exitCode')],
|
|
242
|
+
['stdoutTail', frameText(source, 'stdoutTail')],
|
|
243
|
+
['stderrTail', frameText(source, 'stderrTail')],
|
|
244
|
+
['detail', frameText(source, 'detail')],
|
|
245
|
+
// Only a literal `true` claims truncation; anything else leaves the
|
|
246
|
+
// excerpt unlabelled rather than labelling a complete one as cut.
|
|
247
|
+
['truncated', source['truncated'] === true ? true : undefined],
|
|
248
|
+
] as const) {
|
|
249
|
+
if (parsed !== undefined) (attempt as Record<string, unknown>)[key] = parsed;
|
|
250
|
+
}
|
|
251
|
+
if (Object.keys(attempt).length > 0) attempts.push(attempt);
|
|
252
|
+
}
|
|
253
|
+
return attempts.length === 0 ? undefined : attempts;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** An unrecognised verdict is dropped, and rendering then says `unknown`. */
|
|
257
|
+
function comparisonFrame(value: unknown): AttemptEvidenceComparison | undefined {
|
|
258
|
+
return value === 'differs' || value === 'unchanged' || value === 'unknown' ? value : undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function frameRecord(value: unknown): Record<string, unknown> | undefined {
|
|
262
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
263
|
+
? value as Record<string, unknown> : undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function frameText(frame: Record<string, unknown>, key: string): string | undefined {
|
|
267
|
+
return typeof frame[key] === 'string' ? (frame[key] as string).slice(0, 8192) : undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function frameCount(frame: Record<string, unknown>, key: string): number | undefined {
|
|
271
|
+
return typeof frame[key] === 'number' && Number.isSafeInteger(frame[key])
|
|
272
|
+
? frame[key] as number : undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
207
275
|
/** The IPC frame is a claim, not a durable terminal fact or a sandbox boundary. */
|
|
208
276
|
export async function verifyAuthoredNodeResult(
|
|
209
277
|
result: AuthoredFlowExecutionResult, metadata: AuthoredRootMetadata,
|
|
@@ -212,6 +280,9 @@ export async function verifyAuthoredNodeResult(
|
|
|
212
280
|
const invalid = (why = ''): never => { throw new Error(`authored runtime result has no matching durable completion${process.env['FLOWS_VERIFIER_DEBUG'] && why ? ` (${why})` : ''}`); };
|
|
213
281
|
if (result.rootRunId !== rootRunId || result.name !== metadata.flowName
|
|
214
282
|
|| !isLoweredCompletion(result.completionReason)
|
|
283
|
+
// Type- and bound-check the claimed detail here, so the marker comparison
|
|
284
|
+
// below compares two values this process would itself have produced.
|
|
285
|
+
|| (result.completionDetail !== undefined && !isDurableCompletionDetail(result.completionDetail))
|
|
215
286
|
|| !Array.isArray(result.journalSteps) || result.journalSteps.length === 0) invalid('frame');
|
|
216
287
|
const terminal = result.journalSteps.at(-1)!;
|
|
217
288
|
if (!terminal || !/^complete-[1-9][0-9]*$/.test(terminal.id)) invalid('terminal');
|
|
@@ -288,8 +359,11 @@ export async function verifyAuthoredNodeResult(
|
|
|
288
359
|
// rejected a frame whose reason is not lowerable at all — that one is
|
|
289
360
|
// the runtime validation of untrusted IPC, and it is why nothing has
|
|
290
361
|
// to be re-asserted here just to satisfy the type.
|
|
362
|
+
// The detail is inside the marker, so this one comparison also
|
|
363
|
+
// attests it: a frame cannot claim a detail the journal does not
|
|
364
|
+
// hold, drop one it does, or alter a character of it.
|
|
291
365
|
if (step?.type !== 'deterministic'
|
|
292
|
-
|| step.command !== completionMarker(result.completionReason)) invalid('marker');
|
|
366
|
+
|| step.command !== completionMarker(result.completionReason, result.completionDetail)) invalid('marker');
|
|
293
367
|
}
|
|
294
368
|
}
|
|
295
369
|
} finally { journal.close(); }
|
package/src/authored-root.ts
CHANGED
|
@@ -4,7 +4,9 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
5
|
import { canonicalize } from './canonical.js';
|
|
6
6
|
import { compileSpec, toKernelSpec } from './compile.js';
|
|
7
|
-
import { executeAuthoredFlow,
|
|
7
|
+
import { executeAuthoredFlow, type AuthoredFlowExecutionResult } from './authored-flow-executor.js';
|
|
8
|
+
import { isDurableCompletionDetail, isLoweredCompletion } from './authored-completion.js';
|
|
9
|
+
import { AUTHORED_ROOT_KIND } from './authored-verdict.js';
|
|
8
10
|
import {
|
|
9
11
|
loadAuthoredFlow,
|
|
10
12
|
type LoadedAuthoredFlow,
|
|
@@ -19,7 +21,12 @@ import { AuthoredFlowExecutionError, AuthoredHumanParked } from './authored-flow
|
|
|
19
21
|
import { readOpenHumanWaits } from './authored-human.js';
|
|
20
22
|
import { isSurfaceCompletionReason } from './authored-step-output.js';
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Taken from the projection module rather than spelled twice: `flows status`
|
|
26
|
+
* has to recognise the same root this file writes, and it must not import the
|
|
27
|
+
* executor to do it.
|
|
28
|
+
*/
|
|
29
|
+
const ROOT_KIND = AUTHORED_ROOT_KIND;
|
|
23
30
|
|
|
24
31
|
export interface AuthoredRootExtension {
|
|
25
32
|
readonly name: string;
|
|
@@ -104,6 +111,7 @@ export async function executeDurableAuthoredFlow(
|
|
|
104
111
|
return await completedRootResult(journal, outcome.run_id);
|
|
105
112
|
}
|
|
106
113
|
assertRootCanDispatch(outcome);
|
|
114
|
+
options.lifecycle?.onRunStarted?.({ runId: outcome.run_id, flow: definition.name });
|
|
107
115
|
// `run.start` is an idempotent receipt. If the first caller died after
|
|
108
116
|
// the daemon dispatched this root, a same-daemon retry sees the existing
|
|
109
117
|
// active run but receives no second dispatch from start itself. Resume is
|
|
@@ -150,6 +158,7 @@ export async function resumeDurableAuthoredFlow(
|
|
|
150
158
|
}
|
|
151
159
|
assertRootCanDispatch(outcome);
|
|
152
160
|
await assertNoOpenHumanWait(journal, outcome);
|
|
161
|
+
options.lifecycle?.onRunStarted?.({ runId: rootRunId, flow: metadata.flowName, resumed: true });
|
|
153
162
|
const dispatch = await dispatchWait.promise;
|
|
154
163
|
return await driveRoot(loaded, metadata, journal, peer, dispatch, options);
|
|
155
164
|
} finally {
|
|
@@ -230,6 +239,7 @@ async function driveRoot(
|
|
|
230
239
|
dispatch.run_id, dispatch.step_id, dispatch.attempt,
|
|
231
240
|
dispatch.idempotency_key, 'success', {
|
|
232
241
|
output: { name: result.name, completionReason: result.completionReason,
|
|
242
|
+
...(result.completionDetail === undefined ? {} : { completionDetail: result.completionDetail }),
|
|
233
243
|
journalSteps: result.journalSteps,
|
|
234
244
|
...(result.executionRuntime === undefined ? {} : { executionRuntime: result.executionRuntime }) },
|
|
235
245
|
started_pins: dispatch.pins, end_pins: dispatch.pins,
|
|
@@ -372,9 +382,13 @@ async function completedRootResult(
|
|
|
372
382
|
if (!isCompletedRootOutput(output)) {
|
|
373
383
|
throw new Error('completed authored root has no durable result');
|
|
374
384
|
}
|
|
385
|
+
// The stored detail, never a freshly computed one: redaction reads the
|
|
386
|
+
// CURRENT environment, so recomputing here would let a completed run report
|
|
387
|
+
// something its journal does not hold.
|
|
375
388
|
return Object.freeze({
|
|
376
389
|
name: output.name,
|
|
377
390
|
completionReason: output.completionReason,
|
|
391
|
+
...(output.completionDetail === undefined ? {} : { completionDetail: output.completionDetail }),
|
|
378
392
|
journalSteps: Object.freeze(output.journalSteps.map(step => Object.freeze({ ...step }))),
|
|
379
393
|
rootRunId,
|
|
380
394
|
});
|
|
@@ -385,6 +399,10 @@ function isCompletedRootOutput(value: unknown): value is Omit<AuthoredFlowExecut
|
|
|
385
399
|
const output = value as Partial<AuthoredFlowExecutionResult>;
|
|
386
400
|
return typeof output.name === 'string'
|
|
387
401
|
&& isLoweredCompletion(output.completionReason)
|
|
402
|
+
// A malformed detail fails the whole readback rather than being dropped:
|
|
403
|
+
// silently discarding it would report the verdict without the evidence
|
|
404
|
+
// the journal says it was recorded with.
|
|
405
|
+
&& (output.completionDetail === undefined || isDurableCompletionDetail(output.completionDetail))
|
|
388
406
|
&& Array.isArray(output.journalSteps)
|
|
389
407
|
&& output.journalSteps.every(step => typeof step === 'object' && step !== null
|
|
390
408
|
&& typeof step.id === 'string' && typeof step.runId === 'string'
|
|
@@ -22,19 +22,23 @@ export interface AuthoredStepContext {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Shared by every `f.*` verb that lowers to
|
|
26
|
-
*
|
|
25
|
+
* Shared by every `f.*` verb that lowers to its own kernel run: find the
|
|
26
|
+
* operation's `step.completed` entry, record it, and refuse anything but a
|
|
27
27
|
* clean success before handing the raw `output` back for verb-specific
|
|
28
28
|
* extraction (a plain string for `f.run`, an `AgentResult` for `f.agent`).
|
|
29
29
|
*
|
|
30
|
-
* Callers are responsible for having already
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
30
|
+
* Callers are responsible for having already waited for the run to reach a
|
|
31
|
+
* terminal state — `f.run`'s caller relies on `runStart`'s own immediate
|
|
32
|
+
* response (the kernel drives a deterministic step to completion inline, no
|
|
33
|
+
* race); `f.agent`'s caller relies on `classifyOutcome` (cli/run.ts) having
|
|
34
|
+
* already polled to one. Given that, a single read from the start of this
|
|
35
|
+
* run's small journal is enough, and no polling happens here.
|
|
36
|
+
*
|
|
37
|
+
* What is NOT delegated to the caller is the run's verdict. A child spec has
|
|
38
|
+
* more than one step whenever the author declared a `.gate()`, and that gate
|
|
39
|
+
* step runs after the producer, so the producer's own `success` is not the
|
|
40
|
+
* run's. Both are checked here, and the run-level check is what makes a failed
|
|
41
|
+
* gate reject the authored operation.
|
|
38
42
|
*
|
|
39
43
|
* A non-success completion used to throw `journal step "run-1" completed with
|
|
40
44
|
* retries_exhausted` and stop there, discarding the evidence it was holding:
|
|
@@ -63,26 +67,19 @@ export async function readCompletedStepOutput(
|
|
|
63
67
|
const edges = context.stepEdges?.(stepId);
|
|
64
68
|
journalSteps.push(Object.freeze({ id: stepId, runId, completionReason: reason, ...edges }));
|
|
65
69
|
if (reason !== 'success') {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
message += renderInspection(where);
|
|
80
|
-
message += await alsoRecord(journal, context.rootRunId, {
|
|
81
|
-
step: stepId, runId, state: 'completed', completionReason: reason,
|
|
82
|
-
...(details?.stepId === undefined ? {} : { kernelStep: details.stepId }),
|
|
83
|
-
...edges,
|
|
84
|
-
});
|
|
85
|
-
throw new AuthoredFlowExecutionError('step_failed', message, reason, runId, { ...details, ...where });
|
|
70
|
+
throw await stepFailure(journal, runId, stepId, reason, context, edges,
|
|
71
|
+
`journal step "${stepId}" completed with ${reason}`);
|
|
72
|
+
}
|
|
73
|
+
// An authored operation does not always lower to ONE kernel step. A declared
|
|
74
|
+
// `.gate()` becomes its own barrier step that runs AFTER the producer
|
|
75
|
+
// (`lowerNamedGates`), so the producer's own success is not the run's
|
|
76
|
+
// verdict. Reading only the producer's entry let a failed gate resolve as
|
|
77
|
+
// though the command had passed — the `|| true` invisibility this module's
|
|
78
|
+
// recording policy exists to remove, reappearing one layer up.
|
|
79
|
+
const runReason = entries.map(runCompletionReason).find((value) => value !== undefined);
|
|
80
|
+
if (runReason !== undefined && runReason !== 'success') {
|
|
81
|
+
throw await stepFailure(journal, runId, stepId, runReason, context, edges,
|
|
82
|
+
`journal run for step "${stepId}" completed with ${runReason}`);
|
|
86
83
|
}
|
|
87
84
|
await recordAuthoredChild(journal, context.rootRunId, {
|
|
88
85
|
step: stepId, runId, state: 'completed', completionReason: reason, ...edges,
|
|
@@ -90,6 +87,42 @@ export async function readCompletedStepOutput(
|
|
|
90
87
|
return completed.payload.output;
|
|
91
88
|
}
|
|
92
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The one `step_failed` grammar both the step-level and the run-level refusal
|
|
92
|
+
* report through: inspect the journal for the step that actually failed, render
|
|
93
|
+
* its evidence, name where to look, and index the child before throwing.
|
|
94
|
+
*/
|
|
95
|
+
async function stepFailure(
|
|
96
|
+
journal: JournalClient,
|
|
97
|
+
runId: string,
|
|
98
|
+
stepId: string,
|
|
99
|
+
reason: ProtocolCompletionReason | ProtocolRunCompletionReason,
|
|
100
|
+
context: AuthoredStepContext,
|
|
101
|
+
edges: ReturnType<NonNullable<AuthoredStepContext['stepEdges']>>,
|
|
102
|
+
header: string,
|
|
103
|
+
): Promise<AuthoredFlowExecutionError> {
|
|
104
|
+
let message = header;
|
|
105
|
+
let details: StepFailedDetails | undefined;
|
|
106
|
+
try {
|
|
107
|
+
details = await stepFailureDetails(journal, runId);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
// Inspection must not erase the already known step failure; the two
|
|
110
|
+
// failures stay separately visible, as in classifyOutcome.
|
|
111
|
+
message += ` Could not inspect the failed step: ${errorMessage(error)}`;
|
|
112
|
+
}
|
|
113
|
+
if (details !== undefined) message += renderStepEvidence(details);
|
|
114
|
+
// Appended whatever inspection found — including nothing. A failure shape
|
|
115
|
+
// this reader does not recognise must still end with somewhere to go.
|
|
116
|
+
const where = inspectionHint(runId, details?.stepId ?? stepId, context.dataDir);
|
|
117
|
+
message += renderInspection(where);
|
|
118
|
+
message += await alsoRecord(journal, context.rootRunId, {
|
|
119
|
+
step: stepId, runId, state: 'completed', completionReason: reason,
|
|
120
|
+
...(details?.stepId === undefined ? {} : { kernelStep: details.stepId }),
|
|
121
|
+
...edges,
|
|
122
|
+
});
|
|
123
|
+
return new AuthoredFlowExecutionError('step_failed', message, reason, runId, { ...details, ...where });
|
|
124
|
+
}
|
|
125
|
+
|
|
93
126
|
export async function readSuccessfulOutput(
|
|
94
127
|
journal: JournalClient,
|
|
95
128
|
outcome: RunOutcome,
|
|
@@ -98,24 +131,74 @@ export async function readSuccessfulOutput(
|
|
|
98
131
|
context: AuthoredStepContext = {},
|
|
99
132
|
): Promise<string> {
|
|
100
133
|
const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps, context)
|
|
101
|
-
.catch(
|
|
102
|
-
if (error instanceof AuthoredFlowExecutionError
|
|
103
|
-
&& (error.completionReason === 'timeout' || error.completionReason === 'lease_expired')) {
|
|
104
|
-
// A timeout is still a failed command, and the evidence extracted for
|
|
105
|
-
// it is the same evidence. Re-labelling the error must not delete it.
|
|
106
|
-
throw new AuthoredFlowExecutionError('lease_exceeded',
|
|
107
|
-
`f.run step "${stepId}" exceeded its command timeout.`
|
|
108
|
-
+ ` ${error.message.slice(`${error.code}: `.length)}`,
|
|
109
|
-
error.completionReason, error.runId, error.details);
|
|
110
|
-
}
|
|
111
|
-
throw error;
|
|
112
|
-
});
|
|
134
|
+
.catch(relabelCommandTimeout(stepId));
|
|
113
135
|
if (!isRecord(output) || typeof output['stdout_tail'] !== 'string') {
|
|
114
136
|
throw protocolViolation(outcome.run_id, `step "${stepId}" has no string stdout_tail`);
|
|
115
137
|
}
|
|
116
138
|
return output['stdout_tail'];
|
|
117
139
|
}
|
|
118
140
|
|
|
141
|
+
/** What `f.run` resolves to under `onNonZero: 'record'` (surface `RunResult`). */
|
|
142
|
+
export interface RecordedRunOutcome {
|
|
143
|
+
ok: boolean;
|
|
144
|
+
exitCode: number;
|
|
145
|
+
output: string;
|
|
146
|
+
stdout: string;
|
|
147
|
+
stderr: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The read side of `onNonZero: 'record'`.
|
|
152
|
+
*
|
|
153
|
+
* A recorded red exit is a SUCCESSFUL step, so this takes the same path as
|
|
154
|
+
* `readSuccessfulOutput` and every field comes from the journaled envelope —
|
|
155
|
+
* nothing is re-run and nothing is re-measured. What still throws is what the
|
|
156
|
+
* policy never covered: a timeout, a signal, a step that never ran, or a
|
|
157
|
+
* declared content or schema gate that failed. The `-1` the executor writes
|
|
158
|
+
* for "no exit status" is refused rather than handed back as a plausible
|
|
159
|
+
* code, mirroring the kernel's own rule, so a malformed envelope can never
|
|
160
|
+
* reach an author's `if (!result.ok)` as a real verdict.
|
|
161
|
+
*/
|
|
162
|
+
export async function readRecordedOutcome(
|
|
163
|
+
journal: JournalClient,
|
|
164
|
+
outcome: RunOutcome,
|
|
165
|
+
stepId: string,
|
|
166
|
+
journalSteps: AuthoredFlowJournalStep[],
|
|
167
|
+
context: AuthoredStepContext = {},
|
|
168
|
+
): Promise<RecordedRunOutcome> {
|
|
169
|
+
const envelope = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps, context)
|
|
170
|
+
.catch(relabelCommandTimeout(stepId));
|
|
171
|
+
const exitCode = isRecord(envelope) ? envelope['exit_code'] : undefined;
|
|
172
|
+
if (!isRecord(envelope) || typeof envelope['stdout_tail'] !== 'string'
|
|
173
|
+
|| typeof envelope['stderr_tail'] !== 'string'
|
|
174
|
+
|| typeof exitCode !== 'number' || !Number.isInteger(exitCode) || exitCode < 0) {
|
|
175
|
+
throw protocolViolation(outcome.run_id,
|
|
176
|
+
`step "${stepId}" recorded no {exit_code, stdout_tail, stderr_tail} to report`);
|
|
177
|
+
}
|
|
178
|
+
const stdout = envelope['stdout_tail'];
|
|
179
|
+
const stderr = envelope['stderr_tail'];
|
|
180
|
+
return {
|
|
181
|
+
ok: exitCode === 0, exitCode, stdout, stderr,
|
|
182
|
+
// Concatenated, not interleaved: the two tails were captured separately,
|
|
183
|
+
// so joining them is a reading convenience and is documented as one.
|
|
184
|
+
output: stdout !== '' && stderr !== '' ? `${stdout}\n${stderr}` : stdout + stderr,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** A timeout is still a failed command; re-labelling it must not delete its evidence. */
|
|
189
|
+
function relabelCommandTimeout(stepId: string): (error: unknown) => never {
|
|
190
|
+
return (error) => {
|
|
191
|
+
if (error instanceof AuthoredFlowExecutionError
|
|
192
|
+
&& (error.completionReason === 'timeout' || error.completionReason === 'lease_expired')) {
|
|
193
|
+
throw new AuthoredFlowExecutionError('lease_exceeded',
|
|
194
|
+
`f.run step "${stepId}" exceeded its command timeout.`
|
|
195
|
+
+ ` ${error.message.slice(`${error.code}: `.length)}`,
|
|
196
|
+
error.completionReason, error.runId, error.details);
|
|
197
|
+
}
|
|
198
|
+
throw error;
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
119
202
|
interface StepCompletedEntry {
|
|
120
203
|
entry_type: 'step.completed';
|
|
121
204
|
step_id: string;
|
|
@@ -135,6 +218,20 @@ function isStepCompleted(value: unknown, stepId: string): value is StepCompleted
|
|
|
135
218
|
&& 'output' in payload;
|
|
136
219
|
}
|
|
137
220
|
|
|
221
|
+
/**
|
|
222
|
+
* The run's own terminal reason, when this entry is the one carrying it.
|
|
223
|
+
*
|
|
224
|
+
* Anything else — including a `run.completed` whose payload this reader does
|
|
225
|
+
* not recognise — yields `undefined` and leaves the step-level verdict
|
|
226
|
+
* untouched, so an unfamiliar journal shape cannot invent a failure.
|
|
227
|
+
*/
|
|
228
|
+
function runCompletionReason(entry: unknown): ProtocolRunCompletionReason | undefined {
|
|
229
|
+
if (!isRecord(entry) || entry['entry_type'] !== 'run.completed') return undefined;
|
|
230
|
+
const payload = entry['payload'];
|
|
231
|
+
const reason = isRecord(payload) ? payload['completionReason'] : undefined;
|
|
232
|
+
return isSurfaceRunCompletionReason(reason) ? reason : undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
138
235
|
export function isSurfaceCompletionReason(value: unknown): value is ProtocolCompletionReason {
|
|
139
236
|
return typeof value === 'string'
|
|
140
237
|
&& (COMPLETION_REASONS as readonly string[]).includes(value);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Project an authored flow's own verdict out of its root journal.
|
|
2
|
+
//
|
|
3
|
+
// `foldRunState` answers what the KERNEL recorded: the root step succeeded, so
|
|
4
|
+
// the run completed with `success`. That is true and must stay printed. It is
|
|
5
|
+
// also not what a reader asking "why did this fail?" needs when the body
|
|
6
|
+
// declared `step_failed` — a verdict the kernel's vocabulary cannot express on
|
|
7
|
+
// a step that succeeded, so it travels as output on the root's own
|
|
8
|
+
// `step.completed`.
|
|
9
|
+
//
|
|
10
|
+
// This is a separate, narrow projection rather than a branch inside the fold:
|
|
11
|
+
// the fold is generic over every run and has no business knowing what an
|
|
12
|
+
// authored root is, and the executor this vocabulary comes from must not
|
|
13
|
+
// become a dependency of `flows status`.
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
isDurableCompletionDetail,
|
|
17
|
+
isLoweredCompletion,
|
|
18
|
+
type LoweredCompletionReason,
|
|
19
|
+
} from './authored-completion.js';
|
|
20
|
+
import type { JournalEvent } from './journal-reader.js';
|
|
21
|
+
|
|
22
|
+
/** The `instruction` discriminator `authored-root.ts` writes into the root step. */
|
|
23
|
+
export const AUTHORED_ROOT_KIND = 'relayflows.authored-root.v1';
|
|
24
|
+
|
|
25
|
+
export interface AuthoredVerdict {
|
|
26
|
+
readonly reason: LoweredCompletionReason;
|
|
27
|
+
/** Absent when the body called `done()` with one argument, as most do. */
|
|
28
|
+
readonly detail?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The verdict an authored root attested, or `null` when this journal holds none.
|
|
33
|
+
*
|
|
34
|
+
* `null` is the answer for an ordinary run, for an authored run still in
|
|
35
|
+
* flight, and for one whose root output is malformed. The last of those
|
|
36
|
+
* matters: an ordinary flow may name a step `authored-root`, and a worker may
|
|
37
|
+
* journal anything into a step's output, so the step id alone attests nothing.
|
|
38
|
+
* A verdict is reported only when the run's own spec declares the authored
|
|
39
|
+
* root — carrying {@link AUTHORED_ROOT_KIND} metadata — and the root's last
|
|
40
|
+
* `step.completed` is a terminal kernel `success` whose output is a complete,
|
|
41
|
+
* in-bounds authored result. A retry or park completion is not terminal and a
|
|
42
|
+
* later attempt may still change the answer, so neither is promoted.
|
|
43
|
+
*
|
|
44
|
+
* This is a projection for a read-only view, so a malformed record yields no
|
|
45
|
+
* verdict rather than an error: refusing to render a whole status because one
|
|
46
|
+
* output field is the wrong type would replace the answer with a worse one.
|
|
47
|
+
*/
|
|
48
|
+
export function authoredVerdictOf(events: readonly JournalEvent[]): AuthoredVerdict | null {
|
|
49
|
+
const spawned = events[0];
|
|
50
|
+
if (spawned === undefined || spawned.entry_type !== 'run.spawned') return null;
|
|
51
|
+
if (!declaresAuthoredRoot(spawned)) return null;
|
|
52
|
+
|
|
53
|
+
const completed = [...events].reverse().find((event) =>
|
|
54
|
+
event.entry_type === 'step.completed' && event.step_id === 'authored-root');
|
|
55
|
+
const payload = record(completed?.payload);
|
|
56
|
+
if (payload === null || payload['completionReason'] !== 'success') return null;
|
|
57
|
+
const disposition = payload['disposition'];
|
|
58
|
+
if (disposition !== undefined && disposition !== 'step_done') return null;
|
|
59
|
+
|
|
60
|
+
const output = record(payload['output']);
|
|
61
|
+
if (output === null || typeof output['name'] !== 'string') return null;
|
|
62
|
+
const reason = output['completionReason'];
|
|
63
|
+
if (!isLoweredCompletion(reason)) return null;
|
|
64
|
+
const detail = output['completionDetail'];
|
|
65
|
+
if (detail !== undefined && !isDurableCompletionDetail(detail)) return null;
|
|
66
|
+
|
|
67
|
+
return Object.freeze({ reason, ...(detail === undefined ? {} : { detail }) });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function declaresAuthoredRoot(spawned: JournalEvent): boolean {
|
|
71
|
+
const spec = record(record(spawned.payload)?.['spec']);
|
|
72
|
+
const steps = spec?.['steps'];
|
|
73
|
+
if (!Array.isArray(steps)) return false;
|
|
74
|
+
const step = record(steps[0]);
|
|
75
|
+
if (step === null || step['id'] !== 'authored-root') return false;
|
|
76
|
+
if (typeof step['instruction'] !== 'string') return false;
|
|
77
|
+
try {
|
|
78
|
+
return record(JSON.parse(step['instruction']))?.['kind'] === AUTHORED_ROOT_KIND;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function record(value: unknown): Record<string, unknown> | null {
|
|
85
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
86
|
+
? value as Record<string, unknown> : null;
|
|
87
|
+
}
|