Merge pull request #2765 from code-yeongyu/fix/issue-2024

fix: skip keyword injection for non-OMO agents (fixes #2024)
This commit is contained in:
YeonGyu-Kim
2026-03-23 18:39:48 +09:00
committed by GitHub
5 changed files with 125 additions and 2 deletions

View File

@@ -2,7 +2,7 @@ export const CODE_BLOCK_PATTERN = /```[\s\S]*?```/g
export const INLINE_CODE_PATTERN = /`[^`]+`/g
// Re-export from submodules
export { isPlannerAgent, getUltraworkMessage } from "./ultrawork"
export { isPlannerAgent, isNonOmoAgent, getUltraworkMessage } from "./ultrawork"
export { SEARCH_PATTERN, SEARCH_MESSAGE } from "./search"
export { ANALYZE_PATTERN, ANALYZE_MESSAGE } from "./analyze"

View File

@@ -1,6 +1,6 @@
import type { PluginInput } from "@opencode-ai/plugin"
import { detectKeywordsWithType, extractPromptText } from "./detector"
import { isPlannerAgent } from "./constants"
import { isPlannerAgent, isNonOmoAgent } from "./constants"
import { log } from "../../shared"
import {
isSystemDirective,
@@ -45,6 +45,12 @@ export function createKeywordDetectorHook(ctx: PluginInput, _collector?: Context
const currentAgent = getSessionAgent(input.sessionID) ?? input.agent
// Skip all keyword injection for non-OMO agents (e.g., OpenCode-Builder, Plan)
if (isNonOmoAgent(currentAgent)) {
log(`[keyword-detector] Skipping keyword injection for non-OMO agent`, { sessionID: input.sessionID, agent: currentAgent })
return
}
// Remove system-reminder content to prevent automated system messages from triggering mode keywords
const cleanText = removeSystemReminders(promptText)
const modelID = input.model?.modelID

View File

@@ -746,3 +746,109 @@ describe("keyword-detector agent-specific ultrawork messages", () => {
expect(textPart!.text).not.toContain("YOU ARE A PLANNER, NOT AN IMPLEMENTER")
})
})
describe("keyword-detector non-OMO agent skipping", () => {
let logCalls: Array<{ msg: string; data?: unknown }>
let logSpy: ReturnType<typeof spyOn>
beforeEach(() => {
_resetForTesting()
logCalls = []
logSpy = spyOn(sharedModule, "log").mockImplementation((msg: string, data?: unknown) => {
logCalls.push({ msg, data })
})
})
afterEach(() => {
logSpy?.mockRestore()
_resetForTesting()
})
function createMockPluginInput() {
return {
client: {
tui: {
showToast: async () => {},
},
},
} as any
}
test("should skip all keyword injection for OpenCode-Builder agent", async () => {
// given - keyword-detector hook with Builder agent
const collector = new ContextCollector()
const hook = createKeywordDetectorHook(createMockPluginInput(), collector)
const sessionID = "builder-session"
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "ultrawork search and analyze this code" }],
}
// when - keyword detection runs with OpenCode-Builder agent
await hook["chat.message"]({ sessionID, agent: "OpenCode-Builder" }, output)
// then - no keywords should be injected
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toBe("ultrawork search and analyze this code")
})
test("should skip all keyword injection for Plan agent", async () => {
// given - keyword-detector hook with Plan agent
const collector = new ContextCollector()
const hook = createKeywordDetectorHook(createMockPluginInput(), collector)
const sessionID = "plan-session"
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "search mode analyze mode ultrawork" }],
}
// when - keyword detection runs with Plan agent
await hook["chat.message"]({ sessionID, agent: "Plan" }, output)
// then - no keywords should be injected for non-OMO Plan agent
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toBe("search mode analyze mode ultrawork")
})
test("should still inject keywords for OMO agents like Sisyphus", async () => {
// given - keyword-detector hook with Sisyphus agent
const collector = new ContextCollector()
const hook = createKeywordDetectorHook(createMockPluginInput(), collector)
const sessionID = "sisyphus-session-omo"
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "ultrawork implement this" }],
}
// when - keyword detection runs with Sisyphus (OMO agent)
await hook["chat.message"]({ sessionID, agent: "sisyphus" }, output)
// then - keywords should be injected normally
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toContain("YOU MUST LEVERAGE ALL AVAILABLE AGENTS")
expect(textPart!.text).toContain("implement this")
})
test("should skip keyword injection for agent names containing 'builder'", async () => {
// given - keyword-detector hook with a builder-variant agent name
const collector = new ContextCollector()
const hook = createKeywordDetectorHook(createMockPluginInput(), collector)
const sessionID = "custom-builder-session"
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "search this codebase" }],
}
// when - keyword detection runs with a builder-type agent
await hook["chat.message"]({ sessionID, agent: "Custom-Builder" }, output)
// then - search-mode should NOT be injected
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toBe("search this codebase")
expect(textPart!.text).not.toContain("[search-mode]")
})
})

View File

@@ -10,6 +10,7 @@
export {
isPlannerAgent,
isNonOmoAgent,
isGptModel,
isGeminiModel,
getUltraworkSource,

View File

@@ -23,6 +23,16 @@ export function isPlannerAgent(agentName?: string): boolean {
return /\bplan\b/.test(normalized)
}
/**
* Checks if agent is a non-OMO agent (e.g., OpenCode's built-in Builder/Plan).
* Non-OMO agents should not receive keyword injection (search-mode, analyze-mode, etc.).
*/
export function isNonOmoAgent(agentName?: string): boolean {
if (!agentName) return false
const lowerName = agentName.toLowerCase()
return lowerName.includes("builder") || lowerName === "plan"
}
export { isGptModel, isGeminiModel }
/** Ultrawork message source type */