@ferris1225/pi-subagents 4.1.9 → 4.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -507
- package/agents/cleaner.md +51 -51
- package/agents/documenter.md +39 -39
- package/agents/explorer.md +40 -40
- package/agents/reviewer.md +6 -7
- package/agents/worker.md +2 -2
- package/package.json +1 -1
- package/src/background.ts +6 -6
- package/src/config.ts +4 -2
- package/src/dispatch.ts +12 -88
- package/src/monitor.ts +1 -1
- package/src/prompt.ts +22 -40
- package/src/runtime.ts +3 -3
- package/src/setup.ts +25 -193
- package/src/thread-lifecycle.ts +54 -54
- package/src/{fixloop.ts → workflow.ts} +248 -378
package/src/prompt.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Builds the
|
|
3
|
-
* system prompt via `before_agent_start`.
|
|
4
|
-
*
|
|
2
|
+
* Builds the delegation directive injected into the parent model's
|
|
3
|
+
* system prompt via `before_agent_start`. It is paid on every turn, so it
|
|
4
|
+
* stays a lean routing contract — when a child context pays for itself,
|
|
5
|
+
* how wide to fan out, and how results come back. Tool metadata stays
|
|
6
|
+
* intentionally minimal so role/process guidance is not paid for twice.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import type { AgentConfig } from "./agents.ts";
|
|
@@ -22,13 +24,10 @@ export function buildDelegationDirective(
|
|
|
22
24
|
const hasCleaner = agents.some((agent) => agent.name === "cleaner");
|
|
23
25
|
const hasDocumenter = agents.some((agent) => agent.name === "documenter");
|
|
24
26
|
const hasReviewer = agents.some((agent) => agent.name === "reviewer");
|
|
25
|
-
const hasMultiple = agents.length > 1;
|
|
26
|
-
const autoFixEnabled = hasWorker;
|
|
27
27
|
const codeWriterNames = [
|
|
28
28
|
...(hasWorker ? ["worker"] : []),
|
|
29
29
|
...(hasCleaner ? ["cleaner"] : []),
|
|
30
30
|
];
|
|
31
|
-
const reviewedWriterNames = [...codeWriterNames];
|
|
32
31
|
const namedWorktreeTargets = [
|
|
33
32
|
...(hasWorker ? ["worker"] : []),
|
|
34
33
|
...(hasCleaner ? ["cleaner"] : []),
|
|
@@ -39,71 +38,54 @@ export function buildDelegationDirective(
|
|
|
39
38
|
: namedWorktreeTargets.length === 1
|
|
40
39
|
? `${namedWorktreeTargets[0]} or another`
|
|
41
40
|
: `${namedWorktreeTargets.slice(0, -1).join(", ")}, ${namedWorktreeTargets.at(-1)}, or another`;
|
|
42
|
-
const managedWriterWorkflowRule = reviewedWriterNames.length === 0
|
|
43
|
-
? undefined
|
|
44
|
-
: hasReviewer && hasDocumenter
|
|
45
|
-
? `Successful top-level ${reviewedWriterNames.join("/")} runs continue through the enabled reviewer gate. Only REVIEW_PASS can authorize documenter, which runs for DOCUMENTATION: NEEDED or a missing marker; the workflow delivers once. Never duplicate stages.`
|
|
46
|
-
: hasReviewer
|
|
47
|
-
? `Successful top-level ${reviewedWriterNames.join("/")} runs continue through the enabled reviewer gate and then deliver once; never duplicate the gate.`
|
|
48
|
-
: hasDocumenter
|
|
49
|
-
? `With reviewer disabled, successful top-level ${reviewedWriterNames.join("/")} runs use documenter as the conservative final fallback and then deliver once; never duplicate the fallback.`
|
|
50
|
-
: undefined;
|
|
51
41
|
|
|
52
42
|
const dispatchRules = [
|
|
53
|
-
"
|
|
43
|
+
"Route substantive work to sub-agents so your context stays lean for orchestration; inline only trivial work — a one-shot lookup, a single focused read/edit, or an answer already in context.",
|
|
54
44
|
...(hasExplorer
|
|
55
45
|
? [
|
|
56
|
-
"Use `explorer`
|
|
46
|
+
"Use `explorer` for any broad or multi-file search; it is a retrieval index, never a gate — re-read load-bearing files before acting on its findings.",
|
|
57
47
|
]
|
|
58
48
|
: []),
|
|
59
49
|
...(hasWorker
|
|
60
|
-
? ["Use `worker` for a self-contained implementation, fix, refactor, or test whose separate context pays for itself
|
|
50
|
+
? ["Use `worker` for a self-contained implementation, fix, refactor, or test whose separate context pays for itself."]
|
|
61
51
|
: []),
|
|
62
52
|
...(hasCleaner
|
|
63
53
|
? [
|
|
64
|
-
`Use \`cleaner\` only for user-authorized cleanup
|
|
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.`,
|
|
65
55
|
]
|
|
66
56
|
: []),
|
|
67
57
|
...(hasDocumenter
|
|
68
58
|
? [
|
|
69
|
-
`Use \`documenter\` directly only for explicit
|
|
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.` : ""}`,
|
|
70
60
|
]
|
|
71
61
|
: []),
|
|
72
62
|
...(hasReviewer
|
|
73
63
|
? [
|
|
74
|
-
`Use \`reviewer\` for read-only assessments or
|
|
64
|
+
`Use \`reviewer\` for read-only assessments or gates.${codeWriterNames.length > 0 ? ` Successful ${codeWriterNames.join("/")} runs already get one fresh gate and then deliver once.` : ""} Advisory output has no VERDICT and cannot authorize edits; dispatch your own re-verification with \`advisory: true\`.`,
|
|
75
65
|
]
|
|
76
66
|
: []),
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
|
|
80
|
-
? [
|
|
81
|
-
"Dispatch independent work in one `tasks` array and let the resumed main agent start dependent work only after prerequisites finish.",
|
|
82
|
-
]
|
|
83
|
-
: []),
|
|
84
|
-
`Filesystem isolation: single tasks default to shared${hasWorker ? "; parallel worker tasks default to detached Git worktrees" : ""}${hasCleaner ? "; cleaner defaults to shared" : ""}${hasDocumenter ? "; documenter defaults to shared" : ""}. Request \`isolation: "worktree"\` only for ${worktreeTargets} write-capable agent in a Git repository with committed HEAD. Read-only agents reject it, and setup/integration failure never falls back silently to shared.`,
|
|
67
|
+
"You own the fan-out breadth: every genuinely independent unit goes in one `tasks` array — there is no per-call cap, and extra tasks queue for the next free process slot. One child owns one coherent deliverable and its files; no two children share a file or re-answer the same question; dependent work starts only after its prerequisite delivers.",
|
|
68
|
+
"Brief each child with the complete goal, exact paths, constraints, and expected output; it has no conversation memory and cannot delegate. Continue a parked/settled thread with `subagent_control resume`.",
|
|
69
|
+
`Single tasks share the checkout${hasWorker ? "; parallel workers default to detached Git worktrees" : ""}. Request \`isolation: "worktree"\` only for ${worktreeTargets} write-capable agent in a Git repo with committed HEAD; setup failure never silently falls back to shared.`,
|
|
85
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.",
|
|
86
|
-
"Trust but verify: inspect actual changes/results before reporting completion.",
|
|
87
71
|
];
|
|
88
72
|
|
|
89
73
|
const handoffRules = [
|
|
90
|
-
"Dispatch ends this turn;
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"A delivered result does not mean siblings are finished. Before declaring the overall task done, use `subagent_status` to confirm that no runs remain active.",
|
|
74
|
+
"Dispatch ends this turn; each completion resumes the main agent automatically — never sleep, poll, or call `subagent_wait` to hold the turn.",
|
|
75
|
+
"Results are already shown; add only your conclusion or next action, never a restatement.",
|
|
76
|
+
"Before declaring the overall task done, use `subagent_status` to confirm that no runs remain active.",
|
|
94
77
|
];
|
|
95
78
|
|
|
96
79
|
const verificationRules = [
|
|
97
|
-
"Never report an unrun check as passed;
|
|
98
|
-
...(managedWriterWorkflowRule ? [managedWriterWorkflowRule] : []),
|
|
80
|
+
"Never report an unrun check as passed; surface unavailable checks and pre-existing failures honestly, and inspect actual changes before reporting completion.",
|
|
99
81
|
...(hasReviewer
|
|
100
82
|
? [
|
|
101
83
|
...(hasDocumenter
|
|
102
84
|
? [
|
|
103
|
-
|
|
85
|
+
"A direct REVIEW_PASS with DOCUMENTATION: CLEAN delivers immediately; NEEDED or a missing marker runs one documentation sync.",
|
|
104
86
|
]
|
|
105
87
|
: []),
|
|
106
|
-
"
|
|
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.",
|
|
107
89
|
"Use multi-model cross-review only when explicitly requested or for genuinely high-risk security, unsafe/FFI, persistence-migration, or concurrency changes.",
|
|
108
90
|
]
|
|
109
91
|
: []),
|
|
@@ -113,9 +95,9 @@ export function buildDelegationDirective(
|
|
|
113
95
|
return `
|
|
114
96
|
## Sub-agent delegation (pi-subagents)
|
|
115
97
|
|
|
116
|
-
The \`subagent\` tool
|
|
98
|
+
The \`subagent\` tool runs isolated leaf Pi child processes and context windows; each completion automatically resumes the main agent.
|
|
117
99
|
|
|
118
|
-
|
|
100
|
+
Agents:
|
|
119
101
|
${catalog}
|
|
120
102
|
|
|
121
103
|
Dispatch:
|
package/src/runtime.ts
CHANGED
|
@@ -50,7 +50,7 @@ export interface SubagentThread {
|
|
|
50
50
|
executionCwd: string;
|
|
51
51
|
thinkingLevel?: ThinkingLevel;
|
|
52
52
|
isolation: IsolationMode;
|
|
53
|
-
/** Report-only reviewer dispatch: verdicts never chain into
|
|
53
|
+
/** Report-only reviewer dispatch: verdicts never chain into a managed workflow. */
|
|
54
54
|
advisoryReview: boolean;
|
|
55
55
|
worktree?: WorktreeIsolation;
|
|
56
56
|
state: ThreadState;
|
|
@@ -137,8 +137,8 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
|
|
|
137
137
|
// Computing this at delivery (emit) time — not when the item was
|
|
138
138
|
// pushed — reflects the current monitor state, since finishing runs
|
|
139
139
|
// are removed from the monitor before their completion is pushed.
|
|
140
|
-
// Managed-workflow parents remain "running" through
|
|
141
|
-
//
|
|
140
|
+
// Managed-workflow parents remain "running" through reviewer and
|
|
141
|
+
// documenter stages, so they are included without a special case.
|
|
142
142
|
const active = monitor
|
|
143
143
|
.getRuns()
|
|
144
144
|
.filter((run) => isRunActiveStatus(run.status))
|
package/src/setup.ts
CHANGED
|
@@ -1,25 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Interactive configuration wizard for /subagents-setup.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The wizard stays one level deep and exposes only what most users touch:
|
|
5
|
+
* which agents run, the model each runs on, and the delegation directive
|
|
6
|
+
* toggle. Everything else (per-agent thinking, agent scope, idle timeout,
|
|
7
|
+
* result lines, notifications) is config-file-only; model failures hand
|
|
8
|
+
* directly to the current main model, and thinking defaults to capability-
|
|
9
|
+
* aware Auto.
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
import { stat } from "node:fs/promises";
|
|
11
|
-
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
12
13
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
13
14
|
import {
|
|
14
|
-
AGENT_SCOPE_VALUES,
|
|
15
15
|
BUILTIN_AGENT_NAMES,
|
|
16
16
|
DEFAULT_CONFIG,
|
|
17
17
|
DEFAULT_ENABLED_AGENTS,
|
|
18
|
-
DEFAULT_IDLE_TIMEOUT_SEC,
|
|
19
|
-
DEFAULT_THINKING_LEVEL,
|
|
20
|
-
type AgentScope,
|
|
21
18
|
type SubagentsConfig,
|
|
22
|
-
type ThinkingLevel,
|
|
23
19
|
errorMessage,
|
|
24
20
|
getConfigPath,
|
|
25
21
|
loadConfig,
|
|
@@ -31,28 +27,9 @@ import {
|
|
|
31
27
|
availableModelsInScope,
|
|
32
28
|
buildModelPickerItems,
|
|
33
29
|
currentModelRef,
|
|
34
|
-
findModelByRef,
|
|
35
30
|
modelRef,
|
|
36
|
-
resolveThinkingLevel,
|
|
37
|
-
supportedThinkingLevels,
|
|
38
31
|
} from "./models.ts";
|
|
39
32
|
import { promptSelectMany, promptSelectOne } from "./ui.ts";
|
|
40
|
-
import { discoverAgents } from "./agents.ts";
|
|
41
|
-
|
|
42
|
-
const AUTO_THINKING = "__auto_thinking__";
|
|
43
|
-
|
|
44
|
-
function actualAgentThinkingDefault(
|
|
45
|
-
ctx: ExtensionCommandContext,
|
|
46
|
-
config: SubagentsConfig,
|
|
47
|
-
agentName: string,
|
|
48
|
-
): ThinkingLevel {
|
|
49
|
-
const { agents } = discoverAgents(ctx.cwd, {
|
|
50
|
-
scope: config.agentScope,
|
|
51
|
-
enabledNames: config.enabledAgents,
|
|
52
|
-
projectTrusted: ctx.isProjectTrusted(),
|
|
53
|
-
});
|
|
54
|
-
return agents.find((agent) => agent.name === agentName)?.thinking ?? DEFAULT_THINKING_LEVEL;
|
|
55
|
-
}
|
|
56
33
|
|
|
57
34
|
/** Short, selection-friendly descriptions for the built-in agents. */
|
|
58
35
|
const MODULE_HINTS: Record<string, string> = {
|
|
@@ -121,61 +98,6 @@ async function pickAgentModel(
|
|
|
121
98
|
return pickConfiguredModel(ctx, `Model for "${agentName}"?`, currentRef, escNote);
|
|
122
99
|
}
|
|
123
100
|
|
|
124
|
-
const THINKING_LEVEL_HINTS: Record<ThinkingLevel, string> = {
|
|
125
|
-
off: "no reasoning tokens",
|
|
126
|
-
minimal: "minimal reasoning",
|
|
127
|
-
low: "light reasoning",
|
|
128
|
-
medium: "balanced reasoning",
|
|
129
|
-
high: "deep reasoning",
|
|
130
|
-
xhigh: "extra-deep reasoning",
|
|
131
|
-
max: "strongest reasoning",
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
function effectiveModelForChoice(
|
|
135
|
-
ctx: ExtensionCommandContext,
|
|
136
|
-
choice: string,
|
|
137
|
-
): Model<Api> | undefined {
|
|
138
|
-
if (choice === CURRENT_MAIN_MODEL) return ctx.model;
|
|
139
|
-
return findModelByRef(availableModelsInScope(ctx), choice);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Auto is the default. Manual rows are exactly the levels Pi exposes for the
|
|
143
|
-
* selected model; unsupported xhigh/max entries never appear. */
|
|
144
|
-
async function pickAgentStrength(
|
|
145
|
-
ctx: ExtensionCommandContext,
|
|
146
|
-
agentName: string,
|
|
147
|
-
model: Model<Api> | undefined,
|
|
148
|
-
current: ThinkingLevel | undefined,
|
|
149
|
-
agentDefault: ThinkingLevel,
|
|
150
|
-
escNote = "cancels this setup pass",
|
|
151
|
-
): Promise<ThinkingLevel | typeof AUTO_THINKING | undefined> {
|
|
152
|
-
const supported = supportedThinkingLevels(model);
|
|
153
|
-
const automatic = resolveThinkingLevel(model, agentDefault);
|
|
154
|
-
// No model metadata, or a non-reasoning model whose only valid value is off:
|
|
155
|
-
// Auto is already the complete and least surprising choice.
|
|
156
|
-
if (supported.length <= 1) return AUTO_THINKING;
|
|
157
|
-
|
|
158
|
-
const currentEffective = current ? resolveThinkingLevel(model, current) : undefined;
|
|
159
|
-
const modelName = model ? modelRef(model) : "current main model";
|
|
160
|
-
const options = [
|
|
161
|
-
{
|
|
162
|
-
value: AUTO_THINKING,
|
|
163
|
-
label: `auto — ${automatic} for ${modelName}${current === undefined ? " (current, recommended)" : " (recommended)"}`,
|
|
164
|
-
},
|
|
165
|
-
...supported.map((level) => ({
|
|
166
|
-
value: level,
|
|
167
|
-
label: `${level} — ${THINKING_LEVEL_HINTS[level]}${current !== undefined && currentEffective === level ? " (current)" : ""}`,
|
|
168
|
-
})),
|
|
169
|
-
];
|
|
170
|
-
return promptSelectOne(
|
|
171
|
-
ctx,
|
|
172
|
-
`Thinking for "${agentName}"?`,
|
|
173
|
-
`Only levels supported by ${modelName} are shown • Enter selects • Esc ${escNote}`,
|
|
174
|
-
options,
|
|
175
|
-
current === undefined ? AUTO_THINKING : currentEffective,
|
|
176
|
-
) as Promise<ThinkingLevel | typeof AUTO_THINKING | undefined>;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
101
|
async function pickAgentToConfigure(
|
|
180
102
|
ctx: ExtensionCommandContext,
|
|
181
103
|
enabledAgents: readonly string[],
|
|
@@ -195,11 +117,10 @@ async function pickAgentToConfigure(
|
|
|
195
117
|
interface ConfiguredAgentChoice {
|
|
196
118
|
name: string;
|
|
197
119
|
model: string;
|
|
198
|
-
strength: ThinkingLevel | typeof AUTO_THINKING;
|
|
199
120
|
}
|
|
200
121
|
|
|
201
|
-
/** Configure
|
|
202
|
-
* agent
|
|
122
|
+
/** Configure agents while preserving the UI back stack: model selection returns
|
|
123
|
+
* to the agent picker on Esc; agent-picker Esc ends this configuration pass. */
|
|
203
124
|
async function configureOneAgent(
|
|
204
125
|
ctx: ExtensionCommandContext,
|
|
205
126
|
config: SubagentsConfig,
|
|
@@ -207,27 +128,14 @@ async function configureOneAgent(
|
|
|
207
128
|
while (true) {
|
|
208
129
|
const name = await pickAgentToConfigure(ctx, config.enabledAgents);
|
|
209
130
|
if (name === undefined) return undefined;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (modelChoice === undefined) break;
|
|
219
|
-
const model = effectiveModelForChoice(ctx, modelChoice);
|
|
220
|
-
const strength = await pickAgentStrength(
|
|
221
|
-
ctx,
|
|
222
|
-
name,
|
|
223
|
-
model,
|
|
224
|
-
config.agentThinkingLevels[name],
|
|
225
|
-
actualAgentThinkingDefault(ctx, config, name),
|
|
226
|
-
"returns to model selection",
|
|
227
|
-
);
|
|
228
|
-
if (strength === undefined) continue;
|
|
229
|
-
return { name, model: modelChoice, strength };
|
|
230
|
-
}
|
|
131
|
+
const model = await pickAgentModel(
|
|
132
|
+
ctx,
|
|
133
|
+
name,
|
|
134
|
+
config.agentModels[name],
|
|
135
|
+
"returns to agent selection",
|
|
136
|
+
);
|
|
137
|
+
if (model === undefined) continue;
|
|
138
|
+
return { name, model };
|
|
231
139
|
}
|
|
232
140
|
}
|
|
233
141
|
|
|
@@ -239,40 +147,6 @@ async function pickInjection(ctx: ExtensionCommandContext, current: boolean): Pr
|
|
|
239
147
|
return choice.startsWith("On");
|
|
240
148
|
}
|
|
241
149
|
|
|
242
|
-
const IDLE_TIMEOUT_STEPS = [0, 30, 60, 90, 120, 180, 300, 600];
|
|
243
|
-
|
|
244
|
-
async function pickCount(
|
|
245
|
-
ctx: ExtensionCommandContext,
|
|
246
|
-
title: string,
|
|
247
|
-
steps: readonly number[],
|
|
248
|
-
current: number,
|
|
249
|
-
defaultValue: number,
|
|
250
|
-
): Promise<number | undefined> {
|
|
251
|
-
const values = [...new Set([...steps, current])].sort((a, b) => a - b);
|
|
252
|
-
const options = values.map((value) => {
|
|
253
|
-
const tags = [value === current ? "current" : "", value === defaultValue ? "default" : ""]
|
|
254
|
-
.filter(Boolean)
|
|
255
|
-
.join(", ");
|
|
256
|
-
return tags ? `${value} (${tags})` : String(value);
|
|
257
|
-
});
|
|
258
|
-
const choice = await ctx.ui.select(title, options);
|
|
259
|
-
return choice === undefined ? undefined : Number.parseInt(choice, 10);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async function pickScope(ctx: ExtensionCommandContext, current: AgentScope): Promise<AgentScope | undefined> {
|
|
263
|
-
const labels: Record<AgentScope, string> = {
|
|
264
|
-
user: "user — built-in + ~/.pi/agent/agents (default)",
|
|
265
|
-
project: "project — built-in + nearest .pi/agents only",
|
|
266
|
-
both: "both — user agents, overridden by project agents",
|
|
267
|
-
};
|
|
268
|
-
const options = AGENT_SCOPE_VALUES.map((scope) =>
|
|
269
|
-
scope === current ? `${labels[scope]} (current)` : labels[scope],
|
|
270
|
-
);
|
|
271
|
-
const choice = await ctx.ui.select("Which agent directories to discover from?", options);
|
|
272
|
-
if (choice === undefined) return undefined;
|
|
273
|
-
return AGENT_SCOPE_VALUES.find((scope) => choice.startsWith(scope));
|
|
274
|
-
}
|
|
275
|
-
|
|
276
150
|
function keepAgentEntries<T>(record: Record<string, T>, enabled: readonly string[]): Record<string, T> {
|
|
277
151
|
const keep = new Set(enabled);
|
|
278
152
|
return Object.fromEntries(Object.entries(record).filter(([name]) => keep.has(name)));
|
|
@@ -291,16 +165,6 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
291
165
|
|
|
292
166
|
const injection = await pickInjection(ctx, base.proactiveInjection);
|
|
293
167
|
if (injection === undefined) return false;
|
|
294
|
-
const scope = await pickScope(ctx, base.agentScope);
|
|
295
|
-
if (scope === undefined) return false;
|
|
296
|
-
const idleTimeoutSec = await pickCount(
|
|
297
|
-
ctx,
|
|
298
|
-
"Idle timeout in seconds? (0 = disabled)",
|
|
299
|
-
IDLE_TIMEOUT_STEPS,
|
|
300
|
-
base.idleTimeoutSec,
|
|
301
|
-
DEFAULT_IDLE_TIMEOUT_SEC,
|
|
302
|
-
);
|
|
303
|
-
if (idleTimeoutSec === undefined) return false;
|
|
304
168
|
|
|
305
169
|
const next: SubagentsConfig = {
|
|
306
170
|
enabledAgents: enabled,
|
|
@@ -310,49 +174,20 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
310
174
|
notifyOnReviewPass: base.notifyOnReviewPass,
|
|
311
175
|
maxResultLines: base.maxResultLines,
|
|
312
176
|
proactiveInjection: injection,
|
|
313
|
-
agentScope:
|
|
314
|
-
idleTimeoutSec,
|
|
177
|
+
agentScope: base.agentScope,
|
|
178
|
+
idleTimeoutSec: base.idleTimeoutSec,
|
|
315
179
|
};
|
|
316
180
|
await saveConfig(next, configPath);
|
|
317
181
|
ctx.ui.notify(`pi-subagents configured with Auto thinking. Saved to ${configPath}`, "info");
|
|
318
182
|
return true;
|
|
319
183
|
}
|
|
320
184
|
|
|
321
|
-
async function updateRuntimeSetting(
|
|
322
|
-
ctx: ExtensionCommandContext,
|
|
323
|
-
config: SubagentsConfig,
|
|
324
|
-
): Promise<SubagentsConfig | undefined> {
|
|
325
|
-
while (true) {
|
|
326
|
-
const choice = await ctx.ui.select("Runtime setting", [
|
|
327
|
-
"Proactive injection",
|
|
328
|
-
"Agent scope",
|
|
329
|
-
"Idle timeout",
|
|
330
|
-
]);
|
|
331
|
-
if (choice === undefined) return undefined;
|
|
332
|
-
const next = { ...config };
|
|
333
|
-
if (choice.startsWith("Proactive")) {
|
|
334
|
-
const value = await pickInjection(ctx, config.proactiveInjection);
|
|
335
|
-
if (value === undefined) continue;
|
|
336
|
-
next.proactiveInjection = value;
|
|
337
|
-
} else if (choice.startsWith("Agent scope")) {
|
|
338
|
-
const value = await pickScope(ctx, config.agentScope);
|
|
339
|
-
if (value === undefined) continue;
|
|
340
|
-
next.agentScope = value;
|
|
341
|
-
} else {
|
|
342
|
-
const value = await pickCount(ctx, "Idle timeout in seconds?", IDLE_TIMEOUT_STEPS, config.idleTimeoutSec, DEFAULT_IDLE_TIMEOUT_SEC);
|
|
343
|
-
if (value === undefined) continue;
|
|
344
|
-
next.idleTimeoutSec = value;
|
|
345
|
-
}
|
|
346
|
-
return next;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
185
|
async function runMenu(ctx: ExtensionCommandContext, configPath: string, config: SubagentsConfig): Promise<void> {
|
|
351
186
|
while (true) {
|
|
352
187
|
const choice = await ctx.ui.select("pi-subagents settings", [
|
|
353
188
|
"Enable/disable agents",
|
|
354
|
-
"Configure
|
|
355
|
-
"
|
|
189
|
+
"Configure agent models",
|
|
190
|
+
"Proactive injection",
|
|
356
191
|
"Full re-setup",
|
|
357
192
|
]);
|
|
358
193
|
if (choice === undefined) return;
|
|
@@ -395,17 +230,14 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
|
|
|
395
230
|
next.agentModels = keepAgentEntries(next.agentModels, enabled);
|
|
396
231
|
next.agentThinkingLevels = keepAgentEntries(next.agentThinkingLevels, enabled);
|
|
397
232
|
} else if (choice.startsWith("Configure")) {
|
|
398
|
-
// Per-agent loop:
|
|
399
|
-
//
|
|
400
|
-
// choices and returns to this settings menu.
|
|
233
|
+
// Per-agent loop: model Esc returns to the agent picker; agent-picker
|
|
234
|
+
// Esc saves completed choices and returns to this settings menu.
|
|
401
235
|
let configuredAny = false;
|
|
402
236
|
while (true) {
|
|
403
237
|
const picked = await configureOneAgent(ctx, next);
|
|
404
238
|
if (!picked) break;
|
|
405
239
|
configuredAny = true;
|
|
406
240
|
next.agentModels = applyAgentModelChoice(next.agentModels, picked.name, picked.model);
|
|
407
|
-
if (picked.strength === AUTO_THINKING) delete next.agentThinkingLevels[picked.name];
|
|
408
|
-
else next.agentThinkingLevels[picked.name] = picked.strength;
|
|
409
241
|
}
|
|
410
242
|
if (!configuredAny) continue;
|
|
411
243
|
await saveConfig(next, configPath);
|
|
@@ -413,9 +245,9 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
|
|
|
413
245
|
config = next;
|
|
414
246
|
continue;
|
|
415
247
|
} else {
|
|
416
|
-
const
|
|
417
|
-
if (
|
|
418
|
-
next =
|
|
248
|
+
const injection = await pickInjection(ctx, next.proactiveInjection);
|
|
249
|
+
if (injection === undefined) continue;
|
|
250
|
+
next.proactiveInjection = injection;
|
|
419
251
|
}
|
|
420
252
|
|
|
421
253
|
await saveConfig(next, configPath);
|
package/src/thread-lifecycle.ts
CHANGED
|
@@ -46,13 +46,12 @@ import {
|
|
|
46
46
|
} from "./format.ts";
|
|
47
47
|
import {
|
|
48
48
|
canStartManagedWorkflow,
|
|
49
|
-
formatChainSummary,
|
|
50
49
|
formatManagedWorkflowSummary,
|
|
51
50
|
getManagedWorkflowPlan,
|
|
52
51
|
workflowAgentAvailability,
|
|
53
52
|
type ManagedWorkflowOutcome,
|
|
54
53
|
type ManagedWorkflowPlan,
|
|
55
|
-
} from "./
|
|
54
|
+
} from "./workflow.ts";
|
|
56
55
|
import {
|
|
57
56
|
availableModelsInScope,
|
|
58
57
|
currentModelRef,
|
|
@@ -289,18 +288,23 @@ export interface ResumeReservation {
|
|
|
289
288
|
}
|
|
290
289
|
|
|
291
290
|
/** The dispatcher's full internal entry point; the public tool surface only
|
|
292
|
-
* uses the first
|
|
291
|
+
* uses the first four parameters. */
|
|
292
|
+
export interface StartBackgroundOptions {
|
|
293
|
+
/** Resume path only: the thread whose retained context continues. */
|
|
294
|
+
existingThread?: SubagentThread;
|
|
295
|
+
appendedObjectiveOnResume?: boolean;
|
|
296
|
+
environment?: DispatchEnvironment;
|
|
297
|
+
seed?: SessionSeed;
|
|
298
|
+
resumeReservation?: ResumeReservation;
|
|
299
|
+
advisoryReview?: boolean;
|
|
300
|
+
}
|
|
301
|
+
|
|
293
302
|
export type StartBackgroundInternal = (
|
|
294
303
|
agentName: string,
|
|
295
304
|
task: string,
|
|
296
305
|
cwd: string | undefined,
|
|
297
306
|
isolation?: IsolationMode,
|
|
298
|
-
|
|
299
|
-
appendedObjectiveOnResume?: boolean,
|
|
300
|
-
environment?: DispatchEnvironment,
|
|
301
|
-
seed?: SessionSeed,
|
|
302
|
-
resumeReservation?: ResumeReservation,
|
|
303
|
-
options?: { advisoryReview?: boolean },
|
|
307
|
+
options?: StartBackgroundOptions,
|
|
304
308
|
) => Promise<SingleResult>;
|
|
305
309
|
|
|
306
310
|
export interface ThreadLifecycleDeps {
|
|
@@ -398,13 +402,15 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
398
402
|
task: string,
|
|
399
403
|
cwd: string | undefined,
|
|
400
404
|
isolation: IsolationMode = "shared",
|
|
401
|
-
|
|
402
|
-
appendedObjectiveOnResume = false,
|
|
403
|
-
environment?: DispatchEnvironment,
|
|
404
|
-
seed?: SessionSeed,
|
|
405
|
-
resumeReservation?: ResumeReservation,
|
|
406
|
-
options?: { advisoryReview?: boolean },
|
|
405
|
+
startOptions: StartBackgroundOptions = {},
|
|
407
406
|
): Promise<SingleResult> => {
|
|
407
|
+
const {
|
|
408
|
+
existingThread,
|
|
409
|
+
appendedObjectiveOnResume = false,
|
|
410
|
+
environment,
|
|
411
|
+
seed,
|
|
412
|
+
resumeReservation,
|
|
413
|
+
} = startOptions;
|
|
408
414
|
if (!runtime.sessionActive) {
|
|
409
415
|
return failedStartResult(agentName, task, "Parent session shut down before this subagent generation could start.");
|
|
410
416
|
}
|
|
@@ -421,7 +427,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
421
427
|
const resolveLiveAgentTools = (candidate: AgentConfig): AgentConfig =>
|
|
422
428
|
resolveAgentTools({ ...candidate, tools: discoveredAgent.tools }, runtime.getActiveTools());
|
|
423
429
|
const resolvedAgent = resolveLiveAgentTools(discoveredAgent);
|
|
424
|
-
const advisoryReview =
|
|
430
|
+
const advisoryReview = startOptions.advisoryReview ?? existingThread?.advisoryReview ?? false;
|
|
425
431
|
const agent = agentName === "reviewer"
|
|
426
432
|
? advisoryReview
|
|
427
433
|
? withAdvisoryReviewContract(resolvedAgent)
|
|
@@ -471,9 +477,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
471
477
|
const priorTask = existingThread?.task;
|
|
472
478
|
const priorSessionId = seed?.sessionId ?? existingThread?.sessionId;
|
|
473
479
|
const priorSessionDir = seed?.sessionDir ?? existingThread?.sessionDir;
|
|
474
|
-
if (existingThread && resumeReservation && !ownsResumeReservation(runtime, existingThread, resumeReservation)) {
|
|
475
|
-
return failedStartResult(agentName, task, `Run #${existingThread.id} changed while resume was preparing; no new generation was started.`);
|
|
476
|
-
}
|
|
477
480
|
const runId = existingThread?.id ?? monitor.addRun(agent.name, task, route.agent.model, thinkingLevel, {
|
|
478
481
|
isolation,
|
|
479
482
|
...(worktreeGroup ? { worktreeId: worktreeGroup } : {}),
|
|
@@ -532,13 +535,13 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
532
535
|
// A newly admitted generation owns no output yet. Keeping the prior
|
|
533
536
|
// generation here would make a queued stop publish stale task,
|
|
534
537
|
// session metadata as this generation's partial.
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
538
|
+
thread.lastResult = undefined;
|
|
539
|
+
if (seed?.sessionId && seed.sessionDir) {
|
|
540
|
+
thread.sessionId = seed.sessionId;
|
|
541
|
+
thread.sessionDir = seed.sessionDir;
|
|
542
|
+
}
|
|
543
|
+
thread.retireOnSettle = false;
|
|
544
|
+
thread.isolationFailureNotified = false;
|
|
542
545
|
} else {
|
|
543
546
|
thread = {
|
|
544
547
|
id: runId,
|
|
@@ -674,23 +677,20 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
674
677
|
if (thread.retireOnSettle) runtime.retireThreadSession(thread);
|
|
675
678
|
let workflowOutcome: ManagedWorkflowOutcome | undefined;
|
|
676
679
|
const workflowPlan = getManagedWorkflowPlan(result, workflowAvailability, thread.advisoryReview);
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
runId,
|
|
692
|
-
workflowPlan.kind === "auto-fix" ? "auto-fix chain running" : "managed workflow running",
|
|
693
|
-
);
|
|
680
|
+
if (workflowPlan && runtime.sessionActive) {
|
|
681
|
+
// The continuation is runtime-initiated (gate review,
|
|
682
|
+
// documentation sync): release this generation's
|
|
683
|
+
// concurrency slot so managed chains never starve manual
|
|
684
|
+
// dispatches. Cancellation and quiescence guarantees are
|
|
685
|
+
// unchanged — the task stays abortable and awaited.
|
|
686
|
+
runtime.backgroundQueue.suspend(generationController);
|
|
687
|
+
thread.state = "running";
|
|
688
|
+
// The stable parent row now represents workflow ownership, not whichever
|
|
689
|
+
// model stage ran most recently. Internal rows own their exact role/model/
|
|
690
|
+
// thinking/timing telemetry and remain independently queryable.
|
|
691
|
+
monitor.setManagedWorkflow(runId, true);
|
|
692
|
+
monitor.setStatus(runId, "running");
|
|
693
|
+
monitor.setActivity(runId, "managed workflow running");
|
|
694
694
|
workflowOutcome = await runManagedWorkflow({
|
|
695
695
|
plan: workflowPlan,
|
|
696
696
|
initialResult: result,
|
|
@@ -793,9 +793,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
793
793
|
|
|
794
794
|
if (workflowOutcome) {
|
|
795
795
|
const lastStep = workflowOutcome.steps[workflowOutcome.steps.length - 1]!;
|
|
796
|
-
let block = workflowOutcome.
|
|
797
|
-
? formatChainSummary(workflowOutcome.steps, result)
|
|
798
|
-
: formatManagedWorkflowSummary(workflowOutcome.steps, result);
|
|
796
|
+
let block = formatManagedWorkflowSummary(workflowOutcome.steps, result);
|
|
799
797
|
const finalVerdict = lastStep.result.agent === "reviewer"
|
|
800
798
|
? reviewVerdict(getResultOutput(lastStep.result))
|
|
801
799
|
: undefined;
|
|
@@ -805,7 +803,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
805
803
|
}
|
|
806
804
|
if (modelLevel) block += `\n\n${modelLevelTakeoverNote(result, { runId })}`;
|
|
807
805
|
runtime.sendCompletionGroup([{
|
|
808
|
-
agent:
|
|
806
|
+
agent: `managed workflow (${result.agent})`,
|
|
809
807
|
block,
|
|
810
808
|
triggerTurn: true,
|
|
811
809
|
usage: sumUsage(workflowOutcome.steps.map((step) => step.result.usage)),
|
|
@@ -1197,15 +1195,17 @@ export function installThreadLifecycle(thread: SubagentThread, deps: ThreadLifec
|
|
|
1197
1195
|
nextTask,
|
|
1198
1196
|
thread.cwd,
|
|
1199
1197
|
thread.isolation,
|
|
1200
|
-
thread,
|
|
1201
|
-
objective !== undefined,
|
|
1202
1198
|
{
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1199
|
+
existingThread: thread,
|
|
1200
|
+
appendedObjectiveOnResume: objective !== undefined,
|
|
1201
|
+
environment: {
|
|
1202
|
+
ctx: currentCtx,
|
|
1203
|
+
config: currentConfig,
|
|
1204
|
+
agents: currentAgents,
|
|
1205
|
+
},
|
|
1206
|
+
seed,
|
|
1207
|
+
resumeReservation: reservation,
|
|
1206
1208
|
},
|
|
1207
|
-
seed,
|
|
1208
|
-
reservation,
|
|
1209
1209
|
);
|
|
1210
1210
|
if (pending.exitCode !== -1) {
|
|
1211
1211
|
if (clonedSession) {
|