@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/holder-command.ts
CHANGED
|
@@ -1,56 +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 { deprecationLine } from "./deprecations";
|
|
6
|
-
import { deliverReply, emitToRun, eventIdProblem } from "./emit";
|
|
7
|
-
import { printEmit } from "./emit-command";
|
|
8
5
|
import { messageOf } from "./errors";
|
|
9
6
|
import { isCode } from "./files";
|
|
10
7
|
import help from "./help/reply.txt" with { type: "text" };
|
|
11
8
|
import { ItemFold } from "./inbox-command";
|
|
12
9
|
import { coordinatorDir } from "./machine";
|
|
13
|
-
import {
|
|
10
|
+
import { asksJson, printRefusal, refusalLine } from "./refusals";
|
|
11
|
+
import { failed } from "./run-commands";
|
|
14
12
|
import { findRunByRef, RunNotFound, type RunStatus } from "./run-state";
|
|
15
|
-
import {
|
|
13
|
+
import { RunUnreachable } from "./run-unreachable";
|
|
14
|
+
import { readAttempts, readNumberedEvents } from "./runs";
|
|
16
15
|
import { exchange } from "./socket-exchange";
|
|
17
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
|
+
|
|
18
20
|
export const replyUsage = help.trimEnd();
|
|
19
21
|
|
|
20
22
|
type Options = {
|
|
21
23
|
command: "reply" | "send";
|
|
22
24
|
ref: string;
|
|
23
|
-
author?: string;
|
|
24
25
|
to?: string;
|
|
25
26
|
text?: string;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
* `--event-id`: the message's own id, which a participant routed to the code takes as its event's. It
|
|
30
|
-
* goes to no agent.
|
|
31
|
-
*/
|
|
32
|
-
| { id: string; as: string; agent?: undefined }
|
|
33
|
-
);
|
|
27
|
+
agent: string;
|
|
28
|
+
as?: string;
|
|
29
|
+
};
|
|
34
30
|
|
|
35
|
-
|
|
31
|
+
type HolderIo = Out & {
|
|
36
32
|
/** Stdin, when no text is given as an argument. */
|
|
37
33
|
stdin(): Promise<string>;
|
|
38
34
|
/** Then there is nothing piped in to read: refused rather than waited on. */
|
|
39
35
|
stdinIsTerminal?: boolean;
|
|
40
|
-
environment
|
|
41
|
-
home
|
|
36
|
+
environment: Readonly<Record<string, string | undefined>>;
|
|
37
|
+
home: string;
|
|
42
38
|
};
|
|
43
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
|
+
|
|
44
43
|
export async function holderCommand(
|
|
45
44
|
argv: readonly string[],
|
|
46
45
|
shellCwd: string,
|
|
47
46
|
io: HolderIo,
|
|
48
47
|
): Promise<number> {
|
|
49
|
-
// Help only as the first word after the command: a message's text may say anything.
|
|
50
|
-
if (argv[1] === "-h" || argv[1] === "--help") {
|
|
51
|
-
io.stdout(replyUsage);
|
|
52
|
-
return 0;
|
|
53
|
-
}
|
|
54
48
|
let place: ReadPlace;
|
|
55
49
|
let json: boolean;
|
|
56
50
|
let options: Options;
|
|
@@ -60,34 +54,34 @@ export async function holderCommand(
|
|
|
60
54
|
json = parsed.json;
|
|
61
55
|
options = parseHolder(argv[0] as "reply" | "send", parsed.rest);
|
|
62
56
|
} catch (error) {
|
|
63
|
-
io
|
|
57
|
+
printRefusal(io, { json: asksJson(argv), v: HOLDER_V }, error, "usage", replyUsage);
|
|
64
58
|
return 2;
|
|
65
59
|
}
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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;
|
|
74
70
|
};
|
|
75
|
-
const refused = (code: ParticipantCode, error: string) => say({ v: 2, code, error }, "");
|
|
76
71
|
const usage = (problem: string) => {
|
|
77
|
-
io
|
|
72
|
+
printRefusal(io, { json, v: HOLDER_V }, problem, "usage", replyUsage);
|
|
78
73
|
return 2;
|
|
79
74
|
};
|
|
80
75
|
if (options.text !== undefined && options.text.trim() === "")
|
|
81
76
|
return usage("nothing to say: the text is blank");
|
|
82
77
|
if (options.text === undefined && io.stdinIsTerminal)
|
|
83
78
|
return usage("nothing to say: give the text as an argument, or pipe it in");
|
|
84
|
-
if (options.id !== undefined) return toTheCode(options, place, json, io, { refused, usage });
|
|
85
79
|
try {
|
|
86
80
|
let status: RunStatus;
|
|
87
81
|
try {
|
|
88
|
-
status = await findRunByRef(options.ref, place, io.home
|
|
82
|
+
status = await findRunByRef(options.ref, place, io.home);
|
|
89
83
|
} catch (error) {
|
|
90
|
-
if (error instanceof RunNotFound) return refused(
|
|
84
|
+
if (error instanceof RunNotFound) return refused(error.code, error.message);
|
|
91
85
|
throw error;
|
|
92
86
|
}
|
|
93
87
|
if (status.state !== "running")
|
|
@@ -100,7 +94,7 @@ export async function holderCommand(
|
|
|
100
94
|
"no-participant",
|
|
101
95
|
`${status.ref} has no participant${options.as ? ` ${options.as}` : ""} to reach yet; try again once it connects one`,
|
|
102
96
|
);
|
|
103
|
-
const dir = coordinatorDir(io.home
|
|
97
|
+
const dir = coordinatorDir(io.home, io.environment);
|
|
104
98
|
// Live with no socket: its attempt has not connected one yet, or is opening it.
|
|
105
99
|
const socket = await liveCoordinatorSocket(dir, status);
|
|
106
100
|
if (!socket) return unreachable();
|
|
@@ -119,23 +113,25 @@ export async function holderCommand(
|
|
|
119
113
|
// A socket an attempt left when it was killed: this attempt has not opened its own yet.
|
|
120
114
|
if (["ECONNREFUSED", "ENOENT", "ENOTSOCK"].some((code) => isCode(error, code)))
|
|
121
115
|
return unreachable();
|
|
122
|
-
|
|
116
|
+
// Open and silent, or gone mid-answer: the run doesn't answer.
|
|
117
|
+
throw new RunUnreachable(`${status.ref} did not answer: ${messageOf(error)}`);
|
|
123
118
|
}
|
|
124
119
|
if (!answer)
|
|
125
|
-
throw new
|
|
120
|
+
throw new RunUnreachable(
|
|
126
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.`,
|
|
127
122
|
);
|
|
128
|
-
const result = JSON.parse(answer) as
|
|
129
|
-
if (result.code !== "delivered") return
|
|
130
|
-
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}`);
|
|
131
126
|
// Held for an agent awaiting this participant's answer: it comes with that answer.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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,
|
|
135
131
|
`delivered as ${result.id}${awaited ? `; ${options.agent} still awaits the reply to ${awaited}` : ""}`,
|
|
136
132
|
);
|
|
137
133
|
} catch (error) {
|
|
138
|
-
return failed(error, place, io, messageOf(error));
|
|
134
|
+
return failed(error, place, io, messageOf(error), { json, v: HOLDER_V });
|
|
139
135
|
}
|
|
140
136
|
}
|
|
141
137
|
|
|
@@ -143,8 +139,6 @@ function parseHolder(command: "reply" | "send", rest: readonly string[]): Option
|
|
|
143
139
|
const words: string[] = [];
|
|
144
140
|
let to: string | undefined;
|
|
145
141
|
let as: string | undefined;
|
|
146
|
-
let id: string | undefined;
|
|
147
|
-
let author: string | undefined;
|
|
148
142
|
for (let index = 0; index < rest.length; index += 1) {
|
|
149
143
|
const arg = rest[index]!;
|
|
150
144
|
const value = () => {
|
|
@@ -155,34 +149,19 @@ function parseHolder(command: "reply" | "send", rest: readonly string[]): Option
|
|
|
155
149
|
};
|
|
156
150
|
if (arg === "--to" && command === "reply") to = value();
|
|
157
151
|
else if (arg === "--as") as = value();
|
|
158
|
-
else if (arg === "--event-id"
|
|
159
|
-
|
|
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
|
+
);
|
|
160
156
|
else if (arg === "--") {
|
|
161
157
|
words.push(...rest.slice(index + 1));
|
|
162
158
|
break;
|
|
163
159
|
} else if (arg.startsWith("--")) throw new Error(`unknown option: ${arg}`);
|
|
164
160
|
else words.push(arg);
|
|
165
161
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
let text: string | undefined;
|
|
170
|
-
let extra: string[];
|
|
171
|
-
if (id !== undefined) {
|
|
172
|
-
[ref, text, ...extra] = words;
|
|
173
|
-
if (!ref) throw new Error("send needs a run, <workflow>/<id> or <id>");
|
|
174
|
-
if (!as) throw new Error("--event-id sends as a participant routed to the code: --as names it");
|
|
175
|
-
const problem = eventIdProblem(id);
|
|
176
|
-
if (problem) throw new Error(`--event-id ${id}: ${problem}`);
|
|
177
|
-
target = { id, as };
|
|
178
|
-
} else {
|
|
179
|
-
let agent: string | undefined;
|
|
180
|
-
[ref, agent, text, ...extra] = words;
|
|
181
|
-
if (author !== undefined) throw new Error("--author goes with --event-id");
|
|
182
|
-
if (!ref || !agent?.trim())
|
|
183
|
-
throw new Error(`${command} needs a run, <workflow>/<id> or <id>, and an agent`);
|
|
184
|
-
target = { agent, ...(as === undefined ? {} : { as }) };
|
|
185
|
-
}
|
|
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`);
|
|
186
165
|
if (extra.length > 0) throw new Error("the text is one argument: quote it, or pipe it in");
|
|
187
166
|
if (as !== undefined && as.trim() === "") throw new Error("--as needs a participant's key");
|
|
188
167
|
if (to !== undefined && !/^\d+\.\d+$/.test(to))
|
|
@@ -190,10 +169,10 @@ function parseHolder(command: "reply" | "send", rest: readonly string[]): Option
|
|
|
190
169
|
return {
|
|
191
170
|
command,
|
|
192
171
|
ref,
|
|
193
|
-
|
|
172
|
+
agent,
|
|
173
|
+
...(as === undefined ? {} : { as }),
|
|
194
174
|
...(to === undefined ? {} : { to }),
|
|
195
175
|
...(text === undefined ? {} : { text }),
|
|
196
|
-
...(author === undefined ? {} : { author }),
|
|
197
176
|
};
|
|
198
177
|
}
|
|
199
178
|
|
|
@@ -221,64 +200,3 @@ async function awaitedBy(
|
|
|
221
200
|
);
|
|
222
201
|
return open.at(-1);
|
|
223
202
|
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* `awf send {ref} --as {participant} --event-id {message id} {text}`, deprecated: `awf emit {ref}
|
|
227
|
-
* {participant}:reply`. It keeps its own refusals and exit codes for the beta: a participant the
|
|
228
|
-
* run never connected is refused; one the run recorded as routed to the code, declared or by the
|
|
229
|
-
* deprecated `to: "code"`, gets the reply as before, the workflow's file not loaded; any other is
|
|
230
|
-
* `awf emit`'s to check against the definition.
|
|
231
|
-
*/
|
|
232
|
-
async function toTheCode(
|
|
233
|
-
options: Extract<Options, { id: string }>,
|
|
234
|
-
place: ReadPlace,
|
|
235
|
-
json: boolean,
|
|
236
|
-
io: HolderIo,
|
|
237
|
-
say: { refused(code: ParticipantCode, error: string): number; usage(problem: string): number },
|
|
238
|
-
): Promise<number> {
|
|
239
|
-
const home = io.home ?? homedir();
|
|
240
|
-
const environment = io.environment ?? process.env;
|
|
241
|
-
const { id, as } = options;
|
|
242
|
-
const { refused } = say;
|
|
243
|
-
io.stderr(deprecationLine("send-event-id"));
|
|
244
|
-
let found: RunStatus;
|
|
245
|
-
try {
|
|
246
|
-
found = await findRunByRef(options.ref, place, home);
|
|
247
|
-
} catch (error) {
|
|
248
|
-
if (error instanceof RunNotFound) return refused("not-found", error.message);
|
|
249
|
-
return failed(error, place, io, messageOf(error));
|
|
250
|
-
}
|
|
251
|
-
const connected = (await readEvents(found.run.dir)).filter(
|
|
252
|
-
(line) => line.kind === "participant-connected" && line.participant === as,
|
|
253
|
-
);
|
|
254
|
-
// As it always did: only a participant the run has connected; `awf emit` doesn't ask.
|
|
255
|
-
if (!found.run.record.events || connected.length === 0)
|
|
256
|
-
return refused(
|
|
257
|
-
"no-participant",
|
|
258
|
-
`${found.ref} has no participant ${as} connected to its code; --event-id is for one`,
|
|
259
|
-
);
|
|
260
|
-
const text = options.text ?? (await io.stdin());
|
|
261
|
-
if (text.trim() === "") return say.usage("nothing to say: stdin was empty");
|
|
262
|
-
const data = { text, ...(options.author === undefined ? {} : { author: options.author }) };
|
|
263
|
-
const context = { home, environment };
|
|
264
|
-
// Recorded as routed to the code, by either form: delivered as before, the file not loaded.
|
|
265
|
-
if (connected.some((line) => line.kind === "participant-connected" && line.to)) {
|
|
266
|
-
try {
|
|
267
|
-
return printEmit(await deliverReply(found, as, { data, id }, context), json, io);
|
|
268
|
-
} catch (error) {
|
|
269
|
-
return failed(error, place, io, messageOf(error));
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
// Connected to agents only, which send has always refused: whatever stops `awf emit`, a file
|
|
273
|
-
// that won't load included, is that refusal, with its reason.
|
|
274
|
-
let result: Awaited<ReturnType<typeof emitToRun>>;
|
|
275
|
-
try {
|
|
276
|
-
result = await emitToRun(found, { name: `${as}:reply`, data, id }, context);
|
|
277
|
-
} catch (error) {
|
|
278
|
-
return refused(
|
|
279
|
-
"no-participant",
|
|
280
|
-
`${found.ref} has no participant ${as} whose messages go to its code: ${messageOf(error)}`,
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
return printEmit(result, json, io);
|
|
284
|
-
}
|
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 {
|
|
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;
|
|
@@ -100,14 +93,13 @@ export async function inboxCommand(
|
|
|
100
93
|
const parsed = parsePlace(argv, shellCwd);
|
|
101
94
|
place = parsed.place;
|
|
102
95
|
json = parsed.json;
|
|
103
|
-
options = parseInbox(parsed.rest
|
|
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,12 +178,12 @@ 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
|
|
|
201
|
-
function parseInbox(rest: readonly string[]
|
|
186
|
+
function parseInbox(rest: readonly string[]): Options {
|
|
202
187
|
const options: Options = { all: false, open: false, waitSeconds: 0 };
|
|
203
188
|
for (let index = 0; index < rest.length; index += 1) {
|
|
204
189
|
const arg = rest[index]!;
|
|
@@ -212,8 +197,7 @@ function parseInbox(rest: readonly string[], deprecated: (line: string) => void)
|
|
|
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
|
-
options.waitSeconds = parseWait(value(), MAX_WAIT_SECONDS, deprecated);
|
|
200
|
+
else if (arg === "--wait") options.waitSeconds = parseWait(value(), MAX_WAIT_SECONDS);
|
|
217
201
|
else if (arg.startsWith("-")) throw new Error(`unknown option: ${arg}`);
|
|
218
202
|
else if (options.ref === undefined) options.ref = arg;
|
|
219
203
|
else throw new Error(`one run at a time: ${options.ref} and ${arg}`);
|
|
@@ -268,7 +252,7 @@ type Watched = {
|
|
|
268
252
|
/** The attempt files when `attempts` was read; undefined before the first read. */
|
|
269
253
|
token?: string;
|
|
270
254
|
attempts: AttemptRecord[];
|
|
271
|
-
/** Each attempt's last wait label, from
|
|
255
|
+
/** Each attempt's last wait label, from `run.jsonl`, read with `attempts`. */
|
|
272
256
|
labels: ReadonlyMap<number, string>;
|
|
273
257
|
fold: ItemFold;
|
|
274
258
|
mark: LogMark;
|
|
@@ -628,7 +612,7 @@ function openQuestions(runs: readonly RunItems[]): InboxItemJson[] {
|
|
|
628
612
|
});
|
|
629
613
|
return merged(open).map(({ ref, item }) => {
|
|
630
614
|
const { position: _position, ...shown } = item;
|
|
631
|
-
return { v:
|
|
615
|
+
return { v: INBOX_V, ref, ...shown } as InboxItemJson;
|
|
632
616
|
});
|
|
633
617
|
}
|
|
634
618
|
|
|
@@ -655,7 +639,7 @@ function textOf(item: InboxItemJson, all: boolean): string {
|
|
|
655
639
|
? `${item.agent} → ${item.participant}`
|
|
656
640
|
: `${item.participant} → ${item.agent}`;
|
|
657
641
|
return item.state === "opened"
|
|
658
|
-
? `${head}route ${arrow} opened “${item.purpose}”${item.timeoutMs === undefined ? "" : ` ${
|
|
642
|
+
? `${head}route ${arrow} opened “${item.purpose}”${item.timeoutMs === undefined ? "" : ` ${duration(item.timeoutMs)}`}`
|
|
659
643
|
: `${head}route ${arrow} closed ${item.reason}`;
|
|
660
644
|
}
|
|
661
645
|
case "message": {
|
|
@@ -702,11 +686,13 @@ function decodeCursor(text: string): CursorState | undefined {
|
|
|
702
686
|
}
|
|
703
687
|
}
|
|
704
688
|
|
|
705
|
-
/**
|
|
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
|
+
*/
|
|
706
693
|
async function waitLabels(runDir: string): Promise<Map<number, string>> {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
return labels;
|
|
694
|
+
return readRunLines(runDir).then(
|
|
695
|
+
({ lines }) => labelsOf(lines),
|
|
696
|
+
() => new Map(),
|
|
697
|
+
);
|
|
712
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";
|
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,7 +1,8 @@
|
|
|
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." },
|
|
@@ -12,7 +13,7 @@ export default defineWorkflow({
|
|
|
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",
|