@agentswf/engine 0.1.0-beta.4 → 0.1.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/package.json +4 -4
  2. package/src/agent-cli/cli.ts +57 -17
  3. package/src/attempt-ending.ts +2 -4
  4. package/src/attempt-output.ts +2 -1
  5. package/src/attempt-view.ts +1 -1
  6. package/src/control-plane.ts +37 -5
  7. package/src/coordinator-socket.ts +15 -6
  8. package/src/deadlines.ts +69 -2
  9. package/src/decisions/directory.ts +9 -11
  10. package/src/deprecations.ts +177 -0
  11. package/src/duration.ts +50 -20
  12. package/src/emit-command.ts +55 -41
  13. package/src/emit.ts +124 -15
  14. package/src/help/emit.txt +12 -4
  15. package/src/help/inbox.txt +2 -2
  16. package/src/help/logs.txt +5 -4
  17. package/src/help/reply.txt +5 -5
  18. package/src/help/run.txt +21 -7
  19. package/src/herdr-run-session.ts +2 -2
  20. package/src/holder-command.ts +36 -31
  21. package/src/inbox-command.ts +7 -10
  22. package/src/init-template/hello/workflow.test.ts.template +3 -3
  23. package/src/init-template/hello/workflow.ts.template +2 -2
  24. package/src/keep-alive.ts +28 -13
  25. package/src/logs-command.ts +12 -12
  26. package/src/mailbox.ts +2 -20
  27. package/src/messaging/limits.ts +4 -4
  28. package/src/messaging/messaging.ts +26 -21
  29. package/src/messaging/routes.ts +1 -1
  30. package/src/messaging/text.ts +7 -5
  31. package/src/operator-cli.ts +23 -9
  32. package/src/operator-runtime.ts +5 -5
  33. package/src/pane-layout.ts +125 -32
  34. package/src/progress-view.ts +8 -8
  35. package/src/run-command.ts +25 -4
  36. package/src/run-commands.ts +1 -1
  37. package/src/run-prepare.ts +46 -8
  38. package/src/run-usage.ts +1 -1
  39. package/src/run-view.ts +9 -4
  40. package/src/runs-command.ts +12 -10
  41. package/src/runs.ts +44 -8
  42. package/src/sources.ts +42 -6
  43. package/src/stage-ledger.ts +42 -12
  44. package/src/stage-plan.ts +37 -1
  45. package/src/test-command.ts +7 -3
  46. package/src/testing.ts +7 -8
  47. package/src/tools.ts +46 -18
  48. package/src/workflow-loader.ts +5 -2
  49. package/src/workflow-runner.ts +553 -235
  50. package/src/workflow-testing/coordinator.typecheck.ts +7 -7
  51. package/src/workflow-testing/coordinators.ts +2 -2
  52. package/src/workflow-testing/host.ts +107 -38
  53. package/src/workflow-testing/index.ts +45 -836
  54. package/src/workflow-testing/run-test.ts +1213 -0
  55. package/src/workflow-testing/script.ts +102 -41
  56. package/src/workflow-testing/workflow-testing.typecheck.ts +11 -8
