@agentswf/engine 0.1.0-beta.3 → 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.
Files changed (49) hide show
  1. package/package.json +5 -6
  2. package/src/agent-cli/cli.ts +571 -0
  3. package/src/agent-cli/client.ts +115 -0
  4. package/src/agent-launcher.ts +15 -25
  5. package/src/allowance-command.ts +3 -15
  6. package/src/attempt-output.ts +1 -1
  7. package/src/control-plane.ts +1 -1
  8. package/src/emit-command.ts +2 -13
  9. package/src/help/allowance.txt +13 -0
  10. package/src/help/emit.txt +13 -0
  11. package/src/help/inbox.txt +12 -0
  12. package/src/help/init.txt +12 -0
  13. package/src/help/logs.txt +13 -0
  14. package/src/help/reply.txt +18 -0
  15. package/src/help/run.txt +44 -0
  16. package/src/help/runs.txt +11 -0
  17. package/src/help/test.txt +9 -0
  18. package/src/help/tick.txt +12 -0
  19. package/src/here.ts +1 -1
  20. package/src/holder-command.ts +2 -20
  21. package/src/inbox-command.ts +2 -14
  22. package/src/init-command.ts +2 -11
  23. package/src/init-template/AGENTS.md.template +3 -3
  24. package/src/init-template/hello/workflow.ts.template +4 -4
  25. package/src/logs-command.ts +2 -15
  26. package/src/mailbox.ts +15 -2
  27. package/src/messaging/limits.ts +1 -1
  28. package/src/messaging/messaging.ts +22 -22
  29. package/src/messaging/routes.ts +1 -1
  30. package/src/messaging/text.ts +17 -17
  31. package/src/operation-liveness.ts +1 -1
  32. package/src/operator-runtime.ts +4 -7
  33. package/src/result-validation.ts +1 -1
  34. package/src/run-command.ts +2 -43
  35. package/src/run-usage.ts +1 -1
  36. package/src/runs-command.ts +2 -13
  37. package/src/sandboxes.ts +3 -2
  38. package/src/simple-input.ts +13 -3
  39. package/src/stage-ledger.ts +1 -1
  40. package/src/test-command.ts +2 -11
  41. package/src/testing.ts +1 -1
  42. package/src/tick-command.ts +2 -13
  43. package/src/tools.ts +16 -7
  44. package/src/workflow-runner.ts +85 -38
  45. package/src/workflow-testing/coordinator.typecheck.ts +12 -13
  46. package/src/workflow-testing/host.ts +8 -8
  47. package/src/workflow-testing/index.ts +1 -2
  48. package/src/workflow-testing/script.ts +4 -4
  49. package/src/workflow-testing/workflow-testing.typecheck.ts +2 -2
