@agentswf/engine 0.1.0-beta.2 → 0.1.0-beta.4
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/package.json +5 -6
- package/src/agent-cli/cli.ts +571 -0
- package/src/agent-cli/client.ts +115 -0
- package/src/agent-launcher.ts +15 -25
- package/src/allowance-command.ts +31 -21
- package/src/attempt-ending.ts +23 -5
- package/src/attempt-output.ts +48 -18
- package/src/attempt-view.ts +25 -15
- package/src/commands.ts +176 -0
- package/src/control-plane.ts +73 -22
- package/src/coordinator-port.ts +29 -0
- package/src/coordinator-prompt.ts +237 -0
- package/src/coordinator-socket.ts +236 -0
- package/src/deadlines.ts +47 -10
- package/src/decisions/directory.ts +8 -6
- package/src/duration.ts +29 -0
- package/src/emit-command.ts +119 -0
- package/src/emit.ts +246 -0
- package/src/files.ts +11 -1
- package/src/help/allowance.txt +13 -0
- package/src/help/emit.txt +13 -0
- package/src/help/inbox.txt +12 -0
- package/src/help/init.txt +12 -0
- package/src/help/logs.txt +13 -0
- package/src/help/reply.txt +18 -0
- package/src/help/run.txt +44 -0
- package/src/help/runs.txt +11 -0
- package/src/help/test.txt +9 -0
- package/src/help/tick.txt +12 -0
- package/src/here.ts +9 -3
- package/src/hold-run-lock.fixture.ts +8 -0
- package/src/holder-command.ts +279 -0
- package/src/inbox-command.ts +715 -0
- package/src/init-command.ts +29 -11
- package/src/init-template/AGENTS.md.template +4 -4
- package/src/init-template/hello/workflow.test.ts.template +2 -2
- package/src/init-template/hello/workflow.ts.template +16 -20
- package/src/keep-alive.ts +40 -0
- package/src/logs-command.ts +106 -35
- package/src/machine.ts +12 -1
- package/src/mailbox.ts +990 -0
- package/src/message-operation.ts +26 -3
- package/src/messaging/limits.ts +1 -1
- package/src/messaging/messaging.ts +478 -85
- package/src/messaging/routes.ts +11 -4
- package/src/messaging/text.ts +25 -22
- package/src/operation-liveness.ts +55 -9
- package/src/operator-cli.ts +290 -119
- package/src/operator-runtime.ts +8 -8
- package/src/progress-view.ts +49 -7
- package/src/projects.ts +57 -0
- package/src/result-slots.ts +43 -19
- package/src/result-validation.ts +18 -27
- package/src/run-command.ts +29 -66
- package/src/run-commands.ts +13 -2
- package/src/run-lock.ts +123 -0
- package/src/run-prepare.ts +94 -22
- package/src/run-state.ts +125 -6
- package/src/run-usage.ts +2 -2
- package/src/run-view.ts +0 -9
- package/src/runs-command.ts +43 -25
- package/src/runs.ts +142 -23
- package/src/sandboxes.ts +11 -4
- package/src/simple-input.ts +268 -0
- package/src/socket-exchange.ts +26 -0
- package/src/sources.ts +70 -0
- package/src/stage-ledger.ts +89 -14
- package/src/stage-plan.ts +23 -0
- package/src/stop-crashed.ts +39 -0
- package/src/test-command.ts +34 -11
- package/src/testing.ts +72 -4
- package/src/tick-command.ts +76 -0
- package/src/tools.ts +645 -0
- package/src/wake.ts +264 -0
- package/src/workflow-loader.ts +32 -22
- package/src/workflow-runner.ts +1277 -97
- package/src/workflow-testing/coordinator.typecheck.ts +54 -0
- package/src/workflow-testing/coordinators.ts +137 -0
- package/src/workflow-testing/host.ts +67 -7
- package/src/workflow-testing/index.ts +488 -34
- package/src/workflow-testing/script.ts +74 -4
- package/src/workflow-testing/workflow-testing.typecheck.ts +19 -2
package/src/emit.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { type EmitResultJson, MAILBOX_RECORD_VERSION } from "@agentswf/contract/records";
|
|
4
|
+
import { parseJsonSchema, validate } from "@agentswf/contract/schema";
|
|
5
|
+
import {
|
|
6
|
+
AWF_ID_PREFIX,
|
|
7
|
+
eventWorkflowProblem,
|
|
8
|
+
type JsonValue,
|
|
9
|
+
nameProblem,
|
|
10
|
+
} from "@agentswf/contract/workflow";
|
|
11
|
+
import { messageOf } from "./errors";
|
|
12
|
+
import { exists } from "./files";
|
|
13
|
+
import { appendMailbox, foldMailbox, readMailbox } from "./mailbox";
|
|
14
|
+
import { projectOf, projectRunRoot, recordProject } from "./projects";
|
|
15
|
+
import { runLockFile, withRunLock } from "./run-lock";
|
|
16
|
+
import { prepareArgs } from "./run-prepare";
|
|
17
|
+
import { createRun, idProblem, openRun, type Run, readAttempts } from "./runs";
|
|
18
|
+
import { type WakeContext, type Woken, wake } from "./wake";
|
|
19
|
+
import type { EventWorkflow } from "./workflow-loader";
|
|
20
|
+
|
|
21
|
+
/** An event awf refuses to take at all: a name, shape or id no run of the target could have. */
|
|
22
|
+
export class EmitError extends Error {
|
|
23
|
+
constructor(message: string) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "EmitError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type EmitContext = Omit<WakeContext, "file"> & {
|
|
30
|
+
/** As `WorkflowEvent.from`: the sending run's ref, or `operator`. */
|
|
31
|
+
from: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A sender's way in: checks the event against its target and delivers it to the run it names. A wrong name,
|
|
36
|
+
* shape or id throws an `EmitError`; the run's state is the outcome.
|
|
37
|
+
*/
|
|
38
|
+
export async function emitEvent(
|
|
39
|
+
target: EventWorkflow,
|
|
40
|
+
event: { run: string; name: string; data: JsonValue; id?: string },
|
|
41
|
+
context: EmitContext,
|
|
42
|
+
): Promise<EmitResultJson> {
|
|
43
|
+
const workflow = target.meta.name;
|
|
44
|
+
const { name, data } = event;
|
|
45
|
+
checkEvent(target, event);
|
|
46
|
+
const id = event.id ?? `${AWF_ID_PREFIX}${randomBytes(8).toString("hex")}`;
|
|
47
|
+
const at = new Date(context.now?.() ?? Date.now()).toISOString();
|
|
48
|
+
const runId = event.run;
|
|
49
|
+
const problem = idProblem(runId);
|
|
50
|
+
if (problem) throw new EmitError(`the run's id: ${problem}`);
|
|
51
|
+
const project = await projectOf(target.file, context.home).catch((error: unknown) => {
|
|
52
|
+
throw new EmitError(messageOf(error));
|
|
53
|
+
});
|
|
54
|
+
const starts = target.starts?.includes(name) ?? true;
|
|
55
|
+
return deliver(
|
|
56
|
+
{
|
|
57
|
+
runRoot: projectRunRoot(project),
|
|
58
|
+
workflow,
|
|
59
|
+
runId,
|
|
60
|
+
event: { id, name, data, from: context.from, at },
|
|
61
|
+
deduplicate: event.id !== undefined,
|
|
62
|
+
file: target.file,
|
|
63
|
+
...(starts ? { start: { project, target } } : {}),
|
|
64
|
+
},
|
|
65
|
+
context,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** An event for a run of a workflow with events, as `deliver` takes it. */
|
|
70
|
+
export type Delivery = {
|
|
71
|
+
runRoot: string;
|
|
72
|
+
workflow: string;
|
|
73
|
+
runId: string;
|
|
74
|
+
event: { id: string; name: string; data: JsonValue; from: string; at: string };
|
|
75
|
+
/** False for an id awf made at random, which nothing repeats. */
|
|
76
|
+
deduplicate: boolean;
|
|
77
|
+
/** Given when the event may start the run: what a new run records, and the workflow it prepares. */
|
|
78
|
+
start?: { project: string; target: EventWorkflow };
|
|
79
|
+
/** The file a sender named, which a start or continue runs; absent, the run's last. */
|
|
80
|
+
file?: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The one way in, for every event a run of a workflow with events gets: a sender's, a participant's reply, or awf's
|
|
85
|
+
* own. Under the run's lock it finds a duplicate, refuses the event, or creates the run, appends
|
|
86
|
+
* the event and wakes the run.
|
|
87
|
+
*/
|
|
88
|
+
export async function deliver(
|
|
89
|
+
delivery: Delivery,
|
|
90
|
+
context: Omit<WakeContext, "file">,
|
|
91
|
+
): Promise<EmitResultJson> {
|
|
92
|
+
const { runRoot, workflow, runId, event, start, file } = delivery;
|
|
93
|
+
const woken = { ...context, ...(file ? { file } : {}) };
|
|
94
|
+
const ref = `${workflow}/${runId}`;
|
|
95
|
+
return withRunLock(runLockFile(runRoot, workflow, runId), async () => {
|
|
96
|
+
let run: Run | undefined = (await exists(join(runRoot, workflow, runId, "run.json")))
|
|
97
|
+
? await openRun(runRoot, workflow, runId)
|
|
98
|
+
: undefined;
|
|
99
|
+
// What was due is `awf tick`'s to say; an emit's caller knows it sent an event.
|
|
100
|
+
const result = (outcome: EmitResultJson["outcome"], { cause: _, ...woken }: Woken) =>
|
|
101
|
+
({ v: 1, outcome, ref, eventId: event.id, ...woken }) satisfies EmitResultJson;
|
|
102
|
+
if (run) {
|
|
103
|
+
// Not appended again, but it still wakes the run: a sender retrying after a launch that failed
|
|
104
|
+
// sends the same id.
|
|
105
|
+
if (delivery.deduplicate && foldMailbox(await readMailbox(run.dir)).events.has(event.id))
|
|
106
|
+
return result("duplicate", await wake(run, { ...woken, nudgeLive: false }));
|
|
107
|
+
if ((await readAttempts(run)).at(-1)?.outcome === "completed")
|
|
108
|
+
return result("refused", { did: "none", why: "returned" });
|
|
109
|
+
}
|
|
110
|
+
const starts = !run;
|
|
111
|
+
if (!run) {
|
|
112
|
+
if (!start) return result("refused", { did: "none", why: "not-a-start" });
|
|
113
|
+
checkStartArgs(start.target, start.project);
|
|
114
|
+
run = await createEventRun(runRoot, workflow, runId, start.project, file, context);
|
|
115
|
+
}
|
|
116
|
+
await appendMailbox(run.dir, {
|
|
117
|
+
version: MAILBOX_RECORD_VERSION,
|
|
118
|
+
kind: "event",
|
|
119
|
+
...event,
|
|
120
|
+
...(starts ? { starts: true as const } : {}),
|
|
121
|
+
});
|
|
122
|
+
return result("accepted", await wake(run, woken));
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A run an event starts has no argv: its args are what `prepare` makes of none, checked before the
|
|
128
|
+
* run is made, so a workflow that needs args refuses the sender and leaves no run.
|
|
129
|
+
*/
|
|
130
|
+
function checkStartArgs(target: EventWorkflow, project: string): void {
|
|
131
|
+
try {
|
|
132
|
+
prepareArgs(target, [], project);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
throw new EmitError(
|
|
135
|
+
`${target.meta.name}: a run an event starts has no arguments, which its prepare refuses: ${messageOf(error)}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A new run of a workflow with events, in `project`, recorded so `awf tick` finds it. */
|
|
141
|
+
async function createEventRun(
|
|
142
|
+
runRoot: string,
|
|
143
|
+
workflow: string,
|
|
144
|
+
id: string,
|
|
145
|
+
project: string,
|
|
146
|
+
file: string | undefined,
|
|
147
|
+
context: Pick<WakeContext, "home" | "environment">,
|
|
148
|
+
argv: readonly string[] = [],
|
|
149
|
+
): Promise<Run> {
|
|
150
|
+
const run = await createRun(runRoot, {
|
|
151
|
+
id,
|
|
152
|
+
workflow,
|
|
153
|
+
argv: [...argv],
|
|
154
|
+
cwd: project,
|
|
155
|
+
sandbox: null,
|
|
156
|
+
events: true,
|
|
157
|
+
env: { PATH: context.environment.PATH ?? "" },
|
|
158
|
+
...(file ? { file } : {}),
|
|
159
|
+
});
|
|
160
|
+
await recordProject(context.home, project);
|
|
161
|
+
return run;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** What `awf run --id` did to a run of a workflow with events: started it, or found it there. */
|
|
165
|
+
export type Opened = { run: string; created: boolean } & Woken;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* `awf run --id` for a workflow with events: creates the run with no event, under its lock, and
|
|
169
|
+
* starts it, so it runs to its first wait and parks there. One already there is only woken, as
|
|
170
|
+
* `awf tick` would; one that returned is refused, as its events are.
|
|
171
|
+
*/
|
|
172
|
+
export async function openEventRun(
|
|
173
|
+
target: EventWorkflow,
|
|
174
|
+
id: string,
|
|
175
|
+
argv: readonly string[],
|
|
176
|
+
context: Omit<WakeContext, "file">,
|
|
177
|
+
): Promise<Opened> {
|
|
178
|
+
const workflow = target.meta.name;
|
|
179
|
+
const problem = idProblem(id);
|
|
180
|
+
if (problem) throw new EmitError(problem);
|
|
181
|
+
const project = await projectOf(target.file, context.home).catch((error: unknown) => {
|
|
182
|
+
throw new EmitError(messageOf(error));
|
|
183
|
+
});
|
|
184
|
+
const runRoot = projectRunRoot(project);
|
|
185
|
+
const ref = `${workflow}/${id}`;
|
|
186
|
+
return withRunLock(runLockFile(runRoot, workflow, id), async () => {
|
|
187
|
+
const woken = { ...context, file: target.file };
|
|
188
|
+
if (await exists(join(runRoot, workflow, id, "run.json"))) {
|
|
189
|
+
const run = await openRun(runRoot, workflow, id);
|
|
190
|
+
if ((await readAttempts(run)).at(-1)?.outcome === "completed")
|
|
191
|
+
return { run: ref, created: false, did: "none", why: "returned" };
|
|
192
|
+
if (argv.length > 0 && JSON.stringify(argv) !== JSON.stringify(run.record.argv))
|
|
193
|
+
throw new EmitError(
|
|
194
|
+
`${ref} is there already, started with ${run.record.argv.length > 0 ? "arguments of its own" : "none"}; awf run --id ${id} with none wakes it`,
|
|
195
|
+
);
|
|
196
|
+
return { run: ref, created: false, ...(await wake(run, woken)) };
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
prepareArgs(target, argv, project);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
throw new EmitError(`prepare: ${messageOf(error)}`);
|
|
202
|
+
}
|
|
203
|
+
const run = await createEventRun(runRoot, workflow, id, project, target.file, context, argv);
|
|
204
|
+
return { run: ref, created: true, ...(await wake(run, woken)) };
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The event's name, shape and id, as the target declares them; an `EmitError` when they don't fit. */
|
|
209
|
+
export function checkEvent(
|
|
210
|
+
target: EventWorkflow,
|
|
211
|
+
event: { name: string; data: JsonValue; id?: string },
|
|
212
|
+
): void {
|
|
213
|
+
const workflow = target.meta.name;
|
|
214
|
+
const problem = eventWorkflowProblem(target);
|
|
215
|
+
if (problem) throw new EmitError(`${workflow}: ${problem}`);
|
|
216
|
+
if (!isAbsolute(target.file))
|
|
217
|
+
throw new EmitError(`${workflow}: its file, ${target.file}, is import.meta.path, absolute`);
|
|
218
|
+
if (event.name.includes(":"))
|
|
219
|
+
throw new EmitError(`${event.name}: a name with a colon is awf's own, and not sent`);
|
|
220
|
+
const schema = target.events[event.name];
|
|
221
|
+
if (!schema)
|
|
222
|
+
throw new EmitError(
|
|
223
|
+
`${workflow} declares no event ${event.name}; its events are ${Object.keys(target.events).join(", ")}`,
|
|
224
|
+
);
|
|
225
|
+
let parsed: ReturnType<typeof parseJsonSchema>;
|
|
226
|
+
try {
|
|
227
|
+
parsed = parseJsonSchema(schema);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
throw new EmitError(`${workflow}'s schema for ${event.name}: ${String(error)}`);
|
|
230
|
+
}
|
|
231
|
+
const errors = validate(parsed, event.data);
|
|
232
|
+
if (errors.length > 0)
|
|
233
|
+
throw new EmitError(
|
|
234
|
+
`${event.name}'s data does not fit its schema: ${errors.map((e) => `${e.path || "/"}: ${e.message}`).join("; ")}`,
|
|
235
|
+
);
|
|
236
|
+
const badId = event.id === undefined ? undefined : eventIdProblem(event.id);
|
|
237
|
+
if (badId) throw new EmitError(`the id ${JSON.stringify(event.id)}: ${badId}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** What is wrong with an id a sender gives an event, if anything: ids starting `awf.` are awf's. */
|
|
241
|
+
export function eventIdProblem(id: string): string | undefined {
|
|
242
|
+
return (
|
|
243
|
+
nameProblem(id) ??
|
|
244
|
+
(id.startsWith(AWF_ID_PREFIX) ? `ids starting ${AWF_ID_PREFIX} are awf's own` : undefined)
|
|
245
|
+
);
|
|
246
|
+
}
|
package/src/files.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
2
3
|
import { link, lstat, open, rename, unlink } from "node:fs/promises";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
/** `path` with its symlinks resolved, or only made absolute when it doesn't exist. */
|
|
7
|
+
export function realPath(path: string): string {
|
|
8
|
+
try {
|
|
9
|
+
return realpathSync(resolve(path));
|
|
10
|
+
} catch {
|
|
11
|
+
return resolve(path);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
4
14
|
|
|
5
15
|
/**
|
|
6
16
|
* Writes `value` to `path` whole: a temp file beside it, synced, then renamed into place. A crash
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
usage: awf allowance [harness...] [--json]
|
|
2
|
+
|
|
3
|
+
What is left of each harness's plan, as the harness itself shows it: claude's /usage, as
|
|
4
|
+
claude -p /usage prints it; codex's /status, as its app-server answers it; and cursor's /usage,
|
|
5
|
+
read in a Herdr pane. pi has none of its own. Nothing asks a model. Every harness when
|
|
6
|
+
none is named: claude, codex, pi, cursor.
|
|
7
|
+
|
|
8
|
+
A plan's monthly price is an estimate at the vendor's list price as of the table's date:
|
|
9
|
+
plans 2026-10-05.
|
|
10
|
+
|
|
11
|
+
--json prints one document, the interface for scripts, with the plan as the
|
|
12
|
+
harness names it and no price; what it prints otherwise is for reading. A harness that could
|
|
13
|
+
not be read says why. Exits 0 once every harness was asked, 2 on a usage error.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
usage: awf emit --target <file> --id <id> <event> [<json>] [--event-id <id>] [--json]
|
|
2
|
+
|
|
3
|
+
Sends an event to the run of a workflow with events that --id names. That run starts if it has
|
|
4
|
+
none and the event is one that starts one, and is woken if it has one: told while it runs,
|
|
5
|
+
continued when it is parked or crashed. The run lives in the project of <file>, the nearest
|
|
6
|
+
directory above it holding .awf/, whoever sends. --event-id names the occurrence: the same id
|
|
7
|
+
again is a duplicate.
|
|
8
|
+
|
|
9
|
+
Prints what became of the event, and what was done to the run: accepted, duplicate or refused.
|
|
10
|
+
--json prints one document. Exits 0 for any of the three; 1 when <file> could not be loaded, and
|
|
11
|
+
nothing is kept, or when the run's attempt could not be started (the event is kept, and the next
|
|
12
|
+
event or awf tick tries again); 2 on a usage error, for a workflow with no events, or when the
|
|
13
|
+
event is wrong for its target: its name, its data, or its id.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
usage: awf inbox [<run>] [--open] [--workflow <name>] [--as <key>] [--after <cursor>] [--wait <seconds>]
|
|
2
|
+
[--cwd <directory>] [--run-root <directory>] [--json]
|
|
3
|
+
|
|
4
|
+
What agents said to a participant outside the run, and what it said back: routes, messages,
|
|
5
|
+
answers, questions that closed unanswered, and each attempt's end, oldest first. Without a run,
|
|
6
|
+
every run here that connected one; --workflow narrows that. A run is <workflow>/<id>, or an <id>
|
|
7
|
+
only one workflow has. --after reads on from a cursor the last answer gave; --wait
|
|
8
|
+
waits up to that many seconds, at most 300, when nothing is new. --open lists the questions
|
|
9
|
+
still waiting for an answer, a snapshot. --json prints one item per line.
|
|
10
|
+
|
|
11
|
+
Exits 0 on a read that worked, 1 when there is no such run, it has no participant, or the
|
|
12
|
+
cursor is another's, 2 on a usage error, 3 when the run's files could not be read.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
usage: awf init [dir] [--no-install]
|
|
2
|
+
|
|
3
|
+
Sets up the current directory, or dir, for writing workflows: hello/workflow.ts, a workflow
|
|
4
|
+
with one agent, and hello/workflow.test.ts, its test; package.json, tsconfig.json, .gitignore
|
|
5
|
+
and AGENTS.md. Then runs bun install, unless --no-install, for editor types: awf runs a
|
|
6
|
+
workflow against its own copies either way.
|
|
7
|
+
|
|
8
|
+
Never overwrites. A file that exists is skipped, except that package.json gains the
|
|
9
|
+
devDependencies and scripts it lacks and .gitignore the lines it lacks.
|
|
10
|
+
|
|
11
|
+
Exits 0; 1 when a file could not be written, or bun install failed (the files written are
|
|
12
|
+
kept); 2 on a usage error.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
usage: awf logs [<run>] [@<handle>] [--agent <key>] [--stage <name>] [--after <cursor>]
|
|
2
|
+
[--limit <n>] [--wait <seconds> | --follow] [--full] [--cwd <directory>]
|
|
3
|
+
[--run-root <directory>] [--json]
|
|
4
|
+
|
|
5
|
+
What a run did and is doing: its events and what its agents did, read from their own session
|
|
6
|
+
files, merged by time. With no run, the newest here. Without --after, the last 40 events; with
|
|
7
|
+
it, what came after, oldest first. Every answer ends with the command that gets only what is
|
|
8
|
+
new. --wait: with nothing to show yet, wait for the first event, at most that many seconds, up
|
|
9
|
+
to 300; --follow prints each event as it comes until the run ends. @<handle> names one event,
|
|
10
|
+
and --full prints its text, with common credential shapes masked; --json carries it always.
|
|
11
|
+
|
|
12
|
+
Exits 0 on any read that worked, 1 when there is no such run or the cursor or handle does not
|
|
13
|
+
fit it, 2 on a usage error, 3 when the run's files could not be read.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
usage: awf reply <run> <agent> [--to <id>] [--as <participant>] [<text>]
|
|
2
|
+
awf send <run> <agent> [--as <participant>] [<text>]
|
|
3
|
+
awf send <run> --as <participant> --event-id <id> [--author <name>] [<text>]
|
|
4
|
+
each takes [--cwd <directory>] [--run-root <directory>] [--json]
|
|
5
|
+
|
|
6
|
+
A run is <workflow>/<id>, or an <id> only one workflow has, here or in a project awf knows.
|
|
7
|
+
Answers a question an agent asked a participant outside the run, or messages an agent first,
|
|
8
|
+
over a route the workflow allowed. The text is the argument, or stdin without one. --to names
|
|
9
|
+
the question, as awf inbox prints it; it may be left out when the agent has one open. --as
|
|
10
|
+
names the participant when the workflow connects more than one. Neither waits for an answer:
|
|
11
|
+
it comes back in awf inbox. With --event-id, send is a reply on a participant the run routes to its
|
|
12
|
+
code: no agent, it reaches the run whether it runs or is parked, and the same id again is a
|
|
13
|
+
duplicate. The id is one the run has not had from anyone, as a chat's message ids are. It prints
|
|
14
|
+
as awf emit does, and exits 0 for whatever the run did with it (accepted, duplicate, or refused
|
|
15
|
+
by a run that returned); 1 when the run or participant isn't found, or its attempt didn't start.
|
|
16
|
+
|
|
17
|
+
Exits 0 when delivered, 1 when refused (the code says why), 2 on a usage error, 3 when the run
|
|
18
|
+
could not be reached.
|
package/src/help/run.txt
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
usage: awf run [options] <workflow-file> [options] [-- workflow arguments...]
|
|
2
|
+
awf run [options] <workflow-file> --id <id> --continue
|
|
3
|
+
run options: --id <id>, --continue, --from-stage <stage>, --values <file>,
|
|
4
|
+
--timeout <duration>, --run-root <directory>, --cwd <directory>, --sandbox <file>,
|
|
5
|
+
--json, --no-watch, --here
|
|
6
|
+
|
|
7
|
+
Each awf run is an attempt of a run: a new run, with --id's id or a generated one, or, with
|
|
8
|
+
--continue, the run --id names, with the arguments, --cwd and --sandbox it was started with. A continue reuses
|
|
9
|
+
the stages that succeeded, up to the first with no record or to --from-stage, and runs the rest.
|
|
10
|
+
A new run with --from-stage starts there. A stage before it with no record to reuse takes its
|
|
11
|
+
value from --values, a JSON object by stage name, checked by its result; one that returns
|
|
12
|
+
nothing is passed. Without one the attempt stops, naming the stage and its schema.
|
|
13
|
+
A run is kept in .awf/runs/<workflow>/<id> under --cwd, unless --run-root names another folder
|
|
14
|
+
for .awf/runs.
|
|
15
|
+
A run has no deadline unless --timeout gives it one, per attempt.
|
|
16
|
+
A workflow that knows how to present its result prints that; --json prints the full result instead.
|
|
17
|
+
Either way the full result is kept as output.json in the run's folder, beside report.md
|
|
18
|
+
when the workflow writes one. A run that fails or is cancelled once its agents have started keeps
|
|
19
|
+
output.json too, with what it spent and why it ended; --json prints it. A second Ctrl-C stops
|
|
20
|
+
awf at once, without it.
|
|
21
|
+
--cwd sets the directory the workflow and its agents work in; it defaults to the current one.
|
|
22
|
+
--sandbox puts every agent of the run in one sandbox, working in --cwd; the file is a JSON spec
|
|
23
|
+
such as {"read": ["/data/request.md"], "srt": {}}, naming srt or docker, whose relative paths are
|
|
24
|
+
from --cwd. A workflow that opens a sandbox of its own is refused.
|
|
25
|
+
A sandbox with its own Herdr, as a docker box has, gets a tab in the run's workspace showing its
|
|
26
|
+
panes; --no-watch leaves it out, and awf still prints the command that shows them.
|
|
27
|
+
--here, run by an agent in a Herdr pane, starts the run in a new tab and has it take that agent's
|
|
28
|
+
session over as one of its agents, from its next turn until the run ends. It prints a line for
|
|
29
|
+
the agent to end its turn with, which is how the run finds the session.
|
|
30
|
+
A run of a workflow with events is named by --id, or by the workflow's id(args). awf run starts it
|
|
31
|
+
with its arguments and no event, detached in its project, as awf emit would, and the run waits at
|
|
32
|
+
its first receive. A run already there is only woken. A run an event starts has no argv: its args
|
|
33
|
+
are what prepare makes of none.
|
|
34
|
+
Exits 0 when the run completed or parked, 1 when it failed or timed out, 2 on a usage error or
|
|
35
|
+
when the run is refused, 3 when it stopped and can go on with --continue, and 128 plus the
|
|
36
|
+
signal's number when it was interrupted.
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
awf run examples/minimum-review/review-loop.ts
|
|
40
|
+
awf run --timeout 20m examples/minimum-review/review-loop.ts
|
|
41
|
+
awf run examples/minimum-review/review-loop.ts -- packages/engine/src
|
|
42
|
+
awf run examples/minimum-review/review-loop.ts --cwd ../other-repo
|
|
43
|
+
|
|
44
|
+
Workflow files are trusted code and run with your filesystem and process authority.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
usage: awf runs [--limit <n>] [--cwd <directory>] [--run-root <directory>] [--json]
|
|
2
|
+
awf runs <run> [--cwd <directory>] [--run-root <directory>] [--json]
|
|
3
|
+
|
|
4
|
+
The runs here, newest first, or one run in depth: its attempts, the stages of the last, what
|
|
5
|
+
each agent is doing, and what to run next. The list shows 20 unless --limit says another
|
|
6
|
+
number. A run is <workflow>/<id>, or an <id> only one
|
|
7
|
+
workflow has. Runs are listed under --cwd's .awf/runs, or --run-root, as awf run keeps them; a
|
|
8
|
+
run named is found there first, then in each project awf has started a run with events in.
|
|
9
|
+
|
|
10
|
+
--json prints one document, the interface for scripts. Exits 0 on any read that worked, 1 when
|
|
11
|
+
there is no such run, 2 on a usage error, 3 when the run's files could not be read.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
usage: awf test [paths...] [-t <pattern>] [--watch] [--timeout <duration>]
|
|
2
|
+
|
|
3
|
+
Runs a workflow's tests (*.test.ts and *.spec.ts files) under the current directory, or under
|
|
4
|
+
each file or directory given, with agentswf/workflow, agentswf/testing and typebox served as
|
|
5
|
+
awf run serves them: nothing is installed. -t runs the tests whose names match the pattern;
|
|
6
|
+
--watch reruns them on a change; --timeout bounds each test, 5s by default.
|
|
7
|
+
|
|
8
|
+
Exits 0 when every test passed, 1 when one failed or none was found, 2 on a usage error. What
|
|
9
|
+
it prints is for reading, not parsing.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
usage: awf tick [--cwd <directory>] [--run-root <directory>] [--json]
|
|
2
|
+
awf tick --all [--json]
|
|
3
|
+
|
|
4
|
+
Wakes every run of a workflow with events under --cwd's .awf/runs, or --run-root, the runs awf
|
|
5
|
+
runs lists there, as an event would: continues a parked run whose wait's timer is due, and a
|
|
6
|
+
crashed or failed one until it has failed three times in a row. Runs of other workflows are left
|
|
7
|
+
as they are. --all wakes them in every project awf has started one in, as ~/.awf/projects lists
|
|
8
|
+
them. Run it on any schedule; a run is continued once however many wakers come at once.
|
|
9
|
+
|
|
10
|
+
Prints a line for each run it woke, and nothing when it woke none. --json prints one document.
|
|
11
|
+
Exits 0; 1 when a project is gone or a run could not be woken, the others still woken; 2 on a
|
|
12
|
+
usage error.
|
package/src/here.ts
CHANGED
|
@@ -53,6 +53,12 @@ export async function startHere(
|
|
|
53
53
|
stderr(`awf: ${ready.refused}`);
|
|
54
54
|
return 2;
|
|
55
55
|
}
|
|
56
|
+
if ("opens" in ready) {
|
|
57
|
+
stderr(
|
|
58
|
+
"awf: a workflow with events runs detached, in its project; awf run --id starts it, without --here",
|
|
59
|
+
);
|
|
60
|
+
return 2;
|
|
61
|
+
}
|
|
56
62
|
for (const line of ready.prepared.continued?.warnings ?? []) notice(line);
|
|
57
63
|
const env = environment.environment ?? process.env;
|
|
58
64
|
const run = environment.herdr ?? runProcess;
|
|
@@ -99,7 +105,7 @@ export async function startHere(
|
|
|
99
105
|
// Under codex this can be another pane's workspace (E8); the run's tab still works from it.
|
|
100
106
|
workspace,
|
|
101
107
|
cwd: command.shellCwd,
|
|
102
|
-
label: tabLabel(ready.loaded.
|
|
108
|
+
label: tabLabel(ready.loaded.workflow.meta.name, id),
|
|
103
109
|
argv: [
|
|
104
110
|
...self,
|
|
105
111
|
"run",
|
|
@@ -125,7 +131,7 @@ export async function startHere(
|
|
|
125
131
|
"Run the workflow from a shell with awf run instead.",
|
|
126
132
|
);
|
|
127
133
|
}
|
|
128
|
-
notice(startedLine(ready.loaded.
|
|
134
|
+
notice(startedLine(ready.loaded.workflow.meta.name, id, command));
|
|
129
135
|
stdout(
|
|
130
136
|
[
|
|
131
137
|
`awf: ${basename(ready.loaded.file)} is starting in Herdr tab ${started.tabId}. Once this turn ends it takes this session over: each of its steps arrives here as a prompt, and a last message hands the session back.`,
|
|
@@ -192,7 +198,7 @@ function passedSession(
|
|
|
192
198
|
}
|
|
193
199
|
|
|
194
200
|
/**
|
|
195
|
-
* What lets a sandboxed session reach Herdr's socket, which `--here` and every `
|
|
201
|
+
* What lets a sandboxed session reach Herdr's socket, which `--here` and every `wfa` call need: its
|
|
196
202
|
* harness's own advice, for the harness whose session variable this shell has.
|
|
197
203
|
*/
|
|
198
204
|
function sandboxFix(env: Readonly<Record<string, string | undefined>>): string {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Holds a run's lock from another process for `emit.test.ts`: `bun hold-run-lock.fixture.ts {file} {ms}`.
|
|
2
|
+
import { withRunLock } from "./run-lock";
|
|
3
|
+
|
|
4
|
+
const [file, ms] = process.argv.slice(2);
|
|
5
|
+
await withRunLock(file!, async () => {
|
|
6
|
+
console.log("held");
|
|
7
|
+
await Bun.sleep(Number(ms));
|
|
8
|
+
});
|