@llblab/pi-actors 0.42.2 → 0.43.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 (254) hide show
  1. package/AGENTS.md +121 -175
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +113 -276
  4. package/dist/fixtures/protocol/control-endpoint.json +6 -0
  5. package/dist/fixtures/protocol/control-record.json +9 -0
  6. package/dist/fixtures/protocol/recipe-summary.json +4 -12
  7. package/dist/fixtures/protocol/trace-event.json +9 -0
  8. package/dist/lib/async-runs.d.ts +14 -38
  9. package/dist/lib/async-runs.js +158 -108
  10. package/dist/lib/control.d.ts +12 -0
  11. package/dist/lib/control.js +84 -0
  12. package/dist/lib/execution-sessions.d.ts +17 -0
  13. package/dist/lib/execution-sessions.js +85 -0
  14. package/dist/lib/file-state.d.ts +1 -0
  15. package/dist/lib/file-state.js +17 -5
  16. package/dist/lib/inspector-actions.d.ts +2 -2
  17. package/dist/lib/inspector-actions.js +2 -2
  18. package/dist/lib/inspector-command.js +3 -3
  19. package/dist/lib/inspector-overlay.d.ts +52 -70
  20. package/dist/lib/inspector-overlay.js +532 -905
  21. package/dist/lib/inspector.d.ts +3 -71
  22. package/dist/lib/inspector.js +19 -665
  23. package/dist/lib/limits.d.ts +4 -2
  24. package/dist/lib/limits.js +4 -2
  25. package/dist/lib/observability.d.ts +17 -17
  26. package/dist/lib/observability.js +45 -84
  27. package/dist/lib/pi.d.ts +1 -1
  28. package/dist/lib/prompts.d.ts +1 -1
  29. package/dist/lib/prompts.js +2 -2
  30. package/dist/lib/recipe-control.d.ts +7 -0
  31. package/dist/lib/recipe-control.js +39 -0
  32. package/dist/lib/recipes-discovery.js +2 -0
  33. package/dist/lib/recipes-references.d.ts +1 -14
  34. package/dist/lib/recipes-references.js +6 -21
  35. package/dist/lib/review-projection.js +1 -5
  36. package/dist/lib/run-ui-runtime.js +2 -2
  37. package/dist/lib/runs-control-delivery.d.ts +21 -0
  38. package/dist/lib/runs-control-delivery.js +127 -0
  39. package/dist/lib/runs-controls.d.ts +35 -0
  40. package/dist/lib/runs-controls.js +144 -0
  41. package/dist/lib/runs-retention.d.ts +7 -0
  42. package/dist/lib/runs-retention.js +27 -3
  43. package/dist/lib/runs-start.js +4 -2
  44. package/dist/lib/runs-status.js +11 -6
  45. package/dist/lib/runs-trace.d.ts +24 -0
  46. package/dist/lib/runs-trace.js +98 -0
  47. package/dist/lib/runtime-notifier.d.ts +1 -1
  48. package/dist/lib/runtime-notifier.js +1 -1
  49. package/dist/lib/tools-inspect.d.ts +3 -3
  50. package/dist/lib/tools-inspect.js +203 -708
  51. package/dist/lib/tools-local.js +2 -10
  52. package/dist/lib/tools-message.d.ts +7 -7
  53. package/dist/lib/tools-message.js +95 -396
  54. package/dist/lib/tools-response.d.ts +1 -4
  55. package/dist/lib/tools-response.js +5 -39
  56. package/dist/lib/tools-spawn.js +16 -28
  57. package/dist/lib/tools.js +1 -2
  58. package/dist/lib/trace-projection.d.ts +22 -0
  59. package/dist/lib/trace-projection.js +165 -0
  60. package/dist/recipes/draft-review.json +0 -10
  61. package/dist/recipes/lens-swarm.json +0 -14
  62. package/dist/recipes/music-player.json +10 -19
  63. package/dist/recipes/pipeline-architect-coordinator.json +0 -11
  64. package/dist/recipes/pipeline-artifact-bundle.json +1 -22
  65. package/dist/recipes/pipeline-artifact-report.json +1 -18
  66. package/dist/recipes/pipeline-artifact-write.json +1 -18
  67. package/dist/recipes/pipeline-async-run-ops.json +0 -12
  68. package/dist/recipes/pipeline-checkpoint-continuation.json +0 -14
  69. package/dist/recipes/pipeline-development-tasking.json +0 -12
  70. package/dist/recipes/pipeline-docs-maintenance.json +0 -12
  71. package/dist/recipes/pipeline-media-library.json +0 -12
  72. package/dist/recipes/pipeline-quorum-review.json +0 -12
  73. package/dist/recipes/pipeline-release-readiness.json +0 -12
  74. package/dist/recipes/pipeline-release-summary.json +0 -12
  75. package/dist/recipes/pipeline-repo-health.json +0 -12
  76. package/dist/recipes/pipeline-research-synthesis.json +0 -11
  77. package/dist/recipes/pipeline-review-readiness.json +0 -12
  78. package/dist/recipes/resource-locker.json +27 -0
  79. package/dist/recipes/subagent-artifact.json +0 -9
  80. package/dist/recipes/subagent-checkpoint.json +0 -10
  81. package/dist/recipes/subagent-conflict-report.json +0 -11
  82. package/dist/recipes/subagent-contradiction-map.json +0 -11
  83. package/dist/recipes/subagent-critic.json +0 -11
  84. package/dist/recipes/subagent-evidence-map.json +0 -11
  85. package/dist/recipes/subagent-followup.json +0 -10
  86. package/dist/recipes/subagent-judge.json +0 -11
  87. package/dist/recipes/subagent-merge.json +0 -11
  88. package/dist/recipes/subagent-normalize.json +0 -11
  89. package/dist/recipes/subagent-plan.json +0 -11
  90. package/dist/recipes/subagent-preflight.json +0 -11
  91. package/dist/recipes/subagent-prompt.json +0 -10
  92. package/dist/recipes/subagent-quorum.json +0 -10
  93. package/dist/recipes/subagent-review-coordinator.json +0 -14
  94. package/dist/recipes/subagent-review.json +0 -11
  95. package/dist/recipes/subagent-task-card.json +0 -11
  96. package/dist/recipes/subagent-tools.json +0 -10
  97. package/dist/recipes/subagent-verify.json +0 -11
  98. package/dist/recipes/subagents-prompts.json +0 -10
  99. package/dist/recipes/tool-review.json +0 -10
  100. package/dist/scripts/async-runner.mjs +25 -25
  101. package/dist/scripts/conformance.mjs +4 -2
  102. package/dist/scripts/locker.mjs +200 -66
  103. package/dist/scripts/music-player.mjs +159 -150
  104. package/dist/scripts/recipe-utils.mjs +6 -96
  105. package/dist/scripts/release-gates.mjs +60 -0
  106. package/dist/scripts/validate-recipe.mjs +3 -53
  107. package/dist/skills/actors/SKILL.md +53 -266
  108. package/dist/skills/swarm/SKILL.md +11 -33
  109. package/docs/0.43-baseline.md +44 -0
  110. package/docs/README.md +3 -3
  111. package/docs/actor-inspector.md +26 -64
  112. package/docs/actors-deep-reference.md +92 -50
  113. package/docs/async-runs.md +81 -328
  114. package/docs/command-templates.md +2 -2
  115. package/docs/component-recipes.md +30 -133
  116. package/docs/recipe-library.md +57 -182
  117. package/docs/task-first-recipes.md +10 -12
  118. package/docs/template-recipes.md +76 -289
  119. package/docs/tool-registry.md +41 -161
  120. package/fixtures/protocol/control-endpoint.json +6 -0
  121. package/fixtures/protocol/control-record.json +9 -0
  122. package/fixtures/protocol/recipe-summary.json +4 -12
  123. package/fixtures/protocol/trace-event.json +9 -0
  124. package/lib/async-runs.ts +202 -201
  125. package/lib/control.ts +102 -0
  126. package/lib/execution-sessions.ts +111 -0
  127. package/lib/file-state.ts +17 -4
  128. package/lib/inspector-actions.ts +2 -2
  129. package/lib/inspector-command.ts +3 -3
  130. package/lib/inspector-overlay.ts +577 -1121
  131. package/lib/inspector.ts +46 -979
  132. package/lib/limits.ts +4 -2
  133. package/lib/observability.ts +63 -104
  134. package/lib/pi.ts +1 -1
  135. package/lib/prompts.ts +2 -2
  136. package/lib/recipe-control.ts +45 -0
  137. package/lib/recipes-discovery.ts +2 -0
  138. package/lib/recipes-references.ts +9 -45
  139. package/lib/review-projection.ts +1 -5
  140. package/lib/run-ui-runtime.ts +2 -2
  141. package/lib/runs-control-delivery.ts +181 -0
  142. package/lib/runs-controls.ts +204 -0
  143. package/lib/runs-retention.ts +38 -3
  144. package/lib/runs-start.ts +4 -2
  145. package/lib/runs-status.ts +11 -6
  146. package/lib/runs-trace.ts +132 -0
  147. package/lib/runtime-notifier.ts +1 -1
  148. package/lib/tools-inspect.ts +240 -901
  149. package/lib/tools-local.ts +2 -12
  150. package/lib/tools-message.ts +112 -519
  151. package/lib/tools-response.ts +5 -52
  152. package/lib/tools-spawn.ts +16 -32
  153. package/lib/tools.ts +1 -2
  154. package/lib/trace-projection.ts +221 -0
  155. package/package.json +2 -1
  156. package/recipes/draft-review.json +0 -10
  157. package/recipes/lens-swarm.json +0 -14
  158. package/recipes/music-player.json +10 -19
  159. package/recipes/pipeline-architect-coordinator.json +0 -11
  160. package/recipes/pipeline-artifact-bundle.json +1 -22
  161. package/recipes/pipeline-artifact-report.json +1 -18
  162. package/recipes/pipeline-artifact-write.json +1 -18
  163. package/recipes/pipeline-async-run-ops.json +0 -12
  164. package/recipes/pipeline-checkpoint-continuation.json +0 -14
  165. package/recipes/pipeline-development-tasking.json +0 -12
  166. package/recipes/pipeline-docs-maintenance.json +0 -12
  167. package/recipes/pipeline-media-library.json +0 -12
  168. package/recipes/pipeline-quorum-review.json +0 -12
  169. package/recipes/pipeline-release-readiness.json +0 -12
  170. package/recipes/pipeline-release-summary.json +0 -12
  171. package/recipes/pipeline-repo-health.json +0 -12
  172. package/recipes/pipeline-research-synthesis.json +0 -11
  173. package/recipes/pipeline-review-readiness.json +0 -12
  174. package/recipes/resource-locker.json +27 -0
  175. package/recipes/subagent-artifact.json +0 -9
  176. package/recipes/subagent-checkpoint.json +0 -10
  177. package/recipes/subagent-conflict-report.json +0 -11
  178. package/recipes/subagent-contradiction-map.json +0 -11
  179. package/recipes/subagent-critic.json +0 -11
  180. package/recipes/subagent-evidence-map.json +0 -11
  181. package/recipes/subagent-followup.json +0 -10
  182. package/recipes/subagent-judge.json +0 -11
  183. package/recipes/subagent-merge.json +0 -11
  184. package/recipes/subagent-normalize.json +0 -11
  185. package/recipes/subagent-plan.json +0 -11
  186. package/recipes/subagent-preflight.json +0 -11
  187. package/recipes/subagent-prompt.json +0 -10
  188. package/recipes/subagent-quorum.json +0 -10
  189. package/recipes/subagent-review-coordinator.json +0 -14
  190. package/recipes/subagent-review.json +0 -11
  191. package/recipes/subagent-task-card.json +0 -11
  192. package/recipes/subagent-tools.json +0 -10
  193. package/recipes/subagent-verify.json +0 -11
  194. package/recipes/subagents-prompts.json +0 -10
  195. package/recipes/tool-review.json +0 -10
  196. package/scripts/async-runner.mjs +25 -25
  197. package/scripts/conformance.mjs +4 -2
  198. package/scripts/locker.mjs +200 -66
  199. package/scripts/music-player.mjs +159 -150
  200. package/scripts/recipe-utils.mjs +6 -96
  201. package/scripts/release-gates.mjs +60 -0
  202. package/scripts/validate-recipe.mjs +3 -53
  203. package/skills/actors/SKILL.md +53 -266
  204. package/skills/swarm/SKILL.md +11 -33
  205. package/dist/fixtures/protocol/actor-message-branch.json +0 -13
  206. package/dist/fixtures/protocol/mailbox-contract.json +0 -15
  207. package/dist/fixtures/protocol/room-message.json +0 -11
  208. package/dist/fixtures/protocol/room-roster.json +0 -11
  209. package/dist/fixtures/protocol/run-inbox-message.json +0 -9
  210. package/dist/fixtures/protocol/run-outbox-event.json +0 -9
  211. package/dist/lib/mailbox-loop.d.ts +0 -41
  212. package/dist/lib/mailbox-loop.js +0 -60
  213. package/dist/lib/messages.d.ts +0 -25
  214. package/dist/lib/messages.js +0 -122
  215. package/dist/lib/rooms.d.ts +0 -104
  216. package/dist/lib/rooms.js +0 -647
  217. package/dist/lib/runs-mailbox.d.ts +0 -25
  218. package/dist/lib/runs-mailbox.js +0 -146
  219. package/dist/lib/runs-messages.d.ts +0 -15
  220. package/dist/lib/runs-messages.js +0 -179
  221. package/dist/lib/runs-outbox.d.ts +0 -41
  222. package/dist/lib/runs-outbox.js +0 -87
  223. package/dist/lib/tools-mailbox.d.ts +0 -8
  224. package/dist/lib/tools-mailbox.js +0 -48
  225. package/dist/recipes/actor-worker.json +0 -39
  226. package/dist/recipes/coordinator-locker.json +0 -45
  227. package/dist/recipes/locker.json +0 -45
  228. package/dist/recipes/pipeline-room-swarm.json +0 -50
  229. package/dist/recipes/subagent-message.json +0 -32
  230. package/dist/recipes/utility-actor-message.json +0 -23
  231. package/dist/scripts/actor-worker.mjs +0 -214
  232. package/dist/scripts/coordinator.mjs +0 -799
  233. package/docs/actor-messages.md +0 -225
  234. package/fixtures/protocol/actor-message-branch.json +0 -13
  235. package/fixtures/protocol/mailbox-contract.json +0 -15
  236. package/fixtures/protocol/room-message.json +0 -11
  237. package/fixtures/protocol/room-roster.json +0 -11
  238. package/fixtures/protocol/run-inbox-message.json +0 -9
  239. package/fixtures/protocol/run-outbox-event.json +0 -9
  240. package/lib/mailbox-loop.ts +0 -144
  241. package/lib/messages.ts +0 -151
  242. package/lib/rooms.ts +0 -939
  243. package/lib/runs-mailbox.ts +0 -208
  244. package/lib/runs-messages.ts +0 -252
  245. package/lib/runs-outbox.ts +0 -144
  246. package/lib/tools-mailbox.ts +0 -56
  247. package/recipes/actor-worker.json +0 -39
  248. package/recipes/coordinator-locker.json +0 -45
  249. package/recipes/locker.json +0 -45
  250. package/recipes/pipeline-room-swarm.json +0 -50
  251. package/recipes/subagent-message.json +0 -32
  252. package/recipes/utility-actor-message.json +0 -23
  253. package/scripts/actor-worker.mjs +0 -214
  254. package/scripts/coordinator.mjs +0 -799
