@narumitw/pi-subagents 1.0.2 → 2.0.1

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 (47) hide show
  1. package/README.md +198 -188
  2. package/package.json +2 -2
  3. package/src/agents/built-ins.ts +13 -66
  4. package/src/agents/catalog.ts +19 -2
  5. package/src/agents/discovery.ts +31 -15
  6. package/src/auto-transport.ts +7 -1
  7. package/src/child-peer-bridge.ts +124 -0
  8. package/src/child-peer-tools.ts +132 -0
  9. package/src/completion-delivery.ts +19 -5
  10. package/src/completion-render.ts +189 -0
  11. package/src/completion-routing.ts +24 -0
  12. package/src/config-ui.ts +11 -17
  13. package/src/consult-registration.ts +3 -2
  14. package/src/create-stateful-transport.ts +15 -2
  15. package/src/execution-ui.ts +0 -72
  16. package/src/in-process-transport.ts +39 -7
  17. package/src/inspect-tool.ts +3 -1
  18. package/src/peer-communication.ts +352 -0
  19. package/src/peer-transport.ts +49 -0
  20. package/src/persistence.ts +26 -1
  21. package/src/pi-args.ts +2 -0
  22. package/src/registry-types.ts +7 -0
  23. package/src/registry.ts +240 -41
  24. package/src/result-contract.ts +20 -5
  25. package/src/rpc-transport.ts +56 -26
  26. package/src/runner.ts +13 -1
  27. package/src/spawn-idempotency.ts +2 -0
  28. package/src/stateful-agent-view.ts +3 -1
  29. package/src/stateful-guidance.ts +11 -11
  30. package/src/stateful-safety.ts +0 -45
  31. package/src/stateful-tool-params.ts +11 -3
  32. package/src/stateful.ts +119 -47
  33. package/src/subagents.ts +6 -8
  34. package/src/subprocess-transport.ts +49 -28
  35. package/src/task-path.ts +65 -0
  36. package/src/transport-ui.ts +0 -6
  37. package/src/transport.ts +2 -1
  38. package/src/workflow-ui.ts +4 -4
  39. package/src/automation-contract.ts +0 -709
  40. package/src/automation-planner.ts +0 -65
  41. package/src/automation-registration.ts +0 -137
  42. package/src/automation-tool.ts +0 -40
  43. package/src/automation.ts +0 -435
  44. package/src/execution-profiles.ts +0 -95
  45. package/src/workflow-plan-compiler.ts +0 -618
  46. package/src/workflow-plan-patch.ts +0 -636
  47. package/src/workflow-planning-benchmark.ts +0 -95
