@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.
- package/CHANGELOG.md +24 -0
- package/dist/public.d.ts +1 -3
- package/docs/architecture/architecture.md +86 -57
- package/docs/plans/0264-remove-extension-lifecycle-control.md +275 -0
- package/docs/plans/0265-born-complete-subagent-session.md +330 -0
- package/docs/retro/0264-remove-extension-lifecycle-control.md +89 -0
- package/docs/retro/0265-born-complete-subagent-session.md +58 -0
- package/package.json +1 -1
- package/src/config/agent-types.ts +0 -2
- package/src/config/custom-agents.ts +0 -30
- package/src/config/default-agents.ts +1 -7
- package/src/config/invocation-config.ts +0 -3
- package/src/index.ts +3 -5
- package/src/lifecycle/agent-manager.ts +9 -10
- package/src/lifecycle/agent.ts +56 -55
- 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/service/service-adapter.ts +0 -1
- package/src/service/service.ts +0 -1
- package/src/session/conversation.ts +49 -0
- package/src/session/prompts.ts +2 -23
- package/src/session/session-config.ts +10 -45
- package/src/settings.ts +1 -1
- package/src/tools/agent-tool.ts +0 -5
- package/src/tools/background-spawner.ts +0 -1
- package/src/tools/foreground-runner.ts +0 -1
- package/src/tools/get-result-tool.ts +1 -1
- package/src/tools/spawn-config.ts +1 -5
- package/src/types.ts +0 -7
- package/src/ui/agent-config-editor.ts +0 -5
- package/src/ui/agent-creation-wizard.ts +0 -4
- package/src/ui/display.ts +1 -2
- package/src/lifecycle/agent-runner.ts +0 -472
- package/src/lifecycle/execution-state.ts +0 -17
- package/src/session/safe-fs.ts +0 -45
- package/src/session/skill-loader.ts +0 -104
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 264
|
|
3
|
+
issue_title: "Remove isolated / extensions:false / noSkills from core"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Remove the extension-lifecycle-control axis from the core
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
The core still carries an extension-lifecycle-control axis — `isolated`, `extensions: false`, and `noSkills` — that lets a spawn blanket-disable a child's extensions and skills.
|
|
11
|
+
Per ADR 0002, this is policy that does not belong in a minimal orchestrator.
|
|
12
|
+
Deny-at-use (the in-child permission layer, shipped in Step 1 / #261) already covers what `isolated` pretended to do for tools.
|
|
13
|
+
Prevent-load (refusing to bind an extension for true sandboxing) is genuinely generative and is deliberately left as a *latent, un-built* provider seam — we do not ship a vacant hook.
|
|
14
|
+
|
|
15
|
+
This is Phase 16, Step 4.
|
|
16
|
+
With the axis gone, children always load the parent's extensions and skills, and the recursion guard — which currently gates on `cfg.extensions` — becomes unconditional.
|
|
17
|
+
|
|
18
|
+
## Goals
|
|
19
|
+
|
|
20
|
+
- Remove `isolated` from the spawn API, `SubagentsService`, the lifecycle plumbing, and the config assembler.
|
|
21
|
+
- Remove the `extensions` boolean from `AgentConfig` and the assembler (children always inherit extensions).
|
|
22
|
+
- Remove `noSkills` from the assembler and the resource-loader options.
|
|
23
|
+
- Collapse the skill-curation axis symmetrically: remove `AgentConfig.skills` and the skill-**preload** path (`skill-loader.ts`, `safe-fs.ts`, `preloadSkills`, `PromptExtras`, `extras.skillBlocks`).
|
|
24
|
+
Children always load Pi's full skill system, exactly as `skills: true` does today.
|
|
25
|
+
- Make the recursion guard unconditional — it always strips `subagent` / `get_subagent_result` / `steer_subagent` from children, keyed off the core's own tool names.
|
|
26
|
+
- This is a **breaking** change: the public `SpawnOptions.isolated` field and the `isolated:` / `extensions:` / `skills:` custom-agent frontmatter keys are removed.
|
|
27
|
+
Suggested commits use `feat!:`.
|
|
28
|
+
|
|
29
|
+
## Non-Goals
|
|
30
|
+
|
|
31
|
+
- Born-complete child execution / dissolving the runner — that is Step 5 (#265) and depends on this step.
|
|
32
|
+
`RunOptions`, `runAgent`, `ConcreteAgentRunner`, and `Agent.run()` survive this issue (with `isolated` removed from them).
|
|
33
|
+
- Shipping a prevent-load provider seam — ADR 0002 leaves it latent until a real consumer needs it.
|
|
34
|
+
- Changing `builtinToolNames` (the `tools:` frontmatter allowlist) — that is a separate, surviving concern.
|
|
35
|
+
- Changing deny-at-use behavior in `@gotgenes/pi-permission-system` — already in place from #261.
|
|
36
|
+
|
|
37
|
+
## Background
|
|
38
|
+
|
|
39
|
+
Relevant modules and how they relate:
|
|
40
|
+
|
|
41
|
+
- `src/types.ts` — declares `AgentConfig` (`extensions`, `skills`, `isolated`) and `AgentInvocation` (`isolated`).
|
|
42
|
+
- `src/config/default-agents.ts` — the three embedded agents set `extensions: true`, `skills: true`.
|
|
43
|
+
- `src/config/custom-agents.ts` — parses `extensions` / `skills` / `isolated` from `.md` frontmatter via `resolveBoolExtensions` and `inheritField`.
|
|
44
|
+
- `src/config/invocation-config.ts` — merges `isolated` from agent config + tool params.
|
|
45
|
+
- `src/config/agent-types.ts` — an absolute-fallback `AgentConfig` literal sets `extensions: true`, `skills: true`.
|
|
46
|
+
- `src/session/session-config.ts` — `assembleSessionConfig` derives `extensions`/`skills` from `options.isolated`, calls `io.preloadSkills` when `skills` is `string[]`, and computes `noSkills`.
|
|
47
|
+
Returns `SessionConfig.{ extensions, noSkills, extras }`.
|
|
48
|
+
- `src/session/prompts.ts` — `buildAgentPrompt` injects `extras.skillBlocks` as `# Preloaded Skill:` sections; `PromptExtras` carries only `skillBlocks` (memory was removed in #185).
|
|
49
|
+
- `src/session/skill-loader.ts` — `preloadSkills` reads named skill files from disk; consumes `safe-fs.ts`.
|
|
50
|
+
- `src/session/safe-fs.ts` — symlink/path-traversal guards; **only consumer is `skill-loader.ts`**.
|
|
51
|
+
- `src/lifecycle/agent-runner.ts` — `RunOptions.isolated` flows into the assembler; `createResourceLoader` is called with `noExtensions: !cfg.extensions` and `noSkills: cfg.noSkills`; the recursion guard runs only `if (cfg.extensions)`.
|
|
52
|
+
`ResourceLoaderOptions` (a local narrow interface, not the SDK type) declares `noExtensions?` / `noSkills?`.
|
|
53
|
+
- `src/lifecycle/agent.ts` — `AgentInit.isolated`, the `_isolated` field, and `isolated:` in the `runner.run(...)` call.
|
|
54
|
+
- `src/lifecycle/agent-manager.ts` — `AgentSpawnConfig.isolated` and its pass-through to `new Agent(...)`.
|
|
55
|
+
- `src/tools/{agent-tool,spawn-config,foreground-runner,background-spawner}.ts` — tool schema `isolated`, `SpawnExecution.isolated`, and pass-through.
|
|
56
|
+
- `src/service/{service,service-adapter}.ts` — public `SpawnOptions.isolated` and its pass-through to the manager.
|
|
57
|
+
- `src/ui/{display,agent-config-editor,agent-creation-wizard}.ts` — `isolated` tag, eject-content emission, and generation-prompt template text.
|
|
58
|
+
- `src/index.ts` — wires `preloadSkills` and `buildAgentPrompt` into `assemblerIO`.
|
|
59
|
+
|
|
60
|
+
AGENTS.md constraints that apply:
|
|
61
|
+
|
|
62
|
+
- Conventional Commits; breaking changes use `feat!:`.
|
|
63
|
+
- Do not edit `CHANGELOG.md` (release-please owns it).
|
|
64
|
+
- When removing an export, grep all `src/` and `test/` for the symbol before finalizing (done below).
|
|
65
|
+
- When adding/removing a module, check `docs/architecture/` for layout listings and complexity tables that reference it (done below — Mermaid + tree + field tables).
|
|
66
|
+
- One-sentence-per-line markdown; sequential numbering restarting per heading.
|
|
67
|
+
|
|
68
|
+
## Design Overview
|
|
69
|
+
|
|
70
|
+
This is a **symmetric collapse**, not a refactor.
|
|
71
|
+
Two parallel axes leave the core together:
|
|
72
|
+
|
|
73
|
+
| Axis | Today | After |
|
|
74
|
+
| ---------- | -------------------------------------------------------------- | --------------------------------------------- |
|
|
75
|
+
| Extensions | `extensions: true \| false`; `isolated` forces `false` | always inherit (field gone) |
|
|
76
|
+
| Skills | `skills: true \| string[] \| false`; `isolated` forces `false` | always inherit full skill system (field gone) |
|
|
77
|
+
|
|
78
|
+
The `skills` field collapses for the same reason `extensions` does: `noSkills` is the single mechanism behind **both** restriction modes (`skills: false` → no skills; `skills: string[]` → only those, preloaded into the prompt with the SDK loader suppressed).
|
|
79
|
+
Removing `noSkills` without removing `AgentConfig.skills` would leave a field that silently stops restricting — a `string[]` agent would get its baked-in skills *plus* the full system.
|
|
80
|
+
ADR 0002 says children always load the parent's skills, which is exactly the `skills: true` path; the other two values are skill curation/policy and leave the core.
|
|
81
|
+
|
|
82
|
+
### Assembler after collapse
|
|
83
|
+
|
|
84
|
+
`assembleSessionConfig` loses the `isolated` branch, the `preloadSkills` block, and the `noSkills`/`extensions`/`extras` outputs:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
export interface AssemblerOptions {
|
|
88
|
+
cwd?: string;
|
|
89
|
+
model?: unknown;
|
|
90
|
+
thinkingLevel?: ThinkingLevel;
|
|
91
|
+
// isolated removed
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SessionConfig {
|
|
95
|
+
effectiveCwd: string;
|
|
96
|
+
systemPrompt: string;
|
|
97
|
+
toolNames: string[];
|
|
98
|
+
model: unknown;
|
|
99
|
+
thinkingLevel: ThinkingLevel | undefined;
|
|
100
|
+
agentMaxTurns: number | undefined;
|
|
101
|
+
// extensions, noSkills, extras removed
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`AssemblerIO` drops `preloadSkills`, keeping only `buildAgentPrompt` (now called without `extras`).
|
|
106
|
+
|
|
107
|
+
### Runner after collapse
|
|
108
|
+
|
|
109
|
+
The resource-loader call drops the two suppression flags, and the guard runs unconditionally:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
const loader = deps.io.createResourceLoader({
|
|
113
|
+
cwd: cfg.effectiveCwd,
|
|
114
|
+
agentDir,
|
|
115
|
+
noPromptTemplates: true,
|
|
116
|
+
noThemes: true,
|
|
117
|
+
noContextFiles: true,
|
|
118
|
+
systemPromptOverride: () => cfg.systemPrompt,
|
|
119
|
+
appendSystemPromptOverride: () => [],
|
|
120
|
+
// noExtensions, noSkills removed
|
|
121
|
+
});
|
|
122
|
+
// ...
|
|
123
|
+
await session.bindExtensions({});
|
|
124
|
+
// Recursion guard — now unconditional (children always load extensions).
|
|
125
|
+
const filtered = filterActiveTools(session.getActiveToolNames());
|
|
126
|
+
session.setActiveToolsByName(filtered);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`ResourceLoaderOptions` drops `noExtensions?` / `noSkills?` (a local narrow interface — removing the latent fields keeps us honest per ADR 0002's "no vacant hooks").
|
|
130
|
+
|
|
131
|
+
### Call-site sketch — recursion guard (Tell-Don't-Ask check)
|
|
132
|
+
|
|
133
|
+
The guard already asks the session for its active tools and tells it the filtered set — unchanged except for removing the `if`.
|
|
134
|
+
No new collaborator, no reach-through; the only structural change is that `cfg.extensions` is no longer consulted, so `SessionConfig` no longer needs to expose it.
|
|
135
|
+
This is a narrowing of the assembler's output contract, which is the desired direction.
|
|
136
|
+
|
|
137
|
+
### Edge cases
|
|
138
|
+
|
|
139
|
+
- Append-mode agents: `parentSystemPrompt` still embeds parent context via `systemPromptOverride`; nothing about that path depends on skills/extensions flags.
|
|
140
|
+
- Custom agents with legacy `extensions:` / `skills:` / `isolated:` frontmatter: the keys are silently ignored after this change (no parse, no warning).
|
|
141
|
+
`resolveBoolExtensions` already warned on the deprecated allowlist syntax; that warning path is removed with the parser.
|
|
142
|
+
- The absolute-fallback `AgentConfig` in `agent-types.ts` drops `extensions`/`skills` like every other construction site.
|
|
143
|
+
|
|
144
|
+
## Module-Level Changes
|
|
145
|
+
|
|
146
|
+
### Source — `isolated` axis
|
|
147
|
+
|
|
148
|
+
- `src/types.ts` — remove `AgentConfig.isolated`, `AgentInvocation.isolated`.
|
|
149
|
+
- `src/config/invocation-config.ts` — remove `isolated` from `AgentInvocationParams` and the return object.
|
|
150
|
+
- `src/config/custom-agents.ts` — remove the `isolated:` frontmatter parse.
|
|
151
|
+
- `src/session/session-config.ts` — remove `AssemblerOptions.isolated` and the `options.isolated ? false : ...` derivation (read `agentConfig.extensions`/`agentConfig.skills` directly, for now).
|
|
152
|
+
- `src/lifecycle/agent-runner.ts` — remove `RunOptions.isolated` and the `isolated:` argument passed to `assembleSessionConfig`.
|
|
153
|
+
- `src/lifecycle/agent.ts` — remove `AgentInit.isolated`, the `_isolated` field + constructor assignment, and `isolated:` in the `runner.run(...)` call.
|
|
154
|
+
- `src/lifecycle/agent-manager.ts` — remove `AgentSpawnConfig.isolated` and its pass-through to `new Agent(...)`.
|
|
155
|
+
- `src/tools/agent-tool.ts` — remove the `isolated` schema property.
|
|
156
|
+
- `src/tools/spawn-config.ts` — remove `SpawnExecution.isolated`, the `const isolated = resolvedConfig.isolated`, `isolated` in `agentInvocation`, and `isolated` in the `execution` return.
|
|
157
|
+
- `src/tools/foreground-runner.ts` / `src/tools/background-spawner.ts` — remove `isolated: execution.isolated`.
|
|
158
|
+
- `src/service/service.ts` — remove `SpawnOptions.isolated`.
|
|
159
|
+
- `src/service/service-adapter.ts` — remove `isolated: options?.isolated` from the `manager.spawn(...)` call.
|
|
160
|
+
- `src/ui/display.ts` — remove `if (invocation.isolated) tags.push("isolated")` and the `isolated` mention in the JSDoc example.
|
|
161
|
+
- `src/ui/agent-config-editor.ts` — remove the `isolated: true` line from `buildEjectContent`.
|
|
162
|
+
- `src/ui/agent-creation-wizard.ts` — remove the `isolated:` template line and the "Set isolated: true …" guideline.
|
|
163
|
+
|
|
164
|
+
### Source — `extensions` axis + unconditional guard
|
|
165
|
+
|
|
166
|
+
- `src/types.ts` — remove `AgentConfig.extensions`.
|
|
167
|
+
- `src/config/default-agents.ts` — remove `extensions: true` from all three agents.
|
|
168
|
+
- `src/config/agent-types.ts` — remove `extensions: true` from the absolute-fallback literal.
|
|
169
|
+
- `src/config/custom-agents.ts` — remove the `extensions:` frontmatter parse and delete `resolveBoolExtensions`.
|
|
170
|
+
- `src/session/session-config.ts` — remove `SessionConfig.extensions` and stop assigning it.
|
|
171
|
+
- `src/lifecycle/agent-runner.ts` — remove `ResourceLoaderOptions.noExtensions`, drop `noExtensions` from the `createResourceLoader` call, and make the recursion guard unconditional (delete `if (cfg.extensions)`); update the explanatory comment.
|
|
172
|
+
- `src/ui/agent-config-editor.ts` — remove the `extensions: false` line from `buildEjectContent`.
|
|
173
|
+
- `src/ui/agent-creation-wizard.ts` — remove the `extensions:` template line.
|
|
174
|
+
|
|
175
|
+
### Source — `skills` axis + preload path
|
|
176
|
+
|
|
177
|
+
- `src/types.ts` — remove `AgentConfig.skills`.
|
|
178
|
+
- `src/config/default-agents.ts` — remove `skills: true` from all three agents.
|
|
179
|
+
- `src/config/agent-types.ts` — remove `skills: true` from the absolute-fallback literal.
|
|
180
|
+
- `src/config/custom-agents.ts` — remove the `skills:` frontmatter parse and delete `inheritField` (its only remaining callers are `skills`/`extensions`; `csvList`, `parseCsvField`, `str`, `nonNegativeInt` stay — `csvList` still serves `tools:`).
|
|
181
|
+
- `src/session/session-config.ts` — remove `AssemblerIO.preloadSkills`, `SessionConfig.noSkills`, `SessionConfig.extras`, the `extras`/`preloadSkills` block, and the `extras` argument to `buildAgentPrompt`.
|
|
182
|
+
- `src/session/prompts.ts` — remove `PromptExtras`, the `extras` parameter, and the `extrasSuffix` logic.
|
|
183
|
+
- `src/session/skill-loader.ts` — **delete** (export `preloadSkills`, `PreloadedSkill`).
|
|
184
|
+
- `src/session/safe-fs.ts` — **delete** (sole consumer was `skill-loader.ts`).
|
|
185
|
+
- `src/lifecycle/agent-runner.ts` — remove `ResourceLoaderOptions.noSkills` and drop `noSkills` from the `createResourceLoader` call.
|
|
186
|
+
- `src/index.ts` — remove the `preloadSkills` import and its `assemblerIO.preloadSkills` wiring.
|
|
187
|
+
- `src/ui/agent-config-editor.ts` — remove the `skills: false` / `skills: <list>` lines from `buildEjectContent`.
|
|
188
|
+
- `src/ui/agent-creation-wizard.ts` — remove the `skills:` template line.
|
|
189
|
+
|
|
190
|
+
### Tests
|
|
191
|
+
|
|
192
|
+
- `test/session/skill-loader.test.ts` — **delete**.
|
|
193
|
+
- `test/session/safe-fs.test.ts` — **delete**.
|
|
194
|
+
- `test/session/session-config.test.ts` — remove the `isolated`-mode `describe`, the `noSkills` assertions, and the preload tests; keep model-resolution and prompt-assembly assertions.
|
|
195
|
+
- `test/session/prompts.test.ts` — remove `isolated`/`extensions`/`skills` from `AgentConfig` fixtures and any `skillBlocks`/`extras` cases.
|
|
196
|
+
- `test/config/invocation-config.test.ts` — remove `isolated` cases and fixture fields.
|
|
197
|
+
- `test/config/custom-agents.test.ts` — remove `extensions` / `skills` / `isolated` frontmatter-parsing tests.
|
|
198
|
+
- `test/config/agent-types.test.ts` — remove `extensions` / `skills` / `isolated` from fixtures.
|
|
199
|
+
- `test/lifecycle/agent-runner-extension-tools.test.ts` — remove `extensions`/`skills`/`isolated` from the mock config; delete the "extensions: false skips the filter entirely" test; keep/adjust the post-bind guard tests to assert the guard runs unconditionally.
|
|
200
|
+
- `test/tools/spawn-config.test.ts` — remove the "sets isolated from params" test and `isolated` fixture fields.
|
|
201
|
+
- `test/tools/background-spawner.test.ts` / `test/tools/foreground-runner.test.ts` — remove `isolated` from fixtures and `agentInvocation` assertions.
|
|
202
|
+
- `test/tools/result-renderer.test.ts` — remove the `"isolated"` tag case.
|
|
203
|
+
- `test/ui/agent-config-editor.test.ts` — remove the `isolated` / `extensions: false` eject-emission tests.
|
|
204
|
+
- `test/display.test.ts` — remove `extensions`/`skills` from `AgentConfig` fixtures.
|
|
205
|
+
- `test/helpers/runner-io.ts` — remove `extensions`/`skills`/`isolated` from `DEFAULT_AGENT_CONFIG`.
|
|
206
|
+
- `test/helpers/runner-io.test.ts` — remove the `config.extensions`/`config.skills` assertions and the `extensions: true` override case.
|
|
207
|
+
- `test/helpers/ui-stubs.ts` — remove `extensions: true` / `skills: true` from the stub `AgentConfig`.
|
|
208
|
+
|
|
209
|
+
### Docs
|
|
210
|
+
|
|
211
|
+
- `docs/architecture/architecture.md` —
|
|
212
|
+
remove the `SafeFs` and `SkillLoader` nodes from the session-domain Mermaid subgraph;
|
|
213
|
+
remove `safe-fs.ts` and `skill-loader.ts` from the directory-tree listing;
|
|
214
|
+
drop `isolated` from the `SpawnOptions` field list (≈ line 418) and the `RunOptions` field list (≈ line 651);
|
|
215
|
+
mark Phase 16 Step 4 (#264) done in the roadmap and update the "Children always load the parent's extensions and skills" note;
|
|
216
|
+
reflect the smaller session domain (8 → 6 modules).
|
|
217
|
+
- `.pi/skills/package-pi-subagents/SKILL.md` — update the Session domain row (module count 8 → 6, drop `safe-fs.ts` / `skill-loader.ts` from the file list).
|
|
218
|
+
|
|
219
|
+
## Test Impact Analysis
|
|
220
|
+
|
|
221
|
+
1. New coverage enabled — minimal (this is a removal).
|
|
222
|
+
The one behavioral assertion worth strengthening: the post-bind recursion guard now runs **unconditionally**.
|
|
223
|
+
Update `agent-runner-extension-tools.test.ts` so a case that previously relied on `extensions: true` instead asserts the guard always calls `setActiveToolsByName` and always excludes `EXCLUDED_TOOL_NAMES`, with no config dependence.
|
|
224
|
+
2. Tests that become redundant and are removed —
|
|
225
|
+
`skill-loader.test.ts` and `safe-fs.test.ts` (modules deleted);
|
|
226
|
+
the `isolated`-mode `describe` in `session-config.test.ts`;
|
|
227
|
+
the "extensions: false skips the filter entirely" case;
|
|
228
|
+
`isolated` parameter tests in `spawn-config.test.ts` / `invocation-config.test.ts`;
|
|
229
|
+
`extensions` / `skills` frontmatter-parsing tests in `custom-agents.test.ts`.
|
|
230
|
+
3. Tests that must stay (genuinely exercise surviving layers) —
|
|
231
|
+
prompt assembly (`prompts.test.ts`, minus `extras`);
|
|
232
|
+
model resolution in `session-config.test.ts`;
|
|
233
|
+
the post-bind guard ordering tests (adjusted, not removed);
|
|
234
|
+
custom-agent `tools:` parsing (`csvList` survives).
|
|
235
|
+
|
|
236
|
+
## TDD Order
|
|
237
|
+
|
|
238
|
+
Each cycle ends green (`pnpm run check` + `pnpm -r run test`).
|
|
239
|
+
Because removing a field from `AgentConfig` / `AgentInvocation` breaks every reader and every object-literal construction site at once (TS excess-property + property-access), each cycle folds its test updates into the same commit (per the testing skill's removal rule).
|
|
240
|
+
The three axes are split so each commit stays reviewable and leaves the repo compiling.
|
|
241
|
+
|
|
242
|
+
1. Remove the `isolated` axis end-to-end.
|
|
243
|
+
Surface: `types.ts` (`AgentConfig.isolated`, `AgentInvocation.isolated`), `invocation-config.ts`, `custom-agents.ts` (parse only), `session-config.ts` (`AssemblerOptions.isolated` + derivation), `agent-runner.ts` (`RunOptions.isolated`), `agent.ts`, `agent-manager.ts`, `tools/*`, `service*.ts`, `ui/*` (isolated bits), plus all listed tests.
|
|
244
|
+
After: `extensions`/`skills`/`noSkills` still function; the assembler reads `agentConfig.extensions`/`agentConfig.skills` directly.
|
|
245
|
+
Commit: `feat!: remove isolated from the subagent spawn API and lifecycle (#264)`.
|
|
246
|
+
2. Remove the `extensions` axis; make the recursion guard unconditional.
|
|
247
|
+
Surface: `types.ts` (`AgentConfig.extensions`), `default-agents.ts`, `agent-types.ts` (fallback), `custom-agents.ts` (parse + delete `resolveBoolExtensions`), `session-config.ts` (`SessionConfig.extensions`), `agent-runner.ts` (`ResourceLoaderOptions.noExtensions`, drop `noExtensions` arg, unconditional guard), `ui/agent-config-editor.ts` + `ui/agent-creation-wizard.ts` (extensions bits), plus tests (including the guard-always-runs update and deleting the "skips filter" case).
|
|
248
|
+
Commit: `feat!: always inherit extensions; make the recursion guard unconditional (#264)`.
|
|
249
|
+
3. Remove the `skills` axis, `noSkills`, and the skill-preload path.
|
|
250
|
+
Surface: `types.ts` (`AgentConfig.skills`), `default-agents.ts`, `agent-types.ts` (fallback), `custom-agents.ts` (parse + delete `inheritField`), `session-config.ts` (`AssemblerIO.preloadSkills`, `SessionConfig.noSkills`/`extras`, preload block, `buildAgentPrompt` call), `prompts.ts` (`PromptExtras`/`extras`/`extrasSuffix`), delete `skill-loader.ts` + `safe-fs.ts`, `agent-runner.ts` (`ResourceLoaderOptions.noSkills`, drop arg), `index.ts` (wiring), `ui/*` (skills bits), plus tests (delete `skill-loader.test.ts` + `safe-fs.test.ts`).
|
|
251
|
+
Commit: `feat!: always inherit skills; remove noSkills and the skill-preload path (#264)`.
|
|
252
|
+
4. Update the architecture doc and package skill.
|
|
253
|
+
Surface: `docs/architecture/architecture.md` (Mermaid session subgraph, directory tree, `SpawnOptions`/`RunOptions` field lists, roadmap status), `.pi/skills/package-pi-subagents/SKILL.md` (session domain row).
|
|
254
|
+
Commit: `docs: record removal of the extension-lifecycle-control axis (#264)`.
|
|
255
|
+
|
|
256
|
+
## Risks and Mitigations
|
|
257
|
+
|
|
258
|
+
- Risk: `resolveBoolExtensions` / `inheritField` deletion removes a helper still used elsewhere.
|
|
259
|
+
Mitigation: greps confirm `resolveBoolExtensions` is used only by the removed `extensions` parse, and `inheritField` only by `extensions`/`skills`; `csvList`/`parseCsvField` (used by `tools:`) stay.
|
|
260
|
+
- Risk: deleting `safe-fs.ts` orphans an import.
|
|
261
|
+
Mitigation: grep confirms `skill-loader.ts` is its sole consumer; `safe-fs.test.ts` is deleted in the same cycle.
|
|
262
|
+
- Risk: removing `SpawnOptions.isolated` breaks a published consumer of the service.
|
|
263
|
+
Mitigation: this is an intentional breaking change (ADR 0002); `feat!:` triggers a major bump via release-please.
|
|
264
|
+
The public type surface is verified by `pnpm run verify:public-types` after Step 3.
|
|
265
|
+
- Risk: skill behavior silently changes for agents that relied on `skills: string[]` curation.
|
|
266
|
+
Mitigation: documented in Goals as breaking; children now inherit the full skill system, which is strictly more capable, and deny-at-use governs what they may act on.
|
|
267
|
+
- Risk: a test file is touched in multiple cycles (e.g. `runner-io.ts`).
|
|
268
|
+
Mitigation: each cycle removes only the field it owns; ordering is fixed (isolated → extensions → skills) so no cycle leaves a dangling reference.
|
|
269
|
+
|
|
270
|
+
## Open Questions
|
|
271
|
+
|
|
272
|
+
- Should custom-agent `.md` files with now-defunct `extensions:` / `skills:` / `isolated:` frontmatter emit a one-time deprecation warning, or be silently ignored?
|
|
273
|
+
Deferred: silent-ignore matches the Phase 14 precedent for the removed `disallowed_tools` field; revisit only if users report confusion.
|
|
274
|
+
- Does `verify:public-types` need a new negative assertion that `isolated` is absent from `SpawnOptions`?
|
|
275
|
+
Deferred to Step 3 implementation — the existing consumer type-check will fail if a stale field lingers.
|
|
@@ -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.
|