@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
@@ -3,6 +3,7 @@ import type { LogEventJson, LogsJson, RouteEnds, RunLogLine } from "@agentswf/co
3
3
  import type { JsonObject } from "@agentswf/contract/workflow";
4
4
  import { duration } from "./duration";
5
5
  import { messageOf } from "./errors";
6
+ import help from "./help/logs.txt" with { type: "text" };
6
7
  import {
7
8
  clockTime,
8
9
  command,
@@ -34,21 +35,7 @@ import {
34
35
  import { findRun, findRunByRef, type RunStatus } from "./run-state";
35
36
  import { describeActivity } from "./run-view";
36
37
 
37
- export const logsUsage = [
38
- "usage: awf logs [<run>] [@<handle>] [--agent <key>] [--stage <name>] [--after <cursor>]",
39
- " [--limit <n>] [--wait <seconds> | --follow] [--full] [--cwd <directory>]",
40
- " [--run-root <directory>] [--json]",
41
- "",
42
- "What a run did and is doing: its events and what its agents did, read from their own session",
43
- "files, merged by time. With no run, the newest here. Without --after, the last 40 events; with",
44
- "it, what came after, oldest first. Every answer ends with the command that gets only what is",
45
- "new. --wait: with nothing to show yet, wait for the first event, at most that many seconds, up",
46
- "to 300; --follow prints each event as it comes until the run ends. @<handle> names one event,",
47
- "and --full prints its text, with common credential shapes masked; --json carries it always.",
48
- "",
49
- "Exits 0 on any read that worked, 1 when there is no such run or the cursor or handle does not",
50
- "fit it, 2 on a usage error, 3 when the run's files could not be read.",
51
- ].join("\n");
38
+ export const logsUsage = help.trimEnd();
52
39
 
53
40
  const DEFAULT_LIMIT = 40;
54
41
  /**
package/src/mailbox.ts CHANGED
@@ -26,7 +26,19 @@ export function mailboxFile(runDir: string): string {
26
26
  /** The run's mailbox in the order written, less a line a crash tore and kinds this awf doesn't know. */
27
27
  export async function readMailbox(runDir: string): Promise<MailboxRecord[]> {
28
28
  const lines = await readLines<MailboxRecord>(mailboxFile(runDir));
29
- return lines.filter((line) => line.version === MAILBOX_RECORD_VERSION && KINDS.has(line.kind));
29
+ return lines
30
+ .filter((line) => line.version === MAILBOX_RECORD_VERSION && KINDS.has(line.kind))
31
+ .map(withAnswered);
32
+ }
33
+
34
+ /** A turn's end written before 0.1.0-beta.4 has no `answered`, which a workflow would read as false. */
35
+ function withAnswered(line: MailboxRecord): MailboxRecord {
36
+ if (line.kind !== "event" || line.name !== "agent:ended") return line;
37
+ const end = line.data as { outcome?: { kind: string; answered?: boolean } };
38
+ if (end?.outcome === undefined || end.outcome.answered !== undefined) return line;
39
+ const answered = end.outcome.kind === "answered";
40
+ const data = { ...end, outcome: { ...end.outcome, answered } } as unknown as JsonValue;
41
+ return { ...line, data };
30
42
  }
31
43
 
32
44
  const KINDS = new Set<string>([
@@ -137,6 +149,7 @@ export async function recoverTurns(
137
149
  turn: turn.turn,
138
150
  meta: turn.meta,
139
151
  outcome: {
152
+ answered: false,
140
153
  kind: "failed",
141
154
  reason: `attempt ${turn.attempt} ended before this turn did; its work, if any, is in the agent's session`,
142
155
  retryable: true,
@@ -972,6 +985,6 @@ function checked(options: ReceiveOptions | undefined): {
972
985
  throw new Error("receive's label is a string");
973
986
  if (take !== "all" && take !== "oldest") throw new Error('receive\'s take is "all" or "oldest"');
974
987
  if ("until" in (options ?? {}))
975
- throw new Error("receive has no until: a timer, workflow.timer(name, when), wakes it");
988
+ throw new Error("receive has no until: a timer, wf.timer(name, when), wakes it");
976
989
  return { ...(label === undefined ? {} : { label }), take };
977
990
  }
@@ -13,7 +13,7 @@ export const MESSAGE_LIMITS: MessageLimits = {
13
13
  /**
14
14
  * Where an asker may wait for an answer, by harness and placement: on where M2 passed
15
15
  * (`docs/findings/messaging-m2.md`), off elsewhere. A pane that is `confirmed` waits only where it
16
- * confirms what it was sent, as `wf waiting` needs. The fake harness always may. cursor is off:
16
+ * confirms what it was sent, as `wfa waiting` needs. The fake harness always may. cursor is off:
17
17
  * its turns ran past their deadline in M2.
18
18
  */
19
19
  export const AWAIT_REPLY: Readonly<Record<string, { headless?: true; pane?: true | "confirmed" }>> =
@@ -47,7 +47,7 @@ export interface MessagingHost {
47
47
  agent(key: AgentKey): AgentView | undefined;
48
48
  /** Whether it may `--await-reply` where it runs. */
49
49
  canAwait(key: AgentKey): boolean;
50
- /** The launcher it runs `wf` through. */
50
+ /** The launcher it runs `wfa` through. */
51
51
  launcher(key: AgentKey): string;
52
52
  /** Something is new for this agent: messages for it, or an end to its wait. */
53
53
  changed(key: AgentKey): void;
@@ -207,7 +207,7 @@ type Operation = OperationInfo & {
207
207
  /**
208
208
  * The messaging logic of one attempt: routes, admission, inboxes and owed answers. It decides every
209
209
  * case the design lists and writes the record; it starts nothing. The runner opens and continues
210
- * operations when it says, and the control plane hands it `wf`'s commands.
210
+ * operations when it says, and the control plane hands it `wfa`'s commands.
211
211
  */
212
212
  export class Messaging {
213
213
  readonly limits: MessageLimits;
@@ -702,8 +702,8 @@ export class Messaging {
702
702
  }
703
703
 
704
704
  /**
705
- * A refusal of `wf result` or `wf waiting` given this call id, or the acknowledgement of a
706
- * `wf waiting` while an answer is awaited; null when the command goes on as before.
705
+ * A refusal of `wfa result` or `wfa waiting` given this call id, or the acknowledgement of a
706
+ * `wfa waiting` while an answer is awaited; null when the command goes on as before.
707
707
  */
708
708
  checkFinish(
709
709
  agent: AgentKey,
@@ -715,7 +715,7 @@ export class Messaging {
715
715
  | null {
716
716
  const operation = this.#operations.get(id);
717
717
  if (!operation || operation.agent !== agent) return null;
718
- const wf = this.#host.launcher(agent);
718
+ const wfa = this.#host.launcher(agent);
719
719
  // A message operation's call takes no result, ended or not.
720
720
  if (operation.kind === "message") {
721
721
  return {
@@ -733,8 +733,8 @@ export class Messaging {
733
733
  code: "owes-reply",
734
734
  error:
735
735
  command === "result"
736
- ? `you owe ${peer} a reply. Send it with ${wf} reply ${peer}, then run ${wf} result ${id} again.`
737
- : `you owe ${peer} a reply; send it with ${wf} reply ${peer} first.`,
736
+ ? `you owe ${peer} a reply. Send it with ${wfa} reply ${peer}, then run ${wfa} result ${id} again.`
737
+ : `you owe ${peer} a reply; send it with ${wfa} reply ${peer} first.`,
738
738
  };
739
739
  }
740
740
  const waited = operation.awaits;
@@ -743,7 +743,7 @@ export class Messaging {
743
743
  kind: "awaiting",
744
744
  peer: waited.owedBy,
745
745
  ...(waited.until === undefined ? {} : { until: waited.until }),
746
- notice: `you already await ${waited.owedBy}'s reply${waited.until === undefined ? "" : `, until ${clock(waited.until, this.#host.now())}`}. End your turn, or finish with ${wf} result ${id}.`,
746
+ notice: `you already await ${waited.owedBy}'s reply${waited.until === undefined ? "" : `, until ${clock(waited.until, this.#host.now())}`}. End your turn, or finish with ${wfa} result ${id}.`,
747
747
  };
748
748
  }
749
749
  return null;
@@ -815,7 +815,7 @@ export class Messaging {
815
815
  ):
816
816
  | { kind: "ok"; operation: Operation; route: Route; bytes: number }
817
817
  | { kind: "refused"; code: SendCode; error: string } {
818
- const wf = this.#host.launcher(from);
818
+ const wfa = this.#host.launcher(from);
819
819
  const operation = this.#current.get(from);
820
820
  if (!operation) return refuse("no-operation", "this agent has no open operation.");
821
821
  const { peer } = request;
@@ -826,14 +826,14 @@ export class Messaging {
826
826
  return refuse(
827
827
  "no-route",
828
828
  owes
829
- ? `your route with ${peer} only answers its requests. Answer it with ${wf} reply ${peer}.`
829
+ ? `your route with ${peer} only answers its requests. Answer it with ${wfa} reply ${peer}.`
830
830
  : `your route with ${peer} only answers its requests, and it asked nothing. Nothing to send; end your turn.`,
831
831
  );
832
832
  };
833
833
  if (initiating.length === 0) {
834
834
  return this.#open(peer, from)
835
835
  ? replyOnly()
836
- : refuse("no-route", `no route to ${peer}. Your routes: ${wf} peers`);
836
+ : refuse("no-route", `no route to ${peer}. Your routes: ${wfa} peers`);
837
837
  }
838
838
  const agent = this.#outside.has(peer) ? undefined : this.#host.agent(peer);
839
839
  if (agent?.state === "stopped")
@@ -889,7 +889,7 @@ export class Messaging {
889
889
  ):
890
890
  | { kind: "ok"; operation: Operation; owed: Obligation; bytes: number }
891
891
  | { kind: "refused"; code: ReplyCode; error: string } {
892
- const wf = this.#host.launcher(from);
892
+ const wfa = this.#host.launcher(from);
893
893
  const operation = this.#current.get(from);
894
894
  if (!operation) return refuse("no-operation", "this agent has no open operation.");
895
895
  const { peer } = request;
@@ -898,7 +898,7 @@ export class Messaging {
898
898
  (each) => each.asker === peer && each.owedBy === from && each.request.shown,
899
899
  );
900
900
  if (!latest || latest.state === "replied") {
901
- return refuse("none-owed", `you owe ${peer} nothing. Send it with ${wf} send ${peer}.`);
901
+ return refuse("none-owed", `you owe ${peer} nothing. Send it with ${wfa} send ${peer}.`);
902
902
  }
903
903
  if (latest.state !== "open" || latest.owingOperation !== operation) {
904
904
  const agent = this.#host.agent(peer);
@@ -906,7 +906,7 @@ export class Messaging {
906
906
  "expired",
907
907
  agent?.state === "stopped"
908
908
  ? `${peer} has stopped${agent.reason ? ` (${JSON.stringify(agent.reason)})` : ""}; nothing reaches it now.`
909
- : `${peer} stopped waiting at ${clock(latest.endedAt ?? this.#host.now(), this.#host.now())}. Send it with ${wf} send ${peer}.`,
909
+ : `${peer} stopped waiting at ${clock(latest.endedAt ?? this.#host.now(), this.#host.now())}. Send it with ${wfa} send ${peer}.`,
910
910
  );
911
911
  }
912
912
  const route = latest.request.route;
@@ -950,7 +950,7 @@ export class Messaging {
950
950
  /** The question a reply answers, which counts as answered here. */
951
951
  answering?: Obligation,
952
952
  ): { kind: "refused"; code: "awaiting" | "unsupported" | "cycle"; error: string } | undefined {
953
- const wf = this.#host.launcher(from);
953
+ const wfa = this.#host.launcher(from);
954
954
  if (operation.awaits?.state === "open") {
955
955
  return refuse(
956
956
  "awaiting",
@@ -984,7 +984,7 @@ export class Messaging {
984
984
  return refuse(
985
985
  "cycle",
986
986
  owing
987
- ? `${who}, and you owe ${last === peer ? "it" : last} a reply. Answer with ${wf} reply ${last}; it may --await-reply.`
987
+ ? `${who}, and you owe ${last === peer ? "it" : last} a reply. Answer with ${wfa} reply ${last}; it may --await-reply.`
988
988
  : `${who}. Send without --await-reply and end your turn; ${last}'s request reaches you then.`,
989
989
  );
990
990
  }
@@ -1095,7 +1095,7 @@ export class Messaging {
1095
1095
  }
1096
1096
 
1097
1097
  /**
1098
- * Durable before `wf send` returns: a failed write rejects, and `wf` says it may have been
1098
+ * Durable before `wfa send` returns: a failed write rejects, and `wfa` says it may have been
1099
1099
  * queued. An outside participant's message says what it came through instead of an operation.
1100
1100
  * A question to one carries when it closes unanswered, if it has a limit.
1101
1101
  */
@@ -1134,13 +1134,13 @@ export class Messaging {
1134
1134
  }
1135
1135
 
1136
1136
  #awaitNotice(operation: Operation, { until }: { until?: number }): string {
1137
- const wf = this.#host.launcher(operation.agent);
1137
+ const wfa = this.#host.launcher(operation.agent);
1138
1138
  return [
1139
1139
  `Its reply starts your next turn (${until === undefined ? "no deadline" : `deadline ${clock(until, this.#host.now())}`}).`,
1140
1140
  "Go on with work that does not need the answer, then end your turn with one line saying what you await; do not poll or sleep.",
1141
1141
  ...(operation.kind === "workflow"
1142
1142
  ? [
1143
- `If you find the answer first, or decide not to wait, finish with ${wf} result ${operation.id}; the wait is dropped.`,
1143
+ `If you find the answer first, or decide not to wait, finish with ${wfa} result ${operation.id}; the wait is dropped.`,
1144
1144
  ]
1145
1145
  : []),
1146
1146
  ].join("\n");
@@ -1323,7 +1323,7 @@ export class Messaging {
1323
1323
  /**
1324
1324
  * What is held for an operation whose turn has ended, deciding nothing: `continues` when its turn
1325
1325
  * end would go on with it, `held` when messages wait for a later operation or to cut a check-in's
1326
- * `wf waiting` short, `none` otherwise.
1326
+ * `wfa waiting` short, `none` otherwise.
1327
1327
  */
1328
1328
  pending(id: string): Pending {
1329
1329
  const operation = this.#operations.get(id);
@@ -1405,7 +1405,7 @@ export class Messaging {
1405
1405
  };
1406
1406
  }
1407
1407
 
1408
- /** The line `wf` leads with while this agent's operation owes or awaits an answer. */
1408
+ /** The line `wfa` leads with while this agent's operation owes or awaits an answer. */
1409
1409
  owing(agent: AgentKey): string | undefined {
1410
1410
  const { owes, awaits } = this.#standing(agent);
1411
1411
  return owingLine(owes, awaits, this.#host.launcher(agent), this.#host.now());
@@ -1476,7 +1476,7 @@ export class Messaging {
1476
1476
 
1477
1477
  /**
1478
1478
  * This agent's open routes, either end, in the order granted; with `stopped`, also those a peer's
1479
- * stop closed, so `wf peers` can say it stopped.
1479
+ * stop closed, so `wfa peers` can say it stopped.
1480
1480
  */
1481
1481
  #routesOf(agent: AgentKey, stopped = false): Route[] {
1482
1482
  return [...new Set([...this.#routes.values()].flat())]
@@ -29,7 +29,7 @@ export type Route = {
29
29
  open: boolean;
30
30
  /** Why it closed. */
31
31
  closed?: "stage-ended" | "agent-stopped" | "released" | "attempt-ended";
32
- /** Its place among the attempt's grants, which `wf peers` lists in. */
32
+ /** Its place among the attempt's grants, which `wfa peers` lists in. */
33
33
  readonly order: number;
34
34
  };
35
35
 
@@ -4,7 +4,7 @@ import type { AgentKey } from "@agentswf/contract/workflow";
4
4
 
5
5
  /**
6
6
  * Everything an agent reads about messaging. The engine writes it all, since only it knows the
7
- * launcher's path, the routes' purposes and what closes a cycle; `wf` prints it as given. `wf`
7
+ * launcher's path, the routes' purposes and what closes a cycle; `wfa` prints it as given. `wfa`
8
8
  * here is always the launcher's full path: the command is not on the agent's PATH.
9
9
  */
10
10
 
@@ -13,7 +13,7 @@ export type Recipient = {
13
13
  launcher: string;
14
14
  /** The operation's call id. */
15
15
  callId: string;
16
- /** A workflow's own operation finishes with `wf result`; a message operation ends its turn. */
16
+ /** A workflow's own operation finishes with `wfa result`; a message operation ends its turn. */
17
17
  kind: "workflow" | "message";
18
18
  };
19
19
 
@@ -92,7 +92,7 @@ export function envelope(
92
92
  } = {},
93
93
  ): string {
94
94
  const tag = options.tag ?? nonce();
95
- const wf = to.launcher;
95
+ const wfa = to.launcher;
96
96
  const blocks: string[] = [];
97
97
  const owed = new Set<AgentKey>();
98
98
  for (const item of items) {
@@ -104,7 +104,7 @@ export function envelope(
104
104
  if (item.answers) {
105
105
  blocks.push(
106
106
  [
107
- `The reply you were waiting for, from ${item.from}${item.asks ? `; it asks something back, so you owe it a reply: answer with ${wf} reply ${item.from}` : ""}.`,
107
+ `The reply you were waiting for, from ${item.from}${item.asks ? `; it asks something back, so you owe it a reply: answer with ${wfa} reply ${item.from}` : ""}.`,
108
108
  "",
109
109
  fenced("reply", item.from, item.text, tag),
110
110
  ].join("\n"),
@@ -114,13 +114,13 @@ export function envelope(
114
114
  const stop = /[.!?]$/.test(item.purpose) ? "" : ".";
115
115
  const head = `Message from ${item.from}, on the route ${quoted(item.purpose)}${stop}`;
116
116
  const lead = options.opens ? ` This is a message, not a new task (call ${to.callId}).` : "";
117
- const asks = item.asks ? ` A reply is required: answer with ${wf} reply ${item.from}.` : "";
117
+ const asks = item.asks ? ` A reply is required: answer with ${wfa} reply ${item.from}.` : "";
118
118
  blocks.push(
119
119
  [`${head}${lead}${asks}`, "", fenced("message", item.from, item.text, tag)].join("\n"),
120
120
  );
121
121
  }
122
122
  const lines = [blocks.join("\n\n")];
123
- if (options.announced?.length) lines.push(announcement(options.announced, wf));
123
+ if (options.announced?.length) lines.push(announcement(options.announced, wfa));
124
124
  const plain = items.some((item) => item.kind === "message" && !item.answers);
125
125
  if (plain) lines.push(GUARD);
126
126
  lines.push(next(items, to, owed));
@@ -129,7 +129,7 @@ export function envelope(
129
129
 
130
130
  /** The closing line: whether the task goes on, and how it ends. */
131
131
  function next(items: readonly Delivered[], to: Recipient, owed: ReadonlySet<AgentKey>): string {
132
- const wf = to.launcher;
132
+ const wfa = to.launcher;
133
133
  const failed = items.some((item) => item.kind === "no-reply");
134
134
  const answered = items.some((item) => item.kind === "message" && item.answers);
135
135
  if (to.kind === "message") {
@@ -140,9 +140,9 @@ function next(items: readonly Delivered[], to: Recipient, owed: ReadonlySet<Agen
140
140
  return `Your work for call ${to.callId} continues; end your turn when it is done.`;
141
141
  return `No reply is required; end your turn once you have acted on it ${call}.`;
142
142
  }
143
- const finish = `finish it with ${wf} result ${to.callId}`;
143
+ const finish = `finish it with ${wfa} result ${to.callId}`;
144
144
  if (owed.size > 0) return `Your task ${to.callId} goes on: once you have replied, ${finish}.`;
145
- if (failed) return `Continue without it, or finish with ${wf} result ${to.callId}.`;
145
+ if (failed) return `Continue without it, or finish with ${wfa} result ${to.callId}.`;
146
146
  return `Your task ${to.callId} continues; ${finish}.`;
147
147
  }
148
148
 
@@ -162,12 +162,12 @@ function routeLine({ peer, can, purpose }: RouteLine): string {
162
162
  /** What an operation's prompt says about the agent's routes, once, in a few lines. */
163
163
  export function routeLesson(routes: readonly RouteLine[], launcher: string): string {
164
164
  if (routes.length === 0) return "";
165
- const wf = launcher;
165
+ const wfa = launcher;
166
166
  return [
167
167
  "You can message other agents on these routes:",
168
168
  ...routes.map(routeLine),
169
- `See them with ${wf} peers. Send with ${wf} send <peer> <<'EOF' ... EOF. To ask and wait for the answer, send with ${wf} send --await-reply <peer>; the answer starts your next turn, so end your turn then, and do not poll or sleep.`,
170
- `Answer a question with ${wf} reply <peer> <<'EOF' ... EOF; only a reply counts as the answer.`,
169
+ `See them with ${wfa} peers. Send with ${wfa} send <peer> <<'EOF' ... EOF. To ask and wait for the answer, send with ${wfa} send --await-reply <peer>; the answer starts your next turn, so end your turn then, and do not poll or sleep.`,
170
+ `Answer a question with ${wfa} reply <peer> <<'EOF' ... EOF; only a reply counts as the answer.`,
171
171
  ].join("\n");
172
172
  }
173
173
 
@@ -181,14 +181,14 @@ function announcement(routes: readonly RouteLine[], launcher: string): string {
181
181
 
182
182
  /** The one reminder a turn that ends owing gets. */
183
183
  export function reminder(owed: readonly AgentKey[], to: Recipient): string {
184
- const wf = to.launcher;
184
+ const wfa = to.launcher;
185
185
  const who = owed.join(" and ");
186
- const how = owed.map((peer) => `${wf} reply ${peer}`).join(", then ");
187
- const then = to.kind === "workflow" ? `then ${wf} result ${to.callId}` : "then end your turn";
186
+ const how = owed.map((peer) => `${wfa} reply ${peer}`).join(", then ");
187
+ const then = to.kind === "workflow" ? `then ${wfa} result ${to.callId}` : "then end your turn";
188
188
  return `You owe ${who} a reply; send it with ${how}, ${then}.`;
189
189
  }
190
190
 
191
- /** The line every `wf` command leads with while the operation owes or awaits an answer. */
191
+ /** The line every `wfa` command leads with while the operation owes or awaits an answer. */
192
192
  export function owingLine(
193
193
  owes: readonly AgentKey[],
194
194
  awaits: { peer: AgentKey; until?: number } | undefined,
@@ -207,7 +207,7 @@ export function owingLine(
207
207
  return parts.length === 0 ? undefined : `${parts.join("; ")}.`;
208
208
  }
209
209
 
210
- /** `wf peers`'s table, the line about what is owed, and nothing for empty cells. */
210
+ /** `wfa peers`'s table, the line about what is owed, and nothing for empty cells. */
211
211
  export function peersTable(
212
212
  peers: readonly PeerView[],
213
213
  awaits: { peer: AgentKey; until?: number } | undefined,
@@ -506,7 +506,7 @@ export async function superviseOperation(options: Options): Promise<SupervisedOu
506
506
  deliveryUntil === undefined &&
507
507
  receiptUntil === undefined;
508
508
  const messages = options.messages;
509
- // Before any check-in: what continues it goes now, cutting a `wf waiting` short, and an
509
+ // Before any check-in: what continues it goes now, cutting a `wfa waiting` short, and an
510
510
  // operation awaiting an answer waits for it with no check-in.
511
511
  if (messages && settledTurn) {
512
512
  if (messages.pending() === "continues") {
@@ -56,7 +56,7 @@ export type OperatorRuntimeInstallation = {
56
56
  sandboxes?: SandboxProviders;
57
57
  /** The decision models workflows may ask, and their aliases. */
58
58
  decisions?: DecisionInstallation;
59
- /** How `workflow.allowance` reads a harness's plan, as `awf allowance` does. */
59
+ /** How `wf.allowance` reads a harness's plan, as `awf allowance` does. */
60
60
  allowance?: (harness: Harness) => Promise<HarnessAllowance>;
61
61
  cleanup(): Promise<void>;
62
62
  };
@@ -181,7 +181,7 @@ export async function installOperatorRuntime(
181
181
  sandboxes: await installSandboxes(environment),
182
182
  decisions: installDecisions({ OPENROUTER_API_KEY: await openRouterKey(environment) }),
183
183
  allowance: allowanceReader(environment, run, undefined, home),
184
- // Nothing to undo: the agent's `wf` is a launcher the control plane installs beside its own
184
+ // Nothing to undo: the agent's `wfa` is a launcher the control plane installs beside its own
185
185
  // socket, and the control plane removes both when the run closes.
186
186
  cleanup: async () => undefined,
187
187
  };
@@ -282,9 +282,8 @@ export function herdrConfig(session: string): HerdrConfig {
282
282
 
283
283
  /**
284
284
  * The sandbox providers this machine has: srt when its CLI is on `PATH`, docker when its CLI is;
285
- * its daemon is first asked when a sandbox opens. srt is the default when installed, as it runs
286
- * the project's own toolchain, which an image lacks (story 004), and when it is there but cannot
287
- * be set up: a spec naming none then fails saying why, rather than moving to docker unsaid.
285
+ * its daemon is first asked when a sandbox opens. There is no default: a spec names its provider,
286
+ * since the two give agents different machines, the host's toolchain or an image.
288
287
  */
289
288
  export async function installSandboxes(
290
289
  environment: Readonly<Record<string, string | undefined>>,
@@ -304,10 +303,8 @@ export async function installSandboxes(
304
303
  ...(srt ? { srt: createSrtProvider(srt) } : {}),
305
304
  ...(docker ? { docker: createDockerProvider(docker) } : {}),
306
305
  };
307
- const fallback = srt || unavailable.srt ? "srt" : docker ? "docker" : undefined;
308
306
  return {
309
307
  installed,
310
- ...(fallback ? { default: fallback } : {}),
311
308
  ...(Object.keys(unavailable).length > 0 ? { unavailable } : {}),
312
309
  };
313
310
  }
@@ -8,7 +8,7 @@ export type CandidateEvaluation =
8
8
  | { kind: "rejected"; error: string };
9
9
 
10
10
  /**
11
- * A result as `wf` sent it, validated without repairing or persisting; the result-slot transition
11
+ * A result as `wfa` sent it, validated without repairing or persisting; the result-slot transition
12
12
  * owns acceptance.
13
13
  */
14
14
  export async function evaluateGiven(
@@ -3,52 +3,11 @@ import { resolve } from "node:path";
3
3
  import type { JsonValue } from "@agentswf/contract/workflow";
4
4
  import { parseDuration } from "./duration";
5
5
  import { messageOf } from "./errors";
6
+ import help from "./help/run.txt" with { type: "text" };
6
7
  import { SESSION_CODE } from "./here";
7
8
  import { idProblem, runRootOf, stageNameProblem } from "./runs";
8
9
 
9
- export const usage = [
10
- "usage: awf run [options] <workflow-file> [options] [-- workflow arguments...]",
11
- " awf run [options] <workflow-file> --id <id> --continue",
12
- "run options: --id <id>, --continue, --from-stage <stage>, --values <file>,",
13
- " --timeout <duration>, --run-root <directory>, --cwd <directory>, --sandbox <file>,",
14
- " --json, --no-watch, --here",
15
- "",
16
- "Each awf run is an attempt of a run: a new run, with --id's id or a generated one, or, with",
17
- "--continue, the run --id names, with the arguments, --cwd and --sandbox it was started with. A continue reuses",
18
- "the stages that succeeded, up to the first with no record or to --from-stage, and runs the rest.",
19
- "A new run with --from-stage starts there. A stage before it with no record to reuse takes its",
20
- "value from --values, a JSON object by stage name, checked by its result; one that returns",
21
- "nothing is passed. Without one the attempt stops, naming the stage and its schema.",
22
- "A stop exits 3. A run is kept in .awf/runs/<workflow>/<id> under --cwd, unless --run-root names",
23
- "another folder for .awf/runs.",
24
- "A run has no deadline unless --timeout gives it one, per attempt.",
25
- "A workflow that knows how to present its result prints that; --json prints the full result instead.",
26
- "Either way the full result is kept as output.json in the run's folder, beside report.md",
27
- "when the workflow writes one. A run that fails or is cancelled once its agents have started keeps",
28
- "output.json too, with what it spent and why it ended; --json prints it. A second Ctrl-C stops",
29
- "awf at once, without it.",
30
- "--cwd sets the directory the workflow and its agents work in; it defaults to the current one.",
31
- "--sandbox puts every agent of the run in one sandbox, working in --cwd; the file is a JSON spec",
32
- 'such as {"read": ["/data/request.md"], "srt": {}}, whose relative paths are from --cwd. A',
33
- "workflow that opens a sandbox of its own is refused.",
34
- "A sandbox with its own Herdr, as a docker box has, gets a tab in the run's workspace showing its",
35
- "panes; --no-watch leaves it out, and awf still prints the command that shows them.",
36
- "--here, run by an agent in a Herdr pane, starts the run in a new tab and has it take that agent's",
37
- "session over as one of its agents, from its next turn until the run ends. It prints a line for",
38
- "the agent to end its turn with, which is how the run finds the session.",
39
- "A run of a workflow with events is named by --id, or by the workflow's id(args). awf run starts it",
40
- "with its arguments and no event, detached in its project, as awf emit would, and the run waits at",
41
- "its first receive. A run already there is only woken. A run an event starts has no argv: its args",
42
- "are what prepare makes of none.",
43
- "",
44
- "Examples:",
45
- " awf run examples/minimum-review/review-loop.ts",
46
- " awf run --timeout 20m examples/minimum-review/review-loop.ts",
47
- " awf run examples/minimum-review/review-loop.ts -- packages/engine/src",
48
- " awf run examples/minimum-review/review-loop.ts --cwd ../other-repo",
49
- "",
50
- "Workflow files are trusted code and run with your filesystem and process authority.",
51
- ].join("\n");
10
+ export const usage = help.trimEnd();
52
11
 
53
12
  export type RunCommand = {
54
13
  /** Where paths typed on the command line resolve. */
package/src/run-usage.ts CHANGED
@@ -31,7 +31,7 @@ export type AccountedAgent = {
31
31
  cwd: string;
32
32
  /** A sandboxed agent's own harness home, where its sessions are. */
33
33
  home?: string;
34
- /** Every native session id seen for the agent so far, from its `wf` calls and its adapter. */
34
+ /** Every native session id seen for the agent so far, from its `wfa` calls and its adapter. */
35
35
  sessions(): readonly string[];
36
36
  /** When its host first prompted it, where that waits on the agent first; see `window`. */
37
37
  promptedAt?(): number | undefined;
@@ -2,6 +2,7 @@ import { homedir } from "node:os";
2
2
  import type { RunJson, RunsJson } from "@agentswf/contract/records";
3
3
  import { duration } from "./duration";
4
4
  import { messageOf } from "./errors";
5
+ import help from "./help/runs.txt" with { type: "text" };
5
6
  import {
6
7
  clockTime,
7
8
  command,
@@ -23,19 +24,7 @@ import { encodeCursor } from "./run-read";
23
24
  import { findRunByRef, listRuns, type RunStatus, runStatus } from "./run-state";
24
25
  import { type AgentView, describeActivity, type RunView, viewRun } from "./run-view";
25
26
 
26
- export const runsUsage = [
27
- "usage: awf runs [--limit <n>] [--cwd <directory>] [--run-root <directory>] [--json]",
28
- " awf runs <run> [--cwd <directory>] [--run-root <directory>] [--json]",
29
- "",
30
- "The runs here, newest first, or one run in depth: its attempts, the stages of the last, what",
31
- "each agent is doing, and what to run next. The list shows 20 unless --limit says another",
32
- "number. A run is <workflow>/<id>, or an <id> only one",
33
- "workflow has. Runs are listed under --cwd's .awf/runs, or --run-root, as awf run keeps them; a",
34
- "run named is found there first, then in each project awf has started a run with events in.",
35
- "",
36
- "--json prints one document, the interface for scripts. Exits 0 on any read that worked, 1 when",
37
- "there is no such run, 2 on a usage error, 3 when the run's files could not be read.",
38
- ].join("\n");
27
+ export const runsUsage = help.trimEnd();
39
28
 
40
29
  const LISTED = 20;
41
30
 
package/src/sandboxes.ts CHANGED
@@ -7,6 +7,7 @@ import type { SandboxRecord } from "@agentswf/contract/records";
7
7
  import type {
8
8
  AbsoluteDeadline,
9
9
  AgentExecution,
10
+ SandboxEnvironment,
10
11
  SandboxEnvironmentKey,
11
12
  SandboxRef,
12
13
  } from "@agentswf/contract/workflow";
@@ -141,7 +142,7 @@ export class RunSandboxes {
141
142
  return this.options.sandboxes?.run !== undefined;
142
143
  }
143
144
 
144
- /** `workflow.sandboxes.open`. */
145
+ /** `wf.sandboxes.open`. */
145
146
  async open(spec: unknown): Promise<SandboxRef> {
146
147
  if (this.hasRunSandbox) throw new Error(RUN_SANDBOX_ONLY);
147
148
  const key = (spec as { key?: unknown } | null)?.key;
@@ -306,7 +307,7 @@ export class RunSandboxes {
306
307
  network: [...resolved.network],
307
308
  ...(resolved.sockets.length > 0 ? { sockets: [...resolved.sockets] } : {}),
308
309
  ...(resolved.webSearch ? { webSearch: true } : {}),
309
- [provider]: resolved.environment,
310
+ ...({ [provider]: resolved.environment } as SandboxEnvironment),
310
311
  },
311
312
  directory,
312
313
  gitdirs: resolved.gitdirs.map((gitdir) => ({ ...gitdir })),
@@ -19,10 +19,18 @@ type Kind =
19
19
  | { type: "boolean" }
20
20
  | { type: "enum"; values: string[] };
21
21
 
22
- /** Said once in a prompt, under the first block taught with flags, and with every refusal of them. */
22
+ /**
23
+ * Said once in a prompt, under the first block with a free-text flag, and with every refusal of
24
+ * flags that take text. Enum, number and boolean values are shell words as they stand.
25
+ */
23
26
  export const QUOTING_RULE =
24
27
  "Put every value in single quotes; a ' inside a value is written '\\''.";
25
28
 
29
+ /** Whether a flag takes free text, the only value the shell can break. */
30
+ export function takesText(simple: Simple): boolean {
31
+ return simple.some(({ kind }) => kind.type === "string");
32
+ }
33
+
26
34
  const MAX_PROPERTIES = 6;
27
35
  const NAME = /^[a-z][a-z0-9_]*$/;
28
36
  /** An enum value the usage line can show between `|`s and the agent can type as it stands. */
@@ -137,7 +145,7 @@ export type ReadFlags = { ok: true; value: JsonObject } | { ok: false; error: st
137
145
  export type ReadGiven = { ok: true; value: unknown } | { ok: false; error: string };
138
146
 
139
147
  /**
140
- * An input as `wf` sent it, as a value: JSON parsed, flags read by the schema, nothing as `{}`.
148
+ * An input as `wfa` sent it, as a value: JSON parsed, flags read by the schema, nothing as `{}`.
141
149
  * The schema's own check comes after, in `checkGiven`.
142
150
  */
143
151
  export function readGiven(schema: JsonSchema | undefined, given: GivenInput): ReadGiven {
@@ -181,7 +189,9 @@ export function checkGiven(
181
189
  }
182
190
 
183
191
  function expected(simple: Simple): string {
184
- return `expected: ${usageLine(simple)}\n${QUOTING_RULE}`;
192
+ return [`expected: ${usageLine(simple)}`, ...(takesText(simple) ? [QUOTING_RULE] : [])].join(
193
+ "\n",
194
+ );
185
195
  }
186
196
 
187
197
  /**
@@ -174,7 +174,7 @@ export class StageLedger {
174
174
  }
175
175
 
176
176
  /**
177
- * `workflow.stop`, called in `stage` or between stages: what to throw. The first stop is kept,
177
+ * `wf.stop`, called in `stage` or between stages: what to throw. The first stop is kept,
178
178
  * and any later stop throws it again; once it was found caught, that instead. After the
179
179
  * workflow's body ended, a stop ends nothing.
180
180
  */
@@ -4,21 +4,12 @@ import { resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { parseDuration } from "./duration";
6
6
  import { messageOf } from "./errors";
7
+ import help from "./help/test.txt" with { type: "text" };
7
8
  import type { Out } from "./run-commands";
8
9
 
9
10
  const PRELOAD = fileURLToPath(new URL("./workflow-test-preload.ts", import.meta.url));
10
11
 
11
- export const testUsage = [
12
- "usage: awf test [paths...] [-t <pattern>] [--watch] [--timeout <duration>]",
13
- "",
14
- "Runs a workflow's tests (*.test.ts and *.spec.ts files) under the current directory, or under",
15
- "each file or directory given, with agentswf/workflow, agentswf/testing and typebox served as",
16
- "awf run serves them: nothing is installed. -t runs the tests whose names match the pattern;",
17
- "--watch reruns them on a change; --timeout bounds each test, 5s by default.",
18
- "",
19
- "Exits 0 when every test passed, 1 when one failed or none was found, 2 on a usage error. What",
20
- "it prints is for reading, not parsing.",
21
- ].join("\n");
12
+ export const testUsage = help.trimEnd();
22
13
 
23
14
  export type TestCommand = { paths: string[]; bunArgs: string[] };
24
15