@agentswf/engine 0.1.0-beta.5 → 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 +28 -55
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +4 -4
- package/src/attempt-output.ts +13 -31
- package/src/attempt-view.ts +33 -32
- 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 +4 -6
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +27 -28
- package/src/deadlines.ts +6 -40
- package/src/decisions/directory.ts +50 -37
- 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 +19 -33
- package/src/emit-command.ts +38 -22
- package/src/emit.ts +148 -59
- 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 +11 -7
- package/src/help/inbox.txt +5 -3
- package/src/help/logs.txt +7 -5
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +13 -8
- 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 +5 -13
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +54 -136
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +42 -56
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.ts.template +3 -2
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +7 -19
- package/src/logs-command.ts +134 -53
- package/src/message-operation.ts +6 -3
- package/src/messaging/messaging.ts +8 -10
- package/src/messaging/text.ts +2 -2
- 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 +206 -90
- package/src/operator-runtime.ts +24 -2
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +49 -96
- package/src/progress-view.ts +2 -2
- 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 +27 -31
- package/src/run-commands.ts +42 -114
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/run-policy/stage-plan.ts +154 -0
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +165 -41
- 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 +17 -11
- package/src/run-view.ts +73 -14
- package/src/runs-command.ts +81 -63
- package/src/runs.ts +284 -222
- 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 +2 -39
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +111 -153
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +37 -19
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +66 -18
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +11 -30
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +49 -2
- package/src/workflow-runner.ts +525 -618
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +1 -1
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +28 -29
- package/src/workflow-testing/index.ts +9 -27
- package/src/workflow-testing/run-test.ts +201 -286
- package/src/workflow-testing/script.ts +7 -39
- package/src/deprecations.ts +0 -177
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -972
- package/src/stage-plan.ts +0 -186
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- package/src/workflow-testing/workflow-testing.typecheck.ts +0 -67
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,24 +30,29 @@ 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
57
|
import {
|
|
51
58
|
claimNext,
|
|
@@ -56,10 +63,11 @@ import {
|
|
|
56
63
|
} from "./run-prepare";
|
|
57
64
|
import { findRun } from "./run-state";
|
|
58
65
|
import { type Attempt, createRun, discardRun, endAttempt, type Run, RunRefused } from "./runs";
|
|
59
|
-
import { runsCommand } from "./runs-command";
|
|
66
|
+
import { RUNS_V, runsCommand } from "./runs-command";
|
|
67
|
+
import { readSpent } from "./spend";
|
|
60
68
|
import { stopCrashed } from "./stop-crashed";
|
|
61
69
|
import { testCommand } from "./test-command";
|
|
62
|
-
import { tickCommand } from "./tick-command";
|
|
70
|
+
import { TICK_V, tickCommand } from "./tick-command";
|
|
63
71
|
import type { Launch } from "./wake";
|
|
64
72
|
import type { EventWorkflow, LoadedWorkflow } from "./workflow-loader";
|
|
65
73
|
import {
|
|
@@ -102,6 +110,11 @@ export type OperatorEnvironment = {
|
|
|
102
110
|
readAllowance?: (harness: Harness) => Promise<HarnessAllowance>;
|
|
103
111
|
/** Starts or continues an event run's attempt for `awf emit`; absent, a detached `awf run`. */
|
|
104
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;
|
|
105
118
|
/**
|
|
106
119
|
* Where agents may `--await-reply`, past the engine's list: for the live checks that decide
|
|
107
120
|
* what goes on it (story 029's M2 gate).
|
|
@@ -118,6 +131,12 @@ export type OperatorEnvironment = {
|
|
|
118
131
|
stdin?: () => Promise<string>;
|
|
119
132
|
};
|
|
120
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
|
+
|
|
121
140
|
type Output = {
|
|
122
141
|
stdout: (text: string) => void;
|
|
123
142
|
stderr: (text: string) => void;
|
|
@@ -127,11 +146,38 @@ type Output = {
|
|
|
127
146
|
now: () => number;
|
|
128
147
|
};
|
|
129
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
|
+
|
|
130
165
|
export async function runOperatorCli(
|
|
131
166
|
argv: readonly string[],
|
|
132
|
-
|
|
167
|
+
given: OperatorEnvironment = {},
|
|
133
168
|
): Promise<number> {
|
|
134
|
-
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
|
+
};
|
|
135
181
|
const write = (text: string) => void process.stderr.write(text);
|
|
136
182
|
const stderr =
|
|
137
183
|
environment.stderr ??
|
|
@@ -158,16 +204,17 @@ export async function runOperatorCli(
|
|
|
158
204
|
stderr,
|
|
159
205
|
stdin: environment.stdin ?? (() => new Response(Bun.stdin.stream()).text()),
|
|
160
206
|
...(environment.stdin ? {} : { stdinIsTerminal: process.stdin.isTTY === true }),
|
|
161
|
-
|
|
162
|
-
|
|
207
|
+
environment: environment.environment,
|
|
208
|
+
home: environment.home,
|
|
163
209
|
});
|
|
164
210
|
const wakes = {
|
|
165
|
-
home: environment.home
|
|
166
|
-
environment: environment.environment
|
|
211
|
+
home: environment.home,
|
|
212
|
+
environment: environment.environment,
|
|
167
213
|
...(environment.launch ? { launch: environment.launch } : {}),
|
|
168
214
|
};
|
|
215
|
+
const output = { stdout, stderr, notice, terminal, now };
|
|
169
216
|
const handlers = {
|
|
170
|
-
run: () => runCommand(argv, environment,
|
|
217
|
+
run: () => runCommand(argv, environment, output),
|
|
171
218
|
test: () => {
|
|
172
219
|
const captured = environment.stdout || environment.stderr ? { stdout, stderr } : undefined;
|
|
173
220
|
return testCommand(argv.slice(1), cwd, {
|
|
@@ -185,14 +232,36 @@ export async function runOperatorCli(
|
|
|
185
232
|
}),
|
|
186
233
|
emit: () => emitCommand(argv.slice(1), cwd, { stdout, stderr }, wakes),
|
|
187
234
|
tick: () => tickCommand(argv.slice(1), cwd, { stdout, stderr }, wakes),
|
|
188
|
-
runs: () => runsCommand(argv.slice(1), cwd, { stdout, stderr },
|
|
189
|
-
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
|
+
),
|
|
190
259
|
inbox: () =>
|
|
191
260
|
inboxCommand(
|
|
192
261
|
argv.slice(1),
|
|
193
262
|
cwd,
|
|
194
263
|
{ stdout, stderr },
|
|
195
|
-
{ home:
|
|
264
|
+
{ home: environment.home, ...(environment.signal ? { signal: environment.signal } : {}) },
|
|
196
265
|
),
|
|
197
266
|
reply: () => holder(),
|
|
198
267
|
send: () => holder(),
|
|
@@ -202,12 +271,7 @@ export async function runOperatorCli(
|
|
|
202
271
|
stderr,
|
|
203
272
|
read:
|
|
204
273
|
environment.readAllowance ??
|
|
205
|
-
allowanceReader(
|
|
206
|
-
environment.environment ?? process.env,
|
|
207
|
-
undefined,
|
|
208
|
-
environment.signal,
|
|
209
|
-
environment.home ?? homedir(),
|
|
210
|
-
),
|
|
274
|
+
allowanceReader(environment.environment, undefined, environment.signal, environment.home),
|
|
211
275
|
now,
|
|
212
276
|
}),
|
|
213
277
|
} satisfies Record<CommandName, () => Promise<number>>;
|
|
@@ -222,14 +286,56 @@ export async function runOperatorCli(
|
|
|
222
286
|
return 2;
|
|
223
287
|
}
|
|
224
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}`);
|
|
225
292
|
return 0;
|
|
226
293
|
}
|
|
227
294
|
if (word === "help" || word === "-h" || word === "--help") return help(rest, stdout, stderr);
|
|
228
|
-
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
|
+
}
|
|
229
312
|
stderr(unknownCommand(word));
|
|
230
313
|
return 2;
|
|
231
314
|
}
|
|
232
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
|
+
|
|
233
339
|
/** `awf help`, `awf help {command}` and `awf help reference`; `-h` and `--help` are `help`. */
|
|
234
340
|
function help(
|
|
235
341
|
words: readonly string[],
|
|
@@ -254,14 +360,13 @@ function help(
|
|
|
254
360
|
return 0;
|
|
255
361
|
}
|
|
256
362
|
|
|
257
|
-
/** `awf run`: parses it, then starts or continues the run, here or in a tab. */
|
|
258
363
|
/**
|
|
259
364
|
* `awf run --id` for a workflow with events: the run is made with no event and started detached, as
|
|
260
365
|
* an event starts one, so it runs to its first wait and parks; one already there is only woken.
|
|
261
366
|
*/
|
|
262
367
|
async function openRun(
|
|
263
368
|
{ loaded, id, argv, panes }: OpensEventRun,
|
|
264
|
-
environment:
|
|
369
|
+
environment: Resolved,
|
|
265
370
|
{ stdout, stderr }: Output,
|
|
266
371
|
): Promise<number> {
|
|
267
372
|
try {
|
|
@@ -270,53 +375,42 @@ async function openRun(
|
|
|
270
375
|
id,
|
|
271
376
|
argv,
|
|
272
377
|
{
|
|
273
|
-
home: environment.home
|
|
274
|
-
environment: environment.environment
|
|
378
|
+
home: environment.home,
|
|
379
|
+
environment: environment.environment,
|
|
275
380
|
...(environment.launch ? { launch: environment.launch } : {}),
|
|
276
381
|
},
|
|
277
382
|
panes,
|
|
278
383
|
);
|
|
279
|
-
|
|
280
|
-
stdout
|
|
281
|
-
`${opened.run}${opened.created ? "" : " was there"}${did === undefined ? "" : `: ${did}`}`,
|
|
282
|
-
);
|
|
283
|
-
if (opened.why !== "launch-failed") return 0;
|
|
284
|
-
stderr(`awf: the run's attempt did not start: ${opened.error}`);
|
|
285
|
-
return 1;
|
|
384
|
+
stdout(`${opened.run}${opened.created ? "" : " was there"}${didOf(opened)}`);
|
|
385
|
+
return launchExit(opened, { stdout, stderr });
|
|
286
386
|
} catch (error) {
|
|
287
|
-
|
|
387
|
+
printRefusal({ stdout, stderr }, NO_JSON, error, readFailureCode(error));
|
|
288
388
|
return error instanceof EmitError ? 2 : 1;
|
|
289
389
|
}
|
|
290
390
|
}
|
|
291
391
|
|
|
392
|
+
/** `awf run`: parses it, then starts or continues the run, here or in a tab. */
|
|
292
393
|
async function runCommand(
|
|
293
394
|
argv: readonly string[],
|
|
294
|
-
environment:
|
|
395
|
+
environment: Resolved,
|
|
295
396
|
output: Output,
|
|
296
397
|
): Promise<number> {
|
|
297
|
-
const { stdout, stderr, notice,
|
|
298
|
-
let parsed: RunCommand
|
|
398
|
+
const { stdout, stderr, notice, now } = output;
|
|
399
|
+
let parsed: RunCommand;
|
|
299
400
|
try {
|
|
300
|
-
parsed = parseCommand(argv, environment.cwd ?? process.cwd()
|
|
401
|
+
parsed = parseCommand(argv, environment.cwd ?? process.cwd());
|
|
301
402
|
} catch (error) {
|
|
302
|
-
stderr
|
|
403
|
+
printRefusal({ stdout, stderr }, { json: asksJson(argv), v: RUN_V }, error, "usage", usage);
|
|
303
404
|
return 2;
|
|
304
405
|
}
|
|
305
|
-
if (parsed === "help") {
|
|
306
|
-
stdout(usage);
|
|
307
|
-
return 0;
|
|
308
|
-
}
|
|
309
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);
|
|
310
410
|
const meant = await runNamed(command);
|
|
311
411
|
if (meant) {
|
|
312
|
-
const look = shellLine([
|
|
313
|
-
|
|
314
|
-
"runs",
|
|
315
|
-
meant,
|
|
316
|
-
...(command.cwdGiven ? ["--cwd", command.cwd] : []),
|
|
317
|
-
...(command.runRootGiven ? ["--run-root", command.runRoot] : []),
|
|
318
|
-
]);
|
|
319
|
-
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}`);
|
|
320
414
|
return 2;
|
|
321
415
|
}
|
|
322
416
|
if (command.here) return startHere(argv, command, environment, stdout, stderr, notice);
|
|
@@ -326,19 +420,20 @@ async function runCommand(
|
|
|
326
420
|
if (command.session !== undefined) await showOwnTab(environment);
|
|
327
421
|
return exitCode;
|
|
328
422
|
};
|
|
329
|
-
const home = environment
|
|
423
|
+
const { home } = environment;
|
|
330
424
|
const { sandboxes, callers } = machinePaths(home);
|
|
331
425
|
const ready = await loadAndPrepare(command, now(), home);
|
|
332
426
|
if ("refused" in ready) {
|
|
333
|
-
|
|
427
|
+
refused(ready.code, ready.refused);
|
|
334
428
|
return unstarted(2);
|
|
335
429
|
}
|
|
336
430
|
if ("opens" in ready) return openRun(ready.opens, environment, output);
|
|
337
431
|
// After loading: an event run's root is its project's, which this checks too.
|
|
338
432
|
command = ready.command;
|
|
339
|
-
if (
|
|
340
|
-
|
|
341
|
-
|
|
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`,
|
|
342
437
|
);
|
|
343
438
|
return unstarted(2);
|
|
344
439
|
}
|
|
@@ -347,28 +442,24 @@ async function runCommand(
|
|
|
347
442
|
// Loading the workflow imports operator-supplied code before anything listens to the signal, so
|
|
348
443
|
// a Ctrl-C in it would otherwise be swallowed and have to be pressed again.
|
|
349
444
|
if (environment.signal?.aborted) {
|
|
445
|
+
if (command.json) stdout(refusalLine(RUN_V, "not-started", CANCELLED));
|
|
350
446
|
return unstarted(cancelledBeforeStart(environment.signal, stderr));
|
|
351
447
|
}
|
|
352
448
|
let calling: { caller: Caller; release: () => void } | undefined;
|
|
353
449
|
if (command.session !== undefined) {
|
|
354
450
|
const taken = await takeCaller(command.session, callers, environment);
|
|
355
451
|
if (typeof taken === "string") {
|
|
356
|
-
|
|
452
|
+
refused("not-started", `--session: ${taken}`);
|
|
357
453
|
return 1;
|
|
358
454
|
}
|
|
359
455
|
calling = taken;
|
|
360
456
|
if (environment.signal?.aborted) {
|
|
361
457
|
calling.release();
|
|
458
|
+
if (command.json) stdout(refusalLine(RUN_V, "not-started", CANCELLED));
|
|
362
459
|
return cancelledBeforeStart(environment.signal, stderr);
|
|
363
460
|
}
|
|
364
461
|
}
|
|
365
|
-
return runAttempt(command, ready, calling, environment,
|
|
366
|
-
stdout,
|
|
367
|
-
stderr,
|
|
368
|
-
notice,
|
|
369
|
-
terminal,
|
|
370
|
-
now,
|
|
371
|
-
});
|
|
462
|
+
return runAttempt(command, ready, calling, environment, output);
|
|
372
463
|
}
|
|
373
464
|
|
|
374
465
|
/** The run `awf run {ref}` names where `{ref}` is no file: one meant for `awf runs`. */
|
|
@@ -386,11 +477,11 @@ async function runAttempt(
|
|
|
386
477
|
command: RunCommand,
|
|
387
478
|
{ loaded, prepared }: { loaded: LoadedWorkflow; prepared: PreparedRun },
|
|
388
479
|
calling: { caller: Caller; release: () => void } | undefined,
|
|
389
|
-
environment:
|
|
390
|
-
home: string,
|
|
480
|
+
environment: Resolved,
|
|
391
481
|
output: Output,
|
|
392
482
|
): Promise<number> {
|
|
393
483
|
const { stdout, stderr, notice, terminal, now } = output;
|
|
484
|
+
const { home } = environment;
|
|
394
485
|
const { workflow } = loaded;
|
|
395
486
|
const { meta } = workflow;
|
|
396
487
|
/** The run, once it is claimed. */
|
|
@@ -410,21 +501,21 @@ async function runAttempt(
|
|
|
410
501
|
if (failed) stderr(`awf: the calling session was not told the run ended: ${failed}`);
|
|
411
502
|
};
|
|
412
503
|
|
|
413
|
-
const refuse = async (why: string) => {
|
|
414
|
-
stderr
|
|
504
|
+
const refuse = async (why: string, code: RefusalCode) => {
|
|
505
|
+
printRefusal({ stdout, stderr }, { json: command.json, v: RUN_V }, why, code);
|
|
415
506
|
await handOver(`did not start: ${why}`);
|
|
416
507
|
return 2;
|
|
417
508
|
};
|
|
418
509
|
// The operator's, never where the workflow's agents work.
|
|
419
|
-
const coordinatorRoot = coordinatorDir(home, environment.environment
|
|
510
|
+
const coordinatorRoot = coordinatorDir(home, environment.environment);
|
|
420
511
|
try {
|
|
421
512
|
await prepareCoordinatorDir(
|
|
422
513
|
coordinatorRoot,
|
|
423
514
|
command.cwd,
|
|
424
|
-
Boolean(
|
|
515
|
+
Boolean(environment.environment.AWF_COORDINATOR_DIR),
|
|
425
516
|
);
|
|
426
517
|
} catch (error) {
|
|
427
|
-
return refuse(messageOf(error));
|
|
518
|
+
return refuse(messageOf(error), "internal-error");
|
|
428
519
|
}
|
|
429
520
|
try {
|
|
430
521
|
run =
|
|
@@ -440,15 +531,16 @@ async function runAttempt(
|
|
|
440
531
|
} catch (error) {
|
|
441
532
|
return refuse(
|
|
442
533
|
`${error instanceof RunRefused ? "" : `${command.runRoot}: `}${messageOf(error)}`,
|
|
534
|
+
readFailureCode(error),
|
|
443
535
|
);
|
|
444
536
|
}
|
|
445
537
|
let attempt: Attempt;
|
|
446
538
|
try {
|
|
447
|
-
attempt = await claimNext(run, loaded, command, notice, now());
|
|
539
|
+
attempt = await claimNext(run, loaded, command, notice, now(), environment.launchId);
|
|
448
540
|
} catch (error) {
|
|
449
541
|
// A run this call created and never ran is not left to hold its id.
|
|
450
542
|
if (!prepared.continued) await discardRun(run).catch(() => undefined);
|
|
451
|
-
return refuse(messageOf(error));
|
|
543
|
+
return refuse(messageOf(error), readFailureCode(error));
|
|
452
544
|
}
|
|
453
545
|
const { id, cwd } = run.record;
|
|
454
546
|
const locked = <T>(work: () => Promise<T>) =>
|
|
@@ -462,7 +554,7 @@ async function runAttempt(
|
|
|
462
554
|
if (run.record.events && previous && previous.ended === undefined)
|
|
463
555
|
await stopCrashed(previous, workspaceLabel(meta.name, run.record.id, previous.attempt), {
|
|
464
556
|
home,
|
|
465
|
-
environment: environment.environment
|
|
557
|
+
environment: environment.environment,
|
|
466
558
|
}).catch((error: unknown) =>
|
|
467
559
|
notice(
|
|
468
560
|
`awf: what attempt ${previous.attempt} left running was not all stopped: ${messageOf(error)}`,
|
|
@@ -473,14 +565,26 @@ async function runAttempt(
|
|
|
473
565
|
attempt,
|
|
474
566
|
loaded,
|
|
475
567
|
stderr,
|
|
568
|
+
now,
|
|
476
569
|
...(!run.record.events ? {} : { locked }),
|
|
477
570
|
};
|
|
478
|
-
const close = (end: AttemptEnd, kept: Kept, cleanupAlso?: string) =>
|
|
479
|
-
|
|
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, {
|
|
480
583
|
workflow,
|
|
481
584
|
run: { id, dir: run.dir },
|
|
482
585
|
n,
|
|
483
|
-
earlier
|
|
586
|
+
earlier,
|
|
587
|
+
...(spentBefore ? { spentBefore } : {}),
|
|
484
588
|
json: command.json,
|
|
485
589
|
fromStage: command.fromStage !== undefined,
|
|
486
590
|
now: now(),
|
|
@@ -492,6 +596,7 @@ async function runAttempt(
|
|
|
492
596
|
stderr,
|
|
493
597
|
notice,
|
|
494
598
|
});
|
|
599
|
+
};
|
|
495
600
|
// Timed from here: finding the calling session can take minutes the run itself never had.
|
|
496
601
|
const startedAt = now();
|
|
497
602
|
// Every way out from here writes the attempt's ending, before the hand-back: a session told the
|
|
@@ -510,7 +615,12 @@ async function runAttempt(
|
|
|
510
615
|
const said = cancelled
|
|
511
616
|
? "run cancelled before it started"
|
|
512
617
|
: `${end.ending.reason}; the run did not start`;
|
|
513
|
-
|
|
618
|
+
printRefusal(
|
|
619
|
+
{ stdout, stderr },
|
|
620
|
+
{ json: command.json, v: RUN_V },
|
|
621
|
+
`${said}, and nothing of it is kept`,
|
|
622
|
+
"not-started",
|
|
623
|
+
);
|
|
514
624
|
await handOver(`did not start: ${cancelled ? "cancelled" : end.ending.reason}`);
|
|
515
625
|
return end.exitCode;
|
|
516
626
|
}
|
|
@@ -556,7 +666,7 @@ async function runAttempt(
|
|
|
556
666
|
placed = (session) => {
|
|
557
667
|
progress.placedIn(session.name);
|
|
558
668
|
const label = workspaceLabel(meta.name, id, n);
|
|
559
|
-
const insideHerdr =
|
|
669
|
+
const insideHerdr = environment.environment.HERDR_ENV === "1";
|
|
560
670
|
for (const line of describeRunSession(session, label, insideHerdr)) progress.log(line);
|
|
561
671
|
};
|
|
562
672
|
// Interactive: a coordinator's questions are asked here, unless the output is for a program.
|
|
@@ -590,7 +700,8 @@ async function runAttempt(
|
|
|
590
700
|
run: {
|
|
591
701
|
dir: run.dir,
|
|
592
702
|
id,
|
|
593
|
-
|
|
703
|
+
lines: attempt.lines,
|
|
704
|
+
ops: attempt.ops,
|
|
594
705
|
label: workspaceLabel(meta.name, id, n),
|
|
595
706
|
...(command.fromStage === undefined ? {} : { fromStage: command.fromStage }),
|
|
596
707
|
...(command.values === undefined ? {} : { values: command.values.stages }),
|
|
@@ -617,7 +728,7 @@ async function runAttempt(
|
|
|
617
728
|
await emitEvent(target, event, {
|
|
618
729
|
from,
|
|
619
730
|
home,
|
|
620
|
-
environment: environment.environment
|
|
731
|
+
environment: environment.environment,
|
|
621
732
|
...(environment.launch ? { launch: environment.launch } : {}),
|
|
622
733
|
})
|
|
623
734
|
).outcome,
|
|
@@ -698,8 +809,10 @@ export function stderrLines(
|
|
|
698
809
|
};
|
|
699
810
|
}
|
|
700
811
|
|
|
812
|
+
const CANCELLED = "run cancelled before it started";
|
|
813
|
+
|
|
701
814
|
function cancelledBeforeStart(signal: AbortSignal, stderr: (line: string) => void): number {
|
|
702
|
-
stderr(
|
|
815
|
+
stderr(`awf: ${CANCELLED}`);
|
|
703
816
|
return signalExitCode(signal.reason);
|
|
704
817
|
}
|
|
705
818
|
|
|
@@ -738,16 +851,19 @@ const EXIT_GRACE_MS = 2_000;
|
|
|
738
851
|
* Cancels `controller` at the first signal that asks the run to stop, and stops the process at
|
|
739
852
|
* once at a second the operator sends, without the run's cleanup. Ctrl-C reaches the whole
|
|
740
853
|
* process group and `bun awf` forwards it once more, so a repeat within `REPEAT_MS` is ignored:
|
|
741
|
-
* taken as the second, it would kill the run mid-cleanup and leave its sandboxes behind.
|
|
742
|
-
*
|
|
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.
|
|
743
857
|
*/
|
|
744
858
|
export function cancelOnSignals(
|
|
745
859
|
controller: AbortController,
|
|
746
860
|
exit: (code: number) => void = (code) => process.exit(code),
|
|
747
861
|
now: () => number = Date.now,
|
|
862
|
+
attached: () => boolean = terminalAttached,
|
|
748
863
|
): () => void {
|
|
749
864
|
let first: number | undefined;
|
|
750
865
|
const cancel = (signal: NodeJS.Signals) => {
|
|
866
|
+
if (signal === "SIGINT" && attached()) return;
|
|
751
867
|
if (first === undefined) {
|
|
752
868
|
first = now();
|
|
753
869
|
controller.abort(signal);
|
package/src/operator-runtime.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
readClaudeBilling,
|
|
24
24
|
readCodexBilling,
|
|
25
25
|
readCursorLogin,
|
|
26
|
+
runAttached,
|
|
26
27
|
runProcess,
|
|
27
28
|
withholding,
|
|
28
29
|
} from "@agentswf/harness";
|
|
@@ -37,7 +38,7 @@ import { createSrtProvider, findSrt } from "@agentswf/sandbox/srt";
|
|
|
37
38
|
import { createOpenRouterProvider } from "./decisions/openrouter";
|
|
38
39
|
import type { DecisionInstallation } from "./decisions/seam";
|
|
39
40
|
import { messageOf } from "./errors";
|
|
40
|
-
import { findOrigin,
|
|
41
|
+
import { findOrigin, workspaceLock } from "./herdr-placement";
|
|
41
42
|
import {
|
|
42
43
|
ensureRunSession,
|
|
43
44
|
parseSessions,
|
|
@@ -48,6 +49,7 @@ import { markWorkspace } from "./herdr-workspace-marks";
|
|
|
48
49
|
|
|
49
50
|
export type { RunSession } from "./herdr-run-session";
|
|
50
51
|
|
|
52
|
+
import type { Attach } from "./open-command";
|
|
51
53
|
import { OPERATOR_ALIASES } from "./operator-aliases";
|
|
52
54
|
|
|
53
55
|
export type OperatorRuntimeInstallation = {
|
|
@@ -129,7 +131,6 @@ export async function installOperatorRuntime(
|
|
|
129
131
|
const config: HerdrConfig = {
|
|
130
132
|
...runConfig(name),
|
|
131
133
|
onRunWorkspace: mark.bind,
|
|
132
|
-
sessionFor: (named) => namedSession(named, { run, environment, home }),
|
|
133
134
|
origin: () =>
|
|
134
135
|
findOrigin({
|
|
135
136
|
run,
|
|
@@ -424,6 +425,27 @@ const WITHHELD_ENVIRONMENT = [
|
|
|
424
425
|
"AWF_CALLER_SESSIONS",
|
|
425
426
|
];
|
|
426
427
|
|
|
428
|
+
/**
|
|
429
|
+
* How `awf open` runs a harness on an agent's session: its fork and `-p` turn, and its TUI on this
|
|
430
|
+
* terminal, each withholding what no agent inherits (story 042).
|
|
431
|
+
*/
|
|
432
|
+
export function openRuntime(environment: Readonly<Record<string, string | undefined>>): {
|
|
433
|
+
run: RunProcess;
|
|
434
|
+
attach: Attach;
|
|
435
|
+
providers: () => Promise<SandboxProviders>;
|
|
436
|
+
} {
|
|
437
|
+
const withheld = Object.fromEntries(WITHHELD_ENVIRONMENT.map((name) => [name, undefined]));
|
|
438
|
+
return {
|
|
439
|
+
run: withholding(runProcess, WITHHELD_ENVIRONMENT),
|
|
440
|
+
// A sandbox's command has exactly its own environment; on the host, the operator's less these.
|
|
441
|
+
attach: (input) =>
|
|
442
|
+
input.exact
|
|
443
|
+
? runAttached(input, {})
|
|
444
|
+
: runAttached({ ...input, env: { ...input.env, ...withheld } }, environment),
|
|
445
|
+
providers: () => installSandboxes(environment),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
427
449
|
/**
|
|
428
450
|
* Unset in a run's panes besides: the config the run session's server starts with, which a `herdr`
|
|
429
451
|
* started in a pane would otherwise read in place of the operator's.
|