@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.
@@ -109,7 +109,12 @@ export function resolveRecipePath(
109
109
 
110
110
  function isBareRecipeName(value: string): boolean {
111
111
  const trimmed = value.trim();
112
- return Boolean(trimmed) && !trimmed.includes("/") && !trimmed.startsWith("~") && !trimmed.includes("{");
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(value: string, currentRecipeRoot: string): string {
122
- if (!isBareRecipeName(value)) return resolveRecipePath(value, currentRecipeRoot);
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) => parseMarkdownScalar(line.replace(/^\s*-\s+/, "")));
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(/^\s{2}([A-Za-z_][A-Za-z0-9_.-]*):\s*(.*)$/);
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((line, index) => index > 0 && line.trim() === "---");
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) return Object.hasOwn(frontmatter, "template") ? frontmatter : undefined;
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")) return undefined;
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) => expandImportNodes(item, imports, options) as CommandTemplateConfig,
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(imported.config.template, defaults, record);
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(getImportFrom(binding), dirname(path));
676
- const config = readResolvedRecipeConfig(importPath, [...stack, path], options);
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(getImportFrom(binding), dirname(path));
840
- return collectRecipeContextRecords(importFile, [...stack, path], [...importPath, childAlias], childAlias);
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 messages = ActorRooms.readBranchInboxMessages(
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 = message.from
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 = assertRunExistsForActorMessage(runId);
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 toolResult = await tool.execute(
1189
- `message:${message.type}`,
1190
- messageBodyToToolParams(message),
1191
- _signal,
1192
- _onUpdate,
1193
- ctx,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.22.3",
3
+ "version": "0.22.4",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -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.3
5
+ version: 0.22.4
6
6
  ---
7
7
 
8
8
  # Actors (pi-actors)
@@ -2,7 +2,7 @@
2
2
  name: swarm
3
3
  description: Subagent orchestration with scoped locks and quorum consensus. Use for multi-model review, parallel scoped work, delegated audit, and coordinated subagent execution.
4
4
  metadata:
5
- version: 0.22.3
5
+ version: 0.22.4
6
6
  ---
7
7
 
8
8
  # Swarm