@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
@@ -6,6 +6,7 @@ import type {
6
6
  ToolFailure,
7
7
  } from "@agentswf/contract/wire";
8
8
  import type {
9
+ Duration,
9
10
  Effort,
10
11
  JsonObject,
11
12
  JsonValue,
@@ -78,7 +79,11 @@ export type Message = {
78
79
  answers?: true;
79
80
  };
80
81
 
81
- export type SendOptions = { awaitReply?: boolean; timeoutMs?: number };
82
+ export type SendOptions = {
83
+ awaitReply?: boolean;
84
+ /** How long an awaited reply is waited for, as `wfa send --timeout` gives it. */
85
+ timeout?: Duration;
86
+ };
82
87
 
83
88
  /** How a send or reply ended; a refusal is a value to test, not a throw. */
84
89
  export type MessageOutcome =
@@ -89,7 +94,8 @@ export type MessageOutcome =
89
94
 
90
95
  /**
91
96
  * A message that woke the agent, and what its script may do in that turn: `reply` and `send` as
92
- * the turn's own, or end it with `silent` or `fail`. Returning ends the turn as `silent` does.
97
+ * the turn's own. Returning nothing ends the turn without a reply, and one owed is reminded once,
98
+ * then fails; returning `noAnswer.failed(reason)` fails it as a failed harness does.
93
99
  */
94
100
  export type ReceivedMessage = Message & {
95
101
  /** The turn it woke: whether it is a reminder (`nudge`), and every message shown in it. */
@@ -97,26 +103,24 @@ export type ReceivedMessage = Message & {
97
103
  reply(text: string, options?: SendOptions): Promise<MessageOutcome>;
98
104
  send(peer: string, text: string, options?: SendOptions): Promise<MessageOutcome>;
99
105
  tool(name: string, input?: JsonObject): Promise<ToolOutcome>;
100
- /** Ends the turn without a reply; one owed is reminded once, then fails. */
101
- silent(): Reply;
102
- /** The harness failed: the message operation fails, and the agent stops. */
103
- fail(reason?: string): Reply;
104
106
  };
105
107
 
106
108
  /**
107
109
  * A message operation's script: called with the message that woke the agent, and again with the
108
110
  * next one when held messages continue the operation, or with the same one, `turn.nudge` set, when
109
- * it is reminded of a reply it owes.
111
+ * it is reminded of a reply it owes. A `noAnswer` it returns ends the turn as that says; nothing,
112
+ * or what a `reply` or `send` resolved to, ends it without a reply. A throw fails the test.
110
113
  */
111
114
  export type MessageScript = (
112
115
  message: ReceivedMessage,
113
- ) => Reply | MessageOutcome | undefined | Promise<Reply | MessageOutcome | undefined>;
116
+ // biome-ignore lint/suspicious/noConfusingVoidType: an async script that returns nothing is Promise<void>.
117
+ ) => void | NoAnswer | MessageOutcome | Promise<void | NoAnswer | MessageOutcome>;
114
118
 
115
119
  /**
116
- * How a participant's script's `reply` or `send` ended, as `awf reply --json` reports it: a reply's
117
- * `id` is the question's, a send's the new message's.
120
+ * How a participant's script's `reply` or `send` ended, as `awf reply --json` prints it without the
121
+ * `v`: delivered, `id` a reply's question or a send's new message, or refused with its `code`.
118
122
  */
119
- export type ParticipantOutcome = { code: "delivered"; id: string } | { code: ParticipantCode };
123
+ export type ParticipantOutcome = { id: string } | { code: ParticipantCode };
120
124
 
121
125
  /**
122
126
  * A message an agent sent a participant, and what the participant's script may do about it, as
@@ -157,7 +161,7 @@ export type ParticipantScript = (event: ParticipantRoute | ParticipantMessage) =
157
161
 
158
162
  const ENTRY: unique symbol = Symbol("agentswf.script-entry");
159
163
 
160
- type Respond = (turn: Turn) => JsonValue | Reply | Promise<JsonValue | Reply>;
164
+ type Respond = (turn: Turn) => JsonValue | NoAnswer | Promise<JsonValue | NoAnswer>;
161
165
 
162
166
  /** An answer to a turn that asks for its schema, or for text. Made by `answer`. */
163
167
  export type Answer = {
@@ -172,11 +176,13 @@ type Ending =
172
176
  | { kind: "hang" }
173
177
  | { kind: "interrupted" };
174
178
 
175
- /** A turn ended without an answer. Made by `reply`. */
176
- export type Reply = { readonly [ENTRY]: { kind: "reply"; ending: Ending } };
179
+ /** A turn ended without an answer. Made by `noAnswer`. */
180
+ export type NoAnswer = {
181
+ readonly [ENTRY]: { kind: "no-answer"; ending: Ending };
182
+ };
177
183
 
178
184
  /** Every turn the agent is asked, alike; or a list, one entry per turn in order. */
179
- export type Script = Answer | Reply | readonly (Answer | Reply)[];
185
+ export type Script = Answer | NoAnswer | readonly (Answer | NoAnswer)[];
180
186
 
181
187
  /** What a schema's answer is: the carrier the author surface types with, else TypeBox's `Static`. */
182
188
  export type AnswerOf<S> = S extends { readonly "~output"?: infer O }
@@ -186,11 +192,11 @@ export type AnswerOf<S> = S extends { readonly "~output"?: infer O }
186
192
  : StaticOf<S>;
187
193
  type StaticOf<S> = S extends Type.TSchema ? Type.Static<S> : JsonValue;
188
194
 
189
- type Answering<A> = A | ((turn: Turn) => A | Reply | Promise<A | Reply>);
195
+ type Answering<A> = A | ((turn: Turn) => A | NoAnswer | Promise<A | NoAnswer>);
190
196
 
191
197
  /**
192
198
  * An answer to a turn that asks for `schema`: a value of its type, or a function of the turn that
193
- * returns one or a `reply`. A function that returns a reply is asked again for the nudge, with
199
+ * returns one or a `noAnswer`. A function that returns no answer is asked again for the nudge, with
194
200
  * `turn.nudge` set.
195
201
  */
196
202
  export function answer<const S extends OutputSchema>(
@@ -206,46 +212,69 @@ export function answer(...args: [unknown] | [unknown, unknown]): Answer {
206
212
  return { [ENTRY]: { kind: "answer", schema: schema as OutputSchema | undefined, respond } };
207
213
  }
208
214
 
209
- const ending = (value: Ending): Reply => ({ [ENTRY]: { kind: "reply", ending: value } });
215
+ const ending = (value: Ending): NoAnswer => ({ [ENTRY]: { kind: "no-answer", ending: value } });
210
216
 
211
- /** Ends a turn without an answer. A reply answers no question, so it takes no schema. */
212
- export const reply = {
213
- /** Reports a cooperative wait; later check-ins invoke the same answer function again. */
214
- waiting: (reason: string, timeoutMs?: number): Reply =>
215
- ending({ kind: "waiting", reason, ...(timeoutMs === undefined ? {} : { timeoutMs }) }),
217
+ /** Ends a turn without an answer, so it takes no schema. */
218
+ export const noAnswer = {
216
219
  /** Ends without a result; recovery follows the placement's production support. */
217
- silent: (): Reply => ending({ kind: "silent" }),
220
+ silent: (): NoAnswer => ending({ kind: "silent" }),
218
221
  /** The agent is stuck, as on a permission prompt. */
219
- blocked: (reason = "waiting on a permission prompt"): Reply =>
222
+ blocked: (reason = "waiting on a permission prompt"): NoAnswer =>
220
223
  ending({ kind: "blocked", reason }),
221
224
  /** The harness failed. */
222
- failed: (reason = "harness crashed"): Reply => ending({ kind: "failed", reason }),
225
+ failed: (reason = "harness crashed"): NoAnswer => ending({ kind: "failed", reason }),
223
226
  /**
224
227
  * The agent's harness cannot sign in, as with an expired login: the turn fails with `login` set,
225
228
  * naming the agent's harness, `provider` where it logs in to several, and what to run.
226
229
  */
227
- needsLogin: (provider?: string): Reply =>
230
+ needsLogin: (provider?: string): NoAnswer =>
228
231
  ending({ kind: "needs-login", ...(provider === undefined ? {} : { provider }) }),
229
232
  /** The harness reported the turn timed out; it ends at once. */
230
- timedOut: (reason = "turn timed out"): Reply => ending({ kind: "timed-out", reason }),
233
+ timedOut: (reason = "turn timed out"): NoAnswer => ending({ kind: "timed-out", reason }),
231
234
  /** Holds the turn until the engine cancels it. */
232
- hang: (): Reply => ending({ kind: "hang" }),
235
+ hang: (): NoAnswer => ending({ kind: "hang" }),
233
236
  /**
234
237
  * The operator stopped the turn in their pane, as only the calling session's can be: it settles
235
238
  * `cancelled`, is never nudged, and the session takes the next turn.
236
239
  */
237
- interrupted: (): Reply => ending({ kind: "interrupted" }),
240
+ interrupted: (): NoAnswer => ending({ kind: "interrupted" }),
238
241
  };
239
242
 
240
243
  /**
241
- * How a turn ended, as `run.turns` keeps it: `awaiting-reply` when its script awaits a reply, which
242
- * a later turn of the same call resumes it with.
244
+ * Reports a cooperative wait, `wfa waiting`; later check-ins invoke the same answer function again.
245
+ * Only the engine's own tests use it: whether an agent may wait is its harness's, not a workflow's.
246
+ */
247
+ export const waiting = (reason: string, timeoutMs?: number): NoAnswer =>
248
+ ending({ kind: "waiting", reason, ...(timeoutMs === undefined ? {} : { timeoutMs }) });
249
+
250
+ /**
251
+ * How a turn ended, as `run.turns` keeps it: the engine's word, but for `waiting`, a check-in, and
252
+ * `awaiting-reply`, a script awaiting a reply, which a later turn of the same call resumes it with.
243
253
  */
244
- export type TurnOutcome = "answered" | Ending["kind"] | "cancelled" | "awaiting-reply";
254
+ export type TestTurnOutcome =
255
+ | "answered"
256
+ | "unanswered"
257
+ | "blocked"
258
+ | "failed"
259
+ | "timed-out"
260
+ | "cancelled"
261
+ | "waiting"
262
+ | "awaiting-reply";
263
+
264
+ /**
265
+ * A turn's outcome until the engine's record replaces it: what a turn before its operation's last,
266
+ * a nudged one, keeps.
267
+ */
268
+ export function outcomeOf(kind: Exclude<Ending["kind"], "waiting">): TestTurnOutcome {
269
+ if (kind === "silent") return "unanswered";
270
+ if (kind === "needs-login") return "failed";
271
+ if (kind === "hang" || kind === "interrupted") return "cancelled";
272
+ return kind;
273
+ }
245
274
 
246
275
  export type Step =
247
276
  | { kind: "answer"; value: JsonValue }
248
- | { kind: "reply"; ending: Ending }
277
+ | { kind: "no-answer"; ending: Ending }
249
278
  /** The script cannot meet this turn: the test's mistake, not the workflow's. */
250
279
  | { kind: "script-error"; message: string };
251
280
 
@@ -333,7 +362,7 @@ class Lists<E> {
333
362
  type Of = { noun: string; option: string; whose: string; expected: string };
334
363
 
335
364
  /** Each agent's turn scripts, or its compactions'. */
336
- export class Scripts extends Lists<Answer | Reply> {
365
+ export class Scripts extends Lists<Answer | NoAnswer> {
337
366
  readonly #noun: string;
338
367
 
339
368
  constructor(
@@ -345,7 +374,7 @@ export class Scripts extends Lists<Answer | Reply> {
345
374
  {
346
375
  ...of,
347
376
  whose: of.noun === "turn" ? "script" : `${of.noun} script`,
348
- expected: "answer(…), reply.…()",
377
+ expected: "answer(…), noAnswer.…()",
349
378
  },
350
379
  isEntry,
351
380
  );
@@ -357,14 +386,14 @@ export class Scripts extends Lists<Answer | Reply> {
357
386
  const found = this.entry(turn.agent, index);
358
387
  if ("why" in found) return this.error(turn, found.why);
359
388
  const meant = found.entry[ENTRY];
360
- if (meant.kind === "reply") return { kind: "reply", ending: meant.ending };
389
+ if (meant.kind === "no-answer") return { ...meant };
361
390
  if (!sameSchema(meant.schema, turn.schema)) {
362
391
  return this.error(
363
392
  turn,
364
393
  `it asks for ${describeSchema(turn.schema)}; its script answers ${describeSchema(meant.schema)}`,
365
394
  );
366
395
  }
367
- let out: JsonValue | Reply | undefined;
396
+ let out: JsonValue | NoAnswer | undefined;
368
397
  try {
369
398
  out = await meant.respond(turn);
370
399
  } catch (error) {
@@ -373,7 +402,7 @@ export class Scripts extends Lists<Answer | Reply> {
373
402
  if (out === undefined) return this.error(turn, "its script returned nothing");
374
403
  if (isEntry(out)) {
375
404
  const given = out[ENTRY];
376
- if (given.kind === "reply") return { kind: "reply", ending: given.ending };
405
+ if (given.kind === "no-answer") return { ...given };
377
406
  return this.error(turn, "its script returned answer(…); a function returns the value itself");
378
407
  }
379
408
  return { kind: "answer", value: out };
@@ -403,8 +432,8 @@ export class MessageScripts extends Lists<MessageScript> {
403
432
  }
404
433
 
405
434
  /**
406
- * Runs the script for the agent's `index`th message operation with `message`; what it returns
407
- * ends the turn, as `silent` unless it is a `reply`.
435
+ * Runs the script for the agent's `index`th message operation with `message`: what it returns
436
+ * ends the turn silent unless it is a `noAnswer`.
408
437
  */
409
438
  async step(message: ReceivedMessage, index: number): Promise<Step> {
410
439
  const { turn } = message;
@@ -427,13 +456,13 @@ export class MessageScripts extends Lists<MessageScript> {
427
456
  }
428
457
  if (isEntry(out)) {
429
458
  const given = out[ENTRY];
430
- if (given.kind === "reply") return { kind: "reply", ending: given.ending };
459
+ if (given.kind === "no-answer") return { ...given };
431
460
  return {
432
461
  kind: "script-error",
433
462
  message: `${where}: a message script ends a turn, it answers no call`,
434
463
  };
435
464
  }
436
- return { kind: "reply", ending: { kind: "silent" } };
465
+ return { kind: "no-answer", ending: { kind: "silent" } };
437
466
  }
438
467
  }
439
468
 
@@ -459,7 +488,7 @@ export function findByKey<T>(
459
488
  };
460
489
  }
461
490
 
462
- function isEntry(value: unknown): value is Answer | Reply {
491
+ function isEntry(value: unknown): value is Answer | NoAnswer {
463
492
  return typeof value === "object" && value !== null && ENTRY in value;
464
493
  }
465
494
 
@@ -489,7 +518,7 @@ function globMatches(pattern: string, key: string): boolean {
489
518
  return new RegExp(`^${escaped.join(".*")}$`).test(key);
490
519
  }
491
520
 
492
- export function firstLine(text: string): string {
521
+ function firstLine(text: string): string {
493
522
  const line = text.split("\n", 1)[0]!;
494
523
  return line.length > 100 ? `${line.slice(0, 100)}…` : line;
495
524
  }
@@ -1,8 +0,0 @@
1
- // Holds a run's lock from another process for `emit.test.ts`: `bun hold-run-lock.fixture.ts {file} {ms}`.
2
- import { withRunLock } from "./run-lock";
3
-
4
- const [file, ms] = process.argv.slice(2);
5
- await withRunLock(file!, async () => {
6
- console.log("held");
7
- await Bun.sleep(Number(ms));
8
- });