@gotgenes/pi-subagents 11.2.0 → 11.4.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.
@@ -11,7 +11,10 @@
11
11
  * Behavior (abort, steer buffering, worktree setup) lives on the agent
12
12
  * rather than on AgentManager — each agent manages its own lifecycle concerns.
13
13
  *
14
- * Phase-specific collaborators (execution, worktreeState, notification) are attached
14
+ * Worktree isolation is delegated to an optional WorktreeIsolation collaborator
15
+ * (set at construction when isolation is requested); its presence IS the mode.
16
+ *
17
+ * Phase-specific collaborators (execution, notification) are attached
15
18
  * after construction as lifecycle information becomes available.
16
19
  */
17
20
 
@@ -23,12 +26,11 @@ import type { ExecutionState } from "#src/lifecycle/execution-state";
23
26
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
24
27
  import type { LifetimeUsage } from "#src/lifecycle/usage";
25
28
  import { addUsage } from "#src/lifecycle/usage";
26
- import type { WorktreeManager } from "#src/lifecycle/worktree";
27
- import { WorktreeState } from "#src/lifecycle/worktree-state";
29
+ import type { WorktreeIsolation } from "#src/lifecycle/worktree-isolation";
28
30
  import { NotificationState } from "#src/observation/notification-state";
29
31
  import { subscribeAgentObserver } from "#src/observation/record-observer";
30
32
  import type { RunConfig } from "#src/runtime";
31
- import type { AgentInvocation, CompactionInfo, IsolationMode, ParentSessionInfo, SubagentType, ThinkingLevel } from "#src/types";
33
+ import type { AgentInvocation, CompactionInfo, ParentSessionInfo, SubagentType, ThinkingLevel } from "#src/types";
32
34
 
33
35
  /** Per-agent lifecycle observer — created by AgentManager for each spawn. */
