@relayflows/sdk 2.0.21 → 2.0.23
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 +18 -9
- package/dist/agent-artifacts.d.ts.map +1 -1
- package/dist/agent-artifacts.js +27 -11
- package/dist/agent-artifacts.js.map +1 -1
- package/dist/agent-transcript.d.ts +186 -0
- package/dist/agent-transcript.d.ts.map +1 -0
- package/dist/agent-transcript.js +585 -0
- package/dist/agent-transcript.js.map +1 -0
- package/dist/authored-flow-error.d.ts +33 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js +20 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +4 -1
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-node-entry.js +5 -1
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts +11 -0
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +30 -1
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-root.js +3 -1
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-source-authority.js +13 -0
- package/dist/authored-source-authority.js.map +1 -1
- package/dist/authored-step-index.d.ts +82 -0
- package/dist/authored-step-index.d.ts.map +1 -0
- package/dist/authored-step-index.js +120 -0
- package/dist/authored-step-index.js.map +1 -0
- package/dist/authored-step-output.d.ts +23 -2
- package/dist/authored-step-output.d.ts.map +1 -1
- package/dist/authored-step-output.js +47 -5
- package/dist/authored-step-output.js.map +1 -1
- package/dist/authored-worker-step.d.ts +10 -2
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +81 -7
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +67 -12
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-read.d.ts +31 -0
- package/dist/cli/cloud-read.d.ts.map +1 -0
- package/dist/cli/cloud-read.js +482 -0
- package/dist/cli/cloud-read.js.map +1 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +53 -24
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/status.d.ts +43 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +321 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/step-failure.d.ts +26 -0
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +99 -10
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cli-commands.d.ts +41 -0
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +33 -0
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +4 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud-http.d.ts +12 -0
- package/dist/cloud-http.d.ts.map +1 -1
- package/dist/cloud-http.js +14 -6
- package/dist/cloud-http.js.map +1 -1
- package/dist/cloud-read.d.ts +154 -0
- package/dist/cloud-read.d.ts.map +1 -0
- package/dist/cloud-read.js +373 -0
- package/dist/cloud-read.js.map +1 -0
- package/dist/cloud-transcript.d.ts +94 -0
- package/dist/cloud-transcript.d.ts.map +1 -0
- package/dist/cloud-transcript.js +342 -0
- package/dist/cloud-transcript.js.map +1 -0
- package/dist/communication/environment.d.ts +3 -0
- package/dist/communication/environment.d.ts.map +1 -0
- package/dist/communication/environment.js +34 -0
- package/dist/communication/environment.js.map +1 -0
- package/dist/communication/history.d.ts +6 -0
- package/dist/communication/history.d.ts.map +1 -0
- package/dist/communication/history.js +38 -0
- package/dist/communication/history.js.map +1 -0
- package/dist/communication/local.d.ts +6 -0
- package/dist/communication/local.d.ts.map +1 -0
- package/dist/communication/local.js +56 -0
- package/dist/communication/local.js.map +1 -0
- package/dist/communication/preflight.d.ts +6 -0
- package/dist/communication/preflight.d.ts.map +1 -0
- package/dist/communication/preflight.js +37 -0
- package/dist/communication/preflight.js.map +1 -0
- package/dist/communication/projection.d.ts +8 -0
- package/dist/communication/projection.d.ts.map +1 -0
- package/dist/communication/projection.js +23 -0
- package/dist/communication/projection.js.map +1 -0
- package/dist/communication/relay.d.ts +78 -0
- package/dist/communication/relay.d.ts.map +1 -0
- package/dist/communication/relay.js +89 -0
- package/dist/communication/relay.js.map +1 -0
- package/dist/communication/session.d.ts +24 -0
- package/dist/communication/session.d.ts.map +1 -0
- package/dist/communication/session.js +100 -0
- package/dist/communication/session.js.map +1 -0
- package/dist/communication/spec.d.ts +20 -0
- package/dist/communication/spec.d.ts.map +1 -0
- package/dist/communication/spec.js +113 -0
- package/dist/communication/spec.js.map +1 -0
- package/dist/communication/tool.d.ts +3 -0
- package/dist/communication/tool.d.ts.map +1 -0
- package/dist/communication/tool.js +23 -0
- package/dist/communication/tool.js.map +1 -0
- package/dist/communication/tools.d.ts +11 -0
- package/dist/communication/tools.d.ts.map +1 -0
- package/dist/communication/tools.js +65 -0
- package/dist/communication/tools.js.map +1 -0
- package/dist/communication/worker.d.ts +6 -0
- package/dist/communication/worker.d.ts.map +1 -0
- package/dist/communication/worker.js +120 -0
- package/dist/communication/worker.js.map +1 -0
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +8 -1
- package/dist/compile.js.map +1 -1
- package/dist/failure-kinds.d.ts +30 -2
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +9 -0
- package/dist/failure-kinds.js.map +1 -1
- 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/journal-client.d.ts +4 -1
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +11 -2
- package/dist/journal-client.js.map +1 -1
- package/dist/journal-reader.d.ts +1 -1
- package/dist/journal-reader.d.ts.map +1 -1
- package/dist/journal-reader.js +3 -1
- package/dist/journal-reader.js.map +1 -1
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +12 -1
- package/dist/llm-worker.js.map +1 -1
- package/dist/permissions-preflight.d.ts +22 -0
- package/dist/permissions-preflight.d.ts.map +1 -0
- package/dist/permissions-preflight.js +49 -0
- package/dist/permissions-preflight.js.map +1 -0
- package/dist/preflight.d.ts +13 -2
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +29 -6
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +39 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/pty-sidechannel.d.ts +7 -0
- package/dist/pty-sidechannel.d.ts.map +1 -1
- package/dist/pty-sidechannel.js.map +1 -1
- package/dist/redact.d.ts +16 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +104 -0
- package/dist/redact.js.map +1 -0
- package/dist/run-state.d.ts +124 -0
- package/dist/run-state.d.ts.map +1 -0
- package/dist/run-state.js +368 -0
- package/dist/run-state.js.map +1 -0
- package/dist/spec.d.ts +24 -1
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/step-env.d.ts +25 -0
- package/dist/step-env.d.ts.map +1 -0
- package/dist/step-env.js +34 -0
- package/dist/step-env.js.map +1 -0
- package/dist/transcript-tail.d.ts +54 -0
- package/dist/transcript-tail.d.ts.map +1 -0
- package/dist/transcript-tail.js +176 -0
- package/dist/transcript-tail.js.map +1 -0
- package/dist/worker-cli.d.ts +10 -0
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +138 -17
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-usage.d.ts +7 -2
- package/dist/worker-usage.d.ts.map +1 -1
- package/dist/worker-usage.js +21 -3
- package/dist/worker-usage.js.map +1 -1
- package/dist/worker.d.ts +1 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +41 -7
- package/dist/worker.js.map +1 -1
- package/dist/wrapper-runtime.d.ts +7 -1
- package/dist/wrapper-runtime.d.ts.map +1 -1
- package/dist/wrapper-runtime.js +7 -1
- package/dist/wrapper-runtime.js.map +1 -1
- package/dist/wrapper-session.d.ts +8 -0
- package/dist/wrapper-session.d.ts.map +1 -1
- package/dist/wrapper-session.js +156 -23
- package/dist/wrapper-session.js.map +1 -1
- package/package.json +14 -2
- package/src/agent-artifacts.ts +27 -11
- package/src/agent-transcript.ts +651 -0
- package/src/authored-flow-error.ts +17 -0
- package/src/authored-flow-executor.ts +4 -1
- package/src/authored-node-entry.ts +5 -1
- package/src/authored-node-runner.ts +33 -1
- package/src/authored-root.ts +2 -1
- package/src/authored-source-authority.ts +13 -0
- package/src/authored-step-index.ts +157 -0
- package/src/authored-step-output.ts +66 -14
- package/src/authored-worker-step.ts +88 -15
- package/src/cli/check.ts +69 -11
- package/src/cli/cloud-read.ts +518 -0
- package/src/cli/run.ts +43 -24
- package/src/cli/status.ts +349 -0
- package/src/cli/step-failure.ts +100 -10
- package/src/cli-commands.ts +33 -0
- package/src/cli.ts +27 -0
- package/src/cloud-http.ts +32 -6
- package/src/cloud-read.ts +515 -0
- package/src/cloud-transcript.ts +422 -0
- package/src/communication/environment.ts +33 -0
- package/src/communication/history.ts +38 -0
- package/src/communication/local.ts +41 -0
- package/src/communication/preflight.ts +34 -0
- package/src/communication/projection.ts +23 -0
- package/src/communication/relay.ts +102 -0
- package/src/communication/session.ts +84 -0
- package/src/communication/spec.ts +106 -0
- package/src/communication/tool.ts +22 -0
- package/src/communication/tools.ts +50 -0
- package/src/communication/worker.ts +95 -0
- package/src/compile.ts +4 -1
- package/src/failure-kinds.ts +31 -1
- package/src/index.ts +10 -1
- package/src/journal-client.ts +12 -2
- package/src/journal-reader.ts +4 -2
- package/src/llm-worker.ts +12 -1
- package/src/permissions-preflight.ts +53 -0
- package/src/preflight.ts +42 -7
- package/src/protocol.ts +15 -0
- package/src/pty-sidechannel.ts +7 -0
- package/src/redact.ts +106 -0
- package/src/run-state.ts +468 -0
- package/src/spec.ts +18 -1
- package/src/step-env.ts +47 -0
- package/src/transcript-tail.ts +193 -0
- package/src/worker-cli.ts +139 -18
- package/src/worker-usage.ts +26 -3
- package/src/worker.ts +42 -6
- package/src/wrapper-runtime.ts +7 -1
- package/src/wrapper-session.ts +159 -24
|
@@ -206,7 +206,10 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
206
206
|
let requestedCompletion: LoweredCompletionReason | undefined;
|
|
207
207
|
|
|
208
208
|
const lowerDeterministic = authoredDeterministicRunner(
|
|
209
|
-
definition.name, journal, journalSteps, budget,
|
|
209
|
+
definition.name, journal, journalSteps, budget, {
|
|
210
|
+
...(options.rootRunId === undefined ? {} : { rootRunId: options.rootRunId }),
|
|
211
|
+
...(options.dataDir === undefined ? {} : { dataDir: options.dataDir }),
|
|
212
|
+
},
|
|
210
213
|
);
|
|
211
214
|
|
|
212
215
|
const worker = authoredWorkerRunner(
|
|
@@ -81,8 +81,12 @@ try {
|
|
|
81
81
|
const message = error instanceof Error ? error.message : 'authored body failed';
|
|
82
82
|
const prefix = error instanceof AuthoredFlowExecutionError ? `${error.code}: ` : '';
|
|
83
83
|
send({ type: 'error', message: prefix && message.startsWith(prefix) ? message.slice(prefix.length) : message,
|
|
84
|
+
// `details` is the failing child step's journal evidence. Without it the
|
|
85
|
+
// parent can only re-render the message, and the machine-readable
|
|
86
|
+
// diagnostic loses the command's exit code and output tails.
|
|
84
87
|
...(error instanceof AuthoredFlowExecutionError ? { code: error.code,
|
|
85
|
-
completionReason: error.completionReason, runId: error.runId
|
|
88
|
+
completionReason: error.completionReason, runId: error.runId,
|
|
89
|
+
details: error.details } : {}),
|
|
86
90
|
...(error instanceof AuthoredHumanParked ? { wait: error.wait } : {}) });
|
|
87
91
|
process.exitCode = 1;
|
|
88
92
|
} finally {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
AuthoredFlowExecutionError, AuthoredHumanParked,
|
|
14
14
|
type AuthoredFlowExecutionErrorCode, type AuthoredHumanWait,
|
|
15
15
|
} from './authored-flow-error.js';
|
|
16
|
+
import type { StepFailedDetails } from './failure-kinds.js';
|
|
16
17
|
import { HUMAN_WAIT_ID } from './authored-human.js';
|
|
17
18
|
import { assertAuthoredPromiseHooks } from './authored-runtime-capability.js';
|
|
18
19
|
|
|
@@ -139,7 +140,8 @@ export async function runAuthoredInNode(
|
|
|
139
140
|
? new AuthoredHumanParked(message.wait, rootRunId)
|
|
140
141
|
: typeof message.code === 'string'
|
|
141
142
|
? new AuthoredFlowExecutionError(message.code as AuthoredFlowExecutionErrorCode,
|
|
142
|
-
message.message, message.completionReason, message.runId
|
|
143
|
+
message.message, message.completionReason, message.runId,
|
|
144
|
+
stepFailedFrame(message.details))
|
|
143
145
|
: new Error(message.message);
|
|
144
146
|
} else throw new Error('unknown authored runtime message');
|
|
145
147
|
} catch (error) { stop(error instanceof Error ? error : new Error('invalid authored runtime message')); }
|
|
@@ -171,6 +173,36 @@ function isHumanWaitFrame(value: unknown): value is AuthoredHumanWait {
|
|
|
171
173
|
&& typeof wait.to === 'string' && wait.to !== '';
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Step evidence arriving over IPC, reduced to the fields `StepFailedDetails`
|
|
178
|
+
* declares and the types it declares them as. The child is the same pinned
|
|
179
|
+
* payload the parent hashed, but the frame is still a claim: an unrecognised
|
|
180
|
+
* or mistyped field is dropped rather than trusted, and a malformed `details`
|
|
181
|
+
* yields no details at all. It throws on nothing `JSON.parse` can produce —
|
|
182
|
+
* the run is already failing, and losing the original failure to report a bad
|
|
183
|
+
* evidence frame would replace the answer with a worse one.
|
|
184
|
+
*/
|
|
185
|
+
export function stepFailedFrame(value: unknown): StepFailedDetails | undefined {
|
|
186
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;
|
|
187
|
+
const frame = value as Record<string, unknown>;
|
|
188
|
+
const text = (key: string): string | undefined =>
|
|
189
|
+
typeof frame[key] === 'string' ? (frame[key] as string).slice(0, 8192) : undefined;
|
|
190
|
+
const count = (key: string): number | undefined =>
|
|
191
|
+
typeof frame[key] === 'number' && Number.isSafeInteger(frame[key]) ? frame[key] as number : undefined;
|
|
192
|
+
const details: StepFailedDetails = {};
|
|
193
|
+
for (const [key, parsed] of [
|
|
194
|
+
['stepId', text('stepId')], ['stepType', text('stepType')],
|
|
195
|
+
['completionReason', text('completionReason')], ['attempt', count('attempt')],
|
|
196
|
+
['maxIterations', count('maxIterations')], ['exitCode', count('exitCode')],
|
|
197
|
+
['stdoutTail', text('stdoutTail')], ['stderrTail', text('stderrTail')],
|
|
198
|
+
['detail', text('detail')], ['transcriptPath', text('transcriptPath')],
|
|
199
|
+
['hint', text('hint')], ['journalPath', text('journalPath')],
|
|
200
|
+
] as const) {
|
|
201
|
+
if (parsed !== undefined) (details as Record<string, unknown>)[key] = parsed;
|
|
202
|
+
}
|
|
203
|
+
return Object.keys(details).length === 0 ? undefined : details;
|
|
204
|
+
}
|
|
205
|
+
|
|
174
206
|
/** The IPC frame is a claim, not a durable terminal fact or a sandbox boundary. */
|
|
175
207
|
export async function verifyAuthoredNodeResult(
|
|
176
208
|
result: AuthoredFlowExecutionResult, metadata: AuthoredRootMetadata,
|
package/src/authored-root.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { RunOutcome, StepDispatchEvent } from './protocol.js';
|
|
|
15
15
|
import { SPEC_SCHEMA_VERSION } from './spec.js';
|
|
16
16
|
import type { RunLifecycleOptions } from './cli/run.js';
|
|
17
17
|
import { withWorkerLease } from './worker-lease.js';
|
|
18
|
-
import { AuthoredHumanParked } from './authored-flow-error.js';
|
|
18
|
+
import { AuthoredFlowExecutionError, AuthoredHumanParked } from './authored-flow-error.js';
|
|
19
19
|
import { readOpenHumanWaits } from './authored-human.js';
|
|
20
20
|
import { isSurfaceCompletionReason } from './authored-step-output.js';
|
|
21
21
|
|
|
@@ -234,6 +234,7 @@ async function driveRoot(
|
|
|
234
234
|
throw error;
|
|
235
235
|
}
|
|
236
236
|
await terminalizeRootFailure(peer, dispatch, error);
|
|
237
|
+
if (error instanceof AuthoredFlowExecutionError) error.rootRunId = dispatch.run_id;
|
|
237
238
|
throw error;
|
|
238
239
|
}
|
|
239
240
|
}
|
|
@@ -46,7 +46,20 @@ function installPinnedSourceLoader(sources: Map<string, string>): void {
|
|
|
46
46
|
pinnedSourceLoaderInstalled = true;
|
|
47
47
|
// Node >=22.14 supports register and stripTypeScriptTypes. A loader thread
|
|
48
48
|
// receives the captured bytes, never reopening mutable authored paths.
|
|
49
|
+
// The `stripTypeScriptTypes` call below makes Node emit
|
|
50
|
+
// `ExperimentalWarning: stripTypeScriptTypes …` on the child's inherited
|
|
51
|
+
// stderr, where a bootstrap that reads the CLI's stderr takes it for the
|
|
52
|
+
// error text. It is emitted on this loader thread, so a listener on the
|
|
53
|
+
// main thread cannot intercept it, and `--disable-warning` on the child
|
|
54
|
+
// would silence every ExperimentalWarning the authored body raises too.
|
|
55
|
+
// Dropping it by its own text, on this thread only, is the narrow form:
|
|
56
|
+
// Node's own printer still handles every other warning.
|
|
49
57
|
const loader = `
|
|
58
|
+
const emitWarning = process.emitWarning.bind(process);
|
|
59
|
+
process.emitWarning = (warning, ...rest) => {
|
|
60
|
+
if (rest[0] === 'ExperimentalWarning' && String(warning).startsWith('stripTypeScriptTypes')) return;
|
|
61
|
+
emitWarning(warning, ...rest);
|
|
62
|
+
};
|
|
50
63
|
import {stripTypeScriptTypes} from 'node:module';
|
|
51
64
|
let sources;
|
|
52
65
|
export function initialize(data){sources=new Map(data);}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { JournalClient } from './journal-client.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The root run's durable index of the child runs an authored body opened.
|
|
5
|
+
*
|
|
6
|
+
* An authored flow is not one kernel run: every `f.run`, `f.agent`, `f.llm`
|
|
7
|
+
* and lowered gate is its own single-step child run with its own
|
|
8
|
+
* `<dataDir>/runs/<runId>.sqlite3`. The only thing that ties a child back to
|
|
9
|
+
* the root id a caller (or Cloud) knows is an index — and until now that index
|
|
10
|
+
* existed only in the root's SUCCESS completion output
|
|
11
|
+
* (`authored-root.ts` `driveRoot`). A failed body took
|
|
12
|
+
* `terminalizeRootFailure`, which journals `{ error }` and nothing else, so a
|
|
13
|
+
* failed run named no child at all. A failed run is exactly when its children
|
|
14
|
+
* are worth reading.
|
|
15
|
+
*
|
|
16
|
+
* The completion output could not be made to carry it either: the root is
|
|
17
|
+
* lowered as `type: 'agent'`, and the kernel nulls a failed agent's `output`
|
|
18
|
+
* (`preserve_failure_output`, relayflowd-core/src/machine.rs), preserving only
|
|
19
|
+
* a 2,000-character render in `verification.detail`. So the index is written
|
|
20
|
+
* where a failure cannot erase it — appended to a stream on the ROOT run as
|
|
21
|
+
* each child is opened, using the same `journal.streamAppend` durability the
|
|
22
|
+
* executor already relies on for predicate verdicts.
|
|
23
|
+
*
|
|
24
|
+
* Each record is bounded on its own rather than the index being truncated as
|
|
25
|
+
* one blob: a hundredth child must not push the first one out of the record.
|
|
26
|
+
*/
|
|
27
|
+
export const AUTHORED_STEP_STREAM = 'authored-steps';
|
|
28
|
+
|
|
29
|
+
/** Versioned so a reader can tell this record from a future one. */
|
|
30
|
+
const RECORD_KIND = 'relayflows.authored-step.v1';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Per-field cap. Step ids and run ids are short by construction; the cap
|
|
34
|
+
* exists so a hostile or buggy id cannot make one record unbounded.
|
|
35
|
+
*/
|
|
36
|
+
const FIELD_CHARS = 256;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `admitted` and `completed` are kept distinct on purpose. `admitted` says
|
|
40
|
+
* only "this child run exists and here is where to read it" — it never
|
|
41
|
+
* carries a `completionReason`, because manufacturing one for an attempt
|
|
42
|
+
* that has not completed would turn an unfinished child into a false fact.
|
|
43
|
+
* `completed` is written only from a terminal `step.completed` the journal
|
|
44
|
+
* actually holds.
|
|
45
|
+
*/
|
|
46
|
+
export interface AuthoredStepRecord {
|
|
47
|
+
readonly index: typeof RECORD_KIND;
|
|
48
|
+
readonly step: string;
|
|
49
|
+
readonly runId: string;
|
|
50
|
+
readonly state: 'admitted' | 'completed';
|
|
51
|
+
/** Present only on `completed`. */
|
|
52
|
+
readonly completionReason?: string;
|
|
53
|
+
/**
|
|
54
|
+
* The kernel step the completion came from, when it is not the authored
|
|
55
|
+
* operation's own id. A child spec has two steps once a named gate is
|
|
56
|
+
* lowered, so the terminal failure can be `<step>.verify` rather than
|
|
57
|
+
* `<step>` — saying which is the difference between an index and a guess.
|
|
58
|
+
*/
|
|
59
|
+
readonly kernelStep?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Append one record, or do nothing when there is no root to append to.
|
|
64
|
+
*
|
|
65
|
+
* The undefined-root case is the non-durable executor seam (`executeAuthoredFlow`
|
|
66
|
+
* called without `rootRunId`), which has no root run and therefore nothing to
|
|
67
|
+
* index. It is not a silent fallback: there is no durable place for the record
|
|
68
|
+
* to go, and the caller already knows it opted out of one.
|
|
69
|
+
*
|
|
70
|
+
* Append failures propagate (AGENTS.md rule 4). Callers on a path that is
|
|
71
|
+
* already failing must surface the persistence error ALONGSIDE the original
|
|
72
|
+
* failure rather than letting it replace it — see `alsoRecord`.
|
|
73
|
+
*/
|
|
74
|
+
export async function recordAuthoredChild(
|
|
75
|
+
journal: JournalClient,
|
|
76
|
+
rootRunId: string | undefined,
|
|
77
|
+
record: Omit<AuthoredStepRecord, 'index'>,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
if (rootRunId === undefined) return;
|
|
80
|
+
await journal.streamAppend(rootRunId, AUTHORED_STEP_STREAM, {
|
|
81
|
+
index: RECORD_KIND,
|
|
82
|
+
step: bound(record.step),
|
|
83
|
+
runId: bound(record.runId),
|
|
84
|
+
state: record.state,
|
|
85
|
+
...(record.completionReason === undefined ? {} : { completionReason: bound(record.completionReason) }),
|
|
86
|
+
...(record.kernelStep === undefined || record.kernelStep === record.step
|
|
87
|
+
? {} : { kernelStep: bound(record.kernelStep) }),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Record a child on a path that is already throwing, keeping both facts.
|
|
93
|
+
*
|
|
94
|
+
* A failed append must not be relabelled as successful collection, and it must
|
|
95
|
+
* not replace the failure the caller is about to report. It returns the note
|
|
96
|
+
* to append to that failure's message instead.
|
|
97
|
+
*/
|
|
98
|
+
export async function alsoRecord(
|
|
99
|
+
journal: JournalClient,
|
|
100
|
+
rootRunId: string | undefined,
|
|
101
|
+
record: Omit<AuthoredStepRecord, 'index'>,
|
|
102
|
+
): Promise<string> {
|
|
103
|
+
try {
|
|
104
|
+
await recordAuthoredChild(journal, rootRunId, record);
|
|
105
|
+
return '';
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return `\nCould not persist the step index: ${error instanceof Error ? error.message : String(error)}`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Fold the root's stream back into one record per authored step.
|
|
113
|
+
*
|
|
114
|
+
* A `completed` record supersedes the `admitted` one for the same step; first
|
|
115
|
+
* admission order is preserved so the index reads in the order the body ran.
|
|
116
|
+
* Records this version does not recognise are skipped rather than trusted.
|
|
117
|
+
*/
|
|
118
|
+
export async function readAuthoredStepIndex(
|
|
119
|
+
journal: JournalClient,
|
|
120
|
+
rootRunId: string,
|
|
121
|
+
): Promise<AuthoredStepRecord[]> {
|
|
122
|
+
const index = new Map<string, AuthoredStepRecord>();
|
|
123
|
+
let offset = 0;
|
|
124
|
+
for (;;) {
|
|
125
|
+
const page = await journal.streamRead(rootRunId, AUTHORED_STEP_STREAM, offset, 1000);
|
|
126
|
+
for (const message of page.messages) {
|
|
127
|
+
// `stream.read` returns either the envelope or the bare message,
|
|
128
|
+
// matching how the executor reads predicate verdicts.
|
|
129
|
+
const raw = (message as { message?: unknown }).message ?? message;
|
|
130
|
+
if (!isAuthoredStepRecord(raw)) continue;
|
|
131
|
+
const previous = index.get(raw.step);
|
|
132
|
+
// An `admitted` record arriving after a `completed` one (a resumed body
|
|
133
|
+
// re-admitting the same child under its stable admission key) must not
|
|
134
|
+
// un-complete it.
|
|
135
|
+
if (previous?.state === 'completed' && raw.state === 'admitted') continue;
|
|
136
|
+
index.set(raw.step, raw);
|
|
137
|
+
}
|
|
138
|
+
if (page.messages.length === 0 || page.next_offset <= offset) break;
|
|
139
|
+
offset = page.next_offset;
|
|
140
|
+
}
|
|
141
|
+
return [...index.values()];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isAuthoredStepRecord(value: unknown): value is AuthoredStepRecord {
|
|
145
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
|
|
146
|
+
const record = value as Partial<AuthoredStepRecord>;
|
|
147
|
+
return record.index === RECORD_KIND
|
|
148
|
+
&& typeof record.step === 'string' && record.step.length > 0
|
|
149
|
+
&& typeof record.runId === 'string' && record.runId.length > 0
|
|
150
|
+
&& (record.state === 'admitted' || record.state === 'completed')
|
|
151
|
+
&& (record.completionReason === undefined || typeof record.completionReason === 'string')
|
|
152
|
+
&& (record.kernelStep === undefined || typeof record.kernelStep === 'string');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function bound(value: string): string {
|
|
156
|
+
return value.length <= FIELD_CHARS ? value : `${value.slice(0, FIELD_CHARS)}…`;
|
|
157
|
+
}
|
|
@@ -3,6 +3,21 @@ import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
|
3
3
|
import type { JournalClient } from './journal-client.js';
|
|
4
4
|
import type { CompletionReason as ProtocolCompletionReason, RunCompletionReason as ProtocolRunCompletionReason, RunOutcome } from './protocol.js';
|
|
5
5
|
import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
|
|
6
|
+
import type { StepFailedDetails } from './failure-kinds.js';
|
|
7
|
+
import { inspectionHint, renderInspection, renderStepEvidence, stepFailureDetails } from './cli/step-failure.js';
|
|
8
|
+
import { alsoRecord, recordAuthoredChild } from './authored-step-index.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What an authored operation needs in order to leave readable evidence:
|
|
12
|
+
* the root run its child index belongs to, and the data dir whose journals
|
|
13
|
+
* a reader would open. Both optional — the non-durable executor seam has no
|
|
14
|
+
* root, and a caller that did not name a data dir still gets the `flows
|
|
15
|
+
* replay <run-id>` hint, just without the on-disk path.
|
|
16
|
+
*/
|
|
17
|
+
export interface AuthoredStepContext {
|
|
18
|
+
readonly rootRunId?: string;
|
|
19
|
+
readonly dataDir?: string;
|
|
20
|
+
}
|
|
6
21
|
|
|
7
22
|
/**
|
|
8
23
|
* Shared by every `f.*` verb that lowers to one kernel step run in isolation:
|
|
@@ -18,12 +33,23 @@ import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
|
|
|
18
33
|
* terminal state. Given that, a single read from the start of this run's
|
|
19
34
|
* (small, single-step) journal is enough — no polling here, and no run
|
|
20
35
|
* outcome ever needs re-checking.
|
|
36
|
+
*
|
|
37
|
+
* A non-success completion used to throw `journal step "run-1" completed with
|
|
38
|
+
* retries_exhausted` and stop there, discarding the evidence it was holding:
|
|
39
|
+
* the kernel PRESERVES a failed deterministic step's `output`
|
|
40
|
+
* (`preserve_failure_output`, relayflowd-core/src/machine.rs), so
|
|
41
|
+
* `{exit_code, stdout_tail, stderr_tail}` — the answer to "which command
|
|
42
|
+
* failed and what did it print" — was in hand and dropped. Four different
|
|
43
|
+
* causes produced that one identical line. It now reads the same extractor
|
|
44
|
+
* `f.agent`/`f.llm` already reach through `classifyOutcome`, so both step
|
|
45
|
+
* families report one grammar instead of two divergent ones.
|
|
21
46
|
*/
|
|
22
47
|
export async function readCompletedStepOutput(
|
|
23
48
|
journal: JournalClient,
|
|
24
49
|
runId: string,
|
|
25
50
|
stepId: string,
|
|
26
51
|
journalSteps: AuthoredFlowJournalStep[],
|
|
52
|
+
context: AuthoredStepContext = {},
|
|
27
53
|
): Promise<unknown> {
|
|
28
54
|
const entries = (await journal.journalRead(runId, 1)).entries;
|
|
29
55
|
const completed = entries.find((entry) => isStepCompleted(entry, stepId));
|
|
@@ -34,13 +60,29 @@ export async function readCompletedStepOutput(
|
|
|
34
60
|
const reason = completed.payload.completionReason;
|
|
35
61
|
journalSteps.push(Object.freeze({ id: stepId, runId, completionReason: reason }));
|
|
36
62
|
if (reason !== 'success') {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
63
|
+
let message = `journal step "${stepId}" completed with ${reason}`;
|
|
64
|
+
let details: StepFailedDetails | undefined;
|
|
65
|
+
try {
|
|
66
|
+
details = await stepFailureDetails(journal, runId);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
// Inspection must not erase the already known step failure; the two
|
|
69
|
+
// failures stay separately visible, as in classifyOutcome.
|
|
70
|
+
message += ` Could not inspect the failed step: ${errorMessage(error)}`;
|
|
71
|
+
}
|
|
72
|
+
if (details !== undefined) message += renderStepEvidence(details);
|
|
73
|
+
// Appended whatever inspection found — including nothing. A failure shape
|
|
74
|
+
// this reader does not recognise must still end with somewhere to go.
|
|
75
|
+
const where = inspectionHint(runId, details?.stepId ?? stepId, context.dataDir);
|
|
76
|
+
message += renderInspection(where);
|
|
77
|
+
message += await alsoRecord(journal, context.rootRunId, {
|
|
78
|
+
step: stepId, runId, state: 'completed', completionReason: reason,
|
|
79
|
+
...(details?.stepId === undefined ? {} : { kernelStep: details.stepId }),
|
|
80
|
+
});
|
|
81
|
+
throw new AuthoredFlowExecutionError('step_failed', message, reason, runId, { ...details, ...where });
|
|
43
82
|
}
|
|
83
|
+
await recordAuthoredChild(journal, context.rootRunId, {
|
|
84
|
+
step: stepId, runId, state: 'completed', completionReason: reason,
|
|
85
|
+
});
|
|
44
86
|
return completed.payload.output;
|
|
45
87
|
}
|
|
46
88
|
|
|
@@ -49,15 +91,21 @@ export async function readSuccessfulOutput(
|
|
|
49
91
|
outcome: RunOutcome,
|
|
50
92
|
stepId: string,
|
|
51
93
|
journalSteps: AuthoredFlowJournalStep[],
|
|
94
|
+
context: AuthoredStepContext = {},
|
|
52
95
|
): Promise<string> {
|
|
53
|
-
const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
96
|
+
const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps, context)
|
|
97
|
+
.catch(error => {
|
|
98
|
+
if (error instanceof AuthoredFlowExecutionError
|
|
99
|
+
&& (error.completionReason === 'timeout' || error.completionReason === 'lease_expired')) {
|
|
100
|
+
// A timeout is still a failed command, and the evidence extracted for
|
|
101
|
+
// it is the same evidence. Re-labelling the error must not delete it.
|
|
102
|
+
throw new AuthoredFlowExecutionError('lease_exceeded',
|
|
103
|
+
`f.run step "${stepId}" exceeded its command timeout.`
|
|
104
|
+
+ ` ${error.message.slice(`${error.code}: `.length)}`,
|
|
105
|
+
error.completionReason, error.runId, error.details);
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
});
|
|
61
109
|
if (!isRecord(output) || typeof output['stdout_tail'] !== 'string') {
|
|
62
110
|
throw protocolViolation(outcome.run_id, `step "${stepId}" has no string stdout_tail`);
|
|
63
111
|
}
|
|
@@ -102,6 +150,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
102
150
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
103
151
|
}
|
|
104
152
|
|
|
153
|
+
function errorMessage(error: unknown): string {
|
|
154
|
+
return error instanceof Error ? error.message : String(error);
|
|
155
|
+
}
|
|
156
|
+
|
|
105
157
|
function protocolViolation(runId: string, detail: string): AuthoredFlowExecutionError {
|
|
106
158
|
return new AuthoredFlowExecutionError(
|
|
107
159
|
'journal_protocol_violation',
|
|
@@ -3,15 +3,17 @@ import { parseBudget } from './budget.js';
|
|
|
3
3
|
import type { AgentOptions, AgentResult, LlmOptions, NamedGate } from '@relayflows/surface';
|
|
4
4
|
import { compileSpec, toKernelSpec } from './compile.js';
|
|
5
5
|
import { checkAuthoredFlow } from './cli/check.js';
|
|
6
|
-
import { classifyOutcome, type RunLifecycleOptions } from './cli/run.js';
|
|
6
|
+
import { classifyOutcome, type RunLifecycleOptions, type RunReport } from './cli/run.js';
|
|
7
7
|
import type { PreflightDiagnostic } from './preflight.js';
|
|
8
8
|
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
9
9
|
import type { JournalClient } from './journal-client.js';
|
|
10
10
|
import { SPEC_SCHEMA_VERSION, type FlowSpec, type PermissionsSpec, type StepSpec } from './spec.js';
|
|
11
|
-
import { isSurfaceCompletionReason, readCompletedStepOutput, readSuccessfulOutput } from './authored-step-output.js';
|
|
11
|
+
import { isSurfaceCompletionReason, readCompletedStepOutput, readSuccessfulOutput, type AuthoredStepContext } from './authored-step-output.js';
|
|
12
12
|
import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
|
|
13
13
|
import { snapshotJsonValue } from './json-value.js';
|
|
14
14
|
import { authoredChildAdmissionKey } from './authored-admission.js';
|
|
15
|
+
import { alsoRecord, recordAuthoredChild } from './authored-step-index.js';
|
|
16
|
+
import type { StepFailedDetails } from './failure-kinds.js';
|
|
15
17
|
|
|
16
18
|
const WORKSPACE_PERMISSION_ANNOTATION = /:\s*(readonly|readwrite)\s*$/i;
|
|
17
19
|
|
|
@@ -22,6 +24,10 @@ export function authoredWorkerRunner(
|
|
|
22
24
|
localAgentStream?: string, budget?: AuthoredBudget, headerBudget?: unknown,
|
|
23
25
|
rootRunId?: string,
|
|
24
26
|
) {
|
|
27
|
+
const context: AuthoredStepContext = {
|
|
28
|
+
...(rootRunId === undefined ? {} : { rootRunId }),
|
|
29
|
+
...(waitOptions.dataDir === undefined ? {} : { dataDir: waitOptions.dataDir }),
|
|
30
|
+
};
|
|
25
31
|
async function run(step: StepSpec): Promise<unknown> {
|
|
26
32
|
const id = step.id;
|
|
27
33
|
const authoring: FlowSpec = { version: SPEC_SCHEMA_VERSION, name: `${definition.name}/${id}`, steps: [step], ...(headerBudget === undefined ? {} : { budget: parseBudget(headerBudget) }) };
|
|
@@ -46,6 +52,10 @@ export function authoredWorkerRunner(
|
|
|
46
52
|
}
|
|
47
53
|
const spec = toKernelSpec(resolved);
|
|
48
54
|
const consume = async (outcome: import('./protocol.js').RunOutcome) => {
|
|
55
|
+
// Written BEFORE the wait, not after it. An agent runs for as long as its
|
|
56
|
+
// lease allows; if this process dies mid-step, this record is the only
|
|
57
|
+
// thing that still names the child run holding the evidence.
|
|
58
|
+
await recordAuthoredChild(journal, rootRunId, { step: id, runId: outcome.run_id, state: 'admitted' });
|
|
49
59
|
// Reuse the declarative CLI's own wait/classification (cli/run.ts) rather
|
|
50
60
|
// than a hand-rolled poll: `step.completed` and the run's own terminal
|
|
51
61
|
// state are appended as two SEPARATE actions (kernel/relayflowd-core/src/machine.rs
|
|
@@ -69,17 +79,38 @@ export function authoredWorkerRunner(
|
|
|
69
79
|
);
|
|
70
80
|
}
|
|
71
81
|
if (execution.exitCode !== 0) {
|
|
82
|
+
// `execution.report.completionReason` is the RUN's reason (normally
|
|
83
|
+
// `step_failed`), not the step's. The step's terminal facts are the
|
|
84
|
+
// ones `classifyOutcome` already extracted with `stepFailureDetails`
|
|
85
|
+
// and assigned onto this diagnostic — read them from there rather than
|
|
86
|
+
// re-walking the journal or mislabelling the run reason as a step one.
|
|
87
|
+
const diagnostic = execution.report.diagnostics.at(-1);
|
|
88
|
+
const details = stepDetails(diagnostic);
|
|
72
89
|
const reason = execution.report.completionReason;
|
|
90
|
+
let message = diagnostic?.message
|
|
91
|
+
?? `flow "${definition.name}" step "${id}" did not complete successfully `
|
|
92
|
+
+ `(status: ${execution.report.status ?? 'unknown'})`;
|
|
93
|
+
// Only a reason the journal actually recorded for a step is indexed;
|
|
94
|
+
// an unfinished or unreadable child is left as `admitted`, never given
|
|
95
|
+
// a manufactured completion.
|
|
96
|
+
if (isSurfaceCompletionReason(details?.completionReason)) {
|
|
97
|
+
journalSteps.push(Object.freeze({
|
|
98
|
+
id, runId: outcome.run_id, completionReason: details.completionReason,
|
|
99
|
+
}));
|
|
100
|
+
message += await alsoRecord(journal, rootRunId, {
|
|
101
|
+
step: id, runId: outcome.run_id, state: 'completed',
|
|
102
|
+
completionReason: details.completionReason,
|
|
103
|
+
...(details.stepId === undefined ? {} : { kernelStep: details.stepId }),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
73
106
|
throw new AuthoredFlowExecutionError(
|
|
74
|
-
'step_failed',
|
|
75
|
-
execution.report.diagnostics.at(-1)?.message
|
|
76
|
-
?? `flow "${definition.name}" step "${id}" did not complete successfully `
|
|
77
|
-
+ `(status: ${execution.report.status ?? 'unknown'})`,
|
|
107
|
+
'step_failed', message,
|
|
78
108
|
isSurfaceCompletionReason(reason) ? reason : undefined,
|
|
79
109
|
outcome.run_id,
|
|
110
|
+
details,
|
|
80
111
|
);
|
|
81
112
|
}
|
|
82
|
-
return readCompletedStepOutput(journal, outcome.run_id, id, journalSteps);
|
|
113
|
+
return readCompletedStepOutput(journal, outcome.run_id, id, journalSteps, context);
|
|
83
114
|
};
|
|
84
115
|
const admissionKey = authoredChildAdmissionKey(rootRunId, id);
|
|
85
116
|
return budget === undefined
|
|
@@ -173,11 +204,19 @@ export function authoredWorkerRunner(
|
|
|
173
204
|
};
|
|
174
205
|
}
|
|
175
206
|
|
|
176
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* Deterministic commands execute inline under their per-invocation lease.
|
|
209
|
+
*
|
|
210
|
+
* `context` carries the root run (so each child is indexed on it) and the
|
|
211
|
+
* data dir (so a failure can name the journal on disk). It is the same object
|
|
212
|
+
* the worker runner builds; a failed `f.run` and a failed `f.agent` now report
|
|
213
|
+
* through one grammar.
|
|
214
|
+
*/
|
|
177
215
|
export function authoredDeterministicRunner(
|
|
178
216
|
name: string, journal: JournalClient, journalSteps: AuthoredFlowJournalStep[], budget: AuthoredBudget,
|
|
179
|
-
|
|
217
|
+
context: AuthoredStepContext = {},
|
|
180
218
|
) {
|
|
219
|
+
const rootRunId = context.rootRunId;
|
|
181
220
|
return async (id: string, command: string, terminal = false, leaseMs?: number, verification?: NamedGate): Promise<string> => {
|
|
182
221
|
const spec = toKernelSpec(compileSpec({
|
|
183
222
|
version: SPEC_SCHEMA_VERSION,
|
|
@@ -189,11 +228,45 @@ export function authoredDeterministicRunner(
|
|
|
189
228
|
}],
|
|
190
229
|
}));
|
|
191
230
|
const admissionKey = authoredChildAdmissionKey(rootRunId, id);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
);
|
|
231
|
+
const consume = async (outcome: import('./protocol.js').RunOutcome): Promise<string> => {
|
|
232
|
+
await recordAuthoredChild(journal, rootRunId, { step: id, runId: outcome.run_id, state: 'admitted' });
|
|
233
|
+
return readSuccessfulOutput(journal, outcome, id, journalSteps, context);
|
|
234
|
+
};
|
|
235
|
+
if (terminal) return consume(await journal.runStart(spec, undefined, admissionKey));
|
|
236
|
+
return budget.execute(journal, spec, consume, admissionKey);
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Lift the step-shaped fields off a run diagnostic.
|
|
242
|
+
*
|
|
243
|
+
* `RunDiagnostic extends StepFailedDetails`, so `classifyOutcome`'s
|
|
244
|
+
* `Object.assign(diagnostic, details)` leaves the extraction right there. The
|
|
245
|
+
* fields are picked explicitly rather than spread, so `severity`, `kind` and
|
|
246
|
+
* `message` — which describe the RUN, not the step — cannot leak into
|
|
247
|
+
* something typed as the step's evidence.
|
|
248
|
+
*/
|
|
249
|
+
function stepDetails(
|
|
250
|
+
diagnostic: RunReport['diagnostics'][number] | undefined,
|
|
251
|
+
): StepFailedDetails | undefined {
|
|
252
|
+
if (diagnostic === undefined) return undefined;
|
|
253
|
+
// `RunDiagnostic` is the only member of this union that extends
|
|
254
|
+
// `StepFailedDetails`; the check-report shapes carry none of these fields,
|
|
255
|
+
// so reading them as optionals yields `undefined` and contributes nothing.
|
|
256
|
+
const found = diagnostic as Partial<StepFailedDetails>;
|
|
257
|
+
const details: StepFailedDetails = {
|
|
258
|
+
...(found.stepId === undefined ? {} : { stepId: found.stepId }),
|
|
259
|
+
...(found.stepType === undefined ? {} : { stepType: found.stepType }),
|
|
260
|
+
...(found.completionReason === undefined ? {} : { completionReason: found.completionReason }),
|
|
261
|
+
...(found.attempt === undefined ? {} : { attempt: found.attempt }),
|
|
262
|
+
...(found.maxIterations === undefined ? {} : { maxIterations: found.maxIterations }),
|
|
263
|
+
...(found.exitCode === undefined ? {} : { exitCode: found.exitCode }),
|
|
264
|
+
...(found.stdoutTail === undefined ? {} : { stdoutTail: found.stdoutTail }),
|
|
265
|
+
...(found.stderrTail === undefined ? {} : { stderrTail: found.stderrTail }),
|
|
266
|
+
...(found.detail === undefined ? {} : { detail: found.detail }),
|
|
267
|
+
...(found.transcriptPath === undefined ? {} : { transcriptPath: found.transcriptPath }),
|
|
268
|
+
...(found.hint === undefined ? {} : { hint: found.hint }),
|
|
269
|
+
...(found.journalPath === undefined ? {} : { journalPath: found.journalPath }),
|
|
198
270
|
};
|
|
271
|
+
return Object.keys(details).length === 0 ? undefined : details;
|
|
199
272
|
}
|