@agentswf/engine 0.1.0-beta.2 → 0.1.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -6
- package/src/agent-cli/cli.ts +571 -0
- package/src/agent-cli/client.ts +115 -0
- package/src/agent-launcher.ts +15 -25
- package/src/allowance-command.ts +31 -21
- package/src/attempt-ending.ts +23 -5
- package/src/attempt-output.ts +48 -18
- package/src/attempt-view.ts +25 -15
- package/src/commands.ts +176 -0
- package/src/control-plane.ts +73 -22
- package/src/coordinator-port.ts +29 -0
- package/src/coordinator-prompt.ts +237 -0
- package/src/coordinator-socket.ts +236 -0
- package/src/deadlines.ts +47 -10
- package/src/decisions/directory.ts +8 -6
- package/src/duration.ts +29 -0
- package/src/emit-command.ts +119 -0
- package/src/emit.ts +246 -0
- package/src/files.ts +11 -1
- package/src/help/allowance.txt +13 -0
- package/src/help/emit.txt +13 -0
- package/src/help/inbox.txt +12 -0
- package/src/help/init.txt +12 -0
- package/src/help/logs.txt +13 -0
- package/src/help/reply.txt +18 -0
- package/src/help/run.txt +44 -0
- package/src/help/runs.txt +11 -0
- package/src/help/test.txt +9 -0
- package/src/help/tick.txt +12 -0
- package/src/here.ts +9 -3
- package/src/hold-run-lock.fixture.ts +8 -0
- package/src/holder-command.ts +279 -0
- package/src/inbox-command.ts +715 -0
- package/src/init-command.ts +29 -11
- package/src/init-template/AGENTS.md.template +4 -4
- package/src/init-template/hello/workflow.test.ts.template +2 -2
- package/src/init-template/hello/workflow.ts.template +16 -20
- package/src/keep-alive.ts +40 -0
- package/src/logs-command.ts +106 -35
- package/src/machine.ts +12 -1
- package/src/mailbox.ts +990 -0
- package/src/message-operation.ts +26 -3
- package/src/messaging/limits.ts +1 -1
- package/src/messaging/messaging.ts +478 -85
- package/src/messaging/routes.ts +11 -4
- package/src/messaging/text.ts +25 -22
- package/src/operation-liveness.ts +55 -9
- package/src/operator-cli.ts +290 -119
- package/src/operator-runtime.ts +8 -8
- package/src/progress-view.ts +49 -7
- package/src/projects.ts +57 -0
- package/src/result-slots.ts +43 -19
- package/src/result-validation.ts +18 -27
- package/src/run-command.ts +29 -66
- package/src/run-commands.ts +13 -2
- package/src/run-lock.ts +123 -0
- package/src/run-prepare.ts +94 -22
- package/src/run-state.ts +125 -6
- package/src/run-usage.ts +2 -2
- package/src/run-view.ts +0 -9
- package/src/runs-command.ts +43 -25
- package/src/runs.ts +142 -23
- package/src/sandboxes.ts +11 -4
- package/src/simple-input.ts +268 -0
- package/src/socket-exchange.ts +26 -0
- package/src/sources.ts +70 -0
- package/src/stage-ledger.ts +89 -14
- package/src/stage-plan.ts +23 -0
- package/src/stop-crashed.ts +39 -0
- package/src/test-command.ts +34 -11
- package/src/testing.ts +72 -4
- package/src/tick-command.ts +76 -0
- package/src/tools.ts +645 -0
- package/src/wake.ts +264 -0
- package/src/workflow-loader.ts +32 -22
- package/src/workflow-runner.ts +1277 -97
- package/src/workflow-testing/coordinator.typecheck.ts +54 -0
- package/src/workflow-testing/coordinators.ts +137 -0
- package/src/workflow-testing/host.ts +67 -7
- package/src/workflow-testing/index.ts +488 -34
- package/src/workflow-testing/script.ts +74 -4
- package/src/workflow-testing/workflow-testing.typecheck.ts +19 -2
package/src/init-command.ts
CHANGED
|
@@ -1,21 +1,39 @@
|
|
|
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 { messageOf } from "./errors";
|
|
5
|
+
import help from "./help/init.txt" with { type: "text" };
|
|
6
|
+
import type { Out } from "./run-commands";
|
|
4
7
|
|
|
5
|
-
export const initUsage =
|
|
6
|
-
"usage: awf init [dir] [--no-install]",
|
|
7
|
-
"",
|
|
8
|
-
"Sets up the current directory, or dir, for writing workflows: hello/workflow.ts, a workflow",
|
|
9
|
-
"with one agent, and hello/workflow.test.ts, its test; package.json, tsconfig.json, .gitignore",
|
|
10
|
-
"and AGENTS.md. Then runs bun install, unless --no-install, for editor types: awf runs a",
|
|
11
|
-
"workflow against its own copies either way.",
|
|
12
|
-
"",
|
|
13
|
-
"Never overwrites. A file that exists is skipped, except that package.json gains the",
|
|
14
|
-
"devDependencies and scripts it lacks and .gitignore the lines it lacks.",
|
|
15
|
-
].join("\n");
|
|
8
|
+
export const initUsage = help.trimEnd();
|
|
16
9
|
|
|
17
10
|
export type InitCommand = { dir: string; install: boolean };
|
|
18
11
|
|
|
12
|
+
/** `awf init`: a folder set up for writing workflows. */
|
|
13
|
+
export async function initCommand(
|
|
14
|
+
argv: readonly string[],
|
|
15
|
+
cwd: string,
|
|
16
|
+
io: Out & { install?: (dir: string) => Promise<number> },
|
|
17
|
+
): Promise<number> {
|
|
18
|
+
let command: InitCommand | "help";
|
|
19
|
+
try {
|
|
20
|
+
command = parseInitCommand(argv, cwd);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
io.stderr(`awf: ${messageOf(error)}\n\n${initUsage}`);
|
|
23
|
+
return 2;
|
|
24
|
+
}
|
|
25
|
+
if (command === "help") {
|
|
26
|
+
io.stdout(initUsage);
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return await runInit(command, cwd, io, io.install);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
io.stderr(`awf: ${messageOf(error)}`);
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
19
37
|
export function parseInitCommand(argv: readonly string[], cwd: string): InitCommand | "help" {
|
|
20
38
|
let dir: string | undefined;
|
|
21
39
|
let install = true;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Workflows
|
|
2
2
|
|
|
3
|
-
This folder holds [agents.wf](https://
|
|
3
|
+
This folder holds [agents.wf](https://agents.wf) workflows: TypeScript files
|
|
4
4
|
that run coding agents, started with `awf`.
|
|
5
5
|
|
|
6
6
|
- A workflow is a folder with a `workflow.ts`, which default-exports
|
|
7
|
-
`
|
|
7
|
+
`defineWorkflow(…)` from `agentswf/workflow`, and a `workflow.test.ts` beside it.
|
|
8
8
|
`hello/` is the smallest one; copy it to start another.
|
|
9
|
-
- The API, with every option and outcome:
|
|
10
|
-
https://
|
|
9
|
+
- The API, with every option and outcome, as Markdown:
|
|
10
|
+
https://agents.wf/reference/workflow-api.md. Every other page is in https://agents.wf/llms.txt.
|
|
11
11
|
- `awf test` runs the tests with each agent replaced by scripted answers: no agent starts, and it
|
|
12
12
|
costs nothing. After a change, run it and `bun run check`, which typechecks.
|
|
13
13
|
- `awf run hello/workflow.ts` starts real agents, on the operator's accounts. Don't run a workflow
|
|
@@ -3,7 +3,7 @@ import { answer, reply, 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, null, {
|
|
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, null, { agents: { hello: reply.silent() } });
|
|
15
15
|
expect(run.value).toHaveProperty("unanswered");
|
|
16
16
|
expect(run.turnsOf("hello").map((turn) => turn.nudge)).toEqual([false, true]);
|
|
17
17
|
});
|
|
@@ -1,26 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineWorkflow } from "agentswf/workflow";
|
|
2
2
|
import Type from "typebox";
|
|
3
3
|
|
|
4
4
|
export const ANSWER = Type.Object({ answer: Type.Integer() }, { additionalProperties: false });
|
|
5
5
|
|
|
6
|
-
export default
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return isAnswered(outcome) ? outcome.value : { unanswered: outcome.reason };
|
|
22
|
-
},
|
|
6
|
+
export default defineWorkflow({
|
|
7
|
+
meta: { name: "hello", description: "Ask one agent a question with a known answer." },
|
|
8
|
+
async run(wf) {
|
|
9
|
+
// A headless agent needs neither Herdr nor a sandbox. Swap in any harness and model your
|
|
10
|
+
// account has: "claude" in a pane, the default placement, runs on a subscription.
|
|
11
|
+
const agent = await wf.agents.open({
|
|
12
|
+
key: "hello",
|
|
13
|
+
runtime: { harness: "codex", model: "gpt-6-luna", placement: "headless" },
|
|
14
|
+
});
|
|
15
|
+
const { outcome } = await agent.run<Type.Static<typeof ANSWER>>({
|
|
16
|
+
prompt: "What is 17 multiplied by 23? Work it out yourself; do not run any code.",
|
|
17
|
+
schema: ANSWER,
|
|
18
|
+
timeoutMs: 3 * 60_000,
|
|
19
|
+
});
|
|
20
|
+
return outcome.answered ? outcome.value : { unanswered: outcome.reason };
|
|
23
21
|
},
|
|
24
|
-
// Turns the command line into args: `awf run hello/workflow.ts -- ...`.
|
|
25
|
-
prepare: () => ({}),
|
|
26
22
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { EventAgentOpenSpec } from "@agentswf/contract/workflow";
|
|
3
|
+
|
|
4
|
+
export type KeepAlive = NonNullable<EventAgentOpenSpec["keepAlive"]>;
|
|
5
|
+
|
|
6
|
+
/** Claude's prompt cache lives an hour for the main conversation on a subscription. */
|
|
7
|
+
export const DEFAULT_WINDOW_MS = 60 * 60_000;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Set while an `onTimeout` handler runs: a turn begun in it opens no window, so a wrap-up can't keep
|
|
11
|
+
* its own agent live. A message turn opens one whatever the context: it is the peer's.
|
|
12
|
+
*/
|
|
13
|
+
const inHandler = new AsyncLocalStorage<boolean>();
|
|
14
|
+
|
|
15
|
+
export function inTimeoutHandler(): boolean {
|
|
16
|
+
return inHandler.getStore() === true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function asTimeoutHandler<T>(fn: () => T): T {
|
|
20
|
+
return inHandler.run(true, fn);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** `keepAlive` as an open gave it, checked: what a typed caller can't get wrong, a plain one can. */
|
|
24
|
+
export function checkedKeepAlive(key: string, given: unknown): KeepAlive {
|
|
25
|
+
if (typeof given !== "object" || given === null)
|
|
26
|
+
throw new Error(`agent ${key}: keepAlive is { timeoutMs?, onTimeout? }`);
|
|
27
|
+
const { timeoutMs, onTimeout } = given as Record<string, unknown>;
|
|
28
|
+
if (
|
|
29
|
+
timeoutMs !== undefined &&
|
|
30
|
+
(typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
31
|
+
)
|
|
32
|
+
throw new Error(`agent ${key}: keepAlive's timeoutMs is a positive number of milliseconds`);
|
|
33
|
+
if (onTimeout !== undefined && typeof onTimeout !== "function")
|
|
34
|
+
throw new Error(`agent ${key}: keepAlive's onTimeout is a function`);
|
|
35
|
+
// A copy: a later change to the caller's object would skip this check.
|
|
36
|
+
return {
|
|
37
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
38
|
+
...(onTimeout === undefined ? {} : { onTimeout: onTimeout as KeepAlive["onTimeout"] }),
|
|
39
|
+
} as KeepAlive;
|
|
40
|
+
}
|
package/src/logs-command.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
1
2
|
import type { LogEventJson, LogsJson, RouteEnds, RunLogLine } from "@agentswf/contract/records";
|
|
3
|
+
import type { JsonObject } from "@agentswf/contract/workflow";
|
|
4
|
+
import { duration } from "./duration";
|
|
2
5
|
import { messageOf } from "./errors";
|
|
6
|
+
import help from "./help/logs.txt" with { type: "text" };
|
|
3
7
|
import {
|
|
4
8
|
clockTime,
|
|
5
9
|
command,
|
|
@@ -28,24 +32,10 @@ import {
|
|
|
28
32
|
readRun,
|
|
29
33
|
timeOf,
|
|
30
34
|
} from "./run-read";
|
|
31
|
-
import { findRun, type RunStatus } from "./run-state";
|
|
32
|
-
import { describeActivity
|
|
35
|
+
import { findRun, findRunByRef, type RunStatus } from "./run-state";
|
|
36
|
+
import { describeActivity } from "./run-view";
|
|
33
37
|
|
|
34
|
-
export const logsUsage =
|
|
35
|
-
"usage: awf logs [<run>] [@<handle>] [--agent <key>] [--stage <name>] [--after <cursor>]",
|
|
36
|
-
" [--limit <n>] [--wait <seconds> | --follow] [--full] [--cwd <directory>]",
|
|
37
|
-
" [--run-root <directory>] [--json]",
|
|
38
|
-
"",
|
|
39
|
-
"What a run did and is doing: its events and what its agents did, read from their own session",
|
|
40
|
-
"files, merged by time. With no run, the newest here. Without --after, the last 40 events; with",
|
|
41
|
-
"it, what came after, oldest first. Every answer ends with the command that gets only what is",
|
|
42
|
-
"new. --wait: with nothing to show yet, wait for the first event, at most that many seconds, up",
|
|
43
|
-
"to 300; --follow prints each event as it comes until the run ends. @<handle> names one event,",
|
|
44
|
-
"and --full prints its text, with common credential shapes masked; --json carries it always.",
|
|
45
|
-
"",
|
|
46
|
-
"Exits 0 on any read that worked, 1 when there is no such run or the cursor or handle does not",
|
|
47
|
-
"fit it, 2 on a usage error, 3 when the run's files could not be read.",
|
|
48
|
-
].join("\n");
|
|
38
|
+
export const logsUsage = help.trimEnd();
|
|
49
39
|
|
|
50
40
|
const DEFAULT_LIMIT = 40;
|
|
51
41
|
/**
|
|
@@ -82,6 +72,7 @@ export async function logsCommand(
|
|
|
82
72
|
shellCwd: string,
|
|
83
73
|
out: Out,
|
|
84
74
|
clock: LogsClock = REAL_CLOCK,
|
|
75
|
+
home: string = homedir(),
|
|
85
76
|
): Promise<number> {
|
|
86
77
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
87
78
|
out.stdout(logsUsage);
|
|
@@ -101,7 +92,10 @@ export async function logsCommand(
|
|
|
101
92
|
return 2;
|
|
102
93
|
}
|
|
103
94
|
try {
|
|
104
|
-
const { status, alsoRunning } =
|
|
95
|
+
const { status, alsoRunning } =
|
|
96
|
+
options.ref === undefined
|
|
97
|
+
? await findRun(place.runRoot, undefined)
|
|
98
|
+
: { status: await findRunByRef(options.ref, place, home), alsoRunning: [] };
|
|
105
99
|
const again = shellLine(command(place, "awf", "logs", status.ref));
|
|
106
100
|
let after: Cursor | undefined;
|
|
107
101
|
if (options.after !== undefined) {
|
|
@@ -200,20 +194,31 @@ class Reader {
|
|
|
200
194
|
});
|
|
201
195
|
}
|
|
202
196
|
|
|
203
|
-
|
|
204
|
-
|
|
197
|
+
/** The run read again where it was found, which may be another project's. */
|
|
198
|
+
async refresh(): Promise<RunStatus> {
|
|
199
|
+
this.status = (await findRun(this.status.run.root, this.status.ref)).status;
|
|
205
200
|
return this.status;
|
|
206
201
|
}
|
|
207
202
|
}
|
|
208
203
|
|
|
209
204
|
type Read = Awaited<ReturnType<Reader["next"]>>;
|
|
210
205
|
|
|
211
|
-
/**
|
|
212
|
-
type Places = {
|
|
206
|
+
/** Where each operation ran, and each tool call's input, by its id, so its end shows it. */
|
|
207
|
+
type Places = {
|
|
208
|
+
stage: Map<string, string>;
|
|
209
|
+
agent: Map<string, string>;
|
|
210
|
+
calls: Map<string, { input: JsonObject; ended: boolean }>;
|
|
211
|
+
};
|
|
213
212
|
|
|
214
213
|
function operationPlaces(lines: readonly { line: RunLogLine }[]): Places {
|
|
215
|
-
const places: Places = { stage: new Map(), agent: new Map() };
|
|
214
|
+
const places: Places = { stage: new Map(), agent: new Map(), calls: new Map() };
|
|
216
215
|
for (const { line } of lines) {
|
|
216
|
+
if (line.kind === "tool-called")
|
|
217
|
+
places.calls.set(line.call, { input: line.input, ended: false });
|
|
218
|
+
if (line.kind === "tool-ended") {
|
|
219
|
+
const call = places.calls.get(line.call);
|
|
220
|
+
if (call) call.ended = true;
|
|
221
|
+
}
|
|
217
222
|
if (!("operationId" in line) || !line.operationId) continue;
|
|
218
223
|
if ("stage" in line && line.stage) places.stage.set(line.operationId, line.stage);
|
|
219
224
|
if ("agent" in line && line.agent) places.agent.set(line.operationId, line.agent);
|
|
@@ -257,10 +262,17 @@ function sidesOf(event: ReadEvent): readonly string[] {
|
|
|
257
262
|
}
|
|
258
263
|
}
|
|
259
264
|
|
|
260
|
-
function kept(read: Read, options: LogsOptions) {
|
|
265
|
+
function kept(read: Read, options: LogsOptions, json = false) {
|
|
261
266
|
const stages = operationPlaces(read.log.lines);
|
|
262
267
|
const events = read.events.filter(
|
|
263
268
|
(event) =>
|
|
269
|
+
// A call that has ended shows on its end's line, with its input; `--json` keeps both.
|
|
270
|
+
!(
|
|
271
|
+
!json &&
|
|
272
|
+
event.source !== "activity" &&
|
|
273
|
+
event.line.kind === "tool-called" &&
|
|
274
|
+
stages.calls.get(event.line.call)?.ended
|
|
275
|
+
) &&
|
|
264
276
|
(options.agent === undefined ||
|
|
265
277
|
agentOf(event, stages) === options.agent ||
|
|
266
278
|
sidesOf(event).includes(options.agent)) &&
|
|
@@ -290,7 +302,7 @@ async function page(
|
|
|
290
302
|
clock: LogsClock,
|
|
291
303
|
): Promise<number> {
|
|
292
304
|
let read = await reader.next();
|
|
293
|
-
let { events, stages } = kept(read, options);
|
|
305
|
+
let { events, stages } = kept(read, options, json);
|
|
294
306
|
const deadline = clock.now() + options.waitSeconds * 1000;
|
|
295
307
|
let waited = false;
|
|
296
308
|
// Without a cursor, a wait reads on from its first read: nothing before it is kept.
|
|
@@ -305,14 +317,14 @@ async function page(
|
|
|
305
317
|
waited = true;
|
|
306
318
|
if (!reader.from) from = read.cursor;
|
|
307
319
|
await clock.sleep(POLL_MS);
|
|
308
|
-
await reader.refresh(
|
|
320
|
+
await reader.refresh();
|
|
309
321
|
read = await reader.next(from);
|
|
310
|
-
({ events, stages } = kept(read, options));
|
|
322
|
+
({ events, stages } = kept(read, options, json));
|
|
311
323
|
if (events.length > 0) {
|
|
312
324
|
// A burst comes back as one page.
|
|
313
325
|
await clock.sleep(SETTLE_MS);
|
|
314
326
|
read = await reader.next(from);
|
|
315
|
-
({ events, stages } = kept(read, options));
|
|
327
|
+
({ events, stages } = kept(read, options, json));
|
|
316
328
|
}
|
|
317
329
|
}
|
|
318
330
|
const status = reader.status;
|
|
@@ -417,7 +429,7 @@ async function follow(
|
|
|
417
429
|
if (reader.status.state !== "running") break;
|
|
418
430
|
await clock.sleep(POLL_MS);
|
|
419
431
|
}
|
|
420
|
-
await reader.refresh(
|
|
432
|
+
await reader.refresh();
|
|
421
433
|
read = await reader.next(cursor);
|
|
422
434
|
}
|
|
423
435
|
out.stdout(trailer(reader.status, read, place, undefined, undefined, undefined));
|
|
@@ -532,11 +544,13 @@ function describeEvent(event: ReadEvent, stages: Places): string {
|
|
|
532
544
|
const agent = agentOf(event, stages);
|
|
533
545
|
const where = [stageOf(event, stages), agent && said(agent, 60)].filter(Boolean).join(" ");
|
|
534
546
|
const what =
|
|
535
|
-
event.source === "activity"
|
|
547
|
+
event.source === "activity"
|
|
548
|
+
? describeActivity(event.activity)
|
|
549
|
+
: describeLine(event.line, stages);
|
|
536
550
|
return `@${event.handle} ${clockTime(event.at)} ${where ? `${where} ` : ""}${what}`;
|
|
537
551
|
}
|
|
538
552
|
|
|
539
|
-
function describeLine(line: RunLogLine): string {
|
|
553
|
+
function describeLine(line: RunLogLine, places: Places): string {
|
|
540
554
|
const operation = (of: "turn" | "compact") => (of === "compact" ? "compaction" : "turn");
|
|
541
555
|
switch (line.kind) {
|
|
542
556
|
case "attempt-started":
|
|
@@ -576,15 +590,21 @@ function describeLine(line: RunLogLine): string {
|
|
|
576
590
|
case "decision-ended":
|
|
577
591
|
return `decision ${line.decision} ${line.outcome}${line.error ? ` ${said(line.error)}` : ""}`;
|
|
578
592
|
case "route-opened":
|
|
579
|
-
return `route ${routeEnds(line)} opened "${said(line.purpose)}" ${line.maxMessages} messages
|
|
593
|
+
return `route ${routeEnds(line)} opened "${said(line.purpose)}" ${line.maxMessages} messages${
|
|
594
|
+
line.timeoutMs === undefined ? "" : `, a question closes after ${duration(line.timeoutMs)}`
|
|
595
|
+
}`;
|
|
580
596
|
case "route-closed":
|
|
581
597
|
return `route ${routeEnds(line)} closed ${line.reason}`;
|
|
582
598
|
case "message-sent":
|
|
583
599
|
return `message ${line.message} ${said(line.from, 60)} → ${said(line.to, 60)} ${line.bytes} B${
|
|
584
|
-
line.
|
|
585
|
-
}${
|
|
600
|
+
line.via === undefined ? "" : ` via ${line.via}`
|
|
601
|
+
}${line.replyTo === undefined ? "" : `, reply to ${line.replyTo}`}${
|
|
586
602
|
line.awaitsReply
|
|
587
|
-
? `, reply owed ${said(line.from, 60)} awaits it
|
|
603
|
+
? `, reply owed ${said(line.from, 60)} awaits it${
|
|
604
|
+
line.awaitsReply.until
|
|
605
|
+
? ` until ${clockTime(Date.parse(line.awaitsReply.until))}`
|
|
606
|
+
: ""
|
|
607
|
+
}`
|
|
588
608
|
: ""
|
|
589
609
|
}`;
|
|
590
610
|
case "message-refused":
|
|
@@ -595,9 +615,60 @@ function describeLine(line: RunLogLine): string {
|
|
|
595
615
|
return `message ${line.message} undelivered ${line.reason}`;
|
|
596
616
|
case "request-ended":
|
|
597
617
|
return `request ${line.message} ${line.end}${line.reason ? ` ${said(line.reason)}` : ""}`;
|
|
618
|
+
case "participant-connected":
|
|
619
|
+
return `${said(line.participant, 60)} connected${line.to === "code" ? " to the code" : ""}`;
|
|
620
|
+
case "participant-post":
|
|
621
|
+
return `run → ${said(line.participant, 60)} ${said(line.text)}`;
|
|
622
|
+
// A call is one line, its end's: what it was given, how it ended, and what it said back.
|
|
623
|
+
// Shown alone only while its end isn't in the log: still running, or cut short by a crash.
|
|
624
|
+
case "tool-called":
|
|
625
|
+
return toolLine(
|
|
626
|
+
line.tool,
|
|
627
|
+
line.input,
|
|
628
|
+
places.calls.get(line.call)?.ended ? "called" : "no end yet",
|
|
629
|
+
);
|
|
630
|
+
case "tool-ended": {
|
|
631
|
+
const answered =
|
|
632
|
+
line.outcome !== "ok"
|
|
633
|
+
? line.error
|
|
634
|
+
: typeof line.result === "string"
|
|
635
|
+
? `→ ${line.result}`
|
|
636
|
+
: line.result === undefined
|
|
637
|
+
? undefined
|
|
638
|
+
: `→ ${JSON.stringify(line.result)}`;
|
|
639
|
+
return toolLine(
|
|
640
|
+
line.tool,
|
|
641
|
+
places.calls.get(line.call)?.input,
|
|
642
|
+
// Most calls take under a second; a duration says something only past one.
|
|
643
|
+
line.durationMs < 1000 ? line.outcome : `${line.outcome} ${duration(line.durationMs)}`,
|
|
644
|
+
answered,
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
case "tool-refused":
|
|
648
|
+
return [
|
|
649
|
+
`tool call ${said(line.tool, 60)}`,
|
|
650
|
+
"refused",
|
|
651
|
+
line.code,
|
|
652
|
+
line.error && said(line.error),
|
|
653
|
+
]
|
|
654
|
+
.filter(Boolean)
|
|
655
|
+
.join(" ");
|
|
598
656
|
}
|
|
599
657
|
}
|
|
600
658
|
|
|
659
|
+
/** `tool call close_item {"id":"a1"} ok → Closed a1.`, apart from the harness's own tools. */
|
|
660
|
+
function toolLine(
|
|
661
|
+
tool: string,
|
|
662
|
+
input: JsonObject | undefined,
|
|
663
|
+
how: string,
|
|
664
|
+
answered?: string,
|
|
665
|
+
): string {
|
|
666
|
+
const given = input && Object.keys(input).length > 0 ? ` ${said(JSON.stringify(input), 80)}` : "";
|
|
667
|
+
return [`tool call ${said(tool, 60)}${given}`, how, answered && said(answered)]
|
|
668
|
+
.filter(Boolean)
|
|
669
|
+
.join(" ");
|
|
670
|
+
}
|
|
671
|
+
|
|
601
672
|
function routeEnds(line: RouteEnds): string {
|
|
602
673
|
return line.between
|
|
603
674
|
? `${said(line.between[0], 60)} ↔ ${said(line.between[1], 60)}`
|
package/src/machine.ts
CHANGED
|
@@ -4,13 +4,15 @@ import type { RunRecord } from "@agentswf/contract/records";
|
|
|
4
4
|
/**
|
|
5
5
|
* What awf keeps per machine, under `~/.awf`, whatever a run's root: the marks of sessions runs
|
|
6
6
|
* drive, each sandbox's homes, kept outside the project, whose run root a provider denies, and the
|
|
7
|
-
* Herdr sessions it runs agents in: their config, and each run's mark on its workspace
|
|
7
|
+
* Herdr sessions it runs agents in: their config, and each run's mark on its workspace; and the
|
|
8
|
+
* sockets holders reach live runs' coordinators through.
|
|
8
9
|
*/
|
|
9
10
|
export function machinePaths(home: string): {
|
|
10
11
|
root: string;
|
|
11
12
|
callers: string;
|
|
12
13
|
sandboxes: string;
|
|
13
14
|
herdr: string;
|
|
15
|
+
coordinator: string;
|
|
14
16
|
} {
|
|
15
17
|
const root = join(home, ".awf");
|
|
16
18
|
return {
|
|
@@ -18,6 +20,7 @@ export function machinePaths(home: string): {
|
|
|
18
20
|
callers: join(root, "callers"),
|
|
19
21
|
sandboxes: join(root, "sandboxes"),
|
|
20
22
|
herdr: join(root, "herdr"),
|
|
23
|
+
coordinator: join(root, "coordinator"),
|
|
21
24
|
};
|
|
22
25
|
}
|
|
23
26
|
|
|
@@ -25,3 +28,11 @@ export function machinePaths(home: string): {
|
|
|
25
28
|
export function sandboxesDirOf(home: string, run: Pick<RunRecord, "workflow" | "id">): string {
|
|
26
29
|
return join(machinePaths(home).sandboxes, run.workflow, run.id);
|
|
27
30
|
}
|
|
31
|
+
|
|
32
|
+
/** Where live runs' coordinator sockets are: `AWF_COORDINATOR_DIR`, or `~/.awf/coordinator`. */
|
|
33
|
+
export function coordinatorDir(
|
|
34
|
+
home: string,
|
|
35
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
36
|
+
): string {
|
|
37
|
+
return environment.AWF_COORDINATOR_DIR || machinePaths(home).coordinator;
|
|
38
|
+
}
|