@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.
- package/AGENTS.md +7 -5
- package/CHANGELOG.md +20 -0
- package/README.md +2 -1
- package/dist/lib/async-runs.d.ts +1 -0
- package/dist/lib/async-runs.js +4 -1
- package/dist/lib/execution.d.ts +1 -0
- package/dist/lib/execution.js +1 -0
- package/dist/lib/extension-runtime.js +25 -9
- package/dist/lib/inspector.js +1 -0
- package/dist/lib/prompts.d.ts +6 -5
- package/dist/lib/prompts.js +22 -23
- package/dist/lib/recipes-context.d.ts +12 -3
- package/dist/lib/recipes-context.js +28 -4
- package/dist/lib/recipes-discovery.d.ts +22 -0
- package/dist/lib/recipes-discovery.js +108 -23
- package/dist/lib/recipes-references.d.ts +18 -0
- package/dist/lib/recipes-references.js +129 -38
- package/dist/lib/registry.d.ts +23 -6
- package/dist/lib/registry.js +294 -101
- package/dist/lib/runtime.d.ts +30 -3
- package/dist/lib/runtime.js +108 -9
- package/dist/lib/tools-inspect.d.ts +3 -0
- package/dist/lib/tools-inspect.js +190 -26
- package/dist/lib/tools-local.d.ts +2 -2
- package/dist/lib/tools-local.js +5 -2
- package/dist/lib/tools-register.js +2 -1
- package/dist/lib/tools-response.js +7 -1
- package/dist/lib/tools-spawn.d.ts +2 -2
- package/dist/lib/tools-spawn.js +1 -1
- package/dist/lib/tools.d.ts +4 -1
- package/dist/lib/tools.js +2 -0
- package/dist/scripts/conformance.mjs +1 -0
- package/dist/skills/actors/SKILL.md +76 -65
- package/dist/skills/actors/references/diagnostics.md +44 -0
- package/dist/skills/actors/references/persistent-tools.md +74 -0
- package/dist/skills/actors/references/recipes.md +51 -0
- package/dist/skills/actors/references/runs.md +39 -0
- package/dist/skills/artifacts/SKILL.md +24 -7
- package/dist/skills/media/SKILL.md +35 -7
- package/dist/skills/project-work/SKILL.md +28 -7
- package/dist/skills/recipe-memory/SKILL.md +27 -7
- package/dist/skills/swarm/SKILL.md +41 -445
- package/dist/skills/swarm/references/development-swarm.md +87 -539
- package/dist/skills/swarm/references/review-swarms.md +115 -0
- package/docs/README.md +5 -5
- package/docs/recipe-library.md +15 -10
- package/docs/template-recipes.md +3 -1
- package/docs/tool-registry.md +18 -6
- package/lib/async-runs.ts +5 -1
- package/lib/execution.ts +2 -0
- package/lib/extension-runtime.ts +33 -14
- package/lib/inspector.ts +1 -0
- package/lib/prompts.ts +24 -24
- package/lib/recipes-context.ts +49 -4
- package/lib/recipes-discovery.ts +186 -25
- package/lib/recipes-references.ts +176 -44
- package/lib/registry.ts +441 -113
- package/lib/runtime.ts +147 -12
- package/lib/tools-inspect.ts +254 -28
- package/lib/tools-local.ts +9 -3
- package/lib/tools-register.ts +4 -3
- package/lib/tools-response.ts +7 -1
- package/lib/tools-spawn.ts +3 -2
- package/lib/tools.ts +4 -1
- package/package.json +1 -1
- package/scripts/conformance.mjs +1 -0
- package/skills/actors/SKILL.md +76 -65
- package/skills/actors/references/diagnostics.md +44 -0
- package/skills/actors/references/persistent-tools.md +74 -0
- package/skills/actors/references/recipes.md +51 -0
- package/skills/actors/references/runs.md +39 -0
- package/skills/artifacts/SKILL.md +24 -7
- package/skills/media/SKILL.md +35 -7
- package/skills/project-work/SKILL.md +28 -7
- package/skills/recipe-memory/SKILL.md +27 -7
- package/skills/swarm/SKILL.md +41 -445
- package/skills/swarm/references/development-swarm.md +87 -539
- package/skills/swarm/references/review-swarms.md +115 -0
package/lib/runtime.ts
CHANGED
|
@@ -10,7 +10,10 @@ 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 RecipesReferences from "./recipes-references.ts";
|
|
16
|
+
import * as RecipesUsage from "./recipes-usage.ts";
|
|
14
17
|
import * as ToolsLocal from "./tools-local.ts";
|
|
15
18
|
|
|
16
19
|
export interface RuntimeContext {
|
|
@@ -20,11 +23,19 @@ export interface RuntimeContext {
|
|
|
20
23
|
};
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
export interface RuntimeToolActivation {
|
|
27
|
+
active_tool: boolean;
|
|
28
|
+
activation: "current_session" | "unverified";
|
|
29
|
+
callable_now: boolean;
|
|
30
|
+
host_registered: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
export interface ToolRegistryRuntimeDeps {
|
|
24
34
|
configPath: string;
|
|
25
35
|
exec: RegisteredToolExec;
|
|
26
36
|
recipeRoot?: string;
|
|
27
37
|
getActiveTools?: () => string[];
|
|
38
|
+
getAllTools?: () => Array<{ name: string }>;
|
|
28
39
|
registerTool: (
|
|
29
40
|
definition: ReturnType<typeof ToolsLocal.createRuntimeToolDefinition>,
|
|
30
41
|
) => void;
|
|
@@ -32,16 +43,32 @@ export interface ToolRegistryRuntimeDeps {
|
|
|
32
43
|
setActiveTools?: (toolNames: string[]) => void;
|
|
33
44
|
}
|
|
34
45
|
|
|
46
|
+
export interface RecipeRegistryStatus {
|
|
47
|
+
active_tool_count: number;
|
|
48
|
+
last_scan_counts: { active: number; rejected: number; scanned: number };
|
|
49
|
+
last_scan_at?: string;
|
|
50
|
+
registry_generation: number;
|
|
51
|
+
resolution_generation?: string;
|
|
52
|
+
watch_status: "closed" | "failed" | "watching_parent" | "watching_root";
|
|
53
|
+
watched_root: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
export interface ToolRegistryRuntime {
|
|
57
|
+
getStatus(): RecipeRegistryStatus;
|
|
58
|
+
getToolStatus(name: string): Record<string, unknown> | undefined;
|
|
36
59
|
getToolNameBlocker(name: string): string | undefined;
|
|
37
60
|
getTools(): Map<string, Config.RegisteredTool>;
|
|
38
|
-
loadTools(
|
|
61
|
+
loadTools(
|
|
62
|
+
ctx: RuntimeContext,
|
|
63
|
+
resolutionContext?: RecipeResolutionContext,
|
|
64
|
+
): void;
|
|
39
65
|
notify(
|
|
40
66
|
ctx: RuntimeContext,
|
|
41
67
|
message: string,
|
|
42
68
|
type: "info" | "warning" | "error",
|
|
43
69
|
): void;
|
|
44
|
-
registerRuntimeTool(cfg: Config.RegisteredTool):
|
|
70
|
+
registerRuntimeTool(cfg: Config.RegisteredTool): RuntimeToolActivation;
|
|
71
|
+
setWatchStatus(status: RecipeRegistryStatus["watch_status"]): void;
|
|
45
72
|
}
|
|
46
73
|
|
|
47
74
|
export interface RecipeToolReloadWatcher {
|
|
@@ -55,6 +82,14 @@ export function createAutoToolsRuntime(
|
|
|
55
82
|
const tools = new Map<string, Config.RegisteredTool>();
|
|
56
83
|
const runtimeToolFingerprints = new Map<string, string>();
|
|
57
84
|
const runtimeTools = new Set<string>();
|
|
85
|
+
const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
|
|
86
|
+
const status: RecipeRegistryStatus = {
|
|
87
|
+
active_tool_count: 0,
|
|
88
|
+
last_scan_counts: { active: 0, rejected: 0, scanned: 0 },
|
|
89
|
+
registry_generation: 0,
|
|
90
|
+
watch_status: "closed",
|
|
91
|
+
watched_root: recipeRoot,
|
|
92
|
+
};
|
|
58
93
|
function notify(
|
|
59
94
|
ctx: RuntimeContext,
|
|
60
95
|
message: string,
|
|
@@ -77,6 +112,25 @@ export function createAutoToolsRuntime(
|
|
|
77
112
|
template: cfg.template,
|
|
78
113
|
});
|
|
79
114
|
}
|
|
115
|
+
function registeredToolSource(cfg: Config.RegisteredTool): string {
|
|
116
|
+
const raw = cfg.sourcePath
|
|
117
|
+
? RecipesReferences.readRawRecipeConfig(cfg.sourcePath)
|
|
118
|
+
: undefined;
|
|
119
|
+
const template = raw?.template;
|
|
120
|
+
if (
|
|
121
|
+
typeof template === "string" &&
|
|
122
|
+
/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(template)
|
|
123
|
+
) {
|
|
124
|
+
return template;
|
|
125
|
+
}
|
|
126
|
+
if (
|
|
127
|
+
typeof template === "string" &&
|
|
128
|
+
(template.endsWith(".json") || template.endsWith(".md"))
|
|
129
|
+
) {
|
|
130
|
+
return `<explicit-file:${basename(template)}>`;
|
|
131
|
+
}
|
|
132
|
+
return "template";
|
|
133
|
+
}
|
|
80
134
|
function deactivateMissingRuntimeTools(activeNames: Set<string>): void {
|
|
81
135
|
const stale = [...runtimeTools].filter((name) => !activeNames.has(name));
|
|
82
136
|
if (stale.length === 0) return;
|
|
@@ -90,12 +144,30 @@ export function createAutoToolsRuntime(
|
|
|
90
144
|
deps.getActiveTools().filter((name) => !staleSet.has(name)),
|
|
91
145
|
);
|
|
92
146
|
}
|
|
93
|
-
function
|
|
147
|
+
function activationFor(name: string): RuntimeToolActivation {
|
|
148
|
+
const hostRegistered = deps.getAllTools?.().some((tool) => tool.name === name) ?? false;
|
|
149
|
+
const activeTool = deps.getActiveTools?.().includes(name) ?? false;
|
|
150
|
+
return {
|
|
151
|
+
active_tool: activeTool,
|
|
152
|
+
activation:
|
|
153
|
+
hostRegistered && activeTool ? "current_session" : "unverified",
|
|
154
|
+
callable_now: hostRegistered && activeTool,
|
|
155
|
+
host_registered: hostRegistered,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function registerRuntimeTool(cfg: Config.RegisteredTool): RuntimeToolActivation {
|
|
94
159
|
const fingerprint = getToolFingerprint(cfg);
|
|
95
|
-
if (runtimeToolFingerprints.get(cfg.name)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
160
|
+
if (runtimeToolFingerprints.get(cfg.name) !== fingerprint) {
|
|
161
|
+
deps.registerTool(ToolsLocal.createRuntimeToolDefinition(cfg, deps.exec));
|
|
162
|
+
runtimeTools.add(cfg.name);
|
|
163
|
+
runtimeToolFingerprints.set(cfg.name, fingerprint);
|
|
164
|
+
}
|
|
165
|
+
if (deps.getActiveTools && deps.setActiveTools) {
|
|
166
|
+
deps.setActiveTools([
|
|
167
|
+
...new Set([...deps.getActiveTools(), cfg.name]),
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
return activationFor(cfg.name);
|
|
99
171
|
}
|
|
100
172
|
function isStartupActionableRegistryWarning(warning: string): boolean {
|
|
101
173
|
if (warning.includes(" shadows ")) return false;
|
|
@@ -135,13 +207,21 @@ export function createAutoToolsRuntime(
|
|
|
135
207
|
lines.push("Other registry diagnostics:");
|
|
136
208
|
lines.push(...other.map((warning) => `• ${warning}`));
|
|
137
209
|
}
|
|
210
|
+
lines.push("Next: inspect target=recipes view=doctor");
|
|
138
211
|
return `${lines.join("\n")}\n`;
|
|
139
212
|
}
|
|
140
|
-
function loadTools(
|
|
213
|
+
function loadTools(
|
|
214
|
+
ctx: RuntimeContext,
|
|
215
|
+
resolutionContext?: RecipeResolutionContext,
|
|
216
|
+
) {
|
|
141
217
|
const warnings: string[] = [];
|
|
142
|
-
const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
|
|
143
218
|
const discovered = RecipesDiscovery.discoverRecipeSources([
|
|
144
|
-
{
|
|
219
|
+
{
|
|
220
|
+
root: recipeRoot,
|
|
221
|
+
defaultTool: true,
|
|
222
|
+
mutableUsage: true,
|
|
223
|
+
resolutionContext,
|
|
224
|
+
},
|
|
145
225
|
]);
|
|
146
226
|
warnings.push(...discovered.diagnostics);
|
|
147
227
|
tools.clear();
|
|
@@ -164,28 +244,77 @@ export function createAutoToolsRuntime(
|
|
|
164
244
|
}
|
|
165
245
|
registerRuntimeTool(cfg);
|
|
166
246
|
}
|
|
247
|
+
status.active_tool_count = tools.size;
|
|
248
|
+
status.last_scan_at = new Date().toISOString();
|
|
249
|
+
status.last_scan_counts = {
|
|
250
|
+
active: tools.size,
|
|
251
|
+
rejected: discovered.entries.filter((entry) => entry.invalid).length,
|
|
252
|
+
scanned: discovered.entries.length,
|
|
253
|
+
};
|
|
254
|
+
status.registry_generation += 1;
|
|
255
|
+
status.resolution_generation = resolutionContext?.generation;
|
|
167
256
|
const startupWarnings = warnings.filter(isStartupActionableRegistryWarning);
|
|
168
257
|
if (startupWarnings.length > 0) {
|
|
169
258
|
notify(ctx, formatRecipeToolWarnings(startupWarnings), "warning");
|
|
170
259
|
}
|
|
171
260
|
}
|
|
172
261
|
return {
|
|
262
|
+
getStatus: () => ({
|
|
263
|
+
...status,
|
|
264
|
+
last_scan_counts: { ...status.last_scan_counts },
|
|
265
|
+
}),
|
|
266
|
+
getToolStatus: (name) => {
|
|
267
|
+
const cfg = tools.get(name);
|
|
268
|
+
if (!cfg) return undefined;
|
|
269
|
+
const usage = cfg.sourcePath
|
|
270
|
+
? RecipesUsage.readRecipeUsage(cfg.sourcePath)
|
|
271
|
+
: undefined;
|
|
272
|
+
const activation = activationFor(name);
|
|
273
|
+
const args = RecipesDiscovery.summarizeRegisteredToolArgs(cfg);
|
|
274
|
+
return {
|
|
275
|
+
...activation,
|
|
276
|
+
activation_boundary: activation.callable_now
|
|
277
|
+
? "current_session"
|
|
278
|
+
: !activation.host_registered
|
|
279
|
+
? "host_registration"
|
|
280
|
+
: "active_tool_set",
|
|
281
|
+
persisted: Boolean(cfg.sourcePath),
|
|
282
|
+
registry_active: true,
|
|
283
|
+
source: registeredToolSource(cfg),
|
|
284
|
+
required_args: args.required,
|
|
285
|
+
optional_args: args.optional,
|
|
286
|
+
next_actions: activation.callable_now
|
|
287
|
+
? [`call tool ${name}`]
|
|
288
|
+
: [
|
|
289
|
+
`register_tool name=${name} update=true`,
|
|
290
|
+
`inspect target=tool:${name} view=status`,
|
|
291
|
+
],
|
|
292
|
+
...(usage?.launch_kind ? { launch_kind: usage.launch_kind } : {}),
|
|
293
|
+
spawn_calls: Number(usage?.spawn_calls ?? 0),
|
|
294
|
+
tool_calls: Number(usage?.tool_calls ?? 0),
|
|
295
|
+
};
|
|
296
|
+
},
|
|
173
297
|
getToolNameBlocker,
|
|
174
298
|
getTools: () => tools,
|
|
175
299
|
loadTools,
|
|
176
300
|
notify,
|
|
177
301
|
registerRuntimeTool,
|
|
302
|
+
setWatchStatus: (watchStatus) => {
|
|
303
|
+
status.watch_status = watchStatus;
|
|
304
|
+
},
|
|
178
305
|
};
|
|
179
306
|
}
|
|
180
307
|
|
|
181
308
|
export interface RecipeToolReloadWatcherDeps {
|
|
182
309
|
exists?: (path: string) => boolean;
|
|
310
|
+
getResolutionContext?: () => RecipeResolutionContext | undefined;
|
|
183
311
|
recipeRoot?: string;
|
|
184
312
|
watchPath?: typeof watch;
|
|
185
313
|
}
|
|
186
314
|
|
|
187
315
|
export function createRecipeToolReloadWatcher(
|
|
188
|
-
runtime: Pick<ToolRegistryRuntime, "loadTools"
|
|
316
|
+
runtime: Pick<ToolRegistryRuntime, "loadTools"> &
|
|
317
|
+
Partial<Pick<ToolRegistryRuntime, "setWatchStatus">>,
|
|
189
318
|
deps: RecipeToolReloadWatcherDeps = {},
|
|
190
319
|
): RecipeToolReloadWatcher {
|
|
191
320
|
const pathExists = deps.exists ?? existsSync;
|
|
@@ -194,6 +323,8 @@ export function createRecipeToolReloadWatcher(
|
|
|
194
323
|
let rootWatcher: FSWatcher | undefined;
|
|
195
324
|
let parentWatcher: FSWatcher | undefined;
|
|
196
325
|
let failureNotified = false;
|
|
326
|
+
const setWatchStatus = (watchStatus: RecipeRegistryStatus["watch_status"]): void =>
|
|
327
|
+
runtime.setWatchStatus?.(watchStatus);
|
|
197
328
|
const close = (): void => {
|
|
198
329
|
const closingRoot = rootWatcher;
|
|
199
330
|
rootWatcher = undefined;
|
|
@@ -203,10 +334,12 @@ export function createRecipeToolReloadWatcher(
|
|
|
203
334
|
closingParent?.close();
|
|
204
335
|
if (reloadTimeout) clearTimeout(reloadTimeout);
|
|
205
336
|
reloadTimeout = undefined;
|
|
337
|
+
setWatchStatus("closed");
|
|
206
338
|
};
|
|
207
339
|
const notifyFailure = (ctx: RuntimeContext): void => {
|
|
208
340
|
if (failureNotified) return;
|
|
209
341
|
failureNotified = true;
|
|
342
|
+
setWatchStatus("failed");
|
|
210
343
|
ctx.ui.notify(
|
|
211
344
|
"Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.",
|
|
212
345
|
"warning",
|
|
@@ -216,7 +349,7 @@ export function createRecipeToolReloadWatcher(
|
|
|
216
349
|
failureNotified = false;
|
|
217
350
|
if (reloadTimeout) clearTimeout(reloadTimeout);
|
|
218
351
|
reloadTimeout = setTimeout(() => {
|
|
219
|
-
runtime.loadTools(ctx);
|
|
352
|
+
runtime.loadTools(ctx, deps.getResolutionContext?.());
|
|
220
353
|
ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
|
|
221
354
|
}, 150);
|
|
222
355
|
reloadTimeout.unref?.();
|
|
@@ -245,6 +378,7 @@ export function createRecipeToolReloadWatcher(
|
|
|
245
378
|
scheduleReload(ctx);
|
|
246
379
|
});
|
|
247
380
|
parentWatcher = watcher;
|
|
381
|
+
setWatchStatus("watching_parent");
|
|
248
382
|
watcher.on("error", () => {
|
|
249
383
|
if (parentWatcher !== watcher) return;
|
|
250
384
|
parentWatcher = undefined;
|
|
@@ -274,6 +408,7 @@ export function createRecipeToolReloadWatcher(
|
|
|
274
408
|
scheduleReload(ctx);
|
|
275
409
|
});
|
|
276
410
|
rootWatcher = watcher;
|
|
411
|
+
setWatchStatus("watching_root");
|
|
277
412
|
watcher.on("error", () => {
|
|
278
413
|
if (rootWatcher !== watcher) return;
|
|
279
414
|
rootWatcher = undefined;
|
package/lib/tools-inspect.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Owns exact inspect target/view dispatch; source projection stays in domain modules.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { statSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
7
|
+
import { existsSync, statSync } from "node:fs";
|
|
8
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
9
9
|
|
|
10
10
|
import * as AsyncRuns from "./async-runs.ts";
|
|
11
11
|
import * as ControlProjection from "./control-projection.ts";
|
|
@@ -13,12 +13,14 @@ import * as Inspector from "./inspector.ts";
|
|
|
13
13
|
import * as Limits from "./limits.ts";
|
|
14
14
|
import { computeRunControlCapacity } from "./run-evidence-policy.ts";
|
|
15
15
|
import * as Paths from "./paths.ts";
|
|
16
|
+
import type * as RecipeResolution from "./recipes-context.ts";
|
|
16
17
|
import * as RecipesDiscovery from "./recipes-discovery.ts";
|
|
17
18
|
import * as RecipesReferences from "./recipes-references.ts";
|
|
18
19
|
import * as ReviewDiagnostics from "./review-diagnostics.ts";
|
|
19
20
|
import * as RunsControls from "./runs-controls.ts";
|
|
20
21
|
import * as RunControlDelivery from "./runs-control-delivery.ts";
|
|
21
22
|
import * as RunsTrace from "./runs-trace.ts";
|
|
23
|
+
import * as Runtime from "./runtime.ts";
|
|
22
24
|
import * as RuntimeIdentity from "./runtime-identity.ts";
|
|
23
25
|
import * as RuntimeTriage from "./runtime-triage.ts";
|
|
24
26
|
import * as Schema from "./schema.ts";
|
|
@@ -32,8 +34,10 @@ const maybeJsonText = ToolsResponse.maybeJsonText;
|
|
|
32
34
|
export interface InspectToolDeps<TContext = unknown> {
|
|
33
35
|
getRunStatus?: (runOrDir: string) => Record<string, any>;
|
|
34
36
|
getTool?: (name: string) => any | undefined;
|
|
37
|
+
getToolStatus?: (name: string) => Record<string, unknown> | undefined;
|
|
35
38
|
listRuns?: () => Array<Record<string, any>>;
|
|
36
39
|
recipeRoot?: string;
|
|
40
|
+
registryStatus?: () => Runtime.RecipeRegistryStatus;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
function runtimeStatus(): Record<string, unknown> {
|
|
@@ -191,8 +195,148 @@ function inspectRuntime(
|
|
|
191
195
|
throw new Error("inspect runtime supports view=status, view=runs, or view=triage.");
|
|
192
196
|
}
|
|
193
197
|
|
|
198
|
+
function portableSkillRecipePath(
|
|
199
|
+
file: string,
|
|
200
|
+
skill: string,
|
|
201
|
+
namespaces: Record<string, string[]>,
|
|
202
|
+
): string {
|
|
203
|
+
const root = namespaces[skill]?.[0];
|
|
204
|
+
if (!root) return `<active-skill:${skill}>`;
|
|
205
|
+
const relation = relative(root, file);
|
|
206
|
+
return !relation.startsWith("..") && !isAbsolute(relation)
|
|
207
|
+
? `<active-skill:${skill}>/${relation.replaceAll("\\", "/")}`
|
|
208
|
+
: `<active-skill:${skill}>`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function portableSkillRecipeDiagnosticReason(
|
|
212
|
+
diagnostic: RecipesReferences.SkillRecipeComponentDiagnostic,
|
|
213
|
+
namespaces: Record<string, string[]>,
|
|
214
|
+
): string {
|
|
215
|
+
let reason = diagnostic.reason;
|
|
216
|
+
if (diagnostic.file) {
|
|
217
|
+
reason = reason.replaceAll(
|
|
218
|
+
diagnostic.file,
|
|
219
|
+
portableSkillRecipePath(
|
|
220
|
+
diagnostic.file,
|
|
221
|
+
diagnostic.skill,
|
|
222
|
+
namespaces,
|
|
223
|
+
),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
for (const root of namespaces[diagnostic.skill] ?? []) {
|
|
227
|
+
reason = reason.replaceAll(root, `<active-skill:${diagnostic.skill}>`);
|
|
228
|
+
}
|
|
229
|
+
return reason;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function inspectRecipeIdentity(
|
|
233
|
+
identity: string,
|
|
234
|
+
resolutionContext: RecipeResolution.RecipeResolutionContext | undefined,
|
|
235
|
+
namespaces: Record<string, string[]>,
|
|
236
|
+
inventory: RecipesReferences.SkillRecipeComponentInventory,
|
|
237
|
+
registryStatus: Runtime.RecipeRegistryStatus | undefined,
|
|
238
|
+
): Record<string, unknown> {
|
|
239
|
+
const match = /^([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\/([a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/u.exec(identity);
|
|
240
|
+
const skill = match?.[1] ?? "";
|
|
241
|
+
const roots = skill ? namespaces[skill] ?? [] : [];
|
|
242
|
+
const skillActive = roots.length === 1;
|
|
243
|
+
const matchingComponent = inventory.components.find(
|
|
244
|
+
(component) => component.identity === identity,
|
|
245
|
+
);
|
|
246
|
+
const matchingRejection = inventory.rejected.find(
|
|
247
|
+
(diagnostic) =>
|
|
248
|
+
diagnostic.skill === skill &&
|
|
249
|
+
(diagnostic.stem === undefined || diagnostic.stem === match?.[2]),
|
|
250
|
+
);
|
|
251
|
+
let sourceLocation: string | undefined;
|
|
252
|
+
let rejectedReason: string | undefined;
|
|
253
|
+
let resolvable = false;
|
|
254
|
+
if (!match) {
|
|
255
|
+
rejectedReason =
|
|
256
|
+
"Identity must use canonical <skill>/<recipe> syntax.";
|
|
257
|
+
} else if (roots.length === 0) {
|
|
258
|
+
rejectedReason = `Active Skill Recipe not found: ${identity}`;
|
|
259
|
+
} else if (roots.length > 1) {
|
|
260
|
+
rejectedReason = matchingRejection
|
|
261
|
+
? portableSkillRecipeDiagnosticReason(matchingRejection, namespaces)
|
|
262
|
+
: `Duplicate active Skill identity ${skill}`;
|
|
263
|
+
} else if (matchingRejection) {
|
|
264
|
+
rejectedReason = portableSkillRecipeDiagnosticReason(
|
|
265
|
+
matchingRejection,
|
|
266
|
+
namespaces,
|
|
267
|
+
);
|
|
268
|
+
if (matchingRejection.file) {
|
|
269
|
+
sourceLocation = portableSkillRecipePath(
|
|
270
|
+
matchingRejection.file,
|
|
271
|
+
skill,
|
|
272
|
+
namespaces,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
try {
|
|
277
|
+
const file = RecipesReferences.resolveRecipeReferencePath(
|
|
278
|
+
identity,
|
|
279
|
+
resolutionContext?.cwd,
|
|
280
|
+
resolutionContext?.activeSkills,
|
|
281
|
+
);
|
|
282
|
+
if (!file || !existsSync(file)) {
|
|
283
|
+
rejectedReason = `Skill Recipe component not found: ${identity}`;
|
|
284
|
+
} else {
|
|
285
|
+
sourceLocation = portableSkillRecipePath(file, skill, namespaces);
|
|
286
|
+
const recipe = RecipesReferences.readResolvedRecipeConfig(file, [], {
|
|
287
|
+
skillContext: resolutionContext?.activeSkills,
|
|
288
|
+
});
|
|
289
|
+
if (recipe) resolvable = true;
|
|
290
|
+
else {
|
|
291
|
+
rejectedReason =
|
|
292
|
+
RecipesReferences.diagnoseRawRecipeConfigFailure(file) ??
|
|
293
|
+
`Recipe could not be resolved: ${identity}`;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} catch (error) {
|
|
297
|
+
rejectedReason = error instanceof Error ? error.message : String(error);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const componentStatus = resolvable
|
|
301
|
+
? "available"
|
|
302
|
+
: !match
|
|
303
|
+
? "invalid_identity"
|
|
304
|
+
: roots.length === 0
|
|
305
|
+
? "skill_inactive"
|
|
306
|
+
: roots.length > 1
|
|
307
|
+
? "ambiguous_skill"
|
|
308
|
+
: matchingRejection
|
|
309
|
+
? "rejected"
|
|
310
|
+
: matchingComponent
|
|
311
|
+
? "unresolvable"
|
|
312
|
+
: "missing";
|
|
313
|
+
return {
|
|
314
|
+
identity,
|
|
315
|
+
skill_active: skillActive,
|
|
316
|
+
resolvable,
|
|
317
|
+
catalog_partial: inventory.partial,
|
|
318
|
+
component_status: componentStatus,
|
|
319
|
+
...(sourceLocation ? { source_location: sourceLocation } : {}),
|
|
320
|
+
resolution_generation:
|
|
321
|
+
resolutionContext?.generation ?? registryStatus?.resolution_generation ?? "unavailable",
|
|
322
|
+
...(rejectedReason ? { rejected_reason: rejectedReason } : {}),
|
|
323
|
+
next_actions: resolvable
|
|
324
|
+
? [
|
|
325
|
+
`spawn recipe=${identity}`,
|
|
326
|
+
`register_tool name=<tool-name> from=${identity}`,
|
|
327
|
+
]
|
|
328
|
+
: roots.length === 0 && skill
|
|
329
|
+
? [
|
|
330
|
+
`activate Skill ${skill}`,
|
|
331
|
+
`inspect target=recipes view=doctor identity=${identity}`,
|
|
332
|
+
]
|
|
333
|
+
: ["inspect target=recipes view=imports verbose=true"],
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
194
337
|
function inspectRecipes(
|
|
195
338
|
view: string,
|
|
339
|
+
input: Record<string, unknown>,
|
|
196
340
|
deps: InspectToolDeps,
|
|
197
341
|
context: unknown,
|
|
198
342
|
): Record<string, unknown> {
|
|
@@ -214,37 +358,70 @@ function inspectRecipes(
|
|
|
214
358
|
const discovered = RecipesDiscovery.discoverRecipeSources([
|
|
215
359
|
{ root: recipeRoot, defaultTool: true, mutableUsage: true },
|
|
216
360
|
]);
|
|
217
|
-
const
|
|
218
|
-
context && typeof context === "object" && "
|
|
361
|
+
const resolutionContext =
|
|
362
|
+
context && typeof context === "object" && "recipeResolutionContext" in context
|
|
219
363
|
? (context as {
|
|
220
|
-
|
|
221
|
-
}).
|
|
364
|
+
recipeResolutionContext?: RecipeResolution.RecipeResolutionContext;
|
|
365
|
+
}).recipeResolutionContext
|
|
222
366
|
: undefined;
|
|
223
367
|
const activeSkillContext =
|
|
224
|
-
|
|
368
|
+
resolutionContext?.activeSkills ??
|
|
369
|
+
RecipesReferences.EMPTY_ACTIVE_SKILL_RECIPE_CONTEXT;
|
|
225
370
|
const skillRecipeNamespaces = RecipesReferences.getActiveSkillRecipeNamespaces(
|
|
226
371
|
activeSkillContext,
|
|
227
372
|
);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
373
|
+
const skillInventory =
|
|
374
|
+
RecipesReferences.inventoryActiveSkillRecipeComponents(activeSkillContext);
|
|
375
|
+
const registryStatus = deps.registryStatus?.();
|
|
376
|
+
const identity =
|
|
377
|
+
typeof input.identity === "string" ? input.identity.trim() : "";
|
|
378
|
+
if (identity && view !== "doctor") {
|
|
379
|
+
throw new Error("inspect recipes identity is supported only with view=doctor.");
|
|
380
|
+
}
|
|
381
|
+
if (view === "doctor" && identity) {
|
|
382
|
+
return inspectRecipeIdentity(
|
|
383
|
+
identity,
|
|
384
|
+
resolutionContext,
|
|
385
|
+
skillRecipeNamespaces,
|
|
386
|
+
skillInventory,
|
|
387
|
+
registryStatus,
|
|
388
|
+
);
|
|
244
389
|
}
|
|
390
|
+
const skillRecipeComponents = skillInventory.components.map((component) => ({
|
|
391
|
+
identity: component.identity,
|
|
392
|
+
source_kind: "active_skill_component",
|
|
393
|
+
skill: component.skill,
|
|
394
|
+
stem: component.stem,
|
|
395
|
+
imports: component.imports,
|
|
396
|
+
}));
|
|
397
|
+
const skillRecipeComponentDiagnostics = skillInventory.rejected.map(
|
|
398
|
+
(diagnostic) => ({
|
|
399
|
+
error: portableSkillRecipeDiagnosticReason(
|
|
400
|
+
diagnostic,
|
|
401
|
+
skillRecipeNamespaces,
|
|
402
|
+
),
|
|
403
|
+
...(diagnostic.file
|
|
404
|
+
? {
|
|
405
|
+
file: portableSkillRecipePath(
|
|
406
|
+
diagnostic.file,
|
|
407
|
+
diagnostic.skill,
|
|
408
|
+
skillRecipeNamespaces,
|
|
409
|
+
),
|
|
410
|
+
}
|
|
411
|
+
: {}),
|
|
412
|
+
skill: diagnostic.skill,
|
|
413
|
+
...(diagnostic.stem ? { stem: diagnostic.stem } : {}),
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
245
416
|
const discoverySummary = RecipesDiscovery.summarizeDiscovery(discovered);
|
|
246
417
|
const summary = {
|
|
247
418
|
...discoverySummary,
|
|
419
|
+
...(registryStatus
|
|
420
|
+
? {
|
|
421
|
+
...registryStatus,
|
|
422
|
+
watched_root: "~/.pi/agent/recipes",
|
|
423
|
+
}
|
|
424
|
+
: {}),
|
|
248
425
|
active: Array.isArray(discoverySummary.active)
|
|
249
426
|
? discoverySummary.active.map((entry) => ({
|
|
250
427
|
...(entry as Record<string, unknown>),
|
|
@@ -253,6 +430,7 @@ function inspectRecipes(
|
|
|
253
430
|
: discoverySummary.active,
|
|
254
431
|
skill_recipe_components: skillRecipeComponents,
|
|
255
432
|
skill_recipe_component_diagnostics: skillRecipeComponentDiagnostics,
|
|
433
|
+
skill_recipe_catalog_partial: skillInventory.partial,
|
|
256
434
|
active_skill_recipe_identities: Object.keys(skillRecipeNamespaces).sort(),
|
|
257
435
|
skill_recipe_namespace_diagnostics: Object.entries(skillRecipeNamespaces)
|
|
258
436
|
.filter(([, roots]) => roots.length > 1)
|
|
@@ -366,18 +544,63 @@ function inspectTool(
|
|
|
366
544
|
throw new Error("inspect tool:<name> supports view=status or view=schema.");
|
|
367
545
|
}
|
|
368
546
|
const tool = deps.getTool?.(name);
|
|
369
|
-
|
|
547
|
+
const status = deps.getToolStatus?.(name);
|
|
548
|
+
if (!tool && !status) {
|
|
549
|
+
throw new Error(
|
|
550
|
+
`Registered tool not found: ${name}. Next: inspect target=recipes view=doctor; do not substitute spawn for tool invocation.`,
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
if (view === "schema" && !tool) {
|
|
554
|
+
throw new Error(
|
|
555
|
+
`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.`,
|
|
556
|
+
);
|
|
557
|
+
}
|
|
370
558
|
return {
|
|
371
559
|
name,
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
560
|
+
...(view === "status" ? status : {}),
|
|
561
|
+
...(tool
|
|
562
|
+
? {
|
|
563
|
+
description: tool.description,
|
|
564
|
+
parameters: tool.parameters,
|
|
565
|
+
promptSnippet: tool.promptSnippet,
|
|
566
|
+
}
|
|
567
|
+
: {}),
|
|
375
568
|
};
|
|
376
569
|
}
|
|
377
570
|
|
|
378
571
|
function compactResult(target: string, view: string, details: Record<string, unknown>): string {
|
|
379
572
|
if (target === "runtime") return compactRuntime(view, details);
|
|
573
|
+
if (target === "recipes" && typeof details.identity === "string") {
|
|
574
|
+
const lines = [
|
|
575
|
+
`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)}`,
|
|
576
|
+
...(details.source_location
|
|
577
|
+
? [`source=${String(details.source_location)}`]
|
|
578
|
+
: []),
|
|
579
|
+
`resolution_generation=${String(details.resolution_generation)}`,
|
|
580
|
+
...(details.rejected_reason
|
|
581
|
+
? [`rejected_reason=${String(details.rejected_reason)}`]
|
|
582
|
+
: []),
|
|
583
|
+
...((details.next_actions as unknown[] | undefined) ?? []).map(
|
|
584
|
+
(action) => `next=${String(action)}`,
|
|
585
|
+
),
|
|
586
|
+
];
|
|
587
|
+
return `\n${lines.join("\n")}`;
|
|
588
|
+
}
|
|
380
589
|
if (target === "recipes") return ToolsResponse.compactRecipeRegistry(details);
|
|
590
|
+
if (target.startsWith("tool:") && view === "status") {
|
|
591
|
+
const required = Array.isArray(details.required_args)
|
|
592
|
+
? details.required_args.map(String).join(",") || "none"
|
|
593
|
+
: "unknown";
|
|
594
|
+
const optional = Array.isArray(details.optional_args)
|
|
595
|
+
? details.optional_args.map(String).join(",") || "none"
|
|
596
|
+
: "unknown";
|
|
597
|
+
const next = Array.isArray(details.next_actions) && details.next_actions.length > 0
|
|
598
|
+
? String(details.next_actions[0])
|
|
599
|
+
: details.callable_now === true
|
|
600
|
+
? `call tool ${target.slice(5)}`
|
|
601
|
+
: `register_tool name=${target.slice(5)} update=true`;
|
|
602
|
+
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}`;
|
|
603
|
+
}
|
|
381
604
|
if (target.startsWith("tool:")) return `\ntool=${target.slice(5)} view=${view}`;
|
|
382
605
|
return `\nrun=${target.slice(4)} view=${view}`;
|
|
383
606
|
}
|
|
@@ -392,6 +615,9 @@ export function createInspectToolDefinition<TContext = unknown>(
|
|
|
392
615
|
"Inspect a Run's Recipe, Trace, or Control evidence, or runtime/recipe/tool diagnostics.",
|
|
393
616
|
parameters: Schema.objectSchema(
|
|
394
617
|
{
|
|
618
|
+
identity: Schema.stringSchema(
|
|
619
|
+
"Optional canonical <skill>/<recipe> identity for focused Recipe doctor diagnosis.",
|
|
620
|
+
),
|
|
395
621
|
lines: Schema.stringSchema("Bounded item count for Trace or recent Controls."),
|
|
396
622
|
source: Schema.stringSchema(
|
|
397
623
|
"Optional Trace source: all, lifecycle, control, process, agent, artifact, or runtime.",
|
|
@@ -419,7 +645,7 @@ export function createInspectToolDefinition<TContext = unknown>(
|
|
|
419
645
|
if (target === "runtime") {
|
|
420
646
|
details = inspectRuntime(view, input, ctx, deps);
|
|
421
647
|
} else if (target === "recipes") {
|
|
422
|
-
details = inspectRecipes(view, deps, ctx);
|
|
648
|
+
details = inspectRecipes(view, input, deps, ctx);
|
|
423
649
|
} else {
|
|
424
650
|
const run = parseRunTarget(target);
|
|
425
651
|
if (run) details = inspectRun(run, view, input, ctx, deps);
|