feat(config): add custom_agents overrides and strict agent validation
This commit is contained in:
parent
15519b9580
commit
ae12f2e9d2
@ -3157,6 +3157,210 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"custom_agents": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^(?!(?:build|plan|sisyphus|hephaestus|sisyphus-junior|OpenCode-Builder|prometheus|metis|momus|oracle|librarian|explore|multimodal-looker|atlas)$).+"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"model": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"skills": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 2
|
||||||
|
},
|
||||||
|
"top_p": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 1
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"prompt_append": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disable": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"subagent",
|
||||||
|
"primary",
|
||||||
|
"all"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^#[0-9A-Fa-f]{6}$"
|
||||||
|
},
|
||||||
|
"permission": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"edit": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ask",
|
||||||
|
"allow",
|
||||||
|
"deny"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bash": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ask",
|
||||||
|
"allow",
|
||||||
|
"deny"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ask",
|
||||||
|
"allow",
|
||||||
|
"deny"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"webfetch": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ask",
|
||||||
|
"allow",
|
||||||
|
"deny"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"task": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ask",
|
||||||
|
"allow",
|
||||||
|
"deny"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"doom_loop": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ask",
|
||||||
|
"allow",
|
||||||
|
"deny"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"external_directory": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ask",
|
||||||
|
"allow",
|
||||||
|
"deny"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"maxTokens": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"thinking": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"enabled",
|
||||||
|
"disabled"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"budgetTokens": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"ultrawork": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"model": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"model"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"reasoningEffort": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high",
|
||||||
|
"xhigh"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"textVerbosity": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"providerOptions": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"additionalProperties": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"propertyNames": {
|
"propertyNames": {
|
||||||
|
|||||||
@ -1,11 +1,25 @@
|
|||||||
export {
|
export {
|
||||||
OhMyOpenCodeConfigSchema,
|
OhMyOpenCodeConfigSchema,
|
||||||
|
AgentOverrideConfigSchema,
|
||||||
|
AgentOverridesSchema,
|
||||||
|
CustomAgentOverridesSchema,
|
||||||
|
McpNameSchema,
|
||||||
|
AgentNameSchema,
|
||||||
|
OverridableAgentNameSchema,
|
||||||
|
HookNameSchema,
|
||||||
|
BuiltinCommandNameSchema,
|
||||||
|
SisyphusAgentConfigSchema,
|
||||||
|
ExperimentalConfigSchema,
|
||||||
|
RalphLoopConfigSchema,
|
||||||
|
TmuxConfigSchema,
|
||||||
|
TmuxLayoutSchema,
|
||||||
} from "./schema"
|
} from "./schema"
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
OhMyOpenCodeConfig,
|
OhMyOpenCodeConfig,
|
||||||
AgentOverrideConfig,
|
AgentOverrideConfig,
|
||||||
AgentOverrides,
|
AgentOverrides,
|
||||||
|
CustomAgentOverrides,
|
||||||
McpName,
|
McpName,
|
||||||
AgentName,
|
AgentName,
|
||||||
HookName,
|
HookName,
|
||||||
|
|||||||
32
src/config/schema-document.test.ts
Normal file
32
src/config/schema-document.test.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { createOhMyOpenCodeJsonSchema } from "../../script/build-schema-document"
|
||||||
|
|
||||||
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
||||||
|
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("schema document generation", () => {
|
||||||
|
test("custom_agents schema allows arbitrary custom agent keys with override shape", () => {
|
||||||
|
// given
|
||||||
|
const schema = createOhMyOpenCodeJsonSchema()
|
||||||
|
|
||||||
|
// when
|
||||||
|
const rootProperties = asRecord(schema.properties)
|
||||||
|
const agentsSchema = asRecord(rootProperties?.agents)
|
||||||
|
const customAgentsSchema = asRecord(rootProperties?.custom_agents)
|
||||||
|
const customPropertyNames = asRecord(customAgentsSchema?.propertyNames)
|
||||||
|
const customAdditionalProperties = asRecord(customAgentsSchema?.additionalProperties)
|
||||||
|
const customAgentProperties = asRecord(customAdditionalProperties?.properties)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(agentsSchema).toBeDefined()
|
||||||
|
expect(agentsSchema?.additionalProperties).toBeFalse()
|
||||||
|
expect(customAgentsSchema).toBeDefined()
|
||||||
|
expect(customPropertyNames?.pattern).toBeDefined()
|
||||||
|
expect(customAdditionalProperties).toBeDefined()
|
||||||
|
expect(customAgentProperties?.model).toEqual({ type: "string" })
|
||||||
|
expect(customAgentProperties?.temperature).toEqual(
|
||||||
|
expect.objectContaining({ type: "number" }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -530,6 +530,79 @@ describe("Sisyphus-Junior agent override", () => {
|
|||||||
expect(result.data.agents?.momus?.category).toBe("quick")
|
expect(result.data.agents?.momus?.category).toBe("quick")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("schema accepts custom_agents override keys", () => {
|
||||||
|
// given
|
||||||
|
const config = {
|
||||||
|
custom_agents: {
|
||||||
|
translator: {
|
||||||
|
model: "google/gemini-3-flash-preview",
|
||||||
|
temperature: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = OhMyOpenCodeConfigSchema.safeParse(config)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.data.custom_agents?.translator?.model).toBe("google/gemini-3-flash-preview")
|
||||||
|
expect(result.data.custom_agents?.translator?.temperature).toBe(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("schema rejects unknown keys under agents", () => {
|
||||||
|
// given
|
||||||
|
const config = {
|
||||||
|
agents: {
|
||||||
|
sisyphuss: {
|
||||||
|
model: "openai/gpt-5.3-codex",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = OhMyOpenCodeConfigSchema.safeParse(config)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("schema rejects built-in agent names under custom_agents", () => {
|
||||||
|
// given
|
||||||
|
const config = {
|
||||||
|
custom_agents: {
|
||||||
|
sisyphus: {
|
||||||
|
model: "openai/gpt-5.3-codex",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = OhMyOpenCodeConfigSchema.safeParse(config)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("schema rejects built-in agent names under custom_agents case-insensitively", () => {
|
||||||
|
// given
|
||||||
|
const config = {
|
||||||
|
custom_agents: {
|
||||||
|
Sisyphus: {
|
||||||
|
model: "openai/gpt-5.3-codex",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = OhMyOpenCodeConfigSchema.safeParse(config)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("BrowserAutomationProviderSchema", () => {
|
describe("BrowserAutomationProviderSchema", () => {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { FallbackModelsSchema } from "./fallback-models"
|
import { FallbackModelsSchema } from "./fallback-models"
|
||||||
|
import { OverridableAgentNameSchema } from "./agent-names"
|
||||||
import { AgentPermissionSchema } from "./internal/permission"
|
import { AgentPermissionSchema } from "./internal/permission"
|
||||||
|
|
||||||
export const AgentOverrideConfigSchema = z.object({
|
export const AgentOverrideConfigSchema = z.object({
|
||||||
@ -55,7 +56,7 @@ export const AgentOverrideConfigSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const AgentOverridesSchema = z.object({
|
const BuiltinAgentOverridesSchema = z.object({
|
||||||
build: AgentOverrideConfigSchema.optional(),
|
build: AgentOverrideConfigSchema.optional(),
|
||||||
plan: AgentOverrideConfigSchema.optional(),
|
plan: AgentOverrideConfigSchema.optional(),
|
||||||
sisyphus: AgentOverrideConfigSchema.optional(),
|
sisyphus: AgentOverrideConfigSchema.optional(),
|
||||||
@ -70,7 +71,38 @@ export const AgentOverridesSchema = z.object({
|
|||||||
explore: AgentOverrideConfigSchema.optional(),
|
explore: AgentOverrideConfigSchema.optional(),
|
||||||
"multimodal-looker": AgentOverrideConfigSchema.optional(),
|
"multimodal-looker": AgentOverrideConfigSchema.optional(),
|
||||||
atlas: AgentOverrideConfigSchema.optional(),
|
atlas: AgentOverrideConfigSchema.optional(),
|
||||||
})
|
}).strict()
|
||||||
|
|
||||||
|
export const AgentOverridesSchema = BuiltinAgentOverridesSchema
|
||||||
|
|
||||||
|
const RESERVED_CUSTOM_AGENT_NAMES = OverridableAgentNameSchema.options
|
||||||
|
const RESERVED_CUSTOM_AGENT_NAME_SET = new Set(
|
||||||
|
RESERVED_CUSTOM_AGENT_NAMES.map((name) => name.toLowerCase()),
|
||||||
|
)
|
||||||
|
const RESERVED_CUSTOM_AGENT_NAME_PATTERN = new RegExp(
|
||||||
|
`^(?!(?:${RESERVED_CUSTOM_AGENT_NAMES.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})$).+`,
|
||||||
|
)
|
||||||
|
|
||||||
|
export const CustomAgentOverridesSchema = z
|
||||||
|
.record(
|
||||||
|
z.string().regex(
|
||||||
|
RESERVED_CUSTOM_AGENT_NAME_PATTERN,
|
||||||
|
"custom_agents key cannot reuse built-in agent override name",
|
||||||
|
),
|
||||||
|
AgentOverrideConfigSchema,
|
||||||
|
)
|
||||||
|
.superRefine((value, ctx) => {
|
||||||
|
for (const key of Object.keys(value)) {
|
||||||
|
if (RESERVED_CUSTOM_AGENT_NAME_SET.has(key.toLowerCase())) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: [key],
|
||||||
|
message: "custom_agents key cannot reuse built-in agent override name",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export type AgentOverrideConfig = z.infer<typeof AgentOverrideConfigSchema>
|
export type AgentOverrideConfig = z.infer<typeof AgentOverrideConfigSchema>
|
||||||
export type AgentOverrides = z.infer<typeof AgentOverridesSchema>
|
export type AgentOverrides = z.infer<typeof AgentOverridesSchema>
|
||||||
|
export type CustomAgentOverrides = z.infer<typeof CustomAgentOverridesSchema>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { AnyMcpNameSchema } from "../../mcp/types"
|
import { AnyMcpNameSchema } from "../../mcp/types"
|
||||||
import { BuiltinAgentNameSchema, BuiltinSkillNameSchema } from "./agent-names"
|
import { BuiltinAgentNameSchema, BuiltinSkillNameSchema } from "./agent-names"
|
||||||
import { AgentOverridesSchema } from "./agent-overrides"
|
import { AgentOverridesSchema, CustomAgentOverridesSchema } from "./agent-overrides"
|
||||||
import { BabysittingConfigSchema } from "./babysitting"
|
import { BabysittingConfigSchema } from "./babysitting"
|
||||||
import { BackgroundTaskConfigSchema } from "./background-task"
|
import { BackgroundTaskConfigSchema } from "./background-task"
|
||||||
import { BrowserAutomationConfigSchema } from "./browser-automation"
|
import { BrowserAutomationConfigSchema } from "./browser-automation"
|
||||||
@ -38,6 +38,7 @@ export const OhMyOpenCodeConfigSchema = z.object({
|
|||||||
/** Enable model fallback on API errors (default: false). Set to true to enable automatic model switching when model errors occur. */
|
/** Enable model fallback on API errors (default: false). Set to true to enable automatic model switching when model errors occur. */
|
||||||
model_fallback: z.boolean().optional(),
|
model_fallback: z.boolean().optional(),
|
||||||
agents: AgentOverridesSchema.optional(),
|
agents: AgentOverridesSchema.optional(),
|
||||||
|
custom_agents: CustomAgentOverridesSchema.optional(),
|
||||||
categories: CategoriesConfigSchema.optional(),
|
categories: CategoriesConfigSchema.optional(),
|
||||||
claude_code: ClaudeCodeConfigSchema.optional(),
|
claude_code: ClaudeCodeConfigSchema.optional(),
|
||||||
sisyphus_agent: SisyphusAgentConfigSchema.optional(),
|
sisyphus_agent: SisyphusAgentConfigSchema.optional(),
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import { mergeConfigs, parseConfigPartially } from "./plugin-config";
|
import {
|
||||||
|
detectLikelyBuiltinAgentTypos,
|
||||||
|
detectUnknownBuiltinAgentKeys,
|
||||||
|
mergeConfigs,
|
||||||
|
parseConfigPartially,
|
||||||
|
} from "./plugin-config";
|
||||||
import type { OhMyOpenCodeConfig } from "./config";
|
import type { OhMyOpenCodeConfig } from "./config";
|
||||||
|
|
||||||
describe("mergeConfigs", () => {
|
describe("mergeConfigs", () => {
|
||||||
@ -115,6 +120,27 @@ describe("mergeConfigs", () => {
|
|||||||
expect(result.disabled_hooks).toContain("session-recovery");
|
expect(result.disabled_hooks).toContain("session-recovery");
|
||||||
expect(result.disabled_hooks?.length).toBe(3);
|
expect(result.disabled_hooks?.length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should deep merge custom_agents", () => {
|
||||||
|
const base: OhMyOpenCodeConfig = {
|
||||||
|
custom_agents: {
|
||||||
|
translator: { model: "google/gemini-3-flash-preview" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const override: OhMyOpenCodeConfig = {
|
||||||
|
custom_agents: {
|
||||||
|
translator: { temperature: 0 },
|
||||||
|
"database-architect": { model: "openai/gpt-5.3-codex" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = mergeConfigs(base, override)
|
||||||
|
|
||||||
|
expect(result.custom_agents?.translator?.model).toBe("google/gemini-3-flash-preview")
|
||||||
|
expect(result.custom_agents?.translator?.temperature).toBe(0)
|
||||||
|
expect(result.custom_agents?.["database-architect"]?.model).toBe("openai/gpt-5.3-codex")
|
||||||
|
})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -165,7 +191,9 @@ describe("parseConfigPartially", () => {
|
|||||||
|
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result!.disabled_hooks).toEqual(["comment-checker"]);
|
expect(result!.disabled_hooks).toEqual(["comment-checker"]);
|
||||||
expect(result!.agents).toBeUndefined();
|
expect(result!.agents?.oracle?.model).toBe("openai/gpt-5.2");
|
||||||
|
expect(result!.agents?.momus?.model).toBe("openai/gpt-5.2");
|
||||||
|
expect((result!.agents as Record<string, unknown>)?.prometheus).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should preserve valid agents when a non-agent section is invalid", () => {
|
it("should preserve valid agents when a non-agent section is invalid", () => {
|
||||||
@ -182,6 +210,36 @@ describe("parseConfigPartially", () => {
|
|||||||
expect(result!.agents?.oracle?.model).toBe("openai/gpt-5.2");
|
expect(result!.agents?.oracle?.model).toBe("openai/gpt-5.2");
|
||||||
expect(result!.disabled_hooks).toEqual(["not-a-real-hook"]);
|
expect(result!.disabled_hooks).toEqual(["not-a-real-hook"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should preserve valid built-in agent entries when agents contains unknown keys", () => {
|
||||||
|
const rawConfig = {
|
||||||
|
agents: {
|
||||||
|
sisyphus: { model: "openai/gpt-5.3-codex" },
|
||||||
|
sisyphuss: { model: "openai/gpt-5.3-codex" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = parseConfigPartially(rawConfig);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.agents?.sisyphus?.model).toBe("openai/gpt-5.3-codex");
|
||||||
|
expect((result!.agents as Record<string, unknown>)?.sisyphuss).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve valid custom_agents entries when custom_agents contains reserved names", () => {
|
||||||
|
const rawConfig = {
|
||||||
|
custom_agents: {
|
||||||
|
translator: { model: "google/gemini-3-flash-preview" },
|
||||||
|
sisyphus: { model: "openai/gpt-5.3-codex" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = parseConfigPartially(rawConfig);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.custom_agents?.translator?.model).toBe("google/gemini-3-flash-preview");
|
||||||
|
expect((result!.custom_agents as Record<string, unknown>)?.sisyphus).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("completely invalid config", () => {
|
describe("completely invalid config", () => {
|
||||||
@ -237,3 +295,79 @@ describe("parseConfigPartially", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("detectLikelyBuiltinAgentTypos", () => {
|
||||||
|
it("detects near-miss builtin agent keys", () => {
|
||||||
|
const rawConfig = {
|
||||||
|
agents: {
|
||||||
|
sisyphuss: { model: "openai/gpt-5.2" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const warnings = detectLikelyBuiltinAgentTypos(rawConfig)
|
||||||
|
|
||||||
|
expect(warnings).toEqual([
|
||||||
|
{
|
||||||
|
key: "sisyphuss",
|
||||||
|
suggestion: "sisyphus",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("suggests canonical key casing for OpenCode-Builder typos", () => {
|
||||||
|
const rawConfig = {
|
||||||
|
agents: {
|
||||||
|
"opencode-buildr": { model: "openai/gpt-5.2" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const warnings = detectLikelyBuiltinAgentTypos(rawConfig)
|
||||||
|
|
||||||
|
expect(warnings).toEqual([
|
||||||
|
{
|
||||||
|
key: "opencode-buildr",
|
||||||
|
suggestion: "OpenCode-Builder",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not flag valid custom agent names", () => {
|
||||||
|
const rawConfig = {
|
||||||
|
agents: {
|
||||||
|
translator: { model: "google/gemini-3-flash-preview" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const warnings = detectLikelyBuiltinAgentTypos(rawConfig)
|
||||||
|
|
||||||
|
expect(warnings).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("detectUnknownBuiltinAgentKeys", () => {
|
||||||
|
it("returns unknown keys under agents", () => {
|
||||||
|
const rawConfig = {
|
||||||
|
agents: {
|
||||||
|
sisyphus: { model: "openai/gpt-5.2" },
|
||||||
|
translator: { model: "google/gemini-3-flash-preview" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const unknownKeys = detectUnknownBuiltinAgentKeys(rawConfig)
|
||||||
|
|
||||||
|
expect(unknownKeys).toEqual(["translator"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns empty array when all keys are built-ins", () => {
|
||||||
|
const rawConfig = {
|
||||||
|
agents: {
|
||||||
|
sisyphus: { model: "openai/gpt-5.2" },
|
||||||
|
prometheus: { model: "openai/gpt-5.2" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const unknownKeys = detectUnknownBuiltinAgentKeys(rawConfig)
|
||||||
|
|
||||||
|
expect(unknownKeys).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "./config";
|
import {
|
||||||
|
OhMyOpenCodeConfigSchema,
|
||||||
|
OverridableAgentNameSchema,
|
||||||
|
type OhMyOpenCodeConfig,
|
||||||
|
} from "./config";
|
||||||
import {
|
import {
|
||||||
log,
|
log,
|
||||||
deepMerge,
|
deepMerge,
|
||||||
@ -11,6 +15,81 @@ import {
|
|||||||
migrateConfigFile,
|
migrateConfigFile,
|
||||||
} from "./shared";
|
} from "./shared";
|
||||||
|
|
||||||
|
const BUILTIN_AGENT_OVERRIDE_KEYS = OverridableAgentNameSchema.options;
|
||||||
|
const BUILTIN_AGENT_OVERRIDE_KEYS_BY_LOWER = new Map(
|
||||||
|
BUILTIN_AGENT_OVERRIDE_KEYS.map((key) => [key.toLowerCase(), key]),
|
||||||
|
);
|
||||||
|
|
||||||
|
function levenshteinDistance(a: string, b: string): number {
|
||||||
|
const rows = a.length + 1;
|
||||||
|
const cols = b.length + 1;
|
||||||
|
const matrix: number[][] = Array.from({ length: rows }, () => Array(cols).fill(0));
|
||||||
|
|
||||||
|
for (let i = 0; i < rows; i += 1) matrix[i][0] = i;
|
||||||
|
for (let j = 0; j < cols; j += 1) matrix[0][j] = j;
|
||||||
|
|
||||||
|
for (let i = 1; i < rows; i += 1) {
|
||||||
|
for (let j = 1; j < cols; j += 1) {
|
||||||
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
||||||
|
matrix[i][j] = Math.min(
|
||||||
|
matrix[i - 1][j] + 1,
|
||||||
|
matrix[i][j - 1] + 1,
|
||||||
|
matrix[i - 1][j - 1] + cost,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matrix[rows - 1][cols - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentTypoWarning = {
|
||||||
|
key: string;
|
||||||
|
suggestion: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function detectLikelyBuiltinAgentTypos(
|
||||||
|
rawConfig: Record<string, unknown>,
|
||||||
|
): AgentTypoWarning[] {
|
||||||
|
const agents = rawConfig.agents;
|
||||||
|
if (!agents || typeof agents !== "object") return [];
|
||||||
|
|
||||||
|
const warnings: AgentTypoWarning[] = [];
|
||||||
|
for (const key of Object.keys(agents)) {
|
||||||
|
const lowerKey = key.toLowerCase();
|
||||||
|
if (BUILTIN_AGENT_OVERRIDE_KEYS_BY_LOWER.has(lowerKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bestMatchLower: string | undefined;
|
||||||
|
let bestDistance = Number.POSITIVE_INFINITY;
|
||||||
|
for (const builtinKey of BUILTIN_AGENT_OVERRIDE_KEYS) {
|
||||||
|
const distance = levenshteinDistance(lowerKey, builtinKey.toLowerCase());
|
||||||
|
if (distance < bestDistance) {
|
||||||
|
bestDistance = distance;
|
||||||
|
bestMatchLower = builtinKey.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestMatchLower && bestDistance <= 2) {
|
||||||
|
const suggestion = BUILTIN_AGENT_OVERRIDE_KEYS_BY_LOWER.get(bestMatchLower) ?? bestMatchLower;
|
||||||
|
warnings.push({ key, suggestion });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function detectUnknownBuiltinAgentKeys(
|
||||||
|
rawConfig: Record<string, unknown>,
|
||||||
|
): string[] {
|
||||||
|
const agents = rawConfig.agents;
|
||||||
|
if (!agents || typeof agents !== "object") return [];
|
||||||
|
|
||||||
|
return Object.keys(agents).filter(
|
||||||
|
(key) => !BUILTIN_AGENT_OVERRIDE_KEYS_BY_LOWER.has(key.toLowerCase()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function parseConfigPartially(
|
export function parseConfigPartially(
|
||||||
rawConfig: Record<string, unknown>
|
rawConfig: Record<string, unknown>
|
||||||
): OhMyOpenCodeConfig | null {
|
): OhMyOpenCodeConfig | null {
|
||||||
@ -22,7 +101,52 @@ export function parseConfigPartially(
|
|||||||
const partialConfig: Record<string, unknown> = {};
|
const partialConfig: Record<string, unknown> = {};
|
||||||
const invalidSections: string[] = [];
|
const invalidSections: string[] = [];
|
||||||
|
|
||||||
|
const parseAgentSectionEntries = (sectionKey: "agents" | "custom_agents"): void => {
|
||||||
|
const rawSection = rawConfig[sectionKey];
|
||||||
|
if (!rawSection || typeof rawSection !== "object") return;
|
||||||
|
|
||||||
|
const parsedSection: Record<string, unknown> = {};
|
||||||
|
const invalidEntries: string[] = [];
|
||||||
|
|
||||||
|
for (const [entryKey, entryValue] of Object.entries(rawSection)) {
|
||||||
|
const singleEntryResult = OhMyOpenCodeConfigSchema.safeParse({
|
||||||
|
[sectionKey]: { [entryKey]: entryValue },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (singleEntryResult.success) {
|
||||||
|
const parsed = singleEntryResult.data as Record<string, unknown>;
|
||||||
|
const parsedSectionValue = parsed[sectionKey];
|
||||||
|
if (parsedSectionValue && typeof parsedSectionValue === "object") {
|
||||||
|
const typedSection = parsedSectionValue as Record<string, unknown>;
|
||||||
|
if (typedSection[entryKey] !== undefined) {
|
||||||
|
parsedSection[entryKey] = typedSection[entryKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryErrors = singleEntryResult.error.issues
|
||||||
|
.map((issue) => `${entryKey}: ${issue.message}`)
|
||||||
|
.join(", ");
|
||||||
|
if (entryErrors) {
|
||||||
|
invalidEntries.push(entryErrors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(parsedSection).length > 0) {
|
||||||
|
partialConfig[sectionKey] = parsedSection;
|
||||||
|
}
|
||||||
|
if (invalidEntries.length > 0) {
|
||||||
|
invalidSections.push(`${sectionKey}: ${invalidEntries.join(", ")}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for (const key of Object.keys(rawConfig)) {
|
for (const key of Object.keys(rawConfig)) {
|
||||||
|
if (key === "agents" || key === "custom_agents") {
|
||||||
|
parseAgentSectionEntries(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const sectionResult = OhMyOpenCodeConfigSchema.safeParse({ [key]: rawConfig[key] });
|
const sectionResult = OhMyOpenCodeConfigSchema.safeParse({ [key]: rawConfig[key] });
|
||||||
if (sectionResult.success) {
|
if (sectionResult.success) {
|
||||||
const parsed = sectionResult.data as Record<string, unknown>;
|
const parsed = sectionResult.data as Record<string, unknown>;
|
||||||
@ -58,6 +182,29 @@ export function loadConfigFromPath(
|
|||||||
|
|
||||||
migrateConfigFile(configPath, rawConfig);
|
migrateConfigFile(configPath, rawConfig);
|
||||||
|
|
||||||
|
const typoWarnings = detectLikelyBuiltinAgentTypos(rawConfig);
|
||||||
|
if (typoWarnings.length > 0) {
|
||||||
|
const warningMsg = typoWarnings
|
||||||
|
.map((warning) => `agents.${warning.key} (did you mean agents.${warning.suggestion}?)`)
|
||||||
|
.join(", ");
|
||||||
|
log(`Potential agent override typos in ${configPath}: ${warningMsg}`);
|
||||||
|
addConfigLoadError({
|
||||||
|
path: configPath,
|
||||||
|
error: `Potential agent override typos detected: ${warningMsg}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const unknownAgentKeys = detectUnknownBuiltinAgentKeys(rawConfig);
|
||||||
|
if (unknownAgentKeys.length > 0) {
|
||||||
|
const unknownKeysMsg = unknownAgentKeys.map((key) => `agents.${key}`).join(", ");
|
||||||
|
const migrationHint = "Move custom entries from agents.* to custom_agents.*";
|
||||||
|
log(`Unknown built-in agent override keys in ${configPath}: ${unknownKeysMsg}. ${migrationHint}`);
|
||||||
|
addConfigLoadError({
|
||||||
|
path: configPath,
|
||||||
|
error: `Unknown built-in agent override keys: ${unknownKeysMsg}. ${migrationHint}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const result = OhMyOpenCodeConfigSchema.safeParse(rawConfig);
|
const result = OhMyOpenCodeConfigSchema.safeParse(rawConfig);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@ -98,6 +245,7 @@ export function mergeConfigs(
|
|||||||
...base,
|
...base,
|
||||||
...override,
|
...override,
|
||||||
agents: deepMerge(base.agents, override.agents),
|
agents: deepMerge(base.agents, override.agents),
|
||||||
|
custom_agents: deepMerge(base.custom_agents, override.custom_agents),
|
||||||
categories: deepMerge(base.categories, override.categories),
|
categories: deepMerge(base.categories, override.categories),
|
||||||
disabled_agents: [
|
disabled_agents: [
|
||||||
...new Set([
|
...new Set([
|
||||||
@ -170,6 +318,7 @@ export function loadPluginConfig(
|
|||||||
|
|
||||||
log("Final merged config", {
|
log("Final merged config", {
|
||||||
agents: config.agents,
|
agents: config.agents,
|
||||||
|
custom_agents: config.custom_agents,
|
||||||
disabled_agents: config.disabled_agents,
|
disabled_agents: config.disabled_agents,
|
||||||
disabled_mcps: config.disabled_mcps,
|
disabled_mcps: config.disabled_mcps,
|
||||||
disabled_hooks: config.disabled_hooks,
|
disabled_hooks: config.disabled_hooks,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import type { OhMyOpenCodeConfig } from "../config";
|
|||||||
import { log, migrateAgentConfig } from "../shared";
|
import { log, migrateAgentConfig } from "../shared";
|
||||||
import { AGENT_NAME_MAP } from "../shared/migration";
|
import { AGENT_NAME_MAP } from "../shared/migration";
|
||||||
import { getAgentDisplayName } from "../shared/agent-display-names";
|
import { getAgentDisplayName } from "../shared/agent-display-names";
|
||||||
|
import { mergeCategories } from "../shared/merge-categories";
|
||||||
import {
|
import {
|
||||||
discoverConfigSourceSkills,
|
discoverConfigSourceSkills,
|
||||||
discoverOpencodeGlobalSkills,
|
discoverOpencodeGlobalSkills,
|
||||||
@ -17,6 +18,13 @@ import { reorderAgentsByPriority } from "./agent-priority-order";
|
|||||||
import { remapAgentKeysToDisplayNames } from "./agent-key-remapper";
|
import { remapAgentKeysToDisplayNames } from "./agent-key-remapper";
|
||||||
import { buildPrometheusAgentConfig } from "./prometheus-agent-config-builder";
|
import { buildPrometheusAgentConfig } from "./prometheus-agent-config-builder";
|
||||||
import { buildPlanDemoteConfig } from "./plan-model-inheritance";
|
import { buildPlanDemoteConfig } from "./plan-model-inheritance";
|
||||||
|
import {
|
||||||
|
applyCustomAgentOverrides,
|
||||||
|
collectCustomAgentSummariesFromRecord,
|
||||||
|
mergeCustomAgentSummaries,
|
||||||
|
collectKnownCustomAgentNames,
|
||||||
|
filterSummariesByKnownNames,
|
||||||
|
} from "./custom-agent-utils";
|
||||||
|
|
||||||
type AgentConfigRecord = Record<string, Record<string, unknown> | undefined> & {
|
type AgentConfigRecord = Record<string, Record<string, unknown> | undefined> & {
|
||||||
build?: Record<string, unknown>;
|
build?: Record<string, unknown>;
|
||||||
@ -78,22 +86,6 @@ export async function applyAgentConfig(params: {
|
|||||||
const useTaskSystem = params.pluginConfig.experimental?.task_system ?? false;
|
const useTaskSystem = params.pluginConfig.experimental?.task_system ?? false;
|
||||||
const disableOmoEnv = params.pluginConfig.experimental?.disable_omo_env ?? false;
|
const disableOmoEnv = params.pluginConfig.experimental?.disable_omo_env ?? false;
|
||||||
|
|
||||||
const builtinAgents = await createBuiltinAgents(
|
|
||||||
migratedDisabledAgents,
|
|
||||||
params.pluginConfig.agents,
|
|
||||||
params.ctx.directory,
|
|
||||||
currentModel,
|
|
||||||
params.pluginConfig.categories,
|
|
||||||
params.pluginConfig.git_master,
|
|
||||||
allDiscoveredSkills,
|
|
||||||
params.ctx.client,
|
|
||||||
browserProvider,
|
|
||||||
currentModel,
|
|
||||||
disabledSkills,
|
|
||||||
useTaskSystem,
|
|
||||||
disableOmoEnv,
|
|
||||||
);
|
|
||||||
|
|
||||||
const includeClaudeAgents = params.pluginConfig.claude_code?.agents ?? true;
|
const includeClaudeAgents = params.pluginConfig.claude_code?.agents ?? true;
|
||||||
const userAgents = includeClaudeAgents ? loadUserAgents() : {};
|
const userAgents = includeClaudeAgents ? loadUserAgents() : {};
|
||||||
const projectAgents = includeClaudeAgents ? loadProjectAgents(params.ctx.directory) : {};
|
const projectAgents = includeClaudeAgents ? loadProjectAgents(params.ctx.directory) : {};
|
||||||
@ -106,6 +98,44 @@ export async function applyAgentConfig(params: {
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const configAgent = params.config.agent as AgentConfigRecord | undefined;
|
||||||
|
const mergedCategories = mergeCategories(params.pluginConfig.categories)
|
||||||
|
const knownCustomAgentNames = collectKnownCustomAgentNames(
|
||||||
|
userAgents as Record<string, unknown>,
|
||||||
|
projectAgents as Record<string, unknown>,
|
||||||
|
pluginAgents as Record<string, unknown>,
|
||||||
|
configAgent as Record<string, unknown> | undefined,
|
||||||
|
)
|
||||||
|
|
||||||
|
const customAgentSummaries = mergeCustomAgentSummaries(
|
||||||
|
collectCustomAgentSummariesFromRecord(userAgents as Record<string, unknown>),
|
||||||
|
collectCustomAgentSummariesFromRecord(projectAgents as Record<string, unknown>),
|
||||||
|
collectCustomAgentSummariesFromRecord(pluginAgents as Record<string, unknown>),
|
||||||
|
collectCustomAgentSummariesFromRecord(configAgent as Record<string, unknown> | undefined),
|
||||||
|
filterSummariesByKnownNames(
|
||||||
|
collectCustomAgentSummariesFromRecord(
|
||||||
|
params.pluginConfig.custom_agents as Record<string, unknown> | undefined,
|
||||||
|
),
|
||||||
|
knownCustomAgentNames,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const builtinAgents = await createBuiltinAgents(
|
||||||
|
migratedDisabledAgents,
|
||||||
|
params.pluginConfig.agents,
|
||||||
|
params.ctx.directory,
|
||||||
|
currentModel,
|
||||||
|
params.pluginConfig.categories,
|
||||||
|
params.pluginConfig.git_master,
|
||||||
|
allDiscoveredSkills,
|
||||||
|
customAgentSummaries,
|
||||||
|
browserProvider,
|
||||||
|
currentModel,
|
||||||
|
disabledSkills,
|
||||||
|
useTaskSystem,
|
||||||
|
disableOmoEnv,
|
||||||
|
);
|
||||||
|
|
||||||
const isSisyphusEnabled = params.pluginConfig.sisyphus_agent?.disabled !== true;
|
const isSisyphusEnabled = params.pluginConfig.sisyphus_agent?.disabled !== true;
|
||||||
const builderEnabled =
|
const builderEnabled =
|
||||||
params.pluginConfig.sisyphus_agent?.default_builder_enabled ?? false;
|
params.pluginConfig.sisyphus_agent?.default_builder_enabled ?? false;
|
||||||
@ -114,8 +144,6 @@ export async function applyAgentConfig(params: {
|
|||||||
const shouldDemotePlan = plannerEnabled && replacePlan;
|
const shouldDemotePlan = plannerEnabled && replacePlan;
|
||||||
const configuredDefaultAgent = getConfiguredDefaultAgent(params.config);
|
const configuredDefaultAgent = getConfiguredDefaultAgent(params.config);
|
||||||
|
|
||||||
const configAgent = params.config.agent as AgentConfigRecord | undefined;
|
|
||||||
|
|
||||||
if (isSisyphusEnabled && builtinAgents.sisyphus) {
|
if (isSisyphusEnabled && builtinAgents.sisyphus) {
|
||||||
if (configuredDefaultAgent) {
|
if (configuredDefaultAgent) {
|
||||||
(params.config as { default_agent?: string }).default_agent =
|
(params.config as { default_agent?: string }).default_agent =
|
||||||
@ -159,6 +187,7 @@ export async function applyAgentConfig(params: {
|
|||||||
pluginPrometheusOverride: prometheusOverride,
|
pluginPrometheusOverride: prometheusOverride,
|
||||||
userCategories: params.pluginConfig.categories,
|
userCategories: params.pluginConfig.categories,
|
||||||
currentModel,
|
currentModel,
|
||||||
|
customAgentSummaries,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,6 +240,24 @@ export async function applyAgentConfig(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.config.agent) {
|
||||||
|
const builtinOverrideKeys = new Set([
|
||||||
|
...Object.keys(builtinAgents).map((key) => key.toLowerCase()),
|
||||||
|
"build",
|
||||||
|
"plan",
|
||||||
|
"sisyphus-junior",
|
||||||
|
"opencode-builder",
|
||||||
|
])
|
||||||
|
|
||||||
|
applyCustomAgentOverrides({
|
||||||
|
mergedAgents: params.config.agent as Record<string, unknown>,
|
||||||
|
userOverrides: params.pluginConfig.custom_agents,
|
||||||
|
builtinOverrideKeys,
|
||||||
|
mergedCategories,
|
||||||
|
directory: params.ctx.directory,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (params.config.agent) {
|
if (params.config.agent) {
|
||||||
params.config.agent = remapAgentKeysToDisplayNames(
|
params.config.agent = remapAgentKeysToDisplayNames(
|
||||||
params.config.agent as Record<string, unknown>,
|
params.config.agent as Record<string, unknown>,
|
||||||
|
|||||||
@ -162,6 +162,197 @@ describe("Sisyphus-Junior model inheritance", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("custom agent overrides", () => {
|
||||||
|
test("passes custom agent summaries into builtin agent prompt builder", async () => {
|
||||||
|
// #given
|
||||||
|
;(agentLoader.loadUserAgents as any).mockReturnValue({
|
||||||
|
translator: {
|
||||||
|
name: "translator",
|
||||||
|
mode: "subagent",
|
||||||
|
description: "Translate and localize text",
|
||||||
|
prompt: "Translate content",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as {
|
||||||
|
mock: { calls: unknown[][] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginConfig: OhMyOpenCodeConfig = {
|
||||||
|
sisyphus_agent: {
|
||||||
|
planner_enabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const config: Record<string, unknown> = {
|
||||||
|
model: "anthropic/claude-opus-4-6",
|
||||||
|
agent: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = createConfigHandler({
|
||||||
|
ctx: { directory: "/tmp" },
|
||||||
|
pluginConfig,
|
||||||
|
modelCacheState: {
|
||||||
|
anthropicContext1MEnabled: false,
|
||||||
|
modelContextLimitsCache: new Map(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// #when
|
||||||
|
await handler(config)
|
||||||
|
|
||||||
|
// #then
|
||||||
|
const firstCallArgs = createBuiltinAgentsMock.mock.calls[0]
|
||||||
|
expect(firstCallArgs).toBeDefined()
|
||||||
|
expect(Array.isArray(firstCallArgs[7])).toBe(true)
|
||||||
|
expect(firstCallArgs[7]).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "translator",
|
||||||
|
description: "Translate and localize text",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("applies oh-my-opencode agent overrides to custom Claude agents", async () => {
|
||||||
|
// #given
|
||||||
|
;(agentLoader.loadUserAgents as any).mockReturnValue({
|
||||||
|
translator: {
|
||||||
|
name: "translator",
|
||||||
|
mode: "subagent",
|
||||||
|
description: "(user) translator",
|
||||||
|
prompt: "Base translator prompt",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const pluginConfig: OhMyOpenCodeConfig = {
|
||||||
|
custom_agents: {
|
||||||
|
translator: {
|
||||||
|
model: "google/gemini-3-flash-preview",
|
||||||
|
temperature: 0,
|
||||||
|
prompt_append: "Always preserve placeholders exactly.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: Record<string, unknown> = {
|
||||||
|
model: "anthropic/claude-opus-4-6",
|
||||||
|
agent: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = createConfigHandler({
|
||||||
|
ctx: { directory: "/tmp" },
|
||||||
|
pluginConfig,
|
||||||
|
modelCacheState: {
|
||||||
|
anthropicContext1MEnabled: false,
|
||||||
|
modelContextLimitsCache: new Map(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// #when
|
||||||
|
await handler(config)
|
||||||
|
|
||||||
|
// #then
|
||||||
|
const agentConfig = config.agent as Record<string, { model?: string; temperature?: number; prompt?: string }>
|
||||||
|
expect(agentConfig.translator).toBeDefined()
|
||||||
|
expect(agentConfig.translator.model).toBe("google/gemini-3-flash-preview")
|
||||||
|
expect(agentConfig.translator.temperature).toBe(0)
|
||||||
|
expect(agentConfig.translator.prompt).toContain("Base translator prompt")
|
||||||
|
expect(agentConfig.translator.prompt).toContain("Always preserve placeholders exactly.")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("prometheus prompt includes custom agent catalog for planning", async () => {
|
||||||
|
// #given
|
||||||
|
;(agentLoader.loadUserAgents as any).mockReturnValue({
|
||||||
|
translator: {
|
||||||
|
name: "translator",
|
||||||
|
mode: "subagent",
|
||||||
|
description: "Translate and localize locale files",
|
||||||
|
prompt: "Translate content",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const pluginConfig: OhMyOpenCodeConfig = {
|
||||||
|
sisyphus_agent: {
|
||||||
|
planner_enabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const config: Record<string, unknown> = {
|
||||||
|
model: "anthropic/claude-opus-4-6",
|
||||||
|
agent: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = createConfigHandler({
|
||||||
|
ctx: { directory: "/tmp" },
|
||||||
|
pluginConfig,
|
||||||
|
modelCacheState: {
|
||||||
|
anthropicContext1MEnabled: false,
|
||||||
|
modelContextLimitsCache: new Map(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// #when
|
||||||
|
await handler(config)
|
||||||
|
|
||||||
|
// #then
|
||||||
|
const agentsConfig = config.agent as Record<string, { prompt?: string }>
|
||||||
|
const pKey = getAgentDisplayName("prometheus")
|
||||||
|
expect(agentsConfig[pKey]).toBeDefined()
|
||||||
|
expect(agentsConfig[pKey].prompt).toContain("<custom_agent_catalog>")
|
||||||
|
expect(agentsConfig[pKey].prompt).toContain("translator")
|
||||||
|
expect(agentsConfig[pKey].prompt).toContain("Translate and localize locale files")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("prometheus prompt excludes unknown custom_agents entries", async () => {
|
||||||
|
// #given
|
||||||
|
;(agentLoader.loadUserAgents as any).mockReturnValue({
|
||||||
|
translator: {
|
||||||
|
name: "translator",
|
||||||
|
mode: "subagent",
|
||||||
|
description: "Translate and localize locale files",
|
||||||
|
prompt: "Translate content",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const pluginConfig: OhMyOpenCodeConfig = {
|
||||||
|
custom_agents: {
|
||||||
|
translator: {
|
||||||
|
description: "Translate and localize locale files",
|
||||||
|
},
|
||||||
|
ghostwriter: {
|
||||||
|
description: "This agent does not exist in runtime",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sisyphus_agent: {
|
||||||
|
planner_enabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: Record<string, unknown> = {
|
||||||
|
model: "anthropic/claude-opus-4-6",
|
||||||
|
agent: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = createConfigHandler({
|
||||||
|
ctx: { directory: "/tmp" },
|
||||||
|
pluginConfig,
|
||||||
|
modelCacheState: {
|
||||||
|
anthropicContext1MEnabled: false,
|
||||||
|
modelContextLimitsCache: new Map(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// #when
|
||||||
|
await handler(config)
|
||||||
|
|
||||||
|
// #then
|
||||||
|
const agentsConfig = config.agent as Record<string, { prompt?: string }>
|
||||||
|
const pKey = getAgentDisplayName("prometheus")
|
||||||
|
expect(agentsConfig[pKey]).toBeDefined()
|
||||||
|
expect(agentsConfig[pKey].prompt).toContain("translator")
|
||||||
|
expect(agentsConfig[pKey].prompt).not.toContain("ghostwriter")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("Plan agent demote behavior", () => {
|
describe("Plan agent demote behavior", () => {
|
||||||
test("orders core agents as sisyphus -> hephaestus -> prometheus -> atlas", async () => {
|
test("orders core agents as sisyphus -> hephaestus -> prometheus -> atlas", async () => {
|
||||||
// #given
|
// #given
|
||||||
|
|||||||
136
src/plugin-handlers/custom-agent-utils.ts
Normal file
136
src/plugin-handlers/custom-agent-utils.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import type { AgentConfig } from "@opencode-ai/sdk";
|
||||||
|
import { applyOverrides } from "../agents/builtin-agents/agent-overrides";
|
||||||
|
import type { AgentOverrideConfig } from "../agents/types";
|
||||||
|
import type { OhMyOpenCodeConfig } from "../config";
|
||||||
|
import { getAgentConfigKey } from "../shared/agent-display-names";
|
||||||
|
import { AGENT_NAME_MAP } from "../shared/migration";
|
||||||
|
import { mergeCategories } from "../shared/merge-categories";
|
||||||
|
|
||||||
|
const RESERVED_AGENT_KEYS = new Set(
|
||||||
|
[
|
||||||
|
"build",
|
||||||
|
"plan",
|
||||||
|
"sisyphus-junior",
|
||||||
|
"opencode-builder",
|
||||||
|
...Object.keys(AGENT_NAME_MAP),
|
||||||
|
...Object.values(AGENT_NAME_MAP),
|
||||||
|
].map((key) => getAgentConfigKey(key).toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
export type AgentSummary = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
hidden?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function applyCustomAgentOverrides(params: {
|
||||||
|
mergedAgents: Record<string, unknown>;
|
||||||
|
userOverrides: OhMyOpenCodeConfig["custom_agents"] | undefined;
|
||||||
|
builtinOverrideKeys: Set<string>;
|
||||||
|
mergedCategories: ReturnType<typeof mergeCategories>;
|
||||||
|
directory: string;
|
||||||
|
}): void {
|
||||||
|
if (!params.userOverrides) return;
|
||||||
|
|
||||||
|
for (const [overrideKey, override] of Object.entries(params.userOverrides)) {
|
||||||
|
if (!override) continue;
|
||||||
|
|
||||||
|
const normalizedOverrideKey = getAgentConfigKey(overrideKey).toLowerCase();
|
||||||
|
if (params.builtinOverrideKeys.has(normalizedOverrideKey)) continue;
|
||||||
|
|
||||||
|
const existingKey = Object.keys(params.mergedAgents).find(
|
||||||
|
(key) => key.toLowerCase() === overrideKey.toLowerCase() || key.toLowerCase() === normalizedOverrideKey,
|
||||||
|
);
|
||||||
|
if (!existingKey) continue;
|
||||||
|
|
||||||
|
const existingAgent = params.mergedAgents[existingKey];
|
||||||
|
if (!existingAgent || typeof existingAgent !== "object") continue;
|
||||||
|
|
||||||
|
params.mergedAgents[existingKey] = applyOverrides(
|
||||||
|
existingAgent as AgentConfig,
|
||||||
|
override as AgentOverrideConfig,
|
||||||
|
params.mergedCategories,
|
||||||
|
params.directory,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectCustomAgentSummariesFromRecord(
|
||||||
|
agents: Record<string, unknown> | undefined,
|
||||||
|
): AgentSummary[] {
|
||||||
|
if (!agents) return [];
|
||||||
|
|
||||||
|
const summaries: AgentSummary[] = [];
|
||||||
|
for (const [name, value] of Object.entries(agents)) {
|
||||||
|
const normalizedName = getAgentConfigKey(name).toLowerCase();
|
||||||
|
if (RESERVED_AGENT_KEYS.has(normalizedName)) continue;
|
||||||
|
if (!value || typeof value !== "object") continue;
|
||||||
|
|
||||||
|
const agentValue = value as Record<string, unknown>;
|
||||||
|
const description = typeof agentValue.description === "string" ? agentValue.description : "";
|
||||||
|
|
||||||
|
summaries.push({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
hidden: agentValue.hidden === true,
|
||||||
|
disabled: agentValue.disabled === true,
|
||||||
|
enabled: agentValue.enabled === false ? false : true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return summaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeCustomAgentSummaries(...summaryGroups: AgentSummary[][]): AgentSummary[] {
|
||||||
|
const merged = new Map<string, AgentSummary>();
|
||||||
|
|
||||||
|
for (const group of summaryGroups) {
|
||||||
|
for (const summary of group) {
|
||||||
|
const key = summary.name.toLowerCase();
|
||||||
|
if (!merged.has(key)) {
|
||||||
|
merged.set(key, summary);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = merged.get(key);
|
||||||
|
if (!existing) continue;
|
||||||
|
|
||||||
|
const existingDescription = existing.description.trim();
|
||||||
|
const incomingDescription = summary.description.trim();
|
||||||
|
if (!existingDescription && incomingDescription) {
|
||||||
|
merged.set(key, summary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(merged.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectKnownCustomAgentNames(
|
||||||
|
...agentGroups: Array<Record<string, unknown> | undefined>
|
||||||
|
): Set<string> {
|
||||||
|
const knownNames = new Set<string>();
|
||||||
|
|
||||||
|
for (const group of agentGroups) {
|
||||||
|
if (!group) continue;
|
||||||
|
|
||||||
|
for (const [name, value] of Object.entries(group)) {
|
||||||
|
const normalizedName = getAgentConfigKey(name).toLowerCase();
|
||||||
|
if (RESERVED_AGENT_KEYS.has(normalizedName)) continue;
|
||||||
|
if (!value || typeof value !== "object") continue;
|
||||||
|
|
||||||
|
knownNames.add(normalizedName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return knownNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterSummariesByKnownNames(
|
||||||
|
summaries: AgentSummary[],
|
||||||
|
knownNames: Set<string>,
|
||||||
|
): AgentSummary[] {
|
||||||
|
return summaries.filter((summary) => knownNames.has(summary.name.toLowerCase()));
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import type { CategoryConfig } from "../config/schema";
|
import type { CategoryConfig } from "../config/schema";
|
||||||
import { PROMETHEUS_PERMISSION, getPrometheusPrompt } from "../agents/prometheus";
|
import { PROMETHEUS_PERMISSION, getPrometheusPrompt } from "../agents/prometheus";
|
||||||
import { resolvePromptAppend } from "../agents/builtin-agents/resolve-file-uri";
|
import { resolvePromptAppend } from "../agents/builtin-agents/resolve-file-uri";
|
||||||
|
import { parseRegisteredAgentSummaries } from "../agents/custom-agent-summaries";
|
||||||
import { AGENT_MODEL_REQUIREMENTS } from "../shared/model-requirements";
|
import { AGENT_MODEL_REQUIREMENTS } from "../shared/model-requirements";
|
||||||
import {
|
import {
|
||||||
fetchAvailableModels,
|
fetchAvailableModels,
|
||||||
@ -27,6 +28,7 @@ export async function buildPrometheusAgentConfig(params: {
|
|||||||
pluginPrometheusOverride: PrometheusOverride | undefined;
|
pluginPrometheusOverride: PrometheusOverride | undefined;
|
||||||
userCategories: Record<string, CategoryConfig> | undefined;
|
userCategories: Record<string, CategoryConfig> | undefined;
|
||||||
currentModel: string | undefined;
|
currentModel: string | undefined;
|
||||||
|
customAgentSummaries?: unknown;
|
||||||
}): Promise<Record<string, unknown>> {
|
}): Promise<Record<string, unknown>> {
|
||||||
const categoryConfig = params.pluginPrometheusOverride?.category
|
const categoryConfig = params.pluginPrometheusOverride?.category
|
||||||
? resolveCategoryConfig(params.pluginPrometheusOverride.category, params.userCategories)
|
? resolveCategoryConfig(params.pluginPrometheusOverride.category, params.userCategories)
|
||||||
@ -65,11 +67,18 @@ export async function buildPrometheusAgentConfig(params: {
|
|||||||
const maxTokensToUse =
|
const maxTokensToUse =
|
||||||
params.pluginPrometheusOverride?.maxTokens ?? categoryConfig?.maxTokens;
|
params.pluginPrometheusOverride?.maxTokens ?? categoryConfig?.maxTokens;
|
||||||
|
|
||||||
|
const customAgentCatalog = parseRegisteredAgentSummaries(params.customAgentSummaries)
|
||||||
|
const customAgentBlock = customAgentCatalog.length > 0
|
||||||
|
? `\n\n<custom_agent_catalog>\nAvailable custom agents for planning/delegation:\n${customAgentCatalog
|
||||||
|
.map((agent) => `- ${agent.name}: ${agent.description || "No description provided"}`)
|
||||||
|
.join("\n")}\n</custom_agent_catalog>`
|
||||||
|
: ""
|
||||||
|
|
||||||
const base: Record<string, unknown> = {
|
const base: Record<string, unknown> = {
|
||||||
...(resolvedModel ? { model: resolvedModel } : {}),
|
...(resolvedModel ? { model: resolvedModel } : {}),
|
||||||
...(variantToUse ? { variant: variantToUse } : {}),
|
...(variantToUse ? { variant: variantToUse } : {}),
|
||||||
mode: "all",
|
mode: "all",
|
||||||
prompt: getPrometheusPrompt(resolvedModel),
|
prompt: getPrometheusPrompt(resolvedModel) + customAgentBlock,
|
||||||
permission: PROMETHEUS_PERMISSION,
|
permission: PROMETHEUS_PERMISSION,
|
||||||
description: `${(params.configAgentPlan?.description as string) ?? "Plan agent"} (Prometheus - OhMyOpenCode)`,
|
description: `${(params.configAgentPlan?.description as string) ?? "Plan agent"} (Prometheus - OhMyOpenCode)`,
|
||||||
color: (params.configAgentPlan?.color as string) ?? "#FF5722",
|
color: (params.configAgentPlan?.color as string) ?? "#FF5722",
|
||||||
|
|||||||
@ -79,4 +79,56 @@ describe("resolveSubagentExecution", () => {
|
|||||||
error: "network timeout",
|
error: "network timeout",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("uses inherited model for custom agents without explicit model", async () => {
|
||||||
|
//#given
|
||||||
|
const args = createBaseArgs({ subagent_type: "translator" })
|
||||||
|
const executorCtx = createExecutorContext(async () => ({
|
||||||
|
data: [{ name: "translator", mode: "subagent" }],
|
||||||
|
}))
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await resolveSubagentExecution(
|
||||||
|
args,
|
||||||
|
executorCtx,
|
||||||
|
"sisyphus",
|
||||||
|
"deep",
|
||||||
|
"openai/gpt-5.3-codex",
|
||||||
|
"anthropic/claude-opus-4-6",
|
||||||
|
)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.error).toBeUndefined()
|
||||||
|
expect(result.agentToUse).toBe("translator")
|
||||||
|
expect(result.categoryModel).toEqual({
|
||||||
|
providerID: "openai",
|
||||||
|
modelID: "gpt-5.3-codex",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses system default model when inherited model is unavailable", async () => {
|
||||||
|
//#given
|
||||||
|
const args = createBaseArgs({ subagent_type: "translator" })
|
||||||
|
const executorCtx = createExecutorContext(async () => ({
|
||||||
|
data: [{ name: "translator", mode: "subagent" }],
|
||||||
|
}))
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await resolveSubagentExecution(
|
||||||
|
args,
|
||||||
|
executorCtx,
|
||||||
|
"sisyphus",
|
||||||
|
"deep",
|
||||||
|
undefined,
|
||||||
|
"anthropic/claude-opus-4-6",
|
||||||
|
)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.error).toBeUndefined()
|
||||||
|
expect(result.agentToUse).toBe("translator")
|
||||||
|
expect(result.categoryModel).toEqual({
|
||||||
|
providerID: "anthropic",
|
||||||
|
modelID: "claude-opus-4-6",
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -15,7 +15,9 @@ export async function resolveSubagentExecution(
|
|||||||
args: DelegateTaskArgs,
|
args: DelegateTaskArgs,
|
||||||
executorCtx: ExecutorContext,
|
executorCtx: ExecutorContext,
|
||||||
parentAgent: string | undefined,
|
parentAgent: string | undefined,
|
||||||
categoryExamples: string
|
categoryExamples: string,
|
||||||
|
inheritedModel?: string,
|
||||||
|
systemDefaultModel?: string,
|
||||||
): Promise<{ agentToUse: string; categoryModel: { providerID: string; modelID: string; variant?: string } | undefined; fallbackChain?: FallbackEntry[]; error?: string }> {
|
): Promise<{ agentToUse: string; categoryModel: { providerID: string; modelID: string; variant?: string } | undefined; fallbackChain?: FallbackEntry[]; error?: string }> {
|
||||||
const { client, agentOverrides } = executorCtx
|
const { client, agentOverrides } = executorCtx
|
||||||
|
|
||||||
@ -123,6 +125,16 @@ Create the work plan directly - that's your job as the planning agent.`,
|
|||||||
if (!categoryModel && matchedAgent.model) {
|
if (!categoryModel && matchedAgent.model) {
|
||||||
categoryModel = matchedAgent.model
|
categoryModel = matchedAgent.model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!categoryModel) {
|
||||||
|
const fallbackModel = inheritedModel ?? systemDefaultModel
|
||||||
|
if (fallbackModel) {
|
||||||
|
const parsedFallback = parseModelString(fallbackModel)
|
||||||
|
if (parsedFallback) {
|
||||||
|
categoryModel = parsedFallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
log("[delegate-task] Failed to resolve subagent execution", {
|
log("[delegate-task] Failed to resolve subagent execution", {
|
||||||
|
|||||||
@ -221,7 +221,14 @@ export function createDelegateTask(options: DelegateTaskToolOptions): ToolDefini
|
|||||||
return executeUnstableAgentTask(args, ctx, options, parentContext, agentToUse, categoryModel, systemContent, actualModel)
|
return executeUnstableAgentTask(args, ctx, options, parentContext, agentToUse, categoryModel, systemContent, actualModel)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const resolution = await resolveSubagentExecution(args, options, parentContext.agent, categoryExamples)
|
const resolution = await resolveSubagentExecution(
|
||||||
|
args,
|
||||||
|
options,
|
||||||
|
parentContext.agent,
|
||||||
|
categoryExamples,
|
||||||
|
inheritedModel,
|
||||||
|
systemDefaultModel,
|
||||||
|
)
|
||||||
if (resolution.error) {
|
if (resolution.error) {
|
||||||
return resolution.error
|
return resolution.error
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user