@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
package/lib/registry.ts CHANGED
@@ -10,9 +10,15 @@ import { dirname, join, relative, resolve } from "node:path";
10
10
  import * as CommandTemplates from "./command-templates.ts";
11
11
  import * as Config from "./config.ts";
12
12
  import * as ExecutionOutput from "./execution-output.ts";
13
- import { withFileMutationLock, writeJsonAtomic } from "./file-state.ts";
13
+ import {
14
+ withFileMutationLock,
15
+ writeJsonAtomic,
16
+ writeTextAtomic,
17
+ } from "./file-state.ts";
14
18
  import * as Identity from "./identity.ts";
15
19
  import * as Paths from "./paths.ts";
20
+ import * as RecipesContext from "./recipes-context.ts";
21
+ import * as RecipesDiscovery from "./recipes-discovery.ts";
16
22
  import * as RecipesReferences from "./recipes-references.ts";
17
23
  import * as Schema from "./schema.ts";
18
24
 
@@ -28,16 +34,24 @@ export interface RegisterToolInput {
28
34
  }
29
35
 
30
36
  export interface RegisterToolResultDetails {
37
+ active_tool?: boolean;
38
+ activation?: "current_session" | "unverified";
31
39
  args?: string[];
32
40
  async?: boolean;
41
+ callable_now?: boolean;
33
42
  config?: string;
34
43
  defaults?: Record<string, string>;
35
44
  draft?: string;
45
+ host_registered?: boolean;
46
+ persisted?: boolean;
36
47
  promoted?: boolean;
48
+ registry_active?: boolean;
49
+ resolved?: boolean;
37
50
  recipeName?: string;
38
51
  template?: CommandTemplates.CommandTemplateValue;
39
52
  templateWarnings?: string[];
40
53
  tool: string;
54
+ validated?: boolean;
41
55
  }
42
56
 
43
57
  export interface RegisterToolResult {
@@ -56,7 +70,14 @@ export interface RegisterToolRuntimeDeps<TContext> {
56
70
  message: string,
57
71
  type: "info" | "warning" | "error",
58
72
  ) => void;
59
- registerRuntimeTool: (cfg: Config.RegisteredTool) => void;
73
+ registerRuntimeTool: (cfg: Config.RegisteredTool) =>
74
+ | {
75
+ active_tool: boolean;
76
+ activation: "current_session" | "unverified";
77
+ callable_now: boolean;
78
+ host_registered: boolean;
79
+ }
80
+ | void;
60
81
  reservedToolNames: Set<string>;
61
82
  setActiveTools: (toolNames: string[]) => void;
62
83
  }
@@ -139,8 +160,8 @@ function promoteDraftRecipe<TContext>(
139
160
  ),
140
161
  );
141
162
  }
