@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/event-ids.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { AWF_ID_PREFIX } from "@agentswf/contract/workflow";
|
|
2
|
+
import type { DerivedIds } from "./run-policy/waits";
|
|
3
|
+
|
|
4
|
+
function hashed(kind: string, text: string): string {
|
|
5
|
+
const hash = new Bun.CryptoHasher("sha256").update(text).digest("hex");
|
|
6
|
+
return `${AWF_ID_PREFIX}${kind}.${hash.slice(0, 16)}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The id timer `name` is taken under: made from its name, so it fires once. */
|
|
10
|
+
export function timerEventId(name: string): string {
|
|
11
|
+
return hashed("timer", name);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The id of branch `branch`'s time read from state, `due` an ISO time: made from both, so a time is
|
|
16
|
+
* returned once by a branch of that name, whichever loop reads it.
|
|
17
|
+
*/
|
|
18
|
+
export function atEventId(branch: string, due: string): string {
|
|
19
|
+
return hashed("at", `${branch}\n${due}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The id of the end of `agent`'s turn `turn`, begun in attempt `began`: made from the three, so it
|
|
24
|
+
* fits the grammar whatever each is, and is the same whoever writes it, so one end is ever kept.
|
|
25
|
+
*/
|
|
26
|
+
export function endIdOf(agent: string, turn: string, began: number): string {
|
|
27
|
+
return hashed("end", `${agent}\0${turn}\0${began}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The derived ids, as the waits' policy takes them. */
|
|
31
|
+
export const DERIVED_IDS: DerivedIds = { timer: timerEventId, at: atEventId };
|
package/src/files.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
2
|
+
import { realpathSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { link, lstat, open, rename, unlink } from "node:fs/promises";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
|
|
6
6
|
/** `path` with its symlinks resolved, or only made absolute when it doesn't exist. */
|
|
7
7
|
export function realPath(path: string): string {
|
|
@@ -32,14 +32,38 @@ export async function writeWhole(path: string, text: string): Promise<void> {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* `text` at `path` whole, synchronously, for a writer that must not wait between two steps: a temp
|
|
37
|
+
* file renamed into place, mode `0600`. Not synced: it needs only to exist and be whole.
|
|
38
|
+
*/
|
|
39
|
+
export function writeWholeSync(path: string, text: string): void {
|
|
40
|
+
const temporary = temporaryBeside(path);
|
|
41
|
+
try {
|
|
42
|
+
writeFileSync(temporary, text, { mode: 0o600, flag: "wx" });
|
|
43
|
+
renameSync(temporary, path);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
try {
|
|
46
|
+
unlinkSync(temporary);
|
|
47
|
+
} catch {
|
|
48
|
+
// Never made, or gone already.
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
/**
|
|
36
55
|
* Links a complete file holding `value` to `path`, which fails when `path` exists: the first to
|
|
37
56
|
* link holds it. False when it was taken.
|
|
38
57
|
*/
|
|
39
|
-
export
|
|
58
|
+
export function linkNew(path: string, value: unknown): Promise<boolean> {
|
|
59
|
+
return linkNewText(path, jsonText(value));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** As `linkNew`, for text. */
|
|
63
|
+
export async function linkNewText(path: string, text: string): Promise<boolean> {
|
|
40
64
|
const temporary = temporaryBeside(path);
|
|
41
65
|
try {
|
|
42
|
-
await writeSynced(temporary,
|
|
66
|
+
await writeSynced(temporary, text);
|
|
43
67
|
await link(temporary, path);
|
|
44
68
|
return true;
|
|
45
69
|
} catch (error) {
|
|
@@ -62,6 +86,22 @@ async function writeSynced(path: string, text: string): Promise<void> {
|
|
|
62
86
|
}
|
|
63
87
|
}
|
|
64
88
|
|
|
89
|
+
/** Makes the entries of folder `dir`, a rename or link into it, durable. */
|
|
90
|
+
export async function syncDir(dir: string): Promise<void> {
|
|
91
|
+
const handle = await open(dir, "r");
|
|
92
|
+
try {
|
|
93
|
+
await handle.sync();
|
|
94
|
+
} finally {
|
|
95
|
+
await handle.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Whether `path` is below `root`, not `root` itself; a child named `..x` is below it. */
|
|
100
|
+
export function isInside(root: string, path: string): boolean {
|
|
101
|
+
const inside = relative(root, path);
|
|
102
|
+
return inside !== "" && inside !== ".." && !inside.startsWith(`..${sep}`) && !isAbsolute(inside);
|
|
103
|
+
}
|
|
104
|
+
|
|
65
105
|
/** Whether `path` names anything, a dangling link included. */
|
|
66
106
|
export async function exists(path: string): Promise<boolean> {
|
|
67
107
|
return lstat(path).then(
|
package/src/hand-over.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// The global `awf`'s first step, before the engine is imported: a project whose installed `agentswf`
|
|
2
|
+
// is another version runs on that one (ADR 0015). Imports nothing of the engine, so a hand-over
|
|
3
|
+
// costs a Bun start and no more, and the engine's own checks can share how a project's engine is
|
|
4
|
+
// found.
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
|
|
9
|
+
/** The `agentswf` installed in a `node_modules` at or above a directory. */
|
|
10
|
+
export type InstalledEngine = {
|
|
11
|
+
/** Its package's folder, its real path: what `AWF_ENGINE` names. */
|
|
12
|
+
dir: string;
|
|
13
|
+
/** The directory whose `node_modules` holds it: where its `awf` is run from. */
|
|
14
|
+
root: string;
|
|
15
|
+
/** Its `package.json`'s `version`, not the range a project declares. */
|
|
16
|
+
version: string;
|
|
17
|
+
/** Its `awf`, as its `package.json`'s `bin` gives it. */
|
|
18
|
+
bin: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A project's engine: installed, declared in the nearest `package.json` at or above it and not
|
|
23
|
+
* installed (or installed broken), or none, which any engine runs.
|
|
24
|
+
*/
|
|
25
|
+
export type ProjectEngine =
|
|
26
|
+
| ({ kind: "installed" } & InstalledEngine)
|
|
27
|
+
| { kind: "declared"; manifest: string }
|
|
28
|
+
| { kind: "none" };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The `agentswf` found by walking up from `from` for `node_modules/agentswf/package.json`. Not
|
|
32
|
+
* Bun's resolver, whose auto-install answers with a cache copy where nothing is installed. One
|
|
33
|
+
* whose manifest gives no version or `awf` is broken, and its `package.json` is returned instead.
|
|
34
|
+
*/
|
|
35
|
+
export function installedEngine(from: string): InstalledEngine | undefined {
|
|
36
|
+
const found = findEngine(from);
|
|
37
|
+
return found && "version" in found ? found : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function findEngine(from: string): InstalledEngine | { broken: string } | undefined {
|
|
41
|
+
for (let dir = resolve(from); ; dir = dirname(dir)) {
|
|
42
|
+
const manifest = join(dir, "node_modules", "agentswf", "package.json");
|
|
43
|
+
if (existsSync(manifest)) {
|
|
44
|
+
const read = readManifest(manifest);
|
|
45
|
+
const bin = typeof read?.bin === "object" ? read.bin?.awf : read?.bin;
|
|
46
|
+
if (typeof read?.version !== "string" || typeof bin !== "string") return { broken: manifest };
|
|
47
|
+
const root = realpathSync(dirname(manifest));
|
|
48
|
+
return { dir: root, root: dir, version: read.version, bin: resolve(root, bin) };
|
|
49
|
+
}
|
|
50
|
+
if (dirname(dir) === dir) return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* `project`'s engine: the one installed above it, else one the nearest `package.json` at or above
|
|
56
|
+
* it declares.
|
|
57
|
+
*/
|
|
58
|
+
export function projectEngine(project: string): ProjectEngine {
|
|
59
|
+
const found = findEngine(project);
|
|
60
|
+
if (found && "broken" in found) return { kind: "declared", manifest: found.broken };
|
|
61
|
+
if (found) return { kind: "installed", ...found };
|
|
62
|
+
for (let dir = resolve(project); ; dir = dirname(dir)) {
|
|
63
|
+
const manifest = join(dir, "package.json");
|
|
64
|
+
if (existsSync(manifest)) {
|
|
65
|
+
const read = readManifest(manifest);
|
|
66
|
+
const declared = ["dependencies", "devDependencies", "optionalDependencies"].some((field) => {
|
|
67
|
+
const deps = read?.[field];
|
|
68
|
+
return typeof deps === "object" && deps !== null && "agentswf" in deps;
|
|
69
|
+
});
|
|
70
|
+
return declared ? { kind: "declared", manifest } : { kind: "none" };
|
|
71
|
+
}
|
|
72
|
+
if (dirname(dir) === dir) return { kind: "none" };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type Manifest = Record<string, unknown> & {
|
|
77
|
+
version?: unknown;
|
|
78
|
+
bin?: string | { awf?: unknown };
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function readManifest(file: string): Manifest | undefined {
|
|
82
|
+
try {
|
|
83
|
+
const value: unknown = JSON.parse(readFileSync(file, "utf8"));
|
|
84
|
+
return typeof value === "object" && value !== null ? (value as Manifest) : undefined;
|
|
85
|
+
} catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Set on an `awf` a hand-over runs: the engine it is, so that one doesn't hand over again. */
|
|
91
|
+
export const ENGINE_VARIABLE = "AWF_ENGINE";
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* What the global `awf` does first: when the `agentswf` installed above the shell's directory is
|
|
95
|
+
* another version than its own, it warns, runs that one's `awf` with the same argv, and exits as
|
|
96
|
+
* it does. Returns when this `awf` runs the command itself. `init`, `help`, `--version` and
|
|
97
|
+
* `tick --all` name no project's runs, and are never handed over.
|
|
98
|
+
*/
|
|
99
|
+
export async function handOver(own: {
|
|
100
|
+
/** This `awf`'s `agentswf` folder. */
|
|
101
|
+
dir: string;
|
|
102
|
+
version: string;
|
|
103
|
+
argv?: readonly string[];
|
|
104
|
+
cwd?: string;
|
|
105
|
+
environment?: Readonly<Record<string, string | undefined>>;
|
|
106
|
+
}): Promise<void> {
|
|
107
|
+
const argv = own.argv ?? process.argv.slice(2);
|
|
108
|
+
const environment = own.environment ?? process.env;
|
|
109
|
+
const target = handOverTarget(argv, own.cwd ?? process.cwd(), own.version);
|
|
110
|
+
if (!target) return;
|
|
111
|
+
// The child, walking from the same directory, finds itself; the guard is for a loop all the same.
|
|
112
|
+
if (environment[ENGINE_VARIABLE] === realpathSync(own.dir)) return;
|
|
113
|
+
process.stderr.write(
|
|
114
|
+
`awf: awf ${own.version} is not this project's ${target.version}; running the project's. Update one, or run bunx awf here.\n`,
|
|
115
|
+
);
|
|
116
|
+
const child = Bun.spawn({
|
|
117
|
+
cmd: [process.execPath, "--no-env-file", target.bin, ...argv],
|
|
118
|
+
env: { ...environment, [ENGINE_VARIABLE]: target.dir },
|
|
119
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
120
|
+
});
|
|
121
|
+
const forward = (signal: NodeJS.Signals) => () => child.kill(signal);
|
|
122
|
+
// Each is forwarded, for a signal sent to this process alone. The terminal's Ctrl-C reaches the
|
|
123
|
+
// child twice, through the process group too, and a run takes a repeat within a second as one;
|
|
124
|
+
// the prompt comes back once the child has ended, not before.
|
|
125
|
+
const listeners: [NodeJS.Signals, () => void][] = [
|
|
126
|
+
["SIGTERM", forward("SIGTERM")],
|
|
127
|
+
["SIGHUP", forward("SIGHUP")],
|
|
128
|
+
["SIGINT", forward("SIGINT")],
|
|
129
|
+
];
|
|
130
|
+
for (const [signal, listener] of listeners) process.on(signal, listener);
|
|
131
|
+
const code = await child.exited;
|
|
132
|
+
for (const [signal, listener] of listeners) process.off(signal, listener);
|
|
133
|
+
if (child.signalCode) process.kill(process.pid, child.signalCode);
|
|
134
|
+
process.exit(code ?? 1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The engine `argv`, typed in `cwd`, is handed over to; undefined when this one runs it. */
|
|
138
|
+
export function handOverTarget(
|
|
139
|
+
argv: readonly string[],
|
|
140
|
+
cwd: string,
|
|
141
|
+
version: string,
|
|
142
|
+
): InstalledEngine | undefined {
|
|
143
|
+
const [word] = argv;
|
|
144
|
+
if (word === undefined || NOT_HANDED_OVER.has(word)) return undefined;
|
|
145
|
+
if (word === "tick" && argv.includes("--all")) return undefined;
|
|
146
|
+
const installed = installedEngine(cwd);
|
|
147
|
+
return installed && installed.version !== version ? installed : undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const NOT_HANDED_OVER = new Set(["init", "help", "-h", "--help", "--version"]);
|
package/src/help/allowance.txt
CHANGED
|
@@ -8,6 +8,7 @@ none is named: claude, codex, pi, cursor.
|
|
|
8
8
|
A plan's monthly price is an estimate at the vendor's list price as of the table's date:
|
|
9
9
|
plans 2026-10-05.
|
|
10
10
|
|
|
11
|
-
--json prints one document, the interface for scripts, with the plan as the
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
--json prints one document, the interface for scripts, with the plan as the harness names it and
|
|
12
|
+
no price; what it prints otherwise is for reading. A harness that could not be read says why; a
|
|
13
|
+
failure is a refusal whose code says why: usage or internal-error. Exits 0 once every harness
|
|
14
|
+
was asked, 2 on a usage error.
|
package/src/help/emit.txt
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
usage: awf emit --target <file> --id <id> <event> [<json>] [--event-id <id>] [--json]
|
|
1
|
+
usage: awf emit --target <file> --id <id> <event> [<json>] [--event-id <id>] [--source <name>] [--json]
|
|
2
|
+
awf emit <run> <key>:reply '{"text": …, "author": …}' [--event-id <id>] [--source <name>] [--cwd <directory>] [--run-root <directory>] [--json]
|
|
2
3
|
|
|
3
4
|
Sends an event to the run of a workflow with events that --id names. That run starts if it has
|
|
4
5
|
none and the event is one that starts one, and is woken if it has one: told while it runs,
|
|
5
6
|
continued when it is parked or crashed. The run lives in the project of <file>, the nearest
|
|
6
7
|
directory above it holding .awf/, whoever sends. --event-id names the occurrence: the same id
|
|
7
|
-
again is a duplicate.
|
|
8
|
+
again is a duplicate. --source names the integration that sent it, such as github, which the
|
|
9
|
+
event a wait returns carries.
|
|
10
|
+
|
|
11
|
+
The second form sends a participant's message to the code of a run that is there, <workflow>/<id>
|
|
12
|
+
or an <id> only one workflow has, here or in a project awf knows: <key>:reply, which the workflow
|
|
13
|
+
declares, checked against the file the run's last attempt ran. "author" is optional. Its
|
|
14
|
+
--event-id is the message's own id, so a resend is a duplicate. It never starts a run, and may
|
|
15
|
+
arrive before the run connects the participant: it waits for the run's next wait. The first form
|
|
16
|
+
takes only the workflow's own events.
|
|
8
17
|
|
|
9
18
|
Prints what became of the event, and what was done to the run: accepted, duplicate or refused.
|
|
10
|
-
--json prints one document
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
--json prints one document: the result, or a refusal whose code says why it failed: usage,
|
|
20
|
+
no-run, ambiguous, id, file, event, prepare, project, no-engine, other-engine, unreachable or
|
|
21
|
+
internal-error. Exits 0 for any of the three; 1 when <file> could not be loaded, or the run
|
|
22
|
+
isn't found or can't be read, and nothing is kept, or when the run's attempt could not be
|
|
23
|
+
started (the event is kept, and the next event or awf tick tries again); 2 on a usage error, for
|
|
24
|
+
a workflow with no events, or when the event is wrong for its target: its name, its data, or its
|
|
25
|
+
id.
|
package/src/help/inbox.txt
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
usage: awf inbox [<run>] [--open] [--workflow <name>] [--as <key>] [--after <cursor>] [--wait <
|
|
1
|
+
usage: awf inbox [<run>] [--open] [--workflow <name>] [--as <key>] [--after <cursor>] [--wait <duration>]
|
|
2
2
|
[--cwd <directory>] [--run-root <directory>] [--json]
|
|
3
3
|
|
|
4
4
|
What agents said to a participant outside the run, and what it said back: routes, messages,
|
|
5
5
|
answers, questions that closed unanswered, and each attempt's end, oldest first. Without a run,
|
|
6
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
|
-
|
|
9
|
-
|
|
7
|
+
only one workflow has. --after reads on from a cursor the last answer gave; --wait waits up to
|
|
8
|
+
that long, at most 5m, when nothing is new. --open lists the questions still waiting for an
|
|
9
|
+
answer, a snapshot. --json prints one item per line, and when it fails a refusal last whose code
|
|
10
|
+
says why: usage, no-run, ambiguous, id, cursor, no-participant, unreachable, no-engine,
|
|
11
|
+
other-engine or internal-error.
|
|
10
12
|
|
|
11
13
|
Exits 0 on a read that worked, 1 when there is no such run, it has no participant, or the
|
|
12
14
|
cursor is another's, 2 on a usage error, 3 when the run's files could not be read.
|
package/src/help/logs.txt
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
usage: awf logs [<run>] [@<handle>] [--agent <key>] [--stage <name>] [--after <cursor>]
|
|
2
|
-
[--limit <n>] [--wait <
|
|
2
|
+
[--limit <n>] [--wait <duration> | --follow] [--full] [--cwd <directory>]
|
|
3
3
|
[--run-root <directory>] [--json]
|
|
4
4
|
|
|
5
5
|
What a run did and is doing: its events and what its agents did, read from their own session
|
|
6
6
|
files, merged by time. With no run, the newest here. Without --after, the last 40 events; with
|
|
7
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
|
|
9
|
-
|
|
10
|
-
and --full prints its text, with common credential shapes masked; --json carries it
|
|
8
|
+
new. --wait: with nothing to show yet, wait for the first event, at most that long, up to 5m,
|
|
9
|
+
such as 90s; --follow prints each event as it comes until the run ends. @<handle> names one
|
|
10
|
+
event, and --full prints its text, with common credential shapes masked; --json carries it
|
|
11
|
+
always.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
With --json, a failure prints a refusal whose code says why: usage, no-run, ambiguous, id,
|
|
14
|
+
cursor, unreachable, no-engine, other-engine or internal-error. Exits 0 on any read that worked,
|
|
15
|
+
1 when there is no such run or the cursor or handle does not fit it, 2 on a usage error, 3 when
|
|
16
|
+
the run's files could not be read.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
usage: awf open <run> <agent> [--no-fork] [--session <id>] [-p [<text>]]
|
|
2
|
+
[--cwd <directory>] [--run-root <directory>] [--json]
|
|
3
|
+
|
|
4
|
+
A run is <workflow>/<id>, or an <id> only one workflow has, here or in a project awf knows.
|
|
5
|
+
Without an agent, it prints the command that opens each of the run's agents.
|
|
6
|
+
Opens a copy of an agent's latest session in its harness's own TUI, where the agent worked: its
|
|
7
|
+
working directory, its home and skills, its sandbox, on the model and effort it ran on, with the
|
|
8
|
+
harness's own permission prompts. The copy is made without asking the model, and is yours: the
|
|
9
|
+
run and the agent's session are unchanged, and awf keeps no record of it.
|
|
10
|
+
|
|
11
|
+
--no-fork opens the session itself: what you say stays with the agent, awf logs shows it, and the
|
|
12
|
+
next attempt resumes it. It is refused while the run is live; while it is open, no attempt starts
|
|
13
|
+
and an event waits, and once the harness exits a run with events is woken for it.
|
|
14
|
+
--session opens another session in the agent's place: a copy an earlier awf open made, as it is;
|
|
15
|
+
or one of the agent's earlier sessions, as its latest would be.
|
|
16
|
+
-p asks one question headless and prints the answer; the text is its argument, or stdin without
|
|
17
|
+
one. It runs as the agent's turns ran, without prompts, in its sandbox when it had one; on
|
|
18
|
+
claude, billed per token headless, it is refused unless the agent ran metered.
|
|
19
|
+
As the harness exits, awf prints the command that opens the same session again.
|
|
20
|
+
|
|
21
|
+
With --json, -p prints {"v": 1, "session", "forkedFrom"?, "reply", "again"}, or a refusal whose
|
|
22
|
+
code says why: usage, no-run, ambiguous, id, no-session, live, opened, unreachable, no-engine,
|
|
23
|
+
other-engine or internal-error. no-session is an agent with no session or operation to open;
|
|
24
|
+
live, --no-fork, or a sandboxed agent, on a running run, or a --no-fork whose session changed as
|
|
25
|
+
it held the run; opened, a run another awf open --no-fork holds, or a sandbox another awf open
|
|
26
|
+
has open; internal-error, a copy or a -p question that failed. A TUI has no JSON: --json without
|
|
27
|
+
-p is a usage error. Exits with the TUI's exit code, or 0 once -p printed its answer; 1 when
|
|
28
|
+
refused or failed; 2 on a usage error, or a -p or a TUI the agent's harness can't give; 3 when
|
|
29
|
+
the run or the agent's place could not be read, or awf itself failed.
|
package/src/help/reply.txt
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
usage: awf reply <run> <agent> [--to <id>] [--as <participant>] [<text>]
|
|
2
2
|
awf send <run> <agent> [--as <participant>] [<text>]
|
|
3
|
-
awf send <run> --as <participant> --event-id <id> [--author <name>] [<text>]
|
|
4
3
|
each takes [--cwd <directory>] [--run-root <directory>] [--json]
|
|
5
4
|
|
|
6
5
|
A run is <workflow>/<id>, or an <id> only one workflow has, here or in a project awf knows.
|
|
@@ -8,11 +7,10 @@ Answers a question an agent asked a participant outside the run, or messages an
|
|
|
8
7
|
over a route the workflow allowed. The text is the argument, or stdin without one. --to names
|
|
9
8
|
the question, as awf inbox prints it; it may be left out when the agent has one open. --as
|
|
10
9
|
names the participant when the workflow connects more than one. Neither waits for an answer:
|
|
11
|
-
it comes back in awf inbox.
|
|
12
|
-
|
|
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.
|
|
10
|
+
it comes back in awf inbox. A message on a participant to the run's code is sent with awf emit
|
|
11
|
+
<run> <participant>:reply '{"text":…,"author":…}' --event-id <id>.
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
With --json, each prints {"v": 3, "id": …} when delivered, or a refusal whose code says why: the
|
|
14
|
+
participant's, or usage, no-run, ambiguous, id, unreachable, no-engine, other-engine or
|
|
15
|
+
internal-error. Exits 0 when delivered, 1 when refused (the code says why), 2 on a usage error,
|
|
16
|
+
3 when the run could not be reached.
|
package/src/help/run.txt
CHANGED
|
@@ -2,35 +2,53 @@ usage: awf run [options] <workflow-file> [options] [-- workflow arguments...]
|
|
|
2
2
|
awf run [options] <workflow-file> --id <id> --continue
|
|
3
3
|
run options: --id <id>, --continue, --from-stage <stage>, --values <file>,
|
|
4
4
|
--timeout <duration>, --run-root <directory>, --cwd <directory>, --sandbox <file>,
|
|
5
|
+
--workspace <run|origin|name>, --keep-panes <never|on-failure|always>,
|
|
5
6
|
--json, --no-watch, --here
|
|
6
7
|
|
|
7
8
|
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
|
|
9
|
-
|
|
9
|
+
--continue, the run --id names, with the arguments, --cwd, --sandbox, --workspace and
|
|
10
|
+
--keep-panes it was started with. A continue reuses the stages that succeeded, up to the first
|
|
11
|
+
with no record or to --from-stage, and runs the rest.
|
|
10
12
|
A new run with --from-stage starts there. A stage before it with no record to reuse takes its
|
|
11
13
|
value from --values, a JSON object by stage name, checked by its result; one that returns
|
|
12
14
|
nothing is passed. Without one the attempt stops, naming the stage and its schema.
|
|
13
15
|
A run is kept in .awf/runs/<workflow>/<id> under --cwd, unless --run-root names another folder
|
|
14
|
-
for .awf/runs.
|
|
15
|
-
|
|
16
|
+
for .awf/runs. A workflow that gives file: import.meta.path keeps its runs in its file's project
|
|
17
|
+
unless --run-root names another folder: the nearest directory at or above the file holding .awf/,
|
|
18
|
+
else the file's own, so --continue finds a run a program started there from anywhere.
|
|
19
|
+
A run has no deadline unless --timeout gives it one, per attempt: a duration, a decimal and s, m,
|
|
20
|
+
h or d, such as 20m or 1d.
|
|
16
21
|
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
|
|
22
|
+
Either way the full result is kept as out.json in the run's folder, beside report.md
|
|
18
23
|
when the workflow writes one. A run that fails or is cancelled once its agents have started keeps
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
out.json too, with what it spent and why it ended; --json prints it. When no run comes of it,
|
|
25
|
+
--json prints a refusal whose code says why: usage, file, prepare, no-id, id, no-run,
|
|
26
|
+
other-args, not-started, unreachable, project, no-engine, other-engine or internal-error;
|
|
27
|
+
--here takes no --json, nor does opening a run of a workflow with events, which prints no
|
|
28
|
+
document. A second Ctrl-C stops awf at once, without it; a continue then reads
|
|
29
|
+
what its agents spent.
|
|
21
30
|
--cwd sets the directory the workflow and its agents work in; it defaults to the current one.
|
|
22
31
|
--sandbox puts every agent of the run in one sandbox, working in --cwd; the file is a JSON spec
|
|
23
32
|
such as {"read": ["/data/request.md"], "srt": {}}, naming srt or docker, whose relative paths are
|
|
24
33
|
from --cwd. A workflow that opens a sandbox of its own is refused.
|
|
25
34
|
A sandbox with its own Herdr, as a docker box has, gets a tab in the run's workspace showing its
|
|
26
35
|
panes; --no-watch leaves it out, and awf still prints the command that shows them.
|
|
36
|
+
--workspace says where each TUI agent's tab opens: run, the run's own Herdr workspace (the
|
|
37
|
+
default); origin, the one awf run was typed in; or any other word, the workspace with that label,
|
|
38
|
+
found or made. --keep-panes says whether an agent's pane stays once it is done: never (the
|
|
39
|
+
default), on-failure, when its last turn wasn't answered, or always. A kept pane's harness is left
|
|
40
|
+
running, and yours. Both are kept with the run for every attempt, a woken one too. A run an event
|
|
41
|
+
started has neither, and takes the defaults. A continue may give either again with the run's
|
|
42
|
+
value, or its default where the run had none, and no other.
|
|
27
43
|
--here, run by an agent in a Herdr pane, starts the run in a new tab and has it take that agent's
|
|
28
44
|
session over as one of its agents, from its next turn until the run ends. It prints a line for
|
|
29
45
|
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)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
are what prepare makes of none.
|
|
46
|
+
A run of a workflow with events is named by --id, or by the workflow's id(args); one without
|
|
47
|
+
starts may have an id made, as any run. awf run starts it with its arguments and no event,
|
|
48
|
+
detached in its project, as awf emit would, and the run waits at its first receive. A run already
|
|
49
|
+
there is only woken. A run an event starts has no argv: its args are what prepare makes of none.
|
|
50
|
+
Its --continue takes --from-stage, as any run's does, and no --values. A --from-stage the attempt
|
|
51
|
+
never reaches runs the attempt until it would park or return, and stops it there.
|
|
34
52
|
Exits 0 when the run completed or parked, 1 when it failed or timed out, 2 on a usage error or
|
|
35
53
|
when the run is refused, 3 when it stopped and can go on with --continue, and 128 plus the
|
|
36
54
|
signal's number when it was interrupted.
|
|
@@ -40,5 +58,6 @@ Examples:
|
|
|
40
58
|
awf run --timeout 20m examples/minimum-review/review-loop.ts
|
|
41
59
|
awf run examples/minimum-review/review-loop.ts -- packages/engine/src
|
|
42
60
|
awf run examples/minimum-review/review-loop.ts --cwd ../other-repo
|
|
61
|
+
awf run examples/pane-layout/workflow.ts --workspace origin --keep-panes on-failure
|
|
43
62
|
|
|
44
63
|
Workflow files are trusted code and run with your filesystem and process authority.
|
package/src/help/runs.txt
CHANGED
|
@@ -7,5 +7,7 @@ number. A run is <workflow>/<id>, or an <id> only one
|
|
|
7
7
|
workflow has. Runs are listed under --cwd's .awf/runs, or --run-root, as awf run keeps them; a
|
|
8
8
|
run named is found there first, then in each project awf has started a run with events in.
|
|
9
9
|
|
|
10
|
-
--json prints one document, the interface for scripts
|
|
11
|
-
|
|
10
|
+
--json prints one document, the interface for scripts: when it fails, a refusal whose code says
|
|
11
|
+
why: usage, no-run, ambiguous, id, unreachable, no-engine, other-engine or internal-error. Exits
|
|
12
|
+
0 on any read that worked, 1 when there is no such run, 2 on a usage error, 3 when the run's
|
|
13
|
+
files could not be read.
|
package/src/help/tick.txt
CHANGED
|
@@ -5,8 +5,13 @@ Wakes every run of a workflow with events under --cwd's .awf/runs, or --run-root
|
|
|
5
5
|
runs lists there, as an event would: continues a parked run whose wait's timer is due, and a
|
|
6
6
|
crashed or failed one until it has failed three times in a row. Runs of other workflows are left
|
|
7
7
|
as they are. --all wakes them in every project awf has started one in, as ~/.awf/projects lists
|
|
8
|
-
them.
|
|
8
|
+
them. A project's runs are woken by its own agentswf: one at another version than this awf runs
|
|
9
|
+
its own awf tick for them. Run it on any schedule; a run is continued once however many wakers
|
|
10
|
+
come at once.
|
|
9
11
|
|
|
10
|
-
Prints a line for each run it woke, and nothing when it woke none
|
|
11
|
-
|
|
12
|
-
usage
|
|
12
|
+
Prints a line for each run it woke, and nothing when it woke none, and one line on stderr
|
|
13
|
+
counting runs with events it can't read, which it doesn't wake; --json prints one document,
|
|
14
|
+
those runs under unreadable, on exit 1 too, or a refusal whose code says why it failed: usage or
|
|
15
|
+
internal-error. Exits 0; 1 when a project is gone, declares agentswf without installing it, or
|
|
16
|
+
its own agentswf failed, or a run could not be woken, the others still woken; 2 on a usage
|
|
17
|
+
error.
|
package/src/herdr-placement.ts
CHANGED
|
@@ -2,46 +2,16 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { mkdir, rename, rmdir, stat } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { HERDR_VERSION, type RunProcess } from "@agentswf/harness";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { isCode } from "./files";
|
|
6
|
+
import { parseSessions } from "./herdr-run-session";
|
|
7
7
|
import { machinePaths } from "./machine";
|
|
8
8
|
|
|
9
|
-
/** Where a layout may open a session awf does not otherwise use: one named `awf-…` it starts. */
|
|
10
|
-
const OWN_SESSION = /^awf(-|$)/;
|
|
11
9
|
const LOCK_POLL_MS = 100;
|
|
12
10
|
/** Held inside a session's queue: a short wait, then made anyway. */
|
|
13
11
|
const LOCK_WAIT_MS = 5_000;
|
|
14
12
|
/** A lock older than this was left by a run that died holding it. */
|
|
15
13
|
const LOCK_STALE_MS = 30_000;
|
|
16
14
|
|
|
17
|
-
/**
|
|
18
|
-
* Whether a session a layout names can be used: undefined when it can, else why not. One named
|
|
19
|
-
* `awf-…` is started, swept and restarted as the run session is; any other must be running. Either
|
|
20
|
-
* must answer, on the Herdr the adapter drives.
|
|
21
|
-
*/
|
|
22
|
-
export async function namedSession(
|
|
23
|
-
name: string,
|
|
24
|
-
deps: RunSessionDeps,
|
|
25
|
-
): Promise<string | undefined> {
|
|
26
|
-
if (OWN_SESSION.test(name)) {
|
|
27
|
-
try {
|
|
28
|
-
await ensureRunSession(name, deps);
|
|
29
|
-
} catch (error) {
|
|
30
|
-
return messageOf(error);
|
|
31
|
-
}
|
|
32
|
-
} else {
|
|
33
|
-
const listed = await deps.run({
|
|
34
|
-
argv: ["herdr", "session", "list", "--json"],
|
|
35
|
-
timeoutMs: 10_000,
|
|
36
|
-
});
|
|
37
|
-
const found = parseSessions(listed.stdout).find((session) => session.name === name);
|
|
38
|
-
if (!found?.running) {
|
|
39
|
-
return "it is not running, and awf starts only sessions named awf-…";
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return wrongVersion(name, deps.run);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
15
|
/** Why the session's server is not the Herdr the adapter drives; undefined when it is. */
|
|
46
16
|
async function wrongVersion(session: string, run: RunProcess): Promise<string | undefined> {
|
|
47
17
|
const status = await run({
|
|
@@ -154,7 +124,7 @@ export function workspaceLock(home: string) {
|
|
|
154
124
|
await mkdir(lock);
|
|
155
125
|
return () => rmdir(lock).catch(() => undefined);
|
|
156
126
|
} catch (error) {
|
|
157
|
-
if ((error
|
|
127
|
+
if (!isCode(error, "EEXIST")) throw error;
|
|
158
128
|
}
|
|
159
129
|
const made = await stat(lock).then(
|
|
160
130
|
(found) => found.mtimeMs,
|
package/src/herdr-run-session.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import { link, mkdir, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
4
3
|
import { dirname, join } from "node:path";
|
|
5
4
|
import type { RunProcess } from "@agentswf/harness";
|
|
6
5
|
import { messageOf } from "./errors";
|
|
6
|
+
import { linkNewText } from "./files";
|
|
7
7
|
import {
|
|
8
8
|
liveness,
|
|
9
9
|
type MarkedPane,
|
|
@@ -17,7 +17,7 @@ import { machinePaths } from "./machine";
|
|
|
17
17
|
import { SESSION_NAME } from "./pane-layout";
|
|
18
18
|
import type { ProcessProbe } from "./runs";
|
|
19
19
|
|
|
20
|
-
/** Where a run's
|
|
20
|
+
/** Where a run's TUI agents open when `AWF_HERDR_SESSION` names nowhere else. */
|
|
21
21
|
const DEFAULT_RUN_SESSION = "awf";
|
|
22
22
|
|
|
23
23
|
/** What may start: the shared session is awf's by its name, since which run created it is not knowable. */
|
|
@@ -53,7 +53,7 @@ const READY_POLL_MS = 200;
|
|
|
53
53
|
const READY_WITHIN_MS = 10_000;
|
|
54
54
|
|
|
55
55
|
/** Starts a server that outlives this process; settles once it has started, or could not. */
|
|
56
|
-
|
|
56
|
+
type StartServer = (
|
|
57
57
|
argv: readonly string[],
|
|
58
58
|
options: { cwd: string; env: Record<string, string> },
|
|
59
59
|
) => Promise<void>;
|
|
@@ -87,7 +87,7 @@ export type RunSession = {
|
|
|
87
87
|
kept?: KeptPane[];
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
type KeptPane = { run: string; session: string; paneId: string };
|
|
91
91
|
|
|
92
92
|
/** `AWF_HERDR_SESSION`, else `awf`; refused when Herdr could not use it as a session name. */
|
|
93
93
|
export function runSessionName(environment: Readonly<Record<string, string | undefined>>): string {
|
|
@@ -101,7 +101,7 @@ export function runSessionName(environment: Readonly<Record<string, string | und
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
|
-
* The Herdr session a run's
|
|
104
|
+
* The Herdr session a run's TUI agents open in, running and kept: started detached when down and
|
|
105
105
|
* named `awf` or `awf-…`, from a minimal environment and a quiet config, so it outlives this run
|
|
106
106
|
* for the next. Any other name that is down is refused: it may be an operator's stopped session,
|
|
107
107
|
* and bringing it back headless is not awf's call. Then the workspaces of runs that ended are
|
|
@@ -431,15 +431,7 @@ export function serverEnvironment(
|
|
|
431
431
|
async function quietConfig(home: string): Promise<string> {
|
|
432
432
|
const file = join(machinePaths(home).herdr, "config.toml");
|
|
433
433
|
await mkdir(dirname(file), { recursive: true });
|
|
434
|
-
|
|
435
|
-
try {
|
|
436
|
-
await writeFile(temporary, QUIET_CONFIG, { flag: "wx" });
|
|
437
|
-
await link(temporary, file);
|
|
438
|
-
} catch (error) {
|
|
439
|
-
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
440
|
-
} finally {
|
|
441
|
-
await unlink(temporary).catch(() => undefined);
|
|
442
|
-
}
|
|
434
|
+
await linkNewText(file, QUIET_CONFIG);
|
|
443
435
|
return file;
|
|
444
436
|
}
|
|
445
437
|
|