@gotgenes/pi-subagents 11.1.0 → 11.3.0
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 +19 -0
- package/docs/architecture/architecture.md +229 -161
- package/docs/architecture/history/phase-15-domain-model-evolution.md +73 -0
- package/docs/plans/0232-agent-resume-internal-observer-lifecycle.md +180 -0
- package/docs/plans/0256-extract-worktree-isolation.md +256 -0
- package/docs/retro/0232-agent-resume-internal-observer-lifecycle.md +109 -0
- package/docs/retro/0256-extract-worktree-isolation.md +45 -0
- package/package.json +1 -1
- package/src/lifecycle/agent-manager.ts +10 -25
- package/src/lifecycle/agent.ts +52 -45
- package/src/lifecycle/worktree-isolation.ts +59 -0
- package/src/service/service-adapter.ts +1 -1
- package/src/lifecycle/worktree-state.ts +0 -45
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ 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
|
+
## [11.3.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.2.0...pi-subagents-v11.3.0) (2026-05-29)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **pi-subagents:** add WorktreeIsolation collaborator ([ee7ab73](https://github.com/gotgenes/pi-packages/commit/ee7ab73a53f8643b5887856c33d53786a5a5a9cc))
|
|
14
|
+
|
|
15
|
+
## [11.2.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.1.0...pi-subagents-v11.2.0) (2026-05-28)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* add Agent.resume() with internal observer lifecycle ([6cffb47](https://github.com/gotgenes/pi-packages/commit/6cffb47079e385b0ccd12e358c12357291be2ef0))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* release abort-signal listener when worktree setup fails ([ce2cac6](https://github.com/gotgenes/pi-packages/commit/ce2cac6788ffc90316f759e40e4df29576a70128))
|
|
26
|
+
|
|
8
27
|
## [11.1.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.0.1...pi-subagents-v11.1.0) (2026-05-28)
|
|
9
28
|
|
|
10
29
|
|
|
@@ -112,7 +112,7 @@ classDiagram
|
|
|
112
112
|
+toolUses: number
|
|
113
113
|
+lifetimeUsage: LifetimeUsage
|
|
114
114
|
+execution?: ExecutionState
|
|
115
|
-
+
|
|
115
|
+
+worktree?: WorktreeIsolation
|
|
116
116
|
+notification?: NotificationState
|
|
117
117
|
+markRunning()
|
|
118
118
|
+markCompleted()
|
|
@@ -121,12 +121,13 @@ classDiagram
|
|
|
121
121
|
+markError()
|
|
122
122
|
+markStopped()
|
|
123
123
|
+resetForResume()
|
|
124
|
+
+run()
|
|
125
|
+
+resume(prompt, signal)
|
|
124
126
|
+abort(): boolean
|
|
125
127
|
+queueSteer(message)
|
|
126
128
|
+flushPendingSteers(session)
|
|
127
|
-
+
|
|
128
|
-
+
|
|
129
|
-
+failRun(err, worktrees)
|
|
129
|
+
+completeRun(result)
|
|
130
|
+
+failRun(err)
|
|
130
131
|
+wireSignal(signal, onAbort)
|
|
131
132
|
+attachObserver(unsub)
|
|
132
133
|
+releaseListeners()
|
|
@@ -136,7 +137,7 @@ classDiagram
|
|
|
136
137
|
class AgentManager {
|
|
137
138
|
+spawn(snapshot, type, prompt, config)
|
|
138
139
|
+spawnAndWait(snapshot, type, prompt, config)
|
|
139
|
-
+resume(id,
|
|
140
|
+
+resume(id, prompt, signal)
|
|
140
141
|
+getRecord(id): Agent
|
|
141
142
|
+listAgents(): Agent[]
|
|
142
143
|
+abort(id)
|
|
@@ -275,7 +276,7 @@ src/
|
|
|
275
276
|
│ ├── execution-state.ts session/output phase state
|
|
276
277
|
│ ├── permission-bridge.ts optional bridge to pi-permission-system registry
|
|
277
278
|
│ ├── worktree.ts git worktree isolation
|
|
278
|
-
│ ├── worktree-
|
|
279
|
+
│ ├── worktree-isolation.ts worktree lifecycle collaborator
|
|
279
280
|
│ └── usage.ts token usage tracking
|
|
280
281
|
│
|
|
281
282
|
├── observation/ progress tracking and notification
|
|
@@ -488,13 +489,13 @@ This is achieved across three phases: Phase 14 (strip policy), Phase 16 (invert
|
|
|
488
489
|
| Metric | Value |
|
|
489
490
|
| -------------------------- | --------------------------------- |
|
|
490
491
|
| Health score | 78/100 (B) |
|
|
491
|
-
| Total LOC |
|
|
492
|
+
| Total LOC | 7,778 (57 files) |
|
|
492
493
|
| Dead code | 0 files, 0 exports |
|
|
493
494
|
| Maintainability index | 90.8 (good) |
|
|
494
495
|
| Avg cyclomatic complexity | 1.4 |
|
|
495
496
|
| P90 cyclomatic complexity | 2 |
|
|
496
497
|
| Production duplication | 11 lines (1 internal clone group) |
|
|
497
|
-
| Test duplication |
|
|
498
|
+
| Test duplication | 42 clone groups, 661 lines |
|
|
498
499
|
| Fallow refactoring targets | 0 |
|
|
499
500
|
|
|
500
501
|
### Dependency bag inventory
|
|
@@ -685,185 +686,252 @@ See [phase-14-strip-policy.md](history/phase-14-strip-policy.md) for details.
|
|
|
685
686
|
[#239]: https://github.com/gotgenes/pi-packages/issues/239
|
|
686
687
|
[#242]: https://github.com/gotgenes/pi-packages/issues/242
|
|
687
688
|
|
|
688
|
-
## Improvement roadmap (Phase
|
|
689
|
+
## Improvement roadmap (Phase 16 — agent collaborator architecture)
|
|
689
690
|
|
|
690
|
-
Phase
|
|
691
|
+
Phase 16 gives Agent proper collaborators so it can do its work without accumulating raw materials.
|
|
691
692
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
`
|
|
695
|
-
The
|
|
693
|
+
Phase 15 established the principle: Agent owns its lifecycle, not a manager.
|
|
694
|
+
But in practice, Agent received 9 raw config fields and a shared generic runner, then assembled the runner call itself.
|
|
695
|
+
The runner (`ConcreteAgentRunner`) is a stateless service — one instance shared across all agents — so every per-agent concern (snapshot, prompt, model, maxTurns, etc.) had to live on Agent as private fields.
|
|
696
|
+
The result: `AgentInit` has ~20 optional fields, and Agent stores ~87 `this._` references.
|
|
696
697
|
|
|
697
|
-
The
|
|
698
|
+
The deeper issue: the "runner" conflates two concerns.
|
|
699
|
+
Session *creation* (platform plumbing — resource loaders, extension binding, tool filtering, env detection) is genuinely separate from session *interaction* (prompt, steer, abort, resume).
|
|
700
|
+
Pi's own `Agent` class (in `packages/agent/`) already handles the interaction — it owns the transcript, runs the turn loop, executes tools, manages steering queues.
|
|
701
|
+
Our extension's novel value is **child session orchestration within a parent session**: creating child sessions with config derived from the parent, managing concurrency, wiring lifecycle across sessions, and enabling resume.
|
|
702
|
+
We should leverage the Pi session for interaction and focus on what's novel.
|
|
698
703
|
|
|
699
|
-
###
|
|
704
|
+
### Target architecture
|
|
700
705
|
|
|
701
|
-
Agent receives three
|
|
706
|
+
Agent receives three collaborators at construction, each ready to go:
|
|
702
707
|
|
|
703
|
-
|
|
|
704
|
-
|
|
|
705
|
-
|
|
|
706
|
-
|
|
|
707
|
-
|
|
|
708
|
+
| Collaborator | Absorbs | Agent tells it |
|
|
709
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- |
|
|
710
|
+
| Session factory | runner + snapshot + prompt + model + maxTurns + isolated + thinkingLevel + parentSession + getRunConfig (9 fields) | "create me a configured child session" |
|
|
711
|
+
| WorktreeIsolation | worktrees + isolation + worktreeState (3 fields) | `setup()`, `cleanup(description)` |
|
|
712
|
+
| AgentLifecycleObserver | (already exists, 0 new fields) | `onStarted`, `onSessionCreated`, `onRunFinished`, `onCompacted` |
|
|
708
713
|
|
|
709
|
-
|
|
714
|
+
After the session factory creates a session, Agent owns it directly — prompt, steer, abort, resume are Agent's verbs, not a collaborator's.
|
|
715
|
+
The shared `ConcreteAgentRunner` becomes a factory that produces per-agent session factories.
|
|
716
|
+
The "runner" concept dissolves.
|
|
710
717
|
|
|
711
|
-
|
|
712
|
-
2. Call `this.runner.run()` (has the runner).
|
|
713
|
-
3. Handle session creation internally: set `execution`, flush pending steers, attach record-observer.
|
|
714
|
-
4. Notify lifecycle observer (started, session created, completed, compacted).
|
|
715
|
-
5. Clean up worktree on completion or error.
|
|
716
|
-
6. Transition status.
|
|
718
|
+
`AgentInit` shrinks from ~20 to ~10 fields:
|
|
717
719
|
|
|
718
|
-
|
|
720
|
+
- 4 identity (`id`, `type`, `description`, `invocation`)
|
|
721
|
+
- 2 status (`status`, `startedAt` — for tests/restore)
|
|
722
|
+
- 3 collaborators (`sessionFactory`, `worktree`, `observer`)
|
|
723
|
+
- 1 wiring (`signal`)
|
|
719
724
|
|
|
720
|
-
|
|
721
|
-
- Decides when to run (immediate or queue) and calls `agent.run()`.
|
|
722
|
-
- Provides high-level actions: abort, list, cleanup.
|
|
723
|
-
- Does *not* own the runner, worktrees, or any run-orchestration logic.
|
|
725
|
+
Agent's `run()` becomes coordination, not assembly:
|
|
724
726
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
+
```text
|
|
728
|
+
mark running → notify observer → wire signal
|
|
729
|
+
→ tell worktree to setup
|
|
730
|
+
→ tell session factory to create session
|
|
731
|
+
→ own the session: flush steers, subscribe observers, prompt, track turns
|
|
732
|
+
→ on completion: tell worktree to cleanup, transition status, notify observer
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
Agent's `resume()` is trivially Agent's work — it already has the session:
|
|
736
|
+
|
|
737
|
+
```text
|
|
738
|
+
reset status → re-subscribe observer → prompt the existing session → transition status
|
|
739
|
+
```
|
|
727
740
|
|
|
728
|
-
|
|
729
|
-
Agent handles session creation internally during `run()` and notifies external observers via the lifecycle observer pattern.
|
|
741
|
+
### What we can commit to
|
|
730
742
|
|
|
731
|
-
The
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
743
|
+
1. **The runner is not a collaborator — it's Agent's core behavior conflated with a session factory.**
|
|
744
|
+
The shared `ConcreteAgentRunner` becomes a factory.
|
|
745
|
+
Each agent receives a per-agent session factory with config already bound.
|
|
746
|
+
Once the session exists, Agent interacts with it directly.
|
|
735
747
|
|
|
736
|
-
|
|
737
|
-
|
|
748
|
+
2. **WorktreeIsolation is a genuine collaborator.**
|
|
749
|
+
Created by the factory (AgentManager) only when `isolation === "worktree"`.
|
|
750
|
+
Agent tells it `setup()` and `cleanup()` instead of managing worktree internals.
|
|
751
|
+
The null check (`this.worktree?.setup()`) replaces the mode check (`this._isolation !== "worktree"`).
|
|
738
752
|
|
|
739
|
-
|
|
753
|
+
3. **AgentLifecycleObserver is already a well-designed collaborator.**
|
|
754
|
+
No changes needed — Agent tells it about lifecycle events.
|
|
740
755
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
| ~~`AgentRecord` is anemic — no behavior, manager reaches in 37×~~ | B: Oversized | 5 | 3 | ✅ |
|
|
744
|
-
| Agent cannot run itself — manager orchestrates 10 external touches | C: Coupling | 5 | 3 | 15 |
|
|
745
|
-
| Scheduling tangled into `AgentManager` (3 fields, 3 methods) | A: Coupling | 4 | 2 | 12 |
|
|
746
|
-
| ~~`startAgent` uses `.then()`/`.catch()` instead of async/await~~ | C: Callbacks | 3 | 2 | ✅ |
|
|
747
|
-
| ~~`onSessionCreated` callback flows through 3 layers~~ | C: Callbacks | 3 | 2 | subsumed |
|
|
748
|
-
| `resume()` duplicates observer subscribe/unsubscribe pattern | A: Redundant | 2 | 1 | 8 |
|
|
749
|
-
| `exec`/`registry` relay-only deps on `AgentManager` | C: Coupling | 2 | 1 | 6 |
|
|
756
|
+
4. **AgentInit must shrink dramatically.**
|
|
757
|
+
~20 optional fields → ~10, with clear grouping: identity + collaborators + wiring.
|
|
750
758
|
|
|
751
|
-
###
|
|
759
|
+
### Resolved investigations
|
|
752
760
|
|
|
753
|
-
|
|
754
|
-
Move per-agent behavior from `AgentManager` into the agent:
|
|
761
|
+
All five investigations have been resolved by examining Pi's `AgentSession` SDK interface (source: `@earendil-works/pi-coding-agent` + Pi's `packages/agent/src/agent.ts`).
|
|
755
762
|
|
|
756
|
-
1. `
|
|
757
|
-
2. `Agent.queueSteer(message)` / `Agent.flushPendingSteers(session)` — moves pending steers from manager map to per-agent array.
|
|
758
|
-
3. `Agent.setupWorktree(worktrees, isolation)` — moves worktree creation into the agent.
|
|
763
|
+
#### 1. `AgentSession` SDK interface — resolved
|
|
759
764
|
|
|
760
|
-
|
|
761
|
-
- Smell: B (anemic domain model) + C (manager reaching into records)
|
|
762
|
-
- Outcome: `AgentManager` delegates via Tell-Don't-Ask; per-agent state lives on the agent
|
|
765
|
+
AgentSession provides everything Agent needs for direct session interaction:
|
|
763
766
|
|
|
764
|
-
|
|
767
|
+
| What Agent needs | AgentSession provides |
|
|
768
|
+
| ------------------------- | ------------------------------------------------------------------------------------------ |
|
|
769
|
+
| Prompt (initial + resume) | `session.prompt(text)` — works for both; calling it again on an existing session IS resume |
|
|
770
|
+
| Steer | `session.steer(text)` |
|
|
771
|
+
| Abort | `session.abort()` — async, waits for idle |
|
|
772
|
+
| Subscribe to events | `session.subscribe(listener)` — turn_end, message_end, tool_execution_end, compaction_end |
|
|
773
|
+
| Read messages | `session.messages` |
|
|
774
|
+
| Get session file | `session.sessionManager.getSessionFile()` |
|
|
775
|
+
| Dispose | `session.dispose()` |
|
|
765
776
|
|
|
766
|
-
|
|
767
|
-
`
|
|
768
|
-
|
|
769
|
-
Worktree setup was hoisted to callers (`spawn`, `drainQueue`) to preserve the synchronous-throw contract.
|
|
777
|
+
Key finding: `session.prompt(text)` handles both initial run and resume — our current `resumeAgent()` already just calls this.
|
|
778
|
+
The core Pi `Agent` (accessible via `session.agent`) owns the transcript, turn loop, tool execution, and steering/follow-up queues.
|
|
779
|
+
Our Agent should call `session.prompt()` directly and subscribe to events for turn-limit enforcement.
|
|
770
780
|
|
|
771
|
-
|
|
772
|
-
- Target: `src/lifecycle/agent-manager.ts`, `src/lifecycle/agent.ts`
|
|
773
|
-
- Smell: C (raw promise callbacks)
|
|
774
|
-
- Outcome: zero `.then()`/`.catch()` in `agent-manager.ts`; `RunHandle` deleted; Agent owns run lifecycle
|
|
781
|
+
#### 2. Session factory boundary — resolved
|
|
775
782
|
|
|
776
|
-
|
|
783
|
+
The factory encapsulates everything *before* Agent starts using the session.
|
|
784
|
+
The seam is clean: factory produces a ready-to-use `AgentSession`, Agent operates it.
|
|
777
785
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
786
|
+
```text
|
|
787
|
+
Factory creates (platform plumbing):
|
|
788
|
+
detect env → assemble config → create resource loader → reload
|
|
789
|
+
→ create session manager → new session
|
|
790
|
+
→ createAgentSession() → bindExtensions() → filter tools (recursion guard)
|
|
791
|
+
→ register with permission bridge
|
|
792
|
+
→ return { session, outputFile, cleanup }
|
|
793
|
+
|
|
794
|
+
Agent takes over (novel orchestration):
|
|
795
|
+
→ subscribe for turn tracking (maxTurns + graceTurns)
|
|
796
|
+
→ session.prompt(text)
|
|
797
|
+
→ collect response from session.messages
|
|
798
|
+
→ session.steer() / session.abort() for turn limits
|
|
799
|
+
→ call cleanup() when done
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
Factory input: per-agent config (snapshot, prompt, model, maxTurns, isolated, thinkingLevel, parentSession) bound at construction, plus per-call `cwd` from worktree.
|
|
803
|
+
Factory output: `{ session: AgentSession, outputFile?: string, cleanup: () => void }`.
|
|
804
|
+
|
|
805
|
+
#### 3. Turn-limit enforcement — Agent's job via session subscription
|
|
806
|
+
|
|
807
|
+
Agent subscribes to session events and enforces turn limits — this is novel orchestration that Pi's Agent doesn't provide:
|
|
808
|
+
|
|
809
|
+
```typescript
|
|
810
|
+
session.subscribe((event) => {
|
|
811
|
+
if (event.type === "turn_end") {
|
|
812
|
+
turnCount++;
|
|
813
|
+
if (turnCount >= maxTurns) session.steer("wrap up");
|
|
814
|
+
if (turnCount >= maxTurns + graceTurns) session.abort();
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
```
|
|
781
818
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
- Outcome: `AgentManager` loses 2 fields; `AgentManagerOptions` shrinks from 7 to 5 fields; runner is self-contained
|
|
819
|
+
This uses `session.subscribe()`, `session.steer()`, and `session.abort()` directly.
|
|
820
|
+
No runner involvement needed.
|
|
785
821
|
|
|
786
|
-
|
|
822
|
+
#### 4. Response collection — Agent's job, simplified
|
|
787
823
|
|
|
788
|
-
Agent
|
|
789
|
-
|
|
790
|
-
`
|
|
791
|
-
`startAgent` is deleted from `AgentManager`.
|
|
792
|
-
The `onSessionCreated` callback is removed from `AgentSpawnConfig` — replaced by `AgentLifecycleObserver` passed at construction.
|
|
793
|
-
`SpawnArgs` is deleted — Agent has its config from construction.
|
|
794
|
-
The queue is simplified from `{ id, args }[]` to `string[]` (agent IDs only).
|
|
824
|
+
Agent collects the response directly from `session.messages` after `prompt()` completes.
|
|
825
|
+
The existing `getLastAssistantText()` helper (which reads `session.messages`) already works as a fallback.
|
|
826
|
+
The streaming `collectResponseText()` subscriber can move onto Agent for real-time text collection during the run.
|
|
795
827
|
|
|
796
|
-
|
|
828
|
+
#### 5. Permission bridge — factory-internal
|
|
797
829
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
830
|
+
The bridge calls (`registerChildSession` / `unregisterChildSession`) bracket `bindExtensions()` inside the factory.
|
|
831
|
+
Since the factory owns `createAgentSession()` and `bindExtensions()`, both bridge calls become factory-internal.
|
|
832
|
+
The factory returns a `cleanup()` function that Agent calls on completion; `cleanup()` handles `unregisterChildSession()` along with any other teardown.
|
|
833
|
+
Agent never sees or imports the permission bridge.
|
|
834
|
+
This naturally resolves the original Phase 16 dependency-inversion concern.
|
|
802
835
|
|
|
803
|
-
###
|
|
836
|
+
### Steps
|
|
804
837
|
|
|
805
|
-
|
|
806
|
-
The queue stores agent IDs — not `SpawnArgs`.
|
|
807
|
-
Drain calls `agent.run()` directly — no worktree setup, no args threading.
|
|
808
|
-
`SettingsManager` talks to the queue directly — `notifyConcurrencyChanged()` is eliminated from `AgentManager`.
|
|
838
|
+
#### Step 1: Extract `WorktreeIsolation` collaborator — [#256]
|
|
809
839
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
840
|
+
Create a collaborator that owns the worktree lifecycle: setup, path access, and cleanup.
|
|
841
|
+
Agent receives `worktree?: WorktreeIsolation` instead of `_worktrees` + `_isolation` + managing `worktreeState` internally.
|
|
842
|
+
The null check (`this.worktree?.setup()`) replaces the mode check (`this._isolation !== "worktree"`).
|
|
843
|
+
AgentManager creates the collaborator only when `isolation === "worktree"` and passes it to Agent ready to go.
|
|
814
844
|
|
|
815
|
-
|
|
845
|
+
- Target: new `src/lifecycle/worktree-isolation.ts`, `src/lifecycle/agent.ts`, `src/lifecycle/agent-manager.ts`
|
|
846
|
+
- Smell: C (Ask-Don't-Tell — Agent checks `_isolation !== "worktree"` and orchestrates `_worktrees.create()` + `worktreeState.performCleanup()` instead of telling a collaborator)
|
|
847
|
+
- Outcome: Agent loses `_worktrees`, `_isolation` fields + `setupWorktree()` method; `completeRun()`/`failRun()` simplify from 4-line null-check blocks to `this.worktree?.cleanup()`; AgentInit loses 2 fields
|
|
816
848
|
|
|
817
|
-
|
|
818
|
-
`Agent.resume(prompt, signal)` manages its own observer subscription lifecycle using the same internal wiring as `run()`.
|
|
819
|
-
`AgentManager.resume()` becomes a one-liner delegation to `agent.resume(prompt, signal)` — no manual `subscribeRecordObserver` / try-finally.
|
|
849
|
+
#### Step 2: Extract `ChildSessionFactory` from runner — [#257]
|
|
820
850
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
851
|
+
Define the factory interface and extract session creation logic from `runAgent()` into a factory class.
|
|
852
|
+
The factory is per-agent: constructed by AgentManager with config (snapshot, prompt, model, maxTurns, isolated, thinkingLevel, parentSession, getRunConfig) already bound.
|
|
853
|
+
The shared `ConcreteAgentRunner` gains a `createFactory(config)` method that produces per-agent factories.
|
|
854
|
+
`runAgent()` delegates to the factory internally during this step (lift-and-shift — Agent is not changed yet).
|
|
855
|
+
Permission bridge calls (`registerChildSession` / `unregisterChildSession`) move inside the factory.
|
|
856
|
+
|
|
857
|
+
```typescript
|
|
858
|
+
interface ChildSessionFactory {
|
|
859
|
+
create(cwd?: string): Promise<ChildSessionResult>;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
interface ChildSessionResult {
|
|
863
|
+
session: AgentSession;
|
|
864
|
+
outputFile?: string;
|
|
865
|
+
cleanup: () => void;
|
|
866
|
+
}
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
- Target: new `src/lifecycle/child-session-factory.ts`, `src/lifecycle/agent-runner.ts`
|
|
870
|
+
- Smell: B (conflated concerns — `runAgent()` mixes session creation with session interaction)
|
|
871
|
+
- Outcome: session creation is independently testable; `permission-bridge.ts` imports move from runner to factory; factory interface is narrow (one method)
|
|
872
|
+
|
|
873
|
+
#### Step 3: Agent owns session lifecycle — run + resume via factory — [#258]
|
|
874
|
+
|
|
875
|
+
The central step: Agent's `run()` calls `this.factory.create()` to get a session, then interacts with it directly.
|
|
876
|
+
Agent absorbs turn-limit enforcement (subscribe to `turn_end`, steer/abort on limits), response collection (read `session.messages` after prompt), and abort forwarding (wire parent signal to `session.abort()`).
|
|
877
|
+
Agent's `resume()` calls `session.prompt()` directly — the session already exists from the initial run.
|
|
878
|
+
`AgentInit` shrinks: loses `_runner`, `_snapshot`, `_prompt`, `_model`, `_maxTurns`, `_isolated`, `_thinkingLevel`, `_parentSession`, `_getRunConfig` (9 fields); gains `factory` (1 field).
|
|
879
|
+
Combined with Step 1, AgentInit goes from ~20 to ~10 fields.
|
|
880
|
+
|
|
881
|
+
- Depends on: Step 1 (worktree is a collaborator), Step 2 (factory exists)
|
|
882
|
+
- Target: `src/lifecycle/agent.ts`, `src/lifecycle/agent-manager.ts`, `src/tools/foreground-runner.ts`, `src/tools/background-spawner.ts`
|
|
883
|
+
- Smell: C (Agent assembles 9 raw fields into a runner call instead of telling a collaborator) + B (runner conflates creation and interaction)
|
|
884
|
+
- Outcome: Agent owns session interaction; `run()` is coordination not assembly; `resume()` is trivially `session.prompt()`; AgentInit has ~10 fields
|
|
885
|
+
|
|
886
|
+
#### Step 4: Dissolve runner concept — [#259]
|
|
887
|
+
|
|
888
|
+
Delete `AgentRunner` interface, `ConcreteAgentRunner` class, `runAgent()` function, `resumeAgent()` function.
|
|
889
|
+
The shared service that creates per-agent factories gets a clean interface (e.g., `SessionFactoryProvider`).
|
|
890
|
+
Clean up dead types: `RunOptions`, `RunResult`, `ResumeOptions` — replaced by the factory interface and direct session interaction.
|
|
891
|
+
Retain `getAgentConversation()` (used by conversation viewer) and `normalizeMaxTurns()` (used by spawn-config).
|
|
892
|
+
|
|
893
|
+
- Depends on: Step 3
|
|
894
|
+
- Target: `src/lifecycle/agent-runner.ts`, `src/lifecycle/agent.ts`, `src/index.ts`
|
|
895
|
+
- Smell: A (dead code after runner dissolution)
|
|
896
|
+
- Outcome: `agent-runner.ts` shrinks from 467 to ~50 lines (retained helpers only) or is deleted with helpers relocated; the "runner" concept is gone from the architecture
|
|
825
897
|
|
|
826
898
|
### Step dependency diagram
|
|
827
899
|
|
|
828
900
|
```mermaid
|
|
829
901
|
flowchart LR
|
|
830
|
-
S1["Step 1<br/>
|
|
831
|
-
S2["Step 2<br/>
|
|
832
|
-
S3["Step 3<br/>
|
|
833
|
-
S4["Step 4<br/>
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
S1 --> S2
|
|
838
|
-
S2 --> S4
|
|
902
|
+
S1["Step 1<br/>WorktreeIsolation"]
|
|
903
|
+
S2["Step 2<br/>ChildSessionFactory"]
|
|
904
|
+
S3["Step 3<br/>Agent owns session"]
|
|
905
|
+
S4["Step 4<br/>Dissolve runner"]
|
|
906
|
+
|
|
907
|
+
S1 --> S3
|
|
908
|
+
S2 --> S3
|
|
839
909
|
S3 --> S4
|
|
840
|
-
S4 --> S5
|
|
841
|
-
S4 --> S6
|
|
842
910
|
```
|
|
843
911
|
|
|
844
912
|
### Tracks
|
|
845
913
|
|
|
846
|
-
1. **Track A — Foundation** (
|
|
847
|
-
|
|
848
|
-
2. **Track B —
|
|
849
|
-
|
|
850
|
-
Step 6 depends on Step 4.
|
|
851
|
-
3. **Track C — Scheduling** (Step 5): ConcurrencyQueue extraction.
|
|
852
|
-
Depends on Step 4 (queue drains via `agent.run()`).
|
|
914
|
+
1. **Track A — Foundation** (Steps 1, 2): Extract collaborators.
|
|
915
|
+
Independent of each other — can proceed in parallel.
|
|
916
|
+
2. **Track B — Integration** (Steps 3, 4): Agent uses collaborators, runner dissolves.
|
|
917
|
+
Sequential; depends on Track A completing.
|
|
853
918
|
|
|
854
|
-
|
|
919
|
+
### Relationship to the original Phase 16 plan
|
|
855
920
|
|
|
856
|
-
Phase 16
|
|
857
|
-
|
|
921
|
+
The original Phase 16 ("invert dependencies") targeted permission-bridge removal, `extensions: false` removal, and `isolated` dissolution.
|
|
922
|
+
The permission-bridge concern is resolved by Step 2 — the factory handles registration internally, and Agent never imports the bridge.
|
|
923
|
+
The `extensions`/`isolated` concerns are secondary and may move to a later phase once the collaborator architecture is in place.
|
|
858
924
|
|
|
859
|
-
|
|
860
|
-
Key changes:
|
|
925
|
+
### Fallow health snapshot (2026-05-28)
|
|
861
926
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
927
|
+
| Metric | Value |
|
|
928
|
+
| ---------------------- | ------------------------------------------------------------------- |
|
|
929
|
+
| Health score | 78/100 (B) — deductions: hotspots -10, unit size -10, coupling -2.5 |
|
|
930
|
+
| Dead code | 0 files, 0 exports |
|
|
931
|
+
| Production duplication | 11 lines (1 internal clone in `agent-config-editor.ts`) |
|
|
932
|
+
| Test duplication | 42 clone groups, 661 lines (3.1%) |
|
|
933
|
+
| Hotspot #1 | `index.ts` — 70.0, accelerating (128 commits) |
|
|
934
|
+
| Refactoring targets | 0 |
|
|
867
935
|
|
|
868
936
|
## Improvement roadmap (Phase 17 — extract UI)
|
|
869
937
|
|
|
@@ -877,25 +945,25 @@ Phases 1–5, 7–14 are complete.
|
|
|
877
945
|
Phase 6 (UI extraction to a separate package) is deferred.
|
|
878
946
|
Detailed records are preserved in per-phase history files:
|
|
879
947
|
|
|
880
|
-
| Phase
|
|
881
|
-
|
|
|
882
|
-
| 1
|
|
883
|
-
| 2
|
|
884
|
-
| 3
|
|
885
|
-
| 4
|
|
886
|
-
| 5
|
|
887
|
-
| 6
|
|
888
|
-
| 7
|
|
889
|
-
| 8
|
|
890
|
-
| 9
|
|
891
|
-
| 10
|
|
892
|
-
| 11
|
|
893
|
-
| 12
|
|
894
|
-
| 13
|
|
895
|
-
| 14
|
|
896
|
-
| 15
|
|
897
|
-
| 16
|
|
898
|
-
| 17
|
|
948
|
+
| Phase | Title | Status | History |
|
|
949
|
+
| ----- | --------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------ |
|
|
950
|
+
| 1 | Export SubagentsService API boundary | Complete | [phase-1-api-boundary.md](history/phase-1-api-boundary.md) |
|
|
951
|
+
| 2 | Remove scheduling subsystem | Complete | [phase-2-remove-scheduling.md](history/phase-2-remove-scheduling.md) |
|
|
952
|
+
| 3 | Remove group-join, RPC; replace output-file | Complete | [phase-3-remove-rpc-groupjoin.md](history/phase-3-remove-rpc-groupjoin.md) |
|
|
953
|
+
| 4 | Implement and publish SubagentsService | Complete | [phase-4-implement-service.md](history/phase-4-implement-service.md) |
|
|
954
|
+
| 5 | Decompose index.ts | Complete | [phase-5-decompose-index.md](history/phase-5-decompose-index.md) |
|
|
955
|
+
| 6 | Extract UI to separate package | Deferred → Phase 17 | — |
|
|
956
|
+
| 7 | Encapsulation and dependency narrowing | Complete | [phase-7-encapsulation.md](history/phase-7-encapsulation.md) |
|
|
957
|
+
| 8 | Testability, display extraction, menu decomposition | Complete | [phase-8-testability.md](history/phase-8-testability.md) |
|
|
958
|
+
| 9 | Observation consolidation, ctx elimination | Complete | [phase-9-observation-ctx.md](history/phase-9-observation-ctx.md) |
|
|
959
|
+
| 10 | Domain organization, bag decomposition, complexity | Complete | [phase-10-structural-decomposition.md](history/phase-10-structural-decomposition.md) |
|
|
960
|
+
| 11 | Closure factories to classes | Complete | [phase-11-closure-to-class.md](history/phase-11-closure-to-class.md) |
|
|
961
|
+
| 12 | Complexity reduction and test fixture extraction | Complete | [phase-12-complexity-test-fixtures.md](history/phase-12-complexity-test-fixtures.md) |
|
|
962
|
+
| 13 | Remaining structural smells | Complete | [phase-13-remaining-smells.md](history/phase-13-remaining-smells.md) |
|
|
963
|
+
| 14 | Strip policy from core | Complete | [phase-14-strip-policy.md](history/phase-14-strip-policy.md) |
|
|
964
|
+
| 15 | Domain model evolution | Complete | [phase-15-domain-model-evolution.md](history/phase-15-domain-model-evolution.md) |
|
|
965
|
+
| 16 | Agent collaborator architecture | Planned | — |
|
|
966
|
+
| 17 | Extract UI to separate package | Planned | — |
|
|
899
967
|
|
|
900
968
|
### Structural refactoring issues
|
|
901
969
|
|
|
@@ -915,6 +983,7 @@ Detailed records are preserved in per-phase history files:
|
|
|
915
983
|
| Phase 13 | #214, #215, #216, #217, #218, #219 | Closure-to-class, buildParentContext, startAgent decomp, overwrite guard, settings SDK, test duplication |
|
|
916
984
|
| Phase 14 | #237, #238, #239, #242 | Remove disallowed_tools, remove extensions filtering, collapse filterActiveTools, rename Agent to subagent |
|
|
917
985
|
| Phase 15 | #227, #228, #231, #229, #230, #232 | Agent domain model, async startAgent, runner self-contained, Agent.run(), ConcurrencyQueue, Agent.resume() |
|
|
986
|
+
| Phase 16 | #256, #257, #258, #259 | WorktreeIsolation, ChildSessionFactory, Agent owns session, dissolve runner |
|
|
918
987
|
|
|
919
988
|
The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
|
|
920
989
|
|
|
@@ -947,9 +1016,8 @@ The upstream test suite is run periodically as a regression canary for the agent
|
|
|
947
1016
|
[#217]: https://github.com/gotgenes/pi-packages/issues/217
|
|
948
1017
|
[#218]: https://github.com/gotgenes/pi-packages/issues/218
|
|
949
1018
|
[#219]: https://github.com/gotgenes/pi-packages/issues/219
|
|
950
|
-
[#227]: https://github.com/gotgenes/pi-packages/issues/227
|
|
951
|
-
[#228]: https://github.com/gotgenes/pi-packages/issues/228
|
|
952
|
-
[#229]: https://github.com/gotgenes/pi-packages/issues/229
|
|
953
|
-
[#230]: https://github.com/gotgenes/pi-packages/issues/230
|
|
954
1019
|
[#231]: https://github.com/gotgenes/pi-packages/issues/231
|
|
955
|
-
[#
|
|
1020
|
+
[#256]: https://github.com/gotgenes/pi-packages/issues/256
|
|
1021
|
+
[#257]: https://github.com/gotgenes/pi-packages/issues/257
|
|
1022
|
+
[#258]: https://github.com/gotgenes/pi-packages/issues/258
|
|
1023
|
+
[#259]: https://github.com/gotgenes/pi-packages/issues/259
|