@gotgenes/pi-subagents 1.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/.markdownlint-cli2.yaml +19 -0
- package/.prettierignore +5 -0
- package/.release-please-manifest.json +3 -0
- package/AGENTS.md +85 -0
- package/CHANGELOG.md +495 -0
- package/LICENSE +21 -0
- package/README.md +528 -0
- package/dist/agent-manager.d.ts +108 -0
- package/dist/agent-manager.js +390 -0
- package/dist/agent-runner.d.ts +93 -0
- package/dist/agent-runner.js +428 -0
- package/dist/agent-types.d.ts +48 -0
- package/dist/agent-types.js +136 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.js +56 -0
- package/dist/cross-extension-rpc.d.ts +46 -0
- package/dist/cross-extension-rpc.js +54 -0
- package/dist/custom-agents.d.ts +14 -0
- package/dist/custom-agents.js +127 -0
- package/dist/default-agents.d.ts +7 -0
- package/dist/default-agents.js +119 -0
- package/dist/env.d.ts +6 -0
- package/dist/env.js +28 -0
- package/dist/group-join.d.ts +32 -0
- package/dist/group-join.js +116 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1731 -0
- package/dist/invocation-config.d.ts +22 -0
- package/dist/invocation-config.js +15 -0
- package/dist/memory.d.ts +49 -0
- package/dist/memory.js +151 -0
- package/dist/model-resolver.d.ts +19 -0
- package/dist/model-resolver.js +62 -0
- package/dist/output-file.d.ts +24 -0
- package/dist/output-file.js +86 -0
- package/dist/prompts.d.ts +29 -0
- package/dist/prompts.js +72 -0
- package/dist/schedule-store.d.ts +36 -0
- package/dist/schedule-store.js +144 -0
- package/dist/schedule.d.ts +109 -0
- package/dist/schedule.js +338 -0
- package/dist/settings.d.ts +66 -0
- package/dist/settings.js +130 -0
- package/dist/skill-loader.d.ts +24 -0
- package/dist/skill-loader.js +93 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.js +5 -0
- package/dist/ui/agent-widget.d.ts +134 -0
- package/dist/ui/agent-widget.js +451 -0
- package/dist/ui/conversation-viewer.d.ts +35 -0
- package/dist/ui/conversation-viewer.js +252 -0
- package/dist/ui/schedule-menu.d.ts +16 -0
- package/dist/ui/schedule-menu.js +95 -0
- package/dist/usage.d.ts +50 -0
- package/dist/usage.js +49 -0
- package/dist/worktree.d.ts +36 -0
- package/dist/worktree.js +139 -0
- package/docs/decisions/0001-deferred-patches.md +75 -0
- package/package.json +68 -0
- package/prek.toml +24 -0
- package/release-please-config.json +22 -0
- package/src/agent-manager.ts +482 -0
- package/src/agent-runner.ts +625 -0
- package/src/agent-types.ts +164 -0
- package/src/context.ts +58 -0
- package/src/cross-extension-rpc.ts +95 -0
- package/src/custom-agents.ts +136 -0
- package/src/default-agents.ts +123 -0
- package/src/env.ts +33 -0
- package/src/group-join.ts +141 -0
- package/src/index.ts +1894 -0
- package/src/invocation-config.ts +40 -0
- package/src/memory.ts +165 -0
- package/src/model-resolver.ts +81 -0
- package/src/output-file.ts +96 -0
- package/src/prompts.ts +105 -0
- package/src/schedule-store.ts +143 -0
- package/src/schedule.ts +365 -0
- package/src/settings.ts +186 -0
- package/src/skill-loader.ts +102 -0
- package/src/types.ts +176 -0
- package/src/ui/agent-widget.ts +533 -0
- package/src/ui/conversation-viewer.ts +261 -0
- package/src/ui/schedule-menu.ts +104 -0
- package/src/usage.ts +60 -0
- package/src/worktree.ts +162 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-manager.ts — Tracks agents, background execution, resume support.
|
|
3
|
+
*
|
|
4
|
+
* Background agents are subject to a configurable concurrency limit (default: 4).
|
|
5
|
+
* Excess agents are queued and auto-started as running agents complete.
|
|
6
|
+
* Foreground agents bypass the queue (they block the parent anyway).
|
|
7
|
+
*/
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { resumeAgent, runAgent } from "./agent-runner.js";
|
|
10
|
+
import { addUsage } from "./usage.js";
|
|
11
|
+
import { cleanupWorktree, createWorktree, pruneWorktrees, } from "./worktree.js";
|
|
12
|
+
/** Default max concurrent background agents. */
|
|
13
|
+
const DEFAULT_MAX_CONCURRENT = 4;
|
|
14
|
+
export class AgentManager {
|
|
15
|
+
agents = new Map();
|
|
16
|
+
cleanupInterval;
|
|
17
|
+
onComplete;
|
|
18
|
+
onStart;
|
|
19
|
+
onCompact;
|
|
20
|
+
maxConcurrent;
|
|
21
|
+
/** Queue of background agents waiting to start. */
|
|
22
|
+
queue = [];
|
|
23
|
+
/** Number of currently running background agents. */
|
|
24
|
+
runningBackground = 0;
|
|
25
|
+
constructor(onComplete, maxConcurrent = DEFAULT_MAX_CONCURRENT, onStart, onCompact) {
|
|
26
|
+
this.onComplete = onComplete;
|
|
27
|
+
this.onStart = onStart;
|
|
28
|
+
this.onCompact = onCompact;
|
|
29
|
+
this.maxConcurrent = maxConcurrent;
|
|
30
|
+
// Cleanup completed agents after 10 minutes (but keep sessions for resume)
|
|
31
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
|
32
|
+
this.cleanupInterval.unref();
|
|
33
|
+
}
|
|
34
|
+
/** Update the max concurrent background agents limit. */
|
|
35
|
+
setMaxConcurrent(n) {
|
|
36
|
+
this.maxConcurrent = Math.max(1, n);
|
|
37
|
+
// Start queued agents if the new limit allows
|
|
38
|
+
this.drainQueue();
|
|
39
|
+
}
|
|
40
|
+
getMaxConcurrent() {
|
|
41
|
+
return this.maxConcurrent;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Spawn an agent and return its ID immediately (for background use).
|
|
45
|
+
* If the concurrency limit is reached, the agent is queued.
|
|
46
|
+
*/
|
|
47
|
+
spawn(pi, ctx, type, prompt, options) {
|
|
48
|
+
const id = randomUUID().slice(0, 17);
|
|
49
|
+
const abortController = new AbortController();
|
|
50
|
+
const record = {
|
|
51
|
+
id,
|
|
52
|
+
type,
|
|
53
|
+
description: options.description,
|
|
54
|
+
status: options.isBackground ? "queued" : "running",
|
|
55
|
+
toolUses: 0,
|
|
56
|
+
startedAt: Date.now(),
|
|
57
|
+
abortController,
|
|
58
|
+
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
59
|
+
compactionCount: 0,
|
|
60
|
+
invocation: options.invocation,
|
|
61
|
+
};
|
|
62
|
+
this.agents.set(id, record);
|
|
63
|
+
const args = { pi, ctx, type, prompt, options };
|
|
64
|
+
if (options.isBackground && !options.bypassQueue && this.runningBackground >= this.maxConcurrent) {
|
|
65
|
+
// Queue it — will be started when a running agent completes
|
|
66
|
+
this.queue.push({ id, args });
|
|
67
|
+
return id;
|
|
68
|
+
}
|
|
69
|
+
// startAgent can throw (e.g. strict worktree-isolation failure) — clean
|
|
70
|
+
// up the record so callers don't see an orphan in `listAgents()`.
|
|
71
|
+
try {
|
|
72
|
+
this.startAgent(id, record, args);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
this.agents.delete(id);
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
return id;
|
|
79
|
+
}
|
|
80
|
+
/** Actually start an agent (called immediately or from queue drain). */
|
|
81
|
+
startAgent(id, record, { pi, ctx, type, prompt, options }) {
|
|
82
|
+
// Worktree isolation: try to create a temporary git worktree. Strict —
|
|
83
|
+
// fail loud if not possible (no silent fallback to main tree). Done
|
|
84
|
+
// BEFORE state mutation so a throw doesn't leave the record half-running.
|
|
85
|
+
let worktreeCwd;
|
|
86
|
+
if (options.isolation === "worktree") {
|
|
87
|
+
const wt = createWorktree(ctx.cwd, id);
|
|
88
|
+
if (!wt) {
|
|
89
|
+
throw new Error('Cannot run with isolation: "worktree" — not a git repo, no commits yet, or `git worktree add` failed. ' +
|
|
90
|
+
'Initialize git and commit at least once, or omit `isolation`.');
|
|
91
|
+
}
|
|
92
|
+
record.worktree = wt;
|
|
93
|
+
worktreeCwd = wt.path;
|
|
94
|
+
}
|
|
95
|
+
record.status = "running";
|
|
96
|
+
record.startedAt = Date.now();
|
|
97
|
+
if (options.isBackground)
|
|
98
|
+
this.runningBackground++;
|
|
99
|
+
this.onStart?.(record);
|
|
100
|
+
// Wire parent abort signal to stop the subagent when the parent is interrupted
|
|
101
|
+
let detachParentSignal;
|
|
102
|
+
if (options.signal) {
|
|
103
|
+
const onParentAbort = () => this.abort(id);
|
|
104
|
+
options.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
105
|
+
detachParentSignal = () => options.signal.removeEventListener("abort", onParentAbort);
|
|
106
|
+
}
|
|
107
|
+
const detach = () => { detachParentSignal?.(); detachParentSignal = undefined; };
|
|
108
|
+
const promise = runAgent(ctx, type, prompt, {
|
|
109
|
+
pi,
|
|
110
|
+
model: options.model,
|
|
111
|
+
maxTurns: options.maxTurns,
|
|
112
|
+
isolated: options.isolated,
|
|
113
|
+
inheritContext: options.inheritContext,
|
|
114
|
+
thinkingLevel: options.thinkingLevel,
|
|
115
|
+
cwd: worktreeCwd,
|
|
116
|
+
signal: record.abortController.signal,
|
|
117
|
+
onToolActivity: (activity) => {
|
|
118
|
+
if (activity.type === "end")
|
|
119
|
+
record.toolUses++;
|
|
120
|
+
options.onToolActivity?.(activity);
|
|
121
|
+
},
|
|
122
|
+
onTurnEnd: options.onTurnEnd,
|
|
123
|
+
onTextDelta: options.onTextDelta,
|
|
124
|
+
onAssistantUsage: (usage) => {
|
|
125
|
+
addUsage(record.lifetimeUsage, usage);
|
|
126
|
+
options.onAssistantUsage?.(usage);
|
|
127
|
+
},
|
|
128
|
+
onCompaction: (info) => {
|
|
129
|
+
record.compactionCount++;
|
|
130
|
+
this.onCompact?.(record, info);
|
|
131
|
+
options.onCompaction?.(info);
|
|
132
|
+
},
|
|
133
|
+
onSessionCreated: (session) => {
|
|
134
|
+
record.session = session;
|
|
135
|
+
// Flush any steers that arrived before the session was ready
|
|
136
|
+
if (record.pendingSteers?.length) {
|
|
137
|
+
for (const msg of record.pendingSteers) {
|
|
138
|
+
session.steer(msg).catch(() => { });
|
|
139
|
+
}
|
|
140
|
+
record.pendingSteers = undefined;
|
|
141
|
+
}
|
|
142
|
+
options.onSessionCreated?.(session);
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
.then(({ responseText, session, aborted, steered }) => {
|
|
146
|
+
// Don't overwrite status if externally stopped via abort()
|
|
147
|
+
if (record.status !== "stopped") {
|
|
148
|
+
record.status = aborted ? "aborted" : steered ? "steered" : "completed";
|
|
149
|
+
}
|
|
150
|
+
record.result = responseText;
|
|
151
|
+
record.session = session;
|
|
152
|
+
record.completedAt ??= Date.now();
|
|
153
|
+
detach();
|
|
154
|
+
// Final flush of streaming output file
|
|
155
|
+
if (record.outputCleanup) {
|
|
156
|
+
try {
|
|
157
|
+
record.outputCleanup();
|
|
158
|
+
}
|
|
159
|
+
catch { /* ignore */ }
|
|
160
|
+
record.outputCleanup = undefined;
|
|
161
|
+
}
|
|
162
|
+
// Clean up worktree if used
|
|
163
|
+
if (record.worktree) {
|
|
164
|
+
const wtResult = cleanupWorktree(ctx.cwd, record.worktree, options.description);
|
|
165
|
+
record.worktreeResult = wtResult;
|
|
166
|
+
if (wtResult.hasChanges && wtResult.branch) {
|
|
167
|
+
record.result = (record.result ?? "") +
|
|
168
|
+
`\n\n---\nChanges saved to branch \`${wtResult.branch}\`. Merge with: \`git merge ${wtResult.branch}\``;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (options.isBackground) {
|
|
172
|
+
this.runningBackground--;
|
|
173
|
+
try {
|
|
174
|
+
this.onComplete?.(record);
|
|
175
|
+
}
|
|
176
|
+
catch { /* ignore completion side-effect errors */ }
|
|
177
|
+
this.drainQueue();
|
|
178
|
+
}
|
|
179
|
+
return responseText;
|
|
180
|
+
})
|
|
181
|
+
.catch((err) => {
|
|
182
|
+
// Don't overwrite status if externally stopped via abort()
|
|
183
|
+
if (record.status !== "stopped") {
|
|
184
|
+
record.status = "error";
|
|
185
|
+
}
|
|
186
|
+
record.error = err instanceof Error ? err.message : String(err);
|
|
187
|
+
record.completedAt ??= Date.now();
|
|
188
|
+
detach();
|
|
189
|
+
// Final flush of streaming output file on error
|
|
190
|
+
if (record.outputCleanup) {
|
|
191
|
+
try {
|
|
192
|
+
record.outputCleanup();
|
|
193
|
+
}
|
|
194
|
+
catch { /* ignore */ }
|
|
195
|
+
record.outputCleanup = undefined;
|
|
196
|
+
}
|
|
197
|
+
// Best-effort worktree cleanup on error
|
|
198
|
+
if (record.worktree) {
|
|
199
|
+
try {
|
|
200
|
+
const wtResult = cleanupWorktree(ctx.cwd, record.worktree, options.description);
|
|
201
|
+
record.worktreeResult = wtResult;
|
|
202
|
+
}
|
|
203
|
+
catch { /* ignore cleanup errors */ }
|
|
204
|
+
}
|
|
205
|
+
if (options.isBackground) {
|
|
206
|
+
this.runningBackground--;
|
|
207
|
+
this.onComplete?.(record);
|
|
208
|
+
this.drainQueue();
|
|
209
|
+
}
|
|
210
|
+
return "";
|
|
211
|
+
});
|
|
212
|
+
record.promise = promise;
|
|
213
|
+
}
|
|
214
|
+
/** Start queued agents up to the concurrency limit. */
|
|
215
|
+
drainQueue() {
|
|
216
|
+
while (this.queue.length > 0 && this.runningBackground < this.maxConcurrent) {
|
|
217
|
+
const next = this.queue.shift();
|
|
218
|
+
const record = this.agents.get(next.id);
|
|
219
|
+
if (!record || record.status !== "queued")
|
|
220
|
+
continue;
|
|
221
|
+
try {
|
|
222
|
+
this.startAgent(next.id, record, next.args);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
// Late failure (e.g. strict worktree-isolation) — surface on the record
|
|
226
|
+
// so the user/agent can see it via /agents, then keep draining.
|
|
227
|
+
record.status = "error";
|
|
228
|
+
record.error = err instanceof Error ? err.message : String(err);
|
|
229
|
+
record.completedAt = Date.now();
|
|
230
|
+
this.onComplete?.(record);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Spawn an agent and wait for completion (foreground use).
|
|
236
|
+
* Foreground agents bypass the concurrency queue.
|
|
237
|
+
*/
|
|
238
|
+
async spawnAndWait(pi, ctx, type, prompt, options) {
|
|
239
|
+
const id = this.spawn(pi, ctx, type, prompt, { ...options, isBackground: false });
|
|
240
|
+
const record = this.agents.get(id);
|
|
241
|
+
await record.promise;
|
|
242
|
+
return record;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Resume an existing agent session with a new prompt.
|
|
246
|
+
*/
|
|
247
|
+
async resume(id, prompt, signal) {
|
|
248
|
+
const record = this.agents.get(id);
|
|
249
|
+
if (!record?.session)
|
|
250
|
+
return undefined;
|
|
251
|
+
record.status = "running";
|
|
252
|
+
record.startedAt = Date.now();
|
|
253
|
+
record.completedAt = undefined;
|
|
254
|
+
record.result = undefined;
|
|
255
|
+
record.error = undefined;
|
|
256
|
+
try {
|
|
257
|
+
const responseText = await resumeAgent(record.session, prompt, {
|
|
258
|
+
onToolActivity: (activity) => {
|
|
259
|
+
if (activity.type === "end")
|
|
260
|
+
record.toolUses++;
|
|
261
|
+
},
|
|
262
|
+
onAssistantUsage: (usage) => {
|
|
263
|
+
addUsage(record.lifetimeUsage, usage);
|
|
264
|
+
},
|
|
265
|
+
onCompaction: (info) => {
|
|
266
|
+
record.compactionCount++;
|
|
267
|
+
this.onCompact?.(record, info);
|
|
268
|
+
},
|
|
269
|
+
signal,
|
|
270
|
+
});
|
|
271
|
+
record.status = "completed";
|
|
272
|
+
record.result = responseText;
|
|
273
|
+
record.completedAt = Date.now();
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
record.status = "error";
|
|
277
|
+
record.error = err instanceof Error ? err.message : String(err);
|
|
278
|
+
record.completedAt = Date.now();
|
|
279
|
+
}
|
|
280
|
+
return record;
|
|
281
|
+
}
|
|
282
|
+
getRecord(id) {
|
|
283
|
+
return this.agents.get(id);
|
|
284
|
+
}
|
|
285
|
+
listAgents() {
|
|
286
|
+
return [...this.agents.values()].sort((a, b) => b.startedAt - a.startedAt);
|
|
287
|
+
}
|
|
288
|
+
abort(id) {
|
|
289
|
+
const record = this.agents.get(id);
|
|
290
|
+
if (!record)
|
|
291
|
+
return false;
|
|
292
|
+
// Remove from queue if queued
|
|
293
|
+
if (record.status === "queued") {
|
|
294
|
+
this.queue = this.queue.filter(q => q.id !== id);
|
|
295
|
+
record.status = "stopped";
|
|
296
|
+
record.completedAt = Date.now();
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
if (record.status !== "running")
|
|
300
|
+
return false;
|
|
301
|
+
record.abortController?.abort();
|
|
302
|
+
record.status = "stopped";
|
|
303
|
+
record.completedAt = Date.now();
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
/** Dispose a record's session and remove it from the map. */
|
|
307
|
+
removeRecord(id, record) {
|
|
308
|
+
record.session?.dispose?.();
|
|
309
|
+
record.session = undefined;
|
|
310
|
+
this.agents.delete(id);
|
|
311
|
+
}
|
|
312
|
+
cleanup() {
|
|
313
|
+
const cutoff = Date.now() - 10 * 60_000;
|
|
314
|
+
for (const [id, record] of this.agents) {
|
|
315
|
+
if (record.status === "running" || record.status === "queued")
|
|
316
|
+
continue;
|
|
317
|
+
if ((record.completedAt ?? 0) >= cutoff)
|
|
318
|
+
continue;
|
|
319
|
+
this.removeRecord(id, record);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Remove all completed/stopped/errored records immediately.
|
|
324
|
+
* Called on session start/switch so tasks from a prior session don't persist.
|
|
325
|
+
*/
|
|
326
|
+
clearCompleted() {
|
|
327
|
+
for (const [id, record] of this.agents) {
|
|
328
|
+
if (record.status === "running" || record.status === "queued")
|
|
329
|
+
continue;
|
|
330
|
+
this.removeRecord(id, record);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/** Whether any agents are still running or queued. */
|
|
334
|
+
hasRunning() {
|
|
335
|
+
return [...this.agents.values()].some(r => r.status === "running" || r.status === "queued");
|
|
336
|
+
}
|
|
337
|
+
/** Abort all running and queued agents immediately. */
|
|
338
|
+
abortAll() {
|
|
339
|
+
let count = 0;
|
|
340
|
+
// Clear queued agents first
|
|
341
|
+
for (const queued of this.queue) {
|
|
342
|
+
const record = this.agents.get(queued.id);
|
|
343
|
+
if (record) {
|
|
344
|
+
record.status = "stopped";
|
|
345
|
+
record.completedAt = Date.now();
|
|
346
|
+
count++;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
this.queue = [];
|
|
350
|
+
// Abort running agents
|
|
351
|
+
for (const record of this.agents.values()) {
|
|
352
|
+
if (record.status === "running") {
|
|
353
|
+
record.abortController?.abort();
|
|
354
|
+
record.status = "stopped";
|
|
355
|
+
record.completedAt = Date.now();
|
|
356
|
+
count++;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return count;
|
|
360
|
+
}
|
|
361
|
+
/** Wait for all running and queued agents to complete (including queued ones). */
|
|
362
|
+
async waitForAll() {
|
|
363
|
+
// Loop because drainQueue respects the concurrency limit — as running
|
|
364
|
+
// agents finish they start queued ones, which need awaiting too.
|
|
365
|
+
while (true) {
|
|
366
|
+
this.drainQueue();
|
|
367
|
+
const pending = [...this.agents.values()]
|
|
368
|
+
.filter(r => r.status === "running" || r.status === "queued")
|
|
369
|
+
.map(r => r.promise)
|
|
370
|
+
.filter(Boolean);
|
|
371
|
+
if (pending.length === 0)
|
|
372
|
+
break;
|
|
373
|
+
await Promise.allSettled(pending);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
dispose() {
|
|
377
|
+
clearInterval(this.cleanupInterval);
|
|
378
|
+
// Clear queue
|
|
379
|
+
this.queue = [];
|
|
380
|
+
for (const record of this.agents.values()) {
|
|
381
|
+
record.session?.dispose();
|
|
382
|
+
}
|
|
383
|
+
this.agents.clear();
|
|
384
|
+
// Prune any orphaned git worktrees (crash recovery)
|
|
385
|
+
try {
|
|
386
|
+
pruneWorktrees(process.cwd());
|
|
387
|
+
}
|
|
388
|
+
catch { /* ignore */ }
|
|
389
|
+
}
|
|
390
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-runner.ts — Core execution engine: creates sessions, runs agents, collects results.
|
|
3
|
+
*/
|
|
4
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
5
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { type AgentSession, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { SubagentType, ThinkingLevel } from "./types.js";
|
|
8
|
+
/** Normalize max turns. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
9
|
+
export declare function normalizeMaxTurns(n: number | undefined): number | undefined;
|
|
10
|
+
/** Get the default max turns value. undefined = unlimited. */
|
|
11
|
+
export declare function getDefaultMaxTurns(): number | undefined;
|
|
12
|
+
/** Set the default max turns value. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
13
|
+
export declare function setDefaultMaxTurns(n: number | undefined): void;
|
|
14
|
+
/** Get the grace turns value. */
|
|
15
|
+
export declare function getGraceTurns(): number;
|
|
16
|
+
/** Set the grace turns value (minimum 1). */
|
|
17
|
+
export declare function setGraceTurns(n: number): void;
|
|
18
|
+
/** Info about a tool event in the subagent. */
|
|
19
|
+
export interface ToolActivity {
|
|
20
|
+
type: "start" | "end";
|
|
21
|
+
toolName: string;
|
|
22
|
+
}
|
|
23
|
+
export interface RunOptions {
|
|
24
|
+
/** ExtensionAPI instance — used for pi.exec() instead of execSync. */
|
|
25
|
+
pi: ExtensionAPI;
|
|
26
|
+
model?: Model<any>;
|
|
27
|
+
maxTurns?: number;
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
isolated?: boolean;
|
|
30
|
+
inheritContext?: boolean;
|
|
31
|
+
thinkingLevel?: ThinkingLevel;
|
|
32
|
+
/** Override working directory (e.g. for worktree isolation). */
|
|
33
|
+
cwd?: string;
|
|
34
|
+
/** Called on tool start/end with activity info. */
|
|
35
|
+
onToolActivity?: (activity: ToolActivity) => void;
|
|
36
|
+
/** Called on streaming text deltas from the assistant response. */
|
|
37
|
+
onTextDelta?: (delta: string, fullText: string) => void;
|
|
38
|
+
onSessionCreated?: (session: AgentSession) => void;
|
|
39
|
+
/** Called at the end of each agentic turn with the cumulative count. */
|
|
40
|
+
onTurnEnd?: (turnCount: number) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Called once per assistant message_end with that message's usage delta.
|
|
43
|
+
* Lets callers maintain a lifetime accumulator that survives compaction
|
|
44
|
+
* (which replaces session.state.messages and resets stats-derived sums).
|
|
45
|
+
*/
|
|
46
|
+
onAssistantUsage?: (usage: {
|
|
47
|
+
input: number;
|
|
48
|
+
output: number;
|
|
49
|
+
cacheWrite: number;
|
|
50
|
+
}) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Called when the session successfully compacts. `tokensBefore` is upstream's
|
|
53
|
+
* pre-compaction context size estimate. Aborted compactions don't fire.
|
|
54
|
+
*/
|
|
55
|
+
onCompaction?: (info: {
|
|
56
|
+
reason: "manual" | "threshold" | "overflow";
|
|
57
|
+
tokensBefore: number;
|
|
58
|
+
}) => void;
|
|
59
|
+
}
|
|
60
|
+
export interface RunResult {
|
|
61
|
+
responseText: string;
|
|
62
|
+
session: AgentSession;
|
|
63
|
+
/** True if the agent was hard-aborted (max_turns + grace exceeded). */
|
|
64
|
+
aborted: boolean;
|
|
65
|
+
/** True if the agent was steered to wrap up (hit soft turn limit) but finished in time. */
|
|
66
|
+
steered: boolean;
|
|
67
|
+
}
|
|
68
|
+
export declare function runAgent(ctx: ExtensionContext, type: SubagentType, prompt: string, options: RunOptions): Promise<RunResult>;
|
|
69
|
+
/**
|
|
70
|
+
* Send a new prompt to an existing session (resume).
|
|
71
|
+
*/
|
|
72
|
+
export declare function resumeAgent(session: AgentSession, prompt: string, options?: {
|
|
73
|
+
onToolActivity?: (activity: ToolActivity) => void;
|
|
74
|
+
onAssistantUsage?: (usage: {
|
|
75
|
+
input: number;
|
|
76
|
+
output: number;
|
|
77
|
+
cacheWrite: number;
|
|
78
|
+
}) => void;
|
|
79
|
+
onCompaction?: (info: {
|
|
80
|
+
reason: "manual" | "threshold" | "overflow";
|
|
81
|
+
tokensBefore: number;
|
|
82
|
+
}) => void;
|
|
83
|
+
signal?: AbortSignal;
|
|
84
|
+
}): Promise<string>;
|
|
85
|
+
/**
|
|
86
|
+
* Send a steering message to a running subagent.
|
|
87
|
+
* The message will interrupt the agent after its current tool execution.
|
|
88
|
+
*/
|
|
89
|
+
export declare function steerAgent(session: AgentSession, message: string): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Get the subagent's conversation messages as formatted text.
|
|
92
|
+
*/
|
|
93
|
+
export declare function getAgentConversation(session: AgentSession): string;
|