@gotgenes/pi-subagents 5.7.0 → 5.8.1
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 +23 -0
- package/docs/architecture/architecture.md +9 -10
- package/docs/plans/0066-replace-as-any-with-sdk-types.md +215 -0
- package/docs/retro/0070-extract-event-handlers.md +49 -0
- package/docs/retro/0087-evolve-subagent-runtime-methods.md +42 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +1 -1
- package/src/handlers/index.ts +6 -0
- package/src/handlers/lifecycle.ts +62 -0
- package/src/handlers/tool-start.ts +32 -0
- package/src/index.ts +22 -30
- 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 +20 -31
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ 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.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.8.0...pi-subagents-v5.8.1) (2026-05-20)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* 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))
|
|
14
|
+
* **retro:** add retro notes for issue [#70](https://github.com/gotgenes/pi-packages/issues/70) ([0668956](https://github.com/gotgenes/pi-packages/commit/0668956e99383df7a62fddde57dd4182bf491a5e))
|
|
15
|
+
* **retro:** record architecture update for [#70](https://github.com/gotgenes/pi-packages/issues/70) ([49d4fae](https://github.com/gotgenes/pi-packages/commit/49d4faee4ee711e385dbf59334b0b737897d26a6))
|
|
16
|
+
* 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))
|
|
17
|
+
|
|
18
|
+
## [5.8.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.7.0...pi-subagents-v5.8.0) (2026-05-20)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* add SessionLifecycleHandler ([d8d95fa](https://github.com/gotgenes/pi-packages/commit/d8d95fa92561e0068444c3c925f21b50825a741c))
|
|
24
|
+
* add ToolStartHandler ([c293e41](https://github.com/gotgenes/pi-packages/commit/c293e41e25d9742f807f85d0efe0a0e5605b29a6))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Documentation
|
|
28
|
+
|
|
29
|
+
* **retro:** add retro notes for issue [#87](https://github.com/gotgenes/pi-packages/issues/87) ([1701fe4](https://github.com/gotgenes/pi-packages/commit/1701fe41d582d7180b2622c2e03db954e8d2c2af))
|
|
30
|
+
|
|
8
31
|
## [5.7.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.6.0...pi-subagents-v5.7.0) (2026-05-20)
|
|
9
32
|
|
|
10
33
|
|
|
@@ -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,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.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 87
|
|
3
|
+
issue_title: "refactor: evolve SubagentRuntime from data bag to object with methods"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #87 — evolve SubagentRuntime from data bag to object with methods
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-20T18:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned, implemented, and shipped #87 — converting `SubagentRuntime` from a plain interface + factory into a class with session-context methods (`setSessionContext`, `clearSessionContext`) and widget delegation methods (`setUICtx`, `onTurnStart`, `markFinished`, `updateWidget`, `ensureTimer`).
|
|
13
|
+
All 10 call sites in `index.ts` were migrated, eliminating raw `currentCtx` field writes and `runtime.widget!` reach-throughs.
|
|
14
|
+
Released as `pi-subagents-v5.7.0`.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
#### What went well
|
|
19
|
+
|
|
20
|
+
- The 3-step TDD cycle executed cleanly with zero rework or deviations from the plan.
|
|
21
|
+
Test count went from 5 → 16 in `runtime.test.ts`; all 512 package tests stayed green throughout.
|
|
22
|
+
- The user identified a missing architecture update and a pre-existing hallucination (`@earendil-works/pi-subagents` → `@gotgenes/pi-subagents`) after the plan commit.
|
|
23
|
+
The fix was a single well-scoped commit (`ddee1a0`) that corrected 10 scope references, updated the dependency graph to include #87 as a precursor to #70, and fixed list numbering per phase heading.
|
|
24
|
+
- The plan's "Call sites to migrate" tables with exact line numbers and before/after code made step 3 (the refactoring commit) purely mechanical — no design decisions at implementation time.
|
|
25
|
+
|
|
26
|
+
#### What caused friction (agent side)
|
|
27
|
+
|
|
28
|
+
- `missing-context` — In TDD step 2, the new `describe` block used `vi.fn()` but the test file's import was `{ describe, expect, it }` without `vi`.
|
|
29
|
+
The first red run surfaced this immediately (`ReferenceError: vi is not defined`) and it was fixed before the green step.
|
|
30
|
+
Impact: one extra test run, no rework commits.
|
|
31
|
+
- `scope-drift` — The `/plan-issue` prompt does not instruct updating `architecture.md`, but the user noticed #87 was missing from the roadmap's dependency graph.
|
|
32
|
+
The agent hadn't checked `architecture.md` during planning even though #87 is explicitly listed as a precursor to #70 in the #70 plan.
|
|
33
|
+
Impact: user-caught; required a follow-up edit pass after the plan commit.
|
|
34
|
+
|
|
35
|
+
#### What caused friction (user side)
|
|
36
|
+
|
|
37
|
+
No friction observed.
|
|
38
|
+
The user's correction about `architecture.md` was well-targeted and caught a real gap.
|
|
39
|
+
|
|
40
|
+
### Changes made
|
|
41
|
+
|
|
42
|
+
1. Retro file created at `packages/pi-subagents/docs/retro/0087-evolve-subagent-runtime-methods.md`.
|
package/package.json
CHANGED
package/src/agent-manager.ts
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session lifecycle event handlers: session_start, session_before_switch, session_shutdown.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from index.ts so each handler can be tested in isolation
|
|
5
|
+
* with mocked narrow interfaces.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Narrow manager interface — only the methods lifecycle handlers call. */
|
|
9
|
+
export interface LifecycleManager {
|
|
10
|
+
clearCompleted(): void;
|
|
11
|
+
abortAll(): void;
|
|
12
|
+
dispose(): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Narrow runtime interface — only the methods lifecycle handlers call. */
|
|
16
|
+
export interface LifecycleRuntime {
|
|
17
|
+
setSessionContext(pi: unknown, ctx: unknown): void;
|
|
18
|
+
clearSessionContext(): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Handles session lifecycle events.
|
|
23
|
+
*
|
|
24
|
+
* Constructor deps:
|
|
25
|
+
* - `pi` — the ExtensionAPI instance, stored in runtime on session_start
|
|
26
|
+
* - `runtime` — owns session context state
|
|
27
|
+
* - `manager` — manages agent lifecycle (clear, abort, dispose)
|
|
28
|
+
* - `disposeNotifications` — tears down the notification system on shutdown
|
|
29
|
+
* - `unpublishService` — unpublishes the SubagentsService symbol on shutdown
|
|
30
|
+
*/
|
|
31
|
+
export class SessionLifecycleHandler {
|
|
32
|
+
constructor(
|
|
33
|
+
private readonly pi: unknown,
|
|
34
|
+
private readonly runtime: LifecycleRuntime,
|
|
35
|
+
private readonly manager: LifecycleManager,
|
|
36
|
+
private readonly disposeNotifications: () => void,
|
|
37
|
+
private readonly unpublishService: () => void,
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
handleSessionStart(_event: unknown, ctx: unknown): void {
|
|
41
|
+
this.runtime.setSessionContext(this.pi, ctx);
|
|
42
|
+
this.manager.clearCompleted();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
handleSessionBeforeSwitch(): void {
|
|
46
|
+
this.manager.clearCompleted();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Cleanup order matters:
|
|
50
|
+
// 1. Unpublish service — prevent new cross-extension calls
|
|
51
|
+
// 2. Clear session context — no more session state
|
|
52
|
+
// 3. Abort all agents — stop running work
|
|
53
|
+
// 4. Dispose notifications — cancel pending nudges/timers
|
|
54
|
+
// 5. Dispose manager — final cleanup
|
|
55
|
+
async handleSessionShutdown(): Promise<void> {
|
|
56
|
+
this.unpublishService();
|
|
57
|
+
this.runtime.clearSessionContext();
|
|
58
|
+
this.manager.abortAll();
|
|
59
|
+
this.disposeNotifications();
|
|
60
|
+
this.manager.dispose();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool_execution_start event handler.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from index.ts so the handler can be tested in isolation
|
|
5
|
+
* with a mocked narrow runtime interface.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Narrow runtime interface — only the widget-delegation methods the handler calls. */
|
|
9
|
+
export interface ToolStartRuntime {
|
|
10
|
+
setUICtx(ctx: unknown): void;
|
|
11
|
+
onTurnStart(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Minimal context shape for tool_execution_start — only the field the handler reads. */
|
|
15
|
+
interface ToolStartCtx {
|
|
16
|
+
ui: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handles tool_execution_start events.
|
|
21
|
+
*
|
|
22
|
+
* Grabs UI context from the first tool execution of each turn
|
|
23
|
+
* and signals the widget to clear lingering state.
|
|
24
|
+
*/
|
|
25
|
+
export class ToolStartHandler {
|
|
26
|
+
constructor(private readonly runtime: ToolStartRuntime) {}
|
|
27
|
+
|
|
28
|
+
handleToolExecutionStart(_event: unknown, ctx: ToolStartCtx): void {
|
|
29
|
+
this.runtime.setUICtx(ctx.ui);
|
|
30
|
+
this.runtime.onTurnStart();
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { AgentManager } from "./agent-manager.js";
|
|
|
16
16
|
import { getAgentConversation, normalizeMaxTurns, resumeAgent, runAgent, steerAgent } from "./agent-runner.js";
|
|
17
17
|
import { getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, resolveAgentConfig, } from "./agent-types.js";
|
|
18
18
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
19
|
+
import { SessionLifecycleHandler, ToolStartHandler } from "./handlers/index.js";
|
|
19
20
|
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
20
21
|
import { buildEventData, createNotificationSystem } from "./notification.js";
|
|
21
22
|
import { createNotificationRenderer } from "./renderer.js";
|
|
@@ -55,7 +56,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
55
56
|
// runtime.widget is assigned after AgentManager construction; arrow closures
|
|
56
57
|
// capture `runtime` by reference so they always read the current value.
|
|
57
58
|
const notifications = createNotificationSystem({
|
|
58
|
-
sendMessage: (msg, opts) => pi.sendMessage(msg
|
|
59
|
+
sendMessage: (msg, opts) => pi.sendMessage(msg, opts),
|
|
59
60
|
agentActivity: runtime.agentActivity,
|
|
60
61
|
markFinished: (id) => runtime.markFinished(id),
|
|
61
62
|
updateWidget: () => runtime.updateWidget(),
|
|
@@ -122,33 +123,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
122
123
|
});
|
|
123
124
|
publishSubagentsService(service);
|
|
124
125
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
126
|
+
const lifecycle = new SessionLifecycleHandler(
|
|
127
|
+
pi,
|
|
128
|
+
runtime,
|
|
129
|
+
manager,
|
|
130
|
+
() => notifications.dispose(),
|
|
131
|
+
unpublishSubagentsService,
|
|
132
|
+
);
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
pi.on("session_shutdown",
|
|
137
|
-
unpublishSubagentsService();
|
|
138
|
-
runtime.clearSessionContext();
|
|
139
|
-
manager.abortAll();
|
|
140
|
-
notifications.dispose();
|
|
141
|
-
manager.dispose();
|
|
142
|
-
});
|
|
134
|
+
pi.on("session_start", (event, ctx) => lifecycle.handleSessionStart(event, ctx));
|
|
135
|
+
pi.on("session_before_switch", () => lifecycle.handleSessionBeforeSwitch());
|
|
136
|
+
pi.on("session_shutdown", () => lifecycle.handleSessionShutdown());
|
|
143
137
|
|
|
144
138
|
// Live widget: show running agents above editor
|
|
145
139
|
runtime.widget = new AgentWidget(manager, runtime.agentActivity);
|
|
146
140
|
|
|
147
141
|
// Grab UI context from first tool execution + clear lingering widget on new turn
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
runtime.onTurnStart();
|
|
151
|
-
});
|
|
142
|
+
const toolStart = new ToolStartHandler(runtime);
|
|
143
|
+
pi.on("tool_execution_start", (event, ctx) => toolStart.handleToolExecutionStart(event, ctx));
|
|
152
144
|
|
|
153
145
|
/** Build the full type list text dynamically from the unified registry. */
|
|
154
146
|
const buildTypeListText = () => {
|
|
@@ -193,8 +185,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
193
185
|
|
|
194
186
|
pi.registerTool(defineTool(createAgentTool({
|
|
195
187
|
manager: {
|
|
196
|
-
spawn: (ctx, type, prompt, opts) => manager.spawn(pi, ctx
|
|
197
|
-
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),
|
|
198
190
|
resume: (id, prompt, signal) => manager.resume(id, prompt, signal),
|
|
199
191
|
getRecord: (id) => manager.getRecord(id),
|
|
200
192
|
getMaxConcurrent: () => manager.getMaxConcurrent(),
|
|
@@ -213,14 +205,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
213
205
|
availableTypesText: getAvailableTypes().join(", "),
|
|
214
206
|
agentDir: getAgentDir(),
|
|
215
207
|
getDefaultMaxTurns: () => runtime.defaultMaxTurns,
|
|
216
|
-
})
|
|
208
|
+
})));
|
|
217
209
|
|
|
218
210
|
// ---- get_subagent_result tool ----
|
|
219
211
|
|
|
220
212
|
pi.registerTool(defineTool(createGetResultTool({
|
|
221
213
|
getRecord: (id) => manager.getRecord(id),
|
|
222
214
|
cancelNudge: (key) => notifications.cancelNudge(key),
|
|
223
|
-
getConversation: (session) => getAgentConversation(session
|
|
215
|
+
getConversation: (session) => getAgentConversation(session),
|
|
224
216
|
})));
|
|
225
217
|
|
|
226
218
|
// ---- steer_subagent tool ----
|
|
@@ -228,7 +220,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
228
220
|
pi.registerTool(defineTool(createSteerTool({
|
|
229
221
|
getRecord: (id) => manager.getRecord(id),
|
|
230
222
|
emitEvent: (name, data) => pi.events.emit(name, data),
|
|
231
|
-
steerAgent: (session, message) => steerAgent(session
|
|
223
|
+
steerAgent: (session, message) => steerAgent(session, message),
|
|
232
224
|
})));
|
|
233
225
|
|
|
234
226
|
// ---- /agents interactive menu ----
|
|
@@ -237,7 +229,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
237
229
|
manager: {
|
|
238
230
|
listAgents: () => manager.listAgents(),
|
|
239
231
|
getRecord: (id) => manager.getRecord(id),
|
|
240
|
-
spawnAndWait: (piArg, ctx, type, prompt, opts) => manager.spawnAndWait(
|
|
232
|
+
spawnAndWait: (piArg, ctx, type, prompt, opts) => manager.spawnAndWait(piArg ?? pi, ctx, type, prompt, opts),
|
|
241
233
|
getMaxConcurrent: () => manager.getMaxConcurrent(),
|
|
242
234
|
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
243
235
|
},
|
|
@@ -247,7 +239,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
247
239
|
const cfg = resolveAgentConfig(type);
|
|
248
240
|
if (!cfg.model) return 'inherit';
|
|
249
241
|
if (registry) {
|
|
250
|
-
const resolved = resolveModel(cfg.model, registry
|
|
242
|
+
const resolved = resolveModel(cfg.model, registry);
|
|
251
243
|
if (typeof resolved === 'string') return 'inherit';
|
|
252
244
|
}
|
|
253
245
|
return getModelLabelFromConfig(cfg.model);
|
|
@@ -276,6 +268,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
276
268
|
|
|
277
269
|
pi.registerCommand('agents', {
|
|
278
270
|
description: 'Manage agents',
|
|
279
|
-
handler: async (_args, ctx) => { await agentsMenuHandler(ctx
|
|
271
|
+
handler: async (_args, ctx) => { await agentsMenuHandler(ctx); },
|
|
280
272
|
});
|
|
281
273
|
}
|
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. */
|
|
@@ -50,20 +53,6 @@ export interface AgentMenuDeps {
|
|
|
50
53
|
|
|
51
54
|
// ---- Narrow UI context types ----
|
|
52
55
|
|
|
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
56
|
// ---- Factory ----
|
|
68
57
|
|
|
69
58
|
/**
|
|
@@ -83,7 +72,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
83
72
|
return undefined;
|
|
84
73
|
}
|
|
85
74
|
|
|
86
|
-
async function showAgentsMenu(ctx:
|
|
75
|
+
async function showAgentsMenu(ctx: ExtensionContext) {
|
|
87
76
|
deps.reloadCustomAgents();
|
|
88
77
|
const allNames = getAllTypes();
|
|
89
78
|
|
|
@@ -137,7 +126,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
137
126
|
}
|
|
138
127
|
}
|
|
139
128
|
|
|
140
|
-
async function showAllAgentsList(ctx:
|
|
129
|
+
async function showAllAgentsList(ctx: ExtensionContext) {
|
|
141
130
|
const allNames = getAllTypes();
|
|
142
131
|
if (allNames.length === 0) {
|
|
143
132
|
ctx.ui.notify("No agents.", "info");
|
|
@@ -191,7 +180,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
191
180
|
}
|
|
192
181
|
}
|
|
193
182
|
|
|
194
|
-
async function showRunningAgents(ctx:
|
|
183
|
+
async function showRunningAgents(ctx: ExtensionContext) {
|
|
195
184
|
const agents = deps.manager.listAgents();
|
|
196
185
|
if (agents.length === 0) {
|
|
197
186
|
ctx.ui.notify("No agents.", "info");
|
|
@@ -215,7 +204,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
215
204
|
await showRunningAgents(ctx);
|
|
216
205
|
}
|
|
217
206
|
|
|
218
|
-
async function viewAgentConversation(ctx:
|
|
207
|
+
async function viewAgentConversation(ctx: ExtensionContext, record: AgentRecord) {
|
|
219
208
|
if (!record.session) {
|
|
220
209
|
ctx.ui.notify(
|
|
221
210
|
`Agent is ${record.status === "queued" ? "queued" : "expired"} — no session available.`,
|
|
@@ -245,7 +234,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
245
234
|
);
|
|
246
235
|
}
|
|
247
236
|
|
|
248
|
-
async function showAgentDetail(ctx:
|
|
237
|
+
async function showAgentDetail(ctx: ExtensionContext, name: string) {
|
|
249
238
|
if (resolveType(name) == null) {
|
|
250
239
|
ctx.ui.notify(`Agent config not found for "${name}".`, "warning");
|
|
251
240
|
return;
|
|
@@ -312,7 +301,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
312
301
|
}
|
|
313
302
|
}
|
|
314
303
|
|
|
315
|
-
async function ejectAgent(ctx:
|
|
304
|
+
async function ejectAgent(ctx: ExtensionContext, name: string, cfg: AgentConfig) {
|
|
316
305
|
const location = await ctx.ui.select("Choose location", [
|
|
317
306
|
"Project (.pi/agents/)",
|
|
318
307
|
`Personal (${deps.personalAgentsDir})`,
|
|
@@ -363,7 +352,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
363
352
|
ctx.ui.notify(`Ejected ${name} to ${targetPath}`, "info");
|
|
364
353
|
}
|
|
365
354
|
|
|
366
|
-
async function disableAgent(ctx:
|
|
355
|
+
async function disableAgent(ctx: ExtensionContext, name: string) {
|
|
367
356
|
const file = findAgentFile(name);
|
|
368
357
|
if (file) {
|
|
369
358
|
const content = readFileSync(file.path, "utf-8");
|
|
@@ -397,7 +386,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
397
386
|
ctx.ui.notify(`Disabled ${name} (${targetPath})`, "info");
|
|
398
387
|
}
|
|
399
388
|
|
|
400
|
-
async function enableAgent(ctx:
|
|
389
|
+
async function enableAgent(ctx: ExtensionContext, name: string) {
|
|
401
390
|
const file = findAgentFile(name);
|
|
402
391
|
if (!file) return;
|
|
403
392
|
|
|
@@ -416,7 +405,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
416
405
|
}
|
|
417
406
|
}
|
|
418
407
|
|
|
419
|
-
async function showCreateWizard(ctx:
|
|
408
|
+
async function showCreateWizard(ctx: ExtensionContext) {
|
|
420
409
|
const location = await ctx.ui.select("Choose location", [
|
|
421
410
|
"Project (.pi/agents/)",
|
|
422
411
|
`Personal (${deps.personalAgentsDir})`,
|
|
@@ -440,7 +429,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
440
429
|
}
|
|
441
430
|
}
|
|
442
431
|
|
|
443
|
-
async function showGenerateWizard(ctx:
|
|
432
|
+
async function showGenerateWizard(ctx: ExtensionContext, targetDir: string) {
|
|
444
433
|
const description = await ctx.ui.input("Describe what this agent should do");
|
|
445
434
|
if (!description) return;
|
|
446
435
|
|
|
@@ -526,7 +515,7 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
526
515
|
}
|
|
527
516
|
}
|
|
528
517
|
|
|
529
|
-
async function showManualWizard(ctx:
|
|
518
|
+
async function showManualWizard(ctx: ExtensionContext, targetDir: string) {
|
|
530
519
|
const name = await ctx.ui.input("Agent name (filename, no spaces)");
|
|
531
520
|
if (!name) return;
|
|
532
521
|
|
|
@@ -621,7 +610,7 @@ ${systemPrompt}
|
|
|
621
610
|
ctx.ui.notify(`Created ${targetPath}`, "info");
|
|
622
611
|
}
|
|
623
612
|
|
|
624
|
-
async function showSettings(ctx:
|
|
613
|
+
async function showSettings(ctx: ExtensionContext) {
|
|
625
614
|
const choice = await ctx.ui.select("Settings", [
|
|
626
615
|
`Max concurrency (current: ${deps.manager.getMaxConcurrent()})`,
|
|
627
616
|
`Default max turns (current: ${deps.getDefaultMaxTurns() ?? "unlimited"})`,
|
|
@@ -677,13 +666,13 @@ ${systemPrompt}
|
|
|
677
666
|
}
|
|
678
667
|
}
|
|
679
668
|
|
|
680
|
-
function notifyApplied(ctx:
|
|
669
|
+
function notifyApplied(ctx: ExtensionContext, successMsg: string) {
|
|
681
670
|
const { message, level } = deps.saveSettings(deps.snapshotSettings(), successMsg);
|
|
682
|
-
ctx.ui.notify(message, level);
|
|
671
|
+
ctx.ui.notify(message, level as "info" | "warning" | "error");
|
|
683
672
|
}
|
|
684
673
|
|
|
685
674
|
// Return the handler function
|
|
686
|
-
return async (ctx:
|
|
675
|
+
return async (ctx: ExtensionContext) => {
|
|
687
676
|
await showAgentsMenu(ctx);
|
|
688
677
|
};
|
|
689
678
|
}
|