@@ -0,0 +1,115 @@
1
+ import { createConnection, type Socket } from "node:net";
2
+ import {
3
+ type ControlRequest,
4
+ decodeResponseFor,
5
+ type ResponseFor,
6
+ type ResultSubmitRequest,
7
+ type ResultSubmitResponse,
8
+ type WaitingRequest,
9
+ type WaitingResponse,
10
+ } from "@agentswf/contract/wire";
11
+
12
+ const MAX_RESPONSE_BYTES = 256 * 1024;
13
+
14
+ /** The connection never opened: nothing was written, so nothing reached the engine. */
15
+ export class NotSentError extends Error {}
16
+
17
+ export function submitResult(
18
+ endpoint: string,
19
+ request: ResultSubmitRequest,
20
+ timeoutSeconds = 30,
21
+ connect: (endpoint: string) => Socket = createConnection,
22
+ ): Promise<ResultSubmitResponse> {
23
+ return submitControl(endpoint, request, timeoutSeconds, connect);
24
+ }
25
+
26
+ export function submitWaiting(
27
+ endpoint: string,
28
+ request: WaitingRequest,
29
+ timeoutSeconds = 30,
30
+ connect: (endpoint: string) => Socket = createConnection,
31
+ ): Promise<WaitingResponse> {
32
+ return submitControl(endpoint, request, timeoutSeconds, connect);
33
+ }
34
+
35
+ /** Sends one request and resolves with a response that answers its command. */
36
+ export function submitControl<R extends ControlRequest>(
37
+ endpoint: string,
38
+ request: R,
39
+ /** Null waits as long as the engine takes, as a tool call does: it answers once the call settles. */
40
+ timeoutSeconds: number | null = 30,
41
+ connect: (endpoint: string) => Socket = createConnection,
42
+ ): Promise<ResponseFor[R["command"]]> {
43
+ type Response = ResponseFor[R["command"]];
44
+ if (timeoutSeconds !== null && (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)) {
45
+ return Promise.reject(new Error("control-plane timeout must be positive"));
46
+ }
47
+ const outgoing = Buffer.from(`${JSON.stringify(request)}\n`);
48
+
49
+ return new Promise<Response>((resolve, reject) => {
50
+ const chunks: Buffer[] = [];
51
+ let bytes = 0;
52
+ let settled = false;
53
+ let connected = false;
54
+ const socket = connect(endpoint);
55
+ const lifetime =
56
+ timeoutSeconds === null
57
+ ? undefined
58
+ : setTimeout(
59
+ () => fail(new Error("control plane response timed out")),
60
+ timeoutSeconds * 1000,
61
+ );
62
+
63
+ const fail = (error: Error): void => {
64
+ if (settled) return;
65
+ settled = true;
66
+ clearTimeout(lifetime);
67
+ socket.destroy();
68
+ reject(error);
69
+ };
70
+
71
+ socket.once("connect", () => {
72
+ connected = true;
73
+ socket.end(outgoing);
74
+ });
75
+ socket.on("data", (data) => {
76
+ if (settled) return;
77
+ const chunk = typeof data === "string" ? Buffer.from(data) : data;
78
+ bytes += chunk.byteLength;
79
+ if (bytes > MAX_RESPONSE_BYTES) {
80
+ fail(new Error("control-plane response exceeds the size limit"));
81
+ return;
82
+ }
83
+ chunks.push(chunk);
84
+ });
85
+ socket.once("end", () => {
86
+ if (settled) return;
87
+ const frame = Buffer.concat(chunks).toString("utf8");
88
+ if (!frame.endsWith("\n") || frame.slice(0, -1).includes("\n")) {
89
+ fail(new Error("control plane returned an invalid response frame"));
90
+ return;
91
+ }
92
+ let parsed: unknown;
93
+ try {
94
+ parsed = JSON.parse(frame.slice(0, -1));
95
+ } catch {
96
+ fail(new Error("control plane returned invalid JSON"));
97
+ return;
98
+ }
99
+ const decoded = decodeResponseFor<R["command"]>(request.command, parsed);
100
+ if (!decoded.ok) {
101
+ fail(new Error(decoded.error));
102
+ return;
103
+ }
104
+ settled = true;
105
+ clearTimeout(lifetime);
106
+ resolve(decoded.value);
107
+ });
108
+ socket.once("error", (error) =>
109
+ fail(connected ? error : new NotSentError(error.message, { cause: error })),
110
+ );
111
+ socket.once("close", () => {
112
+ if (!settled) fail(new Error("control plane closed without a response"));
113
+ });
114
+ });
115
+ }
@@ -1,9 +1,11 @@
1
- import { chmod, mkdir, readFile, realpath, writeFile } from "node:fs/promises";
2
- import { createRequire } from "node:module";
3
- import { basename, dirname, join, resolve } from "node:path";
1
+ import { chmod, mkdir, realpath, writeFile } from "node:fs/promises";
2
+ import { basename, dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
4
  import { type AgentDoor, shellQuote } from "@agentswf/sandbox";
5
5
 
6
- /** bun loads the `.env` of the directory it starts in: an agent's, which is none of `wf`'s. */
6
+ const AGENT_CLI = fileURLToPath(new URL("./agent-cli/cli.ts", import.meta.url));
7
+
8
+ /** bun loads the `.env` of the directory it starts in: an agent's, which is none of `wfa`'s. */
7
9
  const NO_ENV = "--no-env-file";
8
10
 
9
11
  /**
@@ -27,19 +29,18 @@ export async function installAgentLauncher(
27
29
  */
28
30
  sessionEnv?: string,
29
31
  ): Promise<string> {
30
- const command = await resolveAgentCommand();
31
- return writeLauncher(directory, [process.execPath, NO_ENV, command], endpoint, sessionEnv);
32
+ return writeLauncher(directory, [process.execPath, NO_ENV, AGENT_CLI], endpoint, sessionEnv);
32
33
  }
