{"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../../../src/watchdog/review.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACnG,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,EAAqC,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AASrG,OAAO,KAAK,EAAE,sBAAsB,EAAyB,MAAM,cAAc,CAAC;AAClF,OAAO,EACN,KAAK,sBAAsB,EAQ3B,MAAM,YAAY,CAAC;AAuBpB,KAAK,uBAAuB,GAAG,gBAAgB,GAAG,CAAC,MAAM,gBAAgB,GAAG,SAAS,CAAC,CAAC;AAEvF,KAAK,aAAa,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;AAEhC,UAAU,kBAAkB;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,4BAA4B;IAC5C,KAAK,EAAE,aAAa,CAAC;IACrB,aAAa,EAAE,aAAa,CAAC;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,+BAA+B;IAC/C,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;IACnD,gBAAgB,CAAC,EAAE,MAAM,aAAa,GAAG,SAAS,CAAC;CACnD;AAqFD,wBAAsB,0BAA0B,CAC/C,GAAG,EAAE,gBAAgB,EACrB,MAAM,EAAE,sBAAsB,EAC9B,OAAO,GAAE;IAAE,oBAAoB,CAAC,EAAE,aAAa,CAAA;CAAO,GACpD,OAAO,CAAC,4BAA4B,CAAC,CAmCvC;AAiGD,wBAAgB,wBAAwB,CACvC,QAAQ,EAAE,uBAAuB,EACjC,OAAO,GAAE,+BAAoC,GAC3C,sBAAsB,CA6DxB","sourcesContent":["import { Agent, type AgentTool, type StreamFn, type ThinkingLevel } from \"@lpb-work/pi-agent-core\";\nimport type { Model } from \"@lpb-work/pi-ai\";\nimport { streamSimple } from \"@lpb-work/pi-ai/compat\";\nimport { convertToLlm, createReadOnlyTools, type ExtensionContext } from \"@lpb-work/pi-coding-agent\";\nimport { type Static, Type } from \"typebox\";\nimport { resolveModelCandidate } from \"../runs/shared/model-fallback.ts\";\nimport {\n\tresolveEffectiveThinking,\n\tsplitKnownThinkingSuffix,\n\tTHINKING_LEVELS,\n\ttoModelInfo,\n} from \"../shared/model-info.ts\";\nimport type { WatchdogReviewFunction, WatchdogReviewRequest } from \"./runtime.ts\";\nimport {\n\ttype ResolvedWatchdogConfig,\n\tWATCHDOG_WARNING_CATEGORIES,\n\tWATCHDOG_WARNING_CONFIDENCES,\n\tWATCHDOG_WARNING_SEVERITIES,\n\ttype WatchdogCategory,\n\ttype WatchdogConfidence,\n\ttype WatchdogSeverity,\n\ttype WatchdogWarning,\n} from \"./types.ts\";\n\nconst WATCHDOG_ALLOWED_TOOL_NAMES = new Set([\"read\", \"grep\", \"find\", \"ls\", \"watchdog_warn\"]);\n\nconst WatchdogWarnParams = Type.Object(\n\t{\n\t\tseverity: Type.String({\n\t\t\tenum: WATCHDOG_WARNING_SEVERITIES,\n\t\t\tdescription: \"concern for actionable risk, blocker for a likely wrong or unsafe outcome\",\n\t\t}),\n\t\tsummary: Type.String({ description: \"One concise sentence naming the issue.\" }),\n\t\tevidence: Type.String({ description: \"Specific evidence from the turn delta or inspected files.\" }),\n\t\trecommendedAction: Type.String({\n\t\t\tdescription: \"Specific action the parent should take before accepting or continuing.\",\n\t\t}),\n\t\tcategory: Type.Optional(Type.String({ enum: WATCHDOG_WARNING_CATEGORIES })),\n\t\tconfidence: Type.Optional(Type.String({ enum: WATCHDOG_WARNING_CONFIDENCES })),\n\t},\n\t{ additionalProperties: false },\n);\n\ntype WatchdogWarnParams = Static<typeof WatchdogWarnParams>;\n\ntype WatchdogContextProvider = ExtensionContext | (() => ExtensionContext | undefined);\n\ntype RegistryModel = Model<any>;\n\ninterface WatchdogReviewAuth {\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tenv?: Record<string, string>;\n}\n\nexport interface WatchdogReviewModelSelection {\n\tmodel: RegistryModel;\n\tthinkingLevel: ThinkingLevel;\n\tauth: WatchdogReviewAuth;\n\texplicit: boolean;\n}\n\nexport interface CreateMainWatchdogReviewOptions {\n\tstreamFn?: StreamFn;\n\tcreateReadOnlyTools?: (cwd: string) => AgentTool[];\n\tgetThinkingLevel?: () => ThinkingLevel | undefined;\n}\n\nfunction fullModelId(model: Pick<RegistryModel, \"provider\" | \"id\">): string {\n\treturn `${model.provider}/${model.id}`;\n}\n\nfunction splitProviderModel(value: string): { provider: string; id: string } | undefined {\n\tconst slashIndex = value.indexOf(\"/\");\n\tif (slashIndex <= 0 || slashIndex === value.length - 1) return undefined;\n\treturn { provider: value.slice(0, slashIndex), id: value.slice(slashIndex + 1) };\n}\n\nfunction assertThinkingLevel(value: string, source: string): ThinkingLevel {\n\tif ((THINKING_LEVELS as readonly string[]).includes(value)) return value as ThinkingLevel;\n\tthrow new Error(\n\t\t`Unsupported watchdog thinking level '${value}' from ${source}; expected ${THINKING_LEVELS.join(\", \")} or false.`,\n\t);\n}\n\nfunction contextThinkingLevel(\n\tctx: ExtensionContext,\n\tcurrentThinkingLevel: ThinkingLevel | undefined,\n): ThinkingLevel | undefined {\n\tif (currentThinkingLevel) return currentThinkingLevel;\n\tconst value = (ctx as { thinkingLevel?: unknown }).thinkingLevel;\n\treturn typeof value === \"string\" && (THINKING_LEVELS as readonly string[]).includes(value)\n\t\t? (value as ThinkingLevel)\n\t\t: undefined;\n}\n\nfunction resolveReviewThinking(input: {\n\tmodelString: string;\n\tconfigThinking: string | false | undefined;\n\tctx: ExtensionContext;\n\tallowContextThinking: boolean;\n\tcurrentThinkingLevel?: ThinkingLevel;\n}): ThinkingLevel {\n\tconst fromModelOrConfig = resolveEffectiveThinking(input.modelString, input.configThinking);\n\tif (fromModelOrConfig) return assertThinkingLevel(fromModelOrConfig, \"watchdog model/config\");\n\tif (input.configThinking === false) return \"off\";\n\tif (input.configThinking !== undefined) return assertThinkingLevel(input.configThinking, \"watchdog config\");\n\tif (input.allowContextThinking) return contextThinkingLevel(input.ctx, input.currentThinkingLevel) ?? \"off\";\n\treturn \"off\";\n}\n\nfunction resolveConfiguredModel(\n\tctx: ExtensionContext,\n\trawModel: string,\n): { model: RegistryModel; modelString: string } {\n\tconst availableModels = ctx.modelRegistry.getAvailable().map(toModelInfo);\n\tconst preferredProvider = typeof ctx.model?.provider === \"string\" ? ctx.model.provider : undefined;\n\tconst resolved = resolveModelCandidate(rawModel, availableModels, preferredProvider);\n\tif (!resolved) {\n\t\tthrow new Error(\n\t\t\t`Configured watchdog model '${rawModel}' did not match exactly one authenticated available model. Use provider/model or configure credentials for the intended provider.`,\n\t\t);\n\t}\n\tconst { baseModel } = splitKnownThinkingSuffix(resolved);\n\tconst named = splitProviderModel(baseModel);\n\tif (!named) {\n\t\tthrow new Error(\n\t\t\t`Configured watchdog model '${rawModel}' did not match exactly one authenticated available model. Use provider/model or configure credentials for the intended provider.`,\n\t\t);\n\t}\n\n\tconst model = ctx.modelRegistry.find(named.provider, named.id);\n\tif (!model) throw new Error(`Configured watchdog model '${rawModel}' was not found as '${baseModel}'.`);\n\tif (!ctx.modelRegistry.hasConfiguredAuth(model)) {\n\t\tthrow new Error(\n\t\t\t`Configured watchdog model '${baseModel}' is not authenticated. Configure credentials for provider '${named.provider}' or choose an authenticated model.`,\n\t\t);\n\t}\n\treturn { model, modelString: resolved };\n}\n\nasync function resolveReviewAuth(ctx: ExtensionContext, model: RegistryModel): Promise<WatchdogReviewAuth> {\n\tconst auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);\n\tif (auth.ok === false) throw new Error(`Watchdog model auth failed for ${fullModelId(model)}: ${auth.error}`);\n\treturn {\n\t\t...(auth.apiKey ? { apiKey: auth.apiKey } : {}),\n\t\t...(auth.headers ? { headers: auth.headers } : {}),\n\t\t...(auth.env ? { env: auth.env } : {}),\n\t};\n}\n\nexport async function resolveWatchdogReviewModel(\n\tctx: ExtensionContext,\n\tconfig: ResolvedWatchdogConfig,\n\toptions: { currentThinkingLevel?: ThinkingLevel } = {},\n): Promise<WatchdogReviewModelSelection> {\n\tif (config.main.model) {\n\t\tconst resolved = resolveConfiguredModel(ctx, config.main.model);\n\t\treturn {\n\t\t\tmodel: resolved.model,\n\t\t\tthinkingLevel: resolveReviewThinking({\n\t\t\t\tmodelString: resolved.modelString,\n\t\t\t\tconfigThinking: config.main.thinking,\n\t\t\t\tctx,\n\t\t\t\tallowContextThinking: false,\n\t\t\t\tcurrentThinkingLevel: options.currentThinkingLevel,\n\t\t\t}),\n\t\t\tauth: await resolveReviewAuth(ctx, resolved.model),\n\t\t\texplicit: true,\n\t\t};\n\t}\n\n\tconst currentModel = ctx.model;\n\tif (!currentModel) {\n\t\tthrow new Error(\n\t\t\t\"Main watchdog review cannot run because the current Pi session model is unavailable and subagents.watchdog.main.model is not configured.\",\n\t\t);\n\t}\n\treturn {\n\t\tmodel: currentModel,\n\t\tthinkingLevel: resolveReviewThinking({\n\t\t\tmodelString: fullModelId(currentModel),\n\t\t\tconfigThinking: config.main.thinking,\n\t\t\tctx,\n\t\t\tallowContextThinking: true,\n\t\t\tcurrentThinkingLevel: options.currentThinkingLevel,\n\t\t}),\n\t\tauth: await resolveReviewAuth(ctx, currentModel),\n\t\texplicit: false,\n\t};\n}\n\nfunction nonEmptyString(value: string, field: string): string {\n\tconst trimmed = value.trim();\n\tif (!trimmed) throw new Error(`watchdog_warn.${field} must be a non-empty string.`);\n\treturn trimmed;\n}\n\nfunction toWatchdogWarning(params: WatchdogWarnParams): WatchdogWarning {\n\treturn {\n\t\tseverity: params.severity as WatchdogSeverity,\n\t\tcategory: (params.category ?? \"other\") as WatchdogCategory,\n\t\tconfidence: (params.confidence ?? \"medium\") as WatchdogConfidence,\n\t\tsource: \"main\",\n\t\tsummary: nonEmptyString(params.summary, \"summary\"),\n\t\tevidence: nonEmptyString(params.evidence, \"evidence\"),\n\t\trecommendedAction: nonEmptyString(params.recommendedAction, \"recommendedAction\"),\n\t};\n}\n\nfunction createWatchdogWarnTool(\n\trequest: WatchdogReviewRequest,\n): AgentTool<typeof WatchdogWarnParams, { accepted: boolean }> {\n\treturn {\n\t\tname: \"watchdog_warn\",\n\t\tlabel: \"Watchdog warning\",\n\t\tdescription: [\n\t\t\t\"Emit one actionable main-session watchdog warning.\",\n\t\t\t\"Use only for medium/high confidence concerns or blockers that the parent should consider before accepting the work.\",\n\t\t\t\"Do not use for nits, praise, informational notes, or clean reviews.\",\n\t\t].join(\" \"),\n\t\tparameters: WatchdogWarnParams,\n\t\texecutionMode: \"sequential\",\n\t\tasync execute(_toolCallId, params) {\n\t\t\tconst warning = toWatchdogWarning(params);\n\t\t\tconst accepted = request.emitWarning(warning);\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: accepted\n\t\t\t\t\t\t\t? \"Watchdog warning recorded.\"\n\t\t\t\t\t\t\t: \"Watchdog warning was ignored by the runtime guard because it was stale, duplicate, or over budget.\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails: { accepted },\n\t\t\t};\n\t\t},\n\t};\n}\n\nfunction buildWatchdogSystemPrompt(ctx: ExtensionContext, options: { hasScope?: boolean } = {}): string {\n\treturn [\n\t\t\"You are the main-session subagent watchdog for Pi.\",\n\t\t`Working directory: ${ctx.cwd}`,\n\t\t\"Review only the supplied parent turn delta. Inspect repository files only when needed to verify a concrete concern.\",\n\t\toptions.hasScope\n\t\t\t? \"When the review input includes a Current scope block, treat newer scope prompts as superseding/mutating older prompts and use category='scope-drift' for work that serves no current scope item.\"\n\t\t\t: undefined,\n\t\t\"You are read-only. You may use read, grep, find, and ls. Do not edit files, run shell commands, spawn agents, or mutate state.\",\n\t\t\"Emit warnings only by calling watchdog_warn. Freeform assistant text is ignored and must not be used to report warnings.\",\n\t\t\"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.\",\n\t\t\"Do not emit nits, style preferences, low-confidence guesses, informational notes, praise, or summaries.\",\n\t\t\"If the turn is clean, call no tools and end normally.\",\n\t\t\"Use severity='blocker' only when the issue should stop acceptance until addressed; otherwise use severity='concern'.\",\n\t]\n\t\t.filter((line): line is string => Boolean(line))\n\t\t.join(\"\\n\");\n}\n\nfunction buildReviewPrompt(request: WatchdogReviewRequest, selection: WatchdogReviewModelSelection): string {\n\treturn [\n\t\t\"Review this parent-session turn delta for subagent-watchdog-worthy issues.\",\n\t\t`Review id: ${request.reviewId}; epoch: ${request.epoch}; review model: ${fullModelId(selection.model)}; thinking: ${selection.thinkingLevel}.`,\n\t\t\"Call watchdog_warn for each qualifying concern or blocker. Call no tools when clean.\",\n\t\t\"<turn_delta>\",\n\t\trequest.delta,\n\t\t\"</turn_delta>\",\n\t].join(\"\\n\\n\");\n}\n\nfunction finalStopReason(agent: Agent): \"stop\" | \"error\" | \"aborted\" | \"length\" {\n\tfor (let index = agent.state.messages.length - 1; index >= 0; index--) {\n\t\tconst message = agent.state.messages[index];\n\t\tif (message && typeof message === \"object\" && (message as { role?: unknown }).role === \"assistant\") {\n\t\t\tconst stopReason = (message as { stopReason?: unknown }).stopReason;\n\t\t\tif (stopReason === \"error\" || stopReason === \"aborted\" || stopReason === \"length\") return stopReason;\n\t\t\treturn \"stop\";\n\t\t}\n\t}\n\treturn \"stop\";\n}\n\nfunction resolveContext(provider: WatchdogContextProvider): ExtensionContext | undefined {\n\treturn typeof provider === \"function\" ? provider() : provider;\n}\n\nexport function createMainWatchdogReview(\n\tprovider: WatchdogContextProvider,\n\toptions: CreateMainWatchdogReviewOptions = {},\n): WatchdogReviewFunction {\n\treturn async (request) => {\n\t\tconst ctx = resolveContext(provider);\n\t\tif (!ctx) throw new Error(\"Main watchdog review cannot run without an active Pi extension context.\");\n\t\tif (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: \"aborted\" };\n\t\tconst selection = await resolveWatchdogReviewModel(ctx, request.config, {\n\t\t\tcurrentThinkingLevel: options.getThinkingLevel?.(),\n\t\t});\n\t\tif (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: \"aborted\" };\n\t\tconst auth = selection.auth;\n\t\tconst registeredProvider = (\n\t\t\tctx.modelRegistry as {\n\t\t\t\tgetRegisteredProviderConfig?: (provider: string) => { api?: string; streamSimple?: StreamFn } | undefined;\n\t\t\t}\n\t\t).getRegisteredProviderConfig?.(selection.model.provider);\n\t\tconst baseStreamFn =\n\t\t\toptions.streamFn ??\n\t\t\t(registeredProvider?.streamSimple && registeredProvider.api === selection.model.api\n\t\t\t\t? registeredProvider.streamSimple\n\t\t\t\t: streamSimple);\n\t\tconst streamFn: StreamFn = (model, context, streamOptions) =>\n\t\t\tbaseStreamFn(model, context, {\n\t\t\t\t...streamOptions,\n\t\t\t\t...(auth.apiKey ? { apiKey: auth.apiKey } : {}),\n\t\t\t\tenv: auth.env || streamOptions?.env ? { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } : undefined,\n\t\t\t\theaders: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },\n\t\t\t});\n\t\tconst tools = [\n\t\t\t...(options.createReadOnlyTools ?? createReadOnlyTools)(ctx.cwd).filter(\n\t\t\t\t(tool) => WATCHDOG_ALLOWED_TOOL_NAMES.has(tool.name) && tool.name !== \"watchdog_warn\",\n\t\t\t),\n\t\t\tcreateWatchdogWarnTool(request),\n\t\t];\n\t\tconst agent = new Agent({\n\t\t\tinitialState: {\n\t\t\t\tsystemPrompt: buildWatchdogSystemPrompt(ctx, { hasScope: request.hasScope }),\n\t\t\t\tmodel: selection.model,\n\t\t\t\tthinkingLevel: selection.thinkingLevel,\n\t\t\t\ttools,\n\t\t\t},\n\t\t\tconvertToLlm,\n\t\t\tstreamFn,\n\t\t\tgetApiKey: (providerName) => (providerName === selection.model.provider ? auth.apiKey : undefined),\n\t\t\tbeforeToolCall: async ({ toolCall }) =>\n\t\t\t\tWATCHDOG_ALLOWED_TOOL_NAMES.has(toolCall.name)\n\t\t\t\t\t? undefined\n\t\t\t\t\t: { block: true, reason: `Watchdog reviews are read-only; tool '${toolCall.name}' is not allowed.` },\n\t\t\ttoolExecution: \"sequential\",\n\t\t});\n\t\tconst abort = () => agent.abort();\n\t\tctx.signal?.addEventListener(\"abort\", abort, { once: true });\n\t\trequest.signal?.addEventListener(\"abort\", abort, { once: true });\n\t\ttry {\n\t\t\tif (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: \"aborted\" };\n\t\t\tawait agent.prompt(buildReviewPrompt(request, selection));\n\t\t} finally {\n\t\t\tctx.signal?.removeEventListener(\"abort\", abort);\n\t\t\trequest.signal?.removeEventListener(\"abort\", abort);\n\t\t}\n\t\treturn { stopReason: finalStopReason(agent) };\n\t};\n}\n"]}