fix(agents): keep object keys clean by separating list display from runtime name

Discovered via post-implementation review (Oracle goal verification): the
prior commit's prefix swap (ZWSP -> ASCII spaces) inherited a pre-existing
architectural bug from the ZWSP era. `getAgentListDisplayName()` was an
alias for `getAgentRuntimeName()`, which meant every callsite that used
the "list display" name as an OBJECT KEY (config.agent keys, lookup keys,
HTTP-header-bound paths) ended up carrying the sort prefix.

This worked silently with ZWSP because zero-width characters are visually
invisible. With ASCII space prefixes, the same bug becomes user-visible
and violates the explicit RFC 7230 constraint documented in AGENTS.md:
"ZWSP MUST NOT appear in object keys (used as HTTP header values)."

Fix: separate the two concepts that were conflated.

- `getAgentListDisplayName(key)` now returns the CLEAN display name
  (alias of `getAgentDisplayName`). Used for object keys, config keys,
  and any path where the name will be sent over HTTP.
- `getAgentRuntimeName(key)` keeps its prefixed return value. Used ONLY
  for the `.name` field that OpenCode reads for `localeCompare` sort.

`agent-key-remapper.ts` was already correct: it uses `getAgentRuntimeName`
for the `.name` field. The bug was that `getAgentListDisplayName` (used
as the object key) also returned the prefix.

Test updates:
- agent-display-names.test.ts splits the assertions: getAgentListDisplayName
  asserts clean names, new getAgentRuntimeName describe asserts prefixes
- All other tests using getAgentListDisplayName as an expected object key
  continue to pass because they always wanted clean names

Verification:
- bun test: 5769 pass / 10 pre-existing failures (unchanged)
- bun run typecheck: clean
- Manual: agent-key-remapper output keys verified RFC 7230 safe (no
  leading whitespace, no ZWSP); name fields preserve descending-space
  prefix for canonical core agent ordering
This commit is contained in:
YeonGyu-Kim
2026-04-27 15:30:20 +09:00
parent 0e9bb5969d
commit f100a8565b
2 changed files with 22 additions and 20 deletions
+19 -6
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from "bun:test"
import { AGENT_DISPLAY_NAMES, getAgentConfigKey, getAgentDisplayName, getAgentListDisplayName, normalizeAgentForPrompt, normalizeAgentForPromptKey } from "./agent-display-names"
import { AGENT_DISPLAY_NAMES, getAgentConfigKey, getAgentDisplayName, getAgentListDisplayName, getAgentRuntimeName, normalizeAgentForPrompt, normalizeAgentForPromptKey } from "./agent-display-names"
describe("getAgentDisplayName", () => {
it("returns display name for lowercase config key (new format)", () => {
@@ -194,11 +194,11 @@ describe("getAgentConfigKey", () => {
})
describe("getAgentListDisplayName", () => {
it("applies leading-space stable-sort prefixes so OpenCode localeCompare yields canonical order", () => {
expect(getAgentListDisplayName("sisyphus")).toBe(" Sisyphus - Ultraworker")
expect(getAgentListDisplayName("hephaestus")).toBe(" Hephaestus - Deep Agent")
expect(getAgentListDisplayName("prometheus")).toBe(" Prometheus - Plan Builder")
expect(getAgentListDisplayName("atlas")).toBe(" Atlas - Plan Executor")
it("returns clean display names for object keys (no leading whitespace, RFC 7230 safe)", () => {
expect(getAgentListDisplayName("sisyphus")).toBe("Sisyphus - Ultraworker")
expect(getAgentListDisplayName("hephaestus")).toBe("Hephaestus - Deep Agent")
expect(getAgentListDisplayName("prometheus")).toBe("Prometheus - Plan Builder")
expect(getAgentListDisplayName("atlas")).toBe("Atlas - Plan Executor")
})
it("keeps non-core agents unprefixed for list display", () => {
@@ -206,6 +206,19 @@ describe("getAgentListDisplayName", () => {
})
})
describe("getAgentRuntimeName", () => {
it("applies leading-space stable-sort prefixes so OpenCode localeCompare yields canonical order", () => {
expect(getAgentRuntimeName("sisyphus")).toBe(" Sisyphus - Ultraworker")
expect(getAgentRuntimeName("hephaestus")).toBe(" Hephaestus - Deep Agent")
expect(getAgentRuntimeName("prometheus")).toBe(" Prometheus - Plan Builder")
expect(getAgentRuntimeName("atlas")).toBe(" Atlas - Plan Executor")
})
it("keeps non-core agents unprefixed (no entry in AGENT_LIST_SORT_PREFIXES)", () => {
expect(getAgentRuntimeName("oracle")).toBe("oracle")
})
})
describe("normalizeAgentForPrompt", () => {
it("strips core UI ordering prefixes back to canonical display names", () => {
expect(normalizeAgentForPrompt(getAgentListDisplayName("sisyphus"))).toBe("Sisyphus - Ultraworker")
+3 -14
View File
@@ -50,31 +50,20 @@ export function getAgentRuntimeName(configKey: string): string {
return prefix ? `${prefix}${displayName}` : displayName
}
/**
* Get display name for an agent config key.
* Uses case-insensitive lookup for backward compatibility.
* Returns original key if not found.
*/
export function getAgentDisplayName(configKey: string): string {
// Try exact match first
const exactMatch = AGENT_DISPLAY_NAMES[configKey]
if (exactMatch !== undefined) return exactMatch
// Fall back to case-insensitive search
const lowerKey = configKey.toLowerCase()
for (const [k, v] of Object.entries(AGENT_DISPLAY_NAMES)) {
if (k.toLowerCase() === lowerKey) return v
}
// Unknown agent: return original key
return configKey
}
/**
* Runtime-facing agent name used for OpenCode list ordering.
*/
export function getAgentListDisplayName(configKey: string): string {
return getAgentRuntimeName(configKey)
return getAgentDisplayName(configKey)
}
const REVERSE_DISPLAY_NAMES: Record<string, string> = Object.fromEntries(