@llblab/pi-actors 0.39.0 → 0.40.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/AGENTS.md +10 -3
- package/BACKLOG.md +4 -11
- package/CHANGELOG.md +51 -0
- package/README.md +5 -5
- package/dist/index.js +6 -4
- package/dist/lib/async-runs.d.ts +4 -0
- package/dist/lib/async-runs.js +112 -17
- package/dist/lib/command-templates.d.ts +9 -0
- package/dist/lib/command-templates.js +92 -11
- package/dist/lib/config.js +0 -5
- package/dist/lib/execution.d.ts +31 -0
- package/dist/lib/execution.js +145 -12
- package/dist/lib/file-state.d.ts +1 -0
- package/dist/lib/file-state.js +91 -3
- package/dist/lib/observability.js +5 -2
- package/dist/lib/prompts.d.ts +1 -2
- package/dist/lib/prompts.js +3 -4
- package/dist/lib/recipes-context.js +17 -9
- package/dist/lib/recipes-discovery.js +11 -5
- package/dist/lib/recipes-references.d.ts +1 -1
- package/dist/lib/recipes-references.js +4 -5
- package/dist/lib/recipes-usage.d.ts +2 -0
- package/dist/lib/recipes-usage.js +35 -21
- package/dist/lib/registry.d.ts +0 -2
- package/dist/lib/registry.js +33 -10
- package/dist/lib/runs-ownership.d.ts +7 -0
- package/dist/lib/runs-ownership.js +82 -0
- package/dist/lib/runs-process.d.ts +17 -2
- package/dist/lib/runs-process.js +99 -11
- package/dist/lib/runs-retention.d.ts +3 -0
- package/dist/lib/runs-retention.js +18 -3
- package/dist/lib/runs-start.d.ts +2 -2
- package/dist/lib/runs-start.js +51 -17
- package/dist/lib/runs-status.d.ts +1 -1
- package/dist/lib/runs-status.js +8 -6
- package/dist/lib/runtime.js +69 -13
- package/dist/lib/tools-inspect.d.ts +2 -0
- package/dist/lib/tools-inspect.js +39 -2
- package/dist/lib/tools-register.js +0 -1
- package/dist/lib/tools-spawn.js +3 -2
- package/dist/lib/tools.d.ts +1 -0
- package/dist/lib/tools.js +3 -0
- package/dist/pi-actors/index.js +1 -0
- package/dist/recipes/subagent-judge.json +2 -1
- package/dist/recipes/subagent-merge.json +2 -1
- package/dist/recipes/subagent-normalize.json +2 -1
- package/dist/recipes/subagent-review-coordinator.json +1 -1
- package/dist/recipes/subagent-review.json +2 -1
- package/dist/recipes/subagent-verify.json +2 -1
- package/dist/scripts/async-runner.mjs +274 -6
- package/dist/scripts/build-dist.mjs +14 -1
- package/dist/skills/actors/SKILL.md +15 -8
- package/dist/skills/swarm/SKILL.md +4 -2
- package/docs/actor-messages.md +1 -1
- package/docs/async-runs.md +14 -5
- package/docs/command-templates.md +4 -2
- package/docs/recipe-library.md +1 -0
- package/docs/template-recipes.md +5 -7
- package/docs/tool-registry.md +4 -2
- package/index.ts +18 -7
- package/lib/async-runs.ts +138 -19
- package/lib/command-templates.ts +132 -13
- package/lib/config.ts +0 -4
- package/lib/execution.ts +198 -13
- package/lib/file-state.ts +106 -3
- package/lib/observability.ts +8 -2
- package/lib/prompts.ts +3 -5
- package/lib/recipes-context.ts +17 -9
- package/lib/recipes-discovery.ts +10 -5
- package/lib/recipes-references.ts +5 -6
- package/lib/recipes-usage.ts +36 -20
- package/lib/registry.ts +43 -13
- package/lib/runs-ownership.ts +117 -0
- package/lib/runs-process.ts +138 -16
- package/lib/runs-retention.ts +22 -2
- package/lib/runs-start.ts +89 -31
- package/lib/runs-status.ts +15 -6
- package/lib/runtime.ts +64 -12
- package/lib/tools-inspect.ts +46 -4
- package/lib/tools-register.ts +0 -3
- package/lib/tools-spawn.ts +5 -5
- package/lib/tools.ts +8 -0
- package/package.json +2 -2
- package/recipes/subagent-judge.json +2 -1
- package/recipes/subagent-merge.json +2 -1
- package/recipes/subagent-normalize.json +2 -1
- package/recipes/subagent-review-coordinator.json +1 -1
- package/recipes/subagent-review.json +2 -1
- package/recipes/subagent-verify.json +2 -1
- package/scripts/async-runner.mjs +274 -6
- package/scripts/build-dist.mjs +14 -1
- package/skills/actors/SKILL.md +15 -8
- package/skills/swarm/SKILL.md +4 -2
|
@@ -2,11 +2,21 @@
|
|
|
2
2
|
* Async run retention operations.
|
|
3
3
|
* Owns terminal-run archive and prune filesystem behavior.
|
|
4
4
|
*/
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
5
6
|
import { cpSync, mkdirSync, renameSync, rmSync } from "node:fs";
|
|
6
7
|
import { basename, dirname, join } from "node:path";
|
|
7
8
|
import { writeJsonAtomic } from "./file-state.js";
|
|
8
9
|
import { resolveArtifactManifest, } from "./runs-artifacts.js";
|
|
9
10
|
import { safeRunId } from "./runs-identity.js";
|
|
11
|
+
import { assertOwnedRunStateDirectory } from "./runs-ownership.js";
|
|
12
|
+
function retainedArtifactFilename(name, path) {
|
|
13
|
+
const readableName = name.replace(/[^A-Za-z0-9_.-]+/g, "_") || "artifact";
|
|
14
|
+
const identity = createHash("sha256")
|
|
15
|
+
.update(`${name}\0${path}`)
|
|
16
|
+
.digest("hex")
|
|
17
|
+
.slice(0, 8);
|
|
18
|
+
return `${readableName}-${identity}--${basename(path)}`;
|
|
19
|
+
}
|
|
10
20
|
function archivePathFor(run, stateDir) {
|
|
11
21
|
const archiveRoot = join(dirname(stateDir), "archived");
|
|
12
22
|
mkdirSync(archiveRoot, { recursive: true });
|
|
@@ -15,6 +25,7 @@ function archivePathFor(run, stateDir) {
|
|
|
15
25
|
export function archiveTerminalRun(status) {
|
|
16
26
|
const stateDir = String(status.state_dir);
|
|
17
27
|
const run = String(status.run ?? basename(stateDir));
|
|
28
|
+
assertOwnedRunStateDirectory(stateDir, run);
|
|
18
29
|
const archiveDir = archivePathFor(run, stateDir);
|
|
19
30
|
renameSync(stateDir, archiveDir);
|
|
20
31
|
mkdirSync(stateDir, { recursive: true });
|
|
@@ -29,9 +40,10 @@ export function archiveTerminalRun(status) {
|
|
|
29
40
|
writeJsonAtomic(join(stateDir, "archive-tombstone.json"), tombstone);
|
|
30
41
|
return tombstone;
|
|
31
42
|
}
|
|
32
|
-
export function pruneTerminalRun(status, options = {}) {
|
|
43
|
+
export function pruneTerminalRun(status, options = {}, deps = {}) {
|
|
33
44
|
const stateDir = String(status.state_dir);
|
|
34
45
|
const run = String(status.run ?? basename(stateDir));
|
|
46
|
+
assertOwnedRunStateDirectory(stateDir, run);
|
|
35
47
|
const manifest = resolveArtifactManifest(status.artifacts);
|
|
36
48
|
const preserved = {};
|
|
37
49
|
if (options.preserveArtifacts && manifest) {
|
|
@@ -40,8 +52,11 @@ export function pruneTerminalRun(status, options = {}) {
|
|
|
40
52
|
for (const [name, artifact] of Object.entries(manifest)) {
|
|
41
53
|
if (!artifact.exists)
|
|
42
54
|
continue;
|
|
43
|
-
const target = join(preserveRoot,
|
|
44
|
-
cpSync(artifact.path, target, {
|
|
55
|
+
const target = join(preserveRoot, retainedArtifactFilename(name, artifact.path));
|
|
56
|
+
(deps.copyArtifact ?? cpSync)(artifact.path, target, {
|
|
57
|
+
force: true,
|
|
58
|
+
preserveTimestamps: true,
|
|
59
|
+
});
|
|
45
60
|
preserved[name] = target;
|
|
46
61
|
}
|
|
47
62
|
}
|
package/dist/lib/runs-start.d.ts
CHANGED
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
* preparation before a new runner process is spawned.
|
|
5
5
|
*/
|
|
6
6
|
export type RunJsonReader = (path: string) => Record<string, unknown> | undefined;
|
|
7
|
-
export declare function assertNoActiveRunState(stateDir: string, readJson: RunJsonReader,
|
|
7
|
+
export declare function assertNoActiveRunState(stateDir: string, readJson: RunJsonReader, _runnerPath: string): void;
|
|
8
8
|
export declare function acquireStateStartLock(stateDir: string): () => void;
|
|
9
|
-
export declare function prepareStateDirForStart(stateDir: string, readJson: RunJsonReader,
|
|
9
|
+
export declare function prepareStateDirForStart(stateDir: string, readJson: RunJsonReader, _runnerPath: string): void;
|
package/dist/lib/runs-start.js
CHANGED
|
@@ -3,33 +3,64 @@
|
|
|
3
3
|
* Owns active-run reuse checks, start lock acquisition, and safe state-dir
|
|
4
4
|
* preparation before a new runner process is spawned.
|
|
5
5
|
*/
|
|
6
|
-
import { mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
-
import { isAlive,
|
|
8
|
+
import { isAlive, readProcessIdentity, verifyRunProcessIdentity, } from "./runs-process.js";
|
|
9
9
|
const START_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
|
|
10
|
-
export function assertNoActiveRunState(stateDir, readJson,
|
|
10
|
+
export function assertNoActiveRunState(stateDir, readJson, _runnerPath) {
|
|
11
11
|
const meta = readJson(join(stateDir, "run.json"));
|
|
12
12
|
if (!meta)
|
|
13
13
|
return;
|
|
14
14
|
const pid = Number(meta.pid || 0);
|
|
15
15
|
const cwd = String(meta.cwd ?? "");
|
|
16
|
-
if (!pid || !isAlive(pid)
|
|
16
|
+
if (!pid || !isAlive(pid))
|
|
17
17
|
return;
|
|
18
|
-
|
|
18
|
+
const identity = verifyRunProcessIdentity(pid, meta.process_identity);
|
|
19
|
+
if (identity.status === "owner_mismatch") {
|
|
20
|
+
throw new Error(`Run state process identity does not match the live pid: ${String(meta.run ?? stateDir)}. Refusing to reuse the state directory while pid ${pid} is alive.`);
|
|
21
|
+
}
|
|
22
|
+
if (identity.status === "unsupported_proof") {
|
|
23
|
+
throw new Error(`Run state process identity proof is unavailable: ${String(meta.run ?? stateDir)}. Refusing to reuse the state directory while pid ${pid} is alive.`);
|
|
24
|
+
}
|
|
25
|
+
if (identity.valid) {
|
|
26
|
+
throw new Error(`Run state already has an active owned process: ${String(meta.run ?? stateDir)}. Stop it before reusing the same run_id or state_dir.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function writeStartLockOwner(lockDir, recovered = false) {
|
|
30
|
+
const processIdentity = readProcessIdentity(process.pid);
|
|
31
|
+
writeFileSync(join(lockDir, "owner.json"), `${JSON.stringify({
|
|
32
|
+
pid: process.pid,
|
|
33
|
+
createdAt: new Date().toISOString(),
|
|
34
|
+
...(processIdentity ? { process_identity: processIdentity } : {}),
|
|
35
|
+
...(recovered ? { recovered: true } : {}),
|
|
36
|
+
})}\n`, "utf8");
|
|
37
|
+
}
|
|
38
|
+
function isStartLockOwnerProvenDead(lockDir) {
|
|
39
|
+
try {
|
|
40
|
+
const owner = JSON.parse(readFileSync(join(lockDir, "owner.json"), "utf8"));
|
|
41
|
+
const pid = Number(owner.pid || 0);
|
|
42
|
+
if (!pid)
|
|
43
|
+
return false;
|
|
44
|
+
return verifyRunProcessIdentity(pid, owner.process_identity).status === "dead_pid";
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
19
49
|
}
|
|
20
50
|
export function acquireStateStartLock(stateDir) {
|
|
21
51
|
const lockDir = join(stateDir, ".start.lock");
|
|
22
52
|
try {
|
|
23
53
|
mkdirSync(lockDir);
|
|
24
|
-
|
|
54
|
+
writeStartLockOwner(lockDir);
|
|
25
55
|
}
|
|
26
56
|
catch (error) {
|
|
27
57
|
try {
|
|
28
58
|
const stat = statSync(lockDir);
|
|
29
|
-
if (Date.now() - stat.mtimeMs > START_LOCK_MAX_AGE_MS
|
|
59
|
+
if (Date.now() - stat.mtimeMs > START_LOCK_MAX_AGE_MS &&
|
|
60
|
+
isStartLockOwnerProvenDead(lockDir)) {
|
|
30
61
|
rmSync(lockDir, { recursive: true, force: true });
|
|
31
62
|
mkdirSync(lockDir);
|
|
32
|
-
|
|
63
|
+
writeStartLockOwner(lockDir, true);
|
|
33
64
|
return () => rmSync(lockDir, { recursive: true, force: true });
|
|
34
65
|
}
|
|
35
66
|
}
|
|
@@ -40,17 +71,20 @@ export function acquireStateStartLock(stateDir) {
|
|
|
40
71
|
}
|
|
41
72
|
return () => rmSync(lockDir, { recursive: true, force: true });
|
|
42
73
|
}
|
|
43
|
-
export function prepareStateDirForStart(stateDir, readJson,
|
|
74
|
+
export function prepareStateDirForStart(stateDir, readJson, _runnerPath) {
|
|
44
75
|
const existing = readJson(join(stateDir, "run.json"));
|
|
45
76
|
const existingPid = Number(existing?.pid || 0);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
if (existingPid && isAlive(existingPid)) {
|
|
78
|
+
const identity = verifyRunProcessIdentity(existingPid, existing?.process_identity);
|
|
79
|
+
if (identity.status === "owner_mismatch") {
|
|
80
|
+
throw new Error(`Run state process identity does not match the live pid: ${String(existing?.run ?? stateDir)}. Refusing to prepare the state directory while pid ${existingPid} is alive.`);
|
|
81
|
+
}
|
|
82
|
+
if (identity.status === "unsupported_proof") {
|
|
83
|
+
throw new Error(`Run state process identity proof is unavailable: ${String(existing?.run ?? stateDir)}. Refusing to prepare the state directory while pid ${existingPid} is alive.`);
|
|
84
|
+
}
|
|
85
|
+
if (identity.valid) {
|
|
86
|
+
throw new Error(`Run state already has an active owned process: ${String(existing?.run ?? stateDir)}. Stop it before restarting.`);
|
|
87
|
+
}
|
|
54
88
|
}
|
|
55
89
|
for (const file of [
|
|
56
90
|
"events.jsonl",
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
* Owns status derivation from run state files plus bounded log/event tails.
|
|
4
4
|
*/
|
|
5
5
|
export type RunJsonReader = (path: string) => Record<string, unknown> | undefined;
|
|
6
|
-
export declare function buildRunStatus(stateDir: string, runOrDir: string, meta: Record<string, unknown>, readJson: RunJsonReader,
|
|
6
|
+
export declare function buildRunStatus(stateDir: string, runOrDir: string, meta: Record<string, unknown>, readJson: RunJsonReader, _runnerPath: string, runnerIdentityGraceMs: number): Record<string, unknown>;
|
|
7
7
|
export declare function tailFile(path: string, lines: number): string;
|
|
8
8
|
export declare function tailLines(path: string, lines: number): string[];
|
package/dist/lib/runs-status.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
-
import {
|
|
7
|
+
import { isWithinRunnerIdentityGrace, verifyRunProcessIdentity, } from "./runs-process.js";
|
|
8
8
|
import { readJsonlFileResilient } from "./state-readers.js";
|
|
9
9
|
function getInterruptedRunStatus(stateDir) {
|
|
10
10
|
const events = readJsonlFileResilient(join(stateDir, "events.jsonl")).records.slice(-200);
|
|
@@ -16,14 +16,14 @@ function getInterruptedRunStatus(stateDir) {
|
|
|
16
16
|
}
|
|
17
17
|
return undefined;
|
|
18
18
|
}
|
|
19
|
-
export function buildRunStatus(stateDir, runOrDir, meta, readJson,
|
|
19
|
+
export function buildRunStatus(stateDir, runOrDir, meta, readJson, _runnerPath, runnerIdentityGraceMs) {
|
|
20
20
|
const result = readJson(join(stateDir, "result.json"));
|
|
21
21
|
const pid = Number(meta.pid || 0);
|
|
22
|
+
const processIdentity = verifyRunProcessIdentity(pid, meta.process_identity);
|
|
22
23
|
const aliveOwnedRunner = Boolean(pid &&
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
isWithinRunnerIdentityGrace(meta, runnerIdentityGraceMs)));
|
|
24
|
+
(processIdentity.valid ||
|
|
25
|
+
(processIdentity.status === "unsupported_proof" &&
|
|
26
|
+
isWithinRunnerIdentityGrace(meta, runnerIdentityGraceMs))));
|
|
27
27
|
const status = result
|
|
28
28
|
? Number(result.code ?? 0) === 0
|
|
29
29
|
? "done"
|
|
@@ -35,8 +35,10 @@ export function buildRunStatus(stateDir, runOrDir, meta, readJson, runnerPath, r
|
|
|
35
35
|
return {
|
|
36
36
|
...meta,
|
|
37
37
|
eventsFile: join(stateDir, "events.jsonl"),
|
|
38
|
+
evidenceFile: join(stateDir, "review-evidence.json"),
|
|
38
39
|
inboxFile: join(stateDir, "inbox.jsonl"),
|
|
39
40
|
outboxFile: join(stateDir, "outbox.jsonl"),
|
|
41
|
+
process_identity_status: processIdentity.status,
|
|
40
42
|
progress: readJson(join(stateDir, "progress.json")) || null,
|
|
41
43
|
result: result || null,
|
|
42
44
|
...(terminalHandled ? { terminal_handled: terminalHandled } : {}),
|
package/dist/lib/runtime.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Owns persisted tool loading, reserved-name guards, runtime registration, and warning notification
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, watch } from "node:fs";
|
|
7
|
+
import { basename, dirname } from "node:path";
|
|
7
8
|
import * as Paths from "./paths.js";
|
|
8
9
|
import * as RecipesDiscovery from "./recipes-discovery.js";
|
|
9
10
|
import * as ToolsLocal from "./tools-local.js";
|
|
@@ -126,10 +127,13 @@ export function createAutoToolsRuntime(deps) {
|
|
|
126
127
|
export function createRecipeToolReloadWatcher(runtime) {
|
|
127
128
|
let reloadTimeout;
|
|
128
129
|
let rootWatcher;
|
|
130
|
+
let parentWatcher;
|
|
129
131
|
let failureNotified = false;
|
|
130
132
|
const close = () => {
|
|
131
133
|
rootWatcher?.close();
|
|
132
134
|
rootWatcher = undefined;
|
|
135
|
+
parentWatcher?.close();
|
|
136
|
+
parentWatcher = undefined;
|
|
133
137
|
if (reloadTimeout)
|
|
134
138
|
clearTimeout(reloadTimeout);
|
|
135
139
|
reloadTimeout = undefined;
|
|
@@ -150,23 +154,75 @@ export function createRecipeToolReloadWatcher(runtime) {
|
|
|
150
154
|
}, 150);
|
|
151
155
|
reloadTimeout.unref?.();
|
|
152
156
|
};
|
|
157
|
+
const watchParent = (ctx, recipeRoot) => {
|
|
158
|
+
if (parentWatcher || rootWatcher)
|
|
159
|
+
return;
|
|
160
|
+
const parent = dirname(recipeRoot);
|
|
161
|
+
if (!existsSync(parent)) {
|
|
162
|
+
notifyFailure(ctx);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
parentWatcher = watch(parent, (_event, changedFile) => {
|
|
167
|
+
if (changedFile &&
|
|
168
|
+
String(changedFile) !== basename(recipeRoot) &&
|
|
169
|
+
!existsSync(recipeRoot)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (!existsSync(recipeRoot))
|
|
173
|
+
return;
|
|
174
|
+
parentWatcher?.close();
|
|
175
|
+
parentWatcher = undefined;
|
|
176
|
+
watchRoot(ctx, recipeRoot);
|
|
177
|
+
scheduleReload(ctx);
|
|
178
|
+
});
|
|
179
|
+
parentWatcher.on("error", () => {
|
|
180
|
+
parentWatcher?.close();
|
|
181
|
+
parentWatcher = undefined;
|
|
182
|
+
notifyFailure(ctx);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
notifyFailure(ctx);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const watchRoot = (ctx, recipeRoot) => {
|
|
190
|
+
if (rootWatcher)
|
|
191
|
+
return;
|
|
192
|
+
if (!existsSync(recipeRoot)) {
|
|
193
|
+
watchParent(ctx, recipeRoot);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
rootWatcher = watch(recipeRoot, () => {
|
|
198
|
+
if (!existsSync(recipeRoot)) {
|
|
199
|
+
rootWatcher?.close();
|
|
200
|
+
rootWatcher = undefined;
|
|
201
|
+
scheduleReload(ctx);
|
|
202
|
+
watchParent(ctx, recipeRoot);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
scheduleReload(ctx);
|
|
206
|
+
});
|
|
207
|
+
rootWatcher.on("error", () => {
|
|
208
|
+
rootWatcher?.close();
|
|
209
|
+
rootWatcher = undefined;
|
|
210
|
+
if (existsSync(recipeRoot))
|
|
211
|
+
notifyFailure(ctx);
|
|
212
|
+
else
|
|
213
|
+
watchParent(ctx, recipeRoot);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
notifyFailure(ctx);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
153
220
|
return {
|
|
154
221
|
close,
|
|
155
222
|
watch(ctx) {
|
|
156
|
-
|
|
157
|
-
if (rootWatcher || !existsSync(recipeRoot))
|
|
223
|
+
if (rootWatcher || parentWatcher)
|
|
158
224
|
return;
|
|
159
|
-
|
|
160
|
-
rootWatcher = watch(recipeRoot, () => scheduleReload(ctx));
|
|
161
|
-
rootWatcher.on("error", () => {
|
|
162
|
-
rootWatcher?.close();
|
|
163
|
-
rootWatcher = undefined;
|
|
164
|
-
notifyFailure(ctx);
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
notifyFailure(ctx);
|
|
169
|
-
}
|
|
225
|
+
watchRoot(ctx, Paths.getRecipeRoot());
|
|
170
226
|
},
|
|
171
227
|
};
|
|
172
228
|
}
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* Owns the public inspect execution path and compact observation responses
|
|
5
5
|
*/
|
|
6
6
|
export interface InspectToolDeps<TContext = unknown> {
|
|
7
|
+
getRunStatus?: (runOrDir: string) => Record<string, any>;
|
|
7
8
|
getTool?: (name: string) => any | undefined;
|
|
9
|
+
listRuns?: () => Array<Record<string, any>>;
|
|
8
10
|
packagedRecipeRoot?: string;
|
|
9
11
|
recipeRoot?: string;
|
|
10
12
|
}
|
|
@@ -122,6 +122,28 @@ function compactArtifactPath(value) {
|
|
|
122
122
|
const record = asRecord(value);
|
|
123
123
|
return String(record.path ?? "<missing>");
|
|
124
124
|
}
|
|
125
|
+
function readBoundedReviewEvidence(status, limit) {
|
|
126
|
+
const evidenceFile = status.evidenceFile;
|
|
127
|
+
if (typeof evidenceFile !== "string" || !existsSync(evidenceFile))
|
|
128
|
+
return undefined;
|
|
129
|
+
try {
|
|
130
|
+
const manifest = asRecord(JSON.parse(readFileSync(evidenceFile, "utf8")));
|
|
131
|
+
const commands = Array.isArray(manifest.commands) ? manifest.commands : [];
|
|
132
|
+
const bounded = commands.slice(-Math.max(1, limit));
|
|
133
|
+
return {
|
|
134
|
+
...manifest,
|
|
135
|
+
commands: bounded,
|
|
136
|
+
commands_total: commands.length,
|
|
137
|
+
...(bounded.length < commands.length ? { commands_truncated: true } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return {
|
|
142
|
+
status: "invalid",
|
|
143
|
+
error: "review evidence manifest is unreadable",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
125
147
|
function compactActorFiles(status) {
|
|
126
148
|
const run = String(status.run ?? "<unknown>");
|
|
127
149
|
const artifacts = asRecord(status.artifacts);
|
|
@@ -130,6 +152,7 @@ function compactActorFiles(status) {
|
|
|
130
152
|
status.stderrLog,
|
|
131
153
|
status.eventsFile,
|
|
132
154
|
status.outboxFile,
|
|
155
|
+
status.evidenceFile,
|
|
133
156
|
status.state_dir
|
|
134
157
|
? `${String(status.state_dir)}/communication.json`
|
|
135
158
|
: undefined,
|
|
@@ -268,7 +291,16 @@ function isTriageHighRiskRecipe(recipe) {
|
|
|
268
291
|
function getPiActorsTriage(ctx, deps) {
|
|
269
292
|
const runtime = getPiActorsRuntimeStatus();
|
|
270
293
|
const currentSession = ToolsAccess.getContextSessionId(ctx);
|
|
271
|
-
const
|
|
294
|
+
const listRuns = deps.listRuns ?? AsyncRuns.listRuns;
|
|
295
|
+
const getRunStatus = deps.getRunStatus ?? AsyncRuns.getRunStatus;
|
|
296
|
+
const allRuns = listRuns().flatMap((run) => {
|
|
297
|
+
try {
|
|
298
|
+
return [getRunStatus(String(run.state_dir))];
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
});
|
|
272
304
|
const visibleRuns = currentSession
|
|
273
305
|
? allRuns.filter((run) => !run.ownerId || run.ownerId === currentSession)
|
|
274
306
|
: allRuns;
|
|
@@ -644,13 +676,18 @@ export function createInspectToolDefinition(deps = {}) {
|
|
|
644
676
|
case "files": {
|
|
645
677
|
const status = ToolsAccess.assertRunAccessibleToContext(runId, ctx);
|
|
646
678
|
const artifactManifest = AsyncRuns.resolveArtifactManifest(status.artifacts);
|
|
679
|
+
const reviewEvidence = readBoundedReviewEvidence(status, Number(input.lines || Limits.DEFAULT_INSPECT_LINES));
|
|
647
680
|
const details = artifactManifest
|
|
648
681
|
? {
|
|
649
682
|
...status,
|
|
683
|
+
...(reviewEvidence ? { review_evidence: reviewEvidence } : {}),
|
|
650
684
|
artifact_manifest: artifactManifest,
|
|
651
685
|
next_actions: ToolsResponse.artifactNextActions(status.run ?? runId, asRecord(status.artifacts)),
|
|
652
686
|
}
|
|
653
|
-
:
|
|
687
|
+
: {
|
|
688
|
+
...status,
|
|
689
|
+
...(reviewEvidence ? { review_evidence: reviewEvidence } : {}),
|
|
690
|
+
};
|
|
654
691
|
return {
|
|
655
692
|
content: [
|
|
656
693
|
{
|
|
@@ -26,7 +26,6 @@ export function createRegisterToolDefinition(deps) {
|
|
|
26
26
|
description: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.description),
|
|
27
27
|
draft: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.draft),
|
|
28
28
|
name: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.name),
|
|
29
|
-
state_dir: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.state_dir),
|
|
30
29
|
template: unionSchema([
|
|
31
30
|
stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
|
|
32
31
|
looseObjectSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
|
package/dist/lib/tools-spawn.js
CHANGED
|
@@ -94,7 +94,6 @@ export function createSpawnToolDefinition() {
|
|
|
94
94
|
as: Schema.stringSchema("Optional actor address for the spawned run, e.g. run:<id>."),
|
|
95
95
|
file: Schema.stringSchema("Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes."),
|
|
96
96
|
recipe: Schema.stringSchema("Alias for file; template recipe JSON file/name to spawn."),
|
|
97
|
-
state_dir: Schema.stringSchema("Optional explicit run state directory."),
|
|
98
97
|
template: Schema.unionSchema([
|
|
99
98
|
Schema.stringSchema("Inline command template string"),
|
|
100
99
|
Schema.arraySchema("Inline command-template sequence or parallel tree"),
|
|
@@ -105,6 +104,9 @@ export function createSpawnToolDefinition() {
|
|
|
105
104
|
}, []),
|
|
106
105
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
107
106
|
const input = asRecord(params);
|
|
107
|
+
if (input.state_dir !== undefined) {
|
|
108
|
+
throw new Error("spawn.state_dir is not supported; run state is runtime-owned so run:<id> remains addressable and retention-safe.");
|
|
109
|
+
}
|
|
108
110
|
const runId = runIdFromActorAddress(typeof input.as === "string" ? input.as : undefined);
|
|
109
111
|
const recipe = typeof input.file === "string"
|
|
110
112
|
? input.file
|
|
@@ -118,7 +120,6 @@ export function createSpawnToolDefinition() {
|
|
|
118
120
|
launch_source: "spawn",
|
|
119
121
|
ownerId: getRunOwnerId(ctx),
|
|
120
122
|
run_id: runId,
|
|
121
|
-
state_dir: typeof input.state_dir === "string" ? input.state_dir : undefined,
|
|
122
123
|
...(input.template !== undefined
|
|
123
124
|
? {
|
|
124
125
|
template: input.template,
|
package/dist/lib/tools.d.ts
CHANGED
|
@@ -18,5 +18,6 @@ export interface CoreActorToolDefinitionDeps<TContext extends RuntimeToolContext
|
|
|
18
18
|
registryRuntime: Pick<RegisterToolRuntimeDeps<TContext>, "getToolNameBlocker" | "getTools" | "notify" | "registerRuntimeTool">;
|
|
19
19
|
setActiveTools: (toolNames: string[]) => void;
|
|
20
20
|
}
|
|
21
|
+
export declare function resolveActiveRuntimeTool(name: string, activeTools: Pick<Map<string, unknown>, "has">, getDefinition: (name: string) => unknown): unknown;
|
|
21
22
|
export declare const RESERVED_TOOL_NAMES: Set<string>;
|
|
22
23
|
export declare function createCoreActorToolDefinitions<TContext extends RuntimeToolContext>(deps: CoreActorToolDefinitionDeps<TContext>): ActorToolDefinition[];
|
package/dist/lib/tools.js
CHANGED
|
@@ -7,6 +7,9 @@ import * as ToolsInspect from "./tools-inspect.js";
|
|
|
7
7
|
import * as ToolsMessage from "./tools-message.js";
|
|
8
8
|
import * as ToolsRegister from "./tools-register.js";
|
|
9
9
|
import * as ToolsSpawn from "./tools-spawn.js";
|
|
10
|
+
export function resolveActiveRuntimeTool(name, activeTools, getDefinition) {
|
|
11
|
+
return activeTools.has(name) ? getDefinition(name) : undefined;
|
|
12
|
+
}
|
|
10
13
|
export const RESERVED_TOOL_NAMES = new Set([
|
|
11
14
|
"read",
|
|
12
15
|
"write",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "../index.js";
|
|
@@ -28,5 +28,6 @@
|
|
|
28
28
|
"run.failed"
|
|
29
29
|
]
|
|
30
30
|
},
|
|
31
|
-
"
|
|
31
|
+
"accept_output": "review_evidence",
|
|
32
|
+
"template": "pi -p --model {model} --thinking {thinking} {tools?--tools:--no-tools} {tools} Judge this report: {report}. Rubric: {rubric}. Evidence: {evidence}. Begin the response with ACTOR_REVIEW_RESULT on its own line. Output format: {output_format}"
|
|
32
33
|
}
|
|
@@ -30,5 +30,6 @@
|
|
|
30
30
|
"run.failed"
|
|
31
31
|
]
|
|
32
32
|
},
|
|
33
|
-
"
|
|
33
|
+
"accept_output": "review_evidence",
|
|
34
|
+
"template": "pi -p --model {model} --thinking {thinking} {tools?--tools:--no-tools} {tools} Merge these subagent outputs using mode {mode}: {inputs}. Merge policy: {merge_policy}. Risk policy: {risk_policy}. Begin the response with ACTOR_REVIEW_RESULT on its own line. Output format: {output_format}"
|
|
34
35
|
}
|
|
@@ -26,5 +26,6 @@
|
|
|
26
26
|
"run.failed"
|
|
27
27
|
]
|
|
28
28
|
},
|
|
29
|
-
"
|
|
29
|
+
"accept_output": "review_evidence",
|
|
30
|
+
"template": "pi -p --model {model} --thinking {thinking} {tools?--tools:--no-tools} {tools} Normalize this subagent output. Begin the response with ACTOR_REVIEW_RESULT on its own line. Target format: {format}. Preservation policy: {preservation_policy}. Input: {input}"
|
|
30
31
|
}
|
|
@@ -174,7 +174,7 @@
|
|
|
174
174
|
"values": {
|
|
175
175
|
"input": "Use merged and judged output from stdin.",
|
|
176
176
|
"format": "{output_format}",
|
|
177
|
-
"preservation_policy": "Preserve branch status and explicitly mark Status as complete, degraded, or insufficient_data.",
|
|
177
|
+
"preservation_policy": "Preserve branch status and explicitly mark Status as complete, degraded, or insufficient_data. Include stable ACTOR_EVIDENCE_REF values from stdin in the Evidence section for every reviewer, verifier, merger, and judge source used. Do not claim complete when any required evidence reference is absent or inconsistent.",
|
|
178
178
|
"model": "{merger_model}",
|
|
179
179
|
"thinking": "{thinking}",
|
|
180
180
|
"tools": "{tools}"
|
|
@@ -32,5 +32,6 @@
|
|
|
32
32
|
"run.failed"
|
|
33
33
|
]
|
|
34
34
|
},
|
|
35
|
-
"
|
|
35
|
+
"accept_output": "review_evidence",
|
|
36
|
+
"template": "pi -p --model {model} --thinking {thinking} {tools?--tools:--no-tools} {tools} Review {scope} through this lens: {lens}. Instructions: {instructions}. Evidence policy: {evidence_policy}. Risk policy: {risk_policy}. Begin the response with ACTOR_REVIEW_RESULT on its own line. Output format: {output_format}"
|
|
36
37
|
}
|
|
@@ -29,5 +29,6 @@
|
|
|
29
29
|
"run.failed"
|
|
30
30
|
]
|
|
31
31
|
},
|
|
32
|
-
"
|
|
32
|
+
"accept_output": "review_evidence",
|
|
33
|
+
"template": "pi -p --model {model} --thinking {thinking} {tools?--tools:--no-tools} {tools} Verify this claim against the provided evidence. Claim: {claim}. Evidence or artifact paths: {evidence}. Acceptance: {acceptance}. Evidence policy: {evidence_policy}. Begin the response with ACTOR_REVIEW_RESULT on its own line. Output format: {output_format}"
|
|
33
34
|
}
|