@gotgenes/pi-subagents 12.1.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/public.d.ts +1 -3
  3. package/docs/architecture/architecture.md +86 -57
  4. package/docs/plans/0264-remove-extension-lifecycle-control.md +275 -0
  5. package/docs/plans/0265-born-complete-subagent-session.md +330 -0
  6. package/docs/retro/0264-remove-extension-lifecycle-control.md +89 -0
  7. package/docs/retro/0265-born-complete-subagent-session.md +58 -0
  8. package/package.json +1 -1
  9. package/src/config/agent-types.ts +0 -2
  10. package/src/config/custom-agents.ts +0 -30
  11. package/src/config/default-agents.ts +1 -7
  12. package/src/config/invocation-config.ts +0 -3
  13. package/src/index.ts +3 -5
  14. package/src/lifecycle/agent-manager.ts +9 -10
  15. package/src/lifecycle/agent.ts +56 -55
  16. package/src/lifecycle/create-subagent-session.ts +242 -0
  17. package/src/lifecycle/subagent-session.ts +204 -0
  18. package/src/lifecycle/turn-limits.ts +13 -0
  19. package/src/runtime.ts +1 -1
  20. package/src/service/service-adapter.ts +0 -1
  21. package/src/service/service.ts +0 -1
  22. package/src/session/conversation.ts +49 -0
  23. package/src/session/prompts.ts +2 -23
  24. package/src/session/session-config.ts +10 -45
  25. package/src/settings.ts +1 -1
  26. package/src/tools/agent-tool.ts +0 -5
  27. package/src/tools/background-spawner.ts +0 -1
  28. package/src/tools/foreground-runner.ts +0 -1
  29. package/src/tools/get-result-tool.ts +1 -1
  30. package/src/tools/spawn-config.ts +1 -5
  31. package/src/types.ts +0 -7
  32. package/src/ui/agent-config-editor.ts +0 -5
  33. package/src/ui/agent-creation-wizard.ts +0 -4
  34. package/src/ui/display.ts +1 -2
  35. package/src/lifecycle/agent-runner.ts +0 -472
  36. package/src/lifecycle/execution-state.ts +0 -17
  37. package/src/session/safe-fs.ts +0 -45
  38. package/src/session/skill-loader.ts +0 -104
