@ferris1225/pi-subagents 4.1.13 → 4.1.16
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 +333 -291
- package/agents/cleaner.md +24 -30
- package/agents/documenter.md +23 -20
- package/agents/explorer.md +7 -2
- package/agents/reviewer.md +82 -77
- package/agents/worker.md +45 -37
- package/package.json +1 -1
- package/src/announcements.ts +59 -54
- package/src/background.ts +26 -10
- package/src/completion.ts +7 -1
- package/src/config.ts +1 -1
- package/src/dispatch.ts +133 -133
- package/src/durable.ts +85 -19
- package/src/format.ts +179 -167
- package/src/monitor.ts +4 -2
- package/src/prompt.ts +14 -27
- package/src/runtime.ts +18 -14
- package/src/setup.ts +3 -3
- package/src/spawn.ts +650 -642
- package/src/temp-hygiene.ts +0 -28
- package/src/thread-lifecycle.ts +85 -99
- package/src/tools.ts +712 -708
- package/src/widget.ts +9 -0
- package/src/workflow.ts +199 -248
- package/src/worktree.ts +64 -37
package/src/format.ts
CHANGED
|
@@ -1,167 +1,179 @@
|
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
: ""
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (result.
|
|
119
|
-
if (result.
|
|
120
|
-
lines.push(
|
|
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
|
-
const
|
|
153
|
-
const
|
|
154
|
-
? `
|
|
155
|
-
:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
+
/** Project-scoped directory the full result is written to when the output
|
|
80
|
+
* is truncated: <projectRoot>/results. */
|
|
81
|
+
resultRoot?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function formatCompletionBlock(
|
|
85
|
+
result: SingleResult,
|
|
86
|
+
maxResultLines: number,
|
|
87
|
+
options: CompletionFormatOptions = {},
|
|
88
|
+
): string {
|
|
89
|
+
const failed = isFailedResult(result);
|
|
90
|
+
const failedTools = result.failedTools ?? [];
|
|
91
|
+
const status = failed
|
|
92
|
+
? "failed"
|
|
93
|
+
: options.failedToolDetails && failedTools.length > 0
|
|
94
|
+
? `completed with ${failedTools.length} failed tool call${failedTools.length === 1 ? "" : "s"}`
|
|
95
|
+
: "completed";
|
|
96
|
+
const usage = formatUsage(result.usage);
|
|
97
|
+
const output = getResultOutput(result);
|
|
98
|
+
const { text, truncated } = truncateResultOutput(output, maxResultLines);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
|
+
const lines = [`### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}${fallbackNote}${startupRetryNote}${runNote}`, "", `Task: ${formatTaskSummary(result.task, 80, false)}`, ""];
|
|
106
|
+
if (result.isolation === "worktree") {
|
|
107
|
+
const isolation =
|
|
108
|
+
result.integrationStatus === "integrated"
|
|
109
|
+
? "worktree · changes integrated into the original working tree"
|
|
110
|
+
: result.integrationStatus === "no_changes"
|
|
111
|
+
? "worktree · no changes; temporary worktree removed"
|
|
112
|
+
: result.integrationStatus === "retained"
|
|
113
|
+
? result.integrationApplied
|
|
114
|
+
? "worktree · changes applied, but cleanup failed; recovery artifacts retained"
|
|
115
|
+
: "worktree · integration failed; recovery artifacts retained"
|
|
116
|
+
: "worktree · isolated";
|
|
117
|
+
lines.push(`Isolation: ${isolation}`);
|
|
118
|
+
if (result.integrationWorktreePath) lines.push(`Retained worktree: ${result.integrationWorktreePath}`);
|
|
119
|
+
if (result.integrationPatchPath) lines.push(`Retained patch: ${result.integrationPatchPath}`);
|
|
120
|
+
if (result.integrationError) lines.push(`Integration error: ${result.integrationError}`);
|
|
121
|
+
lines.push("");
|
|
122
|
+
}
|
|
123
|
+
lines.push(text);
|
|
124
|
+
// Failed-tool diagnostics are deliberate opt-in via subagent_status: agents
|
|
125
|
+
// report their own verification in the output above, and a transient failed
|
|
126
|
+
// call (no-match grep, rejected edit) is noise in an automatic delivery.
|
|
127
|
+
if (options.failedToolDetails && failedTools.length > 0) {
|
|
128
|
+
lines.push(
|
|
129
|
+
"",
|
|
130
|
+
`⚠ ${failedTools.length} failed tool call${failedTools.length === 1 ? "" : "s"}:`,
|
|
131
|
+
...failedTools.map((tool) => `- ${tool.toolName}: ${tool.error.trim() || "(no output)"}`),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (truncated) {
|
|
135
|
+
// The full text lives on disk so the main agent can read it on demand.
|
|
136
|
+
const artifact = options.resultRoot
|
|
137
|
+
? writeResultArtifact(output, result.agent, options.resultRoot)
|
|
138
|
+
: "(result root unavailable)";
|
|
139
|
+
lines.push("", `(output truncated to ${maxResultLines} lines; full result: ${artifact})`);
|
|
140
|
+
}
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Instruction appended to a model-level failure: the sub-agent's provider never
|
|
145
|
+
* produced usable output (or the run stalled), so the task is handed back to the
|
|
146
|
+
* main window instead of being left as a dead failure. When the run preserved a
|
|
147
|
+
* session with earlier work (and the run id is known), steer the main agent to
|
|
148
|
+
* RESUME it in-context once a model is available, instead of re-dispatching
|
|
149
|
+
* fresh (which would re-scan everything). */
|
|
150
|
+
export function modelLevelTakeoverNote(result: SingleResult, opts?: { runId?: number }): string {
|
|
151
|
+
const retry = result.modelFallbackFrom ? ", and the current main model also failed" : "";
|
|
152
|
+
const detail = result.errorMessage?.trim();
|
|
153
|
+
const cause = detail
|
|
154
|
+
? `its model/provider call failed (${detail})`
|
|
155
|
+
: "its model was unavailable or failed (or the run stalled)";
|
|
156
|
+
const sessionPreserved = Boolean(result.sessionDir && result.sessionId) && opts?.runId !== undefined;
|
|
157
|
+
const recovery = sessionPreserved
|
|
158
|
+
? ` 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.`
|
|
159
|
+
: ` Please execute this task in the main window with your own tools; do not re-dispatch it as a sub-agent.`;
|
|
160
|
+
return `The sub-agent could not complete this task: ${cause}${retry}.${recovery}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Instruction appended whenever a failing gate verdict is delivered: the
|
|
164
|
+
* findings return to the main agent, which owns the fix decision — the runtime
|
|
165
|
+
* never auto-fixes. Stating it at this exact decision point keeps the main
|
|
166
|
+
* agent from relaying the findings to the user and stopping. */
|
|
167
|
+
export function reviewFailFollowUpNote(): string {
|
|
168
|
+
return "This gate failed and the findings are yours to resolve now: fix them inline or dispatch a worker briefed with these fix instructions, then re-verify the change. Ask the user only before a genuinely destructive or scope-changing fix; do not deliver while a finding stands.";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Resolve a run-id request to actual ids: an exact numeric match always wins
|
|
172
|
+
* (so "1" never fans out to 10, 11, …); only when no exact match exists does a
|
|
173
|
+
* prefix match run, as a convenience for partial ids. Keeps single-digit lookups
|
|
174
|
+
* from returning — or, for subagent_stop, acting on — a whole prefix family. */
|
|
175
|
+
export function matchRunIds(ids: number[], requested: string): number[] {
|
|
176
|
+
const exact = ids.filter((id) => String(id) === requested);
|
|
177
|
+
if (exact.length > 0) return exact;
|
|
178
|
+
return ids.filter((id) => String(id).startsWith(requested));
|
|
179
|
+
}
|
package/src/monitor.ts
CHANGED
|
@@ -759,11 +759,13 @@ export function statusIcon(status: RunStatus, theme: Theme): string {
|
|
|
759
759
|
}
|
|
760
760
|
}
|
|
761
761
|
|
|
762
|
-
/** User-facing status label used by tool/status rendering.
|
|
762
|
+
/** User-facing status label used by tool/status rendering. Queued runs say
|
|
763
|
+
* so: anything vaguer ("ready") reads like an unexplained cap and made models
|
|
764
|
+
* stop dispatching while slots were merely pacing. */
|
|
763
765
|
export function statusLabel(status: RunStatus): string {
|
|
764
766
|
switch (status) {
|
|
765
767
|
case "queued":
|
|
766
|
-
return "
|
|
768
|
+
return "queued";
|
|
767
769
|
case "running":
|
|
768
770
|
return "running";
|
|
769
771
|
case "interrupting":
|
package/src/prompt.ts
CHANGED
|
@@ -40,53 +40,40 @@ export function buildDelegationDirective(
|
|
|
40
40
|
: `${namedWorktreeTargets.slice(0, -1).join(", ")}, ${namedWorktreeTargets.at(-1)}, or another`;
|
|
41
41
|
|
|
42
42
|
const dispatchRules = [
|
|
43
|
-
|
|
43
|
+
`Delegate aggressively: child contexts are cheap, yours is scarce. Inline only trivial work — a one-shot lookup, a single focused edit, an answer already in context${hasWorker ? "; default every non-trivial implementation, fix, refactor, or test task to `worker`" : ""}.`,
|
|
44
44
|
...(hasExplorer
|
|
45
45
|
? [
|
|
46
|
-
"
|
|
46
|
+
"`explorer`: broad or multi-file search. Its findings are leads, never proof — re-read load-bearing files before acting yourself (a child you brief re-verifies). Split a broad question into several parallel explorers with disjoint scopes.",
|
|
47
47
|
]
|
|
48
48
|
: []),
|
|
49
|
-
...(hasWorker
|
|
50
|
-
? ["Use `worker` for a self-contained implementation, fix, refactor, or test whose separate context pays for itself."]
|
|
51
|
-
: []),
|
|
52
49
|
...(hasCleaner
|
|
53
|
-
? [
|
|
54
|
-
`Use \`cleaner\` only for user-authorized cleanup or deduplication; it applies every safe proven cut without item-by-item approval, and never runs as a pre-commit gate or by PR count.`,
|
|
55
|
-
]
|
|
50
|
+
? ["`cleaner`: only user-authorized cleanup or dedup; it applies every safe proven cut without per-item approval and is never a gate."]
|
|
56
51
|
: []),
|
|
57
52
|
...(hasDocumenter
|
|
58
|
-
? [
|
|
59
|
-
`Use \`documenter\` directly only for explicit standalone documentation work; a top-level documenter delivers without a gate.${codeWriterNames.length > 0 ? ` The runtime runs the final docs sync after REVIEW_PASS with DOCUMENTATION: NEEDED or a missing marker, or as the reviewer-disabled fallback; writers sync docs they directly affect — never dispatch a duplicate.` : ""}`,
|
|
60
|
-
]
|
|
53
|
+
? ["`documenter`: standalone docs/comment work, or syncing real drift a change left — writers already sync what they directly affect."]
|
|
61
54
|
: []),
|
|
62
55
|
...(hasReviewer
|
|
63
56
|
? [
|
|
64
|
-
|
|
57
|
+
`\`reviewer\`: read-only assessments and gates${codeWriterNames.length > 0 ? `; successful ${codeWriterNames.join("/")} runs get one fresh gate, and failing gates are fixed by the reviewer itself in bounded fix/re-review rounds that converge on the fixes (a still-failing gate returns to you)` : ""}. Advisory output has no VERDICT and cannot authorize edits.`,
|
|
65
58
|
]
|
|
66
59
|
: []),
|
|
67
|
-
|
|
68
|
-
"Brief each child
|
|
69
|
-
`
|
|
70
|
-
"A configured child model/provider failure automatically continues the same retained session on the current main model; do not redispatch. Ordinary tool/task failures stay on the selected model.",
|
|
60
|
+
`Parallelize by default: map the todo list onto ONE \`tasks\` dispatch. One child owns one deliverable and its files; only genuinely dependent work waits for its prerequisite.`,
|
|
61
|
+
"Brief each child completely — goal, exact paths, constraints, expected output; it has no conversation memory and cannot delegate. Resume parked threads with `subagent_control resume`.",
|
|
62
|
+
`Request \`isolation: "worktree"\` only for ${worktreeTargets} write-capable agent in a repo with committed HEAD.`,
|
|
71
63
|
];
|
|
72
64
|
|
|
73
65
|
const handoffRules = [
|
|
74
|
-
"Dispatch ends
|
|
66
|
+
"Dispatch never blocks or ends your turn — keep working; each completion resumes you automatically. Never sleep, poll, or `subagent_wait` to hold the turn.",
|
|
75
67
|
"Results are already shown; add only your conclusion or next action, never a restatement.",
|
|
76
|
-
"Before declaring the overall task done,
|
|
68
|
+
"Before declaring the overall task done, `subagent_status` must show no active runs.",
|
|
77
69
|
];
|
|
78
70
|
|
|
79
71
|
const verificationRules = [
|
|
80
|
-
"Never report an unrun check as passed; surface unavailable checks and pre-existing failures
|
|
72
|
+
"Never report an unrun check as passed; surface unavailable checks and pre-existing failures, and inspect actual changes before reporting completion.",
|
|
81
73
|
...(hasReviewer
|
|
82
74
|
? [
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"A direct REVIEW_PASS with DOCUMENTATION: CLEAN delivers immediately; NEEDED or a missing marker runs one documentation sync.",
|
|
86
|
-
]
|
|
87
|
-
: []),
|
|
88
|
-
"A REVIEW_FAIL — direct or from a managed gate — returns the findings to you: resolve them yourself, inline or via a worker you brief, without waiting for the user; the runtime never auto-fixes. Ask only for genuinely destructive or scope-changing fixes. Advisory reports cannot trigger writes.",
|
|
89
|
-
"Use multi-model cross-review only when explicitly requested or for genuinely high-risk security, unsafe/FFI, persistence-migration, or concurrency changes.",
|
|
75
|
+
"A REVIEW_FAIL from a gate you dispatched directly returns its findings to you: fix them inline or via a briefed worker without waiting for the user (ask only for genuinely destructive or scope-changing fixes), then re-verify ONCE. If the gate still fails, report the remaining findings and move on — never loop gate dispatches.",
|
|
76
|
+
"Multi-model cross-review only when explicitly requested or for high-risk security, unsafe/FFI, persistence-migration, or concurrency changes.",
|
|
90
77
|
]
|
|
91
78
|
: []),
|
|
92
79
|
"Commit or push only when explicitly requested, applicable checks pass, and no review finding remains unresolved.",
|
|
@@ -95,7 +82,7 @@ export function buildDelegationDirective(
|
|
|
95
82
|
return `
|
|
96
83
|
## Sub-agent delegation (pi-subagents)
|
|
97
84
|
|
|
98
|
-
|
|
85
|
+
\`subagent\` runs isolated leaf Pi child processes in the background; each completion resumes you.
|
|
99
86
|
|
|
100
87
|
Agents:
|
|
101
88
|
${catalog}
|
package/src/runtime.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { rmSync } from "node:fs";
|
|
13
|
-
import {
|
|
13
|
+
import { resolveSubagentConcurrency, BackgroundTaskQueue } from "./background.ts";
|
|
14
14
|
import {
|
|
15
15
|
completionGroupTriggersTurn,
|
|
16
16
|
createCompletionBatcher,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
type CompletionMessageItem,
|
|
21
21
|
} from "./completion.ts";
|
|
22
22
|
import { type ThinkingLevel } from "./config.ts";
|
|
23
|
-
import { threadRecordFromThread, upsertThreadRecord, type ThreadRecord } from "./durable.ts";
|
|
23
|
+
import { removeThreadRecord, threadRecordFromThread, upsertThreadRecord, type ThreadRecord } from "./durable.ts";
|
|
24
24
|
import { isRunActiveStatus, monitor } from "./monitor.ts";
|
|
25
25
|
import type { RpcRunControl } from "./rpc-run.ts";
|
|
26
26
|
import type { StartBackgroundInternal } from "./thread-lifecycle.ts";
|
|
@@ -50,8 +50,6 @@ export interface SubagentThread {
|
|
|
50
50
|
executionCwd: string;
|
|
51
51
|
thinkingLevel?: ThinkingLevel;
|
|
52
52
|
isolation: IsolationMode;
|
|
53
|
-
/** Report-only reviewer dispatch: verdicts never chain into a managed workflow. */
|
|
54
|
-
advisoryReview: boolean;
|
|
55
53
|
worktree?: WorktreeIsolation;
|
|
56
54
|
state: ThreadState;
|
|
57
55
|
control: RpcRunControl;
|
|
@@ -122,7 +120,7 @@ export interface SubagentRuntime {
|
|
|
122
120
|
}
|
|
123
121
|
|
|
124
122
|
export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRuntime {
|
|
125
|
-
const backgroundQueue = new BackgroundTaskQueue(
|
|
123
|
+
const backgroundQueue = new BackgroundTaskQueue(resolveSubagentConcurrency());
|
|
126
124
|
|
|
127
125
|
const runtime: SubagentRuntime = {
|
|
128
126
|
configPath,
|
|
@@ -142,7 +140,7 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
|
|
|
142
140
|
const active = monitor
|
|
143
141
|
.getRuns()
|
|
144
142
|
.filter((run) => isRunActiveStatus(run.status))
|
|
145
|
-
.map((run) => ({ id: run.id, agent: run.agent, label: run.label }));
|
|
143
|
+
.map((run) => ({ id: run.id, agent: run.agent, label: run.label, queued: run.status === "queued" }));
|
|
146
144
|
const message = {
|
|
147
145
|
customType: "subagent-result",
|
|
148
146
|
content: formatCompletionMessage(items) + formatActiveRunsFooter(active),
|
|
@@ -242,10 +240,14 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
|
|
|
242
240
|
Promise.allSettled(preflights),
|
|
243
241
|
runtime.backgroundQueue.waitForIdle(),
|
|
244
242
|
]);
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
243
|
+
// Only interrupted (parked) threads stay resumable across reloads:
|
|
244
|
+
// each keeps its durable record and retained artifacts. Settled
|
|
245
|
+
// threads drop their record — the manifest exists only while
|
|
246
|
+
// unfinished work needs it — and their sessions are deleted now. A
|
|
247
|
+
// thread whose settlement finished during the wait above already
|
|
248
|
+
// wrote (or removed) its own record; the lastResult-derived state
|
|
249
|
+
// below matches it.
|
|
250
|
+
const settledIds: number[] = [];
|
|
249
251
|
const records: ThreadRecord[] = [];
|
|
250
252
|
for (const thread of runtime.threads.values()) {
|
|
251
253
|
if (thread.retired) continue;
|
|
@@ -258,11 +260,13 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
|
|
|
258
260
|
} else {
|
|
259
261
|
state = "parked";
|
|
260
262
|
}
|
|
261
|
-
records.push(threadRecordFromThread(thread, state));
|
|
263
|
+
if (state === "parked") records.push(threadRecordFromThread(thread, state));
|
|
264
|
+
else settledIds.push(thread.id);
|
|
262
265
|
}
|
|
263
|
-
await Promise.all(
|
|
264
|
-
records.map((record) => upsertThreadRecord(runtime.configPath, record).catch(() => undefined)),
|
|
265
|
-
|
|
266
|
+
await Promise.all([
|
|
267
|
+
...records.map((record) => upsertThreadRecord(runtime.configPath, record).catch(() => undefined)),
|
|
268
|
+
...settledIds.map((runId) => removeThreadRecord(runtime.configPath, runId).catch(() => undefined)),
|
|
269
|
+
]);
|
|
266
270
|
// Retained-failure recovery records are persisted by the finalization
|
|
267
271
|
// itself; shutdown only drops sessions no record claims anymore.
|
|
268
272
|
const referenced = new Set(
|
package/src/setup.ts
CHANGED
|
@@ -307,9 +307,9 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
|
|
|
307
307
|
next.agentThinkingLevels.cleaner = config.agentThinkingLevels.reviewer;
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
|
-
// Documenter intentionally follows the faster explorer route.
|
|
311
|
-
//
|
|
312
|
-
// overrides instead of silently choosing a stronger model.
|
|
310
|
+
// Documenter intentionally follows the faster explorer route. When it
|
|
311
|
+
// is re-enabled after being explicitly disabled, it inherits any
|
|
312
|
+
// explorer overrides instead of silently choosing a stronger model.
|
|
313
313
|
if (!config.enabledAgents.includes("documenter") && enabled.includes("documenter")) {
|
|
314
314
|
if (!next.agentModels.documenter && config.agentModels.explorer) {
|
|
315
315
|
next.agentModels.documenter = config.agentModels.explorer;
|