@clanker-code/pi-subagents 0.10.8 → 0.11.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.
package/AGENTS.md CHANGED
@@ -31,6 +31,8 @@ See the general guide at `~/.llm-general/npm-autopublish-via-ci.md` for other re
31
31
 
32
32
  ## Keeping Upstream In Sync
33
33
 
34
+ **Do NOT pull in upstream changes without consulting Max first.** You may preview the changes and whether there will be any conflicts, and if so estimate how complex it will be to fix — but do not merge, rebase, or cherry-pick without explicit approval.
35
+
34
36
  Periodically run:
35
37
 
36
38
  ```bash
package/CHANGELOG.md CHANGED
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.11.1] - 2026-06-28
11
+
12
+ ### Fixed
13
+ - **Depth 2+ subagents now appear in the TUI widget** — each child session's `DefaultResourceLoader` previously created its own isolated event bus, so lifecycle events (`subagents:created`, `subagents:started`, `subagents:completed`, `subagents:failed`) from depth 2+ agents never reached the parent's widget listener. A forwarding event bus now wraps the parent bus: the child gets its own isolated local bus, but lifecycle events are forwarded to the parent so the widget renders the full recursive agent tree.
14
+ - **Agent tool description shows next spawn depth instead of agent's own depth** — the `{{currentDepth}}` placeholder and recursive guideline in the Agent tool description now show `extensionDepth + 1` (the depth the *next* spawned agent would be at) instead of `extensionDepth` (the agent's own depth), eliminating the off-by-one confusion where a depth-1 agent displayed "1/4" but spawned agents at depth 2.
15
+ - **Event bus propagation covers all spawn paths** — RPC-spawned agents (`cross-extension-rpc.ts`), scheduled agents (`schedule.ts`), and `spawnAndWait` foreground agents now correctly receive the parent's event bus and recursive depth metadata, so lifecycle events from agents spawned via any path are visible in the parent widget.
16
+ - **Dashboard UI action handlers** — steer, abort, and view-result row actions in the subagents management modal now route to the correct handler functions instead of silently no-opping.
17
+ - **Dashboard UI duplicate module push guard** — the `ui_management` probe no longer pushes duplicate module entries when called multiple times in the same session.
18
+
19
+ ### Added
20
+ - **Dashboard UI model column** — the subagents management modal now shows the model used by each agent (e.g. "opus", "sonnet") in a dedicated column.
21
+
22
+ ## [0.11.0] - 2026-06-28
23
+
24
+ ### Added
25
+ - **Dashboard UI module integration** — `pi-agent-dashboard` users can now see subagents in the dashboard. Three integration points: a footer-segment decorator showing running/completed agent counts, a `/subagents` management-modal command that opens a table view of all subagent history with row actions (view result, abort, steer), and round-trip event handlers wired through the `ui_management` protocol. Lifecycle events trigger automatic dashboard invalidation.
26
+ - **Compact view for `get_subagent_result`** — when tool output is collapsed in the TUI, `get_subagent_result` now shows first 20 + last 20 lines of the result body with a styled divider (`─────── ⋐ N lines hidden from preview ⋑ ───────`). The full content is still passed through to the LLM. The divider format also applies to the `Agent` tool's collapsed view.
27
+ - **`list_subagents` and `clear_subagents` tools** — agents can inspect retained subagent records with a compact `List Agents` renderer and clear stale completed records with a compact `Clear Agents` renderer. `list_subagents` defaults to active/problem agents plus the two most recent successful completions and reports hidden done counts; `all: true` shows the full retained list. `clear_subagents` defaults to successful completions older than 5 minutes, accepts explicit IDs/prefixes, and refuses to clear running or queued agents.
28
+
29
+ ### Changed
30
+ - **`Agent` defaults omitted `subagent_type` to `general-purpose`** — callers can omit the type for the default general-purpose agent instead of receiving a missing-argument error.
31
+ - **Built-in `Explore` no longer pins Haiku** — the default Explore agent now inherits the parent/session model unless the caller or a custom agent definition supplies a model.
32
+ - **`snipMiddleLines` divider updated** — the collapsed-output divider now uses a styled format with locale-aware line counts instead of plain text.
33
+
34
+ ### Fixed
35
+ - **Nested subagents now appear in the live widget as soon as they are created** — the widget listens to `subagents:created` lifecycle events in addition to started/completed/failed updates, and created events now include record metadata so queued recursive agents do not render as running while waiting.
36
+ - **`get_subagent_result` peek now bounds embedded multiline output by rendered lines** — JSONL transcript entries whose text blocks contain newlines are split before `peek.lines`, `peek.after`, regex filtering, and character truncation are applied, preventing a small requested line count from returning huge multiline tool/file output records.
37
+ - **`get_subagent_result` renderResult uses typed status** — `GetResultDetails.status` is now typed as `AgentRecord['status']` and correctly renders `queued` status with the accent spinner icon.
38
+
10
39
  ## [0.10.8] - 2026-06-23
11
40
 
12
41
  ### Changed
package/README.md CHANGED
@@ -32,6 +32,7 @@ Upstream changes are reviewed for cherry-picking when practical; otherwise they
32
32
  - **Conversation viewer** — select any agent in `/agents` to open a live-scrolling overlay of its full conversation (auto-follows new content, scroll up to pause). Stop a still-running agent from here by pressing `x` (then `x` again to confirm) — works for background agents too
33
33
  - **Custom agent types** — define agents in `.pi/agents/<name>.md` with YAML frontmatter: custom system prompts, model selection, thinking levels, tool restrictions
34
34
  - **Mid-run steering** — inject messages into running agents to redirect their work without restarting
35
+ - **Agent record maintenance** — `list_subagents` gives the LLM a compact retained-agent summary (active, problem, recent done, hidden done count) and `clear_subagents` clears old completed records without touching active agents
35
36
  - **Session resume** — pick up where an agent left off, preserving full conversation context
36
37
  - **Graceful turn limits** — agents get a "wrap up" warning before hard abort, producing clean partial results instead of cut-off output
37
38
  - **Case-insensitive agent types** — `"explore"`, `"Explore"`, `"EXPLORE"` all work. Unknown types fall back to general-purpose with a note
@@ -43,6 +44,7 @@ Upstream changes are reviewed for cherry-picking when practical; otherwise they
43
44
  - **Tool denylist** — block specific tools via `disallowed_tools` frontmatter
44
45
  - **Styled completion notifications** — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML. Expandable to show a bounded preview and transcript path. Group completions render each agent individually
45
46
  - **Event bus** — lifecycle events (`subagents:created`, `started`, `completed`, `failed`, `steered`, `compacted`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity
47
+ - **Dashboard UI integration** — `pi-agent-dashboard` users can see subagents in the dashboard: a footer badge shows running/completed counts, the `/subagents` command opens a table view of all subagent history with row actions (view result, abort, steer), and lifecycle events trigger automatic UI refresh. No React or SDK required — uses the dashboard's Extension UI Module System
46
48
  - **Cross-extension RPC** — other pi extensions can spawn and stop subagents via the `pi.events` event bus (`subagents:rpc:ping`, `subagents:rpc:spawn`, `subagents:rpc:stop`). Standardized reply envelopes with protocol versioning. Emits `subagents:ready` on load
47
49
  - **Schedule subagents** — pass `schedule` to the `Agent` tool to fire on cron / interval / one-shot. Session-scoped jobs with PID-locked persistence; results land via the same steering-style `subagent-notification` path as manual background completions; manage via `/agents → Scheduled jobs`
48
50
  - **Model scope enforcement** — opt-in validation that subagent model choices stay within your pi `enabledModels` allowlist (sourced from `/scoped-models`, with both global and project-local pi settings honored). Caller-supplied out-of-scope → hard error to orchestrator; frontmatter-pinned out-of-scope → warning + runs anyway (frontmatter authoritative). Toggle via `/agents → Settings → Scope models`
@@ -153,7 +155,7 @@ Group completions render each agent as a separate block. The LLM receives struct
153
155
  | Type | Tools | Model | Prompt Mode | Description |
154
156
  |------|-------|-------|-------------|-------------|
155
157
  | `general-purpose` | all 7 | inherit | `append` (parent twin) | Inherits the parent's full system prompt — same rules, CLAUDE.md, project conventions |
156
- | `Explore` | read, bash, grep, find, ls | haiku (falls back to inherit) | `replace` (standalone) | Fast codebase exploration (read-only) |
158
+ | `Explore` | read, bash, grep, find, ls | inherit | `replace` (standalone) | Fast codebase exploration (read-only) |
157
159
  | `Plan` | read, bash, grep, find, ls | inherit | `replace` (standalone) | Software architect for implementation planning (read-only) |
158
160
 
159
161
  The `general-purpose` agent is a **parent twin** — it receives the parent's entire system prompt plus a sub-agent context bridge, so it follows the same rules the parent does. Explore and Plan use standalone prompts tailored to their read-only roles.
@@ -270,7 +272,7 @@ Launch a sub-agent.
270
272
  |-----------|------|----------|-------------|
271
273
  | `prompt` | string | yes | The task for the agent |
272
274
  | `description` | string | yes | Short 3-5 word summary (shown in UI) |
273
- | `subagent_type` | string | yes | Agent type (built-in or custom) |
275
+ | `subagent_type` | string | no | Agent type (built-in or custom). Defaults to `general-purpose` |
274
276
  | `model` | string | no | Model — `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`) |
275
277
  | `thinking` | string | no | Thinking level: off, minimal, low, medium, high, xhigh |
276
278
  | `max_turns` | number | no | Max agentic turns. Omit for unlimited (default) |
@@ -301,6 +303,24 @@ Send a steering message to a running agent. The message interrupts after the cur
301
303
  | `agent_id` | string | yes | Agent ID to steer |
302
304
  | `message` | string | yes | Message to inject into agent conversation |
303
305
 
306
+ ### `list_subagents`
307
+
308
+ List retained subagent records with a compact custom renderer. By default it shows queued/running agents, failed/stopped/aborted agents, and the two most recent successful agents that have not been cleaned up yet. It also reports how many successful completed agents are hidden. Pass `all: true` for the full retained list.
309
+
310
+ | Parameter | Type | Required | Description |
311
+ |-----------|------|----------|-------------|
312
+ | `all` | boolean | no | Show every retained subagent instead of the default compact subset |
313
+
314
+ ### `clear_subagents`
315
+
316
+ Clear retained terminal subagent records with a compact custom renderer. By default it clears successful completed/steered agents older than 5 minutes. You can provide explicit IDs or unique prefixes to clear specific terminal agents. Running and queued agents are never cleared; attempts to clear them are reported as errors.
317
+
318
+ | Parameter | Type | Required | Description |
319
+ |-----------|------|----------|-------------|
320
+ | `agent_ids` | string[] | no | Exact IDs or unique prefixes to clear; ignores the age threshold |
321
+ | `older_than_minutes` | number | no | Default-mode age threshold. Defaults to 5 |
322
+ | `include_errors` | boolean | no | Also clear failed/stopped/aborted terminal records older than the threshold |
323
+
304
324
  ## Commands
305
325
 
306
326
  | Command | Description |
@@ -8,6 +8,7 @@
8
8
  import type { Model } from "@earendil-works/pi-ai";
9
9
  import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
10
10
  import { type ToolActivity } from "./agent-runner.js";
11
+ import type { EventBus } from "./cross-extension-rpc.js";
11
12
  import { type AgentInvocation, type AgentRecord, type IsolationMode, type SubagentType, type ThinkingLevel } from "./types.js";
12
13
  export type OnAgentComplete = (record: AgentRecord) => void;
13
14
  export type OnAgentStart = (record: AgentRecord) => void;
@@ -69,6 +70,8 @@ interface SpawnOptions {
69
70
  }) => void;
70
71
  /** Called when the session successfully compacts. */
71
72
  onCompaction?: (info: CompactionInfo) => void;
73
+ /** Parent's event bus — shared with child sessions so lifecycle events propagate to the parent widget. */
74
+ eventBus?: EventBus;
72
75
  }
73
76
  interface ResumeOptions {
74
77
  signal?: AbortSignal;
@@ -94,6 +97,10 @@ export declare class AgentManager {
94
97
  private queue;
95
98
  /** Number of currently running background agents. */
96
99
  private runningBackground;
100
+ /** Background agents by id; foreground agents must not emit background completion callbacks. */
101
+ private backgroundAgentIds;
102
+ /** Background terminal callbacks already emitted; prevents abort/settle double delivery. */
103
+ private completedBackgroundCallbacks;
97
104
  constructor(onComplete?: OnAgentComplete, maxConcurrent?: number, onStart?: OnAgentStart, onCompact?: OnAgentCompact);
98
105
  /** Update the max concurrent background agents limit. */
99
106
  setMaxConcurrent(n: number): void;
@@ -103,6 +110,8 @@ export declare class AgentManager {
103
110
  * If the concurrency limit is reached, the agent is queued.
104
111
  */
105
112
  spawn(pi: ExtensionAPI, ctx: ExtensionContext, type: SubagentType, prompt: string, options: SpawnOptions): string;
113
+ /** Emit background completion once, optionally releasing a running concurrency slot. */
114
+ private completeBackground;
106
115
  /** Actually start an agent (called immediately or from queue drain). */
107
116
  private startAgent;
108
117
  /** Start queued agents up to the concurrency limit. */
@@ -119,6 +128,8 @@ export declare class AgentManager {
119
128
  abort(id: string): boolean;
120
129
  /** Dispose a record's session and remove it from the map. */
121
130
  private removeRecord;
131
+ /** Remove selected terminal records. Running and queued records are never removed. */
132
+ clearRecords(ids: string[]): string[];
122
133
  private cleanup;
123
134
  /**
124
135
  * Remove all completed/stopped/errored records immediately.
@@ -51,6 +51,10 @@ export class AgentManager {
51
51
  queue = [];
52
52
  /** Number of currently running background agents. */
53
53
  runningBackground = 0;
54
+ /** Background agents by id; foreground agents must not emit background completion callbacks. */
55
+ backgroundAgentIds = new Set();
56
+ /** Background terminal callbacks already emitted; prevents abort/settle double delivery. */
57
+ completedBackgroundCallbacks = new Set();
54
58
  constructor(onComplete, maxConcurrent = DEFAULT_MAX_CONCURRENT, onStart, onCompact) {
55
59
  this.onComplete = onComplete;
56
60
  this.onStart = onStart;
@@ -103,6 +107,8 @@ export class AgentManager {
103
107
  options.onOutputFileCreated?.(record.outputFile, id);
104
108
  }
105
109
  this.agents.set(id, record);
110
+ if (options.isBackground)
111
+ this.backgroundAgentIds.add(id);
106
112
  const args = { pi, ctx, type, prompt, options };
107
113
  if (options.isBackground && !options.bypassQueue && this.runningBackground >= this.maxConcurrent) {
108
114
  // Queue it — will be started when a running agent completes
@@ -115,11 +121,26 @@ export class AgentManager {
115
121
  this.startAgent(id, record, args);
116
122
  }
117
123
  catch (err) {
124
+ this.backgroundAgentIds.delete(id);
118
125
  this.agents.delete(id);
119
126
  throw err;
120
127
  }
121
128
  return id;
122
129
  }
130
+ /** Emit background completion once, optionally releasing a running concurrency slot. */
131
+ completeBackground(record, releaseRunningSlot, drain = true) {
132
+ if (this.completedBackgroundCallbacks.has(record.id))
133
+ return;
134
+ this.completedBackgroundCallbacks.add(record.id);
135
+ if (releaseRunningSlot)
136
+ this.runningBackground = Math.max(0, this.runningBackground - 1);
137
+ try {
138
+ this.onComplete?.(record);
139
+ }
140
+ catch { /* ignore completion side-effect errors */ }
141
+ if (drain)
142
+ this.drainQueue();
143
+ }
123
144
  /** Actually start an agent (called immediately or from queue drain). */
124
145
  startAgent(id, record, { pi, ctx, type, prompt, options }) {
125
146
  // Re-validate a caller-supplied cwd: queued spawns can start minutes after
@@ -197,6 +218,7 @@ export class AgentManager {
197
218
  },
198
219
  depth: record.depth,
199
220
  parentAgentId: record.parentAgentId,
221
+ eventBus: options.eventBus,
200
222
  onSessionCreated: (session) => {
201
223
  record.session = session;
202
224
  // Flush any steers that arrived before the session was ready
@@ -239,12 +261,7 @@ export class AgentManager {
239
261
  }
240
262
  }
241
263
  if (options.isBackground) {
242
- this.runningBackground--;
243
- try {
244
- this.onComplete?.(record);
245
- }
246
- catch { /* ignore completion side-effect errors */ }
247
- this.drainQueue();
264
+ this.completeBackground(record, true);
248
265
  }
249
266
  return responseText;
250
267
  })
@@ -273,9 +290,7 @@ export class AgentManager {
273
290
  catch { /* ignore cleanup errors */ }
274
291
  }
275
292
  if (options.isBackground) {
276
- this.runningBackground--;
277
- this.onComplete?.(record);
278
- this.drainQueue();
293
+ this.completeBackground(record, true);
279
294
  }
280
295
  return "";
281
296
  });
@@ -297,7 +312,7 @@ export class AgentManager {
297
312
  record.status = "error";
298
313
  record.error = err instanceof Error ? err.message : String(err);
299
314
  record.completedAt = Date.now();
300
- this.onComplete?.(record);
315
+ this.completeBackground(record, false, false);
301
316
  }
302
317
  }
303
318
  }
@@ -326,6 +341,8 @@ export class AgentManager {
326
341
  record.error = undefined;
327
342
  record.resultConsumed = false;
328
343
  record.abortController = new AbortController();
344
+ this.backgroundAgentIds.add(id);
345
+ this.completedBackgroundCallbacks.delete(id);
329
346
  this.runningBackground++;
330
347
  this.onStart?.(record);
331
348
  const onParentAbort = () => this.abort(id);
@@ -354,12 +371,7 @@ export class AgentManager {
354
371
  record.result = responseText;
355
372
  record.completedAt = Date.now();
356
373
  detach();
357
- this.runningBackground--;
358
- try {
359
- this.onComplete?.(record);
360
- }
361
- catch { /* ignore completion side-effect errors */ }
362
- this.drainQueue();
374
+ this.completeBackground(record, true);
363
375
  return responseText;
364
376
  }).catch((err) => {
365
377
  if (record.status !== "stopped")
@@ -367,12 +379,7 @@ export class AgentManager {
367
379
  record.error = err instanceof Error ? err.message : String(err);
368
380
  record.completedAt = Date.now();
369
381
  detach();
370
- this.runningBackground--;
371
- try {
372
- this.onComplete?.(record);
373
- }
374
- catch { /* ignore completion side-effect errors */ }
375
- this.drainQueue();
382
+ this.completeBackground(record, true);
376
383
  return "";
377
384
  });
378
385
  record.promise = promise;
@@ -393,6 +400,7 @@ export class AgentManager {
393
400
  this.queue = this.queue.filter(q => q.id !== id);
394
401
  record.status = "stopped";
395
402
  record.completedAt = Date.now();
403
+ this.completeBackground(record, false);
396
404
  return true;
397
405
  }
398
406
  if (record.status !== "running")
@@ -400,14 +408,32 @@ export class AgentManager {
400
408
  record.abortController?.abort();
401
409
  record.status = "stopped";
402
410
  record.completedAt = Date.now();
411
+ if (this.backgroundAgentIds.has(id))
412
+ this.completeBackground(record, true);
403
413
  return true;
404
414
  }
405
415
  /** Dispose a record's session and remove it from the map. */
406
416
  removeRecord(id, record) {
407
417
  record.session?.dispose?.();
408
418
  record.session = undefined;
419
+ this.backgroundAgentIds.delete(id);
420
+ this.completedBackgroundCallbacks.delete(id);
409
421
  this.agents.delete(id);
410
422
  }
423
+ /** Remove selected terminal records. Running and queued records are never removed. */
424
+ clearRecords(ids) {
425
+ const removed = [];
426
+ for (const id of ids) {
427
+ const record = this.agents.get(id);
428
+ if (!record)
429
+ continue;
430
+ if (record.status === "running" || record.status === "queued")
431
+ continue;
432
+ this.removeRecord(id, record);
433
+ removed.push(id);
434
+ }
435
+ return removed;
436
+ }
411
437
  cleanup() {
412
438
  const cutoff = Date.now() - 10 * 60_000;
413
439
  for (const [id, record] of this.agents) {
@@ -442,6 +468,8 @@ export class AgentManager {
442
468
  if (record) {
443
469
  record.status = "stopped";
444
470
  record.completedAt = Date.now();
471
+ if (this.backgroundAgentIds.has(record.id))
472
+ this.completedBackgroundCallbacks.add(record.id);
445
473
  count++;
446
474
  }
447
475
  }
@@ -452,9 +480,12 @@ export class AgentManager {
452
480
  record.abortController?.abort();
453
481
  record.status = "stopped";
454
482
  record.completedAt = Date.now();
483
+ if (this.backgroundAgentIds.has(record.id))
484
+ this.completedBackgroundCallbacks.add(record.id);
455
485
  count++;
456
486
  }
457
487
  }
488
+ this.runningBackground = 0;
458
489
  return count;
459
490
  }
460
491
  /** Wait for all running and queued agents to complete (including queued ones). */
@@ -480,6 +511,8 @@ export class AgentManager {
480
511
  record.session?.dispose();
481
512
  }
482
513
  this.agents.clear();
514
+ this.backgroundAgentIds.clear();
515
+ this.completedBackgroundCallbacks.clear();
483
516
  // Prune any orphaned git worktrees (crash recovery)
484
517
  try {
485
518
  pruneWorktrees(process.cwd());
@@ -4,6 +4,7 @@
4
4
  import type { Model } from "@earendil-works/pi-ai";
5
5
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
6
6
  import { type AgentSession, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
+ import type { EventBus } from "./cross-extension-rpc.js";
7
8
  import { type SubagentType, type ThinkingLevel } from "./types.js";
8
9
  /**
9
10
  * Tool names registered by THIS extension. Single source of truth so the
@@ -15,10 +16,20 @@ export declare const SUBAGENT_TOOL_NAMES: {
15
16
  readonly AGENT: "Agent";
16
17
  readonly GET_RESULT: "get_subagent_result";
17
18
  readonly STEER: "steer_subagent";
19
+ readonly LIST_SUBAGENTS: "list_subagents";
20
+ readonly CLEAR_SUBAGENTS: "clear_subagents";
18
21
  readonly LIST_MODELS: "list_models";
19
22
  };
23
+ /**
24
+ * Create a forwarding event bus for a child session.
25
+ * The child gets its own local bus for emit/on, but lifecycle events
26
+ * (subagents:*) are also forwarded to the parent bus so the parent widget
27
+ * can display depth 2+ agents.
28
+ */
29
+ export declare function createForwardingEventBus(parentBus: EventBus): EventBus;
20
30
  export declare function getCurrentExtensionDepth(): number;
21
31
  export declare function getCurrentExtensionAgentId(): string | undefined;
32
+ export declare function getCurrentExtensionParentAgentId(): string | undefined;
22
33
  /**
23
34
  * Canonical name of an extension for `extensions: [...]` allowlist matching.
24
35
  * Lowercased — extension names match case-insensitively so `extensions: [Mcp]`
@@ -128,6 +139,9 @@ export interface RunOptions {
128
139
  depth?: number;
129
140
  /** Parent subagent id when spawned recursively from another subagent. */
130
141
  parentAgentId?: string;
142
+ /** Parent's event bus — shared with the child session so lifecycle events
143
+ * (subagents:created, subagents:started, etc.) propagate to the parent widget. */
144
+ eventBus?: EventBus;
131
145
  }
132
146
  export interface RunResult {
133
147
  responseText: string;
@@ -4,7 +4,7 @@
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { homedir } from "node:os";
6
6
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
7
- import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
7
+ import { createAgentSession, createEventBus, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
8
8
  import { BUILTIN_TOOL_NAMES, getAgentConfig, getConfig, getMemoryToolNames, getReadOnlyMemoryToolNames, getToolNamesForType } from "./agent-types.js";
9
9
  import { buildParentContext, extractText } from "./context.js";
10
10
  import { DEFAULT_AGENTS } from "./default-agents.js";
@@ -23,6 +23,8 @@ export const SUBAGENT_TOOL_NAMES = {
23
23
  AGENT: "Agent",
24
24
  GET_RESULT: "get_subagent_result",
25
25
  STEER: "steer_subagent",
26
+ LIST_SUBAGENTS: "list_subagents",
27
+ CLEAR_SUBAGENTS: "clear_subagents",
26
28
  LIST_MODELS: "list_models",
27
29
  };
28
30
  /**
@@ -38,6 +40,38 @@ const RECURSIVE_TOOL_NAMES = [
38
40
  SUBAGENT_TOOL_NAMES.STEER,
39
41
  ];
40
42
  const EXTENSION_DEPTH_KEY = Symbol.for("pi-subagents:extension-depth");
43
+ /** Lifecycle event names that should propagate from child to parent sessions. */
44
+ const FORWARDABLE_EVENTS = new Set([
45
+ "subagents:created",
46
+ "subagents:started",
47
+ "subagents:completed",
48
+ "subagents:failed",
49
+ "subagents:compacted",
50
+ ]);
51
+ /**
52
+ * Create a forwarding event bus for a child session.
53
+ * The child gets its own local bus for emit/on, but lifecycle events
54
+ * (subagents:*) are also forwarded to the parent bus so the parent widget
55
+ * can display depth 2+ agents.
56
+ */
57
+ export function createForwardingEventBus(parentBus) {
58
+ // Use the parent's EventBus factory to create a properly isolated local bus
59
+ const localBus = createEventBus();
60
+ return {
61
+ on(event, handler) {
62
+ // Subscribe to local bus only — child doesn't see parent/sibling events
63
+ return localBus.on(event, handler);
64
+ },
65
+ emit(event, data) {
66
+ // Always emit on local bus for child's own listeners
67
+ localBus.emit(event, data);
68
+ // Forward lifecycle events to parent bus for parent widget visibility
69
+ if (FORWARDABLE_EVENTS.has(event)) {
70
+ parentBus.emit(event, data);
71
+ }
72
+ },
73
+ };
74
+ }
41
75
  const AUTO_EXPOSE_EXTENSION_NAMES = new Set(["pi-c2c"]);
42
76
  let extensionDepthLoadChain = Promise.resolve();
43
77
  const packageNameCache = new Map();
@@ -53,7 +87,11 @@ function getLoadingExtensionAgentId() {
53
87
  const value = globalThis[EXTENSION_DEPTH_KEY];
54
88
  return value && typeof value.agentId === "string" ? value.agentId : undefined;
55
89
  }
56
- async function withLoadingExtensionDepth(depth, agentId, fn) {
90
+ function getLoadingExtensionParentAgentId() {
91
+ const value = globalThis[EXTENSION_DEPTH_KEY];
92
+ return value && typeof value.parentAgentId === "string" ? value.parentAgentId : undefined;
93
+ }
94
+ async function withLoadingExtensionDepth(depth, agentId, parentAgentId, fn) {
57
95
  const previous = extensionDepthLoadChain;
58
96
  let release;
59
97
  extensionDepthLoadChain = new Promise((resolve) => {
@@ -63,7 +101,7 @@ async function withLoadingExtensionDepth(depth, agentId, fn) {
63
101
  try {
64
102
  const g = globalThis;
65
103
  const prev = g[EXTENSION_DEPTH_KEY];
66
- g[EXTENSION_DEPTH_KEY] = { depth, agentId };
104
+ g[EXTENSION_DEPTH_KEY] = { depth, agentId, parentAgentId };
67
105
  try {
68
106
  return await fn();
69
107
  }
@@ -84,6 +122,9 @@ export function getCurrentExtensionDepth() {
84
122
  export function getCurrentExtensionAgentId() {
85
123
  return getLoadingExtensionAgentId();
86
124
  }
125
+ export function getCurrentExtensionParentAgentId() {
126
+ return getLoadingExtensionParentAgentId();
127
+ }
87
128
  /**
88
129
  * Canonical name of an extension for `extensions: [...]` allowlist matching.
89
130
  * Lowercased — extension names match case-insensitively so `extensions: [Mcp]`
@@ -429,6 +470,10 @@ export async function runAgent(ctx, type, prompt, options) {
429
470
  }),
430
471
  };
431
472
  };
473
+ // Create a forwarding event bus so the child session's lifecycle events
474
+ // (subagents:created, subagents:started, etc.) propagate to the parent's
475
+ // event bus — making depth 2+ agents visible in the parent widget.
476
+ const childEventBus = options.eventBus ? createForwardingEventBus(options.eventBus) : undefined;
432
477
  const loader = new DefaultResourceLoader({
433
478
  cwd: configCwd,
434
479
  agentDir,
@@ -441,8 +486,9 @@ export async function runAgent(ctx, type, prompt, options) {
441
486
  noContextFiles: true,
442
487
  systemPromptOverride: () => systemPrompt,
443
488
  appendSystemPromptOverride: () => [],
489
+ eventBus: childEventBus,
444
490
  });
445
- await withLoadingExtensionDepth(depth, options.agentId, () => loader.reload());
491
+ await withLoadingExtensionDepth(depth, options.agentId, options.parentAgentId, () => loader.reload());
446
492
  // Plain entries in `tools:` are expected to be built-in names (extension tools
447
493
  // go through `ext:`), so an unknown name there is unambiguously a typo. Previously
448
494
  // this produced a silently broken agent (#75) — pi-mono accepted the bogus name
@@ -2,7 +2,13 @@ import type { ToolDescriptionMode } from "./settings.js";
2
2
  export declare function getModelLabelFromConfig(model: string): string;
3
3
  export interface AgentToolDescriptionOptions {
4
4
  mode: ToolDescriptionMode;
5
- extensionDepth: number;
5
+ /**
6
+ * Depth at which the NEXT spawned subagent will run.
7
+ * This is `extensionDepth + 1` — the agent's own depth plus one.
8
+ * Displayed as "Current recursive depth" in the tool description so the
9
+ * LLM sees the depth of the agent it is about to create, not its own depth.
10
+ */
11
+ nextSubagentDepth: number;
6
12
  schedulingEnabled: boolean;
7
13
  }
8
14
  export declare function buildScheduleGuideline(schedulingEnabled: boolean): string;
@@ -39,7 +39,7 @@ export function buildScheduleGuideline(schedulingEnabled) {
39
39
  }
40
40
  export function buildAgentToolDescription(options) {
41
41
  const scheduleGuideline = buildScheduleGuideline(options.schedulingEnabled);
42
- const recursiveGuideline = `Recursive agents are allowed through depth ${MAX_RECURSIVE_DEPTH}. Current recursive depth: ${options.extensionDepth}/${MAX_RECURSIVE_DEPTH}.`;
42
+ const recursiveGuideline = `Recursive agents are allowed through depth ${MAX_RECURSIVE_DEPTH}. Current recursive depth: ${options.nextSubagentDepth}/${MAX_RECURSIVE_DEPTH}.`;
43
43
  const compactAgentToolDescription = `Launch an autonomous agent for complex, multi-step tasks. Agent types:
