@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.
Files changed (51) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +5 -1
  4. package/dist/index.d.ts +1 -2
  5. package/dist/index.js +10 -106
  6. package/dist/lib/async-runs.d.ts +2 -0
  7. package/dist/lib/async-runs.js +33 -4
  8. package/dist/lib/config.js +1 -0
  9. package/dist/lib/extension-runtime.d.ts +21 -0
  10. package/dist/lib/extension-runtime.js +116 -0
  11. package/dist/lib/inspector.js +8 -2
  12. package/dist/lib/recipes-discovery.js +20 -3
  13. package/dist/lib/recipes-references.d.ts +10 -0
  14. package/dist/lib/recipes-references.js +168 -5
  15. package/dist/lib/schema.d.ts +3 -0
  16. package/dist/lib/schema.js +34 -1
  17. package/dist/lib/tools-inspect.js +10 -0
  18. package/dist/lib/tools-local.js +27 -10
  19. package/dist/lib/tools-spawn.js +2 -2
  20. package/dist/scripts/async-runner.mjs +0 -16
  21. package/dist/scripts/executable-block-style.d.mts +1 -0
  22. package/dist/scripts/executable-block-style.mjs +73 -0
  23. package/dist/scripts/locker.mjs +0 -21
  24. package/dist/scripts/recipe-utils.mjs +0 -1
  25. package/dist/scripts/release-gates.mjs +14 -11
  26. package/dist/scripts/validate-recipe.mjs +0 -1
  27. package/dist/skills/actors/SKILL.md +2 -2
  28. package/docs/command-templates.md +2 -2
  29. package/docs/recipe-library.md +2 -0
  30. package/docs/template-recipes.md +10 -4
  31. package/docs/tool-registry.md +1 -1
  32. package/index.ts +18 -107
  33. package/lib/async-runs.ts +43 -9
  34. package/lib/config.ts +4 -0
  35. package/lib/extension-runtime.ts +139 -0
  36. package/lib/inspector.ts +16 -2
  37. package/lib/recipes-discovery.ts +23 -3
  38. package/lib/recipes-references.ts +227 -7
  39. package/lib/schema.ts +53 -1
  40. package/lib/tools-inspect.ts +11 -0
  41. package/lib/tools-local.ts +49 -19
  42. package/lib/tools-spawn.ts +2 -2
  43. package/package.json +2 -2
  44. package/scripts/async-runner.mjs +0 -16
  45. package/scripts/executable-block-style.d.mts +1 -0
  46. package/scripts/executable-block-style.mjs +73 -0
  47. package/scripts/locker.mjs +0 -21
  48. package/scripts/recipe-utils.mjs +0 -1
  49. package/scripts/release-gates.mjs +14 -11
  50. package/scripts/validate-recipe.mjs +0 -1
  51. package/skills/actors/SKILL.md +2 -2
@@ -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, {})));
@@ -369,7 +369,6 @@ if (!command) {
369
369
  usage();
370
370
  process.exit(1);
371
371
  }
372
-
373
372
  if (command === "run-summary")
374
373
  runSummary(args[0] ?? "~/.pi/agent/tmp/pi-actors/runs");
375
374
  else if (command === "run-ops-snapshot")
@@ -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, defaults, imports, artifacts, command-template flags, and `control: ["action"]` when a long-lived service actually consumes actor-local inputs.
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. Defaults belong in `defaults` or inline placeholder defaults; hosts may normalize interactive shorthand such as `request_timeout:int=60000` before persistence.
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 data; use inline defaults for compact local literals. 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.
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
 
@@ -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:
@@ -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 `defaults`;
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. Resolution enforces Recipe-root priority, file-size/depth bounds, and cycle rejection.
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
 
@@ -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 packaged or skill-owned Recipe by path and delegates by alias. Do not duplicate its executable template, defaults, Control declaration, or artifacts. Install only specific capabilities; internal automatic-review Recipes must not become user-callable tools.
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 AutomaticReviewRuntime from "./lib/automatic-review-runtime.ts";
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
- let activeRunContext: Pi.ExtensionContext | undefined;
22
- const getRunOwnerId = Pi.getSessionId;
23
- const automaticReview = AutomaticReviewRuntime.createAutomaticReviewRuntime({
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: { ...(fileParams.values ?? {}), ...(params.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 values = {
481
- ...(packagedRepo ? { repo: packagedRepo } : {}),
482
- ...(startParams.values || {}),
483
- run_id: run,
484
- state_dir: stateDir,
485
- trace_file: join(stateDir, "trace.jsonl"),
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: resolved.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,