@hheei/pi-subagents 0.2.4 → 0.3.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 (43) hide show
  1. package/README.md +32 -17
  2. package/dist/agent-resolver.d.ts +9 -0
  3. package/dist/agent-resolver.js +278 -51
  4. package/dist/bridge-client.d.ts +5 -0
  5. package/dist/bridge-client.js +6 -0
  6. package/dist/builtin-agents.js +50 -37
  7. package/dist/child-bridge.d.ts +0 -8
  8. package/dist/child-bridge.js +60 -43
  9. package/dist/child-control.d.ts +5 -3
  10. package/dist/child-control.js +176 -35
  11. package/dist/commands.d.ts +1 -4
  12. package/dist/commands.js +1 -52
  13. package/dist/domain.d.ts +12 -12
  14. package/dist/domain.js +2 -1
  15. package/dist/extension.d.ts +5 -3
  16. package/dist/extension.js +39 -42
  17. package/dist/host-adapter.js +1 -1
  18. package/dist/info.d.ts +9 -0
  19. package/dist/info.js +54 -0
  20. package/dist/launch-spec.d.ts +0 -1
  21. package/dist/launch-spec.js +38 -18
  22. package/dist/manager.d.ts +4 -2
  23. package/dist/manager.js +207 -58
  24. package/dist/registry.js +19 -5
  25. package/dist/rpc-events.d.ts +1 -1
  26. package/dist/rpc-events.js +0 -1
  27. package/dist/session-bootstrap.d.ts +2 -1
  28. package/dist/session-bootstrap.js +22 -14
  29. package/dist/state.d.ts +13 -0
  30. package/dist/state.js +59 -16
  31. package/dist/status.d.ts +14 -0
  32. package/dist/status.js +36 -0
  33. package/dist/tool-activation.d.ts +3 -4
  34. package/dist/tool-activation.js +13 -215
  35. package/dist/tool-presentation.d.ts +17 -0
  36. package/dist/tool-presentation.js +160 -0
  37. package/dist/tools.d.ts +6 -1
  38. package/dist/tools.js +133 -180
  39. package/package.json +6 -6
  40. package/dist/session-fork.d.ts +0 -16
  41. package/dist/session-fork.js +0 -67
  42. package/dist/widget.d.ts +0 -12
  43. package/dist/widget.js +0 -149
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
  }
@@ -150,18 +152,20 @@ export interface SubagentRecord {
150
152
  }
