@gotgenes/pi-subagents 13.2.1 → 14.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.
- package/CHANGELOG.md +18 -0
- package/README.md +3 -1
- package/dist/public.d.ts +34 -35
- package/docs/architecture/architecture.md +50 -49
- package/docs/architecture/history/phase-16-invert-dependencies.md +2 -1
- package/docs/decisions/0003-publish-bundled-type-declarations.md +3 -1
- package/docs/plans/0051-update-adr-0001-hard-fork.md +8 -6
- package/docs/plans/0257-extract-child-session-factory.md +3 -1
- package/docs/plans/0262-add-workspace-provider-seam.md +41 -39
- package/docs/plans/0264-remove-extension-lifecycle-control.md +100 -98
- package/docs/plans/0265-born-complete-subagent-session.md +5 -2
- package/docs/plans/0270-type-consumable-public-surface.md +3 -1
- package/docs/plans/0280-rename-agent-to-subagent.md +197 -0
- package/docs/retro/0051-update-adr-0001-hard-fork.md +4 -2
- package/docs/retro/0257-extract-child-session-factory.md +3 -1
- package/docs/retro/0262-add-workspace-provider-seam.md +3 -1
- package/docs/retro/0264-remove-extension-lifecycle-control.md +3 -1
- package/docs/retro/0270-type-consumable-public-surface.md +4 -2
- package/docs/retro/0280-rename-agent-to-subagent.md +96 -0
- package/package.json +1 -1
- package/src/index.ts +9 -9
- package/src/lifecycle/child-lifecycle.ts +9 -8
- package/src/lifecycle/create-subagent-session.ts +5 -2
- package/src/lifecycle/{agent-manager.ts → subagent-manager.ts} +27 -27
- package/src/lifecycle/subagent-session.ts +6 -4
- package/src/lifecycle/{agent.ts → subagent.ts} +28 -28
- package/src/lifecycle/turn-limits.ts +1 -1
- package/src/lifecycle/workspace.ts +2 -2
- package/src/observation/notification.ts +9 -9
- package/src/observation/record-observer.ts +9 -9
- package/src/runtime.ts +1 -1
- package/src/service/service-adapter.ts +10 -10
- package/src/service/service.ts +5 -9
- package/src/tools/agent-tool.ts +5 -5
- package/src/tools/background-spawner.ts +3 -3
- package/src/tools/foreground-runner.ts +5 -5
- package/src/tools/get-result-tool.ts +2 -2
- package/src/tools/steer-tool.ts +2 -2
- package/src/types.ts +1 -1
- package/src/ui/agent-creation-wizard.ts +2 -2
- package/src/ui/agent-menu.ts +5 -5
- package/src/ui/agent-widget.ts +2 -2
- package/src/ui/conversation-viewer.ts +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* subagent-manager.ts - Tracks subagents, background execution, resume support.
|
|
3
3
|
*
|
|
4
4
|
* Background agents are subject to a configurable concurrency limit (default: 4).
|
|
5
5
|
* Excess agents are queued and auto-started as running agents complete.
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
10
|
import type { Model } from "@earendil-works/pi-ai";
|
|
11
11
|
import { debugLog } from "#src/debug";
|
|
12
|
-
import { Agent, type AgentLifecycleObserver } from "#src/lifecycle/agent";
|
|
13
12
|
import type { ConcurrencyQueue } from "#src/lifecycle/concurrency-queue";
|
|
14
13
|
import type { CreateSubagentSessionParams } from "#src/lifecycle/create-subagent-session";
|
|
15
14
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
15
|
+
import { Subagent, type SubagentLifecycleObserver } from "#src/lifecycle/subagent";
|
|
16
16
|
import type { SubagentSession } from "#src/lifecycle/subagent-session";
|
|
17
17
|
import type { WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
18
18
|
|
|
@@ -20,15 +20,15 @@ import type { RunConfig } from "#src/runtime";
|
|
|
20
20
|
import type { AgentInvocation, CompactionInfo, ParentSessionInfo, SubagentType, ThinkingLevel } from "#src/types";
|
|
21
21
|
|
|
22
22
|
/** Observer interface for agent lifecycle notifications. */
|
|
23
|
-
export interface
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
export interface SubagentManagerObserver {
|
|
24
|
+
onSubagentStarted(record: Subagent): void;
|
|
25
|
+
onSubagentCompleted(record: Subagent): void;
|
|
26
|
+
onSubagentCompacted(record: Subagent, info: CompactionInfo): void;
|
|
27
27
|
/** Fires synchronously after a background agent record is created (before run). */
|
|
28
|
-
|
|
28
|
+
onSubagentCreated(record: Subagent): void;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export interface
|
|
31
|
+
export interface SubagentManagerOptions {
|
|
32
32
|
/** Assembly factory that produces a born-complete SubagentSession per spawn. */
|
|
33
33
|
createSubagentSession: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
|
|
34
34
|
/** Concurrency queue — owns scheduling, limit checks, and drain logic. */
|
|
@@ -36,7 +36,7 @@ export interface AgentManagerOptions {
|
|
|
36
36
|
/** Base working directory handed to a workspace provider (the parent cwd). */
|
|
37
37
|
baseCwd: string;
|
|
38
38
|
getRunConfig?: () => RunConfig;
|
|
39
|
-
observer?:
|
|
39
|
+
observer?: SubagentManagerObserver;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export interface AgentSpawnConfig {
|
|
@@ -56,16 +56,16 @@ export interface AgentSpawnConfig {
|
|
|
56
56
|
invocation?: AgentInvocation;
|
|
57
57
|
/** Parent abort signal - when aborted, the subagent is also stopped. */
|
|
58
58
|
signal?: AbortSignal;
|
|
59
|
-
/** Per-
|
|
60
|
-
observer?:
|
|
59
|
+
/** Per-subagent lifecycle observer — replaces onSessionCreated callback. */
|
|
60
|
+
observer?: SubagentLifecycleObserver;
|
|
61
61
|
/** Parent session identity - grouped fields that travel together from the tool boundary. */
|
|
62
62
|
parentSession?: ParentSessionInfo;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
export class
|
|
66
|
-
private agents = new Map<string,
|
|
65
|
+
export class SubagentManager {
|
|
66
|
+
private agents = new Map<string, Subagent>();
|
|
67
67
|
private cleanupInterval: ReturnType<typeof setInterval>;
|
|
68
|
-
private readonly observer?:
|
|
68
|
+
private readonly observer?: SubagentManagerObserver;
|
|
69
69
|
private readonly createSubagentSession: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
|
|
70
70
|
private readonly queue: ConcurrencyQueue;
|
|
71
71
|
private readonly baseCwd: string;
|
|
@@ -77,7 +77,7 @@ export class AgentManager {
|
|
|
77
77
|
return this._workspaceProvider;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
constructor(options:
|
|
80
|
+
constructor(options: SubagentManagerOptions) {
|
|
81
81
|
this.createSubagentSession = options.createSubagentSession;
|
|
82
82
|
this.queue = options.queue;
|
|
83
83
|
this.baseCwd = options.baseCwd;
|
|
@@ -106,11 +106,11 @@ export class AgentManager {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/** Compose a per-agent lifecycle observer from manager and spawn-config concerns. */
|
|
109
|
-
private buildObserver(options: AgentSpawnConfig):
|
|
109
|
+
private buildObserver(options: AgentSpawnConfig): SubagentLifecycleObserver {
|
|
110
110
|
return {
|
|
111
111
|
onStarted: (agent) => {
|
|
112
112
|
if (options.isBackground) this.queue.markStarted();
|
|
113
|
-
this.observer?.
|
|
113
|
+
this.observer?.onSubagentStarted(agent);
|
|
114
114
|
},
|
|
115
115
|
onSessionCreated: options.observer?.onSessionCreated
|
|
116
116
|
? (agent) => options.observer!.onSessionCreated!(agent)
|
|
@@ -118,11 +118,11 @@ export class AgentManager {
|
|
|
118
118
|
onRunFinished: (agent) => {
|
|
119
119
|
if (options.isBackground) {
|
|
120
120
|
this.queue.markFinished();
|
|
121
|
-
try { this.observer?.
|
|
121
|
+
try { this.observer?.onSubagentCompleted(agent); } catch (err) { debugLog("onSubagentCompleted observer", err); }
|
|
122
122
|
}
|
|
123
123
|
},
|
|
124
124
|
onCompacted: (agent, info) => {
|
|
125
|
-
this.observer?.
|
|
125
|
+
this.observer?.onSubagentCompacted(agent, info);
|
|
126
126
|
},
|
|
127
127
|
};
|
|
128
128
|
}
|
|
@@ -138,7 +138,7 @@ export class AgentManager {
|
|
|
138
138
|
options: AgentSpawnConfig,
|
|
139
139
|
): string {
|
|
140
140
|
const id = randomUUID().slice(0, 17);
|
|
141
|
-
const record = new
|
|
141
|
+
const record = new Subagent({
|
|
142
142
|
id,
|
|
143
143
|
type,
|
|
144
144
|
description: options.description,
|
|
@@ -163,7 +163,7 @@ export class AgentManager {
|
|
|
163
163
|
this.agents.set(id, record);
|
|
164
164
|
|
|
165
165
|
if (options.isBackground) {
|
|
166
|
-
this.observer?.
|
|
166
|
+
this.observer?.onSubagentCreated(record);
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
if (options.isBackground && !options.bypassQueue && this.queue.isFull()) {
|
|
@@ -185,7 +185,7 @@ export class AgentManager {
|
|
|
185
185
|
type: SubagentType,
|
|
186
186
|
prompt: string,
|
|
187
187
|
options: Omit<AgentSpawnConfig, "isBackground">,
|
|
188
|
-
): Promise<
|
|
188
|
+
): Promise<Subagent> {
|
|
189
189
|
const id = this.spawn(snapshot, type, prompt, { ...options, isBackground: false });
|
|
190
190
|
const record = this.agents.get(id)!;
|
|
191
191
|
await record.promise;
|
|
@@ -194,24 +194,24 @@ export class AgentManager {
|
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
196
|
* Resume an existing agent session with a new prompt.
|
|
197
|
-
* Delegates to
|
|
197
|
+
* Delegates to Subagent.resume(), which owns the observer subscription lifecycle.
|
|
198
198
|
*/
|
|
199
199
|
async resume(
|
|
200
200
|
id: string,
|
|
201
201
|
prompt: string,
|
|
202
202
|
signal?: AbortSignal,
|
|
203
|
-
): Promise<
|
|
203
|
+
): Promise<Subagent | undefined> {
|
|
204
204
|
const agent = this.agents.get(id);
|
|
205
205
|
if (!agent?.isSessionReady()) return undefined;
|
|
206
206
|
await agent.resume(prompt, signal);
|
|
207
207
|
return agent;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
getRecord(id: string):
|
|
210
|
+
getRecord(id: string): Subagent | undefined {
|
|
211
211
|
return this.agents.get(id);
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
listAgents():
|
|
214
|
+
listAgents(): Subagent[] {
|
|
215
215
|
return [...this.agents.values()].sort(
|
|
216
216
|
(a, b) => b.startedAt - a.startedAt,
|
|
217
217
|
);
|
|
@@ -232,7 +232,7 @@ export class AgentManager {
|
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
/** Dispose a record's session and remove it from the map. */
|
|
235
|
-
private removeRecord(id: string, record:
|
|
235
|
+
private removeRecord(id: string, record: Subagent): void {
|
|
236
236
|
record.disposeSession();
|
|
237
237
|
this.agents.delete(id);
|
|
238
238
|
}
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* A SubagentSession wraps one SDK AgentSession plus its turn-driving and teardown.
|
|
5
5
|
* It is born complete: `createSubagentSession()` returns a fully usable instance
|
|
6
6
|
* (session created, extensions bound, recursion guard applied), so the only thing
|
|
7
|
-
* left for `
|
|
7
|
+
* left for `Subagent` to do is coordinate — drive the turn loop, steer, dispose.
|
|
8
8
|
*
|
|
9
9
|
* Turn driving lives here, on the object that owns the AgentSession, rather than
|
|
10
|
-
* reaching through `subagentSession.session` from `
|
|
10
|
+
* reaching through `subagentSession.session` from `Subagent` (Law of Demeter).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {
|
|
@@ -44,7 +44,9 @@ export interface TurnLoopOptions {
|
|
|
44
44
|
export interface SubagentSessionMeta {
|
|
45
45
|
/** Path to the persisted session JSONL file, if the session was persisted. */
|
|
46
46
|
outputFile: string | undefined;
|
|
47
|
-
/** Child session
|
|
47
|
+
/** Child session id — the registry key carried on session-created/disposed events. */
|
|
48
|
+
sessionId: string;
|
|
49
|
+
/** Child session directory — carried on the completed event as transcript location. */
|
|
48
50
|
sessionDir: string;
|
|
49
51
|
agentName: string;
|
|
50
52
|
/** Per-agent max-turns from the resolved agent config — middle precedence. */
|
|
@@ -180,7 +182,7 @@ export class SubagentSession {
|
|
|
180
182
|
dispose(): void {
|
|
181
183
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- dispose may not exist on all session implementations
|
|
182
184
|
this._session.dispose?.();
|
|
183
|
-
this.meta.lifecycle.disposed({
|
|
185
|
+
this.meta.lifecycle.disposed({ sessionId: this.meta.sessionId });
|
|
184
186
|
}
|
|
185
187
|
}
|
|
186
188
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* subagent.ts — Subagent class with encapsulated status-transition logic and per-subagent behavior.
|
|
3
3
|
*
|
|
4
4
|
* Status transitions (status, result, error, startedAt, completedAt) are owned
|
|
5
5
|
* by the class and exposed via transition methods. External code reads these
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* Stats (toolUses, lifetimeUsage, compactionCount) are owned by the class and
|
|
9
9
|
* accumulated via mutation methods (incrementToolUses, addUsage, incrementCompactions).
|
|
10
10
|
*
|
|
11
|
-
* Behavior (abort, steer buffering) lives on the
|
|
12
|
-
*
|
|
11
|
+
* Behavior (abort, steer buffering) lives on the subagent rather than on
|
|
12
|
+
* SubagentManager — each subagent manages its own lifecycle concerns.
|
|
13
13
|
*
|
|
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.
|
|
@@ -28,23 +28,23 @@ 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";
|
|
30
30
|
import { NotificationState } from "#src/observation/notification-state";
|
|
31
|
-
import {
|
|
31
|
+
import { subscribeSubagentObserver } from "#src/observation/record-observer";
|
|
32
32
|
import type { RunConfig } from "#src/runtime";
|
|
33
33
|
import type { AgentInvocation, CompactionInfo, ParentSessionInfo, SubagentType, ThinkingLevel } from "#src/types";
|
|
34
34
|
|
|
35
|
-
/** Per-
|
|
36
|
-
export interface
|
|
37
|
-
/** Fires when the
|
|
38
|
-
onStarted?(agent:
|
|
39
|
-
/** Fires once the session is created — the
|
|
40
|
-
onSessionCreated?(agent:
|
|
35
|
+
/** Per-subagent lifecycle observer — created by SubagentManager for each spawn. */
|
|
36
|
+
export interface SubagentLifecycleObserver {
|
|
37
|
+
/** Fires when the subagent transitions to running (inside run(), after markRunning). */
|
|
38
|
+
onStarted?(agent: Subagent): void;
|
|
39
|
+
/** Fires once the session is created — the subagent's subagentSession is now available. */
|
|
40
|
+
onSessionCreated?(agent: Subagent): void;
|
|
41
41
|
/** Fires once when the run completes or fails (for concurrency drain). */
|
|
42
|
-
onRunFinished?(agent:
|
|
42
|
+
onRunFinished?(agent: Subagent): void;
|
|
43
43
|
/** Fires on compaction events during the run. */
|
|
44
|
-
onCompacted?(agent:
|
|
44
|
+
onCompacted?(agent: Subagent, info: CompactionInfo): void;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export type
|
|
47
|
+
export type SubagentStatus =
|
|
48
48
|
| "queued"
|
|
49
49
|
| "running"
|
|
50
50
|
| "completed"
|
|
@@ -53,7 +53,7 @@ export type AgentStatus =
|
|
|
53
53
|
| "stopped"
|
|
54
54
|
| "error";
|
|
55
55
|
|
|
56
|
-
export interface
|
|
56
|
+
export interface SubagentInit {
|
|
57
57
|
// Identity
|
|
58
58
|
id: string;
|
|
59
59
|
type: SubagentType;
|
|
@@ -61,7 +61,7 @@ export interface AgentInit {
|
|
|
61
61
|
invocation?: AgentInvocation;
|
|
62
62
|
|
|
63
63
|
// Status (for tests and restore scenarios)
|
|
64
|
-
status?:
|
|
64
|
+
status?: SubagentStatus;
|
|
65
65
|
startedAt?: number;
|
|
66
66
|
completedAt?: number;
|
|
67
67
|
result?: string;
|
|
@@ -70,7 +70,7 @@ export interface AgentInit {
|
|
|
70
70
|
// Shared deps (required for run(), optional for tests)
|
|
71
71
|
/** Assembly factory that produces a born-complete SubagentSession. */
|
|
72
72
|
createSubagentSession?: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
|
|
73
|
-
observer?:
|
|
73
|
+
observer?: SubagentLifecycleObserver;
|
|
74
74
|
getRunConfig?: () => RunConfig;
|
|
75
75
|
/** Resolves the registered workspace provider (if any) at run-start. */
|
|
76
76
|
getWorkspaceProvider?: () => WorkspaceProvider | undefined;
|
|
@@ -88,7 +88,7 @@ export interface AgentInit {
|
|
|
88
88
|
signal?: AbortSignal;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export class
|
|
91
|
+
export class Subagent {
|
|
92
92
|
// Identity — set once at construction
|
|
93
93
|
readonly id: string;
|
|
94
94
|
readonly type: SubagentType;
|
|
@@ -96,8 +96,8 @@ export class Agent {
|
|
|
96
96
|
readonly invocation?: AgentInvocation;
|
|
97
97
|
|
|
98
98
|
// Transition state — encapsulated behind getters, mutated only via transition methods
|
|
99
|
-
private _status:
|
|
100
|
-
get status():
|
|
99
|
+
private _status: SubagentStatus;
|
|
100
|
+
get status(): SubagentStatus { return this._status; }
|
|
101
101
|
|
|
102
102
|
private _result?: string;
|
|
103
103
|
get result(): string | undefined { return this._result; }
|
|
@@ -128,7 +128,7 @@ export class Agent {
|
|
|
128
128
|
|
|
129
129
|
// Shared deps — optional (required for run())
|
|
130
130
|
private readonly _createSubagentSession?: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
|
|
131
|
-
readonly observer?:
|
|
131
|
+
readonly observer?: SubagentLifecycleObserver;
|
|
132
132
|
private readonly _getRunConfig?: () => RunConfig;
|
|
133
133
|
private readonly _getWorkspaceProvider?: () => WorkspaceProvider | undefined;
|
|
134
134
|
private readonly _baseCwd: string;
|
|
@@ -200,7 +200,7 @@ export class Agent {
|
|
|
200
200
|
return this.subagentSession?.messages ?? [];
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
constructor(init:
|
|
203
|
+
constructor(init: SubagentInit) {
|
|
204
204
|
// Identity
|
|
205
205
|
this.id = init.id;
|
|
206
206
|
this.type = init.type;
|
|
@@ -254,10 +254,10 @@ export class Agent {
|
|
|
254
254
|
*/
|
|
255
255
|
async run(): Promise<void> {
|
|
256
256
|
if (!this._createSubagentSession) {
|
|
257
|
-
throw new Error("
|
|
257
|
+
throw new Error("Subagent not configured for execution — missing session factory");
|
|
258
258
|
}
|
|
259
259
|
if (!this._snapshot || !this._prompt) {
|
|
260
|
-
throw new Error("
|
|
260
|
+
throw new Error("Subagent not configured for execution — missing snapshot or prompt");
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
this.markRunning(Date.now());
|
|
@@ -301,7 +301,7 @@ export class Agent {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
this.flushPendingSteers();
|
|
304
|
-
this.attachObserver(
|
|
304
|
+
this.attachObserver(subscribeSubagentObserver(this.subagentSession, this, {
|
|
305
305
|
onCompact: (r, info) => this.observer?.onCompacted?.(r, info),
|
|
306
306
|
}));
|
|
307
307
|
this.observer?.onSessionCreated?.(this);
|
|
@@ -332,11 +332,11 @@ export class Agent {
|
|
|
332
332
|
async resume(prompt: string, signal?: AbortSignal): Promise<void> {
|
|
333
333
|
const subagentSession = this.subagentSession;
|
|
334
334
|
if (!subagentSession) {
|
|
335
|
-
throw new Error("
|
|
335
|
+
throw new Error("Subagent not configured for resume — missing session");
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
this.resetForResume(Date.now());
|
|
339
|
-
this.attachObserver(
|
|
339
|
+
this.attachObserver(subscribeSubagentObserver(subagentSession, this, {
|
|
340
340
|
onCompact: (r, info) => this.observer?.onCompacted?.(r, info),
|
|
341
341
|
}));
|
|
342
342
|
|
|
@@ -428,7 +428,7 @@ export class Agent {
|
|
|
428
428
|
/**
|
|
429
429
|
* Abort a running agent: fire AbortController and transition to stopped.
|
|
430
430
|
* Returns false if the agent is not running.
|
|
431
|
-
* Queue removal is handled by
|
|
431
|
+
* Queue removal is handled by SubagentManager via ConcurrencyQueue.dequeue().
|
|
432
432
|
*/
|
|
433
433
|
abort(): boolean {
|
|
434
434
|
if (this._status !== "running") return false;
|
|
@@ -497,7 +497,7 @@ export class Agent {
|
|
|
497
497
|
|
|
498
498
|
let finalResult = result.responseText;
|
|
499
499
|
if (this._workspace) {
|
|
500
|
-
const finalStatus:
|
|
500
|
+
const finalStatus: SubagentStatus = result.aborted
|
|
501
501
|
? "aborted"
|
|
502
502
|
: result.steered
|
|
503
503
|
? "steered"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* turn-limits.ts — Pure turn-limit normalization for subagent execution.
|
|
3
3
|
*
|
|
4
4
|
* Extracted from agent-runner.ts (issue #265) so the turn-counting policy has a
|
|
5
|
-
* focused home independent of session assembly. Consumed by the
|
|
5
|
+
* focused home independent of session assembly. Consumed by the subagent tool's
|
|
6
6
|
* spawn-config resolution and by the turn loop in SubagentSession.
|
|
7
7
|
*/
|
|
8
8
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* synchronously at run-start. The core has no knowledge of git or worktrees.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type {
|
|
14
|
+
import type { SubagentStatus } from "#src/lifecycle/subagent";
|
|
15
15
|
import type { AgentInvocation, SubagentType } from "#src/types";
|
|
16
16
|
|
|
17
17
|
/** Context the core hands a provider when a child run starts. */
|
|
@@ -24,7 +24,7 @@ export interface WorkspacePrepareContext {
|
|
|
24
24
|
|
|
25
25
|
/** Outcome the core reports to a workspace when the run ends. */
|
|
26
26
|
export interface WorkspaceDisposeOutcome {
|
|
27
|
-
status:
|
|
27
|
+
status: SubagentStatus;
|
|
28
28
|
description: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { debugLog } from "#src/debug";
|
|
2
2
|
import { getLifetimeTotal } from "#src/lifecycle/usage";
|
|
3
|
-
import type {
|
|
3
|
+
import type { Subagent } from "#src/types";
|
|
4
4
|
import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
5
5
|
|
|
6
6
|
/** Details attached to custom notification messages for visual rendering. */
|
|
@@ -42,7 +42,7 @@ export function getStatusLabel(status: string, error?: string): string {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** Format a structured task notification matching Claude Code's <task-notification> XML. */
|
|
45
|
-
export function formatTaskNotification(record:
|
|
45
|
+
export function formatTaskNotification(record: Subagent, resultMaxLen: number): string {
|
|
46
46
|
const status = getStatusLabel(record.status, record.error);
|
|
47
47
|
const durationMs = record.completedAt ? record.completedAt - record.startedAt : 0;
|
|
48
48
|
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
@@ -64,7 +64,7 @@ export function formatTaskNotification(record: Agent, resultMaxLen: number): str
|
|
|
64
64
|
toolCallId ? `<tool-use-id>${escapeXml(toolCallId)}</tool-use-id>` : null,
|
|
65
65
|
outputFile ? `<output-file>${escapeXml(outputFile)}</output-file>` : null,
|
|
66
66
|
`<status>${escapeXml(status)}</status>`,
|
|
67
|
-
`<summary>
|
|
67
|
+
`<summary>Subagent "${escapeXml(record.description)}" ${record.status}</summary>`,
|
|
68
68
|
`<result>${escapeXml(resultPreview)}</result>`,
|
|
69
69
|
`<usage><total_tokens>${totalTokens}</total_tokens><tool_uses>${record.toolUses}</tool_uses>${ctxXml}${compactXml}<duration_ms>${durationMs}</duration_ms></usage>`,
|
|
70
70
|
"</task-notification>",
|
|
@@ -75,7 +75,7 @@ export function formatTaskNotification(record: Agent, resultMaxLen: number): str
|
|
|
75
75
|
|
|
76
76
|
/** Build notification details for the custom message renderer. */
|
|
77
77
|
export function buildNotificationDetails(
|
|
78
|
-
record:
|
|
78
|
+
record: Subagent,
|
|
79
79
|
resultMaxLen: number,
|
|
80
80
|
activity?: AgentActivityTracker,
|
|
81
81
|
): NotificationDetails {
|
|
@@ -100,8 +100,8 @@ export function buildNotificationDetails(
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
/** Build event data for lifecycle events from
|
|
104
|
-
export function buildEventData(record:
|
|
103
|
+
/** Build event data for lifecycle events from a Subagent. */
|
|
104
|
+
export function buildEventData(record: Subagent) {
|
|
105
105
|
const durationMs = record.completedAt ? record.completedAt - record.startedAt : Date.now() - record.startedAt;
|
|
106
106
|
const u = record.lifetimeUsage;
|
|
107
107
|
const total = getLifetimeTotal(u);
|
|
@@ -126,7 +126,7 @@ export function buildEventData(record: Agent) {
|
|
|
126
126
|
|
|
127
127
|
export interface NotificationSystem {
|
|
128
128
|
cancelNudge: (key: string) => void;
|
|
129
|
-
sendCompletion: (record:
|
|
129
|
+
sendCompletion: (record: Subagent) => void;
|
|
130
130
|
cleanupCompleted: (id: string) => void;
|
|
131
131
|
dispose: () => void;
|
|
132
132
|
}
|
|
@@ -154,7 +154,7 @@ export class NotificationManager implements NotificationSystem {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
sendCompletion(record:
|
|
157
|
+
sendCompletion(record: Subagent): void {
|
|
158
158
|
this.agentActivity.delete(record.id);
|
|
159
159
|
this.markFinished(record.id);
|
|
160
160
|
this.scheduleNudge(record.id, () => this.emitIndividualNudge(record));
|
|
@@ -187,7 +187,7 @@ export class NotificationManager implements NotificationSystem {
|
|
|
187
187
|
);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
private emitIndividualNudge(record:
|
|
190
|
+
private emitIndividualNudge(record: Subagent): void {
|
|
191
191
|
if (record.notification?.resultConsumed) return;
|
|
192
192
|
|
|
193
193
|
const notification = formatTaskNotification(record, 500);
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* record-observer.ts — Subscribes to session events and updates
|
|
2
|
+
* record-observer.ts — Subscribes to session events and updates Subagent stats.
|
|
3
3
|
*
|
|
4
|
-
* Replaces the scattered callback-wrapping logic in
|
|
4
|
+
* Replaces the scattered callback-wrapping logic in SubagentManager's startAgent()
|
|
5
5
|
* and resume() with a single direct subscription.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type {
|
|
8
|
+
import type { Subagent } from "#src/lifecycle/subagent";
|
|
9
9
|
import type { CompactionInfo, SubscribableSession } from "#src/types";
|
|
10
10
|
|
|
11
|
-
export interface
|
|
12
|
-
onCompact?: (record:
|
|
11
|
+
export interface SubagentObserverOptions {
|
|
12
|
+
onCompact?: (record: Subagent, info: CompactionInfo) => void;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Subscribe to session events and accumulate stats on the
|
|
16
|
+
* Subscribe to session events and accumulate stats on the subagent record.
|
|
17
17
|
*
|
|
18
18
|
* Handles:
|
|
19
19
|
* - `tool_execution_end` → `record.incrementToolUses()`
|
|
@@ -22,10 +22,10 @@ export interface AgentObserverOptions {
|
|
|
22
22
|
*
|
|
23
23
|
* @returns An unsubscribe function.
|
|
24
24
|
*/
|
|
25
|
-
export function
|
|
25
|
+
export function subscribeSubagentObserver(
|
|
26
26
|
session: SubscribableSession,
|
|
27
|
-
record:
|
|
28
|
-
options?:
|
|
27
|
+
record: Subagent,
|
|
28
|
+
options?: SubagentObserverOptions,
|
|
29
29
|
): () => void {
|
|
30
30
|
return session.subscribe((event) => {
|
|
31
31
|
if (event.type === "tool_execution_end") {
|
package/src/runtime.ts
CHANGED
|
@@ -49,7 +49,7 @@ export class SubagentRuntime {
|
|
|
49
49
|
*/
|
|
50
50
|
readonly agentActivity: Map<string, AgentActivityTracker> = new Map();
|
|
51
51
|
/**
|
|
52
|
-
* Persistent widget reference. Null until constructed after
|
|
52
|
+
* Persistent widget reference. Null until constructed after SubagentManager.
|
|
53
53
|
* Delegation methods use optional chaining so callers never need `widget!`.
|
|
54
54
|
*/
|
|
55
55
|
widget: WidgetLike | null = null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* service-adapter.ts — Adapter that wraps
|
|
2
|
+
* service-adapter.ts — Adapter that wraps SubagentManager to satisfy SubagentsService.
|
|
3
3
|
*
|
|
4
4
|
* Handles model resolution at the API boundary, record serialization
|
|
5
5
|
* (stripping non-serializable fields), and session gating.
|
|
@@ -9,13 +9,13 @@ import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
|
9
9
|
import type { WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
10
10
|
import type { SpawnOptions, SubagentRecord, SubagentsService } from "#src/service/service";
|
|
11
11
|
import type { ModelRegistry } from "#src/session/model-resolver";
|
|
12
|
-
import type {
|
|
12
|
+
import type { SessionContext, Subagent } from "#src/types";
|
|
13
13
|
|
|
14
|
-
/** Narrow interface for the
|
|
15
|
-
export interface
|
|
14
|
+
/** Narrow interface for the SubagentManager — avoids coupling to the concrete class. */
|
|
15
|
+
export interface SubagentManagerLike {
|
|
16
16
|
spawn(snapshot: ParentSnapshot, type: string, prompt: string, options: unknown): string;
|
|
17
|
-
getRecord(id: string):
|
|
18
|
-
listAgents():
|
|
17
|
+
getRecord(id: string): Subagent | undefined;
|
|
18
|
+
listAgents(): Subagent[];
|
|
19
19
|
abort(id: string): boolean;
|
|
20
20
|
waitForAll(): Promise<void>;
|
|
21
21
|
hasRunning(): boolean;
|
|
@@ -31,10 +31,10 @@ export interface ServiceRuntimeLike {
|
|
|
31
31
|
buildSnapshot(inheritContext: boolean): ParentSnapshot;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/** Adapter that wraps
|
|
34
|
+
/** Adapter that wraps SubagentManager to satisfy SubagentsService. */
|
|
35
35
|
export class SubagentsServiceAdapter implements SubagentsService {
|
|
36
36
|
constructor(
|
|
37
|
-
private readonly manager:
|
|
37
|
+
private readonly manager: SubagentManagerLike,
|
|
38
38
|
private readonly resolveModel: (input: string, registry: ModelRegistry) => unknown,
|
|
39
39
|
private readonly runtime: ServiceRuntimeLike,
|
|
40
40
|
) {}
|
|
@@ -108,10 +108,10 @@ export class SubagentsServiceAdapter implements SubagentsService {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
|
-
* Convert an internal
|
|
111
|
+
* Convert an internal Subagent to a serializable SubagentRecord.
|
|
112
112
|
* Uses an explicit allowlist — new fields must be opted in.
|
|
113
113
|
*/
|
|
114
|
-
export function toSubagentRecord(record:
|
|
114
|
+
export function toSubagentRecord(record: Subagent): SubagentRecord {
|
|
115
115
|
const out: SubagentRecord = {
|
|
116
116
|
id: record.id,
|
|
117
117
|
type: record.type,
|
package/src/service/service.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* svc?.spawn("Explore", "Check for stale TODOs");
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import type { SubagentStatus } from "#src/lifecycle/subagent";
|
|
12
13
|
import type { LifetimeUsage } from "#src/lifecycle/usage";
|
|
13
14
|
import type {
|
|
14
15
|
Workspace,
|
|
@@ -18,6 +19,10 @@ import type {
|
|
|
18
19
|
WorkspaceProvider,
|
|
19
20
|
} from "#src/lifecycle/workspace";
|
|
20
21
|
|
|
22
|
+
|
|
23
|
+
// SubagentStatus is defined in the lifecycle layer (single home) and re-exported
|
|
24
|
+
// here for the public API surface — mirrors the LifetimeUsage / workspace pattern.
|
|
25
|
+
export type { SubagentStatus } from "#src/lifecycle/subagent";
|
|
21
26
|
// Generative extension seam (ADR 0002, Phase 16 Step 2). The provider type
|
|
22
27
|
// and all four collaborator types it references are re-exported by name so
|
|
23
28
|
// consumers can import them directly rather than recovering them via
|
|
@@ -31,15 +36,6 @@ export type {
|
|
|
31
36
|
WorkspaceProvider,
|
|
32
37
|
};
|
|
33
38
|
|
|
34
|
-
export type SubagentStatus =
|
|
35
|
-
| "queued"
|
|
36
|
-
| "running"
|
|
37
|
-
| "completed"
|
|
38
|
-
| "steered"
|
|
39
|
-
| "aborted"
|
|
40
|
-
| "stopped"
|
|
41
|
-
| "error";
|
|
42
|
-
|
|
43
39
|
/** Serializable snapshot of an agent's state — no live session objects. */
|
|
44
40
|
export interface SubagentRecord {
|
|
45
41
|
id: string;
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -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 } from "#src/lifecycle/agent-manager";
|
|
8
7
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
8
|
+
import type { AgentSpawnConfig } from "#src/lifecycle/subagent-manager";
|
|
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 {
|
|
14
|
+
import type { ParentSessionInfo, Subagent } 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";
|
|
@@ -33,9 +33,9 @@ export interface AgentActivityAccess {
|
|
|
33
33
|
/** Narrow manager interface — only the methods the Agent tool calls. */
|
|
34
34
|
export interface AgentToolManager {
|
|
35
35
|
spawn: (snapshot: ParentSnapshot, type: string, prompt: string, opts: AgentSpawnConfig) => string;
|
|
36
|
-
spawnAndWait: (snapshot: ParentSnapshot, type: string, prompt: string, opts: Omit<AgentSpawnConfig, "isBackground">) => Promise<
|
|
37
|
-
resume: (id: string, prompt: string, signal: AbortSignal) => Promise<
|
|
38
|
-
getRecord: (id: string) =>
|
|
36
|
+
spawnAndWait: (snapshot: ParentSnapshot, type: string, prompt: string, opts: Omit<AgentSpawnConfig, "isBackground">) => Promise<Subagent>;
|
|
37
|
+
resume: (id: string, prompt: string, signal: AbortSignal) => Promise<Subagent | undefined>;
|
|
38
|
+
getRecord: (id: string) => Subagent | undefined;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/** Narrow runtime interface — the Agent tool's slice of SubagentRuntime. */
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import type { AgentSpawnConfig } from "#src/lifecycle/agent-manager";
|
|
2
1
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
2
|
+
import type { AgentSpawnConfig } from "#src/lifecycle/subagent-manager";
|
|
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 {
|
|
6
|
+
import type { ParentSessionInfo, Subagent } from "#src/types";
|
|
7
7
|
import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
8
8
|
import { subscribeUIObserver } from "#src/ui/ui-observer";
|
|
9
9
|
|
|
10
10
|
/** Narrow manager interface for the background spawner. */
|
|
11
11
|
export interface BackgroundManagerDeps {
|
|
12
12
|
spawn(snapshot: ParentSnapshot, type: string, prompt: string, opts: AgentSpawnConfig): string;
|
|
13
|
-
getRecord(id: string):
|
|
13
|
+
getRecord(id: string): Subagent | undefined;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/** Narrow widget interface for the background spawner. */
|