@gotgenes/pi-subagents 10.2.0 → 11.0.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.
@@ -15,16 +15,32 @@
15
15
  * after construction as lifecycle information becomes available.
16
16
  */
17
17
 
18
+ import type { Model } from "@earendil-works/pi-ai";
18
19
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
19
20
  import { debugLog } from "#src/debug";
20
- import type { RunResult } from "#src/lifecycle/agent-runner";
21
+ import type { AgentRunner, RunResult } from "#src/lifecycle/agent-runner";
21
22
  import type { ExecutionState } from "#src/lifecycle/execution-state";
23
+ import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
22
24
  import type { LifetimeUsage } from "#src/lifecycle/usage";
23
25
  import { addUsage } from "#src/lifecycle/usage";
24
26
  import type { WorktreeManager } from "#src/lifecycle/worktree";
25
27
  import { WorktreeState } from "#src/lifecycle/worktree-state";
26
- import type { NotificationState } from "#src/observation/notification-state";
27
- import type { AgentInvocation, IsolationMode, SubagentType } from "#src/types";
28
+ import { NotificationState } from "#src/observation/notification-state";
29
+ import { subscribeAgentObserver } from "#src/observation/record-observer";
30
+ import type { RunConfig } from "#src/runtime";
31
+ import type { AgentInvocation, CompactionInfo, IsolationMode, ParentSessionInfo, SubagentType, ThinkingLevel } from "#src/types";
32
+
33
+ /** Per-agent lifecycle observer — created by AgentManager for each spawn. */
34
+ export interface AgentLifecycleObserver {
35
+ /** Fires when the agent transitions to running (inside run(), after markRunning). */
36
+ onStarted?(agent: Agent): void;
37
+ /** Fires when the runner creates the session — delivers the session to external consumers. */
38
+ onSessionCreated?(agent: Agent, session: AgentSession): void;
39
+ /** Fires once when the run completes or fails (for concurrency drain). */
40
+ onRunFinished?(agent: Agent): void;
41
+ /** Fires on compaction events during the run. */
42
+ onCompacted?(agent: Agent, info: CompactionInfo): void;
43
+ }
28
44
 
29
45
  export type AgentStatus =
30
46
  | "queued"
@@ -36,17 +52,36 @@ export type AgentStatus =
36
52
  | "error";
37
53
 
38
54
  export interface AgentInit {
55
+ // Identity
39
56
  id: string;
40
57
  type: SubagentType;
41
58
  description: string;
59
+ invocation?: AgentInvocation;
60
+
61
+ // Status (for tests and restore scenarios)
42
62
  status?: AgentStatus;
43
63
  startedAt?: number;
44
64
  completedAt?: number;
45
65
  result?: string;
46
66
  error?: string;
47
- abortController?: AbortController;
48
- invocation?: AgentInvocation;
49
- promise?: Promise<void>;
67
+
68
+ // Shared deps (required for run(), optional for tests)
69
+ runner?: AgentRunner;
70
+ worktrees?: WorktreeManager;
71
+ observer?: AgentLifecycleObserver;
72
+ getRunConfig?: () => RunConfig;
73
+
74
+ // Run config (required for run(), optional for tests)
75
+ snapshot?: ParentSnapshot;
76
+ prompt?: string;
77
+ model?: Model<any>;
78
+ maxTurns?: number;
79
+ isolated?: boolean;
80
+ thinkingLevel?: ThinkingLevel;
81
+ isolation?: IsolationMode;
82
+ parentSession?: ParentSessionInfo;
83
+ isBackground?: boolean;
84
+ signal?: AbortSignal;
50
85
  }
51
86
 
