@gotgenes/pi-subagents 13.0.0 → 13.1.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 +9 -0
- package/dist/public.d.ts +1 -1
- package/docs/architecture/architecture.md +74 -41
- package/docs/plans/0265-born-complete-subagent-session.md +330 -0
- package/docs/retro/0264-remove-extension-lifecycle-control.md +41 -0
- package/docs/retro/0265-born-complete-subagent-session.md +58 -0
- package/package.json +1 -1
- package/src/index.ts +3 -3
- package/src/lifecycle/agent-manager.ts +9 -8
- package/src/lifecycle/agent.ts +56 -51
- package/src/lifecycle/create-subagent-session.ts +242 -0
- package/src/lifecycle/subagent-session.ts +204 -0
- package/src/lifecycle/turn-limits.ts +13 -0
- package/src/runtime.ts +1 -1
- package/src/session/conversation.ts +49 -0
- package/src/session/session-config.ts +8 -8
- package/src/settings.ts +1 -1
- package/src/tools/get-result-tool.ts +1 -1
- package/src/tools/spawn-config.ts +1 -1
- package/src/lifecycle/agent-runner.ts +0 -464
- package/src/lifecycle/execution-state.ts +0 -17
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [13.1.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v13.0.0...pi-subagents-v13.1.0) (2026-05-30)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add createSubagentSession factory ([62c319d](https://github.com/gotgenes/pi-packages/commit/62c319d6703a6f58a829f372b609daea36170987))
|
|
14
|
+
* add SubagentSession with turn-loop and disposal behavior ([69f8f4b](https://github.com/gotgenes/pi-packages/commit/69f8f4bf78431be990a9eb6fbe592e59cc313912))
|
|
15
|
+
* dissolve the runner; Agent drives SubagentSession directly ([fbe71b0](https://github.com/gotgenes/pi-packages/commit/fbe71b02759551e60b4e22e96bb28299e444feb2))
|
|
16
|
+
|
|
8
17
|
## [13.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v12.1.0...pi-subagents-v13.0.0) (2026-05-30)
|
|
9
18
|
|
|
10
19
|
|
package/dist/public.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ interface AgentInvocation {
|
|
|
45
45
|
* The child's working directory is supplied by a registered WorkspaceProvider
|
|
46
46
|
* (the workspace seam); with no provider the child runs in the parent cwd.
|
|
47
47
|
*
|
|
48
|
-
* Phase-specific collaborators (
|
|
48
|
+
* Phase-specific collaborators (subagentSession, notification) are attached
|
|
49
49
|
* after construction as lifecycle information becomes available.
|
|
50
50
|
*/
|
|
51
51
|
|
|
@@ -53,7 +53,8 @@ flowchart TB
|
|
|
53
53
|
direction TB
|
|
54
54
|
AgentManager["AgentManager<br/>(spawn, abort, collection)"]
|
|
55
55
|
ConcurrencyQueue["ConcurrencyQueue<br/>(scheduling, drain)"]
|
|
56
|
-
|
|
56
|
+
CreateSubagentSession["createSubagentSession<br/>(assembly factory)"]
|
|
57
|
+
SubagentSession["SubagentSession<br/>(turn loop, steer, dispose)"]
|
|
57
58
|
Agent["Agent<br/>(status, behavior: abort/steer/run lifecycle)"]
|
|
58
59
|
ParentSnapshot["ParentSnapshot<br/>(frozen parent state)"]
|
|
59
60
|
Workspace["workspace<br/>(provider seam: child cwd + teardown)"]
|
|
@@ -85,13 +86,15 @@ flowchart TB
|
|
|
85
86
|
end
|
|
86
87
|
|
|
87
88
|
AgentTool --> AgentManager
|
|
88
|
-
AgentManager -->
|
|
89
|
-
|
|
89
|
+
AgentManager --> Agent
|
|
90
|
+
Agent --> CreateSubagentSession & SubagentSession
|
|
91
|
+
CreateSubagentSession --> SubagentSession
|
|
92
|
+
CreateSubagentSession --> SessionConfig
|
|
90
93
|
SessionConfig --> AgentTypeRegistry
|
|
91
94
|
SessionConfig --> Prompts & Env
|
|
92
95
|
AgentTypeRegistry --> DefaultAgents & CustomAgents
|
|
93
|
-
RecordObserver -.->|subscribes|
|
|
94
|
-
UIObserver -.->|subscribes|
|
|
96
|
+
RecordObserver -.->|subscribes| SubagentSession
|
|
97
|
+
UIObserver -.->|subscribes| SubagentSession
|
|
95
98
|
Widget -.->|polls| AgentManager
|
|
96
99
|
```
|
|
97
100
|
|
|
@@ -108,7 +111,7 @@ classDiagram
|
|
|
108
111
|
+error?: string
|
|
109
112
|
+toolUses: number
|
|
110
113
|
+lifetimeUsage: LifetimeUsage
|
|
111
|
-
+
|
|
114
|
+
+subagentSession?: SubagentSession
|
|
112
115
|
+notification?: NotificationState
|
|
113
116
|
+markRunning()
|
|
114
117
|
+markCompleted()
|
|
@@ -121,13 +124,13 @@ classDiagram
|
|
|
121
124
|
+resume(prompt, signal)
|
|
122
125
|
+abort(): boolean
|
|
123
126
|
+queueSteer(message)
|
|
124
|
-
+flushPendingSteers(
|
|
127
|
+
+flushPendingSteers()
|
|
125
128
|
+completeRun(result)
|
|
126
129
|
+failRun(err)
|
|
130
|
+
+disposeSession()
|
|
127
131
|
+wireSignal(signal, onAbort)
|
|
128
132
|
+attachObserver(unsub)
|
|
129
133
|
+releaseListeners()
|
|
130
|
-
+setOnRunFinished(fn)
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
class AgentManager {
|
|
@@ -210,24 +213,31 @@ sequenceDiagram
|
|
|
210
213
|
participant Tool as subagent tool
|
|
211
214
|
participant Spawn as spawn-config
|
|
212
215
|
participant Mgr as AgentManager
|
|
213
|
-
participant
|
|
216
|
+
participant Ag as Agent
|
|
217
|
+
participant Factory as createSubagentSession
|
|
214
218
|
participant Asm as assembleSessionConfig
|
|
219
|
+
participant Sub as SubagentSession
|
|
215
220
|
participant Child as Child session
|
|
216
221
|
|
|
217
222
|
LLM->>Tool: subagent(type, prompt, ...)
|
|
218
223
|
Tool->>Spawn: resolveSpawnConfig(params)
|
|
219
224
|
Spawn-->>Tool: ResolvedSpawnConfig
|
|
220
225
|
Tool->>Mgr: spawn(snapshot, type, prompt, config)
|
|
221
|
-
Mgr->>
|
|
222
|
-
|
|
223
|
-
Asm
|
|
224
|
-
|
|
225
|
-
Child
|
|
226
|
-
|
|
227
|
-
Note over
|
|
228
|
-
|
|
226
|
+
Mgr->>Ag: run()
|
|
227
|
+
Ag->>Factory: createSubagentSession(params, deps)
|
|
228
|
+
Factory->>Asm: assembleSessionConfig(type, ctx, opts, env, registry, io)
|
|
229
|
+
Asm-->>Factory: SessionConfig
|
|
230
|
+
Factory->>Child: create session + bind extensions
|
|
231
|
+
Factory-->>Ag: SubagentSession (born complete)
|
|
232
|
+
Note over Ag: agent-observer + ui-observer subscribe to session events
|
|
233
|
+
Ag->>Sub: runTurnLoop(prompt, opts)
|
|
234
|
+
Sub->>Child: prompt + drive turn loop
|
|
235
|
+
Child-->>Sub: result text
|
|
236
|
+
Sub-->>Ag: TurnLoopResult
|
|
237
|
+
Ag-->>Mgr: update Agent
|
|
229
238
|
Mgr-->>Tool: Agent
|
|
230
239
|
Tool-->>LLM: formatted result
|
|
240
|
+
Note over Mgr: disposeSession() fires `disposed` at cleanup (resume-detectable)
|
|
231
241
|
```
|
|
232
242
|
|
|
233
243
|
## Module organization
|
|
@@ -257,17 +267,19 @@ src/
|
|
|
257
267
|
│ ├── prompts.ts system prompt building
|
|
258
268
|
│ ├── content-items.ts shared message content parsing (tool-call names, assistant content)
|
|
259
269
|
│ ├── context.ts parent conversation extraction
|
|
270
|
+
│ ├── conversation.ts render a session's messages as formatted text
|
|
260
271
|
│ ├── env.ts git/platform detection
|
|
261
272
|
│ ├── model-resolver.ts fuzzy model name resolution
|
|
262
273
|
│ └── session-dir.ts session directory derivation
|
|
263
274
|
│
|
|
264
275
|
├── lifecycle/ agent execution and state tracking
|
|
265
276
|
│ ├── agent-manager.ts collection manager + observer wiring
|
|
266
|
-
│ ├──
|
|
277
|
+
│ ├── create-subagent-session.ts assembly factory: session creation, binding, tool filtering
|
|
278
|
+
│ ├── subagent-session.ts born-complete child session: turn loop, steer, dispose
|
|
279
|
+
│ ├── turn-limits.ts normalizeMaxTurns (turn-count policy)
|
|
267
280
|
│ ├── agent.ts owns full execution lifecycle (run, abort, steer, workspace)
|
|
268
281
|
│ ├── concurrency-queue.ts background agent scheduling with configurable concurrency limit
|
|
269
282
|
│ ├── parent-snapshot.ts immutable spawn-time parent state
|
|
270
|
-
│ ├── execution-state.ts session/output phase state
|
|
271
283
|
│ ├── child-lifecycle.ts child-execution lifecycle event publisher
|
|
272
284
|
│ ├── workspace.ts workspace provider seam (generative extension surface)
|
|
273
285
|
│ └── usage.ts token usage tracking
|
|
@@ -327,7 +339,7 @@ flowchart TD
|
|
|
327
339
|
subgraph core["@gotgenes/pi-subagents"]
|
|
328
340
|
direction TB
|
|
329
341
|
exports["SubagentsService API<br/>publish / getSubagentsService<br/>SubagentRecord, SubagentStatus"]
|
|
330
|
-
engine["Tools: subagent, get_subagent_result,<br/>steer_subagent<br/>AgentManager,
|
|
342
|
+
engine["Tools: subagent, get_subagent_result,<br/>steer_subagent<br/>AgentManager, createSubagentSession, SubagentSession"]
|
|
331
343
|
ui_int["Internal UI: widget, viewer,<br/>/agents menu"]
|
|
332
344
|
end
|
|
333
345
|
|
|
@@ -344,13 +356,14 @@ They declare this package as an optional peer dependency and use dynamic import
|
|
|
344
356
|
- The three tools: `subagent` (née `Agent`), `get_subagent_result`, `steer_subagent`.
|
|
345
357
|
- `AgentManager` — spawn, abort, resume, collection management, observer wiring.
|
|
346
358
|
- `ConcurrencyQueue` — background agent scheduling with configurable concurrency limit.
|
|
347
|
-
- `
|
|
359
|
+
- `createSubagentSession` — assembly factory: session creation and extension binding; returns a born-complete `SubagentSession`.
|
|
360
|
+
- `SubagentSession` — the born-complete child session: drives the turn loop (`runTurnLoop`/`resumeTurnLoop`), steers, and disposes (firing `disposed` at true session disposal, so resume executions are registry-detected).
|
|
348
361
|
- `child-lifecycle` — publishes the child-execution lifecycle (`spawning`, `session-created` before `bindExtensions()`, `completed`, `disposed`) on `pi.events`.
|
|
349
362
|
Reactive consumers subscribe: `@gotgenes/pi-permission-system` registers each child session on `session-created` and unregisters it on `disposed`.
|
|
350
363
|
This replaced the former outbound `permission-bridge` (#261, ADR 0002) — the core no longer looks up a named consumer.
|
|
351
364
|
- `workspace` — the single generative seam (#262, ADR 0002): a registered `WorkspaceProvider` supplies a child's cwd plus bracketed `dispose()` at run-start.
|
|
352
365
|
With no provider, children run in the parent cwd (default unchanged); the git worktree strategy lives behind this seam in `@gotgenes/pi-subagents-worktrees` (#263, the seam's first consumer).
|
|
353
|
-
- `session-config` — pure configuration assembler (
|
|
366
|
+
- `session-config` — pure configuration assembler (called by `createSubagentSession`).
|
|
354
367
|
- `SubagentRuntime` — session-scoped state bag with methods.
|
|
355
368
|
- `ParentSnapshot` — immutable snapshot of parent session state, captured once at spawn time.
|
|
356
369
|
- `record-observer` — session-event observer that updates record statistics without callback threading.
|
|
@@ -532,20 +545,21 @@ This is achieved across phases: Phase 14 (strip policy), Phase 16 (invert depend
|
|
|
532
545
|
These interfaces carry hidden dependencies that obscure true coupling.
|
|
533
546
|
Bags with 10+ fields are the highest priority for decomposition.
|
|
534
547
|
|
|
535
|
-
| Interface
|
|
536
|
-
|
|
|
537
|
-
| `ResolvedSpawnConfig`
|
|
538
|
-
| `AgentSpawnConfig`
|
|
539
|
-
| `
|
|
540
|
-
| `
|
|
541
|
-
| `
|
|
542
|
-
| `
|
|
543
|
-
| `
|
|
544
|
-
| `
|
|
545
|
-
| `
|
|
546
|
-
| `
|
|
547
|
-
| `
|
|
548
|
-
| `
|
|
548
|
+
| Interface | Fields | Consumers | Severity |
|
|
549
|
+
| ----------------------------- | ------------------------------------------------------------ | ------------------------------------------------- | --------- |
|
|
550
|
+
| `ResolvedSpawnConfig` | 3 nested | foreground-runner, background-spawner, agent-tool | ✓ done |
|
|
551
|
+
| `AgentSpawnConfig` | 13 → 13 (ParentSessionInfo nested) | agent-manager (internal) | ✓ done |
|
|
552
|
+
| `CreateSubagentSessionParams` | 6 (snapshot, type, cwd, parentSession, model, thinkingLevel) | create-subagent-session | ✓ done |
|
|
553
|
+
| `TurnLoopOptions` | 4 (maxTurns, defaultMaxTurns, graceTurns, signal) | subagent-session | ✓ done |
|
|
554
|
+
| `SessionConfig` | 6 (flat fields; extensions/noSkills/extras removed in #264) | session-config (output of assembler) | ✓ done |
|
|
555
|
+
| `NotificationDetails` | 10 | notification | Low (DTO) |
|
|
556
|
+
| `ResourceLoaderOptions` | 10 | create-subagent-session (SDK bridge) | Low (SDK) |
|
|
557
|
+
| `SubagentSessionIO` | split → `EnvironmentIO` (3) + `SessionFactoryIO` (5+1) | create-subagent-session | ✓ done |
|
|
558
|
+
| `CreateSessionOptions` | 9 | create-subagent-session (SDK bridge) | Low (SDK) |
|
|
559
|
+
| `AgentToolDeps` | 8 | agent-tool | ✓ done |
|
|
560
|
+
| `AgentMenuDeps` | 8 | agent-menu | ✓ done |
|
|
561
|
+
| `ConversationViewerOptions` | 8 | conversation-viewer | Low |
|
|
562
|
+
| `AgentInit` | 8 | agent | Low |
|
|
549
563
|
|
|
550
564
|
### Complexity hotspots
|
|
551
565
|
|
|
@@ -575,6 +589,22 @@ The prior clone group between `agent-runner.ts` and `message-formatters.ts` was
|
|
|
575
589
|
The 20-line clone group between `agent-config-editor.ts` and `agent-creation-wizard.ts` was resolved in #217 — extracted into `ui/agent-file-writer.ts` (`writeAgentFile`).
|
|
576
590
|
One 11-line internal clone group remains within `agent-config-editor.ts` (lines 135–145 / 173–183).
|
|
577
591
|
|
|
592
|
+
### Session encapsulation debt (Law of Demeter) — [#277]
|
|
593
|
+
|
|
594
|
+
Discovered while planning [#265].
|
|
595
|
+
Consumers reach the raw SDK `AgentSession` through the `Agent.session` getter (`this.subagentSession?.session`) and operate on it directly, instead of telling the owning agent what they want.
|
|
596
|
+
These are Law of Demeter / Tell-Don't-Ask reach-throughs; the fix is intent-revealing methods on the owning object (`Subagent` / `SubagentSession`, introduced by [#265]).
|
|
597
|
+
|
|
598
|
+
| Reach-through | Sites | Missing method |
|
|
599
|
+
| ------------------------------------- | --------------------------------------------------- | ------------------------------------------------ |
|
|
600
|
+
| Steer (buffer-or-deliver, duplicated) | `service-adapter.ts` (~L93), `steer-tool.ts` (~L43) | `Subagent.steer(message)` |
|
|
601
|
+
| Conversation viewing | `get-result-tool.ts:84`, `agent-menu.ts:255` | `Subagent.getConversation()` |
|
|
602
|
+
| Session-readiness guard | `agent-tool.ts:111`, `agent-manager.ts:203` | `Subagent.isSessionReady()` |
|
|
603
|
+
| Session disposal | `agent-manager.ts:235,309` | `SubagentSession.dispose()` — resolved by [#265] |
|
|
604
|
+
|
|
605
|
+
[#265] introduces `SubagentSession` and routes the run / resume / dispose path (and steering during a run) through it; the consumer-facing reach-throughs above are deferred to [#277].
|
|
606
|
+
The steer buffer-or-deliver decision is duplicated across two call sites — two callers performing the same reach-through plus the same decision is the signal for the missing method.
|
|
607
|
+
|
|
578
608
|
### Proposed bag decompositions
|
|
579
609
|
|
|
580
610
|
#### ResolvedSpawnConfig (15 fields → 3 value objects)
|
|
@@ -785,11 +815,13 @@ The `skills` curation axis collapsed symmetrically with `extensions`: `AgentConf
|
|
|
785
815
|
- Depended on: Step 1 (deny-at-use over events).
|
|
786
816
|
- Outcome: the `isolated`/`extensions`/`noSkills`/`skills` axis is gone; the guard is unconditional.
|
|
787
817
|
|
|
788
|
-
#### Step 5: Born-complete child execution; dissolve the runner — [#265]
|
|
818
|
+
#### Step 5: Born-complete child execution; dissolve the runner — [#265] ✅ Delivered
|
|
789
819
|
|
|
790
|
-
|
|
791
|
-
`
|
|
792
|
-
|
|
820
|
+
`createSubagentSession()` is an assembly factory that returns a born-complete `SubagentSession` (session created, extensions bound, recursion guard applied).
|
|
821
|
+
`SubagentSession` owns turn driving (`runTurnLoop`/`resumeTurnLoop`), steering, and disposal.
|
|
822
|
+
`Agent.run()` is coordination, not assembly; `runAgent` / `resumeAgent` / `ConcreteAgentRunner` / `AgentRunner` / `RunOptions` / `RunResult` / `ExecutionState` dissolved.
|
|
823
|
+
`getAgentConversation()` relocated to `session/conversation.ts`; `normalizeMaxTurns()` to `lifecycle/turn-limits.ts`.
|
|
824
|
+
`disposed` now fires at true session disposal (cleanup), so resume executions are registry-detected (closing the gap deferred from #261).
|
|
793
825
|
|
|
794
826
|
- Depends on: Steps 2–4.
|
|
795
827
|
- Outcome: the "runner" concept is gone; `Agent.run()` is coordination, not assembly — the structural goal of the abandoned collaborator plan, reached cleanly.
|
|
@@ -885,7 +917,7 @@ If they land, upstream gains the peer-dep fix and the two RepOne patches.
|
|
|
885
917
|
This fork continues independently regardless.
|
|
886
918
|
|
|
887
919
|
Upstream fixes and ideas are cherry-picked when they align with this fork's scope.
|
|
888
|
-
The upstream test suite is run periodically as a regression canary for the
|
|
920
|
+
The upstream test suite is run periodically as a regression canary for the session assembly core.
|
|
889
921
|
|
|
890
922
|
[earendil-works/pi#4207]: https://github.com/earendil-works/pi/issues/4207
|
|
891
923
|
[gotgenes/pi-packages]: https://github.com/gotgenes/pi-packages
|
|
@@ -915,3 +947,4 @@ The upstream test suite is run periodically as a regression canary for the agent
|
|
|
915
947
|
[#263]: https://github.com/gotgenes/pi-packages/issues/263
|
|
916
948
|
[#264]: https://github.com/gotgenes/pi-packages/issues/264
|
|
917
949
|
[#265]: https://github.com/gotgenes/pi-packages/issues/265
|
|
950
|
+
[#277]: https://github.com/gotgenes/pi-packages/issues/277
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 265
|
|
3
|
+
issue_title: "Born-complete child execution; dissolve the runner"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Born-complete `SubagentSession`; dissolve the runner
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
Phase 16, Step 5 of ADR 0002.
|
|
11
|
+
Today a subagent run is assembled by a monolithic `runAgent()` (the "runner") in `src/lifecycle/agent-runner.ts`: it creates the child session, binds extensions, drives the turn loop, collects the result, and emits the child-execution lifecycle events.
|
|
12
|
+
`Agent` then sequences workspace teardown and status transitions around it through an injected `AgentRunner` interface.
|
|
13
|
+
With the cwd now resolved through the `WorkspaceProvider` seam (Step 2) and worktrees evicted to a sibling package (Step 3), there is nothing left for a separate runner layer to assemble.
|
|
14
|
+
|
|
15
|
+
Child-session creation should instead produce a *born-complete* value object — a `SubagentSession` that wraps one SDK `AgentSession` plus its turn-driving and teardown — and the runner concept should dissolve.
|
|
16
|
+
`Agent.run()` becomes coordination, not assembly.
|
|
17
|
+
|
|
18
|
+
This step also closes the determinism gap deferred from #261: today `session-created`/`disposed` bracket only the first turn loop ("executing now"), so a resume — a *second* turn loop on the same session — fires no events and the permission system falls back to its filesystem-path heuristic.
|
|
19
|
+
Moving unregistration to true session disposal shifts the registry from "executing now" to "exists", and resume executions become registry-detected for free.
|
|
20
|
+
|
|
21
|
+
## Goals
|
|
22
|
+
|
|
23
|
+
- Introduce a born-complete `SubagentSession` (`{ session, outputFile?, dispose() }` plus turn-driving behavior) produced by a `createSubagentSession()` factory.
|
|
24
|
+
- `Agent` owns session interaction directly: it tells `SubagentSession` to run/resume turn loops, steer, and dispose — no injected runner.
|
|
25
|
+
- Dissolve the runner: remove `runAgent`, `resumeAgent`, `ConcreteAgentRunner`, `AgentRunner`, `RunOptions`, `RunResult`, `ResumeOptions`.
|
|
26
|
+
- Retain `getAgentConversation()` and `normalizeMaxTurns()` by relocating them to focused homes.
|
|
27
|
+
- Move child-session registration to creation and unregistration to true session disposal, so resume executions are registry-detected (the `disposed` event fires at cleanup, not at run-completion).
|
|
28
|
+
- No two-phase `setup()` / late-bound `cwd`: the factory receives a resolved `cwd` value and builds a session that is fully usable the moment it is returned.
|
|
29
|
+
|
|
30
|
+
This is an internal structural refactor.
|
|
31
|
+
The public `SubagentsService` surface (`spawn`, `resume`, `steer`, `registerWorkspaceProvider`) is unchanged, so the change is **non-breaking** for consumers — no `feat!:`.
|
|
32
|
+
The one externally observable change is positive: the permission registry now detects resume executions.
|
|
33
|
+
|
|
34
|
+
## Non-Goals
|
|
35
|
+
|
|
36
|
+
- Renaming the `Agent` class to `Subagent` — deferred to its own follow-up issue (mechanical, ~19 files, orthogonal to this structural change).
|
|
37
|
+
In this issue the class stays `Agent`; only the new object is named `SubagentSession` (consistent with the existing `SubagentType` / `SubagentSessionDir` naming family).
|
|
38
|
+
- Retiring the remaining `agent.session` reach-throughs (steer tool/service buffer-or-deliver, conversation viewing, resume-readiness guards) — tracked in #277.
|
|
39
|
+
`SubagentSession` exposes a `.session` accessor so the existing observer wiring and consumers keep working unchanged; #277 retires those.
|
|
40
|
+
- A resume-aware workspace lifecycle (re-establishing a worktree before a resume).
|
|
41
|
+
A worktree's natural lifetime is one turn loop, not the session; worktree + resume is already degenerate today and stays so.
|
|
42
|
+
See Open Questions.
|
|
43
|
+
- UI extraction (Phase 17).
|
|
44
|
+
|
|
45
|
+
## Background
|
|
46
|
+
|
|
47
|
+
Relevant modules:
|
|
48
|
+
|
|
49
|
+
- `src/lifecycle/agent-runner.ts` — the runner being dissolved.
|
|
50
|
+
`runAgent()` does assembly + turn loop + result collection + lifecycle events; `resumeAgent()` re-prompts an existing session; `ConcreteAgentRunner` wraps both behind the `AgentRunner` interface injected into `AgentManager`.
|
|
51
|
+
Also currently the home of the retained `getAgentConversation()` and `normalizeMaxTurns()`, plus the SDK-bridge IO interfaces (`EnvironmentIO`, `SessionFactoryIO`, `RunnerIO`, `ResourceLoaderOptions`, `CreateSessionOptions`) and the recursion guard `filterActiveTools()`.
|
|
52
|
+
- `src/lifecycle/agent.ts` — `Agent` holds `runner`, `execution: ExecutionState`, workspace prepare/dispose, status transitions, steer buffering, and `run()`/`resume()`.
|
|
53
|
+
- `src/lifecycle/execution-state.ts` — `ExecutionState { session, outputFile }`, attached to `Agent` on session creation.
|
|
54
|
+
Subsumed by `SubagentSession`.
|
|
55
|
+
- `src/lifecycle/agent-manager.ts` — constructs `Agent`s with the injected `runner`; disposes sessions in `removeRecord`/`dispose`/`cleanup`.
|
|
56
|
+
- `src/lifecycle/child-lifecycle.ts` — the `ChildLifecyclePublisher` (`spawning`, `sessionCreated`, `completed`, `disposed`).
|
|
57
|
+
Unchanged here; only *when* `disposed` fires moves.
|
|
58
|
+
- `src/lifecycle/workspace.ts` — the abstract `WorkspaceProvider`/`Workspace` seam.
|
|
59
|
+
The core has zero git/worktree knowledge; all worktree mechanics live in `@gotgenes/pi-subagents-worktrees`, untouched by this issue.
|
|
60
|
+
- `src/session/session-config.ts` — `assembleSessionConfig()`, the pure assembler `runAgent()` calls.
|
|
61
|
+
Unchanged; the factory calls it instead.
|
|
62
|
+
|
|
63
|
+
Registry semantics (the determinism gap): The permission system (`pi-permission-system/src/subagent-lifecycle-events.ts`) registers on `subagents:child:session-created` and unregisters on `subagents:child:disposed`, keyed by `sessionDir`.
|
|
64
|
+
That subscription code does **not** change.
|
|
65
|
+
Today `disposed` fires in `runAgent`'s `finally` (end of the first turn loop), so the registry entry is gone before any resume.
|
|
66
|
+
After this change `disposed` fires when the session is truly disposed (`AgentManager` cleanup / session switch / shutdown), so the entry spans the session's whole existence — every turn loop, including resumes.
|
|
67
|
+
|
|
68
|
+
The two-lifetimes fact (why Option A): A workspace's natural lifetime is **one turn loop** (the run): the `WorkspaceProvider`'s `dispose()` returns a `resultAddendum` that is folded into the run's result, so it must be called at run-completion.
|
|
69
|
+
A session's lifetime spans **many turn loops** (run + resumes) and ends at cleanup.
|
|
70
|
+
Different clocks ⇒ different resources.
|
|
71
|
+
The workspace therefore stays a separate `Agent`-sequenced resource (prepare at run-start, dispose at run-completion, exactly as today); only the session becomes the born-complete object.
|
|
72
|
+
|
|
73
|
+
AGENTS.md constraints that apply:
|
|
74
|
+
|
|
75
|
+
- Ship-source package with a public type bundle (ADR 0003): none of the dissolved types (`RunOptions`, `RunResult`, `AgentRunner`) are part of `service.ts`, so `public.d.ts` is unaffected.
|
|
76
|
+
Run `pnpm run verify:public-types` is **not** required (no public-surface change), but `pnpm run check` is.
|
|
77
|
+
- fallow dead-code: new exports (`SubagentSession`, `createSubagentSession`) must have a production consumer by the end of the work; transient intermediate commits where they are consumed only by tests are acceptable because fallow runs at pre-completion, against the final state.
|
|
78
|
+
- `#src/` path-alias imports only; ES2024 target.
|
|
79
|
+
|
|
80
|
+
## Design Overview
|
|
81
|
+
|
|
82
|
+
Two new lifecycle modules replace the runner.
|
|
83
|
+
|
|
84
|
+
### `SubagentSession` — the born-complete object (owns runtime behavior)
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
/** Outcome of one turn loop. */
|
|
88
|
+
export interface TurnLoopResult {
|
|
89
|
+
responseText: string;
|
|
90
|
+
aborted: boolean; // hard-aborted (max turns + grace exceeded)
|
|
91
|
+
steered: boolean; // soft-limit steer fired, finished in time
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface TurnLoopOptions {
|
|
95
|
+
maxTurns?: number;
|
|
96
|
+
graceTurns?: number;
|
|
97
|
+
signal?: AbortSignal;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* One child AgentSession plus its turn-driving and teardown — born complete.
|
|
102
|
+
* Construction (createSubagentSession) yields a fully usable instance: the
|
|
103
|
+
* session exists, extensions are bound, the recursion guard is applied.
|
|
104
|
+
*/
|
|
105
|
+
export class SubagentSession {
|
|
106
|
+
constructor(
|
|
107
|
+
private readonly _session: AgentSession,
|
|
108
|
+
private readonly meta: {
|
|
109
|
+
outputFile: string | undefined;
|
|
110
|
+
sessionDir: string;
|
|
111
|
+
agentName: string;
|
|
112
|
+
lifecycle: ChildLifecyclePublisher;
|
|
113
|
+
},
|
|
114
|
+
) {}
|
|
115
|
+
|
|
116
|
+
/** Wrapped session — exposed for observer wiring + consumers; retired by #277. */
|
|
117
|
+
get session(): AgentSession { return this._session; }
|
|
118
|
+
get outputFile(): string | undefined { return this.meta.outputFile; }
|
|
119
|
+
|
|
120
|
+
/** Drive the initial run's turn loop; emits `completed` on success. */
|
|
121
|
+
runTurnLoop(prompt: string, opts: TurnLoopOptions): Promise<TurnLoopResult>;
|
|
122
|
+
|
|
123
|
+
/** Re-prompt the same session (resume); does not emit `completed`. */
|
|
124
|
+
resumeTurnLoop(prompt: string, signal?: AbortSignal): Promise<string>;
|
|
125
|
+
|
|
126
|
+
/** Deliver a steer to the live session. */
|
|
127
|
+
steer(message: string): Promise<void>;
|
|
128
|
+
|
|
129
|
+
/** Tear down: session.dispose() + emit `disposed` (registry unregister). */
|
|
130
|
+
dispose(): void;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`runTurnLoop` / `resumeTurnLoop` absorb the turn-counting, soft/hard-limit steer+abort, abort-signal forwarding, and response-text collection currently inside `runAgent`/`resumeAgent`, plus the private helpers `collectResponseText`, `getLastAssistantText`, `forwardAbortSignal`.
|
|
135
|
+
Placing them on `SubagentSession` (the object that owns the `AgentSession`) — rather than reaching through `subagentSession.session` from `Agent` — keeps the design free of the Law-of-Demeter violation that an inline-on-`Agent` or free-function approach would introduce.
|
|
136
|
+
|
|
137
|
+
### `createSubagentSession` — the assembly factory
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
export interface SubagentSessionDeps { // (was RunnerDeps)
|
|
141
|
+
io: SubagentSessionIO; // EnvironmentIO & SessionFactoryIO (moved verbatim)
|
|
142
|
+
exec: ShellExec;
|
|
143
|
+
registry: AgentConfigLookup;
|
|
144
|
+
lifecycle: ChildLifecyclePublisher;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface CreateSubagentSessionParams {
|
|
148
|
+
snapshot: ParentSnapshot;
|
|
149
|
+
type: SubagentType;
|
|
150
|
+
cwd?: string; // resolved workspace cwd; undefined → parent cwd
|
|
151
|
+
parentSession?: ParentSessionInfo;
|
|
152
|
+
model?: Model<any>;
|
|
153
|
+
thinkingLevel?: ThinkingLevel;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function createSubagentSession(
|
|
157
|
+
params: CreateSubagentSessionParams,
|
|
158
|
+
deps: SubagentSessionDeps,
|
|
159
|
+
): Promise<SubagentSession>;
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Body (the assembly portion of `runAgent`, unchanged in substance):
|
|
163
|
+
|
|
164
|
+
1. `lifecycle.spawning(...)`.
|
|
165
|
+
2. `detectEnv(exec, cwd ?? snapshot.cwd)` → `assembleSessionConfig(...)`.
|
|
166
|
+
3. `createResourceLoader` → `reload()`; `createSessionManager` → `newSession(...)`; `createSession(...)`.
|
|
167
|
+
4. Construct `SubagentSession` (session, outputFile, sessionDir, agentName, lifecycle).
|
|
168
|
+
5. `lifecycle.sessionCreated({ sessionDir, agentName, parentSessionId })` — synchronous, before `bindExtensions()` (the pre-bind ordering the permission registry depends on).
|
|
169
|
+
6. `try { await session.bindExtensions({}); applyRecursionGuard(session); } catch (err) { subagentSession.dispose(); throw err; }` — if binding fails *after* `sessionCreated`, dispose (emit `disposed` + `session.dispose()`) before rethrowing, so registration is never leaked.
|
|
170
|
+
7. Return the `SubagentSession`.
|
|
171
|
+
|
|
172
|
+
Note the factory takes a resolved `cwd` value, never the `WorkspaceProvider`.
|
|
173
|
+
The provider stays inside `Agent` (Option A): threading the provider + its prepare-context through the factory just to call `prepare()` would be a parameter-relay smell; `cwd` is a value the factory consumes directly (`detectEnv`, `assembleSessionConfig`, `createSession`).
|
|
174
|
+
|
|
175
|
+
### Lifecycle-event ownership
|
|
176
|
+
|
|
177
|
+
| Event | Emitted by | When |
|
|
178
|
+
| ----------------- | ----------------------------- | --------------------------------------------------------------- |
|
|
179
|
+
| `spawning` | `createSubagentSession` | run start, before session creation |
|
|
180
|
+
| `session-created` | `createSubagentSession` | after creation, before `bindExtensions()` |
|
|
181
|
+
| `completed` | `SubagentSession.runTurnLoop` | end of the run's turn loop (success path) |
|
|
182
|
+
| `disposed` | `SubagentSession.dispose` | true session disposal (cleanup) — **moved** from run-completion |
|
|
183
|
+
|
|
184
|
+
`resume` neither creates a session nor emits `completed`/`disposed` — it re-prompts the live session, preserving today's behavior.
|
|
185
|
+
|
|
186
|
+
### `Agent.run()` — coordination, not assembly (consumer call-site sketch)
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
async run(): Promise<void> {
|
|
190
|
+
this.markRunning(Date.now());
|
|
191
|
+
this.observer?.onStarted?.(this);
|
|
192
|
+
this.wireSignal(this._signal, () => this.abort());
|
|
193
|
+
|
|
194
|
+
let cwd: string | undefined;
|
|
195
|
+
try { // workspace prepare — unchanged
|
|
196
|
+
const provider = this._getWorkspaceProvider?.();
|
|
197
|
+
if (provider) { this._workspace = await provider.prepare({ ... }); cwd = this._workspace?.cwd; }
|
|
198
|
+
} catch (err) { this.markError(err); this.releaseListeners(); this.observer?.onRunFinished?.(this); return; }
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
this.subagentSession = await this._createSubagentSession({
|
|
202
|
+
snapshot: this._snapshot!, type: this.type, cwd,
|
|
203
|
+
parentSession: this._parentSession, model: this._model, thinkingLevel: this._thinkingLevel,
|
|
204
|
+
});
|
|
205
|
+
} catch (err) { this.failRun(err); return; } // factory already disposed its own session
|
|
206
|
+
|
|
207
|
+
this.flushPendingSteers(); // → this.subagentSession.steer(msg)
|
|
208
|
+
this.attachObserver(subscribeAgentObserver(this.subagentSession.session, this, { ... }));
|
|
209
|
+
this.observer?.onSessionCreated?.(this, this.subagentSession.session);
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const result = await this.subagentSession.runTurnLoop(this._prompt!, {
|
|
213
|
+
maxTurns: this._maxTurns, graceTurns: cfg?.graceTurns, signal: this.abortController.signal,
|
|
214
|
+
// (maxTurns resolution stays: per-call ?? agentMaxTurns ?? defaultMaxTurns, via normalizeMaxTurns)
|
|
215
|
+
});
|
|
216
|
+
this.completeRun(result); // workspace teardown + status; no execution rebuild
|
|
217
|
+
} catch (err) { this.failRun(err); }
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`Agent.resume()` becomes `await this.subagentSession!.resumeTurnLoop(prompt, signal)` wrapped in the existing reset/observer/markCompleted/markError/releaseListeners scaffolding — no runner.
|
|
222
|
+
|
|
223
|
+
`completeRun(result: TurnLoopResult)` drops the `session`/`sessionFile` fields (the `SubagentSession` already holds them) and no longer rebuilds `execution`; it does workspace teardown (folding `resultAddendum`) and the status transition, exactly as today.
|
|
224
|
+
|
|
225
|
+
`Agent.execution: ExecutionState` becomes `Agent.subagentSession?: SubagentSession`; the `session` / `outputFile` getters delegate to it.
|
|
226
|
+
A new `Agent.disposeSession()` calls `this.subagentSession?.dispose()`, invoked by `AgentManager` where `record.session?.dispose?.()` is called today.
|
|
227
|
+
|
|
228
|
+
The `subscribeAgentObserver(subagentSession.session, ...)` wiring and `observer.onSessionCreated(agent, session)` still pass the raw `AgentSession`; these are the observer reach-throughs explicitly deferred to #277.
|
|
229
|
+
The `Agent.session` getter likewise still exposes the wrapped session for the external consumers (steer tool, get-result, menu) that #277 retires.
|
|
230
|
+
|
|
231
|
+
### Edge cases
|
|
232
|
+
|
|
233
|
+
- Creation failure after `session-created`: the factory disposes (emit `disposed` + `session.dispose()`) before rethrowing → no registry leak; symmetric with the success path.
|
|
234
|
+
- Turn-loop throw: `SubagentSession` exists and stays registered; `Agent.failRun` runs (workspace teardown + error status); `disposed` fires later at cleanup — symmetric register/unregister regardless of run success or failure.
|
|
235
|
+
- Graceful abort (max turns + grace): `runTurnLoop` returns `{ aborted: true }` and emits `completed` (matching today); a *thrown* error skips `completed`.
|
|
236
|
+
|
|
237
|
+
## Module-Level Changes
|
|
238
|
+
|
|
239
|
+
New:
|
|
240
|
+
|
|
241
|
+
- `src/lifecycle/subagent-session.ts` — `SubagentSession` class, `TurnLoopResult`, `TurnLoopOptions`, and the private turn-loop helpers (`collectResponseText`, `getLastAssistantText`, `forwardAbortSignal`).
|
|
242
|
+
- `src/lifecycle/create-subagent-session.ts` — `createSubagentSession`, `SubagentSessionDeps`, `CreateSubagentSessionParams`, the SDK-bridge IO interfaces moved from `agent-runner.ts` (`EnvironmentIO`, `SessionFactoryIO`, `SubagentSessionIO`, `ResourceLoaderLike`, `SessionManagerLike`, `ResourceLoaderOptions`, `CreateSessionOptions`), and the recursion guard `applyRecursionGuard`/`filterActiveTools` + `EXCLUDED_TOOL_NAMES`.
|
|
243
|
+
- `src/lifecycle/turn-limits.ts` — `normalizeMaxTurns`.
|
|
244
|
+
- `src/session/conversation.ts` — `getAgentConversation` + `formatAttribution`.
|
|
245
|
+
|
|
246
|
+
Changed:
|
|
247
|
+
|
|
248
|
+
- `src/lifecycle/agent.ts` — drop `runner`/`AgentRunner`/`RunResult`/`ExecutionState`; add injected `createSubagentSession` factory dep; `execution` → `subagentSession`; rewrite `run()`/`resume()`; `completeRun(result: TurnLoopResult)`; add `disposeSession()`; `flushPendingSteers()` delegates to `subagentSession.steer`; update the "missing runner" guard messages.
|
|
249
|
+
- `src/lifecycle/agent-manager.ts` — `AgentManagerOptions.runner: AgentRunner` → `createSubagentSession: (params) => Promise<SubagentSession>`; pass it into each `Agent`; `removeRecord`/`dispose` call `record.disposeSession()` instead of `record.session?.dispose?.()`.
|
|
250
|
+
- `src/index.ts` — drop `ConcreteAgentRunner`/`RunnerDeps`; build `SubagentSessionDeps`; pass `createSubagentSession: (p) => createSubagentSession(p, deps)` to `AgentManager`.
|
|
251
|
+
- `src/tools/get-result-tool.ts` — import `getAgentConversation` from `#src/session/conversation`.
|
|
252
|
+
- `src/tools/spawn-config.ts` — import `normalizeMaxTurns` from `#src/lifecycle/turn-limits`.
|
|
253
|
+
- `src/settings.ts` — update the `normalizeMaxTurns()` doc-comment reference.
|
|
254
|
+
- `src/runtime.ts` — update the `RunConfig` doc comment that mentions `RunOptions`.
|
|
255
|
+
- `src/session/session-config.ts` — update doc comments referencing `runAgent()` → `createSubagentSession()`.
|
|
256
|
+
- `docs/architecture/architecture.md` — update the domain dependency diagram (drop `agent-runner` node, add `SubagentSession`/`createSubagentSession`), the execution-flow sequence diagram, the current-layout listing (lifecycle dir), the dependency-bag inventory rows (`RunOptions`, `RunnerIO`, `CreateSessionOptions`, `ResourceLoaderOptions` now belong to the factory module), and mark Step 5 delivered.
|
|
257
|
+
- `.pi/skills/package-pi-subagents/SKILL.md` — the "Lifecycle domain" table lists `agent-runner.ts`; update to the new modules.
|
|
258
|
+
|
|
259
|
+
Removed (final step):
|
|
260
|
+
|
|
261
|
+
- `src/lifecycle/agent-runner.ts` — `runAgent`, `resumeAgent`, `ConcreteAgentRunner`, `AgentRunner`, `RunOptions`, `RunResult`, `ResumeOptions`, `RunContext`, `RunnerDeps`, `RunnerIO` (all migrated or deleted).
|
|
262
|
+
- `src/lifecycle/execution-state.ts` — `ExecutionState` (subsumed by `SubagentSession`).
|
|
263
|
+
|
|
264
|
+
Symbol-removal sweep (grep before finalizing each removal step): `runAgent`, `resumeAgent`, `ConcreteAgentRunner`, `AgentRunner`, `RunResult`, `RunOptions`, `ResumeOptions`, `RunContext`, `RunnerDeps`, `RunnerIO`, `ExecutionState`, `execution-state`, `agent-runner`.
|
|
265
|
+
|
|
266
|
+
## Test Impact Analysis
|
|
267
|
+
|
|
268
|
+
New unit tests the extraction enables:
|
|
269
|
+
|
|
270
|
+
- `SubagentSession` in isolation — construct with a mock `AgentSession` and a `ChildLifecyclePublisher` mock; assert `runTurnLoop` turn-limit behavior (soft steer, hard abort, grace window), response capture, `completed` emission, `resumeTurnLoop` re-prompt, `steer` delegation, and `dispose` (session.dispose + `disposed`).
|
|
271
|
+
Previously these lived as `runAgent`/`resumeAgent` tests entangled with assembly.
|
|
272
|
+
- `createSubagentSession` — assembly + `spawning`/`session-created` ordering + dispose-on-creation-failure, with no turn-loop noise.
|
|
273
|
+
|
|
274
|
+
Tests that become redundant / simplified:
|
|
275
|
+
|
|
276
|
+
- `test/lifecycle/concrete-agent-runner.test.ts` — deleted; `ConcreteAgentRunner` is gone, its delegation coverage absorbed by the factory + `SubagentSession` tests.
|
|
277
|
+
- `Agent.run()` tests no longer re-drive turn events through a mock runner; they assert coordination against a stub `SubagentSession` whose `runTurnLoop` resolves to a canned `TurnLoopResult`.
|
|
278
|
+
|
|
279
|
+
Tests that must stay (genuinely exercise the layer):
|
|
280
|
+
|
|
281
|
+
- The turn-limit behavior tests (now retargeted from the runner to `SubagentSession.runTurnLoop`).
|
|
282
|
+
- The recursion-guard / extension-tool filtering tests (now `createSubagentSession`).
|
|
283
|
+
- The child-lifecycle ordering tests (now split across the factory and `SubagentSession`).
|
|
284
|
+
- The workspace prepare/dispose tests in `agent.test.ts` — unchanged (Option A leaves that path intact); only assertions that read `runner.run`'s args switch to reading the `createSubagentSession` factory params.
|
|
285
|
+
|
|
286
|
+
## TDD Order
|
|
287
|
+
|
|
288
|
+
Lift-and-shift: introduce the new modules alongside the runner, swap consumers atomically, delete the runner last.
|
|
289
|
+
Each step compiles and the suite passes; run `pnpm run check` after every step that touches a shared interface.
|
|
290
|
+
|
|
291
|
+
1. Extract `normalizeMaxTurns` → `src/lifecycle/turn-limits.ts`; update `agent-runner.ts` (internal use), `spawn-config.ts`, and the `settings.ts` comment; move `agent-runner-settings.test.ts` → `turn-limits.test.ts`.
|
|
292
|
+
Commit `refactor: extract normalizeMaxTurns to turn-limits`.
|
|
293
|
+
2. Extract `getAgentConversation` → `src/session/conversation.ts`; update `get-result-tool.ts` import and `test/agent-conversation.test.ts` import.
|
|
294
|
+
Commit `refactor: extract getAgentConversation to session/conversation`.
|
|
295
|
+
3. Add `SubagentSession` (`src/lifecycle/subagent-session.ts`) with `runTurnLoop`/`resumeTurnLoop`/`steer`/`dispose` + the turn-loop helpers (copied from `agent-runner.ts`; the originals are deleted in step 6 — transient duplication).
|
|
296
|
+
New `test/lifecycle/subagent-session.test.ts` (turn limits, response capture, `completed`/`disposed` emission, resume) — retargeted from the runner's turn-limit + final-output tests.
|
|
297
|
+
Commit `feat: add SubagentSession with turn-loop and disposal behavior`.
|
|
298
|
+
4. Add `createSubagentSession` (`src/lifecycle/create-subagent-session.ts`) + `SubagentSessionDeps`/`CreateSubagentSessionParams` + the IO interfaces (moved; re-export from `agent-runner.ts` if still needed there, else copied) + the recursion guard.
|
|
299
|
+
New `test/lifecycle/create-subagent-session.test.ts` and `create-subagent-session-extension-tools.test.ts` — from the runner's assembly, `spawning`/`session-created` ordering, and recursion-guard tests, plus a dispose-on-creation-failure test.
|
|
300
|
+
Commit `feat: add createSubagentSession factory`.
|
|
301
|
+
5. Swap `Agent` + `AgentManager` + `index.ts` to the factory/`SubagentSession`; drop `runner` from `AgentInit`/`AgentManagerOptions`/`index`; `execution` → `subagentSession`; add `disposeSession()`; `disposed` now fires at cleanup.
|
|
302
|
+
This is the atomic call-site swap (the `runner` dep is type-coupled across `Agent` ↔ `AgentManager` ↔ `index`), so it lands with its test updates in one commit: `agent.test.ts` (run/resume/completeRun/workspace/disposeSession sections), `agent-manager.test.ts` (`createManager` helper + the dispose-on-cleanup test), `test/helpers/manager-stubs.ts` (runner stubs → factory stubs), `test/print-mode.test.ts` (mock `createSubagentSession` instead of `runAgent`).
|
|
303
|
+
These are localized edits to large files, not full rewrites — the bulk of `agent.test.ts`/`agent-manager.test.ts` (status transitions, getters, queue) is untouched.
|
|
304
|
+
Commit `feat: dissolve the runner; Agent drives SubagentSession directly`.
|
|
305
|
+
6. Delete `agent-runner.ts`, `execution-state.ts`, and `concrete-agent-runner.test.ts`; rename `test/helpers/runner-io.ts` → `subagent-session-io.ts` (and its factory functions); update `session-config.ts` / `runtime.ts` doc comments; run the symbol-removal grep sweep.
|
|
306
|
+
Commit `refactor: remove agent-runner and ExecutionState`.
|
|
307
|
+
7. Update `docs/architecture/architecture.md` (diagrams, layout, bag inventory, mark Step 5 delivered) and the `package-pi-subagents` skill's lifecycle-domain table.
|
|
308
|
+
Commit `docs: record runner dissolution and SubagentSession (#265)`.
|
|
309
|
+
|
|
310
|
+
## Risks and Mitigations
|
|
311
|
+
|
|
312
|
+
- **Registry entry persists longer (cross-package behavior).**
|
|
313
|
+
`disposed` now fires at session disposal, so permission-registry entries live from creation to cleanup.
|
|
314
|
+
Mitigation: `AgentManager.dispose()` (session_shutdown) disposes every `SubagentSession`, firing `disposed` for each; the permission system's subscription is unchanged.
|
|
315
|
+
Verify with `pnpm -r run test` (the permission system mocks the bus, so no timing coupling).
|
|
316
|
+
- **Transient duplication (steps 3–5).**
|
|
317
|
+
The turn-loop helpers and assembly exist in both `agent-runner.ts` and the new modules until step 6.
|
|
318
|
+
Mitigation: deleted in step 6; fallow runs at pre-completion against the final state.
|
|
319
|
+
- **Large-file test edits in step 5.**
|
|
320
|
+
Mitigation: edits are confined to the run/resume/dispose describe blocks and the `createManager` helper; the new turn-limit/assembly coverage already lives in steps 3–4's dedicated files, so step 5 only adapts coordination assertions.
|
|
321
|
+
- **`disposed` not fired on a path that disposes the raw session directly.**
|
|
322
|
+
Mitigation: grep for every `session?.dispose` / `.dispose()` on a session in `agent-manager.ts` and route all of them through `record.disposeSession()`.
|
|
323
|
+
|
|
324
|
+
## Open Questions
|
|
325
|
+
|
|
326
|
+
- Resume-aware workspaces: should a resumed worktree agent re-establish (or reattach) a workspace before the next `session.prompt()`?
|
|
327
|
+
Today it runs in the removed worktree directory (degenerate).
|
|
328
|
+
This needs `WorkspaceProvider` support for resume and is out of scope; capture as a follow-up if it becomes a real need.
|
|
329
|
+
- Whether `completed` should also fire on resume (it does not today).
|
|
330
|
+
Deferred — preserve current behavior; revisit only with a concrete consumer.
|