@gotgenes/pi-subagents 5.8.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 CHANGED
@@ -5,6 +5,16 @@ 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
+
8
18
  ## [5.8.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.7.0...pi-subagents-v5.8.0) (2026-05-20)
9
19
 
10
20
 
@@ -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
- - Add session-context methods (`setSessionContext`, `clearSessionContext`) and widget delegation methods (`setUICtx`, `onTurnStart`, `markFinished`, `updateWidget`, `ensureTimer`).
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
- - Move the four inline lambdas (`session_start`, `session_before_switch`, `session_shutdown`, `tool_execution_start`) into named handler modules.
383
- - Requires Issues #69 and #87 because handlers need the `SubagentRuntime` with methods as their deps.
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 in progress.
434
- The next issue is #87 (runtime methods), which unblocks #70 (handler extraction).
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "5.8.0",
3
+ "version": "5.8.1",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },
@@ -42,7 +42,7 @@ interface SpawnArgs {
42
42
  options: SpawnOptions;
43
43
  }
44
44
 
45
- interface SpawnOptions {
45
+ export interface SpawnOptions {
46
46
  description: string;
47
47
  model?: Model<any>;
48
48
  maxTurns?: number;
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 as any, opts as any),
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 as any, type, prompt, opts as any),
189
- spawnAndWait: (ctx, type, prompt, opts) => manager.spawnAndWait(pi, ctx as any, type, prompt, opts as any),
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
- }) as any));
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 as any),
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 as any, message),
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((piArg ?? pi) as any, ctx as any, type, prompt, opts as any),
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 as any);
242
+ const resolved = resolveModel(cfg.model, registry);
243
243
  if (typeof resolved === 'string') return 'inherit';
244
244
  }
245
245
  return getModelLabelFromConfig(cfg.model);
@@ -268,6 +268,6 @@ export default function (pi: ExtensionAPI) {
268
268
 
269
269
  pi.registerCommand('agents', {
270
270
  description: 'Manage agents',
271
- handler: async (_args, ctx) => { await agentsMenuHandler(ctx as any); },
271
+ handler: async (_args, ctx) => { await agentsMenuHandler(ctx); },
272
272
  });
273
273
  }
@@ -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: (msg: unknown, opts: unknown) => void;
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;
@@ -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: unknown, type: string, prompt: string, opts: object) => string;
125
- spawnAndWait: (ctx: unknown, type: string, prompt: string, opts: object) => Promise<AgentRecord>;
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: unknown) => void) | undefined,
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: unknown) => string | undefined;
12
+ getConversation: (session: AgentSession) => string | undefined;
12
13
  }
13
14
 
14
15
  /** Create the get_subagent_result tool definition (without Pi SDK wrapper). */
@@ -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: unknown, message: string) => Promise<void>;
11
+ steerAgent: (session: AgentSession, message: string) => Promise<void>;
11
12
  }
12
13
 
13
14
  /** Create the steer_subagent tool definition (without Pi SDK wrapper). */
@@ -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: unknown, ctx: unknown, type: string, prompt: string, opts: object) => Promise<AgentRecord>;
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?: unknown) => string;
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: MenuContext) {
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: MenuContext) {
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: MenuContext) {
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: MenuContext, record: AgentRecord) {
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: MenuContext, name: string) {
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: MenuContext, name: string, cfg: AgentConfig) {
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: MenuContext, name: string) {
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: MenuContext, name: string) {
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: MenuContext) {
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: MenuContext, targetDir: string) {
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: MenuContext, targetDir: string) {
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: MenuContext) {
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: MenuContext, successMsg: string) {
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: MenuContext) => {
675
+ return async (ctx: ExtensionContext) => {
687
676
  await showAgentsMenu(ctx);
688
677
  };
689
678
  }