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

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 (82) hide show
  1. package/package.json +5 -6
  2. package/src/agent-cli/cli.ts +571 -0
  3. package/src/agent-cli/client.ts +115 -0
  4. package/src/agent-launcher.ts +15 -25
  5. package/src/allowance-command.ts +31 -21
  6. package/src/attempt-ending.ts +23 -5
  7. package/src/attempt-output.ts +48 -18
  8. package/src/attempt-view.ts +25 -15
  9. package/src/commands.ts +176 -0
  10. package/src/control-plane.ts +73 -22
  11. package/src/coordinator-port.ts +29 -0
  12. package/src/coordinator-prompt.ts +237 -0
  13. package/src/coordinator-socket.ts +236 -0
  14. package/src/deadlines.ts +47 -10
  15. package/src/decisions/directory.ts +8 -6
  16. package/src/duration.ts +29 -0
  17. package/src/emit-command.ts +119 -0
  18. package/src/emit.ts +246 -0
  19. package/src/files.ts +11 -1
  20. package/src/help/allowance.txt +13 -0
  21. package/src/help/emit.txt +13 -0
  22. package/src/help/inbox.txt +12 -0
  23. package/src/help/init.txt +12 -0
  24. package/src/help/logs.txt +13 -0
  25. package/src/help/reply.txt +18 -0
  26. package/src/help/run.txt +44 -0
  27. package/src/help/runs.txt +11 -0
  28. package/src/help/test.txt +9 -0
  29. package/src/help/tick.txt +12 -0
  30. package/src/here.ts +9 -3
  31. package/src/hold-run-lock.fixture.ts +8 -0
  32. package/src/holder-command.ts +279 -0
  33. package/src/inbox-command.ts +715 -0
  34. package/src/init-command.ts +29 -11
  35. package/src/init-template/AGENTS.md.template +4 -4
  36. package/src/init-template/hello/workflow.test.ts.template +2 -2
  37. package/src/init-template/hello/workflow.ts.template +16 -20
  38. package/src/keep-alive.ts +40 -0
  39. package/src/logs-command.ts +106 -35
  40. package/src/machine.ts +12 -1
  41. package/src/mailbox.ts +990 -0
  42. package/src/message-operation.ts +26 -3
  43. package/src/messaging/limits.ts +1 -1
  44. package/src/messaging/messaging.ts +478 -85
  45. package/src/messaging/routes.ts +11 -4
  46. package/src/messaging/text.ts +25 -22
  47. package/src/operation-liveness.ts +55 -9
  48. package/src/operator-cli.ts +290 -119
  49. package/src/operator-runtime.ts +8 -8
  50. package/src/progress-view.ts +49 -7
  51. package/src/projects.ts +57 -0
  52. package/src/result-slots.ts +43 -19
  53. package/src/result-validation.ts +18 -27
  54. package/src/run-command.ts +29 -66
  55. package/src/run-commands.ts +13 -2
  56. package/src/run-lock.ts +123 -0
  57. package/src/run-prepare.ts +94 -22
  58. package/src/run-state.ts +125 -6
  59. package/src/run-usage.ts +2 -2
  60. package/src/run-view.ts +0 -9
  61. package/src/runs-command.ts +43 -25
  62. package/src/runs.ts +142 -23
  63. package/src/sandboxes.ts +11 -4
  64. package/src/simple-input.ts +268 -0
  65. package/src/socket-exchange.ts +26 -0
  66. package/src/sources.ts +70 -0
  67. package/src/stage-ledger.ts +89 -14
  68. package/src/stage-plan.ts +23 -0
  69. package/src/stop-crashed.ts +39 -0
  70. package/src/test-command.ts +34 -11
  71. package/src/testing.ts +72 -4
  72. package/src/tick-command.ts +76 -0
  73. package/src/tools.ts +645 -0
  74. package/src/wake.ts +264 -0
  75. package/src/workflow-loader.ts +32 -22
  76. package/src/workflow-runner.ts +1277 -97
  77. package/src/workflow-testing/coordinator.typecheck.ts +54 -0
  78. package/src/workflow-testing/coordinators.ts +137 -0
  79. package/src/workflow-testing/host.ts +67 -7
  80. package/src/workflow-testing/index.ts +488 -34
  81. package/src/workflow-testing/script.ts +74 -4
  82. package/src/workflow-testing/workflow-testing.typecheck.ts +19 -2
@@ -1,9 +1,14 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { mkdir, realpath, rm } from "node:fs/promises";
4
+ import { homedir } from "node:os";
4
5
  import { dirname, join, relative } from "node:path";
5
6
  import { isDeepStrictEqual } from "node:util";
