fix(delegate-task): category built-in model takes precedence over inherited model

Previously, when using categories like 'quick', the parent session's model
(e.g., Opus 4.5) would override the category's built-in model (e.g., Haiku).

Fixed priority: userConfig.model → category built-in → systemDefault

The inherited model from parent session no longer affects category-based
delegation - categories have their own explicit models.
This commit is contained in:
justsisyphus 2026-01-20 17:05:17 +09:00
parent 3e5265700b
commit 3d3d3e493b
4 changed files with 45 additions and 48 deletions

View File

@ -122,10 +122,8 @@ describe("buildAgent with category and skills", () => {
// #when // #when
const agent = buildAgent(source["test-agent"], TEST_MODEL) const agent = buildAgent(source["test-agent"], TEST_MODEL)
// #then - DEFAULT_CATEGORIES only has temperature, not model // #then - category's built-in model is applied
// Model remains undefined since neither factory nor category provides it expect(agent.model).toBe("google/gemini-3-pro-preview")
expect(agent.model).toBeUndefined()
expect(agent.temperature).toBe(0.7)
}) })
test("agent with category and existing model keeps existing model", () => { test("agent with category and existing model keeps existing model", () => {
@ -142,9 +140,8 @@ describe("buildAgent with category and skills", () => {
// #when // #when
const agent = buildAgent(source["test-agent"], TEST_MODEL) const agent = buildAgent(source["test-agent"], TEST_MODEL)
// #then // #then - explicit model takes precedence over category
expect(agent.model).toBe("custom/model") expect(agent.model).toBe("custom/model")
expect(agent.temperature).toBe(0.7)
}) })
test("agent with category inherits variant", () => { test("agent with category inherits variant", () => {
@ -247,9 +244,9 @@ describe("buildAgent with category and skills", () => {
// #when // #when
const agent = buildAgent(source["test-agent"], TEST_MODEL) const agent = buildAgent(source["test-agent"], TEST_MODEL)
// #then - DEFAULT_CATEGORIES["ultrabrain"] only has temperature, not model // #then - category's built-in model and skills are applied
expect(agent.model).toBeUndefined() expect(agent.model).toBe("openai/gpt-5.2-codex")
expect(agent.temperature).toBe(0.1) expect(agent.variant).toBe("xhigh")
expect(agent.prompt).toContain("Role: Designer-Turned-Developer") expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
expect(agent.prompt).toContain("Task description") expect(agent.prompt).toContain("Task description")
}) })

View File

@ -10,10 +10,10 @@ describe("Prometheus category config resolution", () => {
// #when // #when
const config = resolveCategoryConfig(categoryName) const config = resolveCategoryConfig(categoryName)
// #then - DEFAULT_CATEGORIES only has temperature, not model // #then
expect(config).toBeDefined() expect(config).toBeDefined()
expect(config?.model).toBeUndefined() expect(config?.model).toBe("openai/gpt-5.2-codex")
expect(config?.temperature).toBe(0.1) expect(config?.variant).toBe("xhigh")
}) })
test("resolves visual-engineering category config", () => { test("resolves visual-engineering category config", () => {
@ -23,10 +23,9 @@ describe("Prometheus category config resolution", () => {
// #when // #when
const config = resolveCategoryConfig(categoryName) const config = resolveCategoryConfig(categoryName)
// #then - DEFAULT_CATEGORIES only has temperature, not model // #then
expect(config).toBeDefined() expect(config).toBeDefined()
expect(config?.model).toBeUndefined() expect(config?.model).toBe("google/gemini-3-pro-preview")
expect(config?.temperature).toBe(0.7)
}) })
test("user categories override default categories", () => { test("user categories override default categories", () => {
@ -71,10 +70,10 @@ describe("Prometheus category config resolution", () => {
// #when // #when
const config = resolveCategoryConfig(categoryName, userCategories) const config = resolveCategoryConfig(categoryName, userCategories)
// #then - falls back to DEFAULT_CATEGORIES which has no model // #then - falls back to DEFAULT_CATEGORIES
expect(config).toBeDefined() expect(config).toBeDefined()
expect(config?.model).toBeUndefined() expect(config?.model).toBe("openai/gpt-5.2-codex")
expect(config?.temperature).toBe(0.1) expect(config?.variant).toBe("xhigh")
}) })
test("preserves all category properties (temperature, top_p, tools, etc.)", () => { test("preserves all category properties (temperature, top_p, tools, etc.)", () => {

View File

@ -226,21 +226,21 @@ describe("sisyphus-task", () => {
expect(result!.config.temperature).toBe(0.3) expect(result!.config.temperature).toBe(0.3)
}) })
test("inheritedModel takes precedence over systemDefaultModel", () => { test("category built-in model takes precedence over inheritedModel", () => {
// #given - builtin category, parent model provided // #given - builtin category with its own model, parent model also provided
const categoryName = "visual-engineering" const categoryName = "visual-engineering"
const inheritedModel = "cliproxy/claude-opus-4-5" const inheritedModel = "cliproxy/claude-opus-4-5"
// #when // #when
const result = resolveCategoryConfig(categoryName, { inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL }) const result = resolveCategoryConfig(categoryName, { inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
// #then - inheritedModel wins over systemDefaultModel // #then - category's built-in model wins over inheritedModel
expect(result).not.toBeNull() expect(result).not.toBeNull()
expect(result!.config.model).toBe("cliproxy/claude-opus-4-5") expect(result!.config.model).toBe("google/gemini-3-pro-preview")
}) })
test("inheritedModel is used as fallback when category has no user model", () => { test("systemDefaultModel is used as fallback when custom category has no model", () => {
// #given - custom category with no model defined, only inheritedModel as fallback // #given - custom category with no model defined
const categoryName = "my-custom-no-model" const categoryName = "my-custom-no-model"
const userCategories = { "my-custom-no-model": { temperature: 0.5 } } as unknown as Record<string, CategoryConfig> const userCategories = { "my-custom-no-model": { temperature: 0.5 } } as unknown as Record<string, CategoryConfig>
const inheritedModel = "cliproxy/claude-opus-4-5" const inheritedModel = "cliproxy/claude-opus-4-5"
@ -248,9 +248,9 @@ describe("sisyphus-task", () => {
// #when // #when
const result = resolveCategoryConfig(categoryName, { userCategories, inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL }) const result = resolveCategoryConfig(categoryName, { userCategories, inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
// #then - parent model is used as fallback since custom category has no user model // #then - systemDefaultModel is used since custom category has no built-in model
expect(result).not.toBeNull() expect(result).not.toBeNull()
expect(result!.config.model).toBe("cliproxy/claude-opus-4-5") expect(result!.config.model).toBe(SYSTEM_DEFAULT_MODEL)
}) })
test("user model takes precedence over inheritedModel", () => { test("user model takes precedence over inheritedModel", () => {
@ -918,18 +918,18 @@ describe("sisyphus-task", () => {
expect(resolved!.config.model).toBe("anthropic/claude-sonnet-4-5") expect(resolved!.config.model).toBe("anthropic/claude-sonnet-4-5")
}) })
test("inheritedModel takes precedence over systemDefaultModel for builtin category", () => { test("category built-in model takes precedence over inheritedModel for builtin category", () => {
// #given - builtin ultrabrain category, inherited model from parent // #given - builtin ultrabrain category with its own model, inherited model also provided
const categoryName = "ultrabrain" const categoryName = "ultrabrain"
const inheritedModel = "cliproxy/claude-opus-4-5" const inheritedModel = "cliproxy/claude-opus-4-5"
// #when // #when
const resolved = resolveCategoryConfig(categoryName, { inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL }) const resolved = resolveCategoryConfig(categoryName, { inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
// #then - inheritedModel wins over systemDefaultModel // #then - category's built-in model wins (ultrabrain uses gpt-5.2-codex)
expect(resolved).not.toBeNull() expect(resolved).not.toBeNull()
const actualModel = resolved!.config.model const actualModel = resolved!.config.model
expect(actualModel).toBe("cliproxy/claude-opus-4-5") expect(actualModel).toBe("openai/gpt-5.2-codex")
}) })
test("when user defines model - modelInfo should report user-defined regardless of inheritedModel", () => { test("when user defines model - modelInfo should report user-defined regardless of inheritedModel", () => {
@ -977,18 +977,18 @@ describe("sisyphus-task", () => {
// ===== TESTS FOR resolveModel() INTEGRATION (TDD GREEN) ===== // ===== TESTS FOR resolveModel() INTEGRATION (TDD GREEN) =====
// These tests verify the NEW behavior where categories do NOT have default models // These tests verify the NEW behavior where categories do NOT have default models
test("FIXED: inheritedModel takes precedence over systemDefaultModel", () => { test("FIXED: category built-in model takes precedence over inheritedModel", () => {
// #given a builtin category, and an inherited model from parent // #given a builtin category with its own model, and an inherited model from parent
// The NEW correct chain: userConfig?.model ?? inheritedModel ?? systemDefaultModel // The CORRECT chain: userConfig?.model ?? categoryBuiltIn ?? systemDefaultModel
const categoryName = "ultrabrain" const categoryName = "ultrabrain"
const inheritedModel = "anthropic/claude-opus-4-5" // inherited from parent session const inheritedModel = "anthropic/claude-opus-4-5"
// #when userConfig.model is undefined and inheritedModel is set // #when category has a built-in model (gpt-5.2-codex for ultrabrain)
const resolved = resolveCategoryConfig(categoryName, { inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL }) const resolved = resolveCategoryConfig(categoryName, { inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
// #then inheritedModel should be used, NOT systemDefaultModel // #then category's built-in model should be used, NOT inheritedModel
expect(resolved).not.toBeNull() expect(resolved).not.toBeNull()
expect(resolved!.model).toBe("anthropic/claude-opus-4-5") expect(resolved!.model).toBe("openai/gpt-5.2-codex")
}) })
test("FIXED: systemDefaultModel is used when no userConfig.model and no inheritedModel", () => { test("FIXED: systemDefaultModel is used when no userConfig.model and no inheritedModel", () => {
@ -1027,8 +1027,8 @@ describe("sisyphus-task", () => {
expect(resolved!.model).toBe("custom/user-model") expect(resolved!.model).toBe("custom/user-model")
}) })
test("FIXED: empty string in userConfig.model is treated as unset and falls back", () => { test("FIXED: empty string in userConfig.model is treated as unset and falls back to systemDefault", () => {
// #given userConfig.model is empty string "" // #given userConfig.model is empty string "" for a custom category (no built-in model)
const categoryName = "custom-empty-model" const categoryName = "custom-empty-model"
const userCategories = { "custom-empty-model": { model: "", temperature: 0.3 } } const userCategories = { "custom-empty-model": { model: "", temperature: 0.3 } }
const inheritedModel = "anthropic/claude-opus-4-5" const inheritedModel = "anthropic/claude-opus-4-5"
@ -1036,13 +1036,13 @@ describe("sisyphus-task", () => {
// #when resolveCategoryConfig is called // #when resolveCategoryConfig is called
const resolved = resolveCategoryConfig(categoryName, { userCategories, inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL }) const resolved = resolveCategoryConfig(categoryName, { userCategories, inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
// #then should fall back to inheritedModel since "" is normalized to undefined // #then should fall back to systemDefaultModel since custom category has no built-in model
expect(resolved).not.toBeNull() expect(resolved).not.toBeNull()
expect(resolved!.model).toBe("anthropic/claude-opus-4-5") expect(resolved!.model).toBe(SYSTEM_DEFAULT_MODEL)
}) })
test("FIXED: undefined userConfig.model falls back to inheritedModel", () => { test("FIXED: undefined userConfig.model falls back to category built-in model", () => {
// #given user explicitly sets a category but leaves model undefined // #given user sets a builtin category but leaves model undefined
const categoryName = "visual-engineering" const categoryName = "visual-engineering"
// Using type assertion since we're testing fallback behavior for categories without model // Using type assertion since we're testing fallback behavior for categories without model
const userCategories = { "visual-engineering": { temperature: 0.2 } } as unknown as Record<string, CategoryConfig> const userCategories = { "visual-engineering": { temperature: 0.2 } } as unknown as Record<string, CategoryConfig>
@ -1051,9 +1051,9 @@ describe("sisyphus-task", () => {
// #when resolveCategoryConfig is called // #when resolveCategoryConfig is called
const resolved = resolveCategoryConfig(categoryName, { userCategories, inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL }) const resolved = resolveCategoryConfig(categoryName, { userCategories, inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
// #then should use inheritedModel // #then should use category's built-in model (gemini-3-pro-preview for visual-engineering)
expect(resolved).not.toBeNull() expect(resolved).not.toBeNull()
expect(resolved!.model).toBe("anthropic/claude-opus-4-5") expect(resolved!.model).toBe("google/gemini-3-pro-preview")
}) })
test("systemDefaultModel is used when no other model is available", () => { test("systemDefaultModel is used when no other model is available", () => {

View File

@ -124,11 +124,12 @@ export function resolveCategoryConfig(
return null return null
} }
// Model priority: user override > inherited from parent > default config > system default // Model priority for categories: user override > category default > system default
// Categories have explicit models - no inheritance from parent session
const model = resolveModel({ const model = resolveModel({
userModel: userConfig?.model, userModel: userConfig?.model,
inheritedModel, inheritedModel: defaultConfig?.model, // Category's built-in model takes precedence over system default
systemDefault: defaultConfig?.model ?? systemDefaultModel, systemDefault: systemDefaultModel,
}) })
const config: CategoryConfig = { const config: CategoryConfig = {
...defaultConfig, ...defaultConfig,