@agentswf/engine 0.1.0-beta.4 → 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 +35 -22
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +5 -7
- package/src/attempt-output.ts +15 -32
- package/src/attempt-view.ts +34 -33
- 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 +41 -11
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +41 -33
- package/src/deadlines.ts +40 -7
- package/src/decisions/directory.ts +51 -40
- 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 +43 -27
- package/src/emit-command.ts +88 -58
- package/src/emit.ts +254 -56
- 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 +18 -6
- package/src/help/inbox.txt +6 -4
- package/src/help/logs.txt +9 -6
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +30 -11
- 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 +7 -15
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +56 -133
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +40 -57
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.test.ts.template +3 -3
- package/src/init-template/hello/workflow.ts.template +5 -4
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +15 -12
- package/src/logs-command.ts +128 -47
- package/src/message-operation.ts +6 -3
- package/src/messaging/limits.ts +4 -4
- package/src/messaging/messaging.ts +31 -28
- package/src/messaging/routes.ts +1 -1
- package/src/messaging/text.ts +9 -7
- 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 +224 -94
- package/src/operator-runtime.ts +29 -7
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +105 -59
- package/src/progress-view.ts +10 -10
- 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 +45 -28
- package/src/run-commands.ts +43 -115
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/{stage-plan.ts → run-policy/stage-plan.ts} +46 -42
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +205 -43
- 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 +18 -12
- package/src/run-view.ts +81 -17
- package/src/runs-command.ts +83 -63
- package/src/runs.ts +310 -212
- 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 +28 -29
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +119 -131
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +36 -14
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +73 -26
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +25 -16
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +54 -4
- package/src/workflow-runner.ts +805 -580
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +3 -3
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +121 -53
- package/src/workflow-testing/index.ts +33 -842
- package/src/workflow-testing/run-test.ts +1128 -0
- package/src/workflow-testing/script.ts +74 -45
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -990
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- package/src/workflow-testing/workflow-testing.typecheck.ts +0 -64
package/src/init-command.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { existsSync, lstatSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
3
|
import manifest from "../package.json" with { type: "json" };
|
|
4
|
+
import { type Out, shellWord } from "./command-line";
|
|
4
5
|
import { messageOf } from "./errors";
|
|
5
6
|
import help from "./help/init.txt" with { type: "text" };
|
|
6
|
-
import
|
|
7
|
+
import { NO_JSON, printRefusal } from "./refusals";
|
|
7
8
|
|
|
8
9
|
export const initUsage = help.trimEnd();
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
type InitCommand = { dir: string; install: boolean };
|
|
11
12
|
|
|
12
13
|
/** `awf init`: a folder set up for writing workflows. */
|
|
13
14
|
export async function initCommand(
|
|
@@ -15,17 +16,13 @@ export async function initCommand(
|
|
|
15
16
|
cwd: string,
|
|
16
17
|
io: Out & { install?: (dir: string) => Promise<number> },
|
|
17
18
|
): Promise<number> {
|
|
18
|
-
let command: InitCommand
|
|
19
|
+
let command: InitCommand;
|
|
19
20
|
try {
|
|
20
21
|
command = parseInitCommand(argv, cwd);
|
|
21
22
|
} catch (error) {
|
|
22
|
-
io
|
|
23
|
+
printRefusal(io, NO_JSON, error, "usage", initUsage);
|
|
23
24
|
return 2;
|
|
24
25
|
}
|
|
25
|
-
if (command === "help") {
|
|
26
|
-
io.stdout(initUsage);
|
|
27
|
-
return 0;
|
|
28
|
-
}
|
|
29
26
|
try {
|
|
30
27
|
return await runInit(command, cwd, io, io.install);
|
|
31
28
|
} catch (error) {
|
|
@@ -34,11 +31,10 @@ export async function initCommand(
|
|
|
34
31
|
}
|
|
35
32
|
}
|
|
36
33
|
|
|
37
|
-
export function parseInitCommand(argv: readonly string[], cwd: string): InitCommand
|
|
34
|
+
export function parseInitCommand(argv: readonly string[], cwd: string): InitCommand {
|
|
38
35
|
let dir: string | undefined;
|
|
39
36
|
let install = true;
|
|
40
37
|
for (const option of argv) {
|
|
41
|
-
if (option === "-h" || option === "--help") return "help";
|
|
42
38
|
if (option === "--no-install") install = false;
|
|
43
39
|
else if (option.startsWith("-")) throw new Error(`unknown option: ${option}`);
|
|
44
40
|
else if (dir !== undefined) throw new Error(`one directory only, not ${dir} and ${option}`);
|
|
@@ -86,7 +82,7 @@ function targetOf(dir: string, path: string): Target {
|
|
|
86
82
|
* Writes what `awf init` writes into `dir`, and says what it did to each file. Everything that
|
|
87
83
|
* would stop it, such as a package.json that isn't a JSON object, is found before it writes.
|
|
88
84
|
*/
|
|
89
|
-
|
|
85
|
+
function initFolder(dir: string): Change[] {
|
|
90
86
|
for (const parent of new Set(COPIED.map(dirname))) {
|
|
91
87
|
const stat = lstatSync(join(dir, parent), { throwIfNoEntry: false });
|
|
92
88
|
if (stat !== undefined && !stat.isDirectory()) {
|
|
@@ -213,13 +209,8 @@ function indentOf(text: string): string | number {
|
|
|
213
209
|
return /^\{\s*\r?\n([ \t]+)"/.exec(text)?.[1] ?? 2;
|
|
214
210
|
}
|
|
215
211
|
|
|
216
|
-
/** As a shell reads it: bare when that's safe, otherwise single-quoted. */
|
|
217
|
-
function shellWord(word: string): string {
|
|
218
|
-
return /^[\w./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
212
|
/** `awf init`: writes the folder, installs unless told not to, and says what to run next. */
|
|
222
|
-
|
|
213
|
+
async function runInit(
|
|
223
214
|
command: InitCommand,
|
|
224
215
|
cwd: string,
|
|
225
216
|
output: { stdout(text: string): void; stderr(text: string): void },
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
|
-
import { answer,
|
|
2
|
+
import { answer, noAnswer, testWorkflow } from "agentswf/testing";
|
|
3
3
|
import hello, { ANSWER } from "./workflow";
|
|
4
4
|
|
|
5
5
|
test("returns the agent's answer", async () => {
|
|
6
|
-
const run = await testWorkflow(hello,
|
|
6
|
+
const run = await testWorkflow(hello, {
|
|
7
7
|
agents: { hello: answer(ANSWER, { answer: 391 }) },
|
|
8
8
|
});
|
|
9
9
|
expect(run.value).toEqual({ answer: 391 });
|
|
@@ -11,7 +11,7 @@ test("returns the agent's answer", async () => {
|
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
test("an agent that never answers is nudged once, then the result says why", async () => {
|
|
14
|
-
const run = await testWorkflow(hello,
|
|
14
|
+
const run = await testWorkflow(hello, { agents: { hello: noAnswer.silent() } });
|
|
15
15
|
expect(run.value).toHaveProperty("unanswered");
|
|
16
16
|
expect(run.turnsOf("hello").map((turn) => turn.nudge)).toEqual([false, true]);
|
|
17
17
|
});
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
+
import { typed } from "agentswf/typebox";
|
|
1
2
|
import { defineWorkflow } from "agentswf/workflow";
|
|
2
3
|
import Type from "typebox";
|
|
3
4
|
|
|
4
|
-
export const ANSWER = Type.Object({ answer: Type.Integer() }, { additionalProperties: false });
|
|
5
|
+
export const ANSWER = typed(Type.Object({ answer: Type.Integer() }, { additionalProperties: false }));
|
|
5
6
|
|
|
6
7
|
export default defineWorkflow({
|
|
7
8
|
meta: { name: "hello", description: "Ask one agent a question with a known answer." },
|
|
8
9
|
async run(wf) {
|
|
9
10
|
// A headless agent needs neither Herdr nor a sandbox. Swap in any harness and model your
|
|
10
|
-
// account has: "claude" in
|
|
11
|
+
// account has: "claude" in its TUI, the default placement, runs on a subscription.
|
|
11
12
|
const agent = await wf.agents.open({
|
|
12
13
|
key: "hello",
|
|
13
14
|
runtime: { harness: "codex", model: "gpt-6-luna", placement: "headless" },
|
|
14
15
|
});
|
|
15
|
-
const { outcome } = await agent.run
|
|
16
|
+
const { outcome } = await agent.run({
|
|
16
17
|
prompt: "What is 17 multiplied by 23? Work it out yourself; do not run any code.",
|
|
17
18
|
schema: ANSWER,
|
|
18
|
-
|
|
19
|
+
timeout: "3m",
|
|
19
20
|
});
|
|
20
21
|
return outcome.answered ? outcome.value : { unanswered: outcome.reason };
|
|
21
22
|
},
|
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
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import type
|
|
2
|
+
import { type AgentOpenSpec, positiveDurationMilliseconds } from "@agentswf/contract/workflow";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/** `keepAlive` as the engine keeps it: its window in milliseconds. */
|
|
5
|
+
export type KeepAlive = {
|
|
6
|
+
windowMs?: number;
|
|
7
|
+
onTimeout?: NonNullable<AgentOpenSpec["keepAlive"]>["onTimeout"];
|
|
8
|
+
};
|
|
5
9
|
|
|
6
10
|
/** Claude's prompt cache lives an hour for the main conversation on a subscription. */
|
|
7
11
|
export const DEFAULT_WINDOW_MS = 60 * 60_000;
|
|
@@ -20,21 +24,20 @@ export function asTimeoutHandler<T>(fn: () => T): T {
|
|
|
20
24
|
return inHandler.run(true, fn);
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
/** `keepAlive` as an open gave it, checked:
|
|
27
|
+
/** `keepAlive` as an open gave it, checked: a plain caller can get wrong what a typed one can't. */
|
|
24
28
|
export function checkedKeepAlive(key: string, given: unknown): KeepAlive {
|
|
25
29
|
if (typeof given !== "object" || given === null)
|
|
26
|
-
throw new Error(`agent ${key}: keepAlive is {
|
|
27
|
-
const {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
throw new Error(`agent ${key}: keepAlive's timeoutMs is a positive number of milliseconds`);
|
|
30
|
+
throw new Error(`agent ${key}: keepAlive is { window?, onTimeout? }`);
|
|
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`);
|
|
33
36
|
if (onTimeout !== undefined && typeof onTimeout !== "function")
|
|
34
37
|
throw new Error(`agent ${key}: keepAlive's onTimeout is a function`);
|
|
35
38
|
// A copy: a later change to the caller's object would skip this check.
|
|
36
39
|
return {
|
|
37
|
-
...(
|
|
40
|
+
...(windowMs === undefined ? {} : { windowMs }),
|
|
38
41
|
...(onTimeout === undefined ? {} : { onTimeout: onTimeout as KeepAlive["onTimeout"] }),
|
|
39
|
-
}
|
|
42
|
+
};
|
|
40
43
|
}
|
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 { duration } from "./
|
|
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;
|
|
@@ -88,7 +86,7 @@ export async function logsCommand(
|
|
|
88
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,18 +106,25 @@ 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
|
|
|
@@ -147,11 +153,7 @@ function parseLogs(rest: readonly string[]): LogsOptions {
|
|
|
147
153
|
throw new Error("--limit needs a whole number");
|
|
148
154
|
options.limit = limit;
|
|
149
155
|
} else if (arg === "--wait") {
|
|
150
|
-
|
|
151
|
-
if (!Number.isFinite(seconds) || seconds < 0 || seconds > MAX_WAIT_SECONDS) {
|
|
152
|
-
throw new Error(`--wait needs seconds from 0 to ${MAX_WAIT_SECONDS}`);
|
|
153
|
-
}
|
|
154
|
-
options.waitSeconds = seconds;
|
|
156
|
+
options.waitSeconds = parseWait(value(), MAX_WAIT_SECONDS);
|
|
155
157
|
waitGiven = true;
|
|
156
158
|
} else if (arg === "--follow") options.follow = true;
|
|
157
159
|
else if (arg === "--full") options.full = true;
|
|
@@ -228,8 +230,9 @@ function operationPlaces(lines: readonly { line: RunLogLine }[]): Places {
|
|
|
228
230
|
|
|
229
231
|
function agentOf(event: ReadEvent, places: Places): string | undefined {
|
|
230
232
|
if (event.source === "activity") return event.agent;
|
|
233
|
+
if (event.source === "run") return event.line.agent;
|
|
231
234
|
if ("agent" in event.line && event.line.agent) return event.line.agent;
|
|
232
|
-
// A
|
|
235
|
+
// A line that names only its operation, such as a message's delivery, is its operation's agent's.
|
|
233
236
|
return "operationId" in event.line && event.line.operationId
|
|
234
237
|
? places.agent.get(event.line.operationId)
|
|
235
238
|
: undefined;
|
|
@@ -239,12 +242,15 @@ function stageOf(event: ReadEvent, places: Places): string | undefined {
|
|
|
239
242
|
if (event.source === "activity") {
|
|
240
243
|
return event.operation ? places.stage.get(event.operation) : undefined;
|
|
241
244
|
}
|
|
245
|
+
if (event.source === "run") {
|
|
246
|
+
return event.line.operation ? places.stage.get(event.line.operation) : undefined;
|
|
247
|
+
}
|
|
242
248
|
return "stage" in event.line ? event.line.stage : undefined;
|
|
243
249
|
}
|
|
244
250
|
|
|
245
251
|
/** The agents a messaging line is between, so `--agent` follows it from either side. */
|
|
246
252
|
function sidesOf(event: ReadEvent): readonly string[] {
|
|
247
|
-
if (event.source
|
|
253
|
+
if (event.source !== "log") return [];
|
|
248
254
|
const { line } = event;
|
|
249
255
|
switch (line.kind) {
|
|
250
256
|
case "route-opened":
|
|
@@ -262,14 +268,32 @@ function sidesOf(event: ReadEvent): readonly string[] {
|
|
|
262
268
|
}
|
|
263
269
|
}
|
|
264
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
|
+
|
|
265
287
|
function kept(read: Read, options: LogsOptions, json = false) {
|
|
266
288
|
const stages = operationPlaces(read.log.lines);
|
|
289
|
+
const quiet = json ? new Set<number>() : quietSessions(read);
|
|
267
290
|
const events = read.events.filter(
|
|
268
291
|
(event) =>
|
|
292
|
+
!(event.source === "run" && quiet.has(decodeHandleLine(event.handle))) &&
|
|
269
293
|
// A call that has ended shows on its end's line, with its input; `--json` keeps both.
|
|
270
294
|
!(
|
|
271
295
|
!json &&
|
|
272
|
-
event.source
|
|
296
|
+
event.source === "log" &&
|
|
273
297
|
event.line.kind === "tool-called" &&
|
|
274
298
|
stages.calls.get(event.line.call)?.ended
|
|
275
299
|
) &&
|
|
@@ -281,6 +305,11 @@ function kept(read: Read, options: LogsOptions, json = false) {
|
|
|
281
305
|
return { events, stages };
|
|
282
306
|
}
|
|
283
307
|
|
|
308
|
+
function decodeHandleLine(handle: string): number {
|
|
309
|
+
const decoded = decodeHandle(handle);
|
|
310
|
+
return decoded?.kind === "run" ? decoded.line : 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
284
313
|
function sameLine(a: ReadEvent, b: ReadEvent): boolean {
|
|
285
314
|
const first = decodeHandle(a.handle);
|
|
286
315
|
const second = decodeHandle(b.handle);
|
|
@@ -348,6 +377,7 @@ async function page(
|
|
|
348
377
|
}
|
|
349
378
|
}
|
|
350
379
|
const stage = currentStage(status, read.log.lines);
|
|
380
|
+
const screens = await keptScreens(status.run.dir, shown);
|
|
351
381
|
const filters = [
|
|
352
382
|
...(options.agent ? ["--agent", options.agent] : []),
|
|
353
383
|
...(options.stage ? ["--stage", options.stage] : []),
|
|
@@ -359,7 +389,7 @@ async function page(
|
|
|
359
389
|
if (json) {
|
|
360
390
|
const { started, ended } = span(status, clock.now());
|
|
361
391
|
const document: LogsJson = {
|
|
362
|
-
v:
|
|
392
|
+
v: LOGS_V,
|
|
363
393
|
run: {
|
|
364
394
|
ref: status.ref,
|
|
365
395
|
state: status.state,
|
|
@@ -368,7 +398,7 @@ async function page(
|
|
|
368
398
|
ambiguous: alsoRunning.length > 0,
|
|
369
399
|
...(status.state !== "running" && ended !== undefined ? { duration: ended - started } : {}),
|
|
370
400
|
},
|
|
371
|
-
events: shown.map((event) => publish(event, stages, PAGE_TEXT)),
|
|
401
|
+
events: shown.map((event) => publish(event, stages, screens, PAGE_TEXT)),
|
|
372
402
|
cursor: encodeCursor(cursor),
|
|
373
403
|
more: { ...(older ? { older: true } : {}), ...(newer ? { newer: true } : {}) },
|
|
374
404
|
...(next ? { next } : {}),
|
|
@@ -380,6 +410,11 @@ async function page(
|
|
|
380
410
|
const lines: string[] = [
|
|
381
411
|
header(status, stage, lastAt ? Date.parse(timeOf(lastAt.line)) : undefined),
|
|
382
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
|
+
}
|
|
383
418
|
if (alsoRunning.length > 0) {
|
|
384
419
|
const count = alsoRunning.length + (status.state === "running" ? 1 : 0);
|
|
385
420
|
lines.push(`note: ${count} runs running; also ${alsoRunning.join(", ")}`);
|
|
@@ -388,12 +423,12 @@ async function page(
|
|
|
388
423
|
const of = options.agent ? ` for ${options.agent}` : "";
|
|
389
424
|
// A cut read has more than any --limit reaches: paging from the start reads it all.
|
|
390
425
|
const more = read.cut
|
|
391
|
-
? `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)])}`
|
|
392
427
|
: "raise --limit for more";
|
|
393
428
|
const total = read.cut ? `the last ${events.length} read` : `${events.length}`;
|
|
394
429
|
lines.push(`(showing ${shown.length} of ${total}${of}; ${more})`);
|
|
395
430
|
}
|
|
396
|
-
lines.push(...shown.map((event) => describeEvent(event, stages)));
|
|
431
|
+
lines.push(...shown.map((event) => describeEvent(event, stages, screens)));
|
|
397
432
|
lines.push(
|
|
398
433
|
trailer(
|
|
399
434
|
status,
|
|
@@ -422,7 +457,8 @@ async function follow(
|
|
|
422
457
|
const { events, stages } = kept(read, options);
|
|
423
458
|
// Without a cursor it starts from the last `--limit`, as a page would.
|
|
424
459
|
const shown = first && !reader.from ? events.slice(-options.limit) : events;
|
|
425
|
-
|
|
460
|
+
const screens = await keptScreens(reader.status.run.dir, shown);
|
|
461
|
+
for (const event of shown) out.stdout(describeEvent(event, stages, screens));
|
|
426
462
|
cursor = read.cursor;
|
|
427
463
|
// A read that stopped short goes on at once.
|
|
428
464
|
if (!read.more) {
|
|
@@ -449,34 +485,38 @@ async function oneEvent(
|
|
|
449
485
|
const handle = decodeHandle(options.handle!);
|
|
450
486
|
if (!handle) throw missing;
|
|
451
487
|
// Read from where the handle points, and only its file.
|
|
488
|
+
const none = Number.MAX_SAFE_INTEGER;
|
|
452
489
|
const read = await readRun(
|
|
453
490
|
status.run.dir,
|
|
454
491
|
status.run.record.cwd,
|
|
455
492
|
handle.kind === "event"
|
|
456
|
-
? { log: handle.line - 1, files: {} }
|
|
457
|
-
:
|
|
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 } },
|
|
458
497
|
{
|
|
459
498
|
ended: status.state !== "running",
|
|
460
499
|
full: options.full || json,
|
|
461
|
-
only: handle.kind === "
|
|
500
|
+
only: handle.kind === "activity" ? handle.file : "",
|
|
462
501
|
},
|
|
463
502
|
);
|
|
464
503
|
const stages = operationPlaces(read.log.lines);
|
|
465
504
|
const event = read.events.find((each) => each.handle === encodeHandle(handle));
|
|
466
505
|
if (!event) throw missing;
|
|
506
|
+
const screens = await keptScreens(status.run.dir, [event]);
|
|
467
507
|
if (json) {
|
|
468
508
|
const document: LogsJson = {
|
|
469
|
-
v:
|
|
509
|
+
v: LOGS_V,
|
|
470
510
|
run: { ref: status.ref, state: status.state, checked: status.checked },
|
|
471
|
-
events: [publish(event, stages)],
|
|
511
|
+
events: [publish(event, stages, screens)],
|
|
472
512
|
};
|
|
473
513
|
out.stdout(JSON.stringify(document, null, 2));
|
|
474
514
|
return 0;
|
|
475
515
|
}
|
|
476
|
-
const lines = [describeEvent(event, stages)];
|
|
516
|
+
const lines = [describeEvent(event, stages, screens)];
|
|
477
517
|
if (options.full) {
|
|
478
518
|
lines.unshift(
|
|
479
|
-
`(${
|
|
519
|
+
`(${{ activity: "native text", log: "the log's line", run: "run.jsonl's line" }[event.source]}; common credential shapes are masked)`,
|
|
480
520
|
);
|
|
481
521
|
const text =
|
|
482
522
|
event.source === "activity" ? event.text : JSON.stringify(masked(event.line), null, 2);
|
|
@@ -516,7 +556,12 @@ function trailer(
|
|
|
516
556
|
}
|
|
517
557
|
|
|
518
558
|
/** An event as `--json` publishes it: the line's own fields, or the activity, with its keys. */
|
|
519
|
-
function publish(
|
|
559
|
+
function publish(
|
|
560
|
+
event: ReadEvent,
|
|
561
|
+
stages: Places,
|
|
562
|
+
screens: Screens,
|
|
563
|
+
limit = Number.POSITIVE_INFINITY,
|
|
564
|
+
): LogEventJson {
|
|
520
565
|
if (event.source === "activity") {
|
|
521
566
|
const stage = event.operation ? stages.stage.get(event.operation) : undefined;
|
|
522
567
|
return {
|
|
@@ -530,24 +575,60 @@ function publish(event: ReadEvent, stages: Places, limit = Number.POSITIVE_INFIN
|
|
|
530
575
|
...(event.text === undefined ? {} : { text: masked(event.text, limit) as string }),
|
|
531
576
|
};
|
|
532
577
|
}
|
|
578
|
+
if (event.source === "run") {
|
|
579
|
+
return { handle: event.handle, ...(masked(event.line, limit) as typeof event.line) };
|
|
580
|
+
}
|
|
533
581
|
const { version: _version, ...fields } = event.line;
|
|
534
582
|
return {
|
|
535
583
|
handle: event.handle,
|
|
536
584
|
at: event.at,
|
|
537
585
|
...(masked(fields, limit) as typeof fields),
|
|
538
586
|
...("operationId" in fields ? { operation: fields.operationId } : {}),
|
|
587
|
+
...screenOf(event, screens),
|
|
539
588
|
};
|
|
540
589
|
}
|
|
541
590
|
|
|
542
|
-
/**
|
|
543
|
-
|
|
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 {
|
|
544
620
|
const agent = agentOf(event, stages);
|
|
545
621
|
const where = [stageOf(event, stages), agent && said(agent, 60)].filter(Boolean).join(" ");
|
|
546
622
|
const what =
|
|
547
623
|
event.source === "activity"
|
|
548
624
|
? describeActivity(event.activity)
|
|
549
|
-
:
|
|
550
|
-
|
|
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
|
+
}`;
|
|
551
632
|
}
|
|
552
633
|
|
|
553
634
|
function describeLine(line: RunLogLine, places: Places): string {
|
|
@@ -573,8 +654,8 @@ function describeLine(line: RunLogLine, places: Places): string {
|
|
|
573
654
|
: "";
|
|
574
655
|
return `${operation(line.kind)} ended ${line.outcome}${took}`;
|
|
575
656
|
}
|
|
576
|
-
case "
|
|
577
|
-
return `
|
|
657
|
+
case "sandbox-opened":
|
|
658
|
+
return `sandbox ${said(line.key, 60)} opened ${line.provider}`;
|
|
578
659
|
case "agent-opened":
|
|
579
660
|
return `opened ${line.harness}${line.caller ? " (the calling session)" : ""}`;
|
|
580
661
|
case "agent-stopped":
|
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),
|
package/src/messaging/limits.ts
CHANGED
|
@@ -12,14 +12,14 @@ export const MESSAGE_LIMITS: MessageLimits = {
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Where an asker may wait for an answer, by harness and placement: on where M2 passed
|
|
15
|
-
* (`docs/findings/messaging-m2.md`), off elsewhere. A
|
|
15
|
+
* (`docs/findings/messaging-m2.md`), off elsewhere. A TUI that is `confirmed` waits only where it
|
|
16
16
|
* confirms what it was sent, as `wfa waiting` needs. The fake harness always may. cursor is off:
|
|
17
17
|
* its turns ran past their deadline in M2.
|
|
18
18
|
*/
|
|
19
|
-
export const AWAIT_REPLY: Readonly<Record<string, { headless?: true;
|
|
19
|
+
export const AWAIT_REPLY: Readonly<Record<string, { headless?: true; tui?: true | "confirmed" }>> =
|
|
20
20
|
{
|
|
21
|
-
fake: { headless: true,
|
|
22
|
-
claude: { headless: true,
|
|
21
|
+
fake: { headless: true, tui: true },
|
|
22
|
+
claude: { headless: true, tui: "confirmed" },
|
|
23
23
|
codex: { headless: true },
|
|
24
24
|
pi: { headless: true },
|
|
25
25
|
};
|