@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.
@@ -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.
@@ -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": "2.0.0",
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
@@ -12,20 +12,20 @@
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, type ExtensionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
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 { GroupJoinManager } from "./group-join.js";
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";
25
+ import { publishSubagentsService, unpublishSubagentsService } from "./service.js";
26
+ import { createSubagentsService } from "./service-adapter.js";
27
27
  import { applyAndEmitLoaded, type SubagentsSettings, saveAndEmitChanged } from "./settings.js";
28
- import { type AgentConfig, type AgentInvocation, type AgentRecord, type JoinMode, type NotificationDetails, type SubagentType } from "./types.js";
28
+ import { type AgentConfig, type AgentInvocation, type AgentRecord, type NotificationDetails, type SubagentType } from "./types.js";
29
29
  import {
30
30
  type AgentActivity,
31
31
  type AgentDetails,
@@ -246,8 +246,7 @@ export default function (pi: ExtensionAPI) {
246
246
  return line;
247
247
  }
248
248
 
249
- const all = [d, ...(d.others ?? [])];
250
- return new Text(all.map(renderOne).join("\n"), 0, 0);
249
+ return new Text(renderOne(d), 0, 0);
251
250
  }
252
251
  );
253
252
 
@@ -307,40 +306,6 @@ export default function (pi: ExtensionAPI) {
307
306
  widget.update();
308
307
  }
309
308
 
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
309
  /** Helper: build event data for lifecycle events from an AgentRecord. */
345
310
  function buildEventData(record: AgentRecord) {
346
311
  const durationMs = record.completedAt ? record.completedAt - record.startedAt : Date.now() - record.startedAt;
@@ -366,7 +331,7 @@ export default function (pi: ExtensionAPI) {
366
331
  };
367
332
  }
368
333
 
369
- // Background completion: route through group join or send individual nudge
334
+ // Background completion: emit lifecycle event and send individual nudge
370
335
  const manager = new AgentManager((record) => {
371
336
  // Emit lifecycle event based on terminal status
372
337
  const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
@@ -392,19 +357,7 @@ export default function (pi: ExtensionAPI) {
392
357
  return;
393
358
  }
394
359
 
395
- // If this agent is pending batch finalization (debounce window still open),
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
360
+ sendIndividualNudge(record);
408
361
  widget.update();
409
362
  }, undefined, (record) => {
410
363
  // Emit started event when agent transitions to running (including from queue)
@@ -425,23 +378,19 @@ export default function (pi: ExtensionAPI) {
425
378
  });
426
379
  });
427
380
 
428
- // Expose manager via Symbol.for() global registry for cross-package access.
429
- // Standard Node.js pattern for cross-package singletons (used by OpenTelemetry, etc.).
430
- const MANAGER_KEY = Symbol.for("pi-subagents:manager");
431
- (globalThis as any)[MANAGER_KEY] = {
432
- waitForAll: () => manager.waitForAll(),
433
- hasRunning: () => manager.hasRunning(),
434
- spawn: (piRef: any, ctx: any, type: string, prompt: string, options: any) =>
435
- manager.spawn(piRef, ctx, type, prompt, options),
436
- getRecord: (id: string) => manager.getRecord(id),
437
- };
438
-
439
- // --- Cross-extension RPC via pi.events ---
440
- let currentCtx: ExtensionContext | undefined;
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);
441
391
 
442
- // Capture ctx from session_start for RPC spawn handler.
443
392
  pi.on("session_start", async (_event, ctx) => {
444
- currentCtx = ctx;
393
+ currentCtx = { pi, ctx };
445
394
  manager.clearCompleted();
446
395
  });
447
396
 
@@ -449,24 +398,11 @@ export default function (pi: ExtensionAPI) {
449
398
  manager.clearCompleted();
450
399
  });
451
400
 
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
401
  // On shutdown, abort all agents immediately and clean up.
463
402
  // If the session is going down, there's nothing left to consume agent results.
464
403
  pi.on("session_shutdown", async () => {
465
- unsubSpawnRpc();
466
- unsubStopRpc();
467
- unsubPingRpc();
404
+ unpublishSubagentsService();
468
405
  currentCtx = undefined;
469
- delete (globalThis as any)[MANAGER_KEY];
470
406
  manager.abortAll();
471
407
  for (const timer of pendingNudges.values()) clearTimeout(timer);
472
408
  pendingNudges.clear();
@@ -476,55 +412,6 @@ export default function (pi: ExtensionAPI) {
476
412
  // Live widget: show running agents above editor
477
413
  const widget = new AgentWidget(manager, agentActivity);
478
414
 
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
415
  // Grab UI context from first tool execution + clear lingering widget on new turn
529
416
  pi.on("tool_execution_start", async (_event, ctx) => {
530
417
  widget.setUICtx(ctx.ui as UICtx);
@@ -574,7 +461,6 @@ export default function (pi: ExtensionAPI) {
574
461
  setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
575
462
  setDefaultMaxTurns,
576
463
  setGraceTurns,
577
- setDefaultJoinMode,
578
464
  },
579
465
  (event, payload) => pi.events.emit(event, payload),
580
466
  );
@@ -870,28 +756,15 @@ Guidelines:
870
756
  return textResult(err instanceof Error ? err.message : String(err));
871
757
  }
872
758
 
873
- // Set output file + join mode synchronously after spawn, before the
759
+ // Set output file synchronously after spawn, before the
874
760
  // event loop yields — onSessionCreated is async so this is safe.
875
- const joinMode = resolveJoinMode(defaultJoinMode, true);
876
761
  const record = manager.getRecord(id);
877
- if (record && joinMode) {
878
- record.joinMode = joinMode;
762
+ if (record) {
879
763
  record.toolCallId = toolCallId;
880
764
  record.outputFile = createOutputFilePath(ctx.cwd, id, ctx.sessionManager.getSessionId());
881
765
  writeInitialEntry(record.outputFile, id, params.prompt, ctx.cwd);
882
766
  }
883
767
 
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
768
  agentActivity.set(id, bgState);
896
769
  widget.ensureTimer();
897
770
  widget.update();
@@ -1678,7 +1551,6 @@ ${systemPrompt}
1678
1551
  // normalizeMaxTurns() in agent-runner.ts (which maps 0 → undefined).
1679
1552
  defaultMaxTurns: getDefaultMaxTurns() ?? 0,
1680
1553
  graceTurns: getGraceTurns(),
1681
- defaultJoinMode: getDefaultJoinMode(),
1682
1554
  };
1683
1555
  }
1684
1556
 
@@ -1687,7 +1559,6 @@ ${systemPrompt}
1687
1559
  `Max concurrency (current: ${manager.getMaxConcurrent()})`,
1688
1560
  `Default max turns (current: ${getDefaultMaxTurns() ?? "unlimited"})`,
1689
1561
  `Grace turns (current: ${getGraceTurns()})`,
1690
- `Join mode (current: ${getDefaultJoinMode()})`,
1691
1562
  ]);
1692
1563
  if (!choice) return;
1693
1564
 
@@ -1727,17 +1598,6 @@ ${systemPrompt}
1727
1598
  ctx.ui.notify("Must be a positive integer.", "warning");
1728
1599
  }
1729
1600
  }
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
1601
  }
1742
1602
  }
1743
1603
 
@@ -1,4 +1,4 @@
1
- import type { AgentConfig, IsolationMode, JoinMode, ThinkingLevel } from "./types.js";
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
- }