@gotgenes/pi-subagents 11.3.0 → 11.5.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 +16 -0
- package/docs/architecture/architecture.md +149 -249
- package/docs/decisions/0002-extensions-on-a-minimal-core.md +98 -0
- package/docs/plans/0257-extract-child-session-factory.md +283 -0
- package/docs/plans/0262-add-workspace-provider-seam.md +262 -0
- package/docs/retro/0256-extract-worktree-isolation.md +44 -0
- package/docs/retro/0257-extract-child-session-factory.md +31 -0
- package/docs/retro/0262-add-workspace-provider-seam.md +44 -0
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/lifecycle/agent-manager.ts +30 -0
- package/src/lifecycle/agent-runner.ts +14 -9
- package/src/lifecycle/agent.ts +44 -7
- package/src/lifecycle/child-lifecycle.ts +89 -0
- package/src/lifecycle/workspace.ts +47 -0
- package/src/service/service-adapter.ts +6 -0
- package/src/service/service.ts +13 -1
- package/src/lifecycle/permission-bridge.ts +0 -63
package/src/lifecycle/agent.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type { ExecutionState } from "#src/lifecycle/execution-state";
|
|
|
26
26
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
27
27
|
import type { LifetimeUsage } from "#src/lifecycle/usage";
|
|
28
28
|
import { addUsage } from "#src/lifecycle/usage";
|
|
29
|
+
import type { Workspace, WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
29
30
|
import type { WorktreeIsolation } from "#src/lifecycle/worktree-isolation";
|
|
30
31
|
import { NotificationState } from "#src/observation/notification-state";
|
|
31
32
|
import { subscribeAgentObserver } from "#src/observation/record-observer";
|
|
@@ -72,6 +73,10 @@ export interface AgentInit {
|
|
|
72
73
|
worktree?: WorktreeIsolation;
|
|
73
74
|
observer?: AgentLifecycleObserver;
|
|
74
75
|
getRunConfig?: () => RunConfig;
|
|
76
|
+
/** Resolves the registered workspace provider (if any) at run-start. */
|
|
77
|
+
getWorkspaceProvider?: () => WorkspaceProvider | undefined;
|
|
78
|
+
/** Parent working directory handed to a workspace provider's prepare(). */
|
|
79
|
+
baseCwd?: string;
|
|
75
80
|
|
|
76
81
|
// Run config (required for run(), optional for tests)
|
|
77
82
|
snapshot?: ParentSnapshot;
|
|
@@ -129,6 +134,10 @@ export class Agent {
|
|
|
129
134
|
readonly worktree?: WorktreeIsolation;
|
|
130
135
|
readonly observer?: AgentLifecycleObserver;
|
|
131
136
|
private readonly _getRunConfig?: () => RunConfig;
|
|
137
|
+
private readonly _getWorkspaceProvider?: () => WorkspaceProvider | undefined;
|
|
138
|
+
private readonly _baseCwd: string;
|
|
139
|
+
/** Workspace prepared at run-start by a provider — undefined when none is registered. */
|
|
140
|
+
private _workspace?: Workspace;
|
|
132
141
|
|
|
133
142
|
// Run config — optional (required for run())
|
|
134
143
|
private readonly _snapshot?: ParentSnapshot;
|
|
@@ -186,6 +195,8 @@ export class Agent {
|
|
|
186
195
|
this.worktree = init.worktree;
|
|
187
196
|
this.observer = init.observer;
|
|
188
197
|
this._getRunConfig = init.getRunConfig;
|
|
198
|
+
this._getWorkspaceProvider = init.getWorkspaceProvider;
|
|
199
|
+
this._baseCwd = init.baseCwd ?? "";
|
|
189
200
|
|
|
190
201
|
// Run config
|
|
191
202
|
this._snapshot = init.snapshot;
|
|
@@ -223,8 +234,23 @@ export class Agent {
|
|
|
223
234
|
this.observer?.onStarted?.(this);
|
|
224
235
|
this.wireSignal(this._signal, () => this.abort());
|
|
225
236
|
|
|
237
|
+
let cwd: string | undefined;
|
|
226
238
|
try {
|
|
227
|
-
|
|
239
|
+
// Provider-first: a registered workspace provider supplies the cwd and
|
|
240
|
+
// owns teardown; otherwise fall back to the legacy worktree collaborator.
|
|
241
|
+
const provider = this._getWorkspaceProvider?.();
|
|
242
|
+
if (provider) {
|
|
243
|
+
this._workspace = await provider.prepare({
|
|
244
|
+
agentId: this.id,
|
|
245
|
+
agentType: this.type,
|
|
246
|
+
baseCwd: this._baseCwd,
|
|
247
|
+
invocation: this.invocation,
|
|
248
|
+
});
|
|
249
|
+
cwd = this._workspace?.cwd;
|
|
250
|
+
} else {
|
|
251
|
+
this.worktree?.setup();
|
|
252
|
+
cwd = this.worktree?.path;
|
|
253
|
+
}
|
|
228
254
|
} catch (err) {
|
|
229
255
|
this.markError(err);
|
|
230
256
|
this.releaseListeners();
|
|
@@ -236,7 +262,7 @@ export class Agent {
|
|
|
236
262
|
try {
|
|
237
263
|
const result = await this._runner.run(this._snapshot, this.type, this._prompt, {
|
|
238
264
|
context: {
|
|
239
|
-
cwd
|
|
265
|
+
cwd,
|
|
240
266
|
parentSession: this._parentSession,
|
|
241
267
|
},
|
|
242
268
|
model: this._model,
|
|
@@ -442,9 +468,19 @@ export class Agent {
|
|
|
442
468
|
this.releaseListeners();
|
|
443
469
|
|
|
444
470
|
let finalResult = result.responseText;
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
471
|
+
if (this._workspace) {
|
|
472
|
+
const finalStatus: AgentStatus = result.aborted
|
|
473
|
+
? "aborted"
|
|
474
|
+
: result.steered
|
|
475
|
+
? "steered"
|
|
476
|
+
: "completed";
|
|
477
|
+
const disposeResult = this._workspace.dispose({ status: finalStatus, description: this.description });
|
|
478
|
+
if (disposeResult?.resultAddendum) finalResult += disposeResult.resultAddendum;
|
|
479
|
+
} else {
|
|
480
|
+
const wtResult = this.worktree?.cleanup(this.description);
|
|
481
|
+
if (wtResult?.hasChanges && wtResult.branch) {
|
|
482
|
+
finalResult += `\n\n---\nChanges saved to branch \`${wtResult.branch}\`. Merge with: \`git merge ${wtResult.branch}\``;
|
|
483
|
+
}
|
|
448
484
|
}
|
|
449
485
|
|
|
450
486
|
if (result.aborted) this.markAborted(finalResult);
|
|
@@ -465,8 +501,9 @@ export class Agent {
|
|
|
465
501
|
this.releaseListeners();
|
|
466
502
|
|
|
467
503
|
try {
|
|
468
|
-
this.
|
|
469
|
-
|
|
504
|
+
if (this._workspace) this._workspace.dispose({ status: "error", description: this.description });
|
|
505
|
+
else this.worktree?.cleanup(this.description);
|
|
506
|
+
} catch (cleanupErr) { debugLog("workspace dispose on agent error", cleanupErr); }
|
|
470
507
|
|
|
471
508
|
this.observer?.onRunFinished?.(this);
|
|
472
509
|
}
|
|
@@ -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,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workspace.ts — The single generative extension seam (ADR 0002, Phase 16 Step 2).
|
|
3
|
+
*
|
|
4
|
+
* "Where does a child run, and what brackets the run?" is a strategy (git
|
|
5
|
+
* worktree, container, tmpdir, remote sandbox), not core behavior. The core
|
|
6
|
+
* needs only a working directory plus a disposal hook; the default — the
|
|
7
|
+
* parent's cwd, with no setup/teardown — is always correct.
|
|
8
|
+
*
|
|
9
|
+
* Unlike the observational lifecycle events in child-lifecycle.ts, this is a
|
|
10
|
+
* *generative* seam: a registered provider returns a value the core consumes
|
|
11
|
+
* synchronously at run-start. The core has no knowledge of git or worktrees.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { AgentStatus } from "#src/lifecycle/agent";
|
|
15
|
+
import type { AgentInvocation, SubagentType } from "#src/types";
|
|
16
|
+
|
|
17
|
+
/** Context the core hands a provider when a child run starts. */
|
|
18
|
+
export interface WorkspacePrepareContext {
|
|
19
|
+
agentId: string;
|
|
20
|
+
agentType: SubagentType;
|
|
21
|
+
baseCwd: string;
|
|
22
|
+
invocation?: AgentInvocation;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Outcome the core reports to a workspace when the run ends. */
|
|
26
|
+
export interface WorkspaceDisposeOutcome {
|
|
27
|
+
status: AgentStatus;
|
|
28
|
+
description: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What dispose may hand back for the core to fold into the child result. */
|
|
32
|
+
export interface WorkspaceDisposeResult {
|
|
33
|
+
/** Appended verbatim to the child's result text — the provider owns the wording. */
|
|
34
|
+
resultAddendum?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A prepared working directory plus its bracketed teardown. Born complete. */
|
|
38
|
+
export interface Workspace {
|
|
39
|
+
/** The working directory — already exists when the workspace is handed back. */
|
|
40
|
+
readonly cwd: string;
|
|
41
|
+
dispose(outcome: WorkspaceDisposeOutcome): WorkspaceDisposeResult | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The single generative seam: supplies a child's workspace. */
|
|
45
|
+
export interface WorkspaceProvider {
|
|
46
|
+
prepare(ctx: WorkspacePrepareContext): Promise<Workspace | undefined>;
|
|
47
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
9
|
+
import type { WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
9
10
|
import type { SpawnOptions, SubagentRecord, SubagentsService } from "#src/service/service";
|
|
10
11
|
import type { ModelRegistry } from "#src/session/model-resolver";
|
|
11
12
|
import type { Agent, SessionContext } from "#src/types";
|
|
@@ -18,6 +19,7 @@ export interface AgentManagerLike {
|
|
|
18
19
|
abort(id: string): boolean;
|
|
19
20
|
waitForAll(): Promise<void>;
|
|
20
21
|
hasRunning(): boolean;
|
|
22
|
+
registerWorkspaceProvider(provider: WorkspaceProvider): () => void;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -107,6 +109,10 @@ export class SubagentsServiceAdapter implements SubagentsService {
|
|
|
107
109
|
hasRunning(): boolean {
|
|
108
110
|
return this.manager.hasRunning();
|
|
109
111
|
}
|
|
112
|
+
|
|
113
|
+
registerWorkspaceProvider(provider: WorkspaceProvider): () => void {
|
|
114
|
+
return this.manager.registerWorkspaceProvider(provider);
|
|
115
|
+
}
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
/**
|
package/src/service/service.ts
CHANGED
|
@@ -10,8 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { LifetimeUsage } from "#src/lifecycle/usage";
|
|
13
|
+
import type { WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
// Generative extension seam (ADR 0002, Phase 16 Step 2). Only the provider
|
|
16
|
+
// entry-point type is re-exported here; a consumer assigning to
|
|
17
|
+
// `WorkspaceProvider` gets `Workspace` and the context types via inference.
|
|
18
|
+
// The worktrees package (#263) adds named re-exports when it imports them.
|
|
19
|
+
export type { LifetimeUsage, WorkspaceProvider };
|
|
15
20
|
|
|
16
21
|
export type SubagentStatus =
|
|
17
22
|
| "queued"
|
|
@@ -73,6 +78,13 @@ export interface SubagentsService {
|
|
|
73
78
|
|
|
74
79
|
/** Whether any agents are running or queued. */
|
|
75
80
|
hasRunning(): boolean;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Register the single workspace provider that supplies a child's working
|
|
84
|
+
* directory plus bracketed setup/teardown. Throws if one is already
|
|
85
|
+
* registered. Returns a disposer that unregisters the provider.
|
|
86
|
+
*/
|
|
87
|
+
registerWorkspaceProvider(provider: WorkspaceProvider): () => void;
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
/** Event channel constants for pi.events subscriptions. */
|
|
@@ -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
|
-
}
|