@llblab/pi-actors 0.46.0 → 0.46.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 (50) hide show
  1. package/BACKLOG.md +582 -0
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +1 -1
  4. package/dist/lib/async-runs.d.ts +1 -0
  5. package/dist/lib/async-runs.js +4 -1
  6. package/dist/lib/execution.d.ts +1 -0
  7. package/dist/lib/execution.js +1 -0
  8. package/dist/lib/extension-runtime.js +25 -9
  9. package/dist/lib/inspector.js +1 -0
  10. package/dist/lib/prompts.d.ts +1 -1
  11. package/dist/lib/prompts.js +2 -2
  12. package/dist/lib/recipes-context.d.ts +12 -3
  13. package/dist/lib/recipes-context.js +28 -4
  14. package/dist/lib/recipes-discovery.d.ts +18 -0
  15. package/dist/lib/recipes-discovery.js +95 -22
  16. package/dist/lib/recipes-references.d.ts +18 -0
  17. package/dist/lib/recipes-references.js +120 -33
  18. package/dist/lib/registry.d.ts +14 -1
  19. package/dist/lib/registry.js +110 -87
  20. package/dist/lib/runtime.d.ts +30 -3
  21. package/dist/lib/runtime.js +73 -9
  22. package/dist/lib/tools-inspect.d.ts +3 -0
  23. package/dist/lib/tools-inspect.js +40 -20
  24. package/dist/lib/tools-local.d.ts +2 -2
  25. package/dist/lib/tools-local.js +5 -2
  26. package/dist/lib/tools-response.js +2 -0
  27. package/dist/lib/tools-spawn.d.ts +2 -2
  28. package/dist/lib/tools-spawn.js +1 -1
  29. package/dist/lib/tools.d.ts +4 -1
  30. package/dist/lib/tools.js +2 -0
  31. package/dist/skills/actors/SKILL.md +3 -3
  32. package/docs/template-recipes.md +3 -1
  33. package/docs/tool-registry.md +9 -3
  34. package/lib/async-runs.ts +5 -1
  35. package/lib/execution.ts +2 -0
  36. package/lib/extension-runtime.ts +33 -14
  37. package/lib/inspector.ts +1 -0
  38. package/lib/prompts.ts +2 -2
  39. package/lib/recipes-context.ts +49 -4
  40. package/lib/recipes-discovery.ts +164 -24
  41. package/lib/recipes-references.ts +163 -39
  42. package/lib/registry.ts +185 -94
  43. package/lib/runtime.ts +108 -12
  44. package/lib/tools-inspect.ts +57 -23
  45. package/lib/tools-local.ts +9 -3
  46. package/lib/tools-response.ts +2 -0
  47. package/lib/tools-spawn.ts +3 -2
  48. package/lib/tools.ts +4 -1
  49. package/package.json +1 -1
  50. package/skills/actors/SKILL.md +3 -3
@@ -7,11 +7,20 @@ import { existsSync, watch } from "node:fs";
7
7
  import { basename, dirname } from "node:path";
8
8
  import * as Paths from "./paths.js";
9
9
  import * as RecipesDiscovery from "./recipes-discovery.js";
10
+ import * as RecipesUsage from "./recipes-usage.js";
10
11
  import * as ToolsLocal from "./tools-local.js";