33
34
 
34
35
  /**
35
- * A sandboxed agent's door: the same launcher, running the bundled `wf` with the host's bun by
36
+ * A sandboxed agent's door: the same launcher, running the bundled `wfa` with the host's bun by
36
37
  * its real path, since a sandbox reads neither the workspace's sources nor bun's links under `~`.
37
38
  * A box copies `boxScript` to the launcher's path, which runs the bundle with the box's own bun.
38
39
  */
39
40
  export async function installSandboxedDoor(
40
41
  directory: string,
41
42
  endpoint: string,
42
- /** The bundled `wf`'s source, written beside the launcher. */
43
+ /** The bundled `wfa`'s source, written beside the launcher. */
43
44
  source: string,
44
45
  sessionEnv?: string,
45
46
  ): Promise<AgentDoor> {
@@ -50,7 +51,7 @@ export async function installSandboxedDoor(
50
51
  const socket = join(real, basename(endpoint));
51
52
  // In the agent's own directory: bun will not load a module from the control plane's, which is
52
53
  // not listable so that no agent can find another's socket.
53
- const bundle = join(real, "wf.js");
54
+ const bundle = join(real, "wfa.js");
54
55
  await writeFile(bundle, source, { mode: 0o500 });
55
56
  const launcher = await writeLauncher(real, [bun, NO_ENV, bundle], socket, sessionEnv);
56
57
  return {
@@ -64,12 +65,12 @@ export async function installSandboxedDoor(
64
65
  }
65
66
 
66
67
  /**
67
- * `wf` as one file's source, which a sandbox can read where it cannot read this workspace. Built by
68
+ * `wfa` as one file's source, which a sandbox can read where it cannot read this workspace. Built by
68
69
  * a `bun build` of its own: in process, the bundler resolved packages against the test runner's
69
70
  * state and missed the workspace's links.
70
71
  */
71
72
  export async function buildAgentBundle(): Promise<string> {
72
- const entry = await resolveAgentCommand();
73
+ const entry = AGENT_CLI;
73
74
  const built = Bun.spawn({
74
75
  cmd: [process.execPath, "build", entry, "--target=bun"],
75
76
  cwd: dirname(entry),
@@ -81,7 +82,7 @@ export async function buildAgentBundle(): Promise<string> {
81
82
  new Response(built.stdout).text(),
82
83
  new Response(built.stderr).text(),
83
84
  ]);
84
- if (code !== 0) throw new Error(`could not bundle wf: ${stderr.trim().slice(0, 400)}`);
85
+ if (code !== 0) throw new Error(`could not bundle wfa: ${stderr.trim().slice(0, 400)}`);
85
86
  return bundle;
86
87
  }
87
88
 
@@ -93,7 +94,7 @@ async function writeLauncher(
93
94
  ): Promise<string> {
94
95
  await mkdir(directory, { recursive: true });
95
96
  await chmod(directory, 0o700);
96
- const path = join(directory, "wf");
97
+ const path = join(directory, "wfa");
97
98
  // Not writable, so an agent that redirects into its own launcher fails rather than replacing its
98
99
  // only way to answer. A `chmod` still gets past it; a sandbox denies the write outright.
99
100
  await writeFile(path, launcherScript(command, endpoint, sessionEnv, path), { mode: 0o500 });
@@ -105,7 +106,7 @@ function launcherScript(
105
106
  command: readonly string[],
106
107
  endpoint: string,
107
108
  sessionEnv: string | undefined,
108
- /** Its own path, which `wf` names in what it prints, as an agent must type it. */
109
+ /** Its own path, which `wfa` names in what it prints, as an agent must type it. */
109
110
  path: string,
110
111
  ): string {
111
112
  if (sessionEnv !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(sessionEnv)) {
@@ -114,14 +115,3 @@ function launcherScript(
114
115
  const session = sessionEnv ? ` --session "\${${sessionEnv}:-}"` : "";
115
116
  return `#!/bin/sh\nexec ${command.map(shellQuote).join(" ")} --at ${shellQuote(endpoint)} --as ${shellQuote(path)}${session} "$@"\n`;
116
117
  }
117
-
118
- async function resolveAgentCommand(): Promise<string> {
119
- const require = createRequire(import.meta.url);
120
- const manifestPath = require.resolve("@agentswf/wf/package.json");
121
- const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as {
122
- bin?: string | Record<string, string>;
123
- };
124
- const relative = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.wf;
125
- if (!relative) throw new Error("@agentswf/wf does not publish the wf command");
126
- return resolve(dirname(manifestPath), relative);
127
- }
@@ -4,24 +4,12 @@ import type {
4
4
  HarnessAllowance,
5
5
  } from "@agentswf/contract/records";
6
6
  import { HARNESS_NAMES, type Harness } from "@agentswf/harness";
7
- import { PLAN_BASIS, planPrice } from "./accounting/plans";
7
+ import { planPrice } from "./accounting/plans";
8
8
  import { messageOf } from "./errors";
9
+ import help from "./help/allowance.txt" with { type: "text" };
9
10
  import type { Out } from "./run-commands";
10
11
 
11
- export const allowanceUsage = [
12
- "usage: awf allowance [harness...] [--json]",
13
- "",
14
- "What is left of each harness's plan, as the harness itself shows it: claude's /usage, codex's",
15
- "/status, and cursor's /usage, read in a Herdr pane. Nothing asks a model. Every harness when",
16
- `none is named: ${HARNESS_NAMES.join(", ")}.`,
17
- "",
18
- "A plan's monthly price is the vendor's list price as of the table's date, not what was",
19
- `charged: ${PLAN_BASIS}.`,
20
- "",
21
- `--json prints one document, the interface for scripts, with the plan as the`,
22
- "harness names it and no price; what it prints otherwise is for reading. A harness that could",
23
- "not be read says why. Exits 0 once every harness was asked, 2 on a usage error.",
24
- ].join("\n");
12
+ export const allowanceUsage = help.trimEnd();
25
13
 
26
14
  export type AllowanceCommand = { harnesses: Harness[]; json: boolean } | "help";
27
15
 
@@ -107,7 +107,7 @@ export async function keepRecords(
107
107
  const unacknowledged =
108
108
  ending.kind === "completed" && attempt.run.record.events
109
109
  ? await readMailbox(dir).then(
110
- // The event that started the run is `workflow.started`'s, taken or not.
110
+ // The event that started the run is `wf.started`'s, taken or not.
111
111
  (records) => {
112
112
  const state = foldMailbox(records);
113
113
  return state.unacknowledged.filter((id) => id !== state.started?.id).length;
@@ -21,7 +21,7 @@ export type MessagingHandler = Pick<
21
21
  "checkFinish" | "peers" | "send" | "reply" | "owing"
22
22
  >;
23
23
 
24
- /** `wf` accepts a 1 MiB value; escaped into the request's JSON it can double, plus the envelope. */
24
+ /** `wfa` accepts a 1 MiB value; escaped into the request's JSON it can double, plus the envelope. */
25
25
  const MAX_RESULT_REQUEST_BYTES = 2 * 1024 * 1024 + 4 * 1024;
26
26
  const MAX_CONTROL_CONNECTIONS = 64;
27
27
  const CONNECTION_TIMEOUT_SECONDS = 30;
@@ -2,23 +2,12 @@ import type { EmitResultJson } from "@agentswf/contract/records";
2
2
  import { isEventWorkflow, type JsonValue } from "@agentswf/contract/workflow";
3
3
  import { EmitError, emitEvent } from "./emit";
4
4
  import { messageOf } from "./errors";
5
+ import help from "./help/emit.txt" with { type: "text" };
5
6
  import type { Out } from "./run-commands";
6
7
  import type { Launch } from "./wake";
7
8
  import { loadWorkflowFile } from "./workflow-loader";
8
9
 
9
- export const emitUsage = [
10
- "usage: awf emit --target <file> --id <id> <event> [<json>] [--event-id <id>] [--json]",
11
- "",
12
- "Sends an event to the run of a workflow with events that --id names. That run starts if it has",
13
- "none and the event is one that starts one, and is woken if it has one: nudged while it runs, continued when it is",
14
- "parked or crashed. The run lives in the project of <file>, the nearest directory above it",
15
- "holding .awf/, whoever sends. --event-id names the occurrence: the same id again is a duplicate.",
16
- "",
17
- "Prints what became of the event, and what was done to the run: accepted, duplicate or refused.",
18
- "--json prints one document. Exits 0 for any of the three, 1 when the run's attempt could not be",
19
- "started (the event is kept, and the next event or awf tick tries again), 2 when the event is",
20
- "wrong for its target: its name, its data, or its id.",
21
- ].join("\n");
10
+ export const emitUsage = help.trimEnd();
22
11
 
23
12
  export async function emitCommand(
24
13
  argv: readonly string[],
@@ -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.
@@ -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
@@ -198,7 +198,7 @@ function passedSession(
198
198
  }
199
199
 
200
200
  /**
201
- * What lets a sandboxed session reach Herdr's socket, which `--here` and every `wf` call need: its
201
+ * What lets a sandboxed session reach Herdr's socket, which `--here` and every `wfa` call need: its
202
202
  * harness's own advice, for the harness whose session variable this shell has.
203
203
  */
204
204
  function sandboxFix(env: Readonly<Record<string, string | undefined>>): string {
@@ -6,6 +6,7 @@ import { deliver, eventIdProblem } from "./emit";
6
6
  import { printEmit } from "./emit-command";
7
7
  import { messageOf } from "./errors";
8
8
  import { isCode } from "./files";
9
+ import help from "./help/reply.txt" with { type: "text" };
9
10
  import { ItemFold } from "./inbox-command";
10
11
  import { coordinatorDir } from "./machine";
11
12
  import { failed, type Out, parsePlace, type ReadPlace } from "./run-commands";
@@ -13,26 +14,7 @@ import { findRunByRef, RunNotFound, type RunStatus } from "./run-state";
13
14
  import { readAttempts, readEvents, readNumberedEvents } from "./runs";
14
15
  import { exchange } from "./socket-exchange";
15
16
 
16
- export const replyUsage = [
17
- "usage: awf reply <run> <agent> [--to <id>] [--as <participant>] [<text>]",
18
- " awf send <run> <agent> [--as <participant>] [<text>]",
19
- " awf send <run> --as <participant> --event-id <id> [--author <name>] [<text>]",
20
- "each takes [--cwd <directory>] [--run-root <directory>] [--json]",
21
- "",
22
- "A run is <workflow>/<id>, or an <id> only one workflow has, here or in a project awf knows.",
23
- "Answers a question an agent asked a participant outside the run, or messages an agent first,",
24
- "over a route the workflow allowed. The text is the argument, or stdin without one. --to names",
25
- "the question, as awf inbox prints it; it may be left out when the agent has one open. --as",
26
- "names the participant when the workflow connects more than one. Neither waits for an answer:",
27
- "it comes back in awf inbox. With --event-id, send is a reply on a participant the run routes to its",
28
- "code: no agent, it reaches the run whether it runs or is parked, and the same id again is a",
29
- "duplicate. The id is one the run has not had from anyone, as a chat's message ids are. It prints",
30
- "as awf emit does, and exits 0 for whatever the run did with it (delivered, duplicate, or refused",
31
- "by a run that returned); 1 when the run or participant isn't found, or its attempt didn't start.",
32
- "",
33
- "Exits 0 when delivered, 1 when refused (the code says why), 2 on a usage error, 3 when the run",
34
- "could not be reached.",
35
- ].join("\n");
17
+ export const replyUsage = help.trimEnd();
36
18
 
37
19
  type Options = {
38
20
  command: "reply" | "send";
@@ -14,6 +14,7 @@ import { maskCredentials } from "@agentswf/harness";
14
14
  import { duration } from "./duration";
15
15
  import { messageOf } from "./errors";
16
16
  import { exists } from "./files";
17
+ import help from "./help/inbox.txt" with { type: "text" };
17
18
  import { readMailbox } from "./mailbox";
18
19
  import {
19
20
  clockTime,
@@ -34,20 +35,7 @@ import {
34
35
  readEventsFrom,
35
36
  } from "./runs";
36
37
 
37
- export const inboxUsage = [
38
- "usage: awf inbox [<run>] [--open] [--workflow <name>] [--as <key>] [--after <cursor>] [--wait <seconds>]",
39
- " [--cwd <directory>] [--run-root <directory>] [--json]",
40
- "",
41
- "What agents said to a participant outside the run, and what it said back: routes, messages,",
42
- "answers, questions that closed unanswered, and each attempt's end, oldest first. Without a run,",
43
- "every run here that connected one; --workflow narrows that. A run is <workflow>/<id>, or an <id>",
44
- "only one workflow has. --after reads on from a cursor the last answer gave; --wait",
45
- "waits up to that many seconds, at most 300, when nothing is new. --open lists the questions",
46
- "still waiting for an answer, a snapshot. --json prints one item per line.",
47
- "",
48
- "Exits 0 on a read that worked, 1 when there is no such run, it has no participant, or the",
49
- "cursor is another's, 2 on a usage error, 3 when the run's files could not be read.",
50
- ].join("\n");
38
+ export const inboxUsage = help.trimEnd();
51
39
 
52
40
  const MAX_WAIT_SECONDS = 300;
53
41
  const POLL_MS = 1_000;
@@ -2,19 +2,10 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, statSync, writeFileSync
2
2
  import { basename, dirname, join, relative, resolve } from "node:path";
3
3
  import manifest from "../package.json" with { type: "json" };
4
4
  import { messageOf } from "./errors";
5
+ import help from "./help/init.txt" with { type: "text" };
5
6
  import type { Out } from "./run-commands";
6
7
 
7
- export const initUsage = [
8
- "usage: awf init [dir] [--no-install]",
9
- "",
10
- "Sets up the current directory, or dir, for writing workflows: hello/workflow.ts, a workflow",
11
- "with one agent, and hello/workflow.test.ts, its test; package.json, tsconfig.json, .gitignore",
12
- "and AGENTS.md. Then runs bun install, unless --no-install, for editor types: awf runs a",
13
- "workflow against its own copies either way.",
14
- "",
15
- "Never overwrites. A file that exists is skipped, except that package.json gains the",
16
- "devDependencies and scripts it lacks and .gitignore the lines it lacks.",
17
- ].join("\n");
8
+ export const initUsage = help.trimEnd();
18
9
 
19
10
  export type InitCommand = { dir: string; install: boolean };
20
11
 
@@ -1,13 +1,13 @@
1
1
  # Workflows
2
2
 
3
- This folder holds [agents.wf](https://github.com/romanlv/agentswf) workflows: TypeScript files
3
+ This folder holds [agents.wf](https://agents.wf) workflows: TypeScript files
4
4
  that run coding agents, started with `awf`.
5
5
 
6
6
  - A workflow is a folder with a `workflow.ts`, which default-exports
7
7
  `defineWorkflow(…)` from `agentswf/workflow`, and a `workflow.test.ts` beside it.
8
8
  `hello/` is the smallest one; copy it to start another.
9
- - The API, with every option and outcome:
10
- https://github.com/romanlv/agentswf/blob/main/docs/workflow-api.md
9
+ - The API, with every option and outcome, as Markdown:
10
+ https://agents.wf/reference/workflow-api.md. Every other page is in https://agents.wf/llms.txt.
11
11
  - `awf test` runs the tests with each agent replaced by scripted answers: no agent starts, and it
12
12
  costs nothing. After a change, run it and `bun run check`, which typechecks.
13
13
  - `awf run hello/workflow.ts` starts real agents, on the operator's accounts. Don't run a workflow
@@ -1,14 +1,14 @@
1
- import { defineWorkflow, isAnswered } from "agentswf/workflow";
1
+ import { defineWorkflow } from "agentswf/workflow";
2
2
  import Type from "typebox";
3
3
 
4
4
  export const ANSWER = Type.Object({ answer: Type.Integer() }, { additionalProperties: false });
5
5
 
6
6
  export default defineWorkflow({
7
7
  meta: { name: "hello", description: "Ask one agent a question with a known answer." },
8
- async run(workflow) {
8
+ async run(wf) {
9
9
  // A headless agent needs neither Herdr nor a sandbox. Swap in any harness and model your
10
10
  // account has: "claude" in a pane, the default placement, runs on a subscription.
11
- const agent = await workflow.agents.open({
11
+ const agent = await wf.agents.open({
12
12
  key: "hello",
13
13
  runtime: { harness: "codex", model: "gpt-6-luna", placement: "headless" },
14
14
  });
@@ -17,6 +17,6 @@ export default defineWorkflow({
17
17
  schema: ANSWER,
18
18
  timeoutMs: 3 * 60_000,
19
19
  });
20
- return isAnswered(outcome) ? outcome.value : { unanswered: outcome.reason };
20
+ return outcome.answered ? outcome.value : { unanswered: outcome.reason };
21
21
  },
22
22
  });