@agentswf/engine 0.1.0-beta.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +16 -0
  3. package/package.json +13 -9
  4. package/src/accounting/format.ts +4 -3
  5. package/src/accounting/spent.ts +128 -0
  6. package/src/accounting/summary.ts +40 -32
  7. package/src/agent-cli/cli.ts +28 -55
  8. package/src/agent-cli/client.ts +1 -27
  9. package/src/allowance-command.ts +8 -10
  10. package/src/attempt-ending.ts +4 -4
  11. package/src/attempt-output.ts +13 -31
  12. package/src/attempt-view.ts +33 -32
  13. package/src/author-typebox.ts +13 -0
  14. package/src/client/checks.ts +94 -0
  15. package/src/client/exported.ts +17 -0
  16. package/src/client/index.ts +122 -0
  17. package/src/client/locate.ts +34 -0
  18. package/src/client/published.ts +3 -0
  19. package/src/client/runs.ts +337 -0
  20. package/src/command-line.ts +64 -0
  21. package/src/commands.ts +7 -1
  22. package/src/control-plane.ts +4 -6
  23. package/src/coordinator-prompt.ts +4 -3
  24. package/src/coordinator-socket.ts +27 -28
  25. package/src/deadlines.ts +6 -40
  26. package/src/decisions/directory.ts +50 -37
  27. package/src/decisions/fake.ts +1 -1
  28. package/src/decisions/openrouter.ts +4 -2
  29. package/src/decisions/seam.ts +4 -4
  30. package/src/display-path.ts +3 -3
  31. package/src/duration.ts +19 -33
  32. package/src/emit-command.ts +38 -22
  33. package/src/emit.ts +148 -59
  34. package/src/event-ids.ts +31 -0
  35. package/src/files.ts +44 -4
  36. package/src/hand-over.ts +150 -0
  37. package/src/help/allowance.txt +4 -3
  38. package/src/help/emit.txt +11 -7
  39. package/src/help/inbox.txt +5 -3
  40. package/src/help/logs.txt +7 -5
  41. package/src/help/open.txt +29 -0
  42. package/src/help/reply.txt +6 -8
  43. package/src/help/run.txt +13 -8
  44. package/src/help/runs.txt +4 -2
  45. package/src/help/tick.txt +9 -4
  46. package/src/herdr-placement.ts +3 -33
  47. package/src/herdr-run-session.ts +5 -13
  48. package/src/herdr-workspace-marks.ts +7 -17
  49. package/src/holder-command.ts +54 -136
  50. package/src/in-lines.ts +28 -0
  51. package/src/inbox-command.ts +42 -56
  52. package/src/index.ts +1 -1
  53. package/src/init-command.ts +8 -17
  54. package/src/init-template/hello/workflow.ts.template +3 -2
  55. package/src/jsonl.ts +10 -6
  56. package/src/keep-alive.ts +7 -19
  57. package/src/logs-command.ts +134 -53
  58. package/src/message-operation.ts +6 -3
  59. package/src/messaging/messaging.ts +8 -10
  60. package/src/messaging/text.ts +2 -2
  61. package/src/open-command.ts +770 -0
  62. package/src/operation-events.ts +4 -6
  63. package/src/operation-liveness.ts +19 -4
  64. package/src/operator-cli.ts +206 -90
  65. package/src/operator-runtime.ts +24 -2
  66. package/src/ops.ts +270 -0
  67. package/src/pane-layout.ts +49 -96
  68. package/src/progress-view.ts +2 -2
  69. package/src/project-engine.ts +79 -0
  70. package/src/projects.ts +13 -2
  71. package/src/refusals.ts +119 -0
  72. package/src/result-slots.ts +11 -31
  73. package/src/result-validation.ts +2 -2
  74. package/src/run-command.ts +27 -31
  75. package/src/run-commands.ts +42 -114
  76. package/src/run-event-log.ts +1 -1
  77. package/src/run-lines.ts +159 -0
  78. package/src/run-lock.ts +57 -1
  79. package/src/run-policy/stage-plan.ts +154 -0
  80. package/src/run-policy/stages.ts +113 -0
  81. package/src/run-policy/waits.ts +452 -0
  82. package/src/run-prepare.ts +165 -41
  83. package/src/run-read.ts +109 -41
  84. package/src/run-state.ts +170 -50
  85. package/src/run-text.ts +54 -0
  86. package/src/run-unreachable.ts +18 -0
  87. package/src/run-usage.ts +17 -11
  88. package/src/run-view.ts +73 -14
  89. package/src/runs-command.ts +81 -63
  90. package/src/runs.ts +284 -222
  91. package/src/sandbox-homes.ts +65 -13
  92. package/src/sandboxes.ts +182 -28
  93. package/src/session-lines.ts +93 -0
  94. package/src/skills/run-skills.ts +8 -1
  95. package/src/sources.ts +2 -39
  96. package/src/spend.ts +333 -0
  97. package/src/stage-ledger.ts +111 -153
  98. package/src/stop-crashed.ts +6 -5
  99. package/src/stopped.ts +4 -4
  100. package/src/test-command.ts +37 -19
  101. package/src/testing/operation-events.ts +3 -23
  102. package/src/testing.ts +66 -18
  103. package/src/tick-command.ts +145 -15
  104. package/src/tools.ts +11 -30
  105. package/src/turn-ends.ts +37 -0
  106. package/src/waits.ts +651 -0
  107. package/src/wake.ts +84 -54
  108. package/src/workflow-loader.ts +49 -2
  109. package/src/workflow-runner.ts +525 -618
  110. package/src/workflow-test-preload.ts +4 -1
  111. package/src/workflow-testing/coordinators.ts +1 -1
  112. package/src/workflow-testing/fake-client.ts +387 -0
  113. package/src/workflow-testing/host.ts +28 -29
  114. package/src/workflow-testing/index.ts +9 -27
  115. package/src/workflow-testing/run-test.ts +201 -286
  116. package/src/workflow-testing/script.ts +7 -39
  117. package/src/deprecations.ts +0 -177
  118. package/src/hold-run-lock.fixture.ts +0 -8
  119. package/src/mailbox.ts +0 -972
  120. package/src/stage-plan.ts +0 -186
  121. package/src/workflow-testing/coordinator.typecheck.ts +0 -54
  122. package/src/workflow-testing/workflow-testing.typecheck.ts +0 -67
@@ -1,14 +1,14 @@
1
1
  import { mkdtempSync, rmSync } from "node:fs";
