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