@ferris1225/pi-subagents 4.2.7 → 4.2.12
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/LICENSE +23 -23
- package/README.md +48 -3
- package/agents/executor.md +4 -3
- package/agents/explorer.md +37 -37
- package/package.json +9 -1
- package/src/agents.ts +237 -237
- package/src/announcements.ts +81 -78
- package/src/background.ts +205 -205
- package/src/completion.ts +165 -165
- package/src/config.ts +308 -308
- package/src/dispatch.ts +83 -14
- package/src/format.ts +183 -165
- package/src/index.ts +102 -100
- package/src/models.ts +203 -203
- package/src/monitor.ts +3 -2
- package/src/prompt.ts +2 -1
- package/src/recovery.ts +163 -163
- package/src/runtime.ts +33 -0
- package/src/session-fork.ts +86 -86
- package/src/setup.ts +341 -341
- package/src/spawn.ts +663 -658
- package/src/status.ts +67 -0
- package/src/temp-hygiene.ts +230 -230
- package/src/tools.ts +1 -1
- package/src/ui.ts +248 -248
- package/src/widget.ts +268 -266
- package/src/worktree.ts +974 -943
package/src/dispatch.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* completion ownership live in thread-lifecycle.ts.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
9
|
+
import { StringEnum, type Usage } from "@earendil-works/pi-ai";
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { Text } from "@earendil-works/pi-tui";
|
|
12
12
|
import { join, resolve } from "node:path";
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
formatToolActivity,
|
|
20
20
|
monitor,
|
|
21
21
|
statusIcon,
|
|
22
|
+
statusLabel,
|
|
23
|
+
sumUsage,
|
|
22
24
|
type RunView,
|
|
23
25
|
type RunWaitReason,
|
|
24
26
|
} from "./monitor.ts";
|
|
@@ -32,6 +34,7 @@ import {
|
|
|
32
34
|
type SingleResult,
|
|
33
35
|
type SubagentDetails,
|
|
34
36
|
type SubagentLiveEvent,
|
|
37
|
+
type UsageStats,
|
|
35
38
|
} from "./spawn.ts";
|
|
36
39
|
import {
|
|
37
40
|
createBackgroundDispatcher,
|
|
@@ -116,6 +119,31 @@ export function defaultIsolationMode(
|
|
|
116
119
|
return mode === "parallel" && writeCapable ? "worktree" : "shared";
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
/** Map the child's own usage tally onto pi's tool-result `Usage`, so sub-agent
|
|
123
|
+
* token spend lands in the parent's footer, /session, and RPC session totals
|
|
124
|
+
* instead of being invisible. Only the total cost is known here: a child
|
|
125
|
+
* reports one cost number, not a per-bucket split. */
|
|
126
|
+
function toToolUsage(stats: UsageStats): Usage {
|
|
127
|
+
return {
|
|
128
|
+
input: stats.input,
|
|
129
|
+
output: stats.output,
|
|
130
|
+
cacheRead: stats.cacheRead,
|
|
131
|
+
cacheWrite: stats.cacheWrite,
|
|
132
|
+
totalTokens: stats.input + stats.output + stats.cacheRead + stats.cacheWrite,
|
|
133
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: stats.cost },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Usage of the runs awaited in-turn. Omitted entirely in the background path:
|
|
138
|
+
* those children have not finished when the tool returns, so any number there
|
|
139
|
+
* would be a fabrication. */
|
|
140
|
+
function toolUsage(runtime: SubagentRuntime, runIds: number[]): { usage?: Usage } {
|
|
141
|
+
const parts = runIds
|
|
142
|
+
.map((id) => runtime.settledRuns.get(id)?.usage)
|
|
143
|
+
.filter((usage): usage is UsageStats => usage !== undefined);
|
|
144
|
+
return parts.length > 0 ? { usage: toToolUsage(sumUsage(parts)) } : {};
|
|
145
|
+
}
|
|
146
|
+
|
|
119
147
|
/** In-turn wait behind dispatch `wait: true` — the escape hatch for one-shot
|
|
120
148
|
* `pi -p` parents that exit at end of turn: hold the call until every run it
|
|
121
149
|
* started settles, then hand back their result blocks. Interactive sessions
|
|
@@ -130,6 +158,7 @@ export async function awaitRunResults(
|
|
|
130
158
|
signal: AbortSignal | undefined,
|
|
131
159
|
maxResultLines: number,
|
|
132
160
|
fallbackCwd: string,
|
|
161
|
+
onProgress?: (text: string) => void,
|
|
133
162
|
): Promise<string> {
|
|
134
163
|
const waitForRun = (runId: number): Promise<{ result?: SingleResult; note?: string }> => {
|
|
135
164
|
const already = runtime.settledRuns.get(runId);
|
|
@@ -189,12 +218,38 @@ export async function awaitRunResults(
|
|
|
189
218
|
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
190
219
|
});
|
|
191
220
|
};
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
221
|
+
// One shared subscription drives the progress line: each waiter already
|
|
222
|
+
// subscribes for its own settlement, and the tool card wants a single
|
|
223
|
+
// rolled-up line rather than one per run.
|
|
224
|
+
let lastProgress: string | undefined;
|
|
225
|
+
const emitProgress = onProgress
|
|
226
|
+
? (): void => {
|
|
227
|
+
const parts = runIds.map((id) => {
|
|
228
|
+
const settled = runtime.settledRuns.get(id);
|
|
229
|
+
if (settled) return `#${id} ${isFailedResult(settled) ? "failed" : "done"}`;
|
|
230
|
+
const live = monitor.findRun(id);
|
|
231
|
+
return live ? `#${id} ${statusLabel(live.status)}` : `#${id} …`;
|
|
232
|
+
});
|
|
233
|
+
const text = `Waiting in-turn on ${runIds.length} run${runIds.length === 1 ? "" : "s"} · ${parts.join(", ")}`;
|
|
234
|
+
// The monitor notifies on every usage and activity change; this line
|
|
235
|
+
// names only statuses, so most notifications leave it identical.
|
|
236
|
+
if (text === lastProgress) return;
|
|
237
|
+
lastProgress = text;
|
|
238
|
+
onProgress(text);
|
|
239
|
+
}
|
|
240
|
+
: undefined;
|
|
241
|
+
const progressUnsub = emitProgress ? monitor.subscribe(emitProgress) : undefined;
|
|
242
|
+
emitProgress?.();
|
|
243
|
+
try {
|
|
244
|
+
const outcomes = await Promise.all(runIds.map(waitForRun));
|
|
245
|
+
return outcomes.map((outcome) =>
|
|
246
|
+
outcome.result
|
|
247
|
+
? formatCompletionBlock(outcome.result, maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, outcome.result.projectCwd ?? fallbackCwd) })
|
|
248
|
+
: (outcome.note ?? "(no outcome)"),
|
|
249
|
+
).join("\n\n");
|
|
250
|
+
} finally {
|
|
251
|
+
progressUnsub?.();
|
|
252
|
+
}
|
|
198
253
|
}
|
|
199
254
|
|
|
200
255
|
export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
@@ -203,16 +258,22 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
203
258
|
// refreshes the fallback context, config, and agent catalog it resolves.
|
|
204
259
|
const environmentRef: { current: DispatchEnvironment | undefined } = { current: undefined };
|
|
205
260
|
|
|
206
|
-
//
|
|
207
|
-
//
|
|
261
|
+
// Terminal rows stay in the monitor until the next beginTurn so the footer
|
|
262
|
+
// can count them beside siblings that are still live. The widget ignores
|
|
263
|
+
// them. A second finishRun for the same endedAt is a no-op; a resume
|
|
264
|
+
// clears endedAt, so the next settlement notifies again.
|
|
265
|
+
const publishedEndedAt = new Map<number, number>();
|
|
208
266
|
const finishRun = (
|
|
209
267
|
runId: number,
|
|
210
268
|
status: "done" | "failed",
|
|
211
269
|
opts?: { silent?: boolean },
|
|
212
270
|
): void => {
|
|
271
|
+
const run = monitor.findRun(runId);
|
|
272
|
+
if (!run) return;
|
|
213
273
|
monitor.setStatus(runId, status); // stamps endedAt for the elapsed time
|
|
214
|
-
const
|
|
215
|
-
if (
|
|
274
|
+
const endedAt = monitor.findRun(runId)?.endedAt;
|
|
275
|
+
if (endedAt !== undefined && publishedEndedAt.get(runId) === endedAt) return;
|
|
276
|
+
if (endedAt !== undefined) publishedEndedAt.set(runId, endedAt);
|
|
216
277
|
if (opts?.silent || !runtime.sessionActive) return;
|
|
217
278
|
const icon = status === "done" ? "✓" : "✗";
|
|
218
279
|
environmentRef.current?.ctx.ui.notify(`${icon} #${run.id} ${monitor.summarize(run)}`, status === "done" ? "info" : "error");
|
|
@@ -332,7 +393,13 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
332
393
|
"Dispatch isolated background agents for recon, implementation, cleanup, docs sync, or result merging; never blocks your turn, and completions wake you automatically.",
|
|
333
394
|
parameters: SubagentParams,
|
|
334
395
|
|
|
335
|
-
async execute(_toolCallId, params, signal,
|
|
396
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
397
|
+
// `wait: true` holds this call for minutes and would otherwise show a
|
|
398
|
+
// blank card; the background path returns at once and has nothing to
|
|
399
|
+
// stream. Frames carry the final details shape because renderResult
|
|
400
|
+
// falls back to "(no output)" without it.
|
|
401
|
+
const makeProgress = (details: SubagentDetails): ((text: string) => void) | undefined =>
|
|
402
|
+
onUpdate ? (text: string): void => onUpdate({ content: [{ type: "text", text }], details }) : undefined;
|
|
336
403
|
// Run ids are allocated below; restore raises the allocator above every
|
|
337
404
|
// id a durable record still owns, so a dispatch racing it could hand a
|
|
338
405
|
// fresh run the id of a parked thread and overwrite its record.
|
|
@@ -438,7 +505,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
438
505
|
const startedIds = startedRuns
|
|
439
506
|
.map((result) => result.runId)
|
|
440
507
|
.filter((id): id is number => id !== undefined);
|
|
441
|
-
const blocks = await awaitRunResults(runtime, startedIds, signal, config.maxResultLines, ctx.cwd);
|
|
508
|
+
const blocks = await awaitRunResults(runtime, startedIds, signal, config.maxResultLines, ctx.cwd, makeProgress(makeDetails("parallel", true)(results)));
|
|
442
509
|
const text = [
|
|
443
510
|
`Started ${started} subagent${started === 1 ? "" : "s"} (${startedRefs.join(", ")}) and waited in-turn.`,
|
|
444
511
|
...(failureLines.length > 0
|
|
@@ -450,6 +517,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
450
517
|
return {
|
|
451
518
|
content: [{ type: "text", text }],
|
|
452
519
|
details: makeDetails("parallel", true)(results),
|
|
520
|
+
...toolUsage(runtime, startedIds),
|
|
453
521
|
};
|
|
454
522
|
}
|
|
455
523
|
const text = [
|
|
@@ -483,10 +551,11 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
483
551
|
}
|
|
484
552
|
const runRef = result.runId === undefined ? result.agent : `#${result.runId} ${result.agent}`;
|
|
485
553
|
if (params.wait && result.runId !== undefined) {
|
|
486
|
-
const blocks = await awaitRunResults(runtime, [result.runId], signal, config.maxResultLines, ctx.cwd);
|
|
554
|
+
const blocks = await awaitRunResults(runtime, [result.runId], signal, config.maxResultLines, ctx.cwd, makeProgress(makeDetails("single", true)([result])));
|
|
487
555
|
return {
|
|
488
556
|
content: [{ type: "text", text: `Started ${runRef} and waited in-turn.\n\n${blocks}` }],
|
|
489
557
|
details: makeDetails("single", true)([result]),
|
|
558
|
+
...toolUsage(runtime, [result.runId]),
|
|
490
559
|
};
|
|
491
560
|
}
|
|
492
561
|
return {
|
package/src/format.ts
CHANGED
|
@@ -1,165 +1,183 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure formatting/result helpers shared by the subagent tool and completion
|
|
3
|
-
* delivery: usage rendering, completion blocks, synthetic result constructors,
|
|
4
|
-
* and run-id matching.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { AgentConfig } from "./agents.ts";
|
|
8
|
-
import {
|
|
9
|
-
import { emptyUsage } from "./rpc-run.ts";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
type
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (count >=
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (usage.
|
|
69
|
-
if (usage.
|
|
70
|
-
if (usage.
|
|
71
|
-
if (usage.
|
|
72
|
-
if (usage.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
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
|
-
);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
? `
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Pure formatting/result helpers shared by the subagent tool and completion
|
|
3
|
+
* delivery: usage rendering, completion blocks, synthetic result constructors,
|
|
4
|
+
* and run-id matching.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AgentConfig } from "./agents.ts";
|
|
8
|
+
import { runLabel, shrinkRunLabel } from "./monitor.ts";
|
|
9
|
+
import { emptyUsage } from "./rpc-run.ts";
|
|
10
|
+
import {
|
|
11
|
+
RESULT_LINE_MAX,
|
|
12
|
+
getResultOutput,
|
|
13
|
+
isFailedResult,
|
|
14
|
+
truncateResultOutput,
|
|
15
|
+
writeResultArtifact,
|
|
16
|
+
type SingleResult,
|
|
17
|
+
type UsageStats,
|
|
18
|
+
} from "./spawn.ts";
|
|
19
|
+
|
|
20
|
+
export function queuedResult(agent: AgentConfig, task: string, thinking?: string): SingleResult {
|
|
21
|
+
return {
|
|
22
|
+
agent: agent.name,
|
|
23
|
+
task,
|
|
24
|
+
exitCode: -1,
|
|
25
|
+
messages: [],
|
|
26
|
+
stderr: "",
|
|
27
|
+
usage: emptyUsage(),
|
|
28
|
+
model: agent.model,
|
|
29
|
+
...(thinking ? { thinking } : {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function failedStartResult(agentName: string, task: string, errorMessage: string): SingleResult {
|
|
34
|
+
return {
|
|
35
|
+
agent: agentName,
|
|
36
|
+
task,
|
|
37
|
+
exitCode: 1,
|
|
38
|
+
messages: [],
|
|
39
|
+
stderr: errorMessage,
|
|
40
|
+
usage: emptyUsage(),
|
|
41
|
+
errorMessage,
|
|
42
|
+
dispatchFailed: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Failed result for a background task that crashed with an exception (spawn
|
|
47
|
+
* infra, delivery API, ...) instead of returning a normal result. */
|
|
48
|
+
export function dispatchFailedResult(agent: AgentConfig, task: string, error: unknown, thinking?: string): SingleResult {
|
|
49
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
50
|
+
return {
|
|
51
|
+
...queuedResult(agent, task, thinking),
|
|
52
|
+
exitCode: 1,
|
|
53
|
+
stderr: errorMessage,
|
|
54
|
+
stopReason: "error",
|
|
55
|
+
errorMessage,
|
|
56
|
+
dispatchFailed: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function formatTokens(count: number): string {
|
|
61
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
62
|
+
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
63
|
+
return String(count);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function formatUsage(usage: UsageStats): string {
|
|
67
|
+
const parts: string[] = [];
|
|
68
|
+
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
69
|
+
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
70
|
+
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
71
|
+
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
72
|
+
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
73
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
74
|
+
return parts.join(" ");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface CompletionFormatOptions {
|
|
78
|
+
/** Project-scoped directory the full result is written to when the output
|
|
79
|
+
* is truncated: <projectRoot>/results. */
|
|
80
|
+
resultRoot?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Display width of the task-derived label folded into the completion heading:
|
|
84
|
+
* tight enough to stay a hint rather than a second copy of the task. */
|
|
85
|
+
const COMPLETION_LABEL_MAX = 24;
|
|
86
|
+
|
|
87
|
+
export function formatCompletionBlock(
|
|
88
|
+
result: SingleResult,
|
|
89
|
+
maxResultLines: number,
|
|
90
|
+
options: CompletionFormatOptions = {},
|
|
91
|
+
): string {
|
|
92
|
+
const failed = isFailedResult(result);
|
|
93
|
+
const failedTools = result.failedTools ?? [];
|
|
94
|
+
const status = failed ? "failed" : "completed";
|
|
95
|
+
const usage = formatUsage(result.usage);
|
|
96
|
+
const output = getResultOutput(result);
|
|
97
|
+
const { text, truncated, shownLines, totalLines, widthClipped } = truncateResultOutput(output, maxResultLines);
|
|
98
|
+
const fallbackNote = result.modelFallbackFrom
|
|
99
|
+
? ` (selected model ${result.modelFallbackFrom} failed → main ${result.model ?? "dynamic default"})`
|
|
100
|
+
: "";
|
|
101
|
+
const startupRetryNote = result.startupRetries
|
|
102
|
+
? ` (recovered after ${result.startupRetries} startup retr${result.startupRetries === 1 ? "y" : "ies"} — concurrent pi startup race)`
|
|
103
|
+
: "";
|
|
104
|
+
const runNote = result.runId !== undefined ? ` · run #${result.runId}` : "";
|
|
105
|
+
// A short task-derived label, not the task itself: the parent authored the
|
|
106
|
+
// task and still has it in context, but a wide fan-out of same-agent runs
|
|
107
|
+
// needs more than a run id to tell completions apart.
|
|
108
|
+
const label = shrinkRunLabel(runLabel(result.task), COMPLETION_LABEL_MAX);
|
|
109
|
+
const lines = [
|
|
110
|
+
`### [${result.agent}${label ? `·${label}` : ""}] ${status}${usage ? ` (${usage})` : ""}${fallbackNote}${startupRetryNote}${runNote}`,
|
|
111
|
+
"",
|
|
112
|
+
];
|
|
113
|
+
if (result.isolation === "worktree") {
|
|
114
|
+
const isolation =
|
|
115
|
+
result.integrationStatus === "integrated"
|
|
116
|
+
? "worktree · changes integrated into the original working tree"
|
|
117
|
+
: result.integrationStatus === "no_changes"
|
|
118
|
+
? "worktree · no changes; temporary worktree removed"
|
|
119
|
+
: result.integrationStatus === "retained"
|
|
120
|
+
? result.integrationApplied
|
|
121
|
+
? "worktree · changes applied, but cleanup failed; recovery artifacts retained"
|
|
122
|
+
: "worktree · integration failed; recovery artifacts retained"
|
|
123
|
+
: "worktree · isolated";
|
|
124
|
+
lines.push(`Isolation: ${isolation}`);
|
|
125
|
+
if (result.integrationWorktreePath) lines.push(`Retained worktree: ${result.integrationWorktreePath}`);
|
|
126
|
+
if (result.integrationPatchPath) lines.push(`Retained patch: ${result.integrationPatchPath}`);
|
|
127
|
+
if (result.integrationError) lines.push(`Integration error: ${result.integrationError}`);
|
|
128
|
+
lines.push("");
|
|
129
|
+
}
|
|
130
|
+
lines.push(text);
|
|
131
|
+
// Failed-tool diagnostics ride along only when the run itself failed: they
|
|
132
|
+
// explain the failure, while on a successful run a transient failed call
|
|
133
|
+
// (no-match grep, rejected edit) is noise the agent already worked around.
|
|
134
|
+
if (failed && failedTools.length > 0) {
|
|
135
|
+
lines.push(
|
|
136
|
+
"",
|
|
137
|
+
`⚠ ${failedTools.length} failed tool call${failedTools.length === 1 ? "" : "s"}:`,
|
|
138
|
+
...failedTools.map((tool) => `- ${tool.toolName}: ${tool.error.trim() || "(no output)"}`),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (truncated) {
|
|
142
|
+
// The full text lives on disk so the main agent can read it on demand.
|
|
143
|
+
const artifact = options.resultRoot
|
|
144
|
+
? writeResultArtifact(output, result.agent, options.resultRoot)
|
|
145
|
+
: "(result root unavailable)";
|
|
146
|
+
// State the real loss and condition the read: handing the parent both a
|
|
147
|
+
// summary and a full-text entrance invites the same content twice.
|
|
148
|
+
const lineLoss = shownLines < totalLines ? `${shownLines} of ${totalLines} lines shown` : `${shownLines} line${shownLines === 1 ? "" : "s"} shown`;
|
|
149
|
+
const widthLoss = widthClipped ? `clipped to ${RESULT_LINE_MAX} characters` : undefined;
|
|
150
|
+
const loss = widthLoss ? `${lineLoss}, ${widthLoss}` : lineLoss;
|
|
151
|
+
lines.push("", `(${loss}; full result ${artifact} — read only if these are insufficient)`);
|
|
152
|
+
}
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Instruction appended to a model-level failure: the sub-agent's provider never
|
|
157
|
+
* produced usable output (or the run stalled), so the task is handed back to the
|
|
158
|
+
* main window instead of being left as a dead failure. When the run preserved a
|
|
159
|
+
* session with earlier work (and the run id is known), steer the main agent to
|
|
160
|
+
* RESUME it in-context once a model is available, instead of re-dispatching
|
|
161
|
+
* fresh (which would re-scan everything). */
|
|
162
|
+
export function modelLevelTakeoverNote(result: SingleResult, opts?: { runId?: number }): string {
|
|
163
|
+
const retry = result.modelFallbackFrom ? ", and the current main model also failed" : "";
|
|
164
|
+
const detail = result.errorMessage?.trim();
|
|
165
|
+
const cause = detail
|
|
166
|
+
? `its model/provider call failed (${detail})`
|
|
167
|
+
: "its model was unavailable or failed (or the run stalled)";
|
|
168
|
+
const sessionPreserved = Boolean(result.sessionDir && result.sessionId) && opts?.runId !== undefined;
|
|
169
|
+
const recovery = sessionPreserved
|
|
170
|
+
? ` The sub-agent's earlier work in this run is preserved. Once a model is available again, call subagent_control with { action: "resume", id: ${opts!.runId} } to CONTINUE it in-context (it keeps the same run id and does not re-scan), or execute the task in the main window with your own tools.`
|
|
171
|
+
: ` Please execute this task in the main window with your own tools; do not re-dispatch it as a sub-agent.`;
|
|
172
|
+
return `The sub-agent could not complete this task: ${cause}${retry}.${recovery}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Resolve a run-id request to actual ids: an exact numeric match always wins
|
|
176
|
+
* (so "1" never fans out to 10, 11, …); only when no exact match exists does a
|
|
177
|
+
* prefix match run, as a convenience for partial ids. Keeps single-digit lookups
|
|
178
|
+
* from returning — or, for subagent_stop, acting on — a whole prefix family. */
|
|
179
|
+
export function matchRunIds(ids: number[], requested: string): number[] {
|
|
180
|
+
const exact = ids.filter((id) => String(id) === requested);
|
|
181
|
+
if (exact.length > 0) return exact;
|
|
182
|
+
return ids.filter((id) => String(id).startsWith(requested));
|
|
183
|
+
}
|