Merge pull request #1812 from code-yeongyu/refactor/remove-subagent-question-blocker-hook
refactor: remove redundant subagent-question-blocker hook
This commit is contained in:
commit
6df24d3592
@ -69,7 +69,6 @@
|
|||||||
"directory-readme-injector",
|
"directory-readme-injector",
|
||||||
"empty-task-response-detector",
|
"empty-task-response-detector",
|
||||||
"think-mode",
|
"think-mode",
|
||||||
"subagent-question-blocker",
|
|
||||||
"anthropic-context-window-limit-recovery",
|
"anthropic-context-window-limit-recovery",
|
||||||
"preemptive-compaction",
|
"preemptive-compaction",
|
||||||
"rules-injector",
|
"rules-injector",
|
||||||
|
|||||||
@ -13,7 +13,6 @@ export const HookNameSchema = z.enum([
|
|||||||
"directory-readme-injector",
|
"directory-readme-injector",
|
||||||
"empty-task-response-detector",
|
"empty-task-response-detector",
|
||||||
"think-mode",
|
"think-mode",
|
||||||
"subagent-question-blocker",
|
|
||||||
"anthropic-context-window-limit-recovery",
|
"anthropic-context-window-limit-recovery",
|
||||||
"preemptive-compaction",
|
"preemptive-compaction",
|
||||||
"rules-injector",
|
"rules-injector",
|
||||||
|
|||||||
@ -36,7 +36,6 @@ export { createStartWorkHook } from "./start-work";
|
|||||||
export { createAtlasHook } from "./atlas";
|
export { createAtlasHook } from "./atlas";
|
||||||
export { createDelegateTaskRetryHook } from "./delegate-task-retry";
|
export { createDelegateTaskRetryHook } from "./delegate-task-retry";
|
||||||
export { createQuestionLabelTruncatorHook } from "./question-label-truncator";
|
export { createQuestionLabelTruncatorHook } from "./question-label-truncator";
|
||||||
export { createSubagentQuestionBlockerHook } from "./subagent-question-blocker";
|
|
||||||
export { createStopContinuationGuardHook, type StopContinuationGuard } from "./stop-continuation-guard";
|
export { createStopContinuationGuardHook, type StopContinuationGuard } from "./stop-continuation-guard";
|
||||||
export { createCompactionContextInjector } from "./compaction-context-injector";
|
export { createCompactionContextInjector } from "./compaction-context-injector";
|
||||||
export { createCompactionTodoPreserverHook } from "./compaction-todo-preserver";
|
export { createCompactionTodoPreserverHook } from "./compaction-todo-preserver";
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
import type { Hooks } from "@opencode-ai/plugin"
|
|
||||||
import { subagentSessions } from "../../features/claude-code-session-state"
|
|
||||||
import { log } from "../../shared"
|
|
||||||
|
|
||||||
export function createSubagentQuestionBlockerHook(): Hooks {
|
|
||||||
return {
|
|
||||||
"tool.execute.before": async (input) => {
|
|
||||||
const toolName = input.tool?.toLowerCase()
|
|
||||||
if (toolName !== "question" && toolName !== "askuserquestion") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subagentSessions.has(input.sessionID)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log("[subagent-question-blocker] Blocking question tool call from subagent session", {
|
|
||||||
sessionID: input.sessionID,
|
|
||||||
tool: input.tool,
|
|
||||||
})
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
"Question tool is disabled for subagent sessions. " +
|
|
||||||
"Subagents should complete their work autonomously without asking questions to users. " +
|
|
||||||
"If you need clarification, return to the parent agent with your findings and uncertainties."
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
import { describe, test, expect, beforeEach } from "bun:test"
|
|
||||||
import { createSubagentQuestionBlockerHook } from "./index"
|
|
||||||
import { subagentSessions, _resetForTesting } from "../../features/claude-code-session-state"
|
|
||||||
|
|
||||||
describe("createSubagentQuestionBlockerHook", () => {
|
|
||||||
const hook = createSubagentQuestionBlockerHook()
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
_resetForTesting()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("tool.execute.before", () => {
|
|
||||||
test("allows question tool for non-subagent sessions", async () => {
|
|
||||||
// given
|
|
||||||
const sessionID = "ses_main"
|
|
||||||
const input = { tool: "question", sessionID, callID: "call_1" }
|
|
||||||
const output = { args: { questions: [] } }
|
|
||||||
|
|
||||||
// when
|
|
||||||
const result = hook["tool.execute.before"]?.(input as any, output as any)
|
|
||||||
|
|
||||||
// then
|
|
||||||
await expect(result).resolves.toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("blocks question tool for subagent sessions", async () => {
|
|
||||||
// given
|
|
||||||
const sessionID = "ses_subagent"
|
|
||||||
subagentSessions.add(sessionID)
|
|
||||||
const input = { tool: "question", sessionID, callID: "call_1" }
|
|
||||||
const output = { args: { questions: [] } }
|
|
||||||
|
|
||||||
// when
|
|
||||||
const result = hook["tool.execute.before"]?.(input as any, output as any)
|
|
||||||
|
|
||||||
// then
|
|
||||||
await expect(result).rejects.toThrow("Question tool is disabled for subagent sessions")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("blocks Question tool (case insensitive) for subagent sessions", async () => {
|
|
||||||
// given
|
|
||||||
const sessionID = "ses_subagent"
|
|
||||||
subagentSessions.add(sessionID)
|
|
||||||
const input = { tool: "Question", sessionID, callID: "call_1" }
|
|
||||||
const output = { args: { questions: [] } }
|
|
||||||
|
|
||||||
// when
|
|
||||||
const result = hook["tool.execute.before"]?.(input as any, output as any)
|
|
||||||
|
|
||||||
// then
|
|
||||||
await expect(result).rejects.toThrow("Question tool is disabled for subagent sessions")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("blocks AskUserQuestion tool for subagent sessions", async () => {
|
|
||||||
// given
|
|
||||||
const sessionID = "ses_subagent"
|
|
||||||
subagentSessions.add(sessionID)
|
|
||||||
const input = { tool: "AskUserQuestion", sessionID, callID: "call_1" }
|
|
||||||
const output = { args: { questions: [] } }
|
|
||||||
|
|
||||||
// when
|
|
||||||
const result = hook["tool.execute.before"]?.(input as any, output as any)
|
|
||||||
|
|
||||||
// then
|
|
||||||
await expect(result).rejects.toThrow("Question tool is disabled for subagent sessions")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("ignores non-question tools for subagent sessions", async () => {
|
|
||||||
// given
|
|
||||||
const sessionID = "ses_subagent"
|
|
||||||
subagentSessions.add(sessionID)
|
|
||||||
const input = { tool: "bash", sessionID, callID: "call_1" }
|
|
||||||
const output = { args: { command: "ls" } }
|
|
||||||
|
|
||||||
// when
|
|
||||||
const result = hook["tool.execute.before"]?.(input as any, output as any)
|
|
||||||
|
|
||||||
// then
|
|
||||||
await expect(result).resolves.toBeUndefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export { createSubagentQuestionBlockerHook } from "./hook";
|
|
||||||
@ -19,7 +19,6 @@ import {
|
|||||||
createPrometheusMdOnlyHook,
|
createPrometheusMdOnlyHook,
|
||||||
createSisyphusJuniorNotepadHook,
|
createSisyphusJuniorNotepadHook,
|
||||||
createQuestionLabelTruncatorHook,
|
createQuestionLabelTruncatorHook,
|
||||||
createSubagentQuestionBlockerHook,
|
|
||||||
createPreemptiveCompactionHook,
|
createPreemptiveCompactionHook,
|
||||||
} from "../../hooks"
|
} from "../../hooks"
|
||||||
import { createAnthropicEffortHook } from "../../hooks/anthropic-effort"
|
import { createAnthropicEffortHook } from "../../hooks/anthropic-effort"
|
||||||
@ -49,7 +48,6 @@ export type SessionHooks = {
|
|||||||
prometheusMdOnly: ReturnType<typeof createPrometheusMdOnlyHook> | null
|
prometheusMdOnly: ReturnType<typeof createPrometheusMdOnlyHook> | null
|
||||||
sisyphusJuniorNotepad: ReturnType<typeof createSisyphusJuniorNotepadHook> | null
|
sisyphusJuniorNotepad: ReturnType<typeof createSisyphusJuniorNotepadHook> | null
|
||||||
questionLabelTruncator: ReturnType<typeof createQuestionLabelTruncatorHook>
|
questionLabelTruncator: ReturnType<typeof createQuestionLabelTruncatorHook>
|
||||||
subagentQuestionBlocker: ReturnType<typeof createSubagentQuestionBlockerHook>
|
|
||||||
taskResumeInfo: ReturnType<typeof createTaskResumeInfoHook>
|
taskResumeInfo: ReturnType<typeof createTaskResumeInfoHook>
|
||||||
anthropicEffort: ReturnType<typeof createAnthropicEffortHook> | null
|
anthropicEffort: ReturnType<typeof createAnthropicEffortHook> | null
|
||||||
}
|
}
|
||||||
@ -149,7 +147,6 @@ export function createSessionHooks(args: {
|
|||||||
: null
|
: null
|
||||||
|
|
||||||
const questionLabelTruncator = createQuestionLabelTruncatorHook()
|
const questionLabelTruncator = createQuestionLabelTruncatorHook()
|
||||||
const subagentQuestionBlocker = createSubagentQuestionBlockerHook()
|
|
||||||
const taskResumeInfo = createTaskResumeInfoHook()
|
const taskResumeInfo = createTaskResumeInfoHook()
|
||||||
|
|
||||||
const anthropicEffort = isHookEnabled("anthropic-effort")
|
const anthropicEffort = isHookEnabled("anthropic-effort")
|
||||||
@ -174,7 +171,6 @@ export function createSessionHooks(args: {
|
|||||||
prometheusMdOnly,
|
prometheusMdOnly,
|
||||||
sisyphusJuniorNotepad,
|
sisyphusJuniorNotepad,
|
||||||
questionLabelTruncator,
|
questionLabelTruncator,
|
||||||
subagentQuestionBlocker,
|
|
||||||
taskResumeInfo,
|
taskResumeInfo,
|
||||||
anthropicEffort,
|
anthropicEffort,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,38 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
const { describe, expect, test } = require("bun:test")
|
||||||
import { createToolExecuteBeforeHandler } from "./tool-execute-before"
|
const { createToolExecuteBeforeHandler } = require("./tool-execute-before")
|
||||||
import type { CreatedHooks } from "../create-hooks"
|
|
||||||
|
|
||||||
describe("createToolExecuteBeforeHandler", () => {
|
describe("createToolExecuteBeforeHandler", () => {
|
||||||
|
test("does not execute subagent question blocker hook for question tool", async () => {
|
||||||
|
//#given
|
||||||
|
const ctx = {
|
||||||
|
client: {
|
||||||
|
session: {
|
||||||
|
messages: async () => ({ data: [] }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const hooks = {
|
||||||
|
subagentQuestionBlocker: {
|
||||||
|
"tool.execute.before": async () => {
|
||||||
|
throw new Error("subagentQuestionBlocker should not run")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = createToolExecuteBeforeHandler({ ctx, hooks })
|
||||||
|
const input = { tool: "question", sessionID: "ses_sub", callID: "call_1" }
|
||||||
|
const output = { args: { questions: [] } as Record<string, unknown> }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const run = handler(input, output)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
await expect(run).resolves.toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
describe("task tool subagent_type normalization", () => {
|
describe("task tool subagent_type normalization", () => {
|
||||||
const emptyHooks = {} as CreatedHooks
|
const emptyHooks = {}
|
||||||
|
|
||||||
function createCtxWithSessionMessages(messages: Array<{ info?: { agent?: string; role?: string } }> = []) {
|
function createCtxWithSessionMessages(messages: Array<{ info?: { agent?: string; role?: string } }> = []) {
|
||||||
return {
|
return {
|
||||||
@ -13,7 +41,7 @@ describe("createToolExecuteBeforeHandler", () => {
|
|||||||
messages: async () => ({ data: messages }),
|
messages: async () => ({ data: messages }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as Parameters<typeof createToolExecuteBeforeHandler>[0]["ctx"]
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("sets subagent_type to sisyphus-junior when category is provided without subagent_type", async () => {
|
test("sets subagent_type to sisyphus-junior when category is provided without subagent_type", async () => {
|
||||||
@ -136,3 +164,5 @@ describe("createToolExecuteBeforeHandler", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|||||||
@ -17,7 +17,6 @@ export function createToolExecuteBeforeHandler(args: {
|
|||||||
const { ctx, hooks } = args
|
const { ctx, hooks } = args
|
||||||
|
|
||||||
return async (input, output): Promise<void> => {
|
return async (input, output): Promise<void> => {
|
||||||
await hooks.subagentQuestionBlocker?.["tool.execute.before"]?.(input, output)
|
|
||||||
await hooks.writeExistingFileGuard?.["tool.execute.before"]?.(input, output)
|
await hooks.writeExistingFileGuard?.["tool.execute.before"]?.(input, output)
|
||||||
await hooks.questionLabelTruncator?.["tool.execute.before"]?.(input, output)
|
await hooks.questionLabelTruncator?.["tool.execute.before"]?.(input, output)
|
||||||
await hooks.claudeCodeHooks?.["tool.execute.before"]?.(input, output)
|
await hooks.claudeCodeHooks?.["tool.execute.before"]?.(input, output)
|
||||||
|
|||||||
91
src/tools/call-omo-agent/sync-executor.test.ts
Normal file
91
src/tools/call-omo-agent/sync-executor.test.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
const { describe, test, expect, mock } = require("bun:test")
|
||||||
|
|
||||||
|
mock.module("./session-creator", () => ({
|
||||||
|
createOrGetSession: mock(async () => ({ sessionID: "ses-test-123" })),
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("./completion-poller", () => ({
|
||||||
|
waitForCompletion: mock(async () => {}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("./message-processor", () => ({
|
||||||
|
processMessages: mock(async () => "agent response"),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe("executeSync", () => {
|
||||||
|
test("passes question=false via tools parameter to block question tool", async () => {
|
||||||
|
//#given
|
||||||
|
const { executeSync } = require("./sync-executor")
|
||||||
|
|
||||||
|
let promptArgs: any
|
||||||
|
const promptAsync = mock(async (input: any) => {
|
||||||
|
promptArgs = input
|
||||||
|
return { data: {} }
|
||||||
|
})
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
subagent_type: "explore",
|
||||||
|
description: "test task",
|
||||||
|
prompt: "find something",
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolContext = {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
messageID: "msg-1",
|
||||||
|
agent: "sisyphus",
|
||||||
|
abort: new AbortController().signal,
|
||||||
|
metadata: mock(async () => {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
client: {
|
||||||
|
session: { promptAsync },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await executeSync(args, toolContext, ctx as any)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(promptAsync).toHaveBeenCalled()
|
||||||
|
expect(promptArgs.body.tools.question).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("passes task=false via tools parameter", async () => {
|
||||||
|
//#given
|
||||||
|
const { executeSync } = require("./sync-executor")
|
||||||
|
|
||||||
|
let promptArgs: any
|
||||||
|
const promptAsync = mock(async (input: any) => {
|
||||||
|
promptArgs = input
|
||||||
|
return { data: {} }
|
||||||
|
})
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
subagent_type: "librarian",
|
||||||
|
description: "search docs",
|
||||||
|
prompt: "find docs",
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolContext = {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
messageID: "msg-2",
|
||||||
|
agent: "sisyphus",
|
||||||
|
abort: new AbortController().signal,
|
||||||
|
metadata: mock(async () => {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
client: {
|
||||||
|
session: { promptAsync },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await executeSync(args, toolContext, ctx as any)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(promptAsync).toHaveBeenCalled()
|
||||||
|
expect(promptArgs.body.tools.task).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -35,6 +35,7 @@ export async function executeSync(
|
|||||||
tools: {
|
tools: {
|
||||||
...getAgentToolRestrictions(args.subagent_type),
|
...getAgentToolRestrictions(args.subagent_type),
|
||||||
task: false,
|
task: false,
|
||||||
|
question: false,
|
||||||
},
|
},
|
||||||
parts: [{ type: "text", text: args.prompt }],
|
parts: [{ type: "text", text: args.prompt }],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,45 @@
|
|||||||
const { describe, test, expect, mock } = require("bun:test")
|
const { describe, test, expect, mock } = require("bun:test")
|
||||||
|
|
||||||
describe("sendSyncPrompt", () => {
|
describe("sendSyncPrompt", () => {
|
||||||
|
test("passes question=false via tools parameter", async () => {
|
||||||
|
//#given
|
||||||
|
const { sendSyncPrompt } = require("./sync-prompt-sender")
|
||||||
|
|
||||||
|
let promptArgs: any
|
||||||
|
const promptAsync = mock(async (input: any) => {
|
||||||
|
promptArgs = input
|
||||||
|
return { data: {} }
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
promptAsync,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
sessionID: "test-session",
|
||||||
|
agentToUse: "sisyphus-junior",
|
||||||
|
args: {
|
||||||
|
description: "test task",
|
||||||
|
prompt: "test prompt",
|
||||||
|
run_in_background: false,
|
||||||
|
load_skills: [],
|
||||||
|
},
|
||||||
|
systemContent: undefined,
|
||||||
|
categoryModel: undefined,
|
||||||
|
toastManager: null,
|
||||||
|
taskId: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await sendSyncPrompt(mockClient as any, input)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(promptAsync).toHaveBeenCalled()
|
||||||
|
expect(promptArgs.body.tools.question).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
test("applies agent tool restrictions for explore agent", async () => {
|
test("applies agent tool restrictions for explore agent", async () => {
|
||||||
//#given
|
//#given
|
||||||
const { sendSyncPrompt } = require("./sync-prompt-sender")
|
const { sendSyncPrompt } = require("./sync-prompt-sender")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user