From f035be842d40202299fbde2a0dae9cd9ec09baa9 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 8 Feb 2026 13:34:47 +0900 Subject: [PATCH 1/2] fix(agents): include custom agents in orchestrator delegation prompt (#1623) --- bun.lock | 28 ++--- src/agents/dynamic-agent-prompt-builder.ts | 4 +- src/agents/utils.test.ts | 50 +++++++++ src/agents/utils.ts | 105 ++++++++++++++++-- .../unstable-agent-babysitter/index.test.ts | 3 + 5 files changed, 165 insertions(+), 25 deletions(-) diff --git a/bun.lock b/bun.lock index 7c5f969e..4a416c88 100644 --- a/bun.lock +++ b/bun.lock @@ -28,13 +28,13 @@ "typescript": "^5.7.3", }, "optionalDependencies": { - "oh-my-opencode-darwin-arm64": "3.3.0", - "oh-my-opencode-darwin-x64": "3.3.0", - "oh-my-opencode-linux-arm64": "3.3.0", - "oh-my-opencode-linux-arm64-musl": "3.3.0", - "oh-my-opencode-linux-x64": "3.3.0", - "oh-my-opencode-linux-x64-musl": "3.3.0", - "oh-my-opencode-windows-x64": "3.3.0", + "oh-my-opencode-darwin-arm64": "3.3.1", + "oh-my-opencode-darwin-x64": "3.3.1", + "oh-my-opencode-linux-arm64": "3.3.1", + "oh-my-opencode-linux-arm64-musl": "3.3.1", + "oh-my-opencode-linux-x64": "3.3.1", + "oh-my-opencode-linux-x64-musl": "3.3.1", + "oh-my-opencode-windows-x64": "3.3.1", }, }, }, @@ -226,19 +226,19 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.3.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-P2kZKJqZaA4j0qtGM3I8+ZeH204ai27ni/OXLjtFdOewRjJgrahxaC1XslgK7q/KU9fXz6BQfEqAjbvyPf/rgQ=="], + "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.3.1", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-R+o42Km6bsIaW6D3I8uu2HCF3BjIWqa/fg38W5y4hJEOw4mL0Q7uV4R+0vtrXRHo9crXTK9ag0fqVQUm+Y6iAQ=="], - "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.3.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-RopOorbW1WyhMQJ+ipuqiOA1GICS+3IkOwNyEe0KZlCLpoEDTyFopIL87HSns+gEQPMxnknroDp8lzxn1AKgjw=="], + "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.3.1", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-7VTbpR1vH3OEkoJxBKtYuxFPX8M3IbJKoeHWME9iK6FpT11W1ASsjyuhvzB1jcxSeqF8ddMnjitlG5ub6h5EVw=="], - "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.3.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-297iEfuK+05g+q64crPW78Zbgm/j5PGjDDweSPkZ6rI6SEfHMvOIkGxMvN8gugM3zcH8FOCQXoY2nC8b6x3pwQ=="], + "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.3.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-BZ/r/CFlvbOxkdZZrRoT16xFOjibRZHuwQnaE4f0JvOzgK6/HWp3zJI1+2/aX/oK5GA6lZxNWRrJC/SKUi8LEg=="], - "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.3.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-oVxP0+yn66HQYfrl9QT6I7TumRzciuPB4z24+PwKEVcDjPbWXQqLY1gwOGHZAQBPLf0vwewv9ybEDVD42RRH4g=="], + "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.3.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-U90Wruf21h+CJbtcrS7MeTAc/5VOF6RI+5jr7qj/cCxjXNJtjhyJdz/maehArjtgf304+lYCM/Mh1i+G2D3YFQ=="], - "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.3.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-k9LoLkisLJwJNR1J0Bh1bjGtGBkl5D9WzFPSdZCAlyiT6TgG9w5erPTlXqtl2Lt0We5tYUVYlkEIHRMK/ugNsQ=="], + "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.3.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-sYzohSNdwsAhivbXcbhPdF1qqQi2CCI7FSgbmvvfBOMyZ8HAgqOFqYW2r3GPdmtywzkjOTvCzTG56FZwEjx15w=="], - "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.3.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-7asXCeae7wBxJrzoZ7J6Yo1oaOxwUN3bTO7jWurCTMs5TDHO+pEHysgv/nuF1jvj1T+r1vg1H5ZmopuKy1qvXg=="], + "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.3.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-aG5pZ4eWS0YSGUicOnjMkUPrIqQV4poYF+d9SIvrfvlaMcK6WlQn7jXzgNCwJsfGn5lyhSmjshZBEU+v79Ua3w=="], - "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.3.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-ABvwfaXb2xdrpbivzlPPJzIm5vXp+QlVakkaHEQf3TU6Mi/+fehH6Qhq/KMh66FDO2gq3xmxbH7nktHRQp9kNA=="], + "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.3.1", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-FGH7cnzBqNwjSkzCDglMsVttaq+MsykAxa7ehaFK+0dnBZArvllS3W13a3dGaANHMZzfK0vz8hNDUdVi7Z63cA=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], diff --git a/src/agents/dynamic-agent-prompt-builder.ts b/src/agents/dynamic-agent-prompt-builder.ts index c70da062..defaeecb 100644 --- a/src/agents/dynamic-agent-prompt-builder.ts +++ b/src/agents/dynamic-agent-prompt-builder.ts @@ -1,8 +1,8 @@ -import type { AgentPromptMetadata, BuiltinAgentName } from "./types" +import type { AgentPromptMetadata } from "./types" import { truncateDescription } from "../shared/truncate-description" export interface AvailableAgent { - name: BuiltinAgentName + name: string description: string metadata: AgentPromptMetadata } diff --git a/src/agents/utils.test.ts b/src/agents/utils.test.ts index 88883feb..a101840f 100644 --- a/src/agents/utils.test.ts +++ b/src/agents/utils.test.ts @@ -249,6 +249,56 @@ describe("createBuiltinAgents with model overrides", () => { expect(agents.sisyphus.prompt).toContain("frontend-ui-ux") expect(agents.sisyphus.prompt).toContain("git-master") }) + + test("includes custom agents from OpenCode registry in orchestrator prompts", async () => { + // #given + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( + new Set([ + "anthropic/claude-opus-4-6", + "kimi-for-coding/k2p5", + "opencode/kimi-k2.5-free", + "zai-coding-plan/glm-4.7", + "opencode/glm-4.7-free", + "openai/gpt-5.2", + ]) + ) + + const client = { + agent: { + list: async () => ({ + data: [ + { + name: "researcher", + description: "Research agent for deep analysis", + mode: "subagent", + hidden: false, + }, + ], + }), + }, + } + + try { + // #when + const agents = await createBuiltinAgents( + [], + {}, + undefined, + TEST_DEFAULT_MODEL, + undefined, + undefined, + [], + client + ) + + // #then + expect(agents.sisyphus.prompt).toContain("researcher") + expect(agents.hephaestus.prompt).toContain("researcher") + expect(agents.atlas.prompt).toContain("researcher") + } finally { + fetchSpy.mockRestore() + } + }) }) describe("createBuiltinAgents without systemDefaultModel", () => { diff --git a/src/agents/utils.ts b/src/agents/utils.ts index 5aac0ebb..bdd95488 100644 --- a/src/agents/utils.ts +++ b/src/agents/utils.ts @@ -11,7 +11,18 @@ import { createAtlasAgent, atlasPromptMetadata } from "./atlas" import { createMomusAgent, momusPromptMetadata } from "./momus" import { createHephaestusAgent } from "./hephaestus" import type { AvailableAgent, AvailableCategory, AvailableSkill } from "./dynamic-agent-prompt-builder" -import { deepMerge, fetchAvailableModels, resolveModelPipeline, AGENT_MODEL_REQUIREMENTS, readConnectedProvidersCache, isModelAvailable, isAnyFallbackModelAvailable, isAnyProviderConnected, migrateAgentConfig } from "../shared" +import { + deepMerge, + fetchAvailableModels, + resolveModelPipeline, + AGENT_MODEL_REQUIREMENTS, + readConnectedProvidersCache, + isModelAvailable, + isAnyFallbackModelAvailable, + isAnyProviderConnected, + migrateAgentConfig, + truncateDescription, +} 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" @@ -52,6 +63,65 @@ function isFactory(source: AgentSource): source is AgentFactory { return typeof source === "function" } +type RegisteredAgentSummary = { + name: string + description: string +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null +} + +function parseRegisteredAgentSummaries(input: unknown): RegisteredAgentSummary[] { + if (!Array.isArray(input)) return [] + + const result: RegisteredAgentSummary[] = [] + for (const item of input) { + if (!isRecord(item)) continue + + const name = typeof item.name === "string" ? item.name : undefined + if (!name) continue + + const hidden = item.hidden + if (hidden === true) continue + + const description = typeof item.description === "string" ? item.description : "" + result.push({ name, description }) + } + + return result +} + +async function fetchRegisteredAgentsFromClient(client: unknown): Promise { + if (!isRecord(client)) return [] + const agentObj = client.agent + if (!isRecord(agentObj)) return [] + const listFn = agentObj.list + if (typeof listFn !== "function") return [] + + try { + const response = await listFn.call(agentObj) + if (!isRecord(response)) return [] + return parseRegisteredAgentSummaries(response.data) + } catch { + return [] + } +} + +function buildCustomAgentMetadata(agentName: string, description: string): AgentPromptMetadata { + const shortDescription = truncateDescription(description).trim() + return { + category: "specialist", + cost: "CHEAP", + triggers: [ + { + domain: `Custom agent: ${agentName}`, + trigger: shortDescription || "Use when this agent's description matches the task", + }, + ], + } +} + export function buildAgent( source: AgentSource, model: string, @@ -279,6 +349,10 @@ export async function createBuiltinAgents( const availableSkills: AvailableSkill[] = [...builtinAvailable, ...discoveredAvailable] + const registeredAgents = await fetchRegisteredAgentsFromClient(client) + const builtinAgentNames = new Set(Object.keys(agentSources).map((n) => n.toLowerCase())) + const disabledAgentNames = new Set(disabledAgents.map((n) => n.toLowerCase())) + // Collect general agents first (for availableAgents), but don't add to result yet const pendingAgentConfigs: Map = new Map() @@ -335,14 +409,27 @@ export async function createBuiltinAgents( // Store for later - will be added after sisyphus and hephaestus pendingAgentConfigs.set(name, config) - const metadata = agentMetadata[agentName] - if (metadata) { - availableAgents.push({ - name: agentName, - description: config.description ?? "", - metadata, - }) - } + const metadata = agentMetadata[agentName] + if (metadata) { + availableAgents.push({ + name: agentName, + description: config.description ?? "", + metadata, + }) + } + } + + for (const agent of registeredAgents) { + const lowerName = agent.name.toLowerCase() + if (builtinAgentNames.has(lowerName)) continue + if (disabledAgentNames.has(lowerName)) continue + if (availableAgents.some((a) => a.name.toLowerCase() === lowerName)) continue + + availableAgents.push({ + name: agent.name, + description: agent.description, + metadata: buildCustomAgentMetadata(agent.name, agent.description), + }) } const sisyphusOverride = agentOverrides["sisyphus"] diff --git a/src/hooks/unstable-agent-babysitter/index.test.ts b/src/hooks/unstable-agent-babysitter/index.test.ts index f9900e7d..9fc309ec 100644 --- a/src/hooks/unstable-agent-babysitter/index.test.ts +++ b/src/hooks/unstable-agent-babysitter/index.test.ts @@ -21,6 +21,9 @@ function createMockPluginInput(options: { prompt: async (input: unknown) => { promptCalls.push({ input }) }, + promptAsync: async (input: unknown) => { + promptCalls.push({ input }) + }, }, }, } From 321b319b586e5e274c8f22f5438b0c3bd7a70234 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 8 Feb 2026 15:34:47 +0900 Subject: [PATCH 2/2] fix(agents): use config data instead of client API to avoid init deadlock (#1623) --- src/agents/utils.test.ts | 196 ++++++++++++++++++++++++-- src/agents/utils.ts | 43 +++--- src/plugin-handlers/config-handler.ts | 82 +++++++---- 3 files changed, 257 insertions(+), 64 deletions(-) diff --git a/src/agents/utils.test.ts b/src/agents/utils.test.ts index a101840f..dfe9d972 100644 --- a/src/agents/utils.test.ts +++ b/src/agents/utils.test.ts @@ -250,7 +250,7 @@ describe("createBuiltinAgents with model overrides", () => { expect(agents.sisyphus.prompt).toContain("git-master") }) - test("includes custom agents from OpenCode registry in orchestrator prompts", async () => { + test("includes custom agents in orchestrator prompts when provided via config", async () => { // #given const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( new Set([ @@ -263,20 +263,13 @@ describe("createBuiltinAgents with model overrides", () => { ]) ) - const client = { - agent: { - list: async () => ({ - data: [ - { - name: "researcher", - description: "Research agent for deep analysis", - mode: "subagent", - hidden: false, - }, - ], - }), + const customAgentSummaries = [ + { + name: "researcher", + description: "Research agent for deep analysis", + hidden: false, }, - } + ] try { // #when @@ -288,7 +281,7 @@ describe("createBuiltinAgents with model overrides", () => { undefined, undefined, [], - client + customAgentSummaries ) // #then @@ -299,6 +292,179 @@ describe("createBuiltinAgents with model overrides", () => { fetchSpy.mockRestore() } }) + + test("excludes hidden custom agents from orchestrator prompts", async () => { + // #given + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( + new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"]) + ) + + const customAgentSummaries = [ + { + name: "hidden-agent", + description: "Should never show", + hidden: true, + }, + ] + + try { + // #when + const agents = await createBuiltinAgents( + [], + {}, + undefined, + TEST_DEFAULT_MODEL, + undefined, + undefined, + [], + customAgentSummaries + ) + + // #then + expect(agents.sisyphus.prompt).not.toContain("hidden-agent") + expect(agents.hephaestus.prompt).not.toContain("hidden-agent") + expect(agents.atlas.prompt).not.toContain("hidden-agent") + } finally { + fetchSpy.mockRestore() + } + }) + + test("excludes disabled custom agents from orchestrator prompts", async () => { + // #given + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( + new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"]) + ) + + const customAgentSummaries = [ + { + name: "disabled-agent", + description: "Should never show", + disabled: true, + }, + ] + + try { + // #when + const agents = await createBuiltinAgents( + [], + {}, + undefined, + TEST_DEFAULT_MODEL, + undefined, + undefined, + [], + customAgentSummaries + ) + + // #then + expect(agents.sisyphus.prompt).not.toContain("disabled-agent") + expect(agents.hephaestus.prompt).not.toContain("disabled-agent") + expect(agents.atlas.prompt).not.toContain("disabled-agent") + } finally { + fetchSpy.mockRestore() + } + }) + + test("excludes custom agents when disabledAgents contains their name (case-insensitive)", async () => { + // #given + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( + new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"]) + ) + + const disabledAgents = ["ReSeArChEr"] + const customAgentSummaries = [ + { + name: "researcher", + description: "Should never show", + }, + ] + + try { + // #when + const agents = await createBuiltinAgents( + disabledAgents, + {}, + undefined, + TEST_DEFAULT_MODEL, + undefined, + undefined, + [], + customAgentSummaries + ) + + // #then + expect(agents.sisyphus.prompt).not.toContain("researcher") + expect(agents.hephaestus.prompt).not.toContain("researcher") + expect(agents.atlas.prompt).not.toContain("researcher") + } finally { + fetchSpy.mockRestore() + } + }) + + test("deduplicates custom agents case-insensitively", async () => { + // #given + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( + new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"]) + ) + + const customAgentSummaries = [ + { name: "Researcher", description: "First" }, + { name: "researcher", description: "Second" }, + ] + + try { + // #when + const agents = await createBuiltinAgents( + [], + {}, + undefined, + TEST_DEFAULT_MODEL, + undefined, + undefined, + [], + customAgentSummaries + ) + + // #then + const matches = agents.sisyphus.prompt.match(/Custom agent: researcher/gi) ?? [] + expect(matches.length).toBe(1) + } finally { + fetchSpy.mockRestore() + } + }) + + test("sanitizes custom agent strings for markdown tables", async () => { + // #given + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( + new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"]) + ) + + const customAgentSummaries = [ + { + name: "table-agent", + description: "Line1\nAlpha | Beta", + }, + ] + + try { + // #when + const agents = await createBuiltinAgents( + [], + {}, + undefined, + TEST_DEFAULT_MODEL, + undefined, + undefined, + [], + customAgentSummaries + ) + + // #then + expect(agents.sisyphus.prompt).toContain("Line1 Alpha \\| Beta") + } finally { + fetchSpy.mockRestore() + } + }) }) describe("createBuiltinAgents without systemDefaultModel", () => { diff --git a/src/agents/utils.ts b/src/agents/utils.ts index bdd95488..55d6187b 100644 --- a/src/agents/utils.ts +++ b/src/agents/utils.ts @@ -68,6 +68,14 @@ type RegisteredAgentSummary = { description: string } +function sanitizeMarkdownTableCell(value: string): string { + return value + .replace(/\r?\n/g, " ") + .replace(/\|/g, "\\|") + .replace(/\s+/g, " ") + .trim() +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null } @@ -85,37 +93,28 @@ function parseRegisteredAgentSummaries(input: unknown): RegisteredAgentSummary[] const hidden = item.hidden if (hidden === true) continue + const disabled = item.disabled + if (disabled === true) continue + + const enabled = item.enabled + if (enabled === false) continue + const description = typeof item.description === "string" ? item.description : "" - result.push({ name, description }) + result.push({ name, description: sanitizeMarkdownTableCell(description) }) } return result } -async function fetchRegisteredAgentsFromClient(client: unknown): Promise { - if (!isRecord(client)) return [] - const agentObj = client.agent - if (!isRecord(agentObj)) return [] - const listFn = agentObj.list - if (typeof listFn !== "function") return [] - - try { - const response = await listFn.call(agentObj) - if (!isRecord(response)) return [] - return parseRegisteredAgentSummaries(response.data) - } catch { - return [] - } -} - function buildCustomAgentMetadata(agentName: string, description: string): AgentPromptMetadata { - const shortDescription = truncateDescription(description).trim() + const shortDescription = sanitizeMarkdownTableCell(truncateDescription(description)) + const safeAgentName = sanitizeMarkdownTableCell(agentName) return { category: "specialist", cost: "CHEAP", triggers: [ { - domain: `Custom agent: ${agentName}`, + domain: `Custom agent: ${safeAgentName}`, trigger: shortDescription || "Use when this agent's description matches the task", }, ], @@ -303,13 +302,13 @@ export async function createBuiltinAgents( categories?: CategoriesConfig, gitMasterConfig?: GitMasterConfig, discoveredSkills: LoadedSkill[] = [], - client?: any, + customAgentSummaries?: unknown, browserProvider?: BrowserAutomationProvider, uiSelectedModel?: string, disabledSkills?: Set ): Promise> { const connectedProviders = readConnectedProvidersCache() - // IMPORTANT: Do NOT pass client to fetchAvailableModels during plugin initialization. + // IMPORTANT: Do NOT call OpenCode client APIs during plugin initialization. // This function is called from config handler, and calling client API causes deadlock. // See: https://github.com/code-yeongyu/oh-my-opencode/issues/1301 const availableModels = await fetchAvailableModels(undefined, { @@ -349,7 +348,7 @@ export async function createBuiltinAgents( const availableSkills: AvailableSkill[] = [...builtinAvailable, ...discoveredAvailable] - const registeredAgents = await fetchRegisteredAgentsFromClient(client) + const registeredAgents = parseRegisteredAgentSummaries(customAgentSummaries) const builtinAgentNames = new Set(Object.keys(agentSources).map((n) => n.toLowerCase())) const disabledAgentNames = new Set(disabledAgents.map((n) => n.toLowerCase())) diff --git a/src/plugin-handlers/config-handler.ts b/src/plugin-handlers/config-handler.ts index 41adbaf2..1c5c7cd3 100644 --- a/src/plugin-handlers/config-handler.ts +++ b/src/plugin-handlers/config-handler.ts @@ -183,19 +183,40 @@ export function createConfigHandler(deps: ConfigHandlerDeps) { // Pass it as uiSelectedModel so it takes highest priority in model resolution const currentModel = config.model as string | undefined; const disabledSkills = new Set(pluginConfig.disabled_skills ?? []); - const builtinAgents = await createBuiltinAgents( - migratedDisabledAgents, - pluginConfig.agents, - ctx.directory, - undefined, // systemDefaultModel - let fallback chain handle this - pluginConfig.categories, - pluginConfig.git_master, - allDiscoveredSkills, - ctx.client, - browserProvider, - currentModel, // uiSelectedModel - takes highest priority - disabledSkills - ); + + type AgentConfig = Record< + string, + Record | undefined + > & { + build?: Record; + plan?: Record; + explore?: { tools?: Record }; + librarian?: { tools?: Record }; + "multimodal-looker"?: { tools?: Record }; + atlas?: { tools?: Record }; + sisyphus?: { tools?: Record }; + }; + const configAgent = config.agent as AgentConfig | undefined; + + function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; + } + + function buildCustomAgentSummaryInput(agents: Record | undefined): unknown[] { + if (!agents) return []; + + const result: unknown[] = []; + for (const [name, value] of Object.entries(agents)) { + if (!isRecord(value)) continue; + + const description = typeof value.description === "string" ? value.description : ""; + const hidden = value.hidden === true; + const disabled = value.disabled === true || value.enabled === false; + result.push({ name, description, hidden, disabled }); + } + + return result; + } // Claude Code agents: Do NOT apply permission migration // Claude Code uses whitelist-based tools format which is semantically different @@ -216,6 +237,27 @@ export function createConfigHandler(deps: ConfigHandlerDeps) { ]) ); + const customAgentSummaries = [ + ...buildCustomAgentSummaryInput(configAgent), + ...buildCustomAgentSummaryInput(userAgents), + ...buildCustomAgentSummaryInput(projectAgents), + ...buildCustomAgentSummaryInput(pluginAgents), + ]; + + const builtinAgents = await createBuiltinAgents( + migratedDisabledAgents, + pluginConfig.agents, + ctx.directory, + undefined, // systemDefaultModel - let fallback chain handle this + pluginConfig.categories, + pluginConfig.git_master, + allDiscoveredSkills, + customAgentSummaries, + browserProvider, + currentModel, // uiSelectedModel - takes highest priority + disabledSkills + ); + const isSisyphusEnabled = pluginConfig.sisyphus_agent?.disabled !== true; const builderEnabled = pluginConfig.sisyphus_agent?.default_builder_enabled ?? false; @@ -224,20 +266,6 @@ export function createConfigHandler(deps: ConfigHandlerDeps) { const replacePlan = pluginConfig.sisyphus_agent?.replace_plan ?? true; const shouldDemotePlan = plannerEnabled && replacePlan; - type AgentConfig = Record< - string, - Record | undefined - > & { - build?: Record; - plan?: Record; - explore?: { tools?: Record }; - librarian?: { tools?: Record }; - "multimodal-looker"?: { tools?: Record }; - atlas?: { tools?: Record }; - sisyphus?: { tools?: Record }; - }; - const configAgent = config.agent as AgentConfig | undefined; - if (isSisyphusEnabled && builtinAgents.sisyphus) { (config as { default_agent?: string }).default_agent = "sisyphus";