Merge pull request #3668 from code-yeongyu/fix/skills-override-resolution

fix(agents): resolve skills override into agent prompt post-merge (fixes #3544)
This commit is contained in:
YeonGyu-Kim
2026-04-27 17:30:40 +09:00
committed by GitHub
5 changed files with 79 additions and 37 deletions
+2 -15
View File
@@ -1,9 +1,7 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentFactory } from "./types"
import type { CategoriesConfig, CategoryConfig, GitMasterConfig } from "../config/schema"
import type { BrowserAutomationProvider } from "../config/schema"
import type { CategoriesConfig, CategoryConfig } from "../config/schema"
import { mergeCategories } from "../shared/merge-categories"
import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content"
export type AgentSource = AgentFactory | AgentConfig
@@ -14,10 +12,7 @@ export function isFactory(source: AgentSource): source is AgentFactory {
export function buildAgent(
source: AgentSource,
model: string,
categories?: CategoriesConfig,
gitMasterConfig?: GitMasterConfig,
browserProvider?: BrowserAutomationProvider,
disabledSkills?: Set<string>
categories?: CategoriesConfig
): AgentConfig {
const base = isFactory(source) ? source(model) : { ...source }
const categoryConfigs: Record<string, CategoryConfig> = mergeCategories(categories)
@@ -38,13 +33,5 @@ export function buildAgent(
}
}
if (agentWithCategory.skills?.length) {
const { resolved } = resolveMultipleSkills(agentWithCategory.skills, { gitMasterConfig, browserProvider, disabledSkills })
if (resolved.size > 0) {
const skillContent = Array.from(resolved.values()).join("\n\n")
base.prompt = skillContent + (base.prompt ? "\n\n" + base.prompt : "")
}
}
return base
}
+26
View File
@@ -0,0 +1,26 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { BrowserAutomationProvider, GitMasterConfig } from "../config/schema"
import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content"
type AgentConfigWithSkills = AgentConfig & { skills?: string[] }
export function resolveAgentSkills(
config: AgentConfig,
options: {
gitMasterConfig?: GitMasterConfig
browserProvider?: BrowserAutomationProvider
disabledSkills?: Set<string>
} = {}
): AgentConfig {
const { skills, ...configWithoutSkills } = config as AgentConfigWithSkills
if (!skills?.length) return configWithoutSkills
const { resolved } = resolveMultipleSkills(skills, options)
if (resolved.size === 0) return configWithoutSkills
const skillContent = Array.from(resolved.values()).join("\n\n")
return {
...configWithoutSkills,
prompt: skillContent + (configWithoutSkills.prompt ? "\n\n" + configWithoutSkills.prompt : ""),
}
}
+3 -1
View File
@@ -5,6 +5,7 @@ import type { BrowserAutomationProvider } from "../../config/schema"
import type { AvailableAgent } from "../dynamic-agent-prompt-builder"
import { AGENT_MODEL_REQUIREMENTS, isModelAvailable } from "../../shared"
import { buildAgent, isFactory } from "../agent-builder"
import { resolveAgentSkills } from "../agent-skill-resolution"
import { applyOverrides } from "./agent-overrides"
import { applyEnvironmentContext } from "./environment-context"
import { applyModelResolution, getFirstFallbackModel } from "./model-resolution"
@@ -92,7 +93,7 @@ export function collectPendingBuiltinAgents(input: {
if (!resolution) continue
const { model, variant: resolvedVariant } = resolution
let config = buildAgent(source, model, mergedCategories, gitMasterConfig, browserProvider, disabledSkills)
let config = buildAgent(source, model, mergedCategories)
// Apply resolved variant from model fallback chain
if (resolvedVariant) {
@@ -104,6 +105,7 @@ export function collectPendingBuiltinAgents(input: {
}
config = applyOverrides(config, override, mergedCategories, directory)
config = resolveAgentSkills(config, { gitMasterConfig, browserProvider, disabledSkills })
// Store for later - will be added after sisyphus and hephaestus
pendingAgentConfigs.set(name, config)
+3
View File
@@ -138,7 +138,10 @@ export type OverridableAgentName = "build" | BuiltinAgentName;
export type AgentName = BuiltinAgentName;
export type AgentOverrideConfig = Partial<AgentConfig> & {
category?: string;
prompt_append?: string;
skills?: string[];
tools?: Record<string, boolean>;
variant?: string;
fallback_models?: string | (string | import("../config/schema/fallback-models").FallbackModelObject)[];
};
+45 -21
View File
@@ -2,6 +2,8 @@
import { describe, test, expect, beforeEach, afterEach, spyOn, mock } from "bun:test"
import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentOverrides } from "./types"
import { resolveAgentSkills } from "./agent-skill-resolution"
import { clearSkillCache } from "../features/opencode-skill-loader/skill-content"
import * as connectedProvidersCache from "../shared/connected-providers-cache"
import * as modelAvailability from "../shared/model-availability"
@@ -1060,7 +1062,7 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"], TEST_MODEL)
const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL))
// #then - category's built-in model is applied
expect(agent.model).toBe("google/gemini-3.1-pro")
@@ -1078,7 +1080,7 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"], TEST_MODEL)
const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL))
// #then - explicit model takes precedence over category
expect(agent.model).toBe("custom/model")
@@ -1121,7 +1123,7 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"], TEST_MODEL)
const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL))
// #then
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
@@ -1141,7 +1143,7 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"], TEST_MODEL)
const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL))
// #then
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
@@ -1161,7 +1163,7 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"], TEST_MODEL)
const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL))
// #then
expect(agent.model).toBe("custom/model")
@@ -1182,7 +1184,7 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"], TEST_MODEL)
const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL))
// #then - category's built-in model and skills are applied
expect(agent.model).toBe("openai/gpt-5.5")
@@ -1203,7 +1205,7 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"], TEST_MODEL)
const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL))
// #then
// Note: The factory receives model, but if category doesn't exist, it's not applied
@@ -1224,7 +1226,7 @@ describe("buildAgent with category and skills", () => {
}
// #when
const agent = buildAgent(source["test-agent"], TEST_MODEL)
const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL))
// #then
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
@@ -1261,7 +1263,7 @@ describe("buildAgent with category and skills", () => {
}
// #when - browserProvider is "agent-browser"
const agent = buildAgent(source["test-agent"], TEST_MODEL, undefined, undefined, "agent-browser")
const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL), { browserProvider: "agent-browser" })
// #then - agent-browser skill content should be in prompt
expect(agent.prompt).toContain("agent-browser")
@@ -1280,7 +1282,7 @@ describe("buildAgent with category and skills", () => {
}
// #when - no browserProvider (defaults to playwright)
const agent = buildAgent(source["test-agent"], TEST_MODEL)
const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL))
// #then - agent-browser skill not found, only base prompt remains
expect(agent.prompt).toBe("Base prompt")
@@ -1288,6 +1290,28 @@ describe("buildAgent with category and skills", () => {
})
})
describe("createBuiltinAgents with skill overrides", () => {
test("injects user configured skills into standard agent prompt", async () => {
// #given
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set())
const overrides = {
librarian: { skills: ["frontend-ui-ux"] },
} as AgentOverrides
try {
// #when
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
// #then
expect(agents.librarian.prompt).toContain("Role: Designer-Turned-Developer")
expect(agents.librarian.prompt).toContain("THE LIBRARIAN")
expect("skills" in agents.librarian).toBe(false)
} finally {
fetchSpy.mockRestore()
}
})
})
describe("override.category expansion in createBuiltinAgents", () => {
let providerModelsSpy: ReturnType<typeof spyOn>
let fetchSpy: ReturnType<typeof spyOn>
@@ -1303,7 +1327,7 @@ describe("override.category expansion in createBuiltinAgents", () => {
test("standard agent override with category expands category properties", async () => {
// #given
const overrides = {
oracle: { category: "ultrabrain" } as any,
oracle: { category: "ultrabrain" },
}
// #when
@@ -1318,7 +1342,7 @@ describe("override.category expansion in createBuiltinAgents", () => {
test("standard agent override with category AND direct variant - direct wins", async () => {
// #given - ultrabrain has variant=xhigh, but direct override says "max"
const overrides = {
oracle: { category: "ultrabrain", variant: "max" } as any,
oracle: { category: "ultrabrain", variant: "max" },
}
// #when
@@ -1338,7 +1362,7 @@ describe("override.category expansion in createBuiltinAgents", () => {
},
}
const overrides = {
oracle: { category: "test-cat", reasoningEffort: "low" } as any,
oracle: { category: "test-cat", reasoningEffort: "low" as const },
}
// #when
@@ -1358,7 +1382,7 @@ describe("override.category expansion in createBuiltinAgents", () => {
},
}
const overrides = {
oracle: { category: "reasoning-cat" } as any,
oracle: { category: "reasoning-cat" },
}
// #when
@@ -1372,7 +1396,7 @@ describe("override.category expansion in createBuiltinAgents", () => {
test("sisyphus override with category expands category properties", async () => {
// #given
const overrides = {
sisyphus: { category: "ultrabrain" } as any,
sisyphus: { category: "ultrabrain" },
}
// #when
@@ -1387,7 +1411,7 @@ describe("override.category expansion in createBuiltinAgents", () => {
test("atlas override with category expands category properties", async () => {
// #given
const overrides = {
atlas: { category: "ultrabrain" } as any,
atlas: { category: "ultrabrain" },
}
// #when
@@ -1402,7 +1426,7 @@ describe("override.category expansion in createBuiltinAgents", () => {
test("override with non-existent category has no effect on config", async () => {
// #given
const overrides = {
oracle: { category: "non-existent-category" } as any,
oracle: { category: "non-existent-category" },
}
// #when
@@ -1430,7 +1454,7 @@ describe("agent override tools migration", () => {
test("tools: { x: false } is migrated to permission: { x: deny }", async () => {
// #given
const overrides = {
explore: { tools: { "jetbrains_*": false } } as any,
explore: { tools: { "jetbrains_*": false } },
}
// #when
@@ -1445,7 +1469,7 @@ describe("agent override tools migration", () => {
test("tools: { x: true } is migrated to permission: { x: allow }", async () => {
// #given
const overrides = {
librarian: { tools: { "jetbrains_get_*": true } } as any,
librarian: { tools: { "jetbrains_get_*": true } },
}
// #when
@@ -1460,7 +1484,7 @@ describe("agent override tools migration", () => {
test("tools config is removed after migration", async () => {
// #given
const overrides = {
explore: { tools: { "some_tool": false } } as any,
explore: { tools: { "some_tool": false } },
}
// #when
@@ -1468,7 +1492,7 @@ describe("agent override tools migration", () => {
// #then
expect(agents.explore).toBeDefined()
expect((agents.explore as any).tools).toBeUndefined()
expect("tools" in agents.explore).toBe(false)
})
})