@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
@@ -10,10 +10,12 @@ import {
10
10
  RESERVED_PARTICIPANT_KEYS,
11
11
  } from "@agentswf/contract/participant";
12
12
  import type {
13
+ AgentOp,
13
14
  AgentPaneRecord,
14
15
  AgentSkillsRecord,
15
16
  RunAccounting,
16
17
  RunPanes,
18
+ RunView,
17
19
  SandboxRecord,
18
20
  SettledDecision,
19
21
  SettledOperation,
@@ -45,10 +47,8 @@ import {
45
47
  awfEventOf,
46
48
  type Branches,
47
49
  type CallerForkSpec,
48
- type CodeParticipantRef,
49
50
  type CompactionId,
50
51
  type CompactSpec,
51
- DEPRECATED_DEFINITION,
52
52
  DeadlineExceededError,
53
53
  type Duration,
54
54
  durationMilliseconds,
@@ -56,12 +56,14 @@ import {
56
56
  type EventLike,
57
57
  type ForkRuntime,
58
58
  type HarnessAllowance,
59
+ type HarnessKind,
59
60
  isEventWorkflow,
60
61
  isJsonValue,
61
62
  type JsonObject,
62
63
  type JsonValue,
63
64
  type KeepPane,
64
65
  type MessageAccess,
66
+ type NativeSessionRef,
65
67
  type Next,
66
68
  nameProblem,
67
69
  type OperationRecord,
@@ -75,7 +77,6 @@ import {
75
77
  placementOf,
76
78
  positiveDurationMilliseconds,
77
79
  type RaceBranches,
78
- REPORT_DEPRECATED,
79
80
  type RunResult,
80
81
  type RuntimeSelection,
81
82
  type SettingsSpec,
@@ -85,6 +86,7 @@ import {
85
86
  type StageSummary,
86
87
  type StartedTurn,
87
88
  type Taken,
89
+ type Timeout,
88
90
  type TimeRead,
89
91
  type Timer,
90
92
  type TurnId,
@@ -101,7 +103,8 @@ import {
101
103
  findHarness,
102
104
  HARNESS_NAMES,
103
105
  type Harness,
104
- hostHome,
106
+ type HarnessHomes,
107
+ OPERATOR_HOMES,
105
108
  settingsRefusal,
106
109
  skillsLayout,
107
110
  } from "@agentswf/harness";
@@ -130,11 +133,9 @@ import {
130
133
  import type { CoordinatorPort } from "./coordinator-port";
131
134
  import {
132
135
  assertDeadline,
133
- assertDeadlineValue,
134
136
  boundedDeadline,
135
137
  deadlineWithin,
136
138
  earlierDeadline,
137
- type GivenBound,
138
139
  harnessDeadline,
139
140
  runUntilStopped,
140
141
  scheduleAt,
@@ -143,7 +144,6 @@ import {
143
144
  } from "./deadlines";
144
145
  import { RunDecisions } from "./decisions/directory";
145
146
  import type { DecisionInstallation } from "./decisions/seam";
146
- import { type Deprecated, RunDeprecations } from "./deprecations";
147
147
  import { parseWhen } from "./duration";
148
148
  import { emitEvent } from "./emit";
149
149
  import { messageOf } from "./errors";
@@ -154,15 +154,6 @@ import {
154
154
  inTimeoutHandler,
155
155
  type KeepAlive,
156
156
  } from "./keep-alive";
157
- import {
158
- AT_DUE,
159
- type RestoredTurn,
160
- RunMailbox,
161
- RunParked,
162
- recoverTurns,
163
- type TurnEnd,
164
- unrunUsage,
165
- } from "./mailbox";
166
157
  import { superviseMessageOperation } from "./message-operation";
167
158
  import { canAwaitReply } from "./messaging/limits";
168
159
  import type { HolderRequest } from "./messaging/messaging";
@@ -177,16 +168,19 @@ import {
177
168
  type SupervisedOutcome,
178
169
  superviseOperation,
179
170
  } from "./operation-liveness";
171
+ import { type EarlierOps, OpIds, writeScreen } from "./ops";
180
172
  import {
181
173
  besideOf,
182
174
  checkPaneOptions,
183
175
  keepsPane,
184
176
  type PaneAsked,
185
177
  paneAsked,
186
- storedLayout,
178
+ storedPane,
187
179
  } from "./pane-layout";
188
180
  import { createResultSlotRegistry, type ResultSlotRegistry } from "./result-slots";
189
181
  import { RunEventLog } from "./run-event-log";
182
+ import { type RunLines, readRunLines } from "./run-lines";
183
+ import { branchOf, matches, type RestoredTurn } from "./run-policy/waits";
190
184
  import {
191
185
  type AgentProgress,
192
186
  type GroupProgress,
@@ -200,7 +194,7 @@ import {
200
194
  type OperationTags,
201
195
  type RunLedger,
202
196
  } from "./run-usage";
203
- import { endTurnsLine, readAttempts, readStageRecords, readTurns } from "./runs";
197
+ import { endLogLine, readAttempts, workDir } from "./runs";
204
198
  import { type CredentialLocks, placeCarried, seedHome } from "./sandbox-homes";
205
199
  import {
206
200
  RUN_SANDBOX_ONLY,
@@ -208,23 +202,16 @@ import {
208
202
  type RunSandboxOptions,
209
203
  type SeatedAgent,
210
204
  } from "./sandboxes";
205
+ import { SessionLines, type SessionPlace } from "./session-lines";
211
206
  import { QUOTING_RULE, simpleInput, takesText, usageLine } from "./simple-input";
212
207
  import { placeSkills, RunSkills, readSkillSources } from "./skills/run-skills";
213
- import {
214
- asSource,
215
- branchOf,
216
- branchValue,
217
- isSignal,
218
- matches,
219
- presented,
220
- signalOf,
221
- source,
222
- takeOf,
223
- } from "./sources";
208
+ import { asSource, branchValue, isSignal, presented, signalOf, source, takeOf } from "./sources";
209
+ import { keepSpend, settleEarlier } from "./spend";
224
210
  import { StageLedger, type StageProgress } from "./stage-ledger";
225
- import { lastRedo } from "./stage-plan";
226
211
  import { FromStageUnreached } from "./stopped";
227
212
  import { inToolHandler, outsideTools, refusedInTool, ToolCalls, toolsLesson } from "./tools";
213
+ import { unrunUsage } from "./turn-ends";
214
+ import { RunParked, RunWaits, type TurnEnd } from "./waits";
228
215
  import type { EventWorkflow } from "./workflow-loader";
229
216
 
230
217
  export { WorkflowCancelledError } from "./deadlines";
@@ -242,12 +229,17 @@ export type RunWorkflowOptions = {
242
229
  defaultWindowMs?: number;
243
230
  /** The run's time, which its timers and `wf.now()` read; absent, the clock's. A test's is virtual. */
244
231
  now?: () => number;
232
+ /** Where the control plane's socket directory is made; `/tmp` unless a test watches its own. */
233
+ socketRoot?: string;
245
234
  runRoot: string;
246
235
  /** The run this is an attempt of, whose folder the operator claimed under `runRoot`. */
247
236
  run: {
248
237
  dir: string;
249
238
  id: string;
250
- attempt: number;
239
+ /** The attempt's writer on `run.jsonl`, which its claim made; its `attempt` is this attempt. */
240
+ lines: RunLines;
241
+ /** What the claim's listing of `ops/` found: each key's last `n`, numbered on from. */
242
+ ops: EarlierOps;
251
243
  /** How the attempt is named to a person: its Herdr workspace's label. */
252
244
  label?: string;
253
245
  /** `--from-stage`: the stage this attempt starts at, reusing those before it. */
@@ -258,14 +250,14 @@ export type RunWorkflowOptions = {
258
250
  panes?: RunPanes;
259
251
  /** A run of a workflow with `events`. */
260
252
  events?: true;
261
- /** With `events`: runs `work` holding the run's lock, as every append to the mailbox is made. */
253
+ /** With `events`: runs `work` holding the run's lock, as every sender appends to `in.jsonl`. */
262
254
  locked?<T>(work: () => Promise<T>): Promise<T>;
263
255
  /**
264
256
  * With `events`: false when the attempt before this one has no ending, its process gone with its
265
257
  * agents perhaps still running: its sessions aren't reopened until they are stopped (Q17).
266
258
  */
267
259
  resumeSessions?: boolean;
268
- /** With `events`: calls `nudge` whenever awf tells the run its mailbox has a new event. */
260
+ /** With `events`: calls `nudge` whenever awf tells the run it has a new event. */
269
261
  listenNudges?(nudge: () => void): void;
270
262
  /**
271
263
  * Writes the attempt's `parked` ending, which the run's wait calls holding the run's lock: an
@@ -454,14 +446,11 @@ type AgentIdentity = {
454
446
  /** Its skills' sources, checked; absent when it has the operator's. */
455
447
  skills?: SkillSource[];
456
448
  instructions?: string;
457
- labels?: AgentOpenSpec["labels"];
458
449
  /** The agent it was forked from, and the spec it was forked with. */
459
450
  forkedFrom?: { parent: AgentKey; spec: CallerForkSpec };
460
- /** As written; a reopen compares them so, never where the pane ended up. */
451
+ /** As written; a reopen compares it so, never where the pane ended up. */
461
452
  pane?: PaneSpec;
462
- layout?: PaneLayout;
463
- keepPane?: KeepPane;
464
- /** What its host is asked for its pane: these, with the operator's flags applied. */
453
+ /** What its host is asked for its pane: it, with the operator's flags applied. */
465
454
  asked?: PaneAsked;
466
455
  };
467
456
 
@@ -492,8 +481,9 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
492
481
  args: Args,
493
482
  options: RunWorkflowOptions,
494
483
  ): Promise<WorkflowRunHandle<Result>> {
495
- assertDeadline(options.deadline);
484
+ // Cancellation wins when the deadline has passed too, as runUntilStopped has it.
496
485
  if (options.signal?.aborted) throw new WorkflowCancelledError(options.signal.reason);
486
+ assertDeadline(options.deadline);
497
487
  // Before anything opens, so a refusal leaves nothing to close.
498
488
  const withEvents = isEventWorkflow(definition);
499
489
  const locked = options.run.locked ?? (<T>(work: () => Promise<T>) => work());
@@ -501,18 +491,36 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
501
491
  throw new Error(
502
492
  "a run of a workflow with events takes no --values: before --from-stage, a stage with no record runs",
503
493
  );
504
- const { dir: runDir, id: runId, attempt } = options.run;
505
- const startedAt = Date.now();
494
+ const { dir: runDir, id: runId } = options.run;
495
+ const { attempt } = options.run.lines;
506
496
  const cwd = options.cwd ?? process.cwd();
507
- // Before anything opens: a record this awf can't read refuses the attempt, and a crash may have
508
- // torn the last line an earlier attempt appended.
509
- const recorded = await readStageRecords(runDir);
510
- // The last --from-stage an earlier attempt redid, whose stale records still rerun as reached.
511
- const redo = withEvents
512
- ? lastRedo(await readAttempts({ dir: runDir }), recorded, attempt)
513
- : undefined;
497
+ // Before anything opens: a line this awf can't read refuses the attempt.
498
+ const { lines: written, view: recorded } = await readRunLines(runDir);
499
+ // The version each earlier attempt ran, which a line it wrote is reused under.
500
+ const earlier = await readAttempts({ dir: runDir });
501
+ // While no agent of this attempt can resume a session an earlier one's spend is still in.
502
+ await settleEarlier(runDir, {
503
+ attempt,
504
+ attempts: earlier,
505
+ lines: written,
506
+ accounting: options.runtime.host.accounting,
507
+ now: Date.now(),
508
+ ...(options.signal ? { signal: options.signal } : {}),
509
+ }).then(
510
+ ({ failed }) => {
511
+ for (const each of failed) options.onLog?.(`awf: spend.json not written for ${each}`);
512
+ },
513
+ (error: unknown) =>
514
+ options.onLog?.(`awf: earlier attempts' spend was not settled: ${messageOf(error)}`),
515
+ );
516
+ const startedAt = Date.now();
517
+ const versions = new Map(
518
+ earlier.flatMap(({ attempt: n, workflowVersion }) =>
519
+ workflowVersion === undefined ? [] : [[n, workflowVersion] as const],
520
+ ),
521
+ );
514
522
  // The log is observation: one that cannot be written says so as it is, and fails nothing.
515
- await endTurnsLine(runDir).catch(() => undefined);
523
+ await endLogLine(runDir).catch(() => undefined);
516
524
  const slots = createResultSlotRegistry({
517
525
  runDir,
518
526
  ...(options.livenessPolicy
@@ -522,7 +530,10 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
522
530
  }
523
531
  : {}),
524
532
  });
525
- const control = await startResultControlPlane({ slots });
533
+ const control = await startResultControlPlane({
534
+ slots,
535
+ ...(options.socketRoot ? { socketRoot: options.socketRoot } : {}),
536
+ });
526
537
  let host: AgentRunHost;
527
538
  const openingHost = Promise.resolve().then(() =>
528
539
  options.runtime.host.openRun({
@@ -559,12 +570,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
559
570
  });
560
571
  };
561
572
  const warn = say("warning");
562
- const deprecations = new RunDeprecations((text) => warn(text));
563
- if (definition.meta.whenToUse !== undefined) deprecations.used("whenToUse");
564
- if ((definition as { [DEPRECATED_DEFINITION]?: unknown })[DEPRECATED_DEFINITION])
565
- deprecations.used("defineExecutableWorkflow");
566
- // Each turn as it settles, so an attempt that dies keeps the sessions and times of its turns.
567
- const turns: TurnRecord[] = [];
573
+ const sessionLines = new SessionLines({ lines: options.run.lines, failed: (text) => warn(text) });
568
574
  const ledger = createRunLedger({ accounting: options.runtime.host.accounting, startedAt });
569
575
  const recordTurn: RecordTurn = (outcome, kind) => {
570
576
  const turn: TurnRecord = {
@@ -575,19 +581,16 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
575
581
  callPath: outcome.usage.callPath,
576
582
  ...pick(outcome.usage, TURN_OPERATION_FIELDS),
577
583
  };
578
- turns.push(turn);
579
584
  events.turn(turn);
580
585
  };
581
586
  const stages = new StageLedger({
582
- runDir,
583
- attempt,
584
- records: recorded,
587
+ lines: options.run.lines,
588
+ run: recorded,
589
+ versions,
585
590
  ...(options.run.fromStage === undefined ? {} : { fromStage: options.run.fromStage }),
586
591
  ...(options.run.values === undefined ? {} : { values: options.run.values }),
587
592
  ...(definition.meta.version === undefined ? {} : { workflowVersion: definition.meta.version }),
588
593
  ...(withEvents ? { byContent: true } : {}),
589
- ...(redo === undefined ? {} : { redo }),
590
- turns: () => turns,
591
594
  event: (body, at) => events.event(body, at),
592
595
  });
593
596
  const progress = new RunProgress();
@@ -601,6 +604,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
601
604
  cwd,
602
605
  deadline: options.deadline,
603
606
  log: (message) => warn(message),
607
+ opened: (record) => events.event({ kind: "sandbox-opened", ...record }),
604
608
  });
605
609
  if (sandboxes.hasRunSandbox) {
606
610
  try {
@@ -615,9 +619,11 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
615
619
  throw new Error(`the run's sandbox did not open: ${messageOf(error)}`);
616
620
  }
617
621
  }
622
+ const ops = new OpIds(runDir, options.run.lines.attempt, options.run.ops);
618
623
  const decisions = new RunDecisions({
619
624
  ...(options.decisions ? { installation: options.decisions } : {}),
620
625
  runDir,
626
+ ops,
621
627
  event: (body) => events.event(body),
622
628
  });
623
629
  const skills = new RunSkills({
@@ -628,26 +634,23 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
628
634
  const fatal = Promise.withResolvers<never>();
629
635
  void fatal.promise.catch(() => undefined);
630
636
  const now = options.now ?? Date.now;
631
- // A turn an earlier attempt started and never ended, its process gone, ends failed before the code
632
- // runs again (Q6); the turns whose ends no wait took are their agents' turns again.
633
- const restoredTurns = withEvents ? await recoverTurns(runDir, attempt, locked, now) : undefined;
634
- // An event run's agents reopen on the sessions its earlier attempts' turns ran on.
637
+ // An event run's agents reopen on the sessions its earlier attempts recorded.
635
638
  const keptSessions =
636
639
  withEvents && options.run.resumeSessions !== false
637
- ? keptSessionsOf(await readTurns(runDir))
640
+ ? keptSessionsOf(recorded.sessions)
638
641
  : undefined;
639
- // Every run's timers live in a mailbox; a run of a workflow with events takes its events there too.
642
+ // Every run's timers are its own lines; a run of a workflow with events takes its events with them.
640
643
  // A plain run without `park` waits for its timers in its process.
641
644
  const { park } = options.run;
642
- const waits = new RunMailbox({
645
+ const waits = new RunWaits({
643
646
  runDir,
644
- attempt,
647
+ lines: options.run.lines,
645
648
  locked,
646
649
  now,
647
650
  ...(park
648
651
  ? {
649
652
  parking: {
650
- idle: (stage) => owner.idle(stage),
653
+ idle: () => owner.idle(),
651
654
  timeout: (signal) => owner.timeout(signal),
652
655
  liveUntil: () => owner.liveUntil(),
653
656
  park: async (wait) => {
@@ -665,22 +668,28 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
665
668
  : {}),
666
669
  });
667
670
  await waits.open();
668
- const mailbox = withEvents ? waits : undefined;
669
- if (mailbox) options.run.listenNudges?.(() => mailbox.nudge());
670
- // The mailbox's parking asks the owner, made after it: by then it is.
671
+ const eventWaits = withEvents ? waits : undefined;
672
+ if (eventWaits) options.run.listenNudges?.(() => eventWaits.nudge());
673
+ // A turn an earlier attempt started whose end no wait took is its agent's turn again (the claim
674
+ // ended any a crash left running).
675
+ const restoredTurns = eventWaits?.restored;
676
+ // The waits' parking asks the owner, made after it: by then it is.
671
677
  const owner: WorkflowOwner = new WorkflowOwner({
672
- ...(mailbox ? { mailbox } : {}),
678
+ ...(eventWaits ? { eventWaits } : {}),
673
679
  waits,
674
680
  ...(keptSessions ? { keptSessions } : {}),
681
+ sessionLines,
675
682
  ...(restoredTurns ? { restoredTurns } : {}),
676
683
  failRun: (error) => fatal.reject(error),
677
684
  decisions,
685
+ ops,
678
686
  ...(options.allowance ? { allowance: options.allowance } : {}),
679
687
  sandboxes,
680
688
  skills,
681
689
  runDir,
682
690
  locks,
683
691
  environment,
692
+ ...(options.sandboxes?.homes ? { homes: options.sandboxes.homes } : {}),
684
693
  progress,
685
694
  recordTurn,
686
695
  stages,
@@ -696,7 +705,6 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
696
705
  ledger,
697
706
  onLog: warn,
698
707
  log: say("log"),
699
- deprecations,
700
708
  events,
701
709
  onCallerStopped: options.onCallerStopped,
702
710
  livenessPolicy: options.livenessPolicy,
@@ -746,7 +754,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
746
754
  failed = true;
747
755
  failure = error;
748
756
  }
749
- if (mailbox && !mailbox.parked)
757
+ if (eventWaits && !eventWaits.parked)
750
758
  await owner.endStartedTurns(failed ? "the workflow failed" : "the workflow returned");
751
759
  // Once `parked` is written, the attempt is parked, whatever the code did after.
752
760
  if (waits.parked) {
@@ -762,7 +770,6 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
762
770
  failure = ending;
763
771
  }
764
772
  bodyEnded = true;
765
- deprecations.settle();
766
773
  // What the body left running enters no stage; one left open is failed once its turns settle.
767
774
  stages.seal(failed ? messageOf(failure) : "the workflow returned before the stage ended");
768
775
  const cutReading = () => reading.abort();
@@ -795,12 +802,21 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
795
802
 
796
803
  // Before the wait for session files, which is bookkeeping rather than the run's own time.
797
804
  const finishedAt = new Date().toISOString();
805
+ // A session that moved as its agent closed is recorded before the attempt ends.
806
+ owner.recordSessions();
807
+ await sessionLines.flushed();
798
808
  await stages
799
809
  .close()
800
- .catch((error) => warn(`awf: a stage record was not written: ${messageOf(error)}`));
810
+ .catch((error) => warn(`awf: a stage's line was not written: ${messageOf(error)}`));
801
811
  await events.close();
802
812
  const usage = await ledger.settle(reading.signal);
803
813
  signal.removeEventListener("abort", cutReading);
814
+ // What a stop or the read's deadline left unread is the next attempt's to read again.
815
+ const reader = options.runtime.host.accounting;
816
+ const failedSpend = await keepSpend(runDir, attempt, usage, {
817
+ reads: (execution) => reader !== undefined && (reader.canRead?.(execution) ?? true),
818
+ }).catch((error: unknown) => [messageOf(error)]);
819
+ for (const each of failedSpend) options.onLog?.(`awf: spend.json not written for ${each}`);
804
820
  const times = { startedAt: new Date(startedAt).toISOString(), finishedAt };
805
821
  const opened = sandboxes.records();
806
822
  const given = skills.records();
@@ -942,6 +958,8 @@ class WorkflowOwner {
942
958
  readonly #cleanupFailures: Error[] = [];
943
959
  readonly context: WorkflowContext;
944
960
  readonly #agents = new Map<string, AgentEntry>();
961
+ /** The skills each agent's harness was given, as placed: what its session lines record. */
962
+ readonly #givenSkills = new Map<string, AgentSkills>();
945
963
  /** Each agent once it has opened, by key: what messaging asks about, and what a ref must be. */
946
964
  readonly #logical = new Map<string, LogicalAgent>();
947
965
  readonly #messaging: Messaging;
@@ -974,7 +992,9 @@ class WorkflowOwner {
974
992
  defaultWindowMs?: number;
975
993
  locks: CredentialLocks;
976
994
  environment: Readonly<Record<string, string | undefined>>;
995
+ homes?: HarnessHomes;
977
996
  decisions: RunDecisions;
997
+ ops: OpIds;
978
998
  allowance?: (harness: Harness) => Promise<HarnessAllowance>;
979
999
  stages: StageLedger;
980
1000
  runId: string;
@@ -992,20 +1012,21 @@ class WorkflowOwner {
992
1012
  onLog?: Warn;
993
1013
  /** The workflow's own log lines. */
994
1014
  log(message: string, fields?: JsonObject): void;
995
- deprecations: RunDeprecations;
996
1015
  events: RunEventLog;
997
1016
  onCallerStopped?: RunWorkflowOptions["onCallerStopped"];
998
1017
  awaitReply: NonNullable<RunWorkflowOptions["awaitReply"]>;
999
1018
  coordinators: readonly CoordinatorPort[];
1000
- /** A run of a workflow with `events`: its mailbox, which its waits take from. */
1001
- mailbox?: RunMailbox;
1019
+ /** A run of a workflow with `events`: its waits, which take its events. */
1020
+ eventWaits?: RunWaits;
1002
1021
  /** The run's time. */
1003
1022
  now: () => number;
1004
- /** Its timers: the mailbox for a run with `events`, one of its own for a plain run. */
1005
- waits: RunMailbox;
1006
- /** With `mailbox`: each root agent's last session in earlier attempts, which it reopens on. */
1023
+ /** Its timers and waits, every run's. */
1024
+ waits: RunWaits;
1025
+ /** With `eventWaits`: each agent's latest session in earlier attempts, which it reopens on. */
1007
1026
  keptSessions?: ReadonlyMap<string, NativeFork>;
1008
- /** With `mailbox`: each agent's turn an earlier attempt started whose end no wait took. */
1027
+ /** Where each session an agent is seen with is recorded, as it is. */
1028
+ sessionLines: SessionLines;
1029
+ /** With `eventWaits`: each agent's turn an earlier attempt started whose end no wait took. */
1009
1030
  restoredTurns?: ReadonlyMap<string, RestoredTurn>;
1010
1031
  emit: Emitter;
1011
1032
  /** The workflow's name, which with the run's id is the ref its events are `from`. */
@@ -1033,6 +1054,7 @@ class WorkflowOwner {
1033
1054
  },
1034
1055
  schedule: (at, action) => scheduleAt({ unixMilliseconds: at }, action),
1035
1056
  now: Date.now,
1057
+ question: (n) => `${options.attempt}.${n}`,
1036
1058
  record: async (event) => {
1037
1059
  this.#messagesChanged();
1038
1060
  if (event.kind === "message-sent") {
@@ -1061,7 +1083,6 @@ class WorkflowOwner {
1061
1083
  this.#tools = new ToolCalls<ExecutionScope>({
1062
1084
  record: (event) => options.events.event(event),
1063
1085
  runIn: (scope, fn) => (scope ? scopes.run(scope, fn) : scopes.exit(fn)),
1064
- deprecated: (name, call) => options.deprecations.used(name, call),
1065
1086
  });
1066
1087
  options.control.attachTools(this.#tools);
1067
1088
  const holder = {
@@ -1074,8 +1095,9 @@ class WorkflowOwner {
1074
1095
  runId: options.runId,
1075
1096
  attempt: options.attempt,
1076
1097
  cwd: options.cwd,
1098
+ // A new `Date` each time, so one a workflow changes isn't the run's.
1077
1099
  get deadline() {
1078
- return runDeadline(options.deadline, () => options.deprecations.used("run-deadline-ms"));
1100
+ return options.deadline && new Date(options.deadline.unixMilliseconds);
1079
1101
  },
1080
1102
  agents: {
1081
1103
  open: (spec) => {
@@ -1085,14 +1107,6 @@ class WorkflowOwner {
1085
1107
  return Promise.reject(error);
1086
1108
  }
1087
1109
  },
1088
- stop: (key, reason) => {
1089
- options.deprecations.used("agents-stop");
1090
- try {
1091
- return this.stopAgent(key, reason, "agents.stop");
1092
- } catch (error) {
1093
- return Promise.reject(error);
1094
- }
1095
- },
1096
1110
  caller: (spec) => {
1097
1111
  try {
1098
1112
  return this.caller(spec.key);
@@ -1136,7 +1150,6 @@ class WorkflowOwner {
1136
1150
  const decided = this.track(
1137
1151
  options.decisions.decide(spec, {
1138
1152
  deadline: scope?.deadline ?? options.deadline,
1139
- deprecated: (name, call) => options.deprecations.used(name, call),
1140
1153
  ...(scope ? { add: (cancel) => scope.add((stop) => cancel(stop.reason)) } : {}),
1141
1154
  ...(scope?.stage === undefined ? {} : { stage: scope.stage }),
1142
1155
  }),
@@ -1173,12 +1186,7 @@ class WorkflowOwner {
1173
1186
  const inherited = scopes.getStore()?.deadline ?? options.deadline;
1174
1187
  let deadline: AbsoluteDeadline | undefined;
1175
1188
  try {
1176
- deadline = boundedDeadline(
1177
- { timeout: parallelOptions?.timeout, deadline: parallelOptions?.deadline },
1178
- inherited,
1179
- (name, call) => options.deprecations.used(name, call),
1180
- "on parallel",
1181
- );
1189
+ deadline = boundedDeadline(parallelOptions?.timeout, inherited);
1182
1190
  } catch (error) {
1183
1191
  return Promise.reject(error);
1184
1192
  }
@@ -1216,7 +1224,7 @@ class WorkflowOwner {
1216
1224
  undefined,
1217
1225
  );
1218
1226
  },
1219
- // Every run waits on its own mailbox; one without events never parks, so it waits here.
1227
+ // Every run waits on its own lines; one without events never parks, so it waits here.
1220
1228
  receive: (spec?: WaitOptions) => this.receive(options.waits, spec),
1221
1229
  race: ((name: string, branches: RaceBranches, raceOptions?: WaitOptions) =>
1222
1230
  this.race(options.waits, name, branches, raceOptions)) as WorkflowContext["race"],
@@ -1235,8 +1243,8 @@ class WorkflowOwner {
1235
1243
  return new Date(options.now() + milliseconds);
1236
1244
  },
1237
1245
  signal: ((name: string) => signalSource(name, options.declared)) as WorkflowContext["signal"],
1238
- ...(options.mailbox?.started
1239
- ? { started: options.mailbox.started as WorkflowContext["started"] }
1246
+ ...(options.eventWaits?.started
1247
+ ? { started: options.eventWaits.started as WorkflowContext["started"] }
1240
1248
  : {}),
1241
1249
  };
1242
1250
  }
@@ -1298,12 +1306,11 @@ class WorkflowOwner {
1298
1306
  // A turn's end taken: its agent takes other work again.
1299
1307
  for (const event of taken) {
1300
1308
  if (event.name !== "agent:ended") continue;
1301
- this.#reporting((event.data as { outcome?: unknown }).outcome);
1302
1309
  const { turn } = event.data as unknown as TurnEnd;
1303
1310
  if (this.#restored.get(event.from)?.turn === turn) this.#restored.delete(event.from);
1304
1311
  this.#logical.get(event.from)?.endTaken(turn);
1305
1312
  }
1306
- // The mailbox keeps awf's record names; the code is handed the names it declares.
1313
+ // `in.jsonl` and the turns' ends keep awf's record names; the code is handed the names it declares.
1307
1314
  return taken.map((event) => presented(event, this.options.declared));
1308
1315
  } finally {
1309
1316
  removeFromScope?.();
@@ -1312,7 +1319,7 @@ class WorkflowOwner {
1312
1319
  }
1313
1320
 
1314
1321
  /** A timer, by name: the recorded one, or set now. A race's own are named `{race}@{branch}`. */
1315
- private timer(mailbox: RunMailbox, name: string, when: When, own = false): Timer {
1322
+ private timer(waits: RunWaits, name: string, when: When, own = false): Timer {
1316
1323
  if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
1317
1324
  if (typeof name !== "string") throw new Error("a timer's name is a string");
1318
1325
  if (!own) {
@@ -1322,7 +1329,7 @@ class WorkflowOwner {
1322
1329
  if (problem) throw new Error(`timer ${name}: its name is ${problem}`);
1323
1330
  }
1324
1331
  const given = parseWhen(when, this.options.now());
1325
- const { timer, asked } = mailbox.setTimer(name, given.due, given.when);
1332
+ const { timer, asked } = waits.setTimer(name, given.due, given.when);
1326
1333
  if (timer.cancelled)
1327
1334
  this.options.onLog?.(
1328
1335
  `awf: timer ${name} was cancelled; a name stays cancelled, so it won't fire`,
@@ -1331,12 +1338,12 @@ class WorkflowOwner {
1331
1338
  this.options.onLog?.(
1332
1339
  `awf: timer ${name} is due ${timer.due}, as first set (${timer.when}); ${asked} is ignored`,
1333
1340
  );
1334
- return timerSource(mailbox, name, timer.due);
1341
+ return timerSource(waits, name, timer.due);
1335
1342
  }
1336
1343
 
1337
1344
  /** Waits for the first of `branches`, recorded under `name`. */
1338
1345
  private async race(
1339
- mailbox: RunMailbox,
1346
+ waits: RunWaits,
1340
1347
  name: string,
1341
1348
  branches: RaceBranches,
1342
1349
  raceOptions: WaitOptions | undefined,
@@ -1368,7 +1375,7 @@ class WorkflowOwner {
1368
1375
  const owned = new Map<string, string>();
1369
1376
  const pairs: (readonly [string, WaitTake])[] = given.map(([branch, source]) => {
1370
1377
  if (!own(source)) return [branch, takes.get(branch) as WaitTake] as const;
1371
- const timer = this.timer(mailbox, `${name}@${branch}`, source as When, true);
1378
+ const timer = this.timer(waits, `${name}@${branch}`, source as When, true);
1372
1379
  owned.set(branch, timer.name);
1373
1380
  return [branch, { name: "timer:due", from: timer.name }] as const;
1374
1381
  });
@@ -1376,7 +1383,7 @@ class WorkflowOwner {
1376
1383
  let event: WorkflowEvent<string, JsonValue>;
1377
1384
  try {
1378
1385
  [event] = (await this.#holdingWait(scope, async (signal) => {
1379
- const won = await mailbox.race(name, pairs, owned, label, signal);
1386
+ const won = await waits.race(name, pairs, owned, label, signal);
1380
1387
  branch = won.branch;
1381
1388
  return [won.event];
1382
1389
  })) as [WorkflowEvent<string, JsonValue>];
@@ -1384,7 +1391,7 @@ class WorkflowOwner {
1384
1391
  // Cut short, not parked: its own timers would fire into a later wait. A park's rerun races
1385
1392
  // again, and so does a continue's after a park refused short of its --from-stage.
1386
1393
  if (!(error instanceof RunParked || error instanceof FromStageUnreached))
1387
- await Promise.all([...owned.values()].map((timer) => mailbox.cancelTimer(timer))).catch(
1394
+ await Promise.all([...owned.values()].map((timer) => waits.cancelTimer(timer))).catch(
1388
1395
  () => undefined,
1389
1396
  );
1390
1397
  throw error;
@@ -1396,7 +1403,7 @@ class WorkflowOwner {
1396
1403
 
1397
1404
  /** A loop over the events `branches` take, one at a time. */
1398
1405
  private select(
1399
- mailbox: RunMailbox,
1406
+ waits: RunWaits,
1400
1407
  branches: Branches,
1401
1408
  selectOptions: WaitOptions | undefined,
1402
1409
  ): AsyncIterable<Next<Branches>> {
@@ -1411,7 +1418,9 @@ class WorkflowOwner {
1411
1418
  ([branch, source]) =>
1412
1419
  [
1413
1420
  branch,
1414
- reads.has(branch) ? { name: AT_DUE, from: branch } : sourceTake("select", branch, source),
1421
+ reads.has(branch)
1422
+ ? { name: "at:due", from: branch }
1423
+ : sourceTake("select", branch, source),
1415
1424
  ] as const,
1416
1425
  );
1417
1426
  this.#checkSources("select", pairs);
@@ -1427,7 +1436,7 @@ class WorkflowOwner {
1427
1436
  if (scope?.stage !== undefined)
1428
1437
  throw new Error(`select is in stage ${scope.stage}: a loop's wait is not a stage's`);
1429
1438
  const [event] = await this.#holdingWait(scope, async (signal) => [
1430
- await mailbox.select(takes, labelOf(selectOptions?.label), signal),
1439
+ await waits.select(takes, labelOf(selectOptions?.label), signal),
1431
1440
  ]);
1432
1441
  const taken = event as WorkflowEvent<string, JsonValue>;
1433
1442
  const branch = branchOf(pairs, taken) as string;
@@ -1442,58 +1451,37 @@ class WorkflowOwner {
1442
1451
  };
1443
1452
  }
1444
1453
 
1445
- /** A started turn among a wait's branches, which naming its agent replaces. */
1446
1454
  /**
1447
- * A wait's branches as a run can take them: a started turn among them is deprecated, and a run
1448
- * without events, which hears no agent's end or reply to the code, refuses a branch waiting on
1449
- * one, which would never come.
1455
+ * A wait's branches as a run can take them: a run without events, which hears no agent's end or
1456
+ * reply to the code, refuses a branch waiting on one, which would never come.
1450
1457
  */
1451
1458
  #checkSources(what: string, takes: Iterable<readonly [string, WaitTake]>): void {
1452
1459
  const given = [...takes];
1453
- if (!this.options.mailbox) {
1460
+ if (!this.options.eventWaits) {
1454
1461
  const unheard = given.find(([, take]) => HEARD_WITH_EVENTS.has(take.name));
1455
1462
  if (unheard)
1456
1463
  throw new Error(
1457
1464
  `${what}'s ${unheard[0]} waits on ${unheard[1].name === "agent:ended" ? "an agent's end" : "a participant's reply to the code"}, which a run without events never hears; declare events in the workflow`,
1458
1465
  );
1459
1466
  }
1460
- if (given.some(([, take]) => take.turn !== undefined))
1461
- this.options.deprecations.used("turn-source");
1462
1467
  }
1463
1468
 
1464
- /** `outcome`, which a deprecated name the contract defines reports its use on. */
1465
- // Best effort: an outcome the workflow copies or freezes, or a stage reuses, reports nothing.
1466
- #reporting = <T>(outcome: T): T => {
1467
- if (typeof outcome === "object" && outcome !== null && !Object.isFrozen(outcome))
1468
- Object.defineProperty(outcome, REPORT_DEPRECATED, {
1469
- value: this.options.deprecations.reported,
1470
- configurable: true,
1471
- });
1472
- return outcome;
1473
- };
1474
-
1475
1469
  #waiting = false;
1476
1470
  /** The races and sleeps this attempt has run, each by its name once. */
1477
1471
  readonly #raced = new Set<string>();
1478
1472
  /** Each agent's turn an earlier attempt started whose end no wait has taken yet. */
1479
1473
  readonly #restored: Map<string, RestoredTurn>;
1480
1474
 
1481
- /** How an agent of an event run records its started turns in the mailbox. */
1482
- #turnsOf(key: string, mailbox: RunMailbox): LogicalAgentTurns {
1475
+ /** How an agent of an event run records its started turns in `run.jsonl`. */
1476
+ #turnsOf(key: string, waits: RunWaits): LogicalAgentTurns {
1483
1477
  return {
1484
- begin: (turn, meta, execution) => mailbox.beginTurn(key, turn, meta, execution),
1485
- end: (turn, meta, outcome) => mailbox.endTurn(key, { turn, meta, outcome }),
1478
+ begin: (turn, meta, execution) => waits.beginTurn(key, turn, meta, execution),
1479
+ end: (turn, meta, outcome) => waits.endTurn(key, { turn, meta, outcome }),
1486
1480
  checkDeclared: () => {
1487
- const { declared, deprecations } = this.options;
1488
- if (declared.has(`${key}:ended`)) return;
1489
- // A workflow that declares an end has moved to declaring them; one written before can't have.
1490
- if ([...declared].some((name) => awfEventOf(name)?.kind === "ended"))
1481
+ if (!this.options.declared.has(`${key}:ended`))
1491
1482
  throw new Error(
1492
- key.includes(":")
1493
- ? `agent ${key}: start()'s end arrives as {key}:ended, which a key with a colon can't name: rename it ${key.replaceAll(":", "-")} and declare "${key.replaceAll(":", "-")}:ended": AgentEnded in events`
1494
- : `agent ${key}: start()'s end arrives as ${key}:ended, which the workflow declares in events, "${key}:ended": AgentEnded, as it does its other agents' ends`,
1483
+ `agent ${key}: start()'s end arrives as ${key}:ended, which the workflow declares in events, "${key}:ended": AgentEnded`,
1495
1484
  );
1496
- deprecations.used("undeclared-ended", key);
1497
1485
  },
1498
1486
  // Read when the agent opens: a wait before then may have taken the end.
1499
1487
  restored: () => this.#restored.get(key),
@@ -1518,10 +1506,9 @@ class WorkflowOwner {
1518
1506
  * Nothing of the run is working but its wait: no agent's turn, message or compaction, and no
1519
1507
  * decision, sandbox or agent opening in flight.
1520
1508
  */
1521
- idle(waitStage?: string): boolean {
1522
- const open = this.options.stages.openStage;
1509
+ idle(): boolean {
1523
1510
  return (
1524
- (open === undefined || open === waitStage) &&
1511
+ this.options.stages.openStage === undefined &&
1525
1512
  this.#inFlight.size === 0 &&
1526
1513
  [...this.#logical.values()].every((agent) => !agent.busy)
1527
1514
  );
@@ -1561,36 +1548,17 @@ class WorkflowOwner {
1561
1548
  }
1562
1549
  return false;
1563
1550
  }
1564
- /** The batch a wait in a stage took, by stage, until the stage ends: what it must return. */
1565
- readonly #stageBatches = new Map<string, WorkflowEvent<string, JsonValue>[]>();
1566
- /** Stages around a wait this attempt entered: each is entered once, as a loop over one spins. */
1567
- readonly #waitedStages = new Set<string>();
1568
-
1569
- /** A wait: one at a time, and in a stage, the stage's batch. */
1551
+ /** A wait, outside any stage: a wait a rerun gets the same answer from is a `race`. */
1570
1552
  private async receive(
1571
- mailbox: RunMailbox,
1553
+ waits: RunWaits,
1572
1554
  spec: WaitOptions | undefined,
1573
1555
  ): Promise<WorkflowEvent<string, JsonValue>[]> {
1574
1556
  const scope = this.#beginWait("receive()");
1575
- const label = receiveLabel(spec);
1576
- if (scope?.stage !== undefined) this.options.deprecations.used("stage-receive");
1577
- if (scope?.stage !== undefined && this.#stageBatches.has(scope.stage))
1578
- throw new Error(`stage ${scope.stage} has waited once; a stage holds one wait`);
1579
- try {
1580
- const taken = await this.#holdingWait(scope, (signal) =>
1581
- mailbox.receive(label, scope?.stage, signal),
1557
+ if (scope?.stage !== undefined)
1558
+ throw new Error(
1559
+ `receive() is refused in stage ${scope.stage}: wait outside it with wf.race(name, …), which is recorded under its name`,
1582
1560
  );
1583
- if (scope?.stage !== undefined) {
1584
- this.#stageBatches.set(scope.stage, taken);
1585
- this.#waitedStages.add(scope.stage);
1586
- }
1587
- return taken;
1588
- } catch (error) {
1589
- // Parked in a stage: left unended now, before anything closes it, for the next attempt.
1590
- if (error instanceof RunParked && scope?.stage !== undefined)
1591
- this.options.stages.abandon(scope.stage);
1592
- throw error;
1593
- }
1561
+ return this.#holdingWait(scope, (signal) => waits.receive(receiveLabel(spec), signal));
1594
1562
  }
1595
1563
 
1596
1564
  /** An agent as messaging sees it, from its entry and its operations. */
@@ -1621,27 +1589,14 @@ class WorkflowOwner {
1621
1589
  !isJsonValue(labels))
1622
1590
  )
1623
1591
  throw new Error("a participant's labels are a JSON object");
1624
- const to = (spec as { to?: unknown }).to;
1625
- if (to !== undefined && to !== "code")
1626
- throw new Error(`a participant's messages go to agents, or to: "code"; not ${String(to)}`);
1627
- const { declared, deprecations } = this.options;
1628
- const replies = declared.has(`${key}:reply`);
1629
- if (to === "code") {
1630
- if (!this.options.mailbox)
1631
- throw new Error(
1632
- "a participant routed to the code needs a workflow with events, whose waits take its replies",
1633
- );
1634
- // A workflow that declares a reply has moved to declaring them; one written before can't have.
1635
- if (!replies && [...declared].some((name) => awfEventOf(name)?.kind === "reply"))
1636
- throw new Error(
1637
- `participant ${key}: its messages reach the code when "${key}:reply": ParticipantReply is declared in events, as this workflow's other replies are; to: "code" is deprecated`,
1638
- );
1639
- deprecations.used("to-code", key);
1640
- }
1592
+ if ((spec as { to?: unknown }).to !== undefined)
1593
+ throw new Error(
1594
+ `participant ${key} is connected without to: its messages reach the code when the workflow declares "${key}:reply": ParticipantReply in events`,
1595
+ );
1641
1596
  this.#messaging.connect(
1642
1597
  key,
1643
1598
  labels === undefined ? undefined : structuredClone(labels),
1644
- to === "code" || replies ? "code" : undefined,
1599
+ this.options.declared.has(`${key}:reply`) ? "code" : undefined,
1645
1600
  );
1646
1601
  const ref: ParticipantRef = {
1647
1602
  ...participantRef(key),
@@ -1656,21 +1611,6 @@ class WorkflowOwner {
1656
1611
  return this.#messaging.release(key, reason);
1657
1612
  },
1658
1613
  };
1659
- if (to === "code") {
1660
- const take = { name: "participant:reply", from: key };
1661
- const routed = asSource(
1662
- Object.freeze({
1663
- ...ref,
1664
- is: (event: { name: string; from: string }) => {
1665
- deprecations.used("participant-is");
1666
- return matches(event, take);
1667
- },
1668
- }),
1669
- take,
1670
- ) as unknown as CodeParticipantRef;
1671
- this.#outsideRefs.set(key, routed);
1672
- return routed;
1673
- }
1674
1614
  this.#outsideRefs.set(key, ref);
1675
1615
  return ref;
1676
1616
  }
@@ -1678,7 +1618,9 @@ class WorkflowOwner {
1678
1618
  /** A key an agent may not take: a reserved one, or an outside participant's. */
1679
1619
  #assertFreeKey(key: string, who: string): void {
1680
1620
  if (typeof key === "string" && key.includes(":"))
1681
- this.options.deprecations.used("colon-key", key);
1621
+ throw new Error(
1622
+ `${key} has a colon, kept for awf's own event names, {key}:ended and {key}:reply; ${who} takes another key, such as ${key.replaceAll(":", "-")}`,
1623
+ );
1682
1624
  if ((RESERVED_PARTICIPANT_KEYS as readonly string[]).includes(key.toLowerCase()))
1683
1625
  throw new Error(
1684
1626
  `${key} is reserved: an agent could read a message from it as authority; ${who} takes another key`,
@@ -1726,11 +1668,9 @@ class WorkflowOwner {
1726
1668
  throw new Error("a route between agents names two of them");
1727
1669
  ends = { between: [end(shape.between[0]), end(shape.between[1])] };
1728
1670
  } else ends = { from: end(shape.from), to: end(shape.to) };
1729
- if (access.timeout != null && access.timeoutMs != null)
1730
- throw new Error("a route's timeout is given alone: timeoutMs is its deprecated form");
1731
1671
  const timeoutMs =
1732
1672
  access.timeout == null
1733
- ? (access.timeoutMs ?? undefined)
1673
+ ? undefined
1734
1674
  : positiveDurationMilliseconds(access.timeout, "a route's timeout");
1735
1675
  this.#messaging.grant(ends, {
1736
1676
  purpose: access.purpose,
@@ -1738,8 +1678,6 @@ class WorkflowOwner {
1738
1678
  timeoutMs,
1739
1679
  scope: scope?.messages ?? this.#messaging.attempt,
1740
1680
  });
1741
- if (access.timeout == null && access.timeoutMs != null)
1742
- this.options.deprecations.used("timeout-ms", "on a route");
1743
1681
  }
1744
1682
 
1745
1683
  /** Who owns a message operation a route in `scope` opens: its stage, or the attempt. */
@@ -1911,22 +1849,7 @@ class WorkflowOwner {
1911
1849
  });
1912
1850
  if (entered.kind === "reuse") {
1913
1851
  entered.release();
1914
- // A stage around a wait returns the batch it took, which its record doesn't hold.
1915
- const batch =
1916
- entered.value === undefined && !options
1917
- ? (await this.options.mailbox?.batchOf(name))?.map((event) =>
1918
- presented(event, this.options.declared),
1919
- )
1920
- : undefined;
1921
- if (batch) {
1922
- this.options.deprecations.used("stage-receive");
1923
- if (this.#waitedStages.has(name))
1924
- throw new Error(
1925
- `stage ${name} around a wait was entered again in this attempt, and would return the same batch forever; name it by what it waits for`,
1926
- );
1927
- this.#waitedStages.add(name);
1928
- }
1929
- return (batch as JsonValue | undefined) ?? entered.value;
1852
+ return entered.value;
1930
1853
  }
1931
1854
  const open = entered.stage;
1932
1855
  const messages = this.#messaging.scope(name);
@@ -1944,22 +1867,9 @@ class WorkflowOwner {
1944
1867
  this.#messaging.finishing(messages);
1945
1868
  scope.seal();
1946
1869
  await this.drainMessages(messages, () => scope.settleOwned());
1947
- // A timer it set is written before its record, which a rerun reuses without its work.
1870
+ // A timer it set is written before its line, which a rerun reuses without its work.
1948
1871
  await this.options.waits.flush();
1949
1872
  if (this.options.stages.stopped?.stage === name) throw this.options.stages.caught();
1950
- const batch = this.#stageBatches.get(name);
1951
- if (batch && !options) {
1952
- const same =
1953
- Array.isArray(returned) &&
1954
- returned.length === batch.length &&
1955
- returned.every((event, i) => (event as { id?: unknown })?.id === batch[i]?.id);
1956
- if (!same)
1957
- throw new Error(
1958
- `stage ${name} returned other events than its wait took; its work is the wait alone`,
1959
- );
1960
- await open.succeed(undefined);
1961
- return batch as JsonValue;
1962
- }
1963
1873
  const value = stageValue(name, options, returned);
1964
1874
  await open.succeed(value, summaryOf(name, options, value, this.options.onLog));
1965
1875
  return value;
@@ -1979,52 +1889,31 @@ class WorkflowOwner {
1979
1889
  }
1980
1890
  throw error;
1981
1891
  } finally {
1982
- this.#stageBatches.delete(name);
1983
1892
  this.#messaging.closeScope(messages);
1984
1893
  this.#stageScopes.delete(messages);
1985
1894
  removeFromParent?.();
1986
1895
  }
1987
1896
  }
1988
1897
 
1989
- /** An agent's pane options as written, which a reopen is compared by; a deprecated one logged. */
1990
- private writtenPane(spec: {
1991
- pane?: PaneSpec;
1992
- layout?: PaneLayout;
1993
- keepPane?: KeepPane;
1994
- }): Pick<AgentIdentity, "pane" | "layout" | "keepPane"> {
1995
- if (spec.layout !== undefined) this.options.deprecations.used("pane-layout");
1996
- if (spec.keepPane !== undefined) this.options.deprecations.used("keep-pane");
1997
- return {
1998
- ...(spec.pane === undefined ? {} : { pane: storedLayout(spec.pane) }),
1999
- ...(spec.layout === undefined ? {} : { layout: storedLayout(spec.layout) }),
2000
- ...(spec.keepPane === undefined ? {} : { keepPane: spec.keepPane }),
2001
- };
2002
- }
2003
-
2004
1898
  private openAgent(spec: AgentOpenSpec): Promise<AgentRef> {
2005
1899
  if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
2006
- if (spec.labels !== undefined) this.options.deprecations.used("labels");
2007
1900
  scopes.getStore()?.assertAccepting();
2008
1901
  this.#assertFreeKey(spec.key, "an agent");
2009
1902
  if (this.#caller?.key === spec.key) {
2010
1903
  throw new Error(`agent ${spec.key} is the calling session; it is not opened`);
2011
1904
  }
1905
+ this.options.ops.claim(spec.key, "agent");
2012
1906
  const inRunSandbox = this.options.sandboxes.hasRunSandbox;
2013
1907
  if (inRunSandbox && spec.sandbox !== undefined)
2014
1908
  throw new Error(`agent ${spec.key}: ${RUN_SANDBOX_ONLY}`);
2015
1909
  const existing = this.#agents.get(spec.key);
2016
1910
  if (existing?.stopped !== undefined) throw stoppedError(spec.key, existing.stopped);
2017
1911
  const given = spec.keepAlive;
2018
- if (given !== undefined && !this.options.mailbox)
1912
+ if (given !== undefined && !this.options.eventWaits)
2019
1913
  throw new Error(
2020
1914
  `agent ${spec.key}: keepAlive is for a workflow with events, whose runs park; this one's never do`,
2021
1915
  );
2022
- const keepAlive =
2023
- given === undefined
2024
- ? undefined
2025
- : checkedKeepAlive(spec.key, given, () =>
2026
- this.options.deprecations.used("keep-alive-timeout-ms"),
2027
- );
1916
+ const keepAlive = given === undefined ? undefined : checkedKeepAlive(spec.key, given);
2028
1917
  const execution = existing
2029
1918
  ? constrainExistingExecution(spec.runtime, existing.identity.execution)
2030
1919
  : withKnownEffort(resolveExecution(spec.runtime, this.options.runtime));
@@ -2034,25 +1923,19 @@ class WorkflowOwner {
2034
1923
  spec.sandbox !== undefined || existing?.identity.sandbox !== undefined || inRunSandbox,
2035
1924
  };
2036
1925
  checkPaneOptions(spec.key, spec, paneless);
2037
- const written = this.writtenPane(spec);
1926
+ const pane = spec.pane === undefined ? undefined : storedPane(spec.pane);
2038
1927
  const identity: AgentIdentity = {
2039
1928
  execution,
2040
1929
  cwd: spec.cwd ?? this.options.cwd,
2041
1930
  ...(spec.instructions === undefined ? {} : { instructions: spec.instructions }),
2042
- ...(spec.labels === undefined ? {} : { labels: structuredClone(spec.labels) }),
2043
1931
  ...(spec.sandbox === undefined ? {} : { sandbox: spec.sandbox }),
2044
1932
  ...(spec.skills === undefined ? {} : { skills: readSkillSources(spec.skills) }),
2045
- ...written,
2046
- asked: paneAsked(written, this.options.panes, !paneless.headless && !paneless.sandboxed),
1933
+ ...(pane === undefined ? {} : { pane }),
1934
+ asked: paneAsked(pane, this.options.panes, !paneless.headless && !paneless.sandboxed),
2047
1935
  };
2048
1936
  const scope = scopes.getStore();
2049
1937
  const inheritedDeadline = scope?.deadline ?? this.options.deadline;
2050
- const effectiveDeadline = boundedDeadline(
2051
- { timeout: spec.timeout, deadline: spec.deadline },
2052
- inheritedDeadline,
2053
- (name, call) => this.options.deprecations.used(name, call),
2054
- "on open",
2055
- );
1938
+ const effectiveDeadline = boundedDeadline(spec.timeout, inheritedDeadline);
2056
1939
  assertDeadline(effectiveDeadline);
2057
1940
  if (existing) {
2058
1941
  assertCompatibleAgent(spec.key, existing.identity, identity, spec);
@@ -2107,8 +1990,7 @@ class WorkflowOwner {
2107
1990
  // A kept session already holds the instructions; they are not sent again.
2108
1991
  ...(kept ? { continues: kept } : {}),
2109
1992
  ...(spec.instructions === undefined || kept ? {} : { instructions: spec.instructions }),
2110
- ...(this.options.mailbox ? { paneAtFirstTurn: true as const } : {}),
2111
- ...(spec.labels === undefined ? {} : { labels: spec.labels }),
1993
+ ...(this.options.eventWaits ? { paneAtFirstTurn: true as const } : {}),
2112
1994
  ...(seated ? { occupant: seated.occupant } : {}),
2113
1995
  ...(seated?.webSearch ? { webSearch: true } : {}),
2114
1996
  ...(skills ? { skills } : {}),
@@ -2148,7 +2030,7 @@ class WorkflowOwner {
2148
2030
 
2149
2031
  /**
2150
2032
  * Ends an agent the workflow has no more use for, as an operation that fails ends it: what it
2151
- * is running is cancelled, and its pane closed or, where `keepPane` says, kept and released.
2033
+ * is running is cancelled, and its pane closed or, where `--keep-panes` says, kept and released.
2152
2034
  */
2153
2035
  private stopAgent(key: string, reason: string | undefined, what: string): Promise<boolean> {
2154
2036
  if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
@@ -2214,6 +2096,7 @@ class WorkflowOwner {
2214
2096
  const scope = scopes.getStore();
2215
2097
  scope?.assertAccepting();
2216
2098
  this.#assertFreeKey(key, "an agent");
2099
+ this.options.ops.claim(key, "agent");
2217
2100
  const found = this.options.runtime.host.caller;
2218
2101
  if (!found) return Promise.resolve(null);
2219
2102
  if (this.#caller) {
@@ -2241,7 +2124,9 @@ class WorkflowOwner {
2241
2124
  };
2242
2125
  const ledger = this.options.ledger.agent(accounted);
2243
2126
  this.options.skills.record(key, "operator");
2244
- const opened = this.options.control.openChannel(key, (id) => sessions.launcher.add(id));
2127
+ const opened = this.options.control.openChannel(key, (id) =>
2128
+ this.launcherReported(key, execution.harness, sessions, id),
2129
+ );
2245
2130
  const state = opened.then(async (channel) => {
2246
2131
  try {
2247
2132
  const launcher = await installAgentLauncher(
@@ -2337,7 +2222,10 @@ class WorkflowOwner {
2337
2222
  ): { state: Promise<LogicalAgent>; channel: Promise<ResultChannel> } {
2338
2223
  const opened = placed.then((skills) => {
2339
2224
  if (skills?.given.ownHome) accounted.home = skills.given.ownHome;
2340
- return this.options.control.openChannel(spec.key, (id) => sessions.launcher.add(id));
2225
+ if (skills) this.#givenSkills.set(spec.key, skills.given);
2226
+ return this.options.control.openChannel(spec.key, (id) =>
2227
+ this.launcherReported(spec.key, execution.harness, sessions, id),
2228
+ );
2341
2229
  });
2342
2230
  const reachable = opened.then(async (channel) => ({
2343
2231
  channel,
@@ -2415,7 +2303,7 @@ class WorkflowOwner {
2415
2303
  const given = skillsLayout(
2416
2304
  execution.harness,
2417
2305
  resolved.map((skill) => skill.name),
2418
- { bundle: join(runDir, "agents", id), cwd: realCwd },
2306
+ { bundle: join(workDir(runDir), "agents", id), cwd: realCwd },
2419
2307
  );
2420
2308
  this.options.skills.record(key, resolved, given.ownHome);
2421
2309
  const { ownHome } = given;
@@ -2426,8 +2314,12 @@ class WorkflowOwner {
2426
2314
  await mkdir(dirname(ownHome), { recursive: true });
2427
2315
  const seeded = await seedHome(
2428
2316
  ownHome,
2429
- join(runDir, "staging", id),
2430
- hostHome(execution.harness, ownHome, this.options.environment),
2317
+ join(workDir(runDir), "staging", id),
2318
+ (this.options.homes ?? OPERATOR_HOMES).host(
2319
+ execution.harness,
2320
+ ownHome,
2321
+ this.options.environment,
2322
+ ),
2431
2323
  realCwd,
2432
2324
  this.options.locks,
2433
2325
  async (staged) => {
@@ -2476,10 +2368,11 @@ class WorkflowOwner {
2476
2368
  // What the agent really ran in, which is where accounting finds its sessions.
2477
2369
  accounted.cwd = seat.cwd;
2478
2370
  accounted.home = seat.home;
2371
+ if (seat.skills) this.#givenSkills.set(spec.key, seat.skills);
2479
2372
  let channel: ResultChannel | undefined;
2480
2373
  try {
2481
2374
  channel = await this.options.control.openChannel(spec.key, (id) =>
2482
- sessions.launcher.add(id),
2375
+ this.launcherReported(spec.key, execution.harness, sessions, id),
2483
2376
  );
2484
2377
  const door = await installSandboxedDoor(
2485
2378
  dirname(channel.endpoint),
@@ -2543,6 +2436,7 @@ class WorkflowOwner {
2543
2436
  execution,
2544
2437
  session,
2545
2438
  slots: this.options.slots,
2439
+ ops: this.options.ops,
2546
2440
  endpoint,
2547
2441
  launcher,
2548
2442
  deadline: this.options.deadline,
@@ -2550,6 +2444,11 @@ class WorkflowOwner {
2550
2444
  progress: this.options.progress,
2551
2445
  recordTurn: this.options.recordTurn,
2552
2446
  events: this.options.events,
2447
+ sessionsKnown: (sessions, operation) =>
2448
+ this.options.sessionLines.known(key, sessions, {
2449
+ ...(operation === undefined ? {} : { operation }),
2450
+ ...this.placeOf(key),
2451
+ }),
2553
2452
  track: (promise) => this.track(promise),
2554
2453
  isRunClosing: () => this.#closed || this.#cleanupFailures.length > 0,
2555
2454
  checkOperation: () => this.options.stages.checkOperation(),
@@ -2559,7 +2458,7 @@ class WorkflowOwner {
2559
2458
  this.options.failRun(error);
2560
2459
  },
2561
2460
  livenessPolicy: this.options.livenessPolicy,
2562
- ...(this.options.mailbox ? { turns: this.#turnsOf(key, this.options.mailbox) } : {}),
2461
+ ...(this.options.eventWaits ? { turns: this.#turnsOf(key, this.options.eventWaits) } : {}),
2563
2462
  fork: (spec, take, settings) => this.forkAgent(key, spec, take, settings),
2564
2463
  close: (reason) => {
2565
2464
  try {
@@ -2568,8 +2467,6 @@ class WorkflowOwner {
2568
2467
  return Promise.reject(error);
2569
2468
  }
2570
2469
  },
2571
- deprecated: (name, subject) => this.options.deprecations.used(name, subject),
2572
- reporting: (outcome) => this.#reporting(outcome),
2573
2470
  keepPane: () => this.#agents.get(key)?.identity.asked?.keepPane,
2574
2471
  keepAlive: () => this.#agents.get(key)?.keepAlive,
2575
2472
  defaultWindowMs: this.options.defaultWindowMs ?? DEFAULT_WINDOW_MS,
@@ -2613,7 +2510,6 @@ class WorkflowOwner {
2613
2510
  unopened?: AgentEntry,
2614
2511
  ): Promise<AgentRef> {
2615
2512
  if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
2616
- if (spec.labels !== undefined) this.options.deprecations.used("labels");
2617
2513
  const scope = scopes.getStore();
2618
2514
  scope?.assertAccepting();
2619
2515
  this.options.stages.checkOperation();
@@ -2621,20 +2517,13 @@ class WorkflowOwner {
2621
2517
  throw new Error(`agent ${spec.key} is the calling session; it is not opened`);
2622
2518
  }
2623
2519
  this.#assertFreeKey(spec.key, "an agent");
2520
+ this.options.ops.claim(spec.key, "agent");
2624
2521
  // Out before the clone, which throws on a handler, and out of the spec a repeat is compared by.
2625
2522
  // A bound is the call's, not the fork's: a repeat with another is the same fork.
2626
2523
  const { tools, timeout, ...given } = spec;
2627
- const forkSpec = structuredClone(
2628
- forkSpecOf(given, () => this.options.deprecations.used("fork-runtime")),
2629
- );
2630
- if (forkSpec.layout !== undefined) forkSpec.layout = storedLayout(forkSpec.layout);
2631
- if (forkSpec.pane !== undefined) forkSpec.pane = storedLayout(forkSpec.pane);
2632
- const deadline = boundedDeadline(
2633
- { timeout },
2634
- scope?.deadline ?? this.options.deadline,
2635
- (name, call) => this.options.deprecations.used(name, call),
2636
- "on fork",
2637
- );
2524
+ const forkSpec = structuredClone(forkSpecOf(given));
2525
+ if (forkSpec.pane !== undefined) forkSpec.pane = storedPane(forkSpec.pane);
2526
+ const deadline = boundedDeadline(timeout, scope?.deadline ?? this.options.deadline);
2638
2527
  assertDeadline(deadline);
2639
2528
  // A fork of the calling session is one whichever way it was asked for.
2640
2529
  const from = unopened || this.#caller?.key === parentKey ? CALLING : parentKey;
@@ -2662,7 +2551,7 @@ class WorkflowOwner {
2662
2551
  // child's home: anything added to the parent's would be read as the parent's.
2663
2552
  const copy =
2664
2553
  sandboxed || opening.accounted.home !== undefined
2665
- ? join(this.options.runDir, "forks", randomUUID())
2554
+ ? join(workDir(this.options.runDir), "forks", randomUUID())
2666
2555
  : undefined;
2667
2556
  const execution = withKnownEffort(forkExecution(parentSettings, forkSpec.runtime));
2668
2557
  // A fork of the calling session is an agent the run opened, on the model the session's files
@@ -2671,17 +2560,15 @@ class WorkflowOwner {
2671
2560
  delete execution.caller;
2672
2561
  const headless = placementOf(execution) === "headless";
2673
2562
  checkPaneOptions(spec.key, forkSpec, { headless, sandboxed });
2674
- const written = this.writtenPane(forkSpec);
2675
2563
  const identity: AgentIdentity = {
2676
2564
  execution,
2677
2565
  cwd: parent.identity.cwd,
2678
2566
  ...(forkSpec.instructions === undefined ? {} : { instructions: forkSpec.instructions }),
2679
- ...(forkSpec.labels === undefined ? {} : { labels: forkSpec.labels }),
2680
2567
  ...(parent.identity.skills ? { skills: parent.identity.skills } : {}),
2681
2568
  ...(parent.identity.sandbox === undefined ? {} : { sandbox: parent.identity.sandbox }),
2682
2569
  forkedFrom: { parent: from, spec: forkSpec },
2683
- ...written,
2684
- asked: paneAsked(written, this.options.panes, !headless && !sandboxed),
2570
+ ...(forkSpec.pane === undefined ? {} : { pane: forkSpec.pane }),
2571
+ asked: paneAsked(forkSpec.pane, this.options.panes, !headless && !sandboxed),
2685
2572
  };
2686
2573
 
2687
2574
  this.options.progress.agentOpened(spec.key, scope?.group);
@@ -2746,7 +2633,6 @@ class WorkflowOwner {
2746
2633
  cwd,
2747
2634
  execution,
2748
2635
  ...(forkSpec.instructions === undefined ? {} : { instructions: forkSpec.instructions }),
2749
- ...(forkSpec.labels === undefined ? {} : { labels: forkSpec.labels }),
2750
2636
  ...(seated ? { occupant: seated.occupant } : {}),
2751
2637
  ...(seated?.webSearch ? { webSearch: true } : {}),
2752
2638
  ...(given ? { skills: given } : {}),
@@ -2810,6 +2696,10 @@ class WorkflowOwner {
2810
2696
  ): void {
2811
2697
  state.then(
2812
2698
  () => {
2699
+ const entry = this.#agents.get(accounted.key);
2700
+ const skills = this.options.skills.of(accounted.key);
2701
+ const pane = entry && paneOf(entry);
2702
+ const seat = this.options.sandboxes.admitted(accounted.key);
2813
2703
  this.options.events.event({
2814
2704
  kind: "agent-opened",
2815
2705
  agent: accounted.key,
@@ -2817,17 +2707,26 @@ class WorkflowOwner {
2817
2707
  harness: accounted.execution.harness,
2818
2708
  cwd: accounted.cwd,
2819
2709
  ...(accounted.home === undefined ? {} : { home: accounted.home }),
2710
+ ...(seat ? { sandbox: seat.sandbox, domains: seat.domains } : {}),
2820
2711
  ...(as && "caller" in as ? { caller: true } : {}),
2712
+ ...(skills
2713
+ ? {
2714
+ skills: {
2715
+ skills: skills.skills,
2716
+ ...(skills.home === undefined ? {} : { home: skills.home }),
2717
+ },
2718
+ }
2719
+ : {}),
2720
+ ...(pane ? { pane } : {}),
2821
2721
  });
2822
2722
  // A fork's session is made before its first operation names it.
2823
2723
  if (!as || !("fork" in as)) return;
2824
- for (const id of accounted.sessions()) {
2825
- this.options.events.event({
2826
- kind: "session-known",
2827
- session: { harness: accounted.execution.harness, id },
2828
- agent: accounted.key,
2829
- });
2830
- }
2724
+ const { harness } = accounted.execution;
2725
+ this.options.sessionLines.known(
2726
+ accounted.key,
2727
+ accounted.sessions().map((id) => ({ harness, id })),
2728
+ this.placeOf(accounted.key),
2729
+ );
2831
2730
  },
2832
2731
  () => undefined,
2833
2732
  );
@@ -2876,22 +2775,57 @@ class WorkflowOwner {
2876
2775
  /** Where each TUI agent's pane went, as its host reported it, in the order opened. */
2877
2776
  panes(): AgentPaneRecord[] {
2878
2777
  return [...this.#agents].flatMap(([agent, entry]) => {
2879
- const placed = entry.opening?.sessions.harness?.pane?.();
2880
- if (!placed) return [];
2881
- const { pane, layout, keepPane } = entry.identity;
2882
- return [
2883
- {
2884
- callPath: [],
2885
- agent,
2886
- ...(pane === undefined ? {} : { pane }),
2887
- ...(layout === undefined ? {} : { layout }),
2888
- ...(keepPane === undefined ? {} : { keepPane }),
2889
- placed,
2890
- },
2891
- ];
2778
+ const pane = paneOf(entry);
2779
+ return pane ? [{ callPath: [], agent, ...pane }] : [];
2892
2780
  });
2893
2781
  }
2894
2782
 
2783
+ /**
2784
+ * Where an agent's sessions live, once it is known: its own harness home, the sandbox that home is
2785
+ * in, and the skills its harness was given, which `awf open` reopens (story 042).
2786
+ */
2787
+ private placeOf(key: string): SessionPlace {
2788
+ const home = this.#agents.get(key)?.opening?.accounted.home;
2789
+ const sandbox = this.options.sandboxes.sessionSandbox(key);
2790
+ const skills = this.#givenSkills.get(key);
2791
+ return {
2792
+ ...(home === undefined ? {} : { home }),
2793
+ ...(sandbox ? { sandbox } : {}),
2794
+ ...(skills ? { skills: { names: [...skills.names], directory: skills.directory } } : {}),
2795
+ };
2796
+ }
2797
+
2798
+ /** A session an agent's `wfa` named: recorded at once, less one a fork or parent holds. */
2799
+ private launcherReported(
2800
+ key: string,
2801
+ harness: HarnessKind,
2802
+ sessions: AgentSessions,
2803
+ id: string,
2804
+ ): void {
2805
+ sessions.launcher.add(id);
2806
+ this.options.sessionLines.known(
2807
+ key,
2808
+ reportedSessions(sessions).map((session) => ({ harness, id: session })),
2809
+ this.placeOf(key),
2810
+ );
2811
+ }
2812
+
2813
+ /**
2814
+ * Every agent's sessions as its close left them: one that moved in a compaction or fork at close,
2815
+ * or that its harness saw after its last operation, is what the next continue resumes.
2816
+ */
2817
+ recordSessions(): void {
2818
+ for (const [key, entry] of this.#agents) {
2819
+ if (!entry.opening) continue;
2820
+ const { harness } = entry.opening.accounted.execution;
2821
+ this.options.sessionLines.known(
2822
+ key,
2823
+ reportedSessions(entry.opening.sessions).map((id) => ({ harness, id })),
2824
+ this.placeOf(key),
2825
+ );
2826
+ }
2827
+ }
2828
+
2895
2829
  /** A reopened agent names the sandbox it runs in, or none: a different one is a conflict. */
2896
2830
  private async assertSameSandbox(
2897
2831
  key: string,
@@ -2910,6 +2844,14 @@ class WorkflowOwner {
2910
2844
  }
2911
2845
  }
2912
2846
 
2847
+ /** An agent's pane as its workflow wrote it and as its host placed it; undefined for no pane. */
2848
+ function paneOf(entry: AgentEntry): Omit<AgentPaneRecord, "callPath" | "agent"> | undefined {
2849
+ const placed = entry.opening?.sessions.harness?.pane?.();
2850
+ if (!placed) return undefined;
2851
+ const { pane } = entry.identity;
2852
+ return { ...(pane === undefined ? {} : { pane }), placed };
2853
+ }
2854
+
2913
2855
  // What a wait takes of it is type-only, and `asSource` marks it at run time.
2914
2856
  // biome-ignore lint/correctness/noUnusedVariables: merged into the class below.
2915
2857
  interface LogicalAgent extends Source<Taken<AgentEnded>> {}
@@ -2918,18 +2860,6 @@ interface LogicalAgent extends Source<Taken<AgentEnded>> {}
2918
2860
  class LogicalAgent implements AgentRef {
2919
2861
  readonly [PARTICIPANT] = true as const;
2920
2862
  readonly #operationStops = new Set<(stop: OperationStop) => Promise<void>>();
2921
- readonly #operations = new Map<
2922
- string,
2923
- {
2924
- spec: AgentRunTextSpec | AgentRunStructuredSpec<JsonValue>;
2925
- result: Promise<RunResult<JsonValue>>;
2926
- }
2927
- >();
2928
- readonly #compactions = new Map<
2929
- string,
2930
- { spec: CompactSpec; result: Promise<TurnOutcome<string>> }
2931
- >();
2932
- readonly #sets = new Map<string, { spec: SettingsSpec; result: Promise<TurnOutcome<null>> }>();
2933
2863
  /** Its settings now; see `AgentRef.execution`. */
2934
2864
  #execution: AgentExecution;
2935
2865
  /** What each `set` queued and not yet settled changes, in queue order. */
@@ -2970,6 +2900,8 @@ class LogicalAgent implements AgentRef {
2970
2900
  execution: AgentExecution;
2971
2901
  session: HarnessSession;
2972
2902
  slots: ResultSlotRegistry;
2903
+ /** Numbers its operations, `{key}.{n}`. */
2904
+ ops: OpIds;
2973
2905
  endpoint: string;
2974
2906
  /** The path this agent is told to run; see `installAgentLauncher`. */
2975
2907
  launcher: string;
@@ -2978,6 +2910,8 @@ class LogicalAgent implements AgentRef {
2978
2910
  progress: RunProgress;
2979
2911
  recordTurn: RecordTurn;
2980
2912
  events: RunEventLog;
2913
+ /** Records the sessions it is seen with, in `operation` when one is named. */
2914
+ sessionsKnown(sessions: readonly NativeSessionRef[], operation?: string): void;
2981
2915
  track<T>(promise: Promise<T>): Promise<T>;
2982
2916
  isRunClosing(): boolean;
2983
2917
  /** Throws when no turn may start: the attempt is looking on for values it needs. */
@@ -2998,10 +2932,7 @@ class LogicalAgent implements AgentRef {
2998
2932
  ): Promise<AgentRef>;
2999
2933
  /** The workflow ends it: the run tears it down and its key is not opened again. */
3000
2934
  close(reason: string | undefined): Promise<boolean>;
3001
- deprecated(name: Deprecated, subject?: string): void;
3002
- /** An outcome as the workflow is handed it, which a deprecated name it is given reports on. */
3003
- reporting<T>(outcome: T): T;
3004
- /** As the workflow wrote it; decides, at close, whether its pane stays. */
2935
+ /** As the operator gave it; decides, at close, whether its pane stays. */
3005
2936
  keepPane(): KeepPane | undefined;
3006
2937
  /** As the workflow's last open of it gave it. */
3007
2938
  keepAlive(): KeepAlive | undefined;
@@ -3019,7 +2950,7 @@ class LogicalAgent implements AgentRef {
3019
2950
  const restored = options.turns?.restored();
3020
2951
  if (restored)
3021
2952
  this.#started = {
3022
- ref: turnRef(options.key, restored.turn, async () => false, options.deprecated),
2953
+ ref: turnRef(restored.turn, async () => false),
3023
2954
  meta: restored.meta,
3024
2955
  ended: true,
3025
2956
  done: Promise.resolve(),
@@ -3138,30 +3069,24 @@ class LogicalAgent implements AgentRef {
3138
3069
  const refused = this.#refusedWhileStarted("start");
3139
3070
  if (refused) throw refused;
3140
3071
  const { meta = null, ...run } = spec;
3141
- if (run.id !== undefined) this.options.deprecated("call-id");
3142
- const id = run.id ?? randomUUID();
3072
+ const id = randomUUID();
3143
3073
  if (!isJsonValue(meta)) throw new Error("a turn's meta is a JSON value");
3144
3074
  const ended = Promise.withResolvers<void>();
3145
3075
  let written: TurnOutcome<JsonValue>["kind"] | undefined;
3146
3076
  const started = {
3147
- ref: turnRef(
3148
- this.key,
3149
- id,
3150
- async (reason?: string) => {
3151
- if (started.ended) return false;
3152
- // Its turn waits for the handler, which would wait for the turn's end.
3153
- const inTool = refusedInTool(this.key, "cancel()");
3154
- if (inTool) throw inTool;
3155
- const stop = { kind: "cancelled" as const, reason: reason ?? "the turn was cancelled" };
3156
- // Its own operation only: one queued behind a message settles cancelled when it is reached.
3157
- const own = this.#stopsByTurn.get(id);
3158
- if (own) await own(stop);
3159
- else this.#cancelledTurns.set(id, stop.reason);
3160
- await started.done;
3161
- return written === "cancelled";
3162
- },
3163
- this.options.deprecated,
3164
- ),
3077
+ ref: turnRef(id, async (reason?: string) => {
3078
+ if (started.ended) return false;
3079
+ // Its turn waits for the handler, which would wait for the turn's end.
3080
+ const inTool = refusedInTool(this.key, "cancel()");
3081
+ if (inTool) throw inTool;
3082
+ const stop = { kind: "cancelled" as const, reason: reason ?? "the turn was cancelled" };
3083
+ // Its own operation only: one queued behind a message settles cancelled when it is reached.
3084
+ const own = this.#stopsByTurn.get(id);
3085
+ if (own) await own(stop);
3086
+ else this.#cancelledTurns.set(id, stop.reason);
3087
+ await started.done;
3088
+ return written === "cancelled";
3089
+ }),
3165
3090
  meta,
3166
3091
  ended: false,
3167
3092
  done: ended.promise,
@@ -3180,13 +3105,13 @@ class LogicalAgent implements AgentRef {
3180
3105
  this.#launching = true;
3181
3106
  let launched: Promise<RunResult<JsonValue>>;
3182
3107
  try {
3183
- launched = outsideTools(() => this.run({ ...run, id } as AgentRunStructuredSpec<JsonValue>));
3108
+ launched = outsideTools(() => this.#runTurn(run, id));
3184
3109
  } catch (error) {
3185
3110
  launched = Promise.reject(error);
3186
3111
  } finally {
3187
3112
  this.#launching = false;
3188
3113
  }
3189
- // Tracked until the end is in the mailbox, so the run doesn't park with a turn's end on its way.
3114
+ // Tracked until the end is written, so the run doesn't park with a turn's end on its way.
3190
3115
  this.options.track(
3191
3116
  launched
3192
3117
  .then(
@@ -3231,7 +3156,7 @@ class LogicalAgent implements AgentRef {
3231
3156
  Meta extends JsonValue = JsonValue,
3232
3157
  T extends JsonValue = JsonValue,
3233
3158
  E extends EventLike = EventLike,
3234
- >(event: E): event is E & AgentEndOf<E, Meta, T> {
3159
+ >(event: E): event is E & AgentEndOf<Meta, T> {
3235
3160
  return matches(event, { name: "agent:ended", from: this.key });
3236
3161
  }
3237
3162
 
@@ -3240,54 +3165,24 @@ class LogicalAgent implements AgentRef {
3240
3165
  run<T extends JsonValue>(
3241
3166
  spec: AgentRunTextSpec | AgentRunStructuredSpec<T>,
3242
3167
  ): Promise<RunResult<string | T>> {
3243
- if (this.#closed) return Promise.reject(new Error("logical agent is closed"));
3244
- const refused = this.#refusedWhileStarted("run");
3245
- if (refused) return Promise.reject(refused);
3246
- const scope = scopes.getStore();
3247
- try {
3248
- scope?.assertAccepting();
3249
- this.options.checkOperation();
3250
- } catch (error) {
3251
- return Promise.reject(error);
3252
- }
3168
+ return this.#runTurn(spec, randomUUID()) as Promise<RunResult<string | T>>;
3169
+ }
3170
+
3171
+ /** A turn under `id`: `start()`'s, which its end is recorded by, or a new one. */
3172
+ #runTurn(
3173
+ spec: AgentRunTextSpec | AgentRunStructuredSpec<JsonValue>,
3174
+ id: TurnId,
3175
+ ): Promise<RunResult<JsonValue>> {
3176
+ const admitted = this.#admit<RunResult<JsonValue>, AbsoluteDeadline | undefined>(
3177
+ "run",
3178
+ { checkOperation: true },
3179
+ (scope) => this.operationDeadline(spec, scope),
3180
+ );
3181
+ if ("rejected" in admitted) return admitted.rejected;
3182
+ const { scope, prepared: deadline } = admitted;
3253
3183
  const opensWindow = !inTimeoutHandler();
3254
- if (spec.id !== undefined && !this.#launching) this.options.deprecated("call-id");
3255
- const id = spec.id ?? randomUUID();
3256
- const completeSpec = structuredClone({ ...spec, id }) as
3257
- | AgentRunTextSpec
3258
- | AgentRunStructuredSpec<JsonValue>;
3259
- const existing = this.#operations.get(id);
3260
- if (existing) {
3261
- if (!isDeepStrictEqual(existing.spec, completeSpec)) {
3262
- const rejected = Promise.reject<RunResult<string | T>>(
3263
- new Error(`turn id ${id} was reused with a different specification`),
3264
- );
3265
- scope?.track(rejected);
3266
- return rejected;
3267
- }
3268
- scope?.track(existing.result);
3269
- return existing.result as Promise<RunResult<string | T>>;
3270
- }
3271
- let deadline: AbsoluteDeadline | undefined;
3272
- try {
3273
- deadline = this.operationDeadline(spec, scope, this.#launching ? "on start" : "on run");
3274
- const recoveryDeadline = spec.nudge ? spec.nudge.deadline : undefined;
3275
- if (recoveryDeadline) {
3276
- this.options.deprecated("nudge-deadline");
3277
- const own = { unixMilliseconds: recoveryDeadline.unixMilliseconds };
3278
- assertDeadlineValue(own);
3279
- deadline = earlierDeadline(deadline, own);
3280
- }
3281
- } catch (error) {
3282
- const rejected = Promise.reject<RunResult<string | T>>(error);
3283
- scope?.track(rejected);
3284
- return rejected;
3285
- }
3286
- const result = this.queue(async () => {
3287
- if (this.#closed || this.options.isRunClosing()) {
3288
- throw new Error("logical agent is closed");
3289
- }
3290
- scope?.assertActive();
3184
+ const completeSpec: TurnSpec = structuredClone({ ...spec, id });
3185
+ const result = this.#queueActive(scope, async () => {
3291
3186
  const cancelled = this.#cancelledTurns.get(id);
3292
3187
  if (cancelled !== undefined) {
3293
3188
  this.#cancelledTurns.delete(id);
@@ -3301,7 +3196,19 @@ class LogicalAgent implements AgentRef {
3301
3196
  }
3302
3197
  const tags = tagsOf(scope, completeSpec.label);
3303
3198
  // Drawn here, so a turn that throws before it is delivered is still written as failed.
3304
- const operation = this.operationLog(randomUUID(), "turn", tags);
3199
+ let schema: JsonSchema | undefined;
3200
+ try {
3201
+ schema = resultSchema(completeSpec.schema);
3202
+ } catch {
3203
+ // Thrown again as the turn runs, under its id, so it is written as failed.
3204
+ }
3205
+ const { id: drawn, lost } = this.options.ops.draw(this.key, {
3206
+ kind: "turn",
3207
+ ...this.opWhere(tags),
3208
+ question: completeSpec.prompt,
3209
+ ...(schema === undefined ? {} : { schema }),
3210
+ });
3211
+ const operation = this.operationLog(drawn, "turn", tags, lost);
3305
3212
  this.options.progress.turnStarted(this.key, tags);
3306
3213
  try {
3307
3214
  const settled = await this.executeOperation(completeSpec, scope, deadline, operation);
@@ -3315,60 +3222,25 @@ class LogicalAgent implements AgentRef {
3315
3222
  await this.options.afterOperation?.();
3316
3223
  }
3317
3224
  });
3318
- const tracked = this.options.track(
3319
- result.then((settled) => {
3320
- this.options.reporting(settled.outcome);
3321
- return settled;
3322
- }),
3323
- );
3324
- scope?.track(tracked);
3325
- this.#operations.set(id, { spec: completeSpec, result: tracked });
3326
- return tracked as Promise<RunResult<string | T>>;
3225
+ return this.#tracked(scope, result);
3327
3226
  }
3328
3227
 
3329
3228
  compact(spec: CompactSpec): Promise<TurnOutcome<string>> {
3330
- if (this.#closed) return Promise.reject(new Error("logical agent is closed"));
3331
- const refused = this.#refusedWhileStarted("compact");
3332
- if (refused) return Promise.reject(refused);
3333
- const scope = scopes.getStore();
3334
- try {
3335
- scope?.assertAccepting();
3336
- this.options.checkOperation();
3337
- } catch (error) {
3338
- return Promise.reject(error);
3339
- }
3340
- if (spec.id !== undefined) this.options.deprecated("call-id");
3341
- const id = spec.id ?? randomUUID();
3342
- const completeSpec = structuredClone({ ...spec, id });
3343
- const existing = this.#compactions.get(id);
3344
- if (existing) {
3345
- if (!isDeepStrictEqual(existing.spec, completeSpec)) {
3346
- const rejected = Promise.reject<TurnOutcome<string>>(
3347
- new Error(`compaction id ${id} was reused with a different specification`),
3348
- );
3349
- scope?.track(rejected);
3350
- return rejected;
3351
- }
3352
- scope?.track(existing.result);
3353
- return existing.result;
3354
- }
3355
- let deadline: AbsoluteDeadline;
3356
- try {
3357
- deadline = harnessDeadline(this.operationDeadline(spec, scope, "on compact"));
3358
- } catch (error) {
3359
- const rejected = Promise.reject<TurnOutcome<string>>(error);
3360
- scope?.track(rejected);
3361
- return rejected;
3362
- }
3363
- const result = this.queue(async () => {
3364
- if (this.#closed || this.options.isRunClosing()) {
3365
- throw new Error("logical agent is closed");
3366
- }
3367
- scope?.assertActive();
3229
+ const admitted = this.#admit<TurnOutcome<string>, AbsoluteDeadline>(
3230
+ "compact",
3231
+ { checkOperation: true },
3232
+ (scope) => harnessDeadline(this.operationDeadline(spec, scope)),
3233
+ );
3234
+ if ("rejected" in admitted) return admitted.rejected;
3235
+ const { scope, prepared: deadline } = admitted;
3236
+ const id = randomUUID();
3237
+ const result = this.#queueActive(scope, async () => {
3368
3238
  const tags = tagsOf(scope);
3369
- const operation = this.operationLog(randomUUID(), "compact", tags);
3239
+ const drawn = this.options.ops.draw(this.key, { kind: "compact", ...this.opWhere(tags) });
3240
+ const operation = this.operationLog(drawn.id, "compact", tags);
3370
3241
  this.options.progress.turnStarted(this.key, tags, "compact");
3371
3242
  try {
3243
+ if (drawn.lost !== undefined) throw new Error(drawn.lost);
3372
3244
  const outcome = await this.executeCompaction(id, spec.prompt, deadline, scope, operation);
3373
3245
  this.turnEnded(outcome, "compact");
3374
3246
  return outcome;
@@ -3380,48 +3252,23 @@ class LogicalAgent implements AgentRef {
3380
3252
  await this.options.afterOperation?.();
3381
3253
  }
3382
3254
  });
3383
- const tracked = this.options.track(result.then(this.options.reporting));
3384
- scope?.track(tracked);
3385
- this.#compactions.set(id, { spec: completeSpec, result: tracked });
3386
- return tracked;
3255
+ return this.#tracked(scope, result);
3387
3256
  }
3388
3257
 
3389
3258
  /** Not a turn: progress counts none for it, and it asks the model nothing. */
3390
3259
  set(spec: SettingsSpec): Promise<TurnOutcome<null>> {
3391
- if (this.#closed) return Promise.reject(new Error("logical agent is closed"));
3392
- const refused = this.#refusedWhileStarted("set");
3393
- if (refused) return Promise.reject(refused);
3394
- const scope = scopes.getStore();
3395
- try {
3396
- scope?.assertAccepting();
3397
- } catch (error) {
3398
- return Promise.reject(error);
3399
- }
3400
- if (spec.id !== undefined) this.options.deprecated("call-id");
3401
- const id = spec.id ?? randomUUID();
3402
- const completeSpec = structuredClone({ ...spec, id });
3403
- const existing = this.#sets.get(id);
3404
- if (existing) {
3405
- if (!isDeepStrictEqual(existing.spec, completeSpec)) {
3406
- const rejected = Promise.reject<TurnOutcome<null>>(
3407
- new Error(`settings id ${id} was reused with a different specification`),
3408
- );
3409
- scope?.track(rejected);
3410
- return rejected;
3411
- }
3412
- scope?.track(existing.result);
3413
- return existing.result;
3414
- }
3415
- let change: SettingsChange;
3416
- let deadline: AbsoluteDeadline;
3417
- try {
3418
- change = this.settingsChange(spec);
3419
- deadline = harnessDeadline(this.operationDeadline(spec, scope, "on set"));
3420
- } catch (error) {
3421
- const rejected = Promise.reject<TurnOutcome<null>>(error);
3422
- scope?.track(rejected);
3423
- return rejected;
3424
- }
3260
+ const admitted = this.#admit<
3261
+ TurnOutcome<null>,
3262
+ { change: SettingsChange; deadline: AbsoluteDeadline }
3263
+ >("set", { checkOperation: false }, (scope) => ({
3264
+ change: this.settingsChange(spec),
3265
+ deadline: harnessDeadline(this.operationDeadline(spec, scope)),
3266
+ }));
3267
+ if ("rejected" in admitted) return admitted.rejected;
3268
+ const {
3269
+ scope,
3270
+ prepared: { change, deadline },
3271
+ } = admitted;
3425
3272
  this.#pendingSets.push(change);
3426
3273
  const result = this.queue(async () => {
3427
3274
  try {
@@ -3436,12 +3283,68 @@ class LogicalAgent implements AgentRef {
3436
3283
  this.#pendingSets.splice(this.#pendingSets.indexOf(change), 1);
3437
3284
  }
3438
3285
  });
3439
- const tracked = this.options.track(result.then(this.options.reporting));
3286
+ return this.#tracked(scope, result);
3287
+ }
3288
+
3289
+ /**
3290
+ * The checks before an operation is queued, in order: the agent open, no started turn in its
3291
+ * way, the scope accepting and, with `checkOperation`, the stage; then `prepare`, whose refusal
3292
+ * the scope tracks. A refusal is a rejection, never a throw.
3293
+ */
3294
+ #admit<T, P>(
3295
+ what: string,
3296
+ options: { checkOperation: boolean },
3297
+ prepare: (scope: ExecutionScope | undefined) => P,
3298
+ ): { scope: ExecutionScope | undefined; prepared: P } | { rejected: Promise<T> } {
3299
+ if (this.#closed) return { rejected: Promise.reject(new Error("logical agent is closed")) };
3300
+ const refused = this.#refusedWhileStarted(what);
3301
+ if (refused) return { rejected: Promise.reject(refused) };
3302
+ const scope = scopes.getStore();
3303
+ try {
3304
+ scope?.assertAccepting();
3305
+ if (options.checkOperation) this.options.checkOperation();
3306
+ } catch (error) {
3307
+ return { rejected: Promise.reject(error) };
3308
+ }
3309
+ try {
3310
+ return { scope, prepared: prepare(scope) };
3311
+ } catch (error) {
3312
+ const rejected = Promise.reject<T>(error);
3313
+ scope?.track(rejected);
3314
+ return { rejected };
3315
+ }
3316
+ }
3317
+
3318
+ /** `work` once the queue reaches it, the agent and run still open and `scope` still active. */
3319
+ #queueActive<T>(scope: ExecutionScope | undefined, work: () => Promise<T>): Promise<T> {
3320
+ return this.queue(async () => {
3321
+ if (this.#closed || this.options.isRunClosing()) {
3322
+ throw new Error("logical agent is closed");
3323
+ }
3324
+ scope?.assertActive();
3325
+ return work();
3326
+ });
3327
+ }
3328
+
3329
+ #tracked<T>(scope: ExecutionScope | undefined, result: Promise<T>): Promise<T> {
3330
+ const tracked = this.options.track(result);
3440
3331
  scope?.track(tracked);
3441
- this.#sets.set(id, { spec: completeSpec, result: tracked });
3442
3332
  return tracked;
3443
3333
  }
3444
3334
 
3335
+ /**
3336
+ * Whose an operation is, the settings it runs at and where, as its `op.json` says: what its spend
3337
+ * is read with, by the next attempt when this one never settles it.
3338
+ */
3339
+ private opWhere(tags: { stage?: string }): AgentOp & { stage?: string } {
3340
+ return {
3341
+ agent: this.key,
3342
+ execution: this.#execution,
3343
+ cwd: this.options.ledger.cwd(),
3344
+ ...(tags.stage === undefined ? {} : { stage: tags.stage }),
3345
+ };
3346
+ }
3347
+
3445
3348
  /**
3446
3349
  * An operation's start and failure in the run's event log, under the id its record keeps. Its
3447
3350
  * settling is the turn line `recordTurn` writes.
@@ -3450,10 +3353,12 @@ class LogicalAgent implements AgentRef {
3450
3353
  operationId: string,
3451
3354
  of: TurnRecord["kind"],
3452
3355
  tags: OperationTags,
3356
+ unrecorded?: string,
3453
3357
  ): OperationLog {
3454
3358
  let deliveredAt: number | undefined;
3455
3359
  /** Named by its host before delivery, for its start to carry. */
3456
3360
  const named: string[] = [];
3361
+ // Its sessions as of now, recorded before the line that names it, as a turn's are.
3457
3362
  const fields = () => {
3458
3363
  const { callPath, sessions, ...record } = this.options.ledger.unsettled(
3459
3364
  operationId,
@@ -3463,16 +3368,16 @@ class LogicalAgent implements AgentRef {
3463
3368
  );
3464
3369
  const harness = this.#execution.harness;
3465
3370
  const fresh = named.filter((id) => !sessions.some((known) => known.id === id));
3466
- return {
3467
- of,
3468
- callPath,
3469
- ...record,
3470
- sessions: [...sessions, ...fresh.map((id) => ({ harness, id }))],
3471
- };
3371
+ this.options.sessionsKnown(
3372
+ [...sessions, ...fresh.map((id) => ({ harness, id }))],
3373
+ operationId,
3374
+ );
3375
+ return { of, callPath, ...record };
3472
3376
  };
3473
3377
  return {
3474
3378
  operationId,
3475
3379
  tags,
3380
+ ...(unrecorded === undefined ? {} : { unrecorded }),
3476
3381
  delivered: (at: number) => {
3477
3382
  deliveredAt = at;
3478
3383
  this.#hasRun = true;
@@ -3484,11 +3389,7 @@ class LogicalAgent implements AgentRef {
3484
3389
  named.push(id);
3485
3390
  return;
3486
3391
  }
3487
- this.options.events.event({
3488
- kind: "session-known",
3489
- session: { harness: this.#execution.harness, id },
3490
- operationId,
3491
- });
3392
+ this.options.sessionsKnown([{ harness: this.#execution.harness, id }], operationId);
3492
3393
  },
3493
3394
  failed: (error: unknown) =>
3494
3395
  this.options.events.event({
@@ -3499,7 +3400,7 @@ class LogicalAgent implements AgentRef {
3499
3400
  };
3500
3401
  }
3501
3402
 
3502
- /** A turn or compaction that settled, as the view shows it and `turns.jsonl` keeps it. */
3403
+ /** A turn or compaction that settled, as the view shows it and `log.jsonl` keeps it. */
3503
3404
  private turnEnded(
3504
3405
  outcome: TurnOutcome<JsonValue>,
3505
3406
  kind: TurnRecord["kind"],
@@ -3513,6 +3414,7 @@ class LogicalAgent implements AgentRef {
3513
3414
  this.#last = outcome.kind;
3514
3415
  const reason = "reason" in outcome ? outcome.reason : undefined;
3515
3416
  this.options.progress.turnSettled(this.key, outcome.kind, reason);
3417
+ this.options.sessionsKnown(outcome.usage.sessions, outcome.usage.operationId);
3516
3418
  this.options.recordTurn(outcome, kind);
3517
3419
  }
3518
3420
 
@@ -3540,11 +3442,7 @@ class LogicalAgent implements AgentRef {
3540
3442
  // The harness bounds its fork by this deadline and stops it there, so the session is idle again
3541
3443
  // when the fork rejects.
3542
3444
  const take: TakeFork = (deadline, into) =>
3543
- this.queue(async () => {
3544
- if (this.#closed || this.options.isRunClosing()) {
3545
- throw new Error("logical agent is closed");
3546
- }
3547
- scope?.assertActive();
3445
+ this.#queueActive(scope, async () => {
3548
3446
  // The fork was opened at the settings it would copy; a `set` before it that did not take
3549
3447
  // would leave it recording settings it never ran at.
3550
3448
  if (!isDeepStrictEqual(sessionSettings(this.#execution), sessionSettings(settings))) {
@@ -3564,17 +3462,10 @@ class LogicalAgent implements AgentRef {
3564
3462
 
3565
3463
  /** Its `timeout`, never later than the scope's or the run's; else the earlier of those. */
3566
3464
  private operationDeadline(
3567
- spec: GivenBound,
3465
+ spec: { timeout?: Timeout },
3568
3466
  scope: ExecutionScope | undefined,
3569
- call: string,
3570
3467
  ): AbsoluteDeadline | undefined {
3571
- const { timeout, deadline, timeoutMs } = spec;
3572
- return boundedDeadline(
3573
- { timeout, deadline, timeoutMs },
3574
- this.deadlineCeiling(scope),
3575
- this.options.deprecated,
3576
- call,
3577
- );
3468
+ return boundedDeadline(spec.timeout, this.deadlineCeiling(scope));
3578
3469
  }
3579
3470
 
3580
3471
  /** The deadlines around an operation: its scope's and the run's, if either has one. */
@@ -3595,9 +3486,19 @@ class LogicalAgent implements AgentRef {
3595
3486
  `agent ${this.key} is the calling session, whose model and effort are the operator's`,
3596
3487
  );
3597
3488
  }
3489
+ const removed = Object.keys(spec).filter(
3490
+ (field) =>
3491
+ REMOVED_SETTINGS_FIELDS.has(field) && (spec as Record<string, unknown>)[field] != null,
3492
+ );
3493
+ if (removed.length > 0) {
3494
+ const bound = removed.some((field) => field !== "id") ? ": bound it with timeout" : "";
3495
+ throw new Error(`set takes no ${removed.join(", ")}${bound}`);
3496
+ }
3598
3497
  // A workflow is untyped JavaScript at run time: a harness or placement asked for must not be
3599
3498
  // dropped in silence.
3600
- const others = Object.keys(spec).filter((field) => !SETTINGS_FIELDS.has(field));
3499
+ const others = Object.keys(spec).filter(
3500
+ (field) => !SETTINGS_FIELDS.has(field) && !REMOVED_SETTINGS_FIELDS.has(field),
3501
+ );
3601
3502
  if (others.length > 0) {
3602
3503
  throw new Error(
3603
3504
  `set changes an agent's model and effort, not its ${others.join(", ")}: another harness or placement is another agent`,
@@ -3732,13 +3633,17 @@ class LogicalAgent implements AgentRef {
3732
3633
  // Once what it was running has ended, so its last operation's line comes first.
3733
3634
  if (first) {
3734
3635
  const { key, execution } = this.options;
3735
- const stoppedLine = () =>
3636
+ const stoppedLine = () => {
3637
+ const placed = this.options.session.pane?.();
3736
3638
  this.options.events.event({
3737
3639
  kind: "agent-stopped",
3738
3640
  agent: key,
3739
3641
  callPath: [],
3740
3642
  harness: execution.harness,
3643
+ ...(placed?.kept ? { kept: true } : {}),
3644
+ ...(placed?.notKept === undefined ? {} : { notKept: placed.notKept }),
3741
3645
  });
3646
+ };
3742
3647
  closed.then(stoppedLine, stoppedLine);
3743
3648
  }
3744
3649
  return closed;
@@ -3776,7 +3681,7 @@ class LogicalAgent implements AgentRef {
3776
3681
  }
3777
3682
 
3778
3683
  private async executeOperation(
3779
- spec: AgentRunTextSpec | AgentRunStructuredSpec<JsonValue>,
3684
+ spec: TurnSpec,
3780
3685
  scope: ExecutionScope | undefined,
3781
3686
  operationDeadline: AbsoluteDeadline | undefined,
3782
3687
  operation: OperationLog,
@@ -3787,10 +3692,10 @@ class LogicalAgent implements AgentRef {
3787
3692
  }
3788
3693
 
3789
3694
  private async executeWaitingOperation(
3790
- spec: AgentRunTextSpec | AgentRunStructuredSpec<JsonValue>,
3695
+ spec: TurnSpec,
3791
3696
  scope: ExecutionScope | undefined,
3792
3697
  initial: AbsoluteDeadline | undefined,
3793
- { operationId, tags, delivered, session }: OperationLog,
3698
+ { operationId, tags, delivered, session, unrecorded }: OperationLog,
3794
3699
  nudge: Exclude<AgentRunTextSpec["nudge"], false>,
3795
3700
  ): Promise<RunResult<JsonValue>> {
3796
3701
  const schema = resultSchema(spec.schema);
@@ -3852,7 +3757,7 @@ class LogicalAgent implements AgentRef {
3852
3757
  ].join("\n")
3853
3758
  : "");
3854
3759
  const events = createOperationEvents(this.options.runDir, operationId);
3855
- const messages = this.operationMessages(operationId, `${spec.id!}:message`, {
3760
+ const messages = this.operationMessages(operationId, `${spec.id}:message`, {
3856
3761
  deadline: () => harnessDeadline(deadline),
3857
3762
  authored,
3858
3763
  session,
@@ -3863,6 +3768,7 @@ class LogicalAgent implements AgentRef {
3863
3768
  event: events.record,
3864
3769
  operationId,
3865
3770
  agentId: this.key,
3771
+ ...(unrecorded === undefined ? {} : { unrecorded }),
3866
3772
  question: spec.prompt,
3867
3773
  schema,
3868
3774
  ...(deadline ? { deadline } : {}),
@@ -3884,7 +3790,7 @@ class LogicalAgent implements AgentRef {
3884
3790
  this.options.session
3885
3791
  .start(
3886
3792
  {
3887
- id: spec.id!,
3793
+ id: spec.id,
3888
3794
  prompt: [
3889
3795
  operationPrompt(prompt, schema, this.options.launcher, operationId),
3890
3796
  toolsLesson(
@@ -3904,7 +3810,7 @@ class LogicalAgent implements AgentRef {
3904
3810
  .then((turn) => this.confirmed(operationId, joined?.numbers ?? [], turn)),
3905
3811
  successor: (turn, sequence, deliverySignal) =>
3906
3812
  turn.nudge({
3907
- id: `${spec.id!}:check-in:${sequence}`,
3813
+ id: `${spec.id}:check-in:${sequence}`,
3908
3814
  deadline: harnessDeadline(deadline),
3909
3815
  deliverySignal,
3910
3816
  prompt: checkInPrompt(recoveryPrompt),
@@ -3918,24 +3824,25 @@ class LogicalAgent implements AgentRef {
3918
3824
  return settle(request);
3919
3825
  };
3920
3826
  this.#operationStops.add(stop);
3921
- this.#stopsByTurn.set(spec.id!, stop);
3827
+ this.#stopsByTurn.set(spec.id, stop);
3922
3828
  const remove = scope?.add(stop);
3923
3829
  return () => {
3924
3830
  this.#operationStops.delete(stop);
3925
- if (this.#stopsByTurn.get(spec.id!) === stop) this.#stopsByTurn.delete(spec.id!);
3831
+ if (this.#stopsByTurn.get(spec.id) === stop) this.#stopsByTurn.delete(spec.id);
3926
3832
  remove?.();
3927
3833
  };
3928
3834
  },
3929
3835
  phase: (phase, reason, until) =>
3930
3836
  this.options.progress.turnPhase(this.key, phase, reason, until),
3931
3837
  delivered,
3838
+ keepScreen: (turn) => keepScreen(this.options.runDir, operationId, turn),
3932
3839
  })
3933
3840
  .catch(
3934
3841
  (error: unknown): SupervisedOutcome => ({
3935
3842
  kind: "failed",
3936
3843
  reason: messageOf(error),
3937
3844
  cleanupUnresolved: true,
3938
- charges: [],
3845
+ reportedCosts: [],
3939
3846
  settledAt: Date.now(),
3940
3847
  }),
3941
3848
  )
@@ -3955,7 +3862,7 @@ class LogicalAgent implements AgentRef {
3955
3862
  ...(outcome.deliveredAt === undefined ? {} : { deliveredAt: outcome.deliveredAt }),
3956
3863
  settledAt: outcome.settledAt,
3957
3864
  },
3958
- outcome.charges,
3865
+ outcome.reportedCosts,
3959
3866
  );
3960
3867
  if (outcome.kind === "answered")
3961
3868
  return {
@@ -4101,7 +4008,10 @@ class LogicalAgent implements AgentRef {
4101
4008
  stage: ExecutionScope | undefined,
4102
4009
  ): Promise<void> {
4103
4010
  const { messaging } = this.options;
4104
- const operationId = randomUUID();
4011
+ const { id: operationId, lost } = this.options.ops.draw(this.key, {
4012
+ kind: "message",
4013
+ ...this.opWhere(stage?.stage === undefined ? {} : { stage: stage.stage }),
4014
+ });
4105
4015
  const ceiling = this.deadlineCeiling(stage)?.unixMilliseconds;
4106
4016
  let deadline = Math.min(Date.now() + messaging.limits.operationMs, ceiling ?? Infinity);
4107
4017
  messaging.opened({
@@ -4124,7 +4034,7 @@ class LogicalAgent implements AgentRef {
4124
4034
  let end: SupervisedOutcome["kind"] | undefined;
4125
4035
  try {
4126
4036
  const opening = messaging.take(operationId, "opens");
4127
- // Withdrawn before it could be shown: nothing to wake it for.
4037
+ // Withdrawn before it could be shown: nothing to wake it for. Its folder stays, its number used.
4128
4038
  if (!opening) return;
4129
4039
  if (opening.deadline !== undefined) deadline = opening.deadline;
4130
4040
  const cause = { message: { from } };
@@ -4135,6 +4045,7 @@ class LogicalAgent implements AgentRef {
4135
4045
  const operation = this.operationLog(operationId, "turn", tags);
4136
4046
  this.options.progress.turnStarted(this.key, tags);
4137
4047
  try {
4048
+ if (lost !== undefined) throw new Error(lost);
4138
4049
  const outcome = await this.superviseMessage(operationId, deadline, opening, {
4139
4050
  cause,
4140
4051
  tags,
@@ -4249,6 +4160,7 @@ class LogicalAgent implements AgentRef {
4249
4160
  phase: (phase, reason, until) =>
4250
4161
  this.options.progress.turnPhase(this.key, phase, reason, until),
4251
4162
  delivered: operation.delivered,
4163
+ keepScreen: (turn) => keepScreen(this.options.runDir, operationId, turn),
4252
4164
  ...(this.options.livenessPolicy ? { policy: this.options.livenessPolicy } : {}),
4253
4165
  });
4254
4166
  } finally {
@@ -4265,7 +4177,7 @@ class LogicalAgent implements AgentRef {
4265
4177
  ...(outcome.deliveredAt === undefined ? {} : { deliveredAt: outcome.deliveredAt }),
4266
4178
  settledAt: outcome.settledAt,
4267
4179
  },
4268
- outcome.charges,
4180
+ outcome.reportedCosts,
4269
4181
  );
4270
4182
  // Recorded `answered` with no value: it did what it was woken for, and takes no result.
4271
4183
  this.turnEnded(
@@ -4296,7 +4208,7 @@ class LogicalAgent implements AgentRef {
4296
4208
  const times: { deliveredAt?: number } = {};
4297
4209
  const settle = (
4298
4210
  native: HarnessTurnOutcome | "expired",
4299
- /** A compaction left to end on its own, whose charges come when it does. */
4211
+ /** A compaction left to end on its own, whose reported costs come when it does. */
4300
4212
  later?: Promise<HarnessTurnOutcome>,
4301
4213
  ): TurnOutcome<string> => {
4302
4214
  const usage = entry.settle(
@@ -4304,7 +4216,7 @@ class LogicalAgent implements AgentRef {
4304
4216
  ...times,
4305
4217
  settledAt: Math.min(Date.now(), deadline.unixMilliseconds),
4306
4218
  },
4307
- native === "expired" ? [] : native.chargesUsd,
4219
+ native === "expired" ? [] : native.reportedCostsUsd,
4308
4220
  later,
4309
4221
  );
4310
4222
  if (native === "expired") {
@@ -4352,7 +4264,7 @@ class LogicalAgent implements AgentRef {
4352
4264
  state: "failed",
4353
4265
  detail: messageOf(error),
4354
4266
  resultEvidence: { kind: "unavailable" },
4355
- chargesUsd: [],
4267
+ reportedCostsUsd: [],
4356
4268
  });
4357
4269
  }
4358
4270
  times.deliveredAt = Date.now();
@@ -4366,8 +4278,14 @@ class LogicalAgent implements AgentRef {
4366
4278
  cancelTimer = scheduleAt(deadline, () => resolve("expired"));
4367
4279
  }),
4368
4280
  ]).finally(() => cancelTimer?.());
4369
- if (native === "expired") return settle(native, (await this.leaveFinishing(turn)).later);
4370
- return settle(native);
4281
+ if (native === "expired") {
4282
+ await keepScreen(this.options.runDir, operationId, turn);
4283
+ return settle(native, (await this.leaveFinishing(turn)).later);
4284
+ }
4285
+ const outcome = settle(native);
4286
+ if (!outcome.answered && outcome.kind !== "cancelled")
4287
+ await keepScreen(this.options.runDir, operationId, turn);
4288
+ return outcome;
4371
4289
  } finally {
4372
4290
  removeCanceller?.();
4373
4291
  }
@@ -4464,6 +4382,10 @@ function sourceTake(what: string, branch: string, source: unknown): WaitTake {
4464
4382
  throw new Error(
4465
4383
  `${what}'s ${branch} is wf.at(), a select's: a race answers once, so it takes a timer, wf.timer(name, date)`,
4466
4384
  );
4385
+ if (typeof source === "object" && source !== null && startedTurns.has(source))
4386
+ throw new Error(
4387
+ `${what}'s ${branch} is a started turn: name its agent, { ${branch}: agent }, which has at most one`,
4388
+ );
4467
4389
  throw new Error(`${what}'s ${branch} is not a source: a timer, a signal, or an agent`);
4468
4390
  }
4469
4391
 
@@ -4487,32 +4409,27 @@ function signalSource(name: string, declared: ReadonlySet<string>): unknown {
4487
4409
  throw new Error(
4488
4410
  `signal ${name}: an agent's end is waited on by naming the agent, { done: ${awf.key} }, not a signal`,
4489
4411
  );
4490
- // The mailbox keeps a reply under its record name, from the participant's key.
4412
+ // `in.jsonl` keeps a reply under its record name, from the participant's key.
4491
4413
  const take = awf ? { name: "participant:reply", from: awf.key } : { name };
4492
4414
  return signalOf({ name }, take);
4493
4415
  }
4494
4416
 
4495
4417
  /** Timer `name`'s source. */
4496
- function timerSource(mailbox: RunMailbox, name: string, due: string): Timer {
4418
+ function timerSource(waits: RunWaits, name: string, due: string): Timer {
4497
4419
  return source(
4498
- { name, due, cancel: () => mailbox.cancelTimer(name) },
4420
+ { name, due, cancel: () => waits.cancelTimer(name) },
4499
4421
  { name: "timer:due", from: name },
4500
4422
  ) as unknown as Timer;
4501
4423
  }
4502
4424
 
4503
- /** A started turn, as the code holds it: its id, its cancel, and a source of its end. */
4504
- function turnRef(
4505
- agent: string,
4506
- id: string,
4507
- cancel: (reason?: string) => Promise<boolean>,
4508
- deprecated: (name: Deprecated) => void,
4509
- ): StartedTurn {
4510
- const take = { name: "agent:ended", from: agent, turn: id };
4511
- const is = (event: { name: string; from: string; data?: unknown }) => {
4512
- deprecated("turn-is");
4513
- return matches(event, take);
4514
- };
4515
- return asSource(Object.freeze({ id, cancel, is }), take) as unknown as StartedTurn;
4425
+ /** The turns `start()` handed out, which a wait refuses by name. */
4426
+ const startedTurns = new WeakSet<object>();
4427
+
4428
+ /** A started turn, as the code holds it: its id and its cancel. A wait names its agent. */
4429
+ function turnRef(id: string, cancel: (reason?: string) => Promise<boolean>): StartedTurn {
4430
+ const turn = Object.freeze({ id, cancel });
4431
+ startedTurns.add(turn);
4432
+ return turn;
4516
4433
  }
4517
4434
 
4518
4435
  class ExecutionScope {
@@ -4689,6 +4606,17 @@ async function executeParallel<Item, Result>(
4689
4606
  }
4690
4607
  }
4691
4608
 
4609
+ /** How long reading a pane for an operation's evidence may hold up its release. */
4610
+ const SCREEN_READ_MS = 5_000;
4611
+
4612
+ /** Keeps what `turn`'s pane shows, the evidence of an operation that ended without an answer. */
4613
+ async function keepScreen(runDir: string, operationId: string, turn: HarnessTurn): Promise<void> {
4614
+ if (!turn.screen) return;
4615
+ const deadline = { unixMilliseconds: Date.now() + SCREEN_READ_MS };
4616
+ const screen = await waitForDeadline(turn.screen(), deadline).catch(() => undefined);
4617
+ if (screen?.trim()) await writeScreen(runDir, operationId, screen).catch(() => undefined);
4618
+ }
4619
+
4692
4620
  /**
4693
4621
  * Compaction keeps its existing bounded release behavior. Structured operations use the
4694
4622
  * supervisor, which requires confirmed native completion before returning an answer.
@@ -4775,17 +4703,21 @@ type Warn = (message: string, fields?: JsonObject, stage?: string) => void;
4775
4703
  type OperationLog = {
4776
4704
  operationId: string;
4777
4705
  tags: OperationTags;
4706
+ /** Why its `op.json` wasn't written as it was drawn: a turn fails as its result slot would. */
4707
+ unrecorded?: string;
4778
4708
  delivered(at: number): void;
4779
4709
  /** A session its host named while it ran: on its start if that is still to come. */
4780
4710
  session(id: string): void;
4781
4711
  failed(error: unknown): void;
4782
4712
  };
4783
4713
 
4714
+ /** A turn as the agent runs it: its spec, under the id it is recorded by. */
4715
+ type TurnSpec = (AgentRunTextSpec | AgentRunStructuredSpec<JsonValue>) & { id: TurnId };
4784
4716
  /** A native fork, and the parent's operation it was copied after: null when it ran none. */
4785
4717
  type TakenFork = { native: NativeFork; after: string | null };
4786
4718
  type TakeFork = (deadline: AbsoluteDeadline | undefined, into?: SessionCopy) => Promise<TakenFork>;
4787
4719
 
4788
- /** A settled turn or compaction, kept in the run's `turns.jsonl`. */
4720
+ /** A settled turn or compaction, kept in the run's `log.jsonl`. */
4789
4721
  type RecordTurn = (outcome: TurnOutcome<JsonValue>, kind: TurnRecord["kind"]) => void;
4790
4722
 
4791
4723
  function pick<T extends object, K extends keyof T>(value: T, keys: readonly K[]): Pick<T, K> {
@@ -4948,7 +4880,10 @@ function withKnownEffort(execution: AgentExecution): AgentExecution {
4948
4880
  /** What `set` changes. */
4949
4881
  type SettingsChange = Partial<Pick<AgentExecution, "model" | "effort">>;
4950
4882
 
4951
- const SETTINGS_FIELDS = new Set(["id", "model", "effort", "timeout", "deadline", "timeoutMs"]);
4883
+ const SETTINGS_FIELDS = new Set(["model", "effort", "timeout"]);
4884
+
4885
+ /** Removed in 0.1.0-beta.6; refused, as the switch would run otherwise than written. */
4886
+ const REMOVED_SETTINGS_FIELDS = new Set(["id", "deadline", "timeoutMs"]);
4952
4887
 
4953
4888
  function sessionSettings({ model, effort }: AgentExecution): SessionSettings {
4954
4889
  return { model, ...(effort === undefined ? {} : { effort }) };
@@ -4996,22 +4931,17 @@ function forkExecution(parent: AgentExecution, runtime: ForkRuntime = {}): Agent
4996
4931
 
4997
4932
  const FORK_RUNTIME_FIELDS = ["placement", "metered", "effort"] as const;
4998
4933
 
4999
- /**
5000
- * A fork's spec with its runtime written one way, `runtime`, so a repeat in either form compares
5001
- * equal. The flat fields are deprecated, and refused beside `runtime` rather than merged with it.
5002
- */
5003
- function forkSpecOf(spec: CallerForkSpec, deprecated: () => void): CallerForkSpec {
5004
- const { placement, metered, effort, runtime, ...rest } = spec;
5005
- const flat = definedFields({ placement, metered, effort });
5006
- if (Object.keys(flat).length > 0) {
5007
- if (runtime !== undefined) {
5008
- throw new Error(
5009
- `agent ${spec.key}: a fork's placement, metered and effort go in its runtime, not beside it`,
5010
- );
5011
- }
5012
- deprecated();
5013
- return { ...rest, runtime: flat };
5014
- }
4934
+ /** A fork's spec, its `runtime` checked: what a typed caller can't get wrong, a plain one can. */
4935
+ function forkSpecOf(spec: CallerForkSpec): CallerForkSpec {
4936
+ const { runtime, ...rest } = spec;
4937
+ // Removed in 0.1.0-beta.6; refused, as the fork would run otherwise than written.
4938
+ const flat = FORK_RUNTIME_FIELDS.filter(
4939
+ (field) => (rest as Record<string, unknown>)[field] !== undefined,
4940
+ );
4941
+ if (flat.length > 0)
4942
+ throw new Error(
4943
+ `agent ${spec.key}: a fork's ${flat.join(", ")} go in its runtime: fork({ key, runtime: { ${flat.join(", ")} } })`,
4944
+ );
5015
4945
  if (runtime === undefined) return rest;
5016
4946
  if (typeof runtime !== "object" || runtime === null || Array.isArray(runtime)) {
5017
4947
  throw new Error(
@@ -5103,7 +5033,7 @@ function assertCompatibleAgent(
5103
5033
  if (spec.skills !== undefined && !isDeepStrictEqual(existing.skills, requested.skills)) {
5104
5034
  conflict(key, "skills");
5105
5035
  }
5106
- for (const field of ["instructions", "labels", "pane", "layout", "keepPane"] as const) {
5036
+ for (const field of ["instructions", "pane"] as const) {
5107
5037
  if (spec[field] !== undefined && !isDeepStrictEqual(existing[field], requested[field])) {
5108
5038
  conflict(key, field);
5109
5039
  }
@@ -5118,18 +5048,14 @@ function stoppedError(key: string, reason: string): Error {
5118
5048
  return new Error(`agent ${key} was stopped (${reason}); its key is not opened again`);
5119
5049
  }
5120
5050
 
5121
- /**
5122
- * Each root agent's last native session in a run's turns, where its harness can resume it: what a
5123
- * event run's continue reopens it on.
5124
- */
5125
- export function keptSessionsOf(turns: readonly TurnRecord[]): Map<string, NativeFork> {
5126
- const kept = new Map<string, NativeFork>();
5127
- for (const turn of turns) {
5128
- if ((turn.callPath?.length ?? 0) > 0) continue;
5129
- const session = turn.sessions.at(-1);
5130
- if (session) kept.set(turn.agent, { harness: session.harness, sessionRef: session.id });
5131
- }
5132
- return kept;
5051
+ /** Each agent's latest `session` line: what an event run's continue reopens it on. */
5052
+ export function keptSessionsOf(sessions: RunView["sessions"]): Map<string, NativeFork> {
5053
+ return new Map(
5054
+ Object.values(sessions.latest).map((line) => [
5055
+ line.agent,
5056
+ { harness: line.harness, sessionRef: line.session },
5057
+ ]),
5058
+ );
5133
5059
  }
5134
5060
 
5135
5061
  /**
@@ -5140,25 +5066,6 @@ type LogicalAgentTurns = {
5140
5066
  begin(turn: TurnId, meta: JsonValue, execution: AgentExecution): Promise<void>;
5141
5067
  end(turn: TurnId, meta: JsonValue, outcome: TurnOutcome<JsonValue>): Promise<void>;
5142
5068
  restored(): RestoredTurn | undefined;
5143
- /** Throws, or logs the deprecation, unless the workflow declares this agent's `{key}:ended`. */
5069
+ /** Throws unless the workflow declares this agent's `{key}:ended`. */
5144
5070
  checkDeclared(): void;
5145
5071
  };
5146
-
5147
- /**
5148
- * The run's deadline as `wf.deadline` hands it: a new `Date` each time, so one a workflow changes
5149
- * isn't the run's, whose deprecated `unixMilliseconds` reports its use.
5150
- */
5151
- function runDeadline(
5152
- deadline: AbsoluteDeadline | undefined,
5153
- deprecated: () => void,
5154
- ): WorkflowContext["deadline"] {
5155
- if (!deadline) return undefined;
5156
- const date = new Date(deadline.unixMilliseconds);
5157
- Object.defineProperty(date, "unixMilliseconds", {
5158
- get() {
5159
- deprecated();
5160
- return date.getTime();
5161
- },
5162
- });
5163
- return date as WorkflowContext["deadline"];
5164
- }