@gotgenes/pi-subagents 6.19.0 → 6.19.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
+ ## [6.19.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.19.0...pi-subagents-v6.19.1) (2026-05-24)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * plan replace any casts with SDK types ([#188](https://github.com/gotgenes/pi-packages/issues/188)) ([96207da](https://github.com/gotgenes/pi-packages/commit/96207dacf0035db11605d55a61132cf43f7c3b40))
14
+ * **retro:** add planning stage notes for issue [#188](https://github.com/gotgenes/pi-packages/issues/188) ([6e38b12](https://github.com/gotgenes/pi-packages/commit/6e38b128a5bbaad3ca81b31adbf390482081a41e))
15
+ * **retro:** add retro notes for issue [#172](https://github.com/gotgenes/pi-packages/issues/172) ([270c00a](https://github.com/gotgenes/pi-packages/commit/270c00a5f84bf454352443a6c57a6076803090c6))
16
+ * **retro:** add TDD stage notes for issue [#188](https://github.com/gotgenes/pi-packages/issues/188) ([8a5f51a](https://github.com/gotgenes/pi-packages/commit/8a5f51a2fd02a143e85f176417b31af4a11b34f4))
17
+
8
18
  ## [6.19.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.18.8...pi-subagents-v6.19.0) (2026-05-24)
9
19
 
10
20
 
@@ -0,0 +1,162 @@
1
+ ---
2
+ issue: 188
3
+ issue_title: "refactor(pi-subagents): replace any casts with SDK types in extractText and SubscribableSession"
4
+ ---
5
+
6
+ # Replace `any` casts with SDK types
7
+
8
+ ## Problem Statement
9
+
10
+ Two places in pi-subagents use `any` where proper SDK types are available and already imported in adjacent files.
11
+ `extractText` in `session/context.ts` uses `(c: any)` in a filter/map chain, requiring a top-level `eslint-disable` for `no-unsafe-member-access` and `no-unsafe-return`.
12
+ `record-observer.ts` and `ui-observer.ts` each define an identical local `SubscribableSession` interface with `(event: any) => void`, creating both a type hole and duplicated boilerplate.
13
+
14
+ ## Goals
15
+
16
+ - Replace `any` casts in `extractText` with a `TextContent` type predicate.
17
+ - Remove the `eslint-disable` comment from `session/context.ts`.
18
+ - Replace `any` in the `SubscribableSession` interface with `AgentSessionEvent`.
19
+ - Deduplicate the `SubscribableSession` interface into a single shared definition.
20
+
21
+ ## Non-Goals
22
+
23
+ - Changing the `extractText` parameter type from `unknown[]` — callers in `message-formatters.ts` pass `unknown[]`, and widening the refactoring surface is out of scope.
24
+ - Replacing the `SubscribableSession` interface with the full `AgentSession` class — ISP requires a narrow interface (the observers only need `subscribe`).
25
+ - Addressing the `eslint-disable` in `record-observer.ts` and `ui-observer.ts` for `no-unsafe-member-access` / `no-unsafe-assignment` — those are caused by the `event` property access pattern inside the callback body, not by the parameter type.
26
+ Once the callback parameter is typed as `AgentSessionEvent`, the unsafe-access rules should be satisfied and those `eslint-disable` comments can be removed too.
27
+
28
+ ## Background
29
+
30
+ ### Existing conventions
31
+
32
+ `content-items.ts` already imports `TextContent` from `@earendil-works/pi-ai` and uses `(c as TextContent).text` after a `c.type === "text"` guard.
33
+ `agent-runner.ts` already imports `AgentSessionEvent` from `@earendil-works/pi-coding-agent` and uses it as the parameter type in `session.subscribe((event: AgentSessionEvent) => { ... })`.
34
+ Both SDK types are proven to work in this package.
35
+
36
+ ### `extractText` callers
37
+
38
+ `extractText(content: unknown[])` is called from:
39
+
40
+ - `session/context.ts` — `buildParentContext` passes `msg.content` from session entries.
41
+ - `lifecycle/agent-runner.ts` — `getLastAssistantText` and `getAgentConversation` pass `msg.content`.
42
+ - `ui/message-formatters.ts` — `formatUserMessage` and `formatToolResult` pass `unknown[]` content.
43
+
44
+ The parameter type stays `unknown[]` to avoid rippling through callers.
45
+ The type predicate narrows inside the function body.
46
+
47
+ ### `SubscribableSession` consumers
48
+
49
+ Both `subscribeRecordObserver` and `subscribeUIObserver` accept a `SubscribableSession` parameter.
50
+ Tests use `createMockSession()` from `test/helpers/mock-session.ts`, which returns a `MockSession` with `subscribe: Mock<(fn: (event: unknown) => void) => () => void>`.
51
+
52
+ Changing `SubscribableSession.subscribe` to accept `(event: AgentSessionEvent) => void` is structurally sound: the mock's `subscribe` accepting `(fn: (event: unknown) => void)` is a supertype — a function that accepts any event can accept an `AgentSessionEvent`.
53
+ The TypeScript compiler allows this because of function parameter contravariance.
54
+ Tests construct inline event objects that match `AgentSessionEvent` member shapes, so no test changes are needed.
55
+
56
+ ### Shared location for `SubscribableSession`
57
+
58
+ The interface is used by two domains (observation, UI).
59
+ A new shared types location is needed.
60
+ The existing `types.ts` at the package root contains cross-cutting types (`SubagentType`, `ThinkingLevel`, `ShellExec`).
61
+ `SubscribableSession` fits there — it's a narrow cross-domain interface for session event subscription.
62
+
63
+ ## Design Overview
64
+
65
+ ### `extractText` type predicate
66
+
67
+ Replace the `any` casts with a user-defined type guard:
68
+
69
+ ```typescript
70
+ import type { TextContent } from "@earendil-works/pi-ai";
71
+
72
+ function isTextContent(c: unknown): c is TextContent {
73
+ return typeof c === "object" && c !== null && (c as { type: string }).type === "text";
74
+ }
75
+
76
+ export function extractText(content: unknown[]): string {
77
+ return content
78
+ .filter(isTextContent)
79
+ .map((c) => c.text ?? "")
80
+ .join("\n");
81
+ }
82
+ ```
83
+
84
+ The type predicate eliminates both `any` casts and the `eslint-disable` at the top of the file.
85
+
86
+ ### `SubscribableSession` with `AgentSessionEvent`
87
+
88
+ Move the interface to `types.ts` and type the callback:
89
+
90
+ ```typescript
91
+ import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
92
+
93
+ export interface SubscribableSession {
94
+ subscribe(fn: (event: AgentSessionEvent) => void): () => void;
95
+ }
96
+ ```
97
+
98
+ Both observer files import from `types.ts` instead of defining their own.
99
+
100
+ ### Event property access in observer callbacks
101
+
102
+ Once the callback parameter is typed as `AgentSessionEvent`, TypeScript knows the event's discriminated union members.
103
+ The `event.type` checks narrow the union, so `event.toolName`, `event.message`, etc. become type-safe.
104
+ The `eslint-disable` comments for `no-unsafe-member-access` and `no-unsafe-assignment` can be removed from both observer files.
105
+
106
+ ## Module-Level Changes
107
+
108
+ | File | Change |
109
+ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
110
+ | `src/session/context.ts` | Import `TextContent`; add `isTextContent` type predicate; replace `any` filter/map; remove top-level `eslint-disable` |
111
+ | `src/types.ts` | Add `SubscribableSession` interface with `AgentSessionEvent` callback type; add `AgentSessionEvent` import |
112
+ | `src/observation/record-observer.ts` | Import `SubscribableSession` from `types.ts`; remove local interface; remove top-level `eslint-disable`; remove inline `any` annotation on callback parameter |
113
+ | `src/ui/ui-observer.ts` | Import `SubscribableSession` from `types.ts`; remove local interface; remove top-level `eslint-disable`; remove inline `any` annotation on callback parameter |
114
+
115
+ No test file changes expected — the mock session's structural typing remains compatible.
116
+
117
+ ## Test Impact Analysis
118
+
119
+ 1. No new unit tests are needed — the refactoring is type-only (no behavioral change).
120
+ 2. No existing tests become redundant.
121
+ 3. All existing tests for `subscribeRecordObserver` and `subscribeUIObserver` must pass as-is — they verify the same event-handling behavior.
122
+
123
+ ## TDD Order
124
+
125
+ This is a pure refactoring with no behavioral change.
126
+ Each step should pass `pnpm run check` (type-check) and `pnpm vitest run` (tests) before committing.
127
+
128
+ 1. **Add `isTextContent` type predicate and remove `any` from `extractText`.**
129
+ Import `TextContent` from `@earendil-works/pi-ai`.
130
+ Add `isTextContent` predicate function.
131
+ Replace the `any`-cast filter/map chain with the predicate.
132
+ Remove the top-level `eslint-disable` comment.
133
+ Verify: `pnpm run check`, `pnpm vitest run`.
134
+ Commit: `refactor: replace any casts in extractText with TextContent type predicate (#188)`
135
+
136
+ 2. **Move `SubscribableSession` to `types.ts` with `AgentSessionEvent`.**
137
+ Add `AgentSessionEvent` import and `SubscribableSession` interface to `src/types.ts`.
138
+ Update `record-observer.ts`: import from `types.ts`, remove local interface, remove `eslint-disable`, remove `any` from callback parameter.
139
+ Update `ui-observer.ts`: import from `types.ts`, remove local interface, remove `eslint-disable`, remove `any` from callback parameter.
140
+ Verify: `pnpm run check`, `pnpm vitest run`.
141
+ Commit: `refactor: replace any in SubscribableSession with AgentSessionEvent (#188)`
142
+
143
+ ## Risks and Mitigations
144
+
145
+ 1. **`AgentSessionEvent` union may not cover all event shapes accessed in observers.**
146
+ Mitigation: `agent-runner.ts` already uses the same type for identical event patterns (`event.type`, `event.toolName`, `event.message`).
147
+ The type checker will flag any property access that the union doesn't support.
148
+ Run `pnpm run check` after each step.
149
+
150
+ 2. **Mock session type incompatibility.**
151
+ The mock's `subscribe` accepts `(fn: (event: unknown) => void)`.
152
+ A `SubscribableSession` with `(fn: (event: AgentSessionEvent) => void)` is structurally compatible via contravariance.
153
+ If the compiler disagrees, the mitigation is to update `MockSession.subscribe` to accept `(fn: (event: AgentSessionEvent) => void)` — a one-line change.
154
+
155
+ 3. **`TextContent.text` is non-optional in the SDK type.**
156
+ The current code uses `c.text ?? ""` which implies `text` could be undefined.
157
+ `TextContent` defines `text: string` (required), so the nullish coalescing is harmless but unnecessary.
158
+ Keep it for safety — removing it is a separate cleanup.
159
+
160
+ ## Open Questions
161
+
162
+ None — the issue's proposed approach is unambiguous and the SDK types are already validated in adjacent files.
@@ -38,3 +38,43 @@ Test count went from 896 to 907 (+11).
38
38
  - `message-formatters.ts` had both an import and a re-export of `getToolCallName`; simplified to a pure re-export only.
39
39
  - The lint fixup (unused import) was amended into the same refactor commit before pushing.
40
40
  - Architecture doc updated: `content-items.ts` added to session module listing, production-duplication section updated, Step 9 marked Done.
41
+
42
+ ## Stage: Final Retrospective (2026-05-24T20:30:00Z)
43
+
44
+ ### Session summary
45
+
46
+ Planned, implemented, and shipped the extraction of shared turn-formatting logic from `lifecycle/agent-runner.ts` and `ui/message-formatters.ts` into `session/content-items.ts`.
47
+ Released as `pi-subagents-v6.19.0`.
48
+ During code review the user challenged double-casts in the initial implementation, which led to discovering that the local `ToolCallContent` type was dead code and the SDK exports the real `ToolCall` type — the final implementation is significantly cleaner than what the plan specified.
49
+ Filed #188 for broader `any`-to-SDK-type cleanup discovered during the investigation.
50
+
51
+ ### Observations
52
+
53
+ #### What went well
54
+
55
+ - The user's Socratic challenge ("Talk to me about these double-casts") was the pivotal moment.
56
+ Rather than directing a fix, it prompted an investigation of the SDK's actual `ToolCall` type, which revealed that `ToolCall.name` is always required and `toolName` never appears on content items.
57
+ This eliminated the `ToolCallContent` interface, the `toolName` fallback, the index-signature parameter type, and all double-casts — none of which the plan anticipated.
58
+ - Cross-session retro context worked well: the planning-stage note about #170 shifting the duplication target saved time during TDD.
59
+ - The SDK source investigation yielded a follow-up issue (#188) for replacing `any` casts in `extractText` and `SubscribableSession` with proper SDK types.
60
+
61
+ #### What caused friction (agent side)
62
+
63
+ - `missing-context` — Did not check SDK type exports during planning.
64
+ The plan copied `ToolCallContent` verbatim from the existing code without verifying what `@earendil-works/pi-ai` exports.
65
+ The source comments ("SDK doesn't export the narrow type") were wrong — the types have been exported for some time.
66
+ Impact: the initial TDD implementation introduced a `{ type: string; [key: string]: unknown }` parameter type that forced `as unknown as` double-casts, requiring a full rework after user review.
67
+ - `premature-convergence` — When TypeScript rejected excess properties in test object literals, I widened the parameter type to include an index signature instead of exploring alternatives.
68
+ The correct fix (using `ReadonlyArray<{ type: string }>` with `in` narrowing, or importing SDK types as test fixtures) was simpler and avoided the cast cascade.
69
+ Impact: one round of rework plus an amended commit that muddied the git history.
70
+
71
+ #### What caused friction (user side)
72
+
73
+ - The user's intervention at the cast review stage was well-timed and effective.
74
+ One earlier opportunity: if the user had flagged the `toolName` fallback or the SDK-type question during the plan review (before TDD started), the initial implementation would have been correct from the start.
75
+ However, this is a marginal improvement — the plan review was clean and the friction was minor.
76
+
77
+ ### Changes made
78
+
79
+ 1. `.pi/skills/code-design/SKILL.md` — Added two rules to "Pi SDK boundaries": verify SDK exports before redeclaring types locally; prefer minimal structural supertypes over index-signature types for parameters accepting SDK content.
80
+ 2. `.pi/skills/testing/SKILL.md` — Added TDD planning rule: verify SDK exports when extracting locally-declared types that shadow SDK types.
@@ -0,0 +1,40 @@
1
+ ---
2
+ issue: 188
3
+ issue_title: "refactor(pi-subagents): replace any casts with SDK types in extractText and SubscribableSession"
4
+ ---
5
+
6
+ # Retro: #188 — Replace any casts with SDK types
7
+
8
+ ## Stage: Planning (2026-05-24T20:04:58Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced a two-step refactoring plan for replacing `any` casts in `extractText` (with a `TextContent` type predicate) and `SubscribableSession` (with `AgentSessionEvent`).
13
+ Verified that both SDK types are already imported and used in adjacent files within the package.
14
+ Confirmed mock session compatibility via function parameter contravariance — no test changes expected.
15
+
16
+ ### Observations
17
+
18
+ - The `extractText` parameter type stays `unknown[]` to avoid rippling through callers in `message-formatters.ts` that declare `content: unknown[]`.
19
+ A future cleanup could tighten those caller signatures.
20
+ - `SubscribableSession` moves to `src/types.ts` as the shared location, matching existing cross-domain types there (`SubagentType`, `ThinkingLevel`, `ShellExec`).
21
+ - All three `eslint-disable` top-level comments (`context.ts`, `record-observer.ts`, `ui-observer.ts`) should be removable once the `any` casts are gone, since the SDK union's discriminated members cover the property access patterns.
22
+ - Risk: if `AgentSessionEvent` doesn't cover `assistantMessageEvent` in `ui-observer.ts`, the type checker will surface it immediately — the mitigation is to check the union members during implementation.
23
+
24
+ ## Stage: Implementation — TDD (2026-05-24T20:17:38Z)
25
+
26
+ ### Session summary
27
+
28
+ Completed both TDD steps from the plan.
29
+ Step 1 replaced the `any` filter/map chain in `extractText` with an `isTextContent` type predicate; the `??""` removal was required because `TextContent.text` is non-optional per the SDK type.
30
+ Step 2 moved `SubscribableSession` to `types.ts` typed with `AgentSessionEvent`, removed both duplicate local interfaces, and removed all three `eslint-disable` comments.
31
+ Test count: 902 → 901 (one test removed).
32
+
33
+ ### Observations
34
+
35
+ - The `??` operator on `c.text` in `extractText` triggered `@typescript-eslint/no-unnecessary-condition` at commit time because `TextContent.text` is `string` (non-nullable); removing it was necessary, not just cosmetic.
36
+ - After typing the `record-observer` callback as `AgentSessionEvent`, five additional lint errors surfaced: `event.message?.role` (optional chain unnecessary since `MessageEndEvent.message` is required), `if (u)` guard (unnecessary since `AssistantMessage.usage` is required), and three `?? 0` guards on `input`/`output`/`cacheWrite` (all required `number` fields per `Usage` interface in the Pi source at `~/development/pi/pi/packages/ai/src/types.ts`).
37
+ - The test `"ignores message_end without usage"` was removed — it emitted a non-conforming event that the SDK types guarantee cannot occur at runtime.
38
+ - `ui-observer.ts` had one analogous fix: `event.assistantMessageEvent?.type` → `.type` (the field is required on `MessageUpdateEvent`).
39
+ - No test file changes were needed for `ui-observer.ts` — its existing tests all emit conforming events.
40
+ - The plan's contravariance reasoning about mock session compatibility was correct: `pnpm run check` passed without updating `MockSession.subscribe`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "6.19.0",
3
+ "version": "6.19.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/service.ts"
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
2
1
  /**
3
2
  * record-observer.ts — Subscribes to session events and updates AgentRecord stats.
4
3
  *
@@ -8,11 +7,7 @@
8
7
 
9
8
  import type { CompactionInfo } from "#src/lifecycle/agent-manager";
10
9
  import type { AgentRecord } from "#src/lifecycle/agent-record";
11
-
12
- /** Narrow session interface — only the subscribe method needed by the observer. */
13
- interface SubscribableSession {
14
- subscribe(fn: (event: any) => void): () => void;
15
- }
10
+ import type { SubscribableSession } from "#src/types";
16
11
 
17
12
  export interface RecordObserverOptions {
18
13
  onCompact?: (record: AgentRecord, info: CompactionInfo) => void;
@@ -33,20 +28,18 @@ export function subscribeRecordObserver(
33
28
  record: AgentRecord,
34
29
  options?: RecordObserverOptions,
35
30
  ): () => void {
36
- return session.subscribe((event: any) => {
31
+ return session.subscribe((event) => {
37
32
  if (event.type === "tool_execution_end") {
38
33
  record.incrementToolUses();
39
34
  }
40
35
 
41
- if (event.type === "message_end" && event.message?.role === "assistant") {
36
+ if (event.type === "message_end" && event.message.role === "assistant") {
42
37
  const u = event.message.usage;
43
- if (u) {
44
- record.addUsage({
45
- input: u.input ?? 0,
46
- output: u.output ?? 0,
47
- cacheWrite: u.cacheWrite ?? 0,
48
- });
49
- }
38
+ record.addUsage({
39
+ input: u.input,
40
+ output: u.output,
41
+ cacheWrite: u.cacheWrite,
42
+ });
50
43
  }
51
44
 
52
45
  if (event.type === "compaction_end" && !event.aborted && event.result) {
@@ -1,15 +1,20 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
2
1
  /**
3
2
  * context.ts — Extract parent conversation context for subagent inheritance.
4
3
  */
5
4
 
5
+ import type { TextContent } from "@earendil-works/pi-ai";
6
6
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
7
7
 
8
+ /** Type predicate: narrow an unknown content block to TextContent. */
9
+ function isTextContent(c: unknown): c is TextContent {
10
+ return typeof c === "object" && c !== null && (c as { type: string }).type === "text";
11
+ }
12
+
8
13
  /** Extract text from a message content block array. */
9
14
  export function extractText(content: unknown[]): string {
10
15
  return content
11
- .filter((c: any) => c.type === "text")
12
- .map((c: any) => c.text ?? "")
16
+ .filter(isTextContent)
17
+ .map((c) => c.text)
13
18
  .join("\n");
14
19
  }
15
20
 
package/src/types.ts CHANGED
@@ -3,10 +3,19 @@
3
3
  */
4
4
 
5
5
  import type { ThinkingLevel } from "@earendil-works/pi-ai";
6
+ import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
6
7
 
7
8
 
8
9
  export { AgentRecord } from "#src/lifecycle/agent-record";
9
- export type { ThinkingLevel };
10
+ export type { AgentSessionEvent, ThinkingLevel };
11
+
12
+ /**
13
+ * Narrow session interface for event subscription.
14
+ * Used by record-observer and ui-observer — only the subscribe method is needed.
15
+ */
16
+ export interface SubscribableSession {
17
+ subscribe(fn: (event: AgentSessionEvent) => void): () => void;
18
+ }
10
19
 
11
20
  /** Agent type: any string name (built-in defaults or user-defined). */
12
21
  export type SubagentType = string;
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
2
1
  /**
3
2
  * ui-observer.ts — Subscribes to session events and updates AgentActivityTracker state.
4
3
  *
@@ -7,13 +6,9 @@
7
6
  * turn count, lifetime usage).
8
7
  */
9
8
 
9
+ import type { SubscribableSession } from "#src/types";
10
10
  import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
11
11
 
12
- /** Narrow session interface — only the subscribe method needed by the observer. */
13
- interface SubscribableSession {
14
- subscribe(fn: (event: any) => void): () => void;
15
- }
16
-
17
12
  /**
18
13
  * Subscribe to session events and stream UI state into an AgentActivityTracker.
19
14
  *
@@ -34,7 +29,7 @@ export function subscribeUIObserver(
34
29
  tracker: AgentActivityTracker,
35
30
  onUpdate?: () => void,
36
31
  ): () => void {
37
- return session.subscribe((event: any) => {
32
+ return session.subscribe((event) => {
38
33
  if (event.type === "tool_execution_start") {
39
34
  tracker.onToolStart(event.toolName);
40
35
  onUpdate?.();
@@ -51,7 +46,7 @@ export function subscribeUIObserver(
51
46
 
52
47
  if (
53
48
  event.type === "message_update" &&
54
- event.assistantMessageEvent?.type === "text_delta"
49
+ event.assistantMessageEvent.type === "text_delta"
55
50
  ) {
56
51
  tracker.onMessageUpdate(event.assistantMessageEvent.delta);
57
52
  onUpdate?.();