@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.
@@ -46,3 +46,44 @@ Test count went 1016 → 951 (−65): deleted `skill-loader.test.ts` and `safe-f
46
46
  - The `spawn-config.test.ts` `agentInvocation` snapshot carried a stale `isolation: undefined` leftover (from the #263 worktree eviction) that `toEqual` had been silently ignoring; removed it alongside `isolated: false` for a clean exact-match assertion.
47
47
  - `verify:public-types` confirmed the breaking `SpawnOptions.isolated` removal type-checks against an external consumer; no lockfile changes; `dist/` correctly gitignored after the type-bundle build.
48
48
  - Pre-completion reviewer: **PASS** — both acceptance criteria code-verified, all deterministic checks green, 6 Mermaid diagrams render, docs accurate, zero dead code.
49
+
50
+ ## Stage: Final Retrospective (2026-05-30T01:13:26Z)
51
+
52
+ ### Session summary
53
+
54
+ Shipped #264 end-to-end across three stages (Planning → TDD → Ship) in one conversation: planned the four-cycle removal, implemented it as three `feat!:` commits plus docs, and released `pi-subagents-v13.0.0` via the release-please PR (#276).
55
+ The run had zero rework and a PASS pre-completion review; test count moved 1016 → 951.
56
+
57
+ ### Observations
58
+
59
+ #### What went well
60
+
61
+ - The planning-stage `ask-user` gate caught a real scope trap before any code was written.
62
+ The issue named three fields (`isolated`, `extensions: false`, `noSkills`), but `noSkills` turned out to be the single mechanism behind **two** skill-restriction modes (skill-disable and `skills: string[]` preload).
63
+ Removing it while keeping `AgentConfig.skills` would have left a field that silently stops restricting — a mid-cycle-3 wall.
64
+ Catching it at planning time expanded the clean scope to four collapsing fields and produced a symmetric, reviewable plan.
65
+ - The four-cycle split (`isolated` → `extensions` + unconditional guard → `skills`/`noSkills`/preload → docs) held with no cross-cycle dangling references, validating the lift-and-shift discipline for shared-interface removal.
66
+ - Verification ran incrementally (`pnpm run check` + `pnpm run test` after every cycle), so each commit landed green; the only end-of-run surprise was a pre-commit `end-of-file-fixer` hook touching `custom-agents.ts` (handled with a re-add, no rework).
67
+
68
+ #### What caused friction (agent side)
69
+
70
+ - `other` (tooling) — BSD `sed` on macOS does not support `\|` alternation in basic regex; the first bulk fixture-deletion attempt silently matched nothing.
71
+ Impact: one wasted tool call, no rework; resolved by switching to `sed -E`.
72
+
73
+ #### What caused friction (user side)
74
+
75
+ - The opening sequencing question ("is there work that should precede this") was first read literally as a prerequisite check (#261/#262/#263), and the user had to rephrase to get at the within-issue sequencing point that surfaced the `noSkills`/`skills` coupling.
76
+ Opportunity, not criticism: offering both readings of an ambiguous "what comes first" question up front would have reached the valuable discovery one turn sooner.
77
+
78
+ #### Diagnostic details
79
+
80
+ - **Model-performance correlation** — one subagent dispatch (`pre-completion-reviewer`, 325s, 43 tool uses) on a judgment-heavy review task; appropriate assignment.
81
+ No Explore/Plan subagents were needed — planning-stage exploration was direct file reads and targeted greps, efficient for this removal's scope.
82
+ - **Escalation-delay tracking** — no rabbit-holes; the `sed` issue resolved in one retry.
83
+ - **Feedback-loop gap analysis** — no gap; `check`/`test`/`lint` ran after each cycle, with `fallow dead-code` and `verify:public-types` at the end.
84
+
85
+ ### Changes made
86
+
87
+ 1. Appended this Final Retrospective stage entry to `packages/pi-subagents/docs/retro/0264-remove-extension-lifecycle-control.md`.
88
+ 2. Considered but **declined** (user: "too narrow") a removal-coupling detection rule for `.pi/prompts/plan-issue.md` — the heuristic that a field named for removal may be the mechanism behind a separate surviving feature.
89
+ No prompt or `AGENTS.md` changes were made this retro.
@@ -0,0 +1,58 @@
1
+ ---
2
+ issue: 265
3
+ issue_title: "Born-complete child execution; dissolve the runner"
4
+ ---
5
+
6
+ # Retro: #265 — Born-complete child execution; dissolve the runner
7
+
8
+ ## Stage: Planning (2026-05-30T02:30:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced the implementation plan for dissolving the `agent-runner` and introducing a born-complete `SubagentSession`.
13
+ Most of the session was a design dialogue that resolved naming, the turn-loop home, a discovered Law-of-Demeter cluster, and the workspace-ownership fork before any plan text was written.
14
+ Plan committed as `0265-born-complete-subagent-session.md`; a side-quest filed #277 and added an architecture-doc breadcrumb for discovered debt.
15
+
16
+ ### Observations
17
+
18
+ - Vocabulary was pinned down explicitly because "execution" is overloaded: granular execution = one turn loop (one `session.prompt()`, run or resume); the born-complete object spans the whole session lifetime (run + resumes).
19
+ The object is named `SubagentSession` (matches the existing `SubagentType` / `SubagentSessionDir` / `SubagentSessionRegistry` family; cohesive with the deferred `Agent` → `Subagent` rename).
20
+ Turn driving is `runTurnLoop` / `resumeTurnLoop`; resume is *not* an SDK `session.resume()` — it is `session.prompt()` again on the retained session.
21
+ - The turn-loop home is **on `SubagentSession`** (methods), not inline on `Agent` and not a free function.
22
+ The user caught that `subagent.driveTurnLoop(subagentSession.session, …)` is a Law-of-Demeter reach-through; putting the behavior on the object that owns the `AgentSession` is both LoD-correct and more testable (satisfying the user's conditional "inline only if straightforward to test").
23
+ - Workspace ownership locked to **Option A** (session-only `SubagentSession`; `Agent` keeps workspace prepare/dispose).
24
+ Decisive reasoning: the workspace and the session have genuinely different lifetimes (workspace dies at run-completion to fold its `resultAddendum` into the result; session survives to cleanup for resume + the new registry boundary), so they are different resources.
25
+ Option B would fuse them into one object needing two teardown methods, and would thread the `WorkspaceProvider` + prepare-context through the factory just to call `prepare()` — a parameter-relay smell the user flagged.
26
+ The factory takes a resolved `cwd` value (used directly), never the provider.
27
+ - Worktrees are already out of the core (#263) — confirmed zero git code in `pi-subagents/src/` (only doc comments).
28
+ The A/B fork is purely about how the core sequences its abstract `WorkspaceProvider` seam; `@gotgenes/pi-subagents-worktrees` is untouched.
29
+ - Registry semantics: moving `disposed` from run-completion to true session disposal makes resume executions registry-detected (closes the gap deferred from #261).
30
+ The permission system's subscription code does not change; only *when* `disposed` fires moves.
31
+ Edge case planned: `createSubagentSession` must dispose on a post-`session-created` failure to avoid a registry leak.
32
+ - Discovered debt captured (the user's "it is in doing the work that we discover the work to be done"): filed #277 for the remaining `agent.session` reach-throughs (steer buffer-or-deliver duplicated across `steer-tool` + `service-adapter`, conversation viewing, resume-readiness guards) and added a "Session encapsulation debt (Law of Demeter)" subsection to `architecture.md` (commit `038a1283`).
33
+ `SubagentSession` exposes a `.session` accessor in #265 so observer wiring + consumers keep working; #277 retires those.
34
+ - Two follow-ups deliberately deferred and noted in the plan's Non-Goals / Open Questions: the `Agent` → `Subagent` class rename (mechanical, ~19 files — separate issue) and resume-aware workspaces (a worktree's lifetime is one turn loop; worktree + resume is degenerate today).
35
+ - The change is non-breaking (no `feat!:`): the dissolved types (`RunOptions`, `RunResult`, `AgentRunner`) are internal, so `public.d.ts` is unaffected.
36
+ TDD order uses lift-and-shift across 7 steps to keep each commit compiling; transient duplication of the turn-loop helpers/assembly exists between steps 3–5 and is deleted in step 6.
37
+
38
+ ## Stage: Implementation — TDD (2026-05-29T22:18:00Z)
39
+
40
+ ### Session summary
41
+
42
+ Executed all 7 TDD steps from the plan via lift-and-shift, one commit per step, each leaving the suite green.
43
+ Introduced `SubagentSession` (`runTurnLoop`/`resumeTurnLoop`/`steer`/`dispose`) and the `createSubagentSession()` assembly factory, swapped `Agent`/`AgentManager`/`index.ts` onto them, then deleted `agent-runner.ts` + `execution-state.ts` and the three runner test files.
44
+ Package test count went 951 → 960 (net +9: new `subagent-session`/`create-subagent-session`/`turn-limits` suites added, the redundant runner suites deleted).
45
+ Pre-completion reviewer: initial FAIL (MD060 table alignment in SKILL.md, auto-fixed by `rumdl fmt`), PASS on re-check after fix + stale doc cleanup.
46
+
47
+ ### Observations
48
+
49
+ - The plan sketch's `TurnLoopOptions` listed only `maxTurns`/`graceTurns`/`signal`, but preserving the old `runAgent` precedence `per-call ?? agentMaxTurns ?? defaultMaxTurns` required threading `defaultMaxTurns` through `TurnLoopOptions` and storing `agentMaxTurns` + `parentContext` in `SubagentSession` meta (both are session-level facts known at creation).
50
+ This is a correctness-preserving deviation, well covered by three precedence tests plus a parent-context-prepend test in `subagent-session.test.ts`.
51
+ - The atomic call-site swap (step 5) touched more test files than the plan's step-5 list anticipated: every tool/service test that set `record.execution = { session, outputFile }` (`steer-tool`, `agent-tool`, `background-spawner`, `foreground-runner`, `get-result-tool`, `service-adapter`) had to migrate to `record.subagentSession = toSubagentSession(createSubagentSessionStub(...))`.
52
+ Added `createSubagentSessionStub`/`toSubagentSession` to `mock-session.ts` so the migration was a one-line change per call site; the stub's `steer`/`dispose` delegate to the underlying `MockSession` so existing session-spy assertions kept working unchanged.
53
+ - `disposed` moved from `runAgent`'s `finally` (run-completion) to `SubagentSession.dispose()`, invoked by `AgentManager` via the new `Agent.disposeSession()` (routing both `record.session?.dispose?.()` call sites at `agent-manager.ts:235,309`).
54
+ The full cross-package suite confirms the permission system (1504 tests) is unaffected — its subscription code did not change, only *when* `disposed` fires.
55
+ - Test-helper gotcha: `makeSubagentSession`'s `outputFile` default initially swallowed an explicit `undefined` via `?? default`; fixed with an `"outputFile" in metaOverrides` presence check (the testing-skill "Partial spread erases explicit undefined" family).
56
+ - `print-mode.test.ts` now mocks `#src/lifecycle/create-subagent-session` (was `#src/lifecycle/agent-runner`); `index.ts` wraps the factory as `(params) => createSubagentSession(params, deps)`, so the module mock still intercepts it.
57
+ - fallow stayed clean throughout — the transient duplication of IO interfaces + turn-loop helpers between `agent-runner.ts` and the new modules (steps 3–5) was removed in step 6 before the pre-completion gate ran.
58
+ - Reviewer's two minor non-blocking notes: SKILL.md Session-domain count now lists `conversation.ts` but still omits the pre-existing `content-items.ts` (drift predates this issue); `create-subagent-session.ts` keeps an accurate "old runner's runAgent()" provenance comment.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "13.0.0",
3
+ "version": "13.1.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/index.ts CHANGED
@@ -24,9 +24,9 @@ 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 RunnerDeps } from "#src/lifecycle/agent-runner";
28
27
  import { createChildLifecyclePublisher } from "#src/lifecycle/child-lifecycle";
