fix(delegate-task): make plan agent categories/skills dynamic
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -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 = `<Category_Context>
|
||||
You are working on VISUAL/UI tasks.
|
||||
@@ -231,7 +235,7 @@ export const CATEGORY_DESCRIPTIONS: Record<string, string> = {
|
||||
* 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 = `<system>
|
||||
export const PLAN_AGENT_SYSTEM_PREPEND_STATIC_BEFORE_SKILLS = `<system>
|
||||
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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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("<system>")
|
||||
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("<system>")
|
||||
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("<system>")
|
||||
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", () => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<string>
|
||||
availableCategories?: AvailableCategory[]
|
||||
availableSkills?: AvailableSkill[]
|
||||
onSyncSessionCreated?: (event: SyncSessionCreatedEvent) => Promise<void>
|
||||
}
|
||||
|
||||
@@ -49,4 +55,6 @@ export interface BuildSystemContentInput {
|
||||
skillContent?: string
|
||||
categoryPromptAppend?: string
|
||||
agentName?: string
|
||||
availableCategories?: AvailableCategory[]
|
||||
availableSkills?: AvailableSkill[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user