feat(categories): add disable field to CategoryConfigSchema
Allow individual categories to be disabled via `disable: true` in config. Introduce shared `mergeCategories()` utility to centralize category merging and disabled filtering across all 7 consumption sites.
This commit is contained in:
parent
67b4665c28
commit
bfe1730e9f
@ -2,7 +2,7 @@ import type { AgentConfig } from "@opencode-ai/sdk"
|
|||||||
import type { AgentFactory } from "./types"
|
import type { AgentFactory } from "./types"
|
||||||
import type { CategoriesConfig, CategoryConfig, GitMasterConfig } from "../config/schema"
|
import type { CategoriesConfig, CategoryConfig, GitMasterConfig } from "../config/schema"
|
||||||
import type { BrowserAutomationProvider } from "../config/schema"
|
import type { BrowserAutomationProvider } from "../config/schema"
|
||||||
import { DEFAULT_CATEGORIES } from "../tools/delegate-task/constants"
|
import { mergeCategories } from "../shared/merge-categories"
|
||||||
import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content"
|
import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content"
|
||||||
|
|
||||||
export type AgentSource = AgentFactory | AgentConfig
|
export type AgentSource = AgentFactory | AgentConfig
|
||||||
@ -20,9 +20,7 @@ export function buildAgent(
|
|||||||
disabledSkills?: Set<string>
|
disabledSkills?: Set<string>
|
||||||
): AgentConfig {
|
): AgentConfig {
|
||||||
const base = isFactory(source) ? source(model) : { ...source }
|
const base = isFactory(source) ? source(model) : { ...source }
|
||||||
const categoryConfigs: Record<string, CategoryConfig> = categories
|
const categoryConfigs: Record<string, CategoryConfig> = mergeCategories(categories)
|
||||||
? { ...DEFAULT_CATEGORIES, ...categories }
|
|
||||||
: DEFAULT_CATEGORIES
|
|
||||||
|
|
||||||
const agentWithCategory = base as AgentConfig & { category?: string; skills?: string[]; variant?: string }
|
const agentWithCategory = base as AgentConfig & { category?: string; skills?: string[]; variant?: string }
|
||||||
if (agentWithCategory.category) {
|
if (agentWithCategory.category) {
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { isGptModel } from "../types"
|
|||||||
import type { AvailableAgent, AvailableSkill, AvailableCategory } from "../dynamic-agent-prompt-builder"
|
import type { AvailableAgent, AvailableSkill, AvailableCategory } from "../dynamic-agent-prompt-builder"
|
||||||
import { buildCategorySkillsDelegationGuide } from "../dynamic-agent-prompt-builder"
|
import { buildCategorySkillsDelegationGuide } from "../dynamic-agent-prompt-builder"
|
||||||
import type { CategoryConfig } from "../../config/schema"
|
import type { CategoryConfig } from "../../config/schema"
|
||||||
import { DEFAULT_CATEGORIES } from "../../tools/delegate-task/constants"
|
import { mergeCategories } from "../../shared/merge-categories"
|
||||||
import { createAgentToolRestrictions } from "../../shared/permission-compat"
|
import { createAgentToolRestrictions } from "../../shared/permission-compat"
|
||||||
|
|
||||||
import { getDefaultAtlasPrompt } from "./default"
|
import { getDefaultAtlasPrompt } from "./default"
|
||||||
@ -70,7 +70,7 @@ function buildDynamicOrchestratorPrompt(ctx?: OrchestratorContext): string {
|
|||||||
const userCategories = ctx?.userCategories
|
const userCategories = ctx?.userCategories
|
||||||
const model = ctx?.model
|
const model = ctx?.model
|
||||||
|
|
||||||
const allCategories = { ...DEFAULT_CATEGORIES, ...userCategories }
|
const allCategories = mergeCategories(userCategories)
|
||||||
const availableCategories: AvailableCategory[] = Object.entries(allCategories).map(([name]) => ({
|
const availableCategories: AvailableCategory[] = Object.entries(allCategories).map(([name]) => ({
|
||||||
name,
|
name,
|
||||||
description: getCategoryDescription(name, userCategories),
|
description: getCategoryDescription(name, userCategories),
|
||||||
|
|||||||
@ -7,7 +7,8 @@
|
|||||||
|
|
||||||
import type { CategoryConfig } from "../../config/schema"
|
import type { CategoryConfig } from "../../config/schema"
|
||||||
import { formatCustomSkillsBlock, type AvailableAgent, type AvailableSkill } from "../dynamic-agent-prompt-builder"
|
import { formatCustomSkillsBlock, type AvailableAgent, type AvailableSkill } from "../dynamic-agent-prompt-builder"
|
||||||
import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "../../tools/delegate-task/constants"
|
import { CATEGORY_DESCRIPTIONS } from "../../tools/delegate-task/constants"
|
||||||
|
import { mergeCategories } from "../../shared/merge-categories"
|
||||||
import { truncateDescription } from "../../shared/truncate-description"
|
import { truncateDescription } from "../../shared/truncate-description"
|
||||||
|
|
||||||
export const getCategoryDescription = (name: string, userCategories?: Record<string, CategoryConfig>) =>
|
export const getCategoryDescription = (name: string, userCategories?: Record<string, CategoryConfig>) =>
|
||||||
@ -33,7 +34,7 @@ ${rows.join("\n")}`
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildCategorySection(userCategories?: Record<string, CategoryConfig>): string {
|
export function buildCategorySection(userCategories?: Record<string, CategoryConfig>): string {
|
||||||
const allCategories = { ...DEFAULT_CATEGORIES, ...userCategories }
|
const allCategories = mergeCategories(userCategories)
|
||||||
const categoryRows = Object.entries(allCategories).map(([name, config]) => {
|
const categoryRows = Object.entries(allCategories).map(([name, config]) => {
|
||||||
const temp = config.temperature ?? 0.5
|
const temp = config.temperature ?? 0.5
|
||||||
return `| \`${name}\` | ${temp} | ${getCategoryDescription(name, userCategories)} |`
|
return `| \`${name}\` | ${temp} | ${getCategoryDescription(name, userCategories)} |`
|
||||||
@ -116,7 +117,7 @@ task(category="[category]", load_skills=["skill-1", "skill-2"], run_in_backgroun
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildDecisionMatrix(agents: AvailableAgent[], userCategories?: Record<string, CategoryConfig>): string {
|
export function buildDecisionMatrix(agents: AvailableAgent[], userCategories?: Record<string, CategoryConfig>): string {
|
||||||
const allCategories = { ...DEFAULT_CATEGORIES, ...userCategories }
|
const allCategories = mergeCategories(userCategories)
|
||||||
|
|
||||||
const categoryRows = Object.entries(allCategories).map(([name]) =>
|
const categoryRows = Object.entries(allCategories).map(([name]) =>
|
||||||
`| ${getCategoryDescription(name, userCategories)} | \`category="${name}", load_skills=[...]\` |`
|
`| ${getCategoryDescription(name, userCategories)} | \`category="${name}", load_skills=[...]\` |`
|
||||||
|
|||||||
@ -14,7 +14,8 @@ import { createMomusAgent, momusPromptMetadata } from "./momus"
|
|||||||
import { createHephaestusAgent } from "./hephaestus"
|
import { createHephaestusAgent } from "./hephaestus"
|
||||||
import type { AvailableCategory } from "./dynamic-agent-prompt-builder"
|
import type { AvailableCategory } from "./dynamic-agent-prompt-builder"
|
||||||
import { fetchAvailableModels, readConnectedProvidersCache } from "../shared"
|
import { fetchAvailableModels, readConnectedProvidersCache } from "../shared"
|
||||||
import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "../tools/delegate-task/constants"
|
import { CATEGORY_DESCRIPTIONS } from "../tools/delegate-task/constants"
|
||||||
|
import { mergeCategories } from "../shared/merge-categories"
|
||||||
import { buildAvailableSkills } from "./builtin-agents/available-skills"
|
import { buildAvailableSkills } from "./builtin-agents/available-skills"
|
||||||
import { collectPendingBuiltinAgents } from "./builtin-agents/general-agents"
|
import { collectPendingBuiltinAgents } from "./builtin-agents/general-agents"
|
||||||
import { maybeCreateSisyphusConfig } from "./builtin-agents/sisyphus-agent"
|
import { maybeCreateSisyphusConfig } from "./builtin-agents/sisyphus-agent"
|
||||||
@ -78,9 +79,7 @@ export async function createBuiltinAgents(
|
|||||||
|
|
||||||
const result: Record<string, AgentConfig> = {}
|
const result: Record<string, AgentConfig> = {}
|
||||||
|
|
||||||
const mergedCategories = categories
|
const mergedCategories = mergeCategories(categories)
|
||||||
? { ...DEFAULT_CATEGORIES, ...categories }
|
|
||||||
: DEFAULT_CATEGORIES
|
|
||||||
|
|
||||||
const availableCategories: AvailableCategory[] = Object.entries(mergedCategories).map(([name]) => ({
|
const availableCategories: AvailableCategory[] = Object.entries(mergedCategories).map(([name]) => ({
|
||||||
name,
|
name,
|
||||||
|
|||||||
@ -20,6 +20,8 @@ export const CategoryConfigSchema = z.object({
|
|||||||
prompt_append: z.string().optional(),
|
prompt_append: z.string().optional(),
|
||||||
/** Mark agent as unstable - forces background mode for monitoring. Auto-enabled for gemini/minimax models. */
|
/** Mark agent as unstable - forces background mode for monitoring. Auto-enabled for gemini/minimax models. */
|
||||||
is_unstable_agent: z.boolean().optional(),
|
is_unstable_agent: z.boolean().optional(),
|
||||||
|
/** Disable this category. Disabled categories are excluded from task delegation. */
|
||||||
|
disable: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const BuiltinCategoryNameSchema = z.enum([
|
export const BuiltinCategoryNameSchema = z.enum([
|
||||||
|
|||||||
@ -1,19 +1,14 @@
|
|||||||
import type { AvailableCategory } from "../agents/dynamic-agent-prompt-builder"
|
import type { AvailableCategory } from "../agents/dynamic-agent-prompt-builder"
|
||||||
import type { OhMyOpenCodeConfig } from "../config"
|
import type { OhMyOpenCodeConfig } from "../config"
|
||||||
|
import { CATEGORY_DESCRIPTIONS } from "../tools/delegate-task/constants"
|
||||||
import {
|
import { mergeCategories } from "../shared/merge-categories"
|
||||||
CATEGORY_DESCRIPTIONS,
|
|
||||||
DEFAULT_CATEGORIES,
|
|
||||||
} from "../tools/delegate-task/constants"
|
|
||||||
|
|
||||||
export function createAvailableCategories(
|
export function createAvailableCategories(
|
||||||
pluginConfig: OhMyOpenCodeConfig,
|
pluginConfig: OhMyOpenCodeConfig,
|
||||||
): AvailableCategory[] {
|
): AvailableCategory[] {
|
||||||
const mergedCategories = pluginConfig.categories
|
const categories = mergeCategories(pluginConfig.categories)
|
||||||
? { ...DEFAULT_CATEGORIES, ...pluginConfig.categories }
|
|
||||||
: DEFAULT_CATEGORIES
|
|
||||||
|
|
||||||
return Object.entries(mergedCategories).map(([name, categoryConfig]) => {
|
return Object.entries(categories).map(([name, categoryConfig]) => {
|
||||||
const model =
|
const model =
|
||||||
typeof categoryConfig.model === "string" ? categoryConfig.model : undefined
|
typeof categoryConfig.model === "string" ? categoryConfig.model : undefined
|
||||||
|
|
||||||
|
|||||||
84
src/shared/merge-categories.test.ts
Normal file
84
src/shared/merge-categories.test.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { describe, it, expect } from "bun:test"
|
||||||
|
import { mergeCategories } from "./merge-categories"
|
||||||
|
import { DEFAULT_CATEGORIES } from "../tools/delegate-task/constants"
|
||||||
|
|
||||||
|
describe("mergeCategories", () => {
|
||||||
|
it("returns all default categories when no user config provided", () => {
|
||||||
|
//#given
|
||||||
|
const userCategories = undefined
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = mergeCategories(userCategories)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(Object.keys(result)).toEqual(Object.keys(DEFAULT_CATEGORIES))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("filters out categories with disable: true", () => {
|
||||||
|
//#given
|
||||||
|
const userCategories = {
|
||||||
|
"quick": { disable: true },
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = mergeCategories(userCategories)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result["quick"]).toBeUndefined()
|
||||||
|
expect(Object.keys(result).length).toBe(Object.keys(DEFAULT_CATEGORIES).length - 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("keeps categories with disable: false", () => {
|
||||||
|
//#given
|
||||||
|
const userCategories = {
|
||||||
|
"quick": { disable: false },
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = mergeCategories(userCategories)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result["quick"]).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("allows user to add custom categories", () => {
|
||||||
|
//#given
|
||||||
|
const userCategories = {
|
||||||
|
"my-custom": { model: "openai/gpt-5.2", description: "Custom category" },
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = mergeCategories(userCategories)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result["my-custom"]).toBeDefined()
|
||||||
|
expect(result["my-custom"].model).toBe("openai/gpt-5.2")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("allows user to disable custom categories", () => {
|
||||||
|
//#given
|
||||||
|
const userCategories = {
|
||||||
|
"my-custom": { model: "openai/gpt-5.2", disable: true },
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = mergeCategories(userCategories)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result["my-custom"]).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("user overrides merge with defaults", () => {
|
||||||
|
//#given
|
||||||
|
const userCategories = {
|
||||||
|
"ultrabrain": { model: "anthropic/claude-opus-4-6" },
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = mergeCategories(userCategories)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result["ultrabrain"]).toBeDefined()
|
||||||
|
expect(result["ultrabrain"].model).toBe("anthropic/claude-opus-4-6")
|
||||||
|
})
|
||||||
|
})
|
||||||
18
src/shared/merge-categories.ts
Normal file
18
src/shared/merge-categories.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { CategoriesConfig, CategoryConfig } from "../config/schema"
|
||||||
|
import { DEFAULT_CATEGORIES } from "../tools/delegate-task/constants"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge default and user categories, filtering out disabled ones.
|
||||||
|
* Single source of truth for category merging across the codebase.
|
||||||
|
*/
|
||||||
|
export function mergeCategories(
|
||||||
|
userCategories?: CategoriesConfig,
|
||||||
|
): Record<string, CategoryConfig> {
|
||||||
|
const merged = userCategories
|
||||||
|
? { ...DEFAULT_CATEGORIES, ...userCategories }
|
||||||
|
: { ...DEFAULT_CATEGORIES }
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(merged).filter(([, config]) => !config.disable),
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -32,7 +32,10 @@ export function resolveCategoryConfig(
|
|||||||
const userConfig = userCategories?.[categoryName]
|
const userConfig = userCategories?.[categoryName]
|
||||||
const hasExplicitUserConfig = userConfig !== undefined
|
const hasExplicitUserConfig = userConfig !== undefined
|
||||||
|
|
||||||
// Check if category requires a specific model - bypass if user explicitly provides config
|
if (userConfig?.disable) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const categoryReq = CATEGORY_MODEL_REQUIREMENTS[categoryName]
|
const categoryReq = CATEGORY_MODEL_REQUIREMENTS[categoryName]
|
||||||
if (categoryReq?.requiresModel && availableModels && !hasExplicitUserConfig) {
|
if (categoryReq?.requiresModel && availableModels && !hasExplicitUserConfig) {
|
||||||
if (!isModelAvailable(categoryReq.requiresModel, availableModels)) {
|
if (!isModelAvailable(categoryReq.requiresModel, availableModels)) {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { ModelFallbackInfo } from "../../features/task-toast-manager/types"
|
import type { ModelFallbackInfo } from "../../features/task-toast-manager/types"
|
||||||
import type { DelegateTaskArgs } from "./types"
|
import type { DelegateTaskArgs } from "./types"
|
||||||
import type { ExecutorContext } from "./executor-types"
|
import type { ExecutorContext } from "./executor-types"
|
||||||
import { DEFAULT_CATEGORIES } from "./constants"
|
import { mergeCategories } from "../../shared/merge-categories"
|
||||||
import { SISYPHUS_JUNIOR_AGENT } from "./sisyphus-junior-agent"
|
import { SISYPHUS_JUNIOR_AGENT } from "./sisyphus-junior-agent"
|
||||||
import { resolveCategoryConfig } from "./categories"
|
import { resolveCategoryConfig } from "./categories"
|
||||||
import { parseModelString } from "./model-string-parser"
|
import { parseModelString } from "./model-string-parser"
|
||||||
@ -30,7 +30,8 @@ export async function resolveCategoryExecution(
|
|||||||
const availableModels = await getAvailableModelsForDelegateTask(client)
|
const availableModels = await getAvailableModelsForDelegateTask(client)
|
||||||
|
|
||||||
const categoryName = args.category!
|
const categoryName = args.category!
|
||||||
const categoryExists = DEFAULT_CATEGORIES[categoryName] !== undefined || userCategories?.[categoryName] !== undefined
|
const enabledCategories = mergeCategories(userCategories)
|
||||||
|
const categoryExists = enabledCategories[categoryName] !== undefined
|
||||||
|
|
||||||
const resolved = resolveCategoryConfig(categoryName, {
|
const resolved = resolveCategoryConfig(categoryName, {
|
||||||
userCategories,
|
userCategories,
|
||||||
@ -41,7 +42,7 @@ export async function resolveCategoryExecution(
|
|||||||
|
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
const requirement = CATEGORY_MODEL_REQUIREMENTS[categoryName]
|
const requirement = CATEGORY_MODEL_REQUIREMENTS[categoryName]
|
||||||
const allCategoryNames = Object.keys({ ...DEFAULT_CATEGORIES, ...userCategories }).join(", ")
|
const allCategoryNames = Object.keys(enabledCategories).join(", ")
|
||||||
|
|
||||||
if (categoryExists && requirement?.requiresModel) {
|
if (categoryExists && requirement?.requiresModel) {
|
||||||
return {
|
return {
|
||||||
@ -146,7 +147,7 @@ Available categories: ${allCategoryNames}`,
|
|||||||
const categoryPromptAppend = resolved.promptAppend || undefined
|
const categoryPromptAppend = resolved.promptAppend || undefined
|
||||||
|
|
||||||
if (!categoryModel && !actualModel) {
|
if (!categoryModel && !actualModel) {
|
||||||
const categoryNames = Object.keys({ ...DEFAULT_CATEGORIES, ...userCategories })
|
const categoryNames = Object.keys(enabledCategories)
|
||||||
return {
|
return {
|
||||||
agentToUse: "",
|
agentToUse: "",
|
||||||
categoryModel: undefined,
|
categoryModel: undefined,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin"
|
import { tool, type ToolDefinition } from "@opencode-ai/plugin"
|
||||||
import type { DelegateTaskArgs, ToolContextWithMetadata, DelegateTaskToolOptions } from "./types"
|
import type { DelegateTaskArgs, ToolContextWithMetadata, DelegateTaskToolOptions } from "./types"
|
||||||
import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "./constants"
|
import { CATEGORY_DESCRIPTIONS } from "./constants"
|
||||||
|
import { mergeCategories } from "../../shared/merge-categories"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
import { buildSystemContent } from "./prompt-builder"
|
import { buildSystemContent } from "./prompt-builder"
|
||||||
import type {
|
import type {
|
||||||
@ -26,7 +27,7 @@ export { buildSystemContent } from "./prompt-builder"
|
|||||||
export function createDelegateTask(options: DelegateTaskToolOptions): ToolDefinition {
|
export function createDelegateTask(options: DelegateTaskToolOptions): ToolDefinition {
|
||||||
const { userCategories } = options
|
const { userCategories } = options
|
||||||
|
|
||||||
const allCategories = { ...DEFAULT_CATEGORIES, ...userCategories }
|
const allCategories = mergeCategories(userCategories)
|
||||||
const categoryNames = Object.keys(allCategories)
|
const categoryNames = Object.keys(allCategories)
|
||||||
const categoryExamples = categoryNames.map(k => `'${k}'`).join(", ")
|
const categoryExamples = categoryNames.map(k => `'${k}'`).join(", ")
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user