@agentswf/engine 0.1.0-beta.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/package.json +13 -9
- package/src/accounting/format.ts +4 -3
- package/src/accounting/spent.ts +128 -0
- package/src/accounting/summary.ts +40 -32
- package/src/agent-cli/cli.ts +28 -55
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +4 -4
- package/src/attempt-output.ts +13 -31
- package/src/attempt-view.ts +33 -32
- package/src/author-typebox.ts +13 -0
- package/src/client/checks.ts +94 -0
- package/src/client/exported.ts +17 -0
- package/src/client/index.ts +122 -0
- package/src/client/locate.ts +34 -0
- package/src/client/published.ts +3 -0
- package/src/client/runs.ts +337 -0
- package/src/command-line.ts +64 -0
- package/src/commands.ts +7 -1
- package/src/control-plane.ts +4 -6
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +27 -28
- package/src/deadlines.ts +6 -40
- package/src/decisions/directory.ts +50 -37
- package/src/decisions/fake.ts +1 -1
- package/src/decisions/openrouter.ts +4 -2
- package/src/decisions/seam.ts +4 -4
- package/src/display-path.ts +3 -3
- package/src/duration.ts +19 -33
- package/src/emit-command.ts +38 -22
- package/src/emit.ts +148 -59
- package/src/event-ids.ts +31 -0
- package/src/files.ts +44 -4
- package/src/hand-over.ts +150 -0
- package/src/help/allowance.txt +4 -3
- package/src/help/emit.txt +11 -7
- package/src/help/inbox.txt +5 -3
- package/src/help/logs.txt +7 -5
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +13 -8
- package/src/help/runs.txt +4 -2
- package/src/help/tick.txt +9 -4
- package/src/herdr-placement.ts +3 -33
- package/src/herdr-run-session.ts +5 -13
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +54 -136
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +42 -56
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.ts.template +3 -2
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +7 -19
- package/src/logs-command.ts +134 -53
- package/src/message-operation.ts +6 -3
- package/src/messaging/messaging.ts +8 -10
- package/src/messaging/text.ts +2 -2
- package/src/open-command.ts +770 -0
- package/src/operation-events.ts +4 -6
- package/src/operation-liveness.ts +19 -4
- package/src/operator-cli.ts +206 -90
- package/src/operator-runtime.ts +24 -2
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +49 -96
- package/src/progress-view.ts +2 -2
- package/src/project-engine.ts +79 -0
- package/src/projects.ts +13 -2
- package/src/refusals.ts +119 -0
- package/src/result-slots.ts +11 -31
- package/src/result-validation.ts +2 -2
- package/src/run-command.ts +27 -31
- package/src/run-commands.ts +42 -114
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/run-policy/stage-plan.ts +154 -0
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +165 -41
- package/src/run-read.ts +109 -41
- package/src/run-state.ts +170 -50
- package/src/run-text.ts +54 -0
- package/src/run-unreachable.ts +18 -0
- package/src/run-usage.ts +17 -11
- package/src/run-view.ts +73 -14
- package/src/runs-command.ts +81 -63
- package/src/runs.ts +284 -222
- package/src/sandbox-homes.ts +65 -13
- package/src/sandboxes.ts +182 -28
- package/src/session-lines.ts +93 -0
- package/src/skills/run-skills.ts +8 -1
- package/src/sources.ts +2 -39
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +111 -153
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +37 -19
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +66 -18
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +11 -30
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +49 -2
- package/src/workflow-runner.ts +525 -618
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +1 -1
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +28 -29
- package/src/workflow-testing/index.ts +9 -27
- package/src/workflow-testing/run-test.ts +201 -286
- package/src/workflow-testing/script.ts +7 -39
- package/src/deprecations.ts +0 -177
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -972
- package/src/stage-plan.ts +0 -186
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- package/src/workflow-testing/workflow-testing.typecheck.ts +0 -67
|
@@ -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
|
+
}
|