@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-subagents",
3
- "version": "1.0.2",
3
+ "version": "2.0.1",
4
4
  "description": "Pi extension for delegating work to specialized isolated subagents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,7 +48,7 @@
48
48
  "typescript": "7.0.2"
49
49
  },
50
50
  "dependencies": {
51
- "@narumitw/pi-tui-kit": "^0.49.1",
51
+ "@narumitw/pi-tui-kit": "^0.56.0",
52
52
  "proper-lockfile": "^4.1.2"
53
53
  },
54
54
  "repository": {
@@ -7,58 +7,26 @@ import type { AgentConfig } from "./types.js";
7
7
 
8
8
  export const BUILT_IN_AGENTS: AgentConfig[] = [
9
9
  {
10
- name: "scout",
10
+ name: "explorer",
11
11
  description:
12
- "Read-only codebase reconnaissance; returns concise findings with paths and evidence.",
13
- tools: ["read", "grep", "find", "ls", "bash"],
12
+ "Read-only codebase exploration for specific questions; returns concise findings with paths and evidence.",
13
+ tools: ["read", "grep", "find", "ls"],
14
14
  capabilityManifest: builtInManifest(["repository-search", "code-evidence"], "read", [
15
15
  "evidence-gathering",
16
16
  ]),
17
+ thinkingLevel: "low",
17
18
  source: "built-in",
18
- filePath: "built-in:scout",
19
+ filePath: "built-in:explorer",
19
20
  systemPrompt: [
20
- "You are a scout subagent. Explore the codebase quickly and report grounded findings.",
21
- "Do not edit files. Prefer read, grep, find, ls, and safe bash inspection commands.",
21
+ "You are an explorer subagent. Explore the codebase for specific, well-scoped questions and report grounded findings.",
22
+ "Do not edit files, run shell commands, or use any tool outside the read-only inspection set.",
23
+ "Do not install dependencies, run formatters, start servers, run tests, or execute long-running commands.",
22
24
  "Return concise bullets with exact file paths, symbols, and open questions.",
23
25
  ].join("\n"),
24
26
  },
25
- {
26
- name: "planner",
27
- description: "Turns reconnaissance into a lean implementation or migration plan.",
28
- tools: ["read", "grep", "find", "ls"],
29
- capabilityManifest: builtInManifest(
30
- ["task-decomposition", "implementation-planning", "migration-planning"],
31
- "read",
32
- ),
33
- source: "built-in",
34
- filePath: "built-in:planner",
35
- systemPrompt: [
36
- "You are a planner subagent. Produce executable, verifiable plans only.",
37
- "Do not modify files. Ground the plan in the repository's actual structure.",
38
- "Call out assumptions, risks, sequencing, and verification commands.",
39
- ].join("\n"),
40
- },
41
- {
42
- name: "reviewer",
43
- description: "Independent code review agent that inspects existing verification evidence.",
44
- tools: ["read", "grep", "find", "ls", "bash"],
45
- capabilityManifest: builtInManifest(
46
- ["code-review", "evidence-review", "security-baseline"],
47
- "read",
48
- ["independent-review"],
49
- ),
50
- source: "built-in",
51
- filePath: "built-in:reviewer",
52
- systemPrompt: [
53
- "You are a reviewer subagent. Review changes adversarially and assess claims against the code and existing evidence.",
54
- "Do not edit files or run tests, builds, benchmarks, formatters, or other long-running verification commands.",
55
- "Inspect code, diffs, test definitions, and existing verification evidence. Recommend any additional commands for the main agent to run.",
56
- "Report PASS, FAIL, or PARTIAL with evidence, commands inspected, and specific follow-ups.",
57
- ].join("\n"),
58
- },
59
27
  {
60
28
  name: "worker",
61
- description: "General-purpose implementation worker with the default Pi tool set.",
29
+ description: "Bounded implementation and command-execution worker with clear ownership.",
62
30
  capabilityManifest: builtInManifest(
63
31
  ["implementation", "command-execution", "repository-modification"],
64
32
  "write",
@@ -67,28 +35,6 @@ export const BUILT_IN_AGENTS: AgentConfig[] = [
67
35
  filePath: "built-in:worker",
68
36
  systemPrompt: workerSystemPrompt(),
69
37
  },
70
- {
71
- name: "general",
72
- description: "Alias for worker; kept for model-generated subagent names.",
73
- capabilityManifest: builtInManifest(
74
- ["implementation", "command-execution", "repository-modification"],
75
- "write",
76
- ),
77
- source: "built-in",
78
- filePath: "built-in:general",
79
- systemPrompt: workerSystemPrompt(),
80
- },
81
- {
82
- name: "general-purpose",
83
- description: "Alias for worker; compatible with common subagent naming conventions.",
84
- capabilityManifest: builtInManifest(
85
- ["implementation", "command-execution", "repository-modification"],
86
- "write",
87
- ),
88
- source: "built-in",
89
- filePath: "built-in:general-purpose",
90
- systemPrompt: workerSystemPrompt(),
91
- },
92
38
  ];
93
39
 
94
40
  export function getBuiltInAgent(name: string): AgentConfig | undefined {
@@ -117,8 +63,9 @@ function builtInManifest(
117
63
 
118
64
  function workerSystemPrompt(): string {
119
65
  return [
120
- "You are a focused worker subagent running in an isolated Pi process.",
121
- "Complete the delegated task directly. Keep scope tight and avoid unrelated changes.",
122
- "When done, summarize files changed, commands run, and any remaining risks.",
66
+ "You are a focused worker subagent running in an isolated Pi child context.",
67
+ "Complete only the bounded delegated implementation slice and respect its assigned file or responsibility ownership.",
68
+ "Keep scope tight, avoid unrelated changes, and do not overwrite concurrent work outside your ownership.",
69
+ "The main agent owns integration and final verification; report changed files, commands run, and remaining risks for that handoff.",
123
70
  ].join("\n");
124
71
  }
@@ -8,7 +8,7 @@ import {
8
8
  type AgentDiscoveryResult,
9
9
  discoverAgents,
10
10
  } from "./discovery.js";
11
- import type { AgentConfig, SubagentSettings } from "./types.js";
11
+ import { type AgentConfig, resolveAgentToolNames, type SubagentSettings } from "./types.js";
12
12
 
13
13
  export function formatAgentList(
14
14
  agents: AgentConfig[],
@@ -70,6 +70,21 @@ function normalizeCatalogDescription(description: string, maxLength: number): st
70
70
 
71
71
  type CatalogScope = "user" | "project" | "project-fallback";
72
72
 
73
+ function formatDeclaredItems(items: readonly string[] | undefined): string {
74
+ if (items === undefined) return "undeclared";
75
+ return items.length > 0 ? items.join(", ") : "none";
76
+ }
77
+
78
+ function catalogAgentContractMetadata(agent: AgentConfig): string {
79
+ const manifest = agent.capabilityManifest;
80
+ return [
81
+ `capabilities: ${formatDeclaredItems(manifest?.capabilities)}`,
82
+ `tools: ${formatDeclaredItems(resolveAgentToolNames(agent.tools))}`,
83
+ `filesystem: ${manifest?.authority?.filesystem ?? "undeclared"}`,
84
+ `result formats: ${formatDeclaredItems(manifest?.resultFormats)}`,
85
+ ].join("; ");
86
+ }
87
+
73
88
  function catalogAgentLine(
74
89
  agent: AgentConfig,
75
90
  scope: CatalogScope,
@@ -88,7 +103,7 @@ function catalogAgentLine(
88
103
  ? "; overrides the default user definition for project/both"
89
104
  : "; scope-specific fallback for the default user override"
90
105
  : "";
91
- return `- ${agent.name} [source: ${agent.source}; ${scopeLabel}${collision}] — ${normalizeCatalogDescription(agent.description, maxDescriptionLength)}`;
106
+ return `- ${agent.name} [source: ${agent.source}; ${scopeLabel}${collision}] [${catalogAgentContractMetadata(agent)}] — ${normalizeCatalogDescription(agent.description, maxDescriptionLength)}`;
92
107
  }
93
108
 
94
109
  /**
@@ -153,6 +168,8 @@ export function formatAgentCatalog(
153
168
  const render = (entries: typeof allEntries, omitted: number): string => {
154
169
  const lines = [
155
170
  "Available agent definitions (metadata only; runtime validation and trust remain authoritative).",
171
+ "Use capability and tool identifiers exactly as shown when authoring enforced contracts.",
172
+ "Enforced contract readPaths, writePaths, network, and secrets guarantees are unsupported.",
156
173
  ];
157
174
  const userLines = entries
158
175
  .filter((entry) => entry.scope === "user")
@@ -179,6 +179,24 @@ function hasOwn(obj: object, key: PropertyKey): boolean {
179
179
  return Object.hasOwn(obj, key);
180
180
  }
181
181
 
182
+ function applyAgentOverride(
183
+ agent: AgentConfig,
184
+ override: NonNullable<SubagentSettings["agents"]>[string],
185
+ ): AgentConfig {
186
+ const nextAgent: AgentConfig = { ...agent };
187
+ if (hasOwn(override, "tools")) nextAgent.tools = override.tools;
188
+ if (hasOwn(override, "model")) {
189
+ nextAgent.model = override.model === null ? undefined : override.model;
190
+ }
191
+ if (hasOwn(override, "thinkingLevel")) {
192
+ nextAgent.thinkingLevel = override.thinkingLevel === null ? undefined : override.thinkingLevel;
193
+ }
194
+ if (hasOwn(override, "timeoutMs")) {
195
+ nextAgent.timeoutMs = override.timeoutMs === null ? undefined : override.timeoutMs;
196
+ }
197
+ return nextAgent;
198
+ }
199
+
182
200
  export function discoverAgents(
183
201
  cwd: string,
184
202
  scope: AgentScope,
@@ -217,23 +235,21 @@ export function discoverAgents(
217
235
 
218
236
  // Apply user-configured overrides (from /subagents → Agent tool settings) on top of
219
237
  // the final resolved agent map, regardless of agent source.
220
- for (const [name, override] of Object.entries(config?.agents ?? {})) {
238
+ const configuredAgents = config?.agents ?? {};
239
+ for (const [name, override] of Object.entries(configuredAgents)) {
221
240
  const agent = agentMap.get(name);
222
241
  if (!agent) continue;
223
-
224
- const nextAgent: AgentConfig = { ...agent };
225
- if (hasOwn(override, "tools")) nextAgent.tools = override.tools;
226
- if (hasOwn(override, "model")) {
227
- nextAgent.model = override.model === null ? undefined : override.model;
228
- }
229
- if (hasOwn(override, "thinkingLevel")) {
230
- nextAgent.thinkingLevel =
231
- override.thinkingLevel === null ? undefined : override.thinkingLevel;
232
- }
233
- if (hasOwn(override, "timeoutMs")) {
234
- nextAgent.timeoutMs = override.timeoutMs === null ? undefined : override.timeoutMs;
235
- }
236
- agentMap.set(name, nextAgent);
242
+ agentMap.set(name, applyAgentOverride(agent, override));
243
+ }
244
+ const legacyScoutOverride = configuredAgents.scout;
245
+ const explorerAgent = agentMap.get("explorer");
246
+ if (
247
+ legacyScoutOverride &&
248
+ explorerAgent &&
249
+ !hasOwn(configuredAgents, "explorer") &&
250
+ !agentMap.has("scout")
251
+ ) {
252
+ agentMap.set("explorer", applyAgentOverride(explorerAgent, legacyScoutOverride));
237
253
  }
238
254
 
239
255
  const omittedAgentDefinitions =
@@ -1,6 +1,6 @@
1
1
  import { discoverAgents } from "./agents/discovery.js";
2
2
  import type { SubagentSettings } from "./agents/types.js";
3
- import type { ManagedAgent, TurnOutcome } from "./registry.js";
3
+ import type { AgentMailboxMessage, ManagedAgent, TurnOutcome } from "./registry.js";
4
4
  import { isWriteCapable } from "./stateful-safety.js";
5
5
  import type { SubagentTransport } from "./transport.js";
6
6
  import type {
@@ -48,6 +48,12 @@ export class AutoTransport implements SubagentTransport {
48
48
  };
49
49
  }
50
50
 
51
+ async deliverMessage(agent: ManagedAgent, message: AgentMailboxMessage): Promise<boolean> {
52
+ const selection = this.selections.get(agent.id);
53
+ if (!selection) return false;
54
+ return this.transport(selection.kind).deliverMessage?.(agent, message) ?? false;
55
+ }
56
+
51
57
  async release(agent: ManagedAgent): Promise<void> {
52
58
  const selection = this.selections.get(agent.id);
53
59
  this.selections.delete(agent.id);
@@ -0,0 +1,124 @@
1
+ import net from "node:net";
2
+ import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
3
+ import { type ChildPeerClient, createChildPeerExtension } from "./child-peer-tools.js";
4
+ import type { AgentMailboxMessage } from "./registry.js";
5
+
6
+ const MAX_RESPONSE_BYTES = 64 * 1024;
7
+ const REQUEST_TIMEOUT_MS = 2_000;
8
+
9
+ const captured = captureBridgeEnvironment();
10
+
11
+ const childPeerBridge: ExtensionFactory = captured
12
+ ? createChildPeerExtension(createProcessPeerClient(captured))
13
+ : () => undefined;
14
+
15
+ export default childPeerBridge;
16
+
17
+ interface CapturedBridgeEnvironment {
18
+ host: "127.0.0.1";
19
+ port: number;
20
+ token: string;
21
+ }
22
+
23
+ export function captureBridgeEnvironment(): CapturedBridgeEnvironment | undefined {
24
+ const host = process.env.PI_SUBAGENT_PEER_HOST;
25
+ const rawPort = process.env.PI_SUBAGENT_PEER_PORT;
26
+ const token = process.env.PI_SUBAGENT_PEER_TOKEN;
27
+ delete process.env.PI_SUBAGENT_PEER_HOST;
28
+ delete process.env.PI_SUBAGENT_PEER_PORT;
29
+ delete process.env.PI_SUBAGENT_PEER_TOKEN;
30
+ if (!host && !rawPort && !token) return undefined;
31
+ const port = Number(rawPort);
32
+ if (host !== "127.0.0.1" || !Number.isSafeInteger(port) || port < 1 || port > 65_535 || !token) {
33
+ throw new Error("Invalid pi-subagents peer bridge environment");
34
+ }
35
+ return { host, port, token };
36
+ }
37
+
38
+ function createProcessPeerClient(environment: CapturedBridgeEnvironment): ChildPeerClient {
39
+ const request = async (input: Record<string, unknown>): Promise<Record<string, unknown>> => {
40
+ const response = await requestBridge(environment, { ...input, token: environment.token });
41
+ if (response.ok !== true) {
42
+ throw new Error(
43
+ typeof response.error === "string"
44
+ ? response.error
45
+ : "Subagent peer bridge rejected the request",
46
+ );
47
+ }
48
+ return response;
49
+ };
50
+ return {
51
+ async send(target, message, deduplicationKey) {
52
+ const response = await request({
53
+ action: "send",
54
+ target,
55
+ message,
56
+ ...(deduplicationKey ? { deduplicationKey } : {}),
57
+ });
58
+ if (!isMailboxMessage(response.message)) {
59
+ throw new Error("Subagent peer bridge returned an invalid message receipt");
60
+ }
61
+ return response.message;
62
+ },
63
+ async list() {
64
+ return (await request({ action: "list" })).peers ?? [];
65
+ },
66
+ async acknowledge(messageIds, completionIds) {
67
+ await request({ action: "acknowledge", messageIds, completionIds });
68
+ },
69
+ };
70
+ }
71
+
72
+ function requestBridge(
73
+ environment: CapturedBridgeEnvironment,
74
+ request: Record<string, unknown>,
75
+ ): Promise<Record<string, unknown>> {
76
+ return new Promise((resolve, reject) => {
77
+ const socket = net.createConnection({ host: environment.host, port: environment.port });
78
+ let response = Buffer.alloc(0);
79
+ let settled = false;
80
+ const finish = (error?: Error, value?: Record<string, unknown>) => {
81
+ if (settled) return;
82
+ settled = true;
83
+ clearTimeout(timer);
84
+ socket.destroy();
85
+ if (error) reject(error);
86
+ else resolve(value ?? {});
87
+ };
88
+ const timer = setTimeout(
89
+ () => finish(new Error("Subagent peer bridge request timed out")),
90
+ REQUEST_TIMEOUT_MS,
91
+ );
92
+ timer.unref();
93
+ socket.once("connect", () => socket.end(`${JSON.stringify(request)}\n`));
94
+ socket.on("data", (chunk: Buffer) => {
95
+ response = Buffer.concat([response, chunk]);
96
+ if (response.byteLength > MAX_RESPONSE_BYTES) {
97
+ finish(new Error("Subagent peer bridge response exceeded its size limit"));
98
+ }
99
+ });
100
+ socket.once("error", (error) => finish(error));
101
+ socket.once("close", () => {
102
+ if (settled) return;
103
+ try {
104
+ const parsed = JSON.parse(response.toString("utf8")) as unknown;
105
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
106
+ finish(undefined, parsed as Record<string, unknown>);
107
+ } catch {
108
+ finish(new Error("Subagent peer bridge returned malformed JSON"));
109
+ }
110
+ });
111
+ });
112
+ }
113
+
114
+ function isMailboxMessage(value: unknown): value is AgentMailboxMessage {
115
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
116
+ const message = value as Record<string, unknown>;
117
+ return (
118
+ typeof message.id === "string" &&
119
+ typeof message.senderId === "string" &&
120
+ typeof message.recipientId === "string" &&
121
+ typeof message.content === "string" &&
122
+ typeof message.createdAt === "number"
123
+ );
124
+ }
@@ -0,0 +1,132 @@
1
+ import { defineTool, type ExtensionFactory } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { redactPrivateText } from "./context.js";
4
+ import { MAX_TOOL_MESSAGE_BYTES, truncateUtf8 } from "./limits.js";
5
+ import type { AgentMailboxMessage } from "./registry.js";
6
+
7
+ const MAX_PEER_MESSAGE_BYTES = 16 * 1024;
8
+ const MAX_DEDUPLICATION_KEY_LENGTH = 256;
9
+ const PEER_ENVELOPE_HEADER =
10
+ /^Message Type: SUBAGENT_(?:COMPLETION|PEER_MESSAGE)\nProtocol: pi-subagents:v1\nMessage ID: (msg_[^\s]+)\n(?:Completion ID: (completion:[^\s]+)\n)?Sender ID: [^\n]+\n(?:Sender Path: [^\n]+\n)?Payload:\n/u;
11
+
12
+ export interface ChildPeerClient {
13
+ send(target: string, message: string, deduplicationKey?: string): Promise<AgentMailboxMessage>;
14
+ list(): Promise<unknown>;
15
+ acknowledge(messageIds: readonly string[], completionIds: readonly string[]): Promise<void>;
16
+ }
17
+
18
+ export function createChildPeerExtension(client: ChildPeerClient): ExtensionFactory {
19
+ return (pi) => {
20
+ pi.registerTool(
21
+ defineTool({
22
+ name: "subagent_peer_send",
23
+ label: "Send Peer Message",
24
+ description:
25
+ "Send a bounded queue-only message to /root or another retained agent by canonical task path. The runtime binds your sender identity; this tool never starts an idle turn.",
26
+ promptSnippet: "Send a queue-only message to another retained agent",
27
+ parameters: Type.Object(
28
+ {
29
+ target: Type.String({ minLength: 1, maxLength: 2_048 }),
30
+ message: Type.String({ minLength: 1, maxLength: MAX_PEER_MESSAGE_BYTES }),
31
+ deduplicationKey: Type.Optional(
32
+ Type.String({ minLength: 1, maxLength: MAX_DEDUPLICATION_KEY_LENGTH }),
33
+ ),
34
+ },
35
+ { additionalProperties: false },
36
+ ),
37
+ async execute(_id, params) {
38
+ const message = await client.send(params.target, params.message, params.deduplicationKey);
39
+ return {
40
+ content: [
41
+ {
42
+ type: "text" as const,
43
+ text: `Queued ${message.id} for ${message.recipientId}; delivery does not start an idle turn.`,
44
+ },
45
+ ],
46
+ details: {
47
+ messageId: message.id,
48
+ recipientId: message.recipientId,
49
+ createdAt: message.createdAt,
50
+ },
51
+ };
52
+ },
53
+ }),
54
+ );
55
+ pi.registerTool(
56
+ defineTool({
57
+ name: "subagent_peer_list",
58
+ label: "List Peers",
59
+ description:
60
+ "List bounded identity and lifecycle metadata for /root and retained agents in this session.",
61
+ promptSnippet: "List retained peer task paths",
62
+ parameters: Type.Object({}, { additionalProperties: false }),
63
+ async execute() {
64
+ const peers = await client.list();
65
+ const text = truncateUtf8(JSON.stringify(peers, null, 2), MAX_TOOL_MESSAGE_BYTES).text;
66
+ return { content: [{ type: "text" as const, text }], details: { peers } };
67
+ },
68
+ }),
69
+ );
70
+
71
+ const acknowledgedMessageIds = new Set<string>();
72
+ const acknowledgedCompletionIds = new Set<string>();
73
+ pi.on("context", async (event) => {
74
+ const visible = visibleDeliveryIds(event.messages);
75
+ const messageIds = [...visible.messageIds].filter((id) => !acknowledgedMessageIds.has(id));
76
+ const completionIds = [...visible.completionIds].filter(
77
+ (id) => !acknowledgedCompletionIds.has(id),
78
+ );
79
+ if (messageIds.length === 0 && completionIds.length === 0) return;
80
+ await client.acknowledge(messageIds, completionIds);
81
+ for (const id of messageIds) acknowledgedMessageIds.add(id);
82
+ for (const id of completionIds) acknowledgedCompletionIds.add(id);
83
+ });
84
+ };
85
+ }
86
+
87
+ export function formatPeerMessage(message: AgentMailboxMessage, senderPath?: string): string {
88
+ return truncateUtf8(
89
+ [
90
+ `Message Type: ${message.completionId ? "SUBAGENT_COMPLETION" : "SUBAGENT_PEER_MESSAGE"}`,
91
+ "Protocol: pi-subagents:v1",
92
+ `Message ID: ${message.id}`,
93
+ ...(message.completionId ? [`Completion ID: ${message.completionId}`] : []),
94
+ `Sender ID: ${message.senderId}`,
95
+ ...(senderPath ? [`Sender Path: ${senderPath}`] : []),
96
+ "Payload:",
97
+ redactPrivateText(message.content),
98
+ ].join("\n"),
99
+ MAX_TOOL_MESSAGE_BYTES,
100
+ ).text;
101
+ }
102
+
103
+ export function visibleDeliveryIds(messages: readonly unknown[]): {
104
+ messageIds: Set<string>;
105
+ completionIds: Set<string>;
106
+ } {
107
+ const messageIds = new Set<string>();
108
+ const completionIds = new Set<string>();
109
+ for (const message of messages) {
110
+ for (const text of messageText(message)) {
111
+ const match = PEER_ENVELOPE_HEADER.exec(text);
112
+ if (!match) continue;
113
+ messageIds.add(match[1]);
114
+ if (match[2]) completionIds.add(match[2]);
115
+ }
116
+ }
117
+ return { messageIds, completionIds };
118
+ }
119
+
120
+ function messageText(message: unknown): string[] {
121
+ if (!message || typeof message !== "object" || Array.isArray(message)) return [];
122
+ const candidate = message as Record<string, unknown>;
123
+ if (candidate.role !== "user") return [];
124
+ const content = candidate.content;
125
+ if (typeof content === "string") return [content];
126
+ if (!Array.isArray(content)) return [];
127
+ return content.flatMap((part) => {
128
+ if (!part || typeof part !== "object" || Array.isArray(part)) return [];
129
+ const text = (part as Record<string, unknown>).text;
130
+ return typeof text === "string" ? [text] : [];
131
+ });
132
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { CompletionDelivery } from "./agents/types.js";
3
+ import { SUBAGENT_COMPLETION_MESSAGE_TYPE } from "./completion-render.js";
3
4
  import { redactPrivateText } from "./context.js";
4
5
  import { DEFAULT_MAX_CONTEXT_BYTES, MAX_TOOL_MESSAGE_BYTES, truncateUtf8 } from "./limits.js";
5
6
  import type { AgentTurnCompletion, ManagedAgent } from "./registry.js";
@@ -16,7 +17,10 @@ interface CompletionMetadata {
16
17
  runId: string;
17
18
  generation: number;
18
19
  agentId: string;
20
+ taskPath?: string;
21
+ recipientPath?: string;
19
22
  agent: string;
23
+ task: string;
20
24
  state: string;
21
25
  transport?: string;
22
26
  structuredResult?: ManagedAgent["structuredResult"];
@@ -25,7 +29,7 @@ interface CompletionMetadata {
25
29
  }
26
30
 
27
31
  interface CompletionMessage {
28
- customType: "pi-subagent-completion";
32
+ customType: typeof SUBAGENT_COMPLETION_MESSAGE_TYPE;
29
33
  content: string;
30
34
  display: true;
31
35
  details:
@@ -107,7 +111,11 @@ export class CompletionDeliveryBroker {
107
111
  if (triggerTurn) this.wakeInFlight = true;
108
112
  this.awaitingParentAck.push(...batch);
109
113
  try {
110
- this.pi.sendMessage(message, { deliverAs: "steer", triggerTurn });
114
+ // Pi treats explicit false as immediate insertion instead of queued steering while streaming.
115
+ this.pi.sendMessage(message, {
116
+ deliverAs: "steer",
117
+ ...(triggerTurn ? { triggerTurn: true } : {}),
118
+ });
111
119
  } catch (primaryError) {
112
120
  this.removeAwaiting(batch);
113
121
  if (triggerTurn) this.wakeInFlight = false;
@@ -202,7 +210,8 @@ function completionIdsFromContext(messages: readonly unknown[]): Set<string> {
202
210
  for (const message of messages) {
203
211
  if (!message || typeof message !== "object" || Array.isArray(message)) continue;
204
212
  const record = message as Record<string, unknown>;
205
- if (record.role !== "custom" || record.customType !== "pi-subagent-completion") continue;
213
+ if (record.role !== "custom" || record.customType !== SUBAGENT_COMPLETION_MESSAGE_TYPE)
214
+ continue;
206
215
  const details = record.details;
207
216
  if (!details || typeof details !== "object" || Array.isArray(details)) continue;
208
217
  const metadata = details as Record<string, unknown>;
@@ -236,7 +245,7 @@ function buildCompletionMessage(completions: AgentTurnCompletion[]): CompletionM
236
245
  if (completions.length === 1) {
237
246
  const completion = completions[0];
238
247
  return {
239
- customType: "pi-subagent-completion",
248
+ customType: SUBAGENT_COMPLETION_MESSAGE_TYPE,
240
249
  content: buildDetachedCompletionMessage(completion),
241
250
  display: true,
242
251
  details: completionMetadata(completion),
@@ -256,7 +265,7 @@ function buildCompletionMessage(completions: AgentTurnCompletion[]): CompletionM
256
265
  DEFAULT_MAX_CONTEXT_BYTES,
257
266
  ).text;
258
267
  return {
259
- customType: "pi-subagent-completion",
268
+ customType: SUBAGENT_COMPLETION_MESSAGE_TYPE,
260
269
  content,
261
270
  display: true,
262
271
  details: {
@@ -273,7 +282,10 @@ function completionMetadata(completion: AgentTurnCompletion): CompletionMetadata
273
282
  runId: completion.runId,
274
283
  generation: completion.generation,
275
284
  agentId: completion.agent.id,
285
+ ...(completion.agent.taskPath ? { taskPath: completion.agent.taskPath } : {}),
286
+ ...(completion.recipientPath ? { recipientPath: completion.recipientPath } : {}),
276
287
  agent: completion.agent.agent,
288
+ task: sanitizeCompletionLine(completion.task, 256) || "(unknown task)",
277
289
  state: completion.agent.state,
278
290
  ...(completion.agent.telemetry?.transport
279
291
  ? { transport: completion.agent.telemetry.transport }
@@ -306,6 +318,8 @@ export function buildDetachedCompletionMessage(completion: AgentTurnCompletion):
306
318
  `Run ID: ${completion.runId}`,
307
319
  `Generation: ${completion.generation}`,
308
320
  `Agent ID: ${completion.agent.id}`,
321
+ ...(completion.agent.taskPath ? [`Agent Path: ${completion.agent.taskPath}`] : []),
322
+ ...(completion.recipientPath ? [`Recipient Path: ${completion.recipientPath}`] : []),
309
323
  `Agent: ${agentName}`,
310
324
  `Task: ${task}`,
311
325
  `State: ${completion.agent.state}`,