@llblab/pi-actors 0.20.2 → 0.21.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/BACKLOG.md +0 -85
- package/CHANGELOG.md +17 -0
- package/README.md +7 -1
- package/dist/index.js +11 -0
- package/dist/lib/actor-rooms.d.ts +1 -0
- package/dist/lib/actor-rooms.js +15 -1
- package/dist/lib/async-runs.d.ts +17 -1
- package/dist/lib/async-runs.js +127 -36
- package/dist/lib/command-templates.js +8 -1
- package/dist/lib/observability.d.ts +15 -0
- package/dist/lib/observability.js +103 -18
- package/dist/lib/recipe-discovery.js +13 -5
- package/dist/lib/recipe-references.js +137 -11
- package/dist/lib/tools.js +5 -5
- package/docs/README.md +1 -1
- package/docs/actor-messages.md +4 -1
- package/docs/async-runs.md +3 -3
- package/docs/recipe-library.md +14 -4
- package/docs/template-recipes.md +37 -7
- package/docs/tool-registry.md +4 -3
- package/index.ts +14 -0
- package/lib/actor-rooms.ts +23 -1
- package/lib/async-runs.ts +186 -48
- package/lib/command-templates.ts +8 -1
- package/lib/observability.ts +133 -20
- package/lib/recipe-discovery.ts +21 -6
- package/lib/recipe-references.ts +141 -17
- package/lib/tools.ts +10 -8
- package/package.json +1 -1
- package/recipes/pipeline-room-swarm.json +1 -1
- package/scripts/coordinator.mjs +60 -32
- package/scripts/locker.mjs +61 -23
- package/scripts/validate-recipe.mjs +2 -2
- package/skills/actors/SKILL.md +10 -9
- package/skills/swarm/SKILL.md +1 -1
package/lib/observability.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
8
|
-
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
8
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
9
9
|
|
|
10
10
|
import * as AsyncRuns from "./async-runs.ts";
|
|
11
11
|
import * as Paths from "./paths.ts";
|
|
@@ -52,9 +52,25 @@ export interface RunSummary {
|
|
|
52
52
|
|
|
53
53
|
export interface RunRetirementCandidate {
|
|
54
54
|
activeSubagents: number;
|
|
55
|
+
childRuns: number;
|
|
55
56
|
descendantSubagents: number;
|
|
56
57
|
run: string;
|
|
57
58
|
stateDir: string;
|
|
59
|
+
terminalChildRuns: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface RunRetirementExecution {
|
|
63
|
+
action: "stop" | "cancel" | "skip" | "failed";
|
|
64
|
+
error?: string;
|
|
65
|
+
run: string;
|
|
66
|
+
stateDir: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface RunRetirementExecutorOptions {
|
|
70
|
+
attempted?: Set<string>;
|
|
71
|
+
cancelRun: (candidate: RunRetirementCandidate) => Record<string, unknown>;
|
|
72
|
+
notify?: (message: string, level: "info" | "warning" | "error") => void;
|
|
73
|
+
sendStop: (candidate: RunRetirementCandidate) => Promise<unknown>;
|
|
58
74
|
}
|
|
59
75
|
|
|
60
76
|
export interface RunTransition {
|
|
@@ -93,6 +109,7 @@ const TERMINAL = new Set<RunObservedStatus>([
|
|
|
93
109
|
"killed",
|
|
94
110
|
]);
|
|
95
111
|
const PROC_DESCENDANT_SCAN_TTL_MS = 1000;
|
|
112
|
+
const RUN_STATE_DISCOVERY_MAX_DEPTH = 8;
|
|
96
113
|
|
|
97
114
|
const procDescendantScanCache = new Map<
|
|
98
115
|
string,
|
|
@@ -120,6 +137,30 @@ function getUpdatedAt(status: Record<string, unknown>): string | undefined {
|
|
|
120
137
|
: undefined;
|
|
121
138
|
}
|
|
122
139
|
|
|
140
|
+
function listRunStateDirs(
|
|
141
|
+
stateRoot: string,
|
|
142
|
+
depth = 0,
|
|
143
|
+
seen = new Set<string>(),
|
|
144
|
+
): string[] {
|
|
145
|
+
if (!existsSync(stateRoot) || seen.has(stateRoot)) return [];
|
|
146
|
+
seen.add(stateRoot);
|
|
147
|
+
let entries: import("node:fs").Dirent[];
|
|
148
|
+
try {
|
|
149
|
+
entries = readdirSync(stateRoot, { withFileTypes: true });
|
|
150
|
+
} catch {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
const result: string[] = [];
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
if (!entry.isDirectory()) continue;
|
|
156
|
+
const child = join(stateRoot, entry.name);
|
|
157
|
+
if (existsSync(join(child, "run.json"))) result.push(child);
|
|
158
|
+
if (depth + 1 < RUN_STATE_DISCOVERY_MAX_DEPTH)
|
|
159
|
+
result.push(...listRunStateDirs(child, depth + 1, seen));
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
123
164
|
function observeRun(stateDir: string): RunObservation | undefined {
|
|
124
165
|
try {
|
|
125
166
|
const status = AsyncRuns.getRunStatus(stateDir);
|
|
@@ -178,9 +219,8 @@ export function summarizeRuns(
|
|
|
178
219
|
total: 0,
|
|
179
220
|
};
|
|
180
221
|
}
|
|
181
|
-
const runs =
|
|
182
|
-
.
|
|
183
|
-
.map((entry) => observeRun(join(stateRoot, entry.name)))
|
|
222
|
+
const runs = listRunStateDirs(stateRoot)
|
|
223
|
+
.map((stateDir) => observeRun(stateDir))
|
|
184
224
|
.filter((run): run is RunObservation => Boolean(run))
|
|
185
225
|
.filter((run) => ownerId === undefined || run.ownerId === ownerId)
|
|
186
226
|
.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
|
|
@@ -242,7 +282,7 @@ function getRunningRunPidMap(stateRoot: string, ownerId?: string): Map<string, s
|
|
|
242
282
|
const pids = new Map<string, string>();
|
|
243
283
|
for (const run of summarizeRunsWithoutSubagents(stateRoot, ownerId).runs) {
|
|
244
284
|
if (run.status !== "running") continue;
|
|
245
|
-
const status = AsyncRuns.getRunStatus(join(stateRoot, run.run));
|
|
285
|
+
const status = AsyncRuns.getRunStatus(run.stateDir ?? join(stateRoot, run.run));
|
|
246
286
|
const pid = Number(status.pid || 0);
|
|
247
287
|
if (pid > 0) pids.set(String(pid), run.run);
|
|
248
288
|
}
|
|
@@ -264,9 +304,8 @@ function summarizeRunsWithoutSubagents(
|
|
|
264
304
|
runs: [],
|
|
265
305
|
total: 0,
|
|
266
306
|
};
|
|
267
|
-
const runs =
|
|
268
|
-
.
|
|
269
|
-
.map((entry) => observeRun(join(stateRoot, entry.name)))
|
|
307
|
+
const runs = listRunStateDirs(stateRoot)
|
|
308
|
+
.map((stateDir) => observeRun(stateDir))
|
|
270
309
|
.filter((run): run is RunObservation => Boolean(run))
|
|
271
310
|
.filter((run) => ownerId === undefined || run.ownerId === ownerId)
|
|
272
311
|
.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
|
|
@@ -370,28 +409,102 @@ export function renderRunStatus(
|
|
|
370
409
|
return renderSubagentStatus(summary.runningSubagents, frame);
|
|
371
410
|
}
|
|
372
411
|
|
|
412
|
+
function isNestedStateDir(parent: string, child: string): boolean {
|
|
413
|
+
const path = relative(parent, child);
|
|
414
|
+
return Boolean(path) && !path.startsWith("..") && !isAbsolute(path);
|
|
415
|
+
}
|
|
416
|
+
|
|
373
417
|
export function findRunRetirementCandidates(
|
|
374
418
|
summary: RunSummary,
|
|
375
419
|
): RunRetirementCandidate[] {
|
|
376
420
|
return summary.runs
|
|
377
|
-
.
|
|
421
|
+
.map((run) => {
|
|
378
422
|
const activeSubagents = Math.max(0, Math.floor(run.activeSubagents ?? 0));
|
|
379
423
|
const descendantSubagents = Math.max(0, Math.floor(run.descendantSubagents ?? 0));
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
424
|
+
const childRuns = run.stateDir
|
|
425
|
+
? summary.runs.filter(
|
|
426
|
+
(child) =>
|
|
427
|
+
child.stateDir !== undefined &&
|
|
428
|
+
child.stateDir !== run.stateDir &&
|
|
429
|
+
isNestedStateDir(run.stateDir!, child.stateDir),
|
|
430
|
+
)
|
|
431
|
+
: [];
|
|
432
|
+
const runningChildRuns = childRuns.filter((child) => child.status === "running").length;
|
|
433
|
+
return {
|
|
434
|
+
activeSubagents,
|
|
435
|
+
childRuns: childRuns.length,
|
|
436
|
+
descendantSubagents,
|
|
437
|
+
ready:
|
|
438
|
+
run.status === "running" &&
|
|
439
|
+
run.retireWhen === "children_terminal" &&
|
|
440
|
+
run.stateDir !== undefined &&
|
|
441
|
+
!run.terminalHandled &&
|
|
442
|
+
activeSubagents + descendantSubagents + runningChildRuns <= 0,
|
|
443
|
+
run,
|
|
444
|
+
terminalChildRuns: childRuns.filter((child) => TERMINAL.has(child.status)).length,
|
|
445
|
+
};
|
|
386
446
|
})
|
|
387
|
-
.
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
447
|
+
.filter((item) => item.ready)
|
|
448
|
+
.map((item) => ({
|
|
449
|
+
activeSubagents: item.activeSubagents,
|
|
450
|
+
childRuns: item.childRuns,
|
|
451
|
+
descendantSubagents: item.descendantSubagents,
|
|
452
|
+
run: item.run.run,
|
|
453
|
+
stateDir: item.run.stateDir!,
|
|
454
|
+
terminalChildRuns: item.terminalChildRuns,
|
|
392
455
|
}));
|
|
393
456
|
}
|
|
394
457
|
|
|
458
|
+
export async function executeRunRetirements(
|
|
459
|
+
summary: RunSummary,
|
|
460
|
+
options: RunRetirementExecutorOptions,
|
|
461
|
+
): Promise<RunRetirementExecution[]> {
|
|
462
|
+
const results: RunRetirementExecution[] = [];
|
|
463
|
+
for (const candidate of findRunRetirementCandidates(summary)) {
|
|
464
|
+
if (options.attempted?.has(candidate.stateDir)) {
|
|
465
|
+
results.push({ action: "skip", run: candidate.run, stateDir: candidate.stateDir });
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
options.attempted?.add(candidate.stateDir);
|
|
469
|
+
try {
|
|
470
|
+
await options.sendStop(candidate);
|
|
471
|
+
options.notify?.(
|
|
472
|
+
`Retiring actor ${candidate.run} after child runs reached terminal state`,
|
|
473
|
+
"info",
|
|
474
|
+
);
|
|
475
|
+
results.push({ action: "stop", run: candidate.run, stateDir: candidate.stateDir });
|
|
476
|
+
continue;
|
|
477
|
+
} catch (error) {
|
|
478
|
+
try {
|
|
479
|
+
const cancelResult = options.cancelRun(candidate);
|
|
480
|
+
const cancelled = Boolean((cancelResult as { cancelled?: unknown }).cancelled);
|
|
481
|
+
options.notify?.(
|
|
482
|
+
cancelled
|
|
483
|
+
? `Retiring actor ${candidate.run} by cancellation after graceful stop failed`
|
|
484
|
+
: `Actor retirement skipped for ${candidate.run}: ${error instanceof Error ? error.message : String(error)}`,
|
|
485
|
+
cancelled ? "warning" : "error",
|
|
486
|
+
);
|
|
487
|
+
results.push({
|
|
488
|
+
action: cancelled ? "cancel" : "skip",
|
|
489
|
+
...(cancelled ? {} : { error: error instanceof Error ? error.message : String(error) }),
|
|
490
|
+
run: candidate.run,
|
|
491
|
+
stateDir: candidate.stateDir,
|
|
492
|
+
});
|
|
493
|
+
} catch (cancelError) {
|
|
494
|
+
const message = cancelError instanceof Error ? cancelError.message : String(cancelError);
|
|
495
|
+
options.notify?.(`Actor retirement failed for ${candidate.run}: ${message}`, "error");
|
|
496
|
+
results.push({
|
|
497
|
+
action: "failed",
|
|
498
|
+
error: message,
|
|
499
|
+
run: candidate.run,
|
|
500
|
+
stateDir: candidate.stateDir,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return results;
|
|
506
|
+
}
|
|
507
|
+
|
|
395
508
|
export function detectRunTransitions(
|
|
396
509
|
previous: Map<string, RunObservedStatus>,
|
|
397
510
|
summary: RunSummary,
|
package/lib/recipe-discovery.ts
CHANGED
|
@@ -88,11 +88,11 @@ function listRecipeFiles(root: string): string[] {
|
|
|
88
88
|
.filter(
|
|
89
89
|
(entry) =>
|
|
90
90
|
entry.isFile() &&
|
|
91
|
-
entry.name.endsWith(".json") &&
|
|
91
|
+
(entry.name.endsWith(".json") || entry.name.endsWith(".md")) &&
|
|
92
92
|
entry.name !== "legacy-tool-registry-migration-report.json",
|
|
93
93
|
)
|
|
94
94
|
.map((entry) => join(root, entry.name))
|
|
95
|
-
.sort();
|
|
95
|
+
.sort((a, b) => a.replace(/\.md$/, ".json").localeCompare(b.replace(/\.md$/, ".json")) || (a.endsWith(".json") ? -1 : 1));
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function getRecipeConfigDiagnostics(
|
|
@@ -100,9 +100,15 @@ function getRecipeConfigDiagnostics(
|
|
|
100
100
|
config: TemplateRecipeConfig | undefined,
|
|
101
101
|
): string[] {
|
|
102
102
|
if (!config) return [`Invalid recipe: ${file}`];
|
|
103
|
+
const commandTemplateConfig =
|
|
104
|
+
typeof config.template === "object" && config.template !== null
|
|
105
|
+
? config.template
|
|
106
|
+
: config;
|
|
103
107
|
return CommandTemplates.getCommandTemplateWarnings(
|
|
104
|
-
|
|
105
|
-
).map(
|
|
108
|
+
commandTemplateConfig as CommandTemplates.CommandTemplateConfig,
|
|
109
|
+
).map(
|
|
110
|
+
(warning) => `Recipe ${file}: ${warning}`,
|
|
111
|
+
);
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
function readDiscoveredRecipe(
|
|
@@ -218,13 +224,22 @@ export function discoverRecipeSources(
|
|
|
218
224
|
const diagnostics: string[] = getRecipeRootDiagnostics(sources);
|
|
219
225
|
for (const [id, bucket] of byId) {
|
|
220
226
|
bucket.sort(
|
|
221
|
-
(a, b) =>
|
|
227
|
+
(a, b) =>
|
|
228
|
+
a.priority - b.priority ||
|
|
229
|
+
a.path.replace(/\.md$/, ".json").localeCompare(b.path.replace(/\.md$/, ".json")) ||
|
|
230
|
+
(a.path.endsWith(".json") ? -1 : 1),
|
|
222
231
|
);
|
|
223
232
|
const winner = bucket[0];
|
|
224
233
|
winner.active = true;
|
|
225
234
|
winner.shadows = bucket.slice(1).map((entry) => entry.path);
|
|
226
235
|
active.set(id, winner);
|
|
227
|
-
for (const shadow of bucket.slice(1))
|
|
236
|
+
for (const shadow of bucket.slice(1)) {
|
|
237
|
+
shadow.shadowed = true;
|
|
238
|
+
if (winner.path.endsWith(".json") && shadow.path.endsWith(".md"))
|
|
239
|
+
shadow.diagnostics.push(
|
|
240
|
+
`Markdown recipe ${shadow.path} is shadowed by JSON recipe ${winner.path}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
228
243
|
if (winner.invalid)
|
|
229
244
|
diagnostics.push(
|
|
230
245
|
`Recipe ${id} is invalid and blocks lower-priority recipes`,
|
package/lib/recipe-references.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
|
-
import { basename, dirname, resolve } from "node:path";
|
|
9
|
+
import { basename, dirname, extname, resolve } from "node:path";
|
|
10
10
|
|
|
11
11
|
import type {
|
|
12
12
|
CommandTemplateConfig,
|
|
@@ -101,7 +101,9 @@ export function resolveRecipePath(
|
|
|
101
101
|
if (expanded.includes("/")) return resolve(expanded);
|
|
102
102
|
return resolve(
|
|
103
103
|
recipeRoot,
|
|
104
|
-
expanded.endsWith(".json")
|
|
104
|
+
expanded.endsWith(".json") || expanded.endsWith(".md")
|
|
105
|
+
? expanded
|
|
106
|
+
: `${expanded}.json`,
|
|
105
107
|
);
|
|
106
108
|
}
|
|
107
109
|
|
|
@@ -110,20 +112,26 @@ function isBareRecipeName(value: string): boolean {
|
|
|
110
112
|
return Boolean(trimmed) && !trimmed.includes("/") && !trimmed.startsWith("~") && !trimmed.includes("{");
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
function
|
|
115
|
+
function recipeNameFiles(value: string): string[] {
|
|
114
116
|
const trimmed = value.trim();
|
|
115
|
-
|
|
117
|
+
if (trimmed.endsWith(".json") || trimmed.endsWith(".md")) return [trimmed];
|
|
118
|
+
return [`${trimmed}.json`, `${trimmed}.md`];
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
function resolveRecipeImportPath(value: string, currentRecipeRoot: string): string {
|
|
119
122
|
if (!isBareRecipeName(value)) return resolveRecipePath(value, currentRecipeRoot);
|
|
120
|
-
const file = recipeNameFile(value);
|
|
121
123
|
const roots = [
|
|
122
124
|
Paths.getRecipeRoot(),
|
|
123
125
|
currentRecipeRoot,
|
|
124
126
|
Paths.getPackagedRecipeRoot(),
|
|
125
127
|
];
|
|
126
|
-
const candidates = [
|
|
128
|
+
const candidates = [
|
|
129
|
+
...new Set(
|
|
130
|
+
roots.flatMap((root) =>
|
|
131
|
+
recipeNameFiles(value).map((file) => resolve(root, file)),
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
];
|
|
127
135
|
return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
|
|
128
136
|
}
|
|
129
137
|
|
|
@@ -134,14 +142,14 @@ export function getRecipePath(
|
|
|
134
142
|
if (typeof value !== "string") return undefined;
|
|
135
143
|
const trimmed = value.trim();
|
|
136
144
|
if (!trimmed || hasWhitespace(trimmed)) return undefined;
|
|
137
|
-
if (trimmed.endsWith(".json")
|
|
138
|
-
|
|
145
|
+
if (trimmed.endsWith(".json") || trimmed.endsWith(".md"))
|
|
146
|
+
return resolveRecipePath(trimmed, recipeRoot);
|
|
147
|
+
const jsonPath = resolveRecipePath(trimmed, recipeRoot);
|
|
148
|
+
const mdPath = resolveRecipePath(`${trimmed}.md`, recipeRoot);
|
|
149
|
+
const path = existsSync(jsonPath) ? jsonPath : mdPath;
|
|
139
150
|
if (!existsSync(path)) return undefined;
|
|
140
151
|
try {
|
|
141
|
-
const raw =
|
|
142
|
-
string,
|
|
143
|
-
unknown
|
|
144
|
-
>;
|
|
152
|
+
const raw = readRawRecipeConfig(path);
|
|
145
153
|
return raw && typeof raw === "object" && Object.hasOwn(raw, "template")
|
|
146
154
|
? path
|
|
147
155
|
: undefined;
|
|
@@ -232,6 +240,123 @@ function getRecipeCommandTemplate(
|
|
|
232
240
|
return normalizeRecipeTemplate({ ...envelope, template });
|
|
233
241
|
}
|
|
234
242
|
|
|
243
|
+
function parseMarkdownScalar(value: string): unknown {
|
|
244
|
+
const trimmed = value.trim();
|
|
245
|
+
if (!trimmed) return "";
|
|
246
|
+
if (
|
|
247
|
+
(trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
248
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]"))
|
|
249
|
+
) {
|
|
250
|
+
try {
|
|
251
|
+
return JSON.parse(trimmed);
|
|
252
|
+
} catch {
|
|
253
|
+
return trimmed;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const quoted = trimmed.match(/^(?:"([^"]*)"|'([^']*)')$/);
|
|
257
|
+
if (quoted) return quoted[1] ?? quoted[2] ?? "";
|
|
258
|
+
if (trimmed === "true") return true;
|
|
259
|
+
if (trimmed === "false") return false;
|
|
260
|
+
if (trimmed === "null") return null;
|
|
261
|
+
if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
|
|
262
|
+
return trimmed;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function parseMarkdownFrontmatterObject(
|
|
266
|
+
lines: string[],
|
|
267
|
+
): Record<string, unknown> | unknown[] {
|
|
268
|
+
if (lines.every((line) => /^\s*-\s+/.test(line))) {
|
|
269
|
+
return lines.map((line) => parseMarkdownScalar(line.replace(/^\s*-\s+/, "")));
|
|
270
|
+
}
|
|
271
|
+
const result: Record<string, unknown> = {};
|
|
272
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
273
|
+
const match = lines[index].match(/^\s{2}([A-Za-z_][A-Za-z0-9_.-]*):\s*(.*)$/);
|
|
274
|
+
if (!match) continue;
|
|
275
|
+
if (match[2]) {
|
|
276
|
+
result[match[1]] = parseMarkdownScalar(match[2]);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const nested: string[] = [];
|
|
280
|
+
while (index + 1 < lines.length && /^\s{4}/.test(lines[index + 1])) {
|
|
281
|
+
index += 1;
|
|
282
|
+
nested.push(lines[index].slice(2));
|
|
283
|
+
}
|
|
284
|
+
result[match[1]] = parseMarkdownFrontmatterObject(nested);
|
|
285
|
+
}
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function parseMarkdownFrontmatter(value: string): Record<string, unknown> {
|
|
290
|
+
const result: Record<string, unknown> = {};
|
|
291
|
+
const lines = value.split(/\r?\n/);
|
|
292
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
293
|
+
const line = lines[index];
|
|
294
|
+
if (!line.trim() || line.trim().startsWith("#")) continue;
|
|
295
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_.-]*):\s*(.*)$/);
|
|
296
|
+
if (!match) continue;
|
|
297
|
+
if (match[2]) {
|
|
298
|
+
result[match[1]] = parseMarkdownScalar(match[2]);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const nested: string[] = [];
|
|
302
|
+
while (index + 1 < lines.length && /^\s+/.test(lines[index + 1])) {
|
|
303
|
+
index += 1;
|
|
304
|
+
nested.push(lines[index]);
|
|
305
|
+
}
|
|
306
|
+
result[match[1]] = parseMarkdownFrontmatterObject(nested);
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function findMarkdownRecipeFence(
|
|
312
|
+
body: string,
|
|
313
|
+
): { info: string; body: string } | undefined {
|
|
314
|
+
const pattern = /```([^\n`]*)\n([\s\S]*?)```/g;
|
|
315
|
+
for (const match of body.matchAll(pattern)) {
|
|
316
|
+
const info = match[1].trim().toLowerCase();
|
|
317
|
+
if (
|
|
318
|
+
info.includes("recipe") ||
|
|
319
|
+
info.includes("template") ||
|
|
320
|
+
info.includes("command") ||
|
|
321
|
+
info.includes("json")
|
|
322
|
+
) {
|
|
323
|
+
return { info, body: match[2].trim() };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function parseMarkdownRecipeConfig(
|
|
330
|
+
content: string,
|
|
331
|
+
): Record<string, unknown> | undefined {
|
|
332
|
+
const lines = content.split(/\r?\n/);
|
|
333
|
+
if (lines[0]?.trim() !== "---") return undefined;
|
|
334
|
+
const end = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
335
|
+
if (end === -1) return undefined;
|
|
336
|
+
const frontmatter = parseMarkdownFrontmatter(lines.slice(1, end).join("\n"));
|
|
337
|
+
const fence = findMarkdownRecipeFence(lines.slice(end + 1).join("\n"));
|
|
338
|
+
if (!fence) return Object.hasOwn(frontmatter, "template") ? frontmatter : undefined;
|
|
339
|
+
const text = fence.body.trim();
|
|
340
|
+
if (!text) return undefined;
|
|
341
|
+
if (
|
|
342
|
+
fence.info.includes("json") ||
|
|
343
|
+
fence.info.includes("recipe") ||
|
|
344
|
+
text.startsWith("{") ||
|
|
345
|
+
text.startsWith("[") ||
|
|
346
|
+
text.startsWith('"')
|
|
347
|
+
) {
|
|
348
|
+
try {
|
|
349
|
+
const parsed = JSON.parse(text) as unknown;
|
|
350
|
+
if (isRecord(parsed) && Object.hasOwn(parsed, "template"))
|
|
351
|
+
return { ...frontmatter, ...parsed };
|
|
352
|
+
return { ...frontmatter, template: parsed };
|
|
353
|
+
} catch {
|
|
354
|
+
if (fence.info.includes("json") || fence.info.includes("recipe")) return undefined;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return { ...frontmatter, template: text };
|
|
358
|
+
}
|
|
359
|
+
|
|
235
360
|
export function readRawRecipeConfig(
|
|
236
361
|
path: string,
|
|
237
362
|
): Record<string, unknown> | undefined {
|
|
@@ -243,10 +368,9 @@ export function readRawRecipeConfig(
|
|
|
243
368
|
);
|
|
244
369
|
}
|
|
245
370
|
try {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
>;
|
|
371
|
+
const content = readFileSync(path, "utf8");
|
|
372
|
+
if (path.endsWith(".md")) return parseMarkdownRecipeConfig(content);
|
|
373
|
+
const raw = JSON.parse(content) as Record<string, unknown>;
|
|
250
374
|
return raw && typeof raw === "object" ? raw : undefined;
|
|
251
375
|
} catch {
|
|
252
376
|
return undefined;
|
|
@@ -254,7 +378,7 @@ export function readRawRecipeConfig(
|
|
|
254
378
|
}
|
|
255
379
|
|
|
256
380
|
export function getRecipeIdFromPath(file: string): string {
|
|
257
|
-
return basename(file,
|
|
381
|
+
return basename(file, extname(file));
|
|
258
382
|
}
|
|
259
383
|
|
|
260
384
|
function readRecipeConfig(value: unknown): TemplateRecipeConfig | undefined {
|
package/lib/tools.ts
CHANGED
|
@@ -1089,7 +1089,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
|
|
|
1089
1089
|
} else if (message.type === "control.kill") {
|
|
1090
1090
|
result = AsyncRuns.killRun(address.value);
|
|
1091
1091
|
} else {
|
|
1092
|
-
result = AsyncRuns.sendRunMessage(
|
|
1092
|
+
result = await AsyncRuns.sendRunMessage(
|
|
1093
1093
|
address.value,
|
|
1094
1094
|
messageBodyToRunLine(message),
|
|
1095
1095
|
);
|
|
@@ -1131,7 +1131,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
|
|
|
1131
1131
|
ActorRooms.writeCommunicationSnapshot(stateDir, runId);
|
|
1132
1132
|
ActorRooms.appendBranchInboxMessage(stateDir, runId, message.to, message);
|
|
1133
1133
|
}
|
|
1134
|
-
result = AsyncRuns.sendRunMessage(
|
|
1134
|
+
result = await AsyncRuns.sendRunMessage(
|
|
1135
1135
|
address.value,
|
|
1136
1136
|
JSON.stringify(message),
|
|
1137
1137
|
);
|
|
@@ -1143,16 +1143,18 @@ export function createActorMessageToolDefinition<TContext = unknown>(
|
|
|
1143
1143
|
if (!stateDir) throw new Error(`${message.to} has no run state directory.`);
|
|
1144
1144
|
const recipients = getRoomMulticastRecipients(message, runId);
|
|
1145
1145
|
const roomResult = ActorRooms.appendRoomMessage(stateDir, address.room, message);
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1146
|
+
await Promise.all(
|
|
1147
|
+
recipients.map((recipient) =>
|
|
1148
|
+
AsyncRuns.sendRunMessage(
|
|
1149
|
+
runId,
|
|
1150
|
+
JSON.stringify({ ...message, to: recipient }),
|
|
1151
|
+
),
|
|
1150
1152
|
),
|
|
1151
1153
|
);
|
|
1152
1154
|
result = {
|
|
1153
1155
|
...roomResult,
|
|
1154
|
-
...(
|
|
1155
|
-
? { multicast: recipients, multicast_count:
|
|
1156
|
+
...(recipients.length > 0
|
|
1157
|
+
? { multicast: recipients, multicast_count: recipients.length }
|
|
1156
1158
|
: {}),
|
|
1157
1159
|
};
|
|
1158
1160
|
} else if (address.kind === "tool" && address.value) {
|
package/package.json
CHANGED