@ferris1225/pi-subagents 0.29.0 → 0.32.2
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/README.md +130 -74
- package/package.json +1 -1
- package/src/agents.ts +8 -13
- package/src/background.ts +59 -6
- package/src/completion.ts +28 -0
- package/src/config.ts +14 -2
- package/src/dispatch.ts +1438 -289
- package/src/format.ts +38 -6
- package/src/index.ts +7 -4
- package/src/inspector-panel.ts +363 -0
- package/src/inspector.ts +369 -0
- package/src/models.ts +192 -50
- package/src/monitor.ts +141 -7
- package/src/prompt.ts +9 -2
- package/src/recovery.ts +145 -0
- package/src/rpc-run.ts +1016 -0
- package/src/runtime.ts +183 -8
- package/src/session-fork.ts +84 -0
- package/src/setup.ts +271 -184
- package/src/spawn.ts +562 -856
- package/src/tools.ts +407 -65
- package/src/trajectory.ts +503 -0
- package/src/ui.ts +32 -16
- package/src/widget.ts +22 -5
- package/src/worktree.ts +687 -0
package/src/tools.ts
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Thread controls and lookup tools around the subagent runtime:
|
|
3
|
+
* subagent_control (steer/retarget/park/resume), subagent_wait (in-turn result
|
|
4
|
+
* lookup), subagent_status, and destructive subagent_stop.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
7
8
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
9
|
import { Text } from "@earendil-works/pi-tui";
|
|
9
10
|
import { Type } from "typebox";
|
|
10
|
-
import { loadConfig } from "./config.ts";
|
|
11
|
+
import { DEFAULT_MAX_RESULT_LINES, loadConfig } from "./config.ts";
|
|
11
12
|
import { emptyUsage, formatCompletionBlock, formatUsage, matchRunIds } from "./format.ts";
|
|
12
13
|
import {
|
|
13
14
|
formatElapsed,
|
|
14
15
|
formatUsageCompact,
|
|
16
|
+
isRunActiveStatus,
|
|
15
17
|
monitor,
|
|
18
|
+
runLabel,
|
|
16
19
|
statusLabel,
|
|
20
|
+
type RunStatus,
|
|
17
21
|
} from "./monitor.ts";
|
|
18
|
-
import type { SubagentRuntime } from "./runtime.ts";
|
|
19
|
-
import { isFailedResult, type SingleResult } from "./spawn.ts";
|
|
22
|
+
import type { SubagentRuntime, SubagentThread } from "./runtime.ts";
|
|
23
|
+
import { getResultOutput, isFailedResult, type SingleResult } from "./spawn.ts";
|
|
24
|
+
import { inspectorStore } from "./trajectory.ts";
|
|
20
25
|
|
|
21
26
|
/** In-turn result lookup. Dispatch already ended the turn and results arrive as
|
|
22
27
|
* wake-up messages, so the default must NOT block: a settled run returns its
|
|
@@ -37,6 +42,150 @@ function renderFirstLine(result: { content?: unknown }, label: string, theme: an
|
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
45
|
+
const SubagentControlParams = Type.Object({
|
|
46
|
+
action: StringEnum(["steer", "retarget", "park", "resume", "fork"] as const, {
|
|
47
|
+
description: "Control operation for the logical sub-agent thread.",
|
|
48
|
+
}),
|
|
49
|
+
id: Type.Integer({ minimum: 1, description: "Stable run id shown in the subagent widget/status output." }),
|
|
50
|
+
instruction: Type.Optional(
|
|
51
|
+
Type.String({ description: "Instruction queued by steer after the current child tool batch." }),
|
|
52
|
+
),
|
|
53
|
+
objective: Type.Optional(
|
|
54
|
+
Type.String({ description: "Replacement objective for retarget, or optional objective for resume/fork." }),
|
|
55
|
+
),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
pi.registerTool({
|
|
59
|
+
name: "subagent_control",
|
|
60
|
+
label: "Subagent Control",
|
|
61
|
+
description: [
|
|
62
|
+
"Control an existing sub-agent thread by stable run id.",
|
|
63
|
+
"steer queues an instruction after the current child tool batch.",
|
|
64
|
+
"retarget aborts the current objective to a stable checkpoint, suppresses that aborted completion, then starts the replacement objective in the same session.",
|
|
65
|
+
"park aborts to a stable checkpoint, terminates the child, preserves context, and releases its concurrency slot.",
|
|
66
|
+
"resume restarts a parked, completed, or failed retained thread with the same run id; objective is optional.",
|
|
67
|
+
"fork copies a parked/completed/failed retained session branch into a new logical thread and run id; an isolated checkpoint must be settled and integrated first; objective is optional.",
|
|
68
|
+
].join(" "),
|
|
69
|
+
promptSnippet: "Control a subagent thread: steer, retarget, park, resume, or fork by stable run id.",
|
|
70
|
+
promptGuidelines: [
|
|
71
|
+
"Use subagent_control steer to refine active work without restarting it; the instruction is delivered after the child's current tool batch.",
|
|
72
|
+
"Use subagent_control retarget when the active objective is obsolete; do not call subagent_stop and start a fresh thread because retarget preserves context and suppresses the abandoned completion.",
|
|
73
|
+
"Use subagent_control park to checkpoint useful context while releasing the process/concurrency slot, and resume to continue the same run id later.",
|
|
74
|
+
"Use subagent_control fork only on a parked or settled retained thread; isolated work must settle and integrate before it can fork. Fork creates a new run id while leaving the source untouched.",
|
|
75
|
+
"Use subagent_stop only for destructive cancellation; it retires that thread's retained session without retiring independent forks.",
|
|
76
|
+
],
|
|
77
|
+
parameters: SubagentControlParams,
|
|
78
|
+
|
|
79
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
80
|
+
const thread = runtime.threads.get(params.id);
|
|
81
|
+
if (!thread) {
|
|
82
|
+
return { content: [{ type: "text", text: `No subagent thread matches run #${params.id}.` }], details: {} };
|
|
83
|
+
}
|
|
84
|
+
const nonBlank = (value: string | undefined): string | undefined => {
|
|
85
|
+
const trimmed = value?.trim();
|
|
86
|
+
return trimmed ? trimmed : undefined;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
switch (params.action) {
|
|
91
|
+
case "steer": {
|
|
92
|
+
const instruction = nonBlank(params.instruction);
|
|
93
|
+
if (!instruction) {
|
|
94
|
+
return { content: [{ type: "text", text: "steer requires a non-blank instruction." }], details: {} };
|
|
95
|
+
}
|
|
96
|
+
if (!(["running", "steering"] as const).includes(thread.control.getPhase() as any)) {
|
|
97
|
+
return { content: [{ type: "text", text: `Run #${thread.id} is ${thread.control.getPhase()}; only a running thread can be steered.` }], details: {} };
|
|
98
|
+
}
|
|
99
|
+
await thread.control.steer(instruction);
|
|
100
|
+
inspectorStore.get(thread.id).trajectory.append({ kind: "steer", instruction });
|
|
101
|
+
return { content: [{ type: "text", text: `Queued steering instruction for run #${thread.id} after its current tool batch.` }], details: {} };
|
|
102
|
+
}
|
|
103
|
+
case "retarget": {
|
|
104
|
+
const objective = nonBlank(params.objective);
|
|
105
|
+
if (!objective) {
|
|
106
|
+
return { content: [{ type: "text", text: "retarget requires a non-blank objective." }], details: {} };
|
|
107
|
+
}
|
|
108
|
+
const phase = thread.control.getPhase();
|
|
109
|
+
if (thread.state === "queued" && phase === "queued") {
|
|
110
|
+
thread.task = objective;
|
|
111
|
+
thread.control.retargetPending(objective);
|
|
112
|
+
monitor.setTask(thread.id, objective);
|
|
113
|
+
inspectorStore.get(thread.id).trajectory.append({ kind: "retarget", objective });
|
|
114
|
+
return { content: [{ type: "text", text: `Updated queued run #${thread.id} to the new objective; no child was spawned by this control action.` }], details: {} };
|
|
115
|
+
}
|
|
116
|
+
if (!(["starting", "running", "steering", "interrupting", "retrying"] as const).includes(phase as any)) {
|
|
117
|
+
return { content: [{ type: "text", text: `Run #${thread.id} is ${thread.state}; use resume with objective to restart retained context.` }], details: {} };
|
|
118
|
+
}
|
|
119
|
+
thread.task = objective;
|
|
120
|
+
monitor.setTask(thread.id, objective);
|
|
121
|
+
await thread.control.retarget(objective);
|
|
122
|
+
inspectorStore.get(thread.id).trajectory.append({ kind: "retarget", objective });
|
|
123
|
+
return { content: [{ type: "text", text: `Retargeted run #${thread.id} in the same session; the aborted objective will not be delivered as a completion.` }], details: {} };
|
|
124
|
+
}
|
|
125
|
+
case "park": {
|
|
126
|
+
if (thread.state === "parked") {
|
|
127
|
+
return { content: [{ type: "text", text: `Run #${thread.id} is already parked.` }], details: {} };
|
|
128
|
+
}
|
|
129
|
+
const disposition = await thread.park();
|
|
130
|
+
return disposition === "queued"
|
|
131
|
+
? {
|
|
132
|
+
content: [{ type: "text", text: `Parked queued run #${thread.id}; it never spawned a child or empty session.` }],
|
|
133
|
+
details: {},
|
|
134
|
+
}
|
|
135
|
+
: {
|
|
136
|
+
content: [{ type: "text", text: `Parked run #${thread.id} at a stable checkpoint; its session is retained and concurrency slot released.` }],
|
|
137
|
+
details: {},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
case "resume": {
|
|
141
|
+
if (thread.retired) {
|
|
142
|
+
return { content: [{ type: "text", text: `Run #${thread.id} was retired by subagent_stop and has no resumable session.` }], details: {} };
|
|
143
|
+
}
|
|
144
|
+
if (!(["parked", "completed", "failed"] as const).includes(thread.state as any)) {
|
|
145
|
+
return { content: [{ type: "text", text: `Run #${thread.id} is ${thread.state}; it must be parked or settled before resume.` }], details: {} };
|
|
146
|
+
}
|
|
147
|
+
const objective = params.objective === undefined ? undefined : nonBlank(params.objective);
|
|
148
|
+
if (params.objective !== undefined && !objective) {
|
|
149
|
+
return { content: [{ type: "text", text: "resume objective must be non-blank when provided." }], details: {} };
|
|
150
|
+
}
|
|
151
|
+
const pending = await thread.resume(objective, ctx);
|
|
152
|
+
if (pending.exitCode !== -1) {
|
|
153
|
+
return { content: [{ type: "text", text: getResultOutput(pending) }], details: {} };
|
|
154
|
+
}
|
|
155
|
+
return { content: [{ type: "text", text: `Resumed run #${thread.id}${objective ? " with a new objective" : " from retained context"}; completion will arrive automatically.` }], details: {}, terminate: true };
|
|
156
|
+
}
|
|
157
|
+
case "fork": {
|
|
158
|
+
const objective = params.objective === undefined ? undefined : nonBlank(params.objective);
|
|
159
|
+
if (params.objective !== undefined && !objective) {
|
|
160
|
+
return { content: [{ type: "text", text: "fork objective must be non-blank when provided." }], details: {} };
|
|
161
|
+
}
|
|
162
|
+
const pending = await thread.fork(objective, ctx);
|
|
163
|
+
if (pending.exitCode !== -1 || pending.runId === undefined) {
|
|
164
|
+
return { content: [{ type: "text", text: getResultOutput(pending) }], details: {} };
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
content: [{
|
|
168
|
+
type: "text",
|
|
169
|
+
text: `Forked run #${thread.id} into new run #${pending.runId}${objective ? " with a new objective" : " from retained context"}; the source is unchanged and child completion will arrive automatically.`,
|
|
170
|
+
}],
|
|
171
|
+
details: { sourceRunId: thread.id, childRunId: pending.runId, result: pending },
|
|
172
|
+
terminate: true,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw new Error(`Could not ${params.action} run #${thread.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
renderCall(args, theme) {
|
|
182
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("subagent_control "))}${theme.fg("accent", `${args.action} #${args.id}`)}`, 0, 0);
|
|
183
|
+
},
|
|
184
|
+
renderResult(result, _options, theme) {
|
|
185
|
+
return renderFirstLine(result, "subagent_control ", theme);
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
40
189
|
const SubagentWaitParams = Type.Object({
|
|
41
190
|
id: Type.Optional(
|
|
42
191
|
Type.String({
|
|
@@ -80,8 +229,8 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
80
229
|
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) && params.timeoutMs >= 0
|
|
81
230
|
? params.timeoutMs
|
|
82
231
|
: SUBAGENT_WAIT_DEFAULT_TIMEOUT_MS;
|
|
83
|
-
const isActive = (run: { status:
|
|
84
|
-
run.status
|
|
232
|
+
const isActive = (run: { status: RunStatus; retained?: boolean }): boolean =>
|
|
233
|
+
isRunActiveStatus(run.status) || run.retained === true;
|
|
85
234
|
|
|
86
235
|
const requested = params.id?.trim();
|
|
87
236
|
// A run that already settled resolves immediately with its result.
|
|
@@ -145,7 +294,12 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
145
294
|
finish({ result: current });
|
|
146
295
|
return;
|
|
147
296
|
}
|
|
148
|
-
|
|
297
|
+
const live = monitor.findRun(runId);
|
|
298
|
+
if (live?.status === "parked") {
|
|
299
|
+
finish({ note: `run #${runId} was parked at a stable checkpoint; use subagent_control resume to continue it` });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (!live) {
|
|
149
303
|
// Removal is followed synchronously by registerRunResult in the
|
|
150
304
|
// finishing task; re-check on the next tick so the result wins.
|
|
151
305
|
setTimeout(() => {
|
|
@@ -242,11 +396,20 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
242
396
|
const activeId = matchRunIds(runs.map((run) => run.id), requested)[0];
|
|
243
397
|
const active = activeId === undefined ? undefined : runs.find((run) => run.id === activeId);
|
|
244
398
|
if (active) {
|
|
399
|
+
const parked = active.status === "parked";
|
|
400
|
+
const activeThread = runtime.threads.get(active.id);
|
|
401
|
+
const metadata = [
|
|
402
|
+
activeThread?.isolation === "worktree" ? `worktree ${active.integrationStatus ?? activeThread.worktree?.state ?? "active"}` : undefined,
|
|
403
|
+
activeThread?.forkedFromRunId !== undefined ? `forked from #${activeThread.forkedFromRunId}` : undefined,
|
|
404
|
+
(activeThread?.forkChildRunIds.length ?? 0) > 0 ? `forks ${activeThread!.forkChildRunIds.map((id) => `#${id}`).join(",")}` : undefined,
|
|
405
|
+
].filter(Boolean).join(" · ");
|
|
245
406
|
return {
|
|
246
407
|
content: [
|
|
247
408
|
{
|
|
248
409
|
type: "text",
|
|
249
|
-
text:
|
|
410
|
+
text: parked
|
|
411
|
+
? `Run #${active.id} ${active.agent} is parked with retained context${metadata ? ` (${metadata})` : ""}. Use subagent_control resume to restart it, or subagent_stop to retire it.`
|
|
412
|
+
: `Run #${active.id} ${active.agent} is still active (${active.activity ?? statusLabel(active.status)}${metadata ? ` · ${metadata}` : ""}). Use subagent_wait to block for its result, subagent_control to steer/park it, or subagent_stop to cancel it.`,
|
|
250
413
|
},
|
|
251
414
|
],
|
|
252
415
|
details: {},
|
|
@@ -257,29 +420,59 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
257
420
|
|
|
258
421
|
const now = Date.now();
|
|
259
422
|
const activeRuns = monitor.getRuns().filter(
|
|
260
|
-
(run) => run.status
|
|
423
|
+
(run) => isRunActiveStatus(run.status) || run.retained,
|
|
261
424
|
);
|
|
262
425
|
const activeLines = activeRuns.map((run) => {
|
|
426
|
+
const thread = runtime.threads.get(run.id);
|
|
427
|
+
const model = run.modelFallbackFrom
|
|
428
|
+
? `${run.model ?? "?"} (pool fallback from ${run.modelFallbackFrom})`
|
|
429
|
+
: (run.model ?? "?");
|
|
263
430
|
const parts = [
|
|
264
431
|
`#${run.id} ${run.agent}`,
|
|
265
|
-
run.
|
|
432
|
+
run.label,
|
|
433
|
+
model,
|
|
266
434
|
formatUsageCompact(run.usage),
|
|
267
435
|
formatElapsed(run, now),
|
|
436
|
+
thread?.isolation === "worktree" ? `worktree ${run.integrationStatus ?? thread.worktree?.state ?? "active"}` : undefined,
|
|
437
|
+
thread?.forkedFromRunId !== undefined ? `forked from #${thread.forkedFromRunId}` : undefined,
|
|
438
|
+
(thread?.forkChildRunIds.length ?? 0) > 0 ? `forks ${thread!.forkChildRunIds.map((id) => `#${id}`).join(",")}` : undefined,
|
|
268
439
|
].filter(Boolean);
|
|
269
440
|
return `- ${parts.join(" · ")} · ${run.activity ?? statusLabel(run.status)}`;
|
|
270
441
|
});
|
|
442
|
+
const parkedThreads = [...runtime.threads.values()].filter((thread) => thread.state === "parked");
|
|
443
|
+
const parkedLines = parkedThreads.map((thread) => {
|
|
444
|
+
const relations = [
|
|
445
|
+
thread.forkedFromRunId !== undefined ? `forked from #${thread.forkedFromRunId}` : undefined,
|
|
446
|
+
thread.forkChildRunIds.length > 0 ? `forks ${thread.forkChildRunIds.map((id) => `#${id}`).join(",")}` : undefined,
|
|
447
|
+
].filter(Boolean);
|
|
448
|
+
const relation = relations.length > 0 ? ` · ${relations.join(" · ")}` : "";
|
|
449
|
+
const isolation = thread.isolation === "worktree" ? ` · worktree ${thread.worktree?.state ?? "active"}` : "";
|
|
450
|
+
return `- #${thread.id} ${thread.agentName} · ${runLabel(thread.task)} · parked${thread.sessionDir ? " · context retained" : " · not started"}${isolation}${relation}`;
|
|
451
|
+
});
|
|
271
452
|
const completed = [...runtime.settledRuns.entries()].slice(-5);
|
|
272
453
|
const completedLines = completed.map(([id, result]) => {
|
|
273
454
|
const usage = formatUsage(result.usage);
|
|
274
|
-
|
|
455
|
+
const label = runLabel(result.task);
|
|
456
|
+
const model = result.modelFallbackFrom
|
|
457
|
+
? `${result.model ?? "?"} (pool fallback from ${result.modelFallbackFrom})`
|
|
458
|
+
: (result.model ?? "?");
|
|
459
|
+
const isolation = result.isolation === "worktree" ? ` · worktree ${result.integrationStatus ?? "unknown"}` : "";
|
|
460
|
+
const relations = [
|
|
461
|
+
result.forkedFromRunId !== undefined ? `forked from #${result.forkedFromRunId}` : undefined,
|
|
462
|
+
(result.forkChildRunIds?.length ?? 0) > 0 ? `forks ${result.forkChildRunIds!.map((childId) => `#${childId}`).join(",")}` : undefined,
|
|
463
|
+
].filter(Boolean);
|
|
464
|
+
const relation = relations.length > 0 ? ` · ${relations.join(" · ")}` : "";
|
|
465
|
+
return `- #${id} ${result.agent}${label ? ` · ${label}` : ""} · ${isFailedResult(result) ? "failed" : "completed"} · ${model}${isolation}${relation}${usage ? ` · ${usage}` : ""}`;
|
|
275
466
|
});
|
|
276
467
|
|
|
277
468
|
const sections: string[] = [];
|
|
278
469
|
sections.push(`### Active subagent runs (${activeRuns.length})`);
|
|
279
470
|
sections.push(activeLines.length > 0 ? activeLines.join("\n") : "(none)");
|
|
471
|
+
sections.push(`### Parked subagent threads (${parkedThreads.length})`);
|
|
472
|
+
sections.push(parkedLines.length > 0 ? parkedLines.join("\n") : "(none)");
|
|
280
473
|
sections.push(`### Finished this session (${runtime.settledRuns.size})`);
|
|
281
474
|
sections.push(completedLines.length > 0 ? completedLines.join("\n") : "(none)");
|
|
282
|
-
sections.push("Pass a run id to subagent_status for the full result,
|
|
475
|
+
sections.push("Pass a run id to subagent_status for the full result, use subagent_control to steer/park/resume/fork, or subagent_wait for active work.");
|
|
283
476
|
return { content: [{ type: "text", text: sections.join("\n\n") }], details: {} };
|
|
284
477
|
},
|
|
285
478
|
|
|
@@ -312,8 +505,8 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
312
505
|
name: "subagent_stop",
|
|
313
506
|
label: "Subagent Stop",
|
|
314
507
|
description: [
|
|
315
|
-
"
|
|
316
|
-
"Pass id (run id or prefix) to stop one
|
|
508
|
+
"Destructively stop a sub-agent thread: terminate active work, deliver its aborted partial result, and retire any retained session so it cannot be resumed.",
|
|
509
|
+
"Pass id (run id or prefix) to stop one active, parked, or completed thread; all: true stops every active run.",
|
|
317
510
|
].join(" "),
|
|
318
511
|
promptSnippet: "Stop a running background subagent (id from the widget/subagent_status; or all: true).",
|
|
319
512
|
promptGuidelines: [
|
|
@@ -322,71 +515,220 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
322
515
|
],
|
|
323
516
|
parameters: SubagentStopParams,
|
|
324
517
|
|
|
325
|
-
async execute(_toolCallId, params, _signal, _onUpdate) {
|
|
518
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
519
|
+
// Start config I/O without yielding: every target below must be claimed
|
|
520
|
+
// synchronously before a resume/fork preflight can cross its next await.
|
|
521
|
+
const configPromise = loadConfig(runtime.configPath).catch(() => undefined);
|
|
522
|
+
const completionResults: SingleResult[] = [];
|
|
523
|
+
const candidateIds = params.all === true
|
|
524
|
+
? [...new Set([
|
|
525
|
+
...runtime.runControllers.keys(),
|
|
526
|
+
...[...runtime.threads.values()]
|
|
527
|
+
.filter((thread) =>
|
|
528
|
+
thread.lifecycleOperation !== undefined ||
|
|
529
|
+
["queued", "resuming", "running", "steering", "interrupting"].includes(thread.state),
|
|
530
|
+
)
|
|
531
|
+
.map((thread) => thread.id),
|
|
532
|
+
])]
|
|
533
|
+
: [...runtime.threads.keys()];
|
|
326
534
|
const targets =
|
|
327
535
|
params.all === true
|
|
328
|
-
?
|
|
536
|
+
? candidateIds
|
|
329
537
|
: params.id !== undefined && params.id.trim() !== ""
|
|
330
|
-
? matchRunIds(
|
|
538
|
+
? matchRunIds(candidateIds, params.id.trim())
|
|
331
539
|
: [];
|
|
332
540
|
|
|
333
541
|
if (targets.length === 0) {
|
|
334
|
-
const
|
|
542
|
+
const available = [...runtime.threads.keys()].map((id) => `#${id}`).join(", ");
|
|
335
543
|
return {
|
|
336
|
-
content: [
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
: `No active subagent run matches "${params.id}".${activeList ? ` Active runs: ${activeList}.` : ""}`,
|
|
343
|
-
},
|
|
344
|
-
],
|
|
544
|
+
content: [{
|
|
545
|
+
type: "text",
|
|
546
|
+
text: params.all === true
|
|
547
|
+
? "No active subagent runs to stop."
|
|
548
|
+
: `No subagent thread matches "${params.id}".${available ? ` Known threads: ${available}.` : ""}`,
|
|
549
|
+
}],
|
|
345
550
|
details: {},
|
|
346
551
|
};
|
|
347
552
|
}
|
|
348
553
|
|
|
349
|
-
const
|
|
554
|
+
const claimed: Array<{
|
|
555
|
+
runId: number;
|
|
556
|
+
thread: SubagentThread;
|
|
557
|
+
run: ReturnType<typeof monitor.findRun>;
|
|
558
|
+
previousState: SubagentThread["state"];
|
|
559
|
+
wasQueued: boolean;
|
|
560
|
+
wasResuming: boolean;
|
|
561
|
+
wasActive: boolean;
|
|
562
|
+
generation: number;
|
|
563
|
+
controller: AbortController | undefined;
|
|
564
|
+
completion: Promise<void>;
|
|
565
|
+
stopVersion: number;
|
|
566
|
+
stopMessage: string;
|
|
567
|
+
}> = [];
|
|
350
568
|
for (const runId of targets) {
|
|
351
|
-
const
|
|
352
|
-
if (!
|
|
353
|
-
|
|
354
|
-
|
|
569
|
+
const thread = runtime.threads.get(runId);
|
|
570
|
+
if (!thread) continue;
|
|
571
|
+
const previousState = thread.state;
|
|
572
|
+
const wasQueued = previousState === "queued";
|
|
573
|
+
const wasResuming = previousState === "resuming";
|
|
574
|
+
const wasActive =
|
|
575
|
+
thread.lifecycleOperation !== undefined ||
|
|
576
|
+
["queued", "resuming", "running", "steering", "interrupting"].includes(previousState);
|
|
577
|
+
const stopVersion = ++thread.lifecycleVersion;
|
|
578
|
+
// Stop-all claims every target before the first await. This invalidates
|
|
579
|
+
// all concurrent resume/fork preflights as one synchronous operation.
|
|
580
|
+
thread.lifecycleOperation = "stop";
|
|
581
|
+
thread.retired = true;
|
|
582
|
+
thread.retireOnSettle = true;
|
|
583
|
+
thread.state = "stopped";
|
|
584
|
+
const stopMessage = wasQueued
|
|
585
|
+
? "Stopped by subagent_stop before the run started."
|
|
586
|
+
: wasResuming
|
|
587
|
+
? "Stopped by subagent_stop while resume was preparing."
|
|
588
|
+
: wasActive
|
|
589
|
+
? "Stopped by subagent_stop."
|
|
590
|
+
: previousState === "parked"
|
|
591
|
+
? "Stopped by subagent_stop from a parked checkpoint."
|
|
592
|
+
: "Retired by subagent_stop.";
|
|
593
|
+
claimed.push({
|
|
594
|
+
runId,
|
|
595
|
+
thread,
|
|
596
|
+
run: monitor.findRun(runId),
|
|
597
|
+
previousState,
|
|
598
|
+
wasQueued,
|
|
599
|
+
wasResuming,
|
|
600
|
+
wasActive,
|
|
601
|
+
generation: thread.generation,
|
|
602
|
+
controller: thread.queueController,
|
|
603
|
+
completion: thread.generationCompletion,
|
|
604
|
+
stopVersion,
|
|
605
|
+
stopMessage,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const stopped: string[] = [];
|
|
610
|
+
const retainedIntegration: string[] = [];
|
|
611
|
+
for (const claim of claimed) {
|
|
612
|
+
const {
|
|
613
|
+
runId,
|
|
614
|
+
thread,
|
|
615
|
+
run,
|
|
616
|
+
previousState,
|
|
617
|
+
wasQueued,
|
|
618
|
+
wasResuming,
|
|
619
|
+
wasActive,
|
|
620
|
+
generation,
|
|
621
|
+
controller,
|
|
622
|
+
completion,
|
|
623
|
+
stopVersion,
|
|
624
|
+
stopMessage,
|
|
625
|
+
} = claim;
|
|
626
|
+
await thread.control.stop(stopMessage).catch(() => undefined);
|
|
627
|
+
runtime.backgroundQueue.cancel(controller);
|
|
628
|
+
await completion;
|
|
629
|
+
if (runtime.runControllers.get(runId) === controller) runtime.runControllers.delete(runId);
|
|
630
|
+
if (thread.queueController === controller) thread.queueController = undefined;
|
|
631
|
+
|
|
632
|
+
// Dispatch yields publication ownership as soon as stop claims the
|
|
633
|
+
// lifecycle. Synthesize and publish the one aborted result here only when
|
|
634
|
+
// this stop actually interrupted unfinished work.
|
|
635
|
+
let stoppedResult: SingleResult | undefined;
|
|
636
|
+
if (
|
|
637
|
+
wasQueued ||
|
|
638
|
+
wasResuming ||
|
|
639
|
+
previousState === "parked" ||
|
|
640
|
+
!runtime.settledRuns.has(runId)
|
|
641
|
+
) {
|
|
642
|
+
const prior = thread.lastResult;
|
|
643
|
+
stoppedResult = prior
|
|
644
|
+
? {
|
|
645
|
+
...prior,
|
|
646
|
+
parked: undefined,
|
|
647
|
+
exitCode: 1,
|
|
648
|
+
stopReason: "aborted",
|
|
649
|
+
errorMessage: stopMessage,
|
|
650
|
+
runId,
|
|
651
|
+
}
|
|
652
|
+
: {
|
|
653
|
+
agent: thread.agentName,
|
|
654
|
+
agentSource: "builtin",
|
|
655
|
+
task: thread.task,
|
|
656
|
+
exitCode: 1,
|
|
657
|
+
messages: [],
|
|
658
|
+
stderr: stopMessage,
|
|
659
|
+
usage: emptyUsage(),
|
|
660
|
+
model: run?.model,
|
|
661
|
+
thinking: run?.thinking,
|
|
662
|
+
stopReason: "aborted",
|
|
663
|
+
errorMessage: stopMessage,
|
|
664
|
+
runId,
|
|
665
|
+
isolation: thread.isolation,
|
|
666
|
+
originalCwd: thread.cwd,
|
|
667
|
+
isolationCwd: thread.executionCwd,
|
|
668
|
+
};
|
|
669
|
+
const finalization = await thread.finalizeIsolation(generation, stoppedResult);
|
|
670
|
+
if (finalization?.status === "retained") retainedIntegration.push(`#${runId}`);
|
|
671
|
+
runtime.registerRunResult(runId, stoppedResult);
|
|
672
|
+
thread.lastResult = stoppedResult;
|
|
673
|
+
}
|
|
674
|
+
monitor.setStatus(runId, "failed");
|
|
675
|
+
if (stoppedResult) {
|
|
676
|
+
const inspectState = inspectorStore.get(runId);
|
|
677
|
+
const alreadyStampedStopped = inspectState.trajectory.getGenerationEvents().some(
|
|
678
|
+
(event) => event.kind === "settled" && event.status === "stopped",
|
|
679
|
+
);
|
|
680
|
+
if (!alreadyStampedStopped) {
|
|
681
|
+
inspectState.trajectory.append({
|
|
682
|
+
kind: "settled",
|
|
683
|
+
status: "stopped",
|
|
684
|
+
model: stoppedResult.model ?? run?.model,
|
|
685
|
+
isolation: thread.isolation,
|
|
686
|
+
...(stoppedResult.integrationStatus && stoppedResult.integrationStatus !== "pending"
|
|
687
|
+
? { integrationStatus: stoppedResult.integrationStatus }
|
|
688
|
+
: {}),
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
const stoppedRun = monitor.findRun(runId);
|
|
692
|
+
inspectState.retainFrom(stoppedRun
|
|
693
|
+
? {
|
|
694
|
+
...stoppedRun,
|
|
695
|
+
agent: stoppedResult.agent,
|
|
696
|
+
task: stoppedResult.task,
|
|
697
|
+
model: stoppedResult.model ?? stoppedRun.model,
|
|
698
|
+
usage: stoppedResult.usage,
|
|
699
|
+
}
|
|
700
|
+
: {
|
|
701
|
+
agent: stoppedResult.agent,
|
|
702
|
+
task: stoppedResult.task,
|
|
703
|
+
model: stoppedResult.model,
|
|
704
|
+
thinking: stoppedResult.thinking,
|
|
705
|
+
status: "failed",
|
|
706
|
+
endedAt: inspectState.trajectory.summary().endedAt,
|
|
707
|
+
usage: stoppedResult.usage,
|
|
708
|
+
});
|
|
709
|
+
completionResults.push(stoppedResult);
|
|
355
710
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
// a started task owns its own (real, partial-output) result.
|
|
361
|
-
const controller = runtime.runControllers.get(runId);
|
|
362
|
-
controller?.abort();
|
|
363
|
-
// A queued run never reaches the child-spawn code path, so its abort
|
|
364
|
-
// goes through the queue's cancelled callback with no result object;
|
|
365
|
-
// register a synthetic aborted result so subagent_wait resolves.
|
|
366
|
-
if (run.status === "queued" && runtime.runControllers.has(runId)) {
|
|
367
|
-
runtime.registerRunResult(runId, {
|
|
368
|
-
agent: run.agent,
|
|
369
|
-
agentSource: "builtin",
|
|
370
|
-
task: run.task,
|
|
371
|
-
exitCode: 1,
|
|
372
|
-
messages: [],
|
|
373
|
-
stderr: "Stopped by subagent_stop before the run started.",
|
|
374
|
-
usage: emptyUsage(),
|
|
375
|
-
model: run.model,
|
|
376
|
-
thinking: run.thinking,
|
|
377
|
-
stopReason: "aborted",
|
|
378
|
-
errorMessage: "Stopped by subagent_stop before the run started.",
|
|
379
|
-
});
|
|
711
|
+
monitor.removeRun(runId);
|
|
712
|
+
runtime.retireThreadSession(thread);
|
|
713
|
+
if (thread.lifecycleVersion === stopVersion && thread.lifecycleOperation === "stop") {
|
|
714
|
+
thread.lifecycleOperation = undefined;
|
|
380
715
|
}
|
|
381
|
-
stopped.push(`#${runId} ${
|
|
716
|
+
stopped.push(`#${runId} ${thread.agentName}${wasQueued ? " (queued)" : wasActive ? "" : ` (${previousState})`}`);
|
|
717
|
+
}
|
|
718
|
+
if (completionResults.length > 0) {
|
|
719
|
+
const maxResultLines = (await configPromise)?.maxResultLines ?? DEFAULT_MAX_RESULT_LINES;
|
|
720
|
+
runtime.sendCompletionGroup(completionResults.map((result) => ({
|
|
721
|
+
agent: result.agent,
|
|
722
|
+
block: formatCompletionBlock(result, maxResultLines, ctx.cwd),
|
|
723
|
+
triggerTurn: true,
|
|
724
|
+
})));
|
|
725
|
+
runtime.completionBatcher.flush();
|
|
382
726
|
}
|
|
383
727
|
return {
|
|
384
|
-
content: [
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
},
|
|
389
|
-
],
|
|
728
|
+
content: [{
|
|
729
|
+
type: "text",
|
|
730
|
+
text: `Stopped ${stopped.length} thread${stopped.length === 1 ? "" : "s"}: ${stopped.join(", ")}. Retained sessions were retired; worktree changes are integrated on settlement.${retainedIntegration.length > 0 ? ` Integration failed for ${retainedIntegration.join(", ")}; inspect its result for retained recovery paths.` : ""}`,
|
|
731
|
+
}],
|
|
390
732
|
details: {},
|
|
391
733
|
};
|
|
392
734
|
},
|