fix(thinking-block-validator): handle text content parts in message validation

Previously, the validator only checked for tool_use parts, causing 'Expected thinking but found text' errors when messages had text content. Renamed hasToolParts to hasContentParts to include both tool_use and text types.

Also added CommentCheckerConfigSchema support for custom prompt configuration.

🤖 Generated with assistance of OhMyOpenCode (https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2025-12-28 14:55:27 +09:00
parent 19f504fcfa
commit 092718f82d
3 changed files with 20 additions and 10 deletions

View File

@@ -115,6 +115,11 @@ export const SisyphusAgentConfigSchema = z.object({
replace_plan: z.boolean().optional(), replace_plan: z.boolean().optional(),
}) })
export const CommentCheckerConfigSchema = z.object({
/** Custom prompt to replace the default warning message. Use {{comments}} placeholder for detected comments XML. */
custom_prompt: z.string().optional(),
})
export const DynamicContextPruningConfigSchema = z.object({ export const DynamicContextPruningConfigSchema = z.object({
/** Enable dynamic context pruning (default: false) */ /** Enable dynamic context pruning (default: false) */
enabled: z.boolean().default(false), enabled: z.boolean().default(false),
@@ -175,6 +180,7 @@ export const OhMyOpenCodeConfigSchema = z.object({
claude_code: ClaudeCodeConfigSchema.optional(), claude_code: ClaudeCodeConfigSchema.optional(),
google_auth: z.boolean().optional(), google_auth: z.boolean().optional(),
sisyphus_agent: SisyphusAgentConfigSchema.optional(), sisyphus_agent: SisyphusAgentConfigSchema.optional(),
comment_checker: CommentCheckerConfigSchema.optional(),
experimental: ExperimentalConfigSchema.optional(), experimental: ExperimentalConfigSchema.optional(),
auto_update: z.boolean().optional(), auto_update: z.boolean().optional(),
}) })
@@ -185,6 +191,7 @@ export type AgentOverrides = z.infer<typeof AgentOverridesSchema>
export type AgentName = z.infer<typeof AgentNameSchema> export type AgentName = z.infer<typeof AgentNameSchema>
export type HookName = z.infer<typeof HookNameSchema> export type HookName = z.infer<typeof HookNameSchema>
export type SisyphusAgentConfig = z.infer<typeof SisyphusAgentConfigSchema> export type SisyphusAgentConfig = z.infer<typeof SisyphusAgentConfigSchema>
export type CommentCheckerConfig = z.infer<typeof CommentCheckerConfigSchema>
export type ExperimentalConfig = z.infer<typeof ExperimentalConfigSchema> export type ExperimentalConfig = z.infer<typeof ExperimentalConfigSchema>
export type DynamicContextPruningConfig = z.infer<typeof DynamicContextPruningConfigSchema> export type DynamicContextPruningConfig = z.infer<typeof DynamicContextPruningConfigSchema>

View File

@@ -1,5 +1,6 @@
import type { PendingCall } from "./types" import type { PendingCall } from "./types"
import { runCommentChecker, getCommentCheckerPath, startBackgroundInit, type HookInput } from "./cli" import { runCommentChecker, getCommentCheckerPath, startBackgroundInit, type HookInput } from "./cli"
import type { CommentCheckerConfig } from "../../config/schema"
import * as fs from "fs" import * as fs from "fs"
import { existsSync } from "fs" import { existsSync } from "fs"
@@ -32,8 +33,8 @@ function cleanupOldPendingCalls(): void {
setInterval(cleanupOldPendingCalls, 10_000) setInterval(cleanupOldPendingCalls, 10_000)
export function createCommentCheckerHooks() { export function createCommentCheckerHooks(config?: CommentCheckerConfig) {
debugLog("createCommentCheckerHooks called") debugLog("createCommentCheckerHooks called", { config })
// Start background CLI initialization (may trigger lazy download) // Start background CLI initialization (may trigger lazy download)
startBackgroundInit() startBackgroundInit()
@@ -123,7 +124,7 @@ export function createCommentCheckerHooks() {
// CLI mode only // CLI mode only
debugLog("using CLI:", cliPath) debugLog("using CLI:", cliPath)
await processWithCli(input, pendingCall, output, cliPath) await processWithCli(input, pendingCall, output, cliPath, config?.custom_prompt)
} catch (err) { } catch (err) {
debugLog("tool.execute.after failed:", err) debugLog("tool.execute.after failed:", err)
} }
@@ -135,7 +136,8 @@ async function processWithCli(
input: { tool: string; sessionID: string; callID: string }, input: { tool: string; sessionID: string; callID: string },
pendingCall: PendingCall, pendingCall: PendingCall,
output: { output: string }, output: { output: string },
cliPath: string cliPath: string,
customPrompt?: string
): Promise<void> { ): Promise<void> {
debugLog("using CLI mode with path:", cliPath) debugLog("using CLI mode with path:", cliPath)
@@ -154,7 +156,7 @@ async function processWithCli(
}, },
} }
const result = await runCommentChecker(hookInput, cliPath) const result = await runCommentChecker(hookInput, cliPath, customPrompt)
if (result.hasComments && result.message) { if (result.hasComments && result.message) {
debugLog("CLI detected comments, appending message") debugLog("CLI detected comments, appending message")

View File

@@ -51,14 +51,15 @@ function isExtendedThinkingModel(modelID: string): boolean {
} }
/** /**
* Check if a message has tool parts (tool_use) * Check if a message has any content parts (tool_use, text, or other non-thinking content)
*/ */
function hasToolParts(parts: Part[]): boolean { function hasContentParts(parts: Part[]): boolean {
if (!parts || parts.length === 0) return false if (!parts || parts.length === 0) return false
return parts.some((part: Part) => { return parts.some((part: Part) => {
const type = part.type as string const type = part.type as string
return type === "tool" || type === "tool_use" // Include tool parts and text parts (anything that's not thinking/reasoning)
return type === "tool" || type === "tool_use" || type === "text"
}) })
} }
@@ -154,8 +155,8 @@ export function createThinkingBlockValidatorHook(): MessagesTransformHook {
// Only check assistant messages // Only check assistant messages
if (msg.info.role !== "assistant") continue if (msg.info.role !== "assistant") continue
// Check if message has tool parts but doesn't start with thinking // Check if message has content parts but doesn't start with thinking
if (hasToolParts(msg.parts) && !startsWithThinkingBlock(msg.parts)) { if (hasContentParts(msg.parts) && !startsWithThinkingBlock(msg.parts)) {
// Find thinking content from previous turns // Find thinking content from previous turns
const previousThinking = findPreviousThinkingContent(messages, i) const previousThinking = findPreviousThinkingContent(messages, i)