@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 +2 -0
- package/CHANGELOG.md +29 -0
- package/README.md +22 -2
- package/dist/agent-manager.d.ts +11 -0
- package/dist/agent-manager.js +55 -22
- package/dist/agent-runner.d.ts +14 -0
- package/dist/agent-runner.js +50 -4
- package/dist/agent-tool-description.d.ts +7 -1
- package/dist/agent-tool-description.js +3 -3
- package/dist/cross-extension-rpc.d.ts +4 -0
- package/dist/cross-extension-rpc.js +11 -1
- package/dist/dashboard-ui.d.ts +15 -0
- package/dist/dashboard-ui.js +231 -0
- package/dist/default-agents.js +0 -1
- package/dist/index.js +104 -13
- package/dist/peek.js +8 -2
- package/dist/schedule.d.ts +9 -1
- package/dist/schedule.js +7 -1
- package/dist/subagent-list-clear.d.ts +57 -0
- package/dist/subagent-list-clear.js +331 -0
- package/dist/ui/agent-tool-rendering.js +1 -1
- package/dist/ui/agent-widget-tree.js +19 -2
- package/dist/ui/agent-widget.d.ts +7 -1
- package/dist/ui/agent-widget.js +52 -10
- package/package.json +1 -1
- package/src/agent-manager.ts +48 -13
- package/src/agent-runner.ts +59 -3
- package/src/agent-tool-description.ts +10 -4
- package/src/cross-extension-rpc.ts +14 -1
- package/src/dashboard-ui.ts +291 -0
- package/src/default-agents.ts +0 -1
- package/src/index.ts +121 -17
- package/src/peek.ts +7 -2
- package/src/schedule.ts +20 -1
- package/src/subagent-list-clear.ts +405 -0
- package/src/ui/agent-tool-rendering.ts +1 -1
- package/src/ui/agent-widget-tree.ts +16 -2
- package/src/ui/agent-widget.ts +50 -10
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 |
|
|
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 |
|
|
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 |
|
package/dist/agent-manager.d.ts
CHANGED
|
@@ -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.
|
package/dist/agent-manager.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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());
|
package/dist/agent-runner.d.ts
CHANGED
|
@@ -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;
|
package/dist/agent-runner.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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;
|