@agentswf/engine 0.0.0-stage → 0.1.0-beta.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 (77) hide show
  1. package/package.json +39 -4
  2. package/src/accounting/format.ts +252 -0
  3. package/src/accounting/plans.ts +38 -0
  4. package/src/accounting/prices.ts +98 -0
  5. package/src/accounting/summary.ts +385 -0
  6. package/src/accounting/tokens.ts +34 -0
  7. package/src/agent-launcher.ts +127 -0
  8. package/src/allowance-command.ts +105 -0
  9. package/src/attempt-ending.ts +198 -0
  10. package/src/attempt-output.ts +132 -0
  11. package/src/attempt-view.ts +362 -0
  12. package/src/canonical-json.ts +12 -0
  13. package/src/control-plane.ts +424 -0
  14. package/src/deadlines.ts +106 -0
  15. package/src/decisions/directory.ts +432 -0
  16. package/src/decisions/fake.ts +60 -0
  17. package/src/decisions/openrouter.ts +161 -0
  18. package/src/decisions/seam.ts +70 -0
  19. package/src/display-path.ts +16 -0
  20. package/src/duration.ts +12 -0
  21. package/src/errors.ts +4 -0
  22. package/src/files.ts +78 -0
  23. package/src/herdr-placement.ts +182 -0
  24. package/src/herdr-run-session.ts +530 -0
  25. package/src/herdr-workspace-marks.ts +194 -0
  26. package/src/here.ts +289 -0
  27. package/src/index.ts +5 -0
  28. package/src/jsonl.ts +74 -0
  29. package/src/logs-command.ts +605 -0
  30. package/src/machine.ts +27 -0
  31. package/src/message-operation.ts +245 -0
  32. package/src/messaging/limits.ts +35 -0
  33. package/src/messaging/messaging.ts +1303 -0
  34. package/src/messaging/routes.ts +79 -0
  35. package/src/messaging/text.ts +251 -0
  36. package/src/operation-events.ts +182 -0
  37. package/src/operation-liveness.ts +674 -0
  38. package/src/operator-aliases.ts +10 -0
  39. package/src/operator-cli.ts +569 -0
  40. package/src/operator-runtime.ts +503 -0
  41. package/src/pane-layout.ts +125 -0
  42. package/src/progress-view.ts +568 -0
  43. package/src/result-slots.ts +325 -0
  44. package/src/result-validation.ts +47 -0
  45. package/src/run-command.ts +241 -0
  46. package/src/run-commands.ts +215 -0
  47. package/src/run-event-log.ts +72 -0
  48. package/src/run-prepare.ts +178 -0
  49. package/src/run-progress.ts +137 -0
  50. package/src/run-read.ts +425 -0
  51. package/src/run-state.ts +120 -0
  52. package/src/run-usage.ts +393 -0
  53. package/src/run-view.ts +225 -0
  54. package/src/runs-command.ts +356 -0
  55. package/src/runs.ts +645 -0
  56. package/src/sandbox-homes.ts +211 -0
  57. package/src/sandboxes.ts +516 -0
  58. package/src/skills/fetch.ts +305 -0
  59. package/src/skills/run-skills.ts +181 -0
  60. package/src/skills/sources.ts +87 -0
  61. package/src/skills/tree.ts +60 -0
  62. package/src/stage-ledger.ts +535 -0
  63. package/src/stage-plan.ts +127 -0
  64. package/src/stage-schema.ts +88 -0
  65. package/src/stopped.ts +36 -0
  66. package/src/test-command.ts +110 -0
  67. package/src/testing/operation-events.ts +89 -0
  68. package/src/testing.ts +164 -0
  69. package/src/workflow-loader.ts +113 -0
  70. package/src/workflow-runner.ts +3665 -0
  71. package/src/workflow-test-preload.ts +13 -0
  72. package/src/workflow-testing/decisions.ts +129 -0
  73. package/src/workflow-testing/host.ts +811 -0
  74. package/src/workflow-testing/index.ts +420 -0
  75. package/src/workflow-testing/script.ts +447 -0
  76. package/src/workflow-testing/workflow-testing.typecheck.ts +47 -0
  77. package/README.md +0 -3
