pi-subagents 0.65.1 → 0.67.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/CHANGELOG.md +123 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/researcher.md +23 -13
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +20 -3
- package/docs/configuration.md +25 -5
- package/docs/extension-api.md +124 -18
- package/docs/missions.md +8 -0
- package/docs/models.md +59 -2
- package/docs/observability.md +46 -6
- package/docs/standalone-background.md +49 -0
- package/docs/tool-reference.md +20 -10
- package/docs/watchdog.md +35 -4
- package/docs/workflows.md +40 -19
- package/inspector-runner.mjs +2 -2
- package/package.json +2 -1
- package/prompts/parallel-review.md +1 -1
- package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
- package/skills/pi-subagents/SKILL.md +14 -0
- package/skills/pi-subagents/references/execution-controls.md +20 -5
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/advertised-agent-prompt.ts +94 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +14 -0
- package/src/agents/builtin-names.ts +1 -0
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +76 -45
- package/src/api/shared-types.ts +3 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/fanout-child.ts +63 -4
- package/src/extension/index.ts +58 -8
- package/src/extension/public-execution.ts +4 -3
- package/src/extension/rpc.ts +8 -21
- package/src/extension/schemas.ts +71 -80
- package/src/extension/tool-description.ts +29 -81
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +104 -67
- package/src/runs/background/active-async-capacity.ts +22 -18
- package/src/runs/background/async-execution.ts +45 -56
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-resume.ts +5 -9
- package/src/runs/background/async-status-snapshot.ts +10 -12
- package/src/runs/background/async-status.ts +17 -9
- package/src/runs/background/auto-drain.ts +44 -30
- package/src/runs/background/binary-bootstrap.ts +33 -0
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +117 -13
- package/src/runs/background/owned-process-tree.ts +35 -8
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/run-child-session.ts +121 -36
- package/src/runs/background/run-status.ts +78 -5
- package/src/runs/background/runner-aliases.ts +28 -9
- package/src/runs/background/runner-child-launch.ts +88 -0
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/scheduled-runs.ts +40 -13
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +458 -239
- package/src/runs/background/subagent-wait.ts +54 -8
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/background/wait-tool.ts +1 -1
- package/src/runs/foreground/async-steering-action.ts +37 -7
- package/src/runs/foreground/execution.ts +145 -56
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +584 -297
- package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
- package/src/runs/shared/acceptance.ts +7 -4
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +51 -47
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +167 -3
- package/src/runs/shared/child-launch.ts +28 -13
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +3 -1
- package/src/runs/shared/child-session.ts +75 -8
- package/src/runs/shared/child-tool-plan.ts +124 -5
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +6 -3
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +21 -11
- package/src/runs/shared/model-fallback.ts +12 -6
- package/src/runs/shared/nested-events.ts +5 -5
- package/src/runs/shared/orca-progress-tabs.ts +7 -1
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +2 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/src/runs/shared/run-fanout-budget.ts +8 -8
- package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +366 -208
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +65 -2
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/types.ts +85 -61
- package/src/shared/utils.ts +7 -2
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -1
- package/src/slash/subagents-admin.ts +11 -4
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +27 -19
- package/src/tui/render.ts +172 -33
- package/src/watchdog/child-status.ts +8 -0
- package/src/watchdog/model-selection.ts +20 -0
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +1 -0
- package/src/watchdog/register-main.ts +31 -27
- package/src/watchdog/review.ts +132 -67
- package/src/watchdog/runtime.ts +82 -20
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +9 -3
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +4 -0
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +275 -17
- package/src/workflows/workflow-checklist.ts +13 -17
- package/src/workflows/workflow-child-summary.ts +57 -8
- package/src/workflows/workflow-preflight.ts +19 -19
- package/src/workflows/workflow-receipt.ts +3 -3
- package/src/workflows/workflow-resources.ts +96 -21
- package/src/workflows/workflow-settlement.ts +3 -0
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -3,7 +3,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
3
3
|
import { resolveEffectiveThinking, splitKnownThinkingSuffix, THINKING_LEVELS, type ThinkingLevel } from "../shared/model-info.ts";
|
|
4
4
|
import { SLASH_TEXT_RESULT_TYPE } from "../shared/types.ts";
|
|
5
5
|
import { captureWatchdogDiffBaseline, type WatchdogDiffBaseline } from "./diff-tool.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { formatWatchdogRecommendation, recommendWatchdogModel, resolveWatchdogModelInput, parseWatchdogThinkingInput } from "./model-selection.ts";
|
|
7
7
|
import { renderWatchdogWarning } from "./render.ts";
|
|
8
8
|
import { createMainWatchdogReview } from "./review.ts";
|
|
9
9
|
import { MainWatchdogRuntime, type WatchdogReviewFunction } from "./runtime.ts";
|
|
@@ -63,11 +63,16 @@ function mainThinkingLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function mainModelLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>, ctx: ExtensionContext): string {
|
|
66
|
+
const fallbacks = fallbackLine(snapshot.config.main.fallbackModels);
|
|
66
67
|
if (snapshot.config.main.model) {
|
|
67
68
|
const source = snapshot.sessionModelOverride?.model ? "session override" : "configured";
|
|
68
|
-
return `Main model: ${splitKnownThinkingSuffix(snapshot.config.main.model).baseModel} (${source})`;
|
|
69
|
+
return `Main model: ${splitKnownThinkingSuffix(snapshot.config.main.model).baseModel} (${source})${fallbacks}`;
|
|
69
70
|
}
|
|
70
|
-
return `Main model: ${currentSessionModelLine(ctx)}`;
|
|
71
|
+
return `Main model: ${currentSessionModelLine(ctx)}${fallbacks}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function fallbackLine(models: string[] | undefined): string {
|
|
75
|
+
return models === undefined ? "" : ` · fallbacks ${models.length ? models.join(" → ") : "none"}`;
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
function childrenLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>): string {
|
|
@@ -80,19 +85,19 @@ function childrenLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>):
|
|
|
80
85
|
const bits = [agent];
|
|
81
86
|
if (override.enabled !== undefined) bits.push(boolLabel(override.enabled));
|
|
82
87
|
if (override.model) bits.push(splitKnownThinkingSuffix(override.model).baseModel);
|
|
88
|
+
if (override.fallbackModels !== undefined) bits.push(fallbackLine(override.fallbackModels));
|
|
83
89
|
if (override.thinking !== undefined) bits.push(`thinking ${override.thinking === false ? "off" : override.thinking}`);
|
|
84
90
|
return bits.join(" ");
|
|
85
91
|
}).join("; ")}`
|
|
86
92
|
: "";
|
|
87
|
-
return `Children: ${boolLabel(snapshot.config.enabled && children.enabled)} · model ${model} · thinking ${thinking}${overrideText}`;
|
|
93
|
+
return `Children: ${boolLabel(snapshot.config.enabled && children.enabled)} · model ${model}${fallbackLine(children.fallbackModels)} · thinking ${thinking}${overrideText}`;
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
function recommendationLine(ctx: ExtensionContext): string {
|
|
96
|
+
function recommendationLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>, ctx: ExtensionContext): string {
|
|
91
97
|
try {
|
|
92
|
-
|
|
93
|
-
return `Recommended strong watchdog: ${recommendation.model}:${recommendation.thinking} (${recommendation.label}, complementary reviewer)`;
|
|
98
|
+
return formatWatchdogRecommendation(recommendWatchdogModel(ctx, snapshot.config.main));
|
|
94
99
|
} catch (error) {
|
|
95
|
-
return `
|
|
100
|
+
return `Watchdog recommendation unavailable (${messageFromError(error)})`;
|
|
96
101
|
}
|
|
97
102
|
}
|
|
98
103
|
|
|
@@ -111,7 +116,8 @@ export function buildWatchdogStatus(snapshot: ReturnType<MainWatchdogRuntime["ge
|
|
|
111
116
|
"Subagent watchdog",
|
|
112
117
|
`Main: ${boolLabel(snapshot.enabled)}${!snapshot.config.enabled && snapshot.sessionOverride === undefined ? " (default off)" : ""}`,
|
|
113
118
|
`Runtime: ${statusLabel(snapshot.status)}${snapshot.bufferedDeltas > 0 ? ` · buffered deltas ${snapshot.bufferedDeltas}` : ""}`,
|
|
114
|
-
`Review trigger: ${snapshot.reviewTrigger === "repo-edits" ? "repo edits only" : "every non-empty turn delta"}`,
|
|
119
|
+
`Review trigger: ${snapshot.reviewTrigger === "repo-edits" ? snapshot.config.clarification ? "repo edits + bounded main orchestration activity" : "repo edits only" : "every non-empty turn delta"}`,
|
|
120
|
+
`Main clarification: ${snapshot.config.clarification ? "on" : "off"}`,
|
|
115
121
|
`Scope context: ${snapshot.config.scope.enabled ? "on" : "off"}`,
|
|
116
122
|
`Cadence: ${snapshot.config.cadence.everyNTools === null ? "boundary only" : `every ${snapshot.config.cadence.everyNTools} tools + boundary`}`,
|
|
117
123
|
lspLine(snapshot),
|
|
@@ -119,7 +125,7 @@ export function buildWatchdogStatus(snapshot: ReturnType<MainWatchdogRuntime["ge
|
|
|
119
125
|
mainModelLine(snapshot, ctx),
|
|
120
126
|
`Main thinking: ${mainThinkingLine(snapshot, ctx)}`,
|
|
121
127
|
childrenLine(snapshot),
|
|
122
|
-
recommendationLine(ctx),
|
|
128
|
+
recommendationLine(snapshot, ctx),
|
|
123
129
|
`Agent-end timeout: ${snapshot.config.agentEndTimeoutMs}ms`,
|
|
124
130
|
`Stalemate: ${snapshot.boundaryRepeats}/${snapshot.config.stalemateRepeats}${snapshot.stalemate ? " · stopped" : ""}`,
|
|
125
131
|
`Rules: ${snapshot.config.rules ? `${Object.keys(snapshot.config.rules.roleModels).length} role models · ${snapshot.config.rules.action}` : "none"}`,
|
|
@@ -145,7 +151,7 @@ export function buildWatchdogStatus(snapshot: ReturnType<MainWatchdogRuntime["ge
|
|
|
145
151
|
"",
|
|
146
152
|
"Model commands:",
|
|
147
153
|
"- /subagents-watchdog recommend-model",
|
|
148
|
-
"- /subagents-watchdog model recommended",
|
|
154
|
+
"- /subagents-watchdog model recommended (saves to user settings, not project settings)",
|
|
149
155
|
"- /subagents-watchdog model <provider/model[:thinking]>",
|
|
150
156
|
"- /subagents-watchdog model inherit",
|
|
151
157
|
"- /subagents-watchdog session model recommended",
|
|
@@ -171,12 +177,12 @@ function parseThinkingCommand(raw: string): ThinkingLevel | false | null {
|
|
|
171
177
|
return parseWatchdogThinkingInput(value, "/subagents-watchdog thinking") ?? null;
|
|
172
178
|
}
|
|
173
179
|
|
|
174
|
-
function resolveModelCommandValue(ctx: ExtensionCommandContext, raw: string): { model: string | null; thinking: ThinkingLevel | false | null; description: string } {
|
|
180
|
+
function resolveModelCommandValue(runtime: MainWatchdogRuntime, ctx: ExtensionCommandContext, raw: string): { model: string | null; thinking: ThinkingLevel | false | null; description: string } {
|
|
175
181
|
const value = raw.trim();
|
|
176
182
|
if (!value) throw new Error("Expected a model, 'recommended', or 'inherit'.");
|
|
177
183
|
if (value === "inherit") return { model: null, thinking: null, description: "current session model and thinking" };
|
|
178
184
|
if (value === "recommended") {
|
|
179
|
-
const recommendation =
|
|
185
|
+
const recommendation = recommendWatchdogModel(ctx, runtime.getSnapshot(ctx.cwd).config.main);
|
|
180
186
|
return {
|
|
181
187
|
model: recommendation.model,
|
|
182
188
|
thinking: recommendation.thinking,
|
|
@@ -191,18 +197,18 @@ function resolveModelCommandValue(ctx: ExtensionCommandContext, raw: string): {
|
|
|
191
197
|
};
|
|
192
198
|
}
|
|
193
199
|
|
|
194
|
-
function buildRecommendationText(ctx: ExtensionCommandContext): string {
|
|
195
|
-
const recommendation =
|
|
200
|
+
function buildRecommendationText(runtime: MainWatchdogRuntime, ctx: ExtensionCommandContext): string {
|
|
201
|
+
const recommendation = recommendWatchdogModel(ctx, runtime.getSnapshot(ctx.cwd).config.main);
|
|
196
202
|
return [
|
|
197
203
|
"Subagent watchdog recommended model",
|
|
198
204
|
`Current session: ${currentSessionModelLine(ctx as ExtensionContext)}`,
|
|
199
|
-
`Recommended: ${recommendation.model}:${recommendation.thinking}`,
|
|
205
|
+
recommendation.source === "configured" ? formatWatchdogRecommendation(recommendation) : `Recommended: ${recommendation.model}:${recommendation.thinking}`,
|
|
200
206
|
`Reason: ${recommendation.reason}`,
|
|
201
207
|
"",
|
|
202
208
|
"Apply for this session:",
|
|
203
209
|
"/subagents-watchdog session model recommended",
|
|
204
210
|
"",
|
|
205
|
-
"Save as your user default:",
|
|
211
|
+
"Save as your user default (affects other projects without overrides; does not change project settings):",
|
|
206
212
|
"/subagents-watchdog model recommended",
|
|
207
213
|
].join("\n");
|
|
208
214
|
}
|
|
@@ -221,12 +227,7 @@ function buildCheckText(runtime: MainWatchdogRuntime, ctx: ExtensionCommandConte
|
|
|
221
227
|
}
|
|
222
228
|
lines.push(`Main thinking: ${mainThinkingLine(snapshot, ctx as ExtensionContext)}`);
|
|
223
229
|
lines.push(lspLine(snapshot));
|
|
224
|
-
|
|
225
|
-
const recommendation = recommendStrongWatchdogModel(ctx as ExtensionContext);
|
|
226
|
-
lines.push(`Recommended strong watchdog: ${recommendation.model}:${recommendation.thinking}`);
|
|
227
|
-
} catch (error) {
|
|
228
|
-
lines.push(`Recommended strong watchdog: unavailable (${messageFromError(error)})`);
|
|
229
|
-
}
|
|
230
|
+
lines.push(recommendationLine(snapshot, ctx));
|
|
230
231
|
return lines.join("\n");
|
|
231
232
|
}
|
|
232
233
|
|
|
@@ -258,7 +259,7 @@ async function handleWatchdogCommand(
|
|
|
258
259
|
}
|
|
259
260
|
if (input === "recommend-model") {
|
|
260
261
|
try {
|
|
261
|
-
sendSlashText(pi, buildRecommendationText(ctx));
|
|
262
|
+
sendSlashText(pi, buildRecommendationText(runtime, ctx));
|
|
262
263
|
} catch (error) {
|
|
263
264
|
sendSlashText(pi, `Subagent watchdog recommended model\n\n${messageFromError(error)}`);
|
|
264
265
|
}
|
|
@@ -301,7 +302,7 @@ async function handleWatchdogCommand(
|
|
|
301
302
|
if (input.startsWith("session model ")) {
|
|
302
303
|
const rawModel = input.slice("session model ".length);
|
|
303
304
|
try {
|
|
304
|
-
const value = resolveModelCommandValue(ctx, rawModel);
|
|
305
|
+
const value = resolveModelCommandValue(runtime, ctx, rawModel);
|
|
305
306
|
const snapshot = value.model === null
|
|
306
307
|
? runtime.clearSessionModel(ctx.cwd)
|
|
307
308
|
: runtime.setSessionModel({ model: value.model, thinking: value.thinking ?? null }, ctx.cwd);
|
|
@@ -319,7 +320,7 @@ async function handleWatchdogCommand(
|
|
|
319
320
|
if (input.startsWith("model ")) {
|
|
320
321
|
const rawModel = input.slice("model ".length);
|
|
321
322
|
try {
|
|
322
|
-
const value = resolveModelCommandValue(ctx, rawModel);
|
|
323
|
+
const value = resolveModelCommandValue(runtime, ctx, rawModel);
|
|
323
324
|
const settingsPath = writeWatchdogModelSettings({
|
|
324
325
|
scope: "user",
|
|
325
326
|
target: { kind: "main" },
|
|
@@ -329,7 +330,7 @@ async function handleWatchdogCommand(
|
|
|
329
330
|
runtime.refreshConfig(ctx.cwd);
|
|
330
331
|
const snapshot = runtime.getSnapshot(ctx.cwd);
|
|
331
332
|
sendSlashText(pi, [
|
|
332
|
-
`Subagent watchdog model saved: ${value.description}.`,
|
|
333
|
+
`Subagent watchdog model saved to user settings: ${value.description}. Project and session overrides still take precedence.`,
|
|
333
334
|
`Updated: ${settingsPath}`,
|
|
334
335
|
`Main now: ${boolLabel(snapshot.enabled)}`,
|
|
335
336
|
value.model === null ? "The watchdog now inherits the current session model and thinking." : "Run /subagents-watchdog on if the watchdog is still off.",
|
|
@@ -387,6 +388,7 @@ export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatc
|
|
|
387
388
|
reviewDescription: options.review ? "injected seam" : "real model review",
|
|
388
389
|
reviewChangesOnly: true,
|
|
389
390
|
displayWarning: (details, options) => pi.sendMessage(createWatchdogWarningMessage(details, { display: true, details }), options),
|
|
391
|
+
displayClarification: (content) => pi.sendMessage({ customType: "subagent_watchdog_clarification", content, display: true }, { deliverAs: "steer", triggerTurn: true }),
|
|
390
392
|
});
|
|
391
393
|
|
|
392
394
|
pi.registerMessageRenderer<WatchdogWarningDetails>(SUBAGENT_WATCHDOG_WARNING_TYPE, (message, renderOptions, theme) => {
|
|
@@ -421,6 +423,8 @@ export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatc
|
|
|
421
423
|
rememberContext(ctx);
|
|
422
424
|
runtime.handleTurnEnd(event, ctx);
|
|
423
425
|
});
|
|
426
|
+
pi.on("input", (event) => { if (event.source !== "extension") runtime.handleUserInput(); });
|
|
427
|
+
pi.on("model_select", () => runtime.handleModelChange());
|
|
424
428
|
pi.on("tool_result", (_event, ctx) => {
|
|
425
429
|
rememberContext(ctx);
|
|
426
430
|
runtime.handleToolResult(ctx);
|
package/src/watchdog/review.ts
CHANGED
|
@@ -3,12 +3,13 @@ import { createReadOnlyTools, convertToLlm, type ExtensionContext } from "@earen
|
|
|
3
3
|
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
4
4
|
import type { Model, ProviderHeaders } from "@earendil-works/pi-ai";
|
|
5
5
|
import { Type, type Static } from "typebox";
|
|
6
|
-
import { resolveModelCandidate } from "../runs/shared/model-fallback.ts";
|
|
6
|
+
import { buildModelCandidates, isContextOverflow, isRetryableModelFailureAttempt, resolveModelCandidate } from "../runs/shared/model-fallback.ts";
|
|
7
7
|
import { agentStreamOptions } from "../shared/agent-stream-options.ts";
|
|
8
|
+
import { opencodeSessionHeaders } from "../shared/opencode-session-headers.ts";
|
|
8
9
|
import { resolveEffectiveThinking, splitKnownThinkingSuffix, THINKING_LEVELS, toModelInfo } from "../shared/model-info.ts";
|
|
9
10
|
import { createWatchdogDiffTool, WATCHDOG_DIFF_TOOL_NAME, type WatchdogDiffBaseline } from "./diff-tool.ts";
|
|
10
11
|
import { loadWatchdogGuidance } from "./guidance.ts";
|
|
11
|
-
import type
|
|
12
|
+
import { boundWatchdogReviewText, type WatchdogReviewFunction, type WatchdogReviewRequest } from "./runtime.ts";
|
|
12
13
|
import {
|
|
13
14
|
WATCHDOG_WARNING_CATEGORIES,
|
|
14
15
|
WATCHDOG_WARNING_CONFIDENCES,
|
|
@@ -22,6 +23,8 @@ import {
|
|
|
22
23
|
|
|
23
24
|
const WATCHDOG_ALLOWED_TOOL_NAMES = new Set(["read", "grep", "find", "ls", "watchdog_warn", WATCHDOG_DIFF_TOOL_NAME]);
|
|
24
25
|
|
|
26
|
+
const WatchdogAskParams = Type.Object({ question: Type.String(), evidence: Type.String() }, { additionalProperties: false });
|
|
27
|
+
|
|
25
28
|
const WatchdogWarnParams = Type.Object({
|
|
26
29
|
severity: Type.String({ enum: WATCHDOG_WARNING_SEVERITIES, description: "concern for actionable risk, blocker for a likely wrong or unsafe outcome" }),
|
|
27
30
|
summary: Type.String({ description: "One concise sentence naming the issue." }),
|
|
@@ -114,9 +117,16 @@ function resolveConfiguredModel(ctx: ExtensionContext, rawModel: string): { mode
|
|
|
114
117
|
return { model, modelString: resolved };
|
|
115
118
|
}
|
|
116
119
|
|
|
120
|
+
class WatchdogAuthError extends Error {}
|
|
121
|
+
|
|
117
122
|
async function resolveReviewAuth(ctx: ExtensionContext, model: RegistryModel): Promise<WatchdogReviewAuth> {
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
let auth;
|
|
124
|
+
try {
|
|
125
|
+
auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw new WatchdogAuthError(`Watchdog model auth failed for ${fullModelId(model)}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
128
|
+
}
|
|
129
|
+
if (auth.ok === false) throw new WatchdogAuthError(`Watchdog model auth failed for ${fullModelId(model)}: ${auth.error}`);
|
|
120
130
|
return {
|
|
121
131
|
...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
|
|
122
132
|
...(auth.headers ? { headers: auth.headers } : {}),
|
|
@@ -214,7 +224,7 @@ export function buildWatchdogSystemPrompt(ctx: Pick<ExtensionContext, "cwd">, op
|
|
|
214
224
|
"You are the main-session subagent watchdog for Pi.",
|
|
215
225
|
`Working directory: ${ctx.cwd}`,
|
|
216
226
|
"Review only the supplied parent turn delta. Inspect repository files only when needed to verify a concrete concern.",
|
|
217
|
-
options.hasScope ? "
|
|
227
|
+
options.hasScope ? "Use the Current scope record alongside supplied activity evidence; an unrelated user question does not cancel older authorized work." : undefined,
|
|
218
228
|
`You are read-only. You may use ${options.hasDiff ? "read, grep, find, ls, and watchdog_diff (the full repo diff since the session baseline; pass a path to narrow it)" : "read, grep, find, and ls"}. Do not edit files, run shell commands, spawn agents, or mutate state.`,
|
|
219
229
|
"Emit warnings only by calling watchdog_warn. Freeform assistant text is ignored and must not be used to report warnings.",
|
|
220
230
|
"Emit only medium/high confidence actionable concerns or blockers: missed user constraints, correctness risks, test gaps that matter, unsafe changes, stale facts, loop risks, or scope drift.",
|
|
@@ -236,18 +246,6 @@ function buildReviewPrompt(request: WatchdogReviewRequest, selection: WatchdogRe
|
|
|
236
246
|
].join("\n\n");
|
|
237
247
|
}
|
|
238
248
|
|
|
239
|
-
function finalStopReason(agent: Agent): "stop" | "error" | "aborted" | "length" {
|
|
240
|
-
for (let index = agent.state.messages.length - 1; index >= 0; index--) {
|
|
241
|
-
const message = agent.state.messages[index];
|
|
242
|
-
if (message && typeof message === "object" && (message as { role?: unknown }).role === "assistant") {
|
|
243
|
-
const stopReason = (message as { stopReason?: unknown }).stopReason;
|
|
244
|
-
if (stopReason === "error" || stopReason === "aborted" || stopReason === "length") return stopReason;
|
|
245
|
-
return "stop";
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return "stop";
|
|
249
|
-
}
|
|
250
|
-
|
|
251
249
|
function resolveContext(provider: WatchdogContextProvider): ExtensionContext | undefined {
|
|
252
250
|
return typeof provider === "function" ? provider() : provider;
|
|
253
251
|
}
|
|
@@ -256,59 +254,126 @@ export function createMainWatchdogReview(provider: WatchdogContextProvider, opti
|
|
|
256
254
|
return async (request) => {
|
|
257
255
|
const ctx = resolveContext(provider);
|
|
258
256
|
if (!ctx) throw new Error("Main watchdog review cannot run without an active Pi extension context.");
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
?
|
|
270
|
-
|
|
271
|
-
|
|
257
|
+
const aborted = () => ctx.signal?.aborted || request.signal?.aborted;
|
|
258
|
+
if (aborted()) return { stopReason: "aborted" };
|
|
259
|
+
const inherited = !request.config.main.model && ctx.model ? fullModelId(ctx.model) : undefined;
|
|
260
|
+
const candidates = request.config.main.fallbackModels?.length
|
|
261
|
+
? buildModelCandidates(request.config.main.model ?? inherited, request.config.main.fallbackModels,
|
|
262
|
+
ctx.modelRegistry.getAvailable().map(toModelInfo), ctx.model?.provider, { primaryModelFromParent: Boolean(inherited) })
|
|
263
|
+
: [request.config.main.model];
|
|
264
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
265
|
+
if (aborted()) return { stopReason: "aborted" };
|
|
266
|
+
const candidate = candidates[index];
|
|
267
|
+
const config = { ...request.config, main: { ...request.config.main, model: candidate === inherited ? undefined : candidate } };
|
|
268
|
+
try {
|
|
269
|
+
const attempt = await runWatchdogAttempt(ctx, { ...request, config }, options);
|
|
270
|
+
if (aborted()) return { stopReason: "aborted" };
|
|
271
|
+
if (!attempt.retryable || index === candidates.length - 1) return attempt.result;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (aborted()) return { stopReason: "aborted" };
|
|
274
|
+
if (!(error instanceof WatchdogAuthError)) throw error;
|
|
275
|
+
if (isContextOverflow(error.message) || !isRetryableModelFailureAttempt({ error: error.message }) || index === candidates.length - 1) throw error.cause ?? error;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
throw new Error("No usable watchdog model candidates.");
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function runWatchdogAttempt(ctx: ExtensionContext, request: WatchdogReviewRequest, options: CreateMainWatchdogReviewOptions): Promise<{
|
|
283
|
+
result: Awaited<ReturnType<WatchdogReviewFunction>>;
|
|
284
|
+
retryable?: boolean;
|
|
285
|
+
}> {
|
|
286
|
+
const selection = await resolveWatchdogReviewModel(ctx, request.config, {
|
|
287
|
+
currentThinkingLevel: options.getThinkingLevel?.(),
|
|
288
|
+
});
|
|
289
|
+
if (ctx.signal?.aborted || request.signal?.aborted) return { result: { stopReason: "aborted" } };
|
|
290
|
+
const auth = selection.auth;
|
|
291
|
+
const registeredProvider = (ctx.modelRegistry as {
|
|
292
|
+
getRegisteredProviderConfig?: (provider: string) => { api?: string; streamSimple?: StreamFn } | undefined;
|
|
293
|
+
}).getRegisteredProviderConfig?.(selection.model.provider);
|
|
294
|
+
const baseStreamFn = options.streamFn ?? (registeredProvider?.streamSimple && registeredProvider.api === selection.model.api
|
|
295
|
+
? registeredProvider.streamSimple
|
|
296
|
+
: streamSimple);
|
|
297
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
298
|
+
const streamFn: StreamFn = (model, context, streamOptions) => {
|
|
299
|
+
// Agent may enter one final loop iteration after an aborted mixed tool batch.
|
|
300
|
+
// Never send that iteration to the provider after an intentional yield.
|
|
301
|
+
if (clarification) throw new Error("Watchdog review yielded for clarification.");
|
|
302
|
+
return baseStreamFn(model, context, {
|
|
272
303
|
...streamOptions,
|
|
273
304
|
...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
|
|
274
305
|
env: auth.env || streamOptions?.env ? { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } : undefined,
|
|
275
|
-
headers: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
|
|
276
|
-
});
|
|
277
|
-
const diffBaseline = options.diffBaseline?.();
|
|
278
|
-
const tools = [
|
|
279
|
-
...(options.createReadOnlyTools ?? createReadOnlyTools)(ctx.cwd).filter((tool) => WATCHDOG_ALLOWED_TOOL_NAMES.has(tool.name) && tool.name !== "watchdog_warn"),
|
|
280
|
-
createWatchdogWarnTool(request),
|
|
281
|
-
...(diffBaseline ? [createWatchdogDiffTool(diffBaseline)] : []),
|
|
282
|
-
];
|
|
283
|
-
const agent = new Agent({
|
|
284
|
-
initialState: {
|
|
285
|
-
systemPrompt: buildWatchdogSystemPrompt(ctx, {
|
|
286
|
-
hasScope: request.hasScope,
|
|
287
|
-
guidance: loadWatchdogGuidance(ctx.cwd, request.config.guidance.watchdogMd),
|
|
288
|
-
hasDiff: diffBaseline !== undefined,
|
|
289
|
-
}),
|
|
290
|
-
model: selection.model,
|
|
291
|
-
thinkingLevel: selection.thinkingLevel,
|
|
292
|
-
tools,
|
|
293
|
-
},
|
|
294
|
-
convertToLlm,
|
|
295
|
-
...agentStreamOptions(streamFn),
|
|
296
|
-
getApiKey: (providerName) => providerName === selection.model.provider ? auth.apiKey : undefined,
|
|
297
|
-
beforeToolCall: async ({ toolCall }) => WATCHDOG_ALLOWED_TOOL_NAMES.has(toolCall.name)
|
|
298
|
-
? undefined
|
|
299
|
-
: { block: true, reason: `Watchdog reviews are read-only; tool '${toolCall.name}' is not allowed.` },
|
|
300
|
-
toolExecution: "sequential",
|
|
306
|
+
headers: { ...opencodeSessionHeaders(model, sessionId), ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
|
|
301
307
|
});
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
308
|
+
};
|
|
309
|
+
const diffBaseline = options.diffBaseline?.();
|
|
310
|
+
let clarification: { question: string; evidence: string } | undefined;
|
|
311
|
+
let warned = false;
|
|
312
|
+
let toolCount = 0;
|
|
313
|
+
const warnRequest = request.allowClarification ? { ...request, emitWarning: (warning: WatchdogWarning) => {
|
|
314
|
+
if (clarification) return false;
|
|
315
|
+
const accepted = request.emitWarning(warning);
|
|
316
|
+
warned ||= accepted;
|
|
317
|
+
return accepted;
|
|
318
|
+
} } : request;
|
|
319
|
+
const tools = [
|
|
320
|
+
...(options.createReadOnlyTools ?? createReadOnlyTools)(ctx.cwd).filter((tool) => WATCHDOG_ALLOWED_TOOL_NAMES.has(tool.name) && tool.name !== "watchdog_warn"),
|
|
321
|
+
createWatchdogWarnTool(warnRequest),
|
|
322
|
+
...(diffBaseline ? [createWatchdogDiffTool(diffBaseline)] : []),
|
|
323
|
+
];
|
|
324
|
+
if (request.allowClarification) tools.push({
|
|
325
|
+
name: "watchdog_ask",
|
|
326
|
+
label: "Watchdog clarification",
|
|
327
|
+
description: "Send one focused question when missing task status, intent or other orchestrator context prevents a concrete review judgment. Yields and ends this review so the orchestrator can handle the message and continue; no answer or follow-up review is required. Never ask for approval or permission, or after recording a warning. Question and evidence are capped at 1000 and 2000 characters.",
|
|
328
|
+
parameters: WatchdogAskParams,
|
|
329
|
+
executionMode: "sequential",
|
|
330
|
+
async execute(_id, rawParams) {
|
|
331
|
+
const params = rawParams as Static<typeof WatchdogAskParams>;
|
|
332
|
+
if (warned || clarification || ctx.signal?.aborted || request.signal?.aborted) throw new Error("Clarification unavailable after warning, yield, or cancellation.");
|
|
333
|
+
if (!params.question.trim() || !params.evidence.trim()) throw new Error("A focused question and concrete evidence are required.");
|
|
334
|
+
clarification = { question: boundWatchdogReviewText(params.question.trim(), 1_000), evidence: boundWatchdogReviewText(params.evidence.trim(), 2_000) };
|
|
335
|
+
agent.abort(); // Intentional yield also stops mixed tool batches; terminate alone does not.
|
|
336
|
+
return { content: [{ type: "text", text: "Review yielded for clarification." }], details: {} };
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
const agent = new Agent({
|
|
340
|
+
initialState: {
|
|
341
|
+
systemPrompt: buildWatchdogSystemPrompt(ctx, {
|
|
342
|
+
hasScope: request.hasScope,
|
|
343
|
+
guidance: loadWatchdogGuidance(ctx.cwd, request.config.guidance.watchdogMd),
|
|
344
|
+
hasDiff: diffBaseline !== undefined,
|
|
345
|
+
}),
|
|
346
|
+
model: selection.model,
|
|
347
|
+
thinkingLevel: selection.thinkingLevel,
|
|
348
|
+
tools,
|
|
349
|
+
},
|
|
350
|
+
convertToLlm,
|
|
351
|
+
...agentStreamOptions(streamFn),
|
|
352
|
+
getApiKey: (providerName) => providerName === selection.model.provider ? auth.apiKey : undefined,
|
|
353
|
+
beforeToolCall: async ({ toolCall }) => !clarification && (WATCHDOG_ALLOWED_TOOL_NAMES.has(toolCall.name) || (request.allowClarification && toolCall.name === "watchdog_ask"))
|
|
354
|
+
? undefined
|
|
355
|
+
: { block: true, reason: `Watchdog reviews are read-only; tool '${toolCall.name}' is not allowed.` },
|
|
356
|
+
toolExecution: "sequential",
|
|
357
|
+
});
|
|
358
|
+
// Include rejected/invalid calls as well as read-only work and findings.
|
|
359
|
+
agent.subscribe((event) => { if (event.type === "tool_execution_start") toolCount++; });
|
|
360
|
+
const abort = () => agent.abort();
|
|
361
|
+
ctx.signal?.addEventListener("abort", abort, { once: true });
|
|
362
|
+
request.signal?.addEventListener("abort", abort, { once: true });
|
|
363
|
+
try {
|
|
364
|
+
if (ctx.signal?.aborted || request.signal?.aborted) return { result: { stopReason: "aborted" } };
|
|
365
|
+
await agent.prompt(buildReviewPrompt(request, selection));
|
|
366
|
+
} finally {
|
|
367
|
+
ctx.signal?.removeEventListener("abort", abort);
|
|
368
|
+
request.signal?.removeEventListener("abort", abort);
|
|
369
|
+
}
|
|
370
|
+
if (ctx.signal?.aborted || request.signal?.aborted) return { result: { stopReason: "aborted" } };
|
|
371
|
+
const terminal = agent.state.messages.findLast((message) => message.role === "assistant");
|
|
372
|
+
const reason = terminal && "stopReason" in terminal ? terminal.stopReason : undefined;
|
|
373
|
+
const stopReason = reason === "error" || reason === "aborted" || reason === "length" ? reason : "stop";
|
|
374
|
+
const error = terminal && "errorMessage" in terminal && typeof terminal.errorMessage === "string" ? terminal.errorMessage : undefined;
|
|
375
|
+
return {
|
|
376
|
+
result: clarification ? { clarification } : { stopReason },
|
|
377
|
+
retryable: !clarification && stopReason === "error" && !isContextOverflow(error) && isRetryableModelFailureAttempt({ error, messages: agent.state.messages, toolCount }),
|
|
313
378
|
};
|
|
314
379
|
}
|