@agentswf/engine 0.1.0-beta.4 → 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 +4 -4
- package/src/agent-cli/cli.ts +57 -17
- package/src/attempt-ending.ts +2 -4
- package/src/attempt-output.ts +2 -1
- package/src/attempt-view.ts +1 -1
- package/src/control-plane.ts +37 -5
- 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 +55 -41
- package/src/emit.ts +124 -15
- package/src/help/emit.txt +12 -4
- package/src/help/inbox.txt +2 -2
- package/src/help/logs.txt +5 -4
- package/src/help/reply.txt +5 -5
- package/src/help/run.txt +21 -7
- package/src/herdr-run-session.ts +2 -2
- package/src/holder-command.ts +36 -31
- package/src/inbox-command.ts +7 -10
- package/src/init-template/hello/workflow.test.ts.template +3 -3
- package/src/init-template/hello/workflow.ts.template +2 -2
- package/src/keep-alive.ts +28 -13
- package/src/logs-command.ts +12 -12
- package/src/mailbox.ts +2 -20
- package/src/messaging/limits.ts +4 -4
- package/src/messaging/messaging.ts +26 -21
- package/src/messaging/routes.ts +1 -1
- package/src/messaging/text.ts +7 -5
- package/src/operator-cli.ts +23 -9
- package/src/operator-runtime.ts +5 -5
- package/src/pane-layout.ts +125 -32
- package/src/progress-view.ts +8 -8
- package/src/run-command.ts +25 -4
- package/src/run-commands.ts +1 -1
- package/src/run-prepare.ts +46 -8
- package/src/run-usage.ts +1 -1
- package/src/run-view.ts +9 -4
- package/src/runs-command.ts +12 -10
- package/src/runs.ts +44 -8
- package/src/sources.ts +42 -6
- package/src/stage-ledger.ts +42 -12
- package/src/stage-plan.ts +37 -1
- package/src/test-command.ts +7 -3
- package/src/testing.ts +7 -8
- package/src/tools.ts +46 -18
- package/src/workflow-loader.ts +5 -2
- package/src/workflow-runner.ts +553 -235
- package/src/workflow-testing/coordinator.typecheck.ts +7 -7
- package/src/workflow-testing/coordinators.ts +2 -2
- package/src/workflow-testing/host.ts +107 -38
- package/src/workflow-testing/index.ts +45 -836
- package/src/workflow-testing/run-test.ts +1213 -0
- package/src/workflow-testing/script.ts +102 -41
- package/src/workflow-testing/workflow-testing.typecheck.ts +11 -8
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
|
@@ -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();
|
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/run-command.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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";
|
|
6
7
|
import help from "./help/run.txt" with { type: "text" };
|
|
7
8
|
import { SESSION_CODE } from "./here";
|
|
9
|
+
import { KEEP_PANES, parseWorkspace } from "./pane-layout";
|
|
8
10
|
import { idProblem, runRootOf, stageNameProblem } from "./runs";
|
|
9
11
|
|
|
10
12
|
export const usage = help.trimEnd();
|
|
@@ -37,6 +39,8 @@ export type RunCommand = {
|
|
|
37
39
|
watch: boolean;
|
|
38
40
|
/** The sandbox every agent runs in, as `--sandbox`'s file gave it. */
|
|
39
41
|
sandbox?: unknown;
|
|
42
|
+
/** `--workspace` and `--keep-panes`, which the run records; absent, neither was given. */
|
|
43
|
+
panes?: RunPanes;
|
|
40
44
|
/** Started from an agent's shell: start the run in a tab and take that session over. */
|
|
41
45
|
here: boolean;
|
|
42
46
|
/** The code the calling session replies with, which the run finds it by. */
|
|
@@ -44,7 +48,12 @@ export type RunCommand = {
|
|
|
44
48
|
};
|
|
45
49
|
|
|
46
50
|
/** `awf run`'s options and workflow; `"help"` for a `-h` or `--help` anywhere before `--`. */
|
|
47
|
-
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" {
|
|
48
57
|
if (argv[0] !== "run") throw new Error("expected the run command");
|
|
49
58
|
const end = argv.indexOf("--");
|
|
50
59
|
const own = end < 0 ? argv : argv.slice(0, end);
|
|
@@ -63,6 +72,7 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand |
|
|
|
63
72
|
let sandbox: unknown;
|
|
64
73
|
let here = false;
|
|
65
74
|
let session: string | undefined;
|
|
75
|
+
const panes: RunPanes = {};
|
|
66
76
|
let workflowFile: string | undefined;
|
|
67
77
|
// awf's own options may come before or after the workflow file; only `--` ends them.
|
|
68
78
|
let index = 1;
|
|
@@ -91,8 +101,8 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand |
|
|
|
91
101
|
}
|
|
92
102
|
const value = argv[index + 1];
|
|
93
103
|
if (option === "--timeout") {
|
|
94
|
-
if (!value) throw new Error("--timeout needs a duration such as 30m");
|
|
95
|
-
timeoutMilliseconds = parseDuration(value);
|
|
104
|
+
if (!value) throw new Error("--timeout needs a duration such as 30m or 1d");
|
|
105
|
+
timeoutMilliseconds = parseDuration(value, "--timeout", deprecated);
|
|
96
106
|
timeout = value;
|
|
97
107
|
} else if (option === "--run-root") {
|
|
98
108
|
if (!value) throw new Error("--run-root needs a directory");
|
|
@@ -122,6 +132,16 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand |
|
|
|
122
132
|
if (!value || !SESSION_CODE.test(value))
|
|
123
133
|
throw new Error("--session needs the code --here printed");
|
|
124
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;
|
|
125
145
|
} else if (option === "--sandbox") {
|
|
126
146
|
if (!value) throw new Error("--sandbox needs a JSON file");
|
|
127
147
|
if (sandbox !== undefined) throw new Error("--sandbox given twice");
|
|
@@ -160,6 +180,7 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand |
|
|
|
160
180
|
here,
|
|
161
181
|
...(session === undefined ? {} : { session }),
|
|
162
182
|
...(sandbox === undefined ? {} : { sandbox }),
|
|
183
|
+
...(Object.keys(panes).length === 0 ? {} : { panes }),
|
|
163
184
|
};
|
|
164
185
|
}
|
|
165
186
|
|
package/src/run-commands.ts
CHANGED
package/src/run-prepare.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import {
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { type AttemptRecord, RUN_EVENT_VERSIONS, type RunPanes } from "@agentswf/contract/records";
|
|
4
5
|
import { isEventWorkflow, type JsonValue } from "@agentswf/contract/workflow";
|
|
5
6
|
import { refusalMessage } from "./attempt-view";
|
|
6
7
|
import { messageOf } from "./errors";
|
|
8
|
+
import { exists } from "./files";
|
|
7
9
|
import { projectOf, projectRunRoot } from "./projects";
|
|
8
10
|
import type { RunCommand } from "./run-command";
|
|
9
11
|
import {
|
|
@@ -14,6 +16,7 @@ import {
|
|
|
14
16
|
checkFree,
|
|
15
17
|
claimAttempt,
|
|
16
18
|
endTurnsLine,
|
|
19
|
+
generateId,
|
|
17
20
|
idProblem,
|
|
18
21
|
openRun,
|
|
19
22
|
previousGone,
|
|
@@ -38,6 +41,14 @@ export type PreparedRun = {
|
|
|
38
41
|
continued?: { run: Run; attempts: AttemptRecord[]; warnings: string[] };
|
|
39
42
|
};
|
|
40
43
|
|
|
44
|
+
/** `awf run --id` of a workflow with events, which starts the run detached, or wakes it. */
|
|
45
|
+
export type OpensEventRun = {
|
|
46
|
+
loaded: LoadedWorkflow;
|
|
47
|
+
id: string;
|
|
48
|
+
argv: readonly string[];
|
|
49
|
+
panes?: RunPanes;
|
|
50
|
+
};
|
|
51
|
+
|
|
41
52
|
/**
|
|
42
53
|
* The command's workflow loaded and its run prepared, or why not, as awf says it: in the shell that
|
|
43
54
|
* typed the command, before anything starts.
|
|
@@ -49,7 +60,7 @@ export async function loadAndPrepare(
|
|
|
49
60
|
): Promise<
|
|
50
61
|
| { loaded: LoadedWorkflow; prepared: PreparedRun; command: RunCommand }
|
|
51
62
|
| { refused: string }
|
|
52
|
-
| { opens:
|
|
63
|
+
| { opens: OpensEventRun }
|
|
53
64
|
> {
|
|
54
65
|
let loaded: LoadedWorkflow;
|
|
55
66
|
try {
|
|
@@ -74,12 +85,23 @@ export async function loadAndPrepare(
|
|
|
74
85
|
try {
|
|
75
86
|
const id =
|
|
76
87
|
command.id ??
|
|
77
|
-
derivedId(loaded.workflow, prepareArgs(loaded.workflow, command.workflowArgs, project))
|
|
88
|
+
derivedId(loaded.workflow, prepareArgs(loaded.workflow, command.workflowArgs, project)) ??
|
|
89
|
+
// Without starts no sender names its runs, so one may be generated, as a plain run's is.
|
|
90
|
+
(loaded.workflow.starts === undefined
|
|
91
|
+
? await unusedId(command.runRoot, loaded.workflow.meta.name, now)
|
|
92
|
+
: undefined);
|
|
78
93
|
if (id === undefined)
|
|
79
94
|
return {
|
|
80
|
-
refused: `a run of ${loaded.file} is named by its id, which --id or the workflow's id(args) gives: awf run --id {id} starts one, awf emit --target ${loaded.file} --id {id} {event} {json} starts one with an event`,
|
|
95
|
+
refused: `a run of ${loaded.file}, which starts on events, is named by its id, which --id or the workflow's id(args) gives: awf run --id {id} starts one, awf emit --target ${loaded.file} --id {id} {event} {json} starts one with an event`,
|
|
81
96
|
};
|
|
82
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
opens: {
|
|
99
|
+
loaded,
|
|
100
|
+
id,
|
|
101
|
+
argv: command.workflowArgs,
|
|
102
|
+
...(command.panes ? { panes: command.panes } : {}),
|
|
103
|
+
},
|
|
104
|
+
};
|
|
83
105
|
} catch (error) {
|
|
84
106
|
return { refused: `${error instanceof RunRefused ? "" : "prepare: "}${messageOf(error)}` };
|
|
85
107
|
}
|
|
@@ -115,6 +137,7 @@ async function prepareRun(
|
|
|
115
137
|
argv: command.workflowArgs,
|
|
116
138
|
...(command.cwdGiven ? { cwd: command.cwd } : {}),
|
|
117
139
|
...(command.sandbox === undefined ? {} : { sandbox: command.sandbox }),
|
|
140
|
+
...(command.panes === undefined ? {} : { panes: command.panes }),
|
|
118
141
|
});
|
|
119
142
|
const { id, argv, cwd } = run.record;
|
|
120
143
|
if (!statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) {
|
|
@@ -155,13 +178,28 @@ export function prepareArgs(
|
|
|
155
178
|
return args;
|
|
156
179
|
}
|
|
157
180
|
|
|
181
|
+
/**
|
|
182
|
+
* A generated id no run of `workflow` holds: `awf run` of one already there would wake it. Taken
|
|
183
|
+
* between this and the open, by a run started the same minute with the same four digits, it is.
|
|
184
|
+
*/
|
|
185
|
+
async function unusedId(runRoot: string, workflow: string, now: number): Promise<string> {
|
|
186
|
+
for (let tries = 1; ; tries += 1) {
|
|
187
|
+
const id = generateId(new Date(now));
|
|
188
|
+
if (!(await exists(join(runRoot, workflow, id)))) return id;
|
|
189
|
+
if (tries >= 5) throw new RunRefused("every id generated was taken; --id gives one");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
158
193
|
/**
|
|
159
194
|
* Why `awf run` doesn't take a workflow with `events` as asked: every attempt after the first is
|
|
160
|
-
* started by a wake, which passes on none of the flags a run would have to keep.
|
|
195
|
+
* started by a wake, which passes on none of the flags a run would have to keep. A `--from-stage`
|
|
196
|
+
* is the one attempt's, and its records keep what it redid.
|
|
161
197
|
*/
|
|
162
198
|
function eventRunRefusal(command: RunCommand): string | undefined {
|
|
163
|
-
if (command.
|
|
164
|
-
return "a run of a workflow with events takes no --from-stage
|
|
199
|
+
if (command.values !== undefined)
|
|
200
|
+
return "a run of a workflow with events takes no --values: before --from-stage, a stage with no record runs";
|
|
201
|
+
if (command.fromStage !== undefined && command.continueId === undefined)
|
|
202
|
+
return "a run of a workflow with events takes --from-stage only with --continue";
|
|
165
203
|
if (command.continueId !== undefined) {
|
|
166
204
|
if (command.workflowArgs.length > 0)
|
|
167
205
|
return "a continue takes no arguments: it goes on with those its run was started with";
|
package/src/run-usage.ts
CHANGED
|
@@ -35,7 +35,7 @@ export type AccountedAgent = {
|
|
|
35
35
|
sessions(): readonly string[];
|
|
36
36
|
/** When its host first prompted it, where that waits on the agent first; see `window`. */
|
|
37
37
|
promptedAt?(): number | undefined;
|
|
38
|
-
/** The caller handed back early by
|
|
38
|
+
/** The caller handed back early by its close(). */
|
|
39
39
|
releasedAt?(): number | undefined;
|
|
40
40
|
/** When a fork's session was made: what it holds from before is its parent's (ADR 0009). */
|
|
41
41
|
forkedAt?(): number | undefined;
|