@hheei/pi-subagents 0.2.4 → 0.4.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 (61) hide show
  1. package/README.md +35 -20
  2. package/dist/agent-resolver.d.ts +9 -0
  3. package/dist/agent-resolver.js +278 -51
  4. package/dist/builtin-agents.js +50 -37
  5. package/dist/child-bridge.d.ts +0 -8
  6. package/dist/child-bridge.js +60 -43
  7. package/dist/child-control.d.ts +6 -4
  8. package/dist/child-control.js +251 -47
  9. package/dist/child-process.d.ts +2 -0
  10. package/dist/child-process.js +24 -12
  11. package/dist/commands.d.ts +1 -4
  12. package/dist/commands.js +1 -52
  13. package/dist/domain.d.ts +27 -12
  14. package/dist/domain.js +2 -1
  15. package/dist/extension.d.ts +7 -6
  16. package/dist/extension.js +96 -112
  17. package/dist/host-adapter.d.ts +19 -0
  18. package/dist/host-adapter.js +140 -1
  19. package/dist/info.d.ts +9 -0
  20. package/dist/info.js +80 -0
  21. package/dist/launch-spec.d.ts +0 -1
  22. package/dist/launch-spec.js +38 -18
  23. package/dist/lease.d.ts +83 -0
  24. package/dist/lease.js +239 -0
  25. package/dist/mailbox.d.ts +46 -0
  26. package/dist/mailbox.js +368 -0
  27. package/dist/manager.d.ts +20 -20
  28. package/dist/manager.js +421 -398
  29. package/dist/process-lock.d.ts +11 -0
  30. package/dist/process-lock.js +55 -0
  31. package/dist/protocol.d.ts +2 -2
  32. package/dist/protocol.js +1 -0
  33. package/dist/registry.d.ts +1 -1
  34. package/dist/registry.js +99 -82
  35. package/dist/rpc-events.d.ts +1 -1
  36. package/dist/rpc-events.js +2 -1
  37. package/dist/runtime.d.ts +8 -21
  38. package/dist/runtime.js +61 -83
  39. package/dist/session-bootstrap.d.ts +2 -1
  40. package/dist/session-bootstrap.js +22 -14
  41. package/dist/state-db.d.ts +52 -0
  42. package/dist/state-db.js +209 -0
  43. package/dist/state.d.ts +25 -0
  44. package/dist/state.js +166 -37
  45. package/dist/status.d.ts +14 -0
  46. package/dist/status.js +36 -0
  47. package/dist/tool-activation.d.ts +3 -4
  48. package/dist/tool-activation.js +13 -215
  49. package/dist/tool-presentation.d.ts +14 -0
  50. package/dist/tool-presentation.js +193 -0
  51. package/dist/tools.d.ts +6 -1
  52. package/dist/tools.js +134 -180
  53. package/package.json +6 -6
  54. package/dist/bridge-client.d.ts +0 -55
  55. package/dist/bridge-client.js +0 -453
  56. package/dist/bridge-server.d.ts +0 -72
  57. package/dist/bridge-server.js +0 -490
  58. package/dist/session-fork.d.ts +0 -16
  59. package/dist/session-fork.js +0 -67
  60. package/dist/widget.d.ts +0 -12
  61. package/dist/widget.js +0 -149
@@ -1,7 +1,8 @@
1
1
  import { errorMessage, isRecord } from "@hheei/pi-ext-core";
2
- import { ChildBridgeClient } from "./bridge-client.js";
3
- import { BridgeError } from "./protocol.js";
2
+ import { attachChildMailbox } from "./mailbox.js";
3
+ import { BridgeError, CHILD_INPUT_EVENT } from "./protocol.js";
4
4
  import { FORWARDED_PI_EVENT_TYPES, shouldForwardPiEvent } from "./rpc-events.js";
5
+ import { assistantUpdatePhase, transcriptFacts } from "./state.js";
5
6
  const CHILD_INPUT_SOURCES = ["interactive", "extension", "rpc"];
6
7
  /** The source of a Pi input event, when it is one the parent is allowed to hear about. */