34
36
  export interface AgentLifecycleObserver {
@@ -67,7 +69,7 @@ export interface AgentInit {
67
69
 
68
70
  // Shared deps (required for run(), optional for tests)
69
71
  runner?: AgentRunner;
70
- worktrees?: WorktreeManager;
72
+ worktree?: WorktreeIsolation;
71
73
  observer?: AgentLifecycleObserver;
72
74
  getRunConfig?: () => RunConfig;
73
75
 
@@ -78,7 +80,6 @@ export interface AgentInit {
78
80
  maxTurns?: number;
79
81
  isolated?: boolean;
80
82
  thinkingLevel?: ThinkingLevel;
81
- isolation?: IsolationMode;
82
83
  parentSession?: ParentSessionInfo;
83
84
  isBackground?: boolean;
84
85
  signal?: AbortSignal;
@@ -124,7 +125,8 @@ export class Agent {
124
125
 
125
126
  // Shared deps — optional (required for run())
126
127
  private readonly _runner?: AgentRunner;
127
- private readonly _worktrees?: WorktreeManager;
128
+ /** Worktree isolation collaborator — present only when isolation: "worktree". */
129
+ readonly worktree?: WorktreeIsolation;
128
130
  readonly observer?: AgentLifecycleObserver;
129
131
  private readonly _getRunConfig?: () => RunConfig;
130
132
 
@@ -135,37 +137,13 @@ export class Agent {
135
137
  private readonly _maxTurns?: number;
136
138
  private readonly _isolated?: boolean;
137
139
  private readonly _thinkingLevel?: ThinkingLevel;
138
- private readonly _isolation?: IsolationMode;
139
140
  private readonly _parentSession?: ParentSessionInfo;
140
141
  private readonly _signal?: AbortSignal;
141
142
 
142
143
  // Phase-specific collaborators — each born complete when their info becomes available
143
144
  execution?: ExecutionState;
144
- worktreeState?: WorktreeState;
145
145
  notification?: NotificationState;
146
146
 
147
- /**
148
- * Create a git worktree for isolated execution, set worktreeState, and return the worktree path.
149
- * Returns undefined if isolation is not "worktree".
150
- * Throws if worktree creation fails (strict isolation).
151
- * Uses this._worktrees and this._isolation (set at construction).
152
- */
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);
159
- if (!wt) {
160
- throw new Error(
161
- 'Cannot run with isolation: "worktree" — not a git repo, no commits yet, or `git worktree add` failed. ' +
162
- 'Initialize git and commit at least once, or omit `isolation`.',
163
- );
164
- }
165
- this.worktreeState = new WorktreeState(wt);
166
- return wt.path;
167
- }
168
-
169
147
  // Steer buffer — messages queued before the session is ready
170
148
  private _pendingSteers: string[] = [];
171
149
  /** Number of steer messages waiting to be delivered. */
@@ -205,7 +183,7 @@ export class Agent {
205
183
 
206
184
  // Shared deps
207
185
  this._runner = init.runner;
208
- this._worktrees = init.worktrees;
186
+ this.worktree = init.worktree;
209
187
  this.observer = init.observer;
210
188
  this._getRunConfig = init.getRunConfig;
211
189
 
@@ -216,7 +194,6 @@ export class Agent {
216
194
  this._maxTurns = init.maxTurns;
217
195
  this._isolated = init.isolated;
218
196
  this._thinkingLevel = init.thinkingLevel;
219
- this._isolation = init.isolation;
220
197
  this._parentSession = init.parentSession;
221
198
  this._signal = init.signal;
222
199
 
@@ -247,7 +224,7 @@ export class Agent {
247
224
  this.wireSignal(this._signal, () => this.abort());
248
225
 
249
226
  try {
250
- this.setupWorktree();
227
+ this.worktree?.setup();
251
228
  } catch (err) {
252
229
  this.markError(err);
253
230
  this.releaseListeners();
@@ -259,7 +236,7 @@ export class Agent {
259
236
  try {
260
237
  const result = await this._runner.run(this._snapshot, this.type, this._prompt, {
261
238
  context: {
262
- cwd: this.worktreeState?.path,
239
+ cwd: this.worktree?.path,
263
240
  parentSession: this._parentSession,
264
241
  },
265
242
  model: this._model,
@@ -465,11 +442,9 @@ export class Agent {
465
442
  this.releaseListeners();
466
443
 
467
444
  let finalResult = result.responseText;
468
- if (this.worktreeState && this._worktrees) {
469
- const wtResult = this.worktreeState.performCleanup(this._worktrees, this.description);
470
- if (wtResult.hasChanges && wtResult.branch) {
471
- finalResult += `\n\n---\nChanges saved to branch \`${wtResult.branch}\`. Merge with: \`git merge ${wtResult.branch}\``;
472
- }
445
+ const wtResult = this.worktree?.cleanup(this.description);
446
+ if (wtResult?.hasChanges && wtResult.branch) {
447
+ finalResult += `\n\n---\nChanges saved to branch \`${wtResult.branch}\`. Merge with: \`git merge ${wtResult.branch}\``;
473
448
  }
474
449
 
475
450
  if (result.aborted) this.markAborted(finalResult);
@@ -489,11 +464,9 @@ export class Agent {
489
464
  this.markError(err);
490
465
  this.releaseListeners();
491
466
 
492
- if (this.worktreeState && this._worktrees) {
493
- try {
494
- this.worktreeState.performCleanup(this._worktrees, this.description);
495
- } catch (cleanupErr) { debugLog("cleanupWorktree on agent error", cleanupErr); }
496
- }
467
+ try {
468
+ this.worktree?.cleanup(this.description);
469
+ } catch (cleanupErr) { debugLog("cleanupWorktree on agent error", cleanupErr); }
497
470
 
498
471
  this.observer?.onRunFinished?.(this);
499
472
  }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * child-lifecycle.ts — Child-execution lifecycle event contract and publisher.
3
+ *
4
+ * The core publishes its child-execution lifecycle as ordered events on the Pi
5
+ * event bus; reactive consumers (permissions, telemetry, UI) subscribe rather
6
+ * than the core reaching out to them (ADR 0002). This module owns the channel
7
+ * names, payload shapes, and the publisher that emits them.
8
+ *
9
+ * The publisher takes an injected `emit` callback so this module stays free of
10
+ * Pi SDK imports — `index.ts` wires it to `pi.events.emit`.
11
+ */
12
+
13
+ /** Emitted at the start of a child run, before the session is created. */
14
+ export const SUBAGENT_CHILD_SPAWNING = "subagents:child:spawning";
15
+
16
+ /**
17
+ * Emitted after the child session is created, immediately before
18
+ * `bindExtensions()`. Carries the child identity consumers need to register
19
+ * the session. Subscribers must register synchronously so the entry lands
20
+ * before binding proceeds (see ADR 0002 / the event-bus synchronous-dispatch
21
+ * guarantee).
22
+ */
23
+ export const SUBAGENT_CHILD_SESSION_CREATED = "subagents:child:session-created";
24
+
25
+ /** Emitted after the child's prompt resolves (normal, steered, or aborted). */
26
+ export const SUBAGENT_CHILD_COMPLETED = "subagents:child:completed";
27
+
28
+ /** Emitted in the run's `finally` — always fires, on success and error. */
29
+ export const SUBAGENT_CHILD_DISPOSED = "subagents:child:disposed";
30
+
31
+ /** Payload for `subagents:child:spawning`. */
32
+ export interface ChildSpawningEvent {
33
+ agentName: string;
34
+ parentSessionId?: string;
35
+ }
36
+
37
+ /** Payload for `subagents:child:session-created`. */
38
+ export interface ChildSessionCreatedEvent {
39
+ /** Child session directory — the registry key. */
40
+ sessionDir: string;
41
+ agentName: string;
42
+ parentSessionId?: string;
43
+ }
44
+
45
+ /** Payload for `subagents:child:completed`. */
46
+ export interface ChildCompletedEvent {
47
+ sessionDir: string;
48
+ agentName: string;
49
+ /** True if the run was hard-aborted (max turns + grace exceeded). */
50
+ aborted: boolean;
51
+ /** True if the run was steered to wrap up (soft turn limit) but finished. */
52
+ steered: boolean;
53
+ }
54
+
55
+ /** Payload for `subagents:child:disposed`. */
56
+ export interface ChildDisposedEvent {
57
+ sessionDir: string;
58
+ }
59
+
60
+ /** Narrow emit seam — injected, never imports the Pi SDK. */
61
+ export type LifecycleEmit = (channel: string, data: unknown) => void;
62
+
63
+ /** Publishes the child-execution lifecycle on the event bus. */
64
+ export interface ChildLifecyclePublisher {
65
+ spawning(event: ChildSpawningEvent): void;
66
+ sessionCreated(event: ChildSessionCreatedEvent): void;
67
+ completed(event: ChildCompletedEvent): void;
68
+ disposed(event: ChildDisposedEvent): void;
69
+ }
70
+
71
+ /** Build a publisher backed by an injected `emit` callback. */
72
+ export function createChildLifecyclePublisher(
73
+ emit: LifecycleEmit,
74
+ ): ChildLifecyclePublisher {
75
+ return {
76
+ spawning(event) {
77
+ emit(SUBAGENT_CHILD_SPAWNING, event);
78
+ },
79
+ sessionCreated(event) {
80
+ emit(SUBAGENT_CHILD_SESSION_CREATED, event);
81
+ },
82
+ completed(event) {
83
+ emit(SUBAGENT_CHILD_COMPLETED, event);
84
+ },
85
+ disposed(event) {
86
+ emit(SUBAGENT_CHILD_DISPOSED, event);
87
+ },
88
+ };
89
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * worktree-isolation.ts — WorktreeIsolation: collaborator that owns the
3
+ * git-worktree lifecycle for an isolated agent.
4
+ *
5
+ * Constructed by AgentManager only when isolation === "worktree", bound to a
6
+ * WorktreeManager and the agent id. Agent tells it `setup()` and
7
+ * `cleanup(description)` instead of managing worktree internals itself.
8
+ *
9
+ * The presence/absence of this collaborator IS the isolation mode: Agent calls
10
+ * `this.worktree?.setup()` rather than checking an isolation string.
11
+ */
12
+
13
+ import type { WorktreeCleanupResult, WorktreeInfo, WorktreeManager } from "#src/lifecycle/worktree";
14
+
15
+ export class WorktreeIsolation {
16
+ private _info?: WorktreeInfo;
17
+ private _cleanupResult?: WorktreeCleanupResult;
18
+
19
+ constructor(
20
+ private readonly worktrees: WorktreeManager,
21
+ private readonly agentId: string,
22
+ ) {}
23
+
24
+ /** Absolute worktree path — undefined before setup(). */
25
+ get path(): string | undefined {
26
+ return this._info?.path;
27
+ }
28
+
29
+ /** Cleanup outcome — undefined until cleanup() runs. */
30
+ get cleanupResult(): WorktreeCleanupResult | undefined {
31
+ return this._cleanupResult;
32
+ }
33
+
34
+ /**
35
+ * Create the git worktree and store its info.
36
+ * Throws on failure (strict isolation — no silent fallback).
37
+ */
38
+ setup(): void {
39
+ const wt = this.worktrees.create(this.agentId);
40
+ if (!wt) {
41
+ throw new Error(
42
+ 'Cannot run with isolation: "worktree" — not a git repo, no commits yet, or `git worktree add` failed. ' +
43
+ "Initialize git and commit at least once, or omit `isolation`.",
44
+ );
45
+ }
46
+ this._info = wt;
47
+ }
48
+
49
+ /**
50
+ * Perform worktree cleanup and record the result.
51
+ * No-op returning { hasChanges: false } if setup never ran.
52
+ */
53
+ cleanup(description: string): WorktreeCleanupResult {
54
+ if (!this._info) return { hasChanges: false };
55
+ const result = this.worktrees.cleanup(this._info, description);
56
+ this._cleanupResult = result;
57
+ return result;
58
+ }
59
+ }
@@ -128,7 +128,7 @@ export function toSubagentRecord(record: Agent): SubagentRecord {
128
128
  if (record.result !== undefined) out.result = record.result;
129
129
  if (record.error !== undefined) out.error = record.error;
130
130
  if (record.completedAt !== undefined) out.completedAt = record.completedAt;
131
- const worktreeResult = record.worktreeState?.cleanupResult;
131
+ const worktreeResult = record.worktree?.cleanupResult;
132
132
  if (worktreeResult !== undefined) out.worktreeResult = worktreeResult;
133
133
 
134
134
  return out;
@@ -1,63 +0,0 @@
1
- /**
2
- * permission-bridge.ts — Cross-extension bridge to @gotgenes/pi-permission-system.
3
- *
4
- * pi-subagents does not import pi-permission-system directly. Instead it
5
- * accesses the published PermissionsService via a process-global Symbol.for()
6
- * key, the same mechanism pi-permission-system uses to publish itself.
7
- *
8
- * When pi-permission-system is not installed, getPermissionsService() returns
9
- * undefined and all registration calls are silent no-ops.
10
- */
11
-
12
- /**
13
- * The two PermissionsService methods pi-subagents needs.
14
- *
15
- * Follows ISP — does not expose the full PermissionsService surface
16
- * (checkPermission, getToolPermission, etc.) to avoid coupling.
17
- */
18
- interface PermissionsServiceConsumer {
19
- registerSubagentSession(
20
- sessionKey: string,
21
- info: { parentSessionId?: string; agentName: string },
22
- ): void;
23
- unregisterSubagentSession(sessionKey: string): void;
24
- }
25
-
26
- const PERMISSION_SERVICE_KEY = Symbol.for(
27
- "@gotgenes/pi-permission-system:service",
28
- );
29
-
30
- function getPermissionsService(): PermissionsServiceConsumer | undefined {
31
- return (globalThis as Record<symbol, unknown>)[
32
- PERMISSION_SERVICE_KEY
33
- ] as PermissionsServiceConsumer | undefined;
34
- }
35
-
36
- /**
37
- * Register a child session with pi-permission-system's SubagentSessionRegistry.
38
- *
39
- * Must be called after deriving sessionDir but before session.bindExtensions()
40
- * so isSubagentExecutionContext() hits the registry on the first check during
41
- * child extension initialization.
42
- *
43
- * @param sessionKey - The session directory path (unique per session).
44
- * @param info - Agent name and optional parent session ID for forwarding.
45
- */
46
- export function registerChildSession(
47
- sessionKey: string,
48
- info: { parentSessionId?: string; agentName: string },
49
- ): void {
50
- getPermissionsService()?.registerSubagentSession(sessionKey, info);
51
- }
52
-
53
- /**
54
- * Unregister a child session from pi-permission-system's SubagentSessionRegistry.
55
- *
56
- * Must be called in a finally block so cleanup happens on both success and
57
- * error paths.
58
- *
59
- * @param sessionKey - The session directory path used during registration.
60
- */
61
- export function unregisterChildSession(sessionKey: string): void {
62
- getPermissionsService()?.unregisterSubagentSession(sessionKey);
63
- }
@@ -1,45 +0,0 @@
1
- /**
2
- * worktree-state.ts — WorktreeState: lifecycle-phase object for worktree-isolated agents.
3
- *
4
- * Constructed once when the worktree is set up (before the run begins).
5
- * Only exists for agents with isolation: "worktree".
6
- * cleanupResult is recorded once at completion or error — it is not set at construction.
7
- */
8
-
9
- import type { WorktreeCleanupResult, WorktreeInfo, WorktreeManager } from "#src/lifecycle/worktree";
10
-
11
- export type { WorktreeCleanupResult, WorktreeInfo };
12
-
13
- export class WorktreeState {
14
- /** Absolute path to the worktree directory. */
15
- readonly path: string;
16
- /** Branch name created for this worktree. */
17
- readonly branch: string;
18
-
19
- private _cleanupResult?: WorktreeCleanupResult;
20
-
21
- constructor(info: WorktreeInfo) {
22
- this.path = info.path;
23
- this.branch = info.branch;
24
- }
25
-
26
- /** Result of the worktree cleanup — undefined until recordCleanup is called. */
27
- get cleanupResult(): WorktreeCleanupResult | undefined {
28
- return this._cleanupResult;
29
- }
30
-
31
- /** Record the cleanup result. Called once on agent completion or error. */
32
- recordCleanup(result: WorktreeCleanupResult): void {
33
- this._cleanupResult = result;
34
- }
35
-
36
- /**
37
- * Perform worktree cleanup and record the result.
38
- * Tell-Don't-Ask: callers no longer need to orchestrate cleanup + recordCleanup separately.
39
- */
40
- performCleanup(worktrees: WorktreeManager, description: string): WorktreeCleanupResult {
41
- const result = worktrees.cleanup(this, description);
42
- this._cleanupResult = result;
43
- return result;
44
- }
45
- }