@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.
- package/package.json +39 -4
- package/src/accounting/format.ts +252 -0
- package/src/accounting/plans.ts +38 -0
- package/src/accounting/prices.ts +98 -0
- package/src/accounting/summary.ts +385 -0
- package/src/accounting/tokens.ts +34 -0
- package/src/agent-launcher.ts +127 -0
- package/src/allowance-command.ts +105 -0
- package/src/attempt-ending.ts +198 -0
- package/src/attempt-output.ts +132 -0
- package/src/attempt-view.ts +362 -0
- package/src/canonical-json.ts +12 -0
- package/src/control-plane.ts +424 -0
- package/src/deadlines.ts +106 -0
- package/src/decisions/directory.ts +432 -0
- package/src/decisions/fake.ts +60 -0
- package/src/decisions/openrouter.ts +161 -0
- package/src/decisions/seam.ts +70 -0
- package/src/display-path.ts +16 -0
- package/src/duration.ts +12 -0
- package/src/errors.ts +4 -0
- package/src/files.ts +78 -0
- package/src/herdr-placement.ts +182 -0
- package/src/herdr-run-session.ts +530 -0
- package/src/herdr-workspace-marks.ts +194 -0
- package/src/here.ts +289 -0
- package/src/index.ts +5 -0
- package/src/init-command.ts +240 -0
- package/src/init-template/AGENTS.md.template +16 -0
- package/src/init-template/hello/workflow.test.ts.template +17 -0
- package/src/init-template/hello/workflow.ts.template +26 -0
- package/src/init-template/tsconfig.json.template +11 -0
- package/src/jsonl.ts +74 -0
- package/src/logs-command.ts +605 -0
- package/src/machine.ts +27 -0
- package/src/message-operation.ts +245 -0
- package/src/messaging/limits.ts +35 -0
- package/src/messaging/messaging.ts +1303 -0
- package/src/messaging/routes.ts +79 -0
- package/src/messaging/text.ts +251 -0
- package/src/operation-events.ts +182 -0
- package/src/operation-liveness.ts +674 -0
- package/src/operator-aliases.ts +10 -0
- package/src/operator-cli.ts +600 -0
- package/src/operator-runtime.ts +503 -0
- package/src/pane-layout.ts +125 -0
- package/src/progress-view.ts +568 -0
- package/src/result-slots.ts +325 -0
- package/src/result-validation.ts +47 -0
- package/src/run-command.ts +242 -0
- package/src/run-commands.ts +215 -0
- package/src/run-event-log.ts +72 -0
- package/src/run-prepare.ts +178 -0
- package/src/run-progress.ts +137 -0
- package/src/run-read.ts +425 -0
- package/src/run-state.ts +120 -0
- package/src/run-usage.ts +393 -0
- package/src/run-view.ts +225 -0
- package/src/runs-command.ts +356 -0
- package/src/runs.ts +645 -0
- package/src/sandbox-homes.ts +211 -0
- package/src/sandboxes.ts +521 -0
- package/src/skills/fetch.ts +305 -0
- package/src/skills/run-skills.ts +181 -0
- package/src/skills/sources.ts +87 -0
- package/src/skills/tree.ts +60 -0
- package/src/stage-ledger.ts +535 -0
- package/src/stage-plan.ts +127 -0
- package/src/stage-schema.ts +88 -0
- package/src/stopped.ts +36 -0
- package/src/test-command.ts +110 -0
- package/src/testing/operation-events.ts +89 -0
- package/src/testing.ts +164 -0
- package/src/workflow-loader.ts +113 -0
- package/src/workflow-runner.ts +3666 -0
- package/src/workflow-test-preload.ts +13 -0
- package/src/workflow-testing/decisions.ts +129 -0
- package/src/workflow-testing/host.ts +811 -0
- package/src/workflow-testing/index.ts +420 -0
- package/src/workflow-testing/script.ts +447 -0
- package/src/workflow-testing/workflow-testing.typecheck.ts +47 -0
- package/README.md +0 -3
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
type SandboxRecord,
|
|
6
|
+
STAGE_RECORD_VERSION,
|
|
7
|
+
type StageNeed,
|
|
8
|
+
type StageRecord,
|
|
9
|
+
} from "@agentswf/contract/records";
|
|
10
|
+
import {
|
|
11
|
+
EXECUTABLE_WORKFLOW_KIND,
|
|
12
|
+
type ExecutableWorkflow,
|
|
13
|
+
type JsonObject,
|
|
14
|
+
type JsonValue,
|
|
15
|
+
type RuntimeAliases,
|
|
16
|
+
type WorkflowDefinition,
|
|
17
|
+
} from "@agentswf/contract/workflow";
|
|
18
|
+
import { type Harness, sandboxTokens } from "@agentswf/harness";
|
|
19
|
+
import { createFakeSandboxProvider } from "@agentswf/sandbox/testing/fake";
|
|
20
|
+
import { messageOf } from "../errors";
|
|
21
|
+
import { OPERATOR_ALIASES } from "../operator-aliases";
|
|
22
|
+
import { createRun, readEvents, readStageRecords, writeStageRecord } from "../runs";
|
|
23
|
+
import { primaryFailure, WorkflowStopped } from "../stopped";
|
|
24
|
+
import { runWorkflow, type SettledRun, WorkflowRunError } from "../workflow-runner";
|
|
25
|
+
import { createScriptedDecisions, type DecisionRequest, type DecisionScript } from "./decisions";
|
|
26
|
+
import {
|
|
27
|
+
type AgentSandbox,
|
|
28
|
+
type CompactionRecord,
|
|
29
|
+
createScriptedHost,
|
|
30
|
+
type OpenedAgent,
|
|
31
|
+
type SetRecord,
|
|
32
|
+
type TurnRecord,
|
|
33
|
+
} from "./host";
|
|
34
|
+
import { type MessageScript, MessageScripts, type Script, Scripts } from "./script";
|
|
35
|
+
|
|
36
|
+
export type { DecisionRequest, DecisionScript } from "./decisions";
|
|
37
|
+
export {
|
|
38
|
+
answer,
|
|
39
|
+
type Message,
|
|
40
|
+
type MessageOutcome,
|
|
41
|
+
type MessageScript,
|
|
42
|
+
type ReceivedMessage,
|
|
43
|
+
reply,
|
|
44
|
+
type Script,
|
|
45
|
+
type SendOptions,
|
|
46
|
+
type Turn,
|
|
47
|
+
} from "./script";
|
|
48
|
+
|
|
49
|
+
export type TestOptions = {
|
|
50
|
+
/** Each agent's script, by its key or a pattern such as `"review:*"`. */
|
|
51
|
+
agents?: Readonly<Record<string, Script>>;
|
|
52
|
+
/**
|
|
53
|
+
* Each agent's compactions, apart from its turns: `answer("summary")` or a `reply`, a list one
|
|
54
|
+
* entry per compaction. An agent with none here has every compaction answer `""`.
|
|
55
|
+
*/
|
|
56
|
+
compactions?: Readonly<Record<string, Script>>;
|
|
57
|
+
/**
|
|
58
|
+
* Each agent's message operations, apart from its turns, by its key or a pattern: a script for
|
|
59
|
+
* all of them, or a list, one per operation. An agent a message wakes with none here fails the
|
|
60
|
+
* test.
|
|
61
|
+
*/
|
|
62
|
+
messages?: Readonly<Record<string, MessageScript | readonly MessageScript[]>>;
|
|
63
|
+
/**
|
|
64
|
+
* Where an agent may `--await-reply`: `listed`, where the engine's list allows it for the
|
|
65
|
+
* agent's harness and placement, as a run would; `anywhere`, to test a workflow's messaging on a
|
|
66
|
+
* harness not listed yet.
|
|
67
|
+
*/
|
|
68
|
+
awaitReply?: "listed" | "anywhere";
|
|
69
|
+
/** Each decision's answers, by its key or a pattern. */
|
|
70
|
+
decisions?: Readonly<Record<string, DecisionScript>>;
|
|
71
|
+
/** Runtime aliases beside the ones `awf run` installs, `claude` and `codex`; the same name replaces one. */
|
|
72
|
+
runtimes?: RuntimeAliases;
|
|
73
|
+
/** The run's deadline; an hour, as `awf run`'s. */
|
|
74
|
+
timeoutMs?: number;
|
|
75
|
+
/** Real time with no turn or decision starting or ending before the test fails; 2 s. */
|
|
76
|
+
stallMs?: number;
|
|
77
|
+
/** Where the agents work; by default a temporary directory, removed afterwards. */
|
|
78
|
+
cwd?: string;
|
|
79
|
+
/**
|
|
80
|
+
* The session the run is started from, by its harness; absent, `agents.caller` and
|
|
81
|
+
* `agents.forkCaller` answer `null`. It is handed over, as `awf run --here` hands one over, and its
|
|
82
|
+
* turns are answered from the script under the key the workflow gives it; with `here: false` it
|
|
83
|
+
* waits on the run, and `agents.caller` answers `null`. A fork of it runs on `model`, the model its
|
|
84
|
+
* files show; without one, `forkCaller` answers `null`, as it does where they name none.
|
|
85
|
+
*/
|
|
86
|
+
caller?: { harness: Harness; model?: string; here?: boolean };
|
|
87
|
+
/**
|
|
88
|
+
* Stages an earlier attempt recorded, by name: each value, or `undefined` for a stage that
|
|
89
|
+
* returns nothing. Given, the run is that run's continue, reusing them without calling their
|
|
90
|
+
* work, as `awf run --continue` would.
|
|
91
|
+
*/
|
|
92
|
+
recorded?: Readonly<Record<string, JsonValue | undefined>>;
|
|
93
|
+
/**
|
|
94
|
+
* The stage the attempt starts at, as `--from-stage`: a continue of `recorded`, or a new run
|
|
95
|
+
* started there.
|
|
96
|
+
*/
|
|
97
|
+
fromStage?: string;
|
|
98
|
+
/** Values of stages before `fromStage` with no record to reuse, as `awf run --values` gives them. */
|
|
99
|
+
values?: Readonly<Record<string, JsonValue>>;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** What the run did. Under `parallel`, what started first is scheduling: read by key. */
|
|
103
|
+
export type TestRun<Result> = {
|
|
104
|
+
/**
|
|
105
|
+
* What the workflow returned. When it threw instead, reading this throws, with the workflow's
|
|
106
|
+
* error as the cause: `expect(() => run.value).toThrow(…)`. So does spreading or serialising the
|
|
107
|
+
* run.
|
|
108
|
+
*/
|
|
109
|
+
readonly value: Result;
|
|
110
|
+
/** Every turn, nudges included, in the order they started. */
|
|
111
|
+
turns: TurnRecord[];
|
|
112
|
+
/** One agent's turns, in its session's order. */
|
|
113
|
+
turnsOf(agent: string): TurnRecord[];
|
|
114
|
+
/** Every compaction, in the order they started; not among the turns. */
|
|
115
|
+
compactions: CompactionRecord[];
|
|
116
|
+
/** One agent's compactions, in order. */
|
|
117
|
+
compactionsOf(agent: string): CompactionRecord[];
|
|
118
|
+
/** Every switch of an agent's model or effort its host was asked for, in order; not among the turns. */
|
|
119
|
+
sets: SetRecord[];
|
|
120
|
+
/** One agent's switches, in order. */
|
|
121
|
+
setsOf(agent: string): SetRecord[];
|
|
122
|
+
/** The messages admitted for one agent, in the order sent: who sent each, whether it asked, and how it ended. */
|
|
123
|
+
messagesOf(recipient: string): MessageRecord[];
|
|
124
|
+
/** Every agent opened, in the order opened. */
|
|
125
|
+
agents: OpenedAgent[];
|
|
126
|
+
/** The agent opened under `key`; throws, naming the keys opened, when there is none. */
|
|
127
|
+
agentOf(key: string): OpenedAgent;
|
|
128
|
+
decisions: DecisionRequest[];
|
|
129
|
+
logs: { message: string; fields?: JsonObject }[];
|
|
130
|
+
/**
|
|
131
|
+
* Each stage's record in the order entered, as the run wrote it to disk: sessions, times and
|
|
132
|
+
* reason included, which the run's own summaries leave out.
|
|
133
|
+
*/
|
|
134
|
+
stages: StageRecord[];
|
|
135
|
+
/** How the attempt stopped, apart from a failure, and the value it stopped for; absent when it didn't. */
|
|
136
|
+
stopped?: { reason: string; stage?: string; needs?: readonly StageNeed[] };
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Runs `workflow` through the real engine, answering each agent from its script and each decision
|
|
141
|
+
* from its own. It rejects when a script cannot meet what the workflow asked, or leaves a list
|
|
142
|
+
* unfinished, or the run stalls: those fail the test, never the workflow.
|
|
143
|
+
*/
|
|
144
|
+
export async function testWorkflow<Args extends JsonValue, Result extends JsonValue>(
|
|
145
|
+
workflow: WorkflowDefinition<Args, Result> | ExecutableWorkflow<Args, Result>,
|
|
146
|
+
args: NoInfer<Args>,
|
|
147
|
+
options: TestOptions = {},
|
|
148
|
+
): Promise<TestRun<Result>> {
|
|
149
|
+
const definition = isExecutable(workflow) ? workflow.definition : workflow;
|
|
150
|
+
const scripts = new Scripts(options.agents);
|
|
151
|
+
const compactionScripts = new Scripts(options.compactions, {
|
|
152
|
+
noun: "compaction",
|
|
153
|
+
option: "compactions",
|
|
154
|
+
});
|
|
155
|
+
const messageScripts = new MessageScripts(options.messages);
|
|
156
|
+
if (options.values !== undefined && options.fromStage === undefined) {
|
|
157
|
+
throw new Error("values give the stages before fromStage: give fromStage too");
|
|
158
|
+
}
|
|
159
|
+
const stopping = new AbortController();
|
|
160
|
+
const problems: string[] = [];
|
|
161
|
+
const stallMs = options.stallMs ?? 2_000;
|
|
162
|
+
let stall: ReturnType<typeof setTimeout> | undefined;
|
|
163
|
+
const events = {
|
|
164
|
+
onActivity() {
|
|
165
|
+
clearTimeout(stall);
|
|
166
|
+
stall = setTimeout(() => {
|
|
167
|
+
const waiting = host
|
|
168
|
+
.inFlight()
|
|
169
|
+
.map((turn) => `"${turn.agent}" turn ${turn.n}${turn.nudge ? " (nudge)" : ""}`);
|
|
170
|
+
stopWith(
|
|
171
|
+
`the run stalled: nothing started or ended for ${stallMs} ms; ${
|
|
172
|
+
waiting.length > 0 ? `in flight: ${waiting.join(", ")}` : "no turn in flight"
|
|
173
|
+
}`,
|
|
174
|
+
);
|
|
175
|
+
}, stallMs);
|
|
176
|
+
},
|
|
177
|
+
onScriptError: (message: string) => stopWith(message),
|
|
178
|
+
};
|
|
179
|
+
const stopWith = (message: string) => {
|
|
180
|
+
problems.push(message);
|
|
181
|
+
stopping.abort(new Error(message));
|
|
182
|
+
};
|
|
183
|
+
const temporary: string[] = [];
|
|
184
|
+
const directory = () => {
|
|
185
|
+
const path = mkdtempSync(join(tmpdir(), "awf-test-"));
|
|
186
|
+
temporary.push(path);
|
|
187
|
+
return path;
|
|
188
|
+
};
|
|
189
|
+
const cwd = options.cwd ?? directory();
|
|
190
|
+
const host = createScriptedHost(
|
|
191
|
+
scripts,
|
|
192
|
+
compactionScripts,
|
|
193
|
+
messageScripts,
|
|
194
|
+
events,
|
|
195
|
+
options.caller ? { ...options.caller, cwd } : undefined,
|
|
196
|
+
);
|
|
197
|
+
const decisions = createScriptedDecisions(options.decisions ?? {}, events);
|
|
198
|
+
const logs: TestRun<Result>["logs"] = [];
|
|
199
|
+
// Sandboxes are opened by a provider that confines nothing and hosts panes.
|
|
200
|
+
const sandboxes = createFakeSandboxProvider({ panes: { prelude: "true", ready: "ready" } });
|
|
201
|
+
|
|
202
|
+
let settled: { value: Result } | { error: unknown };
|
|
203
|
+
let sandboxOf = new Map<string, AgentSandbox>();
|
|
204
|
+
let stages: StageRecord[] = [];
|
|
205
|
+
let messages: (MessageRecord & { to: string })[] = [];
|
|
206
|
+
const runRoot = directory();
|
|
207
|
+
const run = await createRun(runRoot, {
|
|
208
|
+
id: "test",
|
|
209
|
+
workflow: definition.meta.name,
|
|
210
|
+
argv: [],
|
|
211
|
+
cwd,
|
|
212
|
+
sandbox: null,
|
|
213
|
+
}).catch((error: unknown) => {
|
|
214
|
+
for (const path of temporary) rmSync(path, { recursive: true, force: true });
|
|
215
|
+
throw error;
|
|
216
|
+
});
|
|
217
|
+
if (options.recorded) await recordEarlierAttempt(run.dir, options.recorded);
|
|
218
|
+
const recordedStages = async ({ stages: entered = [] }: SettledRun) => {
|
|
219
|
+
const records = await readStageRecords(run.dir);
|
|
220
|
+
// A stage the plan stopped has no record of this attempt's: the one there is an earlier one's.
|
|
221
|
+
return entered.flatMap(({ stage, attempt }) => {
|
|
222
|
+
const record = records.get(stage);
|
|
223
|
+
return record?.attempt === attempt ? [record] : [];
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
events.onActivity();
|
|
227
|
+
try {
|
|
228
|
+
const result = await runWorkflow(definition, args, {
|
|
229
|
+
runRoot,
|
|
230
|
+
livenessPolicy: { quietMs: 1, responseMs: 25, deliveryMs: 100, releaseMs: 1000 },
|
|
231
|
+
...(options.awaitReply === "anywhere" ? { awaitReply: () => true } : {}),
|
|
232
|
+
run: {
|
|
233
|
+
dir: run.dir,
|
|
234
|
+
id: run.record.id,
|
|
235
|
+
attempt: options.recorded ? 2 : 1,
|
|
236
|
+
...(options.fromStage === undefined ? {} : { fromStage: options.fromStage }),
|
|
237
|
+
...(options.values === undefined
|
|
238
|
+
? {}
|
|
239
|
+
: { values: new Map(Object.entries(options.values)) }),
|
|
240
|
+
},
|
|
241
|
+
cwd,
|
|
242
|
+
deadline: { unixMilliseconds: Date.now() + (options.timeoutMs ?? 60 * 60_000) },
|
|
243
|
+
signal: stopping.signal,
|
|
244
|
+
runtime: { aliases: { ...OPERATOR_ALIASES, ...options.runtimes }, host: host.factory },
|
|
245
|
+
sandboxes: {
|
|
246
|
+
providers: {
|
|
247
|
+
installed: { srt: sandboxes.provider, docker: sandboxes.provider },
|
|
248
|
+
default: "srt",
|
|
249
|
+
},
|
|
250
|
+
sandboxesDir: directory(),
|
|
251
|
+
machineRoot: directory(),
|
|
252
|
+
// A sandbox here launches nothing, so no harness's login is needed: a stand-in for each.
|
|
253
|
+
environment: {
|
|
254
|
+
...process.env,
|
|
255
|
+
...Object.fromEntries(sandboxTokens().map((name) => [name, "workflow-test"])),
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
decisions: {
|
|
259
|
+
providers: { scripted: decisions.provider },
|
|
260
|
+
aliases: { jev: { provider: "scripted", model: "jev" } },
|
|
261
|
+
},
|
|
262
|
+
onLog: (message, fields) => logs.push({ message, ...(fields ? { fields } : {}) }),
|
|
263
|
+
});
|
|
264
|
+
settled = { value: result.value };
|
|
265
|
+
sandboxOf = sandboxesOf(result.sandboxes);
|
|
266
|
+
stages = await recordedStages(result);
|
|
267
|
+
} catch (caught) {
|
|
268
|
+
settled = { error: caught instanceof WorkflowRunError ? caught.cause : caught };
|
|
269
|
+
if (caught instanceof WorkflowRunError) {
|
|
270
|
+
sandboxOf = sandboxesOf(caught.sandboxes);
|
|
271
|
+
stages = await recordedStages(caught);
|
|
272
|
+
}
|
|
273
|
+
} finally {
|
|
274
|
+
clearTimeout(stall);
|
|
275
|
+
messages = await messagesIn(run.dir);
|
|
276
|
+
for (const path of temporary) rmSync(path, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
if (problems.length === 0) {
|
|
279
|
+
const leftovers = [
|
|
280
|
+
...scripts.leftovers(),
|
|
281
|
+
...compactionScripts.leftovers(),
|
|
282
|
+
...messageScripts.leftovers(),
|
|
283
|
+
];
|
|
284
|
+
// What the workflow threw may be why a list went unfinished.
|
|
285
|
+
if (leftovers.length > 0 && "error" in settled) {
|
|
286
|
+
leftovers.push(`the workflow threw: ${messageOf(settled.error)}`);
|
|
287
|
+
}
|
|
288
|
+
problems.push(...leftovers);
|
|
289
|
+
}
|
|
290
|
+
if (problems.length > 0) throw new Error(problems.join("\n"));
|
|
291
|
+
const agents = host.agents.map((agent) => {
|
|
292
|
+
const sandbox = sandboxOf.get(agent.key);
|
|
293
|
+
return sandbox === undefined ? agent : { ...agent, sandbox };
|
|
294
|
+
});
|
|
295
|
+
return {
|
|
296
|
+
get value() {
|
|
297
|
+
if ("value" in settled) return settled.value;
|
|
298
|
+
throw new Error(`the workflow threw: ${messageOf(settled.error)}`, { cause: settled.error });
|
|
299
|
+
},
|
|
300
|
+
turns: host.turns,
|
|
301
|
+
turnsOf: (agent) => host.turns.filter((turn) => turn.agent === agent),
|
|
302
|
+
compactions: host.compactions,
|
|
303
|
+
compactionsOf: (agent) => host.compactions.filter((compaction) => compaction.agent === agent),
|
|
304
|
+
sets: host.sets,
|
|
305
|
+
setsOf: (agent) => host.sets.filter((set) => set.agent === agent),
|
|
306
|
+
messagesOf: (recipient) =>
|
|
307
|
+
messages.flatMap(({ to, ...each }) => (to === recipient ? [each] : [])),
|
|
308
|
+
agents,
|
|
309
|
+
agentOf(key) {
|
|
310
|
+
const agent = agents.find((opened) => opened.key === key);
|
|
311
|
+
if (agent) return agent;
|
|
312
|
+
const opened = agents.map((known) => `"${known.key}"`).join(", ") || "none";
|
|
313
|
+
throw new Error(`no agent "${key}" was opened; opened: ${opened}`);
|
|
314
|
+
},
|
|
315
|
+
decisions: decisions.asked,
|
|
316
|
+
logs,
|
|
317
|
+
stages,
|
|
318
|
+
...stoppedOf(settled),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* A message as `messagesOf` gives it. A question ends `replied` or `failed`, `reason` saying why it
|
|
324
|
+
* failed (`timed-out`, `withdrawn`, `released`, or how its peer failed); any other message ends
|
|
325
|
+
* `delivered` once shown, or `undelivered` with the reason. `pending` until then.
|
|
326
|
+
*/
|
|
327
|
+
export type MessageRecord = {
|
|
328
|
+
from: string;
|
|
329
|
+
awaitReply: boolean;
|
|
330
|
+
end: "replied" | "failed" | "delivered" | "undelivered" | "pending";
|
|
331
|
+
reason?: string;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/** The attempt's messages, read from the run's event log. */
|
|
335
|
+
async function messagesIn(dir: string): Promise<(MessageRecord & { to: string })[]> {
|
|
336
|
+
const found = new Map<number, MessageRecord & { to: string }>();
|
|
337
|
+
for (const event of await readEvents(dir)) {
|
|
338
|
+
if (event.kind === "message-sent") {
|
|
339
|
+
found.set(event.message, {
|
|
340
|
+
from: event.from,
|
|
341
|
+
to: event.to,
|
|
342
|
+
awaitReply: event.awaitsReply !== undefined,
|
|
343
|
+
end: "pending",
|
|
344
|
+
});
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const message =
|
|
348
|
+
"message" in event && typeof event.message === "number"
|
|
349
|
+
? found.get(event.message)
|
|
350
|
+
: undefined;
|
|
351
|
+
if (!message) continue;
|
|
352
|
+
if (event.kind === "request-ended") {
|
|
353
|
+
message.end = event.end === "replied" ? "replied" : "failed";
|
|
354
|
+
const reason =
|
|
355
|
+
event.end === "failed" ? event.reason : event.end === "replied" ? undefined : event.end;
|
|
356
|
+
if (reason !== undefined) message.reason = reason;
|
|
357
|
+
} else if (!message.awaitReply && event.kind === "message-delivery") {
|
|
358
|
+
message.end = "delivered";
|
|
359
|
+
} else if (!message.awaitReply && event.kind === "message-undelivered") {
|
|
360
|
+
message.end = "undelivered";
|
|
361
|
+
message.reason = event.reason;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return [...found.values()];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Writes `recorded` as attempt 1's succeeded stages into a run's folder, a second apart in the order
|
|
369
|
+
* given: a continue orders what is recorded by when each stage started.
|
|
370
|
+
*/
|
|
371
|
+
async function recordEarlierAttempt(
|
|
372
|
+
dir: string,
|
|
373
|
+
recorded: Readonly<Record<string, JsonValue | undefined>>,
|
|
374
|
+
): Promise<void> {
|
|
375
|
+
let at = Date.now() - Object.keys(recorded).length * 1_000;
|
|
376
|
+
for (const [stage, value] of Object.entries(recorded)) {
|
|
377
|
+
const time = new Date(at).toISOString();
|
|
378
|
+
at += 1_000;
|
|
379
|
+
await writeStageRecord(dir, {
|
|
380
|
+
version: STAGE_RECORD_VERSION,
|
|
381
|
+
stage,
|
|
382
|
+
attempt: 1,
|
|
383
|
+
outcome: "succeeded",
|
|
384
|
+
started: time,
|
|
385
|
+
ended: time,
|
|
386
|
+
sessions: [],
|
|
387
|
+
...(value === undefined ? {} : { value }),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function isExecutable<Args extends JsonValue, Result extends JsonValue>(
|
|
393
|
+
workflow: WorkflowDefinition<Args, Result> | ExecutableWorkflow<Args, Result>,
|
|
394
|
+
): workflow is ExecutableWorkflow<Args, Result> {
|
|
395
|
+
return (workflow as { kind?: unknown }).kind === EXECUTABLE_WORKFLOW_KIND;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function sandboxesOf(sandboxes: readonly SandboxRecord[] | undefined): Map<string, AgentSandbox> {
|
|
399
|
+
return new Map(
|
|
400
|
+
(sandboxes ?? []).flatMap(({ key, provider, spec, domains, agents }) =>
|
|
401
|
+
agents.map(({ agent }) => [agent, { key, provider, spec, domains }] as const),
|
|
402
|
+
),
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Decided as `awf run` decides it: a stop is the run's first failure, whatever failed beside it. */
|
|
407
|
+
function stoppedOf(
|
|
408
|
+
settled: { value: unknown } | { error: unknown },
|
|
409
|
+
): Partial<Pick<TestRun<unknown>, "stopped">> {
|
|
410
|
+
if (!("error" in settled)) return {};
|
|
411
|
+
const stop = primaryFailure(settled.error);
|
|
412
|
+
if (!(stop instanceof WorkflowStopped)) return {};
|
|
413
|
+
return {
|
|
414
|
+
stopped: {
|
|
415
|
+
reason: stop.reason,
|
|
416
|
+
...(stop.stage === undefined ? {} : { stage: stop.stage }),
|
|
417
|
+
...(stop.needs === undefined ? {} : { needs: stop.needs }),
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
}
|