142
- const config = RecipesReferences.readResolvedRecipeConfig(draftPath);
143
- if (!config) {
163
+ const raw = RecipesReferences.readRawRecipeConfig(draftPath);
164
+ if (!raw) {
144
165
  const reason = RecipesReferences.diagnoseRawRecipeConfigFailure(draftPath);
145
166
  throw new Error(
146
167
  ExecutionOutput.formatToolText(
@@ -148,20 +169,35 @@ function promoteDraftRecipe<TContext>(
148
169
  ),
149
170
  );
150
171
  }
151
- const promoted = buildConfig(
152
- name,
172
+ const authoredRecipe = buildAuthoredRecipe(
153
173
  {
154
174
  ...input,
155
- description: input.description ?? config.description,
156
- template: draftPath,
175
+ description:
176
+ input.description ??
177
+ (typeof raw.description === "string" ? raw.description : undefined),
157
178
  },
158
179
  existing,
180
+ raw,
181
+ );
182
+ const resolutionContext = getRegistrationResolutionContext(ctx, deps);
183
+ const candidate = RecipesDiscovery.admitUserRecipe(
184
+ targetPath,
185
+ resolutionContext,
186
+ authoredRecipe,
159
187
  );
160
- const raw = RecipesReferences.readRawRecipeConfig(draftPath)!;
188
+ if (!candidate.validated || !candidate.tool) {
189
+ throw new Error(
190
+ ExecutionOutput.formatToolText(
191
+ `Draft recipe is invalid: ${candidate.diagnostics.join("; ")}`,
192
+ ),
193
+ );
194
+ }
161
195
  mkdirSync(dirname(targetPath), { recursive: true });
162
- writeJsonAtomic(targetPath, raw);
163
- promoted.template = targetPath;
164
- promoted.sourcePath = targetPath;
196
+ writeJsonAtomic(targetPath, authoredRecipe);
197
+ const promoted = RecipesDiscovery.admitUserRecipe(
198
+ targetPath,
199
+ resolutionContext,
200
+ ).tool!;
165
201
  tools.set(name, promoted);
166
202
  deps.registerRuntimeTool(promoted);
167
203
  deps.notify(ctx, `Promoted draft recipe: ${name}`, "info");
@@ -185,18 +221,12 @@ function promoteDraftRecipe<TContext>(
185
221
 
186
222
  function persistToolRecipe<TContext>(
187
223
  deps: RegisterToolRuntimeDeps<TContext>,
188
- cfg: Config.RegisteredTool,
224
+ name: string,
225
+ recipe: Record<string, unknown>,
189
226
  ): string {
190
- const path = getToolRecipePath(deps, cfg.name);
227
+ const path = getToolRecipePath(deps, name);
191
228
  mkdirSync(dirname(path), { recursive: true });
192
- writeJsonAtomic(path, {
193
- description: cfg.description,
194
- ...(cfg.recipe?.async !== undefined ? { async: cfg.recipe.async } : {}),
195
- ...(cfg.storedArgs ? { args: cfg.storedArgs } : {}),
196
- ...(cfg.storedDefaults ? { defaults: cfg.storedDefaults } : {}),
197
- ...(cfg.recipe?.values ? { values: cfg.recipe.values } : {}),
198
- template: cfg.template,
199
- });
229
+ writeJsonAtomic(path, recipe);
200
230
  return path;
201
231
  }
202
232
 
@@ -238,18 +268,27 @@ function deleteTool<TContext>(
238
268
  function readAuthoritativeStoredTool<TContext>(
239
269
  deps: RegisterToolRuntimeDeps<TContext>,
240
270
  name: string,
241
- ): { exists: boolean; tool?: Config.RegisteredTool } {
271
+ ): {
272
+ bytes?: string;
273
+ exists: boolean;
274
+ recipe?: Record<string, unknown>;
275
+ tool?: Config.RegisteredTool;
276
+ } {
242
277
  const path = getToolRecipePath(deps, name);
243
278
  if (!existsSync(path)) return { exists: false };
244
279
  try {
280
+ const bytes = readFileSync(path, "utf8");
281
+ const recipe = JSON.parse(bytes);
245
282
  const normalized = Config.normalizeStoredTool(
246
283
  name,
247
- JSON.parse(readFileSync(path, "utf8")),
284
+ recipe,
248
285
  deps.reservedToolNames,
249
286
  );
250
- if (!normalized.cfg) return { exists: true };
287
+ if (!normalized.cfg) return { bytes, exists: true, recipe };
251
288
  return {
289
+ bytes,
252
290
  exists: true,
291
+ recipe,
253
292
  tool: { ...normalized.cfg, sourcePath: path },
254
293
  };
255
294
  } catch {
@@ -285,11 +324,31 @@ function getInputTemplate(
285
324
  );
286
325
  }
287
326
 
288
- function buildConfig(
289
- name: string,
327
+ function getRegistrationResolutionContext<TContext>(
328
+ ctx: TContext,
329
+ deps: RegisterToolRuntimeDeps<TContext>,
330
+ ): RecipesContext.RecipeResolutionContext {
331
+ if (
332
+ ctx &&
333
+ typeof ctx === "object" &&
334
+ "recipeResolutionContext" in ctx
335
+ ) {
336
+ const resolutionContext = (ctx as {
337
+ recipeResolutionContext?: RecipesContext.RecipeResolutionContext;
338
+ }).recipeResolutionContext;
339
+ if (resolutionContext) return resolutionContext;
340
+ }
341
+ return RecipesContext.createEmptyRecipeResolutionContext(
342
+ "offline-register-tool",
343
+ getRecipeRoot(deps),
344
+ );
345
+ }
346
+
347
+ function buildAuthoredRecipe(
290
348
  input: RegisterToolInput,
291
349
  existing: Config.RegisteredTool | undefined,
292
- ): Config.RegisteredTool {
350
+ existingRecipe: Record<string, unknown> | undefined,
351
+ ): Record<string, unknown> {
293
352
  const explicitArgs =
294
353
  input.args === undefined
295
354
  ? undefined
@@ -311,68 +370,29 @@ function buildConfig(
311
370
  );
312
371
  }
313
372
  const finalTemplate =
314
- template === undefined || template === "" ? existing?.template : template;
373
+ template === undefined || template === ""
374
+ ? existingRecipe?.template ?? existing?.template
375
+ : template;
315
376
  if (!finalTemplate) {
316
377
  throw new Error(
317
378
  ExecutionOutput.formatToolText("Tool template is required."),
318
379
  );
319
380
  }
320
- const inputRecipe = typeof input.async === "boolean" ? name : undefined;
321
- const recipe: RecipesReferences.TemplateRecipeConfig | undefined = inputRecipe
322
- ? {
323
- name: inputRecipe,
324
- ...(typeof input.async === "boolean" ? { async: input.async } : {}),
325
- template: finalTemplate,
326
- ...(input.values && typeof input.values === "object"
327
- ? { values: input.values }
328
- : {}),
329
- }
330
- : template === undefined
331
- ? existing?.recipe
332
- : undefined;
333
- const defaults = explicitArgs?.defaults ?? existing?.storedDefaults ?? {};
334
- const storedArgs = explicitArgs
335
- ? explicitArgs.declarations
336
- : existing?.storedArgs;
337
- const storedDefaults =
338
- Object.keys(defaults).length > 0 ? defaults : undefined;
339
- const recipeTemplate = RecipesReferences.getRecipeTemplate(finalTemplate);
340
- const argTemplate = recipeTemplate ?? finalTemplate;
341
- const argTemplateConfig: CommandTemplates.CommandTemplateConfig =
342
- typeof argTemplate === "object" && !Array.isArray(argTemplate)
343
- ? {
344
- ...argTemplate,
345
- ...(storedArgs !== undefined ? { args: storedArgs } : {}),
346
- defaults: { ...(argTemplate.defaults ?? {}), ...defaults },
347
- }
348
- : {
349
- args: storedArgs,
350
- defaults,
351
- template: argTemplate,
352
- };
353
- const inferredArgTypes = Schema.getTemplateArgTypes(argTemplateConfig);
354
- const argTypes = {
355
- ...inferredArgTypes,
356
- ...(existing?.argTypes ?? {}),
357
- ...(explicitArgs?.argTypes ?? {}),
358
- };
359
- return {
360
- name,
381
+ const authored: Record<string, unknown> = {
382
+ ...(existingRecipe ?? {}),
361
383
  description,
362
384
  template: finalTemplate,
363
- ...(recipe ? { recipe } : {}),
364
- args:
365
- RecipesReferences.isRecipeTool(finalTemplate, recipe) &&
366
- storedArgs !== undefined
367
- ? Schema.getExplicitToolArgNames(storedArgs)
368
- : RecipesReferences.isRecipeReference(finalTemplate) && !recipeTemplate
369
- ? Schema.getExplicitToolArgNames(storedArgs)
370
- : Schema.getToolArgNames(argTemplateConfig),
371
- defaults,
372
- ...(Object.keys(argTypes).length > 0 ? { argTypes } : {}),
373
- ...(storedArgs !== undefined ? { storedArgs } : {}),
374
- ...(storedDefaults !== undefined ? { storedDefaults } : {}),
375
385
  };
386
+ if (typeof input.async === "boolean") authored.async = input.async;
387
+ if (explicitArgs) {
388
+ authored.args = explicitArgs.declarations;
389
+ if (Object.keys(explicitArgs.defaults).length > 0)
390
+ authored.defaults = explicitArgs.defaults;
391
+ else delete authored.defaults;
392
+ }
393
+ if (input.values && typeof input.values === "object")
394
+ authored.values = input.values;
395
+ return authored;
376
396
  }
377
397
 
378
398
  function executeRegisterToolUnlocked<TContext>(
@@ -416,47 +436,118 @@ function executeRegisterToolUnlocked<TContext>(
416
436
  ),
417
437
  );
418
438
  }
419
- const cfg = buildConfig(name, input, existing);
420
- let recipePath: string;
439
+ const recipePath = getToolRecipePath(deps, name);
440
+ const authoredRecipe = buildAuthoredRecipe(
441
+ input,
442
+ existing,
443
+ authoritative.recipe,
444
+ );
445
+ const resolutionContext = getRegistrationResolutionContext(ctx, deps);
446
+ const candidate = RecipesDiscovery.admitUserRecipe(
447
+ recipePath,
448
+ resolutionContext,
449
+ authoredRecipe,
450
+ );
451
+ if (!candidate.validated || !candidate.tool) {
452
+ throw new Error(
453
+ ExecutionOutput.formatToolText(
454
+ `Tool recipe validation failed: ${candidate.diagnostics.join("; ")}`,
455
+ ),
456
+ );
457
+ }
458
+ const activeBefore = deps.getActiveTools();
459
+ let persisted = false;
460
+ let activation:
461
+ | {
462
+ active_tool: boolean;
463
+ activation: "current_session" | "unverified";
464
+ callable_now: boolean;
465
+ host_registered: boolean;
466
+ }
467
+ | undefined;
468
+ let cfg: Config.RegisteredTool;
421
469
  try {
422
- recipePath = persistToolRecipe(deps, cfg);
470
+ persistToolRecipe(deps, name, authoredRecipe);
471
+ persisted = true;
472
+ const admitted = RecipesDiscovery.admitUserRecipe(
473
+ recipePath,
474
+ resolutionContext,
475
+ );
476
+ if (!admitted.validated || !admitted.tool) {
477
+ throw new Error(
478
+ `Persisted tool recipe admission failed: ${admitted.diagnostics.join("; ")}`,
479
+ );
480
+ }
481
+ cfg = admitted.tool;
482
+ tools.set(name, cfg);
483
+ activation = deps.registerRuntimeTool(cfg) ?? undefined;
484
+ if (
485
+ activation &&
486
+ (!activation.host_registered ||
487
+ !activation.active_tool ||
488
+ !activation.callable_now)
489
+ ) {
490
+ throw new Error(
491
+ `Host activation verification failed: ${JSON.stringify(activation)}`,
492
+ );
493
+ }
423
494
  } catch (error) {
495
+ if (persisted) {
496
+ if (authoritative.bytes !== undefined)
497
+ writeTextAtomic(recipePath, authoritative.bytes);
498
+ else if (existsSync(recipePath)) unlinkSync(recipePath);
499
+ }
500
+ if (existing) {
501
+ tools.set(name, existing);
502
+ deps.registerRuntimeTool(existing);
503
+ } else {
504
+ tools.delete(name);
505
+ }
506
+ deps.setActiveTools(activeBefore);
424
507
  throw new Error(
425
508
  ExecutionOutput.formatToolText(
426
- `Failed to persist tool recipe: ${error instanceof Error ? error.message : String(error)}`,
509
+ `Tool registration transaction failed: ${error instanceof Error ? error.message : String(error)}`,
427
510
  ),
428
511
  );
429
512
  }
430
- cfg.sourcePath = recipePath;
431
- tools.set(name, cfg);
432
- deps.registerRuntimeTool(cfg);
433
- deps.notify(ctx, `Tool persisted: ${name}`, "info");
513
+ deps.notify(ctx, `Tool activated: ${name}`, "info");
434
514
  const templateWarnings = CommandTemplates.getCommandTemplateWarnings(
435
- typeof cfg.template === "object" && !Array.isArray(cfg.template)
436
- ? cfg.template
437
- : { template: cfg.template! },
515
+ typeof cfg.recipe?.template === "object" && !Array.isArray(cfg.recipe.template)
516
+ ? cfg.recipe.template
517
+ : { template: cfg.recipe?.template ?? cfg.template! },
438
518
  );
439
519
  const warningText =
440
520
  templateWarnings.length > 0
441
521
  ? `\nWarnings:\n${templateWarnings.map((warning) => `- ${warning}`).join("\n")}`
442
522
  : "";
523
+ const outcomeText = activation?.callable_now
524
+ ? `${existing ? "Updated" : "Registered"} and activated tool "${name}"`
525
+ : `Persisted tool "${name}"; current-session activation is unverified`;
443
526
  return {
444
527
  content: [
445
528
  textContent(
446
529
  ExecutionOutput.formatToolText(
447
- `${existing ? "Updated" : "Registered"} tool "${name}" (args: ${Schema.formatToolArgs(cfg.args)}).${warningText}`,
530
+ `${outcomeText} (args: ${Schema.formatToolArgs(cfg.args)}).${warningText}`,
448
531
  ),
449
532
  ),
450
533
  ],
451
534
  details: {
535
+ active_tool: activation?.active_tool ?? false,
536
+ activation: activation?.activation ?? "unverified",
452
537
  args: cfg.args,
538
+ callable_now: activation?.callable_now ?? false,
453
539
  config: recipePath,
454
540
  defaults: cfg.defaults,
541
+ host_registered: activation?.host_registered ?? false,
542
+ persisted: true,
543
+ registry_active: tools.get(name) === cfg,
544
+ resolved: true,
455
545
  ...(cfg.recipe?.async !== undefined ? { async: cfg.recipe.async } : {}),
456
546
  ...(cfg.recipe?.name ? { recipeName: cfg.recipe.name } : {}),
457
547
  ...(cfg.template ? { template: cfg.template } : {}),
458
548
  ...(templateWarnings.length > 0 ? { templateWarnings } : {}),
459
549
  tool: name,
550
+ validated: true,
460
551
  },
461
552
  };
462
553
  }
package/lib/runtime.ts CHANGED
@@ -10,7 +10,9 @@ import { basename, dirname } from "node:path";
10
10
  import * as Config from "./config.ts";
11
11
  import type { RegisteredToolExec } from "./execution.ts";
12
12
  import * as Paths from "./paths.ts";
13
+ import type { RecipeResolutionContext } from "./recipes-context.ts";
13
14
  import * as RecipesDiscovery from "./recipes-discovery.ts";
15
+ import * as RecipesUsage from "./recipes-usage.ts";
14
16
  import * as ToolsLocal from "./tools-local.ts";
15
17
 
16
18
  export interface RuntimeContext {
@@ -20,11 +22,19 @@ export interface RuntimeContext {
20
22
  };
21
23
  }
22
24
 
25
+ export interface RuntimeToolActivation {
26
+ active_tool: boolean;
27
+ activation: "current_session" | "unverified";
28
+ callable_now: boolean;
29
+ host_registered: boolean;
30
+ }
31
+
23
32
  export interface ToolRegistryRuntimeDeps {
24
33
  configPath: string;
25
34
  exec: RegisteredToolExec;
26
35
  recipeRoot?: string;
27
36
  getActiveTools?: () => string[];
37
+ getAllTools?: () => Array<{ name: string }>;
28
38
  registerTool: (
29
39
  definition: ReturnType<typeof ToolsLocal.createRuntimeToolDefinition>,
30
40
  ) => void;
@@ -32,16 +42,32 @@ export interface ToolRegistryRuntimeDeps {
32
42
  setActiveTools?: (toolNames: string[]) => void;
33
43
  }
34
44
 
45
+ export interface RecipeRegistryStatus {
46
+ active_tool_count: number;
47
+ last_scan_counts: { active: number; rejected: number; scanned: number };
48
+ last_scan_at?: string;
49
+ registry_generation: number;
50
+ resolution_generation?: string;
51
+ watch_status: "closed" | "failed" | "watching_parent" | "watching_root";
52
+ watched_root: string;
53
+ }
54
+
35
55
  export interface ToolRegistryRuntime {
56
+ getStatus(): RecipeRegistryStatus;
57
+ getToolStatus(name: string): Record<string, unknown> | undefined;
36
58
  getToolNameBlocker(name: string): string | undefined;
37
59
  getTools(): Map<string, Config.RegisteredTool>;
38
- loadTools(ctx: RuntimeContext): void;
60
+ loadTools(
61
+ ctx: RuntimeContext,
62
+ resolutionContext?: RecipeResolutionContext,
63
+ ): void;
39
64
  notify(
40
65
  ctx: RuntimeContext,
41
66
  message: string,
42
67
  type: "info" | "warning" | "error",
43
68
  ): void;
44
- registerRuntimeTool(cfg: Config.RegisteredTool): void;
69
+ registerRuntimeTool(cfg: Config.RegisteredTool): RuntimeToolActivation;
70
+ setWatchStatus(status: RecipeRegistryStatus["watch_status"]): void;
45
71
  }
46
72
 
47
73
  export interface RecipeToolReloadWatcher {
@@ -55,6 +81,14 @@ export function createAutoToolsRuntime(
55
81
  const tools = new Map<string, Config.RegisteredTool>();
56
82
  const runtimeToolFingerprints = new Map<string, string>();
57
83
  const runtimeTools = new Set<string>();
84
+ const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
85
+ const status: RecipeRegistryStatus = {
86
+ active_tool_count: 0,
87
+ last_scan_counts: { active: 0, rejected: 0, scanned: 0 },
88
+ registry_generation: 0,
89
+ watch_status: "closed",
90
+ watched_root: recipeRoot,
91
+ };
58
92
  function notify(
59
93
  ctx: RuntimeContext,
60
94
  message: string,
@@ -90,12 +124,30 @@ export function createAutoToolsRuntime(
90
124
  deps.getActiveTools().filter((name) => !staleSet.has(name)),
91
125
  );
92
126
  }
93
- function registerRuntimeTool(cfg: Config.RegisteredTool) {
127
+ function activationFor(name: string): RuntimeToolActivation {
128
+ const hostRegistered = deps.getAllTools?.().some((tool) => tool.name === name) ?? false;
129
+ const activeTool = deps.getActiveTools?.().includes(name) ?? false;
130
+ return {
131
+ active_tool: activeTool,
132
+ activation:
133
+ hostRegistered && activeTool ? "current_session" : "unverified",
134
+ callable_now: hostRegistered && activeTool,
135
+ host_registered: hostRegistered,
136
+ };
137
+ }
138
+ function registerRuntimeTool(cfg: Config.RegisteredTool): RuntimeToolActivation {
94
139
  const fingerprint = getToolFingerprint(cfg);
95
- if (runtimeToolFingerprints.get(cfg.name) === fingerprint) return;
96
- deps.registerTool(ToolsLocal.createRuntimeToolDefinition(cfg, deps.exec));
97
- runtimeTools.add(cfg.name);
98
- runtimeToolFingerprints.set(cfg.name, fingerprint);
140
+ if (runtimeToolFingerprints.get(cfg.name) !== fingerprint) {
141
+ deps.registerTool(ToolsLocal.createRuntimeToolDefinition(cfg, deps.exec));
142
+ runtimeTools.add(cfg.name);
143
+ runtimeToolFingerprints.set(cfg.name, fingerprint);
144
+ }
145
+ if (deps.getActiveTools && deps.setActiveTools) {
146
+ deps.setActiveTools([
147
+ ...new Set([...deps.getActiveTools(), cfg.name]),
148
+ ]);
149
+ }
150
+ return activationFor(cfg.name);
99
151
  }
100
152
  function isStartupActionableRegistryWarning(warning: string): boolean {
101
153
  if (warning.includes(" shadows ")) return false;
@@ -137,11 +189,18 @@ export function createAutoToolsRuntime(
137
189
  }
138
190
  return `${lines.join("\n")}\n`;
139
191
  }
140
- function loadTools(ctx: RuntimeContext) {
192
+ function loadTools(
193
+ ctx: RuntimeContext,
194
+ resolutionContext?: RecipeResolutionContext,
195
+ ) {
141
196
  const warnings: string[] = [];
142
- const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
143
197
  const discovered = RecipesDiscovery.discoverRecipeSources([
144
- { root: recipeRoot, defaultTool: true, mutableUsage: true },
198
+ {
199
+ root: recipeRoot,
200
+ defaultTool: true,
201
+ mutableUsage: true,
202
+ resolutionContext,
203
+ },
145
204
  ]);
146
205
  warnings.push(...discovered.diagnostics);
147
206
  tools.clear();
@@ -164,28 +223,59 @@ export function createAutoToolsRuntime(
164
223
  }
165
224
  registerRuntimeTool(cfg);
166
225
  }
226
+ status.active_tool_count = tools.size;
227
+ status.last_scan_at = new Date().toISOString();
228
+ status.last_scan_counts = {
229
+ active: tools.size,
230
+ rejected: discovered.entries.filter((entry) => entry.invalid).length,
231
+ scanned: discovered.entries.length,
232
+ };
233
+ status.registry_generation += 1;
234
+ status.resolution_generation = resolutionContext?.generation;
167
235
  const startupWarnings = warnings.filter(isStartupActionableRegistryWarning);
168
236
  if (startupWarnings.length > 0) {
169
237
  notify(ctx, formatRecipeToolWarnings(startupWarnings), "warning");
170
238
  }
171
239
  }
172
240
  return {
241
+ getStatus: () => ({
242
+ ...status,
243
+ last_scan_counts: { ...status.last_scan_counts },
244
+ }),
245
+ getToolStatus: (name) => {
246
+ const cfg = tools.get(name);
247
+ if (!cfg) return undefined;
248
+ const usage = cfg.sourcePath
249
+ ? RecipesUsage.readRecipeUsage(cfg.sourcePath)
250
+ : undefined;
251
+ return {
252
+ ...activationFor(name),
253
+ launch_kind: usage?.launch_kind,
254
+ spawn_calls: Number(usage?.spawn_calls ?? 0),
255
+ tool_calls: Number(usage?.tool_calls ?? 0),
256
+ };
257
+ },
173
258
  getToolNameBlocker,
174
259
  getTools: () => tools,
175
260
  loadTools,
176
261
  notify,
177
262
  registerRuntimeTool,
263
+ setWatchStatus: (watchStatus) => {
264
+ status.watch_status = watchStatus;
265
+ },
178
266
  };
179
267
  }
180
268
 
181
269
  export interface RecipeToolReloadWatcherDeps {
182
270
  exists?: (path: string) => boolean;
271
+ getResolutionContext?: () => RecipeResolutionContext | undefined;
183
272
  recipeRoot?: string;
184
273
  watchPath?: typeof watch;
185
274
  }
186
275
 
187
276
  export function createRecipeToolReloadWatcher(
188
- runtime: Pick<ToolRegistryRuntime, "loadTools">,
277
+ runtime: Pick<ToolRegistryRuntime, "loadTools"> &
278
+ Partial<Pick<ToolRegistryRuntime, "setWatchStatus">>,
189
279
  deps: RecipeToolReloadWatcherDeps = {},
190
280
  ): RecipeToolReloadWatcher {
191
281
  const pathExists = deps.exists ?? existsSync;
@@ -194,6 +284,8 @@ export function createRecipeToolReloadWatcher(
194
284
  let rootWatcher: FSWatcher | undefined;
195
285
  let parentWatcher: FSWatcher | undefined;
196
286
  let failureNotified = false;
287
+ const setWatchStatus = (watchStatus: RecipeRegistryStatus["watch_status"]): void =>
288
+ runtime.setWatchStatus?.(watchStatus);
197
289
  const close = (): void => {
198
290
  const closingRoot = rootWatcher;
199
291
  rootWatcher = undefined;
@@ -203,10 +295,12 @@ export function createRecipeToolReloadWatcher(
203
295
  closingParent?.close();
204
296
  if (reloadTimeout) clearTimeout(reloadTimeout);
205
297
  reloadTimeout = undefined;
298
+ setWatchStatus("closed");
206
299
  };
207
300
  const notifyFailure = (ctx: RuntimeContext): void => {
208
301
  if (failureNotified) return;
209
302
  failureNotified = true;
303
+ setWatchStatus("failed");
210
304
  ctx.ui.notify(
211
305
  "Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.",
212
306
  "warning",
@@ -216,7 +310,7 @@ export function createRecipeToolReloadWatcher(
216
310
  failureNotified = false;
217
311
  if (reloadTimeout) clearTimeout(reloadTimeout);
218
312
  reloadTimeout = setTimeout(() => {
219
- runtime.loadTools(ctx);
313
+ runtime.loadTools(ctx, deps.getResolutionContext?.());
220
314
  ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
221
315
  }, 150);
222
316
  reloadTimeout.unref?.();
@@ -245,6 +339,7 @@ export function createRecipeToolReloadWatcher(
245
339
  scheduleReload(ctx);
246
340
  });
247
341
  parentWatcher = watcher;
342
+ setWatchStatus("watching_parent");
248
343
  watcher.on("error", () => {
249
344
  if (parentWatcher !== watcher) return;
250
345
  parentWatcher = undefined;
@@ -274,6 +369,7 @@ export function createRecipeToolReloadWatcher(
274
369
  scheduleReload(ctx);
275
370
  });
276
371
  rootWatcher = watcher;
372
+ setWatchStatus("watching_root");
277
373
  watcher.on("error", () => {
278
374
  if (rootWatcher !== watcher) return;
279
375
  rootWatcher = undefined;