@agentswf/engine 0.1.0-beta.3 → 0.1.0-beta.5
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 +611 -0
- package/src/agent-cli/client.ts +115 -0
- package/src/agent-launcher.ts +15 -25
- package/src/allowance-command.ts +3 -15
- package/src/attempt-ending.ts +2 -4
- package/src/attempt-output.ts +3 -2
- package/src/attempt-view.ts +1 -1
- package/src/control-plane.ts +38 -6
- package/src/coordinator-socket.ts +15 -6
- package/src/deadlines.ts +69 -2
- package/src/decisions/directory.ts +9 -11
- package/src/deprecations.ts +177 -0
- package/src/duration.ts +50 -20
- package/src/emit-command.ts +57 -54
- package/src/emit.ts +124 -15
- package/src/help/allowance.txt +13 -0
- package/src/help/emit.txt +21 -0
- package/src/help/inbox.txt +12 -0
- package/src/help/init.txt +12 -0
- package/src/help/logs.txt +14 -0
- package/src/help/reply.txt +18 -0
- package/src/help/run.txt +58 -0
- package/src/help/runs.txt +11 -0
- package/src/help/test.txt +9 -0
- package/src/help/tick.txt +12 -0
- package/src/herdr-run-session.ts +2 -2
- package/src/here.ts +1 -1
- package/src/holder-command.ts +38 -51
- package/src/inbox-command.ts +9 -24
- package/src/init-command.ts +2 -11
- package/src/init-template/AGENTS.md.template +3 -3
- package/src/init-template/hello/workflow.test.ts.template +3 -3
- package/src/init-template/hello/workflow.ts.template +6 -6
- package/src/keep-alive.ts +28 -13
- package/src/logs-command.ts +14 -27
- package/src/mailbox.ts +16 -21
- package/src/messaging/limits.ts +5 -5
- package/src/messaging/messaging.ts +48 -43
- package/src/messaging/routes.ts +2 -2
- package/src/messaging/text.ts +24 -22
- package/src/operation-liveness.ts +1 -1
- package/src/operator-cli.ts +23 -9
- package/src/operator-runtime.ts +9 -12
- package/src/pane-layout.ts +125 -32
- package/src/progress-view.ts +8 -8
- package/src/result-validation.ts +1 -1
- package/src/run-command.ts +27 -47
- package/src/run-commands.ts +1 -1
- package/src/run-prepare.ts +46 -8
- package/src/run-usage.ts +2 -2
- package/src/run-view.ts +9 -4
- package/src/runs-command.ts +14 -23
- package/src/runs.ts +44 -8
- package/src/sandboxes.ts +3 -2
- package/src/simple-input.ts +13 -3
- package/src/sources.ts +42 -6
- package/src/stage-ledger.ts +43 -13
- package/src/stage-plan.ts +37 -1
- package/src/test-command.ts +9 -14
- package/src/testing.ts +8 -9
- package/src/tick-command.ts +2 -13
- package/src/tools.ts +62 -25
- package/src/workflow-loader.ts +5 -2
- package/src/workflow-runner.ts +636 -271
- package/src/workflow-testing/coordinator.typecheck.ts +12 -13
- package/src/workflow-testing/coordinators.ts +2 -2
- package/src/workflow-testing/host.ts +115 -46
- package/src/workflow-testing/index.ts +45 -837
- package/src/workflow-testing/run-test.ts +1213 -0
- package/src/workflow-testing/script.ts +106 -45
- package/src/workflow-testing/workflow-testing.typecheck.ts +13 -10
|
@@ -506,7 +506,7 @@ export async function superviseOperation(options: Options): Promise<SupervisedOu
|
|
|
506
506
|
deliveryUntil === undefined &&
|
|
507
507
|
receiptUntil === undefined;
|
|
508
508
|
const messages = options.messages;
|
|
509
|
-
// Before any check-in: what continues it goes now, cutting a `
|
|
509
|
+
// Before any check-in: what continues it goes now, cutting a `wfa waiting` short, and an
|
|
510
510
|
// operation awaiting an answer waits for it with no check-in.
|
|
511
511
|
if (messages && settledTurn) {
|
|
512
512
|
if (messages.pending() === "continues") {
|
package/src/operator-cli.ts
CHANGED
|
@@ -47,7 +47,13 @@ import { ANSI, describeRunSession, type Terminal, watchProgress } from "./progre
|
|
|
47
47
|
import { parseCommand, type RunCommand, usage } from "./run-command";
|
|
48
48
|
import { placeFlags, shellLine, startedLine } from "./run-commands";
|
|
49
49
|
import { runLockFile, withRunLock } from "./run-lock";
|
|
50
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
claimNext,
|
|
52
|
+
loadAndPrepare,
|
|
53
|
+
type OpensEventRun,
|
|
54
|
+
type PreparedRun,
|
|
55
|
+
workspaceLabel,
|
|
56
|
+
} from "./run-prepare";
|
|
51
57
|
import { findRun } from "./run-state";
|
|
52
58
|
import { type Attempt, createRun, discardRun, endAttempt, type Run, RunRefused } from "./runs";
|
|
53
59
|
import { runsCommand } from "./runs-command";
|
|
@@ -254,16 +260,22 @@ function help(
|
|
|
254
260
|
* an event starts one, so it runs to its first wait and parks; one already there is only woken.
|
|
255
261
|
*/
|
|
256
262
|
async function openRun(
|
|
257
|
-
{ loaded, id, argv }:
|
|
263
|
+
{ loaded, id, argv, panes }: OpensEventRun,
|
|
258
264
|
environment: OperatorEnvironment,
|
|
259
265
|
{ stdout, stderr }: Output,
|
|
260
266
|
): Promise<number> {
|
|
261
267
|
try {
|
|
262
|
-
const opened = await openEventRun(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
268
|
+
const opened = await openEventRun(
|
|
269
|
+
loaded.workflow as EventWorkflow,
|
|
270
|
+
id,
|
|
271
|
+
argv,
|
|
272
|
+
{
|
|
273
|
+
home: environment.home ?? homedir(),
|
|
274
|
+
environment: environment.environment ?? process.env,
|
|
275
|
+
...(environment.launch ? { launch: environment.launch } : {}),
|
|
276
|
+
},
|
|
277
|
+
panes,
|
|
278
|
+
);
|
|
267
279
|
const did = opened.did === "none" ? opened.why : opened.did;
|
|
268
280
|
stdout(
|
|
269
281
|
`${opened.run}${opened.created ? "" : " was there"}${did === undefined ? "" : `: ${did}`}`,
|
|
@@ -285,7 +297,7 @@ async function runCommand(
|
|
|
285
297
|
const { stdout, stderr, notice, terminal, now } = output;
|
|
286
298
|
let parsed: RunCommand | "help";
|
|
287
299
|
try {
|
|
288
|
-
parsed = parseCommand(argv, environment.cwd ?? process.cwd());
|
|
300
|
+
parsed = parseCommand(argv, environment.cwd ?? process.cwd(), stderr);
|
|
289
301
|
} catch (error) {
|
|
290
302
|
stderr(`awf: ${messageOf(error)}\n\n${usage}`);
|
|
291
303
|
return 2;
|
|
@@ -423,6 +435,7 @@ async function runAttempt(
|
|
|
423
435
|
argv: command.workflowArgs,
|
|
424
436
|
cwd: command.cwd,
|
|
425
437
|
sandbox: (command.sandbox ?? null) as JsonValue,
|
|
438
|
+
...(command.panes ? { panes: command.panes } : {}),
|
|
426
439
|
}));
|
|
427
440
|
} catch (error) {
|
|
428
441
|
return refuse(
|
|
@@ -505,7 +518,7 @@ async function runAttempt(
|
|
|
505
518
|
await handOver(toldOf(end, kept));
|
|
506
519
|
return close(end, kept, cleanupAlso);
|
|
507
520
|
};
|
|
508
|
-
// Relied on: the session is told only from a
|
|
521
|
+
// Relied on: the session is told only from a TUI agent's `openRun`, inside `startWorkflow`, by
|
|
509
522
|
// when `placed` says it on the progress.
|
|
510
523
|
let placed: (session: RunSession) => void = () => undefined;
|
|
511
524
|
let installed: OperatorRuntimeInstallation;
|
|
@@ -581,6 +594,7 @@ async function runAttempt(
|
|
|
581
594
|
label: workspaceLabel(meta.name, id, n),
|
|
582
595
|
...(command.fromStage === undefined ? {} : { fromStage: command.fromStage }),
|
|
583
596
|
...(command.values === undefined ? {} : { values: command.values.stages }),
|
|
597
|
+
...(run.record.panes ? { panes: run.record.panes } : {}),
|
|
584
598
|
...(!events
|
|
585
599
|
? {}
|
|
586
600
|
: {
|
package/src/operator-runtime.ts
CHANGED
|
@@ -56,7 +56,7 @@ export type OperatorRuntimeInstallation = {
|
|
|
56
56
|
sandboxes?: SandboxProviders;
|
|
57
57
|
/** The decision models workflows may ask, and their aliases. */
|
|
58
58
|
decisions?: DecisionInstallation;
|
|
59
|
-
/** How `
|
|
59
|
+
/** How `wf.allowance` reads a harness's plan, as `awf allowance` does. */
|
|
60
60
|
allowance?: (harness: Harness) => Promise<HarnessAllowance>;
|
|
61
61
|
cleanup(): Promise<void>;
|
|
62
62
|
};
|
|
@@ -75,12 +75,12 @@ export type OperatorRuntimeOptions = {
|
|
|
75
75
|
shellCwd?: string;
|
|
76
76
|
/** The operator's home. */
|
|
77
77
|
home?: string;
|
|
78
|
-
/** Told once, when the first
|
|
78
|
+
/** Told once, when the first TUI agent's session is ready. */
|
|
79
79
|
onRunSession?: (session: RunSession) => void;
|
|
80
80
|
};
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
|
-
* One run host for every agent: a Herdr tab for a
|
|
83
|
+
* One run host for every agent: a Herdr tab for a TUI agent, a subprocess per turn for a
|
|
84
84
|
* headless one. `WITHHELD_ENVIRONMENT` is cleared from both.
|
|
85
85
|
*/
|
|
86
86
|
export async function installOperatorRuntime(
|
|
@@ -98,7 +98,7 @@ export async function installOperatorRuntime(
|
|
|
98
98
|
} = options;
|
|
99
99
|
const unmetered = withholding(run, WITHHELD_ENVIRONMENT);
|
|
100
100
|
refuseMeteredCredentials(environment);
|
|
101
|
-
// Checked now, before any stage spends anything; made ready only for the first
|
|
101
|
+
// Checked now, before any stage spends anything; made ready only for the first TUI agent.
|
|
102
102
|
const name = runSessionName(environment);
|
|
103
103
|
const session = runSessionOnce(() => name, { run, environment, home }, onRunSession);
|
|
104
104
|
const runConfig = (session: string): HerdrConfig => ({
|
|
@@ -119,9 +119,9 @@ export async function installOperatorRuntime(
|
|
|
119
119
|
const host = loginChecked(
|
|
120
120
|
unmetered,
|
|
121
121
|
createPlacementHostFactory({
|
|
122
|
-
// The session is made ready when the first
|
|
122
|
+
// The session is made ready when the first TUI agent opens, so an all-headless run never
|
|
123
123
|
// calls Herdr.
|
|
124
|
-
|
|
124
|
+
tui: {
|
|
125
125
|
accounting: createSessionAccounting(unmetered),
|
|
126
126
|
openRun: async (spec) => {
|
|
127
127
|
const { name } = await session();
|
|
@@ -181,7 +181,7 @@ export async function installOperatorRuntime(
|
|
|
181
181
|
sandboxes: await installSandboxes(environment),
|
|
182
182
|
decisions: installDecisions({ OPENROUTER_API_KEY: await openRouterKey(environment) }),
|
|
183
183
|
allowance: allowanceReader(environment, run, undefined, home),
|
|
184
|
-
// Nothing to undo: the agent's `
|
|
184
|
+
// Nothing to undo: the agent's `wfa` is a launcher the control plane installs beside its own
|
|
185
185
|
// socket, and the control plane removes both when the run closes.
|
|
186
186
|
cleanup: async () => undefined,
|
|
187
187
|
};
|
|
@@ -282,9 +282,8 @@ export function herdrConfig(session: string): HerdrConfig {
|
|
|
282
282
|
|
|
283
283
|
/**
|
|
284
284
|
* The sandbox providers this machine has: srt when its CLI is on `PATH`, docker when its CLI is;
|
|
285
|
-
* its daemon is first asked when a sandbox opens.
|
|
286
|
-
* the
|
|
287
|
-
* 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.
|
|
288
287
|
*/
|
|
289
288
|
export async function installSandboxes(
|
|
290
289
|
environment: Readonly<Record<string, string | undefined>>,
|
|
@@ -304,10 +303,8 @@ export async function installSandboxes(
|
|
|
304
303
|
...(srt ? { srt: createSrtProvider(srt) } : {}),
|
|
305
304
|
...(docker ? { docker: createDockerProvider(docker) } : {}),
|
|
306
305
|
};
|
|
307
|
-
const fallback = srt || unavailable.srt ? "srt" : docker ? "docker" : undefined;
|
|
308
306
|
return {
|
|
309
307
|
installed,
|
|
310
|
-
...(fallback ? { default: fallback } : {}),
|
|
311
308
|
...(Object.keys(unavailable).length > 0 ? { unavailable } : {}),
|
|
312
309
|
};
|
|
313
310
|
}
|
package/src/pane-layout.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RunPanes } from "@agentswf/contract/records";
|
|
2
|
+
import type {
|
|
3
|
+
KeepPane,
|
|
4
|
+
PaneLayout,
|
|
5
|
+
PaneSpec,
|
|
6
|
+
PaneWorkspace,
|
|
7
|
+
TurnOutcome,
|
|
8
|
+
} from "@agentswf/contract/workflow";
|
|
2
9
|
|
|
3
10
|
/** A terminal session's name: Herdr makes `sessions/{name}` from it, and a leading `-` reads as an option. */
|
|
4
11
|
export const SESSION_NAME = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
@@ -6,53 +13,75 @@ export const SESSION_NAME = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
|
6
13
|
const SHARE = { min: 0.2, max: 0.8 };
|
|
7
14
|
const NEW_TAB_FIELDS = new Set(["session", "workspace", "tab"]);
|
|
8
15
|
const BESIDE_FIELDS = new Set(["beside", "side", "share"]);
|
|
9
|
-
const
|
|
16
|
+
export const KEEP_PANES: readonly KeepPane[] = ["never", "on-failure", "always"];
|
|
17
|
+
|
|
18
|
+
type WrittenPane = { pane?: unknown; layout?: unknown; keepPane?: unknown };
|
|
10
19
|
|
|
11
20
|
/**
|
|
12
21
|
* Refuses, before anything is placed, what the agent's own spec fixes about its pane: what a
|
|
13
|
-
* workflow, untyped at run time, could get wrong in `layout` or `keepPane`, and
|
|
14
|
-
* with no pane of awf's to place (docs/design/pane-layout.md, "Refused at open").
|
|
22
|
+
* workflow, untyped at run time, could get wrong in `pane`, `layout` or `keepPane`, and any of them
|
|
23
|
+
* on an agent with no pane of awf's to place (docs/design/pane-layout.md, "Refused at open").
|
|
15
24
|
*/
|
|
16
25
|
export function checkPaneOptions(
|
|
17
26
|
key: string,
|
|
18
|
-
spec:
|
|
27
|
+
spec: WrittenPane,
|
|
19
28
|
agent: { headless: boolean; sandboxed: boolean },
|
|
20
29
|
): void {
|
|
21
30
|
const refuse = (why: string): never => {
|
|
22
31
|
throw new Error(`agent ${key}: ${why}`);
|
|
23
32
|
};
|
|
24
|
-
if (spec.layout === undefined && spec.keepPane === undefined) return;
|
|
25
|
-
|
|
33
|
+
if (spec.pane === undefined && spec.layout === undefined && spec.keepPane === undefined) return;
|
|
34
|
+
const old = spec.pane === undefined;
|
|
35
|
+
if (agent.headless) {
|
|
36
|
+
refuse(
|
|
37
|
+
`${old ? "layout and keepPane place" : "pane places"} a pane, and a headless agent has none`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
26
40
|
if (agent.sandboxed) {
|
|
27
41
|
refuse(
|
|
28
|
-
"layout and keepPane are not yet for an agent in a sandbox, whose pane is in its own Herdr
|
|
42
|
+
`${old ? "layout and keepPane are" : "pane is"} not yet for an agent in a sandbox, whose pane is in its own Herdr`,
|
|
29
43
|
);
|
|
30
44
|
}
|
|
31
|
-
if (spec.
|
|
45
|
+
if (spec.pane !== undefined && spec.layout !== undefined) {
|
|
46
|
+
refuse("give pane, not layout beside it: layout is deprecated");
|
|
47
|
+
}
|
|
48
|
+
if (spec.keepPane !== undefined && !KEEP_PANES.includes(spec.keepPane as KeepPane)) {
|
|
32
49
|
refuse(`keepPane is never, on-failure or always, not ${JSON.stringify(spec.keepPane)}`);
|
|
33
50
|
}
|
|
34
|
-
if (spec.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
if (spec.pane !== undefined) checkShape(key, "pane", spec.pane, refuse);
|
|
52
|
+
if (spec.layout !== undefined) checkShape(key, "layout", spec.layout, refuse);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function checkShape(
|
|
56
|
+
key: string,
|
|
57
|
+
named: "pane" | "layout",
|
|
58
|
+
given: unknown,
|
|
59
|
+
refuse: (why: string) => never,
|
|
60
|
+
): void {
|
|
61
|
+
const a = named === "pane" ? "a pane" : "a layout";
|
|
62
|
+
if (typeof given !== "object" || given === null || Array.isArray(given)) {
|
|
63
|
+
refuse(`${a} is an object, not ${JSON.stringify(given)}`);
|
|
38
64
|
}
|
|
39
|
-
const
|
|
40
|
-
|
|
65
|
+
const layout = given as Record<string, unknown>;
|
|
66
|
+
const fields = Object.keys(layout).filter(
|
|
67
|
+
(field) => layout[field] !== undefined && !(named === "pane" && field === "front"),
|
|
41
68
|
);
|
|
42
|
-
|
|
43
|
-
|
|
69
|
+
if (named === "pane" && layout.front !== undefined && typeof layout.front !== "boolean") {
|
|
70
|
+
refuse(`front is true or false, not ${JSON.stringify(layout.front)}`);
|
|
71
|
+
}
|
|
72
|
+
if (layout.beside !== undefined) {
|
|
44
73
|
const others = fields.filter((field) => !BESIDE_FIELDS.has(field));
|
|
45
74
|
if (others.length > 0) {
|
|
46
|
-
refuse(
|
|
75
|
+
refuse(`${a} beside another pane takes side and share, not ${others.join(", ")}`);
|
|
47
76
|
}
|
|
48
|
-
if (typeof
|
|
49
|
-
refuse(`beside names an agent's key, not ${JSON.stringify(
|
|
77
|
+
if (typeof layout.beside !== "string" || layout.beside === "") {
|
|
78
|
+
refuse(`beside names an agent's key, not ${JSON.stringify(layout.beside)}`);
|
|
50
79
|
}
|
|
51
|
-
if (
|
|
52
|
-
if (
|
|
53
|
-
refuse(`side is right or below, not ${JSON.stringify(
|
|
80
|
+
if (layout.beside === key) refuse("a pane cannot be placed beside itself");
|
|
81
|
+
if (layout.side !== "right" && layout.side !== "below") {
|
|
82
|
+
refuse(`side is right or below, not ${JSON.stringify(layout.side)}`);
|
|
54
83
|
}
|
|
55
|
-
const share =
|
|
84
|
+
const share = layout.share;
|
|
56
85
|
if (
|
|
57
86
|
share !== undefined &&
|
|
58
87
|
(typeof share !== "number" || !(share >= SHARE.min && share <= SHARE.max))
|
|
@@ -61,13 +90,16 @@ export function checkPaneOptions(
|
|
|
61
90
|
}
|
|
62
91
|
return;
|
|
63
92
|
}
|
|
64
|
-
const
|
|
93
|
+
const allowed = named === "pane" ? new Set(["tab"]) : NEW_TAB_FIELDS;
|
|
94
|
+
const others = fields.filter((field) => !allowed.has(field));
|
|
65
95
|
if (others.length > 0) {
|
|
66
96
|
refuse(
|
|
67
|
-
|
|
97
|
+
named === "pane"
|
|
98
|
+
? `a pane takes tab, or beside and side, and front; not ${others.join(", ")}${others.some((field) => field === "workspace" || field === "session") ? ": the workspace is the operator's, awf run --workspace" : ""}`
|
|
99
|
+
: `a layout takes session, workspace and tab, or beside and side; not ${others.join(", ")}`,
|
|
68
100
|
);
|
|
69
101
|
}
|
|
70
|
-
const { session, workspace, tab } =
|
|
102
|
+
const { session, workspace, tab } = layout;
|
|
71
103
|
if (session !== undefined && (typeof session !== "string" || !SESSION_NAME.test(session))) {
|
|
72
104
|
refuse(
|
|
73
105
|
`session ${JSON.stringify(session)} is not a session name: lowercase letters, digits and -, up to 32, not starting with -`,
|
|
@@ -94,13 +126,17 @@ export function checkPaneOptions(
|
|
|
94
126
|
}
|
|
95
127
|
|
|
96
128
|
/**
|
|
97
|
-
* A checked layout as it is kept and compared: fields given as `undefined` dropped,
|
|
98
|
-
* that leaves one out matches.
|
|
129
|
+
* A checked `pane` or `layout` as it is kept and compared: fields given as `undefined` dropped, and
|
|
130
|
+
* `front: false`, which is its absence, so a reopen that leaves one out matches.
|
|
99
131
|
*/
|
|
100
|
-
export function storedLayout(layout:
|
|
132
|
+
export function storedLayout<T extends PaneLayout | PaneSpec>(layout: T): T {
|
|
133
|
+
// Not an object, it is left for `checkPaneOptions` to refuse by name.
|
|
134
|
+
if (typeof layout !== "object" || layout === null || Array.isArray(layout)) return layout;
|
|
101
135
|
return Object.fromEntries(
|
|
102
|
-
Object.entries(structuredClone(layout)).filter(
|
|
103
|
-
|
|
136
|
+
Object.entries(structuredClone(layout)).filter(
|
|
137
|
+
([field, value]) => value !== undefined && !(field === "front" && value === false),
|
|
138
|
+
),
|
|
139
|
+
) as T;
|
|
104
140
|
}
|
|
105
141
|
|
|
106
142
|
function nonEmpty(value: unknown): value is string {
|
|
@@ -112,6 +148,57 @@ export function besideOf(layout: PaneLayout | undefined): string | undefined {
|
|
|
112
148
|
return layout && "beside" in layout ? layout.beside : undefined;
|
|
113
149
|
}
|
|
114
150
|
|
|
151
|
+
/** What the host is asked for one agent's pane. */
|
|
152
|
+
export type PaneAsked = {
|
|
153
|
+
layout?: PaneLayout;
|
|
154
|
+
front?: true;
|
|
155
|
+
keepPane?: KeepPane;
|
|
156
|
+
/** For a pane beside another: where its own tab opens if its target has no pane. */
|
|
157
|
+
fallbackWorkspace?: PaneWorkspace;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* What the host is asked for an agent's pane: the workflow's tab or neighbour and `front`, in the
|
|
162
|
+
* operator's workspace and kept as the operator says. The operator's flags win over a deprecated
|
|
163
|
+
* `layout`'s session and workspace and a deprecated `keepPane`; absent, those apply as before. An
|
|
164
|
+
* agent with no pane of awf's, headless or in a sandbox, is asked nothing.
|
|
165
|
+
*/
|
|
166
|
+
export function paneAsked(
|
|
167
|
+
written: { pane?: PaneSpec; layout?: PaneLayout; keepPane?: KeepPane },
|
|
168
|
+
run: RunPanes | undefined,
|
|
169
|
+
hasPane: boolean,
|
|
170
|
+
): PaneAsked {
|
|
171
|
+
if (!hasPane) return {};
|
|
172
|
+
const keepPane = run?.keep ?? written.keepPane;
|
|
173
|
+
const workspace = run?.workspace;
|
|
174
|
+
const layout = placedLayout(written, workspace);
|
|
175
|
+
return {
|
|
176
|
+
...(layout === undefined ? {} : { layout }),
|
|
177
|
+
...(written.pane?.front ? { front: true as const } : {}),
|
|
178
|
+
...(keepPane === undefined ? {} : { keepPane }),
|
|
179
|
+
...(besideOf(layout) !== undefined && workspace !== undefined
|
|
180
|
+
? { fallbackWorkspace: workspace }
|
|
181
|
+
: {}),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function placedLayout(
|
|
186
|
+
{ pane, layout }: { pane?: PaneSpec; layout?: PaneLayout },
|
|
187
|
+
workspace: PaneWorkspace | undefined,
|
|
188
|
+
): PaneLayout | undefined {
|
|
189
|
+
const written = pane ?? layout;
|
|
190
|
+
if (written?.beside !== undefined) {
|
|
191
|
+
const { beside, side, share } = written;
|
|
192
|
+
return { beside, side, ...(share === undefined ? {} : { share }) };
|
|
193
|
+
}
|
|
194
|
+
const tab = written?.tab;
|
|
195
|
+
if (workspace !== undefined) {
|
|
196
|
+
return { workspace, ...(tab === undefined ? {} : { tab }) };
|
|
197
|
+
}
|
|
198
|
+
if (layout !== undefined) return layout;
|
|
199
|
+
return tab === undefined ? undefined : { tab };
|
|
200
|
+
}
|
|
201
|
+
|
|
115
202
|
/**
|
|
116
203
|
* Whether a done agent's pane stays, by `keepPane` and how its last operation ended: `on-failure`
|
|
117
204
|
* keeps it unless that was answered, or there was none.
|
|
@@ -123,3 +210,9 @@ export function keepsPane(
|
|
|
123
210
|
if (keepPane === "always") return true;
|
|
124
211
|
return keepPane === "on-failure" && last !== undefined && last !== "answered";
|
|
125
212
|
}
|
|
213
|
+
|
|
214
|
+
/** `--workspace`'s value: the two keywords, else a workspace's label. */
|
|
215
|
+
export function parseWorkspace(value: string): PaneWorkspace {
|
|
216
|
+
if (value === "run" || value === "origin") return value;
|
|
217
|
+
return { name: value };
|
|
218
|
+
}
|
package/src/progress-view.ts
CHANGED
|
@@ -40,7 +40,7 @@ export function renderProgress(
|
|
|
40
40
|
paint: Paint;
|
|
41
41
|
/** Once the run is over: what each stage's agents cost, by stage. */
|
|
42
42
|
figures?: ReadonlyMap<string, string>;
|
|
43
|
-
/** The Herdr session
|
|
43
|
+
/** The Herdr session TUI agents opened in, once one has. */
|
|
44
44
|
herdrSession?: string;
|
|
45
45
|
},
|
|
46
46
|
): string[] {
|
|
@@ -168,7 +168,7 @@ export function progressEvents(
|
|
|
168
168
|
turn.phase === "waiting"
|
|
169
169
|
? `waiting (agent-reported): ${oneLine(turn.waitingReason ?? "")}${turn.checkInAt === undefined ? "" : ` · check-in at ${clock(turn.checkInAt - view.startedAt)}`}`
|
|
170
170
|
: turn.phase === "awaiting-reply"
|
|
171
|
-
? `awaiting
|
|
171
|
+
? `awaiting-reply${turn.waitingReason === undefined ? "" : ` from ${oneLine(turn.waitingReason)}`}${turn.checkInAt === undefined ? "" : ` · until ${clock(turn.checkInAt - view.startedAt)}`}`
|
|
172
172
|
: oneLine(turn.phase.replaceAll("-", " "));
|
|
173
173
|
add(view.now, "agent phase", `… ${oneLine(agent.key)} · ${note}`);
|
|
174
174
|
}
|
|
@@ -400,7 +400,7 @@ function agentState(
|
|
|
400
400
|
function awaitingNote(turn: NonNullable<Agent["turn"]>, now: number): string {
|
|
401
401
|
const since = turn.phaseAt ?? turn.startedAt;
|
|
402
402
|
const of = turn.checkInAt === undefined ? "" : ` of ${duration(turn.checkInAt - since)}`;
|
|
403
|
-
return `awaiting
|
|
403
|
+
return `awaiting-reply${turn.waitingReason === undefined ? "" : ` from ${oneLine(turn.waitingReason)}`} · ${duration(Math.max(0, now - since))}${of}`;
|
|
404
404
|
}
|
|
405
405
|
|
|
406
406
|
/** A message admitted: who to whom, its size, and whether it asks or answers. */
|
|
@@ -412,20 +412,20 @@ export function messageLine(message: WorkflowRunSnapshot["messages"][number]): s
|
|
|
412
412
|
}
|
|
413
413
|
|
|
414
414
|
/**
|
|
415
|
-
* Where to see a
|
|
415
|
+
* Where to see a TUI agent Herdr read as blocked on a prompt: its session plays no sound, so this
|
|
416
416
|
* is the one sign. Not the calling session's, which is the operator's own pane.
|
|
417
417
|
*/
|
|
418
418
|
function whereBlocked(agent: Agent, herdrSession: string | undefined): string {
|
|
419
419
|
return agent.turn?.outcome === "blocked" &&
|
|
420
420
|
herdrSession !== undefined &&
|
|
421
|
-
placementOf(agent.execution) === "
|
|
421
|
+
placementOf(agent.execution) === "tui" &&
|
|
422
422
|
!agent.execution.caller
|
|
423
423
|
? ` · herdr session attach ${herdrSession}`
|
|
424
424
|
: "";
|
|
425
425
|
}
|
|
426
426
|
|
|
427
427
|
/**
|
|
428
|
-
* Where a run's
|
|
428
|
+
* Where a run's TUI agents are, said once as the first opens; the workspace closes with the run.
|
|
429
429
|
* A client inside Herdr can't attach to another session, so from there it says where to.
|
|
430
430
|
*/
|
|
431
431
|
export function describeRunSession(
|
|
@@ -519,7 +519,7 @@ export function watchProgress(
|
|
|
519
519
|
let handle: WorkflowRunHandle<JsonValue> | undefined;
|
|
520
520
|
let last: WorkflowRunSnapshot | undefined;
|
|
521
521
|
let herdrSession: string | undefined;
|
|
522
|
-
// A
|
|
522
|
+
// A TUI agent's session can be ready after the run has ended, as a cancel lands while it starts:
|
|
523
523
|
// nothing is said under the closing block.
|
|
524
524
|
let stopped = false;
|
|
525
525
|
let drawn = 0;
|
|
@@ -594,7 +594,7 @@ export function watchProgress(
|
|
|
594
594
|
if (under) clear();
|
|
595
595
|
else tick();
|
|
596
596
|
},
|
|
597
|
-
/** The Herdr session
|
|
597
|
+
/** The Herdr session TUI agents opened in, which a blocked one's line names. */
|
|
598
598
|
placedIn(session: string) {
|
|
599
599
|
if (!stopped) herdrSession = session;
|
|
600
600
|
},
|
package/src/result-validation.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type CandidateEvaluation =
|
|
|
8
8
|
| { kind: "rejected"; error: string };
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* A result as `
|
|
11
|
+
* A result as `wfa` sent it, validated without repairing or persisting; the result-slot transition
|
|
12
12
|
* owns acceptance.
|
|
13
13
|
*/
|
|
14
14
|
export async function evaluateGiven(
|
package/src/run-command.ts
CHANGED
|
@@ -1,54 +1,15 @@
|
|
|
1
1
|
import { readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import type {
|
|
3
|
+
import type { RunPanes } from "@agentswf/contract/records";
|
|
4
|
+
import type { JsonValue, KeepPane } from "@agentswf/contract/workflow";
|
|
4
5
|
import { parseDuration } from "./duration";
|
|
5
6
|
import { messageOf } from "./errors";
|
|
7
|
+
import help from "./help/run.txt" with { type: "text" };
|
|
6
8
|
import { SESSION_CODE } from "./here";
|
|
9
|
+
import { KEEP_PANES, parseWorkspace } from "./pane-layout";
|
|
7
10
|
import { idProblem, runRootOf, stageNameProblem } from "./runs";
|
|
8
11
|
|
|
9
|
-
export const usage =
|
|
10
|
-
"usage: awf run [options] <workflow-file> [options] [-- workflow arguments...]",
|
|
11
|
-
" awf run [options] <workflow-file> --id <id> --continue",
|
|
12
|
-
"run options: --id <id>, --continue, --from-stage <stage>, --values <file>,",
|
|
13
|
-
" --timeout <duration>, --run-root <directory>, --cwd <directory>, --sandbox <file>,",
|
|
14
|
-
" --json, --no-watch, --here",
|
|
15
|
-
"",
|
|
16
|
-
"Each awf run is an attempt of a run: a new run, with --id's id or a generated one, or, with",
|
|
17
|
-
"--continue, the run --id names, with the arguments, --cwd and --sandbox it was started with. A continue reuses",
|
|
18
|
-
"the stages that succeeded, up to the first with no record or to --from-stage, and runs the rest.",
|
|
19
|
-
"A new run with --from-stage starts there. A stage before it with no record to reuse takes its",
|
|
20
|
-
"value from --values, a JSON object by stage name, checked by its result; one that returns",
|
|
21
|
-
"nothing is passed. Without one the attempt stops, naming the stage and its schema.",
|
|
22
|
-
"A stop exits 3. A run is kept in .awf/runs/<workflow>/<id> under --cwd, unless --run-root names",
|
|
23
|
-
"another folder for .awf/runs.",
|
|
24
|
-
"A run has no deadline unless --timeout gives it one, per attempt.",
|
|
25
|
-
"A workflow that knows how to present its result prints that; --json prints the full result instead.",
|
|
26
|
-
"Either way the full result is kept as output.json in the run's folder, beside report.md",
|
|
27
|
-
"when the workflow writes one. A run that fails or is cancelled once its agents have started keeps",
|
|
28
|
-
"output.json too, with what it spent and why it ended; --json prints it. A second Ctrl-C stops",
|
|
29
|
-
"awf at once, without it.",
|
|
30
|
-
"--cwd sets the directory the workflow and its agents work in; it defaults to the current one.",
|
|
31
|
-
"--sandbox puts every agent of the run in one sandbox, working in --cwd; the file is a JSON spec",
|
|
32
|
-
'such as {"read": ["/data/request.md"], "srt": {}}, whose relative paths are from --cwd. A',
|
|
33
|
-
"workflow that opens a sandbox of its own is refused.",
|
|
34
|
-
"A sandbox with its own Herdr, as a docker box has, gets a tab in the run's workspace showing its",
|
|
35
|
-
"panes; --no-watch leaves it out, and awf still prints the command that shows them.",
|
|
36
|
-
"--here, run by an agent in a Herdr pane, starts the run in a new tab and has it take that agent's",
|
|
37
|
-
"session over as one of its agents, from its next turn until the run ends. It prints a line for",
|
|
38
|
-
"the agent to end its turn with, which is how the run finds the session.",
|
|
39
|
-
"A run of a workflow with events is named by --id, or by the workflow's id(args). awf run starts it",
|
|
40
|
-
"with its arguments and no event, detached in its project, as awf emit would, and the run waits at",
|
|
41
|
-
"its first receive. A run already there is only woken. A run an event starts has no argv: its args",
|
|
42
|
-
"are what prepare makes of none.",
|
|
43
|
-
"",
|
|
44
|
-
"Examples:",
|
|
45
|
-
" awf run examples/minimum-review/review-loop.ts",
|
|
46
|
-
" awf run --timeout 20m examples/minimum-review/review-loop.ts",
|
|
47
|
-
" awf run examples/minimum-review/review-loop.ts -- packages/engine/src",
|
|
48
|
-
" awf run examples/minimum-review/review-loop.ts --cwd ../other-repo",
|
|
49
|
-
"",
|
|
50
|
-
"Workflow files are trusted code and run with your filesystem and process authority.",
|
|
51
|
-
].join("\n");
|
|
12
|
+
export const usage = help.trimEnd();
|
|
52
13
|
|
|
53
14
|
export type RunCommand = {
|
|
54
15
|
/** Where paths typed on the command line resolve. */
|
|
@@ -78,6 +39,8 @@ export type RunCommand = {
|
|
|
78
39
|
watch: boolean;
|
|
79
40
|
/** The sandbox every agent runs in, as `--sandbox`'s file gave it. */
|
|
80
41
|
sandbox?: unknown;
|
|
42
|
+
/** `--workspace` and `--keep-panes`, which the run records; absent, neither was given. */
|
|
43
|
+
panes?: RunPanes;
|
|
81
44
|
/** Started from an agent's shell: start the run in a tab and take that session over. */
|
|
82
45
|
here: boolean;
|
|
83
46
|
/** The code the calling session replies with, which the run finds it by. */
|
|
@@ -85,7 +48,12 @@ export type RunCommand = {
|
|
|
85
48
|
};
|
|
86
49
|
|
|
87
50
|
/** `awf run`'s options and workflow; `"help"` for a `-h` or `--help` anywhere before `--`. */
|
|
88
|
-
export function parseCommand(
|
|
51
|
+
export function parseCommand(
|
|
52
|
+
argv: readonly string[],
|
|
53
|
+
cwd: string,
|
|
54
|
+
/** Told each deprecated form given, once. */
|
|
55
|
+
deprecated: (line: string) => void = () => {},
|
|
56
|
+
): RunCommand | "help" {
|
|
89
57
|
if (argv[0] !== "run") throw new Error("expected the run command");
|
|
90
58
|
const end = argv.indexOf("--");
|
|
91
59
|
const own = end < 0 ? argv : argv.slice(0, end);
|
|
@@ -104,6 +72,7 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand |
|
|
|
104
72
|
let sandbox: unknown;
|
|
105
73
|
let here = false;
|
|
106
74
|
let session: string | undefined;
|
|
75
|
+
const panes: RunPanes = {};
|
|
107
76
|
let workflowFile: string | undefined;
|
|
108
77
|
// awf's own options may come before or after the workflow file; only `--` ends them.
|
|
109
78
|
let index = 1;
|
|
@@ -132,8 +101,8 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand |
|
|
|
132
101
|
}
|
|
133
102
|
const value = argv[index + 1];
|
|
134
103
|
if (option === "--timeout") {
|
|
135
|
-
if (!value) throw new Error("--timeout needs a duration such as 30m");
|
|
136
|
-
timeoutMilliseconds = parseDuration(value);
|
|
104
|
+
if (!value) throw new Error("--timeout needs a duration such as 30m or 1d");
|
|
105
|
+
timeoutMilliseconds = parseDuration(value, "--timeout", deprecated);
|
|
137
106
|
timeout = value;
|
|
138
107
|
} else if (option === "--run-root") {
|
|
139
108
|
if (!value) throw new Error("--run-root needs a directory");
|
|
@@ -163,6 +132,16 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand |
|
|
|
163
132
|
if (!value || !SESSION_CODE.test(value))
|
|
164
133
|
throw new Error("--session needs the code --here printed");
|
|
165
134
|
session = value;
|
|
135
|
+
} else if (option === "--workspace") {
|
|
136
|
+
if (!value?.trim() || value.startsWith("-"))
|
|
137
|
+
throw new Error("--workspace needs run, origin or a workspace's name");
|
|
138
|
+
if (panes.workspace !== undefined) throw new Error("--workspace given twice");
|
|
139
|
+
panes.workspace = parseWorkspace(value);
|
|
140
|
+
} else if (option === "--keep-panes") {
|
|
141
|
+
if (!KEEP_PANES.includes(value as KeepPane))
|
|
142
|
+
throw new Error("--keep-panes needs never, on-failure or always");
|
|
143
|
+
if (panes.keep !== undefined) throw new Error("--keep-panes given twice");
|
|
144
|
+
panes.keep = value as KeepPane;
|
|
166
145
|
} else if (option === "--sandbox") {
|
|
167
146
|
if (!value) throw new Error("--sandbox needs a JSON file");
|
|
168
147
|
if (sandbox !== undefined) throw new Error("--sandbox given twice");
|
|
@@ -201,6 +180,7 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand |
|
|
|
201
180
|
here,
|
|
202
181
|
...(session === undefined ? {} : { session }),
|
|
203
182
|
...(sandbox === undefined ? {} : { sandbox }),
|
|
183
|
+
...(Object.keys(panes).length === 0 ? {} : { panes }),
|
|
204
184
|
};
|
|
205
185
|
}
|
|
206
186
|
|