feat(agents): wire agent_definitions and opencode.json agents into precedence chain

- Modified agent-config-handler.ts to load and integrate both new agent sources
- Added loadAgentDefinitions() and readOpencodeConfigAgents() calls in loading phase
- Integrated both sources into agent precedence chains (both Sisyphus-enabled and disabled paths)
- Added detailed logging for new agent sources
- Added filtering logic to respect disabled_agents configuration
- Extended agent-config-handler.test.ts with 7 new integration tests
- All tests passing (18/18 integration, 65/65 loader suite)

Wave 3 of agent definitions enhancement complete.
This commit is contained in:
Brandon Webb
2026-04-14 13:34:30 -04:00
committed by YeonGyu-Kim
parent 5755a90c3b
commit 39bda91bc7
5 changed files with 298 additions and 4 deletions
@@ -61,6 +61,8 @@ describe("applyAgentConfig builtin override protection", () => {
let discoverGlobalAgentsSkillsSpy: ReturnType<typeof spyOn>
let loadUserAgentsSpy: ReturnType<typeof spyOn>
let loadProjectAgentsSpy: ReturnType<typeof spyOn>
let loadAgentDefinitionsSpy: ReturnType<typeof spyOn>
let readOpencodeConfigAgentsSpy: ReturnType<typeof spyOn>
let migrateAgentConfigSpy: ReturnType<typeof spyOn>
let logSpy: ReturnType<typeof spyOn>
@@ -140,6 +142,11 @@ describe("applyAgentConfig builtin override protection", () => {
loadUserAgentsSpy = spyOn(agentLoader, "loadUserAgents").mockReturnValue({})
loadProjectAgentsSpy = spyOn(agentLoader, "loadProjectAgents").mockReturnValue({})
loadAgentDefinitionsSpy = spyOn(agentLoader, "loadAgentDefinitions").mockReturnValue({})
readOpencodeConfigAgentsSpy = spyOn(
agentLoader,
"readOpencodeConfigAgents",
).mockReturnValue({})
migrateAgentConfigSpy = spyOn(shared, "migrateAgentConfig").mockImplementation(
(config: Record<string, unknown>) => config,
@@ -159,6 +166,8 @@ describe("applyAgentConfig builtin override protection", () => {
discoverGlobalAgentsSkillsSpy.mockRestore()
loadUserAgentsSpy.mockRestore()
loadProjectAgentsSpy.mockRestore()
loadAgentDefinitionsSpy.mockRestore()
readOpencodeConfigAgentsSpy.mockRestore()
migrateAgentConfigSpy.mockRestore()
logSpy.mockRestore()
})
@@ -441,4 +450,208 @@ describe("applyAgentConfig builtin override protection", () => {
]),
)
})
describe("agent_definitions and opencode.json integration", () => {
test("agent_definitions agents appear in output", async () => {
// given
loadAgentDefinitionsSpy.mockReturnValue({
"my-custom-agent": {
name: "my-custom-agent",
prompt: "test custom agent from agent_definitions",
mode: "subagent",
},
})
const pluginConfig = createPluginConfig()
pluginConfig.agent_definitions = ["/fake/path/agent.md"]
// when
const result = await applyAgentConfig({
config: createBaseConfig(),
pluginConfig,
ctx: { directory: "/tmp" },
pluginComponents: createPluginComponents(),
})
// then
expect(result["my-custom-agent"]).toBeDefined()
expect(result["my-custom-agent"]?.prompt).toBe("test custom agent from agent_definitions")
})
test("opencode.json agents appear in output", async () => {
// given
readOpencodeConfigAgentsSpy.mockReturnValue({
"opencode-agent": {
name: "opencode-agent",
prompt: "test opencode config agent",
mode: "subagent",
description: "(opencode-config) OC",
},
})
// when
const result = await applyAgentConfig({
config: createBaseConfig(),
pluginConfig: createPluginConfig(),
ctx: { directory: "/tmp" },
pluginComponents: createPluginComponents(),
})
// then
expect(result["opencode-agent"]).toBeDefined()
expect(result["opencode-agent"]?.prompt).toBe("test opencode config agent")
expect(result["opencode-agent"]?.description).toBe("(opencode-config) OC")
})
test("agent_definitions agents subject to disabled_agents filtering", async () => {
// given
loadAgentDefinitionsSpy.mockReturnValue({
"disabled-custom-agent": {
name: "disabled-custom-agent",
prompt: "this should be filtered",
mode: "subagent",
},
})
const pluginConfig = createPluginConfig()
pluginConfig.agent_definitions = ["/fake/path/agent.md"]
pluginConfig.disabled_agents = ["disabled-custom-agent"]
// when
const result = await applyAgentConfig({
config: createBaseConfig(),
pluginConfig,
ctx: { directory: "/tmp" },
pluginComponents: createPluginComponents(),
})
// then
expect(result["disabled-custom-agent"]).toBeUndefined()
})
test("agent_definitions cannot override builtin agents", async () => {
// given
loadAgentDefinitionsSpy.mockReturnValue({
oracle: {
name: "oracle",
prompt: "evil override prompt",
mode: "subagent",
},
})
const pluginConfig = createPluginConfig()
pluginConfig.agent_definitions = ["/fake/path/agent.md"]
// when
const result = await applyAgentConfig({
config: createBaseConfig(),
pluginConfig,
ctx: { directory: "/tmp" },
pluginComponents: createPluginComponents(),
})
// then
expect(result.oracle).toBeDefined()
expect(result.oracle?.prompt).not.toBe("evil override prompt")
})
test("precedence: configAgents override agent_definitions", async () => {
// given
loadAgentDefinitionsSpy.mockReturnValue({
"shared-name": {
name: "shared-name",
prompt: "from-definitions",
mode: "subagent",
},
})
const config = createBaseConfig()
;(config as Record<string, unknown>).agent = {
"shared-name": {
name: "shared-name",
prompt: "from-config",
mode: "subagent",
},
}
const pluginConfig = createPluginConfig()
pluginConfig.agent_definitions = ["/fake/path/agent.md"]
// when
const result = await applyAgentConfig({
config,
pluginConfig,
ctx: { directory: "/tmp" },
pluginComponents: createPluginComponents(),
})
// then
expect(result["shared-name"]).toBeDefined()
expect(result["shared-name"]?.prompt).toBe("from-config")
})
test("precedence: agent_definitions overrides project agents", async () => {
// given
loadProjectAgentsSpy.mockReturnValue({
"shared-name": {
name: "shared-name",
prompt: "from-project",
mode: "subagent",
},
})
loadAgentDefinitionsSpy.mockReturnValue({
"shared-name": {
name: "shared-name",
prompt: "from-definitions",
mode: "subagent",
},
})
const pluginConfig = createPluginConfig()
pluginConfig.agent_definitions = ["/fake/path/agent.md"]
// when
const result = await applyAgentConfig({
config: createBaseConfig(),
pluginConfig,
ctx: { directory: "/tmp" },
pluginComponents: createPluginComponents(),
})
// then
expect(result["shared-name"]).toBeDefined()
expect(result["shared-name"]?.prompt).toBe("from-definitions")
})
test("both Sisyphus-enabled and disabled paths include new sources", async () => {
// given
loadAgentDefinitionsSpy.mockReturnValue({
"definitions-agent": {
name: "definitions-agent",
prompt: "from agent_definitions",
mode: "subagent",
},
})
readOpencodeConfigAgentsSpy.mockReturnValue({
"opencode-agent": {
name: "opencode-agent",
prompt: "from opencode.json",
mode: "subagent",
},
})
const pluginConfig = createPluginConfig()
pluginConfig.agent_definitions = ["/fake/path/agent.md"]
if (pluginConfig.sisyphus_agent) {
pluginConfig.sisyphus_agent.planner_enabled = false
}
// when
const result = await applyAgentConfig({
config: createBaseConfig(),
pluginConfig,
ctx: { directory: "/tmp" },
pluginComponents: createPluginComponents(),
})
// then
expect(result["definitions-agent"]).toBeDefined()
expect(result["definitions-agent"]?.prompt).toBe("from agent_definitions")
expect(result["opencode-agent"]).toBeDefined()
expect(result["opencode-agent"]?.prompt).toBe("from opencode.json")
})
})
})