diff --git a/src/config/schema/hooks.ts b/src/config/schema/hooks.ts index 4acc37584..776cf3d83 100644 --- a/src/config/schema/hooks.ts +++ b/src/config/schema/hooks.ts @@ -51,6 +51,7 @@ export const HookNameSchema = z.enum([ "anthropic-effort", "hashline-read-enhancer", "read-image-resizer", + "delegate-task-english-directive", ]) export type HookName = z.infer diff --git a/src/hooks/delegate-task-english-directive/hook.ts b/src/hooks/delegate-task-english-directive/hook.ts new file mode 100644 index 000000000..2397cd090 --- /dev/null +++ b/src/hooks/delegate-task-english-directive/hook.ts @@ -0,0 +1,24 @@ +export const TARGET_SUBAGENT_TYPES = ["explore", "librarian", "oracle", "plan"] as const + +export const ENGLISH_DIRECTIVE = + "**YOU MUST ALWAYS THINK, REASON, AND RESPOND IN ENGLISH REGARDLESS OF THE USER'S QUERY LANGUAGE.**" + +export function createDelegateTaskEnglishDirectiveHook() { + return { + "tool.execute.before": async ( + input: { tool: string; sessionID: string; callID: string; input: Record }, + _output: { title: string; output: string; metadata: unknown } + ) => { + if (input.tool.toLowerCase() !== "task") return + + const args = input.input + const subagentType = args.subagent_type + if (typeof subagentType !== "string") return + if (!TARGET_SUBAGENT_TYPES.includes(subagentType as (typeof TARGET_SUBAGENT_TYPES)[number])) return + + if (typeof args.prompt === "string") { + args.prompt = `${args.prompt}\n\n${ENGLISH_DIRECTIVE}` + } + }, + } +} diff --git a/src/hooks/delegate-task-english-directive/index.test.ts b/src/hooks/delegate-task-english-directive/index.test.ts new file mode 100644 index 000000000..38d6cadb8 --- /dev/null +++ b/src/hooks/delegate-task-english-directive/index.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from "bun:test" + +import { createDelegateTaskEnglishDirectiveHook, ENGLISH_DIRECTIVE, TARGET_SUBAGENT_TYPES } from "./index" + +describe("delegate-task-english-directive", () => { + const hook = createDelegateTaskEnglishDirectiveHook() + const handler = hook["tool.execute.before"] + + describe("#given a task tool call with a targeted subagent_type", () => { + const targetTypes = ["explore", "librarian", "oracle", "plan"] + + for (const subagentType of targetTypes) { + describe(`#when subagent_type is "${subagentType}"`, () => { + it(`#then should append English directive to prompt`, async () => { + const originalPrompt = "Find auth patterns in the codebase" + const input = { tool: "Task", sessionID: "ses_123", callID: "call_1", input: { subagent_type: subagentType, prompt: originalPrompt } } + const output = { title: "", output: "", metadata: undefined } + + await handler(input, output) + + expect(input.input.prompt).toBe(`${originalPrompt}\n\n${ENGLISH_DIRECTIVE}`) + }) + }) + } + }) + + describe("#given a task tool call with a non-targeted subagent_type", () => { + describe("#when subagent_type is 'metis'", () => { + it("#then should not modify the prompt", async () => { + const originalPrompt = "Analyze this request" + const input = { tool: "Task", sessionID: "ses_123", callID: "call_1", input: { subagent_type: "metis", prompt: originalPrompt } } + const output = { title: "", output: "", metadata: undefined } + + await handler(input, output) + + expect(input.input.prompt).toBe(originalPrompt) + }) + }) + }) + + describe("#given a task tool call using category instead of subagent_type", () => { + describe("#when only category is provided", () => { + it("#then should not modify the prompt", async () => { + const originalPrompt = "Fix the button styling" + const input = { tool: "Task", sessionID: "ses_123", callID: "call_1", input: { category: "visual-engineering", prompt: originalPrompt } } + const output = { title: "", output: "", metadata: undefined } + + await handler(input, output) + + expect(input.input.prompt).toBe(originalPrompt) + }) + }) + }) + + describe("#given a non-task tool call", () => { + describe("#when tool is 'Bash'", () => { + it("#then should not modify anything", async () => { + const input = { tool: "Bash", sessionID: "ses_123", callID: "call_1", input: { command: "ls" } } + const output = { title: "", output: "", metadata: undefined } + + await handler(input, output) + + expect(input.input).toEqual({ command: "ls" }) + }) + }) + }) + + describe("#given a task tool call with empty prompt", () => { + describe("#when prompt is empty string", () => { + it("#then should still append directive", async () => { + const input = { tool: "Task", sessionID: "ses_123", callID: "call_1", input: { subagent_type: "explore", prompt: "" } } + const output = { title: "", output: "", metadata: undefined } + + await handler(input, output) + + expect(input.input.prompt).toBe(`\n\n${ENGLISH_DIRECTIVE}`) + }) + }) + }) + + describe("#given TARGET_SUBAGENT_TYPES constant", () => { + it("#then should contain exactly explore, librarian, oracle, and plan", () => { + expect(TARGET_SUBAGENT_TYPES).toEqual(["explore", "librarian", "oracle", "plan"]) + }) + }) + + describe("#given ENGLISH_DIRECTIVE constant", () => { + it("#then should be bold uppercase text", () => { + expect(ENGLISH_DIRECTIVE).toContain("**") + expect(ENGLISH_DIRECTIVE).toMatch(/[A-Z]/) + }) + + it("#then should instruct English-only thinking and responding", () => { + const lower = ENGLISH_DIRECTIVE.toLowerCase() + expect(lower).toContain("english") + }) + }) +}) diff --git a/src/hooks/delegate-task-english-directive/index.ts b/src/hooks/delegate-task-english-directive/index.ts new file mode 100644 index 000000000..2194a7264 --- /dev/null +++ b/src/hooks/delegate-task-english-directive/index.ts @@ -0,0 +1 @@ +export { createDelegateTaskEnglishDirectiveHook, TARGET_SUBAGENT_TYPES, ENGLISH_DIRECTIVE } from "./hook" diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 77b2c3c13..e7b7d5995 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -52,3 +52,4 @@ export { createWriteExistingFileGuardHook } from "./write-existing-file-guard"; export { createHashlineReadEnhancerHook } from "./hashline-read-enhancer"; export { createJsonErrorRecoveryHook, JSON_ERROR_TOOL_EXCLUDE_LIST, JSON_ERROR_PATTERNS, JSON_ERROR_REMINDER } from "./json-error-recovery"; export { createReadImageResizerHook } from "./read-image-resizer" +export { createDelegateTaskEnglishDirectiveHook } from "./delegate-task-english-directive" diff --git a/src/plugin/hooks/create-session-hooks.ts b/src/plugin/hooks/create-session-hooks.ts index 42f62bf9e..9dc763646 100644 --- a/src/plugin/hooks/create-session-hooks.ts +++ b/src/plugin/hooks/create-session-hooks.ts @@ -16,6 +16,7 @@ import { createRalphLoopHook, createEditErrorRecoveryHook, createDelegateTaskRetryHook, + createDelegateTaskEnglishDirectiveHook, createTaskResumeInfoHook, createStartWorkHook, createPrometheusMdOnlyHook, @@ -60,6 +61,7 @@ export type SessionHooks = { taskResumeInfo: ReturnType | null anthropicEffort: ReturnType | null runtimeFallback: ReturnType | null + delegateTaskEnglishDirective: ReturnType | null } export function createSessionHooks(args: { @@ -215,6 +217,10 @@ export function createSessionHooks(args: { ? safeHook("delegate-task-retry", () => createDelegateTaskRetryHook(ctx)) : null + const delegateTaskEnglishDirective = isHookEnabled("delegate-task-english-directive") + ? safeHook("delegate-task-english-directive", () => createDelegateTaskEnglishDirectiveHook()) + : null + const startWork = isHookEnabled("start-work") ? safeHook("start-work", () => createStartWorkHook(ctx)) : null @@ -276,6 +282,7 @@ export function createSessionHooks(args: { ralphLoop, editErrorRecovery, delegateTaskRetry, + delegateTaskEnglishDirective, startWork, prometheusMdOnly, sisyphusJuniorNotepad,