@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.
- 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/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 +569 -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 +241 -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 +516 -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 +3665 -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,605 @@
|
|
|
1
|
+
import type { LogEventJson, LogsJson, RouteEnds, RunLogLine } from "@agentswf/contract/records";
|
|
2
|
+
import { messageOf } from "./errors";
|
|
3
|
+
import {
|
|
4
|
+
clockTime,
|
|
5
|
+
command,
|
|
6
|
+
continueOf,
|
|
7
|
+
currentStage,
|
|
8
|
+
failed,
|
|
9
|
+
masked,
|
|
10
|
+
nextLogs,
|
|
11
|
+
type Out,
|
|
12
|
+
parsePlace,
|
|
13
|
+
type ReadPlace,
|
|
14
|
+
said,
|
|
15
|
+
shellLine,
|
|
16
|
+
span,
|
|
17
|
+
stateWord,
|
|
18
|
+
} from "./run-commands";
|
|
19
|
+
import {
|
|
20
|
+
type Cursor,
|
|
21
|
+
cutCursor,
|
|
22
|
+
decodeCursor,
|
|
23
|
+
decodeHandle,
|
|
24
|
+
encodeCursor,
|
|
25
|
+
encodeHandle,
|
|
26
|
+
type FileCache,
|
|
27
|
+
type ReadEvent,
|
|
28
|
+
readRun,
|
|
29
|
+
timeOf,
|
|
30
|
+
} from "./run-read";
|
|
31
|
+
import { findRun, type RunStatus } from "./run-state";
|
|
32
|
+
import { describeActivity, duration } from "./run-view";
|
|
33
|
+
|
|
34
|
+
export const logsUsage = [
|
|
35
|
+
"usage: awf logs [<run>] [@<handle>] [--agent <key>] [--stage <name>] [--after <cursor>]",
|
|
36
|
+
" [--limit <n>] [--wait <seconds> | --follow] [--full] [--cwd <directory>]",
|
|
37
|
+
" [--run-root <directory>] [--json]",
|
|
38
|
+
"",
|
|
39
|
+
"What a run did and is doing: its events and what its agents did, read from their own session",
|
|
40
|
+
"files, merged by time. With no run, the newest here. Without --after, the last 40 events; with",
|
|
41
|
+
"it, what came after, oldest first. Every answer ends with the command that gets only what is",
|
|
42
|
+
"new. --wait: with nothing to show yet, wait for the first event, at most that many seconds, up",
|
|
43
|
+
"to 300; --follow prints each event as it comes until the run ends. @<handle> names one event,",
|
|
44
|
+
"and --full prints its text, with common credential shapes masked; --json carries it always.",
|
|
45
|
+
"",
|
|
46
|
+
"Exits 0 on any read that worked, 1 when there is no such run or the cursor or handle does not",
|
|
47
|
+
"fit it, 2 on a usage error, 3 when the run's files could not be read.",
|
|
48
|
+
].join("\n");
|
|
49
|
+
|
|
50
|
+
const DEFAULT_LIMIT = 40;
|
|
51
|
+
/**
|
|
52
|
+
* In a page's JSON, how much of a string the run wrote or an agent's native text: enough for a
|
|
53
|
+
* result or a message whole, short of a dumped file. `@{handle}` gives it whole.
|
|
54
|
+
*/
|
|
55
|
+
const PAGE_TEXT = 50_000;
|
|
56
|
+
const MAX_WAIT_SECONDS = 300;
|
|
57
|
+
/** After a first new event, how long to wait for the rest of its burst. */
|
|
58
|
+
const SETTLE_MS = 1_000;
|
|
59
|
+
const POLL_MS = 1_000;
|
|
60
|
+
|
|
61
|
+
type LogsOptions = {
|
|
62
|
+
ref?: string;
|
|
63
|
+
handle?: string;
|
|
64
|
+
agent?: string;
|
|
65
|
+
stage?: string;
|
|
66
|
+
after?: string;
|
|
67
|
+
limit: number;
|
|
68
|
+
waitSeconds: number;
|
|
69
|
+
follow: boolean;
|
|
70
|
+
full: boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** The clock and the pause between polls, which tests drive. */
|
|
74
|
+
export type LogsClock = { now(): number; sleep(ms: number): Promise<void> };
|
|
75
|
+
|
|
76
|
+
const REAL_CLOCK: LogsClock = { now: Date.now, sleep: (ms) => Bun.sleep(ms) };
|
|
77
|
+
|
|
78
|
+
class DoesNotFit extends Error {}
|
|
79
|
+
|
|
80
|
+
export async function logsCommand(
|
|
81
|
+
argv: readonly string[],
|
|
82
|
+
shellCwd: string,
|
|
83
|
+
out: Out,
|
|
84
|
+
clock: LogsClock = REAL_CLOCK,
|
|
85
|
+
): Promise<number> {
|
|
86
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
87
|
+
out.stdout(logsUsage);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
let place: ReadPlace;
|
|
91
|
+
let json: boolean;
|
|
92
|
+
let options: LogsOptions;
|
|
93
|
+
try {
|
|
94
|
+
const parsed = parsePlace(argv, shellCwd);
|
|
95
|
+
place = parsed.place;
|
|
96
|
+
json = parsed.json;
|
|
97
|
+
options = parseLogs(parsed.rest);
|
|
98
|
+
if (options.follow && json) throw new Error("--follow is for reading; --json takes --wait");
|
|
99
|
+
} catch (error) {
|
|
100
|
+
out.stderr(`awf: ${messageOf(error)}\n\n${logsUsage}`);
|
|
101
|
+
return 2;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const { status, alsoRunning } = await findRun(place.runRoot, options.ref);
|
|
105
|
+
const again = shellLine(command(place, "awf", "logs", status.ref));
|
|
106
|
+
let after: Cursor | undefined;
|
|
107
|
+
if (options.after !== undefined) {
|
|
108
|
+
after = decodeCursor(options.after);
|
|
109
|
+
if (!after)
|
|
110
|
+
throw new DoesNotFit(
|
|
111
|
+
`cursor ${options.after} does not fit this run; start again: ${again}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const read = new Reader(status, after, clock, options.limit * 2, json);
|
|
115
|
+
// Lines are numbered by their place in the file, past any that were skipped.
|
|
116
|
+
if (after && after.log > ((await read.next()).log.lines.at(-1)?.number ?? 0)) {
|
|
117
|
+
throw new DoesNotFit(`cursor ${options.after} does not fit this run; start again: ${again}`);
|
|
118
|
+
}
|
|
119
|
+
if (options.handle !== undefined) return await oneEvent(status, options, json, out, again);
|
|
120
|
+
if (options.follow) return await follow(read, options, place, out, clock);
|
|
121
|
+
return await page(read, alsoRunning, options, place, json, out, clock);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error instanceof DoesNotFit) {
|
|
124
|
+
out.stderr(error.message);
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
return failed(error, place, out, messageOf(error));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseLogs(rest: readonly string[]): LogsOptions {
|
|
132
|
+
const options: LogsOptions = {
|
|
133
|
+
limit: DEFAULT_LIMIT,
|
|
134
|
+
waitSeconds: 0,
|
|
135
|
+
follow: false,
|
|
136
|
+
full: false,
|
|
137
|
+
};
|
|
138
|
+
let waitGiven = false;
|
|
139
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
140
|
+
const arg = rest[index]!;
|
|
141
|
+
const value = () => {
|
|
142
|
+
const given = rest[index + 1];
|
|
143
|
+
if (given === undefined || given.startsWith("--")) throw new Error(`${arg} needs a value`);
|
|
144
|
+
index += 1;
|
|
145
|
+
return given;
|
|
146
|
+
};
|
|
147
|
+
if (arg === "--agent") options.agent = value();
|
|
148
|
+
else if (arg === "--stage") options.stage = value();
|
|
149
|
+
else if (arg === "--after") options.after = value();
|
|
150
|
+
else if (arg === "--limit") {
|
|
151
|
+
const limit = Number(value());
|
|
152
|
+
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
153
|
+
throw new Error("--limit needs a whole number");
|
|
154
|
+
options.limit = limit;
|
|
155
|
+
} else if (arg === "--wait") {
|
|
156
|
+
const seconds = Number(value());
|
|
157
|
+
if (!Number.isFinite(seconds) || seconds < 0 || seconds > MAX_WAIT_SECONDS) {
|
|
158
|
+
throw new Error(`--wait needs seconds from 0 to ${MAX_WAIT_SECONDS}`);
|
|
159
|
+
}
|
|
160
|
+
options.waitSeconds = seconds;
|
|
161
|
+
waitGiven = true;
|
|
162
|
+
} else if (arg === "--follow") options.follow = true;
|
|
163
|
+
else if (arg === "--full") options.full = true;
|
|
164
|
+
else if (arg.startsWith("@")) options.handle = arg.slice(1);
|
|
165
|
+
else if (arg.startsWith("-")) throw new Error(`unknown option: ${arg}`);
|
|
166
|
+
else if (options.ref === undefined) options.ref = arg;
|
|
167
|
+
else throw new Error(`one run at a time: ${options.ref} and ${arg}`);
|
|
168
|
+
}
|
|
169
|
+
if (options.follow && waitGiven) throw new Error("--follow and --wait do not go together");
|
|
170
|
+
if (options.handle && (waitGiven || options.follow)) {
|
|
171
|
+
throw new Error("@<handle> reads one event; it takes no --wait or --follow");
|
|
172
|
+
}
|
|
173
|
+
if (options.full && !options.handle) {
|
|
174
|
+
throw new Error("--full prints one event: name it, @<handle>");
|
|
175
|
+
}
|
|
176
|
+
return options;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Reads one run again and again from one cursor, keeping its files found between reads. */
|
|
180
|
+
class Reader {
|
|
181
|
+
readonly #files: FileCache = new Map();
|
|
182
|
+
constructor(
|
|
183
|
+
public status: RunStatus,
|
|
184
|
+
readonly from: Cursor | undefined,
|
|
185
|
+
readonly clock: LogsClock,
|
|
186
|
+
/** Lines of each native file one read takes: about what a page shows. */
|
|
187
|
+
readonly lines: number,
|
|
188
|
+
/** Keeps each activity's native text. */
|
|
189
|
+
readonly full: boolean,
|
|
190
|
+
) {}
|
|
191
|
+
|
|
192
|
+
next(cursor = this.from) {
|
|
193
|
+
return readRun(this.status.run.dir, this.status.run.record.cwd, cursor, {
|
|
194
|
+
ended: this.status.state !== "running",
|
|
195
|
+
files: this.#files,
|
|
196
|
+
now: this.clock.now(),
|
|
197
|
+
lines: this.lines,
|
|
198
|
+
...(this.full ? { full: true } : {}),
|
|
199
|
+
...(cursor ? {} : { tail: true }),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async refresh(runRoot: string): Promise<RunStatus> {
|
|
204
|
+
this.status = (await findRun(runRoot, this.status.ref)).status;
|
|
205
|
+
return this.status;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
type Read = Awaited<ReturnType<Reader["next"]>>;
|
|
210
|
+
|
|
211
|
+
/** Each operation's stage and agent, from the lines that name them. */
|
|
212
|
+
type Places = { stage: Map<string, string>; agent: Map<string, string> };
|
|
213
|
+
|
|
214
|
+
function operationPlaces(lines: readonly { line: RunLogLine }[]): Places {
|
|
215
|
+
const places: Places = { stage: new Map(), agent: new Map() };
|
|
216
|
+
for (const { line } of lines) {
|
|
217
|
+
if (!("operationId" in line) || !line.operationId) continue;
|
|
218
|
+
if ("stage" in line && line.stage) places.stage.set(line.operationId, line.stage);
|
|
219
|
+
if ("agent" in line && line.agent) places.agent.set(line.operationId, line.agent);
|
|
220
|
+
}
|
|
221
|
+
return places;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function agentOf(event: ReadEvent, places: Places): string | undefined {
|
|
225
|
+
if (event.source === "activity") return event.agent;
|
|
226
|
+
if ("agent" in event.line && event.line.agent) return event.line.agent;
|
|
227
|
+
// A session a headless turn named names its operation, and so its agent.
|
|
228
|
+
return "operationId" in event.line && event.line.operationId
|
|
229
|
+
? places.agent.get(event.line.operationId)
|
|
230
|
+
: undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function stageOf(event: ReadEvent, places: Places): string | undefined {
|
|
234
|
+
if (event.source === "activity") {
|
|
235
|
+
return event.operation ? places.stage.get(event.operation) : undefined;
|
|
236
|
+
}
|
|
237
|
+
return "stage" in event.line ? event.line.stage : undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** The agents a messaging line is between, so `--agent` follows it from either side. */
|
|
241
|
+
function sidesOf(event: ReadEvent): readonly string[] {
|
|
242
|
+
if (event.source === "activity") return [];
|
|
243
|
+
const { line } = event;
|
|
244
|
+
switch (line.kind) {
|
|
245
|
+
case "route-opened":
|
|
246
|
+
case "route-closed":
|
|
247
|
+
return line.between ?? [line.from, line.to];
|
|
248
|
+
case "message-sent":
|
|
249
|
+
case "message-refused":
|
|
250
|
+
case "message-delivery":
|
|
251
|
+
case "message-undelivered":
|
|
252
|
+
return [line.from, line.to];
|
|
253
|
+
case "request-ended":
|
|
254
|
+
return [line.owedBy, line.asker];
|
|
255
|
+
default:
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function kept(read: Read, options: LogsOptions) {
|
|
261
|
+
const stages = operationPlaces(read.log.lines);
|
|
262
|
+
const events = read.events.filter(
|
|
263
|
+
(event) =>
|
|
264
|
+
(options.agent === undefined ||
|
|
265
|
+
agentOf(event, stages) === options.agent ||
|
|
266
|
+
sidesOf(event).includes(options.agent)) &&
|
|
267
|
+
(options.stage === undefined || stageOf(event, stages) === options.stage),
|
|
268
|
+
);
|
|
269
|
+
return { events, stages };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function sameLine(a: ReadEvent, b: ReadEvent): boolean {
|
|
273
|
+
const first = decodeHandle(a.handle);
|
|
274
|
+
const second = decodeHandle(b.handle);
|
|
275
|
+
return (
|
|
276
|
+
first?.kind === "activity" &&
|
|
277
|
+
second?.kind === "activity" &&
|
|
278
|
+
first.file === second.file &&
|
|
279
|
+
first.offset === second.offset
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function page(
|
|
284
|
+
reader: Reader,
|
|
285
|
+
alsoRunning: string[],
|
|
286
|
+
options: LogsOptions,
|
|
287
|
+
place: ReadPlace,
|
|
288
|
+
json: boolean,
|
|
289
|
+
out: Out,
|
|
290
|
+
clock: LogsClock,
|
|
291
|
+
): Promise<number> {
|
|
292
|
+
let read = await reader.next();
|
|
293
|
+
let { events, stages } = kept(read, options);
|
|
294
|
+
const deadline = clock.now() + options.waitSeconds * 1000;
|
|
295
|
+
let waited = false;
|
|
296
|
+
// Without a cursor, a wait reads on from its first read: nothing before it is kept.
|
|
297
|
+
let from = reader.from;
|
|
298
|
+
// Waits while there is nothing it keeps to show and the run goes on.
|
|
299
|
+
while (
|
|
300
|
+
events.length === 0 &&
|
|
301
|
+
!read.more &&
|
|
302
|
+
reader.status.state === "running" &&
|
|
303
|
+
clock.now() < deadline
|
|
304
|
+
) {
|
|
305
|
+
waited = true;
|
|
306
|
+
if (!reader.from) from = read.cursor;
|
|
307
|
+
await clock.sleep(POLL_MS);
|
|
308
|
+
await reader.refresh(place.runRoot);
|
|
309
|
+
read = await reader.next(from);
|
|
310
|
+
({ events, stages } = kept(read, options));
|
|
311
|
+
if (events.length > 0) {
|
|
312
|
+
// A burst comes back as one page.
|
|
313
|
+
await clock.sleep(SETTLE_MS);
|
|
314
|
+
read = await reader.next(from);
|
|
315
|
+
({ events, stages } = kept(read, options));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const status = reader.status;
|
|
319
|
+
|
|
320
|
+
// Without a cursor the newest `limit`; with one, the oldest `limit` after it.
|
|
321
|
+
let shown: ReadEvent[];
|
|
322
|
+
let cursor = read.cursor;
|
|
323
|
+
let older = false;
|
|
324
|
+
let newer = false;
|
|
325
|
+
if (!reader.from) {
|
|
326
|
+
shown = events.slice(-options.limit);
|
|
327
|
+
older = events.length > shown.length || read.cut;
|
|
328
|
+
} else {
|
|
329
|
+
let end = Math.min(options.limit, events.length);
|
|
330
|
+
// A line's items stay together.
|
|
331
|
+
while (end < events.length && sameLine(events[end - 1]!, events[end]!)) end += 1;
|
|
332
|
+
shown = events.slice(0, end);
|
|
333
|
+
newer = end < events.length || read.more;
|
|
334
|
+
if (end < events.length) {
|
|
335
|
+
cursor = cutCursor(read.events, read.events.indexOf(events[end]!), read.cursor);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const stage = currentStage(status, read.log.lines);
|
|
339
|
+
const filters = [
|
|
340
|
+
...(options.agent ? ["--agent", options.agent] : []),
|
|
341
|
+
...(options.stage ? ["--stage", options.stage] : []),
|
|
342
|
+
];
|
|
343
|
+
// A run that ended has a next only while its page stopped before the newest event.
|
|
344
|
+
const running = status.state === "running";
|
|
345
|
+
const next =
|
|
346
|
+
running || newer ? nextLogs(status, place, cursor, filters, running && !newer) : undefined;
|
|
347
|
+
if (json) {
|
|
348
|
+
const { started, ended } = span(status, clock.now());
|
|
349
|
+
const document: LogsJson = {
|
|
350
|
+
v: 1,
|
|
351
|
+
run: {
|
|
352
|
+
ref: status.ref,
|
|
353
|
+
state: status.state,
|
|
354
|
+
checked: status.checked,
|
|
355
|
+
...(stage ? { stage } : {}),
|
|
356
|
+
ambiguous: alsoRunning.length > 0,
|
|
357
|
+
...(status.state !== "running" && ended !== undefined ? { duration: ended - started } : {}),
|
|
358
|
+
},
|
|
359
|
+
events: shown.map((event) => publish(event, stages, PAGE_TEXT)),
|
|
360
|
+
cursor: encodeCursor(cursor),
|
|
361
|
+
more: { ...(older ? { older: true } : {}), ...(newer ? { newer: true } : {}) },
|
|
362
|
+
...(next ? { next } : {}),
|
|
363
|
+
};
|
|
364
|
+
out.stdout(JSON.stringify(document, null, 2));
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
|
367
|
+
const lastAt = read.log.lines.at(-1);
|
|
368
|
+
const lines: string[] = [
|
|
369
|
+
header(status, stage, lastAt ? Date.parse(timeOf(lastAt.line)) : undefined),
|
|
370
|
+
];
|
|
371
|
+
if (alsoRunning.length > 0) {
|
|
372
|
+
const count = alsoRunning.length + (status.state === "running" ? 1 : 0);
|
|
373
|
+
lines.push(`note: ${count} runs running; also ${alsoRunning.join(", ")}`);
|
|
374
|
+
}
|
|
375
|
+
if (older) {
|
|
376
|
+
const of = options.agent ? ` for ${options.agent}` : "";
|
|
377
|
+
// A cut read has more than any --limit reaches: paging from the start reads it all.
|
|
378
|
+
const more = read.cut
|
|
379
|
+
? `from the start: ${shellLine([...command(place, "awf", "logs", status.ref), ...filters, "--after", encodeCursor({ log: 0, files: {} })])}`
|
|
380
|
+
: "raise --limit for more";
|
|
381
|
+
const total = read.cut ? `the last ${events.length} read` : `${events.length}`;
|
|
382
|
+
lines.push(`(showing ${shown.length} of ${total}${of}; ${more})`);
|
|
383
|
+
}
|
|
384
|
+
lines.push(...shown.map((event) => describeEvent(event, stages)));
|
|
385
|
+
lines.push(
|
|
386
|
+
trailer(
|
|
387
|
+
status,
|
|
388
|
+
read,
|
|
389
|
+
place,
|
|
390
|
+
next,
|
|
391
|
+
stage,
|
|
392
|
+
waited && shown.length === 0 ? options.waitSeconds : undefined,
|
|
393
|
+
),
|
|
394
|
+
);
|
|
395
|
+
out.stdout(lines.join("\n"));
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function follow(
|
|
400
|
+
reader: Reader,
|
|
401
|
+
options: LogsOptions,
|
|
402
|
+
place: ReadPlace,
|
|
403
|
+
out: Out,
|
|
404
|
+
clock: LogsClock,
|
|
405
|
+
): Promise<number> {
|
|
406
|
+
let cursor = reader.from;
|
|
407
|
+
let read = await reader.next(cursor);
|
|
408
|
+
out.stdout(header(reader.status, currentStage(reader.status, read.log.lines)));
|
|
409
|
+
for (let first = true; ; first = false) {
|
|
410
|
+
const { events, stages } = kept(read, options);
|
|
411
|
+
// Without a cursor it starts from the last `--limit`, as a page would.
|
|
412
|
+
const shown = first && !reader.from ? events.slice(-options.limit) : events;
|
|
413
|
+
for (const event of shown) out.stdout(describeEvent(event, stages));
|
|
414
|
+
cursor = read.cursor;
|
|
415
|
+
// A read that stopped short goes on at once.
|
|
416
|
+
if (!read.more) {
|
|
417
|
+
if (reader.status.state !== "running") break;
|
|
418
|
+
await clock.sleep(POLL_MS);
|
|
419
|
+
}
|
|
420
|
+
await reader.refresh(place.runRoot);
|
|
421
|
+
read = await reader.next(cursor);
|
|
422
|
+
}
|
|
423
|
+
out.stdout(trailer(reader.status, read, place, undefined, undefined, undefined));
|
|
424
|
+
return 0;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function oneEvent(
|
|
428
|
+
status: RunStatus,
|
|
429
|
+
options: LogsOptions,
|
|
430
|
+
json: boolean,
|
|
431
|
+
out: Out,
|
|
432
|
+
again: string,
|
|
433
|
+
): Promise<number> {
|
|
434
|
+
const missing = new DoesNotFit(
|
|
435
|
+
`@${options.handle} is no event of ${status.ref}; see its events: ${again}`,
|
|
436
|
+
);
|
|
437
|
+
const handle = decodeHandle(options.handle!);
|
|
438
|
+
if (!handle) throw missing;
|
|
439
|
+
// Read from where the handle points, and only its file.
|
|
440
|
+
const read = await readRun(
|
|
441
|
+
status.run.dir,
|
|
442
|
+
status.run.record.cwd,
|
|
443
|
+
handle.kind === "event"
|
|
444
|
+
? { log: handle.line - 1, files: {} }
|
|
445
|
+
: { log: Number.MAX_SAFE_INTEGER, files: { [handle.file]: handle.offset } },
|
|
446
|
+
{
|
|
447
|
+
ended: status.state !== "running",
|
|
448
|
+
full: options.full || json,
|
|
449
|
+
only: handle.kind === "event" ? "" : handle.file,
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
const stages = operationPlaces(read.log.lines);
|
|
453
|
+
const event = read.events.find((each) => each.handle === encodeHandle(handle));
|
|
454
|
+
if (!event) throw missing;
|
|
455
|
+
if (json) {
|
|
456
|
+
const document: LogsJson = {
|
|
457
|
+
v: 1,
|
|
458
|
+
run: { ref: status.ref, state: status.state, checked: status.checked },
|
|
459
|
+
events: [publish(event, stages)],
|
|
460
|
+
};
|
|
461
|
+
out.stdout(JSON.stringify(document, null, 2));
|
|
462
|
+
return 0;
|
|
463
|
+
}
|
|
464
|
+
const lines = [describeEvent(event, stages)];
|
|
465
|
+
if (options.full) {
|
|
466
|
+
lines.unshift(
|
|
467
|
+
`(${event.source === "activity" ? "native text" : "the log's line"}; common credential shapes are masked)`,
|
|
468
|
+
);
|
|
469
|
+
const text =
|
|
470
|
+
event.source === "activity" ? event.text : JSON.stringify(masked(event.line), null, 2);
|
|
471
|
+
if (text) lines.push(...text.split("\n").map((line) => ` ${line}`));
|
|
472
|
+
}
|
|
473
|
+
out.stdout(lines.join("\n"));
|
|
474
|
+
return 0;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function header(status: RunStatus, stage: string | undefined, lastEventAt?: number): string {
|
|
478
|
+
return [status.ref, stateWord(status, lastEventAt), ...(stage ? [`stage ${stage}`] : [])].join(
|
|
479
|
+
" ",
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function trailer(
|
|
484
|
+
status: RunStatus,
|
|
485
|
+
read: Read,
|
|
486
|
+
place: ReadPlace,
|
|
487
|
+
next: string[] | undefined,
|
|
488
|
+
stage: string | undefined,
|
|
489
|
+
nothingIn: number | undefined,
|
|
490
|
+
): string {
|
|
491
|
+
if (status.state === "running") {
|
|
492
|
+
const said = `next: ${shellLine(next ?? command(place, "awf", "logs", status.ref))}`;
|
|
493
|
+
if (nothingIn === undefined) return said;
|
|
494
|
+
return `nothing new in ${nothingIn}s · running${stage ? `, stage ${stage}` : ""} · ${said}`;
|
|
495
|
+
}
|
|
496
|
+
if (next) return `next: ${shellLine(next)} · run ${status.state}, more to read`;
|
|
497
|
+
if (status.state === "interrupted") {
|
|
498
|
+
const go = continueOf(status, read.log.lines, place);
|
|
499
|
+
return `run interrupted${go ? ` · continue: ${go}` : ""}`;
|
|
500
|
+
}
|
|
501
|
+
const { ended, started } = span(status, Date.now());
|
|
502
|
+
const took = ended === undefined ? "" : ` in ${duration(ended - started)}`;
|
|
503
|
+
return `run ended: ${status.state}${took} · result: ${shellLine(command(place, "awf", "runs", status.ref))}`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** An event as `--json` publishes it: the line's own fields, or the activity, with its keys. */
|
|
507
|
+
function publish(event: ReadEvent, stages: Places, limit = Number.POSITIVE_INFINITY): LogEventJson {
|
|
508
|
+
if (event.source === "activity") {
|
|
509
|
+
const stage = event.operation ? stages.stage.get(event.operation) : undefined;
|
|
510
|
+
return {
|
|
511
|
+
handle: event.handle,
|
|
512
|
+
at: event.at,
|
|
513
|
+
kind: "activity",
|
|
514
|
+
operation: event.operation,
|
|
515
|
+
...(event.agent ? { agent: event.agent } : {}),
|
|
516
|
+
...(stage ? { stage } : {}),
|
|
517
|
+
activity: event.activity,
|
|
518
|
+
...(event.text === undefined ? {} : { text: masked(event.text, limit) as string }),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
const { version: _version, ...fields } = event.line;
|
|
522
|
+
return {
|
|
523
|
+
handle: event.handle,
|
|
524
|
+
at: event.at,
|
|
525
|
+
...(masked(fields, limit) as typeof fields),
|
|
526
|
+
...("operationId" in fields ? { operation: fields.operationId } : {}),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** `@e216 09:27:02 fix fixer turn ended answered 1m48s`. */
|
|
531
|
+
function describeEvent(event: ReadEvent, stages: Places): string {
|
|
532
|
+
const agent = agentOf(event, stages);
|
|
533
|
+
const where = [stageOf(event, stages), agent && said(agent, 60)].filter(Boolean).join(" ");
|
|
534
|
+
const what =
|
|
535
|
+
event.source === "activity" ? describeActivity(event.activity) : describeLine(event.line);
|
|
536
|
+
return `@${event.handle} ${clockTime(event.at)} ${where ? `${where} ` : ""}${what}`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function describeLine(line: RunLogLine): string {
|
|
540
|
+
const operation = (of: "turn" | "compact") => (of === "compact" ? "compaction" : "turn");
|
|
541
|
+
switch (line.kind) {
|
|
542
|
+
case "attempt-started":
|
|
543
|
+
return `attempt ${line.attempt} started`;
|
|
544
|
+
case "attempt-ended":
|
|
545
|
+
return `attempt ${line.attempt} ended ${line.outcome}`;
|
|
546
|
+
case "stage-started":
|
|
547
|
+
return "stage started";
|
|
548
|
+
case "stage-ended":
|
|
549
|
+
return `stage ${line.source ?? "ended"} ${line.outcome}`;
|
|
550
|
+
case "operation-started":
|
|
551
|
+
return `${operation(line.of)} started${line.label ? ` label "${said(line.label)}"` : ""}`;
|
|
552
|
+
case "operation-failed":
|
|
553
|
+
return `${operation(line.of)} failed ${said(line.error)}`;
|
|
554
|
+
case "turn":
|
|
555
|
+
case "compact": {
|
|
556
|
+
const took =
|
|
557
|
+
line.deliveredAt && line.settledAt
|
|
558
|
+
? ` ${duration(Date.parse(line.settledAt) - Date.parse(line.deliveredAt))}`
|
|
559
|
+
: "";
|
|
560
|
+
return `${operation(line.kind)} ended ${line.outcome}${took}`;
|
|
561
|
+
}
|
|
562
|
+
case "session-known":
|
|
563
|
+
return `session ${line.session.harness} ${line.session.id}`;
|
|
564
|
+
case "agent-opened":
|
|
565
|
+
return `opened ${line.harness}${line.caller ? " (the calling session)" : ""}`;
|
|
566
|
+
case "agent-stopped":
|
|
567
|
+
return "stopped";
|
|
568
|
+
case "agent-forked":
|
|
569
|
+
return `forked from ${line.from ? said(line.from, 60) : "the calling session"}`;
|
|
570
|
+
case "log":
|
|
571
|
+
return said(line.text);
|
|
572
|
+
case "warning":
|
|
573
|
+
return `warning: ${said(line.text)}`;
|
|
574
|
+
case "decision-asked":
|
|
575
|
+
return `decision "${said(line.key)}" asked ${line.model}`;
|
|
576
|
+
case "decision-ended":
|
|
577
|
+
return `decision ${line.decision} ${line.outcome}${line.error ? ` ${said(line.error)}` : ""}`;
|
|
578
|
+
case "route-opened":
|
|
579
|
+
return `route ${routeEnds(line)} opened "${said(line.purpose)}" ${line.maxMessages} messages`;
|
|
580
|
+
case "route-closed":
|
|
581
|
+
return `route ${routeEnds(line)} closed ${line.reason}`;
|
|
582
|
+
case "message-sent":
|
|
583
|
+
return `message ${line.message} ${said(line.from, 60)} → ${said(line.to, 60)} ${line.bytes} B${
|
|
584
|
+
line.replyTo === undefined ? "" : `, reply to ${line.replyTo}`
|
|
585
|
+
}${
|
|
586
|
+
line.awaitsReply
|
|
587
|
+
? `, reply owed ${said(line.from, 60)} awaits it until ${clockTime(Date.parse(line.awaitsReply.until))}`
|
|
588
|
+
: ""
|
|
589
|
+
}`;
|
|
590
|
+
case "message-refused":
|
|
591
|
+
return `${line.command} ${said(line.from, 60)} → ${said(line.to, 60)} refused ${line.code}`;
|
|
592
|
+
case "message-delivery":
|
|
593
|
+
return `message ${line.message} ${line.state}`;
|
|
594
|
+
case "message-undelivered":
|
|
595
|
+
return `message ${line.message} undelivered ${line.reason}`;
|
|
596
|
+
case "request-ended":
|
|
597
|
+
return `request ${line.message} ${line.end}${line.reason ? ` ${said(line.reason)}` : ""}`;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function routeEnds(line: RouteEnds): string {
|
|
602
|
+
return line.between
|
|
603
|
+
? `${said(line.between[0], 60)} ↔ ${said(line.between[1], 60)}`
|
|
604
|
+
: `${said(line.from, 60)} → ${said(line.to, 60)}`;
|
|
605
|
+
}
|
package/src/machine.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { RunRecord } from "@agentswf/contract/records";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What awf keeps per machine, under `~/.awf`, whatever a run's root: the marks of sessions runs
|
|
6
|
+
* drive, each sandbox's homes, kept outside the project, whose run root a provider denies, and the
|
|
7
|
+
* Herdr sessions it runs agents in: their config, and each run's mark on its workspace.
|
|
8
|
+
*/
|
|
9
|
+
export function machinePaths(home: string): {
|
|
10
|
+
root: string;
|
|
11
|
+
callers: string;
|
|
12
|
+
sandboxes: string;
|
|
13
|
+
herdr: string;
|
|
14
|
+
} {
|
|
15
|
+
const root = join(home, ".awf");
|
|
16
|
+
return {
|
|
17
|
+
root,
|
|
18
|
+
callers: join(root, "callers"),
|
|
19
|
+
sandboxes: join(root, "sandboxes"),
|
|
20
|
+
herdr: join(root, "herdr"),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Where a run's sandboxes keep their homes: found by the run, though outside it. */
|
|
25
|
+
export function sandboxesDirOf(home: string, run: Pick<RunRecord, "workflow" | "id">): string {
|
|
26
|
+
return join(machinePaths(home).sandboxes, run.workflow, run.id);
|
|
27
|
+
}
|