44
44
  ${buildCompactTypeListText()}
45
45
 
@@ -49,7 +49,7 @@ Notes:
49
49
  - description: 3-5 words (shown in UI). Prompts must be self-contained — the agent has not seen this conversation.
50
50
  - Parallel work: one message, multiple Agent calls; they all run in the background. You are notified when agents finish — never poll or sleep.
51
51
  - Background by default: when you have useful independent work, launch it and continue. Doing nothing while an agent runs is worse than letting background work proceed.
52
- - Recursive agents: current depth ${options.extensionDepth}/${MAX_RECURSIVE_DEPTH}; you may spawn subagents until depth ${MAX_RECURSIVE_DEPTH}.
52
+ - Recursive agents: current depth ${options.nextSubagentDepth}/${MAX_RECURSIVE_DEPTH}; you may spawn subagents until depth ${MAX_RECURSIVE_DEPTH}.
53
53
  - The result is not shown to the user — summarize it for them. Verify an agent's claimed code changes before reporting work done.
54
54
  - resume continues a previous agent by ID; steer_subagent messages a running one.
55
55
  - list_models enumerates the model registry the \`model:\` param accepts — call it before picking a model explicitly.
@@ -105,7 +105,7 @@ Terse command-style prompts produce shallow, generic work.
105
105
  compactTypeList: buildCompactTypeListText,
106
106
  agentDir: getAgentDir,
107
107
  scheduleGuideline: () => scheduleGuideline,
108
- currentDepth: () => String(options.extensionDepth),
108
+ currentDepth: () => String(options.nextSubagentDepth),
109
109
  maxDepth: () => String(MAX_RECURSIVE_DEPTH),
110
110
  recursiveGuideline: () => recursiveGuideline,
111
111
  };
@@ -33,6 +33,10 @@ export interface RpcDeps {
33
33
  pi: unknown;
34
34
  getCtx: () => unknown | undefined;
35
35
  manager: SpawnCapable;
36
+ /** Default recursive depth for RPC-spawned subagents in this session. */
37
+ depth?: number;
38
+ /** Parent subagent id for RPC-spawned subagents in this session. */
39
+ parentAgentId?: string;
36
40
  }
37
41
  export interface RpcHandle {
38
42
  unsubPing: () => void;
@@ -66,7 +66,17 @@ export function registerRpcHandlers(deps) {
66
66
  }
67
67
  normalizedOptions = { ...normalizedOptions, model: resolved };
68
68
  }
69
- return { id: manager.spawn(pi, ctx, type, prompt, normalizedOptions) };
69
+ const spawnOptions = {
70
+ ...normalizedOptions,
71
+ eventBus: events,
72
+ depth: normalizedOptions.depth ?? deps.depth,
73
+ parentAgentId: normalizedOptions.parentAgentId ?? deps.parentAgentId,
74
+ };
75
+ if (spawnOptions.depth === undefined)
76
+ delete spawnOptions.depth;
77
+ if (spawnOptions.parentAgentId === undefined)
78
+ delete spawnOptions.parentAgentId;
79
+ return { id: manager.spawn(pi, ctx, type, prompt, spawnOptions) };
70
80
  });
71
81
  const unsubStop = handleRpc(events, "subagents:rpc:stop", ({ agentId }) => {
72
82
  if (!manager.abort(agentId))
@@ -0,0 +1,15 @@
1
+ /**
2
+ * dashboard-ui.ts — Register dashboard UI modules for subagent visibility.
3
+ *
4
+ * Provides three integration points with pi-agent-dashboard:
5
+ * 1. Footer-segment decorator showing running/completed agent counts
6
+ * 2. Management-modal module with a table view of all subagent history
7
+ * 3. Round-trip event handlers for data fetch, abort, and steer actions
8
+ */
9
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
+ import type { AgentManager } from "./agent-manager.js";
11
+ /**
12
+ * Register all dashboard UI integration points.
13
+ * Call once during extension setup when pi.events is available.
14
+ */
15
+ export declare function registerDashboardModules(pi: ExtensionAPI, manager: AgentManager): void;