29
28
  import { ConcurrencyQueue } from "#src/lifecycle/concurrency-queue";
29
+ import { createSubagentSession, type SubagentSessionDeps } from "#src/lifecycle/create-subagent-session";
30
30
  import { buildParentSnapshot } from "#src/lifecycle/parent-snapshot";
31
31
  import { buildEventData, type NotificationDetails, NotificationManager } from "#src/observation/notification";
32
32
  import { createNotificationRenderer } from "#src/observation/renderer";
@@ -132,7 +132,7 @@ export default function (pi: ExtensionAPI) {
132
132
  },
133
133
  };
134
134
 
135
- const runnerDeps: RunnerDeps = {
135
+ const subagentSessionDeps: SubagentSessionDeps = {
136
136
  io: {
137
137
  detectEnv,
138
138
  getAgentDir,
@@ -162,7 +162,7 @@ export default function (pi: ExtensionAPI) {
162
162
  );
163
163
 
164
164
  const manager = new AgentManager({
165
- runner: new ConcreteAgentRunner(runnerDeps),
165
+ createSubagentSession: (params) => createSubagentSession(params, subagentSessionDeps),
166
166
  baseCwd: process.cwd(),
167
167
  observer,
168
168
  queue,
@@ -10,9 +10,10 @@ import { randomUUID } from "node:crypto";
10
10
  import type { Model } from "@earendil-works/pi-ai";
11
11
  import { debugLog } from "#src/debug";
12
12
  import { Agent, type AgentLifecycleObserver } from "#src/lifecycle/agent";
13
- import type { AgentRunner } from "#src/lifecycle/agent-runner";
14
13
  import type { ConcurrencyQueue } from "#src/lifecycle/concurrency-queue";
14
+ import type { CreateSubagentSessionParams } from "#src/lifecycle/create-subagent-session";
15
15
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
16
+ import type { SubagentSession } from "#src/lifecycle/subagent-session";
16
17
  import type { WorkspaceProvider } from "#src/lifecycle/workspace";
17
18
 
18
19
  import type { RunConfig } from "#src/runtime";
@@ -28,7 +29,8 @@ export interface AgentManagerObserver {
28
29
  }
29
30
 
30
31
  export interface AgentManagerOptions {
31
- runner: AgentRunner;
32
+ /** Assembly factory that produces a born-complete SubagentSession per spawn. */
33
+ createSubagentSession: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
32
34
  /** Concurrency queue — owns scheduling, limit checks, and drain logic. */
33
35
  queue: ConcurrencyQueue;
34
36
  /** Base working directory handed to a workspace provider (the parent cwd). */
@@ -64,7 +66,7 @@ export class AgentManager {
64
66
  private agents = new Map<string, Agent>();
65
67
  private cleanupInterval: ReturnType<typeof setInterval>;
66
68
  private readonly observer?: AgentManagerObserver;
67
- private readonly runner: AgentRunner;
69
+ private readonly createSubagentSession: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
68
70
  private readonly queue: ConcurrencyQueue;
69
71
  private readonly baseCwd: string;
70
72
  private getRunConfig?: () => RunConfig;
@@ -76,7 +78,7 @@ export class AgentManager {
76
78
  }
77
79
 
78
80
  constructor(options: AgentManagerOptions) {
79
- this.runner = options.runner;
81
+ this.createSubagentSession = options.createSubagentSession;
80
82
  this.queue = options.queue;
81
83
  this.baseCwd = options.baseCwd;
82
84
  this.observer = options.observer;
@@ -152,7 +154,7 @@ export class AgentManager {
152
154
  parentSession: options.parentSession,
153
155
  signal: options.signal,
154
156
  // Shared deps
155
- runner: this.runner,
157
+ createSubagentSession: this.createSubagentSession,
156
158
  observer: this.buildObserver(options),
157
159
  getRunConfig: this.getRunConfig,
158
160
  baseCwd: this.baseCwd,
@@ -231,8 +233,7 @@ export class AgentManager {
231
233
 
232
234
  /** Dispose a record's session and remove it from the map. */
233
235
  private removeRecord(id: string, record: Agent): void {
234
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- dispose may not exist on all session implementations
235
- record.session?.dispose?.();
236
+ record.disposeSession();
236
237
  this.agents.delete(id);
237
238
  }
238
239
 
@@ -306,7 +307,7 @@ export class AgentManager {
306
307
  // Clear queue
307
308
  this.queue.clear();
308
309
  for (const record of this.agents.values()) {
309
- record.session?.dispose();
310
+ record.disposeSession();
310
311
  }
311
312
  this.agents.clear();
312
313
  }
@@ -14,16 +14,16 @@
14
14
  * The child's working directory is supplied by a registered WorkspaceProvider
15
15
  * (the workspace seam); with no provider the child runs in the parent cwd.
16
16
  *
17
- * Phase-specific collaborators (execution, notification) are attached
17
+ * Phase-specific collaborators (subagentSession, notification) are attached
18
18
  * after construction as lifecycle information becomes available.
19
19
  */
20
20
 
21
21
  import type { Model } from "@earendil-works/pi-ai";
22
22
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
23
23
  import { debugLog } from "#src/debug";
24
- import type { AgentRunner, RunResult } from "#src/lifecycle/agent-runner";
25
- import type { ExecutionState } from "#src/lifecycle/execution-state";
24
+ import type { CreateSubagentSessionParams } from "#src/lifecycle/create-subagent-session";
26
25
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
26
+ import type { SubagentSession, TurnLoopResult } from "#src/lifecycle/subagent-session";
27
27
  import type { LifetimeUsage } from "#src/lifecycle/usage";
28
28
  import { addUsage } from "#src/lifecycle/usage";
29
29
  import type { Workspace, WorkspaceProvider } from "#src/lifecycle/workspace";
@@ -36,7 +36,7 @@ import type { AgentInvocation, CompactionInfo, ParentSessionInfo, SubagentType,
36
36
  export interface AgentLifecycleObserver {
37
37
  /** Fires when the agent transitions to running (inside run(), after markRunning). */
38
38
  onStarted?(agent: Agent): void;
39
- /** Fires when the runner creates the session — delivers the session to external consumers. */
39
+ /** Fires once the session is created — delivers the session to external consumers. */
40
40
  onSessionCreated?(agent: Agent, session: AgentSession): void;
41
41
  /** Fires once when the run completes or fails (for concurrency drain). */
42
42
  onRunFinished?(agent: Agent): void;
@@ -68,7 +68,8 @@ export interface AgentInit {
68
68
  error?: string;
69
69
 
70
70
  // Shared deps (required for run(), optional for tests)
71
- runner?: AgentRunner;
71
+ /** Assembly factory that produces a born-complete SubagentSession. */
72
+ createSubagentSession?: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
72
73
  observer?: AgentLifecycleObserver;
73
74
  getRunConfig?: () => RunConfig;
74
75
  /** Resolves the registered workspace provider (if any) at run-start. */
@@ -126,7 +127,7 @@ export class Agent {
126
127
  promise?: Promise<void>;
127
128
 
128
129
  // Shared deps — optional (required for run())
129
- private readonly _runner?: AgentRunner;
130
+ private readonly _createSubagentSession?: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
130
131
  readonly observer?: AgentLifecycleObserver;
131
132
  private readonly _getRunConfig?: () => RunConfig;
132
133
  private readonly _getWorkspaceProvider?: () => WorkspaceProvider | undefined;
@@ -144,7 +145,8 @@ export class Agent {
144
145
  private readonly _signal?: AbortSignal;
145
146
 
146
147
  // Phase-specific collaborators — each born complete when their info becomes available
147
- execution?: ExecutionState;
148
+ /** The born-complete child session — set when the factory returns inside run(). */
149
+ subagentSession?: SubagentSession;
148
150
  notification?: NotificationState;
149
151
 
150
152
  // Steer buffer — messages queued before the session is ready
@@ -154,12 +156,12 @@ export class Agent {
154
156
 
155
157
  /** The active agent session, or undefined before the session is created. */
156
158
  get session(): AgentSession | undefined {
157
- return this.execution?.session;
159
+ return this.subagentSession?.session;
158
160
  }
159
161
 
160
162
  /** Path to the agent's session JSONL file, or undefined if not yet available. */
161
163
  get outputFile(): string | undefined {
162
- return this.execution?.outputFile;
164
+ return this.subagentSession?.outputFile;
163
165
  }
164
166
 
165
167
  constructor(init: AgentInit) {
@@ -185,7 +187,7 @@ export class Agent {
185
187
  this.abortController = new AbortController();
186
188
 
187
189
  // Shared deps
188
- this._runner = init.runner;
190
+ this._createSubagentSession = init.createSubagentSession;
189
191
  this.observer = init.observer;
190
192
  this._getRunConfig = init.getRunConfig;
191
193
  this._getWorkspaceProvider = init.getWorkspaceProvider;
@@ -207,16 +209,16 @@ export class Agent {
207
209
  }
208
210
 
209
211
  /**
210
- * Execute the full agent lifecycle: workspace preparation, runner invocation,
211
- * session-creation handling, observer wiring, workspace disposal, and
212
+ * Execute the full agent lifecycle: workspace preparation, session creation
213
+ * via the factory, observer wiring, the turn loop, workspace disposal, and
212
214
  * status transitions.
213
215
  *
214
- * Requires runner and snapshot to be set at construction.
216
+ * Requires the session factory and snapshot to be set at construction.
215
217
  * The returned promise always resolves (errors are captured internally).
216
218
  */
217
219
  async run(): Promise<void> {
218
- if (!this._runner) {
219
- throw new Error("Agent not configured for execution — missing runner");
220
+ if (!this._createSubagentSession) {
221
+ throw new Error("Agent not configured for execution — missing session factory");
220
222
  }
221
223
  if (!this._snapshot || !this._prompt) {
222
224
  throw new Error("Agent not configured for execution — missing snapshot or prompt");
@@ -247,29 +249,35 @@ export class Agent {
247
249
  return;
248
250
  }
249
251
 
250
- const runConfig = this._getRunConfig?.();
251
252
  try {
252
- const result = await this._runner.run(this._snapshot, this.type, this._prompt, {
253
- context: {
254
- cwd,
255
- parentSession: this._parentSession,
256
- },
253
+ this.subagentSession = await this._createSubagentSession({
254
+ snapshot: this._snapshot,
255
+ type: this.type,
256
+ cwd,
257
+ parentSession: this._parentSession,
257
258
  model: this._model,
259
+ thinkingLevel: this._thinkingLevel,
260
+ });
261
+ } catch (err) {
262
+ // The factory disposed its own session on a post-creation failure.
263
+ this.failRun(err);
264
+ return;
265
+ }
266
+
267
+ const session = this.subagentSession.session;
268
+ this.flushPendingSteers();
269
+ this.attachObserver(subscribeAgentObserver(session, this, {
270
+ onCompact: (r, info) => this.observer?.onCompacted?.(r, info),
271
+ }));
272
+ this.observer?.onSessionCreated?.(this, session);
273
+
274
+ const runConfig = this._getRunConfig?.();
275
+ try {
276
+ const result = await this.subagentSession.runTurnLoop(this._prompt, {
258
277
  maxTurns: this._maxTurns,
259
278
  defaultMaxTurns: runConfig?.defaultMaxTurns,
260
279
  graceTurns: runConfig?.graceTurns,
261
- thinkingLevel: this._thinkingLevel,
262
280
  signal: this.abortController.signal,
263
- onSessionCreated: (session) => {
264
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- sessionManager is typed as always present but Pi SDK may not provide it
265
- const outputFile = session.sessionManager?.getSessionFile?.() ?? undefined;
266
- this.execution = { session, outputFile };
267
- this.flushPendingSteers(session);
268
- this.attachObserver(subscribeAgentObserver(session, this, {
269
- onCompact: (r, info) => this.observer?.onCompacted?.(r, info),
270
- }));
271
- this.observer?.onSessionCreated?.(this, session);
272
- },
273
281
  });
274
282
  this.completeRun(result);
275
283
  } catch (err) {
@@ -281,27 +289,24 @@ export class Agent {
281
289
  * Resume an existing session with a new prompt, managing the observer
282
290
  * subscription lifecycle internally (same wiring as run()).
283
291
  *
284
- * Requires runner and an existing session (set when the original run created it).
292
+ * Requires an existing SubagentSession (set when the original run created it).
285
293
  * The returned promise always resolves (errors are captured internally).
286
- * The parent signal flows straight through to runner.resume — resume does not
294
+ * The parent signal flows straight through to resumeTurnLoop — resume does not
287
295
  * route through this.abortController.
288
296
  */
289
297
  async resume(prompt: string, signal?: AbortSignal): Promise<void> {
290
- if (!this._runner) {
291
- throw new Error("Agent not configured for execution — missing runner");
292
- }
293
- const session = this.session;
294
- if (!session) {
298
+ const subagentSession = this.subagentSession;
299
+ if (!subagentSession) {
295
300
  throw new Error("Agent not configured for resume — missing session");
296
301
  }
297
302
 
298
303
  this.resetForResume(Date.now());
299
- this.attachObserver(subscribeAgentObserver(session, this, {
304
+ this.attachObserver(subscribeAgentObserver(subagentSession.session, this, {
300
305
  onCompact: (r, info) => this.observer?.onCompacted?.(r, info),
301
306
  }));
302
307
 
303
308
  try {
304
- const responseText = await this._runner.resume(session, prompt, { signal });
309
+ const responseText = await subagentSession.resumeTurnLoop(prompt, signal);
305
310
  this.markCompleted(responseText);
306
311
  } catch (err) {
307
312
  this.markError(err);
@@ -407,11 +412,11 @@ export class Agent {
407
412
 
408
413
  /**
409
414
  * Flush all buffered steer messages to the session and clear the buffer.
410
- * Called from onSessionCreated once the session is available.
415
+ * Called once the session is available, delegating to SubagentSession.steer.
411
416
  */
412
- flushPendingSteers(session: AgentSession): void {
417
+ flushPendingSteers(): void {
413
418
  for (const msg of this._pendingSteers) {
414
- session.steer(msg).catch(() => {});
419
+ this.subagentSession?.steer(msg).catch(() => {});
415
420
  }
416
421
  this._pendingSteers = [];
417
422
  }
@@ -451,8 +456,8 @@ export class Agent {
451
456
  this._detachFn = undefined;
452
457
  }
453
458
 
454
- /** Complete a run: release listeners, dispose the workspace, status transition, execution update, notify observer. */
455
- completeRun(result: RunResult): void {
459
+ /** Complete a run: release listeners, dispose the workspace, status transition, notify observer. */
460
+ completeRun(result: TurnLoopResult): void {
456
461
  this.releaseListeners();
457
462
 
458
463
  let finalResult = result.responseText;
@@ -470,14 +475,14 @@ export class Agent {
470
475
  else if (result.steered) this.markSteered(finalResult);
471
476
  else this.markCompleted(finalResult);
472
477
 
473
- this.execution = {
474
- session: result.session,
475
- outputFile: result.sessionFile ?? this.execution?.outputFile,
476
- };
477
-
478
478
  this.observer?.onRunFinished?.(this);
479
479
  }
480
480
 
481
+ /** Dispose the wrapped session, firing the `disposed` lifecycle event. */
482
+ disposeSession(): void {
483
+ this.subagentSession?.dispose();
484
+ }
485
+
481
486
  /** Fail a run: mark error, release listeners, best-effort workspace dispose, notify observer. */
482
487
  failRun(err: unknown): void {
483
488
  this.markError(err);