@@ -0,0 +1,189 @@
1
+ import type { MessageRenderer, Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Box, type Component, Spacer, sliceByColumn, visibleWidth } from "@earendil-works/pi-tui";
3
+ import { MAX_TOOL_MESSAGE_BYTES } from "./limits.js";
4
+ import {
5
+ COLLAPSED_LIST_LIMIT,
6
+ expansionHint,
7
+ type RenderStatus,
8
+ recordList,
9
+ recordValue,
10
+ safeBlock,
11
+ safeLine,
12
+ statusBadge,
13
+ } from "./render-common.js";
14
+
15
+ export const SUBAGENT_COMPLETION_MESSAGE_TYPE = "pi-subagent-completion";
16
+
17
+ export const renderCompletionMessage: MessageRenderer = (message, options, theme) => {
18
+ const box = new Box(options.outputPad, 1, (text) => theme.bg("customMessageBg", text));
19
+ if (options.expanded) {
20
+ box.addChild(
21
+ new ExactText(
22
+ theme.fg("customMessageLabel", theme.bold(`[${SUBAGENT_COMPLETION_MESSAGE_TYPE}]`)),
23
+ ),
24
+ );
25
+ box.addChild(new Spacer(1));
26
+ box.addChild(
27
+ new ExactText(
28
+ safeBlock(messageText(message.content), "(no completion content)", MAX_TOOL_MESSAGE_BYTES),
29
+ (text) => theme.fg("customMessageText", text),
30
+ ),
31
+ );
32
+ return box;
33
+ }
34
+
35
+ box.addChild(new ExactText(collapsedCompletion(message.content, message.details, theme)));
36
+ return box;
37
+ };
38
+
39
+ function collapsedCompletion(contentValue: unknown, detailsValue: unknown, theme: Theme): string {
40
+ const content = safeBlock(messageText(contentValue), "", MAX_TOOL_MESSAGE_BYTES);
41
+ const details = recordValue(detailsValue) ?? {};
42
+ const completions = recordList(details.completions);
43
+ if (completions.length > 0 || details.completionCount !== undefined) {
44
+ return collapsedBatch(details, completions, theme);
45
+ }
46
+ return collapsedSingle(content, details, theme);
47
+ }
48
+
49
+ function collapsedSingle(content: string, details: Record<string, unknown>, theme: Theme): string {
50
+ const agent = optionalLine(details.agent) || extractedField(content, "Agent") || "subagent";
51
+ const state = optionalLine(details.state) || extractedField(content, "State") || "completed";
52
+ const task = optionalLine(details.task) || extractedField(content, "Task");
53
+ const payload = payloadPreview(content);
54
+ const lines = [
55
+ `${statusBadge(theme, renderStatus(state))}${theme.fg("muted", " · ")}${theme.fg("customMessageLabel", theme.bold(agent))}`,
56
+ ];
57
+ if (task) lines.push(`${theme.fg("muted", "Task: ")}${theme.fg("customMessageText", task)}`);
58
+ if (payload) {
59
+ lines.push(`${theme.fg("muted", "Payload: ")}${theme.fg("customMessageText", payload)}`);
60
+ }
61
+ lines.push(expansionHint());
62
+ return lines.join("\n");
63
+ }
64
+
65
+ function collapsedBatch(
66
+ details: Record<string, unknown>,
67
+ completions: Record<string, unknown>[],
68
+ theme: Theme,
69
+ ): string {
70
+ const declaredCount =
71
+ typeof details.completionCount === "number" && Number.isFinite(details.completionCount)
72
+ ? Math.max(0, Math.floor(details.completionCount))
73
+ : completions.length;
74
+ const count = Math.max(declaredCount, completions.length);
75
+ const statuses = completions.map((completion) => renderStatus(optionalLine(completion.state)));
76
+ const icon = statuses.includes("failed")
77
+ ? theme.fg("error", "✗")
78
+ : statuses.some((status) => status !== "completed" && status !== "closed")
79
+ ? theme.fg("warning", "◐")
80
+ : theme.fg("success", "✓");
81
+ const lines = [
82
+ `${icon} ${theme.fg("customMessageLabel", theme.bold(`${count} subagent completions`))}`,
83
+ ];
84
+ for (const completion of completions.slice(0, COLLAPSED_LIST_LIMIT)) {
85
+ const agent = optionalLine(completion.agent) || "subagent";
86
+ const state = optionalLine(completion.state) || "completed";
87
+ const task = optionalLine(completion.task);
88
+ lines.push(
89
+ `${theme.fg("muted", "• ")}${theme.fg("accent", agent)} · ${statusBadge(theme, renderStatus(state))}${task ? theme.fg("dim", ` — ${task}`) : ""}`,
90
+ );
91
+ }
92
+ const visibleCount = Math.min(completions.length, COLLAPSED_LIST_LIMIT);
93
+ if (count > visibleCount) {
94
+ lines.push(theme.fg("muted", `… ${count - visibleCount} more`));
95
+ }
96
+ lines.push(expansionHint());
97
+ return lines.join("\n");
98
+ }
99
+
100
+ function messageText(value: unknown): string {
101
+ if (typeof value === "string") return value;
102
+ if (!Array.isArray(value)) return "";
103
+ return value
104
+ .flatMap((part) => {
105
+ const record = recordValue(part);
106
+ return record?.type === "text" && typeof record.text === "string" ? [record.text] : [];
107
+ })
108
+ .join("\n");
109
+ }
110
+
111
+ function extractedField(content: string, label: string): string {
112
+ const prefix = `${label}:`;
113
+ const line = content.split("\n").find((candidate) => candidate.startsWith(prefix));
114
+ return line ? safeLine(line.slice(prefix.length), "", 512) : "";
115
+ }
116
+
117
+ function payloadPreview(content: string): string {
118
+ const marker = "\nPayload:\n";
119
+ const start = content.indexOf(marker);
120
+ if (start < 0) return "";
121
+ for (const line of content.slice(start + marker.length).split("\n")) {
122
+ const preview = safeLine(line, "", 1024);
123
+ if (preview) return preview;
124
+ }
125
+ return "";
126
+ }
127
+
128
+ function optionalLine(value: unknown): string {
129
+ return typeof value === "string" ? safeLine(value, "", 512) : "";
130
+ }
131
+
132
+ function renderStatus(state: string): RenderStatus {
133
+ switch (state) {
134
+ case "failed":
135
+ return "failed";
136
+ case "cancelled":
137
+ return "cancelled";
138
+ case "interrupted":
139
+ return "interrupted";
140
+ case "closed":
141
+ return "closed";
142
+ case "blocked":
143
+ case "needs-input":
144
+ case "abstained":
145
+ case "stale":
146
+ return "warning";
147
+ default:
148
+ return "completed";
149
+ }
150
+ }
151
+
152
+ class ExactText implements Component {
153
+ constructor(
154
+ private readonly value: string,
155
+ private readonly style: (text: string) => string = (text) => text,
156
+ ) {}
157
+
158
+ render(width: number): string[] {
159
+ const safeWidth = Math.max(1, width);
160
+ return this.value
161
+ .split("\n")
162
+ .flatMap((line) => hardWrapExact(line, safeWidth))
163
+ .map(this.style);
164
+ }
165
+
166
+ invalidate(): void {}
167
+ }
168
+
169
+ function hardWrapExact(value: string, width: number): string[] {
170
+ if (value.length === 0) return [""];
171
+ const columns = visibleWidth(value);
172
+ if (columns === 0) return [value];
173
+ const lines: string[] = [];
174
+ let column = 0;
175
+ while (column < columns) {
176
+ const chunk = sliceByColumn(value, column, width, true);
177
+ const chunkWidth = visibleWidth(chunk);
178
+ if (chunkWidth > 0) {
179
+ lines.push(chunk);
180
+ column += chunkWidth;
181
+ continue;
182
+ }
183
+ const oversized = sliceByColumn(value, column, width, false);
184
+ const oversizedWidth = visibleWidth(oversized);
185
+ lines.push("?".repeat(width));
186
+ column += Math.max(1, oversizedWidth);
187
+ }
188
+ return lines;
189
+ }
@@ -0,0 +1,24 @@
1
+ import type { ManagedAgent, PersistedAgentCompletion } from "./registry-types.js";
2
+ import { ROOT_TASK_PATH } from "./task-path.js";
3
+
4
+ export type CompletionRecipient = Pick<PersistedAgentCompletion, "recipientId" | "recipientPath">;
5
+
6
+ export function resolveCompletionRecipient(
7
+ agent: Pick<ManagedAgent, "id" | "parentId">,
8
+ getAgent: (
9
+ id: string,
10
+ ) => Pick<ManagedAgent, "id" | "parentId" | "state" | "taskPath"> | undefined,
11
+ ): CompletionRecipient {
12
+ const visited = new Set([agent.id]);
13
+ let parentId = agent.parentId;
14
+ while (parentId && !visited.has(parentId)) {
15
+ visited.add(parentId);
16
+ const parent = getAgent(parentId);
17
+ if (!parent) break;
18
+ if (parent.state !== "closed") {
19
+ return { recipientId: parent.id, recipientPath: parent.taskPath ?? parent.id };
20
+ }
21
+ parentId = parent.parentId;
22
+ }
23
+ return { recipientId: "root", recipientPath: ROOT_TASK_PATH };
24
+ }
package/src/config-ui.ts CHANGED
@@ -22,12 +22,10 @@ import {
22
22
  applyAgentModel,
23
23
  applyAgentThinking,
24
24
  applyAgentTimeout,
25
- applyExecutionProfileFromUi,
26
25
  executionAgentPickerScreen,
27
26
  executionAgentScreen,
28
27
  executionModelInputScreen,
29
28
  executionModelScreen,
30
- executionProfileScreen,
31
29
  executionThinkingScreen,
32
30
  executionTimeoutInputScreen,
33
31
  resetAgentExecution,
@@ -195,7 +193,6 @@ export async function showSubagentManager(
195
193
  | "performance"
196
194
  | "responsiveness"
197
195
  | "transport"
198
- | "execution-profiles"
199
196
  | "execution-agent-picker"
200
197
  | "execution-agent"
201
198
  | "execution-thinking"
@@ -213,7 +210,6 @@ export async function showSubagentManager(
213
210
  | "set-workflow"
214
211
  | "clear-agents"
215
212
  | "set-transport"
216
- | "apply-execution-profile"
217
213
  | "pick-execution-agent"
218
214
  | "set-agent-thinking"
219
215
  | "set-agent-model"
@@ -246,7 +242,7 @@ export async function showSubagentManager(
246
242
  {
247
243
  id: "workflow",
248
244
  label: "Change delegation",
249
- description: "Choose all methods, async only, or blocking only",
245
+ description: "Choose async only (recommended) or a compatibility workflow",
250
246
  to: "workflow",
251
247
  },
252
248
  {
@@ -280,6 +276,8 @@ export async function showSubagentManager(
280
276
  title: "Change Delegation",
281
277
  lines: [
282
278
  `Current: ${workflowLabel(active)}`,
279
+ "Recommended: Async only keeps the main agent responsive and omits blocking delegation and consultation.",
280
+ "Final-answer-dependent detached work needs automatic resume.",
283
281
  ...(snapshot.value !== active
284
282
  ? [`Configured after reload: ${workflowLabel(snapshot.value)}`]
285
283
  : []),
@@ -294,21 +292,21 @@ export async function showSubagentManager(
294
292
  ? []
295
293
  : [
296
294
  {
297
- id: "all",
298
- label: "All delegation methods",
299
- description: "Allow blocking batches and reusable async agents",
295
+ id: "async-only",
296
+ label: "Async only · Recommended",
297
+ description: "Detached lifecycle plus inspection; omit blocking and consultation",
300
298
  action: "set-workflow" as const,
301
299
  },
302
300
  {
303
- id: "async-only",
304
- label: "Async only",
305
- description: "Keep the root responsive; remove blocking subagent",
301
+ id: "all",
302
+ label: "All delegation methods",
303
+ description: "Compatibility: async, blocking, inspection, and consultation",
306
304
  action: "set-workflow" as const,
307
305
  },
308
306
  {
309
307
  id: "blocking-only",
310
308
  label: "Blocking only",
311
- description: "Keep blocking batches; remove reusable async agents",
309
+ description: "Compatibility: blocking and consultation without async lifecycle",
312
310
  action: "set-workflow" as const,
313
311
  },
314
312
  ],
@@ -378,7 +376,7 @@ export async function showSubagentManager(
378
376
  {
379
377
  id: "performance",
380
378
  label: "Performance and execution",
381
- description: "Transport, responsiveness, profiles, and agent defaults",
379
+ description: "Transport, responsiveness, and agent defaults",
382
380
  to: "performance",
383
381
  },
384
382
  { id: "back", label: "Back", action: "back" },
@@ -397,7 +395,6 @@ export async function showSubagentManager(
397
395
  to: "responsiveness",
398
396
  },
399
397
  { id: "transport", label: "Detached transport", to: "transport" },
400
- { id: "profiles", label: "Execution profiles", to: "execution-profiles" },
401
398
  {
402
399
  id: "agents",
403
400
  label: "Agent execution defaults",
@@ -410,7 +407,6 @@ export async function showSubagentManager(
410
407
  }),
411
408
  responsiveness: () => responsivenessSetupScreen(runtime),
412
409
  transport: () => transportSettingsScreen(runtime),
413
- "execution-profiles": () => executionProfileScreen(),
414
410
  "execution-agent-picker": () => executionAgentPickerScreen(availableAgents),
415
411
  "execution-agent": () => executionAgentScreen(selectedExecutionAgent),
416
412
  "execution-thinking": () => executionThinkingScreen(selectedExecutionAgent),
@@ -568,8 +564,6 @@ export async function showSubagentManager(
568
564
  },
569
565
  "set-transport": async ({ itemId, signal }) =>
570
566
  applyTransportSetting(itemId, ctx, runtime, signal, isCurrent),
571
- "apply-execution-profile": async ({ itemId, signal }) =>
572
- applyExecutionProfileFromUi(itemId, ctx, signal, isCurrent),
573
567
  "pick-execution-agent": async ({ itemId }) => {
574
568
  selectedExecutionAgent = availableAgents.find((agent) => agent.name === itemId);
575
569
  return selectedExecutionAgent
@@ -49,9 +49,10 @@ export function registerSubagentConsult(
49
49
  description: baseDescription(),
50
50
  promptSnippet: "Consult one constrained read-only subagent and wait for its answer",
51
51
  promptGuidelines: [
52
- "Use subagent_consult for bounded reconnaissance, planning, or review whose result is required in the current turn.",
52
+ "Use subagent_consult only for bounded read-only evidence gathering when an independent perspective is worth making the main agent wait.",
53
+ "Keep ordinary planning and review in the main agent with applicable skills and deterministic checks; use subagent_consult only when synchronous read-only isolation adds concrete value.",
53
54
  "Set subagent_consult timeoutMs to the shortest realistic work deadline for the task difficulty; split oversized consultations instead of extending the deadline merely to compensate for broad scope.",
54
- "Implementation-shaped tasks remain read-only and can return only analysis or instructions.",
55
+ "Implementation-shaped subagent_consult tasks remain read-only and can return only analysis or instructions.",
55
56
  ],
56
57
  parameters: SubagentConsultParams,
57
58
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
@@ -2,7 +2,8 @@ import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
2
2
  import type { SubagentSettings, SubagentTransportKind } from "./agents/types.js";
3
3
  import { cachedModuleLoader, throwIfAborted } from "./cached-module-loader.js";
4
4
  import type { ChildSessionFactory, ParentRuntimeSnapshot } from "./in-process-transport.js";
5
- import type { ManagedAgent, TurnOutcome } from "./registry.js";
5
+ import type { PeerTransportRuntime } from "./peer-transport.js";
6
+ import type { AgentMailboxMessage, ManagedAgent, TurnOutcome } from "./registry.js";
6
7
  import type { SubagentTransport } from "./transport.js";
7
8
  import type { TransportProgressCallback } from "./transport-types.js";
8
9
 
@@ -12,6 +13,7 @@ export interface CreateStatefulTransportOptions {
12
13
  getParentRuntime(): ParentRuntimeSnapshot;
13
14
  getSettings(): SubagentSettings | undefined;
14
15
  createInProcessSession?: ChildSessionFactory;
16
+ peerRuntime?: PeerTransportRuntime;
15
17
  loadTransport?: () => Promise<SubagentTransport>;
16
18
  }
17
19
 
@@ -58,6 +60,12 @@ class LazyStatefulTransport implements SubagentTransport {
58
60
  return transport.runTurn(agent, task, signal, onProgress);
59
61
  }
60
62
 
63
+ async deliverMessage(agent: ManagedAgent, message: AgentMailboxMessage): Promise<boolean> {
64
+ const transport = this.loaded;
65
+ if (!transport?.deliverMessage || this.closed) return false;
66
+ return transport.deliverMessage(agent, message);
67
+ }
68
+
61
69
  async release(agent: ManagedAgent): Promise<void> {
62
70
  const transport =
63
71
  this.loaded ?? (this.loading ? await this.loading.catch(() => undefined) : undefined);
@@ -100,7 +108,10 @@ async function loadStatefulTransport(
100
108
  ): Promise<SubagentTransport> {
101
109
  const subprocess = async () => {
102
110
  const { SubprocessTransport } = await import("./subprocess-transport.js");
103
- return new SubprocessTransport({ getSettings: options.getSettings });
111
+ return new SubprocessTransport({
112
+ getSettings: options.getSettings,
113
+ peerRuntime: options.peerRuntime,
114
+ });
104
115
  };
105
116
  const inProcess = async () => {
106
117
  const [{ discoverAgents }, { InProcessTransport }] = await Promise.all([
@@ -109,6 +120,7 @@ async function loadStatefulTransport(
109
120
  ]);
110
121
  return new InProcessTransport({
111
122
  modelRegistry: options.modelRegistry,
123
+ peerRuntime: options.peerRuntime,
112
124
  getParentRuntime: options.getParentRuntime,
113
125
  createSession: options.createInProcessSession,
114
126
  discoverAgent: (agent) =>
@@ -122,6 +134,7 @@ async function loadStatefulTransport(
122
134
  return new RpcTransport({
123
135
  getSettings: options.getSettings,
124
136
  getParentRuntime: options.getParentRuntime,
137
+ peerRuntime: options.peerRuntime,
125
138
  });
126
139
  };
127
140
  switch (options.kind) {
@@ -1,77 +1,9 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { AgentConfig, SubagentThinkingLevel } from "./agents/types.js";
3
- import {
4
- applyExecutionProfile,
5
- EXECUTION_PROFILES,
6
- type ExecutionProfile,
7
- executionProfileDescription,
8
- executionProfileLabel,
9
- executionProfilePreview,
10
- inspectExecutionProfile,
11
- } from "./execution-profiles.js";
12
3
  import { MAX_SUBAGENT_TIMEOUT_MS } from "./limits.js";
13
4
  import { safeTerminalLine as safeTerminalText } from "./safe-text.js";
14
5
  import { readSubagentSettings, updateAgentSettingsPatch } from "./settings.js";
15
6
 
16
- export function executionProfileScreen() {
17
- const current = inspectExecutionProfile();
18
- return {
19
- kind: "actions" as const,
20
- title: "Execution Profiles",
21
- lines: [
22
- `Current built-in mapping: ${current === "custom" ? "Custom or inherited" : executionProfileLabel(current)}`,
23
- "Profiles change only built-in agent thinking defaults.",
24
- "Models, timeouts, tools, transport, context, and explicit tool-call values are preserved.",
25
- ],
26
- items: [
27
- ...EXECUTION_PROFILES.map((profile) => ({
28
- id: profile,
29
- label: executionProfileLabel(profile),
30
- description: executionProfileDescription(profile),
31
- action: "apply-execution-profile" as const,
32
- })),
33
- { id: "back", label: "Back", action: "back" as const },
34
- ],
35
- hint: "back" as const,
36
- };
37
- }
38
-
39
- export async function applyExecutionProfileFromUi(
40
- profileValue: string,
41
- ctx: ExtensionCommandContext,
42
- signal: AbortSignal,
43
- isCurrent: () => boolean,
44
- ) {
45
- if (!EXECUTION_PROFILES.includes(profileValue as ExecutionProfile)) {
46
- return { kind: "rejected" as const };
47
- }
48
- const profile = profileValue as ExecutionProfile;
49
- const before = executionSettingsFingerprint();
50
- const confirmed = await ctx.ui.confirm(
51
- `Apply ${executionProfileLabel(profile)} profile?`,
52
- [
53
- executionProfileDescription(profile),
54
- ...executionProfilePreview(profile),
55
- "Existing model, timeout, and tool overrides remain unchanged.",
56
- ].join("\n"),
57
- { signal },
58
- );
59
- if (signal.aborted || !isCurrent()) return { kind: "close" as const };
60
- if (!confirmed) return { kind: "rejected" as const };
61
- if (before !== executionSettingsFingerprint()) {
62
- ctx.ui.notify("Agent execution settings changed while confirming; review again.", "warning");
63
- return { kind: "rejected" as const };
64
- }
65
- try {
66
- applyExecutionProfile(profile);
67
- ctx.ui.notify(`Applied ${executionProfileLabel(profile)} profile.`, "info");
68
- return { kind: "stay" as const };
69
- } catch (error) {
70
- ctx.ui.notify(`Execution profile was not saved: ${formatError(error)}`, "error");
71
- return { kind: "rejected" as const };
72
- }
73
- }
74
-
75
7
  export function executionAgentPickerScreen(agents: readonly AgentConfig[]) {
76
8
  const configured = readSubagentSettings()?.agents ?? {};
77
9
  return {
@@ -311,10 +243,6 @@ function hasTerminalControl(value: string): boolean {
311
243
  });
312
244
  }
313
245
 
314
- function executionSettingsFingerprint(): string {
315
- return JSON.stringify(readSubagentSettings()?.agents ?? {});
316
- }
317
-
318
246
  function formatError(error: unknown): string {
319
247
  return safeTerminalText(error instanceof Error ? error.message : String(error));
320
248
  }
@@ -9,12 +9,18 @@ import {
9
9
  } from "@earendil-works/pi-coding-agent";
10
10
  import { discoverAgents } from "./agents/discovery.js";
11
11
  import type { AgentConfig, SubagentThinkingLevel } from "./agents/types.js";
12
+ import { formatPeerMessage } from "./child-peer-tools.js";
12
13
  import { redactPrivateText } from "./context.js";
13
14
  import { appendDelegationContract } from "./delegation-contract.js";
14
15
  import { resolveDefaultSubagentTimeoutMs } from "./execution/runtime-policy.js";
15
16
  import { DEFAULT_MAX_CONTEXT_BYTES, DEFAULT_MAX_OUTPUT_BYTES, truncateUtf8 } from "./limits.js";
17
+ import {
18
+ CHILD_PEER_TOOL_NAMES,
19
+ createInProcessPeerExtension,
20
+ type PeerTransportRuntime,
21
+ } from "./peer-transport.js";
16
22
  import { resolvePiPromptResources } from "./prompt-resources.js";
17
- import type { AgentTurn, ManagedAgent, TurnOutcome } from "./registry.js";
23
+ import type { AgentMailboxMessage, AgentTurn, ManagedAgent, TurnOutcome } from "./registry.js";
18
24
  import { appendResultInstruction } from "./result-contract.js";
19
25
  import { safeTerminalLine } from "./safe-text.js";
20
26
  import { readSubagentSettings } from "./settings.js";
@@ -79,6 +85,7 @@ export interface ChildSession {
79
85
  readonly model?: string;
80
86
  readonly thinkingLevel?: SubagentThinkingLevel;
81
87
  prompt(text: string): Promise<void>;
88
+ steer?(text: string): Promise<void>;
82
89
  subscribe(listener: (event: unknown) => void): () => void;
83
90
  abort(): Promise<void>;
84
91
  dispose(): void;
@@ -93,6 +100,7 @@ export interface ChildSessionCreateOptions {
93
100
  modelRegistry: ModelRegistry;
94
101
  parentRuntime: ParentRuntimeSnapshot;
95
102
  tools?: string[];
103
+ peerRuntime?: PeerTransportRuntime;
96
104
  }
97
105
 
98
106
  export type ChildSessionFactory = (options: ChildSessionCreateOptions) => Promise<ChildSession>;
@@ -105,6 +113,7 @@ export interface InProcessTransportOptions {
105
113
  defaultTimeoutMs?: number;
106
114
  abortGraceMs?: number;
107
115
  timeoutFinalizationMs?: number;
116
+ peerRuntime?: PeerTransportRuntime;
108
117
  }
109
118
 
110
119
  interface ChildSessionRecord {
@@ -362,6 +371,13 @@ export class InProcessTransport implements SubagentTransport {
362
371
  }
363
372
  }
364
373
 
374
+ async deliverMessage(agent: ManagedAgent, message: AgentMailboxMessage): Promise<boolean> {
375
+ const record = this.sessions.get(agent.id);
376
+ if (!record || record.disposed || !record.session.steer) return false;
377
+ await record.session.steer(formatPeerMessage(message));
378
+ return true;
379
+ }
380
+
365
381
  async release(agent: ManagedAgent): Promise<void> {
366
382
  await this.releaseById(agent.id);
367
383
  }
@@ -384,6 +400,7 @@ export class InProcessTransport implements SubagentTransport {
384
400
  const record = this.sessions.get(agentId);
385
401
  if (!record) return;
386
402
  this.sessions.delete(agentId);
403
+ this.options.peerRuntime?.revoke(agentId);
387
404
  if (record.disposed) return;
388
405
  record.disposed = true;
389
406
  const failures: unknown[] = [];
@@ -423,6 +440,7 @@ export class InProcessTransport implements SubagentTransport {
423
440
  modelRegistry: this.options.modelRegistry,
424
441
  parentRuntime: this.options.getParentRuntime(),
425
442
  tools,
443
+ peerRuntime: this.options.peerRuntime,
426
444
  });
427
445
  const record: ChildSessionRecord = {
428
446
  session,
@@ -563,6 +581,9 @@ export async function createSdkChildSession(
563
581
  agentDir,
564
582
  options.agentConfig.systemPrompt,
565
583
  projectTrusted,
584
+ options.peerRuntime
585
+ ? createInProcessPeerExtension(options.peerRuntime, options.agent.id)
586
+ : undefined,
566
587
  );
567
588
  copyRegisteredProviders(
568
589
  options.modelRegistry as unknown as RegisteredProviderRegistry,
@@ -577,18 +598,24 @@ export async function createSdkChildSession(
577
598
  const model = resolved.model;
578
599
  const sessionManager = SessionManager.inMemory(options.agent.cwd);
579
600
  seedChildSessionManager(sessionManager, options, model);
601
+ const selectedTools =
602
+ options.tools === undefined
603
+ ? undefined
604
+ : options.peerRuntime
605
+ ? [...options.tools, ...CHILD_PEER_TOOL_NAMES]
606
+ : options.tools;
580
607
  const created = await coreSupport.createAgentSessionFromServices({
581
608
  services,
582
609
  sessionManager,
583
610
  model,
584
611
  thinkingLevel: resolved.thinkingLevel,
585
- tools: options.tools,
586
- noTools: options.tools?.length === 0 ? "all" : undefined,
612
+ tools: selectedTools,
613
+ noTools: selectedTools?.length === 0 ? "all" : undefined,
587
614
  });
588
615
  const session = created.session;
589
- if (options.tools !== undefined) {
616
+ if (selectedTools !== undefined) {
590
617
  const active = session.getActiveToolNames();
591
- const expected = [...options.tools].sort();
618
+ const expected = [...selectedTools].sort();
592
619
  if (
593
620
  active.length !== expected.length ||
594
621
  [...active].sort().some((name, index) => name !== expected[index])
@@ -616,6 +643,7 @@ export async function createSdkChildSession(
616
643
  return session.thinkingLevel;
617
644
  },
618
645
  prompt: (text) => session.prompt(text),
646
+ steer: (text) => session.steer(text),
619
647
  subscribe: (listener) => session.subscribe((event) => listener(event)),
620
648
  abort: () => session.abort(),
621
649
  dispose: () => session.dispose(),
@@ -713,6 +741,7 @@ async function prepareInProcessServices(
713
741
  agentDir: string,
714
742
  agentSystemPrompt: string,
715
743
  projectTrusted: boolean,
744
+ peerExtension?: import("@earendil-works/pi-coding-agent").ExtensionFactory,
716
745
  ): Promise<{ services: AgentSessionServices; support: CoreSessionSupport }> {
717
746
  const promptResources = await resolvePiPromptResources(cwd, projectTrusted, agentDir);
718
747
  const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
@@ -724,6 +753,7 @@ async function prepareInProcessServices(
724
753
  settingsManager,
725
754
  resourceLoaderOptions: {
726
755
  noExtensions: true,
756
+ ...(peerExtension ? { extensionFactories: [peerExtension] } : {}),
727
757
  appendSystemPrompt: [
728
758
  ...promptResources.appendSystemPromptPaths,
729
759
  ...(agentSystemPrompt.trim() ? [agentSystemPrompt] : []),
@@ -787,8 +817,10 @@ export function buildCurrentTurnPrompt(agent: ManagedAgent, task: string): strin
787
817
  const messages = agent.mailbox
788
818
  .filter((message) => ids.has(message.id))
789
819
  .slice(-20)
790
- .map((message) => `From ${message.senderId}: ${redactPrivateText(message.content)}`)
791
- .join("\n");
820
+ .map((message) =>
821
+ formatPeerMessage({ ...message, content: redactPrivateText(message.content) }),
822
+ )
823
+ .join("\n\n");
792
824
  const base = messages
793
825
  ? `${redactPrivateText(task)}\n\nMailbox messages:\n${messages}`
794
826
  : redactPrivateText(task);
@@ -28,7 +28,9 @@ export const SubagentInspectParams = Type.Object(
28
28
  {
29
29
  action: StringEnum(INSPECT_ACTIONS),
30
30
  agent: Type.Optional(Type.String({ minLength: 1 })),
31
- agentId: Type.Optional(Type.String({ minLength: 1 })),
31
+ agentId: Type.Optional(
32
+ Type.String({ minLength: 1, description: "Retained agent ID or canonical task path." }),
33
+ ),
32
34
  workflowId: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
33
35
  agentScope: Type.Optional(AgentScopeSchema),
34
36
  limit: Type.Optional(LimitSchema),