@gotgenes/pi-subagents 7.5.0 → 7.5.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,15 @@ 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
|
+
## [7.5.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.5.0...pi-subagents-v7.5.1) (2026-05-26)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* plan decompose buildParentContext ([#215](https://github.com/gotgenes/pi-packages/issues/215)) ([9103609](https://github.com/gotgenes/pi-packages/commit/910360991b50c320927c1457bfef6b7cb5624b7b))
|
|
14
|
+
* **retro:** add planning stage notes for issue [#215](https://github.com/gotgenes/pi-packages/issues/215) ([5c534d5](https://github.com/gotgenes/pi-packages/commit/5c534d5efb640ef1d72d6ccf7bf2e15ac2acf755))
|
|
15
|
+
* **retro:** add TDD stage notes for issue [#215](https://github.com/gotgenes/pi-packages/issues/215) ([79064d0](https://github.com/gotgenes/pi-packages/commit/79064d072c36c2f92013dbfba58ce1de1ab01bce))
|
|
16
|
+
|
|
8
17
|
## [7.5.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.4.0...pi-subagents-v7.5.0) (2026-05-26)
|
|
9
18
|
|
|
10
19
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 215
|
|
3
|
+
issue_title: "Decompose buildParentContext (cognitive 30) (Phase 13, Step 2)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Decompose `buildParentContext`
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`buildParentContext` in `src/session/context.ts` is the only remaining fallow refactoring target in the package.
|
|
11
|
+
The function has a cognitive complexity of 30, driven by a loop with three type-check branches (`message`, `compaction`, default), each with sub-branches for role (`user` vs `assistant`) and content type (`string` vs array).
|
|
12
|
+
The architecture roadmap (Phase 13, Step 2) targets cognitive complexity < 10 and function body < 15 LOC.
|
|
13
|
+
|
|
14
|
+
## Goals
|
|
15
|
+
|
|
16
|
+
- Extract per-entry-type formatters: `formatMessageEntry(entry)` and `formatCompactionEntry(entry)`.
|
|
17
|
+
- Reduce `buildParentContext` to a loop + filter + join orchestrator (< 15 LOC).
|
|
18
|
+
- Achieve cognitive complexity < 10 for all functions in the file.
|
|
19
|
+
- Add unit tests for the extracted formatters and the orchestrator.
|
|
20
|
+
|
|
21
|
+
## Non-Goals
|
|
22
|
+
|
|
23
|
+
- Changing the public API surface (`buildParentContext`, `extractText`) — signatures stay the same.
|
|
24
|
+
- Moving `extractText` to another module (noted as a follow-up in prior plans but out of scope).
|
|
25
|
+
- Refactoring callers (`parent-snapshot.ts`) — they are already tested via mocks.
|
|
26
|
+
|
|
27
|
+
## Background
|
|
28
|
+
|
|
29
|
+
### Current file: `src/session/context.ts`
|
|
30
|
+
|
|
31
|
+
The file exports two functions:
|
|
32
|
+
|
|
33
|
+
1. `extractText(content: unknown[]): string` — filters an array of content blocks to `TextContent` items and joins their `.text` values.
|
|
34
|
+
Used by `agent-runner.ts`, `message-formatters.ts`, and `buildParentContext` itself.
|
|
35
|
+
2. `buildParentContext(ctx: SessionContext): string` — iterates session branch entries, formatting `message` entries (user/assistant) and `compaction` entries into a text representation prefixed with a header.
|
|
36
|
+
|
|
37
|
+
The file also defines three local types (`MessageEntry`, `CompactionEntry`, `BranchEntry`) and one helper (`isTextContent`).
|
|
38
|
+
|
|
39
|
+
### Callers
|
|
40
|
+
|
|
41
|
+
- `buildParentContext` is called only from `parent-snapshot.ts` (where it is mocked in tests).
|
|
42
|
+
- `extractText` is called from `agent-runner.ts`, `message-formatters.ts`, and internally within `buildParentContext`.
|
|
43
|
+
|
|
44
|
+
### Existing tests
|
|
45
|
+
|
|
46
|
+
There are no direct unit tests for `context.ts`.
|
|
47
|
+
`parent-snapshot.test.ts` mocks `buildParentContext` entirely, so the formatting logic is currently untested.
|
|
48
|
+
|
|
49
|
+
## Design Overview
|
|
50
|
+
|
|
51
|
+
### Extracted formatters
|
|
52
|
+
|
|
53
|
+
Each formatter takes a typed entry and returns `string | undefined` (undefined when the entry should be skipped):
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
function formatMessageEntry(entry: MessageEntry): string | undefined {
|
|
57
|
+
const msg = entry.message;
|
|
58
|
+
const text =
|
|
59
|
+
typeof msg.content === "string"
|
|
60
|
+
? msg.content
|
|
61
|
+
: extractText(msg.content);
|
|
62
|
+
if (!text.trim()) return undefined;
|
|
63
|
+
if (msg.role === "user") return `[User]: ${text.trim()}`;
|
|
64
|
+
if (msg.role === "assistant") return `[Assistant]: ${text.trim()}`;
|
|
65
|
+
return undefined; // skip toolResult and other roles
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatCompactionEntry(entry: CompactionEntry): string | undefined {
|
|
69
|
+
return entry.summary ? `[Summary]: ${entry.summary}` : undefined;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Simplified orchestrator
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
export function buildParentContext(ctx: SessionContext): string {
|
|
77
|
+
const entries = ctx.sessionManager.getBranch();
|
|
78
|
+
if (!entries || entries.length === 0) return "";
|
|
79
|
+
|
|
80
|
+
const parts = (entries as BranchEntry[])
|
|
81
|
+
.map(formatBranchEntry)
|
|
82
|
+
.filter((p): p is string => p !== undefined);
|
|
83
|
+
|
|
84
|
+
if (parts.length === 0) return "";
|
|
85
|
+
|
|
86
|
+
return `# Parent Conversation Context
|
|
87
|
+
The following is the conversation history from the parent session that spawned you.
|
|
88
|
+
Use this context to understand what has been discussed and decided so far.
|
|
89
|
+
|
|
90
|
+
${parts.join("\n\n")}
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
# Your Task (below)
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
A thin dispatcher (`formatBranchEntry`) routes by `type`:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
function formatBranchEntry(entry: BranchEntry): string | undefined {
|
|
102
|
+
if (entry.type === "message") return formatMessageEntry(entry as MessageEntry);
|
|
103
|
+
if (entry.type === "compaction") return formatCompactionEntry(entry as CompactionEntry);
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Complexity analysis
|
|
109
|
+
|
|
110
|
+
- `formatMessageEntry`: 3 branches (string-vs-array, empty check, role) — estimated cognitive complexity ~4.
|
|
111
|
+
- `formatCompactionEntry`: 1 branch — estimated cognitive complexity ~1.
|
|
112
|
+
- `formatBranchEntry`: 2 branches — estimated cognitive complexity ~2.
|
|
113
|
+
- `buildParentContext`: 2 branches (empty entries, empty parts) — estimated cognitive complexity ~3.
|
|
114
|
+
|
|
115
|
+
All well under the < 10 target.
|
|
116
|
+
|
|
117
|
+
## Module-Level Changes
|
|
118
|
+
|
|
119
|
+
### `src/session/context.ts`
|
|
120
|
+
|
|
121
|
+
1. Add `formatMessageEntry(entry: MessageEntry): string | undefined` — private helper.
|
|
122
|
+
2. Add `formatCompactionEntry(entry: CompactionEntry): string | undefined` — private helper.
|
|
123
|
+
3. Add `formatBranchEntry(entry: BranchEntry): string | undefined` — private dispatcher.
|
|
124
|
+
4. Simplify `buildParentContext` body to use `map(formatBranchEntry).filter(...)`.
|
|
125
|
+
5. No changes to exports — `buildParentContext` and `extractText` signatures are unchanged.
|
|
126
|
+
6. No changes to local types (`MessageEntry`, `CompactionEntry`, `BranchEntry`) or `isTextContent`.
|
|
127
|
+
|
|
128
|
+
### `test/session/context.test.ts` (new)
|
|
129
|
+
|
|
130
|
+
Unit tests for:
|
|
131
|
+
|
|
132
|
+
- `extractText` — string extraction from mixed content arrays.
|
|
133
|
+
- `buildParentContext` — end-to-end formatting with user, assistant, compaction, and skipped entries.
|
|
134
|
+
|
|
135
|
+
The formatters are private, so they are tested indirectly through `buildParentContext`.
|
|
136
|
+
|
|
137
|
+
## Test Impact Analysis
|
|
138
|
+
|
|
139
|
+
1. The new `context.test.ts` enables direct testing of formatting logic that was previously untested (mocked away in `parent-snapshot.test.ts`).
|
|
140
|
+
2. No existing tests become redundant — `parent-snapshot.test.ts` tests snapshot assembly, not formatting.
|
|
141
|
+
3. No existing tests need modification — the public API is unchanged.
|
|
142
|
+
|
|
143
|
+
## TDD Order
|
|
144
|
+
|
|
145
|
+
1. **Red → Green:** Add `test/session/context.test.ts` with tests for `extractText` — empty array, text-only, mixed content types, no text content.
|
|
146
|
+
Commit: `test: add extractText unit tests (#215)`
|
|
147
|
+
|
|
148
|
+
2. **Red → Green:** Add tests for `buildParentContext` — empty branch, user messages, assistant messages, compaction entries with/without summary, mixed entry types, entries with empty text (skipped), non-message/non-compaction entries (skipped), string vs array content.
|
|
149
|
+
Commit: `test: add buildParentContext unit tests (#215)`
|
|
150
|
+
|
|
151
|
+
3. **Refactor:** Extract `formatMessageEntry`, `formatCompactionEntry`, and `formatBranchEntry` from `buildParentContext`.
|
|
152
|
+
Simplify `buildParentContext` to map/filter/join.
|
|
153
|
+
All tests from steps 1–2 must still pass.
|
|
154
|
+
Commit: `refactor: decompose buildParentContext into per-entry formatters (#215)`
|
|
155
|
+
|
|
156
|
+
## Risks and Mitigations
|
|
157
|
+
|
|
158
|
+
| Risk | Mitigation |
|
|
159
|
+
| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
160
|
+
| Behavioral regression in formatting | Steps 1–2 lock in current behavior with tests before refactoring |
|
|
161
|
+
| Extracted helpers expose implementation details | Helpers are private (not exported); tested indirectly via public API |
|
|
162
|
+
| `eslint-disable` comment for `no-unnecessary-condition` on `getBranch()` check may need adjustment | Preserve the comment — runtime nullability is documented |
|
|
163
|
+
|
|
164
|
+
## Open Questions
|
|
165
|
+
|
|
166
|
+
None — the decomposition target and strategy are specified by the architecture roadmap.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 215
|
|
3
|
+
issue_title: "Decompose buildParentContext (cognitive 30) (Phase 13, Step 2)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #215 — Decompose buildParentContext
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-25T12:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Produced a 3-step TDD plan to decompose `buildParentContext` in `src/session/context.ts`.
|
|
13
|
+
Steps 1–2 add tests locking current behavior for `extractText` and `buildParentContext`; step 3 extracts three private helpers (`formatMessageEntry`, `formatCompactionEntry`, `formatBranchEntry`) and simplifies the orchestrator to map/filter/join.
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- No existing unit tests cover `context.ts` — `parent-snapshot.test.ts` mocks `buildParentContext` entirely, so the formatting logic is currently untested.
|
|
18
|
+
- The decomposition is straightforward with no design ambiguity; the architecture roadmap specifies the exact extraction targets.
|
|
19
|
+
- All extracted helpers remain private (not exported), keeping the public API surface unchanged.
|
|
20
|
+
- The `eslint-disable` comment on the `getBranch()` nullability check must be preserved through the refactoring step.
|
|
21
|
+
|
|
22
|
+
## Stage: Implementation — TDD (2026-05-25T22:36:00Z)
|
|
23
|
+
|
|
24
|
+
### Session summary
|
|
25
|
+
|
|
26
|
+
Completed all 3 TDD steps: 2 test-only commits locking `extractText` (5 tests) and `buildParentContext` (14 tests) behavior, then a refactor commit extracting `formatMessageEntry`, `formatCompactionEntry`, and `formatBranchEntry`.
|
|
27
|
+
Test count increased from 939 to 958 (+19).
|
|
28
|
+
All checks green: full suite, `pnpm run check`, `pnpm run lint`, `pnpm fallow dead-code`.
|
|
29
|
+
|
|
30
|
+
### Observations
|
|
31
|
+
|
|
32
|
+
- Because `extractText` and `buildParentContext` already existed, both test steps passed immediately (no red phase) — this is correct for behavior-locking tests before a refactor.
|
|
33
|
+
- The `makeCtx` helper in the test file creates a minimal `SessionContext` satisfying only `sessionManager.getBranch()`; the extra required fields (`cwd`, `model`, `modelRegistry`, `getSystemPrompt`) are satisfied with stubs.
|
|
34
|
+
- The `eslint-disable` comment on the `getBranch()` nullability check was preserved unchanged through the refactor.
|
|
35
|
+
- No deviations from the plan.
|
package/package.json
CHANGED
package/src/session/context.ts
CHANGED
|
@@ -30,6 +30,28 @@ export function extractText(content: unknown[]): string {
|
|
|
30
30
|
.join("\n");
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Format a message entry (user/assistant); returns undefined for roles to skip. */
|
|
34
|
+
function formatMessageEntry(entry: MessageEntry): string | undefined {
|
|
35
|
+
const msg = entry.message;
|
|
36
|
+
const text = typeof msg.content === "string" ? msg.content : extractText(msg.content);
|
|
37
|
+
if (!text.trim()) return undefined;
|
|
38
|
+
if (msg.role === "user") return `[User]: ${text.trim()}`;
|
|
39
|
+
if (msg.role === "assistant") return `[Assistant]: ${text.trim()}`;
|
|
40
|
+
return undefined; // skip toolResult and other roles
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Format a compaction entry; returns undefined when no summary is present. */
|
|
44
|
+
function formatCompactionEntry(entry: CompactionEntry): string | undefined {
|
|
45
|
+
return entry.summary ? `[Summary]: ${entry.summary}` : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Dispatch a branch entry to the appropriate formatter. */
|
|
49
|
+
function formatBranchEntry(entry: BranchEntry): string | undefined {
|
|
50
|
+
if (entry.type === "message") return formatMessageEntry(entry as MessageEntry);
|
|
51
|
+
if (entry.type === "compaction") return formatCompactionEntry(entry as CompactionEntry);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
33
55
|
/**
|
|
34
56
|
* Build a text representation of the parent conversation context.
|
|
35
57
|
* Used when inherit_context is true to give the subagent visibility
|
|
@@ -40,30 +62,9 @@ export function buildParentContext(ctx: SessionContext): string {
|
|
|
40
62
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- getBranch() may return undefined at runtime despite its type
|
|
41
63
|
if (!entries || entries.length === 0) return "";
|
|
42
64
|
|
|
43
|
-
const parts
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (rawEntry.type === "message") {
|
|
47
|
-
const entry = rawEntry as MessageEntry;
|
|
48
|
-
const msg = entry.message;
|
|
49
|
-
if (msg.role === "user") {
|
|
50
|
-
const text = typeof msg.content === "string"
|
|
51
|
-
? msg.content
|
|
52
|
-
: extractText(msg.content);
|
|
53
|
-
if (text.trim()) parts.push(`[User]: ${text.trim()}`);
|
|
54
|
-
} else if (msg.role === "assistant") {
|
|
55
|
-
const text = typeof msg.content === "string" ? msg.content : extractText(msg.content);
|
|
56
|
-
if (text.trim()) parts.push(`[Assistant]: ${text.trim()}`);
|
|
57
|
-
}
|
|
58
|
-
// Skip toolResult messages — too verbose for context
|
|
59
|
-
} else if (rawEntry.type === "compaction") {
|
|
60
|
-
// Include compaction summaries — they're already condensed
|
|
61
|
-
const entry = rawEntry as CompactionEntry;
|
|
62
|
-
if (entry.summary) {
|
|
63
|
-
parts.push(`[Summary]: ${entry.summary}`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
65
|
+
const parts = (entries as BranchEntry[])
|
|
66
|
+
.map(formatBranchEntry)
|
|
67
|
+
.filter((p): p is string => p !== undefined);
|
|
67
68
|
|
|
68
69
|
if (parts.length === 0) return "";
|
|
69
70
|
|