@agentswf/engine 0.1.0-beta.3 → 0.1.0-beta.5

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 (72) hide show
  1. package/package.json +5 -6
  2. package/src/agent-cli/cli.ts +611 -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-ending.ts +2 -4
  7. package/src/attempt-output.ts +3 -2
  8. package/src/attempt-view.ts +1 -1
  9. package/src/control-plane.ts +38 -6
  10. package/src/coordinator-socket.ts +15 -6
  11. package/src/deadlines.ts +69 -2
  12. package/src/decisions/directory.ts +9 -11
  13. package/src/deprecations.ts +177 -0
  14. package/src/duration.ts +50 -20
  15. package/src/emit-command.ts +57 -54
  16. package/src/emit.ts +124 -15
  17. package/src/help/allowance.txt +13 -0
  18. package/src/help/emit.txt +21 -0
  19. package/src/help/inbox.txt +12 -0
  20. package/src/help/init.txt +12 -0
  21. package/src/help/logs.txt +14 -0
  22. package/src/help/reply.txt +18 -0
  23. package/src/help/run.txt +58 -0
  24. package/src/help/runs.txt +11 -0
  25. package/src/help/test.txt +9 -0
  26. package/src/help/tick.txt +12 -0
  27. package/src/herdr-run-session.ts +2 -2
  28. package/src/here.ts +1 -1
  29. package/src/holder-command.ts +38 -51
  30. package/src/inbox-command.ts +9 -24
  31. package/src/init-command.ts +2 -11
  32. package/src/init-template/AGENTS.md.template +3 -3
  33. package/src/init-template/hello/workflow.test.ts.template +3 -3
  34. package/src/init-template/hello/workflow.ts.template +6 -6
  35. package/src/keep-alive.ts +28 -13
  36. package/src/logs-command.ts +14 -27
  37. package/src/mailbox.ts +16 -21
  38. package/src/messaging/limits.ts +5 -5
  39. package/src/messaging/messaging.ts +48 -43
  40. package/src/messaging/routes.ts +2 -2
  41. package/src/messaging/text.ts +24 -22
  42. package/src/operation-liveness.ts +1 -1
  43. package/src/operator-cli.ts +23 -9
  44. package/src/operator-runtime.ts +9 -12
  45. package/src/pane-layout.ts +125 -32
  46. package/src/progress-view.ts +8 -8
  47. package/src/result-validation.ts +1 -1
  48. package/src/run-command.ts +27 -47
  49. package/src/run-commands.ts +1 -1
  50. package/src/run-prepare.ts +46 -8
  51. package/src/run-usage.ts +2 -2
  52. package/src/run-view.ts +9 -4
  53. package/src/runs-command.ts +14 -23
  54. package/src/runs.ts +44 -8
  55. package/src/sandboxes.ts +3 -2
  56. package/src/simple-input.ts +13 -3
  57. package/src/sources.ts +42 -6
  58. package/src/stage-ledger.ts +43 -13
  59. package/src/stage-plan.ts +37 -1
  60. package/src/test-command.ts +9 -14
  61. package/src/testing.ts +8 -9
  62. package/src/tick-command.ts +2 -13
  63. package/src/tools.ts +62 -25
  64. package/src/workflow-loader.ts +5 -2
  65. package/src/workflow-runner.ts +636 -271
  66. package/src/workflow-testing/coordinator.typecheck.ts +12 -13
  67. package/src/workflow-testing/coordinators.ts +2 -2
  68. package/src/workflow-testing/host.ts +115 -46
  69. package/src/workflow-testing/index.ts +45 -837
  70. package/src/workflow-testing/run-test.ts +1213 -0
  71. package/src/workflow-testing/script.ts +106 -45
  72. package/src/workflow-testing/workflow-testing.typecheck.ts +13 -10
@@ -1,8 +1,9 @@
1
1
  import { homedir } from "node:os";
2
2
  import type { LogEventJson, LogsJson, RouteEnds, RunLogLine } from "@agentswf/contract/records";
