@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.
- package/BACKLOG.md +582 -0
- package/CHANGELOG.md +9 -0
- package/README.md +1 -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 +1 -1
- package/dist/lib/prompts.js +2 -2
- package/dist/lib/recipes-context.d.ts +12 -3
- package/dist/lib/recipes-context.js +28 -4
- package/dist/lib/recipes-discovery.d.ts +18 -0
- package/dist/lib/recipes-discovery.js +95 -22
- package/dist/lib/recipes-references.d.ts +18 -0
- package/dist/lib/recipes-references.js +120 -33
- package/dist/lib/registry.d.ts +14 -1
- package/dist/lib/registry.js +110 -87
- package/dist/lib/runtime.d.ts +30 -3
- package/dist/lib/runtime.js +73 -9
- package/dist/lib/tools-inspect.d.ts +3 -0
- package/dist/lib/tools-inspect.js +40 -20
- package/dist/lib/tools-local.d.ts +2 -2
- package/dist/lib/tools-local.js +5 -2
- package/dist/lib/tools-response.js +2 -0
- 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/skills/actors/SKILL.md +3 -3
- package/docs/template-recipes.md +3 -1
- package/docs/tool-registry.md +9 -3
- 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 +2 -2
- package/lib/recipes-context.ts +49 -4
- package/lib/recipes-discovery.ts +164 -24
- package/lib/recipes-references.ts +163 -39
- package/lib/registry.ts +185 -94
- package/lib/runtime.ts +108 -12
- package/lib/tools-inspect.ts +57 -23
- package/lib/tools-local.ts +9 -3
- package/lib/tools-response.ts +2 -0
- package/lib/tools-spawn.ts +3 -2
- package/lib/tools.ts +4 -1
- package/package.json +1 -1
- 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 {
|
|
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) =>
|
|
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
|
|
143
|
-
if (!
|
|
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
|
|
152
|
-
name,
|
|
172
|
+
const authoredRecipe = buildAuthoredRecipe(
|
|
153
173
|
{
|
|
154
174
|
...input,
|
|
155
|
-
description:
|
|
156
|
-
|
|
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
|
-
|
|
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,
|
|
163
|
-
promoted
|
|
164
|
-
|
|
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
|
-
|
|
224
|
+
name: string,
|
|
225
|
+
recipe: Record<string, unknown>,
|
|
189
226
|
): string {
|
|
190
|
-
const path = getToolRecipePath(deps,
|
|
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
|
-
): {
|
|
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
|
-
|
|
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
|
|
289
|
-
|
|
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
|
-
|
|
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 === ""
|
|
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
|
|
321
|
-
|
|
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
|
|
420
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
509
|
+
`Tool registration transaction failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
427
510
|
),
|
|
428
511
|
);
|
|
429
512
|
}
|
|
430
|
-
|
|
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
|
-
`${
|
|
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(
|
|
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):
|
|
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
|
|
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)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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(
|
|
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
|
-
{
|
|
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;
|