@gotgenes/pi-subagents 6.1.0 → 6.3.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 +28 -0
- package/docs/architecture/architecture.md +5 -3
- package/docs/plans/0099-replace-ctx-with-parent-snapshot.md +488 -0
- package/docs/plans/0100-replace-callback-threading-with-session-subscription.md +454 -0
- package/docs/retro/0098-extract-agent-record-state-machine.md +46 -0
- package/docs/retro/0099-replace-ctx-with-parent-snapshot.md +37 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +30 -50
- package/src/agent-runner.ts +18 -135
- package/src/env.ts +4 -5
- package/src/index.ts +4 -3
- package/src/parent-snapshot.ts +27 -0
- package/src/record-observer.ts +60 -0
- package/src/service-adapter.ts +2 -2
- package/src/tools/agent-tool.ts +27 -64
- package/src/types.ts +30 -0
- package/src/ui/agent-menu.ts +2 -3
- package/src/ui/ui-observer.ts +83 -0
package/src/agent-manager.ts
CHANGED
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
10
|
import type { Model } from "@earendil-works/pi-ai";
|
|
11
|
-
import type { AgentSession,
|
|
11
|
+
import type { AgentSession, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { AgentRecord } from "./agent-record.js";
|
|
13
|
-
import type { AgentRunner
|
|
13
|
+
import type { AgentRunner } from "./agent-runner.js";
|
|
14
14
|
import { debugLog } from "./debug.js";
|
|
15
|
+
import { buildParentSnapshot } from "./parent-snapshot.js";
|
|
16
|
+
import { subscribeRecordObserver } from "./record-observer.js";
|
|
15
17
|
import type { RunConfig } from "./runtime.js";
|
|
16
|
-
import type { AgentInvocation, IsolationMode, SubagentType, ThinkingLevel } from "./types.js";
|
|
17
|
-
import { addUsage } from "./usage.js";
|
|
18
|
+
import type { AgentInvocation, IsolationMode, ParentSnapshot, ShellExec, SubagentType, ThinkingLevel } from "./types.js";
|
|
18
19
|
import type { WorktreeManager } from "./worktree.js";
|
|
19
20
|
|
|
20
21
|
export type OnAgentComplete = (record: AgentRecord) => void;
|
|
@@ -28,6 +29,7 @@ const DEFAULT_MAX_CONCURRENT = 4;
|
|
|
28
29
|
export interface AgentManagerOptions {
|
|
29
30
|
runner: AgentRunner;
|
|
30
31
|
worktrees: WorktreeManager;
|
|
32
|
+
exec: ShellExec;
|
|
31
33
|
maxConcurrent?: number;
|
|
32
34
|
getRunConfig?: () => RunConfig;
|
|
33
35
|
onStart?: OnAgentStart;
|
|
@@ -36,8 +38,7 @@ export interface AgentManagerOptions {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
interface SpawnArgs {
|
|
39
|
-
|
|
40
|
-
ctx: ExtensionContext;
|
|
41
|
+
snapshot: ParentSnapshot;
|
|
41
42
|
type: SubagentType;
|
|
42
43
|
prompt: string;
|
|
43
44
|
options: SpawnOptions;
|
|
@@ -63,18 +64,8 @@ export interface SpawnOptions {
|
|
|
63
64
|
invocation?: AgentInvocation;
|
|
64
65
|
/** Parent abort signal — when aborted, the subagent is also stopped. */
|
|
65
66
|
signal?: AbortSignal;
|
|
66
|
-
/** Called
|
|
67
|
-
onToolActivity?: (activity: ToolActivity) => void;
|
|
68
|
-
/** Called on streaming text deltas from the assistant response. */
|
|
69
|
-
onTextDelta?: (delta: string, fullText: string) => void;
|
|
70
|
-
/** Called when the agent session is created (for accessing session stats). */
|
|
67
|
+
/** Called when the agent session is created — the one remaining callback. */
|
|
71
68
|
onSessionCreated?: (session: AgentSession) => void;
|
|
72
|
-
/** Called at the end of each agentic turn with the cumulative count. */
|
|
73
|
-
onTurnEnd?: (turnCount: number) => void;
|
|
74
|
-
/** Called once per assistant message_end with that message's usage delta. */
|
|
75
|
-
onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
|
|
76
|
-
/** Called when the session successfully compacts. */
|
|
77
|
-
onCompaction?: (info: CompactionInfo) => void;
|
|
78
69
|
/** Path to the parent session's JSONL file (for deriving the subagent session directory). */
|
|
79
70
|
parentSessionFile?: string;
|
|
80
71
|
/** Session ID of the parent agent (stored in the child session's parentSession header). */
|
|
@@ -89,6 +80,7 @@ export class AgentManager {
|
|
|
89
80
|
private onCompact?: OnAgentCompact;
|
|
90
81
|
private readonly runner: AgentRunner;
|
|
91
82
|
private readonly worktrees: WorktreeManager;
|
|
83
|
+
private readonly exec: ShellExec;
|
|
92
84
|
private maxConcurrent: number;
|
|
93
85
|
private getRunConfig?: () => RunConfig;
|
|
94
86
|
|
|
@@ -100,6 +92,7 @@ export class AgentManager {
|
|
|
100
92
|
constructor(options: AgentManagerOptions) {
|
|
101
93
|
this.runner = options.runner;
|
|
102
94
|
this.worktrees = options.worktrees;
|
|
95
|
+
this.exec = options.exec;
|
|
103
96
|
this.onComplete = options.onComplete;
|
|
104
97
|
this.onStart = options.onStart;
|
|
105
98
|
this.onCompact = options.onCompact;
|
|
@@ -126,7 +119,6 @@ export class AgentManager {
|
|
|
126
119
|
* If the concurrency limit is reached, the agent is queued.
|
|
127
120
|
*/
|
|
128
121
|
spawn(
|
|
129
|
-
pi: ExtensionAPI,
|
|
130
122
|
ctx: ExtensionContext,
|
|
131
123
|
type: SubagentType,
|
|
132
124
|
prompt: string,
|
|
@@ -145,7 +137,8 @@ export class AgentManager {
|
|
|
145
137
|
});
|
|
146
138
|
this.agents.set(id, record);
|
|
147
139
|
|
|
148
|
-
const
|
|
140
|
+
const snapshot = buildParentSnapshot(ctx, options.inheritContext);
|
|
141
|
+
const args: SpawnArgs = { snapshot, type, prompt, options };
|
|
149
142
|
|
|
150
143
|
if (options.isBackground && !options.bypassQueue && this.runningBackground >= this.maxConcurrent) {
|
|
151
144
|
// Queue it — will be started when a running agent completes
|
|
@@ -165,7 +158,7 @@ export class AgentManager {
|
|
|
165
158
|
}
|
|
166
159
|
|
|
167
160
|
/** Actually start an agent (called immediately or from queue drain). */
|
|
168
|
-
private startAgent(id: string, record: AgentRecord, {
|
|
161
|
+
private startAgent(id: string, record: AgentRecord, { snapshot, type, prompt, options }: SpawnArgs) {
|
|
169
162
|
// Worktree isolation: try to create a temporary git worktree. Strict —
|
|
170
163
|
// fail loud if not possible (no silent fallback to main tree). Done
|
|
171
164
|
// BEFORE state mutation so a throw doesn't leave the record half-running.
|
|
@@ -195,35 +188,21 @@ export class AgentManager {
|
|
|
195
188
|
}
|
|
196
189
|
const detach = () => { detachParentSignal?.(); detachParentSignal = undefined; };
|
|
197
190
|
|
|
191
|
+
let unsubRecordObserver: (() => void) | undefined;
|
|
192
|
+
|
|
198
193
|
const runConfig = this.getRunConfig?.();
|
|
199
|
-
const promise = this.runner.run(
|
|
200
|
-
|
|
194
|
+
const promise = this.runner.run(snapshot, type, prompt, {
|
|
195
|
+
exec: this.exec,
|
|
201
196
|
model: options.model,
|
|
202
197
|
maxTurns: options.maxTurns,
|
|
203
198
|
defaultMaxTurns: runConfig?.defaultMaxTurns,
|
|
204
199
|
graceTurns: runConfig?.graceTurns,
|
|
205
200
|
isolated: options.isolated,
|
|
206
|
-
inheritContext: options.inheritContext,
|
|
207
201
|
thinkingLevel: options.thinkingLevel,
|
|
208
202
|
cwd: worktreeCwd,
|
|
209
203
|
parentSessionFile: options.parentSessionFile,
|
|
210
204
|
parentSessionId: options.parentSessionId,
|
|
211
205
|
signal: record.abortController!.signal,
|
|
212
|
-
onToolActivity: (activity) => {
|
|
213
|
-
if (activity.type === "end") record.toolUses++;
|
|
214
|
-
options.onToolActivity?.(activity);
|
|
215
|
-
},
|
|
216
|
-
onTurnEnd: options.onTurnEnd,
|
|
217
|
-
onTextDelta: options.onTextDelta,
|
|
218
|
-
onAssistantUsage: (usage) => {
|
|
219
|
-
addUsage(record.lifetimeUsage, usage);
|
|
220
|
-
options.onAssistantUsage?.(usage);
|
|
221
|
-
},
|
|
222
|
-
onCompaction: (info) => {
|
|
223
|
-
record.compactionCount++;
|
|
224
|
-
this.onCompact?.(record, info);
|
|
225
|
-
options.onCompaction?.(info);
|
|
226
|
-
},
|
|
227
206
|
onSessionCreated: (session) => {
|
|
228
207
|
record.session = session;
|
|
229
208
|
// Capture the session file path early so it's available for display
|
|
@@ -237,10 +216,15 @@ export class AgentManager {
|
|
|
237
216
|
}
|
|
238
217
|
record.pendingSteers = undefined;
|
|
239
218
|
}
|
|
219
|
+
// Subscribe record observer for stats accumulation
|
|
220
|
+
unsubRecordObserver = subscribeRecordObserver(session, record, {
|
|
221
|
+
onCompact: (r, info) => this.onCompact?.(r, info),
|
|
222
|
+
});
|
|
240
223
|
options.onSessionCreated?.(session);
|
|
241
224
|
},
|
|
242
225
|
})
|
|
243
226
|
.then(({ responseText, session, aborted, steered, sessionFile }) => {
|
|
227
|
+
unsubRecordObserver?.();
|
|
244
228
|
detach();
|
|
245
229
|
|
|
246
230
|
// Clean up worktree before transition so the final result includes branch text
|
|
@@ -271,6 +255,7 @@ export class AgentManager {
|
|
|
271
255
|
.catch((err) => {
|
|
272
256
|
record.markError(err);
|
|
273
257
|
|
|
258
|
+
unsubRecordObserver?.();
|
|
274
259
|
detach();
|
|
275
260
|
|
|
276
261
|
// Best-effort worktree cleanup on error
|
|
@@ -314,13 +299,12 @@ export class AgentManager {
|
|
|
314
299
|
* Foreground agents bypass the concurrency queue.
|
|
315
300
|
*/
|
|
316
301
|
async spawnAndWait(
|
|
317
|
-
pi: ExtensionAPI,
|
|
318
302
|
ctx: ExtensionContext,
|
|
319
303
|
type: SubagentType,
|
|
320
304
|
prompt: string,
|
|
321
305
|
options: Omit<SpawnOptions, "isBackground">,
|
|
322
306
|
): Promise<AgentRecord> {
|
|
323
|
-
const id = this.spawn(
|
|
307
|
+
const id = this.spawn(ctx, type, prompt, { ...options, isBackground: false });
|
|
324
308
|
const record = this.agents.get(id)!;
|
|
325
309
|
await record.promise;
|
|
326
310
|
return record;
|
|
@@ -339,23 +323,19 @@ export class AgentManager {
|
|
|
339
323
|
|
|
340
324
|
record.resetForResume(Date.now());
|
|
341
325
|
|
|
326
|
+
const unsubResume = subscribeRecordObserver(record.session, record, {
|
|
327
|
+
onCompact: (r, info) => this.onCompact?.(r, info),
|
|
328
|
+
});
|
|
329
|
+
|
|
342
330
|
try {
|
|
343
331
|
const responseText = await this.runner.resume(record.session, prompt, {
|
|
344
|
-
onToolActivity: (activity) => {
|
|
345
|
-
if (activity.type === "end") record.toolUses++;
|
|
346
|
-
},
|
|
347
|
-
onAssistantUsage: (usage) => {
|
|
348
|
-
addUsage(record.lifetimeUsage, usage);
|
|
349
|
-
},
|
|
350
|
-
onCompaction: (info) => {
|
|
351
|
-
record.compactionCount++;
|
|
352
|
-
this.onCompact?.(record, info);
|
|
353
|
-
},
|
|
354
332
|
signal,
|
|
355
333
|
});
|
|
356
334
|
record.markCompleted(responseText);
|
|
357
335
|
} catch (err) {
|
|
358
336
|
record.markError(err);
|
|
337
|
+
} finally {
|
|
338
|
+
unsubResume();
|
|
359
339
|
}
|
|
360
340
|
|
|
361
341
|
return record;
|
package/src/agent-runner.ts
CHANGED
|
@@ -3,22 +3,20 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { Model } from "@earendil-works/pi-ai";
|
|
6
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
6
|
import {
|
|
8
7
|
type AgentSession,
|
|
9
8
|
type AgentSessionEvent,
|
|
10
9
|
createAgentSession,
|
|
11
10
|
DefaultResourceLoader,
|
|
12
|
-
type ExtensionAPI,
|
|
13
11
|
getAgentDir,
|
|
14
12
|
SessionManager,
|
|
15
13
|
SettingsManager,
|
|
16
14
|
} from "@earendil-works/pi-coding-agent";
|
|
17
|
-
import {
|
|
15
|
+
import { extractText } from "./context.js";
|
|
18
16
|
import { detectEnv } from "./env.js";
|
|
19
17
|
import { assembleSessionConfig } from "./session-config.js";
|
|
20
18
|
import { deriveSubagentSessionDir } from "./session-dir.js";
|
|
21
|
-
import type { SubagentType, ThinkingLevel } from "./types.js";
|
|
19
|
+
import type { ParentSnapshot, ShellExec, SubagentType, ThinkingLevel } from "./types.js";
|
|
22
20
|
|
|
23
21
|
/** Names of tools registered by this extension that subagents must NOT inherit. */
|
|
24
22
|
const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"];
|
|
@@ -66,20 +64,13 @@ export function normalizeMaxTurns(n: number | undefined): number | undefined {
|
|
|
66
64
|
}
|
|
67
65
|
|
|
68
66
|
|
|
69
|
-
/** Info about a tool event in the subagent. */
|
|
70
|
-
export interface ToolActivity {
|
|
71
|
-
type: "start" | "end";
|
|
72
|
-
toolName: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
67
|
export interface RunOptions {
|
|
76
|
-
/**
|
|
77
|
-
|
|
68
|
+
/** Shell-exec callback for detectEnv — injected from pi.exec(). */
|
|
69
|
+
exec: ShellExec;
|
|
78
70
|
model?: Model<any>;
|
|
79
71
|
maxTurns?: number;
|
|
80
72
|
signal?: AbortSignal;
|
|
81
73
|
isolated?: boolean;
|
|
82
|
-
inheritContext?: boolean;
|
|
83
74
|
thinkingLevel?: ThinkingLevel;
|
|
84
75
|
/** Override working directory (e.g. for worktree isolation). */
|
|
85
76
|
cwd?: string;
|
|
@@ -87,31 +78,8 @@ export interface RunOptions {
|
|
|
87
78
|
parentSessionFile?: string;
|
|
88
79
|
/** Session ID of the parent agent (stored in the child session's parentSession header). */
|
|
89
80
|
parentSessionId?: string;
|
|
90
|
-
/** Called
|
|
91
|
-
onToolActivity?: (activity: ToolActivity) => void;
|
|
92
|
-
/** Called on streaming text deltas from the assistant response. */
|
|
93
|
-
onTextDelta?: (delta: string, fullText: string) => void;
|
|
81
|
+
/** Called once after session creation — session delivery mechanism. */
|
|
94
82
|
onSessionCreated?: (session: AgentSession) => void;
|
|
95
|
-
/** Called at the end of each agentic turn with the cumulative count. */
|
|
96
|
-
onTurnEnd?: (turnCount: number) => void;
|
|
97
|
-
/**
|
|
98
|
-
* Called once per assistant message_end with that message's usage delta.
|
|
99
|
-
* Lets callers maintain a lifetime accumulator that survives compaction
|
|
100
|
-
* (which replaces session.state.messages and resets stats-derived sums).
|
|
101
|
-
*/
|
|
102
|
-
onAssistantUsage?: (usage: {
|
|
103
|
-
input: number;
|
|
104
|
-
output: number;
|
|
105
|
-
cacheWrite: number;
|
|
106
|
-
}) => void;
|
|
107
|
-
/**
|
|
108
|
-
* Called when the session successfully compacts. `tokensBefore` is upstream's
|
|
109
|
-
* pre-compaction context size estimate. Aborted compactions don't fire.
|
|
110
|
-
*/
|
|
111
|
-
onCompaction?: (info: {
|
|
112
|
-
reason: "manual" | "threshold" | "overflow";
|
|
113
|
-
tokensBefore: number;
|
|
114
|
-
}) => void;
|
|
115
83
|
/**
|
|
116
84
|
* Default max turns from runtime config. Falls back to the module-scope
|
|
117
85
|
* `defaultMaxTurns` during the lift-and-shift migration; superseded by
|
|
@@ -138,9 +106,6 @@ export interface RunResult {
|
|
|
138
106
|
|
|
139
107
|
/** Options for resuming an existing agent session. */
|
|
140
108
|
export interface ResumeOptions {
|
|
141
|
-
onToolActivity?: (activity: ToolActivity) => void;
|
|
142
|
-
onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
|
|
143
|
-
onCompaction?: (info: { reason: "manual" | "threshold" | "overflow"; tokensBefore: number }) => void;
|
|
144
109
|
signal?: AbortSignal;
|
|
145
110
|
}
|
|
146
111
|
|
|
@@ -149,7 +114,7 @@ export interface ResumeOptions {
|
|
|
149
114
|
* SDK session orchestration in runAgent/resumeAgent.
|
|
150
115
|
*/
|
|
151
116
|
export interface AgentRunner {
|
|
152
|
-
run(
|
|
117
|
+
run(snapshot: ParentSnapshot, type: SubagentType, prompt: string, options: RunOptions): Promise<RunResult>;
|
|
153
118
|
resume(session: AgentSession, prompt: string, options?: ResumeOptions): Promise<string>;
|
|
154
119
|
}
|
|
155
120
|
|
|
@@ -199,23 +164,23 @@ function forwardAbortSignal(
|
|
|
199
164
|
}
|
|
200
165
|
|
|
201
166
|
export async function runAgent(
|
|
202
|
-
|
|
167
|
+
snapshot: ParentSnapshot,
|
|
203
168
|
type: SubagentType,
|
|
204
169
|
prompt: string,
|
|
205
170
|
options: RunOptions,
|
|
206
171
|
): Promise<RunResult> {
|
|
207
172
|
// Resolve working directory upfront — needed for detectEnv before assembly.
|
|
208
|
-
const effectiveCwd = options.cwd ??
|
|
209
|
-
const env = await detectEnv(options.
|
|
173
|
+
const effectiveCwd = options.cwd ?? snapshot.cwd;
|
|
174
|
+
const env = await detectEnv(options.exec, effectiveCwd);
|
|
210
175
|
|
|
211
176
|
// Assemble session configuration (synchronous, no SDK objects).
|
|
212
177
|
const cfg = assembleSessionConfig(
|
|
213
178
|
type,
|
|
214
179
|
{
|
|
215
|
-
cwd:
|
|
216
|
-
parentSystemPrompt:
|
|
217
|
-
parentModel:
|
|
218
|
-
modelRegistry:
|
|
180
|
+
cwd: snapshot.cwd,
|
|
181
|
+
parentSystemPrompt: snapshot.systemPrompt,
|
|
182
|
+
parentModel: snapshot.model,
|
|
183
|
+
modelRegistry: snapshot.modelRegistry,
|
|
219
184
|
},
|
|
220
185
|
{
|
|
221
186
|
cwd: options.cwd,
|
|
@@ -259,7 +224,7 @@ export async function runAgent(
|
|
|
259
224
|
agentDir,
|
|
260
225
|
sessionManager,
|
|
261
226
|
settingsManager: SettingsManager.create(cfg.effectiveCwd, agentDir),
|
|
262
|
-
modelRegistry:
|
|
227
|
+
modelRegistry: snapshot.modelRegistry as any,
|
|
263
228
|
model: cfg.model as Model<any> | undefined,
|
|
264
229
|
tools: cfg.toolNames,
|
|
265
230
|
resourceLoader: loader,
|
|
@@ -287,14 +252,7 @@ export async function runAgent(
|
|
|
287
252
|
// (e.g. loading credentials, setting up state). Placed after tool filtering
|
|
288
253
|
// so extension-provided skills/prompts from extendResourcesFromExtensions()
|
|
289
254
|
// respect the active tool set. All ExtensionBindings fields are optional.
|
|
290
|
-
await session.bindExtensions({
|
|
291
|
-
onError: (err) => {
|
|
292
|
-
options.onToolActivity?.({
|
|
293
|
-
type: "end",
|
|
294
|
-
toolName: `extension-error:${err.extensionPath}`,
|
|
295
|
-
});
|
|
296
|
-
},
|
|
297
|
-
});
|
|
255
|
+
await session.bindExtensions({});
|
|
298
256
|
|
|
299
257
|
// Patch 2 (RepOne #443): re-filter active tools after bindExtensions.
|
|
300
258
|
// Extension-registered tools (added during bindExtensions) are not in the
|
|
@@ -322,11 +280,9 @@ export async function runAgent(
|
|
|
322
280
|
let softLimitReached = false;
|
|
323
281
|
let aborted = false;
|
|
324
282
|
|
|
325
|
-
let currentMessageText = "";
|
|
326
283
|
const unsubTurns = session.subscribe((event: AgentSessionEvent) => {
|
|
327
284
|
if (event.type === "turn_end") {
|
|
328
285
|
turnCount++;
|
|
329
|
-
options.onTurnEnd?.(turnCount);
|
|
330
286
|
if (maxTurns != null) {
|
|
331
287
|
if (!softLimitReached && turnCount >= maxTurns) {
|
|
332
288
|
softLimitReached = true;
|
|
@@ -339,52 +295,15 @@ export async function runAgent(
|
|
|
339
295
|
}
|
|
340
296
|
}
|
|
341
297
|
}
|
|
342
|
-
if (event.type === "message_start") {
|
|
343
|
-
currentMessageText = "";
|
|
344
|
-
}
|
|
345
|
-
if (
|
|
346
|
-
event.type === "message_update" &&
|
|
347
|
-
event.assistantMessageEvent.type === "text_delta"
|
|
348
|
-
) {
|
|
349
|
-
currentMessageText += event.assistantMessageEvent.delta;
|
|
350
|
-
options.onTextDelta?.(
|
|
351
|
-
event.assistantMessageEvent.delta,
|
|
352
|
-
currentMessageText,
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
if (event.type === "tool_execution_start") {
|
|
356
|
-
options.onToolActivity?.({ type: "start", toolName: event.toolName });
|
|
357
|
-
}
|
|
358
|
-
if (event.type === "tool_execution_end") {
|
|
359
|
-
options.onToolActivity?.({ type: "end", toolName: event.toolName });
|
|
360
|
-
}
|
|
361
|
-
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
362
|
-
const u = (event.message as any).usage;
|
|
363
|
-
if (u)
|
|
364
|
-
options.onAssistantUsage?.({
|
|
365
|
-
input: u.input ?? 0,
|
|
366
|
-
output: u.output ?? 0,
|
|
367
|
-
cacheWrite: u.cacheWrite ?? 0,
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
if (event.type === "compaction_end" && !event.aborted && event.result) {
|
|
371
|
-
options.onCompaction?.({
|
|
372
|
-
reason: event.reason,
|
|
373
|
-
tokensBefore: event.result.tokensBefore,
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
298
|
});
|
|
377
299
|
|
|
378
300
|
const collector = collectResponseText(session);
|
|
379
301
|
const cleanupAbort = forwardAbortSignal(session, options.signal);
|
|
380
302
|
|
|
381
|
-
//
|
|
303
|
+
// Prepend parent context if it was captured at spawn time
|
|
382
304
|
let effectivePrompt = prompt;
|
|
383
|
-
if (
|
|
384
|
-
|
|
385
|
-
if (parentContext) {
|
|
386
|
-
effectivePrompt = parentContext + prompt;
|
|
387
|
-
}
|
|
305
|
+
if (snapshot.parentContext) {
|
|
306
|
+
effectivePrompt = snapshot.parentContext + prompt;
|
|
388
307
|
}
|
|
389
308
|
|
|
390
309
|
try {
|
|
@@ -417,46 +336,10 @@ export async function resumeAgent(
|
|
|
417
336
|
const collector = collectResponseText(session);
|
|
418
337
|
const cleanupAbort = forwardAbortSignal(session, options.signal);
|
|
419
338
|
|
|
420
|
-
const unsubEvents =
|
|
421
|
-
options.onToolActivity || options.onAssistantUsage || options.onCompaction
|
|
422
|
-
? session.subscribe((event: AgentSessionEvent) => {
|
|
423
|
-
if (event.type === "tool_execution_start")
|
|
424
|
-
options.onToolActivity?.({
|
|
425
|
-
type: "start",
|
|
426
|
-
toolName: event.toolName,
|
|
427
|
-
});
|
|
428
|
-
if (event.type === "tool_execution_end")
|
|
429
|
-
options.onToolActivity?.({ type: "end", toolName: event.toolName });
|
|
430
|
-
if (
|
|
431
|
-
event.type === "message_end" &&
|
|
432
|
-
event.message.role === "assistant"
|
|
433
|
-
) {
|
|
434
|
-
const u = (event.message as any).usage;
|
|
435
|
-
if (u)
|
|
436
|
-
options.onAssistantUsage?.({
|
|
437
|
-
input: u.input ?? 0,
|
|
438
|
-
output: u.output ?? 0,
|
|
439
|
-
cacheWrite: u.cacheWrite ?? 0,
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
if (
|
|
443
|
-
event.type === "compaction_end" &&
|
|
444
|
-
!event.aborted &&
|
|
445
|
-
event.result
|
|
446
|
-
) {
|
|
447
|
-
options.onCompaction?.({
|
|
448
|
-
reason: event.reason,
|
|
449
|
-
tokensBefore: event.result.tokensBefore,
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
})
|
|
453
|
-
: () => {};
|
|
454
|
-
|
|
455
339
|
try {
|
|
456
340
|
await session.prompt(prompt);
|
|
457
341
|
} finally {
|
|
458
342
|
collector.unsubscribe();
|
|
459
|
-
unsubEvents();
|
|
460
343
|
cleanupAbort();
|
|
461
344
|
}
|
|
462
345
|
|
package/src/env.ts
CHANGED
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
* env.ts — Detect environment info (git, platform) for subagent system prompts.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
5
|
import { debugLog } from "./debug.js";
|
|
7
|
-
import type { EnvInfo } from "./types.js";
|
|
6
|
+
import type { EnvInfo, ShellExec } from "./types.js";
|
|
8
7
|
|
|
9
|
-
export async function detectEnv(
|
|
8
|
+
export async function detectEnv(exec: ShellExec, cwd: string): Promise<EnvInfo> {
|
|
10
9
|
let isGitRepo = false;
|
|
11
10
|
let branch = "";
|
|
12
11
|
|
|
13
12
|
try {
|
|
14
|
-
const result = await
|
|
13
|
+
const result = await exec("git", ["rev-parse", "--is-inside-work-tree"], { cwd, timeout: 5000 });
|
|
15
14
|
isGitRepo = result.code === 0 && result.stdout.trim() === "true";
|
|
16
15
|
} catch (err) {
|
|
17
16
|
debugLog("git rev-parse", err);
|
|
@@ -19,7 +18,7 @@ export async function detectEnv(pi: ExtensionAPI, cwd: string): Promise<EnvInfo>
|
|
|
19
18
|
|
|
20
19
|
if (isGitRepo) {
|
|
21
20
|
try {
|
|
22
|
-
const result = await
|
|
21
|
+
const result = await exec("git", ["branch", "--show-current"], { cwd, timeout: 5000 });
|
|
23
22
|
branch = result.code === 0 ? result.stdout.trim() : "unknown";
|
|
24
23
|
} catch (err) {
|
|
25
24
|
debugLog("git branch", err);
|
package/src/index.ts
CHANGED
|
@@ -66,6 +66,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
66
66
|
const manager = new AgentManager({
|
|
67
67
|
runner: { run: runAgent, resume: resumeAgent },
|
|
68
68
|
worktrees: new GitWorktreeManager(process.cwd()),
|
|
69
|
+
exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
|
|
69
70
|
onComplete: (record) => {
|
|
70
71
|
// Emit lifecycle event based on terminal status
|
|
71
72
|
const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
|
|
@@ -185,8 +186,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
185
186
|
|
|
186
187
|
pi.registerTool(defineTool(createAgentTool({
|
|
187
188
|
manager: {
|
|
188
|
-
spawn: (ctx, type, prompt, opts) => manager.spawn(
|
|
189
|
-
spawnAndWait: (ctx, type, prompt, opts) => manager.spawnAndWait(
|
|
189
|
+
spawn: (ctx, type, prompt, opts) => manager.spawn(ctx, type, prompt, opts),
|
|
190
|
+
spawnAndWait: (ctx, type, prompt, opts) => manager.spawnAndWait(ctx, type, prompt, opts),
|
|
190
191
|
resume: (id, prompt, signal) => manager.resume(id, prompt, signal),
|
|
191
192
|
getRecord: (id) => manager.getRecord(id),
|
|
192
193
|
getMaxConcurrent: () => manager.getMaxConcurrent(),
|
|
@@ -229,7 +230,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
229
230
|
manager: {
|
|
230
231
|
listAgents: () => manager.listAgents(),
|
|
231
232
|
getRecord: (id) => manager.getRecord(id),
|
|
232
|
-
spawnAndWait: (
|
|
233
|
+
spawnAndWait: (ctx, type, prompt, opts) => manager.spawnAndWait(ctx, type, prompt, opts),
|
|
233
234
|
getMaxConcurrent: () => manager.getMaxConcurrent(),
|
|
234
235
|
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
235
236
|
},
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* parent-snapshot.ts — Capture parent session state as a plain data snapshot.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { buildParentContext } from "./context.js";
|
|
7
|
+
import type { ParentSnapshot } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build an immutable snapshot of the parent session state.
|
|
11
|
+
*
|
|
12
|
+
* Called once at spawn time so queued agents capture state as it existed
|
|
13
|
+
* when the user requested the agent, not when a queue slot opens.
|
|
14
|
+
*/
|
|
15
|
+
export function buildParentSnapshot(
|
|
16
|
+
ctx: ExtensionContext,
|
|
17
|
+
inheritContext?: boolean,
|
|
18
|
+
): ParentSnapshot {
|
|
19
|
+
const parentContext = inheritContext ? buildParentContext(ctx) : undefined;
|
|
20
|
+
return {
|
|
21
|
+
cwd: ctx.cwd,
|
|
22
|
+
systemPrompt: ctx.getSystemPrompt(),
|
|
23
|
+
model: ctx.model,
|
|
24
|
+
modelRegistry: ctx.modelRegistry,
|
|
25
|
+
parentContext: parentContext || undefined,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* record-observer.ts — Subscribes to session events and updates AgentRecord stats.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the scattered callback-wrapping logic in AgentManager's startAgent()
|
|
5
|
+
* and resume() with a single direct subscription.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CompactionInfo } from "./agent-manager.js";
|
|
9
|
+
import type { AgentRecord } from "./agent-record.js";
|
|
10
|
+
import { addUsage } from "./usage.js";
|
|
11
|
+
|
|
12
|
+
/** Narrow session interface — only the subscribe method needed by the observer. */
|
|
13
|
+
interface SubscribableSession {
|
|
14
|
+
subscribe(fn: (event: any) => void): () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RecordObserverOptions {
|
|
18
|
+
onCompact?: (record: AgentRecord, info: CompactionInfo) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Subscribe to session events and accumulate stats on the agent record.
|
|
23
|
+
*
|
|
24
|
+
* Handles:
|
|
25
|
+
* - `tool_execution_end` → `record.toolUses++`
|
|
26
|
+
* - `message_end` (assistant, with usage) → `addUsage(record.lifetimeUsage, …)`
|
|
27
|
+
* - `compaction_end` (not aborted) → `record.compactionCount++`, call `onCompact`
|
|
28
|
+
*
|
|
29
|
+
* @returns An unsubscribe function.
|
|
30
|
+
*/
|
|
31
|
+
export function subscribeRecordObserver(
|
|
32
|
+
session: SubscribableSession,
|
|
33
|
+
record: AgentRecord,
|
|
34
|
+
options?: RecordObserverOptions,
|
|
35
|
+
): () => void {
|
|
36
|
+
return session.subscribe((event: any) => {
|
|
37
|
+
if (event.type === "tool_execution_end") {
|
|
38
|
+
record.toolUses++;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
42
|
+
const u = event.message.usage;
|
|
43
|
+
if (u) {
|
|
44
|
+
addUsage(record.lifetimeUsage, {
|
|
45
|
+
input: u.input ?? 0,
|
|
46
|
+
output: u.output ?? 0,
|
|
47
|
+
cacheWrite: u.cacheWrite ?? 0,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (event.type === "compaction_end" && !event.aborted && event.result) {
|
|
53
|
+
record.compactionCount++;
|
|
54
|
+
options?.onCompact?.(record, {
|
|
55
|
+
reason: event.reason,
|
|
56
|
+
tokensBefore: event.result.tokensBefore,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
package/src/service-adapter.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { AgentRecord } from "./types.js";
|
|
|
11
11
|
|
|
12
12
|
/** Narrow interface for the AgentManager — avoids coupling to the concrete class. */
|
|
13
13
|
export interface AgentManagerLike {
|
|
14
|
-
spawn(
|
|
14
|
+
spawn(ctx: unknown, type: string, prompt: string, options: unknown): string;
|
|
15
15
|
getRecord(id: string): AgentRecord | undefined;
|
|
16
16
|
listAgents(): AgentRecord[];
|
|
17
17
|
abort(id: string): boolean;
|
|
@@ -54,7 +54,7 @@ export function createSubagentsService(deps: AdapterDeps): SubagentsService {
|
|
|
54
54
|
const description = options?.description ?? prompt.slice(0, 80);
|
|
55
55
|
const isBackground = !(options?.foreground ?? false);
|
|
56
56
|
|
|
57
|
-
return manager.spawn(session.
|
|
57
|
+
return manager.spawn(session.ctx, type, prompt, {
|
|
58
58
|
description,
|
|
59
59
|
model,
|
|
60
60
|
maxTurns: options?.maxTurns,
|