@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/jsonl.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { open, readFile } from "node:fs/promises";
|
|
2
2
|
import { isCode } from "./files";
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const queues = new Map<string, Promise<void>>();
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
export function appendLine(path: string, line: string): Promise<void> {
|
|
7
|
+
return queued(path, () => appendCompleteLine(path, line));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Runs `work` once every earlier write queued on `path` has settled, failed or not. */
|
|
11
|
+
export async function queued(path: string, work: () => Promise<void>): Promise<void> {
|
|
12
|
+
const current = (queues.get(path) ?? Promise.resolve()).catch(() => undefined).then(work);
|
|
13
|
+
queues.set(path, current);
|
|
10
14
|
try {
|
|
11
15
|
await current;
|
|
12
16
|
} finally {
|
|
13
|
-
if (
|
|
17
|
+
if (queues.get(path) === current) queues.delete(path);
|
|
14
18
|
}
|
|
15
19
|
}
|
|
16
20
|
|
package/src/keep-alive.ts
CHANGED
|
@@ -24,27 +24,15 @@ export function asTimeoutHandler<T>(fn: () => T): T {
|
|
|
24
24
|
return inHandler.run(true, fn);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
* Its deprecated `timeoutMs` is reported, and refused beside `window`.
|
|
30
|
-
*/
|
|
31
|
-
export function checkedKeepAlive(key: string, given: unknown, deprecated: () => void): KeepAlive {
|
|
27
|
+
/** `keepAlive` as an open gave it, checked: a plain caller can get wrong what a typed one can't. */
|
|
28
|
+
export function checkedKeepAlive(key: string, given: unknown): KeepAlive {
|
|
32
29
|
if (typeof given !== "object" || given === null)
|
|
33
30
|
throw new Error(`agent ${key}: keepAlive is { window?, onTimeout? }`);
|
|
34
|
-
const { window,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
`agent ${key}: keepAlive's window is given alone: timeoutMs is its deprecated form`,
|
|
40
|
-
);
|
|
41
|
-
windowMs = positiveDurationMilliseconds(window, `agent ${key}: keepAlive's window`);
|
|
42
|
-
} else if (timeoutMs != null) {
|
|
43
|
-
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
44
|
-
throw new Error(`agent ${key}: keepAlive's timeoutMs is a positive number of milliseconds`);
|
|
45
|
-
deprecated();
|
|
46
|
-
windowMs = timeoutMs;
|
|
47
|
-
}
|
|
31
|
+
const { window, onTimeout } = given as Record<string, unknown>;
|
|
32
|
+
const windowMs =
|
|
33
|
+
window == null
|
|
34
|
+
? undefined
|
|
35
|
+
: positiveDurationMilliseconds(window, `agent ${key}: keepAlive's window`);
|
|
48
36
|
if (onTimeout !== undefined && typeof onTimeout !== "function")
|
|
49
37
|
throw new Error(`agent ${key}: keepAlive's onTimeout is a function`);
|
|
50
38
|
// A copy: a later change to the caller's object would skip this check.
|
package/src/logs-command.ts
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import type { LogEventJson, LogsJson, RouteEnds, RunLogLine } from "@agentswf/contract/records";
|
|
3
3
|
import type { JsonObject } from "@agentswf/contract/workflow";
|
|
4
|
-
import {
|
|
4
|
+
import { duration } from "./accounting/format";
|
|
5
|
+
import { command, type Out, parsePlace, type ReadPlace, shellLine } from "./command-line";
|
|
6
|
+
import { parseWait } from "./duration";
|
|
5
7
|
import { messageOf } from "./errors";
|
|
8
|
+
import { exists } from "./files";
|
|
6
9
|
import help from "./help/logs.txt" with { type: "text" };
|
|
10
|
+
import { screenFile } from "./ops";
|
|
11
|
+
import { asksJson, printRefusal, refusalLine } from "./refusals";
|
|
7
12
|
import {
|
|
8
|
-
clockTime,
|
|
9
|
-
command,
|
|
10
13
|
continueOf,
|
|
11
14
|
currentStage,
|
|
12
15
|
failed,
|
|
13
|
-
|
|
16
|
+
MAX_WAIT_SECONDS,
|
|
14
17
|
nextLogs,
|
|
15
|
-
|
|
16
|
-
parsePlace,
|
|
17
|
-
type ReadPlace,
|
|
18
|
-
said,
|
|
19
|
-
shellLine,
|
|
18
|
+
POLL_MS,
|
|
20
19
|
span,
|
|
21
20
|
stateWord,
|
|
22
21
|
} from "./run-commands";
|
|
@@ -30,11 +29,16 @@ import {
|
|
|
30
29
|
type FileCache,
|
|
31
30
|
type ReadEvent,
|
|
32
31
|
readRun,
|
|
32
|
+
START,
|
|
33
33
|
timeOf,
|
|
34
34
|
} from "./run-read";
|
|
35
|
-
import { findRun, findRunByRef, type RunStatus } from "./run-state";
|
|
35
|
+
import { checkRunEngine, findRun, findRunByRef, type RunStatus } from "./run-state";
|
|
36
|
+
import { clockTime, masked, said } from "./run-text";
|
|
36
37
|
import { describeActivity } from "./run-view";
|
|
37
38
|
|
|
39
|
+
/** `awf logs --json`'s `"v"`, which its refusals carry too. */
|
|
40
|
+
export const LOGS_V = 1 satisfies LogsJson["v"];
|
|
41
|
+
|
|
38
42
|
export const logsUsage = help.trimEnd();
|
|
39
43
|
|
|
40
44
|
const DEFAULT_LIMIT = 40;
|
|
@@ -43,10 +47,8 @@ const DEFAULT_LIMIT = 40;
|
|
|
43
47
|
* result or a message whole, short of a dumped file. `@{handle}` gives it whole.
|
|
44
48
|
*/
|
|
45
49
|
const PAGE_TEXT = 50_000;
|
|
46
|
-
const MAX_WAIT_SECONDS = 300;
|
|
47
50
|
/** After a first new event, how long to wait for the rest of its burst. */
|
|
48
51
|
const SETTLE_MS = 1_000;
|
|
49
|
-
const POLL_MS = 1_000;
|
|
50
52
|
|
|
51
53
|
type LogsOptions = {
|
|
52
54
|
ref?: string;
|
|
@@ -74,10 +76,6 @@ export async function logsCommand(
|
|
|
74
76
|
clock: LogsClock = REAL_CLOCK,
|
|
75
77
|
home: string = homedir(),
|
|
76
78
|
): Promise<number> {
|
|
77
|
-
if (argv.includes("-h") || argv.includes("--help")) {
|
|
78
|
-
out.stdout(logsUsage);
|
|
79
|
-
return 0;
|
|
80
|
-
}
|
|
81
79
|
let place: ReadPlace;
|
|
82
80
|
let json: boolean;
|
|
83
81
|
let options: LogsOptions;
|
|
@@ -85,10 +83,10 @@ export async function logsCommand(
|
|
|
85
83
|
const parsed = parsePlace(argv, shellCwd);
|
|
86
84
|
place = parsed.place;
|
|
87
85
|
json = parsed.json;
|
|
88
|
-
options = parseLogs(parsed.rest
|
|
86
|
+
options = parseLogs(parsed.rest);
|
|
89
87
|
if (options.follow && json) throw new Error("--follow is for reading; --json takes --wait");
|
|
90
88
|
} catch (error) {
|
|
91
|
-
out
|
|
89
|
+
printRefusal(out, { json: asksJson(argv), v: LOGS_V }, error, "usage", logsUsage);
|
|
92
90
|
return 2;
|
|
93
91
|
}
|
|
94
92
|
try {
|
|
@@ -96,6 +94,7 @@ export async function logsCommand(
|
|
|
96
94
|
options.ref === undefined
|
|
97
95
|
? await findRun(place.runRoot, undefined)
|
|
98
96
|
: { status: await findRunByRef(options.ref, place, home), alsoRunning: [] };
|
|
97
|
+
await checkRunEngine(status, home);
|
|
99
98
|
const again = shellLine(command(place, "awf", "logs", status.ref));
|
|
100
99
|
let after: Cursor | undefined;
|
|
101
100
|
if (options.after !== undefined) {
|
|
@@ -107,22 +106,29 @@ export async function logsCommand(
|
|
|
107
106
|
}
|
|
108
107
|
const read = new Reader(status, after, clock, options.limit * 2, json);
|
|
109
108
|
// Lines are numbered by their place in the file, past any that were skipped.
|
|
110
|
-
if (after
|
|
111
|
-
|
|
109
|
+
if (after) {
|
|
110
|
+
const { log } = await read.next();
|
|
111
|
+
const past = after.run > log.runLines && log.unreadSessions === undefined;
|
|
112
|
+
if (after.log > (log.lines.at(-1)?.number ?? 0) || past) {
|
|
113
|
+
throw new DoesNotFit(
|
|
114
|
+
`cursor ${options.after} does not fit this run; start again: ${again}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
112
117
|
}
|
|
113
118
|
if (options.handle !== undefined) return await oneEvent(status, options, json, out, again);
|
|
114
119
|
if (options.follow) return await follow(read, options, place, out, clock);
|
|
115
120
|
return await page(read, alsoRunning, options, place, json, out, clock);
|
|
116
121
|
} catch (error) {
|
|
117
122
|
if (error instanceof DoesNotFit) {
|
|
123
|
+
if (json) out.stdout(refusalLine(LOGS_V, "cursor", error.message));
|
|
118
124
|
out.stderr(error.message);
|
|
119
125
|
return 1;
|
|
120
126
|
}
|
|
121
|
-
return failed(error, place, out, messageOf(error));
|
|
127
|
+
return failed(error, place, out, messageOf(error), { json, v: LOGS_V });
|
|
122
128
|
}
|
|
123
129
|
}
|
|
124
130
|
|
|
125
|
-
function parseLogs(rest: readonly string[]
|
|
131
|
+
function parseLogs(rest: readonly string[]): LogsOptions {
|
|
126
132
|
const options: LogsOptions = {
|
|
127
133
|
limit: DEFAULT_LIMIT,
|
|
128
134
|
waitSeconds: 0,
|
|
@@ -147,7 +153,7 @@ function parseLogs(rest: readonly string[], deprecated: (line: string) => void):
|
|
|
147
153
|
throw new Error("--limit needs a whole number");
|
|
148
154
|
options.limit = limit;
|
|
149
155
|
} else if (arg === "--wait") {
|
|
150
|
-
options.waitSeconds = parseWait(value(), MAX_WAIT_SECONDS
|
|
156
|
+
options.waitSeconds = parseWait(value(), MAX_WAIT_SECONDS);
|
|
151
157
|
waitGiven = true;
|
|
152
158
|
} else if (arg === "--follow") options.follow = true;
|
|
153
159
|
else if (arg === "--full") options.full = true;
|
|
@@ -224,8 +230,9 @@ function operationPlaces(lines: readonly { line: RunLogLine }[]): Places {
|
|
|
224
230
|
|
|
225
231
|
function agentOf(event: ReadEvent, places: Places): string | undefined {
|
|
226
232
|
if (event.source === "activity") return event.agent;
|
|
233
|
+
if (event.source === "run") return event.line.agent;
|
|
227
234
|
if ("agent" in event.line && event.line.agent) return event.line.agent;
|
|
228
|
-
// A
|
|
235
|
+
// A line that names only its operation, such as a message's delivery, is its operation's agent's.
|
|
229
236
|
return "operationId" in event.line && event.line.operationId
|
|
230
237
|
? places.agent.get(event.line.operationId)
|
|
231
238
|
: undefined;
|
|
@@ -235,12 +242,15 @@ function stageOf(event: ReadEvent, places: Places): string | undefined {
|
|
|
235
242
|
if (event.source === "activity") {
|
|
236
243
|
return event.operation ? places.stage.get(event.operation) : undefined;
|
|
237
244
|
}
|
|
245
|
+
if (event.source === "run") {
|
|
246
|
+
return event.line.operation ? places.stage.get(event.line.operation) : undefined;
|
|
247
|
+
}
|
|
238
248
|
return "stage" in event.line ? event.line.stage : undefined;
|
|
239
249
|
}
|
|
240
250
|
|
|
241
251
|
/** The agents a messaging line is between, so `--agent` follows it from either side. */
|
|
242
252
|
function sidesOf(event: ReadEvent): readonly string[] {
|
|
243
|
-
if (event.source
|
|
253
|
+
if (event.source !== "log") return [];
|
|
244
254
|
const { line } = event;
|
|
245
255
|
switch (line.kind) {
|
|
246
256
|
case "route-opened":
|
|
@@ -258,14 +268,32 @@ function sidesOf(event: ReadEvent): readonly string[] {
|
|
|
258
268
|
}
|
|
259
269
|
}
|
|
260
270
|
|
|
271
|
+
/**
|
|
272
|
+
* The `run.jsonl` lines text leaves out: an agent's first session, which its opening and first turn
|
|
273
|
+
* already stand for, unless it is a fork's, made apart from any turn.
|
|
274
|
+
*/
|
|
275
|
+
function quietSessions(read: Read): Set<number> {
|
|
276
|
+
const forked = new Set<string>();
|
|
277
|
+
for (const { line } of read.log.lines) if (line.kind === "agent-forked") forked.add(line.agent);
|
|
278
|
+
const quiet = new Set<number>();
|
|
279
|
+
const seen = new Set<string>();
|
|
280
|
+
for (const { number, line } of read.log.named) {
|
|
281
|
+
if (!seen.has(line.agent) && !forked.has(line.agent)) quiet.add(number);
|
|
282
|
+
seen.add(line.agent);
|
|
283
|
+
}
|
|
284
|
+
return quiet;
|
|
285
|
+
}
|
|
286
|
+
|
|
261
287
|
function kept(read: Read, options: LogsOptions, json = false) {
|
|
262
288
|
const stages = operationPlaces(read.log.lines);
|
|
289
|
+
const quiet = json ? new Set<number>() : quietSessions(read);
|
|
263
290
|
const events = read.events.filter(
|
|
264
291
|
(event) =>
|
|
292
|
+
!(event.source === "run" && quiet.has(decodeHandleLine(event.handle))) &&
|
|
265
293
|
// A call that has ended shows on its end's line, with its input; `--json` keeps both.
|
|
266
294
|
!(
|
|
267
295
|
!json &&
|
|
268
|
-
event.source
|
|
296
|
+
event.source === "log" &&
|
|
269
297
|
event.line.kind === "tool-called" &&
|
|
270
298
|
stages.calls.get(event.line.call)?.ended
|
|
271
299
|
) &&
|
|
@@ -277,6 +305,11 @@ function kept(read: Read, options: LogsOptions, json = false) {
|
|
|
277
305
|
return { events, stages };
|
|
278
306
|
}
|
|
279
307
|
|
|
308
|
+
function decodeHandleLine(handle: string): number {
|
|
309
|
+
const decoded = decodeHandle(handle);
|
|
310
|
+
return decoded?.kind === "run" ? decoded.line : 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
280
313
|
function sameLine(a: ReadEvent, b: ReadEvent): boolean {
|
|
281
314
|
const first = decodeHandle(a.handle);
|
|
282
315
|
const second = decodeHandle(b.handle);
|
|
@@ -344,6 +377,7 @@ async function page(
|
|
|
344
377
|
}
|
|
345
378
|
}
|
|
346
379
|
const stage = currentStage(status, read.log.lines);
|
|
380
|
+
const screens = await keptScreens(status.run.dir, shown);
|
|
347
381
|
const filters = [
|
|
348
382
|
...(options.agent ? ["--agent", options.agent] : []),
|
|
349
383
|
...(options.stage ? ["--stage", options.stage] : []),
|
|
@@ -355,7 +389,7 @@ async function page(
|
|
|
355
389
|
if (json) {
|
|
356
390
|
const { started, ended } = span(status, clock.now());
|
|
357
391
|
const document: LogsJson = {
|
|
358
|
-
v:
|
|
392
|
+
v: LOGS_V,
|
|
359
393
|
run: {
|
|
360
394
|
ref: status.ref,
|
|
361
395
|
state: status.state,
|
|
@@ -364,7 +398,7 @@ async function page(
|
|
|
364
398
|
ambiguous: alsoRunning.length > 0,
|
|
365
399
|
...(status.state !== "running" && ended !== undefined ? { duration: ended - started } : {}),
|
|
366
400
|
},
|
|
367
|
-
events: shown.map((event) => publish(event, stages, PAGE_TEXT)),
|
|
401
|
+
events: shown.map((event) => publish(event, stages, screens, PAGE_TEXT)),
|
|
368
402
|
cursor: encodeCursor(cursor),
|
|
369
403
|
more: { ...(older ? { older: true } : {}), ...(newer ? { newer: true } : {}) },
|
|
370
404
|
...(next ? { next } : {}),
|
|
@@ -376,6 +410,11 @@ async function page(
|
|
|
376
410
|
const lines: string[] = [
|
|
377
411
|
header(status, stage, lastAt ? Date.parse(timeOf(lastAt.line)) : undefined),
|
|
378
412
|
];
|
|
413
|
+
if (read.log.unreadSessions !== undefined) {
|
|
414
|
+
lines.push(
|
|
415
|
+
`note: run.jsonl could not be read, so no agent's activity shows: ${said(read.log.unreadSessions)}`,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
379
418
|
if (alsoRunning.length > 0) {
|
|
380
419
|
const count = alsoRunning.length + (status.state === "running" ? 1 : 0);
|
|
381
420
|
lines.push(`note: ${count} runs running; also ${alsoRunning.join(", ")}`);
|
|
@@ -384,12 +423,12 @@ async function page(
|
|
|
384
423
|
const of = options.agent ? ` for ${options.agent}` : "";
|
|
385
424
|
// A cut read has more than any --limit reaches: paging from the start reads it all.
|
|
386
425
|
const more = read.cut
|
|
387
|
-
? `from the start: ${shellLine([...command(place, "awf", "logs", status.ref), ...filters, "--after", encodeCursor(
|
|
426
|
+
? `from the start: ${shellLine([...command(place, "awf", "logs", status.ref), ...filters, "--after", encodeCursor(START)])}`
|
|
388
427
|
: "raise --limit for more";
|
|
389
428
|
const total = read.cut ? `the last ${events.length} read` : `${events.length}`;
|
|
390
429
|
lines.push(`(showing ${shown.length} of ${total}${of}; ${more})`);
|
|
391
430
|
}
|
|
392
|
-
lines.push(...shown.map((event) => describeEvent(event, stages)));
|
|
431
|
+
lines.push(...shown.map((event) => describeEvent(event, stages, screens)));
|
|
393
432
|
lines.push(
|
|
394
433
|
trailer(
|
|
395
434
|
status,
|
|
@@ -418,7 +457,8 @@ async function follow(
|
|
|
418
457
|
const { events, stages } = kept(read, options);
|
|
419
458
|
// Without a cursor it starts from the last `--limit`, as a page would.
|
|
420
459
|
const shown = first && !reader.from ? events.slice(-options.limit) : events;
|
|
421
|
-
|
|
460
|
+
const screens = await keptScreens(reader.status.run.dir, shown);
|
|
461
|
+
for (const event of shown) out.stdout(describeEvent(event, stages, screens));
|
|
422
462
|
cursor = read.cursor;
|
|
423
463
|
// A read that stopped short goes on at once.
|
|
424
464
|
if (!read.more) {
|
|
@@ -445,34 +485,38 @@ async function oneEvent(
|
|
|
445
485
|
const handle = decodeHandle(options.handle!);
|
|
446
486
|
if (!handle) throw missing;
|
|
447
487
|
// Read from where the handle points, and only its file.
|
|
488
|
+
const none = Number.MAX_SAFE_INTEGER;
|
|
448
489
|
const read = await readRun(
|
|
449
490
|
status.run.dir,
|
|
450
491
|
status.run.record.cwd,
|
|
451
492
|
handle.kind === "event"
|
|
452
|
-
? { log: handle.line - 1, files: {} }
|
|
453
|
-
:
|
|
493
|
+
? { log: handle.line - 1, run: none, files: {} }
|
|
494
|
+
: handle.kind === "run"
|
|
495
|
+
? { log: none, run: handle.line - 1, files: {} }
|
|
496
|
+
: { log: none, run: none, files: { [handle.file]: handle.offset } },
|
|
454
497
|
{
|
|
455
498
|
ended: status.state !== "running",
|
|
456
499
|
full: options.full || json,
|
|
457
|
-
only: handle.kind === "
|
|
500
|
+
only: handle.kind === "activity" ? handle.file : "",
|
|
458
501
|
},
|
|
459
502
|
);
|
|
460
503
|
const stages = operationPlaces(read.log.lines);
|
|
461
504
|
const event = read.events.find((each) => each.handle === encodeHandle(handle));
|
|
462
505
|
if (!event) throw missing;
|
|
506
|
+
const screens = await keptScreens(status.run.dir, [event]);
|
|
463
507
|
if (json) {
|
|
464
508
|
const document: LogsJson = {
|
|
465
|
-
v:
|
|
509
|
+
v: LOGS_V,
|
|
466
510
|
run: { ref: status.ref, state: status.state, checked: status.checked },
|
|
467
|
-
events: [publish(event, stages)],
|
|
511
|
+
events: [publish(event, stages, screens)],
|
|
468
512
|
};
|
|
469
513
|
out.stdout(JSON.stringify(document, null, 2));
|
|
470
514
|
return 0;
|
|
471
515
|
}
|
|
472
|
-
const lines = [describeEvent(event, stages)];
|
|
516
|
+
const lines = [describeEvent(event, stages, screens)];
|
|
473
517
|
if (options.full) {
|
|
474
518
|
lines.unshift(
|
|
475
|
-
`(${
|
|
519
|
+
`(${{ activity: "native text", log: "the log's line", run: "run.jsonl's line" }[event.source]}; common credential shapes are masked)`,
|
|
476
520
|
);
|
|
477
521
|
const text =
|
|
478
522
|
event.source === "activity" ? event.text : JSON.stringify(masked(event.line), null, 2);
|
|
@@ -507,12 +551,17 @@ function trailer(
|
|
|
507
551
|
return `run interrupted${go ? ` · continue: ${go}` : ""}`;
|
|
508
552
|
}
|
|
509
553
|
const { ended, started } = span(status, Date.now());
|
|
510
|
-
const took = ended === undefined ? "" : ` in ${
|
|
554
|
+
const took = ended === undefined ? "" : ` in ${duration(ended - started)}`;
|
|
511
555
|
return `run ended: ${status.state}${took} · result: ${shellLine(command(place, "awf", "runs", status.ref))}`;
|
|
512
556
|
}
|
|
513
557
|
|
|
514
558
|
/** An event as `--json` publishes it: the line's own fields, or the activity, with its keys. */
|
|
515
|
-
function publish(
|
|
559
|
+
function publish(
|
|
560
|
+
event: ReadEvent,
|
|
561
|
+
stages: Places,
|
|
562
|
+
screens: Screens,
|
|
563
|
+
limit = Number.POSITIVE_INFINITY,
|
|
564
|
+
): LogEventJson {
|
|
516
565
|
if (event.source === "activity") {
|
|
517
566
|
const stage = event.operation ? stages.stage.get(event.operation) : undefined;
|
|
518
567
|
return {
|
|
@@ -526,24 +575,60 @@ function publish(event: ReadEvent, stages: Places, limit = Number.POSITIVE_INFIN
|
|
|
526
575
|
...(event.text === undefined ? {} : { text: masked(event.text, limit) as string }),
|
|
527
576
|
};
|
|
528
577
|
}
|
|
578
|
+
if (event.source === "run") {
|
|
579
|
+
return { handle: event.handle, ...(masked(event.line, limit) as typeof event.line) };
|
|
580
|
+
}
|
|
529
581
|
const { version: _version, ...fields } = event.line;
|
|
530
582
|
return {
|
|
531
583
|
handle: event.handle,
|
|
532
584
|
at: event.at,
|
|
533
585
|
...(masked(fields, limit) as typeof fields),
|
|
534
586
|
...("operationId" in fields ? { operation: fields.operationId } : {}),
|
|
587
|
+
...screenOf(event, screens),
|
|
535
588
|
};
|
|
536
589
|
}
|
|
537
590
|
|
|
538
|
-
/**
|
|
539
|
-
|
|
591
|
+
/** The screens kept for the operations of the events shown, by operation id. */
|
|
592
|
+
type Screens = ReadonlyMap<string, string>;
|
|
593
|
+
|
|
594
|
+
/** A turn or compaction that ended without an answer, and the file its screen would be kept in. */
|
|
595
|
+
function screenOf(event: ReadEvent, screens: Screens): { screen?: string } {
|
|
596
|
+
if (event.source !== "log") return {};
|
|
597
|
+
const { line } = event;
|
|
598
|
+
if (line.kind !== "turn" && line.kind !== "compact") return {};
|
|
599
|
+
const screen = screens.get(line.operationId);
|
|
600
|
+
return screen ? { screen } : {};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function keptScreens(runDir: string, events: readonly ReadEvent[]): Promise<Screens> {
|
|
604
|
+
const screens = new Map<string, string>();
|
|
605
|
+
await Promise.all(
|
|
606
|
+
events.map(async (event) => {
|
|
607
|
+
if (event.source !== "log") return;
|
|
608
|
+
const { line } = event;
|
|
609
|
+
if (line.kind !== "turn" && line.kind !== "compact") return;
|
|
610
|
+
if (line.outcome === "answered" || line.outcome === "cancelled") return;
|
|
611
|
+
const file = screenFile(runDir, line.operationId);
|
|
612
|
+
if (await exists(file)) screens.set(line.operationId, file);
|
|
613
|
+
}),
|
|
614
|
+
);
|
|
615
|
+
return screens;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** `@e216 09:27:02 fix fixer turn ended answered 1m 48s`. */
|
|
619
|
+
function describeEvent(event: ReadEvent, stages: Places, screens: Screens): string {
|
|
540
620
|
const agent = agentOf(event, stages);
|
|
541
621
|
const where = [stageOf(event, stages), agent && said(agent, 60)].filter(Boolean).join(" ");
|
|
542
622
|
const what =
|
|
543
623
|
event.source === "activity"
|
|
544
624
|
? describeActivity(event.activity)
|
|
545
|
-
:
|
|
546
|
-
|
|
625
|
+
: event.source === "run"
|
|
626
|
+
? `session ${event.line.harness} ${event.line.session}`
|
|
627
|
+
: describeLine(event.line, stages);
|
|
628
|
+
const { screen } = screenOf(event, screens);
|
|
629
|
+
return `@${event.handle} ${clockTime(event.at)} ${where ? `${where} ` : ""}${what}${
|
|
630
|
+
screen ? ` screen: ${screen}` : ""
|
|
631
|
+
}`;
|
|
547
632
|
}
|
|
548
633
|
|
|
549
634
|
function describeLine(line: RunLogLine, places: Places): string {
|
|
@@ -565,12 +650,12 @@ function describeLine(line: RunLogLine, places: Places): string {
|
|
|
565
650
|
case "compact": {
|
|
566
651
|
const took =
|
|
567
652
|
line.deliveredAt && line.settledAt
|
|
568
|
-
? ` ${
|
|
653
|
+
? ` ${duration(Date.parse(line.settledAt) - Date.parse(line.deliveredAt))}`
|
|
569
654
|
: "";
|
|
570
655
|
return `${operation(line.kind)} ended ${line.outcome}${took}`;
|
|
571
656
|
}
|
|
572
|
-
case "
|
|
573
|
-
return `
|
|
657
|
+
case "sandbox-opened":
|
|
658
|
+
return `sandbox ${said(line.key, 60)} opened ${line.provider}`;
|
|
574
659
|
case "agent-opened":
|
|
575
660
|
return `opened ${line.harness}${line.caller ? " (the calling session)" : ""}`;
|
|
576
661
|
case "agent-stopped":
|
|
@@ -587,9 +672,7 @@ function describeLine(line: RunLogLine, places: Places): string {
|
|
|
587
672
|
return `decision ${line.decision} ${line.outcome}${line.error ? ` ${said(line.error)}` : ""}`;
|
|
588
673
|
case "route-opened":
|
|
589
674
|
return `route ${routeEnds(line)} opened "${said(line.purpose)}" ${line.maxMessages} messages${
|
|
590
|
-
line.timeoutMs === undefined
|
|
591
|
-
? ""
|
|
592
|
-
: `, a question closes after ${formatDuration(line.timeoutMs)}`
|
|
675
|
+
line.timeoutMs === undefined ? "" : `, a question closes after ${duration(line.timeoutMs)}`
|
|
593
676
|
}`;
|
|
594
677
|
case "route-closed":
|
|
595
678
|
return `route ${routeEnds(line)} closed ${line.reason}`;
|
|
@@ -638,9 +721,7 @@ function describeLine(line: RunLogLine, places: Places): string {
|
|
|
638
721
|
line.tool,
|
|
639
722
|
places.calls.get(line.call)?.input,
|
|
640
723
|
// Most calls take under a second; a duration says something only past one.
|
|
641
|
-
line.durationMs < 1000
|
|
642
|
-
? line.outcome
|
|
643
|
-
: `${line.outcome} ${formatDuration(line.durationMs)}`,
|
|
724
|
+
line.durationMs < 1000 ? line.outcome : `${line.outcome} ${duration(line.durationMs)}`,
|
|
644
725
|
answered,
|
|
645
726
|
);
|
|
646
727
|
}
|
package/src/message-operation.ts
CHANGED
|
@@ -31,12 +31,14 @@ export async function superviseMessageOperation(options: {
|
|
|
31
31
|
event?(event: LivenessEvent): void;
|
|
32
32
|
phase?(phase: OperationPhase, reason?: string, until?: number): void;
|
|
33
33
|
delivered?(at: number): void;
|
|
34
|
+
/** Keeps what its turn shows when it ends without an answer, before the turn is released. */
|
|
35
|
+
keepScreen?(turn: HarnessTurn): Promise<void>;
|
|
34
36
|
policy?: LivenessPolicy;
|
|
35
37
|
now?: () => number;
|
|
36
38
|
}): Promise<SupervisedOutcome> {
|
|
37
39
|
const now = options.now ?? Date.now;
|
|
38
40
|
const policy = options.policy ?? DEFAULT_LIVENESS_POLICY;
|
|
39
|
-
const
|
|
41
|
+
const reportedCosts: number[] = [];
|
|
40
42
|
let sequence = 0;
|
|
41
43
|
let deliveredAt: number | undefined;
|
|
42
44
|
let stop: OperationStop | undefined;
|
|
@@ -63,7 +65,7 @@ export async function superviseMessageOperation(options: {
|
|
|
63
65
|
kind,
|
|
64
66
|
reason,
|
|
65
67
|
cleanupUnresolved: false,
|
|
66
|
-
|
|
68
|
+
reportedCosts,
|
|
67
69
|
...(deliveredAt === undefined ? {} : { deliveredAt }),
|
|
68
70
|
settledAt:
|
|
69
71
|
kind === "timed-out"
|
|
@@ -139,7 +141,7 @@ export async function superviseMessageOperation(options: {
|
|
|
139
141
|
void turn.settled.then(
|
|
140
142
|
(value) => {
|
|
141
143
|
outcome = value;
|
|
142
|
-
|
|
144
|
+
reportedCosts.push(...value.reportedCostsUsd);
|
|
143
145
|
changed();
|
|
144
146
|
},
|
|
145
147
|
(error: unknown) => {
|
|
@@ -251,6 +253,7 @@ export async function superviseMessageOperation(options: {
|
|
|
251
253
|
emit("releasing");
|
|
252
254
|
if (turn) {
|
|
253
255
|
const answered = result.kind === "answered";
|
|
256
|
+
if (!answered && result.kind !== "cancelled") await options.keepScreen?.(turn);
|
|
254
257
|
const until = bound(answered);
|
|
255
258
|
const release = await Promise.race([
|
|
256
259
|
turn.release(stop?.reason ?? result.reason, until, { answered }).catch(() => undefined),
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
import type { ParticipantCode, PeerView, ReplyCode, SendCode } from "@agentswf/contract/wire";
|
|
10
10
|
import type { AgentKey, JsonObject } from "@agentswf/contract/workflow";
|
|
11
11
|
import type { AuthoredMessage } from "@agentswf/harness/adapter";
|
|
12
|
-
import {
|
|
12
|
+
import { duration } from "../accounting/format";
|
|
13
13
|
import { MESSAGE_LIMITS } from "./limits";
|
|
14
14
|
import {
|
|
15
15
|
directions,
|
|
@@ -53,6 +53,8 @@ export interface MessagingHost {
|
|
|
53
53
|
changed(key: AgentKey): void;
|
|
54
54
|
schedule(at: number, action: () => void): () => void;
|
|
55
55
|
now(): number;
|
|
56
|
+
/** Question `n`'s id as `awf inbox` prints it, the attempt before it: what a refusal names. */
|
|
57
|
+
question(n: number): string;
|
|
56
58
|
/** Written to the run's event log; `message-sent` is awaited before a send is acknowledged. */
|
|
57
59
|
record(event: MessagingEventBody): Promise<void>;
|
|
58
60
|
}
|
|
@@ -117,7 +119,7 @@ export type HolderRequest = {
|
|
|
117
119
|
/** A holder's result: a reply's `message` is the question's number, a send's the new message's. */
|
|
118
120
|
export type HolderResult =
|
|
119
121
|
| { code: "delivered"; message: number }
|
|
120
|
-
| { code:
|
|
122
|
+
| { code: ParticipantCode; error: string; choices?: string[] };
|
|
121
123
|
|
|
122
124
|
/** A question open to an outside participant, and when it closes unanswered if it has a limit. */
|
|
123
125
|
export type OpenQuestion = {
|
|
@@ -444,7 +446,7 @@ export class Messaging {
|
|
|
444
446
|
return holderRefusal(
|
|
445
447
|
"ambiguous",
|
|
446
448
|
`${agent} has ${open.length} questions open to ${as.key}; name one with --to.`,
|
|
447
|
-
open.map((each) =>
|
|
449
|
+
open.map((each) => this.#host.question(each.request.n)),
|
|
448
450
|
);
|
|
449
451
|
owed = open[0];
|
|
450
452
|
if (!owed) return holderRefusal("expired", `${agent} has no question open to ${as.key}.`);
|
|
@@ -459,7 +461,7 @@ export class Messaging {
|
|
|
459
461
|
if (!owed)
|
|
460
462
|
return holderRefusal(
|
|
461
463
|
"expired",
|
|
462
|
-
`${request.to} is not a question open from ${agent}${as.key === undefined ? "" : ` to ${as.key}`}.`,
|
|
464
|
+
`${this.#host.question(request.to)} is not a question open from ${agent}${as.key === undefined ? "" : ` to ${as.key}`}.`,
|
|
463
465
|
);
|
|
464
466
|
}
|
|
465
467
|
const route = owed.request.route;
|
|
@@ -1085,7 +1087,7 @@ export class Messaging {
|
|
|
1085
1087
|
obligation,
|
|
1086
1088
|
"timed-out",
|
|
1087
1089
|
obligation.closes?.by === "route"
|
|
1088
|
-
? `the question closed unanswered after ${
|
|
1090
|
+
? `the question closed unanswered after ${duration(obligation.closes.ms)}`
|
|
1089
1091
|
: `your --timeout passed at ${clock(closesAt, this.#host.now())}`,
|
|
1090
1092
|
);
|
|
1091
1093
|
}
|
|
@@ -1681,11 +1683,7 @@ function refuse<C extends SendCode | ReplyCode>(code: C, error: string) {
|
|
|
1681
1683
|
return { kind: "refused" as const, code, error };
|
|
1682
1684
|
}
|
|
1683
1685
|
|
|
1684
|
-
function holderRefusal(
|
|
1685
|
-
code: Exclude<ParticipantCode, "not-found">,
|
|
1686
|
-
error: string,
|
|
1687
|
-
choices?: string[],
|
|
1688
|
-
): HolderResult {
|
|
1686
|
+
function holderRefusal(code: ParticipantCode, error: string, choices?: string[]): HolderResult {
|
|
1689
1687
|
return { code, error, ...(choices ? { choices } : {}) };
|
|
1690
1688
|
}
|
|
1691
1689
|
|
package/src/messaging/text.ts
CHANGED
|
@@ -31,7 +31,7 @@ export type Delivered =
|
|
|
31
31
|
}
|
|
32
32
|
| { kind: "no-reply"; from: AgentKey; reason: string };
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
function quoted(purpose: string): string {
|
|
35
35
|
return `“${purpose}”`;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -53,7 +53,7 @@ export function clock(at: number, now: number): string {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/** At least eight hex characters, as the design asks; a fresh one per delivery. */
|
|
56
|
-
|
|
56
|
+
function nonce(): string {
|
|
57
57
|
return randomBytes(4).toString("hex");
|
|
58
58
|
}
|
|
59
59
|
|