@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.
Files changed (72) hide show
  1. package/package.json +5 -6
  2. package/src/agent-cli/cli.ts +611 -0
  3. package/src/agent-cli/client.ts +115 -0
  4. package/src/agent-launcher.ts +15 -25
  5. package/src/allowance-command.ts +3 -15
  6. package/src/attempt-ending.ts +2 -4
  7. package/src/attempt-output.ts +3 -2
  8. package/src/attempt-view.ts +1 -1
  9. package/src/control-plane.ts +38 -6
  10. package/src/coordinator-socket.ts +15 -6
  11. package/src/deadlines.ts +69 -2
  12. package/src/decisions/directory.ts +9 -11
  13. package/src/deprecations.ts +177 -0
  14. package/src/duration.ts +50 -20
  15. package/src/emit-command.ts +57 -54
  16. package/src/emit.ts +124 -15
  17. package/src/help/allowance.txt +13 -0
  18. package/src/help/emit.txt +21 -0
  19. package/src/help/inbox.txt +12 -0
  20. package/src/help/init.txt +12 -0
  21. package/src/help/logs.txt +14 -0
  22. package/src/help/reply.txt +18 -0
  23. package/src/help/run.txt +58 -0
  24. package/src/help/runs.txt +11 -0
  25. package/src/help/test.txt +9 -0
  26. package/src/help/tick.txt +12 -0
  27. package/src/herdr-run-session.ts +2 -2
  28. package/src/here.ts +1 -1
  29. package/src/holder-command.ts +38 -51
  30. package/src/inbox-command.ts +9 -24
  31. package/src/init-command.ts +2 -11
  32. package/src/init-template/AGENTS.md.template +3 -3
  33. package/src/init-template/hello/workflow.test.ts.template +3 -3
  34. package/src/init-template/hello/workflow.ts.template +6 -6
  35. package/src/keep-alive.ts +28 -13
  36. package/src/logs-command.ts +14 -27
  37. package/src/mailbox.ts +16 -21
  38. package/src/messaging/limits.ts +5 -5
  39. package/src/messaging/messaging.ts +48 -43
  40. package/src/messaging/routes.ts +2 -2
  41. package/src/messaging/text.ts +24 -22
  42. package/src/operation-liveness.ts +1 -1
  43. package/src/operator-cli.ts +23 -9
  44. package/src/operator-runtime.ts +9 -12
  45. package/src/pane-layout.ts +125 -32
  46. package/src/progress-view.ts +8 -8
  47. package/src/result-validation.ts +1 -1
  48. package/src/run-command.ts +27 -47
  49. package/src/run-commands.ts +1 -1
  50. package/src/run-prepare.ts +46 -8
  51. package/src/run-usage.ts +2 -2
  52. package/src/run-view.ts +9 -4
  53. package/src/runs-command.ts +14 -23
  54. package/src/runs.ts +44 -8
  55. package/src/sandboxes.ts +3 -2
  56. package/src/simple-input.ts +13 -3
  57. package/src/sources.ts +42 -6
  58. package/src/stage-ledger.ts +43 -13
  59. package/src/stage-plan.ts +37 -1
  60. package/src/test-command.ts +9 -14
  61. package/src/testing.ts +8 -9
  62. package/src/tick-command.ts +2 -13
  63. package/src/tools.ts +62 -25
  64. package/src/workflow-loader.ts +5 -2
  65. package/src/workflow-runner.ts +636 -271
  66. package/src/workflow-testing/coordinator.typecheck.ts +12 -13
  67. package/src/workflow-testing/coordinators.ts +2 -2
  68. package/src/workflow-testing/host.ts +115 -46
  69. package/src/workflow-testing/index.ts +45 -837
  70. package/src/workflow-testing/run-test.ts +1213 -0
  71. package/src/workflow-testing/script.ts +106 -45
  72. 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 `wf waiting` short, and an
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") {
@@ -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 { claimNext, loadAndPrepare, type PreparedRun, workspaceLabel } from "./run-prepare";
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 }: { loaded: LoadedWorkflow; id: string; argv: readonly string[] },
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(loaded.workflow as EventWorkflow, id, argv, {
263
- home: environment.home ?? homedir(),
264
- environment: environment.environment ?? process.env,
265
- ...(environment.launch ? { launch: environment.launch } : {}),
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 pane agent's `openRun`, inside `startWorkflow`, by
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
  : {
@@ -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 `workflow.allowance` reads a harness's plan, as `awf allowance` does. */
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 pane agent's session is ready. */
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 pane agent, a subprocess per turn 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 pane agent.
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 pane agent opens, so an all-headless run never
122
+ // The session is made ready when the first TUI agent opens, so an all-headless run never
123
123
  // calls Herdr.
124
- pane: {
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 `wf` is a launcher the control plane installs beside its own
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. srt is the default when installed, as it runs
286
- * the project's own toolchain, which an image lacks (story 004), and when it is there but cannot
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
  }
@@ -1,4 +1,11 @@
1
- import type { KeepPane, PaneLayout, TurnOutcome } from "@agentswf/contract/workflow";
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 KEEP: readonly KeepPane[] = ["never", "on-failure", "always"];
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 either on an agent
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: { layout?: unknown; keepPane?: unknown },
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
- if (agent.headless) refuse("layout and keepPane place a pane, and a headless agent has none");
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.keepPane !== undefined && !KEEP.includes(spec.keepPane as KeepPane)) {
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.layout === undefined) return;
35
- const layout = spec.layout;
36
- if (typeof layout !== "object" || layout === null || Array.isArray(layout)) {
37
- refuse(`a layout is an object, not ${JSON.stringify(layout)}`);
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 fields = Object.keys(layout as object).filter(
40
- (field) => (layout as Record<string, unknown>)[field] !== undefined,
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
- const given = layout as Record<string, unknown>;
43
- if (given.beside !== undefined) {
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(`a layout beside another pane takes side and share, not ${others.join(", ")}`);
75
+ refuse(`${a} beside another pane takes side and share, not ${others.join(", ")}`);
47
76
  }
48
- if (typeof given.beside !== "string" || given.beside === "") {
49
- refuse(`beside names an agent's key, not ${JSON.stringify(given.beside)}`);
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 (given.beside === key) refuse("a pane cannot be placed beside itself");
52
- if (given.side !== "right" && given.side !== "below") {
53
- refuse(`side is right or below, not ${JSON.stringify(given.side)}`);
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 = given.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 others = fields.filter((field) => !NEW_TAB_FIELDS.has(field));
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
- `a layout takes session, workspace and tab, or beside and side; not ${others.join(", ")}`,
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 } = given;
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, so a reopen
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: PaneLayout): PaneLayout {
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(([, value]) => value !== undefined),
103
- ) as PaneLayout;
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
+ }
@@ -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 pane agents opened in, once one has. */
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 ${oneLine(turn.waitingReason ?? "a reply")}${turn.checkInAt === undefined ? "" : ` · until ${clock(turn.checkInAt - view.startedAt)}`}`
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 ${oneLine(turn.waitingReason ?? "a reply")} · ${duration(Math.max(0, now - since))}${of}`;
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 pane agent Herdr read as blocked on a prompt: its session plays no sound, so this
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) === "pane" &&
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 pane agents are, said once as the first opens; the workspace closes with the run.
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 pane agent's session can be ready after the run has ended, as a cancel lands while it starts:
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 pane agents opened in, which a blocked one's line names. */
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
  },
@@ -8,7 +8,7 @@ export type CandidateEvaluation =
8
8
  | { kind: "rejected"; error: string };
9
9
 
10
10
  /**
11
- * A result as `wf` sent it, validated without repairing or persisting; the result-slot transition
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(
@@ -1,54 +1,15 @@
1
1
  import { readFileSync, statSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import type { JsonValue } from "@agentswf/contract/workflow";
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(argv: readonly string[], cwd: string): RunCommand | "help" {
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
 
@@ -137,7 +137,7 @@ export function nextLogs(
137
137
  ...filters,
138
138
  "--after",
139
139
  encodeCursor(cursor),
140
- ...(wait ? ["--wait", "90"] : []),
140
+ ...(wait ? ["--wait", "90s"] : []),
141
141
  ];
142
142
  }
143
143