@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.
- 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 +35 -22
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +5 -7
- package/src/attempt-output.ts +15 -32
- package/src/attempt-view.ts +34 -33
- 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 +41 -11
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +41 -33
- package/src/deadlines.ts +40 -7
- package/src/decisions/directory.ts +51 -40
- 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 +43 -27
- package/src/emit-command.ts +88 -58
- package/src/emit.ts +254 -56
- 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 +18 -6
- package/src/help/inbox.txt +6 -4
- package/src/help/logs.txt +9 -6
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +30 -11
- 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 +7 -15
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +56 -133
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +40 -57
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.test.ts.template +3 -3
- package/src/init-template/hello/workflow.ts.template +5 -4
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +15 -12
- package/src/logs-command.ts +128 -47
- package/src/message-operation.ts +6 -3
- package/src/messaging/limits.ts +4 -4
- package/src/messaging/messaging.ts +31 -28
- package/src/messaging/routes.ts +1 -1
- package/src/messaging/text.ts +9 -7
- 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 +224 -94
- package/src/operator-runtime.ts +29 -7
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +105 -59
- package/src/progress-view.ts +10 -10
- 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 +45 -28
- package/src/run-commands.ts +43 -115
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/{stage-plan.ts → run-policy/stage-plan.ts} +46 -42
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +205 -43
- 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 +18 -12
- package/src/run-view.ts +81 -17
- package/src/runs-command.ts +83 -63
- package/src/runs.ts +310 -212
- 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 +28 -29
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +119 -131
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +36 -14
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +73 -26
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +25 -16
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +54 -4
- package/src/workflow-runner.ts +805 -580
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +3 -3
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +121 -53
- package/src/workflow-testing/index.ts +33 -842
- package/src/workflow-testing/run-test.ts +1128 -0
- package/src/workflow-testing/script.ts +74 -45
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -990
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- package/src/workflow-testing/workflow-testing.typecheck.ts +0 -64
package/src/stage-ledger.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
STAGE_RECORD_VERSION,
|
|
3
|
-
type StageNeed,
|
|
4
|
-
type StageRecord,
|
|
5
|
-
type TurnRecord,
|
|
6
|
-
} from "@agentswf/contract/records";
|
|
1
|
+
import type { RunView, StageLine, StageNeed } from "@agentswf/contract/records";
|
|
7
2
|
import type { JsonSchema } from "@agentswf/contract/schema";
|
|
8
3
|
import type { JsonValue, StageOutcome, StageSummary } from "@agentswf/contract/workflow";
|
|
9
4
|
import type { RunEventBody } from "./run-event-log";
|
|
10
|
-
import {
|
|
11
|
-
import { planStage, planStageByContent } from "./stage-plan";
|
|
5
|
+
import type { RunLines } from "./run-lines";
|
|
6
|
+
import { planStage, planStageByContent } from "./run-policy/stage-plan";
|
|
7
|
+
import { recordedStages, StageReuse, upcomingStages } from "./run-policy/stages";
|
|
8
|
+
import { stageNameProblem } from "./runs";
|
|
12
9
|
import { placeholderOf } from "./stage-schema";
|
|
13
10
|
import { primaryFailure, WorkflowStopped } from "./stopped";
|
|
14
11
|
|
|
@@ -28,11 +25,11 @@ export type StageProgress = Omit<StageSummary, "value" | "outcome" | "spanMs"> &
|
|
|
28
25
|
outcome?: StageOutcome;
|
|
29
26
|
startedAt: number;
|
|
30
27
|
endedAt?: number;
|
|
31
|
-
/** When a reused stage's
|
|
28
|
+
/** When a reused stage's line was written, which the view ages. */
|
|
32
29
|
recordedAt?: number;
|
|
33
30
|
};
|
|
34
31
|
|
|
35
|
-
/** A stage without its value, which its stage
|
|
32
|
+
/** A stage without its value, which its stage line keeps. */
|
|
36
33
|
export function withoutValue<Stage extends { value?: JsonValue }>(
|
|
37
34
|
stage: Stage,
|
|
38
35
|
): Omit<Stage, "value"> {
|
|
@@ -48,22 +45,24 @@ export type StageEntry = {
|
|
|
48
45
|
summary(value: JsonValue): string | undefined;
|
|
49
46
|
};
|
|
50
47
|
|
|
51
|
-
/** A stage entered: reused from its
|
|
48
|
+
/** A stage entered: reused from its line or a value given without running, or run. */
|
|
52
49
|
type EnteredStage =
|
|
53
50
|
/** `release` once the value is handed back: until then the stage counts as open. */
|
|
54
51
|
| { kind: "reuse"; value: JsonValue | undefined; release(): void }
|
|
55
52
|
| { kind: "run"; stage: OpenStage };
|
|
56
53
|
|
|
57
54
|
/**
|
|
58
|
-
* One attempt's stages: which are entered, which one is open, and each one's
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
55
|
+
* One attempt's stages: which are entered, which one is open, and each one's end, a `stage` line of
|
|
56
|
+
* `run.jsonl`. A stage is a name entered at most once per attempt, one at a time. Until the
|
|
57
|
+
* attempt's start point each is reused or stops it, as the stage plan says; the rest is the
|
|
58
|
+
* runner's.
|
|
62
59
|
*/
|
|
63
60
|
export class StageLedger {
|
|
64
61
|
/** Each stage entered, in order, as it stands. */
|
|
65
62
|
readonly #stages = new Map<string, StageProgress & { value?: JsonValue }>();
|
|
66
63
|
readonly #reused = new Set<string>();
|
|
64
|
+
/** The line each stage reused from an earlier attempt was reused from. */
|
|
65
|
+
readonly #reusedLines = new Map<string, StageLine>();
|
|
67
66
|
/** Whether the attempt has reached its start point, after which every stage runs. */
|
|
68
67
|
#started = false;
|
|
69
68
|
readonly #writes = new Set<Promise<void>>();
|
|
@@ -86,25 +85,30 @@ export class StageLedger {
|
|
|
86
85
|
*/
|
|
87
86
|
readonly #looked = new Set<string>();
|
|
88
87
|
#needed: WorkflowStopped | undefined;
|
|
89
|
-
/**
|
|
90
|
-
readonly #
|
|
88
|
+
/** What each stage entered may reuse, this attempt's own lines included. */
|
|
89
|
+
readonly #reuse: StageReuse;
|
|
90
|
+
/** The run's stages as the attempt began, each by its latest line, in the order they started. */
|
|
91
|
+
readonly #recorded: StageLine[];
|
|
92
|
+
/** Of those, the ones a continue may still come to: none the last redo left behind. */
|
|
93
|
+
readonly #upcoming: string[];
|
|
94
|
+
readonly #attempt: number;
|
|
91
95
|
|
|
92
96
|
constructor(
|
|
93
97
|
private readonly options: {
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
/** The attempt's writer on `run.jsonl`, which each stage's end is a line of. */
|
|
99
|
+
lines: Pick<RunLines, "attempt" | "append">;
|
|
100
|
+
/** `run.jsonl` as the attempt began: what a continue may reuse, and the last redo. */
|
|
101
|
+
run?: Pick<RunView, "stages" | "redo">;
|
|
102
|
+
/** Each earlier attempt's `meta.version`, which a line it wrote was written under. */
|
|
103
|
+
versions?: ReadonlyMap<number, string>;
|
|
96
104
|
workflowVersion?: string;
|
|
97
|
-
/** The attempt's turns as they settled, which name the stage each ran in. */
|
|
98
|
-
turns: () => readonly TurnRecord[];
|
|
99
|
-
/** The run's stage records as the attempt began: what a continue may reuse. */
|
|
100
|
-
records?: ReadonlyMap<string, StageRecord>;
|
|
101
105
|
/** `--from-stage`: the stage the attempt starts at. */
|
|
102
106
|
fromStage?: string;
|
|
103
|
-
/** `--values`: the values of stages before `fromStage` that have no
|
|
107
|
+
/** `--values`: the values of stages before `fromStage` that have no line to reuse. */
|
|
104
108
|
values?: ReadonlyMap<string, JsonValue>;
|
|
105
109
|
/**
|
|
106
110
|
* A run of a workflow with `events`: a stage runs once ever, reused wherever it is entered,
|
|
107
|
-
* as `planStageByContent` says
|
|
111
|
+
* as `planStageByContent` says, and no `values`. Its start point is `fromStage` alone.
|
|
108
112
|
*/
|
|
109
113
|
byContent?: boolean;
|
|
110
114
|
/** Writes the run's event log. */
|
|
@@ -112,7 +116,11 @@ export class StageLedger {
|
|
|
112
116
|
now?: () => Date;
|
|
113
117
|
},
|
|
114
118
|
) {
|
|
115
|
-
|
|
119
|
+
const run = options.run ?? { stages: {} };
|
|
120
|
+
this.#attempt = options.lines.attempt;
|
|
121
|
+
this.#reuse = new StageReuse(run, this.#attempt);
|
|
122
|
+
this.#recorded = recordedStages(run);
|
|
123
|
+
this.#upcoming = upcomingStages(run).map((line) => line.name);
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
/** The stage open now, if one is. */
|
|
@@ -126,13 +134,12 @@ export class StageLedger {
|
|
|
126
134
|
}
|
|
127
135
|
|
|
128
136
|
/**
|
|
129
|
-
* The run's
|
|
130
|
-
* point nothing runs, so for an attempt that never reached it they are still the
|
|
137
|
+
* The run's stages as the attempt began, each by its latest line, in the order they started.
|
|
138
|
+
* Before the start point nothing runs, so for an attempt that never reached it they are still the
|
|
139
|
+
* run's.
|
|
131
140
|
*/
|
|
132
|
-
get recorded():
|
|
133
|
-
return
|
|
134
|
-
a.started.localeCompare(b.started),
|
|
135
|
-
);
|
|
141
|
+
get recorded(): StageLine[] {
|
|
142
|
+
return this.#recorded;
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
/** The stop that ended the attempt, even if the workflow caught it. */
|
|
@@ -142,7 +149,7 @@ export class StageLedger {
|
|
|
142
149
|
|
|
143
150
|
/**
|
|
144
151
|
* Each stage entered that has ended, in order: reused, run, or stopped by the plan as it was
|
|
145
|
-
* entered, which ran nothing and
|
|
152
|
+
* entered, which ran nothing and wrote no line.
|
|
146
153
|
*/
|
|
147
154
|
get summaries(): StageSummary[] {
|
|
148
155
|
return [...this.#stages.values()].flatMap(
|
|
@@ -161,11 +168,11 @@ export class StageLedger {
|
|
|
161
168
|
// By content, an earlier event's stage may never be entered again: none is upcoming.
|
|
162
169
|
const upcoming = this.options.byContent
|
|
163
170
|
? []
|
|
164
|
-
: this.
|
|
165
|
-
const recorded = (stage: string) => this.options.records?.get(stage)?.ended;
|
|
171
|
+
: this.#upcoming.filter((stage) => !this.#stages.has(stage));
|
|
166
172
|
return {
|
|
167
173
|
stages: [...this.#stages.values()].map((entered) => {
|
|
168
|
-
const ended =
|
|
174
|
+
const ended =
|
|
175
|
+
entered.source === "reused" ? this.#reusedLines.get(entered.stage)?.at : undefined;
|
|
169
176
|
const stage = withoutValue(entered);
|
|
170
177
|
return ended === undefined ? stage : { ...stage, recordedAt: Date.parse(ended) };
|
|
171
178
|
}),
|
|
@@ -200,10 +207,9 @@ export class StageLedger {
|
|
|
200
207
|
|
|
201
208
|
/**
|
|
202
209
|
* Enters `name`: reused or provided when the plan says so, or opened to run. A value provided is
|
|
203
|
-
*
|
|
204
|
-
* one open, or a
|
|
205
|
-
* that fails the attempt.
|
|
206
|
-
* `replaced/`.
|
|
210
|
+
* written as its line before it is handed back. Throws why it can't be: a bad name, a second
|
|
211
|
+
* entry, another one open, or a line that stops the attempt. Entered after a stop, the stop was
|
|
212
|
+
* caught, and that fails the attempt.
|
|
207
213
|
*/
|
|
208
214
|
async enter(name: string, entry: StageEntry): Promise<EnteredStage> {
|
|
209
215
|
// Looking on, a stop caught is the one for the values missing, which ends the attempt anyway.
|
|
@@ -212,9 +218,13 @@ export class StageLedger {
|
|
|
212
218
|
this.#check(name);
|
|
213
219
|
const at = this.#now().getTime();
|
|
214
220
|
if (this.options.byContent) return this.#enterByContent(name, entry, at);
|
|
221
|
+
const line = this.#reuse.enter(name);
|
|
222
|
+
const lineVersion = line && this.options.versions?.get(line.attempt);
|
|
215
223
|
const decision = planStage(
|
|
216
224
|
{
|
|
217
|
-
|
|
225
|
+
...(line === undefined ? {} : { line }),
|
|
226
|
+
...(lineVersion === undefined ? {} : { lineVersion }),
|
|
227
|
+
recorded: this.#recorded.map((each) => each.name),
|
|
218
228
|
...(this.options.fromStage === undefined ? {} : { fromStage: this.options.fromStage }),
|
|
219
229
|
...(this.options.values === undefined ? {} : { values: this.options.values }),
|
|
220
230
|
...(this.options.workflowVersion === undefined
|
|
@@ -253,8 +263,8 @@ export class StageLedger {
|
|
|
253
263
|
}
|
|
254
264
|
if (decision.kind === "reuse" || decision.kind === "provide") {
|
|
255
265
|
const held = this.#hold(name, decision.value);
|
|
256
|
-
let
|
|
257
|
-
if (decision.kind === "reuse")
|
|
266
|
+
let line: StageLine;
|
|
267
|
+
if (decision.kind === "reuse") line = decision.line;
|
|
258
268
|
else {
|
|
259
269
|
const written = this.#provide(name, decision.value, entry, at);
|
|
260
270
|
// Its failure is the stage's, thrown below.
|
|
@@ -265,13 +275,14 @@ export class StageLedger {
|
|
|
265
275
|
),
|
|
266
276
|
);
|
|
267
277
|
try {
|
|
268
|
-
|
|
278
|
+
line = await written;
|
|
269
279
|
} catch (error) {
|
|
270
280
|
held.release();
|
|
271
281
|
throw error;
|
|
272
282
|
}
|
|
273
283
|
}
|
|
274
284
|
this.#reused.add(name);
|
|
285
|
+
if (decision.kind === "reuse") this.#reusedLines.set(name, line);
|
|
275
286
|
this.options.event?.(
|
|
276
287
|
{
|
|
277
288
|
kind: "stage-ended",
|
|
@@ -281,7 +292,7 @@ export class StageLedger {
|
|
|
281
292
|
},
|
|
282
293
|
at,
|
|
283
294
|
);
|
|
284
|
-
const { attempt, summary, provided } =
|
|
295
|
+
const { attempt, summary, provided } = line;
|
|
285
296
|
this.#stages.set(name, {
|
|
286
297
|
stage: name,
|
|
287
298
|
source: "reused",
|
|
@@ -295,28 +306,21 @@ export class StageLedger {
|
|
|
295
306
|
});
|
|
296
307
|
return held;
|
|
297
308
|
}
|
|
298
|
-
// Open while the records it outdates are moved, so no other stage enters beside it.
|
|
299
|
-
const stage = this.#opened(name, at);
|
|
300
309
|
if (decision.start) {
|
|
301
|
-
try {
|
|
302
|
-
await replaceStale(this.options.runDir, name, this.#reused);
|
|
303
|
-
} catch (error) {
|
|
304
|
-
// Not entered after all: nothing ran, so nothing is recorded, and no stage is left open.
|
|
305
|
-
this.#stages.delete(name);
|
|
306
|
-
this.#open = undefined;
|
|
307
|
-
throw error;
|
|
308
|
-
}
|
|
309
310
|
this.#started = true;
|
|
311
|
+
this.#reuse.redoFrom(name);
|
|
310
312
|
}
|
|
311
|
-
return { kind: "run", stage };
|
|
313
|
+
return { kind: "run", stage: this.#opened(name, at, decision.start) };
|
|
312
314
|
}
|
|
313
315
|
|
|
314
316
|
/**
|
|
315
|
-
* Enters a stage by content: reused from its succeeded
|
|
316
|
-
*
|
|
317
|
+
* Enters a stage by content: reused from its succeeded line, this attempt's included, run when it
|
|
318
|
+
* has none or a failed one. After the redo's stage in this attempt, a line older than the redo is
|
|
319
|
+
* outdated and runs again: see `StageRedo`.
|
|
317
320
|
*/
|
|
318
321
|
async #enterByContent(name: string, entry: StageEntry, at: number): Promise<EnteredStage> {
|
|
319
|
-
|
|
322
|
+
if (!this.#started && name === this.options.fromStage) return this.#startByContent(name, at);
|
|
323
|
+
const decision = planStageByContent(this.#reuse.enter(name), entry.result);
|
|
320
324
|
if (decision.kind === "stop") {
|
|
321
325
|
this.#stopped = new WorkflowStopped(decision.reason, name, true);
|
|
322
326
|
this.#stoppedAt(name, at);
|
|
@@ -327,7 +331,8 @@ export class StageLedger {
|
|
|
327
331
|
this.#reused.add(name);
|
|
328
332
|
// Entered again in the attempt that ran or reused it: nothing more to show or log.
|
|
329
333
|
if (this.#stages.get(name)?.outcome === "succeeded") return held;
|
|
330
|
-
|
|
334
|
+
this.#reusedLines.set(name, decision.line);
|
|
335
|
+
const { attempt, summary } = decision.line;
|
|
331
336
|
this.options.event?.(
|
|
332
337
|
{ kind: "stage-ended", stage: name, outcome: "succeeded", source: "reused" },
|
|
333
338
|
at,
|
|
@@ -347,6 +352,19 @@ export class StageLedger {
|
|
|
347
352
|
return { kind: "run", stage: this.#opened(name, at) };
|
|
348
353
|
}
|
|
349
354
|
|
|
355
|
+
/**
|
|
356
|
+
* `--from-stage` reached by content: it runs, and this attempt is the redo, which its line says.
|
|
357
|
+
* An attempt that dies while it runs writes no line, so the old one stands and the redo, which
|
|
358
|
+
* nothing then shows, is issued again. The stages after it rerun as they are entered, not all at
|
|
359
|
+
* once: one never entered keeps its line.
|
|
360
|
+
*/
|
|
361
|
+
#startByContent(name: string, at: number): EnteredStage {
|
|
362
|
+
this.#reuse.enter(name);
|
|
363
|
+
this.#reuse.redoFrom(name);
|
|
364
|
+
this.#started = true;
|
|
365
|
+
return { kind: "run", stage: this.#opened(name, at, true) };
|
|
366
|
+
}
|
|
367
|
+
|
|
350
368
|
/** A stage the plan stopped as it was entered: shown stopped, though it ran nothing. */
|
|
351
369
|
#stoppedAt(name: string, at: number): void {
|
|
352
370
|
this.options.event?.({ kind: "stage-ended", stage: name, outcome: "stopped" }, at);
|
|
@@ -354,7 +372,7 @@ export class StageLedger {
|
|
|
354
372
|
stage: name,
|
|
355
373
|
source: "ran",
|
|
356
374
|
outcome: "stopped",
|
|
357
|
-
attempt: this
|
|
375
|
+
attempt: this.#attempt,
|
|
358
376
|
startedAt: at,
|
|
359
377
|
endedAt: at,
|
|
360
378
|
});
|
|
@@ -422,34 +440,29 @@ export class StageLedger {
|
|
|
422
440
|
return (interrupted ? undefined : this.#stopForNeeded()) ?? failure;
|
|
423
441
|
}
|
|
424
442
|
|
|
425
|
-
/**
|
|
443
|
+
/** Writes a value given for a stage, or none for one that returns nothing: no turn ran it. */
|
|
426
444
|
async #provide(
|
|
427
445
|
name: string,
|
|
428
446
|
value: JsonValue | undefined,
|
|
429
447
|
entry: StageEntry,
|
|
430
448
|
at: number,
|
|
431
|
-
): Promise<
|
|
449
|
+
): Promise<StageLine> {
|
|
432
450
|
const time = new Date(at).toISOString();
|
|
433
451
|
const summary = value === undefined ? undefined : entry.summary(value);
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
452
|
+
const line: StageLine = {
|
|
453
|
+
kind: "stage",
|
|
454
|
+
attempt: this.#attempt,
|
|
455
|
+
at: time,
|
|
456
|
+
name,
|
|
438
457
|
outcome: "succeeded",
|
|
439
458
|
started: time,
|
|
440
|
-
ended: time,
|
|
441
|
-
...(this.options.workflowVersion === undefined
|
|
442
|
-
? {}
|
|
443
|
-
: { workflowVersion: this.options.workflowVersion }),
|
|
444
|
-
sessions: [],
|
|
445
459
|
...(summary === undefined ? {} : { summary }),
|
|
446
460
|
...(value === undefined ? {} : { value }),
|
|
447
461
|
provided: true,
|
|
448
462
|
};
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
return record;
|
|
463
|
+
await this.options.lines.append(line);
|
|
464
|
+
this.#reuse.wrote(line);
|
|
465
|
+
return line;
|
|
453
466
|
}
|
|
454
467
|
|
|
455
468
|
#check(name: string): void {
|
|
@@ -468,66 +481,55 @@ export class StageLedger {
|
|
|
468
481
|
}
|
|
469
482
|
}
|
|
470
483
|
|
|
471
|
-
|
|
484
|
+
/**
|
|
485
|
+
* Opens `name` to run, from `startedAt`, the one clock read its line's `started` and the log's
|
|
486
|
+
* `stage-started` both carry, which `awf runs` pairs them by. `redo` on the stage the attempt
|
|
487
|
+
* started at.
|
|
488
|
+
*/
|
|
489
|
+
#opened(name: string, startedAt: number, redo = false): OpenStage {
|
|
472
490
|
const entered: StageProgress & { value?: JsonValue } = {
|
|
473
491
|
stage: name,
|
|
474
492
|
source: "ran",
|
|
475
|
-
attempt: this
|
|
493
|
+
attempt: this.#attempt,
|
|
476
494
|
startedAt,
|
|
477
495
|
};
|
|
478
496
|
this.#stages.set(name, entered);
|
|
479
497
|
this.options.event?.({ kind: "stage-started", stage: name }, startedAt);
|
|
480
498
|
let ended: Promise<void> | undefined;
|
|
481
|
-
// A
|
|
482
|
-
const end = (fields: Pick<
|
|
499
|
+
// A line that fails to be written fails the stage with that error, whatever its work did.
|
|
500
|
+
const end = (fields: Pick<StageLine, "outcome" | "reason" | "summary" | "value">) => {
|
|
483
501
|
if (ended) return ended;
|
|
484
502
|
if (this.#open === stage) this.#open = undefined;
|
|
485
503
|
const at = this.#now();
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
504
|
+
const line: StageLine = {
|
|
505
|
+
kind: "stage",
|
|
506
|
+
attempt: this.#attempt,
|
|
507
|
+
at: at.toISOString(),
|
|
508
|
+
name,
|
|
490
509
|
outcome: fields.outcome,
|
|
491
510
|
...(fields.reason === undefined ? {} : { reason: fields.reason }),
|
|
492
511
|
started: new Date(startedAt).toISOString(),
|
|
493
|
-
ended: at.toISOString(),
|
|
494
|
-
...(this.options.workflowVersion === undefined
|
|
495
|
-
? {}
|
|
496
|
-
: { workflowVersion: this.options.workflowVersion }),
|
|
497
|
-
sessions: sessionsOf(this.options.turns(), name),
|
|
498
512
|
...(fields.summary === undefined ? {} : { summary: fields.summary }),
|
|
499
513
|
...(fields.value === undefined ? {} : { value: fields.value }),
|
|
514
|
+
...(redo ? { redo: true as const } : {}),
|
|
500
515
|
};
|
|
501
516
|
Object.assign(entered, {
|
|
502
|
-
outcome:
|
|
517
|
+
outcome: line.outcome,
|
|
503
518
|
endedAt: at.getTime(),
|
|
504
|
-
...(
|
|
505
|
-
...(
|
|
519
|
+
...(line.summary === undefined ? {} : { summary: line.summary }),
|
|
520
|
+
...(line.value === undefined ? {} : { value: line.value }),
|
|
506
521
|
});
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
: Promise.resolve()
|
|
516
|
-
)
|
|
517
|
-
.then(() => writeStageRecord(this.options.runDir, record))
|
|
518
|
-
.then(
|
|
519
|
-
() => {
|
|
520
|
-
this.#records.set(name, record);
|
|
521
|
-
},
|
|
522
|
-
(error: unknown) => {
|
|
523
|
-
entered.outcome = "failed";
|
|
524
|
-
throw error;
|
|
525
|
-
},
|
|
526
|
-
);
|
|
527
|
-
// Its outcome once the record is written: one that is not fails the stage.
|
|
522
|
+
ended = this.options.lines.append(line).then(
|
|
523
|
+
() => this.#reuse.wrote(line),
|
|
524
|
+
(error: unknown) => {
|
|
525
|
+
entered.outcome = "failed";
|
|
526
|
+
throw error;
|
|
527
|
+
},
|
|
528
|
+
);
|
|
529
|
+
// Its outcome once the line is written: one that is not fails the stage.
|
|
528
530
|
const logEnd = () =>
|
|
529
531
|
this.options.event?.(
|
|
530
|
-
{ kind: "stage-ended", stage: name, outcome: entered.outcome ??
|
|
532
|
+
{ kind: "stage-ended", stage: name, outcome: entered.outcome ?? line.outcome },
|
|
531
533
|
at.getTime(),
|
|
532
534
|
);
|
|
533
535
|
ended.then(logEnd, logEnd);
|
|
@@ -550,7 +552,7 @@ export class StageLedger {
|
|
|
550
552
|
}
|
|
551
553
|
|
|
552
554
|
/**
|
|
553
|
-
* Leaves the open stage `name` unended and
|
|
555
|
+
* Leaves the open stage `name` unended and without a line, as the run parked at a wait in it: the
|
|
554
556
|
* next attempt runs it again.
|
|
555
557
|
*/
|
|
556
558
|
abandon(name: string): void {
|
|
@@ -568,8 +570,8 @@ export class StageLedger {
|
|
|
568
570
|
}
|
|
569
571
|
|
|
570
572
|
/**
|
|
571
|
-
* Fails the stage still open, once what ran in it has settled, and waits for every
|
|
572
|
-
*
|
|
573
|
+
* Fails the stage still open, once what ran in it has settled, and waits for every line: none is
|
|
574
|
+
* written after the attempt ends.
|
|
573
575
|
*/
|
|
574
576
|
async close(): Promise<void> {
|
|
575
577
|
this.seal("the attempt ended");
|
|
@@ -594,17 +596,3 @@ export class StageLedger {
|
|
|
594
596
|
return this.#failures.get(first) ?? this.#closedOpen;
|
|
595
597
|
}
|
|
596
598
|
}
|
|
597
|
-
|
|
598
|
-
/** The session each of the stage's turns ran on, once per agent and session. */
|
|
599
|
-
function sessionsOf(turns: readonly TurnRecord[], stage: string): StageRecord["sessions"] {
|
|
600
|
-
const seen = new Map<string, StageRecord["sessions"][number]>();
|
|
601
|
-
for (const turn of turns) {
|
|
602
|
-
const session = turn.sessions.at(-1);
|
|
603
|
-
if (turn.stage !== stage || !session) continue;
|
|
604
|
-
const key = `${turn.agent}\0${session.harness}\0${session.id}`;
|
|
605
|
-
if (!seen.has(key)) {
|
|
606
|
-
seen.set(key, { agent: turn.agent, harness: session.harness, session: session.id });
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
return [...seen.values()];
|
|
610
|
-
}
|
package/src/stop-crashed.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ClaimedAttempt } from "@agentswf/contract/records";
|
|
2
2
|
import { runProcess } from "@agentswf/harness";
|
|
3
3
|
import { ensureRunSession, runSessionName } from "./herdr-run-session";
|
|
4
4
|
import { readMarks } from "./herdr-workspace-marks";
|
|
@@ -12,18 +12,19 @@ import { probeProcess } from "./runs";
|
|
|
12
12
|
* attempt orphaned isn't reached here; it is not stopped yet.
|
|
13
13
|
*/
|
|
14
14
|
export async function stopCrashed(
|
|
15
|
-
previous:
|
|
15
|
+
previous: ClaimedAttempt,
|
|
16
16
|
label: string,
|
|
17
17
|
context: { home: string; environment: Readonly<Record<string, string | undefined>> },
|
|
18
18
|
): Promise<void> {
|
|
19
19
|
// Only once its leader is gone for good: a live group's id is never handed out again, so a group
|
|
20
20
|
// still there is the attempt's leftovers, never another process that took its pid since. One an
|
|
21
21
|
// `awf run` typed in a terminal led is its pipeline's, which goes with it.
|
|
22
|
-
|
|
22
|
+
const { pid } = previous;
|
|
23
|
+
if (pid !== undefined && probeProcess(pid) === "gone") {
|
|
23
24
|
try {
|
|
24
|
-
process.kill(-
|
|
25
|
+
process.kill(-pid, "SIGTERM");
|
|
25
26
|
await Bun.sleep(500);
|
|
26
|
-
process.kill(-
|
|
27
|
+
process.kill(-pid, "SIGKILL");
|
|
27
28
|
} catch {
|
|
28
29
|
// No such group, or it ended at the first signal.
|
|
29
30
|
}
|
package/src/stopped.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { StageLine, StageNeed } from "@agentswf/contract/records";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Ends an attempt `stopped`, apart from `failed`: the workflow's `stop`, or a continue that can't
|
|
@@ -9,7 +9,7 @@ export class WorkflowStopped extends Error {
|
|
|
9
9
|
readonly reason: string,
|
|
10
10
|
/** The stage it stopped in; absent between stages. */
|
|
11
11
|
readonly stage?: string,
|
|
12
|
-
/** Whether going on takes `--from-stage {stage}`: a
|
|
12
|
+
/** Whether going on takes `--from-stage {stage}`: a line a continue can't reuse. */
|
|
13
13
|
readonly redo = false,
|
|
14
14
|
/** The stages whose values, given with `--values`, let the attempt start at a later one. */
|
|
15
15
|
readonly needs?: readonly StageNeed[],
|
|
@@ -23,8 +23,8 @@ export class WorkflowStopped extends Error {
|
|
|
23
23
|
export class FromStageUnreached extends WorkflowStopped {
|
|
24
24
|
constructor(
|
|
25
25
|
readonly fromStage: string,
|
|
26
|
-
/**
|
|
27
|
-
readonly recorded: readonly
|
|
26
|
+
/** Each of the run's stages by its latest line, in the order they started: those to choose from. */
|
|
27
|
+
readonly recorded: readonly StageLine[],
|
|
28
28
|
) {
|
|
29
29
|
super(`never reached ${fromStage}`);
|
|
30
30
|
}
|
package/src/test-command.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { constants } from "node:os";
|
|
3
|
-
import { resolve } from "node:path";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import type { Out } from "./command-line";
|
|
5
6
|
import { parseDuration } from "./duration";
|
|
6
7
|
import { messageOf } from "./errors";
|
|
7
8
|
import help from "./help/test.txt" with { type: "text" };
|
|
8
|
-
import
|
|
9
|
+
import { checkProjectEngine } from "./project-engine";
|
|
10
|
+
import { NO_JSON, printRefusal } from "./refusals";
|
|
9
11
|
|
|
10
12
|
const PRELOAD = fileURLToPath(new URL("./workflow-test-preload.ts", import.meta.url));
|
|
11
13
|
|
|
12
14
|
export const testUsage = help.trimEnd();
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
type TestCommand = { paths: string[]; bunArgs: string[] };
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* `awf test`: the workflow tests under the given paths, with Bun's test runner. Their output goes
|
|
@@ -22,16 +24,26 @@ export async function testCommand(
|
|
|
22
24
|
cwd: string,
|
|
23
25
|
io: Out & { output?: Out; signal?: AbortSignal },
|
|
24
26
|
): Promise<number> {
|
|
25
|
-
let tests: TestCommand
|
|
27
|
+
let tests: TestCommand;
|
|
26
28
|
try {
|
|
27
29
|
tests = parseTestCommand(argv, cwd);
|
|
28
30
|
} catch (error) {
|
|
29
|
-
io
|
|
31
|
+
printRefusal(io, NO_JSON, error, "usage", testUsage);
|
|
30
32
|
return 2;
|
|
31
33
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
try {
|
|
35
|
+
// Each test's workflow gets this engine's author surface: its project, and each path's, must
|
|
36
|
+
// run on it (ADR 0015).
|
|
37
|
+
// The paths given, or, with none, everything under here: bun test loads no other.
|
|
38
|
+
for (const place of tests.paths.length > 0 ? tests.paths : [cwd]) {
|
|
39
|
+
const isDir = statSync(resolve(cwd, place), { throwIfNoEntry: false })?.isDirectory();
|
|
40
|
+
const dir = resolve(cwd, isDir ? place : dirname(place));
|
|
41
|
+
// In a workspace whose packages each install agentswf, the tests' files name the engine.
|
|
42
|
+
checkProjectEngine(dir, { files: isDir ? testFiles(dir) : [resolve(cwd, place)] });
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
io.stderr(`awf: ${messageOf(error)}`);
|
|
46
|
+
return 2;
|
|
35
47
|
}
|
|
36
48
|
return runWorkflowTests(tests, cwd, {
|
|
37
49
|
...(io.signal ? { signal: io.signal } : {}),
|
|
@@ -43,13 +55,12 @@ export async function testCommand(
|
|
|
43
55
|
* Only these flags pass through, so the published command is awf's, not the whole of `bun test`'s,
|
|
44
56
|
* and a later runner can take its place. A path must exist; Bun would take it as a filter.
|
|
45
57
|
*/
|
|
46
|
-
|
|
58
|
+
function parseTestCommand(argv: readonly string[], cwd: string): TestCommand {
|
|
47
59
|
const paths: string[] = [];
|
|
48
60
|
const bunArgs: string[] = [];
|
|
49
61
|
for (let index = 0; index < argv.length; index += 1) {
|
|
50
62
|
const option = argv[index]!;
|
|
51
63
|
const value = argv[index + 1];
|
|
52
|
-
if (option === "-h" || option === "--help") return "help";
|
|
53
64
|
if (option === "--watch") {
|
|
54
65
|
bunArgs.push("--watch");
|
|
55
66
|
} else if (option === "-t") {
|
|
@@ -58,7 +69,7 @@ export function parseTestCommand(argv: readonly string[], cwd: string): TestComm
|
|
|
58
69
|
index += 1;
|
|
59
70
|
} else if (option === "--timeout") {
|
|
60
71
|
if (value === undefined) throw new Error("--timeout needs a duration such as 10s");
|
|
61
|
-
bunArgs.push("--timeout", String(parseDuration(value)));
|
|
72
|
+
bunArgs.push("--timeout", String(parseDuration(value, "--timeout")));
|
|
62
73
|
index += 1;
|
|
63
74
|
} else if (option.startsWith("-")) {
|
|
64
75
|
throw new Error(`unknown option: ${option}`);
|
|
@@ -75,12 +86,12 @@ export function parseTestCommand(argv: readonly string[], cwd: string): TestComm
|
|
|
75
86
|
* Runs `bun test` in `cwd` with the author surface preloaded, and resolves to its exit code. When
|
|
76
87
|
* `signal` aborts, as on Ctrl-C to awf alone, the tests are stopped with the same signal.
|
|
77
88
|
*/
|
|
78
|
-
|
|
89
|
+
async function runWorkflowTests(
|
|
79
90
|
command: TestCommand,
|
|
80
91
|
cwd: string,
|
|
81
92
|
options: {
|
|
82
93
|
signal?: AbortSignal;
|
|
83
|
-
output?:
|
|
94
|
+
output?: Out;
|
|
84
95
|
} = {},
|
|
85
96
|
): Promise<number> {
|
|
86
97
|
const { signal, output } = options;
|
|
@@ -131,3 +142,14 @@ export async function runWorkflowTests(
|
|
|
131
142
|
process.off("exit", orphaned);
|
|
132
143
|
}
|
|
133
144
|
}
|
|
145
|
+
|
|
146
|
+
/** A test file `bun test` would find under a folder, outside `node_modules`, for each folder. */
|
|
147
|
+
function testFiles(dir: string): string[] {
|
|
148
|
+
const byFolder = new Map<string, string>();
|
|
149
|
+
for (const path of new Bun.Glob("**/*.{test,spec}.{ts,tsx,js,jsx,mts,mjs}").scanSync({
|
|
150
|
+
cwd: dir,
|
|
151
|
+
}))
|
|
152
|
+
if (!path.split("/").includes("node_modules") && !byFolder.has(dirname(path)))
|
|
153
|
+
byFolder.set(dirname(path), join(dir, path));
|
|
154
|
+
return [...byFolder.values()];
|
|
155
|
+
}
|