@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
|
@@ -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": "
|
|
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,
|
|
15
|
+
import { defineTool, type ExtensionAPI, type ExtensionCommandContext, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { Text } from "@earendil-works/pi-tui";
|
|
17
17
|
import { Type } from "@sinclair/typebox";
|
|
18
18
|
import { AgentManager } from "./agent-manager.js";
|
|
19
19
|
import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
20
20
|
import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, resolveType } from "./agent-types.js";
|
|
21
|
-
import { registerRpcHandlers } from "./cross-extension-rpc.js";
|
|
22
21
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
23
|
-
import {
|
|
24
|
-
import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
|
|
22
|
+
import { resolveAgentInvocationConfig } from "./invocation-config.js";
|
|
25
23
|
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
26
24
|
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
package/src/invocation-config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentConfig, IsolationMode,
|
|
1
|
+
import type { AgentConfig, IsolationMode, ThinkingLevel } from "./types.js";
|
|
2
2
|
|
|
3
3
|
interface AgentInvocationParams {
|
|
4
4
|
model?: string;
|
|
@@ -34,7 +34,3 @@ export function resolveAgentInvocationConfig(
|
|
|
34
34
|
isolation: agentConfig?.isolation ?? params.isolation,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
export function resolveJoinMode(defaultJoinMode: JoinMode, runInBackground: boolean): JoinMode | undefined {
|
|
39
|
-
return runInBackground ? defaultJoinMode : undefined;
|
|
40
|
-
}
|