@llblab/pi-actors 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/runtime.ts CHANGED
@@ -6,6 +6,9 @@
6
6
 
7
7
  import * as Config from "./config.ts";
8
8
  import type { RegisteredToolExec } from "./execution.ts";
9
+ import * as Paths from "./paths.ts";
10
+ import * as RecipeDiscovery from "./recipe-discovery.ts";
11
+ import * as RecipeMigration from "./recipe-migration.ts";
9
12
  import * as Tools from "./tools.ts";
10
13
 
11
14
  export interface RuntimeContext {
@@ -22,11 +25,15 @@ export interface ToolInfoLike {
22
25
  export interface ToolRegistryRuntimeDeps {
23
26
  configPath: string;
24
27
  exec: RegisteredToolExec;
28
+ packagedRecipeRoot?: string;
29
+ recipeRoot?: string;
30
+ getActiveTools?: () => string[];
25
31
  getAllTools: () => ToolInfoLike[];
26
32
  registerTool: (
27
33
  definition: ReturnType<typeof Tools.createRuntimeToolDefinition>,
28
34
  ) => void;
29
35
  reservedToolNames: Set<string>;
36
+ setActiveTools?: (toolNames: string[]) => void;
30
37
  }
31
38
 
32
39
  export interface ToolRegistryRuntime {
@@ -45,6 +52,7 @@ export function createAutoToolsRuntime(
45
52
  deps: ToolRegistryRuntimeDeps,
46
53
  ): ToolRegistryRuntime {
47
54
  const tools = new Map<string, Config.RegisteredTool>();
55
+ const runtimeToolFingerprints = new Map<string, string>();
48
56
  const runtimeTools = new Set<string>();
49
57
  function notify(
50
58
  ctx: RuntimeContext,
@@ -60,35 +68,77 @@ export function createAutoToolsRuntime(
60
68
  ? `Tool "${name}" is already registered outside pi-actors.`
61
69
  : undefined;
62
70
  }
71
+ function getToolFingerprint(cfg: Config.RegisteredTool): string {
72
+ return JSON.stringify({
73
+ args: cfg.args,
74
+ argTypes: cfg.argTypes,
75
+ defaults: cfg.defaults,
76
+ description: cfg.description,
77
+ recipe: cfg.recipe,
78
+ template: cfg.template,
79
+ });
80
+ }
81
+ function deactivateMissingRuntimeTools(activeNames: Set<string>): void {
82
+ const stale = [...runtimeTools].filter((name) => !activeNames.has(name));
83
+ if (stale.length === 0) return;
84
+ for (const name of stale) {
85
+ runtimeTools.delete(name);
86
+ runtimeToolFingerprints.delete(name);
87
+ }
88
+ if (!deps.getActiveTools || !deps.setActiveTools) return;
89
+ const staleSet = new Set(stale);
90
+ deps.setActiveTools(
91
+ deps.getActiveTools().filter((name) => !staleSet.has(name)),
92
+ );
93
+ }
63
94
  function registerRuntimeTool(cfg: Config.RegisteredTool) {
95
+ const fingerprint = getToolFingerprint(cfg);
96
+ if (runtimeToolFingerprints.get(cfg.name) === fingerprint) return;
64
97
  deps.registerTool(Tools.createRuntimeToolDefinition(cfg, deps.exec));
65
98
  runtimeTools.add(cfg.name);
99
+ runtimeToolFingerprints.set(cfg.name, fingerprint);
66
100
  }
67
101
  function loadTools(ctx: RuntimeContext) {
68
- const loaded = Config.loadToolConfig(
69
- deps.configPath,
70
- deps.reservedToolNames,
71
- );
102
+ const warnings: string[] = [];
103
+ const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
104
+ const packagedRecipeRoot = deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot();
105
+ const migration = RecipeMigration.migrateLegacyToolRegistry({
106
+ configPath: deps.configPath,
107
+ recipeRoot,
108
+ reservedToolNames: deps.reservedToolNames,
109
+ });
110
+ warnings.push(...migration.warnings);
111
+ if (migration.conflicts.length > 0)
112
+ warnings.push(`Recipe migration conflicts: ${migration.conflicts.join(", ")}`);
113
+ if (migration.invalid.length > 0)
114
+ warnings.push(`Recipe migration invalid entries: ${migration.invalid.join(", ")}`);
115
+ const discovered = RecipeDiscovery.discoverRecipeSources([
116
+ { root: recipeRoot, defaultTool: true, mutableUsage: true },
117
+ { root: packagedRecipeRoot },
118
+ ]);
119
+ warnings.push(...discovered.diagnostics);
72
120
  tools.clear();
73
- for (const [name, cfg] of loaded.tools) tools.set(name, cfg);
74
- if (loaded.changed) {
75
- const saveError = Config.saveTools(deps.configPath, tools);
76
- if (saveError) {
77
- loaded.warnings.push(
78
- `Failed to normalize ${deps.configPath}: ${saveError}`,
121
+ for (const entry of discovered.active.values()) {
122
+ try {
123
+ const cfg = RecipeDiscovery.toRegisteredTool(entry);
124
+ if (cfg) tools.set(cfg.name, cfg);
125
+ } catch (error) {
126
+ warnings.push(
127
+ `Recipe ${entry.id} could not be exposed as a tool: ${error instanceof Error ? error.message : String(error)}`,
79
128
  );
80
129
  }
81
130
  }
131
+ deactivateMissingRuntimeTools(new Set(tools.keys()));
82
132
  for (const cfg of tools.values()) {
83
133
  const conflict = getExternalToolConflict(cfg.name);
84
134
  if (conflict) {
85
- loaded.warnings.push(conflict);
135
+ warnings.push(conflict);
86
136
  continue;
87
137
  }
88
138
  registerRuntimeTool(cfg);
89
139
  }
90
- if (loaded.warnings.length > 0) {
91
- notify(ctx, `Auto-tools: ${loaded.warnings.join("; ")}`, "warning");
140
+ if (warnings.length > 0) {
141
+ notify(ctx, `Recipe tools: ${warnings.join("; ")}`, "warning");
92
142
  }
93
143
  }
94
144
  return {
package/lib/tools.ts CHANGED
@@ -9,8 +9,11 @@ import * as AsyncRuns from "./async-runs.ts";
9
9
  import * as CommandTemplates from "./command-templates.ts";
10
10
  import type { RegisteredTool } from "./config.ts";
11
11
  import * as Execution from "./execution.ts";
12
+ import * as Paths from "./paths.ts";
12
13
  import * as Prompts from "./prompts.ts";
14
+ import * as RecipeDiscovery from "./recipe-discovery.ts";
13
15
  import * as RecipeReferences from "./recipe-references.ts";
16
+ import * as RecipeUsage from "./recipe-usage.ts";
14
17
  import * as Registry from "./registry.ts";
15
18
  import * as Schema from "./schema.ts";
16
19
 
@@ -229,6 +232,17 @@ function compactToolActor(name: string, tool: Record<string, unknown>): string {
229
232
  return `\ntool=${name} description=${String(tool.description ?? "").replaceAll(/\s+/g, "_")} args=${Object.keys(properties).join(",")} required=${required}`;
230
233
  }
231
234
 
235
+ function compactRecipeRegistry(summary: Record<string, unknown>): string {
236
+ const active = Array.isArray(summary.active) ? summary.active.length : 0;
237
+ const shadowed = Array.isArray(summary.shadowed) ? summary.shadowed.length : 0;
238
+ const invalid = Array.isArray(summary.invalid) ? summary.invalid.length : 0;
239
+ const disabled = Array.isArray(summary.disabled) ? summary.disabled.length : 0;
240
+ const diagnostics = Array.isArray(summary.diagnostics)
241
+ ? summary.diagnostics.length
242
+ : 0;
243
+ return `\nrecipes active=${active} shadowed=${shadowed} invalid=${invalid} disabled=${disabled} diagnostics=${diagnostics}`;
244
+ }
245
+
232
246
  function compactActorMessageResult(
233
247
  message: ActorMessages.ActorMessage,
234
248
  result: Record<string, unknown>,
@@ -427,6 +441,8 @@ export function createSpawnToolDefinition<
427
441
 
428
442
  export interface InspectToolDeps<TContext = unknown> {
429
443
  getTool?: (name: string) => any | undefined;
444
+ packagedRecipeRoot?: string;
445
+ recipeRoot?: string;
430
446
  }
431
447
 
432
448
  function getContextSessionId(ctx: unknown): string | undefined {
@@ -494,8 +510,31 @@ export function createInspectToolDefinition<TContext = unknown>(
494
510
  ) {
495
511
  const input = asRecord(params);
496
512
  const target = String(input.target ?? "");
497
- const address = ActorMessages.parseActorAddress(target);
498
513
  const view = String(input.view ?? "");
514
+ if (target === "recipes" || target === "recipe-registry") {
515
+ if (view !== "status" && view !== "summary") {
516
+ throw new Error("inspect recipes supports view=status or view=summary.");
517
+ }
518
+ const discovered = RecipeDiscovery.discoverRecipeSources([
519
+ { root: deps.recipeRoot ?? Paths.getRecipeRoot(), defaultTool: true, mutableUsage: true },
520
+ { root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
521
+ ]);
522
+ const summary = RecipeDiscovery.summarizeDiscovery(discovered);
523
+ return {
524
+ content: [
525
+ {
526
+ type: "text" as const,
527
+ text: maybeJsonText(
528
+ summary,
529
+ input.verbose === true,
530
+ compactRecipeRegistry(summary),
531
+ ),
532
+ },
533
+ ],
534
+ details: summary,
535
+ };
536
+ }
537
+ const address = ActorMessages.parseActorAddress(target);
499
538
  if (address.kind === "coordinator") {
500
539
  if (view !== "status" && view !== "runs") {
501
540
  throw new Error(
@@ -881,6 +920,7 @@ export function createRuntimeToolDefinition(
881
920
  ctx: AsyncRunToolContext,
882
921
  ) {
883
922
  try {
923
+ if (cfg.sourcePath) RecipeUsage.recordRecipeLaunch(cfg.sourcePath);
884
924
  if (isAsyncRecipe) {
885
925
  const input = params as Record<string, unknown>;
886
926
  const { run_id, ...values } = input;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "private": false,
5
5
  "description": "Actor runtime and orchestrator for agent-managed local processes",
6
6
  "keywords": [
@@ -40,7 +40,8 @@
40
40
  "AGENTS.md",
41
41
  "BACKLOG.md",
42
42
  "CHANGELOG.md",
43
- "docs"
43
+ "docs",
44
+ "banner.jpg"
44
45
  ],
45
46
  "pi": {
46
47
  "extensions": [
@@ -49,7 +50,8 @@
49
50
  "skills": [
50
51
  "./skills/actors/SKILL.md",
51
52
  "./skills/swarm/SKILL.md"
52
- ]
53
+ ],
54
+ "image": "https://github.com/llblab/pi-actors/raw/main/banner.jpg"
53
55
  },
54
56
  "peerDependencies": {
55
57
  "@earendil-works/pi-coding-agent": "*"
@@ -2,7 +2,7 @@
2
2
  name: actors
3
3
  description: Highest-density practical guide for pi-actors. Read this skill whenever prompt and tools are not enough for spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
4
4
  metadata:
5
- version: 0.15.0
5
+ version: 0.16.1
6
6
  ---
7
7
 
8
8
  # Actors (pi-actors)
@@ -93,6 +93,7 @@ Check `inspect view=mailbox` before domain-specific messages.
93
93
  { "target": "run:repo-health", "view": "messages" }
94
94
  { "target": "run:repo-health", "view": "artifacts" }
95
95
  { "target": "tool:music_player", "view": "status" }
96
+ { "target": "recipes", "view": "status" }
96
97
  { "target": "coordinator", "view": "status" }
97
98
  ```
98
99
 
@@ -104,6 +105,7 @@ Views:
104
105
  - `mailbox`: declared accepts/emits contract.
105
106
  - `files`: run state directory file list.
106
107
  - `artifacts`: declared artifact paths/status.
108
+ - `recipes` target: registry summary for active, shadowed, invalid, disabled, and diagnostic recipe entries.
107
109
 
108
110
  Let terminal notifications arrive; avoid sleep-poll loops except during diagnosis.
109
111
 
@@ -165,22 +167,38 @@ Rules:
165
167
  4. Use `imports` to compose recipes; imported recipes are definitions, not nested async runs.
166
168
  5. Declare `mailbox` for actors that accept or emit meaningful messages.
167
169
  6. Declare `artifacts` for durable outputs the coordinator should inspect.
168
- 7. Keep packaged recipes generic: no machine-local paths, no private companion identities, no project-specific defaults unless the recipe is explicitly project-specific.
169
- 8. Do not ship concrete model-version defaults in packaged recipes; expose `model`, `models`, and stage-specific model args so the caller must choose current policy at launch.
170
+ 7. Recipe identity comes from the filename basename when `name` is omitted.
171
+ 8. Keep packaged recipes generic: no machine-local paths, no private companion identities, no project-specific defaults unless the recipe is explicitly project-specific.
172
+ 9. Do not ship concrete model-version defaults in packaged recipes; expose `model`, `models`, and stage-specific model args so the caller must choose current policy at launch.
173
+
174
+ Priority for same-name recipes:
175
+
176
+ 1. No recipe: no capability.
177
+ 2. Packaged pi-actors recipe: standard-library declarative actor component.
178
+ 3. Explicit ad hoc user recipe file outside `~/.pi/agent/recipes`.
179
+ 4. User recipe in `~/.pi/agent/recipes/*.json`: highest-priority operator tool surface.
180
+
181
+ Only matching filename ids compete. Higher priority shadows lower priority. An invalid or `disabled: true` higher-priority recipe blocks fallback so the agent does not silently run standard-library behavior when a user override is broken or intentionally disabled.
182
+
183
+ Muscle-memory lens: every recipe in `~/.pi/agent/recipes/*.json` becomes an easy-to-call tool by default. This is intentionally sticky: successful local patterns can quickly become durable agent muscle memory. The tradeoff is tool-surface clutter; accidental or one-off tools behave like persistent intrusive thoughts until an agent/operator focuses on cleanup.
184
+
185
+ Usage lens: user recipes may carry extension-maintained launch metadata such as `usage.calls` and `usage.last_called`. The extension increments the counter when it starts that concrete recipe; agents should not hand-edit counters as part of normal recipe maintenance. Treat usage as evidence for usefulness analysis: heavily used recipes are good candidates for promotion, documentation, or stronger tests; unused recipes are cleanup or `tool: false` candidates. Do not use failure counts as a primary usefulness signal because failures may reflect bad caller judgment rather than bad recipes. Do not delete or demote solely from counters without operator approval.
186
+
187
+ Cleanup rule: periodically inspect `~/.pi/agent/recipes` as the live muscle-memory set. For each stale, duplicate, too-specific, or low-value recipe, choose one explicit action: keep as a tool, set `tool: false` to retain recipe-only memory, merge into a better recipe, or delete/archive the file. Prefer demotion over deletion when the recipe may still be useful as a component. Never silently remove tools during unrelated work.
170
188
 
171
189
  ## Registered Tools
172
190
 
173
- `register_tool` persists trusted local capabilities in `~/.pi/agent/actors-tools.json`.
191
+ `register_tool` persists trusted local capabilities as recipe files in `~/.pi/agent/recipes/*.json`.
174
192
 
175
- Use it when a command/template/recipe should become durable agent muscle memory. Prefer typed args or placeholder-derived args; use `update=true` for replacement and `template=null` or `template=""` for deletion.
193
+ Use it when a command/template/recipe should become durable agent muscle memory. Prefer typed args or placeholder-derived args; use `update=true` for replacement and `template=null` or `template=""` for deletion. `register_tool` should create/update/delete recipe files in the user recipe root; direct file editing is allowed but is the lower-level path.
176
194
 
177
195
  Tool templates may be:
178
196
 
179
197
  - A foreground command template.
180
198
  - A file-backed recipe name/path.
181
- - A co-located recipe, optionally `async: true`.
199
+ - A complete recipe body, optionally `async: true`.
182
200
 
183
- Registered tools are convenient buttons; recipes are the reusable semantic definitions behind them.
201
+ The user recipe root is the default tool set; packaged recipes are the lower-priority standard library and opt into tool exposure with `tool: true`. Ideal runtime behavior is reactive: create/edit/delete recipe files, validate them, then connect valid tools or surface diagnostics without requiring agents to hand-maintain a separate registry.
184
202
 
185
203
  ## Recipe Navigator
186
204
 
@@ -2,7 +2,7 @@
2
2
  name: swarm
3
3
  description: Subagent orchestration with scoped locks and quorum consensus. Use for multi-model review, parallel scoped work, delegated audit, and coordinated subagent execution.
4
4
  metadata:
5
- version: 0.15.0
5
+ version: 0.16.1
6
6
  ---
7
7
 
8
8
  # Swarm