@gotgenes/pi-subagents 2.0.0 → 3.0.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/CHANGELOG.md +23 -0
- package/README.md +8 -92
- package/docs/plans/0049-remove-group-join-output-file-rpc.md +163 -0
- package/package.json +1 -1
- package/src/index.ts +9 -153
- package/src/invocation-config.ts +1 -5
- package/src/settings.ts +0 -10
- package/src/types.ts +1 -6
- package/src/cross-extension-rpc.ts +0 -95
- package/src/group-join.ts +0 -141
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v2.0.0...pi-subagents-v3.0.0) (2026-05-17)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### ⚠ BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* The JoinMode type and defaultJoinMode setting are removed from the public settings interface.
|
|
14
|
+
* The join-mode setting (smart/async/group) is removed. Background agents always notify individually on completion.
|
|
15
|
+
* The subagents:ready event is no longer emitted. Extensions should use the typed SubagentsAPI ([#48](https://github.com/gotgenes/pi-packages/issues/48)) instead of event-based RPC discovery.
|
|
16
|
+
* The subagents:rpc:ping, subagents:rpc:spawn, and subagents:rpc:stop event channels are no longer registered. Use the typed SubagentsAPI via Symbol.for() instead.
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* remove group-join and cross-extension-rpc source ([b7d7f21](https://github.com/gotgenes/pi-packages/commit/b7d7f21af265e2ff95f0534f5c0b51f71b8f1e7f))
|
|
21
|
+
* remove group-join wiring from index.ts ([4e2dc7f](https://github.com/gotgenes/pi-packages/commit/4e2dc7f8a98e441308f229745dfc42d09784d786))
|
|
22
|
+
* remove join-mode types and settings ([1d98793](https://github.com/gotgenes/pi-packages/commit/1d98793eb85aa3d9815274aa443c34bb4434b6f9))
|
|
23
|
+
* remove RPC wiring from index.ts ([3a960af](https://github.com/gotgenes/pi-packages/commit/3a960af8f6bb83219497d6229d12c6859cf3eb71))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Documentation
|
|
27
|
+
|
|
28
|
+
* plan removal of group-join, output-file, and ad-hoc RPC ([#49](https://github.com/gotgenes/pi-packages/issues/49)) ([853a97f](https://github.com/gotgenes/pi-packages/commit/853a97f1c47051868b2ffddd7d5509f765a80d07))
|
|
29
|
+
* remove group-join and RPC from README and AGENTS ([9f65f7a](https://github.com/gotgenes/pi-packages/commit/9f65f7a21611af8dead00a5fb34d7d10f3d6ab43))
|
|
30
|
+
|
|
8
31
|
## [2.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v1.0.2...pi-subagents-v2.0.0) (2026-05-17)
|
|
9
32
|
|
|
10
33
|
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
|
|
|
17
17
|
## Features
|
|
18
18
|
|
|
19
19
|
- **Claude Code look & feel** — same tool names, calling conventions, and UI patterns (`Agent`, `get_subagent_result`, `steer_subagent`) — feels native
|
|
20
|
-
- **Parallel background agents** — spawn multiple agents that run concurrently with automatic queuing (configurable concurrency limit, default 4) and
|
|
20
|
+
- **Parallel background agents** — spawn multiple agents that run concurrently with automatic queuing (configurable concurrency limit, default 4) and individual completion notifications
|
|
21
21
|
- **Live widget UI** — persistent above-editor widget with animated spinners, live tool activity, token counts, and colored status icons
|
|
22
22
|
- **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)
|
|
23
23
|
- **Custom agent types** — define agents in `.pi/agents/<name>.md` with YAML frontmatter: custom system prompts, model selection, thinking levels, tool restrictions
|
|
@@ -31,9 +31,9 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
|
|
|
31
31
|
- **Git worktree isolation** — run agents in isolated repo copies; changes auto-committed to branches on completion
|
|
32
32
|
- **Skill preloading** — inject named skills into agent system prompts, discovered from `.pi/skills/`, `.agents/skills/`, and global locations (Pi-standard `<name>/SKILL.md` directory layout supported)
|
|
33
33
|
- **Tool denylist** — block specific tools via `disallowed_tools` frontmatter
|
|
34
|
-
- **Styled completion notifications** — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML. Expandable to show full output
|
|
34
|
+
- **Styled completion notifications** — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML. Expandable to show full output
|
|
35
35
|
- **Event bus** — lifecycle events (`subagents:created`, `started`, `completed`, `failed`, `steered`, `compacted`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
|
|
38
38
|
## Install
|
|
39
39
|
|
|
@@ -103,7 +103,7 @@ Background agent completion notifications render as styled boxes:
|
|
|
103
103
|
transcript: .pi/output/agent-abc123.jsonl
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
The LLM receives structured `<task-notification>` XML for parsing, while the user sees the themed visual.
|
|
107
107
|
|
|
108
108
|
## Default Agent Types
|
|
109
109
|
|
|
@@ -232,7 +232,7 @@ The `/agents` command opens an interactive menu:
|
|
|
232
232
|
Running agents (2) — 1 running, 1 done ← only shown when agents exist
|
|
233
233
|
Agent types (6) ← unified list: defaults + custom
|
|
234
234
|
Create new agent ← manual wizard or AI-generated
|
|
235
|
-
Settings ← max concurrency, max turns, grace turns
|
|
235
|
+
Settings ← max concurrency, max turns, grace turns
|
|
236
236
|
```
|
|
237
237
|
|
|
238
238
|
- **Agent types** — unified list with source indicators: `•` (project), `◦` (global), `✕` (disabled). Select an agent to manage it:
|
|
@@ -243,7 +243,7 @@ Settings ← max concurrency, max turns, grac
|
|
|
243
243
|
- **Eject** — writes the embedded default config as a `.md` file to project or personal location, so you can customize it
|
|
244
244
|
- **Disable/Enable** — toggle agent availability. Disabled agents stay visible in the list (marked `✕`) and can be re-enabled
|
|
245
245
|
- **Create new agent** — choose project/personal location, then manual wizard (step-by-step prompts for name, tools, model, thinking, system prompt) or AI-generated (describe what the agent should do and a sub-agent writes the `.md` file). Any name is allowed, including default agent names (overrides them)
|
|
246
|
-
- **Settings** — configure max concurrency, default max turns, grace turns
|
|
246
|
+
- **Settings** — configure max concurrency, default max turns, and grace turns at runtime
|
|
247
247
|
|
|
248
248
|
## Graceful Max Turns
|
|
249
249
|
|
|
@@ -266,29 +266,14 @@ Background agents are subject to a configurable concurrency limit (default: 4).
|
|
|
266
266
|
|
|
267
267
|
Foreground agents bypass the queue — they block the parent anyway.
|
|
268
268
|
|
|
269
|
-
## Join Strategies
|
|
270
|
-
|
|
271
|
-
When background agents complete, they notify the main agent. The **join mode** controls how these notifications are delivered. It applies only to background agents.
|
|
272
|
-
|
|
273
|
-
| Mode | Behavior |
|
|
274
|
-
|------|----------|
|
|
275
|
-
| `smart` (default) | 2+ background agents spawned in the same turn are auto-grouped into a single consolidated notification. Solo agents notify individually. |
|
|
276
|
-
| `async` | Each agent sends its own notification on completion (original behavior). Best when results need incremental processing. |
|
|
277
|
-
| `group` | Force grouping even when spawning a single agent. Useful when you know more agents will follow. |
|
|
278
|
-
|
|
279
|
-
**Timeout behavior:** When agents are grouped, a 30-second timeout starts after the first agent completes. If not all agents finish in time, a partial notification is sent with completed results and remaining agents continue with a shorter 15-second re-batch window for stragglers.
|
|
280
|
-
|
|
281
|
-
**Configuration:**
|
|
282
|
-
- Configure join mode in `/agents` → Settings → Join mode
|
|
283
|
-
|
|
284
269
|
## Persistent Settings
|
|
285
270
|
|
|
286
|
-
Runtime tuning values set via `/agents` → Settings (max concurrency, default max turns, grace turns
|
|
271
|
+
Runtime tuning values set via `/agents` → Settings (max concurrency, default max turns, grace turns) persist across pi restarts. Two files, merged on load:
|
|
287
272
|
|
|
288
273
|
- **Global:** `~/.pi/agent/subagents.json` — your machine-wide defaults. Edit by hand; the `/agents` menu never writes here.
|
|
289
274
|
- **Project:** `<cwd>/.pi/subagents.json` — per-project overrides. Written by `/agents` → Settings.
|
|
290
275
|
|
|
291
|
-
**Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `4`, default max turns unlimited, grace turns `5
|
|
276
|
+
**Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `4`, default max turns unlimited, grace turns `5`).
|
|
292
277
|
|
|
293
278
|
**Example — global defaults for a beefy machine:**
|
|
294
279
|
|
|
@@ -318,78 +303,11 @@ Agent lifecycle events are emitted via `pi.events.emit()` so other extensions ca
|
|
|
318
303
|
| `subagents:failed` | Agent errored, stopped, or aborted | same as completed + `error`, `status` |
|
|
319
304
|
| `subagents:steered` | Steering message sent | `id`, `message` |
|
|
320
305
|
| `subagents:compacted` | Agent's session successfully compacted | `id`, `type`, `description`, `reason` (`"manual"` / `"threshold"` / `"overflow"`), `tokensBefore`, `compactionCount` |
|
|
321
|
-
| `subagents:ready` | Extension loaded and RPC handlers registered | — |
|
|
322
306
|
| `subagents:settings_loaded` | Persisted settings applied at extension init | `settings` (merged global + project) |
|
|
323
307
|
| `subagents:settings_changed` | `/agents` → Settings mutation was applied | `settings`, `persisted` (`boolean` — `false` on write failure) |
|
|
324
308
|
|
|
325
309
|
`tokens.total` = `input + output + cacheWrite`. `cacheRead` is excluded — each turn's `cacheRead` is the cumulative cached prefix re-read on that one API call, so summing per-message would over-count it. Use `contextUsage.percent` (surfaced as `(NN%)` in the widget) for current context size.
|
|
326
310
|
|
|
327
|
-
## Cross-Extension RPC
|
|
328
|
-
|
|
329
|
-
Other pi extensions can spawn and stop subagents programmatically via the `pi.events` event bus, without importing this package directly.
|
|
330
|
-
|
|
331
|
-
All RPC replies use a standardized envelope: `{ success: true, data?: T }` on success, `{ success: false, error: string }` on failure.
|
|
332
|
-
|
|
333
|
-
### Discovery
|
|
334
|
-
|
|
335
|
-
Listen for `subagents:ready` to know when RPC handlers are available:
|
|
336
|
-
|
|
337
|
-
```typescript
|
|
338
|
-
pi.events.on("subagents:ready", () => {
|
|
339
|
-
// RPC handlers are registered — safe to call ping/spawn/stop
|
|
340
|
-
});
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
### Ping
|
|
344
|
-
|
|
345
|
-
Check if the subagents extension is loaded and get the protocol version:
|
|
346
|
-
|
|
347
|
-
```typescript
|
|
348
|
-
const requestId = crypto.randomUUID();
|
|
349
|
-
const unsub = pi.events.on(`subagents:rpc:ping:reply:${requestId}`, (reply) => {
|
|
350
|
-
unsub();
|
|
351
|
-
if (reply.success) console.log("Protocol version:", reply.data.version);
|
|
352
|
-
});
|
|
353
|
-
pi.events.emit("subagents:rpc:ping", { requestId });
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
### Spawn
|
|
357
|
-
|
|
358
|
-
Spawn a subagent and receive its ID:
|
|
359
|
-
|
|
360
|
-
```typescript
|
|
361
|
-
const requestId = crypto.randomUUID();
|
|
362
|
-
const unsub = pi.events.on(`subagents:rpc:spawn:reply:${requestId}`, (reply) => {
|
|
363
|
-
unsub();
|
|
364
|
-
if (!reply.success) {
|
|
365
|
-
console.error("Spawn failed:", reply.error);
|
|
366
|
-
} else {
|
|
367
|
-
console.log("Agent ID:", reply.data.id);
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
pi.events.emit("subagents:rpc:spawn", {
|
|
371
|
-
requestId,
|
|
372
|
-
type: "general-purpose",
|
|
373
|
-
prompt: "Do something useful",
|
|
374
|
-
options: { description: "My task", run_in_background: true },
|
|
375
|
-
});
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
### Stop
|
|
379
|
-
|
|
380
|
-
Stop a running agent by ID:
|
|
381
|
-
|
|
382
|
-
```typescript
|
|
383
|
-
const requestId = crypto.randomUUID();
|
|
384
|
-
const unsub = pi.events.on(`subagents:rpc:stop:reply:${requestId}`, (reply) => {
|
|
385
|
-
unsub();
|
|
386
|
-
if (!reply.success) console.error("Stop failed:", reply.error);
|
|
387
|
-
});
|
|
388
|
-
pi.events.emit("subagents:rpc:stop", { requestId, agentId: "agent-id-here" });
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
Reply channels are scoped per `requestId`, so concurrent requests don't interfere.
|
|
392
|
-
|
|
393
311
|
## Persistent Agent Memory
|
|
394
312
|
|
|
395
313
|
Agents can have persistent memory across sessions. Set `memory` in frontmatter to enable:
|
|
@@ -477,8 +395,6 @@ src/
|
|
|
477
395
|
agent-types.ts # Unified agent registry (defaults + user), tool name resolution
|
|
478
396
|
agent-runner.ts # Session creation, execution, graceful max_turns, steer/resume
|
|
479
397
|
agent-manager.ts # Agent lifecycle, concurrency queue, completion notifications
|
|
480
|
-
cross-extension-rpc.ts # RPC handlers for cross-extension spawn/ping via pi.events
|
|
481
|
-
group-join.ts # Group join manager: batched completion notifications with timeout
|
|
482
398
|
custom-agents.ts # Load user-defined agents from .pi/agents/*.md
|
|
483
399
|
memory.ts # Persistent agent memory (resolve, read, build prompt blocks)
|
|
484
400
|
skill-loader.ts # Preload skills (Pi-standard + Agent Skills spec layouts)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 49
|
|
3
|
+
issue_title: "feat: remove group-join, output-file, and ad-hoc RPC"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Remove group-join and ad-hoc RPC
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
Two optional subsystems remain in the core that are either consumer concerns or replaced by the typed `SubagentsAPI` (#48):
|
|
11
|
+
|
|
12
|
+
1. **Group join** — grouped completion notifications add batching complexity for marginal UX benefit when individual notifications are sufficient.
|
|
13
|
+
2. **Ad-hoc RPC** — untyped RPC over `pi.events` with per-request reply channels is replaced by the typed `SubagentsAPI` published via `Symbol.for()`.
|
|
14
|
+
|
|
15
|
+
Removing these two subsystems reduces the core's surface area by ~220 source LOC, eliminates the join-mode settings system, and simplifies the completion callback path in `index.ts`.
|
|
16
|
+
|
|
17
|
+
The **output file** subsystem (`src/output-file.ts`) is retained — it provides valuable post-hoc debugging transcripts for subagent sessions.
|
|
18
|
+
A separate issue (#61) tracks porting it to Pi's official JSONL session format.
|
|
19
|
+
|
|
20
|
+
## Goals
|
|
21
|
+
|
|
22
|
+
- Delete `src/group-join.ts` (141 LOC) and `src/cross-extension-rpc.ts` (80 LOC).
|
|
23
|
+
- Delete `test/cross-extension-rpc.test.ts`.
|
|
24
|
+
- Remove all group-join wiring from `index.ts`: `GroupJoinManager` instantiation, batch tracking (`currentBatchAgents`, `finalizeBatch`, `batchFinalizeTimer`, `batchCounter`), join-mode configuration state, and settings menu entry.
|
|
25
|
+
- Remove all RPC wiring from `index.ts`: `registerRpcHandlers` import, `currentCtx` capture for RPC, `unsubPing/Spawn/Stop` teardown, and the `subagents:ready` broadcast.
|
|
26
|
+
- Remove `JoinMode` type and `joinMode`/`groupId` fields from `types.ts`.
|
|
27
|
+
- Remove `NotificationDetails.others` field (no longer needed without grouping).
|
|
28
|
+
- Remove `defaultJoinMode` from `SubagentsSettings` and `SettingsAppliers` in `settings.ts`.
|
|
29
|
+
- Remove `resolveJoinMode` from `invocation-config.ts`.
|
|
30
|
+
- Simplify the completion callback in `index.ts` to always send an individual notification.
|
|
31
|
+
- Preserve existing lifecycle events (`subagents:started`, `subagents:completed`, `subagents:failed`) — they are already emitted.
|
|
32
|
+
- This is a **breaking change** (`feat!:`) — the join-mode setting is removed and RPC channels are no longer registered.
|
|
33
|
+
|
|
34
|
+
## Non-Goals
|
|
35
|
+
|
|
36
|
+
- Removing the `Symbol.for("pi-subagents:manager")` global accessor — that belongs to #48 (implement SubagentsAPI), which replaces it with the typed `publishSubagentsAPI()`.
|
|
37
|
+
- Removing `bypassQueue` from `SpawnOptions` — it remains useful for the typed API.
|
|
38
|
+
- Providing migration shims for RPC consumers — none known.
|
|
39
|
+
- Removing or modifying `src/output-file.ts` — retained for debugging value; porting to Pi's JSONL format is tracked in #61.
|
|
40
|
+
|
|
41
|
+
## Background
|
|
42
|
+
|
|
43
|
+
The architecture doc marks these modules as "removing."
|
|
44
|
+
Issue #52 (remove scheduled subagents) is already implemented and merged.
|
|
45
|
+
Issue #48 (implement SubagentsAPI) depends on RPC removal here since the typed API replaces the untyped RPC.
|
|
46
|
+
|
|
47
|
+
The `index.ts` file is the primary wiring layer affected.
|
|
48
|
+
The completion callback currently routes through `groupJoin.onAgentComplete()` and only falls through to `sendIndividualNudge()` on `'pass'`.
|
|
49
|
+
After this change, the callback always calls `sendIndividualNudge()` directly, which simplifies the control flow from ~90 lines of group/batch logic down to a single function call.
|
|
50
|
+
|
|
51
|
+
The `currentCtx` variable captured in `session_start` exists solely for the RPC spawn handler.
|
|
52
|
+
After RPC removal, session lifecycle hooks simplify: `session_start` only needs `manager.clearCompleted()`.
|
|
53
|
+
|
|
54
|
+
## Design Overview
|
|
55
|
+
|
|
56
|
+
This is a pure deletion change with minor simplification of the remaining completion path.
|
|
57
|
+
|
|
58
|
+
After removal, the background-agent completion flow becomes:
|
|
59
|
+
|
|
60
|
+
1. `AgentManager` calls `onComplete(record)`.
|
|
61
|
+
2. `onComplete` emits `subagents:completed` or `subagents:failed` on `pi.events`.
|
|
62
|
+
3. `onComplete` persists the record via `pi.appendEntry`.
|
|
63
|
+
4. If `record.resultConsumed`, clean up widget and return.
|
|
64
|
+
5. Otherwise, call `sendIndividualNudge(record)` — which schedules the notification with a 200ms debounce window (retained for `get_subagent_result` cancellation).
|
|
65
|
+
|
|
66
|
+
The `NotificationDetails` interface stays (individual notifications still use it) but loses the `others` field.
|
|
67
|
+
The `outputFile` field on `NotificationDetails` stays since output-file is retained.
|
|
68
|
+
|
|
69
|
+
### Settings changes
|
|
70
|
+
|
|
71
|
+
`SubagentsSettings` loses `defaultJoinMode`.
|
|
72
|
+
The settings menu loses the "Join mode" entry.
|
|
73
|
+
`snapshotSettings()` and `persistToastFor()` patterns are unchanged — they just carry one fewer field.
|
|
74
|
+
|
|
75
|
+
### Types changes
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// Remove from types.ts:
|
|
79
|
+
export type JoinMode = 'async' | 'group' | 'smart';
|
|
80
|
+
|
|
81
|
+
// Remove from AgentRecord:
|
|
82
|
+
groupId?: string;
|
|
83
|
+
joinMode?: JoinMode;
|
|
84
|
+
|
|
85
|
+
// Remove from NotificationDetails:
|
|
86
|
+
others?: NotificationDetails[];
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`AgentRecord.outputFile`, `outputCleanup`, and `toolCallId` are retained — they support the output-file subsystem which remains in scope.
|
|
90
|
+
|
|
91
|
+
## Module-Level Changes
|
|
92
|
+
|
|
93
|
+
### Delete
|
|
94
|
+
|
|
95
|
+
| File | Lines |
|
|
96
|
+
| ---------------------------------- | ----- |
|
|
97
|
+
| `src/group-join.ts` | 141 |
|
|
98
|
+
| `src/cross-extension-rpc.ts` | 80 |
|
|
99
|
+
| `test/cross-extension-rpc.test.ts` | ~220 |
|
|
100
|
+
|
|
101
|
+
### Modify
|
|
102
|
+
|
|
103
|
+
| File | Change |
|
|
104
|
+
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
105
|
+
| `src/index.ts` | Remove imports for group-join and RPC modules. Remove `GroupJoinManager` instantiation and the grouped-delivery callback (~30 lines). Remove batch tracking state and `finalizeBatch()` (~40 lines). Remove join-mode state (`defaultJoinMode`, `getDefaultJoinMode`, `setDefaultJoinMode`). Remove RPC registration, `currentCtx` capture, `unsubPing/Spawn/Stop` teardown. Remove `subagents:ready` emit. Remove "Join mode" settings menu entry and `snapshotSettings` join-mode field. Simplify `onComplete` callback to always call `sendIndividualNudge`. Remove the `currentBatchAgents` deferred-notification check. Remove join-mode resolution in background spawn path. |
|
|
106
|
+
| `src/types.ts` | Remove `JoinMode` type. Remove `groupId` and `joinMode` from `AgentRecord`. Remove `others` from `NotificationDetails`. |
|
|
107
|
+
| `src/settings.ts` | Remove `JoinMode` import. Remove `defaultJoinMode` from `SubagentsSettings`. Remove `setDefaultJoinMode` from `SettingsAppliers`. Remove `VALID_JOIN_MODES`. Remove sanitize clause and `applySettings` clause for `defaultJoinMode`. |
|
|
108
|
+
| `src/invocation-config.ts` | Remove `JoinMode` import. Remove `resolveJoinMode` export. |
|
|
109
|
+
| `README.md` | Remove "Cross-extension RPC" section. Remove join-mode documentation. Remove `subagents:ready` from events table. Update settings persistence paragraph to drop join-mode. |
|
|
110
|
+
| `.pi/skills/package-pi-subagents/SKILL.md` | Remove `cross-extension-rpc.ts` and `group-join.ts` from architecture diagram and module tables. Update `index.ts` description. |
|
|
111
|
+
|
|
112
|
+
## Test Impact Analysis
|
|
113
|
+
|
|
114
|
+
1. No new unit tests are needed — this is pure deletion.
|
|
115
|
+
2. `test/cross-extension-rpc.test.ts` becomes entirely redundant and is deleted.
|
|
116
|
+
3. `test/output-file.test.ts` is retained (output-file stays).
|
|
117
|
+
4. There is no dedicated `group-join.test.ts` — the group-join logic was only tested indirectly through integration.
|
|
118
|
+
5. Existing tests for `agent-manager`, `agent-runner`, `settings`, and `invocation-config` must be checked for references to `joinMode`, `groupId`, `defaultJoinMode`, or `resolveJoinMode` — any such references need updating.
|
|
119
|
+
6. The notification renderer test (if any) may reference `others` on `NotificationDetails` — check and remove.
|
|
120
|
+
|
|
121
|
+
## TDD Order
|
|
122
|
+
|
|
123
|
+
Since this is a removal (not a feature), the order is deletion-first with validation passes.
|
|
124
|
+
|
|
125
|
+
1. **Delete source files for both subsystems.**
|
|
126
|
+
Delete `src/group-join.ts`, `src/cross-extension-rpc.ts`.
|
|
127
|
+
Delete `test/cross-extension-rpc.test.ts`.
|
|
128
|
+
Commit: `feat!: remove group-join and cross-extension-rpc source`
|
|
129
|
+
|
|
130
|
+
2. **Remove RPC wiring from `index.ts`.**
|
|
131
|
+
Remove `registerRpcHandlers` import and call. Remove `currentCtx` state and RPC-related `session_start`/`session_shutdown` logic (keep `manager.clearCompleted()` call). Remove `unsubPing/Spawn/Stop` teardown. Remove `subagents:ready` emit.
|
|
132
|
+
Commit: `feat!: remove RPC wiring from index.ts`
|
|
133
|
+
|
|
134
|
+
3. **Remove group-join wiring from `index.ts`.**
|
|
135
|
+
Remove `GroupJoinManager` import and instantiation (including the grouped-delivery callback). Remove batch tracking (`currentBatchAgents`, `batchFinalizeTimer`, `batchCounter`, `finalizeBatch`). Remove `defaultJoinMode` state, `getDefaultJoinMode`, `setDefaultJoinMode`. Remove join-mode resolution in background spawn path. Remove "Join mode" settings menu entry. Remove `defaultJoinMode` from `snapshotSettings()`. Simplify the `onComplete` callback: remove `currentBatchAgents` check and `groupJoin.onAgentComplete()` routing — always call `sendIndividualNudge(record)`. Remove `setDefaultJoinMode` from `applyAndEmitLoaded` appliers.
|
|
136
|
+
Commit: `feat!: remove group-join wiring from index.ts`
|
|
137
|
+
|
|
138
|
+
4. **Clean up types, settings, and invocation-config.**
|
|
139
|
+
Remove `JoinMode` type from `types.ts`. Remove `groupId` and `joinMode` from `AgentRecord`. Remove `others` from `NotificationDetails`. Remove `defaultJoinMode` from `SubagentsSettings` and `SettingsAppliers` in `settings.ts`. Remove `VALID_JOIN_MODES` and sanitize/apply clauses. Remove `resolveJoinMode` and `JoinMode` import from `invocation-config.ts`.
|
|
140
|
+
Commit: `feat!: remove join-mode types and settings`
|
|
141
|
+
|
|
142
|
+
5. **Verify all tests pass and fix straggling references.**
|
|
143
|
+
Run `pnpm vitest run` and `pnpm run check`. Fix any test fixtures or assertions that reference removed fields (`joinMode`, `groupId`, `defaultJoinMode`, `resolveJoinMode`).
|
|
144
|
+
Commit (if fixes needed): `test: remove references to deleted subsystems from test fixtures`
|
|
145
|
+
|
|
146
|
+
6. **Update documentation.**
|
|
147
|
+
Update `README.md`: remove "Cross-extension RPC" section, join-mode documentation, `subagents:ready` event row. Update settings persistence paragraph.
|
|
148
|
+
Update `.pi/skills/package-pi-subagents/SKILL.md`: remove `cross-extension-rpc.ts` and `group-join.ts` from architecture diagram and module tables, update `index.ts` description.
|
|
149
|
+
Commit: `docs: remove group-join and RPC from README and AGENTS`
|
|
150
|
+
|
|
151
|
+
## Risks and Mitigations
|
|
152
|
+
|
|
153
|
+
| Risk | Mitigation |
|
|
154
|
+
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
155
|
+
| Missed references cause compile errors | `grep -rn 'group.join\|GroupJoin\|registerRpcHandlers\|resolveJoinMode\|JoinMode' src/` after each step. `pnpm run check` catches import errors. |
|
|
156
|
+
| Test fixtures reference removed fields | Step 5 explicitly scans for and fixes these. TypeScript `noEmit` check catches type mismatches. |
|
|
157
|
+
| `Symbol.for("pi-subagents:manager")` accidentally removed | Explicitly out of scope — this belongs to #48. The global accessor stays until the typed API replaces it. |
|
|
158
|
+
| Breaking change not communicated | `feat!:` commit prefix triggers a major version bump via release-please. |
|
|
159
|
+
| Settings file with `defaultJoinMode` on disk | `sanitize()` already drops unknown fields silently — once the field is removed from the interface, existing files just have an inert JSON key that is ignored on load. |
|
|
160
|
+
|
|
161
|
+
## Open Questions
|
|
162
|
+
|
|
163
|
+
None — the issue scope is fully specified and the architecture doc ratified these removals.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -12,20 +12,18 @@
|
|
|
12
12
|
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
|
-
import { defineTool, type ExtensionAPI, type ExtensionCommandContext,
|
|
15
|
+
import { defineTool, type ExtensionAPI, type ExtensionCommandContext, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { Text } from "@earendil-works/pi-tui";
|
|
17
17
|
import { Type } from "@sinclair/typebox";
|
|
18
18
|
import { AgentManager } from "./agent-manager.js";
|
|
19
19
|
import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
20
20
|
import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, resolveType } from "./agent-types.js";
|
|
21
|
-
import { registerRpcHandlers } from "./cross-extension-rpc.js";
|
|
22
21
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
23
|
-
import {
|
|
24
|
-
import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
|
|
22
|
+
import { resolveAgentInvocationConfig } from "./invocation-config.js";
|
|
25
23
|
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
26
24
|
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
27
25
|
import { applyAndEmitLoaded, type SubagentsSettings, saveAndEmitChanged } from "./settings.js";
|
|
28
|
-
import { type AgentConfig, type AgentInvocation, type AgentRecord, type
|
|
26
|
+
import { type AgentConfig, type AgentInvocation, type AgentRecord, type NotificationDetails, type SubagentType } from "./types.js";
|
|
29
27
|
import {
|
|
30
28
|
type AgentActivity,
|
|
31
29
|
type AgentDetails,
|
|
@@ -246,8 +244,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
246
244
|
return line;
|
|
247
245
|
}
|
|
248
246
|
|
|
249
|
-
|
|
250
|
-
return new Text(all.map(renderOne).join("\n"), 0, 0);
|
|
247
|
+
return new Text(renderOne(d), 0, 0);
|
|
251
248
|
}
|
|
252
249
|
);
|
|
253
250
|
|
|
@@ -307,40 +304,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
307
304
|
widget.update();
|
|
308
305
|
}
|
|
309
306
|
|
|
310
|
-
// ---- Group join manager ----
|
|
311
|
-
const groupJoin = new GroupJoinManager(
|
|
312
|
-
(records, partial) => {
|
|
313
|
-
for (const r of records) { agentActivity.delete(r.id); widget.markFinished(r.id); }
|
|
314
|
-
|
|
315
|
-
const groupKey = `group:${records.map(r => r.id).join(",")}`;
|
|
316
|
-
scheduleNudge(groupKey, () => {
|
|
317
|
-
// Re-check at send time
|
|
318
|
-
const unconsumed = records.filter(r => !r.resultConsumed);
|
|
319
|
-
if (unconsumed.length === 0) { widget.update(); return; }
|
|
320
|
-
|
|
321
|
-
const notifications = unconsumed.map(r => formatTaskNotification(r, 300)).join('\n\n');
|
|
322
|
-
const label = partial
|
|
323
|
-
? `${unconsumed.length} agent(s) finished (partial — others still running)`
|
|
324
|
-
: `${unconsumed.length} agent(s) finished`;
|
|
325
|
-
|
|
326
|
-
const [first, ...rest] = unconsumed;
|
|
327
|
-
const details = buildNotificationDetails(first, 300, agentActivity.get(first.id));
|
|
328
|
-
if (rest.length > 0) {
|
|
329
|
-
details.others = rest.map(r => buildNotificationDetails(r, 300, agentActivity.get(r.id)));
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
pi.sendMessage<NotificationDetails>({
|
|
333
|
-
customType: "subagent-notification",
|
|
334
|
-
content: `Background agent group completed: ${label}\n\n${notifications}\n\nUse get_subagent_result for full output.`,
|
|
335
|
-
display: true,
|
|
336
|
-
details,
|
|
337
|
-
}, { deliverAs: "followUp", triggerTurn: true });
|
|
338
|
-
});
|
|
339
|
-
widget.update();
|
|
340
|
-
},
|
|
341
|
-
30_000,
|
|
342
|
-
);
|
|
343
|
-
|
|
344
307
|
/** Helper: build event data for lifecycle events from an AgentRecord. */
|
|
345
308
|
function buildEventData(record: AgentRecord) {
|
|
346
309
|
const durationMs = record.completedAt ? record.completedAt - record.startedAt : Date.now() - record.startedAt;
|
|
@@ -366,7 +329,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
366
329
|
};
|
|
367
330
|
}
|
|
368
331
|
|
|
369
|
-
// Background completion:
|
|
332
|
+
// Background completion: emit lifecycle event and send individual nudge
|
|
370
333
|
const manager = new AgentManager((record) => {
|
|
371
334
|
// Emit lifecycle event based on terminal status
|
|
372
335
|
const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
|
|
@@ -392,19 +355,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
392
355
|
return;
|
|
393
356
|
}
|
|
394
357
|
|
|
395
|
-
|
|
396
|
-
// don't send an individual nudge — finalizeBatch will pick it up retroactively.
|
|
397
|
-
if (currentBatchAgents.some(a => a.id === record.id)) {
|
|
398
|
-
widget.update();
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const result = groupJoin.onAgentComplete(record);
|
|
403
|
-
if (result === 'pass') {
|
|
404
|
-
sendIndividualNudge(record);
|
|
405
|
-
}
|
|
406
|
-
// 'held' → do nothing, group will fire later
|
|
407
|
-
// 'delivered' → group callback already fired
|
|
358
|
+
sendIndividualNudge(record);
|
|
408
359
|
widget.update();
|
|
409
360
|
}, undefined, (record) => {
|
|
410
361
|
// Emit started event when agent transitions to running (including from queue)
|
|
@@ -436,12 +387,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
436
387
|
getRecord: (id: string) => manager.getRecord(id),
|
|
437
388
|
};
|
|
438
389
|
|
|
439
|
-
|
|
440
|
-
let currentCtx: ExtensionContext | undefined;
|
|
441
|
-
|
|
442
|
-
// Capture ctx from session_start for RPC spawn handler.
|
|
443
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
444
|
-
currentCtx = ctx;
|
|
390
|
+
pi.on("session_start", async (_event, _ctx) => {
|
|
445
391
|
manager.clearCompleted();
|
|
446
392
|
});
|
|
447
393
|
|
|
@@ -449,23 +395,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
449
395
|
manager.clearCompleted();
|
|
450
396
|
});
|
|
451
397
|
|
|
452
|
-
const { unsubPing: unsubPingRpc, unsubSpawn: unsubSpawnRpc, unsubStop: unsubStopRpc } = registerRpcHandlers({
|
|
453
|
-
events: pi.events,
|
|
454
|
-
pi,
|
|
455
|
-
getCtx: () => currentCtx,
|
|
456
|
-
manager,
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
// Broadcast readiness so extensions loaded after us can discover us
|
|
460
|
-
pi.events.emit("subagents:ready", {});
|
|
461
|
-
|
|
462
398
|
// On shutdown, abort all agents immediately and clean up.
|
|
463
399
|
// If the session is going down, there's nothing left to consume agent results.
|
|
464
400
|
pi.on("session_shutdown", async () => {
|
|
465
|
-
unsubSpawnRpc();
|
|
466
|
-
unsubStopRpc();
|
|
467
|
-
unsubPingRpc();
|
|
468
|
-
currentCtx = undefined;
|
|
469
401
|
delete (globalThis as any)[MANAGER_KEY];
|
|
470
402
|
manager.abortAll();
|
|
471
403
|
for (const timer of pendingNudges.values()) clearTimeout(timer);
|
|
@@ -476,55 +408,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
476
408
|
// Live widget: show running agents above editor
|
|
477
409
|
const widget = new AgentWidget(manager, agentActivity);
|
|
478
410
|
|
|
479
|
-
// ---- Join mode configuration ----
|
|
480
|
-
let defaultJoinMode: JoinMode = 'smart';
|
|
481
|
-
function getDefaultJoinMode(): JoinMode { return defaultJoinMode; }
|
|
482
|
-
function setDefaultJoinMode(mode: JoinMode) { defaultJoinMode = mode; }
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
// ---- Batch tracking for smart join mode ----
|
|
486
|
-
// Collects background agent IDs spawned in the current turn for smart grouping.
|
|
487
|
-
// Uses a debounced timer: each new agent resets the 100ms window so that all
|
|
488
|
-
// parallel tool calls (which may be dispatched across multiple microtasks by the
|
|
489
|
-
// framework) are captured in the same batch.
|
|
490
|
-
let currentBatchAgents: { id: string; joinMode: JoinMode }[] = [];
|
|
491
|
-
let batchFinalizeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
492
|
-
let batchCounter = 0;
|
|
493
|
-
|
|
494
|
-
/** Finalize the current batch: if 2+ smart-mode agents, register as a group. */
|
|
495
|
-
function finalizeBatch() {
|
|
496
|
-
batchFinalizeTimer = undefined;
|
|
497
|
-
const batchAgents = [...currentBatchAgents];
|
|
498
|
-
currentBatchAgents = [];
|
|
499
|
-
|
|
500
|
-
const smartAgents = batchAgents.filter(a => a.joinMode === 'smart' || a.joinMode === 'group');
|
|
501
|
-
if (smartAgents.length >= 2) {
|
|
502
|
-
const groupId = `batch-${++batchCounter}`;
|
|
503
|
-
const ids = smartAgents.map(a => a.id);
|
|
504
|
-
groupJoin.registerGroup(groupId, ids);
|
|
505
|
-
// Retroactively process agents that already completed during the debounce window.
|
|
506
|
-
// Their onComplete fired but was deferred (agent was in currentBatchAgents),
|
|
507
|
-
// so we feed them into the group now.
|
|
508
|
-
for (const id of ids) {
|
|
509
|
-
const record = manager.getRecord(id);
|
|
510
|
-
if (!record) continue;
|
|
511
|
-
record.groupId = groupId;
|
|
512
|
-
if (record.completedAt != null && !record.resultConsumed) {
|
|
513
|
-
groupJoin.onAgentComplete(record);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
} else {
|
|
517
|
-
// No group formed — send individual nudges for any agents that completed
|
|
518
|
-
// during the debounce window and had their notification deferred.
|
|
519
|
-
for (const { id } of batchAgents) {
|
|
520
|
-
const record = manager.getRecord(id);
|
|
521
|
-
if (record?.completedAt != null && !record.resultConsumed) {
|
|
522
|
-
sendIndividualNudge(record);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
411
|
// Grab UI context from first tool execution + clear lingering widget on new turn
|
|
529
412
|
pi.on("tool_execution_start", async (_event, ctx) => {
|
|
530
413
|
widget.setUICtx(ctx.ui as UICtx);
|
|
@@ -574,7 +457,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
574
457
|
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
575
458
|
setDefaultMaxTurns,
|
|
576
459
|
setGraceTurns,
|
|
577
|
-
setDefaultJoinMode,
|
|
578
460
|
},
|
|
579
461
|
(event, payload) => pi.events.emit(event, payload),
|
|
580
462
|
);
|
|
@@ -870,28 +752,15 @@ Guidelines:
|
|
|
870
752
|
return textResult(err instanceof Error ? err.message : String(err));
|
|
871
753
|
}
|
|
872
754
|
|
|
873
|
-
// Set output file
|
|
755
|
+
// Set output file synchronously after spawn, before the
|
|
874
756
|
// event loop yields — onSessionCreated is async so this is safe.
|
|
875
|
-
const joinMode = resolveJoinMode(defaultJoinMode, true);
|
|
876
757
|
const record = manager.getRecord(id);
|
|
877
|
-
if (record
|
|
878
|
-
record.joinMode = joinMode;
|
|
758
|
+
if (record) {
|
|
879
759
|
record.toolCallId = toolCallId;
|
|
880
760
|
record.outputFile = createOutputFilePath(ctx.cwd, id, ctx.sessionManager.getSessionId());
|
|
881
761
|
writeInitialEntry(record.outputFile, id, params.prompt, ctx.cwd);
|
|
882
762
|
}
|
|
883
763
|
|
|
884
|
-
if (joinMode == null || joinMode === 'async') {
|
|
885
|
-
// Foreground/no join mode or explicit async — not part of any batch
|
|
886
|
-
} else {
|
|
887
|
-
// smart or group — add to current batch
|
|
888
|
-
currentBatchAgents.push({ id, joinMode });
|
|
889
|
-
// Debounce: reset timer on each new agent so parallel tool calls
|
|
890
|
-
// dispatched across multiple event loop ticks are captured together
|
|
891
|
-
if (batchFinalizeTimer) clearTimeout(batchFinalizeTimer);
|
|
892
|
-
batchFinalizeTimer = setTimeout(finalizeBatch, 100);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
764
|
agentActivity.set(id, bgState);
|
|
896
765
|
widget.ensureTimer();
|
|
897
766
|
widget.update();
|
|
@@ -1678,7 +1547,6 @@ ${systemPrompt}
|
|
|
1678
1547
|
// normalizeMaxTurns() in agent-runner.ts (which maps 0 → undefined).
|
|
1679
1548
|
defaultMaxTurns: getDefaultMaxTurns() ?? 0,
|
|
1680
1549
|
graceTurns: getGraceTurns(),
|
|
1681
|
-
defaultJoinMode: getDefaultJoinMode(),
|
|
1682
1550
|
};
|
|
1683
1551
|
}
|
|
1684
1552
|
|
|
@@ -1687,7 +1555,6 @@ ${systemPrompt}
|
|
|
1687
1555
|
`Max concurrency (current: ${manager.getMaxConcurrent()})`,
|
|
1688
1556
|
`Default max turns (current: ${getDefaultMaxTurns() ?? "unlimited"})`,
|
|
1689
1557
|
`Grace turns (current: ${getGraceTurns()})`,
|
|
1690
|
-
`Join mode (current: ${getDefaultJoinMode()})`,
|
|
1691
1558
|
]);
|
|
1692
1559
|
if (!choice) return;
|
|
1693
1560
|
|
|
@@ -1727,17 +1594,6 @@ ${systemPrompt}
|
|
|
1727
1594
|
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
1728
1595
|
}
|
|
1729
1596
|
}
|
|
1730
|
-
} else if (choice.startsWith("Join mode")) {
|
|
1731
|
-
const val = await ctx.ui.select("Default join mode for background agents", [
|
|
1732
|
-
"smart — auto-group 2+ agents in same turn (default)",
|
|
1733
|
-
"async — always notify individually",
|
|
1734
|
-
"group — always group background agents",
|
|
1735
|
-
]);
|
|
1736
|
-
if (val) {
|
|
1737
|
-
const mode = val.split(" ")[0] as JoinMode;
|
|
1738
|
-
setDefaultJoinMode(mode);
|
|
1739
|
-
notifyApplied(ctx, `Default join mode set to ${mode}`);
|
|
1740
|
-
}
|
|
1741
1597
|
}
|
|
1742
1598
|
}
|
|
1743
1599
|
|
package/src/invocation-config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentConfig, IsolationMode,
|
|
1
|
+
import type { AgentConfig, IsolationMode, ThinkingLevel } from "./types.js";
|
|
2
2
|
|
|
3
3
|
interface AgentInvocationParams {
|
|
4
4
|
model?: string;
|
|
@@ -34,7 +34,3 @@ export function resolveAgentInvocationConfig(
|
|
|
34
34
|
isolation: agentConfig?.isolation ?? params.isolation,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
export function resolveJoinMode(defaultJoinMode: JoinMode, runInBackground: boolean): JoinMode | undefined {
|
|
39
|
-
return runInBackground ? defaultJoinMode : undefined;
|
|
40
|
-
}
|
package/src/settings.ts
CHANGED
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import type { JoinMode } from "./types.js";
|
|
9
|
-
|
|
10
8
|
export interface SubagentsSettings {
|
|
11
9
|
maxConcurrent?: number;
|
|
12
10
|
/**
|
|
@@ -16,7 +14,6 @@ export interface SubagentsSettings {
|
|
|
16
14
|
*/
|
|
17
15
|
defaultMaxTurns?: number;
|
|
18
16
|
graceTurns?: number;
|
|
19
|
-
defaultJoinMode?: JoinMode;
|
|
20
17
|
}
|
|
21
18
|
|
|
22
19
|
/** Setter hooks used by applySettings to wire persisted values into in-memory state. */
|
|
@@ -24,14 +21,11 @@ export interface SettingsAppliers {
|
|
|
24
21
|
setMaxConcurrent: (n: number) => void;
|
|
25
22
|
setDefaultMaxTurns: (n: number) => void;
|
|
26
23
|
setGraceTurns: (n: number) => void;
|
|
27
|
-
setDefaultJoinMode: (mode: JoinMode) => void;
|
|
28
24
|
}
|
|
29
25
|
|
|
30
26
|
/** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
|
|
31
27
|
export type SettingsEmit = (event: string, payload: unknown) => void;
|
|
32
28
|
|
|
33
|
-
const VALID_JOIN_MODES: ReadonlySet<string> = new Set<JoinMode>(["async", "group", "smart"]);
|
|
34
|
-
|
|
35
29
|
// Sanity ceilings — prevent hand-edited configs from asking for values that
|
|
36
30
|
// make no operational sense (e.g. 1e6 concurrent subagents). Permissive enough
|
|
37
31
|
// that any realistic power-user setting passes through.
|
|
@@ -65,9 +59,6 @@ function sanitize(raw: unknown): SubagentsSettings {
|
|
|
65
59
|
) {
|
|
66
60
|
out.graceTurns = r.graceTurns as number;
|
|
67
61
|
}
|
|
68
|
-
if (typeof r.defaultJoinMode === "string" && VALID_JOIN_MODES.has(r.defaultJoinMode)) {
|
|
69
|
-
out.defaultJoinMode = r.defaultJoinMode as JoinMode;
|
|
70
|
-
}
|
|
71
62
|
return out;
|
|
72
63
|
}
|
|
73
64
|
|
|
@@ -121,7 +112,6 @@ export function applySettings(s: SubagentsSettings, appliers: SettingsAppliers):
|
|
|
121
112
|
if (typeof s.maxConcurrent === "number") appliers.setMaxConcurrent(s.maxConcurrent);
|
|
122
113
|
if (typeof s.defaultMaxTurns === "number") appliers.setDefaultMaxTurns(s.defaultMaxTurns);
|
|
123
114
|
if (typeof s.graceTurns === "number") appliers.setGraceTurns(s.graceTurns);
|
|
124
|
-
if (s.defaultJoinMode) appliers.setDefaultJoinMode(s.defaultJoinMode);
|
|
125
115
|
}
|
|
126
116
|
|
|
127
117
|
/**
|
package/src/types.ts
CHANGED
|
@@ -55,8 +55,6 @@ export interface AgentConfig {
|
|
|
55
55
|
source?: "default" | "project" | "global";
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
export type JoinMode = 'async' | 'group' | 'smart';
|
|
59
|
-
|
|
60
58
|
export interface AgentRecord {
|
|
61
59
|
id: string;
|
|
62
60
|
type: SubagentType;
|
|
@@ -70,8 +68,6 @@ export interface AgentRecord {
|
|
|
70
68
|
session?: AgentSession;
|
|
71
69
|
abortController?: AbortController;
|
|
72
70
|
promise?: Promise<string>;
|
|
73
|
-
groupId?: string;
|
|
74
|
-
joinMode?: JoinMode;
|
|
75
71
|
/** Set when result was already consumed via get_subagent_result — suppresses completion notification. */
|
|
76
72
|
resultConsumed?: boolean;
|
|
77
73
|
/** Steering messages queued before the session was ready. */
|
|
@@ -122,8 +118,7 @@ export interface NotificationDetails {
|
|
|
122
118
|
outputFile?: string;
|
|
123
119
|
error?: string;
|
|
124
120
|
resultPreview: string;
|
|
125
|
-
|
|
126
|
-
others?: NotificationDetails[];
|
|
121
|
+
|
|
127
122
|
}
|
|
128
123
|
|
|
129
124
|
export interface EnvInfo {
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cross-extension RPC handlers for the subagents extension.
|
|
3
|
-
*
|
|
4
|
-
* Exposes ping, spawn, and stop RPCs over the pi.events event bus,
|
|
5
|
-
* using per-request scoped reply channels.
|
|
6
|
-
*
|
|
7
|
-
* Reply envelope follows pi-mono convention:
|
|
8
|
-
* success → { success: true, data?: T }
|
|
9
|
-
* error → { success: false, error: string }
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/** Minimal event bus interface needed by the RPC handlers. */
|
|
13
|
-
export interface EventBus {
|
|
14
|
-
on(event: string, handler: (data: unknown) => void): () => void;
|
|
15
|
-
emit(event: string, data: unknown): void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** RPC reply envelope — matches pi-mono's RpcResponse shape. */
|
|
19
|
-
export type RpcReply<T = void> =
|
|
20
|
-
| { success: true; data?: T }
|
|
21
|
-
| { success: false; error: string };
|
|
22
|
-
|
|
23
|
-
/** RPC protocol version — bumped when the envelope or method contracts change. */
|
|
24
|
-
export const PROTOCOL_VERSION = 2;
|
|
25
|
-
|
|
26
|
-
/** Minimal AgentManager interface needed by the spawn/stop RPCs. */
|
|
27
|
-
export interface SpawnCapable {
|
|
28
|
-
spawn(pi: unknown, ctx: unknown, type: string, prompt: string, options: any): string;
|
|
29
|
-
abort(id: string): boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface RpcDeps {
|
|
33
|
-
events: EventBus;
|
|
34
|
-
pi: unknown; // passed through to manager.spawn
|
|
35
|
-
getCtx: () => unknown | undefined; // returns current ExtensionContext
|
|
36
|
-
manager: SpawnCapable;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface RpcHandle {
|
|
40
|
-
unsubPing: () => void;
|
|
41
|
-
unsubSpawn: () => void;
|
|
42
|
-
unsubStop: () => void;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Wire a single RPC handler: listen on `channel`, run `fn(params)`,
|
|
47
|
-
* emit the reply envelope on `channel:reply:${requestId}`.
|
|
48
|
-
*/
|
|
49
|
-
function handleRpc<P extends { requestId: string }>(
|
|
50
|
-
events: EventBus,
|
|
51
|
-
channel: string,
|
|
52
|
-
fn: (params: P) => unknown | Promise<unknown>,
|
|
53
|
-
): () => void {
|
|
54
|
-
return events.on(channel, async (raw: unknown) => {
|
|
55
|
-
const params = raw as P;
|
|
56
|
-
try {
|
|
57
|
-
const data = await fn(params);
|
|
58
|
-
const reply: { success: true; data?: unknown } = { success: true };
|
|
59
|
-
if (data !== undefined) reply.data = data;
|
|
60
|
-
events.emit(`${channel}:reply:${params.requestId}`, reply);
|
|
61
|
-
} catch (err: any) {
|
|
62
|
-
events.emit(`${channel}:reply:${params.requestId}`, {
|
|
63
|
-
success: false, error: err?.message ?? String(err),
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Register ping, spawn, and stop RPC handlers on the event bus.
|
|
71
|
-
* Returns unsub functions for cleanup.
|
|
72
|
-
*/
|
|
73
|
-
export function registerRpcHandlers(deps: RpcDeps): RpcHandle {
|
|
74
|
-
const { events, pi, getCtx, manager } = deps;
|
|
75
|
-
|
|
76
|
-
const unsubPing = handleRpc(events, "subagents:rpc:ping", () => {
|
|
77
|
-
return { version: PROTOCOL_VERSION };
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const unsubSpawn = handleRpc<{ requestId: string; type: string; prompt: string; options?: any }>(
|
|
81
|
-
events, "subagents:rpc:spawn", ({ type, prompt, options }) => {
|
|
82
|
-
const ctx = getCtx();
|
|
83
|
-
if (!ctx) throw new Error("No active session");
|
|
84
|
-
return { id: manager.spawn(pi, ctx, type, prompt, options ?? {}) };
|
|
85
|
-
},
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
const unsubStop = handleRpc<{ requestId: string; agentId: string }>(
|
|
89
|
-
events, "subagents:rpc:stop", ({ agentId }) => {
|
|
90
|
-
if (!manager.abort(agentId)) throw new Error("Agent not found");
|
|
91
|
-
},
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
return { unsubPing, unsubSpawn, unsubStop };
|
|
95
|
-
}
|
package/src/group-join.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* group-join.ts — Manages grouped background agent completion notifications.
|
|
3
|
-
*
|
|
4
|
-
* Instead of each agent individually nudging the main agent on completion,
|
|
5
|
-
* agents in a group are held until all complete (or a timeout fires),
|
|
6
|
-
* then a single consolidated notification is sent.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { AgentRecord } from "./types.js";
|
|
10
|
-
|
|
11
|
-
export type DeliveryCallback = (records: AgentRecord[], partial: boolean) => void;
|
|
12
|
-
|
|
13
|
-
interface AgentGroup {
|
|
14
|
-
groupId: string;
|
|
15
|
-
agentIds: Set<string>;
|
|
16
|
-
completedRecords: Map<string, AgentRecord>;
|
|
17
|
-
timeoutHandle?: ReturnType<typeof setTimeout>;
|
|
18
|
-
delivered: boolean;
|
|
19
|
-
/** Shorter timeout for stragglers after a partial delivery. */
|
|
20
|
-
isStraggler: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Default timeout: 30s after first completion in a group. */
|
|
24
|
-
const DEFAULT_TIMEOUT = 30_000;
|
|
25
|
-
/** Straggler re-batch timeout: 15s. */
|
|
26
|
-
const STRAGGLER_TIMEOUT = 15_000;
|
|
27
|
-
|
|
28
|
-
export class GroupJoinManager {
|
|
29
|
-
private groups = new Map<string, AgentGroup>();
|
|
30
|
-
private agentToGroup = new Map<string, string>();
|
|
31
|
-
|
|
32
|
-
constructor(
|
|
33
|
-
private deliverCb: DeliveryCallback,
|
|
34
|
-
private groupTimeout = DEFAULT_TIMEOUT,
|
|
35
|
-
) {}
|
|
36
|
-
|
|
37
|
-
/** Register a group of agent IDs that should be joined. */
|
|
38
|
-
registerGroup(groupId: string, agentIds: string[]): void {
|
|
39
|
-
const group: AgentGroup = {
|
|
40
|
-
groupId,
|
|
41
|
-
agentIds: new Set(agentIds),
|
|
42
|
-
completedRecords: new Map(),
|
|
43
|
-
delivered: false,
|
|
44
|
-
isStraggler: false,
|
|
45
|
-
};
|
|
46
|
-
this.groups.set(groupId, group);
|
|
47
|
-
for (const id of agentIds) {
|
|
48
|
-
this.agentToGroup.set(id, groupId);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Called when an agent completes.
|
|
54
|
-
* Returns:
|
|
55
|
-
* - 'pass' — agent is not grouped, caller should send individual nudge
|
|
56
|
-
* - 'held' — result held, waiting for group completion
|
|
57
|
-
* - 'delivered' — this completion triggered the group notification
|
|
58
|
-
*/
|
|
59
|
-
onAgentComplete(record: AgentRecord): 'delivered' | 'held' | 'pass' {
|
|
60
|
-
const groupId = this.agentToGroup.get(record.id);
|
|
61
|
-
if (!groupId) return 'pass';
|
|
62
|
-
|
|
63
|
-
const group = this.groups.get(groupId);
|
|
64
|
-
if (!group || group.delivered) return 'pass';
|
|
65
|
-
|
|
66
|
-
group.completedRecords.set(record.id, record);
|
|
67
|
-
|
|
68
|
-
// All done — deliver immediately
|
|
69
|
-
if (group.completedRecords.size >= group.agentIds.size) {
|
|
70
|
-
this.deliver(group, false);
|
|
71
|
-
return 'delivered';
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// First completion in this batch — start timeout
|
|
75
|
-
if (!group.timeoutHandle) {
|
|
76
|
-
const timeout = group.isStraggler ? STRAGGLER_TIMEOUT : this.groupTimeout;
|
|
77
|
-
group.timeoutHandle = setTimeout(() => {
|
|
78
|
-
this.onTimeout(group);
|
|
79
|
-
}, timeout);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return 'held';
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
private onTimeout(group: AgentGroup): void {
|
|
86
|
-
if (group.delivered) return;
|
|
87
|
-
group.timeoutHandle = undefined;
|
|
88
|
-
|
|
89
|
-
// Partial delivery — some agents still running
|
|
90
|
-
const remaining = new Set<string>();
|
|
91
|
-
for (const id of group.agentIds) {
|
|
92
|
-
if (!group.completedRecords.has(id)) remaining.add(id);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Clean up agentToGroup for delivered agents (they won't complete again)
|
|
96
|
-
for (const id of group.completedRecords.keys()) {
|
|
97
|
-
this.agentToGroup.delete(id);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Deliver what we have
|
|
101
|
-
this.deliverCb([...group.completedRecords.values()], true);
|
|
102
|
-
|
|
103
|
-
// Set up straggler group for remaining agents
|
|
104
|
-
group.completedRecords.clear();
|
|
105
|
-
group.agentIds = remaining;
|
|
106
|
-
group.isStraggler = true;
|
|
107
|
-
// Timeout will be started when the next straggler completes
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
private deliver(group: AgentGroup, partial: boolean): void {
|
|
111
|
-
if (group.timeoutHandle) {
|
|
112
|
-
clearTimeout(group.timeoutHandle);
|
|
113
|
-
group.timeoutHandle = undefined;
|
|
114
|
-
}
|
|
115
|
-
group.delivered = true;
|
|
116
|
-
this.deliverCb([...group.completedRecords.values()], partial);
|
|
117
|
-
this.cleanupGroup(group.groupId);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
private cleanupGroup(groupId: string): void {
|
|
121
|
-
const group = this.groups.get(groupId);
|
|
122
|
-
if (!group) return;
|
|
123
|
-
for (const id of group.agentIds) {
|
|
124
|
-
this.agentToGroup.delete(id);
|
|
125
|
-
}
|
|
126
|
-
this.groups.delete(groupId);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Check if an agent is in a group. */
|
|
130
|
-
isGrouped(agentId: string): boolean {
|
|
131
|
-
return this.agentToGroup.has(agentId);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
dispose(): void {
|
|
135
|
-
for (const group of this.groups.values()) {
|
|
136
|
-
if (group.timeoutHandle) clearTimeout(group.timeoutHandle);
|
|
137
|
-
}
|
|
138
|
-
this.groups.clear();
|
|
139
|
-
this.agentToGroup.clear();
|
|
140
|
-
}
|
|
141
|
-
}
|