@agentswf/engine 0.1.0-beta.2 → 0.1.0-beta.4
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/package.json +5 -6
- package/src/agent-cli/cli.ts +571 -0
- package/src/agent-cli/client.ts +115 -0
- package/src/agent-launcher.ts +15 -25
- package/src/allowance-command.ts +31 -21
- package/src/attempt-ending.ts +23 -5
- package/src/attempt-output.ts +48 -18
- package/src/attempt-view.ts +25 -15
- package/src/commands.ts +176 -0
- package/src/control-plane.ts +73 -22
- package/src/coordinator-port.ts +29 -0
- package/src/coordinator-prompt.ts +237 -0
- package/src/coordinator-socket.ts +236 -0
- package/src/deadlines.ts +47 -10
- package/src/decisions/directory.ts +8 -6
- package/src/duration.ts +29 -0
- package/src/emit-command.ts +119 -0
- package/src/emit.ts +246 -0
- package/src/files.ts +11 -1
- package/src/help/allowance.txt +13 -0
- package/src/help/emit.txt +13 -0
- package/src/help/inbox.txt +12 -0
- package/src/help/init.txt +12 -0
- package/src/help/logs.txt +13 -0
- package/src/help/reply.txt +18 -0
- package/src/help/run.txt +44 -0
- package/src/help/runs.txt +11 -0
- package/src/help/test.txt +9 -0
- package/src/help/tick.txt +12 -0
- package/src/here.ts +9 -3
- package/src/hold-run-lock.fixture.ts +8 -0
- package/src/holder-command.ts +279 -0
- package/src/inbox-command.ts +715 -0
- package/src/init-command.ts +29 -11
- package/src/init-template/AGENTS.md.template +4 -4
- package/src/init-template/hello/workflow.test.ts.template +2 -2
- package/src/init-template/hello/workflow.ts.template +16 -20
- package/src/keep-alive.ts +40 -0
- package/src/logs-command.ts +106 -35
- package/src/machine.ts +12 -1
- package/src/mailbox.ts +990 -0
- package/src/message-operation.ts +26 -3
- package/src/messaging/limits.ts +1 -1
- package/src/messaging/messaging.ts +478 -85
- package/src/messaging/routes.ts +11 -4
- package/src/messaging/text.ts +25 -22
- package/src/operation-liveness.ts +55 -9
- package/src/operator-cli.ts +290 -119
- package/src/operator-runtime.ts +8 -8
- package/src/progress-view.ts +49 -7
- package/src/projects.ts +57 -0
- package/src/result-slots.ts +43 -19
- package/src/result-validation.ts +18 -27
- package/src/run-command.ts +29 -66
- package/src/run-commands.ts +13 -2
- package/src/run-lock.ts +123 -0
- package/src/run-prepare.ts +94 -22
- package/src/run-state.ts +125 -6
- package/src/run-usage.ts +2 -2
- package/src/run-view.ts +0 -9
- package/src/runs-command.ts +43 -25
- package/src/runs.ts +142 -23
- package/src/sandboxes.ts +11 -4
- package/src/simple-input.ts +268 -0
- package/src/socket-exchange.ts +26 -0
- package/src/sources.ts +70 -0
- package/src/stage-ledger.ts +89 -14
- package/src/stage-plan.ts +23 -0
- package/src/stop-crashed.ts +39 -0
- package/src/test-command.ts +34 -11
- package/src/testing.ts +72 -4
- package/src/tick-command.ts +76 -0
- package/src/tools.ts +645 -0
- package/src/wake.ts +264 -0
- package/src/workflow-loader.ts +32 -22
- package/src/workflow-runner.ts +1277 -97
- package/src/workflow-testing/coordinator.typecheck.ts +54 -0
- package/src/workflow-testing/coordinators.ts +137 -0
- package/src/workflow-testing/host.ts +67 -7
- package/src/workflow-testing/index.ts +488 -34
- package/src/workflow-testing/script.ts +74 -4
- package/src/workflow-testing/workflow-testing.typecheck.ts +19 -2
package/src/operator-cli.ts
CHANGED
|
@@ -7,13 +7,7 @@ import type { JsonObject, JsonValue } from "@agentswf/contract/workflow";
|
|
|
7
7
|
import { type Harness, handBack, type RunProcess } from "@agentswf/harness";
|
|
8
8
|
import manifest from "../package.json" with { type: "json" };
|
|
9
9
|
import { stageFigures } from "./accounting/format";
|
|
10
|
-
import {
|
|
11
|
-
type AllowanceCommand,
|
|
12
|
-
allowanceUsage,
|
|
13
|
-
describeReport,
|
|
14
|
-
parseAllowanceCommand,
|
|
15
|
-
readReport,
|
|
16
|
-
} from "./allowance-command";
|
|
10
|
+
import { allowanceCommand } from "./allowance-command";
|
|
17
11
|
import {
|
|
18
12
|
type AttemptEnd,
|
|
19
13
|
cleanupFailed,
|
|
@@ -25,11 +19,23 @@ import {
|
|
|
25
19
|
} from "./attempt-ending";
|
|
26
20
|
import { type Kept, keepRecords } from "./attempt-output";
|
|
27
21
|
import { continueCommand, printEnding, toldOf } from "./attempt-view";
|
|
22
|
+
import { type CommandName, helpOf, index, isCommand, reference, unknownCommand } from "./commands";
|
|
23
|
+
import {
|
|
24
|
+
type LineReader,
|
|
25
|
+
promptCoordinator,
|
|
26
|
+
replyHints,
|
|
27
|
+
terminalReader,
|
|
28
|
+
} from "./coordinator-prompt";
|
|
29
|
+
import { coordinatorSocket, prepareCoordinatorDir } from "./coordinator-socket";
|
|
30
|
+
import { EmitError, emitEvent, openEventRun } from "./emit";
|
|
31
|
+
import { emitCommand } from "./emit-command";
|
|
28
32
|
import { messageOf } from "./errors";
|
|
29
33
|
import { type Caller, showOwnTab, startHere, takeCaller } from "./here";
|
|
30
|
-
import {
|
|
34
|
+
import { holderCommand } from "./holder-command";
|
|
35
|
+
import { inboxCommand } from "./inbox-command";
|
|
36
|
+
import { initCommand } from "./init-command";
|
|
31
37
|
import { logsCommand } from "./logs-command";
|
|
32
|
-
import { machinePaths, sandboxesDirOf } from "./machine";
|
|
38
|
+
import { coordinatorDir, machinePaths, sandboxesDirOf } from "./machine";
|
|
33
39
|
import {
|
|
34
40
|
allowanceReader,
|
|
35
41
|
herdrConfig,
|
|
@@ -39,13 +45,17 @@ import {
|
|
|
39
45
|
} from "./operator-runtime";
|
|
40
46
|
import { ANSI, describeRunSession, type Terminal, watchProgress } from "./progress-view";
|
|
41
47
|
import { parseCommand, type RunCommand, usage } from "./run-command";
|
|
42
|
-
import { shellLine, startedLine } from "./run-commands";
|
|
48
|
+
import { placeFlags, shellLine, startedLine } from "./run-commands";
|
|
49
|
+
import { runLockFile, withRunLock } from "./run-lock";
|
|
43
50
|
import { claimNext, loadAndPrepare, type PreparedRun, workspaceLabel } from "./run-prepare";
|
|
44
51
|
import { findRun } from "./run-state";
|
|
45
|
-
import { type Attempt, createRun, discardRun, type Run, RunRefused } from "./runs";
|
|
52
|
+
import { type Attempt, createRun, discardRun, endAttempt, type Run, RunRefused } from "./runs";
|
|
46
53
|
import { runsCommand } from "./runs-command";
|
|
47
|
-
import {
|
|
48
|
-
import
|
|
54
|
+
import { stopCrashed } from "./stop-crashed";
|
|
55
|
+
import { testCommand } from "./test-command";
|
|
56
|
+
import { tickCommand } from "./tick-command";
|
|
57
|
+
import type { Launch } from "./wake";
|
|
58
|
+
import type { EventWorkflow, LoadedWorkflow } from "./workflow-loader";
|
|
49
59
|
import {
|
|
50
60
|
type RunWorkflowOptions,
|
|
51
61
|
startWorkflow,
|
|
@@ -60,7 +70,7 @@ export type OperatorEnvironment = {
|
|
|
60
70
|
stdout?: (text: string) => void;
|
|
61
71
|
stderr?: (text: string) => void;
|
|
62
72
|
installRuntime?: (
|
|
63
|
-
timeoutMilliseconds: number,
|
|
73
|
+
timeoutMilliseconds: number | undefined,
|
|
64
74
|
options: {
|
|
65
75
|
watchSandboxes: boolean;
|
|
66
76
|
caller?: Caller;
|
|
@@ -84,11 +94,22 @@ export type OperatorEnvironment = {
|
|
|
84
94
|
install?: (dir: string) => Promise<number>;
|
|
85
95
|
/** How `awf allowance` reads a harness's plan. */
|
|
86
96
|
readAllowance?: (harness: Harness) => Promise<HarnessAllowance>;
|
|
97
|
+
/** Starts or continues an event run's attempt for `awf emit`; absent, a detached `awf run`. */
|
|
98
|
+
launch?: Launch;
|
|
87
99
|
/**
|
|
88
100
|
* Where agents may `--await-reply`, past the engine's list: for the live checks that decide
|
|
89
101
|
* what goes on it (story 029's M2 gate).
|
|
90
102
|
*/
|
|
91
103
|
awaitReply?: RunWorkflowOptions["awaitReply"];
|
|
104
|
+
/** Answers for the run's coordinators in place of a person: for the live checks of story 032. */
|
|
105
|
+
coordinators?: RunWorkflowOptions["coordinators"];
|
|
106
|
+
/**
|
|
107
|
+
* Given, stdin is a terminal and these are the lines typed at it: `awf run` asks a coordinator's
|
|
108
|
+
* questions there. Absent, it is so when stdin and stderr are both terminals.
|
|
109
|
+
*/
|
|
110
|
+
lines?: LineReader;
|
|
111
|
+
/** What `awf reply` and `awf send` read their text from without an argument; stdin otherwise. */
|
|
112
|
+
stdin?: () => Promise<string>;
|
|
92
113
|
};
|
|
93
114
|
|
|
94
115
|
type Output = {
|
|
@@ -124,28 +145,156 @@ export async function runOperatorCli(
|
|
|
124
145
|
stderr(`awf: ${tooOld}`);
|
|
125
146
|
return 1;
|
|
126
147
|
}
|
|
127
|
-
|
|
148
|
+
const cwd = environment.cwd ?? process.cwd();
|
|
149
|
+
const holder = () =>
|
|
150
|
+
holderCommand(argv, cwd, {
|
|
151
|
+
stdout,
|
|
152
|
+
stderr,
|
|
153
|
+
stdin: environment.stdin ?? (() => new Response(Bun.stdin.stream()).text()),
|
|
154
|
+
...(environment.stdin ? {} : { stdinIsTerminal: process.stdin.isTTY === true }),
|
|
155
|
+
...(environment.environment ? { environment: environment.environment } : {}),
|
|
156
|
+
...(environment.home ? { home: environment.home } : {}),
|
|
157
|
+
});
|
|
158
|
+
const wakes = {
|
|
159
|
+
home: environment.home ?? homedir(),
|
|
160
|
+
environment: environment.environment ?? process.env,
|
|
161
|
+
...(environment.launch ? { launch: environment.launch } : {}),
|
|
162
|
+
};
|
|
163
|
+
const handlers = {
|
|
164
|
+
run: () => runCommand(argv, environment, { stdout, stderr, notice, terminal, now }),
|
|
165
|
+
test: () => {
|
|
166
|
+
const captured = environment.stdout || environment.stderr ? { stdout, stderr } : undefined;
|
|
167
|
+
return testCommand(argv.slice(1), cwd, {
|
|
168
|
+
stdout,
|
|
169
|
+
stderr,
|
|
170
|
+
...(captured ? { output: captured } : {}),
|
|
171
|
+
...(environment.signal ? { signal: environment.signal } : {}),
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
init: () =>
|
|
175
|
+
initCommand(argv.slice(1), cwd, {
|
|
176
|
+
stdout,
|
|
177
|
+
stderr,
|
|
178
|
+
...(environment.install ? { install: environment.install } : {}),
|
|
179
|
+
}),
|
|
180
|
+
emit: () => emitCommand(argv.slice(1), cwd, { stdout, stderr }, wakes),
|
|
181
|
+
tick: () => tickCommand(argv.slice(1), cwd, { stdout, stderr }, wakes),
|
|
182
|
+
runs: () => runsCommand(argv.slice(1), cwd, { stdout, stderr }, Date.now(), wakes.home),
|
|
183
|
+
logs: () => logsCommand(argv.slice(1), cwd, { stdout, stderr }, undefined, wakes.home),
|
|
184
|
+
inbox: () =>
|
|
185
|
+
inboxCommand(
|
|
186
|
+
argv.slice(1),
|
|
187
|
+
cwd,
|
|
188
|
+
{ stdout, stderr },
|
|
189
|
+
{ home: wakes.home, ...(environment.signal ? { signal: environment.signal } : {}) },
|
|
190
|
+
),
|
|
191
|
+
reply: () => holder(),
|
|
192
|
+
send: () => holder(),
|
|
193
|
+
allowance: () =>
|
|
194
|
+
allowanceCommand(argv.slice(1), {
|
|
195
|
+
stdout,
|
|
196
|
+
stderr,
|
|
197
|
+
read:
|
|
198
|
+
environment.readAllowance ??
|
|
199
|
+
allowanceReader(
|
|
200
|
+
environment.environment ?? process.env,
|
|
201
|
+
undefined,
|
|
202
|
+
environment.signal,
|
|
203
|
+
environment.home ?? homedir(),
|
|
204
|
+
),
|
|
205
|
+
now,
|
|
206
|
+
}),
|
|
207
|
+
} satisfies Record<CommandName, () => Promise<number>>;
|
|
208
|
+
const [word, ...rest] = argv;
|
|
209
|
+
if (word === undefined) {
|
|
210
|
+
stdout(index());
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
if (word === "--version") {
|
|
214
|
+
if (rest.length > 0) {
|
|
215
|
+
stderr("awf: --version takes nothing after it; awf -h lists the commands");
|
|
216
|
+
return 2;
|
|
217
|
+
}
|
|
128
218
|
stdout(describeVersion());
|
|
129
219
|
return 0;
|
|
130
220
|
}
|
|
131
|
-
if (
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
221
|
+
if (word === "help" || word === "-h" || word === "--help") return help(rest, stdout, stderr);
|
|
222
|
+
if (isCommand(word)) return handlers[word]();
|
|
223
|
+
stderr(unknownCommand(word));
|
|
224
|
+
return 2;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** `awf help`, `awf help {command}` and `awf help reference`; `-h` and `--help` are `help`. */
|
|
228
|
+
function help(
|
|
229
|
+
words: readonly string[],
|
|
230
|
+
stdout: (text: string) => void,
|
|
231
|
+
stderr: (text: string) => void,
|
|
232
|
+
): number {
|
|
233
|
+
const [word] = words;
|
|
234
|
+
if (words.length > 1) {
|
|
235
|
+
stderr("awf: help takes one command; awf -h lists the commands");
|
|
236
|
+
return 2;
|
|
135
237
|
}
|
|
136
|
-
if (
|
|
137
|
-
|
|
238
|
+
if (word === undefined || word === "help" || word === "-h" || word === "--help") {
|
|
239
|
+
stdout(index());
|
|
240
|
+
} else if (word === "reference") {
|
|
241
|
+
stdout(reference());
|
|
242
|
+
} else if (isCommand(word)) {
|
|
243
|
+
stdout(helpOf(word));
|
|
244
|
+
} else {
|
|
245
|
+
stderr(unknownCommand(word));
|
|
246
|
+
return 2;
|
|
138
247
|
}
|
|
139
|
-
|
|
140
|
-
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** `awf run`: parses it, then starts or continues the run, here or in a tab. */
|
|
252
|
+
/**
|
|
253
|
+
* `awf run --id` for a workflow with events: the run is made with no event and started detached, as
|
|
254
|
+
* an event starts one, so it runs to its first wait and parks; one already there is only woken.
|
|
255
|
+
*/
|
|
256
|
+
async function openRun(
|
|
257
|
+
{ loaded, id, argv }: { loaded: LoadedWorkflow; id: string; argv: readonly string[] },
|
|
258
|
+
environment: OperatorEnvironment,
|
|
259
|
+
{ stdout, stderr }: Output,
|
|
260
|
+
): Promise<number> {
|
|
261
|
+
try {
|
|
262
|
+
const opened = await openEventRun(loaded.workflow as EventWorkflow, id, argv, {
|
|
263
|
+
home: environment.home ?? homedir(),
|
|
264
|
+
environment: environment.environment ?? process.env,
|
|
265
|
+
...(environment.launch ? { launch: environment.launch } : {}),
|
|
266
|
+
});
|
|
267
|
+
const did = opened.did === "none" ? opened.why : opened.did;
|
|
268
|
+
stdout(
|
|
269
|
+
`${opened.run}${opened.created ? "" : " was there"}${did === undefined ? "" : `: ${did}`}`,
|
|
270
|
+
);
|
|
271
|
+
if (opened.why !== "launch-failed") return 0;
|
|
272
|
+
stderr(`awf: the run's attempt did not start: ${opened.error}`);
|
|
273
|
+
return 1;
|
|
274
|
+
} catch (error) {
|
|
275
|
+
stderr(`awf: ${messageOf(error)}`);
|
|
276
|
+
return error instanceof EmitError ? 2 : 1;
|
|
141
277
|
}
|
|
142
|
-
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function runCommand(
|
|
281
|
+
argv: readonly string[],
|
|
282
|
+
environment: OperatorEnvironment,
|
|
283
|
+
output: Output,
|
|
284
|
+
): Promise<number> {
|
|
285
|
+
const { stdout, stderr, notice, terminal, now } = output;
|
|
286
|
+
let parsed: RunCommand | "help";
|
|
143
287
|
try {
|
|
144
|
-
|
|
288
|
+
parsed = parseCommand(argv, environment.cwd ?? process.cwd());
|
|
145
289
|
} catch (error) {
|
|
146
290
|
stderr(`awf: ${messageOf(error)}\n\n${usage}`);
|
|
147
291
|
return 2;
|
|
148
292
|
}
|
|
293
|
+
if (parsed === "help") {
|
|
294
|
+
stdout(usage);
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
let command: RunCommand = parsed;
|
|
149
298
|
const meant = await runNamed(command);
|
|
150
299
|
if (meant) {
|
|
151
300
|
const look = shellLine([
|
|
@@ -167,17 +316,20 @@ export async function runOperatorCli(
|
|
|
167
316
|
};
|
|
168
317
|
const home = environment.home ?? homedir();
|
|
169
318
|
const { sandboxes, callers } = machinePaths(home);
|
|
319
|
+
const ready = await loadAndPrepare(command, now(), home);
|
|
320
|
+
if ("refused" in ready) {
|
|
321
|
+
stderr(`awf: ${ready.refused}`);
|
|
322
|
+
return unstarted(2);
|
|
323
|
+
}
|
|
324
|
+
if ("opens" in ready) return openRun(ready.opens, environment, output);
|
|
325
|
+
// After loading: an event run's root is its project's, which this checks too.
|
|
326
|
+
command = ready.command;
|
|
170
327
|
if (!relative(command.runRoot, sandboxes).startsWith("..")) {
|
|
171
328
|
stderr(
|
|
172
329
|
`awf: --run-root ${command.runRoot} holds ${sandboxes}, which every sandbox must reach and none may reach the run root`,
|
|
173
330
|
);
|
|
174
331
|
return unstarted(2);
|
|
175
332
|
}
|
|
176
|
-
const ready = await loadAndPrepare(command, now());
|
|
177
|
-
if ("refused" in ready) {
|
|
178
|
-
stderr(`awf: ${ready.refused}`);
|
|
179
|
-
return unstarted(2);
|
|
180
|
-
}
|
|
181
333
|
for (const line of ready.prepared.continued?.warnings ?? []) notice(line);
|
|
182
334
|
|
|
183
335
|
// Loading the workflow imports operator-supplied code before anything listens to the signal, so
|
|
@@ -227,8 +379,8 @@ async function runAttempt(
|
|
|
227
379
|
output: Output,
|
|
228
380
|
): Promise<number> {
|
|
229
381
|
const { stdout, stderr, notice, terminal, now } = output;
|
|
230
|
-
const {
|
|
231
|
-
const { meta } =
|
|
382
|
+
const { workflow } = loaded;
|
|
383
|
+
const { meta } = workflow;
|
|
232
384
|
/** The run, once it is claimed. */
|
|
233
385
|
let run: Run | undefined;
|
|
234
386
|
// Once the run has taken the session over, however it ends, the session gets it back.
|
|
@@ -251,6 +403,17 @@ async function runAttempt(
|
|
|
251
403
|
await handOver(`did not start: ${why}`);
|
|
252
404
|
return 2;
|
|
253
405
|
};
|
|
406
|
+
// The operator's, never where the workflow's agents work.
|
|
407
|
+
const coordinatorRoot = coordinatorDir(home, environment.environment ?? process.env);
|
|
408
|
+
try {
|
|
409
|
+
await prepareCoordinatorDir(
|
|
410
|
+
coordinatorRoot,
|
|
411
|
+
command.cwd,
|
|
412
|
+
Boolean((environment.environment ?? process.env).AWF_COORDINATOR_DIR),
|
|
413
|
+
);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
return refuse(messageOf(error));
|
|
416
|
+
}
|
|
254
417
|
try {
|
|
255
418
|
run =
|
|
256
419
|
prepared.continued?.run ??
|
|
@@ -275,13 +438,33 @@ async function runAttempt(
|
|
|
275
438
|
return refuse(messageOf(error));
|
|
276
439
|
}
|
|
277
440
|
const { id, cwd } = run.record;
|
|
441
|
+
const locked = <T>(work: () => Promise<T>) =>
|
|
442
|
+
withRunLock(runLockFile(run.root, meta.name, id), work);
|
|
278
443
|
const sandbox = run.record.sandbox ?? undefined;
|
|
279
444
|
const n = attempt.record.attempt;
|
|
445
|
+
// Claimed first, which `wake` waits for holding the run's lock; only then waited on: two
|
|
446
|
+
// attempts never share an agent's session.
|
|
447
|
+
const previous = prepared.continued?.attempts.at(-1);
|
|
448
|
+
// An event run continued after a crash: what that attempt left working is stopped first (Q17).
|
|
449
|
+
if (run.record.events && previous && previous.ended === undefined)
|
|
450
|
+
await stopCrashed(previous, workspaceLabel(meta.name, run.record.id, previous.attempt), {
|
|
451
|
+
home,
|
|
452
|
+
environment: environment.environment ?? process.env,
|
|
453
|
+
}).catch((error: unknown) =>
|
|
454
|
+
notice(
|
|
455
|
+
`awf: what attempt ${previous.attempt} left running was not all stopped: ${messageOf(error)}`,
|
|
456
|
+
),
|
|
457
|
+
);
|
|
280
458
|
const goOn: GoOn = (stop, entered) => continueCommand(command, id, stop, entered);
|
|
281
|
-
const records = {
|
|
459
|
+
const records = {
|
|
460
|
+
attempt,
|
|
461
|
+
loaded,
|
|
462
|
+
stderr,
|
|
463
|
+
...(!run.record.events ? {} : { locked }),
|
|
464
|
+
};
|
|
282
465
|
const close = (end: AttemptEnd, kept: Kept, cleanupAlso?: string) =>
|
|
283
466
|
printEnding(end, kept, {
|
|
284
|
-
|
|
467
|
+
workflow,
|
|
285
468
|
run: { id, dir: run.dir },
|
|
286
469
|
n,
|
|
287
470
|
earlier: prepared.continued?.attempts ?? [],
|
|
@@ -302,7 +485,10 @@ async function runAttempt(
|
|
|
302
485
|
// run is over may continue it at once. One that never does is interrupted. A new run that never
|
|
303
486
|
// started is not kept at all, so the same command starts it again, unless another attempt has
|
|
304
487
|
// claimed it since: then its records are kept as a continued run's are.
|
|
305
|
-
const deadline =
|
|
488
|
+
const deadline =
|
|
489
|
+
command.timeoutMilliseconds === undefined
|
|
490
|
+
? undefined
|
|
491
|
+
: { unixMilliseconds: startedAt + command.timeoutMilliseconds };
|
|
306
492
|
const endUnstarted = async (error: unknown, cleanupAlso?: string) => {
|
|
307
493
|
const end = endedBeforeStart(error, deadline, { runId: id, startedAt, endedAt: now() }, goOn);
|
|
308
494
|
const cancelled = end.ending.kind === "cancelled";
|
|
@@ -360,9 +546,33 @@ async function runAttempt(
|
|
|
360
546
|
const insideHerdr = (environment.environment ?? process.env).HERDR_ENV === "1";
|
|
361
547
|
for (const line of describeRunSession(session, label, insideHerdr)) progress.log(line);
|
|
362
548
|
};
|
|
549
|
+
// Interactive: a coordinator's questions are asked here, unless the output is for a program.
|
|
550
|
+
const reader =
|
|
551
|
+
terminal && !command.json
|
|
552
|
+
? (environment.lines ?? (process.stdin.isTTY ? terminalReader() : undefined))
|
|
553
|
+
: undefined;
|
|
554
|
+
const prompt = reader && promptCoordinator(reader, progress);
|
|
555
|
+
// Without a prompt, each question says how to answer it, so the run does not look stuck.
|
|
556
|
+
const asked =
|
|
557
|
+
prompt ??
|
|
558
|
+
replyHints(`${meta.name}/${id}`, placeFlags(command), n, (line) => progress.log(line));
|
|
559
|
+
progress.showsMessages((message) => asked.shows(message));
|
|
560
|
+
const { events } = run.record;
|
|
561
|
+
let nudge: (() => void) | undefined;
|
|
562
|
+
const socket = coordinatorSocket({
|
|
563
|
+
dir: coordinatorRoot,
|
|
564
|
+
runDir: run.dir,
|
|
565
|
+
attempt: n,
|
|
566
|
+
...(events ? { nudged: () => nudge?.() } : {}),
|
|
567
|
+
failed: (reason) =>
|
|
568
|
+
progress.log(
|
|
569
|
+
`awf: the participants' socket did not open, so awf reply and awf send cannot reach this run: ${reason}`,
|
|
570
|
+
),
|
|
571
|
+
});
|
|
572
|
+
const coordinators = [...(environment.coordinators ?? []), socket, asked];
|
|
363
573
|
let handle: WorkflowRunHandle<JsonValue>;
|
|
364
574
|
try {
|
|
365
|
-
handle = await startWorkflow(
|
|
575
|
+
handle = await startWorkflow(workflow, prepared.args, {
|
|
366
576
|
runRoot: run.root,
|
|
367
577
|
run: {
|
|
368
578
|
dir: run.dir,
|
|
@@ -371,24 +581,54 @@ async function runAttempt(
|
|
|
371
581
|
label: workspaceLabel(meta.name, id, n),
|
|
372
582
|
...(command.fromStage === undefined ? {} : { fromStage: command.fromStage }),
|
|
373
583
|
...(command.values === undefined ? {} : { values: command.values.stages }),
|
|
584
|
+
...(!events
|
|
585
|
+
? {}
|
|
586
|
+
: {
|
|
587
|
+
events,
|
|
588
|
+
locked,
|
|
589
|
+
listenNudges: (given) => {
|
|
590
|
+
nudge = given;
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
// After a crash its agents start new sessions: a harness may still count the killed
|
|
594
|
+
// writer as its session's, as codex's thread store did (story 034, task 6).
|
|
595
|
+
resumeSessions: previous === undefined || previous.ended !== undefined,
|
|
596
|
+
park: async () => {
|
|
597
|
+
await endAttempt(attempt, { outcome: "parked" });
|
|
598
|
+
},
|
|
599
|
+
}),
|
|
374
600
|
},
|
|
601
|
+
emit: async (target, event, from) =>
|
|
602
|
+
(
|
|
603
|
+
await emitEvent(target, event, {
|
|
604
|
+
from,
|
|
605
|
+
home,
|
|
606
|
+
environment: environment.environment ?? process.env,
|
|
607
|
+
...(environment.launch ? { launch: environment.launch } : {}),
|
|
608
|
+
})
|
|
609
|
+
).outcome,
|
|
375
610
|
runtime: installed.config,
|
|
376
611
|
sandboxes: {
|
|
377
612
|
providers: installed.sandboxes ?? { installed: {} },
|
|
378
613
|
sandboxesDir: sandboxesDirOf(home, run.record),
|
|
379
614
|
machineRoot: machinePaths(home).root,
|
|
615
|
+
coordinatorRoot,
|
|
380
616
|
...(sandbox === undefined ? {} : { run: sandbox }),
|
|
381
617
|
},
|
|
382
618
|
...(installed.decisions ? { decisions: installed.decisions } : {}),
|
|
619
|
+
...(installed.allowance ? { allowance: installed.allowance } : {}),
|
|
383
620
|
deadline,
|
|
384
621
|
cwd,
|
|
385
622
|
...(environment.signal ? { signal: environment.signal } : {}),
|
|
386
623
|
...(environment.awaitReply ? { awaitReply: environment.awaitReply } : {}),
|
|
624
|
+
...(coordinators.length > 0 ? { coordinators } : {}),
|
|
387
625
|
onCallerStopped: () => handOver("stopped using this session", true),
|
|
388
626
|
onLog: (logMessage, fields?: JsonObject) =>
|
|
389
627
|
progress.log(fields ? `${logMessage} ${JSON.stringify(fields)}` : logMessage),
|
|
390
628
|
});
|
|
391
629
|
} catch (error) {
|
|
630
|
+
prompt?.close();
|
|
631
|
+
await socket.close();
|
|
392
632
|
progress.stop();
|
|
393
633
|
return endUnstarted(error, await cleanUp());
|
|
394
634
|
}
|
|
@@ -399,6 +639,9 @@ async function runAttempt(
|
|
|
399
639
|
} catch (error) {
|
|
400
640
|
finished = { error };
|
|
401
641
|
}
|
|
642
|
+
prompt?.close();
|
|
643
|
+
// Gone with the run: a holder now finds it ended.
|
|
644
|
+
await socket.close();
|
|
402
645
|
|
|
403
646
|
let end = decideEnding(finished, deadline, goOn);
|
|
404
647
|
progress.stop(end.settled ? stageFigures(end.settled.accounting) : undefined);
|
|
@@ -426,89 +669,6 @@ async function runAttempt(
|
|
|
426
669
|
return close(end, kept, cleanupAlso);
|
|
427
670
|
}
|
|
428
671
|
|
|
429
|
-
/** `awf allowance`: what is left of each harness's plan. */
|
|
430
|
-
async function allowanceCommand(
|
|
431
|
-
argv: readonly string[],
|
|
432
|
-
environment: OperatorEnvironment,
|
|
433
|
-
stdout: (text: string) => void,
|
|
434
|
-
stderr: (text: string) => void,
|
|
435
|
-
): Promise<number> {
|
|
436
|
-
let command: AllowanceCommand;
|
|
437
|
-
try {
|
|
438
|
-
command = parseAllowanceCommand(argv);
|
|
439
|
-
} catch (error) {
|
|
440
|
-
stderr(`awf: ${messageOf(error)}\n\n${allowanceUsage}`);
|
|
441
|
-
return 2;
|
|
442
|
-
}
|
|
443
|
-
if (command === "help") {
|
|
444
|
-
stdout(allowanceUsage);
|
|
445
|
-
return 0;
|
|
446
|
-
}
|
|
447
|
-
const read =
|
|
448
|
-
environment.readAllowance ??
|
|
449
|
-
allowanceReader(
|
|
450
|
-
environment.environment ?? process.env,
|
|
451
|
-
undefined,
|
|
452
|
-
environment.signal,
|
|
453
|
-
environment.home ?? homedir(),
|
|
454
|
-
);
|
|
455
|
-
const report = await readReport(command.harnesses, read, (environment.now ?? Date.now)());
|
|
456
|
-
stdout(command.json ? JSON.stringify(report, null, 2) : describeReport(report));
|
|
457
|
-
return 0;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/** `awf init`: a folder set up for writing workflows. */
|
|
461
|
-
async function initCommand(
|
|
462
|
-
argv: readonly string[],
|
|
463
|
-
environment: OperatorEnvironment,
|
|
464
|
-
stdout: (text: string) => void,
|
|
465
|
-
stderr: (text: string) => void,
|
|
466
|
-
): Promise<number> {
|
|
467
|
-
const cwd = environment.cwd ?? process.cwd();
|
|
468
|
-
let command: InitCommand | "help";
|
|
469
|
-
try {
|
|
470
|
-
command = parseInitCommand(argv, cwd);
|
|
471
|
-
} catch (error) {
|
|
472
|
-
stderr(`awf: ${messageOf(error)}\n\n${initUsage}`);
|
|
473
|
-
return 2;
|
|
474
|
-
}
|
|
475
|
-
if (command === "help") {
|
|
476
|
-
stdout(initUsage);
|
|
477
|
-
return 0;
|
|
478
|
-
}
|
|
479
|
-
try {
|
|
480
|
-
return await runInit(command, cwd, { stdout, stderr }, environment.install);
|
|
481
|
-
} catch (error) {
|
|
482
|
-
stderr(`awf: ${messageOf(error)}`);
|
|
483
|
-
return 1;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/** `awf test`: the workflow tests under the given paths, with Bun's test runner. */
|
|
488
|
-
async function testCommand(
|
|
489
|
-
argv: readonly string[],
|
|
490
|
-
environment: OperatorEnvironment,
|
|
491
|
-
stdout: (text: string) => void,
|
|
492
|
-
stderr: (text: string) => void,
|
|
493
|
-
): Promise<number> {
|
|
494
|
-
let tests: TestCommand | "help";
|
|
495
|
-
try {
|
|
496
|
-
tests = parseTestCommand(argv, environment.cwd ?? process.cwd());
|
|
497
|
-
} catch (error) {
|
|
498
|
-
stderr(`awf: ${messageOf(error)}\n\n${testUsage}`);
|
|
499
|
-
return 2;
|
|
500
|
-
}
|
|
501
|
-
if (tests === "help") {
|
|
502
|
-
stdout(testUsage);
|
|
503
|
-
return 0;
|
|
504
|
-
}
|
|
505
|
-
const captured = environment.stdout || environment.stderr ? { stdout, stderr } : undefined;
|
|
506
|
-
return runWorkflowTests(tests, environment.cwd ?? process.cwd(), {
|
|
507
|
-
...(environment.signal ? { signal: environment.signal } : {}),
|
|
508
|
-
...(captured ? { output: captured } : {}),
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
|
|
512
672
|
/**
|
|
513
673
|
* Not `console.error`, which Bun paints red on a terminal: a workflow's log and the accounting
|
|
514
674
|
* are ordinary lines. Only awf's own problems, which it prefixes `awf: `, are red: their first line.
|
|
@@ -557,6 +717,8 @@ function describeVersion(): string {
|
|
|
557
717
|
|
|
558
718
|
/** How soon a repeated signal is the copy `bun awf` forwards, not the operator pressing again. */
|
|
559
719
|
const REPEAT_MS = 1_000;
|
|
720
|
+
/** How long a command that has returned and written its output may keep the process. */
|
|
721
|
+
const EXIT_GRACE_MS = 2_000;
|
|
560
722
|
|
|
561
723
|
/**
|
|
562
724
|
* Cancels `controller` at the first signal that asks the run to stop, and stops the process at
|
|
@@ -595,6 +757,15 @@ export async function main(): Promise<void> {
|
|
|
595
757
|
} finally {
|
|
596
758
|
stop();
|
|
597
759
|
}
|
|
760
|
+
// A workflow's body can leave a timer or a pending call behind once its run has ended and its
|
|
761
|
+
// records are written; a woken run, detached, would otherwise live on with nothing to reap it.
|
|
762
|
+
// Output still queued for a slow reader is written first.
|
|
763
|
+
await Promise.all(
|
|
764
|
+
[process.stdout, process.stderr].map(
|
|
765
|
+
(stream) => new Promise<void>((resolve) => stream.write("", () => resolve())),
|
|
766
|
+
),
|
|
767
|
+
);
|
|
768
|
+
setTimeout(() => process.exit(), EXIT_GRACE_MS).unref();
|
|
598
769
|
}
|
|
599
770
|
|
|
600
771
|
if (import.meta.main) await main();
|
package/src/operator-runtime.ts
CHANGED
|
@@ -56,6 +56,8 @@ export type OperatorRuntimeInstallation = {
|
|
|
56
56
|
sandboxes?: SandboxProviders;
|
|
57
57
|
/** The decision models workflows may ask, and their aliases. */
|
|
58
58
|
decisions?: DecisionInstallation;
|
|
59
|
+
/** How `wf.allowance` reads a harness's plan, as `awf allowance` does. */
|
|
60
|
+
allowance?: (harness: Harness) => Promise<HarnessAllowance>;
|
|
59
61
|
cleanup(): Promise<void>;
|
|
60
62
|
};
|
|
61
63
|
|
|
@@ -82,7 +84,7 @@ export type OperatorRuntimeOptions = {
|
|
|
82
84
|
* headless one. `WITHHELD_ENVIRONMENT` is cleared from both.
|
|
83
85
|
*/
|
|
84
86
|
export async function installOperatorRuntime(
|
|
85
|
-
timeoutMilliseconds: number,
|
|
87
|
+
timeoutMilliseconds: number | undefined,
|
|
86
88
|
options: OperatorRuntimeOptions = {},
|
|
87
89
|
): Promise<OperatorRuntimeInstallation> {
|
|
88
90
|
const {
|
|
@@ -101,7 +103,7 @@ export async function installOperatorRuntime(
|
|
|
101
103
|
const session = runSessionOnce(() => name, { run, environment, home }, onRunSession);
|
|
102
104
|
const runConfig = (session: string): HerdrConfig => ({
|
|
103
105
|
...herdrConfig(session),
|
|
104
|
-
commandTimeoutMs: Math.min(timeoutMilliseconds, 150_000),
|
|
106
|
+
commandTimeoutMs: Math.min(timeoutMilliseconds ?? Number.POSITIVE_INFINITY, 150_000),
|
|
105
107
|
emptyEnvironment: PANE_WITHHELD_ENVIRONMENT,
|
|
106
108
|
acceptWorkspaceTrust: true,
|
|
107
109
|
watchSandboxes,
|
|
@@ -178,7 +180,8 @@ export async function installOperatorRuntime(
|
|
|
178
180
|
},
|
|
179
181
|
sandboxes: await installSandboxes(environment),
|
|
180
182
|
decisions: installDecisions({ OPENROUTER_API_KEY: await openRouterKey(environment) }),
|
|
181
|
-
|
|
183
|
+
allowance: allowanceReader(environment, run, undefined, home),
|
|
184
|
+
// Nothing to undo: the agent's `wfa` is a launcher the control plane installs beside its own
|
|
182
185
|
// socket, and the control plane removes both when the run closes.
|
|
183
186
|
cleanup: async () => undefined,
|
|
184
187
|
};
|
|
@@ -279,9 +282,8 @@ export function herdrConfig(session: string): HerdrConfig {
|
|
|
279
282
|
|
|
280
283
|
/**
|
|
281
284
|
* The sandbox providers this machine has: srt when its CLI is on `PATH`, docker when its CLI is;
|
|
282
|
-
* its daemon is first asked when a sandbox opens.
|
|
283
|
-
* the
|
|
284
|
-
* be set up: a spec naming none then fails saying why, rather than moving to docker unsaid.
|
|
285
|
+
* its daemon is first asked when a sandbox opens. There is no default: a spec names its provider,
|
|
286
|
+
* since the two give agents different machines, the host's toolchain or an image.
|
|
285
287
|
*/
|
|
286
288
|
export async function installSandboxes(
|
|
287
289
|
environment: Readonly<Record<string, string | undefined>>,
|
|
@@ -301,10 +303,8 @@ export async function installSandboxes(
|
|
|
301
303
|
...(srt ? { srt: createSrtProvider(srt) } : {}),
|
|
302
304
|
...(docker ? { docker: createDockerProvider(docker) } : {}),
|
|
303
305
|
};
|
|
304
|
-
const fallback = srt || unavailable.srt ? "srt" : docker ? "docker" : undefined;
|
|
305
306
|
return {
|
|
306
307
|
installed,
|
|
307
|
-
...(fallback ? { default: fallback } : {}),
|
|
308
308
|
...(Object.keys(unavailable).length > 0 ? { unavailable } : {}),
|
|
309
309
|
};
|
|
310
310
|
}
|