@@ -13,6 +13,7 @@ import type {
13
13
  AgentPaneRecord,
14
14
  AgentSkillsRecord,
15
15
  RunAccounting,
16
+ RunPanes,
16
17
  SandboxRecord,
17
18
  SettledDecision,
18
19
  SettledOperation,
@@ -31,7 +32,8 @@ import {
31
32
  } from "@agentswf/contract/schema";
32
33
  import {
33
34
  type AbsoluteDeadline,
34
- type AgentEndedEvent,
35
+ type AgentEnded,
36
+ type AgentEndOf,
35
37
  type AgentExecution,
36
38
  type AgentForkSpec,
37
39
  type AgentKey,
@@ -40,42 +42,49 @@ import {
40
42
  type AgentRunStructuredSpec,
41
43
  type AgentRunTextSpec,
42
44
  type AgentStartSpec,
45
+ awfEventOf,
43
46
  type Branches,
47
+ type CallerForkSpec,
44
48
  type CodeParticipantRef,
45
49
  type CompactionId,
46
50
  type CompactSpec,
51
+ DEPRECATED_DEFINITION,
47
52
  DeadlineExceededError,
48
53
  type Duration,
54
+ durationMilliseconds,
49
55
  type EmitOutcome,
50
- type EventAgentOpenSpec,
51
- type EventAgentRef,
56
+ type EventLike,
57
+ type ForkRuntime,
52
58
  type HarnessAllowance,
53
59
  isEventWorkflow,
54
60
  isJsonValue,
55
61
  type JsonObject,
56
62
  type JsonValue,
57
63
  type KeepPane,
58
- type Matched,
59
64
  type MessageAccess,
60
65
  type Next,
61
66
  nameProblem,
62
67
  type OperationRecord,
63
68
  type OutputSchema,
64
69
  type PaneLayout,
70
+ type PaneSpec,
71
+ type ParticipantDirectory,
65
72
  type ParticipantRef,
66
73
  type ParticipantSpec,
67
74
  type PlacementChoice,
68
75
  placementOf,
76
+ positiveDurationMilliseconds,
69
77
  type RaceBranches,
70
- type ReceiveOptions,
78
+ REPORT_DEPRECATED,
71
79
  type RunResult,
72
80
  type RuntimeSelection,
73
- type SelectOptions,
74
81
  type SettingsSpec,
75
82
  type SkillSource,
83
+ type Source,
76
84
  type StageOptions,
77
85
  type StageSummary,
78
86
  type StartedTurn,
87
+ type Taken,
79
88
  type TimeRead,
80
89
  type Timer,
81
90
  type TurnId,
@@ -83,8 +92,8 @@ import {
83
92
  type WaitOptions,
84
93
  type When,
85
94
  type WorkflowContext,
86
- type WorkflowDefinition,
87
95
  type WorkflowEvent,
96
+ type WorkflowMeta,
88
97
  } from "@agentswf/contract/workflow";
89
98
  import {
90
99
  type AgentSkills,
@@ -122,8 +131,10 @@ import type { CoordinatorPort } from "./coordinator-port";
122
131
  import {
123
132
  assertDeadline,
124
133
  assertDeadlineValue,
134
+ boundedDeadline,
125
135
  deadlineWithin,
126
136
  earlierDeadline,
137
+ type GivenBound,
127
138
  harnessDeadline,
128
139
  runUntilStopped,
129
140
  scheduleAt,
@@ -132,6 +143,7 @@ import {
132
143
  } from "./deadlines";
133
144
  import { RunDecisions } from "./decisions/directory";
134
145
  import type { DecisionInstallation } from "./decisions/seam";
146
+ import { type Deprecated, RunDeprecations } from "./deprecations";
135
147
  import { parseWhen } from "./duration";
136
148
  import { emitEvent } from "./emit";
137
149
  import { messageOf } from "./errors";
@@ -165,7 +177,14 @@ import {
165
177
  type SupervisedOutcome,
166
178
  superviseOperation,
167
179
  } from "./operation-liveness";
168
- import { besideOf, checkPaneOptions, keepsPane, storedLayout } from "./pane-layout";
180
+ import {
181
+ besideOf,
182
+ checkPaneOptions,
183
+ keepsPane,
184
+ type PaneAsked,
185
+ paneAsked,
186
+ storedLayout,
187
+ } from "./pane-layout";
169
188
  import { createResultSlotRegistry, type ResultSlotRegistry } from "./result-slots";
170
189
  import { RunEventLog } from "./run-event-log";
171
190
  import {
@@ -181,7 +200,7 @@ import {
181
200
  type OperationTags,
182
201
  type RunLedger,
183
202
  } from "./run-usage";
184
- import { endTurnsLine, readStageRecords, readTurns } from "./runs";
203
+ import { endTurnsLine, readAttempts, readStageRecords, readTurns } from "./runs";
185
204
  import { type CredentialLocks, placeCarried, seedHome } from "./sandbox-homes";
186
205
  import {
187
206
  RUN_SANDBOX_ONLY,
@@ -191,18 +210,35 @@ import {
191
210
  } from "./sandboxes";
192
211
  import { QUOTING_RULE, simpleInput, takesText, usageLine } from "./simple-input";
193
212
  import { placeSkills, RunSkills, readSkillSources } from "./skills/run-skills";
194
- import { asSource, branchOf, branchValue, source, takeOf } from "./sources";
213
+ import {
214
+ asSource,
215
+ branchOf,
216
+ branchValue,
217
+ isSignal,
218
+ matches,
219
+ presented,
220
+ signalOf,
221
+ source,
222
+ takeOf,
223
+ } from "./sources";
195
224
  import { StageLedger, type StageProgress } from "./stage-ledger";
225
+ import { lastRedo } from "./stage-plan";
196
226
  import { FromStageUnreached } from "./stopped";
197
227
  import { inToolHandler, outsideTools, refusedInTool, ToolCalls, toolsLesson } from "./tools";
198
228
  import type { EventWorkflow } from "./workflow-loader";
199
229
 
200
230
  export { WorkflowCancelledError } from "./deadlines";
201
231
 
232
+ /** What the runner needs of a workflow: its meta and its run. */
233
+ export type RunnableWorkflow<Args extends JsonValue, Result extends JsonValue> = {
234
+ readonly meta: WorkflowMeta;
235
+ run(context: WorkflowContext, args: Args): Promise<Result>;
236
+ };
237
+
202
238
  export type RunWorkflowOptions = {
203
239
  /** Internal timing policy; workflow authors use the operation deadline. */
204
240
  livenessPolicy?: LivenessPolicy;
205
- /** A `keepAlive` window that gives no `timeoutMs`; absent, an hour. A test's is short. */
241
+ /** A `keepAlive` that gives no `window`; absent, an hour. A test's is short. */
206
242
  defaultWindowMs?: number;
207
243
  /** The run's time, which its timers and `wf.now()` read; absent, the clock's. A test's is virtual. */
208
244
  now?: () => number;
@@ -218,6 +254,8 @@ export type RunWorkflowOptions = {
218
254
  fromStage?: string;
219
255
  /** `--values`: the values of stages before `fromStage` that have no record to reuse. */
220
256
  values?: ReadonlyMap<string, JsonValue>;
257
+ /** `--workspace` and `--keep-panes`, as the run's record keeps them for every attempt. */
258
+ panes?: RunPanes;
221
259
  /** A run of a workflow with `events`. */
222
260
  events?: true;
223
261
  /** With `events`: runs `work` holding the run's lock, as every append to the mailbox is made. */
@@ -244,7 +282,7 @@ export type RunWorkflowOptions = {
244
282
  deadline?: AbsoluteDeadline;
245
283
  cwd?: string;
246
284
  onLog?: (message: string, fields?: JsonObject) => void;
247
- /** The operator hands the session back once agents.stop has released it. */
285
+ /** The operator hands the session back once close() has released it. */
248
286
  onCallerStopped?: (reason: string) => Promise<void>;
249
287
  signal?: AbortSignal;
250
288
  /** The sandbox providers agents may run in; without them, a sandboxed agent is refused. */
@@ -304,7 +342,7 @@ export type SettledRun = {
304
342
  sandboxes?: SandboxRecord[];
305
343
  /** Each agent's skills; absent when no agent opened. */
306
344
  skills?: AgentSkillsRecord[];
307
- /** Where each pane agent's pane went; absent when the run placed none. */
345
+ /** Where each TUI agent's pane went; absent when the run placed none. */
308
346
  panes?: AgentPaneRecord[];
309
347
  /** Every decision the run asked, in the order asked; absent when it asked none. */
310
348
  decisions?: SettledDecision[];
@@ -392,7 +430,7 @@ type AgentEntry = {
392
430
  * `undefined` when the channel never opened; the agent state carries the reason.
393
431
  */
394
432
  channel: Promise<ResultChannel | undefined>;
395
- /** Why `agents.stop` ended it; its key is not opened again. */
433
+ /** Why `close()` ended it; its key is not opened again. */
396
434
  stopped?: string;
397
435
  stopping?: Promise<boolean>;
398
436
  stopComplete?: boolean;
@@ -418,10 +456,13 @@ type AgentIdentity = {
418
456
  instructions?: string;
419
457
  labels?: AgentOpenSpec["labels"];
420
458
  /** The agent it was forked from, and the spec it was forked with. */
421
- forkedFrom?: { parent: AgentKey; spec: AgentForkSpec };
459
+ forkedFrom?: { parent: AgentKey; spec: CallerForkSpec };
422
460
  /** As written; a reopen compares them so, never where the pane ended up. */
461
+ pane?: PaneSpec;
423
462
  layout?: PaneLayout;
424
463
  keepPane?: KeepPane;
464
+ /** What its host is asked for its pane: these, with the operator's flags applied. */
465
+ asked?: PaneAsked;
425
466
  };
426
467
 
427
468
  const CLEANUP_GRACE_MILLISECONDS = 5_000;
@@ -434,7 +475,7 @@ const FORK_TIMEOUT_MS = 60_000;
434
475
  const scopes = new AsyncLocalStorage<ExecutionScope>();
435
476
 
436
477
  export async function runWorkflow<Args extends JsonValue, Result extends JsonValue>(
437
- definition: WorkflowDefinition<Args, Result>,
478
+ definition: RunnableWorkflow<Args, Result>,
438
479
  args: Args,
439
480
  options: RunWorkflowOptions,
440
481
  ): Promise<WorkflowRunResult<Result>> {
@@ -447,7 +488,7 @@ export async function runWorkflow<Args extends JsonValue, Result extends JsonVal
447
488
  * handle's `result`'s.
448
489
  */
449
490
  export async function startWorkflow<Args extends JsonValue, Result extends JsonValue>(
450
- definition: WorkflowDefinition<Args, Result>,
491
+ definition: RunnableWorkflow<Args, Result>,
451
492
  args: Args,
452
493
  options: RunWorkflowOptions,
453
494
  ): Promise<WorkflowRunHandle<Result>> {
@@ -456,14 +497,20 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
456
497
  // Before anything opens, so a refusal leaves nothing to close.
457
498
  const withEvents = isEventWorkflow(definition);
458
499
  const locked = options.run.locked ?? (<T>(work: () => Promise<T>) => work());
459
- if (withEvents && (options.run.fromStage !== undefined || options.run.values))
460
- throw new Error("a run of a workflow with events takes no --from-stage or --values");
500
+ if (withEvents && options.run.values)
501
+ throw new Error(
502
+ "a run of a workflow with events takes no --values: before --from-stage, a stage with no record runs",
503
+ );
461
504
  const { dir: runDir, id: runId, attempt } = options.run;
462
505
  const startedAt = Date.now();
463
506
  const cwd = options.cwd ?? process.cwd();
464
507
  // Before anything opens: a record this awf can't read refuses the attempt, and a crash may have
465
508
  // torn the last line an earlier attempt appended.
466
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;
467
514
  // The log is observation: one that cannot be written says so as it is, and fails nothing.
468
515
  await endTurnsLine(runDir).catch(() => undefined);
469
516
  const slots = createResultSlotRegistry({
@@ -512,6 +559,10 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
512
559
  });
513
560
  };
514
561
  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");
515
566
  // Each turn as it settles, so an attempt that dies keeps the sessions and times of its turns.
516
567
  const turns: TurnRecord[] = [];
517
568
  const ledger = createRunLedger({ accounting: options.runtime.host.accounting, startedAt });
@@ -535,6 +586,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
535
586
  ...(options.run.values === undefined ? {} : { values: options.run.values }),
536
587
  ...(definition.meta.version === undefined ? {} : { workflowVersion: definition.meta.version }),
537
588
  ...(withEvents ? { byContent: true } : {}),
589
+ ...(redo === undefined ? {} : { redo }),
538
590
  turns: () => turns,
539
591
  event: (body, at) => events.event(body, at),
540
592
  });
@@ -586,18 +638,26 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
586
638
  : undefined;
587
639
  // Every run's timers live in a mailbox; a run of a workflow with events takes its events there too.
588
640
  // A plain run without `park` waits for its timers in its process.
641
+ const { park } = options.run;
589
642
  const waits = new RunMailbox({
590
643
  runDir,
591
644
  attempt,
592
645
  locked,
593
646
  now,
594
- ...(options.run.park
647
+ ...(park
595
648
  ? {
596
649
  parking: {
597
650
  idle: (stage) => owner.idle(stage),
598
651
  timeout: (signal) => owner.timeout(signal),
599
652
  liveUntil: () => owner.liveUntil(),
600
- park: options.run.park,
653
+ park: async (wait) => {
654
+ // No wake carries `--from-stage`: parked short of it, the redo would be lost unsaid.
655
+ const unreached = stages.fromStageUnreached;
656
+ if (unreached === undefined) return park(wait);
657
+ const stop = new FromStageUnreached(unreached, stages.recorded);
658
+ fatal.reject(stop);
659
+ throw stop;
660
+ },
601
661
  ...(options.run.live ? { live: options.run.live } : {}),
602
662
  ...(options.run.skip ? { skip: options.run.skip } : {}),
603
663
  },
@@ -636,6 +696,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
636
696
  ledger,
637
697
  onLog: warn,
638
698
  log: say("log"),
699
+ deprecations,
639
700
  events,
640
701
  onCallerStopped: options.onCallerStopped,
641
702
  livenessPolicy: options.livenessPolicy,
@@ -644,6 +705,8 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
644
705
  coordinators: options.coordinators ?? [],
645
706
  emit: options.emit ?? machineEmitter,
646
707
  workflow: definition.meta.name,
708
+ declared: new Set(withEvents ? Object.keys(definition.events) : []),
709
+ ...(options.run.panes ? { panes: options.run.panes } : {}),
647
710
  });
648
711
  const stopped = new AbortController();
649
712
  // Aborted by a stop that comes after the body ended; the one that ended it does not cut short the
@@ -699,6 +762,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
699
762
  failure = ending;
700
763
  }
701
764
  bodyEnded = true;
765
+ deprecations.settle();
702
766
  // What the body left running enters no stage; one left open is failed once its turns settle.
703
767
  stages.seal(failed ? messageOf(failure) : "the workflow returned before the stage ended");
704
768
  const cutReading = () => reading.abort();
@@ -928,6 +992,7 @@ class WorkflowOwner {
928
992
  onLog?: Warn;
929
993
  /** The workflow's own log lines. */
930
994
  log(message: string, fields?: JsonObject): void;
995
+ deprecations: RunDeprecations;
931
996
  events: RunEventLog;
932
997
  onCallerStopped?: RunWorkflowOptions["onCallerStopped"];
933
998
  awaitReply: NonNullable<RunWorkflowOptions["awaitReply"]>;
@@ -945,6 +1010,10 @@ class WorkflowOwner {
945
1010
  emit: Emitter;
946
1011
  /** The workflow's name, which with the run's id is the ref its events are `from`. */
947
1012
  workflow: string;
1013
+ /** The names its `events` declares, which `signal` takes. */
1014
+ declared: ReadonlySet<string>;
1015
+ /** The operator's choices for every TUI agent's pane on the host. */
1016
+ panes?: RunPanes;
948
1017
  },
949
1018
  ) {
950
1019
  this.#restored = new Map(options.restoredTurns ?? []);
@@ -992,6 +1061,7 @@ class WorkflowOwner {
992
1061
  this.#tools = new ToolCalls<ExecutionScope>({
993
1062
  record: (event) => options.events.event(event),
994
1063
  runIn: (scope, fn) => (scope ? scopes.run(scope, fn) : scopes.exit(fn)),
1064
+ deprecated: (name, call) => options.deprecations.used(name, call),
995
1065
  });
996
1066
  options.control.attachTools(this.#tools);
997
1067
  const holder = {
@@ -1004,7 +1074,9 @@ class WorkflowOwner {
1004
1074
  runId: options.runId,
1005
1075
  attempt: options.attempt,
1006
1076
  cwd: options.cwd,
1007
- deadline: options.deadline,
1077
+ get deadline() {
1078
+ return runDeadline(options.deadline, () => options.deprecations.used("run-deadline-ms"));
1079
+ },
1008
1080
  agents: {
1009
1081
  open: (spec) => {
1010
1082
  try {
@@ -1013,20 +1085,10 @@ class WorkflowOwner {
1013
1085
  return Promise.reject(error);
1014
1086
  }
1015
1087
  },
1016
- attach: (key, runtime) => {
1017
- if (this.#caller?.key !== key) return unavailable("agents.attach");
1018
- const stopped = this.#agents.get(key)?.stopped;
1019
- if (stopped !== undefined) return Promise.reject(stoppedError(key, stopped));
1020
- if (runtime !== undefined) {
1021
- return Promise.reject(
1022
- new Error(`agent ${key} is the calling session, whose runtime is the operator's`),
1023
- );
1024
- }
1025
- return this.#caller.state;
1026
- },
1027
1088
  stop: (key, reason) => {
1089
+ options.deprecations.used("agents-stop");
1028
1090
  try {
1029
- return this.stopAgent(key, reason);
1091
+ return this.stopAgent(key, reason, "agents.stop");
1030
1092
  } catch (error) {
1031
1093
  return Promise.reject(error);
1032
1094
  }
@@ -1074,6 +1136,7 @@ class WorkflowOwner {
1074
1136
  const decided = this.track(
1075
1137
  options.decisions.decide(spec, {
1076
1138
  deadline: scope?.deadline ?? options.deadline,
1139
+ deprecated: (name, call) => options.deprecations.used(name, call),
1077
1140
  ...(scope ? { add: (cancel) => scope.add((stop) => cancel(stop.reason)) } : {}),
1078
1141
  ...(scope?.stage === undefined ? {} : { stage: scope.stage }),
1079
1142
  }),
@@ -1083,17 +1146,10 @@ class WorkflowOwner {
1083
1146
  },
1084
1147
  },
1085
1148
  participants: {
1086
- connect: async (spec) => this.connectParticipant(spec),
1087
- get: async (key) =>
1088
- (this.#messaging.connected().includes(key) ? this.#outsideRefs.get(key) : undefined) ??
1089
- (this.#logical.get(key) as AgentRef | undefined) ??
1090
- null,
1149
+ connect: (async (spec: ParticipantSpec) =>
1150
+ this.connectParticipant(spec)) as ParticipantDirectory["connect"],
1091
1151
  },
1092
1152
  messages: { allow: async (access) => this.allowRoute(access) },
1093
- steps: {
1094
- run: () => unavailable("steps.run"),
1095
- sleep: () => unavailable("steps.sleep"),
1096
- },
1097
1153
  emit: async (target, run, name, data, emitOptions) => {
1098
1154
  if (this.#closed || this.#cleanupFailures.length)
1099
1155
  throw new Error("workflow context is closed");
@@ -1115,9 +1171,17 @@ class WorkflowOwner {
1115
1171
  },
1116
1172
  parallel: (items, operation, parallelOptions) => {
1117
1173
  const inherited = scopes.getStore()?.deadline ?? options.deadline;
1118
- const deadline = parallelOptions?.deadline
1119
- ? earlierDeadline(parallelOptions.deadline, inherited)
1120
- : inherited;
1174
+ let deadline: AbsoluteDeadline | undefined;
1175
+ 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
+ );
1182
+ } catch (error) {
1183
+ return Promise.reject(error);
1184
+ }
1121
1185
  const label = parallelOptions?.label;
1122
1186
  return runParallel(items, operation, deadline, parallelOptions?.concurrency, () =>
1123
1187
  label === undefined ? undefined : options.progress.group(label, items.length),
@@ -1129,7 +1193,6 @@ class WorkflowOwner {
1129
1193
  },
1130
1194
  stage: ((name: string, ...rest: unknown[]) =>
1131
1195
  this.runStage(name, rest)) as WorkflowContext["stage"],
1132
- call: () => unavailable("call"),
1133
1196
  usage: () => options.ledger.records(),
1134
1197
  allowance: async (harness) => {
1135
1198
  if (this.#closed || this.#cleanupFailures.length)
@@ -1153,33 +1216,29 @@ class WorkflowOwner {
1153
1216
  undefined,
1154
1217
  );
1155
1218
  },
1219
+ // Every run waits on its own mailbox; one without events never parks, so it waits here.
1220
+ receive: (spec?: WaitOptions) => this.receive(options.waits, spec),
1221
+ race: ((name: string, branches: RaceBranches, raceOptions?: WaitOptions) =>
1222
+ this.race(options.waits, name, branches, raceOptions)) as WorkflowContext["race"],
1223
+ select: ((branches: Branches, selectOptions?: WaitOptions) =>
1224
+ this.select(options.waits, branches, selectOptions)) as WorkflowContext["select"],
1225
+ timer: (name: string, when: When) => this.timer(options.waits, name, when),
1226
+ at: (read: () => Date | null) => timeRead(read),
1227
+ after: (duration: Duration) => {
1228
+ this.#notReading("wf.after()");
1229
+ let milliseconds: number;
1230
+ try {
1231
+ milliseconds = durationMilliseconds(duration);
1232
+ } catch (error) {
1233
+ throw new Error(`wf.after takes a duration: ${messageOf(error)}`);
1234
+ }
1235
+ return new Date(options.now() + milliseconds);
1236
+ },
1237
+ signal: ((name: string) => signalSource(name, options.declared)) as WorkflowContext["signal"],
1238
+ ...(options.mailbox?.started
1239
+ ? { started: options.mailbox.started as WorkflowContext["started"] }
1240
+ : {}),
1156
1241
  };
1157
- const { mailbox } = options;
1158
- Object.assign(
1159
- this.context,
1160
- mailbox
1161
- ? {
1162
- receive: (spec?: ReceiveOptions) => this.receive(mailbox, spec),
1163
- race: (name: string, branches: RaceBranches, raceOptions?: WaitOptions) =>
1164
- this.race(mailbox, name, branches, raceOptions),
1165
- select: (branches: Branches, selectOptions?: SelectOptions) =>
1166
- this.select(mailbox, branches, selectOptions),
1167
- timer: (name: string, when: When) => this.timer(mailbox, name, when),
1168
- timers: () => source({}, { name: "timer:due" }),
1169
- at: (read: () => Date | null) => timeRead(read),
1170
- after: (duration: Duration) => {
1171
- this.#notReading("wf.after()");
1172
- if (typeof duration !== "string" || !/^\d+(?:\.\d+)?[dhm]$/.test(duration))
1173
- throw new Error(
1174
- `wf.after takes days, hours or minutes, "1d", "3h", "1.5h"; not ${String(duration)}`,
1175
- );
1176
- return new Date(parseWhen(duration, options.now()).due);
1177
- },
1178
- signal: (name: string) => signalSource(name),
1179
- ...(mailbox.started ? { started: mailbox.started } : {}),
1180
- }
1181
- : { receive: () => Promise.reject(new Error("receive needs a workflow with events")) },
1182
- );
1183
1242
  }
1184
1243
 
1185
1244
  /** Set while a select reads a time from state. */
@@ -1239,11 +1298,13 @@ class WorkflowOwner {
1239
1298
  // A turn's end taken: its agent takes other work again.
1240
1299
  for (const event of taken) {
1241
1300
  if (event.name !== "agent:ended") continue;
1301
+ this.#reporting((event.data as { outcome?: unknown }).outcome);
1242
1302
  const { turn } = event.data as unknown as TurnEnd;
1243
1303
  if (this.#restored.get(event.from)?.turn === turn) this.#restored.delete(event.from);
1244
1304
  this.#logical.get(event.from)?.endTaken(turn);
1245
1305
  }
1246
- return taken;
1306
+ // The mailbox keeps awf's record names; the code is handed the names it declares.
1307
+ return taken.map((event) => presented(event, this.options.declared));
1247
1308
  } finally {
1248
1309
  removeFromScope?.();
1249
1310
  this.#waiting = false;
@@ -1291,8 +1352,7 @@ class WorkflowOwner {
1291
1352
  throw new Error(
1292
1353
  `race ${name} has answered in this attempt; a wait in a loop is named per round, as approval-\${round}`,
1293
1354
  );
1294
- this.#raced.add(name);
1295
- const label = checkedLabel(raceOptions?.label);
1355
+ const label = labelOf(raceOptions?.label);
1296
1356
  const given = branchesOf(`race ${name}`, branches, true);
1297
1357
  // A sleep's time may be a Date, its own timer as a duration is.
1298
1358
  const own = (source: unknown) => typeof source === "string" || source instanceof Date;
@@ -1302,6 +1362,9 @@ class WorkflowOwner {
1302
1362
  own(source) ? [] : [[branch, sourceTake(`race ${name}`, branch, source)] as const],
1303
1363
  ),
1304
1364
  );
1365
+ this.#checkSources(`race ${name}`, takes.entries());
1366
+ // Only once its label and branches are checked: a refused race leaves its name to a retry.
1367
+ this.#raced.add(name);
1305
1368
  const owned = new Map<string, string>();
1306
1369
  const pairs: (readonly [string, WaitTake])[] = given.map(([branch, source]) => {
1307
1370
  if (!own(source)) return [branch, takes.get(branch) as WaitTake] as const;
@@ -1309,29 +1372,33 @@ class WorkflowOwner {
1309
1372
  owned.set(branch, timer.name);
1310
1373
  return [branch, { name: "timer:due", from: timer.name }] as const;
1311
1374
  });
1312
- let won: { branch: string; event: WorkflowEvent<string, JsonValue> } | undefined;
1375
+ let branch: string | undefined;
1376
+ let event: WorkflowEvent<string, JsonValue>;
1313
1377
  try {
1314
- await this.#holdingWait(scope, async (signal) => {
1315
- won = await mailbox.race(name, pairs, owned, label, signal);
1378
+ [event] = (await this.#holdingWait(scope, async (signal) => {
1379
+ const won = await mailbox.race(name, pairs, owned, label, signal);
1380
+ branch = won.branch;
1316
1381
  return [won.event];
1317
- });
1382
+ })) as [WorkflowEvent<string, JsonValue>];
1318
1383
  } catch (error) {
1319
- // Cut short, not parked: its own timers would fire into a later wait. A park's rerun races again.
1320
- if (!(error instanceof RunParked))
1384
+ // Cut short, not parked: its own timers would fire into a later wait. A park's rerun races
1385
+ // again, and so does a continue's after a park refused short of its --from-stage.
1386
+ if (!(error instanceof RunParked || error instanceof FromStageUnreached))
1321
1387
  await Promise.all([...owned.values()].map((timer) => mailbox.cancelTimer(timer))).catch(
1322
1388
  () => undefined,
1323
1389
  );
1324
1390
  throw error;
1325
1391
  }
1326
- const { branch, event } = won as { branch: string; event: WorkflowEvent<string, JsonValue> };
1327
- return { key: branch, [branch]: branchValue(event) } as Next<RaceBranches>;
1392
+ const key = branch as string;
1393
+ const whole = isSignal(given.find(([each]) => each === key)?.[1]);
1394
+ return { key, [key]: branchValue(event, whole) } as Next<RaceBranches>;
1328
1395
  }
1329
1396
 
1330
1397
  /** A loop over the events `branches` take, one at a time. */
1331
1398
  private select(
1332
1399
  mailbox: RunMailbox,
1333
1400
  branches: Branches,
1334
- selectOptions: SelectOptions | undefined,
1401
+ selectOptions: WaitOptions | undefined,
1335
1402
  ): AsyncIterable<Next<Branches>> {
1336
1403
  const given = branchesOf("select", branches, false);
1337
1404
  const reads = new Map(
@@ -1347,6 +1414,7 @@ class WorkflowOwner {
1347
1414
  reads.has(branch) ? { name: AT_DUE, from: branch } : sourceTake("select", branch, source),
1348
1415
  ] as const,
1349
1416
  );
1417
+ this.#checkSources("select", pairs);
1350
1418
  // Read at each pass of the wait: a time follows the state as it moves.
1351
1419
  const takes = (): WaitTake[] =>
1352
1420
  pairs.map(([branch, take]) => {
@@ -1354,20 +1422,17 @@ class WorkflowOwner {
1354
1422
  const due = read && this.#readTime(branch, read);
1355
1423
  return due === undefined ? take : { ...take, due };
1356
1424
  });
1357
- const labelOf = () => {
1358
- const given = selectOptions?.label;
1359
- return checkedLabel(typeof given === "function" ? given() : given);
1360
- };
1361
1425
  const next = async (): Promise<Next<Branches>> => {
1362
1426
  const scope = this.#beginWait("select");
1363
1427
  if (scope?.stage !== undefined)
1364
1428
  throw new Error(`select is in stage ${scope.stage}: a loop's wait is not a stage's`);
1365
1429
  const [event] = await this.#holdingWait(scope, async (signal) => [
1366
- await mailbox.select(takes, labelOf(), signal),
1430
+ await mailbox.select(takes, labelOf(selectOptions?.label), signal),
1367
1431
  ]);
1368
1432
  const taken = event as WorkflowEvent<string, JsonValue>;
1369
1433
  const branch = branchOf(pairs, taken) as string;
1370
- return { key: branch, [branch]: branchValue(taken) } as Next<Branches>;
1434
+ const whole = isSignal(given.find(([each]) => each === branch)?.[1]);
1435
+ return { key: branch, [branch]: branchValue(taken, whole) } as Next<Branches>;
1371
1436
  };
1372
1437
  return {
1373
1438
  [Symbol.asyncIterator]: () => ({
@@ -1377,6 +1442,36 @@ class WorkflowOwner {
1377
1442
  };
1378
1443
  }
1379
1444
 
1445
+ /** A started turn among a wait's branches, which naming its agent replaces. */
1446
+ /**
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.
1450
+ */
1451
+ #checkSources(what: string, takes: Iterable<readonly [string, WaitTake]>): void {
1452
+ const given = [...takes];
1453
+ if (!this.options.mailbox) {
1454
+ const unheard = given.find(([, take]) => HEARD_WITH_EVENTS.has(take.name));
1455
+ if (unheard)
1456
+ throw new Error(
1457
+ `${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
+ );
1459
+ }
1460
+ if (given.some(([, take]) => take.turn !== undefined))
1461
+ this.options.deprecations.used("turn-source");
1462
+ }
1463
+
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
+
1380
1475
  #waiting = false;
1381
1476
  /** The races and sleeps this attempt has run, each by its name once. */
1382
1477
  readonly #raced = new Set<string>();
@@ -1388,6 +1483,18 @@ class WorkflowOwner {
1388
1483
  return {
1389
1484
  begin: (turn, meta, execution) => mailbox.beginTurn(key, turn, meta, execution),
1390
1485
  end: (turn, meta, outcome) => mailbox.endTurn(key, { turn, meta, outcome }),
1486
+ 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"))
1491
+ 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`,
1495
+ );
1496
+ deprecations.used("undeclared-ended", key);
1497
+ },
1391
1498
  // Read when the agent opens: a wait before then may have taken the end.
1392
1499
  restored: () => this.#restored.get(key),
1393
1500
  };
@@ -1442,7 +1549,7 @@ class WorkflowOwner {
1442
1549
  if (!onTimeout) continue;
1443
1550
  try {
1444
1551
  await runUntilStopped(
1445
- async () => asTimeoutHandler(() => onTimeout(agent as unknown as EventAgentRef)),
1552
+ async () => asTimeoutHandler(() => onTimeout(agent)),
1446
1553
  signal,
1447
1554
  undefined,
1448
1555
  );
@@ -1462,14 +1569,16 @@ class WorkflowOwner {
1462
1569
  /** A wait: one at a time, and in a stage, the stage's batch. */
1463
1570
  private async receive(
1464
1571
  mailbox: RunMailbox,
1465
- spec: ReceiveOptions | undefined,
1572
+ spec: WaitOptions | undefined,
1466
1573
  ): Promise<WorkflowEvent<string, JsonValue>[]> {
1467
1574
  const scope = this.#beginWait("receive()");
1575
+ const label = receiveLabel(spec);
1576
+ if (scope?.stage !== undefined) this.options.deprecations.used("stage-receive");
1468
1577
  if (scope?.stage !== undefined && this.#stageBatches.has(scope.stage))
1469
1578
  throw new Error(`stage ${scope.stage} has waited once; a stage holds one wait`);
1470
1579
  try {
1471
1580
  const taken = await this.#holdingWait(scope, (signal) =>
1472
- mailbox.receive(spec, scope?.stage, signal),
1581
+ mailbox.receive(label, scope?.stage, signal),
1473
1582
  );
1474
1583
  if (scope?.stage !== undefined) {
1475
1584
  this.#stageBatches.set(scope.stage, taken);
@@ -1515,17 +1624,32 @@ class WorkflowOwner {
1515
1624
  const to = (spec as { to?: unknown }).to;
1516
1625
  if (to !== undefined && to !== "code")
1517
1626
  throw new Error(`a participant's messages go to agents, or to: "code"; not ${String(to)}`);
1518
- if (to === "code" && !this.options.mailbox)
1519
- throw new Error(
1520
- "a participant routed to the code needs a workflow with events, whose waits take its replies",
1521
- );
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
+ }
1522
1641
  this.#messaging.connect(
1523
1642
  key,
1524
1643
  labels === undefined ? undefined : structuredClone(labels),
1525
- to === "code" ? "code" : undefined,
1644
+ to === "code" || replies ? "code" : undefined,
1526
1645
  );
1527
1646
  const ref: ParticipantRef = {
1528
1647
  ...participantRef(key),
1648
+ post: async (text: string) => {
1649
+ if (typeof text !== "string" || text.trim() === "")
1650
+ throw new Error("post takes the text to write");
1651
+ await this.#messaging.post(key, text);
1652
+ },
1529
1653
  release: async (reason) => {
1530
1654
  if (reason !== undefined && typeof reason !== "string")
1531
1655
  throw new Error("a release's reason is a string");
@@ -1533,16 +1657,16 @@ class WorkflowOwner {
1533
1657
  },
1534
1658
  };
1535
1659
  if (to === "code") {
1536
- const routed = source(
1537
- {
1660
+ const take = { name: "participant:reply", from: key };
1661
+ const routed = asSource(
1662
+ Object.freeze({
1538
1663
  ...ref,
1539
- post: async (text: string) => {
1540
- if (typeof text !== "string" || text.trim() === "")
1541
- throw new Error("post takes the text to write");
1542
- await this.#messaging.post(key, text);
1664
+ is: (event: { name: string; from: string }) => {
1665
+ deprecations.used("participant-is");
1666
+ return matches(event, take);
1543
1667
  },
1544
- },
1545
- { name: "participant:reply", from: key },
1668
+ }),
1669
+ take,
1546
1670
  ) as unknown as CodeParticipantRef;
1547
1671
  this.#outsideRefs.set(key, routed);
1548
1672
  return routed;
@@ -1553,6 +1677,8 @@ class WorkflowOwner {
1553
1677
 
1554
1678
  /** A key an agent may not take: a reserved one, or an outside participant's. */
1555
1679
  #assertFreeKey(key: string, who: string): void {
1680
+ if (typeof key === "string" && key.includes(":"))
1681
+ this.options.deprecations.used("colon-key", key);
1556
1682
  if ((RESERVED_PARTICIPANT_KEYS as readonly string[]).includes(key.toLowerCase()))
1557
1683
  throw new Error(
1558
1684
  `${key} is reserved: an agent could read a message from it as authority; ${who} takes another key`,
@@ -1600,12 +1726,20 @@ class WorkflowOwner {
1600
1726
  throw new Error("a route between agents names two of them");
1601
1727
  ends = { between: [end(shape.between[0]), end(shape.between[1])] };
1602
1728
  } 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
+ const timeoutMs =
1732
+ access.timeout == null
1733
+ ? (access.timeoutMs ?? undefined)
1734
+ : positiveDurationMilliseconds(access.timeout, "a route's timeout");
1603
1735
  this.#messaging.grant(ends, {
1604
1736
  purpose: access.purpose,
1605
1737
  maxMessages: access.maxMessages,
1606
- timeoutMs: access.timeoutMs,
1738
+ timeoutMs,
1607
1739
  scope: scope?.messages ?? this.#messaging.attempt,
1608
1740
  });
1741
+ if (access.timeout == null && access.timeoutMs != null)
1742
+ this.options.deprecations.used("timeout-ms", "on a route");
1609
1743
  }
1610
1744
 
1611
1745
  /** Who owns a message operation a route in `scope` opens: its stage, or the attempt. */
@@ -1694,7 +1828,7 @@ class WorkflowOwner {
1694
1828
  : "cancelled",
1695
1829
  reason: "workflow closing",
1696
1830
  })
1697
- .then(() => state.close("workflow complete")),
1831
+ .then(() => state.terminate("workflow complete")),
1698
1832
  () => undefined,
1699
1833
  ),
1700
1834
  );
@@ -1780,9 +1914,12 @@ class WorkflowOwner {
1780
1914
  // A stage around a wait returns the batch it took, which its record doesn't hold.
1781
1915
  const batch =
1782
1916
  entered.value === undefined && !options
1783
- ? await this.options.mailbox?.batchOf(name)
1917
+ ? (await this.options.mailbox?.batchOf(name))?.map((event) =>
1918
+ presented(event, this.options.declared),
1919
+ )
1784
1920
  : undefined;
1785
1921
  if (batch) {
1922
+ this.options.deprecations.used("stage-receive");
1786
1923
  if (this.#waitedStages.has(name))
1787
1924
  throw new Error(
1788
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`,
@@ -1849,8 +1986,24 @@ class WorkflowOwner {
1849
1986
  }
1850
1987
  }
1851
1988
 
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
+
1852
2004
  private openAgent(spec: AgentOpenSpec): Promise<AgentRef> {
1853
2005
  if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
2006
+ if (spec.labels !== undefined) this.options.deprecations.used("labels");
1854
2007
  scopes.getStore()?.assertAccepting();
1855
2008
  this.#assertFreeKey(spec.key, "an agent");
1856
2009
  if (this.#caller?.key === spec.key) {
@@ -1861,20 +2014,27 @@ class WorkflowOwner {
1861
2014
  throw new Error(`agent ${spec.key}: ${RUN_SANDBOX_ONLY}`);
1862
2015
  const existing = this.#agents.get(spec.key);
1863
2016
  if (existing?.stopped !== undefined) throw stoppedError(spec.key, existing.stopped);
1864
- const given = (spec as EventAgentOpenSpec).keepAlive;
2017
+ const given = spec.keepAlive;
1865
2018
  if (given !== undefined && !this.options.mailbox)
1866
2019
  throw new Error(
1867
2020
  `agent ${spec.key}: keepAlive is for a workflow with events, whose runs park; this one's never do`,
1868
2021
  );
1869
- const keepAlive = given === undefined ? undefined : checkedKeepAlive(spec.key, given);
2022
+ const keepAlive =
2023
+ given === undefined
2024
+ ? undefined
2025
+ : checkedKeepAlive(spec.key, given, () =>
2026
+ this.options.deprecations.used("keep-alive-timeout-ms"),
2027
+ );
1870
2028
  const execution = existing
1871
2029
  ? constrainExistingExecution(spec.runtime, existing.identity.execution)
1872
2030
  : withKnownEffort(resolveExecution(spec.runtime, this.options.runtime));
1873
- checkPaneOptions(spec.key, spec, {
2031
+ const paneless = {
1874
2032
  headless: placementOf(execution) === "headless",
1875
2033
  sandboxed:
1876
2034
  spec.sandbox !== undefined || existing?.identity.sandbox !== undefined || inRunSandbox,
1877
- });
2035
+ };
2036
+ checkPaneOptions(spec.key, spec, paneless);
2037
+ const written = this.writtenPane(spec);
1878
2038
  const identity: AgentIdentity = {
1879
2039
  execution,
1880
2040
  cwd: spec.cwd ?? this.options.cwd,
@@ -1882,14 +2042,17 @@ class WorkflowOwner {
1882
2042
  ...(spec.labels === undefined ? {} : { labels: structuredClone(spec.labels) }),
1883
2043
  ...(spec.sandbox === undefined ? {} : { sandbox: spec.sandbox }),
1884
2044
  ...(spec.skills === undefined ? {} : { skills: readSkillSources(spec.skills) }),
1885
- ...(spec.layout === undefined ? {} : { layout: storedLayout(spec.layout) }),
1886
- ...(spec.keepPane === undefined ? {} : { keepPane: spec.keepPane }),
2045
+ ...written,
2046
+ asked: paneAsked(written, this.options.panes, !paneless.headless && !paneless.sandboxed),
1887
2047
  };
1888
2048
  const scope = scopes.getStore();
1889
2049
  const inheritedDeadline = scope?.deadline ?? this.options.deadline;
1890
- const effectiveDeadline = spec.deadline
1891
- ? earlierDeadline(spec.deadline, inheritedDeadline)
1892
- : inheritedDeadline;
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
+ );
1893
2056
  assertDeadline(effectiveDeadline);
1894
2057
  if (existing) {
1895
2058
  assertCompatibleAgent(spec.key, existing.identity, identity, spec);
@@ -1950,9 +2113,9 @@ class WorkflowOwner {
1950
2113
  ...(seated?.webSearch ? { webSearch: true } : {}),
1951
2114
  ...(skills ? { skills } : {}),
1952
2115
  ...(home ? { home } : {}),
1953
- ...paneOptions(identity),
2116
+ ...identity.asked,
1954
2117
  }),
1955
- layoutFallback: this.besideFallback(identity.layout, effectiveDeadline)(),
2118
+ layoutFallback: this.besideFallback(identity.asked?.layout, effectiveDeadline)(),
1956
2119
  };
1957
2120
  let skills: Promise<PlacedSkills | undefined> = Promise.resolve(undefined);
1958
2121
  let opened: { state: Promise<LogicalAgent>; channel: Promise<ResultChannel> };
@@ -1987,9 +2150,9 @@ class WorkflowOwner {
1987
2150
  * Ends an agent the workflow has no more use for, as an operation that fails ends it: what it
1988
2151
  * is running is cancelled, and its pane closed or, where `keepPane` says, kept and released.
1989
2152
  */
1990
- private stopAgent(key: string, reason?: string): Promise<boolean> {
2153
+ private stopAgent(key: string, reason: string | undefined, what: string): Promise<boolean> {
1991
2154
  if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
1992
- const inTool = refusedInTool(key, "agents.stop");
2155
+ const inTool = refusedInTool(key, what);
1993
2156
  if (inTool) throw inTool;
1994
2157
  const scope = scopes.getStore();
1995
2158
  scope?.assertAccepting();
@@ -2014,7 +2177,7 @@ class WorkflowOwner {
2014
2177
  }
2015
2178
  this.options.progress.agentStopped(key, entry.stopped!);
2016
2179
  try {
2017
- await state.close(entry.stopped);
2180
+ await state.terminate(entry.stopped);
2018
2181
  } finally {
2019
2182
  await entry.channel.then((channel) => channel?.close());
2020
2183
  }
@@ -2044,7 +2207,7 @@ class WorkflowOwner {
2044
2207
  /**
2045
2208
  * The session the run was started from, under the key the workflow names: `null` when there is
2046
2209
  * none. It is found, not opened: its harness, directory and skills are the operator's, and it
2047
- * answers through a launcher by path as any pane agent does (ADR 0010).
2210
+ * answers through a launcher by path as any TUI agent does (ADR 0010).
2048
2211
  */
2049
2212
  private caller(key: string): Promise<AgentRef | null> {
2050
2213
  if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
@@ -2121,7 +2284,7 @@ class WorkflowOwner {
2121
2284
  * it is that agent's own fork, taken in its queue; otherwise it is taken now, from a parent the
2122
2285
  * run never opened. `null`, and the reason logged, where there is none to fork.
2123
2286
  */
2124
- private forkCaller(spec: AgentForkSpec): Promise<AgentRef | null> {
2287
+ private forkCaller(spec: CallerForkSpec): Promise<AgentRef | null> {
2125
2288
  if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
2126
2289
  const scope = scopes.getStore();
2127
2290
  scope?.assertAccepting();
@@ -2150,11 +2313,10 @@ class WorkflowOwner {
2150
2313
  channel: Promise.resolve(undefined),
2151
2314
  };
2152
2315
  parent.state.catch(() => undefined);
2153
- const deadline = scope?.deadline ?? this.options.deadline;
2154
2316
  return this.forkAgent(
2155
2317
  CALLING,
2156
2318
  spec,
2157
- (into) =>
2319
+ (deadline, into) =>
2158
2320
  calling
2159
2321
  .fork(deadlineWithin(FORK_TIMEOUT_MS, deadline), into, this.#closingRun.signal)
2160
2322
  .then((native) => ({ native, after: null })),
@@ -2399,7 +2561,16 @@ class WorkflowOwner {
2399
2561
  livenessPolicy: this.options.livenessPolicy,
2400
2562
  ...(this.options.mailbox ? { turns: this.#turnsOf(key, this.options.mailbox) } : {}),
2401
2563
  fork: (spec, take, settings) => this.forkAgent(key, spec, take, settings),
2402
- keepPane: () => this.#agents.get(key)?.identity.keepPane,
2564
+ close: (reason) => {
2565
+ try {
2566
+ return this.stopAgent(key, reason, "close()");
2567
+ } catch (error) {
2568
+ return Promise.reject(error);
2569
+ }
2570
+ },
2571
+ deprecated: (name, subject) => this.options.deprecations.used(name, subject),
2572
+ reporting: (outcome) => this.#reporting(outcome),
2573
+ keepPane: () => this.#agents.get(key)?.identity.asked?.keepPane,
2403
2574
  keepAlive: () => this.#agents.get(key)?.keepAlive,
2404
2575
  defaultWindowMs: this.options.defaultWindowMs ?? DEFAULT_WINDOW_MS,
2405
2576
  now: this.options.now,
@@ -2431,14 +2602,18 @@ class WorkflowOwner {
2431
2602
  private forkAgent(
2432
2603
  parentKey: string,
2433
2604
  spec: AgentForkSpec,
2434
- /** Queues the native fork, or its copy `into` another home; or why its host cannot fork. */
2435
- take: ((into?: SessionCopy) => Promise<TakenFork>) | string,
2605
+ /**
2606
+ * Queues the native fork, or its copy `into` another home, within the fork's `deadline`; or why
2607
+ * its host cannot fork.
2608
+ */
2609
+ take: TakeFork | string,
2436
2610
  /** The parent's settings once every `set` queued before the fork has run. */
2437
2611
  parentSettings: AgentExecution,
2438
2612
  /** A parent the run never opened: the session it was started from. */
2439
2613
  unopened?: AgentEntry,
2440
2614
  ): Promise<AgentRef> {
2441
2615
  if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
2616
+ if (spec.labels !== undefined) this.options.deprecations.used("labels");
2442
2617
  const scope = scopes.getStore();
2443
2618
  scope?.assertAccepting();
2444
2619
  this.options.stages.checkOperation();
@@ -2447,10 +2622,19 @@ class WorkflowOwner {
2447
2622
  }
2448
2623
  this.#assertFreeKey(spec.key, "an agent");
2449
2624
  // Out before the clone, which throws on a handler, and out of the spec a repeat is compared by.
2450
- const { tools, ...cloned } = spec;
2451
- const forkSpec = structuredClone(cloned);
2625
+ // A bound is the call's, not the fork's: a repeat with another is the same fork.
2626
+ const { tools, timeout, ...given } = spec;
2627
+ const forkSpec = structuredClone(
2628
+ forkSpecOf(given, () => this.options.deprecations.used("fork-runtime")),
2629
+ );
2452
2630
  if (forkSpec.layout !== undefined) forkSpec.layout = storedLayout(forkSpec.layout);
2453
- const deadline = scope?.deadline ?? this.options.deadline;
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
+ );
2454
2638
  assertDeadline(deadline);
2455
2639
  // A fork of the calling session is one whichever way it was asked for.
2456
2640
  const from = unopened || this.#caller?.key === parentKey ? CALLING : parentKey;
@@ -2480,15 +2664,14 @@ class WorkflowOwner {
2480
2664
  sandboxed || opening.accounted.home !== undefined
2481
2665
  ? join(this.options.runDir, "forks", randomUUID())
2482
2666
  : undefined;
2483
- const execution = withKnownEffort(forkExecution(parentSettings, forkSpec));
2667
+ const execution = withKnownEffort(forkExecution(parentSettings, forkSpec.runtime));
2484
2668
  // A fork of the calling session is an agent the run opened, on the model the session's files
2485
2669
  // show, which the fork reads: `""` until then (ADR 0010).
2486
2670
  const ofCaller = execution.caller === true;
2487
2671
  delete execution.caller;
2488
- checkPaneOptions(spec.key, forkSpec, {
2489
- headless: placementOf(execution) === "headless",
2490
- sandboxed,
2491
- });
2672
+ const headless = placementOf(execution) === "headless";
2673
+ checkPaneOptions(spec.key, forkSpec, { headless, sandboxed });
2674
+ const written = this.writtenPane(forkSpec);
2492
2675
  const identity: AgentIdentity = {
2493
2676
  execution,
2494
2677
  cwd: parent.identity.cwd,
@@ -2497,8 +2680,8 @@ class WorkflowOwner {
2497
2680
  ...(parent.identity.skills ? { skills: parent.identity.skills } : {}),
2498
2681
  ...(parent.identity.sandbox === undefined ? {} : { sandbox: parent.identity.sandbox }),
2499
2682
  forkedFrom: { parent: from, spec: forkSpec },
2500
- ...(forkSpec.layout === undefined ? {} : { layout: forkSpec.layout }),
2501
- ...(forkSpec.keepPane === undefined ? {} : { keepPane: forkSpec.keepPane }),
2683
+ ...written,
2684
+ asked: paneAsked(written, this.options.panes, !headless && !sandboxed),
2502
2685
  };
2503
2686
 
2504
2687
  this.options.progress.agentOpened(spec.key, scope?.group);
@@ -2512,11 +2695,11 @@ class WorkflowOwner {
2512
2695
  sessions: () => reportedSessions(sessions),
2513
2696
  forkedAt: () => forkedAt,
2514
2697
  };
2515
- const besideAtFork = this.besideFallback(identity.layout, deadline);
2698
+ const besideAtFork = this.besideFallback(identity.asked?.layout, deadline);
2516
2699
  let continues: NativeFork | undefined;
2517
2700
  // The fork is asked for now, in the parent's queue; the child opens once it is made. One that
2518
2701
  // was not made leaves nothing behind: its key may be forked again, after the parent's turn.
2519
- const forked = take(copy === undefined ? undefined : { directory: copy })
2702
+ const forked = take(deadline, copy === undefined ? undefined : { directory: copy })
2520
2703
  .then(({ native: fork, after }) => {
2521
2704
  if (ofCaller) {
2522
2705
  if (!fork.model) throw new Error(`agent ${parentKey}'s fork named no model to run on`);
@@ -2569,7 +2752,7 @@ class WorkflowOwner {
2569
2752
  ...(given ? { skills: given } : {}),
2570
2753
  ...(home ? { home } : {}),
2571
2754
  ...(continues ? { continues } : {}),
2572
- ...paneOptions(identity),
2755
+ ...identity.asked,
2573
2756
  }),
2574
2757
  // The target as it stands when `fork` is called, waited for once the fork is made.
2575
2758
  layoutFallback: forked.then(besideAtFork, () => undefined),
@@ -2690,16 +2873,17 @@ class WorkflowOwner {
2690
2873
  );
2691
2874
  }
2692
2875
 
2693
- /** Where each pane agent's pane went, as its host reported it, in the order opened. */
2876
+ /** Where each TUI agent's pane went, as its host reported it, in the order opened. */
2694
2877
  panes(): AgentPaneRecord[] {
2695
2878
  return [...this.#agents].flatMap(([agent, entry]) => {
2696
2879
  const placed = entry.opening?.sessions.harness?.pane?.();
2697
2880
  if (!placed) return [];
2698
- const { layout, keepPane } = entry.identity;
2881
+ const { pane, layout, keepPane } = entry.identity;
2699
2882
  return [
2700
2883
  {
2701
2884
  callPath: [],
2702
2885
  agent,
2886
+ ...(pane === undefined ? {} : { pane }),
2703
2887
  ...(layout === undefined ? {} : { layout }),
2704
2888
  ...(keepPane === undefined ? {} : { keepPane }),
2705
2889
  placed,
@@ -2726,6 +2910,11 @@ class WorkflowOwner {
2726
2910
  }
2727
2911
  }
2728
2912
 
2913
+ // What a wait takes of it is type-only, and `asSource` marks it at run time.
2914
+ // biome-ignore lint/correctness/noUnusedVariables: merged into the class below.
2915
+ interface LogicalAgent extends Source<Taken<AgentEnded>> {}
2916
+
2917
+ // biome-ignore lint/suspicious/noUnsafeDeclarationMerging: the interface adds a type-only member.
2729
2918
  class LogicalAgent implements AgentRef {
2730
2919
  readonly [PARTICIPANT] = true as const;
2731
2920
  readonly #operationStops = new Set<(stop: OperationStop) => Promise<void>>();
@@ -2804,14 +2993,19 @@ class LogicalAgent implements AgentRef {
2804
2993
  */
2805
2994
  fork(
2806
2995
  spec: AgentForkSpec,
2807
- take: ((into?: SessionCopy) => Promise<TakenFork>) | string,
2996
+ take: TakeFork | string,
2808
2997
  settings: AgentExecution,
2809
2998
  ): Promise<AgentRef>;
2999
+ /** The workflow ends it: the run tears it down and its key is not opened again. */
3000
+ 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;
2810
3004
  /** As the workflow wrote it; decides, at close, whether its pane stays. */
2811
3005
  keepPane(): KeepPane | undefined;
2812
3006
  /** As the workflow's last open of it gave it. */
2813
3007
  keepAlive(): KeepAlive | undefined;
2814
- /** Its window when `keepAlive` gives no `timeoutMs`. */
3008
+ /** Its window when `keepAlive` gives no `window`. */
2815
3009
  defaultWindowMs: number;
2816
3010
  /** The run's time, which its window is measured on. */
2817
3011
  now: () => number;
@@ -2825,7 +3019,7 @@ class LogicalAgent implements AgentRef {
2825
3019
  const restored = options.turns?.restored();
2826
3020
  if (restored)
2827
3021
  this.#started = {
2828
- ref: turnRef(options.key, restored.turn, async () => false),
3022
+ ref: turnRef(options.key, restored.turn, async () => false, options.deprecated),
2829
3023
  meta: restored.meta,
2830
3024
  ended: true,
2831
3025
  done: Promise.resolve(),
@@ -2865,7 +3059,7 @@ class LogicalAgent implements AgentRef {
2865
3059
  liveUntil(now = this.options.now()): number | undefined {
2866
3060
  const keepAlive = this.options.keepAlive();
2867
3061
  if (this.#closed || !keepAlive || this.#lastEnd === undefined) return undefined;
2868
- const until = this.#lastEnd + (keepAlive.timeoutMs ?? this.options.defaultWindowMs);
3062
+ const until = this.#lastEnd + (keepAlive.windowMs ?? this.options.defaultWindowMs);
2869
3063
  return until > now ? until : undefined;
2870
3064
  }
2871
3065
 
@@ -2928,8 +3122,12 @@ class LogicalAgent implements AgentRef {
2928
3122
 
2929
3123
  async start(spec: AgentStartSpec): Promise<StartedTurn> {
2930
3124
  const { turns } = this.options;
2931
- if (!turns) return unavailable("AgentRef.start");
3125
+ if (!turns)
3126
+ throw new Error(
3127
+ `agent ${this.key}: start() is for a workflow with events, at whose waits its end arrives; run() waits for the end here`,
3128
+ );
2932
3129
  if (this.#closed) throw new Error("logical agent is closed");
3130
+ turns.checkDeclared();
2933
3131
  const inTool = refusedInTool(this.key, "start()");
2934
3132
  if (inTool) throw inTool;
2935
3133
  const scope = scopes.getStore();
@@ -2940,24 +3138,30 @@ class LogicalAgent implements AgentRef {
2940
3138
  const refused = this.#refusedWhileStarted("start");
2941
3139
  if (refused) throw refused;
2942
3140
  const { meta = null, ...run } = spec;
3141
+ if (run.id !== undefined) this.options.deprecated("call-id");
2943
3142
  const id = run.id ?? randomUUID();
2944
3143
  if (!isJsonValue(meta)) throw new Error("a turn's meta is a JSON value");
2945
3144
  const ended = Promise.withResolvers<void>();
2946
3145
  let written: TurnOutcome<JsonValue>["kind"] | undefined;
2947
3146
  const started = {
2948
- ref: turnRef(this.key, id, async (reason?: string) => {
2949
- if (started.ended) return false;
2950
- // Its turn waits for the handler, which would wait for the turn's end.
2951
- const inTool = refusedInTool(this.key, "cancel()");
2952
- if (inTool) throw inTool;
2953
- const stop = { kind: "cancelled" as const, reason: reason ?? "the turn was cancelled" };
2954
- // Its own operation only: one queued behind a message settles cancelled when it is reached.
2955
- const own = this.#stopsByTurn.get(id);
2956
- if (own) await own(stop);
2957
- else this.#cancelledTurns.set(id, stop.reason);
2958
- await started.done;
2959
- return written === "cancelled";
2960
- }),
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
+ ),
2961
3165
  meta,
2962
3166
  ended: false,
2963
3167
  done: ended.promise,
@@ -3023,11 +3227,12 @@ class LogicalAgent implements AgentRef {
3023
3227
  );
3024
3228
  }
3025
3229
 
3026
- is<Meta extends JsonValue = JsonValue, T extends JsonValue = JsonValue>(event: {
3027
- readonly name: string;
3028
- readonly from: string;
3029
- }): event is AgentEndedEvent<Meta, T> & Matched {
3030
- return event.name === "agent:ended" && event.from === this.key;
3230
+ is<
3231
+ Meta extends JsonValue = JsonValue,
3232
+ T extends JsonValue = JsonValue,
3233
+ E extends EventLike = EventLike,
3234
+ >(event: E): event is E & AgentEndOf<E, Meta, T> {
3235
+ return matches(event, { name: "agent:ended", from: this.key });
3031
3236
  }
3032
3237
 
3033
3238
  run(spec: AgentRunTextSpec): Promise<RunResult<string>>;
@@ -3046,6 +3251,7 @@ class LogicalAgent implements AgentRef {
3046
3251
  return Promise.reject(error);
3047
3252
  }
3048
3253
  const opensWindow = !inTimeoutHandler();
3254
+ if (spec.id !== undefined && !this.#launching) this.options.deprecated("call-id");
3049
3255
  const id = spec.id ?? randomUUID();
3050
3256
  const completeSpec = structuredClone({ ...spec, id }) as
3051
3257
  | AgentRunTextSpec
@@ -3064,11 +3270,13 @@ class LogicalAgent implements AgentRef {
3064
3270
  }
3065
3271
  let deadline: AbsoluteDeadline | undefined;
3066
3272
  try {
3067
- deadline = this.operationDeadline(spec, scope);
3273
+ deadline = this.operationDeadline(spec, scope, this.#launching ? "on start" : "on run");
3068
3274
  const recoveryDeadline = spec.nudge ? spec.nudge.deadline : undefined;
3069
3275
  if (recoveryDeadline) {
3070
- assertDeadlineValue(recoveryDeadline);
3071
- deadline = earlierDeadline(deadline, recoveryDeadline);
3276
+ this.options.deprecated("nudge-deadline");
3277
+ const own = { unixMilliseconds: recoveryDeadline.unixMilliseconds };
3278
+ assertDeadlineValue(own);
3279
+ deadline = earlierDeadline(deadline, own);
3072
3280
  }
3073
3281
  } catch (error) {
3074
3282
  const rejected = Promise.reject<RunResult<string | T>>(error);
@@ -3107,7 +3315,12 @@ class LogicalAgent implements AgentRef {
3107
3315
  await this.options.afterOperation?.();
3108
3316
  }
3109
3317
  });
3110
- const tracked = this.options.track(result);
3318
+ const tracked = this.options.track(
3319
+ result.then((settled) => {
3320
+ this.options.reporting(settled.outcome);
3321
+ return settled;
3322
+ }),
3323
+ );
3111
3324
  scope?.track(tracked);
3112
3325
  this.#operations.set(id, { spec: completeSpec, result: tracked });
3113
3326
  return tracked as Promise<RunResult<string | T>>;
@@ -3124,6 +3337,7 @@ class LogicalAgent implements AgentRef {
3124
3337
  } catch (error) {
3125
3338
  return Promise.reject(error);
3126
3339
  }
3340
+ if (spec.id !== undefined) this.options.deprecated("call-id");
3127
3341
  const id = spec.id ?? randomUUID();
3128
3342
  const completeSpec = structuredClone({ ...spec, id });
3129
3343
  const existing = this.#compactions.get(id);
@@ -3140,7 +3354,7 @@ class LogicalAgent implements AgentRef {
3140
3354
  }
3141
3355
  let deadline: AbsoluteDeadline;
3142
3356
  try {
3143
- deadline = harnessDeadline(this.operationDeadline(spec, scope));
3357
+ deadline = harnessDeadline(this.operationDeadline(spec, scope, "on compact"));
3144
3358
  } catch (error) {
3145
3359
  const rejected = Promise.reject<TurnOutcome<string>>(error);
3146
3360
  scope?.track(rejected);
@@ -3166,7 +3380,7 @@ class LogicalAgent implements AgentRef {
3166
3380
  await this.options.afterOperation?.();
3167
3381
  }
3168
3382
  });
3169
- const tracked = this.options.track(result);
3383
+ const tracked = this.options.track(result.then(this.options.reporting));
3170
3384
  scope?.track(tracked);
3171
3385
  this.#compactions.set(id, { spec: completeSpec, result: tracked });
3172
3386
  return tracked;
@@ -3183,6 +3397,7 @@ class LogicalAgent implements AgentRef {
3183
3397
  } catch (error) {
3184
3398
  return Promise.reject(error);
3185
3399
  }
3400
+ if (spec.id !== undefined) this.options.deprecated("call-id");
3186
3401
  const id = spec.id ?? randomUUID();
3187
3402
  const completeSpec = structuredClone({ ...spec, id });
3188
3403
  const existing = this.#sets.get(id);
@@ -3201,7 +3416,7 @@ class LogicalAgent implements AgentRef {
3201
3416
  let deadline: AbsoluteDeadline;
3202
3417
  try {
3203
3418
  change = this.settingsChange(spec);
3204
- deadline = harnessDeadline(this.operationDeadline(spec, scope));
3419
+ deadline = harnessDeadline(this.operationDeadline(spec, scope, "on set"));
3205
3420
  } catch (error) {
3206
3421
  const rejected = Promise.reject<TurnOutcome<null>>(error);
3207
3422
  scope?.track(rejected);
@@ -3221,7 +3436,7 @@ class LogicalAgent implements AgentRef {
3221
3436
  this.#pendingSets.splice(this.#pendingSets.indexOf(change), 1);
3222
3437
  }
3223
3438
  });
3224
- const tracked = this.options.track(result);
3439
+ const tracked = this.options.track(result.then(this.options.reporting));
3225
3440
  scope?.track(tracked);
3226
3441
  this.#sets.set(id, { spec: completeSpec, result: tracked });
3227
3442
  return tracked;
@@ -3306,7 +3521,6 @@ class LogicalAgent implements AgentRef {
3306
3521
  const refused = this.#refusedWhileStarted("fork");
3307
3522
  if (refused) return Promise.reject(refused);
3308
3523
  const scope = scopes.getStore();
3309
- const deadline = this.deadlineCeiling(scope);
3310
3524
  const { session, execution } = this.options;
3311
3525
  const settings = this.queuedExecution();
3312
3526
  const fork = session.fork?.bind(session);
@@ -3325,7 +3539,7 @@ class LogicalAgent implements AgentRef {
3325
3539
  }
3326
3540
  // The harness bounds its fork by this deadline and stops it there, so the session is idle again
3327
3541
  // when the fork rejects.
3328
- const take = (into?: SessionCopy) =>
3542
+ const take: TakeFork = (deadline, into) =>
3329
3543
  this.queue(async () => {
3330
3544
  if (this.#closed || this.options.isRunClosing()) {
3331
3545
  throw new Error("logical agent is closed");
@@ -3348,19 +3562,19 @@ class LogicalAgent implements AgentRef {
3348
3562
  }
3349
3563
  }
3350
3564
 
3351
- /** `deadline` or `timeoutMs`, never later than the scope's or the run's; else the earlier of those. */
3565
+ /** Its `timeout`, never later than the scope's or the run's; else the earlier of those. */
3352
3566
  private operationDeadline(
3353
- spec: { deadline?: AbsoluteDeadline; timeoutMs?: number },
3567
+ spec: GivenBound,
3354
3568
  scope: ExecutionScope | undefined,
3569
+ call: string,
3355
3570
  ): AbsoluteDeadline | undefined {
3356
- if (spec.deadline && spec.timeoutMs !== undefined) {
3357
- throw new Error("an operation cannot specify both deadline and timeoutMs");
3358
- }
3359
- const ceiling = this.deadlineCeiling(scope);
3360
- if (spec.timeoutMs !== undefined) return deadlineWithin(spec.timeoutMs, ceiling);
3361
- if (!spec.deadline) return ceiling;
3362
- assertDeadlineValue(spec.deadline);
3363
- return earlierDeadline(spec.deadline, ceiling);
3571
+ const { timeout, deadline, timeoutMs } = spec;
3572
+ return boundedDeadline(
3573
+ { timeout, deadline, timeoutMs },
3574
+ this.deadlineCeiling(scope),
3575
+ this.options.deprecated,
3576
+ call,
3577
+ );
3364
3578
  }
3365
3579
 
3366
3580
  /** The deadlines around an operation: its scope's and the run's, if either has one. */
@@ -3432,7 +3646,7 @@ class LogicalAgent implements AgentRef {
3432
3646
  };
3433
3647
  }
3434
3648
  const next: AgentExecution = { ...this.#execution, ...change };
3435
- const pane = placementOf(next) === "pane";
3649
+ const pane = placementOf(next) === "tui";
3436
3650
  let cancelled: string | undefined;
3437
3651
  // A pane's switch waits on its screen; a scope cancelled meanwhile closes the agent, as a
3438
3652
  // half-made switch leaves it at settings nobody knows. A headless one takes no time.
@@ -3440,7 +3654,7 @@ class LogicalAgent implements AgentRef {
3440
3654
  ? scope?.add(async (stop) => {
3441
3655
  cancelled = stop.reason;
3442
3656
  this.#last = "cancelled";
3443
- await this.close(stop.reason);
3657
+ await this.terminate(stop.reason);
3444
3658
  })
3445
3659
  : undefined;
3446
3660
  try {
@@ -3477,10 +3691,15 @@ class LogicalAgent implements AgentRef {
3477
3691
  if (this.options.execution.caller) return;
3478
3692
  // Before its outcome is recorded: whether its pane stays turns on how this one ended.
3479
3693
  this.#last = ended;
3480
- void this.close(reason).catch(() => undefined);
3694
+ void this.terminate(reason).catch(() => undefined);
3695
+ }
3696
+
3697
+ close(reason?: string): Promise<boolean> {
3698
+ return this.options.close(reason);
3481
3699
  }
3482
3700
 
3483
- close(reason?: string): Promise<void> {
3701
+ /** What `close()`, a failed operation and the run's end each come to. */
3702
+ terminate(reason?: string): Promise<void> {
3484
3703
  const first = !this.#closed;
3485
3704
  this.#closed = true;
3486
3705
  if (first) {
@@ -3564,7 +3783,6 @@ class LogicalAgent implements AgentRef {
3564
3783
  ): Promise<RunResult<JsonValue>> {
3565
3784
  const nudge = spec.nudge === false ? undefined : (spec.nudge ?? this.defaultNudge());
3566
3785
  scope?.assertActive();
3567
- if (nudge?.deadline) assertDeadlineValue(nudge.deadline);
3568
3786
  return this.executeWaitingOperation(spec, scope, operationDeadline, operation, nudge);
3569
3787
  }
3570
3788
 
@@ -4183,10 +4401,27 @@ function checkedWhen(when: unknown): string | Date {
4183
4401
  throw new Error('sleep is for a duration, "2h", or until a Date');
4184
4402
  }
4185
4403
 
4186
- function checkedLabel(label: unknown): string | undefined {
4187
- if (label !== undefined && typeof label !== "string")
4188
- throw new Error("a wait's label is a string");
4189
- return label;
4404
+ /** Receive's options as given, checked: what a typed caller can't get wrong, a plain one can. */
4405
+ function receiveLabel(options: unknown): string | undefined {
4406
+ if (options === undefined) return undefined;
4407
+ if (typeof options !== "object" || options === null) throw new Error("receive takes { label? }");
4408
+ // `take: "all"` was the default, so only "oldest" asked for what receive no longer does.
4409
+ const { take } = options as { take?: unknown };
4410
+ if (take !== undefined && take !== "all")
4411
+ throw new Error(
4412
+ "receive takes every event not yet acknowledged; wf.select takes one at a time",
4413
+ );
4414
+ if ("until" in options)
4415
+ throw new Error("receive has no until: a timer, wf.timer(name, when), wakes it");
4416
+ return labelOf((options as WaitOptions).label);
4417
+ }
4418
+
4419
+ /** A wait's label, a function's asked now. */
4420
+ function labelOf(label: unknown): string | undefined {
4421
+ const given = typeof label === "function" ? label() : label;
4422
+ if (given !== undefined && typeof given !== "string")
4423
+ throw new Error("a wait's label is a string, or a function that returns one");
4424
+ return given;
4190
4425
  }
4191
4426
 
4192
4427
  /** A wait's branches, by name, checked: a race may give a duration where a select may not. */
@@ -4229,16 +4464,32 @@ function sourceTake(what: string, branch: string, source: unknown): WaitTake {
4229
4464
  throw new Error(
4230
4465
  `${what}'s ${branch} is wf.at(), a select's: a race answers once, so it takes a timer, wf.timer(name, date)`,
4231
4466
  );
4232
- throw new Error(
4233
- `${what}'s ${branch} is not a source: a timer, a signal, a participant routed to the code, an agent, or a started turn`,
4234
- );
4467
+ throw new Error(`${what}'s ${branch} is not a source: a timer, a signal, or an agent`);
4235
4468
  }
4236
4469
 
4470
+ /** The sources only a run of a workflow with events hears. */
4471
+ const HEARD_WITH_EVENTS: ReadonlySet<string> = new Set(["agent:ended", "participant:reply"]);
4472
+
4237
4473
  /** One of the workflow's own events, by name, as a source. */
4238
- function signalSource(name: string): unknown {
4239
- if (typeof name !== "string" || name.includes(":"))
4240
- throw new Error(`a signal is one of the workflow's own events, by name; not ${String(name)}`);
4241
- return source({ name }, { name });
4474
+ function signalSource(name: string, declared: ReadonlySet<string>): unknown {
4475
+ if (typeof name !== "string")
4476
+ throw new Error(
4477
+ `a signal is one of the workflow's declared events, by name; not ${String(name)}`,
4478
+ );
4479
+ if (!declared.has(name))
4480
+ throw new Error(
4481
+ declared.size === 0
4482
+ ? `signal ${name}: the workflow declares no events`
4483
+ : `signal ${name}: the workflow declares no such event; its events are ${[...declared].join(", ")}`,
4484
+ );
4485
+ const awf = awfEventOf(name);
4486
+ if (awf?.kind === "ended")
4487
+ throw new Error(
4488
+ `signal ${name}: an agent's end is waited on by naming the agent, { done: ${awf.key} }, not a signal`,
4489
+ );
4490
+ // The mailbox keeps a reply under its record name, from the participant's key.
4491
+ const take = awf ? { name: "participant:reply", from: awf.key } : { name };
4492
+ return signalOf({ name }, take);
4242
4493
  }
4243
4494
 
4244
4495
  /** Timer `name`'s source. */
@@ -4254,11 +4505,14 @@ function turnRef(
4254
4505
  agent: string,
4255
4506
  id: string,
4256
4507
  cancel: (reason?: string) => Promise<boolean>,
4508
+ deprecated: (name: Deprecated) => void,
4257
4509
  ): StartedTurn {
4258
- return source(
4259
- { id, cancel },
4260
- { name: "agent:ended", from: agent, turn: id },
4261
- ) as unknown as 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;
4262
4516
  }
4263
4517
 
4264
4518
  class ExecutionScope {
@@ -4529,6 +4783,7 @@ type OperationLog = {
4529
4783
 
4530
4784
  /** A native fork, and the parent's operation it was copied after: null when it ran none. */
4531
4785
  type TakenFork = { native: NativeFork; after: string | null };
4786
+ type TakeFork = (deadline: AbsoluteDeadline | undefined, into?: SessionCopy) => Promise<TakenFork>;
4532
4787
 
4533
4788
  /** A settled turn or compaction, kept in the run's `turns.jsonl`. */
4534
4789
  type RecordTurn = (outcome: TurnOutcome<JsonValue>, kind: TurnRecord["kind"]) => void;
@@ -4693,46 +4948,101 @@ function withKnownEffort(execution: AgentExecution): AgentExecution {
4693
4948
  /** What `set` changes. */
4694
4949
  type SettingsChange = Partial<Pick<AgentExecution, "model" | "effort">>;
4695
4950
 
4696
- const SETTINGS_FIELDS = new Set(["id", "model", "effort", "deadline", "timeoutMs"]);
4951
+ const SETTINGS_FIELDS = new Set(["id", "model", "effort", "timeout", "deadline", "timeoutMs"]);
4697
4952
 
4698
4953
  function sessionSettings({ model, effort }: AgentExecution): SessionSettings {
4699
4954
  return { model, ...(effort === undefined ? {} : { effort }) };
4700
4955
  }
4701
4956
 
4702
4957
  /**
4703
- * A pane is the default and is left unsaid, and `metered` is kept only where it means something,
4958
+ * A TUI is the default and is left unsaid, and `metered` is kept only where it means something,
4704
4959
  * so the same agent always has one identity.
4705
4960
  */
4706
4961
  function storedPlacement(choice: PlacementChoice): PlacementChoice {
4707
- // A workflow is untyped JavaScript at run time; a misspelt placement must not become a pane.
4962
+ knownPlacement(choice);
4963
+ return placementOf(choice) === "headless"
4964
+ ? { placement: "headless", ...(choice.metered === true ? { metered: true as const } : {}) }
4965
+ : {};
4966
+ }
4967
+
4968
+ /** A workflow is untyped JavaScript at run time; a misspelt placement must not become a TUI. */
4969
+ function knownPlacement(choice: PlacementChoice): void {
4708
4970
  if (
4709
4971
  choice.placement !== undefined &&
4710
- choice.placement !== "pane" &&
4972
+ choice.placement !== "tui" &&
4711
4973
  choice.placement !== "headless"
4712
4974
  ) {
4975
+ const renamed = (choice.placement as string) === "pane" ? ` ("pane" is now "tui")` : "";
4713
4976
  throw new Error(
4714
- `unknown placement ${JSON.stringify(choice.placement)}; expected pane or headless`,
4977
+ `unknown placement ${JSON.stringify(choice.placement)}; expected tui or headless${renamed}`,
4715
4978
  );
4716
4979
  }
4717
- return placementOf(choice) === "headless"
4718
- ? { placement: "headless", ...(choice.metered === true ? { metered: true as const } : {}) }
4719
- : {};
4720
4980
  }
4721
4981
 
4722
4982
  /**
4723
4983
  * The parent's harness, model and effort, in the placement and at the effort the fork names, else
4724
4984
  * its parent's.
4725
4985
  */
4726
- function forkExecution(parent: AgentExecution, spec: AgentForkSpec): AgentExecution {
4986
+ function forkExecution(parent: AgentExecution, runtime: ForkRuntime = {}): AgentExecution {
4727
4987
  const { placement: _placement, metered: _metered, ...target } = parent;
4728
- const choice = spec.placement === undefined && spec.metered === undefined ? parent : spec;
4988
+ const choice =
4989
+ runtime.placement === undefined && runtime.metered === undefined ? parent : runtime;
4729
4990
  return {
4730
4991
  ...target,
4731
- ...(spec.effort === undefined ? {} : { effort: spec.effort }),
4992
+ ...(runtime.effort === undefined ? {} : { effort: runtime.effort }),
4732
4993
  ...storedPlacement(choice),
4733
4994
  };
4734
4995
  }
4735
4996
 
4997
+ const FORK_RUNTIME_FIELDS = ["placement", "metered", "effort"] as const;
4998
+
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
+ }
5015
+ if (runtime === undefined) return rest;
5016
+ if (typeof runtime !== "object" || runtime === null || Array.isArray(runtime)) {
5017
+ throw new Error(
5018
+ `agent ${spec.key}: a fork's runtime is { placement?, metered?, effort? }, not ${JSON.stringify(runtime)}`,
5019
+ );
5020
+ }
5021
+ // A workflow is untyped JavaScript at run time; a harness or model asked for must not be dropped.
5022
+ const others = Object.keys(runtime).filter(
5023
+ (field) =>
5024
+ !(FORK_RUNTIME_FIELDS as readonly string[]).includes(field) &&
5025
+ (runtime as Record<string, unknown>)[field] !== undefined,
5026
+ );
5027
+ if (others.length > 0) {
5028
+ throw new Error(
5029
+ `agent ${spec.key}: a fork continues its parent's session on its harness and model; its runtime takes placement, metered and effort, not ${others.join(", ")}`,
5030
+ );
5031
+ }
5032
+ const given = definedFields({
5033
+ placement: runtime.placement,
5034
+ metered: runtime.metered,
5035
+ effort: runtime.effort,
5036
+ });
5037
+ return Object.keys(given).length === 0 ? rest : { ...rest, runtime: given };
5038
+ }
5039
+
5040
+ function definedFields<T extends object>(fields: T): Partial<T> {
5041
+ return Object.fromEntries(
5042
+ Object.entries(fields).filter(([, value]) => value !== undefined),
5043
+ ) as Partial<T>;
5044
+ }
5045
+
4736
5046
  function constrainExistingExecution(
4737
5047
  selection: RuntimeSelection,
4738
5048
  existing: AgentExecution,
@@ -4757,6 +5067,7 @@ function constrainExistingExecution(
4757
5067
  if (selection.effort !== undefined && selection.effort !== existing.effort) {
4758
5068
  throw new Error("existing agent was opened at a different effort");
4759
5069
  }
5070
+ knownPlacement(selection);
4760
5071
  const placed = placementOf(existing);
4761
5072
  if (
4762
5073
  (selection.placement !== undefined && placementOf(selection) !== placed) ||
@@ -4792,23 +5103,13 @@ function assertCompatibleAgent(
4792
5103
  if (spec.skills !== undefined && !isDeepStrictEqual(existing.skills, requested.skills)) {
4793
5104
  conflict(key, "skills");
4794
5105
  }
4795
- for (const field of ["instructions", "labels", "layout", "keepPane"] as const) {
5106
+ for (const field of ["instructions", "labels", "pane", "layout", "keepPane"] as const) {
4796
5107
  if (spec[field] !== undefined && !isDeepStrictEqual(existing[field], requested[field])) {
4797
5108
  conflict(key, field);
4798
5109
  }
4799
5110
  }
4800
5111
  }
4801
5112
 
4802
- function paneOptions({
4803
- layout,
4804
- keepPane,
4805
- }: AgentIdentity): Pick<HarnessActivation, "layout" | "keepPane"> {
4806
- return {
4807
- ...(layout === undefined ? {} : { layout }),
4808
- ...(keepPane === undefined ? {} : { keepPane }),
4809
- };
4810
- }
4811
-
4812
5113
  function conflict(key: string, field: string): never {
4813
5114
  throw new Error(`agent ${key} is already open with different ${field}`);
4814
5115
  }
@@ -4817,10 +5118,6 @@ function stoppedError(key: string, reason: string): Error {
4817
5118
  return new Error(`agent ${key} was stopped (${reason}); its key is not opened again`);
4818
5119
  }
4819
5120
 
4820
- function unavailable(name: string): Promise<never> {
4821
- return Promise.reject(new Error(`${name} is not implemented by the minimum workflow runner`));
4822
- }
4823
-
4824
5121
  /**
4825
5122
  * Each root agent's last native session in a run's turns, where its harness can resume it: what a
4826
5123
  * event run's continue reopens it on.
@@ -4843,4 +5140,25 @@ type LogicalAgentTurns = {
4843
5140
  begin(turn: TurnId, meta: JsonValue, execution: AgentExecution): Promise<void>;
4844
5141
  end(turn: TurnId, meta: JsonValue, outcome: TurnOutcome<JsonValue>): Promise<void>;
4845
5142
  restored(): RestoredTurn | undefined;
5143
+ /** Throws, or logs the deprecation, unless the workflow declares this agent's `{key}:ended`. */
5144
+ checkDeclared(): void;
4846
5145
  };
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
+ }