7
8
  export function childInputSource(event) {
@@ -22,47 +23,150 @@ export function promptMessage(payload) {
22
23
  * Answers a parent request with this child's Pi state. Idleness is the child's own answer: the
23
24
  * parent never infers it from how long ago it last heard something.
24
25
  */
25
- export function childState(ctx) {
26
- const sessionPath = ctx.sessionManager.getSessionFile();
26
+ export function childState(ctx, isCompacting = false) {
27
+ const path = ctx.sessionManager.getSessionFile();
27
28
  return {
28
29
  idle: ctx.isIdle(),
29
30
  pendingMessages: ctx.hasPendingMessages(),
31
+ ...(isCompacting ? { compacting: true } : {}),
30
32
  sessionId: ctx.sessionManager.getSessionId(),
31
- ...(sessionPath ? { sessionPath } : {}),
33
+ ...(path ? { path } : {}),
32
34
  };
33
35
  }
34
36
  /**
35
- * Truncates bulky data (like huge file reads, grep blocks, or base64 images) in advisory events
36
- * before they are serialized and pushed over the bridge. The parent projector only requires
37
- * turn-level signals, not multi-megabyte payloads.
37
+ * The dedup key for a streaming update: its phase plus the tool it names. Two deltas with the same
38
+ * key are the same telemetry, so only the first is forwarded while the parent is listening.
39
+ */
40
+ function assistantPhaseKey(event) {
41
+ const phase = assistantUpdatePhase(event.assistantMessageEvent, event.message);
42
+ if (phase === undefined)
43
+ return undefined;
44
+ return phase.toolName === undefined ? phase.kind : `${phase.kind}:${phase.toolName}`;
45
+ }
46
+ /** Text kept from a message part: enough for the parent's summary, never the whole payload. */
47
+ const BRIDGE_MESSAGE_TEXT_LIMIT = 200;
48
+ /** Text kept from a tool result part: the parent shows tool activity, not tool output. */
49
+ const BRIDGE_TOOL_TEXT_LIMIT = 1000;
50
+ /** Content parts kept per message, so a frame stays bounded no matter how long the stream runs. */
51
+ const BRIDGE_MAX_CONTENT_PARTS = 32;
52
+ /**
53
+ * Reduces one content part to what the parent's projector reads. Text is truncated, image data is
54
+ * replaced by a marker, and a tool call keeps its name but not its arguments: the parent projects
55
+ * turn-level activity, so it never needs the payload that produced it.
56
+ */
57
+ function compactContentPart(part, textLimit) {
58
+ if (!isRecord(part))
59
+ return part;
60
+ if (part.type === "text" && typeof part.text === "string") {
61
+ return part.text.length > textLimit
62
+ ? { ...part, text: `${part.text.slice(0, textLimit)}\n... [truncated for bridge event]` }
63
+ : part;
64
+ }
65
+ if (part.type === "image") {
66
+ return {
67
+ type: "image",
68
+ mimeType: part.mimeType,
69
+ data: "[image omitted for bridge event]",
70
+ };
71
+ }
72
+ if (part.type === "toolCall") {
73
+ return {
74
+ type: "toolCall",
75
+ ...(typeof part.id === "string" ? { id: part.id } : {}),
76
+ ...(typeof part.name === "string" ? { name: part.name } : {}),
77
+ };
78
+ }
79
+ return typeof part.type === "string" ? { type: part.type } : {};
80
+ }
81
+ function compactContent(content, textLimit) {
82
+ if (!Array.isArray(content))
83
+ return undefined;
84
+ return content
85
+ .slice(0, BRIDGE_MAX_CONTENT_PARTS)
86
+ .map((part) => compactContentPart(part, textLimit));
87
+ }
88
+ /** Keeps only the turn-level message fields the projector reads, dropping the rest. */
89
+ function compactMessage(message, textLimit) {
90
+ const value = isRecord(message) ? message : undefined;
91
+ if (value === undefined)
92
+ return undefined;
93
+ const content = compactContent(value.content, textLimit);
94
+ return {
95
+ ...(typeof value.role === "string" ? { role: value.role } : {}),
96
+ ...(typeof value.stopReason === "string" ? { stopReason: value.stopReason } : {}),
97
+ ...(typeof value.errorMessage === "string" ? { errorMessage: value.errorMessage } : {}),
98
+ ...(content === undefined ? {} : { content }),
99
+ };
100
+ }
101
+ /**
102
+ * Compacts an advisory event to the telemetry the parent projects before it is serialized and
103
+ * pushed over the bridge. A streaming update carries the whole partial message — text, tool
104
+ * arguments, the accumulating `partial` — which is what the parent neither reads nor needs; only
105
+ * the phase metadata (type, content index) and the tool name survive, so a frame stays far below
106
+ * the protocol limit under any payload size.
38
107
  */
39
108
  export function sanitizeEventForBridge(event) {
40
109
  if (!isRecord(event))
41
110
  return event;
111
+ if (event.type === "message_update") {
112
+ const update = isRecord(event.assistantMessageEvent) ? event.assistantMessageEvent : undefined;
113
+ const message = compactMessage(event.message, BRIDGE_MESSAGE_TEXT_LIMIT);
114
+ return {
115
+ type: "message_update",
116
+ ...(message === undefined ? {} : { message }),
117
+ ...(update === undefined
118
+ ? {}
119
+ : {
120
+ assistantMessageEvent: {
121
+ ...(typeof update.type === "string" ? { type: update.type } : {}),
122
+ ...(typeof update.contentIndex === "number"
123
+ ? { contentIndex: update.contentIndex }
124
+ : {}),
125
+ },
126
+ }),
127
+ };
128
+ }
129
+ if (event.type === "tool_execution_start") {
130
+ return {
131
+ type: "tool_execution_start",
132
+ ...(typeof event.toolName === "string" ? { toolName: event.toolName } : {}),
133
+ ...(typeof event.toolCallId === "string" ? { toolCallId: event.toolCallId } : {}),
134
+ };
135
+ }
42
136
  if (event.type === "tool_execution_end" && isRecord(event.result)) {
43
- const result = event.result;
44
- if (Array.isArray(result.content)) {
45
- const sanitizedContent = result.content.map((part) => {
46
- if (isRecord(part) &&
47
- part.type === "text" &&
48
- typeof part.text === "string" &&
49
- part.text.length > 2000) {
50
- return {
51
- ...part,
52
- text: `${part.text.slice(0, 1000)}\n... [truncated for bridge event]`,
53
- };
54
- }
55
- if (isRecord(part) && part.type === "image") {
56
- return {
57
- type: "image",
58
- mimeType: part.mimeType,
59
- data: "[image omitted for bridge event]",
60
- };
61
- }
62
- return part;
63
- });
64
- return { ...event, result: { ...result, content: sanitizedContent } };
65
- }
137
+ const content = compactContent(event.result.content, BRIDGE_TOOL_TEXT_LIMIT);
138
+ return {
139
+ ...event,
140
+ result: {
141
+ ...(content === undefined ? {} : { content }),
142
+ ...(typeof event.result.isError === "boolean" ? { isError: event.result.isError } : {}),
143
+ },
144
+ };
145
+ }
146
+ if (event.type === "turn_end") {
147
+ const message = compactMessage(event.message, BRIDGE_MESSAGE_TEXT_LIMIT);
148
+ return {
149
+ type: "turn_end",
150
+ ...(typeof event.turnIndex === "number" ? { turnIndex: event.turnIndex } : {}),
151
+ ...(message === undefined ? {} : { message }),
152
+ };
153
+ }
154
+ if (event.type === "agent_end" || event.type === "agent_settled") {
155
+ const messages = Array.isArray(event.messages) ? event.messages : undefined;
156
+ const last = messages?.[messages.length - 1];
157
+ const message = compactMessage(last ?? event.message, BRIDGE_MESSAGE_TEXT_LIMIT);
158
+ return {
159
+ type: event.type,
160
+ ...(event.type === "agent_settled" ? { aborted: event.aborted } : {}),
161
+ ...(message === undefined ? {} : { message, messages: [message] }),
162
+ };
163
+ }
164
+ if (event.type === "compaction_start" || event.type === "compaction_end") {
165
+ return {
166
+ type: event.type,
167
+ ...(typeof event.reason === "string" ? { reason: event.reason } : {}),
168
+ ...(typeof event.aborted === "boolean" ? { aborted: event.aborted } : {}),
169
+ };
66
170
  }
67
171
  return event;
68
172
  }
@@ -73,6 +177,21 @@ export function sanitizeEventForBridge(event) {
73
177
  export function registerChildControl(pi, options) {
74
178
  const isActive = options.isActive ?? (() => true);
75
179
  let ctx;
180
+ let isCompacting = false;
181
+ const pendingCompactionDeliveries = [];
182
+ const flushCompactionQueue = () => {
183
+ if (pendingCompactionDeliveries.length === 0)
184
+ return;
185
+ const queued = pendingCompactionDeliveries.splice(0);
186
+ for (const item of queued) {
187
+ if (item.deliverAs !== undefined) {
188
+ pi.sendUserMessage(item.message, { deliverAs: item.deliverAs });
189
+ }
190
+ else {
191
+ pi.sendUserMessage(item.message);
192
+ }
193
+ }
194
+ };
76
195
  const requireContext = () => {
77
196
  if (ctx === undefined) {
78
197
  throw new BridgeError("session_not_ready", "This child has no active Pi session yet");
@@ -83,12 +202,40 @@ export function registerChildControl(pi, options) {
83
202
  const message = promptMessage(payload);
84
203
  // A message needs a session to land in: while the process is between sessions the parent
85
204
  // hears that instead of a silent drop.
86
- requireContext();
205
+ const path = requireContext().sessionManager.getSessionFile();
206
+ if (path === undefined) {
207
+ throw new BridgeError("session_not_ready", "This child has no session file path");
208
+ }
209
+ if (isCompacting) {
210
+ pendingCompactionDeliveries.push({ message, deliverAs });
211
+ return { accepted: true, path, queuedForCompaction: true };
212
+ }
87
213
  if (deliverAs !== undefined)
88
214
  pi.sendUserMessage(message, { deliverAs });
89
215
  else
90
216
  pi.sendUserMessage(message);
91
- return { accepted: true };
217
+ return { accepted: true, path };
218
+ };
219
+ const currentEntries = () => {
220
+ const sm = requireContext().sessionManager;
221
+ const branch = typeof sm.getBranch === "function" ? sm.getBranch() : undefined;
222
+ return Array.isArray(branch) && branch.length > 0 ? branch : sm.getEntries();
223
+ };
224
+ let cachedFacts;
225
+ const sessionFacts = (expected) => {
226
+ const sm = requireContext().sessionManager;
227
+ const leaf = typeof sm.getLeafId === "function" ? sm.getLeafId() : undefined;
228
+ // Append-only: a new message, compaction, or branch moves the leaf. The same leaf is the
229
+ // same transcript, so turn_end and agent_end do not each copy and walk it.
230
+ if (typeof leaf === "string" && leaf.length > 0) {
231
+ const key = `${leaf}:${expected ?? ""}`;
232
+ if (cachedFacts?.key === key)
233
+ return cachedFacts.facts;
234
+ const facts = transcriptFacts(currentEntries(), expected);
235
+ cachedFacts = { key, facts };
236
+ return facts;
237
+ }
238
+ return transcriptFacts(currentEntries(), expected);
92
239
  };
93
240
  const handleRequest = async (request) => {
94
241
  if (!isActive()) {
@@ -104,9 +251,14 @@ export function registerChildControl(pi, options) {
104
251
  case "follow_up":
105
252
  return deliverPrompt(request.payload, "followUp");
106
253
  case "get_state":
107
- return childState(requireContext());
108
- case "get_entries":
109
- return { entries: requireContext().sessionManager.getEntries() };
254
+ return childState(requireContext(), isCompacting);
255
+ case "get_entries": {
256
+ return { entries: currentEntries() };
257
+ }
258
+ case "session_facts": {
259
+ const expected = isRecord(request.payload) ? request.payload.expectedInput : undefined;
260
+ return sessionFacts(typeof expected === "string" ? expected : undefined);
261
+ }
110
262
  case "abort":
111
263
  requireContext().abort();
112
264
  return { accepted: true };
@@ -117,36 +269,88 @@ export function registerChildControl(pi, options) {
117
269
  throw new BridgeError("unsupported_operation", `This child does not implement ${request.operation}`);
118
270
  }
119
271
  };
120
- const client = new ChildBridgeClient({
121
- endpoint: options.endpoint,
122
- identity: options.identity,
272
+ const dispose = [];
273
+ let currentPhase;
274
+ /** The compact event carrying the current phase, resent when the parent reconnects. */
275
+ const phaseResetEvents = [
276
+ "agent_start",
277
+ "agent_end",
278
+ "turn_start",
279
+ "turn_end",
280
+ "agent_settled",
281
+ "tool_execution_start",
282
+ "tool_execution_end",
283
+ "compaction_start",
284
+ "compaction_end",
285
+ ];
286
+ const client = attachChildMailbox({
287
+ filePath: options.endpoint,
288
+ parentSessionId: options.identity.parentSessionId,
289
+ subagentId: options.identity.subagentId,
123
290
  handleRequest,
124
291
  ...(options.diagnose === undefined ? {} : { diagnose: options.diagnose }),
125
292
  });
126
- const dispose = [];
127
293
  // `pi.on` is declared as literal-name overloads, so a loop over the forwarded event types cannot
128
294
  // be typed through them; every name in FORWARDED_PI_EVENT_TYPES is a real Pi event.
129
295
  const onPiEvent = pi.on;
130
296
  for (const type of FORWARDED_PI_EVENT_TYPES) {
131
297
  dispose.push(onPiEvent(type, (event) => {
132
- if (!isActive())
298
+ if (!isActive() || !client.connected)
133
299
  return;
134
- if (shouldForwardPiEvent(event))
135
- client.sendEvent(sanitizeEventForBridge(event));
300
+ if (!shouldForwardPiEvent(event))
301
+ return;
302
+ const compact = sanitizeEventForBridge(event);
303
+ if (isRecord(event)) {
304
+ if (typeof event.type === "string" && phaseResetEvents.includes(event.type)) {
305
+ currentPhase = undefined;
306
+ }
307
+ else if (event.type === "message_update") {
308
+ const phase = assistantPhaseKey(event);
309
+ if (phase !== undefined) {
310
+ // While the parent is listening, repetitive deltas in the same phase are dropped to
311
+ // prevent socket floods. A disconnected parent suppresses nothing: the phase is
312
+ // resent from `onConnect` instead, so a dropped delta cannot hide it.
313
+ if (phase === currentPhase && client.connected)
314
+ return;
315
+ currentPhase = phase;
316
+ }
317
+ }
318
+ }
319
+ client.sendEvent(compact);
136
320
  }));
137
321
  }
138
322
  dispose.push(pi.on("input", (event) => {
139
- if (!isActive())
323
+ if (!isActive() || !client.connected)
140
324
  return;
141
325
  const source = childInputSource(event);
142
- if (source !== undefined)
143
- client.sendChildInput(source);
326
+ if (source !== undefined) {
327
+ client.sendEvent({
328
+ type: CHILD_INPUT_EVENT,
329
+ parentSessionId: options.identity.parentSessionId,
330
+ childId: options.identity.subagentId,
331
+ runtimeIdentity: options.identity.runtimeIdentity,
332
+ source,
333
+ });
334
+ }
144
335
  }));
145
336
  dispose.push(pi.on("session_start", (_event, context) => {
146
337
  ctx = context;
147
338
  }));
339
+ dispose.push(onPiEvent("compaction_start", () => {
340
+ isCompacting = true;
341
+ }));
342
+ dispose.push(onPiEvent("compaction_end", () => {
343
+ isCompacting = false;
344
+ queueMicrotask(() => {
345
+ if (!isCompacting) {
346
+ flushCompactionQueue();
347
+ }
348
+ });
349
+ }));
148
350
  dispose.push(pi.on("session_shutdown", () => {
149
351
  ctx = undefined;
352
+ isCompacting = false;
353
+ pendingCompactionDeliveries.length = 0;
150
354
  }));
151
355
  let started = false;
152
356
  return {
@@ -158,7 +362,7 @@ export function registerChildControl(pi, options) {
158
362
  started = true;
159
363
  client.start();
160
364
  },
161
- report: (operation, payload, signal) => client.sendReport(operation, payload, signal),
365
+ report: (operation, payload, signal) => client.report(operation, payload, signal),
162
366
  sendEvent: (event) => client.sendEvent(event),
163
367
  dispose: () => {
164
368
  for (const off of dispose) {
@@ -19,3 +19,5 @@ export interface ChildRuntime {
19
19
  * supervisor process, so nothing else can end a background child after its parent is gone.
20
20
  */
21
21
  export declare function spawnChildRuntime(options: ChildProcessOptions): ChildRuntime;
22
+ /** The process-scoped stdin leash for a background child that survived an extension reload. */
23
+ export declare function attachRetainedChild(pid: number): ChildRuntime | undefined;
@@ -18,6 +18,7 @@ function hasExited(child) {
18
18
  * would send it an EOF and end a child that only its parent's exit is supposed to end.
19
19
  */
20
20
  const retained = new Set();
21
+ const retainedRuntimes = new Map();
21
22
  /**
22
23
  * Starts a detached-from-the-terminal child Pi. The caller keeps the handle: this package has no
23
24
  * supervisor process, so nothing else can end a background child after its parent is gone.
@@ -43,22 +44,14 @@ export function spawnChildRuntime(options) {
43
44
  resolve(code);
44
45
  });
45
46
  });
46
- if (child.pid === undefined) {
47
+ const pid = child.pid;
48
+ if (pid === undefined) {
47
49
  child.kill("SIGKILL");
48
50
  throw new Error(`Child process ${options.command} did not start`);
49
51
  }
50
52
  retained.add(child);
51
- // stdout is the child's Pi protocol stream and stderr its diagnostics; neither is a control
52
- // channel, so they are drained to keep the pipes from filling and reported only as diagnostics.
53
- child.stdout.on("data", () => { });
54
- child.stderr.on("data", (chunk) => {
55
- const text = chunk.toString("utf8").trim();
56
- if (text !== "")
57
- diagnose(text);
58
- });
59
- child.stdin.on("error", () => { });
60
- return {
61
- pid: child.pid,
53
+ const runtime = {
54
+ pid,
62
55
  get alive() {
63
56
  return !hasExited(child);
64
57
  },
@@ -76,6 +69,25 @@ export function spawnChildRuntime(options) {
76
69
  }
77
70
  },
78
71
  };
72
+ retainedRuntimes.set(pid, runtime);
73
+ void exited.then(() => {
74
+ retainedRuntimes.delete(pid);
75
+ });
76
+ // stdout is the child's Pi protocol stream and stderr its diagnostics; neither is a control
77
+ // channel, so they are drained to keep the pipes from filling and reported only as diagnostics.
78
+ child.stdout.on("data", () => { });
79
+ child.stderr.on("data", (chunk) => {
80
+ const text = chunk.toString("utf8").trim();
81
+ if (text !== "")
82
+ diagnose(text);
83
+ });
84
+ child.stdin.on("error", () => { });
85
+ return runtime;
86
+ }
87
+ /** The process-scoped stdin leash for a background child that survived an extension reload. */
88
+ export function attachRetainedChild(pid) {
89
+ const runtime = retainedRuntimes.get(pid);
90
+ return runtime?.alive === true ? runtime : undefined;
79
91
  }
80
92
  /** True when `work` settled within the window; the child process outlives this wait either way. */
81
93
  function settled(work, timeoutMs) {
@@ -1,6 +1,3 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { type PublicSubagent } from "./domain.js";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
2
  import type { SubagentManager } from "./manager.js";
4
- export declare function formatStatusLine(children: readonly PublicSubagent[]): string | undefined;
5
3
  export declare function registerParentCommands(pi: ExtensionAPI, manager: SubagentManager): void;
6
- export declare function bindParentStatus(_pi: ExtensionAPI, context: ExtensionContext, manager: SubagentManager, signal?: AbortSignal): () => void;
package/dist/commands.js CHANGED
@@ -1,23 +1,8 @@
1
1
  import { splitSubcommand, subcommandCompletions } from "@hheei/pi-ext-core";
2
2
  import { toVisualSubagentState, VISUAL_SUBAGENT_GLYPH } from "./domain.js";
3
- const STATUS_KEY = "pi-subagents";
4
3
  /** Verbs of the dispatcher below; the completer and the no-argument picker share them. */
5
4
  const SUBAGENT_SUBCOMMANDS = ["list", "inspect", "send", "stop"];
6
5
  const SUBAGENT_COMMAND_USAGE = "Usage: /subagents list|inspect|send|stop [id]";
7
- export function formatStatusLine(children) {
8
- const visible = children.filter((child) => child.state !== "done" && child.state !== "error");
9
- if (visible.length === 0)
10
- return undefined;
11
- return visible
12
- .map((child) => {
13
- const visual = toVisualSubagentState(child.state, child.interrupted);
14
- const glyph = VISUAL_SUBAGENT_GLYPH[visual];
15
- const name = child.displayName ?? child.agent;
16
- const mode = child.presentation === "panel" ? `${visual} panel` : visual;
17
- return `${glyph} ${name} ${mode}`;
18
- })
19
- .join(" · ");
20
- }
21
6
  function notifyResult(ctx, result, ok) {
22
7
  if ("reason" in result && typeof result.reason === "string") {
23
8
  ctx.ui.notify(result.reason, "error");
@@ -61,7 +46,7 @@ export function registerParentCommands(pi, manager) {
61
46
  getArgumentCompletions: subcommandCompletions(SUBAGENT_SUBCOMMANDS),
62
47
  handler: async (args, ctx) => {
63
48
  if (ctx.mode !== "tui") {
64
- ctx.ui.notify("Use spawn_agent / send_agent / get_agent / stop_agent in this mode");
49
+ ctx.ui.notify("Use agent_spawn / agent_send / agent_get / agent_stop in this mode");
65
50
  return;
66
51
  }
67
52
  const { verb, rest: target } = splitSubcommand(args);
@@ -109,39 +94,3 @@ export function registerParentCommands(pi, manager) {
109
94
  },
110
95
  });
111
96
  }
112
- export function bindParentStatus(_pi, context, manager, signal) {
113
- let disposed = false;
114
- const refresh = () => {
115
- if (disposed || signal?.aborted)
116
- return;
117
- void manager
118
- .list()
119
- .then((children) => {
120
- if (disposed || signal?.aborted)
121
- return;
122
- try {
123
- context.ui.setStatus(STATUS_KEY, formatStatusLine(children));
124
- }
125
- catch {
126
- // Context may be stale after session replacement or reload.
127
- }
128
- })
129
- .catch(() => { });
130
- };
131
- const unsubscribe = manager.onChange(refresh);
132
- refresh();
133
- return () => {
134
- if (disposed)
135
- return;
136
- disposed = true;
137
- unsubscribe();
138
- if (signal?.aborted)
139
- return;
140
- try {
141
- context.ui.setStatus(STATUS_KEY, undefined);
142
- }
143
- catch {
144
- // Context may be stale after session replacement or reload.
145
- }
146
- };
147
- }
package/dist/domain.d.ts CHANGED
@@ -67,6 +67,11 @@ export interface Selection {
67
67
  /** Extensions and skills are selected the same way; the aliases keep the two roles named. */
68
68
  export type ExtensionSelection = Selection;
69
69
  export type SkillSelection = Selection;
70
+ export interface McpSelection {
71
+ readonly enabled: boolean;
72
+ readonly servers?: readonly string[];
73
+ readonly excluded?: readonly string[];
74
+ }
70
75
  export interface EffectiveLaunchConfig {
71
76
  /** Logical child id; the same value the child bridge reports back. */
72
77
  readonly subagentId: string;
@@ -77,8 +82,6 @@ export interface EffectiveLaunchConfig {
77
82
  readonly sessionDir: string;
78
83
  /** Known only once the session has been flushed to disk. */
79
84
  readonly sessionPath?: string;
80
- /** Optional path to a prior session JSONL file referenced for on-demand context inspection. */
81
- readonly referencedSessionPath?: string;
82
85
  readonly agent: ResolvedAgentIdentity;
83
86
  readonly model: ResolvedModel;
84
87
  readonly thinking: ResolvedThinking;
@@ -86,10 +89,9 @@ export interface EffectiveLaunchConfig {
86
89
  readonly excludeTools: readonly string[];
87
90
  readonly extensions: ExtensionSelection;
88
91
  readonly skills: SkillSelection;
92
+ readonly mcp: McpSelection;
89
93
  readonly prompt: string;
90
94
  readonly bridgeExtensionPath: string;
91
- /** Frozen at spawn. Interactive children do not auto-wake the parent except via contact_parent. */
92
- readonly interactive: boolean;
93
95
  /** Whether the subagent operates in codemode-only mode. */
94
96
  readonly codemodeOnly?: boolean;
95
97
  /**
@@ -106,7 +108,7 @@ export interface ResolvedAgentPolicy {
106
108
  readonly excludeTools: readonly string[];
107
109
  readonly extensions: ExtensionSelection;
108
110
  readonly skills: SkillSelection;
109
- readonly interactive: boolean;
111
+ readonly mcp: McpSelection;
110
112
  readonly codemodeOnly?: boolean;
111
113
  readonly enabled?: boolean;
112
114
  }
@@ -114,9 +116,24 @@ export interface ResolvedAgentPolicy {
114
116
  * The runtime attachment a child was last launched with. It is evidence, never a liveness proof:
115
117
  * whether that process is still serving this child is answered by the bridge connection.
116
118
  */
119
+ export type PanelHost = "herdr" | "cmux";
120
+ /**
121
+ * The surface that owns a child runtime. A panel id is a host tab or surface. A process id is a
122
+ * background child. Neither one is liveness by itself: the host list or a pid check says whether
123
+ * it is still there.
124
+ */
125
+ export type RuntimeAttachment = {
126
+ readonly kind: "panel";
127
+ readonly host: PanelHost;
128
+ readonly id: string;
129
+ } | {
130
+ readonly kind: "process";
131
+ readonly pid: number;
132
+ };
117
133
  export interface RuntimeMetadata {
118
134
  readonly runtimeIdentity: string;
119
135
  readonly endpoint: string;
136
+ readonly attachment?: RuntimeAttachment;
120
137
  }
121
138
  export interface UsageSummary {
122
139
  readonly inputTokens: number;
@@ -150,18 +167,20 @@ export interface SubagentRecord {
150
167
  }
151
168
  export interface PublicSubagent {
152
169
  readonly id: string;
170
+ /** Pi-assigned session file path; the first write may still be pending. */
171
+ readonly path?: string;
153
172
  readonly agent: string;
154
173
  readonly displayName?: string;
155
174
  readonly state: SubagentState;
156
175
  readonly presentation: Presentation;
157
176
  readonly cwd: string;
158
177
  readonly sessionId: string;
178
+ readonly task?: string;
159
179
  readonly summary?: string;
160
180
  readonly activeTool?: string;
161
181
  readonly usage?: UsageSummary;
162
182
  readonly interrupted?: string;
163
183
  readonly freshness: "live" | "last_known";
164
- readonly interactive: boolean;
165
184
  readonly model: ResolvedModel;
166
185
  readonly thinking: ResolvedThinking;
167
186
  readonly createdAt: string;
@@ -183,21 +202,17 @@ export interface SpawnSubagentInput {
183
202
  * Where to run this child. `auto` (the default) means a panel whenever this parent has a
184
203
  * presentation host, and the background otherwise; the Task tool pins `background` because a
185
204
  * Task is settled by its parent and never handed to a human. This is not a model-facing
186
- * parameter: `spawn_agent` always spawns with `auto`.
205
+ * parameter: `agent_spawn` always spawns with `auto`.
187
206
  */
188
207
  readonly presentation?: Presentation | "auto";
189
208
  /** Optional child session title; the child branch prefixes it with the subagent marker. */
190
209
  readonly title?: string;
191
- /**
192
- * Optional session or agent to fork context from (e.g. 'parent', or a subagent id like 'agent-1').
193
- * Thinking blocks are stripped and context is compacted if pi-ext-memory is active.
194
- */
195
- readonly forkFrom?: string;
196
210
  }
197
211
  /** True when a manager call returned a structured failure instead of a value. */
198
212
  export declare function isOperationError(value: unknown): value is OperationError;
199
213
  /** Child bridge tool name; agent tool policy must never remove it. */
200
214
  export declare const CONTACT_PARENT_TOOL_NAME: "contact_parent";
215
+ export declare const DEFAULT_PI_TOOLS: readonly ["read", "bash", "edit", "write"];
201
216
  /** Environment contract between a parent launch and the child branch of this extension. */
202
217
  export declare const BRIDGE_ENVIRONMENT_KEYS: {
203
218
  readonly parentSessionId: "PI_SUBAGENTS_PARENT_SESSION_ID";
package/dist/domain.js CHANGED
@@ -10,12 +10,12 @@ export function normalizeSubagentState(value) {
10
10
  case "idle":
11
11
  return "running";
12
12
  case "done":
13
+ case "stopped":
13
14
  return "done";
14
15
  case "blocked":
15
16
  return "blocked";
16
17
  case "error":
17
18
  case "failed":
18
- case "stopped":
19
19
  return "error";
20
20
  default:
21
21
  return undefined;
@@ -53,6 +53,7 @@ export function isOperationError(value) {
53
53
  }
54
54
  /** Child bridge tool name; agent tool policy must never remove it. */
55
55
  export const CONTACT_PARENT_TOOL_NAME = "contact_parent";
56
+ export const DEFAULT_PI_TOOLS = ["read", "bash", "edit", "write"];
56
57
  /** Environment contract between a parent launch and the child branch of this extension. */
57
58
  export const BRIDGE_ENVIRONMENT_KEYS = {
58
59
  parentSessionId: "PI_SUBAGENTS_PARENT_SESSION_ID",