@gotgenes/pi-subagents 3.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 +24 -0
- package/docs/plans/0048-implement-subagents-api.md +303 -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 +16 -12
- package/src/service-adapter.ts +130 -0
- package/src/service.ts +104 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@ 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
|
+
|
|
8
32
|
## [3.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v2.0.0...pi-subagents-v3.0.0) (2026-05-17)
|
|
9
33
|
|
|
10
34
|
|
|
@@ -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.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 48
|
|
3
|
+
issue_title: "feat: implement and publish SubagentsService at extension init"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #48 — implement and publish SubagentsService at extension init
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-17T15:30:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned and implemented a typed `SubagentsService` interface with `Symbol.for()` accessor functions, an adapter wrapping `AgentManager` with model resolution and record serialization, and wired it into the extension init.
|
|
13
|
+
Released as `@gotgenes/pi-subagents@4.0.0` (breaking: old untyped global removed).
|
|
14
|
+
The plan was revised mid-session to align naming with `pi-permission-system`'s established conventions after the user flagged the discrepancy.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
#### What went well
|
|
19
|
+
|
|
20
|
+
- TDD execution was clean: 8 steps, zero rework, all 33 new tests green on first pass.
|
|
21
|
+
- The adapter design was well-scoped — `index.ts` wiring was +16/−12 lines, and the narrow `AgentManagerLike` interface made test mocks trivial.
|
|
22
|
+
- The allowlist serialization pattern (`toSubagentRecord`) prevents future leaks of non-serializable fields by default.
|
|
23
|
+
|
|
24
|
+
#### What caused friction (agent side)
|
|
25
|
+
|
|
26
|
+
- `missing-context` — The initial plan adopted the issue body's naming verbatim (`SubagentsAPI`, `api.ts`, `pi:service:subagents`, `(globalThis as any)`) without checking `pi-permission-system` for the established convention (`SubagentsService`, `service.ts`, `@gotgenes/<pkg>:service`, `Record<symbol, unknown>`).
|
|
27
|
+
The user had to explicitly ask "Does this structure follow the pattern set forth by pi-permission-system?"
|
|
28
|
+
Impact: full plan rewrite (replaced entire file), issue title update, issue body update — ~15 minutes of rework across 3 user turns.
|
|
29
|
+
This was **user-caught**.
|
|
30
|
+
|
|
31
|
+
- `missing-context` — Same pattern as the #49 retro: following the issue spec literally without checking the codebase.
|
|
32
|
+
The architecture doc also used the stale naming, reinforcing the wrong choice.
|
|
33
|
+
Root cause: the "Gather context" step in `/plan-issue` didn't include a cross-package convention check.
|
|
34
|
+
|
|
35
|
+
#### What caused friction (user side)
|
|
36
|
+
|
|
37
|
+
- The user had to perform mechanical oversight ("Does this follow the pi-permission-system pattern?") that the planner should have caught independently.
|
|
38
|
+
If the `/plan-issue` prompt included a step to grep sibling packages for established API patterns, this would have been a design decision surfaced during planning rather than a correction after the fact.
|
|
39
|
+
|
|
40
|
+
### Changes made
|
|
41
|
+
|
|
42
|
+
1. Created `packages/pi-subagents/docs/retro/0048-implement-subagents-api.md` (this file).
|
|
43
|
+
2. Updated `.pi/skills/package-pi-subagents/SKILL.md` — changed `SubagentsAPI` → `SubagentsService` in Implementation Priorities; added `service.ts` and `service-adapter.ts` to module dependency graph and descriptions.
|
|
44
|
+
3. Updated `.pi/prompts/plan-issue.md` — added step 7 to Gather context: check sibling packages for established API patterns before adopting issue body naming.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 49
|
|
3
|
+
issue_title: "feat: remove group-join, output-file, and ad-hoc RPC"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #49 — remove group-join, output-file, and ad-hoc RPC
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-17T15:15:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned and implemented the removal of group-join and ad-hoc RPC from `pi-subagents`, releasing v3.0.0.
|
|
13
|
+
The original scope included `output-file.ts` removal, but the user intervened to retain it for post-hoc debugging value.
|
|
14
|
+
A new issue (#61) was filed to port the output-file format to Pi's official JSONL session schema.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
#### What went well
|
|
19
|
+
|
|
20
|
+
- User intervention produced a materially better outcome — retaining debugging transcripts and identifying a format conformance gap that became #61.
|
|
21
|
+
- TDD execution was clean: 6 steps, zero rework, all tests green on first pass after each step.
|
|
22
|
+
- The `feat!:` → release-please → v3.0.0 pipeline worked smoothly end-to-end.
|
|
23
|
+
|
|
24
|
+
#### What caused friction (agent side)
|
|
25
|
+
|
|
26
|
+
- `missing-context` — Included `output-file.ts` removal in the initial plan without questioning its debugging value, despite AGENTS.md's rule "Ask before removing functionality or changing defaults." The issue body explicitly listed it for removal so I followed the spec literally. Impact: required plan revision (amend commit), scope-narrowing comment on issue, and filing #61 — roughly 10 minutes of rework, but produced a better design.
|
|
27
|
+
|
|
28
|
+
- `missing-context` — When asked whether output-file adheres to Pi's session format, searched the web (`web_search` for "Claude Code session JSONL format") instead of checking the local `~/development/pi/pi` monorepo. The user had to explicitly say "~/development/pi/pi has the code for Pi's JSONL format." Impact: one extra round-trip and less authoritative initial answer (Claude Code's format vs Pi's `SessionManager`). Self-identified after user redirect.
|
|
29
|
+
|
|
30
|
+
- `instruction-violation` (self-identified) — Shell-escaped the `gh issue comment` body incorrectly; backtick-wrapped `src/output-file.ts` was interpreted by bash. Caught immediately via `gh issue view` and fixed with `--edit-last`. Impact: trivial — one extra command.
|
|
31
|
+
|
|
32
|
+
#### What caused friction (user side)
|
|
33
|
+
|
|
34
|
+
- The issue body listed output-file for removal without noting its debugging value. The user's "How confident are we in getting rid of the logging system?" intervention was the correction. If the issue had marked output-file removal as "tentative pending debugging value assessment," the plan would have surfaced it as a design decision from the start. Minor — the discussion was quick and productive.
|
|
35
|
+
|
|
36
|
+
### Changes made
|
|
37
|
+
|
|
38
|
+
1. Created `packages/pi-subagents/docs/retro/0049-remove-group-join-output-file-rpc.md` (this file).
|
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotgenes/pi-subagents",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"exports": {
|
|
5
|
+
".": "./src/service.ts"
|
|
6
|
+
},
|
|
4
7
|
"description": "A pi extension that brings Claude Code-style autonomous sub-agents to pi. Friendly fork of @tintinweb/pi-subagents.",
|
|
5
8
|
"author": {
|
|
6
9
|
"name": "Chris Lasher"
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,8 @@ import { loadCustomAgents } from "./custom-agents.js";
|
|
|
22
22
|
import { resolveAgentInvocationConfig } from "./invocation-config.js";
|
|
23
23
|
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
24
24
|
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
25
|
+
import { publishSubagentsService, unpublishSubagentsService } from "./service.js";
|
|
26
|
+
import { createSubagentsService } from "./service-adapter.js";
|
|
25
27
|
import { applyAndEmitLoaded, type SubagentsSettings, saveAndEmitChanged } from "./settings.js";
|
|
26
28
|
import { type AgentConfig, type AgentInvocation, type AgentRecord, type NotificationDetails, type SubagentType } from "./types.js";
|
|
27
29
|
import {
|
|
@@ -376,18 +378,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
376
378
|
});
|
|
377
379
|
});
|
|
378
380
|
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
381
|
+
// Typed service published via Symbol.for() for cross-extension access.
|
|
382
|
+
// Consumers: const { getSubagentsService } = await import("@gotgenes/pi-subagents");
|
|
383
|
+
let currentCtx: { pi: unknown; ctx: unknown } | undefined;
|
|
384
|
+
const service = createSubagentsService({
|
|
385
|
+
manager,
|
|
386
|
+
resolveModel,
|
|
387
|
+
getCtx: () => currentCtx,
|
|
388
|
+
getModelRegistry: () => (currentCtx?.ctx as { modelRegistry?: ModelRegistry } | undefined)?.modelRegistry,
|
|
389
|
+
});
|
|
390
|
+
publishSubagentsService(service);
|
|
389
391
|
|
|
390
|
-
pi.on("session_start", async (_event,
|
|
392
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
393
|
+
currentCtx = { pi, ctx };
|
|
391
394
|
manager.clearCompleted();
|
|
392
395
|
});
|
|
393
396
|
|
|
@@ -398,7 +401,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
398
401
|
// On shutdown, abort all agents immediately and clean up.
|
|
399
402
|
// If the session is going down, there's nothing left to consume agent results.
|
|
400
403
|
pi.on("session_shutdown", async () => {
|
|
401
|
-
|
|
404
|
+
unpublishSubagentsService();
|
|
405
|
+
currentCtx = undefined;
|
|
402
406
|
manager.abortAll();
|
|
403
407
|
for (const timer of pendingNudges.values()) clearTimeout(timer);
|
|
404
408
|
pendingNudges.clear();
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* service-adapter.ts — Adapter that wraps AgentManager to satisfy SubagentsService.
|
|
3
|
+
*
|
|
4
|
+
* Handles model resolution at the API boundary, record serialization
|
|
5
|
+
* (stripping non-serializable fields), and session gating.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ModelRegistry } from "./model-resolver.js";
|
|
9
|
+
import type { SubagentRecord, SubagentsService } from "./service.js";
|
|
10
|
+
import type { AgentRecord } from "./types.js";
|
|
11
|
+
|
|
12
|
+
/** Narrow interface for the AgentManager — avoids coupling to the concrete class. */
|
|
13
|
+
export interface AgentManagerLike {
|
|
14
|
+
spawn(pi: unknown, ctx: unknown, type: string, prompt: string, options: unknown): string;
|
|
15
|
+
getRecord(id: string): AgentRecord | undefined;
|
|
16
|
+
listAgents(): AgentRecord[];
|
|
17
|
+
abort(id: string): boolean;
|
|
18
|
+
waitForAll(): Promise<void>;
|
|
19
|
+
hasRunning(): boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Dependencies injected into the adapter factory. */
|
|
23
|
+
export interface AdapterDeps {
|
|
24
|
+
manager: AgentManagerLike;
|
|
25
|
+
resolveModel: (input: string, registry: ModelRegistry) => unknown | string;
|
|
26
|
+
getCtx: () => { pi: unknown; ctx: unknown } | undefined;
|
|
27
|
+
getModelRegistry: () => ModelRegistry | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Create a SubagentsService backed by the given dependencies. */
|
|
31
|
+
export function createSubagentsService(deps: AdapterDeps): SubagentsService {
|
|
32
|
+
const { manager } = deps;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
spawn(type: string, prompt: string, options?) {
|
|
36
|
+
const session = deps.getCtx();
|
|
37
|
+
if (!session) {
|
|
38
|
+
throw new Error("No active session — cannot spawn agents outside a session.");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let model: unknown;
|
|
42
|
+
if (options?.model) {
|
|
43
|
+
const registry = deps.getModelRegistry();
|
|
44
|
+
if (!registry) {
|
|
45
|
+
throw new Error("No model registry available.");
|
|
46
|
+
}
|
|
47
|
+
const resolved = deps.resolveModel(options.model, registry);
|
|
48
|
+
if (typeof resolved === "string") {
|
|
49
|
+
throw new Error(resolved);
|
|
50
|
+
}
|
|
51
|
+
model = resolved;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const description = options?.description ?? prompt.slice(0, 80);
|
|
55
|
+
const isBackground = !(options?.foreground ?? false);
|
|
56
|
+
|
|
57
|
+
return manager.spawn(session.pi, session.ctx, type, prompt, {
|
|
58
|
+
description,
|
|
59
|
+
model,
|
|
60
|
+
maxTurns: options?.maxTurns,
|
|
61
|
+
thinkingLevel: options?.thinkingLevel,
|
|
62
|
+
isolated: options?.isolated,
|
|
63
|
+
inheritContext: options?.inheritContext,
|
|
64
|
+
bypassQueue: options?.bypassQueue,
|
|
65
|
+
isolation: options?.isolation,
|
|
66
|
+
isBackground,
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
getRecord(id: string): SubagentRecord | undefined {
|
|
71
|
+
const record = manager.getRecord(id);
|
|
72
|
+
return record ? toSubagentRecord(record) : undefined;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
listAgents(): SubagentRecord[] {
|
|
76
|
+
return manager.listAgents().map(toSubagentRecord);
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
abort(id: string): boolean {
|
|
80
|
+
return manager.abort(id);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
async steer(id: string, message: string): Promise<boolean> {
|
|
84
|
+
const record = manager.getRecord(id);
|
|
85
|
+
if (!record || record.status !== "running") {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (!record.session) {
|
|
89
|
+
// Session not ready yet — queue for delivery once initialized
|
|
90
|
+
if (!record.pendingSteers) record.pendingSteers = [];
|
|
91
|
+
record.pendingSteers.push(message);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
await record.session.steer(message);
|
|
95
|
+
return true;
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
async waitForAll(): Promise<void> {
|
|
99
|
+
return manager.waitForAll();
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
hasRunning(): boolean {
|
|
103
|
+
return manager.hasRunning();
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Convert an internal AgentRecord to a serializable SubagentRecord.
|
|
110
|
+
* Uses an explicit allowlist — new fields must be opted in.
|
|
111
|
+
*/
|
|
112
|
+
export function toSubagentRecord(record: AgentRecord): SubagentRecord {
|
|
113
|
+
const out: SubagentRecord = {
|
|
114
|
+
id: record.id,
|
|
115
|
+
type: record.type,
|
|
116
|
+
description: record.description,
|
|
117
|
+
status: record.status,
|
|
118
|
+
toolUses: record.toolUses,
|
|
119
|
+
startedAt: record.startedAt,
|
|
120
|
+
lifetimeUsage: record.lifetimeUsage,
|
|
121
|
+
compactionCount: record.compactionCount,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (record.result !== undefined) out.result = record.result;
|
|
125
|
+
if (record.error !== undefined) out.error = record.error;
|
|
126
|
+
if (record.completedAt !== undefined) out.completedAt = record.completedAt;
|
|
127
|
+
if (record.worktreeResult !== undefined) out.worktreeResult = record.worktreeResult;
|
|
128
|
+
|
|
129
|
+
return out;
|
|
130
|
+
}
|
package/src/service.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* service.ts — Public API surface for cross-extension access to subagents.
|
|
3
|
+
*
|
|
4
|
+
* Consumers declare this package as an optional peer dependency and use
|
|
5
|
+
* dynamic import to access the accessor functions:
|
|
6
|
+
*
|
|
7
|
+
* const { getSubagentsService } = await import("@gotgenes/pi-subagents");
|
|
8
|
+
* const svc = getSubagentsService();
|
|
9
|
+
* svc?.spawn("Explore", "Check for stale TODOs");
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { LifetimeUsage } from "./usage.js";
|
|
13
|
+
|
|
14
|
+
export type { LifetimeUsage };
|
|
15
|
+
|
|
16
|
+
export type SubagentStatus =
|
|
17
|
+
| "queued"
|
|
18
|
+
| "running"
|
|
19
|
+
| "completed"
|
|
20
|
+
| "steered"
|
|
21
|
+
| "aborted"
|
|
22
|
+
| "stopped"
|
|
23
|
+
| "error";
|
|
24
|
+
|
|
25
|
+
/** Serializable snapshot of an agent's state — no live session objects. */
|
|
26
|
+
export interface SubagentRecord {
|
|
27
|
+
id: string;
|
|
28
|
+
type: string;
|
|
29
|
+
description: string;
|
|
30
|
+
status: SubagentStatus;
|
|
31
|
+
result?: string;
|
|
32
|
+
error?: string;
|
|
33
|
+
toolUses: number;
|
|
34
|
+
startedAt: number;
|
|
35
|
+
completedAt?: number;
|
|
36
|
+
lifetimeUsage: LifetimeUsage;
|
|
37
|
+
compactionCount: number;
|
|
38
|
+
worktreeResult?: { hasChanges: boolean; branch?: string };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Options for spawning an agent via the service. */
|
|
42
|
+
export interface SpawnOptions {
|
|
43
|
+
description?: string;
|
|
44
|
+
model?: string;
|
|
45
|
+
maxTurns?: number;
|
|
46
|
+
thinkingLevel?: string;
|
|
47
|
+
isolated?: boolean;
|
|
48
|
+
inheritContext?: boolean;
|
|
49
|
+
foreground?: boolean;
|
|
50
|
+
bypassQueue?: boolean;
|
|
51
|
+
isolation?: "worktree";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The public service contract for cross-extension subagent access. */
|
|
55
|
+
export interface SubagentsService {
|
|
56
|
+
/** Spawn an agent. Returns the agent ID immediately. */
|
|
57
|
+
spawn(type: string, prompt: string, options?: SpawnOptions): string;
|
|
58
|
+
|
|
59
|
+
/** Get a snapshot of an agent's current state. */
|
|
60
|
+
getRecord(id: string): SubagentRecord | undefined;
|
|
61
|
+
|
|
62
|
+
/** List all tracked agents, most recent first. */
|
|
63
|
+
listAgents(): SubagentRecord[];
|
|
64
|
+
|
|
65
|
+
/** Abort a running or queued agent. Returns false if not found. */
|
|
66
|
+
abort(id: string): boolean;
|
|
67
|
+
|
|
68
|
+
/** Send a steering message to a running agent. */
|
|
69
|
+
steer(id: string, message: string): Promise<boolean>;
|
|
70
|
+
|
|
71
|
+
/** Wait for all running and queued agents to complete. */
|
|
72
|
+
waitForAll(): Promise<void>;
|
|
73
|
+
|
|
74
|
+
/** Whether any agents are running or queued. */
|
|
75
|
+
hasRunning(): boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Event channel constants for pi.events subscriptions. */
|
|
79
|
+
export const SUBAGENT_EVENTS = {
|
|
80
|
+
STARTED: "subagents:started",
|
|
81
|
+
COMPLETED: "subagents:completed",
|
|
82
|
+
ACTIVITY: "subagents:activity",
|
|
83
|
+
} as const;
|
|
84
|
+
|
|
85
|
+
// ---- Accessor functions ----
|
|
86
|
+
|
|
87
|
+
const SERVICE_KEY = Symbol.for("@gotgenes/pi-subagents:service");
|
|
88
|
+
|
|
89
|
+
/** Publish the SubagentsService on globalThis for cross-extension access. */
|
|
90
|
+
export function publishSubagentsService(service: SubagentsService): void {
|
|
91
|
+
(globalThis as Record<symbol, unknown>)[SERVICE_KEY] = service;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Retrieve the published SubagentsService, or undefined if not yet published. */
|
|
95
|
+
export function getSubagentsService(): SubagentsService | undefined {
|
|
96
|
+
return (globalThis as Record<symbol, unknown>)[SERVICE_KEY] as
|
|
97
|
+
| SubagentsService
|
|
98
|
+
| undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Remove the SubagentsService from globalThis (call on shutdown/reload). */
|
|
102
|
+
export function unpublishSubagentsService(): void {
|
|
103
|
+
delete (globalThis as Record<symbol, unknown>)[SERVICE_KEY];
|
|
104
|
+
}
|