@ferris1225/pi-subagents 2.0.3 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -19
- package/agents/reviewer.md +7 -9
- package/package.json +1 -1
- package/src/announcements.ts +70 -70
- package/src/completion.ts +10 -2
- package/src/dispatch.ts +30 -997
- package/src/fixloop.ts +24 -21
- package/src/format.ts +174 -177
- package/src/index.ts +3 -2
- package/src/monitor.ts +19 -0
- package/src/prompt.ts +2 -2
- package/src/rpc-run.ts +1125 -1125
- package/src/thread-lifecycle.ts +1063 -0
- package/src/tools.ts +1 -0
- package/src/widget.ts +186 -144
- package/src/worktree.ts +687 -687
package/src/fixloop.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
|
|
15
|
-
import { extractKeyFragments, formatUsageCompact } from "./monitor.ts";
|
|
15
|
+
import { extractKeyFragments, formatUsageCompact, sumUsage } from "./monitor.ts";
|
|
16
16
|
import type { SubagentsConfig } from "./config.ts";
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -40,7 +40,7 @@ export function shouldTriggerFixLoop(result: SingleResult, config: SubagentsConf
|
|
|
40
40
|
/**
|
|
41
41
|
* Build the worker task brief for one fix round from a reviewer's findings.
|
|
42
42
|
* The worker gets the full review text so it can address concrete file:line
|
|
43
|
-
* issues, with instructions to fix
|
|
43
|
+
* issues, with instructions to fix every reported finding and self-verify.
|
|
44
44
|
*/
|
|
45
45
|
export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, maxRounds: number): string {
|
|
46
46
|
const review = getResultOutput(reviewerResult);
|
|
@@ -53,8 +53,9 @@ export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, m
|
|
|
53
53
|
review,
|
|
54
54
|
`---`,
|
|
55
55
|
``,
|
|
56
|
-
`Fix
|
|
57
|
-
`
|
|
56
|
+
`Fix EVERY finding in the reviewer's findings list — there is no severity triage; all of them get fixed.`,
|
|
57
|
+
`If a finding is factually wrong or clearly out of scope, say so explicitly instead of fixing it.`,
|
|
58
|
+
`Do NOT refactor unrelated code beyond what the findings require.`,
|
|
58
59
|
`After editing, run the project's format/build/tests when they exist and report`,
|
|
59
60
|
`exactly what you changed (paths + short rationale) so a reviewer can verify.`,
|
|
60
61
|
remaining > 0
|
|
@@ -120,18 +121,7 @@ export function formatChainSummary(steps: readonly ChainStep[]): string {
|
|
|
120
121
|
const id = step.runId !== undefined ? `#${step.runId} ` : "";
|
|
121
122
|
lines.push(`- ${id}${step.result.agent} · ${step.relation} · ${stepStatus(step)}${suffix}`);
|
|
122
123
|
}
|
|
123
|
-
const total = steps.
|
|
124
|
-
(acc, step) => {
|
|
125
|
-
acc.input += step.result.usage.input;
|
|
126
|
-
acc.output += step.result.usage.output;
|
|
127
|
-
acc.cacheRead += step.result.usage.cacheRead;
|
|
128
|
-
acc.cacheWrite += step.result.usage.cacheWrite;
|
|
129
|
-
acc.cost += step.result.usage.cost;
|
|
130
|
-
acc.turns += step.result.usage.turns;
|
|
131
|
-
return acc;
|
|
132
|
-
},
|
|
133
|
-
{ input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
134
|
-
);
|
|
124
|
+
const total = sumUsage(steps.map((step) => step.result.usage));
|
|
135
125
|
const usage = formatUsageCompact(total);
|
|
136
126
|
lines.push("", `Totals: ${steps.length} run${steps.length === 1 ? "" : "s"}${usage ? ` · ${usage}` : ""}`);
|
|
137
127
|
const ids = steps.filter((step) => step.runId !== undefined).map((step) => `#${step.runId}`);
|
|
@@ -141,11 +131,14 @@ export function formatChainSummary(steps: readonly ChainStep[]): string {
|
|
|
141
131
|
|
|
142
132
|
/**
|
|
143
133
|
* The re-review brief handed to the reviewer after a worker fix round. Includes
|
|
144
|
-
* the prior review
|
|
145
|
-
*
|
|
134
|
+
* the prior review AND the worker's report so the reviewer can adjudicate
|
|
135
|
+
* rejections instead of restating findings. The convergence contract keeps
|
|
136
|
+
* rounds from ping-ponging: rule on the open findings once, add only defects
|
|
137
|
+
* this round's edits introduced, never re-open a verified resolution.
|
|
146
138
|
*/
|
|
147
|
-
export function buildReReviewBrief(reviewerResult: SingleResult, round: number): string {
|
|
139
|
+
export function buildReReviewBrief(reviewerResult: SingleResult, round: number, workerResult: SingleResult): string {
|
|
148
140
|
const review = getResultOutput(reviewerResult);
|
|
141
|
+
const workerReport = getResultOutput(workerResult);
|
|
149
142
|
return [
|
|
150
143
|
`Re-review after auto-fix round ${round}.`,
|
|
151
144
|
``,
|
|
@@ -154,8 +147,18 @@ export function buildReReviewBrief(reviewerResult: SingleResult, round: number):
|
|
|
154
147
|
review,
|
|
155
148
|
`---`,
|
|
156
149
|
``,
|
|
157
|
-
`
|
|
158
|
-
|
|
150
|
+
`The worker's report (what it changed, plus any finding it rejected as factually wrong or out of scope):`,
|
|
151
|
+
`---`,
|
|
152
|
+
workerReport,
|
|
153
|
+
`---`,
|
|
154
|
+
``,
|
|
155
|
+
`Rule on EVERY previous finding: resolved, or still open. A finding the worker rejected must be`,
|
|
156
|
+
`adjudicated ONCE — accept the rejection unless you can concretely refute the worker's reasoning;`,
|
|
157
|
+
`never simply restate the finding for another round.`,
|
|
158
|
+
`Run \`git diff\` to see what changed, then add NEW findings only when they are defects this round's`,
|
|
159
|
+
`edits introduced or exposed (or a load-bearing issue the earlier review genuinely missed).`,
|
|
160
|
+
`Do NOT re-open a finding you verified as resolved.`,
|
|
161
|
+
`REQUEST_CHANGES only while an open finding remains; otherwise APPROVE.`,
|
|
159
162
|
`End with your machine-readable verdict line as usual (VERDICT: REVIEW_PASS / REVIEW_FAIL).`,
|
|
160
163
|
].join("\n");
|
|
161
164
|
}
|
package/src/format.ts
CHANGED
|
@@ -1,177 +1,174 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure formatting/result helpers shared by the subagent tool and the lookup
|
|
3
|
-
* tools (subagent_wait/status): usage rendering, completion blocks, synthetic
|
|
4
|
-
* result constructors, and run-id matching.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { AgentConfig } from "./agents.ts";
|
|
8
|
-
import { formatTaskSummary } from "./monitor.ts";
|
|
9
|
-
import { emptyUsage } from "./rpc-run.ts";
|
|
10
|
-
import {
|
|
11
|
-
getResultOutput,
|
|
12
|
-
isFailedResult,
|
|
13
|
-
truncateResultOutput,
|
|
14
|
-
writeResultArtifact,
|
|
15
|
-
type SingleResult,
|
|
16
|
-
type UsageStats,
|
|
17
|
-
} from "./spawn.ts";
|
|
18
|
-
|
|
19
|
-
export function queuedResult(agent: AgentConfig, task: string, thinking?: string): SingleResult {
|
|
20
|
-
return {
|
|
21
|
-
agent: agent.name,
|
|
22
|
-
task,
|
|
23
|
-
exitCode: -1,
|
|
24
|
-
messages: [],
|
|
25
|
-
stderr: "",
|
|
26
|
-
usage: emptyUsage(),
|
|
27
|
-
model: agent.model,
|
|
28
|
-
...(thinking ? { thinking } : {}),
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function failedStartResult(agentName: string, task: string, errorMessage: string): SingleResult {
|
|
33
|
-
return {
|
|
34
|
-
agent: agentName,
|
|
35
|
-
task,
|
|
36
|
-
exitCode: 1,
|
|
37
|
-
messages: [],
|
|
38
|
-
stderr: errorMessage,
|
|
39
|
-
usage: emptyUsage(),
|
|
40
|
-
errorMessage,
|
|
41
|
-
dispatchFailed: true,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Failed result for a background task that crashed with an exception (spawn
|
|
46
|
-
* infra, delivery API, ...) instead of returning a normal result. */
|
|
47
|
-
export function dispatchFailedResult(agent: AgentConfig, task: string, error: unknown, thinking?: string): SingleResult {
|
|
48
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
49
|
-
return {
|
|
50
|
-
...queuedResult(agent, task, thinking),
|
|
51
|
-
exitCode: 1,
|
|
52
|
-
stderr: errorMessage,
|
|
53
|
-
stopReason: "error",
|
|
54
|
-
errorMessage,
|
|
55
|
-
dispatchFailed: true,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function formatTokens(count: number): string {
|
|
60
|
-
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
61
|
-
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
62
|
-
return String(count);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function formatUsage(usage: UsageStats): string {
|
|
66
|
-
const parts: string[] = [];
|
|
67
|
-
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
68
|
-
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
69
|
-
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
70
|
-
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
71
|
-
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
72
|
-
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
73
|
-
return parts.join(" ");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface CompletionFormatOptions {
|
|
77
|
-
/** Include individual failed-tool errors. Reserved for explicit status lookup. */
|
|
78
|
-
failedToolDetails?: boolean;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function formatCompletionBlock(
|
|
82
|
-
result: SingleResult,
|
|
83
|
-
maxResultLines: number,
|
|
84
|
-
cwd?: string,
|
|
85
|
-
options: CompletionFormatOptions = {},
|
|
86
|
-
): string {
|
|
87
|
-
const failed = isFailedResult(result);
|
|
88
|
-
const failedTools = result.failedTools ?? [];
|
|
89
|
-
const status = failed
|
|
90
|
-
? "failed"
|
|
91
|
-
: options.failedToolDetails && failedTools.length > 0
|
|
92
|
-
? `completed with ${failedTools.length} failed tool call${failedTools.length === 1 ? "" : "s"}`
|
|
93
|
-
: "completed";
|
|
94
|
-
const usage = formatUsage(result.usage);
|
|
95
|
-
const output = getResultOutput(result);
|
|
96
|
-
const { text, truncated } = truncateResultOutput(output, maxResultLines);
|
|
97
|
-
const fallbackNote = result.modelFallbackFrom
|
|
98
|
-
? ` (selected model ${result.modelFallbackFrom} failed → main ${result.model ?? "dynamic default"})`
|
|
99
|
-
: "";
|
|
100
|
-
const startupRetryNote = result.startupRetries
|
|
101
|
-
? ` (recovered after ${result.startupRetries} startup retr${result.startupRetries === 1 ? "y" : "ies"} — concurrent pi startup race)`
|
|
102
|
-
: "";
|
|
103
|
-
const relations = [
|
|
104
|
-
result.forkedFromRunId !== undefined ? `forked from #${result.forkedFromRunId}` : undefined,
|
|
105
|
-
(result.forkChildRunIds?.length ?? 0) > 0 ? `fork children ${result.forkChildRunIds!.map((id) => `#${id}`).join(", ")}` : undefined,
|
|
106
|
-
].filter((value): value is string => Boolean(value));
|
|
107
|
-
const relationNote = relations.length > 0 ? ` · ${relations.join(" · ")}` : "";
|
|
108
|
-
const runNote = result.runId !== undefined ? ` · run #${result.runId}` : "";
|
|
109
|
-
const lines = [`### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}${fallbackNote}${startupRetryNote}${runNote}`, "", `Task: ${formatTaskSummary(result.task, 80, false)}`, ""];
|
|
110
|
-
if (result.isolation === "worktree") {
|
|
111
|
-
const isolation =
|
|
112
|
-
result.integrationStatus === "integrated"
|
|
113
|
-
? "worktree · changes integrated into the original working tree"
|
|
114
|
-
: result.integrationStatus === "no_changes"
|
|
115
|
-
? "worktree · no changes; temporary worktree removed"
|
|
116
|
-
: result.integrationStatus === "retained"
|
|
117
|
-
? result.integrationApplied
|
|
118
|
-
? "worktree · changes applied, but cleanup failed; recovery artifacts retained"
|
|
119
|
-
: "worktree · integration failed; recovery artifacts retained"
|
|
120
|
-
: "worktree · isolated";
|
|
121
|
-
lines.push(`Isolation: ${isolation}${relationNote}`);
|
|
122
|
-
if (result.integrationWorktreePath) lines.push(`Retained worktree: ${result.integrationWorktreePath}`);
|
|
123
|
-
if (result.integrationPatchPath) lines.push(`Retained patch: ${result.integrationPatchPath}`);
|
|
124
|
-
if (result.integrationError) lines.push(`Integration error: ${result.integrationError}`);
|
|
125
|
-
lines.push("");
|
|
126
|
-
} else if (relations.length > 0) {
|
|
127
|
-
lines.push(`Relation: ${relations.join(" · ")}`, "");
|
|
128
|
-
}
|
|
129
|
-
lines.push(text);
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
if (options.failedToolDetails && failedTools.length > 0) {
|
|
134
|
-
lines.push(
|
|
135
|
-
"",
|
|
136
|
-
`⚠ ${failedTools.length} failed tool call${failedTools.length === 1 ? "" : "s"}:`,
|
|
137
|
-
...failedTools.map((tool) => `- ${tool.toolName}: ${tool.error.trim() || "(no output)"}`),
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
: "
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (exact.length > 0) return exact;
|
|
176
|
-
return ids.filter((id) => String(id).startsWith(requested));
|
|
177
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Pure formatting/result helpers shared by the subagent tool and the lookup
|
|
3
|
+
* tools (subagent_wait/status): usage rendering, completion blocks, synthetic
|
|
4
|
+
* result constructors, and run-id matching.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AgentConfig } from "./agents.ts";
|
|
8
|
+
import { formatTaskSummary } from "./monitor.ts";
|
|
9
|
+
import { emptyUsage } from "./rpc-run.ts";
|
|
10
|
+
import {
|
|
11
|
+
getResultOutput,
|
|
12
|
+
isFailedResult,
|
|
13
|
+
truncateResultOutput,
|
|
14
|
+
writeResultArtifact,
|
|
15
|
+
type SingleResult,
|
|
16
|
+
type UsageStats,
|
|
17
|
+
} from "./spawn.ts";
|
|
18
|
+
|
|
19
|
+
export function queuedResult(agent: AgentConfig, task: string, thinking?: string): SingleResult {
|
|
20
|
+
return {
|
|
21
|
+
agent: agent.name,
|
|
22
|
+
task,
|
|
23
|
+
exitCode: -1,
|
|
24
|
+
messages: [],
|
|
25
|
+
stderr: "",
|
|
26
|
+
usage: emptyUsage(),
|
|
27
|
+
model: agent.model,
|
|
28
|
+
...(thinking ? { thinking } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function failedStartResult(agentName: string, task: string, errorMessage: string): SingleResult {
|
|
33
|
+
return {
|
|
34
|
+
agent: agentName,
|
|
35
|
+
task,
|
|
36
|
+
exitCode: 1,
|
|
37
|
+
messages: [],
|
|
38
|
+
stderr: errorMessage,
|
|
39
|
+
usage: emptyUsage(),
|
|
40
|
+
errorMessage,
|
|
41
|
+
dispatchFailed: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Failed result for a background task that crashed with an exception (spawn
|
|
46
|
+
* infra, delivery API, ...) instead of returning a normal result. */
|
|
47
|
+
export function dispatchFailedResult(agent: AgentConfig, task: string, error: unknown, thinking?: string): SingleResult {
|
|
48
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
49
|
+
return {
|
|
50
|
+
...queuedResult(agent, task, thinking),
|
|
51
|
+
exitCode: 1,
|
|
52
|
+
stderr: errorMessage,
|
|
53
|
+
stopReason: "error",
|
|
54
|
+
errorMessage,
|
|
55
|
+
dispatchFailed: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatTokens(count: number): string {
|
|
60
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
61
|
+
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
62
|
+
return String(count);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function formatUsage(usage: UsageStats): string {
|
|
66
|
+
const parts: string[] = [];
|
|
67
|
+
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
68
|
+
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
69
|
+
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
70
|
+
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
71
|
+
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
72
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
73
|
+
return parts.join(" ");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CompletionFormatOptions {
|
|
77
|
+
/** Include individual failed-tool errors. Reserved for explicit status lookup. */
|
|
78
|
+
failedToolDetails?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function formatCompletionBlock(
|
|
82
|
+
result: SingleResult,
|
|
83
|
+
maxResultLines: number,
|
|
84
|
+
cwd?: string,
|
|
85
|
+
options: CompletionFormatOptions = {},
|
|
86
|
+
): string {
|
|
87
|
+
const failed = isFailedResult(result);
|
|
88
|
+
const failedTools = result.failedTools ?? [];
|
|
89
|
+
const status = failed
|
|
90
|
+
? "failed"
|
|
91
|
+
: options.failedToolDetails && failedTools.length > 0
|
|
92
|
+
? `completed with ${failedTools.length} failed tool call${failedTools.length === 1 ? "" : "s"}`
|
|
93
|
+
: "completed";
|
|
94
|
+
const usage = formatUsage(result.usage);
|
|
95
|
+
const output = getResultOutput(result);
|
|
96
|
+
const { text, truncated } = truncateResultOutput(output, maxResultLines);
|
|
97
|
+
const fallbackNote = result.modelFallbackFrom
|
|
98
|
+
? ` (selected model ${result.modelFallbackFrom} failed → main ${result.model ?? "dynamic default"})`
|
|
99
|
+
: "";
|
|
100
|
+
const startupRetryNote = result.startupRetries
|
|
101
|
+
? ` (recovered after ${result.startupRetries} startup retr${result.startupRetries === 1 ? "y" : "ies"} — concurrent pi startup race)`
|
|
102
|
+
: "";
|
|
103
|
+
const relations = [
|
|
104
|
+
result.forkedFromRunId !== undefined ? `forked from #${result.forkedFromRunId}` : undefined,
|
|
105
|
+
(result.forkChildRunIds?.length ?? 0) > 0 ? `fork children ${result.forkChildRunIds!.map((id) => `#${id}`).join(", ")}` : undefined,
|
|
106
|
+
].filter((value): value is string => Boolean(value));
|
|
107
|
+
const relationNote = relations.length > 0 ? ` · ${relations.join(" · ")}` : "";
|
|
108
|
+
const runNote = result.runId !== undefined ? ` · run #${result.runId}` : "";
|
|
109
|
+
const lines = [`### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}${fallbackNote}${startupRetryNote}${runNote}`, "", `Task: ${formatTaskSummary(result.task, 80, false)}`, ""];
|
|
110
|
+
if (result.isolation === "worktree") {
|
|
111
|
+
const isolation =
|
|
112
|
+
result.integrationStatus === "integrated"
|
|
113
|
+
? "worktree · changes integrated into the original working tree"
|
|
114
|
+
: result.integrationStatus === "no_changes"
|
|
115
|
+
? "worktree · no changes; temporary worktree removed"
|
|
116
|
+
: result.integrationStatus === "retained"
|
|
117
|
+
? result.integrationApplied
|
|
118
|
+
? "worktree · changes applied, but cleanup failed; recovery artifacts retained"
|
|
119
|
+
: "worktree · integration failed; recovery artifacts retained"
|
|
120
|
+
: "worktree · isolated";
|
|
121
|
+
lines.push(`Isolation: ${isolation}${relationNote}`);
|
|
122
|
+
if (result.integrationWorktreePath) lines.push(`Retained worktree: ${result.integrationWorktreePath}`);
|
|
123
|
+
if (result.integrationPatchPath) lines.push(`Retained patch: ${result.integrationPatchPath}`);
|
|
124
|
+
if (result.integrationError) lines.push(`Integration error: ${result.integrationError}`);
|
|
125
|
+
lines.push("");
|
|
126
|
+
} else if (relations.length > 0) {
|
|
127
|
+
lines.push(`Relation: ${relations.join(" · ")}`, "");
|
|
128
|
+
}
|
|
129
|
+
lines.push(text);
|
|
130
|
+
// Failed-tool diagnostics are deliberate opt-in via subagent_status: agents
|
|
131
|
+
// report their own verification in the output above, and a transient failed
|
|
132
|
+
// call (no-match grep, rejected edit) is noise in an automatic delivery.
|
|
133
|
+
if (options.failedToolDetails && failedTools.length > 0) {
|
|
134
|
+
lines.push(
|
|
135
|
+
"",
|
|
136
|
+
`⚠ ${failedTools.length} failed tool call${failedTools.length === 1 ? "" : "s"}:`,
|
|
137
|
+
...failedTools.map((tool) => `- ${tool.toolName}: ${tool.error.trim() || "(no output)"}`),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (truncated) {
|
|
141
|
+
// The full text lives on disk so the main agent can read it on demand.
|
|
142
|
+
lines.push("", `(output truncated to ${maxResultLines} lines; full result: ${writeResultArtifact(output, result.agent, result.projectCwd ?? cwd)})`);
|
|
143
|
+
}
|
|
144
|
+
return lines.join("\n");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Instruction appended to a model-level failure: the sub-agent's provider never
|
|
148
|
+
* produced usable output (or the run stalled), so the task is handed back to the
|
|
149
|
+
* main window instead of being left as a dead failure. When the run preserved a
|
|
150
|
+
* session with earlier work (and the run id is known), steer the main agent to
|
|
151
|
+
* RESUME it in-context once a model is available, instead of re-dispatching
|
|
152
|
+
* fresh (which would re-scan everything). */
|
|
153
|
+
export function modelLevelTakeoverNote(result: SingleResult, opts?: { runId?: number }): string {
|
|
154
|
+
const retry = result.modelFallbackFrom ? ", and the current main model also failed" : "";
|
|
155
|
+
const detail = result.errorMessage?.trim();
|
|
156
|
+
const cause = detail
|
|
157
|
+
? `its model/provider call failed (${detail})`
|
|
158
|
+
: "its model was unavailable or failed (or the run stalled)";
|
|
159
|
+
const sessionPreserved = Boolean(result.sessionDir && result.sessionId) && opts?.runId !== undefined;
|
|
160
|
+
const recovery = sessionPreserved
|
|
161
|
+
? ` 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.`
|
|
162
|
+
: ` Please execute this task in the main window with your own tools; do not re-dispatch it as a sub-agent.`;
|
|
163
|
+
return `The sub-agent could not complete this task: ${cause}${retry}.${recovery}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Resolve a run-id request to actual ids: an exact numeric match always wins
|
|
167
|
+
* (so "1" never fans out to 10, 11, …); only when no exact match exists does a
|
|
168
|
+
* prefix match run, as a convenience for partial ids. Keeps single-digit lookups
|
|
169
|
+
* from returning — or, for subagent_stop, acting on — a whole prefix family. */
|
|
170
|
+
export function matchRunIds(ids: number[], requested: string): number[] {
|
|
171
|
+
const exact = ids.filter((id) => String(id) === requested);
|
|
172
|
+
if (exact.length > 0) return exact;
|
|
173
|
+
return ids.filter((id) => String(id).startsWith(requested));
|
|
174
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Assembly point: builds the shared runtime and registers everything.
|
|
5
5
|
* The heavy lifting lives in focused modules:
|
|
6
|
-
* - dispatch.ts
|
|
7
|
-
* -
|
|
6
|
+
* - dispatch.ts — the `subagent` tool contract and auto-fix chain
|
|
7
|
+
* - thread-lifecycle.ts — queued generations, resume/fork, isolation settlement
|
|
8
|
+
* - tools.ts — subagent_control / subagent_wait / status / stop
|
|
8
9
|
* - announcements.ts — session-start recovery, notices, and widget install
|
|
9
10
|
* - widget.ts — active-only TUI run status
|
|
10
11
|
* - runtime.ts — shared per-session state
|
package/src/monitor.ts
CHANGED
|
@@ -53,12 +53,15 @@ export interface RunView {
|
|
|
53
53
|
groupId?: string;
|
|
54
54
|
/** Human-readable role within a chain, e.g. "fix round 1" or "re-review round 1". */
|
|
55
55
|
relationLabel?: string;
|
|
56
|
+
/** Owning run for chain children: the triggering reviewer whose row represents the chain. */
|
|
57
|
+
parentRunId?: number;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
/** Optional chain metadata for runs spawned by an auto-fix loop. */
|
|
59
61
|
export interface RunChainMeta {
|
|
60
62
|
groupId?: string;
|
|
61
63
|
relationLabel?: string;
|
|
64
|
+
parentRunId?: number;
|
|
62
65
|
isolation?: IsolationMode;
|
|
63
66
|
forkedFromRunId?: number;
|
|
64
67
|
}
|
|
@@ -248,6 +251,21 @@ export function formatUsageCompact(usage: UsageStats): string {
|
|
|
248
251
|
return parts.join(" ");
|
|
249
252
|
}
|
|
250
253
|
|
|
254
|
+
/** Aggregate usage across several runs (chain steps or a completion group). */
|
|
255
|
+
export function sumUsage(parts: readonly UsageStats[]): UsageStats {
|
|
256
|
+
const total: UsageStats = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
257
|
+
for (const part of parts) {
|
|
258
|
+
total.input += part.input;
|
|
259
|
+
total.output += part.output;
|
|
260
|
+
total.cacheRead += part.cacheRead;
|
|
261
|
+
total.cacheWrite += part.cacheWrite;
|
|
262
|
+
total.cost += part.cost;
|
|
263
|
+
total.contextTokens += part.contextTokens;
|
|
264
|
+
total.turns += part.turns;
|
|
265
|
+
}
|
|
266
|
+
return total;
|
|
267
|
+
}
|
|
268
|
+
|
|
251
269
|
export function formatDuration(ms: number): string {
|
|
252
270
|
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
253
271
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
@@ -393,6 +411,7 @@ export class MonitorStore {
|
|
|
393
411
|
usage: emptyUsage(),
|
|
394
412
|
...(meta?.groupId ? { groupId: meta.groupId } : {}),
|
|
395
413
|
...(meta?.relationLabel ? { relationLabel: meta.relationLabel } : {}),
|
|
414
|
+
...(meta?.parentRunId !== undefined ? { parentRunId: meta.parentRunId } : {}),
|
|
396
415
|
...(meta?.isolation ? { isolation: meta.isolation, integrationStatus: meta.isolation === "worktree" ? "pending" : undefined } : {}),
|
|
397
416
|
...(meta?.forkedFromRunId !== undefined ? { forkedFromRunId: meta.forkedFromRunId } : {}),
|
|
398
417
|
});
|
package/src/prompt.ts
CHANGED
|
@@ -78,7 +78,7 @@ Result handoff (do not re-state):
|
|
|
78
78
|
|
|
79
79
|
Review & verification:
|
|
80
80
|
- Never report an unrun check as passed; report it as unavailable or as a pre-existing failure.
|
|
81
|
-
${hasReviewer ? `- For non-trivial diffs${hasCleaner ? " (including cleaner edits)" : ""}, run one fresh read-only \`reviewer\` sub-agent before reporting done. Fix
|
|
81
|
+
${hasReviewer ? `- For non-trivial diffs${hasCleaner ? " (including cleaner edits)" : ""}, run one fresh read-only \`reviewer\` sub-agent before reporting done. Fix every finding the reviewer reports and re-review at most once.
|
|
82
82
|
- Use multi-model cross-review only when explicitly requested or for genuinely high-risk changes (security, unsafe/FFI, persistence-migration, concurrency). Reviewers are read-only; only the main agent edits.
|
|
83
|
-
` : ""}- Commit or push only when explicitly requested, applicable checks pass, and no
|
|
83
|
+
` : ""}- Commit or push only when explicitly requested, applicable checks pass, and no unresolved review findings remain.`;
|
|
84
84
|
}
|