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:
committed by
YeonGyu-Kim
parent
5755a90c3b
commit
39bda91bc7
@@ -1,2 +1,5 @@
|
||||
export * from "./types"
|
||||
export * from "./loader"
|
||||
export * from "./agent-definitions-loader"
|
||||
export * from "./opencode-config-agents-reader"
|
||||
export * from "./json-agent-loader"
|
||||
|
||||
@@ -229,6 +229,34 @@ describe("readOpencodeConfigAgents", () => {
|
||||
fs.rmSync(tempDir, { recursive: true })
|
||||
})
|
||||
|
||||
it("supports agent key as fallback when agents key is not present", () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-test-"))
|
||||
const opencodeDir = path.join(tempDir, ".opencode")
|
||||
fs.mkdirSync(opencodeDir, { recursive: true })
|
||||
|
||||
const configPath = path.join(opencodeDir, "opencode.json")
|
||||
fs.writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify({
|
||||
agent: {
|
||||
"fallback-agent": {
|
||||
description: "Using agent key",
|
||||
mode: "subagent",
|
||||
prompt: "Fallback prompt",
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const result = readOpencodeConfigAgents(tempDir)
|
||||
|
||||
expect(result).toHaveProperty("fallback-agent")
|
||||
expect(result["fallback-agent"].description).toBe("(opencode-config) Using agent key")
|
||||
expect(result["fallback-agent"].prompt).toBe("Fallback prompt")
|
||||
|
||||
fs.rmSync(tempDir, { recursive: true })
|
||||
})
|
||||
|
||||
it("prioritizes project-level opencode.json over user-level", () => {
|
||||
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-project-"))
|
||||
const projectOpencodeDir = path.join(projectDir, ".opencode")
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { ClaudeCodeAgentConfig } from "./types"
|
||||
|
||||
interface OpencodeConfigWithAgents {
|
||||
agents?: Record<string, unknown>
|
||||
agent?: Record<string, unknown>
|
||||
agent_definitions?: string | string[]
|
||||
}
|
||||
|
||||
@@ -103,8 +104,10 @@ export function readOpencodeConfigAgents(directory: string): Record<string, Clau
|
||||
|
||||
const configDir = path.dirname(configPath)
|
||||
|
||||
if (parseResult.data.agents && typeof parseResult.data.agents === "object") {
|
||||
for (const [agentName, agentData] of Object.entries(parseResult.data.agents)) {
|
||||
const agentsToLoad = parseResult.data.agents || parseResult.data.agent
|
||||
|
||||
if (agentsToLoad && typeof agentsToLoad === "object") {
|
||||
for (const [agentName, agentData] of Object.entries(agentsToLoad)) {
|
||||
const converted = convertInlineAgent(agentData)
|
||||
if (converted) {
|
||||
result[agentName] = converted
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,7 +14,14 @@ import {
|
||||
discoverProjectClaudeSkills,
|
||||
discoverUserClaudeSkills,
|
||||
} from "../features/opencode-skill-loader";
|
||||
import { loadProjectAgents, loadUserAgents, loadOpencodeGlobalAgents, loadOpencodeProjectAgents } from "../features/claude-code-agent-loader";
|
||||
import {
|
||||
loadProjectAgents,
|
||||
loadUserAgents,
|
||||
loadOpencodeGlobalAgents,
|
||||
loadOpencodeProjectAgents,
|
||||
loadAgentDefinitions,
|
||||
readOpencodeConfigAgents,
|
||||
} from "../features/claude-code-agent-loader";
|
||||
import type { PluginComponents } from "./plugin-components-loader";
|
||||
import { reorderAgentsByPriority } from "./agent-priority-order";
|
||||
import { remapAgentKeysToDisplayNames } from "./agent-key-remapper";
|
||||
@@ -33,7 +40,6 @@ type AgentConfigRecord = Record<string, Record<string, unknown> | undefined> & {
|
||||
function getConfiguredDefaultAgent(config: Record<string, unknown>): string | undefined {
|
||||
const defaultAgent = config.default_agent;
|
||||
if (typeof defaultAgent !== "string") return undefined;
|
||||
|
||||
const trimmedDefaultAgent = defaultAgent.trim();
|
||||
return trimmedDefaultAgent.length > 0 ? trimmedDefaultAgent : undefined;
|
||||
}
|
||||
@@ -100,6 +106,11 @@ export async function applyAgentConfig(params: {
|
||||
const opencodeProjectAgents = loadOpencodeProjectAgents(params.ctx.directory);
|
||||
const rawPluginAgents = params.pluginComponents.agents;
|
||||
|
||||
const agentDefinitionAgents = params.pluginConfig.agent_definitions
|
||||
? loadAgentDefinitions(params.pluginConfig.agent_definitions, "definition-file")
|
||||
: {};
|
||||
const opencodeConfigAgents = readOpencodeConfigAgents(params.ctx.directory);
|
||||
|
||||
const pluginAgents = Object.fromEntries(
|
||||
Object.entries(rawPluginAgents).map(([key, value]) => {
|
||||
if (!value) return [key, value];
|
||||
@@ -118,6 +129,8 @@ export async function applyAgentConfig(params: {
|
||||
...Object.entries(opencodeGlobalAgents),
|
||||
...Object.entries(opencodeProjectAgents),
|
||||
...Object.entries(pluginAgents).filter(([, config]) => config !== undefined),
|
||||
...Object.entries(agentDefinitionAgents),
|
||||
...Object.entries(opencodeConfigAgents),
|
||||
]
|
||||
.filter(([, config]) => config != null)
|
||||
.map(([name, config]) => ({
|
||||
@@ -127,6 +140,20 @@ export async function applyAgentConfig(params: {
|
||||
: "",
|
||||
}));
|
||||
|
||||
log(
|
||||
"[agent-config-handler] Agent sources loaded",
|
||||
{
|
||||
user: Object.keys(userAgents).length,
|
||||
project: Object.keys(projectAgents).length,
|
||||
opencodeGlobal: Object.keys(opencodeGlobalAgents).length,
|
||||
opencodeProject: Object.keys(opencodeProjectAgents).length,
|
||||
plugin: Object.keys(pluginAgents).length,
|
||||
agentDefinitions: Object.keys(agentDefinitionAgents).length,
|
||||
opencodeConfig: Object.keys(opencodeConfigAgents).length,
|
||||
config: Object.keys(configAgent ?? {}).length,
|
||||
}
|
||||
);
|
||||
|
||||
const builtinAgents = await createBuiltinAgents(
|
||||
migratedDisabledAgents,
|
||||
params.pluginConfig.agents,
|
||||
@@ -269,6 +296,14 @@ export async function applyAgentConfig(params: {
|
||||
opencodeProjectAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
const filteredAgentDefinitionAgents = filterProtectedAgentOverrides(
|
||||
agentDefinitionAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
const filteredOpencodeConfigAgents = filterProtectedAgentOverrides(
|
||||
opencodeConfigAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
|
||||
params.config.agent = {
|
||||
...agentConfig,
|
||||
@@ -283,6 +318,8 @@ export async function applyAgentConfig(params: {
|
||||
...filterDisabledAgents(filteredOpencodeGlobalAgents),
|
||||
...filterDisabledAgents(filteredProjectAgents),
|
||||
...filterDisabledAgents(filteredOpencodeProjectAgents),
|
||||
...filterDisabledAgents(filteredAgentDefinitionAgents),
|
||||
...filterDisabledAgents(filteredOpencodeConfigAgents),
|
||||
...filteredConfigAgents,
|
||||
build: { ...migratedBuild, mode: "subagent", hidden: true },
|
||||
...(planDemoteConfig ? { plan: planDemoteConfig } : {}),
|
||||
@@ -311,6 +348,14 @@ export async function applyAgentConfig(params: {
|
||||
opencodeProjectAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
const filteredAgentDefinitionAgents = filterProtectedAgentOverrides(
|
||||
agentDefinitionAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
const filteredOpencodeConfigAgents = filterProtectedAgentOverrides(
|
||||
opencodeConfigAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
|
||||
const defaultedConfigAgents = configAgent
|
||||
? Object.fromEntries(
|
||||
@@ -331,6 +376,8 @@ export async function applyAgentConfig(params: {
|
||||
...filterDisabledAgents(filteredOpencodeGlobalAgents),
|
||||
...filterDisabledAgents(filteredProjectAgents),
|
||||
...filterDisabledAgents(filteredOpencodeProjectAgents),
|
||||
...filterDisabledAgents(filteredAgentDefinitionAgents),
|
||||
...filterDisabledAgents(filteredOpencodeConfigAgents),
|
||||
...defaultedConfigAgents,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user