@gotgenes/pi-subagents 10.2.0 → 10.2.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 +13 -0
- package/docs/architecture/architecture.md +111 -57
- package/docs/plans/0231-push-exec-registry-to-runner.md +245 -0
- package/docs/retro/0228-async-start-agent-dissolve-run-handle.md +38 -0
- package/docs/retro/0231-push-exec-registry-to-runner.md +40 -0
- package/package.json +1 -1
- package/src/index.ts +17 -15
- package/src/lifecycle/agent-manager.ts +1 -10
- package/src/lifecycle/agent-runner.ts +30 -21
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ 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
|
+
## [10.2.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v10.2.0...pi-subagents-v10.2.1) (2026-05-27)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* **pi-subagents:** renumber Phase 15 steps to match execution order ([598bb65](https://github.com/gotgenes/pi-packages/commit/598bb653ac8b63756e8c00dfcf19d3167e2dbc37))
|
|
14
|
+
* **pi-subagents:** revise Phase 15 roadmap for Agent-born-complete vision ([e04583e](https://github.com/gotgenes/pi-packages/commit/e04583e75bfc1314674a6f3181762a26733fb830))
|
|
15
|
+
* plan push exec/registry relay deps to runner construction ([#231](https://github.com/gotgenes/pi-packages/issues/231)) ([646b4d5](https://github.com/gotgenes/pi-packages/commit/646b4d5085e0f7d36a397b43b3b46e0537c3141f))
|
|
16
|
+
* **retro:** add planning stage notes for issue [#231](https://github.com/gotgenes/pi-packages/issues/231) ([dc0daee](https://github.com/gotgenes/pi-packages/commit/dc0daee634c17cf2a40336e27f551bfa2ce0e249))
|
|
17
|
+
* **retro:** add retro notes for issue [#228](https://github.com/gotgenes/pi-packages/issues/228) ([d5b563b](https://github.com/gotgenes/pi-packages/commit/d5b563b6484cbd6a89cd7e9e87ebd431aed128fc))
|
|
18
|
+
* **retro:** add TDD stage notes for issue [#231](https://github.com/gotgenes/pi-packages/issues/231) ([28094ae](https://github.com/gotgenes/pi-packages/commit/28094ae812141ea1c93a22be50ed29d31b7a979a))
|
|
19
|
+
* update architecture for runner self-contained ([#231](https://github.com/gotgenes/pi-packages/issues/231)) ([80dd339](https://github.com/gotgenes/pi-packages/commit/80dd339d7dee9b312b52af2b74756c5748619a49))
|
|
20
|
+
|
|
8
21
|
## [10.2.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v10.1.0...pi-subagents-v10.2.0) (2026-05-27)
|
|
9
22
|
|
|
10
23
|
|
|
@@ -599,23 +599,21 @@ export interface ParentSessionInfo {
|
|
|
599
599
|
|
|
600
600
|
`AgentSpawnConfig` now carries `parentSession?: ParentSessionInfo` instead of three flat optional fields.
|
|
601
601
|
|
|
602
|
-
#### RunOptions (12 fields → extract RunContext) — done ([#169][169])
|
|
602
|
+
#### RunOptions (12 fields → extract RunContext) — done ([#169][169]), updated by [#231]
|
|
603
603
|
|
|
604
|
-
|
|
605
|
-
`
|
|
604
|
+
`RunContext` was extracted and nested as `RunOptions.context` in #169.
|
|
605
|
+
Issue #231 moved the two static dependencies (`exec`, `registry`) to `RunnerDeps` on `ConcreteAgentRunner`, leaving `RunContext` with only per-call fields:
|
|
606
606
|
|
|
607
607
|
```typescript
|
|
608
|
-
/**
|
|
608
|
+
/** Per-call execution context — fields that vary per spawn. */
|
|
609
609
|
export interface RunContext {
|
|
610
|
-
exec: ShellExec;
|
|
611
|
-
registry: AgentConfigLookup;
|
|
612
610
|
cwd?: string;
|
|
613
611
|
parentSession?: ParentSessionInfo;
|
|
614
612
|
}
|
|
615
613
|
```
|
|
616
614
|
|
|
617
615
|
The remaining `RunOptions` fields (`model`, `maxTurns`, `signal`, `isolated`, `thinkingLevel`, `defaultMaxTurns`, `graceTurns`, `onSessionCreated`) are genuine execution parameters.
|
|
618
|
-
`RunOptions` now has 9 fields: 1 nested `context: RunContext` plus 8 flat execution fields.
|
|
616
|
+
`RunOptions` now has 9 fields: 1 nested `context: RunContext` (2 per-call fields) plus 8 flat execution fields.
|
|
619
617
|
|
|
620
618
|
#### SessionConfig (11 fields → extract ToolFilterConfig) — done ([#168][168])
|
|
621
619
|
|
|
@@ -686,25 +684,66 @@ See [phase-14-strip-policy.md](history/phase-14-strip-policy.md) for details.
|
|
|
686
684
|
|
|
687
685
|
## Improvement roadmap (Phase 15 — domain model evolution)
|
|
688
686
|
|
|
689
|
-
Phase 15
|
|
690
|
-
|
|
691
|
-
`AgentManager`
|
|
692
|
-
|
|
693
|
-
`
|
|
687
|
+
Phase 15 evolves `Agent` from a passive state machine into an object that **owns its entire execution lifecycle**.
|
|
688
|
+
|
|
689
|
+
Steps 1–2 (complete) moved per-agent behavior from `AgentManager` onto `Agent`: abort, steer buffering, worktree setup, and run lifecycle methods (`completeRun`, `failRun`).
|
|
690
|
+
However, Agent still cannot *run itself*.
|
|
691
|
+
`AgentManager.startAgent()` orchestrates the entire execution: calling the runner, handling session creation, wiring observers, and cleaning up worktrees.
|
|
692
|
+
The manager reaches into Agent 10 times across `spawn()` + `startAgent()` — writing to `notification`, `execution`, and `promise` after construction, passing its own `worktrees` and `runner` as method arguments, and threading `onSessionCreated` callbacks through three layers.
|
|
693
|
+
|
|
694
|
+
The remaining steps address this by making **Agent born complete**: constructed with all dependencies and configuration, owning its entire execution lifecycle.
|
|
695
|
+
|
|
696
|
+
### Architecture target
|
|
697
|
+
|
|
698
|
+
Agent receives three concerns at construction:
|
|
699
|
+
|
|
700
|
+
| Concern | Fields | Lifetime |
|
|
701
|
+
| ----------- | ----------------------------------------------------------------------------- | ------------------------- |
|
|
702
|
+
| Identity | id, type, description, invocation | Immutable |
|
|
703
|
+
| Run config | snapshot, prompt, model, isolation, maxTurns, thinking, signal, parentSession | Immutable per-run |
|
|
704
|
+
| Shared deps | runner, worktrees | Shared service references |
|
|
705
|
+
|
|
706
|
+
`Agent.run()` encapsulates the full execution lifecycle:
|
|
707
|
+
|
|
708
|
+
1. Set up worktree internally (knows its own isolation mode, has worktrees).
|
|
709
|
+
2. Call `this.runner.run()` (has the runner).
|
|
710
|
+
3. Handle session creation internally: set `execution`, flush pending steers, attach record-observer.
|
|
711
|
+
4. Notify lifecycle observer (started, session created, completed, compacted).
|
|
712
|
+
5. Clean up worktree on completion or error.
|
|
713
|
+
6. Transition status.
|
|
714
|
+
|
|
715
|
+
`AgentManager` becomes a collection manager + concurrency controller:
|
|
716
|
+
|
|
717
|
+
- Creates complete Agent objects, stores them in the map.
|
|
718
|
+
- Decides when to run (immediate or queue) and calls `agent.run()`.
|
|
719
|
+
- Provides high-level actions: abort, list, cleanup.
|
|
720
|
+
- Does *not* own the runner, worktrees, or any run-orchestration logic.
|
|
721
|
+
|
|
722
|
+
The queue stores agent IDs, not `SpawnArgs`.
|
|
723
|
+
When capacity opens, the manager looks up the agent and calls `agent.run()` — the agent already has everything.
|
|
724
|
+
|
|
725
|
+
The `onSessionCreated` callback that currently threads through `AgentSpawnConfig` → `startAgent` → `RunOptions` → runner disappears.
|
|
726
|
+
Agent handles session creation internally during `run()` and notifies external observers via the lifecycle observer pattern.
|
|
727
|
+
|
|
728
|
+
The synchronous-throw contract for worktree failure (introduced in Step 2's hoist) is replaced by a uniform async error surface.
|
|
729
|
+
Worktree failures inside `agent.run()` propagate through the promise.
|
|
730
|
+
For background agents, errors surface via `get_subagent_result` and appear in `/agents`.
|
|
731
|
+
For foreground agents, `spawnAndWait` awaits the promise naturally.
|
|
694
732
|
|
|
695
733
|
The scheduling concern (queue, concurrency counter, drain) is tangled into `AgentManager` alongside collection management and run orchestration.
|
|
696
734
|
`notifyConcurrencyChanged()` is a scheduling method exposed as a public API so settings can poke the queue — a cross-concern leak.
|
|
697
735
|
|
|
698
736
|
### Findings summary
|
|
699
737
|
|
|
700
|
-
| Finding
|
|
701
|
-
|
|
|
702
|
-
|
|
|
703
|
-
|
|
|
704
|
-
|
|
|
705
|
-
| `
|
|
706
|
-
| `
|
|
707
|
-
| `
|
|
738
|
+
| Finding | Category | Impact | Risk | Priority |
|
|
739
|
+
| ------------------------------------------------------------------ | ------------ | ------ | ---- | -------- |
|
|
740
|
+
| ~~`AgentRecord` is anemic — no behavior, manager reaches in 37×~~ | B: Oversized | 5 | 3 | ✅ |
|
|
741
|
+
| Agent cannot run itself — manager orchestrates 10 external touches | C: Coupling | 5 | 3 | 15 |
|
|
742
|
+
| Scheduling tangled into `AgentManager` (3 fields, 3 methods) | A: Coupling | 4 | 2 | 12 |
|
|
743
|
+
| ~~`startAgent` uses `.then()`/`.catch()` instead of async/await~~ | C: Callbacks | 3 | 2 | ✅ |
|
|
744
|
+
| ~~`onSessionCreated` callback flows through 3 layers~~ | C: Callbacks | 3 | 2 | subsumed |
|
|
745
|
+
| `resume()` duplicates observer subscribe/unsubscribe pattern | A: Redundant | 2 | 1 | 8 |
|
|
746
|
+
| `exec`/`registry` relay-only deps on `AgentManager` | C: Coupling | 2 | 1 | 6 |
|
|
708
747
|
|
|
709
748
|
### Step 1: Evolve AgentRecord into Agent with behavior — [#227] ✅ Complete
|
|
710
749
|
|
|
@@ -731,43 +770,54 @@ Worktree setup was hoisted to callers (`spawn`, `drainQueue`) to preserve the sy
|
|
|
731
770
|
- Smell: C (raw promise callbacks)
|
|
732
771
|
- Outcome: zero `.then()`/`.catch()` in `agent-manager.ts`; `RunHandle` deleted; Agent owns run lifecycle
|
|
733
772
|
|
|
734
|
-
### Step 3:
|
|
773
|
+
### Step 3: Push exec/registry relay deps to runner construction — [#231] ✅
|
|
735
774
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
775
|
+
`exec` and `registry` moved from `AgentManager` to `ConcreteAgentRunner` via a new `RunnerDeps` interface.
|
|
776
|
+
`RunContext` shrunk from 4 to 2 per-call fields (`cwd`, `parentSession`).
|
|
777
|
+
`AgentManagerOptions` shrunk from 7 to 5 fields.
|
|
739
778
|
|
|
740
|
-
- Target: `src/lifecycle/agent-manager.ts`, `src/
|
|
741
|
-
- Smell: C (
|
|
742
|
-
- Outcome: `
|
|
779
|
+
- Target: `src/lifecycle/agent-manager.ts`, `src/lifecycle/agent-runner.ts`, `src/index.ts`
|
|
780
|
+
- Smell: C (relay-only dependencies)
|
|
781
|
+
- Outcome: `AgentManager` loses 2 fields; `AgentManagerOptions` shrinks from 7 to 5 fields; runner is self-contained
|
|
782
|
+
|
|
783
|
+
### Step 4: Agent born complete — Agent.run() absorbs startAgent — [#229]
|
|
784
|
+
|
|
785
|
+
Agent receives `runner`, `worktrees`, and a lifecycle observer at construction.
|
|
786
|
+
Agent creates its own `NotificationState` from `parentSession.toolCallId` — no external write.
|
|
787
|
+
`Agent.run()` encapsulates the entire execution lifecycle: worktree setup, runner invocation, session-creation handling, observer wiring, worktree cleanup, and status transitions.
|
|
788
|
+
`startAgent` is deleted from `AgentManager`.
|
|
789
|
+
The `onSessionCreated` callback is removed from `AgentSpawnConfig` — Agent handles session creation internally and notifies via the lifecycle observer.
|
|
790
|
+
`SpawnArgs` is deleted — Agent has its config from construction.
|
|
791
|
+
|
|
792
|
+
`AgentManager.spawn()` becomes: create complete Agent, put in map, call `agent.run()` or queue the agent ID.
|
|
793
|
+
|
|
794
|
+
- Depends on: #228, #231
|
|
795
|
+
- Target: `src/lifecycle/agent.ts`, `src/lifecycle/agent-manager.ts`, `src/tools/background-spawner.ts`, `src/tools/foreground-runner.ts`
|
|
796
|
+
- Smell: C (manager orchestrates 10 external touches on Agent) + C (callback flowing through 3 layers)
|
|
797
|
+
- Outcome: Agent owns its entire execution lifecycle; `startAgent`, `SpawnArgs`, `onSessionCreated` callback deleted; zero post-construction writes from `AgentManager`
|
|
743
798
|
|
|
744
|
-
### Step
|
|
799
|
+
### Step 5: Extract ConcurrencyQueue from AgentManager — [#230]
|
|
745
800
|
|
|
746
801
|
Extract `queue[]`, `runningBackground`, `_getMaxConcurrent`, `drainQueue()`, `finalizeBackgroundRun()` into a `ConcurrencyQueue` class.
|
|
802
|
+
The queue stores agent IDs — not `SpawnArgs`.
|
|
803
|
+
Drain calls `agent.run()` directly — no worktree setup, no args threading.
|
|
747
804
|
`SettingsManager` talks to the queue directly — `notifyConcurrencyChanged()` is eliminated from `AgentManager`.
|
|
748
805
|
|
|
806
|
+
- Depends on: #229
|
|
749
807
|
- Target: new `src/lifecycle/concurrency-queue.ts`, `src/lifecycle/agent-manager.ts`, `src/index.ts`
|
|
750
808
|
- Smell: A (tangled concerns) + C (cross-concern leak via `notifyConcurrencyChanged`)
|
|
751
|
-
- Outcome: `AgentManager` loses 3 fields, 3 methods (~40 lines); scheduling is independently testable
|
|
752
|
-
|
|
753
|
-
### Step 5: Push exec/registry relay deps to runner construction — [#231]
|
|
754
|
-
|
|
755
|
-
`AgentManager` receives `exec` and `registry` in its constructor but only relays them to `runner.run()` via `context`.
|
|
756
|
-
Move them to `ConcreteAgentRunner` construction.
|
|
757
|
-
|
|
758
|
-
- Target: `src/lifecycle/agent-manager.ts`, `src/lifecycle/agent-runner.ts`, `src/index.ts`
|
|
759
|
-
- Smell: C (relay-only dependencies)
|
|
760
|
-
- Outcome: `AgentManager` loses 2 fields; `AgentManagerOptions` shrinks from 7 to 5 fields
|
|
809
|
+
- Outcome: `AgentManager` loses 3 fields, 3 methods (~40 lines); scheduling is independently testable; queue interface is trivial (agent has everything)
|
|
761
810
|
|
|
762
|
-
### Step 6:
|
|
811
|
+
### Step 6: Agent.resume() with internal observer lifecycle — [#232]
|
|
763
812
|
|
|
764
|
-
|
|
765
|
-
|
|
813
|
+
Agent has the runner from construction.
|
|
814
|
+
`Agent.resume(prompt, signal)` manages its own observer subscription lifecycle using the same internal wiring as `run()`.
|
|
815
|
+
`AgentManager.resume()` becomes a one-liner delegation to `agent.resume(prompt, signal)` — no manual `subscribeRecordObserver` / try-finally.
|
|
766
816
|
|
|
767
|
-
- Depends on: #
|
|
768
|
-
- Target: `src/lifecycle/agent-manager.ts`
|
|
817
|
+
- Depends on: #229
|
|
818
|
+
- Target: `src/lifecycle/agent.ts`, `src/lifecycle/agent-manager.ts`
|
|
769
819
|
- Smell: A (duplicated observer subscribe/unsubscribe pattern)
|
|
770
|
-
- Outcome:
|
|
820
|
+
- Outcome: `AgentManager.resume()` is a 4-line delegation; observer lifecycle is Agent-internal
|
|
771
821
|
|
|
772
822
|
### Step dependency diagram
|
|
773
823
|
|
|
@@ -775,28 +825,32 @@ The agent manages its own observer subscription lifecycle using the same methods
|
|
|
775
825
|
flowchart LR
|
|
776
826
|
S1["Step 1<br/>Agent with behavior"]
|
|
777
827
|
S2["Step 2<br/>async startAgent"]
|
|
778
|
-
S3["Step 3<br/>
|
|
779
|
-
S4["Step 4<br/>
|
|
780
|
-
S5["Step 5<br/>
|
|
781
|
-
S6["Step 6<br/>resume
|
|
828
|
+
S3["Step 3<br/>runner self-contained"]
|
|
829
|
+
S4["Step 4<br/>Agent.run()"]
|
|
830
|
+
S5["Step 5<br/>ConcurrencyQueue"]
|
|
831
|
+
S6["Step 6<br/>Agent.resume()"]
|
|
782
832
|
|
|
783
833
|
S1 --> S2
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
S4
|
|
834
|
+
S2 --> S4
|
|
835
|
+
S3 --> S4
|
|
836
|
+
S4 --> S5
|
|
837
|
+
S4 --> S6
|
|
788
838
|
```
|
|
789
839
|
|
|
790
840
|
### Tracks
|
|
791
841
|
|
|
792
|
-
1. **Track A —
|
|
793
|
-
|
|
794
|
-
2. **Track B —
|
|
842
|
+
1. **Track A — Foundation** (Step 3): Runner becomes self-contained.
|
|
843
|
+
No dependencies on other Phase 15 steps; can start immediately.
|
|
844
|
+
2. **Track B — Agent lifecycle** (Steps 4, 6): Agent born complete, owns run + resume.
|
|
845
|
+
Step 4 depends on Track A + Step 2.
|
|
846
|
+
Step 6 depends on Step 4.
|
|
847
|
+
3. **Track C — Scheduling** (Step 5): ConcurrencyQueue extraction.
|
|
848
|
+
Depends on Step 4 (queue drains via `agent.run()`).
|
|
795
849
|
|
|
796
850
|
## Improvement roadmap (Phase 16 — invert dependencies)
|
|
797
851
|
|
|
798
852
|
Phase 16 completes the architectural inversion by removing the outbound permission bridge and the `extensions: false` / `isolated` concepts.
|
|
799
|
-
It depends on Phase 15's observer
|
|
853
|
+
It depends on Phase 15's lifecycle observer (#229) as the replacement mechanism.
|
|
800
854
|
|
|
801
855
|
Phase 16 is scoped but not yet broken into steps.
|
|
802
856
|
Key changes:
|
|
@@ -856,7 +910,7 @@ Detailed records are preserved in per-phase history files:
|
|
|
856
910
|
| Phase 12 | #205, #206, #207, #208 | renderWidgetLines, showAgentDetail, widget update, shared test fixtures |
|
|
857
911
|
| Phase 13 | #214, #215, #216, #217, #218, #219 | Closure-to-class, buildParentContext, startAgent decomp, overwrite guard, settings SDK, test duplication |
|
|
858
912
|
| Phase 14 | #237, #238, #239, #242 | Remove disallowed_tools, remove extensions filtering, collapse filterActiveTools, rename Agent to subagent |
|
|
859
|
-
| Phase 15 | #227, #228, #
|
|
913
|
+
| Phase 15 | #227, #228, #231, #229, #230, #232 | Agent domain model, async startAgent, runner self-contained, Agent.run(), ConcurrencyQueue, Agent.resume() |
|
|
860
914
|
|
|
861
915
|
The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
|
|
862
916
|
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 231
|
|
3
|
+
issue_title: "Push exec/registry relay deps to runner construction (Phase 15, Step 3)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Push exec/registry relay deps to runner construction
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`AgentManager` receives `exec` and `registry` in its constructor but never uses them directly.
|
|
11
|
+
They are stored as fields solely to relay them into `runner.run()` via the `RunContext` parameter.
|
|
12
|
+
This makes `AgentManager` wider than necessary and prevents the runner from being self-contained — a prerequisite for #229 (Agent.run() absorbs startAgent).
|
|
13
|
+
|
|
14
|
+
## Goals
|
|
15
|
+
|
|
16
|
+
- Move `exec` and `registry` from `AgentManager` construction to `ConcreteAgentRunner` construction.
|
|
17
|
+
- Remove `exec` and `registry` from `AgentManagerOptions` (7 → 5 fields).
|
|
18
|
+
- Remove `exec` and `registry` from `RunContext` (4 → 2 fields).
|
|
19
|
+
- Group runner-owned dependencies in a `RunnerDeps` interface: `{ io, exec, registry }`.
|
|
20
|
+
- Replace `runAgent()`'s `io: RunnerIO` parameter with `deps: RunnerDeps`.
|
|
21
|
+
|
|
22
|
+
## Non-Goals
|
|
23
|
+
|
|
24
|
+
- Dissolving `RunContext` entirely — it shrinks to `{ cwd?, parentSession? }`, which is still a coherent per-call grouping.
|
|
25
|
+
Issue #229 will likely dissolve it when `Agent.run()` calls the runner directly.
|
|
26
|
+
- Changing the `AgentRunner` interface's `run()` signature — callers continue to pass `RunOptions` with `context: RunContext`.
|
|
27
|
+
`ConcreteAgentRunner` merges its stored deps before calling `runAgent()`.
|
|
28
|
+
- Touching `resume()` or `resumeAgent()` — they don't use `exec` or `registry`.
|
|
29
|
+
|
|
30
|
+
## Background
|
|
31
|
+
|
|
32
|
+
Issue #169 extracted `RunContext` from `RunOptions` to group the 4 parent-context fields: `exec`, `registry`, `cwd`, `parentSession`.
|
|
33
|
+
The doc comment describes them as "parent environment and identity" fields.
|
|
34
|
+
However, 2 of the 4 fields (`exec`, `registry`) are static — identical across every `run()` call — while the other 2 (`cwd`, `parentSession`) vary per spawn.
|
|
35
|
+
The static pair are relay-only dependencies on `AgentManager`: stored at construction, never read, only forwarded.
|
|
36
|
+
|
|
37
|
+
From the code-design skill, this is a **parameter relay** smell: intermediaries (`AgentManager`) carry fields they never use, only to thread them to the endpoint (`runAgent`).
|
|
38
|
+
The fix: put them on the object the endpoint owns — the runner.
|
|
39
|
+
|
|
40
|
+
### Key references
|
|
41
|
+
|
|
42
|
+
- `src/lifecycle/agent-manager.ts` — stores `exec` and `registry`, relays them at lines 193–194.
|
|
43
|
+
- `src/lifecycle/agent-runner.ts` — `RunContext` interface (line 125), `ConcreteAgentRunner` class (line 189), `runAgent()` free function (line 236).
|
|
44
|
+
- `src/index.ts` — constructs both `ConcreteAgentRunner` and `AgentManager` (lines 148–157).
|
|
45
|
+
- Phase 15 roadmap in `docs/architecture/architecture.md` § Step 3.
|
|
46
|
+
|
|
47
|
+
## Design Overview
|
|
48
|
+
|
|
49
|
+
### RunnerDeps — grouping runner-owned dependencies
|
|
50
|
+
|
|
51
|
+
A new `RunnerDeps` interface groups the three dependencies that the runner owns:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
export interface RunnerDeps {
|
|
55
|
+
io: RunnerIO;
|
|
56
|
+
exec: ShellExec;
|
|
57
|
+
registry: AgentConfigLookup;
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`ConcreteAgentRunner` takes `RunnerDeps` at construction:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
export class ConcreteAgentRunner implements AgentRunner {
|
|
65
|
+
constructor(private readonly deps: RunnerDeps) {}
|
|
66
|
+
|
|
67
|
+
run(snapshot, type, prompt, options) {
|
|
68
|
+
return runAgent(snapshot, type, prompt, options, this.deps);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`runAgent()` changes its last parameter from `io: RunnerIO` to `deps: RunnerDeps`:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
export async function runAgent(
|
|
77
|
+
snapshot: ParentSnapshot,
|
|
78
|
+
type: SubagentType,
|
|
79
|
+
prompt: string,
|
|
80
|
+
options: RunOptions,
|
|
81
|
+
deps: RunnerDeps,
|
|
82
|
+
): Promise<RunResult> {
|
|
83
|
+
const effectiveCwd = options.context?.cwd ?? snapshot.cwd;
|
|
84
|
+
const env = await deps.io.detectEnv(deps.exec, effectiveCwd);
|
|
85
|
+
// ...
|
|
86
|
+
const cfg = assembleSessionConfig(type, ..., deps.registry, deps.io.assemblerIO);
|
|
87
|
+
// ...
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### RunContext shrinks
|
|
92
|
+
|
|
93
|
+
`RunContext` loses `exec` and `registry`:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
export interface RunContext {
|
|
97
|
+
/** Override working directory (e.g. for worktree isolation). */
|
|
98
|
+
cwd?: string;
|
|
99
|
+
/** Parent session identity (file path + session ID). */
|
|
100
|
+
parentSession?: ParentSessionInfo;
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The `AgentRunner.run()` interface is unchanged — callers still pass `RunOptions` with `context: RunContext`.
|
|
105
|
+
`ConcreteAgentRunner.run()` reads `exec` and `registry` from its own `deps` instead of from `options.context`.
|
|
106
|
+
|
|
107
|
+
### AgentManager loses 2 fields
|
|
108
|
+
|
|
109
|
+
`AgentManagerOptions` removes `exec` and `registry`.
|
|
110
|
+
`AgentManager` removes the corresponding private fields and the `this.exec` / `this.registry` relay in `startAgent()`.
|
|
111
|
+
The `context` object constructed in `startAgent()` shrinks from 4 fields to 2:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
context: {
|
|
115
|
+
cwd: record.worktreeState?.path,
|
|
116
|
+
parentSession: options.parentSession,
|
|
117
|
+
},
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Wiring in index.ts
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const runner = new ConcreteAgentRunner({
|
|
124
|
+
io: runnerIO,
|
|
125
|
+
exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
|
|
126
|
+
registry,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const manager = new AgentManager({
|
|
130
|
+
runner,
|
|
131
|
+
worktrees: new GitWorktreeManager(process.cwd()),
|
|
132
|
+
observer,
|
|
133
|
+
getMaxConcurrent: () => settings.maxConcurrent,
|
|
134
|
+
getRunConfig: () => settings,
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Module-Level Changes
|
|
139
|
+
|
|
140
|
+
### `src/lifecycle/agent-runner.ts`
|
|
141
|
+
|
|
142
|
+
1. Add `RunnerDeps` interface (exported): `{ io: RunnerIO; exec: ShellExec; registry: AgentConfigLookup }`.
|
|
143
|
+
2. Remove `exec` and `registry` from `RunContext`.
|
|
144
|
+
Update doc comment to reflect the 2 remaining per-call fields.
|
|
145
|
+
3. Update `ConcreteAgentRunner` constructor: accept `RunnerDeps` instead of `RunnerIO`.
|
|
146
|
+
4. Update `ConcreteAgentRunner.run()`: pass `this.deps` to `runAgent()`.
|
|
147
|
+
5. Update `runAgent()`: change last parameter from `io: RunnerIO` to `deps: RunnerDeps`.
|
|
148
|
+
Replace `io.` references with `deps.io.`, `options.context.exec` with `deps.exec`, `options.context.registry` with `deps.registry`.
|
|
149
|
+
|
|
150
|
+
### `src/lifecycle/agent-manager.ts`
|
|
151
|
+
|
|
152
|
+
1. Remove `exec: ShellExec` and `registry: AgentTypeRegistry` from `AgentManagerOptions`.
|
|
153
|
+
2. Remove `private readonly exec` and `private readonly registry` fields from `AgentManager`.
|
|
154
|
+
3. Remove assignment of `this.exec` and `this.registry` in the constructor.
|
|
155
|
+
4. Remove `exec: this.exec` and `registry: this.registry` from the `context` object in `startAgent()`.
|
|
156
|
+
5. Remove `ShellExec` and `AgentTypeRegistry` imports (verify no other references first).
|
|
157
|
+
|
|
158
|
+
### `src/index.ts`
|
|
159
|
+
|
|
160
|
+
1. Move `exec` and `registry` from the `AgentManager` constructor call to `ConcreteAgentRunner`:
|
|
161
|
+
`new ConcreteAgentRunner({ io: runnerIO, exec: ..., registry })`.
|
|
162
|
+
2. Remove `exec` and `registry` from the `AgentManager({...})` constructor argument.
|
|
163
|
+
|
|
164
|
+
### `test/lifecycle/agent-runner.test.ts`
|
|
165
|
+
|
|
166
|
+
1. Update all `runAgent(..., io)` calls to `runAgent(..., { io, exec, registry: mockAgentLookup })`.
|
|
167
|
+
2. Remove `exec` and `registry` from `context:` objects in `RunOptions`.
|
|
168
|
+
`context: { exec, registry: mockAgentLookup }` → `context: {}` or `{}`.
|
|
169
|
+
|
|
170
|
+
### `test/lifecycle/agent-runner-extension-tools.test.ts`
|
|
171
|
+
|
|
172
|
+
1. Same pattern as `agent-runner.test.ts`: update `runAgent(..., io)` last param and strip `exec`/`registry` from `context:`.
|
|
173
|
+
|
|
174
|
+
### `test/lifecycle/concrete-agent-runner.test.ts`
|
|
175
|
+
|
|
176
|
+
1. Update `new ConcreteAgentRunner(io)` → `new ConcreteAgentRunner({ io, exec: vi.fn(), registry })`.
|
|
177
|
+
2. Remove `exec` and `registry` from the `context:` in `runner.run()` call options.
|
|
178
|
+
|
|
179
|
+
### `test/lifecycle/agent-manager.test.ts`
|
|
180
|
+
|
|
181
|
+
1. Remove `exec: vi.fn()` and `registry: testRegistry` from `createManager()` factory.
|
|
182
|
+
2. Remove the `testRegistry` construction and `AgentTypeRegistry` import if no other references exist.
|
|
183
|
+
|
|
184
|
+
### `test/helpers/runner-io.ts`
|
|
185
|
+
|
|
186
|
+
1. No structural changes needed — `createRunnerIO()` returns the `RunnerIO` shape, which is unchanged.
|
|
187
|
+
However, add a `createRunnerDeps()` convenience factory that bundles `{ io: createRunnerIO(), exec: vi.fn(), registry: createAgentLookup() }` for runner test files.
|
|
188
|
+
|
|
189
|
+
### `docs/architecture/architecture.md`
|
|
190
|
+
|
|
191
|
+
1. Update the `RunContext` code block in § "RunOptions (12 fields → extract RunContext)" to show only `cwd` and `parentSession`.
|
|
192
|
+
2. Update the field-count description (4 → 2 per-call fields).
|
|
193
|
+
3. Mark Step 3 as complete in the Phase 15 roadmap.
|
|
194
|
+
|
|
195
|
+
## Test Impact Analysis
|
|
196
|
+
|
|
197
|
+
1. No new test surfaces are needed — this is a pure mechanical refactoring (moving constructor parameters).
|
|
198
|
+
The existing runner and manager test suites fully cover the behavior.
|
|
199
|
+
2. No existing tests become redundant — all tests exercise the same interactions, just with deps flowing through a different path.
|
|
200
|
+
3. Existing `agent-manager.test.ts` tests remain as-is in coverage scope.
|
|
201
|
+
They verify `AgentManager` behavior (spawning, queueing, abort, etc.) independent of runner deps.
|
|
202
|
+
4. Existing `agent-runner.test.ts` and `concrete-agent-runner.test.ts` tests remain.
|
|
203
|
+
They verify `runAgent()` and `ConcreteAgentRunner` behavior.
|
|
204
|
+
Call-site patterns change but assertions stay the same.
|
|
205
|
+
|
|
206
|
+
## TDD Order
|
|
207
|
+
|
|
208
|
+
1. **Add `RunnerDeps` interface and update `runAgent()` parameter** — define `RunnerDeps`, change `runAgent()`'s last param from `io` to `deps`, update internal references.
|
|
209
|
+
Update `agent-runner.test.ts` and `agent-runner-extension-tools.test.ts` call sites.
|
|
210
|
+
Commit: `refactor: add RunnerDeps and update runAgent parameter (#231)`
|
|
211
|
+
|
|
212
|
+
2. **Update `ConcreteAgentRunner` to accept `RunnerDeps`** — change constructor from `RunnerIO` to `RunnerDeps`, update `.run()` to pass `this.deps`.
|
|
213
|
+
Update `concrete-agent-runner.test.ts`.
|
|
214
|
+
Add `createRunnerDeps()` helper to `test/helpers/runner-io.ts`.
|
|
215
|
+
Commit: `refactor: ConcreteAgentRunner accepts RunnerDeps (#231)`
|
|
216
|
+
|
|
217
|
+
3. **Remove `exec` and `registry` from `RunContext`** — shrink the interface to 2 fields, update doc comment.
|
|
218
|
+
Strip `exec`/`registry` from `context:` in all runner test call sites.
|
|
219
|
+
Run `pnpm run check` to verify no stale references.
|
|
220
|
+
Commit: `refactor: remove exec and registry from RunContext (#231)`
|
|
221
|
+
|
|
222
|
+
4. **Remove `exec` and `registry` from `AgentManager`** — remove from `AgentManagerOptions`, remove class fields, remove relay in `startAgent()`, clean up imports.
|
|
223
|
+
Update `agent-manager.test.ts` factory.
|
|
224
|
+
Commit: `refactor: remove relay deps from AgentManager (#231)`
|
|
225
|
+
|
|
226
|
+
5. **Update wiring in `index.ts`** — move `exec` and `registry` from `AgentManager` construction to `ConcreteAgentRunner` construction.
|
|
227
|
+
Commit: `refactor: wire exec and registry to ConcreteAgentRunner (#231)`
|
|
228
|
+
|
|
229
|
+
6. **Update architecture docs** — update `RunContext` description and field counts, mark Step 3 complete.
|
|
230
|
+
Commit: `docs: update architecture for runner self-contained (#231)`
|
|
231
|
+
|
|
232
|
+
## Risks and Mitigations
|
|
233
|
+
|
|
234
|
+
1. **Test churn** — ~20 `runAgent()` call sites change their last parameter pattern.
|
|
235
|
+
Mitigation: mechanical find-and-replace; assertions stay identical.
|
|
236
|
+
2. **Step ordering** — Steps 3 and 4 both remove `exec`/`registry` from different types.
|
|
237
|
+
If done in the wrong order, intermediate commits may not type-check.
|
|
238
|
+
Mitigation: Step 1–2 add the new path (`deps`), Step 3 removes from `RunContext` (runner side), Step 4 removes from `AgentManager` (manager side), Step 5 wires them together.
|
|
239
|
+
Each commit is independently valid.
|
|
240
|
+
3. **Import cleanup** — removing `exec`/`registry` from `AgentManager` may leave unused imports (`ShellExec`, `AgentTypeRegistry`).
|
|
241
|
+
Mitigation: grep for other usages before removing; `pnpm run check` catches unused imports.
|
|
242
|
+
|
|
243
|
+
## Open Questions
|
|
244
|
+
|
|
245
|
+
- None — the issue scope is narrow and the design is straightforward.
|
|
@@ -40,3 +40,41 @@ Test count: 986 → 1005.
|
|
|
40
40
|
- Pre-completion reviewer returned WARN for stale `AgentRecord` and `run-handle.ts` references in `architecture.md` class diagram and layout listing.
|
|
41
41
|
These were pre-existing staleness from #227's rename that wasn't fully propagated to Mermaid diagrams.
|
|
42
42
|
Fixed by amending the docs commit.
|
|
43
|
+
|
|
44
|
+
## Stage: Final Retrospective (2026-05-27T21:46:00Z)
|
|
45
|
+
|
|
46
|
+
### Session summary
|
|
47
|
+
|
|
48
|
+
Completed all stages (plan, TDD, ship, retro) in a single session.
|
|
49
|
+
Dissolved `RunHandle` into 6 Agent methods, converted `startAgent` to async/await, released as `pi-subagents-v10.2.0`.
|
|
50
|
+
Test delta: 986 → 1005 (+19).
|
|
51
|
+
|
|
52
|
+
### Observations
|
|
53
|
+
|
|
54
|
+
#### What went well
|
|
55
|
+
|
|
56
|
+
- The user's two redirecting questions during planning ("What's the change that makes this easier?"
|
|
57
|
+
and "Tell me more about RunHandle — is there something that should replace it?") transformed a mechanical "move the class" plan into a "dissolve the abstraction" plan.
|
|
58
|
+
The dissolve approach is architecturally superior and sets up #232 (resume unification) for free.
|
|
59
|
+
- The lift-and-shift decomposition (introduce Agent methods alongside `RunHandle`, then swap and delete) produced 5 independently-green commits.
|
|
60
|
+
The riskiest commit (step 3: delete `RunHandle`, -96/+6 lines) was trivially safe because step 2 had already proven the replacement methods.
|
|
61
|
+
|
|
62
|
+
#### What caused friction (agent side)
|
|
63
|
+
|
|
64
|
+
- `premature-convergence` — The agent planned around the issue's proposed `Agent.createRunHandle()` factory without questioning whether `RunHandle` should exist as a separate class.
|
|
65
|
+
The user had to ask two redirecting questions to push the analysis deeper.
|
|
66
|
+
Impact: plan was rewritten before commit (no wasted implementation), but the user spent two turns guiding analysis the agent should have done proactively.
|
|
67
|
+
- `missing-context` — Plan step 1 (narrow `Promise<string>` to `Promise<void>`) listed only `agent-manager.test.ts` for updates but missed 3 additional test files (`make-agent.test.ts`, `service-adapter.test.ts`, `get-result-tool.test.ts`) that construct `Promise<string>` values.
|
|
68
|
+
The testing skill says "grep for all test files" for type changes — this was not applied during planning.
|
|
69
|
+
Impact: caught by `pnpm run check` in the same step, no rework.
|
|
70
|
+
|
|
71
|
+
#### What caused friction (user side)
|
|
72
|
+
|
|
73
|
+
- No friction observed.
|
|
74
|
+
The user's questioning style (asking "what does it do?
|
|
75
|
+
who needs it?"
|
|
76
|
+
rather than prescribing the solution) was collaborative and effective.
|
|
77
|
+
|
|
78
|
+
### Changes made
|
|
79
|
+
|
|
80
|
+
1. `.pi/prompts/plan-issue.md` — added relocation-dissolution heuristic: when an issue proposes moving a class, list callers and fields touched to check if it should be dissolved into the owner instead.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 231
|
|
3
|
+
issue_title: "Push exec/registry relay deps to runner construction (Phase 15, Step 3)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #231 — Push exec/registry relay deps to runner construction
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-27T21:53:10Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Produced a 6-step TDD plan to move `exec` and `registry` from `AgentManager` to `ConcreteAgentRunner` via a new `RunnerDeps` interface.
|
|
13
|
+
The plan keeps `RunContext` (shrunk to 2 per-call fields) rather than dissolving it — #229 will likely dissolve it when `Agent.run()` calls the runner directly.
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- Confirmed `exec` and `registry` are pure relay deps on `AgentManager` — stored at construction, used only at lines 193–194 to forward into `runner.run()`.
|
|
18
|
+
- Chose `RunnerDeps` bag over separate positional params on `ConcreteAgentRunner` and `runAgent()` — groups all three runner-owned deps (`io`, `exec`, `registry`) in one interface, and `runAgent()` stays at 5 parameters.
|
|
19
|
+
- `AgentManagerOptions.registry` uses the concrete `AgentTypeRegistry` class; `RunContext.registry` uses the narrow `AgentConfigLookup` interface.
|
|
20
|
+
The new `RunnerDeps.registry` uses `AgentConfigLookup` (ISP).
|
|
21
|
+
- Test churn is moderate (~20 `runAgent()` call sites change last param pattern) but mechanical — assertions stay identical.
|
|
22
|
+
- Added a `createRunnerDeps()` test helper to `runner-io.ts` to reduce per-file boilerplate in runner tests.
|
|
23
|
+
|
|
24
|
+
## Stage: Implementation — TDD (2026-05-27T22:05:32Z)
|
|
25
|
+
|
|
26
|
+
### Session summary
|
|
27
|
+
|
|
28
|
+
Implemented the 6-step plan in 4 commits (steps 3–5 merged).
|
|
29
|
+
All 1005 tests pass; no test count change.
|
|
30
|
+
Pre-completion reviewer returned PASS.
|
|
31
|
+
|
|
32
|
+
### Observations
|
|
33
|
+
|
|
34
|
+
- Plan steps 3, 4, and 5 could not be separate commits: removing `exec`/`registry` from `RunContext` (step 3) immediately caused TypeScript excess-property errors in `AgentManager` (step 4) and `index.ts` (step 5).
|
|
35
|
+
Merged all three into one commit.
|
|
36
|
+
The testing skill’s rule “when a TDD step changes an interface that has a single call site, the step must include updating that call site” applies.
|
|
37
|
+
- Shrinking `RunContext` to all-optional fields made pre-existing `as never` casts in `test/helpers/manager-stubs.test.ts` unnecessary (eslint `no-unnecessary-type-assertion`).
|
|
38
|
+
Fixed as a lint cleanup in the doc commit.
|
|
39
|
+
- The `sed`-based bulk replacement for `runAgent(..., io)` → `runAgent(..., { io, exec, registry: mockAgentLookup })` missed one multi-line call site (the `rejects.toThrow` test wrapping the call in `expect()`).
|
|
40
|
+
Caught immediately by the test run.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { AgentTypeRegistry } from "#src/config/agent-types";
|
|
|
24
24
|
import { loadCustomAgents } from "#src/config/custom-agents";
|
|
25
25
|
import { SessionLifecycleHandler, ToolStartHandler } from "#src/handlers/index";
|
|
26
26
|
import { AgentManager, type AgentManagerObserver } from "#src/lifecycle/agent-manager";
|
|
27
|
-
import { ConcreteAgentRunner, type
|
|
27
|
+
import { ConcreteAgentRunner, type RunnerDeps } from "#src/lifecycle/agent-runner";
|
|
28
28
|
import { buildParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
29
29
|
import { GitWorktreeManager } from "#src/lifecycle/worktree";
|
|
30
30
|
import { buildEventData, type NotificationDetails, NotificationManager } from "#src/observation/notification";
|
|
@@ -132,25 +132,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
132
132
|
},
|
|
133
133
|
};
|
|
134
134
|
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
135
|
+
const runnerDeps: RunnerDeps = {
|
|
136
|
+
io: {
|
|
137
|
+
detectEnv,
|
|
138
|
+
getAgentDir,
|
|
139
|
+
createResourceLoader: (opts) => new DefaultResourceLoader(opts),
|
|
140
|
+
deriveSessionDir: deriveSubagentSessionDir,
|
|
141
|
+
createSessionManager: (cwd, dir) => SessionManager.create(cwd, dir),
|
|
142
|
+
createSettingsManager: (cwd, dir) => SdkSettingsManager.create(cwd, dir),
|
|
143
|
+
createSession: (opts) => createAgentSession(opts as any),
|
|
144
|
+
assemblerIO: {
|
|
145
|
+
preloadSkills,
|
|
146
|
+
buildAgentPrompt,
|
|
147
|
+
},
|
|
146
148
|
},
|
|
149
|
+
exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
|
|
150
|
+
registry,
|
|
147
151
|
};
|
|
148
152
|
|
|
149
153
|
const manager = new AgentManager({
|
|
150
|
-
runner: new ConcreteAgentRunner(
|
|
154
|
+
runner: new ConcreteAgentRunner(runnerDeps),
|
|
151
155
|
worktrees: new GitWorktreeManager(process.cwd()),
|
|
152
|
-
exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
|
|
153
|
-
registry,
|
|
154
156
|
observer,
|
|
155
157
|
getMaxConcurrent: () => settings.maxConcurrent,
|
|
156
158
|
getRunConfig: () => settings,
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
10
|
import type { Model } from "@earendil-works/pi-ai";
|
|
11
11
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { AgentTypeRegistry } from "#src/config/agent-types";
|
|
13
12
|
import { debugLog } from "#src/debug";
|
|
14
13
|
import { Agent } from "#src/lifecycle/agent";
|
|
15
14
|
import type { AgentRunner } from "#src/lifecycle/agent-runner";
|
|
@@ -19,7 +18,7 @@ import type { WorktreeManager } from "#src/lifecycle/worktree";
|
|
|
19
18
|
import { NotificationState } from "#src/observation/notification-state";
|
|
20
19
|
import { subscribeAgentObserver } from "#src/observation/record-observer";
|
|
21
20
|
import type { RunConfig } from "#src/runtime";
|
|
22
|
-
import type { AgentInvocation, IsolationMode,
|
|
21
|
+
import type { AgentInvocation, IsolationMode, SubagentType, ThinkingLevel } from "#src/types";
|
|
23
22
|
|
|
24
23
|
export type CompactionInfo = { reason: "manual" | "threshold" | "overflow"; tokensBefore: number };
|
|
25
24
|
|
|
@@ -38,8 +37,6 @@ const DEFAULT_MAX_CONCURRENT = 4;
|
|
|
38
37
|
export interface AgentManagerOptions {
|
|
39
38
|
runner: AgentRunner;
|
|
40
39
|
worktrees: WorktreeManager;
|
|
41
|
-
exec: ShellExec;
|
|
42
|
-
registry: AgentTypeRegistry;
|
|
43
40
|
/** Injected getter for the concurrency limit - owned by SettingsManager. */
|
|
44
41
|
getMaxConcurrent?: () => number;
|
|
45
42
|
getRunConfig?: () => RunConfig;
|
|
@@ -94,8 +91,6 @@ export class AgentManager {
|
|
|
94
91
|
private readonly observer?: AgentManagerObserver;
|
|
95
92
|
private readonly runner: AgentRunner;
|
|
96
93
|
private readonly worktrees: WorktreeManager;
|
|
97
|
-
private readonly exec: ShellExec;
|
|
98
|
-
private readonly registry: AgentTypeRegistry;
|
|
99
94
|
private readonly _getMaxConcurrent: () => number;
|
|
100
95
|
private getRunConfig?: () => RunConfig;
|
|
101
96
|
|
|
@@ -106,8 +101,6 @@ export class AgentManager {
|
|
|
106
101
|
constructor(options: AgentManagerOptions) {
|
|
107
102
|
this.runner = options.runner;
|
|
108
103
|
this.worktrees = options.worktrees;
|
|
109
|
-
this.exec = options.exec;
|
|
110
|
-
this.registry = options.registry;
|
|
111
104
|
this.observer = options.observer;
|
|
112
105
|
this.getRunConfig = options.getRunConfig;
|
|
113
106
|
this._getMaxConcurrent = options.getMaxConcurrent ?? (() => DEFAULT_MAX_CONCURRENT);
|
|
@@ -190,8 +183,6 @@ export class AgentManager {
|
|
|
190
183
|
try {
|
|
191
184
|
const result = await this.runner.run(snapshot, type, prompt, {
|
|
192
185
|
context: {
|
|
193
|
-
exec: this.exec,
|
|
194
|
-
registry: this.registry,
|
|
195
186
|
cwd: record.worktreeState?.path,
|
|
196
187
|
parentSession: options.parentSession,
|
|
197
188
|
},
|
|
@@ -114,19 +114,27 @@ export interface SessionFactoryIO {
|
|
|
114
114
|
*/
|
|
115
115
|
export type RunnerIO = EnvironmentIO & SessionFactoryIO;
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Dependencies owned by the runner — injected at construction time.
|
|
119
|
+
*
|
|
120
|
+
* Groups the IO boundary with the two static domain deps (exec, registry)
|
|
121
|
+
* that every run() call needs but that do not vary per call.
|
|
122
|
+
*/
|
|
123
|
+
export interface RunnerDeps {
|
|
124
|
+
io: RunnerIO;
|
|
125
|
+
exec: ShellExec;
|
|
126
|
+
registry: AgentConfigLookup;
|
|
127
|
+
}
|
|
128
|
+
|
|
117
129
|
// ── Public interfaces ─────────────────────────────────────────────────────────
|
|
118
130
|
|
|
119
131
|
/**
|
|
120
|
-
*
|
|
132
|
+
* Per-call execution context — fields that vary per spawn.
|
|
121
133
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
134
|
+
* Static dependencies (exec, registry) live on RunnerDeps; this interface
|
|
135
|
+
* carries only the two per-call fields that AgentManager supplies at spawn time.
|
|
124
136
|
*/
|
|
125
137
|
export interface RunContext {
|
|
126
|
-
/** Shell-exec callback for detectEnv - injected from pi.exec(). */
|
|
127
|
-
exec: ShellExec;
|
|
128
|
-
/** Agent config lookup - provides resolveAgentConfig and getToolNamesForType. */
|
|
129
|
-
registry: AgentConfigLookup;
|
|
130
138
|
/** Override working directory (e.g. for worktree isolation). */
|
|
131
139
|
cwd?: string;
|
|
132
140
|
/** Parent session identity (file path + session ID). */
|
|
@@ -182,15 +190,16 @@ export interface AgentRunner {
|
|
|
182
190
|
}
|
|
183
191
|
|
|
184
192
|
/**
|
|
185
|
-
* Concrete AgentRunner backed by
|
|
193
|
+
* Concrete AgentRunner backed by RunnerDeps.
|
|
186
194
|
*
|
|
187
|
-
* Captures
|
|
195
|
+
* Captures IO, exec, and registry at construction time so AgentManager
|
|
196
|
+
* remains unaware of runner-internal dependencies.
|
|
188
197
|
*/
|
|
189
198
|
export class ConcreteAgentRunner implements AgentRunner {
|
|
190
|
-
constructor(private readonly
|
|
199
|
+
constructor(private readonly deps: RunnerDeps) {}
|
|
191
200
|
|
|
192
201
|
run(snapshot: ParentSnapshot, type: SubagentType, prompt: string, options: RunOptions): Promise<RunResult> {
|
|
193
|
-
return runAgent(snapshot, type, prompt, options, this.
|
|
202
|
+
return runAgent(snapshot, type, prompt, options, this.deps);
|
|
194
203
|
}
|
|
195
204
|
|
|
196
205
|
resume(session: AgentSession, prompt: string, options?: ResumeOptions): Promise<string> {
|
|
@@ -253,11 +262,11 @@ export async function runAgent(
|
|
|
253
262
|
type: SubagentType,
|
|
254
263
|
prompt: string,
|
|
255
264
|
options: RunOptions,
|
|
256
|
-
|
|
265
|
+
deps: RunnerDeps,
|
|
257
266
|
): Promise<RunResult> {
|
|
258
267
|
// Resolve working directory upfront - needed for detectEnv before assembly.
|
|
259
268
|
const effectiveCwd = options.context.cwd ?? snapshot.cwd;
|
|
260
|
-
const env = await io.detectEnv(
|
|
269
|
+
const env = await deps.io.detectEnv(deps.exec, effectiveCwd);
|
|
261
270
|
|
|
262
271
|
// Assemble session configuration (synchronous, no SDK objects).
|
|
263
272
|
const cfg = assembleSessionConfig(
|
|
@@ -275,11 +284,11 @@ export async function runAgent(
|
|
|
275
284
|
thinkingLevel: options.thinkingLevel,
|
|
276
285
|
},
|
|
277
286
|
env,
|
|
278
|
-
|
|
279
|
-
io.assemblerIO,
|
|
287
|
+
deps.registry,
|
|
288
|
+
deps.io.assemblerIO,
|
|
280
289
|
);
|
|
281
290
|
|
|
282
|
-
const agentDir = io.getAgentDir();
|
|
291
|
+
const agentDir = deps.io.getAgentDir();
|
|
283
292
|
|
|
284
293
|
// Load extensions/skills: true → load; false → don't.
|
|
285
294
|
// Suppress AGENTS.md/CLAUDE.md and APPEND_SYSTEM.md - upstream's
|
|
@@ -287,7 +296,7 @@ export async function runAgent(
|
|
|
287
296
|
// would defeat prompt_mode: replace and isolated: true. Parent context, if
|
|
288
297
|
// wanted, reaches the subagent via prompt_mode: append (parentSystemPrompt
|
|
289
298
|
// is embedded in systemPromptOverride) or inherit_context (conversation).
|
|
290
|
-
const loader = io.createResourceLoader({
|
|
299
|
+
const loader = deps.io.createResourceLoader({
|
|
291
300
|
cwd: cfg.effectiveCwd,
|
|
292
301
|
agentDir,
|
|
293
302
|
noExtensions: !cfg.extensions,
|
|
@@ -303,15 +312,15 @@ export async function runAgent(
|
|
|
303
312
|
// Create a persisted SessionManager so transcripts are written in Pi's
|
|
304
313
|
// official JSONL format. Falls back to a temp directory when the parent
|
|
305
314
|
// session is not persisted (e.g. headless/API mode).
|
|
306
|
-
const sessionDir = io.deriveSessionDir(options.context.parentSession?.parentSessionFile, cfg.effectiveCwd);
|
|
307
|
-
const sessionManager = io.createSessionManager(cfg.effectiveCwd, sessionDir);
|
|
315
|
+
const sessionDir = deps.io.deriveSessionDir(options.context.parentSession?.parentSessionFile, cfg.effectiveCwd);
|
|
316
|
+
const sessionManager = deps.io.createSessionManager(cfg.effectiveCwd, sessionDir);
|
|
308
317
|
sessionManager.newSession({ parentSession: options.context.parentSession?.parentSessionId });
|
|
309
318
|
|
|
310
|
-
const { session } = await io.createSession({
|
|
319
|
+
const { session } = await deps.io.createSession({
|
|
311
320
|
cwd: cfg.effectiveCwd,
|
|
312
321
|
agentDir,
|
|
313
322
|
sessionManager,
|
|
314
|
-
settingsManager: io.createSettingsManager(cfg.effectiveCwd, agentDir),
|
|
323
|
+
settingsManager: deps.io.createSettingsManager(cfg.effectiveCwd, agentDir),
|
|
315
324
|
modelRegistry: snapshot.modelRegistry,
|
|
316
325
|
model: cfg.model,
|
|
317
326
|
tools: cfg.toolNames,
|