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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/package.json +5 -6
  2. package/src/agent-cli/cli.ts +611 -0
  3. package/src/agent-cli/client.ts +115 -0
  4. package/src/agent-launcher.ts +15 -25
  5. package/src/allowance-command.ts +3 -15
  6. package/src/attempt-ending.ts +2 -4
  7. package/src/attempt-output.ts +3 -2
  8. package/src/attempt-view.ts +1 -1
  9. package/src/control-plane.ts +38 -6
  10. package/src/coordinator-socket.ts +15 -6
  11. package/src/deadlines.ts +69 -2
  12. package/src/decisions/directory.ts +9 -11
  13. package/src/deprecations.ts +177 -0
  14. package/src/duration.ts +50 -20
  15. package/src/emit-command.ts +57 -54
  16. package/src/emit.ts +124 -15
  17. package/src/help/allowance.txt +13 -0
  18. package/src/help/emit.txt +21 -0
  19. package/src/help/inbox.txt +12 -0
  20. package/src/help/init.txt +12 -0
  21. package/src/help/logs.txt +14 -0
  22. package/src/help/reply.txt +18 -0
  23. package/src/help/run.txt +58 -0
  24. package/src/help/runs.txt +11 -0
  25. package/src/help/test.txt +9 -0
  26. package/src/help/tick.txt +12 -0
  27. package/src/herdr-run-session.ts +2 -2
  28. package/src/here.ts +1 -1
  29. package/src/holder-command.ts +38 -51
  30. package/src/inbox-command.ts +9 -24
  31. package/src/init-command.ts +2 -11
  32. package/src/init-template/AGENTS.md.template +3 -3
  33. package/src/init-template/hello/workflow.test.ts.template +3 -3
  34. package/src/init-template/hello/workflow.ts.template +6 -6
  35. package/src/keep-alive.ts +28 -13
  36. package/src/logs-command.ts +14 -27
  37. package/src/mailbox.ts +16 -21
  38. package/src/messaging/limits.ts +5 -5
  39. package/src/messaging/messaging.ts +48 -43
  40. package/src/messaging/routes.ts +2 -2
  41. package/src/messaging/text.ts +24 -22
  42. package/src/operation-liveness.ts +1 -1
  43. package/src/operator-cli.ts +23 -9
  44. package/src/operator-runtime.ts +9 -12
  45. package/src/pane-layout.ts +125 -32
  46. package/src/progress-view.ts +8 -8
  47. package/src/result-validation.ts +1 -1
  48. package/src/run-command.ts +27 -47
  49. package/src/run-commands.ts +1 -1
  50. package/src/run-prepare.ts +46 -8
  51. package/src/run-usage.ts +2 -2
  52. package/src/run-view.ts +9 -4
  53. package/src/runs-command.ts +14 -23
  54. package/src/runs.ts +44 -8
  55. package/src/sandboxes.ts +3 -2
  56. package/src/simple-input.ts +13 -3
  57. package/src/sources.ts +42 -6
  58. package/src/stage-ledger.ts +43 -13
  59. package/src/stage-plan.ts +37 -1
  60. package/src/test-command.ts +9 -14
  61. package/src/testing.ts +8 -9
  62. package/src/tick-command.ts +2 -13
  63. package/src/tools.ts +62 -25
  64. package/src/workflow-loader.ts +5 -2
  65. package/src/workflow-runner.ts +636 -271
  66. package/src/workflow-testing/coordinator.typecheck.ts +12 -13
  67. package/src/workflow-testing/coordinators.ts +2 -2
  68. package/src/workflow-testing/host.ts +115 -46
  69. package/src/workflow-testing/index.ts +45 -837
  70. package/src/workflow-testing/run-test.ts +1213 -0
  71. package/src/workflow-testing/script.ts +106 -45
  72. package/src/workflow-testing/workflow-testing.typecheck.ts +13 -10
@@ -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,
@@ -189,22 +208,39 @@ import {
189
208
  type RunSandboxOptions,
190
209
  type SeatedAgent,
191
210
  } from "./sandboxes";
192
- import { QUOTING_RULE, simpleInput, usageLine } from "./simple-input";
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
- /** The run's time, which its timers and `workflow.now()` read; absent, the clock's. A test's is virtual. */
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;
209
245
  runRoot: string;
210
246
  /** The run this is an attempt of, whose folder the operator claimed under `runRoot`. */
