@agentswf/engine 0.1.0-beta.3 → 0.1.0-beta.5
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 +611 -0
- package/src/agent-cli/client.ts +115 -0
- package/src/agent-launcher.ts +15 -25
- package/src/allowance-command.ts +3 -15
- package/src/attempt-ending.ts +2 -4
- package/src/attempt-output.ts +3 -2
- package/src/attempt-view.ts +1 -1
- package/src/control-plane.ts +38 -6
- package/src/coordinator-socket.ts +15 -6
- package/src/deadlines.ts +69 -2
- package/src/decisions/directory.ts +9 -11
- package/src/deprecations.ts +177 -0
- package/src/duration.ts +50 -20
- package/src/emit-command.ts +57 -54
- package/src/emit.ts +124 -15
- package/src/help/allowance.txt +13 -0
- package/src/help/emit.txt +21 -0
- package/src/help/inbox.txt +12 -0
- package/src/help/init.txt +12 -0
- package/src/help/logs.txt +14 -0
- package/src/help/reply.txt +18 -0
- package/src/help/run.txt +58 -0
- package/src/help/runs.txt +11 -0
- package/src/help/test.txt +9 -0
- package/src/help/tick.txt +12 -0
- package/src/herdr-run-session.ts +2 -2
- package/src/here.ts +1 -1
- package/src/holder-command.ts +38 -51
- package/src/inbox-command.ts +9 -24
- package/src/init-command.ts +2 -11
- package/src/init-template/AGENTS.md.template +3 -3
- package/src/init-template/hello/workflow.test.ts.template +3 -3
- package/src/init-template/hello/workflow.ts.template +6 -6
- package/src/keep-alive.ts +28 -13
- package/src/logs-command.ts +14 -27
- package/src/mailbox.ts +16 -21
- package/src/messaging/limits.ts +5 -5
- package/src/messaging/messaging.ts +48 -43
- package/src/messaging/routes.ts +2 -2
- package/src/messaging/text.ts +24 -22
- package/src/operation-liveness.ts +1 -1
- package/src/operator-cli.ts +23 -9
- package/src/operator-runtime.ts +9 -12
- package/src/pane-layout.ts +125 -32
- package/src/progress-view.ts +8 -8
- package/src/result-validation.ts +1 -1
- package/src/run-command.ts +27 -47
- package/src/run-commands.ts +1 -1
- package/src/run-prepare.ts +46 -8
- package/src/run-usage.ts +2 -2
- package/src/run-view.ts +9 -4
- package/src/runs-command.ts +14 -23
- package/src/runs.ts +44 -8
- package/src/sandboxes.ts +3 -2
- package/src/simple-input.ts +13 -3
- package/src/sources.ts +42 -6
- package/src/stage-ledger.ts +43 -13
- package/src/stage-plan.ts +37 -1
- package/src/test-command.ts +9 -14
- package/src/testing.ts +8 -9
- package/src/tick-command.ts +2 -13
- package/src/tools.ts +62 -25
- package/src/workflow-loader.ts +5 -2
- package/src/workflow-runner.ts +636 -271
- package/src/workflow-testing/coordinator.typecheck.ts +12 -13
- package/src/workflow-testing/coordinators.ts +2 -2
- package/src/workflow-testing/host.ts +115 -46
- package/src/workflow-testing/index.ts +45 -837
- package/src/workflow-testing/run-test.ts +1213 -0
- package/src/workflow-testing/script.ts +106 -45
- package/src/workflow-testing/workflow-testing.typecheck.ts +13 -10
package/src/logs-command.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import type { LogEventJson, LogsJson, RouteEnds, RunLogLine } from "@agentswf/contract/records";
|
|
3
3
|
import type { JsonObject } from "@agentswf/contract/workflow";
|
|
4
|
-
import {
|
|
4
|
+
import { formatDuration, parseWait } from "./duration";
|
|
5
5
|
import { messageOf } from "./errors";
|
|
6
|
+
import help from "./help/logs.txt" with { type: "text" };
|
|
6
7
|
import {
|
|
7
8
|
clockTime,
|
|
8
9
|
command,
|
|
@@ -34,21 +35,7 @@ import {
|
|
|
34
35
|
import { findRun, findRunByRef, type RunStatus } from "./run-state";
|
|
35
36
|
import { describeActivity } from "./run-view";
|
|
36
37
|
|
|
37
|
-
export const logsUsage =
|
|
38
|
-
"usage: awf logs [<run>] [@<handle>] [--agent <key>] [--stage <name>] [--after <cursor>]",
|
|
39
|
-
" [--limit <n>] [--wait <seconds> | --follow] [--full] [--cwd <directory>]",
|
|
40
|
-
" [--run-root <directory>] [--json]",
|
|
41
|
-
"",
|
|
42
|
-
"What a run did and is doing: its events and what its agents did, read from their own session",
|
|
43
|
-
"files, merged by time. With no run, the newest here. Without --after, the last 40 events; with",
|
|
44
|
-
"it, what came after, oldest first. Every answer ends with the command that gets only what is",
|
|
45
|
-
"new. --wait: with nothing to show yet, wait for the first event, at most that many seconds, up",
|
|
46
|
-
"to 300; --follow prints each event as it comes until the run ends. @<handle> names one event,",
|
|
47
|
-
"and --full prints its text, with common credential shapes masked; --json carries it always.",
|
|
48
|
-
"",
|
|
49
|
-
"Exits 0 on any read that worked, 1 when there is no such run or the cursor or handle does not",
|
|
50
|
-
"fit it, 2 on a usage error, 3 when the run's files could not be read.",
|
|
51
|
-
].join("\n");
|
|
38
|
+
export const logsUsage = help.trimEnd();
|
|
52
39
|
|
|
53
40
|
const DEFAULT_LIMIT = 40;
|
|
54
41
|
/**
|
|
@@ -98,7 +85,7 @@ export async function logsCommand(
|
|
|
98
85
|
const parsed = parsePlace(argv, shellCwd);
|
|
99
86
|
place = parsed.place;
|
|
100
87
|
json = parsed.json;
|
|
101
|
-
options = parseLogs(parsed.rest);
|
|
88
|
+
options = parseLogs(parsed.rest, (line) => out.stderr(line));
|
|
102
89
|
if (options.follow && json) throw new Error("--follow is for reading; --json takes --wait");
|
|
103
90
|
} catch (error) {
|
|
104
91
|
out.stderr(`awf: ${messageOf(error)}\n\n${logsUsage}`);
|
|
@@ -135,7 +122,7 @@ export async function logsCommand(
|
|
|
135
122
|
}
|
|
136
123
|
}
|
|
137
124
|
|
|
138
|
-
function parseLogs(rest: readonly string[]): LogsOptions {
|
|
125
|
+
function parseLogs(rest: readonly string[], deprecated: (line: string) => void): LogsOptions {
|
|
139
126
|
const options: LogsOptions = {
|
|
140
127
|
limit: DEFAULT_LIMIT,
|
|
141
128
|
waitSeconds: 0,
|
|
@@ -160,11 +147,7 @@ function parseLogs(rest: readonly string[]): LogsOptions {
|
|
|
160
147
|
throw new Error("--limit needs a whole number");
|
|
161
148
|
options.limit = limit;
|
|
162
149
|
} else if (arg === "--wait") {
|
|
163
|
-
|
|
164
|
-
if (!Number.isFinite(seconds) || seconds < 0 || seconds > MAX_WAIT_SECONDS) {
|
|
165
|
-
throw new Error(`--wait needs seconds from 0 to ${MAX_WAIT_SECONDS}`);
|
|
166
|
-
}
|
|
167
|
-
options.waitSeconds = seconds;
|
|
150
|
+
options.waitSeconds = parseWait(value(), MAX_WAIT_SECONDS, deprecated);
|
|
168
151
|
waitGiven = true;
|
|
169
152
|
} else if (arg === "--follow") options.follow = true;
|
|
170
153
|
else if (arg === "--full") options.full = true;
|
|
@@ -524,7 +507,7 @@ function trailer(
|
|
|
524
507
|
return `run interrupted${go ? ` · continue: ${go}` : ""}`;
|
|
525
508
|
}
|
|
526
509
|
const { ended, started } = span(status, Date.now());
|
|
527
|
-
const took = ended === undefined ? "" : ` in ${
|
|
510
|
+
const took = ended === undefined ? "" : ` in ${formatDuration(ended - started)}`;
|
|
528
511
|
return `run ended: ${status.state}${took} · result: ${shellLine(command(place, "awf", "runs", status.ref))}`;
|
|
529
512
|
}
|
|
530
513
|
|
|
@@ -582,7 +565,7 @@ function describeLine(line: RunLogLine, places: Places): string {
|
|
|
582
565
|
case "compact": {
|
|
583
566
|
const took =
|
|
584
567
|
line.deliveredAt && line.settledAt
|
|
585
|
-
? ` ${
|
|
568
|
+
? ` ${formatDuration(Date.parse(line.settledAt) - Date.parse(line.deliveredAt))}`
|
|
586
569
|
: "";
|
|
587
570
|
return `${operation(line.kind)} ended ${line.outcome}${took}`;
|
|
588
571
|
}
|
|
@@ -604,7 +587,9 @@ function describeLine(line: RunLogLine, places: Places): string {
|
|
|
604
587
|
return `decision ${line.decision} ${line.outcome}${line.error ? ` ${said(line.error)}` : ""}`;
|
|
605
588
|
case "route-opened":
|
|
606
589
|
return `route ${routeEnds(line)} opened "${said(line.purpose)}" ${line.maxMessages} messages${
|
|
607
|
-
line.timeoutMs === undefined
|
|
590
|
+
line.timeoutMs === undefined
|
|
591
|
+
? ""
|
|
592
|
+
: `, a question closes after ${formatDuration(line.timeoutMs)}`
|
|
608
593
|
}`;
|
|
609
594
|
case "route-closed":
|
|
610
595
|
return `route ${routeEnds(line)} closed ${line.reason}`;
|
|
@@ -653,7 +638,9 @@ function describeLine(line: RunLogLine, places: Places): string {
|
|
|
653
638
|
line.tool,
|
|
654
639
|
places.calls.get(line.call)?.input,
|
|
655
640
|
// Most calls take under a second; a duration says something only past one.
|
|
656
|
-
line.durationMs < 1000
|
|
641
|
+
line.durationMs < 1000
|
|
642
|
+
? line.outcome
|
|
643
|
+
: `${line.outcome} ${formatDuration(line.durationMs)}`,
|
|
657
644
|
answered,
|
|
658
645
|
);
|
|
659
646
|
}
|
package/src/mailbox.ts
CHANGED
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
AWF_ID_PREFIX,
|
|
12
12
|
type JsonValue,
|
|
13
13
|
type OperationRecord,
|
|
14
|
-
type ReceiveOptions,
|
|
15
14
|
type TurnOutcome,
|
|
16
15
|
type WorkflowEvent,
|
|
17
16
|
} from "@agentswf/contract/workflow";
|
|
@@ -26,7 +25,19 @@ export function mailboxFile(runDir: string): string {
|
|
|
26
25
|
/** The run's mailbox in the order written, less a line a crash tore and kinds this awf doesn't know. */
|
|
27
26
|
export async function readMailbox(runDir: string): Promise<MailboxRecord[]> {
|
|
28
27
|
const lines = await readLines<MailboxRecord>(mailboxFile(runDir));
|
|
29
|
-
return lines
|
|
28
|
+
return lines
|
|
29
|
+
.filter((line) => line.version === MAILBOX_RECORD_VERSION && KINDS.has(line.kind))
|
|
30
|
+
.map(withAnswered);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A turn's end written before 0.1.0-beta.4 has no `answered`, which a workflow would read as false. */
|
|
34
|
+
function withAnswered(line: MailboxRecord): MailboxRecord {
|
|
35
|
+
if (line.kind !== "event" || line.name !== "agent:ended") return line;
|
|
36
|
+
const end = line.data as { outcome?: { kind: string; answered?: boolean } };
|
|
37
|
+
if (end?.outcome === undefined || end.outcome.answered !== undefined) return line;
|
|
38
|
+
const answered = end.outcome.kind === "answered";
|
|
39
|
+
const data = { ...end, outcome: { ...end.outcome, answered } } as unknown as JsonValue;
|
|
40
|
+
return { ...line, data };
|
|
30
41
|
}
|
|
31
42
|
|
|
32
43
|
const KINDS = new Set<string>([
|
|
@@ -137,6 +148,7 @@ export async function recoverTurns(
|
|
|
137
148
|
turn: turn.turn,
|
|
138
149
|
meta: turn.meta,
|
|
139
150
|
outcome: {
|
|
151
|
+
answered: false,
|
|
140
152
|
kind: "failed",
|
|
141
153
|
reason: `attempt ${turn.attempt} ended before this turn did; its work, if any, is in the agent's session`,
|
|
142
154
|
retryable: true,
|
|
@@ -614,12 +626,11 @@ export class RunMailbox {
|
|
|
614
626
|
* nothing.
|
|
615
627
|
*/
|
|
616
628
|
async receive(
|
|
617
|
-
|
|
629
|
+
label: string | undefined,
|
|
618
630
|
stage?: string,
|
|
619
631
|
/** Rejects the wait: the run ended, or the scope it waits in was cancelled. */
|
|
620
632
|
signal?: AbortSignal,
|
|
621
633
|
): Promise<WorkflowEvent<string, JsonValue>[]> {
|
|
622
|
-
const { label, take } = checked(options);
|
|
623
634
|
if (stage !== undefined) {
|
|
624
635
|
const batch = await this.batchOf(stage);
|
|
625
636
|
if (batch) return batch;
|
|
@@ -632,7 +643,7 @@ export class RunMailbox {
|
|
|
632
643
|
return this.#wait(
|
|
633
644
|
{
|
|
634
645
|
...(label === undefined ? {} : { label }),
|
|
635
|
-
take,
|
|
646
|
+
take: "all",
|
|
636
647
|
...(stage === undefined ? {} : { stage }),
|
|
637
648
|
},
|
|
638
649
|
signal,
|
|
@@ -959,19 +970,3 @@ async function mailboxSize(runDir: string): Promise<number> {
|
|
|
959
970
|
() => 0,
|
|
960
971
|
);
|
|
961
972
|
}
|
|
962
|
-
|
|
963
|
-
/** A wait's options as given, checked: what a typed caller can't get wrong, a plain one can. */
|
|
964
|
-
function checked(options: ReceiveOptions | undefined): {
|
|
965
|
-
label?: string;
|
|
966
|
-
take: "all" | "oldest";
|
|
967
|
-
} {
|
|
968
|
-
if (options !== undefined && (typeof options !== "object" || options === null))
|
|
969
|
-
throw new Error("receive takes { label?, take? }");
|
|
970
|
-
const { label, take = "all" } = options ?? {};
|
|
971
|
-
if (label !== undefined && typeof label !== "string")
|
|
972
|
-
throw new Error("receive's label is a string");
|
|
973
|
-
if (take !== "all" && take !== "oldest") throw new Error('receive\'s take is "all" or "oldest"');
|
|
974
|
-
if ("until" in (options ?? {}))
|
|
975
|
-
throw new Error("receive has no until: a timer, workflow.timer(name, when), wakes it");
|
|
976
|
-
return { ...(label === undefined ? {} : { label }), take };
|
|
977
|
-
}
|
package/src/messaging/limits.ts
CHANGED
|
@@ -12,14 +12,14 @@ export const MESSAGE_LIMITS: MessageLimits = {
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Where an asker may wait for an answer, by harness and placement: on where M2 passed
|
|
15
|
-
* (`docs/findings/messaging-m2.md`), off elsewhere. A
|
|
16
|
-
* confirms what it was sent, as `
|
|
15
|
+
* (`docs/findings/messaging-m2.md`), off elsewhere. A TUI that is `confirmed` waits only where it
|
|
16
|
+
* confirms what it was sent, as `wfa waiting` needs. The fake harness always may. cursor is off:
|
|
17
17
|
* its turns ran past their deadline in M2.
|
|
18
18
|
*/
|
|
19
|
-
export const AWAIT_REPLY: Readonly<Record<string, { headless?: true;
|
|
19
|
+
export const AWAIT_REPLY: Readonly<Record<string, { headless?: true; tui?: true | "confirmed" }>> =
|
|
20
20
|
{
|
|
21
|
-
fake: { headless: true,
|
|
22
|
-
claude: { headless: true,
|
|
21
|
+
fake: { headless: true, tui: true },
|
|
22
|
+
claude: { headless: true, tui: "confirmed" },
|
|
23
23
|
codex: { headless: true },
|
|
24
24
|
pi: { headless: true },
|
|
25
25
|
};
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
import type { ParticipantCode, PeerView, ReplyCode, SendCode } from "@agentswf/contract/wire";
|
|
10
10
|
import type { AgentKey, JsonObject } from "@agentswf/contract/workflow";
|
|
11
11
|
import type { AuthoredMessage } from "@agentswf/harness/adapter";
|
|
12
|
-
import {
|
|
12
|
+
import { formatDuration } from "../duration";
|
|
13
13
|
import { MESSAGE_LIMITS } from "./limits";
|
|
14
14
|
import {
|
|
15
15
|
directions,
|
|
@@ -47,7 +47,7 @@ export interface MessagingHost {
|
|
|
47
47
|
agent(key: AgentKey): AgentView | undefined;
|
|
48
48
|
/** Whether it may `--await-reply` where it runs. */
|
|
49
49
|
canAwait(key: AgentKey): boolean;
|
|
50
|
-
/** The launcher it runs `
|
|
50
|
+
/** The launcher it runs `wfa` through. */
|
|
51
51
|
launcher(key: AgentKey): string;
|
|
52
52
|
/** Something is new for this agent: messages for it, or an end to its wait. */
|
|
53
53
|
changed(key: AgentKey): void;
|
|
@@ -180,7 +180,7 @@ type Obligation = {
|
|
|
180
180
|
/** It has a `--timeout` of its own, which only that fails it at. */
|
|
181
181
|
readonly timed: boolean;
|
|
182
182
|
/**
|
|
183
|
-
* When it closes unanswered
|
|
183
|
+
* When it closes unanswered: the route's `timeoutMs` or the asker's
|
|
184
184
|
* shorter `--timeout`, and which of the two.
|
|
185
185
|
*/
|
|
186
186
|
readonly closes?: { at: number; by: "route" | "own"; ms: number };
|
|
@@ -207,7 +207,7 @@ type Operation = OperationInfo & {
|
|
|
207
207
|
/**
|
|
208
208
|
* The messaging logic of one attempt: routes, admission, inboxes and owed answers. It decides every
|
|
209
209
|
* case the design lists and writes the record; it starts nothing. The runner opens and continues
|
|
210
|
-
* operations when it says, and the control plane hands it `
|
|
210
|
+
* operations when it says, and the control plane hands it `wfa`'s commands.
|
|
211
211
|
*/
|
|
212
212
|
export class Messaging {
|
|
213
213
|
readonly limits: MessageLimits;
|
|
@@ -279,15 +279,13 @@ export class Messaging {
|
|
|
279
279
|
}
|
|
280
280
|
}
|
|
281
281
|
const timeoutMs = options.timeoutMs;
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
);
|
|
290
|
-
}
|
|
282
|
+
if (
|
|
283
|
+
timeoutMs !== undefined &&
|
|
284
|
+
(typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1)
|
|
285
|
+
)
|
|
286
|
+
throw new Error(
|
|
287
|
+
`a route's timeoutMs is a positive whole number of milliseconds: not ${timeoutMs}`,
|
|
288
|
+
);
|
|
291
289
|
const open = pairs.map(([from, to]) => this.#open(from, to));
|
|
292
290
|
if (open.some(Boolean)) {
|
|
293
291
|
if (
|
|
@@ -361,7 +359,7 @@ export class Messaging {
|
|
|
361
359
|
});
|
|
362
360
|
}
|
|
363
361
|
|
|
364
|
-
/** Writes `text` to a participant
|
|
362
|
+
/** Writes `text` to a participant, without a turn: an inbox item for it. */
|
|
365
363
|
async post(key: AgentKey, text: string): Promise<string> {
|
|
366
364
|
if (this.#closed) throw new Error("the attempt has ended; nothing more is posted");
|
|
367
365
|
if (!this.#outside.has(key)) throw new Error(`${key} is not connected`);
|
|
@@ -506,7 +504,7 @@ export class Messaging {
|
|
|
506
504
|
return holderRefusal("no-route", `the workflow allows no route from ${from} to ${agent}.`);
|
|
507
505
|
if (view.state === "stopped")
|
|
508
506
|
return holderRefusal(
|
|
509
|
-
"stopped",
|
|
507
|
+
"peer-stopped",
|
|
510
508
|
`${agent} has stopped${view.reason ? ` (${JSON.stringify(view.reason)})` : ""}.`,
|
|
511
509
|
);
|
|
512
510
|
const route = initiating.find((each) => each.open);
|
|
@@ -702,8 +700,8 @@ export class Messaging {
|
|
|
702
700
|
}
|
|
703
701
|
|
|
704
702
|
/**
|
|
705
|
-
* A refusal of `
|
|
706
|
-
* `
|
|
703
|
+
* A refusal of `wfa result` or `wfa waiting` given this call id, or the acknowledgement of a
|
|
704
|
+
* `wfa waiting` while an answer is awaited; null when the command goes on as before.
|
|
707
705
|
*/
|
|
708
706
|
checkFinish(
|
|
709
707
|
agent: AgentKey,
|
|
@@ -715,7 +713,7 @@ export class Messaging {
|
|
|
715
713
|
| null {
|
|
716
714
|
const operation = this.#operations.get(id);
|
|
717
715
|
if (!operation || operation.agent !== agent) return null;
|
|
718
|
-
const
|
|
716
|
+
const wfa = this.#host.launcher(agent);
|
|
719
717
|
// A message operation's call takes no result, ended or not.
|
|
720
718
|
if (operation.kind === "message") {
|
|
721
719
|
return {
|
|
@@ -733,8 +731,8 @@ export class Messaging {
|
|
|
733
731
|
code: "owes-reply",
|
|
734
732
|
error:
|
|
735
733
|
command === "result"
|
|
736
|
-
? `you owe ${peer} a reply. Send it with ${
|
|
737
|
-
: `you owe ${peer} a reply; send it with ${
|
|
734
|
+
? `you owe ${peer} a reply. Send it with ${wfa} reply ${peer}, then run ${wfa} result ${id} again.`
|
|
735
|
+
: `you owe ${peer} a reply; send it with ${wfa} reply ${peer} first.`,
|
|
738
736
|
};
|
|
739
737
|
}
|
|
740
738
|
const waited = operation.awaits;
|
|
@@ -743,7 +741,7 @@ export class Messaging {
|
|
|
743
741
|
kind: "awaiting",
|
|
744
742
|
peer: waited.owedBy,
|
|
745
743
|
...(waited.until === undefined ? {} : { until: waited.until }),
|
|
746
|
-
notice: `you already await ${waited.owedBy}'s reply${waited.until === undefined ? "" : `, until ${clock(waited.until, this.#host.now())}`}. End your turn, or finish with ${
|
|
744
|
+
notice: `you already await ${waited.owedBy}'s reply${waited.until === undefined ? "" : `, until ${clock(waited.until, this.#host.now())}`}. End your turn, or finish with ${wfa} result ${id}.`,
|
|
747
745
|
};
|
|
748
746
|
}
|
|
749
747
|
return null;
|
|
@@ -815,7 +813,7 @@ export class Messaging {
|
|
|
815
813
|
):
|
|
816
814
|
| { kind: "ok"; operation: Operation; route: Route; bytes: number }
|
|
817
815
|
| { kind: "refused"; code: SendCode; error: string } {
|
|
818
|
-
const
|
|
816
|
+
const wfa = this.#host.launcher(from);
|
|
819
817
|
const operation = this.#current.get(from);
|
|
820
818
|
if (!operation) return refuse("no-operation", "this agent has no open operation.");
|
|
821
819
|
const { peer } = request;
|
|
@@ -826,14 +824,14 @@ export class Messaging {
|
|
|
826
824
|
return refuse(
|
|
827
825
|
"no-route",
|
|
828
826
|
owes
|
|
829
|
-
? `your route with ${peer} only answers its requests. Answer it with ${
|
|
827
|
+
? `your route with ${peer} only answers its requests. Answer it with ${wfa} reply ${peer}.`
|
|
830
828
|
: `your route with ${peer} only answers its requests, and it asked nothing. Nothing to send; end your turn.`,
|
|
831
829
|
);
|
|
832
830
|
};
|
|
833
831
|
if (initiating.length === 0) {
|
|
834
832
|
return this.#open(peer, from)
|
|
835
833
|
? replyOnly()
|
|
836
|
-
: refuse("no-route", `no route to ${peer}. Your routes: ${
|
|
834
|
+
: refuse("no-route", `no route to ${peer}. Your routes: ${wfa} peers`);
|
|
837
835
|
}
|
|
838
836
|
const agent = this.#outside.has(peer) ? undefined : this.#host.agent(peer);
|
|
839
837
|
if (agent?.state === "stopped")
|
|
@@ -889,7 +887,7 @@ export class Messaging {
|
|
|
889
887
|
):
|
|
890
888
|
| { kind: "ok"; operation: Operation; owed: Obligation; bytes: number }
|
|
891
889
|
| { kind: "refused"; code: ReplyCode; error: string } {
|
|
892
|
-
const
|
|
890
|
+
const wfa = this.#host.launcher(from);
|
|
893
891
|
const operation = this.#current.get(from);
|
|
894
892
|
if (!operation) return refuse("no-operation", "this agent has no open operation.");
|
|
895
893
|
const { peer } = request;
|
|
@@ -898,7 +896,7 @@ export class Messaging {
|
|
|
898
896
|
(each) => each.asker === peer && each.owedBy === from && each.request.shown,
|
|
899
897
|
);
|
|
900
898
|
if (!latest || latest.state === "replied") {
|
|
901
|
-
return refuse("none-owed", `you owe ${peer} nothing. Send it with ${
|
|
899
|
+
return refuse("none-owed", `you owe ${peer} nothing. Send it with ${wfa} send ${peer}.`);
|
|
902
900
|
}
|
|
903
901
|
if (latest.state !== "open" || latest.owingOperation !== operation) {
|
|
904
902
|
const agent = this.#host.agent(peer);
|
|
@@ -906,7 +904,7 @@ export class Messaging {
|
|
|
906
904
|
"expired",
|
|
907
905
|
agent?.state === "stopped"
|
|
908
906
|
? `${peer} has stopped${agent.reason ? ` (${JSON.stringify(agent.reason)})` : ""}; nothing reaches it now.`
|
|
909
|
-
: `${peer} stopped waiting at ${clock(latest.endedAt ?? this.#host.now(), this.#host.now())}. Send it with ${
|
|
907
|
+
: `${peer} stopped waiting at ${clock(latest.endedAt ?? this.#host.now(), this.#host.now())}. Send it with ${wfa} send ${peer}.`,
|
|
910
908
|
);
|
|
911
909
|
}
|
|
912
910
|
const route = latest.request.route;
|
|
@@ -942,18 +940,20 @@ export class Messaging {
|
|
|
942
940
|
return { kind: "ok", operation, owed: latest, bytes };
|
|
943
941
|
}
|
|
944
942
|
|
|
945
|
-
/** `awaiting`, `unsupported` and `cycle`, checked for any question. */
|
|
943
|
+
/** `already-awaiting`, `unsupported` and `cycle`, checked for any question. */
|
|
946
944
|
#checkAsk(
|
|
947
945
|
from: AgentKey,
|
|
948
946
|
peer: AgentKey,
|
|
949
947
|
operation: Operation,
|
|
950
948
|
/** The question a reply answers, which counts as answered here. */
|
|
951
949
|
answering?: Obligation,
|
|
952
|
-
):
|
|
953
|
-
|
|
950
|
+
):
|
|
951
|
+
| { kind: "refused"; code: "already-awaiting" | "unsupported" | "cycle"; error: string }
|
|
952
|
+
| undefined {
|
|
953
|
+
const wfa = this.#host.launcher(from);
|
|
954
954
|
if (operation.awaits?.state === "open") {
|
|
955
955
|
return refuse(
|
|
956
|
-
"awaiting",
|
|
956
|
+
"already-awaiting",
|
|
957
957
|
`you already await ${operation.awaits.owedBy}. End your turn, or send without --await-reply.`,
|
|
958
958
|
);
|
|
959
959
|
}
|
|
@@ -984,7 +984,7 @@ export class Messaging {
|
|
|
984
984
|
return refuse(
|
|
985
985
|
"cycle",
|
|
986
986
|
owing
|
|
987
|
-
? `${who}, and you owe ${last === peer ? "it" : last} a reply. Answer with ${
|
|
987
|
+
? `${who}, and you owe ${last === peer ? "it" : last} a reply. Answer with ${wfa} reply ${last}; it may --await-reply.`
|
|
988
988
|
: `${who}. Send without --await-reply and end your turn; ${last}'s request reaches you then.`,
|
|
989
989
|
);
|
|
990
990
|
}
|
|
@@ -1045,7 +1045,7 @@ export class Messaging {
|
|
|
1045
1045
|
#ask(message: Message, operation: Operation, timeoutMs?: number): { until?: number } {
|
|
1046
1046
|
message.route.count += 1;
|
|
1047
1047
|
const now = this.#host.now();
|
|
1048
|
-
const routeMs =
|
|
1048
|
+
const routeMs = message.route.timeoutMs;
|
|
1049
1049
|
// The route's limit, or the asker's own when shorter: the shorter wins.
|
|
1050
1050
|
const limit =
|
|
1051
1051
|
routeMs !== undefined && (timeoutMs === undefined || routeMs <= timeoutMs)
|
|
@@ -1069,7 +1069,7 @@ export class Messaging {
|
|
|
1069
1069
|
owedBy: message.to,
|
|
1070
1070
|
...(until === undefined ? {} : { until }),
|
|
1071
1071
|
timed: limitAt < (outside ? Infinity : (operation.deadline ?? Infinity)),
|
|
1072
|
-
...(limit
|
|
1072
|
+
...(limit ? { closes: { ...limit, at: limitAt } } : {}),
|
|
1073
1073
|
state: "open",
|
|
1074
1074
|
};
|
|
1075
1075
|
message.asks = obligation;
|
|
@@ -1085,7 +1085,7 @@ export class Messaging {
|
|
|
1085
1085
|
obligation,
|
|
1086
1086
|
"timed-out",
|
|
1087
1087
|
obligation.closes?.by === "route"
|
|
1088
|
-
? `the question closed unanswered after ${
|
|
1088
|
+
? `the question closed unanswered after ${formatDuration(obligation.closes.ms)}`
|
|
1089
1089
|
: `your --timeout passed at ${clock(closesAt, this.#host.now())}`,
|
|
1090
1090
|
);
|
|
1091
1091
|
}
|
|
@@ -1095,7 +1095,7 @@ export class Messaging {
|
|
|
1095
1095
|
}
|
|
1096
1096
|
|
|
1097
1097
|
/**
|
|
1098
|
-
* Durable before `
|
|
1098
|
+
* Durable before `wfa send` returns: a failed write rejects, and `wfa` says it may have been
|
|
1099
1099
|
* queued. An outside participant's message says what it came through instead of an operation.
|
|
1100
1100
|
* A question to one carries when it closes unanswered, if it has a limit.
|
|
1101
1101
|
*/
|
|
@@ -1134,13 +1134,13 @@ export class Messaging {
|
|
|
1134
1134
|
}
|
|
1135
1135
|
|
|
1136
1136
|
#awaitNotice(operation: Operation, { until }: { until?: number }): string {
|
|
1137
|
-
const
|
|
1137
|
+
const wfa = this.#host.launcher(operation.agent);
|
|
1138
1138
|
return [
|
|
1139
1139
|
`Its reply starts your next turn (${until === undefined ? "no deadline" : `deadline ${clock(until, this.#host.now())}`}).`,
|
|
1140
1140
|
"Go on with work that does not need the answer, then end your turn with one line saying what you await; do not poll or sleep.",
|
|
1141
1141
|
...(operation.kind === "workflow"
|
|
1142
1142
|
? [
|
|
1143
|
-
`If you find the answer first, or decide not to wait, finish with ${
|
|
1143
|
+
`If you find the answer first, or decide not to wait, finish with ${wfa} result ${operation.id}; the wait is dropped.`,
|
|
1144
1144
|
]
|
|
1145
1145
|
: []),
|
|
1146
1146
|
].join("\n");
|
|
@@ -1323,7 +1323,7 @@ export class Messaging {
|
|
|
1323
1323
|
/**
|
|
1324
1324
|
* What is held for an operation whose turn has ended, deciding nothing: `continues` when its turn
|
|
1325
1325
|
* end would go on with it, `held` when messages wait for a later operation or to cut a check-in's
|
|
1326
|
-
* `
|
|
1326
|
+
* `wfa waiting` short, `none` otherwise.
|
|
1327
1327
|
*/
|
|
1328
1328
|
pending(id: string): Pending {
|
|
1329
1329
|
const operation = this.#operations.get(id);
|
|
@@ -1375,7 +1375,7 @@ export class Messaging {
|
|
|
1375
1375
|
return {
|
|
1376
1376
|
peer,
|
|
1377
1377
|
can: this.#can(route, agent),
|
|
1378
|
-
|
|
1378
|
+
kind: "participant",
|
|
1379
1379
|
queued: 0,
|
|
1380
1380
|
purpose: route.purpose,
|
|
1381
1381
|
};
|
|
@@ -1388,8 +1388,13 @@ export class Messaging {
|
|
|
1388
1388
|
return {
|
|
1389
1389
|
peer,
|
|
1390
1390
|
can: this.#can(route, agent),
|
|
1391
|
+
kind: "agent",
|
|
1391
1392
|
state:
|
|
1392
|
-
view?.state === "stopped"
|
|
1393
|
+
view?.state === "stopped"
|
|
1394
|
+
? "stopped"
|
|
1395
|
+
: awaiting
|
|
1396
|
+
? "awaiting-reply"
|
|
1397
|
+
: (view?.state ?? "idle"),
|
|
1393
1398
|
...(awaiting && view?.state !== "stopped" ? { awaits: awaiting } : {}),
|
|
1394
1399
|
...(view?.state === "stopped" && view.reason ? { reason: view.reason } : {}),
|
|
1395
1400
|
queued,
|
|
@@ -1405,7 +1410,7 @@ export class Messaging {
|
|
|
1405
1410
|
};
|
|
1406
1411
|
}
|
|
1407
1412
|
|
|
1408
|
-
/** The line `
|
|
1413
|
+
/** The line `wfa` leads with while this agent's operation owes or awaits an answer. */
|
|
1409
1414
|
owing(agent: AgentKey): string | undefined {
|
|
1410
1415
|
const { owes, awaits } = this.#standing(agent);
|
|
1411
1416
|
return owingLine(owes, awaits, this.#host.launcher(agent), this.#host.now());
|
|
@@ -1476,7 +1481,7 @@ export class Messaging {
|
|
|
1476
1481
|
|
|
1477
1482
|
/**
|
|
1478
1483
|
* This agent's open routes, either end, in the order granted; with `stopped`, also those a peer's
|
|
1479
|
-
* stop closed, so `
|
|
1484
|
+
* stop closed, so `wfa peers` can say it stopped.
|
|
1480
1485
|
*/
|
|
1481
1486
|
#routesOf(agent: AgentKey, stopped = false): Route[] {
|
|
1482
1487
|
return [...new Set([...this.#routes.values()].flat())]
|
package/src/messaging/routes.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type Route = {
|
|
|
21
21
|
readonly asked?: number;
|
|
22
22
|
/** As the workflow gave it, for telling an identical grant from a conflicting one. */
|
|
23
23
|
readonly requested?: number;
|
|
24
|
-
/**
|
|
24
|
+
/** How long a question on the route waits, to an agent or an outside participant. */
|
|
25
25
|
readonly timeoutMs?: number;
|
|
26
26
|
readonly scope: Scope;
|
|
27
27
|
/** Messages counted against it, answers reserved for included. */
|
|
@@ -29,7 +29,7 @@ export type Route = {
|
|
|
29
29
|
open: boolean;
|
|
30
30
|
/** Why it closed. */
|
|
31
31
|
closed?: "stage-ended" | "agent-stopped" | "released" | "attempt-ended";
|
|
32
|
-
/** Its place among the attempt's grants, which `
|
|
32
|
+
/** Its place among the attempt's grants, which `wfa peers` lists in. */
|
|
33
33
|
readonly order: number;
|
|
34
34
|
};
|
|
35
35
|
|
package/src/messaging/text.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { AgentKey } from "@agentswf/contract/workflow";
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Everything an agent reads about messaging. The engine writes it all, since only it knows the
|
|
7
|
-
* launcher's path, the routes' purposes and what closes a cycle; `
|
|
7
|
+
* launcher's path, the routes' purposes and what closes a cycle; `wfa` prints it as given. `wfa`
|
|
8
8
|
* here is always the launcher's full path: the command is not on the agent's PATH.
|
|
9
9
|
*/
|
|
10
10
|
|
|
@@ -13,7 +13,7 @@ export type Recipient = {
|
|
|
13
13
|
launcher: string;
|
|
14
14
|
/** The operation's call id. */
|
|
15
15
|
callId: string;
|
|
16
|
-
/** A workflow's own operation finishes with `
|
|
16
|
+
/** A workflow's own operation finishes with `wfa result`; a message operation ends its turn. */
|
|
17
17
|
kind: "workflow" | "message";
|
|
18
18
|
};
|
|
19
19
|
|
|
@@ -92,7 +92,7 @@ export function envelope(
|
|
|
92
92
|
} = {},
|
|
93
93
|
): string {
|
|
94
94
|
const tag = options.tag ?? nonce();
|
|
95
|
-
const
|
|
95
|
+
const wfa = to.launcher;
|
|
96
96
|
const blocks: string[] = [];
|
|
97
97
|
const owed = new Set<AgentKey>();
|
|
98
98
|
for (const item of items) {
|
|
@@ -104,7 +104,7 @@ export function envelope(
|
|
|
104
104
|
if (item.answers) {
|
|
105
105
|
blocks.push(
|
|
106
106
|
[
|
|
107
|
-
`The reply you were waiting for, from ${item.from}${item.asks ? `; it asks something back, so you owe it a reply: answer with ${
|
|
107
|
+
`The reply you were waiting for, from ${item.from}${item.asks ? `; it asks something back, so you owe it a reply: answer with ${wfa} reply ${item.from}` : ""}.`,
|
|
108
108
|
"",
|
|
109
109
|
fenced("reply", item.from, item.text, tag),
|
|
110
110
|
].join("\n"),
|
|
@@ -114,13 +114,13 @@ export function envelope(
|
|
|
114
114
|
const stop = /[.!?]$/.test(item.purpose) ? "" : ".";
|
|
115
115
|
const head = `Message from ${item.from}, on the route ${quoted(item.purpose)}${stop}`;
|
|
116
116
|
const lead = options.opens ? ` This is a message, not a new task (call ${to.callId}).` : "";
|
|
117
|
-
const asks = item.asks ? ` A reply is required: answer with ${
|
|
117
|
+
const asks = item.asks ? ` A reply is required: answer with ${wfa} reply ${item.from}.` : "";
|
|
118
118
|
blocks.push(
|
|
119
119
|
[`${head}${lead}${asks}`, "", fenced("message", item.from, item.text, tag)].join("\n"),
|
|
120
120
|
);
|
|
121
121
|
}
|
|
122
122
|
const lines = [blocks.join("\n\n")];
|
|
123
|
-
if (options.announced?.length) lines.push(announcement(options.announced,
|
|
123
|
+
if (options.announced?.length) lines.push(announcement(options.announced, wfa));
|
|
124
124
|
const plain = items.some((item) => item.kind === "message" && !item.answers);
|
|
125
125
|
if (plain) lines.push(GUARD);
|
|
126
126
|
lines.push(next(items, to, owed));
|
|
@@ -129,7 +129,7 @@ export function envelope(
|
|
|
129
129
|
|
|
130
130
|
/** The closing line: whether the task goes on, and how it ends. */
|
|
131
131
|
function next(items: readonly Delivered[], to: Recipient, owed: ReadonlySet<AgentKey>): string {
|
|
132
|
-
const
|
|
132
|
+
const wfa = to.launcher;
|
|
133
133
|
const failed = items.some((item) => item.kind === "no-reply");
|
|
134
134
|
const answered = items.some((item) => item.kind === "message" && item.answers);
|
|
135
135
|
if (to.kind === "message") {
|
|
@@ -140,9 +140,9 @@ function next(items: readonly Delivered[], to: Recipient, owed: ReadonlySet<Agen
|
|
|
140
140
|
return `Your work for call ${to.callId} continues; end your turn when it is done.`;
|
|
141
141
|
return `No reply is required; end your turn once you have acted on it ${call}.`;
|
|
142
142
|
}
|
|
143
|
-
const finish = `finish it with ${
|
|
143
|
+
const finish = `finish it with ${wfa} result ${to.callId}`;
|
|
144
144
|
if (owed.size > 0) return `Your task ${to.callId} goes on: once you have replied, ${finish}.`;
|
|
145
|
-
if (failed) return `Continue without it, or finish with ${
|
|
145
|
+
if (failed) return `Continue without it, or finish with ${wfa} result ${to.callId}.`;
|
|
146
146
|
return `Your task ${to.callId} continues; ${finish}.`;
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -162,12 +162,12 @@ function routeLine({ peer, can, purpose }: RouteLine): string {
|
|
|
162
162
|
/** What an operation's prompt says about the agent's routes, once, in a few lines. */
|
|
163
163
|
export function routeLesson(routes: readonly RouteLine[], launcher: string): string {
|
|
164
164
|
if (routes.length === 0) return "";
|
|
165
|
-
const
|
|
165
|
+
const wfa = launcher;
|
|
166
166
|
return [
|
|
167
167
|
"You can message other agents on these routes:",
|
|
168
168
|
...routes.map(routeLine),
|
|
169
|
-
`See them with ${
|
|
170
|
-
`Answer a question with ${
|
|
169
|
+
`See them with ${wfa} peers. Send with ${wfa} send <peer> <<'EOF' ... EOF. To ask and wait for the answer, send with ${wfa} send --await-reply <peer>; the answer starts your next turn, so end your turn then, and do not poll or sleep.`,
|
|
170
|
+
`Answer a question with ${wfa} reply <peer> <<'EOF' ... EOF; only a reply counts as the answer.`,
|
|
171
171
|
].join("\n");
|
|
172
172
|
}
|
|
173
173
|
|
|
@@ -181,14 +181,14 @@ function announcement(routes: readonly RouteLine[], launcher: string): string {
|
|
|
181
181
|
|
|
182
182
|
/** The one reminder a turn that ends owing gets. */
|
|
183
183
|
export function reminder(owed: readonly AgentKey[], to: Recipient): string {
|
|
184
|
-
const
|
|
184
|
+
const wfa = to.launcher;
|
|
185
185
|
const who = owed.join(" and ");
|
|
186
|
-
const how = owed.map((peer) => `${
|
|
187
|
-
const then = to.kind === "workflow" ? `then ${
|
|
186
|
+
const how = owed.map((peer) => `${wfa} reply ${peer}`).join(", then ");
|
|
187
|
+
const then = to.kind === "workflow" ? `then ${wfa} result ${to.callId}` : "then end your turn";
|
|
188
188
|
return `You owe ${who} a reply; send it with ${how}, ${then}.`;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
/** The line every `
|
|
191
|
+
/** The line every `wfa` command leads with while the operation owes or awaits an answer. */
|
|
192
192
|
export function owingLine(
|
|
193
193
|
owes: readonly AgentKey[],
|
|
194
194
|
awaits: { peer: AgentKey; until?: number } | undefined,
|
|
@@ -207,7 +207,7 @@ export function owingLine(
|
|
|
207
207
|
return parts.length === 0 ? undefined : `${parts.join("; ")}.`;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
/** `
|
|
210
|
+
/** `wfa peers`'s table, the line about what is owed, and nothing for empty cells. */
|
|
211
211
|
export function peersTable(
|
|
212
212
|
peers: readonly PeerView[],
|
|
213
213
|
awaits: { peer: AgentKey; until?: number } | undefined,
|
|
@@ -228,11 +228,13 @@ export function peersTable(
|
|
|
228
228
|
const rows = peers.map((peer) => [
|
|
229
229
|
peer.peer,
|
|
230
230
|
peer.can,
|
|
231
|
-
peer.
|
|
232
|
-
?
|
|
233
|
-
: peer.state === "
|
|
234
|
-
? `
|
|
235
|
-
: peer.state
|
|
231
|
+
peer.kind === "participant"
|
|
232
|
+
? "participant"
|
|
233
|
+
: peer.state === "awaiting-reply"
|
|
234
|
+
? `awaiting-reply from ${peer.awaits === viewer ? "you" : peer.awaits}`
|
|
235
|
+
: peer.state === "stopped" && peer.reason
|
|
236
|
+
? `stopped (${peer.reason})`
|
|
237
|
+
: peer.state,
|
|
236
238
|
peer.queued > 0 ? String(peer.queued) : "",
|
|
237
239
|
quoted(peer.purpose),
|
|
238
240
|
]);
|