@gotgenes/pi-subagents 2.0.0 → 4.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 +47 -0
- package/README.md +8 -92
- package/docs/plans/0048-implement-subagents-api.md +303 -0
- package/docs/plans/0049-remove-group-join-output-file-rpc.md +163 -0
- package/docs/retro/0048-implement-subagents-api.md +44 -0
- package/docs/retro/0049-remove-group-join-output-file-rpc.md +38 -0
- package/package.json +4 -1
- package/src/index.ts +22 -162
- package/src/invocation-config.ts +1 -5
- package/src/service-adapter.ts +130 -0
- package/src/service.ts +104 -0
- 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,53 @@ 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
|
+
## [4.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v3.0.0...pi-subagents-v4.0.0) (2026-05-17)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### ⚠ BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* The untyped globalThis[Symbol.for("pi-subagents:manager")] accessor is removed. Use getSubagentsService() from the package's public exports instead.
|
|
14
|
+
* The public API surface is now exported from src/service.ts. The old untyped Symbol.for("pi-subagents:manager") global will be removed in a subsequent commit.
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* add SubagentRecord serializer ([d7afb45](https://github.com/gotgenes/pi-packages/commit/d7afb4569c9e28ce5d4bf7fb1ac560b0bcbb7c90))
|
|
19
|
+
* add SubagentsService types and accessor functions ([468623c](https://github.com/gotgenes/pi-packages/commit/468623c936f45cc30d3c5dde134cc2d21da4a0c4))
|
|
20
|
+
* expose public service entry point via package exports ([0dbeaaf](https://github.com/gotgenes/pi-packages/commit/0dbeaaf39c79717df8cabf59e8ba53652f9bc7af))
|
|
21
|
+
* implement getRecord and listAgents on SubagentsService adapter ([a6da473](https://github.com/gotgenes/pi-packages/commit/a6da47393f6faa3fef93bd065c1ad1a0613d1636))
|
|
22
|
+
* implement spawn with model resolution on SubagentsService adapter ([fd70d82](https://github.com/gotgenes/pi-packages/commit/fd70d828905bc3415fa8b8aebfe4c2a5355209cb))
|
|
23
|
+
* implement steer, abort, waitForAll, hasRunning on adapter ([00f0b99](https://github.com/gotgenes/pi-packages/commit/00f0b99ea978625798ba67a40b375e42006d33e4))
|
|
24
|
+
* publish SubagentsService at extension init, remove old untyped global ([6047e2b](https://github.com/gotgenes/pi-packages/commit/6047e2bbbaf87b5e28325b084b09daf2b0c9b6b9))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Documentation
|
|
28
|
+
|
|
29
|
+
* plan SubagentsService implementation ([#48](https://github.com/gotgenes/pi-packages/issues/48)) ([6bd2af8](https://github.com/gotgenes/pi-packages/commit/6bd2af862fb7e7f429617c154391c800b50c5d86))
|
|
30
|
+
* **retro:** add retro notes for issue [#49](https://github.com/gotgenes/pi-packages/issues/49) ([69a5bfc](https://github.com/gotgenes/pi-packages/commit/69a5bfc94edfc445d46fb495449649998614f86d))
|
|
31
|
+
|
|
32
|
+
## [3.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v2.0.0...pi-subagents-v3.0.0) (2026-05-17)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
### ⚠ BREAKING CHANGES
|
|
36
|
+
|
|
37
|
+
* The JoinMode type and defaultJoinMode setting are removed from the public settings interface.
|
|
38
|
+
* The join-mode setting (smart/async/group) is removed. Background agents always notify individually on completion.
|
|
39
|
+
* 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.
|
|
40
|
+
* 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.
|
|
41
|
+
|
|
42
|
+
### Features
|
|
43
|
+
|
|
44
|
+
* remove group-join and cross-extension-rpc source ([b7d7f21](https://github.com/gotgenes/pi-packages/commit/b7d7f21af265e2ff95f0534f5c0b51f71b8f1e7f))
|
|
45
|
+
* remove group-join wiring from index.ts ([4e2dc7f](https://github.com/gotgenes/pi-packages/commit/4e2dc7f8a98e441308f229745dfc42d09784d786))
|
|
46
|
+
* remove join-mode types and settings ([1d98793](https://github.com/gotgenes/pi-packages/commit/1d98793eb85aa3d9815274aa443c34bb4434b6f9))
|
|
47
|
+
* remove RPC wiring from index.ts ([3a960af](https://github.com/gotgenes/pi-packages/commit/3a960af8f6bb83219497d6229d12c6859cf3eb71))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
### Documentation
|
|
51
|
+
|
|
52
|
+
* 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))
|
|
53
|
+
* remove group-join and RPC from README and AGENTS ([9f65f7a](https://github.com/gotgenes/pi-packages/commit/9f65f7a21611af8dead00a5fb34d7d10f3d6ab43))
|
|
54
|
+
|
|
8
55
|
## [2.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v1.0.2...pi-subagents-v2.0.0) (2026-05-17)
|
|
9
56
|
|
|
10
57
|
|
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,303 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 48
|
|
3
|
+
issue_title: "feat: implement and publish SubagentsAPI at extension init"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Implement and publish SubagentsService
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
The package currently exposes an untyped, undocumented manager via `Symbol.for("pi-subagents:manager")` on `globalThis`.
|
|
11
|
+
This forces consumers to guess the API shape, lacks model resolution at the boundary (causing "No API key found for undefined" crashes when consumers pass string model names), and leaks non-serializable internals (`AgentSession`, `AbortController`) in returned records.
|
|
12
|
+
|
|
13
|
+
The architecture doc specifies a typed interface with `Symbol.for()` accessor functions that other extensions import as an optional peer dependency.
|
|
14
|
+
This issue implements that boundary, following the naming and structural conventions established by `pi-permission-system`.
|
|
15
|
+
|
|
16
|
+
## Goals
|
|
17
|
+
|
|
18
|
+
- Export `SubagentsService` interface, `SubagentRecord`, `SubagentStatus`, `SpawnOptions`, `LifetimeUsage`, accessor functions (`publishSubagentsService`, `getSubagentsService`), and event constants from the package's public entry point.
|
|
19
|
+
- Create `src/service-adapter.ts` — an adapter wrapping `AgentManager` to satisfy `SubagentsService`, handling string model resolution and record serialization.
|
|
20
|
+
- Call `publishSubagentsService()` at extension init; clean up on `session_shutdown`.
|
|
21
|
+
- Remove the old `Symbol.for("pi-subagents:manager")` global key.
|
|
22
|
+
- This is a **breaking change** (`feat!:`) — the old untyped global key is removed and replaced with the typed service under a new key.
|
|
23
|
+
- Follow the naming and structural conventions established by `pi-permission-system` (`service.ts`, `@gotgenes/<pkg>:service` key, `Record<symbol, unknown>` cast).
|
|
24
|
+
|
|
25
|
+
## Non-Goals
|
|
26
|
+
|
|
27
|
+
- Consumer extensions (scheduling, transcript) — these are separate packages.
|
|
28
|
+
- Native Pi service registry integration (`pi.registerService()`) — deferred to a future Pi SDK release.
|
|
29
|
+
- `SubagentsService.resume()` — not part of the initial interface per the architecture doc.
|
|
30
|
+
- Output-file JSONL format migration (#61).
|
|
31
|
+
|
|
32
|
+
## Background
|
|
33
|
+
|
|
34
|
+
### Prerequisite issues
|
|
35
|
+
|
|
36
|
+
- #49 (remove group-join and RPC) — **closed/merged**. The untyped RPC channels are already gone.
|
|
37
|
+
- #52 (remove scheduled subagents) — **closed/merged**.
|
|
38
|
+
- #51 (update ADR for hard fork) — **closed/merged**.
|
|
39
|
+
|
|
40
|
+
### Relevant modules
|
|
41
|
+
|
|
42
|
+
| Module | Role in this change |
|
|
43
|
+
| ----------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| `src/index.ts` | Wiring layer. Currently publishes the untyped global; will call `publishSubagentsService()` instead. |
|
|
45
|
+
| `src/agent-manager.ts` | Core lifecycle manager. The adapter wraps its public methods. |
|
|
46
|
+
| `src/model-resolver.ts` | `resolveModel()` converts string → `Model`. The adapter calls this at the API boundary. |
|
|
47
|
+
| `src/types.ts` | Defines `AgentRecord` (internal, non-serializable). |
|
|
48
|
+
| `src/usage.ts` | Exports `LifetimeUsage` (already serializable). |
|
|
49
|
+
|
|
50
|
+
### Constraints from AGENTS.md
|
|
51
|
+
|
|
52
|
+
- One concern per file — types/accessors in `src/service.ts`, adapter logic in `src/service-adapter.ts`.
|
|
53
|
+
- Avoid `any` unless absolutely necessary — the accessor functions use `Record<symbol, unknown>` on `globalThis`.
|
|
54
|
+
- Pi SDK imports stay out of business-logic modules — `service-adapter.ts` accepts `pi` and `ctx` as narrow interface parameters.
|
|
55
|
+
- Narrow interface types for collaborators — the adapter takes a minimal `AgentManagerLike` interface, not the concrete `AgentManager` class.
|
|
56
|
+
|
|
57
|
+
### Alignment with pi-permission-system
|
|
58
|
+
|
|
59
|
+
This plan deliberately follows the pattern established by `@gotgenes/pi-permission-system`:
|
|
60
|
+
|
|
61
|
+
| Aspect | pi-permission-system | pi-subagents (this plan) |
|
|
62
|
+
| --------------- | ------------------------------------------ | --------------------------------------- |
|
|
63
|
+
| Public file | `src/service.ts` | `src/service.ts` |
|
|
64
|
+
| Interface name | `PermissionsService` | `SubagentsService` |
|
|
65
|
+
| Symbol.for key | `"@gotgenes/pi-permission-system:service"` | `"@gotgenes/pi-subagents:service"` |
|
|
66
|
+
| globalThis cast | `Record<symbol, unknown>` | `Record<symbol, unknown>` |
|
|
67
|
+
| Accessors | `publish/get/unpublishPermissionsService` | `publish/get/unpublishSubagentsService` |
|
|
68
|
+
| exports → | `./src/service.ts` | `./src/service.ts` |
|
|
69
|
+
|
|
70
|
+
The architecture doc uses `SubagentsAPI` naming and `pi:service:subagents` key; it should be updated during implementation to reflect the final naming.
|
|
71
|
+
|
|
72
|
+
## Design Overview
|
|
73
|
+
|
|
74
|
+
### Module decomposition
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
src/service.ts ← SubagentsService interface, SubagentRecord, SpawnOptions,
|
|
78
|
+
SubagentStatus, accessor functions, event constants
|
|
79
|
+
src/service-adapter.ts ← createSubagentsService() factory, record serialization,
|
|
80
|
+
model resolution at the boundary
|
|
81
|
+
src/index.ts ← wire: publishSubagentsService(createSubagentsService(...))
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Types (in `src/service.ts`)
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
export type SubagentStatus =
|
|
88
|
+
| "queued" | "running" | "completed" | "steered"
|
|
89
|
+
| "aborted" | "stopped" | "error";
|
|
90
|
+
|
|
91
|
+
export interface SubagentRecord {
|
|
92
|
+
id: string;
|
|
93
|
+
type: string;
|
|
94
|
+
description: string;
|
|
95
|
+
status: SubagentStatus;
|
|
96
|
+
result?: string;
|
|
97
|
+
error?: string;
|
|
98
|
+
toolUses: number;
|
|
99
|
+
startedAt: number;
|
|
100
|
+
completedAt?: number;
|
|
101
|
+
lifetimeUsage: LifetimeUsage;
|
|
102
|
+
compactionCount: number;
|
|
103
|
+
worktreeResult?: { hasChanges: boolean; branch?: string };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface SpawnOptions {
|
|
107
|
+
description?: string;
|
|
108
|
+
model?: string;
|
|
109
|
+
maxTurns?: number;
|
|
110
|
+
thinkingLevel?: string;
|
|
111
|
+
isolated?: boolean;
|
|
112
|
+
inheritContext?: boolean;
|
|
113
|
+
foreground?: boolean;
|
|
114
|
+
bypassQueue?: boolean;
|
|
115
|
+
isolation?: "worktree";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface SubagentsService {
|
|
119
|
+
spawn(type: string, prompt: string, options?: SpawnOptions): string;
|
|
120
|
+
getRecord(id: string): SubagentRecord | undefined;
|
|
121
|
+
listAgents(): SubagentRecord[];
|
|
122
|
+
abort(id: string): boolean;
|
|
123
|
+
steer(id: string, message: string): Promise<boolean>;
|
|
124
|
+
waitForAll(): Promise<void>;
|
|
125
|
+
hasRunning(): boolean;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const SUBAGENT_EVENTS = {
|
|
129
|
+
STARTED: "subagents:started",
|
|
130
|
+
COMPLETED: "subagents:completed",
|
|
131
|
+
ACTIVITY: "subagents:activity",
|
|
132
|
+
} as const;
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Accessor pattern
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
const SERVICE_KEY = Symbol.for("@gotgenes/pi-subagents:service");
|
|
139
|
+
|
|
140
|
+
export function publishSubagentsService(service: SubagentsService): void {
|
|
141
|
+
(globalThis as Record<symbol, unknown>)[SERVICE_KEY] = service;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function getSubagentsService(): SubagentsService | undefined {
|
|
145
|
+
return (globalThis as Record<symbol, unknown>)[SERVICE_KEY] as
|
|
146
|
+
| SubagentsService
|
|
147
|
+
| undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function unpublishSubagentsService(): void {
|
|
151
|
+
delete (globalThis as Record<symbol, unknown>)[SERVICE_KEY];
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Adapter (`src/service-adapter.ts`)
|
|
156
|
+
|
|
157
|
+
The adapter accepts narrow interfaces rather than concrete classes:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
interface AgentManagerLike {
|
|
161
|
+
spawn(pi: any, ctx: any, type: string, prompt: string, options: any): string;
|
|
162
|
+
getRecord(id: string): AgentRecord | undefined;
|
|
163
|
+
listAgents(): AgentRecord[];
|
|
164
|
+
abort(id: string): boolean;
|
|
165
|
+
waitForAll(): Promise<void>;
|
|
166
|
+
hasRunning(): boolean;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface AdapterDeps {
|
|
170
|
+
manager: AgentManagerLike;
|
|
171
|
+
resolveModel: (input: string, registry: ModelRegistry) => any;
|
|
172
|
+
getCtx: () => { pi: any; ctx: any } | undefined;
|
|
173
|
+
getModelRegistry: () => ModelRegistry | undefined;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Key behaviors:
|
|
178
|
+
|
|
179
|
+
1. **String model resolution** — `spawn()` calls `resolveModel(options.model, registry)` before delegating to the manager.
|
|
180
|
+
If resolution fails, throws with the error string (list of available models).
|
|
181
|
+
2. **Session gating** — throws if `getCtx()` returns `undefined` (no active session).
|
|
182
|
+
3. **Record serialization** — `toSubagentRecord()` strips `session`, `abortController`, `promise`, `pendingSteers`, `outputCleanup` from `AgentRecord`.
|
|
183
|
+
4. **Steer delegation** — uses the same pattern as the `steer_subagent` tool: checks status, queues if session not ready, delegates to `session.steer()`.
|
|
184
|
+
|
|
185
|
+
This mirrors the `pi-permission-system` pattern: a slim `service.ts` defines the contract and accessors; a separate adapter file contains the implementation wiring.
|
|
186
|
+
|
|
187
|
+
### Public entry point
|
|
188
|
+
|
|
189
|
+
The package currently has no explicit `exports` field in `package.json`.
|
|
190
|
+
Since Pi loads the extension via `pi.extensions` (pointing at `./src/index.ts`), the service types and accessors need a separate public entry point.
|
|
191
|
+
Add an `exports` map:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"exports": {
|
|
196
|
+
".": "./src/service.ts"
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
This exposes the types and accessor functions to consumers who `import("@gotgenes/pi-subagents")`.
|
|
202
|
+
The extension entry point (`./src/index.ts`) remains declared in `pi.extensions`.
|
|
203
|
+
This matches the pattern established by `pi-permission-system` (`exports` → `service.ts`, `pi.extensions` → `index.ts`).
|
|
204
|
+
|
|
205
|
+
### Edge cases
|
|
206
|
+
|
|
207
|
+
- **No active session**: `spawn()` throws `"No active session — cannot spawn agents outside a session."`.
|
|
208
|
+
- **Model resolution failure**: `spawn()` throws with the error string from `resolveModel()`.
|
|
209
|
+
- **Missing description**: default to a truncated prompt (`prompt.slice(0, 80)`).
|
|
210
|
+
- **Steer on non-running agent**: returns `false`.
|
|
211
|
+
- **Steer before session ready**: queues the message (returns `true`).
|
|
212
|
+
|
|
213
|
+
### Naming conventions
|
|
214
|
+
|
|
215
|
+
Following `pi-permission-system`'s established pattern:
|
|
216
|
+
|
|
217
|
+
- Public file: `service.ts` (not `api.ts`)
|
|
218
|
+
- Interface: `SubagentsService` (not `SubagentsAPI`)
|
|
219
|
+
- Symbol key: `"@gotgenes/pi-subagents:service"` (scoped package name, not generic `pi:service:*`)
|
|
220
|
+
- globalThis cast: `Record<symbol, unknown>` (not `any`)
|
|
221
|
+
- Accessor names: `publish/get/unpublishSubagentsService`
|
|
222
|
+
|
|
223
|
+
## Module-Level Changes
|
|
224
|
+
|
|
225
|
+
### New files
|
|
226
|
+
|
|
227
|
+
| File | Contents |
|
|
228
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
229
|
+
| `src/service.ts` | `SubagentsService` interface, `SubagentRecord`, `SubagentStatus`, `SpawnOptions`, `LifetimeUsage` re-export, accessor functions, event constants, `unpublishSubagentsService`. |
|
|
230
|
+
| `src/service-adapter.ts` | `createSubagentsService()` factory. `toSubagentRecord()` serializer. Narrow `AgentManagerLike` and `AdapterDeps` interfaces. |
|
|
231
|
+
| `test/service-adapter.test.ts` | Unit tests for the adapter (model resolution, serialization, session gating, steer delegation). |
|
|
232
|
+
| `test/service.test.ts` | Unit tests for accessor functions (publish/get/unpublish round-trip, isolation between keys). |
|
|
233
|
+
|
|
234
|
+
### Modified files
|
|
235
|
+
|
|
236
|
+
| File | Change |
|
|
237
|
+
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
238
|
+
| `src/index.ts` | Import `publishSubagentsService`, `unpublishSubagentsService` from `./service.js` and `createSubagentsService` from `./service-adapter.js`. Replace `Symbol.for("pi-subagents:manager")` block with `publishSubagentsService(createSubagentsService(...))`. In `session_shutdown`, call `unpublishSubagentsService()` instead of `delete (globalThis as any)[MANAGER_KEY]`. |
|
|
239
|
+
| `package.json` | Add `"exports": { ".": "./src/service.ts" }`. |
|
|
240
|
+
| `src/usage.ts` | No change needed — `LifetimeUsage` is already exported. Re-exported from `src/service.ts`. |
|
|
241
|
+
|
|
242
|
+
## Test Impact Analysis
|
|
243
|
+
|
|
244
|
+
1. **New unit tests enabled**: `test/service-adapter.test.ts` tests the adapter in isolation against a mock `AgentManagerLike` — model resolution, record stripping, session gating, steer semantics.
|
|
245
|
+
`test/service.test.ts` tests the accessor functions (publish/get/unpublish lifecycle, `undefined` before publish).
|
|
246
|
+
2. **Existing tests that become redundant**: None — the old `Symbol.for("pi-subagents:manager")` global was not unit-tested.
|
|
247
|
+
3. **Existing tests that must stay**: All `agent-manager.test.ts` and `agent-runner.test.ts` tests remain — they test the internal engine, not the public service boundary.
|
|
248
|
+
Any test referencing `MANAGER_KEY` or `"pi-subagents:manager"` in string assertions must be updated.
|
|
249
|
+
|
|
250
|
+
## TDD Order
|
|
251
|
+
|
|
252
|
+
1. **`src/service.ts` — types, accessors, and event constants.**
|
|
253
|
+
Test: `test/service.test.ts` — `publishSubagentsService` stores on globalThis, `getSubagentsService` retrieves it, `unpublishSubagentsService` removes it, `getSubagentsService` returns `undefined` when not published.
|
|
254
|
+
Commit: `feat!: add SubagentsService types and accessor functions`
|
|
255
|
+
|
|
256
|
+
2. **`src/service-adapter.ts` — `toSubagentRecord()` serializer.**
|
|
257
|
+
Test: `test/service-adapter.test.ts` — given an `AgentRecord` with `session`, `abortController`, `promise`, `pendingSteers`, `outputCleanup`, verify the returned `SubagentRecord` contains only serializable fields.
|
|
258
|
+
Commit: `feat: add SubagentRecord serializer`
|
|
259
|
+
|
|
260
|
+
3. **`src/service-adapter.ts` — `createSubagentsService().getRecord()` and `listAgents()`.**
|
|
261
|
+
Test: verify `getRecord` delegates to manager and serializes; `listAgents` returns serialized records sorted by `startedAt` descending.
|
|
262
|
+
Commit: `feat: implement getRecord and listAgents on SubagentsService adapter`
|
|
263
|
+
|
|
264
|
+
4. **`src/service-adapter.ts` — `spawn()` with model resolution and session gating.**
|
|
265
|
+
Test: (a) throws when `getCtx()` returns `undefined`; (b) resolves string model names via `resolveModel`; (c) throws on model resolution failure; (d) delegates to manager with resolved model; (e) uses truncated prompt as default description.
|
|
266
|
+
Commit: `feat: implement spawn with model resolution on SubagentsService adapter`
|
|
267
|
+
|
|
268
|
+
5. **`src/service-adapter.ts` — `steer()`, `abort()`, `waitForAll()`, `hasRunning()`.**
|
|
269
|
+
Test: `steer` returns `false` for non-running agent, `true` when session queues or delivers; `abort`/`waitForAll`/`hasRunning` delegate to manager.
|
|
270
|
+
Commit: `feat: implement steer, abort, waitForAll, hasRunning on adapter`
|
|
271
|
+
|
|
272
|
+
6. **Wire into `src/index.ts` — replace old global with typed service.**
|
|
273
|
+
Replace `Symbol.for("pi-subagents:manager")` block with `publishSubagentsService(createSubagentsService(...))`.
|
|
274
|
+
Update `session_shutdown` to call `unpublishSubagentsService()`.
|
|
275
|
+
Commit: `feat!: publish SubagentsService at extension init, remove old untyped global`
|
|
276
|
+
|
|
277
|
+
7. **Add `exports` to `package.json`.**
|
|
278
|
+
Add `"exports": { ".": "./src/service.ts" }` so consumers can `import("@gotgenes/pi-subagents")`.
|
|
279
|
+
Commit: `feat: expose public service entry point via package exports`
|
|
280
|
+
|
|
281
|
+
8. **Run full suite and type check.**
|
|
282
|
+
`pnpm vitest run && pnpm run check`.
|
|
283
|
+
Fix any straggling references to `MANAGER_KEY` or `"pi-subagents:manager"` in tests.
|
|
284
|
+
Commit (if fixes needed): `test: update references to old Symbol.for key`
|
|
285
|
+
|
|
286
|
+
## Risks and Mitigations
|
|
287
|
+
|
|
288
|
+
| Risk | Mitigation |
|
|
289
|
+
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
290
|
+
| Consumers relying on the old `pi-subagents:manager` key break silently | This is a `feat!:` (major bump). No other package in this monorepo references the old key. Document migration in CHANGELOG via release-please. |
|
|
291
|
+
| `exports` field breaks Pi's extension loader | Pi loads via `pi.extensions` (`./src/index.ts`), which is separate from `exports`. The `exports` field only affects `import("@gotgenes/pi-subagents")` from consumer code. Same pattern as `pi-permission-system`. |
|
|
292
|
+
| Adapter leaks internal state if `AgentRecord` gains new non-serializable fields | `toSubagentRecord()` uses an explicit allowlist (pick pattern), not a denylist. New fields must be opted in. |
|
|
293
|
+
| `steer()` race condition — session created between status check and queue push | The existing tool handler has the same race window and handles it acceptably. The adapter uses the same pattern (check session → queue if absent → delegate if present). |
|
|
294
|
+
| `resolveModel` returns `any` — type unsafety at boundary | The adapter's `AgentManagerLike.spawn` already accepts `Model<any>` for the `options.model` field. The `any` is confined to the model-resolution seam, matching existing code. |
|
|
295
|
+
| Architecture doc uses different naming (`SubagentsAPI`, `pi:service:subagents`) | Open question documented below. Update the architecture doc during implementation to reflect final naming. |
|
|
296
|
+
|
|
297
|
+
## Open Questions
|
|
298
|
+
|
|
299
|
+
- Should `SubagentsService` be augmented with an `onEvent(channel, callback)` subscription method, or is `pi.events.on(SUBAGENT_EVENTS.COMPLETED, ...)` sufficient for consumers?
|
|
300
|
+
Deferred — consumers already have access to `pi.events` and the event constants are exported.
|
|
301
|
+
- The architecture doc uses `SubagentsAPI` naming and `pi:service:subagents` key.
|
|
302
|
+
This plan intentionally diverges to align with the established `pi-permission-system` pattern (`*Service` naming, `@gotgenes/<pkg>:service` key, `Record<symbol, unknown>` cast).
|
|
303
|
+
The architecture doc should be updated during implementation to reflect the final naming.
|