@agentswf/engine 0.1.0-beta.5 → 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 (122) 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 +28 -55
  8. package/src/agent-cli/client.ts +1 -27
  9. package/src/allowance-command.ts +8 -10
  10. package/src/attempt-ending.ts +4 -4
  11. package/src/attempt-output.ts +13 -31
  12. package/src/attempt-view.ts +33 -32
  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 +4 -6
  23. package/src/coordinator-prompt.ts +4 -3
  24. package/src/coordinator-socket.ts +27 -28
  25. package/src/deadlines.ts +6 -40
  26. package/src/decisions/directory.ts +50 -37
  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 +19 -33
  32. package/src/emit-command.ts +38 -22
  33. package/src/emit.ts +148 -59
  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 +11 -7
  39. package/src/help/inbox.txt +5 -3
  40. package/src/help/logs.txt +7 -5
  41. package/src/help/open.txt +29 -0
  42. package/src/help/reply.txt +6 -8
  43. package/src/help/run.txt +13 -8
  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 +5 -13
  48. package/src/herdr-workspace-marks.ts +7 -17
  49. package/src/holder-command.ts +54 -136
  50. package/src/in-lines.ts +28 -0
  51. package/src/inbox-command.ts +42 -56
  52. package/src/index.ts +1 -1
  53. package/src/init-command.ts +8 -17
  54. package/src/init-template/hello/workflow.ts.template +3 -2
  55. package/src/jsonl.ts +10 -6
  56. package/src/keep-alive.ts +7 -19
  57. package/src/logs-command.ts +134 -53
  58. package/src/message-operation.ts +6 -3
  59. package/src/messaging/messaging.ts +8 -10
  60. package/src/messaging/text.ts +2 -2
  61. package/src/open-command.ts +770 -0
  62. package/src/operation-events.ts +4 -6
  63. package/src/operation-liveness.ts +19 -4
  64. package/src/operator-cli.ts +206 -90
  65. package/src/operator-runtime.ts +24 -2
  66. package/src/ops.ts +270 -0
  67. package/src/pane-layout.ts +49 -96
  68. package/src/progress-view.ts +2 -2
  69. package/src/project-engine.ts +79 -0
  70. package/src/projects.ts +13 -2
  71. package/src/refusals.ts +119 -0
  72. package/src/result-slots.ts +11 -31
  73. package/src/result-validation.ts +2 -2
  74. package/src/run-command.ts +27 -31
  75. package/src/run-commands.ts +42 -114
  76. package/src/run-event-log.ts +1 -1
  77. package/src/run-lines.ts +159 -0
  78. package/src/run-lock.ts +57 -1
  79. package/src/run-policy/stage-plan.ts +154 -0
  80. package/src/run-policy/stages.ts +113 -0
  81. package/src/run-policy/waits.ts +452 -0
  82. package/src/run-prepare.ts +165 -41
  83. package/src/run-read.ts +109 -41
  84. package/src/run-state.ts +170 -50
  85. package/src/run-text.ts +54 -0
  86. package/src/run-unreachable.ts +18 -0
  87. package/src/run-usage.ts +17 -11
  88. package/src/run-view.ts +73 -14
  89. package/src/runs-command.ts +81 -63
  90. package/src/runs.ts +284 -222
  91. package/src/sandbox-homes.ts +65 -13
  92. package/src/sandboxes.ts +182 -28
  93. package/src/session-lines.ts +93 -0
  94. package/src/skills/run-skills.ts +8 -1
  95. package/src/sources.ts +2 -39
  96. package/src/spend.ts +333 -0
  97. package/src/stage-ledger.ts +111 -153
  98. package/src/stop-crashed.ts +6 -5
  99. package/src/stopped.ts +4 -4
  100. package/src/test-command.ts +37 -19
  101. package/src/testing/operation-events.ts +3 -23
  102. package/src/testing.ts +66 -18
  103. package/src/tick-command.ts +145 -15
  104. package/src/tools.ts +11 -30
  105. package/src/turn-ends.ts +37 -0
  106. package/src/waits.ts +651 -0
  107. package/src/wake.ts +84 -54
  108. package/src/workflow-loader.ts +49 -2
  109. package/src/workflow-runner.ts +525 -618
  110. package/src/workflow-test-preload.ts +4 -1
  111. package/src/workflow-testing/coordinators.ts +1 -1
  112. package/src/workflow-testing/fake-client.ts +387 -0
  113. package/src/workflow-testing/host.ts +28 -29
  114. package/src/workflow-testing/index.ts +9 -27
  115. package/src/workflow-testing/run-test.ts +201 -286
  116. package/src/workflow-testing/script.ts +7 -39
  117. package/src/deprecations.ts +0 -177
  118. package/src/hold-run-lock.fixture.ts +0 -8
  119. package/src/mailbox.ts +0 -972
  120. package/src/stage-plan.ts +0 -186
  121. package/src/workflow-testing/coordinator.typecheck.ts +0 -54
  122. package/src/workflow-testing/workflow-testing.typecheck.ts +0 -67
