Merge pull request #677 from jkoelker/fix/add-variant-support
This commit is contained in:
commit
d2a5f47f1c
@ -102,6 +102,9 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -225,6 +228,9 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -348,6 +354,9 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -471,6 +480,9 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -594,6 +606,9 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -717,6 +732,9 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -840,6 +858,9 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -963,6 +984,9 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -1086,6 +1110,9 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -1209,6 +1236,9 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -1332,6 +1362,9 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -1455,6 +1488,9 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -1578,6 +1614,9 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -1701,6 +1740,9 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -1824,6 +1866,9 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -1954,6 +1999,9 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"temperature": {
|
"temperature": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
|
|||||||
@ -76,6 +76,7 @@ export type AgentName = BuiltinAgentName
|
|||||||
|
|
||||||
export type AgentOverrideConfig = Partial<AgentConfig> & {
|
export type AgentOverrideConfig = Partial<AgentConfig> & {
|
||||||
prompt_append?: string
|
prompt_append?: string
|
||||||
|
variant?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AgentOverrides = Partial<Record<OverridableAgentName, AgentOverrideConfig>>
|
export type AgentOverrides = Partial<Record<OverridableAgentName, AgentOverrideConfig>>
|
||||||
|
|||||||
@ -127,6 +127,31 @@ describe("buildAgent with category and skills", () => {
|
|||||||
expect(agent.temperature).toBe(0.7)
|
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", () => {
|
test("agent with skills has content prepended to prompt", () => {
|
||||||
// #given
|
// #given
|
||||||
const source = {
|
const source = {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||||
import type { BuiltinAgentName, AgentOverrideConfig, AgentOverrides, AgentFactory, AgentPromptMetadata } from "./types"
|
import type { BuiltinAgentName, AgentOverrideConfig, AgentOverrides, AgentFactory, AgentPromptMetadata } from "./types"
|
||||||
|
import type { CategoriesConfig, CategoryConfig } from "../config/schema"
|
||||||
import { createSisyphusAgent } from "./sisyphus"
|
import { createSisyphusAgent } from "./sisyphus"
|
||||||
import { createOracleAgent, ORACLE_PROMPT_METADATA } from "./oracle"
|
import { createOracleAgent, ORACLE_PROMPT_METADATA } from "./oracle"
|
||||||
import { createLibrarianAgent, LIBRARIAN_PROMPT_METADATA } from "./librarian"
|
import { createLibrarianAgent, LIBRARIAN_PROMPT_METADATA } from "./librarian"
|
||||||
@ -47,12 +48,19 @@ function isFactory(source: AgentSource): source is AgentFactory {
|
|||||||
return typeof source === "function"
|
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 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) {
|
if (agentWithCategory.category) {
|
||||||
const categoryConfig = DEFAULT_CATEGORIES[agentWithCategory.category]
|
const categoryConfig = categoryConfigs[agentWithCategory.category]
|
||||||
if (categoryConfig) {
|
if (categoryConfig) {
|
||||||
if (!base.model) {
|
if (!base.model) {
|
||||||
base.model = categoryConfig.model
|
base.model = categoryConfig.model
|
||||||
@ -60,6 +68,9 @@ export function buildAgent(source: AgentSource, model?: string): AgentConfig {
|
|||||||
if (base.temperature === undefined && categoryConfig.temperature !== undefined) {
|
if (base.temperature === undefined && categoryConfig.temperature !== undefined) {
|
||||||
base.temperature = categoryConfig.temperature
|
base.temperature = categoryConfig.temperature
|
||||||
}
|
}
|
||||||
|
if (base.variant === undefined && categoryConfig.variant !== undefined) {
|
||||||
|
base.variant = categoryConfig.variant
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,11 +129,16 @@ export function createBuiltinAgents(
|
|||||||
disabledAgents: BuiltinAgentName[] = [],
|
disabledAgents: BuiltinAgentName[] = [],
|
||||||
agentOverrides: AgentOverrides = {},
|
agentOverrides: AgentOverrides = {},
|
||||||
directory?: string,
|
directory?: string,
|
||||||
systemDefaultModel?: string
|
systemDefaultModel?: string,
|
||||||
|
categories?: CategoriesConfig
|
||||||
): Record<string, AgentConfig> {
|
): Record<string, AgentConfig> {
|
||||||
const result: Record<string, AgentConfig> = {}
|
const result: Record<string, AgentConfig> = {}
|
||||||
const availableAgents: AvailableAgent[] = []
|
const availableAgents: AvailableAgent[] = []
|
||||||
|
|
||||||
|
const mergedCategories = categories
|
||||||
|
? { ...DEFAULT_CATEGORIES, ...categories }
|
||||||
|
: DEFAULT_CATEGORIES
|
||||||
|
|
||||||
for (const [name, source] of Object.entries(agentSources)) {
|
for (const [name, source] of Object.entries(agentSources)) {
|
||||||
const agentName = name as BuiltinAgentName
|
const agentName = name as BuiltinAgentName
|
||||||
|
|
||||||
@ -133,7 +149,7 @@ export function createBuiltinAgents(
|
|||||||
const override = agentOverrides[agentName]
|
const override = agentOverrides[agentName]
|
||||||
const model = override?.model
|
const model = override?.model
|
||||||
|
|
||||||
let config = buildAgent(source, model)
|
let config = buildAgent(source, model, mergedCategories)
|
||||||
|
|
||||||
if (agentName === "librarian" && directory && config.prompt) {
|
if (agentName === "librarian" && directory && config.prompt) {
|
||||||
const envContext = createEnvContext()
|
const envContext = createEnvContext()
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { AgentOverrideConfigSchema, BuiltinCategoryNameSchema, OhMyOpenCodeConfigSchema } from "./schema"
|
import { AgentOverrideConfigSchema, BuiltinCategoryNameSchema, CategoryConfigSchema, OhMyOpenCodeConfigSchema } from "./schema"
|
||||||
|
|
||||||
describe("disabled_mcps schema", () => {
|
describe("disabled_mcps schema", () => {
|
||||||
test("should accept built-in MCP names", () => {
|
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", () => {
|
describe("skills field", () => {
|
||||||
test("accepts skills as optional string array", () => {
|
test("accepts skills as optional string array", () => {
|
||||||
// #given
|
// #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", () => {
|
describe("BuiltinCategoryNameSchema", () => {
|
||||||
test("accepts all builtin category names", () => {
|
test("accepts all builtin category names", () => {
|
||||||
// #given
|
// #given
|
||||||
|
|||||||
@ -97,6 +97,7 @@ export const BuiltinCommandNameSchema = z.enum([
|
|||||||
export const AgentOverrideConfigSchema = z.object({
|
export const AgentOverrideConfigSchema = z.object({
|
||||||
/** @deprecated Use `category` instead. Model is inherited from category defaults. */
|
/** @deprecated Use `category` instead. Model is inherited from category defaults. */
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
|
variant: z.string().optional(),
|
||||||
/** Category name to inherit model and other settings from CategoryConfig */
|
/** Category name to inherit model and other settings from CategoryConfig */
|
||||||
category: z.string().optional(),
|
category: z.string().optional(),
|
||||||
/** Skill names to inject into agent prompt */
|
/** Skill names to inject into agent prompt */
|
||||||
@ -153,6 +154,7 @@ export const SisyphusAgentConfigSchema = z.object({
|
|||||||
|
|
||||||
export const CategoryConfigSchema = z.object({
|
export const CategoryConfigSchema = z.object({
|
||||||
model: z.string(),
|
model: z.string(),
|
||||||
|
variant: z.string().optional(),
|
||||||
temperature: z.number().min(0).max(2).optional(),
|
temperature: z.number().min(0).max(2).optional(),
|
||||||
top_p: z.number().min(0).max(1).optional(),
|
top_p: z.number().min(0).max(1).optional(),
|
||||||
maxTokens: z.number().optional(),
|
maxTokens: z.number().optional(),
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export interface BackgroundTask {
|
|||||||
error?: string
|
error?: string
|
||||||
progress?: TaskProgress
|
progress?: TaskProgress
|
||||||
parentModel?: { providerID: string; modelID: string }
|
parentModel?: { providerID: string; modelID: string }
|
||||||
model?: { providerID: string; modelID: string }
|
model?: { providerID: string; modelID: string; variant?: string }
|
||||||
/** Agent name used for concurrency tracking */
|
/** Agent name used for concurrency tracking */
|
||||||
concurrencyKey?: string
|
concurrencyKey?: string
|
||||||
/** Parent session's agent name for notification */
|
/** Parent session's agent name for notification */
|
||||||
@ -46,7 +46,7 @@ export interface LaunchInput {
|
|||||||
parentMessageID: string
|
parentMessageID: string
|
||||||
parentModel?: { providerID: string; modelID: string }
|
parentModel?: { providerID: string; modelID: string }
|
||||||
parentAgent?: string
|
parentAgent?: string
|
||||||
model?: { providerID: string; modelID: string }
|
model?: { providerID: string; modelID: string; variant?: string }
|
||||||
skills?: string[]
|
skills?: string[]
|
||||||
skillContent?: string
|
skillContent?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -210,4 +210,26 @@ describe("keyword-detector session filtering", () => {
|
|||||||
expect(output.message.variant).toBe("max")
|
expect(output.message.variant).toBe("max")
|
||||||
expect(toastCalls).toContain("Ultrawork Mode Activated")
|
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) {
|
if (hasUltrawork) {
|
||||||
log(`[keyword-detector] Ultrawork mode activated`, { sessionID: input.sessionID })
|
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
|
ctx.client.tui
|
||||||
.showToast({
|
.showToast({
|
||||||
|
|||||||
16
src/index.ts
16
src/index.ts
@ -37,6 +37,8 @@ import {
|
|||||||
createContextInjectorMessagesTransformHook,
|
createContextInjectorMessagesTransformHook,
|
||||||
} from "./features/context-injector";
|
} from "./features/context-injector";
|
||||||
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
|
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
|
||||||
|
import { applyAgentVariant, resolveAgentVariant } from "./shared/agent-variant";
|
||||||
|
import { createFirstMessageVariantGate } from "./shared/first-message-variant";
|
||||||
import {
|
import {
|
||||||
discoverUserClaudeSkills,
|
discoverUserClaudeSkills,
|
||||||
discoverProjectClaudeSkills,
|
discoverProjectClaudeSkills,
|
||||||
@ -82,6 +84,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
|
|
||||||
const pluginConfig = loadPluginConfig(ctx.directory, ctx);
|
const pluginConfig = loadPluginConfig(ctx.directory, ctx);
|
||||||
const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
|
const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
|
||||||
|
const firstMessageVariantGate = createFirstMessageVariantGate();
|
||||||
const isHookEnabled = (hookName: HookName) => !disabledHooks.has(hookName);
|
const isHookEnabled = (hookName: HookName) => !disabledHooks.has(hookName);
|
||||||
|
|
||||||
const modelCacheState = createModelCacheState();
|
const modelCacheState = createModelCacheState();
|
||||||
@ -316,6 +319,17 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
"chat.message": async (input, output) => {
|
"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 keywordDetector?.["chat.message"]?.(input, output);
|
||||||
await claudeCodeHooks["chat.message"]?.(input, output);
|
await claudeCodeHooks["chat.message"]?.(input, output);
|
||||||
await contextInjector["chat.message"]?.(input, output);
|
await contextInjector["chat.message"]?.(input, output);
|
||||||
@ -422,6 +436,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
if (!sessionInfo?.parentID) {
|
if (!sessionInfo?.parentID) {
|
||||||
setMainSession(sessionInfo?.id);
|
setMainSession(sessionInfo?.id);
|
||||||
}
|
}
|
||||||
|
firstMessageVariantGate.markSessionCreated(sessionInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "session.deleted") {
|
if (event.type === "session.deleted") {
|
||||||
@ -431,6 +446,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
}
|
}
|
||||||
if (sessionInfo?.id) {
|
if (sessionInfo?.id) {
|
||||||
clearSessionAgent(sessionInfo.id);
|
clearSessionAgent(sessionInfo.id);
|
||||||
|
firstMessageVariantGate.clear(sessionInfo.id);
|
||||||
await skillMcpManager.disconnectSession(sessionInfo.id);
|
await skillMcpManager.disconnectSession(sessionInfo.id);
|
||||||
await lspManager.cleanupTempDirectoryClients();
|
await lspManager.cleanupTempDirectoryClients();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -103,7 +103,8 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
|||||||
pluginConfig.disabled_agents,
|
pluginConfig.disabled_agents,
|
||||||
pluginConfig.agents,
|
pluginConfig.agents,
|
||||||
ctx.directory,
|
ctx.directory,
|
||||||
config.model as string | undefined
|
config.model as string | undefined,
|
||||||
|
pluginConfig.categories
|
||||||
);
|
);
|
||||||
|
|
||||||
// Claude Code agents: Do NOT apply permission migration
|
// 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 "./permission-compat"
|
||||||
export * from "./external-plugin-detector"
|
export * from "./external-plugin-detector"
|
||||||
export * from "./zip-extractor"
|
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", () => {
|
describe("skills parameter", () => {
|
||||||
test("SISYPHUS_TASK_DESCRIPTION documents skills parameter", () => {
|
test("SISYPHUS_TASK_DESCRIPTION documents skills parameter", () => {
|
||||||
// #given / #when / #then
|
// #given / #when / #then
|
||||||
|
|||||||
@ -315,7 +315,7 @@ ${textContent || "(No text output)"}`
|
|||||||
}
|
}
|
||||||
|
|
||||||
let agentToUse: string
|
let agentToUse: string
|
||||||
let categoryModel: { providerID: string; modelID: string } | undefined
|
let categoryModel: { providerID: string; modelID: string; variant?: string } | undefined
|
||||||
let categoryPromptAppend: string | undefined
|
let categoryPromptAppend: string | undefined
|
||||||
|
|
||||||
if (args.category) {
|
if (args.category) {
|
||||||
@ -325,7 +325,12 @@ ${textContent || "(No text output)"}`
|
|||||||
}
|
}
|
||||||
|
|
||||||
agentToUse = SISYPHUS_JUNIOR_AGENT
|
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
|
categoryPromptAppend = resolved.promptAppend || undefined
|
||||||
} else {
|
} else {
|
||||||
agentToUse = args.subagent_type!.trim()
|
agentToUse = args.subagent_type!.trim()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user