diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index 7bd7709f..8392e8fd 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -855,7 +855,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () => .notifyParentSession(task) //#then - expect(capturedBody?.agent).toBe("sisyphus") + expect(capturedBody?.agent).toBe("Sisyphus (Ultraworker)") expect(capturedBody?.model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6" }) manager.shutdown() diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index 61e5d843..2233e0a2 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -15,6 +15,7 @@ import { resolveInheritedPromptTools, createInternalAgentTextPart, } from "../../shared" +import { normalizeAgentForPrompt } from "../../shared/agent-display-names" import { setSessionTools } from "../../shared/session-tools-store" import { SessionCategoryRegistry } from "../../shared/session-category-registry" import { ConcurrencyManager } from "./concurrency" @@ -1311,10 +1312,11 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea } const resolvedTools = resolveInheritedPromptTools(task.parentSessionID, tools) + const promptAgent = normalizeAgentForPrompt(agent) log("[background-agent] notifyParentSession context:", { taskId: task.id, - resolvedAgent: agent, + resolvedAgent: promptAgent, resolvedModel: model, }) @@ -1323,7 +1325,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea path: { id: task.parentSessionID }, body: { noReply: !allComplete, - ...(agent !== undefined ? { agent } : {}), + ...(promptAgent !== undefined ? { agent: promptAgent } : {}), ...(model !== undefined ? { model } : {}), ...(resolvedTools ? { tools: resolvedTools } : {}), parts: [createInternalAgentTextPart(notification)], diff --git a/src/hooks/atlas/boulder-continuation-injector.ts b/src/hooks/atlas/boulder-continuation-injector.ts index 289668b4..acfa7661 100644 --- a/src/hooks/atlas/boulder-continuation-injector.ts +++ b/src/hooks/atlas/boulder-continuation-injector.ts @@ -1,5 +1,6 @@ import type { PluginInput } from "@opencode-ai/plugin" import type { BackgroundManager } from "../../features/background-agent" +import { normalizeAgentForPrompt } from "../../shared/agent-display-names" import { log } from "../../shared/logger" import { createInternalAgentTextPart, resolveInheritedPromptTools } from "../../shared" import { HOOK_NAME } from "./hook-name" @@ -40,6 +41,7 @@ export async function injectBoulderContinuation(input: { const prompt = BOULDER_CONTINUATION_PROMPT.replace(/{PLAN_NAME}/g, planName) + `\n\n[Status: ${total - remaining}/${total} completed, ${remaining} remaining]` + const promptAgent = normalizeAgentForPrompt(agent ?? "atlas") ?? "atlas" try { log(`[${HOOK_NAME}] Injecting boulder continuation`, { sessionID, planName, remaining }) @@ -50,7 +52,7 @@ export async function injectBoulderContinuation(input: { await ctx.client.session.promptAsync({ path: { id: sessionID }, body: { - agent: agent ?? "atlas", + agent: promptAgent, ...(promptContext.model !== undefined ? { model: promptContext.model } : {}), ...(inheritedTools ? { tools: inheritedTools } : {}), parts: [createInternalAgentTextPart(prompt)], diff --git a/src/hooks/atlas/index.test.ts b/src/hooks/atlas/index.test.ts index 065f20b9..1d3b0084 100644 --- a/src/hooks/atlas/index.test.ts +++ b/src/hooks/atlas/index.test.ts @@ -997,7 +997,7 @@ describe("atlas hook", () => { // then - should call prompt for sisyphus expect(mockInput._promptMock).toHaveBeenCalled() const callArgs = mockInput._promptMock.mock.calls[0][0] - expect(callArgs.body.agent).toBe("sisyphus") + expect(callArgs.body.agent).toBe("Sisyphus (Ultraworker)") }) test("should debounce rapid continuation injections (prevent infinite loop)", async () => { diff --git a/src/hooks/ralph-loop/continuation-prompt-injector.ts b/src/hooks/ralph-loop/continuation-prompt-injector.ts index 58f31953..0b71eebd 100644 --- a/src/hooks/ralph-loop/continuation-prompt-injector.ts +++ b/src/hooks/ralph-loop/continuation-prompt-injector.ts @@ -8,6 +8,7 @@ import { normalizeSDKResponse, resolveInheritedPromptTools, } from "../../shared" +import { normalizeAgentForPrompt } from "../../shared/agent-display-names" type MessageInfo = { agent?: string @@ -68,11 +69,12 @@ export async function injectContinuationPrompt( } const inheritedTools = resolveInheritedPromptTools(sourceSessionID, tools) + const promptAgent = normalizeAgentForPrompt(agent) await ctx.client.session.promptAsync({ path: { id: options.sessionID }, body: { - ...(agent !== undefined ? { agent } : {}), + ...(promptAgent ? { agent: promptAgent } : {}), ...(model !== undefined ? { model } : {}), ...(inheritedTools ? { tools: inheritedTools } : {}), parts: [createInternalAgentTextPart(options.prompt)], diff --git a/src/hooks/session-recovery/resume.ts b/src/hooks/session-recovery/resume.ts index e5d187d7..168bb773 100644 --- a/src/hooks/session-recovery/resume.ts +++ b/src/hooks/session-recovery/resume.ts @@ -1,4 +1,5 @@ import type { createOpencodeClient } from "@opencode-ai/sdk" +import { normalizeAgentForPrompt } from "../../shared/agent-display-names" import type { MessageData, ResumeConfig } from "./types" import { createInternalAgentTextPart, resolveInheritedPromptTools } from "../../shared" @@ -25,13 +26,15 @@ export function extractResumeConfig(userMessage: MessageData | undefined, sessio } export async function resumeSession(client: Client, config: ResumeConfig): Promise { + const promptAgent = normalizeAgentForPrompt(config.agent) + try { const inheritedTools = resolveInheritedPromptTools(config.sessionID, config.tools) await client.session.promptAsync({ path: { id: config.sessionID }, body: { parts: [createInternalAgentTextPart(RECOVERY_RESUME_TEXT)], - agent: config.agent, + ...(promptAgent ? { agent: promptAgent } : {}), model: config.model, ...(inheritedTools ? { tools: inheritedTools } : {}), }, diff --git a/src/hooks/unstable-agent-babysitter/unstable-agent-babysitter-hook.ts b/src/hooks/unstable-agent-babysitter/unstable-agent-babysitter-hook.ts index 9bfdbb01..baf9d57c 100644 --- a/src/hooks/unstable-agent-babysitter/unstable-agent-babysitter-hook.ts +++ b/src/hooks/unstable-agent-babysitter/unstable-agent-babysitter-hook.ts @@ -1,5 +1,6 @@ import type { BackgroundManager } from "../../features/background-agent" import { getMainSessionID, getSessionAgent } from "../../features/claude-code-session-state" +import { normalizeAgentForPrompt } from "../../shared/agent-display-names" import { log } from "../../shared/logger" import { createInternalAgentTextPart, resolveInheritedPromptTools } from "../../shared" import { @@ -80,7 +81,7 @@ async function resolveMainSessionTarget( log(`[${HOOK_NAME}] Failed to resolve main session agent`, { sessionID, error: String(error) }) } - return { agent, model, tools: resolveInheritedPromptTools(sessionID, tools) } + return { agent: normalizeAgentForPrompt(agent), model, tools: resolveInheritedPromptTools(sessionID, tools) } } async function getThinkingSummary(ctx: BabysitterContext, sessionID: string): Promise { diff --git a/src/shared/agent-display-names.ts b/src/shared/agent-display-names.ts index 26ec999a..4704c54d 100644 --- a/src/shared/agent-display-names.ts +++ b/src/shared/agent-display-names.ts @@ -53,4 +53,32 @@ export function getAgentConfigKey(agentName: string): string { if (reversed !== undefined) return reversed if (AGENT_DISPLAY_NAMES[lower] !== undefined) return lower return lower -} \ No newline at end of file +} + +/** + * 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 = agentName.trim() + if (!trimmed) { + return undefined + } + + const lower = trimmed.toLowerCase() + const reversed = REVERSE_DISPLAY_NAMES[lower] + if (reversed !== undefined) { + return AGENT_DISPLAY_NAMES[reversed] ?? trimmed + } + if (AGENT_DISPLAY_NAMES[lower] !== undefined) { + return AGENT_DISPLAY_NAMES[lower] + } + + return trimmed +}