@clanker-code/pi-subagents 0.10.8 → 0.11.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.
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +17 -0
- package/README.md +22 -2
- package/bugs.txt +57 -0
- package/dist/agent-manager.d.ts +8 -0
- package/dist/agent-manager.js +54 -22
- package/dist/agent-runner.d.ts +3 -0
- package/dist/agent-runner.js +12 -3
- package/dist/dashboard-ui.d.ts +15 -0
- package/dist/dashboard-ui.js +206 -0
- package/dist/default-agents.js +0 -1
- package/dist/index.js +96 -11
- package/dist/peek.js +8 -2
- 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 +44 -13
- package/src/agent-runner.ts +14 -3
- package/src/dashboard-ui.ts +270 -0
- package/src/default-agents.ts +0 -1
- package/src/index.ts +113 -15
- package/src/peek.ts +7 -2
- 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,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.11.0] - 2026-06-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
- **`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.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **`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.
|
|
19
|
+
- **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.
|
|
20
|
+
- **`snipMiddleLines` divider updated** — the collapsed-output divider now uses a styled format with locale-aware line counts instead of plain text.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **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.
|
|
24
|
+
- **`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.
|
|
25
|
+
- **`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.
|
|
26
|
+
|
|
10
27
|
## [0.10.8] - 2026-06-23
|
|
11
28
|
|
|
12
29
|
### 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/bugs.txt
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# pi-subagents bugs
|
|
2
|
+
|
|
3
|
+
## 2026-06-24: subagent-spawned reviewer c2c alias registrations route to coordinator
|
|
4
|
+
|
|
5
|
+
Observed in `/home/xertrov/src/autoplanet-harness` session while coordinating nested pi Agent reviewers.
|
|
6
|
+
|
|
7
|
+
Symptom:
|
|
8
|
+
- Coordinator received transcript notifications like `Subagent <id> registered as pi-...` for reviewer subagents spawned by an implementer subagent.
|
|
9
|
+
- The spawning implementer subagent did **not** receive those c2c alias registration notifications.
|
|
10
|
+
- This made the coordinator see aliases for subagents it did not directly start, while the parent subagent could not map its Agent handles to c2c aliases.
|
|
11
|
+
|
|
12
|
+
Concrete example:
|
|
13
|
+
- Coordinator directly started T244/G9 implementer: agent `81a4688f-82ce-40e`, alias `pi-8391d3-ae5887a`.
|
|
14
|
+
- T244 spawned reviewer/rereviewer Agent subagents with IDs:
|
|
15
|
+
- `436193fa-ef20-4d0`
|
|
16
|
+
- `97bddbcf-53a8-48c`
|
|
17
|
+
- `56a58df4-c91c-48a`
|
|
18
|
+
- `82ae1d14-707d-415`
|
|
19
|
+
- Coordinator received registrations for aliases including `pi-8391d3-a723e9a`, `pi-8391d3-a0a8a00`, `pi-8391d3-ab7ed6f`, `pi-8391d3-ab167dc`.
|
|
20
|
+
- T244 reported: its transcript only showed Agent result handles/output paths/completion summaries; it did not see `Subagent <id> registered as pi-...` messages and could not map reviewer IDs to c2c aliases.
|
|
21
|
+
|
|
22
|
+
Expected behavior:
|
|
23
|
+
- Alias registration notifications for a subagent spawned by another subagent should be delivered to the spawning subagent's transcript, or at least to both the spawning subagent and coordinator with parent/owner metadata.
|
|
24
|
+
- The notification should include parent/spawner identity so coordinators can distinguish direct children from nested reviewer subagents.
|
|
25
|
+
|
|
26
|
+
Impact:
|
|
27
|
+
- Coordinator receives noisy/ambiguous registrations for unknown aliases.
|
|
28
|
+
- Spawning subagent cannot c2c-message its own reviewers by alias or report alias ownership accurately.
|
|
29
|
+
- Requires extra coordinator debugging messages to identify ownership.
|
|
30
|
+
|
|
31
|
+
Suggested fix:
|
|
32
|
+
- Route `Subagent <id> registered as <alias>` to the agent that invoked the Agent tool.
|
|
33
|
+
- Add fields like `parent_agent_id`, `parent_alias`, and maybe `root_coordinator_alias` to registration notifications.
|
|
34
|
+
- If root coordinator must also receive nested registrations, label them explicitly as nested.
|
|
35
|
+
|
|
36
|
+
Additional observation after filing:
|
|
37
|
+
- Coordinator later received another nested-looking registration: `Subagent 8262e0d6-457e-49c registered as pi-8391d3-ab41e8a`, again without the coordinator directly starting that subagent.
|
|
38
|
+
- Confirmed with T245/G6 implementer `pi-8391d3-a861bf0`: it spawned reviewer Agent IDs `8262e0d6-457e-49c` and `14d29ca7-348e-437`, while coordinator received alias registrations `pi-8391d3-ab41e8a` and `pi-8391d3-aa90cd5`; T245 reported it had not received any c2c alias registration notifications for them.
|
|
39
|
+
- After crash recovery restart, coordinator received another nested-looking registration: `Subagent 4f396572-cab5-42c registered as pi-8391d3-a52ab8a`; likely spawned by a recovery worker/reviewer rather than directly by coordinator.
|
|
40
|
+
- Coordinator also received `Subagent 09dc9e7f-f960-411 registered as pi-8391d3-a219275` during recovery worker activity, again likely nested/reviewer notification routed to coordinator.
|
|
41
|
+
- Coordinator also received `Subagent ee594feb-8fd1-458 registered as pi-8391d3-a8f6d40` while nested reviewer activity was ongoing; likely another nested alias notification routed to coordinator.
|
|
42
|
+
- Coordinator also received `Subagent dd29fe5b-357a-48f registered as pi-8391d3-afd6cfc` during T246 nested rereview activity; likely another nested alias notification routed to coordinator.
|
|
43
|
+
- Coordinator received nested-looking registrations `Subagent 9aa66b38-3ba8-405 registered as pi-8391d3-a6cefa4` and `Subagent d33bb514-be85-4a5 registered as pi-8391d3-ae99126` during active worker review activity.
|
|
44
|
+
- Coordinator received nested-looking registration `Subagent ab7cb925-d76c-489 registered as pi-8391d3-a8c6afd` during active worker review activity.
|
|
45
|
+
- Coordinator received nested-looking registration `Subagent ba23eaaf-a706-459 registered as pi-8391d3-af5d393` during active worker review activity.
|
|
46
|
+
- Coordinator received nested-looking registration `Subagent adf52e5e-862e-4c6 registered as pi-8391d3-aa897ea` during active worker review activity.
|
|
47
|
+
- Coordinator received nested-looking registration `Subagent e6eb1662-8223-458 registered as pi-8391d3-adb2231` during active worker review activity.
|
|
48
|
+
- Coordinator received nested-looking registration `Subagent 565012aa-43b6-407 registered as pi-8391d3-a66be3e` during active worker review activity.
|
|
49
|
+
- Coordinator received nested-looking registration `Subagent 603b211a-9daa-421 registered as pi-8391d3-aa92c43` during active worker review activity.
|
|
50
|
+
- Coordinator received nested-looking registration `Subagent 734f3fa3-958b-4bd registered as pi-8391d3-a028432` during active T248 review activity.
|
|
51
|
+
- Coordinator received nested-looking registration `Subagent 2d565584-fcbc-496 registered as pi-8391d3-ac6e43c` during active T250/T251/T252 review activity.
|
|
52
|
+
- Coordinator received nested-looking registration `Subagent 5e7ee0d1-5112-4b9 registered as pi-8391d3-a7722c2` during active T250/T251/T252 review activity.
|
|
53
|
+
- Coordinator received nested-looking registration `Subagent eae7393b-dfb4-445 registered as pi-8391d3-a2a9d62` during active T250/T251/T252 review activity.
|
|
54
|
+
- Coordinator received nested-looking registration `Subagent 6629a0fe-daef-401 registered as pi-8391d3-ad0b4e5` during active T250 review activity.
|
|
55
|
+
- Coordinator received nested-looking registration `Subagent 49eb5d11-178d-4d2 registered as pi-8391d3-a979978` during active T250 review activity.
|
|
56
|
+
- Coordinator received nested-looking registration `Subagent bf846951-6b2f-4b5 registered as pi-8391d3-aabc2c5` during active T250 rereview activity.
|
|
57
|
+
- Coordinator received nested-looking registration `Subagent 3250e286-4fd4-4a2 registered as pi-8391d3-a78b404` during active T250 rereview activity.
|
package/dist/agent-manager.d.ts
CHANGED
|
@@ -94,6 +94,10 @@ export declare class AgentManager {
|
|
|
94
94
|
private queue;
|
|
95
95
|
/** Number of currently running background agents. */
|
|
96
96
|
private runningBackground;
|
|
97
|
+
/** Background agents by id; foreground agents must not emit background completion callbacks. */
|
|
98
|
+
private backgroundAgentIds;
|
|
99
|
+
/** Background terminal callbacks already emitted; prevents abort/settle double delivery. */
|
|
100
|
+
private completedBackgroundCallbacks;
|
|
97
101
|
constructor(onComplete?: OnAgentComplete, maxConcurrent?: number, onStart?: OnAgentStart, onCompact?: OnAgentCompact);
|
|
98
102
|
/** Update the max concurrent background agents limit. */
|
|
99
103
|
setMaxConcurrent(n: number): void;
|
|
@@ -103,6 +107,8 @@ export declare class AgentManager {
|
|
|
103
107
|
* If the concurrency limit is reached, the agent is queued.
|
|
104
108
|
*/
|
|
105
109
|
spawn(pi: ExtensionAPI, ctx: ExtensionContext, type: SubagentType, prompt: string, options: SpawnOptions): string;
|
|
110
|
+
/** Emit background completion once, optionally releasing a running concurrency slot. */
|
|
111
|
+
private completeBackground;
|
|
106
112
|
/** Actually start an agent (called immediately or from queue drain). */
|
|
107
113
|
private startAgent;
|
|
108
114
|
/** Start queued agents up to the concurrency limit. */
|
|
@@ -119,6 +125,8 @@ export declare class AgentManager {
|
|
|
119
125
|
abort(id: string): boolean;
|
|
120
126
|
/** Dispose a record's session and remove it from the map. */
|
|
121
127
|
private removeRecord;
|
|
128
|
+
/** Remove selected terminal records. Running and queued records are never removed. */
|
|
129
|
+
clearRecords(ids: string[]): string[];
|
|
122
130
|
private cleanup;
|
|
123
131
|
/**
|
|
124
132
|
* 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
|
|
@@ -239,12 +260,7 @@ export class AgentManager {
|
|
|
239
260
|
}
|
|
240
261
|
}
|
|
241
262
|
if (options.isBackground) {
|
|
242
|
-
this.
|
|
243
|
-
try {
|
|
244
|
-
this.onComplete?.(record);
|
|
245
|
-
}
|
|
246
|
-
catch { /* ignore completion side-effect errors */ }
|
|
247
|
-
this.drainQueue();
|
|
263
|
+
this.completeBackground(record, true);
|
|
248
264
|
}
|
|
249
265
|
return responseText;
|
|
250
266
|
})
|
|
@@ -273,9 +289,7 @@ export class AgentManager {
|
|
|
273
289
|
catch { /* ignore cleanup errors */ }
|
|
274
290
|
}
|
|
275
291
|
if (options.isBackground) {
|
|
276
|
-
this.
|
|
277
|
-
this.onComplete?.(record);
|
|
278
|
-
this.drainQueue();
|
|
292
|
+
this.completeBackground(record, true);
|
|
279
293
|
}
|
|
280
294
|
return "";
|
|
281
295
|
});
|
|
@@ -297,7 +311,7 @@ export class AgentManager {
|
|
|
297
311
|
record.status = "error";
|
|
298
312
|
record.error = err instanceof Error ? err.message : String(err);
|
|
299
313
|
record.completedAt = Date.now();
|
|
300
|
-
this.
|
|
314
|
+
this.completeBackground(record, false, false);
|
|
301
315
|
}
|
|
302
316
|
}
|
|
303
317
|
}
|
|
@@ -326,6 +340,8 @@ export class AgentManager {
|
|
|
326
340
|
record.error = undefined;
|
|
327
341
|
record.resultConsumed = false;
|
|
328
342
|
record.abortController = new AbortController();
|
|
343
|
+
this.backgroundAgentIds.add(id);
|
|
344
|
+
this.completedBackgroundCallbacks.delete(id);
|
|
329
345
|
this.runningBackground++;
|
|
330
346
|
this.onStart?.(record);
|
|
331
347
|
const onParentAbort = () => this.abort(id);
|
|
@@ -354,12 +370,7 @@ export class AgentManager {
|
|
|
354
370
|
record.result = responseText;
|
|
355
371
|
record.completedAt = Date.now();
|
|
356
372
|
detach();
|
|
357
|
-
this.
|
|
358
|
-
try {
|
|
359
|
-
this.onComplete?.(record);
|
|
360
|
-
}
|
|
361
|
-
catch { /* ignore completion side-effect errors */ }
|
|
362
|
-
this.drainQueue();
|
|
373
|
+
this.completeBackground(record, true);
|
|
363
374
|
return responseText;
|
|
364
375
|
}).catch((err) => {
|
|
365
376
|
if (record.status !== "stopped")
|
|
@@ -367,12 +378,7 @@ export class AgentManager {
|
|
|
367
378
|
record.error = err instanceof Error ? err.message : String(err);
|
|
368
379
|
record.completedAt = Date.now();
|
|
369
380
|
detach();
|
|
370
|
-
this.
|
|
371
|
-
try {
|
|
372
|
-
this.onComplete?.(record);
|
|
373
|
-
}
|
|
374
|
-
catch { /* ignore completion side-effect errors */ }
|
|
375
|
-
this.drainQueue();
|
|
381
|
+
this.completeBackground(record, true);
|
|
376
382
|
return "";
|
|
377
383
|
});
|
|
378
384
|
record.promise = promise;
|
|
@@ -393,6 +399,7 @@ export class AgentManager {
|
|
|
393
399
|
this.queue = this.queue.filter(q => q.id !== id);
|
|
394
400
|
record.status = "stopped";
|
|
395
401
|
record.completedAt = Date.now();
|
|
402
|
+
this.completeBackground(record, false);
|
|
396
403
|
return true;
|
|
397
404
|
}
|
|
398
405
|
if (record.status !== "running")
|
|
@@ -400,14 +407,32 @@ export class AgentManager {
|
|
|
400
407
|
record.abortController?.abort();
|
|
401
408
|
record.status = "stopped";
|
|
402
409
|
record.completedAt = Date.now();
|
|
410
|
+
if (this.backgroundAgentIds.has(id))
|
|
411
|
+
this.completeBackground(record, true);
|
|
403
412
|
return true;
|
|
404
413
|
}
|
|
405
414
|
/** Dispose a record's session and remove it from the map. */
|
|
406
415
|
removeRecord(id, record) {
|
|
407
416
|
record.session?.dispose?.();
|
|
408
417
|
record.session = undefined;
|
|
418
|
+
this.backgroundAgentIds.delete(id);
|
|
419
|
+
this.completedBackgroundCallbacks.delete(id);
|
|
409
420
|
this.agents.delete(id);
|
|
410
421
|
}
|
|
422
|
+
/** Remove selected terminal records. Running and queued records are never removed. */
|
|
423
|
+
clearRecords(ids) {
|
|
424
|
+
const removed = [];
|
|
425
|
+
for (const id of ids) {
|
|
426
|
+
const record = this.agents.get(id);
|
|
427
|
+
if (!record)
|
|
428
|
+
continue;
|
|
429
|
+
if (record.status === "running" || record.status === "queued")
|
|
430
|
+
continue;
|
|
431
|
+
this.removeRecord(id, record);
|
|
432
|
+
removed.push(id);
|
|
433
|
+
}
|
|
434
|
+
return removed;
|
|
435
|
+
}
|
|
411
436
|
cleanup() {
|
|
412
437
|
const cutoff = Date.now() - 10 * 60_000;
|
|
413
438
|
for (const [id, record] of this.agents) {
|
|
@@ -442,6 +467,8 @@ export class AgentManager {
|
|
|
442
467
|
if (record) {
|
|
443
468
|
record.status = "stopped";
|
|
444
469
|
record.completedAt = Date.now();
|
|
470
|
+
if (this.backgroundAgentIds.has(record.id))
|
|
471
|
+
this.completedBackgroundCallbacks.add(record.id);
|
|
445
472
|
count++;
|
|
446
473
|
}
|
|
447
474
|
}
|
|
@@ -452,9 +479,12 @@ export class AgentManager {
|
|
|
452
479
|
record.abortController?.abort();
|
|
453
480
|
record.status = "stopped";
|
|
454
481
|
record.completedAt = Date.now();
|
|
482
|
+
if (this.backgroundAgentIds.has(record.id))
|
|
483
|
+
this.completedBackgroundCallbacks.add(record.id);
|
|
455
484
|
count++;
|
|
456
485
|
}
|
|
457
486
|
}
|
|
487
|
+
this.runningBackground = 0;
|
|
458
488
|
return count;
|
|
459
489
|
}
|
|
460
490
|
/** Wait for all running and queued agents to complete (including queued ones). */
|
|
@@ -480,6 +510,8 @@ export class AgentManager {
|
|
|
480
510
|
record.session?.dispose();
|
|
481
511
|
}
|
|
482
512
|
this.agents.clear();
|
|
513
|
+
this.backgroundAgentIds.clear();
|
|
514
|
+
this.completedBackgroundCallbacks.clear();
|
|
483
515
|
// Prune any orphaned git worktrees (crash recovery)
|
|
484
516
|
try {
|
|
485
517
|
pruneWorktrees(process.cwd());
|
package/dist/agent-runner.d.ts
CHANGED
|
@@ -15,10 +15,13 @@ export declare const SUBAGENT_TOOL_NAMES: {
|
|
|
15
15
|
readonly AGENT: "Agent";
|
|
16
16
|
readonly GET_RESULT: "get_subagent_result";
|
|
17
17
|
readonly STEER: "steer_subagent";
|
|
18
|
+
readonly LIST_SUBAGENTS: "list_subagents";
|
|
19
|
+
readonly CLEAR_SUBAGENTS: "clear_subagents";
|
|
18
20
|
readonly LIST_MODELS: "list_models";
|
|
19
21
|
};
|
|
20
22
|
export declare function getCurrentExtensionDepth(): number;
|
|
21
23
|
export declare function getCurrentExtensionAgentId(): string | undefined;
|
|
24
|
+
export declare function getCurrentExtensionParentAgentId(): string | undefined;
|
|
22
25
|
/**
|
|
23
26
|
* Canonical name of an extension for `extensions: [...]` allowlist matching.
|
|
24
27
|
* Lowercased — extension names match case-insensitively so `extensions: [Mcp]`
|
package/dist/agent-runner.js
CHANGED
|
@@ -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
|
/**
|
|
@@ -53,7 +55,11 @@ function getLoadingExtensionAgentId() {
|
|
|
53
55
|
const value = globalThis[EXTENSION_DEPTH_KEY];
|
|
54
56
|
return value && typeof value.agentId === "string" ? value.agentId : undefined;
|
|
55
57
|
}
|
|
56
|
-
|
|
58
|
+
function getLoadingExtensionParentAgentId() {
|
|
59
|
+
const value = globalThis[EXTENSION_DEPTH_KEY];
|
|
60
|
+
return value && typeof value.parentAgentId === "string" ? value.parentAgentId : undefined;
|
|
61
|
+
}
|
|
62
|
+
async function withLoadingExtensionDepth(depth, agentId, parentAgentId, fn) {
|
|
57
63
|
const previous = extensionDepthLoadChain;
|
|
58
64
|
let release;
|
|
59
65
|
extensionDepthLoadChain = new Promise((resolve) => {
|
|
@@ -63,7 +69,7 @@ async function withLoadingExtensionDepth(depth, agentId, fn) {
|
|
|
63
69
|
try {
|
|
64
70
|
const g = globalThis;
|
|
65
71
|
const prev = g[EXTENSION_DEPTH_KEY];
|
|
66
|
-
g[EXTENSION_DEPTH_KEY] = { depth, agentId };
|
|
72
|
+
g[EXTENSION_DEPTH_KEY] = { depth, agentId, parentAgentId };
|
|
67
73
|
try {
|
|
68
74
|
return await fn();
|
|
69
75
|
}
|
|
@@ -84,6 +90,9 @@ export function getCurrentExtensionDepth() {
|
|
|
84
90
|
export function getCurrentExtensionAgentId() {
|
|
85
91
|
return getLoadingExtensionAgentId();
|
|
86
92
|
}
|
|
93
|
+
export function getCurrentExtensionParentAgentId() {
|
|
94
|
+
return getLoadingExtensionParentAgentId();
|
|
95
|
+
}
|
|
87
96
|
/**
|
|
88
97
|
* Canonical name of an extension for `extensions: [...]` allowlist matching.
|
|
89
98
|
* Lowercased — extension names match case-insensitively so `extensions: [Mcp]`
|
|
@@ -442,7 +451,7 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
442
451
|
systemPromptOverride: () => systemPrompt,
|
|
443
452
|
appendSystemPromptOverride: () => [],
|
|
444
453
|
});
|
|
445
|
-
await withLoadingExtensionDepth(depth, options.agentId, () => loader.reload());
|
|
454
|
+
await withLoadingExtensionDepth(depth, options.agentId, options.parentAgentId, () => loader.reload());
|
|
446
455
|
// Plain entries in `tools:` are expected to be built-in names (extension tools
|
|
447
456
|
// go through `ext:`), so an unknown name there is unambiguously a typo. Previously
|
|
448
457
|
// this produced a silently broken agent (#75) — pi-mono accepted the bogus name
|
|
@@ -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;
|
|
@@ -0,0 +1,206 @@
|
|
|
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 { formatMs, getDisplayName } from "./ui/agent-widget.js";
|
|
10
|
+
import { getLifetimeTotal } from "./usage.js";
|
|
11
|
+
const NAMESPACE = "subagents";
|
|
12
|
+
const MODULE_ID = "subagents-overview";
|
|
13
|
+
const DATA_EVENT = "subagents:rows";
|
|
14
|
+
const INVALIDATE_DEBOUNCE_MS = 500;
|
|
15
|
+
/**
|
|
16
|
+
* Build a row for the management-modal table from an AgentRecord.
|
|
17
|
+
*/
|
|
18
|
+
function buildAgentRow(record) {
|
|
19
|
+
const durationMs = record.completedAt
|
|
20
|
+
? record.completedAt - record.startedAt
|
|
21
|
+
: Date.now() - record.startedAt;
|
|
22
|
+
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
23
|
+
return {
|
|
24
|
+
id: record.id,
|
|
25
|
+
type: getDisplayName(record.type),
|
|
26
|
+
description: record.description ?? "",
|
|
27
|
+
status: record.status,
|
|
28
|
+
toolUses: record.toolUses ?? 0,
|
|
29
|
+
tokens: totalTokens > 0 ? formatTokenCount(totalTokens) : "—",
|
|
30
|
+
duration: formatMs(durationMs),
|
|
31
|
+
outputFile: record.outputFile ?? "",
|
|
32
|
+
startedAt: record.startedAt,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function formatTokenCount(n) {
|
|
36
|
+
if (n >= 1_000_000)
|
|
37
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
38
|
+
if (n >= 1_000)
|
|
39
|
+
return `${(n / 1_000).toFixed(1)}k`;
|
|
40
|
+
return String(n);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Register all dashboard UI integration points.
|
|
44
|
+
* Call once during extension setup when pi.events is available.
|
|
45
|
+
*/
|
|
46
|
+
export function registerDashboardModules(pi, manager) {
|
|
47
|
+
if (!pi.events)
|
|
48
|
+
return;
|
|
49
|
+
let invalidateTimer;
|
|
50
|
+
function scheduleInvalidate() {
|
|
51
|
+
if (invalidateTimer)
|
|
52
|
+
return;
|
|
53
|
+
invalidateTimer = setTimeout(() => {
|
|
54
|
+
invalidateTimer = undefined;
|
|
55
|
+
pi.events.emit("ui:invalidate", {});
|
|
56
|
+
}, INVALIDATE_DEBOUNCE_MS);
|
|
57
|
+
}
|
|
58
|
+
// ── 1. Module Discovery (ui:list-modules) ──────────────────────────
|
|
59
|
+
pi.events.on("ui:list-modules", ((probe) => {
|
|
60
|
+
const agents = manager.listAgents();
|
|
61
|
+
const running = agents.filter(a => a.status === "running").length;
|
|
62
|
+
const completed = agents.filter(a => a.status === "completed").length;
|
|
63
|
+
const total = agents.length;
|
|
64
|
+
// Footer-segment: running/completed counts
|
|
65
|
+
const parts = [];
|
|
66
|
+
if (running > 0)
|
|
67
|
+
parts.push(`● ${running} running`);
|
|
68
|
+
if (completed > 0)
|
|
69
|
+
parts.push(`✓ ${completed} done`);
|
|
70
|
+
if (total === 0)
|
|
71
|
+
parts.push("No agents");
|
|
72
|
+
probe.modules.push({
|
|
73
|
+
kind: "footer-segment",
|
|
74
|
+
namespace: NAMESPACE,
|
|
75
|
+
id: "agent-counts",
|
|
76
|
+
payload: {
|
|
77
|
+
text: parts.join(" · "),
|
|
78
|
+
tooltip: `${total} total agents (${running} running, ${completed} completed)`,
|
|
79
|
+
icon: "mdiRobot",
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
// Management-modal: subagent overview table
|
|
83
|
+
probe.modules.push({
|
|
84
|
+
kind: "management-modal",
|
|
85
|
+
id: MODULE_ID,
|
|
86
|
+
command: "/subagents",
|
|
87
|
+
title: "Subagents",
|
|
88
|
+
description: "View and manage background subagents",
|
|
89
|
+
icon: "mdiRobotOutline",
|
|
90
|
+
category: "subagents",
|
|
91
|
+
view: {
|
|
92
|
+
kind: "table",
|
|
93
|
+
dataEvent: DATA_EVENT,
|
|
94
|
+
rowKey: "id",
|
|
95
|
+
fields: [
|
|
96
|
+
{ key: "id", label: "ID", kind: "text", width: 120 },
|
|
97
|
+
{ key: "type", label: "Type", kind: "text", width: 100 },
|
|
98
|
+
{ key: "description", label: "Description", kind: "text" },
|
|
99
|
+
{ key: "status", label: "Status", kind: "text", width: 90 },
|
|
100
|
+
{ key: "toolUses", label: "Tools", kind: "number", width: 60 },
|
|
101
|
+
{ key: "tokens", label: "Tokens", kind: "text", width: 80 },
|
|
102
|
+
{ key: "duration", label: "Duration", kind: "text", width: 80 },
|
|
103
|
+
],
|
|
104
|
+
rowActions: [
|
|
105
|
+
{
|
|
106
|
+
id: "view-result",
|
|
107
|
+
label: "View Result",
|
|
108
|
+
icon: "mdiEye",
|
|
109
|
+
variant: "primary",
|
|
110
|
+
event: "subagents:ui:view-result",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "abort",
|
|
114
|
+
label: "Abort",
|
|
115
|
+
icon: "mdiStop",
|
|
116
|
+
variant: "danger",
|
|
117
|
+
event: "subagents:ui:abort",
|
|
118
|
+
confirm: "Abort this running agent?",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "steer",
|
|
122
|
+
label: "Steer",
|
|
123
|
+
icon: "mdiMessageArrowRight",
|
|
124
|
+
variant: "secondary",
|
|
125
|
+
event: "subagents:ui:steer",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
emptyState: "No subagents have been spawned in this session.",
|
|
129
|
+
actions: [
|
|
130
|
+
{
|
|
131
|
+
id: "refresh",
|
|
132
|
+
label: "Refresh",
|
|
133
|
+
icon: "mdiRefresh",
|
|
134
|
+
variant: "secondary",
|
|
135
|
+
event: "subagents:ui:refresh",
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}));
|
|
141
|
+
// ── 2. Data Fetch Handler ──────────────────────────────────────────
|
|
142
|
+
pi.events.on(DATA_EVENT, ((data) => {
|
|
143
|
+
const agents = manager.listAgents();
|
|
144
|
+
data.items = agents.map(buildAgentRow);
|
|
145
|
+
}));
|
|
146
|
+
// ── 3. Action Handlers ─────────────────────────────────────────────
|
|
147
|
+
// Refresh: just invalidate to re-probe + re-fetch
|
|
148
|
+
pi.events.on("subagents:ui:refresh", (() => {
|
|
149
|
+
scheduleInvalidate();
|
|
150
|
+
}));
|
|
151
|
+
// View Result: emit the result as a toast so the dashboard shows it
|
|
152
|
+
pi.events.on("subagents:ui:view-result", ((data) => {
|
|
153
|
+
const agentId = data.params?.id ?? data.id;
|
|
154
|
+
const record = manager.getRecord(agentId);
|
|
155
|
+
if (!record) {
|
|
156
|
+
pi.events.emit("ui:invalidate", { id: MODULE_ID });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const resultText = record.result?.trim() || "No output yet.";
|
|
160
|
+
const preview = resultText.length > 2000
|
|
161
|
+
? resultText.slice(0, 2000) + "\n…(truncated)"
|
|
162
|
+
: resultText;
|
|
163
|
+
pi.events.emit("ui:invalidate", { id: MODULE_ID });
|
|
164
|
+
// Return result as items so it shows in a detail view
|
|
165
|
+
data.items = [{
|
|
166
|
+
id: record.id,
|
|
167
|
+
type: getDisplayName(record.type),
|
|
168
|
+
description: record.description,
|
|
169
|
+
status: record.status,
|
|
170
|
+
result: preview,
|
|
171
|
+
outputFile: record.outputFile ?? "",
|
|
172
|
+
}];
|
|
173
|
+
}));
|
|
174
|
+
// Abort: signal the agent to stop
|
|
175
|
+
pi.events.on("subagents:ui:abort", ((data) => {
|
|
176
|
+
const agentId = data.params?.id ?? data.id;
|
|
177
|
+
const record = manager.getRecord(agentId);
|
|
178
|
+
if (record?.session) {
|
|
179
|
+
// Use the session's abort mechanism
|
|
180
|
+
try {
|
|
181
|
+
record.session.dispose?.();
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Ignore disposal errors
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
scheduleInvalidate();
|
|
188
|
+
}));
|
|
189
|
+
// Steer: for now just invalidate — full steer requires a prompt input
|
|
190
|
+
// which the management-modal form view could support in the future
|
|
191
|
+
pi.events.on("subagents:ui:steer", ((_data) => {
|
|
192
|
+
// TODO: Could open a form view for entering the steer message
|
|
193
|
+
scheduleInvalidate();
|
|
194
|
+
}));
|
|
195
|
+
// ── 4. Invalidate on agent lifecycle events ────────────────────────
|
|
196
|
+
const lifecycleEvents = [
|
|
197
|
+
"subagents:created",
|
|
198
|
+
"subagents:started",
|
|
199
|
+
"subagents:completed",
|
|
200
|
+
"subagents:failed",
|
|
201
|
+
"subagents:compacted",
|
|
202
|
+
];
|
|
203
|
+
for (const event of lifecycleEvents) {
|
|
204
|
+
pi.events.on(event, (() => scheduleInvalidate()));
|
|
205
|
+
}
|
|
206
|
+
}
|