@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/BACKLOG.md +44 -8
- package/CHANGELOG.md +25 -0
- package/README.md +35 -47
- package/banner.jpg +0 -0
- package/docs/template-recipes.md +39 -22
- package/docs/tool-registry.md +48 -42
- package/index.ts +34 -0
- package/lib/async-runs.ts +10 -0
- package/lib/config.ts +1 -0
- package/lib/paths.ts +6 -1
- package/lib/prompts.ts +2 -2
- package/lib/recipe-discovery.ts +231 -0
- package/lib/recipe-migration.ts +123 -0
- package/lib/recipe-references.ts +45 -13
- package/lib/recipe-usage.ts +44 -0
- package/lib/registry.ts +48 -15
- package/lib/runtime.ts +63 -13
- package/lib/tools.ts +41 -1
- package/package.json +5 -3
- package/skills/actors/SKILL.md +25 -7
- package/skills/swarm/SKILL.md +1 -1
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
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
135
|
+
warnings.push(conflict);
|
|
86
136
|
continue;
|
|
87
137
|
}
|
|
88
138
|
registerRuntimeTool(cfg);
|
|
89
139
|
}
|
|
90
|
-
if (
|
|
91
|
-
notify(ctx, `
|
|
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.
|
|
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": "*"
|
package/skills/actors/SKILL.md
CHANGED
|
@@ -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.
|
|
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.
|
|
169
|
-
8.
|
|
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/
|
|
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
|
|
199
|
+
- A complete recipe body, optionally `async: true`.
|
|
182
200
|
|
|
183
|
-
|
|
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
|
|
package/skills/swarm/SKILL.md
CHANGED