@llblab/pi-actors 0.22.2 → 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/AGENTS.md +1 -0
- package/BACKLOG.md +456 -81
- package/CHANGELOG.md +25 -25
- 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/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
|
: {}),
|
package/lib/recipe-references.ts
CHANGED
|
@@ -109,7 +109,12 @@ export function resolveRecipePath(
|
|
|
109
109
|
|
|
110
110
|
function isBareRecipeName(value: string): boolean {
|
|
111
111
|
const trimmed = value.trim();
|
|
112
|
-
return
|
|
112
|
+
return (
|
|
113
|
+
Boolean(trimmed) &&
|
|
114
|
+
!trimmed.includes("/") &&
|
|
115
|
+
!trimmed.startsWith("~") &&
|
|
116
|
+
!trimmed.includes("{")
|
|
117
|
+
);
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
function recipeNameFiles(value: string): string[] {
|
|
@@ -118,8 +123,12 @@ function recipeNameFiles(value: string): string[] {
|
|
|
118
123
|
return [`${trimmed}.json`, `${trimmed}.md`];
|
|
119
124
|
}
|
|
120
125
|
|
|
121
|
-
function resolveRecipeImportPath(
|
|
122
|
-
|
|
126
|
+
function resolveRecipeImportPath(
|
|
127
|
+
value: string,
|
|
128
|
+
currentRecipeRoot: string,
|
|
129
|
+
): string {
|
|
130
|
+
if (!isBareRecipeName(value))
|
|
131
|
+
return resolveRecipePath(value, currentRecipeRoot);
|
|
123
132
|
const roots = [
|
|
124
133
|
Paths.getRecipeRoot(),
|
|
125
134
|
currentRecipeRoot,
|
|
@@ -266,11 +275,15 @@ function parseMarkdownFrontmatterObject(
|
|
|
266
275
|
lines: string[],
|
|
267
276
|
): Record<string, unknown> | unknown[] {
|
|
268
277
|
if (lines.every((line) => /^\s*-\s+/.test(line))) {
|
|
269
|
-
return lines.map((line) =>
|
|
278
|
+
return lines.map((line) =>
|
|
279
|
+
parseMarkdownScalar(line.replace(/^\s*-\s+/, "")),
|
|
280
|
+
);
|
|
270
281
|
}
|
|
271
282
|
const result: Record<string, unknown> = {};
|
|
272
283
|
for (let index = 0; index < lines.length; index += 1) {
|
|
273
|
-
const match = lines[index].match(
|
|
284
|
+
const match = lines[index].match(
|
|
285
|
+
/^\s{2}([A-Za-z_][A-Za-z0-9_.-]*):\s*(.*)$/,
|
|
286
|
+
);
|
|
274
287
|
if (!match) continue;
|
|
275
288
|
if (match[2]) {
|
|
276
289
|
result[match[1]] = parseMarkdownScalar(match[2]);
|
|
@@ -331,11 +344,14 @@ function parseMarkdownRecipeConfig(
|
|
|
331
344
|
): Record<string, unknown> | undefined {
|
|
332
345
|
const lines = content.split(/\r?\n/);
|
|
333
346
|
if (lines[0]?.trim() !== "---") return undefined;
|
|
334
|
-
const end = lines.findIndex(
|
|
347
|
+
const end = lines.findIndex(
|
|
348
|
+
(line, index) => index > 0 && line.trim() === "---",
|
|
349
|
+
);
|
|
335
350
|
if (end === -1) return undefined;
|
|
336
351
|
const frontmatter = parseMarkdownFrontmatter(lines.slice(1, end).join("\n"));
|
|
337
352
|
const fence = findMarkdownRecipeFence(lines.slice(end + 1).join("\n"));
|
|
338
|
-
if (!fence)
|
|
353
|
+
if (!fence)
|
|
354
|
+
return Object.hasOwn(frontmatter, "template") ? frontmatter : undefined;
|
|
339
355
|
const text = fence.body.trim();
|
|
340
356
|
if (!text) return undefined;
|
|
341
357
|
if (
|
|
@@ -351,12 +367,50 @@ function parseMarkdownRecipeConfig(
|
|
|
351
367
|
return { ...frontmatter, ...parsed };
|
|
352
368
|
return { ...frontmatter, template: parsed };
|
|
353
369
|
} catch {
|
|
354
|
-
if (fence.info.includes("json") || fence.info.includes("recipe"))
|
|
370
|
+
if (fence.info.includes("json") || fence.info.includes("recipe"))
|
|
371
|
+
return undefined;
|
|
355
372
|
}
|
|
356
373
|
}
|
|
357
374
|
return { ...frontmatter, template: text };
|
|
358
375
|
}
|
|
359
376
|
|
|
377
|
+
export function diagnoseRawRecipeConfigFailure(
|
|
378
|
+
path: string,
|
|
379
|
+
): string | undefined {
|
|
380
|
+
if (!existsSync(path)) return "file does not exist";
|
|
381
|
+
const size = statSync(path).size;
|
|
382
|
+
if (size > MAX_RECIPE_FILE_BYTES) {
|
|
383
|
+
return `file exceeds size limit ${MAX_RECIPE_FILE_BYTES} bytes`;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
const content = readFileSync(path, "utf8");
|
|
387
|
+
if (path.endsWith(".md")) {
|
|
388
|
+
const lines = content.split(/\r?\n/);
|
|
389
|
+
if (lines[0]?.trim() !== "---") {
|
|
390
|
+
return "Markdown recipe must start with frontmatter";
|
|
391
|
+
}
|
|
392
|
+
const end = lines.findIndex(
|
|
393
|
+
(line, index) => index > 0 && line.trim() === "---",
|
|
394
|
+
);
|
|
395
|
+
if (end === -1) return "Markdown recipe frontmatter is not closed";
|
|
396
|
+
const parsed = parseMarkdownRecipeConfig(content);
|
|
397
|
+
if (!parsed)
|
|
398
|
+
return "Markdown recipe has no executable recipe/template fence";
|
|
399
|
+
if (!Object.hasOwn(parsed, "template"))
|
|
400
|
+
return "recipe must define template";
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
const raw = JSON.parse(content) as unknown;
|
|
404
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
405
|
+
return "JSON recipe must be an object";
|
|
406
|
+
}
|
|
407
|
+
if (!Object.hasOwn(raw, "template")) return "recipe must define template";
|
|
408
|
+
return undefined;
|
|
409
|
+
} catch (error) {
|
|
410
|
+
return error instanceof Error ? error.message : String(error);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
360
414
|
export function readRawRecipeConfig(
|
|
361
415
|
path: string,
|
|
362
416
|
): Record<string, unknown> | undefined {
|
|
@@ -593,7 +647,8 @@ function expandImportNodes(
|
|
|
593
647
|
if (typeof value === "string") return value;
|
|
594
648
|
if (Array.isArray(value)) {
|
|
595
649
|
return value.map(
|
|
596
|
-
(item) =>
|
|
650
|
+
(item) =>
|
|
651
|
+
expandImportNodes(item, imports, options) as CommandTemplateConfig,
|
|
597
652
|
);
|
|
598
653
|
}
|
|
599
654
|
const record = value as Record<string, unknown>;
|
|
@@ -614,7 +669,11 @@ function expandImportNodes(
|
|
|
614
669
|
nodeDefaults,
|
|
615
670
|
nodeValues,
|
|
616
671
|
);
|
|
617
|
-
const expanded = applyDefaultsToTemplate(
|
|
672
|
+
const expanded = applyDefaultsToTemplate(
|
|
673
|
+
imported.config.template,
|
|
674
|
+
defaults,
|
|
675
|
+
record,
|
|
676
|
+
);
|
|
618
677
|
return options.includeActorRecipeContext
|
|
619
678
|
? withActorRecipeContext(expanded, {
|
|
620
679
|
alias: imported.alias,
|
|
@@ -672,8 +731,15 @@ export function readResolvedRecipeConfig(
|
|
|
672
731
|
if (!raw || !Object.hasOwn(raw, "template")) return undefined;
|
|
673
732
|
const imports: Record<string, ImportedRecipe> = {};
|
|
674
733
|
for (const [alias, binding] of Object.entries(getRecipeImports(raw))) {
|
|
675
|
-
const importPath = resolveRecipeImportPath(
|
|
676
|
-
|
|
734
|
+
const importPath = resolveRecipeImportPath(
|
|
735
|
+
getImportFrom(binding),
|
|
736
|
+
dirname(path),
|
|
737
|
+
);
|
|
738
|
+
const config = readResolvedRecipeConfig(
|
|
739
|
+
importPath,
|
|
740
|
+
[...stack, path],
|
|
741
|
+
options,
|
|
742
|
+
);
|
|
677
743
|
if (!config) throw new Error(`Recipe import not found: ${alias}`);
|
|
678
744
|
const bindingDefaults =
|
|
679
745
|
typeof binding === "string" ? undefined : binding.defaults;
|
|
@@ -836,8 +902,16 @@ function collectRecipeContextRecords(
|
|
|
836
902
|
};
|
|
837
903
|
const imports = getRecipeImports(raw);
|
|
838
904
|
const children = Object.entries(imports).flatMap(([childAlias, binding]) => {
|
|
839
|
-
const importFile = resolveRecipeImportPath(
|
|
840
|
-
|
|
905
|
+
const importFile = resolveRecipeImportPath(
|
|
906
|
+
getImportFrom(binding),
|
|
907
|
+
dirname(path),
|
|
908
|
+
);
|
|
909
|
+
return collectRecipeContextRecords(
|
|
910
|
+
importFile,
|
|
911
|
+
[...stack, path],
|
|
912
|
+
[...importPath, childAlias],
|
|
913
|
+
childAlias,
|
|
914
|
+
);
|
|
841
915
|
});
|
|
842
916
|
return [record, ...children];
|
|
843
917
|
}
|
package/lib/registry.ts
CHANGED
|
@@ -161,8 +161,14 @@ function getInputTemplate(
|
|
|
161
161
|
);
|
|
162
162
|
return value;
|
|
163
163
|
}
|
|
164
|
+
if (value && typeof value === "object") {
|
|
165
|
+
CommandTemplates.expandCommandTemplateConfigs(
|
|
166
|
+
value as CommandTemplates.CommandTemplateConfig,
|
|
167
|
+
);
|
|
168
|
+
return value as CommandTemplates.CommandTemplateConfig;
|
|
169
|
+
}
|
|
164
170
|
throw new Error(
|
|
165
|
-
Output.formatToolText("Tool template must be a string or sequence."),
|
|
171
|
+
Output.formatToolText("Tool template must be a string, object, or sequence."),
|
|
166
172
|
);
|
|
167
173
|
}
|
|
168
174
|
|
package/lib/tools.ts
CHANGED
|
@@ -436,6 +436,7 @@ export function createRegisterToolDefinition<TContext>(
|
|
|
436
436
|
),
|
|
437
437
|
template: unionSchema([
|
|
438
438
|
stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
|
|
439
|
+
looseObjectSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
|
|
439
440
|
arraySchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.templateArray),
|
|
440
441
|
nullSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.templateNull),
|
|
441
442
|
]),
|
|
@@ -485,6 +486,27 @@ function messageBodyToToolParams(
|
|
|
485
486
|
return { input: message.body };
|
|
486
487
|
}
|
|
487
488
|
|
|
489
|
+
function formatToolActorFailure(
|
|
490
|
+
tool: string,
|
|
491
|
+
message: ActorMessages.ActorMessage,
|
|
492
|
+
params: Record<string, unknown>,
|
|
493
|
+
error: unknown,
|
|
494
|
+
): Error {
|
|
495
|
+
const original = error instanceof Error ? error.message : String(error);
|
|
496
|
+
const paramsPreview = compactPreview(params, 240) ?? "{}";
|
|
497
|
+
return Object.assign(
|
|
498
|
+
new Error(
|
|
499
|
+
`tool actor ${tool} failed for message type ${message.type}: ${original}; params=${paramsPreview}`,
|
|
500
|
+
),
|
|
501
|
+
{
|
|
502
|
+
message_type: message.type,
|
|
503
|
+
original_error: original,
|
|
504
|
+
params_preview: paramsPreview,
|
|
505
|
+
tool,
|
|
506
|
+
},
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
488
510
|
function runIdFromActorAddress(
|
|
489
511
|
address: string | undefined,
|
|
490
512
|
): string | undefined {
|
|
@@ -915,24 +937,25 @@ export function createInspectToolDefinition<TContext = unknown>(
|
|
|
915
937
|
if (address.kind === "branch") {
|
|
916
938
|
if (view !== "mailbox") throw new Error("inspect branch:<run>/<branch> supports view=mailbox.");
|
|
917
939
|
const status = assertRunAccessibleToContext(runId, ctx);
|
|
918
|
-
const
|
|
940
|
+
const branchInbox = ActorRooms.readBranchInboxDiagnostics(
|
|
919
941
|
String(status.state_dir ?? ""),
|
|
920
942
|
runId,
|
|
921
943
|
target,
|
|
922
944
|
Number(input.lines || 40),
|
|
923
945
|
);
|
|
946
|
+
const messages = branchInbox.messages;
|
|
924
947
|
return {
|
|
925
948
|
content: [
|
|
926
949
|
{
|
|
927
950
|
type: "text" as const,
|
|
928
951
|
text: maybeJsonText(
|
|
929
|
-
messages,
|
|
952
|
+
input.verbose === true ? branchInbox : messages,
|
|
930
953
|
input.verbose === true,
|
|
931
|
-
compactBranchInbox(messages.map((message) => ({ ...message })))
|
|
954
|
+
`${compactBranchInbox(messages.map((message) => ({ ...message })))}${branchInbox.corrupted > 0 ? `\ncorrupted=${branchInbox.corrupted}` : ""}`,
|
|
932
955
|
),
|
|
933
956
|
},
|
|
934
957
|
],
|
|
935
|
-
details: { messages },
|
|
958
|
+
details: { corrupted: branchInbox.corrupted, messages },
|
|
936
959
|
};
|
|
937
960
|
}
|
|
938
961
|
switch (view) {
|
|
@@ -1118,9 +1141,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
|
|
|
1118
1141
|
} else if (address.kind === "branch" && address.value) {
|
|
1119
1142
|
const runId = address.value;
|
|
1120
1143
|
if (message.from) assertMessageSenderBelongsToRun(message, runId, `branch:${runId}/<branch>`);
|
|
1121
|
-
const status =
|
|
1122
|
-
? assertRunExistsForActorMessage(runId)
|
|
1123
|
-
: assertRunAccessibleToContext(runId, ctx);
|
|
1144
|
+
const status = assertRunAccessibleToContext(runId, ctx);
|
|
1124
1145
|
const stateDir = String(status.state_dir ?? "");
|
|
1125
1146
|
if (stateDir && address.branch) {
|
|
1126
1147
|
const ensureBranchMember = (actorAddress: string) => {
|
|
@@ -1159,7 +1180,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
|
|
|
1159
1180
|
} else if (address.kind === "room" && address.value && address.room) {
|
|
1160
1181
|
const runId = address.value;
|
|
1161
1182
|
assertMessageSenderBelongsToRun(message, runId, `room:${runId}`);
|
|
1162
|
-
const status =
|
|
1183
|
+
const status = assertRunAccessibleToContext(runId, ctx);
|
|
1163
1184
|
const stateDir = String(status.state_dir ?? "");
|
|
1164
1185
|
if (!stateDir) throw new Error(`${message.to} has no run state directory.`);
|
|
1165
1186
|
const recipients = getRoomMulticastRecipients(message, runId);
|
|
@@ -1185,13 +1206,19 @@ export function createActorMessageToolDefinition<TContext = unknown>(
|
|
|
1185
1206
|
`tool actor not found or not executable: ${address.value}`,
|
|
1186
1207
|
);
|
|
1187
1208
|
}
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1209
|
+
const toolParams = messageBodyToToolParams(message);
|
|
1210
|
+
let toolResult: unknown;
|
|
1211
|
+
try {
|
|
1212
|
+
toolResult = await tool.execute(
|
|
1213
|
+
`message:${message.type}`,
|
|
1214
|
+
toolParams,
|
|
1215
|
+
_signal,
|
|
1216
|
+
_onUpdate,
|
|
1217
|
+
ctx,
|
|
1218
|
+
);
|
|
1219
|
+
} catch (error) {
|
|
1220
|
+
throw formatToolActorFailure(address.value, message, toolParams, error);
|
|
1221
|
+
}
|
|
1195
1222
|
result = {
|
|
1196
1223
|
invoked: true,
|
|
1197
1224
|
sent: true,
|
package/package.json
CHANGED
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.22.
|
|
5
|
+
version: 0.22.4
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|