@llblab/pi-actors 0.42.0 → 0.42.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/BACKLOG.md +1 -5
- package/CHANGELOG.md +10 -0
- package/README.md +2 -0
- package/dist/lib/async-runs.d.ts +12 -0
- package/dist/lib/async-runs.js +46 -5
- package/dist/lib/command-templates.js +1 -1
- package/dist/lib/inspector-overlay.d.ts +4 -1
- package/dist/lib/inspector-overlay.js +71 -15
- package/dist/lib/inspector.js +1 -1
- package/dist/lib/observability.d.ts +12 -0
- package/dist/lib/observability.js +149 -5
- package/dist/lib/runs-control.d.ts +2 -0
- package/dist/lib/runs-control.js +14 -1
- package/dist/lib/runs-ownership.js +17 -3
- package/dist/lib/runs-process.js +4 -3
- package/dist/lib/runs-start.js +1 -0
- package/dist/lib/runs-status.js +3 -0
- package/dist/lib/tools-inspect.js +2 -1
- package/dist/lib/tools-local.js +17 -2
- package/dist/lib/tools-spawn.js +10 -1
- package/dist/scripts/async-runner.mjs +9 -9
- package/dist/scripts/build-dist.mjs +6 -1
- package/dist/scripts/conformance.mjs +6 -1
- package/dist/scripts/recipe-utils.mjs +3 -3
- package/dist/skills/actors/SKILL.md +1 -1
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/actor-inspector.md +1 -1
- package/docs/async-runs.md +7 -1
- package/docs/tool-registry.md +2 -0
- package/lib/async-runs.ts +65 -5
- package/lib/command-templates.ts +1 -1
- package/lib/inspector-overlay.ts +67 -15
- package/lib/inspector.ts +1 -1
- package/lib/observability.ts +180 -4
- package/lib/runs-control.ts +20 -1
- package/lib/runs-ownership.ts +22 -3
- package/lib/runs-process.ts +4 -3
- package/lib/runs-start.ts +1 -0
- package/lib/runs-status.ts +5 -0
- package/lib/tools-inspect.ts +2 -1
- package/lib/tools-local.ts +21 -2
- package/lib/tools-spawn.ts +14 -1
- package/package.json +4 -3
- package/scripts/async-runner.mjs +9 -9
- package/scripts/build-dist.mjs +6 -1
- package/scripts/conformance.mjs +6 -1
- package/scripts/recipe-utils.mjs +3 -3
- package/skills/actors/SKILL.md +1 -1
- package/skills/swarm/SKILL.md +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Async run process control primitives.
|
|
3
3
|
* Owns: platform signal planning, owned-process signalling, and terminal control markers.
|
|
4
4
|
*/
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
5
6
|
import { type RunProcessIdentity, type RunProcessIdentityResult } from "./runs-process.ts";
|
|
6
7
|
export interface RunProcessSignalPlan {
|
|
7
8
|
args?: string[];
|
|
@@ -12,6 +13,7 @@ export declare function getRunProcessSignalPlan(pid: number, signal: NodeJS.Sign
|
|
|
12
13
|
export interface RunProcessSignalDeps {
|
|
13
14
|
killProcess?: typeof process.kill;
|
|
14
15
|
runtimePlatform?: NodeJS.Platform;
|
|
16
|
+
spawnProcess?: typeof spawnSync;
|
|
15
17
|
verifyIdentity?: (pid: number, expected: RunProcessIdentity, runtimePlatform: NodeJS.Platform) => RunProcessIdentityResult;
|
|
16
18
|
}
|
|
17
19
|
export declare function signalOwnedRunProcess(pid: number, signal: NodeJS.Signals, expectedIdentity?: RunProcessIdentity, deps?: RunProcessSignalDeps): RunProcessSignalPlan;
|
package/dist/lib/runs-control.js
CHANGED
|
@@ -31,8 +31,21 @@ export function signalOwnedRunProcess(pid, signal, expectedIdentity, deps = {})
|
|
|
31
31
|
}
|
|
32
32
|
const plan = getRunProcessSignalPlan(pid, signal, runtimePlatform);
|
|
33
33
|
if (plan.command && plan.args) {
|
|
34
|
-
const
|
|
34
|
+
const spawnProcess = deps.spawnProcess ?? spawnSync;
|
|
35
|
+
let result = spawnProcess(plan.command, plan.args, { encoding: "utf8" });
|
|
36
|
+
if (runtimePlatform === "win32" &&
|
|
37
|
+
result.status !== 0 &&
|
|
38
|
+
!plan.args.includes("/F")) {
|
|
39
|
+
result = spawnProcess(plan.command, [...plan.args, "/F"], {
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
35
43
|
if (result.status !== 0) {
|
|
44
|
+
if (expectedIdentity) {
|
|
45
|
+
const finalProof = (deps.verifyIdentity ?? verifyRunProcessIdentity)(pid, expectedIdentity, runtimePlatform);
|
|
46
|
+
if (finalProof.status === "dead_pid")
|
|
47
|
+
return plan;
|
|
48
|
+
}
|
|
36
49
|
throw new Error(result.stderr?.trim() ||
|
|
37
50
|
result.stdout?.trim() ||
|
|
38
51
|
`${plan.command} failed`);
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, } from "node:fs";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import {
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
8
9
|
import { writeJsonAtomic } from "./file-state.js";
|
|
9
10
|
export const RUN_STATE_OWNERSHIP_FILE = ".pi-actors-run-state.json";
|
|
10
11
|
function markerPath(stateDir) {
|
|
@@ -14,13 +15,26 @@ function comparablePath(path) {
|
|
|
14
15
|
const resolved = resolve(path);
|
|
15
16
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
16
17
|
}
|
|
18
|
+
function isSystemTempRootAlias(resolved, canonical) {
|
|
19
|
+
const tempRoot = resolve(tmpdir());
|
|
20
|
+
const relativeStateDir = relative(tempRoot, resolved);
|
|
21
|
+
if (relativeStateDir === ".." ||
|
|
22
|
+
relativeStateDir.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) ||
|
|
23
|
+
isAbsolute(relativeStateDir)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const canonicalTempRoot = realpathSync.native(tempRoot);
|
|
27
|
+
const expectedCanonical = resolve(canonicalTempRoot, relativeStateDir);
|
|
28
|
+
return comparablePath(canonical) === comparablePath(expectedCanonical);
|
|
29
|
+
}
|
|
17
30
|
function assertCanonicalDirectory(stateDir) {
|
|
18
31
|
const resolved = resolve(stateDir);
|
|
19
32
|
if (lstatSync(resolved).isSymbolicLink()) {
|
|
20
33
|
throw new Error(`Run state directory cannot be a symlink: ${resolved}`);
|
|
21
34
|
}
|
|
22
|
-
const canonical = realpathSync(resolved);
|
|
23
|
-
if (comparablePath(canonical) !== comparablePath(resolved)
|
|
35
|
+
const canonical = realpathSync.native(resolved);
|
|
36
|
+
if (comparablePath(canonical) !== comparablePath(resolved) &&
|
|
37
|
+
!isSystemTempRootAlias(resolved, canonical)) {
|
|
24
38
|
throw new Error(`Run state directory has an ambiguous symlink alias: ${resolved}`);
|
|
25
39
|
}
|
|
26
40
|
return resolved;
|
package/dist/lib/runs-process.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { existsSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
|
|
7
7
|
import { platform } from "node:os";
|
|
8
|
-
import {
|
|
8
|
+
import { posix, win32 } from "node:path";
|
|
9
9
|
export function isAlive(pid) {
|
|
10
10
|
try {
|
|
11
11
|
process.kill(pid, 0);
|
|
@@ -90,8 +90,9 @@ export function captureRunProcessIdentity(pid, cwd, stateDir, runnerPath, runtim
|
|
|
90
90
|
if (!identity.command.includes(runnerPath) || !identity.command.includes(stateDir)) {
|
|
91
91
|
return undefined;
|
|
92
92
|
}
|
|
93
|
-
const
|
|
94
|
-
const
|
|
93
|
+
const pathApi = runtimePlatform === "win32" ? win32 : posix;
|
|
94
|
+
const resolvedCwd = pathApi.resolve(cwd);
|
|
95
|
+
const canonicalCwd = runtimePlatform === platform() && existsSync(resolvedCwd)
|
|
95
96
|
? realpathSync.native(resolvedCwd)
|
|
96
97
|
: resolvedCwd;
|
|
97
98
|
const expectedCwd = runtimePlatform === "win32" ? canonicalCwd.toLowerCase() : canonicalCwd;
|
package/dist/lib/runs-start.js
CHANGED
package/dist/lib/runs-status.js
CHANGED
|
@@ -30,6 +30,7 @@ export function buildRunStatus(stateDir, runOrDir, meta, readJson, _runnerPath,
|
|
|
30
30
|
? "running"
|
|
31
31
|
: (getInterruptedRunStatus(stateDir) ?? "exited");
|
|
32
32
|
const terminalHandled = readJson(join(stateDir, "terminal-handled.json"));
|
|
33
|
+
const terminalDeliveryFailure = readJson(join(stateDir, "terminal-delivery-failure.json"));
|
|
33
34
|
return {
|
|
34
35
|
...meta,
|
|
35
36
|
eventsFile: join(stateDir, "events.jsonl"),
|
|
@@ -39,6 +40,8 @@ export function buildRunStatus(stateDir, runOrDir, meta, readJson, _runnerPath,
|
|
|
39
40
|
process_identity_status: processIdentity.status,
|
|
40
41
|
progress: readJson(join(stateDir, "progress.json")) || null,
|
|
41
42
|
result: result || null,
|
|
43
|
+
...(terminalDeliveryFailure
|
|
44
|
+
? { terminal_delivery_failure: terminalDeliveryFailure } : {}),
|
|
42
45
|
...(terminalHandled ? { terminal_handled: terminalHandled } : {}),
|
|
43
46
|
state_dir: String(meta.state_dir ?? stateDir),
|
|
44
47
|
stderrLog: join(stateDir, "stderr.log"),
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
9
10
|
import * as AsyncRuns from "./async-runs.js";
|
|
10
11
|
import * as Limits from "./limits.js";
|
|
11
12
|
import * as Messages from "./messages.js";
|
|
@@ -229,7 +230,7 @@ function getPiActorsRuntimeStatus() {
|
|
|
229
230
|
catch {
|
|
230
231
|
git_commit = undefined;
|
|
231
232
|
}
|
|
232
|
-
const entrypoint =
|
|
233
|
+
const entrypoint = fileURLToPath(import.meta.url);
|
|
233
234
|
return {
|
|
234
235
|
automatic_recipe_review: Paths.isAutomaticRecipeReviewEnabled(),
|
|
235
236
|
entrypoint,
|
package/dist/lib/tools-local.js
CHANGED
|
@@ -100,6 +100,10 @@ export function createRuntimeToolDefinition(cfg, exec) {
|
|
|
100
100
|
}
|
|
101
101
|
if (isAsyncRecipe)
|
|
102
102
|
paramSchema.run_id = Schema.stringSchema("Optional run id override for this async template recipe invocation.");
|
|
103
|
+
if (isAsyncRecipe) {
|
|
104
|
+
paramSchema.correlation_id = Schema.stringSchema("Optional workflow correlation id preserved in terminal follow-up delivery.");
|
|
105
|
+
paramSchema.transport_context = Schema.looseObjectSchema("Optional originating transport route preserved for detached terminal follow-up.");
|
|
106
|
+
}
|
|
103
107
|
return {
|
|
104
108
|
name: cfg.name,
|
|
105
109
|
label: cfg.name,
|
|
@@ -108,7 +112,7 @@ export function createRuntimeToolDefinition(cfg, exec) {
|
|
|
108
112
|
promptSnippet: isRecipe
|
|
109
113
|
? Prompts.formatRecipeToolPromptSnippet(cfg.recipe?.name ?? String(cfg.template), isAsyncRecipe)
|
|
110
114
|
: Prompts.formatRegisteredToolPromptSnippet(cfg.template),
|
|
111
|
-
async execute(
|
|
115
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
112
116
|
try {
|
|
113
117
|
if (cfg.sourcePath &&
|
|
114
118
|
!RecipesUsage.recordRecipeLaunch(cfg.sourcePath, new Date(), "tool")) {
|
|
@@ -116,7 +120,7 @@ export function createRuntimeToolDefinition(cfg, exec) {
|
|
|
116
120
|
}
|
|
117
121
|
if (isAsyncRecipe) {
|
|
118
122
|
const input = params;
|
|
119
|
-
const { run_id, ...values } = input;
|
|
123
|
+
const { correlation_id, run_id, transport_context, ...values } = input;
|
|
120
124
|
const base = cfg.recipe ? cfg.recipe : { file: String(cfg.template) };
|
|
121
125
|
const runId = typeof run_id === "string" && run_id.trim()
|
|
122
126
|
? run_id.trim()
|
|
@@ -124,6 +128,17 @@ export function createRuntimeToolDefinition(cfg, exec) {
|
|
|
124
128
|
const meta = AsyncRuns.startRun({
|
|
125
129
|
...base,
|
|
126
130
|
launch_source: "tool",
|
|
131
|
+
launch_correlation: {
|
|
132
|
+
...(typeof correlation_id === "string"
|
|
133
|
+
? { correlation_id } : {}),
|
|
134
|
+
tool_call_id: toolCallId,
|
|
135
|
+
},
|
|
136
|
+
...(transport_context &&
|
|
137
|
+
typeof transport_context === "object" &&
|
|
138
|
+
!Array.isArray(transport_context)
|
|
139
|
+
? {
|
|
140
|
+
transport_context: transport_context,
|
|
141
|
+
} : {}),
|
|
127
142
|
ownerId: getRunOwnerId(ctx),
|
|
128
143
|
run_id: runId,
|
|
129
144
|
tool: cfg.name,
|
package/dist/lib/tools-spawn.js
CHANGED
|
@@ -95,6 +95,7 @@ export function createSpawnToolDefinition() {
|
|
|
95
95
|
parameters: Schema.objectSchema({
|
|
96
96
|
artifacts: Schema.looseObjectSchema("Optional named artifact paths for the spawned actor."),
|
|
97
97
|
as: Schema.stringSchema("Optional actor address for the spawned run, e.g. run:<id>."),
|
|
98
|
+
correlation_id: Schema.stringSchema("Optional workflow correlation id preserved in terminal follow-up delivery."),
|
|
98
99
|
file: Schema.stringSchema("Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes."),
|
|
99
100
|
recipe: Schema.stringSchema("Alias for file; template recipe JSON file/name to spawn."),
|
|
100
101
|
template: Schema.unionSchema([
|
|
@@ -103,9 +104,10 @@ export function createSpawnToolDefinition() {
|
|
|
103
104
|
Schema.looseObjectSchema("Inline command-template object with flags such as parallel, repeat, retry, failure, and nested template."),
|
|
104
105
|
]),
|
|
105
106
|
values: Schema.looseObjectSchema("Runtime placeholder values passed to the actor."),
|
|
107
|
+
transport_context: Schema.looseObjectSchema("Optional originating transport route preserved for detached terminal follow-up, e.g. Telegram chat_id and thread_id."),
|
|
106
108
|
verbose: Schema.booleanSchema("Return full JSON instead of compact text."),
|
|
107
109
|
}, []),
|
|
108
|
-
async execute(
|
|
110
|
+
async execute(toolCallId, params, _signal, _onUpdate, ctx) {
|
|
109
111
|
const input = asRecord(params);
|
|
110
112
|
if (input.state_dir !== undefined) {
|
|
111
113
|
throw new Error("spawn.state_dir is not supported; run state is runtime-owned so run:<id> remains addressable and retention-safe.");
|
|
@@ -121,7 +123,14 @@ export function createSpawnToolDefinition() {
|
|
|
121
123
|
meta = AsyncRuns.startRun({
|
|
122
124
|
file: recipe,
|
|
123
125
|
launch_source: "spawn",
|
|
126
|
+
launch_correlation: {
|
|
127
|
+
...(typeof input.correlation_id === "string"
|
|
128
|
+
? { correlation_id: input.correlation_id } : {}),
|
|
129
|
+
tool_call_id: toolCallId,
|
|
130
|
+
},
|
|
124
131
|
ownerId: getRunOwnerId(ctx),
|
|
132
|
+
...(input.transport_context
|
|
133
|
+
? { transport_context: asRecord(input.transport_context) } : {}),
|
|
125
134
|
run_id: runId,
|
|
126
135
|
...(input.template !== undefined
|
|
127
136
|
? {
|
|
@@ -135,7 +135,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
135
135
|
try {
|
|
136
136
|
return readdirSync(sessionDir, { withFileTypes: true })
|
|
137
137
|
.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
138
|
-
.map((entry) => relative(stateDir, join(sessionDir, entry.name)))
|
|
138
|
+
.map((entry) => relative(stateDir, join(sessionDir, entry.name)).replaceAll("\\", "/"))
|
|
139
139
|
.sort();
|
|
140
140
|
} catch {
|
|
141
141
|
return [];
|
|
@@ -167,12 +167,12 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
167
167
|
...(recipeContext ? { recipe_context: recipeContext } : {}),
|
|
168
168
|
command: commandDetail,
|
|
169
169
|
...(materialized.promptFile
|
|
170
|
-
? { prompt_file: relative(stateDir, materialized.promptFile) }
|
|
170
|
+
? { prompt_file: relative(stateDir, materialized.promptFile).replaceAll("\\", "/") }
|
|
171
171
|
: {}),
|
|
172
172
|
...(materialized.promptBytes
|
|
173
173
|
? { prompt_bytes: materialized.promptBytes }
|
|
174
174
|
: {}),
|
|
175
|
-
...(sessionDir ? { session_dir: relative(stateDir, sessionDir) } : {}),
|
|
175
|
+
...(sessionDir ? { session_dir: relative(stateDir, sessionDir).replaceAll("\\", "/") } : {}),
|
|
176
176
|
attempts: [],
|
|
177
177
|
semantic_acceptance:
|
|
178
178
|
options?.evidenceContext?.acceptOutput === "review_evidence" ||
|
|
@@ -209,11 +209,11 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
209
209
|
attempts.push({
|
|
210
210
|
attempt,
|
|
211
211
|
stdout: {
|
|
212
|
-
path: relative(stateDir, stdoutFile),
|
|
212
|
+
path: relative(stateDir, stdoutFile).replaceAll("\\", "/"),
|
|
213
213
|
bytes: existsSync(stdoutFile) ? statSync(stdoutFile).size : 0,
|
|
214
214
|
},
|
|
215
215
|
stderr: {
|
|
216
|
-
path: relative(stateDir, stderrFile),
|
|
216
|
+
path: relative(stateDir, stderrFile).replaceAll("\\", "/"),
|
|
217
217
|
bytes: existsSync(stderrFile) ? statSync(stderrFile).size : 0,
|
|
218
218
|
},
|
|
219
219
|
});
|
|
@@ -248,12 +248,12 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
248
248
|
...(recipeContext ? { recipe_context: recipeContext } : {}),
|
|
249
249
|
command: commandDetail,
|
|
250
250
|
...(materialized.promptFile
|
|
251
|
-
? { prompt_file: relative(stateDir, materialized.promptFile) }
|
|
251
|
+
? { prompt_file: relative(stateDir, materialized.promptFile).replaceAll("\\", "/") }
|
|
252
252
|
: {}),
|
|
253
253
|
...(materialized.promptBytes
|
|
254
254
|
? { prompt_bytes: materialized.promptBytes }
|
|
255
255
|
: {}),
|
|
256
|
-
...(sessionDir ? { session_dir: relative(stateDir, sessionDir) } : {}),
|
|
256
|
+
...(sessionDir ? { session_dir: relative(stateDir, sessionDir).replaceAll("\\", "/") } : {}),
|
|
257
257
|
...(commandSessionFiles(sessionDir).length > 0
|
|
258
258
|
? { session_files: commandSessionFiles(sessionDir) }
|
|
259
259
|
: {}),
|
|
@@ -386,7 +386,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
386
386
|
command: commandDetail,
|
|
387
387
|
...(materialized.promptFile ? { prompt_file: materialized.promptFile } : {}),
|
|
388
388
|
...(materialized.promptBytes ? { prompt_bytes: materialized.promptBytes } : {}),
|
|
389
|
-
...(session.sessionDir ? { session_dir: relative(stateDir, session.sessionDir) } : {}),
|
|
389
|
+
...(session.sessionDir ? { session_dir: relative(stateDir, session.sessionDir).replaceAll("\\", "/") } : {}),
|
|
390
390
|
});
|
|
391
391
|
progressRunning();
|
|
392
392
|
const captureDir = join(stateDir, "captures", commandId);
|
|
@@ -457,7 +457,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
457
457
|
...captureDetails(result),
|
|
458
458
|
...(materialized.promptFile ? { prompt_file: materialized.promptFile } : {}),
|
|
459
459
|
...(materialized.promptBytes ? { prompt_bytes: materialized.promptBytes } : {}),
|
|
460
|
-
...(session.sessionDir ? { session_dir: relative(stateDir, session.sessionDir) } : {}),
|
|
460
|
+
...(session.sessionDir ? { session_dir: relative(stateDir, session.sessionDir).replaceAll("\\", "/") } : {}),
|
|
461
461
|
...(commandSessionFiles(session.sessionDir).length > 0
|
|
462
462
|
? { session_files: commandSessionFiles(session.sessionDir) }
|
|
463
463
|
: {}),
|
|
@@ -20,13 +20,18 @@ import { join } from "node:path";
|
|
|
20
20
|
|
|
21
21
|
function run(command, args) {
|
|
22
22
|
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
23
|
+
if (result.error) throw result.error;
|
|
23
24
|
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
rmSync("dist", { recursive: true, force: true });
|
|
27
28
|
mkdirSync("dist", { recursive: true });
|
|
28
29
|
|
|
29
|
-
run(
|
|
30
|
+
run(process.execPath, [
|
|
31
|
+
join("node_modules", "typescript", "bin", "tsc"),
|
|
32
|
+
"-p",
|
|
33
|
+
"tsconfig.build.json",
|
|
34
|
+
]);
|
|
30
35
|
|
|
31
36
|
mkdirSync(join("dist", "pi-actors"), { recursive: true });
|
|
32
37
|
writeFileSync(
|
|
@@ -28,7 +28,12 @@ function packageRoot() {
|
|
|
28
28
|
|
|
29
29
|
const result = spawnSync(
|
|
30
30
|
process.execPath,
|
|
31
|
-
[
|
|
31
|
+
[
|
|
32
|
+
"--experimental-strip-types",
|
|
33
|
+
"--test",
|
|
34
|
+
"--test-concurrency=1",
|
|
35
|
+
...conformanceSuites,
|
|
36
|
+
],
|
|
32
37
|
{ cwd: packageRoot(), encoding: "utf8", stdio: "pipe" },
|
|
33
38
|
);
|
|
34
39
|
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
statSync,
|
|
17
17
|
writeFileSync,
|
|
18
18
|
} from "node:fs";
|
|
19
|
-
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
19
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
20
20
|
|
|
21
21
|
function usage() {
|
|
22
22
|
console.error(`Usage:
|
|
@@ -101,7 +101,7 @@ function collectRunSummary(rootValue) {
|
|
|
101
101
|
const root = resolve(
|
|
102
102
|
rootValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
|
|
103
103
|
);
|
|
104
|
-
const files = walkFiles(root, 2).filter((file) => file
|
|
104
|
+
const files = walkFiles(root, 2).filter((file) => basename(file) === "run.json");
|
|
105
105
|
const rows = [];
|
|
106
106
|
for (const file of files) {
|
|
107
107
|
const run = readJson(file);
|
|
@@ -118,7 +118,7 @@ function collectRunSummary(rootValue) {
|
|
|
118
118
|
const progress = readJson(join(runDir, "progress.json"));
|
|
119
119
|
const result = readJson(join(runDir, "result.json"));
|
|
120
120
|
rows.push({
|
|
121
|
-
run: run.run_id ?? run.run ?? relative(root, file).split(
|
|
121
|
+
run: run.run_id ?? run.run ?? relative(root, file).split(sep)[0],
|
|
122
122
|
status: getRunStatus(run, progress, result),
|
|
123
123
|
recipe: run.recipe ?? run.recipe_file ?? "",
|
|
124
124
|
updated:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: actors
|
|
3
3
|
description: Required practical guide for non-trivial pi-actors use, including parallel actor launches, subagent fanout, and autonomous coordinator workflows. Read before using or changing spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 0.42.
|
|
5
|
+
version: 0.42.1
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: swarm
|
|
3
3
|
description: Subagent and actor orchestration with scoped locks, fanout, and quorum consensus. Use before launching multiple parallel actors or subagents for independent implementation, artifact generation, review, delegated audit, coordinated execution, or any workflow that needs autonomous coordinator decomposition and integration.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 0.42.
|
|
5
|
+
version: 0.42.1
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Swarm
|
package/docs/actor-inspector.md
CHANGED
|
@@ -29,7 +29,7 @@ Escape Close (or cancel the active options popup)
|
|
|
29
29
|
|
|
30
30
|
Navigation stays bounded by available actions. `↑` on Run does nothing because no higher control exists. `↓` on Tabs enters the timeline only when it contains rows. Empty timelines therefore never receive focus.
|
|
31
31
|
|
|
32
|
-
`K` appears only while Run is focused and the selected owned run reports `running`. It
|
|
32
|
+
`K` appears only while Run is focused and the selected owned run reports `running`. It replaces the Inspector with a dedicated responsive `Confirm Actor Kill` overlay that names the exact `run:<id>`, shows its current status, and states that canonical `control.kill` is destructive and irreversible. Cancel owns initial focus; ←/→/Tab moves between Cancel and Kill actor, Enter activates the focused choice, `Y` confirms directly, and `N`/Escape cancels. Confirmation captures the immutable run generation and routes expected owner/generation through canonical `control.kill`; control compares owner, generation, and running status while serialized against same-directory restart, so terminal, ownership, or replacement-generation races reject without signaling. After the dialog closes, success, cancellation, rejection, and failure remain bounded in the Inspector content area; terminal runs expose no Kill hint and reject a stale keypress.
|
|
33
33
|
|
|
34
34
|
Selection and focus remain separate visual states. Accent-blue text marks the current tab, active filter popup, and applied option. The Run control uses `← … →` markers plus a light neutral background to show both focus and horizontal cycling; menus and timeline rows retain the single `▶` focus marker, while selected tabs retain brackets. Opening a popup keeps its parent filter blue so the relationship remains visible. The footer uses accent color only for key names and arrows; descriptions remain muted.
|
|
35
35
|
|
package/docs/async-runs.md
CHANGED
|
@@ -83,12 +83,16 @@ Use `run_id` on async recipe tools or `as: "run:<id>"` on `spawn` when the calle
|
|
|
83
83
|
|
|
84
84
|
Review commands that require semantic evidence apply marker acceptance before command completion accounting. Rejected code-zero output is reported consistently as a failed command in events, progress, evidence, and outbox delivery; it cannot emit a success-level completion notification. Evidence records are written before command launch and lifecycle cancellation or kill finalizes any running record with its interrupted state, effective exit code, and attempt capture paths. Async attempt stdout/stderr files exist from attempt start, so even small partial streams remain auditable when a command never returns.
|
|
85
85
|
|
|
86
|
+
Terminal follow-ups carry one bounded semantic result in addition to run-file and artifact references. Direct `spawn` and saved async tools persist the originating tool-call correlation; callers may also provide `correlation_id` and a bounded scalar `transport_context`. A transport adapter can preserve an exact route such as `{ "transport": "telegram", "chat_id": 123456, "thread_id": 77 }`, and the same metadata is returned inside terminal follow-up details after detached completion. When a recipe advertises `review.completed`, an explicit matching outbox envelope wins; otherwise a successful accepted review result deterministically synthesizes one from the bounded beginning of `stdout.log`. Failed runs carry their bounded terminal error as `run.failed`.
|
|
87
|
+
|
|
88
|
+
Watcher acceleration and periodic reconciliation share one live in-flight guard. Delivery remains at-least-once across the send/handled-marker crash window, but reentrant watcher/reconciliation races do not create parallel sends. A send failure leaves the run unhandled for retry, notifies the active operator, and persists bounded attempts/error/status evidence in `terminal-delivery-failure.json`; `getRunStatus` exposes the latest record as `terminal_delivery_failure`.
|
|
89
|
+
|
|
86
90
|
## State Files
|
|
87
91
|
|
|
88
92
|
Use ordinary files under the extension temp directory so status tools stay simple and inspectable:
|
|
89
93
|
|
|
90
94
|
- `.pi-actors-run-state.json`: runtime ownership marker binding the run id to the canonical state directory; launch reuse and destructive retention fail closed when it is absent, invalid, mismatched, or reached through a symlink alias. State reuse also fails closed whenever the persisted process identity mismatches a still-live pid, preventing corrupted metadata from admitting overlapping runners.
|
|
91
|
-
- `run.json`: pid, cross-platform `process_identity` proof (start time, command, and canonical cwd where available), optional source metadata (`launch_source`, `tool`, `recipe`, `recipe_file`), command-template config, cwd, coordinator owner id, values, named `artifacts`, mailbox metadata, created time, and state dir. Existing launch cwd aliases are resolved through native `realpath` before proof matching, so symlinked working directories do not degrade control to `unsupported_proof`.
|
|
95
|
+
- `run.json`: pid, cross-platform `process_identity` proof (start time, command, and canonical cwd where available), optional source metadata (`launch_source`, `tool`, `recipe`, `recipe_file`), `launch_correlation`, bounded scalar `transport_context`, command-template config, cwd, coordinator owner id, values, named `artifacts`, mailbox metadata, created time, and state dir. Existing launch cwd aliases are resolved through native `realpath` before proof matching, so symlinked working directories do not degrade control to `unsupported_proof`.
|
|
92
96
|
- `communication.json`: compact actor communication snapshot with self/root/parent, default-room, member, and contact hints for room-aware scripts and agents.
|
|
93
97
|
- `progress.json`: phase, active command count, completed count, failures, updated time, and optional `model_policy` provenance for inherited/explicit model and thinking values.
|
|
94
98
|
- `events.jsonl`: append-only implementation lifecycle log.
|
|
@@ -98,6 +102,8 @@ Use ordinary files under the extension temp directory so status tools stay simpl
|
|
|
98
102
|
- `captures/command-NNN/attempt-NNN/{stdout,stderr}.log`: complete byte-exact command streams, retained even below the bounded in-memory capture limit and separated across retries.
|
|
99
103
|
- `review-evidence.json`: stable command/stage manifest linking prompts, repeated branches, capture attempts, byte counts, exit state, semantic marker acceptance, recipe context, and model/thinking policy; terminal status aligns with the run. Review pipelines inject prior-stage `ACTOR_EVIDENCE_REF` values into downstream prompts, record cited/missing report sources, and fail closed if a normalized report claims `complete` without every required reviewer, verifier, merger, and judge reference.
|
|
100
104
|
- `result.json`: final code, killed flag, output selector, and optional full-output path.
|
|
105
|
+
- `terminal-delivery-failure.json`: latest bounded failed follow-up attempt count, status, error, and timestamp; a later successful retry writes `terminal-handled.json`.
|
|
106
|
+
- `terminal-handled.json`: durable proof that terminal follow-up delivery or an explicit terminal control completed; notification delivery writes it only after the follow-up send returns successfully.
|
|
101
107
|
|
|
102
108
|
Public `spawn` always uses the runtime-owned run root; caller-selected state directories are rejected so `run:<id>` addressing and retention share one boundary. Internal adapters may still supply isolated state directories for deterministic fixtures, but those are not part of the public actor contract. Every launched runner also persists a process identity proof and revalidates it for status, state reuse, message delivery, cancellation, kill, and retirement; dead pids, reused-pid owner mismatches, and unavailable platform proofs remain distinct diagnostics and destructive controls fail closed.
|
|
103
109
|
|
package/docs/tool-registry.md
CHANGED
|
@@ -22,6 +22,8 @@ Because the user recipe directory is sticky agent muscle memory, runtime launche
|
|
|
22
22
|
|
|
23
23
|
`register_tool` is the preferred agent-facing mutation API. It creates, updates, and deletes recipe files in `~/.pi/agent/recipes`; agents do not need to edit the files directly for normal registration. Extension-authored register, update, delete, draft-promotion, and usage-metadata mutations hold a cross-process lock keyed by filesystem-canonical recipe identity across the complete check/read/write/runtime-update window. Existing targets or the nearest existing parent are resolved through `realpath`, so real and symlink aliases serialize while unrelated recipes remain independent; stale locks are reclaimed only after their owner is proven dead. Direct file edits are still valid for operators and advanced agents. Runtime behavior is reactive: file creation, deletion, or edits in the user recipe root trigger validation and tool-set refresh, with invalid recipes surfaced as diagnostics rather than silently ignored. If the recipe root does not exist at session start, an advisory parent watcher detects its creation and switches to the normal root watcher; deletion or rename rearms the parent watcher without polling.
|
|
24
24
|
|
|
25
|
+
Draft-consolidation journals capture root `dev` and `ino` from Node bigint stats and retain them as lossless decimal strings alongside lexical and native-real paths. Some network, virtual, or compatibility filesystems may report weak or zero device/inode identity; those values remain evidence but not a standalone trust claim because recovery also requires unchanged lexical paths, native realpaths, non-reparse directory roots, source/target hashes, and journal CAS. Native Windows regressions use unprivileged NTFS directory junctions to verify canonical mutation/lifecycle locks and fail-closed recovery after draft-root or trusted-root reparse substitution. This evidence supports the current portable process-crash and trusted-state-tree contract; a native handle-relative mutation layer is not justified unless real Windows runs expose a residual substitution window that these independent checks cannot fence.
|
|
26
|
+
|
|
25
27
|
Inspect the loaded pi-actors runtime and discovered registry with:
|
|
26
28
|
|
|
27
29
|
```text
|
package/lib/async-runs.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
statSync,
|
|
17
17
|
writeFileSync,
|
|
18
18
|
} from "node:fs";
|
|
19
|
-
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
19
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
|
|
22
22
|
import type {
|
|
@@ -94,6 +94,29 @@ export interface AsyncRunControlEndpoint {
|
|
|
94
94
|
type: "fifo" | "mailbox" | "named-pipe";
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
export function normalizeRunTransportContext(
|
|
98
|
+
value: unknown,
|
|
99
|
+
): Record<string, string | number | boolean> | undefined {
|
|
100
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
101
|
+
const normalized: Record<string, string | number | boolean> = {};
|
|
102
|
+
for (const [key, item] of Object.entries(
|
|
103
|
+
value as Record<string, unknown>,
|
|
104
|
+
).slice(0, 16)) {
|
|
105
|
+
const safeKey = key.trim().slice(0, 64);
|
|
106
|
+
if (!safeKey) continue;
|
|
107
|
+
if (typeof item === "string") {
|
|
108
|
+
normalized[safeKey] = item.trim().slice(0, 256);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (typeof item === "number" && Number.isFinite(item)) {
|
|
112
|
+
normalized[safeKey] = item;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (typeof item === "boolean") normalized[safeKey] = item;
|
|
116
|
+
}
|
|
117
|
+
return Object.keys(normalized).length ? normalized : undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
97
120
|
export interface AsyncRunStartParams {
|
|
98
121
|
async?: boolean;
|
|
99
122
|
control?: AsyncRunControlEndpoint;
|
|
@@ -102,6 +125,10 @@ export interface AsyncRunStartParams {
|
|
|
102
125
|
lifecycleHooks?: {
|
|
103
126
|
onLockContention?(): void;
|
|
104
127
|
};
|
|
128
|
+
launch_correlation?: {
|
|
129
|
+
correlation_id?: string;
|
|
130
|
+
tool_call_id?: string;
|
|
131
|
+
};
|
|
105
132
|
name?: string;
|
|
106
133
|
ownerId?: string;
|
|
107
134
|
run_id?: string;
|
|
@@ -126,6 +153,7 @@ export interface AsyncRunStartParams {
|
|
|
126
153
|
retry?: number | string;
|
|
127
154
|
failure?: CommandTemplateFailureScope;
|
|
128
155
|
recover?: CommandTemplateValue;
|
|
156
|
+
transport_context?: Record<string, unknown>;
|
|
129
157
|
repeat?: number;
|
|
130
158
|
values?: Record<string, unknown>;
|
|
131
159
|
policy_values?: Record<string, unknown>;
|
|
@@ -140,6 +168,10 @@ export interface AsyncRunMeta {
|
|
|
140
168
|
createdAt: string;
|
|
141
169
|
cwd: string;
|
|
142
170
|
launch_source?: AsyncRunLaunchSource;
|
|
171
|
+
launch_correlation?: {
|
|
172
|
+
correlation_id?: string;
|
|
173
|
+
tool_call_id?: string;
|
|
174
|
+
};
|
|
143
175
|
ownerId?: string;
|
|
144
176
|
pid: number;
|
|
145
177
|
recipe?: string;
|
|
@@ -159,6 +191,7 @@ export interface AsyncRunMeta {
|
|
|
159
191
|
process_identity?: RunProcessIdentity;
|
|
160
192
|
recipe_context_records?: RecipesReferences.TemplateRecipeContextRecord[];
|
|
161
193
|
retire_when?: "children_terminal";
|
|
194
|
+
transport_context?: Record<string, unknown>;
|
|
162
195
|
}
|
|
163
196
|
|
|
164
197
|
const DEFAULT_STATE_ROOT = Paths.getRunStateRoot();
|
|
@@ -242,7 +275,8 @@ function resolveRecipeFile(file: string): string {
|
|
|
242
275
|
function isMutableUsageRecipeFile(file: string): boolean {
|
|
243
276
|
const userRoot = resolve(DEFAULT_RECIPE_ROOT);
|
|
244
277
|
const resolved = resolve(file);
|
|
245
|
-
|
|
278
|
+
const relation = relative(userRoot, resolved);
|
|
279
|
+
return relation !== "" && !relation.startsWith("..") && !isAbsolute(relation);
|
|
246
280
|
}
|
|
247
281
|
|
|
248
282
|
function readRecipeFile(file: string): AsyncRunStartParams {
|
|
@@ -498,6 +532,9 @@ export function startRun(
|
|
|
498
532
|
...(startParams.defaults || {}),
|
|
499
533
|
...values,
|
|
500
534
|
};
|
|
535
|
+
const transportContext = normalizeRunTransportContext(
|
|
536
|
+
startParams.transport_context,
|
|
537
|
+
);
|
|
501
538
|
const artifacts = resolveArtifactPaths(startParams.artifacts, outputValues);
|
|
502
539
|
const meta: AsyncRunMeta = {
|
|
503
540
|
argv: [process.execPath, ...argv],
|
|
@@ -506,6 +543,8 @@ export function startRun(
|
|
|
506
543
|
...(startParams.launch_source
|
|
507
544
|
? { launch_source: startParams.launch_source }
|
|
508
545
|
: {}),
|
|
546
|
+
...(startParams.launch_correlation
|
|
547
|
+
? { launch_correlation: startParams.launch_correlation } : {}),
|
|
509
548
|
...(startParams.ownerId ? { ownerId: startParams.ownerId } : {}),
|
|
510
549
|
pid: 0,
|
|
511
550
|
...(recipe ? { recipe } : {}),
|
|
@@ -530,6 +569,8 @@ export function startRun(
|
|
|
530
569
|
...(startParams.retire_when === "children_terminal"
|
|
531
570
|
? { retire_when: "children_terminal" as const }
|
|
532
571
|
: {}),
|
|
572
|
+
...(transportContext
|
|
573
|
+
? { transport_context: transportContext } : {}),
|
|
533
574
|
};
|
|
534
575
|
writeJsonAtomic(join(stateDir, "run.json"), meta);
|
|
535
576
|
const child = spawn(process.execPath, argv, {
|
|
@@ -576,7 +617,7 @@ export type {
|
|
|
576
617
|
|
|
577
618
|
function resolveRunStateDir(runOrDir: string): string {
|
|
578
619
|
return resolve(
|
|
579
|
-
|
|
620
|
+
/[\\/]/u.test(runOrDir)
|
|
580
621
|
? runOrDir
|
|
581
622
|
: join(DEFAULT_STATE_ROOT, safeRunId(runOrDir)),
|
|
582
623
|
);
|
|
@@ -906,11 +947,11 @@ function finalizeInterruptedReviewEvidence(
|
|
|
906
947
|
return {
|
|
907
948
|
attempt: index + 1,
|
|
908
949
|
stdout: {
|
|
909
|
-
path: relative(stateDir, stdoutFile),
|
|
950
|
+
path: relative(stateDir, stdoutFile).replaceAll("\\", "/"),
|
|
910
951
|
bytes: existsSync(stdoutFile) ? statSync(stdoutFile).size : 0,
|
|
911
952
|
},
|
|
912
953
|
stderr: {
|
|
913
|
-
path: relative(stateDir, stderrFile),
|
|
954
|
+
path: relative(stateDir, stderrFile).replaceAll("\\", "/"),
|
|
914
955
|
bytes: existsSync(stderrFile) ? statSync(stderrFile).size : 0,
|
|
915
956
|
},
|
|
916
957
|
};
|
|
@@ -1030,6 +1071,25 @@ export function markRunTerminalNotificationHandled(
|
|
|
1030
1071
|
});
|
|
1031
1072
|
}
|
|
1032
1073
|
|
|
1074
|
+
export function recordRunTerminalDeliveryFailure(
|
|
1075
|
+
stateDir: string,
|
|
1076
|
+
status: string,
|
|
1077
|
+
error: unknown,
|
|
1078
|
+
): void {
|
|
1079
|
+
const path = join(stateDir, "terminal-delivery-failure.json");
|
|
1080
|
+
const previous = readJson(path);
|
|
1081
|
+
const message = (error instanceof Error ? error.message : String(error))
|
|
1082
|
+
.replaceAll(/\s+/g, " ")
|
|
1083
|
+
.trim()
|
|
1084
|
+
.slice(0, 500);
|
|
1085
|
+
writeJsonAtomic(path, {
|
|
1086
|
+
attempts: Math.max(0, Number(previous?.attempts ?? 0)) + 1,
|
|
1087
|
+
error: message || "unknown delivery failure",
|
|
1088
|
+
status,
|
|
1089
|
+
ts: new Date().toISOString(),
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1033
1093
|
export function cancelRun(
|
|
1034
1094
|
runOrDir: string,
|
|
1035
1095
|
expected: RunControlExpectation = {},
|
package/lib/command-templates.ts
CHANGED
|
@@ -556,7 +556,7 @@ export function splitCommandTemplate(input: string): string[] {
|
|
|
556
556
|
let active = false;
|
|
557
557
|
for (const char of input) {
|
|
558
558
|
if (escaped) {
|
|
559
|
-
current += char
|
|
559
|
+
current += /[\s'"\\]/u.test(char) ? char : `\\${char}`;
|
|
560
560
|
escaped = false;
|
|
561
561
|
active = true;
|
|
562
562
|
continue;
|