@@ -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. */
@@ -253,7 +291,7 @@ export type RunWorkflowOptions = {
253
291
  skillCache?: string;
254
292
  /** The decision models workflows may ask; without them, `decisions.decide` is refused. */
255
293
  decisions?: DecisionInstallation;
256
- /** How `workflow.allowance` reads a harness's plan; without it, every harness reads `none`. */
294
+ /** How `wf.allowance` reads a harness's plan; without it, every harness reads `none`. */
257
295
  allowance?: (harness: Harness) => Promise<HarnessAllowance>;
258
296
  /**
259
297
  * Where an agent may `--await-reply`: the engine's list by harness and placement unless given. A
@@ -267,7 +305,7 @@ export type RunWorkflowOptions = {
267
305
  */
268
306
  coordinators?: readonly CoordinatorPort[];
269
307
  /**
270
- * Sends `workflow.emit`'s event through the one way in; absent, `emitEvent` on this machine. A
308
+ * Sends `wf.emit`'s event through the one way in; absent, `emitEvent` on this machine. A
271
309
  * test records them instead.
272
310
  */
273
311
  emit?: Emitter;
@@ -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();
@@ -891,7 +955,7 @@ class WorkflowOwner {
891
955
  readonly #inFlight = new Set<Promise<unknown>>();
892
956
  #closed = false;
893
957
  #closing: Promise<unknown[]> | undefined;
894
- /** The bundled `wf`'s source, built once, at the first sandboxed agent. */
958
+ /** The bundled `wfa`'s source, built once, at the first sandboxed agent. */
895
959
  #bundle: Promise<string> | undefined;
896
960
  /** The calling session, once the workflow has asked for it (ADR 0010). */
897
961
  #caller: { key: string; state: Promise<LogicalAgent> } | undefined;
@@ -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 ?? []);
@@ -957,7 +1026,7 @@ class WorkflowOwner {
957
1026
  if (!agent || agent.hasStartedTurn) return false;
958
1027
  return options.awaitReply(agent.execution, agent.confirmsDelivery);
959
1028
  },
960
- launcher: (key) => this.#logical.get(key)?.launcher ?? "wf",
1029
+ launcher: (key) => this.#logical.get(key)?.launcher ?? "wfa",
961
1030
  changed: (key) => {
962
1031
  this.#messagesChanged();
963
1032
  this.#logical.get(key)?.messagesChanged();
@@ -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)
@@ -1142,7 +1205,7 @@ class WorkflowOwner {
1142
1205
  },
1143
1206
  log: (message, fields) => options.log(message, fields),
1144
1207
  now: () => {
1145
- this.#notReading("workflow.now()");
1208
+ this.#notReading("wf.now()");
1146
1209
  return new Date(options.now());
1147
1210
  },
1148
1211
  sleep: async (name, when) => {
@@ -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("workflow.after()");
1172
- if (typeof duration !== "string" || !/^\d+(?:\.\d+)?[dhm]$/.test(duration))
1173
- throw new Error(
1174
- `workflow.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. */
@@ -1189,7 +1248,7 @@ class WorkflowOwner {
1189
1248
  #notReading(what: string): void {
1190
1249
  if (this.#reading)
1191
1250
  throw new Error(
1192
- `${what} in workflow.at's read: a time made at the read moves with every wait and never comes due; keep the time in the run's state where it is set`,
1251
+ `${what} in wf.at's read: a time made at the read moves with every wait and never comes due; keep the time in the run's state where it is set`,
1193
1252
  );
1194
1253
  }
1195
1254
 
@@ -1205,7 +1264,7 @@ class WorkflowOwner {
1205
1264
  if (value === null) return undefined;
1206
1265
  if (!(value instanceof Date) || !Number.isFinite(value.getTime()))
1207
1266
  throw new Error(
1208
- `select's ${branch}: workflow.at reads a valid Date or null from the run's state, not ${
1267
+ `select's ${branch}: wf.at reads a valid Date or null from the run's state, not ${
1209
1268
  value instanceof Date ? "an invalid Date" : (JSON.stringify(value) ?? String(value))
1210
1269
  }`,
1211
1270
  );
@@ -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,
@@ -2988,6 +3192,7 @@ class LogicalAgent implements AgentRef {
2988
3192
  .then(
2989
3193
  ({ outcome }) => outcome,
2990
3194
  (error: unknown): TurnOutcome<JsonValue> => ({
3195
+ answered: false,
2991
3196
  kind: "failed",
2992
3197
  reason: messageOf(error),
2993
3198
  retryable: false,
@@ -3022,11 +3227,12 @@ class LogicalAgent implements AgentRef {
3022
3227
  );
3023
3228
  }
3024
3229
 
3025
- is<Meta extends JsonValue = JsonValue, T extends JsonValue = JsonValue>(event: {
3026
- readonly name: string;
3027
- readonly from: string;
3028
- }): event is AgentEndedEvent<Meta, T> & Matched {
3029
- 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 });
3030
3236
  }
3031
3237
 
3032
3238
  run(spec: AgentRunTextSpec): Promise<RunResult<string>>;
@@ -3045,6 +3251,7 @@ class LogicalAgent implements AgentRef {
3045
3251
  return Promise.reject(error);
3046
3252
  }
3047
3253
  const opensWindow = !inTimeoutHandler();
3254
+ if (spec.id !== undefined && !this.#launching) this.options.deprecated("call-id");
3048
3255
  const id = spec.id ?? randomUUID();
3049
3256
  const completeSpec = structuredClone({ ...spec, id }) as
3050
3257
  | AgentRunTextSpec
@@ -3063,11 +3270,13 @@ class LogicalAgent implements AgentRef {
3063
3270
  }
3064
3271
  let deadline: AbsoluteDeadline | undefined;
3065
3272
  try {
3066
- deadline = this.operationDeadline(spec, scope);
3273
+ deadline = this.operationDeadline(spec, scope, this.#launching ? "on start" : "on run");
3067
3274
  const recoveryDeadline = spec.nudge ? spec.nudge.deadline : undefined;
3068
3275
  if (recoveryDeadline) {
3069
- assertDeadlineValue(recoveryDeadline);
3070
- deadline = earlierDeadline(deadline, recoveryDeadline);
3276
+ this.options.deprecated("nudge-deadline");
3277
+ const own = { unixMilliseconds: recoveryDeadline.unixMilliseconds };
3278
+ assertDeadlineValue(own);
3279
+ deadline = earlierDeadline(deadline, own);
3071
3280
  }
3072
3281
  } catch (error) {
3073
3282
  const rejected = Promise.reject<RunResult<string | T>>(error);
@@ -3106,7 +3315,12 @@ class LogicalAgent implements AgentRef {
3106
3315
  await this.options.afterOperation?.();
3107
3316
  }
3108
3317
  });
3109
- 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
+ );
3110
3324
  scope?.track(tracked);
3111
3325
  this.#operations.set(id, { spec: completeSpec, result: tracked });
3112
3326
  return tracked as Promise<RunResult<string | T>>;
@@ -3123,6 +3337,7 @@ class LogicalAgent implements AgentRef {
3123
3337
  } catch (error) {
3124
3338
  return Promise.reject(error);
3125
3339
  }
3340
+ if (spec.id !== undefined) this.options.deprecated("call-id");
3126
3341
  const id = spec.id ?? randomUUID();
3127
3342
  const completeSpec = structuredClone({ ...spec, id });
3128
3343
  const existing = this.#compactions.get(id);
@@ -3139,7 +3354,7 @@ class LogicalAgent implements AgentRef {
3139
3354
  }
3140
3355
  let deadline: AbsoluteDeadline;
3141
3356
  try {
3142
- deadline = harnessDeadline(this.operationDeadline(spec, scope));
3357
+ deadline = harnessDeadline(this.operationDeadline(spec, scope, "on compact"));
3143
3358
  } catch (error) {
3144
3359
  const rejected = Promise.reject<TurnOutcome<string>>(error);
3145
3360
  scope?.track(rejected);
@@ -3165,7 +3380,7 @@ class LogicalAgent implements AgentRef {
3165
3380
  await this.options.afterOperation?.();
3166
3381
  }
3167
3382
  });
3168
- const tracked = this.options.track(result);
3383
+ const tracked = this.options.track(result.then(this.options.reporting));
3169
3384
  scope?.track(tracked);
3170
3385
  this.#compactions.set(id, { spec: completeSpec, result: tracked });
3171
3386
  return tracked;
@@ -3182,6 +3397,7 @@ class LogicalAgent implements AgentRef {
3182
3397
  } catch (error) {
3183
3398
  return Promise.reject(error);
3184
3399
  }
3400
+ if (spec.id !== undefined) this.options.deprecated("call-id");
3185
3401
  const id = spec.id ?? randomUUID();
3186
3402
  const completeSpec = structuredClone({ ...spec, id });
3187
3403
  const existing = this.#sets.get(id);
@@ -3200,7 +3416,7 @@ class LogicalAgent implements AgentRef {
3200
3416
  let deadline: AbsoluteDeadline;
3201
3417
  try {
3202
3418
  change = this.settingsChange(spec);
3203
- deadline = harnessDeadline(this.operationDeadline(spec, scope));
3419
+ deadline = harnessDeadline(this.operationDeadline(spec, scope, "on set"));
3204
3420
  } catch (error) {
3205
3421
  const rejected = Promise.reject<TurnOutcome<null>>(error);
3206
3422
  scope?.track(rejected);
@@ -3220,7 +3436,7 @@ class LogicalAgent implements AgentRef {
3220
3436
  this.#pendingSets.splice(this.#pendingSets.indexOf(change), 1);
3221
3437
  }
3222
3438
  });
3223
- const tracked = this.options.track(result);
3439
+ const tracked = this.options.track(result.then(this.options.reporting));
3224
3440
  scope?.track(tracked);
3225
3441
  this.#sets.set(id, { spec: completeSpec, result: tracked });
3226
3442
  return tracked;
@@ -3305,7 +3521,6 @@ class LogicalAgent implements AgentRef {
3305
3521
  const refused = this.#refusedWhileStarted("fork");
3306
3522
  if (refused) return Promise.reject(refused);
3307
3523
  const scope = scopes.getStore();
3308
- const deadline = this.deadlineCeiling(scope);
3309
3524
  const { session, execution } = this.options;
3310
3525
  const settings = this.queuedExecution();
3311
3526
  const fork = session.fork?.bind(session);
@@ -3324,7 +3539,7 @@ class LogicalAgent implements AgentRef {
3324
3539
  }
3325
3540
  // The harness bounds its fork by this deadline and stops it there, so the session is idle again
3326
3541
  // when the fork rejects.
3327
- const take = (into?: SessionCopy) =>
3542
+ const take: TakeFork = (deadline, into) =>
3328
3543
  this.queue(async () => {
3329
3544
  if (this.#closed || this.options.isRunClosing()) {
3330
3545
  throw new Error("logical agent is closed");
@@ -3347,19 +3562,19 @@ class LogicalAgent implements AgentRef {
3347
3562
  }
3348
3563
  }
3349
3564
 
3350
- /** `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. */
3351
3566
  private operationDeadline(
3352
- spec: { deadline?: AbsoluteDeadline; timeoutMs?: number },
3567
+ spec: GivenBound,
3353
3568
  scope: ExecutionScope | undefined,
3569
+ call: string,
3354
3570
  ): AbsoluteDeadline | undefined {
3355
- if (spec.deadline && spec.timeoutMs !== undefined) {
3356
- throw new Error("an operation cannot specify both deadline and timeoutMs");
3357
- }
3358
- const ceiling = this.deadlineCeiling(scope);
3359
- if (spec.timeoutMs !== undefined) return deadlineWithin(spec.timeoutMs, ceiling);
3360
- if (!spec.deadline) return ceiling;
3361
- assertDeadlineValue(spec.deadline);
3362
- 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
+ );
3363
3578
  }
3364
3579
 
3365
3580
  /** The deadlines around an operation: its scope's and the run's, if either has one. */
@@ -3423,10 +3638,15 @@ class LogicalAgent implements AgentRef {
3423
3638
  const usage = () =>
3424
3639
  entry.settle({ settledAt: Math.min(Date.now(), deadline.unixMilliseconds) }, []);
3425
3640
  if (Date.now() >= deadline.unixMilliseconds) {
3426
- return { kind: "timed-out", reason: "settings deadline exceeded", usage: usage() };
3641
+ return {
3642
+ answered: false,
3643
+ kind: "timed-out",
3644
+ reason: "settings deadline exceeded",
3645
+ usage: usage(),
3646
+ };
3427
3647
  }
3428
3648
  const next: AgentExecution = { ...this.#execution, ...change };
3429
- const pane = placementOf(next) === "pane";
3649
+ const pane = placementOf(next) === "tui";
3430
3650
  let cancelled: string | undefined;
3431
3651
  // A pane's switch waits on its screen; a scope cancelled meanwhile closes the agent, as a
3432
3652
  // half-made switch leaves it at settings nobody knows. A headless one takes no time.
@@ -3434,7 +3654,7 @@ class LogicalAgent implements AgentRef {
3434
3654
  ? scope?.add(async (stop) => {
3435
3655
  cancelled = stop.reason;
3436
3656
  this.#last = "cancelled";
3437
- await this.close(stop.reason);
3657
+ await this.terminate(stop.reason);
3438
3658
  })
3439
3659
  : undefined;
3440
3660
  try {
@@ -3451,15 +3671,16 @@ class LogicalAgent implements AgentRef {
3451
3671
  cancelled !== undefined ? "cancelled" : expired ? "timed-out" : "failed",
3452
3672
  );
3453
3673
  }
3454
- if (cancelled !== undefined) return { kind: "cancelled", reason, usage: usage() };
3674
+ if (cancelled !== undefined)
3675
+ return { answered: false, kind: "cancelled", reason, usage: usage() };
3455
3676
  return expired
3456
- ? { kind: "timed-out", reason, usage: usage() }
3457
- : { kind: "failed", reason, retryable: false, usage: usage() };
3677
+ ? { answered: false, kind: "timed-out", reason, usage: usage() }
3678
+ : { answered: false, kind: "failed", reason, retryable: false, usage: usage() };
3458
3679
  } finally {
3459
3680
  removeCanceller?.();
3460
3681
  }
3461
3682
  this.#execution = next;
3462
- return { kind: "answered", value: null, usage: usage() };
3683
+ return { answered: true, kind: "answered", value: null, usage: usage() };
3463
3684
  }
3464
3685
 
3465
3686
  /**
@@ -3470,10 +3691,15 @@ class LogicalAgent implements AgentRef {
3470
3691
  if (this.options.execution.caller) return;
3471
3692
  // Before its outcome is recorded: whether its pane stays turns on how this one ended.
3472
3693
  this.#last = ended;
3473
- void this.close(reason).catch(() => undefined);
3694
+ void this.terminate(reason).catch(() => undefined);
3474
3695
  }
3475
3696
 
3476
- close(reason?: string): Promise<void> {
3697
+ close(reason?: string): Promise<boolean> {
3698
+ return this.options.close(reason);
3699
+ }
3700
+
3701
+ /** What `close()`, a failed operation and the run's end each come to. */
3702
+ terminate(reason?: string): Promise<void> {
3477
3703
  const first = !this.#closed;
3478
3704
  this.#closed = true;
3479
3705
  if (first) {
@@ -3557,7 +3783,6 @@ class LogicalAgent implements AgentRef {
3557
3783
  ): Promise<RunResult<JsonValue>> {
3558
3784
  const nudge = spec.nudge === false ? undefined : (spec.nudge ?? this.defaultNudge());
3559
3785
  scope?.assertActive();
3560
- if (nudge?.deadline) assertDeadlineValue(nudge.deadline);
3561
3786
  return this.executeWaitingOperation(spec, scope, operationDeadline, operation, nudge);
3562
3787
  }
3563
3788
 
@@ -3607,7 +3832,7 @@ class LogicalAgent implements AgentRef {
3607
3832
  const recoveryPrompt =
3608
3833
  nudge?.prompt ??
3609
3834
  (waitingSupported
3610
- ? "Report the result now, or use wf waiting with a reason if you still need time."
3835
+ ? "Report the result now, or use wfa waiting with a reason if you still need time."
3611
3836
  : "You finished without reporting the requested result. Report it now.");
3612
3837
  // The first prompt carries only the result command; waiting is rare, so it is offered only
3613
3838
  // once a check-in finds the agent idle without an answer.
@@ -3665,7 +3890,7 @@ class LogicalAgent implements AgentRef {
3665
3890
  toolsLesson(
3666
3891
  this.options.tools.taught(this.key),
3667
3892
  this.options.launcher,
3668
- simpleInput(schema) !== undefined,
3893
+ resultSaysQuoting(schema),
3669
3894
  ),
3670
3895
  ]
3671
3896
  .filter(Boolean)
@@ -3733,14 +3958,23 @@ class LogicalAgent implements AgentRef {
3733
3958
  outcome.charges,
3734
3959
  );
3735
3960
  if (outcome.kind === "answered")
3736
- return { outcome: { kind: "answered", value: outcome.value as JsonValue, usage } };
3961
+ return {
3962
+ outcome: { answered: true, kind: "answered", value: outcome.value as JsonValue, usage },
3963
+ };
3737
3964
  if (outcome.kind === "failed") {
3738
3965
  const login = outcome.login ? { login: outcome.login } : {};
3739
3966
  return {
3740
- outcome: { kind: "failed", reason: outcome.reason, retryable: false, ...login, usage },
3967
+ outcome: {
3968
+ answered: false,
3969
+ kind: "failed",
3970
+ reason: outcome.reason,
3971
+ retryable: false,
3972
+ ...login,
3973
+ usage,
3974
+ },
3741
3975
  };
3742
3976
  }
3743
- return { outcome: { kind: outcome.kind, reason: outcome.reason, usage } };
3977
+ return { outcome: { answered: false, kind: outcome.kind, reason: outcome.reason, usage } };
3744
3978
  }
3745
3979
 
3746
3980
  /**
@@ -4036,10 +4270,10 @@ class LogicalAgent implements AgentRef {
4036
4270
  // Recorded `answered` with no value: it did what it was woken for, and takes no result.
4037
4271
  this.turnEnded(
4038
4272
  outcome.kind === "answered"
4039
- ? { kind: "answered", value: null, usage }
4273
+ ? { answered: true, kind: "answered", value: null, usage }
4040
4274
  : outcome.kind === "failed"
4041
- ? { kind: "failed", reason: outcome.reason, retryable: false, usage }
4042
- : { kind: outcome.kind, reason: outcome.reason, usage },
4275
+ ? { answered: false, kind: "failed", reason: outcome.reason, retryable: false, usage }
4276
+ : { answered: false, kind: outcome.kind, reason: outcome.reason, usage },
4043
4277
  "turn",
4044
4278
  true,
4045
4279
  );
@@ -4074,17 +4308,23 @@ class LogicalAgent implements AgentRef {
4074
4308
  later,
4075
4309
  );
4076
4310
  if (native === "expired") {
4077
- return { kind: "timed-out", reason: "compaction deadline exceeded", usage };
4311
+ return {
4312
+ answered: false,
4313
+ kind: "timed-out",
4314
+ reason: "compaction deadline exceeded",
4315
+ usage,
4316
+ };
4078
4317
  }
4079
4318
  if (native.state === "completed") {
4080
4319
  return native.summary === undefined
4081
4320
  ? {
4321
+ answered: false,
4082
4322
  kind: "failed",
4083
4323
  reason: native.detail ?? "the harness did not confirm a compaction",
4084
4324
  retryable: false,
4085
4325
  usage,
4086
4326
  }
4087
- : { kind: "answered", value: native.summary, usage };
4327
+ : { answered: true, kind: "answered", value: native.summary, usage };
4088
4328
  }
4089
4329
  return reconcile<string>(native, usage);
4090
4330
  };
@@ -4156,15 +4396,32 @@ function checkedWhen(when: unknown): string | Date {
4156
4396
  if (when instanceof Date || typeof when === "string") return when;
4157
4397
  if (readOf(when))
4158
4398
  throw new Error(
4159
- "sleep is for a duration or a Date; workflow.at() is a select's branch, read at each of its waits",
4399
+ "sleep is for a duration or a Date; wf.at() is a select's branch, read at each of its waits",
4160
4400
  );
4161
4401
  throw new Error('sleep is for a duration, "2h", or until a Date');
4162
4402
  }
4163
4403
 
4164
- function checkedLabel(label: unknown): string | undefined {
4165
- if (label !== undefined && typeof label !== "string")
4166
- throw new Error("a wait's label is a string");
4167
- 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;
4168
4425
  }
4169
4426
 
4170
4427
  /** A wait's branches, by name, checked: a race may give a duration where a select may not. */
@@ -4176,18 +4433,18 @@ function branchesOf(what: string, branches: unknown, durations: boolean): [strin
4176
4433
  if ("key" in branches) throw new Error(`${what}: no branch is named key, which the answer uses`);
4177
4434
  for (const [branch, source] of entries)
4178
4435
  if (typeof source === "string" && !durations)
4179
- throw new Error(`${what}'s ${branch} is a duration: a timer in a loop is workflow.timer()'s`);
4436
+ throw new Error(`${what}'s ${branch} is a duration: a timer in a loop is wf.timer()'s`);
4180
4437
  return entries;
4181
4438
  }
4182
4439
 
4183
4440
  /** What a time read from state reads, by its source. */
4184
4441
  const timeReads = new WeakMap<object, () => Date | null>();
4185
4442
 
4186
- /** `workflow.at(read)`: a select's branch, due at the time `read` returns. */
4443
+ /** `wf.at(read)`: a select's branch, due at the time `read` returns. */
4187
4444
  function timeRead(read: () => Date | null): TimeRead {
4188
4445
  if (typeof read !== "function")
4189
4446
  throw new Error(
4190
- "workflow.at takes a function that reads a Date or null from the run's state, as () => ledger.nextNudge()",
4447
+ "wf.at takes a function that reads a Date or null from the run's state, as () => ledger.nextNudge()",
4191
4448
  );
4192
4449
  const value = Object.freeze({});
4193
4450
  timeReads.set(value, read);
@@ -4205,18 +4462,34 @@ function sourceTake(what: string, branch: string, source: unknown): WaitTake {
4205
4462
  if (take) return take;
4206
4463
  if (readOf(source))
4207
4464
  throw new Error(
4208
- `${what}'s ${branch} is workflow.at(), a select's: a race answers once, so it takes a timer, workflow.timer(name, date)`,
4465
+ `${what}'s ${branch} is wf.at(), a select's: a race answers once, so it takes a timer, wf.timer(name, date)`,
4209
4466
  );
4210
- throw new Error(
4211
- `${what}'s ${branch} is not a source: a timer, a signal, a participant routed to the code, an agent, or a started turn`,
4212
- );
4467
+ throw new Error(`${what}'s ${branch} is not a source: a timer, a signal, or an agent`);
4213
4468
  }
4214
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
+
4215
4473
  /** One of the workflow's own events, by name, as a source. */
4216
- function signalSource(name: string): unknown {
4217
- if (typeof name !== "string" || name.includes(":"))
4218
- throw new Error(`a signal is one of the workflow's own events, by name; not ${String(name)}`);
4219
- 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);
4220
4493
  }
4221
4494
 
4222
4495
  /** Timer `name`'s source. */
@@ -4232,11 +4505,14 @@ function turnRef(
4232
4505
  agent: string,
4233
4506
  id: string,
4234
4507
  cancel: (reason?: string) => Promise<boolean>,
4508
+ deprecated: (name: Deprecated) => void,
4235
4509
  ): StartedTurn {
4236
- return source(
4237
- { id, cancel },
4238
- { name: "agent:ended", from: agent, turn: id },
4239
- ) 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;
4240
4516
  }
4241
4517
 
4242
4518
  class ExecutionScope {
@@ -4439,20 +4715,41 @@ function reconcile<T extends JsonValue>(
4439
4715
  ): TurnOutcome<T> {
4440
4716
  switch (native.state) {
4441
4717
  case "completed":
4442
- return { kind: "unanswered", reason: "agent settled without an accepted result", usage };
4718
+ return {
4719
+ answered: false,
4720
+ kind: "unanswered",
4721
+ reason: "agent settled without an accepted result",
4722
+ usage,
4723
+ };
4443
4724
  case "blocked":
4444
- return { kind: "blocked", reason: native.detail ?? "agent is blocked", usage };
4725
+ return {
4726
+ answered: false,
4727
+ kind: "blocked",
4728
+ reason: native.detail ?? "agent is blocked",
4729
+ usage,
4730
+ };
4445
4731
  case "timed-out":
4446
- return { kind: "timed-out", reason: native.detail ?? "operation timed out", usage };
4732
+ return {
4733
+ answered: false,
4734
+ kind: "timed-out",
4735
+ reason: native.detail ?? "operation timed out",
4736
+ usage,
4737
+ };
4447
4738
  case "failed":
4448
4739
  return {
4740
+ answered: false,
4449
4741
  kind: "failed",
4450
4742
  reason: native.detail ?? "harness operation failed",
4451
4743
  retryable: false,
4452
4744
  usage,
4453
4745
  };
4454
4746
  case "cancelled":
4455
- return { kind: "cancelled", reason: native.detail ?? "operation cancelled", usage };
4747
+ return {
4748
+ answered: false,
4749
+ kind: "cancelled",
4750
+ reason: native.detail ?? "operation cancelled",
4751
+ usage,
4752
+ };
4456
4753
  }
4457
4754
  }
4458
4755
 
@@ -4486,6 +4783,7 @@ type OperationLog = {
4486
4783
 
4487
4784
  /** A native fork, and the parent's operation it was copied after: null when it ran none. */
4488
4785
  type TakenFork = { native: NativeFork; after: string | null };
4786
+ type TakeFork = (deadline: AbsoluteDeadline | undefined, into?: SessionCopy) => Promise<TakenFork>;
4489
4787
 
4490
4788
  /** A settled turn or compaction, kept in the run's `turns.jsonl`. */
4491
4789
  type RecordTurn = (outcome: TurnOutcome<JsonValue>, kind: TurnRecord["kind"]) => void;
@@ -4566,6 +4864,11 @@ function resultSchema<T extends JsonValue>(schema: OutputSchema<T> | undefined):
4566
4864
  return schema ? parseJsonSchema(schema) : { type: "string" };
4567
4865
  }
4568
4866
 
4867
+ function resultSaysQuoting(schema: JsonSchema): boolean {
4868
+ const simple = simpleInput(schema);
4869
+ return simple !== undefined && takesText(simple);
4870
+ }
4871
+
4569
4872
  function operationPrompt(
4570
4873
  prompt: string,
4571
4874
  schema: JsonSchema,
@@ -4576,7 +4879,7 @@ function operationPrompt(
4576
4879
  // A quoted heredoc passes the JSON through untouched: a quoted argument broke on shell quoting
4577
4880
  // (story 002). Not indented: a copied terminator with leading spaces never closes the heredoc.
4578
4881
  // The schema itself, not a rendering of it: without its bounds, first answers were 0/160 valid
4579
- // (E5). `wf` says what to do about a rejection, so the prompt does not. A simple schema is taught
4882
+ // (E5). `wfa` says what to do about a rejection, so the prompt does not. A simple schema is taught
4580
4883
  // as flags instead, its usage line carrying every bound, which held them as well (story 035, M1).
4581
4884
  const simple = simpleInput(schema);
4582
4885
  if (simple) {
@@ -4586,8 +4889,7 @@ function operationPrompt(
4586
4889
  "When the answer is ready, return it by running:",
4587
4890
  "",
4588
4891
  `${launcher} result ${callId} ${usageLine(simple)}`,
4589
- "",
4590
- QUOTING_RULE,
4892
+ ...(takesText(simple) ? ["", QUOTING_RULE] : []),
4591
4893
  ].join("\n");
4592
4894
  }
4593
4895
  return [
@@ -4646,46 +4948,101 @@ function withKnownEffort(execution: AgentExecution): AgentExecution {
4646
4948
  /** What `set` changes. */
4647
4949
  type SettingsChange = Partial<Pick<AgentExecution, "model" | "effort">>;
4648
4950
 
4649
- const SETTINGS_FIELDS = new Set(["id", "model", "effort", "deadline", "timeoutMs"]);
4951
+ const SETTINGS_FIELDS = new Set(["id", "model", "effort", "timeout", "deadline", "timeoutMs"]);
4650
4952
 
4651
4953
  function sessionSettings({ model, effort }: AgentExecution): SessionSettings {
4652
4954
  return { model, ...(effort === undefined ? {} : { effort }) };
4653
4955
  }
4654
4956
 
4655
4957
  /**
4656
- * 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,
4657
4959
  * so the same agent always has one identity.
4658
4960
  */
4659
4961
  function storedPlacement(choice: PlacementChoice): PlacementChoice {
4660
- // 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 {
4661
4970
  if (
4662
4971
  choice.placement !== undefined &&
4663
- choice.placement !== "pane" &&
4972
+ choice.placement !== "tui" &&
4664
4973
  choice.placement !== "headless"
4665
4974
  ) {
4975
+ const renamed = (choice.placement as string) === "pane" ? ` ("pane" is now "tui")` : "";
4666
4976
  throw new Error(
4667
- `unknown placement ${JSON.stringify(choice.placement)}; expected pane or headless`,
4977
+ `unknown placement ${JSON.stringify(choice.placement)}; expected tui or headless${renamed}`,
4668
4978
  );
4669
4979
  }
4670
- return placementOf(choice) === "headless"
4671
- ? { placement: "headless", ...(choice.metered === true ? { metered: true as const } : {}) }
4672
- : {};
4673
4980
  }
4674
4981
 
4675
4982
  /**
4676
4983
  * The parent's harness, model and effort, in the placement and at the effort the fork names, else
4677
4984
  * its parent's.
4678
4985
  */
4679
- function forkExecution(parent: AgentExecution, spec: AgentForkSpec): AgentExecution {
4986
+ function forkExecution(parent: AgentExecution, runtime: ForkRuntime = {}): AgentExecution {
4680
4987
  const { placement: _placement, metered: _metered, ...target } = parent;
4681
- const choice = spec.placement === undefined && spec.metered === undefined ? parent : spec;
4988
+ const choice =
4989
+ runtime.placement === undefined && runtime.metered === undefined ? parent : runtime;
4682
4990
  return {
4683
4991
  ...target,
4684
- ...(spec.effort === undefined ? {} : { effort: spec.effort }),
4992
+ ...(runtime.effort === undefined ? {} : { effort: runtime.effort }),
4685
4993
  ...storedPlacement(choice),
4686
4994
  };
4687
4995
  }
4688
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
+
4689
5046
  function constrainExistingExecution(
4690
5047
  selection: RuntimeSelection,
4691
5048
  existing: AgentExecution,
@@ -4710,6 +5067,7 @@ function constrainExistingExecution(
4710
5067
  if (selection.effort !== undefined && selection.effort !== existing.effort) {
4711
5068
  throw new Error("existing agent was opened at a different effort");
4712
5069
  }
5070
+ knownPlacement(selection);
4713
5071
  const placed = placementOf(existing);
4714
5072
  if (
4715
5073
  (selection.placement !== undefined && placementOf(selection) !== placed) ||
@@ -4745,23 +5103,13 @@ function assertCompatibleAgent(
4745
5103
  if (spec.skills !== undefined && !isDeepStrictEqual(existing.skills, requested.skills)) {
4746
5104
  conflict(key, "skills");
4747
5105
  }
4748
- for (const field of ["instructions", "labels", "layout", "keepPane"] as const) {
5106
+ for (const field of ["instructions", "labels", "pane", "layout", "keepPane"] as const) {
4749
5107
  if (spec[field] !== undefined && !isDeepStrictEqual(existing[field], requested[field])) {
4750
5108
  conflict(key, field);
4751
5109
  }
4752
5110
  }
4753
5111
  }
4754
5112
 
4755
- function paneOptions({
4756
- layout,
4757
- keepPane,
4758
- }: AgentIdentity): Pick<HarnessActivation, "layout" | "keepPane"> {
4759
- return {
4760
- ...(layout === undefined ? {} : { layout }),
4761
- ...(keepPane === undefined ? {} : { keepPane }),
4762
- };
4763
- }
4764
-
4765
5113
  function conflict(key: string, field: string): never {
4766
5114
  throw new Error(`agent ${key} is already open with different ${field}`);
4767
5115
  }
@@ -4770,10 +5118,6 @@ function stoppedError(key: string, reason: string): Error {
4770
5118
  return new Error(`agent ${key} was stopped (${reason}); its key is not opened again`);
4771
5119
  }
4772
5120
 
4773
- function unavailable(name: string): Promise<never> {
4774
- return Promise.reject(new Error(`${name} is not implemented by the minimum workflow runner`));
4775
- }
4776
-
4777
5121
  /**
4778
5122
  * Each root agent's last native session in a run's turns, where its harness can resume it: what a
4779
5123
  * event run's continue reopens it on.
@@ -4796,4 +5140,25 @@ type LogicalAgentTurns = {
4796
5140
  begin(turn: TurnId, meta: JsonValue, execution: AgentExecution): Promise<void>;
4797
5141
  end(turn: TurnId, meta: JsonValue, outcome: TurnOutcome<JsonValue>): Promise<void>;
4798
5142
  restored(): RestoredTurn | undefined;
5143
+ /** Throws, or logs the deprecation, unless the workflow declares this agent's `{key}:ended`. */
5144
+ checkDeclared(): void;
4799
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
+ }