@agentswf/engine 0.1.0-beta.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +16 -0
  3. package/package.json +13 -9
  4. package/src/accounting/format.ts +4 -3
  5. package/src/accounting/spent.ts +128 -0
  6. package/src/accounting/summary.ts +40 -32
  7. package/src/agent-cli/cli.ts +35 -22
  8. package/src/agent-cli/client.ts +1 -27
  9. package/src/allowance-command.ts +8 -10
  10. package/src/attempt-ending.ts +5 -7
  11. package/src/attempt-output.ts +15 -32
  12. package/src/attempt-view.ts +34 -33
  13. package/src/author-typebox.ts +13 -0
  14. package/src/client/checks.ts +94 -0
  15. package/src/client/exported.ts +17 -0
  16. package/src/client/index.ts +122 -0
  17. package/src/client/locate.ts +34 -0
  18. package/src/client/published.ts +3 -0
  19. package/src/client/runs.ts +337 -0
  20. package/src/command-line.ts +64 -0
  21. package/src/commands.ts +7 -1
  22. package/src/control-plane.ts +41 -11
  23. package/src/coordinator-prompt.ts +4 -3
  24. package/src/coordinator-socket.ts +41 -33
  25. package/src/deadlines.ts +40 -7
  26. package/src/decisions/directory.ts +51 -40
  27. package/src/decisions/fake.ts +1 -1
  28. package/src/decisions/openrouter.ts +4 -2
  29. package/src/decisions/seam.ts +4 -4
  30. package/src/display-path.ts +3 -3
  31. package/src/duration.ts +43 -27
  32. package/src/emit-command.ts +88 -58
  33. package/src/emit.ts +254 -56
  34. package/src/event-ids.ts +31 -0
  35. package/src/files.ts +44 -4
  36. package/src/hand-over.ts +150 -0
  37. package/src/help/allowance.txt +4 -3
  38. package/src/help/emit.txt +18 -6
  39. package/src/help/inbox.txt +6 -4
  40. package/src/help/logs.txt +9 -6
  41. package/src/help/open.txt +29 -0
  42. package/src/help/reply.txt +6 -8
  43. package/src/help/run.txt +30 -11
  44. package/src/help/runs.txt +4 -2
  45. package/src/help/tick.txt +9 -4
  46. package/src/herdr-placement.ts +3 -33
  47. package/src/herdr-run-session.ts +7 -15
  48. package/src/herdr-workspace-marks.ts +7 -17
  49. package/src/holder-command.ts +56 -133
  50. package/src/in-lines.ts +28 -0
  51. package/src/inbox-command.ts +40 -57
  52. package/src/index.ts +1 -1
  53. package/src/init-command.ts +8 -17
  54. package/src/init-template/hello/workflow.test.ts.template +3 -3
  55. package/src/init-template/hello/workflow.ts.template +5 -4
  56. package/src/jsonl.ts +10 -6
  57. package/src/keep-alive.ts +15 -12
  58. package/src/logs-command.ts +128 -47
  59. package/src/message-operation.ts +6 -3
  60. package/src/messaging/limits.ts +4 -4
  61. package/src/messaging/messaging.ts +31 -28
  62. package/src/messaging/routes.ts +1 -1
  63. package/src/messaging/text.ts +9 -7
  64. package/src/open-command.ts +770 -0
  65. package/src/operation-events.ts +4 -6
  66. package/src/operation-liveness.ts +19 -4
  67. package/src/operator-cli.ts +224 -94
  68. package/src/operator-runtime.ts +29 -7
  69. package/src/ops.ts +270 -0
  70. package/src/pane-layout.ts +105 -59
  71. package/src/progress-view.ts +10 -10
  72. package/src/project-engine.ts +79 -0
  73. package/src/projects.ts +13 -2
  74. package/src/refusals.ts +119 -0
  75. package/src/result-slots.ts +11 -31
  76. package/src/result-validation.ts +2 -2
  77. package/src/run-command.ts +45 -28
  78. package/src/run-commands.ts +43 -115
  79. package/src/run-event-log.ts +1 -1
  80. package/src/run-lines.ts +159 -0
  81. package/src/run-lock.ts +57 -1
  82. package/src/{stage-plan.ts → run-policy/stage-plan.ts} +46 -42
  83. package/src/run-policy/stages.ts +113 -0
  84. package/src/run-policy/waits.ts +452 -0
  85. package/src/run-prepare.ts +205 -43
  86. package/src/run-read.ts +109 -41
  87. package/src/run-state.ts +170 -50
  88. package/src/run-text.ts +54 -0
  89. package/src/run-unreachable.ts +18 -0
  90. package/src/run-usage.ts +18 -12
  91. package/src/run-view.ts +81 -17
  92. package/src/runs-command.ts +83 -63
  93. package/src/runs.ts +310 -212
  94. package/src/sandbox-homes.ts +65 -13
  95. package/src/sandboxes.ts +182 -28
  96. package/src/session-lines.ts +93 -0
  97. package/src/skills/run-skills.ts +8 -1
  98. package/src/sources.ts +28 -29
  99. package/src/spend.ts +333 -0
  100. package/src/stage-ledger.ts +119 -131
  101. package/src/stop-crashed.ts +6 -5
  102. package/src/stopped.ts +4 -4
  103. package/src/test-command.ts +36 -14
  104. package/src/testing/operation-events.ts +3 -23
  105. package/src/testing.ts +73 -26
  106. package/src/tick-command.ts +145 -15
  107. package/src/tools.ts +25 -16
  108. package/src/turn-ends.ts +37 -0
  109. package/src/waits.ts +651 -0
  110. package/src/wake.ts +84 -54
  111. package/src/workflow-loader.ts +54 -4
  112. package/src/workflow-runner.ts +805 -580
  113. package/src/workflow-test-preload.ts +4 -1
  114. package/src/workflow-testing/coordinators.ts +3 -3
  115. package/src/workflow-testing/fake-client.ts +387 -0
  116. package/src/workflow-testing/host.ts +121 -53
  117. package/src/workflow-testing/index.ts +33 -842
  118. package/src/workflow-testing/run-test.ts +1128 -0
  119. package/src/workflow-testing/script.ts +74 -45
  120. package/src/hold-run-lock.fixture.ts +0 -8
  121. package/src/mailbox.ts +0 -990
  122. package/src/workflow-testing/coordinator.typecheck.ts +0 -54
  123. package/src/workflow-testing/workflow-testing.typecheck.ts +0 -64
