@llblab/pi-actors 0.44.0 → 0.45.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 +2 -0
- package/CHANGELOG.md +13 -0
- package/README.md +5 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +10 -106
- package/dist/lib/async-runs.d.ts +2 -0
- package/dist/lib/async-runs.js +33 -4
- package/dist/lib/config.js +1 -0
- package/dist/lib/extension-runtime.d.ts +21 -0
- package/dist/lib/extension-runtime.js +116 -0
- package/dist/lib/inspector.js +8 -2
- package/dist/lib/recipes-discovery.js +20 -3
- package/dist/lib/recipes-references.d.ts +10 -0
- package/dist/lib/recipes-references.js +168 -5
- package/dist/lib/schema.d.ts +3 -0
- package/dist/lib/schema.js +34 -1
- package/dist/lib/tools-inspect.js +10 -0
- package/dist/lib/tools-local.js +27 -10
- package/dist/lib/tools-spawn.js +2 -2
- package/dist/scripts/async-runner.mjs +0 -16
- package/dist/scripts/executable-block-style.d.mts +1 -0
- package/dist/scripts/executable-block-style.mjs +73 -0
- package/dist/scripts/locker.mjs +0 -21
- package/dist/scripts/recipe-utils.mjs +0 -1
- package/dist/scripts/release-gates.mjs +14 -11
- package/dist/scripts/validate-recipe.mjs +0 -1
- package/dist/skills/actors/SKILL.md +2 -2
- package/docs/command-templates.md +2 -2
- package/docs/recipe-library.md +2 -0
- package/docs/template-recipes.md +10 -4
- package/docs/tool-registry.md +1 -1
- package/index.ts +18 -107
- package/lib/async-runs.ts +43 -9
- package/lib/config.ts +4 -0
- package/lib/extension-runtime.ts +139 -0
- package/lib/inspector.ts +16 -2
- package/lib/recipes-discovery.ts +23 -3
- package/lib/recipes-references.ts +227 -7
- package/lib/schema.ts +53 -1
- package/lib/tools-inspect.ts +11 -0
- package/lib/tools-local.ts +49 -19
- package/lib/tools-spawn.ts +2 -2
- package/package.json +2 -2
- package/scripts/async-runner.mjs +0 -16
- package/scripts/executable-block-style.d.mts +1 -0
- package/scripts/executable-block-style.mjs +73 -0
- package/scripts/locker.mjs +0 -21
- package/scripts/recipe-utils.mjs +0 -1
- package/scripts/release-gates.mjs +14 -11
- package/scripts/validate-recipe.mjs +0 -1
- package/skills/actors/SKILL.md +2 -2
package/dist/scripts/locker.mjs
CHANGED
|
@@ -65,7 +65,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
65
65
|
const controlPath = join(stateDir, "control.fifo");
|
|
66
66
|
let runInstanceId;
|
|
67
67
|
mkdirSync(stateDir, { recursive: true });
|
|
68
|
-
|
|
69
68
|
function readJson(path, fallback) {
|
|
70
69
|
if (!existsSync(path)) return fallback;
|
|
71
70
|
try {
|
|
@@ -74,11 +73,9 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
74
73
|
return fallback;
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
|
-
|
|
78
76
|
function writeJson(path, value) {
|
|
79
77
|
writeJsonAtomic(path, value);
|
|
80
78
|
}
|
|
81
|
-
|
|
82
79
|
function getControlEndpoint() {
|
|
83
80
|
if (process.platform !== "win32") return { path: controlPath, type: "fifo" };
|
|
84
81
|
const hash = createHash("sha256")
|
|
@@ -87,7 +84,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
87
84
|
.slice(0, 20);
|
|
88
85
|
return { path: `\\\\.\\pipe\\pi-actors-locker-${hash}`, type: "named-pipe" };
|
|
89
86
|
}
|
|
90
|
-
|
|
91
87
|
function writeControlEndpoint(endpoint) {
|
|
92
88
|
if (!runInstanceId) throw new Error("Run generation unavailable for Control endpoint");
|
|
93
89
|
writeJson(join(stateDir, "control-endpoint.json"), {
|
|
@@ -96,7 +92,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
96
92
|
run_instance_id: runInstanceId,
|
|
97
93
|
});
|
|
98
94
|
}
|
|
99
|
-
|
|
100
95
|
function journal(event, data = {}) {
|
|
101
96
|
const release = acquireFileMutationLock(journalPath);
|
|
102
97
|
try {
|
|
@@ -111,11 +106,9 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
111
106
|
writeTextAtomic(journalPath, content);
|
|
112
107
|
} finally { release(); }
|
|
113
108
|
}
|
|
114
|
-
|
|
115
109
|
function encodeJournal(records) {
|
|
116
110
|
return records.length ? `${records.map((record) => JSON.stringify(record)).join("\n")}\n` : "";
|
|
117
111
|
}
|
|
118
|
-
|
|
119
112
|
function emitTrace(kind, summary, data = {}, level = "info") {
|
|
120
113
|
appendRunTraceEvent(stateDir, {
|
|
121
114
|
attention: "followup",
|
|
@@ -125,11 +118,9 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
125
118
|
summary,
|
|
126
119
|
});
|
|
127
120
|
}
|
|
128
|
-
|
|
129
121
|
function now() {
|
|
130
122
|
return Date.now();
|
|
131
123
|
}
|
|
132
|
-
|
|
133
124
|
function cleanExpiredLocks(locks) {
|
|
134
125
|
const current = now();
|
|
135
126
|
const kept = {};
|
|
@@ -139,7 +130,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
139
130
|
}
|
|
140
131
|
return kept;
|
|
141
132
|
}
|
|
142
|
-
|
|
143
133
|
function normalizeControl(line) {
|
|
144
134
|
const trimmed = line.trim();
|
|
145
135
|
if (!trimmed) return undefined;
|
|
@@ -160,8 +150,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
160
150
|
);
|
|
161
151
|
}
|
|
162
152
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
153
|
function tailJournal(count) {
|
|
166
154
|
if (!existsSync(journalPath)) return [];
|
|
167
155
|
return readFileSync(journalPath, "utf8")
|
|
@@ -177,7 +165,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
177
165
|
}
|
|
178
166
|
});
|
|
179
167
|
}
|
|
180
|
-
|
|
181
168
|
function printSnapshot() {
|
|
182
169
|
const locks = cleanExpiredLocks(readJson(locksPath, {}));
|
|
183
170
|
writeJson(locksPath, locks);
|
|
@@ -195,7 +182,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
195
182
|
),
|
|
196
183
|
);
|
|
197
184
|
}
|
|
198
|
-
|
|
199
185
|
function nextTask(queue, locks) {
|
|
200
186
|
const items = Array.isArray(queue.items) ? queue.items : [];
|
|
201
187
|
const index = items.findIndex((item) => {
|
|
@@ -205,7 +191,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
205
191
|
if (index < 0) return undefined;
|
|
206
192
|
return items.splice(index, 1)[0];
|
|
207
193
|
}
|
|
208
|
-
|
|
209
194
|
function handle(control) {
|
|
210
195
|
const action = control.action;
|
|
211
196
|
const body =
|
|
@@ -333,18 +318,15 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
333
318
|
journal("lock.unknown", { action, input: body });
|
|
334
319
|
emitTrace("lock.unknown", `Unknown Control ${action}`, { action, input: body }, "warning");
|
|
335
320
|
}
|
|
336
|
-
|
|
337
321
|
if (mode === "snapshot") {
|
|
338
322
|
printSnapshot();
|
|
339
323
|
process.exit(0);
|
|
340
324
|
}
|
|
341
|
-
|
|
342
325
|
const startupRun = readJson(join(stateDir, "run.json"), undefined);
|
|
343
326
|
if (!startupRun || typeof startupRun.run_instance_id !== "string") {
|
|
344
327
|
throw new Error("Run generation unavailable for Control service");
|
|
345
328
|
}
|
|
346
329
|
runInstanceId = startupRun.run_instance_id;
|
|
347
|
-
|
|
348
330
|
function handleLine(line) {
|
|
349
331
|
const control = normalizeControl(line);
|
|
350
332
|
if (!control) return false;
|
|
@@ -376,7 +358,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
376
358
|
return false;
|
|
377
359
|
}
|
|
378
360
|
}
|
|
379
|
-
|
|
380
361
|
async function serveFifo(endpoint) {
|
|
381
362
|
if (!existsSync(endpoint.path)) {
|
|
382
363
|
const result = spawnSync("mkfifo", [endpoint.path]);
|
|
@@ -403,7 +384,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
403
384
|
fs.closeSync(fd);
|
|
404
385
|
}
|
|
405
386
|
}
|
|
406
|
-
|
|
407
387
|
async function serveNamedPipe(endpoint) {
|
|
408
388
|
let resolveStopped;
|
|
409
389
|
const stopped = new Promise((resolve) => {
|
|
@@ -428,7 +408,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
428
408
|
});
|
|
429
409
|
await stopped;
|
|
430
410
|
}
|
|
431
|
-
|
|
432
411
|
const endpoint = getControlEndpoint();
|
|
433
412
|
writeJson(queuePath, readJson(queuePath, { items: [] }));
|
|
434
413
|
writeJson(locksPath, cleanExpiredLocks(readJson(locksPath, {})));
|
|
@@ -7,6 +7,8 @@ import { dirname, extname, join, normalize, relative, resolve } from "node:path"
|
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
|
|
10
|
+
import { executableBlockBlankLines } from "./executable-block-style.mjs";
|
|
11
|
+
|
|
10
12
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
13
|
const temp = mkdtempSync(join(tmpdir(), "pi-actors-release-index-"));
|
|
12
14
|
const indexPath = join(temp, "index");
|
|
@@ -71,7 +73,6 @@ try {
|
|
|
71
73
|
.filter(Boolean);
|
|
72
74
|
const fileSet = new Set(files);
|
|
73
75
|
console.log(`[release] exact temporary index: ${files.length} files`);
|
|
74
|
-
|
|
75
76
|
const secretPatterns = [
|
|
76
77
|
[/-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----[\s\S]{80,}?-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/u, "private-key block"],
|
|
77
78
|
[/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/u, "AWS access key"],
|
|
@@ -92,7 +93,17 @@ try {
|
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
console.log("[release] bounded secret and public-path hygiene checked");
|
|
95
|
-
|
|
96
|
+
const executableSources = files.filter((path) =>
|
|
97
|
+
/\.(?:[cm]?js|ts)$/u.test(path) && !path.startsWith("dist/"),
|
|
98
|
+
);
|
|
99
|
+
for (const path of executableSources) {
|
|
100
|
+
const blankLines = executableBlockBlankLines(stagedText(path) ?? "");
|
|
101
|
+
check(
|
|
102
|
+
blankLines.length === 0,
|
|
103
|
+
`blank lines inside executable blocks: ${path}:${blankLines.join(",")}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
console.log("[release] executable blocks contain no blank lines");
|
|
96
107
|
const removedPaths = [
|
|
97
108
|
"lib/messages.ts",
|
|
98
109
|
"lib/rooms.ts",
|
|
@@ -113,7 +124,6 @@ try {
|
|
|
113
124
|
"lib/run-inspector-overlay.ts",
|
|
114
125
|
];
|
|
115
126
|
for (const path of removedPaths) check(!fileSet.has(path), `removed surface returned: ${path}`);
|
|
116
|
-
|
|
117
127
|
const currentSurface = /^(?:lib\/|scripts\/|recipes\/|fixtures\/|skills\/|docs\/|README\.md|AGENTS\.md)/u;
|
|
118
128
|
const forbiddenPatterns = [
|
|
119
129
|
[/room:</u, "room address"],
|
|
@@ -144,7 +154,6 @@ try {
|
|
|
144
154
|
}
|
|
145
155
|
}
|
|
146
156
|
console.log("[release] removed-surface and legacy-fallback allowlists checked");
|
|
147
|
-
|
|
148
157
|
const directTraceWrite = /(?:appendFileSync|writeFileSync|writeText(?:Atomic)?)\s*\(\s*[A-Za-z0-9_.]*?(?:trace|event)(?:Path|File)/iu;
|
|
149
158
|
for (const path of files.filter((candidate) =>
|
|
150
159
|
(candidate.startsWith("lib/") && candidate.endsWith(".ts")) ||
|
|
@@ -172,7 +181,6 @@ try {
|
|
|
172
181
|
);
|
|
173
182
|
}
|
|
174
183
|
console.log("[release] canonical Trace and Control writer residue checked");
|
|
175
|
-
|
|
176
184
|
// owner | class | lock | record bound | byte bound | recovery
|
|
177
185
|
const appendOnlyOwners = new Set([
|
|
178
186
|
"lib/runs-trace.ts", // canonical Run evidence | token lock | 2048 | 4 MiB | compact valid suffix
|
|
@@ -189,7 +197,6 @@ try {
|
|
|
189
197
|
check(appendOnlyOwners.has(path), `unregistered shipped append-only writer: ${path}`);
|
|
190
198
|
for (const path of appendOnlyOwners) check(fileSet.has(path), `stale append-only owner inventory: ${path}`);
|
|
191
199
|
console.log(`[release] append-only owner inventory checked: ${appendOnlyOwners.size} source owners`);
|
|
192
|
-
|
|
193
200
|
check(
|
|
194
201
|
(stagedText("scripts/validate-recipe.mjs") ?? "").includes(
|
|
195
202
|
"qaReport.diagnostics.length === 0 && qaReport.warnings.length === 0",
|
|
@@ -197,15 +204,13 @@ try {
|
|
|
197
204
|
"Recipe QA warnings are not release-blocking",
|
|
198
205
|
);
|
|
199
206
|
console.log("[release] zero-warning Recipe QA gate checked");
|
|
200
|
-
|
|
201
|
-
const maximumShippedLines = 29_598;
|
|
207
|
+
const maximumShippedLines = 32_000;
|
|
202
208
|
const shippedPath = /^(?:lib\/|scripts\/|recipes\/|docs\/|skills\/)/u;
|
|
203
209
|
const shippedLines = files
|
|
204
210
|
.filter((path) => shippedPath.test(path))
|
|
205
211
|
.reduce((total, path) => total + (((stagedText(path) ?? "").match(/\n/gu) ?? []).length + 1), 0);
|
|
206
212
|
check(shippedLines <= maximumShippedLines, `shipped lines ${shippedLines} exceed release maximum ${maximumShippedLines}`);
|
|
207
213
|
console.log(`[release] shipped lines ${shippedLines} <= release maximum ${maximumShippedLines}`);
|
|
208
|
-
|
|
209
214
|
const sources = files.filter((path) => path === "index.ts" || (path.startsWith("lib/") && path.endsWith(".ts")));
|
|
210
215
|
const sourceSet = new Set(sources);
|
|
211
216
|
const graph = new Map();
|
|
@@ -240,7 +245,6 @@ try {
|
|
|
240
245
|
}
|
|
241
246
|
for (const path of sources) visit(path);
|
|
242
247
|
console.log(`[release] strict Domain DAG: ${sources.length} sources, acyclic`);
|
|
243
|
-
|
|
244
248
|
for (const path of ["README.md", "AGENTS.md", "BACKLOG.md", "CHANGELOG.md", "docs/README.md"]) {
|
|
245
249
|
check(fileSet.has(path), `missing ABCd root/context file: ${path}`);
|
|
246
250
|
}
|
|
@@ -263,7 +267,6 @@ try {
|
|
|
263
267
|
check(docsIndex.includes(relative("docs", path).replaceAll("\\", "/")), `docs/README.md omits ${path}`);
|
|
264
268
|
}
|
|
265
269
|
console.log("[release] ABCd context roots, routing, and Markdown links checked");
|
|
266
|
-
|
|
267
270
|
if (failures.length > 0) {
|
|
268
271
|
for (const failure of failures) console.error(`[FAIL] ${failure}`);
|
|
269
272
|
process.exitCode = 1;
|
|
@@ -187,7 +187,6 @@ export function validateRecipes(argv) {
|
|
|
187
187
|
if (!targetArg || argv.includes("--help") || argv.includes("-h")) {
|
|
188
188
|
return { help: true, ok: Boolean(targetArg), usage: validateRecipeUsage() };
|
|
189
189
|
}
|
|
190
|
-
|
|
191
190
|
const files = recipeFiles(expandPath(targetArg), all);
|
|
192
191
|
const results = files.map((file) => validateFile(file, qa));
|
|
193
192
|
const failed = results.filter((result) => !result.ok).length;
|
|
@@ -25,11 +25,11 @@ A Run target exposes exactly three inspect views: `recipe`, `trace`, and `contro
|
|
|
25
25
|
|
|
26
26
|
## Recipe
|
|
27
27
|
|
|
28
|
-
A Recipe defines execution. It may declare args,
|
|
28
|
+
A Recipe defines execution. It may declare named typed args, inline fallbacks, configuration `defaults`, composition `values`, imports, artifacts, command-template flags, and `control: ["action"]` when a long-lived service actually consumes actor-local inputs.
|
|
29
29
|
|
|
30
30
|
Do not declare Control for ordinary one-shot work. Runtime lifecycle actions such as `kill` stay runtime-owned and must not appear in Recipe Control declarations. Imported Recipes act as local definitions inside one Run; they do not create nested Runs unless execution explicitly spawns them.
|
|
31
31
|
|
|
32
|
-
Prefer maintained packaged Recipes over ad hoc wrappers. Keep model, thinking, mission, concurrency, quorum, and timeout choices caller-owned unless a Recipe documents a stable policy.
|
|
32
|
+
Prefer maintained packaged Recipes over ad hoc wrappers. Use `std:<recipe>` for exact packaged lookup and `skill:<skill>/<recipe-path>` for a component bundled with a Pi-active Skill. File-backed Recipes own `{recipe_dir}` and Skill Recipes own `{skill_dir}`; callers never pass or override these origins. Skill Recipes are components, not automatic tools. Keep model, thinking, mission, concurrency, quorum, and timeout choices caller-owned unless a Recipe documents a stable policy.
|
|
33
33
|
|
|
34
34
|
## Trace
|
|
35
35
|
|
|
@@ -53,7 +53,7 @@ Common object fields:
|
|
|
53
53
|
- `concurrency`: Optional positive integer cap for a `parallel: true` node. Omit it to launch all children at once.
|
|
54
54
|
- `min_successful`: Optional non-negative integer evidence threshold for a `parallel: true` node. Usable branches are successful branches with non-empty stdout; joins include a `parallel_status` header when this is set.
|
|
55
55
|
- `when`: Optional node guard. A false guard skips the node; strings may be `flag`, `!flag`, or `{flag?yes:no}` style expressions.
|
|
56
|
-
- `args`: Optional placeholder declarations. Untyped names remain valid; compact typed forms such as `file:path`, `request_timeout:int`, `speed:number`, `dry_run:bool`, `prompts:array`, and `mode:enum(check,fix)` are valid when the host supports typed tool schemas.
|
|
56
|
+
- `args`: Optional placeholder declarations. Untyped names remain valid; compact typed forms such as `file:path`, `request_timeout:int`, `speed:number`, `dry_run:bool`, `prompts:array`, and `mode:enum(check,fix)` are valid when the host supports typed tool schemas. `name:type=value` is an optional argument with an inline fallback. Hosts reject duplicate names and conflicting declared/placeholder types rather than selecting one silently.
|
|
57
57
|
- `defaults`: Placeholder default values by name.
|
|
58
58
|
- `timeout`: Optional execution timeout in milliseconds. Omit it, or set `0`, to leave the command unbounded. Set an explicit positive timeout when a tool must fail closed instead of waiting indefinitely. Numeric control fields may be literal numbers or placeholders such as `"{timeout_ms}"`.
|
|
59
59
|
- `delay`: Optional wait in milliseconds before starting this node. Default is no delay. It may be a literal number or placeholder.
|
|
@@ -104,7 +104,7 @@ With runtime values `{ "text": "hello" }`, argv is:
|
|
|
104
104
|
["--text", "hello", "--lang", "ru", "--rate", "+30%"]
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
Use `defaults` for visible configuration
|
|
107
|
+
Use `defaults` for visible fallback configuration, `values` for composition binding, and inline defaults for compact local literals. Named resolution uses caller values before composition values, explicit defaults, and inline defaults; the final selected value is type/enum validated. Prefer flag-style examples such as `/path/to/tool --file {file} --lang {lang=ru}` for readability, but positional forms such as `/path/to/tool {file} {lang=ru}` are valid when the invoked script defines that CLI contract.
|
|
108
108
|
|
|
109
109
|
Use `{env??dev}` for fallback values and `{all?--all:}` to map boolean args to optional text.
|
|
110
110
|
|
package/docs/recipe-library.md
CHANGED
|
@@ -45,6 +45,8 @@ Subagent components provide reusable command-template cells for normalization, p
|
|
|
45
45
|
|
|
46
46
|
Imports compose these definitions inside one parent Run. They are not independently addressable peers. Parent template flags control sequencing, parallelism, retries, failure scope, recovery, and repeated execution.
|
|
47
47
|
|
|
48
|
+
Packaged components can be selected exactly as `std:<recipe-name>`. Recipes bundled under a Pi-active Skill are selected as `skill:<skill-name>/<recipe-path>` and receive runtime-owned `{skill_dir}` plus `{recipe_dir}`. Active Skill components are namespaced library entries, never automatic tools; expose one intentionally through a user Recipe wrapper when a direct tool is desired.
|
|
49
|
+
|
|
48
50
|
## Utility Recipes
|
|
49
51
|
|
|
50
52
|
Utilities wrap deterministic local capabilities such as:
|
package/docs/template-recipes.md
CHANGED
|
@@ -44,7 +44,7 @@ Fences marked `template`, `command-template`, `json`, or `recipe` can define exe
|
|
|
44
44
|
Common Recipe fields:
|
|
45
45
|
|
|
46
46
|
- `name`, `description`, `disabled`;
|
|
47
|
-
- `args`, typed arg declarations, and `
|
|
47
|
+
- `args`, typed arg declarations, inline defaults, `defaults`, and composition `values`;
|
|
48
48
|
- `imports` with optional binding defaults/values;
|
|
49
49
|
- `template`;
|
|
50
50
|
- `async`;
|
|
@@ -71,7 +71,9 @@ Common Recipe fields:
|
|
|
71
71
|
}
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
Imports are local definitions. Named nodes call imported templates inside the same execution graph and Run.
|
|
74
|
+
Imports are local definitions. Named nodes call imported templates inside the same execution graph and Run. Effective values follow `caller > node/import/Recipe values > defaults > inline arg default > missing-value error`, then the selected value is checked against its declared type or enum.
|
|
75
|
+
|
|
76
|
+
Bare references preserve user → adjacent → packaged resolution. `std:<name>` selects a packaged Recipe exactly. `skill:<skill-name>/<recipe-path>` selects a component under the `recipes/` tree of a Skill currently active through Pi resource discovery. Skill Recipes never become tools merely by existing, and duplicate active Skill names fail as ambiguous rather than shadowing silently.
|
|
75
77
|
|
|
76
78
|
Direct delegation can use another Recipe as the entire template. The delegated Recipe remains the source of truth while the wrapper may narrow args/defaults or override selected lifecycle metadata.
|
|
77
79
|
|
|
@@ -102,9 +104,13 @@ Actions must be lowercase ASCII, unique, non-reserved, and at most 64 characters
|
|
|
102
104
|
|
|
103
105
|
Artifact paths resolve under containment policy and appear in Run inspection. Recipes should write declared artifacts deterministically and fail when the requested write policy cannot be honored.
|
|
104
106
|
|
|
107
|
+
## File Origins
|
|
108
|
+
|
|
109
|
+
Every file-backed Recipe receives immutable `{recipe_dir}`. A Recipe under an active Skill also receives `{skill_dir}`, resolved to the directory containing that Skill's `SKILL.md`; using `{skill_dir}` elsewhere fails clearly. These runtime values cannot be declared in `args`, `defaults`, or `values`, and caller input cannot override them. They expand in templates, recursive defaults/values, imports, and artifacts while existing `./` executable behavior remains relative to invocation `cwd`.
|
|
110
|
+
|
|
105
111
|
## Context and Provenance
|
|
106
112
|
|
|
107
|
-
File-backed Runs capture Recipe context records for the entry and imports. The captured bundle explains composition identity and remains generation-local evidence. It does not override the authored task prompt.
|
|
113
|
+
File-backed Runs capture Recipe context records for the entry and imports, including qualified `std:` or `skill:` identity when applicable. The captured bundle explains composition identity and remains generation-local evidence. Runtime origin paths remain in local Run provenance but are omitted from model-facing launch values. It does not override the authored task prompt.
|
|
108
114
|
|
|
109
115
|
Recipes that need a minimal child prompt may opt out of injected Recipe context through the documented `actor_context` launch option.
|
|
110
116
|
|
|
@@ -125,7 +131,7 @@ Resolution fails before launch when required current policy is unavailable. The
|
|
|
125
131
|
|
|
126
132
|
## Resolution and Shadowing
|
|
127
133
|
|
|
128
|
-
User Recipes under `~/.pi/agent/recipes` take priority over packaged Recipes. An invalid active file blocks fallback and reports both paths. Disabled Recipes cannot launch. Registry watchers converge after atomic changes without executing partial definitions.
|
|
134
|
+
User Recipes under `~/.pi/agent/recipes` take priority over adjacent and packaged Recipes for compatible bare lookup. Exact `std:` and `skill:` references bypass that ambiguous space. The active Skill namespace converges from Pi's loaded Skill metadata on startup/reload; pi-actors does not scan ambient Skill roots independently. An invalid active file blocks fallback and reports both paths. Disabled Recipes cannot launch. Registry watchers converge after atomic changes without executing partial definitions.
|
|
129
135
|
|
|
130
136
|
## Validation
|
|
131
137
|
|
package/docs/tool-registry.md
CHANGED
|
@@ -61,7 +61,7 @@ Usage and lineage live in locked metadata ledgers rather than authored Recipe fi
|
|
|
61
61
|
|
|
62
62
|
## Wrapping Existing Recipes
|
|
63
63
|
|
|
64
|
-
Prefer a small user-root wrapper that imports a maintained
|
|
64
|
+
Prefer a small user-root wrapper that imports a maintained Recipe by exact `std:<recipe>` or `skill:<skill>/<recipe-path>` identity and delegates by alias. Skill Recipes remain components and are never exposed merely because their Skill is active. Do not duplicate executable templates, defaults, Control declarations, artifacts, or runtime-owned `{recipe_dir}`/`{skill_dir}`. Install only specific capabilities; internal automatic-review Recipes must not become user-callable tools.
|
|
65
65
|
|
|
66
66
|
## Safety
|
|
67
67
|
|
package/index.ts
CHANGED
|
@@ -1,118 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pi-actors — actor runtime and persistent local tool registry for pi.
|
|
3
3
|
* Zones: composition root, pi agent, actor runtime
|
|
4
|
-
*
|
|
5
|
-
* Wraps command templates as callable pi tools, stores durable user tools as recipe files, and exposes actor orchestration across reloads and sessions.
|
|
4
|
+
* Owns extension composition and Pi event registration, not domain behavior.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
import * as
|
|
9
|
-
import * as CommandTemplates from "./lib/command-templates.ts";
|
|
7
|
+
import * as ExtensionRuntime from "./lib/extension-runtime.ts";
|
|
10
8
|
import * as InspectorCommand from "./lib/inspector-command.ts";
|
|
11
|
-
import * as Paths from "./lib/paths.ts";
|
|
12
9
|
import * as Pi from "./lib/pi.ts";
|
|
13
|
-
import * as Prompts from "./lib/prompts.ts";
|
|
14
|
-
import * as RunUiRuntime from "./lib/run-ui-runtime.ts";
|
|
15
|
-
import * as Runtime from "./lib/runtime.ts";
|
|
16
|
-
import * as Temp from "./lib/temp.ts";
|
|
17
|
-
import * as Tools from "./lib/tools.ts";
|
|
18
|
-
import * as ToolsResponse from "./lib/tools-response.ts";
|
|
19
10
|
|
|
20
11
|
export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
getActiveContext: () => activeRunContext,
|
|
25
|
-
getRunOwnerId,
|
|
26
|
-
getThinkingLevel: () => pi.getThinkingLevel(),
|
|
27
|
-
});
|
|
28
|
-
const runUiRuntime = RunUiRuntime.createRunUiRuntime({
|
|
29
|
-
getActiveContext: () => activeRunContext,
|
|
30
|
-
getRunOwnerId,
|
|
31
|
-
onRunEvent: automaticReview.schedule,
|
|
32
|
-
pi,
|
|
33
|
-
});
|
|
34
|
-
const actorToolDefinitions = new Map<string, Tools.ActorToolDefinition>();
|
|
35
|
-
const withCurrentThinkingContext = <T extends Tools.ActorToolDefinition>(
|
|
36
|
-
definition: T,
|
|
37
|
-
): T => {
|
|
38
|
-
if (typeof definition.execute !== "function") return definition;
|
|
39
|
-
const execute = definition.execute as (...args: unknown[]) => unknown;
|
|
40
|
-
return {
|
|
41
|
-
...definition,
|
|
42
|
-
execute: async (...args: unknown[]) => {
|
|
43
|
-
const nextArgs = [...args];
|
|
44
|
-
const ctx = nextArgs[4];
|
|
45
|
-
if (ctx && typeof ctx === "object") {
|
|
46
|
-
nextArgs[4] = {
|
|
47
|
-
...(ctx as Record<string, unknown>),
|
|
48
|
-
getThinkingLevel: () => pi.getThinkingLevel(),
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
return ToolsResponse.spaceToolResult(await execute(...nextArgs));
|
|
53
|
-
} catch (error) {
|
|
54
|
-
throw ToolsResponse.spaceToolError(error);
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
} as T;
|
|
58
|
-
};
|
|
59
|
-
const runtime = Runtime.createAutoToolsRuntime({
|
|
60
|
-
configPath: Paths.EXTENSION_RUNTIME_PATHS.configPath,
|
|
61
|
-
exec: CommandTemplates.execCommandTemplate,
|
|
62
|
-
getActiveTools: () => pi.getActiveTools(),
|
|
63
|
-
registerTool: (definition) => {
|
|
64
|
-
const wrapped = withCurrentThinkingContext(definition);
|
|
65
|
-
actorToolDefinitions.set(wrapped.name, wrapped);
|
|
66
|
-
pi.registerTool(wrapped);
|
|
67
|
-
},
|
|
68
|
-
reservedToolNames: Tools.RESERVED_TOOL_NAMES,
|
|
69
|
-
setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
|
|
70
|
-
});
|
|
71
|
-
const recipeReload = Runtime.createRecipeToolReloadWatcher(runtime);
|
|
72
|
-
pi.on("resources_discover", async () => {
|
|
73
|
-
const skillPaths = Paths.getExistingExtensionSkillPaths(import.meta.url);
|
|
74
|
-
if (skillPaths.length === 0) return;
|
|
75
|
-
return { skillPaths };
|
|
76
|
-
});
|
|
77
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
78
|
-
// Clear the pre-overlay widget after hot reloads from older pi-actors builds.
|
|
79
|
-
ctx.ui.setWidget("zz-pi-actors-comms", undefined);
|
|
80
|
-
activeRunContext = ctx;
|
|
81
|
-
runUiRuntime.close();
|
|
82
|
-
automaticReview.close();
|
|
83
|
-
recipeReload.close();
|
|
84
|
-
await Temp.prepareExtensionTempDir(Paths.EXTENSION_RUNTIME_PATHS.tempDir);
|
|
85
|
-
if (activeRunContext !== ctx) return;
|
|
86
|
-
automaticReview.start(ctx);
|
|
87
|
-
runtime.loadTools(ctx);
|
|
88
|
-
runUiRuntime.start(ctx);
|
|
89
|
-
recipeReload.watch(ctx);
|
|
90
|
-
});
|
|
91
|
-
pi.on("agent_end", async (_event, ctx) => {
|
|
92
|
-
if (activeRunContext === ctx) automaticReview.schedule();
|
|
93
|
-
});
|
|
94
|
-
pi.on("session_shutdown", async (event, ctx) => {
|
|
95
|
-
activeRunContext = undefined;
|
|
96
|
-
automaticReview.close();
|
|
97
|
-
recipeReload.close();
|
|
98
|
-
runUiRuntime.shutdown(event.reason, ctx);
|
|
99
|
-
});
|
|
100
|
-
InspectorCommand.registerActorInspectorCommand(pi, getRunOwnerId);
|
|
101
|
-
pi.on("before_agent_start", async (event) => ({
|
|
102
|
-
systemPrompt: `${event.systemPrompt}\n\n${Prompts.ONBOARDING_SYSTEM_PROMPT}`,
|
|
103
|
-
}));
|
|
104
|
-
Pi.registerToolDefinitions(
|
|
105
|
-
pi,
|
|
106
|
-
Tools.createCoreActorToolDefinitions<Pi.ExtensionContext>({
|
|
107
|
-
configPath: Paths.EXTENSION_RUNTIME_PATHS.configPath,
|
|
108
|
-
getActiveTools: () => pi.getActiveTools(),
|
|
109
|
-
getRuntimeTool: (name) =>
|
|
110
|
-
Tools.resolveActiveRuntimeTool(name, runtime.getTools(), (activeName) =>
|
|
111
|
-
actorToolDefinitions.get(activeName),
|
|
112
|
-
),
|
|
113
|
-
handleRuntimeControl: automaticReview.handleControl,
|
|
114
|
-
registryRuntime: runtime,
|
|
115
|
-
setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
|
|
116
|
-
}).map(withCurrentThinkingContext),
|
|
12
|
+
const runtime = ExtensionRuntime.createActorExtensionRuntime(pi);
|
|
13
|
+
pi.on("resources_discover", async () =>
|
|
14
|
+
runtime.discoverResources(import.meta.url),
|
|
117
15
|
);
|
|
16
|
+
pi.on("session_start", async (_event, ctx) => runtime.onSessionStart(ctx));
|
|
17
|
+
pi.on("agent_end", async (_event, ctx) => runtime.onAgentEnd(ctx));
|
|
18
|
+
pi.on("session_shutdown", async (event, ctx) =>
|
|
19
|
+
runtime.onSessionShutdown(event.reason, ctx),
|
|
20
|
+
);
|
|
21
|
+
pi.on("before_agent_start", async (event) =>
|
|
22
|
+
runtime.beforeAgentStart(
|
|
23
|
+
event.systemPrompt,
|
|
24
|
+
event.systemPromptOptions.skills ?? [],
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
InspectorCommand.registerActorInspectorCommand(pi, runtime.getRunOwnerId);
|
|
28
|
+
runtime.registerCoreTools();
|
|
118
29
|
}
|
package/lib/async-runs.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import * as Paths from "./paths.ts";
|
|
34
34
|
import * as RecipesReferences from "./recipes-references.ts";
|
|
35
35
|
import * as RecipesUsage from "./recipes-usage.ts";
|
|
36
|
+
import * as Schema from "./schema.ts";
|
|
36
37
|
import {
|
|
37
38
|
resolveArtifactManifest,
|
|
38
39
|
resolveArtifactPaths,
|
|
@@ -128,6 +129,8 @@ export interface AsyncRunStartParams {
|
|
|
128
129
|
template?: CommandTemplateValue;
|
|
129
130
|
args?: string[];
|
|
130
131
|
defaults?: Record<string, unknown>;
|
|
132
|
+
recipe_dir?: string;
|
|
133
|
+
skill_dir?: string;
|
|
131
134
|
parallel?: boolean;
|
|
132
135
|
concurrency?: number | string;
|
|
133
136
|
min_successful?: number | string;
|
|
@@ -309,7 +312,14 @@ function resolveStartParams(params: AsyncRunStartParams): AsyncRunStartParams {
|
|
|
309
312
|
fileParams.run_id ||
|
|
310
313
|
fileParams.name ||
|
|
311
314
|
getRunIdFromFile(fileParams.file),
|
|
312
|
-
values: {
|
|
315
|
+
values: {
|
|
316
|
+
...(fileParams.values ?? {}),
|
|
317
|
+
...(params.values ?? {}),
|
|
318
|
+
...(fileParams.recipe_dir
|
|
319
|
+
? { recipe_dir: fileParams.recipe_dir }
|
|
320
|
+
: {}),
|
|
321
|
+
...(fileParams.skill_dir ? { skill_dir: fileParams.skill_dir } : {}),
|
|
322
|
+
},
|
|
313
323
|
};
|
|
314
324
|
}
|
|
315
325
|
|
|
@@ -477,13 +487,37 @@ export function startRun(
|
|
|
477
487
|
!isAbsolute(recipeRelation)
|
|
478
488
|
? dirname(packagedRecipeRoot)
|
|
479
489
|
: undefined;
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
490
|
+
const argSpec = Schema.parseToolArgDeclarationList(startParams.args ?? []);
|
|
491
|
+
if (argSpec.error) throw new Error(argSpec.error);
|
|
492
|
+
const declaredArgs = new Set(argSpec.args);
|
|
493
|
+
for (const key of Object.keys(startParams.defaults ?? {})) {
|
|
494
|
+
if (declaredArgs.size > 0 && !declaredArgs.has(key))
|
|
495
|
+
throw new Error(`Unknown Recipe default argument: ${key}`);
|
|
496
|
+
}
|
|
497
|
+
const values = Schema.normalizeRuntimeValues(
|
|
498
|
+
{
|
|
499
|
+
...argSpec.defaults,
|
|
500
|
+
...(startParams.defaults || {}),
|
|
501
|
+
...(packagedRepo ? { repo: packagedRepo } : {}),
|
|
502
|
+
...(startParams.values || {}),
|
|
503
|
+
run_id: run,
|
|
504
|
+
state_dir: stateDir,
|
|
505
|
+
trace_file: join(stateDir, "trace.jsonl"),
|
|
506
|
+
},
|
|
507
|
+
argSpec.argTypes,
|
|
508
|
+
);
|
|
509
|
+
const runtimeTemplate =
|
|
510
|
+
resolved.template &&
|
|
511
|
+
typeof resolved.template === "object" &&
|
|
512
|
+
!Array.isArray(resolved.template)
|
|
513
|
+
? {
|
|
514
|
+
...resolved.template,
|
|
515
|
+
defaults: {
|
|
516
|
+
...(resolved.template.defaults ?? {}),
|
|
517
|
+
...values,
|
|
518
|
+
},
|
|
519
|
+
}
|
|
520
|
+
: resolved.template;
|
|
487
521
|
const modelPolicy = describeCurrentPolicyProvenance({
|
|
488
522
|
defaults: startParams.defaults,
|
|
489
523
|
template: resolved.template,
|
|
@@ -557,7 +591,7 @@ export function startRun(
|
|
|
557
591
|
state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
|
|
558
592
|
status: "running",
|
|
559
593
|
...(startParams.tool ? { tool: startParams.tool } : {}),
|
|
560
|
-
template:
|
|
594
|
+
template: runtimeTemplate,
|
|
561
595
|
values,
|
|
562
596
|
model_policy: modelPolicy,
|
|
563
597
|
...(artifacts ? { artifacts } : {}),
|
package/lib/config.ts
CHANGED
|
@@ -188,6 +188,10 @@ export function normalizeStoredTool(
|
|
|
188
188
|
template: argTemplate,
|
|
189
189
|
};
|
|
190
190
|
const inferredArgTypes = Schema.getTemplateArgTypes(argTemplateConfig);
|
|
191
|
+
Schema.assertCompatibleToolArgTypes(
|
|
192
|
+
inferredArgTypes,
|
|
193
|
+
declarations.argTypes,
|
|
194
|
+
);
|
|
191
195
|
const argTypes = { ...inferredArgTypes, ...declarations.argTypes };
|
|
192
196
|
const cfg = {
|
|
193
197
|
name,
|