151
153
  export interface PublicSubagent {
152
154
  readonly id: string;
155
+ /** Pi-assigned session file path; the first write may still be pending. */
156
+ readonly path?: string;
153
157
  readonly agent: string;
154
158
  readonly displayName?: string;
155
159
  readonly state: SubagentState;
156
160
  readonly presentation: Presentation;
157
161
  readonly cwd: string;
158
162
  readonly sessionId: string;
163
+ readonly task?: string;
159
164
  readonly summary?: string;
160
165
  readonly activeTool?: string;
161
166
  readonly usage?: UsageSummary;
162
167
  readonly interrupted?: string;
163
168
  readonly freshness: "live" | "last_known";
164
- readonly interactive: boolean;
165
169
  readonly model: ResolvedModel;
166
170
  readonly thinking: ResolvedThinking;
167
171
  readonly createdAt: string;
@@ -183,21 +187,17 @@ export interface SpawnSubagentInput {
183
187
  * Where to run this child. `auto` (the default) means a panel whenever this parent has a
184
188
  * presentation host, and the background otherwise; the Task tool pins `background` because a
185
189
  * 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`.
190
+ * parameter: `agent_spawn` always spawns with `auto`.
187
191
  */
188
192
  readonly presentation?: Presentation | "auto";
189
193
  /** Optional child session title; the child branch prefixes it with the subagent marker. */
190
194
  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
195
  }
197
196
  /** True when a manager call returned a structured failure instead of a value. */
198
197
  export declare function isOperationError(value: unknown): value is OperationError;
199
198
  /** Child bridge tool name; agent tool policy must never remove it. */
200
199
  export declare const CONTACT_PARENT_TOOL_NAME: "contact_parent";
200
+ export declare const DEFAULT_PI_TOOLS: readonly ["read", "bash", "edit", "write"];
201
201
  /** Environment contract between a parent launch and the child branch of this extension. */
202
202
  export declare const BRIDGE_ENVIRONMENT_KEYS: {
203
203
  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",
@@ -1,9 +1,10 @@
1
- import type { ExtensionAPI, Skill } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionUIContext, Skill } from "@earendil-works/pi-coding-agent";
2
2
  import { type BackgroundDelivery } from "@hheei/pi-ext-core";
3
- import type { SkillCatalogEntry } from "./agent-resolver.js";
3
+ import { type SkillCatalogEntry } from "./agent-resolver.js";
4
4
  import { type ParentChannel } from "./manager.js";
5
5
  /** Parent channel plus the teardown its owner must call. */
6
6
  export interface ParentChannelHandle extends ParentChannel {
7
+ dismiss(childId: string): void;
7
8
  /** Unregisters delivery and appends anything still held without waking the parent. */
8
9
  dispose(): void;
9
10
  }
@@ -14,8 +15,9 @@ export interface ParentChannelHandle extends ParentChannel {
14
15
  * background work finishes; decision requests and blockers bypass that wait. The shared gate
15
16
  * coordinates this channel with Task/Bash results so only one message requests a new idle turn.
16
17
  */
17
- export declare function createParentChannel(pi: Pick<ExtensionAPI, "sendMessage" | "on">, deps: {
18
+ export declare function createParentChannel(pi: Pick<ExtensionAPI, "sendMessage" | "on" | "appendEntry">, deps: {
18
19
  readonly isIdle: () => boolean;
20
+ readonly ui: Pick<ExtensionUIContext, "notify">;
19
21
  readonly delivery: BackgroundDelivery;
20
22
  readonly signal?: AbortSignal;
21
23
  }): ParentChannelHandle;
package/dist/extension.js CHANGED
@@ -1,18 +1,19 @@
1
1
  import { errorMessage, getBackgroundDelivery, registerExtensionLifecycle, TASK_REGISTRY_SERVICE_KEY, waitForService, } from "@hheei/pi-ext-core";
2
+ import { discoverAgents } from "./agent-resolver.js";
2
3
  import { ChildBridgeServer } from "./bridge-server.js";
3
4
  import { registerChildBridge } from "./child-bridge.js";
4
- import { bindParentStatus, registerParentCommands } from "./commands.js";
5
+ import { registerParentCommands } from "./commands.js";
5
6
  import { BRIDGE_ENVIRONMENT_KEYS, isThinkingLevel } from "./domain.js";
6
7
  import { createCmuxHostAdapter, createHerdrHostAdapter, selectHostAdapter, } from "./host-adapter.js";
8
+ import { appendSubagentInfo, displaySubagentText, registerSubagentInfo } from "./info.js";
7
9
  import { SubagentManager } from "./manager.js";
8
10
  import { transformSubagentsMarkdown } from "./markdown.js";
9
11
  import { createSubagentRegistry } from "./registry.js";
10
12
  import { createRuntimeTokenStore, launchChild, openChildPanel, parentBridgeEndpoint, prepareRuntimeDirectory, } from "./runtime.js";
11
13
  import { persistSubagentIntent, resolveSubagentLaunch } from "./session-bootstrap.js";
12
- import { resolveForkSessionReference } from "./session-fork.js";
14
+ import { createSubagentStatus } from "./status.js";
13
15
  import { registerInteractiveToolActivation } from "./tool-activation.js";
14
16
  import { registerParentTools } from "./tools.js";
15
- import { createSubagentWidget } from "./widget.js";
16
17
  const CHILD_ENV_KEYS = Object.values(BRIDGE_ENVIRONMENT_KEYS);
17
18
  /** How long startup waits for a child that survived a parent restart to reconnect its bridge. */
18
19
  const ADOPT_WINDOW_MS = 5_000;
@@ -54,7 +55,9 @@ export function createParentChannel(pi, deps) {
54
55
  const flush = (triggerTurn, urgentOnly = false) => {
55
56
  if (held.length === 0)
56
57
  return;
57
- const reports = urgentOnly ? held.filter((report) => report.reason === "blocked") : [...held];
58
+ const reports = urgentOnly
59
+ ? held.filter((report) => report.reason === "blocked" || report.reason === "error")
60
+ : [...held];
58
61
  if (reports.length === 0)
59
62
  return;
60
63
  for (const report of reports)
@@ -63,7 +66,7 @@ export function createParentChannel(pi, deps) {
63
66
  pi.sendMessage({
64
67
  customType: "subagent-report",
65
68
  content: reports.map(reportText).join("\n\n"),
66
- display: true,
69
+ display: false,
67
70
  details: { reports },
68
71
  }, { triggerTurn, deliverAs: "steer" });
69
72
  }
@@ -71,6 +74,15 @@ export function createParentChannel(pi, deps) {
71
74
  held.unshift(...reports);
72
75
  throw error;
73
76
  }
77
+ for (const report of reports) {
78
+ try {
79
+ appendSubagentInfo(pi, report);
80
+ }
81
+ catch (error) {
82
+ // The model already received this batch; an entry failure must not resend it.
83
+ deps.ui.notify(displaySubagentText(`Could not display subagent report for ${report.childId}: ${errorMessage(error)}`), "error");
84
+ }
85
+ }
74
86
  };
75
87
  const registration = deps.delivery.registerChannel({
76
88
  isIdle: deps.isIdle,
@@ -84,7 +96,12 @@ export function createParentChannel(pi, deps) {
84
96
  if (disposed)
85
97
  throw new Error("Parent report channel is disposed");
86
98
  held.push(report);
87
- registration.request(report.reason === "blocked");
99
+ registration.request(report.reason === "blocked" || report.reason === "error");
100
+ },
101
+ dismiss(childId) {
102
+ const index = held.findIndex((report) => report.childId === childId);
103
+ if (index !== -1)
104
+ held.splice(index, 1);
88
105
  },
89
106
  dispose() {
90
107
  disposed = true;
@@ -113,6 +130,7 @@ export default function piSubagentsExtension(pi) {
113
130
  registerChildBridge(pi, child);
114
131
  return;
115
132
  }
133
+ registerSubagentInfo(pi);
116
134
  registerExtensionLifecycle(pi, {
117
135
  key: "@hheei/pi-subagents",
118
136
  async start(runtime) {
@@ -134,6 +152,7 @@ export default function piSubagentsExtension(pi) {
134
152
  const delivery = getBackgroundDelivery(pi);
135
153
  const channel = createParentChannel(pi, {
136
154
  isIdle: () => context.isIdle(),
155
+ ui: context.ui,
137
156
  delivery,
138
157
  signal: runtime.signal,
139
158
  });
@@ -174,7 +193,8 @@ export default function piSubagentsExtension(pi) {
174
193
  // the only name → path catalog; Pi hands it over before every prompt of this session.
175
194
  let skillCatalog = [];
176
195
  const unsubscribeSkillCatalog = pi.on("before_agent_start", (event) => {
177
- skillCatalog = skillCatalogFromLoaded(event.systemPromptOptions.skills);
196
+ const skills = event.systemPromptOptions?.skills ?? [];
197
+ skillCatalog = skillCatalogFromLoaded(skills);
178
198
  });
179
199
  runtime.resources.add("subagent-skill-catalog", unsubscribeSkillCatalog);
180
200
  manager = new SubagentManager({
@@ -192,27 +212,6 @@ export default function piSubagentsExtension(pi) {
192
212
  }
193
213
  const records = await registry.list().catch(() => []);
194
214
  const existingSubagentIds = records.map((record) => record.subagentId);
195
- let referencedSessionPath;
196
- const forkTarget = input.forkFrom?.trim();
197
- if (forkTarget !== undefined && forkTarget !== "") {
198
- try {
199
- if (forkTarget === "current") {
200
- throw new Error("forkFrom cannot be 'current'. Use 'parent' to reference the parent session or specify a subagent id like 'agent-1'.");
201
- }
202
- const parentSessionFile = context.sessionManager.getSessionFile();
203
- referencedSessionPath = await resolveForkSessionReference({
204
- forkFrom: forkTarget,
205
- ...(parentSessionFile === undefined
206
- ? {}
207
- : { parentSessionPath: parentSessionFile }),
208
- registry,
209
- onWarning: (message) => context.ui.notify(message, "warning"),
210
- });
211
- }
212
- catch (error) {
213
- context.ui.notify(`Referencing session failed: ${errorMessage(error)}; starting fresh session without reference`, "warning");
214
- }
215
- }
216
215
  return resolveSubagentLaunch({
217
216
  input,
218
217
  cwd: context.cwd,
@@ -220,8 +219,6 @@ export default function piSubagentsExtension(pi) {
220
219
  skillCatalog,
221
220
  existingSubagentIds,
222
221
  enforceEnabled: true,
223
- onWarning: (message) => context.ui.notify(message, "warning"),
224
- ...(referencedSessionPath === undefined ? {} : { referencedSessionPath }),
225
222
  parent: {
226
223
  model: { provider: model.provider, id: model.id },
227
224
  thinking,
@@ -295,7 +292,7 @@ export default function piSubagentsExtension(pi) {
295
292
  .map((failure) => `- ${failure.childId}: ${failure.reason}`)
296
293
  .join("\n")}`, "error");
297
294
  }
