25d183fbe8
Agent names like \hephaestus\ cause 'Agent not found' errors because sanitizeSubagentType() was only called in subagent-resolver.ts but not in the direct manager.launch() path or background-executor.ts. - manager.ts: strip leading/trailing backslash/quote chars from input.agent before validation so \hephaestus\ → hephaestus - background-executor.ts: call sanitizeSubagentType(args.subagent_type) instead of passing raw value to manager.launch() - agent-display-names.ts: reuse sanitizeSubagentType in stripAgentListSortPrefix - Add unit tests for all three fix points Fixes: sessions dying with 'Agent not found: \hephaestus\'
146 lines
5.2 KiB
TypeScript
146 lines
5.2 KiB
TypeScript
/**
|
|
* Agent config keys to display names mapping.
|
|
* Config keys are lowercase (e.g., "sisyphus", "atlas").
|
|
* Display names include suffixes for UI/logs (e.g., "Sisyphus - Ultraworker").
|
|
*
|
|
* IMPORTANT: Display names MUST NOT contain parentheses or other characters
|
|
* that are invalid in HTTP header values per RFC 7230. OpenCode passes the
|
|
* agent name in the `x-opencode-agent-name` header, and parentheses cause
|
|
* header validation failures that prevent agents from appearing in the UI
|
|
* type selector dropdown. Use ` - ` (space-dash-space) instead of `(...)`.
|
|
*/
|
|
export const AGENT_DISPLAY_NAMES: Record<string, string> = {
|
|
sisyphus: "Sisyphus - Ultraworker",
|
|
hephaestus: "Hephaestus - Deep Agent",
|
|
prometheus: "Prometheus - Plan Builder",
|
|
atlas: "Atlas - Plan Executor",
|
|
"sisyphus-junior": "Sisyphus-Junior",
|
|
metis: "Metis - Plan Consultant",
|
|
momus: "Momus - Plan Critic",
|
|
athena: "Athena - Council",
|
|
"athena-junior": "Athena-Junior - Council",
|
|
oracle: "oracle",
|
|
librarian: "librarian",
|
|
explore: "explore",
|
|
"multimodal-looker": "multimodal-looker",
|
|
"council-member": "council-member",
|
|
}
|
|
|
|
const INVISIBLE_AGENT_CHARACTERS_REGEX = /[\u200B\u200C\u200D\uFEFF]/g
|
|
const VISIBLE_AGENT_LIST_SORT_PREFIX_REGEX = /^\d+\|/
|
|
const AGENT_WRAPPER_CHARS_REGEX = /^[\\/"']+|[\\/"']+$/g
|
|
|
|
export function stripInvisibleAgentCharacters(agentName: string): string {
|
|
return agentName.replace(INVISIBLE_AGENT_CHARACTERS_REGEX, "")
|
|
}
|
|
|
|
export function stripAgentListSortPrefix(agentName: string): string {
|
|
return stripInvisibleAgentCharacters(agentName).replace(VISIBLE_AGENT_LIST_SORT_PREFIX_REGEX, "").replace(AGENT_WRAPPER_CHARS_REGEX, "")
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
}
|
|
|
|
/**
|
|
* Thin alias for `getAgentDisplayName` preserved for external imports.
|
|
*
|
|
* Earlier versions injected zero-width prefixes here to bias OpenCode's
|
|
* `agent.name` sort. Sort ordering is now enforced by
|
|
* `src/shared/agent-sort-shim.ts`, so this function emits the canonical
|
|
* display name verbatim. Kept exported because downstream modules still
|
|
* import this symbol; do not collapse the call sites without coordinating.
|
|
*/
|
|
export function getAgentListDisplayName(configKey: string): string {
|
|
return getAgentDisplayName(configKey)
|
|
}
|
|
|
|
const REVERSE_DISPLAY_NAMES: Record<string, string> = Object.fromEntries(
|
|
Object.entries(AGENT_DISPLAY_NAMES).map(([key, displayName]) => [displayName.toLowerCase(), key]),
|
|
)
|
|
|
|
// Legacy parenthesized display names for backward compatibility.
|
|
// Old configs/sessions may reference these names; resolve them to config keys.
|
|
const LEGACY_DISPLAY_NAMES: Record<string, string> = {
|
|
"sisyphus (ultraworker)": "sisyphus",
|
|
"hephaestus (deep agent)": "hephaestus",
|
|
"prometheus (plan builder)": "prometheus",
|
|
"atlas (plan executor)": "atlas",
|
|
"metis (plan consultant)": "metis",
|
|
"momus (plan critic)": "momus",
|
|
"athena (council)": "athena",
|
|
"athena-junior (council)": "athena-junior",
|
|
}
|
|
|
|
function resolveKnownAgentConfigKey(agentName: string): string | undefined {
|
|
const lower = stripAgentListSortPrefix(agentName).trim().toLowerCase()
|
|
const reversed = REVERSE_DISPLAY_NAMES[lower]
|
|
if (reversed !== undefined) return reversed
|
|
const legacy = LEGACY_DISPLAY_NAMES[lower]
|
|
if (legacy !== undefined) return legacy
|
|
if (AGENT_DISPLAY_NAMES[lower] !== undefined) return lower
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* Resolve an agent name (display name or config key) to its lowercase config key.
|
|
* "Atlas - Plan Executor" -> "atlas", "Atlas (Plan Executor)" -> "atlas", "atlas" -> "atlas"
|
|
*/
|
|
export function getAgentConfigKey(agentName: string): string {
|
|
const lower = stripAgentListSortPrefix(agentName).trim().toLowerCase()
|
|
return resolveKnownAgentConfigKey(agentName) ?? lower
|
|
}
|
|
|
|
/**
|
|
* Normalize an agent name for prompt APIs.
|
|
* - Known display names -> canonical display names
|
|
* - Known config keys (any case) -> canonical display names
|
|
* - Unknown/custom names -> preserved as-is (trimmed)
|
|
*/
|
|
export function normalizeAgentForPrompt(agentName: string | undefined): string | undefined {
|
|
if (typeof agentName !== "string") {
|
|
return undefined
|
|
}
|
|
|
|
const trimmed = stripAgentListSortPrefix(agentName).trim()
|
|
if (!trimmed) {
|
|
return undefined
|
|
}
|
|
|
|
const configKey = resolveKnownAgentConfigKey(trimmed)
|
|
if (configKey !== undefined) {
|
|
return AGENT_DISPLAY_NAMES[configKey] ?? trimmed
|
|
}
|
|
|
|
return trimmed
|
|
}
|
|
|
|
export function normalizeAgentForPromptKey(agentName: string | undefined): string | undefined {
|
|
if (typeof agentName !== "string") {
|
|
return undefined
|
|
}
|
|
|
|
const trimmed = stripAgentListSortPrefix(agentName).trim()
|
|
if (!trimmed) {
|
|
return undefined
|
|
}
|
|
|
|
return resolveKnownAgentConfigKey(trimmed) ?? trimmed
|
|
}
|