@agentswf/engine 0.1.0-beta.4 → 0.1.0
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/LICENSE +21 -0
- package/README.md +16 -0
- package/package.json +13 -9
- package/src/accounting/format.ts +4 -3
- package/src/accounting/spent.ts +128 -0
- package/src/accounting/summary.ts +40 -32
- package/src/agent-cli/cli.ts +35 -22
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +5 -7
- package/src/attempt-output.ts +15 -32
- package/src/attempt-view.ts +34 -33
- package/src/author-typebox.ts +13 -0
- package/src/client/checks.ts +94 -0
- package/src/client/exported.ts +17 -0
- package/src/client/index.ts +122 -0
- package/src/client/locate.ts +34 -0
- package/src/client/published.ts +3 -0
- package/src/client/runs.ts +337 -0
- package/src/command-line.ts +64 -0
- package/src/commands.ts +7 -1
- package/src/control-plane.ts +41 -11
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +41 -33
- package/src/deadlines.ts +40 -7
- package/src/decisions/directory.ts +51 -40
- package/src/decisions/fake.ts +1 -1
- package/src/decisions/openrouter.ts +4 -2
- package/src/decisions/seam.ts +4 -4
- package/src/display-path.ts +3 -3
- package/src/duration.ts +43 -27
- package/src/emit-command.ts +88 -58
- package/src/emit.ts +254 -56
- package/src/event-ids.ts +31 -0
- package/src/files.ts +44 -4
- package/src/hand-over.ts +150 -0
- package/src/help/allowance.txt +4 -3
- package/src/help/emit.txt +18 -6
- package/src/help/inbox.txt +6 -4
- package/src/help/logs.txt +9 -6
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +30 -11
- package/src/help/runs.txt +4 -2
- package/src/help/tick.txt +9 -4
- package/src/herdr-placement.ts +3 -33
- package/src/herdr-run-session.ts +7 -15
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +56 -133
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +40 -57
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.test.ts.template +3 -3
- package/src/init-template/hello/workflow.ts.template +5 -4
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +15 -12
- package/src/logs-command.ts +128 -47
- package/src/message-operation.ts +6 -3
- package/src/messaging/limits.ts +4 -4
- package/src/messaging/messaging.ts +31 -28
- package/src/messaging/routes.ts +1 -1
- package/src/messaging/text.ts +9 -7
- package/src/open-command.ts +770 -0
- package/src/operation-events.ts +4 -6
- package/src/operation-liveness.ts +19 -4
- package/src/operator-cli.ts +224 -94
- package/src/operator-runtime.ts +29 -7
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +105 -59
- package/src/progress-view.ts +10 -10
- package/src/project-engine.ts +79 -0
- package/src/projects.ts +13 -2
- package/src/refusals.ts +119 -0
- package/src/result-slots.ts +11 -31
- package/src/result-validation.ts +2 -2
- package/src/run-command.ts +45 -28
- package/src/run-commands.ts +43 -115
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/{stage-plan.ts → run-policy/stage-plan.ts} +46 -42
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +205 -43
- package/src/run-read.ts +109 -41
- package/src/run-state.ts +170 -50
- package/src/run-text.ts +54 -0
- package/src/run-unreachable.ts +18 -0
- package/src/run-usage.ts +18 -12
- package/src/run-view.ts +81 -17
- package/src/runs-command.ts +83 -63
- package/src/runs.ts +310 -212
- package/src/sandbox-homes.ts +65 -13
- package/src/sandboxes.ts +182 -28
- package/src/session-lines.ts +93 -0
- package/src/skills/run-skills.ts +8 -1
- package/src/sources.ts +28 -29
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +119 -131
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +36 -14
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +73 -26
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +25 -16
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +54 -4
- package/src/workflow-runner.ts +805 -580
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +3 -3
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +121 -53
- package/src/workflow-testing/index.ts +33 -842
- package/src/workflow-testing/run-test.ts +1128 -0
- package/src/workflow-testing/script.ts +74 -45
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -990
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- package/src/workflow-testing/workflow-testing.typecheck.ts +0 -64
package/src/operator-cli.ts
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import { existsSync, realpathSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join, relative, resolve } from "node:path";
|
|
5
|
-
import type { HarnessAllowance } from "@agentswf/contract/records";
|
|
5
|
+
import type { HarnessAllowance, RefusalCode } from "@agentswf/contract/records";
|
|
6
6
|
import type { JsonObject, JsonValue } from "@agentswf/contract/workflow";
|
|
7
|
-
import { type Harness, handBack, type RunProcess } from "@agentswf/harness";
|
|
7
|
+
import { type Harness, handBack, type RunProcess, terminalAttached } from "@agentswf/harness";
|
|
8
8
|
import manifest from "../package.json" with { type: "json" };
|
|
9
9
|
import { stageFigures } from "./accounting/format";
|
|
10
|
-
import {
|
|
10
|
+
import { earlierSpent, unreadEarlier } from "./accounting/spent";
|
|
11
|
+
import { ALLOWANCE_V, allowanceCommand } from "./allowance-command";
|
|
11
12
|
import {
|
|
12
13
|
type AttemptEnd,
|
|
13
14
|
cleanupFailed,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
} from "./attempt-ending";
|
|
20
21
|
import { type Kept, keepRecords } from "./attempt-output";
|
|
21
22
|
import { continueCommand, printEnding, toldOf } from "./attempt-view";
|
|
23
|
+
import { placeFlags, shellLine } from "./command-line";
|
|
22
24
|
import { type CommandName, helpOf, index, isCommand, reference, unknownCommand } from "./commands";
|
|
23
25
|
import {
|
|
24
26
|
type LineReader,
|
|
@@ -28,32 +30,44 @@ import {
|
|
|
28
30
|
} from "./coordinator-prompt";
|
|
29
31
|
import { coordinatorSocket, prepareCoordinatorDir } from "./coordinator-socket";
|
|
30
32
|
import { EmitError, emitEvent, openEventRun } from "./emit";
|
|
31
|
-
import { emitCommand } from "./emit-command";
|
|
33
|
+
import { didOf, EMIT_V, emitCommand, launchExit } from "./emit-command";
|
|
32
34
|
import { messageOf } from "./errors";
|
|
35
|
+
import { isInside } from "./files";
|
|
36
|
+
import { installedEngine } from "./hand-over";
|
|
33
37
|
import { type Caller, showOwnTab, startHere, takeCaller } from "./here";
|
|
34
|
-
import { holderCommand } from "./holder-command";
|
|
35
|
-
import { inboxCommand } from "./inbox-command";
|
|
38
|
+
import { HOLDER_V, holderCommand } from "./holder-command";
|
|
39
|
+
import { INBOX_V, inboxCommand } from "./inbox-command";
|
|
36
40
|
import { initCommand } from "./init-command";
|
|
37
|
-
import { logsCommand } from "./logs-command";
|
|
41
|
+
import { LOGS_V, logsCommand } from "./logs-command";
|
|
38
42
|
import { coordinatorDir, machinePaths, sandboxesDirOf } from "./machine";
|
|
43
|
+
import { OPEN_V, openCommand } from "./open-command";
|
|
39
44
|
import {
|
|
40
45
|
allowanceReader,
|
|
41
46
|
herdrConfig,
|
|
42
47
|
installOperatorRuntime,
|
|
43
48
|
type OperatorRuntimeInstallation,
|
|
49
|
+
openRuntime,
|
|
44
50
|
type RunSession,
|
|
45
51
|
} from "./operator-runtime";
|
|
46
52
|
import { ANSI, describeRunSession, type Terminal, watchProgress } from "./progress-view";
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
53
|
+
import { asksJson, NO_JSON, printRefusal, readFailureCode, refusalLine } from "./refusals";
|
|
54
|
+
import { parseCommand, RUN_V, type RunCommand, usage } from "./run-command";
|
|
55
|
+
import { startedLine } from "./run-commands";
|
|
49
56
|
import { runLockFile, withRunLock } from "./run-lock";
|
|
50
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
claimNext,
|
|
59
|
+
loadAndPrepare,
|
|
60
|
+
type OpensEventRun,
|
|
61
|
+
type PreparedRun,
|
|
62
|
+
workspaceLabel,
|
|
63
|
+
} from "./run-prepare";
|
|
51
64
|
import { findRun } from "./run-state";
|
|
52
65
|
import { type Attempt, createRun, discardRun, endAttempt, type Run, RunRefused } from "./runs";
|
|
53
|
-
import { runsCommand } from "./runs-command";
|
|
66
|
+
import { RUNS_V, runsCommand } from "./runs-command";
|
|
67
|
+
import { readSpent } from "./spend";
|
|
54
68
|
import { stopCrashed } from "./stop-crashed";
|
|
55
69
|
import { testCommand } from "./test-command";
|
|
56
|
-
import { tickCommand } from "./tick-command";
|
|
70
|
+
import { TICK_V, tickCommand } from "./tick-command";
|
|
57
71
|
import type { Launch } from "./wake";
|
|
58
72
|
import type { EventWorkflow, LoadedWorkflow } from "./workflow-loader";
|
|
59
73
|
import {
|
|
@@ -96,6 +110,11 @@ export type OperatorEnvironment = {
|
|
|
96
110
|
readAllowance?: (harness: Harness) => Promise<HarnessAllowance>;
|
|
97
111
|
/** Starts or continues an event run's attempt for `awf emit`; absent, a detached `awf run`. */
|
|
98
112
|
launch?: Launch;
|
|
113
|
+
/**
|
|
114
|
+
* The background launch that started this process, which its attempt records; absent, the
|
|
115
|
+
* environment's `AWF_LAUNCH`.
|
|
116
|
+
*/
|
|
117
|
+
launchId?: string;
|
|
99
118
|
/**
|
|
100
119
|
* Where agents may `--await-reply`, past the engine's list: for the live checks that decide
|
|
101
120
|
* what goes on it (story 029's M2 gate).
|
|
@@ -112,6 +131,12 @@ export type OperatorEnvironment = {
|
|
|
112
131
|
stdin?: () => Promise<string>;
|
|
113
132
|
};
|
|
114
133
|
|
|
134
|
+
/** An operator environment with the operator's home and environment variables resolved. */
|
|
135
|
+
type Resolved = OperatorEnvironment & {
|
|
136
|
+
home: string;
|
|
137
|
+
environment: Readonly<Record<string, string | undefined>>;
|
|
138
|
+
};
|
|
139
|
+
|
|
115
140
|
type Output = {
|
|
116
141
|
stdout: (text: string) => void;
|
|
117
142
|
stderr: (text: string) => void;
|
|
@@ -121,11 +146,38 @@ type Output = {
|
|
|
121
146
|
now: () => number;
|
|
122
147
|
};
|
|
123
148
|
|
|
149
|
+
/**
|
|
150
|
+
* `given`, its launch taken: the launch names this process's attempt only, so it is read once and
|
|
151
|
+
* kept from anything the process spawns, `process.env` and an injected environment alike, and a run
|
|
152
|
+
* an agent starts doesn't record it.
|
|
153
|
+
*/
|
|
154
|
+
export function takeLaunch(given: OperatorEnvironment): OperatorEnvironment {
|
|
155
|
+
const launchId = given.launchId ?? (given.environment ?? process.env).AWF_LAUNCH;
|
|
156
|
+
delete process.env.AWF_LAUNCH;
|
|
157
|
+
const { AWF_LAUNCH: _, ...kept } = given.environment ?? {};
|
|
158
|
+
return {
|
|
159
|
+
...given,
|
|
160
|
+
...(given.environment ? { environment: kept } : {}),
|
|
161
|
+
...(launchId === undefined ? {} : { launchId }),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
124
165
|
export async function runOperatorCli(
|
|
125
166
|
argv: readonly string[],
|
|
126
|
-
|
|
167
|
+
given: OperatorEnvironment = {},
|
|
127
168
|
): Promise<number> {
|
|
128
|
-
const
|
|
169
|
+
const taken = takeLaunch(given);
|
|
170
|
+
const environment: Resolved = {
|
|
171
|
+
...taken,
|
|
172
|
+
home: taken.home ?? homedir(),
|
|
173
|
+
environment: taken.environment ?? process.env,
|
|
174
|
+
};
|
|
175
|
+
const print = environment.stdout ?? ((text) => console.log(text));
|
|
176
|
+
let printed = false;
|
|
177
|
+
const stdout = (text: string) => {
|
|
178
|
+
printed = true;
|
|
179
|
+
print(text);
|
|
180
|
+
};
|
|
129
181
|
const write = (text: string) => void process.stderr.write(text);
|
|
130
182
|
const stderr =
|
|
131
183
|
environment.stderr ??
|
|
@@ -152,16 +204,17 @@ export async function runOperatorCli(
|
|
|
152
204
|
stderr,
|
|
153
205
|
stdin: environment.stdin ?? (() => new Response(Bun.stdin.stream()).text()),
|
|
154
206
|
...(environment.stdin ? {} : { stdinIsTerminal: process.stdin.isTTY === true }),
|
|
155
|
-
|
|
156
|
-
|
|
207
|
+
environment: environment.environment,
|
|
208
|
+
home: environment.home,
|
|
157
209
|
});
|
|
158
210
|
const wakes = {
|
|
159
|
-
home: environment.home
|
|
160
|
-
environment: environment.environment
|
|
211
|
+
home: environment.home,
|
|
212
|
+
environment: environment.environment,
|
|
161
213
|
...(environment.launch ? { launch: environment.launch } : {}),
|
|
162
214
|
};
|
|
215
|
+
const output = { stdout, stderr, notice, terminal, now };
|
|
163
216
|
const handlers = {
|
|
164
|
-
run: () => runCommand(argv, environment,
|
|
217
|
+
run: () => runCommand(argv, environment, output),
|
|
165
218
|
test: () => {
|
|
166
219
|
const captured = environment.stdout || environment.stderr ? { stdout, stderr } : undefined;
|
|
167
220
|
return testCommand(argv.slice(1), cwd, {
|
|
@@ -179,14 +232,36 @@ export async function runOperatorCli(
|
|
|
179
232
|
}),
|
|
180
233
|
emit: () => emitCommand(argv.slice(1), cwd, { stdout, stderr }, wakes),
|
|
181
234
|
tick: () => tickCommand(argv.slice(1), cwd, { stdout, stderr }, wakes),
|
|
182
|
-
runs: () => runsCommand(argv.slice(1), cwd, { stdout, stderr },
|
|
183
|
-
logs: () =>
|
|
235
|
+
runs: () => runsCommand(argv.slice(1), cwd, { stdout, stderr }, now(), environment.home),
|
|
236
|
+
logs: () =>
|
|
237
|
+
logsCommand(
|
|
238
|
+
argv.slice(1),
|
|
239
|
+
cwd,
|
|
240
|
+
{ stdout, stderr },
|
|
241
|
+
{ now, sleep: (ms) => Bun.sleep(ms) },
|
|
242
|
+
environment.home,
|
|
243
|
+
),
|
|
244
|
+
open: () =>
|
|
245
|
+
openCommand(
|
|
246
|
+
argv.slice(1),
|
|
247
|
+
cwd,
|
|
248
|
+
{ stdout, stderr },
|
|
249
|
+
{
|
|
250
|
+
home: environment.home,
|
|
251
|
+
environment: environment.environment,
|
|
252
|
+
...openRuntime(environment.environment),
|
|
253
|
+
...(environment.signal ? { signal: environment.signal } : {}),
|
|
254
|
+
...(environment.stdin
|
|
255
|
+
? { stdin: environment.stdin }
|
|
256
|
+
: { stdinIsTerminal: process.stdin.isTTY === true }),
|
|
257
|
+
},
|
|
258
|
+
),
|
|
184
259
|
inbox: () =>
|
|
185
260
|
inboxCommand(
|
|
186
261
|
argv.slice(1),
|
|
187
262
|
cwd,
|
|
188
263
|
{ stdout, stderr },
|
|
189
|
-
{ home:
|
|
264
|
+
{ home: environment.home, ...(environment.signal ? { signal: environment.signal } : {}) },
|
|
190
265
|
),
|
|
191
266
|
reply: () => holder(),
|
|
192
267
|
send: () => holder(),
|
|
@@ -196,12 +271,7 @@ export async function runOperatorCli(
|
|
|
196
271
|
stderr,
|
|
197
272
|
read:
|
|
198
273
|
environment.readAllowance ??
|
|
199
|
-
allowanceReader(
|
|
200
|
-
environment.environment ?? process.env,
|
|
201
|
-
undefined,
|
|
202
|
-
environment.signal,
|
|
203
|
-
environment.home ?? homedir(),
|
|
204
|
-
),
|
|
274
|
+
allowanceReader(environment.environment, undefined, environment.signal, environment.home),
|
|
205
275
|
now,
|
|
206
276
|
}),
|
|
207
277
|
} satisfies Record<CommandName, () => Promise<number>>;
|
|
@@ -216,14 +286,56 @@ export async function runOperatorCli(
|
|
|
216
286
|
return 2;
|
|
217
287
|
}
|
|
218
288
|
stdout(describeVersion());
|
|
289
|
+
// Never handed over: what the project here would run on is said beside it.
|
|
290
|
+
const project = installedEngine(cwd);
|
|
291
|
+
if (project) stdout(`this project's agentswf ${project.version}, in ${project.dir}`);
|
|
219
292
|
return 0;
|
|
220
293
|
}
|
|
221
294
|
if (word === "help" || word === "-h" || word === "--help") return help(rest, stdout, stderr);
|
|
222
|
-
if (isCommand(word))
|
|
295
|
+
if (isCommand(word)) {
|
|
296
|
+
if (asksHelp(rest)) {
|
|
297
|
+
stdout(helpOf(word));
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
const v = JSON_VERSION[word as keyof typeof JSON_VERSION];
|
|
301
|
+
if (v === undefined || !asksJson(rest)) return handlers[word]();
|
|
302
|
+
// What a command didn't foresee still ends its stdout with a document, unless it printed one.
|
|
303
|
+
try {
|
|
304
|
+
return await handlers[word]();
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (!printed) printRefusal({ stdout, stderr }, { json: true, v }, error, "internal-error");
|
|
307
|
+
// The stack is the only clue to a failure no command foresaw.
|
|
308
|
+
if (error instanceof Error && error.stack) stderr(error.stack);
|
|
309
|
+
return 1;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
223
312
|
stderr(unknownCommand(word));
|
|
224
313
|
return 2;
|
|
225
314
|
}
|
|
226
315
|
|
|
316
|
+
/** Each `--json` command's `"v"`, which a refusal carries: `awf runs`'s list's for `awf runs`. */
|
|
317
|
+
const JSON_VERSION = {
|
|
318
|
+
run: RUN_V,
|
|
319
|
+
emit: EMIT_V,
|
|
320
|
+
tick: TICK_V,
|
|
321
|
+
runs: RUNS_V,
|
|
322
|
+
logs: LOGS_V,
|
|
323
|
+
inbox: INBOX_V,
|
|
324
|
+
reply: HOLDER_V,
|
|
325
|
+
send: HOLDER_V,
|
|
326
|
+
allowance: ALLOWANCE_V,
|
|
327
|
+
open: OPEN_V,
|
|
328
|
+
} as const;
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* One rule for every command: `-h` or `--help` anywhere before `--` prints its help. A word that
|
|
332
|
+
* must be either, a message's text say, goes after `--`.
|
|
333
|
+
*/
|
|
334
|
+
function asksHelp(words: readonly string[]): boolean {
|
|
335
|
+
const end = words.indexOf("--");
|
|
336
|
+
return (end < 0 ? words : words.slice(0, end)).some((word) => word === "-h" || word === "--help");
|
|
337
|
+
}
|
|
338
|
+
|
|
227
339
|
/** `awf help`, `awf help {command}` and `awf help reference`; `-h` and `--help` are `help`. */
|
|
228
340
|
function help(
|
|
229
341
|
words: readonly string[],
|
|
@@ -248,63 +360,57 @@ function help(
|
|
|
248
360
|
return 0;
|
|
249
361
|
}
|
|
250
362
|
|
|
251
|
-
/** `awf run`: parses it, then starts or continues the run, here or in a tab. */
|
|
252
363
|
/**
|
|
253
364
|
* `awf run --id` for a workflow with events: the run is made with no event and started detached, as
|
|
254
365
|
* an event starts one, so it runs to its first wait and parks; one already there is only woken.
|
|
255
366
|
*/
|
|
256
367
|
async function openRun(
|
|
257
|
-
{ loaded, id, argv }:
|
|
258
|
-
environment:
|
|
368
|
+
{ loaded, id, argv, panes }: OpensEventRun,
|
|
369
|
+
environment: Resolved,
|
|
259
370
|
{ stdout, stderr }: Output,
|
|
260
371
|
): Promise<number> {
|
|
261
372
|
try {
|
|
262
|
-
const opened = await openEventRun(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
373
|
+
const opened = await openEventRun(
|
|
374
|
+
loaded.workflow as EventWorkflow,
|
|
375
|
+
id,
|
|
376
|
+
argv,
|
|
377
|
+
{
|
|
378
|
+
home: environment.home,
|
|
379
|
+
environment: environment.environment,
|
|
380
|
+
...(environment.launch ? { launch: environment.launch } : {}),
|
|
381
|
+
},
|
|
382
|
+
panes,
|
|
270
383
|
);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
return 1;
|
|
384
|
+
stdout(`${opened.run}${opened.created ? "" : " was there"}${didOf(opened)}`);
|
|
385
|
+
return launchExit(opened, { stdout, stderr });
|
|
274
386
|
} catch (error) {
|
|
275
|
-
|
|
387
|
+
printRefusal({ stdout, stderr }, NO_JSON, error, readFailureCode(error));
|
|
276
388
|
return error instanceof EmitError ? 2 : 1;
|
|
277
389
|
}
|
|
278
390
|
}
|
|
279
391
|
|
|
392
|
+
/** `awf run`: parses it, then starts or continues the run, here or in a tab. */
|
|
280
393
|
async function runCommand(
|
|
281
394
|
argv: readonly string[],
|
|
282
|
-
environment:
|
|
395
|
+
environment: Resolved,
|
|
283
396
|
output: Output,
|
|
284
397
|
): Promise<number> {
|
|
285
|
-
const { stdout, stderr, notice,
|
|
286
|
-
let parsed: RunCommand
|
|
398
|
+
const { stdout, stderr, notice, now } = output;
|
|
399
|
+
let parsed: RunCommand;
|
|
287
400
|
try {
|
|
288
401
|
parsed = parseCommand(argv, environment.cwd ?? process.cwd());
|
|
289
402
|
} catch (error) {
|
|
290
|
-
stderr
|
|
403
|
+
printRefusal({ stdout, stderr }, { json: asksJson(argv), v: RUN_V }, error, "usage", usage);
|
|
291
404
|
return 2;
|
|
292
405
|
}
|
|
293
|
-
if (parsed === "help") {
|
|
294
|
-
stdout(usage);
|
|
295
|
-
return 0;
|
|
296
|
-
}
|
|
297
406
|
let command: RunCommand = parsed;
|
|
407
|
+
// With --json, what makes no run is a refusal, as its sentence on stderr says.
|
|
408
|
+
const refused = (code: RefusalCode, error: string) =>
|
|
409
|
+
printRefusal({ stdout, stderr }, { json: command.json, v: RUN_V }, error, code);
|
|
298
410
|
const meant = await runNamed(command);
|
|
299
411
|
if (meant) {
|
|
300
|
-
const look = shellLine([
|
|
301
|
-
|
|
302
|
-
"runs",
|
|
303
|
-
meant,
|
|
304
|
-
...(command.cwdGiven ? ["--cwd", command.cwd] : []),
|
|
305
|
-
...(command.runRootGiven ? ["--run-root", command.runRoot] : []),
|
|
306
|
-
]);
|
|
307
|
-
stderr(`awf: ${command.workflowFile} is not a workflow file; did you mean: ${look}`);
|
|
412
|
+
const look = shellLine(["awf", "runs", meant, ...placeFlags(command)]);
|
|
413
|
+
refused("file", `${command.workflowFile} is not a workflow file; did you mean: ${look}`);
|
|
308
414
|
return 2;
|
|
309
415
|
}
|
|
310
416
|
if (command.here) return startHere(argv, command, environment, stdout, stderr, notice);
|
|
@@ -314,19 +420,20 @@ async function runCommand(
|
|
|
314
420
|
if (command.session !== undefined) await showOwnTab(environment);
|
|
315
421
|
return exitCode;
|
|
316
422
|
};
|
|
317
|
-
const home = environment
|
|
423
|
+
const { home } = environment;
|
|
318
424
|
const { sandboxes, callers } = machinePaths(home);
|
|
319
425
|
const ready = await loadAndPrepare(command, now(), home);
|
|
320
426
|
if ("refused" in ready) {
|
|
321
|
-
|
|
427
|
+
refused(ready.code, ready.refused);
|
|
322
428
|
return unstarted(2);
|
|
323
429
|
}
|
|
324
430
|
if ("opens" in ready) return openRun(ready.opens, environment, output);
|
|
325
431
|
// After loading: an event run's root is its project's, which this checks too.
|
|
326
432
|
command = ready.command;
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
433
|
+
if (relative(command.runRoot, sandboxes) === "" || isInside(command.runRoot, sandboxes)) {
|
|
434
|
+
refused(
|
|
435
|
+
"usage",
|
|
436
|
+
`--run-root ${command.runRoot} holds ${sandboxes}, which every sandbox must reach and none may reach the run root`,
|
|
330
437
|
);
|
|
331
438
|
return unstarted(2);
|
|
332
439
|
}
|
|
@@ -335,28 +442,24 @@ async function runCommand(
|
|
|
335
442
|
// Loading the workflow imports operator-supplied code before anything listens to the signal, so
|
|
336
443
|
// a Ctrl-C in it would otherwise be swallowed and have to be pressed again.
|
|
337
444
|
if (environment.signal?.aborted) {
|
|
445
|
+
if (command.json) stdout(refusalLine(RUN_V, "not-started", CANCELLED));
|
|
338
446
|
return unstarted(cancelledBeforeStart(environment.signal, stderr));
|
|
339
447
|
}
|
|
340
448
|
let calling: { caller: Caller; release: () => void } | undefined;
|
|
341
449
|
if (command.session !== undefined) {
|
|
342
450
|
const taken = await takeCaller(command.session, callers, environment);
|
|
343
451
|
if (typeof taken === "string") {
|
|
344
|
-
|
|
452
|
+
refused("not-started", `--session: ${taken}`);
|
|
345
453
|
return 1;
|
|
346
454
|
}
|
|
347
455
|
calling = taken;
|
|
348
456
|
if (environment.signal?.aborted) {
|
|
349
457
|
calling.release();
|
|
458
|
+
if (command.json) stdout(refusalLine(RUN_V, "not-started", CANCELLED));
|
|
350
459
|
return cancelledBeforeStart(environment.signal, stderr);
|
|
351
460
|
}
|
|
352
461
|
}
|
|
353
|
-
return runAttempt(command, ready, calling, environment,
|
|
354
|
-
stdout,
|
|
355
|
-
stderr,
|
|
356
|
-
notice,
|
|
357
|
-
terminal,
|
|
358
|
-
now,
|
|
359
|
-
});
|
|
462
|
+
return runAttempt(command, ready, calling, environment, output);
|
|
360
463
|
}
|
|
361
464
|
|
|
362
465
|
/** The run `awf run {ref}` names where `{ref}` is no file: one meant for `awf runs`. */
|
|
@@ -374,11 +477,11 @@ async function runAttempt(
|
|
|
374
477
|
command: RunCommand,
|
|
375
478
|
{ loaded, prepared }: { loaded: LoadedWorkflow; prepared: PreparedRun },
|
|
376
479
|
calling: { caller: Caller; release: () => void } | undefined,
|
|
377
|
-
environment:
|
|
378
|
-
home: string,
|
|
480
|
+
environment: Resolved,
|
|
379
481
|
output: Output,
|
|
380
482
|
): Promise<number> {
|
|
381
483
|
const { stdout, stderr, notice, terminal, now } = output;
|
|
484
|
+
const { home } = environment;
|
|
382
485
|
const { workflow } = loaded;
|
|
383
486
|
const { meta } = workflow;
|
|
384
487
|
/** The run, once it is claimed. */
|
|
@@ -398,21 +501,21 @@ async function runAttempt(
|
|
|
398
501
|
if (failed) stderr(`awf: the calling session was not told the run ended: ${failed}`);
|
|
399
502
|
};
|
|
400
503
|
|
|
401
|
-
const refuse = async (why: string) => {
|
|
402
|
-
stderr
|
|
504
|
+
const refuse = async (why: string, code: RefusalCode) => {
|
|
505
|
+
printRefusal({ stdout, stderr }, { json: command.json, v: RUN_V }, why, code);
|
|
403
506
|
await handOver(`did not start: ${why}`);
|
|
404
507
|
return 2;
|
|
405
508
|
};
|
|
406
509
|
// The operator's, never where the workflow's agents work.
|
|
407
|
-
const coordinatorRoot = coordinatorDir(home, environment.environment
|
|
510
|
+
const coordinatorRoot = coordinatorDir(home, environment.environment);
|
|
408
511
|
try {
|
|
409
512
|
await prepareCoordinatorDir(
|
|
410
513
|
coordinatorRoot,
|
|
411
514
|
command.cwd,
|
|
412
|
-
Boolean(
|
|
515
|
+
Boolean(environment.environment.AWF_COORDINATOR_DIR),
|
|
413
516
|
);
|
|
414
517
|
} catch (error) {
|
|
415
|
-
return refuse(messageOf(error));
|
|
518
|
+
return refuse(messageOf(error), "internal-error");
|
|
416
519
|
}
|
|
417
520
|
try {
|
|
418
521
|
run =
|
|
@@ -423,19 +526,21 @@ async function runAttempt(
|
|
|
423
526
|
argv: command.workflowArgs,
|
|
424
527
|
cwd: command.cwd,
|
|
425
528
|
sandbox: (command.sandbox ?? null) as JsonValue,
|
|
529
|
+
...(command.panes ? { panes: command.panes } : {}),
|
|
426
530
|
}));
|
|
427
531
|
} catch (error) {
|
|
428
532
|
return refuse(
|
|
429
533
|
`${error instanceof RunRefused ? "" : `${command.runRoot}: `}${messageOf(error)}`,
|
|
534
|
+
readFailureCode(error),
|
|
430
535
|
);
|
|
431
536
|
}
|
|
432
537
|
let attempt: Attempt;
|
|
433
538
|
try {
|
|
434
|
-
attempt = await claimNext(run, loaded, command, notice, now());
|
|
539
|
+
attempt = await claimNext(run, loaded, command, notice, now(), environment.launchId);
|
|
435
540
|
} catch (error) {
|
|
436
541
|
// A run this call created and never ran is not left to hold its id.
|
|
437
542
|
if (!prepared.continued) await discardRun(run).catch(() => undefined);
|
|
438
|
-
return refuse(messageOf(error));
|
|
543
|
+
return refuse(messageOf(error), readFailureCode(error));
|
|
439
544
|
}
|
|
440
545
|
const { id, cwd } = run.record;
|
|
441
546
|
const locked = <T>(work: () => Promise<T>) =>
|
|
@@ -449,7 +554,7 @@ async function runAttempt(
|
|
|
449
554
|
if (run.record.events && previous && previous.ended === undefined)
|
|
450
555
|
await stopCrashed(previous, workspaceLabel(meta.name, run.record.id, previous.attempt), {
|
|
451
556
|
home,
|
|
452
|
-
environment: environment.environment
|
|
557
|
+
environment: environment.environment,
|
|
453
558
|
}).catch((error: unknown) =>
|
|
454
559
|
notice(
|
|
455
560
|
`awf: what attempt ${previous.attempt} left running was not all stopped: ${messageOf(error)}`,
|
|
@@ -460,14 +565,26 @@ async function runAttempt(
|
|
|
460
565
|
attempt,
|
|
461
566
|
loaded,
|
|
462
567
|
stderr,
|
|
568
|
+
now,
|
|
463
569
|
...(!run.record.events ? {} : { locked }),
|
|
464
570
|
};
|
|
465
|
-
const close = (end: AttemptEnd, kept: Kept, cleanupAlso?: string) =>
|
|
466
|
-
|
|
571
|
+
const close = async (end: AttemptEnd, kept: Kept, cleanupAlso?: string) => {
|
|
572
|
+
const earlier = prepared.continued?.attempts ?? [];
|
|
573
|
+
// Priced now, from what each earlier attempt's operations recorded, its claim's settling included.
|
|
574
|
+
const spentBefore =
|
|
575
|
+
n > 1
|
|
576
|
+
? await readSpent(run.dir, earlier).then(
|
|
577
|
+
(spent) => earlierSpent(spent, earlier, n),
|
|
578
|
+
// Still a total, with every earlier attempt's cost unknown.
|
|
579
|
+
() => unreadEarlier(earlier, n),
|
|
580
|
+
)
|
|
581
|
+
: undefined;
|
|
582
|
+
return printEnding(end, kept, {
|
|
467
583
|
workflow,
|
|
468
584
|
run: { id, dir: run.dir },
|
|
469
585
|
n,
|
|
470
|
-
earlier
|
|
586
|
+
earlier,
|
|
587
|
+
...(spentBefore ? { spentBefore } : {}),
|
|
471
588
|
json: command.json,
|
|
472
589
|
fromStage: command.fromStage !== undefined,
|
|
473
590
|
now: now(),
|
|
@@ -479,6 +596,7 @@ async function runAttempt(
|
|
|
479
596
|
stderr,
|
|
480
597
|
notice,
|
|
481
598
|
});
|
|
599
|
+
};
|
|
482
600
|
// Timed from here: finding the calling session can take minutes the run itself never had.
|
|
483
601
|
const startedAt = now();
|
|
484
602
|
// Every way out from here writes the attempt's ending, before the hand-back: a session told the
|
|
@@ -497,7 +615,12 @@ async function runAttempt(
|
|
|
497
615
|
const said = cancelled
|
|
498
616
|
? "run cancelled before it started"
|
|
499
617
|
: `${end.ending.reason}; the run did not start`;
|
|
500
|
-
|
|
618
|
+
printRefusal(
|
|
619
|
+
{ stdout, stderr },
|
|
620
|
+
{ json: command.json, v: RUN_V },
|
|
621
|
+
`${said}, and nothing of it is kept`,
|
|
622
|
+
"not-started",
|
|
623
|
+
);
|
|
501
624
|
await handOver(`did not start: ${cancelled ? "cancelled" : end.ending.reason}`);
|
|
502
625
|
return end.exitCode;
|
|
503
626
|
}
|
|
@@ -505,7 +628,7 @@ async function runAttempt(
|
|
|
505
628
|
await handOver(toldOf(end, kept));
|
|
506
629
|
return close(end, kept, cleanupAlso);
|
|
507
630
|
};
|
|
508
|
-
// Relied on: the session is told only from a
|
|
631
|
+
// Relied on: the session is told only from a TUI agent's `openRun`, inside `startWorkflow`, by
|
|
509
632
|
// when `placed` says it on the progress.
|
|
510
633
|
let placed: (session: RunSession) => void = () => undefined;
|
|
511
634
|
let installed: OperatorRuntimeInstallation;
|
|
@@ -543,7 +666,7 @@ async function runAttempt(
|
|
|
543
666
|
placed = (session) => {
|
|
544
667
|
progress.placedIn(session.name);
|
|
545
668
|
const label = workspaceLabel(meta.name, id, n);
|
|
546
|
-
const insideHerdr =
|
|
669
|
+
const insideHerdr = environment.environment.HERDR_ENV === "1";
|
|
547
670
|
for (const line of describeRunSession(session, label, insideHerdr)) progress.log(line);
|
|
548
671
|
};
|
|
549
672
|
// Interactive: a coordinator's questions are asked here, unless the output is for a program.
|
|
@@ -577,10 +700,12 @@ async function runAttempt(
|
|
|
577
700
|
run: {
|
|
578
701
|
dir: run.dir,
|
|
579
702
|
id,
|
|
580
|
-
|
|
703
|
+
lines: attempt.lines,
|
|
704
|
+
ops: attempt.ops,
|
|
581
705
|
label: workspaceLabel(meta.name, id, n),
|
|
582
706
|
...(command.fromStage === undefined ? {} : { fromStage: command.fromStage }),
|
|
583
707
|
...(command.values === undefined ? {} : { values: command.values.stages }),
|
|
708
|
+
...(run.record.panes ? { panes: run.record.panes } : {}),
|
|
584
709
|
...(!events
|
|
585
710
|
? {}
|
|
586
711
|
: {
|
|
@@ -603,7 +728,7 @@ async function runAttempt(
|
|
|
603
728
|
await emitEvent(target, event, {
|
|
604
729
|
from,
|
|
605
730
|
home,
|
|
606
|
-
environment: environment.environment
|
|
731
|
+
environment: environment.environment,
|
|
607
732
|
...(environment.launch ? { launch: environment.launch } : {}),
|
|
608
733
|
})
|
|
609
734
|
).outcome,
|
|
@@ -684,8 +809,10 @@ export function stderrLines(
|
|
|
684
809
|
};
|
|
685
810
|
}
|
|
686
811
|
|
|
812
|
+
const CANCELLED = "run cancelled before it started";
|
|
813
|
+
|
|
687
814
|
function cancelledBeforeStart(signal: AbortSignal, stderr: (line: string) => void): number {
|
|
688
|
-
stderr(
|
|
815
|
+
stderr(`awf: ${CANCELLED}`);
|
|
689
816
|
return signalExitCode(signal.reason);
|
|
690
817
|
}
|
|
691
818
|
|
|
@@ -724,16 +851,19 @@ const EXIT_GRACE_MS = 2_000;
|
|
|
724
851
|
* Cancels `controller` at the first signal that asks the run to stop, and stops the process at
|
|
725
852
|
* once at a second the operator sends, without the run's cleanup. Ctrl-C reaches the whole
|
|
726
853
|
* process group and `bun awf` forwards it once more, so a repeat within `REPEAT_MS` is ignored:
|
|
727
|
-
* taken as the second, it would kill the run mid-cleanup and leave its sandboxes behind.
|
|
728
|
-
*
|
|
854
|
+
* taken as the second, it would kill the run mid-cleanup and leave its sandboxes behind. While a
|
|
855
|
+
* person's TUI has the terminal, Ctrl-C is the TUI's: ending awf then would leave it writing a
|
|
856
|
+
* session nothing holds. Returns what removes the handlers.
|
|
729
857
|
*/
|
|
730
858
|
export function cancelOnSignals(
|
|
731
859
|
controller: AbortController,
|
|
732
860
|
exit: (code: number) => void = (code) => process.exit(code),
|
|
733
861
|
now: () => number = Date.now,
|
|
862
|
+
attached: () => boolean = terminalAttached,
|
|
734
863
|
): () => void {
|
|
735
864
|
let first: number | undefined;
|
|
736
865
|
const cancel = (signal: NodeJS.Signals) => {
|
|
866
|
+
if (signal === "SIGINT" && attached()) return;
|
|
737
867
|
if (first === undefined) {
|
|
738
868
|
first = now();
|
|
739
869
|
controller.abort(signal);
|