@llblab/pi-actors 0.46.0 → 0.47.0

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 (78) hide show
  1. package/AGENTS.md +7 -5
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +2 -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 +6 -5
  11. package/dist/lib/prompts.js +22 -23
  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 +22 -0
  15. package/dist/lib/recipes-discovery.js +108 -23
  16. package/dist/lib/recipes-references.d.ts +18 -0
  17. package/dist/lib/recipes-references.js +129 -38
  18. package/dist/lib/registry.d.ts +23 -6
  19. package/dist/lib/registry.js +294 -101
  20. package/dist/lib/runtime.d.ts +30 -3
  21. package/dist/lib/runtime.js +108 -9
  22. package/dist/lib/tools-inspect.d.ts +3 -0
  23. package/dist/lib/tools-inspect.js +190 -26
  24. package/dist/lib/tools-local.d.ts +2 -2
  25. package/dist/lib/tools-local.js +5 -2
  26. package/dist/lib/tools-register.js +2 -1
  27. package/dist/lib/tools-response.js +7 -1
  28. package/dist/lib/tools-spawn.d.ts +2 -2
  29. package/dist/lib/tools-spawn.js +1 -1
  30. package/dist/lib/tools.d.ts +4 -1
  31. package/dist/lib/tools.js +2 -0
  32. package/dist/scripts/conformance.mjs +1 -0
  33. package/dist/skills/actors/SKILL.md +76 -65
  34. package/dist/skills/actors/references/diagnostics.md +44 -0
  35. package/dist/skills/actors/references/persistent-tools.md +74 -0
  36. package/dist/skills/actors/references/recipes.md +51 -0
  37. package/dist/skills/actors/references/runs.md +39 -0
  38. package/dist/skills/artifacts/SKILL.md +24 -7
  39. package/dist/skills/media/SKILL.md +35 -7
  40. package/dist/skills/project-work/SKILL.md +28 -7
  41. package/dist/skills/recipe-memory/SKILL.md +27 -7
  42. package/dist/skills/swarm/SKILL.md +41 -445
  43. package/dist/skills/swarm/references/development-swarm.md +87 -539
  44. package/dist/skills/swarm/references/review-swarms.md +115 -0
  45. package/docs/README.md +5 -5
  46. package/docs/recipe-library.md +15 -10
  47. package/docs/template-recipes.md +3 -1
  48. package/docs/tool-registry.md +18 -6
  49. package/lib/async-runs.ts +5 -1
  50. package/lib/execution.ts +2 -0
  51. package/lib/extension-runtime.ts +33 -14
  52. package/lib/inspector.ts +1 -0
  53. package/lib/prompts.ts +24 -24
  54. package/lib/recipes-context.ts +49 -4
  55. package/lib/recipes-discovery.ts +186 -25
  56. package/lib/recipes-references.ts +176 -44
  57. package/lib/registry.ts +441 -113
  58. package/lib/runtime.ts +147 -12
  59. package/lib/tools-inspect.ts +254 -28
  60. package/lib/tools-local.ts +9 -3
  61. package/lib/tools-register.ts +4 -3
  62. package/lib/tools-response.ts +7 -1
  63. package/lib/tools-spawn.ts +3 -2
  64. package/lib/tools.ts +4 -1
  65. package/package.json +1 -1
  66. package/scripts/conformance.mjs +1 -0
  67. package/skills/actors/SKILL.md +76 -65
  68. package/skills/actors/references/diagnostics.md +44 -0
  69. package/skills/actors/references/persistent-tools.md +74 -0
  70. package/skills/actors/references/recipes.md +51 -0
  71. package/skills/actors/references/runs.md +39 -0
  72. package/skills/artifacts/SKILL.md +24 -7
  73. package/skills/media/SKILL.md +35 -7
  74. package/skills/project-work/SKILL.md +28 -7
  75. package/skills/recipe-memory/SKILL.md +27 -7
  76. package/skills/swarm/SKILL.md +41 -445
  77. package/skills/swarm/references/development-swarm.md +87 -539
  78. package/skills/swarm/references/review-swarms.md +115 -0
@@ -7,11 +7,21 @@ 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 RecipesReferences from "./recipes-references.js";
11
+ import * as RecipesUsage from "./recipes-usage.js";
10
12
  import * as ToolsLocal from "./tools-local.js";
