feat(sisyphus-task): inherit parent model for categories and show fallback warning
- Change model priority: user override > parent model > category default - Add ModelFallbackInfo to track model resolution type - Show warning toast when category uses inherited or default model - Add tests for model fallback info in task toast
This commit is contained in:
@@ -84,13 +84,14 @@ export const SISYPHUS_JUNIOR_DEFAULTS = {
|
||||
} as const
|
||||
|
||||
export function createSisyphusJuniorAgentWithOverrides(
|
||||
override: AgentOverrideConfig | undefined
|
||||
override: AgentOverrideConfig | undefined,
|
||||
systemDefaultModel?: string
|
||||
): AgentConfig {
|
||||
if (override?.disable) {
|
||||
override = undefined
|
||||
}
|
||||
|
||||
const model = override?.model ?? SISYPHUS_JUNIOR_DEFAULTS.model
|
||||
const model = override?.model ?? systemDefaultModel ?? SISYPHUS_JUNIOR_DEFAULTS.model
|
||||
const temperature = override?.temperature ?? SISYPHUS_JUNIOR_DEFAULTS.temperature
|
||||
|
||||
const promptAppend = override?.prompt_append
|
||||
|
||||
@@ -192,7 +192,7 @@ export function createBuiltinAgents(
|
||||
|
||||
if (!disabledAgents.includes("orchestrator-sisyphus")) {
|
||||
const orchestratorOverride = agentOverrides["orchestrator-sisyphus"]
|
||||
const orchestratorModel = orchestratorOverride?.model
|
||||
const orchestratorModel = orchestratorOverride?.model ?? systemDefaultModel
|
||||
let orchestratorConfig = createOrchestratorSisyphusAgent({
|
||||
model: orchestratorModel,
|
||||
availableAgents,
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { TaskToastManager, getTaskToastManager, initTaskToastManager } from "./manager"
|
||||
export type { TrackedTask, TaskStatus, TaskToastOptions } from "./types"
|
||||
export type { TrackedTask, TaskStatus, TaskToastOptions, ModelFallbackInfo } from "./types"
|
||||
|
||||
@@ -142,4 +142,87 @@ describe("TaskToastManager", () => {
|
||||
expect(call.body.message).toContain("Running (1):")
|
||||
})
|
||||
})
|
||||
|
||||
describe("model fallback info in toast message", () => {
|
||||
test("should display warning when model falls back to default", () => {
|
||||
// #given - a task with model fallback to default
|
||||
const task = {
|
||||
id: "task_1",
|
||||
description: "Task with default model",
|
||||
agent: "Sisyphus-Junior",
|
||||
isBackground: false,
|
||||
modelInfo: { model: "anthropic/claude-sonnet-4-5", type: "default" as const },
|
||||
}
|
||||
|
||||
// #when - addTask is called
|
||||
toastManager.addTask(task)
|
||||
|
||||
// #then - toast should show warning with model info
|
||||
expect(mockClient.tui.showToast).toHaveBeenCalled()
|
||||
const call = mockClient.tui.showToast.mock.calls[0][0]
|
||||
expect(call.body.message).toContain("⚠️")
|
||||
expect(call.body.message).toContain("anthropic/claude-sonnet-4-5")
|
||||
expect(call.body.message).toContain("(default)")
|
||||
})
|
||||
|
||||
test("should display warning when model is inherited from parent", () => {
|
||||
// #given - a task with inherited model
|
||||
const task = {
|
||||
id: "task_2",
|
||||
description: "Task with inherited model",
|
||||
agent: "Sisyphus-Junior",
|
||||
isBackground: false,
|
||||
modelInfo: { model: "cliproxy/claude-opus-4-5", type: "inherited" as const },
|
||||
}
|
||||
|
||||
// #when - addTask is called
|
||||
toastManager.addTask(task)
|
||||
|
||||
// #then - toast should show warning with inherited model
|
||||
expect(mockClient.tui.showToast).toHaveBeenCalled()
|
||||
const call = mockClient.tui.showToast.mock.calls[0][0]
|
||||
expect(call.body.message).toContain("⚠️")
|
||||
expect(call.body.message).toContain("cliproxy/claude-opus-4-5")
|
||||
expect(call.body.message).toContain("(inherited)")
|
||||
})
|
||||
|
||||
test("should not display model info when user-defined", () => {
|
||||
// #given - a task with user-defined model
|
||||
const task = {
|
||||
id: "task_3",
|
||||
description: "Task with user model",
|
||||
agent: "Sisyphus-Junior",
|
||||
isBackground: false,
|
||||
modelInfo: { model: "my-provider/my-model", type: "user-defined" as const },
|
||||
}
|
||||
|
||||
// #when - addTask is called
|
||||
toastManager.addTask(task)
|
||||
|
||||
// #then - toast should NOT show model warning
|
||||
expect(mockClient.tui.showToast).toHaveBeenCalled()
|
||||
const call = mockClient.tui.showToast.mock.calls[0][0]
|
||||
expect(call.body.message).not.toContain("⚠️ Model:")
|
||||
expect(call.body.message).not.toContain("(inherited)")
|
||||
expect(call.body.message).not.toContain("(default)")
|
||||
})
|
||||
|
||||
test("should not display model info when not provided", () => {
|
||||
// #given - a task without model info
|
||||
const task = {
|
||||
id: "task_4",
|
||||
description: "Task without model info",
|
||||
agent: "explore",
|
||||
isBackground: true,
|
||||
}
|
||||
|
||||
// #when - addTask is called
|
||||
toastManager.addTask(task)
|
||||
|
||||
// #then - toast should NOT show model warning
|
||||
expect(mockClient.tui.showToast).toHaveBeenCalled()
|
||||
const call = mockClient.tui.showToast.mock.calls[0][0]
|
||||
expect(call.body.message).not.toContain("⚠️ Model:")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import type { TrackedTask, TaskStatus } from "./types"
|
||||
import type { TrackedTask, TaskStatus, ModelFallbackInfo } from "./types"
|
||||
import type { ConcurrencyManager } from "../background-agent/concurrency"
|
||||
|
||||
type OpencodeClient = PluginInput["client"]
|
||||
@@ -25,6 +25,7 @@ export class TaskToastManager {
|
||||
isBackground: boolean
|
||||
status?: TaskStatus
|
||||
skills?: string[]
|
||||
modelInfo?: ModelFallbackInfo
|
||||
}): void {
|
||||
const trackedTask: TrackedTask = {
|
||||
id: task.id,
|
||||
@@ -34,6 +35,7 @@ export class TaskToastManager {
|
||||
startedAt: new Date(),
|
||||
isBackground: task.isBackground,
|
||||
skills: task.skills,
|
||||
modelInfo: task.modelInfo,
|
||||
}
|
||||
|
||||
this.tasks.set(task.id, trackedTask)
|
||||
@@ -105,6 +107,14 @@ export class TaskToastManager {
|
||||
|
||||
const lines: string[] = []
|
||||
|
||||
// Show model fallback warning for the new task if applicable
|
||||
if (newTask.modelInfo && newTask.modelInfo.type !== "user-defined") {
|
||||
const icon = "⚠️"
|
||||
const suffix = newTask.modelInfo.type === "inherited" ? " (inherited)" : " (default)"
|
||||
lines.push(`${icon} Model: ${newTask.modelInfo.model}${suffix}`)
|
||||
lines.push("")
|
||||
}
|
||||
|
||||
if (running.length > 0) {
|
||||
lines.push(`Running (${running.length}):${concurrencyInfo}`)
|
||||
for (const task of running) {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
export type TaskStatus = "running" | "queued" | "completed" | "error"
|
||||
|
||||
export interface ModelFallbackInfo {
|
||||
model: string
|
||||
type: "user-defined" | "inherited" | "default"
|
||||
}
|
||||
|
||||
export interface TrackedTask {
|
||||
id: string
|
||||
description: string
|
||||
@@ -8,6 +13,7 @@ export interface TrackedTask {
|
||||
startedAt: Date
|
||||
isBackground: boolean
|
||||
skills?: string[]
|
||||
modelInfo?: ModelFallbackInfo
|
||||
}
|
||||
|
||||
export interface TaskToastOptions {
|
||||
|
||||
@@ -154,7 +154,8 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
||||
};
|
||||
|
||||
agentConfig["Sisyphus-Junior"] = createSisyphusJuniorAgentWithOverrides(
|
||||
pluginConfig.agents?.["Sisyphus-Junior"]
|
||||
pluginConfig.agents?.["Sisyphus-Junior"],
|
||||
config.model as string | undefined
|
||||
);
|
||||
|
||||
if (builderEnabled) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { findNearestMessageWithFields, findFirstMessageWithAgent, MESSAGE_STORAG
|
||||
import { resolveMultipleSkills } from "../../features/opencode-skill-loader/skill-content"
|
||||
import { createBuiltinSkills } from "../../features/builtin-skills/skills"
|
||||
import { getTaskToastManager } from "../../features/task-toast-manager"
|
||||
import type { ModelFallbackInfo } from "../../features/task-toast-manager/types"
|
||||
import { subagentSessions, getSessionAgent } from "../../features/claude-code-session-state"
|
||||
import { log } from "../../shared/logger"
|
||||
|
||||
@@ -60,7 +61,8 @@ type ToolContextWithMetadata = {
|
||||
|
||||
function resolveCategoryConfig(
|
||||
categoryName: string,
|
||||
userCategories?: CategoriesConfig
|
||||
userCategories?: CategoriesConfig,
|
||||
parentModelString?: string
|
||||
): { config: CategoryConfig; promptAppend: string } | null {
|
||||
const defaultConfig = DEFAULT_CATEGORIES[categoryName]
|
||||
const userConfig = userCategories?.[categoryName]
|
||||
@@ -70,10 +72,12 @@ function resolveCategoryConfig(
|
||||
return null
|
||||
}
|
||||
|
||||
// Model priority: user override > parent model (inherit) > category default > hardcoded fallback
|
||||
// Parent model takes precedence over category default so custom providers work out-of-box
|
||||
const config: CategoryConfig = {
|
||||
...defaultConfig,
|
||||
...userConfig,
|
||||
model: userConfig?.model ?? defaultConfig?.model ?? "anthropic/claude-sonnet-4-5",
|
||||
model: userConfig?.model ?? parentModelString ?? defaultConfig?.model ?? "anthropic/claude-sonnet-4-5",
|
||||
}
|
||||
|
||||
let promptAppend = defaultPromptAppend
|
||||
@@ -329,12 +333,27 @@ ${textContent || "(No text output)"}`
|
||||
let categoryModel: { providerID: string; modelID: string; variant?: string } | undefined
|
||||
let categoryPromptAppend: string | undefined
|
||||
|
||||
const parentModelString = parentModel
|
||||
? `${parentModel.providerID}/${parentModel.modelID}`
|
||||
: undefined
|
||||
|
||||
let modelInfo: ModelFallbackInfo | undefined
|
||||
|
||||
if (args.category) {
|
||||
const resolved = resolveCategoryConfig(args.category, userCategories)
|
||||
const resolved = resolveCategoryConfig(args.category, userCategories, parentModelString)
|
||||
if (!resolved) {
|
||||
return `❌ Unknown category: "${args.category}". Available: ${Object.keys({ ...DEFAULT_CATEGORIES, ...userCategories }).join(", ")}`
|
||||
}
|
||||
|
||||
const userHasDefinedCategory = userCategories?.[args.category]?.model !== undefined
|
||||
if (userHasDefinedCategory) {
|
||||
modelInfo = { model: resolved.config.model, type: "user-defined" }
|
||||
} else if (parentModelString) {
|
||||
modelInfo = { model: parentModelString, type: "inherited" }
|
||||
} else if (DEFAULT_CATEGORIES[args.category]?.model) {
|
||||
modelInfo = { model: DEFAULT_CATEGORIES[args.category].model, type: "default" }
|
||||
}
|
||||
|
||||
agentToUse = SISYPHUS_JUNIOR_AGENT
|
||||
const parsedModel = parseModelString(resolved.config.model)
|
||||
categoryModel = parsedModel
|
||||
@@ -448,6 +467,7 @@ System notifies on completion. Use \`background_output\` with task_id="${task.id
|
||||
agent: agentToUse,
|
||||
isBackground: false,
|
||||
skills: args.skills,
|
||||
modelInfo,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user