@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
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
|
|
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,29 +85,27 @@ export class StageLedger {
|
|
|
86
85
|
*/
|
|
87
86
|
readonly #looked = new Set<string>();
|
|
88
87
|
#needed: WorkflowStopped | undefined;
|
|
89
|
-
/**
|
|
90
|
-
readonly #
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
#
|
|
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;
|
|
96
95
|
|
|
97
96
|
constructor(
|
|
98
97
|
private readonly options: {
|
|
99
|
-
|
|
100
|
-
|
|
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>;
|
|
101
104
|
workflowVersion?: string;
|
|
102
|
-
/** The attempt's turns as they settled, which name the stage each ran in. */
|
|
103
|
-
turns: () => readonly TurnRecord[];
|
|
104
|
-
/** The run's stage records as the attempt began: what a continue may reuse. */
|
|
105
|
-
records?: ReadonlyMap<string, StageRecord>;
|
|
106
105
|
/** `--from-stage`: the stage the attempt starts at. */
|
|
107
106
|
fromStage?: string;
|
|
108
|
-
/** `--values`: the values of stages before `fromStage` that have no
|
|
107
|
+
/** `--values`: the values of stages before `fromStage` that have no line to reuse. */
|
|
109
108
|
values?: ReadonlyMap<string, JsonValue>;
|
|
110
|
-
/** A run of a workflow with `events`: the last `--from-stage` an attempt redid. */
|
|
111
|
-
redo?: StageRedo;
|
|
112
109
|
/**
|
|
113
110
|
* A run of a workflow with `events`: a stage runs once ever, reused wherever it is entered,
|
|
114
111
|
* as `planStageByContent` says, and no `values`. Its start point is `fromStage` alone.
|
|
@@ -119,8 +116,11 @@ export class StageLedger {
|
|
|
119
116
|
now?: () => Date;
|
|
120
117
|
},
|
|
121
118
|
) {
|
|
122
|
-
|
|
123
|
-
this.#
|
|
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);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/** The stage open now, if one is. */
|
|
@@ -134,13 +134,12 @@ export class StageLedger {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
|
-
* The run's
|
|
138
|
-
* 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.
|
|
139
140
|
*/
|
|
140
|
-
get recorded():
|
|
141
|
-
return
|
|
142
|
-
a.started.localeCompare(b.started),
|
|
143
|
-
);
|
|
141
|
+
get recorded(): StageLine[] {
|
|
142
|
+
return this.#recorded;
|
|
144
143
|
}
|
|
145
144
|
|
|
146
145
|
/** The stop that ended the attempt, even if the workflow caught it. */
|
|
@@ -150,7 +149,7 @@ export class StageLedger {
|
|
|
150
149
|
|
|
151
150
|
/**
|
|
152
151
|
* Each stage entered that has ended, in order: reused, run, or stopped by the plan as it was
|
|
153
|
-
* entered, which ran nothing and
|
|
152
|
+
* entered, which ran nothing and wrote no line.
|
|
154
153
|
*/
|
|
155
154
|
get summaries(): StageSummary[] {
|
|
156
155
|
return [...this.#stages.values()].flatMap(
|
|
@@ -169,11 +168,11 @@ export class StageLedger {
|
|
|
169
168
|
// By content, an earlier event's stage may never be entered again: none is upcoming.
|
|
170
169
|
const upcoming = this.options.byContent
|
|
171
170
|
? []
|
|
172
|
-
: this.
|
|
173
|
-
const recorded = (stage: string) => this.options.records?.get(stage)?.ended;
|
|
171
|
+
: this.#upcoming.filter((stage) => !this.#stages.has(stage));
|
|
174
172
|
return {
|
|
175
173
|
stages: [...this.#stages.values()].map((entered) => {
|
|
176
|
-
const ended =
|
|
174
|
+
const ended =
|
|
175
|
+
entered.source === "reused" ? this.#reusedLines.get(entered.stage)?.at : undefined;
|
|
177
176
|
const stage = withoutValue(entered);
|
|
178
177
|
return ended === undefined ? stage : { ...stage, recordedAt: Date.parse(ended) };
|
|
179
178
|
}),
|
|
@@ -208,10 +207,9 @@ export class StageLedger {
|
|
|
208
207
|
|
|
209
208
|
/**
|
|
210
209
|
* Enters `name`: reused or provided when the plan says so, or opened to run. A value provided is
|
|
211
|
-
*
|
|
212
|
-
* one open, or a
|
|
213
|
-
* that fails the attempt.
|
|
214
|
-
* `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.
|
|
215
213
|
*/
|
|
216
214
|
async enter(name: string, entry: StageEntry): Promise<EnteredStage> {
|
|
217
215
|
// Looking on, a stop caught is the one for the values missing, which ends the attempt anyway.
|
|
@@ -220,9 +218,13 @@ export class StageLedger {
|
|
|
220
218
|
this.#check(name);
|
|
221
219
|
const at = this.#now().getTime();
|
|
222
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);
|
|
223
223
|
const decision = planStage(
|
|
224
224
|
{
|
|
225
|
-
|
|
225
|
+
...(line === undefined ? {} : { line }),
|
|
226
|
+
...(lineVersion === undefined ? {} : { lineVersion }),
|
|
227
|
+
recorded: this.#recorded.map((each) => each.name),
|
|
226
228
|
...(this.options.fromStage === undefined ? {} : { fromStage: this.options.fromStage }),
|
|
227
229
|
...(this.options.values === undefined ? {} : { values: this.options.values }),
|
|
228
230
|
...(this.options.workflowVersion === undefined
|
|
@@ -261,8 +263,8 @@ export class StageLedger {
|
|
|
261
263
|
}
|
|
262
264
|
if (decision.kind === "reuse" || decision.kind === "provide") {
|
|
263
265
|
const held = this.#hold(name, decision.value);
|
|
264
|
-
let
|
|
265
|
-
if (decision.kind === "reuse")
|
|
266
|
+
let line: StageLine;
|
|
267
|
+
if (decision.kind === "reuse") line = decision.line;
|
|
266
268
|
else {
|
|
267
269
|
const written = this.#provide(name, decision.value, entry, at);
|
|
268
270
|
// Its failure is the stage's, thrown below.
|
|
@@ -273,13 +275,14 @@ export class StageLedger {
|
|
|
273
275
|
),
|
|
274
276
|
);
|
|
275
277
|
try {
|
|
276
|
-
|
|
278
|
+
line = await written;
|
|
277
279
|
} catch (error) {
|
|
278
280
|
held.release();
|
|
279
281
|
throw error;
|
|
280
282
|
}
|
|
281
283
|
}
|
|
282
284
|
this.#reused.add(name);
|
|
285
|
+
if (decision.kind === "reuse") this.#reusedLines.set(name, line);
|
|
283
286
|
this.options.event?.(
|
|
284
287
|
{
|
|
285
288
|
kind: "stage-ended",
|
|
@@ -289,7 +292,7 @@ export class StageLedger {
|
|
|
289
292
|
},
|
|
290
293
|
at,
|
|
291
294
|
);
|
|
292
|
-
const { attempt, summary, provided } =
|
|
295
|
+
const { attempt, summary, provided } = line;
|
|
293
296
|
this.#stages.set(name, {
|
|
294
297
|
stage: name,
|
|
295
298
|
source: "reused",
|
|
@@ -303,41 +306,21 @@ export class StageLedger {
|
|
|
303
306
|
});
|
|
304
307
|
return held;
|
|
305
308
|
}
|
|
306
|
-
// Open while the records it outdates are moved, so no other stage enters beside it.
|
|
307
|
-
const stage = this.#opened(name, at);
|
|
308
309
|
if (decision.start) {
|
|
309
|
-
try {
|
|
310
|
-
await replaceStale(this.options.runDir, name, this.#reused);
|
|
311
|
-
} catch (error) {
|
|
312
|
-
// Not entered after all: nothing ran, so nothing is recorded, and no stage is left open.
|
|
313
|
-
this.#stages.delete(name);
|
|
314
|
-
this.#open = undefined;
|
|
315
|
-
throw error;
|
|
316
|
-
}
|
|
317
310
|
this.#started = true;
|
|
311
|
+
this.#reuse.redoFrom(name);
|
|
318
312
|
}
|
|
319
|
-
return { kind: "run", stage };
|
|
313
|
+
return { kind: "run", stage: this.#opened(name, at, decision.start) };
|
|
320
314
|
}
|
|
321
315
|
|
|
322
316
|
/**
|
|
323
|
-
* Enters a stage by content: reused from its succeeded
|
|
324
|
-
*
|
|
325
|
-
*
|
|
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`.
|
|
326
320
|
*/
|
|
327
321
|
async #enterByContent(name: string, entry: StageEntry, at: number): Promise<EnteredStage> {
|
|
328
322
|
if (!this.#started && name === this.options.fromStage) return this.#startByContent(name, at);
|
|
329
|
-
const
|
|
330
|
-
const redo = this.#redo;
|
|
331
|
-
// Entered already in this attempt, it has its value here: one stage, one value.
|
|
332
|
-
const stale =
|
|
333
|
-
redo !== undefined &&
|
|
334
|
-
this.#afterRedo &&
|
|
335
|
-
record !== undefined &&
|
|
336
|
-
record.attempt < redo.attempt &&
|
|
337
|
-
!this.#stages.has(name);
|
|
338
|
-
if (name === redo?.stage) this.#afterRedo = true;
|
|
339
|
-
if (stale) return { kind: "run", stage: this.#opened(name, at) };
|
|
340
|
-
const decision = planStageByContent(record, entry.result);
|
|
323
|
+
const decision = planStageByContent(this.#reuse.enter(name), entry.result);
|
|
341
324
|
if (decision.kind === "stop") {
|
|
342
325
|
this.#stopped = new WorkflowStopped(decision.reason, name, true);
|
|
343
326
|
this.#stoppedAt(name, at);
|
|
@@ -348,7 +331,8 @@ export class StageLedger {
|
|
|
348
331
|
this.#reused.add(name);
|
|
349
332
|
// Entered again in the attempt that ran or reused it: nothing more to show or log.
|
|
350
333
|
if (this.#stages.get(name)?.outcome === "succeeded") return held;
|
|
351
|
-
|
|
334
|
+
this.#reusedLines.set(name, decision.line);
|
|
335
|
+
const { attempt, summary } = decision.line;
|
|
352
336
|
this.options.event?.(
|
|
353
337
|
{ kind: "stage-ended", stage: name, outcome: "succeeded", source: "reused" },
|
|
354
338
|
at,
|
|
@@ -369,17 +353,16 @@ export class StageLedger {
|
|
|
369
353
|
}
|
|
370
354
|
|
|
371
355
|
/**
|
|
372
|
-
* `--from-stage` reached by content: it runs, and this attempt is the redo
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
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.
|
|
376
360
|
*/
|
|
377
361
|
#startByContent(name: string, at: number): EnteredStage {
|
|
378
|
-
|
|
362
|
+
this.#reuse.enter(name);
|
|
363
|
+
this.#reuse.redoFrom(name);
|
|
379
364
|
this.#started = true;
|
|
380
|
-
|
|
381
|
-
this.#afterRedo = true;
|
|
382
|
-
return { kind: "run", stage };
|
|
365
|
+
return { kind: "run", stage: this.#opened(name, at, true) };
|
|
383
366
|
}
|
|
384
367
|
|
|
385
368
|
/** A stage the plan stopped as it was entered: shown stopped, though it ran nothing. */
|
|
@@ -389,7 +372,7 @@ export class StageLedger {
|
|
|
389
372
|
stage: name,
|
|
390
373
|
source: "ran",
|
|
391
374
|
outcome: "stopped",
|
|
392
|
-
attempt: this
|
|
375
|
+
attempt: this.#attempt,
|
|
393
376
|
startedAt: at,
|
|
394
377
|
endedAt: at,
|
|
395
378
|
});
|
|
@@ -457,34 +440,29 @@ export class StageLedger {
|
|
|
457
440
|
return (interrupted ? undefined : this.#stopForNeeded()) ?? failure;
|
|
458
441
|
}
|
|
459
442
|
|
|
460
|
-
/**
|
|
443
|
+
/** Writes a value given for a stage, or none for one that returns nothing: no turn ran it. */
|
|
461
444
|
async #provide(
|
|
462
445
|
name: string,
|
|
463
446
|
value: JsonValue | undefined,
|
|
464
447
|
entry: StageEntry,
|
|
465
448
|
at: number,
|
|
466
|
-
): Promise<
|
|
449
|
+
): Promise<StageLine> {
|
|
467
450
|
const time = new Date(at).toISOString();
|
|
468
451
|
const summary = value === undefined ? undefined : entry.summary(value);
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
452
|
+
const line: StageLine = {
|
|
453
|
+
kind: "stage",
|
|
454
|
+
attempt: this.#attempt,
|
|
455
|
+
at: time,
|
|
456
|
+
name,
|
|
473
457
|
outcome: "succeeded",
|
|
474
458
|
started: time,
|
|
475
|
-
ended: time,
|
|
476
|
-
...(this.options.workflowVersion === undefined
|
|
477
|
-
? {}
|
|
478
|
-
: { workflowVersion: this.options.workflowVersion }),
|
|
479
|
-
sessions: [],
|
|
480
459
|
...(summary === undefined ? {} : { summary }),
|
|
481
460
|
...(value === undefined ? {} : { value }),
|
|
482
461
|
provided: true,
|
|
483
462
|
};
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
return record;
|
|
463
|
+
await this.options.lines.append(line);
|
|
464
|
+
this.#reuse.wrote(line);
|
|
465
|
+
return line;
|
|
488
466
|
}
|
|
489
467
|
|
|
490
468
|
#check(name: string): void {
|
|
@@ -503,61 +481,55 @@ export class StageLedger {
|
|
|
503
481
|
}
|
|
504
482
|
}
|
|
505
483
|
|
|
506
|
-
|
|
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 {
|
|
507
490
|
const entered: StageProgress & { value?: JsonValue } = {
|
|
508
491
|
stage: name,
|
|
509
492
|
source: "ran",
|
|
510
|
-
attempt: this
|
|
493
|
+
attempt: this.#attempt,
|
|
511
494
|
startedAt,
|
|
512
495
|
};
|
|
513
496
|
this.#stages.set(name, entered);
|
|
514
497
|
this.options.event?.({ kind: "stage-started", stage: name }, startedAt);
|
|
515
498
|
let ended: Promise<void> | undefined;
|
|
516
|
-
// A
|
|
517
|
-
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">) => {
|
|
518
501
|
if (ended) return ended;
|
|
519
502
|
if (this.#open === stage) this.#open = undefined;
|
|
520
503
|
const at = this.#now();
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
504
|
+
const line: StageLine = {
|
|
505
|
+
kind: "stage",
|
|
506
|
+
attempt: this.#attempt,
|
|
507
|
+
at: at.toISOString(),
|
|
508
|
+
name,
|
|
525
509
|
outcome: fields.outcome,
|
|
526
510
|
...(fields.reason === undefined ? {} : { reason: fields.reason }),
|
|
527
511
|
started: new Date(startedAt).toISOString(),
|
|
528
|
-
ended: at.toISOString(),
|
|
529
|
-
...(this.options.workflowVersion === undefined
|
|
530
|
-
? {}
|
|
531
|
-
: { workflowVersion: this.options.workflowVersion }),
|
|
532
|
-
sessions: sessionsOf(this.options.turns(), name),
|
|
533
512
|
...(fields.summary === undefined ? {} : { summary: fields.summary }),
|
|
534
513
|
...(fields.value === undefined ? {} : { value: fields.value }),
|
|
514
|
+
...(redo ? { redo: true as const } : {}),
|
|
535
515
|
};
|
|
536
516
|
Object.assign(entered, {
|
|
537
|
-
outcome:
|
|
517
|
+
outcome: line.outcome,
|
|
538
518
|
endedAt: at.getTime(),
|
|
539
|
-
...(
|
|
540
|
-
...(
|
|
519
|
+
...(line.summary === undefined ? {} : { summary: line.summary }),
|
|
520
|
+
...(line.value === undefined ? {} : { value: line.value }),
|
|
541
521
|
});
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
this.#records.set(name, record);
|
|
551
|
-
},
|
|
552
|
-
(error: unknown) => {
|
|
553
|
-
entered.outcome = "failed";
|
|
554
|
-
throw error;
|
|
555
|
-
},
|
|
556
|
-
);
|
|
557
|
-
// 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.
|
|
558
530
|
const logEnd = () =>
|
|
559
531
|
this.options.event?.(
|
|
560
|
-
{ kind: "stage-ended", stage: name, outcome: entered.outcome ??
|
|
532
|
+
{ kind: "stage-ended", stage: name, outcome: entered.outcome ?? line.outcome },
|
|
561
533
|
at.getTime(),
|
|
562
534
|
);
|
|
563
535
|
ended.then(logEnd, logEnd);
|
|
@@ -580,7 +552,7 @@ export class StageLedger {
|
|
|
580
552
|
}
|
|
581
553
|
|
|
582
554
|
/**
|
|
583
|
-
* 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
|
|
584
556
|
* next attempt runs it again.
|
|
585
557
|
*/
|
|
586
558
|
abandon(name: string): void {
|
|
@@ -598,8 +570,8 @@ export class StageLedger {
|
|
|
598
570
|
}
|
|
599
571
|
|
|
600
572
|
/**
|
|
601
|
-
* Fails the stage still open, once what ran in it has settled, and waits for every
|
|
602
|
-
*
|
|
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.
|
|
603
575
|
*/
|
|
604
576
|
async close(): Promise<void> {
|
|
605
577
|
this.seal("the attempt ended");
|
|
@@ -624,17 +596,3 @@ export class StageLedger {
|
|
|
624
596
|
return this.#failures.get(first) ?? this.#closedOpen;
|
|
625
597
|
}
|
|
626
598
|
}
|
|
627
|
-
|
|
628
|
-
/** The session each of the stage's turns ran on, once per agent and session. */
|
|
629
|
-
function sessionsOf(turns: readonly TurnRecord[], stage: string): StageRecord["sessions"] {
|
|
630
|
-
const seen = new Map<string, StageRecord["sessions"][number]>();
|
|
631
|
-
for (const turn of turns) {
|
|
632
|
-
const session = turn.sessions.at(-1);
|
|
633
|
-
if (turn.stage !== stage || !session) continue;
|
|
634
|
-
const key = `${turn.agent}\0${session.harness}\0${session.id}`;
|
|
635
|
-
if (!seen.has(key)) {
|
|
636
|
-
seen.set(key, { agent: turn.agent, harness: session.harness, session: session.id });
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
return [...seen.values()];
|
|
640
|
-
}
|
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
|
-
tests = parseTestCommand(argv, cwd
|
|
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,17 +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
|
-
|
|
47
|
-
argv: readonly string[],
|
|
48
|
-
cwd: string,
|
|
49
|
-
deprecated: (line: string) => void = () => {},
|
|
50
|
-
): TestCommand | "help" {
|
|
58
|
+
function parseTestCommand(argv: readonly string[], cwd: string): TestCommand {
|
|
51
59
|
const paths: string[] = [];
|
|
52
60
|
const bunArgs: string[] = [];
|
|
53
61
|
for (let index = 0; index < argv.length; index += 1) {
|
|
54
62
|
const option = argv[index]!;
|
|
55
63
|
const value = argv[index + 1];
|
|
56
|
-
if (option === "-h" || option === "--help") return "help";
|
|
57
64
|
if (option === "--watch") {
|
|
58
65
|
bunArgs.push("--watch");
|
|
59
66
|
} else if (option === "-t") {
|
|
@@ -62,7 +69,7 @@ export function parseTestCommand(
|
|
|
62
69
|
index += 1;
|
|
63
70
|
} else if (option === "--timeout") {
|
|
64
71
|
if (value === undefined) throw new Error("--timeout needs a duration such as 10s");
|
|
65
|
-
bunArgs.push("--timeout", String(parseDuration(value, "--timeout"
|
|
72
|
+
bunArgs.push("--timeout", String(parseDuration(value, "--timeout")));
|
|
66
73
|
index += 1;
|
|
67
74
|
} else if (option.startsWith("-")) {
|
|
68
75
|
throw new Error(`unknown option: ${option}`);
|
|
@@ -79,12 +86,12 @@ export function parseTestCommand(
|
|
|
79
86
|
* Runs `bun test` in `cwd` with the author surface preloaded, and resolves to its exit code. When
|
|
80
87
|
* `signal` aborts, as on Ctrl-C to awf alone, the tests are stopped with the same signal.
|
|
81
88
|
*/
|
|
82
|
-
|
|
89
|
+
async function runWorkflowTests(
|
|
83
90
|
command: TestCommand,
|
|
84
91
|
cwd: string,
|
|
85
92
|
options: {
|
|
86
93
|
signal?: AbortSignal;
|
|
87
|
-
output?:
|
|
94
|
+
output?: Out;
|
|
88
95
|
} = {},
|
|
89
96
|
): Promise<number> {
|
|
90
97
|
const { signal, output } = options;
|
|
@@ -135,3 +142,14 @@ export async function runWorkflowTests(
|
|
|
135
142
|
process.off("exit", orphaned);
|
|
136
143
|
}
|
|
137
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
|
+
}
|