diff --git a/src/agents/dynamic-agent-prompt-builder.ts b/src/agents/dynamic-agent-prompt-builder.ts index 64156bd6b..7c7d35000 100644 --- a/src/agents/dynamic-agent-prompt-builder.ts +++ b/src/agents/dynamic-agent-prompt-builder.ts @@ -20,6 +20,7 @@ export interface AvailableSkill { export interface AvailableCategory { name: string description: string + model?: string } export function categorizeTools(toolNames: string[]): AvailableTool[] { diff --git a/src/index.ts b/src/index.ts index 548912990..481240bbb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -56,8 +56,10 @@ import { discoverOpencodeProjectSkills, mergeSkills, } from "./features/opencode-skill-loader"; +import type { SkillScope } from "./features/opencode-skill-loader/types"; import { createBuiltinSkills } from "./features/builtin-skills"; import { getSystemMcpServerNames } from "./features/claude-code-mcp-loader"; +import type { AvailableSkill } from "./agents/dynamic-agent-prompt-builder"; import { setMainSession, getMainSessionID, @@ -84,6 +86,10 @@ import { createTaskList, createTaskUpdateTool, } from "./tools"; +import { + CATEGORY_DESCRIPTIONS, + DEFAULT_CATEGORIES, +} from "./tools/delegate-task/constants"; import { BackgroundManager } from "./features/background-agent"; import { SkillMcpManager } from "./features/skill-mcp-manager"; import { initTaskToastManager } from "./features/task-toast-manager"; @@ -394,33 +400,6 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { const browserProvider = pluginConfig.browser_automation_engine?.provider ?? "playwright"; const disabledSkills = new Set(pluginConfig.disabled_skills ?? []); - const delegateTask = createDelegateTask({ - manager: backgroundManager, - client: ctx.client, - directory: ctx.directory, - userCategories: pluginConfig.categories, - gitMasterConfig: pluginConfig.git_master, - sisyphusJuniorModel: pluginConfig.agents?.["sisyphus-junior"]?.model, - browserProvider, - disabledSkills, - onSyncSessionCreated: async (event) => { - log("[index] onSyncSessionCreated callback", { - sessionID: event.sessionID, - parentID: event.parentID, - title: event.title, - }); - await tmuxSessionManager.onSessionCreated({ - type: "session.created", - properties: { - info: { - id: event.sessionID, - parentID: event.parentID, - title: event.title, - }, - }, - }); - }, - }); const systemMcpNames = getSystemMcpServerNames(); const builtinSkills = createBuiltinSkills({ browserProvider, disabledSkills }).filter((skill) => { if (skill.mcpConfig) { @@ -447,6 +426,63 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { projectSkills, opencodeProjectSkills, ); + + function mapScopeToLocation(scope: SkillScope): AvailableSkill["location"] { + if (scope === "user" || scope === "opencode") return "user"; + if (scope === "project" || scope === "opencode-project") return "project"; + return "plugin"; + } + + const availableSkills: AvailableSkill[] = mergedSkills.map((skill) => ({ + name: skill.name, + description: skill.definition.description ?? "", + location: mapScopeToLocation(skill.scope), + })); + + const mergedCategories = pluginConfig.categories + ? { ...DEFAULT_CATEGORIES, ...pluginConfig.categories } + : DEFAULT_CATEGORIES; + + const availableCategories = Object.entries(mergedCategories).map( + ([name, categoryConfig]) => ({ + name, + description: + pluginConfig.categories?.[name]?.description + ?? CATEGORY_DESCRIPTIONS[name] + ?? "General tasks", + model: categoryConfig.model, + }), + ); + + const delegateTask = createDelegateTask({ + manager: backgroundManager, + client: ctx.client, + directory: ctx.directory, + userCategories: pluginConfig.categories, + gitMasterConfig: pluginConfig.git_master, + sisyphusJuniorModel: pluginConfig.agents?.["sisyphus-junior"]?.model, + browserProvider, + disabledSkills, + availableCategories, + availableSkills, + onSyncSessionCreated: async (event) => { + log("[index] onSyncSessionCreated callback", { + sessionID: event.sessionID, + parentID: event.parentID, + title: event.title, + }); + await tmuxSessionManager.onSessionCreated({ + type: "session.created", + properties: { + info: { + id: event.sessionID, + parentID: event.parentID, + title: event.title, + }, + }, + }); + }, + }); const skillMcpManager = new SkillMcpManager(); const getSessionIDForMcp = () => getMainSessionID() || ""; const skillTool = createSkillTool({ diff --git a/src/tools/delegate-task/constants.ts b/src/tools/delegate-task/constants.ts index ea405faef..b97ff6b20 100644 --- a/src/tools/delegate-task/constants.ts +++ b/src/tools/delegate-task/constants.ts @@ -1,4 +1,8 @@ import type { CategoryConfig } from "../../config/schema" +import type { + AvailableCategory, + AvailableSkill, +} from "../../agents/dynamic-agent-prompt-builder" export const VISUAL_CATEGORY_PROMPT_APPEND = ` You are working on VISUAL/UI tasks. @@ -231,7 +235,7 @@ export const CATEGORY_DESCRIPTIONS: Record = { * then summarize user requirements and clarify uncertainties before proceeding. * Also MANDATES dependency graphs, parallel execution analysis, and category+skill recommendations. */ -export const PLAN_AGENT_SYSTEM_PREPEND = ` +export const PLAN_AGENT_SYSTEM_PREPEND_STATIC_BEFORE_SKILLS = ` BEFORE you begin planning, you MUST first understand the user's request deeply. MANDATORY CONTEXT GATHERING PROTOCOL: @@ -337,39 +341,9 @@ WHY THIS MATTERS: FOR EVERY TASK, YOU MUST RECOMMEND: 1. Which CATEGORY to use for delegation 2. Which SKILLS to load for the delegated agent +` -### AVAILABLE CATEGORIES - -| Category | Best For | Model | -|----------|----------|-------| -| \`visual-engineering\` | Frontend, UI/UX, design, styling, animation | google/gemini-3-pro | -| \`ultrabrain\` | Complex architecture, deep logical reasoning | openai/gpt-5.3-codex | -| \`artistry\` | Highly creative/artistic tasks, novel ideas | google/gemini-3-pro | -| \`quick\` | Trivial tasks - single file, typo fixes | anthropic/claude-haiku-4-5 | -| \`unspecified-low\` | Moderate effort, doesn't fit other categories | anthropic/claude-sonnet-4-5 | -| \`unspecified-high\` | High effort, doesn't fit other categories | anthropic/claude-opus-4-6 | -| \`writing\` | Documentation, prose, technical writing | google/gemini-3-flash | - -### AVAILABLE SKILLS (ALWAYS EVALUATE ALL) - -Skills inject specialized expertise into the delegated agent. -YOU MUST evaluate EVERY skill and justify inclusions/omissions. - -| Skill | Domain | -|-------|--------| -| \`agent-browser\` | Browser automation, web testing | -| \`frontend-ui-ux\` | Stunning UI/UX design | -| \`git-master\` | Atomic commits, git operations | -| \`dev-browser\` | Persistent browser state automation | -| \`typescript-programmer\` | Production TypeScript code | -| \`python-programmer\` | Production Python code | -| \`svelte-programmer\` | Svelte components | -| \`golang-tui-programmer\` | Go TUI with Charmbracelet | -| \`python-debugger\` | Interactive Python debugging | -| \`data-scientist\` | DuckDB/Polars data processing | -| \`prompt-engineer\` | AI prompt optimization | - -### REQUIRED OUTPUT FORMAT +export const PLAN_AGENT_SYSTEM_PREPEND_STATIC_AFTER_SKILLS = `### REQUIRED OUTPUT FORMAT For EACH task, include a recommendation block: @@ -508,6 +482,58 @@ WHY THIS FORMAT IS MANDATORY: ` +function renderPlanAgentCategoryRows(categories: AvailableCategory[]): string[] { + const sorted = [...categories].sort((a, b) => a.name.localeCompare(b.name)) + return sorted.map((category) => { + const bestFor = category.description || category.name + const model = category.model || "" + return `| \`${category.name}\` | ${bestFor} | ${model} |` + }) +} + +function renderPlanAgentSkillRows(skills: AvailableSkill[]): string[] { + const sorted = [...skills].sort((a, b) => a.name.localeCompare(b.name)) + return sorted.map((skill) => { + const firstSentence = skill.description.split(".")[0] || skill.description + const domain = firstSentence.trim() || skill.name + return `| \`${skill.name}\` | ${domain} |` + }) +} + +export function buildPlanAgentSkillsSection( + categories: AvailableCategory[] = [], + skills: AvailableSkill[] = [] +): string { + const categoryRows = renderPlanAgentCategoryRows(categories) + const skillRows = renderPlanAgentSkillRows(skills) + + return `### AVAILABLE CATEGORIES + +| Category | Best For | Model | +|----------|----------|-------| +${categoryRows.join("\n")} + +### AVAILABLE SKILLS (ALWAYS EVALUATE ALL) + +Skills inject specialized expertise into the delegated agent. +YOU MUST evaluate EVERY skill and justify inclusions/omissions. + +| Skill | Domain | +|-------|--------| +${skillRows.join("\n")}` +} + +export function buildPlanAgentSystemPrepend( + categories: AvailableCategory[] = [], + skills: AvailableSkill[] = [] +): string { + return [ + PLAN_AGENT_SYSTEM_PREPEND_STATIC_BEFORE_SKILLS, + buildPlanAgentSkillsSection(categories, skills), + PLAN_AGENT_SYSTEM_PREPEND_STATIC_AFTER_SKILLS, + ].join("\n\n") +} + /** * List of agent names that should be treated as plan agents. * Case-insensitive matching is used. @@ -524,4 +550,3 @@ export function isPlanAgent(agentName: string | undefined): boolean { const lowerName = agentName.toLowerCase().trim() return PLAN_AGENT_NAMES.some(name => lowerName === name || lowerName.includes(name)) } - diff --git a/src/tools/delegate-task/prompt-builder.ts b/src/tools/delegate-task/prompt-builder.ts index 6e63bc11c..51d32366a 100644 --- a/src/tools/delegate-task/prompt-builder.ts +++ b/src/tools/delegate-task/prompt-builder.ts @@ -1,14 +1,22 @@ -import { PLAN_AGENT_SYSTEM_PREPEND, isPlanAgent } from "./constants" import type { BuildSystemContentInput } from "./types" +import { buildPlanAgentSystemPrepend, isPlanAgent } from "./constants" /** * Build the system content to inject into the agent prompt. * Combines skill content, category prompt append, and plan agent system prepend. */ export function buildSystemContent(input: BuildSystemContentInput): string | undefined { - const { skillContent, categoryPromptAppend, agentName } = input + const { + skillContent, + categoryPromptAppend, + agentName, + availableCategories, + availableSkills, + } = input - const planAgentPrepend = isPlanAgent(agentName) ? PLAN_AGENT_SYSTEM_PREPEND : "" + const planAgentPrepend = isPlanAgent(agentName) + ? buildPlanAgentSystemPrepend(availableCategories, availableSkills) + : "" if (!skillContent && !categoryPromptAppend && !planAgentPrepend) { return undefined diff --git a/src/tools/delegate-task/tools.test.ts b/src/tools/delegate-task/tools.test.ts index effc0bf40..3eabc1bbb 100644 --- a/src/tools/delegate-task/tools.test.ts +++ b/src/tools/delegate-task/tools.test.ts @@ -1994,56 +1994,137 @@ describe("sisyphus-task", () => { test("prepends plan agent system prompt when agentName is 'plan'", () => { // given const { buildSystemContent } = require("./tools") - const { PLAN_AGENT_SYSTEM_PREPEND } = require("./constants") + const { buildPlanAgentSystemPrepend } = require("./constants") + + const availableCategories = [ + { + name: "deep", + description: "Goal-oriented autonomous problem-solving", + model: "openai/gpt-5.3-codex", + }, + ] + const availableSkills = [ + { + name: "typescript-programmer", + description: "Production TypeScript code.", + location: "plugin", + }, + ] // when - const result = buildSystemContent({ agentName: "plan" }) + const result = buildSystemContent({ + agentName: "plan", + availableCategories, + availableSkills, + }) // then expect(result).toContain("") expect(result).toContain("MANDATORY CONTEXT GATHERING PROTOCOL") - expect(result).toBe(PLAN_AGENT_SYSTEM_PREPEND) + expect(result).toContain("### AVAILABLE CATEGORIES") + expect(result).toContain("`deep`") + expect(result).not.toContain("prompt-engineer") + expect(result).toBe(buildPlanAgentSystemPrepend(availableCategories, availableSkills)) }) test("prepends plan agent system prompt when agentName is 'prometheus'", () => { // given const { buildSystemContent } = require("./tools") - const { PLAN_AGENT_SYSTEM_PREPEND } = require("./constants") + const { buildPlanAgentSystemPrepend } = require("./constants") + + const availableCategories = [ + { + name: "ultrabrain", + description: "Complex architecture, deep logical reasoning", + model: "openai/gpt-5.3-codex", + }, + ] + const availableSkills = [ + { + name: "git-master", + description: "Atomic commits, git operations.", + location: "plugin", + }, + ] // when - const result = buildSystemContent({ agentName: "prometheus" }) + const result = buildSystemContent({ + agentName: "prometheus", + availableCategories, + availableSkills, + }) // then expect(result).toContain("") - expect(result).toBe(PLAN_AGENT_SYSTEM_PREPEND) + expect(result).toBe(buildPlanAgentSystemPrepend(availableCategories, availableSkills)) }) test("prepends plan agent system prompt when agentName is 'Prometheus' (case insensitive)", () => { // given const { buildSystemContent } = require("./tools") - const { PLAN_AGENT_SYSTEM_PREPEND } = require("./constants") + const { buildPlanAgentSystemPrepend } = require("./constants") + + const availableCategories = [ + { + name: "quick", + description: "Trivial tasks", + model: "anthropic/claude-haiku-4-5", + }, + ] + const availableSkills = [ + { + name: "dev-browser", + description: "Persistent browser state automation.", + location: "plugin", + }, + ] // when - const result = buildSystemContent({ agentName: "Prometheus" }) + const result = buildSystemContent({ + agentName: "Prometheus", + availableCategories, + availableSkills, + }) // then expect(result).toContain("") - expect(result).toBe(PLAN_AGENT_SYSTEM_PREPEND) + expect(result).toBe(buildPlanAgentSystemPrepend(availableCategories, availableSkills)) }) test("combines plan agent prepend with skill content", () => { // given const { buildSystemContent } = require("./tools") - const { PLAN_AGENT_SYSTEM_PREPEND } = require("./constants") + const { buildPlanAgentSystemPrepend } = require("./constants") const skillContent = "You are a planning expert" + const availableCategories = [ + { + name: "writing", + description: "Documentation, prose, technical writing", + model: "google/gemini-3-flash", + }, + ] + const availableSkills = [ + { + name: "python-programmer", + description: "Production Python code.", + location: "plugin", + }, + ] + const planPrepend = buildPlanAgentSystemPrepend(availableCategories, availableSkills) + // when - const result = buildSystemContent({ skillContent, agentName: "plan" }) + const result = buildSystemContent({ + skillContent, + agentName: "plan", + availableCategories, + availableSkills, + }) // then - expect(result).toContain(PLAN_AGENT_SYSTEM_PREPEND) + expect(result).toContain(planPrepend) expect(result).toContain(skillContent) - expect(result!.indexOf(PLAN_AGENT_SYSTEM_PREPEND)).toBeLessThan(result!.indexOf(skillContent)) + expect(result!.indexOf(planPrepend)).toBeLessThan(result!.indexOf(skillContent)) }) test("does not prepend plan agent prompt for non-plan agents", () => { diff --git a/src/tools/delegate-task/tools.ts b/src/tools/delegate-task/tools.ts index 0cf1d4ff1..b6b0ea543 100644 --- a/src/tools/delegate-task/tools.ts +++ b/src/tools/delegate-task/tools.ts @@ -3,6 +3,10 @@ import type { DelegateTaskArgs, ToolContextWithMetadata, DelegateTaskToolOptions import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "./constants" import { log } from "../../shared" import { buildSystemContent } from "./prompt-builder" +import type { + AvailableCategory, + AvailableSkill, +} from "../../agents/dynamic-agent-prompt-builder" import { resolveSkillContent, resolveParentContext, @@ -26,6 +30,20 @@ export function createDelegateTask(options: DelegateTaskToolOptions): ToolDefini const categoryNames = Object.keys(allCategories) const categoryExamples = categoryNames.map(k => `'${k}'`).join(", ") + const availableCategories: AvailableCategory[] = options.availableCategories + ?? Object.entries(allCategories).map(([name, categoryConfig]) => { + const userDesc = userCategories?.[name]?.description + const builtinDesc = CATEGORY_DESCRIPTIONS[name] + const description = userDesc || builtinDesc || "General tasks" + return { + name, + description, + model: categoryConfig.model, + } + }) + + const availableSkills: AvailableSkill[] = options.availableSkills ?? [] + const categoryList = categoryNames.map(name => { const userDesc = userCategories?.[name]?.description const builtinDesc = CATEGORY_DESCRIPTIONS[name] @@ -150,7 +168,13 @@ Prompts MUST be in English.` }) if (isUnstableAgent && isRunInBackgroundExplicitlyFalse) { - const systemContent = buildSystemContent({ skillContent, categoryPromptAppend, agentName: agentToUse }) + const systemContent = buildSystemContent({ + skillContent, + categoryPromptAppend, + agentName: agentToUse, + availableCategories, + availableSkills, + }) return executeUnstableAgentTask(args, ctx, options, parentContext, agentToUse, categoryModel, systemContent, actualModel) } } else { @@ -162,7 +186,13 @@ Prompts MUST be in English.` categoryModel = resolution.categoryModel } - const systemContent = buildSystemContent({ skillContent, categoryPromptAppend, agentName: agentToUse }) + const systemContent = buildSystemContent({ + skillContent, + categoryPromptAppend, + agentName: agentToUse, + availableCategories, + availableSkills, + }) if (runInBackground) { return executeBackgroundTask(args, ctx, options, parentContext, agentToUse, categoryModel, systemContent) diff --git a/src/tools/delegate-task/types.ts b/src/tools/delegate-task/types.ts index 0b82836d5..e7892fc8f 100644 --- a/src/tools/delegate-task/types.ts +++ b/src/tools/delegate-task/types.ts @@ -1,6 +1,10 @@ import type { PluginInput } from "@opencode-ai/plugin" import type { BackgroundManager } from "../../features/background-agent" import type { CategoriesConfig, GitMasterConfig, BrowserAutomationProvider } from "../../config/schema" +import type { + AvailableCategory, + AvailableSkill, +} from "../../agents/dynamic-agent-prompt-builder" export type OpencodeClient = PluginInput["client"] @@ -42,6 +46,8 @@ export interface DelegateTaskToolOptions { sisyphusJuniorModel?: string browserProvider?: BrowserAutomationProvider disabledSkills?: Set + availableCategories?: AvailableCategory[] + availableSkills?: AvailableSkill[] onSyncSessionCreated?: (event: SyncSessionCreatedEvent) => Promise } @@ -49,4 +55,6 @@ export interface BuildSystemContentInput { skillContent?: string categoryPromptAppend?: string agentName?: string + availableCategories?: AvailableCategory[] + availableSkills?: AvailableSkill[] }