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:
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user