@@ -1,15 +1,13 @@
1
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
2
+ * Public Control facade under the stable `message` tool name.
3
+ * Zones: Control request validation, Run/runtime dispatch, compact receipts
4
+ * Owns public Control execution; journaling, delivery, and lifecycle mutation stay in Run domains.
5
5
  */
6
6
 
7
7
  import * as AsyncRuns from "./async-runs.ts";
8
- import * as Messages from "./messages.ts";
9
- import * as Rooms from "./rooms.ts";
8
+ import * as Control from "./control.ts";
10
9
  import * as Schema from "./schema.ts";
11
10
  import * as ToolsAccess from "./tools-access.ts";
12
- import * as ToolsMailbox from "./tools-mailbox.ts";
13
11
  import * as ToolsResponse from "./tools-response.ts";
14
12
 
15
13
  function asRecord(value: unknown): Record<string, unknown> {
@@ -18,325 +16,63 @@ function asRecord(value: unknown): Record<string, unknown> {
18
16
  : {};
19
17
  }
20
18
 
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 normalizeDeliveryOutcome(
148
- address: Messages.ActorAddress,
149
- result: Record<string, unknown>,
150
- ): Record<string, unknown> {
151
- if (result.reason) return result;
152
- if (address.kind === "run") {
153
- if (result.stopped === true) {
154
- return {
155
- ...result,
156
- consumer: "run-control",
157
- delivered: true,
158
- persisted: true,
159
- reason: "control_applied",
160
- };
161
- }
162
- return {
163
- ...result,
164
- consumer: result.control_type ?? result.control ?? "run-control",
165
- delivered: result.sent === true && result.queued !== true,
166
- persisted: true,
167
- reason: result.delivery_error
168
- ? "delivery_failed_persisted"
169
- : result.queued === true
170
- ? "queued_mailbox"
171
- : "delivered",
172
- };
173
- }
174
- if (address.kind === "branch") {
175
- return {
176
- ...result,
177
- consumer: "branch-mailbox",
178
- delivered: result.sent === true,
179
- persisted: true,
180
- reason: result.delivery_error
181
- ? "branch_persisted_parent_unavailable"
182
- : "branch_persisted_forwarded",
183
- };
184
- }
185
- if (address.kind === "room") {
186
- return {
187
- ...result,
188
- consumer: "room-timeline",
189
- delivered: true,
190
- forwarded: Number(result.multicast_count ?? 0) > 0,
191
- persisted: true,
192
- reason: "room_persisted",
193
- };
194
- }
195
- if (address.kind === "tool") {
196
- return {
197
- ...result,
198
- consumer: "tool",
199
- delivered: true,
200
- persisted: false,
201
- reason: "tool_invoked",
202
- };
203
- }
204
- if (address.kind === "coordinator" || address.kind === "session") {
205
- return {
206
- ...result,
207
- consumer: "run-outbox",
208
- delivered: false,
209
- persisted: true,
210
- queued: true,
211
- reason: `${address.kind}_outbox_persisted`,
212
- };
213
- }
214
- return result;
215
- }
216
-
217
- function actorMessageNextActions(
218
- message: Messages.ActorMessage,
219
- result: Record<string, unknown>,
220
- ): string[] {
221
- const actions: string[] = [];
222
- const address = Messages.parseActorAddress(message.to);
223
- if (result.delivery_error || result.sent === false) {
224
- if (address.kind === "run" && address.value) {
225
- actions.push(`inspect target=run:${address.value} view=status`);
226
- actions.push(`inspect target=run:${address.value} view=mailbox`);
227
- } else if (address.kind === "branch" && address.value) {
228
- actions.push(
229
- `inspect target=branch:${address.value}/${address.branch ?? "main"} view=mailbox`,
230
- );
231
- actions.push(`inspect target=run:${address.value} view=status`);
232
- }
233
- }
234
- if (result.queued === true) {
235
- if (address.kind === "branch" && address.value) {
236
- actions.push(
237
- `inspect target=branch:${address.value}/${address.branch ?? "main"} view=mailbox`,
238
- );
239
- } else if (address.kind === "run" && address.value) {
240
- actions.push(`inspect target=run:${address.value} view=mailbox`);
241
- }
242
- }
243
- return [...new Set(actions)].slice(0, 3);
244
- }
245
-
246
- function compactActorMessageResult(
247
- message: Messages.ActorMessage,
19
+ function compactControlResult(
20
+ request: Control.ControlRequest,
248
21
  result: Record<string, unknown>,
249
22
  ): string {
250
23
  const tokens = [
251
- `to=${message.to}`,
252
- `type=${message.type}`,
253
- `message=${result.sent === true || result.stopped === true ? "sent" : "not_sent"}`,
24
+ `target=${request.target}`,
25
+ `action=${request.action}`,
26
+ `status=${String(result.delivery ?? result.status ?? (result.stopped === true ? "handled" : "accepted"))}`,
254
27
  ];
255
- if (result.bytes !== undefined) tokens.push(`bytes=${String(result.bytes)}`);
256
- if (result.delivered !== undefined)
257
- tokens.push(`delivered=${String(result.delivered)}`);
258
- if (result.queued === true) tokens.push("queued=true");
259
- if (result.persisted !== undefined)
260
- tokens.push(`persisted=${String(result.persisted)}`);
261
- if (result.forwarded !== undefined)
262
- tokens.push(`forwarded=${String(result.forwarded)}`);
263
- if (result.consumer) tokens.push(`consumer=${String(result.consumer)}`);
264
- if (result.reason) tokens.push(`reason=${String(result.reason)}`);
265
- if (result.control) tokens.push(`control=${String(result.control)}`);
266
- if (result.outbox) tokens.push(`messages=${String(result.outbox)}`);
267
- if (result.message_count !== undefined)
268
- tokens.push(`messages=${String(result.message_count)}`);
269
- if (result.roster_count !== undefined)
270
- tokens.push(`roster=${String(result.roster_count)}`);
271
- if (result.room) tokens.push(`room=${String(result.room)}`);
272
- if (result.tool) tokens.push(`tool=${String(result.tool)}`);
273
- if (result.stopped === true) tokens.push("stopped=true");
274
- if (result.signal) tokens.push(`signal=${String(result.signal)}`);
275
- if (result.invoked === true) tokens.push("invoked=true");
276
- if (result.delivery_error) {
28
+ if (result.control_id) tokens.push(`control=${String(result.control_id)}`);
29
+ if (result.reason) tokens.push(`reason=${String(result.reason).replaceAll(/\s+/g, "_")}`);
30
+ if (Array.isArray(result.next_actions) && result.next_actions.length > 0) {
277
31
  tokens.push(
278
- `delivery_error=${ToolsResponse.compactPreview(result.delivery_error, 96)}`,
32
+ `next=${(result.next_actions as string[])
33
+ .map((action) => action.replaceAll(/\s+/g, "_"))
34
+ .join("|")}`,
279
35
  );
280
36
  }
281
- const nextActions = Array.isArray(result.next_actions)
282
- ? (result.next_actions as string[])
283
- : actorMessageNextActions(message, result);
284
- if (nextActions.length > 0)
285
- tokens.push(
286
- `next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`,
287
- );
288
37
  return `\n${tokens.join(" ")}`;
289
38
  }
290
39
 
291
- export interface ActorMessageToolDeps<TContext = unknown> {
292
- getTool?: (name: string) => any | undefined;
40
+ function runNextActions(run: string, result: Record<string, unknown>): string[] {
41
+ if (result.delivery === "queued" || result.delivery === "delivered") {
42
+ return [`inspect target=run:${run} view=control`];
43
+ }
44
+ if (result.stopped === true) return [`inspect target=run:${run} view=control`];
45
+ return [];
46
+ }
47
+
48
+ export interface ControlToolDeps {
49
+ getTool?: (name: string) => unknown;
293
50
  handleRuntimeMessage?: (
294
- type: string,
295
- body: unknown,
51
+ action: string,
52
+ input: unknown,
296
53
  ) => Record<string, unknown>;
297
54
  }
298
55
 
299
- export function createActorMessageToolDefinition<TContext = unknown>(
300
- deps: ActorMessageToolDeps<TContext> = {},
56
+ export function createControlToolDefinition<TContext = unknown>(
57
+ deps: ControlToolDeps = {},
301
58
  ): any {
302
59
  return {
303
60
  name: "message",
304
- label: "Message",
61
+ label: "Control",
305
62
  description:
306
- "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>.",
63
+ "Apply one Control action to an existing Run or the pi-actors runtime.",
307
64
  parameters: Schema.objectSchema(
308
65
  {
309
- body: Schema.unionSchema([
310
- Schema.stringSchema(
311
- "Message body. For run:<id>, this is the run-local command line.",
312
- ),
313
- Schema.looseObjectSchema("Structured JSON message body."),
314
- Schema.arraySchema("Structured JSON message body array."),
66
+ target: Schema.stringSchema("Run target run:<id>, or literal runtime."),
67
+ action: Schema.stringSchema("Lowercase semantic Control action."),
68
+ input: Schema.unionSchema([
69
+ Schema.stringSchema("Optional Control input string."),
70
+ Schema.looseObjectSchema("Optional structured Control input."),
71
+ Schema.arraySchema("Optional structured Control input array."),
315
72
  ]),
316
- correlation_id: Schema.stringSchema(
317
- "Optional correlation id for workflow/task linkage.",
318
- ),
319
- from: Schema.stringSchema(
320
- "Optional sender address, such as coordinator or run:<id>.",
321
- ),
322
- metadata: Schema.looseObjectSchema(
323
- "Optional structured metadata for routing or domain hints.",
324
- ),
325
- reply_to: Schema.stringSchema(
326
- "Optional message id this message replies to.",
327
- ),
328
- summary: Schema.stringSchema("Optional short human-facing summary."),
329
- to: Schema.stringSchema(
330
- "Destination actor address, e.g. run:<id> or tool:<name>; advanced: branch:<run>/<branch>, room:<run>, coordinator, session:<id>.",
331
- ),
332
- type: Schema.stringSchema(
333
- "Semantic message type, e.g. control.approve or checkpoint.needs_scope.",
334
- ),
335
- verbose: Schema.booleanSchema(
336
- "Return full JSON instead of compact text.",
337
- ),
73
+ verbose: Schema.booleanSchema("Return full JSON instead of compact text."),
338
74
  },
339
- ["to", "type"],
75
+ ["target", "action"],
340
76
  ),
341
77
  async execute(
342
78
  _toolCallId: string,
@@ -345,239 +81,96 @@ export function createActorMessageToolDefinition<TContext = unknown>(
345
81
  _onUpdate: unknown,
346
82
  ctx: TContext,
347
83
  ) {
348
- const input = asRecord(params);
349
- const message = Messages.normalizeActorMessage(input);
350
- const address = Messages.parseActorAddress(message.to);
84
+ const request = Control.normalizeControlRequest(params);
351
85
  let result: Record<string, unknown>;
352
- if (address.kind === "run" && address.value) {
353
- const status = ToolsAccess.assertRunAccessibleToContext(
354
- address.value,
355
- ctx,
356
- );
357
- const normalizedMailbox = ToolsMailbox.normalizeMailboxContracts(
358
- asRecord(status.mailbox),
359
- );
360
- const acceptedTypes = new Set(
361
- ToolsMailbox.mailboxTypes(normalizedMailbox.accepts),
362
- );
363
- const advisoryWarnings =
364
- acceptedTypes.size > 0 && !acceptedTypes.has(message.type)
365
- ? [
366
- `Message type ${message.type} is not declared in mailbox.accepts for run:${address.value}.`,
367
- ]
368
- : [];
369
- if (message.type === "control.kill") {
370
- result = typeof status.run_instance_id === "string"
371
- ? AsyncRuns.killRun(address.value, {
372
- ...(typeof status.ownerId === "string"
373
- ? { ownerId: status.ownerId }
374
- : {}),
375
- runInstanceId: status.run_instance_id,
376
- })
377
- : { killed: false, reason: "run generation unavailable" };
378
- } else if (message.type === "control.archive") {
379
- result = AsyncRuns.archiveRun(address.value);
380
- } else if (message.type === "control.prune") {
381
- const body = asRecord(message.body);
382
- result = AsyncRuns.pruneRun(address.value, {
383
- preserveArtifacts:
384
- body.preserve_artifacts === true ||
385
- body.preserveArtifacts === true,
386
- });
387
- } else {
388
- result = await AsyncRuns.sendRunMessage(
389
- address.value,
390
- messageBodyToRunLine(message),
86
+ if (request.target === "runtime") {
87
+ if (request.action !== "review.retry" && request.action !== "review.reset") {
88
+ throw new Error(
89
+ `unsupported runtime Control action: ${request.action}; use review.retry or review.reset`,
391
90
  );
392
91
  }
393
- if (advisoryWarnings.length > 0)
394
- result = { ...result, warnings: advisoryWarnings };
395
- } else if (address.kind === "branch" && address.value) {
396
- const runId = address.value;
397
- if (message.from)
398
- assertMessageSenderBelongsToRun(
399
- message,
400
- runId,
401
- `branch:${runId}/<branch>`,
402
- );
403
- const status = ToolsAccess.assertRunAccessibleToContext(runId, ctx);
404
- const stateDir = String(status.state_dir ?? "");
405
- if (stateDir && address.branch) {
406
- const ensureBranchMember = (actorAddress: string) => {
407
- Rooms.ensureRoomMember(
408
- stateDir,
409
- runId,
410
- "main",
411
- actorAddress,
412
- {
413
- parent: `run:${runId}`,
414
- role: "branch",
415
- status: "present",
416
- },
417
- "Branch joined default room",
418
- );
419
- Rooms.writeBranchCommunicationSnapshot(
420
- stateDir,
421
- runId,
422
- actorAddress,
423
- );
424
- };
425
- ensureBranchMember(message.to);
426
- if (message.from) {
427
- const sender = Messages.parseActorAddress(message.from);
428
- if (sender.kind === "branch" && sender.value === runId) {
429
- ensureBranchMember(message.from);
430
- }
431
- }
432
- Rooms.writeCommunicationSnapshot(stateDir, runId);
92
+ if (!deps.handleRuntimeMessage) {
93
+ throw new Error("runtime Control is unavailable");
433
94
  }
434
- result = await routeBranchEnvelope(
435
- stateDir,
436
- runId,
437
- message.to,
438
- message,
439
- {
440
- source: "direct",
441
- },
442
- );
443
- } else if (address.kind === "room" && address.value && address.room) {
444
- const runId = address.value;
445
- assertMessageSenderBelongsToRun(message, runId, `room:${runId}`);
446
- const status = ToolsAccess.assertRunAccessibleToContext(runId, ctx);
447
- const stateDir = String(status.state_dir ?? "");
448
- if (!stateDir)
449
- throw new Error(`${message.to} has no run state directory.`);
450
- const recipients = getRoomMulticastRecipients(message, runId);
451
- const roomResult = Rooms.appendRoomMessage(
452
- stateDir,
453
- address.room,
454
- message,
455
- );
456
- const multicastResults = await Promise.all(
457
- recipients.map(async (recipient) =>
458
- normalizeDeliveryOutcome(
459
- { kind: "branch", branch: recipient.split("/").at(-1), value: runId },
460
- await routeBranchEnvelope(stateDir, runId, recipient, message, {
461
- source: "room-multicast",
462
- }),
463
- ),
464
- ),
465
- );
466
- result = {
467
- ...roomResult,
468
- ...(recipients.length > 0
469
- ? {
470
- multicast: recipients,
471
- multicast_count: recipients.length,
472
- multicast_results: multicastResults,
473
- }
474
- : {}),
475
- };
476
- } else if (address.kind === "tool" && address.value) {
477
- if (address.value === "pi-actors" && deps.handleRuntimeMessage) {
478
- result = deps.handleRuntimeMessage(message.type, message.body);
95
+ result = deps.handleRuntimeMessage(request.action, request.input);
96
+ } else {
97
+ const run = request.target.slice(4);
98
+ const status = ToolsAccess.assertRunAccessibleToContext(run, ctx);
99
+ const runInstanceId =
100
+ typeof status.run_instance_id === "string"
101
+ ? status.run_instance_id
102
+ : undefined;
103
+ if (!runInstanceId) throw new Error(`Run generation unavailable: ${run}`);
104
+ if (request.action === "kill") {
105
+ result = AsyncRuns.killRun(run, {
106
+ ...(typeof status.ownerId === "string"
107
+ ? { ownerId: status.ownerId }
108
+ : {}),
109
+ runInstanceId,
110
+ });
111
+ } else if (request.action === "archive") {
112
+ result = AsyncRuns.archiveRun(run, {
113
+ ...(typeof status.ownerId === "string"
114
+ ? { ownerId: status.ownerId }
115
+ : {}),
116
+ runInstanceId,
117
+ });
118
+ } else if (request.action === "prune") {
119
+ const input = asRecord(request.input);
120
+ result = AsyncRuns.pruneRun(
121
+ run,
122
+ {
123
+ preserveArtifacts:
124
+ input.preserve_artifacts === true ||
125
+ input.preserveArtifacts === true,
126
+ },
127
+ {
128
+ ...(typeof status.ownerId === "string"
129
+ ? { ownerId: status.ownerId }
130
+ : {}),
131
+ runInstanceId,
132
+ },
133
+ );
479
134
  } else {
480
- const tool = deps.getTool?.(address.value);
481
- if (!tool || typeof tool.execute !== "function") {
135
+ const declared = Array.isArray(status.control)
136
+ ? status.control.filter(
137
+ (action): action is string => typeof action === "string",
138
+ )
139
+ : [];
140
+ if (!declared.includes(request.action)) {
482
141
  throw new Error(
483
- `tool actor not found or not executable: ${address.value}`,
142
+ `undeclared Run Control action: ${request.action}; available=${declared.join(",") || "none"}`,
484
143
  );
485
144
  }
486
- const toolParams = messageBodyToToolParams(message);
487
- let toolResult: unknown;
488
- try {
489
- toolResult = await tool.execute(
490
- `message:${message.type}`,
491
- toolParams,
492
- _signal,
493
- _onUpdate,
494
- ctx,
495
- );
496
- } catch (error) {
497
- throw formatToolActorFailure(
498
- address.value,
499
- message,
500
- toolParams,
501
- error,
502
- );
503
- }
504
- result = {
505
- invoked: true,
506
- sent: true,
507
- tool: address.value,
508
- tool_result: toolResult,
509
- };
510
- }
511
- } else if (address.kind === "coordinator" || address.kind === "session") {
512
- if (!message.from) {
513
- throw new Error(`message to ${address.kind} requires from=run:<id>.`);
514
- }
515
- const sender = Messages.parseActorAddress(message.from);
516
- if (sender.kind !== "run" || !sender.value) {
517
- throw new Error(
518
- `message to ${address.kind} currently requires from=run:<id>.`,
145
+ result = await AsyncRuns.sendRunControl(
146
+ run,
147
+ {
148
+ action: request.action,
149
+ ...(request.input !== undefined ? { input: request.input } : {}),
150
+ run_instance_id: runInstanceId,
151
+ },
152
+ {
153
+ ...(typeof status.ownerId === "string"
154
+ ? { ownerId: status.ownerId }
155
+ : {}),
156
+ },
519
157
  );
520
158
  }
521
- const senderStatus = ToolsAccess.assertRunAccessibleToContext(
522
- sender.value,
523
- ctx,
524
- );
525
- if (address.kind === "session") {
526
- if (!senderStatus.ownerId) {
527
- throw ToolsAccess.sessionMismatchError({
528
- currentSession: undefined,
529
- expectedSession: address.value,
530
- run: sender.value,
531
- target: `session:${address.value}`,
532
- });
533
- }
534
- if (senderStatus.ownerId !== address.value) {
535
- throw ToolsAccess.sessionMismatchError({
536
- currentSession: String(senderStatus.ownerId),
537
- expectedSession: address.value,
538
- run: sender.value,
539
- target: `session:${address.value}`,
540
- });
541
- }
542
- }
543
- result = AsyncRuns.appendRunOutboxEvent(sender.value, {
544
- body: message.body,
545
- correlation_id: message.correlation_id,
546
- delivery:
547
- message.metadata?.requires_response === true ||
548
- address.kind === "session"
549
- ? "followup"
550
- : undefined,
551
- event: message.type,
552
- from: message.from,
553
- metadata: message.metadata,
554
- reply_to: message.reply_to,
555
- summary: message.summary,
556
- to: message.to,
557
- type: message.type,
558
- });
559
- } else {
560
- throw new Error(
561
- `message currently supports run:<id>, branch:<run>/<branch>, room:<run>, tool:<name>, coordinator, and session:<id> destinations; unsupported destination: ${message.to}`,
562
- );
159
+ const nextActions = runNextActions(run, result);
160
+ if (nextActions.length > 0) result = { ...result, next_actions: nextActions };
563
161
  }
564
- result = normalizeDeliveryOutcome(address, result);
565
- const nextActions = actorMessageNextActions(message, result);
566
- const resultWithNext = nextActions.length
567
- ? { ...result, next_actions: nextActions }
568
- : result;
162
+ const response = { control: request, result };
569
163
  return {
570
164
  content: [
571
165
  {
572
166
  type: "text" as const,
573
- text: maybeJsonText(
574
- { message, result: resultWithNext },
575
- input.verbose === true,
576
- compactActorMessageResult(message, resultWithNext),
577
- ),
167
+ text:
168
+ request.verbose === true
169
+ ? ToolsResponse.jsonText(response)
170
+ : compactControlResult(request, result),
578
171
  },
579
172
  ],
580
- details: { message, result: resultWithNext },
173
+ details: response,
581
174
  };
582
175
  },
583
176
  };