@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/wake.ts
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { closeSync, mkdirSync, openSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
MAILBOX_RECORD_VERSION,
|
|
5
|
-
type MailboxRecord,
|
|
6
|
-
type WakeDid,
|
|
7
|
-
type WakeWhy,
|
|
8
|
-
} from "@agentswf/contract/records";
|
|
4
|
+
import type { WakeDid, WakeWhy } from "@agentswf/contract/records";
|
|
9
5
|
import { WIRE_VERSION } from "@agentswf/contract/wire";
|
|
10
6
|
import { coordinatorSocketPath, liveCoordinatorSocket } from "./coordinator-socket";
|
|
11
7
|
import { messageOf } from "./errors";
|
|
12
8
|
import { exists } from "./files";
|
|
13
9
|
import { coordinatorDir } from "./machine";
|
|
14
|
-
import {
|
|
10
|
+
import { liveHolder, runOpenFile } from "./run-lock";
|
|
11
|
+
import { wakeCause } from "./run-policy/waits";
|
|
15
12
|
import { statusOf, stuckOf } from "./run-state";
|
|
16
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
claimedOf,
|
|
15
|
+
generateId,
|
|
16
|
+
launchLog,
|
|
17
|
+
type Run,
|
|
18
|
+
readAttemptFiles,
|
|
19
|
+
readAttempts,
|
|
20
|
+
recordFailedLaunch,
|
|
21
|
+
sameProcess,
|
|
22
|
+
} from "./runs";
|
|
17
23
|
import { exchange } from "./socket-exchange";
|
|
24
|
+
import { readWaits } from "./waits";
|
|
18
25
|
|
|
19
26
|
/**
|
|
20
27
|
* What `wake` did to a run: why it did nothing when it did nothing, and what was due when it
|
|
@@ -29,9 +36,10 @@ export type Woken = {
|
|
|
29
36
|
|
|
30
37
|
/**
|
|
31
38
|
* Starts or continues a run's attempt, returning once that attempt has claimed its number, so
|
|
32
|
-
* its pid is recorded while the run's lock is held.
|
|
39
|
+
* its pid is recorded while the run's lock is held. `launch` names it: its output is
|
|
40
|
+
* `launches/{launch}.log`, and the attempt it starts records it.
|
|
33
41
|
*/
|
|
34
|
-
export type Launch = (run: Run, file: string) => Promise<void>;
|
|
42
|
+
export type Launch = (run: Run, file: string, launch: string) => Promise<void>;
|
|
35
43
|
|
|
36
44
|
export type WakeContext = {
|
|
37
45
|
home: string;
|
|
@@ -39,7 +47,7 @@ export type WakeContext = {
|
|
|
39
47
|
/** The file a sender named; absent, the run's last attempt's. */
|
|
40
48
|
file?: string;
|
|
41
49
|
launch?: Launch;
|
|
42
|
-
/** Asks a live run's wait to read its
|
|
50
|
+
/** Asks a live run's wait to read its events again; false when it couldn't be reached. */
|
|
43
51
|
nudge?: (run: Run) => Promise<boolean>;
|
|
44
52
|
now?: () => number;
|
|
45
53
|
/** False for a duplicate: a live run has the event already, and a nudge would tell it nothing. */
|
|
@@ -51,38 +59,49 @@ export type WakeContext = {
|
|
|
51
59
|
* continue it, or nothing. The caller holds the run's lock. A limit on live runs goes here.
|
|
52
60
|
*/
|
|
53
61
|
export async function wake(run: Run, context: WakeContext): Promise<Woken> {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
return read;
|
|
59
|
-
};
|
|
62
|
+
// Every attempt file, for `stuck` and the next number; what to do is decided from those that ran.
|
|
63
|
+
const files = await readAttemptFiles(run);
|
|
64
|
+
const claimed = claimedOf(files);
|
|
65
|
+
const now = () => new Date(context.now?.() ?? Date.now());
|
|
60
66
|
const launch = async (
|
|
61
67
|
did: "started" | "continued",
|
|
62
68
|
cause: NonNullable<Woken["cause"]>,
|
|
63
69
|
): Promise<Woken> => {
|
|
64
|
-
|
|
70
|
+
// Held by `awf open --no-fork`: nothing starts; the event waits in `in.jsonl` for its exit.
|
|
71
|
+
if (await liveHolder(runOpenFile(run.root, run.record.workflow, run.record.id)))
|
|
72
|
+
return { did: "none", why: "opened" };
|
|
73
|
+
const file = context.file ?? claimed.at(-1)?.file ?? run.record.file;
|
|
74
|
+
const started = now();
|
|
75
|
+
const id = generateId(started, randomBytes(3).toString("hex"));
|
|
65
76
|
try {
|
|
66
77
|
if (file === undefined) throw new Error(`${run.record.id} has no file to run`);
|
|
67
|
-
await (context.launch ?? detachedLaunch(context))(run, file);
|
|
78
|
+
await (context.launch ?? detachedLaunch(context))(run, file, id);
|
|
68
79
|
return { did, cause };
|
|
69
80
|
} catch (error) {
|
|
70
|
-
// The event is in
|
|
81
|
+
// The event is in `in.jsonl`; the next wake tries again, until Q13's limit.
|
|
71
82
|
const reason = messageOf(error);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
const recorded = await recordFailedLaunch(
|
|
84
|
+
run,
|
|
85
|
+
{
|
|
86
|
+
attempt: (files.at(-1)?.attempt ?? 0) + 1,
|
|
87
|
+
launch: id,
|
|
88
|
+
file: file ?? "",
|
|
89
|
+
started: started.toISOString(),
|
|
90
|
+
reason,
|
|
91
|
+
},
|
|
92
|
+
now(),
|
|
93
|
+
).catch(() => true);
|
|
94
|
+
// Its number is taken by an attempt that claimed it after all: the launch started it.
|
|
95
|
+
if (!recorded) return { did, cause };
|
|
79
96
|
return { did: "none", why: "launch-failed", error: reason };
|
|
80
97
|
}
|
|
81
98
|
};
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
99
|
+
// A launch that never claimed its number counts towards `stuck` only: the run is woken as it was
|
|
100
|
+
// before it.
|
|
101
|
+
const stuck = () => stuckOf(files);
|
|
102
|
+
if (claimed.length === 0)
|
|
103
|
+
return stuck() ? { did: "none", why: "stuck" } : launch("started", "event");
|
|
104
|
+
const { state } = statusOf(run, files);
|
|
86
105
|
switch (state) {
|
|
87
106
|
case "running":
|
|
88
107
|
if (context.nudgeLive === false) return { did: "none" };
|
|
@@ -90,18 +109,17 @@ export async function wake(run: Run, context: WakeContext): Promise<Woken> {
|
|
|
90
109
|
? { did: "nudged" }
|
|
91
110
|
: { did: "none", why: "nudge-failed" };
|
|
92
111
|
case "parked": {
|
|
93
|
-
if (
|
|
112
|
+
if (stuck()) return { did: "none", why: "stuck" };
|
|
94
113
|
// Only for what the wait it parked at would take: an event it names, or its timer due.
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return launch("continued", readyFor(state, takes).length > 0 ? "event" : "timer");
|
|
114
|
+
const at = now().getTime();
|
|
115
|
+
const state = await readWaits(run.dir, at);
|
|
116
|
+
const cause = wakeCause(state, state.lastWait?.takes);
|
|
117
|
+
return cause ? launch("continued", cause) : { did: "none", why: "nothing-due" };
|
|
100
118
|
}
|
|
101
119
|
case "interrupted":
|
|
102
120
|
case "failed":
|
|
103
121
|
case "timed-out":
|
|
104
|
-
return
|
|
122
|
+
return stuck()
|
|
105
123
|
? { did: "none", why: "stuck" }
|
|
106
124
|
: launch("continued", state === "interrupted" ? "crashed" : "failed");
|
|
107
125
|
case "stopped":
|
|
@@ -184,16 +202,17 @@ const CLAIM_MILLISECONDS = 60_000;
|
|
|
184
202
|
|
|
185
203
|
/**
|
|
186
204
|
* Runs `awf run {file} --id {id} --continue` in a session of its own (Q4), so it outlives the sender's
|
|
187
|
-
* terminal, in the run's project with its recorded `PATH`, its output in
|
|
188
|
-
* Bun reads the project's `.env` there, as at every
|
|
205
|
+
* terminal, in the run's project with its recorded `PATH`, its output in `launches/{launch}.log`,
|
|
206
|
+
* and `AWF_LAUNCH` naming it to the attempt. Bun reads the project's `.env` there, as at every
|
|
207
|
+
* start, so a rotated token reaches it.
|
|
189
208
|
*/
|
|
190
209
|
function detachedLaunch(context: WakeContext): Launch {
|
|
191
|
-
return async (run, file) => {
|
|
210
|
+
return async (run, file, launch) => {
|
|
192
211
|
const before = (await readAttempts(run)).length;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const log = openSync(
|
|
196
|
-
const env = attemptEnvironment(context.environment, run);
|
|
212
|
+
const path = launchLog(run.dir, launch);
|
|
213
|
+
mkdirSync(join(run.dir, "launches"), { recursive: true, mode: 0o700 });
|
|
214
|
+
const log = openSync(path, "a", 0o600);
|
|
215
|
+
const env = { ...attemptEnvironment(context.environment, run), AWF_LAUNCH: launch };
|
|
197
216
|
let child: ReturnType<typeof Bun.spawn>;
|
|
198
217
|
try {
|
|
199
218
|
child = Bun.spawn({
|
|
@@ -225,6 +244,8 @@ function detachedLaunch(context: WakeContext): Launch {
|
|
|
225
244
|
// on that.
|
|
226
245
|
const previous = claimed.at(-2);
|
|
227
246
|
if (previous && sameProcess(previous.pid, previous.processStart)) return;
|
|
247
|
+
// A plain run opens its socket only when a participant connects: no sender nudges it.
|
|
248
|
+
if (!run.record.events) return;
|
|
228
249
|
// Best effort: an event sent next nudges the run rather than waiting for its poll.
|
|
229
250
|
const socket = coordinatorSocketPath(coordinatorDir(context.home, env), run.dir);
|
|
230
251
|
const opened = Date.now() + SOCKET_MILLISECONDS;
|
|
@@ -238,8 +259,7 @@ function detachedLaunch(context: WakeContext): Launch {
|
|
|
238
259
|
return;
|
|
239
260
|
}
|
|
240
261
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
241
|
-
const tail = await Bun.file(
|
|
242
|
-
.slice(from)
|
|
262
|
+
const tail = await Bun.file(path)
|
|
243
263
|
.text()
|
|
244
264
|
.then((text) => text.trim().split("\n").slice(-3).join(" / "))
|
|
245
265
|
.catch((error: unknown) => messageOf(error));
|
|
@@ -247,13 +267,10 @@ function detachedLaunch(context: WakeContext): Launch {
|
|
|
247
267
|
}
|
|
248
268
|
if (Date.now() > giveUp) {
|
|
249
269
|
// One that claimed meanwhile goes on. Left running, one that did not could still claim the
|
|
250
|
-
// run later
|
|
270
|
+
// run later, past the failed attempt its launcher writes: it is stopped, and waited for.
|
|
271
|
+
if ((await readAttempts(run)).length > before) continue;
|
|
272
|
+
await stopGroup(child);
|
|
251
273
|
if ((await readAttempts(run)).length > before) continue;
|
|
252
|
-
try {
|
|
253
|
-
process.kill(-child.pid, "SIGTERM");
|
|
254
|
-
} catch {
|
|
255
|
-
// It ended meanwhile.
|
|
256
|
-
}
|
|
257
274
|
throw new Error(
|
|
258
275
|
`the attempt of ${run.record.id} did not start within ${CLAIM_MILLISECONDS / 1000} s`,
|
|
259
276
|
);
|
|
@@ -262,3 +279,16 @@ function detachedLaunch(context: WakeContext): Launch {
|
|
|
262
279
|
}
|
|
263
280
|
};
|
|
264
281
|
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Kills a launched child's process group outright, and waits for it: a `SIGTERM` would be a cancel
|
|
285
|
+
* to `awf run`, which ends an event run for good.
|
|
286
|
+
*/
|
|
287
|
+
async function stopGroup(child: ReturnType<typeof Bun.spawn>): Promise<void> {
|
|
288
|
+
try {
|
|
289
|
+
process.kill(-child.pid, "SIGKILL");
|
|
290
|
+
} catch {
|
|
291
|
+
// It ended meanwhile.
|
|
292
|
+
}
|
|
293
|
+
await child.exited;
|
|
294
|
+
}
|
package/src/workflow-loader.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, extname, resolve } from "node:path";
|
|
3
4
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import * as workflowSurface from "@agentswf/contract/
|
|
5
|
+
import * as workflowSurface from "@agentswf/contract/author";
|
|
5
6
|
import {
|
|
6
7
|
type EventSchemas,
|
|
7
8
|
eventWorkflowProblem,
|
|
@@ -13,7 +14,11 @@ import {
|
|
|
13
14
|
import { plugin } from "bun";
|
|
14
15
|
import * as typebox from "typebox";
|
|
15
16
|
import * as typeboxValue from "typebox/value";
|
|
17
|
+
import * as typeboxSurface from "./author-typebox";
|
|
18
|
+
import { messageOf } from "./errors";
|
|
16
19
|
import { realPath } from "./files";
|
|
20
|
+
import { checkProjectEngine, EngineRefused } from "./project-engine";
|
|
21
|
+
import { projectOf } from "./projects";
|
|
17
22
|
import { idProblem } from "./runs";
|
|
18
23
|
|
|
19
24
|
/**
|
|
@@ -23,12 +28,18 @@ import { idProblem } from "./runs";
|
|
|
23
28
|
*/
|
|
24
29
|
const AUTHOR_SURFACE: Record<string, Record<string, unknown>> = {
|
|
25
30
|
"agentswf/workflow": workflowSurface,
|
|
31
|
+
"agentswf/typebox": typeboxSurface,
|
|
26
32
|
typebox,
|
|
27
33
|
"typebox/value": typeboxValue,
|
|
28
34
|
};
|
|
29
35
|
|
|
30
36
|
let surfaceServed = false;
|
|
31
37
|
|
|
38
|
+
/** Whether this process serves the author surface: a program's client never does. */
|
|
39
|
+
export function authorSurfaceServed(): boolean {
|
|
40
|
+
return surfaceServed;
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
/** Serves the author surface to whatever this process imports next. */
|
|
33
44
|
export function serveAuthorSurface(): void {
|
|
34
45
|
if (surfaceServed) return;
|
|
@@ -51,7 +62,33 @@ export type EventWorkflow = Workflow<JsonValue, JsonValue, EventSchemas>;
|
|
|
51
62
|
|
|
52
63
|
export type LoadedWorkflow = { file: string; workflow: AnyWorkflow };
|
|
53
64
|
|
|
54
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Imports the workflow `file` names, once its project is known to run on this engine (ADR 0015),
|
|
67
|
+
* and checks what `tsc` would have checked of it.
|
|
68
|
+
*/
|
|
69
|
+
export async function loadWorkflowFile(
|
|
70
|
+
file: string,
|
|
71
|
+
cwd: string,
|
|
72
|
+
home: string = homedir(),
|
|
73
|
+
): Promise<LoadedWorkflow> {
|
|
74
|
+
try {
|
|
75
|
+
return await load(file, cwd, home);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof EngineRefused || error instanceof WorkflowFileError) throw error;
|
|
78
|
+
throw new WorkflowFileError(messageOf(error), { cause: error });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A workflow file that isn't there, doesn't load, or isn't the workflow it should be. */
|
|
83
|
+
export class WorkflowFileError extends Error {
|
|
84
|
+
readonly code = "file";
|
|
85
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
86
|
+
super(message, options);
|
|
87
|
+
this.name = "WorkflowFileError";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function load(file: string, cwd: string, home: string): Promise<LoadedWorkflow> {
|
|
55
92
|
if (file.includes("://") || file.startsWith("file:")) {
|
|
56
93
|
throw new Error("workflow must be an explicit local file path");
|
|
57
94
|
}
|
|
@@ -66,6 +103,9 @@ export async function loadWorkflowFile(file: string, cwd: string): Promise<Loade
|
|
|
66
103
|
throw new Error(`workflow file not found: ${absolute}`);
|
|
67
104
|
}
|
|
68
105
|
if (!info.isFile()) throw new Error(`workflow path is not a file: ${absolute}`);
|
|
106
|
+
// Before its import: another engine's workflow would get this one's author surface.
|
|
107
|
+
const project = await projectOf(absolute, home).catch(() => dirname(absolute));
|
|
108
|
+
checkProjectEngine(project, { file: absolute });
|
|
69
109
|
|
|
70
110
|
serveAuthorSurface();
|
|
71
111
|
const namespace: unknown = await import(pathToFileURL(absolute).href);
|
|
@@ -81,11 +121,21 @@ export async function loadWorkflowFile(file: string, cwd: string): Promise<Loade
|
|
|
81
121
|
`meta.version: ${JSON.stringify(version)} is not a semver version such as 1.2.0`,
|
|
82
122
|
);
|
|
83
123
|
}
|
|
124
|
+
if (given.events === undefined && given.file !== undefined) {
|
|
125
|
+
// A plain workflow's `file` says where a program finds it and its runs: it must be this one.
|
|
126
|
+
if (typeof given.file !== "string" || realPath(given.file) !== realPath(absolute))
|
|
127
|
+
throw new Error(
|
|
128
|
+
`file is ${JSON.stringify(given.file)}; a workflow gives file: import.meta.path, ${absolute}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
84
131
|
if (given.events !== undefined) {
|
|
85
132
|
// As `tsc` would have checked them, for a file it never saw.
|
|
86
133
|
const eventProblem = eventWorkflowProblem(given);
|
|
87
134
|
if (eventProblem) throw new Error(eventProblem);
|
|
88
|
-
if (
|
|
135
|
+
if (given.file === undefined)
|
|
136
|
+
// Found by the path it was loaded from, which is all a sender with that path needs.
|
|
137
|
+
return { file: absolute, workflow: { ...given, file: absolute } as AnyWorkflow };
|
|
138
|
+
if (realPath(given.file) !== realPath(absolute))
|
|
89
139
|
throw new Error(
|
|
90
140
|
`file is ${JSON.stringify(given.file)}; a workflow with events gives file: import.meta.path, ${absolute}`,
|
|
91
141
|
);
|