Normalize agent name comparisons to handle display name keys

Hooks and tools now use getAgentConfigKey() to resolve agent names (which may
be display names like 'Atlas (Plan Executor)') to lowercase config keys
before comparison.

- session-utils: orchestrator check uses getAgentConfigKey
- atlas event-handler: boulder agent matching uses config keys
- category-skill-reminder: target agent check uses config keys
- todo-continuation-enforcer: skipAgents comparison normalized
- subagent-resolver: resolves 'metis' -> 'Metis (Plan Consultant)' for lookup
This commit is contained in:
YeonGyu-Kim 2026-02-16 20:43:09 +09:00
parent d94a739203
commit 560d13dc70
6 changed files with 28 additions and 17 deletions

View File

@ -2,6 +2,7 @@ import type { PluginInput } from "@opencode-ai/plugin"
import { getPlanProgress, readBoulderState } from "../../features/boulder-state" import { getPlanProgress, readBoulderState } from "../../features/boulder-state"
import { subagentSessions } from "../../features/claude-code-session-state" import { subagentSessions } from "../../features/claude-code-session-state"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { getAgentConfigKey } from "../../shared/agent-display-names"
import { HOOK_NAME } from "./hook-name" import { HOOK_NAME } from "./hook-name"
import { isAbortError } from "./is-abort-error" import { isAbortError } from "./is-abort-error"
import { injectBoulderContinuation } from "./boulder-continuation-injector" import { injectBoulderContinuation } from "./boulder-continuation-injector"
@ -88,11 +89,12 @@ export function createAtlasEventHandler(input: {
} }
const lastAgent = await getLastAgentFromSession(sessionID, ctx.client) const lastAgent = await getLastAgentFromSession(sessionID, ctx.client)
const requiredAgent = (boulderState.agent ?? "atlas").toLowerCase() const lastAgentKey = getAgentConfigKey(lastAgent ?? "")
const lastAgentMatchesRequired = lastAgent === requiredAgent const requiredAgent = getAgentConfigKey(boulderState.agent ?? "atlas")
const lastAgentMatchesRequired = lastAgentKey === requiredAgent
const boulderAgentWasNotExplicitlySet = boulderState.agent === undefined const boulderAgentWasNotExplicitlySet = boulderState.agent === undefined
const boulderAgentDefaultsToAtlas = requiredAgent === "atlas" const boulderAgentDefaultsToAtlas = requiredAgent === "atlas"
const lastAgentIsSisyphus = lastAgent === "sisyphus" const lastAgentIsSisyphus = lastAgentKey === "sisyphus"
const allowSisyphusWhenDefaultAtlas = boulderAgentWasNotExplicitlySet && boulderAgentDefaultsToAtlas && lastAgentIsSisyphus const allowSisyphusWhenDefaultAtlas = boulderAgentWasNotExplicitlySet && boulderAgentDefaultsToAtlas && lastAgentIsSisyphus
const agentMatches = lastAgentMatchesRequired || allowSisyphusWhenDefaultAtlas const agentMatches = lastAgentMatchesRequired || allowSisyphusWhenDefaultAtlas
if (!agentMatches) { if (!agentMatches) {

View File

@ -2,6 +2,7 @@ import type { PluginInput } from "@opencode-ai/plugin"
import type { AvailableSkill } from "../../agents/dynamic-agent-prompt-builder" import type { AvailableSkill } from "../../agents/dynamic-agent-prompt-builder"
import { getSessionAgent } from "../../features/claude-code-session-state" import { getSessionAgent } from "../../features/claude-code-session-state"
import { log } from "../../shared" import { log } from "../../shared"
import { getAgentConfigKey } from "../../shared/agent-display-names"
import { buildReminderMessage } from "./formatter" import { buildReminderMessage } from "./formatter"
/** /**
@ -75,11 +76,11 @@ export function createCategorySkillReminderHook(
function isTargetAgent(sessionID: string, inputAgent?: string): boolean { function isTargetAgent(sessionID: string, inputAgent?: string): boolean {
const agent = getSessionAgent(sessionID) ?? inputAgent const agent = getSessionAgent(sessionID) ?? inputAgent
if (!agent) return false if (!agent) return false
const agentLower = agent.toLowerCase() const agentKey = getAgentConfigKey(agent)
return ( return (
TARGET_AGENTS.has(agentLower) || TARGET_AGENTS.has(agentKey) ||
agentLower.includes("sisyphus") || agentKey.includes("sisyphus") ||
agentLower.includes("atlas") agentKey.includes("atlas")
) )
} }

View File

@ -9,6 +9,7 @@ import {
} from "../../features/hook-message-injector" } from "../../features/hook-message-injector"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { isSqliteBackend } from "../../shared/opencode-storage-detection" import { isSqliteBackend } from "../../shared/opencode-storage-detection"
import { getAgentConfigKey } from "../../shared/agent-display-names"
import { import {
CONTINUATION_PROMPT, CONTINUATION_PROMPT,
@ -103,7 +104,7 @@ export async function injectContinuation(args: {
tools = tools ?? previousMessage?.tools tools = tools ?? previousMessage?.tools
} }
if (agentName && skipAgents.includes(agentName)) { if (agentName && skipAgents.some(s => getAgentConfigKey(s) === getAgentConfigKey(agentName))) {
log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: agentName }) log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: agentName })
return return
} }

View File

@ -4,6 +4,7 @@ import type { BackgroundManager } from "../../features/background-agent"
import type { ToolPermission } from "../../features/hook-message-injector" import type { ToolPermission } from "../../features/hook-message-injector"
import { normalizeSDKResponse } from "../../shared" import { normalizeSDKResponse } from "../../shared"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { getAgentConfigKey } from "../../shared/agent-display-names"
import { import {
ABORT_WINDOW_MS, ABORT_WINDOW_MS,
@ -162,8 +163,9 @@ export async function handleSessionIdle(args: {
log(`[${HOOK_NAME}] Agent check`, { sessionID, agentName: resolvedInfo?.agent, skipAgents, hasCompactionMessage }) log(`[${HOOK_NAME}] Agent check`, { sessionID, agentName: resolvedInfo?.agent, skipAgents, hasCompactionMessage })
if (resolvedInfo?.agent && skipAgents.includes(resolvedInfo.agent)) { const resolvedAgentName = resolvedInfo?.agent
log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: resolvedInfo.agent }) if (resolvedAgentName && skipAgents.some(s => getAgentConfigKey(s) === getAgentConfigKey(resolvedAgentName))) {
log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: resolvedAgentName })
return return
} }
if (hasCompactionMessage && !resolvedInfo?.agent) { if (hasCompactionMessage && !resolvedInfo?.agent) {

View File

@ -2,6 +2,7 @@ import { findNearestMessageWithFields, findNearestMessageWithFieldsFromSDK } fro
import { getMessageDir } from "./opencode-message-dir" import { getMessageDir } from "./opencode-message-dir"
import { isSqliteBackend } from "./opencode-storage-detection" import { isSqliteBackend } from "./opencode-storage-detection"
import { log } from "./logger" import { log } from "./logger"
import { getAgentConfigKey } from "./agent-display-names"
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
export async function isCallerOrchestrator(sessionID?: string, client?: PluginInput["client"]): Promise<boolean> { export async function isCallerOrchestrator(sessionID?: string, client?: PluginInput["client"]): Promise<boolean> {
@ -10,7 +11,7 @@ export async function isCallerOrchestrator(sessionID?: string, client?: PluginIn
if (isSqliteBackend() && client) { if (isSqliteBackend() && client) {
try { try {
const nearest = await findNearestMessageWithFieldsFromSDK(client, sessionID) const nearest = await findNearestMessageWithFieldsFromSDK(client, sessionID)
return nearest?.agent?.toLowerCase() === "atlas" return getAgentConfigKey(nearest?.agent ?? "") === "atlas"
} catch (error) { } catch (error) {
log("[session-utils] SDK orchestrator check failed", { sessionID, error: String(error) }) log("[session-utils] SDK orchestrator check failed", { sessionID, error: String(error) })
return false return false
@ -20,5 +21,5 @@ export async function isCallerOrchestrator(sessionID?: string, client?: PluginIn
const messageDir = getMessageDir(sessionID) const messageDir = getMessageDir(sessionID)
if (!messageDir) return false if (!messageDir) return false
const nearest = findNearestMessageWithFields(messageDir) const nearest = findNearestMessageWithFields(messageDir)
return nearest?.agent?.toLowerCase() === "atlas" return getAgentConfigKey(nearest?.agent ?? "") === "atlas"
} }

View File

@ -4,6 +4,7 @@ import { isPlanFamily } from "./constants"
import { SISYPHUS_JUNIOR_AGENT } from "./sisyphus-junior-agent" import { SISYPHUS_JUNIOR_AGENT } from "./sisyphus-junior-agent"
import { parseModelString } from "./model-string-parser" import { parseModelString } from "./model-string-parser"
import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements" import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
import { getAgentDisplayName, getAgentConfigKey } from "../../shared/agent-display-names"
import { normalizeSDKResponse } from "../../shared" import { normalizeSDKResponse } from "../../shared"
import { getAvailableModelsForDelegateTask } from "./available-models" import { getAvailableModelsForDelegateTask } from "./available-models"
import { resolveModelForDelegateTask } from "./model-selection" import { resolveModelForDelegateTask } from "./model-selection"
@ -54,13 +55,16 @@ Create the work plan directly - that's your job as the planning agent.`,
const callableAgents = agents.filter((a) => a.mode !== "primary") const callableAgents = agents.filter((a) => a.mode !== "primary")
const resolvedDisplayName = getAgentDisplayName(agentToUse)
const matchedAgent = callableAgents.find( const matchedAgent = callableAgents.find(
(agent) => agent.name.toLowerCase() === agentToUse.toLowerCase() (agent) => agent.name.toLowerCase() === agentToUse.toLowerCase()
|| agent.name.toLowerCase() === resolvedDisplayName.toLowerCase()
) )
if (!matchedAgent) { if (!matchedAgent) {
const isPrimaryAgent = agents const isPrimaryAgent = agents
.filter((a) => a.mode === "primary") .filter((a) => a.mode === "primary")
.find((agent) => agent.name.toLowerCase() === agentToUse.toLowerCase()) .find((agent) => agent.name.toLowerCase() === agentToUse.toLowerCase()
|| agent.name.toLowerCase() === resolvedDisplayName.toLowerCase())
if (isPrimaryAgent) { if (isPrimaryAgent) {
return { return {
@ -83,10 +87,10 @@ Create the work plan directly - that's your job as the planning agent.`,
agentToUse = matchedAgent.name agentToUse = matchedAgent.name
const agentNameLower = agentToUse.toLowerCase() const agentConfigKey = getAgentConfigKey(agentToUse)
const agentOverride = agentOverrides?.[agentNameLower as keyof typeof agentOverrides] const agentOverride = agentOverrides?.[agentConfigKey as keyof typeof agentOverrides]
?? (agentOverrides ? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentNameLower)?.[1] : undefined) ?? (agentOverrides ? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentConfigKey)?.[1] : undefined)
const agentRequirement = AGENT_MODEL_REQUIREMENTS[agentNameLower] const agentRequirement = AGENT_MODEL_REQUIREMENTS[agentConfigKey]
if (agentOverride?.model || agentRequirement || matchedAgent.model) { if (agentOverride?.model || agentRequirement || matchedAgent.model) {
const availableModels = await getAvailableModelsForDelegateTask(client) const availableModels = await getAvailableModelsForDelegateTask(client)