@gotgenes/pi-subagents 11.2.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 +7 -0
- package/docs/architecture/architecture.md +226 -160
- package/docs/architecture/history/phase-15-domain-model-evolution.md +73 -0
- package/docs/plans/0256-extract-worktree-isolation.md +256 -0
- package/docs/retro/0232-agent-resume-internal-observer-lifecycle.md +64 -0
- package/docs/retro/0256-extract-worktree-isolation.md +45 -0
- package/package.json +1 -1
- package/src/lifecycle/agent-manager.ts +5 -2
- package/src/lifecycle/agent.ts +18 -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,13 @@ 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
|
+
|
|
8
15
|
## [11.2.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.1.0...pi-subagents-v11.2.0) (2026-05-28)
|
|
9
16
|
|
|
10
17
|
|
|
@@ -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()
|
|
@@ -126,9 +126,8 @@ classDiagram
|
|
|
126
126
|
+abort(): boolean
|
|
127
127
|
+queueSteer(message)
|
|
128
128
|
+flushPendingSteers(session)
|
|
129
|
-
+
|
|
130
|
-
+
|
|
131
|
-
+failRun(err, worktrees)
|
|
129
|
+
+completeRun(result)
|
|
130
|
+
+failRun(err)
|
|
132
131
|
+wireSignal(signal, onAbort)
|
|
133
132
|
+attachObserver(unsub)
|
|
134
133
|
+releaseListeners()
|
|
@@ -277,7 +276,7 @@ src/
|
|
|
277
276
|
│ ├── execution-state.ts session/output phase state
|
|
278
277
|
│ ├── permission-bridge.ts optional bridge to pi-permission-system registry
|
|
279
278
|
│ ├── worktree.ts git worktree isolation
|
|
280
|
-
│ ├── worktree-
|
|
279
|
+
│ ├── worktree-isolation.ts worktree lifecycle collaborator
|
|
281
280
|
│ └── usage.ts token usage tracking
|
|
282
281
|
│
|
|
283
282
|
├── observation/ progress tracking and notification
|
|
@@ -490,13 +489,13 @@ This is achieved across three phases: Phase 14 (strip policy), Phase 16 (invert
|
|
|
490
489
|
| Metric | Value |
|
|
491
490
|
| -------------------------- | --------------------------------- |
|
|
492
491
|
| Health score | 78/100 (B) |
|
|
493
|
-
| Total LOC |
|
|
492
|
+
| Total LOC | 7,778 (57 files) |
|
|
494
493
|
| Dead code | 0 files, 0 exports |
|
|
495
494
|
| Maintainability index | 90.8 (good) |
|
|
496
495
|
| Avg cyclomatic complexity | 1.4 |
|
|
497
496
|
| P90 cyclomatic complexity | 2 |
|
|
498
497
|
| Production duplication | 11 lines (1 internal clone group) |
|
|
499
|
-
| Test duplication |
|
|
498
|
+
| Test duplication | 42 clone groups, 661 lines |
|
|
500
499
|
| Fallow refactoring targets | 0 |
|
|
501
500
|
|
|
502
501
|
### Dependency bag inventory
|
|
@@ -687,185 +686,252 @@ See [phase-14-strip-policy.md](history/phase-14-strip-policy.md) for details.
|
|
|
687
686
|
[#239]: https://github.com/gotgenes/pi-packages/issues/239
|
|
688
687
|
[#242]: https://github.com/gotgenes/pi-packages/issues/242
|
|
689
688
|
|
|
690
|
-
## Improvement roadmap (Phase
|
|
689
|
+
## Improvement roadmap (Phase 16 — agent collaborator architecture)
|
|
691
690
|
|
|
692
|
-
Phase
|
|
691
|
+
Phase 16 gives Agent proper collaborators so it can do its work without accumulating raw materials.
|
|
693
692
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
`
|
|
697
|
-
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.
|
|
698
697
|
|
|
699
|
-
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.
|
|
700
703
|
|
|
701
|
-
###
|
|
704
|
+
### Target architecture
|
|
702
705
|
|
|
703
|
-
Agent receives three
|
|
706
|
+
Agent receives three collaborators at construction, each ready to go:
|
|
704
707
|
|
|
705
|
-
|
|
|
706
|
-
|
|
|
707
|
-
|
|
|
708
|
-
|
|
|
709
|
-
|
|
|
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` |
|
|
710
713
|
|
|
711
|
-
|
|
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.
|
|
712
717
|
|
|
713
|
-
|
|
714
|
-
2. Call `this.runner.run()` (has the runner).
|
|
715
|
-
3. Handle session creation internally: set `execution`, flush pending steers, attach record-observer.
|
|
716
|
-
4. Notify lifecycle observer (started, session created, completed, compacted).
|
|
717
|
-
5. Clean up worktree on completion or error.
|
|
718
|
-
6. Transition status.
|
|
718
|
+
`AgentInit` shrinks from ~20 to ~10 fields:
|
|
719
719
|
|
|
720
|
-
|
|
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`)
|
|
721
724
|
|
|
722
|
-
|
|
723
|
-
- Decides when to run (immediate or queue) and calls `agent.run()`.
|
|
724
|
-
- Provides high-level actions: abort, list, cleanup.
|
|
725
|
-
- Does *not* own the runner, worktrees, or any run-orchestration logic.
|
|
725
|
+
Agent's `run()` becomes coordination, not assembly:
|
|
726
726
|
|
|
727
|
-
|
|
728
|
-
|
|
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
|
+
```
|
|
729
740
|
|
|
730
|
-
|
|
731
|
-
Agent handles session creation internally during `run()` and notifies external observers via the lifecycle observer pattern.
|
|
741
|
+
### What we can commit to
|
|
732
742
|
|
|
733
|
-
The
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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.
|
|
737
747
|
|
|
738
|
-
|
|
739
|
-
|
|
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"`).
|
|
740
752
|
|
|
741
|
-
|
|
753
|
+
3. **AgentLifecycleObserver is already a well-designed collaborator.**
|
|
754
|
+
No changes needed — Agent tells it about lifecycle events.
|
|
742
755
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
| ~~`AgentRecord` is anemic — no behavior, manager reaches in 37×~~ | B: Oversized | 5 | 3 | ✅ |
|
|
746
|
-
| ~~Agent cannot run itself — manager orchestrates 10 external touches~~ | C: Coupling | 5 | 3 | ✅ |
|
|
747
|
-
| ~~Scheduling tangled into `AgentManager` (3 fields, 3 methods)~~ | A: Coupling | 4 | 2 | ✅ |
|
|
748
|
-
| ~~`startAgent` uses `.then()`/`.catch()` instead of async/await~~ | C: Callbacks | 3 | 2 | ✅ |
|
|
749
|
-
| ~~`onSessionCreated` callback flows through 3 layers~~ | C: Callbacks | 3 | 2 | subsumed |
|
|
750
|
-
| ~~`resume()` duplicates observer subscribe/unsubscribe pattern~~ | A: Redundant | 2 | 1 | ✅ |
|
|
751
|
-
| ~~`exec`/`registry` relay-only deps on `AgentManager`~~ | C: Coupling | 2 | 1 | ✅ |
|
|
756
|
+
4. **AgentInit must shrink dramatically.**
|
|
757
|
+
~20 optional fields → ~10, with clear grouping: identity + collaborators + wiring.
|
|
752
758
|
|
|
753
|
-
###
|
|
759
|
+
### Resolved investigations
|
|
754
760
|
|
|
755
|
-
|
|
756
|
-
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`).
|
|
757
762
|
|
|
758
|
-
1. `
|
|
759
|
-
2. `Agent.queueSteer(message)` / `Agent.flushPendingSteers(session)` — moves pending steers from manager map to per-agent array.
|
|
760
|
-
3. `Agent.setupWorktree(worktrees, isolation)` — moves worktree creation into the agent.
|
|
763
|
+
#### 1. `AgentSession` SDK interface — resolved
|
|
761
764
|
|
|
762
|
-
|
|
763
|
-
- Smell: B (anemic domain model) + C (manager reaching into records)
|
|
764
|
-
- 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:
|
|
765
766
|
|
|
766
|
-
|
|
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()` |
|
|
767
776
|
|
|
768
|
-
|
|
769
|
-
`
|
|
770
|
-
|
|
771
|
-
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.
|
|
772
780
|
|
|
773
|
-
|
|
774
|
-
- Target: `src/lifecycle/agent-manager.ts`, `src/lifecycle/agent.ts`
|
|
775
|
-
- Smell: C (raw promise callbacks)
|
|
776
|
-
- Outcome: zero `.then()`/`.catch()` in `agent-manager.ts`; `RunHandle` deleted; Agent owns run lifecycle
|
|
781
|
+
#### 2. Session factory boundary — resolved
|
|
777
782
|
|
|
778
|
-
|
|
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.
|
|
779
785
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
+
```
|
|
783
818
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
- 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.
|
|
787
821
|
|
|
788
|
-
|
|
822
|
+
#### 4. Response collection — Agent's job, simplified
|
|
789
823
|
|
|
790
|
-
Agent
|
|
791
|
-
|
|
792
|
-
`
|
|
793
|
-
`startAgent` is deleted from `AgentManager`.
|
|
794
|
-
The `onSessionCreated` callback is removed from `AgentSpawnConfig` — replaced by `AgentLifecycleObserver` passed at construction.
|
|
795
|
-
`SpawnArgs` is deleted — Agent has its config from construction.
|
|
796
|
-
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.
|
|
797
827
|
|
|
798
|
-
|
|
828
|
+
#### 5. Permission bridge — factory-internal
|
|
799
829
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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.
|
|
804
835
|
|
|
805
|
-
###
|
|
836
|
+
### Steps
|
|
806
837
|
|
|
807
|
-
|
|
808
|
-
The queue stores agent IDs — not `SpawnArgs`.
|
|
809
|
-
Drain calls `agent.run()` directly — no worktree setup, no args threading.
|
|
810
|
-
`SettingsManager` talks to the queue directly — `notifyConcurrencyChanged()` is eliminated from `AgentManager`.
|
|
838
|
+
#### Step 1: Extract `WorktreeIsolation` collaborator — [#256]
|
|
811
839
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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.
|
|
816
844
|
|
|
817
|
-
|
|
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
|
|
818
848
|
|
|
819
|
-
|
|
820
|
-
`Agent.resume(prompt, signal)` manages its own observer subscription lifecycle using the same internal wiring as `run()`.
|
|
821
|
-
`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]
|
|
822
850
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
|
827
897
|
|
|
828
898
|
### Step dependency diagram
|
|
829
899
|
|
|
830
900
|
```mermaid
|
|
831
901
|
flowchart LR
|
|
832
|
-
S1["Step 1<br/>
|
|
833
|
-
S2["Step 2<br/>
|
|
834
|
-
S3["Step 3<br/>
|
|
835
|
-
S4["Step 4<br/>
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
S1 --> S2
|
|
840
|
-
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
|
|
841
909
|
S3 --> S4
|
|
842
|
-
S4 --> S5
|
|
843
|
-
S4 --> S6
|
|
844
910
|
```
|
|
845
911
|
|
|
846
912
|
### Tracks
|
|
847
913
|
|
|
848
|
-
1. **Track A — Foundation** (
|
|
849
|
-
|
|
850
|
-
2. **Track B —
|
|
851
|
-
|
|
852
|
-
Step 6 depends on Step 4.
|
|
853
|
-
3. **Track C — Scheduling** (Step 5): ConcurrencyQueue extraction.
|
|
854
|
-
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.
|
|
855
918
|
|
|
856
|
-
|
|
919
|
+
### Relationship to the original Phase 16 plan
|
|
857
920
|
|
|
858
|
-
Phase 16
|
|
859
|
-
|
|
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.
|
|
860
924
|
|
|
861
|
-
|
|
862
|
-
Key changes:
|
|
925
|
+
### Fallow health snapshot (2026-05-28)
|
|
863
926
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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 |
|
|
869
935
|
|
|
870
936
|
## Improvement roadmap (Phase 17 — extract UI)
|
|
871
937
|
|
|
@@ -879,25 +945,25 @@ Phases 1–5, 7–14 are complete.
|
|
|
879
945
|
Phase 6 (UI extraction to a separate package) is deferred.
|
|
880
946
|
Detailed records are preserved in per-phase history files:
|
|
881
947
|
|
|
882
|
-
| Phase
|
|
883
|
-
|
|
|
884
|
-
| 1
|
|
885
|
-
| 2
|
|
886
|
-
| 3
|
|
887
|
-
| 4
|
|
888
|
-
| 5
|
|
889
|
-
| 6
|
|
890
|
-
| 7
|
|
891
|
-
| 8
|
|
892
|
-
| 9
|
|
893
|
-
| 10
|
|
894
|
-
| 11
|
|
895
|
-
| 12
|
|
896
|
-
| 13
|
|
897
|
-
| 14
|
|
898
|
-
| 15
|
|
899
|
-
| 16
|
|
900
|
-
| 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 | — |
|
|
901
967
|
|
|
902
968
|
### Structural refactoring issues
|
|
903
969
|
|
|
@@ -917,6 +983,7 @@ Detailed records are preserved in per-phase history files:
|
|
|
917
983
|
| Phase 13 | #214, #215, #216, #217, #218, #219 | Closure-to-class, buildParentContext, startAgent decomp, overwrite guard, settings SDK, test duplication |
|
|
918
984
|
| Phase 14 | #237, #238, #239, #242 | Remove disallowed_tools, remove extensions filtering, collapse filterActiveTools, rename Agent to subagent |
|
|
919
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 |
|
|
920
987
|
|
|
921
988
|
The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
|
|
922
989
|
|
|
@@ -949,9 +1016,8 @@ The upstream test suite is run periodically as a regression canary for the agent
|
|
|
949
1016
|
[#217]: https://github.com/gotgenes/pi-packages/issues/217
|
|
950
1017
|
[#218]: https://github.com/gotgenes/pi-packages/issues/218
|
|
951
1018
|
[#219]: https://github.com/gotgenes/pi-packages/issues/219
|
|
952
|
-
[#227]: https://github.com/gotgenes/pi-packages/issues/227
|
|
953
|
-
[#228]: https://github.com/gotgenes/pi-packages/issues/228
|
|
954
|
-
[#229]: https://github.com/gotgenes/pi-packages/issues/229
|
|
955
|
-
[#230]: https://github.com/gotgenes/pi-packages/issues/230
|
|
956
1019
|
[#231]: https://github.com/gotgenes/pi-packages/issues/231
|
|
957
|
-
[#
|
|
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
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Phase 15: Domain model evolution
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
Phase 15 evolved `Agent` from a passive state machine (`AgentRecord`) into an object that **owns its entire execution lifecycle**.
|
|
6
|
+
Before Phase 15, `AgentManager` orchestrated everything: calling the runner, handling session creation, wiring observers, and cleaning up worktrees — reaching into Agent 10+ times across `spawn()` and `startAgent()`.
|
|
7
|
+
After Phase 15, Agent is born complete with all dependencies and configuration, owns `run()` and `resume()`, and manages its own observer and worktree lifecycle.
|
|
8
|
+
|
|
9
|
+
All six steps are closed: [#227], [#228], [#231], [#229], [#230], [#232].
|
|
10
|
+
|
|
11
|
+
## Key changes
|
|
12
|
+
|
|
13
|
+
- `AgentRecord` renamed to `Agent` with full behavioral surface.
|
|
14
|
+
- `Agent.run()` encapsulates the entire execution lifecycle: worktree setup, runner invocation, session-creation handling, observer wiring, worktree cleanup, and status transitions.
|
|
15
|
+
- `Agent.resume()` manages its own observer subscription lifecycle.
|
|
16
|
+
- `startAgent` deleted from `AgentManager` — replaced by `agent.run()`.
|
|
17
|
+
- `ConcurrencyQueue` extracted from `AgentManager` — scheduling is independently testable.
|
|
18
|
+
- `SpawnArgs` deleted — the queue stores agent IDs, not config objects.
|
|
19
|
+
- `onSessionCreated` callback replaced by `AgentLifecycleObserver` passed at construction.
|
|
20
|
+
- `exec` and `registry` relay-only dependencies moved from `AgentManager` to `ConcreteAgentRunner`.
|
|
21
|
+
- `AgentManagerOptions` shrunk from 7 to 5 fields.
|
|
22
|
+
|
|
23
|
+
## Steps
|
|
24
|
+
|
|
25
|
+
### Step 1: Evolve AgentRecord into Agent with behavior — [#227]
|
|
26
|
+
|
|
27
|
+
Renamed `AgentRecord` → `Agent`.
|
|
28
|
+
Moved per-agent behavior from `AgentManager` into the agent: `abort()`, `queueSteer()` / `flushPendingSteers()`, `setupWorktree()`.
|
|
29
|
+
|
|
30
|
+
### Step 2: Convert startAgent to async/await — [#228]
|
|
31
|
+
|
|
32
|
+
Converted `startAgent` to `async` with `try/catch` and dissolved `RunHandle` into `Agent` methods.
|
|
33
|
+
Agent gained run lifecycle methods: `completeRun`, `failRun`, `wireSignal`, `attachObserver`, `releaseListeners`.
|
|
34
|
+
|
|
35
|
+
### Step 3: Push exec/registry relay deps to runner construction — [#231]
|
|
36
|
+
|
|
37
|
+
`exec` and `registry` moved from `AgentManager` to `ConcreteAgentRunner` via `RunnerDeps`.
|
|
38
|
+
`RunContext` shrunk from 4 to 2 per-call fields.
|
|
39
|
+
|
|
40
|
+
### Step 4: Agent born complete — Agent.run() absorbs startAgent — [#229]
|
|
41
|
+
|
|
42
|
+
Agent receives `runner`, `worktrees`, and a lifecycle observer at construction.
|
|
43
|
+
`Agent.run()` encapsulates the entire execution lifecycle.
|
|
44
|
+
`startAgent`, `SpawnArgs`, `onSessionCreated` callback deleted.
|
|
45
|
+
|
|
46
|
+
### Step 5: Extract ConcurrencyQueue from AgentManager — [#230]
|
|
47
|
+
|
|
48
|
+
Extracted `queue[]`, `runningBackground`, `_getMaxConcurrent`, `drainQueue()`, `finalizeBackgroundRun()` into `ConcurrencyQueue`.
|
|
49
|
+
`AgentManager` lost 3 fields and 3 methods (~40 lines).
|
|
50
|
+
|
|
51
|
+
### Step 6: Agent.resume() with internal observer lifecycle — [#232]
|
|
52
|
+
|
|
53
|
+
`Agent.resume(prompt, signal)` manages its own observer subscription lifecycle.
|
|
54
|
+
`AgentManager.resume()` became a one-liner delegation.
|
|
55
|
+
|
|
56
|
+
## Findings summary
|
|
57
|
+
|
|
58
|
+
| Finding | Category | Status |
|
|
59
|
+
| ------------------------------------------------------------------ | ------------ | --------------------- |
|
|
60
|
+
| `AgentRecord` anemic — no behavior, manager reaches in 37× | B: Oversized | ✅ Resolved |
|
|
61
|
+
| Agent cannot run itself — manager orchestrates 10 external touches | C: Coupling | ✅ Resolved |
|
|
62
|
+
| Scheduling tangled into `AgentManager` (3 fields, 3 methods) | A: Coupling | ✅ Resolved |
|
|
63
|
+
| `startAgent` uses `.then()`/`.catch()` instead of async/await | C: Callbacks | ✅ Resolved |
|
|
64
|
+
| `onSessionCreated` callback flows through 3 layers | C: Callbacks | ✅ Subsumed by Step 4 |
|
|
65
|
+
| `resume()` duplicates observer subscribe/unsubscribe pattern | A: Redundant | ✅ Resolved |
|
|
66
|
+
| `exec`/`registry` relay-only deps on `AgentManager` | C: Coupling | ✅ Resolved |
|
|
67
|
+
|
|
68
|
+
[#227]: https://github.com/gotgenes/pi-packages/issues/227
|
|
69
|
+
[#228]: https://github.com/gotgenes/pi-packages/issues/228
|
|
70
|
+
[#229]: https://github.com/gotgenes/pi-packages/issues/229
|
|
71
|
+
[#230]: https://github.com/gotgenes/pi-packages/issues/230
|
|
72
|
+
[#231]: https://github.com/gotgenes/pi-packages/issues/231
|
|
73
|
+
[#232]: https://github.com/gotgenes/pi-packages/issues/232
|