@llblab/pi-actors 0.33.0 → 0.34.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.
Files changed (155) hide show
  1. package/AGENTS.md +22 -11
  2. package/BACKLOG.md +1 -1
  3. package/CHANGELOG.md +22 -1
  4. package/dist/index.js +10 -10
  5. package/dist/lib/async-runs.d.ts +23 -75
  6. package/dist/lib/async-runs.js +49 -801
  7. package/dist/lib/command-templates.d.ts +3 -3
  8. package/dist/lib/command-templates.js +3 -3
  9. package/dist/lib/config.d.ts +2 -2
  10. package/dist/lib/config.js +5 -5
  11. package/dist/lib/{output.d.ts → execution-output.d.ts} +3 -3
  12. package/dist/lib/{output.js → execution-output.js} +3 -3
  13. package/dist/lib/execution.js +1 -1
  14. package/dist/lib/{actor-inspector-tui.d.ts → inspector.d.ts} +3 -2
  15. package/dist/lib/{actor-inspector-tui.js → inspector.js} +3 -2
  16. package/dist/lib/mailbox-loop.d.ts +3 -3
  17. package/dist/lib/mailbox-loop.js +3 -3
  18. package/dist/lib/{actor-messages.d.ts → messages.d.ts} +3 -3
  19. package/dist/lib/{actor-messages.js → messages.js} +3 -3
  20. package/dist/lib/{actor-recipe-context.d.ts → recipes-context.d.ts} +3 -2
  21. package/dist/lib/{actor-recipe-context.js → recipes-context.js} +2 -1
  22. package/dist/lib/{recipe-discovery.d.ts → recipes-discovery.d.ts} +8 -8
  23. package/dist/lib/{recipe-discovery.js → recipes-discovery.js} +6 -6
  24. package/dist/lib/registry.d.ts +1 -1
  25. package/dist/lib/registry.js +22 -22
  26. package/dist/lib/{actor-rooms.d.ts → rooms.d.ts} +4 -4
  27. package/dist/lib/{actor-rooms.js → rooms.js} +3 -3
  28. package/dist/lib/runs-artifacts.d.ts +21 -0
  29. package/dist/lib/runs-artifacts.js +68 -0
  30. package/dist/lib/runs-control.d.ts +13 -0
  31. package/dist/lib/runs-control.js +59 -0
  32. package/dist/lib/runs-identity.d.ts +5 -0
  33. package/dist/lib/runs-identity.js +11 -0
  34. package/dist/lib/runs-index.d.ts +18 -0
  35. package/dist/lib/runs-index.js +119 -0
  36. package/dist/lib/runs-mailbox.d.ts +25 -0
  37. package/dist/lib/runs-mailbox.js +146 -0
  38. package/dist/lib/runs-messages.d.ts +15 -0
  39. package/dist/lib/runs-messages.js +179 -0
  40. package/dist/lib/runs-outbox.d.ts +41 -0
  41. package/dist/lib/runs-outbox.js +87 -0
  42. package/dist/lib/runs-process.d.ts +7 -0
  43. package/dist/lib/runs-process.js +36 -0
  44. package/dist/lib/runs-retention.d.ts +8 -0
  45. package/dist/lib/runs-retention.js +56 -0
  46. package/dist/lib/runs-start.d.ts +9 -0
  47. package/dist/lib/runs-start.js +67 -0
  48. package/dist/lib/runs-status.d.ts +8 -0
  49. package/dist/lib/runs-status.js +60 -0
  50. package/dist/lib/runtime.d.ts +2 -2
  51. package/dist/lib/runtime.js +5 -5
  52. package/dist/lib/schema.d.ts +9 -0
  53. package/dist/lib/schema.js +40 -0
  54. package/dist/lib/tools-access.d.ts +19 -0
  55. package/dist/lib/tools-access.js +44 -0
  56. package/dist/lib/tools-inspect.d.ts +11 -0
  57. package/dist/lib/tools-inspect.js +534 -0
  58. package/dist/lib/tools-local.d.ts +14 -0
  59. package/dist/lib/tools-local.js +156 -0
  60. package/dist/lib/tools-mailbox.d.ts +8 -0
  61. package/dist/lib/tools-mailbox.js +48 -0
  62. package/dist/lib/tools-message.d.ts +9 -0
  63. package/dist/lib/tools-message.js +346 -0
  64. package/dist/lib/tools-register.d.ts +18 -0
  65. package/dist/lib/tools-register.js +40 -0
  66. package/dist/lib/tools-response.d.ts +21 -0
  67. package/dist/lib/tools-response.js +219 -0
  68. package/dist/lib/tools-spawn.d.ts +12 -0
  69. package/dist/lib/tools-spawn.js +159 -0
  70. package/dist/lib/tools.d.ts +8 -39
  71. package/dist/lib/tools.js +11 -1490
  72. package/dist/recipes/utility-coordinator-lock-snapshot.json +1 -1
  73. package/dist/scripts/actor-worker.mjs +186 -7
  74. package/dist/scripts/async-runner.mjs +171 -8
  75. package/dist/scripts/build-dist.mjs +1 -1
  76. package/dist/scripts/conformance.mjs +3 -3
  77. package/dist/scripts/coordinator.mjs +733 -11
  78. package/dist/scripts/locker.mjs +345 -16
  79. package/dist/scripts/recipe-utils.mjs +478 -14
  80. package/dist/scripts/validate-recipe.mjs +110 -10
  81. package/dist/skills/actors/SKILL.md +1 -1
  82. package/dist/skills/swarm/SKILL.md +1 -1
  83. package/docs/recipe-library.md +1 -1
  84. package/docs/template-recipes.md +1 -1
  85. package/index.ts +11 -18
  86. package/lib/async-runs.ts +124 -1016
  87. package/lib/command-templates.ts +3 -3
  88. package/lib/config.ts +7 -7
  89. package/lib/{output.ts → execution-output.ts} +3 -3
  90. package/lib/execution.ts +5 -1
  91. package/lib/{actor-inspector-tui.ts → inspector.ts} +4 -3
  92. package/lib/mailbox-loop.ts +3 -3
  93. package/lib/{actor-messages.ts → messages.ts} +3 -3
  94. package/lib/{actor-recipe-context.ts → recipes-context.ts} +3 -2
  95. package/lib/{recipe-discovery.ts → recipes-discovery.ts} +19 -19
  96. package/lib/{recipe-usage.ts → recipes-usage.ts} +2 -2
  97. package/lib/registry.ts +37 -24
  98. package/lib/{actor-rooms.ts → rooms.ts} +4 -4
  99. package/lib/runs-artifacts.ts +96 -0
  100. package/lib/runs-control.ts +85 -0
  101. package/lib/runs-identity.ts +14 -0
  102. package/lib/runs-index.ts +156 -0
  103. package/lib/runs-mailbox.ts +208 -0
  104. package/lib/runs-messages.ts +252 -0
  105. package/lib/runs-outbox.ts +144 -0
  106. package/lib/runs-process.ts +51 -0
  107. package/lib/runs-retention.ts +77 -0
  108. package/lib/runs-start.ts +98 -0
  109. package/lib/runs-status.ts +78 -0
  110. package/lib/runtime.ts +6 -6
  111. package/lib/schema.ts +55 -0
  112. package/lib/tools-access.ts +68 -0
  113. package/lib/tools-inspect.ts +735 -0
  114. package/lib/tools-local.ts +234 -0
  115. package/lib/tools-mailbox.ts +56 -0
  116. package/lib/tools-message.ts +483 -0
  117. package/lib/tools-register.ts +64 -0
  118. package/lib/tools-response.ts +267 -0
  119. package/lib/tools-spawn.ts +240 -0
  120. package/lib/tools.ts +20 -2040
  121. package/package.json +1 -1
  122. package/recipes/utility-coordinator-lock-snapshot.json +1 -1
  123. package/scripts/actor-worker.mjs +186 -7
  124. package/scripts/async-runner.mjs +171 -8
  125. package/scripts/build-dist.mjs +1 -1
  126. package/scripts/conformance.mjs +3 -3
  127. package/scripts/coordinator.mjs +733 -11
  128. package/scripts/locker.mjs +345 -16
  129. package/scripts/recipe-utils.mjs +478 -14
  130. package/scripts/validate-recipe.mjs +110 -10
  131. package/skills/actors/SKILL.md +1 -1
  132. package/skills/swarm/SKILL.md +1 -1
  133. package/dist/lib/actor-worker.d.ts +0 -16
  134. package/dist/lib/actor-worker.js +0 -134
  135. package/dist/lib/async-runner.d.ts +0 -5
  136. package/dist/lib/async-runner.js +0 -134
  137. package/dist/lib/coordinator.d.ts +0 -5
  138. package/dist/lib/coordinator.js +0 -660
  139. package/dist/lib/locker.d.ts +0 -5
  140. package/dist/lib/locker.js +0 -310
  141. package/dist/lib/recipe-utils.d.ts +0 -5
  142. package/dist/lib/recipe-utils.js +0 -385
  143. package/dist/lib/validate-recipe.d.ts +0 -6
  144. package/dist/lib/validate-recipe.js +0 -104
  145. package/lib/actor-worker.ts +0 -174
  146. package/lib/async-runner.ts +0 -173
  147. package/lib/coordinator.ts +0 -742
  148. package/lib/locker.ts +0 -340
  149. package/lib/recipe-utils.ts +0 -486
  150. package/lib/validate-recipe.ts +0 -110
  151. /package/dist/lib/{recipe-references.d.ts → recipes-references.d.ts} +0 -0
  152. /package/dist/lib/{recipe-references.js → recipes-references.js} +0 -0
  153. /package/dist/lib/{recipe-usage.d.ts → recipes-usage.d.ts} +0 -0
  154. /package/dist/lib/{recipe-usage.js → recipes-usage.js} +0 -0
  155. /package/lib/{recipe-references.ts → recipes-references.ts} +0 -0
