@relayflows/sdk 2.0.21 → 2.0.22
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-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/cli/run.js +2 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/status.d.ts +34 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +307 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/step-failure.js +31 -7
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cli-commands.d.ts +13 -0
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +11 -0
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -0
- package/dist/cli.js.map +1 -1
- package/dist/failure-kinds.d.ts +15 -2
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +8 -0
- package/dist/failure-kinds.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.map +1 -1
- package/dist/preflight.js +6 -0
- package/dist/preflight.js.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 +16 -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 +158 -16
- 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.map +1 -1
- package/dist/worker.js +31 -6
- 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/package.json +2 -2
- package/src/agent-transcript.ts +651 -0
- package/src/cli/run.ts +2 -1
- package/src/cli/status.ts +329 -0
- package/src/cli/step-failure.ts +32 -7
- package/src/cli-commands.ts +11 -0
- package/src/cli.ts +8 -0
- package/src/failure-kinds.ts +15 -1
- 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 +6 -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 +16 -1
- package/src/step-env.ts +47 -0
- package/src/transcript-tail.ts +193 -0
- package/src/worker-cli.ts +157 -17
- package/src/worker-usage.ts +26 -3
- package/src/worker.ts +32 -6
- package/src/wrapper-runtime.ts +7 -1
|
@@ -0,0 +1,329 @@
|
|
|
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
|
+
|
|
27
|
+
export const DEFAULT_TAIL_LINES = 20;
|
|
28
|
+
/** A gate's `detail` may be 2,000 chars (engine/remote.rs); the view shows the head. */
|
|
29
|
+
const DETAIL_LIMIT = 1024;
|
|
30
|
+
const BUSY_RETRIES = 5;
|
|
31
|
+
const BUSY_RETRY_DELAY_MS = 50;
|
|
32
|
+
|
|
33
|
+
/** One transcript tail on disk, already redacted, as `--tail` renders it. */
|
|
34
|
+
export interface TranscriptTail {
|
|
35
|
+
attempt: number;
|
|
36
|
+
bytes: number;
|
|
37
|
+
lines: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type StepTails = { stdout: TranscriptTail | null; stderr: TranscriptTail | null } | null;
|
|
41
|
+
|
|
42
|
+
/** How the verb finds transcript tails; `null` until the tail writer ships. */
|
|
43
|
+
export interface TailSource {
|
|
44
|
+
read(dataDir: string, runId: string, step: StepView, lines: number): Promise<StepTails>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface StatusOptions {
|
|
48
|
+
env?: NodeJS.ProcessEnv;
|
|
49
|
+
now?: () => number;
|
|
50
|
+
tails?: TailSource;
|
|
51
|
+
sleep?: (ms: number) => Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function parseStatusArgs(args: readonly string[]): StatusArgs | undefined {
|
|
55
|
+
let json = false;
|
|
56
|
+
let dataDir: string | undefined;
|
|
57
|
+
let tail: number | undefined;
|
|
58
|
+
const positionals: string[] = [];
|
|
59
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
60
|
+
const argument = args[index]!;
|
|
61
|
+
if (argument === '--json') {
|
|
62
|
+
if (json) return undefined;
|
|
63
|
+
json = true;
|
|
64
|
+
} else if (argument === '--data-dir' || argument === '--tail') {
|
|
65
|
+
const value = args[++index];
|
|
66
|
+
if (value === undefined || value.length === 0 || value.startsWith('-')) return undefined;
|
|
67
|
+
if (argument === '--data-dir') {
|
|
68
|
+
if (dataDir !== undefined) return undefined;
|
|
69
|
+
dataDir = value;
|
|
70
|
+
} else {
|
|
71
|
+
if (tail !== undefined || !/^\d{1,4}$/.test(value)) return undefined;
|
|
72
|
+
tail = Number(value);
|
|
73
|
+
}
|
|
74
|
+
} else if (argument.startsWith('-')) {
|
|
75
|
+
return undefined;
|
|
76
|
+
} else {
|
|
77
|
+
positionals.push(argument);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (positionals.length > 1) return undefined;
|
|
81
|
+
return {
|
|
82
|
+
command: 'status', json,
|
|
83
|
+
...(dataDir === undefined ? {} : { dataDir }),
|
|
84
|
+
...(tail === undefined ? {} : { tail }),
|
|
85
|
+
...(positionals[0] === undefined ? {} : { runId: positionals[0] }),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface Snapshot {
|
|
90
|
+
events: JournalEvent[];
|
|
91
|
+
partial: string[];
|
|
92
|
+
failure?: JournalReadError;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Take one consistent snapshot of the journal. `journal_busy` means a writer
|
|
97
|
+
* was mid-flight, so wait briefly and try again; any other mid-walk failure
|
|
98
|
+
* keeps the events read so far and reports the section as partial.
|
|
99
|
+
*/
|
|
100
|
+
async function snapshot(runId: string, dataDir: string, sleep: (ms: number) => Promise<void>): Promise<Snapshot> {
|
|
101
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
102
|
+
const events: JournalEvent[] = [];
|
|
103
|
+
try {
|
|
104
|
+
for await (const event of walkJournal(runId, dataDir)) events.push(event);
|
|
105
|
+
return { events, partial: [] };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const failure = error instanceof JournalReadError ? error
|
|
108
|
+
: new JournalReadError('journal_read_failed', error instanceof Error ? error.message : String(error));
|
|
109
|
+
if (failure.code === 'journal_busy' && attempt < BUSY_RETRIES) { await sleep(BUSY_RETRY_DELAY_MS); continue; }
|
|
110
|
+
return { events, partial: [failure.code], failure };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function runStatus(args: StatusArgs, io: CliIo, options: StatusOptions = {}): Promise<0 | 1 | 2> {
|
|
116
|
+
const env = options.env ?? process.env;
|
|
117
|
+
const now = options.now ?? Date.now;
|
|
118
|
+
const sleep = options.sleep ?? ((ms) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
|
|
119
|
+
// Discovery: an explicit run id wins; otherwise the step's own identity from
|
|
120
|
+
// the environment the worker set. A shared data dir holds other people's
|
|
121
|
+
// runs, so there is no "list everything" mode to fall back to.
|
|
122
|
+
const runId = args.runId ?? env[RUN_ID_ENV];
|
|
123
|
+
if (runId === undefined || runId.length === 0) {
|
|
124
|
+
io.stderr(`REFUSED [run_unknown] pass <run-id> or run inside a step (${RUN_ID_ENV} unset)`);
|
|
125
|
+
return 2;
|
|
126
|
+
}
|
|
127
|
+
const dataDir = args.dataDir ?? env[DATA_DIR_ENV] ?? DEFAULT_DATA_DIR;
|
|
128
|
+
const thisStep = args.runId === undefined ? env[STEP_ID_ENV] ?? null : null;
|
|
129
|
+
|
|
130
|
+
const taken = await snapshot(runId, dataDir, sleep);
|
|
131
|
+
if (taken.events.length === 0) {
|
|
132
|
+
const failure = taken.failure ?? new JournalReadError('journal_read_failed', 'Journal holds no entries.');
|
|
133
|
+
io.stderr(`REFUSED [${failure.code}] ${failure.message}`);
|
|
134
|
+
return 2;
|
|
135
|
+
}
|
|
136
|
+
let view: RunView;
|
|
137
|
+
try {
|
|
138
|
+
view = foldRunState(taken.events, now());
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const message = error instanceof RunStateError ? error.message : error instanceof Error ? error.message : String(error);
|
|
141
|
+
io.stderr(`REFUSED [run_state_invalid] ${message}`);
|
|
142
|
+
return 2;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Tails are read for JSON (bounded, and the schema carries them) and for
|
|
146
|
+
// text only on request; a plain `flows status` opens the journal and nothing else.
|
|
147
|
+
const wantTails = args.json || args.tail !== undefined;
|
|
148
|
+
const tails = new Map<string, StepTails>();
|
|
149
|
+
if (options.tails !== undefined && wantTails) {
|
|
150
|
+
for (const step of view.steps) {
|
|
151
|
+
try {
|
|
152
|
+
tails.set(step.id, await options.tails.read(dataDir, runId, step, args.tail ?? DEFAULT_TAIL_LINES));
|
|
153
|
+
} catch {
|
|
154
|
+
if (!taken.partial.includes('transcript_tail_unreadable')) taken.partial.push('transcript_tail_unreadable');
|
|
155
|
+
tails.set(step.id, null);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const presented = present(view, thisStep, tails, env);
|
|
161
|
+
if (args.json) {
|
|
162
|
+
io.stdout(canonicalize({ v: 1, ...presented, partial: taken.partial }));
|
|
163
|
+
} else {
|
|
164
|
+
for (const line of renderText(presented, taken.partial, args.tail !== undefined)) io.stdout(line);
|
|
165
|
+
}
|
|
166
|
+
if (taken.failure !== undefined) io.stderr(`FAILED [${taken.failure.code}] ${taken.failure.message}`);
|
|
167
|
+
return taken.partial.length === 0 ? 0 : 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
type PresentedStep = StepView & { tails: StepTails };
|
|
171
|
+
type Presented = Omit<RunView, 'steps'> & { this_step: string | null; steps: PresentedStep[] };
|
|
172
|
+
|
|
173
|
+
/** Apply the redaction and size bounds the on-disk model does not have. */
|
|
174
|
+
function present(view: RunView, thisStep: string | null, tails: Map<string, StepTails>, env: NodeJS.ProcessEnv): Presented {
|
|
175
|
+
return {
|
|
176
|
+
...view,
|
|
177
|
+
this_step: thisStep,
|
|
178
|
+
steps: view.steps.map((step) => ({
|
|
179
|
+
...step,
|
|
180
|
+
// An agent names the files it writes, and it inherits this process's
|
|
181
|
+
// environment, so a path is free text like any other here.
|
|
182
|
+
artifacts: { ...step.artifacts, paths: step.artifacts.paths.map((path) => redact(path, env)) },
|
|
183
|
+
last_attempt: step.last_attempt === null ? null : {
|
|
184
|
+
...step.last_attempt,
|
|
185
|
+
verification: step.last_attempt.verification === null ? null : {
|
|
186
|
+
...step.last_attempt.verification,
|
|
187
|
+
detail: redact(step.last_attempt.verification.detail, env).slice(0, DETAIL_LIMIT),
|
|
188
|
+
},
|
|
189
|
+
// The digest's strings were redacted when it was built (flows#491), by
|
|
190
|
+
// a redactor with a different rule set to this one. Redacting again on
|
|
191
|
+
// the way out costs nothing and closes the difference — this module is
|
|
192
|
+
// the one place that decides what reaches an agent-facing page.
|
|
193
|
+
transcript: step.last_attempt.transcript === null ? null : {
|
|
194
|
+
...step.last_attempt.transcript,
|
|
195
|
+
path: step.last_attempt.transcript.path === null
|
|
196
|
+
? null : redact(step.last_attempt.transcript.path, env),
|
|
197
|
+
model: step.last_attempt.transcript.model === null
|
|
198
|
+
? null : redact(step.last_attempt.transcript.model, env),
|
|
199
|
+
failure: step.last_attempt.transcript.failure === null ? null : {
|
|
200
|
+
...step.last_attempt.transcript.failure,
|
|
201
|
+
excerpt: redact(step.last_attempt.transcript.failure.excerpt, env).slice(0, DETAIL_LIMIT),
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
tails: tails.get(step.id) ?? null,
|
|
206
|
+
})),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const GLYPH: Record<StepView['state'], string> = {
|
|
211
|
+
pending: '○', runnable: '○', running: '↻', backoff: '↻', waiting: '⏸', needs_human: '⏸', done: '✓',
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** Agent-authored names cannot inject terminal control sequences (progress.ts). */
|
|
215
|
+
function safe(name: string): string {
|
|
216
|
+
return name.replace(/[\x00-\x1f\x7f-\x9f]/g, '?');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function formatDuration(ms: number): string {
|
|
220
|
+
const seconds = Math.max(0, ms) / 1000;
|
|
221
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
222
|
+
const minutes = Math.floor(seconds / 60);
|
|
223
|
+
if (minutes < 60) return `${minutes}m${String(Math.floor(seconds % 60)).padStart(2, '0')}s`;
|
|
224
|
+
return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function thousands(value: number): string {
|
|
228
|
+
return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function instant(ms: number): string {
|
|
232
|
+
return new Date(ms).toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function renderText(view: Presented, partial: string[], wantTails: boolean): string[] {
|
|
236
|
+
const lines: string[] = [];
|
|
237
|
+
const spend = `${thousands(view.spend.tokens_in)} in / ${thousands(view.spend.tokens_out)} out / $${view.spend.dollars}`
|
|
238
|
+
+ (view.spend.dollars_unmetered ? ' (unmetered)' : '');
|
|
239
|
+
const finished = view.completion_reason === null ? '' : ` finished ${view.completion_reason}`;
|
|
240
|
+
lines.push(`RUN ${view.run_id} ${safe(view.name)} ${view.status} started ${formatDuration(view.now_ms - view.spawned_at_ms)} ago${finished} spend ${spend}`);
|
|
241
|
+
const counts = (['done', 'running', 'pending', 'backoff', 'waiting', 'needs_human'] as const)
|
|
242
|
+
.filter((key) => view.counts[key] > 0).map((key) => `${view.counts[key]} ${key}`);
|
|
243
|
+
// The denominator is always printed, even for zero steps, so a reader can
|
|
244
|
+
// tell "nothing to show" from "nothing was read".
|
|
245
|
+
lines.push(`steps ${view.counts.total}${counts.length === 0 ? '' : `: ${counts.join(' · ')}`}`);
|
|
246
|
+
if (partial.length > 0) lines.push(`partial: ${partial.join(', ')}`);
|
|
247
|
+
lines.push('');
|
|
248
|
+
const idWidth = Math.max(4, ...view.steps.map((step) => Math.min(24, step.id.length)));
|
|
249
|
+
for (const step of view.steps) lines.push(...renderStep(step, view, idWidth, wantTails));
|
|
250
|
+
return lines;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function renderStep(step: PresentedStep, view: Presented, idWidth: number, wantTails: boolean): string[] {
|
|
254
|
+
// `completion_reason`, not `last_attempt`: an epoch summary carries the
|
|
255
|
+
// reason without an attempt record, and reading the latter marked every
|
|
256
|
+
// compacted success as a failure.
|
|
257
|
+
const failed = step.state === 'done' && step.completion_reason !== 'success';
|
|
258
|
+
const glyph = failed ? '✗' : GLYPH[step.state];
|
|
259
|
+
const cells = [safe(step.id).slice(0, 24).padEnd(idWidth), step.type.padEnd(13), step.state.padEnd(11)];
|
|
260
|
+
const iterations = step.max_iterations === null ? '' : `/${step.max_iterations}`;
|
|
261
|
+
// `> 0` matters for a step carried done by an epoch summary: the summary
|
|
262
|
+
// has no attempt count, so the fold leaves it 0 and printing "0 attempts"
|
|
263
|
+
// would state a fact the journal no longer holds. Blank, like every other
|
|
264
|
+
// state does at 0.
|
|
265
|
+
if (step.state === 'done' && step.attempt > 0) cells.push(`${step.attempt} attempt${step.attempt === 1 ? '' : 's'}`);
|
|
266
|
+
else if (step.attempt > 0) cells.push(`attempt ${step.attempt}${iterations}`);
|
|
267
|
+
if (step.elapsed_ms !== null) cells.push(formatDuration(step.elapsed_ms));
|
|
268
|
+
if (step.state === 'running' && step.lease !== null) {
|
|
269
|
+
cells.push(step.lease.overdue_ms > 0
|
|
270
|
+
? `LEASE OVERDUE by ${formatDuration(step.lease.overdue_ms)}`
|
|
271
|
+
: `lease ok (expires in ${formatDuration(step.lease.deadline_ms - view.now_ms)})`);
|
|
272
|
+
}
|
|
273
|
+
if (step.state === 'backoff' && step.backoff_until_ms !== null) cells.push(`backoff until ${instant(step.backoff_until_ms)}`);
|
|
274
|
+
if (step.wait !== null) cells.push(`awaiting ${step.wait.kind}: ${safe(step.wait.wait_id)}`);
|
|
275
|
+
if (step.state === 'done' && step.completion_reason !== null) {
|
|
276
|
+
// From the step, not the attempt: an epoch-carried step has the reason
|
|
277
|
+
// and no attempt record, and the reason is the thing worth printing.
|
|
278
|
+
cells.push(step.completion_reason);
|
|
279
|
+
const verification = step.last_attempt?.verification ?? null;
|
|
280
|
+
if (verification !== null) cells.push(`gate: ${verification.gate} ${verification.verdict}`);
|
|
281
|
+
}
|
|
282
|
+
if (step.id === view.this_step) cells.push('← this step');
|
|
283
|
+
const lines = [` ${glyph} ${cells.join(' ')}`.trimEnd()];
|
|
284
|
+
|
|
285
|
+
const last = step.last_attempt;
|
|
286
|
+
if (last !== null && step.state !== 'done') {
|
|
287
|
+
lines.push(` last attempt ${last.attempt}: ${last.completion_reason} → ${last.disposition}`);
|
|
288
|
+
}
|
|
289
|
+
if (last?.verification !== null && last?.verification !== undefined && last.verification.verdict !== 'pass') {
|
|
290
|
+
lines.push(` gate: ${last.verification.gate} ${last.verification.verdict.toUpperCase()} — ${JSON.stringify(last.verification.detail)}`);
|
|
291
|
+
}
|
|
292
|
+
const transcript = last?.transcript ?? null;
|
|
293
|
+
if (transcript !== null) {
|
|
294
|
+
// One line of provenance: what ran, how much it cost, and where the full
|
|
295
|
+
// transcript is. `flows status` never prints the transcript itself — that
|
|
296
|
+
// is `--tail`'s job for the live attempt, and the file's for a finished one.
|
|
297
|
+
const facts = [
|
|
298
|
+
transcript.model === null ? null : safe(transcript.model),
|
|
299
|
+
transcript.num_turns === null ? null : `${transcript.num_turns} turn${transcript.num_turns === 1 ? '' : 's'}`,
|
|
300
|
+
transcript.tool_calls === null ? null : `${transcript.tool_calls} tool call${transcript.tool_calls === 1 ? '' : 's'}`,
|
|
301
|
+
transcript.total_cost_usd === null ? null : `$${transcript.total_cost_usd}`,
|
|
302
|
+
].filter((fact): fact is string => fact !== null);
|
|
303
|
+
const size = transcript.bytes === null ? '' : ` (${transcript.bytes} bytes${transcript.truncated ? ', truncated' : ''})`;
|
|
304
|
+
lines.push(` transcript (attempt ${last!.attempt})${facts.length === 0 ? '' : `: ${facts.join(' · ')}`}`);
|
|
305
|
+
if (transcript.path !== null) lines.push(` ${safe(transcript.path)}${size}`);
|
|
306
|
+
if (transcript.failure !== null && transcript.failure.excerpt.length > 0) {
|
|
307
|
+
lines.push(` failed at ${safe(transcript.failure.kind)}: ${JSON.stringify(transcript.failure.excerpt)}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (step.type === 'agent' && last !== null) {
|
|
311
|
+
const shown = step.artifacts.paths.slice(0, 10).map(safe).join(', ');
|
|
312
|
+
const more = step.artifacts.paths.length > 10 ? ` +${step.artifacts.paths.length - 10} more` : '';
|
|
313
|
+
lines.push(` artifacts (attempt ${last.attempt}): ${step.artifacts.journaled
|
|
314
|
+
? step.artifacts.paths.length === 0 ? 'none' : `${shown}${more}`
|
|
315
|
+
: 'none journaled (output was a JSON object)'}`);
|
|
316
|
+
}
|
|
317
|
+
if (wantTails && step.type === 'agent' && step.attempt > 0) {
|
|
318
|
+
for (const stream of ['stdout', 'stderr'] as const) {
|
|
319
|
+
const tail = step.tails?.[stream] ?? null;
|
|
320
|
+
if (tail === null) {
|
|
321
|
+
lines.push(` ${stream} tail: no transcript on disk for attempt ${step.attempt}`);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
lines.push(` ${stream} tail (attempt ${tail.attempt}, last ${tail.lines.length} of ${tail.bytes} bytes):`);
|
|
325
|
+
for (const line of tail.lines) lines.push(` ${safe(line)}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return lines;
|
|
329
|
+
}
|
package/src/cli/step-failure.ts
CHANGED
|
@@ -108,28 +108,53 @@ export function inspectionHint(
|
|
|
108
108
|
* it does not. A truncated render (the daemon caps at 2,000 chars and appends
|
|
109
109
|
* a truncation note) will not parse; that falls through to the raw string,
|
|
110
110
|
* which is still the account of what went wrong.
|
|
111
|
+
*
|
|
112
|
+
* An agent or llm completion now also carries `trajectory_tail.transcript`
|
|
113
|
+
* (agent-transcript.ts) on every attempt. That object is not process-shaped,
|
|
114
|
+
* so it must not shadow the render that is: the first candidate that carries
|
|
115
|
+
* an exit code or a tail wins. The digest contributes what only it has — the
|
|
116
|
+
* failure excerpt the worker picked out of the provider's frames, and the
|
|
117
|
+
* path of the transcript file.
|
|
111
118
|
*/
|
|
112
119
|
function evidence(payload: Record<string, unknown>): Partial<StepFailedDetails> {
|
|
113
120
|
const detail = record(payload['verification'])?.['detail'];
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
121
|
+
const candidates = [
|
|
122
|
+
record(payload['output']),
|
|
123
|
+
record(payload['trajectory_tail']),
|
|
124
|
+
typeof detail === 'string' ? parsed(detail) : undefined,
|
|
125
|
+
].filter((candidate): candidate is Record<string, unknown> => candidate !== undefined);
|
|
126
|
+
const structured = candidates.find(processShaped) ?? candidates[0];
|
|
117
127
|
const exitCode = structured?.['exit_code'];
|
|
118
128
|
const stdout = structured?.['stdout_tail'];
|
|
119
129
|
const stderr = structured?.['stderr_tail'];
|
|
120
|
-
const structuredShape =
|
|
121
|
-
|
|
130
|
+
const structuredShape = structured !== undefined && processShaped(structured);
|
|
131
|
+
const transcript = record(record(payload['trajectory_tail'])?.['transcript']);
|
|
132
|
+
const failure = record(transcript?.['failure']);
|
|
133
|
+
const excerpt = failure?.['excerpt'];
|
|
134
|
+
const transcriptPath = record(transcript?.['file'])?.['path'];
|
|
135
|
+
// The excerpt is the worker's own pick of the failure; a `stderr` excerpt
|
|
136
|
+
// is the same bytes as `stderrTail`, so it is not printed twice.
|
|
137
|
+
const excerptDetail = typeof excerpt === 'string' && excerpt.length > 0
|
|
138
|
+
&& !(failure?.['kind'] === 'stderr' && typeof stderr === 'string')
|
|
139
|
+
? tail(excerpt) : undefined;
|
|
122
140
|
return {
|
|
123
141
|
...(typeof exitCode === 'number' && Number.isSafeInteger(exitCode) ? { exitCode } : {}),
|
|
124
142
|
...(typeof stdout === 'string' && stdout.length > 0 ? { stdoutTail: tail(stdout) } : {}),
|
|
125
143
|
...(typeof stderr === 'string' ? { stderrTail: tail(stderr) } : {}),
|
|
126
144
|
// Keep the daemon's account only when it was NOT just a render of the
|
|
127
145
|
// fields above — otherwise the same bytes print twice.
|
|
128
|
-
...(
|
|
129
|
-
|
|
146
|
+
...(excerptDetail !== undefined ? { detail: excerptDetail }
|
|
147
|
+
: typeof detail === 'string' && detail.length > 0 && !structuredShape
|
|
148
|
+
? { detail: tail(detail) } : {}),
|
|
149
|
+
...(typeof transcriptPath === 'string' && transcriptPath.length > 0 ? { transcriptPath } : {}),
|
|
130
150
|
};
|
|
131
151
|
}
|
|
132
152
|
|
|
153
|
+
function processShaped(value: Record<string, unknown>): boolean {
|
|
154
|
+
return typeof value['exit_code'] === 'number'
|
|
155
|
+
|| typeof value['stdout_tail'] === 'string' || typeof value['stderr_tail'] === 'string';
|
|
156
|
+
}
|
|
157
|
+
|
|
133
158
|
function parsed(value: string): Record<string, unknown> | undefined {
|
|
134
159
|
try {
|
|
135
160
|
return record(JSON.parse(value));
|
package/src/cli-commands.ts
CHANGED
|
@@ -265,6 +265,17 @@ export const CLI_VERBS = [
|
|
|
265
265
|
],
|
|
266
266
|
variants: ['serve-webhook'],
|
|
267
267
|
},
|
|
268
|
+
{
|
|
269
|
+
name: 'status',
|
|
270
|
+
description: 'Show a run’s steps, attempts, leases and last verdicts from its journal; offline, no daemon',
|
|
271
|
+
args: [{ name: 'run-id', description: 'Run to inspect; defaults to RELAYFLOW_RUN_ID inside a step', required: false }],
|
|
272
|
+
options: [
|
|
273
|
+
JSON_OPTION,
|
|
274
|
+
DATA_DIR_OPTION,
|
|
275
|
+
{ flags: '--tail <n>', description: 'Lines of each agent attempt’s transcript tail to show' },
|
|
276
|
+
],
|
|
277
|
+
variants: ['status'],
|
|
278
|
+
},
|
|
268
279
|
{
|
|
269
280
|
name: 'sync',
|
|
270
281
|
description: 'Apply a hosted run’s code changes to a local tree (replaces `agent-relay cloud sync`)',
|
package/src/cli.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { checkAuthoredTriggers } from './cli/check-triggers.js';
|
|
|
24
24
|
import { parseWebhookArgs, runServeWebhook } from './cli/serve-webhook.js';
|
|
25
25
|
import { runDirectFlow } from './cli/direct-run.js';
|
|
26
26
|
import { parseReplayArgs, replayJournal, type ReplayArgs } from './cli/replay.js';
|
|
27
|
+
import { parseStatusArgs, runStatus, type StatusArgs } from './cli/status.js';
|
|
28
|
+
import { transcriptTailSource } from './transcript-tail.js';
|
|
27
29
|
import { checkTypeScriptFlow } from './cli/check-typescript.js';
|
|
28
30
|
import { runCloudCli } from './cli/cloud-run.js';
|
|
29
31
|
import { runCloudSyncCli } from './cli/cloud-sync.js';
|
|
@@ -59,6 +61,7 @@ type CliExitCode = 0 | 1 | 2 | 3;
|
|
|
59
61
|
export type ParsedArgs =
|
|
60
62
|
| { command: 'add'; value: string }
|
|
61
63
|
| ReplayArgs
|
|
64
|
+
| StatusArgs
|
|
62
65
|
| BuildArgs
|
|
63
66
|
| DeployArgs
|
|
64
67
|
| { command: 'serve-webhook'; dataDir: string; port: number; admitted?: readonly string[] }
|
|
@@ -104,6 +107,7 @@ const USAGE = [
|
|
|
104
107
|
'flows resume [--allow-human-influenced] [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <run-id>',
|
|
105
108
|
'flows answer [--json] [--no-spawn] [--data-dir <dir>] [--note <text>] [--by <identity>] <run-id> <wait-id> <yes|no>',
|
|
106
109
|
'flows replay [--allow-human-influenced] [--json] [--data-dir <dir>] <run-id> [--at <step-id>]',
|
|
110
|
+
'flows status [--json] [--data-dir <dir>] [--tail <n>] [<run-id>]',
|
|
107
111
|
'flows observer [--data-dir <dir>]',
|
|
108
112
|
'flows hn-monitor start [--data-dir <dir>] [--poll-interval-ms <n>] <spec.json>',
|
|
109
113
|
].join('\n');
|
|
@@ -195,6 +199,9 @@ export async function runCli(
|
|
|
195
199
|
if (parsed.command === 'schedules') return runCloudSchedulesCli(parsed, io);
|
|
196
200
|
if (parsed.command === 'unschedule') return runCloudUnscheduleCli(parsed, io);
|
|
197
201
|
if (parsed.command === 'replay') return replayJournal(parsed, io);
|
|
202
|
+
// Daemon-free like `check`: reads one journal file and nothing else, so it
|
|
203
|
+
// works inside a step of a run whose daemon is gone (kernel/DAEMON-LIFECYCLE.md §4).
|
|
204
|
+
if (parsed.command === 'status') return runStatus(parsed, io, { tails: transcriptTailSource() });
|
|
198
205
|
if (parsed.command === 'answer') {
|
|
199
206
|
const execution = await answerFlow(parsed.runId, parsed.waitId, parsed.answer, parsed.dataDir, {
|
|
200
207
|
...(parsed.note === undefined ? {} : { note: parsed.note }),
|
|
@@ -498,6 +505,7 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
498
505
|
if (command === undefined || !CLI_VERB_NAMES.has(command)) return undefined;
|
|
499
506
|
if (command === 'add') return args.length === 2 ? { command: 'add', value: args[1]! } : undefined;
|
|
500
507
|
if (command === 'replay') return parseReplayArgs(args.slice(1));
|
|
508
|
+
if (command === 'status') return parseStatusArgs(args.slice(1));
|
|
501
509
|
if (command === 'build') return parseBuildArgs(args.slice(1));
|
|
502
510
|
if (command === 'deploy') {
|
|
503
511
|
// The positional decides the form: an authored source deploys a hosted
|
package/src/failure-kinds.ts
CHANGED
|
@@ -67,6 +67,13 @@ export const CHECK_FAILURE_KINDS = [
|
|
|
67
67
|
* `dollars_unmetered: true`, and cannot cross `maxDollars`; token limits still
|
|
68
68
|
* apply. It replaced the `budget_missing_price` refusal (#421), which older
|
|
69
69
|
* `flows check` reports may still show.
|
|
70
|
+
*
|
|
71
|
+
* `permissions_unenforced` names an agent step that declares `permissions`.
|
|
72
|
+
* Gate 1 validates the declaration and the compiled step spec records it, but
|
|
73
|
+
* nothing reads it to gate a file or network access — enforcement is gate 8
|
|
74
|
+
* (#442). Warning-only by design: the declaration stays legal and the flow
|
|
75
|
+
* still runs, but an author who wrote one must not be left believing it
|
|
76
|
+
* sandboxes the step.
|
|
70
77
|
*/
|
|
71
78
|
export const PREFLIGHT_WARNING_KINDS = [
|
|
72
79
|
'unprovable_effects',
|
|
@@ -74,6 +81,7 @@ export const PREFLIGHT_WARNING_KINDS = [
|
|
|
74
81
|
'command_unprovable',
|
|
75
82
|
'vacuous_gate',
|
|
76
83
|
'budget_unmetered',
|
|
84
|
+
'permissions_unenforced',
|
|
77
85
|
] as const;
|
|
78
86
|
|
|
79
87
|
/**
|
|
@@ -157,8 +165,14 @@ export interface StepFailedDetails {
|
|
|
157
165
|
stdoutTail?: string;
|
|
158
166
|
/** Terminal-safe UTF-8 excerpt, at most 1,024 bytes. */
|
|
159
167
|
stderrTail?: string;
|
|
160
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* The daemon's own account when it was not a render of the fields above, or
|
|
170
|
+
* — preferred when present — the failure excerpt from the worker's
|
|
171
|
+
* transcript digest (`trajectory_tail.transcript.failure`).
|
|
172
|
+
*/
|
|
161
173
|
detail?: string;
|
|
174
|
+
/** The attempt's redacted `stream-json` transcript on disk, when the worker wrote one. */
|
|
175
|
+
transcriptPath?: string;
|
|
162
176
|
/** A runnable `flows replay` invocation for this run. */
|
|
163
177
|
hint?: string;
|
|
164
178
|
/** The on-disk journal for this run, when the data dir is known. */
|
package/src/journal-reader.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface JournalEvent {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export type JournalReadFailure =
|
|
31
|
-
| 'invalid_run_id' | 'run_not_found' | 'step_not_found' | 'journal_read_failed';
|
|
31
|
+
| 'invalid_run_id' | 'run_not_found' | 'step_not_found' | 'journal_busy' | 'journal_read_failed';
|
|
32
32
|
|
|
33
33
|
export class JournalReadError extends Error {
|
|
34
34
|
constructor(readonly code: JournalReadFailure, message: string) {
|
|
@@ -82,7 +82,9 @@ export async function* walkJournal(
|
|
|
82
82
|
if (before[1] !== undefined) await copyFile(`${source}-wal`, `${snapshot}-wal`, constants.COPYFILE_EXCL);
|
|
83
83
|
const after = await Promise.all([fingerprint(source), fingerprint(`${source}-wal`)]);
|
|
84
84
|
if (before.some((value, index) => value !== after[index])) {
|
|
85
|
-
|
|
85
|
+
// Its own code: the journal is fine, a writer was mid-flight. A reader
|
|
86
|
+
// that can wait (`flows status`) retries on this and nothing else.
|
|
87
|
+
throw new JournalReadError('journal_busy', `Cannot read journal for run "${runId}": Journal changed while taking the snapshot; retry the command.`);
|
|
86
88
|
}
|
|
87
89
|
const file = await open(snapshot, 'r');
|
|
88
90
|
try {
|
package/src/llm-worker.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { resolveCliModel } from './cli-adapter.js';
|
|
|
9
9
|
import { withWorkerLease } from './worker-lease.js';
|
|
10
10
|
import { workerInstruction } from './worker-input.js';
|
|
11
11
|
import { jsonSchemaOutputError } from './json-schema.js';
|
|
12
|
+
import { LLM_ERROR_MAX_BYTES, boundTranscriptDigest, boundedText, redactText } from './agent-transcript.js';
|
|
12
13
|
|
|
13
14
|
/** Bare value-producing LLM dispatch, without agent workspace/recovery pins. */
|
|
14
15
|
export class LlmWorker extends EventEmitter {
|
|
@@ -74,11 +75,21 @@ export class LlmWorker extends EventEmitter {
|
|
|
74
75
|
}
|
|
75
76
|
// Never submit schema-invalid JSON as a successful completion. The kernel
|
|
76
77
|
// also runs its own gate before making output available to dependents.
|
|
78
|
+
//
|
|
79
|
+
// `error` is redacted and bounded: unbounded stderr in the tail made the
|
|
80
|
+
// kernel refuse the whole completion past 16 KiB. The digest rides beside
|
|
81
|
+
// it on every completion; an llm step has no data dir, so it has no file.
|
|
82
|
+
const transcript = completed.transcript === undefined ? undefined
|
|
83
|
+
: boundTranscriptDigest({ attempt: dispatch.attempt, exit_code: result.exit_code, ...completed.transcript });
|
|
84
|
+
const trajectoryTail = {
|
|
85
|
+
...(reason === 'success' ? {} : { error: boundedText(redactText(detail), LLM_ERROR_MAX_BYTES).text }),
|
|
86
|
+
...(transcript === undefined ? {} : { transcript }),
|
|
87
|
+
};
|
|
77
88
|
await this.client.stepComplete(dispatch.run_id, dispatch.step_id, dispatch.attempt,
|
|
78
89
|
dispatch.idempotency_key, reason, {
|
|
79
90
|
output,
|
|
80
91
|
...(usage !== undefined ? { usage } : {}),
|
|
81
|
-
...(
|
|
92
|
+
...(Object.keys(trajectoryTail).length === 0 ? {} : { trajectory_tail: trajectoryTail }),
|
|
82
93
|
});
|
|
83
94
|
}
|
|
84
95
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { CompiledFlowSpec } from './compile.js';
|
|
2
|
+
import type { PermissionsSpec } from './spec.js';
|
|
3
|
+
import type { PreflightWarning } from './preflight.js';
|
|
4
|
+
|
|
5
|
+
/** Declared fields, reported in this order so two runs read identically. */
|
|
6
|
+
const PERMISSION_FIELDS = ['fileGlobs', 'networkAllowlist', 'accessPreset'] as const;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* An agent step's `permissions` block is accepted by gate 1, lowered to
|
|
10
|
+
* `file_globs` / `network_allowlist` / `access_preset`, carried in the kernel
|
|
11
|
+
* step spec and journalled with it. Nothing reads it to gate a file or network
|
|
12
|
+
* access: enforcement is gate 8 (#442). A spec that declares it therefore
|
|
13
|
+
* looks sandboxed and is not, and until this warning existed nothing said so
|
|
14
|
+
* at authoring time.
|
|
15
|
+
*
|
|
16
|
+
* Every defined declaration warns, including `{}` and empty arrays. SURFACE.md
|
|
17
|
+
* accepts those without inferring defaults, and an empty block is exactly as
|
|
18
|
+
* likely to read to an author as "a default sandbox applies". Omitting
|
|
19
|
+
* `permissions` stays silent — there is no belief to correct.
|
|
20
|
+
*
|
|
21
|
+
* Warning-only: `ok` is unaffected, the declaration stays legal, and no value
|
|
22
|
+
* is echoed back (a glob or host is often the least interesting part of the
|
|
23
|
+
* declaration and can be long). The message names which fields were declared
|
|
24
|
+
* so the author can find them, and nothing more.
|
|
25
|
+
*/
|
|
26
|
+
export function permissionsDiagnostics(flow: CompiledFlowSpec): PreflightWarning[] {
|
|
27
|
+
const warnings: PreflightWarning[] = [];
|
|
28
|
+
for (const step of flow.steps) {
|
|
29
|
+
if (step.type !== 'agent' || step.permissions === undefined) continue;
|
|
30
|
+
warnings.push({
|
|
31
|
+
severity: 'warning',
|
|
32
|
+
kind: 'permissions_unenforced',
|
|
33
|
+
stepId: step.id,
|
|
34
|
+
message: permissionsMessage(step.id, step.permissions),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return warnings;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function permissionsMessage(stepId: string, permissions: PermissionsSpec): string {
|
|
41
|
+
const declared = PERMISSION_FIELDS.filter((field) => permissions[field] !== undefined);
|
|
42
|
+
const fields = declared.length === 0 ? '' : ` (${declared.join(', ')})`;
|
|
43
|
+
// `readonly` is the one value whose entire meaning is a guarantee, so it is
|
|
44
|
+
// the one value worth naming: an author reading "cannot write" needs to be
|
|
45
|
+
// told, in the same breath, that today nothing stops a write.
|
|
46
|
+
const preset = permissions.accessPreset === 'readonly'
|
|
47
|
+
? " accessPreset: 'readonly' does not prevent writes."
|
|
48
|
+
: '';
|
|
49
|
+
return `Step "${stepId}" declares permissions${fields}. `
|
|
50
|
+
+ 'This declaration is validated and recorded with the step spec; it is not '
|
|
51
|
+
+ 'currently enforced (gate 8 / #442). These permissions do not restrict file '
|
|
52
|
+
+ `or network access.${preset}`;
|
|
53
|
+
}
|
package/src/preflight.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { FlowSpec, StepSpec, TriggerSpec, McpServerConfig } from './spec.js
|
|
|
4
4
|
import { McpError, openMcpSession, type McpDiagnostic } from './mcp-client.js';
|
|
5
5
|
import { BudgetSyntaxError } from './budget.js';
|
|
6
6
|
import { budgetDiagnostics } from './budget-preflight.js';
|
|
7
|
+
import { permissionsDiagnostics } from './permissions-preflight.js';
|
|
7
8
|
import type { TriggerSource } from '@relayflows/surface';
|
|
8
9
|
import { acceptsAnyOutput, inspectStepGate, type StepGateInspection } from './gate-contract.js';
|
|
9
10
|
import { compileSpec, CompileError } from './compile.js';
|
|
@@ -223,6 +224,11 @@ function preflightSync(flow: unknown, options: PreflightOptions): PreflightResul
|
|
|
223
224
|
const resolutionByStep = new Map<string, CliResolution>();
|
|
224
225
|
const cliProbeResults = new Map<string, CliProbeOutcome>();
|
|
225
226
|
|
|
227
|
+
// A pure fact about the compiled snapshot, collected before anything that
|
|
228
|
+
// can return early. An unresolved CLI, an unknown model or a bad scope all
|
|
229
|
+
// refuse below without probing, and the author should still be told that the
|
|
230
|
+
// permissions they declared on the same file are not enforced.
|
|
231
|
+
diagnostics.push(...permissionsDiagnostics(compiled));
|
|
226
232
|
diagnostics.push(...scopeDiagnostics(compiled, options));
|
|
227
233
|
for (const server of new Set(options.mcpServers ?? [])) {
|
|
228
234
|
if (options.mcp !== undefined && Object.hasOwn(options.mcp, server)) continue;
|
package/src/pty-sidechannel.ts
CHANGED
|
@@ -6,6 +6,13 @@ export interface SidechannelContext {
|
|
|
6
6
|
dataDir: string;
|
|
7
7
|
runId: string;
|
|
8
8
|
stepId: string;
|
|
9
|
+
/**
|
|
10
|
+
* Which attempt this is. Names the per-attempt transcript file
|
|
11
|
+
* (`agent-transcript.ts`) and the transcript tails (`transcript-tail.ts`),
|
|
12
|
+
* and is exported into the agent's environment. Optional: without it there
|
|
13
|
+
* is no attempt to name, so neither file is written.
|
|
14
|
+
*/
|
|
15
|
+
attempt?: number;
|
|
9
16
|
onReady?: (path: string) => void;
|
|
10
17
|
onDrive: () => void;
|
|
11
18
|
}
|