@@ -1,6 +1,8 @@
1
1
  // `awf test` preloads this into `bun test`, so a workflow's tests in any folder import the author
2
- // surface and `agentswf/testing` with nothing installed there.
2
+ // surface and `agentswf/testing` with nothing installed there; and `agentswf/client`, so a
3
+ // program's code under test and the fake client share one `ClientError`.
3
4
  import { plugin } from "bun";
5
+ import * as programs from "./client/published";
4
6
  import { serveAuthorSurface } from "./workflow-loader";
5
7
  import * as testing from "./workflow-testing";
6
8
 
@@ -9,5 +11,6 @@ plugin({
9
11
  name: "agentswf testing",
10
12
  setup(build) {
11
13
  build.module("agentswf/testing", () => ({ exports: testing, loader: "object" }));
14
+ build.module("agentswf/client", () => ({ exports: programs, loader: "object" }));
12
15
  },
13
16
  });
@@ -127,7 +127,7 @@ export class ParticipantScripts implements CoordinatorPort {
127
127
  async #outcome(result: Promise<HolderResult>): Promise<ParticipantOutcome> {
128
128
  const settled = await result;
129
129
  return settled.code === "delivered"
130
- ? { code: "delivered", id: this.#id(settled.message) }
130
+ ? { id: this.#id(settled.message) }
131
131
  : { code: settled.code };
132
132
  }
133
133
 
@@ -0,0 +1,387 @@
1
+ // A program's tests' `Client`: keeps the calls it is given and answers as the real one's rules do,
2
+ // or as the test scripts. It makes no run and starts no process.
3
+ import { dirname } from "node:path";
4
+ import {
5
+ type Client,
6
+ ClientError,
7
+ type Run,
8
+ type RunStatus,
9
+ type StartResult,
10
+ } from "@agentswf/contract/client";
11
+ import { type EmitResultJson, emitOk, type InboxItemJson } from "@agentswf/contract/records";
12
+ import {
13
+ awfEventOf,
14
+ isEventWorkflow,
15
+ type JsonValue,
16
+ nameProblem,
17
+ } from "@agentswf/contract/workflow";
18
+ import { canonical } from "../canonical-json";
19
+ import {
20
+ assertEventWorkflow,
21
+ checkArgs,
22
+ checkCursor,
23
+ checkEmit,
24
+ checkRunId,
25
+ derivedId,
26
+ fileOf,
27
+ nameOf,
28
+ } from "../client/checks";
29
+ import { isFinal } from "../client/runs";
30
+ import { checkEvent, EmitError } from "../emit";
31
+ import { messageOf } from "../errors";
32
+ import { prepareArgs } from "../run-prepare";
33
+ import type { AnyWorkflow, EventWorkflow } from "../workflow-loader";
34
+
35
+ /** A call a fake client was given, in order, whether or not it was refused. */
36
+ export type FakeClientCall =
37
+ | {
38
+ kind: "emit";
39
+ workflow: string;
40
+ /** `{workflow}/{id}`. */
41
+ ref: string;
42
+ name: string;
43
+ data: JsonValue;
44
+ eventId?: string;
45
+ source?: string;
46
+ }
47
+ /** `ref` names the run `id`, `id(args)` or the fake named; absent when none could be named. */
48
+ | { kind: "start"; workflow: string; ref?: string; args: JsonValue }
49
+ | { kind: "get"; workflow: string; ref: string }
50
+ | { kind: "inbox"; workflow: string; after?: string };
51
+
52
+ /** What a test scripts; anything it leaves out is answered as the real client's rules would. */
53
+ export type FakeClientScript = {
54
+ /**
55
+ * What became of an emit, for the call given, in place of what the rules would say; undefined
56
+ * answers by the rules. `why` alone means nothing was done (`did: "none"`). What it says is what
57
+ * the fake then knows: a refused or duplicate event leaves no new run, a `launch-failed` a run
58
+ * not launched, a `stuck` one stuck.
59
+ */
60
+ emit?: (
61
+ call: Extract<FakeClientCall, { kind: "emit" }>,
62
+ ) => Partial<Pick<EmitResultJson, "outcome" | "did" | "why" | "error">> | undefined;
63
+ /** A start's `did` and `error`, for the call given; undefined answers by the rules. */
64
+ start?: (
65
+ call: Extract<FakeClientCall, { kind: "start" }> & { ref: string },
66
+ ) => { did: StartResult<JsonValue>["did"]; error?: string } | undefined;
67
+ /**
68
+ * By ref: the rests the run comes to, in order. A wait takes the next one, as the run's next rest
69
+ * after the call; a run that isn't running, by the last one taken, is answered at once, as the
70
+ * real client does. Its status is the last one taken; an emit or start that launches it makes it
71
+ * `running` again, and a launch that fails leaves it at its last rest, or `interrupted` (`stuck`
72
+ * after three) if it has none. Its `final` is the real client's, from its state and whether the
73
+ * workflow takes events, whatever the rest scripted says. A start with no id names its run
74
+ * `fake-1`, `fake-2`, and so on, which this can name. Attempts that fail or crash count toward `stuck` only as the rests
75
+ * scripted say. A run named here is there for `get`; the first `start` or event to
76
+ * reach it, before a wait takes one of its rests, is the one that made it.
77
+ */
78
+ rests?: Record<string, RunStatus<JsonValue>[]>;
79
+ /** What `inbox` returns, for the call given. */
80
+ inbox?: (call: Extract<FakeClientCall, { kind: "inbox" }>) => InboxItemJson[];
81
+ };
82
+
83
+ export type FakeClient = Client & {
84
+ /** Every call, in the order given. */
85
+ readonly calls: FakeClientCall[];
86
+ };
87
+
88
+ /** A run the fake knows: begun by an event or a start, launched or not, and what it took. */
89
+ type Known = {
90
+ /** The args it was started with; absent for one an event began. */
91
+ args?: JsonValue;
92
+ /** Whether an attempt was ever launched: until then, a wake `started` it, after, `continued`. */
93
+ attempted: boolean;
94
+ /** Launches that failed in a row: three make it stuck, as the real rule does. */
95
+ failed: number;
96
+ /** The last rest a wait took. */
97
+ rest?: RunStatus<JsonValue>;
98
+ /** Named only by `rests`: the first start of it is the one that made it. */
99
+ scripted?: true;
100
+ eventIds: Set<string>;
101
+ status: RunStatus<JsonValue>;
102
+ rests: RunStatus<JsonValue>[];
103
+ };
104
+
105
+ const RUNNING: RunStatus<JsonValue> = { state: "running", final: false };
106
+
107
+ /**
108
+ * A `Client` for a program's tests, as `testWorkflow` is for a workflow's: it runs the real
109
+ * client's checks on what it is given, so a test sees the `ClientError` the real one throws, and
110
+ * keeps its calls. Unscripted, it answers as the real rules do: an event to a new id is `started`
111
+ * if it may start a run and refused `not-a-start` if not; to a run there, `refused` `returned` for
112
+ * one that completed (`none`, `returned`, if resent), and otherwise woken as the real `wake` does, a `duplicate` for an event id it
113
+ * has: `started` for a run whose launch failed (`stuck` after three), `nudged` while running,
114
+ * `continued` once parked, failed or crashed, nothing for one stopped, cancelled or stuck; a reply
115
+ * to no run throws `no-run`; a start is `started` for a new id, `exists` for one there with the same args, and
116
+ * `other-args` for other args, or a run an event began.
117
+ */
118
+ export function fakeClient(script: FakeClientScript = {}): FakeClient {
119
+ const calls: FakeClientCall[] = [];
120
+ const known = new Map<string, Known>();
121
+ const fresh = (args?: JsonValue): Known => ({
122
+ ...(args === undefined ? {} : { args }),
123
+ attempted: true,
124
+ failed: 0,
125
+ eventIds: new Set(),
126
+ status: RUNNING,
127
+ rests: [],
128
+ });
129
+ for (const [ref, rests] of Object.entries(script.rests ?? {}))
130
+ known.set(ref, { ...fresh(), rests: [...rests], scripted: true });
131
+
132
+ /** `final`, as the real client says it: from the state, and whether the run takes events. */
133
+ const handle = (ref: string, events: boolean): Run<never> => {
134
+ const run = known.get(ref)!;
135
+ const now = (): RunStatus<JsonValue> => ({
136
+ ...run.status,
137
+ final: isFinal(run.status.state, events),
138
+ });
139
+ const made: Run<JsonValue> = {
140
+ id: ref.slice(ref.indexOf("/") + 1),
141
+ ref,
142
+ status: async () => now(),
143
+ wait: async (options = {}) => {
144
+ options.signal?.throwIfAborted();
145
+ if (options.final ? now().final : run.status.state !== "running") return now();
146
+ for (let next = run.rests.shift(); next; next = run.rests.shift()) {
147
+ run.status = next;
148
+ run.rest = next;
149
+ if (!options.final || now().final) return now();
150
+ }
151
+ const { signal } = options;
152
+ if (!signal)
153
+ throw new Error(
154
+ `fakeClient: ${ref} has no rest scripted for this wait, and no signal ends it`,
155
+ );
156
+ return new Promise<never>((_, reject) =>
157
+ signal.addEventListener("abort", () => reject(signal.reason), { once: true }),
158
+ );
159
+ },
160
+ };
161
+ return made as Run<never>;
162
+ };
163
+
164
+ /** What the real `wake` does to a run in this state: start, nudge, continue, or nothing. */
165
+ const wake = (
166
+ run: Known,
167
+ duplicate: boolean,
168
+ ): Pick<EmitResultJson, "did"> & Partial<Pick<EmitResultJson, "why">> => {
169
+ if (run.failed >= 3) return { did: "none", why: "stuck" };
170
+ if (!run.attempted) return { did: "started" };
171
+ switch (run.status.state) {
172
+ case "running":
173
+ return duplicate ? { did: "none" } : { did: "nudged" };
174
+ case "completed":
175
+ return { did: "none", why: "returned" };
176
+ case "stopped":
177
+ case "cancelled":
178
+ case "stuck":
179
+ return { did: "none", why: run.status.state };
180
+ default:
181
+ // Parked, failed, timed-out or interrupted: continued. A parked run whose wait doesn't take
182
+ // the event is `nothing-due` to the real client; script that.
183
+ return { did: "continued" };
184
+ }
185
+ };
186
+
187
+ /** A run only `rests` names, no wait having taken a rest: the first start of it makes it. */
188
+ const unclaimedByStart = (run: Known): boolean =>
189
+ run.scripted === true && !("args" in run) && run.rest === undefined;
190
+
191
+ /** What the real client then reads of a run whose launch just failed. */
192
+ const launchFailed = (run: Known, events: boolean, stuck: boolean): void => {
193
+ run.failed = stuck ? Math.max(run.failed, 3) : run.failed + 1;
194
+ if (run.failed >= 3) run.status = { state: "stuck", final: true };
195
+ else if (!run.attempted) run.status = { state: "interrupted", final: !events };
196
+ else if (run.rest) run.status = run.rest;
197
+ };
198
+ const launchedNow = (run: Known): void => {
199
+ run.attempted = true;
200
+ run.failed = 0;
201
+ run.status = RUNNING;
202
+ };
203
+
204
+ /** The real client's checks of a workflow it is given that need no file: its `file` and name. */
205
+ const located = (workflow: AnyWorkflow): string => {
206
+ fileOf(workflow);
207
+ return nameOf(workflow);
208
+ };
209
+
210
+ const client: Client = {
211
+ emit: async (workflow, run, name, data, given = {}) => {
212
+ const target = workflow as unknown as EventWorkflow;
213
+ const ref = `${target?.meta?.name}/${run}`;
214
+ const call = {
215
+ kind: "emit" as const,
216
+ workflow: target?.meta?.name,
217
+ ref,
218
+ name,
219
+ data: data as JsonValue,
220
+ ...(given.eventId === undefined ? {} : { eventId: given.eventId }),
221
+ ...(given.source === undefined ? {} : { source: given.source }),
222
+ };
223
+ calls.push(call);
224
+ assertEventWorkflow(target);
225
+ located(target);
226
+ checkEmit(ref, run, name, data, given);
227
+ const reply = awfEventOf(name)?.kind === "reply";
228
+ try {
229
+ checkEvent(
230
+ target,
231
+ { name, data: call.data, ...(given.eventId === undefined ? {} : { id: given.eventId }) },
232
+ reply,
233
+ );
234
+ } catch (error) {
235
+ if (error instanceof EmitError) throw new ClientError(error.code, error.message, ref);
236
+ throw error;
237
+ }
238
+ const sourceProblem = () => {
239
+ if (given.source === undefined) return;
240
+ const bad = nameProblem(given.source);
241
+ if (bad)
242
+ throw new ClientError("event", `the source ${JSON.stringify(given.source)}: ${bad}`, ref);
243
+ };
244
+ if (!reply) sourceProblem();
245
+ checkRunId(run, ref);
246
+ const result = (
247
+ fields: Omit<EmitResultJson, "v" | "ok" | "ref" | "eventId">,
248
+ ): EmitResultJson => ({
249
+ v: 1,
250
+ ok: emitOk(fields.outcome, fields.why),
251
+ ref,
252
+ eventId: given.eventId ?? `awf.fake${calls.length}`,
253
+ ...fields,
254
+ });
255
+ let state = known.get(ref);
256
+ // A run only `rests` names, no wait having taken a rest: its first event is the one that
257
+ // makes it, as a first start is, and its rests are kept.
258
+ const unclaimed = state?.scripted && state.rest === undefined ? state : undefined;
259
+ if (unclaimed) state = undefined;
260
+ if (!state && reply)
261
+ throw new ClientError("no-run", `no run ${ref}: a reply never starts one`, ref);
262
+ if (reply) sourceProblem();
263
+ const duplicate =
264
+ state !== undefined && given.eventId !== undefined && state.eventIds.has(given.eventId);
265
+ let made = false;
266
+ let answer: Omit<EmitResultJson, "v" | "ok" | "ref" | "eventId">;
267
+ if (!duplicate && state?.status.state === "completed")
268
+ answer = { outcome: "refused", did: "none", why: "returned" };
269
+ else if (!state && !(target.starts?.includes(name) ?? true))
270
+ answer = { outcome: "refused", did: "none", why: "not-a-start" };
271
+ else {
272
+ if (!state) {
273
+ // As the real client checks before it makes the run: what `prepare` makes of no argv.
274
+ try {
275
+ prepareArgs(target, [], dirname(target.file!));
276
+ } catch (error) {
277
+ throw new ClientError(
278
+ "prepare",
279
+ `${target.meta.name}: a run an event starts has no arguments, which its prepare refuses: ${messageOf(error)}`,
280
+ ref,
281
+ );
282
+ }
283
+ state = { ...fresh(), attempted: false, rests: unclaimed?.rests ?? [] };
284
+ made = true;
285
+ }
286
+ // A duplicate isn't appended, but wakes the run, as a retry after a failed launch does;
287
+ // a live run has it already, and isn't nudged.
288
+ answer = { outcome: duplicate ? "duplicate" : "accepted", ...wake(state, duplicate) };
289
+ }
290
+ const scripted = script.emit?.(call);
291
+ if (scripted) {
292
+ answer = { ...answer, ...scripted };
293
+ if (scripted.why !== undefined && scripted.did === undefined) answer.did = "none";
294
+ if (scripted.why === undefined && scripted.did !== undefined && scripted.did !== "none")
295
+ delete answer.why;
296
+ }
297
+ if (!state || answer.outcome === "refused" || (made && answer.outcome === "duplicate"))
298
+ return result(answer);
299
+ if (made && unclaimed) {
300
+ // Made in place: a handle `get` gave before sees it.
301
+ Object.assign(unclaimed, state);
302
+ state = unclaimed;
303
+ } else if (made) known.set(ref, state);
304
+ delete state.scripted;
305
+ if (given.eventId !== undefined) state.eventIds.add(given.eventId);
306
+ if (answer.did === "started" || answer.did === "continued") launchedNow(state);
307
+ else if (answer.why === "launch-failed" || answer.why === "stuck")
308
+ launchFailed(state, true, answer.why === "stuck");
309
+ return result(answer);
310
+ },
311
+ start: async (workflow, args, given = {}) => {
312
+ const target = workflow as unknown as AnyWorkflow;
313
+ const call: Extract<FakeClientCall, { kind: "start" }> = {
314
+ kind: "start",
315
+ workflow: target?.meta?.name,
316
+ args: args as JsonValue,
317
+ };
318
+ calls.push(call);
319
+ const name = located(target);
320
+ checkArgs(name, args);
321
+ let id = given.id ?? derivedId(target, args);
322
+ // As the real client generates one: an id no run here holds.
323
+ // A run `rests` names and nothing made yet is the one this start makes.
324
+ for (let n = 1; id === undefined; n++) {
325
+ const there = known.get(`${name}/fake-${n}`);
326
+ if (!there || unclaimedByStart(there)) id = `fake-${n}`;
327
+ }
328
+ const ref = `${name}/${id}`;
329
+ call.ref = ref;
330
+ checkRunId(id, ref);
331
+ let there = known.get(ref);
332
+ if (there && unclaimedByStart(there)) {
333
+ there.args = call.args;
334
+ delete there.scripted;
335
+ there = undefined;
336
+ }
337
+ if (there) {
338
+ if (
339
+ !("args" in there) ||
340
+ JSON.stringify(canonical(there.args as JsonValue)) !==
341
+ JSON.stringify(canonical(call.args))
342
+ )
343
+ throw new ClientError(
344
+ "other-args",
345
+ `${ref} is there already, with other args or none recorded`,
346
+ ref,
347
+ );
348
+ if (there.attempted)
349
+ return { ok: true, run: handle(ref, isEventWorkflow(target)), did: "exists" };
350
+ if (there.failed >= 3)
351
+ return { ok: false, run: handle(ref, isEventWorkflow(target)), did: "stuck" };
352
+ } else if (!known.has(ref)) known.set(ref, fresh(call.args));
353
+ const run = known.get(ref)!;
354
+ const did = script.start?.({ ...call, ref }) ?? { did: there ? "exists" : "started" };
355
+ const answer = {
356
+ ok: did.did === "started" || did.did === "exists",
357
+ run: handle(ref, isEventWorkflow(target)),
358
+ ...did,
359
+ } as StartResult<never>;
360
+ if (answer.did === "launch-failed" || answer.did === "stuck") {
361
+ if (!there) run.attempted = false;
362
+ launchFailed(run, isEventWorkflow(target), answer.did === "stuck");
363
+ } else launchedNow(run);
364
+ return answer;
365
+ },
366
+ get: async (workflow, run) => {
367
+ const ref = `${(workflow as unknown as AnyWorkflow)?.meta?.name}/${run}`;
368
+ calls.push({ kind: "get", workflow: (workflow as unknown as AnyWorkflow)?.meta?.name, ref });
369
+ located(workflow as unknown as AnyWorkflow);
370
+ checkRunId(run, ref);
371
+ if (!known.has(ref)) throw new ClientError("no-run", `no run ${ref}`, ref);
372
+ return handle(ref, isEventWorkflow(workflow as unknown as AnyWorkflow));
373
+ },
374
+ inbox: async (workflow, given = {}) => {
375
+ const call = {
376
+ kind: "inbox" as const,
377
+ workflow: (workflow as unknown as AnyWorkflow)?.meta?.name,
378
+ ...(given.after === undefined ? {} : { after: given.after }),
379
+ };
380
+ calls.push(call);
381
+ located(workflow as unknown as AnyWorkflow);
382
+ checkCursor(given.after);
383
+ return script.inbox?.(call) ?? [];
384
+ },
385
+ };
386
+ return Object.assign(client, { calls });
387
+ }
@@ -16,8 +16,8 @@ import {
16
16
  type Effort,
17
17
  type JsonObject,
18
18
  type KeepPane,
19
- type PaneLayout,
20
19
  type PaneSpec,
20
+ type PaneWorkspace,
21
21
  placementOf,
22
22
  positiveDurationMilliseconds,
23
23
  } from "@agentswf/contract/workflow";
@@ -39,14 +39,11 @@ import {
39
39
  type FakeFork,
40
40
  type FakeSet,
41
41
  } from "@agentswf/harness/testing";
42
- import { submitControl, submitResult, submitWaiting } from "../agent-cli/client";
43
- import type { Deprecated } from "../deprecations";
42
+ import { submitControl } from "../agent-cli/client";
44
43
  import {
45
- deprecatedAs,
46
44
  type Message,
47
45
  type MessageOutcome,
48
46
  type MessageScripts,
49
- noAnswer,
50
47
  outcomeOf,
51
48
  type ReceivedMessage,
52
49
  type Scripts,
@@ -92,7 +89,6 @@ export type OpenedAgent = {
92
89
  key: string;
93
90
  execution: AgentExecution;
94
91
  instructions?: string;
95
- labels?: JsonObject;
96
92
  /** The skills it was given by name; absent when the workflow named none. */
97
93
  skills?: readonly string[];
98
94
  /** The sandbox it ran in; absent on the host. */
@@ -107,19 +103,21 @@ export type OpenedAgent = {
107
103
  * host has no screen, so nothing says where a pane landed or whether a `beside` fell back.
108
104
  */
109
105
  pane?: PaneSpec;
110
- /** What the host was asked, a deprecated `layout`'s session and workspace included. */
111
- layout?: PaneLayout;
112
- /** Why the engine already knew `layout` could not be used: a `beside` an agent with no pane. */
113
- layoutFallback?: string;
114
- keepPane?: KeepPane;
115
- /** Its pane was kept when it was done: `keepPane` asked, and how its last operation ended let it. */
106
+ /**
107
+ * The run's `panes` as the host was asked to apply them to this pane: the `workspace` its tab
108
+ * opens in, which a `beside` takes from the pane it splits, and whether it is `keep`.
109
+ */
110
+ panes?: { workspace?: PaneWorkspace; keep?: KeepPane };
111
+ /** Why the engine already knew `pane` could not be used: a `beside` an agent with no pane. */
112
+ paneFallback?: string;
113
+ /** Its pane was kept when it was done: `panes.keep` asked, and its last operation's end let it. */
116
114
  keptPane?: true;
117
115
  };
118
116
 
119
117
  /** The sandbox an agent ran in, as the run's record keeps it. */
120
118
  export type AgentSandbox = Pick<SandboxRecord, "key" | "provider" | "spec" | "domains">;
121
119
 
122
- export type ScriptedHost = {
120
+ type ScriptedHost = {
123
121
  factory: AgentRunHostFactory;
124
122
  turns: TestTurn[];
125
123
  compactions: CompactionRecord[];
@@ -149,8 +147,6 @@ export function createScriptedHost(
149
147
  onActivity(): void;
150
148
  /** The script could not meet a turn: the test fails. */
151
149
  onScriptError(message: string): void;
152
- /** A script ended a turn through a deprecated name, for the run to log. */
153
- onDeprecated(name: Deprecated, subject?: string): void;
154
150
  /** A turn began, before its script answers: what a test sends during it goes now. */
155
151
  onTurnStarted?(turn: TestTurn): Promise<void>;
156
152
  },
@@ -222,13 +218,10 @@ export function createScriptedHost(
222
218
  failTest(why);
223
219
  throw new Error(why);
224
220
  }
225
- if (options.timeout != null && options.timeoutMs != null)
226
- throw new Error(`a ${command}'s timeout is given alone: timeoutMs is its deprecated form`);
227
221
  const timeoutMs =
228
222
  options.timeout == null
229
- ? (options.timeoutMs ?? undefined)
223
+ ? undefined
230
224
  : positiveDurationMilliseconds(options.timeout, `a ${command}'s timeout`);
231
- if (options.timeoutMs != null) events.onDeprecated("timeout-ms", `on a script's ${command}`);
232
225
  const call = submitControl(operation.binding.endpoint, {
233
226
  version: WIRE_VERSION,
234
227
  command,
@@ -361,8 +354,6 @@ export function createScriptedHost(
361
354
  reply: (text, options) => turn.reply(woke!.from, text, options),
362
355
  send: turn.send,
363
356
  tool: turn.tool,
364
- silent: deprecatedAs("message-silent", noAnswer.silent),
365
- fail: deprecatedAs("message-fail", noAnswer.failed),
366
357
  } satisfies ReceivedMessage,
367
358
  operation.message!,
368
359
  );
@@ -441,7 +432,7 @@ export function createScriptedHost(
441
432
  // As an agent's `wfa send` returns before it can run `wfa result`.
442
433
  if (invocation) await Promise.allSettled([...invocation.calls]);
443
434
  const { endpoint, operationId } = context.binding!;
444
- const response = await submitResult(endpoint, {
435
+ const response = await submitControl(endpoint, {
445
436
  version: WIRE_VERSION,
446
437
  command: "result",
447
438
  operationId,
@@ -468,12 +459,11 @@ export function createScriptedHost(
468
459
  };
469
460
  }
470
461
  const { ending } = step;
471
- if (step.deprecated) events.onDeprecated(step.deprecated);
472
462
  if (ending.kind === "waiting") {
473
463
  return {
474
464
  act: async () => {
475
465
  const { endpoint, operationId } = context.binding!;
476
- const response = await submitWaiting(endpoint, {
466
+ const response = await submitControl(endpoint, {
477
467
  version: WIRE_VERSION,
478
468
  command: "waiting",
479
469
  operationId,
@@ -568,7 +558,6 @@ export function createScriptedHost(
568
558
  return { summary: step.value };
569
559
  }
570
560
  const { ending } = step;
571
- if (step.deprecated) events.onDeprecated(step.deprecated);
572
561
  if (ending.kind === "waiting") return failTest("compaction cannot report waiting");
573
562
  if (ending.kind === "hang") {
574
563
  return {
@@ -761,15 +750,13 @@ export function createScriptedHost(
761
750
  key: request.key,
762
751
  execution: request.execution,
763
752
  ...(request.instructions ? { instructions: request.instructions } : {}),
764
- ...(request.labels ? { labels: request.labels } : {}),
765
753
  ...(request.skills ? { skills: request.skills.names } : {}),
766
754
  ...(request.continues && forks.has(request.continues.sessionRef)
767
755
  ? { forkedFrom: forks.get(request.continues.sessionRef)! }
768
756
  : {}),
769
757
  ...paneOf(request),
770
- ...(request.layout ? { layout: request.layout } : {}),
771
- ...(request.layoutFallback ? { layoutFallback: request.layoutFallback } : {}),
772
- ...(request.keepPane ? { keepPane: request.keepPane } : {}),
758
+ ...panesOf(request),
759
+ ...(request.layoutFallback ? { paneFallback: request.layoutFallback } : {}),
773
760
  };
774
761
  agents.push(opened);
775
762
  return {
@@ -885,6 +872,18 @@ function paneOf({ layout, front }: HarnessActivation): { pane?: PaneSpec } {
885
872
  return { pane: { ...(tab === undefined ? {} : { tab }), ...(front ? { front } : {}) } };
886
873
  }
887
874
 
875
+ /** The run's `panes` as the host was asked to apply them to this pane. */
876
+ function panesOf({ layout, keepPane }: HarnessActivation): Pick<OpenedAgent, "panes"> {
877
+ const workspace = layout?.beside === undefined ? layout?.workspace : undefined;
878
+ if (workspace === undefined && keepPane === undefined) return {};
879
+ return {
880
+ panes: {
881
+ ...(workspace === undefined ? {} : { workspace }),
882
+ ...(keepPane === undefined ? {} : { keep: keepPane }),
883
+ },
884
+ };
885
+ }
886
+
888
887
  /** What the pane host refuses before it opens a pane. */
889
888
  function paneRefusal(activation: HarnessActivation): string | undefined {
890
889
  const { harness } = activation.execution;
@@ -1,13 +1,18 @@
1
1
  // What `agentswf/testing` publishes, which `awf test` serves too: nothing here is the engine's own.
2
- import type { EventSchemas, JsonValue, Workflow, WorkflowMeta } from "@agentswf/contract/workflow";
2
+ import type { EventSchemas, JsonValue, Workflow } from "@agentswf/contract/workflow";
3
3
  import { runTest, type TestOptions, type TestRun, testCall } from "./run-test";
4
4
 
5
5
  export type { DecisionRequest, DecisionScript } from "./decisions";
6
+ export {
7
+ type FakeClient,
8
+ type FakeClientCall,
9
+ type FakeClientScript,
10
+ fakeClient,
11
+ } from "./fake-client";
6
12
  export type { TestTurn } from "./host";
7
13
  export type {
8
14
  EarlierAttempt,
9
15
  EmittedEvent,
10
- EventTestOptions,
11
16
  FileTree,
12
17
  MessageRecord,
13
18
  ScriptedEvent,
@@ -25,7 +30,6 @@ export {
25
30
  type ParticipantRoute,
26
31
  type ParticipantScript,
27
32
  type ReceivedMessage,
28
- reply,
29
33
  type Script,
30
34
  type SendOptions,
31
35
  type TestTurnOutcome,
@@ -33,9 +37,6 @@ export {
33
37
  type Turn,
34
38
  } from "./script";
35
39
 
36
- /** What an array, `null` or a primitive is: args the deprecated form may give second. */
37
- type NotAnObject = null | string | number | boolean | JsonValue[];
38
-
39
40
  /**
40
41
  * Runs `workflow` through the real engine, answering each agent from its script and each decision
41
42
  * from its own. It rejects when a script cannot meet what the workflow asked, or leaves a list
@@ -56,28 +57,9 @@ export async function testWorkflow<Args extends JsonValue, Result extends JsonVa
56
57
  ? [options?: TestOptions<NoInfer<Args>>]
57
58
  : [options: TestOptions<NoInfer<Args>>]
58
59
  ): Promise<TestRun<Result>>;
59
- /** @deprecated Give args among the options: `testWorkflow(workflow, { args, … })`. */
60
- export async function testWorkflow<Args extends JsonValue, Result extends JsonValue>(
61
- workflow: DeprecatedWorkflow<Args, Result>,
62
- args: NoInfer<Args>,
63
- options: Omit<TestOptions<NoInfer<Args>>, "args"> | undefined,
64
- ): Promise<TestRun<Result>>;
65
- /** @deprecated Give args among the options: `testWorkflow(workflow, { args })`. */
66
- export async function testWorkflow<Args extends NotAnObject, Result extends JsonValue>(
67
- workflow: DeprecatedWorkflow<Args, Result>,
68
- args: Args,
69
- ): Promise<TestRun<Result>>;
70
60
  export async function testWorkflow(
71
- workflow: DeprecatedWorkflow<JsonValue, JsonValue>,
61
+ workflow: Parameters<typeof runTest>[0],
72
62
  ...given: unknown[]
73
63
  ): Promise<TestRun<JsonValue>> {
74
- const { options, used } = testCall(workflow, given);
75
- return runTest(workflow, options, { used });
64
+ return runTest(workflow, testCall(workflow, given));
76
65
  }
77
-
78
- /** What the deprecated form still takes: a workflow, or its `meta` and `run` alone. */
79
- type DeprecatedWorkflow<Args extends JsonValue, Result extends JsonValue> = {
80
- readonly meta: WorkflowMeta;
81
- readonly events?: undefined;
82
- run: Workflow<Args, Result>["run"];
83
- };