@agentswf/engine 0.1.0-beta.1 → 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentswf/engine",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.3",
|
|
4
4
|
"description": "agents.wf: the engine that runs workflows made of coding agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/romanlv/agentswf",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"!src/**/fixtures"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@agentswf/wf": "0.1.0-beta.
|
|
33
|
-
"@agentswf/contract": "0.1.0-beta.
|
|
34
|
-
"@agentswf/harness": "0.1.0-beta.
|
|
35
|
-
"@agentswf/sandbox": "0.1.0-beta.
|
|
32
|
+
"@agentswf/wf": "0.1.0-beta.3",
|
|
33
|
+
"@agentswf/contract": "0.1.0-beta.3",
|
|
34
|
+
"@agentswf/harness": "0.1.0-beta.3",
|
|
35
|
+
"@agentswf/sandbox": "0.1.0-beta.3",
|
|
36
36
|
"typebox": "^1.3.34"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
package/src/allowance-command.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type HarnessAllowance,
|
|
1
|
+
import type {
|
|
2
|
+
AllowanceReport,
|
|
3
|
+
AllowanceWindow,
|
|
4
|
+
HarnessAllowance,
|
|
6
5
|
} from "@agentswf/contract/records";
|
|
7
6
|
import { HARNESS_NAMES, type Harness } from "@agentswf/harness";
|
|
8
7
|
import { PLAN_BASIS, planPrice } from "./accounting/plans";
|
|
8
|
+
import { messageOf } from "./errors";
|
|
9
|
+
import type { Out } from "./run-commands";
|
|
9
10
|
|
|
10
11
|
export const allowanceUsage = [
|
|
11
12
|
"usage: awf allowance [harness...] [--json]",
|
|
@@ -17,7 +18,7 @@ export const allowanceUsage = [
|
|
|
17
18
|
"A plan's monthly price is the vendor's list price as of the table's date, not what was",
|
|
18
19
|
`charged: ${PLAN_BASIS}.`,
|
|
19
20
|
"",
|
|
20
|
-
`--json prints
|
|
21
|
+
`--json prints one document, the interface for scripts, with the plan as the`,
|
|
21
22
|
"harness names it and no price; what it prints otherwise is for reading. A harness that could",
|
|
22
23
|
"not be read says why. Exits 0 once every harness was asked, 2 on a usage error.",
|
|
23
24
|
].join("\n");
|
|
@@ -37,6 +38,27 @@ export function parseAllowanceCommand(argv: readonly string[]): AllowanceCommand
|
|
|
37
38
|
return { harnesses: harnesses.length > 0 ? harnesses : [...HARNESS_NAMES], json };
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/** `awf allowance`: what is left of each harness's plan, read with `read`. */
|
|
42
|
+
export async function allowanceCommand(
|
|
43
|
+
argv: readonly string[],
|
|
44
|
+
io: Out & { read: (harness: Harness) => Promise<HarnessAllowance>; now: () => number },
|
|
45
|
+
): Promise<number> {
|
|
46
|
+
let command: AllowanceCommand;
|
|
47
|
+
try {
|
|
48
|
+
command = parseAllowanceCommand(argv);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
io.stderr(`awf: ${messageOf(error)}\n\n${allowanceUsage}`);
|
|
51
|
+
return 2;
|
|
52
|
+
}
|
|
53
|
+
if (command === "help") {
|
|
54
|
+
io.stdout(allowanceUsage);
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
const report = await readReport(command.harnesses, io.read, io.now());
|
|
58
|
+
io.stdout(command.json ? JSON.stringify(report, null, 2) : describeReport(report));
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
40
62
|
/** Every harness asked at once: cursor's pane takes seconds the others need not wait for. */
|
|
41
63
|
export async function readReport(
|
|
42
64
|
harnesses: readonly Harness[],
|
|
@@ -44,7 +66,7 @@ export async function readReport(
|
|
|
44
66
|
now: number,
|
|
45
67
|
): Promise<AllowanceReport> {
|
|
46
68
|
return {
|
|
47
|
-
|
|
69
|
+
v: 1,
|
|
48
70
|
readAt: new Date(now).toISOString(),
|
|
49
71
|
harnesses: await Promise.all(harnesses.map(read)),
|
|
50
72
|
};
|
package/src/attempt-ending.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { PUBLISHED_PRICES } from "./accounting/prices";
|
|
13
13
|
import { summarizeRun } from "./accounting/summary";
|
|
14
14
|
import { messageOf } from "./errors";
|
|
15
|
+
import { RunParked } from "./mailbox";
|
|
15
16
|
import type { Paint } from "./progress-view";
|
|
16
17
|
import { FromStageUnreached, primaryFailure, WorkflowStopped } from "./stopped";
|
|
17
18
|
import {
|
|
@@ -53,6 +54,7 @@ export const OUTCOMES: Record<
|
|
|
53
54
|
{ word: string; told: string; mark: (paint: Paint) => string; exitCode?: number }
|
|
54
55
|
> = {
|
|
55
56
|
completed: { word: "completed", told: "completed", mark: (p) => p.ok("✓"), exitCode: 0 },
|
|
57
|
+
parked: { word: "parked", told: "parked", mark: (p) => p.ok("◇"), exitCode: 0 },
|
|
56
58
|
stopped: { word: "stopped", told: "stopped", mark: (p) => p.busy("■"), exitCode: 3 },
|
|
57
59
|
cancelled: { word: "cancelled", told: "was cancelled", mark: (p) => p.busy("■") },
|
|
58
60
|
"timed-out": { word: "timed out", told: "timed out", mark: (p) => p.bad("✗"), exitCode: 1 },
|
|
@@ -63,7 +65,11 @@ export const OUTCOMES: Record<
|
|
|
63
65
|
export type GoOn = (stop: WorkflowStopped | undefined, entered: readonly StageSummary[]) => string;
|
|
64
66
|
|
|
65
67
|
/** The ending of a run that returned or threw. */
|
|
66
|
-
export function decideEnding(
|
|
68
|
+
export function decideEnding(
|
|
69
|
+
run: Finished,
|
|
70
|
+
deadline: AbsoluteDeadline | undefined,
|
|
71
|
+
goOn: GoOn,
|
|
72
|
+
): AttemptEnd {
|
|
67
73
|
if ("result" in run) {
|
|
68
74
|
const { result } = run;
|
|
69
75
|
return {
|
|
@@ -74,6 +80,18 @@ export function decideEnding(run: Finished, deadline: AbsoluteDeadline, goOn: Go
|
|
|
74
80
|
}
|
|
75
81
|
const { error } = run;
|
|
76
82
|
const settled = error instanceof WorkflowRunError ? error : undefined;
|
|
83
|
+
const parked = primaryFailure(unwrapped(error));
|
|
84
|
+
if (parked instanceof RunParked) {
|
|
85
|
+
return {
|
|
86
|
+
ending: {
|
|
87
|
+
kind: "parked",
|
|
88
|
+
stages: settled?.stages ?? [],
|
|
89
|
+
...parked.wait,
|
|
90
|
+
},
|
|
91
|
+
...(settled ? { settled } : {}),
|
|
92
|
+
exitCode: OUTCOMES.parked.exitCode!,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
77
95
|
const { kind, stop, cancellation } = runOutcome(error, deadline);
|
|
78
96
|
const cause = unwrapped(error);
|
|
79
97
|
const beside =
|
|
@@ -104,10 +122,10 @@ export function decideEnding(run: Finished, deadline: AbsoluteDeadline, goOn: Go
|
|
|
104
122
|
*/
|
|
105
123
|
export function endedBeforeStart(
|
|
106
124
|
error: unknown,
|
|
107
|
-
deadline: AbsoluteDeadline,
|
|
125
|
+
deadline: AbsoluteDeadline | undefined,
|
|
108
126
|
run: { runId: string; startedAt: number; endedAt: number },
|
|
109
127
|
goOn: GoOn,
|
|
110
|
-
): AttemptEnd & { ending: Exclude<Ending<JsonValue>, { kind: "completed" }> } {
|
|
128
|
+
): AttemptEnd & { ending: Exclude<Ending<JsonValue>, { kind: "completed" | "parked" }> } {
|
|
111
129
|
const { kind, cancellation } = runOutcome(error, deadline);
|
|
112
130
|
const times = {
|
|
113
131
|
startedAt: new Date(run.startedAt).toISOString(),
|
|
@@ -156,14 +174,14 @@ export function cleanupFailed(end: AttemptEnd, error: unknown, goOn: string): At
|
|
|
156
174
|
*/
|
|
157
175
|
export function runOutcome(
|
|
158
176
|
error: unknown,
|
|
159
|
-
deadline: AbsoluteDeadline,
|
|
177
|
+
deadline: AbsoluteDeadline | undefined,
|
|
160
178
|
): { kind: UnfinishedOutcome; stop?: WorkflowStopped; cancellation?: WorkflowCancelledError } {
|
|
161
179
|
const cancellation = findCancellation(error);
|
|
162
180
|
const failure = primaryFailure(unwrapped(error));
|
|
163
181
|
const stop = failure instanceof WorkflowStopped ? failure : undefined;
|
|
164
182
|
const timedOut =
|
|
165
183
|
failure instanceof DeadlineExceededError &&
|
|
166
|
-
failure.deadline.unixMilliseconds === deadline
|
|
184
|
+
failure.deadline.unixMilliseconds === deadline?.unixMilliseconds;
|
|
167
185
|
const kind = cancellation ? "cancelled" : stop ? "stopped" : timedOut ? "timed-out" : "failed";
|
|
168
186
|
return { kind, ...(stop ? { stop } : {}), ...(cancellation ? { cancellation } : {}) };
|
|
169
187
|
}
|
package/src/attempt-output.ts
CHANGED
|
@@ -4,13 +4,15 @@ import {
|
|
|
4
4
|
type OutputRecord,
|
|
5
5
|
RUN_EVENT_VERSIONS,
|
|
6
6
|
} from "@agentswf/contract/records";
|
|
7
|
-
import type { Ending,
|
|
7
|
+
import type { Ending, JsonValue } from "@agentswf/contract/workflow";
|
|
8
8
|
import { attemptAccounting } from "./accounting/summary";
|
|
9
9
|
import type { AttemptEnd } from "./attempt-ending";
|
|
10
10
|
import { messageOf } from "./errors";
|
|
11
11
|
import { writeJson, writeWhole } from "./files";
|
|
12
|
+
import { foldMailbox, readMailbox } from "./mailbox";
|
|
12
13
|
import { type Attempt, appendEvent, endAttempt, outputFile, reportFile } from "./runs";
|
|
13
14
|
import { withoutValue } from "./stage-ledger";
|
|
15
|
+
import type { AnyWorkflow, LoadedWorkflow } from "./workflow-loader";
|
|
14
16
|
|
|
15
17
|
/** Where an attempt's records went, and its output record as `--json` prints it. */
|
|
16
18
|
export type Kept = {
|
|
@@ -30,27 +32,34 @@ export async function keepRecords(
|
|
|
30
32
|
end: AttemptEnd,
|
|
31
33
|
context: {
|
|
32
34
|
attempt: Attempt;
|
|
33
|
-
|
|
34
|
-
workflow: { name: string; file: string };
|
|
35
|
+
loaded: LoadedWorkflow;
|
|
35
36
|
stderr: (text: string) => void;
|
|
37
|
+
/** An event run's: runs `work` holding its run's lock. */
|
|
38
|
+
locked?: (work: () => Promise<void>) => Promise<void>;
|
|
36
39
|
},
|
|
37
40
|
): Promise<Kept> {
|
|
38
41
|
const { attempt, stderr } = context;
|
|
39
42
|
const { ending, settled } = end;
|
|
40
43
|
const dir = attempt.run.dir;
|
|
41
|
-
const report = await writeReport(context.
|
|
44
|
+
const report = await writeReport(context.loaded.workflow, ending, dir, stderr);
|
|
42
45
|
const stages = settled?.stages?.map(withoutValue);
|
|
43
|
-
const
|
|
44
|
-
|
|
46
|
+
const unfinished = ending.kind !== "completed" && ending.kind !== "parked" ? ending : undefined;
|
|
47
|
+
const at = unfinished?.stage !== undefined ? { stage: unfinished.stage } : {};
|
|
45
48
|
const ended =
|
|
46
49
|
ending.kind === "completed"
|
|
47
50
|
? { outcome: "completed" as const, value: ending.value }
|
|
48
|
-
:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
: ending.kind === "parked"
|
|
52
|
+
? {
|
|
53
|
+
outcome: "parked" as const,
|
|
54
|
+
...(ending.label === undefined ? {} : { label: ending.label }),
|
|
55
|
+
...(ending.until === undefined ? {} : { until: ending.until }),
|
|
56
|
+
}
|
|
57
|
+
: {
|
|
58
|
+
outcome: ending.kind,
|
|
59
|
+
reason: ending.reason,
|
|
60
|
+
...at,
|
|
61
|
+
...(end.needs ? { needs: [...end.needs] } : {}),
|
|
62
|
+
};
|
|
54
63
|
const kept: Kept = report ? { report } : {};
|
|
55
64
|
const output = outputFile(dir);
|
|
56
65
|
if (!settled) {
|
|
@@ -63,7 +72,7 @@ export async function keepRecords(
|
|
|
63
72
|
version: OUTPUT_RECORD_VERSION,
|
|
64
73
|
runId: settled.runId,
|
|
65
74
|
attempt: attempt.record.attempt,
|
|
66
|
-
workflow: context.workflow,
|
|
75
|
+
workflow: { name: context.loaded.workflow.meta.name, file: context.loaded.file },
|
|
67
76
|
accounting: settled.accounting,
|
|
68
77
|
usage: settled.usage,
|
|
69
78
|
artifacts: dir,
|
|
@@ -91,14 +100,35 @@ export async function keepRecords(
|
|
|
91
100
|
kind: "attempt-ended",
|
|
92
101
|
outcome: ending.kind,
|
|
93
102
|
}).catch((error) => stderr(`awf: the run's event log was not written: ${messageOf(error)}`));
|
|
94
|
-
|
|
103
|
+
// An event run's ending is written holding its run's lock, as parking's is: a sender after it sees
|
|
104
|
+
// the run returned and is refused, so what no wait took is counted whole. Its last batch was
|
|
105
|
+
// handled, as the code returned after it.
|
|
106
|
+
const writeEnding = async () => {
|
|
107
|
+
const unacknowledged =
|
|
108
|
+
ending.kind === "completed" && attempt.run.record.events
|
|
109
|
+
? await readMailbox(dir).then(
|
|
110
|
+
// The event that started the run is `workflow.started`'s, taken or not.
|
|
111
|
+
(records) => {
|
|
112
|
+
const state = foldMailbox(records);
|
|
113
|
+
return state.unacknowledged.filter((id) => id !== state.started?.id).length;
|
|
114
|
+
},
|
|
115
|
+
(error: unknown) => {
|
|
116
|
+
stderr(`awf: what no wait took was not counted: ${messageOf(error)}`);
|
|
117
|
+
return undefined;
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
: undefined;
|
|
95
121
|
await endAttempt(attempt, {
|
|
96
122
|
outcome: ending.kind,
|
|
97
|
-
...(
|
|
123
|
+
...(unacknowledged === undefined ? {} : { unacknowledged }),
|
|
124
|
+
...(unfinished ? { reason: unfinished.reason } : {}),
|
|
98
125
|
...at,
|
|
99
126
|
...(stages ? { stages } : {}),
|
|
100
127
|
...(settled ? { accounting: attemptAccounting(settled.accounting) } : {}),
|
|
101
128
|
});
|
|
129
|
+
};
|
|
130
|
+
try {
|
|
131
|
+
await (context.locked ?? ((work: () => Promise<void>) => work()))(writeEnding);
|
|
102
132
|
} catch (error) {
|
|
103
133
|
stderr(`awf: the attempt's ending was not written to ${attempt.file}: ${messageOf(error)}`);
|
|
104
134
|
}
|
|
@@ -106,7 +136,7 @@ export async function keepRecords(
|
|
|
106
136
|
}
|
|
107
137
|
|
|
108
138
|
async function writeReport(
|
|
109
|
-
|
|
139
|
+
workflow: AnyWorkflow,
|
|
110
140
|
ending: Ending<JsonValue>,
|
|
111
141
|
dir: string,
|
|
112
142
|
stderr: (text: string) => void,
|
|
@@ -114,11 +144,11 @@ async function writeReport(
|
|
|
114
144
|
const file = reportFile(dir);
|
|
115
145
|
// An earlier attempt's report is gone with it: report.md is the last ended attempt's.
|
|
116
146
|
const none = () => rm(file, { force: true }).then(() => undefined);
|
|
117
|
-
if (!
|
|
147
|
+
if (!workflow.report) return none();
|
|
118
148
|
// The result is still in output.json; a report that cannot be rendered or saved should not fail
|
|
119
149
|
// the run, nor cost it its record.
|
|
120
150
|
try {
|
|
121
|
-
const markdown =
|
|
151
|
+
const markdown = workflow.report(
|
|
122
152
|
ending.kind === "completed" ? ending.value : undefined,
|
|
123
153
|
ending,
|
|
124
154
|
);
|
package/src/attempt-view.ts
CHANGED
|
@@ -5,12 +5,7 @@ import type {
|
|
|
5
5
|
StageNeed,
|
|
6
6
|
StageRecord,
|
|
7
7
|
} from "@agentswf/contract/records";
|
|
8
|
-
import type {
|
|
9
|
-
Ending,
|
|
10
|
-
ExecutableWorkflow,
|
|
11
|
-
JsonValue,
|
|
12
|
-
StageSummary,
|
|
13
|
-
} from "@agentswf/contract/workflow";
|
|
8
|
+
import type { Ending, JsonValue, StageSummary } from "@agentswf/contract/workflow";
|
|
14
9
|
import { ago, describeAccounting, describeAttempts } from "./accounting/format";
|
|
15
10
|
import { sumAttempts } from "./accounting/summary";
|
|
16
11
|
import { type AttemptEnd, OUTCOMES } from "./attempt-ending";
|
|
@@ -22,6 +17,7 @@ import type { RunCommand } from "./run-command";
|
|
|
22
17
|
import type { AttemptRefusal } from "./runs";
|
|
23
18
|
import { schemaLines } from "./stage-schema";
|
|
24
19
|
import { FromStageUnreached, type WorkflowStopped } from "./stopped";
|
|
20
|
+
import type { AnyWorkflow } from "./workflow-loader";
|
|
25
21
|
|
|
26
22
|
/**
|
|
27
23
|
* Prints how the attempt ended, and returns its exit code. A completed one's result goes to stdout;
|
|
@@ -32,7 +28,7 @@ export function printEnding(
|
|
|
32
28
|
end: AttemptEnd,
|
|
33
29
|
kept: Kept,
|
|
34
30
|
context: {
|
|
35
|
-
|
|
31
|
+
workflow: AnyWorkflow;
|
|
36
32
|
/** The run's id and folder. */
|
|
37
33
|
run: { id: string; dir: string };
|
|
38
34
|
n: number;
|
|
@@ -56,7 +52,7 @@ export function printEnding(
|
|
|
56
52
|
const { ending, settled } = end;
|
|
57
53
|
const presented =
|
|
58
54
|
ending.kind === "completed" && !context.json
|
|
59
|
-
? present(context.
|
|
55
|
+
? present(context.workflow, ending, stderr)
|
|
60
56
|
: undefined;
|
|
61
57
|
if (end.alsoFailed) stderr(`awf: also failed: ${end.alsoFailed}`);
|
|
62
58
|
const again =
|
|
@@ -67,7 +63,7 @@ export function printEnding(
|
|
|
67
63
|
if (context.cleanupAlso) stderr(`awf: runtime cleanup also failed: ${context.cleanupAlso}`);
|
|
68
64
|
// Beside the result rather than in it: stdout stays the workflow's report or the JSON.
|
|
69
65
|
const lines = describeEnding(ending, {
|
|
70
|
-
named: `${context.
|
|
66
|
+
named: `${context.workflow.meta.name} ${context.run.id}`,
|
|
71
67
|
n: context.n,
|
|
72
68
|
...(settled ? { accounting: settled.accounting } : {}),
|
|
73
69
|
earlier: context.earlier,
|
|
@@ -78,7 +74,7 @@ export function printEnding(
|
|
|
78
74
|
...(end.needs ? { needs: end.needs } : {}),
|
|
79
75
|
...(settled?.panes ? { panes: settled.panes } : {}),
|
|
80
76
|
// As the run's Herdr workspace is labelled.
|
|
81
|
-
runWorkspace: `awf ${context.
|
|
77
|
+
runWorkspace: `awf ${context.workflow.meta.name} ${context.run.id} #${context.n}`,
|
|
82
78
|
paint: context.terminal?.color ? ANSI : PLAIN,
|
|
83
79
|
});
|
|
84
80
|
for (const line of ["", ...lines]) stderr(line);
|
|
@@ -91,7 +87,9 @@ export function printEnding(
|
|
|
91
87
|
export function toldOf(end: AttemptEnd, kept: Kept): string {
|
|
92
88
|
const { ending } = end;
|
|
93
89
|
const told = `${OUTCOMES[ending.kind].told}${kept.output ? `; its record is ${kept.output}` : ""}`;
|
|
94
|
-
|
|
90
|
+
if (ending.kind === "completed") return told;
|
|
91
|
+
if (ending.kind === "parked") return ending.label ? `${told}: ${ending.label}` : told;
|
|
92
|
+
return `${told}: ${ending.reason}`;
|
|
95
93
|
}
|
|
96
94
|
|
|
97
95
|
/**
|
|
@@ -135,6 +133,17 @@ function describeEnding(
|
|
|
135
133
|
if (ending.kind === "completed") {
|
|
136
134
|
return [`${mark} ${word} · ${run}`, ...figures, ...rows([], context)];
|
|
137
135
|
}
|
|
136
|
+
if (ending.kind === "parked") {
|
|
137
|
+
const waits = [ending.label, ending.until && `until ${ending.until}`]
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.join(" · ");
|
|
140
|
+
return [
|
|
141
|
+
`${mark} ${word}${waits ? `: ${waits}` : ""}`,
|
|
142
|
+
` ${run}`,
|
|
143
|
+
...figures,
|
|
144
|
+
...rows([], context),
|
|
145
|
+
];
|
|
146
|
+
}
|
|
138
147
|
// A reason's later lines, such as a schema's problems, sit under it, apart from the rows.
|
|
139
148
|
const [first, ...more] = ending.reason.split("\n");
|
|
140
149
|
const goOn = context.choose
|
|
@@ -233,13 +242,13 @@ function runTotal(n: number, current: RunAccounting, earlier: readonly AttemptRe
|
|
|
233
242
|
|
|
234
243
|
/** A completed attempt's value as the workflow presents it; undefined to print the JSON. */
|
|
235
244
|
function present(
|
|
236
|
-
|
|
245
|
+
workflow: AnyWorkflow,
|
|
237
246
|
ending: Extract<Ending<JsonValue>, { kind: "completed" }>,
|
|
238
247
|
stderr: (text: string) => void,
|
|
239
248
|
): string | undefined {
|
|
240
|
-
if (!
|
|
249
|
+
if (!workflow.present) return undefined;
|
|
241
250
|
try {
|
|
242
|
-
return
|
|
251
|
+
return workflow.present(ending.value, ending)?.trimEnd();
|
|
243
252
|
} catch (error) {
|
|
244
253
|
stderr(`awf: present: ${messageOf(error)}; printing the full result instead`);
|
|
245
254
|
return undefined;
|
|
@@ -343,8 +352,9 @@ export function continueCommand(
|
|
|
343
352
|
...(command.runRootGiven ? ["--run-root", command.runRoot] : []),
|
|
344
353
|
...(command.cwdGiven ? ["--cwd", command.cwd] : []),
|
|
345
354
|
pathFrom(command.shellCwd, command.workflowFile),
|
|
346
|
-
"--
|
|
355
|
+
"--id",
|
|
347
356
|
id,
|
|
357
|
+
"--continue",
|
|
348
358
|
...(redo === undefined ? [] : ["--from-stage", redo]),
|
|
349
359
|
...(values === undefined ? [] : ["--values", values]),
|
|
350
360
|
]
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { allowanceUsage } from "./allowance-command";
|
|
2
|
+
import { emitUsage } from "./emit-command";
|
|
3
|
+
import { replyUsage } from "./holder-command";
|
|
4
|
+
import { inboxUsage } from "./inbox-command";
|
|
5
|
+
import { initUsage } from "./init-command";
|
|
6
|
+
import { logsUsage } from "./logs-command";
|
|
7
|
+
import { usage as runUsage } from "./run-command";
|
|
8
|
+
import { shellLine } from "./run-commands";
|
|
9
|
+
import { runsUsage } from "./runs-command";
|
|
10
|
+
import { testUsage } from "./test-command";
|
|
11
|
+
import { tickUsage } from "./tick-command";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Every `awf` command: what the index, `awf help` and the suggestion read, and what the dispatch is
|
|
15
|
+
* keyed by. An entry's commands share its help; each has its own line in the index.
|
|
16
|
+
*/
|
|
17
|
+
export const COMMANDS = [
|
|
18
|
+
{
|
|
19
|
+
group: "Run a workflow",
|
|
20
|
+
commands: { run: "Start a run of a workflow file, or continue one" },
|
|
21
|
+
help: runUsage,
|
|
22
|
+
},
|
|
23
|
+
{ group: "Run a workflow", commands: { test: "Run a workflow's tests" }, help: testUsage },
|
|
24
|
+
{
|
|
25
|
+
group: "Run a workflow",
|
|
26
|
+
commands: { init: "Set up a directory for writing workflows" },
|
|
27
|
+
help: initUsage,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
group: "Run a workflow",
|
|
31
|
+
commands: { emit: "Send an event to a workflow with events, starting or waking its run" },
|
|
32
|
+
help: emitUsage,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
group: "Run a workflow",
|
|
36
|
+
commands: { tick: "Wake the runs of workflows with events that are due" },
|
|
37
|
+
help: tickUsage,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
group: "Look at runs",
|
|
41
|
+
commands: { runs: "The runs here, or one run in depth" },
|
|
42
|
+
help: runsUsage,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
group: "Look at runs",
|
|
46
|
+
commands: { logs: "What a run did and is doing, merged by time" },
|
|
47
|
+
help: logsUsage,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
group: "Coordinate a run from outside",
|
|
51
|
+
commands: { inbox: "What agents said to a participant outside the run, and what it said back" },
|
|
52
|
+
help: inboxUsage,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
group: "Coordinate a run from outside",
|
|
56
|
+
commands: { reply: "Answer a question an agent asked", send: "Message an agent first" },
|
|
57
|
+
help: replyUsage,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
group: "This machine",
|
|
61
|
+
commands: { allowance: "What is left of each harness's plan" },
|
|
62
|
+
help: allowanceUsage,
|
|
63
|
+
},
|
|
64
|
+
] as const;
|
|
65
|
+
|
|
66
|
+
type Entry = (typeof COMMANDS)[number];
|
|
67
|
+
type NamesOf<Each> = Each extends { commands: infer Commands } ? keyof Commands & string : never;
|
|
68
|
+
export type CommandName = NamesOf<Entry>;
|
|
69
|
+
|
|
70
|
+
const NAMES = COMMANDS.flatMap((entry) => Object.keys(entry.commands)) as CommandName[];
|
|
71
|
+
|
|
72
|
+
export function isCommand(word: string): word is CommandName {
|
|
73
|
+
return (NAMES as string[]).includes(word);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The help `awf help {name}` and `awf {name} -h` print. */
|
|
77
|
+
export function helpOf(name: CommandName): string {
|
|
78
|
+
return COMMANDS.find((entry) => name in entry.commands)!.help;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const WIDTH = Math.max(...NAMES.map((name) => name.length)) + 2;
|
|
82
|
+
|
|
83
|
+
function grouped(): [group: string, entries: Entry[]][] {
|
|
84
|
+
const groups = new Map<string, Entry[]>();
|
|
85
|
+
for (const entry of COMMANDS)
|
|
86
|
+
groups.set(entry.group, [...(groups.get(entry.group) ?? []), entry]);
|
|
87
|
+
return [...groups];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const lines = (entry: Entry) =>
|
|
91
|
+
Object.entries(entry.commands).map(([name, summary]) => `${name.padEnd(WIDTH)}${summary}`);
|
|
92
|
+
|
|
93
|
+
/** What `awf`, `awf -h` and `awf help` print. */
|
|
94
|
+
export function index(): string {
|
|
95
|
+
return [
|
|
96
|
+
"awf runs workflows of coding agents.",
|
|
97
|
+
"",
|
|
98
|
+
"usage: awf <command> [arguments]",
|
|
99
|
+
"",
|
|
100
|
+
...grouped().flatMap(([group, entries]) => [
|
|
101
|
+
group,
|
|
102
|
+
...entries.flatMap(lines).map((line) => ` ${line}`),
|
|
103
|
+
"",
|
|
104
|
+
]),
|
|
105
|
+
"awf <command> -h a command's usage, options and exit codes",
|
|
106
|
+
"awf help reference every command's usage, on one page",
|
|
107
|
+
"awf --version",
|
|
108
|
+
].join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A command's usage lines: its help up to the first blank line. */
|
|
112
|
+
export function usageLines(help: string): string[] {
|
|
113
|
+
const end = help.indexOf("\n\n");
|
|
114
|
+
return (end < 0 ? help : help.slice(0, end)).split("\n");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** What `awf help reference` prints: each command's summary and usage lines, grouped as the index. */
|
|
118
|
+
export function reference(): string {
|
|
119
|
+
return [
|
|
120
|
+
...grouped().flatMap(([group, entries]) => [
|
|
121
|
+
group,
|
|
122
|
+
...entries.flatMap((entry) => [
|
|
123
|
+
...lines(entry).map((line) => ` ${line}`),
|
|
124
|
+
...usageLines(entry.help).map((line) => ` ${line}`),
|
|
125
|
+
"",
|
|
126
|
+
]),
|
|
127
|
+
]),
|
|
128
|
+
"awf <command> -h for what each does, and its exit codes",
|
|
129
|
+
].join("\n");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const WORKFLOW_FILE = /\.(ts|mts|js|mjs)$/;
|
|
133
|
+
|
|
134
|
+
/** The one line `awf {word}` gets when no command is named `word`. */
|
|
135
|
+
export function unknownCommand(word: string): string {
|
|
136
|
+
const said = `awf: no command ${JSON.stringify(word)}`;
|
|
137
|
+
if (WORKFLOW_FILE.test(word)) return `${said}; did you mean: ${shellLine(["awf", "run", word])}`;
|
|
138
|
+
const meant = suggest(word);
|
|
139
|
+
const named =
|
|
140
|
+
meant.length === 0
|
|
141
|
+
? ""
|
|
142
|
+
: ` did you mean ${meant.length === 1 ? meant[0] : `${meant.slice(0, -1).join(", ")} or ${meant.at(-1)}`}?`;
|
|
143
|
+
return `${said};${named} awf -h lists the commands`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The commands `word` is the start of, else the closest within two edits; every one tied for
|
|
148
|
+
* closest.
|
|
149
|
+
*/
|
|
150
|
+
export function suggest(word: string): CommandName[] {
|
|
151
|
+
if (word === "") return [];
|
|
152
|
+
const started = NAMES.filter((name) => name.startsWith(word));
|
|
153
|
+
if (started.length > 0) return started;
|
|
154
|
+
const near = NAMES.map((name) => ({ name, distance: distance(word, name) })).filter(
|
|
155
|
+
(each) => each.distance <= 2,
|
|
156
|
+
);
|
|
157
|
+
const closest = Math.min(...near.map((each) => each.distance));
|
|
158
|
+
return near.filter((each) => each.distance === closest).map((each) => each.name);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Levenshtein distance. */
|
|
162
|
+
function distance(a: string, b: string): number {
|
|
163
|
+
let previous = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
164
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
165
|
+
const current = [i];
|
|
166
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
167
|
+
current[j] = Math.min(
|
|
168
|
+
previous[j]! + 1,
|
|
169
|
+
current[j - 1]! + 1,
|
|
170
|
+
previous[j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
previous = current;
|
|
174
|
+
}
|
|
175
|
+
return previous[b.length]!;
|
|
176
|
+
}
|