@agentswf/engine 0.0.0-stage → 0.1.0-beta.1

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 (82) 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/init-command.ts +240 -0
  29. package/src/init-template/AGENTS.md.template +16 -0
  30. package/src/init-template/hello/workflow.test.ts.template +17 -0
  31. package/src/init-template/hello/workflow.ts.template +26 -0
  32. package/src/init-template/tsconfig.json.template +11 -0
  33. package/src/jsonl.ts +74 -0
  34. package/src/logs-command.ts +605 -0
  35. package/src/machine.ts +27 -0
  36. package/src/message-operation.ts +245 -0
  37. package/src/messaging/limits.ts +35 -0
  38. package/src/messaging/messaging.ts +1303 -0
  39. package/src/messaging/routes.ts +79 -0
  40. package/src/messaging/text.ts +251 -0
  41. package/src/operation-events.ts +182 -0
  42. package/src/operation-liveness.ts +674 -0
  43. package/src/operator-aliases.ts +10 -0
  44. package/src/operator-cli.ts +600 -0
  45. package/src/operator-runtime.ts +503 -0
  46. package/src/pane-layout.ts +125 -0
  47. package/src/progress-view.ts +568 -0
  48. package/src/result-slots.ts +325 -0
  49. package/src/result-validation.ts +47 -0
  50. package/src/run-command.ts +242 -0
  51. package/src/run-commands.ts +215 -0
  52. package/src/run-event-log.ts +72 -0
  53. package/src/run-prepare.ts +178 -0
  54. package/src/run-progress.ts +137 -0
  55. package/src/run-read.ts +425 -0
  56. package/src/run-state.ts +120 -0
  57. package/src/run-usage.ts +393 -0
  58. package/src/run-view.ts +225 -0
  59. package/src/runs-command.ts +356 -0
  60. package/src/runs.ts +645 -0
  61. package/src/sandbox-homes.ts +211 -0
  62. package/src/sandboxes.ts +521 -0
  63. package/src/skills/fetch.ts +305 -0
  64. package/src/skills/run-skills.ts +181 -0
  65. package/src/skills/sources.ts +87 -0
  66. package/src/skills/tree.ts +60 -0
  67. package/src/stage-ledger.ts +535 -0
  68. package/src/stage-plan.ts +127 -0
  69. package/src/stage-schema.ts +88 -0
  70. package/src/stopped.ts +36 -0
  71. package/src/test-command.ts +110 -0
  72. package/src/testing/operation-events.ts +89 -0
  73. package/src/testing.ts +164 -0
  74. package/src/workflow-loader.ts +113 -0
  75. package/src/workflow-runner.ts +3666 -0
  76. package/src/workflow-test-preload.ts +13 -0
  77. package/src/workflow-testing/decisions.ts +129 -0
  78. package/src/workflow-testing/host.ts +811 -0
  79. package/src/workflow-testing/index.ts +420 -0
  80. package/src/workflow-testing/script.ts +447 -0
  81. package/src/workflow-testing/workflow-testing.typecheck.ts +47 -0
  82. package/README.md +0 -3
