Skip to content

Commit 728eaae

Browse files
authored
Merge pull request #1551 from code-yeongyu/fix/plan-agent-dynamic-skills
fix(delegate-task): make plan agent categories/skills dynamic
2 parents 9271f82 + 6b560eb commit 728eaae

File tree

7 files changed

+268
-79
lines changed

7 files changed

+268
-79
lines changed

src/agents/dynamic-agent-prompt-builder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface AvailableSkill {
2020
export interface AvailableCategory {
2121
name: string
2222
description: string
23+
model?: string
2324
}
2425

2526
export function categorizeTools(toolNames: string[]): AvailableTool[] {

src/index.ts

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ import {
5656
discoverOpencodeProjectSkills,
5757
mergeSkills,
5858
} from "./features/opencode-skill-loader";
59+
import type { SkillScope } from "./features/opencode-skill-loader/types";
5960
import { createBuiltinSkills } from "./features/builtin-skills";
6061
import { getSystemMcpServerNames } from "./features/claude-code-mcp-loader";
62+
import type { AvailableSkill } from "./agents/dynamic-agent-prompt-builder";
6163
import {
6264
setMainSession,
6365
getMainSessionID,
@@ -84,6 +86,10 @@ import {
8486
createTaskList,
8587
createTaskUpdateTool,
8688
} from "./tools";
89+
import {
90+
CATEGORY_DESCRIPTIONS,
91+
DEFAULT_CATEGORIES,
92+
} from "./tools/delegate-task/constants";
8793
import { BackgroundManager } from "./features/background-agent";
8894
import { SkillMcpManager } from "./features/skill-mcp-manager";
8995
import { initTaskToastManager } from "./features/task-toast-manager";
@@ -394,6 +400,60 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
394400
const browserProvider =
395401
pluginConfig.browser_automation_engine?.provider ?? "playwright";
396402
const disabledSkills = new Set<string>(pluginConfig.disabled_skills ?? []);
403+
const systemMcpNames = getSystemMcpServerNames();
404+
const builtinSkills = createBuiltinSkills({ browserProvider, disabledSkills }).filter((skill) => {
405+
if (skill.mcpConfig) {
406+
for (const mcpName of Object.keys(skill.mcpConfig)) {
407+
if (systemMcpNames.has(mcpName)) return false;
408+
}
409+
}
410+
return true;
411+
},
412+
);
413+
const includeClaudeSkills = pluginConfig.claude_code?.skills !== false;
414+
const [userSkills, globalSkills, projectSkills, opencodeProjectSkills] =
415+
await Promise.all([
416+
includeClaudeSkills ? discoverUserClaudeSkills() : Promise.resolve([]),
417+
discoverOpencodeGlobalSkills(),
418+
includeClaudeSkills ? discoverProjectClaudeSkills() : Promise.resolve([]),
419+
discoverOpencodeProjectSkills(),
420+
]);
421+
const mergedSkills = mergeSkills(
422+
builtinSkills,
423+
pluginConfig.skills,
424+
userSkills,
425+
globalSkills,
426+
projectSkills,
427+
opencodeProjectSkills,
428+
);
429+
430+
function mapScopeToLocation(scope: SkillScope): AvailableSkill["location"] {
431+
if (scope === "user" || scope === "opencode") return "user";
432+
if (scope === "project" || scope === "opencode-project") return "project";
433+
return "plugin";
434+
}
435+
436+
const availableSkills: AvailableSkill[] = mergedSkills.map((skill) => ({
437+
name: skill.name,
438+
description: skill.definition.description ?? "",
439+
location: mapScopeToLocation(skill.scope),
440+
}));
441+
442+
const mergedCategories = pluginConfig.categories
443+
? { ...DEFAULT_CATEGORIES, ...pluginConfig.categories }
444+
: DEFAULT_CATEGORIES;
445+
446+
const availableCategories = Object.entries(mergedCategories).map(
447+
([name, categoryConfig]) => ({
448+
name,
449+
description:
450+
pluginConfig.categories?.[name]?.description
451+
?? CATEGORY_DESCRIPTIONS[name]
452+
?? "General tasks",
453+
model: categoryConfig.model,
454+
}),
455+
);
456+
397457
const delegateTask = createDelegateTask({
398458
manager: backgroundManager,
399459
client: ctx.client,
@@ -403,6 +463,8 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
403463
sisyphusJuniorModel: pluginConfig.agents?.["sisyphus-junior"]?.model,
404464
browserProvider,
405465
disabledSkills,
466+
availableCategories,
467+
availableSkills,
406468
onSyncSessionCreated: async (event) => {
407469
log("[index] onSyncSessionCreated callback", {
408470
sessionID: event.sessionID,
@@ -421,32 +483,6 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
421483
});
422484
},
423485
});
424-
const systemMcpNames = getSystemMcpServerNames();
425-
const builtinSkills = createBuiltinSkills({ browserProvider, disabledSkills }).filter((skill) => {
426-
if (skill.mcpConfig) {
427-
for (const mcpName of Object.keys(skill.mcpConfig)) {
428-
if (systemMcpNames.has(mcpName)) return false;
429-
}
430-
}
431-
return true;
432-
},
433-
);
434-
const includeClaudeSkills = pluginConfig.claude_code?.skills !== false;
435-
const [userSkills, globalSkills, projectSkills, opencodeProjectSkills] =
436-
await Promise.all([
437-
includeClaudeSkills ? discoverUserClaudeSkills() : Promise.resolve([]),
438-
discoverOpencodeGlobalSkills(),
439-
includeClaudeSkills ? discoverProjectClaudeSkills() : Promise.resolve([]),
440-
discoverOpencodeProjectSkills(),
441-
]);
442-
const mergedSkills = mergeSkills(
443-
builtinSkills,
444-
pluginConfig.skills,
445-
userSkills,
446-
globalSkills,
447-
projectSkills,
448-
opencodeProjectSkills,
449-
);
450486
const skillMcpManager = new SkillMcpManager();
451487
const getSessionIDForMcp = () => getMainSessionID() || "";
452488
const skillTool = createSkillTool({

src/tools/delegate-task/constants.ts

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import type { CategoryConfig } from "../../config/schema"
2+
import type {
3+
AvailableCategory,
4+
AvailableSkill,
5+
} from "../../agents/dynamic-agent-prompt-builder"
26

37
export const VISUAL_CATEGORY_PROMPT_APPEND = `<Category_Context>
48
You are working on VISUAL/UI tasks.
@@ -231,7 +235,7 @@ export const CATEGORY_DESCRIPTIONS: Record<string, string> = {
231235
* then summarize user requirements and clarify uncertainties before proceeding.
232236
* Also MANDATES dependency graphs, parallel execution analysis, and category+skill recommendations.
233237
*/
234-
export const PLAN_AGENT_SYSTEM_PREPEND = `<system>
238+
export const PLAN_AGENT_SYSTEM_PREPEND_STATIC_BEFORE_SKILLS = `<system>
235239
BEFORE you begin planning, you MUST first understand the user's request deeply.
236240
237241
MANDATORY CONTEXT GATHERING PROTOCOL:
@@ -337,39 +341,9 @@ WHY THIS MATTERS:
337341
FOR EVERY TASK, YOU MUST RECOMMEND:
338342
1. Which CATEGORY to use for delegation
339343
2. Which SKILLS to load for the delegated agent
344+
`
340345

341-
### AVAILABLE CATEGORIES
342-
343-
| Category | Best For | Model |
344-
|----------|----------|-------|
345-
| \`visual-engineering\` | Frontend, UI/UX, design, styling, animation | google/gemini-3-pro |
346-
| \`ultrabrain\` | Complex architecture, deep logical reasoning | openai/gpt-5.3-codex |
347-
| \`artistry\` | Highly creative/artistic tasks, novel ideas | google/gemini-3-pro |
348-
| \`quick\` | Trivial tasks - single file, typo fixes | anthropic/claude-haiku-4-5 |
349-
| \`unspecified-low\` | Moderate effort, doesn't fit other categories | anthropic/claude-sonnet-4-5 |
350-
| \`unspecified-high\` | High effort, doesn't fit other categories | anthropic/claude-opus-4-6 |
351-
| \`writing\` | Documentation, prose, technical writing | google/gemini-3-flash |
352-
353-
### AVAILABLE SKILLS (ALWAYS EVALUATE ALL)
354-
355-
Skills inject specialized expertise into the delegated agent.
356-
YOU MUST evaluate EVERY skill and justify inclusions/omissions.
357-
358-
| Skill | Domain |
359-
|-------|--------|
360-
| \`agent-browser\` | Browser automation, web testing |
361-
| \`frontend-ui-ux\` | Stunning UI/UX design |
362-
| \`git-master\` | Atomic commits, git operations |
363-
| \`dev-browser\` | Persistent browser state automation |
364-
| \`typescript-programmer\` | Production TypeScript code |
365-
| \`python-programmer\` | Production Python code |
366-
| \`svelte-programmer\` | Svelte components |
367-
| \`golang-tui-programmer\` | Go TUI with Charmbracelet |
368-
| \`python-debugger\` | Interactive Python debugging |
369-
| \`data-scientist\` | DuckDB/Polars data processing |
370-
| \`prompt-engineer\` | AI prompt optimization |
371-
372-
### REQUIRED OUTPUT FORMAT
346+
export const PLAN_AGENT_SYSTEM_PREPEND_STATIC_AFTER_SKILLS = `### REQUIRED OUTPUT FORMAT
373347
374348
For EACH task, include a recommendation block:
375349
@@ -508,6 +482,58 @@ WHY THIS FORMAT IS MANDATORY:
508482
509483
`
510484

485+
function renderPlanAgentCategoryRows(categories: AvailableCategory[]): string[] {
486+
const sorted = [...categories].sort((a, b) => a.name.localeCompare(b.name))
487+
return sorted.map((category) => {
488+
const bestFor = category.description || category.name
489+
const model = category.model || ""
490+
return `| \`${category.name}\` | ${bestFor} | ${model} |`
491+
})
492+
}
493+
494+
function renderPlanAgentSkillRows(skills: AvailableSkill[]): string[] {
495+
const sorted = [...skills].sort((a, b) => a.name.localeCompare(b.name))
496+
return sorted.map((skill) => {
497+
const firstSentence = skill.description.split(".")[0] || skill.description
498+
const domain = firstSentence.trim() || skill.name
499+
return `| \`${skill.name}\` | ${domain} |`
500+
})
501+
}
502+
503+
export function buildPlanAgentSkillsSection(
504+
categories: AvailableCategory[] = [],
505+
skills: AvailableSkill[] = []
506+
): string {
507+
const categoryRows = renderPlanAgentCategoryRows(categories)
508+
const skillRows = renderPlanAgentSkillRows(skills)
509+
510+
return `### AVAILABLE CATEGORIES
511+
512+
| Category | Best For | Model |
513+
|----------|----------|-------|
514+
${categoryRows.join("\n")}
515+
516+
### AVAILABLE SKILLS (ALWAYS EVALUATE ALL)
517+
518+
Skills inject specialized expertise into the delegated agent.
519+
YOU MUST evaluate EVERY skill and justify inclusions/omissions.
520+
521+
| Skill | Domain |
522+
|-------|--------|
523+
${skillRows.join("\n")}`
524+
}
525+
526+
export function buildPlanAgentSystemPrepend(
527+
categories: AvailableCategory[] = [],
528+
skills: AvailableSkill[] = []
529+
): string {
530+
return [
531+
PLAN_AGENT_SYSTEM_PREPEND_STATIC_BEFORE_SKILLS,
532+
buildPlanAgentSkillsSection(categories, skills),
533+
PLAN_AGENT_SYSTEM_PREPEND_STATIC_AFTER_SKILLS,
534+
].join("\n\n")
535+
}
536+
511537
/**
512538
* List of agent names that should be treated as plan agents.
513539
* Case-insensitive matching is used.
@@ -524,4 +550,3 @@ export function isPlanAgent(agentName: string | undefined): boolean {
524550
const lowerName = agentName.toLowerCase().trim()
525551
return PLAN_AGENT_NAMES.some(name => lowerName === name || lowerName.includes(name))
526552
}
527-

src/tools/delegate-task/prompt-builder.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1-
import { PLAN_AGENT_SYSTEM_PREPEND, isPlanAgent } from "./constants"
21
import type { BuildSystemContentInput } from "./types"
2+
import { buildPlanAgentSystemPrepend, isPlanAgent } from "./constants"
33

44
/**
55
* Build the system content to inject into the agent prompt.
66
* Combines skill content, category prompt append, and plan agent system prepend.
77
*/
88
export function buildSystemContent(input: BuildSystemContentInput): string | undefined {
9-
const { skillContent, categoryPromptAppend, agentName } = input
10-
11-
const planAgentPrepend = isPlanAgent(agentName) ? PLAN_AGENT_SYSTEM_PREPEND : ""
9+
const {
10+
skillContent,
11+
categoryPromptAppend,
12+
agentName,
13+
availableCategories,
14+
availableSkills,
15+
} = input
16+
17+
const planAgentPrepend = isPlanAgent(agentName)
18+
? buildPlanAgentSystemPrepend(availableCategories, availableSkills)
19+
: ""
1220

1321
if (!skillContent && !categoryPromptAppend && !planAgentPrepend) {
1422
return undefined

0 commit comments

Comments
 (0)