@llblab/pi-actors 0.31.0 → 0.33.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/lib/paths.ts CHANGED
@@ -24,7 +24,7 @@ export interface ExtensionRuntimePaths {
24
24
  }
25
25
 
26
26
  export function getConfigPath(agentDir = getAgentDir()): string {
27
- return join(agentDir, "legacy-tool-registry.json");
27
+ return join(agentDir, "tool-registry.json");
28
28
  }
29
29
 
30
30
  export function getExtensionTmpDir(
@@ -63,8 +63,8 @@ export function getRecipeRoot(agentDir = getAgentDir()): string {
63
63
  return join(agentDir, "recipes");
64
64
  }
65
65
 
66
- export function getRecipeCandidateRoot(agentDir = getAgentDir()): string {
67
- return join(getRecipeRoot(agentDir), "candidates");
66
+ export function getRecipeDraftRoot(agentDir = getAgentDir()): string {
67
+ return join(getRecipeRoot(agentDir), "drafts");
68
68
  }
69
69
 
70
70
  export function getPackagedRecipeRoot(): string {
@@ -461,16 +461,16 @@ function remediationForEntry(
461
461
  const riskyDiagnostics = entry.diagnostics.filter(
462
462
  (message) => diagnosticSeverity(message) === "warning",
463
463
  );
464
- const blockedCandidate = entry.shadows[0];
464
+ const blockedFallback = entry.shadows[0];
465
465
  if (entry.invalid) {
466
466
  return {
467
467
  id: entry.id,
468
- kind: blockedCandidate ? "blocking_invalid" : "invalid",
468
+ kind: blockedFallback ? "blocking_invalid" : "invalid",
469
469
  severity: "error",
470
470
  path: entry.path,
471
- ...(blockedCandidate ? { blocked_candidate: blockedCandidate } : {}),
472
- reason: blockedCandidate
473
- ? "invalid higher-priority recipe blocks a lower-priority candidate"
471
+ ...(blockedFallback ? { blocked_fallback: blockedFallback } : {}),
472
+ reason: blockedFallback
473
+ ? "invalid higher-priority recipe blocks a lower-priority fallback"
474
474
  : "recipe is invalid and cannot be exposed as a tool",
475
475
  action:
476
476
  "fix recipe syntax/config, or disable/delete/archive it to restore fallback",
@@ -479,12 +479,12 @@ function remediationForEntry(
479
479
  if (entry.disabled && entry.active) {
480
480
  return {
481
481
  id: entry.id,
482
- kind: blockedCandidate ? "blocking_disabled" : "disabled",
482
+ kind: blockedFallback ? "blocking_disabled" : "disabled",
483
483
  severity: "warning",
484
484
  path: entry.path,
485
- ...(blockedCandidate ? { blocked_candidate: blockedCandidate } : {}),
486
- reason: blockedCandidate
487
- ? "disabled higher-priority recipe intentionally blocks a lower-priority candidate"
485
+ ...(blockedFallback ? { blocked_fallback: blockedFallback } : {}),
486
+ reason: blockedFallback
487
+ ? "disabled higher-priority recipe intentionally blocks a lower-priority fallback"
488
488
  : "recipe is disabled and not exposed as a tool",
489
489
  action:
490
490
  "keep disabled intentionally, re-enable, or delete/archive the file",
@@ -568,13 +568,13 @@ export function getShadowedLaunchDiagnostic(
568
568
  if (!active.invalid && !active.disabled) return undefined;
569
569
  return {
570
570
  active_path: active.path,
571
- blocked_candidate: active.shadows[0],
571
+ blocked_fallback: active.shadows[0],
572
572
  hint: "inspect_recipes_doctor",
573
573
  reason: active.invalid ? "shadowed_invalid" : "shadowed_disabled",
574
574
  };
575
575
  }
576
576
 
577
- export function listCandidateRecipes(root: string): Array<Record<string, unknown>> {
577
+ export function listDraftRecipes(root: string): Array<Record<string, unknown>> {
578
578
  return listRecipeFiles(root).map((path) => {
579
579
  const id = RecipeReferences.getRecipeIdFromPath(path);
580
580
  const config = RecipeReferences.readRawRecipeConfig(path);
package/lib/tools.ts CHANGED
@@ -27,6 +27,12 @@ export type RegisterToolInput = Registry.RegisterToolInput;
27
27
  export type RegisterToolRuntimeDeps<TContext> =
28
28
  Registry.RegisterToolRuntimeDeps<TContext>;
29
29
 
30
+ export interface ActorToolDefinition {
31
+ name: string;
32
+ execute?: (...args: never[]) => unknown;
33
+ [key: string]: unknown;
34
+ }
35
+
30
36
  export interface CoreActorToolDefinitionDeps<TContext extends AsyncRunToolContext> {
31
37
  configPath: string;
32
38
  getActiveTools: () => string[];
@@ -222,8 +228,8 @@ function compactAsyncRunStatus(value: unknown): string {
222
228
  tokens.push(`failures=${failures}`);
223
229
  if (result.code !== undefined) tokens.push(`code=${String(result.code)}`);
224
230
  if (result.killed === true) tokens.push("killed=true");
225
- if (status.candidate_recipe)
226
- tokens.push(`candidate_recipe=${String(status.candidate_recipe)}`);
231
+ const draftRecipe = status.draft_recipe;
232
+ if (draftRecipe) tokens.push(`draft_recipe=${String(draftRecipe)}`);
227
233
  const nextActions = actorRunNextActions(run);
228
234
  if (nextActions.length > 0)
229
235
  tokens.push(`next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`);
@@ -451,6 +457,15 @@ function compactArtifactPath(value: unknown): string {
451
457
  return String(record.path ?? "<missing>");
452
458
  }
453
459
 
460
+ function artifactNextActions(run: unknown, artifacts: Record<string, unknown>): string[] {
461
+ const id = String(run ?? "").trim();
462
+ if (!id || Object.keys(artifacts).length === 0) return [];
463
+ return [
464
+ `inspect target=run:${id} view=artifacts verbose=true`,
465
+ `inspect target=run:${id} view=messages`,
466
+ ];
467
+ }
468
+
454
469
  function compactActorFiles(status: Record<string, unknown>): string {
455
470
  const run = String(status.run ?? "<unknown>");
456
471
  const artifacts = asRecord(status.artifacts);
@@ -469,7 +484,11 @@ function compactActorFiles(status: Record<string, unknown>): string {
469
484
  .map(([key, value]) => `${key}:${compactArtifactPath(value)}`)
470
485
  .join(",")}`
471
486
  : "";
472
- return `\nrun=${run}${artifactText}${files.length ? ` files=${files.join(",")}` : ""}`;
487
+ const nextActions = artifactNextActions(run, artifacts);
488
+ const nextText = nextActions.length
489
+ ? ` next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`
490
+ : "";
491
+ return `\nrun=${run}${artifactText}${files.length ? ` files=${files.join(",")}` : ""}${nextText}`;
473
492
  }
474
493
 
475
494
  function summarizeOtherSessions(
@@ -614,16 +633,50 @@ function compactRecipeDoctor(summary: Record<string, unknown>): string {
614
633
  item.action,
615
634
  Limits.DOCTOR_ACTION_PREVIEW_CHARS,
616
635
  );
617
- const blocked = item.blocked_candidate
618
- ? ` blocked=${compactPreview(item.blocked_candidate, Limits.DOCTOR_ACTION_PREVIEW_CHARS)}`
636
+ const blocked = item.blocked_fallback
637
+ ? ` blocked=${compactPreview(item.blocked_fallback, Limits.DOCTOR_ACTION_PREVIEW_CHARS)}`
619
638
  : "";
620
639
  lines.push(
621
640
  `${String(item.severity ?? "info")} kind=${String(item.kind ?? "inspect")} id=${String(item.id ?? "root")}${blocked} action=${action ?? "inspect"}`,
622
641
  );
623
642
  }
643
+ const nextActions = Array.isArray(summary.next_actions)
644
+ ? (summary.next_actions as string[])
645
+ : [];
646
+ if (nextActions.length > 0) lines[0] = `${lines[0]}${compactNextActions(nextActions)}`;
624
647
  return `\n${lines.join("\n")}`;
625
648
  }
626
649
 
650
+ function recipeRegistryNextActions(summary: Record<string, unknown>, view: string): string[] {
651
+ const actions: string[] = [];
652
+ const drafts = Array.isArray(summary.drafts)
653
+ ? (summary.drafts as Array<Record<string, unknown>>)
654
+ : [];
655
+ const invalid = Array.isArray(summary.invalid) ? summary.invalid.length : 0;
656
+ const diagnostics = Array.isArray(summary.diagnostics)
657
+ ? summary.diagnostics.length
658
+ : 0;
659
+ const topAction = asRecord(summary.top_action);
660
+ if (view !== "doctor" && (invalid > 0 || diagnostics > 0)) {
661
+ actions.push("inspect target=recipes view=doctor");
662
+ }
663
+ if (view === "doctor" && typeof topAction.action === "string") {
664
+ actions.push(String(topAction.action));
665
+ }
666
+ if (drafts.length > 0) {
667
+ actions.push("inspect target=recipes view=summary verbose=true");
668
+ const firstPath = typeof drafts[0]?.path === "string" ? drafts[0].path : undefined;
669
+ if (firstPath) actions.push(`spawn file=${firstPath}`);
670
+ }
671
+ return [...new Set(actions)].slice(0, 4);
672
+ }
673
+
674
+ function compactNextActions(actions: string[]): string {
675
+ return actions.length
676
+ ? ` next=${actions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`
677
+ : "";
678
+ }
679
+
627
680
  function compactRecipeRegistry(summary: Record<string, unknown>): string {
628
681
  const active = Array.isArray(summary.active) ? summary.active.length : 0;
629
682
  const shadowed = Array.isArray(summary.shadowed)
@@ -636,13 +689,39 @@ function compactRecipeRegistry(summary: Record<string, unknown>): string {
636
689
  const diagnostics = Array.isArray(summary.diagnostics)
637
690
  ? summary.diagnostics.length
638
691
  : 0;
639
- const candidates = Array.isArray(summary.candidates)
640
- ? summary.candidates.length
641
- : 0;
692
+ const drafts = Array.isArray(summary.drafts) ? summary.drafts.length : 0;
642
693
  const recommendations = Array.isArray(summary.recommendations)
643
694
  ? summary.recommendations.length
644
695
  : 0;
645
- return `\nrecipes active=${active} candidates=${candidates} shadowed=${shadowed} invalid=${invalid} disabled=${disabled} recommendations=${recommendations} diagnostics=${diagnostics}`;
696
+ const nextActions = Array.isArray(summary.next_actions)
697
+ ? (summary.next_actions as string[])
698
+ : [];
699
+ return `\nrecipes active=${active} drafts=${drafts} shadowed=${shadowed} invalid=${invalid} disabled=${disabled} recommendations=${recommendations} diagnostics=${diagnostics}${compactNextActions(nextActions)}`;
700
+ }
701
+
702
+ function actorMessageNextActions(
703
+ message: ActorMessages.ActorMessage,
704
+ result: Record<string, unknown>,
705
+ ): string[] {
706
+ const actions: string[] = [];
707
+ const address = ActorMessages.parseActorAddress(message.to);
708
+ if (result.delivery_error || result.sent === false) {
709
+ if (address.kind === "run" && address.value) {
710
+ actions.push(`inspect target=run:${address.value} view=status`);
711
+ actions.push(`inspect target=run:${address.value} view=mailbox`);
712
+ } else if (address.kind === "branch" && address.value) {
713
+ actions.push(`inspect target=branch:${address.value}/${address.branch ?? "main"} view=mailbox`);
714
+ actions.push(`inspect target=run:${address.value} view=status`);
715
+ }
716
+ }
717
+ if (result.queued === true) {
718
+ if (address.kind === "branch" && address.value) {
719
+ actions.push(`inspect target=branch:${address.value}/${address.branch ?? "main"} view=mailbox`);
720
+ } else if (address.kind === "run" && address.value) {
721
+ actions.push(`inspect target=run:${address.value} view=mailbox`);
722
+ }
723
+ }
724
+ return [...new Set(actions)].slice(0, 3);
646
725
  }
647
726
 
648
727
  function compactActorMessageResult(
@@ -670,6 +749,11 @@ function compactActorMessageResult(
670
749
  if (result.delivery_error) {
671
750
  tokens.push(`delivery_error=${compactPreview(result.delivery_error, 96)}`);
672
751
  }
752
+ const nextActions = Array.isArray(result.next_actions)
753
+ ? (result.next_actions as string[])
754
+ : actorMessageNextActions(message, result);
755
+ if (nextActions.length > 0)
756
+ tokens.push(`next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`);
673
757
  return `\n${tokens.join(" ")}`;
674
758
  }
675
759
 
@@ -791,11 +875,11 @@ function shadowedRecipeLaunchDiagnostic(
791
875
  return RecipeDiscovery.getShadowedLaunchDiagnostic(discovery, recipe);
792
876
  }
793
877
 
794
- function candidateRecipeName(run: string): string {
878
+ function draftRecipeName(run: string): string {
795
879
  return `${run.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "spawn"}.json`;
796
880
  }
797
881
 
798
- function candidateRecipeDefaults(
882
+ function draftRecipeDefaults(
799
883
  values: Record<string, unknown>,
800
884
  ): Record<string, unknown> | undefined {
801
885
  const ignored = new Set([
@@ -811,13 +895,13 @@ function candidateRecipeDefaults(
811
895
  return Object.keys(defaults).length > 0 ? defaults : undefined;
812
896
  }
813
897
 
814
- function writeSpawnCandidateRecipe(
898
+ function writeSpawnDraftRecipe(
815
899
  input: Record<string, unknown>,
816
900
  meta: AsyncRuns.AsyncRunMeta,
817
901
  ): string | undefined {
818
902
  if (
819
903
  process.env.NODE_TEST_CONTEXT &&
820
- process.env.PI_ACTORS_ENABLE_SPAWN_CANDIDATES_IN_TEST !== "1"
904
+ process.env.PI_ACTORS_ENABLE_SPAWN_DRAFTS_IN_TEST !== "1"
821
905
  )
822
906
  return undefined;
823
907
  if (
@@ -826,13 +910,13 @@ function writeSpawnCandidateRecipe(
826
910
  input.recipe !== undefined
827
911
  )
828
912
  return undefined;
829
- const root = Paths.getRecipeCandidateRoot();
913
+ const root = Paths.getRecipeDraftRoot();
830
914
  mkdirSync(root, { recursive: true });
831
- const path = join(root, candidateRecipeName(String(meta.run)));
832
- const defaults = candidateRecipeDefaults(meta.values);
915
+ const path = join(root, draftRecipeName(String(meta.run)));
916
+ const defaults = draftRecipeDefaults(meta.values);
833
917
  const recipe = {
834
918
  async: true,
835
- description: `Candidate recipe captured from spawn run ${String(meta.run)}`,
919
+ description: `Draft recipe captured from spawn run ${String(meta.run)}`,
836
920
  ...(meta.artifacts ? { artifacts: meta.artifacts } : {}),
837
921
  ...(defaults ? { defaults } : {}),
838
922
  template: input.template,
@@ -848,7 +932,7 @@ function enhanceSpawnRecipeError(error: unknown, recipe: unknown): Error {
848
932
  const original = error instanceof Error ? error.message : String(error);
849
933
  return Object.assign(
850
934
  new Error(
851
- `${original} reason=${diagnostic.reason} active_path=${diagnostic.active_path} blocked_candidate=${diagnostic.blocked_candidate} hint=${diagnostic.hint}`,
935
+ `${original} reason=${diagnostic.reason} active_path=${diagnostic.active_path} blocked_fallback=${diagnostic.blocked_fallback} hint=${diagnostic.hint}`,
852
936
  ),
853
937
  {
854
938
  ...diagnostic,
@@ -1036,11 +1120,11 @@ export function createSpawnToolDefinition<
1036
1120
  } catch (error) {
1037
1121
  throw enhanceSpawnRecipeError(error, recipe);
1038
1122
  }
1039
- const candidateRecipe = writeSpawnCandidateRecipe(input, meta);
1123
+ const draftRecipe = writeSpawnDraftRecipe(input, meta);
1040
1124
  const nextActions = actorRunNextActions(meta.run);
1041
1125
  const details = {
1042
1126
  ...meta,
1043
- ...(candidateRecipe ? { candidate_recipe: candidateRecipe } : {}),
1127
+ ...(draftRecipe ? { draft_recipe: draftRecipe } : {}),
1044
1128
  next_actions: nextActions,
1045
1129
  };
1046
1130
  ActorRooms.ensureDefaultRoom(meta.state_dir, String(meta.run));
@@ -1084,6 +1168,33 @@ function requireContextSessionId(ctx: unknown, actor: string): string {
1084
1168
  return sessionId;
1085
1169
  }
1086
1170
 
1171
+ function sessionMismatchError(input: {
1172
+ currentSession?: string;
1173
+ expectedSession?: string;
1174
+ run?: string;
1175
+ target?: string;
1176
+ }): Error {
1177
+ const ownerSession = input.expectedSession ?? "none";
1178
+ const currentSession = input.currentSession ?? "none";
1179
+ const actor = input.run ? `run:${input.run}` : (input.target ?? "session");
1180
+ const hintTarget = input.expectedSession
1181
+ ? `session:${input.expectedSession}`
1182
+ : "session:all";
1183
+ return Object.assign(
1184
+ new Error(
1185
+ `${actor} reason=session_mismatch owner_session=${ownerSession} current_session=${currentSession} hint=inspect_session:${input.expectedSession ?? "all"}`,
1186
+ ),
1187
+ {
1188
+ current_session: input.currentSession,
1189
+ hint: `inspect target=${hintTarget} view=status`,
1190
+ owner_session: input.expectedSession,
1191
+ reason: "session_mismatch",
1192
+ run: input.run,
1193
+ target: input.target,
1194
+ },
1195
+ );
1196
+ }
1197
+
1087
1198
  function assertRunAccessibleToContext(
1088
1199
  runId: string,
1089
1200
  ctx: unknown,
@@ -1091,18 +1202,11 @@ function assertRunAccessibleToContext(
1091
1202
  const status = AsyncRuns.getRunStatus(runId);
1092
1203
  const sessionId = getContextSessionId(ctx);
1093
1204
  if (sessionId && status.ownerId && status.ownerId !== sessionId) {
1094
- throw Object.assign(
1095
- new Error(
1096
- `run:${runId} reason=session_mismatch owner_session=${status.ownerId} current_session=${sessionId} hint=inspect_session:${status.ownerId}`,
1097
- ),
1098
- {
1099
- current_session: sessionId,
1100
- hint: `inspect target=session:${status.ownerId} view=status`,
1101
- owner_session: status.ownerId,
1102
- reason: "session_mismatch",
1103
- run: runId,
1104
- },
1105
- );
1205
+ throw sessionMismatchError({
1206
+ currentSession: sessionId,
1207
+ expectedSession: String(status.ownerId),
1208
+ run: runId,
1209
+ });
1106
1210
  }
1107
1211
  return status;
1108
1212
  }
@@ -1120,7 +1224,7 @@ export function createInspectToolDefinition<TContext = unknown>(
1120
1224
  name: "inspect",
1121
1225
  label: "Inspect",
1122
1226
  description:
1123
- "Intentionally inspect an actor at decision points, after follow-ups, or during diagnosis instead of polling. Supports run:<id> views: status, tail, messages, artifacts, files, mailbox, communication; room:<run> status/messages/previews/roster/contacts; coordinator/session status; and tool:<name> status/schema.",
1227
+ "Intentionally inspect actors at decision points, after follow-ups, or during diagnosis instead of polling. Core targets are run:<id> and tool:<name>; advanced targets include branch:<run>/<branch>, room:<run>, coordinator, session:<id>, and session:all.",
1124
1228
  parameters: objectSchema(
1125
1229
  {
1126
1230
  lines: stringSchema("Line count for tail/messages views. Default 40."),
@@ -1128,13 +1232,13 @@ export function createInspectToolDefinition<TContext = unknown>(
1128
1232
  "Optional session run filter: all, running, active, terminal, done, failed, cancelled, killed, or exited.",
1129
1233
  ),
1130
1234
  target: stringSchema(
1131
- "Actor address to inspect, e.g. run:<id>, room:<run>, coordinator, session:<id>, session:all, or tool:<name>.",
1235
+ "Actor address to inspect, e.g. run:<id> or tool:<name>; advanced: branch:<run>/<branch>, room:<run>, coordinator, session:<id>, session:all.",
1132
1236
  ),
1133
1237
  verbose: booleanSchema(
1134
1238
  "Return full JSON instead of compact text where available.",
1135
1239
  ),
1136
1240
  view: stringSchema(
1137
- "Inspection view: status, tail, messages, artifacts, files, mailbox, communication, roster, or contacts.",
1241
+ "Inspection view. Core run views: status, tail, messages, artifacts, files, mailbox. Advanced views include communication, roster, and contacts.",
1138
1242
  ),
1139
1243
  },
1140
1244
  ["target", "view"],
@@ -1169,11 +1273,13 @@ export function createInspectToolDefinition<TContext = unknown>(
1169
1273
  { root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
1170
1274
  ]);
1171
1275
  const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
1172
- const summary = {
1276
+ const summaryBase = {
1173
1277
  ...RecipeDiscovery.summarizeDiscovery(discovered),
1174
- candidates: RecipeDiscovery.listCandidateRecipes(
1175
- join(recipeRoot, "candidates"),
1176
- ),
1278
+ drafts: RecipeDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
1279
+ };
1280
+ const summary = {
1281
+ ...summaryBase,
1282
+ next_actions: recipeRegistryNextActions(summaryBase, view),
1177
1283
  };
1178
1284
  return {
1179
1285
  content: [
@@ -1488,7 +1594,14 @@ export function createInspectToolDefinition<TContext = unknown>(
1488
1594
  | undefined,
1489
1595
  );
1490
1596
  const details = artifactManifest
1491
- ? { ...status, artifact_manifest: artifactManifest }
1597
+ ? {
1598
+ ...status,
1599
+ artifact_manifest: artifactManifest,
1600
+ next_actions: artifactNextActions(
1601
+ status.run ?? runId,
1602
+ asRecord(status.artifacts),
1603
+ ),
1604
+ }
1492
1605
  : status;
1493
1606
  return {
1494
1607
  content: [
@@ -1574,7 +1687,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
1574
1687
  name: "message",
1575
1688
  label: "Message",
1576
1689
  description:
1577
- "Send one typed addressed message to steer an existing actor instead of restarting it. Routes to run:<id> mailboxes, branch:<run>/<branch> mailboxes, room:<run> timelines/rosters, tool:<name> calls, and coordinator/session-bound run messages.",
1690
+ "Send one typed addressed message to steer an existing actor instead of restarting it. Core routes are run:<id> and tool:<name>; advanced routes include branch:<run>/<branch>, room:<run> group timelines, coordinator, and session:<id>.",
1578
1691
  parameters: objectSchema(
1579
1692
  {
1580
1693
  body: unionSchema([
@@ -1596,7 +1709,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
1596
1709
  reply_to: stringSchema("Optional message id this message replies to."),
1597
1710
  summary: stringSchema("Optional short human-facing summary."),
1598
1711
  to: stringSchema(
1599
- "Destination actor address, e.g. run:<id>, branch:<run>/<branch>, room:<run>, coordinator, session:<id>, or tool:<name>.",
1712
+ "Destination actor address, e.g. run:<id> or tool:<name>; advanced: branch:<run>/<branch>, room:<run>, coordinator, session:<id>.",
1600
1713
  ),
1601
1714
  type: stringSchema(
1602
1715
  "Semantic message type, e.g. control.approve or checkpoint.needs_scope.",
@@ -1765,14 +1878,20 @@ export function createActorMessageToolDefinition<TContext = unknown>(
1765
1878
  const senderStatus = assertRunAccessibleToContext(sender.value, ctx);
1766
1879
  if (address.kind === "session") {
1767
1880
  if (!senderStatus.ownerId) {
1768
- throw new Error(
1769
- `message to session:${address.value} requires sender run owner ${address.value}; got no owner.`,
1770
- );
1881
+ throw sessionMismatchError({
1882
+ currentSession: undefined,
1883
+ expectedSession: address.value,
1884
+ run: sender.value,
1885
+ target: `session:${address.value}`,
1886
+ });
1771
1887
  }
1772
1888
  if (senderStatus.ownerId !== address.value) {
1773
- throw new Error(
1774
- `message to session:${address.value} requires sender run owner ${address.value}; got ${senderStatus.ownerId}.`,
1775
- );
1889
+ throw sessionMismatchError({
1890
+ currentSession: String(senderStatus.ownerId),
1891
+ expectedSession: address.value,
1892
+ run: sender.value,
1893
+ target: `session:${address.value}`,
1894
+ });
1776
1895
  }
1777
1896
  }
1778
1897
  result = AsyncRuns.appendRunOutboxEvent(sender.value, {
@@ -1796,18 +1915,22 @@ export function createActorMessageToolDefinition<TContext = unknown>(
1796
1915
  `message currently supports run:<id>, branch:<run>/<branch>, room:<run>, tool:<name>, coordinator, and session:<id> destinations; unsupported destination: ${message.to}`,
1797
1916
  );
1798
1917
  }
1918
+ const nextActions = actorMessageNextActions(message, result);
1919
+ const resultWithNext = nextActions.length
1920
+ ? { ...result, next_actions: nextActions }
1921
+ : result;
1799
1922
  return {
1800
1923
  content: [
1801
1924
  {
1802
1925
  type: "text" as const,
1803
1926
  text: maybeJsonText(
1804
- { message, result },
1927
+ { message, result: resultWithNext },
1805
1928
  input.verbose === true,
1806
- compactActorMessageResult(message, result),
1929
+ compactActorMessageResult(message, resultWithNext),
1807
1930
  ),
1808
1931
  },
1809
1932
  ],
1810
- details: { message, result },
1933
+ details: { message, result: resultWithNext },
1811
1934
  };
1812
1935
  },
1813
1936
  };
@@ -1815,7 +1938,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
1815
1938
 
1816
1939
  export function createCoreActorToolDefinitions<TContext extends AsyncRunToolContext>(
1817
1940
  deps: CoreActorToolDefinitionDeps<TContext>,
1818
- ): any[] {
1941
+ ): ActorToolDefinition[] {
1819
1942
  return [
1820
1943
  createRegisterToolDefinition<TContext>({
1821
1944
  configPath: deps.configPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.31.0",
3
+ "version": "0.33.0",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -1,33 +1,49 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Internal conformance runner shim.
4
+ * Internal conformance runner.
5
5
  *
6
- * Runtime logic lives in lib/conformance.ts and is compiled to
7
- * dist/lib/conformance.js for installed JS-only packages.
6
+ * This script is intentionally standalone package/release glue rather than a
7
+ * lib domain: it only selects regression suites and formats their summary.
8
8
  */
9
9
 
10
- import { existsSync } from "node:fs";
11
- import { dirname, join } from "node:path";
12
- import { fileURLToPath, pathToFileURL } from "node:url";
10
+ import { spawnSync } from "node:child_process";
11
+ import { dirname } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const conformanceSuites = [
15
+ "tests/protocol-examples.test.ts",
16
+ "tests/recipe-discovery.test.ts",
17
+ "tests/registry.test.ts",
18
+ "tests/runtime-registry.test.ts",
19
+ "tests/async-runs.test.ts",
20
+ "tests/actor-rooms.test.ts",
21
+ "tests/tools.test.ts",
22
+ ];
13
23
 
14
24
  function packageRoot() {
15
25
  return dirname(dirname(fileURLToPath(import.meta.url)));
16
26
  }
17
27
 
18
- function mainModulePath() {
19
- const root = packageRoot();
20
- const compiled = join(root, "dist", "lib", "conformance.js");
21
- return existsSync(compiled) ? compiled : join(root, "lib", "conformance.ts");
22
- }
28
+ const result = spawnSync(
29
+ process.execPath,
30
+ ["--experimental-strip-types", "--test", ...conformanceSuites],
31
+ { cwd: packageRoot(), encoding: "utf8", stdio: "pipe" },
32
+ );
23
33
 
24
- const { runConformance } = await import(pathToFileURL(mainModulePath()).href);
25
- const report = runConformance(packageRoot());
34
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
35
+ const summary = output
36
+ .split("\n")
37
+ .filter((line) =>
38
+ /^ℹ (tests|pass|fail|cancelled|skipped|todo|duration_ms) /.test(line),
39
+ )
40
+ .join("\n");
26
41
 
27
42
  console.log("pi-actors conformance");
28
- console.log(`suites ${report.suites}`);
29
- if (report.summary) console.log(report.summary);
30
- if (report.code !== 0) {
31
- console.error(report.output.trimEnd());
32
- process.exit(report.code);
43
+ console.log(`suites ${conformanceSuites.length}`);
44
+
45
+ if (summary) console.log(summary);
46
+ if ((result.status ?? 1) !== 0) {
47
+ console.error(output.trimEnd());
48
+ process.exit(result.status ?? 1);
33
49
  }