@vimhead.dev/norn-cli 0.1.0-tip.36679386408.1 → 0.1.0-tip.37307659560.1
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/assets/README.md +3 -2
- package/assets/docs/agents.md +7 -2
- package/assets/docs/cli.md +25 -0
- package/assets/docs/providers.md +3 -3
- package/assets/docs/recovery.md +4 -0
- package/assets/examples/agent-then-analysis/README.md +1 -1
- package/assets/examples/agent-then-analysis/plugin.ts +2 -0
- package/assets/examples/coordinating-multiple-agents/README.md +2 -1
- package/assets/examples/coordinating-multiple-agents/plugin.ts +1 -0
- package/assets/examples/getting-started/README.md +3 -3
- package/assets/examples/getting-started/plugin.ts +1 -0
- package/assets/examples/shared-state/README.md +1 -1
- package/assets/examples/shared-state/plugin.ts +1 -0
- package/assets/examples/worktree-development-loop/README.md +1 -1
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +1 -0
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -0
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +1 -0
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/cli.ts +37 -0
- package/assets/packages/cli/src/client.ts +16 -0
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/cli/src/internal/agents.ts +42 -2
- package/assets/packages/cli/src/internal/execution-context.ts +0 -2
- package/assets/packages/cli/src/internal/run-lease.ts +24 -2
- package/assets/packages/cli/src/internal/run-pruning.ts +169 -0
- package/assets/packages/cli/src/internal/run-store.ts +76 -4
- package/assets/packages/core/src/atomic-files.ts +8 -1
- package/assets/packages/sdk/src/api.ts +29 -1
- package/dist/cli.js +34 -0
- package/dist/client.d.ts +2 -1
- package/dist/client.js +8 -0
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/dist/internal/agents.d.ts +1 -1
- package/dist/internal/agents.js +33 -2
- package/dist/internal/execution-context.d.ts +0 -1
- package/dist/internal/execution-context.js +0 -1
- package/dist/internal/file-coordinator.js +4 -1
- package/dist/internal/launch-request.js +4 -1
- package/dist/internal/run-lease.d.ts +1 -0
- package/dist/internal/run-lease.js +27 -3
- package/dist/internal/run-log.js +4 -1
- package/dist/internal/run-pruning.d.ts +14 -0
- package/dist/internal/run-pruning.js +141 -0
- package/dist/internal/run-state.js +4 -1
- package/dist/internal/run-store.d.ts +5 -0
- package/dist/internal/run-store.js +66 -5
- package/package.json +2 -2
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NornRunInfo,
|
|
3
|
+
NornRunPruneOptions,
|
|
4
|
+
NornRunPruneResult,
|
|
5
|
+
} from "@vimhead.dev/norn";
|
|
6
|
+
import { errorMessage } from "./errors.ts";
|
|
7
|
+
import { getRunLeaseOwner, NornRunLease } from "./run-lease.ts";
|
|
8
|
+
import { getRunInfo, listRuns } from "./run-state.ts";
|
|
9
|
+
import { NornRunStore } from "./run-store.ts";
|
|
10
|
+
|
|
11
|
+
const DURATION_UNITS: Readonly<Record<string, number>> = {
|
|
12
|
+
ms: 1,
|
|
13
|
+
s: 1000,
|
|
14
|
+
m: 60_000,
|
|
15
|
+
h: 3_600_000,
|
|
16
|
+
d: 86_400_000,
|
|
17
|
+
w: 604_800_000,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function parseRunPruneOptions(
|
|
21
|
+
args: readonly string[],
|
|
22
|
+
): NornRunPruneOptions {
|
|
23
|
+
const flags = new Set<string>();
|
|
24
|
+
let olderThan: string | null = null;
|
|
25
|
+
for (let index = 0; index < args.length; index++) {
|
|
26
|
+
const flag = args[index];
|
|
27
|
+
if (!["--delete-runs", "--all", "--dry-run", "--older-than"].includes(flag))
|
|
28
|
+
throw new Error(`runs prune: unknown argument ${flag}`);
|
|
29
|
+
if (flags.has(flag))
|
|
30
|
+
throw new Error(`runs prune: duplicate option ${flag}`);
|
|
31
|
+
flags.add(flag);
|
|
32
|
+
if (flag === "--older-than") {
|
|
33
|
+
const value = args[++index];
|
|
34
|
+
if (value === undefined || value.startsWith("--"))
|
|
35
|
+
throw new Error(
|
|
36
|
+
"runs prune: --older-than requires a duration such as 24h or 7d",
|
|
37
|
+
);
|
|
38
|
+
olderThan = value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
deleteRuns: flags.has("--delete-runs"),
|
|
43
|
+
all: flags.has("--all"),
|
|
44
|
+
olderThan,
|
|
45
|
+
dryRun: flags.has("--dry-run"),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parsePruneDuration(value: string): number {
|
|
50
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d|w)$/.exec(value);
|
|
51
|
+
const duration = match ? Number(match[1]) * DURATION_UNITS[match[2]] : NaN;
|
|
52
|
+
if (
|
|
53
|
+
!Number.isFinite(duration) ||
|
|
54
|
+
duration < 0 ||
|
|
55
|
+
duration > Number.MAX_SAFE_INTEGER
|
|
56
|
+
)
|
|
57
|
+
throw new Error(
|
|
58
|
+
`runs prune: invalid duration ${value}; use a non-negative number followed by ms, s, m, h, d, or w`,
|
|
59
|
+
);
|
|
60
|
+
return duration;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class NornRunPruner {
|
|
64
|
+
private readonly olderThan: string | null;
|
|
65
|
+
private readonly cutoff: number | null;
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
private readonly input: {
|
|
69
|
+
readonly projectRoot: string;
|
|
70
|
+
readonly options: NornRunPruneOptions;
|
|
71
|
+
readonly now: Date;
|
|
72
|
+
},
|
|
73
|
+
) {
|
|
74
|
+
if (input.options.all && !input.options.deleteRuns)
|
|
75
|
+
throw new Error("runs prune: --all requires --delete-runs");
|
|
76
|
+
if (input.options.all && input.options.olderThan !== null)
|
|
77
|
+
throw new Error(
|
|
78
|
+
"runs prune: --all and --older-than are mutually exclusive",
|
|
79
|
+
);
|
|
80
|
+
if (!Number.isFinite(input.now.getTime()))
|
|
81
|
+
throw new Error("Invalid pruning time");
|
|
82
|
+
this.olderThan = input.options.all
|
|
83
|
+
? null
|
|
84
|
+
: (input.options.olderThan ?? "24h");
|
|
85
|
+
this.cutoff =
|
|
86
|
+
this.olderThan === null
|
|
87
|
+
? null
|
|
88
|
+
: input.now.getTime() - parsePruneDuration(this.olderThan);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async prune(): Promise<NornRunPruneResult> {
|
|
92
|
+
const results: NornRunPruneResult["runs"][number][] = [];
|
|
93
|
+
for (const run of await listRuns(this.input.projectRoot)) {
|
|
94
|
+
const identity = { id: run.id, name: run.name, path: run.path };
|
|
95
|
+
let lease: NornRunLease | undefined;
|
|
96
|
+
try {
|
|
97
|
+
let reason = this.findIneligibilityReason(run);
|
|
98
|
+
if (!reason && (await getRunLeaseOwner(run.path)))
|
|
99
|
+
reason = "Run is owned by an executor";
|
|
100
|
+
if (reason) {
|
|
101
|
+
results.push({ ...identity, status: "skipped", reason });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (this.input.options.dryRun) {
|
|
105
|
+
results.push({ ...identity, status: "planned" });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
lease = await NornRunLease.acquire(run.path);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (
|
|
112
|
+
error instanceof Error &&
|
|
113
|
+
error.message.startsWith("Run is already active:")
|
|
114
|
+
) {
|
|
115
|
+
results.push({
|
|
116
|
+
...identity,
|
|
117
|
+
status: "skipped",
|
|
118
|
+
reason: "Run is owned by an executor",
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
reason = this.findIneligibilityReason(await getRunInfo(run.path));
|
|
125
|
+
if (reason) {
|
|
126
|
+
results.push({ ...identity, status: "skipped", reason });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
await lease.assertOwned();
|
|
130
|
+
if (this.input.options.deleteRuns) {
|
|
131
|
+
await lease.deleteRunDirectory();
|
|
132
|
+
} else {
|
|
133
|
+
const store = await NornRunStore.open(run.path);
|
|
134
|
+
await store.pruneCheckpointHistory(this.input.now.toISOString());
|
|
135
|
+
}
|
|
136
|
+
results.push({ ...identity, status: "pruned" });
|
|
137
|
+
} catch (error) {
|
|
138
|
+
results.push({
|
|
139
|
+
...identity,
|
|
140
|
+
status: "failed",
|
|
141
|
+
reason: errorMessage(error),
|
|
142
|
+
});
|
|
143
|
+
} finally {
|
|
144
|
+
await lease?.release();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
mode: this.input.options.deleteRuns ? "runs" : "checkpoints",
|
|
149
|
+
dryRun: this.input.options.dryRun,
|
|
150
|
+
olderThan: this.olderThan,
|
|
151
|
+
runs: results,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private findIneligibilityReason(run: NornRunInfo): string | undefined {
|
|
156
|
+
if (run.status !== "completed" && run.status !== "failed")
|
|
157
|
+
return `Run status is ${run.status}; only completed or failed runs are eligible`;
|
|
158
|
+
const finishedAt =
|
|
159
|
+
run.status === "failed"
|
|
160
|
+
? (run.failed?.failedAt ?? run.outcome?.completedAt ?? run.updatedAt)
|
|
161
|
+
: (run.outcome?.completedAt ?? run.updatedAt);
|
|
162
|
+
const finishedTime = Date.parse(finishedAt);
|
|
163
|
+
if (!Number.isFinite(finishedTime))
|
|
164
|
+
return "Run completion/failure time is invalid";
|
|
165
|
+
if (this.cutoff !== null && finishedTime >= this.cutoff)
|
|
166
|
+
return `Run finished less than or exactly ${this.olderThan} ago`;
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -21,6 +21,7 @@ import { gunzip, gzip } from "node:zlib";
|
|
|
21
21
|
import type { NornRunCheckpoint } from "@vimhead.dev/norn";
|
|
22
22
|
import { isNodeError } from "@vimhead.dev/norn-core/errors";
|
|
23
23
|
import {
|
|
24
|
+
writeBytesAtomically,
|
|
24
25
|
writeJsonAtomically,
|
|
25
26
|
writeTextAtomically,
|
|
26
27
|
} from "@vimhead.dev/norn-core/atomic-files";
|
|
@@ -31,6 +32,7 @@ const SNAPSHOT_VERSION = 1;
|
|
|
31
32
|
const CURRENT_DIR_NAME = "current";
|
|
32
33
|
const STORE_DIR_NAME = "store";
|
|
33
34
|
const CHECKPOINTS_FILE_NAME = "checkpoints.json";
|
|
35
|
+
const PRUNED_HISTORY_FILE_NAME = "checkpoint-history-pruned.json";
|
|
34
36
|
|
|
35
37
|
export function runCurrentRoot(runRoot: string): string {
|
|
36
38
|
return join(runRoot, CURRENT_DIR_NAME);
|
|
@@ -57,15 +59,18 @@ export class NornRunStore {
|
|
|
57
59
|
static async open(root: string): Promise<NornRunStore> {
|
|
58
60
|
const runStore = new NornRunStore(root);
|
|
59
61
|
await access(runStore.currentRoot, constants.R_OK | constants.W_OK);
|
|
60
|
-
await
|
|
62
|
+
if (!(await runStore.isCheckpointHistoryPruned()))
|
|
63
|
+
await access(runStore.storeRoot, constants.R_OK | constants.W_OK);
|
|
61
64
|
return runStore;
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
async currentSnapshotRef(): Promise<string> {
|
|
68
|
+
await this.assertCheckpointHistoryAvailable();
|
|
65
69
|
return readFile(this.currentRefPath, "utf8").then((value) => value.trim());
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
async snapshotCurrent(message: string): Promise<NornRunCheckpoint> {
|
|
73
|
+
await this.assertCheckpointHistoryAvailable();
|
|
69
74
|
const previous = await this.readCheckpointHistory();
|
|
70
75
|
const checkpoint = this.createCheckpoint(
|
|
71
76
|
message,
|
|
@@ -89,6 +94,7 @@ export class NornRunStore {
|
|
|
89
94
|
ref: string,
|
|
90
95
|
prepare: ((stagedRunRoot: string) => Promise<void>) | undefined,
|
|
91
96
|
): Promise<void> {
|
|
97
|
+
await this.assertCheckpointHistoryAvailable();
|
|
92
98
|
const checkpoint = (await this.listCheckpoints()).find(
|
|
93
99
|
(entry) => entry.id === ref,
|
|
94
100
|
);
|
|
@@ -107,9 +113,55 @@ export class NornRunStore {
|
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
async listCheckpoints(): Promise<NornRunCheckpoint[]> {
|
|
116
|
+
if (await this.isCheckpointHistoryPruned()) return [];
|
|
110
117
|
return (await this.readCheckpointHistory()).checkpoints;
|
|
111
118
|
}
|
|
112
119
|
|
|
120
|
+
async pruneCheckpointHistory(prunedAt: string): Promise<void> {
|
|
121
|
+
if (!(await this.isCheckpointHistoryPruned()))
|
|
122
|
+
await writeJsonAtomically(this.prunedHistoryPath, {
|
|
123
|
+
version: 1,
|
|
124
|
+
prunedAt,
|
|
125
|
+
});
|
|
126
|
+
await writeJsonAtomically(this.checkpointsPath, []);
|
|
127
|
+
await rm(this.storeRoot, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private get prunedHistoryPath(): string {
|
|
131
|
+
return join(this.runRoot, PRUNED_HISTORY_FILE_NAME);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async isCheckpointHistoryPruned(): Promise<boolean> {
|
|
135
|
+
let content: string;
|
|
136
|
+
try {
|
|
137
|
+
content = await readFile(this.prunedHistoryPath, "utf8");
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (isNodeError(error) && error.code === "ENOENT") return false;
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
const marker: unknown = JSON.parse(content);
|
|
143
|
+
if (
|
|
144
|
+
!marker ||
|
|
145
|
+
typeof marker !== "object" ||
|
|
146
|
+
!("version" in marker) ||
|
|
147
|
+
marker.version !== 1 ||
|
|
148
|
+
!("prunedAt" in marker) ||
|
|
149
|
+
typeof marker.prunedAt !== "string" ||
|
|
150
|
+
!Number.isFinite(Date.parse(marker.prunedAt))
|
|
151
|
+
)
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Invalid pruned checkpoint history marker: ${this.prunedHistoryPath}`,
|
|
154
|
+
);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async assertCheckpointHistoryAvailable(): Promise<void> {
|
|
159
|
+
if (await this.isCheckpointHistoryPruned())
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Checkpoint history was pruned for this run: ${this.runRoot}`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
113
165
|
private async readCheckpointHistory(): Promise<{
|
|
114
166
|
readonly checkpoints: NornRunCheckpoint[];
|
|
115
167
|
readonly content?: string;
|
|
@@ -191,7 +243,10 @@ export class NornRunStore {
|
|
|
191
243
|
createdAt: checkpoint.createdAt,
|
|
192
244
|
entries,
|
|
193
245
|
};
|
|
194
|
-
await
|
|
246
|
+
await writeBytesAtomically(
|
|
247
|
+
this.snapshotPath(snapshot.id),
|
|
248
|
+
await gzipBuffer(Buffer.from(JSON.stringify(snapshot))),
|
|
249
|
+
);
|
|
195
250
|
return snapshot;
|
|
196
251
|
}
|
|
197
252
|
|
|
@@ -363,12 +418,25 @@ export class NornRunStore {
|
|
|
363
418
|
}
|
|
364
419
|
|
|
365
420
|
private async readSnapshot(id: string): Promise<RunSnapshotManifest> {
|
|
421
|
+
let compressed: Buffer;
|
|
422
|
+
try {
|
|
423
|
+
compressed = await readFile(this.snapshotPath(id));
|
|
424
|
+
} catch (error) {
|
|
425
|
+
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
426
|
+
return parseRunSnapshotManifest(
|
|
427
|
+
JSON.parse(await readFile(this.getLegacySnapshotPath(id), "utf8")),
|
|
428
|
+
);
|
|
429
|
+
}
|
|
366
430
|
return parseRunSnapshotManifest(
|
|
367
|
-
JSON.parse(await
|
|
431
|
+
JSON.parse((await gunzipBuffer(compressed)).toString("utf8")),
|
|
368
432
|
);
|
|
369
433
|
}
|
|
370
434
|
|
|
371
435
|
private snapshotPath(id: string): string {
|
|
436
|
+
return join(this.snapshotsRoot, `${id}.json.gz`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private getLegacySnapshotPath(id: string): string {
|
|
372
440
|
return join(this.snapshotsRoot, `${id}.json`);
|
|
373
441
|
}
|
|
374
442
|
|
|
@@ -378,7 +446,11 @@ export class NornRunStore {
|
|
|
378
446
|
|
|
379
447
|
private assertCheckpointPathMatchesId(checkpoint: NornRunCheckpoint): void {
|
|
380
448
|
const expectedPath = this.snapshotRelativePath(checkpoint.id);
|
|
381
|
-
|
|
449
|
+
const legacyPath = relative(
|
|
450
|
+
this.runRoot,
|
|
451
|
+
this.getLegacySnapshotPath(checkpoint.id),
|
|
452
|
+
);
|
|
453
|
+
if (checkpoint.path !== expectedPath && checkpoint.path !== legacyPath)
|
|
382
454
|
throw new Error(
|
|
383
455
|
`Run checkpoint path does not match id: ${checkpoint.id}`,
|
|
384
456
|
);
|
|
@@ -13,6 +13,13 @@ export async function writeJsonAtomically(
|
|
|
13
13
|
export async function writeTextAtomically(
|
|
14
14
|
path: string,
|
|
15
15
|
content: string,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
await writeBytesAtomically(path, Buffer.from(content, "utf8"));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function writeBytesAtomically(
|
|
21
|
+
path: string,
|
|
22
|
+
content: Uint8Array,
|
|
16
23
|
): Promise<void> {
|
|
17
24
|
await mkdir(dirname(path), { recursive: true });
|
|
18
25
|
let existingMode: number | undefined;
|
|
@@ -23,7 +30,7 @@ export async function writeTextAtomically(
|
|
|
23
30
|
}
|
|
24
31
|
const tmpPath = `${path}.${randomUUID()}.tmp`;
|
|
25
32
|
try {
|
|
26
|
-
await writeFile(tmpPath, content, {
|
|
33
|
+
await writeFile(tmpPath, content, { mode: existingMode });
|
|
27
34
|
if (existingMode !== undefined) await chmod(tmpPath, existingMode);
|
|
28
35
|
await rename(tmpPath, path);
|
|
29
36
|
} catch (error) {
|
|
@@ -383,6 +383,26 @@ export type DeletedNornRunInfo = {
|
|
|
383
383
|
readonly path: string;
|
|
384
384
|
};
|
|
385
385
|
|
|
386
|
+
export type NornRunPruneOptions = {
|
|
387
|
+
readonly deleteRuns: boolean;
|
|
388
|
+
readonly all: boolean;
|
|
389
|
+
readonly olderThan: string | null;
|
|
390
|
+
readonly dryRun: boolean;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
export type NornRunPruneResult = {
|
|
394
|
+
readonly mode: "checkpoints" | "runs";
|
|
395
|
+
readonly dryRun: boolean;
|
|
396
|
+
readonly olderThan: string | null;
|
|
397
|
+
readonly runs: readonly {
|
|
398
|
+
readonly id: string;
|
|
399
|
+
readonly name: string;
|
|
400
|
+
readonly path: string;
|
|
401
|
+
readonly status: "planned" | "pruned" | "skipped" | "failed";
|
|
402
|
+
readonly reason?: string;
|
|
403
|
+
}[];
|
|
404
|
+
};
|
|
405
|
+
|
|
386
406
|
export type NornRunCheckpoint = {
|
|
387
407
|
readonly id: string;
|
|
388
408
|
readonly path: string;
|
|
@@ -458,6 +478,11 @@ export type NornAgentBeforeSessionStartContext = {
|
|
|
458
478
|
readonly events: EventBus;
|
|
459
479
|
};
|
|
460
480
|
|
|
481
|
+
export type NornAgentModelReference = {
|
|
482
|
+
readonly provider: string;
|
|
483
|
+
readonly id: string;
|
|
484
|
+
};
|
|
485
|
+
|
|
461
486
|
export type NornAgentCreateSessionInput = {
|
|
462
487
|
readonly customTools?: readonly ToolDefinition[];
|
|
463
488
|
readonly label: string;
|
|
@@ -466,7 +491,10 @@ export type NornAgentCreateSessionInput = {
|
|
|
466
491
|
readonly beforeSessionStart?: (
|
|
467
492
|
context: NornAgentBeforeSessionStartContext,
|
|
468
493
|
) => MaybePromise<void>;
|
|
469
|
-
readonly
|
|
494
|
+
readonly models: readonly [
|
|
495
|
+
NornAgentModelReference,
|
|
496
|
+
...NornAgentModelReference[],
|
|
497
|
+
];
|
|
470
498
|
readonly thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
|
|
471
499
|
readonly systemPrompt?: string;
|
|
472
500
|
readonly appendSystemPrompt?: readonly string[];
|
package/dist/cli.js
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
import { readRunMetrics } from "./internal/metrics.js";
|
|
49
49
|
import { getRunLeaseOwner, NornRunLease } from "./internal/run-lease.js";
|
|
50
50
|
import { generateRunName } from "./internal/run-names.js";
|
|
51
|
+
import { NornRunPruner, parseRunPruneOptions } from "./internal/run-pruning.js";
|
|
51
52
|
import {
|
|
52
53
|
assertRunVersion,
|
|
53
54
|
getRunInfo,
|
|
@@ -394,6 +395,37 @@ var COMMANDS = [
|
|
|
394
395
|
await signalRun(run, "SIGKILL");
|
|
395
396
|
}
|
|
396
397
|
},
|
|
398
|
+
{
|
|
399
|
+
id: "runs.prune",
|
|
400
|
+
path: ["runs", "prune"],
|
|
401
|
+
description: "Discard checkpoint history or entire failed/completed runs, preserving recent and nonterminal runs.",
|
|
402
|
+
usage: "norn runs prune [--delete-runs] [--older-than <duration>|--all] [--dry-run]",
|
|
403
|
+
options: [
|
|
404
|
+
"--delete-runs: delete entire eligible runs instead of only checkpoint storage",
|
|
405
|
+
"--older-than <duration>: time since completion/failure; default 24h; units ms, s, m, h, d, w",
|
|
406
|
+
"--all: ignore age; requires --delete-runs; incompatible with --older-than",
|
|
407
|
+
"--dry-run: report eligible runs without changing files"
|
|
408
|
+
],
|
|
409
|
+
output: "JSON object under prune with mode, dryRun, olderThan, and per-run planned/pruned/skipped/failed results. Any failed result sets a nonzero exit status.",
|
|
410
|
+
examples: [
|
|
411
|
+
"norn runs prune --dry-run",
|
|
412
|
+
"norn runs prune --older-than 7d",
|
|
413
|
+
"norn runs prune --delete-runs --all"
|
|
414
|
+
],
|
|
415
|
+
execute: async (args) => {
|
|
416
|
+
const options = parseRunPruneOptions(args);
|
|
417
|
+
const project = await findNornProject(process.cwd());
|
|
418
|
+
const pruner = new NornRunPruner({
|
|
419
|
+
projectRoot: project.projectRoot,
|
|
420
|
+
options,
|
|
421
|
+
now: /* @__PURE__ */ new Date()
|
|
422
|
+
});
|
|
423
|
+
const result = await pruner.prune();
|
|
424
|
+
writeJson({ prune: result });
|
|
425
|
+
if (result.runs.some((run) => run.status === "failed"))
|
|
426
|
+
process.exitCode = 1;
|
|
427
|
+
}
|
|
428
|
+
},
|
|
397
429
|
{
|
|
398
430
|
id: "runs.delete",
|
|
399
431
|
path: ["runs", "delete"],
|
|
@@ -468,6 +500,7 @@ var HELP_COMMAND_ORDER = [
|
|
|
468
500
|
"runs.stop",
|
|
469
501
|
"runs.kill",
|
|
470
502
|
"runs.delete",
|
|
503
|
+
"runs.prune",
|
|
471
504
|
"project.init",
|
|
472
505
|
"project.inspect",
|
|
473
506
|
"pi",
|
|
@@ -495,6 +528,7 @@ var HUMAN_COMMAND_SUMMARIES = {
|
|
|
495
528
|
"runs.stop": "Stop a running run.",
|
|
496
529
|
"runs.kill": "Force-stop a running run.",
|
|
497
530
|
"runs.delete": "Delete an inactive run.",
|
|
531
|
+
"runs.prune": "Prune checkpoint storage or old failed/completed runs.",
|
|
498
532
|
"project.init": "Create a Norn project in the current directory.",
|
|
499
533
|
"project.inspect": "Inspect the Norn project.",
|
|
500
534
|
pi: "Run bundled Pi for authentication, providers, and model setup.",
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DeletedNornRunInfo, NornProjectInspection, NornWorkflowCatalogInfo, NornWorkflowInspection, NornRunCheckpoint, NornRunInfo, NornRunMetrics } from "@vimhead.dev/norn";
|
|
1
|
+
import type { DeletedNornRunInfo, NornProjectInspection, NornWorkflowCatalogInfo, NornWorkflowInspection, NornRunCheckpoint, NornRunInfo, NornRunPruneOptions, NornRunPruneResult, NornRunMetrics } from "@vimhead.dev/norn";
|
|
2
2
|
import type { NornRegisteredWorkflow } from "./internal/workflow-registry.ts";
|
|
3
3
|
export { NornProjectLoadError } from "./internal/errors.ts";
|
|
4
4
|
export type NornClientInput = {
|
|
@@ -35,6 +35,7 @@ export type NornClient = {
|
|
|
35
35
|
stop(run: string): Promise<NornRunInfo>;
|
|
36
36
|
kill(run: string): Promise<NornRunInfo>;
|
|
37
37
|
delete(run: string): Promise<DeletedNornRunInfo>;
|
|
38
|
+
prune(options: NornRunPruneOptions): Promise<NornRunPruneResult>;
|
|
38
39
|
logs(run: string, options?: {
|
|
39
40
|
readonly follow?: boolean;
|
|
40
41
|
}): AsyncIterable<Record<string, unknown>>;
|
package/dist/client.js
CHANGED
|
@@ -79,6 +79,14 @@ function createNornClient(input = {}) {
|
|
|
79
79
|
"delete",
|
|
80
80
|
run
|
|
81
81
|
])).deleted,
|
|
82
|
+
prune: async (options) => (await processRunner.readJson([
|
|
83
|
+
"runs",
|
|
84
|
+
"prune",
|
|
85
|
+
...options.deleteRuns ? ["--delete-runs"] : [],
|
|
86
|
+
...options.all ? ["--all"] : [],
|
|
87
|
+
...options.olderThan === null ? [] : ["--older-than", options.olderThan],
|
|
88
|
+
...options.dryRun ? ["--dry-run"] : []
|
|
89
|
+
])).prune,
|
|
82
90
|
logs: (run, options) => processRunner.readJsonLines([
|
|
83
91
|
"runs",
|
|
84
92
|
"logs",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare const NORN_GENERATED_BUILD_INFO: {
|
|
2
2
|
readonly kind: "npm-registry";
|
|
3
|
-
readonly version: "0.1.0-tip.
|
|
4
|
-
readonly commit: "
|
|
3
|
+
readonly version: "0.1.0-tip.37307659560.1";
|
|
4
|
+
readonly commit: "31ff51915f9cd5da805d67b7a4b23c2f10a6de59";
|
|
5
5
|
readonly packageSpec: "@vimhead.dev/norn-cli@tip";
|
|
6
6
|
readonly upgrade: {
|
|
7
7
|
readonly supported: false;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/generated-build-info.ts
|
|
2
2
|
var NORN_GENERATED_BUILD_INFO = {
|
|
3
3
|
"kind": "npm-registry",
|
|
4
|
-
"version": "0.1.0-tip.
|
|
5
|
-
"commit": "
|
|
4
|
+
"version": "0.1.0-tip.37307659560.1",
|
|
5
|
+
"commit": "31ff51915f9cd5da805d67b7a4b23c2f10a6de59",
|
|
6
6
|
"packageSpec": "@vimhead.dev/norn-cli@tip",
|
|
7
7
|
"upgrade": {
|
|
8
8
|
"supported": false,
|
|
@@ -8,7 +8,6 @@ type NornAgentRunnerInput = {
|
|
|
8
8
|
readonly id: string;
|
|
9
9
|
readonly runRoot: string;
|
|
10
10
|
readonly signal?: AbortSignal;
|
|
11
|
-
readonly model?: CreateAgentSessionOptions["model"];
|
|
12
11
|
readonly thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
|
|
13
12
|
readonly agentDir?: string;
|
|
14
13
|
readonly logs: NornRunLogs;
|
|
@@ -20,6 +19,7 @@ export declare class NornAgentRunner {
|
|
|
20
19
|
private readonly responseToolFactory;
|
|
21
20
|
constructor(input: NornAgentRunnerInput);
|
|
22
21
|
createSession(agentInput: NornAgentCreateSessionInput): Promise<NornAgentSession>;
|
|
22
|
+
private selectModel;
|
|
23
23
|
prompt<ResponseSchema extends TSchema>(agentInput: NornAgentSinglePromptInput<ResponseSchema>): Promise<StaticDecode<ResponseSchema>>;
|
|
24
24
|
}
|
|
25
25
|
export {};
|
package/dist/internal/agents.js
CHANGED
|
@@ -76,6 +76,7 @@ var NornAgentRunner = class {
|
|
|
76
76
|
...loader.getExtensions().extensions.flatMap((extension) => [...extension.tools.keys()])
|
|
77
77
|
]
|
|
78
78
|
});
|
|
79
|
+
const model = this.selectModel({ models: agentInput.models, services });
|
|
79
80
|
let session;
|
|
80
81
|
try {
|
|
81
82
|
({ session } = await createAgentSessionFromServices({
|
|
@@ -83,7 +84,7 @@ var NornAgentRunner = class {
|
|
|
83
84
|
sessionManager: SessionManager.create(cwd, sessionDir),
|
|
84
85
|
tools: withAgentResponseTool(agentInput.tools),
|
|
85
86
|
customTools: [this.responseToolFactory.create(), ...customTools],
|
|
86
|
-
model
|
|
87
|
+
model,
|
|
87
88
|
thinkingLevel: agentInput.thinkingLevel ?? this.input.thinkingLevel
|
|
88
89
|
}));
|
|
89
90
|
await agentInput.beforeSessionStart?.({ events: eventBus });
|
|
@@ -91,7 +92,8 @@ var NornAgentRunner = class {
|
|
|
91
92
|
await this.input.logger.record({
|
|
92
93
|
type: "agent.spawned",
|
|
93
94
|
label: agentInput.label,
|
|
94
|
-
cwd
|
|
95
|
+
cwd,
|
|
96
|
+
model: { provider: model.provider, id: model.id }
|
|
95
97
|
});
|
|
96
98
|
return new CreatedNornAgentSession({
|
|
97
99
|
...this.input,
|
|
@@ -112,6 +114,35 @@ var NornAgentRunner = class {
|
|
|
112
114
|
throw error;
|
|
113
115
|
}
|
|
114
116
|
}
|
|
117
|
+
selectModel(input) {
|
|
118
|
+
if (!Array.isArray(input.models) || input.models.length === 0)
|
|
119
|
+
throw new Error("Agent models must be a non-empty ordered list");
|
|
120
|
+
for (const [index, reference] of input.models.entries()) {
|
|
121
|
+
if (!reference || typeof reference.provider !== "string" || reference.provider.trim().length === 0 || typeof reference.id !== "string" || reference.id.trim().length === 0)
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Agent models[${index}] requires non-empty provider and id strings`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const reasons = [];
|
|
127
|
+
for (const reference of input.models) {
|
|
128
|
+
const name = `${reference.provider}/${reference.id}`;
|
|
129
|
+
const model = input.services.modelRuntime.getModel(
|
|
130
|
+
reference.provider,
|
|
131
|
+
reference.id
|
|
132
|
+
);
|
|
133
|
+
if (!model) {
|
|
134
|
+
reasons.push(`${name}: not registered`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!input.services.modelRuntime.hasConfiguredAuth(reference.provider)) {
|
|
138
|
+
reasons.push(`${name}: no configured credentials`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
return model;
|
|
142
|
+
}
|
|
143
|
+
throw new Error(`No usable agent models:
|
|
144
|
+
${reasons.join("\n")}`);
|
|
145
|
+
}
|
|
115
146
|
async prompt(agentInput) {
|
|
116
147
|
const agent = await this.createSession(agentInput);
|
|
117
148
|
try {
|
|
@@ -7,7 +7,6 @@ type NornExecutionContextInput = {
|
|
|
7
7
|
readonly id: string;
|
|
8
8
|
readonly runRoot: string;
|
|
9
9
|
readonly signal?: AbortSignal;
|
|
10
|
-
readonly model?: CreateAgentSessionOptions["model"];
|
|
11
10
|
readonly thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
|
|
12
11
|
readonly agentDir?: string;
|
|
13
12
|
readonly responseCollector: NornAgentResponseCollector;
|
|
@@ -10,6 +10,9 @@ function isNodeError(error) {
|
|
|
10
10
|
|
|
11
11
|
// ../core/src/atomic-files.ts
|
|
12
12
|
async function writeTextAtomically(path, content) {
|
|
13
|
+
await writeBytesAtomically(path, Buffer.from(content, "utf8"));
|
|
14
|
+
}
|
|
15
|
+
async function writeBytesAtomically(path, content) {
|
|
13
16
|
await mkdir(dirname(path), { recursive: true });
|
|
14
17
|
let existingMode;
|
|
15
18
|
try {
|
|
@@ -19,7 +22,7 @@ async function writeTextAtomically(path, content) {
|
|
|
19
22
|
}
|
|
20
23
|
const tmpPath = `${path}.${randomUUID()}.tmp`;
|
|
21
24
|
try {
|
|
22
|
-
await writeFile(tmpPath, content, {
|
|
25
|
+
await writeFile(tmpPath, content, { mode: existingMode });
|
|
23
26
|
if (existingMode !== void 0) await chmod(tmpPath, existingMode);
|
|
24
27
|
await rename(tmpPath, path);
|
|
25
28
|
} catch (error) {
|
|
@@ -19,6 +19,9 @@ async function writeJsonAtomically(path, value) {
|
|
|
19
19
|
`);
|
|
20
20
|
}
|
|
21
21
|
async function writeTextAtomically(path, content) {
|
|
22
|
+
await writeBytesAtomically(path, Buffer.from(content, "utf8"));
|
|
23
|
+
}
|
|
24
|
+
async function writeBytesAtomically(path, content) {
|
|
22
25
|
await mkdir(dirname(path), { recursive: true });
|
|
23
26
|
let existingMode;
|
|
24
27
|
try {
|
|
@@ -28,7 +31,7 @@ async function writeTextAtomically(path, content) {
|
|
|
28
31
|
}
|
|
29
32
|
const tmpPath = `${path}.${randomUUID()}.tmp`;
|
|
30
33
|
try {
|
|
31
|
-
await writeFile(tmpPath, content, {
|
|
34
|
+
await writeFile(tmpPath, content, { mode: existingMode });
|
|
32
35
|
if (existingMode !== void 0) await chmod(tmpPath, existingMode);
|
|
33
36
|
await rename(tmpPath, path);
|
|
34
37
|
} catch (error) {
|
|
@@ -17,6 +17,7 @@ export declare class NornRunLease {
|
|
|
17
17
|
private constructor();
|
|
18
18
|
static acquire(runRoot: string, processOwner?: NornRunProcessOwner): Promise<NornRunLease>;
|
|
19
19
|
assertOwned(): Promise<void>;
|
|
20
|
+
deleteRunDirectory(): Promise<void>;
|
|
20
21
|
release(): Promise<void>;
|
|
21
22
|
private get ownerPath();
|
|
22
23
|
private startHeartbeat;
|