52
87
  export class Agent {
@@ -82,11 +117,28 @@ export class Agent {
82
117
  private _compactionCount: number;
83
118
  get compactionCount(): number { return this._compactionCount; }
84
119
 
85
- /** AbortController for cancelling this agent. Set at construction; used only by AgentManager. */
86
- readonly abortController?: AbortController;
87
- /** Promise for the full agent run (including post-processing). Set once by AgentManager. */
120
+ /** AbortController for cancelling this agent. Created at construction. */
121
+ readonly abortController: AbortController;
122
+ /** Promise for the full agent run (including post-processing). Set by run(). */
88
123
  promise?: Promise<void>;
89
124
 
125
+ // Shared deps — optional (required for run())
126
+ private readonly _runner?: AgentRunner;
127
+ private readonly _worktrees?: WorktreeManager;
128
+ readonly observer?: AgentLifecycleObserver;
129
+ private readonly _getRunConfig?: () => RunConfig;
130
+
131
+ // Run config — optional (required for run())
132
+ private readonly _snapshot?: ParentSnapshot;
133
+ private readonly _prompt?: string;
134
+ private readonly _model?: Model<any>;
135
+ private readonly _maxTurns?: number;
136
+ private readonly _isolated?: boolean;
137
+ private readonly _thinkingLevel?: ThinkingLevel;
138
+ private readonly _isolation?: IsolationMode;
139
+ private readonly _parentSession?: ParentSessionInfo;
140
+ private readonly _signal?: AbortSignal;
141
+
90
142
  // Phase-specific collaborators — each born complete when their info becomes available
91
143
  execution?: ExecutionState;
92
144
  worktreeState?: WorktreeState;
@@ -96,10 +148,14 @@ export class Agent {
96
148
  * Create a git worktree for isolated execution, set worktreeState, and return the worktree path.
97
149
  * Returns undefined if isolation is not "worktree".
98
150
  * Throws if worktree creation fails (strict isolation).
151
+ * Uses this._worktrees and this._isolation (set at construction).
99
152
  */
100
- setupWorktree(worktrees: WorktreeManager, isolation: IsolationMode | undefined): string | undefined {
101
- if (isolation !== "worktree") return undefined;
102
- const wt = worktrees.create(this.id);
153
+ setupWorktree(): string | undefined {
154
+ if (this._isolation !== "worktree") return undefined;
155
+ if (!this._worktrees) {
156
+ throw new Error("Agent not configured for worktree isolation — missing worktrees dependency");
157
+ }
158
+ const wt = this._worktrees.create(this.id);
103
159
  if (!wt) {
104
160
  throw new Error(
105
161
  'Cannot run with isolation: "worktree" — not a git repo, no commits yet, or `git worktree add` failed. ' +
@@ -126,22 +182,107 @@ export class Agent {
126
182
  }
127
183
 
128
184
  constructor(init: AgentInit) {
185
+ // Identity
129
186
  this.id = init.id;
130
187
  this.type = init.type;
131
188
  this.description = init.description;
132
189
  this.invocation = init.invocation;
133
190
 
191
+ // Status
134
192
  this._status = init.status ?? "queued";
135
193
  this._result = init.result;
136
194
  this._error = init.error;
137
195
  this._startedAt = init.startedAt ?? Date.now();
138
196
  this._completedAt = init.completedAt;
139
197
 
198
+ // Stats
140
199
  this._toolUses = 0;
141
200
  this._lifetimeUsage = { input: 0, output: 0, cacheWrite: 0 };
142
201
  this._compactionCount = 0;
143
- this.abortController = init.abortController;
144
- this.promise = init.promise;
202
+
203
+ // Abort controller — always created, never injected
204
+ this.abortController = new AbortController();
205
+
206
+ // Shared deps
207
+ this._runner = init.runner;
208
+ this._worktrees = init.worktrees;
209
+ this.observer = init.observer;
210
+ this._getRunConfig = init.getRunConfig;
211
+
212
+ // Run config
213
+ this._snapshot = init.snapshot;
214
+ this._prompt = init.prompt;
215
+ this._model = init.model;
216
+ this._maxTurns = init.maxTurns;
217
+ this._isolated = init.isolated;
218
+ this._thinkingLevel = init.thinkingLevel;
219
+ this._isolation = init.isolation;
220
+ this._parentSession = init.parentSession;
221
+ this._signal = init.signal;
222
+
223
+ // Notification state — created from parentSession.toolCallId if present
224
+ if (init.parentSession?.toolCallId) {
225
+ this.notification = new NotificationState(init.parentSession.toolCallId);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Execute the full agent lifecycle: worktree setup, runner invocation,
231
+ * session-creation handling, observer wiring, worktree cleanup, and
232
+ * status transitions.
233
+ *
234
+ * Requires runner and snapshot to be set at construction.
235
+ * The returned promise always resolves (errors are captured internally).
236
+ */
237
+ async run(): Promise<void> {
238
+ if (!this._runner) {
239
+ throw new Error("Agent not configured for execution — missing runner");
240
+ }
241
+ if (!this._snapshot || !this._prompt) {
242
+ throw new Error("Agent not configured for execution — missing snapshot or prompt");
243
+ }
244
+
245
+ this.markRunning(Date.now());
246
+ this.observer?.onStarted?.(this);
247
+ this.wireSignal(this._signal, () => this.abort());
248
+
249
+ try {
250
+ this.setupWorktree();
251
+ } catch (err) {
252
+ this.markError(err);
253
+ this.observer?.onRunFinished?.(this);
254
+ return;
255
+ }
256
+
257
+ const runConfig = this._getRunConfig?.();
258
+ try {
259
+ const result = await this._runner.run(this._snapshot, this.type, this._prompt, {
260
+ context: {
261
+ cwd: this.worktreeState?.path,
262
+ parentSession: this._parentSession,
263
+ },
264
+ model: this._model,
265
+ maxTurns: this._maxTurns,
266
+ defaultMaxTurns: runConfig?.defaultMaxTurns,
267
+ graceTurns: runConfig?.graceTurns,
268
+ isolated: this._isolated,
269
+ thinkingLevel: this._thinkingLevel,
270
+ signal: this.abortController.signal,
271
+ onSessionCreated: (session) => {
272
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- sessionManager is typed as always present but Pi SDK may not provide it
273
+ const outputFile = session.sessionManager?.getSessionFile?.() ?? undefined;
274
+ this.execution = { session, outputFile };
275
+ this.flushPendingSteers(session);
276
+ this.attachObserver(subscribeAgentObserver(session, this, {
277
+ onCompact: (r, info) => this.observer?.onCompacted?.(r, info),
278
+ }));
279
+ this.observer?.onSessionCreated?.(this, session);
280
+ },
281
+ });
282
+ this.completeRun(result);
283
+ } catch (err) {
284
+ this.failRun(err);
285
+ }
145
286
  }
146
287
 
147
288
  /** Increment tool use count. Called by record-observer on tool_execution_end. */
@@ -226,7 +367,7 @@ export class Agent {
226
367
  */
227
368
  abort(): boolean {
228
369
  if (this._status !== "running") return false;
229
- this.abortController?.abort();
370
+ this.abortController.abort();
230
371
  this.markStopped();
231
372
  return true;
232
373
  }
@@ -258,13 +399,11 @@ export class Agent {
258
399
  this._result = undefined;
259
400
  this._error = undefined;
260
401
  this.releaseListeners();
261
- this._onRunFinished = undefined;
262
402
  }
263
403
 
264
404
  // --- Per-run listener state (released on completion or resume reset) ---
265
405
  private _unsub?: () => void;
266
406
  private _detachFn?: () => void;
267
- private _onRunFinished?: () => void;
268
407
 
269
408
  /** Wire a parent AbortSignal so it stops this agent when fired. */
270
409
  wireSignal(signal: AbortSignal | undefined, onAbort: () => void): void {
@@ -287,25 +426,13 @@ export class Agent {
287
426
  this._detachFn = undefined;
288
427
  }
289
428
 
290
- /** Set the callback fired once when the run finishes (for concurrency drain). */
291
- setOnRunFinished(fn: (() => void) | undefined): void {
292
- this._onRunFinished = fn;
293
- }
294
-
295
- /** Fire the onRunFinished callback at most once. */
296
- private fireOnRunFinished(): void {
297
- const fn = this._onRunFinished;
298
- this._onRunFinished = undefined;
299
- fn?.();
300
- }
301
-
302
- /** Complete a run: release listeners, worktree cleanup, status transition, execution update, fire onRunFinished. */
303
- completeRun(result: RunResult, worktrees: WorktreeManager): void {
429
+ /** Complete a run: release listeners, worktree cleanup, status transition, execution update, notify observer. */
430
+ completeRun(result: RunResult): void {
304
431
  this.releaseListeners();
305
432
 
306
433
  let finalResult = result.responseText;
307
- if (this.worktreeState) {
308
- const wtResult = this.worktreeState.performCleanup(worktrees, this.description);
434
+ if (this.worktreeState && this._worktrees) {
435
+ const wtResult = this.worktreeState.performCleanup(this._worktrees, this.description);
309
436
  if (wtResult.hasChanges && wtResult.branch) {
310
437
  finalResult += `\n\n---\nChanges saved to branch \`${wtResult.branch}\`. Merge with: \`git merge ${wtResult.branch}\``;
311
438
  }
@@ -320,20 +447,20 @@ export class Agent {
320
447
  outputFile: result.sessionFile ?? this.execution?.outputFile,
321
448
  };
322
449
 
323
- this.fireOnRunFinished();
450
+ this.observer?.onRunFinished?.(this);
324
451
  }
325
452
 
326
- /** Fail a run: mark error, release listeners, best-effort worktree cleanup, fire onRunFinished. */
327
- failRun(err: unknown, worktrees: WorktreeManager): void {
453
+ /** Fail a run: mark error, release listeners, best-effort worktree cleanup, notify observer. */
454
+ failRun(err: unknown): void {
328
455
  this.markError(err);
329
456
  this.releaseListeners();
330
457
 
331
- if (this.worktreeState) {
458
+ if (this.worktreeState && this._worktrees) {
332
459
  try {
333
- this.worktreeState.performCleanup(worktrees, this.description);
460
+ this.worktreeState.performCleanup(this._worktrees, this.description);
334
461
  } catch (cleanupErr) { debugLog("cleanupWorktree on agent error", cleanupErr); }
335
462
  }
336
463
 
337
- this.fireOnRunFinished();
464
+ this.observer?.onRunFinished?.(this);
338
465
  }
339
466
  }
@@ -6,8 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { Agent } from "#src/lifecycle/agent";
9
- import type { CompactionInfo } from "#src/lifecycle/agent-manager";
10
- import type { SubscribableSession } from "#src/types";
9
+ import type { CompactionInfo, SubscribableSession } from "#src/types";
11
10
 
12
11
  export interface AgentObserverOptions {
13
12
  onCompact?: (record: Agent, info: CompactionInfo) => void;
@@ -4,14 +4,14 @@ import { defineTool } from "@earendil-works/pi-coding-agent";
4
4
  import { Text } from "@earendil-works/pi-tui";
5
5
  import { Type } from "@sinclair/typebox";
6
6
  import { AgentTypeRegistry } from "#src/config/agent-types";
7
- import type { AgentSpawnConfig, ParentSessionInfo } from "#src/lifecycle/agent-manager";
7
+ import type { AgentSpawnConfig } from "#src/lifecycle/agent-manager";
8
8
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
9
9
  import { spawnBackground } from "#src/tools/background-spawner";
10
10
  import { runForeground } from "#src/tools/foreground-runner";
11
11
  import { buildDetails, buildTypeListText, textResult } from "#src/tools/helpers";
12
12
  import { renderAgentResult } from "#src/tools/result-renderer";
13
13
  import { type ModelInfo, resolveSpawnConfig } from "#src/tools/spawn-config";
14
- import type { Agent } from "#src/types";
14
+ import type { Agent, ParentSessionInfo } from "#src/types";
15
15
  import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
16
16
  import { type UICtx } from "#src/ui/agent-widget";
17
17
  import { type AgentDetails, getDisplayName } from "#src/ui/display";
@@ -1,9 +1,9 @@
1
- import type { AgentSpawnConfig, ParentSessionInfo } from "#src/lifecycle/agent-manager";
1
+ import type { AgentSpawnConfig } from "#src/lifecycle/agent-manager";
2
2
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
3
3
  import type { AgentActivityAccess } from "#src/tools/agent-tool";
4
4
  import { textResult } from "#src/tools/helpers";
5
5
  import type { ResolvedSpawnConfig } from "#src/tools/spawn-config";
6
- import type { Agent } from "#src/types";
6
+ import type { Agent, ParentSessionInfo } from "#src/types";
7
7
  import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
8
8
  import { subscribeUIObserver } from "#src/ui/ui-observer";
9
9
 
@@ -54,9 +54,11 @@ export function spawnBackground(
54
54
  isBackground: true,
55
55
  isolation: execution.isolation,
56
56
  invocation: execution.agentInvocation,
57
- onSessionCreated: (session) => {
58
- bgState.setSession(session);
59
- subscribeUIObserver(session, bgState);
57
+ observer: {
58
+ onSessionCreated: (_agent, session) => {
59
+ bgState.setSession(session);
60
+ subscribeUIObserver(session, bgState);
61
+ },
60
62
  },
61
63
  });
62
64
  } catch (err) {
@@ -1,5 +1,5 @@
1
1
  import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
2
- import type { AgentSpawnConfig, ParentSessionInfo } from "#src/lifecycle/agent-manager";
2
+ import type { AgentSpawnConfig } from "#src/lifecycle/agent-manager";
3
3
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
4
4
  import type { AgentActivityAccess } from "#src/tools/agent-tool";
5
5
  import {
@@ -9,7 +9,7 @@ import {
9
9
  textResult,
10
10
  } from "#src/tools/helpers";
11
11
  import type { ResolvedSpawnConfig } from "#src/tools/spawn-config";
12
- import type { Agent } from "#src/types";
12
+ import type { Agent, ParentSessionInfo } from "#src/types";
13
13
  import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
14
14
  import {
15
15
  type AgentDetails,
@@ -109,13 +109,15 @@ export async function runForeground(
109
109
  invocation: execution.agentInvocation,
110
110
  signal,
111
111
  parentSession: params.parentSession,
112
- onSessionCreated: (session, record) => {
113
- fgState.setSession(session);
114
- recordRef = record;
115
- unsubUI = subscribeUIObserver(session, fgState, streamUpdate);
116
- fgId = record.id;
117
- agentActivity.set(record.id, fgState);
118
- widget.ensureTimer();
112
+ observer: {
113
+ onSessionCreated: (agent, session) => {
114
+ fgState.setSession(session);
115
+ recordRef = agent;
116
+ unsubUI = subscribeUIObserver(session, fgState, streamUpdate);
117
+ fgId = agent.id;
118
+ agentActivity.set(agent.id, fgState);
119
+ widget.ensureTimer();
120
+ },
119
121
  },
120
122
  },
121
123
  );
package/src/types.ts CHANGED
@@ -105,3 +105,16 @@ export type ShellExec = (
105
105
  args: string[],
106
106
  options?: { cwd?: string; timeout?: number },
107
107
  ) => Promise<{ stdout: string; stderr: string; code: number }>;
108
+
109
+ /** Parent session identity — grouped fields that travel together from the tool boundary. */
110
+ export interface ParentSessionInfo {
111
+ /** Path to the parent session's JSONL file (for deriving the subagent session directory). */
112
+ parentSessionFile?: string;
113
+ /** Session ID of the parent agent (stored in the child session's parentSession header). */
114
+ parentSessionId?: string;
115
+ /** Tool call ID for background notification wiring. When set, spawn attaches NotificationState. */
116
+ toolCallId?: string;
117
+ }
118
+
119
+ /** Compaction event info passed through lifecycle observers. */
120
+ export type CompactionInfo = { reason: "manual" | "threshold" | "overflow"; tokensBefore: number };