fix(prompts): normalize agent names for continuation injections

This commit is contained in:
ismeth 2026-02-18 19:26:46 +01:00 committed by YeonGyu-Kim
parent 21dc48e159
commit 8381ea076a
8 changed files with 47 additions and 9 deletions

View File

@ -855,7 +855,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
.notifyParentSession(task) .notifyParentSession(task)
//#then //#then
expect(capturedBody?.agent).toBe("sisyphus") expect(capturedBody?.agent).toBe("Sisyphus (Ultraworker)")
expect(capturedBody?.model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6" }) expect(capturedBody?.model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6" })
manager.shutdown() manager.shutdown()

View File

@ -15,6 +15,7 @@ import {
resolveInheritedPromptTools, resolveInheritedPromptTools,
createInternalAgentTextPart, createInternalAgentTextPart,
} from "../../shared" } from "../../shared"
import { normalizeAgentForPrompt } from "../../shared/agent-display-names"
import { setSessionTools } from "../../shared/session-tools-store" import { setSessionTools } from "../../shared/session-tools-store"
import { SessionCategoryRegistry } from "../../shared/session-category-registry" import { SessionCategoryRegistry } from "../../shared/session-category-registry"
import { ConcurrencyManager } from "./concurrency" 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 resolvedTools = resolveInheritedPromptTools(task.parentSessionID, tools)
const promptAgent = normalizeAgentForPrompt(agent)
log("[background-agent] notifyParentSession context:", { log("[background-agent] notifyParentSession context:", {
taskId: task.id, taskId: task.id,
resolvedAgent: agent, resolvedAgent: promptAgent,
resolvedModel: model, resolvedModel: model,
}) })
@ -1323,7 +1325,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
path: { id: task.parentSessionID }, path: { id: task.parentSessionID },
body: { body: {
noReply: !allComplete, noReply: !allComplete,
...(agent !== undefined ? { agent } : {}), ...(promptAgent !== undefined ? { agent: promptAgent } : {}),
...(model !== undefined ? { model } : {}), ...(model !== undefined ? { model } : {}),
...(resolvedTools ? { tools: resolvedTools } : {}), ...(resolvedTools ? { tools: resolvedTools } : {}),
parts: [createInternalAgentTextPart(notification)], parts: [createInternalAgentTextPart(notification)],

View File

@ -1,5 +1,6 @@
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import type { BackgroundManager } from "../../features/background-agent" import type { BackgroundManager } from "../../features/background-agent"
import { normalizeAgentForPrompt } from "../../shared/agent-display-names"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { createInternalAgentTextPart, resolveInheritedPromptTools } from "../../shared" import { createInternalAgentTextPart, resolveInheritedPromptTools } from "../../shared"
import { HOOK_NAME } from "./hook-name" import { HOOK_NAME } from "./hook-name"
@ -40,6 +41,7 @@ export async function injectBoulderContinuation(input: {
const prompt = const prompt =
BOULDER_CONTINUATION_PROMPT.replace(/{PLAN_NAME}/g, planName) + BOULDER_CONTINUATION_PROMPT.replace(/{PLAN_NAME}/g, planName) +
`\n\n[Status: ${total - remaining}/${total} completed, ${remaining} remaining]` `\n\n[Status: ${total - remaining}/${total} completed, ${remaining} remaining]`
const promptAgent = normalizeAgentForPrompt(agent ?? "atlas") ?? "atlas"
try { try {
log(`[${HOOK_NAME}] Injecting boulder continuation`, { sessionID, planName, remaining }) log(`[${HOOK_NAME}] Injecting boulder continuation`, { sessionID, planName, remaining })
@ -50,7 +52,7 @@ export async function injectBoulderContinuation(input: {
await ctx.client.session.promptAsync({ await ctx.client.session.promptAsync({
path: { id: sessionID }, path: { id: sessionID },
body: { body: {
agent: agent ?? "atlas", agent: promptAgent,
...(promptContext.model !== undefined ? { model: promptContext.model } : {}), ...(promptContext.model !== undefined ? { model: promptContext.model } : {}),
...(inheritedTools ? { tools: inheritedTools } : {}), ...(inheritedTools ? { tools: inheritedTools } : {}),
parts: [createInternalAgentTextPart(prompt)], parts: [createInternalAgentTextPart(prompt)],

View File

@ -997,7 +997,7 @@ describe("atlas hook", () => {
// then - should call prompt for sisyphus // then - should call prompt for sisyphus
expect(mockInput._promptMock).toHaveBeenCalled() expect(mockInput._promptMock).toHaveBeenCalled()
const callArgs = mockInput._promptMock.mock.calls[0][0] 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 () => { test("should debounce rapid continuation injections (prevent infinite loop)", async () => {

View File

@ -8,6 +8,7 @@ import {
normalizeSDKResponse, normalizeSDKResponse,
resolveInheritedPromptTools, resolveInheritedPromptTools,
} from "../../shared" } from "../../shared"
import { normalizeAgentForPrompt } from "../../shared/agent-display-names"
type MessageInfo = { type MessageInfo = {
agent?: string agent?: string
@ -68,11 +69,12 @@ export async function injectContinuationPrompt(
} }
const inheritedTools = resolveInheritedPromptTools(sourceSessionID, tools) const inheritedTools = resolveInheritedPromptTools(sourceSessionID, tools)
const promptAgent = normalizeAgentForPrompt(agent)
await ctx.client.session.promptAsync({ await ctx.client.session.promptAsync({
path: { id: options.sessionID }, path: { id: options.sessionID },
body: { body: {
...(agent !== undefined ? { agent } : {}), ...(promptAgent ? { agent: promptAgent } : {}),
...(model !== undefined ? { model } : {}), ...(model !== undefined ? { model } : {}),
...(inheritedTools ? { tools: inheritedTools } : {}), ...(inheritedTools ? { tools: inheritedTools } : {}),
parts: [createInternalAgentTextPart(options.prompt)], parts: [createInternalAgentTextPart(options.prompt)],

View File

@ -1,4 +1,5 @@
import type { createOpencodeClient } from "@opencode-ai/sdk" import type { createOpencodeClient } from "@opencode-ai/sdk"
import { normalizeAgentForPrompt } from "../../shared/agent-display-names"
import type { MessageData, ResumeConfig } from "./types" import type { MessageData, ResumeConfig } from "./types"
import { createInternalAgentTextPart, resolveInheritedPromptTools } from "../../shared" 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<boolean> { export async function resumeSession(client: Client, config: ResumeConfig): Promise<boolean> {
const promptAgent = normalizeAgentForPrompt(config.agent)
try { try {
const inheritedTools = resolveInheritedPromptTools(config.sessionID, config.tools) const inheritedTools = resolveInheritedPromptTools(config.sessionID, config.tools)
await client.session.promptAsync({ await client.session.promptAsync({
path: { id: config.sessionID }, path: { id: config.sessionID },
body: { body: {
parts: [createInternalAgentTextPart(RECOVERY_RESUME_TEXT)], parts: [createInternalAgentTextPart(RECOVERY_RESUME_TEXT)],
agent: config.agent, ...(promptAgent ? { agent: promptAgent } : {}),
model: config.model, model: config.model,
...(inheritedTools ? { tools: inheritedTools } : {}), ...(inheritedTools ? { tools: inheritedTools } : {}),
}, },

View File

@ -1,5 +1,6 @@
import type { BackgroundManager } from "../../features/background-agent" import type { BackgroundManager } from "../../features/background-agent"
import { getMainSessionID, getSessionAgent } from "../../features/claude-code-session-state" import { getMainSessionID, getSessionAgent } from "../../features/claude-code-session-state"
import { normalizeAgentForPrompt } from "../../shared/agent-display-names"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { createInternalAgentTextPart, resolveInheritedPromptTools } from "../../shared" import { createInternalAgentTextPart, resolveInheritedPromptTools } from "../../shared"
import { import {
@ -80,7 +81,7 @@ async function resolveMainSessionTarget(
log(`[${HOOK_NAME}] Failed to resolve main session agent`, { sessionID, error: String(error) }) 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<string | null> { async function getThinkingSummary(ctx: BabysitterContext, sessionID: string): Promise<string | null> {

View File

@ -54,3 +54,31 @@ export function getAgentConfigKey(agentName: string): string {
if (AGENT_DISPLAY_NAMES[lower] !== undefined) return lower if (AGENT_DISPLAY_NAMES[lower] !== undefined) return lower
return lower return 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 = 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
}