@agentswf/engine 0.1.0-beta.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +16 -0
  3. package/package.json +13 -9
  4. package/src/accounting/format.ts +4 -3
  5. package/src/accounting/spent.ts +128 -0
  6. package/src/accounting/summary.ts +40 -32
  7. package/src/agent-cli/cli.ts +35 -22
  8. package/src/agent-cli/client.ts +1 -27
  9. package/src/allowance-command.ts +8 -10
  10. package/src/attempt-ending.ts +5 -7
  11. package/src/attempt-output.ts +15 -32
  12. package/src/attempt-view.ts +34 -33
  13. package/src/author-typebox.ts +13 -0
  14. package/src/client/checks.ts +94 -0
  15. package/src/client/exported.ts +17 -0
  16. package/src/client/index.ts +122 -0
  17. package/src/client/locate.ts +34 -0
  18. package/src/client/published.ts +3 -0
  19. package/src/client/runs.ts +337 -0
  20. package/src/command-line.ts +64 -0
  21. package/src/commands.ts +7 -1
  22. package/src/control-plane.ts +41 -11
  23. package/src/coordinator-prompt.ts +4 -3
  24. package/src/coordinator-socket.ts +41 -33
  25. package/src/deadlines.ts +40 -7
  26. package/src/decisions/directory.ts +51 -40
  27. package/src/decisions/fake.ts +1 -1
  28. package/src/decisions/openrouter.ts +4 -2
  29. package/src/decisions/seam.ts +4 -4
  30. package/src/display-path.ts +3 -3
  31. package/src/duration.ts +43 -27
  32. package/src/emit-command.ts +88 -58
  33. package/src/emit.ts +254 -56
  34. package/src/event-ids.ts +31 -0
  35. package/src/files.ts +44 -4
  36. package/src/hand-over.ts +150 -0
  37. package/src/help/allowance.txt +4 -3
  38. package/src/help/emit.txt +18 -6
  39. package/src/help/inbox.txt +6 -4
  40. package/src/help/logs.txt +9 -6
  41. package/src/help/open.txt +29 -0
  42. package/src/help/reply.txt +6 -8
  43. package/src/help/run.txt +30 -11
  44. package/src/help/runs.txt +4 -2
  45. package/src/help/tick.txt +9 -4
  46. package/src/herdr-placement.ts +3 -33
  47. package/src/herdr-run-session.ts +7 -15
  48. package/src/herdr-workspace-marks.ts +7 -17
  49. package/src/holder-command.ts +56 -133
  50. package/src/in-lines.ts +28 -0
  51. package/src/inbox-command.ts +40 -57
  52. package/src/index.ts +1 -1
  53. package/src/init-command.ts +8 -17
  54. package/src/init-template/hello/workflow.test.ts.template +3 -3
  55. package/src/init-template/hello/workflow.ts.template +5 -4
  56. package/src/jsonl.ts +10 -6
  57. package/src/keep-alive.ts +15 -12
  58. package/src/logs-command.ts +128 -47
  59. package/src/message-operation.ts +6 -3
  60. package/src/messaging/limits.ts +4 -4
  61. package/src/messaging/messaging.ts +31 -28
  62. package/src/messaging/routes.ts +1 -1
  63. package/src/messaging/text.ts +9 -7
  64. package/src/open-command.ts +770 -0
  65. package/src/operation-events.ts +4 -6
  66. package/src/operation-liveness.ts +19 -4
  67. package/src/operator-cli.ts +224 -94
  68. package/src/operator-runtime.ts +29 -7
  69. package/src/ops.ts +270 -0
  70. package/src/pane-layout.ts +105 -59
  71. package/src/progress-view.ts +10 -10
  72. package/src/project-engine.ts +79 -0
  73. package/src/projects.ts +13 -2
  74. package/src/refusals.ts +119 -0
  75. package/src/result-slots.ts +11 -31
  76. package/src/result-validation.ts +2 -2
  77. package/src/run-command.ts +45 -28
  78. package/src/run-commands.ts +43 -115
  79. package/src/run-event-log.ts +1 -1
  80. package/src/run-lines.ts +159 -0
  81. package/src/run-lock.ts +57 -1
  82. package/src/{stage-plan.ts → run-policy/stage-plan.ts} +46 -42
  83. package/src/run-policy/stages.ts +113 -0
  84. package/src/run-policy/waits.ts +452 -0
  85. package/src/run-prepare.ts +205 -43
  86. package/src/run-read.ts +109 -41
  87. package/src/run-state.ts +170 -50
  88. package/src/run-text.ts +54 -0
  89. package/src/run-unreachable.ts +18 -0
  90. package/src/run-usage.ts +18 -12
  91. package/src/run-view.ts +81 -17
  92. package/src/runs-command.ts +83 -63
  93. package/src/runs.ts +310 -212
  94. package/src/sandbox-homes.ts +65 -13
  95. package/src/sandboxes.ts +182 -28
  96. package/src/session-lines.ts +93 -0
  97. package/src/skills/run-skills.ts +8 -1
  98. package/src/sources.ts +28 -29
  99. package/src/spend.ts +333 -0
  100. package/src/stage-ledger.ts +119 -131
  101. package/src/stop-crashed.ts +6 -5
  102. package/src/stopped.ts +4 -4
  103. package/src/test-command.ts +36 -14
  104. package/src/testing/operation-events.ts +3 -23
  105. package/src/testing.ts +73 -26
  106. package/src/tick-command.ts +145 -15
  107. package/src/tools.ts +25 -16
  108. package/src/turn-ends.ts +37 -0
  109. package/src/waits.ts +651 -0
  110. package/src/wake.ts +84 -54
  111. package/src/workflow-loader.ts +54 -4
  112. package/src/workflow-runner.ts +805 -580
  113. package/src/workflow-test-preload.ts +4 -1
  114. package/src/workflow-testing/coordinators.ts +3 -3
  115. package/src/workflow-testing/fake-client.ts +387 -0
  116. package/src/workflow-testing/host.ts +121 -53
  117. package/src/workflow-testing/index.ts +33 -842
  118. package/src/workflow-testing/run-test.ts +1128 -0
  119. package/src/workflow-testing/script.ts +74 -45
  120. package/src/hold-run-lock.fixture.ts +0 -8
  121. package/src/mailbox.ts +0 -990
  122. package/src/workflow-testing/coordinator.typecheck.ts +0 -54
  123. package/src/workflow-testing/workflow-testing.typecheck.ts +0 -64