@@ -0,0 +1,245 @@
1
+ import type { HarnessTurn, HarnessTurnOutcome } from "@agentswf/harness/adapter";
2
+ import { scheduleAt } from "./deadlines";
3
+ import { messageOf } from "./errors";
4
+ import {
5
+ DEFAULT_LIVENESS_POLICY,
6
+ type LivenessEvent,
7
+ type LivenessPolicy,
8
+ type OperationMessages,
9
+ type OperationPhase,
10
+ type OperationStop,
11
+ type SupervisedOutcome,
12
+ } from "./operation-liveness";
13
+
14
+ /**
15
+ * A message operation: no result slot and no check-in. Its turn's end goes on to what continues it,
16
+ * a wait for an answer it asked for, the one reminder of an answer it owes, or its end, which is
17
+ * not a failure. A timeout or the harness failing is, and stops the agent as any operation's does.
18
+ * A stop or the deadline ends it whatever it is doing, a turn being started or delivered included.
19
+ */
20
+ export async function superviseMessageOperation(options: {
21
+ /** Moves only later, as the operation takes questions whose askers wait longer. */
22
+ deadline(): number;
23
+ start(): Promise<HarnessTurn>;
24
+ messages: OperationMessages;
25
+ onStop(cancel: (stop: OperationStop) => Promise<void>): () => void;
26
+ event?(event: LivenessEvent): void;
27
+ phase?(phase: OperationPhase, reason?: string, until?: number): void;
28
+ delivered?(at: number): void;
29
+ policy?: LivenessPolicy;
30
+ now?: () => number;
31
+ }): Promise<SupervisedOutcome> {
32
+ const now = options.now ?? Date.now;
33
+ const policy = options.policy ?? DEFAULT_LIVENESS_POLICY;
34
+ const charges: number[] = [];
35
+ let sequence = 0;
36
+ let deliveredAt: number | undefined;
37
+ let stop: OperationStop | undefined;
38
+ let wake = Promise.withResolvers<void>();
39
+ const changed = () => wake.resolve();
40
+ const released = Promise.withResolvers<void>();
41
+ const removeStop = options.onStop((request) => {
42
+ stop ??= request;
43
+ acquisition?.abort();
44
+ changed();
45
+ return released.promise;
46
+ });
47
+ const unwatch = options.messages.onChange(changed);
48
+ const emit = (kind: LivenessEvent["kind"], reason?: string, until?: number, peer?: string) =>
49
+ options.event?.({
50
+ kind,
51
+ at: now(),
52
+ sequence,
53
+ ...(reason === undefined ? {} : { reason }),
54
+ ...(until === undefined ? {} : { until }),
55
+ ...(peer === undefined ? {} : { peer }),
56
+ });
57
+ const ended = (kind: SupervisedOutcome["kind"], reason: string): SupervisedOutcome => ({
58
+ kind,
59
+ reason,
60
+ cleanupUnresolved: false,
61
+ charges,
62
+ ...(deliveredAt === undefined ? {} : { deliveredAt }),
63
+ settledAt: kind === "timed-out" ? Math.min(now(), options.deadline()) : now(),
64
+ });
65
+ /** Until something changes, a stop, the deadline, or `until`, whichever comes first. */
66
+ const nextChange = async (until = Number.POSITIVE_INFINITY) => {
67
+ const cancel = scheduleAt({ unixMilliseconds: Math.min(options.deadline(), until) }, changed);
68
+ try {
69
+ await wake.promise;
70
+ } finally {
71
+ cancel();
72
+ wake = Promise.withResolvers<void>();
73
+ }
74
+ };
75
+ const stopped = () => stop !== undefined || now() >= options.deadline();
76
+ /** Aborts a delivery not yet dispatched when the operation is stopped. */
77
+ let acquisition: AbortController | undefined;
78
+
79
+ /**
80
+ * A turn being started or delivered, raced against a stop and the deadline. One that arrives
81
+ * after the operation ended is released, bounded, so nothing is left running.
82
+ */
83
+ const acquire = async (opening: (signal: AbortSignal) => Promise<HarnessTurn>) => {
84
+ acquisition = new AbortController();
85
+ const pending = opening(acquisition.signal);
86
+ let turn: HarnessTurn | undefined;
87
+ let error: unknown;
88
+ void pending.then(
89
+ (value) => {
90
+ turn = value;
91
+ changed();
92
+ },
93
+ (reason: unknown) => {
94
+ error = reason;
95
+ changed();
96
+ },
97
+ );
98
+ while (turn === undefined && error === undefined && !stopped()) await nextChange();
99
+ acquisition = undefined;
100
+ if (turn === undefined && error === undefined) {
101
+ void pending.then(
102
+ (late) => late.release("operation already ended", bound()).catch(() => undefined),
103
+ () => undefined,
104
+ );
105
+ return undefined;
106
+ }
107
+ if (error !== undefined) throw error;
108
+ return turn;
109
+ };
110
+ const bound = (answered = false) => ({
111
+ unixMilliseconds: now() + (answered ? policy.releaseMs : Math.min(policy.releaseMs, 5_000)),
112
+ });
113
+
114
+ /**
115
+ * The turn's end, or a failure: its settling rejected, its delivery failed, or its dispatch was
116
+ * not confirmed in time.
117
+ */
118
+ const observe = async (
119
+ turn: HarnessTurn,
120
+ /** A reminder's response window: what it owes fails then, if still unanswered. */
121
+ window?: number,
122
+ ): Promise<HarnessTurnOutcome | "stopped"> => {
123
+ let outcome: HarnessTurnOutcome | undefined;
124
+ let failure: string | undefined;
125
+ let deliveryUntil: number | undefined;
126
+ void turn.settled.then(
127
+ (value) => {
128
+ outcome = value;
129
+ charges.push(...value.chargesUsd);
130
+ changed();
131
+ },
132
+ (error: unknown) => {
133
+ failure = messageOf(error);
134
+ changed();
135
+ },
136
+ );
137
+ if (turn.delivery) {
138
+ void turn.delivery.dispatched.then((at) => {
139
+ deliveredAt ??= at;
140
+ if (sequence === 0) options.delivered?.(at);
141
+ deliveryUntil = at + policy.deliveryMs;
142
+ changed();
143
+ });
144
+ void turn.delivery.accepted.then(() => {
145
+ deliveryUntil = undefined;
146
+ changed();
147
+ });
148
+ void turn.delivery.failed.then((reason) => {
149
+ failure = `delivery observation failed: ${reason}`;
150
+ changed();
151
+ });
152
+ } else if (deliveredAt === undefined) {
153
+ deliveredAt = now();
154
+ options.delivered?.(deliveredAt);
155
+ }
156
+ for (;;) {
157
+ if (failure !== undefined) throw new Error(failure);
158
+ if (outcome) return outcome;
159
+ if (stopped()) return "stopped";
160
+ if (deliveryUntil !== undefined && now() >= deliveryUntil) {
161
+ throw new Error("prompt delivery could not be confirmed");
162
+ }
163
+ if (window !== undefined && now() >= window) {
164
+ options.messages.reminderExpired();
165
+ window = undefined;
166
+ }
167
+ await nextChange(Math.min(deliveryUntil ?? Infinity, window ?? Infinity));
168
+ }
169
+ };
170
+
171
+ emit("opened", undefined, options.deadline());
172
+ let turn: HarnessTurn | undefined;
173
+ let result: SupervisedOutcome;
174
+ try {
175
+ result = await (async (): Promise<SupervisedOutcome> => {
176
+ if (stopped()) {
177
+ return stop ? ended(stop.kind, stop.reason) : ended("timed-out", "deadline before start");
178
+ }
179
+ options.phase?.("working");
180
+ turn = await acquire(() => options.start());
181
+ let reminded: number | undefined;
182
+ for (;;) {
183
+ if (!turn || stopped()) break;
184
+ const native = await observe(turn, reminded);
185
+ if (native === "stopped") break;
186
+ if (native.state !== "completed") {
187
+ return ended(
188
+ native.state === "blocked" ? "failed" : native.state,
189
+ native.detail ?? `native turn ${native.state}`,
190
+ );
191
+ }
192
+ reminded = undefined;
193
+ let next = options.messages.turnEnded();
194
+ while (next.kind === "wait" && !stopped()) {
195
+ options.phase?.("awaiting-reply", next.peer, next.until);
196
+ emit("awaiting-reply", undefined, next.until, next.peer);
197
+ // An answer, a failure or a question continues it; nothing else does.
198
+ while (!stopped() && options.messages.pending() !== "continues") await nextChange();
199
+ next = options.messages.turnEnded();
200
+ }
201
+ if (stopped()) break;
202
+ if (next.kind === "settle" || next.kind === "check-in") {
203
+ return ended("answered", "the message was handled");
204
+ }
205
+ if (next.kind === "wait") continue;
206
+ sequence += 1;
207
+ const reminder = next.kind === "remind" ? next.text : undefined;
208
+ if (reminder !== undefined) {
209
+ emit("reminder-due");
210
+ // It has the check-in's response window to answer in.
211
+ reminded = now() + policy.responseMs;
212
+ }
213
+ options.phase?.("working");
214
+ const previous: HarnessTurn = turn;
215
+ turn = await acquire((signal) =>
216
+ options.messages.continueWith(previous, sequence, signal, reminder),
217
+ );
218
+ }
219
+ if (stop) return ended(stop.kind, stop.reason);
220
+ return ended("timed-out", "message operation deadline exceeded");
221
+ })();
222
+ } catch (error) {
223
+ result = ended("failed", messageOf(error));
224
+ } finally {
225
+ unwatch();
226
+ }
227
+ options.phase?.("releasing");
228
+ emit("releasing");
229
+ if (turn) {
230
+ const answered = result.kind === "answered";
231
+ const until = bound(answered);
232
+ const release = await Promise.race([
233
+ turn.release(stop?.reason ?? result.reason, until, { answered }).catch(() => undefined),
234
+ new Promise<undefined>((resolve) => scheduleAt(until, () => resolve(undefined))),
235
+ ]);
236
+ emit("release", release?.kind ?? "unresolved");
237
+ if (release?.kind !== "released" && release?.kind !== "finishing") {
238
+ result.cleanupUnresolved = true;
239
+ }
240
+ }
241
+ emit("terminal", result.reason);
242
+ removeStop();
243
+ released.resolve();
244
+ return result;
245
+ }
@@ -0,0 +1,35 @@
1
+ import type { MessageLimits } from "@agentswf/contract/records";
2
+ import type { AgentExecution } from "@agentswf/contract/workflow";
3
+ import { placementOf } from "@agentswf/contract/workflow";
4
+
5
+ /** The ceilings an attempt runs under, written into its record; a route may only lower `perRoute`. */
6
+ export const MESSAGE_LIMITS: MessageLimits = {
7
+ perRoute: 40,
8
+ bytes: 16 * 1024,
9
+ unread: 16,
10
+ operationMs: 15 * 60_000,
11
+ };
12
+
13
+ /**
14
+ * Where an asker may wait for an answer, by harness and placement: on where M2 passed
15
+ * (`docs/findings/messaging-m2.md`), off elsewhere. A pane that is `confirmed` waits only where it
16
+ * confirms what it was sent, as `wf waiting` needs. The fake harness always may. cursor is off:
17
+ * its turns ran past their deadline in M2.
18
+ */
19
+ export const AWAIT_REPLY: Readonly<Record<string, { headless?: true; pane?: true | "confirmed" }>> =
20
+ {
21
+ fake: { headless: true, pane: true },
22
+ claude: { headless: true, pane: "confirmed" },
23
+ codex: { headless: true },
24
+ pi: { headless: true },
25
+ };
26
+
27
+ /** Whether this agent may `--await-reply` where it runs. */
28
+ export function canAwaitReply(
29
+ execution: AgentExecution,
30
+ confirmsDelivery: boolean,
31
+ list: typeof AWAIT_REPLY = AWAIT_REPLY,
32
+ ): boolean {
33
+ const allowed = list[execution.harness]?.[placementOf(execution)];
34
+ return allowed === true || (allowed === "confirmed" && confirmsDelivery);
35
+ }