@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/AGENTS.md +7 -3
- package/BACKLOG.md +14 -55
- package/CHANGELOG.md +16 -0
- package/README.md +16 -11
- package/dist/lib/observability.js +17 -6
- package/dist/lib/paths.d.ts +1 -1
- package/dist/lib/paths.js +3 -3
- package/dist/lib/recipe-discovery.d.ts +1 -1
- package/dist/lib/recipe-discovery.js +11 -11
- package/dist/lib/tools.d.ts +6 -1
- package/dist/lib/tools.js +150 -37
- package/dist/scripts/conformance.mjs +34 -18
- package/dist/skills/actors/SKILL.md +14 -19
- package/dist/skills/swarm/SKILL.md +3 -3
- package/docs/actor-messages.md +1 -1
- package/docs/tool-registry.md +3 -3
- package/index.ts +1 -1
- package/lib/observability.ts +18 -6
- package/lib/paths.ts +3 -3
- package/lib/recipe-discovery.ts +11 -11
- package/lib/tools.ts +175 -52
- package/package.json +1 -1
- package/scripts/conformance.mjs +34 -18
- package/skills/actors/SKILL.md +14 -19
- package/skills/swarm/SKILL.md +3 -3
- package/dist/lib/conformance.d.ts +0 -12
- package/dist/lib/conformance.js +0 -28
- package/lib/conformance.ts +0 -46
package/dist/lib/tools.js
CHANGED
|
@@ -168,8 +168,9 @@ function compactAsyncRunStatus(value) {
|
|
|
168
168
|
tokens.push(`code=${String(result.code)}`);
|
|
169
169
|
if (result.killed === true)
|
|
170
170
|
tokens.push("killed=true");
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
const draftRecipe = status.draft_recipe;
|
|
172
|
+
if (draftRecipe)
|
|
173
|
+
tokens.push(`draft_recipe=${String(draftRecipe)}`);
|
|
173
174
|
const nextActions = actorRunNextActions(run);
|
|
174
175
|
if (nextActions.length > 0)
|
|
175
176
|
tokens.push(`next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`);
|
|
@@ -357,6 +358,15 @@ function compactArtifactPath(value) {
|
|
|
357
358
|
const record = asRecord(value);
|
|
358
359
|
return String(record.path ?? "<missing>");
|
|
359
360
|
}
|
|
361
|
+
function artifactNextActions(run, artifacts) {
|
|
362
|
+
const id = String(run ?? "").trim();
|
|
363
|
+
if (!id || Object.keys(artifacts).length === 0)
|
|
364
|
+
return [];
|
|
365
|
+
return [
|
|
366
|
+
`inspect target=run:${id} view=artifacts verbose=true`,
|
|
367
|
+
`inspect target=run:${id} view=messages`,
|
|
368
|
+
];
|
|
369
|
+
}
|
|
360
370
|
function compactActorFiles(status) {
|
|
361
371
|
const run = String(status.run ?? "<unknown>");
|
|
362
372
|
const artifacts = asRecord(status.artifacts);
|
|
@@ -375,7 +385,11 @@ function compactActorFiles(status) {
|
|
|
375
385
|
.map(([key, value]) => `${key}:${compactArtifactPath(value)}`)
|
|
376
386
|
.join(",")}`
|
|
377
387
|
: "";
|
|
378
|
-
|
|
388
|
+
const nextActions = artifactNextActions(run, artifacts);
|
|
389
|
+
const nextText = nextActions.length
|
|
390
|
+
? ` next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`
|
|
391
|
+
: "";
|
|
392
|
+
return `\nrun=${run}${artifactText}${files.length ? ` files=${files.join(",")}` : ""}${nextText}`;
|
|
379
393
|
}
|
|
380
394
|
function summarizeOtherSessions(currentSession, allRuns) {
|
|
381
395
|
const otherRuns = allRuns.filter((run) => run.ownerId && run.ownerId !== currentSession);
|
|
@@ -490,13 +504,47 @@ function compactRecipeDoctor(summary) {
|
|
|
490
504
|
}
|
|
491
505
|
for (const item of remediations.slice(0, 8)) {
|
|
492
506
|
const action = compactPreview(item.action, Limits.DOCTOR_ACTION_PREVIEW_CHARS);
|
|
493
|
-
const blocked = item.
|
|
494
|
-
? ` blocked=${compactPreview(item.
|
|
507
|
+
const blocked = item.blocked_fallback
|
|
508
|
+
? ` blocked=${compactPreview(item.blocked_fallback, Limits.DOCTOR_ACTION_PREVIEW_CHARS)}`
|
|
495
509
|
: "";
|
|
496
510
|
lines.push(`${String(item.severity ?? "info")} kind=${String(item.kind ?? "inspect")} id=${String(item.id ?? "root")}${blocked} action=${action ?? "inspect"}`);
|
|
497
511
|
}
|
|
512
|
+
const nextActions = Array.isArray(summary.next_actions)
|
|
513
|
+
? summary.next_actions
|
|
514
|
+
: [];
|
|
515
|
+
if (nextActions.length > 0)
|
|
516
|
+
lines[0] = `${lines[0]}${compactNextActions(nextActions)}`;
|
|
498
517
|
return `\n${lines.join("\n")}`;
|
|
499
518
|
}
|
|
519
|
+
function recipeRegistryNextActions(summary, view) {
|
|
520
|
+
const actions = [];
|
|
521
|
+
const drafts = Array.isArray(summary.drafts)
|
|
522
|
+
? summary.drafts
|
|
523
|
+
: [];
|
|
524
|
+
const invalid = Array.isArray(summary.invalid) ? summary.invalid.length : 0;
|
|
525
|
+
const diagnostics = Array.isArray(summary.diagnostics)
|
|
526
|
+
? summary.diagnostics.length
|
|
527
|
+
: 0;
|
|
528
|
+
const topAction = asRecord(summary.top_action);
|
|
529
|
+
if (view !== "doctor" && (invalid > 0 || diagnostics > 0)) {
|
|
530
|
+
actions.push("inspect target=recipes view=doctor");
|
|
531
|
+
}
|
|
532
|
+
if (view === "doctor" && typeof topAction.action === "string") {
|
|
533
|
+
actions.push(String(topAction.action));
|
|
534
|
+
}
|
|
535
|
+
if (drafts.length > 0) {
|
|
536
|
+
actions.push("inspect target=recipes view=summary verbose=true");
|
|
537
|
+
const firstPath = typeof drafts[0]?.path === "string" ? drafts[0].path : undefined;
|
|
538
|
+
if (firstPath)
|
|
539
|
+
actions.push(`spawn file=${firstPath}`);
|
|
540
|
+
}
|
|
541
|
+
return [...new Set(actions)].slice(0, 4);
|
|
542
|
+
}
|
|
543
|
+
function compactNextActions(actions) {
|
|
544
|
+
return actions.length
|
|
545
|
+
? ` next=${actions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`
|
|
546
|
+
: "";
|
|
547
|
+
}
|
|
500
548
|
function compactRecipeRegistry(summary) {
|
|
501
549
|
const active = Array.isArray(summary.active) ? summary.active.length : 0;
|
|
502
550
|
const shadowed = Array.isArray(summary.shadowed)
|
|
@@ -509,13 +557,37 @@ function compactRecipeRegistry(summary) {
|
|
|
509
557
|
const diagnostics = Array.isArray(summary.diagnostics)
|
|
510
558
|
? summary.diagnostics.length
|
|
511
559
|
: 0;
|
|
512
|
-
const
|
|
513
|
-
? summary.candidates.length
|
|
514
|
-
: 0;
|
|
560
|
+
const drafts = Array.isArray(summary.drafts) ? summary.drafts.length : 0;
|
|
515
561
|
const recommendations = Array.isArray(summary.recommendations)
|
|
516
562
|
? summary.recommendations.length
|
|
517
563
|
: 0;
|
|
518
|
-
|
|
564
|
+
const nextActions = Array.isArray(summary.next_actions)
|
|
565
|
+
? summary.next_actions
|
|
566
|
+
: [];
|
|
567
|
+
return `\nrecipes active=${active} drafts=${drafts} shadowed=${shadowed} invalid=${invalid} disabled=${disabled} recommendations=${recommendations} diagnostics=${diagnostics}${compactNextActions(nextActions)}`;
|
|
568
|
+
}
|
|
569
|
+
function actorMessageNextActions(message, result) {
|
|
570
|
+
const actions = [];
|
|
571
|
+
const address = ActorMessages.parseActorAddress(message.to);
|
|
572
|
+
if (result.delivery_error || result.sent === false) {
|
|
573
|
+
if (address.kind === "run" && address.value) {
|
|
574
|
+
actions.push(`inspect target=run:${address.value} view=status`);
|
|
575
|
+
actions.push(`inspect target=run:${address.value} view=mailbox`);
|
|
576
|
+
}
|
|
577
|
+
else if (address.kind === "branch" && address.value) {
|
|
578
|
+
actions.push(`inspect target=branch:${address.value}/${address.branch ?? "main"} view=mailbox`);
|
|
579
|
+
actions.push(`inspect target=run:${address.value} view=status`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (result.queued === true) {
|
|
583
|
+
if (address.kind === "branch" && address.value) {
|
|
584
|
+
actions.push(`inspect target=branch:${address.value}/${address.branch ?? "main"} view=mailbox`);
|
|
585
|
+
}
|
|
586
|
+
else if (address.kind === "run" && address.value) {
|
|
587
|
+
actions.push(`inspect target=run:${address.value} view=mailbox`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return [...new Set(actions)].slice(0, 3);
|
|
519
591
|
}
|
|
520
592
|
function compactActorMessageResult(message, result) {
|
|
521
593
|
const tokens = [
|
|
@@ -548,6 +620,11 @@ function compactActorMessageResult(message, result) {
|
|
|
548
620
|
if (result.delivery_error) {
|
|
549
621
|
tokens.push(`delivery_error=${compactPreview(result.delivery_error, 96)}`);
|
|
550
622
|
}
|
|
623
|
+
const nextActions = Array.isArray(result.next_actions)
|
|
624
|
+
? result.next_actions
|
|
625
|
+
: actorMessageNextActions(message, result);
|
|
626
|
+
if (nextActions.length > 0)
|
|
627
|
+
tokens.push(`next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`);
|
|
551
628
|
return `\n${tokens.join(" ")}`;
|
|
552
629
|
}
|
|
553
630
|
function maybeJsonText(value, verbose, compact) {
|
|
@@ -621,10 +698,10 @@ function shadowedRecipeLaunchDiagnostic(recipe) {
|
|
|
621
698
|
]);
|
|
622
699
|
return RecipeDiscovery.getShadowedLaunchDiagnostic(discovery, recipe);
|
|
623
700
|
}
|
|
624
|
-
function
|
|
701
|
+
function draftRecipeName(run) {
|
|
625
702
|
return `${run.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "spawn"}.json`;
|
|
626
703
|
}
|
|
627
|
-
function
|
|
704
|
+
function draftRecipeDefaults(values) {
|
|
628
705
|
const ignored = new Set([
|
|
629
706
|
"actor_address",
|
|
630
707
|
"communication_file",
|
|
@@ -635,21 +712,21 @@ function candidateRecipeDefaults(values) {
|
|
|
635
712
|
const defaults = Object.fromEntries(Object.entries(values).filter(([key]) => !ignored.has(key)));
|
|
636
713
|
return Object.keys(defaults).length > 0 ? defaults : undefined;
|
|
637
714
|
}
|
|
638
|
-
function
|
|
715
|
+
function writeSpawnDraftRecipe(input, meta) {
|
|
639
716
|
if (process.env.NODE_TEST_CONTEXT &&
|
|
640
|
-
process.env.
|
|
717
|
+
process.env.PI_ACTORS_ENABLE_SPAWN_DRAFTS_IN_TEST !== "1")
|
|
641
718
|
return undefined;
|
|
642
719
|
if (input.template === undefined ||
|
|
643
720
|
input.file !== undefined ||
|
|
644
721
|
input.recipe !== undefined)
|
|
645
722
|
return undefined;
|
|
646
|
-
const root = Paths.
|
|
723
|
+
const root = Paths.getRecipeDraftRoot();
|
|
647
724
|
mkdirSync(root, { recursive: true });
|
|
648
|
-
const path = join(root,
|
|
649
|
-
const defaults =
|
|
725
|
+
const path = join(root, draftRecipeName(String(meta.run)));
|
|
726
|
+
const defaults = draftRecipeDefaults(meta.values);
|
|
650
727
|
const recipe = {
|
|
651
728
|
async: true,
|
|
652
|
-
description: `
|
|
729
|
+
description: `Draft recipe captured from spawn run ${String(meta.run)}`,
|
|
653
730
|
...(meta.artifacts ? { artifacts: meta.artifacts } : {}),
|
|
654
731
|
...(defaults ? { defaults } : {}),
|
|
655
732
|
template: input.template,
|
|
@@ -662,7 +739,7 @@ function enhanceSpawnRecipeError(error, recipe) {
|
|
|
662
739
|
if (!diagnostic)
|
|
663
740
|
return error instanceof Error ? error : new Error(String(error));
|
|
664
741
|
const original = error instanceof Error ? error.message : String(error);
|
|
665
|
-
return Object.assign(new Error(`${original} reason=${diagnostic.reason} active_path=${diagnostic.active_path}
|
|
742
|
+
return Object.assign(new Error(`${original} reason=${diagnostic.reason} active_path=${diagnostic.active_path} blocked_fallback=${diagnostic.blocked_fallback} hint=${diagnostic.hint}`), {
|
|
666
743
|
...diagnostic,
|
|
667
744
|
original_error: original,
|
|
668
745
|
});
|
|
@@ -783,11 +860,11 @@ export function createSpawnToolDefinition() {
|
|
|
783
860
|
catch (error) {
|
|
784
861
|
throw enhanceSpawnRecipeError(error, recipe);
|
|
785
862
|
}
|
|
786
|
-
const
|
|
863
|
+
const draftRecipe = writeSpawnDraftRecipe(input, meta);
|
|
787
864
|
const nextActions = actorRunNextActions(meta.run);
|
|
788
865
|
const details = {
|
|
789
866
|
...meta,
|
|
790
|
-
...(
|
|
867
|
+
...(draftRecipe ? { draft_recipe: draftRecipe } : {}),
|
|
791
868
|
next_actions: nextActions,
|
|
792
869
|
};
|
|
793
870
|
ActorRooms.ensureDefaultRoom(meta.state_dir, String(meta.run));
|
|
@@ -814,15 +891,29 @@ function requireContextSessionId(ctx, actor) {
|
|
|
814
891
|
}
|
|
815
892
|
return sessionId;
|
|
816
893
|
}
|
|
894
|
+
function sessionMismatchError(input) {
|
|
895
|
+
const ownerSession = input.expectedSession ?? "none";
|
|
896
|
+
const currentSession = input.currentSession ?? "none";
|
|
897
|
+
const actor = input.run ? `run:${input.run}` : (input.target ?? "session");
|
|
898
|
+
const hintTarget = input.expectedSession
|
|
899
|
+
? `session:${input.expectedSession}`
|
|
900
|
+
: "session:all";
|
|
901
|
+
return Object.assign(new Error(`${actor} reason=session_mismatch owner_session=${ownerSession} current_session=${currentSession} hint=inspect_session:${input.expectedSession ?? "all"}`), {
|
|
902
|
+
current_session: input.currentSession,
|
|
903
|
+
hint: `inspect target=${hintTarget} view=status`,
|
|
904
|
+
owner_session: input.expectedSession,
|
|
905
|
+
reason: "session_mismatch",
|
|
906
|
+
run: input.run,
|
|
907
|
+
target: input.target,
|
|
908
|
+
});
|
|
909
|
+
}
|
|
817
910
|
function assertRunAccessibleToContext(runId, ctx) {
|
|
818
911
|
const status = AsyncRuns.getRunStatus(runId);
|
|
819
912
|
const sessionId = getContextSessionId(ctx);
|
|
820
913
|
if (sessionId && status.ownerId && status.ownerId !== sessionId) {
|
|
821
|
-
throw
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
owner_session: status.ownerId,
|
|
825
|
-
reason: "session_mismatch",
|
|
914
|
+
throw sessionMismatchError({
|
|
915
|
+
currentSession: sessionId,
|
|
916
|
+
expectedSession: String(status.ownerId),
|
|
826
917
|
run: runId,
|
|
827
918
|
});
|
|
828
919
|
}
|
|
@@ -835,13 +926,13 @@ export function createInspectToolDefinition(deps = {}) {
|
|
|
835
926
|
return {
|
|
836
927
|
name: "inspect",
|
|
837
928
|
label: "Inspect",
|
|
838
|
-
description: "Intentionally inspect
|
|
929
|
+
description: "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.",
|
|
839
930
|
parameters: objectSchema({
|
|
840
931
|
lines: stringSchema("Line count for tail/messages views. Default 40."),
|
|
841
932
|
status: stringSchema("Optional session run filter: all, running, active, terminal, done, failed, cancelled, killed, or exited."),
|
|
842
|
-
target: stringSchema("Actor address to inspect, e.g. run:<id>, room:<run>, coordinator, session:<id>, session:all
|
|
933
|
+
target: stringSchema("Actor address to inspect, e.g. run:<id> or tool:<name>; advanced: branch:<run>/<branch>, room:<run>, coordinator, session:<id>, session:all."),
|
|
843
934
|
verbose: booleanSchema("Return full JSON instead of compact text where available."),
|
|
844
|
-
view: stringSchema("Inspection view: status, tail, messages, artifacts, files, mailbox
|
|
935
|
+
view: stringSchema("Inspection view. Core run views: status, tail, messages, artifacts, files, mailbox. Advanced views include communication, roster, and contacts."),
|
|
845
936
|
}, ["target", "view"]),
|
|
846
937
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
847
938
|
const input = asRecord(params);
|
|
@@ -863,9 +954,13 @@ export function createInspectToolDefinition(deps = {}) {
|
|
|
863
954
|
{ root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
|
|
864
955
|
]);
|
|
865
956
|
const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
|
|
866
|
-
const
|
|
957
|
+
const summaryBase = {
|
|
867
958
|
...RecipeDiscovery.summarizeDiscovery(discovered),
|
|
868
|
-
|
|
959
|
+
drafts: RecipeDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
|
|
960
|
+
};
|
|
961
|
+
const summary = {
|
|
962
|
+
...summaryBase,
|
|
963
|
+
next_actions: recipeRegistryNextActions(summaryBase, view),
|
|
869
964
|
};
|
|
870
965
|
return {
|
|
871
966
|
content: [
|
|
@@ -1084,7 +1179,11 @@ export function createInspectToolDefinition(deps = {}) {
|
|
|
1084
1179
|
const status = assertRunAccessibleToContext(runId, ctx);
|
|
1085
1180
|
const artifactManifest = AsyncRuns.resolveArtifactManifest(status.artifacts);
|
|
1086
1181
|
const details = artifactManifest
|
|
1087
|
-
? {
|
|
1182
|
+
? {
|
|
1183
|
+
...status,
|
|
1184
|
+
artifact_manifest: artifactManifest,
|
|
1185
|
+
next_actions: artifactNextActions(status.run ?? runId, asRecord(status.artifacts)),
|
|
1186
|
+
}
|
|
1088
1187
|
: status;
|
|
1089
1188
|
return {
|
|
1090
1189
|
content: [
|
|
@@ -1139,7 +1238,7 @@ export function createActorMessageToolDefinition(deps = {}) {
|
|
|
1139
1238
|
return {
|
|
1140
1239
|
name: "message",
|
|
1141
1240
|
label: "Message",
|
|
1142
|
-
description: "Send one typed addressed message to steer an existing actor instead of restarting it.
|
|
1241
|
+
description: "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>.",
|
|
1143
1242
|
parameters: objectSchema({
|
|
1144
1243
|
body: unionSchema([
|
|
1145
1244
|
stringSchema("Message body. For run:<id>, this is the run-local command line."),
|
|
@@ -1151,7 +1250,7 @@ export function createActorMessageToolDefinition(deps = {}) {
|
|
|
1151
1250
|
metadata: looseObjectSchema("Optional structured metadata for routing or domain hints."),
|
|
1152
1251
|
reply_to: stringSchema("Optional message id this message replies to."),
|
|
1153
1252
|
summary: stringSchema("Optional short human-facing summary."),
|
|
1154
|
-
to: stringSchema("Destination actor address, e.g. run:<id
|
|
1253
|
+
to: stringSchema("Destination actor address, e.g. run:<id> or tool:<name>; advanced: branch:<run>/<branch>, room:<run>, coordinator, session:<id>."),
|
|
1155
1254
|
type: stringSchema("Semantic message type, e.g. control.approve or checkpoint.needs_scope."),
|
|
1156
1255
|
verbose: booleanSchema("Return full JSON instead of compact text."),
|
|
1157
1256
|
}, ["to", "type"]),
|
|
@@ -1266,10 +1365,20 @@ export function createActorMessageToolDefinition(deps = {}) {
|
|
|
1266
1365
|
const senderStatus = assertRunAccessibleToContext(sender.value, ctx);
|
|
1267
1366
|
if (address.kind === "session") {
|
|
1268
1367
|
if (!senderStatus.ownerId) {
|
|
1269
|
-
throw
|
|
1368
|
+
throw sessionMismatchError({
|
|
1369
|
+
currentSession: undefined,
|
|
1370
|
+
expectedSession: address.value,
|
|
1371
|
+
run: sender.value,
|
|
1372
|
+
target: `session:${address.value}`,
|
|
1373
|
+
});
|
|
1270
1374
|
}
|
|
1271
1375
|
if (senderStatus.ownerId !== address.value) {
|
|
1272
|
-
throw
|
|
1376
|
+
throw sessionMismatchError({
|
|
1377
|
+
currentSession: String(senderStatus.ownerId),
|
|
1378
|
+
expectedSession: address.value,
|
|
1379
|
+
run: sender.value,
|
|
1380
|
+
target: `session:${address.value}`,
|
|
1381
|
+
});
|
|
1273
1382
|
}
|
|
1274
1383
|
}
|
|
1275
1384
|
result = AsyncRuns.appendRunOutboxEvent(sender.value, {
|
|
@@ -1291,14 +1400,18 @@ export function createActorMessageToolDefinition(deps = {}) {
|
|
|
1291
1400
|
else {
|
|
1292
1401
|
throw new Error(`message currently supports run:<id>, branch:<run>/<branch>, room:<run>, tool:<name>, coordinator, and session:<id> destinations; unsupported destination: ${message.to}`);
|
|
1293
1402
|
}
|
|
1403
|
+
const nextActions = actorMessageNextActions(message, result);
|
|
1404
|
+
const resultWithNext = nextActions.length
|
|
1405
|
+
? { ...result, next_actions: nextActions }
|
|
1406
|
+
: result;
|
|
1294
1407
|
return {
|
|
1295
1408
|
content: [
|
|
1296
1409
|
{
|
|
1297
1410
|
type: "text",
|
|
1298
|
-
text: maybeJsonText({ message, result }, input.verbose === true, compactActorMessageResult(message,
|
|
1411
|
+
text: maybeJsonText({ message, result: resultWithNext }, input.verbose === true, compactActorMessageResult(message, resultWithNext)),
|
|
1299
1412
|
},
|
|
1300
1413
|
],
|
|
1301
|
-
details: { message, result },
|
|
1414
|
+
details: { message, result: resultWithNext },
|
|
1302
1415
|
};
|
|
1303
1416
|
},
|
|
1304
1417
|
};
|
|
@@ -1,33 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Internal conformance runner
|
|
4
|
+
* Internal conformance runner.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 {
|
|
11
|
-
import { dirname
|
|
12
|
-
import { fileURLToPath
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 {
|
|
25
|
-
const
|
|
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 ${
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
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
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: actors
|
|
3
3
|
description: Required practical guide for non-trivial pi-actors use. Read before using or changing spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 0.
|
|
5
|
+
version: 0.33.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|
|
@@ -39,12 +39,11 @@ Trusted local capability
|
|
|
39
39
|
|
|
40
40
|
- **Command template**: portable execution graph. String leaf, sequence array, or object node with controls.
|
|
41
41
|
- **Recipe**: saved JSON definition wrapping a template with args, defaults, imports, mailbox, artifacts, metadata, and optional `async: true`.
|
|
42
|
-
- **Run actor**: one detached execution instance addressable as `run:<id>` with status, logs, messages, mailbox metadata, files,
|
|
43
|
-
- **Room actor**: shared timeline + roster endpoint addressable as `room:<run>`; every spawned run gets `room:<run>`.
|
|
42
|
+
- **Run actor**: one detached execution instance addressable as `run:<id>` with status, logs, messages, mailbox metadata, files, and artifacts.
|
|
44
43
|
- **Tool actor**: registered persistent capability addressable as `tool:<name>` and callable through the generated tool or `message`. `tool:pi-actors` is reserved for runtime/status inspection.
|
|
45
|
-
- **Coordinator/session**: the current pi session endpoint that receives bounded actor follow-ups.
|
|
46
|
-
- **Mailbox**: public interaction contract: message types the actor accepts/emits.
|
|
47
44
|
- **Artifact**: named durable output path declared by a recipe/run.
|
|
45
|
+
- **Mailbox**: interaction contract: message types the actor accepts/emits.
|
|
46
|
+
- **Advanced group/coordination surfaces**: `branch:<run>/<branch>`, `room:<run>`, `coordinator`, `session:<id>`, and communication snapshots exist for multi-actor workflows and diagnostics; do not make them the default mental model.
|
|
48
47
|
|
|
49
48
|
## Three Verbs
|
|
50
49
|
|
|
@@ -86,8 +85,9 @@ Envelope fields:
|
|
|
86
85
|
|
|
87
86
|
- Required: `to`, `type`.
|
|
88
87
|
- Useful: `summary`, `body`, `from`, `reply_to`, `correlation_id`, `metadata`.
|
|
89
|
-
-
|
|
90
|
-
-
|
|
88
|
+
- Core addresses: `run:<id>`, `tool:<name>`.
|
|
89
|
+
- Advanced addresses: `branch:<run>/<branch>`, `room:<run>` for group timeline/roster, `coordinator`, `session:<id>`.
|
|
90
|
+
- Group posts to `room:<run>` require `from` from the same run (`run:<run>` or `branch:<run>/<branch>`).
|
|
91
91
|
- Runtime termination message: `control.kill` is the only documented actor message that kills a run. `control.stop` and `control.cancel` are actor-local mailbox vocabulary only when a recipe declares and handles them. Terminal retention messages: `control.archive`, `control.prune`.
|
|
92
92
|
- Long-lived child processes should remain in the run-owned process group unless the recipe implements an explicit daemon termination bridge; do not leave unowned detached services behind `control.kill`.
|
|
93
93
|
|
|
@@ -99,12 +99,7 @@ Check `inspect view=mailbox` before domain-specific messages.
|
|
|
99
99
|
{ "target": "run:repo-health", "view": "status" }
|
|
100
100
|
{ "target": "run:repo-health", "view": "tail", "lines": "80" }
|
|
101
101
|
{ "target": "run:repo-health", "view": "messages" }
|
|
102
|
-
{ "target": "run:repo-health", "view": "communication" }
|
|
103
102
|
{ "target": "run:repo-health", "view": "artifacts" }
|
|
104
|
-
{ "target": "room:repo-health", "view": "status" }
|
|
105
|
-
{ "target": "room:repo-health", "view": "roster" }
|
|
106
|
-
{ "target": "room:repo-health", "view": "contacts" }
|
|
107
|
-
{ "target": "room:repo-health", "view": "previews" }
|
|
108
103
|
{ "target": "tool:pi-actors", "view": "status" }
|
|
109
104
|
{ "target": "tool:music_player", "view": "status" }
|
|
110
105
|
{ "target": "recipes", "view": "status" }
|
|
@@ -116,10 +111,10 @@ Views:
|
|
|
116
111
|
- `status`: lifecycle, pid, values, progress, result, compact summary.
|
|
117
112
|
- `tail`: recent stdout/stderr/log tail.
|
|
118
113
|
- `messages`: actor messages emitted by the run, or room timeline entries for `room:*`.
|
|
119
|
-
- `communication`: run/branch
|
|
120
|
-
- `roster`: room member list with address, role, parent, caps, claim, status, and last seen.
|
|
121
|
-
- `contacts`: roster-derived direct-message targets without full roster metadata.
|
|
122
|
-
- `previews`: TUI-ready bounded
|
|
114
|
+
- Advanced `communication`: run/branch group-coordination snapshot with self/root/default-room/member/contact hints.
|
|
115
|
+
- Advanced `roster`: room member list with address, role, parent, caps, claim, status, and last seen.
|
|
116
|
+
- Advanced `contacts`: roster-derived direct-message targets without full roster metadata.
|
|
117
|
+
- Advanced `previews`: TUI-ready bounded group-message previews with timestamp/from/to/type/summary/body_preview.
|
|
123
118
|
- `mailbox`: declared accepts/emits contract for runs; queued direct branch inbox messages for `branch:<run>/<branch>` with `id`, status, route/type, and queue/handling timestamps.
|
|
124
119
|
- `files`: run state directory file list.
|
|
125
120
|
- `artifacts`: declared artifact paths/status.
|
|
@@ -218,13 +213,13 @@ Only matching filename ids compete. Higher priority shadows lower priority; with
|
|
|
218
213
|
Muscle-memory lens: pi-actors has two durable executable-memory layers.
|
|
219
214
|
|
|
220
215
|
1. `~/.pi/agent/recipes/*.json` and `*.md` are the agent's active capability memory. Every recipe in that directory becomes an easy-to-call tool automatically and survives into later sessions. Descriptions matter here because they become the tool's operator-facing title/context.
|
|
221
|
-
2. `~/.pi/agent/recipes/
|
|
216
|
+
2. `~/.pi/agent/recipes/drafts/*.json` is draft memory captured from successful inline `spawn template=...` runs. Drafts do not enter the injected tool surface. They remain reusable by explicit path, e.g. `spawn file="~/.pi/agent/recipes/drafts/<name>.json"`, and can be promoted by moving or copying one level up into `~/.pi/agent/recipes`.
|
|
222
217
|
|
|
223
|
-
Agents grow active memory by calling `register_tool` or by deliberate recipe-file edits. They grow
|
|
218
|
+
Agents grow active memory by calling `register_tool` or by deliberate recipe-file edits. They grow draft memory by trying ad hoc actors successfully. Treat both as executable habits: drafts are the workbench/proving ground; root recipes are promoted muscle memory.
|
|
224
219
|
|
|
225
220
|
Usage lens: user recipes may carry extension-maintained launch metadata such as `usage.calls` and `usage.last_called`. The extension increments the counter when it starts that concrete recipe; agents should not hand-edit counters as part of normal recipe maintenance. Treat usage as evidence for usefulness analysis: heavily used recipes are good candidates for promotion, documentation, or stronger tests; unused recipes are cleanup candidates. Do not use failure counts as a primary usefulness signal because failures may reflect bad caller judgment rather than bad recipes. Do not delete or demote solely from counters without operator approval.
|
|
226
221
|
|
|
227
|
-
Promotion lens: successful transient/ad hoc actor runs are evidence, not commands. Inline spawns leave
|
|
222
|
+
Promotion lens: successful transient/ad hoc actor runs are evidence, not commands. Inline spawns leave draft recipes as replayable evidence, not active tools. If a draft is repeatable, parameterized, safe enough, and likely useful later, the agent may promote it by moving/copying it into `~/.pi/agent/recipes` or by calling `register_tool` with a concise name, typed args/defaults, and a reviewed template or recipe path. Do not auto-register every success; do not promote temp paths, secrets, one-off prompts, or project-private assumptions without normalization and approval.
|
|
228
223
|
|
|
229
224
|
Cleanup rule: periodically inspect `~/.pi/agent/recipes` as the live muscle-memory set. For each stale, duplicate, too-specific, or low-value recipe, choose one explicit action: keep as a tool, move it out of the agent recipe root to retain recipe-only memory, merge into a better recipe, or delete/archive the file. Prefer moving over deletion when the recipe may still be useful as a component. Never silently remove tools during unrelated work.
|
|
230
225
|
|
|
@@ -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.
|
|
5
|
+
version: 0.33.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Swarm
|
|
@@ -30,8 +30,8 @@ Maintain this skill as a living orchestration standard. When real swarm work exp
|
|
|
30
30
|
- `Async Run`: A local lifecycle envelope around a command-template swarm composer or utility. It owns state, logs, status, cancellation, and observability, not swarm semantics.
|
|
31
31
|
- `Lens`: A deliberately narrow cognitive role assigned to one subagent, such as security, tests, architecture, economics, or operator UX.
|
|
32
32
|
- `Task Card`: A bounded implementation assignment with goal, allowed files, avoided files, expected output, and validation gates.
|
|
33
|
-
- `Component Capability`: An abstract adapter operation such as launcher, reviewer, verifier, merger, quorum, checkpoint, follow-up, judge, or normalizer. Swarm may target these capabilities, but local adapters bind them to concrete tools, recipes,
|
|
34
|
-
- `
|
|
33
|
+
- `Component Capability`: An abstract adapter operation such as launcher, reviewer, verifier, merger, quorum, checkpoint, follow-up, judge, or normalizer. Swarm may target these capabilities, but local adapters bind them to concrete tools, recipes, draft recipes, command templates, async runs, or services.
|
|
34
|
+
- `Draft Recipe`: A reusable but non-registered recipe captured from a successful inline actor spawn under `~/.pi/agent/recipes/drafts`. It can be replayed by explicit file path and later promoted into the active tool recipe root after enough dogfood.
|
|
35
35
|
- `Coordinator Checkpoint`: A deliberate subagent pause where the subagent preserves its working context, sends a bounded question or status to the orchestrator, receives a coordinator reply, and continues in the same subagent context.
|
|
36
36
|
- `Evidence Checkpoint`: A deliberate stop where a subagent records sources, assumptions, confidence, contradictions, or blocking evidence gaps before synthesis.
|
|
37
37
|
- `Integrator`: The human or agent that merges isolated branches/worktrees into the shared target and owns conflict resolution.
|
package/docs/actor-messages.md
CHANGED
|
@@ -199,7 +199,7 @@ Recipes can declare their conversational surface:
|
|
|
199
199
|
}
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
-
The implementation supports `status`, `tail`, `messages`, `artifacts`, `files`, `mailbox`, and `communication` for `run:<id>` actors, `status`, `messages`, `previews`, `roster`, and `contacts` for `room:<run>` actors, `status`/`runs` for `coordinator`, `session:<id>`, and `session:all` actors with optional status filtering, and `status`/`schema` for registered `tool:<name>` actors. Run mailbox inspection shows recipe-declared mailbox metadata plus recent durable run inbox entries; branch mailbox inspection shows branch-local queued/claimed/handled records. Room `status` returns compact message/roster counts plus `last_message_at`, `last_message_from`, `last_message_type`, and `last_message_summary` when available, without parsing the full timeline into actor envelopes. Use `messages` for actor-envelope inspection. `inspect target=coordinator` requires a current coordinator session; use `session:<id>` or `session:all` when the session is intentionally explicit. Direct `run:<id>` and `room:<run>` inspection respects coordinator-session ownership when the current session is known. `inspect` is for decision points and diagnosis only; examples must not teach sleep-then-inspect polling.
|
|
202
|
+
The implementation supports `status`, `tail`, `messages`, `artifacts`, `files`, `mailbox`, and `communication` for `run:<id>` actors, `status`, `messages`, `previews`, `roster`, and `contacts` for `room:<run>` actors, `status`/`runs` for `coordinator`, `session:<id>`, and `session:all` actors with optional status filtering, and `status`/`schema` for registered `tool:<name>` actors. Run mailbox inspection shows recipe-declared mailbox metadata plus recent durable run inbox entries; branch mailbox inspection shows branch-local queued/claimed/handled records. Room `status` returns compact message/roster counts plus `last_message_at`, `last_message_from`, `last_message_type`, and `last_message_summary` when available, without parsing the full timeline into actor envelopes. Use `messages` for actor-envelope inspection. `inspect target=coordinator` requires a current coordinator session; use `session:<id>` or `session:all` when the session is intentionally explicit. Direct `run:<id>` and `room:<run>` inspection respects coordinator-session ownership when the current session is known. Ownership denials use `reason=session_mismatch owner_session=<id> current_session=<id> hint=inspect_session:<id>`; recover by inspecting the hinted `session:<id>` instead of forcing cross-session control. `inspect` is for decision points and diagnosis only; examples must not teach sleep-then-inspect polling.
|
|
203
203
|
|
|
204
204
|
## Runtime Direction
|
|
205
205
|
|
package/docs/tool-registry.md
CHANGED
|
@@ -10,7 +10,7 @@ The registry source is location-discovered recipes, not a live tool-only JSON fi
|
|
|
10
10
|
|
|
11
11
|
- `~/.pi/agent/recipes/*.json` and `*.md` are the highest-priority user recipe root and the operator-managed tool set.
|
|
12
12
|
- Recipes in that root are tools by location.
|
|
13
|
-
- `~/.pi/agent/recipes/
|
|
13
|
+
- `~/.pi/agent/recipes/drafts/*.json` stores captured inline-spawn draft recipes, not registered tools. Promote one by moving or copying it up one level into `~/.pi/agent/recipes`. `inspect target=recipes view=summary` reports their count, and verbose output lists their paths/descriptions for explicit replay by file path.
|
|
14
14
|
- Packaged pi-actors recipes are the lower-priority standard library of declarative actor config components, not automatically registered tools.
|
|
15
15
|
- Ad hoc recipe files outside the user recipe root are components unless explicitly registered/copied into `~/.pi/agent/recipes`.
|
|
16
16
|
- Recipe identity is the filename basename; `~/.pi/agent/recipes/docs_review.json` and `docs_review.md` both have id/tool name `docs_review`.
|
|
@@ -31,9 +31,9 @@ inspect target=recipes view=summary verbose=true
|
|
|
31
31
|
|
|
32
32
|
`tool:pi-actors` is a reserved runtime-status actor: it reports the loaded package version, package root, source/dist mode, entrypoint path, recipe roots, and git commit when available. Use it after reloads to confirm which extension code is actually live.
|
|
33
33
|
|
|
34
|
-
The recipe summary reports active, shadowed, invalid, disabled, and diagnostic entries so operators can answer why a tool is present, hidden, broken, or disabled. The doctor view keeps the same registry evidence but promotes an advisory action surface: compact output includes the highest-priority `top` remediation plus ordered actions for invalid/blocking, disabled, risky shell-boundary, and shadowed recipes. Verbose inspection keeps the structured `remediations`, `top_action`, diagnostic details, and blocked lower-priority
|
|
34
|
+
The recipe summary reports active, shadowed, invalid, disabled, and diagnostic entries so operators can answer why a tool is present, hidden, broken, or disabled. The doctor view keeps the same registry evidence but promotes an advisory action surface: compact output includes the highest-priority `top` remediation plus ordered actions for invalid/blocking, disabled, risky shell-boundary, and shadowed recipes. Verbose inspection keeps the structured `remediations`, `top_action`, diagnostic details, and blocked lower-priority fallback paths when a broken or disabled higher-priority recipe masks a fallback.
|
|
35
35
|
|
|
36
|
-
Routine shadowing is quiet. If a bare `spawn` recipe launch already fails because an invalid or `disabled: true` user recipe blocks a lower-priority
|
|
36
|
+
Routine shadowing is quiet. If a bare `spawn` recipe launch already fails because an invalid or `disabled: true` user recipe blocks a lower-priority fallback, the launch error adds compact tokens such as `reason=shadowed_invalid` or `reason=shadowed_disabled`, `active_path`, `blocked_fallback`, and `hint=inspect_recipes_doctor`.
|
|
37
37
|
|
|
38
38
|
## Registering Tools
|
|
39
39
|
|
package/index.ts
CHANGED
|
@@ -102,7 +102,7 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
|
|
|
102
102
|
onChange: () =>
|
|
103
103
|
activeRunContext && scheduleRunEventUpdate(activeRunContext),
|
|
104
104
|
});
|
|
105
|
-
const actorToolDefinitions = new Map<string,
|
|
105
|
+
const actorToolDefinitions = new Map<string, Tools.ActorToolDefinition>();
|
|
106
106
|
const runtime = Runtime.createAutoToolsRuntime({
|
|
107
107
|
configPath: Paths.EXTENSION_RUNTIME_PATHS.configPath,
|
|
108
108
|
exec: CommandTemplates.execCommandTemplate,
|
package/lib/observability.ts
CHANGED
|
@@ -1021,19 +1021,31 @@ function formatRecipePersistenceSuggestion(transition: RunTransition): string {
|
|
|
1021
1021
|
return `\nAgent note: this actor was spawned directly and completed successfully. If this pattern fits this machine's recurring workflow, ask the operator whether to save it as a durable recipe/tool under ~/.pi/agent/recipes with register_tool. Do not auto-save without confirmation.`;
|
|
1022
1022
|
}
|
|
1023
1023
|
|
|
1024
|
+
function formatTransitionNextActions(transition: RunTransition): string {
|
|
1025
|
+
const actions = [
|
|
1026
|
+
`inspect target=run:${transition.run} view=status`,
|
|
1027
|
+
transition.to === "done" && Object.keys(transition.artifacts ?? {}).length > 0
|
|
1028
|
+
? `inspect target=run:${transition.run} view=artifacts`
|
|
1029
|
+
: `inspect target=run:${transition.run} view=tail`,
|
|
1030
|
+
`inspect target=run:${transition.run} view=messages`,
|
|
1031
|
+
].filter(Boolean);
|
|
1032
|
+
return `\nNext actions: ${actions.join(" | ")}`;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1024
1035
|
export function formatRunTransitionMessage(transition: RunTransition): string {
|
|
1025
1036
|
const artifacts = formatNamedArtifacts(transition.artifacts);
|
|
1026
1037
|
const runFiles = formatRunFileList(getRunArtifacts(transition));
|
|
1027
1038
|
const persistenceSuggestion = formatRecipePersistenceSuggestion(transition);
|
|
1039
|
+
const nextActions = formatTransitionNextActions(transition);
|
|
1028
1040
|
if (transition.to === "done")
|
|
1029
|
-
return `Run ${transition.run} completed successfully.${artifacts}${runFiles}
|
|
1041
|
+
return `Run ${transition.run} completed successfully.${artifacts}${runFiles}${nextActions}${persistenceSuggestion}`;
|
|
1030
1042
|
if (transition.to === "failed")
|
|
1031
|
-
return `Run ${transition.run} failed.${artifacts}${runFiles}
|
|
1043
|
+
return `Run ${transition.run} failed.${artifacts}${runFiles}${nextActions}`;
|
|
1032
1044
|
if (transition.to === "cancelled")
|
|
1033
|
-
return `Run ${transition.run} was cancelled
|
|
1045
|
+
return `Run ${transition.run} was cancelled.${nextActions}`;
|
|
1034
1046
|
if (transition.to === "killed")
|
|
1035
|
-
return `Run ${transition.run} was force-killed
|
|
1047
|
+
return `Run ${transition.run} was force-killed.${nextActions}`;
|
|
1036
1048
|
if (transition.to === "exited")
|
|
1037
|
-
return `Run ${transition.run} exited before writing a result
|
|
1038
|
-
return `Run ${transition.run} finished with status ${transition.to}
|
|
1049
|
+
return `Run ${transition.run} exited before writing a result.${nextActions}`;
|
|
1050
|
+
return `Run ${transition.run} finished with status ${transition.to}.${nextActions}`;
|
|
1039
1051
|
}
|