@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.
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/package.json +13 -9
- package/src/accounting/format.ts +4 -3
- package/src/accounting/spent.ts +128 -0
- package/src/accounting/summary.ts +40 -32
- package/src/agent-cli/cli.ts +28 -55
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +4 -4
- package/src/attempt-output.ts +13 -31
- package/src/attempt-view.ts +33 -32
- package/src/author-typebox.ts +13 -0
- package/src/client/checks.ts +94 -0
- package/src/client/exported.ts +17 -0
- package/src/client/index.ts +122 -0
- package/src/client/locate.ts +34 -0
- package/src/client/published.ts +3 -0
- package/src/client/runs.ts +337 -0
- package/src/command-line.ts +64 -0
- package/src/commands.ts +7 -1
- package/src/control-plane.ts +4 -6
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +27 -28
- package/src/deadlines.ts +6 -40
- package/src/decisions/directory.ts +50 -37
- package/src/decisions/fake.ts +1 -1
- package/src/decisions/openrouter.ts +4 -2
- package/src/decisions/seam.ts +4 -4
- package/src/display-path.ts +3 -3
- package/src/duration.ts +19 -33
- package/src/emit-command.ts +38 -22
- package/src/emit.ts +148 -59
- package/src/event-ids.ts +31 -0
- package/src/files.ts +44 -4
- package/src/hand-over.ts +150 -0
- package/src/help/allowance.txt +4 -3
- package/src/help/emit.txt +11 -7
- package/src/help/inbox.txt +5 -3
- package/src/help/logs.txt +7 -5
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +13 -8
- package/src/help/runs.txt +4 -2
- package/src/help/tick.txt +9 -4
- package/src/herdr-placement.ts +3 -33
- package/src/herdr-run-session.ts +5 -13
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +54 -136
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +42 -56
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.ts.template +3 -2
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +7 -19
- package/src/logs-command.ts +134 -53
- package/src/message-operation.ts +6 -3
- package/src/messaging/messaging.ts +8 -10
- package/src/messaging/text.ts +2 -2
- package/src/open-command.ts +770 -0
- package/src/operation-events.ts +4 -6
- package/src/operation-liveness.ts +19 -4
- package/src/operator-cli.ts +206 -90
- package/src/operator-runtime.ts +24 -2
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +49 -96
- package/src/progress-view.ts +2 -2
- package/src/project-engine.ts +79 -0
- package/src/projects.ts +13 -2
- package/src/refusals.ts +119 -0
- package/src/result-slots.ts +11 -31
- package/src/result-validation.ts +2 -2
- package/src/run-command.ts +27 -31
- package/src/run-commands.ts +42 -114
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/run-policy/stage-plan.ts +154 -0
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +165 -41
- package/src/run-read.ts +109 -41
- package/src/run-state.ts +170 -50
- package/src/run-text.ts +54 -0
- package/src/run-unreachable.ts +18 -0
- package/src/run-usage.ts +17 -11
- package/src/run-view.ts +73 -14
- package/src/runs-command.ts +81 -63
- package/src/runs.ts +284 -222
- package/src/sandbox-homes.ts +65 -13
- package/src/sandboxes.ts +182 -28
- package/src/session-lines.ts +93 -0
- package/src/skills/run-skills.ts +8 -1
- package/src/sources.ts +2 -39
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +111 -153
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +37 -19
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +66 -18
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +11 -30
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +49 -2
- package/src/workflow-runner.ts +525 -618
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +1 -1
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +28 -29
- package/src/workflow-testing/index.ts +9 -27
- package/src/workflow-testing/run-test.ts +201 -286
- package/src/workflow-testing/script.ts +7 -39
- package/src/deprecations.ts +0 -177
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -972
- package/src/stage-plan.ts +0 -186
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- package/src/workflow-testing/workflow-testing.typecheck.ts +0 -67
package/src/spend.ts
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type Billing,
|
|
5
|
+
type ClaimedAttempt,
|
|
6
|
+
type DecisionArtifact,
|
|
7
|
+
OP_FILE_VERSIONS,
|
|
8
|
+
type OpRecord,
|
|
9
|
+
type RunLine,
|
|
10
|
+
type SettledOperation,
|
|
11
|
+
type SpendRecord,
|
|
12
|
+
} from "@agentswf/contract/records";
|
|
13
|
+
import type { AgentExecution } from "@agentswf/contract/workflow";
|
|
14
|
+
import type { SessionAccounting, UsageRecord } from "@agentswf/harness";
|
|
15
|
+
import { PUBLISHED_PRICES } from "./accounting/prices";
|
|
16
|
+
import { type AttemptSpent, attemptsSpent, type RecordedOp } from "./accounting/spent";
|
|
17
|
+
import { spendOf } from "./accounting/tokens";
|
|
18
|
+
import { runUntilStopped } from "./deadlines";
|
|
19
|
+
import { messageOf } from "./errors";
|
|
20
|
+
import { isCode, linkNew } from "./files";
|
|
21
|
+
import { opFile, readOpFile, spendFile } from "./ops";
|
|
22
|
+
import { unfenced } from "./run-policy/waits";
|
|
23
|
+
import { ACCOUNTING_GRACE_MILLISECONDS, STATUS_SLACK_MILLISECONDS } from "./run-usage";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Each agent operation's `spend.json`: its usage as measured once, while its sessions are there to
|
|
27
|
+
* read, and never priced. Written with `link`, so only one write lands: by the attempt that ran it,
|
|
28
|
+
* as it ends, or by the next attempt, as it starts, for one that attempt left without one.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
type AgentOpRecord = Extract<OpRecord, { agent: string }>;
|
|
32
|
+
|
|
33
|
+
/** A `spend.json` as its writer gives it: its version is added as it is written. */
|
|
34
|
+
type SpendFields = Omit<SpendRecord, "version">;
|
|
35
|
+
|
|
36
|
+
/** A settling past its deadline, or stopped. */
|
|
37
|
+
const STOPPED: unique symbol = Symbol("stopped");
|
|
38
|
+
|
|
39
|
+
/** Writes an operation's `spend.json`, once: false when one is there already. */
|
|
40
|
+
export async function writeSpend(
|
|
41
|
+
runDir: string,
|
|
42
|
+
op: string,
|
|
43
|
+
record: SpendFields,
|
|
44
|
+
): Promise<boolean> {
|
|
45
|
+
return linkNew(spendFile(runDir, op), { version: OP_FILE_VERSIONS.spend, ...record });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What an ending attempt keeps of the operations it drew: each settled one's usage, and nothing
|
|
50
|
+
* spent for one that never reached its agent, such as a message withdrawn before it opened. One
|
|
51
|
+
* whose sessions went unread, its read cut short by a stop or its deadline, is left to the next
|
|
52
|
+
* attempt, which reads them again, unless nothing could ever read its harness's sessions. Returns what failed to
|
|
53
|
+
* write.
|
|
54
|
+
*/
|
|
55
|
+
export async function keepSpend(
|
|
56
|
+
runDir: string,
|
|
57
|
+
attempt: number,
|
|
58
|
+
usage: readonly SettledOperation[],
|
|
59
|
+
options: {
|
|
60
|
+
/** Whether a later attempt could read this operation's sessions: none, and unknown is final. */
|
|
61
|
+
reads(execution: AgentExecution): boolean;
|
|
62
|
+
},
|
|
63
|
+
): Promise<string[]> {
|
|
64
|
+
const ops = await readOps(runDir, { decisions: false }).catch(() => []);
|
|
65
|
+
const settled = new Map(usage.map((record) => [record.operationId, record]));
|
|
66
|
+
const failed: string[] = [];
|
|
67
|
+
await Promise.all(
|
|
68
|
+
ops.flatMap(({ op, spend }) => {
|
|
69
|
+
if (op.kind === "decision" || op.attempt !== attempt || spend) return [];
|
|
70
|
+
const record = settled.get(op.id);
|
|
71
|
+
if (record && record.spend === undefined && options.reads(record.execution)) return [];
|
|
72
|
+
const spent: SpendFields = record
|
|
73
|
+
? recordOf(record)
|
|
74
|
+
: {
|
|
75
|
+
billing: usage.find((each) => each.agent === op.agent)?.billing ?? "unknown",
|
|
76
|
+
spend: [],
|
|
77
|
+
};
|
|
78
|
+
return [
|
|
79
|
+
writeSpend(runDir, op.id, spent).catch((error: unknown) => {
|
|
80
|
+
failed.push(`${op.id}: ${messageOf(error)}`);
|
|
81
|
+
}),
|
|
82
|
+
];
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
return failed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function recordOf(record: SettledOperation): SpendFields {
|
|
89
|
+
return {
|
|
90
|
+
billing: record.billing,
|
|
91
|
+
...(record.spend === undefined ? {} : { spend: record.spend }),
|
|
92
|
+
...(record.reportedCost === undefined ? {} : { reportedCost: record.reportedCost }),
|
|
93
|
+
...(record.deliveredAt === undefined ? {} : { deliveredAt: record.deliveredAt }),
|
|
94
|
+
...(record.settledAt === undefined ? {} : { settledAt: record.settledAt }),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Every operation in `ops/` whose `op.json` reads, with its `spend.json` and a decision's record.
|
|
100
|
+
* A folder whose `op.json` is lost is passed over: its operation failed as it was drawn.
|
|
101
|
+
* `decisions: false` leaves out what decisions used, which an agents' estimate doesn't count.
|
|
102
|
+
* `refuse` throws on an `op.json` or `decision.json` of a version this awf doesn't know, whose
|
|
103
|
+
* operation would otherwise go uncounted; an unreadable `spend.json` is usage unknown either way.
|
|
104
|
+
*/
|
|
105
|
+
export async function readOps(
|
|
106
|
+
runDir: string,
|
|
107
|
+
options: { decisions?: boolean; refuse?: boolean } = {},
|
|
108
|
+
): Promise<RecordedOp[]> {
|
|
109
|
+
const refuse = { refuse: options.refuse === true };
|
|
110
|
+
let names: string[];
|
|
111
|
+
try {
|
|
112
|
+
names = await readdir(join(runDir, "ops"));
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (isCode(error, "ENOENT")) return [];
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
const read = await Promise.all(
|
|
118
|
+
names.map(async (name): Promise<RecordedOp | undefined> => {
|
|
119
|
+
const op = await readOpFile<OpRecord>(opFile(runDir, name), "op", refuse);
|
|
120
|
+
if (!op) return undefined;
|
|
121
|
+
if (op.kind === "decision") {
|
|
122
|
+
if (options.decisions === false) return { op };
|
|
123
|
+
const artifact = await readOpFile<DecisionArtifact>(
|
|
124
|
+
join(runDir, "ops", name, "decision.json"),
|
|
125
|
+
"decision",
|
|
126
|
+
refuse,
|
|
127
|
+
);
|
|
128
|
+
return artifact ? { op, decision: artifact.record } : { op };
|
|
129
|
+
}
|
|
130
|
+
const spend = await readOpFile<SpendRecord>(spendFile(runDir, name), "spend");
|
|
131
|
+
return spend ? { op, spend } : { op };
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
return read.filter((each) => each !== undefined);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Each claimed attempt's accounting from its operations, priced with today's table. Rejects when an
|
|
139
|
+
* operation's file is of a version this awf doesn't know: its callers show the cost as unknown.
|
|
140
|
+
*/
|
|
141
|
+
export async function readSpent(
|
|
142
|
+
runDir: string,
|
|
143
|
+
attempts: readonly ClaimedAttempt[],
|
|
144
|
+
options: { decisions?: boolean } = {},
|
|
145
|
+
): Promise<AttemptSpent[]> {
|
|
146
|
+
const ops = await readOps(runDir, { ...options, refuse: true });
|
|
147
|
+
return attemptsSpent(ops, attempts, PUBLISHED_PRICES);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Settles every agent operation an earlier attempt left without `spend.json`: one interrupted, or
|
|
152
|
+
* whose read was cut short. Called as an attempt starts, once its claim has waited for the
|
|
153
|
+
* previous one and ended its turns, and before any agent opens, while the sessions are still
|
|
154
|
+
* there. Per attempt, the agents with an operation to settle are read in the order they first
|
|
155
|
+
* started one, each request counted once, by the first to log it, as a run's end does. An agent's
|
|
156
|
+
* sessions are its `session` lines of that attempt and the one before it; what they logged from
|
|
157
|
+
* its first operation's start, which keeps a fork's copy of its parent's rows out, until the
|
|
158
|
+
* attempt ended, the next claimed, or now, is shared out by when each operation started. A calling
|
|
159
|
+
* session isn't read: its turns before and after the run are the operator's, and nothing on record
|
|
160
|
+
* says where the run's ended. Bounded as a run's end is: past its grace, or once `signal` aborts,
|
|
161
|
+
* what isn't settled yet is left for a later attempt. What fails is returned, and left too.
|
|
162
|
+
*/
|
|
163
|
+
export async function settleEarlier(
|
|
164
|
+
runDir: string,
|
|
165
|
+
options: {
|
|
166
|
+
/** The attempt settling them. */
|
|
167
|
+
attempt: number;
|
|
168
|
+
/** The claimed attempts before it. */
|
|
169
|
+
attempts: readonly ClaimedAttempt[];
|
|
170
|
+
/** `run.jsonl`'s lines, as `readRunLines` read them. */
|
|
171
|
+
lines: readonly RunLine[];
|
|
172
|
+
accounting: SessionAccounting | undefined;
|
|
173
|
+
now: number;
|
|
174
|
+
signal?: AbortSignal;
|
|
175
|
+
},
|
|
176
|
+
): Promise<{ settled: number; failed: string[] }> {
|
|
177
|
+
const ops = await readOps(runDir, { decisions: false });
|
|
178
|
+
const agentOps = ops.filter(
|
|
179
|
+
(each): each is RecordedOp & { op: AgentOpRecord } => each.op.kind !== "decision",
|
|
180
|
+
);
|
|
181
|
+
const pending = agentOps.filter(
|
|
182
|
+
({ op, spend }) => op.attempt < options.attempt && spend === undefined,
|
|
183
|
+
);
|
|
184
|
+
const failed: string[] = [];
|
|
185
|
+
let settled = 0;
|
|
186
|
+
if (pending.length === 0) return { settled, failed };
|
|
187
|
+
const { accounting } = options;
|
|
188
|
+
const deadline = {
|
|
189
|
+
unixMilliseconds:
|
|
190
|
+
options.now +
|
|
191
|
+
ACCOUNTING_GRACE_MILLISECONDS +
|
|
192
|
+
(accounting?.statusMs ?? 0) +
|
|
193
|
+
STATUS_SLACK_MILLISECONDS,
|
|
194
|
+
};
|
|
195
|
+
const bounded = <T>(call: () => Promise<T>): Promise<T | typeof STOPPED> =>
|
|
196
|
+
runUntilStopped(call, options.signal, deadline).catch(() => STOPPED);
|
|
197
|
+
const lines = unfenced(options.lines).filter(
|
|
198
|
+
(line): line is SessionLine => line.kind === "session",
|
|
199
|
+
);
|
|
200
|
+
const write = async (op: AgentOpRecord, record: SpendFields) => {
|
|
201
|
+
await writeSpend(runDir, op.id, { ...record, settledBy: options.attempt }).then(
|
|
202
|
+
(wrote) => {
|
|
203
|
+
if (wrote) settled += 1;
|
|
204
|
+
},
|
|
205
|
+
(error: unknown) => failed.push(`${op.id}: ${messageOf(error)}`),
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
for (const attempt of [...new Set(pending.map(({ op }) => op.attempt))].sort((a, b) => a - b)) {
|
|
209
|
+
const until = untilOf(attempt, options);
|
|
210
|
+
const own = agentOps
|
|
211
|
+
.filter(({ op }) => op.attempt === attempt)
|
|
212
|
+
.sort((a, b) => Date.parse(a.op.at) - Date.parse(b.op.at));
|
|
213
|
+
const claimed = new Set<string>();
|
|
214
|
+
for (const agent of [...new Set(own.map(({ op }) => op.agent))]) {
|
|
215
|
+
const theirs = own.filter(({ op }) => op.agent === agent);
|
|
216
|
+
const unsettled = theirs.filter(({ spend }) => spend === undefined);
|
|
217
|
+
// A settled agent isn't read again: the window keeps a fork's copy of its rows out.
|
|
218
|
+
if (unsettled.length === 0) continue;
|
|
219
|
+
if (theirs.some(({ op }) => op.execution.caller)) {
|
|
220
|
+
for (const { op } of unsettled) await write(op, { billing: "unknown" });
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const from = Date.parse(theirs[0]!.op.at);
|
|
224
|
+
const read = accounting
|
|
225
|
+
? await bounded(() => readAgent(agent, attempt, theirs, lines, accounting))
|
|
226
|
+
: undefined;
|
|
227
|
+
if (read === STOPPED) return { settled, failed };
|
|
228
|
+
const records = read?.filter((record) => {
|
|
229
|
+
const at = Date.parse(record.at);
|
|
230
|
+
const claim = `${record.harness}\u0000${record.key}`;
|
|
231
|
+
if (claimed.has(claim) || at < from || at > until) return false;
|
|
232
|
+
claimed.add(claim);
|
|
233
|
+
return true;
|
|
234
|
+
});
|
|
235
|
+
const shares = records && share(theirs, records);
|
|
236
|
+
const billings = new Map<string, Billing>();
|
|
237
|
+
for (const { op } of unsettled) {
|
|
238
|
+
const key = JSON.stringify(op.execution);
|
|
239
|
+
let billing = billings.get(key);
|
|
240
|
+
if (billing === undefined) {
|
|
241
|
+
const asked: Billing | typeof STOPPED = accounting
|
|
242
|
+
? await bounded(() => accounting.billing(op.execution, records))
|
|
243
|
+
: "unknown";
|
|
244
|
+
if (asked === STOPPED) return { settled, failed };
|
|
245
|
+
billing = asked;
|
|
246
|
+
billings.set(key, billing);
|
|
247
|
+
}
|
|
248
|
+
await write(op, {
|
|
249
|
+
billing,
|
|
250
|
+
...(shares ? { spend: spendOf(shares.get(op.id) ?? []) } : {}),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return { settled, failed };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
type SessionLine = Extract<RunLine, { kind: "session" }>;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Until when an attempt's agents could have logged: it ended, the next attempt claimed, or now,
|
|
262
|
+
* whichever came first. Now is safe: nothing of the attempt settling them has opened yet.
|
|
263
|
+
*/
|
|
264
|
+
function untilOf(
|
|
265
|
+
attempt: number,
|
|
266
|
+
options: { attempts: readonly ClaimedAttempt[]; now: number },
|
|
267
|
+
): number {
|
|
268
|
+
const record = options.attempts.find((each) => each.attempt === attempt);
|
|
269
|
+
const next = options.attempts.find((each) => each.attempt > attempt);
|
|
270
|
+
return Math.min(
|
|
271
|
+
options.now,
|
|
272
|
+
...(record?.ended ? [Date.parse(record.ended)] : []),
|
|
273
|
+
...(next ? [Date.parse(next.started)] : []),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* An agent's usage records in one attempt, read by its `session` lines with each of its
|
|
279
|
+
* operations' own settings and folder; undefined when none could be read. Each record carries the
|
|
280
|
+
* harness it was read for, which a request's key is unique within.
|
|
281
|
+
*/
|
|
282
|
+
async function readAgent(
|
|
283
|
+
agent: string,
|
|
284
|
+
attempt: number,
|
|
285
|
+
ops: readonly { op: AgentOpRecord }[],
|
|
286
|
+
lines: readonly SessionLine[],
|
|
287
|
+
accounting: SessionAccounting,
|
|
288
|
+
): Promise<(UsageRecord & { harness: string })[] | undefined> {
|
|
289
|
+
const found = new Map<string, UsageRecord & { harness: string }>();
|
|
290
|
+
let read = false;
|
|
291
|
+
const places = new Map(
|
|
292
|
+
ops.map(({ op }) => [JSON.stringify([op.execution, op.cwd]), op] as const),
|
|
293
|
+
);
|
|
294
|
+
for (const op of places.values()) {
|
|
295
|
+
const { harness } = op.execution;
|
|
296
|
+
const sessions = lines.filter(
|
|
297
|
+
(line) => line.agent === agent && line.harness === harness && line.attempt <= attempt,
|
|
298
|
+
);
|
|
299
|
+
const during = sessions.filter((line) => line.attempt === attempt);
|
|
300
|
+
// The session it was on as the attempt began, which a crash may have kept it from writing again.
|
|
301
|
+
const before = sessions.findLast((line) => line.attempt < attempt);
|
|
302
|
+
const seen = [...(before ? [before] : []), ...during];
|
|
303
|
+
const ids = [...new Set(seen.map((line) => line.session))];
|
|
304
|
+
const home = seen.findLast((line) => line.home !== undefined)?.home;
|
|
305
|
+
if (ids.length === 0 && home === undefined) continue;
|
|
306
|
+
const records = await accounting
|
|
307
|
+
.read(op.execution, ids, op.cwd, home)
|
|
308
|
+
.then((each) => each?.records)
|
|
309
|
+
.catch(() => undefined);
|
|
310
|
+
if (!records) continue;
|
|
311
|
+
read = true;
|
|
312
|
+
for (const record of records)
|
|
313
|
+
found.set(`${harness}\u0000${record.key}`, { ...record, harness });
|
|
314
|
+
}
|
|
315
|
+
return read ? [...found.values()] : undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* An operation owns what its agent logged from its start until the next one's; what came before
|
|
320
|
+
* the first is the first's, so a share never drops a record.
|
|
321
|
+
*/
|
|
322
|
+
function share(
|
|
323
|
+
ops: readonly { op: AgentOpRecord }[],
|
|
324
|
+
records: readonly UsageRecord[],
|
|
325
|
+
): Map<string, UsageRecord[]> {
|
|
326
|
+
const shares = new Map<string, UsageRecord[]>();
|
|
327
|
+
for (const record of records) {
|
|
328
|
+
const at = Date.parse(record.at);
|
|
329
|
+
const owner = (ops.findLast(({ op }) => Date.parse(op.at) <= at) ?? ops[0]!).op.id;
|
|
330
|
+
shares.set(owner, [...(shares.get(owner) ?? []), record]);
|
|
331
|
+
}
|
|
332
|
+
return shares;
|
|
333
|
+
}
|