@ferris1225/pi-subagents 4.3.8 → 4.3.10
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 +41 -0
- package/README.md +165 -117
- package/index.ts +2 -0
- package/package.json +12 -10
- package/src/configuration/setup.ts +16 -14
- package/src/delegation/agents.ts +2 -1
- package/src/delegation/dispatch.ts +189 -38
- package/src/delegation/phase-scope.ts +178 -0
- package/src/delegation/prompt.ts +46 -36
- package/src/delegation/risk.ts +168 -0
- 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 +24 -8
- package/src/lifecycle/runtime.ts +27 -63
- package/src/lifecycle/thread-lifecycle.ts +67 -414
- package/src/lifecycle/thread-restore.ts +16 -35
- package/src/lifecycle/thread-shared.ts +9 -63
- package/src/lifecycle/tools.ts +86 -241
- 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);
|
|
@@ -58,6 +56,9 @@ function createRestoredThread(
|
|
|
58
56
|
generation: record.generation,
|
|
59
57
|
agentName: record.agentName,
|
|
60
58
|
task: record.task,
|
|
59
|
+
phaseId: record.phaseId,
|
|
60
|
+
scope: record.scope,
|
|
61
|
+
writeCapable: record.writeCapable ?? record.agentName !== "scout",
|
|
61
62
|
cwd: record.cwd,
|
|
62
63
|
executionCwd: record.executionCwd,
|
|
63
64
|
...(record.thinkingLevel ? { thinkingLevel: record.thinkingLevel as ThinkingLevel } : {}),
|
|
@@ -71,31 +72,15 @@ function createRestoredThread(
|
|
|
71
72
|
sessionId: record.sessionId,
|
|
72
73
|
sessionDir: record.sessionDir,
|
|
73
74
|
lastResult: restoredResultFromSummary(record),
|
|
74
|
-
resume: async () => failedStartResult(record.agentName, record.task, "Thread resume was not initialized."),
|
|
75
75
|
finalizeIsolation: async () => undefined,
|
|
76
76
|
};
|
|
77
|
-
installThreadLifecycle(thread, {
|
|
78
|
-
runtime,
|
|
79
|
-
startBackground: (...args) => {
|
|
80
|
-
const dispatcher = runtime.dispatcher;
|
|
81
|
-
if (!dispatcher) {
|
|
82
|
-
return Promise.resolve(failedStartResult(
|
|
83
|
-
record.agentName,
|
|
84
|
-
record.task,
|
|
85
|
-
`Run #${record.runId} cannot continue: no dispatch context is available yet. Dispatch any subagent once, then retry.`,
|
|
86
|
-
));
|
|
87
|
-
}
|
|
88
|
-
return dispatcher(...args);
|
|
89
|
-
},
|
|
90
|
-
});
|
|
77
|
+
installThreadLifecycle(thread, { runtime });
|
|
91
78
|
return thread;
|
|
92
79
|
}
|
|
93
80
|
|
|
94
|
-
/** Rebuild interrupted
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* left by older versions, which hold no work worth resuming — drop out with
|
|
98
|
-
* 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. */
|
|
99
84
|
export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<number[]> {
|
|
100
85
|
const records = await readThreadRecords(runtime.configPath);
|
|
101
86
|
const restoredIds: number[] = [];
|
|
@@ -118,22 +103,18 @@ export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<n
|
|
|
118
103
|
record.sessionId !== undefined &&
|
|
119
104
|
record.sessionDir !== undefined &&
|
|
120
105
|
sessionExists(record.sessionDir, record.sessionId);
|
|
121
|
-
|
|
122
|
-
await discardRestoredRecord(runtime, record);
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
106
|
+
const restoredRecord = sessionValid ? record : { ...record, sessionId: undefined, sessionDir: undefined };
|
|
125
107
|
const worktree = record.worktree
|
|
126
108
|
? await restoreWorktreeIsolation(record.worktree).catch(() => undefined)
|
|
127
109
|
: undefined;
|
|
128
110
|
const restorationFailed = record.isolation === "worktree" && record.worktree !== undefined && !worktree;
|
|
129
|
-
const thread = createRestoredThread(runtime,
|
|
111
|
+
const thread = createRestoredThread(runtime, restoredRecord, worktree, restorationFailed ? "failed" : "parked");
|
|
130
112
|
runtime.threads.set(record.runId, thread);
|
|
131
|
-
runtime.sessionDirs.add(
|
|
113
|
+
if (thread.sessionDir) runtime.sessionDirs.add(thread.sessionDir);
|
|
132
114
|
if (restorationFailed) {
|
|
133
|
-
const reason = `Run #${record.runId}'s recorded worktree could not be restored; isolated edits may be unavailable. The
|
|
134
|
-
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.`;
|
|
135
116
|
thread.restorationRecord = record;
|
|
136
|
-
const previous =
|
|
117
|
+
const previous = thread.lastResult;
|
|
137
118
|
const failed: SingleResult = {
|
|
138
119
|
agent: record.agentName,
|
|
139
120
|
task: record.task,
|
|
@@ -146,8 +127,8 @@ export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<n
|
|
|
146
127
|
stopReason: "error",
|
|
147
128
|
errorMessage: reason,
|
|
148
129
|
dispatchFailed: true,
|
|
149
|
-
sessionId:
|
|
150
|
-
sessionDir:
|
|
130
|
+
sessionId: thread.sessionId,
|
|
131
|
+
sessionDir: thread.sessionDir,
|
|
151
132
|
projectCwd: record.cwd,
|
|
152
133
|
runId: record.runId,
|
|
153
134
|
isolation: "worktree",
|
|
@@ -225,7 +206,7 @@ export function bootstrapDurableState(runtime: SubagentRuntime): Promise<void> {
|
|
|
225
206
|
}
|
|
226
207
|
try {
|
|
227
208
|
// Sessions and worktrees outlive their process on purpose, so only
|
|
228
|
-
// ownership separates
|
|
209
|
+
// ownership separates a live Pi's state from artifacts a crash
|
|
229
210
|
// abandoned. Valid thread and recovery records always keep their paths.
|
|
230
211
|
const records = await readThreadRecords(runtime.configPath);
|
|
231
212
|
const recoveryRecords = await readRecoveryRecords(runtime.configPath);
|
|
@@ -4,6 +4,7 @@ import { realpath } from "node:fs/promises";
|
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { isWriteCapableAgent, type AgentConfig } from "../delegation/agents.ts";
|
|
7
|
+
import type { PhaseScope } from "../delegation/phase-scope.ts";
|
|
7
8
|
import { roleThinkingLevel, type SubagentsConfig, type ThinkingLevel } from "../configuration/config.ts";
|
|
8
9
|
import { removeThreadRecord, threadRecordFromThread, upsertThreadRecord } from "./durable.ts";
|
|
9
10
|
import {
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
} from "../configuration/models.ts";
|
|
17
18
|
import type { SubagentRuntime, SubagentThread } from "./runtime.ts";
|
|
18
19
|
import { getProjectRoot, type SingleResult } from "../execution/spawn.ts";
|
|
19
|
-
import { resolveRepositoryRoot, type IsolationMode
|
|
20
|
+
import { resolveRepositoryRoot, type IsolationMode } from "../isolation/worktree.ts";
|
|
20
21
|
|
|
21
22
|
/** Control operations must never wait forever on a settling generation: the
|
|
22
23
|
* queue task can legitimately spend minutes in worktree finalization (bounded
|
|
@@ -113,42 +114,8 @@ export async function runInManagedRepositoryLane<T>(
|
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
/**
|
|
117
|
-
*
|
|
118
|
-
export function beginRuntimePreflight(runtime: SubagentRuntime): () => void {
|
|
119
|
-
let resolvePreflight!: () => void;
|
|
120
|
-
const preflight = new Promise<void>((resolve) => {
|
|
121
|
-
resolvePreflight = resolve;
|
|
122
|
-
});
|
|
123
|
-
runtime.preflightOperations.add(preflight);
|
|
124
|
-
return () => {
|
|
125
|
-
runtime.preflightOperations.delete(preflight);
|
|
126
|
-
resolvePreflight();
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Synchronous CAS used by lifecycle controls across their async preflight. */
|
|
131
|
-
export function ownsResumeReservation(
|
|
132
|
-
runtime: SubagentRuntime,
|
|
133
|
-
thread: SubagentThread,
|
|
134
|
-
reservation: { version: number; generation: number; sessionId?: string; sessionDir?: string },
|
|
135
|
-
): boolean {
|
|
136
|
-
return (
|
|
137
|
-
runtime.sessionActive &&
|
|
138
|
-
runtime.threads.get(thread.id) === thread &&
|
|
139
|
-
!thread.retired &&
|
|
140
|
-
thread.lifecycleOperation === "resume" &&
|
|
141
|
-
thread.lifecycleVersion === reservation.version &&
|
|
142
|
-
thread.generation === reservation.generation &&
|
|
143
|
-
thread.sessionId === reservation.sessionId &&
|
|
144
|
-
thread.sessionDir === reservation.sessionDir
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** Fire-and-forget durable checkpoint. Parked threads stay resumable across
|
|
149
|
-
* reloads; a settled thread drops its record so the manifest only exists
|
|
150
|
-
* while unfinished work needs it. The live session keeps working when the
|
|
151
|
-
* 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. */
|
|
152
119
|
export function persistThreadCheckpoint(
|
|
153
120
|
runtime: SubagentRuntime,
|
|
154
121
|
thread: SubagentThread,
|
|
@@ -182,28 +149,12 @@ export interface DispatchEnvironment {
|
|
|
182
149
|
agents: AgentConfig[];
|
|
183
150
|
}
|
|
184
151
|
|
|
185
|
-
|
|
186
|
-
sessionId?: string;
|
|
187
|
-
sessionDir?: string;
|
|
188
|
-
worktree?: WorktreeIsolation;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export interface ResumeReservation {
|
|
192
|
-
version: number;
|
|
193
|
-
generation: number;
|
|
194
|
-
sessionId?: string;
|
|
195
|
-
sessionDir?: string;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** The dispatcher's full internal entry point; the public tool surface only
|
|
199
|
-
* uses the first four parameters. */
|
|
152
|
+
/** Admission metadata for a fresh one-shot run. */
|
|
200
153
|
export interface StartBackgroundOptions {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
seed?: SessionSeed;
|
|
206
|
-
resumeReservation?: ResumeReservation;
|
|
154
|
+
phaseId?: string;
|
|
155
|
+
scope?: PhaseScope;
|
|
156
|
+
/** A false hint cannot downgrade a live write-capable role. */
|
|
157
|
+
writeCapable?: boolean;
|
|
207
158
|
/** Chosen by the tool call before the queue can start a fast child. */
|
|
208
159
|
deliveryRoute?: "background" | "await";
|
|
209
160
|
}
|
|
@@ -218,12 +169,7 @@ export type StartBackgroundInternal = (
|
|
|
218
169
|
|
|
219
170
|
export interface ThreadLifecycleDeps {
|
|
220
171
|
runtime: SubagentRuntime;
|
|
221
|
-
/** Fallback context when a control caller supplies none; restored threads
|
|
222
|
-
* install without one and rely on the per-call context. */
|
|
223
172
|
runCtx?: ExtensionContext;
|
|
224
|
-
/** Fresh dispatch passes the live dispatcher; restored threads resolve it
|
|
225
|
-
* from the runtime at call time so they never pin a stale closure. */
|
|
226
|
-
startBackground: StartBackgroundInternal;
|
|
227
173
|
}
|
|
228
174
|
|
|
229
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,197 +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 SubagentControlParams = Type.Object({
|
|
38
|
-
action: StringEnum(["steer", "resume", "park"] as const, {
|
|
39
|
-
description:
|
|
40
|
-
"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.",
|
|
41
|
-
}),
|
|
42
|
-
id: Type.Integer({ minimum: 1, description: "Stable run id shown by subagent dispatch output." }),
|
|
43
|
-
objective: Type.Optional(
|
|
44
|
-
Type.String({ description: "Guidance for steer (required and nonblank), or an optional appended objective for resume. Ignored by park." }),
|
|
45
|
-
),
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
/** A thread that a steer can continue instead of reject: it is not live, but
|
|
49
|
-
* its retained session can absorb the guidance as an appended objective. */
|
|
50
|
-
type ContinuableState = "completed" | "failed" | "parked";
|
|
51
|
-
|
|
52
29
|
pi.registerTool({
|
|
53
|
-
name: "
|
|
54
|
-
label: "Subagent
|
|
55
|
-
description: "
|
|
56
|
-
parameters:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// 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) {
|
|
61
37
|
await runtime.durableRestore;
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
/** Steering guidance for a thread that is no longer live continues the
|
|
119
|
-
* same thread with that guidance instead of being dropped, so the
|
|
120
|
-
* evidence is never re-bought by a second dispatch. */
|
|
121
|
-
const continueSteer = async (objective: string) => {
|
|
122
|
-
const continuable = (): ContinuableState | undefined =>
|
|
123
|
-
thread.state === "completed" || thread.state === "failed" || thread.state === "parked"
|
|
124
|
-
? thread.state
|
|
125
|
-
: undefined;
|
|
126
|
-
if (thread.lifecycleOperation === "settle" || (!continuable() && thread.control.getPhase() === "settled")) {
|
|
127
|
-
if (!(await quiesced(thread.generationCompletion))) return undefined;
|
|
128
|
-
}
|
|
129
|
-
const state = continuable();
|
|
130
|
-
if (!state || thread.lifecycleOperation) return undefined;
|
|
131
|
-
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 } : {}),
|
|
132
94
|
};
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
switch (params.action) {
|
|
136
|
-
case "steer": {
|
|
137
|
-
const objective = nonBlank(params.objective);
|
|
138
|
-
if (!objective) {
|
|
139
|
-
return textResult("steer objective must be non-blank.");
|
|
140
|
-
}
|
|
141
|
-
if (thread.retired) {
|
|
142
|
-
return textResult(`Run #${thread.id} was retired by subagent_stop and cannot be steered.`);
|
|
143
|
-
}
|
|
144
|
-
const continued = await continueSteer(objective);
|
|
145
|
-
if (continued) return continued;
|
|
146
|
-
const unavailable = inactiveReason();
|
|
147
|
-
if (unavailable) {
|
|
148
|
-
return textResult(`Run #${thread.id} is ${unavailable}; only an active running RPC attempt can be steered. No guidance was sent.`);
|
|
149
|
-
}
|
|
150
|
-
const steered = await thread.control.steer(objective);
|
|
151
|
-
if (!steered.accepted) {
|
|
152
|
-
const resumed = await continueSteer(objective);
|
|
153
|
-
if (resumed) return resumed;
|
|
154
|
-
if (steered.reason === "no-active-attempt") {
|
|
155
|
-
return textResult(`Run #${thread.id} is marked running but has no active RPC attempt; no guidance was sent.`);
|
|
156
|
-
}
|
|
157
|
-
return textResult(`Run #${thread.id} is ${steered.phase}; only an active running RPC attempt can be steered. No guidance was sent.`);
|
|
158
|
-
}
|
|
159
|
-
return textResult(`Steered run #${thread.id} with additional in-scope guidance; its original objective is unchanged.`);
|
|
160
|
-
}
|
|
161
|
-
case "resume": {
|
|
162
|
-
return resumeThread(params.objective);
|
|
163
|
-
}
|
|
164
|
-
case "park": {
|
|
165
|
-
if (thread.retired) {
|
|
166
|
-
return textResult(`Run #${thread.id} was retired by subagent_stop and cannot be parked.`);
|
|
167
|
-
}
|
|
168
|
-
const unavailable = inactiveReason();
|
|
169
|
-
if (unavailable) {
|
|
170
|
-
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.`);
|
|
171
|
-
}
|
|
172
|
-
if (!thread.sessionId || !thread.sessionDir) {
|
|
173
|
-
return textResult(`Run #${thread.id} has no retained session yet; steer it or let it settle instead.`);
|
|
174
|
-
}
|
|
175
|
-
// Park interrupts the child at its next safe point but keeps the
|
|
176
|
-
// session and worktree, so the thread returns to `parked`, not to a
|
|
177
|
-
// failure. Claim synchronously like stop; the generation body sees
|
|
178
|
-
// the claim and leaves publication to this path.
|
|
179
|
-
const parkVersion = ++thread.lifecycleVersion;
|
|
180
|
-
thread.lifecycleOperation = "park";
|
|
181
|
-
const generation = thread.generation;
|
|
182
|
-
const controller = thread.queueController;
|
|
183
|
-
const completion = thread.generationCompletion;
|
|
184
|
-
const ownsPark = (): boolean =>
|
|
185
|
-
runtime.threads.get(thread.id) === thread &&
|
|
186
|
-
thread.generation === generation &&
|
|
187
|
-
thread.lifecycleVersion === parkVersion &&
|
|
188
|
-
thread.lifecycleOperation === "park" &&
|
|
189
|
-
!thread.retired;
|
|
190
|
-
try {
|
|
191
|
-
await quiesced(thread.control.stop("Parked by subagent_control at a stable checkpoint.").catch(() => undefined));
|
|
192
|
-
if (!(await quiesced(completion))) runtime.backgroundQueue.cancel(controller);
|
|
193
|
-
if (!ownsPark()) {
|
|
194
|
-
return textResult(`Run #${thread.id} changed while it was being parked; no checkpoint was recorded by this call.`);
|
|
195
|
-
}
|
|
196
|
-
if (runtime.runControllers.get(thread.id) === controller) runtime.runControllers.delete(thread.id);
|
|
197
|
-
if (thread.queueController === controller) thread.queueController = undefined;
|
|
198
|
-
thread.state = "parked";
|
|
199
|
-
monitor.setStatus(thread.id, "parked");
|
|
200
|
-
thread.elapsedMs = monitor.getElapsedMs(thread.id) ?? thread.elapsedMs;
|
|
201
|
-
persistThreadCheckpoint(runtime, thread, "parked");
|
|
202
|
-
const run = monitor.findRun(thread.id);
|
|
203
|
-
const usage = run ? formatUsageCompact(run.usage) : "";
|
|
204
|
-
if (runtime.sessionActive) {
|
|
205
|
-
ctx.ui.notify(`■ #${thread.id} ${run ? monitor.summarize(run) : thread.agentName} · parked`, "info");
|
|
206
|
-
}
|
|
207
|
-
const retained = thread.isolation === "worktree" ? "session and worktree" : "session";
|
|
208
|
-
return textResult(
|
|
209
|
-
`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.`,
|
|
210
|
-
);
|
|
211
|
-
} finally {
|
|
212
|
-
if (thread.lifecycleVersion === parkVersion && thread.lifecycleOperation === "park") {
|
|
213
|
-
thread.lifecycleOperation = undefined;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
} catch (error) {
|
|
219
|
-
throw new Error(`Could not ${params.action} run #${thread.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
220
|
-
}
|
|
221
95
|
},
|
|
222
|
-
|
|
223
96
|
renderCall(args, theme) {
|
|
224
|
-
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);
|
|
225
98
|
},
|
|
226
|
-
renderResult(result,
|
|
227
|
-
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);
|
|
228
102
|
},
|
|
229
103
|
});
|
|
230
104
|
|
|
@@ -248,37 +122,27 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
248
122
|
|
|
249
123
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
250
124
|
await runtime.durableRestore;
|
|
251
|
-
//
|
|
252
|
-
// synchronously before a resume preflight can cross its next await.
|
|
125
|
+
// Claim every target before awaiting process cleanup or configuration I/O.
|
|
253
126
|
const configPromise = loadConfig(runtime.configPath).catch(() => undefined);
|
|
254
127
|
const completionResults: SingleResult[] = [];
|
|
255
128
|
const candidateIds = params.all === true
|
|
256
129
|
? [...new Set([
|
|
257
130
|
...runtime.runControllers.keys(),
|
|
258
131
|
...[...runtime.threads.values()]
|
|
259
|
-
.filter((thread) =>
|
|
260
|
-
thread.lifecycleOperation !== undefined ||
|
|
261
|
-
["queued", "resuming", "running", "interrupting"].includes(thread.state),
|
|
262
|
-
)
|
|
132
|
+
.filter((thread) => thread.lifecycleOperation !== undefined || ["queued", "running", "interrupting"].includes(thread.state))
|
|
263
133
|
.map((thread) => thread.id),
|
|
264
134
|
])]
|
|
265
135
|
: [...runtime.threads.keys()];
|
|
266
|
-
const targets =
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
: params.id !== undefined && params.id.trim() !== ""
|
|
270
|
-
? matchRunIds(candidateIds, params.id.trim())
|
|
271
|
-
: [];
|
|
136
|
+
const targets = params.all === true
|
|
137
|
+
? candidateIds
|
|
138
|
+
: params.id?.trim() ? matchRunIds(candidateIds, params.id.trim()) : [];
|
|
272
139
|
|
|
273
140
|
if (targets.length === 0) {
|
|
274
141
|
const available = [...runtime.threads.keys()].map((id) => `#${id}`).join(", ");
|
|
275
142
|
return {
|
|
276
|
-
content: [{
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
? "No active subagent runs to stop."
|
|
280
|
-
: `No subagent thread matches "${params.id}".${available ? ` Known threads: ${available}.` : ""}`,
|
|
281
|
-
}],
|
|
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}.` : ""}` }],
|
|
282
146
|
details: {},
|
|
283
147
|
};
|
|
284
148
|
}
|
|
@@ -289,7 +153,6 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
289
153
|
run: ReturnType<typeof monitor.findRun>;
|
|
290
154
|
previousState: SubagentThread["state"];
|
|
291
155
|
wasQueued: boolean;
|
|
292
|
-
wasResuming: boolean;
|
|
293
156
|
wasActive: boolean;
|
|
294
157
|
generation: number;
|
|
295
158
|
controller: AbortController | undefined;
|
|
@@ -302,39 +165,23 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
302
165
|
if (!thread) continue;
|
|
303
166
|
const previousState = thread.state;
|
|
304
167
|
const wasQueued = previousState === "queued";
|
|
305
|
-
const
|
|
306
|
-
const wasActive =
|
|
307
|
-
thread.lifecycleOperation !== undefined ||
|
|
308
|
-
["queued", "resuming", "running", "interrupting"].includes(previousState);
|
|
168
|
+
const wasActive = thread.lifecycleOperation !== undefined || ["queued", "running", "interrupting"].includes(previousState);
|
|
309
169
|
const stopVersion = ++thread.lifecycleVersion;
|
|
310
|
-
// Stop-all claims every target before the first await. This invalidates
|
|
311
|
-
// all concurrent resume preflights as one synchronous operation.
|
|
312
170
|
thread.lifecycleOperation = "stop";
|
|
313
171
|
thread.retired = true;
|
|
314
172
|
thread.retireOnSettle = true;
|
|
315
173
|
thread.state = "stopped";
|
|
316
174
|
const stopMessage = wasQueued
|
|
317
175
|
? "Stopped by subagent_stop before the run started."
|
|
318
|
-
:
|
|
319
|
-
? "Stopped by subagent_stop
|
|
320
|
-
|
|
321
|
-
? "Stopped by subagent_stop."
|
|
322
|
-
: previousState === "parked"
|
|
323
|
-
? "Stopped by subagent_stop from a parked checkpoint."
|
|
324
|
-
: "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.";
|
|
325
179
|
claimed.push({
|
|
326
|
-
runId,
|
|
327
|
-
thread,
|
|
328
|
-
run: monitor.findRun(runId),
|
|
329
|
-
previousState,
|
|
330
|
-
wasQueued,
|
|
331
|
-
wasResuming,
|
|
332
|
-
wasActive,
|
|
180
|
+
runId, thread, run: monitor.findRun(runId), previousState, wasQueued, wasActive,
|
|
333
181
|
generation: thread.generation,
|
|
334
182
|
controller: thread.queueController,
|
|
335
183
|
completion: thread.generationCompletion,
|
|
336
|
-
stopVersion,
|
|
337
|
-
stopMessage,
|
|
184
|
+
stopVersion, stopMessage,
|
|
338
185
|
});
|
|
339
186
|
}
|
|
340
187
|
|
|
@@ -358,7 +205,6 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
358
205
|
run,
|
|
359
206
|
previousState,
|
|
360
207
|
wasQueued,
|
|
361
|
-
wasResuming,
|
|
362
208
|
wasActive,
|
|
363
209
|
generation,
|
|
364
210
|
controller,
|
|
@@ -382,7 +228,6 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
382
228
|
let stoppedResult: SingleResult | undefined;
|
|
383
229
|
if (
|
|
384
230
|
wasQueued ||
|
|
385
|
-
wasResuming ||
|
|
386
231
|
previousState === "parked" ||
|
|
387
232
|
!runtime.settledRuns.has(runId)
|
|
388
233
|
) {
|
|
@@ -441,8 +286,8 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
441
286
|
error: "subagent_stop timed out waiting for worktree integration; it continues in the background",
|
|
442
287
|
}),
|
|
443
288
|
]).catch(() => undefined);
|
|
289
|
+
pendingIntegration.push(`#${runId}`);
|
|
444
290
|
}
|
|
445
|
-
pendingIntegration.push(`#${runId}`);
|
|
446
291
|
} else if (finalization.status === "retained") {
|
|
447
292
|
retainedIntegration.push(`#${runId}`);
|
|
448
293
|
}
|
|
@@ -46,7 +46,7 @@ export function registerAnnouncements(pi: ExtensionAPI, runtime: SubagentRuntime
|
|
|
46
46
|
runtime.restoredNotified = true;
|
|
47
47
|
const ids = runtime.restoredRunIds.map((id) => `#${id}`).join(", ");
|
|
48
48
|
ctx.ui.notify(
|
|
49
|
-
`pi-subagents: restored ${runtime.restoredRunIds.length} interrupted
|
|
49
|
+
`pi-subagents: restored ${runtime.restoredRunIds.length} interrupted run${runtime.restoredRunIds.length === 1 ? "" : "s"} (${ids}) for manual recovery. Inspect with subagent_status; main finishes the work.`,
|
|
50
50
|
"info",
|
|
51
51
|
);
|
|
52
52
|
}
|