fix(custom-agents): align planner catalog and schema validation

This commit is contained in:
edxeth 2026-02-26 21:14:00 +01:00
parent 922ff7f2bc
commit a5749a1392
6 changed files with 147 additions and 24 deletions

View File

@ -3152,7 +3152,7 @@
"type": "object", "type": "object",
"propertyNames": { "propertyNames": {
"type": "string", "type": "string",
"pattern": "^(?!(?:build|plan|sisyphus|hephaestus|sisyphus-junior|OpenCode-Builder|prometheus|metis|momus|oracle|librarian|explore|multimodal-looker|atlas)$).+" "pattern": "^(?!(?:[bB][uU][iI][lL][dD]|[pP][lL][aA][nN]|[sS][iI][sS][yY][pP][hH][uU][sS]|[hH][eE][pP][hH][aA][eE][sS][tT][uU][sS]|[sS][iI][sS][yY][pP][hH][uU][sS]-[jJ][uU][nN][iI][oO][rR]|[oO][pP][eE][nN][cC][oO][dD][eE]-[bB][uU][iI][lL][dD][eE][rR]|[pP][rR][oO][mM][eE][tT][hH][eE][uU][sS]|[mM][eE][tT][iI][sS]|[mM][oO][mM][uU][sS]|[oO][rR][aA][cC][lL][eE]|[lL][iI][bB][rR][aA][rR][iI][aA][nN]|[eE][xX][pP][lL][oO][rR][eE]|[mM][uU][lL][tT][iI][mM][oO][dD][aA][lL]-[lL][oO][oO][kK][eE][rR]|[aA][tT][lL][aA][sS])$).+"
}, },
"additionalProperties": { "additionalProperties": {
"type": "object", "type": "object",

View File

@ -23,6 +23,8 @@ describe("schema document generation", () => {
expect(agentsSchema?.additionalProperties).toBeFalse() expect(agentsSchema?.additionalProperties).toBeFalse()
expect(customAgentsSchema).toBeDefined() expect(customAgentsSchema).toBeDefined()
expect(customPropertyNames?.pattern).toBeDefined() expect(customPropertyNames?.pattern).toBeDefined()
expect(customPropertyNames?.pattern).toContain("[bB][uU][iI][lL][dD]")
expect(customPropertyNames?.pattern).toContain("[pP][lL][aA][nN]")
expect(customAdditionalProperties).toBeDefined() expect(customAdditionalProperties).toBeDefined()
expect(customAgentProperties?.model).toEqual({ type: "string" }) expect(customAgentProperties?.model).toEqual({ type: "string" })
expect(customAgentProperties?.temperature).toEqual( expect(customAgentProperties?.temperature).toEqual(

View File

@ -81,8 +81,27 @@ const RESERVED_CUSTOM_AGENT_NAMES = OverridableAgentNameSchema.options
const RESERVED_CUSTOM_AGENT_NAME_SET = new Set( const RESERVED_CUSTOM_AGENT_NAME_SET = new Set(
RESERVED_CUSTOM_AGENT_NAMES.map((name) => name.toLowerCase()), RESERVED_CUSTOM_AGENT_NAMES.map((name) => name.toLowerCase()),
) )
function escapeRegexLiteral(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}
function toCaseInsensitiveLiteralPattern(value: string): string {
return value
.split("")
.map((char) => {
if (/^[A-Za-z]$/.test(char)) {
const lower = char.toLowerCase()
const upper = char.toUpperCase()
return `[${lower}${upper}]`
}
return escapeRegexLiteral(char)
})
.join("")
}
const RESERVED_CUSTOM_AGENT_NAME_PATTERN = new RegExp( const RESERVED_CUSTOM_AGENT_NAME_PATTERN = new RegExp(
`^(?!(?:${RESERVED_CUSTOM_AGENT_NAMES.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})$).+`, `^(?!(?:${RESERVED_CUSTOM_AGENT_NAMES.map(toCaseInsensitiveLiteralPattern).join("|")})$).+`,
) )
export const CustomAgentOverridesSchema = z export const CustomAgentOverridesSchema = z

View File

@ -82,6 +82,15 @@ export async function applyAgentConfig(params: {
const browserProvider = const browserProvider =
params.pluginConfig.browser_automation_engine?.provider ?? "playwright"; params.pluginConfig.browser_automation_engine?.provider ?? "playwright";
const currentModel = params.config.model as string | undefined; const currentModel = params.config.model as string | undefined;
const disabledAgentNames = new Set(
(migratedDisabledAgents ?? []).map((agent) => agent.toLowerCase()),
);
const filterDisabledAgents = (agents: Record<string, unknown>) =>
Object.fromEntries(
Object.entries(agents).filter(
([name]) => !disabledAgentNames.has(name.toLowerCase()),
),
);
const disabledSkills = new Set<string>(params.pluginConfig.disabled_skills ?? []); const disabledSkills = new Set<string>(params.pluginConfig.disabled_skills ?? []);
const useTaskSystem = params.pluginConfig.experimental?.task_system ?? false; const useTaskSystem = params.pluginConfig.experimental?.task_system ?? false;
const disableOmoEnv = params.pluginConfig.experimental?.disable_omo_env ?? false; const disableOmoEnv = params.pluginConfig.experimental?.disable_omo_env ?? false;
@ -99,19 +108,25 @@ export async function applyAgentConfig(params: {
); );
const configAgent = params.config.agent as AgentConfigRecord | undefined; const configAgent = params.config.agent as AgentConfigRecord | undefined;
const filteredUserAgents = filterDisabledAgents(userAgents as Record<string, unknown>);
const filteredProjectAgents = filterDisabledAgents(projectAgents as Record<string, unknown>);
const filteredPluginAgents = filterDisabledAgents(pluginAgents as Record<string, unknown>);
const filteredConfigAgentsForSummary = filterDisabledAgents(
(configAgent as Record<string, unknown> | undefined) ?? {},
);
const mergedCategories = mergeCategories(params.pluginConfig.categories) const mergedCategories = mergeCategories(params.pluginConfig.categories)
const knownCustomAgentNames = collectKnownCustomAgentNames( const knownCustomAgentNames = collectKnownCustomAgentNames(
userAgents as Record<string, unknown>, filteredUserAgents,
projectAgents as Record<string, unknown>, filteredProjectAgents,
pluginAgents as Record<string, unknown>, filteredPluginAgents,
configAgent as Record<string, unknown> | undefined, filteredConfigAgentsForSummary,
) )
const customAgentSummaries = mergeCustomAgentSummaries( const customAgentSummaries = mergeCustomAgentSummaries(
collectCustomAgentSummariesFromRecord(userAgents as Record<string, unknown>), collectCustomAgentSummariesFromRecord(filteredUserAgents),
collectCustomAgentSummariesFromRecord(projectAgents as Record<string, unknown>), collectCustomAgentSummariesFromRecord(filteredProjectAgents),
collectCustomAgentSummariesFromRecord(pluginAgents as Record<string, unknown>), collectCustomAgentSummariesFromRecord(filteredPluginAgents),
collectCustomAgentSummariesFromRecord(configAgent as Record<string, unknown> | undefined), collectCustomAgentSummariesFromRecord(filteredConfigAgentsForSummary),
filterSummariesByKnownNames( filterSummariesByKnownNames(
collectCustomAgentSummariesFromRecord( collectCustomAgentSummariesFromRecord(
params.pluginConfig.custom_agents as Record<string, unknown> | undefined, params.pluginConfig.custom_agents as Record<string, unknown> | undefined,
@ -135,14 +150,6 @@ export async function applyAgentConfig(params: {
useTaskSystem, useTaskSystem,
disableOmoEnv, disableOmoEnv,
); );
const disabledAgentNames = new Set(
(migratedDisabledAgents ?? []).map(a => a.toLowerCase())
);
const filterDisabledAgents = (agents: Record<string, unknown>) =>
Object.fromEntries(
Object.entries(agents).filter(([name]) => !disabledAgentNames.has(name.toLowerCase()))
);
const isSisyphusEnabled = params.pluginConfig.sisyphus_agent?.disabled !== true; const isSisyphusEnabled = params.pluginConfig.sisyphus_agent?.disabled !== true;
const builderEnabled = const builderEnabled =
params.pluginConfig.sisyphus_agent?.default_builder_enabled ?? false; params.pluginConfig.sisyphus_agent?.default_builder_enabled ?? false;
@ -230,9 +237,9 @@ export async function applyAgentConfig(params: {
...Object.fromEntries( ...Object.fromEntries(
Object.entries(builtinAgents).filter(([key]) => key !== "sisyphus"), Object.entries(builtinAgents).filter(([key]) => key !== "sisyphus"),
), ),
...filterDisabledAgents(userAgents), ...filteredUserAgents,
...filterDisabledAgents(projectAgents), ...filteredProjectAgents,
...filterDisabledAgents(pluginAgents), ...filteredPluginAgents,
...filteredConfigAgents, ...filteredConfigAgents,
build: { ...migratedBuild, mode: "subagent", hidden: true }, build: { ...migratedBuild, mode: "subagent", hidden: true },
...(planDemoteConfig ? { plan: planDemoteConfig } : {}), ...(planDemoteConfig ? { plan: planDemoteConfig } : {}),
@ -240,9 +247,9 @@ export async function applyAgentConfig(params: {
} else { } else {
params.config.agent = { params.config.agent = {
...builtinAgents, ...builtinAgents,
...filterDisabledAgents(userAgents), ...filteredUserAgents,
...filterDisabledAgents(projectAgents), ...filteredProjectAgents,
...filterDisabledAgents(pluginAgents), ...filteredPluginAgents,
...configAgent, ...configAgent,
}; };
} }

View File

@ -352,6 +352,94 @@ describe("custom agent overrides", () => {
expect(agentsConfig[pKey].prompt).not.toContain("ghostwriter") expect(agentsConfig[pKey].prompt).not.toContain("ghostwriter")
}) })
test("prometheus prompt excludes disabled custom agents from catalog", async () => {
// #given
;(agentLoader.loadUserAgents as any).mockReturnValue({
translator: {
name: "translator",
mode: "subagent",
description: "Translate and localize locale files",
prompt: "Translate content",
},
})
const pluginConfig: OhMyOpenCodeConfig = {
disabled_agents: ["translator"],
sisyphus_agent: {
planner_enabled: true,
},
}
const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-6",
agent: {},
}
const handler = createConfigHandler({
ctx: { directory: "/tmp" },
pluginConfig,
modelCacheState: {
anthropicContext1MEnabled: false,
modelContextLimitsCache: new Map(),
},
})
// #when
await handler(config)
// #then
const agentsConfig = config.agent as Record<string, { prompt?: string }>
const pKey = getAgentDisplayName("prometheus")
expect(agentsConfig[pKey]).toBeDefined()
expect(agentsConfig[pKey].prompt).not.toContain("translator")
})
test("prometheus custom prompt override still includes custom agent catalog", async () => {
// #given
;(agentLoader.loadUserAgents as any).mockReturnValue({
translator: {
name: "translator",
mode: "subagent",
description: "Translate and localize locale files",
prompt: "Translate content",
},
})
const pluginConfig: OhMyOpenCodeConfig = {
agents: {
prometheus: {
prompt: "Custom planner prompt",
},
},
sisyphus_agent: {
planner_enabled: true,
},
}
const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-6",
agent: {},
}
const handler = createConfigHandler({
ctx: { directory: "/tmp" },
pluginConfig,
modelCacheState: {
anthropicContext1MEnabled: false,
modelContextLimitsCache: new Map(),
},
})
// #when
await handler(config)
// #then
const agentsConfig = config.agent as Record<string, { prompt?: string }>
const pKey = getAgentDisplayName("prometheus")
expect(agentsConfig[pKey]).toBeDefined()
expect(agentsConfig[pKey].prompt).toContain("Custom planner prompt")
expect(agentsConfig[pKey].prompt).toContain("<custom_agent_catalog>")
expect(agentsConfig[pKey].prompt).toContain("translator")
})
test("custom agent summary merge preserves flags when custom_agents adds description", async () => { test("custom agent summary merge preserves flags when custom_agents adds description", async () => {
// #given // #given
;(agentLoader.loadUserAgents as any).mockReturnValue({ ;(agentLoader.loadUserAgents as any).mockReturnValue({

View File

@ -103,5 +103,12 @@ export async function buildPrometheusAgentConfig(params: {
if (prompt_append && typeof merged.prompt === "string") { if (prompt_append && typeof merged.prompt === "string") {
merged.prompt = merged.prompt + "\n" + resolvePromptAppend(prompt_append); merged.prompt = merged.prompt + "\n" + resolvePromptAppend(prompt_append);
} }
if (
customAgentBlock
&& typeof merged.prompt === "string"
&& !merged.prompt.includes("<custom_agent_catalog>")
) {
merged.prompt = merged.prompt + customAgentBlock;
}
return merged; return merged;
} }