@llblab/pi-actors 0.22.3 → 0.22.4
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 +456 -81
- package/CHANGELOG.md +16 -0
- package/dist/index.js +1 -1
- package/dist/lib/actor-rooms.d.ts +8 -2
- package/dist/lib/actor-rooms.js +58 -9
- package/dist/lib/async-runs.js +32 -10
- package/dist/lib/observability.js +28 -10
- package/dist/lib/recipe-discovery.js +121 -16
- package/dist/lib/recipe-references.d.ts +1 -0
- package/dist/lib/recipe-references.js +40 -1
- package/dist/lib/registry.js +5 -1
- package/dist/lib/tools.js +25 -8
- package/index.ts +1 -1
- package/lib/actor-rooms.ts +79 -11
- package/lib/async-runs.ts +124 -88
- package/lib/observability.ts +85 -24
- package/lib/recipe-discovery.ts +186 -41
- package/lib/recipe-references.ts +88 -14
- package/lib/registry.ts +7 -1
- package/lib/tools.ts +42 -15
- package/package.json +1 -1
- package/skills/actors/SKILL.md +1 -1
- package/skills/swarm/SKILL.md +1 -1
package/lib/observability.ts
CHANGED
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
basename,
|
|
10
|
+
dirname,
|
|
11
|
+
isAbsolute,
|
|
12
|
+
join,
|
|
13
|
+
relative,
|
|
14
|
+
resolve,
|
|
15
|
+
} from "node:path";
|
|
9
16
|
|
|
10
17
|
import * as AsyncRuns from "./async-runs.ts";
|
|
11
18
|
import * as Paths from "./paths.ts";
|
|
@@ -229,13 +236,25 @@ export function summarizeRuns(
|
|
|
229
236
|
const descendantSubagents = processSubagentsByRun.get(run.run) ?? 0;
|
|
230
237
|
return descendantSubagents > 0 ? { ...run, descendantSubagents } : run;
|
|
231
238
|
});
|
|
232
|
-
const runningRuns = runsWithDescendants.filter(
|
|
239
|
+
const runningRuns = runsWithDescendants.filter(
|
|
240
|
+
(run) => run.status === "running",
|
|
241
|
+
);
|
|
233
242
|
const running = runningRuns.length;
|
|
234
|
-
const done = runsWithDescendants.filter(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
243
|
+
const done = runsWithDescendants.filter(
|
|
244
|
+
(run) => run.status === "done",
|
|
245
|
+
).length;
|
|
246
|
+
const exited = runsWithDescendants.filter(
|
|
247
|
+
(run) => run.status === "exited",
|
|
248
|
+
).length;
|
|
249
|
+
const failed = runsWithDescendants.filter(
|
|
250
|
+
(run) => run.status === "failed",
|
|
251
|
+
).length;
|
|
252
|
+
const cancelled = runsWithDescendants.filter(
|
|
253
|
+
(run) => run.status === "cancelled",
|
|
254
|
+
).length;
|
|
255
|
+
const killed = runsWithDescendants.filter(
|
|
256
|
+
(run) => run.status === "killed",
|
|
257
|
+
).length;
|
|
239
258
|
const progressSubagents = runningRuns.reduce(
|
|
240
259
|
(sum, run) => sum + Math.max(1, Math.floor(run.activeSubagents ?? 0)),
|
|
241
260
|
0,
|
|
@@ -244,7 +263,10 @@ export function summarizeRuns(
|
|
|
244
263
|
(sum, count) => sum + count,
|
|
245
264
|
0,
|
|
246
265
|
);
|
|
247
|
-
const runningSubagents = Math.max(
|
|
266
|
+
const runningSubagents = Math.max(
|
|
267
|
+
progressSubagents,
|
|
268
|
+
running + processSubagents,
|
|
269
|
+
);
|
|
248
270
|
return {
|
|
249
271
|
cancelled,
|
|
250
272
|
done,
|
|
@@ -278,11 +300,16 @@ function getProcCommand(pid: string): string {
|
|
|
278
300
|
return (readProcFile(`/proc/${pid}/cmdline`) ?? "").replaceAll("\0", " ");
|
|
279
301
|
}
|
|
280
302
|
|
|
281
|
-
function getRunningRunPidMap(
|
|
303
|
+
function getRunningRunPidMap(
|
|
304
|
+
stateRoot: string,
|
|
305
|
+
ownerId?: string,
|
|
306
|
+
): Map<string, string> {
|
|
282
307
|
const pids = new Map<string, string>();
|
|
283
308
|
for (const run of summarizeRunsWithoutSubagents(stateRoot, ownerId).runs) {
|
|
284
309
|
if (run.status !== "running") continue;
|
|
285
|
-
const status = AsyncRuns.getRunStatus(
|
|
310
|
+
const status = AsyncRuns.getRunStatus(
|
|
311
|
+
run.stateDir ?? join(stateRoot, run.run),
|
|
312
|
+
);
|
|
286
313
|
const pid = Number(status.pid || 0);
|
|
287
314
|
if (pid > 0) pids.set(String(pid), run.run);
|
|
288
315
|
}
|
|
@@ -420,7 +447,10 @@ export function findRunRetirementCandidates(
|
|
|
420
447
|
return summary.runs
|
|
421
448
|
.map((run) => {
|
|
422
449
|
const activeSubagents = Math.max(0, Math.floor(run.activeSubagents ?? 0));
|
|
423
|
-
const descendantSubagents = Math.max(
|
|
450
|
+
const descendantSubagents = Math.max(
|
|
451
|
+
0,
|
|
452
|
+
Math.floor(run.descendantSubagents ?? 0),
|
|
453
|
+
);
|
|
424
454
|
const childRuns = run.stateDir
|
|
425
455
|
? summary.runs.filter(
|
|
426
456
|
(child) =>
|
|
@@ -429,7 +459,9 @@ export function findRunRetirementCandidates(
|
|
|
429
459
|
isNestedStateDir(run.stateDir!, child.stateDir),
|
|
430
460
|
)
|
|
431
461
|
: [];
|
|
432
|
-
const runningChildRuns = childRuns.filter(
|
|
462
|
+
const runningChildRuns = childRuns.filter(
|
|
463
|
+
(child) => child.status === "running",
|
|
464
|
+
).length;
|
|
433
465
|
return {
|
|
434
466
|
activeSubagents,
|
|
435
467
|
childRuns: childRuns.length,
|
|
@@ -441,7 +473,9 @@ export function findRunRetirementCandidates(
|
|
|
441
473
|
!run.terminalHandled &&
|
|
442
474
|
activeSubagents + descendantSubagents + runningChildRuns <= 0,
|
|
443
475
|
run,
|
|
444
|
-
terminalChildRuns: childRuns.filter((child) =>
|
|
476
|
+
terminalChildRuns: childRuns.filter((child) =>
|
|
477
|
+
TERMINAL.has(child.status),
|
|
478
|
+
).length,
|
|
445
479
|
};
|
|
446
480
|
})
|
|
447
481
|
.filter((item) => item.ready)
|
|
@@ -462,7 +496,11 @@ export async function executeRunRetirements(
|
|
|
462
496
|
const results: RunRetirementExecution[] = [];
|
|
463
497
|
for (const candidate of findRunRetirementCandidates(summary)) {
|
|
464
498
|
if (options.attempted?.has(candidate.stateDir)) {
|
|
465
|
-
results.push({
|
|
499
|
+
results.push({
|
|
500
|
+
action: "skip",
|
|
501
|
+
run: candidate.run,
|
|
502
|
+
stateDir: candidate.stateDir,
|
|
503
|
+
});
|
|
466
504
|
continue;
|
|
467
505
|
}
|
|
468
506
|
options.attempted?.add(candidate.stateDir);
|
|
@@ -472,12 +510,18 @@ export async function executeRunRetirements(
|
|
|
472
510
|
`Retiring actor ${candidate.run} after child runs reached terminal state`,
|
|
473
511
|
"info",
|
|
474
512
|
);
|
|
475
|
-
results.push({
|
|
513
|
+
results.push({
|
|
514
|
+
action: "stop",
|
|
515
|
+
run: candidate.run,
|
|
516
|
+
stateDir: candidate.stateDir,
|
|
517
|
+
});
|
|
476
518
|
continue;
|
|
477
519
|
} catch (error) {
|
|
478
520
|
try {
|
|
479
521
|
const cancelResult = options.cancelRun(candidate);
|
|
480
|
-
const cancelled = Boolean(
|
|
522
|
+
const cancelled = Boolean(
|
|
523
|
+
(cancelResult as { cancelled?: unknown }).cancelled,
|
|
524
|
+
);
|
|
481
525
|
options.notify?.(
|
|
482
526
|
cancelled
|
|
483
527
|
? `Retiring actor ${candidate.run} by cancellation after graceful stop failed`
|
|
@@ -486,13 +530,23 @@ export async function executeRunRetirements(
|
|
|
486
530
|
);
|
|
487
531
|
results.push({
|
|
488
532
|
action: cancelled ? "cancel" : "skip",
|
|
489
|
-
...(cancelled
|
|
533
|
+
...(cancelled
|
|
534
|
+
? {}
|
|
535
|
+
: {
|
|
536
|
+
error: error instanceof Error ? error.message : String(error),
|
|
537
|
+
}),
|
|
490
538
|
run: candidate.run,
|
|
491
539
|
stateDir: candidate.stateDir,
|
|
492
540
|
});
|
|
493
541
|
} catch (cancelError) {
|
|
494
|
-
const message =
|
|
495
|
-
|
|
542
|
+
const message =
|
|
543
|
+
cancelError instanceof Error
|
|
544
|
+
? cancelError.message
|
|
545
|
+
: String(cancelError);
|
|
546
|
+
options.notify?.(
|
|
547
|
+
`Actor retirement failed for ${candidate.run}: ${message}`,
|
|
548
|
+
"error",
|
|
549
|
+
);
|
|
496
550
|
results.push({
|
|
497
551
|
action: "failed",
|
|
498
552
|
error: message,
|
|
@@ -505,13 +559,20 @@ export async function executeRunRetirements(
|
|
|
505
559
|
return results;
|
|
506
560
|
}
|
|
507
561
|
|
|
562
|
+
function runObservationKey(
|
|
563
|
+
run: Pick<RunObservation, "run" | "stateDir">,
|
|
564
|
+
): string {
|
|
565
|
+
return run.stateDir ?? run.run;
|
|
566
|
+
}
|
|
567
|
+
|
|
508
568
|
export function detectRunTransitions(
|
|
509
569
|
previous: Map<string, RunObservedStatus>,
|
|
510
570
|
summary: RunSummary,
|
|
511
571
|
): RunTransition[] {
|
|
512
572
|
const transitions: RunTransition[] = [];
|
|
513
573
|
for (const run of summary.runs) {
|
|
514
|
-
const
|
|
574
|
+
const key = runObservationKey(run);
|
|
575
|
+
const old = previous.get(key);
|
|
515
576
|
if (old && old !== run.status && TERMINAL.has(run.status)) {
|
|
516
577
|
transitions.push({
|
|
517
578
|
from: old,
|
|
@@ -525,7 +586,7 @@ export function detectRunTransitions(
|
|
|
525
586
|
...(run.tool ? { tool: run.tool } : {}),
|
|
526
587
|
});
|
|
527
588
|
}
|
|
528
|
-
previous.set(
|
|
589
|
+
previous.set(key, run.status);
|
|
529
590
|
}
|
|
530
591
|
return transitions;
|
|
531
592
|
}
|
|
@@ -599,12 +660,12 @@ export function pruneRunObservationState(
|
|
|
599
660
|
summary: RunSummary,
|
|
600
661
|
terminalRuns: Iterable<string> = [],
|
|
601
662
|
): void {
|
|
602
|
-
const activeRuns = new Set(summary.runs.map((run) => run
|
|
663
|
+
const activeRuns = new Set(summary.runs.map((run) => runObservationKey(run)));
|
|
603
664
|
const terminalRunSet = new Set(terminalRuns);
|
|
604
665
|
const terminalLineKeys = new Set(
|
|
605
666
|
summary.runs
|
|
606
|
-
.filter((run) => terminalRunSet.has(run
|
|
607
|
-
.map((run) => run
|
|
667
|
+
.filter((run) => terminalRunSet.has(runObservationKey(run)))
|
|
668
|
+
.map((run) => runObservationKey(run)),
|
|
608
669
|
);
|
|
609
670
|
const activeLineKeys = new Set(
|
|
610
671
|
summary.runs.map((run) => run.stateDir ?? run.run),
|
package/lib/recipe-discovery.ts
CHANGED
|
@@ -8,9 +8,9 @@ import { createHash } from "node:crypto";
|
|
|
8
8
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
|
|
11
|
+
import * as CommandTemplates from "./command-templates.ts";
|
|
11
12
|
import type { RegisteredTool } from "./config.ts";
|
|
12
13
|
import type { TemplateRecipeConfig } from "./recipe-references.ts";
|
|
13
|
-
import * as CommandTemplates from "./command-templates.ts";
|
|
14
14
|
import * as RecipeReferences from "./recipe-references.ts";
|
|
15
15
|
import * as Schema from "./schema.ts";
|
|
16
16
|
|
|
@@ -61,18 +61,28 @@ function assertToolSafeRepeatConfig(
|
|
|
61
61
|
argTypes: Record<string, { kind: string }>,
|
|
62
62
|
defaults: Record<string, unknown>,
|
|
63
63
|
): void {
|
|
64
|
-
if (typeof config === "string" || config === undefined || config === null)
|
|
64
|
+
if (typeof config === "string" || config === undefined || config === null)
|
|
65
|
+
return;
|
|
65
66
|
if (Array.isArray(config)) {
|
|
66
|
-
for (const step of config)
|
|
67
|
+
for (const step of config)
|
|
68
|
+
assertToolSafeRepeatConfig(step, argTypes, defaults);
|
|
67
69
|
return;
|
|
68
70
|
}
|
|
69
71
|
if (typeof config !== "object") return;
|
|
70
|
-
const node = config as {
|
|
72
|
+
const node = config as {
|
|
73
|
+
repeat?: unknown;
|
|
74
|
+
template?: unknown;
|
|
75
|
+
recover?: unknown;
|
|
76
|
+
};
|
|
71
77
|
if (typeof node.repeat === "string") {
|
|
72
78
|
const trimmed = node.repeat.trim();
|
|
73
79
|
if (!/^\d+$/.test(trimmed)) {
|
|
74
80
|
const match = trimmed.match(/^\{?([A-Za-z_][A-Za-z0-9_-]*)\.length\}?$/);
|
|
75
|
-
if (
|
|
81
|
+
if (
|
|
82
|
+
!match ||
|
|
83
|
+
(argTypes[match[1]]?.kind !== "array" &&
|
|
84
|
+
!Array.isArray(defaults[match[1]]))
|
|
85
|
+
)
|
|
76
86
|
throw new Error(
|
|
77
87
|
"Command template repeat must be a positive integer or {array.length} with an array argument/default.",
|
|
78
88
|
);
|
|
@@ -92,23 +102,30 @@ function listRecipeFiles(root: string): string[] {
|
|
|
92
102
|
entry.name !== "legacy-tool-registry-migration-report.json",
|
|
93
103
|
)
|
|
94
104
|
.map((entry) => join(root, entry.name))
|
|
95
|
-
.sort(
|
|
105
|
+
.sort(
|
|
106
|
+
(a, b) =>
|
|
107
|
+
a
|
|
108
|
+
.replace(/\.md$/, ".json")
|
|
109
|
+
.localeCompare(b.replace(/\.md$/, ".json")) ||
|
|
110
|
+
(a.endsWith(".json") ? -1 : 1),
|
|
111
|
+
);
|
|
96
112
|
}
|
|
97
113
|
|
|
98
114
|
function getRecipeConfigDiagnostics(
|
|
99
115
|
file: string,
|
|
100
116
|
config: TemplateRecipeConfig | undefined,
|
|
101
117
|
): string[] {
|
|
102
|
-
if (!config)
|
|
118
|
+
if (!config) {
|
|
119
|
+
const reason = RecipeReferences.diagnoseRawRecipeConfigFailure(file);
|
|
120
|
+
return [`Invalid recipe: ${file}${reason ? `: ${reason}` : ""}`];
|
|
121
|
+
}
|
|
103
122
|
const commandTemplateConfig =
|
|
104
123
|
typeof config.template === "object" && config.template !== null
|
|
105
124
|
? config.template
|
|
106
125
|
: config;
|
|
107
126
|
return CommandTemplates.getCommandTemplateWarnings(
|
|
108
127
|
commandTemplateConfig as CommandTemplates.CommandTemplateConfig,
|
|
109
|
-
).map(
|
|
110
|
-
(warning) => `Recipe ${file}: ${warning}`,
|
|
111
|
-
);
|
|
128
|
+
).map((warning) => `Recipe ${file}: ${warning}`);
|
|
112
129
|
}
|
|
113
130
|
|
|
114
131
|
function readDiscoveredRecipe(
|
|
@@ -158,13 +175,23 @@ function readDiscoveredRecipe(
|
|
|
158
175
|
}
|
|
159
176
|
}
|
|
160
177
|
|
|
161
|
-
function filesForSource(
|
|
162
|
-
|
|
163
|
-
|
|
178
|
+
function filesForSource(source: RecipeDiscoverySource): Array<{
|
|
179
|
+
root: string;
|
|
180
|
+
file: string;
|
|
181
|
+
defaultTool: boolean;
|
|
182
|
+
mutableUsage: boolean;
|
|
183
|
+
}> {
|
|
164
184
|
const defaultTool = source.defaultTool === true;
|
|
165
185
|
const mutableUsage = source.mutableUsage === true;
|
|
166
186
|
if (source.file)
|
|
167
|
-
return [
|
|
187
|
+
return [
|
|
188
|
+
{
|
|
189
|
+
root: source.root ?? source.file,
|
|
190
|
+
file: source.file,
|
|
191
|
+
defaultTool,
|
|
192
|
+
mutableUsage,
|
|
193
|
+
},
|
|
194
|
+
];
|
|
168
195
|
return source.root
|
|
169
196
|
? listRecipeFiles(source.root).map((file) => ({
|
|
170
197
|
root: source.root!,
|
|
@@ -226,7 +253,9 @@ export function discoverRecipeSources(
|
|
|
226
253
|
bucket.sort(
|
|
227
254
|
(a, b) =>
|
|
228
255
|
a.priority - b.priority ||
|
|
229
|
-
a.path
|
|
256
|
+
a.path
|
|
257
|
+
.replace(/\.md$/, ".json")
|
|
258
|
+
.localeCompare(b.path.replace(/\.md$/, ".json")) ||
|
|
230
259
|
(a.path.endsWith(".json") ? -1 : 1),
|
|
231
260
|
);
|
|
232
261
|
const winner = bucket[0];
|
|
@@ -262,14 +291,18 @@ export function discoverRecipes(roots: string[]): RecipeDiscoveryResult {
|
|
|
262
291
|
return discoverRecipeSources(roots.map((root) => ({ root })));
|
|
263
292
|
}
|
|
264
293
|
|
|
265
|
-
function recipeUsage(
|
|
294
|
+
function recipeUsage(
|
|
295
|
+
config: TemplateRecipeConfig | undefined,
|
|
296
|
+
): Record<string, unknown> | undefined {
|
|
266
297
|
const usage = (config as { usage?: unknown } | undefined)?.usage;
|
|
267
298
|
return usage && typeof usage === "object" && !Array.isArray(usage)
|
|
268
299
|
? (usage as Record<string, unknown>)
|
|
269
300
|
: undefined;
|
|
270
301
|
}
|
|
271
302
|
|
|
272
|
-
function cleanupRecommendation(
|
|
303
|
+
function cleanupRecommendation(
|
|
304
|
+
entry: DiscoveredRecipe,
|
|
305
|
+
): Record<string, unknown> | undefined {
|
|
273
306
|
if (entry.invalid) {
|
|
274
307
|
return {
|
|
275
308
|
id: entry.id,
|
|
@@ -309,7 +342,13 @@ function cleanupRecommendation(entry: DiscoveredRecipe): Record<string, unknown>
|
|
|
309
342
|
id: entry.id,
|
|
310
343
|
path: entry.path,
|
|
311
344
|
reason: "user recipe is a component, not an active tool",
|
|
312
|
-
actions: [
|
|
345
|
+
actions: [
|
|
346
|
+
"keep component",
|
|
347
|
+
"move into tool root",
|
|
348
|
+
"merge",
|
|
349
|
+
"delete",
|
|
350
|
+
"archive",
|
|
351
|
+
],
|
|
313
352
|
};
|
|
314
353
|
}
|
|
315
354
|
if (entry.shadows.length > 0) {
|
|
@@ -342,9 +381,78 @@ export function createRecipeIntegrityManifest(
|
|
|
342
381
|
tool: entry.tool,
|
|
343
382
|
};
|
|
344
383
|
})
|
|
345
|
-
.sort(
|
|
346
|
-
|
|
384
|
+
.sort((a, b) => a.id.localeCompare(b.id) || a.path.localeCompare(b.path));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function diagnosticSeverity(message: string): "info" | "warning" | "error" {
|
|
388
|
+
if (
|
|
389
|
+
/invalid|failed to load|not found|cyclic|exceeds|must define|repeat must/i.test(
|
|
390
|
+
message,
|
|
391
|
+
)
|
|
392
|
+
) {
|
|
393
|
+
return "error";
|
|
394
|
+
}
|
|
395
|
+
if (
|
|
396
|
+
/world-writable|group-writable|invokes bash|eval|destructive|unsafe/i.test(
|
|
397
|
+
message,
|
|
398
|
+
)
|
|
399
|
+
) {
|
|
400
|
+
return "warning";
|
|
401
|
+
}
|
|
402
|
+
return "info";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function diagnosticSuggestedAction(message: string): string {
|
|
406
|
+
if (/must define template/i.test(message))
|
|
407
|
+
return "add a template field or remove the recipe";
|
|
408
|
+
if (/JSON|Expected|parse/i.test(message))
|
|
409
|
+
return "fix recipe syntax or archive the file";
|
|
410
|
+
if (/Markdown recipe/i.test(message))
|
|
411
|
+
return "fix frontmatter and add a fenced template or recipe block";
|
|
412
|
+
if (/cyclic/i.test(message)) return "break the import cycle";
|
|
413
|
+
if (/exceeds.*size/i.test(message))
|
|
414
|
+
return "split large prompt or data into separate files";
|
|
415
|
+
if (/repeat must/i.test(message))
|
|
416
|
+
return "use a positive repeat count or an array-typed repeat source";
|
|
417
|
+
if (/world-writable|group-writable/i.test(message))
|
|
418
|
+
return "tighten recipe root permissions";
|
|
419
|
+
if (/invokes bash/i.test(message))
|
|
420
|
+
return "audit the trusted shell boundary or move details to recipe doctor";
|
|
421
|
+
if (/shadows/i.test(message))
|
|
422
|
+
return "confirm the active override or rename one recipe";
|
|
423
|
+
if (/disabled/i.test(message))
|
|
424
|
+
return "keep disabled intentionally or delete/archive the file";
|
|
425
|
+
return "inspect the recipe and fix or archive it if unexpected";
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function diagnosticDetails(
|
|
429
|
+
result: RecipeDiscoveryResult,
|
|
430
|
+
): Array<Record<string, unknown>> {
|
|
431
|
+
const details: Array<Record<string, unknown>> = [];
|
|
432
|
+
const seen = new Set<string>();
|
|
433
|
+
const push = (message: string, entry?: DiscoveredRecipe): void => {
|
|
434
|
+
const key = `${entry?.path ?? "root"}\n${message}`;
|
|
435
|
+
if (seen.has(key)) return;
|
|
436
|
+
seen.add(key);
|
|
437
|
+
details.push({
|
|
438
|
+
...(entry ? { id: entry.id, path: entry.path } : {}),
|
|
439
|
+
action: diagnosticSuggestedAction(message),
|
|
440
|
+
message,
|
|
441
|
+
severity: diagnosticSeverity(message),
|
|
442
|
+
});
|
|
443
|
+
};
|
|
444
|
+
for (const message of result.diagnostics) push(message);
|
|
445
|
+
for (const entry of result.entries) {
|
|
446
|
+
for (const message of entry.diagnostics) push(message, entry);
|
|
447
|
+
}
|
|
448
|
+
return details.sort((a, b) => {
|
|
449
|
+
const rank = { error: 0, warning: 1, info: 2 } as const;
|
|
450
|
+
return (
|
|
451
|
+
rank[a.severity as keyof typeof rank] -
|
|
452
|
+
rank[b.severity as keyof typeof rank] ||
|
|
453
|
+
String(a.message).localeCompare(String(b.message))
|
|
347
454
|
);
|
|
455
|
+
});
|
|
348
456
|
}
|
|
349
457
|
|
|
350
458
|
function recommendationForEntry(
|
|
@@ -362,29 +470,49 @@ function recommendationForEntry(
|
|
|
362
470
|
return recommendation;
|
|
363
471
|
}
|
|
364
472
|
|
|
365
|
-
export function summarizeDiscovery(
|
|
473
|
+
export function summarizeDiscovery(
|
|
474
|
+
result: RecipeDiscoveryResult,
|
|
475
|
+
): Record<string, unknown> {
|
|
366
476
|
const recommendations = result.entries
|
|
367
|
-
.map((entry) =>
|
|
477
|
+
.map((entry) =>
|
|
478
|
+
recommendationForEntry(entry, result.active.get(entry.id)?.path),
|
|
479
|
+
)
|
|
368
480
|
.filter((entry): entry is Record<string, unknown> => Boolean(entry))
|
|
369
|
-
.sort(
|
|
481
|
+
.sort(
|
|
482
|
+
(a, b) =>
|
|
483
|
+
String(a.id).localeCompare(String(b.id)) ||
|
|
484
|
+
String(a.path).localeCompare(String(b.path)),
|
|
485
|
+
);
|
|
370
486
|
return {
|
|
371
|
-
active: [...result.active.values()]
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
487
|
+
active: [...result.active.values()]
|
|
488
|
+
.map((entry) => ({
|
|
489
|
+
id: entry.id,
|
|
490
|
+
path: entry.path,
|
|
491
|
+
description: entry.config?.description,
|
|
492
|
+
tool: entry.tool,
|
|
493
|
+
disabled: entry.disabled,
|
|
494
|
+
invalid: entry.invalid,
|
|
495
|
+
shadows: entry.shadows,
|
|
496
|
+
...(recipeUsage(entry.config)
|
|
497
|
+
? { usage: recipeUsage(entry.config) }
|
|
498
|
+
: {}),
|
|
499
|
+
}))
|
|
500
|
+
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
381
501
|
shadowed: result.entries
|
|
382
502
|
.filter((entry) => entry.shadowed)
|
|
383
|
-
.map((entry) => ({
|
|
503
|
+
.map((entry) => ({
|
|
504
|
+
id: entry.id,
|
|
505
|
+
path: entry.path,
|
|
506
|
+
shadowedBy: result.active.get(entry.id)?.path,
|
|
507
|
+
}))
|
|
384
508
|
.sort((a, b) => a.id.localeCompare(b.id) || a.path.localeCompare(b.path)),
|
|
385
509
|
invalid: result.entries
|
|
386
510
|
.filter((entry) => entry.invalid)
|
|
387
|
-
.map((entry) => ({
|
|
511
|
+
.map((entry) => ({
|
|
512
|
+
id: entry.id,
|
|
513
|
+
path: entry.path,
|
|
514
|
+
diagnostics: entry.diagnostics,
|
|
515
|
+
}))
|
|
388
516
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
389
517
|
disabled: result.entries
|
|
390
518
|
.filter((entry) => entry.disabled)
|
|
@@ -392,12 +520,16 @@ export function summarizeDiscovery(result: RecipeDiscoveryResult): Record<string
|
|
|
392
520
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
393
521
|
recommendations,
|
|
394
522
|
diagnostics: result.diagnostics,
|
|
523
|
+
diagnostic_details: diagnosticDetails(result),
|
|
395
524
|
integrity_manifest: createRecipeIntegrityManifest(result),
|
|
396
525
|
};
|
|
397
526
|
}
|
|
398
527
|
|
|
399
|
-
export function toRegisteredTool(
|
|
400
|
-
|
|
528
|
+
export function toRegisteredTool(
|
|
529
|
+
entry: DiscoveredRecipe,
|
|
530
|
+
): RegisteredTool | undefined {
|
|
531
|
+
if (!entry.tool || entry.invalid || entry.disabled || !entry.config)
|
|
532
|
+
return undefined;
|
|
401
533
|
const cfg = entry.config;
|
|
402
534
|
const template = entry.path;
|
|
403
535
|
const description = cfg.description ?? `Execute template recipe: ${entry.id}`;
|
|
@@ -407,7 +539,10 @@ export function toRegisteredTool(entry: DiscoveredRecipe): RegisteredTool | unde
|
|
|
407
539
|
? {
|
|
408
540
|
...argTemplate,
|
|
409
541
|
...(cfg.args !== undefined ? { args: cfg.args } : {}),
|
|
410
|
-
defaults: {
|
|
542
|
+
defaults: {
|
|
543
|
+
...(argTemplate.defaults ?? {}),
|
|
544
|
+
...(cfg.defaults ?? {}),
|
|
545
|
+
},
|
|
411
546
|
}
|
|
412
547
|
: { args: cfg.args, defaults: cfg.defaults ?? {}, template: argTemplate };
|
|
413
548
|
const explicitArgTypes = Object.fromEntries(
|
|
@@ -416,7 +551,11 @@ export function toRegisteredTool(entry: DiscoveredRecipe): RegisteredTool | unde
|
|
|
416
551
|
return [parsed.arg, parsed.type];
|
|
417
552
|
}),
|
|
418
553
|
);
|
|
419
|
-
assertToolSafeRepeatConfig(
|
|
554
|
+
assertToolSafeRepeatConfig(
|
|
555
|
+
argTemplateConfig,
|
|
556
|
+
explicitArgTypes,
|
|
557
|
+
cfg.defaults ?? {},
|
|
558
|
+
);
|
|
420
559
|
const argTypes = Schema.getTemplateArgTypes(argTemplateConfig);
|
|
421
560
|
return {
|
|
422
561
|
name: entry.id,
|
|
@@ -425,7 +564,10 @@ export function toRegisteredTool(entry: DiscoveredRecipe): RegisteredTool | unde
|
|
|
425
564
|
recipe: cfg,
|
|
426
565
|
args: Schema.getToolArgNames(argTemplateConfig),
|
|
427
566
|
defaults: Object.fromEntries(
|
|
428
|
-
Object.entries(cfg.defaults ?? {}).map(([key, value]) => [
|
|
567
|
+
Object.entries(cfg.defaults ?? {}).map(([key, value]) => [
|
|
568
|
+
key,
|
|
569
|
+
String(value),
|
|
570
|
+
]),
|
|
429
571
|
),
|
|
430
572
|
...(Object.keys(argTypes).length > 0 ? { argTypes } : {}),
|
|
431
573
|
...(entry.mutableUsage ? { sourcePath: entry.path } : {}),
|
|
@@ -433,7 +575,10 @@ export function toRegisteredTool(entry: DiscoveredRecipe): RegisteredTool | unde
|
|
|
433
575
|
...(cfg.defaults
|
|
434
576
|
? {
|
|
435
577
|
storedDefaults: Object.fromEntries(
|
|
436
|
-
Object.entries(cfg.defaults).map(([key, value]) => [
|
|
578
|
+
Object.entries(cfg.defaults).map(([key, value]) => [
|
|
579
|
+
key,
|
|
580
|
+
String(value),
|
|
581
|
+
]),
|
|
437
582
|
),
|
|
438
583
|
}
|
|
439
584
|
: {}),
|