@agentswf/engine 0.1.0-beta.5 → 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 +28 -55
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +4 -4
- package/src/attempt-output.ts +13 -31
- package/src/attempt-view.ts +33 -32
- 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 +4 -6
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +27 -28
- package/src/deadlines.ts +6 -40
- package/src/decisions/directory.ts +50 -37
- 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 +19 -33
- package/src/emit-command.ts +38 -22
- package/src/emit.ts +148 -59
- 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 +11 -7
- package/src/help/inbox.txt +5 -3
- package/src/help/logs.txt +7 -5
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +13 -8
- 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 +5 -13
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +54 -136
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +42 -56
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.ts.template +3 -2
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +7 -19
- package/src/logs-command.ts +134 -53
- package/src/message-operation.ts +6 -3
- package/src/messaging/messaging.ts +8 -10
- package/src/messaging/text.ts +2 -2
- 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 +206 -90
- package/src/operator-runtime.ts +24 -2
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +49 -96
- package/src/progress-view.ts +2 -2
- 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 +27 -31
- package/src/run-commands.ts +42 -114
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/run-policy/stage-plan.ts +154 -0
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +165 -41
- 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 +17 -11
- package/src/run-view.ts +73 -14
- package/src/runs-command.ts +81 -63
- package/src/runs.ts +284 -222
- 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 +2 -39
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +111 -153
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +37 -19
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +66 -18
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +11 -30
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +49 -2
- package/src/workflow-runner.ts +525 -618
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +1 -1
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +28 -29
- package/src/workflow-testing/index.ts +9 -27
- package/src/workflow-testing/run-test.ts +201 -286
- package/src/workflow-testing/script.ts +7 -39
- package/src/deprecations.ts +0 -177
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -972
- package/src/stage-plan.ts +0 -186
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- package/src/workflow-testing/workflow-testing.typecheck.ts +0 -67
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
|
@@ -7,7 +7,10 @@ import { messageOf } from "./errors";
|
|
|
7
7
|
import help from "./help/run.txt" with { type: "text" };
|
|
8
8
|
import { SESSION_CODE } from "./here";
|
|
9
9
|
import { KEEP_PANES, parseWorkspace } from "./pane-layout";
|
|
10
|
-
import { idProblem, runRootOf, stageNameProblem } from "./runs";
|
|
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;
|
|
11
14
|
|
|
12
15
|
export const usage = help.trimEnd();
|
|
13
16
|
|
|
@@ -47,17 +50,8 @@ export type RunCommand = {
|
|
|
47
50
|
session?: string;
|
|
48
51
|
};
|
|
49
52
|
|
|
50
|
-
/** `awf run`'s options and workflow
|
|
51
|
-
export function parseCommand(
|
|
52
|
-
argv: readonly string[],
|
|
53
|
-
cwd: string,
|
|
54
|
-
/** Told each deprecated form given, once. */
|
|
55
|
-
deprecated: (line: string) => void = () => {},
|
|
56
|
-
): RunCommand | "help" {
|
|
57
|
-
if (argv[0] !== "run") throw new Error("expected the run command");
|
|
58
|
-
const end = argv.indexOf("--");
|
|
59
|
-
const own = end < 0 ? argv : argv.slice(0, end);
|
|
60
|
-
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 {
|
|
61
55
|
let timeout: string | undefined;
|
|
62
56
|
let timeoutMilliseconds: number | undefined;
|
|
63
57
|
let runRoot: string | undefined;
|
|
@@ -102,7 +96,7 @@ export function parseCommand(
|
|
|
102
96
|
const value = argv[index + 1];
|
|
103
97
|
if (option === "--timeout") {
|
|
104
98
|
if (!value) throw new Error("--timeout needs a duration such as 30m or 1d");
|
|
105
|
-
timeoutMilliseconds = parseDuration(value, "--timeout"
|
|
99
|
+
timeoutMilliseconds = parseDuration(value, "--timeout");
|
|
106
100
|
timeout = value;
|
|
107
101
|
} else if (option === "--run-root") {
|
|
108
102
|
if (!value) throw new Error("--run-root needs a directory");
|
|
@@ -117,7 +111,8 @@ export function parseCommand(
|
|
|
117
111
|
} else if (option === "--id") {
|
|
118
112
|
if (!value) throw new Error("--id needs a run's id");
|
|
119
113
|
const problem = idProblem(value);
|
|
120
|
-
|
|
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");
|
|
121
116
|
named = value;
|
|
122
117
|
} else if (option === "--from-stage") {
|
|
123
118
|
if (!value) throw new Error("--from-stage needs a stage's name");
|
|
@@ -153,6 +148,10 @@ export function parseCommand(
|
|
|
153
148
|
}
|
|
154
149
|
if (!workflowFile) throw new Error("run needs one workflow file");
|
|
155
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
|
+
);
|
|
156
155
|
if (continuing && named === undefined)
|
|
157
156
|
throw new Error("--continue goes on with the run --id names");
|
|
158
157
|
const id = continuing ? undefined : named;
|
|
@@ -186,15 +185,7 @@ export function parseCommand(
|
|
|
186
185
|
|
|
187
186
|
/** `--values`'s file: a JSON object of stage values, by stage name. */
|
|
188
187
|
function readValues(file: string): NonNullable<RunCommand["values"]> {
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
192
|
-
} catch (error) {
|
|
193
|
-
throw new Error(`--values: ${messageOf(error)}`);
|
|
194
|
-
}
|
|
195
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
196
|
-
throw new Error(`--values: ${file} must hold a JSON object, each stage's value by its name`);
|
|
197
|
-
}
|
|
188
|
+
const parsed = readJsonObject(file, "--values", "each stage's value by its name");
|
|
198
189
|
for (const stage of Object.keys(parsed)) {
|
|
199
190
|
const problem = stageNameProblem(stage);
|
|
200
191
|
if (problem) throw new Error(`--values: ${problem}`);
|
|
@@ -202,17 +193,22 @@ function readValues(file: string): NonNullable<RunCommand["values"]> {
|
|
|
202
193
|
return { file, stages: new Map(Object.entries(parsed as Record<string, JsonValue>)) };
|
|
203
194
|
}
|
|
204
195
|
|
|
205
|
-
/**
|
|
206
|
-
function
|
|
207
|
-
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;
|
|
208
199
|
try {
|
|
209
|
-
|
|
200
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
210
201
|
} catch (error) {
|
|
211
|
-
throw new Error(
|
|
212
|
-
}
|
|
213
|
-
if (typeof spec !== "object" || spec === null || Array.isArray(spec)) {
|
|
214
|
-
throw new Error(`--sandbox: ${file} must hold a JSON object, a sandbox spec`);
|
|
202
|
+
throw new Error(`${flag}: ${messageOf(error)}`);
|
|
215
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");
|
|
216
212
|
for (const [field, why] of Object.entries(NOT_IN_A_RUN_SANDBOX)) {
|
|
217
213
|
if (field in spec) throw new Error(`--sandbox: the run's sandbox names no ${field}; ${why}`);
|
|
218
214
|
}
|
package/src/run-commands.ts
CHANGED
|
@@ -1,98 +1,34 @@
|
|
|
1
|
+
// What the commands that read runs say of one: its state, span and cost, and the commands that go
|
|
2
|
+
// on from it.
|
|
1
3
|
import { resolve } from "node:path";
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
import { continueCommand
|
|
4
|
+
import type { RunLogLine } from "@agentswf/contract/records";
|
|
5
|
+
import { runEstimate, unknownOps } from "./accounting/spent";
|
|
6
|
+
import { continueCommand } from "./attempt-view";
|
|
7
|
+
import { command, type Out, placeFlags, type ReadPlace, shellLine } from "./command-line";
|
|
8
|
+
import { NO_JSON, readFailureCode, refusalLine } from "./refusals";
|
|
5
9
|
import type { RunCommand } from "./run-command";
|
|
6
10
|
import { type Cursor, encodeCursor } from "./run-read";
|
|
7
11
|
import { RunNotFound, type RunStatus } from "./run-state";
|
|
12
|
+
import { clockTime } from "./run-text";
|
|
8
13
|
import { runRootOf } from "./runs";
|
|
14
|
+
import { readSpent } from "./spend";
|
|
9
15
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
export type Out = { stdout(text: string): void; stderr(text: string): void };
|
|
16
|
-
|
|
17
|
-
/** `--cwd`, `--run-root` and `--json`, taken out of `argv`; the rest is left for the command. */
|
|
18
|
-
export function parsePlace(
|
|
19
|
-
argv: readonly string[],
|
|
20
|
-
shellCwd: string,
|
|
21
|
-
): { place: ReadPlace; json: boolean; rest: string[] } {
|
|
22
|
-
let cwd = shellCwd;
|
|
23
|
-
let runRoot: string | undefined;
|
|
24
|
-
let json = false;
|
|
25
|
-
const repeat: string[] = [];
|
|
26
|
-
const rest: string[] = [];
|
|
27
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
28
|
-
const arg = argv[index]!;
|
|
29
|
-
// What follows is the command's own, a message's text included.
|
|
30
|
-
if (arg === "--") {
|
|
31
|
-
rest.push(...argv.slice(index));
|
|
32
|
-
break;
|
|
33
|
-
}
|
|
34
|
-
if (arg === "--json") json = true;
|
|
35
|
-
else if (arg === "--cwd" || arg === "--run-root") {
|
|
36
|
-
const value = argv[index + 1];
|
|
37
|
-
if (!value || value.startsWith("--")) throw new Error(`${arg} needs a directory`);
|
|
38
|
-
index += 1;
|
|
39
|
-
if (arg === "--cwd") cwd = resolve(shellCwd, value);
|
|
40
|
-
else runRoot = resolve(shellCwd, value);
|
|
41
|
-
repeat.push(arg, value);
|
|
42
|
-
} else rest.push(arg);
|
|
43
|
-
}
|
|
44
|
-
return { place: { shellCwd, runRoot: runRoot ?? runRootOf(cwd), repeat }, json, rest };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** A suggested command, with the place repeated. */
|
|
48
|
-
export function command(place: ReadPlace, ...words: string[]): string[] {
|
|
49
|
-
const [first, second, ...rest] = words;
|
|
50
|
-
return [first!, second!, ...rest.slice(0, 1), ...place.repeat, ...rest.slice(1)];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** A field whose name says it holds a credential, whatever its value looks like. */
|
|
54
|
-
const SECRET_NAME =
|
|
55
|
-
/password|passwd|passphrase|passcode|secret|token|credential|cookie|bearer|authorization|(?:api|access|private|session|ssh|signing|encryption)[_-]?key|^(?:pwd|pass|pin|auth)$/i;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Every string in `value` masked, and everything under a credential's name hidden: a run's text,
|
|
59
|
-
* errors and fields are its workflow's, its agents' and its tools'. Past `limit`, a string is cut.
|
|
60
|
-
*/
|
|
61
|
-
export function masked(value: unknown, limit = Number.POSITIVE_INFINITY, hidden = false): unknown {
|
|
62
|
-
if (typeof value === "string") {
|
|
63
|
-
if (hidden) return "***";
|
|
64
|
-
const shown = maskCredentials(value);
|
|
65
|
-
return shown.length > limit ? `${shown.slice(0, limit - 1)}…` : shown;
|
|
66
|
-
}
|
|
67
|
-
if (hidden && value !== null && typeof value !== "object") return "***";
|
|
68
|
-
if (Array.isArray(value)) return value.map((each) => masked(each, limit, hidden));
|
|
69
|
-
if (value && typeof value === "object") {
|
|
70
|
-
return Object.fromEntries(
|
|
71
|
-
Object.entries(value).map(([key, each]) => [
|
|
72
|
-
key,
|
|
73
|
-
masked(each, limit, hidden || SECRET_NAME.test(key)),
|
|
74
|
-
]),
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
return value;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Text a run wrote, as one masked line of about `limit` characters, so it can never pass for a
|
|
82
|
-
* line of its own or fill a page. `@{handle} --full` prints it whole.
|
|
83
|
-
*/
|
|
84
|
-
export function said(text: string, limit = 120): string {
|
|
85
|
-
const line = maskCredentials(text).replace(/\s*[\r\n]+\s*/g, " ⏎ ");
|
|
86
|
-
return line.length > limit ? `${line.slice(0, limit - 1)}…` : line;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** A suggested command as text to paste into a shell. */
|
|
90
|
-
export function shellLine(words: readonly string[]): string {
|
|
91
|
-
return words.map(shellWord).join(" ");
|
|
92
|
-
}
|
|
16
|
+
/** The longest `--wait` the commands that read runs take. */
|
|
17
|
+
export const MAX_WAIT_SECONDS = 300;
|
|
18
|
+
/** How often they read a run while they wait. */
|
|
19
|
+
export const POLL_MS = 1_000;
|
|
93
20
|
|
|
94
21
|
/** Says a run that is not there, and exits 1; anything else is the run's files, exit 3. */
|
|
95
|
-
export function failed(
|
|
22
|
+
export function failed(
|
|
23
|
+
error: unknown,
|
|
24
|
+
place: ReadPlace,
|
|
25
|
+
out: Out,
|
|
26
|
+
message: string,
|
|
27
|
+
json: { json: boolean; v: number } = NO_JSON,
|
|
28
|
+
): number {
|
|
29
|
+
// The error's own code; else a file that couldn't be read, exit 3's, or a failure of awf's.
|
|
30
|
+
const code = readFailureCode(error);
|
|
31
|
+
if (json.json) out.stdout(refusalLine(json.v, code, message));
|
|
96
32
|
if (error instanceof RunNotFound) {
|
|
97
33
|
out.stderr(`${error.message}; runs here: ${shellLine(command(place, "awf", "runs"))}`);
|
|
98
34
|
return 1;
|
|
@@ -114,16 +50,6 @@ export function startedLine(
|
|
|
114
50
|
return `awf: ${ref} started · watch: ${shellLine(["awf", "logs", ref, ...placeFlags(run)])}`;
|
|
115
51
|
}
|
|
116
52
|
|
|
117
|
-
/** The flags that find this run again from another command: the folder or run root given. */
|
|
118
|
-
export function placeFlags(
|
|
119
|
-
run: Pick<RunCommand, "cwd" | "cwdGiven" | "runRoot" | "runRootGiven">,
|
|
120
|
-
): string[] {
|
|
121
|
-
return [
|
|
122
|
-
...(run.cwdGiven ? ["--cwd", run.cwd] : []),
|
|
123
|
-
...(run.runRootGiven ? ["--run-root", run.runRoot] : []),
|
|
124
|
-
];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
53
|
/** `awf logs` from `cursor`: with `--wait` unless the page stopped short of the newest. */
|
|
128
54
|
export function nextLogs(
|
|
129
55
|
status: RunStatus,
|
|
@@ -205,22 +131,24 @@ export function span(
|
|
|
205
131
|
return { started, ...(ended === undefined ? {} : { ended }), ms: (ended ?? now) - started };
|
|
206
132
|
}
|
|
207
133
|
|
|
208
|
-
/**
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
134
|
+
/**
|
|
135
|
+
* At list prices, today's, over the run's agent operations that recorded their spend, never what
|
|
136
|
+
* was billed; and how many operations' usage isn't known, which makes it a floor.
|
|
137
|
+
*/
|
|
138
|
+
export async function estimateOf(
|
|
139
|
+
status: RunStatus,
|
|
140
|
+
): Promise<{ estimate?: number; unknown: number }> {
|
|
141
|
+
const spent = await readSpent(status.run.dir, status.attempts, { decisions: false }).catch(
|
|
142
|
+
() => undefined,
|
|
212
143
|
);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const date = new Date(at);
|
|
224
|
-
const two = (n: number) => String(n).padStart(2, "0");
|
|
225
|
-
return `${two(date.getHours())}:${two(date.getMinutes())}:${two(date.getSeconds())}`;
|
|
144
|
+
if (!spent) return { unknown: 0 };
|
|
145
|
+
const estimate = runEstimate(spent);
|
|
146
|
+
// A running attempt's operations are read as it ends: not yet unknown.
|
|
147
|
+
const live = status.state === "running" ? status.attempts.at(-1)?.attempt : undefined;
|
|
148
|
+
return {
|
|
149
|
+
...(estimate === undefined ? {} : { estimate }),
|
|
150
|
+
unknown: unknownOps(
|
|
151
|
+
spent.map((each) => (each.attempt === live ? { ...each, unsettled: 0 } : each)),
|
|
152
|
+
),
|
|
153
|
+
};
|
|
226
154
|
}
|
package/src/run-event-log.ts
CHANGED
|
@@ -41,7 +41,7 @@ export class RunEventLog {
|
|
|
41
41
|
if (this.#closed) return;
|
|
42
42
|
this.#writes.push(
|
|
43
43
|
appendEvent(this.options.runDir, record).catch((error) =>
|
|
44
|
-
this.options.failed(`awf: a turn was not recorded in
|
|
44
|
+
this.options.failed(`awf: a turn was not recorded in log.jsonl: ${messageOf(error)}`),
|
|
45
45
|
),
|
|
46
46
|
);
|
|
47
47
|
}
|