From 86ac420b77a3d7eeedd0a1831d71f2ca294f3810 Mon Sep 17 00:00:00 2001 From: jyl Date: Thu, 5 Feb 2026 15:37:02 +0800 Subject: [PATCH] feat: udpate --- .husky/pre-push | 20 - .../src/cli/cmd/tui/routes/session/index.tsx | 9 +- packages/opencode/src/config/config.ts | 20 + packages/opencode/src/session/checker.ts | 469 ++++++++++++++---- packages/opencode/src/session/index.ts | 1 + packages/opencode/src/session/prompt.ts | 35 +- .../opencode/src/session/prompt/checker.txt | 9 + packages/sdk/js/src/gen/types.gen.ts | 3 + packages/sdk/js/src/v2/gen/types.gen.ts | 3 + 9 files changed, 436 insertions(+), 133 deletions(-) delete mode 100755 .husky/pre-push diff --git a/.husky/pre-push b/.husky/pre-push deleted file mode 100755 index 5d3cc5341..000000000 --- a/.husky/pre-push +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh -set -e -# Check if bun version matches package.json -# keep in sync with packages/script/src/index.ts semver qualifier -bun -e ' -import { semver } from "bun"; -const pkg = await Bun.file("package.json").json(); -const expectedBunVersion = pkg.packageManager?.split("@")[1]; -if (!expectedBunVersion) { - throw new Error("packageManager field not found in root package.json"); -} -const expectedBunVersionRange = `^${expectedBunVersion}`; -if (!semver.satisfies(process.versions.bun, expectedBunVersionRange)) { - throw new Error(`This script requires bun@${expectedBunVersionRange}, but you are using bun@${process.versions.bun}`); -} -if (process.versions.bun !== expectedBunVersion) { - console.warn(`Warning: Bun version ${process.versions.bun} differs from expected ${expectedBunVersion}`); -} -' -bun typecheck diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index f7d83b055..112d05897 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1838,13 +1838,16 @@ function Task(props: ToolProps) { ) }) - const current = createMemo(() => tools().findLast((x) => x.state.status !== "pending")) + const executedTools = createMemo(() => tools().filter((x) => x.state.status !== "pending")) + const current = createMemo(() => executedTools().findLast((x) => x.state.status !== "pending")) const isRunning = createMemo(() => props.part.state.status === "running") return ( - + 0 || isRunning())} + > ) { > - {props.input.description} ({tools().length} toolcalls) + {props.input.description} ({executedTools().length} toolcalls) {(item) => { diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 4b40dbee6..325491753 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1170,6 +1170,26 @@ export namespace Config { }) .optional() .describe("Checker agent configuration for hallucination detection"), + evolution: z + .object({ + enabled: z.boolean().optional().describe("Enable prompt evolution system (default: false)"), + model: z + .string() + .optional() + .describe("Model to use for evolution agent in format provider/model (default: same as checker model)"), + frequency: z + .enum(["always", "per_session", "on_failure", "never"]) + .optional() + .describe("When to run prompt evolution checks"), + max_evolutions: z + .number() + .int() + .positive() + .optional() + .describe("Maximum number of evolutions per session (default: 5)"), + }) + .optional() + .describe("Prompt evolution configuration for automatic prompt optimization"), }) .strict() .meta({ diff --git a/packages/opencode/src/session/checker.ts b/packages/opencode/src/session/checker.ts index 7788b676c..75f867651 100644 --- a/packages/opencode/src/session/checker.ts +++ b/packages/opencode/src/session/checker.ts @@ -6,34 +6,63 @@ import { Log } from "../util/log" import { generateObject } from "ai" import { Provider } from "../provider/provider" import { Config } from "../config/config" -import { SessionCompaction } from "./compaction" +import { Instance } from "../project/instance" +import path from "path" +import fs from "fs/promises" import z from "zod" export namespace SessionChecker { const log = Log.create({ service: "session.checker" }) - const FEEDBACK_PREFIX = "[Checker Agent Feedback]: " - - const CheckResultSchema = z.object({ - hasHallucination: z.boolean(), - severity: z.enum(["critical", "major", "minor", "trivial"]).optional(), - type: z - .enum([ - "fabricated_information", - "contradictory_information", - "logical_inconsistency", - "false_confidence", - "tool_output_mismatch", - "unverified_claim", - ]) - .optional(), - issue: z.string().optional(), - evidence: z.string().optional(), - suggestion: z.string().optional(), - confidence: z.number().min(0).max(1).optional(), + const FEEDBACK_PREFIX = "[Checker Feedback]: " + const EVOLUTION_OUTPUT_FILE = "prompt-evolution.md" + const MAX_PROMPT_LENGTH = 10000 + + const PROMPT_INJECTION_PATTERNS = [ + /ignore\s+(previous|all|above)/i, + /system\s+(prompt|instruct)/i, + /override\s+(previous|all)/i, + /do\s+not\s+follow/i, + /disregard\s+(previous|all)/i, + /bypass\s+(restrictions|rules)/i, + /new\s+system\s+prompt/i, + /you\s+are\s+now\s+(a|an)/i, + ] + + function isValidOptimizedPrompt(prompt: string): { valid: boolean; reason?: string } { + if (!prompt.trim()) { + return { valid: false, reason: "prompt is empty" } + } + if (prompt.length > MAX_PROMPT_LENGTH) { + return { valid: false, reason: `prompt exceeds ${MAX_PROMPT_LENGTH} characters` } + } + for (const pattern of PROMPT_INJECTION_PATTERNS) { + if (pattern.test(prompt)) { + return { valid: false, reason: `suspicious pattern detected: ${pattern}` } + } + } + return { valid: true } + } + + const EvolutionResultSchema = z.object({ + shouldEvolve: z.boolean(), + evolutionType: z.enum(["none", "minor", "major", "complete"]), + reasoning: z.string(), + optimizedPrompt: z.string().optional(), + changes: z.array(z.string()), }) - type CheckResult = z.infer + type EvolutionResult = z.infer + + interface EvolutionEntry { + timestamp: number + round: number + userInput: string + originalPrompt: string + optimizedPrompt: string + reasoning: string + changes: string[] + } function isFeedbackMessage(msg: MessageV2.WithParts): boolean { return msg.parts.some((p) => { @@ -45,15 +74,27 @@ export namespace SessionChecker { interface CheckState { checkCount: number + evolutionCount: number lastCheckTime: number | null - lastCheckMessages: string[] + evolutionHistory: EvolutionEntry[] } const stateCache = new Map() + const effectivePromptCache = new Map() + + function truncateText(value: string, maxChars: number): string { + if (value.length <= maxChars) return value + return value.slice(0, maxChars) + "\n...[truncated]" + } function getState(sessionID: string): CheckState { if (!stateCache.has(sessionID)) { - stateCache.set(sessionID, { checkCount: 0, lastCheckTime: null, lastCheckMessages: [] }) + stateCache.set(sessionID, { + checkCount: 0, + evolutionCount: 0, + lastCheckTime: null, + evolutionHistory: [], + }) } return stateCache.get(sessionID)! } @@ -68,147 +109,365 @@ export namespace SessionChecker { function canCheck(sessionID: string, config: Config.Info): boolean { cleanupState(sessionID) const state = getState(sessionID) - const maxChecks = config.checker?.max_checks ?? 10 - if (state.checkCount >= maxChecks) { - log.debug("max checks reached", { sessionID, checkCount: state.checkCount, maxChecks }) - return false - } - const freq = config.checker?.frequency ?? "once_per_session" - if (freq === "never") { - log.debug("checker disabled by frequency config", { sessionID }) + + // Check if evolution is explicitly enabled in config + if (config.evolution?.enabled === false) { return false } - if (freq === "once_per_session" && state.checkCount > 0) { - log.debug("already checked once in this session", { sessionID }) + + const maxEvolutions = config.evolution?.max_evolutions ?? 5 + if (state.evolutionCount >= maxEvolutions) { + log.info("max evolutions reached", { sessionID, count: state.evolutionCount }) return false } + + const freq = config.evolution?.frequency ?? "on_failure" + if (freq === "never") return false + if (freq === "per_session" && state.evolutionCount > 0) return false + + // For "on_failure" or "always", we allow checking as long as we haven't hit max_evolutions return true } - async function getCheckerModel(input: { model: Provider.Model }): Promise { + async function getEvolutionModel(input: { model: Provider.Model }): Promise { const config = await Config.get() - const modelString = config.checker?.model - if (!modelString) { - return input.model - } + const modelString = config.evolution?.model || config.checker?.model + if (!modelString) return input.model const [providerID, modelID] = modelString.split("/") return Provider.getModel(providerID, modelID) } + function computeDeltaPrompt(original: string, optimized: string): string { + const a = original.split("\n") + const b = optimized.split("\n") + const m = a.length + const n = b.length + const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0)) + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]) + } + } + const added: string[] = [] + let i = m + let j = n + while (i > 0 || j > 0) { + if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) { + i-- + j-- + } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) { + added.push(b[j - 1]) + j-- + } else if (i > 0) { + i-- + } else { + j-- + } + } + added.reverse() + const filtered: string[] = [] + let inCode = false + for (const line of added) { + if (line.startsWith("```")) { + inCode = !inCode + continue + } + if (inCode) continue + if (/\[\.\.\. .* \.\.\.]/.test(line)) continue + if (/^\[?\.\.\. ?existing content ?\.\.\.]?$/i.test(line)) continue + if (/^graph\s+TD$/i.test(line)) continue + filtered.push(line) + } + const result = filtered.join("\n").trim() + return result.length > 0 ? result : optimized + } + + async function writeEvolutionEntry(sessionID: string, entry: EvolutionEntry): Promise { + const outputPath = path.join(Instance.directory, EVOLUTION_OUTPUT_FILE) + try { + await fs.stat(outputPath) + } catch { + await fs.writeFile(outputPath, "# Prompt Evolution Log\n\n", "utf-8") + } + const deltaPrompt = computeDeltaPrompt(entry.originalPrompt, entry.optimizedPrompt) + + const content = [ + `## Round ${entry.round} - ${new Date(entry.timestamp).toLocaleString()}`, + `**Session ID:** ${sessionID}`, + "", + "### User Input", + "```", + entry.userInput, + "```", + "", + "### Reasoning", + entry.reasoning, + "", + "### Changes", + ...entry.changes.map((c) => `- ${c}`), + "", + "### Original Prompt", + "```", + entry.originalPrompt, + "```", + "", + "### Optimized Prompt", + "```", + deltaPrompt, + "```", + "", + "---", + "", + ].join("\n") + await fs.appendFile(outputPath, content, "utf-8") + log.info("evolution written to file", { sessionID, filePath: outputPath, round: entry.round }) + } + export async function check(input: { sessionID: string + agent: string messages: MessageV2.WithParts[] model: Provider.Model + currentPrompt: string + agentPrompt?: string abort: AbortSignal }): Promise { const config = await Config.get() - if (config.checker?.enabled === false) { - return false - } + // Use the specific canCheck logic for evolution if (!canCheck(input.sessionID, config)) { return false } - const lastMsg = input.messages[input.messages.length - 1] + const messages = input.messages.filter((m) => !isFeedbackMessage(m)) + if (messages.length < 2) { + log.info("not enough messages for check", { count: messages.length }) + return false + } - if ( - lastMsg.info.role !== "assistant" || - !lastMsg.info.finish || - ["tool-calls", "stop", "error", "unknown"].includes(lastMsg.info.finish) - ) { + const lastAssistantIndex = messages.findLastIndex((m) => m.info.role === "assistant") + if (lastAssistantIndex < 0) { + log.info("could not find last assistant message", { sessionID: input.sessionID }) return false } - const hasRecentFeedback = input.messages.some((m, idx) => { - const recentStart = Math.max(0, input.messages.length - 6) - return idx >= recentStart && isFeedbackMessage(m) - }) - if (hasRecentFeedback) { + const lastAssistantMsg = messages[lastAssistantIndex] + const lastUserMsg = messages + .slice(0, lastAssistantIndex) + .findLast((m) => m.info.role === "user") + + if (!lastUserMsg || !lastAssistantMsg) { + log.info("could not find user/assistant pair", { + sessionID: input.sessionID, + hasAssistant: !!lastAssistantMsg, + hasUser: !!lastUserMsg + }) return false } - log.info("checking for hallucinations", { sessionID: input.sessionID }) + const startIndex = Math.max(0, lastAssistantIndex - 7) + const historyMessages = messages.slice(startIndex, lastAssistantIndex + 1) + + const formatMessage = (m: MessageV2.WithParts): string => { + const role = m.info.role + const agent = m.info.agent + const content = m.parts + .filter((p) => { + if (p.type === "text") return true + if (p.type === "tool") return true + return false + }) + .map((p) => { + if (p.type === "text") { + const text = (p as any).text + return typeof text === "string" ? text : "" + } + if (p.type === "tool") { + const tool = (p as any).tool + const state = (p as any).state + const status = state?.status + const title = typeof state?.title === "string" ? state.title : "" + const output = typeof state?.output === "string" ? truncateText(state.output, 2000) : "" + const err = typeof state?.error === "string" ? truncateText(state.error, 1000) : "" + const header = `[tool:${tool}] status=${status}${title ? " title=" + title : ""}` + if (status === "completed" && output) return header + "\n" + output + if (status === "error" && err) return header + "\n" + err + return header + } + return "" + }) + .filter((x) => x.trim().length > 0) + .join("\n") + return `[${role.toUpperCase()} - Agent: ${agent ?? ""}]\n${content}` + } + + const conversationHistory = historyMessages.map(formatMessage).join("\n\n---\n\n") + + const userInput = lastUserMsg.parts + .filter((p) => p.type === "text" && !(p as any).synthetic) + .map((p) => (p as any).text) + .join("\n") + + if (!userInput.trim()) return false + + log.info("checking prompt evolution", { sessionID: input.sessionID, agent: input.agent }) try { const agent = await Agent.get("checker") if (!agent) { - log.warn("checker agent not found, skipping hallucination check") + log.warn("checker agent not found") return false } - const checkerModel = await getCheckerModel({ model: input.model }) - const language = await Provider.getLanguage(checkerModel) + const evolutionModel = await getEvolutionModel({ model: input.model }) + const language = await Provider.getLanguage(evolutionModel) + + const state = getState(input.sessionID) + const recentEvolutions = state.evolutionHistory + .slice(-3) + .map((e) => `Round ${e.round}: ${e.changes.join("; ")}`) + .join("\n") + + const userSystemOverride = (lastUserMsg.info as any).system + + const analysisPrompt = ` +## Prompt Reflection +Agent: ${input.agent} + +System Prompt: +\`\`\` +${input.currentPrompt} +\`\`\` + +Last User Input (rules and preferences may be embedded here): +\`\`\` +${userInput} +\`\`\` + +User System Override (if any): +\`\`\` +${typeof userSystemOverride === "string" && userSystemOverride.trim().length > 0 ? userSystemOverride : "(none)"} +\`\`\` + +Recent Evolutions (if any): +\`\`\` +${recentEvolutions || "(none)"} +\`\`\` + +Last Interaction: +${conversationHistory} - const filteredMessages = input.messages.filter((m) => !isFeedbackMessage(m)) - const checkerMessages = MessageV2.toModelMessages(filteredMessages, checkerModel) +Task: +Decide if the system prompt should be optimized to better satisfy the user's intent. +Pay special attention to any explicit coding rules, style guides, or preferences mentioned by the user (for example naming conventions, async/await usage, error handling patterns, comment and TODO format, or response language requirements). If these rules are not clearly present in the current system prompt, incorporate them into the optimizedPrompt in a concise and structured way. +Return JSON with: shouldEvolve, evolutionType ("none"|"minor"|"major"|"complete"), reasoning, optimizedPrompt?, changes[]. +` const { object: result } = await generateObject({ model: language, - schema: CheckResultSchema, + schema: EvolutionResultSchema, system: agent.prompt, - messages: checkerMessages, + messages: [{ role: "user", content: analysisPrompt }], abortSignal: input.abort, }) - if (result.hasHallucination && result.confidence && result.confidence > 0.6) { - const state = getState(input.sessionID) - state.checkCount++ - state.lastCheckTime = Date.now() - state.lastCheckMessages.push(lastMsg.info.id) - - log.info("hallucination detected", { - sessionID: input.sessionID, - type: result.type, - severity: result.severity, - confidence: result.confidence, - }) + log.info("checker result", { + sessionID: input.sessionID, + agent: input.agent, + shouldEvolve: result.shouldEvolve, + evolutionType: result.evolutionType, + }) - const feedbackContent = formatFeedback(result) - const feedbackMsg: MessageV2.User = { - id: Identifier.ascending("message"), - sessionID: input.sessionID, - role: "user", - time: { created: Date.now() }, - agent: lastMsg.info.agent, - model: { - providerID: checkerModel.providerID, - modelID: checkerModel.id, - }, - } + if (result.shouldEvolve && result.optimizedPrompt) { + const validation = isValidOptimizedPrompt(result.optimizedPrompt) + if (!validation.valid) { + log.warn("optimized prompt validation failed", { sessionID: input.sessionID, reason: validation.reason }) + } else { + const state = getState(input.sessionID) + state.evolutionCount++ + state.lastCheckTime = Date.now() + const round = state.evolutionHistory.length + 1 + + const originalPrompt = input.agentPrompt && input.agentPrompt.trim().length > 0 + ? input.agentPrompt + : input.currentPrompt + + const entry: EvolutionEntry = { + timestamp: Date.now(), + round, + userInput, + originalPrompt, + optimizedPrompt: result.optimizedPrompt, + reasoning: result.reasoning, + changes: result.changes, + } + + state.evolutionHistory.push(entry) + await writeEvolutionEntry(input.sessionID, entry) + + await Session.update(input.sessionID, (draft) => { + if (!draft.prompts) draft.prompts = {} + draft.prompts[input.agent] = result.optimizedPrompt! + }) + + effectivePromptCache.set(`${input.sessionID}:${lastUserMsg.info.agent}`, result.optimizedPrompt) + + log.info("prompt evolved", { sessionID: input.sessionID, round, evolutionType: result.evolutionType }) - await Session.updateMessage(feedbackMsg) - await Session.updatePart({ - id: Identifier.ascending("part"), - messageID: feedbackMsg.id, - sessionID: input.sessionID, - type: "text", - text: `${FEEDBACK_PREFIX}${feedbackContent}`, - synthetic: true, - } satisfies MessageV2.TextPart) - - return true + const feedbackContent = `## Prompt Optimized (Round ${round}) + +**Reasoning:** ${result.reasoning} + +**Key Changes:** +${result.changes.map((c) => `- ${c}`).join("\n")} + +*Detailed changes have been logged to ${EVOLUTION_OUTPUT_FILE}*` + + const feedbackMsg: MessageV2.User = { + id: Identifier.ascending("message"), + sessionID: input.sessionID, + role: "user", + time: { created: Date.now() }, + agent: input.agent, + model: { + providerID: evolutionModel.providerID, + modelID: evolutionModel.id, + }, + } + + await Session.updateMessage(feedbackMsg) + await Session.updatePart({ + id: Identifier.ascending("part"), + messageID: feedbackMsg.id, + sessionID: input.sessionID, + type: "text", + text: `${FEEDBACK_PREFIX}${feedbackContent}`, + synthetic: true, + } satisfies MessageV2.TextPart) + + log.info("feedback posted", { sessionID: input.sessionID, round }) + return true + } } } catch (e) { - log.error("hallucination check failed", { error: e }) + log.error("evolution check failed", { error: e }) } + log.info("no evolution", { sessionID: input.sessionID, agent: input.agent }) return false } - function formatFeedback(result: CheckResult): string { - const parts: string[] = [] - if (result.type) parts.push(`**Type:** ${result.type.replace(/_/g, " ")}`) - if (result.severity) parts.push(`**Severity:** ${result.severity}`) - if (result.issue) parts.push(`**Issue:** ${result.issue}`) - if (result.evidence) parts.push(`**Evidence:** ${result.evidence}`) - if (result.suggestion) parts.push(`**Suggestion:** ${result.suggestion}`) - if (result.confidence) parts.push(`**Confidence:** ${Math.round(result.confidence * 100)}%`) - return parts.join("\n") + export function getEffectivePrompt(sessionID: string, agentName: string): string | undefined { + return effectivePromptCache.get(`${sessionID}:${agentName}`) } export function resetState(sessionID: string): void { stateCache.delete(sessionID) + for (const key of effectivePromptCache.keys()) { + if (key.startsWith(sessionID + ":")) { + effectivePromptCache.delete(key) + } + } } export function getCheckState(sessionID: string): CheckState | undefined { diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 556fad01f..7e4b4d81b 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -86,6 +86,7 @@ export namespace Session { diff: z.string().optional(), }) .optional(), + prompts: z.record(z.string(), z.string()).optional(), }) .meta({ ref: "Session", diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 1b979763d..11846cd8c 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -12,6 +12,7 @@ import { Provider } from "../provider/provider" import { type Tool as AITool, tool, jsonSchema, type ToolCallOptions } from "ai" import { SessionCompaction } from "./compaction" import { Instance } from "../project/instance" +import { MCP } from "../mcp" import { Bus } from "../bus" import { ProviderTransform } from "../provider/transform" import { SystemPrompt } from "./system" @@ -23,7 +24,7 @@ import MAX_STEPS from "../session/prompt/max-steps.txt" import { defer } from "../util/defer" import { clone } from "remeda" import { ToolRegistry } from "../tool/registry" -import { MCP } from "../mcp" +import { SessionChecker } from "./checker" import { LSP } from "../lsp" import { ReadTool } from "../tool/read" import { ListTool } from "../tool/ls" @@ -46,9 +47,7 @@ import { LLM } from "./llm" import { iife } from "@/util/iife" import { Shell } from "@/shell/shell" import { Truncate } from "@/tool/truncation" -import { SessionChecker } from "./checker" -// @ts-ignore globalThis.AI_SDK_LOG_WARNINGS = false export namespace SessionPrompt { @@ -529,7 +528,28 @@ export namespace SessionPrompt { } // normal processing - const agent = await Agent.get(lastUser.agent) + let agent = await Agent.get(lastUser.agent) + + // Look for optimized prompt in current session or parent sessions + let effectivePrompt: string | undefined + let currentSessionID: string | undefined = sessionID + while (currentSessionID) { + const s = await Session.get(currentSessionID).catch(() => undefined) + if (!s) break + if (s.prompts?.[lastUser.agent]) { + effectivePrompt = s.prompts[lastUser.agent] + break + } + currentSessionID = s.parentID + } + + if (!effectivePrompt) { + effectivePrompt = SessionChecker.getEffectivePrompt(sessionID, lastUser.agent) + } + + if (effectivePrompt) { + agent = { ...agent, prompt: effectivePrompt } + } const maxSteps = agent.steps ?? Infinity const isLastStep = step >= maxSteps msgs = await insertReminders({ @@ -613,12 +633,14 @@ export namespace SessionPrompt { await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: sessionMessages }) + const systemPrompts = [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system())] + const result = await processor.process({ user: lastUser, agent, abort, sessionID, - system: [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system())], + system: systemPrompts, messages: [ ...MessageV2.toModelMessages(sessionMessages, model), ...(isLastStep @@ -636,8 +658,11 @@ export namespace SessionPrompt { if (result === "continue" || result === "stop") { SessionChecker.check({ sessionID, + agent: lastUser.agent, messages: await Session.messages({ sessionID }), model, + currentPrompt: [...systemPrompts, agent.prompt ?? ""].join("\n\n"), + agentPrompt: agent.prompt, abort, }).catch(() => {}) } diff --git a/packages/opencode/src/session/prompt/checker.txt b/packages/opencode/src/session/prompt/checker.txt index 38dcc18a7..750282771 100644 --- a/packages/opencode/src/session/prompt/checker.txt +++ b/packages/opencode/src/session/prompt/checker.txt @@ -124,3 +124,12 @@ You must return a JSON object with this exact schema: - If multiple hallucinations exist, report the most critical one first - If confidence is below 0.6, set hasHallucination to false - Only report issues that can be verified or have strong evidence for + +## Missing Data And Ambiguous Requests + +- When conversation data or agent responses are missing or incomplete, explicitly state this limitation in evidence +- In cases of missing responses, set hasHallucination to false and explain that the assessment is limited by missing data +- If the user asks to "review checker logic", first clarify whether they want: + - A review of the checker agent's own code/prompt and behavior, or + - To use the checker agent to review another agent's response +- Ask for clarification and wait for the user's answer before proceeding when intent is ambiguous diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 8eefe5bfe..cabdb3ad7 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -557,6 +557,9 @@ export type Session = { snapshot?: string diff?: string } + prompts?: { + [key: string]: string + } } export type EventSessionCreated = { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 0556e1ad9..2ab7bde4f 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -783,6 +783,9 @@ export type Session = { snapshot?: string diff?: string } + prompts?: { + [key: string]: string + } } export type EventSessionCreated = {