feat(config): add model variant support
Allow optional model variant config for agents and categories. Propagate category variants into task model payloads so category-driven runs inherit provider-specific variants. Closes: #647
This commit is contained in:
@@ -102,6 +102,9 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -225,6 +228,9 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -348,6 +354,9 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -471,6 +480,9 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -594,6 +606,9 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -717,6 +732,9 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -840,6 +858,9 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -963,6 +984,9 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1086,6 +1110,9 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1209,6 +1236,9 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1332,6 +1362,9 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1455,6 +1488,9 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1578,6 +1614,9 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1701,6 +1740,9 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1824,6 +1866,9 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1954,6 +1999,9 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
|
||||
@@ -76,6 +76,7 @@ export type AgentName = BuiltinAgentName
|
||||
|
||||
export type AgentOverrideConfig = Partial<AgentConfig> & {
|
||||
prompt_append?: string
|
||||
variant?: string
|
||||
}
|
||||
|
||||
export type AgentOverrides = Partial<Record<OverridableAgentName, AgentOverrideConfig>>
|
||||
|
||||
@@ -127,6 +127,31 @@ describe("buildAgent with category and skills", () => {
|
||||
expect(agent.temperature).toBe(0.7)
|
||||
})
|
||||
|
||||
test("agent with category inherits variant", () => {
|
||||
// #given
|
||||
const source = {
|
||||
"test-agent": () =>
|
||||
({
|
||||
description: "Test agent",
|
||||
category: "custom-category",
|
||||
}) as AgentConfig,
|
||||
}
|
||||
|
||||
const categories = {
|
||||
"custom-category": {
|
||||
model: "openai/gpt-5.2",
|
||||
variant: "xhigh",
|
||||
},
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"], undefined, categories)
|
||||
|
||||
// #then
|
||||
expect(agent.model).toBe("openai/gpt-5.2")
|
||||
expect(agent.variant).toBe("xhigh")
|
||||
})
|
||||
|
||||
test("agent with skills has content prepended to prompt", () => {
|
||||
// #given
|
||||
const source = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { BuiltinAgentName, AgentOverrideConfig, AgentOverrides, AgentFactory, AgentPromptMetadata } from "./types"
|
||||
import type { CategoriesConfig, CategoryConfig } from "../config/schema"
|
||||
import { createSisyphusAgent } from "./sisyphus"
|
||||
import { createOracleAgent, ORACLE_PROMPT_METADATA } from "./oracle"
|
||||
import { createLibrarianAgent, LIBRARIAN_PROMPT_METADATA } from "./librarian"
|
||||
@@ -47,12 +48,19 @@ function isFactory(source: AgentSource): source is AgentFactory {
|
||||
return typeof source === "function"
|
||||
}
|
||||
|
||||
export function buildAgent(source: AgentSource, model?: string): AgentConfig {
|
||||
export function buildAgent(
|
||||
source: AgentSource,
|
||||
model?: string,
|
||||
categories?: CategoriesConfig
|
||||
): AgentConfig {
|
||||
const base = isFactory(source) ? source(model) : source
|
||||
const categoryConfigs: Record<string, CategoryConfig> = categories
|
||||
? { ...DEFAULT_CATEGORIES, ...categories }
|
||||
: DEFAULT_CATEGORIES
|
||||
|
||||
const agentWithCategory = base as AgentConfig & { category?: string; skills?: string[] }
|
||||
const agentWithCategory = base as AgentConfig & { category?: string; skills?: string[]; variant?: string }
|
||||
if (agentWithCategory.category) {
|
||||
const categoryConfig = DEFAULT_CATEGORIES[agentWithCategory.category]
|
||||
const categoryConfig = categoryConfigs[agentWithCategory.category]
|
||||
if (categoryConfig) {
|
||||
if (!base.model) {
|
||||
base.model = categoryConfig.model
|
||||
@@ -60,6 +68,9 @@ export function buildAgent(source: AgentSource, model?: string): AgentConfig {
|
||||
if (base.temperature === undefined && categoryConfig.temperature !== undefined) {
|
||||
base.temperature = categoryConfig.temperature
|
||||
}
|
||||
if (base.variant === undefined && categoryConfig.variant !== undefined) {
|
||||
base.variant = categoryConfig.variant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,11 +129,16 @@ export function createBuiltinAgents(
|
||||
disabledAgents: BuiltinAgentName[] = [],
|
||||
agentOverrides: AgentOverrides = {},
|
||||
directory?: string,
|
||||
systemDefaultModel?: string
|
||||
systemDefaultModel?: string,
|
||||
categories?: CategoriesConfig
|
||||
): Record<string, AgentConfig> {
|
||||
const result: Record<string, AgentConfig> = {}
|
||||
const availableAgents: AvailableAgent[] = []
|
||||
|
||||
const mergedCategories = categories
|
||||
? { ...DEFAULT_CATEGORIES, ...categories }
|
||||
: DEFAULT_CATEGORIES
|
||||
|
||||
for (const [name, source] of Object.entries(agentSources)) {
|
||||
const agentName = name as BuiltinAgentName
|
||||
|
||||
@@ -133,7 +149,7 @@ export function createBuiltinAgents(
|
||||
const override = agentOverrides[agentName]
|
||||
const model = override?.model
|
||||
|
||||
let config = buildAgent(source, model)
|
||||
let config = buildAgent(source, model, mergedCategories)
|
||||
|
||||
if (agentName === "librarian" && directory && config.prompt) {
|
||||
const envContext = createEnvContext()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { AgentOverrideConfigSchema, BuiltinCategoryNameSchema, OhMyOpenCodeConfigSchema } from "./schema"
|
||||
import { AgentOverrideConfigSchema, BuiltinCategoryNameSchema, CategoryConfigSchema, OhMyOpenCodeConfigSchema } from "./schema"
|
||||
|
||||
describe("disabled_mcps schema", () => {
|
||||
test("should accept built-in MCP names", () => {
|
||||
@@ -174,6 +174,33 @@ describe("AgentOverrideConfigSchema", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("variant field", () => {
|
||||
test("accepts variant as optional string", () => {
|
||||
// #given
|
||||
const config = { variant: "high" }
|
||||
|
||||
// #when
|
||||
const result = AgentOverrideConfigSchema.safeParse(config)
|
||||
|
||||
// #then
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.variant).toBe("high")
|
||||
}
|
||||
})
|
||||
|
||||
test("rejects non-string variant", () => {
|
||||
// #given
|
||||
const config = { variant: 123 }
|
||||
|
||||
// #when
|
||||
const result = AgentOverrideConfigSchema.safeParse(config)
|
||||
|
||||
// #then
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("skills field", () => {
|
||||
test("accepts skills as optional string array", () => {
|
||||
// #given
|
||||
@@ -303,6 +330,33 @@ describe("AgentOverrideConfigSchema", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("CategoryConfigSchema", () => {
|
||||
test("accepts variant as optional string", () => {
|
||||
// #given
|
||||
const config = { model: "openai/gpt-5.2", variant: "xhigh" }
|
||||
|
||||
// #when
|
||||
const result = CategoryConfigSchema.safeParse(config)
|
||||
|
||||
// #then
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.variant).toBe("xhigh")
|
||||
}
|
||||
})
|
||||
|
||||
test("rejects non-string variant", () => {
|
||||
// #given
|
||||
const config = { model: "openai/gpt-5.2", variant: 123 }
|
||||
|
||||
// #when
|
||||
const result = CategoryConfigSchema.safeParse(config)
|
||||
|
||||
// #then
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("BuiltinCategoryNameSchema", () => {
|
||||
test("accepts all builtin category names", () => {
|
||||
// #given
|
||||
|
||||
@@ -97,6 +97,7 @@ export const BuiltinCommandNameSchema = z.enum([
|
||||
export const AgentOverrideConfigSchema = z.object({
|
||||
/** @deprecated Use `category` instead. Model is inherited from category defaults. */
|
||||
model: z.string().optional(),
|
||||
variant: z.string().optional(),
|
||||
/** Category name to inherit model and other settings from CategoryConfig */
|
||||
category: z.string().optional(),
|
||||
/** Skill names to inject into agent prompt */
|
||||
@@ -153,6 +154,7 @@ export const SisyphusAgentConfigSchema = z.object({
|
||||
|
||||
export const CategoryConfigSchema = z.object({
|
||||
model: z.string(),
|
||||
variant: z.string().optional(),
|
||||
temperature: z.number().min(0).max(2).optional(),
|
||||
top_p: z.number().min(0).max(1).optional(),
|
||||
maxTokens: z.number().optional(),
|
||||
|
||||
@@ -27,7 +27,7 @@ export interface BackgroundTask {
|
||||
error?: string
|
||||
progress?: TaskProgress
|
||||
parentModel?: { providerID: string; modelID: string }
|
||||
model?: { providerID: string; modelID: string }
|
||||
model?: { providerID: string; modelID: string; variant?: string }
|
||||
/** Agent name used for concurrency tracking */
|
||||
concurrencyKey?: string
|
||||
/** Parent session's agent name for notification */
|
||||
@@ -46,7 +46,7 @@ export interface LaunchInput {
|
||||
parentMessageID: string
|
||||
parentModel?: { providerID: string; modelID: string }
|
||||
parentAgent?: string
|
||||
model?: { providerID: string; modelID: string }
|
||||
model?: { providerID: string; modelID: string; variant?: string }
|
||||
skills?: string[]
|
||||
skillContent?: string
|
||||
}
|
||||
|
||||
@@ -210,4 +210,26 @@ describe("keyword-detector session filtering", () => {
|
||||
expect(output.message.variant).toBe("max")
|
||||
expect(toastCalls).toContain("Ultrawork Mode Activated")
|
||||
})
|
||||
|
||||
test("should not override existing variant", async () => {
|
||||
// #given - main session set with pre-existing variant
|
||||
setMainSession("main-123")
|
||||
|
||||
const toastCalls: string[] = []
|
||||
const hook = createKeywordDetectorHook(createMockPluginInput({ toastCalls }))
|
||||
const output = {
|
||||
message: { variant: "low" } as Record<string, unknown>,
|
||||
parts: [{ type: "text", text: "ultrawork mode" }],
|
||||
}
|
||||
|
||||
// #when - ultrawork keyword triggers
|
||||
await hook["chat.message"](
|
||||
{ sessionID: "main-123" },
|
||||
output
|
||||
)
|
||||
|
||||
// #then - existing variant should remain
|
||||
expect(output.message.variant).toBe("low")
|
||||
expect(toastCalls).toContain("Ultrawork Mode Activated")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -47,7 +47,9 @@ export function createKeywordDetectorHook(ctx: PluginInput, collector?: ContextC
|
||||
if (hasUltrawork) {
|
||||
log(`[keyword-detector] Ultrawork mode activated`, { sessionID: input.sessionID })
|
||||
|
||||
output.message.variant = "max"
|
||||
if (output.message.variant === undefined) {
|
||||
output.message.variant = "max"
|
||||
}
|
||||
|
||||
ctx.client.tui
|
||||
.showToast({
|
||||
|
||||
16
src/index.ts
16
src/index.ts
@@ -37,6 +37,8 @@ import {
|
||||
createContextInjectorMessagesTransformHook,
|
||||
} from "./features/context-injector";
|
||||
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
|
||||
import { applyAgentVariant, resolveAgentVariant } from "./shared/agent-variant";
|
||||
import { createFirstMessageVariantGate } from "./shared/first-message-variant";
|
||||
import {
|
||||
discoverUserClaudeSkills,
|
||||
discoverProjectClaudeSkills,
|
||||
@@ -82,6 +84,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
|
||||
const pluginConfig = loadPluginConfig(ctx.directory, ctx);
|
||||
const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
|
||||
const firstMessageVariantGate = createFirstMessageVariantGate();
|
||||
const isHookEnabled = (hookName: HookName) => !disabledHooks.has(hookName);
|
||||
|
||||
const modelCacheState = createModelCacheState();
|
||||
@@ -316,6 +319,17 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
},
|
||||
|
||||
"chat.message": async (input, output) => {
|
||||
const message = (output as { message: { variant?: string } }).message
|
||||
if (firstMessageVariantGate.shouldOverride(input.sessionID)) {
|
||||
const variant = resolveAgentVariant(pluginConfig, input.agent)
|
||||
if (variant !== undefined) {
|
||||
message.variant = variant
|
||||
}
|
||||
firstMessageVariantGate.markApplied(input.sessionID)
|
||||
} else {
|
||||
applyAgentVariant(pluginConfig, input.agent, message)
|
||||
}
|
||||
|
||||
await keywordDetector?.["chat.message"]?.(input, output);
|
||||
await claudeCodeHooks["chat.message"]?.(input, output);
|
||||
await contextInjector["chat.message"]?.(input, output);
|
||||
@@ -422,6 +436,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
if (!sessionInfo?.parentID) {
|
||||
setMainSession(sessionInfo?.id);
|
||||
}
|
||||
firstMessageVariantGate.markSessionCreated(sessionInfo);
|
||||
}
|
||||
|
||||
if (event.type === "session.deleted") {
|
||||
@@ -431,6 +446,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
}
|
||||
if (sessionInfo?.id) {
|
||||
clearSessionAgent(sessionInfo.id);
|
||||
firstMessageVariantGate.clear(sessionInfo.id);
|
||||
await skillMcpManager.disconnectSession(sessionInfo.id);
|
||||
await lspManager.cleanupTempDirectoryClients();
|
||||
}
|
||||
|
||||
@@ -103,7 +103,8 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
||||
pluginConfig.disabled_agents,
|
||||
pluginConfig.agents,
|
||||
ctx.directory,
|
||||
config.model as string | undefined
|
||||
config.model as string | undefined,
|
||||
pluginConfig.categories
|
||||
);
|
||||
|
||||
// Claude Code agents: Do NOT apply permission migration
|
||||
|
||||
83
src/shared/agent-variant.test.ts
Normal file
83
src/shared/agent-variant.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { OhMyOpenCodeConfig } from "../config"
|
||||
import { applyAgentVariant, resolveAgentVariant } from "./agent-variant"
|
||||
|
||||
describe("resolveAgentVariant", () => {
|
||||
test("returns undefined when agent name missing", () => {
|
||||
// #given
|
||||
const config = {} as OhMyOpenCodeConfig
|
||||
|
||||
// #when
|
||||
const variant = resolveAgentVariant(config)
|
||||
|
||||
// #then
|
||||
expect(variant).toBeUndefined()
|
||||
})
|
||||
|
||||
test("returns agent override variant", () => {
|
||||
// #given
|
||||
const config = {
|
||||
agents: {
|
||||
Sisyphus: { variant: "low" },
|
||||
},
|
||||
} as OhMyOpenCodeConfig
|
||||
|
||||
// #when
|
||||
const variant = resolveAgentVariant(config, "Sisyphus")
|
||||
|
||||
// #then
|
||||
expect(variant).toBe("low")
|
||||
})
|
||||
|
||||
test("returns category variant when agent uses category", () => {
|
||||
// #given
|
||||
const config = {
|
||||
agents: {
|
||||
Sisyphus: { category: "ultrabrain" },
|
||||
},
|
||||
categories: {
|
||||
ultrabrain: { model: "openai/gpt-5.2", variant: "xhigh" },
|
||||
},
|
||||
} as OhMyOpenCodeConfig
|
||||
|
||||
// #when
|
||||
const variant = resolveAgentVariant(config, "Sisyphus")
|
||||
|
||||
// #then
|
||||
expect(variant).toBe("xhigh")
|
||||
})
|
||||
})
|
||||
|
||||
describe("applyAgentVariant", () => {
|
||||
test("sets variant when message is undefined", () => {
|
||||
// #given
|
||||
const config = {
|
||||
agents: {
|
||||
Sisyphus: { variant: "low" },
|
||||
},
|
||||
} as OhMyOpenCodeConfig
|
||||
const message: { variant?: string } = {}
|
||||
|
||||
// #when
|
||||
applyAgentVariant(config, "Sisyphus", message)
|
||||
|
||||
// #then
|
||||
expect(message.variant).toBe("low")
|
||||
})
|
||||
|
||||
test("does not override existing variant", () => {
|
||||
// #given
|
||||
const config = {
|
||||
agents: {
|
||||
Sisyphus: { variant: "low" },
|
||||
},
|
||||
} as OhMyOpenCodeConfig
|
||||
const message = { variant: "max" }
|
||||
|
||||
// #when
|
||||
applyAgentVariant(config, "Sisyphus", message)
|
||||
|
||||
// #then
|
||||
expect(message.variant).toBe("max")
|
||||
})
|
||||
})
|
||||
40
src/shared/agent-variant.ts
Normal file
40
src/shared/agent-variant.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { OhMyOpenCodeConfig } from "../config"
|
||||
|
||||
export function resolveAgentVariant(
|
||||
config: OhMyOpenCodeConfig,
|
||||
agentName?: string
|
||||
): string | undefined {
|
||||
if (!agentName) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const agentOverrides = config.agents as
|
||||
| Record<string, { variant?: string; category?: string }>
|
||||
| undefined
|
||||
const agentOverride = agentOverrides?.[agentName]
|
||||
if (!agentOverride) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (agentOverride.variant) {
|
||||
return agentOverride.variant
|
||||
}
|
||||
|
||||
const categoryName = agentOverride.category
|
||||
if (!categoryName) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return config.categories?.[categoryName]?.variant
|
||||
}
|
||||
|
||||
export function applyAgentVariant(
|
||||
config: OhMyOpenCodeConfig,
|
||||
agentName: string | undefined,
|
||||
message: { variant?: string }
|
||||
): void {
|
||||
const variant = resolveAgentVariant(config, agentName)
|
||||
if (variant !== undefined && message.variant === undefined) {
|
||||
message.variant = variant
|
||||
}
|
||||
}
|
||||
32
src/shared/first-message-variant.test.ts
Normal file
32
src/shared/first-message-variant.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createFirstMessageVariantGate } from "./first-message-variant"
|
||||
|
||||
describe("createFirstMessageVariantGate", () => {
|
||||
test("marks new sessions and clears after apply", () => {
|
||||
// #given
|
||||
const gate = createFirstMessageVariantGate()
|
||||
|
||||
// #when
|
||||
gate.markSessionCreated({ id: "session-1" })
|
||||
|
||||
// #then
|
||||
expect(gate.shouldOverride("session-1")).toBe(true)
|
||||
|
||||
// #when
|
||||
gate.markApplied("session-1")
|
||||
|
||||
// #then
|
||||
expect(gate.shouldOverride("session-1")).toBe(false)
|
||||
})
|
||||
|
||||
test("ignores forked sessions", () => {
|
||||
// #given
|
||||
const gate = createFirstMessageVariantGate()
|
||||
|
||||
// #when
|
||||
gate.markSessionCreated({ id: "session-2", parentID: "session-parent" })
|
||||
|
||||
// #then
|
||||
expect(gate.shouldOverride("session-2")).toBe(false)
|
||||
})
|
||||
})
|
||||
28
src/shared/first-message-variant.ts
Normal file
28
src/shared/first-message-variant.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
type SessionInfo = {
|
||||
id?: string
|
||||
parentID?: string
|
||||
}
|
||||
|
||||
export function createFirstMessageVariantGate() {
|
||||
const pending = new Set<string>()
|
||||
|
||||
return {
|
||||
markSessionCreated(info?: SessionInfo) {
|
||||
if (info?.id && !info.parentID) {
|
||||
pending.add(info.id)
|
||||
}
|
||||
},
|
||||
shouldOverride(sessionID?: string) {
|
||||
if (!sessionID) return false
|
||||
return pending.has(sessionID)
|
||||
},
|
||||
markApplied(sessionID?: string) {
|
||||
if (!sessionID) return
|
||||
pending.delete(sessionID)
|
||||
},
|
||||
clear(sessionID?: string) {
|
||||
if (!sessionID) return
|
||||
pending.delete(sessionID)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -21,3 +21,4 @@ export * from "./opencode-version"
|
||||
export * from "./permission-compat"
|
||||
export * from "./external-plugin-detector"
|
||||
export * from "./zip-extractor"
|
||||
export * from "./agent-variant"
|
||||
|
||||
@@ -207,6 +207,70 @@ describe("sisyphus-task", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("category variant", () => {
|
||||
test("passes variant to background model payload", async () => {
|
||||
// #given
|
||||
const { createSisyphusTask } = require("./tools")
|
||||
let launchInput: any
|
||||
|
||||
const mockManager = {
|
||||
launch: async (input: any) => {
|
||||
launchInput = input
|
||||
return {
|
||||
id: "task-variant",
|
||||
sessionID: "session-variant",
|
||||
description: "Variant task",
|
||||
agent: "Sisyphus-Junior",
|
||||
status: "running",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const mockClient = {
|
||||
app: { agents: async () => ({ data: [] }) },
|
||||
session: {
|
||||
create: async () => ({ data: { id: "test-session" } }),
|
||||
prompt: async () => ({ data: {} }),
|
||||
messages: async () => ({ data: [] }),
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createSisyphusTask({
|
||||
manager: mockManager,
|
||||
client: mockClient,
|
||||
userCategories: {
|
||||
ultrabrain: { model: "openai/gpt-5.2", variant: "xhigh" },
|
||||
},
|
||||
})
|
||||
|
||||
const toolContext = {
|
||||
sessionID: "parent-session",
|
||||
messageID: "parent-message",
|
||||
agent: "Sisyphus",
|
||||
abort: new AbortController().signal,
|
||||
}
|
||||
|
||||
// #when
|
||||
await tool.execute(
|
||||
{
|
||||
description: "Variant task",
|
||||
prompt: "Do something",
|
||||
category: "ultrabrain",
|
||||
run_in_background: true,
|
||||
skills: [],
|
||||
},
|
||||
toolContext
|
||||
)
|
||||
|
||||
// #then
|
||||
expect(launchInput.model).toEqual({
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5.2",
|
||||
variant: "xhigh",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("skills parameter", () => {
|
||||
test("SISYPHUS_TASK_DESCRIPTION documents skills parameter", () => {
|
||||
// #given / #when / #then
|
||||
|
||||
@@ -315,7 +315,7 @@ ${textContent || "(No text output)"}`
|
||||
}
|
||||
|
||||
let agentToUse: string
|
||||
let categoryModel: { providerID: string; modelID: string } | undefined
|
||||
let categoryModel: { providerID: string; modelID: string; variant?: string } | undefined
|
||||
let categoryPromptAppend: string | undefined
|
||||
|
||||
if (args.category) {
|
||||
@@ -325,7 +325,12 @@ ${textContent || "(No text output)"}`
|
||||
}
|
||||
|
||||
agentToUse = SISYPHUS_JUNIOR_AGENT
|
||||
categoryModel = parseModelString(resolved.config.model)
|
||||
const parsedModel = parseModelString(resolved.config.model)
|
||||
categoryModel = parsedModel
|
||||
? (resolved.config.variant
|
||||
? { ...parsedModel, variant: resolved.config.variant }
|
||||
: parsedModel)
|
||||
: undefined
|
||||
categoryPromptAppend = resolved.promptAppend || undefined
|
||||
} else {
|
||||
agentToUse = args.subagent_type!.trim()
|
||||
|
||||
Reference in New Issue
Block a user