@@ -0,0 +1,674 @@
1
+ import type { OperationLivenessKind } from "@agentswf/contract/records";
2
+ import type { JsonSchema } from "@agentswf/contract/schema";
3
+ import type { AbsoluteDeadline, TurnLogin } from "@agentswf/contract/workflow";
4
+ import type {
5
+ HarnessReleaseDisposition,
6
+ HarnessTurn,
7
+ HarnessTurnOutcome,
8
+ } from "@agentswf/harness/adapter";
9
+ import { scheduleAt } from "./deadlines";
10
+ import { messageOf } from "./errors";
11
+ import type { Pending, TurnEnd } from "./messaging/messaging";
12
+ import type { ResultSlotEvent, ResultSlotRegistry, ResultSlotSettlement } from "./result-slots";
13
+
14
+ export type LivenessPolicy = {
15
+ quietMs: number;
16
+ responseMs: number;
17
+ deliveryMs: number;
18
+ releaseMs: number;
19
+ };
20
+ export const DEFAULT_LIVENESS_POLICY: LivenessPolicy = {
21
+ quietMs: 30_000,
22
+ responseMs: 120_000,
23
+ deliveryMs: 30_000,
24
+ releaseMs: 30_000,
25
+ };
26
+ export type OperationPhase =
27
+ | "working"
28
+ | "waiting"
29
+ | "awaiting-reply"
30
+ | "check-in-pending"
31
+ | "check-in-response"
32
+ | "releasing";
33
+ export type LivenessEvent = {
34
+ kind: OperationLivenessKind;
35
+ at: number;
36
+ sequence: number;
37
+ reason?: string;
38
+ until?: number;
39
+ peer?: string;
40
+ };
41
+
42
+ /**
43
+ * An operation's messages, which a turn that ends without a result goes on to before 021's
44
+ * check-in: what continues it, a wait for an answer, or a reminder of one it owes.
45
+ */
46
+ export type OperationMessages = {
47
+ /** What is held for it, deciding nothing. */
48
+ pending(): Pending;
49
+ /** The answer it awaits, while it does. */
50
+ awaiting(): { peer: string; until: number } | undefined;
51
+ turnEnded(): TurnEnd;
52
+ /**
53
+ * The turn that carries what is held, or the reminder, once `turn` has settled; it says why it
54
+ * continued.
55
+ */
56
+ continueWith(
57
+ turn: HarnessTurn,
58
+ sequence: number,
59
+ signal: AbortSignal,
60
+ reminder?: string,
61
+ ): Promise<HarnessTurn>;
62
+ /** A reminder's response window passed: what it owed then fails, its askers told. */
63
+ reminderExpired(): void;
64
+ /** Wakes the supervisor when something arrives for it. */
65
+ onChange(wake: () => void): () => void;
66
+ };
67
+ export type SupervisedOutcome = {
68
+ kind: "answered" | "unanswered" | "timed-out" | "cancelled" | "blocked" | "failed";
69
+ value?: unknown;
70
+ reason: string;
71
+ cleanupUnresolved: boolean;
72
+ charges: number[];
73
+ deliveredAt?: number;
74
+ settledAt: number;
75
+ /** A failed turn whose harness showed it cannot sign in. */
76
+ login?: TurnLogin;
77
+ };
78
+
79
+ export type OperationStop = { kind: "cancelled" | "timed-out"; reason: string };
80
+
81
+ type Options = {
82
+ operationId: string;
83
+ agentId: string;
84
+ question: string;
85
+ schema: JsonSchema;
86
+ deadline: AbsoluteDeadline;
87
+ slots: ResultSlotRegistry;
88
+ nudge: boolean;
89
+ waitingSupported?: boolean;
90
+ start(): Promise<HarnessTurn>;
91
+ successor(turn: HarnessTurn, sequence: number, signal: AbortSignal): Promise<HarnessTurn>;
92
+ onStop(cancel: (stop: OperationStop) => Promise<void>): () => void;
93
+ event?(event: LivenessEvent): void;
94
+ phase?(phase: OperationPhase, reason?: string, until?: number): void;
95
+ messages?: OperationMessages;
96
+ /** Once, when the harness first accepts the operation. */
97
+ delivered?(at: number): void;
98
+ policy?: LivenessPolicy;
99
+ now?: () => number;
100
+ schedule?: (at: number, action: () => void) => () => void;
101
+ };
102
+
103
+ /** One operation owns its slot and native handles until bounded release completes. */
104
+ export async function superviseOperation(options: Options): Promise<SupervisedOutcome> {
105
+ const now = options.now ?? Date.now;
106
+ const schedule =
107
+ options.schedule ?? ((at, action) => scheduleAt({ unixMilliseconds: at }, action));
108
+ const policy = options.policy ?? DEFAULT_LIVENESS_POLICY;
109
+ const waitingSupported = options.waitingSupported !== false;
110
+ const deadline = options.deadline.unixMilliseconds;
111
+ let wake = Promise.withResolvers<void>();
112
+ const stopped = Promise.withResolvers<void>();
113
+ const released = Promise.withResolvers<void>();
114
+ const changed = () => wake.resolve();
115
+ let sequence = 0;
116
+ let revision = 0;
117
+ let admitted = false;
118
+ let settlement: ResultSlotSettlement | undefined;
119
+ let stop: OperationStop | undefined;
120
+ let waitUntil: number | undefined;
121
+ let quietUntil: number | undefined;
122
+ let responseUntil: number | undefined;
123
+ let deliveryUntil: number | undefined;
124
+ let receiptUntil: number | undefined;
125
+ let dispatched = false;
126
+ let receiptSatisfied = false;
127
+ /** The answer it waits on, once a turn has ended awaiting it: no check-in while it does. */
128
+ let awaitingPeer: { peer: string; until: number } | undefined;
129
+ /** When a reminder's response window closes: what it owes fails then. */
130
+ let reminderUntil: number | undefined;
131
+ /** Deliveries not yet dispatched, which only a stop aborts. */
132
+ const continuations = new Set<AbortController>();
133
+ let deliveredAt: number | undefined;
134
+ const deliver = (at: number | undefined) => {
135
+ if (deliveredAt !== undefined || at === undefined) return;
136
+ deliveredAt = at;
137
+ options.delivered?.(at);
138
+ };
139
+ let dispatchStartedAt: number | undefined;
140
+ let held: HarnessTurn | undefined;
141
+ let acquiring: Promise<HarnessTurn> | undefined;
142
+ let acquisitionError: unknown;
143
+ let native: HarnessTurnOutcome | undefined;
144
+ let ignoredCancellation = false;
145
+ let controller: AbortController | undefined;
146
+ let finished = false;
147
+ let abandonedAcquisition = false;
148
+ let answerRelease:
149
+ | Promise<import("@agentswf/harness/adapter").HarnessReleaseDisposition>
150
+ | undefined;
151
+ let answerReleaseDeadline: number | undefined;
152
+ const charges: number[] = [];
153
+ const emit = (
154
+ kind: OperationLivenessKind,
155
+ reason?: string,
156
+ until?: number,
157
+ at = now(),
158
+ peer?: string,
159
+ ) =>
160
+ options.event?.({
161
+ kind,
162
+ at,
163
+ sequence,
164
+ ...(reason === undefined ? {} : { reason }),
165
+ ...(until === undefined ? {} : { until }),
166
+ ...(peer === undefined ? {} : { peer }),
167
+ });
168
+ /**
169
+ * A delivery, or the reminder, as the turn after the one that ended: a working turn, no check-in.
170
+ * Unlike a check-in it is not withdrawn when a result or a wait arrives first: what it carries is
171
+ * already shown, and only a stop aborts it.
172
+ */
173
+ const continueWithMessages = (messages: OperationMessages, reminder?: string) => {
174
+ waitUntil = quietUntil = undefined;
175
+ awaitingPeer = undefined;
176
+ sequence += 1;
177
+ const continuation = new AbortController();
178
+ continuations.add(continuation);
179
+ options.phase?.("working");
180
+ if (reminder !== undefined) {
181
+ emit("reminder-due");
182
+ // It has the check-in's response window to answer in.
183
+ reminderUntil = now() + policy.responseMs;
184
+ }
185
+ observe(
186
+ Promise.resolve()
187
+ .then(() => messages.continueWith(held!, sequence, continuation.signal, reminder))
188
+ .finally(() => continuations.delete(continuation)),
189
+ false,
190
+ );
191
+ };
192
+ const clearAnswerTimers = () => {
193
+ quietUntil = responseUntil = deliveryUntil = receiptUntil = undefined;
194
+ };
195
+ const cancelPending = () => {
196
+ if (!dispatched && controller) {
197
+ ignoredCancellation = true;
198
+ controller.abort();
199
+ }
200
+ };
201
+ const onSlot = (event: ResultSlotEvent) => {
202
+ if (finished) return;
203
+ if (event.kind === "waiting") {
204
+ revision += 1;
205
+ waitUntil = event.waitUntil;
206
+ receiptSatisfied = true;
207
+ clearAnswerTimers();
208
+ cancelPending();
209
+ options.phase?.("waiting", event.reason, event.waitUntil);
210
+ emit("waiting", event.reason, event.waitUntil);
211
+ } else if (event.kind === "admitted") {
212
+ admitted = true;
213
+ deliver(dispatchStartedAt ?? event.admittedAt);
214
+ waitUntil = undefined;
215
+ clearAnswerTimers();
216
+ cancelPending();
217
+ beginAnswerRelease();
218
+ emit("admitted", undefined, undefined, event.admittedAt);
219
+ } else if (event.kind === "persisted") {
220
+ emit("persisted", undefined, undefined, event.accepted.acceptedAt);
221
+ }
222
+ changed();
223
+ };
224
+ const removeStop = options.onStop((request) => {
225
+ if (finished) return released.promise;
226
+ stop ??= request;
227
+ stopped.resolve();
228
+ void options.slots.close(options.operationId);
229
+ cancelPending();
230
+ for (const continuation of continuations) continuation.abort();
231
+ changed();
232
+ return released.promise;
233
+ });
234
+ let slot: Awaited<ReturnType<ResultSlotRegistry["open"]>>;
235
+ try {
236
+ slot = await options.slots.open({
237
+ operationId: options.operationId,
238
+ agentId: options.agentId,
239
+ question: options.question,
240
+ schema: options.schema,
241
+ deadline: options.deadline,
242
+ allowWaiting: waitingSupported,
243
+ onEvent: onSlot,
244
+ });
245
+ } catch (error) {
246
+ finished = true;
247
+ removeStop();
248
+ released.resolve();
249
+ return terminal(stop?.kind ?? "failed", stop?.reason ?? messageOf(error));
250
+ }
251
+ emit("opened", undefined, deadline);
252
+ const unwatch = options.messages?.onChange(changed);
253
+ if (stop) void options.slots.close(options.operationId);
254
+ void slot.settled.then((value) => {
255
+ settlement = value;
256
+ changed();
257
+ });
258
+ const observe = (opening: Promise<HarnessTurn>, check: boolean) => {
259
+ acquiring = opening;
260
+ const current = sequence;
261
+ const grantRevision = revision;
262
+ let accepted = false;
263
+ let received = false;
264
+ receiptUntil = undefined;
265
+ const boundMissingReceipt = () => {
266
+ if (
267
+ result === undefined &&
268
+ check &&
269
+ waitingSupported &&
270
+ accepted &&
271
+ !received &&
272
+ native?.state === "completed" &&
273
+ !admitted &&
274
+ !receiptSatisfied &&
275
+ grantRevision === revision &&
276
+ receiptUntil === undefined
277
+ )
278
+ receiptUntil = Math.min(deadline, now() + policy.responseMs);
279
+ };
280
+ const priorNative = native;
281
+ native = undefined;
282
+ dispatched = false;
283
+ receiptSatisfied = false;
284
+ ignoredCancellation = false;
285
+ void opening.then(
286
+ (turn) => {
287
+ if (finished || abandonedAcquisition) {
288
+ void turn
289
+ .release("operation already ended", { unixMilliseconds: now() + policy.releaseMs })
290
+ .catch(() => undefined);
291
+ return;
292
+ }
293
+ held = turn;
294
+ acquiring = undefined;
295
+ if (admitted) beginAnswerRelease();
296
+ const valid = () => !finished && current === sequence;
297
+ void turn.settled.then(
298
+ (outcome) => {
299
+ if (!valid()) return;
300
+ native = outcome;
301
+ charges.push(...outcome.chargesUsd);
302
+ if (
303
+ result === undefined &&
304
+ outcome.state === "completed" &&
305
+ !admitted &&
306
+ waitUntil === undefined &&
307
+ responseUntil === undefined &&
308
+ !check
309
+ )
310
+ quietUntil = now() + (waitingSupported ? policy.quietMs : 0);
311
+ if (result === undefined && !waitingSupported && check && outcome.state === "completed")
312
+ responseUntil = now();
313
+ boundMissingReceipt();
314
+ changed();
315
+ },
316
+ (error) => {
317
+ if (valid()) {
318
+ acquisitionError = error;
319
+ changed();
320
+ }
321
+ },
322
+ );
323
+ if (turn.delivery) {
324
+ void turn.delivery.dispatched.then((at) => {
325
+ if (!valid()) return;
326
+ dispatched = true;
327
+ deliver(at);
328
+ if (
329
+ result === undefined &&
330
+ !admitted &&
331
+ !receiptSatisfied &&
332
+ grantRevision === revision
333
+ )
334
+ deliveryUntil = Math.min(deadline, at + policy.deliveryMs);
335
+ emit("dispatched", undefined, deliveryUntil);
336
+ changed();
337
+ });
338
+ void turn.delivery.accepted.then(() => {
339
+ if (!valid()) return;
340
+ accepted = true;
341
+ boundMissingReceipt();
342
+ deliveryUntil = undefined;
343
+ emit("queue-accepted");
344
+ changed();
345
+ });
346
+ void turn.delivery.received.then((at) => {
347
+ if (!valid()) return;
348
+ received = true;
349
+ receiptUntil = undefined;
350
+ deliveryUntil = undefined;
351
+ if (
352
+ result === undefined &&
353
+ check &&
354
+ !admitted &&
355
+ !receiptSatisfied &&
356
+ grantRevision === revision
357
+ ) {
358
+ responseUntil = Math.min(deadline, at + policy.responseMs);
359
+ options.phase?.("check-in-response", undefined, responseUntil);
360
+ }
361
+ emit("received", undefined, responseUntil);
362
+ changed();
363
+ });
364
+ void turn.delivery.failed.then((reason) => {
365
+ if (!valid()) return;
366
+ acquisitionError = new Error(`delivery observation failed: ${reason}`);
367
+ changed();
368
+ });
369
+ } else if (waitingSupported) {
370
+ acquisitionError = new Error("waiting operation has no delivery evidence");
371
+ changed();
372
+ } else {
373
+ deliver(now());
374
+ }
375
+ changed();
376
+ },
377
+ (error) => {
378
+ if (finished || abandonedAcquisition) return;
379
+ acquiring = undefined;
380
+ if (
381
+ ignoredCancellation &&
382
+ controller?.signal.aborted &&
383
+ error instanceof Error &&
384
+ error.name === "AbortError"
385
+ ) {
386
+ native = priorNative;
387
+ } else {
388
+ acquisitionError = error;
389
+ }
390
+ if (admitted) beginAnswerRelease();
391
+ changed();
392
+ },
393
+ );
394
+ };
395
+ let result: SupervisedOutcome | undefined;
396
+ try {
397
+ if (!stop && now() < deadline) {
398
+ options.phase?.("working");
399
+ observe(
400
+ Promise.resolve().then(() => {
401
+ if (stop || now() >= deadline) throw new Error("operation stopped before dispatch");
402
+ dispatchStartedAt = now();
403
+ return options.start();
404
+ }),
405
+ false,
406
+ );
407
+ }
408
+ for (;;) {
409
+ const at = now();
410
+ if (stop) {
411
+ result = terminal(stop.kind, stop.reason);
412
+ break;
413
+ }
414
+ if (settlement?.kind === "failed") {
415
+ result = terminal("failed", settlement.reason);
416
+ break;
417
+ }
418
+ if (settlement?.kind === "accepted") {
419
+ result = { ...terminal("answered", "answer saved"), value: settlement.value };
420
+ break;
421
+ }
422
+ if (!admitted && (at >= deadline || settlement?.kind === "expired")) {
423
+ result = terminal("timed-out", "operation deadline exceeded");
424
+ break;
425
+ }
426
+ if (settlement?.kind === "closed") {
427
+ result = terminal("cancelled", "operation closed");
428
+ break;
429
+ }
430
+ if (acquisitionError && !admitted) {
431
+ result = terminal("failed", String(acquisitionError));
432
+ break;
433
+ }
434
+ if (!admitted) {
435
+ if (
436
+ native &&
437
+ native.state !== "completed" &&
438
+ !(ignoredCancellation && native.state === "cancelled")
439
+ ) {
440
+ result = {
441
+ ...terminal(
442
+ native.state === "timed-out" ? "timed-out" : native.state,
443
+ native.detail ?? `native turn ${native.state}`,
444
+ ),
445
+ ...(native.state === "failed" && native.login ? { login: native.login } : {}),
446
+ };
447
+ break;
448
+ }
449
+ if (deliveryUntil !== undefined && at >= deliveryUntil) {
450
+ result = terminal("failed", "prompt delivery could not be confirmed");
451
+ break;
452
+ }
453
+ if (receiptUntil !== undefined && at >= receiptUntil) {
454
+ result = terminal(
455
+ "failed",
456
+ "check-in receipt could not be confirmed after native completion",
457
+ );
458
+ break;
459
+ }
460
+ if (responseUntil !== undefined && at >= responseUntil) {
461
+ result = terminal("unanswered", "check-in received no waiting declaration or result");
462
+ break;
463
+ }
464
+ const idle = native !== undefined && !acquiring;
465
+ const settledTurn =
466
+ idle &&
467
+ responseUntil === undefined &&
468
+ deliveryUntil === undefined &&
469
+ receiptUntil === undefined;
470
+ const messages = options.messages;
471
+ // Before any check-in: what continues it goes now, cutting a `wf waiting` short, and an
472
+ // operation awaiting an answer waits for it with no check-in.
473
+ if (messages && settledTurn) {
474
+ if (messages.pending() === "continues") {
475
+ continueWithMessages(messages);
476
+ continue;
477
+ }
478
+ const awaited = messages.awaiting();
479
+ if (awaited) {
480
+ if (awaitingPeer?.peer !== awaited.peer || awaitingPeer.until !== awaited.until) {
481
+ awaitingPeer = awaited;
482
+ options.phase?.("awaiting-reply", awaited.peer, awaited.until);
483
+ emit("awaiting-reply", undefined, awaited.until, now(), awaited.peer);
484
+ }
485
+ waitUntil = quietUntil = undefined;
486
+ } else awaitingPeer = undefined;
487
+ }
488
+ const due =
489
+ waitUntil !== undefined ? at >= waitUntil : quietUntil !== undefined && at >= quietUntil;
490
+ if (idle && due && responseUntil === undefined && deliveryUntil === undefined) {
491
+ const next = messages?.turnEnded();
492
+ if (next?.kind === "continue" || next?.kind === "remind") {
493
+ continueWithMessages(messages!, next.kind === "remind" ? next.text : undefined);
494
+ continue;
495
+ }
496
+ if (!options.nudge) {
497
+ result = terminal("unanswered", "agent settled without an accepted result");
498
+ break;
499
+ }
500
+ waitUntil = quietUntil = undefined;
501
+ sequence += 1;
502
+ controller = new AbortController();
503
+ options.phase?.("check-in-pending");
504
+ emit("check-in-due");
505
+ observe(
506
+ Promise.resolve().then(() => options.successor(held!, sequence, controller!.signal)),
507
+ true,
508
+ );
509
+ }
510
+ // Without nudges, a normal completion retains the existing immediate no-answer rule, after
511
+ // what its messages go on to: a reminder of an answer it owes still comes.
512
+ if (idle && !options.nudge && waitUntil === undefined && !awaitingPeer) {
513
+ const next = settledTurn ? messages?.turnEnded() : undefined;
514
+ if (next?.kind === "continue" || next?.kind === "remind") {
515
+ continueWithMessages(messages!, next.kind === "remind" ? next.text : undefined);
516
+ continue;
517
+ }
518
+ result = terminal("unanswered", "agent settled without an accepted result");
519
+ break;
520
+ }
521
+ if (reminderUntil !== undefined && at >= reminderUntil) {
522
+ reminderUntil = undefined;
523
+ options.messages?.reminderExpired();
524
+ }
525
+ }
526
+ // Timers against this pass's own clock: a time that came due while it ran fires at once,
527
+ // where judging it by a later reading would leave it neither due nor timed.
528
+ const candidates = admitted
529
+ ? []
530
+ : [
531
+ deadline,
532
+ waitUntil,
533
+ quietUntil,
534
+ responseUntil,
535
+ deliveryUntil,
536
+ receiptUntil,
537
+ reminderUntil,
538
+ ].filter((v): v is number => v !== undefined && v > at);
539
+ const signal = wake.promise;
540
+ const cancelTimer = candidates.length ? schedule(Math.min(...candidates), changed) : () => {};
541
+ await signal;
542
+ cancelTimer();
543
+ wake = Promise.withResolvers<void>();
544
+ }
545
+ clearAnswerTimers();
546
+ waitUntil = undefined;
547
+ cancelPending();
548
+ await options.slots.close(options.operationId);
549
+ options.phase?.("releasing");
550
+ emit("releasing");
551
+ let releaseDeadline =
552
+ now() +
553
+ (result.kind === "answered" && !stop ? policy.releaseMs : Math.min(policy.releaseMs, 5000));
554
+ if (acquiring) {
555
+ const acquisition = acquiring;
556
+ let acquired = await bounded(acquisition, releaseDeadline, result.kind === "answered");
557
+ if (!acquired && stop && result.kind === "answered") {
558
+ releaseDeadline = now() + Math.min(policy.releaseMs, 5000);
559
+ acquired = await bounded(acquisition, releaseDeadline);
560
+ }
561
+ if (acquired) held = acquired;
562
+ else if (acquiring === acquisition || !held) {
563
+ abandonedAcquisition = true;
564
+ result.cleanupUnresolved = true;
565
+ }
566
+ }
567
+ if (result.kind === "answered" && !held) result.cleanupUnresolved = true;
568
+ if (held) {
569
+ const stopping = stop !== undefined;
570
+ if (stopping)
571
+ releaseDeadline = Math.min(releaseDeadline, now() + Math.min(policy.releaseMs, 5000));
572
+ const natural = !stopping && result.kind === "answered";
573
+ let release = await releaseHeld(natural, stop?.reason ?? result.reason, releaseDeadline);
574
+ if (stop && natural) {
575
+ release = await releaseHeld(false, stop.reason, now() + Math.min(policy.releaseMs, 5000));
576
+ }
577
+ emit("release", release?.kind ?? "unresolved");
578
+ if (
579
+ release?.kind !== "released" ||
580
+ (!stop && result.kind === "answered" && release.outcome.state !== "completed")
581
+ )
582
+ result.cleanupUnresolved = true;
583
+ }
584
+ if (stop && result.kind === "answered") {
585
+ result.kind = stop.kind;
586
+ result.reason = stop.reason;
587
+ delete result.value;
588
+ }
589
+ if (result.cleanupUnresolved && result.kind !== "cancelled" && result.kind !== "timed-out") {
590
+ result.kind = "failed";
591
+ result.reason = "cleanup-unresolved";
592
+ delete result.value;
593
+ }
594
+ if (result.kind === "answered") result.settledAt = now();
595
+ if (deliveredAt !== undefined) result.deliveredAt = deliveredAt;
596
+ emit("terminal", result.reason);
597
+ return result;
598
+ } catch (error) {
599
+ clearAnswerTimers();
600
+ cancelPending();
601
+ try {
602
+ await options.slots.close(options.operationId);
603
+ } catch {
604
+ // Cleanup is already unresolved; retain the original failure and observed usage.
605
+ }
606
+ return { ...terminal("failed", messageOf(error)), cleanupUnresolved: true };
607
+ } finally {
608
+ finished = true;
609
+ unwatch?.();
610
+ removeStop();
611
+ released.resolve();
612
+ }
613
+ async function releaseHeld(
614
+ answered: boolean,
615
+ reason: string,
616
+ until: number,
617
+ ): Promise<HarnessReleaseDisposition | undefined> {
618
+ if (!held) return undefined;
619
+ if (answered) {
620
+ if (abandonedAcquisition) {
621
+ return bounded(
622
+ held.release(
623
+ reason,
624
+ { unixMilliseconds: until },
625
+ { answered: true, awaitCompletion: true },
626
+ ),
627
+ until,
628
+ );
629
+ }
630
+ beginAnswerRelease();
631
+ return answerRelease ? bounded(answerRelease, answerReleaseDeadline!, true) : undefined;
632
+ }
633
+ return bounded(held.release(reason, { unixMilliseconds: until }), until);
634
+ }
635
+ function beginAnswerRelease() {
636
+ if (finished || abandonedAcquisition || !held || acquiring || answerRelease || stop) return;
637
+ answerReleaseDeadline = now() + policy.releaseMs;
638
+ answerRelease = held.release(
639
+ "answer admitted",
640
+ { unixMilliseconds: answerReleaseDeadline },
641
+ { answered: true, awaitCompletion: true },
642
+ );
643
+ void answerRelease.catch(() => undefined);
644
+ }
645
+ function terminal(kind: SupervisedOutcome["kind"], reason: string): SupervisedOutcome {
646
+ return {
647
+ kind,
648
+ reason,
649
+ cleanupUnresolved: false,
650
+ charges,
651
+ ...(deliveredAt === undefined ? {} : { deliveredAt }),
652
+ settledAt: kind === "timed-out" ? Math.min(now(), deadline) : now(),
653
+ };
654
+ }
655
+ async function bounded<T>(
656
+ promise: Promise<T>,
657
+ until: number,
658
+ interruptOnStop = false,
659
+ ): Promise<T | undefined> {
660
+ let cancel = () => {};
661
+ const expired = new Promise<undefined>((resolve) => {
662
+ cancel = schedule(until, () => resolve(undefined));
663
+ });
664
+ try {
665
+ return await Promise.race([
666
+ promise.catch(() => undefined),
667
+ expired,
668
+ ...(interruptOnStop ? [stopped.promise.then(() => undefined)] : []),
669
+ ]);
670
+ } finally {
671
+ cancel();
672
+ }
673
+ }
674
+ }
@@ -0,0 +1,10 @@
1
+ import type { RuntimeAliases } from "@agentswf/contract/workflow";
2
+
3
+ /**
4
+ * The runtime aliases `awf run` installs. A workflow's test starts from the same ones, so an alias
5
+ * a run would not know fails the test too.
6
+ */
7
+ export const OPERATOR_ALIASES: RuntimeAliases = Object.freeze({
8
+ claude: { harness: "claude", model: "sonnet" },
9
+ codex: { harness: "codex", model: "gpt-5.6-sol" },
10
+ });