@llblab/pi-actors 0.16.3 → 0.17.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 -7
- package/BACKLOG.md +42 -11
- package/CHANGELOG.md +20 -8
- package/README.md +39 -12
- package/banner.jpg +0 -0
- package/docs/actor-messages.md +63 -2
- package/docs/async-runs.md +25 -3
- package/docs/recipe-library.md +3 -3
- package/docs/template-recipes.md +4 -5
- package/docs/tool-registry.md +7 -12
- package/index.ts +58 -3
- package/lib/actor-inspector-tui.ts +426 -0
- package/lib/actor-messages.ts +18 -0
- package/lib/actor-rooms.ts +369 -0
- package/lib/async-runs.ts +17 -1
- package/lib/config.ts +1 -1
- package/lib/paths.ts +1 -1
- package/lib/prompts.ts +3 -2
- package/lib/recipe-discovery.ts +83 -1
- package/lib/recipe-migration.ts +2 -2
- package/lib/recipe-references.ts +2 -0
- package/lib/recipe-usage.ts +24 -2
- package/lib/tools.ts +292 -9
- package/package.json +1 -1
- package/recipes/lens-swarm.json +0 -1
- package/skills/actors/SKILL.md +51 -8
- package/skills/swarm/SKILL.md +1 -1
package/lib/tools.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as ActorMessages from "./actor-messages.ts";
|
|
8
|
+
import * as ActorRooms from "./actor-rooms.ts";
|
|
8
9
|
import * as AsyncRuns from "./async-runs.ts";
|
|
9
10
|
import * as CommandTemplates from "./command-templates.ts";
|
|
10
11
|
import type { RegisteredTool } from "./config.ts";
|
|
@@ -192,6 +193,99 @@ function compactRunMessages(messages: AsyncRuns.RunOutboxEvent[]): string {
|
|
|
192
193
|
.join("\n")}`;
|
|
193
194
|
}
|
|
194
195
|
|
|
196
|
+
function compactPreview(value: unknown, maxLength = 80): string | undefined {
|
|
197
|
+
if (value === undefined) return undefined;
|
|
198
|
+
const text =
|
|
199
|
+
typeof value === "string" ? value : JSON.stringify(value, undefined, 0);
|
|
200
|
+
const compact = text.replaceAll(/\s+/g, "_");
|
|
201
|
+
return compact.length > maxLength
|
|
202
|
+
? `${compact.slice(0, Math.max(0, maxLength - 1))}…`
|
|
203
|
+
: compact;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function compactRoomPreviews(previews: ActorRooms.RoomMessagePreview[]): string {
|
|
207
|
+
if (previews.length === 0) return "\n(no room message previews)";
|
|
208
|
+
return `\n${previews
|
|
209
|
+
.map((preview) =>
|
|
210
|
+
[
|
|
211
|
+
`ts=${preview.timestamp}`,
|
|
212
|
+
preview.from ? `from=${preview.from}` : "",
|
|
213
|
+
`to=${preview.to}`,
|
|
214
|
+
`type=${preview.type}`,
|
|
215
|
+
preview.summary ? `summary=${compactPreview(preview.summary)}` : "",
|
|
216
|
+
preview.body_preview ? `body=${compactPreview(preview.body_preview)}` : "",
|
|
217
|
+
]
|
|
218
|
+
.filter(Boolean)
|
|
219
|
+
.join(" "),
|
|
220
|
+
)
|
|
221
|
+
.join("\n")}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function compactRoomMessages(messages: ActorRooms.RoomTimelineEntry[]): string {
|
|
225
|
+
if (messages.length === 0) return "\n(no room messages)";
|
|
226
|
+
return `\n${messages
|
|
227
|
+
.map((message) =>
|
|
228
|
+
[
|
|
229
|
+
`ts=${message.received_at}`,
|
|
230
|
+
`from=${String(message.from ?? "<unknown>")}`,
|
|
231
|
+
`to=${message.to}`,
|
|
232
|
+
`type=${message.type}`,
|
|
233
|
+
`summary=${String(message.summary ?? "").replaceAll(/\s+/g, "_")}`,
|
|
234
|
+
compactPreview(message.body) ? `body=${compactPreview(message.body)}` : "",
|
|
235
|
+
]
|
|
236
|
+
.filter(Boolean)
|
|
237
|
+
.join(" "),
|
|
238
|
+
)
|
|
239
|
+
.join("\n")}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function compactRoomContacts(contacts: ActorRooms.RoomContact[]): string {
|
|
243
|
+
if (contacts.length === 0) return "\n(no room contacts)";
|
|
244
|
+
return `\n${contacts
|
|
245
|
+
.map((contact) =>
|
|
246
|
+
[
|
|
247
|
+
`address=${contact.address}`,
|
|
248
|
+
contact.role !== undefined ? `role=${String(contact.role)}` : "",
|
|
249
|
+
contact.parent !== undefined ? `parent=${String(contact.parent)}` : "",
|
|
250
|
+
contact.caps !== undefined ? `caps=${Array.isArray(contact.caps) ? contact.caps.join(",") : String(contact.caps)}` : "",
|
|
251
|
+
contact.claim !== undefined ? `claim=${String(contact.claim).replaceAll(/\s+/g, "_")}` : "",
|
|
252
|
+
contact.status !== undefined ? `status=${String(contact.status)}` : "",
|
|
253
|
+
]
|
|
254
|
+
.filter(Boolean)
|
|
255
|
+
.join(" "),
|
|
256
|
+
)
|
|
257
|
+
.join("\n")}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function compactRoomRoster(roster: Record<string, ActorRooms.RoomMember>): string {
|
|
261
|
+
const members = Object.values(roster);
|
|
262
|
+
if (members.length === 0) return "\n(no room members)";
|
|
263
|
+
return `\n${members
|
|
264
|
+
.map((member) =>
|
|
265
|
+
[
|
|
266
|
+
`address=${member.address}`,
|
|
267
|
+
`role=${String(member.role ?? "")}`,
|
|
268
|
+
member.parent !== undefined ? `parent=${String(member.parent)}` : "",
|
|
269
|
+
member.caps !== undefined ? `caps=${Array.isArray(member.caps) ? member.caps.join(",") : String(member.caps)}` : "",
|
|
270
|
+
member.claim !== undefined ? `claim=${String(member.claim).replaceAll(/\s+/g, "_")}` : "",
|
|
271
|
+
`status=${String(member.status ?? "")}`,
|
|
272
|
+
`last_seen=${member.last_seen}`,
|
|
273
|
+
].join(" "),
|
|
274
|
+
)
|
|
275
|
+
.join("\n")}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function compactRoomStatus(status: ActorRooms.RoomStatus): string {
|
|
279
|
+
return `\nroom=${status.room} messages=${status.message_count} roster=${status.roster_count}${status.last_message_at ? ` last_message_at=${status.last_message_at}` : ""}${status.last_message_from ? ` last_from=${status.last_message_from}` : ""}${status.last_message_type ? ` last_type=${status.last_message_type}` : ""}${status.last_message_summary ? ` last_summary=${compactPreview(status.last_message_summary)}` : ""}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function compactCommunicationSnapshot(
|
|
283
|
+
snapshot: ActorRooms.ActorCommunicationSnapshot | undefined,
|
|
284
|
+
): string {
|
|
285
|
+
if (!snapshot) return "\n(no communication snapshot)";
|
|
286
|
+
return `\nself=${snapshot.self} root=${snapshot.root} rooms=${snapshot.rooms.length} updated_at=${snapshot.updated_at}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
195
289
|
function compactActorFiles(status: Record<string, unknown>): string {
|
|
196
290
|
const run = String(status.run ?? "<unknown>");
|
|
197
291
|
const artifacts = asRecord(status.artifacts);
|
|
@@ -200,6 +294,7 @@ function compactActorFiles(status: Record<string, unknown>): string {
|
|
|
200
294
|
status.stderrLog,
|
|
201
295
|
status.eventsFile,
|
|
202
296
|
status.outboxFile,
|
|
297
|
+
status.state_dir ? `${String(status.state_dir)}/communication.json` : undefined,
|
|
203
298
|
status.state_dir ? `${String(status.state_dir)}/result.json` : undefined,
|
|
204
299
|
].filter((file): file is string => typeof file === "string");
|
|
205
300
|
const artifactText = Object.keys(artifacts).length
|
|
@@ -240,7 +335,10 @@ function compactRecipeRegistry(summary: Record<string, unknown>): string {
|
|
|
240
335
|
const diagnostics = Array.isArray(summary.diagnostics)
|
|
241
336
|
? summary.diagnostics.length
|
|
242
337
|
: 0;
|
|
243
|
-
|
|
338
|
+
const recommendations = Array.isArray(summary.recommendations)
|
|
339
|
+
? summary.recommendations.length
|
|
340
|
+
: 0;
|
|
341
|
+
return `\nrecipes active=${active} shadowed=${shadowed} invalid=${invalid} disabled=${disabled} recommendations=${recommendations} diagnostics=${diagnostics}`;
|
|
244
342
|
}
|
|
245
343
|
|
|
246
344
|
function compactActorMessageResult(
|
|
@@ -255,6 +353,11 @@ function compactActorMessageResult(
|
|
|
255
353
|
if (result.bytes !== undefined) tokens.push(`bytes=${String(result.bytes)}`);
|
|
256
354
|
if (result.control) tokens.push(`control=${String(result.control)}`);
|
|
257
355
|
if (result.outbox) tokens.push(`messages=${String(result.outbox)}`);
|
|
356
|
+
if (result.message_count !== undefined)
|
|
357
|
+
tokens.push(`messages=${String(result.message_count)}`);
|
|
358
|
+
if (result.roster_count !== undefined)
|
|
359
|
+
tokens.push(`roster=${String(result.roster_count)}`);
|
|
360
|
+
if (result.room) tokens.push(`room=${String(result.room)}`);
|
|
258
361
|
if (result.tool) tokens.push(`tool=${String(result.tool)}`);
|
|
259
362
|
if (result.stopped === true) tokens.push("stopped=true");
|
|
260
363
|
if (result.signal) tokens.push(`signal=${String(result.signal)}`);
|
|
@@ -352,6 +455,25 @@ function runIdFromActorAddress(
|
|
|
352
455
|
return parsed.value;
|
|
353
456
|
}
|
|
354
457
|
|
|
458
|
+
function assertMessageSenderBelongsToRun(
|
|
459
|
+
message: ActorMessages.ActorMessage,
|
|
460
|
+
run: string,
|
|
461
|
+
routeLabel: string,
|
|
462
|
+
): void {
|
|
463
|
+
if (!message.from) {
|
|
464
|
+
throw new Error(`message to ${message.to} requires from=<actor address>.`);
|
|
465
|
+
}
|
|
466
|
+
const sender = ActorMessages.parseActorAddress(message.from);
|
|
467
|
+
if (
|
|
468
|
+
(sender.kind !== "run" && sender.kind !== "branch") ||
|
|
469
|
+
sender.value !== run
|
|
470
|
+
) {
|
|
471
|
+
throw new Error(
|
|
472
|
+
`message to ${routeLabel} requires from=run:${run} or branch:${run}/<branch>; got ${message.from}.`,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
355
477
|
export function createSpawnToolDefinition<
|
|
356
478
|
TContext extends AsyncRunToolContext,
|
|
357
479
|
>(): any {
|
|
@@ -422,6 +544,8 @@ export function createSpawnToolDefinition<
|
|
|
422
544
|
},
|
|
423
545
|
ctx.cwd,
|
|
424
546
|
);
|
|
547
|
+
ActorRooms.ensureDefaultRoom(meta.state_dir, String(meta.run));
|
|
548
|
+
ActorRooms.writeCommunicationSnapshot(meta.state_dir, String(meta.run));
|
|
425
549
|
return {
|
|
426
550
|
content: [
|
|
427
551
|
{
|
|
@@ -475,6 +599,10 @@ function assertRunAccessibleToContext(
|
|
|
475
599
|
return status;
|
|
476
600
|
}
|
|
477
601
|
|
|
602
|
+
function assertRunExistsForActorMessage(runId: string): Record<string, unknown> {
|
|
603
|
+
return AsyncRuns.getRunStatus(runId);
|
|
604
|
+
}
|
|
605
|
+
|
|
478
606
|
export function createInspectToolDefinition<TContext = unknown>(
|
|
479
607
|
deps: InspectToolDeps<TContext> = {},
|
|
480
608
|
): any {
|
|
@@ -482,7 +610,7 @@ export function createInspectToolDefinition<TContext = unknown>(
|
|
|
482
610
|
name: "inspect",
|
|
483
611
|
label: "Inspect",
|
|
484
612
|
description:
|
|
485
|
-
"Intentionally inspect an actor. Supports run:<id> views: status, tail, messages, artifacts, files, mailbox; coordinator/session status; and tool:<name> status/schema.",
|
|
613
|
+
"Intentionally inspect an actor. 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.",
|
|
486
614
|
parameters: objectSchema(
|
|
487
615
|
{
|
|
488
616
|
lines: stringSchema("Line count for tail/messages views. Default 40."),
|
|
@@ -490,13 +618,13 @@ export function createInspectToolDefinition<TContext = unknown>(
|
|
|
490
618
|
"Optional session run filter: all, running, active, terminal, done, failed, cancelled, killed, or exited.",
|
|
491
619
|
),
|
|
492
620
|
target: stringSchema(
|
|
493
|
-
"Actor address to inspect, e.g. run:<id>, coordinator, session:<id>, session:all, or tool:<name>.",
|
|
621
|
+
"Actor address to inspect, e.g. run:<id>, room:<run>, coordinator, session:<id>, session:all, or tool:<name>.",
|
|
494
622
|
),
|
|
495
623
|
verbose: booleanSchema(
|
|
496
624
|
"Return full JSON instead of compact text where available.",
|
|
497
625
|
),
|
|
498
626
|
view: stringSchema(
|
|
499
|
-
"Inspection view: status, tail, messages, artifacts, files, or
|
|
627
|
+
"Inspection view: status, tail, messages, artifacts, files, mailbox, communication, roster, or contacts.",
|
|
500
628
|
),
|
|
501
629
|
},
|
|
502
630
|
["target", "view"],
|
|
@@ -618,6 +746,100 @@ export function createInspectToolDefinition<TContext = unknown>(
|
|
|
618
746
|
details,
|
|
619
747
|
};
|
|
620
748
|
}
|
|
749
|
+
if (address.kind === "room" && address.value && address.room) {
|
|
750
|
+
const status = assertRunAccessibleToContext(address.value, ctx);
|
|
751
|
+
const stateDir = String(status.state_dir ?? "");
|
|
752
|
+
if (!stateDir) throw new Error(`room:${address.value} has no run state directory.`);
|
|
753
|
+
if (view === "status") {
|
|
754
|
+
const status = ActorRooms.getRoomStatus(stateDir, address.room);
|
|
755
|
+
return {
|
|
756
|
+
content: [
|
|
757
|
+
{
|
|
758
|
+
type: "text" as const,
|
|
759
|
+
text: maybeJsonText(
|
|
760
|
+
status,
|
|
761
|
+
input.verbose === true,
|
|
762
|
+
compactRoomStatus(status),
|
|
763
|
+
),
|
|
764
|
+
},
|
|
765
|
+
],
|
|
766
|
+
details: status,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
if (view === "previews") {
|
|
770
|
+
const previews = ActorRooms.readRoomMessagePreviews(
|
|
771
|
+
stateDir,
|
|
772
|
+
address.room,
|
|
773
|
+
Number(input.lines || 40),
|
|
774
|
+
);
|
|
775
|
+
return {
|
|
776
|
+
content: [
|
|
777
|
+
{
|
|
778
|
+
type: "text" as const,
|
|
779
|
+
text: maybeJsonText(
|
|
780
|
+
previews,
|
|
781
|
+
input.verbose === true,
|
|
782
|
+
compactRoomPreviews(previews),
|
|
783
|
+
),
|
|
784
|
+
},
|
|
785
|
+
],
|
|
786
|
+
details: { previews },
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
if (view === "messages") {
|
|
790
|
+
const messages = ActorRooms.readRoomMessages(
|
|
791
|
+
stateDir,
|
|
792
|
+
address.room,
|
|
793
|
+
Number(input.lines || 40),
|
|
794
|
+
);
|
|
795
|
+
return {
|
|
796
|
+
content: [
|
|
797
|
+
{
|
|
798
|
+
type: "text" as const,
|
|
799
|
+
text: maybeJsonText(
|
|
800
|
+
messages,
|
|
801
|
+
input.verbose === true,
|
|
802
|
+
compactRoomMessages(messages),
|
|
803
|
+
),
|
|
804
|
+
},
|
|
805
|
+
],
|
|
806
|
+
details: { messages },
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
if (view === "contacts") {
|
|
810
|
+
const contacts = ActorRooms.readRoomContacts(stateDir, address.room);
|
|
811
|
+
return {
|
|
812
|
+
content: [
|
|
813
|
+
{
|
|
814
|
+
type: "text" as const,
|
|
815
|
+
text: maybeJsonText(
|
|
816
|
+
contacts,
|
|
817
|
+
input.verbose === true,
|
|
818
|
+
compactRoomContacts(contacts),
|
|
819
|
+
),
|
|
820
|
+
},
|
|
821
|
+
],
|
|
822
|
+
details: { contacts },
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
if (view === "roster") {
|
|
826
|
+
const roster = ActorRooms.readRoomRoster(stateDir, address.room);
|
|
827
|
+
return {
|
|
828
|
+
content: [
|
|
829
|
+
{
|
|
830
|
+
type: "text" as const,
|
|
831
|
+
text: maybeJsonText(
|
|
832
|
+
roster,
|
|
833
|
+
input.verbose === true,
|
|
834
|
+
compactRoomRoster(roster),
|
|
835
|
+
),
|
|
836
|
+
},
|
|
837
|
+
],
|
|
838
|
+
details: { roster },
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
throw new Error("inspect room:<run> supports view=status, view=messages, view=previews, view=roster, or view=contacts.");
|
|
842
|
+
}
|
|
621
843
|
const runId = address.kind === "run" ? address.value : undefined;
|
|
622
844
|
if (!runId)
|
|
623
845
|
throw new Error(
|
|
@@ -702,9 +924,28 @@ export function createInspectToolDefinition<TContext = unknown>(
|
|
|
702
924
|
details: { mailbox },
|
|
703
925
|
};
|
|
704
926
|
}
|
|
927
|
+
case "communication": {
|
|
928
|
+
const status = assertRunAccessibleToContext(runId, ctx);
|
|
929
|
+
const snapshot = ActorRooms.readCommunicationSnapshot(
|
|
930
|
+
String(status.state_dir ?? ""),
|
|
931
|
+
);
|
|
932
|
+
return {
|
|
933
|
+
content: [
|
|
934
|
+
{
|
|
935
|
+
type: "text" as const,
|
|
936
|
+
text: maybeJsonText(
|
|
937
|
+
snapshot ?? {},
|
|
938
|
+
input.verbose === true,
|
|
939
|
+
compactCommunicationSnapshot(snapshot),
|
|
940
|
+
),
|
|
941
|
+
},
|
|
942
|
+
],
|
|
943
|
+
details: { communication: snapshot },
|
|
944
|
+
};
|
|
945
|
+
}
|
|
705
946
|
default:
|
|
706
947
|
throw new Error(
|
|
707
|
-
"inspect view must be one of: status, tail, messages, artifacts, files, mailbox.",
|
|
948
|
+
"inspect view must be one of: status, tail, messages, artifacts, files, mailbox, communication.",
|
|
708
949
|
);
|
|
709
950
|
}
|
|
710
951
|
},
|
|
@@ -722,7 +963,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
|
|
|
722
963
|
name: "message",
|
|
723
964
|
label: "Message",
|
|
724
965
|
description:
|
|
725
|
-
"Send one typed addressed message. Routes to run:<id> mailboxes, branch:<run>/<branch> mailboxes, tool:<name> calls, and coordinator/session-bound run messages.",
|
|
966
|
+
"Send one typed addressed message. Routes to run:<id> mailboxes, branch:<run>/<branch> mailboxes, room:<run> timelines/rosters, tool:<name> calls, and coordinator/session-bound run messages.",
|
|
726
967
|
parameters: objectSchema(
|
|
727
968
|
{
|
|
728
969
|
body: unionSchema([
|
|
@@ -744,7 +985,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
|
|
|
744
985
|
reply_to: stringSchema("Optional message id this message replies to."),
|
|
745
986
|
summary: stringSchema("Optional short human-facing summary."),
|
|
746
987
|
to: stringSchema(
|
|
747
|
-
"Destination actor address, e.g. run:<id>, branch:<run>/<branch>, coordinator, session:<id>, or tool:<name>.",
|
|
988
|
+
"Destination actor address, e.g. run:<id>, branch:<run>/<branch>, room:<run>, coordinator, session:<id>, or tool:<name>.",
|
|
748
989
|
),
|
|
749
990
|
type: stringSchema(
|
|
750
991
|
"Semantic message type, e.g. control.approve or checkpoint.needs_scope.",
|
|
@@ -780,11 +1021,51 @@ export function createActorMessageToolDefinition<TContext = unknown>(
|
|
|
780
1021
|
);
|
|
781
1022
|
}
|
|
782
1023
|
} else if (address.kind === "branch" && address.value) {
|
|
783
|
-
|
|
1024
|
+
const runId = address.value;
|
|
1025
|
+
if (message.from) assertMessageSenderBelongsToRun(message, runId, `branch:${runId}/<branch>`);
|
|
1026
|
+
const status = message.from
|
|
1027
|
+
? assertRunExistsForActorMessage(runId)
|
|
1028
|
+
: assertRunAccessibleToContext(runId, ctx);
|
|
1029
|
+
const stateDir = String(status.state_dir ?? "");
|
|
1030
|
+
if (stateDir && address.branch) {
|
|
1031
|
+
const ensureBranchMember = (actorAddress: string) => {
|
|
1032
|
+
ActorRooms.ensureRoomMember(
|
|
1033
|
+
stateDir,
|
|
1034
|
+
runId,
|
|
1035
|
+
"main",
|
|
1036
|
+
actorAddress,
|
|
1037
|
+
{
|
|
1038
|
+
parent: `run:${runId}`,
|
|
1039
|
+
role: "branch",
|
|
1040
|
+
status: "present",
|
|
1041
|
+
},
|
|
1042
|
+
"Branch joined default room",
|
|
1043
|
+
);
|
|
1044
|
+
ActorRooms.writeBranchCommunicationSnapshot(
|
|
1045
|
+
stateDir,
|
|
1046
|
+
runId,
|
|
1047
|
+
actorAddress,
|
|
1048
|
+
);
|
|
1049
|
+
};
|
|
1050
|
+
ensureBranchMember(message.to);
|
|
1051
|
+
if (message.from) {
|
|
1052
|
+
const sender = ActorMessages.parseActorAddress(message.from);
|
|
1053
|
+
if (sender.kind === "branch" && sender.value === runId) {
|
|
1054
|
+
ensureBranchMember(message.from);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
ActorRooms.writeCommunicationSnapshot(stateDir, runId);
|
|
1058
|
+
}
|
|
784
1059
|
result = AsyncRuns.sendRunMessage(
|
|
785
1060
|
address.value,
|
|
786
1061
|
JSON.stringify(message),
|
|
787
1062
|
);
|
|
1063
|
+
} else if (address.kind === "room" && address.value && address.room) {
|
|
1064
|
+
assertMessageSenderBelongsToRun(message, address.value, `room:${address.value}`);
|
|
1065
|
+
const status = assertRunExistsForActorMessage(address.value);
|
|
1066
|
+
const stateDir = String(status.state_dir ?? "");
|
|
1067
|
+
if (!stateDir) throw new Error(`${message.to} has no run state directory.`);
|
|
1068
|
+
result = { ...ActorRooms.appendRoomMessage(stateDir, address.room, message) };
|
|
788
1069
|
} else if (address.kind === "tool" && address.value) {
|
|
789
1070
|
const tool = deps.getTool?.(address.value);
|
|
790
1071
|
if (!tool || typeof tool.execute !== "function") {
|
|
@@ -842,7 +1123,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
|
|
|
842
1123
|
});
|
|
843
1124
|
} else {
|
|
844
1125
|
throw new Error(
|
|
845
|
-
`message currently supports run:<id>, branch:<run>/<branch>, tool:<name>, coordinator, and session:<id> destinations; unsupported destination: ${message.to}`,
|
|
1126
|
+
`message currently supports run:<id>, branch:<run>/<branch>, room:<run>, tool:<name>, coordinator, and session:<id> destinations; unsupported destination: ${message.to}`,
|
|
846
1127
|
);
|
|
847
1128
|
}
|
|
848
1129
|
return {
|
|
@@ -942,6 +1223,8 @@ export function createRuntimeToolDefinition(
|
|
|
942
1223
|
},
|
|
943
1224
|
ctx.cwd,
|
|
944
1225
|
);
|
|
1226
|
+
ActorRooms.ensureDefaultRoom(meta.state_dir, String(meta.run));
|
|
1227
|
+
ActorRooms.writeCommunicationSnapshot(meta.state_dir, String(meta.run));
|
|
945
1228
|
return {
|
|
946
1229
|
content: [
|
|
947
1230
|
{ type: "text" as const, text: compactAsyncRunStatus(meta) },
|
package/package.json
CHANGED
package/recipes/lens-swarm.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lens-swarm",
|
|
3
3
|
"description": "General-purpose multi-lens review swarm. Launches independent reviewers by lens, then verifies, merges, judges, and normalizes the result.",
|
|
4
|
-
"tool": true,
|
|
5
4
|
"async": true,
|
|
6
5
|
"imports": {
|
|
7
6
|
"coordinator": "subagent-review-coordinator.json"
|
package/skills/actors/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: actors
|
|
3
3
|
description: Highest-density practical guide for pi-actors. Read this skill whenever prompt and tools are not enough for spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 0.
|
|
5
|
+
version: 0.17.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|
|
@@ -37,7 +37,8 @@ Trusted local capability
|
|
|
37
37
|
|
|
38
38
|
- **Command template**: portable execution graph. String leaf, sequence array, or object node with controls.
|
|
39
39
|
- **Recipe**: saved JSON definition wrapping a template with args, defaults, imports, mailbox, artifacts, metadata, and optional `async: true`.
|
|
40
|
-
- **Run actor**: one detached execution instance addressable as `run:<id>` with status, logs, messages, mailbox metadata, files, and artifacts.
|
|
40
|
+
- **Run actor**: one detached execution instance addressable as `run:<id>` with status, logs, messages, mailbox metadata, files, communication snapshot, and artifacts.
|
|
41
|
+
- **Room actor**: shared timeline + roster endpoint addressable as `room:<run>`; every spawned run gets `room:<run>`.
|
|
41
42
|
- **Tool actor**: registered persistent capability addressable as `tool:<name>` and callable through the generated tool or `message`.
|
|
42
43
|
- **Coordinator/session**: the current pi session endpoint that receives bounded actor follow-ups.
|
|
43
44
|
- **Mailbox**: public interaction contract: message types the actor accepts/emits.
|
|
@@ -80,7 +81,8 @@ Envelope fields:
|
|
|
80
81
|
|
|
81
82
|
- Required: `to`, `type`.
|
|
82
83
|
- Useful: `summary`, `body`, `from`, `reply_to`, `correlation_id`, `metadata`.
|
|
83
|
-
- Addresses: `run:<id>`, `branch:<run>/<branch>`, `tool:<name>`, `coordinator`, `session:<id>`.
|
|
84
|
+
- Addresses: `run:<id>`, `branch:<run>/<branch>`, `room:<run>`, `tool:<name>`, `coordinator`, `session:<id>`.
|
|
85
|
+
- Room posts require `from` from the same run (`run:<run>` or `branch:<run>/<branch>`).
|
|
84
86
|
- Standard termination messages: `control.stop`, `control.cancel`, `control.kill`.
|
|
85
87
|
|
|
86
88
|
Check `inspect view=mailbox` before domain-specific messages.
|
|
@@ -91,7 +93,12 @@ Check `inspect view=mailbox` before domain-specific messages.
|
|
|
91
93
|
{ "target": "run:repo-health", "view": "status" }
|
|
92
94
|
{ "target": "run:repo-health", "view": "tail", "lines": "80" }
|
|
93
95
|
{ "target": "run:repo-health", "view": "messages" }
|
|
96
|
+
{ "target": "run:repo-health", "view": "communication" }
|
|
94
97
|
{ "target": "run:repo-health", "view": "artifacts" }
|
|
98
|
+
{ "target": "room:repo-health", "view": "status" }
|
|
99
|
+
{ "target": "room:repo-health", "view": "roster" }
|
|
100
|
+
{ "target": "room:repo-health", "view": "contacts" }
|
|
101
|
+
{ "target": "room:repo-health", "view": "previews" }
|
|
95
102
|
{ "target": "tool:music_player", "view": "status" }
|
|
96
103
|
{ "target": "recipes", "view": "status" }
|
|
97
104
|
{ "target": "coordinator", "view": "status" }
|
|
@@ -101,7 +108,11 @@ Views:
|
|
|
101
108
|
|
|
102
109
|
- `status`: lifecycle, pid, values, progress, result, compact summary.
|
|
103
110
|
- `tail`: recent stdout/stderr/log tail.
|
|
104
|
-
- `messages`: actor messages emitted by the run
|
|
111
|
+
- `messages`: actor messages emitted by the run, or room timeline entries for `room:*`.
|
|
112
|
+
- `communication`: run/branch communication snapshot with self/root/default-room/member/contact hints.
|
|
113
|
+
- `roster`: room member list with address, role, parent, caps, claim, status, and last seen.
|
|
114
|
+
- `contacts`: roster-derived direct-message targets without full roster metadata.
|
|
115
|
+
- `previews`: TUI-ready bounded room message previews with timestamp/from/to/type/summary/body_preview.
|
|
105
116
|
- `mailbox`: declared accepts/emits contract.
|
|
106
117
|
- `files`: run state directory file list.
|
|
107
118
|
- `artifacts`: declared artifact paths/status.
|
|
@@ -109,6 +120,38 @@ Views:
|
|
|
109
120
|
|
|
110
121
|
Let terminal notifications arrive; avoid sleep-poll loops except during diagnosis.
|
|
111
122
|
|
|
123
|
+
## Stable Multi-Actor Review Rules
|
|
124
|
+
|
|
125
|
+
- Prefer independent read-only reviewers for review swarms. Use shared room messages for coordination signals and observability, not for letting reviewers converge early, unless the task explicitly asks for collaborative discussion.
|
|
126
|
+
- Treat inspector-visible communication logs as recipe-quality evidence. Verbose room/direct timelines show whether recipes coordinate clearly, emit useful summaries, over-chat, miss handoffs, choose poor message types, or need better mailbox/artifact conventions. Use `inspect room:<run> view=messages|previews`, `inspect run:<id> view=communication`, and the actor inspector compact/verbose modes to improve recipes after real runs.
|
|
127
|
+
- Smoke-test provider/model availability before launching expensive fanout, or choose a provider known to be configured in this environment. A failed provider fanout creates noisy run transitions without useful review signal.
|
|
128
|
+
- Keep one public communication model: `spawn` creates actors, `message` sends typed envelopes, and `inspect` observes. Avoid adding public side channels or storage nouns when a normal actor address/view can express the operation.
|
|
129
|
+
- Keep route and semantic type separate. Direct, room, coordinator, and session messages may share `type`; delivery behavior comes from `to`.
|
|
130
|
+
- Any UI, summary, or aggregate view that scans run directories must apply coordinator/session ownership filters before exposing summaries or body previews.
|
|
131
|
+
- Treat `communication.json` as visible actor context, not a global mutable truth table. Run-level snapshots should identify the run actor; branch-local snapshots should identify the branch actor.
|
|
132
|
+
- Prefer same-run provenance checks on lateral actor routes. If `from` is accepted for room or branch routes, validate that it belongs to the addressed run.
|
|
133
|
+
|
|
134
|
+
## Persistent Backlog Implementers
|
|
135
|
+
|
|
136
|
+
When using actors as backlog implementers, avoid one-shot subagents that exit after one task. Use long-lived branch actors and keep task selection with the coordinator:
|
|
137
|
+
|
|
138
|
+
1. Coordinator assigns a concrete backlog slice with `task.assign`.
|
|
139
|
+
2. Actor posts `task.claim` to `room:<run>` before editing.
|
|
140
|
+
3. Actor executes and validates the slice.
|
|
141
|
+
4. Actor posts `task.result` and `awaiting_assignment`.
|
|
142
|
+
5. Actor stays alive until the coordinator sends another `task.assign` or an explicit `control.stop`.
|
|
143
|
+
|
|
144
|
+
Use `front`/`back` actors for opposite backlog ends when reducing overlap. Implementer workflows should be packaged as reusable recipe composition, not bespoke scripts: use `coordinator-locker` for queue/assignment/locking, subagent launcher recipes for execution cells, and actor-message utility recipes for structured handoffs. If the existing recipe library cannot express the scenario, add missing reusable component recipes first, then compose the higher-level workflow from them. Supervisors should route coordinator assignments by `body.actor`, preserve the assignment as an object rather than a JSON string, and keep stopped-worker summaries tied to the original actor list.
|
|
145
|
+
|
|
146
|
+
Current packaged building blocks:
|
|
147
|
+
|
|
148
|
+
- `coordinator-locker`: long-lived queue/lock coordinator for assignment and resource ownership.
|
|
149
|
+
- `subagent-prompt`, `subagent-tools`, `subagents-prompts`: execution launchers for one or many agent prompts.
|
|
150
|
+
- `utility-actor-message`: deterministic actor-message envelope construction for handoffs/results.
|
|
151
|
+
- `utility-run-ops-snapshot` and `pipeline-async-run-ops`: inspect live runs/messages before deciding the next assignment.
|
|
152
|
+
|
|
153
|
+
The missing higher-level persistent backlog-implementer workflow is intentionally future work until it can be expressed from reusable recipe cells.
|
|
154
|
+
|
|
112
155
|
## Command Template Standard
|
|
113
156
|
|
|
114
157
|
Forms:
|
|
@@ -180,11 +223,11 @@ Priority for same-name recipes:
|
|
|
180
223
|
|
|
181
224
|
Only matching filename ids compete. Higher priority shadows lower priority. An invalid or `disabled: true` higher-priority recipe blocks fallback so the agent does not silently run standard-library behavior when a user override is broken or intentionally disabled.
|
|
182
225
|
|
|
183
|
-
Muscle-memory lens:
|
|
226
|
+
Muscle-memory lens: `~/.pi/agent/recipes/*.json` is the agent's capability memory. Every recipe in that directory becomes an easy-to-call tool automatically and survives into later sessions. Agents grow this memory either by calling `register_tool`, which writes recipe files there under the hood, or by deliberately editing those recipe files. Treat this directory like `MEMORY.md` for executable habits: useful local patterns belong there; packaged recipes elsewhere are reusable components, not tools.
|
|
184
227
|
|
|
185
|
-
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
|
|
228
|
+
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.
|
|
186
229
|
|
|
187
|
-
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,
|
|
230
|
+
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.
|
|
188
231
|
|
|
189
232
|
## Registered Tools
|
|
190
233
|
|
|
@@ -198,7 +241,7 @@ Tool templates may be:
|
|
|
198
241
|
- A file-backed recipe name/path.
|
|
199
242
|
- A complete recipe body, optionally `async: true`.
|
|
200
243
|
|
|
201
|
-
The user recipe root is the default tool set; packaged recipes are
|
|
244
|
+
The user recipe root is the default tool set by location; packaged recipes are lower-priority standard-library components and are not tools unless copied or registered into the agent recipe root. Ideal runtime behavior is reactive: create/edit/delete recipe files, validate them, then connect valid tools or surface diagnostics without requiring agents to hand-maintain a separate registry.
|
|
202
245
|
|
|
203
246
|
## Recipe Navigator
|
|
204
247
|
|
package/skills/swarm/SKILL.md
CHANGED