@@ -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 { duration } from "../accounting/format";
13
13
  import { MESSAGE_LIMITS } from "./limits";
14
14
  import {
15
15
  directions,
@@ -53,6 +53,8 @@ export interface MessagingHost {
53
53
  changed(key: AgentKey): void;
54
54
  schedule(at: number, action: () => void): () => void;
55
55
  now(): number;
56
+ /** Question `n`'s id as `awf inbox` prints it, the attempt before it: what a refusal names. */
57
+ question(n: number): string;
56
58
  /** Written to the run's event log; `message-sent` is awaited before a send is acknowledged. */
57
59
  record(event: MessagingEventBody): Promise<void>;
58
60
  }
@@ -117,7 +119,7 @@ export type HolderRequest = {
117
119
  /** A holder's result: a reply's `message` is the question's number, a send's the new message's. */
118
120
  export type HolderResult =
119
121
  | { code: "delivered"; message: number }
120
- | { code: Exclude<ParticipantCode, "not-found">; error: string; choices?: string[] };
122
+ | { code: ParticipantCode; error: string; choices?: string[] };
121
123
 
122
124
  /** A question open to an outside participant, and when it closes unanswered if it has a limit. */
123
125
  export type OpenQuestion = {
@@ -180,7 +182,7 @@ type Obligation = {
180
182
  /** It has a `--timeout` of its own, which only that fails it at. */
181
183
  readonly timed: boolean;
182
184
  /**
183
- * When it closes unanswered, on an outside participant: the route's `timeoutMs` or the asker's
185
+ * When it closes unanswered: the route's `timeoutMs` or the asker's
184
186
  * shorter `--timeout`, and which of the two.
185
187
  */
186
188
  readonly closes?: { at: number; by: "route" | "own"; ms: number };
@@ -279,15 +281,13 @@ export class Messaging {
279
281
  }
280
282
  }
281
283
  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
- }
284
+ if (
285
+ timeoutMs !== undefined &&
286
+ (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1)
287
+ )
288
+ throw new Error(
289
+ `a route's timeoutMs is a positive whole number of milliseconds: not ${timeoutMs}`,
290
+ );
291
291
  const open = pairs.map(([from, to]) => this.#open(from, to));
292
292
  if (open.some(Boolean)) {
293
293
  if (
@@ -361,7 +361,7 @@ export class Messaging {
361
361
  });
362
362
  }
363
363
 
364
- /** Writes `text` to a participant routed to the code, without a turn: an inbox item for it. */
364
+ /** Writes `text` to a participant, without a turn: an inbox item for it. */
365
365
  async post(key: AgentKey, text: string): Promise<string> {
366
366
  if (this.#closed) throw new Error("the attempt has ended; nothing more is posted");
367
367
  if (!this.#outside.has(key)) throw new Error(`${key} is not connected`);
@@ -446,7 +446,7 @@ export class Messaging {
446
446
  return holderRefusal(
447
447
  "ambiguous",
448
448
  `${agent} has ${open.length} questions open to ${as.key}; name one with --to.`,
449
- open.map((each) => String(each.request.n)),
449
+ open.map((each) => this.#host.question(each.request.n)),
450
450
  );
451
451
  owed = open[0];
452
452
  if (!owed) return holderRefusal("expired", `${agent} has no question open to ${as.key}.`);
@@ -461,7 +461,7 @@ export class Messaging {
461
461
  if (!owed)
462
462
  return holderRefusal(
463
463
  "expired",
464
- `${request.to} is not a question open from ${agent}${as.key === undefined ? "" : ` to ${as.key}`}.`,
464
+ `${this.#host.question(request.to)} is not a question open from ${agent}${as.key === undefined ? "" : ` to ${as.key}`}.`,
465
465
  );
466
466
  }
467
467
  const route = owed.request.route;
@@ -506,7 +506,7 @@ export class Messaging {
506
506
  return holderRefusal("no-route", `the workflow allows no route from ${from} to ${agent}.`);
507
507
  if (view.state === "stopped")
508
508
  return holderRefusal(
509
- "stopped",
509
+ "peer-stopped",
510
510
  `${agent} has stopped${view.reason ? ` (${JSON.stringify(view.reason)})` : ""}.`,
511
511
  );
512
512
  const route = initiating.find((each) => each.open);
@@ -942,18 +942,20 @@ export class Messaging {
942
942
  return { kind: "ok", operation, owed: latest, bytes };
943
943
  }
944
944
 
945
- /** `awaiting`, `unsupported` and `cycle`, checked for any question. */
945
+ /** `already-awaiting`, `unsupported` and `cycle`, checked for any question. */
946
946
  #checkAsk(
947
947
  from: AgentKey,
948
948
  peer: AgentKey,
949
949
  operation: Operation,
950
950
  /** The question a reply answers, which counts as answered here. */
951
951
  answering?: Obligation,
952
- ): { kind: "refused"; code: "awaiting" | "unsupported" | "cycle"; error: string } | undefined {
952
+ ):
953
+ | { kind: "refused"; code: "already-awaiting" | "unsupported" | "cycle"; error: string }
954
+ | undefined {
953
955
  const wfa = this.#host.launcher(from);
954
956
  if (operation.awaits?.state === "open") {
955
957
  return refuse(
956
- "awaiting",
958
+ "already-awaiting",
957
959
  `you already await ${operation.awaits.owedBy}. End your turn, or send without --await-reply.`,
958
960
  );
959
961
  }
@@ -1045,7 +1047,7 @@ export class Messaging {
1045
1047
  #ask(message: Message, operation: Operation, timeoutMs?: number): { until?: number } {
1046
1048
  message.route.count += 1;
1047
1049
  const now = this.#host.now();
1048
- const routeMs = this.#outside.has(message.to) ? message.route.timeoutMs : undefined;
1050
+ const routeMs = message.route.timeoutMs;
1049
1051
  // The route's limit, or the asker's own when shorter: the shorter wins.
1050
1052
  const limit =
1051
1053
  routeMs !== undefined && (timeoutMs === undefined || routeMs <= timeoutMs)
@@ -1069,7 +1071,7 @@ export class Messaging {
1069
1071
  owedBy: message.to,
1070
1072
  ...(until === undefined ? {} : { until }),
1071
1073
  timed: limitAt < (outside ? Infinity : (operation.deadline ?? Infinity)),
1072
- ...(limit && outside ? { closes: { ...limit, at: limitAt } } : {}),
1074
+ ...(limit ? { closes: { ...limit, at: limitAt } } : {}),
1073
1075
  state: "open",
1074
1076
  };
1075
1077
  message.asks = obligation;
@@ -1375,7 +1377,7 @@ export class Messaging {
1375
1377
  return {
1376
1378
  peer,
1377
1379
  can: this.#can(route, agent),
1378
- state: "outside",
1380
+ kind: "participant",
1379
1381
  queued: 0,
1380
1382
  purpose: route.purpose,
1381
1383
  };
@@ -1388,8 +1390,13 @@ export class Messaging {
1388
1390
  return {
1389
1391
  peer,
1390
1392
  can: this.#can(route, agent),
1393
+ kind: "agent",
1391
1394
  state:
1392
- view?.state === "stopped" ? "stopped" : awaiting ? "awaiting" : (view?.state ?? "idle"),
1395
+ view?.state === "stopped"
1396
+ ? "stopped"
1397
+ : awaiting
1398
+ ? "awaiting-reply"
1399
+ : (view?.state ?? "idle"),
1393
1400
  ...(awaiting && view?.state !== "stopped" ? { awaits: awaiting } : {}),
1394
1401
  ...(view?.state === "stopped" && view.reason ? { reason: view.reason } : {}),
1395
1402
  queued,
@@ -1676,11 +1683,7 @@ function refuse<C extends SendCode | ReplyCode>(code: C, error: string) {
1676
1683
  return { kind: "refused" as const, code, error };
1677
1684
  }
1678
1685
 
1679
- function holderRefusal(
1680
- code: Exclude<ParticipantCode, "not-found">,
1681
- error: string,
1682
- choices?: string[],
1683
- ): HolderResult {
1686
+ function holderRefusal(code: ParticipantCode, error: string, choices?: string[]): HolderResult {
1684
1687
  return { code, error, ...(choices ? { choices } : {}) };
1685
1688
  }
1686
1689
 
@@ -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. */
@@ -31,7 +31,7 @@ export type Delivered =
31
31
  }
32
32
  | { kind: "no-reply"; from: AgentKey; reason: string };
33
33
 
34
- export function quoted(purpose: string): string {
34
+ function quoted(purpose: string): string {
35
35
  return `“${purpose}”`;
36
36
  }
37
37
 
@@ -53,7 +53,7 @@ export function clock(at: number, now: number): string {
53
53
  }
54
54
 
55
55
  /** At least eight hex characters, as the design asks; a fresh one per delivery. */
56
- export function nonce(): string {
56
+ function nonce(): string {
57
57
  return randomBytes(4).toString("hex");
58
58
  }
59
59
 
@@ -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
  ]);