Skip to content

Commit 059aa87

Browse files
committed
feat(agents): add category and skills support to buildAgent
Extend buildAgent() to support: - category: inherit model/temperature from DEFAULT_CATEGORIES - skills: prepend resolved skill content to agent prompt Includes comprehensive test coverage for new functionality. 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
1 parent dfb4f8a commit 059aa87

File tree

2 files changed

+225
-2
lines changed

2 files changed

+225
-2
lines changed

src/agents/utils.test.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, test, expect } from "bun:test"
22
import { createBuiltinAgents } from "./utils"
3+
import type { AgentConfig } from "@opencode-ai/sdk"
34

45
describe("createBuiltinAgents with model overrides", () => {
56
test("Sisyphus with default model has thinking config", () => {
@@ -85,3 +86,182 @@ describe("createBuiltinAgents with model overrides", () => {
8586
expect(agents.Sisyphus.temperature).toBe(0.5)
8687
})
8788
})
89+
90+
describe("buildAgent with category and skills", () => {
91+
const { buildAgent } = require("./utils")
92+
93+
test("agent with category inherits category settings", () => {
94+
// #given
95+
const source = {
96+
"test-agent": () =>
97+
({
98+
description: "Test agent",
99+
category: "visual-engineering",
100+
}) as AgentConfig,
101+
}
102+
103+
// #when
104+
const agent = buildAgent(source["test-agent"])
105+
106+
// #then
107+
expect(agent.model).toBe("google/gemini-3-pro-preview")
108+
expect(agent.temperature).toBe(0.7)
109+
})
110+
111+
test("agent with category and existing model keeps existing model", () => {
112+
// #given
113+
const source = {
114+
"test-agent": () =>
115+
({
116+
description: "Test agent",
117+
category: "visual-engineering",
118+
model: "custom/model",
119+
}) as AgentConfig,
120+
}
121+
122+
// #when
123+
const agent = buildAgent(source["test-agent"])
124+
125+
// #then
126+
expect(agent.model).toBe("custom/model")
127+
expect(agent.temperature).toBe(0.7)
128+
})
129+
130+
test("agent with skills has content prepended to prompt", () => {
131+
// #given
132+
const source = {
133+
"test-agent": () =>
134+
({
135+
description: "Test agent",
136+
skills: ["frontend-ui-ux"],
137+
prompt: "Original prompt content",
138+
}) as AgentConfig,
139+
}
140+
141+
// #when
142+
const agent = buildAgent(source["test-agent"])
143+
144+
// #then
145+
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
146+
expect(agent.prompt).toContain("Original prompt content")
147+
expect(agent.prompt).toMatch(/Designer-Turned-Developer[\s\S]*Original prompt content/s)
148+
})
149+
150+
test("agent with multiple skills has all content prepended", () => {
151+
// #given
152+
const source = {
153+
"test-agent": () =>
154+
({
155+
description: "Test agent",
156+
skills: ["frontend-ui-ux"],
157+
prompt: "Agent prompt",
158+
}) as AgentConfig,
159+
}
160+
161+
// #when
162+
const agent = buildAgent(source["test-agent"])
163+
164+
// #then
165+
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
166+
expect(agent.prompt).toContain("Agent prompt")
167+
})
168+
169+
test("agent without category or skills works as before", () => {
170+
// #given
171+
const source = {
172+
"test-agent": () =>
173+
({
174+
description: "Test agent",
175+
model: "custom/model",
176+
temperature: 0.5,
177+
prompt: "Base prompt",
178+
}) as AgentConfig,
179+
}
180+
181+
// #when
182+
const agent = buildAgent(source["test-agent"])
183+
184+
// #then
185+
expect(agent.model).toBe("custom/model")
186+
expect(agent.temperature).toBe(0.5)
187+
expect(agent.prompt).toBe("Base prompt")
188+
})
189+
190+
test("agent with category and skills applies both", () => {
191+
// #given
192+
const source = {
193+
"test-agent": () =>
194+
({
195+
description: "Test agent",
196+
category: "high-iq",
197+
skills: ["frontend-ui-ux"],
198+
prompt: "Task description",
199+
}) as AgentConfig,
200+
}
201+
202+
// #when
203+
const agent = buildAgent(source["test-agent"])
204+
205+
// #then
206+
expect(agent.model).toBe("openai/gpt-5.2")
207+
expect(agent.temperature).toBe(0.1)
208+
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
209+
expect(agent.prompt).toContain("Task description")
210+
})
211+
212+
test("agent with non-existent category has no effect", () => {
213+
// #given
214+
const source = {
215+
"test-agent": () =>
216+
({
217+
description: "Test agent",
218+
category: "non-existent",
219+
prompt: "Base prompt",
220+
}) as AgentConfig,
221+
}
222+
223+
// #when
224+
const agent = buildAgent(source["test-agent"])
225+
226+
// #then
227+
expect(agent.model).toBeUndefined()
228+
expect(agent.prompt).toBe("Base prompt")
229+
})
230+
231+
test("agent with non-existent skills only prepends found ones", () => {
232+
// #given
233+
const source = {
234+
"test-agent": () =>
235+
({
236+
description: "Test agent",
237+
skills: ["frontend-ui-ux", "non-existent-skill"],
238+
prompt: "Base prompt",
239+
}) as AgentConfig,
240+
}
241+
242+
// #when
243+
const agent = buildAgent(source["test-agent"])
244+
245+
// #then
246+
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
247+
expect(agent.prompt).toContain("Base prompt")
248+
})
249+
250+
test("agent with empty skills array keeps original prompt", () => {
251+
// #given
252+
const source = {
253+
"test-agent": () =>
254+
({
255+
description: "Test agent",
256+
skills: [],
257+
prompt: "Base prompt",
258+
}) as AgentConfig,
259+
}
260+
261+
// #when
262+
const agent = buildAgent(source["test-agent"])
263+
264+
// #then
265+
expect(agent.prompt).toBe("Base prompt")
266+
})
267+
})

src/agents/utils.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ import { createExploreAgent, EXPLORE_PROMPT_METADATA } from "./explore"
77
import { createFrontendUiUxEngineerAgent, FRONTEND_PROMPT_METADATA } from "./frontend-ui-ux-engineer"
88
import { createDocumentWriterAgent, DOCUMENT_WRITER_PROMPT_METADATA } from "./document-writer"
99
import { createMultimodalLookerAgent, MULTIMODAL_LOOKER_PROMPT_METADATA } from "./multimodal-looker"
10+
import { metisAgent } from "./metis"
11+
import { createOrchestratorSisyphusAgent, orchestratorSisyphusAgent } from "./orchestrator-sisyphus"
12+
import { momusAgent } from "./momus"
1013
import type { AvailableAgent } from "./sisyphus-prompt-builder"
1114
import { deepMerge } from "../shared"
15+
import { DEFAULT_CATEGORIES } from "../tools/sisyphus-task/constants"
16+
import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content"
1217

1318
type AgentSource = AgentFactory | AgentConfig
1419

@@ -20,6 +25,9 @@ const agentSources: Record<BuiltinAgentName, AgentSource> = {
2025
"frontend-ui-ux-engineer": createFrontendUiUxEngineerAgent,
2126
"document-writer": createDocumentWriterAgent,
2227
"multimodal-looker": createMultimodalLookerAgent,
28+
"Metis (Plan Consultant)": metisAgent,
29+
"Momus (Plan Reviewer)": momusAgent,
30+
"orchestrator-sisyphus": orchestratorSisyphusAgent,
2331
}
2432

2533
/**
@@ -39,8 +47,31 @@ function isFactory(source: AgentSource): source is AgentFactory {
3947
return typeof source === "function"
4048
}
4149

42-
function buildAgent(source: AgentSource, model?: string): AgentConfig {
43-
return isFactory(source) ? source(model) : source
50+
export function buildAgent(source: AgentSource, model?: string): AgentConfig {
51+
const base = isFactory(source) ? source(model) : source
52+
53+
const agentWithCategory = base as AgentConfig & { category?: string; skills?: string[] }
54+
if (agentWithCategory.category) {
55+
const categoryConfig = DEFAULT_CATEGORIES[agentWithCategory.category]
56+
if (categoryConfig) {
57+
if (!base.model) {
58+
base.model = categoryConfig.model
59+
}
60+
if (base.temperature === undefined && categoryConfig.temperature !== undefined) {
61+
base.temperature = categoryConfig.temperature
62+
}
63+
}
64+
}
65+
66+
if (agentWithCategory.skills?.length) {
67+
const { resolved } = resolveMultipleSkills(agentWithCategory.skills)
68+
if (resolved.size > 0) {
69+
const skillContent = Array.from(resolved.values()).join("\n\n")
70+
base.prompt = skillContent + (base.prompt ? "\n\n" + base.prompt : "")
71+
}
72+
}
73+
74+
return base
4475
}
4576

4677
/**
@@ -96,6 +127,7 @@ export function createBuiltinAgents(
96127
const agentName = name as BuiltinAgentName
97128

98129
if (agentName === "Sisyphus") continue
130+
if (agentName === "orchestrator-sisyphus") continue
99131
if (disabledAgents.includes(agentName)) continue
100132

101133
const override = agentOverrides[agentName]
@@ -142,5 +174,16 @@ export function createBuiltinAgents(
142174
result["Sisyphus"] = sisyphusConfig
143175
}
144176

177+
if (!disabledAgents.includes("orchestrator-sisyphus")) {
178+
const orchestratorOverride = agentOverrides["orchestrator-sisyphus"]
179+
let orchestratorConfig = createOrchestratorSisyphusAgent({ availableAgents })
180+
181+
if (orchestratorOverride) {
182+
orchestratorConfig = mergeAgentConfig(orchestratorConfig, orchestratorOverride)
183+
}
184+
185+
result["orchestrator-sisyphus"] = orchestratorConfig
186+
}
187+
145188
return result
146189
}

0 commit comments

Comments
 (0)