From f383d7abb56c0616eb00159f50d7cada0d6ae19c Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 2 Mar 2026 23:55:48 +0900 Subject: [PATCH] Revert "Merge pull request #1951 from edxeth/feat/custom-agents" This reverts commit 47e300b17e47b6f8f54a2a945673f879ea48cf30, reversing changes made to 243ce1b7e81005d4c2cd2eda0a51f56c715ffe7f. --- assets/oh-my-opencode.schema.json | 233 +----------- docs/guide/overview.md | 2 - docs/reference/configuration.md | 61 ---- script/build-schema-document.ts | 38 +- src/agents/utils.test.ts | 26 +- src/config/index.ts | 14 - src/config/schema-document.test.ts | 38 -- src/config/schema.test.ts | 73 ---- src/config/schema/agent-overrides.ts | 55 +-- src/config/schema/oh-my-opencode-config.ts | 3 +- src/plugin-config.test.ts | 164 +-------- src/plugin-config.ts | 163 +-------- src/plugin-handlers/agent-config-handler.ts | 112 ++---- src/plugin-handlers/config-handler.test.ts | 341 ------------------ src/plugin-handlers/custom-agent-utils.ts | 142 -------- .../prometheus-agent-config-builder.ts | 18 +- .../delegate-task/subagent-resolver.test.ts | 52 --- src/tools/delegate-task/subagent-resolver.ts | 14 +- src/tools/delegate-task/tools.ts | 9 +- 19 files changed, 47 insertions(+), 1511 deletions(-) delete mode 100644 src/config/schema-document.test.ts delete mode 100644 src/plugin-handlers/custom-agent-utils.ts diff --git a/assets/oh-my-opencode.schema.json b/assets/oh-my-opencode.schema.json index 5c52ffb04..a8710453c 100644 --- a/assets/oh-my-opencode.schema.json +++ b/assets/oh-my-opencode.schema.json @@ -3148,16 +3148,6 @@ }, "additionalProperties": false }, - "custom_agents": { - "type": "object", - "propertyNames": { - "type": "string", - "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": { - "$ref": "#/$defs/agentOverrideConfig" - } - }, "categories": { "type": "object", "propertyNames": { @@ -3871,226 +3861,5 @@ } } }, - "additionalProperties": false, - "$defs": { - "agentOverrideConfig": { - "type": "object", - "properties": { - "model": { - "type": "string" - }, - "fallback_models": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "variant": { - "type": "string" - }, - "category": { - "type": "string" - }, - "skills": { - "type": "array", - "items": { - "type": "string" - } - }, - "temperature": { - "type": "number", - "minimum": 0, - "maximum": 2 - }, - "top_p": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "prompt": { - "type": "string" - }, - "prompt_append": { - "type": "string" - }, - "tools": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "boolean" - } - }, - "disable": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "mode": { - "type": "string", - "enum": [ - "subagent", - "primary", - "all" - ] - }, - "color": { - "type": "string", - "pattern": "^#[0-9A-Fa-f]{6}$" - }, - "permission": { - "type": "object", - "properties": { - "edit": { - "type": "string", - "enum": [ - "ask", - "allow", - "deny" - ] - }, - "bash": { - "anyOf": [ - { - "type": "string", - "enum": [ - "ask", - "allow", - "deny" - ] - }, - { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string", - "enum": [ - "ask", - "allow", - "deny" - ] - } - } - ] - }, - "webfetch": { - "type": "string", - "enum": [ - "ask", - "allow", - "deny" - ] - }, - "task": { - "type": "string", - "enum": [ - "ask", - "allow", - "deny" - ] - }, - "doom_loop": { - "type": "string", - "enum": [ - "ask", - "allow", - "deny" - ] - }, - "external_directory": { - "type": "string", - "enum": [ - "ask", - "allow", - "deny" - ] - } - }, - "additionalProperties": false - }, - "maxTokens": { - "type": "number" - }, - "thinking": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "enabled", - "disabled" - ] - }, - "budgetTokens": { - "type": "number" - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "reasoningEffort": { - "type": "string", - "enum": [ - "low", - "medium", - "high", - "xhigh" - ] - }, - "textVerbosity": { - "type": "string", - "enum": [ - "low", - "medium", - "high" - ] - }, - "providerOptions": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "ultrawork": { - "type": "object", - "properties": { - "model": { - "type": "string" - }, - "variant": { - "type": "string" - } - }, - "additionalProperties": false - }, - "compaction": { - "type": "object", - "properties": { - "model": { - "type": "string" - }, - "variant": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - } + "additionalProperties": false } \ No newline at end of file diff --git a/docs/guide/overview.md b/docs/guide/overview.md index 0aaca2c2a..21f337324 100644 --- a/docs/guide/overview.md +++ b/docs/guide/overview.md @@ -68,8 +68,6 @@ User Request When Sisyphus delegates to a subagent, it doesn't pick a model name. It picks a **category** — `visual-engineering`, `ultrabrain`, `quick`, `deep`. The category automatically maps to the right model. You touch nothing. -Custom agents are also first-class in this flow. When custom agents are loaded, planning context includes them, so the orchestrator can choose them proactively when appropriate, and you can call them directly on demand via `task(subagent_type="your-agent")`. - For a deep dive into how agents collaborate, see the [Orchestration System Guide](./orchestration.md). --- diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index a71038848..28eba1193 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -11,7 +11,6 @@ Complete reference for `oh-my-opencode.jsonc` configuration. This document cover - [Quick Start Example](#quick-start-example) - [Core Concepts](#core-concepts) - [Agents](#agents) - - [Custom Agents (`custom_agents`)](#custom-agents-custom_agents) - [Categories](#categories) - [Model Resolution](#model-resolution) - [Task System](#task-system) @@ -131,8 +130,6 @@ Here's a practical starting configuration: Override built-in agent settings. Available agents: `sisyphus`, `hephaestus`, `prometheus`, `oracle`, `librarian`, `explore`, `multimodal-looker`, `metis`, `momus`, `atlas`. -`agents` is intentionally strict and only accepts built-in agent keys. Use `custom_agents` for user-defined agents. - ```json { "agents": { @@ -203,64 +200,6 @@ Control what tools an agent can use: | `doom_loop` | `ask` / `allow` / `deny` | | `external_directory` | `ask` / `allow` / `deny` | -### Custom Agents (`custom_agents`) - -Use `custom_agents` to configure user-defined agents without mixing them into built-in `agents` overrides. - -What this gives you: - -- **Clean separation**: built-ins stay in `agents`, user-defined entries stay in `custom_agents`. -- **Safer config**: keys in `custom_agents` cannot reuse built-in names. -- **First-class orchestration**: loaded custom agents are visible to planner/orchestrator context, so they can be selected proactively during planning and invoked on demand via `task(subagent_type=...)`. -- **Full model controls** for custom agents: `model`, `variant`, `temperature`, `top_p`, `reasoningEffort`, `thinking`, etc. - -Important behavior: - -- `custom_agents` **overrides existing custom agents** loaded at runtime (for example from Claude Code/OpenCode agent sources). -- `custom_agents` does **not** create an agent from thin air by itself; the target custom agent must be present in runtime-loaded agent configs. - -Example: - -```jsonc -{ - "custom_agents": { - "translator": { - "model": "openai/gpt-5.3-codex", - "variant": "high", - "temperature": 0.2, - "prompt_append": "Keep locale placeholders and ICU tokens exactly unchanged." - }, - "reviewer-fast": { - "model": "anthropic/claude-haiku-4-5", - "temperature": 0, - "thinking": { - "type": "enabled", - "budgetTokens": 20000 - } - } - } -} -``` - -On-demand invocation through task delegation: - -```ts -task( - { - subagent_type: "translator", - load_skills: [], - description: "Translate release notes", - prompt: "Translate docs/CHANGELOG.md into Korean while preserving markdown structure.", - run_in_background: false, - }, -) -``` - -Migration note: - -- If you previously put custom entries under `agents.*`, move them to `custom_agents.*`. -- Unknown built-in keys under `agents` are reported with migration hints. - ### Categories Domain-specific model delegation used by the `task()` tool. When Sisyphus delegates work, it picks a category, not a model name. diff --git a/script/build-schema-document.ts b/script/build-schema-document.ts index 2ede3e2b7..f93302fce 100644 --- a/script/build-schema-document.ts +++ b/script/build-schema-document.ts @@ -1,53 +1,17 @@ import * as z from "zod" import { OhMyOpenCodeConfigSchema } from "../src/config/schema" -function asRecord(value: unknown): Record | undefined { - return typeof value === "object" && value !== null ? (value as Record) : undefined -} - -function dedupeCustomAgentOverrideSchema(schema: Record): Record { - const rootProperties = asRecord(schema.properties) - const agentsSchema = asRecord(rootProperties?.agents) - const builtInAgentProps = asRecord(agentsSchema?.properties) - const customAgentsSchema = asRecord(rootProperties?.custom_agents) - const customAdditionalProperties = asRecord(customAgentsSchema?.additionalProperties) - - if (!builtInAgentProps || !customAgentsSchema || !customAdditionalProperties) { - return schema - } - - const referenceAgentSchema = asRecord( - builtInAgentProps.build - ?? builtInAgentProps.oracle - ?? builtInAgentProps.explore, - ) - - if (!referenceAgentSchema) { - return schema - } - - const defs = asRecord(schema.$defs) ?? {} - defs.agentOverrideConfig = referenceAgentSchema - schema.$defs = defs - - customAgentsSchema.additionalProperties = { $ref: "#/$defs/agentOverrideConfig" } - - return schema -} - export function createOhMyOpenCodeJsonSchema(): Record { const jsonSchema = z.toJSONSchema(OhMyOpenCodeConfigSchema, { target: "draft-7", unrepresentable: "any", }) - const schema = { + return { $schema: "http://json-schema.org/draft-07/schema#", $id: "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/dev/assets/oh-my-opencode.schema.json", title: "Oh My OpenCode Configuration", description: "Configuration schema for oh-my-opencode plugin", ...jsonSchema, } - - return dedupeCustomAgentOverrideSchema(schema) } diff --git a/src/agents/utils.test.ts b/src/agents/utils.test.ts index f4ecb5040..129329afa 100644 --- a/src/agents/utils.test.ts +++ b/src/agents/utils.test.ts @@ -242,28 +242,14 @@ describe("createBuiltinAgents with model overrides", () => { test("createBuiltinAgents excludes disabled skills from availableSkills", async () => { // #given const disabledSkills = new Set(["playwright"]) - const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null) - const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( - new Set([ - "anthropic/claude-opus-4-6", - "opencode/kimi-k2.5-free", - "zai-coding-plan/glm-5", - "opencode/big-pickle", - ]) - ) - try { - // #when - const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined, undefined, disabledSkills) + // #when + const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined, undefined, disabledSkills) - // #then - expect(agents.sisyphus.prompt).not.toContain("playwright") - expect(agents.sisyphus.prompt).toContain("frontend-ui-ux") - expect(agents.sisyphus.prompt).toContain("git-master") - } finally { - cacheSpy.mockRestore() - fetchSpy.mockRestore() - } + // #then + expect(agents.sisyphus.prompt).not.toContain("playwright") + expect(agents.sisyphus.prompt).toContain("frontend-ui-ux") + expect(agents.sisyphus.prompt).toContain("git-master") }) test("includes custom agents in orchestrator prompts when provided via config", async () => { diff --git a/src/config/index.ts b/src/config/index.ts index ae2ef967f..2f7f98578 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,25 +1,11 @@ export { OhMyOpenCodeConfigSchema, - AgentOverrideConfigSchema, - AgentOverridesSchema, - CustomAgentOverridesSchema, - McpNameSchema, - AgentNameSchema, - OverridableAgentNameSchema, - HookNameSchema, - BuiltinCommandNameSchema, - SisyphusAgentConfigSchema, - ExperimentalConfigSchema, - RalphLoopConfigSchema, - TmuxConfigSchema, - TmuxLayoutSchema, } from "./schema" export type { OhMyOpenCodeConfig, AgentOverrideConfig, AgentOverrides, - CustomAgentOverrides, McpName, AgentName, HookName, diff --git a/src/config/schema-document.test.ts b/src/config/schema-document.test.ts deleted file mode 100644 index 80bc6d078..000000000 --- a/src/config/schema-document.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { createOhMyOpenCodeJsonSchema } from "../../script/build-schema-document" - -function asRecord(value: unknown): Record | undefined { - return typeof value === "object" && value !== null ? (value as Record) : undefined -} - -describe("schema document generation", () => { - test("custom_agents schema allows arbitrary custom agent keys with override shape", () => { - // given - const schema = createOhMyOpenCodeJsonSchema() - - // when - const rootProperties = asRecord(schema.properties) - const agentsSchema = asRecord(rootProperties?.agents) - const customAgentsSchema = asRecord(rootProperties?.custom_agents) - const customPropertyNames = asRecord(customAgentsSchema?.propertyNames) - const customAdditionalProperties = asRecord(customAgentsSchema?.additionalProperties) - const defs = asRecord(schema.$defs) - const sharedAgentOverrideSchema = asRecord(defs?.agentOverrideConfig) - const sharedAgentProperties = asRecord(sharedAgentOverrideSchema?.properties) - - // then - expect(agentsSchema).toBeDefined() - expect(agentsSchema?.additionalProperties).toBeFalse() - expect(customAgentsSchema).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?.$ref).toBe("#/$defs/agentOverrideConfig") - expect(sharedAgentOverrideSchema).toBeDefined() - expect(sharedAgentProperties?.model).toEqual({ type: "string" }) - expect(sharedAgentProperties?.temperature).toEqual( - expect.objectContaining({ type: "number" }), - ) - }) -}) diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts index 477eaa51b..8a83fcd7d 100644 --- a/src/config/schema.test.ts +++ b/src/config/schema.test.ts @@ -530,79 +530,6 @@ describe("Sisyphus-Junior agent override", () => { expect(result.data.agents?.momus?.category).toBe("quick") } }) - - test("schema accepts custom_agents override keys", () => { - // given - const config = { - custom_agents: { - translator: { - model: "google/gemini-3-flash-preview", - temperature: 0, - }, - }, - } - - // when - const result = OhMyOpenCodeConfigSchema.safeParse(config) - - // then - expect(result.success).toBe(true) - if (result.success) { - expect(result.data.custom_agents?.translator?.model).toBe("google/gemini-3-flash-preview") - expect(result.data.custom_agents?.translator?.temperature).toBe(0) - } - }) - - test("schema rejects unknown keys under agents", () => { - // given - const config = { - agents: { - sisyphuss: { - model: "openai/gpt-5.3-codex", - }, - }, - } - - // when - const result = OhMyOpenCodeConfigSchema.safeParse(config) - - // then - expect(result.success).toBe(false) - }) - - test("schema rejects built-in agent names under custom_agents", () => { - // given - const config = { - custom_agents: { - sisyphus: { - model: "openai/gpt-5.3-codex", - }, - }, - } - - // when - const result = OhMyOpenCodeConfigSchema.safeParse(config) - - // then - expect(result.success).toBe(false) - }) - - test("schema rejects built-in agent names under custom_agents case-insensitively", () => { - // given - const config = { - custom_agents: { - Sisyphus: { - model: "openai/gpt-5.3-codex", - }, - }, - } - - // when - const result = OhMyOpenCodeConfigSchema.safeParse(config) - - // then - expect(result.success).toBe(false) - }) }) describe("BrowserAutomationProviderSchema", () => { diff --git a/src/config/schema/agent-overrides.ts b/src/config/schema/agent-overrides.ts index bc40a7313..623b35efd 100644 --- a/src/config/schema/agent-overrides.ts +++ b/src/config/schema/agent-overrides.ts @@ -1,6 +1,5 @@ import { z } from "zod" import { FallbackModelsSchema } from "./fallback-models" -import { OverridableAgentNameSchema } from "./agent-names" import { AgentPermissionSchema } from "./internal/permission" export const AgentOverrideConfigSchema = z.object({ @@ -56,7 +55,7 @@ export const AgentOverrideConfigSchema = z.object({ .optional(), }) -const BuiltinAgentOverridesSchema = z.object({ +export const AgentOverridesSchema = z.object({ build: AgentOverrideConfigSchema.optional(), plan: AgentOverrideConfigSchema.optional(), sisyphus: AgentOverrideConfigSchema.optional(), @@ -73,57 +72,7 @@ const BuiltinAgentOverridesSchema = z.object({ explore: AgentOverrideConfigSchema.optional(), "multimodal-looker": AgentOverrideConfigSchema.optional(), atlas: AgentOverrideConfigSchema.optional(), -}).strict() - -export const AgentOverridesSchema = BuiltinAgentOverridesSchema - -const RESERVED_CUSTOM_AGENT_NAMES = OverridableAgentNameSchema.options -const RESERVED_CUSTOM_AGENT_NAME_SET = new Set( - 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( - `^(?!(?:${RESERVED_CUSTOM_AGENT_NAMES.map(toCaseInsensitiveLiteralPattern).join("|")})$).+`, -) - -export const CustomAgentOverridesSchema = z - .record( - z.string().regex( - RESERVED_CUSTOM_AGENT_NAME_PATTERN, - "custom_agents key cannot reuse built-in agent override name", - ), - AgentOverrideConfigSchema, - ) - .superRefine((value, ctx) => { - for (const key of Object.keys(value)) { - if (RESERVED_CUSTOM_AGENT_NAME_SET.has(key.toLowerCase())) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: [key], - message: "custom_agents key cannot reuse built-in agent override name", - }) - } - } - }) +}) export type AgentOverrideConfig = z.infer export type AgentOverrides = z.infer -export type CustomAgentOverrides = z.infer diff --git a/src/config/schema/oh-my-opencode-config.ts b/src/config/schema/oh-my-opencode-config.ts index 43a7c6a4f..d24bbef4d 100644 --- a/src/config/schema/oh-my-opencode-config.ts +++ b/src/config/schema/oh-my-opencode-config.ts @@ -1,7 +1,7 @@ import { z } from "zod" import { AnyMcpNameSchema } from "../../mcp/types" import { BuiltinAgentNameSchema, BuiltinSkillNameSchema } from "./agent-names" -import { AgentOverridesSchema, CustomAgentOverridesSchema } from "./agent-overrides" +import { AgentOverridesSchema } from "./agent-overrides" import { BabysittingConfigSchema } from "./babysitting" import { BackgroundTaskConfigSchema } from "./background-task" import { BrowserAutomationConfigSchema } from "./browser-automation" @@ -39,7 +39,6 @@ export const OhMyOpenCodeConfigSchema = z.object({ /** Enable model fallback on API errors (default: false). Set to true to enable automatic model switching when model errors occur. */ model_fallback: z.boolean().optional(), agents: AgentOverridesSchema.optional(), - custom_agents: CustomAgentOverridesSchema.optional(), categories: CategoriesConfigSchema.optional(), claude_code: ClaudeCodeConfigSchema.optional(), sisyphus_agent: SisyphusAgentConfigSchema.optional(), diff --git a/src/plugin-config.test.ts b/src/plugin-config.test.ts index c1bc3441b..1802f00c8 100644 --- a/src/plugin-config.test.ts +++ b/src/plugin-config.test.ts @@ -1,10 +1,5 @@ import { describe, expect, it } from "bun:test"; -import { - detectLikelyBuiltinAgentTypos, - detectUnknownBuiltinAgentKeys, - mergeConfigs, - parseConfigPartially, -} from "./plugin-config"; +import { mergeConfigs, parseConfigPartially } from "./plugin-config"; import type { OhMyOpenCodeConfig } from "./config"; describe("mergeConfigs", () => { @@ -120,27 +115,6 @@ describe("mergeConfigs", () => { expect(result.disabled_hooks).toContain("session-recovery"); expect(result.disabled_hooks?.length).toBe(3); }); - - it("should deep merge custom_agents", () => { - const base: OhMyOpenCodeConfig = { - custom_agents: { - translator: { model: "google/gemini-3-flash-preview" }, - }, - } - - const override: OhMyOpenCodeConfig = { - custom_agents: { - translator: { temperature: 0 }, - "database-architect": { model: "openai/gpt-5.3-codex" }, - }, - } - - const result = mergeConfigs(base, override) - - expect(result.custom_agents?.translator?.model).toBe("google/gemini-3-flash-preview") - expect(result.custom_agents?.translator?.temperature).toBe(0) - expect(result.custom_agents?.["database-architect"]?.model).toBe("openai/gpt-5.3-codex") - }) }); }); @@ -191,9 +165,7 @@ describe("parseConfigPartially", () => { expect(result).not.toBeNull(); expect(result!.disabled_hooks).toEqual(["comment-checker"]); - expect(result!.agents?.oracle?.model).toBe("openai/gpt-5.2"); - expect(result!.agents?.momus?.model).toBe("openai/gpt-5.2"); - expect((result!.agents as Record)?.prometheus).toBeUndefined(); + expect(result!.agents).toBeUndefined(); }); it("should preserve valid agents when a non-agent section is invalid", () => { @@ -210,36 +182,6 @@ describe("parseConfigPartially", () => { expect(result!.agents?.oracle?.model).toBe("openai/gpt-5.2"); expect(result!.disabled_hooks).toEqual(["not-a-real-hook"]); }); - - it("should preserve valid built-in agent entries when agents contains unknown keys", () => { - const rawConfig = { - agents: { - sisyphus: { model: "openai/gpt-5.3-codex" }, - sisyphuss: { model: "openai/gpt-5.3-codex" }, - }, - }; - - const result = parseConfigPartially(rawConfig); - - expect(result).not.toBeNull(); - expect(result!.agents?.sisyphus?.model).toBe("openai/gpt-5.3-codex"); - expect((result!.agents as Record)?.sisyphuss).toBeUndefined(); - }); - - it("should preserve valid custom_agents entries when custom_agents contains reserved names", () => { - const rawConfig = { - custom_agents: { - translator: { model: "google/gemini-3-flash-preview" }, - sisyphus: { model: "openai/gpt-5.3-codex" }, - }, - }; - - const result = parseConfigPartially(rawConfig); - - expect(result).not.toBeNull(); - expect(result!.custom_agents?.translator?.model).toBe("google/gemini-3-flash-preview"); - expect((result!.custom_agents as Record)?.sisyphus).toBeUndefined(); - }); }); describe("completely invalid config", () => { @@ -295,105 +237,3 @@ describe("parseConfigPartially", () => { }); }); }); - -describe("detectLikelyBuiltinAgentTypos", () => { - it("detects near-miss builtin agent keys", () => { - const rawConfig = { - agents: { - sisyphuss: { model: "openai/gpt-5.2" }, - }, - } - - const warnings = detectLikelyBuiltinAgentTypos(rawConfig) - - expect(warnings).toEqual([ - { - key: "sisyphuss", - suggestion: "sisyphus", - }, - ]) - }) - - it("suggests canonical key casing for OpenCode-Builder typos", () => { - const rawConfig = { - agents: { - "opencode-buildr": { model: "openai/gpt-5.2" }, - }, - } - - const warnings = detectLikelyBuiltinAgentTypos(rawConfig) - - expect(warnings).toEqual([ - { - key: "opencode-buildr", - suggestion: "OpenCode-Builder", - }, - ]) - }) - - it("does not flag valid custom agent names", () => { - const rawConfig = { - agents: { - translator: { model: "google/gemini-3-flash-preview" }, - }, - } - - const warnings = detectLikelyBuiltinAgentTypos(rawConfig) - - expect(warnings).toEqual([]) - }) -}) - -describe("detectUnknownBuiltinAgentKeys", () => { - it("returns unknown keys under agents", () => { - const rawConfig = { - agents: { - sisyphus: { model: "openai/gpt-5.2" }, - translator: { model: "google/gemini-3-flash-preview" }, - }, - } - - const unknownKeys = detectUnknownBuiltinAgentKeys(rawConfig) - - expect(unknownKeys).toEqual(["translator"]) - }) - - it("returns empty array when all keys are built-ins", () => { - const rawConfig = { - agents: { - sisyphus: { model: "openai/gpt-5.2" }, - prometheus: { model: "openai/gpt-5.2" }, - }, - } - - const unknownKeys = detectUnknownBuiltinAgentKeys(rawConfig) - - expect(unknownKeys).toEqual([]) - }) - - it("excludes typo keys when explicitly provided", () => { - const rawConfig = { - agents: { - sisyphuss: { model: "openai/gpt-5.2" }, - translator: { model: "google/gemini-3-flash-preview" }, - }, - } - - const unknownKeys = detectUnknownBuiltinAgentKeys(rawConfig, ["sisyphuss"]) - - expect(unknownKeys).toEqual(["translator"]) - }) - - it("excludes typo keys case-insensitively", () => { - const rawConfig = { - agents: { - Sisyphuss: { model: "openai/gpt-5.2" }, - translator: { model: "google/gemini-3-flash-preview" }, - }, - } - - const unknownKeys = detectUnknownBuiltinAgentKeys(rawConfig, ["sisyphuss"]) - - expect(unknownKeys).toEqual(["translator"]) - }) -}) diff --git a/src/plugin-config.ts b/src/plugin-config.ts index c480f9848..fa22c5b3c 100644 --- a/src/plugin-config.ts +++ b/src/plugin-config.ts @@ -1,10 +1,6 @@ import * as fs from "fs"; import * as path from "path"; -import { - OhMyOpenCodeConfigSchema, - OverridableAgentNameSchema, - type OhMyOpenCodeConfig, -} from "./config"; +import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "./config"; import { log, deepMerge, @@ -15,90 +11,6 @@ import { migrateConfigFile, } from "./shared"; -const BUILTIN_AGENT_OVERRIDE_KEYS = OverridableAgentNameSchema.options; -const BUILTIN_AGENT_OVERRIDE_KEYS_BY_LOWER = new Map( - BUILTIN_AGENT_OVERRIDE_KEYS.map((key) => [key.toLowerCase(), key]), -); - -function levenshteinDistance(a: string, b: string): number { - const rows = a.length + 1; - const cols = b.length + 1; - const matrix: number[][] = Array.from({ length: rows }, () => Array(cols).fill(0)); - - for (let i = 0; i < rows; i += 1) matrix[i][0] = i; - for (let j = 0; j < cols; j += 1) matrix[0][j] = j; - - for (let i = 1; i < rows; i += 1) { - for (let j = 1; j < cols; j += 1) { - const cost = a[i - 1] === b[j - 1] ? 0 : 1; - matrix[i][j] = Math.min( - matrix[i - 1][j] + 1, - matrix[i][j - 1] + 1, - matrix[i - 1][j - 1] + cost, - ); - } - } - - return matrix[rows - 1][cols - 1]; -} - -type AgentTypoWarning = { - key: string; - suggestion: string; -}; - -export function detectLikelyBuiltinAgentTypos( - rawConfig: Record, -): AgentTypoWarning[] { - const agents = rawConfig.agents; - if (!agents || typeof agents !== "object") return []; - - const warnings: AgentTypoWarning[] = []; - for (const key of Object.keys(agents)) { - const lowerKey = key.toLowerCase(); - if (BUILTIN_AGENT_OVERRIDE_KEYS_BY_LOWER.has(lowerKey)) { - continue; - } - - let bestMatchLower: string | undefined; - let bestDistance = Number.POSITIVE_INFINITY; - for (const builtinKey of BUILTIN_AGENT_OVERRIDE_KEYS) { - const distance = levenshteinDistance(lowerKey, builtinKey.toLowerCase()); - if (distance < bestDistance) { - bestDistance = distance; - bestMatchLower = builtinKey.toLowerCase(); - } - } - - if (bestMatchLower && bestDistance <= 2) { - const suggestion = BUILTIN_AGENT_OVERRIDE_KEYS_BY_LOWER.get(bestMatchLower) ?? bestMatchLower; - warnings.push({ key, suggestion }); - } - } - - return warnings; -} - -export function detectUnknownBuiltinAgentKeys( - rawConfig: Record, - excludeKeys: string[] = [], -): string[] { - const agents = rawConfig.agents; - if (!agents || typeof agents !== "object") return []; - - const excluded = new Set(excludeKeys.map((key) => key.toLowerCase())); - - return Object.keys(agents).filter( - (key) => { - const lower = key.toLowerCase(); - return ( - !BUILTIN_AGENT_OVERRIDE_KEYS_BY_LOWER.has(lower) - && !excluded.has(lower) - ); - }, - ); -} - export function parseConfigPartially( rawConfig: Record ): OhMyOpenCodeConfig | null { @@ -110,52 +22,7 @@ export function parseConfigPartially( const partialConfig: Record = {}; const invalidSections: string[] = []; - const parseAgentSectionEntries = (sectionKey: "agents" | "custom_agents"): void => { - const rawSection = rawConfig[sectionKey]; - if (!rawSection || typeof rawSection !== "object") return; - - const parsedSection: Record = {}; - const invalidEntries: string[] = []; - - for (const [entryKey, entryValue] of Object.entries(rawSection)) { - const singleEntryResult = OhMyOpenCodeConfigSchema.safeParse({ - [sectionKey]: { [entryKey]: entryValue }, - }); - - if (singleEntryResult.success) { - const parsed = singleEntryResult.data as Record; - const parsedSectionValue = parsed[sectionKey]; - if (parsedSectionValue && typeof parsedSectionValue === "object") { - const typedSection = parsedSectionValue as Record; - if (typedSection[entryKey] !== undefined) { - parsedSection[entryKey] = typedSection[entryKey]; - } - } - continue; - } - - const entryErrors = singleEntryResult.error.issues - .map((issue) => `${entryKey}: ${issue.message}`) - .join(", "); - if (entryErrors) { - invalidEntries.push(entryErrors); - } - } - - if (Object.keys(parsedSection).length > 0) { - partialConfig[sectionKey] = parsedSection; - } - if (invalidEntries.length > 0) { - invalidSections.push(`${sectionKey}: ${invalidEntries.join(", ")}`); - } - }; - for (const key of Object.keys(rawConfig)) { - if (key === "agents" || key === "custom_agents") { - parseAgentSectionEntries(key); - continue; - } - const sectionResult = OhMyOpenCodeConfigSchema.safeParse({ [key]: rawConfig[key] }); if (sectionResult.success) { const parsed = sectionResult.data as Record; @@ -191,32 +58,6 @@ export function loadConfigFromPath( migrateConfigFile(configPath, rawConfig); - const typoWarnings = detectLikelyBuiltinAgentTypos(rawConfig); - if (typoWarnings.length > 0) { - const warningMsg = typoWarnings - .map((warning) => `agents.${warning.key} (did you mean agents.${warning.suggestion}?)`) - .join(", "); - log(`Potential agent override typos in ${configPath}: ${warningMsg}`); - addConfigLoadError({ - path: configPath, - error: `Potential agent override typos detected: ${warningMsg}`, - }); - } - - const unknownAgentKeys = detectUnknownBuiltinAgentKeys( - rawConfig, - typoWarnings.map((warning) => warning.key), - ); - if (unknownAgentKeys.length > 0) { - const unknownKeysMsg = unknownAgentKeys.map((key) => `agents.${key}`).join(", "); - const migrationHint = "Move custom entries from agents.* to custom_agents.*"; - log(`Unknown built-in agent override keys in ${configPath}: ${unknownKeysMsg}. ${migrationHint}`); - addConfigLoadError({ - path: configPath, - error: `Unknown built-in agent override keys: ${unknownKeysMsg}. ${migrationHint}`, - }); - } - const result = OhMyOpenCodeConfigSchema.safeParse(rawConfig); if (result.success) { @@ -257,7 +98,6 @@ export function mergeConfigs( ...base, ...override, agents: deepMerge(base.agents, override.agents), - custom_agents: deepMerge(base.custom_agents, override.custom_agents), categories: deepMerge(base.categories, override.categories), disabled_agents: [ ...new Set([ @@ -330,7 +170,6 @@ export function loadPluginConfig( log("Final merged config", { agents: config.agents, - custom_agents: config.custom_agents, disabled_agents: config.disabled_agents, disabled_mcps: config.disabled_mcps, disabled_hooks: config.disabled_hooks, diff --git a/src/plugin-handlers/agent-config-handler.ts b/src/plugin-handlers/agent-config-handler.ts index 4e61b5c15..08230f55a 100644 --- a/src/plugin-handlers/agent-config-handler.ts +++ b/src/plugin-handlers/agent-config-handler.ts @@ -4,7 +4,6 @@ import type { OhMyOpenCodeConfig } from "../config"; import { log, migrateAgentConfig } from "../shared"; import { AGENT_NAME_MAP } from "../shared/migration"; import { getAgentDisplayName } from "../shared/agent-display-names"; -import { mergeCategories } from "../shared/merge-categories"; import { discoverConfigSourceSkills, discoverOpencodeGlobalSkills, @@ -18,13 +17,6 @@ import { reorderAgentsByPriority } from "./agent-priority-order"; import { remapAgentKeysToDisplayNames } from "./agent-key-remapper"; import { buildPrometheusAgentConfig } from "./prometheus-agent-config-builder"; import { buildPlanDemoteConfig } from "./plan-model-inheritance"; -import { - applyCustomAgentOverrides, - collectCustomAgentSummariesFromRecord, - mergeCustomAgentSummaries, - collectKnownCustomAgentNames, - filterSummariesByKnownNames, -} from "./custom-agent-utils"; type AgentConfigRecord = Record | undefined> & { build?: Record; @@ -82,19 +74,26 @@ export async function applyAgentConfig(params: { const browserProvider = params.pluginConfig.browser_automation_engine?.provider ?? "playwright"; const currentModel = params.config.model as string | undefined; - const disabledAgentNames = new Set( - (migratedDisabledAgents ?? []).map((agent) => agent.toLowerCase()), - ); - const filterDisabledAgents = (agents: Record) => - Object.fromEntries( - Object.entries(agents).filter( - ([name]) => !disabledAgentNames.has(name.toLowerCase()), - ), - ); const disabledSkills = new Set(params.pluginConfig.disabled_skills ?? []); const useTaskSystem = params.pluginConfig.experimental?.task_system ?? false; const disableOmoEnv = params.pluginConfig.experimental?.disable_omo_env ?? false; + const builtinAgents = await createBuiltinAgents( + migratedDisabledAgents, + params.pluginConfig.agents, + params.ctx.directory, + currentModel, + params.pluginConfig.categories, + params.pluginConfig.git_master, + allDiscoveredSkills, + params.ctx.client, + browserProvider, + currentModel, + disabledSkills, + useTaskSystem, + disableOmoEnv, + ); + const includeClaudeAgents = params.pluginConfig.claude_code?.agents ?? true; const userAgents = includeClaudeAgents ? loadUserAgents() : {}; const projectAgents = includeClaudeAgents ? loadProjectAgents(params.ctx.directory) : {}; @@ -107,49 +106,15 @@ export async function applyAgentConfig(params: { ]), ); - const configAgent = params.config.agent as AgentConfigRecord | undefined; - const filteredUserAgents = filterDisabledAgents(userAgents as Record); - const filteredProjectAgents = filterDisabledAgents(projectAgents as Record); - const filteredPluginAgents = filterDisabledAgents(pluginAgents as Record); - const filteredConfigAgentsForSummary = filterDisabledAgents( - (configAgent as Record | undefined) ?? {}, + const disabledAgentNames = new Set( + (migratedDisabledAgents ?? []).map(a => a.toLowerCase()) ); - const mergedCategories = mergeCategories(params.pluginConfig.categories) - const knownCustomAgentNames = collectKnownCustomAgentNames( - filteredUserAgents, - filteredProjectAgents, - filteredPluginAgents, - filteredConfigAgentsForSummary, - ) - const customAgentSummaries = mergeCustomAgentSummaries( - collectCustomAgentSummariesFromRecord(filteredUserAgents), - collectCustomAgentSummariesFromRecord(filteredProjectAgents), - collectCustomAgentSummariesFromRecord(filteredPluginAgents), - collectCustomAgentSummariesFromRecord(filteredConfigAgentsForSummary), - filterSummariesByKnownNames( - collectCustomAgentSummariesFromRecord( - params.pluginConfig.custom_agents as Record | undefined, - ), - knownCustomAgentNames, - ), - ) + const filterDisabledAgents = (agents: Record) => + Object.fromEntries( + Object.entries(agents).filter(([name]) => !disabledAgentNames.has(name.toLowerCase())) + ); - const builtinAgents = await createBuiltinAgents( - migratedDisabledAgents, - params.pluginConfig.agents, - params.ctx.directory, - currentModel, - params.pluginConfig.categories, - params.pluginConfig.git_master, - allDiscoveredSkills, - customAgentSummaries, - browserProvider, - currentModel, - disabledSkills, - useTaskSystem, - disableOmoEnv, - ); const isSisyphusEnabled = params.pluginConfig.sisyphus_agent?.disabled !== true; const builderEnabled = params.pluginConfig.sisyphus_agent?.default_builder_enabled ?? false; @@ -158,6 +123,8 @@ export async function applyAgentConfig(params: { const shouldDemotePlan = plannerEnabled && replacePlan; const configuredDefaultAgent = getConfiguredDefaultAgent(params.config); + const configAgent = params.config.agent as AgentConfigRecord | undefined; + if (isSisyphusEnabled && builtinAgents.sisyphus) { if (configuredDefaultAgent) { (params.config as { default_agent?: string }).default_agent = @@ -201,7 +168,6 @@ export async function applyAgentConfig(params: { pluginPrometheusOverride: prometheusOverride, userCategories: params.pluginConfig.categories, currentModel, - customAgentSummaries, }); } @@ -237,9 +203,9 @@ export async function applyAgentConfig(params: { ...Object.fromEntries( Object.entries(builtinAgents).filter(([key]) => key !== "sisyphus"), ), - ...filteredUserAgents, - ...filteredProjectAgents, - ...filteredPluginAgents, + ...filterDisabledAgents(userAgents), + ...filterDisabledAgents(projectAgents), + ...filterDisabledAgents(pluginAgents), ...filteredConfigAgents, build: { ...migratedBuild, mode: "subagent", hidden: true }, ...(planDemoteConfig ? { plan: planDemoteConfig } : {}), @@ -247,31 +213,13 @@ export async function applyAgentConfig(params: { } else { params.config.agent = { ...builtinAgents, - ...filteredUserAgents, - ...filteredProjectAgents, - ...filteredPluginAgents, + ...filterDisabledAgents(userAgents), + ...filterDisabledAgents(projectAgents), + ...filterDisabledAgents(pluginAgents), ...configAgent, }; } - if (params.config.agent) { - const builtinOverrideKeys = new Set([ - ...Object.keys(builtinAgents).map((key) => key.toLowerCase()), - "build", - "plan", - "sisyphus-junior", - "opencode-builder", - ]) - - applyCustomAgentOverrides({ - mergedAgents: params.config.agent as Record, - userOverrides: params.pluginConfig.custom_agents, - builtinOverrideKeys, - mergedCategories, - directory: params.ctx.directory, - }) - } - if (params.config.agent) { params.config.agent = remapAgentKeysToDisplayNames( params.config.agent as Record, diff --git a/src/plugin-handlers/config-handler.test.ts b/src/plugin-handlers/config-handler.test.ts index 3dbe54f4b..7b735afe4 100644 --- a/src/plugin-handlers/config-handler.test.ts +++ b/src/plugin-handlers/config-handler.test.ts @@ -162,347 +162,6 @@ describe("Sisyphus-Junior model inheritance", () => { }) }) -describe("custom agent overrides", () => { - test("passes custom agent summaries into builtin agent prompt builder", async () => { - // #given - ;(agentLoader.loadUserAgents as any).mockReturnValue({ - translator: { - name: "translator", - mode: "subagent", - description: "Translate and localize text", - prompt: "Translate content", - }, - }) - const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { - mock: { calls: unknown[][] } - } - - const pluginConfig: OhMyOpenCodeConfig = { - sisyphus_agent: { - planner_enabled: true, - }, - } - const config: Record = { - 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 firstCallArgs = createBuiltinAgentsMock.mock.calls[0] - expect(firstCallArgs).toBeDefined() - expect(Array.isArray(firstCallArgs[7])).toBe(true) - expect(firstCallArgs[7]).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - name: "translator", - description: "Translate and localize text", - }), - ]), - ) - }) - - test("applies oh-my-opencode agent overrides to custom Claude agents", async () => { - // #given - ;(agentLoader.loadUserAgents as any).mockReturnValue({ - translator: { - name: "translator", - mode: "subagent", - description: "(user) translator", - prompt: "Base translator prompt", - }, - }) - - const pluginConfig: OhMyOpenCodeConfig = { - custom_agents: { - translator: { - model: "google/gemini-3-flash-preview", - temperature: 0, - prompt_append: "Always preserve placeholders exactly.", - }, - }, - } - - const config: Record = { - 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 agentConfig = config.agent as Record - expect(agentConfig.translator).toBeDefined() - expect(agentConfig.translator.model).toBe("google/gemini-3-flash-preview") - expect(agentConfig.translator.temperature).toBe(0) - expect(agentConfig.translator.prompt).toContain("Base translator prompt") - expect(agentConfig.translator.prompt).toContain("Always preserve placeholders exactly.") - }) - - test("prometheus prompt includes custom agent catalog for planning", async () => { - // #given - ;(agentLoader.loadUserAgents as any).mockReturnValue({ - translator: { - name: "translator", - mode: "subagent", - description: "Translate and localize locale files", - prompt: "Translate content", - }, - }) - - const pluginConfig: OhMyOpenCodeConfig = { - sisyphus_agent: { - planner_enabled: true, - }, - } - const config: Record = { - 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 - const pKey = getAgentDisplayName("prometheus") - expect(agentsConfig[pKey]).toBeDefined() - expect(agentsConfig[pKey].prompt).toContain("") - expect(agentsConfig[pKey].prompt).toContain("translator") - expect(agentsConfig[pKey].prompt).toContain("Translate and localize locale files") - }) - - test("prometheus prompt excludes unknown custom_agents entries", async () => { - // #given - ;(agentLoader.loadUserAgents as any).mockReturnValue({ - translator: { - name: "translator", - mode: "subagent", - description: "Translate and localize locale files", - prompt: "Translate content", - }, - }) - - const pluginConfig: OhMyOpenCodeConfig = { - custom_agents: { - translator: { - description: "Translate and localize locale files", - }, - ghostwriter: { - description: "This agent does not exist in runtime", - }, - }, - sisyphus_agent: { - planner_enabled: true, - }, - } - - const config: Record = { - 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 - const pKey = getAgentDisplayName("prometheus") - expect(agentsConfig[pKey]).toBeDefined() - expect(agentsConfig[pKey].prompt).toContain("translator") - 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 = { - 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 - 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 = { - 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 - const pKey = getAgentDisplayName("prometheus") - expect(agentsConfig[pKey]).toBeDefined() - expect(agentsConfig[pKey].prompt).toContain("Custom planner prompt") - expect(agentsConfig[pKey].prompt).toContain("") - expect(agentsConfig[pKey].prompt).toContain("translator") - }) - - test("custom agent summary merge preserves flags when custom_agents adds description", async () => { - // #given - ;(agentLoader.loadUserAgents as any).mockReturnValue({ - translator: { - name: "translator", - mode: "subagent", - description: "", - hidden: true, - disabled: true, - enabled: false, - prompt: "Translate content", - }, - }) - const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { - mock: { calls: unknown[][] } - } - - const pluginConfig: OhMyOpenCodeConfig = { - custom_agents: { - translator: { - description: "Translate and localize locale files", - }, - }, - sisyphus_agent: { - planner_enabled: true, - }, - } - const config: Record = { - 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 firstCallArgs = createBuiltinAgentsMock.mock.calls[0] - const summaries = firstCallArgs[7] as Array<{ - name: string - description: string - hidden?: boolean - disabled?: boolean - enabled?: boolean - }> - const translatorSummary = summaries.find((summary) => summary.name === "translator") - - expect(translatorSummary).toBeDefined() - expect(translatorSummary?.description).toBe("Translate and localize locale files") - expect(translatorSummary?.hidden).toBe(true) - expect(translatorSummary?.disabled).toBe(true) - expect(translatorSummary?.enabled).toBe(false) - }) -}) - describe("Plan agent demote behavior", () => { test("orders core agents as sisyphus -> hephaestus -> prometheus -> atlas", async () => { // #given diff --git a/src/plugin-handlers/custom-agent-utils.ts b/src/plugin-handlers/custom-agent-utils.ts deleted file mode 100644 index eb0568727..000000000 --- a/src/plugin-handlers/custom-agent-utils.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { AgentConfig } from "@opencode-ai/sdk"; -import { applyOverrides } from "../agents/builtin-agents/agent-overrides"; -import type { AgentOverrideConfig } from "../agents/types"; -import type { OhMyOpenCodeConfig } from "../config"; -import { getAgentConfigKey } from "../shared/agent-display-names"; -import { AGENT_NAME_MAP } from "../shared/migration"; -import { mergeCategories } from "../shared/merge-categories"; - -const RESERVED_AGENT_KEYS = new Set( - [ - "build", - "plan", - "sisyphus-junior", - "opencode-builder", - ...Object.keys(AGENT_NAME_MAP), - ...Object.values(AGENT_NAME_MAP), - ].map((key) => getAgentConfigKey(key).toLowerCase()), -); - -export type AgentSummary = { - name: string; - description: string; - hidden?: boolean; - disabled?: boolean; - enabled?: boolean; -}; - -export function applyCustomAgentOverrides(params: { - mergedAgents: Record; - userOverrides: OhMyOpenCodeConfig["custom_agents"] | undefined; - builtinOverrideKeys: Set; - mergedCategories: ReturnType; - directory: string; -}): void { - if (!params.userOverrides) return; - - for (const [overrideKey, override] of Object.entries(params.userOverrides)) { - if (!override) continue; - - const normalizedOverrideKey = getAgentConfigKey(overrideKey).toLowerCase(); - if (params.builtinOverrideKeys.has(normalizedOverrideKey)) continue; - - const existingKey = Object.keys(params.mergedAgents).find( - (key) => key.toLowerCase() === overrideKey.toLowerCase() || key.toLowerCase() === normalizedOverrideKey, - ); - if (!existingKey) continue; - - const existingAgent = params.mergedAgents[existingKey]; - if (!existingAgent || typeof existingAgent !== "object") continue; - - params.mergedAgents[existingKey] = applyOverrides( - existingAgent as AgentConfig, - override as AgentOverrideConfig, - params.mergedCategories, - params.directory, - ); - } -} - -export function collectCustomAgentSummariesFromRecord( - agents: Record | undefined, -): AgentSummary[] { - if (!agents) return []; - - const summaries: AgentSummary[] = []; - for (const [name, value] of Object.entries(agents)) { - const normalizedName = getAgentConfigKey(name).toLowerCase(); - if (RESERVED_AGENT_KEYS.has(normalizedName)) continue; - if (!value || typeof value !== "object") continue; - - const agentValue = value as Record; - const description = typeof agentValue.description === "string" ? agentValue.description : ""; - - summaries.push({ - name, - description, - hidden: typeof agentValue.hidden === "boolean" ? agentValue.hidden : undefined, - disabled: typeof agentValue.disabled === "boolean" ? agentValue.disabled : undefined, - enabled: typeof agentValue.enabled === "boolean" ? agentValue.enabled : undefined, - }); - } - - return summaries; -} - -export function mergeCustomAgentSummaries(...summaryGroups: AgentSummary[][]): AgentSummary[] { - const merged = new Map(); - - for (const group of summaryGroups) { - for (const summary of group) { - const key = summary.name.toLowerCase(); - if (!merged.has(key)) { - merged.set(key, summary); - continue; - } - - const existing = merged.get(key); - if (!existing) continue; - - const existingDescription = existing.description.trim(); - const incomingDescription = summary.description.trim(); - - merged.set(key, { - ...existing, - ...summary, - hidden: summary.hidden ?? existing.hidden, - disabled: summary.disabled ?? existing.disabled, - enabled: summary.enabled ?? existing.enabled, - description: incomingDescription || existingDescription, - }); - } - } - - return Array.from(merged.values()); -} - -export function collectKnownCustomAgentNames( - ...agentGroups: Array | undefined> -): Set { - const knownNames = new Set(); - - for (const group of agentGroups) { - if (!group) continue; - - for (const [name, value] of Object.entries(group)) { - const normalizedName = getAgentConfigKey(name).toLowerCase(); - if (RESERVED_AGENT_KEYS.has(normalizedName)) continue; - if (!value || typeof value !== "object") continue; - - knownNames.add(normalizedName); - } - } - - return knownNames; -} - -export function filterSummariesByKnownNames( - summaries: AgentSummary[], - knownNames: Set, -): AgentSummary[] { - return summaries.filter((summary) => knownNames.has(summary.name.toLowerCase())); -} diff --git a/src/plugin-handlers/prometheus-agent-config-builder.ts b/src/plugin-handlers/prometheus-agent-config-builder.ts index a2d63c84a..3c080ed10 100644 --- a/src/plugin-handlers/prometheus-agent-config-builder.ts +++ b/src/plugin-handlers/prometheus-agent-config-builder.ts @@ -1,7 +1,6 @@ import type { CategoryConfig } from "../config/schema"; import { PROMETHEUS_PERMISSION, getPrometheusPrompt } from "../agents/prometheus"; import { resolvePromptAppend } from "../agents/builtin-agents/resolve-file-uri"; -import { parseRegisteredAgentSummaries } from "../agents/custom-agent-summaries"; import { AGENT_MODEL_REQUIREMENTS } from "../shared/model-requirements"; import { fetchAvailableModels, @@ -28,7 +27,6 @@ export async function buildPrometheusAgentConfig(params: { pluginPrometheusOverride: PrometheusOverride | undefined; userCategories: Record | undefined; currentModel: string | undefined; - customAgentSummaries?: unknown; }): Promise> { const categoryConfig = params.pluginPrometheusOverride?.category ? resolveCategoryConfig(params.pluginPrometheusOverride.category, params.userCategories) @@ -67,18 +65,11 @@ export async function buildPrometheusAgentConfig(params: { const maxTokensToUse = params.pluginPrometheusOverride?.maxTokens ?? categoryConfig?.maxTokens; - const customAgentCatalog = parseRegisteredAgentSummaries(params.customAgentSummaries) - const customAgentBlock = customAgentCatalog.length > 0 - ? `\n\n\nAvailable custom agents for planning/delegation:\n${customAgentCatalog - .map((agent) => `- ${agent.name}: ${agent.description || "No description provided"}`) - .join("\n")}\n` - : "" - const base: Record = { ...(resolvedModel ? { model: resolvedModel } : {}), ...(variantToUse ? { variant: variantToUse } : {}), mode: "all", - prompt: getPrometheusPrompt(resolvedModel) + customAgentBlock, + prompt: getPrometheusPrompt(resolvedModel), permission: PROMETHEUS_PERMISSION, description: `${(params.configAgentPlan?.description as string) ?? "Plan agent"} (Prometheus - OhMyOpenCode)`, color: (params.configAgentPlan?.color as string) ?? "#FF5722", @@ -103,12 +94,5 @@ export async function buildPrometheusAgentConfig(params: { if (prompt_append && typeof merged.prompt === "string") { merged.prompt = merged.prompt + "\n" + resolvePromptAppend(prompt_append); } - if ( - customAgentBlock - && typeof merged.prompt === "string" - && !merged.prompt.includes("") - ) { - merged.prompt = merged.prompt + customAgentBlock; - } return merged; } diff --git a/src/tools/delegate-task/subagent-resolver.test.ts b/src/tools/delegate-task/subagent-resolver.test.ts index 6c6e78a3f..8482c6cf6 100644 --- a/src/tools/delegate-task/subagent-resolver.test.ts +++ b/src/tools/delegate-task/subagent-resolver.test.ts @@ -79,56 +79,4 @@ describe("resolveSubagentExecution", () => { error: "network timeout", }) }) - - test("uses inherited model for custom agents without explicit model", async () => { - //#given - const args = createBaseArgs({ subagent_type: "translator" }) - const executorCtx = createExecutorContext(async () => ({ - data: [{ name: "translator", mode: "subagent" }], - })) - - //#when - const result = await resolveSubagentExecution( - args, - executorCtx, - "sisyphus", - "deep", - "openai/gpt-5.3-codex", - "anthropic/claude-opus-4-6", - ) - - //#then - expect(result.error).toBeUndefined() - expect(result.agentToUse).toBe("translator") - expect(result.categoryModel).toEqual({ - providerID: "openai", - modelID: "gpt-5.3-codex", - }) - }) - - test("uses system default model when inherited model is unavailable", async () => { - //#given - const args = createBaseArgs({ subagent_type: "translator" }) - const executorCtx = createExecutorContext(async () => ({ - data: [{ name: "translator", mode: "subagent" }], - })) - - //#when - const result = await resolveSubagentExecution( - args, - executorCtx, - "sisyphus", - "deep", - undefined, - "anthropic/claude-opus-4-6", - ) - - //#then - expect(result.error).toBeUndefined() - expect(result.agentToUse).toBe("translator") - expect(result.categoryModel).toEqual({ - providerID: "anthropic", - modelID: "claude-opus-4-6", - }) - }) }) diff --git a/src/tools/delegate-task/subagent-resolver.ts b/src/tools/delegate-task/subagent-resolver.ts index 5ef52b576..1d0e65db6 100644 --- a/src/tools/delegate-task/subagent-resolver.ts +++ b/src/tools/delegate-task/subagent-resolver.ts @@ -15,9 +15,7 @@ export async function resolveSubagentExecution( args: DelegateTaskArgs, executorCtx: ExecutorContext, parentAgent: string | undefined, - categoryExamples: string, - inheritedModel?: string, - systemDefaultModel?: string, + categoryExamples: string ): Promise<{ agentToUse: string; categoryModel: { providerID: string; modelID: string; variant?: string } | undefined; fallbackChain?: FallbackEntry[]; error?: string }> { const { client, agentOverrides } = executorCtx @@ -126,16 +124,6 @@ Create the work plan directly - that's your job as the planning agent.`, if (!categoryModel && matchedAgent.model) { categoryModel = matchedAgent.model } - - if (!categoryModel) { - const fallbackModel = inheritedModel ?? systemDefaultModel - if (fallbackModel) { - const parsedFallback = parseModelString(fallbackModel) - if (parsedFallback) { - categoryModel = parsedFallback - } - } - } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) log("[delegate-task] Failed to resolve subagent execution", { diff --git a/src/tools/delegate-task/tools.ts b/src/tools/delegate-task/tools.ts index 9b0915330..fcd691f1f 100644 --- a/src/tools/delegate-task/tools.ts +++ b/src/tools/delegate-task/tools.ts @@ -226,14 +226,7 @@ export function createDelegateTask(options: DelegateTaskToolOptions): ToolDefini return executeUnstableAgentTask(args, ctx, options, parentContext, agentToUse, categoryModel, systemContent, actualModel) } } else { - const resolution = await resolveSubagentExecution( - args, - options, - parentContext.agent, - categoryExamples, - inheritedModel, - systemDefaultModel, - ) + const resolution = await resolveSubagentExecution(args, options, parentContext.agent, categoryExamples) if (resolution.error) { return resolution.error }