@agentswf/engine 0.1.0-beta.3 → 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 +3 -15
- package/src/attempt-output.ts +1 -1
- package/src/control-plane.ts +1 -1
- package/src/emit-command.ts +2 -13
- 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 +1 -1
- package/src/holder-command.ts +2 -20
- package/src/inbox-command.ts +2 -14
- package/src/init-command.ts +2 -11
- package/src/init-template/AGENTS.md.template +3 -3
- package/src/init-template/hello/workflow.ts.template +4 -4
- package/src/logs-command.ts +2 -15
- package/src/mailbox.ts +15 -2
- package/src/messaging/limits.ts +1 -1
- package/src/messaging/messaging.ts +22 -22
- package/src/messaging/routes.ts +1 -1
- package/src/messaging/text.ts +17 -17
- package/src/operation-liveness.ts +1 -1
- package/src/operator-runtime.ts +4 -7
- package/src/result-validation.ts +1 -1
- package/src/run-command.ts +2 -43
- package/src/run-usage.ts +1 -1
- package/src/runs-command.ts +2 -13
- package/src/sandboxes.ts +3 -2
- package/src/simple-input.ts +13 -3
- package/src/stage-ledger.ts +1 -1
- package/src/test-command.ts +2 -11
- package/src/testing.ts +1 -1
- package/src/tick-command.ts +2 -13
- package/src/tools.ts +16 -7
- package/src/workflow-runner.ts +85 -38
- package/src/workflow-testing/coordinator.typecheck.ts +12 -13
- package/src/workflow-testing/host.ts +8 -8
- package/src/workflow-testing/index.ts +1 -2
- package/src/workflow-testing/script.ts +4 -4
- package/src/workflow-testing/workflow-testing.typecheck.ts +2 -2
package/src/logs-command.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { LogEventJson, LogsJson, RouteEnds, RunLogLine } from "@agentswf/co
|
|
|
3
3
|
import type { JsonObject } from "@agentswf/contract/workflow";
|
|
4
4
|
import { duration } 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
|
/**
|
package/src/mailbox.ts
CHANGED
|
@@ -26,7 +26,19 @@ export function mailboxFile(runDir: string): string {
|
|
|
26
26
|
/** The run's mailbox in the order written, less a line a crash tore and kinds this awf doesn't know. */
|
|
27
27
|
export async function readMailbox(runDir: string): Promise<MailboxRecord[]> {
|
|
28
28
|
const lines = await readLines<MailboxRecord>(mailboxFile(runDir));
|
|
29
|
-
return lines
|
|
29
|
+
return lines
|
|
30
|
+
.filter((line) => line.version === MAILBOX_RECORD_VERSION && KINDS.has(line.kind))
|
|
31
|
+
.map(withAnswered);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A turn's end written before 0.1.0-beta.4 has no `answered`, which a workflow would read as false. */
|
|
35
|
+
function withAnswered(line: MailboxRecord): MailboxRecord {
|
|
36
|
+
if (line.kind !== "event" || line.name !== "agent:ended") return line;
|
|
37
|
+
const end = line.data as { outcome?: { kind: string; answered?: boolean } };
|
|
38
|
+
if (end?.outcome === undefined || end.outcome.answered !== undefined) return line;
|
|
39
|
+
const answered = end.outcome.kind === "answered";
|
|
40
|
+
const data = { ...end, outcome: { ...end.outcome, answered } } as unknown as JsonValue;
|
|
41
|
+
return { ...line, data };
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
const KINDS = new Set<string>([
|
|
@@ -137,6 +149,7 @@ export async function recoverTurns(
|
|
|
137
149
|
turn: turn.turn,
|
|
138
150
|
meta: turn.meta,
|
|
139
151
|
outcome: {
|
|
152
|
+
answered: false,
|
|
140
153
|
kind: "failed",
|
|
141
154
|
reason: `attempt ${turn.attempt} ended before this turn did; its work, if any, is in the agent's session`,
|
|
142
155
|
retryable: true,
|
|
@@ -972,6 +985,6 @@ function checked(options: ReceiveOptions | undefined): {
|
|
|
972
985
|
throw new Error("receive's label is a string");
|
|
973
986
|
if (take !== "all" && take !== "oldest") throw new Error('receive\'s take is "all" or "oldest"');
|
|
974
987
|
if ("until" in (options ?? {}))
|
|
975
|
-
throw new Error("receive has no until: a timer,
|
|
988
|
+
throw new Error("receive has no until: a timer, wf.timer(name, when), wakes it");
|
|
976
989
|
return { ...(label === undefined ? {} : { label }), take };
|
|
977
990
|
}
|
package/src/messaging/limits.ts
CHANGED
|
@@ -13,7 +13,7 @@ export const MESSAGE_LIMITS: MessageLimits = {
|
|
|
13
13
|
/**
|
|
14
14
|
* Where an asker may wait for an answer, by harness and placement: on where M2 passed
|
|
15
15
|
* (`docs/findings/messaging-m2.md`), off elsewhere. A pane that is `confirmed` waits only where it
|
|
16
|
-
* confirms what it was sent, as `
|
|
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
19
|
export const AWAIT_REPLY: Readonly<Record<string, { headless?: true; pane?: true | "confirmed" }>> =
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -702,8 +702,8 @@ export class Messaging {
|
|
|
702
702
|
}
|
|
703
703
|
|
|
704
704
|
/**
|
|
705
|
-
* A refusal of `
|
|
706
|
-
* `
|
|
705
|
+
* A refusal of `wfa result` or `wfa waiting` given this call id, or the acknowledgement of a
|
|
706
|
+
* `wfa waiting` while an answer is awaited; null when the command goes on as before.
|
|
707
707
|
*/
|
|
708
708
|
checkFinish(
|
|
709
709
|
agent: AgentKey,
|
|
@@ -715,7 +715,7 @@ export class Messaging {
|
|
|
715
715
|
| null {
|
|
716
716
|
const operation = this.#operations.get(id);
|
|
717
717
|
if (!operation || operation.agent !== agent) return null;
|
|
718
|
-
const
|
|
718
|
+
const wfa = this.#host.launcher(agent);
|
|
719
719
|
// A message operation's call takes no result, ended or not.
|
|
720
720
|
if (operation.kind === "message") {
|
|
721
721
|
return {
|
|
@@ -733,8 +733,8 @@ export class Messaging {
|
|
|
733
733
|
code: "owes-reply",
|
|
734
734
|
error:
|
|
735
735
|
command === "result"
|
|
736
|
-
? `you owe ${peer} a reply. Send it with ${
|
|
737
|
-
: `you owe ${peer} a reply; send it with ${
|
|
736
|
+
? `you owe ${peer} a reply. Send it with ${wfa} reply ${peer}, then run ${wfa} result ${id} again.`
|
|
737
|
+
: `you owe ${peer} a reply; send it with ${wfa} reply ${peer} first.`,
|
|
738
738
|
};
|
|
739
739
|
}
|
|
740
740
|
const waited = operation.awaits;
|
|
@@ -743,7 +743,7 @@ export class Messaging {
|
|
|
743
743
|
kind: "awaiting",
|
|
744
744
|
peer: waited.owedBy,
|
|
745
745
|
...(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 ${
|
|
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 ${wfa} result ${id}.`,
|
|
747
747
|
};
|
|
748
748
|
}
|
|
749
749
|
return null;
|
|
@@ -815,7 +815,7 @@ export class Messaging {
|
|
|
815
815
|
):
|
|
816
816
|
| { kind: "ok"; operation: Operation; route: Route; bytes: number }
|
|
817
817
|
| { kind: "refused"; code: SendCode; error: string } {
|
|
818
|
-
const
|
|
818
|
+
const wfa = this.#host.launcher(from);
|
|
819
819
|
const operation = this.#current.get(from);
|
|
820
820
|
if (!operation) return refuse("no-operation", "this agent has no open operation.");
|
|
821
821
|
const { peer } = request;
|
|
@@ -826,14 +826,14 @@ export class Messaging {
|
|
|
826
826
|
return refuse(
|
|
827
827
|
"no-route",
|
|
828
828
|
owes
|
|
829
|
-
? `your route with ${peer} only answers its requests. Answer it with ${
|
|
829
|
+
? `your route with ${peer} only answers its requests. Answer it with ${wfa} reply ${peer}.`
|
|
830
830
|
: `your route with ${peer} only answers its requests, and it asked nothing. Nothing to send; end your turn.`,
|
|
831
831
|
);
|
|
832
832
|
};
|
|
833
833
|
if (initiating.length === 0) {
|
|
834
834
|
return this.#open(peer, from)
|
|
835
835
|
? replyOnly()
|
|
836
|
-
: refuse("no-route", `no route to ${peer}. Your routes: ${
|
|
836
|
+
: refuse("no-route", `no route to ${peer}. Your routes: ${wfa} peers`);
|
|
837
837
|
}
|
|
838
838
|
const agent = this.#outside.has(peer) ? undefined : this.#host.agent(peer);
|
|
839
839
|
if (agent?.state === "stopped")
|
|
@@ -889,7 +889,7 @@ export class Messaging {
|
|
|
889
889
|
):
|
|
890
890
|
| { kind: "ok"; operation: Operation; owed: Obligation; bytes: number }
|
|
891
891
|
| { kind: "refused"; code: ReplyCode; error: string } {
|
|
892
|
-
const
|
|
892
|
+
const wfa = this.#host.launcher(from);
|
|
893
893
|
const operation = this.#current.get(from);
|
|
894
894
|
if (!operation) return refuse("no-operation", "this agent has no open operation.");
|
|
895
895
|
const { peer } = request;
|
|
@@ -898,7 +898,7 @@ export class Messaging {
|
|
|
898
898
|
(each) => each.asker === peer && each.owedBy === from && each.request.shown,
|
|
899
899
|
);
|
|
900
900
|
if (!latest || latest.state === "replied") {
|
|
901
|
-
return refuse("none-owed", `you owe ${peer} nothing. Send it with ${
|
|
901
|
+
return refuse("none-owed", `you owe ${peer} nothing. Send it with ${wfa} send ${peer}.`);
|
|
902
902
|
}
|
|
903
903
|
if (latest.state !== "open" || latest.owingOperation !== operation) {
|
|
904
904
|
const agent = this.#host.agent(peer);
|
|
@@ -906,7 +906,7 @@ export class Messaging {
|
|
|
906
906
|
"expired",
|
|
907
907
|
agent?.state === "stopped"
|
|
908
908
|
? `${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 ${
|
|
909
|
+
: `${peer} stopped waiting at ${clock(latest.endedAt ?? this.#host.now(), this.#host.now())}. Send it with ${wfa} send ${peer}.`,
|
|
910
910
|
);
|
|
911
911
|
}
|
|
912
912
|
const route = latest.request.route;
|
|
@@ -950,7 +950,7 @@ export class Messaging {
|
|
|
950
950
|
/** The question a reply answers, which counts as answered here. */
|
|
951
951
|
answering?: Obligation,
|
|
952
952
|
): { kind: "refused"; code: "awaiting" | "unsupported" | "cycle"; error: string } | undefined {
|
|
953
|
-
const
|
|
953
|
+
const wfa = this.#host.launcher(from);
|
|
954
954
|
if (operation.awaits?.state === "open") {
|
|
955
955
|
return refuse(
|
|
956
956
|
"awaiting",
|
|
@@ -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
|
}
|
|
@@ -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);
|
|
@@ -1405,7 +1405,7 @@ export class Messaging {
|
|
|
1405
1405
|
};
|
|
1406
1406
|
}
|
|
1407
1407
|
|
|
1408
|
-
/** The line `
|
|
1408
|
+
/** The line `wfa` leads with while this agent's operation owes or awaits an answer. */
|
|
1409
1409
|
owing(agent: AgentKey): string | undefined {
|
|
1410
1410
|
const { owes, awaits } = this.#standing(agent);
|
|
1411
1411
|
return owingLine(owes, awaits, this.#host.launcher(agent), this.#host.now());
|
|
@@ -1476,7 +1476,7 @@ export class Messaging {
|
|
|
1476
1476
|
|
|
1477
1477
|
/**
|
|
1478
1478
|
* This agent's open routes, either end, in the order granted; with `stopped`, also those a peer's
|
|
1479
|
-
* stop closed, so `
|
|
1479
|
+
* stop closed, so `wfa peers` can say it stopped.
|
|
1480
1480
|
*/
|
|
1481
1481
|
#routesOf(agent: AgentKey, stopped = false): Route[] {
|
|
1482
1482
|
return [...new Set([...this.#routes.values()].flat())]
|
package/src/messaging/routes.ts
CHANGED
|
@@ -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,
|
|
@@ -506,7 +506,7 @@ export async function superviseOperation(options: Options): Promise<SupervisedOu
|
|
|
506
506
|
deliveryUntil === undefined &&
|
|
507
507
|
receiptUntil === undefined;
|
|
508
508
|
const messages = options.messages;
|
|
509
|
-
// Before any check-in: what continues it goes now, cutting a `
|
|
509
|
+
// Before any check-in: what continues it goes now, cutting a `wfa waiting` short, and an
|
|
510
510
|
// operation awaiting an answer waits for it with no check-in.
|
|
511
511
|
if (messages && settledTurn) {
|
|
512
512
|
if (messages.pending() === "continues") {
|
package/src/operator-runtime.ts
CHANGED
|
@@ -56,7 +56,7 @@ export type OperatorRuntimeInstallation = {
|
|
|
56
56
|
sandboxes?: SandboxProviders;
|
|
57
57
|
/** The decision models workflows may ask, and their aliases. */
|
|
58
58
|
decisions?: DecisionInstallation;
|
|
59
|
-
/** How `
|
|
59
|
+
/** How `wf.allowance` reads a harness's plan, as `awf allowance` does. */
|
|
60
60
|
allowance?: (harness: Harness) => Promise<HarnessAllowance>;
|
|
61
61
|
cleanup(): Promise<void>;
|
|
62
62
|
};
|
|
@@ -181,7 +181,7 @@ export async function installOperatorRuntime(
|
|
|
181
181
|
sandboxes: await installSandboxes(environment),
|
|
182
182
|
decisions: installDecisions({ OPENROUTER_API_KEY: await openRouterKey(environment) }),
|
|
183
183
|
allowance: allowanceReader(environment, run, undefined, home),
|
|
184
|
-
// Nothing to undo: the agent's `
|
|
184
|
+
// Nothing to undo: the agent's `wfa` is a launcher the control plane installs beside its own
|
|
185
185
|
// socket, and the control plane removes both when the run closes.
|
|
186
186
|
cleanup: async () => undefined,
|
|
187
187
|
};
|
|
@@ -282,9 +282,8 @@ export function herdrConfig(session: string): HerdrConfig {
|
|
|
282
282
|
|
|
283
283
|
/**
|
|
284
284
|
* The sandbox providers this machine has: srt when its CLI is on `PATH`, docker when its CLI is;
|
|
285
|
-
* its daemon is first asked when a sandbox opens.
|
|
286
|
-
* the
|
|
287
|
-
* be set up: a spec naming none then fails saying why, rather than moving to docker unsaid.
|
|
285
|
+
* its daemon is first asked when a sandbox opens. There is no default: a spec names its provider,
|
|
286
|
+
* since the two give agents different machines, the host's toolchain or an image.
|
|
288
287
|
*/
|
|
289
288
|
export async function installSandboxes(
|
|
290
289
|
environment: Readonly<Record<string, string | undefined>>,
|
|
@@ -304,10 +303,8 @@ export async function installSandboxes(
|
|
|
304
303
|
...(srt ? { srt: createSrtProvider(srt) } : {}),
|
|
305
304
|
...(docker ? { docker: createDockerProvider(docker) } : {}),
|
|
306
305
|
};
|
|
307
|
-
const fallback = srt || unavailable.srt ? "srt" : docker ? "docker" : undefined;
|
|
308
306
|
return {
|
|
309
307
|
installed,
|
|
310
|
-
...(fallback ? { default: fallback } : {}),
|
|
311
308
|
...(Object.keys(unavailable).length > 0 ? { unavailable } : {}),
|
|
312
309
|
};
|
|
313
310
|
}
|
package/src/result-validation.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type CandidateEvaluation =
|
|
|
8
8
|
| { kind: "rejected"; error: string };
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* A result as `
|
|
11
|
+
* A result as `wfa` sent it, validated without repairing or persisting; the result-slot transition
|
|
12
12
|
* owns acceptance.
|
|
13
13
|
*/
|
|
14
14
|
export async function evaluateGiven(
|
package/src/run-command.ts
CHANGED
|
@@ -3,52 +3,11 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import type { JsonValue } from "@agentswf/contract/workflow";
|
|
4
4
|
import { parseDuration } from "./duration";
|
|
5
5
|
import { messageOf } from "./errors";
|
|
6
|
+
import help from "./help/run.txt" with { type: "text" };
|
|
6
7
|
import { SESSION_CODE } from "./here";
|
|
7
8
|
import { idProblem, runRootOf, stageNameProblem } from "./runs";
|
|
8
9
|
|
|
9
|
-
export const usage =
|
|
10
|
-
"usage: awf run [options] <workflow-file> [options] [-- workflow arguments...]",
|
|
11
|
-
" awf run [options] <workflow-file> --id <id> --continue",
|
|
12
|
-
"run options: --id <id>, --continue, --from-stage <stage>, --values <file>,",
|
|
13
|
-
" --timeout <duration>, --run-root <directory>, --cwd <directory>, --sandbox <file>,",
|
|
14
|
-
" --json, --no-watch, --here",
|
|
15
|
-
"",
|
|
16
|
-
"Each awf run is an attempt of a run: a new run, with --id's id or a generated one, or, with",
|
|
17
|
-
"--continue, the run --id names, with the arguments, --cwd and --sandbox it was started with. A continue reuses",
|
|
18
|
-
"the stages that succeeded, up to the first with no record or to --from-stage, and runs the rest.",
|
|
19
|
-
"A new run with --from-stage starts there. A stage before it with no record to reuse takes its",
|
|
20
|
-
"value from --values, a JSON object by stage name, checked by its result; one that returns",
|
|
21
|
-
"nothing is passed. Without one the attempt stops, naming the stage and its schema.",
|
|
22
|
-
"A stop exits 3. A run is kept in .awf/runs/<workflow>/<id> under --cwd, unless --run-root names",
|
|
23
|
-
"another folder for .awf/runs.",
|
|
24
|
-
"A run has no deadline unless --timeout gives it one, per attempt.",
|
|
25
|
-
"A workflow that knows how to present its result prints that; --json prints the full result instead.",
|
|
26
|
-
"Either way the full result is kept as output.json in the run's folder, beside report.md",
|
|
27
|
-
"when the workflow writes one. A run that fails or is cancelled once its agents have started keeps",
|
|
28
|
-
"output.json too, with what it spent and why it ended; --json prints it. A second Ctrl-C stops",
|
|
29
|
-
"awf at once, without it.",
|
|
30
|
-
"--cwd sets the directory the workflow and its agents work in; it defaults to the current one.",
|
|
31
|
-
"--sandbox puts every agent of the run in one sandbox, working in --cwd; the file is a JSON spec",
|
|
32
|
-
'such as {"read": ["/data/request.md"], "srt": {}}, whose relative paths are from --cwd. A',
|
|
33
|
-
"workflow that opens a sandbox of its own is refused.",
|
|
34
|
-
"A sandbox with its own Herdr, as a docker box has, gets a tab in the run's workspace showing its",
|
|
35
|
-
"panes; --no-watch leaves it out, and awf still prints the command that shows them.",
|
|
36
|
-
"--here, run by an agent in a Herdr pane, starts the run in a new tab and has it take that agent's",
|
|
37
|
-
"session over as one of its agents, from its next turn until the run ends. It prints a line for",
|
|
38
|
-
"the agent to end its turn with, which is how the run finds the session.",
|
|
39
|
-
"A run of a workflow with events is named by --id, or by the workflow's id(args). awf run starts it",
|
|
40
|
-
"with its arguments and no event, detached in its project, as awf emit would, and the run waits at",
|
|
41
|
-
"its first receive. A run already there is only woken. A run an event starts has no argv: its args",
|
|
42
|
-
"are what prepare makes of none.",
|
|
43
|
-
"",
|
|
44
|
-
"Examples:",
|
|
45
|
-
" awf run examples/minimum-review/review-loop.ts",
|
|
46
|
-
" awf run --timeout 20m examples/minimum-review/review-loop.ts",
|
|
47
|
-
" awf run examples/minimum-review/review-loop.ts -- packages/engine/src",
|
|
48
|
-
" awf run examples/minimum-review/review-loop.ts --cwd ../other-repo",
|
|
49
|
-
"",
|
|
50
|
-
"Workflow files are trusted code and run with your filesystem and process authority.",
|
|
51
|
-
].join("\n");
|
|
10
|
+
export const usage = help.trimEnd();
|
|
52
11
|
|
|
53
12
|
export type RunCommand = {
|
|
54
13
|
/** Where paths typed on the command line resolve. */
|
package/src/run-usage.ts
CHANGED
|
@@ -31,7 +31,7 @@ export type AccountedAgent = {
|
|
|
31
31
|
cwd: string;
|
|
32
32
|
/** A sandboxed agent's own harness home, where its sessions are. */
|
|
33
33
|
home?: string;
|
|
34
|
-
/** Every native session id seen for the agent so far, from its `
|
|
34
|
+
/** Every native session id seen for the agent so far, from its `wfa` calls and its adapter. */
|
|
35
35
|
sessions(): readonly string[];
|
|
36
36
|
/** When its host first prompted it, where that waits on the agent first; see `window`. */
|
|
37
37
|
promptedAt?(): number | undefined;
|
package/src/runs-command.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import type { RunJson, RunsJson } from "@agentswf/contract/records";
|
|
3
3
|
import { duration } from "./duration";
|
|
4
4
|
import { messageOf } from "./errors";
|
|
5
|
+
import help from "./help/runs.txt" with { type: "text" };
|
|
5
6
|
import {
|
|
6
7
|
clockTime,
|
|
7
8
|
command,
|
|
@@ -23,19 +24,7 @@ import { encodeCursor } from "./run-read";
|
|
|
23
24
|
import { findRunByRef, listRuns, type RunStatus, runStatus } from "./run-state";
|
|
24
25
|
import { type AgentView, describeActivity, type RunView, viewRun } from "./run-view";
|
|
25
26
|
|
|
26
|
-
export const runsUsage =
|
|
27
|
-
"usage: awf runs [--limit <n>] [--cwd <directory>] [--run-root <directory>] [--json]",
|
|
28
|
-
" awf runs <run> [--cwd <directory>] [--run-root <directory>] [--json]",
|
|
29
|
-
"",
|
|
30
|
-
"The runs here, newest first, or one run in depth: its attempts, the stages of the last, what",
|
|
31
|
-
"each agent is doing, and what to run next. The list shows 20 unless --limit says another",
|
|
32
|
-
"number. A run is <workflow>/<id>, or an <id> only one",
|
|
33
|
-
"workflow has. Runs are listed under --cwd's .awf/runs, or --run-root, as awf run keeps them; a",
|
|
34
|
-
"run named is found there first, then in each project awf has started a run with events in.",
|
|
35
|
-
"",
|
|
36
|
-
"--json prints one document, the interface for scripts. Exits 0 on any read that worked, 1 when",
|
|
37
|
-
"there is no such run, 2 on a usage error, 3 when the run's files could not be read.",
|
|
38
|
-
].join("\n");
|
|
27
|
+
export const runsUsage = help.trimEnd();
|
|
39
28
|
|
|
40
29
|
const LISTED = 20;
|
|
41
30
|
|
package/src/sandboxes.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { SandboxRecord } from "@agentswf/contract/records";
|
|
|
7
7
|
import type {
|
|
8
8
|
AbsoluteDeadline,
|
|
9
9
|
AgentExecution,
|
|
10
|
+
SandboxEnvironment,
|
|
10
11
|
SandboxEnvironmentKey,
|
|
11
12
|
SandboxRef,
|
|
12
13
|
} from "@agentswf/contract/workflow";
|
|
@@ -141,7 +142,7 @@ export class RunSandboxes {
|
|
|
141
142
|
return this.options.sandboxes?.run !== undefined;
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
/** `
|
|
145
|
+
/** `wf.sandboxes.open`. */
|
|
145
146
|
async open(spec: unknown): Promise<SandboxRef> {
|
|
146
147
|
if (this.hasRunSandbox) throw new Error(RUN_SANDBOX_ONLY);
|
|
147
148
|
const key = (spec as { key?: unknown } | null)?.key;
|
|
@@ -306,7 +307,7 @@ export class RunSandboxes {
|
|
|
306
307
|
network: [...resolved.network],
|
|
307
308
|
...(resolved.sockets.length > 0 ? { sockets: [...resolved.sockets] } : {}),
|
|
308
309
|
...(resolved.webSearch ? { webSearch: true } : {}),
|
|
309
|
-
[provider]: resolved.environment,
|
|
310
|
+
...({ [provider]: resolved.environment } as SandboxEnvironment),
|
|
310
311
|
},
|
|
311
312
|
directory,
|
|
312
313
|
gitdirs: resolved.gitdirs.map((gitdir) => ({ ...gitdir })),
|
package/src/simple-input.ts
CHANGED
|
@@ -19,10 +19,18 @@ type Kind =
|
|
|
19
19
|
| { type: "boolean" }
|
|
20
20
|
| { type: "enum"; values: string[] };
|
|
21
21
|
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Said once in a prompt, under the first block with a free-text flag, and with every refusal of
|
|
24
|
+
* flags that take text. Enum, number and boolean values are shell words as they stand.
|
|
25
|
+
*/
|
|
23
26
|
export const QUOTING_RULE =
|
|
24
27
|
"Put every value in single quotes; a ' inside a value is written '\\''.";
|
|
25
28
|
|
|
29
|
+
/** Whether a flag takes free text, the only value the shell can break. */
|
|
30
|
+
export function takesText(simple: Simple): boolean {
|
|
31
|
+
return simple.some(({ kind }) => kind.type === "string");
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
const MAX_PROPERTIES = 6;
|
|
27
35
|
const NAME = /^[a-z][a-z0-9_]*$/;
|
|
28
36
|
/** An enum value the usage line can show between `|`s and the agent can type as it stands. */
|
|
@@ -137,7 +145,7 @@ export type ReadFlags = { ok: true; value: JsonObject } | { ok: false; error: st
|
|
|
137
145
|
export type ReadGiven = { ok: true; value: unknown } | { ok: false; error: string };
|
|
138
146
|
|
|
139
147
|
/**
|
|
140
|
-
* An input as `
|
|
148
|
+
* An input as `wfa` sent it, as a value: JSON parsed, flags read by the schema, nothing as `{}`.
|
|
141
149
|
* The schema's own check comes after, in `checkGiven`.
|
|
142
150
|
*/
|
|
143
151
|
export function readGiven(schema: JsonSchema | undefined, given: GivenInput): ReadGiven {
|
|
@@ -181,7 +189,9 @@ export function checkGiven(
|
|
|
181
189
|
}
|
|
182
190
|
|
|
183
191
|
function expected(simple: Simple): string {
|
|
184
|
-
return `expected: ${usageLine(simple)}
|
|
192
|
+
return [`expected: ${usageLine(simple)}`, ...(takesText(simple) ? [QUOTING_RULE] : [])].join(
|
|
193
|
+
"\n",
|
|
194
|
+
);
|
|
185
195
|
}
|
|
186
196
|
|
|
187
197
|
/**
|
package/src/stage-ledger.ts
CHANGED
|
@@ -174,7 +174,7 @@ export class StageLedger {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
/**
|
|
177
|
-
* `
|
|
177
|
+
* `wf.stop`, called in `stage` or between stages: what to throw. The first stop is kept,
|
|
178
178
|
* and any later stop throws it again; once it was found caught, that instead. After the
|
|
179
179
|
* workflow's body ended, a stop ends nothing.
|
|
180
180
|
*/
|
package/src/test-command.ts
CHANGED
|
@@ -4,21 +4,12 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { parseDuration } from "./duration";
|
|
6
6
|
import { messageOf } from "./errors";
|
|
7
|
+
import help from "./help/test.txt" with { type: "text" };
|
|
7
8
|
import type { Out } from "./run-commands";
|
|
8
9
|
|
|
9
10
|
const PRELOAD = fileURLToPath(new URL("./workflow-test-preload.ts", import.meta.url));
|
|
10
11
|
|
|
11
|
-
export const testUsage =
|
|
12
|
-
"usage: awf test [paths...] [-t <pattern>] [--watch] [--timeout <duration>]",
|
|
13
|
-
"",
|
|
14
|
-
"Runs a workflow's tests (*.test.ts and *.spec.ts files) under the current directory, or under",
|
|
15
|
-
"each file or directory given, with agentswf/workflow, agentswf/testing and typebox served as",
|
|
16
|
-
"awf run serves them: nothing is installed. -t runs the tests whose names match the pattern;",
|
|
17
|
-
"--watch reruns them on a change; --timeout bounds each test, 5s by default.",
|
|
18
|
-
"",
|
|
19
|
-
"Exits 0 when every test passed, 1 when one failed or none was found, 2 on a usage error. What",
|
|
20
|
-
"it prints is for reading, not parsing.",
|
|
21
|
-
].join("\n");
|
|
12
|
+
export const testUsage = help.trimEnd();
|
|
22
13
|
|
|
23
14
|
export type TestCommand = { paths: string[]; bunArgs: string[] };
|
|
24
15
|
|