@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/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;
@@ -5,7 +5,14 @@
5
5
  */
6
6
 
7
7
  import { existsSync, readdirSync, readFileSync } from "node:fs";
8
- import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
8
+ import {
9
+ basename,
10
+ dirname,
11
+ isAbsolute,
12
+ join,
13
+ relative,
14
+ resolve,
15
+ } from "node:path";
9
16
 
10
17
  import * as AsyncRuns from "./async-runs.ts";
11
18
  import * as Paths from "./paths.ts";
@@ -229,13 +236,25 @@ export function summarizeRuns(
229
236
  const descendantSubagents = processSubagentsByRun.get(run.run) ?? 0;
230
237
  return descendantSubagents > 0 ? { ...run, descendantSubagents } : run;
231
238
  });
232
- const runningRuns = runsWithDescendants.filter((run) => run.status === "running");
239
+ const runningRuns = runsWithDescendants.filter(
240
+ (run) => run.status === "running",
241
+ );
233
242
  const running = runningRuns.length;
234
- const done = runsWithDescendants.filter((run) => run.status === "done").length;
235
- const exited = runsWithDescendants.filter((run) => run.status === "exited").length;
236
- const failed = runsWithDescendants.filter((run) => run.status === "failed").length;
237
- const cancelled = runsWithDescendants.filter((run) => run.status === "cancelled").length;
238
- const killed = runsWithDescendants.filter((run) => run.status === "killed").length;
243
+ const done = runsWithDescendants.filter(
244
+ (run) => run.status === "done",
245
+ ).length;
246
+ const exited = runsWithDescendants.filter(
247
+ (run) => run.status === "exited",
248
+ ).length;
249
+ const failed = runsWithDescendants.filter(
250
+ (run) => run.status === "failed",
251
+ ).length;
252
+ const cancelled = runsWithDescendants.filter(
253
+ (run) => run.status === "cancelled",
254
+ ).length;
255
+ const killed = runsWithDescendants.filter(
256
+ (run) => run.status === "killed",
257
+ ).length;
239
258
  const progressSubagents = runningRuns.reduce(
240
259
  (sum, run) => sum + Math.max(1, Math.floor(run.activeSubagents ?? 0)),
241
260
  0,
@@ -244,7 +263,10 @@ export function summarizeRuns(
244
263
  (sum, count) => sum + count,
245
264
  0,
246
265
  );
247
- const runningSubagents = Math.max(progressSubagents, running + processSubagents);
266
+ const runningSubagents = Math.max(
267
+ progressSubagents,
268
+ running + processSubagents,
269
+ );
248
270
  return {
249
271
  cancelled,
250
272
  done,
@@ -278,11 +300,16 @@ function getProcCommand(pid: string): string {
278
300
  return (readProcFile(`/proc/${pid}/cmdline`) ?? "").replaceAll("\0", " ");
279
301
  }
280
302
 
281
- function getRunningRunPidMap(stateRoot: string, ownerId?: string): Map<string, string> {
303
+ function getRunningRunPidMap(
304
+ stateRoot: string,
305
+ ownerId?: string,
306
+ ): Map<string, string> {
282
307
  const pids = new Map<string, string>();
283
308
  for (const run of summarizeRunsWithoutSubagents(stateRoot, ownerId).runs) {
284
309
  if (run.status !== "running") continue;
285
- const status = AsyncRuns.getRunStatus(run.stateDir ?? join(stateRoot, run.run));
310
+ const status = AsyncRuns.getRunStatus(
311
+ run.stateDir ?? join(stateRoot, run.run),
312
+ );
286
313
  const pid = Number(status.pid || 0);
287
314
  if (pid > 0) pids.set(String(pid), run.run);
288
315
  }
@@ -420,7 +447,10 @@ export function findRunRetirementCandidates(
420
447
  return summary.runs
421
448
  .map((run) => {
422
449
  const activeSubagents = Math.max(0, Math.floor(run.activeSubagents ?? 0));
423
- const descendantSubagents = Math.max(0, Math.floor(run.descendantSubagents ?? 0));
450
+ const descendantSubagents = Math.max(
451
+ 0,
452
+ Math.floor(run.descendantSubagents ?? 0),
453
+ );
424
454
  const childRuns = run.stateDir
425
455
  ? summary.runs.filter(
426
456
  (child) =>
@@ -429,7 +459,9 @@ export function findRunRetirementCandidates(
429
459
  isNestedStateDir(run.stateDir!, child.stateDir),
430
460
  )
431
461
  : [];
432
- const runningChildRuns = childRuns.filter((child) => child.status === "running").length;
462
+ const runningChildRuns = childRuns.filter(
463
+ (child) => child.status === "running",
464
+ ).length;
433
465
  return {
434
466
  activeSubagents,
435
467
  childRuns: childRuns.length,
@@ -441,7 +473,9 @@ export function findRunRetirementCandidates(
441
473
  !run.terminalHandled &&
442
474
  activeSubagents + descendantSubagents + runningChildRuns <= 0,
443
475
  run,
444
- terminalChildRuns: childRuns.filter((child) => TERMINAL.has(child.status)).length,
476
+ terminalChildRuns: childRuns.filter((child) =>
477
+ TERMINAL.has(child.status),
478
+ ).length,
445
479
  };
446
480
  })
447
481
  .filter((item) => item.ready)
@@ -462,7 +496,11 @@ export async function executeRunRetirements(
462
496
  const results: RunRetirementExecution[] = [];
463
497
  for (const candidate of findRunRetirementCandidates(summary)) {
464
498
  if (options.attempted?.has(candidate.stateDir)) {
465
- results.push({ action: "skip", run: candidate.run, stateDir: candidate.stateDir });
499
+ results.push({
500
+ action: "skip",
501
+ run: candidate.run,
502
+ stateDir: candidate.stateDir,
503
+ });
466
504
  continue;
467
505
  }
468
506
  options.attempted?.add(candidate.stateDir);
@@ -472,12 +510,18 @@ export async function executeRunRetirements(
472
510
  `Retiring actor ${candidate.run} after child runs reached terminal state`,
473
511
  "info",
474
512
  );
475
- results.push({ action: "stop", run: candidate.run, stateDir: candidate.stateDir });
513
+ results.push({
514
+ action: "stop",
515
+ run: candidate.run,
516
+ stateDir: candidate.stateDir,
517
+ });
476
518
  continue;
477
519
  } catch (error) {
478
520
  try {
479
521
  const cancelResult = options.cancelRun(candidate);
480
- const cancelled = Boolean((cancelResult as { cancelled?: unknown }).cancelled);
522
+ const cancelled = Boolean(
523
+ (cancelResult as { cancelled?: unknown }).cancelled,
524
+ );
481
525
  options.notify?.(
482
526
  cancelled
483
527
  ? `Retiring actor ${candidate.run} by cancellation after graceful stop failed`
@@ -486,13 +530,23 @@ export async function executeRunRetirements(
486
530
  );
487
531
  results.push({
488
532
  action: cancelled ? "cancel" : "skip",
489
- ...(cancelled ? {} : { error: error instanceof Error ? error.message : String(error) }),
533
+ ...(cancelled
534
+ ? {}
535
+ : {
536
+ error: error instanceof Error ? error.message : String(error),
537
+ }),
490
538
  run: candidate.run,
491
539
  stateDir: candidate.stateDir,
492
540
  });
493
541
  } catch (cancelError) {
494
- const message = cancelError instanceof Error ? cancelError.message : String(cancelError);
495
- options.notify?.(`Actor retirement failed for ${candidate.run}: ${message}`, "error");
542
+ const message =
543
+ cancelError instanceof Error
544
+ ? cancelError.message
545
+ : String(cancelError);
546
+ options.notify?.(
547
+ `Actor retirement failed for ${candidate.run}: ${message}`,
548
+ "error",
549
+ );
496
550
  results.push({
497
551
  action: "failed",
498
552
  error: message,
@@ -505,13 +559,20 @@ export async function executeRunRetirements(
505
559
  return results;
506
560
  }
507
561
 
562
+ function runObservationKey(
563
+ run: Pick<RunObservation, "run" | "stateDir">,
564
+ ): string {
565
+ return run.stateDir ?? run.run;
566
+ }
567
+
508
568
  export function detectRunTransitions(
509
569
  previous: Map<string, RunObservedStatus>,
510
570
  summary: RunSummary,
511
571
  ): RunTransition[] {
512
572
  const transitions: RunTransition[] = [];
513
573
  for (const run of summary.runs) {
514
- const old = previous.get(run.run);
574
+ const key = runObservationKey(run);
575
+ const old = previous.get(key);
515
576
  if (old && old !== run.status && TERMINAL.has(run.status)) {
516
577
  transitions.push({
517
578
  from: old,
@@ -525,7 +586,7 @@ export function detectRunTransitions(
525
586
  ...(run.tool ? { tool: run.tool } : {}),
526
587
  });
527
588
  }
528
- previous.set(run.run, run.status);
589
+ previous.set(key, run.status);
529
590
  }
530
591
  return transitions;
531
592
  }
@@ -599,12 +660,12 @@ export function pruneRunObservationState(
599
660
  summary: RunSummary,
600
661
  terminalRuns: Iterable<string> = [],
601
662
  ): void {
602
- const activeRuns = new Set(summary.runs.map((run) => run.run));
663
+ const activeRuns = new Set(summary.runs.map((run) => runObservationKey(run)));
603
664
  const terminalRunSet = new Set(terminalRuns);
604
665
  const terminalLineKeys = new Set(
605
666
  summary.runs
606
- .filter((run) => terminalRunSet.has(run.run))
607
- .map((run) => run.stateDir ?? run.run),
667
+ .filter((run) => terminalRunSet.has(runObservationKey(run)))
668
+ .map((run) => runObservationKey(run)),
608
669
  );
609
670
  const activeLineKeys = new Set(
610
671
  summary.runs.map((run) => run.stateDir ?? run.run),