@ferris1225/pi-subagents 4.3.9 → 4.3.11
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 +39 -0
- package/README.md +136 -166
- package/agents/artisan.md +7 -11
- package/agents/scout.md +7 -11
- package/agents/sentinel.md +7 -8
- package/agents/steward.md +8 -9
- package/index.ts +1 -1
- package/package.json +12 -10
- package/src/configuration/setup.ts +16 -14
- package/src/delegation/agents.ts +1 -1
- package/src/delegation/dispatch.ts +18 -23
- package/src/delegation/phase-scope.ts +7 -37
- package/src/delegation/prompt.ts +15 -32
- package/src/execution/rpc-control.ts +2 -29
- package/src/execution/rpc-run.ts +29 -30
- package/src/execution/spawn.ts +21 -20
- package/src/isolation/temp-hygiene.ts +7 -9
- package/src/isolation/worktree.ts +13 -88
- package/src/lifecycle/durable.ts +6 -8
- package/src/lifecycle/runtime.ts +23 -70
- package/src/lifecycle/thread-lifecycle.ts +56 -459
- package/src/lifecycle/thread-restore.ts +13 -35
- package/src/lifecycle/thread-shared.ts +5 -65
- package/src/lifecycle/tools.ts +87 -252
- package/src/presentation/announcements.ts +1 -1
- package/src/presentation/format.ts +6 -16
- package/src/presentation/monitor.ts +0 -91
- package/src/presentation/widget.ts +7 -17
- package/src/execution/session-fork.ts +0 -86
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
restoredResultFromSummary,
|
|
13
13
|
type ThreadRecord,
|
|
14
14
|
} from "./durable.ts";
|
|
15
|
-
import { failedStartResult } from "../presentation/format.ts";
|
|
16
15
|
import { monitor } from "../presentation/monitor.ts";
|
|
17
16
|
import { emptyUsage } from "../execution/rpc-control.ts";
|
|
18
17
|
import type { SubagentRuntime, SubagentThread, ThreadState } from "./runtime.ts";
|
|
@@ -33,8 +32,7 @@ import {
|
|
|
33
32
|
} from "../isolation/worktree.ts";
|
|
34
33
|
import { installThreadLifecycle } from "./thread-lifecycle.ts";
|
|
35
34
|
|
|
36
|
-
/**
|
|
37
|
-
* artifacts go away with the record. */
|
|
35
|
+
/** Release managed artifacts belonging to a discarded, already-settled record. */
|
|
38
36
|
async function discardRestoredRecord(runtime: SubagentRuntime, record: ThreadRecord): Promise<void> {
|
|
39
37
|
if (record.sessionDir) {
|
|
40
38
|
await rm(record.sessionDir, { recursive: true, force: true }).catch(() => undefined);
|
|
@@ -74,31 +72,15 @@ function createRestoredThread(
|
|
|
74
72
|
sessionId: record.sessionId,
|
|
75
73
|
sessionDir: record.sessionDir,
|
|
76
74
|
lastResult: restoredResultFromSummary(record),
|
|
77
|
-
resume: async () => failedStartResult(record.agentName, record.task, "Thread resume was not initialized."),
|
|
78
75
|
finalizeIsolation: async () => undefined,
|
|
79
76
|
};
|
|
80
|
-
installThreadLifecycle(thread, {
|
|
81
|
-
runtime,
|
|
82
|
-
startBackground: (...args) => {
|
|
83
|
-
const dispatcher = runtime.dispatcher;
|
|
84
|
-
if (!dispatcher) {
|
|
85
|
-
return Promise.resolve(failedStartResult(
|
|
86
|
-
record.agentName,
|
|
87
|
-
record.task,
|
|
88
|
-
`Run #${record.runId} cannot continue: no dispatch context is available yet. Dispatch any subagent once, then retry.`,
|
|
89
|
-
));
|
|
90
|
-
}
|
|
91
|
-
return dispatcher(...args);
|
|
92
|
-
},
|
|
93
|
-
});
|
|
77
|
+
installThreadLifecycle(thread, { runtime });
|
|
94
78
|
return thread;
|
|
95
79
|
}
|
|
96
80
|
|
|
97
|
-
/** Rebuild interrupted
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* left by older versions, which hold no work worth resuming — drop out with
|
|
101
|
-
* their artifacts. Returns the restored run ids. */
|
|
81
|
+
/** Rebuild interrupted records for manual recovery after reload. Orphaned children
|
|
82
|
+
* are stopped first; missing session files do not discard isolated edits. Already-
|
|
83
|
+
* settled records from older versions are removed with their managed artifacts. */
|
|
102
84
|
export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<number[]> {
|
|
103
85
|
const records = await readThreadRecords(runtime.configPath);
|
|
104
86
|
const restoredIds: number[] = [];
|
|
@@ -121,22 +103,18 @@ export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<n
|
|
|
121
103
|
record.sessionId !== undefined &&
|
|
122
104
|
record.sessionDir !== undefined &&
|
|
123
105
|
sessionExists(record.sessionDir, record.sessionId);
|
|
124
|
-
|
|
125
|
-
await discardRestoredRecord(runtime, record);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
106
|
+
const restoredRecord = sessionValid ? record : { ...record, sessionId: undefined, sessionDir: undefined };
|
|
128
107
|
const worktree = record.worktree
|
|
129
108
|
? await restoreWorktreeIsolation(record.worktree).catch(() => undefined)
|
|
130
109
|
: undefined;
|
|
131
110
|
const restorationFailed = record.isolation === "worktree" && record.worktree !== undefined && !worktree;
|
|
132
|
-
const thread = createRestoredThread(runtime,
|
|
111
|
+
const thread = createRestoredThread(runtime, restoredRecord, worktree, restorationFailed ? "failed" : "parked");
|
|
133
112
|
runtime.threads.set(record.runId, thread);
|
|
134
|
-
runtime.sessionDirs.add(
|
|
113
|
+
if (thread.sessionDir) runtime.sessionDirs.add(thread.sessionDir);
|
|
135
114
|
if (restorationFailed) {
|
|
136
|
-
const reason = `Run #${record.runId}'s recorded worktree could not be restored; isolated edits may be unavailable. The
|
|
137
|
-
thread.resumeUnavailableReason = reason;
|
|
115
|
+
const reason = `Run #${record.runId}'s recorded worktree could not be restored; isolated edits may be unavailable. The durable record and any remaining artifacts were kept for manual recovery by main.`;
|
|
138
116
|
thread.restorationRecord = record;
|
|
139
|
-
const previous =
|
|
117
|
+
const previous = thread.lastResult;
|
|
140
118
|
const failed: SingleResult = {
|
|
141
119
|
agent: record.agentName,
|
|
142
120
|
task: record.task,
|
|
@@ -149,8 +127,8 @@ export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<n
|
|
|
149
127
|
stopReason: "error",
|
|
150
128
|
errorMessage: reason,
|
|
151
129
|
dispatchFailed: true,
|
|
152
|
-
sessionId:
|
|
153
|
-
sessionDir:
|
|
130
|
+
sessionId: thread.sessionId,
|
|
131
|
+
sessionDir: thread.sessionDir,
|
|
154
132
|
projectCwd: record.cwd,
|
|
155
133
|
runId: record.runId,
|
|
156
134
|
isolation: "worktree",
|
|
@@ -228,7 +206,7 @@ export function bootstrapDurableState(runtime: SubagentRuntime): Promise<void> {
|
|
|
228
206
|
}
|
|
229
207
|
try {
|
|
230
208
|
// Sessions and worktrees outlive their process on purpose, so only
|
|
231
|
-
// ownership separates
|
|
209
|
+
// ownership separates a live Pi's state from artifacts a crash
|
|
232
210
|
// abandoned. Valid thread and recovery records always keep their paths.
|
|
233
211
|
const records = await readThreadRecords(runtime.configPath);
|
|
234
212
|
const recoveryRecords = await readRecoveryRecords(runtime.configPath);
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from "../configuration/models.ts";
|
|
18
18
|
import type { SubagentRuntime, SubagentThread } from "./runtime.ts";
|
|
19
19
|
import { getProjectRoot, type SingleResult } from "../execution/spawn.ts";
|
|
20
|
-
import { resolveRepositoryRoot, type IsolationMode
|
|
20
|
+
import { resolveRepositoryRoot, type IsolationMode } from "../isolation/worktree.ts";
|
|
21
21
|
|
|
22
22
|
/** Control operations must never wait forever on a settling generation: the
|
|
23
23
|
* queue task can legitimately spend minutes in worktree finalization (bounded
|
|
@@ -114,42 +114,8 @@ export async function runInManagedRepositoryLane<T>(
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
/**
|
|
118
|
-
*
|
|
119
|
-
export function beginRuntimePreflight(runtime: SubagentRuntime): () => void {
|
|
120
|
-
let resolvePreflight!: () => void;
|
|
121
|
-
const preflight = new Promise<void>((resolve) => {
|
|
122
|
-
resolvePreflight = resolve;
|
|
123
|
-
});
|
|
124
|
-
runtime.preflightOperations.add(preflight);
|
|
125
|
-
return () => {
|
|
126
|
-
runtime.preflightOperations.delete(preflight);
|
|
127
|
-
resolvePreflight();
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Synchronous CAS used by lifecycle controls across their async preflight. */
|
|
132
|
-
export function ownsResumeReservation(
|
|
133
|
-
runtime: SubagentRuntime,
|
|
134
|
-
thread: SubagentThread,
|
|
135
|
-
reservation: { version: number; generation: number; sessionId?: string; sessionDir?: string },
|
|
136
|
-
): boolean {
|
|
137
|
-
return (
|
|
138
|
-
runtime.sessionActive &&
|
|
139
|
-
runtime.threads.get(thread.id) === thread &&
|
|
140
|
-
!thread.retired &&
|
|
141
|
-
thread.lifecycleOperation === "resume" &&
|
|
142
|
-
thread.lifecycleVersion === reservation.version &&
|
|
143
|
-
thread.generation === reservation.generation &&
|
|
144
|
-
thread.sessionId === reservation.sessionId &&
|
|
145
|
-
thread.sessionDir === reservation.sessionDir
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Fire-and-forget durable checkpoint. Parked threads stay resumable across
|
|
150
|
-
* reloads; a settled thread drops its record so the manifest only exists
|
|
151
|
-
* while unfinished work needs it. The live session keeps working when the
|
|
152
|
-
* manifest is unwritable; only cross-reload resume is degraded. */
|
|
117
|
+
/** Best-effort recovery checkpoint. Interrupted work retains its artifacts;
|
|
118
|
+
* settled work drops the thread record. A failed write never stops the live run. */
|
|
153
119
|
export function persistThreadCheckpoint(
|
|
154
120
|
runtime: SubagentRuntime,
|
|
155
121
|
thread: SubagentThread,
|
|
@@ -183,33 +149,12 @@ export interface DispatchEnvironment {
|
|
|
183
149
|
agents: AgentConfig[];
|
|
184
150
|
}
|
|
185
151
|
|
|
186
|
-
|
|
187
|
-
sessionId?: string;
|
|
188
|
-
sessionDir?: string;
|
|
189
|
-
worktree?: WorktreeIsolation;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export interface ResumeReservation {
|
|
193
|
-
version: number;
|
|
194
|
-
generation: number;
|
|
195
|
-
sessionId?: string;
|
|
196
|
-
sessionDir?: string;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Dispatcher's internal entry point. Public phase/scope claims are normalized
|
|
200
|
-
* into options; resume adds lifecycle-only continuation fields there too. */
|
|
152
|
+
/** Admission metadata for a fresh one-shot run. */
|
|
201
153
|
export interface StartBackgroundOptions {
|
|
202
|
-
/** Normalized identity and claims for a fresh or resumed generation. */
|
|
203
154
|
phaseId?: string;
|
|
204
155
|
scope?: PhaseScope;
|
|
205
|
-
/**
|
|
156
|
+
/** A false hint cannot downgrade a live write-capable role. */
|
|
206
157
|
writeCapable?: boolean;
|
|
207
|
-
/** Resume path only: the thread whose retained context continues. */
|
|
208
|
-
existingThread?: SubagentThread;
|
|
209
|
-
appendedObjectiveOnResume?: boolean;
|
|
210
|
-
environment?: DispatchEnvironment;
|
|
211
|
-
seed?: SessionSeed;
|
|
212
|
-
resumeReservation?: ResumeReservation;
|
|
213
158
|
/** Chosen by the tool call before the queue can start a fast child. */
|
|
214
159
|
deliveryRoute?: "background" | "await";
|
|
215
160
|
}
|
|
@@ -224,12 +169,7 @@ export type StartBackgroundInternal = (
|
|
|
224
169
|
|
|
225
170
|
export interface ThreadLifecycleDeps {
|
|
226
171
|
runtime: SubagentRuntime;
|
|
227
|
-
/** Fallback context when a control caller supplies none; restored threads
|
|
228
|
-
* install without one and rely on the per-call context. */
|
|
229
172
|
runCtx?: ExtensionContext;
|
|
230
|
-
/** Fresh dispatch passes the live dispatcher; restored threads resolve it
|
|
231
|
-
* from the runtime at call time so they never pin a stale closure. */
|
|
232
|
-
startBackground: StartBackgroundInternal;
|
|
233
173
|
}
|
|
234
174
|
|
|
235
175
|
interface DispatchModelRoute {
|
package/src/lifecycle/tools.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thread controls around the subagent runtime: subagent_control
|
|
3
|
-
* (steer/resume/park) and destructive subagent_stop. There is no status/poll
|
|
4
|
-
* tool — completions carry each result (with an on-disk artifact when
|
|
5
|
-
* truncated) and wake the main model, so waiting is never a tool call; the only
|
|
6
|
-
* in-turn block is `wait: true` on a dispatch, for one-shot parents that exit
|
|
7
|
-
* at end of turn.
|
|
8
|
-
*/
|
|
1
|
+
/** Read-only run inspection and destructive cancellation of one-shot runs. */
|
|
9
2
|
|
|
10
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
11
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
4
|
import { Text } from "@earendil-works/pi-tui";
|
|
13
5
|
import { existsSync } from "node:fs";
|
|
@@ -16,11 +8,11 @@ import { DEFAULT_MAX_RESULT_LINES, loadConfig } from "../configuration/config.ts
|
|
|
16
8
|
import { removeThreadRecord } from "./durable.ts";
|
|
17
9
|
import { formatCompletionBlock, matchRunIds } from "../presentation/format.ts";
|
|
18
10
|
import { emptyUsage } from "../execution/rpc-control.ts";
|
|
19
|
-
import {
|
|
11
|
+
import { formatDuration, formatTaskSummary, monitor } from "../presentation/monitor.ts";
|
|
20
12
|
import { persistRecoveryRecords, recoveryRecordFromFinalization } from "../isolation/recovery.ts";
|
|
21
13
|
import type { SubagentRuntime, SubagentThread } from "./runtime.ts";
|
|
22
|
-
import { CONTROL_QUIESCE_TIMEOUT_MS,
|
|
23
|
-
import {
|
|
14
|
+
import { CONTROL_QUIESCE_TIMEOUT_MS, projectResultsRoot, quiesced } from "./thread-shared.ts";
|
|
15
|
+
import { getResultError, type SingleResult } from "../execution/spawn.ts";
|
|
24
16
|
import type { WorktreeFinalization } from "../isolation/worktree.ts";
|
|
25
17
|
|
|
26
18
|
function renderFirstLine(result: { content?: unknown }, label: string, theme: any): Text {
|
|
@@ -34,207 +26,79 @@ function renderFirstLine(result: { content?: unknown }, label: string, theme: an
|
|
|
34
26
|
}
|
|
35
27
|
|
|
36
28
|
export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
37
|
-
const ResumeScopeSchema = Type.Optional(Type.Object({
|
|
38
|
-
paths: Type.Optional(Type.Array(Type.String({ minLength: 1, pattern: "\\S" }))),
|
|
39
|
-
symbols: Type.Optional(Type.Array(Type.Object({
|
|
40
|
-
path: Type.String({ minLength: 1, pattern: "\\S" }),
|
|
41
|
-
name: Type.String({ minLength: 1, pattern: "\\S" }),
|
|
42
|
-
}))),
|
|
43
|
-
}, { description: "Additional declarative write claims for resume; normalized claims are unioned with retained scope and cannot remove it. Scope is conflict metadata, not permissions or a sandbox." }));
|
|
44
|
-
const SubagentControlParams = Type.Object({
|
|
45
|
-
action: StringEnum(["steer", "resume", "park"] as const, {
|
|
46
|
-
description:
|
|
47
|
-
"steer: send guidance to the running attempt (a settled or parked thread continues with it); resume: continue a parked or settled thread; park: pause a running thread at a stable checkpoint, keeping its session and worktree for a later resume.",
|
|
48
|
-
}),
|
|
49
|
-
id: Type.Integer({ minimum: 1, description: "Stable run id shown by subagent dispatch output." }),
|
|
50
|
-
objective: Type.Optional(
|
|
51
|
-
Type.String({ description: "Guidance for steer (required and nonblank), or an optional appended objective for resume. Ignored by park." }),
|
|
52
|
-
),
|
|
53
|
-
scope: ResumeScopeSchema,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
/** A thread that a steer can continue instead of reject: it is not live, but
|
|
57
|
-
* its retained session can absorb the guidance as an appended objective. */
|
|
58
|
-
type ContinuableState = "completed" | "failed" | "parked";
|
|
59
|
-
|
|
60
29
|
pi.registerTool({
|
|
61
|
-
name: "
|
|
62
|
-
label: "Subagent
|
|
63
|
-
description: "
|
|
64
|
-
parameters:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// read the manifest; resuming before that would deny a live run id.
|
|
30
|
+
name: "subagent_status",
|
|
31
|
+
label: "Subagent Status",
|
|
32
|
+
description: "Read current-session run states without waiting or changing execution. Omit id to list runs, or pass an exact numeric id for progress, elapsed time, failure diagnostics, and retained result/recovery paths. Completions arrive automatically; use this for inspection, not a polling loop.",
|
|
33
|
+
parameters: Type.Object({
|
|
34
|
+
id: Type.Optional(Type.Integer({ minimum: 1, description: "Exact run id; omit to list all runs in this parent session." })),
|
|
35
|
+
}),
|
|
36
|
+
async execute(_toolCallId, params) {
|
|
69
37
|
await runtime.durableRestore;
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
? `
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return textResult(`${prefix}: ${mode}; ${context}.`);
|
|
127
|
-
};
|
|
128
|
-
/** Steering guidance for a thread that is no longer live continues the
|
|
129
|
-
* same thread with that guidance instead of being dropped, so the
|
|
130
|
-
* evidence is never re-bought by a second dispatch. */
|
|
131
|
-
const continueSteer = async (objective: string) => {
|
|
132
|
-
const continuable = (): ContinuableState | undefined =>
|
|
133
|
-
thread.state === "completed" || thread.state === "failed" || thread.state === "parked"
|
|
134
|
-
? thread.state
|
|
135
|
-
: undefined;
|
|
136
|
-
if (thread.lifecycleOperation === "settle" || (!continuable() && thread.control.getPhase() === "settled")) {
|
|
137
|
-
if (!(await quiesced(thread.generationCompletion))) return undefined;
|
|
138
|
-
}
|
|
139
|
-
const state = continuable();
|
|
140
|
-
if (!state || thread.lifecycleOperation) return undefined;
|
|
141
|
-
return resumeThread(objective, state);
|
|
38
|
+
const threads = [...runtime.threads.values()]
|
|
39
|
+
.filter((thread) => params.id === undefined || thread.id === params.id)
|
|
40
|
+
.sort((left, right) => left.id - right.id);
|
|
41
|
+
const runs = threads.map((thread) => {
|
|
42
|
+
const live = monitor.findRun(thread.id);
|
|
43
|
+
const result = runtime.settledRuns.get(thread.id) ?? thread.lastResult;
|
|
44
|
+
const state = thread.lifecycleOperation === "stop" ? "interrupting"
|
|
45
|
+
: thread.retired ? "stopped"
|
|
46
|
+
: thread.lifecycleOperation === "settle" ? "settling"
|
|
47
|
+
: thread.state === "parked" ? "interrupted" : thread.state;
|
|
48
|
+
return {
|
|
49
|
+
id: thread.id,
|
|
50
|
+
agent: thread.agentName,
|
|
51
|
+
phaseId: thread.phaseId,
|
|
52
|
+
cwd: thread.cwd,
|
|
53
|
+
executionCwd: thread.executionCwd,
|
|
54
|
+
scope: thread.scope,
|
|
55
|
+
taskSummary: formatTaskSummary(thread.task, 80, false),
|
|
56
|
+
state,
|
|
57
|
+
waitReason: state === "queued" ? live?.waitReason : undefined,
|
|
58
|
+
activity: live?.activity,
|
|
59
|
+
elapsedMs: monitor.getElapsedMs(thread.id) ?? thread.elapsedMs,
|
|
60
|
+
model: live?.model ?? result?.model,
|
|
61
|
+
thinking: live?.thinking ?? result?.thinking ?? thread.thinkingLevel,
|
|
62
|
+
usage: { ...(live?.usage ?? result?.usage ?? emptyUsage()) },
|
|
63
|
+
exitCode: result?.exitCode,
|
|
64
|
+
stopReason: result?.stopReason,
|
|
65
|
+
errorMessage: result ? getResultError(result) : state === "failed" ? "No failure reason was recorded." : undefined,
|
|
66
|
+
resultFile: result?.resultFile,
|
|
67
|
+
sessionDir: thread.retired ? undefined : thread.sessionDir ?? result?.sessionDir,
|
|
68
|
+
isolation: thread.isolation,
|
|
69
|
+
integrationStatus: live?.integrationStatus ?? result?.integrationStatus ?? thread.worktree?.state,
|
|
70
|
+
integrationError: result?.integrationError,
|
|
71
|
+
integrationWorktreePath: result?.integrationWorktreePath ?? (state === "interrupted" ? thread.worktree?.worktreePath : undefined),
|
|
72
|
+
integrationPatchPath: result?.integrationPatchPath,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
const text = runs.length === 0
|
|
76
|
+
? params.id === undefined ? "No subagent runs in this parent session." : `No subagent run matches #${params.id}.`
|
|
77
|
+
: runs.map((run) => {
|
|
78
|
+
const parts = [`#${run.id} ${run.agent}`, run.state, formatDuration(run.elapsedMs)];
|
|
79
|
+
if (run.waitReason) parts.push(`wait: ${run.waitReason}`);
|
|
80
|
+
if (run.activity) parts.push(run.activity);
|
|
81
|
+
if (run.errorMessage) parts.push(formatTaskSummary(run.errorMessage, 300, false));
|
|
82
|
+
const paths = [
|
|
83
|
+
run.resultFile ? `Result: ${run.resultFile}` : undefined,
|
|
84
|
+
run.sessionDir ? `Session: ${run.sessionDir}` : undefined,
|
|
85
|
+
run.integrationWorktreePath ? `Retained worktree: ${run.integrationWorktreePath}` : undefined,
|
|
86
|
+
run.integrationPatchPath ? `Retained patch: ${run.integrationPatchPath}` : undefined,
|
|
87
|
+
].filter(Boolean);
|
|
88
|
+
return `${parts.join(" · ")}\n ${run.taskSummary}${paths.length ? `\n ${paths.join("\n ")}` : ""}`;
|
|
89
|
+
}).join("\n");
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text }],
|
|
92
|
+
details: { runs },
|
|
93
|
+
...(params.id !== undefined && runs.length === 0 ? { isError: true } : {}),
|
|
142
94
|
};
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
switch (params.action) {
|
|
146
|
-
case "steer": {
|
|
147
|
-
const objective = nonBlank(params.objective);
|
|
148
|
-
if (!objective) {
|
|
149
|
-
return textResult("steer objective must be non-blank.");
|
|
150
|
-
}
|
|
151
|
-
if (thread.retired) {
|
|
152
|
-
return textResult(`Run #${thread.id} was retired by subagent_stop and cannot be steered.`);
|
|
153
|
-
}
|
|
154
|
-
const continued = await continueSteer(objective);
|
|
155
|
-
if (continued) return continued;
|
|
156
|
-
const unavailable = inactiveReason();
|
|
157
|
-
if (unavailable) {
|
|
158
|
-
return textResult(`Run #${thread.id} is ${unavailable}; only an active running RPC attempt can be steered. No guidance was sent.`);
|
|
159
|
-
}
|
|
160
|
-
const steered = await thread.control.steer(objective);
|
|
161
|
-
if (!steered.accepted) {
|
|
162
|
-
const resumed = await continueSteer(objective);
|
|
163
|
-
if (resumed) return resumed;
|
|
164
|
-
if (steered.reason === "no-active-attempt") {
|
|
165
|
-
return textResult(`Run #${thread.id} is marked running but has no active RPC attempt; no guidance was sent.`);
|
|
166
|
-
}
|
|
167
|
-
return textResult(`Run #${thread.id} is ${steered.phase}; only an active running RPC attempt can be steered. No guidance was sent.`);
|
|
168
|
-
}
|
|
169
|
-
return textResult(`Steered run #${thread.id} with additional in-scope guidance; its original objective is unchanged.`);
|
|
170
|
-
}
|
|
171
|
-
case "resume": {
|
|
172
|
-
return resumeThread(params.objective);
|
|
173
|
-
}
|
|
174
|
-
case "park": {
|
|
175
|
-
if (thread.retired) {
|
|
176
|
-
return textResult(`Run #${thread.id} was retired by subagent_stop and cannot be parked.`);
|
|
177
|
-
}
|
|
178
|
-
const unavailable = inactiveReason();
|
|
179
|
-
if (unavailable) {
|
|
180
|
-
return textResult(`Run #${thread.id} is ${unavailable}; only an active running RPC attempt can be parked. Use subagent_stop to discard a run that has not started.`);
|
|
181
|
-
}
|
|
182
|
-
if (!thread.sessionId || !thread.sessionDir) {
|
|
183
|
-
return textResult(`Run #${thread.id} has no retained session yet; steer it or let it settle instead.`);
|
|
184
|
-
}
|
|
185
|
-
// Park interrupts the child at its next safe point but keeps the
|
|
186
|
-
// session and worktree, so the thread returns to `parked`, not to a
|
|
187
|
-
// failure. Claim synchronously like stop; the generation body sees
|
|
188
|
-
// the claim and leaves publication to this path.
|
|
189
|
-
const parkVersion = ++thread.lifecycleVersion;
|
|
190
|
-
thread.lifecycleOperation = "park";
|
|
191
|
-
const generation = thread.generation;
|
|
192
|
-
const controller = thread.queueController;
|
|
193
|
-
const completion = thread.generationCompletion;
|
|
194
|
-
const ownsPark = (): boolean =>
|
|
195
|
-
runtime.threads.get(thread.id) === thread &&
|
|
196
|
-
thread.generation === generation &&
|
|
197
|
-
thread.lifecycleVersion === parkVersion &&
|
|
198
|
-
thread.lifecycleOperation === "park" &&
|
|
199
|
-
!thread.retired;
|
|
200
|
-
try {
|
|
201
|
-
await quiesced(thread.control.stop("Parked by subagent_control at a stable checkpoint.").catch(() => undefined));
|
|
202
|
-
if (!(await quiesced(completion))) runtime.backgroundQueue.cancel(controller);
|
|
203
|
-
if (!ownsPark()) {
|
|
204
|
-
return textResult(`Run #${thread.id} changed while it was being parked; no checkpoint was recorded by this call.`);
|
|
205
|
-
}
|
|
206
|
-
if (runtime.runControllers.get(thread.id) === controller) runtime.runControllers.delete(thread.id);
|
|
207
|
-
if (thread.queueController === controller) thread.queueController = undefined;
|
|
208
|
-
thread.state = "parked";
|
|
209
|
-
monitor.setStatus(thread.id, "parked");
|
|
210
|
-
thread.elapsedMs = monitor.getElapsedMs(thread.id) ?? thread.elapsedMs;
|
|
211
|
-
persistThreadCheckpoint(runtime, thread, "parked");
|
|
212
|
-
const run = monitor.findRun(thread.id);
|
|
213
|
-
const usage = run ? formatUsageCompact(run.usage) : "";
|
|
214
|
-
if (runtime.sessionActive) {
|
|
215
|
-
ctx.ui.notify(`■ #${thread.id} ${run ? monitor.summarize(run) : thread.agentName} · parked`, "info");
|
|
216
|
-
}
|
|
217
|
-
const retained = thread.isolation === "worktree" ? "session and worktree" : "session";
|
|
218
|
-
return textResult(
|
|
219
|
-
`Parked run #${thread.id} (${thread.agentName}) at a stable checkpoint${usage ? ` after ${usage}` : ""}. Its retained ${retained} continue on subagent_control resume (optionally with an appended objective) or on steer; subagent_stop discards them.`,
|
|
220
|
-
);
|
|
221
|
-
} finally {
|
|
222
|
-
if (thread.lifecycleVersion === parkVersion && thread.lifecycleOperation === "park") {
|
|
223
|
-
thread.lifecycleOperation = undefined;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
} catch (error) {
|
|
229
|
-
throw new Error(`Could not ${params.action} run #${thread.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
230
|
-
}
|
|
231
95
|
},
|
|
232
|
-
|
|
233
96
|
renderCall(args, theme) {
|
|
234
|
-
return new Text(`${theme.fg("toolTitle", theme.bold("
|
|
97
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("subagent_status "))}${theme.fg("accent", args.id === undefined ? "all" : `#${args.id}`)}`, 0, 0);
|
|
235
98
|
},
|
|
236
|
-
renderResult(result,
|
|
237
|
-
return
|
|
99
|
+
renderResult(result, options, theme) {
|
|
100
|
+
if (options.expanded) return new Text(result.content.map((part) => part.type === "text" ? part.text : "").join("\n"), 0, 0);
|
|
101
|
+
return renderFirstLine(result, "subagent_status ", theme);
|
|
238
102
|
},
|
|
239
103
|
});
|
|
240
104
|
|
|
@@ -253,42 +117,32 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
253
117
|
pi.registerTool({
|
|
254
118
|
name: "subagent_stop",
|
|
255
119
|
label: "Subagent Stop",
|
|
256
|
-
description: "
|
|
120
|
+
description: "Destructively stop and retire one run by id/prefix, or all active runs with all: true. Delivers partial results; stopped runs cannot resume.",
|
|
257
121
|
parameters: SubagentStopParams,
|
|
258
122
|
|
|
259
123
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
260
124
|
await runtime.durableRestore;
|
|
261
|
-
//
|
|
262
|
-
// synchronously before a resume preflight can cross its next await.
|
|
125
|
+
// Claim every target before awaiting process cleanup or configuration I/O.
|
|
263
126
|
const configPromise = loadConfig(runtime.configPath).catch(() => undefined);
|
|
264
127
|
const completionResults: SingleResult[] = [];
|
|
265
128
|
const candidateIds = params.all === true
|
|
266
129
|
? [...new Set([
|
|
267
130
|
...runtime.runControllers.keys(),
|
|
268
131
|
...[...runtime.threads.values()]
|
|
269
|
-
.filter((thread) =>
|
|
270
|
-
thread.lifecycleOperation !== undefined ||
|
|
271
|
-
["queued", "resuming", "running", "interrupting"].includes(thread.state),
|
|
272
|
-
)
|
|
132
|
+
.filter((thread) => thread.lifecycleOperation !== undefined || ["queued", "running", "interrupting"].includes(thread.state))
|
|
273
133
|
.map((thread) => thread.id),
|
|
274
134
|
])]
|
|
275
135
|
: [...runtime.threads.keys()];
|
|
276
|
-
const targets =
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
: params.id !== undefined && params.id.trim() !== ""
|
|
280
|
-
? matchRunIds(candidateIds, params.id.trim())
|
|
281
|
-
: [];
|
|
136
|
+
const targets = params.all === true
|
|
137
|
+
? candidateIds
|
|
138
|
+
: params.id?.trim() ? matchRunIds(candidateIds, params.id.trim()) : [];
|
|
282
139
|
|
|
283
140
|
if (targets.length === 0) {
|
|
284
141
|
const available = [...runtime.threads.keys()].map((id) => `#${id}`).join(", ");
|
|
285
142
|
return {
|
|
286
|
-
content: [{
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
? "No active subagent runs to stop."
|
|
290
|
-
: `No subagent thread matches "${params.id}".${available ? ` Known threads: ${available}.` : ""}`,
|
|
291
|
-
}],
|
|
143
|
+
content: [{ type: "text", text: params.all === true
|
|
144
|
+
? "No active subagent runs to stop."
|
|
145
|
+
: `No subagent thread matches "${params.id}".${available ? ` Known threads: ${available}.` : ""}` }],
|
|
292
146
|
details: {},
|
|
293
147
|
};
|
|
294
148
|
}
|
|
@@ -299,7 +153,6 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
299
153
|
run: ReturnType<typeof monitor.findRun>;
|
|
300
154
|
previousState: SubagentThread["state"];
|
|
301
155
|
wasQueued: boolean;
|
|
302
|
-
wasResuming: boolean;
|
|
303
156
|
wasActive: boolean;
|
|
304
157
|
generation: number;
|
|
305
158
|
controller: AbortController | undefined;
|
|
@@ -312,39 +165,23 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
312
165
|
if (!thread) continue;
|
|
313
166
|
const previousState = thread.state;
|
|
314
167
|
const wasQueued = previousState === "queued";
|
|
315
|
-
const
|
|
316
|
-
const wasActive =
|
|
317
|
-
thread.lifecycleOperation !== undefined ||
|
|
318
|
-
["queued", "resuming", "running", "interrupting"].includes(previousState);
|
|
168
|
+
const wasActive = thread.lifecycleOperation !== undefined || ["queued", "running", "interrupting"].includes(previousState);
|
|
319
169
|
const stopVersion = ++thread.lifecycleVersion;
|
|
320
|
-
// Stop-all claims every target before the first await. This invalidates
|
|
321
|
-
// all concurrent resume preflights as one synchronous operation.
|
|
322
170
|
thread.lifecycleOperation = "stop";
|
|
323
171
|
thread.retired = true;
|
|
324
172
|
thread.retireOnSettle = true;
|
|
325
173
|
thread.state = "stopped";
|
|
326
174
|
const stopMessage = wasQueued
|
|
327
175
|
? "Stopped by subagent_stop before the run started."
|
|
328
|
-
:
|
|
329
|
-
? "Stopped by subagent_stop
|
|
330
|
-
|
|
331
|
-
? "Stopped by subagent_stop."
|
|
332
|
-
: previousState === "parked"
|
|
333
|
-
? "Stopped by subagent_stop from a parked checkpoint."
|
|
334
|
-
: "Retired by subagent_stop.";
|
|
176
|
+
: wasActive ? "Stopped by subagent_stop."
|
|
177
|
+
: previousState === "parked" ? "Stopped by subagent_stop from an interrupted checkpoint."
|
|
178
|
+
: "Retired by subagent_stop.";
|
|
335
179
|
claimed.push({
|
|
336
|
-
runId,
|
|
337
|
-
thread,
|
|
338
|
-
run: monitor.findRun(runId),
|
|
339
|
-
previousState,
|
|
340
|
-
wasQueued,
|
|
341
|
-
wasResuming,
|
|
342
|
-
wasActive,
|
|
180
|
+
runId, thread, run: monitor.findRun(runId), previousState, wasQueued, wasActive,
|
|
343
181
|
generation: thread.generation,
|
|
344
182
|
controller: thread.queueController,
|
|
345
183
|
completion: thread.generationCompletion,
|
|
346
|
-
stopVersion,
|
|
347
|
-
stopMessage,
|
|
184
|
+
stopVersion, stopMessage,
|
|
348
185
|
});
|
|
349
186
|
}
|
|
350
187
|
|
|
@@ -368,7 +205,6 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
368
205
|
run,
|
|
369
206
|
previousState,
|
|
370
207
|
wasQueued,
|
|
371
|
-
wasResuming,
|
|
372
208
|
wasActive,
|
|
373
209
|
generation,
|
|
374
210
|
controller,
|
|
@@ -392,7 +228,6 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
392
228
|
let stoppedResult: SingleResult | undefined;
|
|
393
229
|
if (
|
|
394
230
|
wasQueued ||
|
|
395
|
-
wasResuming ||
|
|
396
231
|
previousState === "parked" ||
|
|
397
232
|
!runtime.settledRuns.has(runId)
|
|
398
233
|
) {
|
|
@@ -451,8 +286,8 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
451
286
|
error: "subagent_stop timed out waiting for worktree integration; it continues in the background",
|
|
452
287
|
}),
|
|
453
288
|
]).catch(() => undefined);
|
|
289
|
+
pendingIntegration.push(`#${runId}`);
|
|
454
290
|
}
|
|
455
|
-
pendingIntegration.push(`#${runId}`);
|
|
456
291
|
} else if (finalization.status === "retained") {
|
|
457
292
|
retainedIntegration.push(`#${runId}`);
|
|
458
293
|
}
|