2
- import { mkdir } from "node:fs/promises";
2
+ import { appendFile, mkdir } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import {
6
- MAILBOX_RECORD_VERSION,
7
- type MailboxRecord,
6
+ type EventLine,
7
+ type RunLine,
8
+ type RunPanes,
9
+ readRun,
8
10
  type SandboxRecord,
9
- STAGE_RECORD_VERSION,
10
- type StageNeed,
11
- type StageRecord,
11
+ type StageLine,
12
12
  } from "@agentswf/contract/records";
13
13
  import {
14
14
  AWF_ID_PREFIX,
@@ -20,31 +20,30 @@ import {
20
20
  isEventWorkflow,
21
21
  type JsonObject,
22
22
  type JsonValue,
23
+ type KeepPane,
23
24
  type OutputOf,
24
- type ParticipantReply,
25
25
  positiveDurationMilliseconds,
26
26
  type RuntimeAliases,
27
27
  WORKFLOW_KIND,
28
28
  type Workflow,
29
29
  } from "@agentswf/contract/workflow";
30
- import { type Harness, sandboxTokens } from "@agentswf/harness";
30
+ import type { Harness } from "@agentswf/harness";
31
+ import { SCRIPTED_HOMES } from "@agentswf/harness/testing";
31
32
  import { createFakeSandboxProvider } from "@agentswf/sandbox/testing/fake";
32
33
  import { decideEnding, type Finished } from "../attempt-ending";
33
34
  import { continueCommand } from "../attempt-view";
34
- import { type Deprecated, RunDeprecations } from "../deprecations";
35
35
  import { checkEvent, eventIdProblem, replyRecord } from "../emit";
36
36
  import { messageOf } from "../errors";
37
- import { appendMailbox, foldMailbox, RunParked, readMailbox } from "../mailbox";
37
+ import { isInside } from "../files";
38
+ import { appendIn, readIn } from "../in-lines";
38
39
  import { OPERATOR_ALIASES } from "../operator-aliases";
39
- import {
40
- createRun,
41
- idProblem,
42
- readEvents,
43
- readStageRecords,
44
- readTurns,
45
- writeStageRecord,
46
- } from "../runs";
47
- import { primaryFailure, WorkflowStopped } from "../stopped";
40
+ import { type EarlierOps, listOps } from "../ops";
41
+ import { KEEP_PANES } from "../pane-layout";
42
+ import { claimRunLines, type RunLines, readRunLines, runLinesFile } from "../run-lines";
43
+ import type { TimerState } from "../run-policy/waits";
44
+ import { createRun, idProblem, readEvents, readTurns } from "../runs";
45
+ import { primaryFailure, type WorkflowStopped } from "../stopped";
46
+ import { RunParked, readWaits } from "../waits";
48
47
  import { assertJsonValue, type EventWorkflow } from "../workflow-loader";
49
48
  import type { RunnableWorkflow } from "../workflow-runner";
50
49
  import { runWorkflow, type SettledRun, WorkflowRunError } from "../workflow-runner";
@@ -112,18 +111,27 @@ type EventOptions<Events extends EventSchemas> = [Events] extends [never]
112
111
  events?: readonly ScriptedEvent<Events>[];
113
112
  };
114
113
 
115
- /** What an earlier attempt left: given, the test's attempt is that run's continue, attempt 2. */
114
+ /**
115
+ * What an earlier attempt left: given, the test's attempt is that run's continue, the attempt after
116
+ * the latest its lines name, attempt 2 when they name none later than 1.
117
+ */
116
118
  export type EarlierAttempt = {
117
119
  /**
118
120
  * The stages it recorded, by name: each value, or `undefined` for a stage that returns nothing,
119
- * reused without calling their work, as `awf run --continue` would.
121
+ * reused without calling their work, as `awf run --continue` would. Written as attempt 1's
122
+ * `stage` lines.
120
123
  */
121
124
  stages?: Readonly<Record<string, JsonValue | undefined>>;
122
125
  /**
123
- * What it left in the run's mailbox, in order: its events, timers, and what its waits and turns
124
- * wrote. A starting `event` is appended after them, as the event that woke it.
126
+ * The events senders gave the run, its `in.jsonl`, in order. A starting `event` is appended
127
+ * after them, as the event that woke it.
128
+ */
129
+ events?: readonly EventLine[];
130
+ /**
131
+ * What the run settled, its `run.jsonl` lines, in order, after `stages`: its timers, what its
132
+ * waits and races took, and the turns it began, as `run.run` gives them.
125
133
  */
126
- mailbox?: readonly MailboxRecord[];
134
+ run?: readonly RunLine[];
127
135
  };
128
136
 
