mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-21 00:21:25 +00:00
Implements prompt-based hooks that allow a smaller/different model to step in at specific agent lifecycle events and provide read-only advisory output. Hook events supported: - PreToolUse: fires before tool execution (with optional tool name matcher) - PostToolUse: fires after tool execution completes - Stop: fires when attempt_completion is invoked Configuration via .roo/hooks.json (project) and ~/.roo/hooks.json (global), using a Claude Code-compatible format. Hooks are read-only (no tool access) and receive conversation context. Uses the active profile API configuration. New files: - src/shared/hooks.ts: types, interfaces, and validation - src/services/hooks/HooksManager.ts: config loading with file watchers - src/services/hooks/HookExecutor.ts: hook prompt building and execution - src/shared/__tests__/hooks.spec.ts: 18 validation tests - src/services/hooks/__tests__/HookExecutor.spec.ts: 15 executor tests Modified files: - packages/types/src/message.ts: added hook_output ClineSay type - src/core/webview/ClineProvider.ts: HooksManager initialization - src/core/assistant-message/presentAssistantMessage.ts: PreToolUse/PostToolUse hooks - src/core/tools/AttemptCompletionTool.ts: Stop hook Addresses #11504
147 lines
3.8 KiB
TypeScript
147 lines
3.8 KiB
TypeScript
/**
|
|
* HookExecutor runs prompt-based hooks by sending the hook prompt (with context)
|
|
* to the configured API provider via the singleCompletionHandler.
|
|
*
|
|
* Hooks are read-only: they have no tool access and can only produce advisory text.
|
|
* The advisory text is injected into the conversation as informational context.
|
|
*/
|
|
|
|
import type { ProviderSettings } from "@roo-code/types"
|
|
|
|
import { singleCompletionHandler } from "../../utils/single-completion-handler"
|
|
import type { HookDefinition, HookEventName, HookContext, HookResult } from "../../shared/hooks"
|
|
|
|
/**
|
|
* Builds a full prompt string from the hook definition and context.
|
|
*/
|
|
export function buildHookPrompt(hook: HookDefinition, context: HookContext): string {
|
|
const parts: string[] = []
|
|
|
|
parts.push("You are a hook that runs at a specific point in an AI coding assistant's workflow.")
|
|
parts.push("Your role is advisory only - you cannot use tools or take actions.")
|
|
parts.push("Provide concise, actionable feedback based on the context below.")
|
|
parts.push("")
|
|
|
|
parts.push(`## Event: ${context.event}`)
|
|
parts.push("")
|
|
|
|
if (context.toolName) {
|
|
parts.push(`**Tool:** ${context.toolName}`)
|
|
}
|
|
|
|
if (context.toolInput) {
|
|
parts.push(`**Tool Input:**`)
|
|
parts.push("```json")
|
|
try {
|
|
parts.push(JSON.stringify(context.toolInput, null, 2))
|
|
} catch {
|
|
parts.push("(unable to serialize tool input)")
|
|
}
|
|
parts.push("```")
|
|
}
|
|
|
|
if (context.toolResult) {
|
|
parts.push(`**Tool Result:**`)
|
|
// Truncate very long results
|
|
const maxResultLen = 2000
|
|
const truncated =
|
|
context.toolResult.length > maxResultLen
|
|
? context.toolResult.slice(0, maxResultLen) + "\n... (truncated)"
|
|
: context.toolResult
|
|
parts.push(truncated)
|
|
}
|
|
|
|
if (context.completionResult) {
|
|
parts.push(`**Completion Result:**`)
|
|
parts.push(context.completionResult)
|
|
}
|
|
|
|
if (context.conversationSummary) {
|
|
parts.push("")
|
|
parts.push("## Recent Conversation Context")
|
|
// Truncate to keep within reasonable limits
|
|
const maxContextLen = 4000
|
|
const truncated =
|
|
context.conversationSummary.length > maxContextLen
|
|
? context.conversationSummary.slice(0, maxContextLen) + "\n... (truncated)"
|
|
: context.conversationSummary
|
|
parts.push(truncated)
|
|
}
|
|
|
|
parts.push("")
|
|
parts.push("## Your Task")
|
|
parts.push(hook.prompt)
|
|
|
|
return parts.join("\n")
|
|
}
|
|
|
|
/**
|
|
* Executes a single hook and returns the result.
|
|
*/
|
|
export async function executeHook(
|
|
hook: HookDefinition,
|
|
context: HookContext,
|
|
apiConfiguration: ProviderSettings,
|
|
hookIndex: number,
|
|
): Promise<HookResult | null> {
|
|
try {
|
|
const prompt = buildHookPrompt(hook, context)
|
|
const output = await singleCompletionHandler(apiConfiguration, prompt)
|
|
|
|
if (!output || output.trim().length === 0) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
output: output.trim(),
|
|
event: context.event,
|
|
hookIndex,
|
|
}
|
|
} catch (error) {
|
|
console.warn(`[HookExecutor] Failed to execute hook (${context.event}[${hookIndex}]):`, error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes all matching hooks for an event and returns their combined results.
|
|
* Hooks are executed sequentially to avoid overwhelming the API.
|
|
*/
|
|
export async function executeHooks(
|
|
hooks: HookDefinition[],
|
|
context: HookContext,
|
|
apiConfiguration: ProviderSettings,
|
|
): Promise<HookResult[]> {
|
|
const results: HookResult[] = []
|
|
|
|
for (let i = 0; i < hooks.length; i++) {
|
|
const result = await executeHook(hooks[i], context, apiConfiguration, i)
|
|
|
|
if (result) {
|
|
results.push(result)
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
/**
|
|
* Formats hook results into a text block that can be injected into the
|
|
* conversation context.
|
|
*/
|
|
export function formatHookResults(results: HookResult[]): string {
|
|
if (results.length === 0) {
|
|
return ""
|
|
}
|
|
|
|
const parts: string[] = []
|
|
parts.push("[Hook Advisory Output]")
|
|
|
|
for (const result of results) {
|
|
parts.push(result.output)
|
|
}
|
|
|
|
parts.push("[End Hook Advisory Output]")
|
|
|
|
return parts.join("\n\n")
|
|
}
|