11
13
  export function createAutoToolsRuntime(deps) {
12
14
  const tools = new Map();
13
15
  const runtimeToolFingerprints = new Map();
14
16
  const runtimeTools = new Set();
17
+ const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
18
+ const status = {
19
+ active_tool_count: 0,
20
+ last_scan_counts: { active: 0, rejected: 0, scanned: 0 },
21
+ registry_generation: 0,
22
+ watch_status: "closed",
23
+ watched_root: recipeRoot,
24
+ };
15
25
  function notify(ctx, message, type) {
16
26
  if (ctx.hasUI)
17
27
  ctx.ui.notify(message, type);
@@ -31,6 +41,21 @@ export function createAutoToolsRuntime(deps) {
31
41
  template: cfg.template,
32
42
  });
33
43
  }
44
+ function registeredToolSource(cfg) {
45
+ const raw = cfg.sourcePath
46
+ ? RecipesReferences.readRawRecipeConfig(cfg.sourcePath)
47
+ : undefined;
48
+ const template = raw?.template;
49
+ if (typeof template === "string" &&
50
+ /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(template)) {
51
+ return template;
52
+ }
53
+ if (typeof template === "string" &&
54
+ (template.endsWith(".json") || template.endsWith(".md"))) {
55
+ return `<explicit-file:${basename(template)}>`;
56
+ }
57
+ return "template";
58
+ }
34
59
  function deactivateMissingRuntimeTools(activeNames) {
35
60
  const stale = [...runtimeTools].filter((name) => !activeNames.has(name));
36
61
  if (stale.length === 0)
@@ -44,13 +69,29 @@ export function createAutoToolsRuntime(deps) {
44
69
  const staleSet = new Set(stale);
45
70
  deps.setActiveTools(deps.getActiveTools().filter((name) => !staleSet.has(name)));
46
71
  }
72
+ function activationFor(name) {
73
+ const hostRegistered = deps.getAllTools?.().some((tool) => tool.name === name) ?? false;
74
+ const activeTool = deps.getActiveTools?.().includes(name) ?? false;
75
+ return {
76
+ active_tool: activeTool,
77
+ activation: hostRegistered && activeTool ? "current_session" : "unverified",
78
+ callable_now: hostRegistered && activeTool,
79
+ host_registered: hostRegistered,
80
+ };
81
+ }
47
82
  function registerRuntimeTool(cfg) {
48
83
  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);
84
+ if (runtimeToolFingerprints.get(cfg.name) !== fingerprint) {
85
+ deps.registerTool(ToolsLocal.createRuntimeToolDefinition(cfg, deps.exec));
86
+ runtimeTools.add(cfg.name);
87
+ runtimeToolFingerprints.set(cfg.name, fingerprint);
88
+ }
89
+ if (deps.getActiveTools && deps.setActiveTools) {
90
+ deps.setActiveTools([
91
+ ...new Set([...deps.getActiveTools(), cfg.name]),
92
+ ]);
93
+ }
94
+ return activationFor(cfg.name);
54
95
  }
55
96
  function isStartupActionableRegistryWarning(warning) {
56
97
  if (warning.includes(" shadows "))
@@ -80,13 +121,18 @@ export function createAutoToolsRuntime(deps) {
80
121
  lines.push("Other registry diagnostics:");
81
122
  lines.push(...other.map((warning) => `• ${warning}`));
82
123
  }
124
+ lines.push("Next: inspect target=recipes view=doctor");
83
125
  return `${lines.join("\n")}\n`;
84
126
  }