3
3
  import type { JsonObject } from "@agentswf/contract/workflow";
4
- import { duration } from "./duration";
4
+ import { formatDuration, parseWait } 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
  /**
@@ -98,7 +85,7 @@ export async function logsCommand(
98
85
  const parsed = parsePlace(argv, shellCwd);
99
86
  place = parsed.place;
100
87
  json = parsed.json;
101
- options = parseLogs(parsed.rest);
88
+ options = parseLogs(parsed.rest, (line) => out.stderr(line));
102
89
  if (options.follow && json) throw new Error("--follow is for reading; --json takes --wait");
103
90
  } catch (error) {
104
91
  out.stderr(`awf: ${messageOf(error)}\n\n${logsUsage}`);
@@ -135,7 +122,7 @@ export async function logsCommand(
135
122
  }
136
123
  }
137
124
 
138
- function parseLogs(rest: readonly string[]): LogsOptions {
125
+ function parseLogs(rest: readonly string[], deprecated: (line: string) => void): LogsOptions {
139
126
  const options: LogsOptions = {
140
127
  limit: DEFAULT_LIMIT,
141
128
  waitSeconds: 0,
@@ -160,11 +147,7 @@ function parseLogs(rest: readonly string[]): LogsOptions {
160
147
  throw new Error("--limit needs a whole number");
161
148
  options.limit = limit;
162
149
  } else if (arg === "--wait") {
163
- const seconds = Number(value());
164
- if (!Number.isFinite(seconds) || seconds < 0 || seconds > MAX_WAIT_SECONDS) {
165
- throw new Error(`--wait needs seconds from 0 to ${MAX_WAIT_SECONDS}`);
166
- }
167
- options.waitSeconds = seconds;
150
+ options.waitSeconds = parseWait(value(), MAX_WAIT_SECONDS, deprecated);
168
151
  waitGiven = true;
169
152
  } else if (arg === "--follow") options.follow = true;
170
153
  else if (arg === "--full") options.full = true;
@@ -524,7 +507,7 @@ function trailer(
524
507
  return `run interrupted${go ? ` · continue: ${go}` : ""}`;
525
508
  }
526
509
  const { ended, started } = span(status, Date.now());
527
- const took = ended === undefined ? "" : ` in ${duration(ended - started)}`;
510
+ const took = ended === undefined ? "" : ` in ${formatDuration(ended - started)}`;
528
511
  return `run ended: ${status.state}${took} · result: ${shellLine(command(place, "awf", "runs", status.ref))}`;
529
512
  }
530
513
 
@@ -582,7 +565,7 @@ function describeLine(line: RunLogLine, places: Places): string {
582
565
  case "compact": {
583
566
  const took =
584
567
  line.deliveredAt && line.settledAt
585
- ? ` ${duration(Date.parse(line.settledAt) - Date.parse(line.deliveredAt))}`
568
+ ? ` ${formatDuration(Date.parse(line.settledAt) - Date.parse(line.deliveredAt))}`
586
569
  : "";
587
570
  return `${operation(line.kind)} ended ${line.outcome}${took}`;
588
571
  }
@@ -604,7 +587,9 @@ function describeLine(line: RunLogLine, places: Places): string {
604
587
  return `decision ${line.decision} ${line.outcome}${line.error ? ` ${said(line.error)}` : ""}`;
605
588
  case "route-opened":
606
589
  return `route ${routeEnds(line)} opened "${said(line.purpose)}" ${line.maxMessages} messages${
607
- line.timeoutMs === undefined ? "" : `, a question closes after ${duration(line.timeoutMs)}`
590
+ line.timeoutMs === undefined
591
+ ? ""
592
+ : `, a question closes after ${formatDuration(line.timeoutMs)}`
608
593
  }`;
609
594
  case "route-closed":
610
595
  return `route ${routeEnds(line)} closed ${line.reason}`;
@@ -653,7 +638,9 @@ function describeLine(line: RunLogLine, places: Places): string {
653
638
  line.tool,
654
639
  places.calls.get(line.call)?.input,
655
640
  // Most calls take under a second; a duration says something only past one.
656
- line.durationMs < 1000 ? line.outcome : `${line.outcome} ${duration(line.durationMs)}`,
641
+ line.durationMs < 1000
642
+ ? line.outcome
643
+ : `${line.outcome} ${formatDuration(line.durationMs)}`,
657
644
  answered,
658
645
  );
659
646
  }
package/src/mailbox.ts CHANGED
@@ -11,7 +11,6 @@ import {
11
11
  AWF_ID_PREFIX,
12
12
  type JsonValue,
13
13
  type OperationRecord,
14
- type ReceiveOptions,
15
14
  type TurnOutcome,
16
15
  type WorkflowEvent,
17
16
  } from "@agentswf/contract/workflow";
@@ -26,7 +25,19 @@ export function mailboxFile(runDir: string): string {
26
25
  /** The run's mailbox in the order written, less a line a crash tore and kinds this awf doesn't know. */