129
137
  type CommonTestOptions = {
@@ -161,12 +169,8 @@ type CommonTestOptions = {
161
169
  runtimes?: RuntimeAliases;
162
170
  /** The run's deadline; none unless given, as `awf run` has none unless `--timeout` gives one. */
163
171
  timeout?: Duration;
164
- /** @deprecated Give `timeout`, a `Duration` such as `"5m"`. */
165
- timeoutMs?: number;
166
172
  /** Real time with no turn or decision starting or ending before the test fails; `"2s"`. */
167
173
  stall?: Duration;
168
- /** @deprecated Give `stall`, a `Duration` such as `"2s"`. */
169
- stallMs?: number;
170
174
  /**
171
175
  * The run's clock is virtual: where a run would park with a timer to come, the clock moves to it
172
176
  * and the timer fires, so a two-day reminder takes no time. After this many, the run parks
@@ -190,10 +194,6 @@ type CommonTestOptions = {
190
194
  maxTurns?: number;
191
195
  /** What an earlier attempt left; given, the test's attempt is its continue. */
192
196
  earlier?: EarlierAttempt;
193
- /** @deprecated Give `earlier: { stages }`. */
194
- recorded?: Readonly<Record<string, JsonValue | undefined>>;
195
- /** @deprecated Give `earlier: { mailbox }`. */
196
- mailbox?: readonly MailboxRecord[];
197
197
  /**
198
198
  * The stage the attempt starts at, as `--from-stage`: a continue of `earlier`, or a new run
199
199
  * started there. A workflow with events takes it only with `earlier`, as `awf run` takes it only
@@ -205,6 +205,12 @@ type CommonTestOptions = {
205
205
  * a workflow with events takes none.
206
206
  */
207
207
  values?: Readonly<Record<string, JsonValue>>;
208
+ /**
209
+ * Where the run's TUI agents' panes go and whether they stay, as `awf run --workspace` and
210
+ * `--keep-panes` give them: `run.agents` shows what each agent's host was asked. Absent, the
211
+ * run's workspace, and no pane kept.
212
+ */
213
+ panes?: RunPanes;
208
214
  };
209
215
 
210
216
  /** Every option a test may give, whatever its workflow's args and events: what the run reads. */
@@ -234,54 +240,34 @@ const OPTION_KEYS: readonly (keyof AnyTestOptions)[] = [
234
240
  "earlier",
235
241
  "fromStage",
236
242
  "values",
243
+ "panes",
237
244
  "event",
238
245
  "events",
239
246
  ];
240
- /** Taken, logged, until story 039's last task removes them; never suggested. */
241
- const DEPRECATED_KEYS: readonly (keyof AnyTestOptions)[] = [
242
- "timeoutMs",
243
- "stallMs",
244
- "recorded",
245
- "mailbox",
246
- ];
247
- const EARLIER_KEYS: readonly (keyof EarlierAttempt)[] = ["stages", "mailbox"];
247
+ const EARLIER_KEYS: readonly (keyof EarlierAttempt)[] = ["stages", "events", "run"];
248
248
 
249
249
  /**
250
- * A test's options as `testWorkflow` was called: `(workflow, options?)`, or the deprecated
251
- * `(workflow, args, options)`. Three arguments are always the deprecated form. With two, the second
252
- * is the options when it is an object, and args otherwise (`null`, a string, a number, a boolean or
253
- * an array), so a workflow's object args are never taken for args there: they are read as options,
254
- * and refused by their first key.
250
+ * A test's options as `testWorkflow` was called, `(workflow, options?)`: a workflow's args given
251
+ * second, or options third, are refused, naming `{ args }`; object args given second are named by
252
+ * their first key.
255
253
  */
256
- export function testCall(
257
- workflow: unknown,
258
- given: readonly unknown[],
259
- ): { options: AnyTestOptions; used: Deprecated[] } {
260
- const deprecatedForm =
261
- given.length >= 2 || (given.length === 1 && given[0] !== undefined && !isObject(given[0]));
262
- if (!deprecatedForm) {
263
- const options = plainOptions(given[0] ?? {});
264
- // Object args given second are named by their key, whatever the workflow is.
265
- const problem = optionsProblem(
266
- options,
267
- isEventWorkflow(workflow),
268
- (workflow as { meta?: { name?: string } } | null)?.meta?.name ?? "the workflow",
254
+ export function testCall(workflow: unknown, given: readonly unknown[]): AnyTestOptions {
255
+ if (given.length > 1)
256
+ throw new Error(
257
+ "testWorkflow takes (workflow, options?): give args among the options, testWorkflow(workflow, { args, … })",
269
258
  );
270
- if (problem) throw new Error(problem);
271
- if (!isWorkflow(workflow))
272
- throw new Error(
273
- "testWorkflow(workflow, options?) tests a workflow defineWorkflow made: define it with defineWorkflow",
274
- );
275
- return { options, used: [] };
276
- }
277
- // The args-second form never existed for a workflow with events: refused, not deprecated.
278
- if (isEventWorkflow(workflow))
279
- throw new Error("a workflow with events is tested as testWorkflow(workflow, { id, event? })");
280
- const [args, optionsGiven = {}] = given;
281
- const options = plainOptions(optionsGiven);
282
- if ("args" in options)
283
- throw new Error("args are given once: testWorkflow(workflow, { args, … }) is the one form");
284
- return { options: { ...options, args: args as JsonValue }, used: ["test-args"] };
259
+ const options = plainOptions(given[0] === undefined ? {} : given[0]);
260
+ const problem = optionsProblem(
261
+ options,
262
+ isEventWorkflow(workflow),
263
+ (workflow as { meta?: { name?: string } } | null)?.meta?.name ?? "the workflow",
264
+ );
265
+ if (problem) throw new Error(problem);
266
+ if (!isWorkflow(workflow))
267
+ throw new Error(
268
+ "testWorkflow(workflow, options?) tests a workflow defineWorkflow made: define it with defineWorkflow",
269
+ );
270
+ return options;
285
271
  }
286
272
 
287
273
  function isObject(value: unknown): value is AnyTestOptions {
@@ -320,56 +306,58 @@ function optionsProblem(
320
306
  name: string,
321
307
  ): string | undefined {
322
308
  for (const key of Object.keys(options) as (keyof AnyTestOptions)[]) {
323
- if (!OPTION_KEYS.includes(key) && !DEPRECATED_KEYS.includes(key))
309
+ if (!OPTION_KEYS.includes(key))
324
310
  return `testWorkflow has no option ${JSON.stringify(key)}: its options are ${OPTION_KEYS.join(", ")}${
325
311
  "args" in options ? "" : "; a workflow's args are given as { args }"
326
312
  }`;
327
313
  if (!withEvents && (key === "event" || key === "events"))
328
314
  return `${key} is an option for a workflow with events, and ${name} declares none`;
329
315
  }
316
+ const panes = panesProblem(options.panes);
317
+ if (panes) return panes;
330
318
  const { earlier } = options;
331
319
  if (earlier === undefined) return undefined;
332
- if (!isObject(earlier)) return "earlier is an object: { stages?, mailbox? }";
320
+ if (!isObject(earlier)) return "earlier is an object: { stages?, events?, run? }";
333
321
  const unknown = Object.keys(earlier).find(
334
322
  (key) => !EARLIER_KEYS.includes(key as keyof EarlierAttempt),
335
323
  );
336
324
  return unknown === undefined
337
325
  ? undefined
338
- : `earlier has no ${JSON.stringify(unknown)}: it gives ${EARLIER_KEYS.join(" and ")}`;
326
+ : `earlier has no ${JSON.stringify(unknown)}: it gives ${EARLIER_KEYS.join(", ")}`;
327
+ }
328
+
329
+ /** Why `panes` isn't what `awf run --workspace` and `--keep-panes` could give; undefined when it is. */
330
+ function panesProblem(panes: unknown): string | undefined {
331
+ if (panes === undefined) return undefined;
332
+ if (!isObject(panes)) return "panes is an object: { workspace?, keep? }";
333
+ const { workspace, keep, ...rest } = panes as Record<string, unknown>;
334
+ const other = Object.keys(rest)[0];
335
+ if (other !== undefined) return `panes has no ${JSON.stringify(other)}: it gives workspace, keep`;
336
+ if (keep !== undefined && !KEEP_PANES.includes(keep as KeepPane))
337
+ return `panes.keep is ${KEEP_PANES.join(", ")}, not ${JSON.stringify(keep)}`;
338
+ const named =
339
+ isObject(workspace) &&
340
+ Object.keys(workspace).join() === "name" &&
341
+ typeof (workspace as { name?: unknown }).name === "string" &&
342
+ (workspace as { name: string }).name.trim() !== "";
343
+ if (workspace !== undefined && workspace !== "run" && workspace !== "origin" && !named)
344
+ return `panes.workspace is "run", "origin" or { name }, not ${JSON.stringify(workspace)}`;
345
+ return undefined;
339
346
  }
340
347
 
341
348
  /** What a test of the engine, not of a workflow, may give `runTest`. */
342
- export type EngineTestOptions = {
349
+ type EngineTestOptions = {
343
350
  /**
344
351
  * Where an agent may `--await-reply`: `listed`, where the engine's list allows it for the
345
352
  * agent's harness and placement, as a run would; `anywhere`, on any harness, the fake's included.
346
353
  */
347
354
  awaitReply?: "listed" | "anywhere";
348
- /** Deprecated forms the call itself used, logged with the run's own. */
349
- used?: readonly Deprecated[];
350
355
  };
351
356
 
352
- /**
353
- * A test's length of time in milliseconds: `field`, a `Duration`, or `old`, its deprecated form in
354
- * milliseconds, which is reported; both at once are refused.
355
- */
356
- function milliseconds(
357
- options: AnyTestOptions,
358
- field: "timeout" | "stall",
359
- old: "timeoutMs" | "stallMs",
360
- deprecated: () => void,
361
- ): number | undefined {
357
+ /** A test's length of time in milliseconds, a `Duration` checked; undefined when not given. */
358
+ function milliseconds(options: AnyTestOptions, field: "timeout" | "stall"): number | undefined {
362
359
  const given = options[field];
363
- const ms = options[old];
364
- if (given != null) {
365
- if (ms != null) throw new Error(`give ${field} alone: ${old} is its deprecated form`);
366
- return positiveDurationMilliseconds(given, `a test's ${field}`);
367
- }
368
- if (ms == null) return undefined;
369
- if (!Number.isSafeInteger(ms) || ms <= 0)
370
- throw new Error(`a test's ${old} is a positive whole number of milliseconds: not ${ms}`);
371
- deprecated();
372
- return ms;
360
+ return given == null ? undefined : positiveDurationMilliseconds(given, `a test's ${field}`);
373
361
  }
374
362
 
375
363
  /** A file's contents, or a directory of them by name. */
@@ -382,41 +370,24 @@ const WAITED_OUT = "the test ended the attempt: everything left waited on a part
382
370
 
383
371
  /**
384
372
  * An event a test sends while the run goes on, at an agent's `n`th turn, `"{agent}#{n}"`: `during`
385
- * it, or `after` it has ended, once the run waits live in an agent's `keepAlive` window.
373
+ * it, or `after` it has ended, once the run waits live in an agent's `keepAlive` window. `n` counts
374
+ * the agent's turns in the test, a nudge's included, and not its compactions: not the `n` of its
375
+ * operation's id, `{agent}.{n}`, which counts each operation once and its compactions too.
386
376
  */
387
377
  export type ScriptedEvent<Events extends EventSchemas> = (
388
378
  | { during: string; after?: undefined }
389
379
  | { after: string; during?: undefined }
390
380
  ) &
391
381
  EventIds &
392
- (
393
- | {
394
- [Name in Exclude<keyof Events & string, `${string}:ended`>]: {
395
- name: Name;
396
- data: OutputOf<Events[Name]>;
397
- };
398
- }[Exclude<keyof Events & string, `${string}:ended`>]
399
- /**
400
- * A reply on a participant routed to the code with `to: "code"`; awf fills its author and id
401
- * when absent.
402
- *
403
- * @deprecated Declare `{key}:reply: ParticipantReply` and script `{ name: "{key}:reply", data }`.
404
- */
405
- | { name: "participant:reply"; from: string; data: ParticipantReply }
406
- );
407
-
408
- /** An event's id in the run's mailbox, as `awf emit --event-id` gives it. */
409
- type EventIds = {
410
- eventId?: string;
411
- /** @deprecated Give it as `eventId`. */
412
- id?: string;
413
- };
382
+ {
383
+ [Name in Exclude<keyof Events & string, `${string}:ended`>]: {
384
+ name: Name;
385
+ data: OutputOf<Events[Name]>;
386
+ };
387
+ }[Exclude<keyof Events & string, `${string}:ended`>];
414
388
 
415
- /** @deprecated Use `TestOptions<Args, Events>`, the one options type. */
416
- export type EventTestOptions<
417
- Events extends EventSchemas,
418
- Args extends JsonValue = JsonValue,
419
- > = TestOptions<Args, Events>;
389
+ /** An event's id in the run's `in.jsonl`, as `awf emit --event-id` gives it. */
390
+ type EventIds = { eventId?: string };
420
391
 
421
392
  /** An event the run sent, to the run whose ref is `ref`, which a test doesn't start. */
422
393
  export type EmittedEvent = { ref: string; name: string; data: JsonValue; eventId?: string };
@@ -450,35 +421,29 @@ export type TestRun<Result> = {
450
421
  decisions: DecisionRequest[];
451
422
  logs: { message: string; fields?: JsonObject }[];
452
423
  /**
453
- * Each stage's record in the order entered, as the run wrote it to disk: sessions, times and
454
- * reason included, which the run's own summaries leave out.
424
+ * Each stage the attempt entered, in order, by its line in `run.jsonl`: one it ran or was given
425
+ * its own, times and reason included, which the run's own summaries leave out; one it reused the
426
+ * line it reused, an earlier attempt's. A stage the plan stopped as it was entered wrote none, and
427
+ * is absent.
455
428
  */
456
- stages: StageRecord[];
429
+ stages: StageLine[];
457
430
  /**
458
431
  * How the attempt ended, as the workflow's `present` and `report` are given it: `completed` with
459
432
  * its value, `parked` at its last wait, or `stopped`, `failed`, `timed-out` or `cancelled` with
460
433
  * the stage it ended in and why.
461
434
  */
462
435
  ending: Ending<Result>;
463
- /**
464
- * @deprecated Read `run.ending`, whose kind is `"stopped"`. How the attempt stopped, and the
465
- * values it stopped for; absent when it didn't.
466
- */
467
- readonly stopped?: { reason: string; stage?: string; needs?: readonly StageNeed[] };
468
436
  /** Every event the run sent to another workflow, in order. */
469
437
  emits: EmittedEvent[];
470
- /**
471
- * @deprecated Read `run.ending`, whose kind is `"parked"`. A run that parked, and its last wait;
472
- * absent when it returned.
473
- */
474
- readonly parked?: { label?: string; until?: string };
475
438
  /** The run's timers in the order set: when each was due, and whether it fired or was cancelled. */
476
439
  timers: { name: string; due: string; state: "fired" | "cancelled" | "pending" }[];
440
+ /** The events senders gave the run, its `in.jsonl` as the run left it, in order. */
441
+ events: EventLine[];
477
442
  /**
478
- * Its mailbox as the run left it: its events, timers, and what its waits and turns wrote, in
479
- * order; a plain run's holds its timers alone.
443
+ * What the run settled, its `run.jsonl` as the run left it, in order: each attempt's `claimed`,
444
+ * and its stages, timers, sessions, what its waits and races took, and the turns it began.
480
445
  */
481
- mailbox: MailboxRecord[];
446
+ run: RunLine[];
482
447
  };
483
448
 
484
449
  /**
@@ -495,11 +460,7 @@ export async function runTest<Result extends JsonValue>(
495
460
  const problem = optionsProblem(options, withEvents !== undefined, workflow.meta.name);
496
461
  if (problem) throw new Error(problem);
497
462
  const logs: TestRun<Result>["logs"] = [];
498
- // A test's own forms, apart from the run's: said in its logs as the run says a warning.
499
- const deprecations = new RunDeprecations((message) => logs.push({ message }));
500
- const deprecated = (name: Deprecated) => deprecations.used(name);
501
- for (const name of engine.used ?? []) deprecated(name);
502
- const earlier = earlierOf(options, deprecated);
463
+ const earlier = earlierOf(options);
503
464
  if (withEvents && options.values !== undefined)
504
465
  throw new Error(
505
466
  "a run of a workflow with events takes no values: before fromStage, a stage with no record runs",
@@ -508,7 +469,7 @@ export async function runTest<Result extends JsonValue>(
508
469
  throw new Error(
509
470
  "a run of a workflow with events takes fromStage only with earlier, as awf run takes --from-stage only with --continue",
510
471
  );
511
- const started = startOf(withEvents, options, deprecated);
472
+ const started = startOf(withEvents, options);
512
473
  const definition = workflow as RunnableWorkflow<JsonValue, Result>;
513
474
  const scripts = new Scripts(options.agents);
514
475
  const compactionScripts = new Scripts(options.compactions, {
@@ -521,11 +482,8 @@ export async function runTest<Result extends JsonValue>(
521
482
  }
522
483
  const stopping = new AbortController();
523
484
  const problems: string[] = [];
524
- const stallMs =
525
- milliseconds(options, "stall", "stallMs", () => deprecations.used("stall-ms")) ?? 2_000;
526
- const timeoutMs = milliseconds(options, "timeout", "timeoutMs", () =>
527
- deprecations.used("timeout-ms", "in testWorkflow's options"),
528
- );
485
+ const stallMs = milliseconds(options, "stall") ?? 2_000;
486
+ const timeoutMs = milliseconds(options, "timeout");
529
487
  let stall: ReturnType<typeof setTimeout> | undefined;
530
488
  /** The test ended the attempt: everything left waited on a coordinator's script. */
531
489
  let waitedOut = false;
@@ -576,8 +534,7 @@ export async function runTest<Result extends JsonValue>(
576
534
  events.onActivity();
577
535
  return true;
578
536
  };
579
- const scripted =
580
- withEvents && new ScriptedEvents(withEvents, options, earlier.mailbox ?? [], now, deprecated);
537
+ const scripted = withEvents && new ScriptedEvents(withEvents, options, earlier.events ?? [], now);
581
538
  const maxTurns = options.maxTurns ?? 20;
582
539
  if (!Number.isInteger(maxTurns) || maxTurns < 1)
583
540
  throw new Error(`maxTurns is a whole number of turns, at least 1, not ${options.maxTurns}`);
@@ -588,7 +545,6 @@ export async function runTest<Result extends JsonValue>(
588
545
  const events = {
589
546
  onActivity: () => arm(stallMs),
590
547
  onScriptError: (message: string) => stopWith(message),
591
- onDeprecated: (name: Deprecated, subject?: string) => deprecations.used(name, subject),
592
548
  onTurnStarted: async (turn: TestTurn) => {
593
549
  turnsStarted += 1;
594
550
  if (turnsStarted > maxTurns)
@@ -597,11 +553,13 @@ export async function runTest<Result extends JsonValue>(
597
553
  );
598
554
  const due = scripted ? scripted.due(turn) : [];
599
555
  if (due.length === 0) return;
600
- for (const record of due) await appendMailbox(run.dir, record);
556
+ for (const line of due) await appendIn(run.dir, line);
601
557
  nudge?.();
602
558
  },
603
559
  };
604
- const attempt = earlier.continues ? 2 : 1;
560
+ const attempt = earlier.continues
561
+ ? Math.max(1, ...(earlier.run ?? []).map((line) => line.attempt)) + 1
562
+ : 1;
605
563
  const coordinators = new ParticipantScripts(options.participants, attempt, events);
606
564
  const stopWith = (message: string) => {
607
565
  problems.push(message);
@@ -650,8 +608,10 @@ export async function runTest<Result extends JsonValue>(
650
608
  };
651
609
  let sandboxOf = new Map<string, AgentSandbox>();
652
610
  const emits: EmittedEvent[] = [];
653
- let mailbox: MailboxRecord[] = [];
654
- let stages: StageRecord[] = [];
611
+ let inLines: EventLine[] = [];
612
+ let runLines: RunLine[] = [];
613
+ let timers: TestRun<Result>["timers"] = [];
614
+ let stages: StageLine[] = [];
655
615
  let messages: (MessageRecord & { to: string })[] = [];
656
616
  const runRoot = directory();
657
617
  const deadline =
@@ -666,15 +626,28 @@ export async function runTest<Result extends JsonValue>(
666
626
  for (const path of temporary) rmSync(path, { recursive: true, force: true });
667
627
  throw error;
668
628
  });
669
- if (earlier.stages) await recordEarlierAttempt(run.dir, earlier.stages);
670
- for (const record of earlier.mailbox ?? []) await appendMailbox(run.dir, record);
671
- if (started.record) await appendMailbox(run.dir, started.record);
629
+ let lines: RunLines;
630
+ let ops: EarlierOps;
631
+ try {
632
+ if (earlier.stages) await recordEarlierAttempt(run.dir, earlier.stages);
633
+ if (earlier.run) await writeEarlierLines(run.dir, earlier.run);
634
+ for (const line of earlier.events ?? []) await appendIn(run.dir, line);
635
+ if (started.record) await appendIn(run.dir, started.record);
636
+ ops = await listOps(run.dir);
637
+ lines = await claimRunLines(run.dir, attempt, new Date(now()).toISOString());
638
+ } catch (error) {
639
+ for (const path of temporary) rmSync(path, { recursive: true, force: true });
640
+ throw error;
641
+ }
672
642
  const recordedStages = async ({ stages: entered = [] }: SettledRun) => {
673
- const records = await readStageRecords(run.dir);
674
- // A stage the plan stopped has no record of this attempt's: the one there is an earlier one's.
675
- return entered.flatMap(({ stage, attempt }) => {
676
- const record = records.get(stage);
677
- return record?.attempt === attempt ? [record] : [];
643
+ const { view } = await readRunLines(run.dir);
644
+ // The run's summaries hold one entry per stage, as it last stood: a stage by content that
645
+ // failed and ran again in this attempt is one entry, its latest line. So each entry's line is
646
+ // the latest its attempt wrote: a stage this attempt ran or was given has its own, a reused one
647
+ // the line it reused. A stage the plan stopped wrote none.
648
+ return entered.flatMap(({ stage, attempt: wrote }) => {
649
+ const line = view.stages[stage]?.history.findLast((each) => each.attempt === wrote);
650
+ return line ? [line] : [];
678
651
  });
679
652
  };
680
653
  events.onActivity();
@@ -694,8 +667,10 @@ export async function runTest<Result extends JsonValue>(
694
667
  run: {
695
668
  dir: run.dir,
696
669
  id: run.record.id,
697
- attempt,
670
+ lines,
671
+ ops,
698
672
  ...(options.fromStage === undefined ? {} : { fromStage: options.fromStage }),
673
+ ...(options.panes === undefined ? {} : { panes: options.panes }),
699
674
  ...(options.values === undefined
700
675
  ? {}
701
676
  : { values: new Map(Object.entries(options.values)) }),
@@ -707,7 +682,7 @@ export async function runTest<Result extends JsonValue>(
707
682
  live: async (until: number) => {
708
683
  liveUntil = until;
709
684
  const due = scripted.ended(host.inFlight());
710
- for (const record of due) await appendMailbox(run.dir, record);
685
+ for (const line of due) await appendIn(run.dir, line);
711
686
  if (due.length > 0) nudge?.();
712
687
  },
713
688
  listenNudges: (given: () => void) => {
@@ -737,11 +712,8 @@ export async function runTest<Result extends JsonValue>(
737
712
  },
738
713
  sandboxesDir: directory(),
739
714
  machineRoot: directory(),
740
- // A sandbox here launches nothing, so no harness's login is needed: a stand-in for each.
741
- environment: {
742
- ...process.env,
743
- ...Object.fromEntries(sandboxTokens().map((name) => [name, "workflow-test"])),
744
- },
715
+ // An agent here launches nothing, so it needs no harness's executable or login.
716
+ homes: SCRIPTED_HOMES,
745
717
  },
746
718
  decisions: {
747
719
  providers: { scripted: decisions.provider },
@@ -777,7 +749,9 @@ export async function runTest<Result extends JsonValue>(
777
749
  }
778
750
  host.settle(await readTurns(run.dir));
779
751
  messages = await messagesIn(run.dir);
780
- mailbox = await readMailbox(run.dir);
752
+ inLines = await readIn(run.dir);
753
+ runLines = (await readRunLines(run.dir)).lines;
754
+ timers = timersOf((await readWaits(run.dir, now())).timers);
781
755
  for (const path of temporary) rmSync(path, { recursive: true, force: true });
782
756
  }
783
757
  if (problems.length === 0 && scripted) problems.push(...scripted.unsent(host.inFlight()));
@@ -827,9 +801,7 @@ export async function runTest<Result extends JsonValue>(
827
801
  continue: goOn(undefined, decided.stages),
828
802
  }
829
803
  : decided;
830
- const stopped = stoppedOf(settled);
831
- const parked = parkedOf(settled);
832
- const result: TestRun<Result> = {
804
+ return {
833
805
  get value() {
834
806
  if ("value" in settled) return settled.value;
835
807
  if (primaryFailure(settled.error) instanceof RunParked)
@@ -842,7 +814,8 @@ export async function runTest<Result extends JsonValue>(
842
814
  },
843
815
  turns: host.turns,
844
816
  emits,
845
- mailbox,
817
+ events: inLines,
818
+ run: runLines,
846
819
  turnsOf: (agent) => host.turns.filter((turn) => turn.agent === agent),
847
820
  compactions: host.compactions,
848
821
  compactionsOf: (agent) => host.compactions.filter((compaction) => compaction.agent === agent),
@@ -860,32 +833,17 @@ export async function runTest<Result extends JsonValue>(
860
833
  decisions: decisions.asked,
861
834
  logs,
862
835
  stages,
863
- timers: timersOf(mailbox),
836
+ timers,
864
837
  ending,
865
838
  };
866
- // Not enumerable, so spreading the run reads neither.
867
- Object.defineProperties(result, {
868
- stopped: {
869
- get: () => {
870
- deprecated("run-stopped");
871
- return stopped;
872
- },
873
- },
874
- parked: {
875
- get: () => {
876
- deprecated("run-parked");
877
- return parked;
878
- },
879
- },
880
- });
881
- return result;
882
839
  }
883
840
 
884
- function timersOf(records: readonly MailboxRecord[]): TestRun<unknown>["timers"] {
885
- return [...foldMailbox(records).timers.values()].map(({ name, due, fired, cancelled }) => ({
841
+ /** Each timer: taken, or due by the run's clock and not cancelled, is `fired`. */
842
+ function timersOf(timers: ReadonlyMap<string, TimerState>): TestRun<unknown>["timers"] {
843
+ return [...timers.values()].map(({ name, due, taken, fired, cancelled }) => ({
886
844
  name,
887
845
  due,
888
- state: fired ? "fired" : cancelled ? "cancelled" : "pending",
846
+ state: taken ? "fired" : cancelled ? "cancelled" : fired ? "fired" : "pending",
889
847
  }));
890
848
  }
891
849
 
@@ -940,36 +898,52 @@ async function messagesIn(dir: string): Promise<(MessageRecord & { to: string })
940
898
  }
941
899
 
942
900
  /**
943
- * Writes `recorded` as attempt 1's succeeded stages into a run's folder, a second apart in the order
944
- * given: a continue orders what is recorded by when each stage started.
901
+ * Writes `recorded` as attempt 1's succeeded stage lines into a run's `run.jsonl`, after attempt 1's
902
+ * claim, a second apart in the order given: a continue orders what is recorded by when each stage
903
+ * started.
945
904
  */
946
905
  async function recordEarlierAttempt(
947
906
  dir: string,
948
907
  recorded: Readonly<Record<string, JsonValue | undefined>>,
949
908
  ): Promise<void> {
950
909
  let at = Date.now() - Object.keys(recorded).length * 1_000;
951
- for (const [stage, value] of Object.entries(recorded)) {
910
+ const lines = await claimRunLines(dir, 1, new Date(at - 1_000).toISOString());
911
+ for (const [name, value] of Object.entries(recorded)) {
952
912
  const time = new Date(at).toISOString();
953
913
  at += 1_000;
954
- await writeStageRecord(dir, {
955
- version: STAGE_RECORD_VERSION,
956
- stage,
914
+ await lines.append({
915
+ kind: "stage",
957
916
  attempt: 1,
917
+ at: time,
918
+ name,
958
919
  outcome: "succeeded",
959
920
  started: time,
960
- ended: time,
961
- sessions: [],
962
921
  ...(value === undefined ? {} : { value }),
963
922
  });
964
923
  }
965
924
  }
966
925
 
926
+ /**
927
+ * Writes `lines`, what an earlier attempt settled, into the run's `run.jsonl` as given, once
928
+ * `readRun` takes them: the test's claim comes after them.
929
+ */
930
+ async function writeEarlierLines(dir: string, lines: readonly RunLine[]): Promise<void> {
931
+ try {
932
+ readRun(lines);
933
+ } catch (error) {
934
+ throw new Error(`earlier.run: ${messageOf(error)}`);
935
+ }
936
+ await appendFile(runLinesFile(dir), lines.map((line) => `${JSON.stringify(line)}\n`).join(""), {
937
+ mode: 0o600,
938
+ });
939
+ }
940
+
967
941
  /** An event a test sends while its run goes on, once its turn begins. */
968
942
  type PendingEvent = {
969
943
  at: string;
970
944
  after: boolean;
971
945
  name: string;
972
- record: MailboxRecord;
946
+ record: EventLine;
973
947
  /** Its turn, once begun: an `after` event waits for it to end. */
974
948
  turn?: TestTurn;
975
949
  };
@@ -981,11 +955,10 @@ class ScriptedEvents {
981
955
  constructor(
982
956
  workflow: EventWorkflow,
983
957
  options: AnyTestOptions,
984
- /** What the earlier attempt left in the mailbox, whose event ids are taken. */
985
- earlier: readonly MailboxRecord[],
958
+ /** The events the earlier attempt left, whose ids are taken. */
959
+ earlier: readonly EventLine[],
986
960
  /** The run's clock, which stamps what is sent. */
987
961
  private readonly now: () => number,
988
- deprecated: (name: Deprecated) => void,
989
962
  ) {
990
963
  const ids = new Set<string>();
991
964
  const idOf = (given: string | undefined, made: string) => {
@@ -998,8 +971,8 @@ class ScriptedEvents {
998
971
  ids.add(id);
999
972
  return id;
1000
973
  };
1001
- for (const record of earlier) if (record.kind === "event") ids.add(record.id);
1002
- if (options.event) idOf(eventIdOf(options.event, deprecated), `${AWF_ID_PREFIX}start`);
974
+ for (const line of earlier) ids.add(line.id);
975
+ if (options.event) idOf(options.event.eventId, `${AWF_ID_PREFIX}start`);
1003
976
  this.#pending = (options.events ?? []).map((event, index) => {
1004
977
  if ((event.during === undefined) === (event.after === undefined))
1005
978
  throw new Error("a scripted event gives during or after, one of them");
@@ -1009,7 +982,7 @@ class ScriptedEvents {
1009
982
  throw new Error(
1010
983
  `a scripted event's ${after ? "after" : "during"} is "{agent}#{n}", an agent's nth turn, not ${at}`,
1011
984
  );
1012
- const id = idOf(eventIdOf(event, deprecated), `${AWF_ID_PREFIX}test.${index + 1}`);
985
+ const id = idOf(event.eventId, `${AWF_ID_PREFIX}test.${index + 1}`);
1013
986
  // A participant's reply is recorded as `awf emit` records one, whatever name it is sent by.
1014
987
  const stamped = new Date().toISOString();
1015
988
  let sent = {
@@ -1020,11 +993,7 @@ class ScriptedEvents {
1020
993
  at: stamped,
1021
994
  };
1022
995
  const reply = awfEventOf(event.name);
1023
- if (event.name === "participant:reply") {
1024
- deprecated("test-participant-reply");
1025
- const old = event as { from: string; data: ParticipantReply };
1026
- sent = replyRecord(old.from, id, { author: "engineer", ...old.data }, stamped);
1027
- } else if (reply?.kind === "reply") {
996
+ if (reply?.kind === "reply") {
1028
997
  checkEvent(workflow, event, true);
1029
998
  sent = replyRecord(reply.key, id, event.data, stamped);
1030
999
  } else checkEvent(workflow, event);
@@ -1032,24 +1001,24 @@ class ScriptedEvents {
1032
1001
  at,
1033
1002
  after,
1034
1003
  name: event.name,
1035
- record: { version: MAILBOX_RECORD_VERSION, kind: "event", ...sent },
1004
+ record: sent,
1036
1005
  };
1037
1006
  });
1038
1007
  }
1039
1008
 
1040
1009
  /** The events due during `turn`, taken off the list, stamped now; those after it wait for it. */
1041
- due(turn: TestTurn): MailboxRecord[] {
1010
+ due(turn: TestTurn): EventLine[] {
1042
1011
  const at = `${turn.agent}#${turn.n}`;
1043
1012
  for (const each of this.#pending) if (each.after && each.at === at) each.turn = turn;
1044
1013
  return this.#take((each) => !each.after && each.at === at);
1045
1014
  }
1046
1015
 
1047
1016
  /** The run waits live: the events after a turn that has ended, taken off the list. */
1048
- ended(inFlight: readonly TestTurn[]): MailboxRecord[] {
1017
+ ended(inFlight: readonly TestTurn[]): EventLine[] {
1049
1018
  return this.#take((each) => each.turn !== undefined && !inFlight.includes(each.turn));
1050
1019
  }
1051
1020
 
1052
- #take(which: (each: PendingEvent) => boolean): MailboxRecord[] {
1021
+ #take(which: (each: PendingEvent) => boolean): EventLine[] {
1053
1022
  const due = this.#pending.filter(which);
1054
1023
  for (const each of due) this.#pending.splice(this.#pending.indexOf(each), 1);
1055
1024
  return due.map((each) => ({ ...each.record, at: new Date(this.now()).toISOString() }));
@@ -1085,7 +1054,7 @@ async function layOut(
1085
1054
  for (const [path, tree] of Object.entries(files ?? {})) {
1086
1055
  // Under the test's own directory, which it removes: a path outside would be left behind.
1087
1056
  const target = resolve(cwd, path);
1088
- if (isAbsolute(path) || relative(cwd, target).startsWith(".."))
1057
+ if (isAbsolute(path) || (relative(cwd, target) !== "" && !isInside(cwd, target)))
1089
1058
  throw new Error(`files lays out under the run's directory; ${path} is outside it`);
1090
1059
  await write(target, tree);
1091
1060
  }
@@ -1098,8 +1067,7 @@ async function layOut(
1098
1067
  function startOf(
1099
1068
  workflow: EventWorkflow | undefined,
1100
1069
  options: AnyTestOptions,
1101
- deprecated: (name: Deprecated) => void,
1102
- ): { id: string; record?: MailboxRecord } {
1070
+ ): { id: string; record?: EventLine } {
1103
1071
  const { event, id = "test" } = options;
1104
1072
  if (typeof id !== "string") throw new Error(`a run's id is a string, not ${JSON.stringify(id)}`);
1105
1073
  const problem = idProblem(id);
@@ -1111,11 +1079,9 @@ function startOf(
1111
1079
  throw new Error(
1112
1080
  `${event.name} doesn't start a run of ${workflow.meta.name}: its starts are ${starts.join(", ")}`,
1113
1081
  );
1114
- const record = {
1115
- version: MAILBOX_RECORD_VERSION,
1082
+ const record: EventLine = {
1116
1083
  at: new Date().toISOString(),
1117
- kind: "event" as const,
1118
- id: eventIdOf(event, deprecated) ?? `${AWF_ID_PREFIX}start`,
1084
+ id: event.eventId ?? `${AWF_ID_PREFIX}start`,
1119
1085
  name: event.name,
1120
1086
  data: event.data,
1121
1087
  from: "operator",
@@ -1124,32 +1090,9 @@ function startOf(
1124
1090
  return { id, record };
1125
1091
  }
1126
1092
 
1127
- /**
1128
- * What the earlier attempt left, from `earlier` or its deprecated forms, `recorded` and `mailbox`;
1129
- * `continues` when it left anything, so the test's attempt is attempt 2.
1130
- */
1131
- function earlierOf(
1132
- options: AnyTestOptions,
1133
- deprecated: (name: Deprecated) => void,
1134
- ): EarlierAttempt & { continues: boolean } {
1135
- const { earlier = {}, recorded, mailbox } = options;
1136
- if (recorded !== undefined) {
1137
- if (earlier.stages !== undefined)
1138
- throw new Error("give earlier.stages alone: recorded is its deprecated form");
1139
- deprecated("test-recorded");
1140
- }
1141
- if (mailbox !== undefined) {
1142
- if (earlier.mailbox !== undefined)
1143
- throw new Error("give earlier.mailbox alone: mailbox is its deprecated form");
1144
- deprecated("test-mailbox");
1145
- }
1146
- const stages = earlier.stages ?? recorded;
1147
- const left = earlier.mailbox ?? mailbox;
1148
- return {
1149
- ...(stages === undefined ? {} : { stages }),
1150
- ...(left === undefined ? {} : { mailbox: left }),
1151
- continues: options.earlier !== undefined || stages !== undefined || left !== undefined,
1152
- };
1093
+ /** What the earlier attempt left; `continues` when `earlier` is given: the test's is a continue. */
1094
+ function earlierOf(options: AnyTestOptions): EarlierAttempt & { continues: boolean } {
1095
+ return { ...options.earlier, continues: options.earlier !== undefined };
1153
1096
  }
1154
1097
 
1155
1098
  /**
@@ -1176,15 +1119,6 @@ function argsOf(
1176
1119
  }
1177
1120
  }
1178
1121
 
1179
- /** An event's id, given as `eventId` or, deprecated, `id`. */
1180
- function eventIdOf(event: EventIds, deprecated: (name: Deprecated) => void): string | undefined {
1181
- if (event.id === undefined) return event.eventId;
1182
- deprecated("test-event-id");
1183
- if (event.eventId !== undefined && event.eventId !== event.id)
1184
- throw new Error(`an event gives eventId ${event.eventId} and id ${event.id}: give eventId`);
1185
- return event.id;
1186
- }
1187
-
1188
1122
  function sandboxesOf(sandboxes: readonly SandboxRecord[] | undefined): Map<string, AgentSandbox> {
1189
1123
  return new Map(
1190
1124
  (sandboxes ?? []).flatMap(({ key, provider, spec, domains, agents }) =>
@@ -1192,22 +1126,3 @@ function sandboxesOf(sandboxes: readonly SandboxRecord[] | undefined): Map<strin
1192
1126
  ),
1193
1127
  );
1194
1128
  }
1195
-
1196
- /** A run that parked, and its last wait: what the attempt ended with. */
1197
- function parkedOf(settled: { value: unknown } | { error: unknown }): TestRun<unknown>["parked"] {
1198
- if (!("error" in settled)) return undefined;
1199
- const parked = primaryFailure(settled.error);
1200
- return parked instanceof RunParked ? parked.wait : undefined;
1201
- }
1202
-
1203
- /** Decided as `awf run` decides it: a stop is the run's first failure, whatever failed beside it. */
1204
- function stoppedOf(settled: { value: unknown } | { error: unknown }): TestRun<unknown>["stopped"] {
1205
- if (!("error" in settled)) return undefined;
1206
- const stop = primaryFailure(settled.error);
1207
- if (!(stop instanceof WorkflowStopped)) return undefined;
1208
- return {
1209
- reason: stop.reason,
1210
- ...(stop.stage === undefined ? {} : { stage: stop.stage }),
1211
- ...(stop.needs === undefined ? {} : { needs: stop.needs }),
1212
- };
1213
- }