@agentswf/engine 0.1.0-beta.2 → 0.1.0-beta.3
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 -5
- package/src/allowance-command.ts +29 -7
- 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 +72 -21
- 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 +130 -0
- package/src/emit.ts +246 -0
- package/src/files.ts +11 -1
- package/src/here.ts +8 -2
- package/src/hold-run-lock.fixture.ts +8 -0
- package/src/holder-command.ts +297 -0
- package/src/inbox-command.ts +727 -0
- package/src/init-command.ts +27 -0
- package/src/init-template/AGENTS.md.template +1 -1
- 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 +104 -20
- package/src/machine.ts +12 -1
- package/src/mailbox.ts +977 -0
- package/src/message-operation.ts +26 -3
- package/src/messaging/messaging.ts +458 -65
- package/src/messaging/routes.ts +10 -3
- package/src/messaging/text.ts +8 -5
- package/src/operation-liveness.ts +54 -8
- package/src/operator-cli.ts +290 -119
- package/src/operator-runtime.ts +5 -2
- 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 +36 -32
- 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 +1 -1
- package/src/run-view.ts +0 -9
- package/src/runs-command.ts +46 -17
- package/src/runs.ts +142 -23
- package/src/sandboxes.ts +8 -2
- package/src/simple-input.ts +258 -0
- package/src/socket-exchange.ts +26 -0
- package/src/sources.ts +70 -0
- package/src/stage-ledger.ts +88 -13
- package/src/stage-plan.ts +23 -0
- package/src/stop-crashed.ts +39 -0
- package/src/test-command.ts +32 -0
- package/src/testing.ts +71 -3
- package/src/tick-command.ts +87 -0
- package/src/tools.ts +636 -0
- package/src/wake.ts +264 -0
- package/src/workflow-loader.ts +32 -22
- package/src/workflow-runner.ts +1210 -77
- package/src/workflow-testing/coordinator.typecheck.ts +55 -0
- package/src/workflow-testing/coordinators.ts +137 -0
- package/src/workflow-testing/host.ts +61 -1
- package/src/workflow-testing/index.ts +488 -33
- package/src/workflow-testing/script.ts +72 -2
- package/src/workflow-testing/workflow-testing.typecheck.ts +19 -2
package/src/init-command.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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 type { Out } from "./run-commands";
|
|
4
6
|
|
|
5
7
|
export const initUsage = [
|
|
6
8
|
"usage: awf init [dir] [--no-install]",
|
|
@@ -16,6 +18,31 @@ export const initUsage = [
|
|
|
16
18
|
|
|
17
19
|
export type InitCommand = { dir: string; install: boolean };
|
|
18
20
|
|
|
21
|
+
/** `awf init`: a folder set up for writing workflows. */
|
|
22
|
+
export async function initCommand(
|
|
23
|
+
argv: readonly string[],
|
|
24
|
+
cwd: string,
|
|
25
|
+
io: Out & { install?: (dir: string) => Promise<number> },
|
|
26
|
+
): Promise<number> {
|
|
27
|
+
let command: InitCommand | "help";
|
|
28
|
+
try {
|
|
29
|
+
command = parseInitCommand(argv, cwd);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
io.stderr(`awf: ${messageOf(error)}\n\n${initUsage}`);
|
|
32
|
+
return 2;
|
|
33
|
+
}
|
|
34
|
+
if (command === "help") {
|
|
35
|
+
io.stdout(initUsage);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return await runInit(command, cwd, io, io.install);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
io.stderr(`awf: ${messageOf(error)}`);
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
19
46
|
export function parseInitCommand(argv: readonly string[], cwd: string): InitCommand | "help" {
|
|
20
47
|
let dir: string | undefined;
|
|
21
48
|
let install = true;
|
|
@@ -4,7 +4,7 @@ This folder holds [agents.wf](https://github.com/romanlv/agentswf) workflows: Ty
|
|
|
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
9
|
- The API, with every option and outcome:
|
|
10
10
|
https://github.com/romanlv/agentswf/blob/main/docs/workflow-api.md
|
|
@@ -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, isAnswered } 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(workflow) {
|
|
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 workflow.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 isAnswered(outcome) ? 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,4 +1,7 @@
|
|
|
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";
|
|
3
6
|
import {
|
|
4
7
|
clockTime,
|
|
@@ -28,8 +31,8 @@ import {
|
|
|
28
31
|
readRun,
|
|
29
32
|
timeOf,
|
|
30
33
|
} from "./run-read";
|
|
31
|
-
import { findRun, type RunStatus } from "./run-state";
|
|
32
|
-
import { describeActivity
|
|
34
|
+
import { findRun, findRunByRef, type RunStatus } from "./run-state";
|
|
35
|
+
import { describeActivity } from "./run-view";
|
|
33
36
|
|
|
34
37
|
export const logsUsage = [
|
|
35
38
|
"usage: awf logs [<run>] [@<handle>] [--agent <key>] [--stage <name>] [--after <cursor>]",
|
|
@@ -82,6 +85,7 @@ export async function logsCommand(
|
|
|
82
85
|
shellCwd: string,
|
|
83
86
|
out: Out,
|
|
84
87
|
clock: LogsClock = REAL_CLOCK,
|
|
88
|
+
home: string = homedir(),
|
|
85
89
|
): Promise<number> {
|
|
86
90
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
87
91
|
out.stdout(logsUsage);
|
|
@@ -101,7 +105,10 @@ export async function logsCommand(
|
|
|
101
105
|
return 2;
|
|
102
106
|
}
|
|
103
107
|
try {
|
|
104
|
-
const { status, alsoRunning } =
|
|
108
|
+
const { status, alsoRunning } =
|
|
109
|
+
options.ref === undefined
|
|
110
|
+
? await findRun(place.runRoot, undefined)
|
|
111
|
+
: { status: await findRunByRef(options.ref, place, home), alsoRunning: [] };
|
|
105
112
|
const again = shellLine(command(place, "awf", "logs", status.ref));
|
|
106
113
|
let after: Cursor | undefined;
|
|
107
114
|
if (options.after !== undefined) {
|
|
@@ -200,20 +207,31 @@ class Reader {
|
|
|
200
207
|
});
|
|
201
208
|
}
|
|
202
209
|
|
|
203
|
-
|
|
204
|
-
|
|
210
|
+
/** The run read again where it was found, which may be another project's. */
|
|
211
|
+
async refresh(): Promise<RunStatus> {
|
|
212
|
+
this.status = (await findRun(this.status.run.root, this.status.ref)).status;
|
|
205
213
|
return this.status;
|
|
206
214
|
}
|
|
207
215
|
}
|
|
208
216
|
|
|
209
217
|
type Read = Awaited<ReturnType<Reader["next"]>>;
|
|
210
218
|
|
|
211
|
-
/**
|
|
212
|
-
type Places = {
|
|
219
|
+
/** Where each operation ran, and each tool call's input, by its id, so its end shows it. */
|
|
220
|
+
type Places = {
|
|
221
|
+
stage: Map<string, string>;
|
|
222
|
+
agent: Map<string, string>;
|
|
223
|
+
calls: Map<string, { input: JsonObject; ended: boolean }>;
|
|
224
|
+
};
|
|
213
225
|
|
|
214
226
|
function operationPlaces(lines: readonly { line: RunLogLine }[]): Places {
|
|
215
|
-
const places: Places = { stage: new Map(), agent: new Map() };
|
|
227
|
+
const places: Places = { stage: new Map(), agent: new Map(), calls: new Map() };
|
|
216
228
|
for (const { line } of lines) {
|
|
229
|
+
if (line.kind === "tool-called")
|
|
230
|
+
places.calls.set(line.call, { input: line.input, ended: false });
|
|
231
|
+
if (line.kind === "tool-ended") {
|
|
232
|
+
const call = places.calls.get(line.call);
|
|
233
|
+
if (call) call.ended = true;
|
|
234
|
+
}
|
|
217
235
|
if (!("operationId" in line) || !line.operationId) continue;
|
|
218
236
|
if ("stage" in line && line.stage) places.stage.set(line.operationId, line.stage);
|
|
219
237
|
if ("agent" in line && line.agent) places.agent.set(line.operationId, line.agent);
|
|
@@ -257,10 +275,17 @@ function sidesOf(event: ReadEvent): readonly string[] {
|
|
|
257
275
|
}
|
|
258
276
|
}
|
|
259
277
|
|
|
260
|
-
function kept(read: Read, options: LogsOptions) {
|
|
278
|
+
function kept(read: Read, options: LogsOptions, json = false) {
|
|
261
279
|
const stages = operationPlaces(read.log.lines);
|
|
262
280
|
const events = read.events.filter(
|
|
263
281
|
(event) =>
|
|
282
|
+
// A call that has ended shows on its end's line, with its input; `--json` keeps both.
|
|
283
|
+
!(
|
|
284
|
+
!json &&
|
|
285
|
+
event.source !== "activity" &&
|
|
286
|
+
event.line.kind === "tool-called" &&
|
|
287
|
+
stages.calls.get(event.line.call)?.ended
|
|
288
|
+
) &&
|
|
264
289
|
(options.agent === undefined ||
|
|
265
290
|
agentOf(event, stages) === options.agent ||
|
|
266
291
|
sidesOf(event).includes(options.agent)) &&
|
|
@@ -290,7 +315,7 @@ async function page(
|
|
|
290
315
|
clock: LogsClock,
|
|
291
316
|
): Promise<number> {
|
|
292
317
|
let read = await reader.next();
|
|
293
|
-
let { events, stages } = kept(read, options);
|
|
318
|
+
let { events, stages } = kept(read, options, json);
|
|
294
319
|
const deadline = clock.now() + options.waitSeconds * 1000;
|
|
295
320
|
let waited = false;
|
|
296
321
|
// Without a cursor, a wait reads on from its first read: nothing before it is kept.
|
|
@@ -305,14 +330,14 @@ async function page(
|
|
|
305
330
|
waited = true;
|
|
306
331
|
if (!reader.from) from = read.cursor;
|
|
307
332
|
await clock.sleep(POLL_MS);
|
|
308
|
-
await reader.refresh(
|
|
333
|
+
await reader.refresh();
|
|
309
334
|
read = await reader.next(from);
|
|
310
|
-
({ events, stages } = kept(read, options));
|
|
335
|
+
({ events, stages } = kept(read, options, json));
|
|
311
336
|
if (events.length > 0) {
|
|
312
337
|
// A burst comes back as one page.
|
|
313
338
|
await clock.sleep(SETTLE_MS);
|
|
314
339
|
read = await reader.next(from);
|
|
315
|
-
({ events, stages } = kept(read, options));
|
|
340
|
+
({ events, stages } = kept(read, options, json));
|
|
316
341
|
}
|
|
317
342
|
}
|
|
318
343
|
const status = reader.status;
|
|
@@ -417,7 +442,7 @@ async function follow(
|
|
|
417
442
|
if (reader.status.state !== "running") break;
|
|
418
443
|
await clock.sleep(POLL_MS);
|
|
419
444
|
}
|
|
420
|
-
await reader.refresh(
|
|
445
|
+
await reader.refresh();
|
|
421
446
|
read = await reader.next(cursor);
|
|
422
447
|
}
|
|
423
448
|
out.stdout(trailer(reader.status, read, place, undefined, undefined, undefined));
|
|
@@ -532,11 +557,13 @@ function describeEvent(event: ReadEvent, stages: Places): string {
|
|
|
532
557
|
const agent = agentOf(event, stages);
|
|
533
558
|
const where = [stageOf(event, stages), agent && said(agent, 60)].filter(Boolean).join(" ");
|
|
534
559
|
const what =
|
|
535
|
-
event.source === "activity"
|
|
560
|
+
event.source === "activity"
|
|
561
|
+
? describeActivity(event.activity)
|
|
562
|
+
: describeLine(event.line, stages);
|
|
536
563
|
return `@${event.handle} ${clockTime(event.at)} ${where ? `${where} ` : ""}${what}`;
|
|
537
564
|
}
|
|
538
565
|
|
|
539
|
-
function describeLine(line: RunLogLine): string {
|
|
566
|
+
function describeLine(line: RunLogLine, places: Places): string {
|
|
540
567
|
const operation = (of: "turn" | "compact") => (of === "compact" ? "compaction" : "turn");
|
|
541
568
|
switch (line.kind) {
|
|
542
569
|
case "attempt-started":
|
|
@@ -576,15 +603,21 @@ function describeLine(line: RunLogLine): string {
|
|
|
576
603
|
case "decision-ended":
|
|
577
604
|
return `decision ${line.decision} ${line.outcome}${line.error ? ` ${said(line.error)}` : ""}`;
|
|
578
605
|
case "route-opened":
|
|
579
|
-
return `route ${routeEnds(line)} opened "${said(line.purpose)}" ${line.maxMessages} messages
|
|
606
|
+
return `route ${routeEnds(line)} opened "${said(line.purpose)}" ${line.maxMessages} messages${
|
|
607
|
+
line.timeoutMs === undefined ? "" : `, a question closes after ${duration(line.timeoutMs)}`
|
|
608
|
+
}`;
|
|
580
609
|
case "route-closed":
|
|
581
610
|
return `route ${routeEnds(line)} closed ${line.reason}`;
|
|
582
611
|
case "message-sent":
|
|
583
612
|
return `message ${line.message} ${said(line.from, 60)} → ${said(line.to, 60)} ${line.bytes} B${
|
|
584
|
-
line.
|
|
585
|
-
}${
|
|
613
|
+
line.via === undefined ? "" : ` via ${line.via}`
|
|
614
|
+
}${line.replyTo === undefined ? "" : `, reply to ${line.replyTo}`}${
|
|
586
615
|
line.awaitsReply
|
|
587
|
-
? `, reply owed ${said(line.from, 60)} awaits it
|
|
616
|
+
? `, reply owed ${said(line.from, 60)} awaits it${
|
|
617
|
+
line.awaitsReply.until
|
|
618
|
+
? ` until ${clockTime(Date.parse(line.awaitsReply.until))}`
|
|
619
|
+
: ""
|
|
620
|
+
}`
|
|
588
621
|
: ""
|
|
589
622
|
}`;
|
|
590
623
|
case "message-refused":
|
|
@@ -595,9 +628,60 @@ function describeLine(line: RunLogLine): string {
|
|
|
595
628
|
return `message ${line.message} undelivered ${line.reason}`;
|
|
596
629
|
case "request-ended":
|
|
597
630
|
return `request ${line.message} ${line.end}${line.reason ? ` ${said(line.reason)}` : ""}`;
|
|
631
|
+
case "participant-connected":
|
|
632
|
+
return `${said(line.participant, 60)} connected${line.to === "code" ? " to the code" : ""}`;
|
|
633
|
+
case "participant-post":
|
|
634
|
+
return `run → ${said(line.participant, 60)} ${said(line.text)}`;
|
|
635
|
+
// A call is one line, its end's: what it was given, how it ended, and what it said back.
|
|
636
|
+
// Shown alone only while its end isn't in the log: still running, or cut short by a crash.
|
|
637
|
+
case "tool-called":
|
|
638
|
+
return toolLine(
|
|
639
|
+
line.tool,
|
|
640
|
+
line.input,
|
|
641
|
+
places.calls.get(line.call)?.ended ? "called" : "no end yet",
|
|
642
|
+
);
|
|
643
|
+
case "tool-ended": {
|
|
644
|
+
const answered =
|
|
645
|
+
line.outcome !== "ok"
|
|
646
|
+
? line.error
|
|
647
|
+
: typeof line.result === "string"
|
|
648
|
+
? `→ ${line.result}`
|
|
649
|
+
: line.result === undefined
|
|
650
|
+
? undefined
|
|
651
|
+
: `→ ${JSON.stringify(line.result)}`;
|
|
652
|
+
return toolLine(
|
|
653
|
+
line.tool,
|
|
654
|
+
places.calls.get(line.call)?.input,
|
|
655
|
+
// Most calls take under a second; a duration says something only past one.
|
|
656
|
+
line.durationMs < 1000 ? line.outcome : `${line.outcome} ${duration(line.durationMs)}`,
|
|
657
|
+
answered,
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
case "tool-refused":
|
|
661
|
+
return [
|
|
662
|
+
`tool call ${said(line.tool, 60)}`,
|
|
663
|
+
"refused",
|
|
664
|
+
line.code,
|
|
665
|
+
line.error && said(line.error),
|
|
666
|
+
]
|
|
667
|
+
.filter(Boolean)
|
|
668
|
+
.join(" ");
|
|
598
669
|
}
|
|
599
670
|
}
|
|
600
671
|
|
|
672
|
+
/** `tool call close_item {"id":"a1"} ok → Closed a1.`, apart from the harness's own tools. */
|
|
673
|
+
function toolLine(
|
|
674
|
+
tool: string,
|
|
675
|
+
input: JsonObject | undefined,
|
|
676
|
+
how: string,
|
|
677
|
+
answered?: string,
|
|
678
|
+
): string {
|
|
679
|
+
const given = input && Object.keys(input).length > 0 ? ` ${said(JSON.stringify(input), 80)}` : "";
|
|
680
|
+
return [`tool call ${said(tool, 60)}${given}`, how, answered && said(answered)]
|
|
681
|
+
.filter(Boolean)
|
|
682
|
+
.join(" ");
|
|
683
|
+
}
|
|
684
|
+
|
|
601
685
|
function routeEnds(line: RouteEnds): string {
|
|
602
686
|
return line.between
|
|
603
687
|
? `${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
|
+
}
|