298
- registerParentTools(pi, manager);
295
+ registerParentTools(pi, manager, await discoverAgents(context.cwd));
299
296
  if (typeof pi.registerMarkdownTransformer === "function") {
300
297
  pi.registerMarkdownTransformer(transformSubagentsMarkdown);
301
298
  }
@@ -316,26 +313,26 @@ export default function piSubagentsExtension(pi) {
316
313
  .catch((error) => {
317
314
  diagnose(`Task registry service unavailable for subagents: ${errorMessage(error)}`);
318
315
  });
319
- const widget = createSubagentWidget(pi, context, runtime.signal);
320
- const refreshWidget = () => {
316
+ const status = createSubagentStatus(context.ui, runtime.signal);
317
+ let refreshVersion = 0;
318
+ const refreshStatus = () => {
321
319
  if (runtime.signal.aborted)
322
320
  return;
321
+ const version = ++refreshVersion;
323
322
  void manager
324
323
  .list()
325
324
  .then((children) => {
326
- if (runtime.signal.aborted)
325
+ if (runtime.signal.aborted || version !== refreshVersion)
327
326
  return;
328
- widget?.refresh(children);
327
+ status.refresh(children);
329
328
  })
330
- .catch(() => { });
329
+ .catch((error) => diagnose(`Subagent status failed: ${errorMessage(error)}`));
331
330
  };
332
- const unsubscribe = manager.onChange(refreshWidget);
333
- const unbindStatus = bindParentStatus(pi, context, manager, runtime.signal);
334
- refreshWidget();
335
- runtime.resources.add("subagent-widget", () => {
331
+ const unsubscribe = manager.onChange(refreshStatus);
332
+ refreshStatus();
333
+ runtime.resources.add("subagent-status", () => {
336
334
  unsubscribe();
337
- unbindStatus();
338
- widget?.dispose();
335
+ status.dispose();
339
336
  });
340
337
  runtime.resources.add("subagent-manager", () => manager.closeLocalConnections());
341
338
  },
@@ -180,7 +180,7 @@ export function createCmuxHostAdapter(options) {
180
180
  noun: "surface",
181
181
  // TODO(cmux-focus): cmux does not report which surface a human is looking at, so
182
182
  // observation cannot answer `focused` and idle reclaim fails open here: a cmux child
183
- // is only ever closed by stop_agent.
183
+ // is only ever closed by agent_stop.
184
184
  reportsFocus: false,
185
185
  observe: async () => {
186
186
  const process = await runner.run("cmux", ["--json", "list-panels"], { timeoutMs });
package/dist/info.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
2
+ import type { ParentChannelReport } from "./manager.js";
3
+ export declare const SUBAGENT_INFO_TYPE = "subagent-info";
4
+ /** Keep line breaks, expand tabs, and remove terminal control characters from child text. */
5
+ export declare function displaySubagentText(text: string): string;
6
+ export declare function renderSubagentInfo(data: unknown, expanded: boolean, theme: Pick<Theme, "fg">): string | undefined;
7
+ /** Native transcript entries survive resume and never enter the model context. */
8
+ export declare function registerSubagentInfo(pi: Pick<ExtensionAPI, "registerEntryRenderer">): void;
9
+ export declare function appendSubagentInfo(pi: Pick<ExtensionAPI, "appendEntry">, report: ParentChannelReport): void;
package/dist/info.js ADDED
@@ -0,0 +1,54 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+ import { Text } from "@earendil-works/pi-tui";
3
+ import { Type } from "typebox";
4
+ import { Value } from "typebox/value";
5
+ import { VISUAL_SUBAGENT_GLYPH, VISUAL_SUBAGENT_TONE } from "./domain.js";
6
+ import { formatSubagentId } from "./status.js";
7
+ export const SUBAGENT_INFO_TYPE = "subagent-info";
8
+ const infoSchema = Type.Object({
9
+ id: Type.String(),
10
+ agent: Type.String(),
11
+ status: Type.Union([
12
+ Type.Literal("running"),
13
+ Type.Literal("done"),
14
+ Type.Literal("blocked"),
15
+ Type.Literal("error"),
16
+ ]),
17
+ message: Type.String(),
18
+ });
19
+ /** Keep line breaks, expand tabs, and remove terminal control characters from child text. */
20
+ export function displaySubagentText(text) {
21
+ return stripVTControlCharacters(text)
22
+ .replaceAll("\t", " ")
23
+ .replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/gu, "");
24
+ }
25
+ export function renderSubagentInfo(data, expanded, theme) {
26
+ if (!Value.Check(infoSchema, data))
27
+ return undefined;
28
+ const id = formatSubagentId(displaySubagentText(data.id).replace(/\s+/gu, " "));
29
+ const agent = displaySubagentText(data.agent).replace(/\s+/gu, " ");
30
+ const heading = theme.fg("dim", "info · agents · ") +
31
+ theme.fg(VISUAL_SUBAGENT_TONE[data.status], VISUAL_SUBAGENT_GLYPH[data.status]) +
32
+ theme.fg("dim", ` ${id} ${agent} · ${data.status}`);
33
+ return expanded ? `${heading}\n${displaySubagentText(data.message)}` : heading;
34
+ }
35
+ /** Native transcript entries survive resume and never enter the model context. */
36
+ export function registerSubagentInfo(pi) {
37
+ pi.registerEntryRenderer(SUBAGENT_INFO_TYPE, (entry, options, theme) => {
38
+ const text = renderSubagentInfo(entry.data, options.expanded, theme);
39
+ return text === undefined ? undefined : new Text(text, 1, 0);
40
+ });
41
+ }
42
+ export function appendSubagentInfo(pi, report) {
43
+ const status = report.reason === "success"
44
+ ? "done"
45
+ : report.reason === "blocked" || report.reason === "error"
46
+ ? report.reason
47
+ : report.status;
48
+ pi.appendEntry(SUBAGENT_INFO_TYPE, {
49
+ id: report.childId,
50
+ agent: report.agent,
51
+ status,
52
+ message: report.message,
53
+ });
54
+ }
@@ -16,7 +16,6 @@ export interface LaunchSpec {
16
16
  readonly env: Readonly<Record<string, string>>;
17
17
  readonly config: EffectiveLaunchConfig;
18
18
  }
19
- export declare const HINDSIGHT_TOOLS: readonly ["hindsight_search_knowledge_pages", "hindsight_list_knowledge_pages", "hindsight_read_knowledge_page", "hindsight_reflect", "hindsight_capture_initiative", "hindsight_ingest_document", "hindsight_sync_status", "hindsight_diagnose"];
20
19
  export declare function stripHindsightContent(text: string): string;
21
20
  export interface BuildLaunchSpecOptions {
22
21
  readonly config: EffectiveLaunchConfig;
@@ -2,17 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { basename, dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { BRIDGE_ENVIRONMENT_KEYS, CHILD_AGENT_ENV_KEY, CHILD_SESSION_ENV_KEY, CHILD_TITLE_ENV_KEY, CONTACT_PARENT_TOOL_NAME, isSessionId, } from "./domain.js";
6
- export const HINDSIGHT_TOOLS = [
7
- "hindsight_search_knowledge_pages",
8
- "hindsight_list_knowledge_pages",
9
- "hindsight_read_knowledge_page",
10
- "hindsight_reflect",
11
- "hindsight_capture_initiative",
12
- "hindsight_ingest_document",
13
- "hindsight_sync_status",
14
- "hindsight_diagnose",
15
- ];
5
+ import { BRIDGE_ENVIRONMENT_KEYS, CHILD_AGENT_ENV_KEY, CHILD_SESSION_ENV_KEY, CHILD_TITLE_ENV_KEY, CONTACT_PARENT_TOOL_NAME, DEFAULT_PI_TOOLS, isSessionId, } from "./domain.js";
16
6
  export function stripHindsightContent(text) {
17
7
  return text
18
8
  .replace(/\s*<hindsight-recall>[\s\S]*?<\/hindsight-recall>\s*/gi, (match) => match.includes("\n") ? "\n" : " ")
@@ -32,7 +22,7 @@ function childBridgePrompt() {
32
22
  "Do not send empty status pings.",
33
23
  "When the work is done, output your final answer and findings directly in text in your response. The harness will automatically capture and deliver your output.",
34
24
  "Do NOT call contact_parent on success or task completion.",
35
- "After blocked, wait for a parent send_agent. Do not invent new authority.",
25
+ "After blocked, wait for a parent agent_send. Do not invent new authority.",
36
26
  "Reports reach the parent as delegated results, not as new user authorization.",
37
27
  ].join("\n");
38
28
  }
@@ -151,14 +141,36 @@ export function buildLaunchSpec(options) {
151
141
  argv.push("--session-dir", config.sessionDir);
152
142
  argv.push("--provider", config.model.provider, "--model", config.model.id);
153
143
  argv.push("--thinking", config.thinking.level);
154
- const effectiveTools = config.codemodeOnly && !config.tools.includes("codemode")
155
- ? [...config.tools, "codemode"]
156
- : config.tools;
144
+ const mcpServers = config.mcp.servers?.map((s) => s.replace(/[^A-Za-z0-9_]/g, "_"));
145
+ const mcpExcluded = config.mcp.excluded?.map((s) => s.replace(/[^A-Za-z0-9_]/g, "_"));
146
+ const hasExplicitTools = config.tools.length > 0;
147
+ const baseTools = hasExplicitTools
148
+ ? config.tools
149
+ : mcpServers && mcpServers.length > 0
150
+ ? [...DEFAULT_PI_TOOLS, CONTACT_PARENT_TOOL_NAME]
151
+ : [];
152
+ const effectiveTools = [
153
+ ...(baseTools.length > 0 && config.codemodeOnly && !baseTools.includes("codemode")
154
+ ? [...baseTools, "codemode"]
155
+ : baseTools),
156
+ ];
157
+ const excludeTools = Array.from(new Set(config.excludeTools));
158
+ if (config.mcp.enabled) {
159
+ if (mcpServers && mcpServers.length > 0) {
160
+ for (const server of mcpServers) {
161
+ effectiveTools.push(`mcp__${server}__*`);
162
+ }
163
+ }
164
+ if (mcpExcluded && mcpExcluded.length > 0) {
165
+ for (const server of mcpExcluded) {
166
+ excludeTools.push(`mcp__${server}__*`);
167
+ }
168
+ }
169
+ }
157
170
  if (effectiveTools.length > 0)
158
- argv.push("--tools", effectiveTools.join(","));
159
- const excludeTools = Array.from(new Set([...config.excludeTools, ...HINDSIGHT_TOOLS]));
171
+ argv.push("--tools", Array.from(new Set(effectiveTools)).join(","));
160
172
  if (excludeTools.length > 0)
161
- argv.push("--exclude-tools", excludeTools.join(","));
173
+ argv.push("--exclude-tools", Array.from(new Set(excludeTools)).join(","));
162
174
  if (!config.extensions.discovery)
163
175
  argv.push("--no-extensions");
164
176
  for (const path of config.extensions.paths)
@@ -167,6 +179,8 @@ export function buildLaunchSpec(options) {
167
179
  argv.push("--no-skills");
168
180
  for (const path of config.skills.paths)
169
181
  argv.push("--skill", path);
182
+ if (!config.mcp.enabled)
183
+ argv.push("--no-mcp");
170
184
  argv.push("--append-system-prompt", config.prompt);
171
185
  const env = {
172
186
  [BRIDGE_ENVIRONMENT_KEYS.parentSessionId]: options.bridge.parentSessionId,
@@ -179,6 +193,12 @@ export function buildLaunchSpec(options) {
179
193
  [CHILD_TITLE_ENV_KEY]: config.title ?? "",
180
194
  PI_HINDSIGHT_DISABLE: "1",
181
195
  ...(config.codemodeOnly ? { PI_CODEMODE_MODE: "only" } : {}),
196
+ ...(mcpExcluded && mcpExcluded.length > 0
197
+ ? { PI_SUBAGENT_MCP_EXCLUDE: mcpExcluded.join(",") }
198
+ : {}),
199
+ ...(mcpServers && mcpServers.length > 0
200
+ ? { PI_SUBAGENT_MCP_SERVERS: mcpServers.join(",") }
201
+ : {}),
182
202
  };
183
203
  return Object.freeze({
184
204
  command: options.invocation.command,
package/dist/manager.d.ts CHANGED
@@ -7,7 +7,7 @@ import type { SubagentRegistry } from "./registry.js";
7
7
  import type { LaunchOutcome, RuntimeTokenStore } from "./runtime.js";
8
8
  /**
9
9
  * How long an idle child keeps its runtime. A background child holds a whole Pi process and its
10
- * session lock, so an idle one is reclaimed after this window and a later `send_agent` starts it
10
+ * session lock, so an idle one is reclaimed after this window and a later `agent_send` starts it
11
11
  * again from the same session.
12
12
  */
13
13
  export declare const SUBAGENT_IDLE_TIMEOUT_MS = 15000;
@@ -25,6 +25,7 @@ export interface ParentChannelReport {
25
25
  }
26
26
  export interface ParentChannel {
27
27
  deliver(report: ParentChannelReport): Promise<void>;
28
+ dismiss?(childId: string): void;
28
29
  }
29
30
  /**
30
31
  * The parent side of the bridge, as the manager needs it: one connection per child, requests the
@@ -82,6 +83,7 @@ export interface PresentationDeps {
82
83
  }
83
84
  export interface SpawnResult {
84
85
  readonly child: PublicSubagent;
86
+ readonly path: string;
85
87
  /** Set when the child did not run where it would have by default. */
86
88
  readonly note?: string;
87
89
  }
@@ -106,7 +108,7 @@ export declare class SubagentManager {
106
108
  * `task_result` and settle events without also owning the bridge connection.
107
109
  */
108
110
  onChildEvent(listener: (childId: string, event: unknown) => void): () => void;
109
- /** Presentation-only: widget and similar projections re-read list() after this. */
111
+ /** Presentation-only: status and similar projections re-read list() after this. */
110
112
  onChange(listener: () => void): () => void;
111
113
  dispose(): void;
112
114
  /**