@agentswf/engine 0.1.0-beta.4 → 0.1.0
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/LICENSE +21 -0
- package/README.md +16 -0
- package/package.json +13 -9
- package/src/accounting/format.ts +4 -3
- package/src/accounting/spent.ts +128 -0
- package/src/accounting/summary.ts +40 -32
- package/src/agent-cli/cli.ts +35 -22
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +5 -7
- package/src/attempt-output.ts +15 -32
- package/src/attempt-view.ts +34 -33
- package/src/author-typebox.ts +13 -0
- package/src/client/checks.ts +94 -0
- package/src/client/exported.ts +17 -0
- package/src/client/index.ts +122 -0
- package/src/client/locate.ts +34 -0
- package/src/client/published.ts +3 -0
- package/src/client/runs.ts +337 -0
- package/src/command-line.ts +64 -0
- package/src/commands.ts +7 -1
- package/src/control-plane.ts +41 -11
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +41 -33
- package/src/deadlines.ts +40 -7
- package/src/decisions/directory.ts +51 -40
- package/src/decisions/fake.ts +1 -1
- package/src/decisions/openrouter.ts +4 -2
- package/src/decisions/seam.ts +4 -4
- package/src/display-path.ts +3 -3
- package/src/duration.ts +43 -27
- package/src/emit-command.ts +88 -58
- package/src/emit.ts +254 -56
- package/src/event-ids.ts +31 -0
- package/src/files.ts +44 -4
- package/src/hand-over.ts +150 -0
- package/src/help/allowance.txt +4 -3
- package/src/help/emit.txt +18 -6
- package/src/help/inbox.txt +6 -4
- package/src/help/logs.txt +9 -6
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +30 -11
- package/src/help/runs.txt +4 -2
- package/src/help/tick.txt +9 -4
- package/src/herdr-placement.ts +3 -33
- package/src/herdr-run-session.ts +7 -15
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +56 -133
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +40 -57
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.test.ts.template +3 -3
- package/src/init-template/hello/workflow.ts.template +5 -4
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +15 -12
- package/src/logs-command.ts +128 -47
- package/src/message-operation.ts +6 -3
- package/src/messaging/limits.ts +4 -4
- package/src/messaging/messaging.ts +31 -28
- package/src/messaging/routes.ts +1 -1
- package/src/messaging/text.ts +9 -7
- package/src/open-command.ts +770 -0
- package/src/operation-events.ts +4 -6
- package/src/operation-liveness.ts +19 -4
- package/src/operator-cli.ts +224 -94
- package/src/operator-runtime.ts +29 -7
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +105 -59
- package/src/progress-view.ts +10 -10
- package/src/project-engine.ts +79 -0
- package/src/projects.ts +13 -2
- package/src/refusals.ts +119 -0
- package/src/result-slots.ts +11 -31
- package/src/result-validation.ts +2 -2
- package/src/run-command.ts +45 -28
- package/src/run-commands.ts +43 -115
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/{stage-plan.ts → run-policy/stage-plan.ts} +46 -42
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +205 -43
- package/src/run-read.ts +109 -41
- package/src/run-state.ts +170 -50
- package/src/run-text.ts +54 -0
- package/src/run-unreachable.ts +18 -0
- package/src/run-usage.ts +18 -12
- package/src/run-view.ts +81 -17
- package/src/runs-command.ts +83 -63
- package/src/runs.ts +310 -212
- package/src/sandbox-homes.ts +65 -13
- package/src/sandboxes.ts +182 -28
- package/src/session-lines.ts +93 -0
- package/src/skills/run-skills.ts +8 -1
- package/src/sources.ts +28 -29
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +119 -131
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +36 -14
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +73 -26
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +25 -16
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +54 -4
- package/src/workflow-runner.ts +805 -580
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +3 -3
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +121 -53
- package/src/workflow-testing/index.ts +33 -842
- package/src/workflow-testing/run-test.ts +1128 -0
- package/src/workflow-testing/script.ts +74 -45
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -990
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- package/src/workflow-testing/workflow-testing.typecheck.ts +0 -64
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import manifest from "../package.json" with { type: "json" };
|
|
3
|
+
import {
|
|
4
|
+
type InstalledEngine,
|
|
5
|
+
installedEngine,
|
|
6
|
+
type ProjectEngine,
|
|
7
|
+
projectEngine,
|
|
8
|
+
} from "./hand-over";
|
|
9
|
+
|
|
10
|
+
/** This engine's version, which every package of a release shares. */
|
|
11
|
+
export const ENGINE_VERSION: string = manifest.version;
|
|
12
|
+
|
|
13
|
+
/** Why an engine won't run a project's runs: `other-engine` or `no-engine`, as a client says it. */
|
|
14
|
+
export class EngineRefused extends Error {
|
|
15
|
+
constructor(
|
|
16
|
+
readonly code: "other-engine" | "no-engine",
|
|
17
|
+
message: string,
|
|
18
|
+
) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "EngineRefused";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The engine a project's runs run on (ADR 0015): the `agentswf` installed above it; else, for a
|
|
26
|
+
* project with none above it, the one its workflow files resolve, as in a workspace whose root
|
|
27
|
+
* holds `.awf/` and whose packages each have a `node_modules`. Files that resolve two versions, or
|
|
28
|
+
* one other than the project's, are refused: one project runs on one engine.
|
|
29
|
+
*/
|
|
30
|
+
export function engineOf(project: string, files: readonly string[] = []): ProjectEngine {
|
|
31
|
+
const engine = projectEngine(project);
|
|
32
|
+
let fromFiles: (InstalledEngine & { file: string }) | undefined;
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const found = installedEngine(dirname(file));
|
|
35
|
+
if (!found) continue;
|
|
36
|
+
const other = engine.kind === "installed" ? engine : fromFiles;
|
|
37
|
+
if (other && other.version !== found.version)
|
|
38
|
+
throw new EngineRefused(
|
|
39
|
+
"other-engine",
|
|
40
|
+
`${file} resolves agentswf ${found.version}, from ${found.root}, and ${engine.kind === "installed" ? `its project ${project}` : (fromFiles?.file ?? project)} ${other.version}: one project runs on one engine`,
|
|
41
|
+
);
|
|
42
|
+
fromFiles ??= { ...found, file };
|
|
43
|
+
}
|
|
44
|
+
if (engine.kind === "installed" || !fromFiles) return engine;
|
|
45
|
+
const { file: _file, ...installed } = fromFiles;
|
|
46
|
+
return { kind: "installed", ...installed };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Refuses `project` unless this engine is the one its runs run on, as `engineOf` finds it with
|
|
51
|
+
* `file`: at this version, or, for a project with none, any engine but a client's.
|
|
52
|
+
*/
|
|
53
|
+
export function checkProjectEngine(
|
|
54
|
+
project: string,
|
|
55
|
+
options: { file?: string; files?: readonly string[]; client?: boolean; version?: string } = {},
|
|
56
|
+
): void {
|
|
57
|
+
const own = options.version ?? ENGINE_VERSION;
|
|
58
|
+
const engine = engineOf(project, [
|
|
59
|
+
...(options.file === undefined ? [] : [options.file]),
|
|
60
|
+
...(options.files ?? []),
|
|
61
|
+
]);
|
|
62
|
+
if (engine.kind === "declared")
|
|
63
|
+
throw new EngineRefused(
|
|
64
|
+
"no-engine",
|
|
65
|
+
`${project} declares agentswf in ${engine.manifest} and has none installed: run bun install there`,
|
|
66
|
+
);
|
|
67
|
+
if (engine.kind === "installed" && engine.version !== own)
|
|
68
|
+
throw new EngineRefused(
|
|
69
|
+
"other-engine",
|
|
70
|
+
options.client
|
|
71
|
+
? `${project} runs on its agentswf ${engine.version}, and this program's agentswf/client is ${own}: install one version in both`
|
|
72
|
+
: `${project} runs on its agentswf ${engine.version}, and this awf is ${own}: run the project's, bunx awf, from ${engine.root}`,
|
|
73
|
+
);
|
|
74
|
+
if (engine.kind === "none" && options.client)
|
|
75
|
+
throw new EngineRefused(
|
|
76
|
+
"no-engine",
|
|
77
|
+
`${project} has no agentswf installed, so it runs on whichever global awf is installed: bun add -d agentswf there`,
|
|
78
|
+
);
|
|
79
|
+
}
|
package/src/projects.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { realPath } from "./files";
|
|
4
4
|
import { appendLine } from "./jsonl";
|
|
5
5
|
import { withRunLock } from "./run-lock";
|
|
@@ -34,7 +34,7 @@ export function projectRunRoot(project: string): string {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/** `~/.awf/projects`: each project awf has started an event run in, one absolute path a line. */
|
|
37
|
-
|
|
37
|
+
function projectsFile(home: string): string {
|
|
38
38
|
return join(home, ".awf", "projects");
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -55,3 +55,14 @@ export async function recordProject(home: string, project: string): Promise<void
|
|
|
55
55
|
await appendLine(file, project);
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The project a run root's runs belong to: the folder holding it as its `.awf/runs`, else, under a
|
|
61
|
+
* `--run-root` of the operator's, the directory the run works in.
|
|
62
|
+
*/
|
|
63
|
+
export function rootProject(runRoot: string, cwd: string): string {
|
|
64
|
+
const root = resolve(runRoot);
|
|
65
|
+
return basename(root) === "runs" && basename(dirname(root)) === ".awf"
|
|
66
|
+
? dirname(dirname(root))
|
|
67
|
+
: cwd;
|
|
68
|
+
}
|
package/src/refusals.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { ClientError, type ClientErrorCode } from "@agentswf/contract/client";
|
|
2
|
+
import { REFUSAL_CODES, type RefusalCode, type RefusalJson } from "@agentswf/contract/records";
|
|
3
|
+
import type { InboxCode, ParticipantCode } from "@agentswf/contract/wire";
|
|
4
|
+
import { EmitError } from "./emit";
|
|
5
|
+
import { messageOf } from "./errors";
|
|
6
|
+
import { EngineRefused } from "./project-engine";
|
|
7
|
+
import { RunNotFound } from "./run-state";
|
|
8
|
+
import { isSystemError, RunUnreachable } from "./run-unreachable";
|
|
9
|
+
import { RunRefused } from "./runs";
|
|
10
|
+
import { WorkflowFileError } from "./workflow-loader";
|
|
11
|
+
|
|
12
|
+
/** Every code a command refuses with. */
|
|
13
|
+
type CommandCode = RefusalCode | ParticipantCode | InboxCode;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The codes only a command prints: a program's call has no argv and reads no run's socket, and
|
|
17
|
+
* `internal-error` is the command's word for an error with no code, which the client throws as it
|
|
18
|
+
* is.
|
|
19
|
+
*/
|
|
20
|
+
const COMMAND_ONLY = new Set<RefusalCode>(
|
|
21
|
+
Object.keys({
|
|
22
|
+
usage: true,
|
|
23
|
+
ambiguous: true,
|
|
24
|
+
"not-started": true,
|
|
25
|
+
unreachable: true,
|
|
26
|
+
"internal-error": true,
|
|
27
|
+
"no-session": true,
|
|
28
|
+
live: true,
|
|
29
|
+
opened: true,
|
|
30
|
+
// Every code a client doesn't throw, so a new one is either a client's or listed here.
|
|
31
|
+
} satisfies Record<Exclude<RefusalCode, ClientErrorCode>, true>) as RefusalCode[],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The code an engine error is refused with, by a `--json` command and as a `ClientError` alike, so
|
|
36
|
+
* the two can't drift; none for an error that isn't the caller's.
|
|
37
|
+
*/
|
|
38
|
+
function refusalCode(error: unknown): RefusalCode | undefined {
|
|
39
|
+
return error instanceof EmitError ||
|
|
40
|
+
error instanceof EngineRefused ||
|
|
41
|
+
error instanceof RunNotFound ||
|
|
42
|
+
error instanceof WorkflowFileError ||
|
|
43
|
+
error instanceof RunUnreachable ||
|
|
44
|
+
error instanceof RunRefused
|
|
45
|
+
? error.code
|
|
46
|
+
: undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `code` as a client's word, when it is one: what a command refused with, read back. */
|
|
50
|
+
export function clientCode(code: unknown): ClientErrorCode | undefined {
|
|
51
|
+
const known = (REFUSAL_CODES as readonly unknown[]).includes(code);
|
|
52
|
+
return known && !COMMAND_ONLY.has(code as RefusalCode) ? (code as ClientErrorCode) : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A failure while reading a run: the error's own code; else a system error, a file that couldn't
|
|
57
|
+
* be read, `unreachable`; else `internal-error`.
|
|
58
|
+
*/
|
|
59
|
+
export function readFailureCode(error: unknown): RefusalCode {
|
|
60
|
+
const cause = error instanceof Error ? error.cause : undefined;
|
|
61
|
+
return (
|
|
62
|
+
refusalCode(error) ??
|
|
63
|
+
(cause === undefined ? undefined : refusalCode(cause)) ??
|
|
64
|
+
(isSystemError(error) ? "unreachable" : "internal-error")
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** An engine error that is the caller's mistake as a `ClientError`; anything else as it is. */
|
|
69
|
+
export function asClientError(error: unknown, ref?: string): unknown {
|
|
70
|
+
const code = refusalCode(error);
|
|
71
|
+
if (code === undefined || COMMAND_ONLY.has(code)) return error;
|
|
72
|
+
return new ClientError(code as ClientErrorCode, messageOf(error), ref);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A failure as a `--json` command prints it: the sentence on stderr, for people, and with `json`
|
|
77
|
+
* the refusal as stdout's last document. The code is the error's own, as `refusalCode` gives it;
|
|
78
|
+
* `otherwise` is the command's word for an error that has none. A caller's mistake never reaches a
|
|
79
|
+
* host as `internal-error`: it gets an error class `refusalCode` reads. `usage` follows the
|
|
80
|
+
* sentence.
|
|
81
|
+
*/
|
|
82
|
+
export function printRefusal(
|
|
83
|
+
out: { stdout(text: string): void; stderr(text: string): void },
|
|
84
|
+
json: { json: boolean; v: number },
|
|
85
|
+
error: unknown,
|
|
86
|
+
otherwise: CommandCode,
|
|
87
|
+
usage?: string,
|
|
88
|
+
): void {
|
|
89
|
+
const sentence = messageOf(error);
|
|
90
|
+
if (json.json) out.stdout(refusalLine(json.v, refusalCode(error) ?? otherwise, sentence));
|
|
91
|
+
out.stderr(`awf: ${sentence}${usage === undefined ? "" : `\n\n${usage}`}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A refusal as its one line on stdout, for a command whose stderr says it in its own words. */
|
|
95
|
+
export function refusalLine(
|
|
96
|
+
v: number,
|
|
97
|
+
code: CommandCode,
|
|
98
|
+
error: string,
|
|
99
|
+
choices?: string[],
|
|
100
|
+
): string {
|
|
101
|
+
return JSON.stringify({
|
|
102
|
+
v,
|
|
103
|
+
code,
|
|
104
|
+
error,
|
|
105
|
+
...(choices ? { choices } : {}),
|
|
106
|
+
} satisfies RefusalJson<CommandCode>);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Whether `argv` asks for `--json`, anywhere before `--`: what follows is a message's text or a
|
|
111
|
+
* workflow's arguments, which can't ask for it, and a usage error counts it before any parse.
|
|
112
|
+
*/
|
|
113
|
+
export function asksJson(argv: readonly string[]): boolean {
|
|
114
|
+
const end = argv.indexOf("--");
|
|
115
|
+
return (end < 0 ? argv : argv.slice(0, end)).includes("--json");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** No `--json`: the failure on stderr only. */
|
|
119
|
+
export const NO_JSON = { json: false, v: 0 };
|
package/src/result-slots.ts
CHANGED
|
@@ -4,11 +4,12 @@ import type { JsonSchema } from "@agentswf/contract/schema";
|
|
|
4
4
|
import type { GivenInput, ResultSubmitCode } from "@agentswf/contract/wire";
|
|
5
5
|
import type { AbsoluteDeadline } from "@agentswf/contract/workflow";
|
|
6
6
|
import { scheduleAt } from "./deadlines";
|
|
7
|
+
import { messageOf } from "./errors";
|
|
8
|
+
import { recordAnswer, writeAcceptedExclusive } from "./ops";
|
|
7
9
|
import { evaluateGiven } from "./result-validation";
|
|
8
|
-
import { recordCandidate, writeAcceptedExclusive, writeCall } from "./runs";
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
type ResultRejectionCode = ResultSubmitCode;
|
|
12
|
+
type ResultSlotAccepted = {
|
|
12
13
|
kind: "accepted";
|
|
13
14
|
value: unknown;
|
|
14
15
|
candidateRecorded: boolean;
|
|
@@ -18,8 +19,8 @@ export type ResultSlotAccepted = {
|
|
|
18
19
|
acceptedAt: number;
|
|
19
20
|
};
|
|
20
21
|
type Rejected = { kind: "rejected"; code: ResultRejectionCode; error: string };
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
type ResultSubmission = ResultSlotAccepted | Rejected;
|
|
23
|
+
type WaitingGrant = { kind: "waiting"; waitUntil: number; deadline?: number };
|
|
23
24
|
export type ResultSlotSettlement =
|
|
24
25
|
| ResultSlotAccepted
|
|
25
26
|
| { kind: "closed" }
|
|
@@ -29,7 +30,7 @@ export type ResultSlotEvent =
|
|
|
29
30
|
| { kind: "waiting"; reason: string; waitUntil: number; at: number; revision: number }
|
|
30
31
|
| { kind: "admitted"; admittedAt: number; revision: number }
|
|
31
32
|
| { kind: "persisted"; accepted: ResultSlotAccepted; revision: number };
|
|
32
|
-
|
|
33
|
+
type ResultSlotBinding = { operationId: string; settled: Promise<ResultSlotSettlement> };
|
|
33
34
|
export type ResultSlotSpec = {
|
|
34
35
|
operationId: string;
|
|
35
36
|
agentId: string;
|
|
@@ -52,8 +53,7 @@ export type ResultSlotRegistryOptions = {
|
|
|
52
53
|
waitMinimumMs?: number;
|
|
53
54
|
};
|
|
54
55
|
interface ResultSlotPersistence {
|
|
55
|
-
|
|
56
|
-
recordCandidate: typeof recordCandidate;
|
|
56
|
+
recordAnswer: typeof recordAnswer;
|
|
57
57
|
writeAcceptedExclusive: typeof writeAcceptedExclusive;
|
|
58
58
|
}
|
|
59
59
|
type SubmissionInput = {
|
|
@@ -84,7 +84,7 @@ type Slot = ResultSlotSpec & {
|
|
|
84
84
|
export function createResultSlotRegistry(options: ResultSlotRegistryOptions): ResultSlotRegistry {
|
|
85
85
|
const now = options.now ?? Date.now;
|
|
86
86
|
const schedule = options.schedule ?? scheduleExpiry;
|
|
87
|
-
const persistence = options.persistence ?? {
|
|
87
|
+
const persistence = options.persistence ?? { recordAnswer, writeAcceptedExclusive };
|
|
88
88
|
const persistenceMs = duration(options.persistenceMs, 5_000);
|
|
89
89
|
const auditMs = duration(options.auditMs, 1_000);
|
|
90
90
|
const waitDefaultMs = duration(options.waitDefaultMs, 120_000);
|
|
@@ -106,7 +106,7 @@ export function createResultSlotRegistry(options: ResultSlotRegistryOptions): Re
|
|
|
106
106
|
try {
|
|
107
107
|
await bounded(
|
|
108
108
|
Promise.resolve().then(() =>
|
|
109
|
-
persistence.
|
|
109
|
+
persistence.recordAnswer(options.runDir, slot.operationId, {
|
|
110
110
|
at: new Date(now()).toISOString(),
|
|
111
111
|
source: input.source,
|
|
112
112
|
// Flags as given, whether or not they convert, so the record shows what the agent typed.
|
|
@@ -143,26 +143,6 @@ export function createResultSlotRegistry(options: ResultSlotRegistryOptions): Re
|
|
|
143
143
|
});
|
|
144
144
|
const slot: Slot = { ...spec, state: "open", settle, cancelExpiry: () => {}, revision: 0 };
|
|
145
145
|
slots.set(spec.operationId, slot);
|
|
146
|
-
try {
|
|
147
|
-
await bounded(
|
|
148
|
-
Promise.resolve().then(() =>
|
|
149
|
-
persistence.writeCall(options.runDir, {
|
|
150
|
-
callId: spec.operationId,
|
|
151
|
-
question: spec.question,
|
|
152
|
-
...(spec.schema ? { schema: spec.schema } : {}),
|
|
153
|
-
}),
|
|
154
|
-
),
|
|
155
|
-
persistenceMs,
|
|
156
|
-
"call persistence timed out",
|
|
157
|
-
);
|
|
158
|
-
} catch (error) {
|
|
159
|
-
if (error instanceof SlotTimeoutError) {
|
|
160
|
-
// The old write may still publish; its operation ID must never be reused.
|
|
161
|
-
slot.state = "failed";
|
|
162
|
-
slot.settle({ kind: "failed", reason: error.message });
|
|
163
|
-
} else slots.delete(spec.operationId);
|
|
164
|
-
throw error;
|
|
165
|
-
}
|
|
166
146
|
if (slot.state === "open") expireAt(slot);
|
|
167
147
|
return { operationId: spec.operationId, settled };
|
|
168
148
|
},
|
|
@@ -253,7 +233,7 @@ export function createResultSlotRegistry(options: ResultSlotRegistryOptions): Re
|
|
|
253
233
|
},
|
|
254
234
|
() => {},
|
|
255
235
|
);
|
|
256
|
-
const reason =
|
|
236
|
+
const reason = messageOf(error);
|
|
257
237
|
if (slot.state === "committing") {
|
|
258
238
|
slot.state = "failed";
|
|
259
239
|
slot.settle({ kind: "failed", reason });
|
package/src/result-validation.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { acceptAny, type SemanticCheck } from "@agentswf/contract";
|
|
2
|
-
import type {
|
|
2
|
+
import type { JsonSchema } from "@agentswf/contract/schema";
|
|
3
3
|
import type { GivenInput } from "@agentswf/contract/wire";
|
|
4
4
|
import { checkGiven, readGiven } from "./simple-input";
|
|
5
5
|
|
|
@@ -12,7 +12,7 @@ export type CandidateEvaluation =
|
|
|
12
12
|
* owns acceptance.
|
|
13
13
|
*/
|
|
14
14
|
export async function evaluateGiven(
|
|
15
|
-
call:
|
|
15
|
+
call: { question: string; schema?: JsonSchema },
|
|
16
16
|
given: GivenInput,
|
|
17
17
|
semantic: SemanticCheck = acceptAny,
|
|
18
18
|
): Promise<CandidateEvaluation> {
|
package/src/run-command.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import type {
|
|
3
|
+
import type { RunPanes } from "@agentswf/contract/records";
|
|
4
|
+
import type { JsonValue, KeepPane } from "@agentswf/contract/workflow";
|
|
4
5
|
import { parseDuration } from "./duration";
|
|
5
6
|
import { messageOf } from "./errors";
|
|
6
7
|
import help from "./help/run.txt" with { type: "text" };
|
|
7
8
|
import { SESSION_CODE } from "./here";
|
|
8
|
-
import {
|
|
9
|
+
import { KEEP_PANES, parseWorkspace } from "./pane-layout";
|
|
10
|
+
import { idProblem, RunRefused, runRootOf, stageNameProblem } from "./runs";
|
|
11
|
+
|
|
12
|
+
/** `awf run --json`'s `"v"`, which its refusals carry too. */
|
|
13
|
+
export const RUN_V = 1;
|
|
9
14
|
|
|
10
15
|
export const usage = help.trimEnd();
|
|
11
16
|
|
|
@@ -37,18 +42,16 @@ export type RunCommand = {
|
|
|
37
42
|
watch: boolean;
|
|
38
43
|
/** The sandbox every agent runs in, as `--sandbox`'s file gave it. */
|
|
39
44
|
sandbox?: unknown;
|
|
45
|
+
/** `--workspace` and `--keep-panes`, which the run records; absent, neither was given. */
|
|
46
|
+
panes?: RunPanes;
|
|
40
47
|
/** Started from an agent's shell: start the run in a tab and take that session over. */
|
|
41
48
|
here: boolean;
|
|
42
49
|
/** The code the calling session replies with, which the run finds it by. */
|
|
43
50
|
session?: string;
|
|
44
51
|
};
|
|
45
52
|
|
|
46
|
-
/** `awf run`'s options and workflow
|
|
47
|
-
export function parseCommand(argv: readonly string[], cwd: string): RunCommand
|
|
48
|
-
if (argv[0] !== "run") throw new Error("expected the run command");
|
|
49
|
-
const end = argv.indexOf("--");
|
|
50
|
-
const own = end < 0 ? argv : argv.slice(0, end);
|
|
51
|
-
if (own.includes("-h") || own.includes("--help")) return "help";
|
|
53
|
+
/** `awf run`'s options and workflow. */
|
|
54
|
+
export function parseCommand(argv: readonly string[], cwd: string): RunCommand {
|
|
52
55
|
let timeout: string | undefined;
|
|
53
56
|
let timeoutMilliseconds: number | undefined;
|
|
54
57
|
let runRoot: string | undefined;
|
|
@@ -63,6 +66,7 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand |
|
|
|
63
66
|
let sandbox: unknown;
|
|
64
67
|
let here = false;
|
|
65
68
|
let session: string | undefined;
|
|
69
|
+
const panes: RunPanes = {};
|
|
66
70
|
let workflowFile: string | undefined;
|
|
67
71
|
// awf's own options may come before or after the workflow file; only `--` ends them.
|
|
68
72
|
let index = 1;
|
|
@@ -91,8 +95,8 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand |
|
|
|
91
95
|
}
|
|
92
96
|
const value = argv[index + 1];
|
|
93
97
|
if (option === "--timeout") {
|
|
94
|
-
if (!value) throw new Error("--timeout needs a duration such as 30m");
|
|
95
|
-
timeoutMilliseconds = parseDuration(value);
|
|
98
|
+
if (!value) throw new Error("--timeout needs a duration such as 30m or 1d");
|
|
99
|
+
timeoutMilliseconds = parseDuration(value, "--timeout");
|
|
96
100
|
timeout = value;
|
|
97
101
|
} else if (option === "--run-root") {
|
|
98
102
|
if (!value) throw new Error("--run-root needs a directory");
|
|
@@ -107,7 +111,8 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand |
|
|
|
107
111
|
} else if (option === "--id") {
|
|
108
112
|
if (!value) throw new Error("--id needs a run's id");
|
|
109
113
|
const problem = idProblem(value);
|
|
110
|
-
|
|
114
|
+
// Refused as any id the grammar refuses is, `id`, still a usage error's exit.
|
|
115
|
+
if (problem) throw new RunRefused(`--id: ${problem}`, "id");
|
|
111
116
|
named = value;
|
|
112
117
|
} else if (option === "--from-stage") {
|
|
113
118
|
if (!value) throw new Error("--from-stage needs a stage's name");
|
|
@@ -122,6 +127,16 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand |
|
|
|
122
127
|
if (!value || !SESSION_CODE.test(value))
|
|
123
128
|
throw new Error("--session needs the code --here printed");
|
|
124
129
|
session = value;
|
|
130
|
+
} else if (option === "--workspace") {
|
|
131
|
+
if (!value?.trim() || value.startsWith("-"))
|
|
132
|
+
throw new Error("--workspace needs run, origin or a workspace's name");
|
|
133
|
+
if (panes.workspace !== undefined) throw new Error("--workspace given twice");
|
|
134
|
+
panes.workspace = parseWorkspace(value);
|
|
135
|
+
} else if (option === "--keep-panes") {
|
|
136
|
+
if (!KEEP_PANES.includes(value as KeepPane))
|
|
137
|
+
throw new Error("--keep-panes needs never, on-failure or always");
|
|
138
|
+
if (panes.keep !== undefined) throw new Error("--keep-panes given twice");
|
|
139
|
+
panes.keep = value as KeepPane;
|
|
125
140
|
} else if (option === "--sandbox") {
|
|
126
141
|
if (!value) throw new Error("--sandbox needs a JSON file");
|
|
127
142
|
if (sandbox !== undefined) throw new Error("--sandbox given twice");
|
|
@@ -133,6 +148,10 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand |
|
|
|
133
148
|
}
|
|
134
149
|
if (!workflowFile) throw new Error("run needs one workflow file");
|
|
135
150
|
if (here && session !== undefined) throw new Error("--here and --session do not go together");
|
|
151
|
+
if (here && json)
|
|
152
|
+
throw new Error(
|
|
153
|
+
"--here prints what the calling agent does next, no document: leave out --json",
|
|
154
|
+
);
|
|
136
155
|
if (continuing && named === undefined)
|
|
137
156
|
throw new Error("--continue goes on with the run --id names");
|
|
138
157
|
const id = continuing ? undefined : named;
|
|
@@ -160,20 +179,13 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand |
|
|
|
160
179
|
here,
|
|
161
180
|
...(session === undefined ? {} : { session }),
|
|
162
181
|
...(sandbox === undefined ? {} : { sandbox }),
|
|
182
|
+
...(Object.keys(panes).length === 0 ? {} : { panes }),
|
|
163
183
|
};
|
|
164
184
|
}
|
|
165
185
|
|
|
166
186
|
/** `--values`'s file: a JSON object of stage values, by stage name. */
|
|
167
187
|
function readValues(file: string): NonNullable<RunCommand["values"]> {
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
171
|
-
} catch (error) {
|
|
172
|
-
throw new Error(`--values: ${messageOf(error)}`);
|
|
173
|
-
}
|
|
174
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
175
|
-
throw new Error(`--values: ${file} must hold a JSON object, each stage's value by its name`);
|
|
176
|
-
}
|
|
188
|
+
const parsed = readJsonObject(file, "--values", "each stage's value by its name");
|
|
177
189
|
for (const stage of Object.keys(parsed)) {
|
|
178
190
|
const problem = stageNameProblem(stage);
|
|
179
191
|
if (problem) throw new Error(`--values: ${problem}`);
|
|
@@ -181,17 +193,22 @@ function readValues(file: string): NonNullable<RunCommand["values"]> {
|
|
|
181
193
|
return { file, stages: new Map(Object.entries(parsed as Record<string, JsonValue>)) };
|
|
182
194
|
}
|
|
183
195
|
|
|
184
|
-
/**
|
|
185
|
-
function
|
|
186
|
-
let
|
|
196
|
+
/** `flag`'s file, which must hold a JSON object: `what`, as its refusal says. */
|
|
197
|
+
function readJsonObject(file: string, flag: string, what: string): object {
|
|
198
|
+
let parsed: unknown;
|
|
187
199
|
try {
|
|
188
|
-
|
|
200
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
189
201
|
} catch (error) {
|
|
190
|
-
throw new Error(
|
|
191
|
-
}
|
|
192
|
-
if (typeof spec !== "object" || spec === null || Array.isArray(spec)) {
|
|
193
|
-
throw new Error(`--sandbox: ${file} must hold a JSON object, a sandbox spec`);
|
|
202
|
+
throw new Error(`${flag}: ${messageOf(error)}`);
|
|
194
203
|
}
|
|
204
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
205
|
+
throw new Error(`${flag}: ${file} must hold a JSON object, ${what}`);
|
|
206
|
+
return parsed;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** `--sandbox`'s file: an inline sandbox spec, whose working directory is the run's. */
|
|
210
|
+
function readSandboxSpec(file: string): object {
|
|
211
|
+
const spec = readJsonObject(file, "--sandbox", "a sandbox spec");
|
|
195
212
|
for (const [field, why] of Object.entries(NOT_IN_A_RUN_SANDBOX)) {
|
|
196
213
|
if (field in spec) throw new Error(`--sandbox: the run's sandbox names no ${field}; ${why}`);
|
|
197
214
|
}
|