85
- function loadTools(ctx) {
127
+ function loadTools(ctx, resolutionContext) {
86
128
  const warnings = [];
87
- const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
88
129
  const discovered = RecipesDiscovery.discoverRecipeSources([
89
- { root: recipeRoot, defaultTool: true, mutableUsage: true },
130
+ {
131
+ root: recipeRoot,
132
+ defaultTool: true,
133
+ mutableUsage: true,
134
+ resolutionContext,
135
+ },
90
136
  ]);
91
137
  warnings.push(...discovered.diagnostics);
92
138
  tools.clear();
@@ -109,17 +155,65 @@ export function createAutoToolsRuntime(deps) {
109
155
  }
110
156
  registerRuntimeTool(cfg);
111
157
  }
158
+ status.active_tool_count = tools.size;
159
+ status.last_scan_at = new Date().toISOString();
160
+ status.last_scan_counts = {
161
+ active: tools.size,
162
+ rejected: discovered.entries.filter((entry) => entry.invalid).length,
163
+ scanned: discovered.entries.length,
164
+ };
165
+ status.registry_generation += 1;
166
+ status.resolution_generation = resolutionContext?.generation;
112
167
  const startupWarnings = warnings.filter(isStartupActionableRegistryWarning);
113
168
  if (startupWarnings.length > 0) {
114
169
  notify(ctx, formatRecipeToolWarnings(startupWarnings), "warning");
115
170
  }
116
171
  }
117
172
  return {
173
+ getStatus: () => ({
174
+ ...status,
175
+ last_scan_counts: { ...status.last_scan_counts },
176
+ }),
177
+ getToolStatus: (name) => {
178
+ const cfg = tools.get(name);
179
+ if (!cfg)
180
+ return undefined;
181
+ const usage = cfg.sourcePath
182
+ ? RecipesUsage.readRecipeUsage(cfg.sourcePath)
183
+ : undefined;
184
+ const activation = activationFor(name);
185
+ const args = RecipesDiscovery.summarizeRegisteredToolArgs(cfg);
186
+ return {
187
+ ...activation,
188
+ activation_boundary: activation.callable_now
189
+ ? "current_session"
190
+ : !activation.host_registered
191
+ ? "host_registration"
192
+ : "active_tool_set",
193
+ persisted: Boolean(cfg.sourcePath),
194
+ registry_active: true,
195
+ source: registeredToolSource(cfg),
196
+ required_args: args.required,
197
+ optional_args: args.optional,
198
+ next_actions: activation.callable_now
199
+ ? [`call tool ${name}`]
200
+ : [
201
+ `register_tool name=${name} update=true`,
202
+ `inspect target=tool:${name} view=status`,
203
+ ],
204
+ ...(usage?.launch_kind ? { launch_kind: usage.launch_kind } : {}),
205
+ spawn_calls: Number(usage?.spawn_calls ?? 0),
206
+ tool_calls: Number(usage?.tool_calls ?? 0),
207
+ };
208
+ },
118
209
  getToolNameBlocker,
119
210
  getTools: () => tools,
120
211
  loadTools,
121
212
  notify,
122
213
  registerRuntimeTool,
214
+ setWatchStatus: (watchStatus) => {
215
+ status.watch_status = watchStatus;
216
+ },
123
217
  };
124
218
  }
125
219
  export function createRecipeToolReloadWatcher(runtime, deps = {}) {
@@ -129,6 +223,7 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
129
223
  let rootWatcher;
130
224
  let parentWatcher;
131
225
  let failureNotified = false;
226
+ const setWatchStatus = (watchStatus) => runtime.setWatchStatus?.(watchStatus);
132
227
  const close = () => {
133
228
  const closingRoot = rootWatcher;
134
229
  rootWatcher = undefined;
@@ -139,11 +234,13 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
139
234
  if (reloadTimeout)
140
235
  clearTimeout(reloadTimeout);
141
236
  reloadTimeout = undefined;
237
+ setWatchStatus("closed");
142
238
  };
143
239
  const notifyFailure = (ctx) => {
144
240
  if (failureNotified)
145
241
  return;
146
242
  failureNotified = true;
243
+ setWatchStatus("failed");
147
244
  ctx.ui.notify("Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.", "warning");
148
245
  };
149
246
  const scheduleReload = (ctx) => {
@@ -151,7 +248,7 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
151
248
  if (reloadTimeout)
152
249
  clearTimeout(reloadTimeout);
153
250
  reloadTimeout = setTimeout(() => {
154
- runtime.loadTools(ctx);
251
+ runtime.loadTools(ctx, deps.getResolutionContext?.());
155
252
  ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
156
253
  }, 150);
157
254
  reloadTimeout.unref?.();
@@ -181,6 +278,7 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
181
278
  scheduleReload(ctx);
182
279
  });
183
280
  parentWatcher = watcher;
