@llblab/pi-actors 0.22.3 → 0.22.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 messages = ActorRooms.readBranchInboxMessages(String(status.state_dir ?? ""), runId, target, Number(input.lines || 40));
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 = message.from
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 = assertRunExistsForActorMessage(runId);
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 toolResult = await tool.execute(`message:${message.type}`, messageBodyToToolParams(message), _signal, _onUpdate, ctx);
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;
@@ -383,24 +383,86 @@ function updateRosterForMessage(
383
383
  return roster;
384
384
  }
385
385
 
386
- export function readBranchInboxMessages(
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
- ): Array<ActorMessage & { id?: string; queued_at?: string; status?: string }> {
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
- return readJsonlTailLines(branchInboxFile(stateDir, branch), limit).map(
396
- (line) => JSON.parse(line),
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 messages = readBranchInboxMessages(stateDir, run, address, Number.MAX_SAFE_INTEGER);
523
+ const records = readAllBranchInboxLines(stateDir, run, address);
462
524
  let changed = false;
463
525
  const timestampKey = `${status}_at`;
464
- const updated = messages.map((message) => {
465
- if (message.id !== id) return message;
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 compacted = compactBranchInboxMessages(updated);
471
- fs.writeFileSync(file, `${compacted.map((message) => JSON.stringify(message)).join("\n")}\n`);
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 {
package/lib/async-runs.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  * Owns detached run state, observation, log tailing, listing, and cancellation safety
5
5
  */
6
6
 
7
- import { randomUUID } from "node:crypto";
8
7
  import { spawn, spawnSync } from "node:child_process";
8
+ import { randomUUID } from "node:crypto";
9
9
  import {
10
10
  closeSync,
11
11
  constants,
@@ -133,7 +133,10 @@ const DEFAULT_RECIPE_ROOT = Paths.getRecipeRoot();
133
133
 
134
134
  function packageRoot(): string {
135
135
  const moduleDir = dirname(fileURLToPath(import.meta.url));
136
- if (basename(moduleDir) === "lib" && basename(dirname(moduleDir)) === "dist") {
136
+ if (
137
+ basename(moduleDir) === "lib" &&
138
+ basename(dirname(moduleDir)) === "dist"
139
+ ) {
137
140
  return dirname(dirname(moduleDir));
138
141
  }
139
142
  return dirname(moduleDir);
@@ -409,84 +412,88 @@ export function startRun(
409
412
  try {
410
413
  assertNoActiveRunState(stateDir);
411
414
  prepareStateDirForStart(stateDir);
412
- const stdout = join(stateDir, "stdout.log");
413
- const stderr = join(stateDir, "stderr.log");
414
- const recipeFile = startParams.file
415
- ? resolveRecipeFile(startParams.file)
416
- : undefined;
417
- const recipe = startParams.name || getRunIdFromFile(recipeFile);
418
- const includeActorRecipeContext =
419
- startParams.actor_context !== false && startParams.actor_context !== "off";
420
- const recipeContextRecords = recipeFile && includeActorRecipeContext
421
- ? RecipeReferences.buildRecipeContextRecords(recipeFile)
422
- : undefined;
423
- if (recipeFile && isMutableUsageRecipeFile(recipeFile)) {
424
- RecipeUsage.recordRecipeLaunch(recipeFile);
425
- }
426
- const outFd = openSync(stdout, "a");
427
- const errFd = openSync(stderr, "a");
428
- const argv = asyncRunnerArgv(stateDir);
429
- const values = {
430
- ...(startParams.values || {}),
431
- actor_address: `run:${run}`,
432
- communication_file: join(stateDir, "communication.json"),
433
- default_room: `room:${run}`,
434
- run_id: run,
435
- state_dir: stateDir,
436
- };
437
- const outputValues = {
438
- ...(startParams.defaults || {}),
439
- ...values,
440
- };
441
- const artifacts = resolveArtifactPaths(startParams.artifacts, outputValues);
442
- const meta: AsyncRunMeta = {
443
- argv: [process.execPath, ...argv],
444
- createdAt: new Date().toISOString(),
445
- cwd,
446
- ...(startParams.launch_source ? { launch_source: startParams.launch_source } : {}),
447
- ...(startParams.ownerId ? { ownerId: startParams.ownerId } : {}),
448
- pid: 0,
449
- ...(recipe ? { recipe } : {}),
450
- ...(recipeFile ? { recipe_file: recipeFile } : {}),
451
- run,
452
- state_dir: stateDir,
453
- status: "running",
454
- ...(startParams.tool ? { tool: startParams.tool } : {}),
455
- template: resolved.template,
456
- values,
457
- ...(artifacts ? { artifacts } : {}),
458
- ...(startParams.control ? { control: startParams.control } : {}),
459
- ...(startParams.mailbox ? { mailbox: startParams.mailbox } : {}),
460
- ...(recipeContextRecords && recipeContextRecords.length > 0
461
- ? { recipe_context_records: recipeContextRecords }
462
- : {}),
463
- ...(startParams.retire_when === "children_terminal"
464
- ? { retire_when: "children_terminal" as const }
465
- : {}),
466
- };
467
- writeJsonAtomic(join(stateDir, "run.json"), meta);
468
- const child = spawn(process.execPath, argv, {
469
- cwd,
470
- detached: true,
471
- stdio: ["ignore", outFd, errFd],
472
- });
473
- closeSync(outFd);
474
- closeSync(errFd);
475
- meta.pid = child.pid ?? 0;
476
- writeJsonAtomic(join(stateDir, "run.json"), meta);
477
- writeJsonAtomic(join(stateDir, "progress.json"), {
478
- completed: 0,
479
- failures: [],
480
- phase: "starting",
481
- updatedAt: new Date().toISOString(),
482
- });
483
- writeFileSync(
484
- join(stateDir, "events.jsonl"),
485
- `${JSON.stringify({ event: "run.start", run, pid: meta.pid, ts: new Date().toISOString() })}\n`,
486
- { flag: "a" },
487
- );
488
- child.unref();
489
- return meta;
415
+ const stdout = join(stateDir, "stdout.log");
416
+ const stderr = join(stateDir, "stderr.log");
417
+ const recipeFile = startParams.file
418
+ ? resolveRecipeFile(startParams.file)
419
+ : undefined;
420
+ const recipe = startParams.name || getRunIdFromFile(recipeFile);
421
+ const includeActorRecipeContext =
422
+ startParams.actor_context !== false &&
423
+ startParams.actor_context !== "off";
424
+ const recipeContextRecords =
425
+ recipeFile && includeActorRecipeContext
426
+ ? RecipeReferences.buildRecipeContextRecords(recipeFile)
427
+ : undefined;
428
+ if (recipeFile && isMutableUsageRecipeFile(recipeFile)) {
429
+ RecipeUsage.recordRecipeLaunch(recipeFile);
430
+ }
431
+ const outFd = openSync(stdout, "a");
432
+ const errFd = openSync(stderr, "a");
433
+ const argv = asyncRunnerArgv(stateDir);
434
+ const values = {
435
+ ...(startParams.values || {}),
436
+ actor_address: `run:${run}`,
437
+ communication_file: join(stateDir, "communication.json"),
438
+ default_room: `room:${run}`,
439
+ run_id: run,
440
+ state_dir: stateDir,
441
+ };
442
+ const outputValues = {
443
+ ...(startParams.defaults || {}),
444
+ ...values,
445
+ };
446
+ const artifacts = resolveArtifactPaths(startParams.artifacts, outputValues);
447
+ const meta: AsyncRunMeta = {
448
+ argv: [process.execPath, ...argv],
449
+ createdAt: new Date().toISOString(),
450
+ cwd,
451
+ ...(startParams.launch_source
452
+ ? { launch_source: startParams.launch_source }
453
+ : {}),
454
+ ...(startParams.ownerId ? { ownerId: startParams.ownerId } : {}),
455
+ pid: 0,
456
+ ...(recipe ? { recipe } : {}),
457
+ ...(recipeFile ? { recipe_file: recipeFile } : {}),
458
+ run,
459
+ state_dir: stateDir,
460
+ status: "running",
461
+ ...(startParams.tool ? { tool: startParams.tool } : {}),
462
+ template: resolved.template,
463
+ values,
464
+ ...(artifacts ? { artifacts } : {}),
465
+ ...(startParams.control ? { control: startParams.control } : {}),
466
+ ...(startParams.mailbox ? { mailbox: startParams.mailbox } : {}),
467
+ ...(recipeContextRecords && recipeContextRecords.length > 0
468
+ ? { recipe_context_records: recipeContextRecords }
469
+ : {}),
470
+ ...(startParams.retire_when === "children_terminal"
471
+ ? { retire_when: "children_terminal" as const }
472
+ : {}),
473
+ };
474
+ writeJsonAtomic(join(stateDir, "run.json"), meta);
475
+ const child = spawn(process.execPath, argv, {
476
+ cwd,
477
+ detached: true,
478
+ stdio: ["ignore", outFd, errFd],
479
+ });
480
+ closeSync(outFd);
481
+ closeSync(errFd);
482
+ meta.pid = child.pid ?? 0;
483
+ writeJsonAtomic(join(stateDir, "run.json"), meta);
484
+ writeJsonAtomic(join(stateDir, "progress.json"), {
485
+ completed: 0,
486
+ failures: [],
487
+ phase: "starting",
488
+ updatedAt: new Date().toISOString(),
489
+ });
490
+ writeFileSync(
491
+ join(stateDir, "events.jsonl"),
492
+ `${JSON.stringify({ event: "run.start", run, pid: meta.pid, ts: new Date().toISOString() })}\n`,
493
+ { flag: "a" },
494
+ );
495
+ child.unref();
496
+ return meta;
490
497
  } finally {
491
498
  releaseStartLock();
492
499
  }
@@ -806,7 +813,10 @@ export function claimRunInboxMessage(
806
813
  ...messages[index],
807
814
  claimed_at: new Date().toISOString(),
808
815
  claimed_by: owner,
809
- id: typeof messages[index].id === "string" ? messages[index].id : randomUUID(),
816
+ id:
817
+ typeof messages[index].id === "string"
818
+ ? messages[index].id
819
+ : randomUUID(),
810
820
  status: "claimed",
811
821
  } satisfies RunInboxMessage;
812
822
  messages[index] = claimed;
@@ -928,9 +938,10 @@ function appendRunInboxMessage(stateDir: string, message: string): string {
928
938
  let record: Record<string, unknown>;
929
939
  try {
930
940
  const parsed = JSON.parse(message) as unknown;
931
- record = parsed && typeof parsed === "object" && !Array.isArray(parsed)
932
- ? (parsed as Record<string, unknown>)
933
- : { body: parsed, type: "run.message" };
941
+ record =
942
+ parsed && typeof parsed === "object" && !Array.isArray(parsed)
943
+ ? (parsed as Record<string, unknown>)
944
+ : { body: parsed, type: "run.message" };
934
945
  } catch {
935
946
  record = { body: message, type: "run.message" };
936
947
  }
@@ -1057,7 +1068,13 @@ export async function sendRunMessage(
1057
1068
  const payload = message.endsWith("\n") ? message : `${message}\n`;
1058
1069
  const payloadBytes = Buffer.byteLength(payload);
1059
1070
  const inboxId = appendRunInboxMessage(stateDir, message);
1060
- const wake = notifyRunMessageWake(stateDir, run, payloadBytes, endpoint, inboxId);
1071
+ const wake = notifyRunMessageWake(
1072
+ stateDir,
1073
+ run,
1074
+ payloadBytes,
1075
+ endpoint,
1076
+ inboxId,
1077
+ );
1061
1078
  const runtimePlatform = options.platform ?? process.platform;
1062
1079
  if (endpoint.type === "mailbox") {
1063
1080
  return {
@@ -1066,6 +1083,7 @@ export async function sendRunMessage(
1066
1083
  control: "inbox.jsonl",
1067
1084
  control_path: endpoint.path,
1068
1085
  control_type: endpoint.type,
1086
+ inbox_id: inboxId,
1069
1087
  queued: true,
1070
1088
  run,
1071
1089
  sent: true,
@@ -1087,6 +1105,7 @@ export async function sendRunMessage(
1087
1105
  control: "control.fifo",
1088
1106
  control_path: endpoint.path,
1089
1107
  control_type: endpoint.type,
1108
+ inbox_id: inboxId,
1090
1109
  run,
1091
1110
  sent: true,
1092
1111
  state_dir: stateDir,
@@ -1104,13 +1123,28 @@ export async function sendRunMessage(
1104
1123
  control: endpoint.path,
1105
1124
  control_path: endpoint.path,
1106
1125
  control_type: endpoint.type,
1126
+ inbox_id: inboxId,
1107
1127
  run,
1108
1128
  sent: true,
1109
1129
  state_dir: stateDir,
1110
1130
  };
1111
1131
  } catch (error) {
1112
- throw new Error(
1113
- `Run control endpoint is not ready: ${endpoint.path}: ${error instanceof Error ? error.message : String(error)}`,
1132
+ const deliveryError =
1133
+ error instanceof Error ? error.message : String(error);
1134
+ throw Object.assign(
1135
+ new Error(
1136
+ `Run control endpoint is not ready: ${endpoint.path}: ${deliveryError}`,
1137
+ ),
1138
+ {
1139
+ control_path: endpoint.path,
1140
+ control_type: endpoint.type,
1141
+ delivery_error: deliveryError,
1142
+ inbox_id: inboxId,
1143
+ queued: true,
1144
+ run,
1145
+ sent: false,
1146
+ state_dir: stateDir,
1147
+ },
1114
1148
  );
1115
1149
  }
1116
1150
  }
@@ -1150,7 +1184,9 @@ function signalOwnedRunProcess(
1150
1184
  const result = spawnSync(plan.command, plan.args, { encoding: "utf8" });
1151
1185
  if (result.status !== 0) {
1152
1186
  throw new Error(
1153
- result.stderr?.trim() || result.stdout?.trim() || `${plan.command} failed`,
1187
+ result.stderr?.trim() ||
1188
+ result.stdout?.trim() ||
1189
+ `${plan.command} failed`,
1154
1190
  );
1155
1191
  }
1156
1192
  return plan;