@@ -0,0 +1,89 @@
1
+ ---
2
+ issue: 264
3
+ issue_title: "Remove isolated / extensions:false / noSkills from core"
4
+ ---
5
+
6
+ # Retro: #264 — Remove isolated / extensions:false / noSkills from core
7
+
8
+ ## Stage: Planning (2026-05-30T00:09:21Z)
9
+
10
+ ### Session summary
11
+
12
+ Planned Phase 16, Step 4: removing the extension-lifecycle-control axis (`isolated`, `extensions: false`, `noSkills`) from the pi-subagents core per ADR 0002.
13
+ Confirmed all three prerequisite Phase 16 steps (#261, #262, #263) are closed, so the explicit "deny-at-use" dependency is satisfied.
14
+ Produced a four-cycle TDD plan (`isolated` → `extensions` → `skills`/`noSkills`/preload → docs) and committed it.
15
+
16
+ ### Observations
17
+
18
+ - Scope expansion decided with the user: the issue names only `isolated` / `extensions: false` / `noSkills`, but `noSkills` is the single mechanism behind **both** skill-restriction modes (`skills: false` and `skills: string[]` preload).
19
+ Removing `noSkills` without also removing `AgentConfig.skills` would leave a field that silently stops restricting.
20
+ Chose the **collapse-skills-fully** option (symmetric with `extensions`): retire `AgentConfig.skills`, `skill-loader.ts`, `safe-fs.ts` (sole consumer was the skill loader), `preloadSkills`, `PromptExtras`, and `extras.skillBlocks`.
21
+ Children always inherit Pi's full skill system — the `skills: true` path.
22
+ - The recursion guard's `if (cfg.extensions)` gate is removed in the `extensions` cycle (cycle 2), since `SessionConfig.extensions` disappears there.
23
+ A guard-always-runs assertion replaces the deleted "extensions: false skips the filter entirely" test.
24
+ - This is a **breaking** change (`feat!:`): public `SpawnOptions.isolated` and the `isolated:` / `extensions:` / `skills:` custom-agent frontmatter keys are removed.
25
+ Custom agents with legacy frontmatter will silently ignore those keys (matches the Phase 14 precedent for `disallowed_tools`).
26
+ - Sequencing note surfaced to the user: some `isolated`-threading removed here (`RunOptions.isolated`, `Agent.run()` plumbing) is structure that Step 5 (#265, dissolve the runner) will delete anyway — small, unavoidable, and #265 depends on this step, so no reordering benefit.
27
+ - Helper-file churn accepted: `test/helpers/runner-io.ts` is touched in all three removal cycles (one field per cycle); ordering is fixed (`isolated` → `extensions` → `skills`) so no cycle leaves a dangling reference.
28
+ - Doc updates identified: `docs/architecture/architecture.md` (Mermaid session subgraph, directory tree, `SpawnOptions`/`RunOptions` field lists, roadmap status) and the `package-pi-subagents` SKILL.md session-domain row (8 → 6 modules).
29
+
30
+ ## Stage: Implementation — TDD (2026-05-30T00:38:00Z)
31
+
32
+ ### Session summary
33
+
34
+ Executed all four plan cycles (`isolated` → `extensions` + unconditional guard → `skills`/`noSkills`/preload → docs) as three `feat!:` commits plus one `docs:` commit.
35
+ The extension-lifecycle-control axis and the `skills` curation axis are gone; children always inherit the parent's extensions and full skill system, and the recursion guard is unconditional.
36
+ Test count went 1016 → 951 (−65): deleted `skill-loader.test.ts` and `safe-fs.test.ts`, plus removed isolated/extensions/skills/preload-specific cases; `check`, `lint`, `fallow dead-code`, and `verify:public-types` all green.
37
+
38
+ ### Observations
39
+
40
+ - The plan's cycle split held up cleanly.
41
+ The only interleaving friction was `test/config/custom-agents.test.ts`, where `extensions` and `skills` assertions shared the same `it` blocks.
42
+ Handled it by retitling the shared tests to skills-only in cycle 2 (keeping skills compiling/passing), then deleting them in cycle 3 — no dangling references between commits.
43
+ - BSD `sed` (macOS) does not support `\|` alternation in basic regex; the standalone-fixture-line deletions needed `sed -E`.
44
+ Worth remembering for future bulk fixture removals.
45
+ - Two in-scope judgment calls beyond the literal plan: (1) kept the generic tag-rendering test in `result-renderer.test.ts` by swapping the example tag `"isolated"` → `"inherit context"` rather than deleting coverage; (2) removed the now-unused `vi` import from `custom-agents.test.ts` after dropping the extensions-deprecation-warning test.
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
+ - `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
+ - 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": "12.1.0",
3
+ "version": "13.1.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -114,8 +114,6 @@ export class AgentTypeRegistry implements AgentConfigLookup {
114
114
  displayName: "Agent",
115
115
  description: "General-purpose agent for complex, multi-step tasks",
116
116
  builtinToolNames: BUILTIN_TOOL_NAMES,
117
- extensions: true,
118
- skills: true,
119
117
  systemPrompt: "",
120
118
  promptMode: "append",
121
119
  };
@@ -58,8 +58,6 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
58
58
  displayName: str(fm.display_name),
59
59
  description: str(fm.description) ?? name,
60
60
  builtinToolNames: csvList(fm.tools, BUILTIN_TOOL_NAMES),
61
- extensions: resolveBoolExtensions(fm.extensions ?? fm.inherit_extensions),
62
- skills: inheritField(fm.skills ?? fm.inherit_skills),
63
61
  model: str(fm.model),
64
62
  thinking: str(fm.thinking) as ThinkingLevel | undefined,
65
63
  maxTurns: nonNegativeInt(fm.max_turns),
@@ -67,7 +65,6 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
67
65
  promptMode: fm.prompt_mode === "append" ? "append" : "replace",
68
66
  inheritContext: fm.inherit_context != null ? fm.inherit_context === true : undefined,
69
67
  runInBackground: fm.run_in_background != null ? fm.run_in_background === true : undefined,
70
- isolated: fm.isolated != null ? fm.isolated === true : undefined,
71
68
  enabled: fm.enabled !== false, // default true; explicitly false disables
72
69
  source,
73
70
  });
@@ -107,30 +104,3 @@ function csvList(val: unknown, defaults: string[]): string[] {
107
104
  if (val === undefined || val === null) return defaults;
108
105
  return parseCsvField(val) ?? [];
109
106
  }
110
-
111
- /**
112
- * Resolve the `extensions` field to a boolean.
113
- * CSV/array values (legacy allowlist syntax) are coerced to `true` with a warning.
114
- */
115
- function resolveBoolExtensions(val: unknown): boolean {
116
- const result = inheritField(val);
117
- if (Array.isArray(result)) {
118
- console.warn(
119
- "[pi-subagents] extensions allowlist syntax is deprecated — treating as \"true\" (inherit all).\n" +
120
- "Use \"permission:\" frontmatter in pi-permission-system for per-tool access control.",
121
- );
122
- return true;
123
- }
124
- return result;
125
- }
126
-
127
- /**
128
- * Parse an inherit field (extensions, skills).
129
- * omitted/true → true (inherit all); false/"none"/empty → false; csv → listed names.
130
- */
131
- function inheritField(val: unknown): true | string[] | false {
132
- if (val === undefined || val === null || val === true) return true;
133
- if (val === false || val === "none") return false;
134
- const items = csvList(val, []);
135
- return items.length > 0 ? items : false;
136
- }
@@ -16,10 +16,8 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
16
16
  displayName: "Agent",
17
17
  description: "General-purpose agent for complex, multi-step tasks",
18
18
  // builtinToolNames omitted — means "all available tools" (resolved at lookup time)
19
- // inheritContext / runInBackground / isolated omitted — strategy fields, callers decide per-call.
19
+ // inheritContext / runInBackground omitted — strategy fields, callers decide per-call.
20
20
  // Setting them to false would lock callsite intent (see resolveAgentInvocationConfig in invocation-config.ts).
21
- extensions: true,
22
- skills: true,
23
21
  systemPrompt: "",
24
22
  promptMode: "append",
25
23
  isDefault: true,
@@ -32,8 +30,6 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
32
30
  displayName: "Explore",
33
31
  description: "Fast codebase exploration agent (read-only)",
34
32
  builtinToolNames: READ_ONLY_TOOLS,
35
- extensions: true,
36
- skills: true,
37
33
  model: "anthropic/claude-haiku-4-5-20251001",
38
34
  systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
39
35
  You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
@@ -74,8 +70,6 @@ Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find,
74
70
  displayName: "Plan",
75
71
  description: "Software architect for implementation planning (read-only)",
76
72
  builtinToolNames: READ_ONLY_TOOLS,
77
- extensions: true,
78
- skills: true,
79
73
  systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
80
74
  You are a software architect and planning specialist.
81
75
  Your role is EXCLUSIVELY to explore the codebase and design implementation plans.
@@ -6,7 +6,6 @@ interface AgentInvocationParams {
6
6
  max_turns?: number;
7
7
  run_in_background?: boolean;
8
8
  inherit_context?: boolean;
9
- isolated?: boolean;
10
9
  }
11
10
 
12
11
  export function resolveAgentInvocationConfig(
@@ -19,7 +18,6 @@ export function resolveAgentInvocationConfig(
19
18
  maxTurns?: number;
20
19
  inheritContext: boolean;
21
20
  runInBackground: boolean;
22
- isolated: boolean;
23
21
  } {
24
22
  return {
25
23
  modelInput: agentConfig?.model ?? params.model,
@@ -28,6 +26,5 @@ export function resolveAgentInvocationConfig(
28
26
  maxTurns: agentConfig?.maxTurns ?? params.max_turns,
29
27
  inheritContext: agentConfig?.inheritContext ?? params.inherit_context ?? false,
30
28
  runInBackground: agentConfig?.runInBackground ?? params.run_in_background ?? false,
31
- isolated: agentConfig?.isolated ?? params.isolated ?? false,
32
29
  };
33
30
  }
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";
@@ -38,7 +38,6 @@ import { detectEnv } from "#src/session/env";
38
38
  import { resolveModel } from "#src/session/model-resolver";
39
39
  import { buildAgentPrompt } from "#src/session/prompts";
40
40
  import { deriveSubagentSessionDir } from "#src/session/session-dir";
41
- import { preloadSkills } from "#src/session/skill-loader";
42
41
  import { SettingsManager } from "#src/settings";
43
42
  import { AgentTool } from "#src/tools/agent-tool";
44
43
  import { GetResultTool } from "#src/tools/get-result-tool";
@@ -133,7 +132,7 @@ export default function (pi: ExtensionAPI) {
133
132
  },
134
133
  };
135
134
 
136
- const runnerDeps: RunnerDeps = {
135
+ const subagentSessionDeps: SubagentSessionDeps = {
137
136
  io: {
138
137
  detectEnv,
139
138
  getAgentDir,
@@ -143,7 +142,6 @@ export default function (pi: ExtensionAPI) {
143
142
  createSettingsManager: (cwd, dir) => SdkSettingsManager.create(cwd, dir),
144
143
  createSession: (opts) => createAgentSession(opts as any),
145
144
  assemblerIO: {
146
- preloadSkills,
147
145
  buildAgentPrompt,
148
146
  },
149
147
  },
@@ -164,7 +162,7 @@ export default function (pi: ExtensionAPI) {
164
162
  );
165
163
 
166
164
  const manager = new AgentManager({
167
- runner: new ConcreteAgentRunner(runnerDeps),
165
+ createSubagentSession: (params) => createSubagentSession(params, subagentSessionDeps),
168
166
  baseCwd: process.cwd(),
169
167
  observer,
170
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). */
@@ -41,7 +43,6 @@ export interface AgentSpawnConfig {
41
43
  description: string;
42
44
  model?: Model<any>;
43
45
  maxTurns?: number;
44
- isolated?: boolean;
45
46
  inheritContext?: boolean;
46
47
  thinkingLevel?: ThinkingLevel;
47
48
  isBackground?: boolean;
@@ -65,7 +66,7 @@ export class AgentManager {
65
66
  private agents = new Map<string, Agent>();
66
67
  private cleanupInterval: ReturnType<typeof setInterval>;
67
68
  private readonly observer?: AgentManagerObserver;
68
- private readonly runner: AgentRunner;
69
+ private readonly createSubagentSession: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
69
70
  private readonly queue: ConcurrencyQueue;
70
71
  private readonly baseCwd: string;
71
72
  private getRunConfig?: () => RunConfig;
@@ -77,7 +78,7 @@ export class AgentManager {
77
78
  }
78
79
 
79
80
  constructor(options: AgentManagerOptions) {
80
- this.runner = options.runner;
81
+ this.createSubagentSession = options.createSubagentSession;
81
82
  this.queue = options.queue;
82
83
  this.baseCwd = options.baseCwd;
83
84
  this.observer = options.observer;
@@ -149,12 +150,11 @@ export class AgentManager {
149
150
  prompt,
150
151
  model: options.model,
151
152
  maxTurns: options.maxTurns,
152
- isolated: options.isolated,
153
153
  thinkingLevel: options.thinkingLevel,
154
154
  parentSession: options.parentSession,
155
155
  signal: options.signal,
156
156
  // Shared deps
157
- runner: this.runner,
157
+ createSubagentSession: this.createSubagentSession,
158
158
  observer: this.buildObserver(options),
159
159
  getRunConfig: this.getRunConfig,
160
160
  baseCwd: this.baseCwd,
@@ -233,8 +233,7 @@ export class AgentManager {
233
233
 
234
234
  /** Dispose a record's session and remove it from the map. */
235
235
  private removeRecord(id: string, record: Agent): void {
236
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- dispose may not exist on all session implementations
237
- record.session?.dispose?.();
236
+ record.disposeSession();
238
237
  this.agents.delete(id);
239
238
  }
240
239
 
@@ -308,7 +307,7 @@ export class AgentManager {
308
307
  // Clear queue
309
308
  this.queue.clear();
310
309
  for (const record of this.agents.values()) {
311
- record.session?.dispose();
310
+ record.disposeSession();
312
311
  }
313
312
  this.agents.clear();
314
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. */
@@ -81,7 +82,6 @@ export interface AgentInit {
81
82
  prompt?: string;
82
83
  model?: Model<any>;
83
84
  maxTurns?: number;
84
- isolated?: boolean;
85
85
  thinkingLevel?: ThinkingLevel;
86
86
  parentSession?: ParentSessionInfo;
87
87
  isBackground?: boolean;
@@ -127,7 +127,7 @@ export class Agent {
127
127
  promise?: Promise<void>;
128
128
 
129
129
  // Shared deps — optional (required for run())
130
- private readonly _runner?: AgentRunner;
130
+ private readonly _createSubagentSession?: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
131
131
  readonly observer?: AgentLifecycleObserver;
132
132
  private readonly _getRunConfig?: () => RunConfig;
133
133
  private readonly _getWorkspaceProvider?: () => WorkspaceProvider | undefined;
@@ -140,13 +140,13 @@ export class Agent {
140
140
  private readonly _prompt?: string;
141
141
  private readonly _model?: Model<any>;
142
142
  private readonly _maxTurns?: number;
143
- private readonly _isolated?: boolean;
144
143
  private readonly _thinkingLevel?: ThinkingLevel;
145
144
  private readonly _parentSession?: ParentSessionInfo;
146
145
  private readonly _signal?: AbortSignal;
147
146
 
148
147
  // Phase-specific collaborators — each born complete when their info becomes available
149
- execution?: ExecutionState;
148
+ /** The born-complete child session — set when the factory returns inside run(). */
149
+ subagentSession?: SubagentSession;
150
150
  notification?: NotificationState;
151
151
 
152
152
  // Steer buffer — messages queued before the session is ready
@@ -156,12 +156,12 @@ export class Agent {
156
156
 
157
157
  /** The active agent session, or undefined before the session is created. */
158
158
  get session(): AgentSession | undefined {
159
- return this.execution?.session;
159
+ return this.subagentSession?.session;
160
160
  }
161
161
 
162
162
  /** Path to the agent's session JSONL file, or undefined if not yet available. */
163
163
  get outputFile(): string | undefined {
164
- return this.execution?.outputFile;
164
+ return this.subagentSession?.outputFile;
165
165
  }
166
166
 
167
167
  constructor(init: AgentInit) {
@@ -187,7 +187,7 @@ export class Agent {
187
187
  this.abortController = new AbortController();
188
188
 
189
189
  // Shared deps
190
- this._runner = init.runner;
190
+ this._createSubagentSession = init.createSubagentSession;
191
191
  this.observer = init.observer;
192
192
  this._getRunConfig = init.getRunConfig;
193
193
  this._getWorkspaceProvider = init.getWorkspaceProvider;
@@ -198,7 +198,6 @@ export class Agent {
198
198
  this._prompt = init.prompt;
199
199
  this._model = init.model;
200
200
  this._maxTurns = init.maxTurns;
201
- this._isolated = init.isolated;
202
201
  this._thinkingLevel = init.thinkingLevel;
203
202
  this._parentSession = init.parentSession;
204
203
  this._signal = init.signal;
@@ -210,16 +209,16 @@ export class Agent {
210
209
  }
211
210
 
212
211
  /**
213
- * Execute the full agent lifecycle: workspace preparation, runner invocation,
214
- * 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
215
214
  * status transitions.
216
215
  *
217
- * Requires runner and snapshot to be set at construction.
216
+ * Requires the session factory and snapshot to be set at construction.
218
217
  * The returned promise always resolves (errors are captured internally).
219
218
  */
220
219
  async run(): Promise<void> {
221
- if (!this._runner) {
222
- 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");
223
222
  }
224
223
  if (!this._snapshot || !this._prompt) {
225
224
  throw new Error("Agent not configured for execution — missing snapshot or prompt");
@@ -250,30 +249,35 @@ export class Agent {
250
249
  return;
251
250
  }
252
251
 
253
- const runConfig = this._getRunConfig?.();
254
252
  try {
255
- const result = await this._runner.run(this._snapshot, this.type, this._prompt, {
256
- context: {
257
- cwd,
258
- parentSession: this._parentSession,
259
- },
253
+ this.subagentSession = await this._createSubagentSession({
254
+ snapshot: this._snapshot,
255
+ type: this.type,
256
+ cwd,
257
+ parentSession: this._parentSession,
260
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, {
261
277
  maxTurns: this._maxTurns,
262
278
  defaultMaxTurns: runConfig?.defaultMaxTurns,
263
279
  graceTurns: runConfig?.graceTurns,
264
- isolated: this._isolated,
265
- thinkingLevel: this._thinkingLevel,
266
280
  signal: this.abortController.signal,
267
- onSessionCreated: (session) => {
268
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- sessionManager is typed as always present but Pi SDK may not provide it
269
- const outputFile = session.sessionManager?.getSessionFile?.() ?? undefined;
270
- this.execution = { session, outputFile };
271
- this.flushPendingSteers(session);
272
- this.attachObserver(subscribeAgentObserver(session, this, {
273
- onCompact: (r, info) => this.observer?.onCompacted?.(r, info),
274
- }));
275
- this.observer?.onSessionCreated?.(this, session);
276
- },
277
281
  });
278
282
  this.completeRun(result);
279
283
  } catch (err) {
@@ -285,27 +289,24 @@ export class Agent {
285
289
  * Resume an existing session with a new prompt, managing the observer
286
290
  * subscription lifecycle internally (same wiring as run()).
287
291
  *
288
- * 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).
289
293
  * The returned promise always resolves (errors are captured internally).
290
- * The parent signal flows straight through to runner.resume — resume does not
294
+ * The parent signal flows straight through to resumeTurnLoop — resume does not
291
295
  * route through this.abortController.
292
296
  */
293
297
  async resume(prompt: string, signal?: AbortSignal): Promise<void> {
294
- if (!this._runner) {
295
- throw new Error("Agent not configured for execution — missing runner");
296
- }
297
- const session = this.session;
298
- if (!session) {
298
+ const subagentSession = this.subagentSession;
299
+ if (!subagentSession) {
299
300
  throw new Error("Agent not configured for resume — missing session");
300
301
  }
301
302
 
302
303
  this.resetForResume(Date.now());
303
- this.attachObserver(subscribeAgentObserver(session, this, {
304
+ this.attachObserver(subscribeAgentObserver(subagentSession.session, this, {
304
305
  onCompact: (r, info) => this.observer?.onCompacted?.(r, info),
305
306
  }));
306
307
 
307
308
  try {
308
- const responseText = await this._runner.resume(session, prompt, { signal });
309
+ const responseText = await subagentSession.resumeTurnLoop(prompt, signal);
309
310
  this.markCompleted(responseText);
310
311
  } catch (err) {
311
312
  this.markError(err);
@@ -411,11 +412,11 @@ export class Agent {
411
412
 
412
413
  /**
413
414
  * Flush all buffered steer messages to the session and clear the buffer.
414
- * Called from onSessionCreated once the session is available.
415
+ * Called once the session is available, delegating to SubagentSession.steer.
415
416
  */
416
- flushPendingSteers(session: AgentSession): void {
417
+ flushPendingSteers(): void {
417
418
  for (const msg of this._pendingSteers) {
418
- session.steer(msg).catch(() => {});
419
+ this.subagentSession?.steer(msg).catch(() => {});
419
420
  }
420
421
  this._pendingSteers = [];
421
422
  }
@@ -455,8 +456,8 @@ export class Agent {
455
456
  this._detachFn = undefined;
456
457
  }
457
458
 
458
- /** Complete a run: release listeners, dispose the workspace, status transition, execution update, notify observer. */
459
- completeRun(result: RunResult): void {
459
+ /** Complete a run: release listeners, dispose the workspace, status transition, notify observer. */
460
+ completeRun(result: TurnLoopResult): void {
460
461
  this.releaseListeners();
461
462
 
462
463
  let finalResult = result.responseText;
@@ -474,14 +475,14 @@ export class Agent {
474
475
  else if (result.steered) this.markSteered(finalResult);
475
476
  else this.markCompleted(finalResult);
476
477
 
477
- this.execution = {
478
- session: result.session,
479
- outputFile: result.sessionFile ?? this.execution?.outputFile,
480
- };
481
-
482
478
  this.observer?.onRunFinished?.(this);
483
479
  }
484
480
 
481
+ /** Dispose the wrapped session, firing the `disposed` lifecycle event. */
482
+ disposeSession(): void {
483
+ this.subagentSession?.dispose();
484
+ }
485
+
485
486
  /** Fail a run: mark error, release listeners, best-effort workspace dispose, notify observer. */
486
487
  failRun(err: unknown): void {
487
488
  this.markError(err);