@gotgenes/pi-subagents 5.8.0 → 5.8.2
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 +18 -0
- package/docs/architecture/architecture.md +9 -10
- package/docs/plans/0066-replace-as-any-with-sdk-types.md +215 -0
- package/docs/plans/0077-inject-project-agents-dir.md +115 -0
- package/docs/retro/0066-replace-as-any-with-sdk-types.md +44 -0
- package/docs/retro/0070-extract-event-handlers.md +49 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +1 -1
- package/src/index.ts +10 -9
- package/src/notification.ts +4 -1
- package/src/tools/agent-tool.ts +9 -7
- package/src/tools/get-result-tool.ts +2 -1
- package/src/tools/steer-tool.ts +2 -1
- package/src/ui/agent-menu.ts +25 -37
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ 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
|
+
## [5.8.2](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.8.1...pi-subagents-v5.8.2) (2026-05-20)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* plan inject projectAgentsDir into AgentMenuDeps ([#77](https://github.com/gotgenes/pi-packages/issues/77)) ([d8cf039](https://github.com/gotgenes/pi-packages/commit/d8cf03944d0abc6230541d67655d89582665a615))
|
|
14
|
+
* **retro:** add retro notes for issue [#66](https://github.com/gotgenes/pi-packages/issues/66) ([ce0f04a](https://github.com/gotgenes/pi-packages/commit/ce0f04a3d84523a4c2e8b7bc998b5bec0f16970f))
|
|
15
|
+
|
|
16
|
+
## [5.8.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.8.0...pi-subagents-v5.8.1) (2026-05-20)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Documentation
|
|
20
|
+
|
|
21
|
+
* plan replace as-any casts with SDK types ([#66](https://github.com/gotgenes/pi-packages/issues/66)) ([a8728bf](https://github.com/gotgenes/pi-packages/commit/a8728bf695a11f649fb908a6a31a55842b76ce32))
|
|
22
|
+
* **retro:** add retro notes for issue [#70](https://github.com/gotgenes/pi-packages/issues/70) ([0668956](https://github.com/gotgenes/pi-packages/commit/0668956e99383df7a62fddde57dd4182bf491a5e))
|
|
23
|
+
* **retro:** record architecture update for [#70](https://github.com/gotgenes/pi-packages/issues/70) ([49d4fae](https://github.com/gotgenes/pi-packages/commit/49d4faee4ee711e385dbf59334b0b737897d26a6))
|
|
24
|
+
* update architecture roadmap with [#87](https://github.com/gotgenes/pi-packages/issues/87), [#70](https://github.com/gotgenes/pi-packages/issues/70) status ([97a2da1](https://github.com/gotgenes/pi-packages/commit/97a2da11cbd73c9f84eb2158e2437cbe8c749208))
|
|
25
|
+
|
|
8
26
|
## [5.8.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.7.0...pi-subagents-v5.8.0) (2026-05-20)
|
|
9
27
|
|
|
10
28
|
|
|
@@ -374,14 +374,13 @@ These build on Phase 1 and should land after it.
|
|
|
374
374
|
- Removed all runtime imports of `agent-runner.ts` and `worktree.ts` from `agent-manager.ts` (only `import type` remains).
|
|
375
375
|
- Migrated all tests from `vi.mock()` module stubs to `vi.fn()` interface stubs.
|
|
376
376
|
|
|
377
|
-
3. **gotgenes/pi-packages#87** — Evolve `SubagentRuntime` from data bag to object with methods
|
|
378
|
-
-
|
|
377
|
+
3. **gotgenes/pi-packages#87** ✓ — Evolve `SubagentRuntime` from data bag to object with methods
|
|
378
|
+
- Added session-context methods (`setSessionContext`, `clearSessionContext`) and widget delegation methods (`setUICtx`, `onTurnStart`, `markFinished`, `updateWidget`, `ensureTimer`).
|
|
379
379
|
- Prerequisite for #70 — without runtime methods, extracted handlers would move LoD violations and output-argument smells into handler classes.
|
|
380
380
|
|
|
381
|
-
4. **gotgenes/pi-packages#70** — Extract event handlers into `src/handlers/`
|
|
382
|
-
-
|
|
383
|
-
-
|
|
384
|
-
- Target: `src/index.ts` ≤150 lines.
|
|
381
|
+
4. **gotgenes/pi-packages#70** ✓ — Extract event handlers into `src/handlers/`
|
|
382
|
+
- Moved the four inline lambdas (`session_start`, `session_before_switch`, `session_shutdown`, `tool_execution_start`) into `SessionLifecycleHandler` and `ToolStartHandler` classes.
|
|
383
|
+
- Handlers call methods on narrow runtime interfaces — no raw field writes, no `widget!` reach-throughs.
|
|
385
384
|
|
|
386
385
|
### Phase 3: Interface polish
|
|
387
386
|
|
|
@@ -407,7 +406,7 @@ Small cleanups that are safest after the structural changes settle.
|
|
|
407
406
|
### Dependency graph
|
|
408
407
|
|
|
409
408
|
```text
|
|
410
|
-
#69 (SubagentRuntime) ✓ ──► #87 (runtime methods) ─┬─► #70 (handler extraction)
|
|
409
|
+
#69 (SubagentRuntime) ✓ ──► #87 (runtime methods) ✓ ─┬─► #70 (handler extraction) ✓
|
|
411
410
|
│
|
|
412
411
|
#71 (pure assembler) ✓ │
|
|
413
412
|
#80 (config lookup) ✓ │
|
|
@@ -427,11 +426,11 @@ Small cleanups that are safest after the structural changes settle.
|
|
|
427
426
|
The recommended sequence is:
|
|
428
427
|
|
|
429
428
|
```text
|
|
430
|
-
#69 ✓ → #71 ✓ → #80 ✓ → #76 ✓ → #84 ✓ → #72 ✓ → #87 → #70 → #66 → #77 → #61
|
|
429
|
+
#69 ✓ → #71 ✓ → #80 ✓ → #76 ✓ → #84 ✓ → #72 ✓ → #87 ✓ → #70 ✓ → #66 → #77 → #61
|
|
431
430
|
```
|
|
432
431
|
|
|
433
|
-
Phase 1 is complete; Phase 2 is
|
|
434
|
-
The next issue is #
|
|
432
|
+
Phase 1 is complete; Phase 2 is complete.
|
|
433
|
+
The next issue is #66 (replace `as any` casts with proper SDK types).
|
|
435
434
|
Issue #22 is a parallel cross-extension track and does not gate the structural work.
|
|
436
435
|
|
|
437
436
|
## Relationship with upstream
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 66
|
|
3
|
+
issue_title: "refactor: replace `as any` casts in extracted tool/menu factories with proper SDK types"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Replace `as any` casts in factory dep interfaces with proper SDK types
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
The decomposition in #54 introduced 14 `as any` casts at the wiring boundary in `src/index.ts`.
|
|
11
|
+
These exist because the factory dep interfaces (`AgentToolManager`, `GetResultDeps`, `SteerToolDeps`, `AgentMenuDeps`, `NotificationDeps`) declare `unknown` or `object` for parameters that are actually well-typed SDK exports.
|
|
12
|
+
Every cast papers over a real type that the SDK already exports.
|
|
13
|
+
For comparison, `pi-permission-system` imports SDK types directly in its internal interfaces and has zero `as any` casts.
|
|
14
|
+
|
|
15
|
+
## Goals
|
|
16
|
+
|
|
17
|
+
- Replace every `as any` cast in `src/index.ts` by typing the corresponding dep interface parameter with the actual SDK type.
|
|
18
|
+
- Keep factory modules decoupled from `AgentManager` where possible — use shared types (`types.ts`) or SDK imports.
|
|
19
|
+
- No runtime behavior change — this is purely a type-safety improvement.
|
|
20
|
+
- Existing tests continue to pass without modification (factory `execute` methods use `ctx: any` or `ctx: unknown`, so test mocks are unaffected).
|
|
21
|
+
|
|
22
|
+
## Non-Goals
|
|
23
|
+
|
|
24
|
+
- Fixing `as any` casts in `agent-runner.ts`, `conversation-viewer.ts`, `tools/helpers.ts`, or `tools/agent-tool.ts:550` — those access untyped SDK message internals, a different concern.
|
|
25
|
+
- Removing the `ctx as UICtx` cast (line 196) — that is a named cast, not `as any`.
|
|
26
|
+
- Removing `as any` from test files (e.g., `agent-menu.test.ts`'s `ctx as any`) — test mocks are intentionally narrow.
|
|
27
|
+
|
|
28
|
+
## Background
|
|
29
|
+
|
|
30
|
+
### Current cast inventory (`src/index.ts`)
|
|
31
|
+
|
|
32
|
+
| Line | Cast | Root cause |
|
|
33
|
+
| ---- | ----------------------------------------------- | --------------------------------------------------------------------------- |
|
|
34
|
+
| 59 | `msg as any, opts as any` | `NotificationDeps.sendMessage(msg: unknown, opts: unknown)` |
|
|
35
|
+
| 188 | `ctx as any, opts as any` | `AgentToolManager.spawn(ctx: unknown, …, opts: object)` |
|
|
36
|
+
| 189 | `ctx as any, opts as any` | `AgentToolManager.spawnAndWait(ctx: unknown, …, opts: object)` |
|
|
37
|
+
| 208 | `}) as any` | Cascading from above — `createAgentTool` return doesn't match `defineTool` |
|
|
38
|
+
| 215 | `session as any` | `GetResultDeps.getConversation(session: unknown)` |
|
|
39
|
+
| 223 | `session as any` | `SteerToolDeps.steerAgent(session: unknown, …)` |
|
|
40
|
+
| 232 | `(piArg ?? pi) as any, ctx as any, opts as any` | `AgentMenuManager.spawnAndWait(pi: unknown, ctx: unknown, …, opts: object)` |
|
|
41
|
+
| 242 | `registry as any` | `AgentMenuDeps.getModelLabel(…, registry?: unknown)` |
|
|
42
|
+
| 271 | `ctx as any` | `MenuContext` not structurally compatible with `ExtensionCommandContext` |
|
|
43
|
+
|
|
44
|
+
### SDK types available
|
|
45
|
+
|
|
46
|
+
All are exported from `@earendil-works/pi-coding-agent`:
|
|
47
|
+
|
|
48
|
+
- `ExtensionContext` — tool execute context (has `ui`, `modelRegistry`, `cwd`, etc.)
|
|
49
|
+
- `ExtensionCommandContext` — command handler context (extends `ExtensionContext`)
|
|
50
|
+
- `ExtensionAPI` — the `pi` object passed to extensions
|
|
51
|
+
- `AgentSession` — session handle (has `steer()`, `messages`, etc.)
|
|
52
|
+
|
|
53
|
+
`ModelRegistry` is already defined locally in `src/model-resolver.ts` and matches the SDK shape.
|
|
54
|
+
`SpawnOptions` is defined locally in `src/agent-manager.ts` (not currently exported).
|
|
55
|
+
|
|
56
|
+
### Relevant AGENTS.md constraints
|
|
57
|
+
|
|
58
|
+
- "Avoid `any` unless absolutely necessary."
|
|
59
|
+
- "Keep Pi SDK imports out of business-logic modules."
|
|
60
|
+
Tool definitions, event handlers, and command handlers are SDK consumers — they may import SDK types directly.
|
|
61
|
+
The restriction targets pure helpers, utilities, and domain modules.
|
|
62
|
+
- "When writing event handlers that consume Pi SDK types, prefer lean local payload interfaces over full SDK event types."
|
|
63
|
+
|
|
64
|
+
The factory modules (`tools/agent-tool.ts`, `tools/get-result-tool.ts`, `tools/steer-tool.ts`, `ui/agent-menu.ts`) are SDK consumers (they define tools and command handlers), so importing SDK types is acceptable.
|
|
65
|
+
The notification module (`notification.ts`) is a pure helper — it should use narrow local types rather than SDK imports.
|
|
66
|
+
|
|
67
|
+
## Design Overview
|
|
68
|
+
|
|
69
|
+
### Strategy per interface
|
|
70
|
+
|
|
71
|
+
1. **`NotificationDeps.sendMessage`** — define narrow inline parameter types matching `ExtensionAPI.sendMessage`'s shape.
|
|
72
|
+
No SDK import needed; the notification module stays SDK-independent.
|
|
73
|
+
|
|
74
|
+
2. **`AgentToolManager`** — import `ExtensionContext` for `ctx`, export + import `SpawnOptions` from `agent-manager.ts` for `opts`.
|
|
75
|
+
The tool module is an SDK consumer, so SDK imports are acceptable.
|
|
76
|
+
|
|
77
|
+
3. **`AgentToolWidget.setUICtx`** — stays `ctx: unknown` (not `as any`; currently `ctx as UICtx`).
|
|
78
|
+
Out of scope.
|
|
79
|
+
|
|
80
|
+
4. **`GetResultDeps.getConversation`** — import `AgentSession` for `session`.
|
|
81
|
+
|
|
82
|
+
5. **`SteerToolDeps.steerAgent`** — import `AgentSession` for `session`.
|
|
83
|
+
|
|
84
|
+
6. **`AgentMenuManager.spawnAndWait`** — import `ExtensionAPI` for `pi`, `ExtensionContext` for `ctx`, export + import `SpawnOptions` for `opts`.
|
|
85
|
+
|
|
86
|
+
7. **`AgentMenuDeps.getModelLabel`** — import `ModelRegistry` from `../model-resolver.js` for `registry`.
|
|
87
|
+
|
|
88
|
+
8. **`MenuContext` structural compatibility** — switch `MenuUI` from property syntax (strict function types) to method syntax (bivariant), and type `modelRegistry` as `ModelRegistry`.
|
|
89
|
+
This makes `ExtensionCommandContext` structurally assignable to `MenuContext`, removing the cast without broadening the handler's dependency on the full SDK type.
|
|
90
|
+
Tests continue using narrow mocks because the handler's parameter is still the narrow `MenuContext`.
|
|
91
|
+
|
|
92
|
+
### `SpawnOptions` export
|
|
93
|
+
|
|
94
|
+
`SpawnOptions` in `agent-manager.ts` is currently a private interface.
|
|
95
|
+
Exporting it as a named `type` export adds no runtime cost and avoids duplicating the 15-field type in each factory.
|
|
96
|
+
Both `tools/agent-tool.ts` and `ui/agent-menu.ts` already import `AgentRecord` from `types.ts`, so an intra-package `import type` from `agent-manager.ts` follows the same pattern.
|
|
97
|
+
|
|
98
|
+
### Cascading cast resolution
|
|
99
|
+
|
|
100
|
+
The `createAgentTool({…}) as any` cast on line 208 exists because TypeScript cannot verify the returned object satisfies `ToolDefinition` when inner types are `unknown`/`object`.
|
|
101
|
+
Once the inner types are correct, the factory return type matches `ToolDefinition` naturally, and the outer `as any` cast resolves without further changes.
|
|
102
|
+
|
|
103
|
+
## Module-Level Changes
|
|
104
|
+
|
|
105
|
+
### `src/agent-manager.ts`
|
|
106
|
+
|
|
107
|
+
- Export the existing `SpawnOptions` interface (add `export` keyword).
|
|
108
|
+
|
|
109
|
+
### `src/notification.ts`
|
|
110
|
+
|
|
111
|
+
- Replace `sendMessage: (msg: unknown, opts: unknown) => void` with narrow inline types:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
sendMessage: (
|
|
115
|
+
msg: { customType: string; content: string; display?: boolean; details?: unknown },
|
|
116
|
+
opts?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
117
|
+
) => void;
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### `src/tools/agent-tool.ts`
|
|
121
|
+
|
|
122
|
+
- Add `import type { ExtensionContext } from "@earendil-works/pi-coding-agent";`
|
|
123
|
+
- Add `import type { SpawnOptions } from "../agent-manager.js";`
|
|
124
|
+
- `AgentToolManager.spawn`: `ctx: unknown` → `ctx: ExtensionContext`, `opts: object` → `opts: SpawnOptions`
|
|
125
|
+
- `AgentToolManager.spawnAndWait`: same changes, opts as `Omit<SpawnOptions, "isBackground">`
|
|
126
|
+
|
|
127
|
+
### `src/tools/get-result-tool.ts`
|
|
128
|
+
|
|
129
|
+
- Add `import type { AgentSession } from "@earendil-works/pi-coding-agent";`
|
|
130
|
+
- `GetResultDeps.getConversation`: `session: unknown` → `session: AgentSession`
|
|
131
|
+
|
|
132
|
+
### `src/tools/steer-tool.ts`
|
|
133
|
+
|
|
134
|
+
- Add `import type { AgentSession } from "@earendil-works/pi-coding-agent";`
|
|
135
|
+
- `SteerToolDeps.steerAgent`: `session: unknown` → `session: AgentSession`
|
|
136
|
+
|
|
137
|
+
### `src/ui/agent-menu.ts`
|
|
138
|
+
|
|
139
|
+
- Add `import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";`
|
|
140
|
+
- Add `import type { SpawnOptions } from "../agent-manager.js";`
|
|
141
|
+
- Add `import type { ModelRegistry } from "../model-resolver.js";`
|
|
142
|
+
- `AgentMenuManager.spawnAndWait`: `pi: unknown` → `pi: ExtensionAPI | null`, `ctx: unknown` → `ctx: ExtensionContext`, `opts: object` → `opts: Omit<SpawnOptions, "isBackground">`
|
|
143
|
+
- `AgentMenuDeps.getModelLabel`: `registry?: unknown` → `registry?: ModelRegistry`
|
|
144
|
+
- `MenuUI`: switch from property syntax to method syntax for structural compatibility with `ExtensionUIContext`.
|
|
145
|
+
Update `notify` second parameter from `level: string` to `type?: string` to match SDK.
|
|
146
|
+
- `MenuContext.modelRegistry`: `unknown` → `ModelRegistry`
|
|
147
|
+
|
|
148
|
+
### `src/index.ts`
|
|
149
|
+
|
|
150
|
+
- Remove all 14 `as any` casts.
|
|
151
|
+
- No new imports needed (types already used transitively).
|
|
152
|
+
|
|
153
|
+
## Test Impact Analysis
|
|
154
|
+
|
|
155
|
+
1. **No new unit tests needed** — this is a type-only refactoring with no behavioral change.
|
|
156
|
+
2. **No existing tests become redundant** — the tests exercise tool execution and menu behavior, not type signatures.
|
|
157
|
+
3. **Existing tests must stay as-is** — factory `execute` methods use `ctx: any` or `ctx: unknown`, so `makeCtx()` test helpers remain compatible.
|
|
158
|
+
Test-side `as any` casts (e.g., `handler(ctx as any)` in `agent-menu.test.ts`) are out of scope.
|
|
159
|
+
|
|
160
|
+
## TDD Order
|
|
161
|
+
|
|
162
|
+
Since this is a type-only change, each step is verified by `pnpm run check` (tsc) rather than new vitest tests.
|
|
163
|
+
The full test suite (`pnpm vitest run`) is run at the end as a regression check.
|
|
164
|
+
|
|
165
|
+
1. **Export `SpawnOptions`** — add `export` to the existing `interface SpawnOptions` in `agent-manager.ts`.
|
|
166
|
+
Verify: `pnpm run check`.
|
|
167
|
+
Commit: `refactor: export SpawnOptions from agent-manager`
|
|
168
|
+
|
|
169
|
+
2. **Type `NotificationDeps`** — replace `(msg: unknown, opts: unknown)` with narrow inline types in `notification.ts`.
|
|
170
|
+
Remove `msg as any, opts as any` casts (line 59) in `index.ts`.
|
|
171
|
+
Verify: `pnpm run check`.
|
|
172
|
+
Commit: `refactor: type NotificationDeps.sendMessage parameters (#66)`
|
|
173
|
+
|
|
174
|
+
3. **Type `AgentToolManager`** — import `ExtensionContext` and `SpawnOptions`, update `spawn` and `spawnAndWait` signatures in `agent-tool.ts`.
|
|
175
|
+
Remove `ctx as any, opts as any` casts (lines 188–189) and the cascading `}) as any` cast (line 208) in `index.ts`.
|
|
176
|
+
Verify: `pnpm run check`.
|
|
177
|
+
Commit: `refactor: type AgentToolManager with ExtensionContext and SpawnOptions (#66)`
|
|
178
|
+
|
|
179
|
+
4. **Type `GetResultDeps`** — import `AgentSession`, update `getConversation` in `get-result-tool.ts`.
|
|
180
|
+
Remove `session as any` cast (line 215) in `index.ts`.
|
|
181
|
+
Verify: `pnpm run check`.
|
|
182
|
+
Commit: `refactor: type GetResultDeps.getConversation with AgentSession (#66)`
|
|
183
|
+
|
|
184
|
+
5. **Type `SteerToolDeps`** — import `AgentSession`, update `steerAgent` in `steer-tool.ts`.
|
|
185
|
+
Remove `session as any` cast (line 223) in `index.ts`.
|
|
186
|
+
Verify: `pnpm run check`.
|
|
187
|
+
Commit: `refactor: type SteerToolDeps.steerAgent with AgentSession (#66)`
|
|
188
|
+
|
|
189
|
+
6. **Type `AgentMenuManager` + `AgentMenuDeps`** — import `ExtensionAPI`, `ExtensionContext`, `SpawnOptions`, `ModelRegistry`.
|
|
190
|
+
Update `spawnAndWait` and `getModelLabel` signatures in `agent-menu.ts`.
|
|
191
|
+
Remove `(piArg ?? pi) as any, ctx as any, opts as any` (line 232) and `registry as any` (line 242) casts in `index.ts`.
|
|
192
|
+
Verify: `pnpm run check`.
|
|
193
|
+
Commit: `refactor: type AgentMenu interfaces with SDK types (#66)`
|
|
194
|
+
|
|
195
|
+
7. **Align `MenuContext` with `ExtensionCommandContext`** — switch `MenuUI` to method syntax, fix `notify` parameter, type `modelRegistry` as `ModelRegistry`.
|
|
196
|
+
Remove `ctx as any` cast (line 271) in `index.ts`.
|
|
197
|
+
Verify: `pnpm run check`.
|
|
198
|
+
Commit: `refactor: align MenuContext for structural ExtensionCommandContext compat (#66)`
|
|
199
|
+
|
|
200
|
+
8. **Final verification** — run `pnpm vitest run` for the full test suite.
|
|
201
|
+
Grep `src/index.ts` for remaining `as any` — expect zero.
|
|
202
|
+
Commit: none (verification only).
|
|
203
|
+
|
|
204
|
+
## Risks and Mitigations
|
|
205
|
+
|
|
206
|
+
| Risk | Mitigation |
|
|
207
|
+
| ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
208
|
+
| Exporting `SpawnOptions` from `agent-manager.ts` couples factory modules to the manager's type | The coupling is type-only (`import type`), adds no runtime dependency, and follows the existing pattern (`AgentRecord` in `types.ts` is already shared). |
|
|
209
|
+
| `MenuUI` method-syntax change could break structural compatibility with test mocks | Test mocks use `vi.fn()` which is bivariant; method syntax on the `MenuUI` interface only affects how TypeScript checks assignability from `ExtensionUIContext`, not from mock objects. |
|
|
210
|
+
| SDK type exports could change in a future Pi release | The SDK types used (`ExtensionContext`, `AgentSession`, `ExtensionAPI`) are stable public API. `ModelRegistry` is a local interface. |
|
|
211
|
+
| Cascading `as any` on `createAgentTool` may not resolve automatically | If TypeScript still cannot infer the return type, add explicit `satisfies ToolDefinition<…>` or a return type annotation. Verified in step 3. |
|
|
212
|
+
|
|
213
|
+
## Open Questions
|
|
214
|
+
|
|
215
|
+
- None — the issue's "Proposed change" section is unambiguous and all SDK types are confirmed available.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 77
|
|
3
|
+
issue_title: "refactor: add projectAgentsDir to AgentMenuDeps instead of reading process.cwd() inline"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Inject projectAgentsDir into AgentMenuDeps
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`createAgentsMenuHandler` computes the project agents directory by reading `process.cwd()` inline via a lambda on line 63 of `ui/agent-menu.ts`:
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The `AgentMenuDeps` interface already carries `personalAgentsDir: string` as an explicit field, but the project-side equivalent bypasses the injection boundary entirely.
|
|
17
|
+
`projectAgentsDir()` is called in at least five places inside the handler (`findAgentFile`, `ejectAgent`, `disableAgent`, `showCreateWizard`, `showManualWizard`).
|
|
18
|
+
This violates the code-style rule: "Do not read `process.cwd()` inside library/utility functions — accept the value as a parameter."
|
|
19
|
+
|
|
20
|
+
## Goals
|
|
21
|
+
|
|
22
|
+
- Add `projectAgentsDir: string` to `AgentMenuDeps`.
|
|
23
|
+
- Remove the inline `projectAgentsDir` lambda from `createAgentsMenuHandler`.
|
|
24
|
+
- Replace all five call sites with `deps.projectAgentsDir`.
|
|
25
|
+
- Wire the value at the call site in `index.ts` as `join(process.cwd(), ".pi", "agents")`.
|
|
26
|
+
- Update the test helper `makeDeps()` in `agent-menu.test.ts` to supply the new field.
|
|
27
|
+
|
|
28
|
+
## Non-Goals
|
|
29
|
+
|
|
30
|
+
- Removing other `process.cwd()` calls in `index.ts` (e.g., `loadCustomAgents`, `GitWorktreeManager`).
|
|
31
|
+
Those are separate concerns tracked or already addressed elsewhere (e.g., #76 for `AgentManager`).
|
|
32
|
+
- Changing `personalAgentsDir` wiring or any other `AgentMenuDeps` fields.
|
|
33
|
+
|
|
34
|
+
## Background
|
|
35
|
+
|
|
36
|
+
### Relevant modules
|
|
37
|
+
|
|
38
|
+
| Module | Role |
|
|
39
|
+
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
+
| `src/ui/agent-menu.ts` | Contains `AgentMenuDeps` interface and `createAgentsMenuHandler` factory. Owns the inline `projectAgentsDir` lambda. |
|
|
41
|
+
| `src/index.ts` | Extension entry point. Constructs the `AgentMenuDeps` object at line 228. Already passes `personalAgentsDir`. |
|
|
42
|
+
| `test/ui/agent-menu.test.ts` | Tests for the menu handler. Has a `makeDeps()` helper that constructs `AgentMenuDeps`. |
|
|
43
|
+
|
|
44
|
+
### Constraints
|
|
45
|
+
|
|
46
|
+
From code-style skill:
|
|
47
|
+
|
|
48
|
+
> Do not read `process.env`, `process.cwd()`, or `process.platform` inside library/utility functions — accept the value as a parameter.
|
|
49
|
+
|
|
50
|
+
This is the same pattern applied in #76 (inject `cwd` into `AgentManager`), now applied to the UI layer.
|
|
51
|
+
|
|
52
|
+
`AgentMenuDeps` is internal — the public API surface (`exports` in `package.json`) is `service.ts` only, so this is a non-breaking change for consumers.
|
|
53
|
+
|
|
54
|
+
## Design Overview
|
|
55
|
+
|
|
56
|
+
The change is mechanical — add a field, remove a lambda, replace call sites:
|
|
57
|
+
|
|
58
|
+
1. Add `projectAgentsDir: string` to `AgentMenuDeps` (mirrors the existing `personalAgentsDir` field).
|
|
59
|
+
2. Delete the `const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");` lambda inside `createAgentsMenuHandler`.
|
|
60
|
+
3. Replace all five `projectAgentsDir()` call sites with `deps.projectAgentsDir`:
|
|
61
|
+
- `findAgentFile` (line 68)
|
|
62
|
+
- `ejectAgent` (line 312)
|
|
63
|
+
- `disableAgent` (line 378)
|
|
64
|
+
- `showCreateWizard` (line 416)
|
|
65
|
+
- `showManualWizard` — uses the `targetDir` value from `showCreateWizard`, so not a direct call site
|
|
66
|
+
4. At the call site in `index.ts`, add `projectAgentsDir: join(process.cwd(), ".pi", "agents")` to the deps object.
|
|
67
|
+
5. In `makeDeps()`, add `projectAgentsDir: "/test-project/.pi/agents"`.
|
|
68
|
+
|
|
69
|
+
No new types, no interface changes beyond the one added field.
|
|
70
|
+
|
|
71
|
+
## Module-Level Changes
|
|
72
|
+
|
|
73
|
+
### `src/ui/agent-menu.ts`
|
|
74
|
+
|
|
75
|
+
- Add `projectAgentsDir: string` to the `AgentMenuDeps` interface.
|
|
76
|
+
- Remove the `const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");` lambda.
|
|
77
|
+
- Replace `projectAgentsDir()` with `deps.projectAgentsDir` at all call sites inside the factory closure.
|
|
78
|
+
- The `join` import from `node:path` may become unused in this file if no other call uses it — verify and remove if so.
|
|
79
|
+
|
|
80
|
+
### `src/index.ts`
|
|
81
|
+
|
|
82
|
+
- Add `projectAgentsDir: join(process.cwd(), ".pi", "agents")` to the `createAgentsMenuHandler({...})` call.
|
|
83
|
+
- Import `join` from `node:path` if not already imported.
|
|
84
|
+
|
|
85
|
+
### `test/ui/agent-menu.test.ts`
|
|
86
|
+
|
|
87
|
+
- Add `projectAgentsDir: "/test-project/.pi/agents"` to `makeDeps()`.
|
|
88
|
+
- No other test logic changes.
|
|
89
|
+
|
|
90
|
+
## Test Impact Analysis
|
|
91
|
+
|
|
92
|
+
1. No new unit tests are strictly required — the refactoring is mechanical and preserves behavior.
|
|
93
|
+
However, a targeted test verifying that `findAgentFile` uses the injected `projectAgentsDir` (not `process.cwd()`) is valuable to prevent regression.
|
|
94
|
+
2. No existing tests become redundant.
|
|
95
|
+
3. All existing tests stay as-is; only the `makeDeps()` helper needs the new field.
|
|
96
|
+
|
|
97
|
+
## TDD Order
|
|
98
|
+
|
|
99
|
+
1. **Red: test that the injected projectAgentsDir is used** — add a test in `agent-menu.test.ts` that mocks `existsSync` and verifies `findAgentFile` (exercised via the menu handler) checks a path under the injected `projectAgentsDir`, not under `process.cwd()`.
|
|
100
|
+
Commit message: `test: verify projectAgentsDir injection in agent menu (#77)`
|
|
101
|
+
|
|
102
|
+
2. **Green: add projectAgentsDir to AgentMenuDeps and wire it** — add the field to `AgentMenuDeps`, remove the inline lambda, replace all call sites with `deps.projectAgentsDir`, wire the value in `index.ts`, and update `makeDeps()` in the test helper.
|
|
103
|
+
Commit message: `refactor: inject projectAgentsDir into AgentMenuDeps (#77)`
|
|
104
|
+
|
|
105
|
+
## Risks and Mitigations
|
|
106
|
+
|
|
107
|
+
| Risk | Mitigation |
|
|
108
|
+
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
109
|
+
| Missing a `projectAgentsDir()` call site | `grep 'projectAgentsDir'` in `agent-menu.ts` confirms exactly five call sites (one definition + four usages). After the change, grep again to verify no `process.cwd()` calls remain. |
|
|
110
|
+
| `join` import becomes unused in `agent-menu.ts` | Check whether `join` is used elsewhere in the file (it is — `findAgentFile` uses `join` for the personal path). The import stays. |
|
|
111
|
+
| Behavioral change | Production call site passes `join(process.cwd(), ".pi", "agents")`, which produces the same value the lambda computed. No behavioral change. |
|
|
112
|
+
|
|
113
|
+
## Open Questions
|
|
114
|
+
|
|
115
|
+
None — the issue's proposed change is unambiguous and mirrors the established `personalAgentsDir` pattern.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 66
|
|
3
|
+
issue_title: "refactor: replace `as any` casts in extracted tool/menu factories with proper SDK types"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #66 — replace `as any` casts with proper SDK types
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-20T18:50:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Replaced all 14 `as any` casts in `src/index.ts` by typing 5 factory dep interfaces with proper SDK types (`ExtensionContext`, `AgentSession`, `ExtensionAPI`, `ModelRegistry`) and the newly-exported `SpawnOptions`.
|
|
13
|
+
Released as `pi-subagents-v5.8.1` with zero behavioral change across 520 tests.
|
|
14
|
+
The plan, implementation, and shipping were completed in a single session with 6 implementation commits.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
#### What went well
|
|
19
|
+
|
|
20
|
+
- Thorough context-gathering during planning paid off: reading all 5 dep interfaces, the SDK `.d.ts` files, test mock helpers, and the `ExtensionUIContext` shape before writing the plan meant most steps landed on first `pnpm run check`.
|
|
21
|
+
- The plan's risk table anticipated the cascading `as any` issue on `createAgentTool` and prepared a mitigation ("add explicit `satisfies ToolDefinition<…>` if needed"), which was close to the actual fix needed.
|
|
22
|
+
- Steps 4 and 5 (`GetResultDeps`, `SteerToolDeps`) were trivially clean — single-type-import changes that compiled immediately.
|
|
23
|
+
|
|
24
|
+
#### What caused friction (agent side)
|
|
25
|
+
|
|
26
|
+
- `missing-context` — The plan assumed `NotificationDeps.sendMessage.display` could be optional (`display?: boolean`) but the SDK's `CustomMessage.display` is required `boolean`.
|
|
27
|
+
First `pnpm run check` after step 2 failed; required one extra edit-verify cycle.
|
|
28
|
+
Impact: added friction but no rework (fixed in the same commit).
|
|
29
|
+
|
|
30
|
+
- `missing-context` — The plan did not check the `execute` function's full signature in `agent-tool.ts`.
|
|
31
|
+
Removing the outer `as any` on `createAgentTool({...})` exposed three additional type mismatches: `onUpdate` parameter type (`unknown` vs `AgentToolResult<any>`), `signal` optionality (`AbortSignal` vs `AbortSignal | undefined`), and `params.description` (`unknown` vs `string`).
|
|
32
|
+
The plan flagged cascading risk but assumed only the return type would be affected.
|
|
33
|
+
Impact: step 3 required three extra edits and two extra `pnpm run check` cycles, noted as a deviation in the commit body.
|
|
34
|
+
|
|
35
|
+
- `missing-context` — Steps 6 and 7 had to be folded into one commit.
|
|
36
|
+
The plan listed them as independent steps but typing `AgentMenuManager.spawnAndWait(ctx: ExtensionContext)` immediately made `MenuContext` incompatible — `showGenerateWizard` passes `ctx: MenuContext` to the dep callback that now expects `ExtensionContext`.
|
|
37
|
+
The testing skill already warns: "when a TDD plan lists separate steps that share a type definition, changing that type in step N breaks steps N+1…N+k."
|
|
38
|
+
The planner failed to recognize that `MenuContext` and `AgentMenuManager.spawnAndWait` share a type dependency through `ctx`.
|
|
39
|
+
Impact: no rework — the steps were folded successfully — but the plan's step count was inaccurate.
|
|
40
|
+
|
|
41
|
+
#### What caused friction (user side)
|
|
42
|
+
|
|
43
|
+
- No user-side friction observed.
|
|
44
|
+
The user ran three sequential prompts (`/plan-issue 66`, `/tdd-plan`, `/ship-issue`) with no corrections or redirects needed.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 70
|
|
3
|
+
issue_title: "refactor: extract event handlers from pi-subagents index.ts into src/handlers/"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #70 — extract event handlers from index.ts
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-20T19:30:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned and implemented #70 — extracting four inline event handler lambdas from `src/index.ts` into `src/handlers/lifecycle.ts` (`SessionLifecycleHandler` class) and `src/handlers/tool-start.ts` (`ToolStartHandler` class).
|
|
13
|
+
During planning, the user identified that the initial plain-function design missed shared collaborators and structural smells, prompting a redesign to class-based handlers and a predecessor issue (#87) to evolve `SubagentRuntime` from a data bag to an object with methods.
|
|
14
|
+
Released as `pi-subagents-v5.8.0` with 8 new handler tests (520 total).
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
#### What went well
|
|
19
|
+
|
|
20
|
+
- The user's progressive questioning during planning ("Are you confident?", "Keep digging", Kent Beck quote) surfaced two concrete structural smells — output arguments on `runtime.currentCtx` writes and 8 LoD violations via `runtime.widget!` reach-throughs — that the initial design would have just relocated rather than fixed.
|
|
21
|
+
- Filing predecessor #87 was the right sequencing call.
|
|
22
|
+
By the time #70 executed, the runtime had proper methods and the handler extraction was purely mechanical.
|
|
23
|
+
- The 3-step TDD cycle executed cleanly with zero rework or deviations from the plan.
|
|
24
|
+
The only issue was a `tsc` type error (`vi.fn()` return type not assignable to `() => void`) caught during the post-TDD type check and fixed in-place before the final commit.
|
|
25
|
+
|
|
26
|
+
#### What caused friction (agent side)
|
|
27
|
+
|
|
28
|
+
- `premature-convergence` — The initial plan used plain functions with per-call `LifecycleDeps`/`ToolStartDeps` interfaces, the first viable approach, without analyzing whether handlers shared collaborators.
|
|
29
|
+
The user had to prompt three times before I switched to class-based handlers with constructor-injected shared deps.
|
|
30
|
+
Impact: three rounds of plan revision before the design was correct; no rework commits, but significant planning churn.
|
|
31
|
+
|
|
32
|
+
- `instruction-violation` (user-caught) — The `/plan-issue` prompt says "load the `design-review` skill and run its checklist on the affected modules."
|
|
33
|
+
I loaded the skill but did not actually run the checklist (grep for access patterns, check LoD, check output arguments) until the user explicitly told me to "keep digging."
|
|
34
|
+
Had I run the checklist proactively, the output-argument and LoD findings would have surfaced on the first pass, and the predecessor issue (#87) would have been identified without user escalation.
|
|
35
|
+
Impact: user had to escalate three times; planning took ~3× longer than necessary.
|
|
36
|
+
|
|
37
|
+
- `wrong-abstraction` — I initially reasoned at the mechanical level ("handlers are small, 1–5 lines, so plain functions are fine") instead of the structural level ("do these handlers share collaborators that a class captures naturally?").
|
|
38
|
+
The code-style skill explicitly says "Do not pass a shared dependency bag to functions that only use a subset" — but I applied it backwards (splitting into per-function deps) rather than recognizing the shared deps as a class cohesion signal.
|
|
39
|
+
Impact: same as premature-convergence above; folded into the same rework cycle.
|
|
40
|
+
|
|
41
|
+
#### What caused friction (user side)
|
|
42
|
+
|
|
43
|
+
- The progressive escalation approach (question → directive → quote) was effective pedagogically but required three turns of user attention on what a proactive design-review checklist run would have caught automatically.
|
|
44
|
+
Earlier intervention with a specific redirect (e.g., "Run the design-review checklist before writing the plan") could have resolved it in one turn.
|
|
45
|
+
|
|
46
|
+
### Changes made
|
|
47
|
+
|
|
48
|
+
1. Retro file created at `packages/pi-subagents/docs/retro/0070-extract-event-handlers.md`.
|
|
49
|
+
2. Updated `packages/pi-subagents/docs/architecture/architecture.md` — marked #87 and #70 as done (✓), updated Phase 2 status to complete, updated next-issue pointer to #66.
|
package/package.json
CHANGED
package/src/agent-manager.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -56,7 +56,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
56
56
|
// runtime.widget is assigned after AgentManager construction; arrow closures
|
|
57
57
|
// capture `runtime` by reference so they always read the current value.
|
|
58
58
|
const notifications = createNotificationSystem({
|
|
59
|
-
sendMessage: (msg, opts) => pi.sendMessage(msg
|
|
59
|
+
sendMessage: (msg, opts) => pi.sendMessage(msg, opts),
|
|
60
60
|
agentActivity: runtime.agentActivity,
|
|
61
61
|
markFinished: (id) => runtime.markFinished(id),
|
|
62
62
|
updateWidget: () => runtime.updateWidget(),
|
|
@@ -185,8 +185,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
185
185
|
|
|
186
186
|
pi.registerTool(defineTool(createAgentTool({
|
|
187
187
|
manager: {
|
|
188
|
-
spawn: (ctx, type, prompt, opts) => manager.spawn(pi, ctx
|
|
189
|
-
spawnAndWait: (ctx, type, prompt, opts) => manager.spawnAndWait(pi, ctx
|
|
188
|
+
spawn: (ctx, type, prompt, opts) => manager.spawn(pi, ctx, type, prompt, opts),
|
|
189
|
+
spawnAndWait: (ctx, type, prompt, opts) => manager.spawnAndWait(pi, ctx, type, prompt, opts),
|
|
190
190
|
resume: (id, prompt, signal) => manager.resume(id, prompt, signal),
|
|
191
191
|
getRecord: (id) => manager.getRecord(id),
|
|
192
192
|
getMaxConcurrent: () => manager.getMaxConcurrent(),
|
|
@@ -205,14 +205,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
205
205
|
availableTypesText: getAvailableTypes().join(", "),
|
|
206
206
|
agentDir: getAgentDir(),
|
|
207
207
|
getDefaultMaxTurns: () => runtime.defaultMaxTurns,
|
|
208
|
-
})
|
|
208
|
+
})));
|
|
209
209
|
|
|
210
210
|
// ---- get_subagent_result tool ----
|
|
211
211
|
|
|
212
212
|
pi.registerTool(defineTool(createGetResultTool({
|
|
213
213
|
getRecord: (id) => manager.getRecord(id),
|
|
214
214
|
cancelNudge: (key) => notifications.cancelNudge(key),
|
|
215
|
-
getConversation: (session) => getAgentConversation(session
|
|
215
|
+
getConversation: (session) => getAgentConversation(session),
|
|
216
216
|
})));
|
|
217
217
|
|
|
218
218
|
// ---- steer_subagent tool ----
|
|
@@ -220,7 +220,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
220
220
|
pi.registerTool(defineTool(createSteerTool({
|
|
221
221
|
getRecord: (id) => manager.getRecord(id),
|
|
222
222
|
emitEvent: (name, data) => pi.events.emit(name, data),
|
|
223
|
-
steerAgent: (session, message) => steerAgent(session
|
|
223
|
+
steerAgent: (session, message) => steerAgent(session, message),
|
|
224
224
|
})));
|
|
225
225
|
|
|
226
226
|
// ---- /agents interactive menu ----
|
|
@@ -229,7 +229,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
229
229
|
manager: {
|
|
230
230
|
listAgents: () => manager.listAgents(),
|
|
231
231
|
getRecord: (id) => manager.getRecord(id),
|
|
232
|
-
spawnAndWait: (piArg, ctx, type, prompt, opts) => manager.spawnAndWait(
|
|
232
|
+
spawnAndWait: (piArg, ctx, type, prompt, opts) => manager.spawnAndWait(piArg ?? pi, ctx, type, prompt, opts),
|
|
233
233
|
getMaxConcurrent: () => manager.getMaxConcurrent(),
|
|
234
234
|
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
235
235
|
},
|
|
@@ -239,7 +239,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
239
239
|
const cfg = resolveAgentConfig(type);
|
|
240
240
|
if (!cfg.model) return 'inherit';
|
|
241
241
|
if (registry) {
|
|
242
|
-
const resolved = resolveModel(cfg.model, registry
|
|
242
|
+
const resolved = resolveModel(cfg.model, registry);
|
|
243
243
|
if (typeof resolved === 'string') return 'inherit';
|
|
244
244
|
}
|
|
245
245
|
return getModelLabelFromConfig(cfg.model);
|
|
@@ -264,10 +264,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
264
264
|
),
|
|
265
265
|
emitEvent: (name, data) => pi.events.emit(name, data),
|
|
266
266
|
personalAgentsDir: join(getAgentDir(), 'agents'),
|
|
267
|
+
projectAgentsDir: join(process.cwd(), '.pi', 'agents'),
|
|
267
268
|
});
|
|
268
269
|
|
|
269
270
|
pi.registerCommand('agents', {
|
|
270
271
|
description: 'Manage agents',
|
|
271
|
-
handler: async (_args, ctx) => { await agentsMenuHandler(ctx
|
|
272
|
+
handler: async (_args, ctx) => { await agentsMenuHandler(ctx); },
|
|
272
273
|
});
|
|
273
274
|
}
|
package/src/notification.ts
CHANGED
|
@@ -109,7 +109,10 @@ export function buildEventData(record: AgentRecord) {
|
|
|
109
109
|
|
|
110
110
|
/** Narrow deps for the notification system — only the methods it actually calls. */
|
|
111
111
|
export interface NotificationDeps {
|
|
112
|
-
sendMessage: (
|
|
112
|
+
sendMessage: (
|
|
113
|
+
msg: { customType: string; content: string; display: boolean; details?: unknown },
|
|
114
|
+
opts?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
115
|
+
) => void;
|
|
113
116
|
agentActivity: Map<string, AgentActivity>;
|
|
114
117
|
markFinished: (id: string) => void;
|
|
115
118
|
updateWidget: () => void;
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
2
3
|
import { Type } from "@sinclair/typebox";
|
|
4
|
+
import type { SpawnOptions } from "../agent-manager.js";
|
|
3
5
|
import { normalizeMaxTurns } from "../agent-runner.js";
|
|
4
6
|
import { resolveAgentConfig, resolveType } from "../agent-types.js";
|
|
5
7
|
import { resolveAgentInvocationConfig } from "../invocation-config.js";
|
|
@@ -121,8 +123,8 @@ export function buildDetails(
|
|
|
121
123
|
|
|
122
124
|
/** Narrow manager interface — only the methods the Agent tool calls. */
|
|
123
125
|
export interface AgentToolManager {
|
|
124
|
-
spawn: (ctx:
|
|
125
|
-
spawnAndWait: (ctx:
|
|
126
|
+
spawn: (ctx: ExtensionContext, type: string, prompt: string, opts: SpawnOptions) => string;
|
|
127
|
+
spawnAndWait: (ctx: ExtensionContext, type: string, prompt: string, opts: Omit<SpawnOptions, "isBackground">) => Promise<AgentRecord>;
|
|
126
128
|
resume: (id: string, prompt: string, signal: AbortSignal) => Promise<AgentRecord | undefined>;
|
|
127
129
|
getRecord: (id: string) => AgentRecord | undefined;
|
|
128
130
|
getMaxConcurrent: () => number;
|
|
@@ -353,8 +355,8 @@ Guidelines:
|
|
|
353
355
|
execute: async (
|
|
354
356
|
toolCallId: string,
|
|
355
357
|
params: Record<string, unknown>,
|
|
356
|
-
signal: AbortSignal,
|
|
357
|
-
onUpdate: ((update:
|
|
358
|
+
signal: AbortSignal | undefined,
|
|
359
|
+
onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
|
|
358
360
|
ctx: any,
|
|
359
361
|
) => {
|
|
360
362
|
// Ensure we have UI context for widget rendering
|
|
@@ -436,7 +438,7 @@ Guidelines:
|
|
|
436
438
|
const record = await deps.manager.resume(
|
|
437
439
|
params.resume as string,
|
|
438
440
|
params.prompt as string,
|
|
439
|
-
signal,
|
|
441
|
+
signal ?? new AbortController().signal,
|
|
440
442
|
);
|
|
441
443
|
if (!record) {
|
|
442
444
|
return textResult(`Failed to resume agent "${params.resume}".`);
|
|
@@ -465,7 +467,7 @@ Guidelines:
|
|
|
465
467
|
|
|
466
468
|
try {
|
|
467
469
|
id = deps.manager.spawn(ctx, subagentType, params.prompt as string, {
|
|
468
|
-
description: params.description,
|
|
470
|
+
description: params.description as string,
|
|
469
471
|
model,
|
|
470
472
|
maxTurns: effectiveMaxTurns,
|
|
471
473
|
isolated,
|
|
@@ -585,7 +587,7 @@ Guidelines:
|
|
|
585
587
|
subagentType,
|
|
586
588
|
params.prompt as string,
|
|
587
589
|
{
|
|
588
|
-
description: params.description,
|
|
590
|
+
description: params.description as string,
|
|
589
591
|
model,
|
|
590
592
|
maxTurns: effectiveMaxTurns,
|
|
591
593
|
isolated,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import { Type } from "@sinclair/typebox";
|
|
2
3
|
import type { AgentRecord } from "../types.js";
|
|
3
4
|
import { formatDuration, getDisplayName } from "../ui/agent-widget.js";
|
|
@@ -8,7 +9,7 @@ import { formatLifetimeTokens, textResult } from "./helpers.js";
|
|
|
8
9
|
export interface GetResultDeps {
|
|
9
10
|
getRecord: (id: string) => AgentRecord | undefined;
|
|
10
11
|
cancelNudge: (key: string) => void;
|
|
11
|
-
getConversation: (session:
|
|
12
|
+
getConversation: (session: AgentSession) => string | undefined;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
/** Create the get_subagent_result tool definition (without Pi SDK wrapper). */
|
package/src/tools/steer-tool.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import { Type } from "@sinclair/typebox";
|
|
2
3
|
import type { AgentRecord } from "../types.js";
|
|
3
4
|
import { getSessionContextPercent } from "../usage.js";
|
|
@@ -7,7 +8,7 @@ import { formatLifetimeTokens, textResult } from "./helpers.js";
|
|
|
7
8
|
export interface SteerToolDeps {
|
|
8
9
|
getRecord: (id: string) => AgentRecord | undefined;
|
|
9
10
|
emitEvent: (name: string, data: unknown) => void;
|
|
10
|
-
steerAgent: (session:
|
|
11
|
+
steerAgent: (session: AgentSession, message: string) => Promise<void>;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/** Create the steer_subagent tool definition (without Pi SDK wrapper). */
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { SpawnOptions } from "../agent-manager.js";
|
|
4
6
|
import {
|
|
5
7
|
BUILTIN_TOOL_NAMES,
|
|
6
8
|
getAllTypes,
|
|
7
9
|
resolveAgentConfig,
|
|
8
10
|
resolveType,
|
|
9
11
|
} from "../agent-types.js";
|
|
12
|
+
import type { ModelRegistry } from "../model-resolver.js";
|
|
10
13
|
import type { AgentConfig, AgentRecord } from "../types.js";
|
|
11
14
|
import type { AgentActivity } from "./agent-widget.js";
|
|
12
15
|
import { formatDuration, getDisplayName } from "./agent-widget.js";
|
|
@@ -18,7 +21,7 @@ export interface AgentMenuManager {
|
|
|
18
21
|
listAgents: () => AgentRecord[];
|
|
19
22
|
getRecord: (id: string) => AgentRecord | undefined;
|
|
20
23
|
/** Used by generate wizard to spawn an agent that writes the .md file. */
|
|
21
|
-
spawnAndWait: (pi:
|
|
24
|
+
spawnAndWait: (pi: ExtensionAPI | null, ctx: ExtensionContext, type: string, prompt: string, opts: Omit<SpawnOptions, "isBackground">) => Promise<AgentRecord>;
|
|
22
25
|
getMaxConcurrent: () => number;
|
|
23
26
|
setMaxConcurrent: (n: number) => void;
|
|
24
27
|
}
|
|
@@ -28,7 +31,7 @@ export interface AgentMenuDeps {
|
|
|
28
31
|
reloadCustomAgents: () => void;
|
|
29
32
|
agentActivity: Map<string, AgentActivity>;
|
|
30
33
|
/** Resolve model label for a given agent type + registry. */
|
|
31
|
-
getModelLabel: (type: string, registry?:
|
|
34
|
+
getModelLabel: (type: string, registry?: ModelRegistry) => string;
|
|
32
35
|
/** Snapshot current settings for persistence. */
|
|
33
36
|
snapshotSettings: () => { maxConcurrent: number; defaultMaxTurns: number; graceTurns: number };
|
|
34
37
|
/** Save settings and return a notification result. */
|
|
@@ -38,6 +41,7 @@ export interface AgentMenuDeps {
|
|
|
38
41
|
) => { message: string; level: string };
|
|
39
42
|
emitEvent: (name: string, data: unknown) => void;
|
|
40
43
|
personalAgentsDir: string;
|
|
44
|
+
projectAgentsDir: string;
|
|
41
45
|
/** Returns the runtime default max turns (undefined = unlimited). */
|
|
42
46
|
getDefaultMaxTurns: () => number | undefined;
|
|
43
47
|
/** Returns the runtime grace turns value. */
|
|
@@ -50,20 +54,6 @@ export interface AgentMenuDeps {
|
|
|
50
54
|
|
|
51
55
|
// ---- Narrow UI context types ----
|
|
52
56
|
|
|
53
|
-
interface MenuUI {
|
|
54
|
-
select: (title: string, options: string[]) => Promise<string | undefined>;
|
|
55
|
-
input: (prompt: string, defaultValue?: string) => Promise<string | undefined>;
|
|
56
|
-
confirm: (title: string, message: string) => Promise<boolean>;
|
|
57
|
-
editor: (title: string, content: string) => Promise<string | undefined>;
|
|
58
|
-
notify: (message: string, level: string) => void;
|
|
59
|
-
custom: <T>(factory: any, options: any) => Promise<T>;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface MenuContext {
|
|
63
|
-
ui: MenuUI;
|
|
64
|
-
modelRegistry?: unknown;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
57
|
// ---- Factory ----
|
|
68
58
|
|
|
69
59
|
/**
|
|
@@ -71,19 +61,17 @@ interface MenuContext {
|
|
|
71
61
|
* Returns a function suitable for `pi.registerCommand("agents", { handler })`.
|
|
72
62
|
*/
|
|
73
63
|
export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
74
|
-
const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");
|
|
75
|
-
|
|
76
64
|
function findAgentFile(
|
|
77
65
|
name: string,
|
|
78
66
|
): { path: string; location: "project" | "personal" } | undefined {
|
|
79
|
-
const projectPath = join(projectAgentsDir
|
|
67
|
+
const projectPath = join(deps.projectAgentsDir, `${name}.md`);
|
|
80
68
|
if (existsSync(projectPath)) return { path: projectPath, location: "project" };
|
|
81
69
|
const personalPath = join(deps.personalAgentsDir, `${name}.md`);
|
|
82
70
|
if (existsSync(personalPath)) return { path: personalPath, location: "personal" };
|
|
83
71
|
return undefined;
|
|
84
72
|
}
|
|
85
73
|
|
|
86
|
-
async function showAgentsMenu(ctx:
|
|
74
|
+
async function showAgentsMenu(ctx: ExtensionContext) {
|
|
87
75
|
deps.reloadCustomAgents();
|
|
88
76
|
const allNames = getAllTypes();
|
|
89
77
|
|
|
@@ -137,7 +125,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
137
125
|
}
|
|
138
126
|
}
|
|
139
127
|
|
|
140
|
-
async function showAllAgentsList(ctx:
|
|
128
|
+
async function showAllAgentsList(ctx: ExtensionContext) {
|
|
141
129
|
const allNames = getAllTypes();
|
|
142
130
|
if (allNames.length === 0) {
|
|
143
131
|
ctx.ui.notify("No agents.", "info");
|
|
@@ -191,7 +179,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
191
179
|
}
|
|
192
180
|
}
|
|
193
181
|
|
|
194
|
-
async function showRunningAgents(ctx:
|
|
182
|
+
async function showRunningAgents(ctx: ExtensionContext) {
|
|
195
183
|
const agents = deps.manager.listAgents();
|
|
196
184
|
if (agents.length === 0) {
|
|
197
185
|
ctx.ui.notify("No agents.", "info");
|
|
@@ -215,7 +203,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
215
203
|
await showRunningAgents(ctx);
|
|
216
204
|
}
|
|
217
205
|
|
|
218
|
-
async function viewAgentConversation(ctx:
|
|
206
|
+
async function viewAgentConversation(ctx: ExtensionContext, record: AgentRecord) {
|
|
219
207
|
if (!record.session) {
|
|
220
208
|
ctx.ui.notify(
|
|
221
209
|
`Agent is ${record.status === "queued" ? "queued" : "expired"} — no session available.`,
|
|
@@ -245,7 +233,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
245
233
|
);
|
|
246
234
|
}
|
|
247
235
|
|
|
248
|
-
async function showAgentDetail(ctx:
|
|
236
|
+
async function showAgentDetail(ctx: ExtensionContext, name: string) {
|
|
249
237
|
if (resolveType(name) == null) {
|
|
250
238
|
ctx.ui.notify(`Agent config not found for "${name}".`, "warning");
|
|
251
239
|
return;
|
|
@@ -312,7 +300,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
312
300
|
}
|
|
313
301
|
}
|
|
314
302
|
|
|
315
|
-
async function ejectAgent(ctx:
|
|
303
|
+
async function ejectAgent(ctx: ExtensionContext, name: string, cfg: AgentConfig) {
|
|
316
304
|
const location = await ctx.ui.select("Choose location", [
|
|
317
305
|
"Project (.pi/agents/)",
|
|
318
306
|
`Personal (${deps.personalAgentsDir})`,
|
|
@@ -320,7 +308,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
320
308
|
if (!location) return;
|
|
321
309
|
|
|
322
310
|
const targetDir = location.startsWith("Project")
|
|
323
|
-
? projectAgentsDir
|
|
311
|
+
? deps.projectAgentsDir
|
|
324
312
|
: deps.personalAgentsDir;
|
|
325
313
|
mkdirSync(targetDir, { recursive: true });
|
|
326
314
|
|
|
@@ -363,7 +351,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
363
351
|
ctx.ui.notify(`Ejected ${name} to ${targetPath}`, "info");
|
|
364
352
|
}
|
|
365
353
|
|
|
366
|
-
async function disableAgent(ctx:
|
|
354
|
+
async function disableAgent(ctx: ExtensionContext, name: string) {
|
|
367
355
|
const file = findAgentFile(name);
|
|
368
356
|
if (file) {
|
|
369
357
|
const content = readFileSync(file.path, "utf-8");
|
|
@@ -386,7 +374,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
386
374
|
if (!location) return;
|
|
387
375
|
|
|
388
376
|
const targetDir = location.startsWith("Project")
|
|
389
|
-
? projectAgentsDir
|
|
377
|
+
? deps.projectAgentsDir
|
|
390
378
|
: deps.personalAgentsDir;
|
|
391
379
|
mkdirSync(targetDir, { recursive: true });
|
|
392
380
|
|
|
@@ -397,7 +385,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
397
385
|
ctx.ui.notify(`Disabled ${name} (${targetPath})`, "info");
|
|
398
386
|
}
|
|
399
387
|
|
|
400
|
-
async function enableAgent(ctx:
|
|
388
|
+
async function enableAgent(ctx: ExtensionContext, name: string) {
|
|
401
389
|
const file = findAgentFile(name);
|
|
402
390
|
if (!file) return;
|
|
403
391
|
|
|
@@ -416,7 +404,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
416
404
|
}
|
|
417
405
|
}
|
|
418
406
|
|
|
419
|
-
async function showCreateWizard(ctx:
|
|
407
|
+
async function showCreateWizard(ctx: ExtensionContext) {
|
|
420
408
|
const location = await ctx.ui.select("Choose location", [
|
|
421
409
|
"Project (.pi/agents/)",
|
|
422
410
|
`Personal (${deps.personalAgentsDir})`,
|
|
@@ -424,7 +412,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
424
412
|
if (!location) return;
|
|
425
413
|
|
|
426
414
|
const targetDir = location.startsWith("Project")
|
|
427
|
-
? projectAgentsDir
|
|
415
|
+
? deps.projectAgentsDir
|
|
428
416
|
: deps.personalAgentsDir;
|
|
429
417
|
|
|
430
418
|
const method = await ctx.ui.select("Creation method", [
|
|
@@ -440,7 +428,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
440
428
|
}
|
|
441
429
|
}
|
|
442
430
|
|
|
443
|
-
async function showGenerateWizard(ctx:
|
|
431
|
+
async function showGenerateWizard(ctx: ExtensionContext, targetDir: string) {
|
|
444
432
|
const description = await ctx.ui.input("Describe what this agent should do");
|
|
445
433
|
if (!description) return;
|
|
446
434
|
|
|
@@ -526,7 +514,7 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
526
514
|
}
|
|
527
515
|
}
|
|
528
516
|
|
|
529
|
-
async function showManualWizard(ctx:
|
|
517
|
+
async function showManualWizard(ctx: ExtensionContext, targetDir: string) {
|
|
530
518
|
const name = await ctx.ui.input("Agent name (filename, no spaces)");
|
|
531
519
|
if (!name) return;
|
|
532
520
|
|
|
@@ -621,7 +609,7 @@ ${systemPrompt}
|
|
|
621
609
|
ctx.ui.notify(`Created ${targetPath}`, "info");
|
|
622
610
|
}
|
|
623
611
|
|
|
624
|
-
async function showSettings(ctx:
|
|
612
|
+
async function showSettings(ctx: ExtensionContext) {
|
|
625
613
|
const choice = await ctx.ui.select("Settings", [
|
|
626
614
|
`Max concurrency (current: ${deps.manager.getMaxConcurrent()})`,
|
|
627
615
|
`Default max turns (current: ${deps.getDefaultMaxTurns() ?? "unlimited"})`,
|
|
@@ -677,13 +665,13 @@ ${systemPrompt}
|
|
|
677
665
|
}
|
|
678
666
|
}
|
|
679
667
|
|
|
680
|
-
function notifyApplied(ctx:
|
|
668
|
+
function notifyApplied(ctx: ExtensionContext, successMsg: string) {
|
|
681
669
|
const { message, level } = deps.saveSettings(deps.snapshotSettings(), successMsg);
|
|
682
|
-
ctx.ui.notify(message, level);
|
|
670
|
+
ctx.ui.notify(message, level as "info" | "warning" | "error");
|
|
683
671
|
}
|
|
684
672
|
|
|
685
673
|
// Return the handler function
|
|
686
|
-
return async (ctx:
|
|
674
|
+
return async (ctx: ExtensionContext) => {
|
|
687
675
|
await showAgentsMenu(ctx);
|
|
688
676
|
};
|
|
689
677
|
}
|