@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
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { mkdir, readdir, readFile,
|
|
2
|
+
import { mkdir, readdir, readFile, rm } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { isCode, writeJson } from "./files";
|
|
4
5
|
import { machinePaths } from "./machine";
|
|
5
6
|
import { type ProcessProbe, processStart, sameProcess } from "./runs";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
const WORKSPACE_MARK_VERSION = 2;
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* A run's claim on its workspace in a shared Herdr session: the run's process, and when that
|
|
@@ -73,7 +74,7 @@ export async function markWorkspace(
|
|
|
73
74
|
};
|
|
74
75
|
try {
|
|
75
76
|
await mkdir(dir, { recursive: true });
|
|
76
|
-
await
|
|
77
|
+
await writeJson(file, mark);
|
|
77
78
|
} catch {
|
|
78
79
|
return unmarked;
|
|
79
80
|
}
|
|
@@ -85,7 +86,7 @@ export async function markWorkspace(
|
|
|
85
86
|
const write = (next: WorkspaceMark) => {
|
|
86
87
|
if (released) return writing;
|
|
87
88
|
current = next;
|
|
88
|
-
writing = writing.then(() =>
|
|
89
|
+
writing = writing.then(() => writeJson(file, current).catch(() => undefined));
|
|
89
90
|
return writing;
|
|
90
91
|
};
|
|
91
92
|
return {
|
|
@@ -110,17 +111,6 @@ export async function markWorkspace(
|
|
|
110
111
|
};
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
/** Renamed into place, so a reader never sees half a mark and skips a live run. */
|
|
114
|
-
async function writeWhole(file: string, mark: WorkspaceMark): Promise<void> {
|
|
115
|
-
const temporary = `${file}.${randomBytes(4).toString("hex")}.tmp`;
|
|
116
|
-
try {
|
|
117
|
-
await writeFile(temporary, JSON.stringify(mark));
|
|
118
|
-
await rename(temporary, file);
|
|
119
|
-
} finally {
|
|
120
|
-
await rm(temporary, { force: true }).catch(() => undefined);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
114
|
export async function readMarks(
|
|
125
115
|
home: string,
|
|
126
116
|
session: string,
|
|
@@ -163,7 +153,7 @@ export async function readAllMarks(
|
|
|
163
153
|
|
|
164
154
|
/** Rewrites a mark read from `file`, as one sweep leaves it. */
|
|
165
155
|
export function rewriteMark(file: string, mark: WorkspaceMark): Promise<void> {
|
|
166
|
-
return
|
|
156
|
+
return writeJson(file, mark).catch(() => undefined);
|
|
167
157
|
}
|
|
168
158
|
|
|
169
159
|
export function removeMark(file: string): Promise<void> {
|
|
@@ -189,6 +179,6 @@ function processExists(pid: number): boolean {
|
|
|
189
179
|
process.kill(pid, 0);
|
|
190
180
|
return true;
|
|
191
181
|
} catch (error) {
|
|
192
|
-
return (error
|
|
182
|
+
return !isCode(error, "ESRCH");
|
|
193
183
|
}
|
|
194
184
|
}
|
package/src/holder-command.ts
CHANGED
|
@@ -1,55 +1,50 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type
|
|
3
|
-
import { type
|
|
1
|
+
import type { ParticipantResultJson, RefusalCode } from "@agentswf/contract/records";
|
|
2
|
+
import { type HolderWireAnswer, type ParticipantCode, WIRE_VERSION } from "@agentswf/contract/wire";
|
|
3
|
+
import { type Out, parsePlace, type ReadPlace } from "./command-line";
|
|
4
4
|
import { liveCoordinatorSocket } from "./coordinator-socket";
|
|
5
|
-
import { deliver, eventIdProblem } from "./emit";
|
|
6
|
-
import { printEmit } from "./emit-command";
|
|
7
5
|
import { messageOf } from "./errors";
|
|
8
6
|
import { isCode } from "./files";
|
|
9
7
|
import help from "./help/reply.txt" with { type: "text" };
|
|
10
8
|
import { ItemFold } from "./inbox-command";
|
|
11
9
|
import { coordinatorDir } from "./machine";
|
|
12
|
-
import {
|
|
10
|
+
import { asksJson, printRefusal, refusalLine } from "./refusals";
|
|
11
|
+
import { failed } from "./run-commands";
|
|
13
12
|
import { findRunByRef, RunNotFound, type RunStatus } from "./run-state";
|
|
14
|
-
import {
|
|
13
|
+
import { RunUnreachable } from "./run-unreachable";
|
|
14
|
+
import { readAttempts, readNumberedEvents } from "./runs";
|
|
15
15
|
import { exchange } from "./socket-exchange";
|
|
16
16
|
|
|
17
|
+
/** `awf reply --json`'s and `awf send --json`'s `"v"`, which their refusals carry too. */
|
|
18
|
+
export const HOLDER_V = 3 satisfies ParticipantResultJson["v"];
|
|
19
|
+
|
|
17
20
|
export const replyUsage = help.trimEnd();
|
|
18
21
|
|
|
19
22
|
type Options = {
|
|
20
23
|
command: "reply" | "send";
|
|
21
24
|
ref: string;
|
|
22
|
-
author?: string;
|
|
23
25
|
to?: string;
|
|
24
26
|
text?: string;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
* `--event-id`: the message's own id, which a participant routed to the code takes as its event's. It
|
|
29
|
-
* goes to no agent.
|
|
30
|
-
*/
|
|
31
|
-
| { id: string; as: string; agent?: undefined }
|
|
32
|
-
);
|
|
27
|
+
agent: string;
|
|
28
|
+
as?: string;
|
|
29
|
+
};
|
|
33
30
|
|
|
34
|
-
|
|
31
|
+
type HolderIo = Out & {
|
|
35
32
|
/** Stdin, when no text is given as an argument. */
|
|
36
33
|
stdin(): Promise<string>;
|
|
37
34
|
/** Then there is nothing piped in to read: refused rather than waited on. */
|
|
38
35
|
stdinIsTerminal?: boolean;
|
|
39
|
-
environment
|
|
40
|
-
home
|
|
36
|
+
environment: Readonly<Record<string, string | undefined>>;
|
|
37
|
+
home: string;
|
|
41
38
|
};
|
|
42
39
|
|
|
40
|
+
/** A refusal of `reply` or `send`: its document with `--json`, its sentence on stderr, exit 1. */
|
|
41
|
+
type Refuse = (code: ParticipantCode | RefusalCode, error: string, choices?: string[]) => number;
|
|
42
|
+
|
|
43
43
|
export async function holderCommand(
|
|
44
44
|
argv: readonly string[],
|
|
45
45
|
shellCwd: string,
|
|
46
46
|
io: HolderIo,
|
|
47
47
|
): Promise<number> {
|
|
48
|
-
// Help only as the first word after the command: a message's text may say anything.
|
|
49
|
-
if (argv[1] === "-h" || argv[1] === "--help") {
|
|
50
|
-
io.stdout(replyUsage);
|
|
51
|
-
return 0;
|
|
52
|
-
}
|
|
53
48
|
let place: ReadPlace;
|
|
54
49
|
let json: boolean;
|
|
55
50
|
let options: Options;
|
|
@@ -59,34 +54,34 @@ export async function holderCommand(
|
|
|
59
54
|
json = parsed.json;
|
|
60
55
|
options = parseHolder(argv[0] as "reply" | "send", parsed.rest);
|
|
61
56
|
} catch (error) {
|
|
62
|
-
io
|
|
57
|
+
printRefusal(io, { json: asksJson(argv), v: HOLDER_V }, error, "usage", replyUsage);
|
|
63
58
|
return 2;
|
|
64
59
|
}
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
60
|
+
const delivered = (id: string, line: string): number => {
|
|
61
|
+
io.stdout(json ? JSON.stringify({ v: HOLDER_V, id } satisfies ParticipantResultJson) : line);
|
|
62
|
+
return 0;
|
|
63
|
+
};
|
|
64
|
+
const refused: Refuse = (code, error, choices) => {
|
|
65
|
+
if (json) io.stdout(refusalLine(HOLDER_V, code, error, choices));
|
|
66
|
+
io.stderr(
|
|
67
|
+
`awf: ${options.command} refused (${code}): ${error}${choices ? ` Choose: ${choices.join(", ")}` : ""}`,
|
|
68
|
+
);
|
|
69
|
+
return 1;
|
|
73
70
|
};
|
|
74
|
-
const refused = (code: ParticipantCode, error: string) => say({ v: 1, code, error }, "");
|
|
75
71
|
const usage = (problem: string) => {
|
|
76
|
-
io
|
|
72
|
+
printRefusal(io, { json, v: HOLDER_V }, problem, "usage", replyUsage);
|
|
77
73
|
return 2;
|
|
78
74
|
};
|
|
79
75
|
if (options.text !== undefined && options.text.trim() === "")
|
|
80
76
|
return usage("nothing to say: the text is blank");
|
|
81
77
|
if (options.text === undefined && io.stdinIsTerminal)
|
|
82
78
|
return usage("nothing to say: give the text as an argument, or pipe it in");
|
|
83
|
-
if (options.id !== undefined) return toTheCode(options, place, json, io, { refused, usage });
|
|
84
79
|
try {
|
|
85
80
|
let status: RunStatus;
|
|
86
81
|
try {
|
|
87
|
-
status = await findRunByRef(options.ref, place, io.home
|
|
82
|
+
status = await findRunByRef(options.ref, place, io.home);
|
|
88
83
|
} catch (error) {
|
|
89
|
-
if (error instanceof RunNotFound) return refused(
|
|
84
|
+
if (error instanceof RunNotFound) return refused(error.code, error.message);
|
|
90
85
|
throw error;
|
|
91
86
|
}
|
|
92
87
|
if (status.state !== "running")
|
|
@@ -99,7 +94,7 @@ export async function holderCommand(
|
|
|
99
94
|
"no-participant",
|
|
100
95
|
`${status.ref} has no participant${options.as ? ` ${options.as}` : ""} to reach yet; try again once it connects one`,
|
|
101
96
|
);
|
|
102
|
-
const dir = coordinatorDir(io.home
|
|
97
|
+
const dir = coordinatorDir(io.home, io.environment);
|
|
103
98
|
// Live with no socket: its attempt has not connected one yet, or is opening it.
|
|
104
99
|
const socket = await liveCoordinatorSocket(dir, status);
|
|
105
100
|
if (!socket) return unreachable();
|
|
@@ -118,23 +113,25 @@ export async function holderCommand(
|
|
|
118
113
|
// A socket an attempt left when it was killed: this attempt has not opened its own yet.
|
|
119
114
|
if (["ECONNREFUSED", "ENOENT", "ENOTSOCK"].some((code) => isCode(error, code)))
|
|
120
115
|
return unreachable();
|
|
121
|
-
|
|
116
|
+
// Open and silent, or gone mid-answer: the run doesn't answer.
|
|
117
|
+
throw new RunUnreachable(`${status.ref} did not answer: ${messageOf(error)}`);
|
|
122
118
|
}
|
|
123
119
|
if (!answer)
|
|
124
|
-
throw new
|
|
125
|
-
`${status.ref} did not answer
|
|
120
|
+
throw new RunUnreachable(
|
|
121
|
+
`${status.ref} did not answer, most likely as it runs an older awf than this one, which speaks wire version ${WIRE_VERSION}: reach it with the awf it started with, or let it end and continue it under this one.`,
|
|
126
122
|
);
|
|
127
|
-
const result = JSON.parse(answer) as
|
|
128
|
-
if (result.code !== "delivered") return
|
|
129
|
-
if (options.command === "reply") return
|
|
123
|
+
const result = JSON.parse(answer) as HolderWireAnswer;
|
|
124
|
+
if (result.code !== "delivered") return refused(result.code, result.error, result.choices);
|
|
125
|
+
if (options.command === "reply") return delivered(result.id, `answered ${result.id}`);
|
|
130
126
|
// Held for an agent awaiting this participant's answer: it comes with that answer.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
// Only the line's wording: the message is delivered, whatever reading this finds.
|
|
128
|
+
const awaited = await awaitedBy(status, options.agent, options.as).catch(() => undefined);
|
|
129
|
+
return delivered(
|
|
130
|
+
result.id,
|
|
134
131
|
`delivered as ${result.id}${awaited ? `; ${options.agent} still awaits the reply to ${awaited}` : ""}`,
|
|
135
132
|
);
|
|
136
133
|
} catch (error) {
|
|
137
|
-
return failed(error, place, io, messageOf(error));
|
|
134
|
+
return failed(error, place, io, messageOf(error), { json, v: HOLDER_V });
|
|
138
135
|
}
|
|
139
136
|
}
|
|
140
137
|
|
|
@@ -142,8 +139,6 @@ function parseHolder(command: "reply" | "send", rest: readonly string[]): Option
|
|
|
142
139
|
const words: string[] = [];
|
|
143
140
|
let to: string | undefined;
|
|
144
141
|
let as: string | undefined;
|
|
145
|
-
let id: string | undefined;
|
|
146
|
-
let author: string | undefined;
|
|
147
142
|
for (let index = 0; index < rest.length; index += 1) {
|
|
148
143
|
const arg = rest[index]!;
|
|
149
144
|
const value = () => {
|
|
@@ -154,44 +149,30 @@ function parseHolder(command: "reply" | "send", rest: readonly string[]): Option
|
|
|
154
149
|
};
|
|
155
150
|
if (arg === "--to" && command === "reply") to = value();
|
|
156
151
|
else if (arg === "--as") as = value();
|
|
157
|
-
else if (arg === "--event-id"
|
|
158
|
-
|
|
152
|
+
else if ((arg === "--event-id" || arg === "--author") && command === "send")
|
|
153
|
+
throw new Error(
|
|
154
|
+
`send ${arg} was removed: a message on a participant to the run's code is awf emit {run} {participant}:reply '{"text":…,"author":…}' --event-id {id}, with {participant}:reply declared in the workflow's events`,
|
|
155
|
+
);
|
|
159
156
|
else if (arg === "--") {
|
|
160
157
|
words.push(...rest.slice(index + 1));
|
|
161
158
|
break;
|
|
162
159
|
} else if (arg.startsWith("--")) throw new Error(`unknown option: ${arg}`);
|
|
163
160
|
else words.push(arg);
|
|
164
161
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
let text: string | undefined;
|
|
169
|
-
let extra: string[];
|
|
170
|
-
if (id !== undefined) {
|
|
171
|
-
[ref, text, ...extra] = words;
|
|
172
|
-
if (!ref) throw new Error("send needs a run, <workflow>/<id> or <id>");
|
|
173
|
-
if (!as) throw new Error("--event-id sends as a participant routed to the code: --as names it");
|
|
174
|
-
const problem = eventIdProblem(id);
|
|
175
|
-
if (problem) throw new Error(`--event-id ${id}: ${problem}`);
|
|
176
|
-
target = { id, as };
|
|
177
|
-
} else {
|
|
178
|
-
let agent: string | undefined;
|
|
179
|
-
[ref, agent, text, ...extra] = words;
|
|
180
|
-
if (author !== undefined) throw new Error("--author goes with --event-id");
|
|
181
|
-
if (!ref || !agent)
|
|
182
|
-
throw new Error(`${command} needs a run, <workflow>/<id> or <id>, and an agent`);
|
|
183
|
-
target = { agent, ...(as === undefined ? {} : { as }) };
|
|
184
|
-
}
|
|
162
|
+
const [ref, agent, text, ...extra] = words;
|
|
163
|
+
if (!ref || !agent?.trim())
|
|
164
|
+
throw new Error(`${command} needs a run, <workflow>/<id> or <id>, and an agent`);
|
|
185
165
|
if (extra.length > 0) throw new Error("the text is one argument: quote it, or pipe it in");
|
|
166
|
+
if (as !== undefined && as.trim() === "") throw new Error("--as needs a participant's key");
|
|
186
167
|
if (to !== undefined && !/^\d+\.\d+$/.test(to))
|
|
187
168
|
throw new Error(`--to takes a question's id as awf inbox prints it, such as 1.5, not ${to}`);
|
|
188
169
|
return {
|
|
189
170
|
command,
|
|
190
171
|
ref,
|
|
191
|
-
|
|
172
|
+
agent,
|
|
173
|
+
...(as === undefined ? {} : { as }),
|
|
192
174
|
...(to === undefined ? {} : { to }),
|
|
193
175
|
...(text === undefined ? {} : { text }),
|
|
194
|
-
...(author === undefined ? {} : { author }),
|
|
195
176
|
};
|
|
196
177
|
}
|
|
197
178
|
|
|
@@ -219,61 +200,3 @@ async function awaitedBy(
|
|
|
219
200
|
);
|
|
220
201
|
return open.at(-1);
|
|
221
202
|
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* `awf send {ref} --as {participant} --event-id {message id} {text}`: a reply on a participant the run
|
|
225
|
-
* routed to the code, delivered through the one way in as `participant:reply`, so it continues a
|
|
226
|
-
* parked run. The run is found here, or in a project awf started an event run in (Q15).
|
|
227
|
-
*/
|
|
228
|
-
async function toTheCode(
|
|
229
|
-
options: Extract<Options, { id: string }>,
|
|
230
|
-
place: ReadPlace,
|
|
231
|
-
json: boolean,
|
|
232
|
-
io: HolderIo,
|
|
233
|
-
say: { refused(code: ParticipantCode, error: string): number; usage(problem: string): number },
|
|
234
|
-
): Promise<number> {
|
|
235
|
-
const home = io.home ?? homedir();
|
|
236
|
-
const environment = io.environment ?? process.env;
|
|
237
|
-
const { id, as } = options;
|
|
238
|
-
const { refused } = say;
|
|
239
|
-
let found: RunStatus;
|
|
240
|
-
try {
|
|
241
|
-
found = await findRunByRef(options.ref, place, home);
|
|
242
|
-
} catch (error) {
|
|
243
|
-
if (error instanceof RunNotFound) return refused("not-found", error.message);
|
|
244
|
-
return failed(error, place, io, messageOf(error));
|
|
245
|
-
}
|
|
246
|
-
const { run } = found;
|
|
247
|
-
const routed = (await readEvents(run.dir)).some(
|
|
248
|
-
(line) =>
|
|
249
|
-
line.kind === "participant-connected" && line.participant === as && line.to === "code",
|
|
250
|
-
);
|
|
251
|
-
if (!run.record.events || !routed)
|
|
252
|
-
return refused(
|
|
253
|
-
"no-participant",
|
|
254
|
-
`${found.ref} has no participant ${as} routed to its code; --event-id is for one`,
|
|
255
|
-
);
|
|
256
|
-
const text = options.text ?? (await io.stdin());
|
|
257
|
-
if (text.trim() === "") return say.usage("nothing to say: stdin was empty");
|
|
258
|
-
try {
|
|
259
|
-
const result = await deliver(
|
|
260
|
-
{
|
|
261
|
-
runRoot: run.root,
|
|
262
|
-
workflow: run.record.workflow,
|
|
263
|
-
runId: run.record.id,
|
|
264
|
-
event: {
|
|
265
|
-
id,
|
|
266
|
-
name: "participant:reply",
|
|
267
|
-
data: { text, ...(options.author === undefined ? {} : { author: options.author }) },
|
|
268
|
-
from: as,
|
|
269
|
-
at: new Date().toISOString(),
|
|
270
|
-
},
|
|
271
|
-
deduplicate: true,
|
|
272
|
-
},
|
|
273
|
-
{ home, environment },
|
|
274
|
-
);
|
|
275
|
-
return printEmit(result, json, io);
|
|
276
|
-
} catch (error) {
|
|
277
|
-
return failed(error, place, io, messageOf(error));
|
|
278
|
-
}
|
|
279
|
-
}
|
package/src/in-lines.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { EventLine } from "@agentswf/contract/records";
|
|
4
|
+
import { appendLine, endTornLine, readLines } from "./jsonl";
|
|
5
|
+
|
|
6
|
+
/** `in.jsonl`: the events senders gave the run, appended under the run's lock. */
|
|
7
|
+
export function inFile(runDir: string): string {
|
|
8
|
+
return join(runDir, "in.jsonl");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** The run's events in the order written, less a line a crash tore. */
|
|
12
|
+
export async function readIn(runDir: string): Promise<EventLine[]> {
|
|
13
|
+
return readLines<EventLine>(inFile(runDir));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Appends an event; the caller holds the run's lock. */
|
|
17
|
+
export async function appendIn(runDir: string, line: EventLine): Promise<void> {
|
|
18
|
+
await endTornLine(inFile(runDir));
|
|
19
|
+
await appendLine(inFile(runDir), JSON.stringify(line));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `in.jsonl`'s size, which a waiting run polls: a sender's append changes it. */
|
|
23
|
+
export async function inSize(runDir: string): Promise<number> {
|
|
24
|
+
return stat(inFile(runDir)).then(
|
|
25
|
+
(stats) => stats.size,
|
|
26
|
+
() => 0,
|
|
27
|
+
);
|
|
28
|
+
}
|
package/src/inbox-command.ts
CHANGED
|
@@ -2,30 +2,27 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type {
|
|
4
4
|
AttemptRecord,
|
|
5
|
-
InboxErrorJson,
|
|
6
5
|
InboxExpiry,
|
|
7
6
|
InboxItemJson,
|
|
7
|
+
RefusalCode,
|
|
8
8
|
RequestEnd,
|
|
9
9
|
RunLogLine,
|
|
10
10
|
} from "@agentswf/contract/records";
|
|
11
11
|
import type { InboxCode } from "@agentswf/contract/wire";
|
|
12
12
|
import type { JsonObject } from "@agentswf/contract/workflow";
|
|
13
13
|
import { maskCredentials } from "@agentswf/harness";
|
|
14
|
-
import { duration } from "./
|
|
14
|
+
import { duration } from "./accounting/format";
|
|
15
|
+
import { command, type Out, parsePlace, type ReadPlace, shellLine } from "./command-line";
|
|
16
|
+
import { parseWait } from "./duration";
|
|
15
17
|
import { messageOf } from "./errors";
|
|
16
18
|
import { exists } from "./files";
|
|
17
19
|
import help from "./help/inbox.txt" with { type: "text" };
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
failed,
|
|
23
|
-
type Out,
|
|
24
|
-
parsePlace,
|
|
25
|
-
type ReadPlace,
|
|
26
|
-
shellLine,
|
|
27
|
-
} from "./run-commands";
|
|
20
|
+
import { asksJson, printRefusal } from "./refusals";
|
|
21
|
+
import { failed, MAX_WAIT_SECONDS, POLL_MS } from "./run-commands";
|
|
22
|
+
import { readRunLines } from "./run-lines";
|
|
23
|
+
import { waitLabels as labelsOf } from "./run-policy/waits";
|
|
28
24
|
import { findRunByRef, listRuns, RunNotFound, statusOf } from "./run-state";
|
|
25
|
+
import { clockTime } from "./run-text";
|
|
29
26
|
import {
|
|
30
27
|
attemptsToken,
|
|
31
28
|
type LogMark,
|
|
@@ -35,10 +32,10 @@ import {
|
|
|
35
32
|
readEventsFrom,
|
|
36
33
|
} from "./runs";
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
/** `awf inbox --json`'s `"v"`, which its refusals carry too. */
|
|
36
|
+
export const INBOX_V = 2 satisfies InboxItemJson["v"];
|
|
39
37
|
|
|
40
|
-
const
|
|
41
|
-
const POLL_MS = 1_000;
|
|
38
|
+
export const inboxUsage = help.trimEnd();
|
|
42
39
|
|
|
43
40
|
/** The clock and the pause between polls, which tests drive. */
|
|
44
41
|
export type InboxClock = { now(): number; sleep(ms: number): Promise<void> };
|
|
@@ -66,7 +63,7 @@ type CursorState = { f: string; r: Record<string, Position> };
|
|
|
66
63
|
|
|
67
64
|
class Refused extends Error {
|
|
68
65
|
constructor(
|
|
69
|
-
readonly code: InboxCode,
|
|
66
|
+
readonly code: InboxCode | RefusalCode,
|
|
70
67
|
message: string,
|
|
71
68
|
) {
|
|
72
69
|
super(message);
|
|
@@ -89,10 +86,6 @@ export async function inboxCommand(
|
|
|
89
86
|
signal?: AbortSignal;
|
|
90
87
|
} = {},
|
|
91
88
|
): Promise<number> {
|
|
92
|
-
if (argv.includes("-h") || argv.includes("--help")) {
|
|
93
|
-
out.stdout(inboxUsage);
|
|
94
|
-
return 0;
|
|
95
|
-
}
|
|
96
89
|
let place: ReadPlace;
|
|
97
90
|
let json: boolean;
|
|
98
91
|
let options: Options;
|
|
@@ -102,12 +95,11 @@ export async function inboxCommand(
|
|
|
102
95
|
json = parsed.json;
|
|
103
96
|
options = parseInbox(parsed.rest);
|
|
104
97
|
} catch (error) {
|
|
105
|
-
out
|
|
98
|
+
printRefusal(out, { json: asksJson(argv), v: INBOX_V }, error, "usage", inboxUsage);
|
|
106
99
|
return 2;
|
|
107
100
|
}
|
|
108
|
-
const refuse = (code: InboxCode, message: string) => {
|
|
109
|
-
|
|
110
|
-
else out.stderr(`awf: ${message}`);
|
|
101
|
+
const refuse = (code: InboxCode | RefusalCode, message: string) => {
|
|
102
|
+
printRefusal(out, { json, v: INBOX_V }, message, code);
|
|
111
103
|
return 1;
|
|
112
104
|
};
|
|
113
105
|
try {
|
|
@@ -115,18 +107,19 @@ export async function inboxCommand(
|
|
|
115
107
|
? `all|${options.workflow ?? ""}|${options.as ?? ""}`
|
|
116
108
|
: `run|${options.ref}|${options.as ?? ""}`;
|
|
117
109
|
let state: CursorState = { f: filter, r: {} };
|
|
118
|
-
// Reading every run here, none is missing or unconnected, so a cursor is checked first, sparing a read.
|
|
119
110
|
let checked = options.after === undefined;
|
|
120
|
-
|
|
111
|
+
const takeCursor = () => {
|
|
121
112
|
const given = decodeCursor(options.after!);
|
|
122
113
|
if (!given || given.f !== filter)
|
|
123
114
|
throw new Refused(
|
|
124
|
-
"
|
|
115
|
+
"cursor",
|
|
125
116
|
`cursor ${options.after} was made with another run or filter; read again without --after`,
|
|
126
117
|
);
|
|
127
118
|
state = given;
|
|
128
119
|
checked = true;
|
|
129
|
-
}
|
|
120
|
+
};
|
|
121
|
+
// Reading every run here, none is missing or unconnected, so a cursor is checked first, sparing a read.
|
|
122
|
+
if (!checked && options.all) takeCursor();
|
|
130
123
|
const deadline = clock.now() + options.waitSeconds * 1000;
|
|
131
124
|
// An event run lives in its project, found through ~/.awf/projects from anywhere (Q15).
|
|
132
125
|
const found =
|
|
@@ -137,14 +130,7 @@ export async function inboxCommand(
|
|
|
137
130
|
const runs = await readInbox(options, state, watch, one);
|
|
138
131
|
// After a run that is not there, or never connected one: those come first.
|
|
139
132
|
if (!checked) {
|
|
140
|
-
|
|
141
|
-
if (!given || given.f !== filter)
|
|
142
|
-
throw new Refused(
|
|
143
|
-
"bad-cursor",
|
|
144
|
-
`cursor ${options.after} was made with another run or filter; read again without --after`,
|
|
145
|
-
);
|
|
146
|
-
state = given;
|
|
147
|
-
checked = true;
|
|
133
|
+
takeCursor();
|
|
148
134
|
continue;
|
|
149
135
|
}
|
|
150
136
|
if (options.open) {
|
|
@@ -165,7 +151,7 @@ export async function inboxCommand(
|
|
|
165
151
|
state = { f: filter, r: { ...state.r, [ref]: item.position } };
|
|
166
152
|
const { position: _position, ...shown } = item;
|
|
167
153
|
const published = {
|
|
168
|
-
v:
|
|
154
|
+
v: 2,
|
|
169
155
|
ref,
|
|
170
156
|
...shown,
|
|
171
157
|
cursor: encodeCursor(state),
|
|
@@ -178,13 +164,12 @@ export async function inboxCommand(
|
|
|
178
164
|
const words = options.all
|
|
179
165
|
? ["awf", "inbox", ...(options.workflow ? ["--workflow", options.workflow] : [])]
|
|
180
166
|
: ["awf", "inbox", options.ref!];
|
|
181
|
-
const again =
|
|
182
|
-
place,
|
|
183
|
-
...words,
|
|
167
|
+
const again = [
|
|
168
|
+
...command(place, ...words),
|
|
184
169
|
...(options.as ? ["--as", options.as] : []),
|
|
185
170
|
"--after",
|
|
186
171
|
encodeCursor(state),
|
|
187
|
-
|
|
172
|
+
];
|
|
188
173
|
out.stdout([...lines, `next: ${shellLine(again)}`].join("\n"));
|
|
189
174
|
}
|
|
190
175
|
return 0;
|
|
@@ -193,8 +178,8 @@ export async function inboxCommand(
|
|
|
193
178
|
}
|
|
194
179
|
} catch (error) {
|
|
195
180
|
if (error instanceof Refused) return refuse(error.code, error.message);
|
|
196
|
-
if (error instanceof RunNotFound) return refuse(
|
|
197
|
-
return failed(error, place, out, messageOf(error));
|
|
181
|
+
if (error instanceof RunNotFound) return refuse(error.code, error.message);
|
|
182
|
+
return failed(error, place, out, messageOf(error), { json, v: INBOX_V });
|
|
198
183
|
}
|
|
199
184
|
}
|
|
200
185
|
|
|
@@ -212,12 +197,8 @@ function parseInbox(rest: readonly string[]): Options {
|
|
|
212
197
|
else if (arg === "--workflow") options.workflow = value();
|
|
213
198
|
else if (arg === "--as") options.as = value();
|
|
214
199
|
else if (arg === "--after") options.after = value();
|
|
215
|
-
else if (arg === "--wait")
|
|
216
|
-
|
|
217
|
-
if (!Number.isFinite(seconds) || seconds < 0 || seconds > MAX_WAIT_SECONDS)
|
|
218
|
-
throw new Error(`--wait needs seconds from 0 to ${MAX_WAIT_SECONDS}`);
|
|
219
|
-
options.waitSeconds = seconds;
|
|
220
|
-
} else if (arg.startsWith("-")) throw new Error(`unknown option: ${arg}`);
|
|
200
|
+
else if (arg === "--wait") options.waitSeconds = parseWait(value(), MAX_WAIT_SECONDS);
|
|
201
|
+
else if (arg.startsWith("-")) throw new Error(`unknown option: ${arg}`);
|
|
221
202
|
else if (options.ref === undefined) options.ref = arg;
|
|
222
203
|
else throw new Error(`one run at a time: ${options.ref} and ${arg}`);
|
|
223
204
|
}
|
|
@@ -271,7 +252,7 @@ type Watched = {
|
|
|
271
252
|
/** The attempt files when `attempts` was read; undefined before the first read. */
|
|
272
253
|
token?: string;
|
|
273
254
|
attempts: AttemptRecord[];
|
|
274
|
-
/** Each attempt's last wait label, from
|
|
255
|
+
/** Each attempt's last wait label, from `run.jsonl`, read with `attempts`. */
|
|
275
256
|
labels: ReadonlyMap<number, string>;
|
|
276
257
|
fold: ItemFold;
|
|
277
258
|
mark: LogMark;
|
|
@@ -631,7 +612,7 @@ function openQuestions(runs: readonly RunItems[]): InboxItemJson[] {
|
|
|
631
612
|
});
|
|
632
613
|
return merged(open).map(({ ref, item }) => {
|
|
633
614
|
const { position: _position, ...shown } = item;
|
|
634
|
-
return { v:
|
|
615
|
+
return { v: INBOX_V, ref, ...shown } as InboxItemJson;
|
|
635
616
|
});
|
|
636
617
|
}
|
|
637
618
|
|
|
@@ -705,11 +686,13 @@ function decodeCursor(text: string): CursorState | undefined {
|
|
|
705
686
|
}
|
|
706
687
|
}
|
|
707
688
|
|
|
708
|
-
/**
|
|
689
|
+
/**
|
|
690
|
+
* Each attempt's last wait label: what a parked attempt is waiting for. A `run.jsonl` that can't be
|
|
691
|
+
* read gives none: the conversation is the log's.
|
|
692
|
+
*/
|
|
709
693
|
async function waitLabels(runDir: string): Promise<Map<number, string>> {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
return labels;
|
|
694
|
+
return readRunLines(runDir).then(
|
|
695
|
+
({ lines }) => labelsOf(lines),
|
|
696
|
+
() => new Map(),
|
|
697
|
+
);
|
|
715
698
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// A finished run is
|
|
1
|
+
// A finished run is priced again from its operations' `spend.json`, or `out.json`'s `usage`, with another table.
|
|
2
2
|
export { describeAccounting } from "./accounting/format";
|
|
3
3
|
export { type ModelRate, type PriceTable, PUBLISHED_PRICES } from "./accounting/prices";
|
|
4
4
|
export { summarizeRun } from "./accounting/summary";
|