281
+ setWatchStatus("watching_parent");
184
282
  watcher.on("error", () => {
185
283
  if (parentWatcher !== watcher)
186
284
  return;
@@ -214,6 +312,7 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
214
312
  scheduleReload(ctx);
215
313
  });
216
314
  rootWatcher = watcher;
315
+ setWatchStatus("watching_root");
217
316
  watcher.on("error", () => {
218
317
  if (rootWatcher !== watcher)
219
318
  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;
@@ -3,8 +3,8 @@
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 { statSync } from "node:fs";
7
- import { join } from "node:path";
6
+ import { existsSync, statSync } from "node:fs";
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,7 +157,114 @@ 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 inspectRecipes(view, deps, context) {
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
+ }
169
+ function portableSkillRecipeDiagnosticReason(diagnostic, namespaces) {
170
+ let reason = diagnostic.reason;
171
+ if (diagnostic.file) {
172
+ reason = reason.replaceAll(diagnostic.file, portableSkillRecipePath(diagnostic.file, diagnostic.skill, namespaces));
173
+ }
174
+ for (const root of namespaces[diagnostic.skill] ?? []) {
175
+ reason = reason.replaceAll(root, `<active-skill:${diagnostic.skill}>`);
176
+ }
177
+ return reason;
178
+ }
179
+ function inspectRecipeIdentity(identity, resolutionContext, namespaces, inventory, registryStatus) {
180
+ const match = /^([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\/([a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/u.exec(identity);
181
+ const skill = match?.[1] ?? "";
182
+ const roots = skill ? namespaces[skill] ?? [] : [];
183
+ const skillActive = roots.length === 1;
184
+ const matchingComponent = inventory.components.find((component) => component.identity === identity);
185
+ const matchingRejection = inventory.rejected.find((diagnostic) => diagnostic.skill === skill &&
186
+ (diagnostic.stem === undefined || diagnostic.stem === match?.[2]));
187
+ let sourceLocation;
188
+ let rejectedReason;
189
+ let resolvable = false;
190
+ if (!match) {
191
+ rejectedReason =
192
+ "Identity must use canonical <skill>/<recipe> syntax.";
193
+ }
194
+ else if (roots.length === 0) {
195
+ rejectedReason = `Active Skill Recipe not found: ${identity}`;
196
+ }
197
+ else if (roots.length > 1) {
198
+ rejectedReason = matchingRejection
199
+ ? portableSkillRecipeDiagnosticReason(matchingRejection, namespaces)
200
+ : `Duplicate active Skill identity ${skill}`;
201
+ }
202
+ else if (matchingRejection) {
203
+ rejectedReason = portableSkillRecipeDiagnosticReason(matchingRejection, namespaces);
204
+ if (matchingRejection.file) {
205
+ sourceLocation = portableSkillRecipePath(matchingRejection.file, skill, namespaces);
206
+ }
207
+ }
208
+ else {
209
+ try {
210
+ const file = RecipesReferences.resolveRecipeReferencePath(identity, resolutionContext?.cwd, resolutionContext?.activeSkills);
211
+ if (!file || !existsSync(file)) {
212
+ rejectedReason = `Skill Recipe component not found: ${identity}`;
213
+ }
214
+ else {
215
+ sourceLocation = portableSkillRecipePath(file, skill, namespaces);
216
+ const recipe = RecipesReferences.readResolvedRecipeConfig(file, [], {
217
+ skillContext: resolutionContext?.activeSkills,
218
+ });
219
+ if (recipe)
220
+ resolvable = true;
221
+ else {
222
+ rejectedReason =
223
+ RecipesReferences.diagnoseRawRecipeConfigFailure(file) ??
224
+ `Recipe could not be resolved: ${identity}`;
225
+ }
226
+ }
227
+ }
228
+ catch (error) {
229
+ rejectedReason = error instanceof Error ? error.message : String(error);
230
+ }
231
+ }
232
+ const componentStatus = resolvable
233
+ ? "available"
234
+ : !match
235
+ ? "invalid_identity"
236
+ : roots.length === 0
237
+ ? "skill_inactive"
238
+ : roots.length > 1
239
+ ? "ambiguous_skill"
240
+ : matchingRejection
241
+ ? "rejected"
242
+ : matchingComponent
243
+ ? "unresolvable"
244
+ : "missing";
245
+ return {
246
+ identity,
247
+ skill_active: skillActive,
248
+ resolvable,
249
+ catalog_partial: inventory.partial,
250
+ component_status: componentStatus,
251
+ ...(sourceLocation ? { source_location: sourceLocation } : {}),
252
+ resolution_generation: resolutionContext?.generation ?? registryStatus?.resolution_generation ?? "unavailable",
253
+ ...(rejectedReason ? { rejected_reason: rejectedReason } : {}),
254
+ next_actions: resolvable
255
+ ? [
256
+ `spawn recipe=${identity}`,
257
+ `register_tool name=<tool-name> from=${identity}`,
258
+ ]
259
+ : roots.length === 0 && skill
260
+ ? [
261
+ `activate Skill ${skill}`,
262
+ `inspect target=recipes view=doctor identity=${identity}`,
263
+ ]
264
+ : ["inspect target=recipes view=imports verbose=true"],
265
+ };
266
+ }
267
+ function inspectRecipes(view, input, deps, context) {
161
268
  if (view !== "status" &&
162
269
  view !== "summary" &&
163
270
  view !== "doctor" &&
@@ -172,30 +279,47 @@ function inspectRecipes(view, deps, context) {
172
279
  const discovered = RecipesDiscovery.discoverRecipeSources([
173
280
  { root: recipeRoot, defaultTool: true, mutableUsage: true },
174
281
  ]);
175
- const skillContext = context && typeof context === "object" && "activeSkillRecipeContext" in context
176
- ? context.activeSkillRecipeContext
282
+ const resolutionContext = context && typeof context === "object" && "recipeResolutionContext" in context
283
+ ? context.recipeResolutionContext
177
284
  : undefined;
178
- const activeSkillContext = skillContext ?? RecipesReferences.EMPTY_ACTIVE_SKILL_RECIPE_CONTEXT;
285
+ const activeSkillContext = resolutionContext?.activeSkills ??
286
+ RecipesReferences.EMPTY_ACTIVE_SKILL_RECIPE_CONTEXT;
179
287
  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
- }));
288
+ const skillInventory = RecipesReferences.inventoryActiveSkillRecipeComponents(activeSkillContext);
289
+ const registryStatus = deps.registryStatus?.();
290
+ const identity = typeof input.identity === "string" ? input.identity.trim() : "";
291
+ if (identity && view !== "doctor") {
292
+ throw new Error("inspect recipes identity is supported only with view=doctor.");
190
293
  }
191
- catch (error) {
192
- skillRecipeComponentDiagnostics.push({
193
- error: error instanceof Error ? error.message : String(error),
194
- });
294
+ if (view === "doctor" && identity) {
295
+ return inspectRecipeIdentity(identity, resolutionContext, skillRecipeNamespaces, skillInventory, registryStatus);
195
296
  }
297
+ const skillRecipeComponents = skillInventory.components.map((component) => ({
298
+ identity: component.identity,
299
+ source_kind: "active_skill_component",
300
+ skill: component.skill,
301
+ stem: component.stem,
302
+ imports: component.imports,
303
+ }));
304
+ const skillRecipeComponentDiagnostics = skillInventory.rejected.map((diagnostic) => ({
305
+ error: portableSkillRecipeDiagnosticReason(diagnostic, skillRecipeNamespaces),
306
+ ...(diagnostic.file
307
+ ? {
308
+ file: portableSkillRecipePath(diagnostic.file, diagnostic.skill, skillRecipeNamespaces),
309
+ }
310
+ : {}),
311
+ skill: diagnostic.skill,
312
+ ...(diagnostic.stem ? { stem: diagnostic.stem } : {}),
313
+ }));
196
314
  const discoverySummary = RecipesDiscovery.summarizeDiscovery(discovered);
197
315
  const summary = {
198
316
  ...discoverySummary,
317
+ ...(registryStatus
318
+ ? {
319
+ ...registryStatus,
320
+ watched_root: "~/.pi/agent/recipes",
321
+ }
322
+ : {}),
199
323
  active: Array.isArray(discoverySummary.active)
200
324
  ? discoverySummary.active.map((entry) => ({
201
325
  ...entry,
@@ -204,6 +328,7 @@ function inspectRecipes(view, deps, context) {
204
328
  : discoverySummary.active,
205
329
  skill_recipe_components: skillRecipeComponents,
206
330
  skill_recipe_component_diagnostics: skillRecipeComponentDiagnostics,
331
+ skill_recipe_catalog_partial: skillInventory.partial,
207
332
  active_skill_recipe_identities: Object.keys(skillRecipeNamespaces).sort(),
208
333
  skill_recipe_namespace_diagnostics: Object.entries(skillRecipeNamespaces)
209
334
  .filter(([, roots]) => roots.length > 1)
@@ -303,20 +428,58 @@ function inspectTool(name, view, deps) {
303
428
  throw new Error("inspect tool:<name> supports view=status or view=schema.");
304
429
  }
305
430
  const tool = deps.getTool?.(name);
306
- if (!tool)
307
- throw new Error(`registered tool not found: ${name}`);
431
+ const status = deps.getToolStatus?.(name);
432
+ if (!tool && !status) {
433
+ throw new Error(`Registered tool not found: ${name}. Next: inspect target=recipes view=doctor; do not substitute spawn for tool invocation.`);
434
+ }
435
+ if (view === "schema" && !tool) {
436
+ throw new Error(`Tool "${name}" is registered but its callable schema is unavailable; callable_now=${String(status?.callable_now ?? false)}, activation_boundary=${String(status?.activation_boundary ?? "host_registration")}. Next: inspect target=tool:${name} view=status.`);
437
+ }
308
438
  return {
309
439
  name,
310
- description: tool.description,
311
- parameters: tool.parameters,
312
- promptSnippet: tool.promptSnippet,
440
+ ...(view === "status" ? status : {}),
441
+ ...(tool
442
+ ? {
443
+ description: tool.description,
444
+ parameters: tool.parameters,
445
+ promptSnippet: tool.promptSnippet,
446
+ }
447
+ : {}),
313
448
  };
314
449
  }
315
450
  function compactResult(target, view, details) {
316
451
  if (target === "runtime")
317
452
  return compactRuntime(view, details);
453
+ if (target === "recipes" && typeof details.identity === "string") {
454
+ const lines = [
455
+ `recipes doctor identity=${details.identity} skill_active=${String(details.skill_active)} resolvable=${String(details.resolvable)} catalog_partial=${String(details.catalog_partial)} component_status=${String(details.component_status)}`,
456
+ ...(details.source_location
457
+ ? [`source=${String(details.source_location)}`]
458
+ : []),
459
+ `resolution_generation=${String(details.resolution_generation)}`,
460
+ ...(details.rejected_reason
461
+ ? [`rejected_reason=${String(details.rejected_reason)}`]
462
+ : []),
463
+ ...(details.next_actions ?? []).map((action) => `next=${String(action)}`),
464
+ ];
465
+ return `\n${lines.join("\n")}`;
466
+ }
318
467
  if (target === "recipes")
319
468
  return ToolsResponse.compactRecipeRegistry(details);
469
+ if (target.startsWith("tool:") && view === "status") {
470
+ const required = Array.isArray(details.required_args)
471
+ ? details.required_args.map(String).join(",") || "none"
472
+ : "unknown";
473
+ const optional = Array.isArray(details.optional_args)
474
+ ? details.optional_args.map(String).join(",") || "none"
475
+ : "unknown";
476
+ const next = Array.isArray(details.next_actions) && details.next_actions.length > 0
477
+ ? String(details.next_actions[0])
478
+ : details.callable_now === true
479
+ ? `call tool ${target.slice(5)}`
480
+ : `register_tool name=${target.slice(5)} update=true`;
481
+ return `\ntool=${target.slice(5)} source=${String(details.source ?? "unknown")} callable_now=${String(details.callable_now ?? false)} activation_boundary=${String(details.activation_boundary ?? "unknown")} required=${required} optional=${optional} launch_kind=${String(details.launch_kind ?? "none")} spawn_calls=${Number(details.spawn_calls ?? 0)} tool_calls=${Number(details.tool_calls ?? 0)} next=${next}`;
482
+ }
320
483
  if (target.startsWith("tool:"))
321
484
  return `\ntool=${target.slice(5)} view=${view}`;
322
485
  return `\nrun=${target.slice(4)} view=${view}`;
@@ -327,6 +490,7 @@ export function createInspectToolDefinition(deps = {}) {
327
490
  label: "Inspect",
328
491
  description: "Inspect a Run's Recipe, Trace, or Control evidence, or runtime/recipe/tool diagnostics.",
329
492
  parameters: Schema.objectSchema({
493
+ identity: Schema.stringSchema("Optional canonical <skill>/<recipe> identity for focused Recipe doctor diagnosis."),
330
494
  lines: Schema.stringSchema("Bounded item count for Trace or recent Controls."),
331
495
  source: Schema.stringSchema("Optional Trace source: all, lifecycle, control, process, agent, artifact, or runtime."),
332
496
  status: Schema.stringSchema("Optional runtime Run status filter."),
@@ -343,7 +507,7 @@ export function createInspectToolDefinition(deps = {}) {
343
507
  details = inspectRuntime(view, input, ctx, deps);
344
508
  }
345
509
  else if (target === "recipes") {
346
- details = inspectRecipes(view, deps, ctx);
510
+ details = inspectRecipes(view, input, deps, ctx);
347
511
  }
348
512
  else {
349
513
  const run = parseRunTarget(target);
@@ -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
  {
@@ -24,7 +24,9 @@ export function createRegisterToolDefinition(deps) {
24
24
  args: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.args),
25
25
  async: booleanSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.async),
26
26
  description: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.description),
27
+ defaults: looseObjectSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.defaults),
27
28
  draft: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.draft),
29
+ from: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.from),
28
30
  name: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.name),
29
31
  template: unionSchema([
30
32
  stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
@@ -33,7 +35,6 @@ export function createRegisterToolDefinition(deps) {
33
35
  nullSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.templateNull),
34
36
  ]),
35
37
  update: booleanSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.update),
36
- values: looseObjectSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.values),
37
38
  }, []),
38
39
  execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => Registry.executeRegisterTool(params, ctx, deps),
39
40
  };
@@ -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)}`);
@@ -202,6 +204,9 @@ export function recipeRegistryNextActions(summary, view) {
202
204
  if (view === "doctor" && typeof topAction.action === "string") {
203
205
  actions.push(String(topAction.action));
204
206
  }
207
+ if (summary.skill_recipe_catalog_partial === true) {
208
+ actions.push("inspect target=recipes view=imports verbose=true");
209
+ }
205
210
  if (drafts.length > 0) {
206
211
  actions.push("inspect target=recipes view=summary verbose=true");
207
212
  const firstPath = typeof drafts[0]?.path === "string" ? drafts[0].path : undefined;
@@ -226,12 +231,13 @@ export function compactRecipeRegistry(summary) {
226
231
  const recommendations = Array.isArray(summary.recommendations)
227
232
  ? summary.recommendations.length
228
233
  : 0;
234
+ const skillCatalogPartial = summary.skill_recipe_catalog_partial === true;
229
235
  const currentPolicy = Array.isArray(summary.active)
230
236
  ? summary.active.filter((entry) => entry.current_policy).length
231
237
  : 0;
232
238
  const nextActions = Array.isArray(summary.next_actions)
233
239
  ? summary.next_actions
234
240
  : [];
235
- return `\nrecipes active=${active} drafts=${drafts} shadowed=${shadowed} invalid=${invalid} disabled=${disabled} current_policy=${currentPolicy} recommendations=${recommendations} diagnostics=${diagnostics}${compactNextActions(nextActions)}`;
241
+ return `\nrecipes active=${active} drafts=${drafts} shadowed=${shadowed} invalid=${invalid} disabled=${disabled} current_policy=${currentPolicy} skill_catalog_partial=${skillCatalogPartial} recommendations=${recommendations} diagnostics=${diagnostics}${compactNextActions(nextActions)}`;
236
242
  }
237
243
  export const DEFAULT_INSPECT_LINES = Limits.DEFAULT_INSPECT_LINES;
@@ -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
  }
@@ -12,6 +12,7 @@ import { dirname } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
 
14
14
  const conformanceSuites = [
15
+ "tests/agent-journeys.test.ts",
15
16
  "tests/control.test.ts",
16
17
  "tests/runs-controls.test.ts",
17
18
  "tests/runs-trace.test.ts",