@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
@@ -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
  });
@@ -100,7 +100,7 @@ export class ParticipantScripts implements CoordinatorPort {
100
100
  const script = this.#scripts[coordinator];
101
101
  if (!script) {
102
102
  this.#events.onScriptError(
103
- `${event.kind === "route" ? "a route" : "a message"} reached coordinator "${coordinator}", which has no script under coordinators`,
103
+ `${event.kind === "route" ? "a route" : "a message"} reached participant "${coordinator}", which has no script under participants`,
104
104
  );
105
105
  return;
106
106
  }
@@ -111,7 +111,7 @@ export class ParticipantScripts implements CoordinatorPort {
111
111
  .then(() => script(event))
112
112
  .catch((error: unknown) =>
113
113
  this.#events.onScriptError(
114
- `coordinator "${coordinator}"'s script threw: ${error instanceof Error ? error.message : String(error)}`,
114
+ `participant "${coordinator}"'s script threw: ${error instanceof Error ? error.message : String(error)}`,
115
115
  ),
116
116
  )
117
117
  .finally(() => {
@@ -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
+ }