fix(agents): use config data instead of client API to avoid init deadlock (#1623)

This commit is contained in:
YeonGyu-Kim
2026-02-08 15:34:47 +09:00
parent ef49e1700c
commit 3a559d2593
3 changed files with 257 additions and 64 deletions
+181 -15
View File
@@ -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", () => {
+21 -22
View File
@@ -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<string, unknown> {
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<RegisteredAgentSummary[]> {
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<string>
): Promise<Record<string, AgentConfig>> {
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()))