From 45b2782d55b49316bbd0cc58285552128c56dcc1 Mon Sep 17 00:00:00 2001 From: justsisyphus Date: Fri, 23 Jan 2026 00:55:01 +0900 Subject: [PATCH] refactor: use case-insensitive matching for agent names Apply case-insensitive utilities across agents, delegation, and tool restrictions. Removes duplicate implementations from agents/utils.ts. Agent validation now normalizes input to canonical names. --- src/agents/utils.ts | 18 +----------------- src/index.ts | 7 ++++--- src/shared/agent-tool-restrictions.ts | 6 ++++-- src/tools/call-omo-agent/tools.ts | 10 +++++++--- src/tools/delegate-task/tools.ts | 21 ++++++++++++++------- 5 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/agents/utils.ts b/src/agents/utils.ts index 333dbf430..dcb8d99be 100644 --- a/src/agents/utils.ts +++ b/src/agents/utils.ts @@ -10,7 +10,7 @@ import { createMetisAgent } from "./metis" import { createAtlasAgent } from "./atlas" import { createMomusAgent } from "./momus" import type { AvailableAgent, AvailableCategory, AvailableSkill } from "./dynamic-agent-prompt-builder" -import { deepMerge, fetchAvailableModels, resolveModelWithFallback, AGENT_MODEL_REQUIREMENTS } from "../shared" +import { deepMerge, fetchAvailableModels, resolveModelWithFallback, AGENT_MODEL_REQUIREMENTS, findCaseInsensitive, includesCaseInsensitive } from "../shared" import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "../tools/delegate-task/constants" import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content" import { createBuiltinSkills } from "../features/builtin-skills" @@ -46,22 +46,6 @@ function isFactory(source: AgentSource): source is AgentFactory { return typeof source === "function" } -function findCaseInsensitive(obj: Record | undefined, key: string): T | undefined { - if (!obj) return undefined - const exactMatch = obj[key] - if (exactMatch !== undefined) return exactMatch - const lowerKey = key.toLowerCase() - for (const [k, v] of Object.entries(obj)) { - if (k.toLowerCase() === lowerKey) return v - } - return undefined -} - -function includesCaseInsensitive(arr: string[], value: string): boolean { - const lowerValue = value.toLowerCase() - return arr.some((item) => item.toLowerCase() === lowerValue) -} - export function buildAgent( source: AgentSource, model: string, diff --git a/src/index.ts b/src/index.ts index 31b0daf49..7df6e557c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -73,7 +73,7 @@ import { BackgroundManager } from "./features/background-agent"; import { SkillMcpManager } from "./features/skill-mcp-manager"; import { initTaskToastManager } from "./features/task-toast-manager"; import { type HookName } from "./config"; -import { log, detectExternalNotificationPlugin, getNotificationConflictWarning, resetMessageCursor } from "./shared"; +import { log, detectExternalNotificationPlugin, getNotificationConflictWarning, resetMessageCursor, includesCaseInsensitive } from "./shared"; import { loadPluginConfig } from "./plugin-config"; import { createModelCacheState, getModelLimit } from "./plugin-state"; import { createConfigHandler } from "./plugin-handlers"; @@ -489,8 +489,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { if (input.tool === "task") { const args = output.args as Record; const subagentType = args.subagent_type as string; - const isExploreOrLibrarian = ["explore", "librarian"].includes( - subagentType + const isExploreOrLibrarian = includesCaseInsensitive( + ["explore", "librarian"], + subagentType ?? "" ); args.tools = { diff --git a/src/shared/agent-tool-restrictions.ts b/src/shared/agent-tool-restrictions.ts index 04ee3d955..9d23ab492 100644 --- a/src/shared/agent-tool-restrictions.ts +++ b/src/shared/agent-tool-restrictions.ts @@ -4,6 +4,8 @@ * true = tool allowed, false = tool denied. */ +import { findCaseInsensitive } from "./case-insensitive" + const EXPLORATION_AGENT_DENYLIST: Record = { write: false, edit: false, @@ -35,10 +37,10 @@ const AGENT_RESTRICTIONS: Record> = { } export function getAgentToolRestrictions(agentName: string): Record { - return AGENT_RESTRICTIONS[agentName] ?? {} + return findCaseInsensitive(AGENT_RESTRICTIONS, agentName) ?? {} } export function hasAgentToolRestrictions(agentName: string): boolean { - const restrictions = AGENT_RESTRICTIONS[agentName] + const restrictions = findCaseInsensitive(AGENT_RESTRICTIONS, agentName) return restrictions !== undefined && Object.keys(restrictions).length > 0 } diff --git a/src/tools/call-omo-agent/tools.ts b/src/tools/call-omo-agent/tools.ts index 94c3d6bd6..373e27709 100644 --- a/src/tools/call-omo-agent/tools.ts +++ b/src/tools/call-omo-agent/tools.ts @@ -4,7 +4,7 @@ import { join } from "node:path" import { ALLOWED_AGENTS, CALL_OMO_AGENT_DESCRIPTION } from "./constants" import type { CallOmoAgentArgs } from "./types" import type { BackgroundManager } from "../../features/background-agent" -import { log, getAgentToolRestrictions } from "../../shared" +import { log, getAgentToolRestrictions, includesCaseInsensitive } from "../../shared" import { consumeNewMessages } from "../../shared/session-cursor" import { findFirstMessageWithAgent, findNearestMessageWithFields, MESSAGE_STORAGE } from "../../features/hook-message-injector" import { getSessionAgent } from "../../features/claude-code-session-state" @@ -46,7 +46,7 @@ export function createCallOmoAgent( description: tool.schema.string().describe("A short (3-5 words) description of the task"), prompt: tool.schema.string().describe("The task for the agent to perform"), subagent_type: tool.schema - .enum(ALLOWED_AGENTS) + .string() .describe("The type of specialized agent to use for this task (explore or librarian only)"), run_in_background: tool.schema .boolean() @@ -57,9 +57,13 @@ export function createCallOmoAgent( const toolCtx = toolContext as ToolContextWithMetadata log(`[call_omo_agent] Starting with agent: ${args.subagent_type}, background: ${args.run_in_background}`) - if (!ALLOWED_AGENTS.includes(args.subagent_type as typeof ALLOWED_AGENTS[number])) { + // Case-insensitive agent validation - allows "Explore", "EXPLORE", "explore" etc. + if (!includesCaseInsensitive([...ALLOWED_AGENTS], args.subagent_type)) { return `Error: Invalid agent type "${args.subagent_type}". Only ${ALLOWED_AGENTS.join(", ")} are allowed.` } + + const normalizedAgent = args.subagent_type.toLowerCase() as typeof ALLOWED_AGENTS[number] + args = { ...args, subagent_type: normalizedAgent } if (args.run_in_background) { if (args.session_id) { diff --git a/src/tools/delegate-task/tools.ts b/src/tools/delegate-task/tools.ts index 2bbf495d0..5f5cec400 100644 --- a/src/tools/delegate-task/tools.ts +++ b/src/tools/delegate-task/tools.ts @@ -11,7 +11,7 @@ import { discoverSkills } from "../../features/opencode-skill-loader" import { getTaskToastManager } from "../../features/task-toast-manager" import type { ModelFallbackInfo } from "../../features/task-toast-manager/types" import { subagentSessions, getSessionAgent } from "../../features/claude-code-session-state" -import { log, getAgentToolRestrictions, resolveModel, getOpenCodeConfigPaths } from "../../shared" +import { log, getAgentToolRestrictions, resolveModel, getOpenCodeConfigPaths, findByNameCaseInsensitive, equalsIgnoreCase } from "../../shared" import { fetchAvailableModels } from "../../shared/model-availability" import { resolveModelWithFallback } from "../../shared/model-resolver" import { CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements" @@ -716,7 +716,7 @@ To resume this session: resume="${sessionID}"` } const agentName = args.subagent_type.trim() - if (agentName === SISYPHUS_JUNIOR_AGENT) { + if (equalsIgnoreCase(agentName, SISYPHUS_JUNIOR_AGENT)) { return `Cannot use subagent_type="${SISYPHUS_JUNIOR_AGENT}" directly. Use category parameter instead (e.g., ${categoryExamples}). Sisyphus-Junior is spawned automatically when you specify a category. Pick the appropriate category for your task domain.` @@ -725,25 +725,32 @@ Sisyphus-Junior is spawned automatically when you specify a category. Pick the a agentToUse = agentName // Validate agent exists and is callable (not a primary agent) + // Uses case-insensitive matching to allow "Oracle", "oracle", "ORACLE" etc. try { const agentsResult = await client.app.agents() type AgentInfo = { name: string; mode?: "subagent" | "primary" | "all" } const agents = (agentsResult as { data?: AgentInfo[] }).data ?? agentsResult as unknown as AgentInfo[] const callableAgents = agents.filter((a) => a.mode !== "primary") - const callableNames = callableAgents.map((a) => a.name) - if (!callableNames.includes(agentToUse)) { - const isPrimaryAgent = agents.some((a) => a.name === agentToUse && a.mode === "primary") + const matchedAgent = findByNameCaseInsensitive(callableAgents, agentToUse) + if (!matchedAgent) { + const isPrimaryAgent = findByNameCaseInsensitive( + agents.filter((a) => a.mode === "primary"), + agentToUse + ) if (isPrimaryAgent) { - return `Cannot call primary agent "${agentToUse}" via delegate_task. Primary agents are top-level orchestrators.` + return `Cannot call primary agent "${isPrimaryAgent.name}" via delegate_task. Primary agents are top-level orchestrators.` } - const availableAgents = callableNames + const availableAgents = callableAgents + .map((a) => a.name) .sort() .join(", ") return `Unknown agent: "${agentToUse}". Available agents: ${availableAgents}` } + // Use the canonical agent name from registration + agentToUse = matchedAgent.name } catch { // If we can't fetch agents, proceed anyway - the session.prompt will fail with a clearer error }