From 9f135af41cdf50e88c4615a7b8a713039e37c30e Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 11 Apr 2026 14:53:01 +0900 Subject: [PATCH] fix: unify agent display names and strip invisible sort prefixes - Replace getAgentRuntimeName with getAgentDisplayName for consistency - Add stripAgentListSortPrefix helper to normalize agent names - Strip sort prefixes in subagent-resolver and sync-executor - Backfill canonical names for core agents when builtin configs omit name - Update tests to match new behavior --- .../agent-config-handler.test.ts | 15 +++- src/plugin-handlers/agent-config-handler.ts | 6 +- .../agent-key-remapper.test.ts | 49 +++++++++++-- src/plugin-handlers/agent-key-remapper.ts | 10 +-- src/plugin-handlers/config-handler.test.ts | 71 +++++++++++++++++-- .../call-omo-agent/sync-executor.test.ts | 21 ++++++ src/tools/call-omo-agent/sync-executor.ts | 8 ++- src/tools/delegate-task/subagent-resolver.ts | 17 +++-- .../subagent-resolver.test.ts | 20 ++++++ 9 files changed, 180 insertions(+), 37 deletions(-) diff --git a/src/plugin-handlers/agent-config-handler.test.ts b/src/plugin-handlers/agent-config-handler.test.ts index 052e3d6be..ac7551782 100644 --- a/src/plugin-handlers/agent-config-handler.test.ts +++ b/src/plugin-handlers/agent-config-handler.test.ts @@ -201,7 +201,10 @@ describe("applyAgentConfig builtin override protection", () => { }) // then - expect(result[BUILTIN_SISYPHUS_DISPLAY_NAME]).toEqual(builtinSisyphusConfig) + expect(result[BUILTIN_SISYPHUS_DISPLAY_NAME]).toEqual({ + ...builtinSisyphusConfig, + name: getAgentDisplayName("sisyphus"), + }) }) test("filters user agents whose key differs from a builtin key only by case", async () => { @@ -223,7 +226,10 @@ describe("applyAgentConfig builtin override protection", () => { }) // then - expect(result[BUILTIN_SISYPHUS_DISPLAY_NAME]).toEqual(builtinSisyphusConfig) + expect(result[BUILTIN_SISYPHUS_DISPLAY_NAME]).toEqual({ + ...builtinSisyphusConfig, + name: getAgentDisplayName("sisyphus"), + }) expect(result.SiSyPhUs).toBeUndefined() }) @@ -247,7 +253,10 @@ describe("applyAgentConfig builtin override protection", () => { }) // then - expect(result[BUILTIN_SISYPHUS_DISPLAY_NAME]).toEqual(builtinSisyphusConfig) + expect(result[BUILTIN_SISYPHUS_DISPLAY_NAME]).toEqual({ + ...builtinSisyphusConfig, + name: getAgentDisplayName("sisyphus"), + }) }) describe("#given protected builtin agents use hyphenated names", () => { diff --git a/src/plugin-handlers/agent-config-handler.ts b/src/plugin-handlers/agent-config-handler.ts index b8c7a9ee6..8c4a8b1fd 100644 --- a/src/plugin-handlers/agent-config-handler.ts +++ b/src/plugin-handlers/agent-config-handler.ts @@ -2,7 +2,7 @@ import { createBuiltinAgents } from "../agents"; import { createSisyphusJuniorAgentWithOverrides } from "../agents/sisyphus-junior"; import type { OhMyOpenCodeConfig } from "../config"; import { isTaskSystemEnabled, log, migrateAgentConfig } from "../shared"; -import { getAgentRuntimeName } from "../shared/agent-display-names"; +import { getAgentDisplayName } from "../shared/agent-display-names"; import { AGENT_NAME_MAP } from "../shared/migration"; import { registerAgentName } from "../features/claude-code-session-state"; import { @@ -159,10 +159,10 @@ export async function applyAgentConfig(params: { if (isSisyphusEnabled && builtinAgents.sisyphus) { if (configuredDefaultAgent) { (params.config as { default_agent?: string }).default_agent = - getAgentRuntimeName(configuredDefaultAgent); + getAgentDisplayName(configuredDefaultAgent); } else { (params.config as { default_agent?: string }).default_agent = - getAgentRuntimeName("sisyphus"); + getAgentDisplayName("sisyphus"); } // Assembly order: Sisyphus -> Hephaestus -> Prometheus -> Atlas diff --git a/src/plugin-handlers/agent-key-remapper.test.ts b/src/plugin-handlers/agent-key-remapper.test.ts index 821a323a6..94a81a0b6 100644 --- a/src/plugin-handlers/agent-key-remapper.test.ts +++ b/src/plugin-handlers/agent-key-remapper.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "bun:test" import { remapAgentKeysToDisplayNames } from "./agent-key-remapper" -import { getAgentDisplayName, getAgentRuntimeName } from "../shared/agent-display-names" +import { getAgentDisplayName } from "../shared/agent-display-names" describe("remapAgentKeysToDisplayNames", () => { it("remaps known agent keys to display names", () => { @@ -106,7 +106,7 @@ describe("remapAgentKeysToDisplayNames", () => { } }) - it("preserves clean keys but rewrites core agent name fields to list-display names for tab cycling", () => { + it("preserves clean keys and rewrites core agent name fields to canonical display names", () => { // given agents with raw config-key names const agents = { sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, @@ -119,7 +119,7 @@ describe("remapAgentKeysToDisplayNames", () => { // when remapping const result = remapAgentKeysToDisplayNames(agents) - // then keys stay HTTP-header-safe, but nested names carry stable list ordering + // then keys stay HTTP-header-safe, and nested names match the lookup-safe display names expect(Object.keys(result).slice(0, 4)).toEqual([ getAgentDisplayName("sisyphus"), getAgentDisplayName("hephaestus"), @@ -127,25 +127,60 @@ describe("remapAgentKeysToDisplayNames", () => { getAgentDisplayName("atlas"), ]) expect(result[getAgentDisplayName("sisyphus")]).toEqual({ - name: getAgentRuntimeName("sisyphus"), + name: getAgentDisplayName("sisyphus"), prompt: "test", mode: "primary", }) expect(result[getAgentDisplayName("hephaestus")]).toEqual({ - name: getAgentRuntimeName("hephaestus"), + name: getAgentDisplayName("hephaestus"), prompt: "test", mode: "primary", }) expect(result[getAgentDisplayName("prometheus")]).toEqual({ - name: getAgentRuntimeName("prometheus"), + name: getAgentDisplayName("prometheus"), prompt: "test", mode: "all", }) expect(result[getAgentDisplayName("atlas")]).toEqual({ - name: getAgentRuntimeName("atlas"), + name: getAgentDisplayName("atlas"), prompt: "test", mode: "primary", }) expect(result.oracle).toEqual({ name: "oracle", prompt: "test", mode: "subagent" }) }) + + it("backfills canonical display names for core agents when builtin configs omit name", () => { + // given builtin-style configs without name fields + const agents = { + sisyphus: { prompt: "test", mode: "primary" }, + hephaestus: { prompt: "test", mode: "primary" }, + prometheus: { prompt: "test", mode: "all" }, + atlas: { prompt: "test", mode: "primary" }, + } + + // when remapping + const result = remapAgentKeysToDisplayNames(agents) + + // then OpenCode receives lookup-safe display names and uses order for sorting + expect(result[getAgentDisplayName("sisyphus")]).toEqual({ + name: getAgentDisplayName("sisyphus"), + prompt: "test", + mode: "primary", + }) + expect(result[getAgentDisplayName("hephaestus")]).toEqual({ + name: getAgentDisplayName("hephaestus"), + prompt: "test", + mode: "primary", + }) + expect(result[getAgentDisplayName("prometheus")]).toEqual({ + name: getAgentDisplayName("prometheus"), + prompt: "test", + mode: "all", + }) + expect(result[getAgentDisplayName("atlas")]).toEqual({ + name: getAgentDisplayName("atlas"), + prompt: "test", + mode: "primary", + }) + }) }) diff --git a/src/plugin-handlers/agent-key-remapper.ts b/src/plugin-handlers/agent-key-remapper.ts index e6dafc1b0..c15241301 100644 --- a/src/plugin-handlers/agent-key-remapper.ts +++ b/src/plugin-handlers/agent-key-remapper.ts @@ -1,21 +1,17 @@ -import { getAgentDisplayName, getAgentRuntimeName } from "../shared/agent-display-names" +import { getAgentDisplayName } from "../shared/agent-display-names" function rewriteAgentNameForListDisplay( key: string, value: unknown, ): unknown { - if (typeof value !== "object" || value === null || !("name" in value)) { + if (typeof value !== "object" || value === null) { return value } const agent = value as Record - if (typeof agent.name !== "string") { - return value - } - return { ...agent, - name: getAgentRuntimeName(key), + name: getAgentDisplayName(key), } } diff --git a/src/plugin-handlers/config-handler.test.ts b/src/plugin-handlers/config-handler.test.ts index e8bc6873e..8fdc2d29c 100644 --- a/src/plugin-handlers/config-handler.test.ts +++ b/src/plugin-handlers/config-handler.test.ts @@ -315,6 +315,63 @@ describe("Plan agent demote behavior", () => { ]) }) + test("backfills core agent runtime names when builtin configs omit name", async () => { + // #given + const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { + mockResolvedValue: (value: Record) => void + } + createBuiltinAgentsMock.mockResolvedValue({ + sisyphus: { prompt: "test", mode: "primary" }, + hephaestus: { prompt: "test", mode: "primary" }, + oracle: { prompt: "test", mode: "subagent" }, + atlas: { prompt: "test", mode: "primary" }, + }) + const pluginConfig = createPluginConfig({ + 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 emittedCoreEntries = Object.entries( + config.agent as Record, + ).slice(0, 4) + + expect(emittedCoreEntries).toEqual([ + [ + getAgentDisplayName("sisyphus"), + expect.objectContaining({ name: getAgentDisplayName("sisyphus") }), + ], + [ + getAgentDisplayName("hephaestus"), + expect.objectContaining({ name: getAgentDisplayName("hephaestus") }), + ], + [ + getAgentDisplayName("prometheus"), + expect.objectContaining({ name: getAgentDisplayName("prometheus") }), + ], + [ + getAgentDisplayName("atlas"), + expect.objectContaining({ name: getAgentDisplayName("atlas") }), + ], + ]) + }) + test("plan agent should be demoted to subagent without inheriting prometheus prompt", async () => { // #given const pluginConfig = createPluginConfig({ @@ -479,7 +536,7 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // then - expect(config.default_agent).toBe(getAgentRuntimeName("hephaestus")) + expect(config.default_agent).toBe(getAgentDisplayName("hephaestus")) }) test("canonicalizes configured default_agent when key uses mixed case", async () => { @@ -503,7 +560,7 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // then - expect(config.default_agent).toBe(getAgentRuntimeName("hephaestus")) + expect(config.default_agent).toBe(getAgentDisplayName("hephaestus")) }) test("canonicalizes configured default_agent key to display name", async () => { @@ -527,7 +584,7 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // #then - expect(config.default_agent).toBe(getAgentRuntimeName("hephaestus")) + expect(config.default_agent).toBe(getAgentDisplayName("hephaestus")) }) test("preserves existing display-name default_agent", async () => { @@ -575,10 +632,10 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // #then - expect(config.default_agent).toBe(getAgentRuntimeName("sisyphus")) + expect(config.default_agent).toBe(getAgentDisplayName("sisyphus")) }) - test("uses runtime default_agent name so OpenCode matches the emitted ordered agent names", async () => { + test("uses canonical default_agent display name so OpenCode lookups match emitted agent keys", async () => { // given const pluginConfig = createPluginConfig({}) const config: Record = { @@ -599,7 +656,7 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // then - expect(config.default_agent).toBe(getAgentRuntimeName("hephaestus")) + expect(config.default_agent).toBe(getAgentDisplayName("hephaestus")) }) test("sets default_agent to sisyphus when configured default_agent is empty after trim", async () => { @@ -623,7 +680,7 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // then - expect(config.default_agent).toBe(getAgentRuntimeName("sisyphus")) + expect(config.default_agent).toBe(getAgentDisplayName("sisyphus")) }) test("preserves custom default_agent names while trimming whitespace", async () => { diff --git a/src/tools/call-omo-agent/sync-executor.test.ts b/src/tools/call-omo-agent/sync-executor.test.ts index 404e3fee0..4100a4da9 100644 --- a/src/tools/call-omo-agent/sync-executor.test.ts +++ b/src/tools/call-omo-agent/sync-executor.test.ts @@ -280,6 +280,27 @@ describe("executeSync", () => { expect(deps.processMessages).not.toHaveBeenCalled() }) + test("strips invisible sort prefixes before sending sync prompts", async () => { + //#given + const executeSync = await importExecuteSync() + const deps = createDependencies() + const toolContext = createToolContext() + const recorder = createPromptAsyncRecorder() + const args = { + subagent_type: "\u200BSisyphus - Ultraworker", + description: "prefixed agent", + prompt: "find something", + run_in_background: false, + } + + //#when + await executeSync(args, toolContext, createContext(recorder.promptAsync) as never, deps) + + //#then + const promptInput = recorder.getCapturedInput() + expect(promptInput?.body.agent).toBe("Sisyphus - Ultraworker") + }) + test("returns generic prompt failure with task metadata", async () => { //#given const executeSync = await importExecuteSync() diff --git a/src/tools/call-omo-agent/sync-executor.ts b/src/tools/call-omo-agent/sync-executor.ts index f0f65d7e1..23089d8ea 100644 --- a/src/tools/call-omo-agent/sync-executor.ts +++ b/src/tools/call-omo-agent/sync-executor.ts @@ -6,6 +6,7 @@ import { getAgentToolRestrictions, log } from "../../shared" import { applySessionPromptParams } from "../../shared/session-prompt-params-helpers" import type { DelegatedModelConfig } from "../../shared/model-resolution-types" import type { FallbackEntry } from "../../shared/model-requirements" +import { stripAgentListSortPrefix } from "../../shared/agent-display-names" import { waitForCompletion } from "./completion-poller" import { processMessages } from "./message-processor" import { createOrGetSession } from "./session-creator" @@ -99,14 +100,15 @@ export async function executeSync( log(`[call_omo_agent] Sending prompt to session ${sessionID}`) log(`[call_omo_agent] Prompt text:`, args.prompt.substring(0, 100)) + const normalizedSubagentType = stripAgentListSortPrefix(args.subagent_type) try { await (ctx.client.session as unknown as SessionWithPromptAsync).promptAsync({ path: { id: sessionID }, body: { - agent: args.subagent_type, + agent: normalizedSubagentType, tools: { - ...getAgentToolRestrictions(args.subagent_type), + ...getAgentToolRestrictions(normalizedSubagentType), task: false, question: false, }, @@ -120,7 +122,7 @@ export async function executeSync( const errorMessage = error instanceof Error ? error.message : String(error) log(`[call_omo_agent] Prompt error:`, errorMessage) if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) { - return `Error: Agent "${args.subagent_type}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.\n\n\nsession_id: ${sessionID}\n` + return `Error: Agent "${normalizedSubagentType}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.\n\n\nsession_id: ${sessionID}\n` } return `Error: Failed to send prompt: ${errorMessage}\n\n\nsession_id: ${sessionID}\n` } diff --git a/src/tools/delegate-task/subagent-resolver.ts b/src/tools/delegate-task/subagent-resolver.ts index 8ba7cdd8e..949e7ee2b 100644 --- a/src/tools/delegate-task/subagent-resolver.ts +++ b/src/tools/delegate-task/subagent-resolver.ts @@ -7,7 +7,7 @@ import { normalizeModelFormat } from "../../shared/model-format-normalizer" import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements" import { normalizeFallbackModels, flattenToFallbackModelStrings } from "../../shared/model-resolver" import { buildFallbackChainFromModels, findMostSpecificFallbackEntry } from "../../shared/fallback-chain-from-models" -import { getAgentDisplayName, getAgentConfigKey } from "../../shared/agent-display-names" +import { getAgentDisplayName, getAgentConfigKey, stripAgentListSortPrefix } from "../../shared/agent-display-names" import { normalizeSDKResponse } from "../../shared" import { log } from "../../shared/logger" import { getAvailableModelsForDelegateTask } from "./available-models" @@ -89,15 +89,18 @@ Create the work plan directly - that's your job as the planning agent.`, const callableAgents = agents.filter((agent) => isTaskCallableAgentMode(agent.mode)) - const resolvedDisplayName = getAgentDisplayName(agentToUse).replace(/^\u200B+/, "") - const normalizedAgentToUse = agentToUse.replace(/^\u200B+/, "") + const resolvedDisplayName = stripAgentListSortPrefix(getAgentDisplayName(agentToUse)) + const normalizedAgentToUse = stripAgentListSortPrefix(agentToUse) const matchedAgent = callableAgents.find( - (agent) => agent.name.toLowerCase() === normalizedAgentToUse.toLowerCase() - || agent.name.toLowerCase() === resolvedDisplayName.toLowerCase() + (agent) => { + const normalizedListedAgentName = stripAgentListSortPrefix(agent.name) + return normalizedListedAgentName.toLowerCase() === normalizedAgentToUse.toLowerCase() + || normalizedListedAgentName.toLowerCase() === resolvedDisplayName.toLowerCase() + } ) if (!matchedAgent) { const availableAgents = callableAgents - .map((a) => a.name) + .map((a) => stripAgentListSortPrefix(a.name)) .sort() .join(", ") return { @@ -107,7 +110,7 @@ Create the work plan directly - that's your job as the planning agent.`, } } - agentToUse = matchedAgent.name + agentToUse = stripAgentListSortPrefix(matchedAgent.name) const agentConfigKey = getAgentConfigKey(agentToUse) const agentOverride = agentOverrides?.[agentConfigKey as keyof typeof agentOverrides] diff --git a/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts b/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts index bf3c4f370..11684a4b3 100644 --- a/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts +++ b/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts @@ -732,4 +732,24 @@ describe("resolveSubagentExecution - agent name sanitization", () => { expect(result.error).toBeUndefined() expect(result.agentToUse).toBe("explore") }) + + test("matches runtime agent names that include invisible sort prefixes", async () => { + //#given + readProviderModelsCacheMock.mockReturnValue({ + models: {}, + connected: [], + updatedAt: "2026-03-03T00:00:00.000Z", + }) + const args = createBaseArgs({ subagent_type: "Sisyphus - Ultraworker" }) + const executorCtx = createExecutorContext(async () => ([ + { name: "\u200BSisyphus - Ultraworker", mode: "subagent", model: "openai/gpt-5.3-codex" }, + ])) + + //#when + const result = await resolveSubagentExecution(args, executorCtx, "oracle", "deep") + + //#then + expect(result.error).toBeUndefined() + expect(result.agentToUse).toBe("Sisyphus - Ultraworker") + }) })