@hheei/pi-subagents 0.2.3 → 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 (44) 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 +42 -44
  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 +299 -114
  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/runtime.js +8 -0
  28. package/dist/session-bootstrap.d.ts +2 -1
  29. package/dist/session-bootstrap.js +22 -14
  30. package/dist/state.d.ts +13 -0
  31. package/dist/state.js +59 -16
  32. package/dist/status.d.ts +14 -0
  33. package/dist/status.js +36 -0
  34. package/dist/tool-activation.d.ts +3 -4
  35. package/dist/tool-activation.js +13 -215
  36. package/dist/tool-presentation.d.ts +17 -0
  37. package/dist/tool-presentation.js +160 -0
  38. package/dist/tools.d.ts +6 -1
  39. package/dist/tools.js +133 -180
  40. package/package.json +6 -6
  41. package/dist/session-fork.d.ts +0 -16
  42. package/dist/session-fork.js +0 -67
  43. package/dist/widget.d.ts +0 -12
  44. 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,21 +1,23 @@
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;
20
+ const START_WINDOW_MS = 20_000;
19
21
  function diagnose(message) {
20
22
  if (process.env.DEBUG || process.env.PI_SUBAGENTS_DEBUG) {
21
23
  console.error(`pi-subagents: ${message}`);
@@ -53,7 +55,9 @@ export function createParentChannel(pi, deps) {
53
55
  const flush = (triggerTurn, urgentOnly = false) => {
54
56
  if (held.length === 0)
55
57
  return;
56
- 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];
57
61
  if (reports.length === 0)
58
62
  return;
59
63
  for (const report of reports)
@@ -62,7 +66,7 @@ export function createParentChannel(pi, deps) {
62
66
  pi.sendMessage({
63
67
  customType: "subagent-report",
64
68
  content: reports.map(reportText).join("\n\n"),
65
- display: true,
69
+ display: false,
66
70
  details: { reports },
67
71
  }, { triggerTurn, deliverAs: "steer" });
68
72
  }
@@ -70,6 +74,15 @@ export function createParentChannel(pi, deps) {
70
74
  held.unshift(...reports);
71
75
  throw error;
72
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
+ }
73
86
  };
74
87
  const registration = deps.delivery.registerChannel({
75
88
  isIdle: deps.isIdle,
@@ -83,7 +96,12 @@ export function createParentChannel(pi, deps) {
83
96
  if (disposed)
84
97
  throw new Error("Parent report channel is disposed");
85
98
  held.push(report);
86
- 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);
87
105
  },
88
106
  dispose() {
89
107
  disposed = true;
@@ -112,6 +130,7 @@ export default function piSubagentsExtension(pi) {
112
130
  registerChildBridge(pi, child);
113
131
  return;
114
132
  }
133
+ registerSubagentInfo(pi);
115
134
  registerExtensionLifecycle(pi, {
116
135
  key: "@hheei/pi-subagents",
117
136
  async start(runtime) {
@@ -133,6 +152,7 @@ export default function piSubagentsExtension(pi) {
133
152
  const delivery = getBackgroundDelivery(pi);
134
153
  const channel = createParentChannel(pi, {
135
154
  isIdle: () => context.isIdle(),
155
+ ui: context.ui,
136
156
  delivery,
137
157
  signal: runtime.signal,
138
158
  });
@@ -173,7 +193,8 @@ export default function piSubagentsExtension(pi) {
173
193
  // the only name → path catalog; Pi hands it over before every prompt of this session.
174
194
  let skillCatalog = [];
175
195
  const unsubscribeSkillCatalog = pi.on("before_agent_start", (event) => {
176
- skillCatalog = skillCatalogFromLoaded(event.systemPromptOptions.skills);
196
+ const skills = event.systemPromptOptions?.skills ?? [];
197
+ skillCatalog = skillCatalogFromLoaded(skills);
177
198
  });
178
199
  runtime.resources.add("subagent-skill-catalog", unsubscribeSkillCatalog);
179
200
  manager = new SubagentManager({
@@ -191,27 +212,6 @@ export default function piSubagentsExtension(pi) {
191
212
  }
192
213
  const records = await registry.list().catch(() => []);
193
214
  const existingSubagentIds = records.map((record) => record.subagentId);
194
- let referencedSessionPath;
195
- const forkTarget = input.forkFrom?.trim();
196
- if (forkTarget !== undefined && forkTarget !== "") {
197
- try {
198
- if (forkTarget === "current") {
199
- throw new Error("forkFrom cannot be 'current'. Use 'parent' to reference the parent session or specify a subagent id like 'agent-1'.");
200
- }
201
- const parentSessionFile = context.sessionManager.getSessionFile();
202
- referencedSessionPath = await resolveForkSessionReference({
203
- forkFrom: forkTarget,
204
- ...(parentSessionFile === undefined
205
- ? {}
206
- : { parentSessionPath: parentSessionFile }),
207
- registry,
208
- onWarning: (message) => context.ui.notify(message, "warning"),
209
- });
210
- }
211
- catch (error) {
212
- context.ui.notify(`Referencing session failed: ${errorMessage(error)}; starting fresh session without reference`, "warning");
213
- }
214
- }
215
215
  return resolveSubagentLaunch({
216
216
  input,
217
217
  cwd: context.cwd,
@@ -219,8 +219,6 @@ export default function piSubagentsExtension(pi) {
219
219
  skillCatalog,
220
220
  existingSubagentIds,
221
221
  enforceEnabled: true,
222
- onWarning: (message) => context.ui.notify(message, "warning"),
223
- ...(referencedSessionPath === undefined ? {} : { referencedSessionPath }),
224
222
  parent: {
225
223
  model: { provider: model.provider, id: model.id },
226
224
  thinking,
@@ -237,7 +235,7 @@ export default function piSubagentsExtension(pi) {
237
235
  record,
238
236
  tokens,
239
237
  waitForBridge: (childId, signal) => server.waitForConnection(childId, {
240
- timeoutMs: ADOPT_WINDOW_MS,
238
+ timeoutMs: START_WINDOW_MS,
241
239
  ...(signal === undefined ? {} : { signal }),
242
240
  }),
243
241
  signal: runtime.signal,
@@ -254,7 +252,7 @@ export default function piSubagentsExtension(pi) {
254
252
  tokens,
255
253
  host: hostSelection.adapter,
256
254
  waitForBridge: (childId, signal) => server.waitForConnection(childId, {
257
- timeoutMs: ADOPT_WINDOW_MS,
255
+ timeoutMs: START_WINDOW_MS,
258
256
  ...(signal === undefined ? {} : { signal }),
259
257
  }),
260
258
  signal: runtime.signal,
@@ -294,7 +292,7 @@ export default function piSubagentsExtension(pi) {
294
292
  .map((failure) => `- ${failure.childId}: ${failure.reason}`)
295
293
  .join("\n")}`, "error");
296
294
  }
297
- registerParentTools(pi, manager);
295
+ registerParentTools(pi, manager, await discoverAgents(context.cwd));
298
296
  if (typeof pi.registerMarkdownTransformer === "function") {
299
297
  pi.registerMarkdownTransformer(transformSubagentsMarkdown);
300
298
  }
@@ -315,26 +313,26 @@ export default function piSubagentsExtension(pi) {
315
313
  .catch((error) => {
316
314
  diagnose(`Task registry service unavailable for subagents: ${errorMessage(error)}`);
317
315
  });
318
- const widget = createSubagentWidget(pi, context, runtime.signal);
319
- const refreshWidget = () => {
316
+ const status = createSubagentStatus(context.ui, runtime.signal);
317
+ let refreshVersion = 0;
318
+ const refreshStatus = () => {
320
319
  if (runtime.signal.aborted)
321
320
  return;
321
+ const version = ++refreshVersion;
322
322
  void manager
323
323
  .list()
324
324
  .then((children) => {
325
- if (runtime.signal.aborted)
325
+ if (runtime.signal.aborted || version !== refreshVersion)
326
326
  return;
327
- widget?.refresh(children);
327
+ status.refresh(children);
328
328
  })
329
- .catch(() => { });
329
+ .catch((error) => diagnose(`Subagent status failed: ${errorMessage(error)}`));
330
330
  };
331
- const unsubscribe = manager.onChange(refreshWidget);
332
- const unbindStatus = bindParentStatus(pi, context, manager, runtime.signal);
333
- refreshWidget();
334
- runtime.resources.add("subagent-widget", () => {
331
+ const unsubscribe = manager.onChange(refreshStatus);
332
+ refreshStatus();
333
+ runtime.resources.add("subagent-status", () => {
335
334
  unsubscribe();
336
- unbindStatus();
337
- widget?.dispose();
335
+ status.dispose();
338
336
  });
339
337
  runtime.resources.add("subagent-manager", () => manager.closeLocalConnections());
340
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
  /**