pi-subagents 0.66.0 → 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 +68 -0
- package/README.md +4 -3
- package/agents/evidence-auditor.md +34 -0
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +6 -3
- package/docs/configuration.md +7 -5
- package/docs/extension-api.md +33 -18
- package/docs/missions.md +8 -0
- package/docs/models.md +1 -1
- package/docs/observability.md +4 -4
- package/docs/standalone-background.md +49 -0
- package/docs/tool-reference.md +18 -8
- package/docs/watchdog.md +35 -4
- package/docs/workflows.md +26 -12
- package/inspector-runner.mjs +2 -2
- package/package.json +1 -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 +7 -5
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/advertised-agent-prompt.ts +34 -3
- package/src/agents/agents.ts +6 -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 +2 -0
- package/src/extension/fanout-child.ts +63 -4
- package/src/extension/index.ts +20 -8
- package/src/extension/public-execution.ts +4 -2
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +67 -78
- package/src/extension/tool-description.ts +29 -82
- 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 +22 -13
- package/src/runs/background/active-async-capacity.ts +4 -0
- package/src/runs/background/async-execution.ts +45 -56
- package/src/runs/background/async-resume.ts +5 -9
- package/src/runs/background/auto-drain.ts +6 -3
- package/src/runs/background/binary-bootstrap.ts +33 -0
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +31 -1
- package/src/runs/background/owned-process-tree.ts +29 -2
- package/src/runs/background/run-child-session.ts +61 -4
- package/src/runs/background/run-status.ts +3 -0
- package/src/runs/background/runner-aliases.ts +11 -3
- package/src/runs/background/runner-child-launch.ts +2 -0
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/scheduled-runs.ts +40 -13
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +47 -31
- package/src/runs/background/subagent-wait.ts +51 -8
- package/src/runs/background/wait-tool.ts +1 -1
- package/src/runs/foreground/async-steering-action.ts +18 -7
- package/src/runs/foreground/execution.ts +44 -31
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +110 -100
- package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
- package/src/runs/shared/acceptance.ts +5 -2
- package/src/runs/shared/async-status-projection.ts +4 -0
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +25 -10
- package/src/runs/shared/child-launch.ts +12 -2
- 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 +33 -2
- package/src/runs/shared/child-tool-plan.ts +122 -3
- package/src/runs/shared/completion-guard.ts +5 -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/llm-intent-arbiter.ts +12 -3
- package/src/runs/shared/model-fallback.ts +2 -0
- package/src/runs/shared/orca-progress-tabs.ts +1 -1
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +9 -3
- 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.ts +42 -12
- 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 +4 -1
- 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 +16 -14
- package/src/tui/render.ts +149 -28
- 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/scripted-workflow.ts +237 -7
- package/src/workflows/workflow-checklist.ts +2 -2
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
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
|
}
|
package/src/watchdog/runtime.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { ruleViolationWarning, type WatchdogRuleViolation } from "./rules.ts";
|
|
13
13
|
import { WatchdogScopeArtifact } from "./scope.ts";
|
|
14
14
|
import { resolveWatchdogConfig } from "./settings.ts";
|
|
15
|
-
import { formatWatchdogTurnDelta } from "./turn-delta.ts";
|
|
15
|
+
import { formatWatchdogOrchestrationActivity, formatWatchdogTurnDelta } from "./turn-delta.ts";
|
|
16
16
|
import {
|
|
17
17
|
type ResolvedWatchdogConfig,
|
|
18
18
|
type WatchdogEndpointConfig,
|
|
@@ -31,6 +31,7 @@ type ReviewStopReason = "stop" | "error" | "aborted" | "length";
|
|
|
31
31
|
export interface WatchdogReviewResult {
|
|
32
32
|
warnings?: WatchdogWarning[];
|
|
33
33
|
stopReason?: ReviewStopReason;
|
|
34
|
+
clarification?: { question: string; evidence: string };
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export interface WatchdogReviewRequest {
|
|
@@ -40,6 +41,7 @@ export interface WatchdogReviewRequest {
|
|
|
40
41
|
reviewId: number;
|
|
41
42
|
config: ResolvedWatchdogConfig;
|
|
42
43
|
emitWarning(warning: WatchdogWarning): boolean;
|
|
44
|
+
allowClarification?: boolean;
|
|
43
45
|
signal?: AbortSignal;
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -81,6 +83,8 @@ interface MainWatchdogRuntimeOptions {
|
|
|
81
83
|
review?: WatchdogReviewFunction;
|
|
82
84
|
reviewDescription?: string;
|
|
83
85
|
displayWarning?: (warning: WatchdogWarningDetails, options?: WatchdogWarningSendOptions) => void;
|
|
86
|
+
/** Supplying this main-session delivery capability gates clarification (children omit it). */
|
|
87
|
+
displayClarification?: (content: string) => void;
|
|
84
88
|
reviewChangesOnly?: boolean;
|
|
85
89
|
lspDiagnostics?: WatchdogLspDiagnosticsFunction;
|
|
86
90
|
repoChangeSignature?: typeof computeWatchdogRepoChangeSignature;
|
|
@@ -88,8 +92,8 @@ interface MainWatchdogRuntimeOptions {
|
|
|
88
92
|
|
|
89
93
|
export type WatchdogWarningSendOptions = { deliverAs: "steer" } | { triggerTurn: false };
|
|
90
94
|
|
|
91
|
-
type ContextLike = Pick<ExtensionContext, "cwd"
|
|
92
|
-
type ReviewDeltaOutcome = "completed" | "timeout" | "stale"
|
|
95
|
+
type ContextLike = Pick<ExtensionContext, "cwd"> & { signal?: AbortSignal };
|
|
96
|
+
type ReviewDeltaOutcome = "completed" | "timeout" | "stale" | NonNullable<WatchdogReviewResult["clarification"]>;
|
|
93
97
|
|
|
94
98
|
const DEFAULT_REVIEW: WatchdogReviewFunction = () => ({ warnings: [] });
|
|
95
99
|
const MAX_REVIEW_INPUT_CHARS = 24_000;
|
|
@@ -170,6 +174,11 @@ export class MainWatchdogRuntime {
|
|
|
170
174
|
private activeReviewAbortController: AbortController | undefined;
|
|
171
175
|
private failedReviews = 0;
|
|
172
176
|
private staleReviews = 0;
|
|
177
|
+
private readonly displayClarification: MainWatchdogRuntimeOptions["displayClarification"];
|
|
178
|
+
private askedThisPrompt = false;
|
|
179
|
+
private activityTail = "";
|
|
180
|
+
private activityPending = false;
|
|
181
|
+
private activityReviewUsed = false;
|
|
173
182
|
|
|
174
183
|
constructor(options: MainWatchdogRuntimeOptions = {}) {
|
|
175
184
|
this.cwd = options.cwd ?? process.cwd();
|
|
@@ -178,6 +187,7 @@ export class MainWatchdogRuntime {
|
|
|
178
187
|
this.reviewConnected = Boolean(options.review);
|
|
179
188
|
this.reviewDescription = options.reviewDescription ?? (options.review ? "injected seam" : "not wired");
|
|
180
189
|
this.displayWarning = options.displayWarning;
|
|
190
|
+
this.displayClarification = options.displayClarification;
|
|
181
191
|
this.reviewChangesOnly = options.reviewChangesOnly === true;
|
|
182
192
|
this.lspDiagnostics = options.lspDiagnostics ?? collectWatchdogLspDiagnostics;
|
|
183
193
|
this.repoChangeSignature = options.repoChangeSignature ?? computeWatchdogRepoChangeSignature;
|
|
@@ -200,6 +210,8 @@ export class MainWatchdogRuntime {
|
|
|
200
210
|
refreshConfig(cwd = this.cwd): WatchdogSettingsResult {
|
|
201
211
|
this.cwd = cwd;
|
|
202
212
|
const wasEnabled = this.isEnabled();
|
|
213
|
+
const hadClarification = this.configResult.config.clarification;
|
|
214
|
+
const previousMain = this.configResult.config.main;
|
|
203
215
|
const session = this.sessionOverrideEnabled === undefined && this.sessionModelOverride === undefined
|
|
204
216
|
? undefined
|
|
205
217
|
: {
|
|
@@ -215,6 +227,10 @@ export class MainWatchdogRuntime {
|
|
|
215
227
|
this.guard = new WatchdogEmissionGuard({ maxWarnings: this.guardMaxWarnings });
|
|
216
228
|
}
|
|
217
229
|
if (wasEnabled && !this.isEnabled()) this.invalidateActiveReview("watchdog disabled");
|
|
230
|
+
if (!this.isEnabled() || !this.configResult.config.clarification) this.clearActivity();
|
|
231
|
+
if (this.displayClarification && (hadClarification || this.configResult.config.clarification) && (this.reviewing || this.waitingAtAgentEnd)) {
|
|
232
|
+
if (!this.configResult.config.clarification || previousMain.model !== this.configResult.config.main.model || previousMain.thinking !== this.configResult.config.main.thinking) this.invalidateActiveReview("clarification configuration changed");
|
|
233
|
+
}
|
|
218
234
|
return this.configResult;
|
|
219
235
|
}
|
|
220
236
|
|
|
@@ -253,6 +269,7 @@ export class MainWatchdogRuntime {
|
|
|
253
269
|
}
|
|
254
270
|
|
|
255
271
|
reset(_reason = "reset", options: { clearReviewInputSignature?: boolean; resetChangeSignature?: boolean; clearLspLedger?: boolean; clearScope?: boolean } = {}): void {
|
|
272
|
+
this.activeReviewAbortController?.abort();
|
|
256
273
|
this.abortActiveAgentEnd();
|
|
257
274
|
this.epoch++;
|
|
258
275
|
this.status = "idle";
|
|
@@ -273,7 +290,10 @@ export class MainWatchdogRuntime {
|
|
|
273
290
|
this.lspLedger.reset();
|
|
274
291
|
this.lastLspSnapshot = undefined;
|
|
275
292
|
}
|
|
276
|
-
if (options.clearScope)
|
|
293
|
+
if (options.clearScope) {
|
|
294
|
+
this.scope.reset();
|
|
295
|
+
this.clearActivity();
|
|
296
|
+
}
|
|
277
297
|
if (options.clearReviewInputSignature) this.lastReviewInputSignature = undefined;
|
|
278
298
|
if (options.resetChangeSignature) this.resetRepoChangeBaseline({ reviewed: true });
|
|
279
299
|
this.guard.reset();
|
|
@@ -281,6 +301,8 @@ export class MainWatchdogRuntime {
|
|
|
281
301
|
}
|
|
282
302
|
|
|
283
303
|
dispose(): void {
|
|
304
|
+
this.clearActivity();
|
|
305
|
+
this.activeReviewAbortController?.abort();
|
|
284
306
|
this.disposed = true;
|
|
285
307
|
this.abortActiveAgentEnd();
|
|
286
308
|
this.epoch++;
|
|
@@ -305,6 +327,8 @@ export class MainWatchdogRuntime {
|
|
|
305
327
|
if (this.disposed) return;
|
|
306
328
|
const incomingPrompt = promptFromBeforeAgentStart(event);
|
|
307
329
|
this.reset("before_agent_start");
|
|
330
|
+
this.askedThisPrompt = false;
|
|
331
|
+
this.activityReviewUsed = false;
|
|
308
332
|
this.resetBoundaryRepeats();
|
|
309
333
|
this.refreshConfig(ctx.cwd);
|
|
310
334
|
this.userPrompt = incomingPrompt;
|
|
@@ -322,6 +346,13 @@ export class MainWatchdogRuntime {
|
|
|
322
346
|
this.refreshConfig(ctx.cwd);
|
|
323
347
|
if (!this.isEnabled()) return;
|
|
324
348
|
try {
|
|
349
|
+
if (this.displayClarification && this.configResult.config.clarification && this.boundaryRepeats === 0) {
|
|
350
|
+
const activity = formatWatchdogOrchestrationActivity(event);
|
|
351
|
+
if (activity) {
|
|
352
|
+
this.activityTail = [this.activityTail, boundWatchdogReviewText(activity, 3_000)].filter(Boolean).join(REVIEW_DELTA_SEPARATOR).slice(-6_000);
|
|
353
|
+
this.activityPending = true;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
325
356
|
this.observedRepoEditThisTurn ||= eventIndicatesRepoEdit(event);
|
|
326
357
|
const delta = formatWatchdogTurnDelta({
|
|
327
358
|
includeUserPrompt: this.includeUserPromptInNextDelta,
|
|
@@ -359,20 +390,19 @@ export class MainWatchdogRuntime {
|
|
|
359
390
|
if (this.disposed) return;
|
|
360
391
|
this.refreshConfig(ctx.cwd);
|
|
361
392
|
if (!this.isEnabled()) return;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
if (changeSignature && changeSignature.key === this.lastReviewedChangeSignature) {
|
|
393
|
+
if (ctx.signal?.aborted) return;
|
|
394
|
+
const changeSignature = this.resolveReviewChangeSignature(this.currentRepoChangeSignature(ctx.cwd), ctx.cwd);
|
|
395
|
+
const activityReview = Boolean(this.displayClarification && this.configResult.config.clarification && !this.activityReviewUsed && this.boundaryRepeats === 0 && this.activityPending);
|
|
396
|
+
const knownEvidence = changeSignature && changeSignature.key === this.lastReviewedChangeSignature;
|
|
397
|
+
if (!activityReview && (knownEvidence || (this.reviewChangesOnly && !changeSignature))) {
|
|
370
398
|
this.clearPendingDeltas();
|
|
371
|
-
this.status = "idle";
|
|
399
|
+
if (knownEvidence || this.status === "queued") this.status = "idle";
|
|
372
400
|
this.resolveWaiters(true);
|
|
373
401
|
return;
|
|
374
402
|
}
|
|
375
403
|
this.cancelMidRunReview();
|
|
404
|
+
if (activityReview && (!changeSignature || knownEvidence)) this.activityReviewUsed = true;
|
|
405
|
+
this.activityPending = false;
|
|
376
406
|
this.waitingAtAgentEnd = true;
|
|
377
407
|
const agentEndEpoch = this.epoch;
|
|
378
408
|
const agentEndId = ++this.agentEndIdCounter;
|
|
@@ -403,7 +433,8 @@ export class MainWatchdogRuntime {
|
|
|
403
433
|
this.resolveWaiters(true);
|
|
404
434
|
return;
|
|
405
435
|
}
|
|
406
|
-
const outcome = await this.reviewDelta(delta, this.configResult.config.agentEndTimeoutMs);
|
|
436
|
+
const outcome = await this.reviewDelta(delta, this.configResult.config.agentEndTimeoutMs, { allowClarification: true });
|
|
437
|
+
if (!this.isAgentEndCurrent(agentEndEpoch, agentEndId)) return;
|
|
407
438
|
this.waitingAtAgentEnd = false;
|
|
408
439
|
if (outcome === "timeout") {
|
|
409
440
|
this.staleReviews++;
|
|
@@ -413,11 +444,16 @@ export class MainWatchdogRuntime {
|
|
|
413
444
|
this.resolveWaiters(true);
|
|
414
445
|
return;
|
|
415
446
|
}
|
|
416
|
-
if (outcome === "completed" && this.status !== "failed" && this.status !== "stale") {
|
|
447
|
+
if ((outcome === "completed" || typeof outcome === "object") && this.status !== "failed" && this.status !== "stale") {
|
|
448
|
+
// A question consumes this evidence just like a completed review, not a pending exchange.
|
|
417
449
|
this.lastReviewInputSignature = signature;
|
|
418
450
|
if (changeSignature) this.lastReviewedChangeSignature = changeSignature.key;
|
|
419
451
|
this.currentChangedPaths = changeSignature?.changedPaths;
|
|
420
452
|
this.status = "idle";
|
|
453
|
+
if (typeof outcome === "object" && !ctx.signal?.aborted) this.displayClarification?.([
|
|
454
|
+
"Main watchdog clarification:", outcome.question, `Evidence: ${outcome.evidence}`,
|
|
455
|
+
"Consider this missing context as you continue the task. This is not approval, permission, or a warning. The reviewer has yielded; no reply or follow-up review is required.",
|
|
456
|
+
].join("\n"));
|
|
421
457
|
}
|
|
422
458
|
this.resolveWaiters(true);
|
|
423
459
|
} finally {
|
|
@@ -468,6 +504,15 @@ export class MainWatchdogRuntime {
|
|
|
468
504
|
};
|
|
469
505
|
}
|
|
470
506
|
|
|
507
|
+
/** A real mid-stream user input can steer without emitting before_agent_start. */
|
|
508
|
+
handleUserInput(): void {
|
|
509
|
+
if (this.displayClarification && this.configResult.config.clarification && (this.reviewing || this.waitingAtAgentEnd)) this.reset("new user input");
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
handleModelChange(): void {
|
|
513
|
+
if (this.displayClarification && this.configResult.config.clarification && (this.reviewing || this.waitingAtAgentEnd)) this.reset("model changed");
|
|
514
|
+
}
|
|
515
|
+
|
|
471
516
|
async waitForIdle(timeoutMs = 1_000): Promise<boolean> {
|
|
472
517
|
return this.waitForSettled(timeoutMs);
|
|
473
518
|
}
|
|
@@ -523,6 +568,7 @@ export class MainWatchdogRuntime {
|
|
|
523
568
|
}
|
|
524
569
|
|
|
525
570
|
private invalidateActiveReview(_reason: string): void {
|
|
571
|
+
this.activeReviewAbortController?.abort();
|
|
526
572
|
this.abortActiveAgentEnd();
|
|
527
573
|
this.epoch++;
|
|
528
574
|
this.status = "idle";
|
|
@@ -567,7 +613,7 @@ export class MainWatchdogRuntime {
|
|
|
567
613
|
this.activeReviewWarning = undefined;
|
|
568
614
|
}
|
|
569
615
|
|
|
570
|
-
private async reviewDelta(delta: string, timeoutMs: number, options: { correction?: boolean } = {}): Promise<ReviewDeltaOutcome> {
|
|
616
|
+
private async reviewDelta(delta: string, timeoutMs: number, options: { correction?: boolean; allowClarification?: boolean } = {}): Promise<ReviewDeltaOutcome> {
|
|
571
617
|
if (this.reviewing || this.disposed) return "stale";
|
|
572
618
|
this.reviewing = true;
|
|
573
619
|
const reviewEpoch = this.epoch;
|
|
@@ -578,6 +624,7 @@ export class MainWatchdogRuntime {
|
|
|
578
624
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
579
625
|
const abortController = new AbortController();
|
|
580
626
|
this.activeReviewAbortController = abortController;
|
|
627
|
+
const allowClarification = Boolean(options.allowClarification && this.displayClarification && this.configResult.config.clarification && !this.askedThisPrompt && !this.stalemate);
|
|
581
628
|
const reviewPromise = Promise.resolve().then(() => this.review({
|
|
582
629
|
delta,
|
|
583
630
|
epoch: reviewEpoch,
|
|
@@ -586,6 +633,7 @@ export class MainWatchdogRuntime {
|
|
|
586
633
|
hasScope: this.scopeBlock().trim().length > 0,
|
|
587
634
|
signal: abortController.signal,
|
|
588
635
|
emitWarning: (warning) => this.acceptWarning(reviewEpoch, reviewId, warning),
|
|
636
|
+
...(allowClarification ? { allowClarification: true } : {}),
|
|
589
637
|
}));
|
|
590
638
|
try {
|
|
591
639
|
const result = await Promise.race([
|
|
@@ -600,6 +648,14 @@ export class MainWatchdogRuntime {
|
|
|
600
648
|
}
|
|
601
649
|
if (!this.isCurrent(reviewEpoch, reviewId)) return "stale";
|
|
602
650
|
if (!result) return "stale";
|
|
651
|
+
if (allowClarification && this.configResult.config.clarification && result.clarification && (!result.stopReason || result.stopReason === "stop") && !this.activeReviewWarning && !result.warnings?.length && !abortController.signal.aborted) {
|
|
652
|
+
const question = result.clarification.question.trim();
|
|
653
|
+
const evidence = result.clarification.evidence.trim();
|
|
654
|
+
if (question && evidence) {
|
|
655
|
+
this.askedThisPrompt = true;
|
|
656
|
+
return { question: boundWatchdogReviewText(question, 1_000), evidence: boundWatchdogReviewText(evidence, 2_000) };
|
|
657
|
+
}
|
|
658
|
+
}
|
|
603
659
|
for (const warning of result.warnings ?? []) this.acceptWarning(reviewEpoch, reviewId, warning);
|
|
604
660
|
if (result.stopReason && result.stopReason !== "stop") {
|
|
605
661
|
this.fail(`Watchdog review ended with stop reason '${result.stopReason}'.`);
|
|
@@ -681,9 +737,8 @@ export class MainWatchdogRuntime {
|
|
|
681
737
|
this.observedRepoEditThisTurn = false;
|
|
682
738
|
}
|
|
683
739
|
|
|
684
|
-
private resolveReviewChangeSignature(
|
|
740
|
+
private resolveReviewChangeSignature(current: WatchdogRepoChangeSignature | undefined, cwd: string): WatchdogRepoChangeSignature | undefined {
|
|
685
741
|
if (!this.reviewChangesOnly) return undefined;
|
|
686
|
-
const current = this.currentRepoChangeSignature(cwd);
|
|
687
742
|
if (current) {
|
|
688
743
|
this.currentChangedPaths = current.changedPaths;
|
|
689
744
|
if (current.key === this.turnStartChangeSignature?.key) return undefined;
|
|
@@ -785,13 +840,14 @@ export class MainWatchdogRuntime {
|
|
|
785
840
|
const changes = changeSignature?.changedPaths.length
|
|
786
841
|
? ["Changed repo paths:", ...changeSignature.changedPaths.slice(0, 200).map((file) => `- ${file}`)].join("\n")
|
|
787
842
|
: "";
|
|
788
|
-
const
|
|
843
|
+
const activity = this.activityTail ? `Recent delivered orchestration activity (oldest first; bounded observations, not a task board; waits/holds are not evidence of neglect):\n${this.activityTail}` : "";
|
|
844
|
+
const contextPieces = [scopeBlock, changes, lspBlock, activity].filter(Boolean);
|
|
789
845
|
if (!contextPieces.length) return boundWatchdogReviewText(input);
|
|
790
846
|
|
|
791
847
|
const maxContextLength = Math.floor(MAX_REVIEW_INPUT_CHARS / 2);
|
|
792
848
|
const maxPieceLength = Math.max(1_000, Math.floor(maxContextLength / contextPieces.length));
|
|
793
849
|
const boundedContext = contextPieces.map((piece) => piece.length > maxPieceLength
|
|
794
|
-
? `${piece.slice(0, maxPieceLength - 6)}\n- ...`
|
|
850
|
+
? piece === activity ? boundWatchdogReviewText(piece, maxPieceLength) : `${piece.slice(0, maxPieceLength - 6)}\n- ...`
|
|
795
851
|
: piece).join(REVIEW_DELTA_SEPARATOR);
|
|
796
852
|
const separatorLength = input ? REVIEW_DELTA_SEPARATOR.length : 0;
|
|
797
853
|
const inputBudget = MAX_REVIEW_INPUT_CHARS - boundedContext.length - separatorLength;
|
|
@@ -808,6 +864,12 @@ export class MainWatchdogRuntime {
|
|
|
808
864
|
this.pendingDeltaChars = 0;
|
|
809
865
|
}
|
|
810
866
|
|
|
867
|
+
private clearActivity(): void {
|
|
868
|
+
if (!this.activityTail) return;
|
|
869
|
+
this.activityTail = "";
|
|
870
|
+
this.activityPending = false;
|
|
871
|
+
}
|
|
872
|
+
|
|
811
873
|
private fail(message: string): void {
|
|
812
874
|
this.failedReviews++;
|
|
813
875
|
this.lastError = message;
|
package/src/watchdog/scope.ts
CHANGED
|
@@ -32,7 +32,7 @@ export class WatchdogScopeArtifact {
|
|
|
32
32
|
if (!this.entries.length) return "";
|
|
33
33
|
return [
|
|
34
34
|
"Current scope:",
|
|
35
|
-
"The following real user prompts are the current scope record, newest last.
|
|
35
|
+
"The following real user prompts are the current scope record, newest last. Side questions are additive, not scope drift or cancellation of older objectives; only explicit changes supersede requirements. Use watchdog_warn for evidence-backed reminders of forgotten authorized work, not dependencies still pending or explicit holds. The orchestrator owns task tracking. Flag unauthorized work as category 'scope-drift'.",
|
|
36
36
|
...this.entries.map((entry, index) => [
|
|
37
37
|
`Scope prompt ${index + 1} (${entry.createdAt}):`,
|
|
38
38
|
entry.prompt,
|
package/src/watchdog/settings.ts
CHANGED
|
@@ -61,6 +61,7 @@ interface ParseMeta {
|
|
|
61
61
|
|
|
62
62
|
export const DEFAULT_WATCHDOG_CONFIG: ResolvedWatchdogConfig = {
|
|
63
63
|
enabled: false,
|
|
64
|
+
clarification: false,
|
|
64
65
|
agentEndTimeoutMs: 30_000,
|
|
65
66
|
severityThreshold: "concern",
|
|
66
67
|
maxWarnings: null,
|
|
@@ -92,6 +93,7 @@ export const DEFAULT_WATCHDOG_CONFIG: ResolvedWatchdogConfig = {
|
|
|
92
93
|
|
|
93
94
|
const WATCHDOG_FIELDS = new Set([
|
|
94
95
|
"enabled",
|
|
96
|
+
"clarification",
|
|
95
97
|
"agentEndTimeoutMs",
|
|
96
98
|
"severityThreshold",
|
|
97
99
|
"maxWarnings",
|
|
@@ -109,9 +111,9 @@ const ROLE_MODEL_RULE_FIELDS = new Set(["allow", "deny", "note"]);
|
|
|
109
111
|
const GUIDANCE_FIELDS = new Set(["watchdogMd"]);
|
|
110
112
|
const SCOPE_FIELDS = new Set(["enabled"]);
|
|
111
113
|
const CADENCE_FIELDS = new Set(["everyNTools"]);
|
|
112
|
-
const ENDPOINT_FIELDS = new Set(["enabled", "model", "thinking"]);
|
|
113
|
-
const CHILDREN_FIELDS = new Set(["enabled", "model", "thinking", "watchdogTailTimeoutMs", "cadence", "overrides"]);
|
|
114
|
-
const CHILD_OVERRIDE_FIELDS = new Set(["enabled", "model", "thinking", "cadence"]);
|
|
114
|
+
const ENDPOINT_FIELDS = new Set(["enabled", "model", "fallbackModels", "thinking"]);
|
|
115
|
+
const CHILDREN_FIELDS = new Set(["enabled", "model", "fallbackModels", "thinking", "watchdogTailTimeoutMs", "cadence", "overrides"]);
|
|
116
|
+
const CHILD_OVERRIDE_FIELDS = new Set(["enabled", "model", "fallbackModels", "thinking", "cadence"]);
|
|
115
117
|
const LSP_FIELDS = new Set(["enabled", "timeoutMs", "maxFiles", "maxDiagnostics"]);
|
|
116
118
|
|
|
117
119
|
function cloneDefaultConfig(): ResolvedWatchdogConfig {
|
|
@@ -219,6 +221,7 @@ function parseEndpointPatch(value: unknown, field: string, meta: ParseMeta): Wat
|
|
|
219
221
|
const input = parseObject(value, field, meta);
|
|
220
222
|
assertKnownFields(input, ENDPOINT_FIELDS, field, meta);
|
|
221
223
|
const patch: WatchdogEndpointPatch = {};
|
|
224
|
+
if ("fallbackModels" in input) patch.fallbackModels = parseStringList(input.fallbackModels, `${field}.fallbackModels`, meta);
|
|
222
225
|
if ("enabled" in input) patch.enabled = parseBoolean(input.enabled, `${field}.enabled`, meta);
|
|
223
226
|
if ("model" in input) patch.model = parseNonEmptyString(input.model, `${field}.model`, meta);
|
|
224
227
|
if ("thinking" in input) patch.thinking = parseThinking(input.thinking, `${field}.thinking`, meta);
|
|
@@ -229,6 +232,7 @@ function parseChildOverridePatch(value: unknown, field: string, meta: ParseMeta)
|
|
|
229
232
|
const input = parseObject(value, field, meta);
|
|
230
233
|
assertKnownFields(input, CHILD_OVERRIDE_FIELDS, field, meta);
|
|
231
234
|
const patch: WatchdogChildOverridePatch = {};
|
|
235
|
+
if ("fallbackModels" in input) patch.fallbackModels = parseStringList(input.fallbackModels, `${field}.fallbackModels`, meta);
|
|
232
236
|
if ("enabled" in input) patch.enabled = parseBoolean(input.enabled, `${field}.enabled`, meta);
|
|
233
237
|
if ("model" in input) patch.model = parseNonEmptyString(input.model, `${field}.model`, meta);
|
|
234
238
|
if ("thinking" in input) patch.thinking = parseThinking(input.thinking, `${field}.thinking`, meta);
|
|
@@ -240,6 +244,7 @@ function parseChildrenPatch(value: unknown, field: string, meta: ParseMeta): Wat
|
|
|
240
244
|
const input = parseObject(value, field, meta);
|
|
241
245
|
assertKnownFields(input, CHILDREN_FIELDS, field, meta);
|
|
242
246
|
const patch: WatchdogChildrenPatch = {};
|
|
247
|
+
if ("fallbackModels" in input) patch.fallbackModels = parseStringList(input.fallbackModels, `${field}.fallbackModels`, meta);
|
|
243
248
|
if ("enabled" in input) patch.enabled = parseBoolean(input.enabled, `${field}.enabled`, meta);
|
|
244
249
|
if ("model" in input) patch.model = parseNonEmptyString(input.model, `${field}.model`, meta);
|
|
245
250
|
if ("thinking" in input) patch.thinking = parseThinking(input.thinking, `${field}.thinking`, meta);
|
|
@@ -303,6 +308,7 @@ function parseWatchdogPatch(value: unknown, field: string, meta: ParseMeta): Wat
|
|
|
303
308
|
assertKnownFields(input, WATCHDOG_FIELDS, field, meta);
|
|
304
309
|
const patch: WatchdogConfigPatch = {};
|
|
305
310
|
if ("enabled" in input) patch.enabled = parseBoolean(input.enabled, `${field}.enabled`, meta);
|
|
311
|
+
if ("clarification" in input) patch.clarification = parseBoolean(input.clarification, `${field}.clarification`, meta);
|
|
306
312
|
if ("agentEndTimeoutMs" in input) {
|
|
307
313
|
patch.agentEndTimeoutMs = parseInteger(input.agentEndTimeoutMs, `${field}.agentEndTimeoutMs`, meta, "a positive integer", (candidate) => candidate >= 1);
|
|
308
314
|
}
|