11
12
  export function createAutoToolsRuntime(deps) {
12
13
  const tools = new Map();
13
14
  const runtimeToolFingerprints = new Map();
14
15
  const runtimeTools = new Set();
16
+ const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
17
+ const status = {
18
+ active_tool_count: 0,
19
+ last_scan_counts: { active: 0, rejected: 0, scanned: 0 },
20
+ registry_generation: 0,
21
+ watch_status: "closed",
22
+ watched_root: recipeRoot,
23
+ };
15
24
  function notify(ctx, message, type) {
16
25
  if (ctx.hasUI)
17
26
  ctx.ui.notify(message, type);
@@ -44,13 +53,29 @@ export function createAutoToolsRuntime(deps) {
44
53
  const staleSet = new Set(stale);
45
54
  deps.setActiveTools(deps.getActiveTools().filter((name) => !staleSet.has(name)));
46
55
  }
56
+ function activationFor(name) {
57
+ const hostRegistered = deps.getAllTools?.().some((tool) => tool.name === name) ?? false;
58
+ const activeTool = deps.getActiveTools?.().includes(name) ?? false;
59
+ return {
60
+ active_tool: activeTool,
61
+ activation: hostRegistered && activeTool ? "current_session" : "unverified",
62
+ callable_now: hostRegistered && activeTool,
63
+ host_registered: hostRegistered,
64
+ };
65
+ }
47
66
  function registerRuntimeTool(cfg) {
48
67
  const fingerprint = getToolFingerprint(cfg);
49
- if (runtimeToolFingerprints.get(cfg.name) === fingerprint)
50
- return;
51
- deps.registerTool(ToolsLocal.createRuntimeToolDefinition(cfg, deps.exec));
52
- runtimeTools.add(cfg.name);
53
- runtimeToolFingerprints.set(cfg.name, fingerprint);
68
+ if (runtimeToolFingerprints.get(cfg.name) !== fingerprint) {
69
+ deps.registerTool(ToolsLocal.createRuntimeToolDefinition(cfg, deps.exec));
70
+ runtimeTools.add(cfg.name);
71
+ runtimeToolFingerprints.set(cfg.name, fingerprint);
72
+ }
73
+ if (deps.getActiveTools && deps.setActiveTools) {
74
+ deps.setActiveTools([
75
+ ...new Set([...deps.getActiveTools(), cfg.name]),
76
+ ]);
77
+ }
78
+ return activationFor(cfg.name);
54
79
  }
55
80
  function isStartupActionableRegistryWarning(warning) {
56
81
  if (warning.includes(" shadows "))
@@ -82,11 +107,15 @@ export function createAutoToolsRuntime(deps) {
82
107
  }
83
108
  return `${lines.join("\n")}\n`;
84
109
  }
85
- function loadTools(ctx) {
110
+ function loadTools(ctx, resolutionContext) {
86
111
  const warnings = [];
87
- const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
88
112
  const discovered = RecipesDiscovery.discoverRecipeSources([
89
- { root: recipeRoot, defaultTool: true, mutableUsage: true },
113
+ {
114
+ root: recipeRoot,
115
+ defaultTool: true,
116
+ mutableUsage: true,
117
+ resolutionContext,
118
+ },
90
119
  ]);
91
120
  warnings.push(...discovered.diagnostics);
92
121
  tools.clear();
@@ -109,17 +138,47 @@ export function createAutoToolsRuntime(deps) {
109
138
  }
110
139
  registerRuntimeTool(cfg);
111
140
  }
141
+ status.active_tool_count = tools.size;
142
+ status.last_scan_at = new Date().toISOString();
143
+ status.last_scan_counts = {
144
+ active: tools.size,
145
+ rejected: discovered.entries.filter((entry) => entry.invalid).length,
146
+ scanned: discovered.entries.length,
147
+ };
148
+ status.registry_generation += 1;
149
+ status.resolution_generation = resolutionContext?.generation;
112
150
  const startupWarnings = warnings.filter(isStartupActionableRegistryWarning);
113
151
  if (startupWarnings.length > 0) {
114
152
  notify(ctx, formatRecipeToolWarnings(startupWarnings), "warning");
115
153
  }
116
154
  }
117
155
  return {
156
+ getStatus: () => ({
157
+ ...status,
158
+ last_scan_counts: { ...status.last_scan_counts },
159
+ }),
160
+ getToolStatus: (name) => {
161
+ const cfg = tools.get(name);
162
+ if (!cfg)
163
+ return undefined;
164
+ const usage = cfg.sourcePath
165
+ ? RecipesUsage.readRecipeUsage(cfg.sourcePath)
166
+ : undefined;
167
+ return {
168
+ ...activationFor(name),
169
+ launch_kind: usage?.launch_kind,
170
+ spawn_calls: Number(usage?.spawn_calls ?? 0),
171
+ tool_calls: Number(usage?.tool_calls ?? 0),
172
+ };
173
+ },
118
174
  getToolNameBlocker,
119
175
  getTools: () => tools,
120
176
  loadTools,
121
177
  notify,
122
178
  registerRuntimeTool,
179
+ setWatchStatus: (watchStatus) => {
180
+ status.watch_status = watchStatus;
181
+ },
123
182
  };
124
183
  }
125
184
  export function createRecipeToolReloadWatcher(runtime, deps = {}) {
@@ -129,6 +188,7 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
129
188
  let rootWatcher;
130
189
  let parentWatcher;
131
190
  let failureNotified = false;
191
+ const setWatchStatus = (watchStatus) => runtime.setWatchStatus?.(watchStatus);
132
192
  const close = () => {
133
193
  const closingRoot = rootWatcher;
134
194
  rootWatcher = undefined;
@@ -139,11 +199,13 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
139
199
  if (reloadTimeout)
140
200
  clearTimeout(reloadTimeout);
141
201
  reloadTimeout = undefined;
202
+ setWatchStatus("closed");
142
203
  };
143
204
  const notifyFailure = (ctx) => {
144
205
  if (failureNotified)
145
206
  return;
146
207
  failureNotified = true;
208
+ setWatchStatus("failed");
147
209
  ctx.ui.notify("Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.", "warning");
148
210
  };
149
211
  const scheduleReload = (ctx) => {
@@ -151,7 +213,7 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
151
213
  if (reloadTimeout)
152
214
  clearTimeout(reloadTimeout);
153
215
  reloadTimeout = setTimeout(() => {
154
- runtime.loadTools(ctx);
216
+ runtime.loadTools(ctx, deps.getResolutionContext?.());
155
217
  ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
156
218
  }, 150);
157
219
  reloadTimeout.unref?.();
@@ -181,6 +243,7 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
181
243
  scheduleReload(ctx);
182
244
  });
183
245
  parentWatcher = watcher;
246
+ setWatchStatus("watching_parent");
184
247
  watcher.on("error", () => {
185
248
  if (parentWatcher !== watcher)
186
249
  return;
@@ -214,6 +277,7 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
214
277
  scheduleReload(ctx);
215
278
  });
216
279
  rootWatcher = watcher;
280
+ setWatchStatus("watching_root");
217
281
  watcher.on("error", () => {
218
282
  if (rootWatcher !== watcher)
219
283
  return;
@@ -3,10 +3,13 @@
3
3
  * Zones: Run Recipe/Trace/Control views and runtime/recipe/tool diagnostics
4
4
  * Owns exact inspect target/view dispatch; source projection stays in domain modules.
5
5
  */
6
+ import * as Runtime from "./runtime.ts";
6
7
  export interface InspectToolDeps<TContext = unknown> {
7
8
  getRunStatus?: (runOrDir: string) => Record<string, any>;
8
9
  getTool?: (name: string) => any | undefined;
10
+ getToolStatus?: (name: string) => Record<string, unknown> | undefined;
9
11
  listRuns?: () => Array<Record<string, any>>;
10
12
  recipeRoot?: string;
13
+ registryStatus?: () => Runtime.RecipeRegistryStatus;
11
14
  }
12
15
  export declare function createInspectToolDefinition<TContext = unknown>(deps?: InspectToolDeps<TContext>): any;
@@ -4,7 +4,7 @@
4
4
  * Owns exact inspect target/view dispatch; source projection stays in domain modules.
5
5
  */
6
6
  import { statSync } from "node:fs";
7
- import { join } from "node:path";
7
+ import { isAbsolute, join, relative } from "node:path";
8
8
  import * as AsyncRuns from "./async-runs.js";
9
9
  import * as ControlProjection from "./control-projection.js";
10
10
  import * as Inspector from "./inspector.js";
@@ -157,6 +157,15 @@ function inspectRuntime(view, input, ctx, deps) {
157
157
  return runtimeTriage(ctx, deps);
158
158
  throw new Error("inspect runtime supports view=status, view=runs, or view=triage.");
159
159
  }
160
+ function portableSkillRecipePath(file, skill, namespaces) {
161
+ const root = namespaces[skill]?.[0];
162
+ if (!root)
163
+ return `<active-skill:${skill}>`;
164
+ const relation = relative(root, file);
165
+ return !relation.startsWith("..") && !isAbsolute(relation)
166
+ ? `<active-skill:${skill}>/${relation.replaceAll("\\", "/")}`
167
+ : `<active-skill:${skill}>`;
168
+ }
160
169
  function inspectRecipes(view, deps, context) {
161
170
  if (view !== "status" &&
162
171
  view !== "summary" &&
@@ -172,30 +181,39 @@ function inspectRecipes(view, deps, context) {
172
181
  const discovered = RecipesDiscovery.discoverRecipeSources([
173
182
  { root: recipeRoot, defaultTool: true, mutableUsage: true },
174
183
  ]);
175
- const skillContext = context && typeof context === "object" && "activeSkillRecipeContext" in context
176
- ? context.activeSkillRecipeContext
184
+ const resolutionContext = context && typeof context === "object" && "recipeResolutionContext" in context
185
+ ? context.recipeResolutionContext
177
186
  : undefined;
178
- const activeSkillContext = skillContext ?? RecipesReferences.EMPTY_ACTIVE_SKILL_RECIPE_CONTEXT;
187
+ const activeSkillContext = resolutionContext?.activeSkills ??
188
+ RecipesReferences.EMPTY_ACTIVE_SKILL_RECIPE_CONTEXT;
179
189
  const skillRecipeNamespaces = RecipesReferences.getActiveSkillRecipeNamespaces(activeSkillContext);
180
- let skillRecipeComponents = [];
181
- const skillRecipeComponentDiagnostics = [];
182
- try {
183
- skillRecipeComponents = RecipesReferences.listActiveSkillRecipeComponents(activeSkillContext).map((component) => ({
184
- identity: component.identity,
185
- source_kind: "active_skill_component",
186
- skill: component.skill,
187
- stem: component.stem,
188
- imports: component.imports,
189
- }));
190
- }
191
- catch (error) {
192
- skillRecipeComponentDiagnostics.push({
193
- error: error instanceof Error ? error.message : String(error),
194
- });
195
- }
190
+ const skillInventory = RecipesReferences.inventoryActiveSkillRecipeComponents(activeSkillContext);
191
+ const skillRecipeComponents = skillInventory.components.map((component) => ({
192
+ identity: component.identity,
193
+ source_kind: "active_skill_component",
194
+ skill: component.skill,
195
+ stem: component.stem,
196
+ imports: component.imports,
197
+ }));
198
+ const skillRecipeComponentDiagnostics = skillInventory.rejected.map((diagnostic) => ({
199
+ error: diagnostic.reason,
200
+ ...(diagnostic.file
201
+ ? {
202
+ file: portableSkillRecipePath(diagnostic.file, diagnostic.skill, skillRecipeNamespaces),
203
+ }
204
+ : {}),
205
+ skill: diagnostic.skill,
206
+ ...(diagnostic.stem ? { stem: diagnostic.stem } : {}),
207
+ }));
196
208
  const discoverySummary = RecipesDiscovery.summarizeDiscovery(discovered);
197
209
  const summary = {
198
210
  ...discoverySummary,
211
+ ...(deps.registryStatus
212
+ ? {
213
+ ...deps.registryStatus(),
214
+ watched_root: "~/.pi/agent/recipes",
215
+ }
216
+ : {}),
199
217
  active: Array.isArray(discoverySummary.active)
200
218
  ? discoverySummary.active.map((entry) => ({
201
219
  ...entry,
@@ -204,6 +222,7 @@ function inspectRecipes(view, deps, context) {
204
222
  : discoverySummary.active,
205
223
  skill_recipe_components: skillRecipeComponents,
206
224
  skill_recipe_component_diagnostics: skillRecipeComponentDiagnostics,
225
+ skill_recipe_catalog_partial: skillInventory.partial,
207
226
  active_skill_recipe_identities: Object.keys(skillRecipeNamespaces).sort(),
208
227
  skill_recipe_namespace_diagnostics: Object.entries(skillRecipeNamespaces)
209
228
  .filter(([, roots]) => roots.length > 1)
@@ -307,6 +326,7 @@ function inspectTool(name, view, deps) {
307
326
  throw new Error(`registered tool not found: ${name}`);
308
327
  return {
309
328
  name,
329
+ ...(view === "status" ? deps.getToolStatus?.(name) : {}),
310
330
  description: tool.description,
311
331
  parameters: tool.parameters,
312
332
  promptSnippet: tool.promptSnippet,
@@ -6,9 +6,9 @@
6
6
  import * as ModelContext from "./model-context.ts";
7
7
  import type { RegisteredTool } from "./config.ts";
8
8
  import * as Execution from "./execution.ts";
9
- import * as RecipesReferences from "./recipes-references.ts";
9
+ import type * as RecipeResolution from "./recipes-context.ts";
10
10
  export interface RuntimeToolContext extends ModelContext.CurrentModelContext {
11
- activeSkillRecipeContext?: RecipesReferences.ActiveSkillRecipeContext;
11
+ recipeResolutionContext?: RecipeResolution.RecipeResolutionContext;
12
12
  cwd: string;
13
13
  sessionManager?: {
14
14
  getSessionId?: () => string;
@@ -44,7 +44,8 @@ function shouldAddRuntimeToolUsageHint(error) {
44
44
  return (/^Argument \S+ must /.test(message) || /^Missing .* value: /.test(message));
45
45
  }
46
46
  function formatRuntimeToolUsageHint(cfg, required, includeRunId) {
47
- const optional = cfg.args.filter((arg) => !required.includes(arg));
47
+ const optional = cfg.args.filter((arg) => !RecipesReferences.isRuntimeOwnedRecipeInput(arg) &&
48
+ !required.includes(arg));
48
49
  const exampleDefaults = {
49
50
  ...Schema.getExplicitToolArgDefaults(cfg.recipe?.args),
50
51
  ...cfg.defaults,
@@ -115,6 +116,8 @@ export function createRuntimeToolDefinition(cfg, exec) {
115
116
  ? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
116
117
  : Schema.getRequiredToolArgNames(requiredTemplateConfig);
117
118
  for (const arg of cfg.args) {
119
+ if (RecipesReferences.isRuntimeOwnedRecipeInput(arg))
120
+ continue;
118
121
  paramSchema[arg] = typedArgSchema(arg, cfg.argTypes?.[arg]);
119
122
  if (requiredArgs.has(arg))
120
123
  required.push(arg);
@@ -160,7 +163,7 @@ export function createRuntimeToolDefinition(cfg, exec) {
160
163
  tool: cfg.name,
161
164
  policy_values: ModelContext.withCurrentModelValues({ ...(cfg.recipe?.values ?? {}), ...values }, ctx),
162
165
  values: resolveRecipeToolValues(cfg, values, ctx),
163
- }, ctx.cwd, { skillContext: ctx.activeSkillRecipeContext });
166
+ }, ctx.cwd, { skillContext: ctx.recipeResolutionContext?.activeSkills });
164
167
  return {
165
168
  content: [
166
169
  {
@@ -91,6 +91,8 @@ export function compactAsyncRunStatus(value) {
91
91
  const result = asRecord(status.result);
92
92
  const run = String(status.run ?? "<unknown>");
93
93
  const tokens = [`run=${run}`, `status=${String(status.status ?? "unknown")}`];
94
+ if (status.launch_kind)
95
+ tokens.push(`launch_kind=${String(status.launch_kind)}`);
94
96
  tokens.push(...compactModelPolicy(status.model_policy ?? progress.model_policy));
95
97
  if (status.tool)
96
98
  tokens.push(`tool=${String(status.tool)}`);
@@ -4,9 +4,9 @@
4
4
  * Owns the public spawn execution path for run-backed actors
5
5
  */
6
6
  import * as ModelContext from "./model-context.ts";
7
- import * as RecipesReferences from "./recipes-references.ts";
7
+ import type * as RecipeResolution from "./recipes-context.ts";
8
8
  export interface SpawnToolContext extends ModelContext.CurrentModelContext {
9
- activeSkillRecipeContext?: RecipesReferences.ActiveSkillRecipeContext;
9
+ recipeResolutionContext?: RecipeResolution.RecipeResolutionContext;
10
10
  cwd: string;
11
11
  sessionManager?: {
12
12
  getSessionId?: () => string;
@@ -111,7 +111,7 @@ export function createSpawnToolDefinition() {
111
111
  artifacts: input.artifacts,
112
112
  }
113
113
  : {}),
114
- }, ctx.cwd, { skillContext: ctx.activeSkillRecipeContext });
114
+ }, ctx.cwd, { skillContext: ctx.recipeResolutionContext?.activeSkills });
115
115
  const draftRecipe = writeSpawnDraftRecipe(input, meta);
116
116
  const nextActions = ToolsResponse.runNextActions(meta.run);
117
117
  const details = {
@@ -15,8 +15,11 @@ export interface CoreActorToolDefinitionDeps<TContext extends RuntimeToolContext
15
15
  configPath: string;
16
16
  getActiveTools: () => string[];
17
17
  getRuntimeTool: (name: string) => unknown;
18
+ getRuntimeToolStatus: (name: string) => Record<string, unknown> | undefined;
18
19
  handleRuntimeControl?: (action: string, input: unknown) => Record<string, unknown>;
19
- registryRuntime: Pick<RegisterToolRuntimeDeps<TContext>, "getToolNameBlocker" | "getTools" | "notify" | "registerRuntimeTool">;
20
+ registryRuntime: Pick<RegisterToolRuntimeDeps<TContext>, "getToolNameBlocker" | "getTools" | "notify" | "registerRuntimeTool"> & {
21
+ getStatus(): import("./runtime.ts").RecipeRegistryStatus;
22
+ };
20
23
  setActiveTools: (toolNames: string[]) => void;
21
24
  }
22
25
  export declare function resolveActiveRuntimeTool(name: string, activeTools: Pick<Map<string, unknown>, "has">, getDefinition: (name: string) => unknown): unknown;
package/dist/lib/tools.js CHANGED
@@ -41,6 +41,8 @@ export function createCoreActorToolDefinitions(deps) {
41
41
  }),
42
42
  ToolsInspect.createInspectToolDefinition({
43
43
  getTool: (name) => deps.getRuntimeTool(name),
44
+ getToolStatus: deps.getRuntimeToolStatus,
45
+ registryStatus: deps.registryRuntime.getStatus,
44
46
  }),
45
47
  ];
46
48
  }
@@ -19,9 +19,9 @@ Use the swarm skill separately for decomposition, quorum design, reviewer lenses
19
19
  - `spawn`: create one Run from a Recipe or inline command template.
20
20
  - `message`: send one actor-local Control to `run:<id>`, or the reserved review actions to `runtime`.
21
21
  - `inspect`: inspect `run:<id>`, `runtime`, `recipes`, or `tool:<name>`.
22
- - `register_tool`: persist a trusted capability; it does not address a running actor.
22
+ - `register_tool`: persist a trusted capability; it does not address a running actor. Trust current-session callability only when its result says `callable_now: true`.
23
23
 
24
- A Run target exposes exactly three inspect views: `recipe`, `trace`, and `control`.
24
+ A Run target exposes exactly three inspect views: `recipe`, `trace`, and `control`. Spawning a user Recipe is still a Recipe launch (`launch_kind: "spawn"`), not evidence that its registered tool was exposed or invoked; registered-tool execution reports `launch_kind: "tool"`, and `inspect target=tool:<name> view=status` separates both usage counts.
25
25
 
26
26
  ## Recipe
27
27
 
@@ -29,7 +29,7 @@ A Recipe defines execution. It may declare named typed args, inline fallbacks, c
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 Skill-owned Recipes over ad hoc wrappers. Skill Recipe identity is `<active Skill name>/<Recipe filename stem>`; Recipe files have no top-level `name`. `SKILL.md` `name` is Pi host metadata matching its directory, not an additional pi-actors identity field. Use `<skill>/<recipe>` for an exact direct component or an explicit `.json` / `.md` path. Entry paths resolve from invocation `cwd`; relative imports resolve from the importing Recipe directory. 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.
32
+ Prefer maintained Skill-owned Recipes over ad hoc wrappers. Skill Recipe identity is `<active Skill name>/<Recipe filename stem>`; Recipe files have no top-level `name`. `SKILL.md` `name` is Pi host metadata matching its directory, not an additional pi-actors identity field. Use `<skill>/<recipe>` for an exact direct component or an explicit `.json` / `.md` path. Entry paths resolve from invocation `cwd`; relative imports resolve from the importing Recipe directory. 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. A rejected component makes catalog inspection partial without blocking exact resolution of unrelated valid components in the same live session context. 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
 
@@ -147,7 +147,9 @@ Resolution fails before launch when required current policy is unavailable. The
147
147
 
148
148
  ## Resolution Context
149
149
 
150
- User Recipes under `~/.pi/agent/recipes` remain intentionally registered tools, not an ambient import namespace. Each session receives an immutable active-Skill resolution context from Pi's loaded Skill metadata; pi-actors does not scan ambient Skill roots independently or keep a process-global mutable namespace. A launch captures its resolved graph, so later Skill changes affect only future launches. An invalid or missing exact target fails without fallback. Disabled Recipes cannot launch. Registry watchers converge after atomic changes without executing partial definitions.
150
+ User Recipes under `~/.pi/agent/recipes` remain intentionally registered tools, not an ambient import namespace. Each session receives one immutable resolution context from Pi's loaded Skill metadata; spawn, user-Recipe admission, registration, schema derivation, live inspection, and watcher reconciliation consume that same context rather than scanning ambient Skill roots or keeping a process-global mutable namespace. A launch captures its resolved graph, so later Skill changes affect only future launches. An invalid or missing exact target fails without fallback. Disabled Recipes cannot launch. Registry watchers converge after atomic changes without executing partial definitions.
151
+
152
+ Active-Skill catalog inventory is fail-soft diagnostic state, not exact-resolution authority. Invalid components are reported individually and make the catalog partial while unrelated valid `<skill>/<recipe>` references remain exactly resolvable.
151
153
 
152
154
  ## Validation
153
155
 
@@ -6,7 +6,7 @@
6
6
  ~/.pi/agent/recipes/*.json
7
7
  ```
8
8
 
9
- Each active valid Recipe becomes an agent-callable tool. `register_tool` creates, updates, promotes, or deletes these files through fenced mutation paths.
9
+ Each valid Recipe admitted against the current session context can become an agent-callable tool. `register_tool` creates, updates, promotes, or deletes these files through fenced mutation paths and reports the resulting activation state.
10
10
 
11
11
  ## Registration
12
12
 
@@ -20,9 +20,13 @@ Register a typed/defaulted template or Recipe-backed definition when reuse justi
20
20
 
21
21
  Promote an immutable captured draft only with its draft path and explicit target name. Name collisions require `update=true`. Invalid content fails before active mutation.
22
22
 
23
+ Registration resolves the effective delegated contract before persistence, then reports distinct `persisted`, `registry_active`, `host_registered`, `active_tool`, and `callable_now` states. Treat the tool as callable in the current session only when `callable_now` is true; persistence alone is not activation proof.
24
+
23
25
  ## Resolution
24
26
 
25
- User Recipes are the only file-discovered tool source. Invalid or disabled user entries fail closed. Active Skill Recipes remain exact components outside tool discovery. Runtime reload watches the user Recipe root and converges after atomic changes; stale watcher generations cannot replace current registration state.
27
+ User Recipes are the only file-discovered tool source. Invalid or disabled user entries fail closed. Active Skill Recipes remain exact components outside tool discovery. Spawn, registration, registry admission, schema derivation, and live inspection resolve against one immutable session context containing the current working directory and active Skills. Runtime reload watches the user Recipe root using that current context and converges after atomic changes; stale watcher generations cannot replace current registration state.
28
+
29
+ Skill component inventory is diagnostic and fail-soft: valid components remain listed and exactly resolvable when an unrelated component is rejected. Recipe inspection reports rejected components and marks the catalog partial instead of treating one bad component as an empty catalog.
26
30
 
27
31
  Inspect registry state with:
28
32
 
@@ -31,7 +35,9 @@ inspect target=recipes view=status
31
35
  inspect target=tool:<name> view=status
32
36
  ```
33
37
 
34
- Recipe inspection reports active, shadowed, invalid, disabled, diagnostic, risk, usage, and review evidence. Tool inspection reports the current capability definition/schema; a registered tool is not a running actor.
38
+ Recipe inspection reports generation, scan/watch state, active, shadowed, invalid, disabled, component rejection, diagnostic, risk, usage, and review evidence. Tool status reports current activation plus separate `tool_calls` and `spawn_calls`; tool schema reports the caller-owned capability contract. A registered tool is not a running actor.
39
+
40
+ `spawn recipe=<name>` executes a Recipe and reports `launch_kind: "spawn"`; it does not prove that a registered tool was exposed or invoked. Registered-tool execution reports `launch_kind: "tool"`.
35
41
 
36
42
  ## Automatic Review
37
43
 
package/lib/async-runs.ts CHANGED
@@ -161,6 +161,7 @@ export interface AsyncRunMeta {
161
161
  argv: string[];
162
162
  createdAt: string;
163
163
  cwd: string;
164
+ launch_kind?: AsyncRunLaunchSource;
164
165
  launch_source?: AsyncRunLaunchSource;
165
166
  launch_correlation?: {
166
167
  correlation_id?: string;
@@ -600,7 +601,10 @@ export function startRun(
600
601
  createdAt: new Date().toISOString(),
601
602
  cwd,
602
603
  ...(startParams.launch_source
603
- ? { launch_source: startParams.launch_source }
604
+ ? {
605
+ launch_kind: startParams.launch_source,
606
+ launch_source: startParams.launch_source,
607
+ }
604
608
  : {}),
605
609
  ...(startParams.launch_correlation
606
610
  ? { launch_correlation: startParams.launch_correlation } : {}),
package/lib/execution.ts CHANGED
@@ -83,6 +83,7 @@ export interface RegisteredToolExecutionResult {
83
83
  command: string;
84
84
  fullOutputPath?: string;
85
85
  killed: boolean;
86
+ launch_kind: "tool";
86
87
  stderrBytes?: number;
87
88
  stderrCapturedBytes?: number;
88
89
  stderrFile?: string;
@@ -1136,6 +1137,7 @@ export async function executeRegisteredTool(
1136
1137
  command,
1137
1138
  fullOutputPath: result.stdoutFile ?? formatted.fullOutputPath,
1138
1139
  killed: result.killed,
1140
+ launch_kind: "tool",
1139
1141
  ...getCaptureDetails(result),
1140
1142
  ...(executed.branches.length > 0 ? { branches: executed.branches } : {}),
1141
1143
  ...(executed.failures.length > 0
@@ -9,6 +9,7 @@ import * as CommandTemplates from "./command-templates.ts";
9
9
  import * as Paths from "./paths.ts";
10
10
  import * as Pi from "./pi.ts";
11
11
  import * as Prompts from "./prompts.ts";
12
+ import * as RecipeResolution from "./recipes-context.ts";
12
13
  import * as RecipesReferences from "./recipes-references.ts";
13
14
  import * as RunUiRuntime from "./run-ui-runtime.ts";
14
15
  import * as Runtime from "./runtime.ts";
@@ -34,14 +35,21 @@ export function createActorExtensionRuntime(
34
35
  pi: Pi.ExtensionAPI,
35
36
  ): ActorExtensionRuntime {
36
37
  let activeRunContext: Pi.ExtensionContext | undefined;
37
- const skillContextsBySession = new Map<
38
+ const recipeResolutionContextsBySession = new Map<
38
39
  string,
39
- RecipesReferences.ActiveSkillRecipeContext
40
+ RecipeResolution.RecipeResolutionContext
40
41
  >();
41
42
  const getRunOwnerId = Pi.getSessionId;
42
- const getSkillContext = (ctx: Pi.ExtensionContext) =>
43
- skillContextsBySession.get(getRunOwnerId(ctx)) ??
44
- RecipesReferences.EMPTY_ACTIVE_SKILL_RECIPE_CONTEXT;
43
+ const getRecipeResolutionContext = (ctx: Pi.ExtensionContext) => {
44
+ const sessionId = getRunOwnerId(ctx);
45
+ const resolutionContext = recipeResolutionContextsBySession.get(sessionId);
46
+ if (!resolutionContext) {
47
+ throw new Error(
48
+ `Recipe resolution context is unavailable for session ${sessionId}.`,
49
+ );
50
+ }
51
+ return resolutionContext;
52
+ };
45
53
  const automaticReview = AutomaticReviewRuntime.createAutomaticReviewRuntime({
46
54
  getActiveContext: () => activeRunContext,
47
55
  getRunOwnerId,
@@ -67,7 +75,7 @@ export function createActorExtensionRuntime(
67
75
  if (ctx && typeof ctx === "object") {
68
76
  nextArgs[4] = {
69
77
  ...(ctx as Record<string, unknown>),
70
- activeSkillRecipeContext: getSkillContext(
78
+ recipeResolutionContext: getRecipeResolutionContext(
71
79
  ctx as Pi.ExtensionContext,
72
80
  ),
73
81
  getThinkingLevel: () => pi.getThinkingLevel(),
@@ -85,6 +93,7 @@ export function createActorExtensionRuntime(
85
93
  configPath: Paths.EXTENSION_RUNTIME_PATHS.configPath,
86
94
  exec: CommandTemplates.execCommandTemplate,
87
95
  getActiveTools: () => pi.getActiveTools(),
96
+ getAllTools: () => pi.getAllTools(),
88
97
  registerTool: (definition) => {
89
98
  const wrapped = withCurrentThinkingContext(definition);
90
99
  actorToolDefinitions.set(wrapped.name, wrapped);
@@ -93,13 +102,22 @@ export function createActorExtensionRuntime(
93
102
  reservedToolNames: Tools.RESERVED_TOOL_NAMES,
94
103
  setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
95
104
  });
96
- const recipeReload = Runtime.createRecipeToolReloadWatcher(runtime);
105
+ const recipeReload = Runtime.createRecipeToolReloadWatcher(runtime, {
106
+ getResolutionContext: () =>
107
+ activeRunContext
108
+ ? getRecipeResolutionContext(activeRunContext)
109
+ : undefined,
110
+ });
97
111
  return {
98
112
  beforeAgentStart(systemPrompt, skills, ctx) {
99
- skillContextsBySession.set(
100
- getRunOwnerId(ctx),
113
+ const sessionId = getRunOwnerId(ctx);
114
+ const resolutionContext = RecipeResolution.createRecipeResolutionContext(
115
+ sessionId,
116
+ ctx.cwd,
101
117
  RecipesReferences.createActiveSkillRecipeContext(skills),
102
118
  );
119
+ recipeResolutionContextsBySession.set(sessionId, resolutionContext);
120
+ runtime.loadTools(ctx, resolutionContext);
103
121
  return {
104
122
  systemPrompt: `${systemPrompt}\n\n${Prompts.ONBOARDING_SYSTEM_PROMPT}`,
105
123
  };
@@ -113,16 +131,17 @@ export function createActorExtensionRuntime(
113
131
  if (activeRunContext === ctx) automaticReview.schedule();
114
132
  },
115
133
  onSessionShutdown(reason, ctx) {
116
- skillContextsBySession.delete(getRunOwnerId(ctx));
134
+ recipeResolutionContextsBySession.delete(getRunOwnerId(ctx));
117
135
  activeRunContext = undefined;
118
136
  automaticReview.close();
119
137
  recipeReload.close();
120
138
  runUiRuntime.shutdown(reason, ctx);
121
139
  },
122
140
  async onSessionStart(ctx) {
123
- skillContextsBySession.set(
124
- getRunOwnerId(ctx),
125
- RecipesReferences.EMPTY_ACTIVE_SKILL_RECIPE_CONTEXT,
141
+ const sessionId = getRunOwnerId(ctx);
142
+ recipeResolutionContextsBySession.set(
143
+ sessionId,
144
+ RecipeResolution.createEmptyRecipeResolutionContext(sessionId, ctx.cwd),
126
145
  );
127
146
  ctx.ui.setWidget("zz-pi-actors-comms", undefined);
128
147
  activeRunContext = ctx;
@@ -132,7 +151,6 @@ export function createActorExtensionRuntime(
132
151
  await Temp.prepareExtensionTempDir(Paths.EXTENSION_RUNTIME_PATHS.tempDir);
133
152
  if (activeRunContext !== ctx) return;
134
153
  automaticReview.start(ctx);
135
- runtime.loadTools(ctx);
136
154
  runUiRuntime.start(ctx);
137
155
  recipeReload.watch(ctx);
138
156
  },
@@ -148,6 +166,7 @@ export function createActorExtensionRuntime(
148
166
  runtime.getTools(),
149
167
  (activeName) => actorToolDefinitions.get(activeName),
150
168
  ),
169
+ getRuntimeToolStatus: runtime.getToolStatus,
151
170
  handleRuntimeControl: automaticReview.handleControl,
152
171
  registryRuntime: runtime,
153
172
  setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
package/lib/inspector.ts CHANGED
@@ -183,6 +183,7 @@ export function readActorInspectorRecipe(
183
183
  logical_reference: primaryLogicalReference,
184
184
  ...(typeof primary?.skill === "string" ? { skill: primary.skill } : {}),
185
185
  source_kind: primarySourceKind,
186
+ launch_kind: meta.launch_kind ?? meta.launch_source,
186
187
  launch_source: meta.launch_source,
187
188
  }),
188
189
  launch: redactedRecord({
package/lib/prompts.ts CHANGED
@@ -14,7 +14,7 @@ export const REGISTER_TOOL_PROMPT_SNIPPET =
14
14
 
15
15
  export const REGISTER_TOOL_GUIDELINES = [
16
16
  "Use register_tool to wrap trusted local commands, scripts, programs, libraries, or template recipes as persistent pi tools.",
17
- "After register_tool succeeds, the new tool is immediately callable and remains available after reload.",
17
+ "After register_tool succeeds, trust its callable_now and activation result; persisted definitions remain available for admission after reload.",
18
18
  'Set template=null or template="" in register_tool to delete a persisted tool.',
19
19
  "Set update=true in register_tool to overwrite an existing tool registration.",
20
20
  ];
@@ -24,7 +24,7 @@ export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
24
24
  - Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.
25
25
  - Command templates stay sync and shell-free: string leaves split into executable + argv, infer .js/.mjs through node→bun→deno run and .sh through bash, and treat operators such as && as literal arguments; use template arrays for sequencing or an explicit trusted shell/script when shell semantics are required. Flags include args/defaults, parallel, concurrency, min_successful, when, timeout, delay, retry, failure, recover, repeat, accept_output, output.
26
26
  - Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.
27
- - ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.
27
+ - ~/.pi/agent/recipes/*.json is actor muscle memory: valid admitted recipes are reconciled as tools across sessions; register_tool writes there and reports current activation truth.
28
28
  - Recipes own template directly and may declare metadata/defaults/imports/control/artifacts; files >1 MiB or import depth >32 fail closed.
29
29
  - Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.
30
30
  - Actor-mode trigger: if work may outlive this turn, need steering/follow-up/artifacts, run as a service, fan out, or be resumed/inspected later, use spawn -> message -> inspect instead of ad hoc shell backgrounding.