diff --git a/src/agents/utils.test.ts b/src/agents/utils.test.ts index 4c482755..68245b81 100644 --- a/src/agents/utils.test.ts +++ b/src/agents/utils.test.ts @@ -1,5 +1,6 @@ import { describe, test, expect } from "bun:test" import { createBuiltinAgents } from "./utils" +import type { AgentConfig } from "@opencode-ai/sdk" describe("createBuiltinAgents with model overrides", () => { test("Sisyphus with default model has thinking config", () => { @@ -85,3 +86,182 @@ describe("createBuiltinAgents with model overrides", () => { expect(agents.Sisyphus.temperature).toBe(0.5) }) }) + +describe("buildAgent with category and skills", () => { + const { buildAgent } = require("./utils") + + test("agent with category inherits category settings", () => { + // #given + const source = { + "test-agent": () => + ({ + description: "Test agent", + category: "visual-engineering", + }) as AgentConfig, + } + + // #when + const agent = buildAgent(source["test-agent"]) + + // #then + expect(agent.model).toBe("google/gemini-3-pro-preview") + expect(agent.temperature).toBe(0.7) + }) + + test("agent with category and existing model keeps existing model", () => { + // #given + const source = { + "test-agent": () => + ({ + description: "Test agent", + category: "visual-engineering", + model: "custom/model", + }) as AgentConfig, + } + + // #when + const agent = buildAgent(source["test-agent"]) + + // #then + expect(agent.model).toBe("custom/model") + expect(agent.temperature).toBe(0.7) + }) + + test("agent with skills has content prepended to prompt", () => { + // #given + const source = { + "test-agent": () => + ({ + description: "Test agent", + skills: ["frontend-ui-ux"], + prompt: "Original prompt content", + }) as AgentConfig, + } + + // #when + const agent = buildAgent(source["test-agent"]) + + // #then + expect(agent.prompt).toContain("Role: Designer-Turned-Developer") + expect(agent.prompt).toContain("Original prompt content") + expect(agent.prompt).toMatch(/Designer-Turned-Developer[\s\S]*Original prompt content/s) + }) + + test("agent with multiple skills has all content prepended", () => { + // #given + const source = { + "test-agent": () => + ({ + description: "Test agent", + skills: ["frontend-ui-ux"], + prompt: "Agent prompt", + }) as AgentConfig, + } + + // #when + const agent = buildAgent(source["test-agent"]) + + // #then + expect(agent.prompt).toContain("Role: Designer-Turned-Developer") + expect(agent.prompt).toContain("Agent prompt") + }) + + test("agent without category or skills works as before", () => { + // #given + const source = { + "test-agent": () => + ({ + description: "Test agent", + model: "custom/model", + temperature: 0.5, + prompt: "Base prompt", + }) as AgentConfig, + } + + // #when + const agent = buildAgent(source["test-agent"]) + + // #then + expect(agent.model).toBe("custom/model") + expect(agent.temperature).toBe(0.5) + expect(agent.prompt).toBe("Base prompt") + }) + + test("agent with category and skills applies both", () => { + // #given + const source = { + "test-agent": () => + ({ + description: "Test agent", + category: "high-iq", + skills: ["frontend-ui-ux"], + prompt: "Task description", + }) as AgentConfig, + } + + // #when + const agent = buildAgent(source["test-agent"]) + + // #then + expect(agent.model).toBe("openai/gpt-5.2") + expect(agent.temperature).toBe(0.1) + expect(agent.prompt).toContain("Role: Designer-Turned-Developer") + expect(agent.prompt).toContain("Task description") + }) + + test("agent with non-existent category has no effect", () => { + // #given + const source = { + "test-agent": () => + ({ + description: "Test agent", + category: "non-existent", + prompt: "Base prompt", + }) as AgentConfig, + } + + // #when + const agent = buildAgent(source["test-agent"]) + + // #then + expect(agent.model).toBeUndefined() + expect(agent.prompt).toBe("Base prompt") + }) + + test("agent with non-existent skills only prepends found ones", () => { + // #given + const source = { + "test-agent": () => + ({ + description: "Test agent", + skills: ["frontend-ui-ux", "non-existent-skill"], + prompt: "Base prompt", + }) as AgentConfig, + } + + // #when + const agent = buildAgent(source["test-agent"]) + + // #then + expect(agent.prompt).toContain("Role: Designer-Turned-Developer") + expect(agent.prompt).toContain("Base prompt") + }) + + test("agent with empty skills array keeps original prompt", () => { + // #given + const source = { + "test-agent": () => + ({ + description: "Test agent", + skills: [], + prompt: "Base prompt", + }) as AgentConfig, + } + + // #when + const agent = buildAgent(source["test-agent"]) + + // #then + expect(agent.prompt).toBe("Base prompt") + }) +}) diff --git a/src/agents/utils.ts b/src/agents/utils.ts index 55788d91..3831ef69 100644 --- a/src/agents/utils.ts +++ b/src/agents/utils.ts @@ -7,8 +7,13 @@ import { createExploreAgent, EXPLORE_PROMPT_METADATA } from "./explore" import { createFrontendUiUxEngineerAgent, FRONTEND_PROMPT_METADATA } from "./frontend-ui-ux-engineer" import { createDocumentWriterAgent, DOCUMENT_WRITER_PROMPT_METADATA } from "./document-writer" import { createMultimodalLookerAgent, MULTIMODAL_LOOKER_PROMPT_METADATA } from "./multimodal-looker" +import { metisAgent } from "./metis" +import { createOrchestratorSisyphusAgent, orchestratorSisyphusAgent } from "./orchestrator-sisyphus" +import { momusAgent } from "./momus" import type { AvailableAgent } from "./sisyphus-prompt-builder" import { deepMerge } from "../shared" +import { DEFAULT_CATEGORIES } from "../tools/sisyphus-task/constants" +import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content" type AgentSource = AgentFactory | AgentConfig @@ -20,6 +25,9 @@ const agentSources: Record = { "frontend-ui-ux-engineer": createFrontendUiUxEngineerAgent, "document-writer": createDocumentWriterAgent, "multimodal-looker": createMultimodalLookerAgent, + "Metis (Plan Consultant)": metisAgent, + "Momus (Plan Reviewer)": momusAgent, + "orchestrator-sisyphus": orchestratorSisyphusAgent, } /** @@ -39,8 +47,31 @@ function isFactory(source: AgentSource): source is AgentFactory { return typeof source === "function" } -function buildAgent(source: AgentSource, model?: string): AgentConfig { - return isFactory(source) ? source(model) : source +export function buildAgent(source: AgentSource, model?: string): AgentConfig { + const base = isFactory(source) ? source(model) : source + + const agentWithCategory = base as AgentConfig & { category?: string; skills?: string[] } + if (agentWithCategory.category) { + const categoryConfig = DEFAULT_CATEGORIES[agentWithCategory.category] + if (categoryConfig) { + if (!base.model) { + base.model = categoryConfig.model + } + if (base.temperature === undefined && categoryConfig.temperature !== undefined) { + base.temperature = categoryConfig.temperature + } + } + } + + if (agentWithCategory.skills?.length) { + const { resolved } = resolveMultipleSkills(agentWithCategory.skills) + if (resolved.size > 0) { + const skillContent = Array.from(resolved.values()).join("\n\n") + base.prompt = skillContent + (base.prompt ? "\n\n" + base.prompt : "") + } + } + + return base } /** @@ -96,6 +127,7 @@ export function createBuiltinAgents( const agentName = name as BuiltinAgentName if (agentName === "Sisyphus") continue + if (agentName === "orchestrator-sisyphus") continue if (disabledAgents.includes(agentName)) continue const override = agentOverrides[agentName] @@ -142,5 +174,16 @@ export function createBuiltinAgents( result["Sisyphus"] = sisyphusConfig } + if (!disabledAgents.includes("orchestrator-sisyphus")) { + const orchestratorOverride = agentOverrides["orchestrator-sisyphus"] + let orchestratorConfig = createOrchestratorSisyphusAgent({ availableAgents }) + + if (orchestratorOverride) { + orchestratorConfig = mergeAgentConfig(orchestratorConfig, orchestratorOverride) + } + + result["orchestrator-sisyphus"] = orchestratorConfig + } + return result }