@@ -0,0 +1,483 @@
1
+ /**
2
+ * Public message tool behavior
3
+ * Zones: addressed actor messages, run controls, branch/room delivery, tool actor invocation
4
+ * Owns the public message execution path and compact delivery responses
5
+ */
6
+
7
+ import * as AsyncRuns from "./async-runs.ts";
8
+ import * as Messages from "./messages.ts";
9
+ import * as Rooms from "./rooms.ts";
10
+ import * as Schema from "./schema.ts";
11
+ import * as ToolsAccess from "./tools-access.ts";
12
+ import * as ToolsMailbox from "./tools-mailbox.ts";
13
+ import * as ToolsResponse from "./tools-response.ts";
14
+
15
+ function asRecord(value: unknown): Record<string, unknown> {
16
+ return value && typeof value === "object" && !Array.isArray(value)
17
+ ? (value as Record<string, unknown>)
18
+ : {};
19
+ }
20
+
21
+ function maybeJsonText(
22
+ value: unknown,
23
+ verbose: boolean | undefined,
24
+ compact: string,
25
+ ): string {
26
+ return verbose ? ToolsResponse.jsonText(value) : compact;
27
+ }
28
+
29
+ function messageBodyToRunLine(message: Messages.ActorMessage): string {
30
+ if (message.type !== "run.message") return JSON.stringify(message);
31
+ if (typeof message.body === "string") return message.body;
32
+ if (message.body === undefined) return message.type;
33
+ return JSON.stringify(message.body);
34
+ }
35
+
36
+ function messageBodyToToolParams(
37
+ message: Messages.ActorMessage,
38
+ ): Record<string, unknown> {
39
+ if (
40
+ message.body &&
41
+ typeof message.body === "object" &&
42
+ !Array.isArray(message.body)
43
+ ) {
44
+ return message.body as Record<string, unknown>;
45
+ }
46
+ if (message.body === undefined) return {};
47
+ return { input: message.body };
48
+ }
49
+
50
+ function formatToolActorFailure(
51
+ tool: string,
52
+ message: Messages.ActorMessage,
53
+ params: Record<string, unknown>,
54
+ error: unknown,
55
+ ): Error {
56
+ const original = error instanceof Error ? error.message : String(error);
57
+ const paramsPreview = ToolsResponse.compactPreview(params, 240) ?? "{}";
58
+ return Object.assign(
59
+ new Error(
60
+ `tool actor ${tool} failed for message type ${message.type}: ${original}; params=${paramsPreview}`,
61
+ ),
62
+ {
63
+ message_type: message.type,
64
+ original_error: original,
65
+ params_preview: paramsPreview,
66
+ tool,
67
+ },
68
+ );
69
+ }
70
+
71
+ function assertMessageSenderBelongsToRun(
72
+ message: Messages.ActorMessage,
73
+ run: string,
74
+ routeLabel: string,
75
+ ): void {
76
+ if (!message.from) {
77
+ throw new Error(`message to ${message.to} requires from=<actor address>.`);
78
+ }
79
+ const sender = Messages.parseActorAddress(message.from);
80
+ if (
81
+ (sender.kind !== "run" && sender.kind !== "branch") ||
82
+ sender.value !== run
83
+ ) {
84
+ throw new Error(
85
+ `message to ${routeLabel} requires from=run:${run} or branch:${run}/<branch>; got ${message.from}.`,
86
+ );
87
+ }
88
+ }
89
+
90
+ async function routeBranchEnvelope(
91
+ stateDir: string,
92
+ runId: string,
93
+ recipient: string,
94
+ message: Messages.ActorMessage,
95
+ _options: { source: "direct" | "room-multicast" },
96
+ ): Promise<Record<string, unknown>> {
97
+ const branchMessage = { ...message, to: recipient };
98
+ Rooms.appendBranchInboxMessage(stateDir, runId, recipient, branchMessage);
99
+ try {
100
+ return await AsyncRuns.sendRunMessage(runId, JSON.stringify(branchMessage));
101
+ } catch (error) {
102
+ const record =
103
+ error && typeof error === "object"
104
+ ? (error as Record<string, unknown>)
105
+ : {};
106
+ if (record.queued === true) {
107
+ return {
108
+ control_path: record.control_path,
109
+ control_type: record.control_type,
110
+ delivery_error:
111
+ record.delivery_error ??
112
+ (error instanceof Error ? error.message : String(error)),
113
+ inbox_id: record.inbox_id,
114
+ queued: true,
115
+ run: runId,
116
+ sent: false,
117
+ state_dir: stateDir,
118
+ };
119
+ }
120
+ throw error;
121
+ }
122
+ }
123
+
124
+ function getRoomMulticastRecipients(
125
+ message: Messages.ActorMessage,
126
+ run: string,
127
+ ): string[] {
128
+ const raw = message.metadata?.recipients;
129
+ if (raw === undefined) return [];
130
+ if (!Array.isArray(raw)) {
131
+ throw new Error("room multicast metadata.recipients must be an array.");
132
+ }
133
+ return raw.map((recipient) => {
134
+ if (typeof recipient !== "string") {
135
+ throw new Error("room multicast recipients must be actor addresses.");
136
+ }
137
+ const parsed = Messages.parseActorAddress(recipient);
138
+ if (parsed.kind !== "branch" || parsed.value !== run) {
139
+ throw new Error(
140
+ `room multicast recipient must be branch:${run}/<branch>; got ${recipient}.`,
141
+ );
142
+ }
143
+ return Messages.formatActorAddress(parsed);
144
+ });
145
+ }
146
+
147
+ function actorMessageNextActions(
148
+ message: Messages.ActorMessage,
149
+ result: Record<string, unknown>,
150
+ ): string[] {
151
+ const actions: string[] = [];
152
+ const address = Messages.parseActorAddress(message.to);
153
+ if (result.delivery_error || result.sent === false) {
154
+ if (address.kind === "run" && address.value) {
155
+ actions.push(`inspect target=run:${address.value} view=status`);
156
+ actions.push(`inspect target=run:${address.value} view=mailbox`);
157
+ } else if (address.kind === "branch" && address.value) {
158
+ actions.push(
159
+ `inspect target=branch:${address.value}/${address.branch ?? "main"} view=mailbox`,
160
+ );
161
+ actions.push(`inspect target=run:${address.value} view=status`);
162
+ }
163
+ }
164
+ if (result.queued === true) {
165
+ if (address.kind === "branch" && address.value) {
166
+ actions.push(
167
+ `inspect target=branch:${address.value}/${address.branch ?? "main"} view=mailbox`,
168
+ );
169
+ } else if (address.kind === "run" && address.value) {
170
+ actions.push(`inspect target=run:${address.value} view=mailbox`);
171
+ }
172
+ }
173
+ return [...new Set(actions)].slice(0, 3);
174
+ }
175
+
176
+ function compactActorMessageResult(
177
+ message: Messages.ActorMessage,
178
+ result: Record<string, unknown>,
179
+ ): string {
180
+ const tokens = [
181
+ `to=${message.to}`,
182
+ `type=${message.type}`,
183
+ `message=${result.sent === true || result.stopped === true ? "sent" : "not_sent"}`,
184
+ ];
185
+ if (result.bytes !== undefined) tokens.push(`bytes=${String(result.bytes)}`);
186
+ if (result.queued === true) tokens.push("queued=true");
187
+ if (result.control) tokens.push(`control=${String(result.control)}`);
188
+ if (result.outbox) tokens.push(`messages=${String(result.outbox)}`);
189
+ if (result.message_count !== undefined)
190
+ tokens.push(`messages=${String(result.message_count)}`);
191
+ if (result.roster_count !== undefined)
192
+ tokens.push(`roster=${String(result.roster_count)}`);
193
+ if (result.room) tokens.push(`room=${String(result.room)}`);
194
+ if (result.tool) tokens.push(`tool=${String(result.tool)}`);
195
+ if (result.stopped === true) tokens.push("stopped=true");
196
+ if (result.signal) tokens.push(`signal=${String(result.signal)}`);
197
+ if (result.invoked === true) tokens.push("invoked=true");
198
+ if (result.delivery_error) {
199
+ tokens.push(
200
+ `delivery_error=${ToolsResponse.compactPreview(result.delivery_error, 96)}`,
201
+ );
202
+ }
203
+ const nextActions = Array.isArray(result.next_actions)
204
+ ? (result.next_actions as string[])
205
+ : actorMessageNextActions(message, result);
206
+ if (nextActions.length > 0)
207
+ tokens.push(
208
+ `next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`,
209
+ );
210
+ return `\n${tokens.join(" ")}`;
211
+ }
212
+
213
+ export interface ActorMessageToolDeps<TContext = unknown> {
214
+ getTool?: (name: string) => any | undefined;
215
+ }
216
+
217
+ export function createActorMessageToolDefinition<TContext = unknown>(
218
+ deps: ActorMessageToolDeps<TContext> = {},
219
+ ): any {
220
+ return {
221
+ name: "message",
222
+ label: "Message",
223
+ description:
224
+ "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>.",
225
+ parameters: Schema.objectSchema(
226
+ {
227
+ body: Schema.unionSchema([
228
+ Schema.stringSchema(
229
+ "Message body. For run:<id>, this is the run-local command line.",
230
+ ),
231
+ Schema.looseObjectSchema("Structured JSON message body."),
232
+ Schema.arraySchema("Structured JSON message body array."),
233
+ ]),
234
+ correlation_id: Schema.stringSchema(
235
+ "Optional correlation id for workflow/task linkage.",
236
+ ),
237
+ from: Schema.stringSchema(
238
+ "Optional sender address, such as coordinator or run:<id>.",
239
+ ),
240
+ metadata: Schema.looseObjectSchema(
241
+ "Optional structured metadata for routing or domain hints.",
242
+ ),
243
+ reply_to: Schema.stringSchema(
244
+ "Optional message id this message replies to.",
245
+ ),
246
+ summary: Schema.stringSchema("Optional short human-facing summary."),
247
+ to: Schema.stringSchema(
248
+ "Destination actor address, e.g. run:<id> or tool:<name>; advanced: branch:<run>/<branch>, room:<run>, coordinator, session:<id>.",
249
+ ),
250
+ type: Schema.stringSchema(
251
+ "Semantic message type, e.g. control.approve or checkpoint.needs_scope.",
252
+ ),
253
+ verbose: Schema.booleanSchema(
254
+ "Return full JSON instead of compact text.",
255
+ ),
256
+ },
257
+ ["to", "type"],
258
+ ),
259
+ async execute(
260
+ _toolCallId: string,
261
+ params: unknown,
262
+ _signal: AbortSignal | undefined,
263
+ _onUpdate: unknown,
264
+ ctx: TContext,
265
+ ) {
266
+ const input = asRecord(params);
267
+ const message = Messages.normalizeActorMessage(input);
268
+ const address = Messages.parseActorAddress(message.to);
269
+ let result: Record<string, unknown>;
270
+ if (address.kind === "run" && address.value) {
271
+ const status = ToolsAccess.assertRunAccessibleToContext(
272
+ address.value,
273
+ ctx,
274
+ );
275
+ const normalizedMailbox = ToolsMailbox.normalizeMailboxContracts(
276
+ asRecord(status.mailbox),
277
+ );
278
+ const acceptedTypes = new Set(
279
+ ToolsMailbox.mailboxTypes(normalizedMailbox.accepts),
280
+ );
281
+ const advisoryWarnings =
282
+ acceptedTypes.size > 0 && !acceptedTypes.has(message.type)
283
+ ? [
284
+ `Message type ${message.type} is not declared in mailbox.accepts for run:${address.value}.`,
285
+ ]
286
+ : [];
287
+ if (message.type === "control.kill") {
288
+ result = AsyncRuns.killRun(address.value);
289
+ } else if (message.type === "control.archive") {
290
+ result = AsyncRuns.archiveRun(address.value);
291
+ } else if (message.type === "control.prune") {
292
+ const body = asRecord(message.body);
293
+ result = AsyncRuns.pruneRun(address.value, {
294
+ preserveArtifacts:
295
+ body.preserve_artifacts === true ||
296
+ body.preserveArtifacts === true,
297
+ });
298
+ } else {
299
+ result = await AsyncRuns.sendRunMessage(
300
+ address.value,
301
+ messageBodyToRunLine(message),
302
+ );
303
+ }
304
+ if (advisoryWarnings.length > 0)
305
+ result = { ...result, warnings: advisoryWarnings };
306
+ } else if (address.kind === "branch" && address.value) {
307
+ const runId = address.value;
308
+ if (message.from)
309
+ assertMessageSenderBelongsToRun(
310
+ message,
311
+ runId,
312
+ `branch:${runId}/<branch>`,
313
+ );
314
+ const status = ToolsAccess.assertRunAccessibleToContext(runId, ctx);
315
+ const stateDir = String(status.state_dir ?? "");
316
+ if (stateDir && address.branch) {
317
+ const ensureBranchMember = (actorAddress: string) => {
318
+ Rooms.ensureRoomMember(
319
+ stateDir,
320
+ runId,
321
+ "main",
322
+ actorAddress,
323
+ {
324
+ parent: `run:${runId}`,
325
+ role: "branch",
326
+ status: "present",
327
+ },
328
+ "Branch joined default room",
329
+ );
330
+ Rooms.writeBranchCommunicationSnapshot(
331
+ stateDir,
332
+ runId,
333
+ actorAddress,
334
+ );
335
+ };
336
+ ensureBranchMember(message.to);
337
+ if (message.from) {
338
+ const sender = Messages.parseActorAddress(message.from);
339
+ if (sender.kind === "branch" && sender.value === runId) {
340
+ ensureBranchMember(message.from);
341
+ }
342
+ }
343
+ Rooms.writeCommunicationSnapshot(stateDir, runId);
344
+ }
345
+ result = await routeBranchEnvelope(
346
+ stateDir,
347
+ runId,
348
+ message.to,
349
+ message,
350
+ {
351
+ source: "direct",
352
+ },
353
+ );
354
+ } else if (address.kind === "room" && address.value && address.room) {
355
+ const runId = address.value;
356
+ assertMessageSenderBelongsToRun(message, runId, `room:${runId}`);
357
+ const status = ToolsAccess.assertRunAccessibleToContext(runId, ctx);
358
+ const stateDir = String(status.state_dir ?? "");
359
+ if (!stateDir)
360
+ throw new Error(`${message.to} has no run state directory.`);
361
+ const recipients = getRoomMulticastRecipients(message, runId);
362
+ const roomResult = Rooms.appendRoomMessage(
363
+ stateDir,
364
+ address.room,
365
+ message,
366
+ );
367
+ await Promise.all(
368
+ recipients.map((recipient) =>
369
+ routeBranchEnvelope(stateDir, runId, recipient, message, {
370
+ source: "room-multicast",
371
+ }),
372
+ ),
373
+ );
374
+ result = {
375
+ ...roomResult,
376
+ ...(recipients.length > 0
377
+ ? { multicast: recipients, multicast_count: recipients.length }
378
+ : {}),
379
+ };
380
+ } else if (address.kind === "tool" && address.value) {
381
+ const tool = deps.getTool?.(address.value);
382
+ if (!tool || typeof tool.execute !== "function") {
383
+ throw new Error(
384
+ `tool actor not found or not executable: ${address.value}`,
385
+ );
386
+ }
387
+ const toolParams = messageBodyToToolParams(message);
388
+ let toolResult: unknown;
389
+ try {
390
+ toolResult = await tool.execute(
391
+ `message:${message.type}`,
392
+ toolParams,
393
+ _signal,
394
+ _onUpdate,
395
+ ctx,
396
+ );
397
+ } catch (error) {
398
+ throw formatToolActorFailure(
399
+ address.value,
400
+ message,
401
+ toolParams,
402
+ error,
403
+ );
404
+ }
405
+ result = {
406
+ invoked: true,
407
+ sent: true,
408
+ tool: address.value,
409
+ tool_result: toolResult,
410
+ };
411
+ } else if (address.kind === "coordinator" || address.kind === "session") {
412
+ if (!message.from) {
413
+ throw new Error(`message to ${address.kind} requires from=run:<id>.`);
414
+ }
415
+ const sender = Messages.parseActorAddress(message.from);
416
+ if (sender.kind !== "run" || !sender.value) {
417
+ throw new Error(
418
+ `message to ${address.kind} currently requires from=run:<id>.`,
419
+ );
420
+ }
421
+ const senderStatus = ToolsAccess.assertRunAccessibleToContext(
422
+ sender.value,
423
+ ctx,
424
+ );
425
+ if (address.kind === "session") {
426
+ if (!senderStatus.ownerId) {
427
+ throw ToolsAccess.sessionMismatchError({
428
+ currentSession: undefined,
429
+ expectedSession: address.value,
430
+ run: sender.value,
431
+ target: `session:${address.value}`,
432
+ });
433
+ }
434
+ if (senderStatus.ownerId !== address.value) {
435
+ throw ToolsAccess.sessionMismatchError({
436
+ currentSession: String(senderStatus.ownerId),
437
+ expectedSession: address.value,
438
+ run: sender.value,
439
+ target: `session:${address.value}`,
440
+ });
441
+ }
442
+ }
443
+ result = AsyncRuns.appendRunOutboxEvent(sender.value, {
444
+ body: message.body,
445
+ correlation_id: message.correlation_id,
446
+ delivery:
447
+ message.metadata?.requires_response === true ||
448
+ address.kind === "session"
449
+ ? "followup"
450
+ : undefined,
451
+ event: message.type,
452
+ from: message.from,
453
+ metadata: message.metadata,
454
+ reply_to: message.reply_to,
455
+ summary: message.summary,
456
+ to: message.to,
457
+ type: message.type,
458
+ });
459
+ } else {
460
+ throw new Error(
461
+ `message currently supports run:<id>, branch:<run>/<branch>, room:<run>, tool:<name>, coordinator, and session:<id> destinations; unsupported destination: ${message.to}`,
462
+ );
463
+ }
464
+ const nextActions = actorMessageNextActions(message, result);
465
+ const resultWithNext = nextActions.length
466
+ ? { ...result, next_actions: nextActions }
467
+ : result;
468
+ return {
469
+ content: [
470
+ {
471
+ type: "text" as const,
472
+ text: maybeJsonText(
473
+ { message, result: resultWithNext },
474
+ input.verbose === true,
475
+ compactActorMessageResult(message, resultWithNext),
476
+ ),
477
+ },
478
+ ],
479
+ details: { message, result: resultWithNext },
480
+ };
481
+ },
482
+ };
483
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Public register_tool behavior
3
+ * Zones: runtime tool registration, tool registry mutation, register_tool schema
4
+ * Owns the public register_tool definition that persists local agent capabilities
5
+ */
6
+
7
+ import * as Prompts from "./prompts.ts";
8
+ import * as Registry from "./registry.ts";
9
+ import * as Schema from "./schema.ts";
10
+
11
+ export type RegisterToolInput = Registry.RegisterToolInput;
12
+ export type RegisterToolRuntimeDeps<TContext> =
13
+ Registry.RegisterToolRuntimeDeps<TContext>;
14
+
15
+ const stringSchema = Schema.stringSchema;
16
+ const booleanSchema = Schema.booleanSchema;
17
+ const nullSchema = Schema.nullSchema;
18
+ const arraySchema = Schema.arraySchema;
19
+ const unionSchema = Schema.unionSchema;
20
+ const objectSchema = Schema.objectSchema;
21
+ const looseObjectSchema = Schema.looseObjectSchema;
22
+
23
+ export function createRegisterToolDefinition<TContext>(
24
+ deps: RegisterToolRuntimeDeps<TContext>,
25
+ ) {
26
+ return {
27
+ name: "register_tool",
28
+ label: "Register Tool",
29
+ description: Prompts.REGISTER_TOOL_DESCRIPTION,
30
+ promptSnippet: Prompts.REGISTER_TOOL_PROMPT_SNIPPET,
31
+ promptGuidelines: Prompts.REGISTER_TOOL_GUIDELINES,
32
+ parameters: objectSchema(
33
+ {
34
+ args: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.args),
35
+ async: booleanSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.async),
36
+ description: stringSchema(
37
+ Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.description,
38
+ ),
39
+ name: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.name),
40
+ state_dir: stringSchema(
41
+ Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.state_dir,
42
+ ),
43
+ template: unionSchema([
44
+ stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
45
+ looseObjectSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
46
+ arraySchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.templateArray),
47
+ nullSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.templateNull),
48
+ ]),
49
+ update: booleanSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.update),
50
+ values: looseObjectSchema(
51
+ Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.values,
52
+ ),
53
+ },
54
+ [],
55
+ ),
56
+ execute: async (
57
+ _toolCallId: string,
58
+ params: unknown,
59
+ _signal: AbortSignal | undefined,
60
+ _onUpdate: unknown,
61
+ ctx: TContext,
62
+ ) => Registry.executeRegisterTool(params, ctx, deps),
63
+ };
64
+ }