27
26
  export async function readMailbox(runDir: string): Promise<MailboxRecord[]> {
28
27
  const lines = await readLines<MailboxRecord>(mailboxFile(runDir));
29
- return lines.filter((line) => line.version === MAILBOX_RECORD_VERSION && KINDS.has(line.kind));
28
+ return lines
29
+ .filter((line) => line.version === MAILBOX_RECORD_VERSION && KINDS.has(line.kind))
30
+ .map(withAnswered);
31
+ }
32
+
33
+ /** A turn's end written before 0.1.0-beta.4 has no `answered`, which a workflow would read as false. */
34
+ function withAnswered(line: MailboxRecord): MailboxRecord {
35
+ if (line.kind !== "event" || line.name !== "agent:ended") return line;
36
+ const end = line.data as { outcome?: { kind: string; answered?: boolean } };
37
+ if (end?.outcome === undefined || end.outcome.answered !== undefined) return line;
38
+ const answered = end.outcome.kind === "answered";
39
+ const data = { ...end, outcome: { ...end.outcome, answered } } as unknown as JsonValue;
40
+ return { ...line, data };
30
41
  }
31
42
 
32
43
  const KINDS = new Set<string>([
@@ -137,6 +148,7 @@ export async function recoverTurns(
137
148
  turn: turn.turn,
138
149
  meta: turn.meta,
139
150
  outcome: {
151
+ answered: false,
140
152
  kind: "failed",
141
153
  reason: `attempt ${turn.attempt} ended before this turn did; its work, if any, is in the agent's session`,
142
154
  retryable: true,
@@ -614,12 +626,11 @@ export class RunMailbox {
614
626
  * nothing.
615
627
  */
616
628
  async receive(
617
- options: ReceiveOptions | undefined,
629
+ label: string | undefined,
618
630
  stage?: string,
619
631
  /** Rejects the wait: the run ended, or the scope it waits in was cancelled. */
620
632
  signal?: AbortSignal,
621
633
  ): Promise<WorkflowEvent<string, JsonValue>[]> {
622
- const { label, take } = checked(options);
623
634
  if (stage !== undefined) {
624
635
  const batch = await this.batchOf(stage);
625
636
  if (batch) return batch;
@@ -632,7 +643,7 @@ export class RunMailbox {
632
643
  return this.#wait(
633
644
  {
634
645
  ...(label === undefined ? {} : { label }),
635
- take,
646
+ take: "all",
636
647
  ...(stage === undefined ? {} : { stage }),
637
648
  },
638
649
  signal,
@@ -959,19 +970,3 @@ async function mailboxSize(runDir: string): Promise<number> {
959
970
  () => 0,
960
971
  );
961
972
  }
962
-
963
- /** A wait's options as given, checked: what a typed caller can't get wrong, a plain one can. */
964
- function checked(options: ReceiveOptions | undefined): {
965
- label?: string;
966
- take: "all" | "oldest";
967
- } {
968
- if (options !== undefined && (typeof options !== "object" || options === null))
969
- throw new Error("receive takes { label?, take? }");
970
- const { label, take = "all" } = options ?? {};
971
- if (label !== undefined && typeof label !== "string")
972
- throw new Error("receive's label is a string");
973
- if (take !== "all" && take !== "oldest") throw new Error('receive\'s take is "all" or "oldest"');
974
- if ("until" in (options ?? {}))
975
- throw new Error("receive has no until: a timer, workflow.timer(name, when), wakes it");
976
- return { ...(label === undefined ? {} : { label }), take };
977
- }
@@ -12,14 +12,14 @@ export const MESSAGE_LIMITS: MessageLimits = {
12
12
 
13
13
  /**
14
14
  * Where an asker may wait for an answer, by harness and placement: on where M2 passed
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:
15
+ * (`docs/findings/messaging-m2.md`), off elsewhere. A TUI that is `confirmed` waits only where it
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
- export const AWAIT_REPLY: Readonly<Record<string, { headless?: true; pane?: true | "confirmed" }>> =
19
+ export const AWAIT_REPLY: Readonly<Record<string, { headless?: true; tui?: true | "confirmed" }>> =
20
20
  {
21
- fake: { headless: true, pane: true },
22
- claude: { headless: true, pane: "confirmed" },
21
+ fake: { headless: true, tui: true },
22
+ claude: { headless: true, tui: "confirmed" },
23
23
  codex: { headless: true },
24
24
  pi: { headless: true },
25
25
  };
@@ -9,7 +9,7 @@ import type {
9
9
  import type { ParticipantCode, PeerView, ReplyCode, SendCode } from "@agentswf/contract/wire";
10
10
  import type { AgentKey, JsonObject } from "@agentswf/contract/workflow";
11
11
  import type { AuthoredMessage } from "@agentswf/harness/adapter";
12
- import { duration } from "../duration";
12
+ import { formatDuration } from "../duration";
13
13
  import { MESSAGE_LIMITS } from "./limits";
14
14
  import {
15
15
  directions,
@@ -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;
@@ -180,7 +180,7 @@ type Obligation = {
180
180
  /** It has a `--timeout` of its own, which only that fails it at. */
181
181
  readonly timed: boolean;
182
182
  /**
183
- * When it closes unanswered, on an outside participant: the route's `timeoutMs` or the asker's
183
+ * When it closes unanswered: the route's `timeoutMs` or the asker's
184
184
  * shorter `--timeout`, and which of the two.
185
185
  */
186
186
  readonly closes?: { at: number; by: "route" | "own"; ms: number };
@@ -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;
@@ -279,15 +279,13 @@ export class Messaging {
279
279
  }
280
280
  }
281
281
  const timeoutMs = options.timeoutMs;
282
- if (timeoutMs !== undefined) {
283
- if (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1)
284
- throw new Error("a route's timeoutMs is a positive whole number of milliseconds");
285
- // Only where an agent can ask an outside participant: nothing else needs it yet.
286
- if (!pairs.some(([, to]) => this.#outside.has(to)))
287
- throw new Error(
288
- "timeoutMs is only for a route an agent asks an outside participant over: between them, or to it",
289
- );
290
- }
282
+ if (
283
+ timeoutMs !== undefined &&
284
+ (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1)
285
+ )
286
+ throw new Error(
287
+ `a route's timeoutMs is a positive whole number of milliseconds: not ${timeoutMs}`,
288
+ );
291
289
  const open = pairs.map(([from, to]) => this.#open(from, to));
292
290
  if (open.some(Boolean)) {
293
291
  if (
@@ -361,7 +359,7 @@ export class Messaging {
361
359
  });
362
360
  }
363
361
 
364
- /** Writes `text` to a participant routed to the code, without a turn: an inbox item for it. */
362
+ /** Writes `text` to a participant, without a turn: an inbox item for it. */
365
363
  async post(key: AgentKey, text: string): Promise<string> {
366
364
  if (this.#closed) throw new Error("the attempt has ended; nothing more is posted");
367
365
  if (!this.#outside.has(key)) throw new Error(`${key} is not connected`);
@@ -506,7 +504,7 @@ export class Messaging {
506
504
  return holderRefusal("no-route", `the workflow allows no route from ${from} to ${agent}.`);
507
505
  if (view.state === "stopped")
508
506
  return holderRefusal(
509
- "stopped",
507
+ "peer-stopped",
510
508
  `${agent} has stopped${view.reason ? ` (${JSON.stringify(view.reason)})` : ""}.`,
511
509
  );
512
510
  const route = initiating.find((each) => each.open);
@@ -702,8 +700,8 @@ export class Messaging {
702
700
  }
703
701
 
704
702
  /**
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.
703
+ * A refusal of `wfa result` or `wfa waiting` given this call id, or the acknowledgement of a
704
+ * `wfa waiting` while an answer is awaited; null when the command goes on as before.
707
705
  */
708
706
  checkFinish(
709
707
  agent: AgentKey,
@@ -715,7 +713,7 @@ export class Messaging {
715
713
  | null {
716
714
  const operation = this.#operations.get(id);
717
715
  if (!operation || operation.agent !== agent) return null;
718
- const wf = this.#host.launcher(agent);
716
+ const wfa = this.#host.launcher(agent);
719
717
  // A message operation's call takes no result, ended or not.
720
718
  if (operation.kind === "message") {
721
719
  return {
@@ -733,8 +731,8 @@ export class Messaging {
733
731
  code: "owes-reply",
734
732
  error:
735
733
  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.`,
734
+ ? `you owe ${peer} a reply. Send it with ${wfa} reply ${peer}, then run ${wfa} result ${id} again.`
735
+ : `you owe ${peer} a reply; send it with ${wfa} reply ${peer} first.`,
738
736
  };
739
737
  }
740
738
  const waited = operation.awaits;
@@ -743,7 +741,7 @@ export class Messaging {
743
741
  kind: "awaiting",
744
742
  peer: waited.owedBy,
745
743
  ...(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}.`,
744
+ 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
745
  };
748
746
  }
749
747
  return null;
@@ -815,7 +813,7 @@ export class Messaging {
815
813
  ):
816
814
  | { kind: "ok"; operation: Operation; route: Route; bytes: number }
817
815
  | { kind: "refused"; code: SendCode; error: string } {
818
- const wf = this.#host.launcher(from);
816
+ const wfa = this.#host.launcher(from);
819
817
  const operation = this.#current.get(from);
820
818
  if (!operation) return refuse("no-operation", "this agent has no open operation.");
821
819
  const { peer } = request;
@@ -826,14 +824,14 @@ export class Messaging {
826
824
  return refuse(
827
825
  "no-route",
828
826
  owes
829
- ? `your route with ${peer} only answers its requests. Answer it with ${wf} reply ${peer}.`
827
+ ? `your route with ${peer} only answers its requests. Answer it with ${wfa} reply ${peer}.`
830
828
  : `your route with ${peer} only answers its requests, and it asked nothing. Nothing to send; end your turn.`,
831
829
  );
832
830
  };
833
831
  if (initiating.length === 0) {
834
832
  return this.#open(peer, from)
835
833
  ? replyOnly()
836
- : refuse("no-route", `no route to ${peer}. Your routes: ${wf} peers`);
834
+ : refuse("no-route", `no route to ${peer}. Your routes: ${wfa} peers`);
837
835
  }
838
836
  const agent = this.#outside.has(peer) ? undefined : this.#host.agent(peer);
839
837
  if (agent?.state === "stopped")
@@ -889,7 +887,7 @@ export class Messaging {
889
887
  ):
890
888
  | { kind: "ok"; operation: Operation; owed: Obligation; bytes: number }
891
889
  | { kind: "refused"; code: ReplyCode; error: string } {
892
- const wf = this.#host.launcher(from);
890
+ const wfa = this.#host.launcher(from);
893
891
  const operation = this.#current.get(from);
894
892
  if (!operation) return refuse("no-operation", "this agent has no open operation.");
895
893
  const { peer } = request;
@@ -898,7 +896,7 @@ export class Messaging {
898
896
  (each) => each.asker === peer && each.owedBy === from && each.request.shown,
899
897
  );
900
898
  if (!latest || latest.state === "replied") {
901
- return refuse("none-owed", `you owe ${peer} nothing. Send it with ${wf} send ${peer}.`);
899
+ return refuse("none-owed", `you owe ${peer} nothing. Send it with ${wfa} send ${peer}.`);
902
900
  }
903
901
  if (latest.state !== "open" || latest.owingOperation !== operation) {
904
902
  const agent = this.#host.agent(peer);
@@ -906,7 +904,7 @@ export class Messaging {
906
904
  "expired",
907
905
  agent?.state === "stopped"
908
906
  ? `${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}.`,
907
+ : `${peer} stopped waiting at ${clock(latest.endedAt ?? this.#host.now(), this.#host.now())}. Send it with ${wfa} send ${peer}.`,
910
908
  );
911
909
  }
912
910
  const route = latest.request.route;
@@ -942,18 +940,20 @@ export class Messaging {
942
940
  return { kind: "ok", operation, owed: latest, bytes };
943
941
  }
944
942
 
945
- /** `awaiting`, `unsupported` and `cycle`, checked for any question. */
943
+ /** `already-awaiting`, `unsupported` and `cycle`, checked for any question. */
946
944
  #checkAsk(
947
945
  from: AgentKey,
948
946
  peer: AgentKey,
949
947
  operation: Operation,
950
948
  /** The question a reply answers, which counts as answered here. */
951
949
  answering?: Obligation,
952
- ): { kind: "refused"; code: "awaiting" | "unsupported" | "cycle"; error: string } | undefined {
953
- const wf = this.#host.launcher(from);
950
+ ):
951
+ | { kind: "refused"; code: "already-awaiting" | "unsupported" | "cycle"; error: string }
952
+ | undefined {
953
+ const wfa = this.#host.launcher(from);
954
954
  if (operation.awaits?.state === "open") {
955
955
  return refuse(
956
- "awaiting",
956
+ "already-awaiting",
957
957
  `you already await ${operation.awaits.owedBy}. End your turn, or send without --await-reply.`,
958
958
  );
959
959
  }
@@ -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
  }
@@ -1045,7 +1045,7 @@ export class Messaging {
1045
1045
  #ask(message: Message, operation: Operation, timeoutMs?: number): { until?: number } {
1046
1046
  message.route.count += 1;
1047
1047
  const now = this.#host.now();
1048
- const routeMs = this.#outside.has(message.to) ? message.route.timeoutMs : undefined;
1048
+ const routeMs = message.route.timeoutMs;
1049
1049
  // The route's limit, or the asker's own when shorter: the shorter wins.
1050
1050
  const limit =
1051
1051
  routeMs !== undefined && (timeoutMs === undefined || routeMs <= timeoutMs)
@@ -1069,7 +1069,7 @@ export class Messaging {
1069
1069
  owedBy: message.to,
1070
1070
  ...(until === undefined ? {} : { until }),
1071
1071
  timed: limitAt < (outside ? Infinity : (operation.deadline ?? Infinity)),
1072
- ...(limit && outside ? { closes: { ...limit, at: limitAt } } : {}),
1072
+ ...(limit ? { closes: { ...limit, at: limitAt } } : {}),
1073
1073
  state: "open",
1074
1074
  };
1075
1075
  message.asks = obligation;
@@ -1085,7 +1085,7 @@ export class Messaging {
1085
1085
  obligation,
1086
1086
  "timed-out",
1087
1087
  obligation.closes?.by === "route"
1088
- ? `the question closed unanswered after ${duration(obligation.closes.ms)}`
1088
+ ? `the question closed unanswered after ${formatDuration(obligation.closes.ms)}`
1089
1089
  : `your --timeout passed at ${clock(closesAt, this.#host.now())}`,
1090
1090
  );
1091
1091
  }
@@ -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);
@@ -1375,7 +1375,7 @@ export class Messaging {
1375
1375
  return {
1376
1376
  peer,
1377
1377
  can: this.#can(route, agent),
1378
- state: "outside",
1378
+ kind: "participant",
1379
1379
  queued: 0,
1380
1380
  purpose: route.purpose,
1381
1381
  };
@@ -1388,8 +1388,13 @@ export class Messaging {
1388
1388
  return {
1389
1389
  peer,
1390
1390
  can: this.#can(route, agent),
1391
+ kind: "agent",
1391
1392
  state:
1392
- view?.state === "stopped" ? "stopped" : awaiting ? "awaiting" : (view?.state ?? "idle"),
1393
+ view?.state === "stopped"
1394
+ ? "stopped"
1395
+ : awaiting
1396
+ ? "awaiting-reply"
1397
+ : (view?.state ?? "idle"),
1393
1398
  ...(awaiting && view?.state !== "stopped" ? { awaits: awaiting } : {}),
1394
1399
  ...(view?.state === "stopped" && view.reason ? { reason: view.reason } : {}),
1395
1400
  queued,
@@ -1405,7 +1410,7 @@ export class Messaging {
1405
1410
  };
1406
1411
  }
1407
1412
 
1408
- /** The line `wf` leads with while this agent's operation owes or awaits an answer. */
1413
+ /** The line `wfa` leads with while this agent's operation owes or awaits an answer. */
1409
1414
  owing(agent: AgentKey): string | undefined {
1410
1415
  const { owes, awaits } = this.#standing(agent);
1411
1416
  return owingLine(owes, awaits, this.#host.launcher(agent), this.#host.now());
@@ -1476,7 +1481,7 @@ export class Messaging {
1476
1481
 
1477
1482
  /**
1478
1483
  * 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.
1484
+ * stop closed, so `wfa peers` can say it stopped.
1480
1485
  */
1481
1486
  #routesOf(agent: AgentKey, stopped = false): Route[] {
1482
1487
  return [...new Set([...this.#routes.values()].flat())]
@@ -21,7 +21,7 @@ export type Route = {
21
21
  readonly asked?: number;
22
22
  /** As the workflow gave it, for telling an identical grant from a conflicting one. */
23
23
  readonly requested?: number;
24
- /** On a route an agent asks an outside participant over: how long a question on it waits. */
24
+ /** How long a question on the route waits, to an agent or an outside participant. */
25
25
  readonly timeoutMs?: number;
26
26
  readonly scope: Scope;
27
27
  /** Messages counted against it, answers reserved for included. */
@@ -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,
@@ -228,11 +228,13 @@ export function peersTable(
228
228
  const rows = peers.map((peer) => [
229
229
  peer.peer,
230
230
  peer.can,
231
- peer.state === "awaiting"
232
- ? `awaiting ${peer.awaits === viewer ? "you" : peer.awaits}`
233
- : peer.state === "stopped" && peer.reason
234
- ? `stopped (${peer.reason})`
235
- : peer.state,
231
+ peer.kind === "participant"
232
+ ? "participant"
233
+ : peer.state === "awaiting-reply"
234
+ ? `awaiting-reply from ${peer.awaits === viewer ? "you" : peer.awaits}`
235
+ : peer.state === "stopped" && peer.reason
236
+ ? `stopped (${peer.reason})`
237
+ : peer.state,
236
238
  peer.queued > 0 ? String(peer.queued) : "",
237
239
  quoted(peer.purpose),
238
240
  ]);