@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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// The authored verdict, written to the root BEFORE the marker is admitted.
|
|
2
|
+
//
|
|
3
|
+
// The terminal marker's command embeds the detail, and the marker run is
|
|
4
|
+
// opened under a stable admission key (`authored-child:<hash(root, step)>`).
|
|
5
|
+
// Those two facts together mean the detail has to be a durable decision, not a
|
|
6
|
+
// recomputed one: normalization redacts against `process.env`, so a credential
|
|
7
|
+
// rotated or removed while this process was down makes the SAME authored
|
|
8
|
+
// sentence normalize differently. A body resumed into that environment would
|
|
9
|
+
// retry the marker's admission key with a drifted spec, and the kernel refuses
|
|
10
|
+
// that as `run_admission_conflict` — losing an explanation the root had
|
|
11
|
+
// already journaled, for a flow whose every step succeeded.
|
|
12
|
+
//
|
|
13
|
+
// So the verdict is appended to a stream on the root run before the marker run
|
|
14
|
+
// is opened, and a later attempt reaching the same terminal step reuses what
|
|
15
|
+
// is recorded. That is exactly the durability the executor already gives a
|
|
16
|
+
// predicate gate (`applyPredicateGate`): record the decision first, so the
|
|
17
|
+
// spec built from it is identical on every attempt and author code is never
|
|
18
|
+
// asked the question twice.
|
|
19
|
+
//
|
|
20
|
+
// Nothing is recorded for a `done()` with no detail. Without one the marker
|
|
21
|
+
// command is a function of the reason alone — no environment, no drift, and
|
|
22
|
+
// nothing to recover — so a flow that does not opt in writes exactly the
|
|
23
|
+
// journal it has always written.
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
isDurableCompletionDetail,
|
|
27
|
+
isLoweredCompletion,
|
|
28
|
+
type LoweredCompletionReason,
|
|
29
|
+
} from './authored-completion.js';
|
|
30
|
+
import type { JournalClient } from './journal-client.js';
|
|
31
|
+
|
|
32
|
+
export const AUTHORED_VERDICT_STREAM = 'authored-verdict';
|
|
33
|
+
|
|
34
|
+
/** Versioned so a reader can tell this record from a future one. */
|
|
35
|
+
const RECORD_KIND = 'relayflows.authored-verdict.v1';
|
|
36
|
+
|
|
37
|
+
export interface AuthoredVerdictRecord {
|
|
38
|
+
readonly reason: LoweredCompletionReason;
|
|
39
|
+
/** Recorded only when the body passed one; see the module comment. */
|
|
40
|
+
readonly detail?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Commit this body's verdict, or recover the one this root already committed.
|
|
45
|
+
*
|
|
46
|
+
* Returns the verdict the marker must be built from — the recorded one when
|
|
47
|
+
* the root holds it, this attempt's otherwise. With no root run there is
|
|
48
|
+
* nothing durable to write to (the non-durable executor seam), so that path
|
|
49
|
+
* passes straight through without a round trip. A root run is always read
|
|
50
|
+
* first: a committed record stays authoritative even for an attempt that has
|
|
51
|
+
* no detail of its own — an optional source can be present on the first
|
|
52
|
+
* execution and gone on the retry, and "this attempt has no detail" is not
|
|
53
|
+
* "this root never recorded one". Only when no record exists AND this attempt
|
|
54
|
+
* has no detail does it return without appending, preserving the no-detail
|
|
55
|
+
* marker, spec hash, and journal exactly as before.
|
|
56
|
+
*/
|
|
57
|
+
export async function commitAuthoredVerdict(
|
|
58
|
+
journal: JournalClient,
|
|
59
|
+
rootRunId: string | undefined,
|
|
60
|
+
step: string,
|
|
61
|
+
verdict: AuthoredVerdictRecord,
|
|
62
|
+
): Promise<AuthoredVerdictRecord> {
|
|
63
|
+
if (rootRunId === undefined) return verdict;
|
|
64
|
+
const committed = await readAuthoredVerdict(journal, rootRunId, step);
|
|
65
|
+
if (committed !== undefined) return committed;
|
|
66
|
+
if (verdict.detail === undefined) return verdict;
|
|
67
|
+
await journal.streamAppend(rootRunId, AUTHORED_VERDICT_STREAM, {
|
|
68
|
+
verdict: RECORD_KIND, step, reason: verdict.reason, detail: verdict.detail,
|
|
69
|
+
});
|
|
70
|
+
return verdict;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The verdict this root recorded for `step`, or `undefined`.
|
|
75
|
+
*
|
|
76
|
+
* The FIRST valid record wins: it is the one the marker was — or was about to
|
|
77
|
+
* be — admitted from, and a later attempt's differently-redacted text must not
|
|
78
|
+
* displace it. Records this version does not recognise, and details that fail
|
|
79
|
+
* the durable check every other read boundary applies, are skipped rather than
|
|
80
|
+
* trusted; the marker then drifts and the kernel refuses it loudly, which is
|
|
81
|
+
* the right outcome for a record this process cannot read.
|
|
82
|
+
*/
|
|
83
|
+
export async function readAuthoredVerdict(
|
|
84
|
+
journal: JournalClient,
|
|
85
|
+
rootRunId: string,
|
|
86
|
+
step: string,
|
|
87
|
+
): Promise<AuthoredVerdictRecord | undefined> {
|
|
88
|
+
let offset = 0;
|
|
89
|
+
for (;;) {
|
|
90
|
+
const page = await journal.streamRead(rootRunId, AUTHORED_VERDICT_STREAM, offset, 1000);
|
|
91
|
+
for (const message of page.messages) {
|
|
92
|
+
// `stream.read` returns either the envelope or the bare message,
|
|
93
|
+
// matching how the executor reads predicate verdicts.
|
|
94
|
+
const raw = (message as { message?: unknown }).message ?? message;
|
|
95
|
+
if (isVerdictRecord(raw) && raw.step === step) {
|
|
96
|
+
return Object.freeze({ reason: raw.reason, detail: raw.detail });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (page.messages.length === 0 || page.next_offset <= offset) break;
|
|
100
|
+
offset = page.next_offset;
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface StoredVerdict {
|
|
106
|
+
readonly verdict: typeof RECORD_KIND;
|
|
107
|
+
readonly step: string;
|
|
108
|
+
readonly reason: LoweredCompletionReason;
|
|
109
|
+
readonly detail: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isVerdictRecord(value: unknown): value is StoredVerdict {
|
|
113
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
|
|
114
|
+
const record = value as Partial<StoredVerdict>;
|
|
115
|
+
return record.verdict === RECORD_KIND
|
|
116
|
+
&& typeof record.step === 'string' && record.step.length > 0
|
|
117
|
+
&& isLoweredCompletion(record.reason)
|
|
118
|
+
&& isDurableCompletionDetail(record.detail);
|
|
119
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// The authored verdict: which completions this runtime lowers, the durable
|
|
2
|
+
// marker that carries one, and the optional detail that says why.
|
|
3
|
+
//
|
|
4
|
+
// Split out of `authored-flow-executor.ts` so the CLI report and the status
|
|
5
|
+
// projection can read this vocabulary without importing the executor — and
|
|
6
|
+
// with it every provider adapter, MCP client and helper the executor pulls
|
|
7
|
+
// in. Nothing here does I/O or reads a clock.
|
|
8
|
+
|
|
9
|
+
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
10
|
+
import { redact } from './redact.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The completion reasons an authored body may declare and this executor lowers.
|
|
14
|
+
*
|
|
15
|
+
* `FlowCompletionReason` is wider than this on purpose — it is the journal's
|
|
16
|
+
* run vocabulary plus authored verdicts — but the two sets drifting silently is
|
|
17
|
+
* exactly what made a type-valid `done("step_failed")` die at runtime as
|
|
18
|
+
* `unsupported_completion`. Every gate that asks "is this a completion this
|
|
19
|
+
* runtime can lower?" now asks this one function, so a reason cannot be
|
|
20
|
+
* accepted in one place and rejected in another.
|
|
21
|
+
*
|
|
22
|
+
* These are internal cross-module helpers for the authored seam (the executor,
|
|
23
|
+
* the durable root, the IPC verifier, the CLI report and `flows status`), NOT
|
|
24
|
+
* public SDK surface. `src/index.ts` deliberately re-exports nothing from this
|
|
25
|
+
* module — keep it that way, or the whole authored seam leaks with them.
|
|
26
|
+
*/
|
|
27
|
+
export const LOWERED_COMPLETIONS = ['success', 'needs_human', 'step_failed', 'declined'] as const;
|
|
28
|
+
export type LoweredCompletionReason = (typeof LOWERED_COMPLETIONS)[number];
|
|
29
|
+
|
|
30
|
+
export function isLoweredCompletion(value: unknown): value is LoweredCompletionReason {
|
|
31
|
+
return typeof value === 'string' && (LOWERED_COMPLETIONS as readonly string[]).includes(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The bound on a normalized completion detail: 2,000 Unicode code points in
|
|
36
|
+
* the FINAL string, {@link COMPLETION_DETAIL_TRUNCATED_SUFFIX} included.
|
|
37
|
+
*
|
|
38
|
+
* Code points, not `String.length`: `.length` counts UTF-16 code units, so an
|
|
39
|
+
* emoji-heavy detail measured that way is half the length a reader would call
|
|
40
|
+
* it. The number matches `@relayflows/surface`'s
|
|
41
|
+
* `COMPLETION_DETAIL_MAX_CODE_POINTS`, which is what an author sees; it is the
|
|
42
|
+
* same order as the kernel's gate-detail cap
|
|
43
|
+
* (`kernel/relayflowd/src/engine/remote.rs`) but deliberately NOT the same
|
|
44
|
+
* bound — that one takes 2,000 characters and then appends its suffix, so its
|
|
45
|
+
* final string is longer than the number it advertises.
|
|
46
|
+
*/
|
|
47
|
+
export const COMPLETION_DETAIL_MAX_CODE_POINTS = 2000;
|
|
48
|
+
|
|
49
|
+
/** Fixed width, so the bound above can reserve exactly its room. */
|
|
50
|
+
export const COMPLETION_DETAIL_TRUNCATED_SUFFIX = '… (truncated)';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Read `done()`'s optional second argument into the detail the marker carries.
|
|
54
|
+
*
|
|
55
|
+
* Absence has four spellings — no argument, `undefined`, `{}`, and
|
|
56
|
+
* `{ detail: undefined }` — and all four mean the same thing: no detail, and a
|
|
57
|
+
* marker command byte-identical to the one the one-argument call has always
|
|
58
|
+
* produced. Whitespace-only joins them: say nothing, or say something.
|
|
59
|
+
*
|
|
60
|
+
* A `detail` that is present and not a string is a programming error the type
|
|
61
|
+
* system already catches for TypeScript callers; the runtime check is here for
|
|
62
|
+
* the same reason `isSurfaceFlowCompletionReason` is, and it refuses with the
|
|
63
|
+
* existing closed `unsupported_completion` code rather than widening the
|
|
64
|
+
* taxonomy. Refusals name the type they got, never the value — a malformed
|
|
65
|
+
* detail is exactly the kind of text that may carry a credential.
|
|
66
|
+
*
|
|
67
|
+
* Redaction runs BEFORE truncation, and the order is load-bearing rather than
|
|
68
|
+
* cosmetic: truncating first can split a token so no pattern matches it any
|
|
69
|
+
* more, which is a truncation that *causes* a leak. What redaction recognises
|
|
70
|
+
* is the SDK's existing policy — known token shapes, named credential fields,
|
|
71
|
+
* and the values of secret-looking environment variables (`redact.ts`) — not a
|
|
72
|
+
* guarantee to recognise every possible secret.
|
|
73
|
+
*
|
|
74
|
+
* Lone surrogates are substituted here too, so what leaves this function is a
|
|
75
|
+
* well-formed string the journal protocol can carry — see {@link wellFormed}.
|
|
76
|
+
*/
|
|
77
|
+
export function normalizeCompletionDetail(
|
|
78
|
+
options: unknown,
|
|
79
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
80
|
+
): string | undefined {
|
|
81
|
+
if (options === undefined) return undefined;
|
|
82
|
+
if (typeof options !== 'object' || options === null || Array.isArray(options)) {
|
|
83
|
+
throw new AuthoredFlowExecutionError(
|
|
84
|
+
'unsupported_completion',
|
|
85
|
+
`done() options must be an object with an optional string "detail"; received ${typeName(options)}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const detail = (options as { detail?: unknown }).detail;
|
|
89
|
+
if (detail === undefined) return undefined;
|
|
90
|
+
if (typeof detail !== 'string') {
|
|
91
|
+
throw new AuthoredFlowExecutionError(
|
|
92
|
+
'unsupported_completion',
|
|
93
|
+
`done() detail must be a string; received ${typeName(detail)}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const redacted = wellFormed(redact(detail, env)).trim();
|
|
97
|
+
return redacted.length === 0 ? undefined : bound(redacted);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Lone UTF-16 surrogates, as code points.
|
|
102
|
+
*
|
|
103
|
+
* Under `u` a subject string is iterated by CODE POINT, so a well-formed pair
|
|
104
|
+
* is one code point above U+FFFF and never enters this class. What the class
|
|
105
|
+
* matches is exactly a surrogate with no partner.
|
|
106
|
+
*/
|
|
107
|
+
const LONE_SURROGATE = /[\uD800-\uDFFF]/u;
|
|
108
|
+
const LONE_SURROGATES = /[\uD800-\uDFFF]/gu;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Replace every lone surrogate with U+FFFD, the replacement character.
|
|
112
|
+
*
|
|
113
|
+
* A JavaScript string is UTF-16 code units, not text. Slicing a character off
|
|
114
|
+
* the end of an agent's output — `(prose + '\u{1F642}').slice(0, -1)` — leaves
|
|
115
|
+
* a high surrogate with no partner, and the type system calls the result a
|
|
116
|
+
* `string`. Nothing downstream does.
|
|
117
|
+
* `JSON.stringify` escapes it, so the marker COMMAND is admitted; the raw
|
|
118
|
+
* detail then travels in the root's `step.complete` output, where the kernel's
|
|
119
|
+
* JSON decoder refuses it ("unexpected end of hex escape") and answers with a
|
|
120
|
+
* `bad_request` carrying a null request id — which resolves no pending
|
|
121
|
+
* request, so the call hangs and the authored explanation never lands.
|
|
122
|
+
*
|
|
123
|
+
* Substituting is right here where refusing is not: the flow has already done
|
|
124
|
+
* its work and reached a verdict, and losing the whole explanation over one
|
|
125
|
+
* broken code unit destroys more than it protects. One code unit in, one code
|
|
126
|
+
* unit out, so the bound below still counts what a reader would count.
|
|
127
|
+
*/
|
|
128
|
+
function wellFormed(text: string): string {
|
|
129
|
+
return LONE_SURROGATE.test(text) ? text.replace(LONE_SURROGATES, '\uFFFD') : text;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Is this a detail a durable record may carry?
|
|
134
|
+
*
|
|
135
|
+
* The gate every read boundary uses — the completed root's output, and the
|
|
136
|
+
* IPC frame the Bun runner returns — so an over-long or wrong-typed value
|
|
137
|
+
* fails closed there instead of flowing into a report as if it had been
|
|
138
|
+
* journaled by this process.
|
|
139
|
+
*/
|
|
140
|
+
export function isDurableCompletionDetail(value: unknown): value is string {
|
|
141
|
+
return typeof value === 'string'
|
|
142
|
+
&& value.length > 0
|
|
143
|
+
&& !LONE_SURROGATE.test(value)
|
|
144
|
+
&& [...value].length <= COMPLETION_DETAIL_MAX_CODE_POINTS;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The detail as ONE line, for a diagnostic message.
|
|
149
|
+
*
|
|
150
|
+
* Cloud renders a run's `error` through a line-oriented view that elides the
|
|
151
|
+
* middle of a long one (`cli/cloud-read.ts` `errorLines`), so a forty-line
|
|
152
|
+
* detail can lose the very finding it exists to carry. Escaping the breaks
|
|
153
|
+
* keeps the whole detail on the single line that renderer cannot elide, and
|
|
154
|
+
* keeps control characters out of a terminal. The unescaped normalized detail
|
|
155
|
+
* still travels in the structured fields beside it.
|
|
156
|
+
*/
|
|
157
|
+
export function singleLineCompletionDetail(detail: string): string {
|
|
158
|
+
return detail.replace(/[\u0000-\u001F\u007F-\u009F]/gu, (char) =>
|
|
159
|
+
NAMED_ESCAPE[char] ?? `\\u${char.codePointAt(0)!.toString(16).padStart(4, '0')}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const NAMED_ESCAPE: Record<string, string> = { '\n': '\\n', '\r': '\\r', '\t': '\\t' };
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The deterministic command that carries an authored verdict into the journal.
|
|
166
|
+
*
|
|
167
|
+
* `success` with no detail lowers to `:` because success needs no marker: the
|
|
168
|
+
* marker run's own kernel `success` already IS that record. Every other
|
|
169
|
+
* lowered verdict is something the kernel's completion vocabulary cannot
|
|
170
|
+
* express on a step that *succeeded*, so it travels as data on stdout and is
|
|
171
|
+
* read back from `step.completed`. It is deliberately not lowered as a failing
|
|
172
|
+
* command: no step failed here, and a fabricated failure would put bogus
|
|
173
|
+
* evidence in the journal for a flow whose steps all ran correctly.
|
|
174
|
+
*
|
|
175
|
+
* A detail rides in the same JSON object, so the marker stays the one durable
|
|
176
|
+
* record of the verdict and the IPC verifier's existing command comparison
|
|
177
|
+
* checks the detail against the journal for free. With no detail the command
|
|
178
|
+
* is byte-identical to the one this function has always returned, which is
|
|
179
|
+
* what keeps `spec_hash` stable for every flow that does not opt in.
|
|
180
|
+
*/
|
|
181
|
+
export function completionMarker(reason: LoweredCompletionReason, detail?: string): string {
|
|
182
|
+
if (detail === undefined) {
|
|
183
|
+
return reason === 'success' ? ':' : `printf '%s' '{"completionReason":"${reason}"}'`;
|
|
184
|
+
}
|
|
185
|
+
// `JSON.stringify` escapes quotes, backslashes and every control character,
|
|
186
|
+
// so the command is one line and the only shell-significant character left
|
|
187
|
+
// in it is `'` — which the single-quote escape below closes.
|
|
188
|
+
const json = JSON.stringify({ completionReason: reason, detail });
|
|
189
|
+
return `printf '%s' '${json.replaceAll("'", "'\\''")}'`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function typeName(value: unknown): string {
|
|
193
|
+
if (value === null) return 'null';
|
|
194
|
+
if (Array.isArray(value)) return 'an array';
|
|
195
|
+
return `a ${typeof value}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function bound(text: string): string {
|
|
199
|
+
const points = [...text];
|
|
200
|
+
if (points.length <= COMPLETION_DETAIL_MAX_CODE_POINTS) return text;
|
|
201
|
+
const keep = COMPLETION_DETAIL_MAX_CODE_POINTS - [...COMPLETION_DETAIL_TRUNCATED_SUFFIX].length;
|
|
202
|
+
return `${points.slice(0, keep).join('')}${COMPLETION_DETAIL_TRUNCATED_SUFFIX}`;
|
|
203
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HumanRecipient } from './human-to.js';
|
|
2
|
-
import type { StepFailedDetails } from './failure-kinds.js';
|
|
2
|
+
import type { ParkCause, StepFailedDetails } from './failure-kinds.js';
|
|
3
3
|
import type {
|
|
4
4
|
CompletionReason as ProtocolCompletionReason,
|
|
5
5
|
RunCompletionReason as ProtocolRunCompletionReason,
|
|
@@ -42,6 +42,18 @@ export type AuthoredFlowExecutionErrorCode =
|
|
|
42
42
|
export class AuthoredFlowExecutionError extends Error {
|
|
43
43
|
/** Set by the durable root driver after the child error crosses any IPC boundary. */
|
|
44
44
|
rootRunId?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Why an `agent_parked`/`llm_parked` child run parked, as the child's own
|
|
47
|
+
* classification established it (`RunReport.parkCause`).
|
|
48
|
+
*
|
|
49
|
+
* Set by the worker runner beside the code, because the code alone does not
|
|
50
|
+
* say: `agent_parked` covers both "nothing is attached to run this step" and
|
|
51
|
+
* the kernel's `needs_human` recovery wait, and only the first is fixed by
|
|
52
|
+
* attaching a worker. The CLI boundary reads this field rather than matching
|
|
53
|
+
* the message, so the remedy it prints cannot drift from the cause. Absent
|
|
54
|
+
* means unestablished — the boundary then says nothing about workers.
|
|
55
|
+
*/
|
|
56
|
+
parkCause?: ParkCause;
|
|
45
57
|
constructor(
|
|
46
58
|
readonly code: AuthoredFlowExecutionErrorCode,
|
|
47
59
|
message: string,
|
|
@@ -13,6 +13,13 @@ import { AuthoredBudget } from './authored-budget.js';
|
|
|
13
13
|
import { assertMemoryReachable, authoredMemory, scriptMemoryScope } from './authored-memory.js';
|
|
14
14
|
import { authoredDeterministicRunner, authoredWorkerRunner } from './authored-worker-step.js';
|
|
15
15
|
import { isSurfaceFlowCompletionReason, isSurfaceRunCompletionReason } from './authored-step-output.js';
|
|
16
|
+
import {
|
|
17
|
+
completionMarker,
|
|
18
|
+
isLoweredCompletion,
|
|
19
|
+
normalizeCompletionDetail,
|
|
20
|
+
type LoweredCompletionReason,
|
|
21
|
+
} from './authored-completion.js';
|
|
22
|
+
import { commitAuthoredVerdict } from './authored-completion-record.js';
|
|
16
23
|
import {
|
|
17
24
|
type AgentResult,
|
|
18
25
|
type LlmOptions,
|
|
@@ -21,6 +28,8 @@ import {
|
|
|
21
28
|
type Ctx,
|
|
22
29
|
type FlowCompletionReason,
|
|
23
30
|
type RunCompletionReason as SurfaceRunCompletionReason,
|
|
31
|
+
type RunOptions,
|
|
32
|
+
type RunResult,
|
|
24
33
|
type Step,
|
|
25
34
|
} from '@relayflows/surface';
|
|
26
35
|
import { createHelpers, helperProviders, type HelperCall, type FlowHandle } from '@relayflows/surface/runtime';
|
|
@@ -96,6 +105,12 @@ export interface AuthoredFlowExecutionResult {
|
|
|
96
105
|
readonly rootRunId?: string;
|
|
97
106
|
readonly name: string;
|
|
98
107
|
readonly completionReason: LoweredCompletionReason;
|
|
108
|
+
/**
|
|
109
|
+
* Why, when the body said. Normalized at `done()` — redacted, trimmed and
|
|
110
|
+
* bounded — and absent for every one-argument call, so the durable output,
|
|
111
|
+
* the IPC frame and the report keep the shapes they had.
|
|
112
|
+
*/
|
|
113
|
+
readonly completionDetail?: string;
|
|
99
114
|
readonly journalSteps: readonly AuthoredFlowJournalStep[];
|
|
100
115
|
}
|
|
101
116
|
|
|
@@ -243,6 +258,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
243
258
|
const stepEdges = (step: string): AuthoredStepEdges | undefined => lifecycle.stepEdges(step);
|
|
244
259
|
let nextStep = 1;
|
|
245
260
|
let requestedCompletion: LoweredCompletionReason | undefined;
|
|
261
|
+
let requestedDetail: string | undefined;
|
|
246
262
|
|
|
247
263
|
const lowerDeterministic = authoredDeterministicRunner(
|
|
248
264
|
definition.name, journal, journalSteps, budget, {
|
|
@@ -325,7 +341,16 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
325
341
|
(await recordedVerdicts)?.set(id, record);
|
|
326
342
|
}
|
|
327
343
|
}
|
|
328
|
-
|
|
344
|
+
// Fixed field order, never the record as it came back: the journal stores
|
|
345
|
+
// stream messages with sorted keys, so JSON.stringify of a resumed record
|
|
346
|
+
// would lower a different command than the first run did, and the gate
|
|
347
|
+
// run's admission key would refuse it as bound to a different spec.
|
|
348
|
+
const canonical: PredicateRecord = {
|
|
349
|
+
gate: 'predicate', step: record.step, verdict: record.verdict,
|
|
350
|
+
...(record.because === undefined ? {} : { because: record.because }),
|
|
351
|
+
...(record.threw === undefined ? {} : { threw: record.threw }),
|
|
352
|
+
};
|
|
353
|
+
const literal = `'${JSON.stringify(canonical).replaceAll("'", "'\\''")}'`;
|
|
329
354
|
const command = record.verdict === 'pass' ? `printf '%s' ${literal}` : `printf '%s' ${literal} >&2; exit 1`;
|
|
330
355
|
try {
|
|
331
356
|
await observeStep(`${id}.gate`, 'deterministic', () => lowerDeterministic(`${id}.gate`, command, false), options.onProgress);
|
|
@@ -373,6 +398,56 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
373
398
|
return trackStep(authoredSteps, llmOp);
|
|
374
399
|
}
|
|
375
400
|
|
|
401
|
+
/**
|
|
402
|
+
* `f.run` (docs/SURFACE.md §1). Declared as overloads like `llmOperation`,
|
|
403
|
+
* because the exit-code policy decides the RESULT TYPE: the default keeps
|
|
404
|
+
* the stdout string every body is written against, and `'record'` widens it
|
|
405
|
+
* to the `RunResult` whose `ok` is the journaled exit code.
|
|
406
|
+
*/
|
|
407
|
+
function runOperation(command: string, options?: RunOptions & { onNonZero?: 'fail' }): Step<string>;
|
|
408
|
+
function runOperation(command: string, options: RunOptions & { onNonZero: 'record' }): Step<RunResult>;
|
|
409
|
+
function runOperation(command: string, options: RunOptions): Step<string | RunResult>;
|
|
410
|
+
function runOperation(command: string, runOptions?: RunOptions): Step<string> | Step<RunResult> {
|
|
411
|
+
assertOperationAllowed('run', definition.name, requestedCompletion);
|
|
412
|
+
const leaseMs = runOptions?.timeout === undefined ? undefined : parseStepTimeout(runOptions.timeout);
|
|
413
|
+
// Refused before an ordinal is consumed or anything is journaled. A
|
|
414
|
+
// misspelled policy must not quietly fall back to the fatal default and
|
|
415
|
+
// take away the very branch the author wrote below this line.
|
|
416
|
+
const policy = runOptions?.onNonZero;
|
|
417
|
+
if (policy !== undefined && policy !== 'fail' && policy !== 'record') {
|
|
418
|
+
throw new AuthoredFlowExecutionError(
|
|
419
|
+
'unsupported_verb',
|
|
420
|
+
`f.run options.onNonZero must be 'fail' or 'record' (got ${JSON.stringify(policy)}).`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
const id = `run-${nextStep++}`;
|
|
424
|
+
if (policy === 'record') {
|
|
425
|
+
let recordOp!: AuthoredFlowOperation<RunResult>;
|
|
426
|
+
recordOp = new AuthoredFlowOperation<RunResult>(
|
|
427
|
+
id, 'run',
|
|
428
|
+
() => assertOperationAllowed('run', definition.name, requestedCompletion),
|
|
429
|
+
() => observeStep(id, 'deterministic',
|
|
430
|
+
() => lowerDeterministic.recording(id, command, leaseMs, recordOp.namedGate), options.onProgress),
|
|
431
|
+
lifecycle,
|
|
432
|
+
// No label: a command is not a display name. It carries literal
|
|
433
|
+
// tokens and URLs, and no prefix of it is safe to show (displayLabel).
|
|
434
|
+
{},
|
|
435
|
+
);
|
|
436
|
+
return trackStep(authoredSteps, recordOp);
|
|
437
|
+
}
|
|
438
|
+
let runOp!: AuthoredFlowOperation<string>;
|
|
439
|
+
runOp = new AuthoredFlowOperation<string>(
|
|
440
|
+
id, 'run',
|
|
441
|
+
() => assertOperationAllowed('run', definition.name, requestedCompletion),
|
|
442
|
+
() => observeStep(id, 'deterministic', () => lowerDeterministic(id, command, false, leaseMs, runOp.namedGate), options.onProgress),
|
|
443
|
+
lifecycle,
|
|
444
|
+
// No label: a command is not a display name. It carries literal
|
|
445
|
+
// tokens and URLs, and no prefix of it is safe to show (displayLabel).
|
|
446
|
+
{},
|
|
447
|
+
);
|
|
448
|
+
return trackStep(authoredSteps, runOp);
|
|
449
|
+
}
|
|
450
|
+
|
|
376
451
|
const slackRun = options.rootRunId ?? randomUUID();
|
|
377
452
|
function slackOperation<T>(call: SlackCall): Step<T> {
|
|
378
453
|
assertOperationAllowed(`slack.${call.verb}`, definition.name, requestedCompletion);
|
|
@@ -435,23 +510,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
435
510
|
() => assertOperationAllowed('memory', definition.name, requestedCompletion),
|
|
436
511
|
definition.header.memory?.script !== false,
|
|
437
512
|
),
|
|
438
|
-
run
|
|
439
|
-
assertOperationAllowed('run', definition.name, requestedCompletion);
|
|
440
|
-
const leaseMs = runOptions?.timeout === undefined ? undefined : parseStepTimeout(runOptions.timeout);
|
|
441
|
-
const id = `run-${nextStep++}`;
|
|
442
|
-
let runOp!: AuthoredFlowOperation<string>;
|
|
443
|
-
runOp = new AuthoredFlowOperation<string>(
|
|
444
|
-
id,
|
|
445
|
-
'run',
|
|
446
|
-
() => assertOperationAllowed('run', definition.name, requestedCompletion),
|
|
447
|
-
() => observeStep(id, 'deterministic', () => lowerDeterministic(id, command, false, leaseMs, runOp.namedGate), options.onProgress),
|
|
448
|
-
lifecycle,
|
|
449
|
-
// No label: a command is not a display name. It carries literal
|
|
450
|
-
// tokens and URLs, and no prefix of it is safe to show (displayLabel).
|
|
451
|
-
{},
|
|
452
|
-
);
|
|
453
|
-
return trackStep(authoredSteps, runOp);
|
|
454
|
-
},
|
|
513
|
+
run: runOperation,
|
|
455
514
|
llm: llmOperation,
|
|
456
515
|
agent(name, options) {
|
|
457
516
|
assertOperationAllowed('agent', definition.name, requestedCompletion);
|
|
@@ -553,7 +612,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
553
612
|
displayLabel(typeof name === 'string' ? name : undefined),
|
|
554
613
|
));
|
|
555
614
|
},
|
|
556
|
-
done(reason) {
|
|
615
|
+
done(reason, doneOptions) {
|
|
557
616
|
if (!isSurfaceFlowCompletionReason(reason)) {
|
|
558
617
|
throw new AuthoredFlowExecutionError(
|
|
559
618
|
'unsupported_completion',
|
|
@@ -586,8 +645,13 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
586
645
|
reason,
|
|
587
646
|
);
|
|
588
647
|
}
|
|
648
|
+
// Validated before the completion is marked, so a malformed `detail`
|
|
649
|
+
// leaves the flow exactly as any other refused `done()` does: not
|
|
650
|
+
// completed, and still able to report the real refusal.
|
|
651
|
+
const detail = normalizeCompletionDetail(doneOptions);
|
|
589
652
|
lifecycle.markCompletion();
|
|
590
653
|
requestedCompletion = reason;
|
|
654
|
+
requestedDetail = detail;
|
|
591
655
|
},
|
|
592
656
|
cloud: unsupportedCloud(
|
|
593
657
|
() => assertOperationAllowed('cloud', definition.name, requestedCompletion),
|
|
@@ -661,12 +725,27 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
661
725
|
// not as a fabricated kernel run.completed reason. The marker step reports
|
|
662
726
|
// what the body decided; it is not itself a step that failed. The CLI turns
|
|
663
727
|
// the verdict into the exit code (success/declined 0, needs_human 3, step_failed 1).
|
|
664
|
-
|
|
665
|
-
|
|
728
|
+
//
|
|
729
|
+
// The verdict is committed to the root BEFORE the marker run is opened, and
|
|
730
|
+
// a resumed body reuses what was committed rather than recomputing it
|
|
731
|
+
// (authored-completion-record.ts). A detail is redacted against
|
|
732
|
+
// `process.env`, so a credential rotated while this process was down would
|
|
733
|
+
// otherwise re-normalize the same authored sentence differently, retry the
|
|
734
|
+
// marker's stable admission key with a drifted spec, and lose the
|
|
735
|
+
// explanation the root had already journaled to `run_admission_conflict`.
|
|
736
|
+
// With no detail there is no environment in the command and nothing to
|
|
737
|
+
// commit, so that path is untouched.
|
|
738
|
+
const terminalId = `complete-${nextStep}`;
|
|
739
|
+
const verdict = await commitAuthoredVerdict(journal, options.rootRunId, terminalId, {
|
|
740
|
+
reason: requestedCompletion,
|
|
741
|
+
...(requestedDetail === undefined ? {} : { detail: requestedDetail }),
|
|
742
|
+
});
|
|
743
|
+
await lowerDeterministic(terminalId, completionMarker(verdict.reason, verdict.detail), true);
|
|
666
744
|
return Object.freeze({
|
|
667
745
|
...(options.rootRunId === undefined ? {} : { rootRunId: options.rootRunId }),
|
|
668
746
|
name: definition.name,
|
|
669
|
-
completionReason:
|
|
747
|
+
completionReason: verdict.reason,
|
|
748
|
+
...(verdict.detail === undefined ? {} : { completionDetail: verdict.detail }),
|
|
670
749
|
journalSteps: Object.freeze([...journalSteps]),
|
|
671
750
|
});
|
|
672
751
|
}
|
|
@@ -686,43 +765,6 @@ function unsupportedVerb(verb: string): AuthoredFlowExecutionError {
|
|
|
686
765
|
);
|
|
687
766
|
}
|
|
688
767
|
|
|
689
|
-
/**
|
|
690
|
-
* The completion reasons an authored body may declare and this executor lowers.
|
|
691
|
-
*
|
|
692
|
-
* `FlowCompletionReason` is wider than this on purpose — it is the journal's
|
|
693
|
-
* run vocabulary plus authored verdicts — but the two sets drifting silently is
|
|
694
|
-
* exactly what made a type-valid `done("step_failed")` die at runtime as
|
|
695
|
-
* `unsupported_completion`. Every gate that asks "is this a completion this
|
|
696
|
-
* runtime can lower?" now asks this one function, so a reason cannot be
|
|
697
|
-
* accepted in one place and rejected in another.
|
|
698
|
-
*
|
|
699
|
-
* These three are internal cross-module helpers for the authored seam (the
|
|
700
|
-
* executor, the durable root, the IPC verifier and the CLI report), NOT public
|
|
701
|
-
* SDK surface. `src/index.ts` deliberately re-exports nothing from this module
|
|
702
|
-
* — keep it that way, or the whole authored seam leaks with them.
|
|
703
|
-
*/
|
|
704
|
-
export const LOWERED_COMPLETIONS = ['success', 'needs_human', 'step_failed', 'declined'] as const;
|
|
705
|
-
export type LoweredCompletionReason = (typeof LOWERED_COMPLETIONS)[number];
|
|
706
|
-
|
|
707
|
-
export function isLoweredCompletion(value: unknown): value is LoweredCompletionReason {
|
|
708
|
-
return typeof value === 'string' && (LOWERED_COMPLETIONS as readonly string[]).includes(value);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
/**
|
|
712
|
-
* The deterministic command that carries an authored verdict into the journal.
|
|
713
|
-
*
|
|
714
|
-
* `success` lowers to `:` because success needs no marker: the marker run's own
|
|
715
|
-
* kernel `success` already IS that record. Every other lowered verdict is
|
|
716
|
-
* something the kernel's completion vocabulary cannot express on a step that
|
|
717
|
-
* *succeeded*, so it travels as data on stdout and is read back from
|
|
718
|
-
* `step.completed`. It is deliberately not lowered as a failing command: no
|
|
719
|
-
* step failed here, and a fabricated failure would put bogus evidence in the
|
|
720
|
-
* journal for a flow whose steps all ran correctly.
|
|
721
|
-
*/
|
|
722
|
-
export function completionMarker(reason: LoweredCompletionReason): string {
|
|
723
|
-
return reason === 'success' ? ':' : `printf '%s' '{"completionReason":"${reason}"}'`;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
768
|
function assertOperationAllowed(
|
|
727
769
|
verb: string,
|
|
728
770
|
flowName: string,
|
package/src/authored-human.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AuthoredFlowExecutionError, type AuthoredHumanWait } from './authored-flow-error.js';
|
|
2
2
|
import type { JournalClient } from './journal-client.js';
|
|
3
|
+
import { shellWord } from './shell-word.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* The answer contract for a parked `f.human`.
|
|
@@ -144,7 +145,3 @@ export function resumeCommand(runId: string, dataDir?: string, localAgent = fals
|
|
|
144
145
|
const dir = dataDir === undefined ? '' : ` --data-dir ${shellWord(dataDir)}`;
|
|
145
146
|
return `flows resume${dir}${localAgent ? ' --local-agent' : ''} ${runId}`;
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
-
function shellWord(value: string): string {
|
|
149
|
-
return /^[A-Za-z0-9_./=:@%+,-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
150
|
-
}
|
|
@@ -88,9 +88,12 @@ try {
|
|
|
88
88
|
// `details` is the failing child step's journal evidence. Without it the
|
|
89
89
|
// parent can only re-render the message, and the machine-readable
|
|
90
90
|
// diagnostic loses the command's exit code and output tails.
|
|
91
|
+
// `parkCause` travels for the same reason: it is the difference between
|
|
92
|
+
// "attach a worker" and "a human has to recover this", and this process is
|
|
93
|
+
// the only one that saw the child's classification.
|
|
91
94
|
...(error instanceof AuthoredFlowExecutionError ? { code: error.code,
|
|
92
95
|
completionReason: error.completionReason, runId: error.runId,
|
|
93
|
-
details: error.details } : {}),
|
|
96
|
+
details: error.details, parkCause: error.parkCause } : {}),
|
|
94
97
|
...(error instanceof AuthoredHumanParked ? { wait: error.wait } : {}) });
|
|
95
98
|
process.exitCode = 1;
|
|
96
99
|
} finally {
|