@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
package/src/cli/run.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { communicationInstruction } from '../communication/spec.js';
|
|
2
|
+
import { checkCommunicationEnvironment, CommunicationEnvironmentError } from '../communication/preflight.js';
|
|
1
3
|
import { parseHumanRecipient } from '../human-to.js';
|
|
2
4
|
import { parseDigestReference } from '../bundle-transport.js';
|
|
3
5
|
import { prepareDigestRun } from './run-digest.js';
|
|
@@ -13,7 +15,7 @@ import { ensureDaemon, type EnsureDaemonOptions } from '../daemon-lifecycle.js';
|
|
|
13
15
|
import { isAuthoredFlowPath } from '../direct-input.js';
|
|
14
16
|
import { daemonRefusal } from './daemon-refusal.js';
|
|
15
17
|
import type { RunFailureKind, RunWarningKind, StepFailedDetails } from '../failure-kinds.js';
|
|
16
|
-
import { inspectionHint, stepFailureDetails } from './step-failure.js';
|
|
18
|
+
import { inspectionHint, renderInspection, renderStepEvidence, stepFailureDetails } from './step-failure.js';
|
|
17
19
|
import { JournalClient, JournalProtocolError } from '../journal-client.js';
|
|
18
20
|
import { attachLocalAgent } from '../local-agent.js';
|
|
19
21
|
import { LlmWorker } from '../llm-worker.js';
|
|
@@ -49,6 +51,8 @@ export interface RunReport {
|
|
|
49
51
|
command: RunCommand;
|
|
50
52
|
path?: string;
|
|
51
53
|
runId?: string;
|
|
54
|
+
/** Authored root to resume/collect when runId identifies a failed child. */
|
|
55
|
+
rootRunId?: string;
|
|
52
56
|
socketPath?: string;
|
|
53
57
|
status?: RunStatus;
|
|
54
58
|
completionReason?: RunCompletionReason;
|
|
@@ -129,16 +133,26 @@ async function executeCheckedFlow(
|
|
|
129
133
|
// Carry the preflight's diagnostics as a RunReport from here on, so the
|
|
130
134
|
// attach step has one accumulator to append to (see `connect`).
|
|
131
135
|
const base = fromCheckReport('run', checked.report);
|
|
136
|
+
if (options.localAgent) {
|
|
137
|
+
try { checkCommunicationEnvironment(checked.flow!); }
|
|
138
|
+
catch (error) { return { exitCode: 2, report: { ...base, diagnostics: [...base.diagnostics,
|
|
139
|
+
{ severity: 'refusal', kind: 'probe_failed', message: error instanceof Error ? error.message : 'Communication environment could not be checked.' }] } }; }
|
|
140
|
+
}
|
|
132
141
|
const client = new JournalClient(socketPath);
|
|
133
142
|
const connected = await connect(client, 'run', dataDir, base, options);
|
|
134
143
|
if (connected !== undefined) return connected;
|
|
135
144
|
|
|
145
|
+
let communicationWorkers: Awaited<ReturnType<typeof import('../communication/local.js').attachCommunicationWorkers>> | undefined;
|
|
136
146
|
let localAgent: Awaited<ReturnType<typeof attachLocalAgent>> | undefined;
|
|
137
147
|
try {
|
|
138
148
|
const spec = toKernelSpec(checked.flow!);
|
|
139
149
|
// Use the checked CLI/model and declared surfaces unchanged. The worker
|
|
140
150
|
// advertises its existing pins; the daemon still owns surface matching.
|
|
141
151
|
if (options.localAgent) localAgent = await attachLocalAgent(client, dataDir, options.onPtyReady);
|
|
152
|
+
if (options.localAgent && spec.steps.some(step => step.type === 'agent' && communicationInstruction(step.instruction))) {
|
|
153
|
+
const { attachCommunicationWorkers } = await import('../communication/local.js');
|
|
154
|
+
communicationWorkers = await attachCommunicationWorkers(spec, socketPath, dataDir);
|
|
155
|
+
}
|
|
142
156
|
const outcome = await client.runStart(spec, options.reuseFromRunId);
|
|
143
157
|
const execution = await classifyOutcome(client, 'run', outcome, base, socketPath, { ...options, dataDir });
|
|
144
158
|
if (options.reuseFromRunId !== undefined) {
|
|
@@ -146,6 +160,8 @@ async function executeCheckedFlow(
|
|
|
146
160
|
}
|
|
147
161
|
return execution;
|
|
148
162
|
} catch (error) {
|
|
163
|
+
if (error instanceof CommunicationEnvironmentError) return { exitCode: 2, report: { ...base,
|
|
164
|
+
diagnostics: [...base.diagnostics, { severity: 'refusal', kind: 'probe_failed', message: error.message }] } };
|
|
149
165
|
if (error instanceof JournalProtocolError && (
|
|
150
166
|
error.code === 'reuse_spec_mismatch' || error.code === 'reuse_run_not_found'
|
|
151
167
|
|| error.code === 'reuse_journal_read_failed'
|
|
@@ -155,9 +171,9 @@ async function executeCheckedFlow(
|
|
|
155
171
|
diagnostics: [...base.diagnostics, { severity: failed ? 'failure' : 'refusal',
|
|
156
172
|
kind: error.code, message: error.message }] } };
|
|
157
173
|
}
|
|
158
|
-
return protocolFailure('run', base, socketPath, localAgent?.failure ?? error);
|
|
174
|
+
return protocolFailure('run', base, socketPath, communicationWorkers?.failure ?? localAgent?.failure ?? error);
|
|
159
175
|
} finally {
|
|
160
|
-
try { await localAgent?.close(); } finally { client.close(); }
|
|
176
|
+
try { await communicationWorkers?.close(); } finally { try { await localAgent?.close(); } finally { client.close(); } }
|
|
161
177
|
}
|
|
162
178
|
}
|
|
163
179
|
|
|
@@ -172,6 +188,7 @@ export async function resumeFlow(
|
|
|
172
188
|
const connected = await connect(client, 'resume', dataDir, base, options);
|
|
173
189
|
if (connected !== undefined) return connected;
|
|
174
190
|
|
|
191
|
+
let communicationWorkers: Awaited<ReturnType<typeof import('../communication/local.js').attachCommunicationWorkers>> | undefined;
|
|
175
192
|
let authoredAgent: Awaited<ReturnType<typeof attachLocalAgent>> | undefined;
|
|
176
193
|
let authoredLlm: LlmWorker | undefined;
|
|
177
194
|
let authoredLlmClient: JournalClient | undefined;
|
|
@@ -206,12 +223,23 @@ export async function resumeFlow(
|
|
|
206
223
|
// slack effect resume plus every other provider from N's codegen. The
|
|
207
224
|
// second call the earlier rebase left is a stale reference from before
|
|
208
225
|
// the helper fanout renamed the API.
|
|
226
|
+
if (options.localAgent) {
|
|
227
|
+
authoredAgent = await attachLocalAgent(client, dataDir, options.onPtyReady);
|
|
228
|
+
const entries = (await client.journalRead(runId, 1)).entries as Array<{ entry_type: string; payload?: { spec?: import('../spec.js').KernelRunSpec } }>;
|
|
229
|
+
const spec = entries.find(entry => entry.entry_type === 'run.spawned')?.payload?.spec;
|
|
230
|
+
if (spec?.steps.some(step => step.type === 'agent' && communicationInstruction(step.instruction))) {
|
|
231
|
+
const { attachCommunicationWorkers } = await import('../communication/local.js');
|
|
232
|
+
communicationWorkers = await attachCommunicationWorkers(spec, socketPath, dataDir);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
209
235
|
let outcome = await client.runResume(runId, options.allowHumanInfluenced);
|
|
210
236
|
if (await resumeHelperEffect(client, runId, dataDir)) {
|
|
211
237
|
outcome = await client.runResume(runId, options.allowHumanInfluenced);
|
|
212
238
|
}
|
|
213
239
|
return await classifyOutcome(client, 'resume', outcome, base, socketPath, { ...options, dataDir });
|
|
214
240
|
} catch (error) {
|
|
241
|
+
if (error instanceof CommunicationEnvironmentError) return { exitCode: 2, report: { ...base, runId, socketPath,
|
|
242
|
+
diagnostics: [...base.diagnostics, { severity: 'refusal', kind: 'probe_failed', message: error.message }] } };
|
|
215
243
|
if (error instanceof JournalProtocolError && error.code === 'human_influenced_run') {
|
|
216
244
|
return { exitCode: 2, report: { ...base, runId, socketPath,
|
|
217
245
|
diagnostics: [{ severity: 'refusal', kind: 'human_influenced_run', message: error.message.replace(/^human_influenced_run: /, '') }] } };
|
|
@@ -255,6 +283,7 @@ export async function resumeFlow(
|
|
|
255
283
|
};
|
|
256
284
|
} finally {
|
|
257
285
|
try {
|
|
286
|
+
await communicationWorkers?.close();
|
|
258
287
|
await authoredLlm?.close();
|
|
259
288
|
authoredLlmClient?.close();
|
|
260
289
|
await authoredAgent?.close();
|
|
@@ -287,16 +316,23 @@ export function authoredStepFailure(
|
|
|
287
316
|
// The failing step runs as its own kernel run, so the error's run id is the
|
|
288
317
|
// one whose journal holds the evidence. The resume target is the fallback.
|
|
289
318
|
const runId = error.runId ?? fallbackRunId;
|
|
319
|
+
const rootRunId = error.rootRunId ?? fallbackRunId;
|
|
290
320
|
return {
|
|
291
321
|
exitCode: 1,
|
|
292
322
|
report: {
|
|
293
323
|
...fromBase(command, base),
|
|
294
324
|
ok: false,
|
|
295
325
|
...(runId === undefined ? {} : { runId }),
|
|
326
|
+
...(rootRunId === undefined ? {} : { rootRunId }),
|
|
296
327
|
socketPath,
|
|
297
328
|
status: 'failed',
|
|
298
329
|
completionReason: 'step_failed',
|
|
299
330
|
diagnostics: [...base.diagnostics, {
|
|
331
|
+
// The structured evidence the failing step left in its own journal.
|
|
332
|
+
// `RunDiagnostic extends StepFailedDetails`, so `--json` gains the
|
|
333
|
+
// fields it already declares instead of leaving them to be re-parsed
|
|
334
|
+
// out of the rendered message.
|
|
335
|
+
...error.details,
|
|
300
336
|
severity: 'failure',
|
|
301
337
|
// A predicate gate that judged false is a run failure with its own
|
|
302
338
|
// name, so the report says which kind of check the body did not pass.
|
|
@@ -522,7 +558,9 @@ export async function classifyOutcome(
|
|
|
522
558
|
let unclassifiedPolls = 0;
|
|
523
559
|
while (current.status === 'parked') {
|
|
524
560
|
const inspection = await inspectOutOfBandStep(client, current.run_id);
|
|
525
|
-
|
|
561
|
+
// A runnable sibling may only be waiting for capacity held by a live
|
|
562
|
+
// attempt. Keep driving until that lease releases before declaring a park.
|
|
563
|
+
if (inspection?.parkedStep !== undefined && (inspection.needsHuman || inspection.runningStep === undefined)) {
|
|
526
564
|
parkedStep = inspection.parkedStep;
|
|
527
565
|
needsHuman = inspection.needsHuman;
|
|
528
566
|
break;
|
|
@@ -603,8 +641,7 @@ export async function classifyOutcome(
|
|
|
603
641
|
// must still end with somewhere to go rather than with a dead end.
|
|
604
642
|
const where = inspectionHint(current.run_id, details?.stepId, options.dataDir);
|
|
605
643
|
Object.assign(diagnostic, where);
|
|
606
|
-
diagnostic.message +=
|
|
607
|
-
+ (where.journalPath === undefined ? '' : `\nJournal: ${where.journalPath}`);
|
|
644
|
+
diagnostic.message += renderInspection(where);
|
|
608
645
|
}
|
|
609
646
|
return {
|
|
610
647
|
exitCode: 1,
|
|
@@ -793,24 +830,6 @@ function errorMessage(error: unknown): string {
|
|
|
793
830
|
return error instanceof Error ? error.message : 'unknown protocol error';
|
|
794
831
|
}
|
|
795
832
|
|
|
796
|
-
/**
|
|
797
|
-
* The evidence half of a `step_failed` diagnostic; `inspectionHint` adds the
|
|
798
|
-
* rest. Each field is printed only when the journal actually carried it — an
|
|
799
|
-
* agent step has no exit code to report, and inventing `exit=undefined` (which
|
|
800
|
-
* is what the deterministic-only version printed for one) is worse than
|
|
801
|
-
* silence on that field.
|
|
802
|
-
*/
|
|
803
|
-
function renderStepEvidence(details: StepFailedDetails): string {
|
|
804
|
-
return ` Step ${JSON.stringify(details.stepId)}`
|
|
805
|
-
+ (details.stepType === undefined ? '' : ` (${details.stepType})`)
|
|
806
|
-
+ ` completionReason: ${details.completionReason}`
|
|
807
|
-
+ (details.exitCode === undefined ? '' : ` exit=${details.exitCode}`)
|
|
808
|
-
+ '.'
|
|
809
|
-
+ (details.detail === undefined ? '' : `\nDetail: ${details.detail}`)
|
|
810
|
-
+ (details.stdoutTail ? `\nStdout (last 1,024 bytes):\n${details.stdoutTail}` : '')
|
|
811
|
-
+ (details.stderrTail ? `\nStderr (last 1,024 bytes):\n${details.stderrTail}` : '');
|
|
812
|
-
}
|
|
813
|
-
|
|
814
833
|
function throwIfCanceled(signal: AbortSignal | undefined, stepId: string): void {
|
|
815
834
|
if (signal?.aborted === true) throw new Error(`waiting for running step "${stepId}" was canceled`);
|
|
816
835
|
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// `flows status`: what an agent can see about its own run, from disk.
|
|
2
|
+
//
|
|
3
|
+
// Read-only and offline. Opens exactly one file — the run's journal, through
|
|
4
|
+
// the same copy-then-verify `walkJournal` that `flows replay` uses — and never
|
|
5
|
+
// the registry, `connection.json`, the daemon socket or the network. There is
|
|
6
|
+
// no credential to hold because nothing here is authenticated: the journal is
|
|
7
|
+
// on the same filesystem as the process asking. Everything a hosted run knows
|
|
8
|
+
// about itself is in that file; whatever is not (its Cloud run id, its
|
|
9
|
+
// sandbox, its listener) is Cloud's to answer, and this verb does not guess.
|
|
10
|
+
|
|
11
|
+
import type { CliIo } from '../cli.js';
|
|
12
|
+
import { canonicalize } from '../canonical.js';
|
|
13
|
+
import { DEFAULT_DATA_DIR } from '../daemon-connection.js';
|
|
14
|
+
import { JournalReadError, walkJournal, type JournalEvent } from '../journal-client.js';
|
|
15
|
+
import { redact } from '../redact.js';
|
|
16
|
+
import { foldRunState, RunStateError, type RunView, type StepView } from '../run-state.js';
|
|
17
|
+
import { DATA_DIR_ENV, RUN_ID_ENV, STEP_ID_ENV } from '../step-env.js';
|
|
18
|
+
|
|
19
|
+
export interface StatusArgs {
|
|
20
|
+
command: 'status';
|
|
21
|
+
json: boolean;
|
|
22
|
+
dataDir?: string;
|
|
23
|
+
tail?: number;
|
|
24
|
+
runId?: string;
|
|
25
|
+
/**
|
|
26
|
+
* `--cloud`: read the run from the Cloud API instead of a local journal.
|
|
27
|
+
*
|
|
28
|
+
* Parsed here because the verb is one verb — a reader should not have to
|
|
29
|
+
* know that a hosted run is a different command — but never *handled* here:
|
|
30
|
+
* `runCli` routes a `--cloud` invocation to `cli/cloud-read.ts`, so this
|
|
31
|
+
* module keeps its property of opening one file and no socket.
|
|
32
|
+
*/
|
|
33
|
+
cloud?: true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const DEFAULT_TAIL_LINES = 20;
|
|
37
|
+
/** A gate's `detail` may be 2,000 chars (engine/remote.rs); the view shows the head. */
|
|
38
|
+
const DETAIL_LIMIT = 1024;
|
|
39
|
+
const BUSY_RETRIES = 5;
|
|
40
|
+
const BUSY_RETRY_DELAY_MS = 50;
|
|
41
|
+
|
|
42
|
+
/** One transcript tail on disk, already redacted, as `--tail` renders it. */
|
|
43
|
+
export interface TranscriptTail {
|
|
44
|
+
attempt: number;
|
|
45
|
+
bytes: number;
|
|
46
|
+
lines: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type StepTails = { stdout: TranscriptTail | null; stderr: TranscriptTail | null } | null;
|
|
50
|
+
|
|
51
|
+
/** How the verb finds transcript tails; `null` until the tail writer ships. */
|
|
52
|
+
export interface TailSource {
|
|
53
|
+
read(dataDir: string, runId: string, step: StepView, lines: number): Promise<StepTails>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface StatusOptions {
|
|
57
|
+
env?: NodeJS.ProcessEnv;
|
|
58
|
+
now?: () => number;
|
|
59
|
+
tails?: TailSource;
|
|
60
|
+
sleep?: (ms: number) => Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function parseStatusArgs(args: readonly string[]): StatusArgs | undefined {
|
|
64
|
+
let json = false;
|
|
65
|
+
let cloud = false;
|
|
66
|
+
let dataDir: string | undefined;
|
|
67
|
+
let tail: number | undefined;
|
|
68
|
+
const positionals: string[] = [];
|
|
69
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
70
|
+
const argument = args[index]!;
|
|
71
|
+
if (argument === '--json') {
|
|
72
|
+
if (json) return undefined;
|
|
73
|
+
json = true;
|
|
74
|
+
} else if (argument === '--cloud') {
|
|
75
|
+
if (cloud) return undefined;
|
|
76
|
+
cloud = true;
|
|
77
|
+
} else if (argument === '--data-dir' || argument === '--tail') {
|
|
78
|
+
const value = args[++index];
|
|
79
|
+
if (value === undefined || value.length === 0 || value.startsWith('-')) return undefined;
|
|
80
|
+
if (argument === '--data-dir') {
|
|
81
|
+
if (dataDir !== undefined) return undefined;
|
|
82
|
+
dataDir = value;
|
|
83
|
+
} else {
|
|
84
|
+
if (tail !== undefined || !/^\d{1,4}$/.test(value)) return undefined;
|
|
85
|
+
tail = Number(value);
|
|
86
|
+
}
|
|
87
|
+
} else if (argument.startsWith('-')) {
|
|
88
|
+
return undefined;
|
|
89
|
+
} else {
|
|
90
|
+
positionals.push(argument);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (positionals.length > 1) return undefined;
|
|
94
|
+
// `--data-dir` and `--tail` name things on this filesystem: a data directory
|
|
95
|
+
// to read a journal out of, and per-attempt tail files beside it. A hosted
|
|
96
|
+
// run has neither, so pairing them with `--cloud` is not a narrower request
|
|
97
|
+
// but a contradiction, and is refused as an invocation rather than silently
|
|
98
|
+
// ignored. A hosted run id is mandatory: there is no ambient one.
|
|
99
|
+
if (cloud && (dataDir !== undefined || tail !== undefined || positionals.length === 0)) return undefined;
|
|
100
|
+
return {
|
|
101
|
+
command: 'status', json,
|
|
102
|
+
...(cloud ? { cloud: true as const } : {}),
|
|
103
|
+
...(dataDir === undefined ? {} : { dataDir }),
|
|
104
|
+
...(tail === undefined ? {} : { tail }),
|
|
105
|
+
...(positionals[0] === undefined ? {} : { runId: positionals[0] }),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface Snapshot {
|
|
110
|
+
events: JournalEvent[];
|
|
111
|
+
partial: string[];
|
|
112
|
+
failure?: JournalReadError;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Take one consistent snapshot of the journal. `journal_busy` means a writer
|
|
117
|
+
* was mid-flight, so wait briefly and try again; any other mid-walk failure
|
|
118
|
+
* keeps the events read so far and reports the section as partial.
|
|
119
|
+
*/
|
|
120
|
+
async function snapshot(runId: string, dataDir: string, sleep: (ms: number) => Promise<void>): Promise<Snapshot> {
|
|
121
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
122
|
+
const events: JournalEvent[] = [];
|
|
123
|
+
try {
|
|
124
|
+
for await (const event of walkJournal(runId, dataDir)) events.push(event);
|
|
125
|
+
return { events, partial: [] };
|
|
126
|
+
} catch (error) {
|
|
127
|
+
const failure = error instanceof JournalReadError ? error
|
|
128
|
+
: new JournalReadError('journal_read_failed', error instanceof Error ? error.message : String(error));
|
|
129
|
+
if (failure.code === 'journal_busy' && attempt < BUSY_RETRIES) { await sleep(BUSY_RETRY_DELAY_MS); continue; }
|
|
130
|
+
return { events, partial: [failure.code], failure };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function runStatus(args: StatusArgs, io: CliIo, options: StatusOptions = {}): Promise<0 | 1 | 2> {
|
|
136
|
+
const env = options.env ?? process.env;
|
|
137
|
+
const now = options.now ?? Date.now;
|
|
138
|
+
const sleep = options.sleep ?? ((ms) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
|
|
139
|
+
// Discovery: an explicit run id wins; otherwise the step's own identity from
|
|
140
|
+
// the environment the worker set. A shared data dir holds other people's
|
|
141
|
+
// runs, so there is no "list everything" mode to fall back to.
|
|
142
|
+
const runId = args.runId ?? env[RUN_ID_ENV];
|
|
143
|
+
if (runId === undefined || runId.length === 0) {
|
|
144
|
+
io.stderr(`REFUSED [run_unknown] pass <run-id> or run inside a step (${RUN_ID_ENV} unset)`);
|
|
145
|
+
return 2;
|
|
146
|
+
}
|
|
147
|
+
const dataDir = args.dataDir ?? env[DATA_DIR_ENV] ?? DEFAULT_DATA_DIR;
|
|
148
|
+
const thisStep = args.runId === undefined ? env[STEP_ID_ENV] ?? null : null;
|
|
149
|
+
|
|
150
|
+
const taken = await snapshot(runId, dataDir, sleep);
|
|
151
|
+
if (taken.events.length === 0) {
|
|
152
|
+
const failure = taken.failure ?? new JournalReadError('journal_read_failed', 'Journal holds no entries.');
|
|
153
|
+
io.stderr(`REFUSED [${failure.code}] ${failure.message}`);
|
|
154
|
+
return 2;
|
|
155
|
+
}
|
|
156
|
+
let view: RunView;
|
|
157
|
+
try {
|
|
158
|
+
view = foldRunState(taken.events, now());
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const message = error instanceof RunStateError ? error.message : error instanceof Error ? error.message : String(error);
|
|
161
|
+
io.stderr(`REFUSED [run_state_invalid] ${message}`);
|
|
162
|
+
return 2;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Tails are read for JSON (bounded, and the schema carries them) and for
|
|
166
|
+
// text only on request; a plain `flows status` opens the journal and nothing else.
|
|
167
|
+
const wantTails = args.json || args.tail !== undefined;
|
|
168
|
+
const tails = new Map<string, StepTails>();
|
|
169
|
+
if (options.tails !== undefined && wantTails) {
|
|
170
|
+
for (const step of view.steps) {
|
|
171
|
+
try {
|
|
172
|
+
tails.set(step.id, await options.tails.read(dataDir, runId, step, args.tail ?? DEFAULT_TAIL_LINES));
|
|
173
|
+
} catch {
|
|
174
|
+
if (!taken.partial.includes('transcript_tail_unreadable')) taken.partial.push('transcript_tail_unreadable');
|
|
175
|
+
tails.set(step.id, null);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const presented = present(view, thisStep, tails, env);
|
|
181
|
+
if (args.json) {
|
|
182
|
+
io.stdout(canonicalize({ v: 1, ...presented, partial: taken.partial }));
|
|
183
|
+
} else {
|
|
184
|
+
for (const line of renderText(presented, taken.partial, args.tail !== undefined)) io.stdout(line);
|
|
185
|
+
}
|
|
186
|
+
if (taken.failure !== undefined) io.stderr(`FAILED [${taken.failure.code}] ${taken.failure.message}`);
|
|
187
|
+
return taken.partial.length === 0 ? 0 : 1;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
type PresentedStep = StepView & { tails: StepTails };
|
|
191
|
+
type Presented = Omit<RunView, 'steps'> & { this_step: string | null; steps: PresentedStep[] };
|
|
192
|
+
|
|
193
|
+
/** Apply the redaction and size bounds the on-disk model does not have. */
|
|
194
|
+
function present(view: RunView, thisStep: string | null, tails: Map<string, StepTails>, env: NodeJS.ProcessEnv): Presented {
|
|
195
|
+
return {
|
|
196
|
+
...view,
|
|
197
|
+
this_step: thisStep,
|
|
198
|
+
steps: view.steps.map((step) => ({
|
|
199
|
+
...step,
|
|
200
|
+
// An agent names the files it writes, and it inherits this process's
|
|
201
|
+
// environment, so a path is free text like any other here.
|
|
202
|
+
artifacts: { ...step.artifacts, paths: step.artifacts.paths.map((path) => redact(path, env)) },
|
|
203
|
+
last_attempt: step.last_attempt === null ? null : {
|
|
204
|
+
...step.last_attempt,
|
|
205
|
+
verification: step.last_attempt.verification === null ? null : {
|
|
206
|
+
...step.last_attempt.verification,
|
|
207
|
+
detail: redact(step.last_attempt.verification.detail, env).slice(0, DETAIL_LIMIT),
|
|
208
|
+
},
|
|
209
|
+
// The digest's strings were redacted when it was built (flows#491), by
|
|
210
|
+
// a redactor with a different rule set to this one. Redacting again on
|
|
211
|
+
// the way out costs nothing and closes the difference — this module is
|
|
212
|
+
// the one place that decides what reaches an agent-facing page.
|
|
213
|
+
transcript: step.last_attempt.transcript === null ? null : {
|
|
214
|
+
...step.last_attempt.transcript,
|
|
215
|
+
path: step.last_attempt.transcript.path === null
|
|
216
|
+
? null : redact(step.last_attempt.transcript.path, env),
|
|
217
|
+
model: step.last_attempt.transcript.model === null
|
|
218
|
+
? null : redact(step.last_attempt.transcript.model, env),
|
|
219
|
+
failure: step.last_attempt.transcript.failure === null ? null : {
|
|
220
|
+
...step.last_attempt.transcript.failure,
|
|
221
|
+
excerpt: redact(step.last_attempt.transcript.failure.excerpt, env).slice(0, DETAIL_LIMIT),
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
tails: tails.get(step.id) ?? null,
|
|
226
|
+
})),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const GLYPH: Record<StepView['state'], string> = {
|
|
231
|
+
pending: '○', runnable: '○', running: '↻', backoff: '↻', waiting: '⏸', needs_human: '⏸', done: '✓',
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/** Agent-authored names cannot inject terminal control sequences (progress.ts). */
|
|
235
|
+
function safe(name: string): string {
|
|
236
|
+
return name.replace(/[\x00-\x1f\x7f-\x9f]/g, '?');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function formatDuration(ms: number): string {
|
|
240
|
+
const seconds = Math.max(0, ms) / 1000;
|
|
241
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
242
|
+
const minutes = Math.floor(seconds / 60);
|
|
243
|
+
if (minutes < 60) return `${minutes}m${String(Math.floor(seconds % 60)).padStart(2, '0')}s`;
|
|
244
|
+
return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function thousands(value: number): string {
|
|
248
|
+
return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function instant(ms: number): string {
|
|
252
|
+
return new Date(ms).toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function renderText(view: Presented, partial: string[], wantTails: boolean): string[] {
|
|
256
|
+
const lines: string[] = [];
|
|
257
|
+
const spend = `${thousands(view.spend.tokens_in)} in / ${thousands(view.spend.tokens_out)} out / $${view.spend.dollars}`
|
|
258
|
+
+ (view.spend.dollars_unmetered ? ' (unmetered)' : '');
|
|
259
|
+
const finished = view.completion_reason === null ? '' : ` finished ${view.completion_reason}`;
|
|
260
|
+
lines.push(`RUN ${view.run_id} ${safe(view.name)} ${view.status} started ${formatDuration(view.now_ms - view.spawned_at_ms)} ago${finished} spend ${spend}`);
|
|
261
|
+
const counts = (['done', 'running', 'pending', 'backoff', 'waiting', 'needs_human'] as const)
|
|
262
|
+
.filter((key) => view.counts[key] > 0).map((key) => `${view.counts[key]} ${key}`);
|
|
263
|
+
// The denominator is always printed, even for zero steps, so a reader can
|
|
264
|
+
// tell "nothing to show" from "nothing was read".
|
|
265
|
+
lines.push(`steps ${view.counts.total}${counts.length === 0 ? '' : `: ${counts.join(' · ')}`}`);
|
|
266
|
+
if (partial.length > 0) lines.push(`partial: ${partial.join(', ')}`);
|
|
267
|
+
lines.push('');
|
|
268
|
+
const idWidth = Math.max(4, ...view.steps.map((step) => Math.min(24, step.id.length)));
|
|
269
|
+
for (const step of view.steps) lines.push(...renderStep(step, view, idWidth, wantTails));
|
|
270
|
+
return lines;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function renderStep(step: PresentedStep, view: Presented, idWidth: number, wantTails: boolean): string[] {
|
|
274
|
+
// `completion_reason`, not `last_attempt`: an epoch summary carries the
|
|
275
|
+
// reason without an attempt record, and reading the latter marked every
|
|
276
|
+
// compacted success as a failure.
|
|
277
|
+
const failed = step.state === 'done' && step.completion_reason !== 'success';
|
|
278
|
+
const glyph = failed ? '✗' : GLYPH[step.state];
|
|
279
|
+
const cells = [safe(step.id).slice(0, 24).padEnd(idWidth), step.type.padEnd(13), step.state.padEnd(11)];
|
|
280
|
+
const iterations = step.max_iterations === null ? '' : `/${step.max_iterations}`;
|
|
281
|
+
// `> 0` matters for a step carried done by an epoch summary: the summary
|
|
282
|
+
// has no attempt count, so the fold leaves it 0 and printing "0 attempts"
|
|
283
|
+
// would state a fact the journal no longer holds. Blank, like every other
|
|
284
|
+
// state does at 0.
|
|
285
|
+
if (step.state === 'done' && step.attempt > 0) cells.push(`${step.attempt} attempt${step.attempt === 1 ? '' : 's'}`);
|
|
286
|
+
else if (step.attempt > 0) cells.push(`attempt ${step.attempt}${iterations}`);
|
|
287
|
+
if (step.elapsed_ms !== null) cells.push(formatDuration(step.elapsed_ms));
|
|
288
|
+
if (step.state === 'running' && step.lease !== null) {
|
|
289
|
+
cells.push(step.lease.overdue_ms > 0
|
|
290
|
+
? `LEASE OVERDUE by ${formatDuration(step.lease.overdue_ms)}`
|
|
291
|
+
: `lease ok (expires in ${formatDuration(step.lease.deadline_ms - view.now_ms)})`);
|
|
292
|
+
}
|
|
293
|
+
if (step.state === 'backoff' && step.backoff_until_ms !== null) cells.push(`backoff until ${instant(step.backoff_until_ms)}`);
|
|
294
|
+
if (step.wait !== null) cells.push(`awaiting ${step.wait.kind}: ${safe(step.wait.wait_id)}`);
|
|
295
|
+
if (step.state === 'done' && step.completion_reason !== null) {
|
|
296
|
+
// From the step, not the attempt: an epoch-carried step has the reason
|
|
297
|
+
// and no attempt record, and the reason is the thing worth printing.
|
|
298
|
+
cells.push(step.completion_reason);
|
|
299
|
+
const verification = step.last_attempt?.verification ?? null;
|
|
300
|
+
if (verification !== null) cells.push(`gate: ${verification.gate} ${verification.verdict}`);
|
|
301
|
+
}
|
|
302
|
+
if (step.id === view.this_step) cells.push('← this step');
|
|
303
|
+
const lines = [` ${glyph} ${cells.join(' ')}`.trimEnd()];
|
|
304
|
+
|
|
305
|
+
const last = step.last_attempt;
|
|
306
|
+
if (last !== null && step.state !== 'done') {
|
|
307
|
+
lines.push(` last attempt ${last.attempt}: ${last.completion_reason} → ${last.disposition}`);
|
|
308
|
+
}
|
|
309
|
+
if (last?.verification !== null && last?.verification !== undefined && last.verification.verdict !== 'pass') {
|
|
310
|
+
lines.push(` gate: ${last.verification.gate} ${last.verification.verdict.toUpperCase()} — ${JSON.stringify(last.verification.detail)}`);
|
|
311
|
+
}
|
|
312
|
+
const transcript = last?.transcript ?? null;
|
|
313
|
+
if (transcript !== null) {
|
|
314
|
+
// One line of provenance: what ran, how much it cost, and where the full
|
|
315
|
+
// transcript is. `flows status` never prints the transcript itself — that
|
|
316
|
+
// is `--tail`'s job for the live attempt, and the file's for a finished one.
|
|
317
|
+
const facts = [
|
|
318
|
+
transcript.model === null ? null : safe(transcript.model),
|
|
319
|
+
transcript.num_turns === null ? null : `${transcript.num_turns} turn${transcript.num_turns === 1 ? '' : 's'}`,
|
|
320
|
+
transcript.tool_calls === null ? null : `${transcript.tool_calls} tool call${transcript.tool_calls === 1 ? '' : 's'}`,
|
|
321
|
+
transcript.total_cost_usd === null ? null : `$${transcript.total_cost_usd}`,
|
|
322
|
+
].filter((fact): fact is string => fact !== null);
|
|
323
|
+
const size = transcript.bytes === null ? '' : ` (${transcript.bytes} bytes${transcript.truncated ? ', truncated' : ''})`;
|
|
324
|
+
lines.push(` transcript (attempt ${last!.attempt})${facts.length === 0 ? '' : `: ${facts.join(' · ')}`}`);
|
|
325
|
+
if (transcript.path !== null) lines.push(` ${safe(transcript.path)}${size}`);
|
|
326
|
+
if (transcript.failure !== null && transcript.failure.excerpt.length > 0) {
|
|
327
|
+
lines.push(` failed at ${safe(transcript.failure.kind)}: ${JSON.stringify(transcript.failure.excerpt)}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (step.type === 'agent' && last !== null) {
|
|
331
|
+
const shown = step.artifacts.paths.slice(0, 10).map(safe).join(', ');
|
|
332
|
+
const more = step.artifacts.paths.length > 10 ? ` +${step.artifacts.paths.length - 10} more` : '';
|
|
333
|
+
lines.push(` artifacts (attempt ${last.attempt}): ${step.artifacts.journaled
|
|
334
|
+
? step.artifacts.paths.length === 0 ? 'none' : `${shown}${more}`
|
|
335
|
+
: 'none journaled (output was a JSON object)'}`);
|
|
336
|
+
}
|
|
337
|
+
if (wantTails && step.type === 'agent' && step.attempt > 0) {
|
|
338
|
+
for (const stream of ['stdout', 'stderr'] as const) {
|
|
339
|
+
const tail = step.tails?.[stream] ?? null;
|
|
340
|
+
if (tail === null) {
|
|
341
|
+
lines.push(` ${stream} tail: no transcript on disk for attempt ${step.attempt}`);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
lines.push(` ${stream} tail (attempt ${tail.attempt}, last ${tail.lines.length} of ${tail.bytes} bytes):`);
|
|
345
|
+
for (const line of tail.lines) lines.push(` ${safe(line)}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return lines;
|
|
349
|
+
}
|