@@ -0,0 +1,113 @@
1
+ import type { ClaimedAttempt, RunView, StageLine } from "@agentswf/contract/records";
2
+
3
+ /**
4
+ * The stage attempt `attempt` started at, which its line says with `redo`: the one `--from-stage`
5
+ * named, or a plain run's first stage not reused. In that attempt and every later one, a stage
6
+ * entered after `stage` whose line an earlier attempt than `attempt` wrote runs again: it was made
7
+ * against the stage before. One never entered after the stage keeps its reuse.
8
+ */
9
+ export type StageRedo = { stage: string; attempt: number };
10
+
11
+ /**
12
+ * What an attempt may reuse of the run's stages, as it enters them: the engine's rule over
13
+ * `readRun`'s view. A stage stands for its latest line, unless the redo outdated it; the plan
14
+ * decides from that line whether it is reused, which it is only when that line succeeded: a
15
+ * continue reuses `settled` only when it is also `last`. One per attempt, told of each line it
16
+ * writes.
17
+ */
18
+ export class StageReuse {
19
+ /** Each stage's latest line, this attempt's included. */
20
+ readonly #last: Map<string, StageLine>;
21
+ #redo: StageRedo | undefined;
22
+ /** Whether this attempt has entered the redo's stage, after which an older line is outdated. */
23
+ #afterRedo = false;
24
+ readonly #entered = new Set<string>();
25
+
26
+ constructor(
27
+ view: Pick<RunView, "stages" | "redo">,
28
+ private readonly attempt: number,
29
+ ) {
30
+ this.#last = new Map(Object.entries(view.stages).map(([name, { last }]) => [name, last]));
31
+ this.#redo = view.redo;
32
+ }
33
+
34
+ /**
35
+ * Enters `name`: the line it stands for now, of any outcome, or undefined when it has none or the
36
+ * redo outdated it. A stage entered already in this attempt keeps the line it had: one stage, one
37
+ * value.
38
+ */
39
+ enter(name: string): StageLine | undefined {
40
+ const line = this.#last.get(name);
41
+ const redo = this.#redo;
42
+ const outdated =
43
+ redo !== undefined &&
44
+ this.#afterRedo &&
45
+ line !== undefined &&
46
+ line.attempt < redo.attempt &&
47
+ !this.#entered.has(name);
48
+ this.#entered.add(name);
49
+ if (name === redo?.stage) this.#afterRedo = true;
50
+ return outdated ? undefined : line;
51
+ }
52
+
53
+ /** This attempt starts at `name`, entered now: a later stage with an older line runs again. */
54
+ redoFrom(name: string): void {
55
+ this.#redo = { stage: name, attempt: this.attempt };
56
+ this.#afterRedo = true;
57
+ }
58
+
59
+ /** A line this attempt wrote, which the stages it enters from now on see. */
60
+ wrote(line: StageLine): void {
61
+ this.#last.set(line.name, line);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Each stage's latest line, in the order they started, attempt then file order breaking a tie:
67
+ * what `--from-stage` chooses from, and what a stage not reached names.
68
+ */
69
+ export function recordedStages(view: Pick<RunView, "stages">): StageLine[] {
70
+ return Object.values(view.stages)
71
+ .map(({ last }) => last)
72
+ .sort((a, b) => a.started.localeCompare(b.started) || a.attempt - b.attempt);
73
+ }
74
+
75
+ /**
76
+ * The stages a continue may still come to, in the order they started: each recorded stage but
77
+ * those the last redo left behind, such as a stage the code no longer has. The redo's attempt ran
78
+ * every stage it entered after its start point again, so a stage whose latest line is older than
79
+ * that attempt, and which started after the redo's stage in the attempt that wrote it, was never
80
+ * entered there. Where that attempt reused the redo's stage, and so wrote it no line, the redo
81
+ * stage's latest line from before it stands in: what that attempt ran came after the stage it
82
+ * reused. A stage before the redo's stage was reused, and is kept; so is one whose order can't be
83
+ * told, and a removed stage that came before it. A stage first run before the redo's stage, in an
84
+ * attempt that reused the redo's stage, is taken for one after it, and left out.
85
+ */
86
+ export function upcomingStages(view: Pick<RunView, "stages" | "redo">): StageLine[] {
87
+ const { redo } = view;
88
+ return recordedStages(view).filter((line) => {
89
+ if (redo === undefined || line.attempt >= redo.attempt) return true;
90
+ const history = view.stages[redo.stage]?.history ?? [];
91
+ const start =
92
+ history.find((earlier) => earlier.attempt === line.attempt) ??
93
+ history.findLast((earlier) => earlier.attempt < line.attempt);
94
+ return start === undefined || Date.parse(line.started) <= Date.parse(start.started);
95
+ });
96
+ }
97
+
98
+ /**
99
+ * The `--from-stage` a continue goes on with when the attempt before it was one that crashed inside
100
+ * its start stage: it has no ending, named a `--from-stage`, and wrote no line for that stage. That
101
+ * redo isn't in the lines, so without it the old lines would stand. Undefined otherwise. The caller
102
+ * has already found the attempt's process gone.
103
+ */
104
+ export function crashedRedo(
105
+ attempts: readonly Pick<ClaimedAttempt, "attempt" | "ended" | "flags">[],
106
+ view: Pick<RunView, "stages">,
107
+ ): string | undefined {
108
+ const last = attempts.at(-1);
109
+ const stage = last?.flags.fromStage;
110
+ if (!last || last.ended !== undefined || stage === undefined) return undefined;
111
+ const wrote = view.stages[stage]?.history.some((line) => line.attempt === last.attempt);
112
+ return wrote ? undefined : stage;
113
+ }
@@ -0,0 +1,452 @@
1
+ import type { EventLine, RunLine, Taken, WaitTake } from "@agentswf/contract/records";
2
+ import { type AgentEnded, awfEventOf, type JsonValue } from "@agentswf/contract/workflow";
3
+
4
+ type LineOf<K extends RunLine["kind"]> = Extract<RunLine, { kind: K }>;
5
+ type WaitLine = LineOf<"wait">;
6
+ export type TurnStartedLine = LineOf<"turn-started">;
7
+
8
+ /**
9
+ * Where an event a wait can take comes from, which orders those at one time: a timer due, then a
10
+ * turn's end, then a sender's event; a time read from state comes after every ready event.
11
+ */
12
+ type Origin = "timer" | "end" | "event" | "time";
13
+ const RANK: Record<Origin, number> = { timer: 0, end: 1, event: 2, time: 3 };
14
+
15
+ /** An event a wait can take, as it would be handed to the code, and where it came from. */
16
+ export type WaitEvent = {
17
+ id: string;
18
+ name: string;
19
+ data: JsonValue;
20
+ from: string;
21
+ at: string;
22
+ origin: Origin;
23
+ /** The event that created the run. */
24
+ starts?: true;
25
+ /** The integration that sent it, as `awf emit --source` named it. */
26
+ source?: string;
27
+ /** A turn's end: the attempt that began the turn. */
28
+ began?: number;
29
+ };
30
+
31
+ /** The ids awf derives for what it never writes as an event: a timer's, and a time read from state's. */
32
+ export type DerivedIds = {
33
+ timer(name: string): string;
34
+ at(branch: string, due: string): string;
35
+ };
36
+
37
+ /** A timer as the lines have it: when it is due, an ISO time, and what became of it. */
38
+ export type TimerState = {
39
+ name: string;
40
+ due: string;
41
+ /** As the code gave it: a duration, or an ISO time. */
42
+ when: string;
43
+ cancelled: boolean;
44
+ /** A wait or a race took it. */
45
+ taken: boolean;
46
+ /** Taken, or due by the time the lines were read: no longer a deadline to wait for. */
47
+ fired: boolean;
48
+ };
49
+
50
+ /** What a wait can take and what the waits have done, from `run.jsonl` and `in.jsonl` at a time. */
51
+ export type WaitState = {
52
+ /** Every event a wait could take or took, by id: the first under an id is kept. */
53
+ events: ReadonlyMap<string, WaitEvent>;
54
+ /** Event ids no wait has acknowledged and none holds, in the order a wait takes them. */
55
+ unacknowledged: string[];
56
+ /** What the last wait outside a stage took that no later wait outside one acknowledged. */
57
+ held?: string[];
58
+ /** What the wait that took `held` takes: absent for a `receive`'s batch. */
59
+ heldTakes?: WaitTake[];
60
+ /** The last wait called, and whether it is still waiting: nothing taken after it. */
61
+ lastWait?: WaitLine & { waiting: boolean };
62
+ /** Every turn `start()` began, in order. */
63
+ turns: readonly TurnStartedLine[];
64
+ /** Every timer by name, as first set. */
65
+ timers: ReadonlyMap<string, TimerState>;
66
+ /** What each race took, by its name: the branch that won and the event. */
67
+ races: ReadonlyMap<string, { branch: string; ids: string[] }>;
68
+ /** The event that created the run. */
69
+ started?: WaitEvent;
70
+ /** The clock it was folded at, which says which timers are due. */
71
+ now: number;
72
+ /** How it derives a timer's and a time's id. */
73
+ ids: DerivedIds;
74
+ };
75
+
76
+ /**
77
+ * The lines a reader acts on: a line of an attempt before the latest `claimed` is a stale writer's,
78
+ * ignored, as `readRun` ignores it.
79
+ */
80
+ export function unfenced(lines: readonly RunLine[]): RunLine[] {
81
+ let claimed = 0;
82
+ return lines.filter((line) => {
83
+ if (line.attempt < claimed) return false;
84
+ if (line.kind === "claimed") claimed = line.attempt;
85
+ return true;
86
+ });
87
+ }
88
+
89
+ /**
90
+ * What the waits see: `in.jsonl`'s events, the turns' ends and the timers due by `now`, merged, and
91
+ * what the `wait`, `took` and `race` lines did with them. A wait outside a stage and a race
92
+ * acknowledges its `acks` and ends the held batch; a `took` in a stage, and a race, acknowledge
93
+ * their own, which the stage or race holds. A cancelled timer counts as acknowledged.
94
+ */
95
+ export function foldWaits(given: {
96
+ lines: readonly RunLine[];
97
+ events: readonly EventLine[];
98
+ now: number;
99
+ ids: DerivedIds;
100
+ }): WaitState {
101
+ const { ids, now } = given;
102
+ const events = new Map<string, WaitEvent>();
103
+ /** The order each id was met in, which breaks a tie of time and origin. */
104
+ const met = new Map<string, number>();
105
+ const add = (event: WaitEvent) => {
106
+ if (events.has(event.id)) return;
107
+ events.set(event.id, event);
108
+ met.set(event.id, met.size);
109
+ };
110
+ for (const line of given.events)
111
+ add({
112
+ id: line.id,
113
+ name: line.name,
114
+ data: line.data,
115
+ from: line.from,
116
+ at: line.at,
117
+ origin: "event",
118
+ ...(line.starts ? { starts: true as const } : {}),
119
+ ...(line.source === undefined ? {} : { source: line.source }),
120
+ });
121
+ const acknowledged = new Set<string>();
122
+ const races = new Map<string, { branch: string; ids: string[] }>();
123
+ const timers = new Map<string, TimerState>();
124
+ const turns: TurnStartedLine[] = [];
125
+ let held: string[] | undefined;
126
+ let heldTakes: WaitTake[] | undefined;
127
+ let lastWait: (WaitLine & { waiting: boolean }) | undefined;
128
+ const took = (entries: readonly Taken[], at: string): string[] =>
129
+ entries.map((entry) => {
130
+ if (entry.timer !== undefined) {
131
+ const timer = timers.get(entry.timer);
132
+ if (timer) timer.taken = true;
133
+ add(timerEvent(entry.id, entry.timer, timer?.due ?? at));
134
+ } else if (entry.branch !== undefined && entry.due !== undefined) {
135
+ add(timeEvent(entry.id, entry.branch, entry.due));
136
+ }
137
+ return entry.id;
138
+ });
139
+ for (const line of unfenced(given.lines)) {
140
+ switch (line.kind) {
141
+ case "timer": {
142
+ let timer = timers.get(line.name);
143
+ if (!timer) {
144
+ timer = {
145
+ name: line.name,
146
+ due: line.due,
147
+ when: line.when,
148
+ cancelled: false,
149
+ taken: false,
150
+ fired: false,
151
+ };
152
+ timers.set(line.name, timer);
153
+ }
154
+ if (line.cancelled) {
155
+ timer.cancelled = true;
156
+ acknowledged.add(ids.timer(line.name));
157
+ }
158
+ break;
159
+ }
160
+ case "wait":
161
+ // A wait in a stage or a race leaves the loop's batch to the loop.
162
+ if (line.stage === undefined && line.race === undefined) {
163
+ for (const id of line.acks ?? []) acknowledged.add(id);
164
+ held = undefined;
165
+ }
166
+ lastWait = { ...line, waiting: true };
167
+ break;
168
+ case "took": {
169
+ const taken = took(line.took, line.at);
170
+ if (lastWait) lastWait.waiting = false;
171
+ // A wait in a stage, before 0.1.0-beta.6, held its batch as acknowledged.
172
+ if (line.stage !== undefined) for (const id of taken) acknowledged.add(id);
173
+ else {
174
+ held = taken.length > 0 ? taken : undefined;
175
+ heldTakes = lastWait?.takes;
176
+ }
177
+ break;
178
+ }
179
+ case "race": {
180
+ const taken = took(line.took, line.at);
181
+ if (lastWait) lastWait.waiting = false;
182
+ for (const id of taken) acknowledged.add(id);
183
+ if (!races.has(line.name)) races.set(line.name, { branch: line.branch, ids: taken });
184
+ break;
185
+ }
186
+ case "turn-started":
187
+ turns.push(line);
188
+ break;
189
+ case "turn-ended":
190
+ add({
191
+ id: line.id,
192
+ name: "agent:ended",
193
+ data: line.end as unknown as JsonValue,
194
+ from: line.agent,
195
+ at: line.at,
196
+ origin: "end",
197
+ began: line.began,
198
+ });
199
+ break;
200
+ }
201
+ }
202
+ for (const timer of timers.values()) {
203
+ timer.fired = timer.taken || Date.parse(timer.due) <= now;
204
+ if (!timer.cancelled && !timer.taken && Date.parse(timer.due) <= now)
205
+ add(timerEvent(ids.timer(timer.name), timer.name, timer.due));
206
+ }
207
+ const taken = new Set(held);
208
+ const order = (a: string, b: string) => {
209
+ const x = events.get(a) as WaitEvent;
210
+ const y = events.get(b) as WaitEvent;
211
+ return (
212
+ Date.parse(x.at) - Date.parse(y.at) ||
213
+ RANK[x.origin] - RANK[y.origin] ||
214
+ (met.get(a) as number) - (met.get(b) as number)
215
+ );
216
+ };
217
+ const started = [...events.values()].find((event) => event.starts);
218
+ return {
219
+ events,
220
+ unacknowledged: [...events.keys()]
221
+ .filter((id) => !acknowledged.has(id) && !taken.has(id))
222
+ .sort(order),
223
+ ...(held ? { held } : {}),
224
+ ...(held && heldTakes ? { heldTakes } : {}),
225
+ ...(lastWait ? { lastWait } : {}),
226
+ turns,
227
+ timers,
228
+ races,
229
+ ...(started ? { started } : {}),
230
+ now,
231
+ ids,
232
+ };
233
+ }
234
+
235
+ function timerEvent(id: string, name: string, due: string): WaitEvent {
236
+ return { id, name: "timer:due", data: { name }, from: name, at: due, origin: "timer" };
237
+ }
238
+
239
+ function timeEvent(id: string, branch: string, due: string): WaitEvent {
240
+ return { id, name: "at:due", data: {}, from: branch, at: due, origin: "time" };
241
+ }
242
+
243
+ /** The events not acknowledged that a wait taking `takes` would take, in order. */
244
+ export function readyFor(state: WaitState, takes: readonly WaitTake[] | undefined): string[] {
245
+ return state.unacknowledged.filter((id) => {
246
+ const event = state.events.get(id);
247
+ return event !== undefined && wouldTake(event, takes);
248
+ });
249
+ }
250
+
251
+ /** Whether `ids`, a crash's held batch, is a select's time read from state, alone. */
252
+ function isTimeRead(state: WaitState, ids: readonly string[]): boolean {
253
+ return ids.length === 1 && state.events.get(ids[0] as string)?.origin === "time";
254
+ }
255
+
256
+ /** Timers neither fired nor cancelled, earliest first. */
257
+ function pendingTimers(state: WaitState): TimerState[] {
258
+ return [...state.timers.values()]
259
+ .filter((timer) => !timer.fired && !timer.cancelled)
260
+ .sort((a, b) => Date.parse(a.due) - Date.parse(b.due));
261
+ }
262
+
263
+ /**
264
+ * A select's times read from state that it hasn't returned, earliest first, and of two at once the
265
+ * branch declared first. A time returned is no deadline, so state left as it was doesn't spin.
266
+ */
267
+ export function pendingTimes(
268
+ state: WaitState,
269
+ takes: readonly WaitTake[] | undefined,
270
+ ): { branch: string; due: string }[] {
271
+ return (takes ?? [])
272
+ .flatMap((take, order) =>
273
+ take.name === "at:due" &&
274
+ take.from !== undefined &&
275
+ take.due !== undefined &&
276
+ !state.events.has(state.ids.at(take.from, take.due))
277
+ ? [{ branch: take.from, due: take.due, order }]
278
+ : [],
279
+ )
280
+ .sort((a, b) => Date.parse(a.due) - Date.parse(b.due) || a.order - b.order)
281
+ .map(({ branch, due }) => ({ branch, due }));
282
+ }
283
+
284
+ /**
285
+ * When the earliest timer, or time read from state, a wait taking `takes` would take is due, epoch
286
+ * ms; undefined with none.
287
+ */
288
+ export function nextTimer(
289
+ state: WaitState,
290
+ takes: readonly WaitTake[] | undefined,
291
+ ): number | undefined {
292
+ const timer = pendingTimers(state).find((each) =>
293
+ wouldTake({ name: "timer:due", from: each.name }, takes),
294
+ );
295
+ const time = pendingTimes(state, takes)[0];
296
+ const dues = [timer?.due, time?.due].flatMap((due) => (due ? [Date.parse(due)] : []));
297
+ return dues.length > 0 ? Math.min(...dues) : undefined;
298
+ }
299
+
300
+ /**
301
+ * Whether a wait taking `takes` has something to take by the clock `state` was folded at: an
302
+ * event, or a timer due. The one test for taking, parking, and waking a parked run.
303
+ */
304
+ export function hasWork(state: WaitState, takes: readonly WaitTake[] | undefined): boolean {
305
+ if (readyFor(state, takes).length > 0) return true;
306
+ const due = nextTimer(state, takes);
307
+ return due !== undefined && due <= state.now;
308
+ }
309
+
310
+ /**
311
+ * What would wake a run parked at a wait taking `takes`, by the clock `state` was folded at: an
312
+ * event it takes, a timer of its due, or nothing.
313
+ */
314
+ export function wakeCause(
315
+ state: WaitState,
316
+ takes: readonly WaitTake[] | undefined,
317
+ ): "event" | "timer" | undefined {
318
+ const ready = readyFor(state, takes);
319
+ if (ready.some((id) => state.events.get(id)?.origin !== "timer")) return "event";
320
+ return ready.length > 0 || hasWork(state, takes) ? "timer" : undefined;
321
+ }
322
+
323
+ /**
324
+ * What an attempt's first `receive` outside a stage returns without waiting: the batch an earlier
325
+ * attempt took and never acknowledged. A select's time read from state is the select's: the
326
+ * receive acknowledges it instead.
327
+ */
328
+ export function firstReceive(state: WaitState): string[] | undefined {
329
+ return state.held && !isTimeRead(state, state.held) ? state.held : undefined;
330
+ }
331
+
332
+ /**
333
+ * What an attempt's first `select` does with the held batch: returns the held event when it is the
334
+ * select's own and still taken by `takes`, or waits, `returns` when what it leaves held goes back
335
+ * to waiting rather than being acknowledged. A held time read from state whose state has moved is
336
+ * acknowledged, not returned.
337
+ */
338
+ export function firstSelect(
339
+ state: WaitState,
340
+ takes: readonly WaitTake[],
341
+ ): { event: WaitEvent } | { returns: boolean } {
342
+ // A select's own, not a receive's batch of one.
343
+ const held =
344
+ state.held?.length === 1 && state.heldTakes
345
+ ? state.events.get(state.held[0] as string)
346
+ : undefined;
347
+ if (held?.origin === "time") {
348
+ const still = takes.some(
349
+ (take) => take.name === "at:due" && take.from === held.from && take.due === held.at,
350
+ );
351
+ return still ? { event: held } : { returns: false };
352
+ }
353
+ if (held && wouldTake(held, takes)) return { event: held };
354
+ return { returns: state.held !== undefined };
355
+ }
356
+
357
+ /** What a wait outside a stage and a race acknowledges as it asks for the next: the held batch. */
358
+ export function acksOf(state: WaitState): string[] | undefined {
359
+ return state.held;
360
+ }
361
+
362
+ /** Of the events none took when the run returned, those that aren't the one that started it. */
363
+ export function untakenAtReturn(state: WaitState): number {
364
+ return state.unacknowledged.filter((id) => id !== state.started?.id).length;
365
+ }
366
+
367
+ /** A turn's three parts: code rerun from the top begins the same turn again, a turn of its own. */
368
+ function turnKey(turn: { agent: string; turn: string }, began: number): string {
369
+ return JSON.stringify([turn.agent, turn.turn, began]);
370
+ }
371
+
372
+ /**
373
+ * The turns `start()` began that have no end: an attempt ended with them in flight, and the next
374
+ * claim ends each, before any wait. Read from the lines the claim acts on.
375
+ */
376
+ export function unendedTurns(lines: readonly RunLine[]): TurnStartedLine[] {
377
+ const kept = unfenced(lines);
378
+ const ended = new Set(
379
+ kept.flatMap((line) => (line.kind === "turn-ended" ? [turnKey(line, line.began)] : [])),
380
+ );
381
+ return kept.flatMap((line) =>
382
+ line.kind === "turn-started" && !ended.has(turnKey(line, line.attempt)) ? [line] : [],
383
+ );
384
+ }
385
+
386
+ /** An agent's turn an earlier attempt started whose end no wait took: its id and meta. */
387
+ export type RestoredTurn = { turn: string; meta: JsonValue };
388
+
389
+ /** Each agent's last turn whose end no wait has taken: what `agent.turn` is when the agent opens. */
390
+ export function restoredTurns(state: WaitState): Map<string, RestoredTurn> {
391
+ const untaken = new Set([...state.unacknowledged, ...(state.held ?? [])]);
392
+ const ends = new Map<string, string>();
393
+ for (const [id, event] of state.events)
394
+ if (event.origin === "end" && event.began !== undefined)
395
+ ends.set(
396
+ turnKey({ agent: event.from, turn: (event.data as AgentEnded).turn }, event.began),
397
+ id,
398
+ );
399
+ const restored = new Map<string, RestoredTurn>();
400
+ for (const turn of state.turns) {
401
+ const id = ends.get(turnKey(turn, turn.attempt));
402
+ if (id !== undefined && untaken.has(id))
403
+ restored.set(turn.agent, { turn: turn.turn, meta: turn.meta });
404
+ }
405
+ return restored;
406
+ }
407
+
408
+ /** Each attempt's last wait label: what a parked attempt was waiting for. */
409
+ export function waitLabels(lines: readonly RunLine[]): Map<number, string> {
410
+ const labels = new Map<number, string>();
411
+ for (const line of unfenced(lines))
412
+ if (line.kind === "wait" && line.label !== undefined) labels.set(line.attempt, line.label);
413
+ return labels;
414
+ }
415
+
416
+ /**
417
+ * The name awf records an event under: awf's keep `agent:ended` and `participant:reply`, from their
418
+ * key, whatever name the workflow declares and is handed, `{key}:ended` or `{key}:reply`.
419
+ */
420
+ export function recordName(event: { name: string; from: string }): string {
421
+ const awf = awfEventOf(event.name);
422
+ if (!awf || awf.key !== event.from) return event.name;
423
+ return awf.kind === "ended" ? "agent:ended" : "participant:reply";
424
+ }
425
+
426
+ /** Whether a wait that takes `take` takes `event`, under its recorded name or its declared one. */
427
+ export function matches(
428
+ event: { name: string; from: string; data?: unknown },
429
+ take: WaitTake,
430
+ ): boolean {
431
+ if (recordName(event) !== take.name) return false;
432
+ if (take.from !== undefined && event.from !== take.from) return false;
433
+ if (take.turn === undefined) return true;
434
+ const data = event.data as { turn?: unknown } | null | undefined;
435
+ return typeof data === "object" && data !== null && data.turn === take.turn;
436
+ }
437
+
438
+ /** Whether a wait taking `wanted`, or everything when it names nothing, takes `event`. */
439
+ function wouldTake(
440
+ event: { name: string; from: string; data?: unknown },
441
+ wanted: readonly WaitTake[] | undefined,
442
+ ): boolean {
443
+ return wanted === undefined || wanted.some((take) => matches(event, take));
444
+ }
445
+
446
+ /** The first of `branches` that takes `event`. */
447
+ export function branchOf(
448
+ branches: readonly (readonly [string, WaitTake])[],
449
+ event: { name: string; from: string; data?: unknown },
450
+ ): string | undefined {
451
+ return branches.find(([, take]) => matches(event, take))?.[0];
452
+ }