6
- import { PARTICIPANT } from "@agentswf/contract/participant";
7
+ import {
8
+ PARTICIPANT,
9
+ participantRef,
10
+ RESERVED_PARTICIPANT_KEYS,
11
+ } from "@agentswf/contract/participant";
7
12
  import type {
8
13
  AgentPaneRecord,
9
14
  AgentSkillsRecord,
@@ -16,6 +21,7 @@ import {
16
21
  TURN_OPERATION_FIELDS,
17
22
  TURN_RECORD_VERSION,
18
23
  type TurnRecord,
24
+ type WaitTake,
19
25
  } from "@agentswf/contract/records";
20
26
  import {
21
27
  formatErrors,
@@ -25,6 +31,7 @@ import {
25
31
  } from "@agentswf/contract/schema";
26
32
  import {
27
33
  type AbsoluteDeadline,
34
+ type AgentEndedEvent,
28
35
  type AgentExecution,
29
36
  type AgentForkSpec,
30
37
  type AgentKey,
@@ -32,35 +39,59 @@ import {
32
39
  type AgentRef,
33
40
  type AgentRunStructuredSpec,
34
41
  type AgentRunTextSpec,
35
- type AgentStructuredTurnSpec,
36
- type AgentTextTurnSpec,
42
+ type AgentStartSpec,
43
+ type Branches,
44
+ type CodeParticipantRef,
37
45
  type CompactionId,
38
46
  type CompactSpec,
39
47
  DeadlineExceededError,
48
+ type Duration,
49
+ type EmitOutcome,
50
+ type EventAgentOpenSpec,
51
+ type EventAgentRef,
52
+ type HarnessAllowance,
53
+ isEventWorkflow,
40
54
  isJsonValue,
41
55
  type JsonObject,
42
56
  type JsonValue,
43
57
  type KeepPane,
58
+ type Matched,
44
59
  type MessageAccess,
60
+ type Next,
61
+ nameProblem,
45
62
  type OperationRecord,
46
63
  type OutputSchema,
47
64
  type PaneLayout,
65
+ type ParticipantRef,
66
+ type ParticipantSpec,
48
67
  type PlacementChoice,
49
68
  placementOf,
69
+ type RaceBranches,
70
+ type ReceiveOptions,
50
71
  type RunResult,
51
72
  type RuntimeSelection,
73
+ type SelectOptions,
52
74
  type SettingsSpec,
53
75
  type SkillSource,
54
76
  type StageOptions,
55
77
  type StageSummary,
78
+ type StartedTurn,
79
+ type TimeRead,
80
+ type Timer,
81
+ type TurnId,
56
82
  type TurnOutcome,
83
+ type WaitOptions,
84
+ type When,
57
85
  type WorkflowContext,
58
86
  type WorkflowDefinition,
87
+ type WorkflowEvent,
59
88
  } from "@agentswf/contract/workflow";
60
89
  import {
61
90
  type AgentSkills,
62
91
  effortRefusal,
63
92
  findHarness,
93
+ HARNESS_NAMES,
94
+ type Harness,
64
95
  hostHome,
65
96
  settingsRefusal,
66
97
  skillsLayout,
@@ -87,11 +118,13 @@ import {
87
118
  type ResultControlPlane,
88
119
  startResultControlPlane,
89
120
  } from "./control-plane";
121
+ import type { CoordinatorPort } from "./coordinator-port";
90
122
  import {
91
123
  assertDeadline,
92
124
  assertDeadlineValue,
93
125
  deadlineWithin,
94
126
  earlierDeadline,
127
+ harnessDeadline,
95
128
  runUntilStopped,
96
129
  scheduleAt,
97
130
  WorkflowCancelledError,
@@ -99,9 +132,28 @@ import {
99
132
  } from "./deadlines";
100
133
  import { RunDecisions } from "./decisions/directory";
101
134
  import type { DecisionInstallation } from "./decisions/seam";
135
+ import { parseWhen } from "./duration";
136
+ import { emitEvent } from "./emit";
102
137
  import { messageOf } from "./errors";
138
+ import {
139
+ asTimeoutHandler,
140
+ checkedKeepAlive,
141
+ DEFAULT_WINDOW_MS,
142
+ inTimeoutHandler,
143
+ type KeepAlive,
144
+ } from "./keep-alive";
145
+ import {
146
+ AT_DUE,
147
+ type RestoredTurn,
148
+ RunMailbox,
149
+ RunParked,
150
+ recoverTurns,
151
+ type TurnEnd,
152
+ unrunUsage,
153
+ } from "./mailbox";
103
154
  import { superviseMessageOperation } from "./message-operation";
104
155
  import { canAwaitReply } from "./messaging/limits";
156
+ import type { HolderRequest } from "./messaging/messaging";
105
157
  import { type AgentView, type Delivery, Messaging, type OperationEnd } from "./messaging/messaging";
106
158
  import type { Scope as MessagingScope } from "./messaging/routes";
107
159
  import { createOperationEvents } from "./operation-events";
@@ -129,7 +181,7 @@ import {
129
181
  type OperationTags,
130
182
  type RunLedger,
131
183
  } from "./run-usage";
132
- import { endTurnsLine, readStageRecords } from "./runs";
184
+ import { endTurnsLine, readStageRecords, readTurns } from "./runs";
133
185
  import { type CredentialLocks, placeCarried, seedHome } from "./sandbox-homes";
134
186
  import {
135
187
  RUN_SANDBOX_ONLY,
@@ -137,15 +189,23 @@ import {
137
189
  type RunSandboxOptions,
138
190
  type SeatedAgent,
139
191
  } from "./sandboxes";
192
+ import { QUOTING_RULE, simpleInput, takesText, usageLine } from "./simple-input";
140
193
  import { placeSkills, RunSkills, readSkillSources } from "./skills/run-skills";
194
+ import { asSource, branchOf, branchValue, source, takeOf } from "./sources";
141
195
  import { StageLedger, type StageProgress } from "./stage-ledger";
142
196
  import { FromStageUnreached } from "./stopped";
197
+ import { inToolHandler, outsideTools, refusedInTool, ToolCalls, toolsLesson } from "./tools";
198
+ import type { EventWorkflow } from "./workflow-loader";
143
199
 
144
200
  export { WorkflowCancelledError } from "./deadlines";
145
201
 
146
202
  export type RunWorkflowOptions = {
147
203
  /** Internal timing policy; workflow authors use the operation deadline. */
148
204
  livenessPolicy?: LivenessPolicy;
205
+ /** A `keepAlive` window that gives no `timeoutMs`; absent, an hour. A test's is short. */
206
+ defaultWindowMs?: number;
207
+ /** The run's time, which its timers and `wf.now()` read; absent, the clock's. A test's is virtual. */
208
+ now?: () => number;
149
209
  runRoot: string;
150
210
  /** The run this is an attempt of, whose folder the operator claimed under `runRoot`. */
151
211
  run: {
@@ -158,9 +218,30 @@ export type RunWorkflowOptions = {
158
218
  fromStage?: string;
159
219
  /** `--values`: the values of stages before `fromStage` that have no record to reuse. */
160
220
  values?: ReadonlyMap<string, JsonValue>;
221
+ /** A run of a workflow with `events`. */
222
+ events?: true;
223
+ /** With `events`: runs `work` holding the run's lock, as every append to the mailbox is made. */
224
+ locked?<T>(work: () => Promise<T>): Promise<T>;
225
+ /**
226
+ * With `events`: false when the attempt before this one has no ending, its process gone with its
227
+ * agents perhaps still running: its sessions aren't reopened until they are stopped (Q17).
228
+ */
229
+ resumeSessions?: boolean;
230
+ /** With `events`: calls `nudge` whenever awf tells the run its mailbox has a new event. */
231
+ listenNudges?(nudge: () => void): void;
232
+ /**
233
+ * Writes the attempt's `parked` ending, which the run's wait calls holding the run's lock: an
234
+ * event run's, or a test's. Absent, the run never parks and waits in its process.
235
+ */
236
+ park?(wait: { label?: string; until?: string }): Promise<void>;
237
+ /** With `park`: the wait stays live to `until` for an agent's window instead. A test's hook. */
238
+ live?(until: number): Promise<void>;
239
+ /** With `park`: moves a virtual clock to the timer due at `until` rather than parking. A test's hook. */
240
+ skip?(until: number): Promise<boolean>;
161
241
  };
162
242
  runtime: AgentRuntimeConfig;
163
- deadline: AbsoluteDeadline;
243
+ /** The run's deadline; absent, it has none, and only deadlines the workflow sets bound it. */
244
+ deadline?: AbsoluteDeadline;
164
245
  cwd?: string;
165
246
  onLog?: (message: string, fields?: JsonObject) => void;
166
247
  /** The operator hands the session back once agents.stop has released it. */
@@ -172,13 +253,36 @@ export type RunWorkflowOptions = {
172
253
  skillCache?: string;
173
254
  /** The decision models workflows may ask; without them, `decisions.decide` is refused. */
174
255
  decisions?: DecisionInstallation;
256
+ /** How `wf.allowance` reads a harness's plan; without it, every harness reads `none`. */
257
+ allowance?: (harness: Harness) => Promise<HarnessAllowance>;
175
258
  /**
176
259
  * Where an agent may `--await-reply`: the engine's list by harness and placement unless given. A
177
260
  * workflow's test allows it everywhere, as the fake harness can.
178
261
  */
179
262
  awaitReply?: (execution: AgentExecution, confirmsDelivery: boolean) => boolean;
263
+ /**
264
+ * Whatever answers for the run's outside participants, each told everything: `awf run`'s prompt,
265
+ * the socket `awf reply` and `awf send` reach, or a test's scripts. Unsupported outside the engine.
266
+ * @internal
267
+ */
268
+ coordinators?: readonly CoordinatorPort[];
269
+ /**
270
+ * Sends `wf.emit`'s event through the one way in; absent, `emitEvent` on this machine. A
271
+ * test records them instead.
272
+ */
273
+ emit?: Emitter;
180
274
  };
181
275
 
276
+ /** Sends an event from a run to its target. */
277
+ export type Emitter = (
278
+ target: EventWorkflow,
279
+ event: { run: string; name: string; data: JsonValue; id?: string },
280
+ from: string,
281
+ ) => Promise<EmitOutcome>;
282
+
283
+ const machineEmitter: Emitter = async (target, event, from) =>
284
+ (await emitEvent(target, event, { from, home: homedir(), environment: process.env })).outcome;
285
+
182
286
  /** What a run is known by once it has ended, whether or not it succeeded. */
183
287
  export type SettledRun = {
184
288
  runId: string;
@@ -263,6 +367,8 @@ export type WorkflowRunSnapshot = {
263
367
  key: AgentKey;
264
368
  /** Messages held for it and not yet shown; absent when none are. */
265
369
  queued?: number;
370
+ /** Idle with `keepAlive`, the end of its window: the run stays live to it. */
371
+ liveUntil?: number;
266
372
  execution: AgentExecution;
267
373
  state:
268
374
  | "starting"
@@ -291,6 +397,8 @@ type AgentEntry = {
291
397
  stopping?: Promise<boolean>;
292
398
  stopComplete?: boolean;
293
399
  releasedAt?: number;
400
+ /** As the open last gave it: a reopen that gives one replaces it. */
401
+ keepAlive?: KeepAlive;
294
402
  /** How it was opened, or found for the calling session, which its forks reuse. */
295
403
  opening?: {
296
404
  sessions: AgentSessions;
@@ -345,6 +453,11 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
345
453
  ): Promise<WorkflowRunHandle<Result>> {
346
454
  assertDeadline(options.deadline);
347
455
  if (options.signal?.aborted) throw new WorkflowCancelledError(options.signal.reason);
456
+ // Before anything opens, so a refusal leaves nothing to close.
457
+ const withEvents = isEventWorkflow(definition);
458
+ 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");
348
461
  const { dir: runDir, id: runId, attempt } = options.run;
349
462
  const startedAt = Date.now();
350
463
  const cwd = options.cwd ?? process.cwd();
@@ -369,7 +482,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
369
482
  runId,
370
483
  ...(options.run.label === undefined ? {} : { label: options.run.label }),
371
484
  cwd,
372
- deadline: options.deadline,
485
+ deadline: harnessDeadline(options.deadline),
373
486
  }),
374
487
  );
375
488
  try {
@@ -421,6 +534,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
421
534
  ...(options.run.fromStage === undefined ? {} : { fromStage: options.run.fromStage }),
422
535
  ...(options.run.values === undefined ? {} : { values: options.run.values }),
423
536
  ...(definition.meta.version === undefined ? {} : { workflowVersion: definition.meta.version }),
537
+ ...(withEvents ? { byContent: true } : {}),
424
538
  turns: () => turns,
425
539
  event: (body, at) => events.event(body, at),
426
540
  });
@@ -461,9 +575,47 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
461
575
  });
462
576
  const fatal = Promise.withResolvers<never>();
463
577
  void fatal.promise.catch(() => undefined);
464
- const owner = new WorkflowOwner({
578
+ const now = options.now ?? Date.now;
579
+ // A turn an earlier attempt started and never ended, its process gone, ends failed before the code
580
+ // runs again (Q6); the turns whose ends no wait took are their agents' turns again.
581
+ const restoredTurns = withEvents ? await recoverTurns(runDir, attempt, locked, now) : undefined;
582
+ // An event run's agents reopen on the sessions its earlier attempts' turns ran on.
583
+ const keptSessions =
584
+ withEvents && options.run.resumeSessions !== false
585
+ ? keptSessionsOf(await readTurns(runDir))
586
+ : undefined;
587
+ // Every run's timers live in a mailbox; a run of a workflow with events takes its events there too.
588
+ // A plain run without `park` waits for its timers in its process.
589
+ const waits = new RunMailbox({
590
+ runDir,
591
+ attempt,
592
+ locked,
593
+ now,
594
+ ...(options.run.park
595
+ ? {
596
+ parking: {
597
+ idle: (stage) => owner.idle(stage),
598
+ timeout: (signal) => owner.timeout(signal),
599
+ liveUntil: () => owner.liveUntil(),
600
+ park: options.run.park,
601
+ ...(options.run.live ? { live: options.run.live } : {}),
602
+ ...(options.run.skip ? { skip: options.run.skip } : {}),
603
+ },
604
+ }
605
+ : {}),
606
+ });
607
+ await waits.open();
608
+ const mailbox = withEvents ? waits : undefined;
609
+ if (mailbox) options.run.listenNudges?.(() => mailbox.nudge());
610
+ // The mailbox's parking asks the owner, made after it: by then it is.
611
+ const owner: WorkflowOwner = new WorkflowOwner({
612
+ ...(mailbox ? { mailbox } : {}),
613
+ waits,
614
+ ...(keptSessions ? { keptSessions } : {}),
615
+ ...(restoredTurns ? { restoredTurns } : {}),
465
616
  failRun: (error) => fatal.reject(error),
466
617
  decisions,
618
+ ...(options.allowance ? { allowance: options.allowance } : {}),
467
619
  sandboxes,
468
620
  skills,
469
621
  runDir,
@@ -475,6 +627,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
475
627
  runId,
476
628
  attempt,
477
629
  cwd,
630
+ now,
478
631
  deadline: options.deadline,
479
632
  runtime: options.runtime,
480
633
  host,
@@ -486,7 +639,11 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
486
639
  events,
487
640
  onCallerStopped: options.onCallerStopped,
488
641
  livenessPolicy: options.livenessPolicy,
642
+ ...(options.defaultWindowMs === undefined ? {} : { defaultWindowMs: options.defaultWindowMs }),
489
643
  awaitReply: options.awaitReply ?? ((execution, confirms) => canAwaitReply(execution, confirms)),
644
+ coordinators: options.coordinators ?? [],
645
+ emit: options.emit ?? machineEmitter,
646
+ workflow: definition.meta.name,
490
647
  });
491
648
  const stopped = new AbortController();
492
649
  // Aborted by a stop that comes after the body ended; the one that ended it does not cut short the
@@ -510,6 +667,8 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
510
667
  return returned;
511
668
  }),
512
669
  fatal.promise,
670
+ // A park ends the body, even code that caught it.
671
+ waits.whenParked,
513
672
  ]),
514
673
  signal,
515
674
  options.deadline,
@@ -524,6 +683,13 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
524
683
  failed = true;
525
684
  failure = error;
526
685
  }
686
+ if (mailbox && !mailbox.parked)
687
+ await owner.endStartedTurns(failed ? "the workflow failed" : "the workflow returned");
688
+ // Once `parked` is written, the attempt is parked, whatever the code did after.
689
+ if (waits.parked) {
690
+ failed = true;
691
+ failure = waits.parked;
692
+ }
527
693
  const ending = stages.settle(
528
694
  failed ? failure : undefined,
529
695
  signal.aborted || failure instanceof DeadlineExceededError,
@@ -619,10 +785,12 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
619
785
  messages: known.messages,
620
786
  agents: agents.map((agent) => {
621
787
  const queued = owner.held(agent.key);
788
+ const liveUntil = owner.liveUntilOf(agent.key);
622
789
  return {
623
790
  ...known.agents.get(agent.key),
624
791
  ...agent,
625
792
  ...(queued > 0 ? { queued } : {}),
793
+ ...(liveUntil === undefined ? {} : { liveUntil }),
626
794
  };
627
795
  }),
628
796
  };
@@ -681,7 +849,7 @@ type PlacedSkills = { given: AgentSkills; writeBack?: () => Promise<void> };
681
849
  type AgentOpening = {
682
850
  spec: AgentOpenSpec;
683
851
  execution: AgentExecution;
684
- deadline: AbsoluteDeadline;
852
+ deadline: AbsoluteDeadline | undefined;
685
853
  sessions: AgentSessions;
686
854
  ledger: AgentLedger;
687
855
  /** Its skills' sources, resolved when it opens; absent when it has the operator's. */
@@ -713,6 +881,7 @@ class WorkflowOwner {
713
881
  /** Each agent once it has opened, by key: what messaging asks about, and what a ref must be. */
714
882
  readonly #logical = new Map<string, LogicalAgent>();
715
883
  readonly #messaging: Messaging;
884
+ readonly #tools: ToolCalls<ExecutionScope>;
716
885
  /** A stage's execution scope, by its messaging scope: what owns the operations its routes open. */
717
886
  readonly #stageScopes = new Map<MessagingScope, ExecutionScope>();
718
887
  /** Message operations routes outside any stage opened, which the attempt owns. */
@@ -722,12 +891,14 @@ class WorkflowOwner {
722
891
  readonly #inFlight = new Set<Promise<unknown>>();
723
892
  #closed = false;
724
893
  #closing: Promise<unknown[]> | undefined;
725
- /** The bundled `wf`'s source, built once, at the first sandboxed agent. */
894
+ /** The bundled `wfa`'s source, built once, at the first sandboxed agent. */
726
895
  #bundle: Promise<string> | undefined;
727
896
  /** The calling session, once the workflow has asked for it (ADR 0010). */
728
897
  #caller: { key: string; state: Promise<LogicalAgent> } | undefined;
729
898
  /** Stops a fork of the session the run was started from, whose process is not an agent's. */
730
899
  readonly #closingRun = new AbortController();
900
+ /** Outside participants the workflow connected, by key: the refs a route's ends must be. */
901
+ readonly #outsideRefs = new Map<string, ParticipantRef>();
731
902
 
732
903
  constructor(
733
904
  private readonly options: {
@@ -736,14 +907,16 @@ class WorkflowOwner {
736
907
  failRun(error: Error): void;
737
908
  runDir: string;
738
909
  livenessPolicy?: LivenessPolicy;
910
+ defaultWindowMs?: number;
739
911
  locks: CredentialLocks;
740
912
  environment: Readonly<Record<string, string | undefined>>;
741
913
  decisions: RunDecisions;
914
+ allowance?: (harness: Harness) => Promise<HarnessAllowance>;
742
915
  stages: StageLedger;
743
916
  runId: string;
744
917
  attempt: number;
745
918
  cwd: string;
746
- deadline: AbsoluteDeadline;
919
+ deadline: AbsoluteDeadline | undefined;
747
920
  runtime: AgentRuntimeConfig;
748
921
  host: AgentRunHost;
749
922
  slots: ResultSlotRegistry;
@@ -758,15 +931,33 @@ class WorkflowOwner {
758
931
  events: RunEventLog;
759
932
  onCallerStopped?: RunWorkflowOptions["onCallerStopped"];
760
933
  awaitReply: NonNullable<RunWorkflowOptions["awaitReply"]>;
934
+ coordinators: readonly CoordinatorPort[];
935
+ /** A run of a workflow with `events`: its mailbox, which its waits take from. */
936
+ mailbox?: RunMailbox;
937
+ /** The run's time. */
938
+ now: () => number;
939
+ /** Its timers: the mailbox for a run with `events`, one of its own for a plain run. */
940
+ waits: RunMailbox;
941
+ /** With `mailbox`: each root agent's last session in earlier attempts, which it reopens on. */
942
+ keptSessions?: ReadonlyMap<string, NativeFork>;
943
+ /** With `mailbox`: each agent's turn an earlier attempt started whose end no wait took. */
944
+ restoredTurns?: ReadonlyMap<string, RestoredTurn>;
945
+ emit: Emitter;
946
+ /** The workflow's name, which with the run's id is the ref its events are `from`. */
947
+ workflow: string;
761
948
  },
762
949
  ) {
950
+ this.#restored = new Map(options.restoredTurns ?? []);
763
951
  this.#messaging = new Messaging({
764
952
  agent: (key) => this.#agentView(key),
765
953
  canAwait: (key) => {
766
954
  const agent = this.#logical.get(key);
767
- return agent ? options.awaitReply(agent.execution, agent.confirmsDelivery) : false;
955
+ // One with a started turn takes messages only once a wait has its end: a reply awaited from
956
+ // it could wait on code that is itself waiting.
957
+ if (!agent || agent.hasStartedTurn) return false;
958
+ return options.awaitReply(agent.execution, agent.confirmsDelivery);
768
959
  },
769
- launcher: (key) => this.#logical.get(key)?.launcher ?? "wf",
960
+ launcher: (key) => this.#logical.get(key)?.launcher ?? "wfa",
770
961
  changed: (key) => {
771
962
  this.#messagesChanged();
772
963
  this.#logical.get(key)?.messagesChanged();
@@ -788,9 +979,27 @@ class WorkflowOwner {
788
979
  if (!(await options.events.event({ ...event, callPath: [] }))) {
789
980
  throw new Error("the run's event log was not written");
790
981
  }
982
+ for (const port of options.coordinators) {
983
+ try {
984
+ port.recorded(event);
985
+ } catch (error) {
986
+ options.onLog?.(`awf: a coordinator did not take an event: ${messageOf(error)}`);
987
+ }
988
+ }
791
989
  },
792
990
  });
793
991
  options.control.attach(this.#messaging);
992
+ this.#tools = new ToolCalls<ExecutionScope>({
993
+ record: (event) => options.events.event(event),
994
+ runIn: (scope, fn) => (scope ? scopes.run(scope, fn) : scopes.exit(fn)),
995
+ });
996
+ options.control.attachTools(this.#tools);
997
+ const holder = {
998
+ reply: (request: HolderRequest) => this.#messaging.holderReply(request),
999
+ send: (request: HolderRequest) => this.#messaging.holderSend(request),
1000
+ open: () => this.#messaging.openQuestions(),
1001
+ };
1002
+ for (const port of options.coordinators) port.attach(holder);
794
1003
  this.context = {
795
1004
  runId: options.runId,
796
1005
  attempt: options.attempt,
@@ -830,6 +1039,8 @@ class WorkflowOwner {
830
1039
  }
831
1040
  },
832
1041
  forkCaller: (spec) => {
1042
+ if ((spec as AgentForkSpec).tools !== undefined)
1043
+ return Promise.reject(new Error("a fork of the calling session takes no tools"));
833
1044
  try {
834
1045
  return this.forkCaller(spec);
835
1046
  } catch (error) {
@@ -872,15 +1083,36 @@ class WorkflowOwner {
872
1083
  },
873
1084
  },
874
1085
  participants: {
875
- connect: () => unavailable("participants.connect"),
876
- get: () => unavailable("participants.get"),
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,
877
1091
  },
878
1092
  messages: { allow: async (access) => this.allowRoute(access) },
879
1093
  steps: {
880
1094
  run: () => unavailable("steps.run"),
881
1095
  sleep: () => unavailable("steps.sleep"),
882
1096
  },
883
- signals: { receive: () => unavailable("signals.receive") },
1097
+ emit: async (target, run, name, data, emitOptions) => {
1098
+ if (this.#closed || this.#cleanupFailures.length)
1099
+ throw new Error("workflow context is closed");
1100
+ scopes.getStore()?.assertAccepting();
1101
+ if (!isEventWorkflow(target))
1102
+ throw new Error("emit's target is a workflow with events, as its file exports it");
1103
+ return this.track(
1104
+ options.emit(
1105
+ target,
1106
+ {
1107
+ run,
1108
+ name,
1109
+ data: data as JsonValue,
1110
+ ...(emitOptions?.eventId === undefined ? {} : { id: emitOptions.eventId }),
1111
+ },
1112
+ `${options.workflow}/${options.runId}`,
1113
+ ),
1114
+ );
1115
+ },
884
1116
  parallel: (items, operation, parallelOptions) => {
885
1117
  const inherited = scopes.getStore()?.deadline ?? options.deadline;
886
1118
  const deadline = parallelOptions?.deadline
@@ -899,8 +1131,357 @@ class WorkflowOwner {
899
1131
  this.runStage(name, rest)) as WorkflowContext["stage"],
900
1132
  call: () => unavailable("call"),
901
1133
  usage: () => options.ledger.records(),
1134
+ allowance: async (harness) => {
1135
+ if (this.#closed || this.#cleanupFailures.length)
1136
+ throw new Error("workflow context is closed");
1137
+ if (!(HARNESS_NAMES as readonly string[]).includes(harness))
1138
+ throw new Error(`allowance: no harness ${harness}; awf has ${HARNESS_NAMES.join(", ")}`);
1139
+ if (!options.allowance)
1140
+ return { harness, read: "none", reason: "this run has no allowance reader" };
1141
+ return this.track(options.allowance(harness as Harness));
1142
+ },
902
1143
  log: (message, fields) => options.log(message, fields),
1144
+ now: () => {
1145
+ this.#notReading("wf.now()");
1146
+ return new Date(options.now());
1147
+ },
1148
+ sleep: async (name, when) => {
1149
+ await this.race(
1150
+ options.waits,
1151
+ name,
1152
+ { sleep: checkedWhen(when) } as RaceBranches,
1153
+ undefined,
1154
+ );
1155
+ },
903
1156
  };
1157
+ const { mailbox } = options;
1158
+ Object.assign(
1159
+ this.context,
1160
+ mailbox
1161
+ ? {
1162
+ receive: (spec?: ReceiveOptions) => this.receive(mailbox, spec),
1163
+ race: (name: string, branches: RaceBranches, raceOptions?: WaitOptions) =>
1164
+ this.race(mailbox, name, branches, raceOptions),
1165
+ select: (branches: Branches, selectOptions?: SelectOptions) =>
1166
+ this.select(mailbox, branches, selectOptions),
1167
+ timer: (name: string, when: When) => this.timer(mailbox, name, when),
1168
+ timers: () => source({}, { name: "timer:due" }),
1169
+ at: (read: () => Date | null) => timeRead(read),
1170
+ after: (duration: Duration) => {
1171
+ this.#notReading("wf.after()");
1172
+ if (typeof duration !== "string" || !/^\d+(?:\.\d+)?[dhm]$/.test(duration))
1173
+ throw new Error(
1174
+ `wf.after takes days, hours or minutes, "1d", "3h", "1.5h"; not ${String(duration)}`,
1175
+ );
1176
+ return new Date(parseWhen(duration, options.now()).due);
1177
+ },
1178
+ signal: (name: string) => signalSource(name),
1179
+ ...(mailbox.started ? { started: mailbox.started } : {}),
1180
+ }
1181
+ : { receive: () => Promise.reject(new Error("receive needs a workflow with events")) },
1182
+ );
1183
+ }
1184
+
1185
+ /** Set while a select reads a time from state. */
1186
+ #reading = false;
1187
+
1188
+ /** Refuses `what` in a time read from state: a time made at the read is never due. */
1189
+ #notReading(what: string): void {
1190
+ if (this.#reading)
1191
+ throw new Error(
1192
+ `${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
+ );
1194
+ }
1195
+
1196
+ /** Branch `branch`'s time, read from state now: an ISO time, or undefined for `null`. */
1197
+ #readTime(branch: string, read: () => Date | null): string | undefined {
1198
+ this.#reading = true;
1199
+ let value: unknown;
1200
+ try {
1201
+ value = read();
1202
+ } finally {
1203
+ this.#reading = false;
1204
+ }
1205
+ if (value === null) return undefined;
1206
+ if (!(value instanceof Date) || !Number.isFinite(value.getTime()))
1207
+ throw new Error(
1208
+ `select's ${branch}: wf.at reads a valid Date or null from the run's state, not ${
1209
+ value instanceof Date ? "an invalid Date" : (JSON.stringify(value) ?? String(value))
1210
+ }`,
1211
+ );
1212
+ return value.toISOString();
1213
+ }
1214
+
1215
+ /** Checks a wait may begin here: one at a time, outside a tool's handler, in an open scope. */
1216
+ #beginWait(what: string): ExecutionScope | undefined {
1217
+ if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
1218
+ if (inToolHandler())
1219
+ throw new Error(`${what} is refused in a tool's handler: it would take the run's events`);
1220
+ const scope = scopes.getStore();
1221
+ scope?.assertAccepting();
1222
+ if (this.#waiting)
1223
+ throw new Error(`${what} was called while another wait is waiting; one wait at a time`);
1224
+ return scope;
1225
+ }
1226
+
1227
+ /** Runs a wait, which ends with the run, as its owner closes, or with the scope it waits in. */
1228
+ async #holdingWait(
1229
+ scope: ExecutionScope | undefined,
1230
+ wait: (signal: AbortSignal) => Promise<WorkflowEvent<string, JsonValue>[]>,
1231
+ ): Promise<WorkflowEvent<string, JsonValue>[]> {
1232
+ this.#waiting = true;
1233
+ const cancelled = new AbortController();
1234
+ const removeFromScope = scope?.add(async (stop) =>
1235
+ cancelled.abort(new Error(String(stop.reason ?? "the wait's scope was cancelled"))),
1236
+ );
1237
+ try {
1238
+ const taken = await wait(AbortSignal.any([this.#closingRun.signal, cancelled.signal]));
1239
+ // A turn's end taken: its agent takes other work again.
1240
+ for (const event of taken) {
1241
+ if (event.name !== "agent:ended") continue;
1242
+ const { turn } = event.data as unknown as TurnEnd;
1243
+ if (this.#restored.get(event.from)?.turn === turn) this.#restored.delete(event.from);
1244
+ this.#logical.get(event.from)?.endTaken(turn);
1245
+ }
1246
+ return taken;
1247
+ } finally {
1248
+ removeFromScope?.();
1249
+ this.#waiting = false;
1250
+ }
1251
+ }
1252
+
1253
+ /** A timer, by name: the recorded one, or set now. A race's own are named `{race}@{branch}`. */
1254
+ private timer(mailbox: RunMailbox, name: string, when: When, own = false): Timer {
1255
+ if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
1256
+ if (typeof name !== "string") throw new Error("a timer's name is a string");
1257
+ if (!own) {
1258
+ const problem = name.includes("@")
1259
+ ? "without @, which a race's own timers use"
1260
+ : nameProblem(name);
1261
+ if (problem) throw new Error(`timer ${name}: its name is ${problem}`);
1262
+ }
1263
+ const given = parseWhen(when, this.options.now());
1264
+ const { timer, asked } = mailbox.setTimer(name, given.due, given.when);
1265
+ if (timer.cancelled)
1266
+ this.options.onLog?.(
1267
+ `awf: timer ${name} was cancelled; a name stays cancelled, so it won't fire`,
1268
+ );
1269
+ else if (asked !== undefined)
1270
+ this.options.onLog?.(
1271
+ `awf: timer ${name} is due ${timer.due}, as first set (${timer.when}); ${asked} is ignored`,
1272
+ );
1273
+ return timerSource(mailbox, name, timer.due);
1274
+ }
1275
+
1276
+ /** Waits for the first of `branches`, recorded under `name`. */
1277
+ private async race(
1278
+ mailbox: RunMailbox,
1279
+ name: string,
1280
+ branches: RaceBranches,
1281
+ raceOptions: WaitOptions | undefined,
1282
+ ): Promise<Next<RaceBranches>> {
1283
+ const scope = this.#beginWait(`race ${name}`);
1284
+ if (typeof name !== "string" || nameProblem(name))
1285
+ throw new Error(
1286
+ `a race's name is ${typeof name === "string" ? nameProblem(name) : "a string"}`,
1287
+ );
1288
+ if (scope?.stage !== undefined)
1289
+ throw new Error(`race ${name} is in stage ${scope.stage}: a race is recorded on its own`);
1290
+ if (this.#raced.has(name))
1291
+ throw new Error(
1292
+ `race ${name} has answered in this attempt; a wait in a loop is named per round, as approval-\${round}`,
1293
+ );
1294
+ this.#raced.add(name);
1295
+ const label = checkedLabel(raceOptions?.label);
1296
+ const given = branchesOf(`race ${name}`, branches, true);
1297
+ // A sleep's time may be a Date, its own timer as a duration is.
1298
+ const own = (source: unknown) => typeof source === "string" || source instanceof Date;
1299
+ // Every branch checked before a timer is set, so a refused race leaves none behind.
1300
+ const takes = new Map(
1301
+ given.flatMap(([branch, source]) =>
1302
+ own(source) ? [] : [[branch, sourceTake(`race ${name}`, branch, source)] as const],
1303
+ ),
1304
+ );
1305
+ const owned = new Map<string, string>();
1306
+ const pairs: (readonly [string, WaitTake])[] = given.map(([branch, source]) => {
1307
+ if (!own(source)) return [branch, takes.get(branch) as WaitTake] as const;
1308
+ const timer = this.timer(mailbox, `${name}@${branch}`, source as When, true);
1309
+ owned.set(branch, timer.name);
1310
+ return [branch, { name: "timer:due", from: timer.name }] as const;
1311
+ });
1312
+ let won: { branch: string; event: WorkflowEvent<string, JsonValue> } | undefined;
1313
+ try {
1314
+ await this.#holdingWait(scope, async (signal) => {
1315
+ won = await mailbox.race(name, pairs, owned, label, signal);
1316
+ return [won.event];
1317
+ });
1318
+ } 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))
1321
+ await Promise.all([...owned.values()].map((timer) => mailbox.cancelTimer(timer))).catch(
1322
+ () => undefined,
1323
+ );
1324
+ throw error;
1325
+ }
1326
+ const { branch, event } = won as { branch: string; event: WorkflowEvent<string, JsonValue> };
1327
+ return { key: branch, [branch]: branchValue(event) } as Next<RaceBranches>;
1328
+ }
1329
+
1330
+ /** A loop over the events `branches` take, one at a time. */
1331
+ private select(
1332
+ mailbox: RunMailbox,
1333
+ branches: Branches,
1334
+ selectOptions: SelectOptions | undefined,
1335
+ ): AsyncIterable<Next<Branches>> {
1336
+ const given = branchesOf("select", branches, false);
1337
+ const reads = new Map(
1338
+ given.flatMap(([branch, source]) => {
1339
+ const read = readOf(source);
1340
+ return read ? [[branch, read] as const] : [];
1341
+ }),
1342
+ );
1343
+ const pairs = given.map(
1344
+ ([branch, source]) =>
1345
+ [
1346
+ branch,
1347
+ reads.has(branch) ? { name: AT_DUE, from: branch } : sourceTake("select", branch, source),
1348
+ ] as const,
1349
+ );
1350
+ // Read at each pass of the wait: a time follows the state as it moves.
1351
+ const takes = (): WaitTake[] =>
1352
+ pairs.map(([branch, take]) => {
1353
+ const read = reads.get(branch);
1354
+ const due = read && this.#readTime(branch, read);
1355
+ return due === undefined ? take : { ...take, due };
1356
+ });
1357
+ const labelOf = () => {
1358
+ const given = selectOptions?.label;
1359
+ return checkedLabel(typeof given === "function" ? given() : given);
1360
+ };
1361
+ const next = async (): Promise<Next<Branches>> => {
1362
+ const scope = this.#beginWait("select");
1363
+ if (scope?.stage !== undefined)
1364
+ throw new Error(`select is in stage ${scope.stage}: a loop's wait is not a stage's`);
1365
+ const [event] = await this.#holdingWait(scope, async (signal) => [
1366
+ await mailbox.select(takes, labelOf(), signal),
1367
+ ]);
1368
+ const taken = event as WorkflowEvent<string, JsonValue>;
1369
+ const branch = branchOf(pairs, taken) as string;
1370
+ return { key: branch, [branch]: branchValue(taken) } as Next<Branches>;
1371
+ };
1372
+ return {
1373
+ [Symbol.asyncIterator]: () => ({
1374
+ next: async () => ({ done: false, value: await next() }),
1375
+ return: async () => ({ done: true, value: undefined }),
1376
+ }),
1377
+ };
1378
+ }
1379
+
1380
+ #waiting = false;
1381
+ /** The races and sleeps this attempt has run, each by its name once. */
1382
+ readonly #raced = new Set<string>();
1383
+ /** Each agent's turn an earlier attempt started whose end no wait has taken yet. */
1384
+ readonly #restored: Map<string, RestoredTurn>;
1385
+
1386
+ /** How an agent of an event run records its started turns in the mailbox. */
1387
+ #turnsOf(key: string, mailbox: RunMailbox): LogicalAgentTurns {
1388
+ return {
1389
+ begin: (turn, meta, execution) => mailbox.beginTurn(key, turn, meta, execution),
1390
+ end: (turn, meta, outcome) => mailbox.endTurn(key, { turn, meta, outcome }),
1391
+ // Read when the agent opens: a wait before then may have taken the end.
1392
+ restored: () => this.#restored.get(key),
1393
+ };
1394
+ }
1395
+
1396
+ /**
1397
+ * The code returned or threw: each started turn still running is cancelled, and its end written,
1398
+ * before the attempt's ending.
1399
+ */
1400
+ async endStartedTurns(reason: string): Promise<void> {
1401
+ // Bounded as cleanup is: what is cut, a continue's recovery ends.
1402
+ await waitForDeadline(
1403
+ Promise.all([...this.#logical.values()].map((agent) => agent.endStarted(reason))),
1404
+ { unixMilliseconds: Date.now() + CLEANUP_GRACE_MILLISECONDS },
1405
+ ).catch((error: unknown) =>
1406
+ this.options.onLog?.(`awf: a started turn's end was not written: ${messageOf(error)}`),
1407
+ );
1408
+ }
1409
+
1410
+ /**
1411
+ * Nothing of the run is working but its wait: no agent's turn, message or compaction, and no
1412
+ * decision, sandbox or agent opening in flight.
1413
+ */
1414
+ idle(waitStage?: string): boolean {
1415
+ const open = this.options.stages.openStage;
1416
+ return (
1417
+ (open === undefined || open === waitStage) &&
1418
+ this.#inFlight.size === 0 &&
1419
+ [...this.#logical.values()].every((agent) => !agent.busy)
1420
+ );
1421
+ }
1422
+
1423
+ /** The end of an idle agent's open window. */
1424
+ liveUntilOf(key: AgentKey): number | undefined {
1425
+ const agent = this.#logical.get(key);
1426
+ return agent && !agent.busy ? agent.liveUntil() : undefined;
1427
+ }
1428
+
1429
+ /** The end of the earliest window an agent holds open: the run stays live to it. */
1430
+ liveUntil(): number | undefined {
1431
+ const ends = [...this.#logical.values()].flatMap((agent) => agent.liveUntil() ?? []);
1432
+ return ends.length > 0 ? Math.min(...ends) : undefined;
1433
+ }
1434
+
1435
+ /**
1436
+ * Calls one agent's due `onTimeout`, in the scope of the wait that calls this, and says whether
1437
+ * it did. A stop or the scope's cancellation stops waiting on it.
1438
+ */
1439
+ async timeout(signal?: AbortSignal): Promise<boolean> {
1440
+ for (const agent of this.#logical.values()) {
1441
+ const onTimeout = agent.takeTimeout();
1442
+ if (!onTimeout) continue;
1443
+ try {
1444
+ await runUntilStopped(
1445
+ async () => asTimeoutHandler(() => onTimeout(agent as unknown as EventAgentRef)),
1446
+ signal,
1447
+ undefined,
1448
+ );
1449
+ } catch (error) {
1450
+ if (signal?.aborted) throw error;
1451
+ this.options.onLog?.(`awf: agent ${agent.key}'s onTimeout threw: ${messageOf(error)}`);
1452
+ }
1453
+ return true;
1454
+ }
1455
+ return false;
1456
+ }
1457
+ /** The batch a wait in a stage took, by stage, until the stage ends: what it must return. */
1458
+ readonly #stageBatches = new Map<string, WorkflowEvent<string, JsonValue>[]>();
1459
+ /** Stages around a wait this attempt entered: each is entered once, as a loop over one spins. */
1460
+ readonly #waitedStages = new Set<string>();
1461
+
1462
+ /** A wait: one at a time, and in a stage, the stage's batch. */
1463
+ private async receive(
1464
+ mailbox: RunMailbox,
1465
+ spec: ReceiveOptions | undefined,
1466
+ ): Promise<WorkflowEvent<string, JsonValue>[]> {
1467
+ const scope = this.#beginWait("receive()");
1468
+ if (scope?.stage !== undefined && this.#stageBatches.has(scope.stage))
1469
+ throw new Error(`stage ${scope.stage} has waited once; a stage holds one wait`);
1470
+ try {
1471
+ const taken = await this.#holdingWait(scope, (signal) =>
1472
+ mailbox.receive(spec, scope?.stage, signal),
1473
+ );
1474
+ if (scope?.stage !== undefined) {
1475
+ this.#stageBatches.set(scope.stage, taken);
1476
+ this.#waitedStages.add(scope.stage);
1477
+ }
1478
+ return taken;
1479
+ } catch (error) {
1480
+ // Parked in a stage: left unended now, before anything closes it, for the next attempt.
1481
+ if (error instanceof RunParked && scope?.stage !== undefined)
1482
+ this.options.stages.abandon(scope.stage);
1483
+ throw error;
1484
+ }
904
1485
  }
905
1486
 
906
1487
  /** An agent as messaging sees it, from its entry and its operations. */
@@ -913,6 +1494,73 @@ class WorkflowOwner {
913
1494
  return { state: agent.busy ? "working" : agent.hasRun ? "idle" : "not-started", ...caller };
914
1495
  }
915
1496
 
1497
+ /** Connects a participant outside the run under a key no agent holds and none reserved. */
1498
+ private connectParticipant(spec: ParticipantSpec): ParticipantRef {
1499
+ if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
1500
+ scopes.getStore()?.assertAccepting();
1501
+ if (typeof spec !== "object" || spec === null || typeof spec.key !== "string" || !spec.key)
1502
+ throw new Error("a participant is connected as { key, labels? }");
1503
+ const { key, labels } = spec;
1504
+ this.#assertFreeKey(key, "a participant");
1505
+ if (this.#agents.has(key) || this.#logical.has(key) || this.#caller?.key === key)
1506
+ throw new Error(`${key} is an agent's key; a participant takes a key of its own`);
1507
+ if (
1508
+ labels !== undefined &&
1509
+ (typeof labels !== "object" ||
1510
+ labels === null ||
1511
+ Array.isArray(labels) ||
1512
+ !isJsonValue(labels))
1513
+ )
1514
+ throw new Error("a participant's labels are a JSON object");
1515
+ const to = (spec as { to?: unknown }).to;
1516
+ if (to !== undefined && to !== "code")
1517
+ 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
+ );
1522
+ this.#messaging.connect(
1523
+ key,
1524
+ labels === undefined ? undefined : structuredClone(labels),
1525
+ to === "code" ? "code" : undefined,
1526
+ );
1527
+ const ref: ParticipantRef = {
1528
+ ...participantRef(key),
1529
+ release: async (reason) => {
1530
+ if (reason !== undefined && typeof reason !== "string")
1531
+ throw new Error("a release's reason is a string");
1532
+ return this.#messaging.release(key, reason);
1533
+ },
1534
+ };
1535
+ if (to === "code") {
1536
+ const routed = source(
1537
+ {
1538
+ ...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);
1543
+ },
1544
+ },
1545
+ { name: "participant:reply", from: key },
1546
+ ) as unknown as CodeParticipantRef;
1547
+ this.#outsideRefs.set(key, routed);
1548
+ return routed;
1549
+ }
1550
+ this.#outsideRefs.set(key, ref);
1551
+ return ref;
1552
+ }
1553
+
1554
+ /** A key an agent may not take: a reserved one, or an outside participant's. */
1555
+ #assertFreeKey(key: string, who: string): void {
1556
+ if ((RESERVED_PARTICIPANT_KEYS as readonly string[]).includes(key.toLowerCase()))
1557
+ throw new Error(
1558
+ `${key} is reserved: an agent could read a message from it as authority; ${who} takes another key`,
1559
+ );
1560
+ if (this.#messaging.isOutside(key))
1561
+ throw new Error(`${key} is a participant outside the run; ${who} takes another key`);
1562
+ }
1563
+
916
1564
  #messagesChanged(): void {
917
1565
  const change = this.#messagingChange;
918
1566
  this.#messagingChange = Promise.withResolvers<void>();
@@ -930,13 +1578,16 @@ class WorkflowOwner {
930
1578
  const end = (ref: unknown): string => {
931
1579
  const key =
932
1580
  typeof ref === "object" && ref !== null ? (ref as { key?: unknown }).key : undefined;
933
- const agent = typeof key === "string" ? this.#logical.get(key) : undefined;
934
- if (!agent || agent !== ref) {
1581
+ const issued =
1582
+ typeof key === "string"
1583
+ ? (this.#logical.get(key) ?? this.#outsideRefs.get(key))
1584
+ : undefined;
1585
+ if (!issued || issued !== ref) {
935
1586
  throw new Error(
936
- `a route's ends are agents this workflow opened; ${typeof key === "string" ? key : "this"} is not one`,
1587
+ `a route's ends are agents this workflow opened or participants it connected; ${typeof key === "string" ? key : "this"} is not one`,
937
1588
  );
938
1589
  }
939
- return agent.key;
1590
+ return issued.key;
940
1591
  };
941
1592
  if (typeof access !== "object" || access === null)
942
1593
  throw new Error("a route is { from, to } or { between }, with a purpose");
@@ -952,6 +1603,7 @@ class WorkflowOwner {
952
1603
  this.#messaging.grant(ends, {
953
1604
  purpose: access.purpose,
954
1605
  maxMessages: access.maxMessages,
1606
+ timeoutMs: access.timeoutMs,
955
1607
  scope: scope?.messages ?? this.#messaging.attempt,
956
1608
  });
957
1609
  }
@@ -1007,7 +1659,8 @@ class WorkflowOwner {
1007
1659
 
1008
1660
  /**
1009
1661
  * The workflow returned: its routes outside any stage take nothing new, and the message
1010
- * operations they opened are waited for, as a stage waits for its own.
1662
+ * operations they opened are waited for, as a stage waits for its own. A question open to an
1663
+ * outside participant keeps it waiting too; once none is, each one still connected is released.
1011
1664
  */
1012
1665
  async settleMessages(): Promise<void> {
1013
1666
  const attempt = this.#messaging.attempt;
@@ -1017,6 +1670,9 @@ class WorkflowOwner {
1017
1670
  const failed = settled.find((each) => each.status === "rejected");
1018
1671
  if (failed?.status === "rejected") throw failed.reason;
1019
1672
  });
1673
+ for (const key of this.#messaging.connected()) {
1674
+ this.#messaging.release(key, "the workflow returned");
1675
+ }
1020
1676
  }
1021
1677
 
1022
1678
  async close(deadline: AbsoluteDeadline): Promise<unknown[]> {
@@ -1033,7 +1689,9 @@ class WorkflowOwner {
1033
1689
  state
1034
1690
  .stopOperations({
1035
1691
  kind:
1036
- Date.now() >= this.options.deadline.unixMilliseconds ? "timed-out" : "cancelled",
1692
+ this.options.deadline && Date.now() >= this.options.deadline.unixMilliseconds
1693
+ ? "timed-out"
1694
+ : "cancelled",
1037
1695
  reason: "workflow closing",
1038
1696
  })
1039
1697
  .then(() => state.close("workflow complete")),
@@ -1119,7 +1777,19 @@ class WorkflowOwner {
1119
1777
  });
1120
1778
  if (entered.kind === "reuse") {
1121
1779
  entered.release();
1122
- return entered.value;
1780
+ // A stage around a wait returns the batch it took, which its record doesn't hold.
1781
+ const batch =
1782
+ entered.value === undefined && !options
1783
+ ? await this.options.mailbox?.batchOf(name)
1784
+ : undefined;
1785
+ if (batch) {
1786
+ if (this.#waitedStages.has(name))
1787
+ throw new Error(
1788
+ `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`,
1789
+ );
1790
+ this.#waitedStages.add(name);
1791
+ }
1792
+ return (batch as JsonValue | undefined) ?? entered.value;
1123
1793
  }
1124
1794
  const open = entered.stage;
1125
1795
  const messages = this.#messaging.scope(name);
@@ -1137,7 +1807,22 @@ class WorkflowOwner {
1137
1807
  this.#messaging.finishing(messages);
1138
1808
  scope.seal();
1139
1809
  await this.drainMessages(messages, () => scope.settleOwned());
1810
+ // A timer it set is written before its record, which a rerun reuses without its work.
1811
+ await this.options.waits.flush();
1140
1812
  if (this.options.stages.stopped?.stage === name) throw this.options.stages.caught();
1813
+ const batch = this.#stageBatches.get(name);
1814
+ if (batch && !options) {
1815
+ const same =
1816
+ Array.isArray(returned) &&
1817
+ returned.length === batch.length &&
1818
+ returned.every((event, i) => (event as { id?: unknown })?.id === batch[i]?.id);
1819
+ if (!same)
1820
+ throw new Error(
1821
+ `stage ${name} returned other events than its wait took; its work is the wait alone`,
1822
+ );
1823
+ await open.succeed(undefined);
1824
+ return batch as JsonValue;
1825
+ }
1141
1826
  const value = stageValue(name, options, returned);
1142
1827
  await open.succeed(value, summaryOf(name, options, value, this.options.onLog));
1143
1828
  return value;
@@ -1149,12 +1834,15 @@ class WorkflowOwner {
1149
1834
  }).catch(() => undefined);
1150
1835
  const stopped = this.options.stages.stopped;
1151
1836
  if (stopped && error === stopped && stopped.stage === name) await open.stop(stopped.reason);
1837
+ // Parked at a wait in it: not ended, so the next attempt runs it again.
1838
+ else if (error instanceof RunParked) this.options.stages.abandon(name);
1152
1839
  else {
1153
1840
  this.options.stages.failedIn(name, error);
1154
1841
  await open.fail(messageOf(error));
1155
1842
  }
1156
1843
  throw error;
1157
1844
  } finally {
1845
+ this.#stageBatches.delete(name);
1158
1846
  this.#messaging.closeScope(messages);
1159
1847
  this.#stageScopes.delete(messages);
1160
1848
  removeFromParent?.();
@@ -1164,6 +1852,7 @@ class WorkflowOwner {
1164
1852
  private openAgent(spec: AgentOpenSpec): Promise<AgentRef> {
1165
1853
  if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
1166
1854
  scopes.getStore()?.assertAccepting();
1855
+ this.#assertFreeKey(spec.key, "an agent");
1167
1856
  if (this.#caller?.key === spec.key) {
1168
1857
  throw new Error(`agent ${spec.key} is the calling session; it is not opened`);
1169
1858
  }
@@ -1172,6 +1861,12 @@ class WorkflowOwner {
1172
1861
  throw new Error(`agent ${spec.key}: ${RUN_SANDBOX_ONLY}`);
1173
1862
  const existing = this.#agents.get(spec.key);
1174
1863
  if (existing?.stopped !== undefined) throw stoppedError(spec.key, existing.stopped);
1864
+ const given = (spec as EventAgentOpenSpec).keepAlive;
1865
+ if (given !== undefined && !this.options.mailbox)
1866
+ throw new Error(
1867
+ `agent ${spec.key}: keepAlive is for a workflow with events, whose runs park; this one's never do`,
1868
+ );
1869
+ const keepAlive = given === undefined ? undefined : checkedKeepAlive(spec.key, given);
1175
1870
  const execution = existing
1176
1871
  ? constrainExistingExecution(spec.runtime, existing.identity.execution)
1177
1872
  : withKnownEffort(resolveExecution(spec.runtime, this.options.runtime));
@@ -1198,6 +1893,8 @@ class WorkflowOwner {
1198
1893
  assertDeadline(effectiveDeadline);
1199
1894
  if (existing) {
1200
1895
  assertCompatibleAgent(spec.key, existing.identity, identity, spec);
1896
+ this.#tools.reopen(spec.key, spec.tools);
1897
+ if (keepAlive) existing.keepAlive = keepAlive;
1201
1898
  const seated =
1202
1899
  spec.sandbox === undefined
1203
1900
  ? existing.state
@@ -1209,7 +1906,20 @@ class WorkflowOwner {
1209
1906
  return attached;
1210
1907
  }
1211
1908
 
1909
+ this.#tools.give(spec.key, spec.tools ?? {});
1212
1910
  this.options.progress.agentOpened(spec.key, scope?.group);
1911
+ const found = this.options.keptSessions?.get(spec.key);
1912
+ // Reopened only where it can be: a key the code now opens on another harness starts afresh, and
1913
+ // so does an agent with a home of its own, skills or a sandbox, made anew at every open, which
1914
+ // its kept session isn't in. Either is given its instructions again.
1915
+ const homeOfItsOwn = spec.skills !== undefined || spec.sandbox !== undefined || inRunSandbox;
1916
+ const kept = found?.harness === execution.harness && !homeOfItsOwn ? found : undefined;
1917
+ if (found && !kept)
1918
+ this.options.onLog?.(
1919
+ homeOfItsOwn
1920
+ ? `agent ${spec.key} has skills or a sandbox, whose home is made anew; it starts a new session`
1921
+ : `agent ${spec.key} now runs ${execution.harness}, not the ${found.harness} its session was kept on; it starts a new session`,
1922
+ );
1213
1923
  // Opened before the session so registration below stays synchronous: two concurrent `agent()`
1214
1924
  // calls for one key must not each build an agent.
1215
1925
  const sessions: AgentSessions = { launcher: new Set(), forks: [] };
@@ -1228,10 +1938,13 @@ class WorkflowOwner {
1228
1938
  ...(identity.skills ? { skills: identity.skills } : {}),
1229
1939
  activation: (cwd, seated, skills, home) => ({
1230
1940
  key: spec.key,
1231
- deadline: effectiveDeadline,
1941
+ deadline: harnessDeadline(effectiveDeadline),
1232
1942
  cwd,
1233
1943
  execution,
1234
- ...(spec.instructions === undefined ? {} : { instructions: spec.instructions }),
1944
+ // A kept session already holds the instructions; they are not sent again.
1945
+ ...(kept ? { continues: kept } : {}),
1946
+ ...(spec.instructions === undefined || kept ? {} : { instructions: spec.instructions }),
1947
+ ...(this.options.mailbox ? { paneAtFirstTurn: true as const } : {}),
1235
1948
  ...(spec.labels === undefined ? {} : { labels: spec.labels }),
1236
1949
  ...(seated ? { occupant: seated.occupant } : {}),
1237
1950
  ...(seated?.webSearch ? { webSearch: true } : {}),
@@ -1263,6 +1976,7 @@ class WorkflowOwner {
1263
1976
  (opened) => opened,
1264
1977
  () => undefined,
1265
1978
  ),
1979
+ ...(keepAlive ? { keepAlive } : {}),
1266
1980
  });
1267
1981
  const activated = this.track(waitForDeadline(ownedState, effectiveDeadline));
1268
1982
  scope?.track(activated);
@@ -1275,6 +1989,8 @@ class WorkflowOwner {
1275
1989
  */
1276
1990
  private stopAgent(key: string, reason?: string): Promise<boolean> {
1277
1991
  if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
1992
+ const inTool = refusedInTool(key, "agents.stop");
1993
+ if (inTool) throw inTool;
1278
1994
  const scope = scopes.getStore();
1279
1995
  scope?.assertAccepting();
1280
1996
  this.options.stages.checkOperation();
@@ -1334,6 +2050,7 @@ class WorkflowOwner {
1334
2050
  if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
1335
2051
  const scope = scopes.getStore();
1336
2052
  scope?.assertAccepting();
2053
+ this.#assertFreeKey(key, "an agent");
1337
2054
  const found = this.options.runtime.host.caller;
1338
2055
  if (!found) return Promise.resolve(null);
1339
2056
  if (this.#caller) {
@@ -1371,7 +2088,7 @@ class WorkflowOwner {
1371
2088
  );
1372
2089
  const session = await this.options.host.openAgent({
1373
2090
  key,
1374
- deadline: this.options.deadline,
2091
+ deadline: harnessDeadline(this.options.deadline),
1375
2092
  cwd: found.cwd,
1376
2093
  execution,
1377
2094
  });
@@ -1659,6 +2376,7 @@ class WorkflowOwner {
1659
2376
  key,
1660
2377
  runDir: this.options.runDir,
1661
2378
  messaging: this.#messaging,
2379
+ tools: this.#tools,
1662
2380
  messageOwner: (scope) => this.#messageOwner(scope),
1663
2381
  execution,
1664
2382
  session,
@@ -1679,8 +2397,12 @@ class WorkflowOwner {
1679
2397
  this.options.failRun(error);
1680
2398
  },
1681
2399
  livenessPolicy: this.options.livenessPolicy,
2400
+ ...(this.options.mailbox ? { turns: this.#turnsOf(key, this.options.mailbox) } : {}),
1682
2401
  fork: (spec, take, settings) => this.forkAgent(key, spec, take, settings),
1683
2402
  keepPane: () => this.#agents.get(key)?.identity.keepPane,
2403
+ keepAlive: () => this.#agents.get(key)?.keepAlive,
2404
+ defaultWindowMs: this.options.defaultWindowMs ?? DEFAULT_WINDOW_MS,
2405
+ now: this.options.now,
1684
2406
  // Its socket is its authority: a kept agent's harness lives on, and must not answer.
1685
2407
  revoke: () =>
1686
2408
  this.#agents
@@ -1723,12 +2445,17 @@ class WorkflowOwner {
1723
2445
  if (this.#caller?.key === spec.key) {
1724
2446
  throw new Error(`agent ${spec.key} is the calling session; it is not opened`);
1725
2447
  }
1726
- const forkSpec = structuredClone(spec);
2448
+ this.#assertFreeKey(spec.key, "an agent");
2449
+ // 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);
1727
2452
  if (forkSpec.layout !== undefined) forkSpec.layout = storedLayout(forkSpec.layout);
1728
2453
  const deadline = scope?.deadline ?? this.options.deadline;
1729
2454
  assertDeadline(deadline);
1730
2455
  // A fork of the calling session is one whichever way it was asked for.
1731
2456
  const from = unopened || this.#caller?.key === parentKey ? CALLING : parentKey;
2457
+ if (from === CALLING && tools !== undefined)
2458
+ throw new Error(`agent ${spec.key}: a fork of the calling session takes no tools`);
1732
2459
  const existing = this.#agents.get(spec.key);
1733
2460
  if (existing) {
1734
2461
  if (existing.stopped !== undefined) throw stoppedError(spec.key, existing.stopped);
@@ -1736,6 +2463,7 @@ class WorkflowOwner {
1736
2463
  if (forked?.parent !== from || !isDeepStrictEqual(forked.spec, forkSpec)) {
1737
2464
  throw new Error(`agent ${spec.key} is already open, not as this fork of ${from}`);
1738
2465
  }
2466
+ this.#tools.reopen(spec.key, tools);
1739
2467
  const attached = this.track(waitForDeadline(existing.state, deadline));
1740
2468
  scope?.track(attached);
1741
2469
  return attached;
@@ -1743,6 +2471,8 @@ class WorkflowOwner {
1743
2471
  const parent = unopened ?? this.#agents.get(parentKey)!;
1744
2472
  const opening = parent.opening!;
1745
2473
  if (typeof take === "string") throw new Error(take);
2474
+ // The parent's tools, with the handlers they have now, unless it gives its own.
2475
+ this.#tools.give(spec.key, tools ?? this.#tools.of(parentKey));
1746
2476
  const sandboxed = parent.identity.sandbox !== undefined || this.options.sandboxes.hasRunSandbox;
1747
2477
  // A parent with a harness home of its own has its session copied out, and the fork made in the
1748
2478
  // child's home: anything added to the parent's would be read as the parent's.
@@ -1829,7 +2559,7 @@ class WorkflowOwner {
1829
2559
  ...(sandboxed && copy ? { carried: { parent: parentKey, forked, copy } } : {}),
1830
2560
  activation: (cwd, seated, given, home) => ({
1831
2561
  key: spec.key,
1832
- deadline,
2562
+ deadline: harnessDeadline(deadline),
1833
2563
  cwd,
1834
2564
  execution,
1835
2565
  ...(forkSpec.instructions === undefined ? {} : { instructions: forkSpec.instructions }),
@@ -1937,7 +2667,7 @@ class WorkflowOwner {
1937
2667
  */
1938
2668
  private besideFallback(
1939
2669
  layout: PaneLayout | undefined,
1940
- deadline: AbsoluteDeadline,
2670
+ deadline: AbsoluteDeadline | undefined,
1941
2671
  ): () => Promise<string | undefined> {
1942
2672
  const known = (reason: string | undefined) => () => Promise.resolve(reason);
1943
2673
  const target = besideOf(layout);
@@ -2030,12 +2760,17 @@ class LogicalAgent implements AgentRef {
2030
2760
  #hasRun = false;
2031
2761
  /** Supervisors of its open operation, woken when something arrives for it. */
2032
2762
  readonly #watchers = new Set<() => void>();
2763
+ /** When its last turn this attempt that opens a window ended. */
2764
+ #lastEnd: number | undefined;
2765
+ /** A turn that opens a window ended since its `onTimeout` was last called. */
2766
+ #timeoutOwed = false;
2033
2767
 
2034
2768
  constructor(
2035
2769
  private readonly options: {
2036
2770
  key: string;
2037
2771
  runDir: string;
2038
2772
  messaging: Messaging;
2773
+ tools: ToolCalls<ExecutionScope>;
2039
2774
  /**
2040
2775
  * What owns a message operation a route in `scope` opens: its stage, or the attempt outside
2041
2776
  * any stage. Undefined once that scope has closed: nothing opens for it then.
@@ -2049,7 +2784,7 @@ class LogicalAgent implements AgentRef {
2049
2784
  endpoint: string;
2050
2785
  /** The path this agent is told to run; see `installAgentLauncher`. */
2051
2786
  launcher: string;
2052
- deadline: AbsoluteDeadline;
2787
+ deadline: AbsoluteDeadline | undefined;
2053
2788
  ledger: AgentLedger;
2054
2789
  progress: RunProgress;
2055
2790
  recordTurn: RecordTurn;
@@ -2062,6 +2797,7 @@ class LogicalAgent implements AgentRef {
2062
2797
  livenessPolicy?: LivenessPolicy;
2063
2798
  /** After each operation settles, whatever its outcome. */
2064
2799
  afterOperation?(): Promise<void>;
2800
+ turns?: LogicalAgentTurns;
2065
2801
  /**
2066
2802
  * Opens `spec`'s agent on the native fork `take` queues in this agent's operations, at
2067
2803
  * `settings`, this agent's once the operations before the fork have run.
@@ -2073,11 +2809,27 @@ class LogicalAgent implements AgentRef {
2073
2809
  ): Promise<AgentRef>;
2074
2810
  /** As the workflow wrote it; decides, at close, whether its pane stays. */
2075
2811
  keepPane(): KeepPane | undefined;
2812
+ /** As the workflow's last open of it gave it. */
2813
+ keepAlive(): KeepAlive | undefined;
2814
+ /** Its window when `keepAlive` gives no `timeoutMs`. */
2815
+ defaultWindowMs: number;
2816
+ /** The run's time, which its window is measured on. */
2817
+ now: () => number;
2076
2818
  /** Closes its result channel, once its pane is kept. */
2077
2819
  revoke(): Promise<void> | undefined;
2078
2820
  },
2079
2821
  ) {
2080
2822
  this.#execution = options.execution;
2823
+ asSource(this, { name: "agent:ended", from: options.key });
2824
+ // A turn an earlier attempt started whose end no wait took is this agent's turn again.
2825
+ const restored = options.turns?.restored();
2826
+ if (restored)
2827
+ this.#started = {
2828
+ ref: turnRef(options.key, restored.turn, async () => false),
2829
+ meta: restored.meta,
2830
+ ended: true,
2831
+ done: Promise.resolve(),
2832
+ };
2081
2833
  }
2082
2834
 
2083
2835
  get key(): string {
@@ -2109,21 +2861,173 @@ class LogicalAgent implements AgentRef {
2109
2861
  return this.#closed ? (this.#closedReason ?? "closed") : undefined;
2110
2862
  }
2111
2863
 
2864
+ /** The end of its window while one is open; a closed agent holds none. */
2865
+ liveUntil(now = this.options.now()): number | undefined {
2866
+ const keepAlive = this.options.keepAlive();
2867
+ if (this.#closed || !keepAlive || this.#lastEnd === undefined) return undefined;
2868
+ const until = this.#lastEnd + (keepAlive.timeoutMs ?? this.options.defaultWindowMs);
2869
+ return until > now ? until : undefined;
2870
+ }
2871
+
2872
+ /**
2873
+ * Its `onTimeout`, once its window has passed with it idle and a turn owed it, taken: it isn't
2874
+ * due again until another turn ends.
2875
+ */
2876
+ takeTimeout(now = this.options.now()): KeepAlive["onTimeout"] {
2877
+ const onTimeout = this.options.keepAlive()?.onTimeout;
2878
+ if (!onTimeout || !this.#timeoutOwed || this.#closed || this.busy || this.#started)
2879
+ return undefined;
2880
+ if (this.liveUntil(now) !== undefined) return undefined;
2881
+ this.#timeoutOwed = false;
2882
+ return onTimeout;
2883
+ }
2884
+
2112
2885
  /**
2113
2886
  * Something arrived for it: its open operation's supervisor looks again, and an agent with
2114
2887
  * nothing queued wakes for a held message, in a message operation.
2115
2888
  */
2116
2889
  messagesChanged(): void {
2117
2890
  for (const wake of this.#watchers) wake();
2118
- if (this.#queued > 0 || this.#closed || this.options.isRunClosing()) return;
2891
+ // A started turn holds messages until a wait has taken its end.
2892
+ if (this.#queued > 0 || this.#started || this.#closed || this.options.isRunClosing()) return;
2119
2893
  const opening = this.options.messaging.opening(this.key);
2120
2894
  if (opening) this.startMessageOperation(opening.scope);
2121
2895
  }
2122
2896
 
2123
- enqueue(_spec: AgentTextTurnSpec): Promise<never>;
2124
- enqueue<T extends JsonValue>(_spec: AgentStructuredTurnSpec<T>): Promise<never>;
2125
- enqueue(): Promise<never> {
2126
- return unavailable("AgentRef.enqueue");
2897
+ /** The turn `start()` began, until a wait takes its end. */
2898
+ #started: { ref: StartedTurn; meta: JsonValue; ended: boolean; done: Promise<void> } | undefined;
2899
+ /** While `start()` hands its turn to `run()`, which refuses one otherwise. */
2900
+ #launching = false;
2901
+ /** Each running turn's own stop, by its id: what a started turn's `cancel()` stops. */
2902
+ readonly #stopsByTurn = new Map<string, (stop: OperationStop) => Promise<void>>();
2903
+ /** Started turns cancelled while still queued behind other work: they settle cancelled. */
2904
+ readonly #cancelledTurns = new Map<string, string>();
2905
+
2906
+ get turn(): StartedTurn | null {
2907
+ return this.#started?.ref ?? null;
2908
+ }
2909
+
2910
+ /** Whether a started turn is set: messages wait, and awaited replies are refused. */
2911
+ get hasStartedTurn(): boolean {
2912
+ return this.#started !== undefined;
2913
+ }
2914
+
2915
+ /** A wait took this turn's end: the agent takes other work again. */
2916
+ endTaken(turn: TurnId): void {
2917
+ if (this.#started?.ref.id !== turn) return;
2918
+ this.#started = undefined;
2919
+ this.messagesChanged();
2920
+ }
2921
+
2922
+ /** Cancels a started turn still running and waits for its end: the code returned or threw. */
2923
+ async endStarted(reason: string): Promise<void> {
2924
+ const started = this.#started;
2925
+ if (!started || started.ended) return;
2926
+ await started.ref.cancel(reason);
2927
+ }
2928
+
2929
+ async start(spec: AgentStartSpec): Promise<StartedTurn> {
2930
+ const { turns } = this.options;
2931
+ if (!turns) return unavailable("AgentRef.start");
2932
+ if (this.#closed) throw new Error("logical agent is closed");
2933
+ const inTool = refusedInTool(this.key, "start()");
2934
+ if (inTool) throw inTool;
2935
+ const scope = scopes.getStore();
2936
+ if (scope !== undefined)
2937
+ throw new Error(
2938
+ `agent ${this.key}: start() is called at the run's root, as a turn not awaited would outlive a stage or a parallel`,
2939
+ );
2940
+ const refused = this.#refusedWhileStarted("start");
2941
+ if (refused) throw refused;
2942
+ const { meta = null, ...run } = spec;
2943
+ const id = run.id ?? randomUUID();
2944
+ if (!isJsonValue(meta)) throw new Error("a turn's meta is a JSON value");
2945
+ const ended = Promise.withResolvers<void>();
2946
+ let written: TurnOutcome<JsonValue>["kind"] | undefined;
2947
+ 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
+ }),
2961
+ meta,
2962
+ ended: false,
2963
+ done: ended.promise,
2964
+ };
2965
+ // Claimed before anything is awaited, so a second start() at once is refused.
2966
+ this.#started = started;
2967
+ try {
2968
+ // Recorded before the agent is launched: a crash between leaves a turn a continue ends.
2969
+ await turns.begin(id, meta, this.options.execution);
2970
+ } catch (error) {
2971
+ this.#started = undefined;
2972
+ // A message held while it began is taken now.
2973
+ this.messagesChanged();
2974
+ throw error;
2975
+ }
2976
+ this.#launching = true;
2977
+ let launched: Promise<RunResult<JsonValue>>;
2978
+ try {
2979
+ launched = outsideTools(() => this.run({ ...run, id } as AgentRunStructuredSpec<JsonValue>));
2980
+ } catch (error) {
2981
+ launched = Promise.reject(error);
2982
+ } finally {
2983
+ this.#launching = false;
2984
+ }
2985
+ // Tracked until the end is in the mailbox, so the run doesn't park with a turn's end on its way.
2986
+ this.options.track(
2987
+ launched
2988
+ .then(
2989
+ ({ outcome }) => outcome,
2990
+ (error: unknown): TurnOutcome<JsonValue> => ({
2991
+ answered: false,
2992
+ kind: "failed",
2993
+ reason: messageOf(error),
2994
+ retryable: false,
2995
+ usage: unrunUsage(this.key, id, this.options.execution),
2996
+ }),
2997
+ )
2998
+ .then(async (outcome) => {
2999
+ await turns.end(id, meta, outcome);
3000
+ written = outcome.kind;
3001
+ })
3002
+ // An end that can't be written leaves the turn set for good: the run can't go on.
3003
+ .catch((error: unknown) =>
3004
+ this.options.cleanupUnresolved(
3005
+ `agent ${this.key}'s turn ${id} ended, and its end was not written: ${messageOf(error)}`,
3006
+ ),
3007
+ )
3008
+ .finally(() => {
3009
+ started.ended = true;
3010
+ ended.resolve();
3011
+ }),
3012
+ );
3013
+ return started.ref;
3014
+ }
3015
+
3016
+ #refusedWhileStarted(what: string): Error | undefined {
3017
+ // First: from a handler this agent is in, its turn waits for the handler, started or not.
3018
+ const inTool = refusedInTool(this.key, `${what}()`);
3019
+ if (inTool) return inTool;
3020
+ if (this.#started && !this.#launching)
3021
+ return new Error(
3022
+ `agent ${this.key} has a turn started (${this.#started.ref.id}) whose end no wait has taken; ${what}() waits for it`,
3023
+ );
3024
+ }
3025
+
3026
+ is<Meta extends JsonValue = JsonValue, T extends JsonValue = JsonValue>(event: {
3027
+ readonly name: string;
3028
+ readonly from: string;
3029
+ }): event is AgentEndedEvent<Meta, T> & Matched {
3030
+ return event.name === "agent:ended" && event.from === this.key;
2127
3031
  }
2128
3032
 
2129
3033
  run(spec: AgentRunTextSpec): Promise<RunResult<string>>;
@@ -2132,6 +3036,8 @@ class LogicalAgent implements AgentRef {
2132
3036
  spec: AgentRunTextSpec | AgentRunStructuredSpec<T>,
2133
3037
  ): Promise<RunResult<string | T>> {
2134
3038
  if (this.#closed) return Promise.reject(new Error("logical agent is closed"));
3039
+ const refused = this.#refusedWhileStarted("run");
3040
+ if (refused) return Promise.reject(refused);
2135
3041
  const scope = scopes.getStore();
2136
3042
  try {
2137
3043
  scope?.assertAccepting();
@@ -2139,6 +3045,7 @@ class LogicalAgent implements AgentRef {
2139
3045
  } catch (error) {
2140
3046
  return Promise.reject(error);
2141
3047
  }
3048
+ const opensWindow = !inTimeoutHandler();
2142
3049
  const id = spec.id ?? randomUUID();
2143
3050
  const completeSpec = structuredClone({ ...spec, id }) as
2144
3051
  | AgentRunTextSpec
@@ -2155,7 +3062,7 @@ class LogicalAgent implements AgentRef {
2155
3062
  scope?.track(existing.result);
2156
3063
  return existing.result as Promise<RunResult<string | T>>;
2157
3064
  }
2158
- let deadline: AbsoluteDeadline;
3065
+ let deadline: AbsoluteDeadline | undefined;
2159
3066
  try {
2160
3067
  deadline = this.operationDeadline(spec, scope);
2161
3068
  const recoveryDeadline = spec.nudge ? spec.nudge.deadline : undefined;
@@ -2173,13 +3080,24 @@ class LogicalAgent implements AgentRef {
2173
3080
  throw new Error("logical agent is closed");
2174
3081
  }
2175
3082
  scope?.assertActive();
3083
+ const cancelled = this.#cancelledTurns.get(id);
3084
+ if (cancelled !== undefined) {
3085
+ this.#cancelledTurns.delete(id);
3086
+ return {
3087
+ outcome: {
3088
+ kind: "cancelled",
3089
+ reason: cancelled,
3090
+ usage: unrunUsage(this.key, id, this.options.execution),
3091
+ },
3092
+ } as RunResult<JsonValue>;
3093
+ }
2176
3094
  const tags = tagsOf(scope, completeSpec.label);
2177
3095
  // Drawn here, so a turn that throws before it is delivered is still written as failed.
2178
3096
  const operation = this.operationLog(randomUUID(), "turn", tags);
2179
3097
  this.options.progress.turnStarted(this.key, tags);
2180
3098
  try {
2181
3099
  const settled = await this.executeOperation(completeSpec, scope, deadline, operation);
2182
- this.turnEnded(settled.outcome, "turn");
3100
+ this.turnEnded(settled.outcome, "turn", opensWindow);
2183
3101
  return settled;
2184
3102
  } catch (error) {
2185
3103
  operation.failed(error);
@@ -2197,6 +3115,8 @@ class LogicalAgent implements AgentRef {
2197
3115
 
2198
3116
  compact(spec: CompactSpec): Promise<TurnOutcome<string>> {
2199
3117
  if (this.#closed) return Promise.reject(new Error("logical agent is closed"));
3118
+ const refused = this.#refusedWhileStarted("compact");
3119
+ if (refused) return Promise.reject(refused);
2200
3120
  const scope = scopes.getStore();
2201
3121
  try {
2202
3122
  scope?.assertAccepting();
@@ -2220,7 +3140,7 @@ class LogicalAgent implements AgentRef {
2220
3140
  }
2221
3141
  let deadline: AbsoluteDeadline;
2222
3142
  try {
2223
- deadline = this.operationDeadline(spec, scope);
3143
+ deadline = harnessDeadline(this.operationDeadline(spec, scope));
2224
3144
  } catch (error) {
2225
3145
  const rejected = Promise.reject<TurnOutcome<string>>(error);
2226
3146
  scope?.track(rejected);
@@ -2255,6 +3175,8 @@ class LogicalAgent implements AgentRef {
2255
3175
  /** Not a turn: progress counts none for it, and it asks the model nothing. */
2256
3176
  set(spec: SettingsSpec): Promise<TurnOutcome<null>> {
2257
3177
  if (this.#closed) return Promise.reject(new Error("logical agent is closed"));
3178
+ const refused = this.#refusedWhileStarted("set");
3179
+ if (refused) return Promise.reject(refused);
2258
3180
  const scope = scopes.getStore();
2259
3181
  try {
2260
3182
  scope?.assertAccepting();
@@ -2279,7 +3201,7 @@ class LogicalAgent implements AgentRef {
2279
3201
  let deadline: AbsoluteDeadline;
2280
3202
  try {
2281
3203
  change = this.settingsChange(spec);
2282
- deadline = this.operationDeadline(spec, scope);
3204
+ deadline = harnessDeadline(this.operationDeadline(spec, scope));
2283
3205
  } catch (error) {
2284
3206
  const rejected = Promise.reject<TurnOutcome<null>>(error);
2285
3207
  scope?.track(rejected);
@@ -2363,7 +3285,16 @@ class LogicalAgent implements AgentRef {
2363
3285
  }
2364
3286
 
2365
3287
  /** A turn or compaction that settled, as the view shows it and `turns.jsonl` keeps it. */
2366
- private turnEnded(outcome: TurnOutcome<JsonValue>, kind: TurnRecord["kind"]): void {
3288
+ private turnEnded(
3289
+ outcome: TurnOutcome<JsonValue>,
3290
+ kind: TurnRecord["kind"],
3291
+ opensWindow = false,
3292
+ ): void {
3293
+ // A failure has closed it by now, and a closed agent holds no window.
3294
+ if (opensWindow && !this.#closed) {
3295
+ this.#lastEnd = this.options.now();
3296
+ this.#timeoutOwed = true;
3297
+ }
2367
3298
  this.#last = outcome.kind;
2368
3299
  const reason = "reason" in outcome ? outcome.reason : undefined;
2369
3300
  this.options.progress.turnSettled(this.key, outcome.kind, reason);
@@ -2372,6 +3303,8 @@ class LogicalAgent implements AgentRef {
2372
3303
 
2373
3304
  fork(spec: AgentForkSpec): Promise<AgentRef> {
2374
3305
  if (this.#closed) return Promise.reject(new Error("logical agent is closed"));
3306
+ const refused = this.#refusedWhileStarted("fork");
3307
+ if (refused) return Promise.reject(refused);
2375
3308
  const scope = scopes.getStore();
2376
3309
  const deadline = this.deadlineCeiling(scope);
2377
3310
  const { session, execution } = this.options;
@@ -2419,7 +3352,7 @@ class LogicalAgent implements AgentRef {
2419
3352
  private operationDeadline(
2420
3353
  spec: { deadline?: AbsoluteDeadline; timeoutMs?: number },
2421
3354
  scope: ExecutionScope | undefined,
2422
- ): AbsoluteDeadline {
3355
+ ): AbsoluteDeadline | undefined {
2423
3356
  if (spec.deadline && spec.timeoutMs !== undefined) {
2424
3357
  throw new Error("an operation cannot specify both deadline and timeoutMs");
2425
3358
  }
@@ -2430,8 +3363,9 @@ class LogicalAgent implements AgentRef {
2430
3363
  return earlierDeadline(spec.deadline, ceiling);
2431
3364
  }
2432
3365
 
2433
- private deadlineCeiling(scope: ExecutionScope | undefined): AbsoluteDeadline {
2434
- return scope ? earlierDeadline(scope.deadline, this.options.deadline) : this.options.deadline;
3366
+ /** The deadlines around an operation: its scope's and the run's, if either has one. */
3367
+ private deadlineCeiling(scope: ExecutionScope | undefined): AbsoluteDeadline | undefined {
3368
+ return earlierDeadline(scope?.deadline, this.options.deadline);
2435
3369
  }
2436
3370
 
2437
3371
  /** Its settings once every `set` queued so far has run. */
@@ -2490,7 +3424,12 @@ class LogicalAgent implements AgentRef {
2490
3424
  const usage = () =>
2491
3425
  entry.settle({ settledAt: Math.min(Date.now(), deadline.unixMilliseconds) }, []);
2492
3426
  if (Date.now() >= deadline.unixMilliseconds) {
2493
- return { kind: "timed-out", reason: "settings deadline exceeded", usage: usage() };
3427
+ return {
3428
+ answered: false,
3429
+ kind: "timed-out",
3430
+ reason: "settings deadline exceeded",
3431
+ usage: usage(),
3432
+ };
2494
3433
  }
2495
3434
  const next: AgentExecution = { ...this.#execution, ...change };
2496
3435
  const pane = placementOf(next) === "pane";
@@ -2518,15 +3457,16 @@ class LogicalAgent implements AgentRef {
2518
3457
  cancelled !== undefined ? "cancelled" : expired ? "timed-out" : "failed",
2519
3458
  );
2520
3459
  }
2521
- if (cancelled !== undefined) return { kind: "cancelled", reason, usage: usage() };
3460
+ if (cancelled !== undefined)
3461
+ return { answered: false, kind: "cancelled", reason, usage: usage() };
2522
3462
  return expired
2523
- ? { kind: "timed-out", reason, usage: usage() }
2524
- : { kind: "failed", reason, retryable: false, usage: usage() };
3463
+ ? { answered: false, kind: "timed-out", reason, usage: usage() }
3464
+ : { answered: false, kind: "failed", reason, retryable: false, usage: usage() };
2525
3465
  } finally {
2526
3466
  removeCanceller?.();
2527
3467
  }
2528
3468
  this.#execution = next;
2529
- return { kind: "answered", value: null, usage: usage() };
3469
+ return { answered: true, kind: "answered", value: null, usage: usage() };
2530
3470
  }
2531
3471
 
2532
3472
  /**
@@ -2619,7 +3559,7 @@ class LogicalAgent implements AgentRef {
2619
3559
  private async executeOperation(
2620
3560
  spec: AgentRunTextSpec | AgentRunStructuredSpec<JsonValue>,
2621
3561
  scope: ExecutionScope | undefined,
2622
- operationDeadline: AbsoluteDeadline,
3562
+ operationDeadline: AbsoluteDeadline | undefined,
2623
3563
  operation: OperationLog,
2624
3564
  ): Promise<RunResult<JsonValue>> {
2625
3565
  const nudge = spec.nudge === false ? undefined : (spec.nudge ?? this.defaultNudge());
@@ -2631,7 +3571,7 @@ class LogicalAgent implements AgentRef {
2631
3571
  private async executeWaitingOperation(
2632
3572
  spec: AgentRunTextSpec | AgentRunStructuredSpec<JsonValue>,
2633
3573
  scope: ExecutionScope | undefined,
2634
- deadline: AbsoluteDeadline,
3574
+ initial: AbsoluteDeadline | undefined,
2635
3575
  { operationId, tags, delivered, session }: OperationLog,
2636
3576
  nudge: Exclude<AgentRunTextSpec["nudge"], false>,
2637
3577
  ): Promise<RunResult<JsonValue>> {
@@ -2639,14 +3579,25 @@ class LogicalAgent implements AgentRef {
2639
3579
  const binding = { endpoint: this.options.endpoint, operationId };
2640
3580
  const entry = this.options.ledger.reserve(operationId, () => this.#execution, tags);
2641
3581
  const { messaging } = this.options;
3582
+ // Moves out by any wait on an outside participant, within the deadlines around it.
3583
+ let deadline = initial;
3584
+ const ceiling = this.deadlineCeiling(scope);
2642
3585
  messaging.opened({
2643
3586
  id: operationId,
2644
3587
  agent: this.key,
2645
3588
  kind: "workflow",
2646
- deadline: deadline.unixMilliseconds,
2647
- ceiling: this.options.deadline.unixMilliseconds,
3589
+ ...(deadline ? { deadline: deadline.unixMilliseconds } : {}),
3590
+ ...(ceiling ? { ceiling: ceiling.unixMilliseconds } : {}),
2648
3591
  scope: scope?.messages ?? messaging.attempt,
2649
3592
  });
3593
+ this.options.tools.opened({
3594
+ id: operationId,
3595
+ agent: this.key,
3596
+ callPath: [],
3597
+ ...(scope?.stage === undefined ? {} : { stage: scope.stage }),
3598
+ scope,
3599
+ deadline: () => deadline?.unixMilliseconds,
3600
+ });
2650
3601
  // After its prompt: how to use its routes, then each message held for it, wrapped.
2651
3602
  const joined = messaging.take(operationId, "joins");
2652
3603
  const prompt = [spec.prompt, messaging.lesson(this.key), joined?.text]
@@ -2663,7 +3614,7 @@ class LogicalAgent implements AgentRef {
2663
3614
  const recoveryPrompt =
2664
3615
  nudge?.prompt ??
2665
3616
  (waitingSupported
2666
- ? "Report the result now, or use wf waiting with a reason if you still need time."
3617
+ ? "Report the result now, or use wfa waiting with a reason if you still need time."
2667
3618
  : "You finished without reporting the requested result. Report it now.");
2668
3619
  // The first prompt carries only the result command; waiting is rare, so it is offered only
2669
3620
  // once a check-in finds the agent idle without an answer.
@@ -2675,12 +3626,16 @@ class LogicalAgent implements AgentRef {
2675
3626
  "Finish or stop work needed for this answer before submitting the result.",
2676
3627
  `If waiting for work to finish, report it now using: ${this.options.launcher} waiting ${operationId} --reason 'what you are waiting for' [--timeout 2m]`,
2677
3628
  "Omit the square brackets when running the optional timeout flag. This command acknowledges your wait immediately; it does not sleep.",
2678
- `The fixed answer deadline is ${new Date(deadline.unixMilliseconds).toISOString()}. Waiting never extends it.`,
3629
+ ...(deadline
3630
+ ? [
3631
+ `The answer deadline is ${new Date(deadline.unixMilliseconds).toISOString()}. ${this.options.launcher} waiting never extends it.`,
3632
+ ]
3633
+ : []),
2679
3634
  ].join("\n")
2680
3635
  : "");
2681
3636
  const events = createOperationEvents(this.options.runDir, operationId);
2682
3637
  const messages = this.operationMessages(operationId, `${spec.id!}:message`, {
2683
- deadline: () => deadline,
3638
+ deadline: () => harnessDeadline(deadline),
2684
3639
  authored,
2685
3640
  session,
2686
3641
  record: events.record,
@@ -2692,7 +3647,17 @@ class LogicalAgent implements AgentRef {
2692
3647
  agentId: this.key,
2693
3648
  question: spec.prompt,
2694
3649
  schema,
2695
- deadline,
3650
+ ...(deadline ? { deadline } : {}),
3651
+ ...(ceiling ? { ceiling: ceiling.unixMilliseconds } : {}),
3652
+ extended: (moved, waiting) => {
3653
+ const next = moved === undefined ? undefined : { unixMilliseconds: moved };
3654
+ this.options.slots.extend(operationId, next);
3655
+ if (waiting || moved === undefined || moved <= (deadline?.unixMilliseconds ?? Infinity))
3656
+ return;
3657
+ deadline = next;
3658
+ messaging.extended(operationId, moved);
3659
+ events.record({ kind: "deadline-extended", at: Date.now(), sequence: 0, until: moved });
3660
+ },
2696
3661
  slots: this.options.slots,
2697
3662
  waitingSupported,
2698
3663
  nudge: nudge !== undefined,
@@ -2702,8 +3667,17 @@ class LogicalAgent implements AgentRef {
2702
3667
  .start(
2703
3668
  {
2704
3669
  id: spec.id!,
2705
- prompt: operationPrompt(prompt, schema, this.options.launcher, operationId),
2706
- deadline,
3670
+ prompt: [
3671
+ operationPrompt(prompt, schema, this.options.launcher, operationId),
3672
+ toolsLesson(
3673
+ this.options.tools.taught(this.key),
3674
+ this.options.launcher,
3675
+ resultSaysQuoting(schema),
3676
+ ),
3677
+ ]
3678
+ .filter(Boolean)
3679
+ .join("\n\n"),
3680
+ deadline: harnessDeadline(deadline),
2707
3681
  authored,
2708
3682
  onSession: session,
2709
3683
  },
@@ -2713,17 +3687,24 @@ class LogicalAgent implements AgentRef {
2713
3687
  successor: (turn, sequence, deliverySignal) =>
2714
3688
  turn.nudge({
2715
3689
  id: `${spec.id!}:check-in:${sequence}`,
2716
- deadline,
3690
+ deadline: harnessDeadline(deadline),
2717
3691
  deliverySignal,
2718
3692
  prompt: checkInPrompt(recoveryPrompt),
2719
3693
  // A check-in delivers no messages: those its first prompt carried are not its own.
2720
3694
  authored: { ...withoutMessages(authored), prompt: recoveryPrompt },
2721
3695
  }),
2722
- onStop: (stop) => {
3696
+ onStop: (settle) => {
3697
+ // A cancel or stop reaches a running tool call at once: the turn may be waiting on it.
3698
+ const stop = (request: OperationStop) => {
3699
+ this.options.tools.abort(operationId, request.kind);
3700
+ return settle(request);
3701
+ };
2723
3702
  this.#operationStops.add(stop);
3703
+ this.#stopsByTurn.set(spec.id!, stop);
2724
3704
  const remove = scope?.add(stop);
2725
3705
  return () => {
2726
3706
  this.#operationStops.delete(stop);
3707
+ if (this.#stopsByTurn.get(spec.id!) === stop) this.#stopsByTurn.delete(spec.id!);
2727
3708
  remove?.();
2728
3709
  };
2729
3710
  },
@@ -2741,6 +3722,8 @@ class LogicalAgent implements AgentRef {
2741
3722
  }),
2742
3723
  )
2743
3724
  .finally(() => events.close());
3725
+ // A call still running is stopped and waited for: run() resolves once its calls have settled.
3726
+ await this.options.tools.ended(operationId);
2744
3727
  // Its wait is dropped and what it still owed fails, before a failure closes the agent.
2745
3728
  messaging.ended(operationId, operationEnd(outcome.kind));
2746
3729
  if (outcome.cleanupUnresolved) {
@@ -2757,14 +3740,23 @@ class LogicalAgent implements AgentRef {
2757
3740
  outcome.charges,
2758
3741
  );
2759
3742
  if (outcome.kind === "answered")
2760
- return { outcome: { kind: "answered", value: outcome.value as JsonValue, usage } };
3743
+ return {
3744
+ outcome: { answered: true, kind: "answered", value: outcome.value as JsonValue, usage },
3745
+ };
2761
3746
  if (outcome.kind === "failed") {
2762
3747
  const login = outcome.login ? { login: outcome.login } : {};
2763
3748
  return {
2764
- outcome: { kind: "failed", reason: outcome.reason, retryable: false, ...login, usage },
3749
+ outcome: {
3750
+ answered: false,
3751
+ kind: "failed",
3752
+ reason: outcome.reason,
3753
+ retryable: false,
3754
+ ...login,
3755
+ usage,
3756
+ },
2765
3757
  };
2766
3758
  }
2767
- return { outcome: { kind: outcome.kind, reason: outcome.reason, usage } };
3759
+ return { outcome: { answered: false, kind: outcome.kind, reason: outcome.reason, usage } };
2768
3760
  }
2769
3761
 
2770
3762
  /**
@@ -2870,14 +3862,17 @@ class LogicalAgent implements AgentRef {
2870
3862
  private startMessageOperation(scope: MessagingScope): void {
2871
3863
  const owner = this.options.messageOwner(scope);
2872
3864
  if (!owner) return;
2873
- const result = this.queue(async () => {
2874
- if (this.#closed || this.options.isRunClosing()) return;
2875
- // What is first now: a message dropped since this was queued leaves another route's, which
2876
- // waits for an operation of its own scope.
2877
- const opening = this.options.messaging.opening(this.key);
2878
- if (opening?.scope !== scope) return;
2879
- await this.executeMessageOperation(opening.from, scope, owner.stage);
2880
- });
3865
+ // Whatever code made the message due, a handler included, nothing waits for its operation.
3866
+ const result = outsideTools(() =>
3867
+ this.queue(async () => {
3868
+ if (this.#closed || this.options.isRunClosing()) return;
3869
+ // What is first now: a message dropped since this was queued leaves another route's, which
3870
+ // waits for an operation of its own scope.
3871
+ const opening = this.options.messaging.opening(this.key);
3872
+ if (opening?.scope !== scope) return;
3873
+ await this.executeMessageOperation(opening.from, scope, owner.stage);
3874
+ }),
3875
+ );
2881
3876
  this.options.track(result).catch(() => undefined);
2882
3877
  owner.adopt(result);
2883
3878
  }
@@ -2889,8 +3884,8 @@ class LogicalAgent implements AgentRef {
2889
3884
  ): Promise<void> {
2890
3885
  const { messaging } = this.options;
2891
3886
  const operationId = randomUUID();
2892
- const ceiling = this.deadlineCeiling(stage).unixMilliseconds;
2893
- let deadline = Math.min(Date.now() + messaging.limits.operationMs, ceiling);
3887
+ const ceiling = this.deadlineCeiling(stage)?.unixMilliseconds;
3888
+ let deadline = Math.min(Date.now() + messaging.limits.operationMs, ceiling ?? Infinity);
2894
3889
  messaging.opened({
2895
3890
  id: operationId,
2896
3891
  agent: this.key,
@@ -2899,6 +3894,14 @@ class LogicalAgent implements AgentRef {
2899
3894
  ceiling,
2900
3895
  scope,
2901
3896
  });
3897
+ this.options.tools.opened({
3898
+ id: operationId,
3899
+ agent: this.key,
3900
+ callPath: [],
3901
+ ...(stage?.stage === undefined ? {} : { stage: stage.stage }),
3902
+ scope: stage,
3903
+ deadline: () => deadline,
3904
+ });
2902
3905
  /** How it ended; unset until its supervisor says, or if it never opened. */
2903
3906
  let end: SupervisedOutcome["kind"] | undefined;
2904
3907
  try {
@@ -2934,9 +3937,13 @@ class LogicalAgent implements AgentRef {
2934
3937
  }
2935
3938
  throw error;
2936
3939
  } finally {
3940
+ // Its agent ended its turn with a call running, say: the call is stopped and waited for, so
3941
+ // nothing after it, nor the next operation, runs with this one's call still running.
3942
+ await this.options.tools.ended(operationId);
2937
3943
  await this.options.afterOperation?.();
2938
3944
  }
2939
3945
  } finally {
3946
+ await this.options.tools.ended(operationId);
2940
3947
  // A message operation that simply ends settles; its wait is dropped and what it owed fails.
2941
3948
  messaging.ended(
2942
3949
  operationId,
@@ -2968,16 +3975,32 @@ class LogicalAgent implements AgentRef {
2968
3975
  cause,
2969
3976
  messages: opening.messages,
2970
3977
  };
3978
+ const extended = (later: number) => {
3979
+ if (later <= deadline) return;
3980
+ deadline = later;
3981
+ messaging.extended(operationId, later);
3982
+ this.options.tools.extended(operationId, later);
3983
+ events.record({ kind: "deadline-extended", at: Date.now(), sequence: 0, until: later });
3984
+ };
3985
+ const ceiling = this.deadlineCeiling(stage)?.unixMilliseconds;
2971
3986
  let outcome: SupervisedOutcome;
2972
3987
  try {
2973
3988
  outcome = await superviseMessageOperation({
2974
3989
  deadline: () => deadline,
3990
+ ...(ceiling === undefined ? {} : { ceiling }),
3991
+ extended,
2975
3992
  start: () =>
2976
3993
  this.options.session
2977
3994
  .start(
2978
3995
  {
2979
3996
  id: operationId,
2980
- prompt: opening.text,
3997
+ // A message turn's tools block, as a workflow turn's: after what it opens on.
3998
+ prompt: [
3999
+ opening.text,
4000
+ toolsLesson(this.options.tools.taught(this.key), this.options.launcher, false),
4001
+ ]
4002
+ .filter(Boolean)
4003
+ .join("\n\n"),
2981
4004
  deadline: { unixMilliseconds: deadline },
2982
4005
  authored,
2983
4006
  onSession: operation.session,
@@ -2990,14 +4013,13 @@ class LogicalAgent implements AgentRef {
2990
4013
  authored,
2991
4014
  session: operation.session,
2992
4015
  record: events.record,
2993
- extended: (later) => {
2994
- if (later <= deadline) return;
2995
- deadline = later;
2996
- messaging.extended(operationId, later);
2997
- events.record({ kind: "deadline-extended", at: Date.now(), sequence: 0, until: later });
2998
- },
4016
+ extended,
2999
4017
  }),
3000
- onStop: (stop) => {
4018
+ onStop: (settle) => {
4019
+ const stop = (request: OperationStop) => {
4020
+ this.options.tools.abort(operationId, request.kind);
4021
+ return settle(request);
4022
+ };
3001
4023
  this.#operationStops.add(stop);
3002
4024
  const remove = stage?.add(stop);
3003
4025
  return () => {
@@ -3030,11 +4052,12 @@ class LogicalAgent implements AgentRef {
3030
4052
  // Recorded `answered` with no value: it did what it was woken for, and takes no result.
3031
4053
  this.turnEnded(
3032
4054
  outcome.kind === "answered"
3033
- ? { kind: "answered", value: null, usage }
4055
+ ? { answered: true, kind: "answered", value: null, usage }
3034
4056
  : outcome.kind === "failed"
3035
- ? { kind: "failed", reason: outcome.reason, retryable: false, usage }
3036
- : { kind: outcome.kind, reason: outcome.reason, usage },
4057
+ ? { answered: false, kind: "failed", reason: outcome.reason, retryable: false, usage }
4058
+ : { answered: false, kind: outcome.kind, reason: outcome.reason, usage },
3037
4059
  "turn",
4060
+ true,
3038
4061
  );
3039
4062
  return outcome;
3040
4063
  }
@@ -3067,17 +4090,23 @@ class LogicalAgent implements AgentRef {
3067
4090
  later,
3068
4091
  );
3069
4092
  if (native === "expired") {
3070
- return { kind: "timed-out", reason: "compaction deadline exceeded", usage };
4093
+ return {
4094
+ answered: false,
4095
+ kind: "timed-out",
4096
+ reason: "compaction deadline exceeded",
4097
+ usage,
4098
+ };
3071
4099
  }
3072
4100
  if (native.state === "completed") {
3073
4101
  return native.summary === undefined
3074
4102
  ? {
4103
+ answered: false,
3075
4104
  kind: "failed",
3076
4105
  reason: native.detail ?? "the harness did not confirm a compaction",
3077
4106
  retryable: false,
3078
4107
  usage,
3079
4108
  }
3080
- : { kind: "answered", value: native.summary, usage };
4109
+ : { answered: true, kind: "answered", value: native.summary, usage };
3081
4110
  }
3082
4111
  return reconcile<string>(native, usage);
3083
4112
  };
@@ -3144,6 +4173,94 @@ class LogicalAgent implements AgentRef {
3144
4173
  }
3145
4174
  }
3146
4175
 
4176
+ /** A sleep's time, checked where it is given. */
4177
+ function checkedWhen(when: unknown): string | Date {
4178
+ if (when instanceof Date || typeof when === "string") return when;
4179
+ if (readOf(when))
4180
+ throw new Error(
4181
+ "sleep is for a duration or a Date; wf.at() is a select's branch, read at each of its waits",
4182
+ );
4183
+ throw new Error('sleep is for a duration, "2h", or until a Date');
4184
+ }
4185
+
4186
+ function checkedLabel(label: unknown): string | undefined {
4187
+ if (label !== undefined && typeof label !== "string")
4188
+ throw new Error("a wait's label is a string");
4189
+ return label;
4190
+ }
4191
+
4192
+ /** A wait's branches, by name, checked: a race may give a duration where a select may not. */
4193
+ function branchesOf(what: string, branches: unknown, durations: boolean): [string, unknown][] {
4194
+ if (typeof branches !== "object" || branches === null || Array.isArray(branches))
4195
+ throw new Error(`${what} takes its branches as an object, { name: source }`);
4196
+ const entries = Object.entries(branches);
4197
+ if (entries.length === 0) throw new Error(`${what} has no branches: it would never return`);
4198
+ if ("key" in branches) throw new Error(`${what}: no branch is named key, which the answer uses`);
4199
+ for (const [branch, source] of entries)
4200
+ if (typeof source === "string" && !durations)
4201
+ throw new Error(`${what}'s ${branch} is a duration: a timer in a loop is wf.timer()'s`);
4202
+ return entries;
4203
+ }
4204
+
4205
+ /** What a time read from state reads, by its source. */
4206
+ const timeReads = new WeakMap<object, () => Date | null>();
4207
+
4208
+ /** `wf.at(read)`: a select's branch, due at the time `read` returns. */
4209
+ function timeRead(read: () => Date | null): TimeRead {
4210
+ if (typeof read !== "function")
4211
+ throw new Error(
4212
+ "wf.at takes a function that reads a Date or null from the run's state, as () => ledger.nextNudge()",
4213
+ );
4214
+ const value = Object.freeze({});
4215
+ timeReads.set(value, read);
4216
+ return value as unknown as TimeRead;
4217
+ }
4218
+
4219
+ /** What a time read from state reads; undefined for any other value. */
4220
+ function readOf(value: unknown): (() => Date | null) | undefined {
4221
+ return typeof value === "object" && value !== null ? timeReads.get(value) : undefined;
4222
+ }
4223
+
4224
+ /** What `source` takes, as a branch gives it. */
4225
+ function sourceTake(what: string, branch: string, source: unknown): WaitTake {
4226
+ const take = takeOf(source);
4227
+ if (take) return take;
4228
+ if (readOf(source))
4229
+ throw new Error(
4230
+ `${what}'s ${branch} is wf.at(), a select's: a race answers once, so it takes a timer, wf.timer(name, date)`,
4231
+ );
4232
+ throw new Error(
4233
+ `${what}'s ${branch} is not a source: a timer, a signal, a participant routed to the code, an agent, or a started turn`,
4234
+ );
4235
+ }
4236
+
4237
+ /** One of the workflow's own events, by name, as a source. */
4238
+ function signalSource(name: string): unknown {
4239
+ if (typeof name !== "string" || name.includes(":"))
4240
+ throw new Error(`a signal is one of the workflow's own events, by name; not ${String(name)}`);
4241
+ return source({ name }, { name });
4242
+ }
4243
+
4244
+ /** Timer `name`'s source. */
4245
+ function timerSource(mailbox: RunMailbox, name: string, due: string): Timer {
4246
+ return source(
4247
+ { name, due, cancel: () => mailbox.cancelTimer(name) },
4248
+ { name: "timer:due", from: name },
4249
+ ) as unknown as Timer;
4250
+ }
4251
+
4252
+ /** A started turn, as the code holds it: its id, its cancel, and a source of its end. */
4253
+ function turnRef(
4254
+ agent: string,
4255
+ id: string,
4256
+ cancel: (reason?: string) => Promise<boolean>,
4257
+ ): StartedTurn {
4258
+ return source(
4259
+ { id, cancel },
4260
+ { name: "agent:ended", from: agent, turn: id },
4261
+ ) as unknown as StartedTurn;
4262
+ }
4263
+
3147
4264
  class ExecutionScope {
3148
4265
  readonly #cancellers = new Set<(stop: OperationStop) => Promise<unknown>>();
3149
4266
  readonly #owned = new Set<Promise<unknown>>();
@@ -3153,7 +4270,7 @@ class ExecutionScope {
3153
4270
  #cancellation: Promise<void> | undefined;
3154
4271
 
3155
4272
  constructor(
3156
- readonly deadline: AbsoluteDeadline,
4273
+ readonly deadline: AbsoluteDeadline | undefined,
3157
4274
  /** The labelled `parallel` it runs in, for progress. */
3158
4275
  readonly group?: GroupProgress,
3159
4276
  /** The workflow stage everything in this scope runs in. */
@@ -3167,8 +4284,8 @@ class ExecutionScope {
3167
4284
  }
3168
4285
 
3169
4286
  assertActive(): void {
3170
- if (this.#cancelled || Date.now() >= this.deadline.unixMilliseconds) {
3171
- throw new DeadlineExceededError(this.deadline);
4287
+ if (this.#cancelled || (this.deadline && Date.now() >= this.deadline.unixMilliseconds)) {
4288
+ throw new DeadlineExceededError(this.deadline ?? { unixMilliseconds: Date.now() });
3172
4289
  }
3173
4290
  }
3174
4291
 
@@ -3236,7 +4353,7 @@ class ExecutionScope {
3236
4353
  function runParallel<Item, Result>(
3237
4354
  items: readonly Item[],
3238
4355
  operation: (item: Item, index: number) => Promise<Result>,
3239
- deadline: AbsoluteDeadline,
4356
+ deadline: AbsoluteDeadline | undefined,
3240
4357
  requestedConcurrency?: number,
3241
4358
  openGroup?: () => GroupProgress | undefined,
3242
4359
  ): Promise<Result[]> {
@@ -3256,7 +4373,7 @@ function runParallel<Item, Result>(
3256
4373
  async function executeParallel<Item, Result>(
3257
4374
  items: readonly Item[],
3258
4375
  operation: (item: Item, index: number) => Promise<Result>,
3259
- deadline: AbsoluteDeadline,
4376
+ deadline: AbsoluteDeadline | undefined,
3260
4377
  concurrency: number,
3261
4378
  parent: ExecutionScope | undefined,
3262
4379
  group?: GroupProgress,
@@ -3293,6 +4410,7 @@ async function executeParallel<Item, Result>(
3293
4410
  });
3294
4411
  let cancelTimer: (() => void) | undefined;
3295
4412
  const expiry = new Promise<never>((_resolve, reject) => {
4413
+ if (!deadline) return;
3296
4414
  cancelTimer = scheduleAt(deadline, () => {
3297
4415
  void scope
3298
4416
  .cancel({ kind: "timed-out", reason: "parallel deadline exceeded" })
@@ -3301,9 +4419,9 @@ async function executeParallel<Item, Result>(
3301
4419
  });
3302
4420
  try {
3303
4421
  await Promise.race([completion, expiry]);
3304
- if (scope.cancelled || Date.now() >= deadline.unixMilliseconds) {
4422
+ if (scope.cancelled || (deadline && Date.now() >= deadline.unixMilliseconds)) {
3305
4423
  await scope.cancel({ kind: "timed-out", reason: "parallel deadline exceeded" });
3306
- throw new DeadlineExceededError(deadline);
4424
+ throw new DeadlineExceededError(deadline ?? { unixMilliseconds: Date.now() });
3307
4425
  }
3308
4426
  return results;
3309
4427
  } catch (error) {
@@ -3343,20 +4461,41 @@ function reconcile<T extends JsonValue>(
3343
4461
  ): TurnOutcome<T> {
3344
4462
  switch (native.state) {
3345
4463
  case "completed":
3346
- return { kind: "unanswered", reason: "agent settled without an accepted result", usage };
4464
+ return {
4465
+ answered: false,
4466
+ kind: "unanswered",
4467
+ reason: "agent settled without an accepted result",
4468
+ usage,
4469
+ };
3347
4470
  case "blocked":
3348
- return { kind: "blocked", reason: native.detail ?? "agent is blocked", usage };
4471
+ return {
4472
+ answered: false,
4473
+ kind: "blocked",
4474
+ reason: native.detail ?? "agent is blocked",
4475
+ usage,
4476
+ };
3349
4477
  case "timed-out":
3350
- return { kind: "timed-out", reason: native.detail ?? "operation timed out", usage };
4478
+ return {
4479
+ answered: false,
4480
+ kind: "timed-out",
4481
+ reason: native.detail ?? "operation timed out",
4482
+ usage,
4483
+ };
3351
4484
  case "failed":
3352
4485
  return {
4486
+ answered: false,
3353
4487
  kind: "failed",
3354
4488
  reason: native.detail ?? "harness operation failed",
3355
4489
  retryable: false,
3356
4490
  usage,
3357
4491
  };
3358
4492
  case "cancelled":
3359
- return { kind: "cancelled", reason: native.detail ?? "operation cancelled", usage };
4493
+ return {
4494
+ answered: false,
4495
+ kind: "cancelled",
4496
+ reason: native.detail ?? "operation cancelled",
4497
+ usage,
4498
+ };
3360
4499
  }
3361
4500
  }
3362
4501
 
@@ -3470,6 +4609,11 @@ function resultSchema<T extends JsonValue>(schema: OutputSchema<T> | undefined):
3470
4609
  return schema ? parseJsonSchema(schema) : { type: "string" };
3471
4610
  }
3472
4611
 
4612
+ function resultSaysQuoting(schema: JsonSchema): boolean {
4613
+ const simple = simpleInput(schema);
4614
+ return simple !== undefined && takesText(simple);
4615
+ }
4616
+
3473
4617
  function operationPrompt(
3474
4618
  prompt: string,
3475
4619
  schema: JsonSchema,
@@ -3480,7 +4624,19 @@ function operationPrompt(
3480
4624
  // A quoted heredoc passes the JSON through untouched: a quoted argument broke on shell quoting
3481
4625
  // (story 002). Not indented: a copied terminator with leading spaces never closes the heredoc.
3482
4626
  // The schema itself, not a rendering of it: without its bounds, first answers were 0/160 valid
3483
- // (E5). `wf` says what to do about a rejection, so the prompt does not.
4627
+ // (E5). `wfa` says what to do about a rejection, so the prompt does not. A simple schema is taught
4628
+ // as flags instead, its usage line carrying every bound, which held them as well (story 035, M1).
4629
+ const simple = simpleInput(schema);
4630
+ if (simple) {
4631
+ return [
4632
+ prompt,
4633
+ "",
4634
+ "When the answer is ready, return it by running:",
4635
+ "",
4636
+ `${launcher} result ${callId} ${usageLine(simple)}`,
4637
+ ...(takesText(simple) ? ["", QUOTING_RULE] : []),
4638
+ ].join("\n");
4639
+ }
3484
4640
  return [
3485
4641
  prompt,
3486
4642
  "",
@@ -3664,3 +4820,27 @@ function stoppedError(key: string, reason: string): Error {
3664
4820
  function unavailable(name: string): Promise<never> {
3665
4821
  return Promise.reject(new Error(`${name} is not implemented by the minimum workflow runner`));
3666
4822
  }
4823
+
4824
+ /**
4825
+ * Each root agent's last native session in a run's turns, where its harness can resume it: what a
4826
+ * event run's continue reopens it on.
4827
+ */
4828
+ export function keptSessionsOf(turns: readonly TurnRecord[]): Map<string, NativeFork> {
4829
+ const kept = new Map<string, NativeFork>();
4830
+ for (const turn of turns) {
4831
+ if ((turn.callPath?.length ?? 0) > 0) continue;
4832
+ const session = turn.sessions.at(-1);
4833
+ if (session) kept.set(turn.agent, { harness: session.harness, sessionRef: session.id });
4834
+ }
4835
+ return kept;
4836
+ }
4837
+
4838
+ /**
4839
+ * In a run of a workflow with `events`: how an agent records a turn `start()` begins and its end,
4840
+ * and the turn an earlier attempt started whose end no wait took, which `turn` is from the start.
4841
+ */
4842
+ type LogicalAgentTurns = {
4843
+ begin(turn: TurnId, meta: JsonValue, execution: AgentExecution): Promise<void>;
4844
+ end(turn: TurnId, meta: JsonValue, outcome: TurnOutcome<JsonValue>): Promise<void>;
4845
+ restored(): RestoredTurn | undefined;
4846
+ };