@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
|
@@ -24,7 +24,9 @@ function assertToolSafeRepeatConfig(config, argTypes, defaults) {
|
|
|
24
24
|
const trimmed = node.repeat.trim();
|
|
25
25
|
if (!/^\d+$/.test(trimmed)) {
|
|
26
26
|
const match = trimmed.match(/^\{?([A-Za-z_][A-Za-z0-9_-]*)\.length\}?$/);
|
|
27
|
-
if (!match ||
|
|
27
|
+
if (!match ||
|
|
28
|
+
(argTypes[match[1]]?.kind !== "array" &&
|
|
29
|
+
!Array.isArray(defaults[match[1]])))
|
|
28
30
|
throw new Error("Command template repeat must be a positive integer or {array.length} with an array argument/default.");
|
|
29
31
|
}
|
|
30
32
|
}
|
|
@@ -39,11 +41,16 @@ function listRecipeFiles(root) {
|
|
|
39
41
|
(entry.name.endsWith(".json") || entry.name.endsWith(".md")) &&
|
|
40
42
|
entry.name !== "legacy-tool-registry-migration-report.json")
|
|
41
43
|
.map((entry) => join(root, entry.name))
|
|
42
|
-
.sort((a, b) => a
|
|
44
|
+
.sort((a, b) => a
|
|
45
|
+
.replace(/\.md$/, ".json")
|
|
46
|
+
.localeCompare(b.replace(/\.md$/, ".json")) ||
|
|
47
|
+
(a.endsWith(".json") ? -1 : 1));
|
|
43
48
|
}
|
|
44
49
|
function getRecipeConfigDiagnostics(file, config) {
|
|
45
|
-
if (!config)
|
|
46
|
-
|
|
50
|
+
if (!config) {
|
|
51
|
+
const reason = RecipeReferences.diagnoseRawRecipeConfigFailure(file);
|
|
52
|
+
return [`Invalid recipe: ${file}${reason ? `: ${reason}` : ""}`];
|
|
53
|
+
}
|
|
47
54
|
const commandTemplateConfig = typeof config.template === "object" && config.template !== null
|
|
48
55
|
? config.template
|
|
49
56
|
: config;
|
|
@@ -94,7 +101,14 @@ function filesForSource(source) {
|
|
|
94
101
|
const defaultTool = source.defaultTool === true;
|
|
95
102
|
const mutableUsage = source.mutableUsage === true;
|
|
96
103
|
if (source.file)
|
|
97
|
-
return [
|
|
104
|
+
return [
|
|
105
|
+
{
|
|
106
|
+
root: source.root ?? source.file,
|
|
107
|
+
file: source.file,
|
|
108
|
+
defaultTool,
|
|
109
|
+
mutableUsage,
|
|
110
|
+
},
|
|
111
|
+
];
|
|
98
112
|
return source.root
|
|
99
113
|
? listRecipeFiles(source.root).map((file) => ({
|
|
100
114
|
root: source.root,
|
|
@@ -139,7 +153,9 @@ export function discoverRecipeSources(sources) {
|
|
|
139
153
|
const diagnostics = getRecipeRootDiagnostics(sources);
|
|
140
154
|
for (const [id, bucket] of byId) {
|
|
141
155
|
bucket.sort((a, b) => a.priority - b.priority ||
|
|
142
|
-
a.path
|
|
156
|
+
a.path
|
|
157
|
+
.replace(/\.md$/, ".json")
|
|
158
|
+
.localeCompare(b.path.replace(/\.md$/, ".json")) ||
|
|
143
159
|
(a.path.endsWith(".json") ? -1 : 1));
|
|
144
160
|
const winner = bucket[0];
|
|
145
161
|
winner.active = true;
|
|
@@ -209,7 +225,13 @@ function cleanupRecommendation(entry) {
|
|
|
209
225
|
id: entry.id,
|
|
210
226
|
path: entry.path,
|
|
211
227
|
reason: "user recipe is a component, not an active tool",
|
|
212
|
-
actions: [
|
|
228
|
+
actions: [
|
|
229
|
+
"keep component",
|
|
230
|
+
"move into tool root",
|
|
231
|
+
"merge",
|
|
232
|
+
"delete",
|
|
233
|
+
"archive",
|
|
234
|
+
],
|
|
213
235
|
};
|
|
214
236
|
}
|
|
215
237
|
if (entry.shadows.length > 0) {
|
|
@@ -241,6 +263,66 @@ export function createRecipeIntegrityManifest(result) {
|
|
|
241
263
|
})
|
|
242
264
|
.sort((a, b) => a.id.localeCompare(b.id) || a.path.localeCompare(b.path));
|
|
243
265
|
}
|
|
266
|
+
function diagnosticSeverity(message) {
|
|
267
|
+
if (/invalid|failed to load|not found|cyclic|exceeds|must define|repeat must/i.test(message)) {
|
|
268
|
+
return "error";
|
|
269
|
+
}
|
|
270
|
+
if (/world-writable|group-writable|invokes bash|eval|destructive|unsafe/i.test(message)) {
|
|
271
|
+
return "warning";
|
|
272
|
+
}
|
|
273
|
+
return "info";
|
|
274
|
+
}
|
|
275
|
+
function diagnosticSuggestedAction(message) {
|
|
276
|
+
if (/must define template/i.test(message))
|
|
277
|
+
return "add a template field or remove the recipe";
|
|
278
|
+
if (/JSON|Expected|parse/i.test(message))
|
|
279
|
+
return "fix recipe syntax or archive the file";
|
|
280
|
+
if (/Markdown recipe/i.test(message))
|
|
281
|
+
return "fix frontmatter and add a fenced template or recipe block";
|
|
282
|
+
if (/cyclic/i.test(message))
|
|
283
|
+
return "break the import cycle";
|
|
284
|
+
if (/exceeds.*size/i.test(message))
|
|
285
|
+
return "split large prompt or data into separate files";
|
|
286
|
+
if (/repeat must/i.test(message))
|
|
287
|
+
return "use a positive repeat count or an array-typed repeat source";
|
|
288
|
+
if (/world-writable|group-writable/i.test(message))
|
|
289
|
+
return "tighten recipe root permissions";
|
|
290
|
+
if (/invokes bash/i.test(message))
|
|
291
|
+
return "audit the trusted shell boundary or move details to recipe doctor";
|
|
292
|
+
if (/shadows/i.test(message))
|
|
293
|
+
return "confirm the active override or rename one recipe";
|
|
294
|
+
if (/disabled/i.test(message))
|
|
295
|
+
return "keep disabled intentionally or delete/archive the file";
|
|
296
|
+
return "inspect the recipe and fix or archive it if unexpected";
|
|
297
|
+
}
|
|
298
|
+
function diagnosticDetails(result) {
|
|
299
|
+
const details = [];
|
|
300
|
+
const seen = new Set();
|
|
301
|
+
const push = (message, entry) => {
|
|
302
|
+
const key = `${entry?.path ?? "root"}\n${message}`;
|
|
303
|
+
if (seen.has(key))
|
|
304
|
+
return;
|
|
305
|
+
seen.add(key);
|
|
306
|
+
details.push({
|
|
307
|
+
...(entry ? { id: entry.id, path: entry.path } : {}),
|
|
308
|
+
action: diagnosticSuggestedAction(message),
|
|
309
|
+
message,
|
|
310
|
+
severity: diagnosticSeverity(message),
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
for (const message of result.diagnostics)
|
|
314
|
+
push(message);
|
|
315
|
+
for (const entry of result.entries) {
|
|
316
|
+
for (const message of entry.diagnostics)
|
|
317
|
+
push(message, entry);
|
|
318
|
+
}
|
|
319
|
+
return details.sort((a, b) => {
|
|
320
|
+
const rank = { error: 0, warning: 1, info: 2 };
|
|
321
|
+
return (rank[a.severity] -
|
|
322
|
+
rank[b.severity] ||
|
|
323
|
+
String(a.message).localeCompare(String(b.message)));
|
|
324
|
+
});
|
|
325
|
+
}
|
|
244
326
|
function recommendationForEntry(entry, activePath) {
|
|
245
327
|
const recommendation = cleanupRecommendation(entry);
|
|
246
328
|
if (!recommendation)
|
|
@@ -257,9 +339,11 @@ export function summarizeDiscovery(result) {
|
|
|
257
339
|
const recommendations = result.entries
|
|
258
340
|
.map((entry) => recommendationForEntry(entry, result.active.get(entry.id)?.path))
|
|
259
341
|
.filter((entry) => Boolean(entry))
|
|
260
|
-
.sort((a, b) => String(a.id).localeCompare(String(b.id)) ||
|
|
342
|
+
.sort((a, b) => String(a.id).localeCompare(String(b.id)) ||
|
|
343
|
+
String(a.path).localeCompare(String(b.path)));
|
|
261
344
|
return {
|
|
262
|
-
active: [...result.active.values()]
|
|
345
|
+
active: [...result.active.values()]
|
|
346
|
+
.map((entry) => ({
|
|
263
347
|
id: entry.id,
|
|
264
348
|
path: entry.path,
|
|
265
349
|
description: entry.config?.description,
|
|
@@ -267,15 +351,26 @@ export function summarizeDiscovery(result) {
|
|
|
267
351
|
disabled: entry.disabled,
|
|
268
352
|
invalid: entry.invalid,
|
|
269
353
|
shadows: entry.shadows,
|
|
270
|
-
...(recipeUsage(entry.config)
|
|
271
|
-
|
|
354
|
+
...(recipeUsage(entry.config)
|
|
355
|
+
? { usage: recipeUsage(entry.config) }
|
|
356
|
+
: {}),
|
|
357
|
+
}))
|
|
358
|
+
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
272
359
|
shadowed: result.entries
|
|
273
360
|
.filter((entry) => entry.shadowed)
|
|
274
|
-
.map((entry) => ({
|
|
361
|
+
.map((entry) => ({
|
|
362
|
+
id: entry.id,
|
|
363
|
+
path: entry.path,
|
|
364
|
+
shadowedBy: result.active.get(entry.id)?.path,
|
|
365
|
+
}))
|
|
275
366
|
.sort((a, b) => a.id.localeCompare(b.id) || a.path.localeCompare(b.path)),
|
|
276
367
|
invalid: result.entries
|
|
277
368
|
.filter((entry) => entry.invalid)
|
|
278
|
-
.map((entry) => ({
|
|
369
|
+
.map((entry) => ({
|
|
370
|
+
id: entry.id,
|
|
371
|
+
path: entry.path,
|
|
372
|
+
diagnostics: entry.diagnostics,
|
|
373
|
+
}))
|
|
279
374
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
280
375
|
disabled: result.entries
|
|
281
376
|
.filter((entry) => entry.disabled)
|
|
@@ -283,6 +378,7 @@ export function summarizeDiscovery(result) {
|
|
|
283
378
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
284
379
|
recommendations,
|
|
285
380
|
diagnostics: result.diagnostics,
|
|
381
|
+
diagnostic_details: diagnosticDetails(result),
|
|
286
382
|
integrity_manifest: createRecipeIntegrityManifest(result),
|
|
287
383
|
};
|
|
288
384
|
}
|
|
@@ -297,7 +393,10 @@ export function toRegisteredTool(entry) {
|
|
|
297
393
|
? {
|
|
298
394
|
...argTemplate,
|
|
299
395
|
...(cfg.args !== undefined ? { args: cfg.args } : {}),
|
|
300
|
-
defaults: {
|
|
396
|
+
defaults: {
|
|
397
|
+
...(argTemplate.defaults ?? {}),
|
|
398
|
+
...(cfg.defaults ?? {}),
|
|
399
|
+
},
|
|
301
400
|
}
|
|
302
401
|
: { args: cfg.args, defaults: cfg.defaults ?? {}, template: argTemplate };
|
|
303
402
|
const explicitArgTypes = Object.fromEntries((cfg.args ?? []).map((arg) => {
|
|
@@ -312,13 +411,19 @@ export function toRegisteredTool(entry) {
|
|
|
312
411
|
template,
|
|
313
412
|
recipe: cfg,
|
|
314
413
|
args: Schema.getToolArgNames(argTemplateConfig),
|
|
315
|
-
defaults: Object.fromEntries(Object.entries(cfg.defaults ?? {}).map(([key, value]) => [
|
|
414
|
+
defaults: Object.fromEntries(Object.entries(cfg.defaults ?? {}).map(([key, value]) => [
|
|
415
|
+
key,
|
|
416
|
+
String(value),
|
|
417
|
+
])),
|
|
316
418
|
...(Object.keys(argTypes).length > 0 ? { argTypes } : {}),
|
|
317
419
|
...(entry.mutableUsage ? { sourcePath: entry.path } : {}),
|
|
318
420
|
...(cfg.args ? { storedArgs: cfg.args } : {}),
|
|
319
421
|
...(cfg.defaults
|
|
320
422
|
? {
|
|
321
|
-
storedDefaults: Object.fromEntries(Object.entries(cfg.defaults).map(([key, value]) => [
|
|
423
|
+
storedDefaults: Object.fromEntries(Object.entries(cfg.defaults).map(([key, value]) => [
|
|
424
|
+
key,
|
|
425
|
+
String(value),
|
|
426
|
+
])),
|
|
322
427
|
}
|
|
323
428
|
: {}),
|
|
324
429
|
};
|
|
@@ -57,6 +57,7 @@ export interface ReadResolvedRecipeConfigOptions {
|
|
|
57
57
|
}
|
|
58
58
|
export declare function resolveRecipePath(value: string, recipeRoot?: string): string;
|
|
59
59
|
export declare function getRecipePath(value: unknown, recipeRoot?: string): string | undefined;
|
|
60
|
+
export declare function diagnoseRawRecipeConfigFailure(path: string): string | undefined;
|
|
60
61
|
export declare function readRawRecipeConfig(path: string): Record<string, unknown> | undefined;
|
|
61
62
|
export declare function getRecipeIdFromPath(file: string): string;
|
|
62
63
|
export declare function readResolvedRecipeConfig(file: string, stack?: string[], options?: ReadResolvedRecipeConfigOptions): TemplateRecipeConfig | undefined;
|
|
@@ -29,7 +29,10 @@ export function resolveRecipePath(value, recipeRoot = Paths.getRecipeRoot()) {
|
|
|
29
29
|
}
|
|
30
30
|
function isBareRecipeName(value) {
|
|
31
31
|
const trimmed = value.trim();
|
|
32
|
-
return Boolean(trimmed) &&
|
|
32
|
+
return (Boolean(trimmed) &&
|
|
33
|
+
!trimmed.includes("/") &&
|
|
34
|
+
!trimmed.startsWith("~") &&
|
|
35
|
+
!trimmed.includes("{"));
|
|
33
36
|
}
|
|
34
37
|
function recipeNameFiles(value) {
|
|
35
38
|
const trimmed = value.trim();
|
|
@@ -260,6 +263,42 @@ function parseMarkdownRecipeConfig(content) {
|
|
|
260
263
|
}
|
|
261
264
|
return { ...frontmatter, template: text };
|
|
262
265
|
}
|
|
266
|
+
export function diagnoseRawRecipeConfigFailure(path) {
|
|
267
|
+
if (!existsSync(path))
|
|
268
|
+
return "file does not exist";
|
|
269
|
+
const size = statSync(path).size;
|
|
270
|
+
if (size > MAX_RECIPE_FILE_BYTES) {
|
|
271
|
+
return `file exceeds size limit ${MAX_RECIPE_FILE_BYTES} bytes`;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
const content = readFileSync(path, "utf8");
|
|
275
|
+
if (path.endsWith(".md")) {
|
|
276
|
+
const lines = content.split(/\r?\n/);
|
|
277
|
+
if (lines[0]?.trim() !== "---") {
|
|
278
|
+
return "Markdown recipe must start with frontmatter";
|
|
279
|
+
}
|
|
280
|
+
const end = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
281
|
+
if (end === -1)
|
|
282
|
+
return "Markdown recipe frontmatter is not closed";
|
|
283
|
+
const parsed = parseMarkdownRecipeConfig(content);
|
|
284
|
+
if (!parsed)
|
|
285
|
+
return "Markdown recipe has no executable recipe/template fence";
|
|
286
|
+
if (!Object.hasOwn(parsed, "template"))
|
|
287
|
+
return "recipe must define template";
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
const raw = JSON.parse(content);
|
|
291
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
292
|
+
return "JSON recipe must be an object";
|
|
293
|
+
}
|
|
294
|
+
if (!Object.hasOwn(raw, "template"))
|
|
295
|
+
return "recipe must define template";
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
return error instanceof Error ? error.message : String(error);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
263
302
|
export function readRawRecipeConfig(path) {
|
|
264
303
|
if (!existsSync(path))
|
|
265
304
|
return undefined;
|
package/dist/lib/registry.js
CHANGED
|
@@ -82,7 +82,11 @@ function getInputTemplate(value) {
|
|
|
82
82
|
throw new Error(Output.formatToolText("Tool template sequence is empty."));
|
|
83
83
|
return value;
|
|
84
84
|
}
|
|
85
|
-
|
|
85
|
+
if (value && typeof value === "object") {
|
|
86
|
+
CommandTemplates.expandCommandTemplateConfigs(value);
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
throw new Error(Output.formatToolText("Tool template must be a string, object, or sequence."));
|
|
86
90
|
}
|
|
87
91
|
function buildConfig(name, input, existing) {
|
|
88
92
|
const explicitArgs = input.args === undefined
|
package/dist/lib/tools.js
CHANGED
|
@@ -361,6 +361,7 @@ export function createRegisterToolDefinition(deps) {
|
|
|
361
361
|
state_dir: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.state_dir),
|
|
362
362
|
template: unionSchema([
|
|
363
363
|
stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
|
|
364
|
+
looseObjectSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
|
|
364
365
|
arraySchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.templateArray),
|
|
365
366
|
nullSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.templateNull),
|
|
366
367
|
]),
|
|
@@ -390,6 +391,16 @@ function messageBodyToToolParams(message) {
|
|
|
390
391
|
return {};
|
|
391
392
|
return { input: message.body };
|
|
392
393
|
}
|
|
394
|
+
function formatToolActorFailure(tool, message, params, error) {
|
|
395
|
+
const original = error instanceof Error ? error.message : String(error);
|
|
396
|
+
const paramsPreview = compactPreview(params, 240) ?? "{}";
|
|
397
|
+
return Object.assign(new Error(`tool actor ${tool} failed for message type ${message.type}: ${original}; params=${paramsPreview}`), {
|
|
398
|
+
message_type: message.type,
|
|
399
|
+
original_error: original,
|
|
400
|
+
params_preview: paramsPreview,
|
|
401
|
+
tool,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
393
404
|
function runIdFromActorAddress(address) {
|
|
394
405
|
if (!address)
|
|
395
406
|
return undefined;
|
|
@@ -672,15 +683,16 @@ export function createInspectToolDefinition(deps = {}) {
|
|
|
672
683
|
if (view !== "mailbox")
|
|
673
684
|
throw new Error("inspect branch:<run>/<branch> supports view=mailbox.");
|
|
674
685
|
const status = assertRunAccessibleToContext(runId, ctx);
|
|
675
|
-
const
|
|
686
|
+
const branchInbox = ActorRooms.readBranchInboxDiagnostics(String(status.state_dir ?? ""), runId, target, Number(input.lines || 40));
|
|
687
|
+
const messages = branchInbox.messages;
|
|
676
688
|
return {
|
|
677
689
|
content: [
|
|
678
690
|
{
|
|
679
691
|
type: "text",
|
|
680
|
-
text: maybeJsonText(messages, input.verbose === true, compactBranchInbox(messages.map((message) => ({ ...message })))),
|
|
692
|
+
text: maybeJsonText(input.verbose === true ? branchInbox : messages, input.verbose === true, `${compactBranchInbox(messages.map((message) => ({ ...message })))}${branchInbox.corrupted > 0 ? `\ncorrupted=${branchInbox.corrupted}` : ""}`),
|
|
681
693
|
},
|
|
682
694
|
],
|
|
683
|
-
details: { messages },
|
|
695
|
+
details: { corrupted: branchInbox.corrupted, messages },
|
|
684
696
|
};
|
|
685
697
|
}
|
|
686
698
|
switch (view) {
|
|
@@ -806,9 +818,7 @@ export function createActorMessageToolDefinition(deps = {}) {
|
|
|
806
818
|
const runId = address.value;
|
|
807
819
|
if (message.from)
|
|
808
820
|
assertMessageSenderBelongsToRun(message, runId, `branch:${runId}/<branch>`);
|
|
809
|
-
const status =
|
|
810
|
-
? assertRunExistsForActorMessage(runId)
|
|
811
|
-
: assertRunAccessibleToContext(runId, ctx);
|
|
821
|
+
const status = assertRunAccessibleToContext(runId, ctx);
|
|
812
822
|
const stateDir = String(status.state_dir ?? "");
|
|
813
823
|
if (stateDir && address.branch) {
|
|
814
824
|
const ensureBranchMember = (actorAddress) => {
|
|
@@ -834,7 +844,7 @@ export function createActorMessageToolDefinition(deps = {}) {
|
|
|
834
844
|
else if (address.kind === "room" && address.value && address.room) {
|
|
835
845
|
const runId = address.value;
|
|
836
846
|
assertMessageSenderBelongsToRun(message, runId, `room:${runId}`);
|
|
837
|
-
const status =
|
|
847
|
+
const status = assertRunAccessibleToContext(runId, ctx);
|
|
838
848
|
const stateDir = String(status.state_dir ?? "");
|
|
839
849
|
if (!stateDir)
|
|
840
850
|
throw new Error(`${message.to} has no run state directory.`);
|
|
@@ -853,7 +863,14 @@ export function createActorMessageToolDefinition(deps = {}) {
|
|
|
853
863
|
if (!tool || typeof tool.execute !== "function") {
|
|
854
864
|
throw new Error(`tool actor not found or not executable: ${address.value}`);
|
|
855
865
|
}
|
|
856
|
-
const
|
|
866
|
+
const toolParams = messageBodyToToolParams(message);
|
|
867
|
+
let toolResult;
|
|
868
|
+
try {
|
|
869
|
+
toolResult = await tool.execute(`message:${message.type}`, toolParams, _signal, _onUpdate, ctx);
|
|
870
|
+
}
|
|
871
|
+
catch (error) {
|
|
872
|
+
throw formatToolActorFailure(address.value, message, toolParams, error);
|
|
873
|
+
}
|
|
857
874
|
result = {
|
|
858
875
|
invoked: true,
|
|
859
876
|
sent: true,
|
package/index.ts
CHANGED
|
@@ -174,7 +174,7 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
|
|
|
174
174
|
observedRuns,
|
|
175
175
|
observedRunEventLines,
|
|
176
176
|
summary,
|
|
177
|
-
transitions.map((transition) => transition.run),
|
|
177
|
+
transitions.map((transition) => transition.stateDir ?? transition.run),
|
|
178
178
|
);
|
|
179
179
|
for (const event of outboxEvents) {
|
|
180
180
|
if (!Observability.shouldNotifyRunOutboxEvent(event)) continue;
|
package/lib/actor-rooms.ts
CHANGED
|
@@ -383,24 +383,86 @@ function updateRosterForMessage(
|
|
|
383
383
|
return roster;
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
-
export
|
|
386
|
+
export interface BranchInboxRecord extends ActorMessage {
|
|
387
|
+
id?: string;
|
|
388
|
+
queued_at?: string;
|
|
389
|
+
status?: string;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export interface BranchInboxReadResult {
|
|
393
|
+
corrupted: number;
|
|
394
|
+
messages: BranchInboxRecord[];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function readBranchInboxFile(
|
|
387
398
|
stateDir: string,
|
|
388
399
|
run: string,
|
|
389
400
|
address: string,
|
|
390
401
|
limit = 40,
|
|
391
|
-
):
|
|
402
|
+
): BranchInboxReadResult {
|
|
392
403
|
const branch = branchIdFromAddress(address, run);
|
|
393
404
|
if (!branch) throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
|
|
394
405
|
try {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
406
|
+
const lines = readJsonlTailLines(branchInboxFile(stateDir, branch), limit);
|
|
407
|
+
const messages: BranchInboxRecord[] = [];
|
|
408
|
+
let corrupted = 0;
|
|
409
|
+
for (const line of lines) {
|
|
410
|
+
try {
|
|
411
|
+
messages.push(JSON.parse(line));
|
|
412
|
+
} catch {
|
|
413
|
+
corrupted += 1;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return { corrupted, messages };
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { corrupted: 0, messages: [] };
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function readAllBranchInboxLines(
|
|
424
|
+
stateDir: string,
|
|
425
|
+
run: string,
|
|
426
|
+
address: string,
|
|
427
|
+
): Array<{ raw: string } | { message: BranchInboxRecord }> {
|
|
428
|
+
const branch = branchIdFromAddress(address, run);
|
|
429
|
+
if (!branch) throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
|
|
430
|
+
try {
|
|
431
|
+
const content = fs.readFileSync(branchInboxFile(stateDir, branch), "utf8");
|
|
432
|
+
return content
|
|
433
|
+
.split("\n")
|
|
434
|
+
.filter((line) => line.length > 0)
|
|
435
|
+
.map((line) => {
|
|
436
|
+
try {
|
|
437
|
+
return { message: JSON.parse(line) as BranchInboxRecord };
|
|
438
|
+
} catch {
|
|
439
|
+
return { raw: line };
|
|
440
|
+
}
|
|
441
|
+
});
|
|
398
442
|
} catch (error) {
|
|
399
443
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
400
444
|
throw error;
|
|
401
445
|
}
|
|
402
446
|
}
|
|
403
447
|
|
|
448
|
+
export function readBranchInboxMessages(
|
|
449
|
+
stateDir: string,
|
|
450
|
+
run: string,
|
|
451
|
+
address: string,
|
|
452
|
+
limit = 40,
|
|
453
|
+
): BranchInboxRecord[] {
|
|
454
|
+
return readBranchInboxFile(stateDir, run, address, limit).messages;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function readBranchInboxDiagnostics(
|
|
458
|
+
stateDir: string,
|
|
459
|
+
run: string,
|
|
460
|
+
address: string,
|
|
461
|
+
limit = 40,
|
|
462
|
+
): BranchInboxReadResult {
|
|
463
|
+
return readBranchInboxFile(stateDir, run, address, limit);
|
|
464
|
+
}
|
|
465
|
+
|
|
404
466
|
export function getBranchInboxTerminalRetainLimit(): number {
|
|
405
467
|
const value = Number(process.env.PI_ACTORS_BRANCH_INBOX_TERMINAL_RETAINED ?? "");
|
|
406
468
|
return Number.isInteger(value) && value >= 0
|
|
@@ -458,17 +520,23 @@ export function updateBranchInboxMessageStatus(
|
|
|
458
520
|
const releaseLock = acquireBranchInboxLock(stateDir, branch);
|
|
459
521
|
try {
|
|
460
522
|
const file = branchInboxFile(stateDir, branch);
|
|
461
|
-
const
|
|
523
|
+
const records = readAllBranchInboxLines(stateDir, run, address);
|
|
462
524
|
let changed = false;
|
|
463
525
|
const timestampKey = `${status}_at`;
|
|
464
|
-
const updated =
|
|
465
|
-
if (message.id !== id) return
|
|
526
|
+
const updated = records.map((record) => {
|
|
527
|
+
if (!("message" in record) || record.message.id !== id) return record;
|
|
466
528
|
changed = true;
|
|
467
|
-
return { ...message, ...metadata, [timestampKey]: new Date().toISOString(), status };
|
|
529
|
+
return { message: { ...record.message, ...metadata, [timestampKey]: new Date().toISOString(), status } };
|
|
468
530
|
});
|
|
469
531
|
if (!changed) return false;
|
|
470
|
-
const
|
|
471
|
-
|
|
532
|
+
const validMessages = updated.flatMap((record) => "message" in record ? [record.message] : []);
|
|
533
|
+
const compactedIds = new Set(compactBranchInboxMessages(validMessages).map((message) => message.id));
|
|
534
|
+
const lines = updated.flatMap((record) => {
|
|
535
|
+
if ("raw" in record) return [record.raw];
|
|
536
|
+
if (record.message.id && !compactedIds.has(record.message.id)) return [];
|
|
537
|
+
return [JSON.stringify(record.message)];
|
|
538
|
+
});
|
|
539
|
+
fs.writeFileSync(file, lines.length ? `${lines.join("\n")}\n` : "");
|
|
472
540
|
notifyActorWake(stateDir, address, "branch.inbox.status", { id, status });
|
|
473
541
|
return true;
|
|
474
542
|
} finally {
|