fix: skip keyword injection for non-OMO agents (fixes #2024)
This commit is contained in:
@@ -2,7 +2,7 @@ export const CODE_BLOCK_PATTERN = /```[\s\S]*?```/g
|
|||||||
export const INLINE_CODE_PATTERN = /`[^`]+`/g
|
export const INLINE_CODE_PATTERN = /`[^`]+`/g
|
||||||
|
|
||||||
// Re-export from submodules
|
// Re-export from submodules
|
||||||
export { isPlannerAgent, getUltraworkMessage } from "./ultrawork"
|
export { isPlannerAgent, isNonOmoAgent, getUltraworkMessage } from "./ultrawork"
|
||||||
export { SEARCH_PATTERN, SEARCH_MESSAGE } from "./search"
|
export { SEARCH_PATTERN, SEARCH_MESSAGE } from "./search"
|
||||||
export { ANALYZE_PATTERN, ANALYZE_MESSAGE } from "./analyze"
|
export { ANALYZE_PATTERN, ANALYZE_MESSAGE } from "./analyze"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import { detectKeywordsWithType, extractPromptText } from "./detector"
|
import { detectKeywordsWithType, extractPromptText } from "./detector"
|
||||||
import { isPlannerAgent } from "./constants"
|
import { isPlannerAgent, isNonOmoAgent } from "./constants"
|
||||||
import { log } from "../../shared"
|
import { log } from "../../shared"
|
||||||
import {
|
import {
|
||||||
isSystemDirective,
|
isSystemDirective,
|
||||||
@@ -45,6 +45,12 @@ export function createKeywordDetectorHook(ctx: PluginInput, _collector?: Context
|
|||||||
|
|
||||||
const currentAgent = getSessionAgent(input.sessionID) ?? input.agent
|
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
|
// Remove system-reminder content to prevent automated system messages from triggering mode keywords
|
||||||
const cleanText = removeSystemReminders(promptText)
|
const cleanText = removeSystemReminders(promptText)
|
||||||
const modelID = input.model?.modelID
|
const modelID = input.model?.modelID
|
||||||
|
|||||||
@@ -746,3 +746,109 @@ describe("keyword-detector agent-specific ultrawork messages", () => {
|
|||||||
expect(textPart!.text).not.toContain("YOU ARE A PLANNER, NOT AN IMPLEMENTER")
|
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]")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
isPlannerAgent,
|
isPlannerAgent,
|
||||||
|
isNonOmoAgent,
|
||||||
isGptModel,
|
isGptModel,
|
||||||
isGeminiModel,
|
isGeminiModel,
|
||||||
getUltraworkSource,
|
getUltraworkSource,
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ export function isPlannerAgent(agentName?: string): boolean {
|
|||||||
return /\bplan\b/.test(normalized)
|
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 }
|
export { isGptModel, isGeminiModel }
|
||||||
|
|
||||||
/** Ultrawork message source type */
|
/** Ultrawork message source type */
|
||||||
|
|||||||
Reference in New Issue
Block a user