Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
12 kB · 339 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340import { randomUUID } from 'crypto'import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'import { query } from '../../query.js'import { logEvent } from '../../services/analytics/index.js'import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/metadata.js'import type { ToolUseContext } from '../../Tool.js'import { type Tool, toolMatchesName } from '../../Tool.js'import { SYNTHETIC_OUTPUT_TOOL_NAME } from '../../tools/SyntheticOutputTool/SyntheticOutputTool.js'import { ALL_AGENT_DISALLOWED_TOOLS } from '../../tools.js'import { asAgentId } from '../../types/ids.js'import type { Message } from '../../types/message.js'import { createAbortController } from '../abortController.js'import { createAttachmentMessage } from '../attachments.js'import { createCombinedAbortSignal } from '../combinedAbortSignal.js'import { logForDebugging } from '../debug.js'import { errorMessage } from '../errors.js'import type { HookResult } from '../hooks.js'import { createUserMessage, handleMessageFromStream } from '../messages.js'import { getSmallFastModel } from '../model/model.js'import { hasPermissionsToUseTool } from '../permissions/permissions.js'import { getAgentTranscriptPath, getTranscriptPath } from '../sessionStorage.js'import type { AgentHook } from '../settings/types.js'import { jsonStringify } from '../slowOperations.js'import { asSystemPrompt } from '../systemPromptType.js'import { addArgumentsToPrompt, createStructuredOutputTool, hookResponseSchema, registerStructuredOutputEnforcement,} from './hookHelpers.js'import { clearSessionHooks } from './sessionHooks.js'
/** * Execute an agent-based hook using a multi-turn LLM query */export async function execAgentHook( hook: AgentHook, hookName: string, hookEvent: HookEvent, jsonInput: string, signal: AbortSignal, toolUseContext: ToolUseContext, toolUseID: string | undefined, // Kept for signature stability with the other exec*Hook functions. // Was used by hook.prompt(messages) before the .transform() was removed // (CC-79) — the only consumer of that was ExitPlanModeV2Tool's // programmatic construction, since refactored into VerifyPlanExecutionTool. _messages: Message[], agentName?: string,): Promise<HookResult> { const effectiveToolUseID = toolUseID || `hook-${randomUUID()}`
// Get transcript path from context const transcriptPath = toolUseContext.agentId ? getAgentTranscriptPath(toolUseContext.agentId) : getTranscriptPath() const hookStartTime = Date.now() try { // Replace $ARGUMENTS with the JSON input const processedPrompt = addArgumentsToPrompt(hook.prompt, jsonInput) logForDebugging( `Hooks: Processing agent hook with prompt: ${processedPrompt}`, )
// Create user message directly - no need for processUserInput which would // trigger UserPromptSubmit hooks and cause infinite recursion const userMessage = createUserMessage({ content: processedPrompt }) const agentMessages = [userMessage]
logForDebugging( `Hooks: Starting agent query with ${agentMessages.length} messages`, )
// Setup timeout and combine with parent signal const hookTimeoutMs = hook.timeout ? hook.timeout * 1000 : 60000 const hookAbortController = createAbortController()
// Combine parent signal with timeout, and have it abort our controller const { signal: parentTimeoutSignal, cleanup: cleanupCombinedSignal } = createCombinedAbortSignal(signal, { timeoutMs: hookTimeoutMs }) const onParentTimeout = () => hookAbortController.abort() parentTimeoutSignal.addEventListener('abort', onParentTimeout)
// Combined signal is just our controller's signal now const combinedSignal = hookAbortController.signal
try { // Create StructuredOutput tool with our schema const structuredOutputTool = createStructuredOutputTool()
// Filter out any existing StructuredOutput tool to avoid duplicates with different schemas // (e.g., when parent context has a StructuredOutput tool from --json-schema flag) const filteredTools = toolUseContext.options.tools.filter( tool => !toolMatchesName(tool, SYNTHETIC_OUTPUT_TOOL_NAME), )
// Use all available tools plus our structured output tool // Filter out disallowed agent tools to prevent stop hook agents from spawning subagents // or entering plan mode, and filter out duplicate StructuredOutput tools const tools: Tool[] = [ ...filteredTools.filter( tool => !ALL_AGENT_DISALLOWED_TOOLS.has(tool.name), ), structuredOutputTool, ]
const systemPrompt = asSystemPrompt([ `You are verifying a stop condition in Claude Code. Your task is to verify that the agent completed the given plan. The conversation transcript is available at: ${transcriptPath}\nYou can read this file to analyze the conversation history if needed.
Use the available tools to inspect the codebase and verify the condition.Use as few steps as possible - be efficient and direct.
When done, return your result using the ${SYNTHETIC_OUTPUT_TOOL_NAME} tool with:- ok: true if the condition is met- ok: false with reason if the condition is not met`, ])
const model = hook.model ?? getSmallFastModel() const MAX_AGENT_TURNS = 50
// Create unique agentId for this hook agent const hookAgentId = asAgentId(`hook-agent-${randomUUID()}`)
// Create a modified toolUseContext for the agent const agentToolUseContext: ToolUseContext = { ...toolUseContext, agentId: hookAgentId, abortController: hookAbortController, options: { ...toolUseContext.options, tools, mainLoopModel: model, isNonInteractiveSession: true, thinkingConfig: { type: 'disabled' as const }, }, setInProgressToolUseIDs: () => {}, getAppState() { const appState = toolUseContext.getAppState() // Add session rule to allow reading transcript file const existingSessionRules = appState.toolPermissionContext.alwaysAllowRules.session ?? [] return { ...appState, toolPermissionContext: { ...appState.toolPermissionContext, mode: 'dontAsk' as const, alwaysAllowRules: { ...appState.toolPermissionContext.alwaysAllowRules, session: [...existingSessionRules, `Read(/${transcriptPath})`], }, }, } }, }
// Register a session-level stop hook to enforce structured output registerStructuredOutputEnforcement( toolUseContext.setAppState, hookAgentId, )
let structuredOutputResult: { ok: boolean; reason?: string } | null = null let turnCount = 0 let hitMaxTurns = false
// Use query() for multi-turn execution for await (const message of query({ messages: agentMessages, systemPrompt, userContext: {}, systemContext: {}, canUseTool: hasPermissionsToUseTool, toolUseContext: agentToolUseContext, querySource: 'hook_agent', })) { // Process stream events to update response length in the spinner handleMessageFromStream( message, () => {}, // onMessage - we handle messages below newContent => toolUseContext.setResponseLength( length => length + newContent.length, ), toolUseContext.setStreamMode ?? (() => {}), () => {}, // onStreamingToolUses - not needed for hooks )
// Skip streaming events for further processing if ( message.type === 'stream_event' || message.type === 'stream_request_start' ) { continue }
// Count assistant turns if (message.type === 'assistant') { turnCount++
// Check if we've hit the turn limit if (turnCount >= MAX_AGENT_TURNS) { hitMaxTurns = true logForDebugging( `Hooks: Agent turn ${turnCount} hit max turns, aborting`, ) hookAbortController.abort() break } }
// Check for structured output in attachments if ( message.type === 'attachment' && message.attachment.type === 'structured_output' ) { const parsed = hookResponseSchema().safeParse(message.attachment.data) if (parsed.success) { structuredOutputResult = parsed.data logForDebugging( `Hooks: Got structured output: ${jsonStringify(structuredOutputResult)}`, ) // Got structured output, abort and exit hookAbortController.abort() break } } }
parentTimeoutSignal.removeEventListener('abort', onParentTimeout) cleanupCombinedSignal()
// Clean up the session hook we registered for this agent clearSessionHooks(toolUseContext.setAppState, hookAgentId)
// Check if we got a result if (!structuredOutputResult) { // If we hit max turns, just log and return cancelled (no UI message) if (hitMaxTurns) { logForDebugging( `Hooks: Agent hook did not complete within ${MAX_AGENT_TURNS} turns`, ) logEvent('tengu_agent_stop_hook_max_turns', { durationMs: Date.now() - hookStartTime, turnCount, agentName: agentName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }) return { hook, outcome: 'cancelled', } }
// For other cases (e.g., agent finished without calling structured output tool), // just log and return cancelled (don't show error to user) logForDebugging(`Hooks: Agent hook did not return structured output`) logEvent('tengu_agent_stop_hook_error', { durationMs: Date.now() - hookStartTime, turnCount, errorType: 1, // 1 = no structured output agentName: agentName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }) return { hook, outcome: 'cancelled', } }
// Return result based on structured output if (!structuredOutputResult.ok) { logForDebugging( `Hooks: Agent hook condition was not met: ${structuredOutputResult.reason}`, ) return { hook, outcome: 'blocking', blockingError: { blockingError: `Agent hook condition was not met: ${structuredOutputResult.reason}`, command: hook.prompt, }, } }
// Condition was met logForDebugging(`Hooks: Agent hook condition was met`) logEvent('tengu_agent_stop_hook_success', { durationMs: Date.now() - hookStartTime, turnCount, agentName: agentName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }) return { hook, outcome: 'success', message: createAttachmentMessage({ type: 'hook_success', hookName, toolUseID: effectiveToolUseID, hookEvent, content: '', }), } } catch (error) { parentTimeoutSignal.removeEventListener('abort', onParentTimeout) cleanupCombinedSignal()
if (combinedSignal.aborted) { return { hook, outcome: 'cancelled', } } throw error } } catch (error) { const errorMsg = errorMessage(error) logForDebugging(`Hooks: Agent hook error: ${errorMsg}`) logEvent('tengu_agent_stop_hook_error', { durationMs: Date.now() - hookStartTime, errorType: 2, // 2 = general error agentName: agentName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }) return { hook, outcome: 'non_blocking_error', message: createAttachmentMessage({ type: 'hook_non_blocking_error', hookName, toolUseID: effectiveToolUseID, hookEvent, stderr: `Error executing agent hook: ${errorMsg}`, stdout: '', exitCode: 1, }), } }}