Revert "Merge pull request #1951 from edxeth/feat/custom-agents"
This reverts commit47e300b17e, reversing changes made to243ce1b7e8.
This commit is contained in:
@@ -3148,16 +3148,6 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"custom_agents": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string",
|
||||
"pattern": "^(?!(?:[bB][uU][iI][lL][dD]|[pP][lL][aA][nN]|[sS][iI][sS][yY][pP][hH][uU][sS]|[hH][eE][pP][hH][aA][eE][sS][tT][uU][sS]|[sS][iI][sS][yY][pP][hH][uU][sS]-[jJ][uU][nN][iI][oO][rR]|[oO][pP][eE][nN][cC][oO][dD][eE]-[bB][uU][iI][lL][dD][eE][rR]|[pP][rR][oO][mM][eE][tT][hH][eE][uU][sS]|[mM][eE][tT][iI][sS]|[mM][oO][mM][uU][sS]|[oO][rR][aA][cC][lL][eE]|[lL][iI][bB][rR][aA][rR][iI][aA][nN]|[eE][xX][pP][lL][oO][rR][eE]|[mM][uU][lL][tT][iI][mM][oO][dD][aA][lL]-[lL][oO][oO][kK][eE][rR]|[aA][tT][lL][aA][sS])$).+"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/agentOverrideConfig"
|
||||
}
|
||||
},
|
||||
"categories": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
@@ -3871,226 +3861,5 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"$defs": {
|
||||
"agentOverrideConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"fallback_models": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"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
|
||||
},
|
||||
"reasoningEffort": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
]
|
||||
},
|
||||
"textVerbosity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
},
|
||||
"providerOptions": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"ultrawork": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"compaction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -68,8 +68,6 @@ User Request
|
||||
|
||||
When Sisyphus delegates to a subagent, it doesn't pick a model name. It picks a **category** — `visual-engineering`, `ultrabrain`, `quick`, `deep`. The category automatically maps to the right model. You touch nothing.
|
||||
|
||||
Custom agents are also first-class in this flow. When custom agents are loaded, planning context includes them, so the orchestrator can choose them proactively when appropriate, and you can call them directly on demand via `task(subagent_type="your-agent")`.
|
||||
|
||||
For a deep dive into how agents collaborate, see the [Orchestration System Guide](./orchestration.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -11,7 +11,6 @@ Complete reference for `oh-my-opencode.jsonc` configuration. This document cover
|
||||
- [Quick Start Example](#quick-start-example)
|
||||
- [Core Concepts](#core-concepts)
|
||||
- [Agents](#agents)
|
||||
- [Custom Agents (`custom_agents`)](#custom-agents-custom_agents)
|
||||
- [Categories](#categories)
|
||||
- [Model Resolution](#model-resolution)
|
||||
- [Task System](#task-system)
|
||||
@@ -131,8 +130,6 @@ Here's a practical starting configuration:
|
||||
|
||||
Override built-in agent settings. Available agents: `sisyphus`, `hephaestus`, `prometheus`, `oracle`, `librarian`, `explore`, `multimodal-looker`, `metis`, `momus`, `atlas`.
|
||||
|
||||
`agents` is intentionally strict and only accepts built-in agent keys. Use `custom_agents` for user-defined agents.
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
@@ -203,64 +200,6 @@ Control what tools an agent can use:
|
||||
| `doom_loop` | `ask` / `allow` / `deny` |
|
||||
| `external_directory` | `ask` / `allow` / `deny` |
|
||||
|
||||
### Custom Agents (`custom_agents`)
|
||||
|
||||
Use `custom_agents` to configure user-defined agents without mixing them into built-in `agents` overrides.
|
||||
|
||||
What this gives you:
|
||||
|
||||
- **Clean separation**: built-ins stay in `agents`, user-defined entries stay in `custom_agents`.
|
||||
- **Safer config**: keys in `custom_agents` cannot reuse built-in names.
|
||||
- **First-class orchestration**: loaded custom agents are visible to planner/orchestrator context, so they can be selected proactively during planning and invoked on demand via `task(subagent_type=...)`.
|
||||
- **Full model controls** for custom agents: `model`, `variant`, `temperature`, `top_p`, `reasoningEffort`, `thinking`, etc.
|
||||
|
||||
Important behavior:
|
||||
|
||||
- `custom_agents` **overrides existing custom agents** loaded at runtime (for example from Claude Code/OpenCode agent sources).
|
||||
- `custom_agents` does **not** create an agent from thin air by itself; the target custom agent must be present in runtime-loaded agent configs.
|
||||
|
||||
Example:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"custom_agents": {
|
||||
"translator": {
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "high",
|
||||
"temperature": 0.2,
|
||||
"prompt_append": "Keep locale placeholders and ICU tokens exactly unchanged."
|
||||
},
|
||||
"reviewer-fast": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
"temperature": 0,
|
||||
"thinking": {
|
||||
"type": "enabled",
|
||||
"budgetTokens": 20000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
On-demand invocation through task delegation:
|
||||
|
||||
```ts
|
||||
task(
|
||||
{
|
||||
subagent_type: "translator",
|
||||
load_skills: [],
|
||||
description: "Translate release notes",
|
||||
prompt: "Translate docs/CHANGELOG.md into Korean while preserving markdown structure.",
|
||||
run_in_background: false,
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Migration note:
|
||||
|
||||
- If you previously put custom entries under `agents.*`, move them to `custom_agents.*`.
|
||||
- Unknown built-in keys under `agents` are reported with migration hints.
|
||||
|
||||
### Categories
|
||||
|
||||
Domain-specific model delegation used by the `task()` tool. When Sisyphus delegates work, it picks a category, not a model name.
|
||||
|
||||
@@ -1,53 +1,17 @@
|
||||
import * as z from "zod"
|
||||
import { OhMyOpenCodeConfigSchema } from "../src/config/schema"
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined
|
||||
}
|
||||
|
||||
function dedupeCustomAgentOverrideSchema(schema: Record<string, unknown>): Record<string, unknown> {
|
||||
const rootProperties = asRecord(schema.properties)
|
||||
const agentsSchema = asRecord(rootProperties?.agents)
|
||||
const builtInAgentProps = asRecord(agentsSchema?.properties)
|
||||
const customAgentsSchema = asRecord(rootProperties?.custom_agents)
|
||||
const customAdditionalProperties = asRecord(customAgentsSchema?.additionalProperties)
|
||||
|
||||
if (!builtInAgentProps || !customAgentsSchema || !customAdditionalProperties) {
|
||||
return schema
|
||||
}
|
||||
|
||||
const referenceAgentSchema = asRecord(
|
||||
builtInAgentProps.build
|
||||
?? builtInAgentProps.oracle
|
||||
?? builtInAgentProps.explore,
|
||||
)
|
||||
|
||||
if (!referenceAgentSchema) {
|
||||
return schema
|
||||
}
|
||||
|
||||
const defs = asRecord(schema.$defs) ?? {}
|
||||
defs.agentOverrideConfig = referenceAgentSchema
|
||||
schema.$defs = defs
|
||||
|
||||
customAgentsSchema.additionalProperties = { $ref: "#/$defs/agentOverrideConfig" }
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
export function createOhMyOpenCodeJsonSchema(): Record<string, unknown> {
|
||||
const jsonSchema = z.toJSONSchema(OhMyOpenCodeConfigSchema, {
|
||||
target: "draft-7",
|
||||
unrepresentable: "any",
|
||||
})
|
||||
|
||||
const schema = {
|
||||
return {
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
$id: "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/dev/assets/oh-my-opencode.schema.json",
|
||||
title: "Oh My OpenCode Configuration",
|
||||
description: "Configuration schema for oh-my-opencode plugin",
|
||||
...jsonSchema,
|
||||
}
|
||||
|
||||
return dedupeCustomAgentOverrideSchema(schema)
|
||||
}
|
||||
|
||||
@@ -242,28 +242,14 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
test("createBuiltinAgents excludes disabled skills from availableSkills", async () => {
|
||||
// #given
|
||||
const disabledSkills = new Set(["playwright"])
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set([
|
||||
"anthropic/claude-opus-4-6",
|
||||
"opencode/kimi-k2.5-free",
|
||||
"zai-coding-plan/glm-5",
|
||||
"opencode/big-pickle",
|
||||
])
|
||||
)
|
||||
|
||||
try {
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined, undefined, disabledSkills)
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined, undefined, disabledSkills)
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus.prompt).not.toContain("playwright")
|
||||
expect(agents.sisyphus.prompt).toContain("frontend-ui-ux")
|
||||
expect(agents.sisyphus.prompt).toContain("git-master")
|
||||
} finally {
|
||||
cacheSpy.mockRestore()
|
||||
fetchSpy.mockRestore()
|
||||
}
|
||||
// #then
|
||||
expect(agents.sisyphus.prompt).not.toContain("playwright")
|
||||
expect(agents.sisyphus.prompt).toContain("frontend-ui-ux")
|
||||
expect(agents.sisyphus.prompt).toContain("git-master")
|
||||
})
|
||||
|
||||
test("includes custom agents in orchestrator prompts when provided via config", async () => {
|
||||
|
||||
@@ -1,25 +1,11 @@
|
||||
export {
|
||||
OhMyOpenCodeConfigSchema,
|
||||
AgentOverrideConfigSchema,
|
||||
AgentOverridesSchema,
|
||||
CustomAgentOverridesSchema,
|
||||
McpNameSchema,
|
||||
AgentNameSchema,
|
||||
OverridableAgentNameSchema,
|
||||
HookNameSchema,
|
||||
BuiltinCommandNameSchema,
|
||||
SisyphusAgentConfigSchema,
|
||||
ExperimentalConfigSchema,
|
||||
RalphLoopConfigSchema,
|
||||
TmuxConfigSchema,
|
||||
TmuxLayoutSchema,
|
||||
} from "./schema"
|
||||
|
||||
export type {
|
||||
OhMyOpenCodeConfig,
|
||||
AgentOverrideConfig,
|
||||
AgentOverrides,
|
||||
CustomAgentOverrides,
|
||||
McpName,
|
||||
AgentName,
|
||||
HookName,
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
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 defs = asRecord(schema.$defs)
|
||||
const sharedAgentOverrideSchema = asRecord(defs?.agentOverrideConfig)
|
||||
const sharedAgentProperties = asRecord(sharedAgentOverrideSchema?.properties)
|
||||
|
||||
// then
|
||||
expect(agentsSchema).toBeDefined()
|
||||
expect(agentsSchema?.additionalProperties).toBeFalse()
|
||||
expect(customAgentsSchema).toBeDefined()
|
||||
expect(customPropertyNames?.pattern).toBeDefined()
|
||||
expect(customPropertyNames?.pattern).toContain("[bB][uU][iI][lL][dD]")
|
||||
expect(customPropertyNames?.pattern).toContain("[pP][lL][aA][nN]")
|
||||
expect(customAdditionalProperties).toBeDefined()
|
||||
expect(customAdditionalProperties?.$ref).toBe("#/$defs/agentOverrideConfig")
|
||||
expect(sharedAgentOverrideSchema).toBeDefined()
|
||||
expect(sharedAgentProperties?.model).toEqual({ type: "string" })
|
||||
expect(sharedAgentProperties?.temperature).toEqual(
|
||||
expect.objectContaining({ type: "number" }),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -530,79 +530,6 @@ describe("Sisyphus-Junior agent override", () => {
|
||||
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", () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { z } from "zod"
|
||||
import { FallbackModelsSchema } from "./fallback-models"
|
||||
import { OverridableAgentNameSchema } from "./agent-names"
|
||||
import { AgentPermissionSchema } from "./internal/permission"
|
||||
|
||||
export const AgentOverrideConfigSchema = z.object({
|
||||
@@ -56,7 +55,7 @@ export const AgentOverrideConfigSchema = z.object({
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const BuiltinAgentOverridesSchema = z.object({
|
||||
export const AgentOverridesSchema = z.object({
|
||||
build: AgentOverrideConfigSchema.optional(),
|
||||
plan: AgentOverrideConfigSchema.optional(),
|
||||
sisyphus: AgentOverrideConfigSchema.optional(),
|
||||
@@ -73,57 +72,7 @@ const BuiltinAgentOverridesSchema = z.object({
|
||||
explore: AgentOverrideConfigSchema.optional(),
|
||||
"multimodal-looker": 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()),
|
||||
)
|
||||
function escapeRegexLiteral(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
}
|
||||
|
||||
function toCaseInsensitiveLiteralPattern(value: string): string {
|
||||
return value
|
||||
.split("")
|
||||
.map((char) => {
|
||||
if (/^[A-Za-z]$/.test(char)) {
|
||||
const lower = char.toLowerCase()
|
||||
const upper = char.toUpperCase()
|
||||
return `[${lower}${upper}]`
|
||||
}
|
||||
|
||||
return escapeRegexLiteral(char)
|
||||
})
|
||||
.join("")
|
||||
}
|
||||
|
||||
const RESERVED_CUSTOM_AGENT_NAME_PATTERN = new RegExp(
|
||||
`^(?!(?:${RESERVED_CUSTOM_AGENT_NAMES.map(toCaseInsensitiveLiteralPattern).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 AgentOverrides = z.infer<typeof AgentOverridesSchema>
|
||||
export type CustomAgentOverrides = z.infer<typeof CustomAgentOverridesSchema>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod"
|
||||
import { AnyMcpNameSchema } from "../../mcp/types"
|
||||
import { BuiltinAgentNameSchema, BuiltinSkillNameSchema } from "./agent-names"
|
||||
import { AgentOverridesSchema, CustomAgentOverridesSchema } from "./agent-overrides"
|
||||
import { AgentOverridesSchema } from "./agent-overrides"
|
||||
import { BabysittingConfigSchema } from "./babysitting"
|
||||
import { BackgroundTaskConfigSchema } from "./background-task"
|
||||
import { BrowserAutomationConfigSchema } from "./browser-automation"
|
||||
@@ -39,7 +39,6 @@ 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. */
|
||||
model_fallback: z.boolean().optional(),
|
||||
agents: AgentOverridesSchema.optional(),
|
||||
custom_agents: CustomAgentOverridesSchema.optional(),
|
||||
categories: CategoriesConfigSchema.optional(),
|
||||
claude_code: ClaudeCodeConfigSchema.optional(),
|
||||
sisyphus_agent: SisyphusAgentConfigSchema.optional(),
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
detectLikelyBuiltinAgentTypos,
|
||||
detectUnknownBuiltinAgentKeys,
|
||||
mergeConfigs,
|
||||
parseConfigPartially,
|
||||
} from "./plugin-config";
|
||||
import { mergeConfigs, parseConfigPartially } from "./plugin-config";
|
||||
import type { OhMyOpenCodeConfig } from "./config";
|
||||
|
||||
describe("mergeConfigs", () => {
|
||||
@@ -120,27 +115,6 @@ describe("mergeConfigs", () => {
|
||||
expect(result.disabled_hooks).toContain("session-recovery");
|
||||
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")
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -191,9 +165,7 @@ describe("parseConfigPartially", () => {
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.disabled_hooks).toEqual(["comment-checker"]);
|
||||
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();
|
||||
expect(result!.agents).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should preserve valid agents when a non-agent section is invalid", () => {
|
||||
@@ -210,36 +182,6 @@ describe("parseConfigPartially", () => {
|
||||
expect(result!.agents?.oracle?.model).toBe("openai/gpt-5.2");
|
||||
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", () => {
|
||||
@@ -295,105 +237,3 @@ 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([])
|
||||
})
|
||||
|
||||
it("excludes typo keys when explicitly provided", () => {
|
||||
const rawConfig = {
|
||||
agents: {
|
||||
sisyphuss: { model: "openai/gpt-5.2" },
|
||||
translator: { model: "google/gemini-3-flash-preview" },
|
||||
},
|
||||
}
|
||||
|
||||
const unknownKeys = detectUnknownBuiltinAgentKeys(rawConfig, ["sisyphuss"])
|
||||
|
||||
expect(unknownKeys).toEqual(["translator"])
|
||||
})
|
||||
|
||||
it("excludes typo keys case-insensitively", () => {
|
||||
const rawConfig = {
|
||||
agents: {
|
||||
Sisyphuss: { model: "openai/gpt-5.2" },
|
||||
translator: { model: "google/gemini-3-flash-preview" },
|
||||
},
|
||||
}
|
||||
|
||||
const unknownKeys = detectUnknownBuiltinAgentKeys(rawConfig, ["sisyphuss"])
|
||||
|
||||
expect(unknownKeys).toEqual(["translator"])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import {
|
||||
OhMyOpenCodeConfigSchema,
|
||||
OverridableAgentNameSchema,
|
||||
type OhMyOpenCodeConfig,
|
||||
} from "./config";
|
||||
import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "./config";
|
||||
import {
|
||||
log,
|
||||
deepMerge,
|
||||
@@ -15,90 +11,6 @@ import {
|
||||
migrateConfigFile,
|
||||
} 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>,
|
||||
excludeKeys: string[] = [],
|
||||
): string[] {
|
||||
const agents = rawConfig.agents;
|
||||
if (!agents || typeof agents !== "object") return [];
|
||||
|
||||
const excluded = new Set(excludeKeys.map((key) => key.toLowerCase()));
|
||||
|
||||
return Object.keys(agents).filter(
|
||||
(key) => {
|
||||
const lower = key.toLowerCase();
|
||||
return (
|
||||
!BUILTIN_AGENT_OVERRIDE_KEYS_BY_LOWER.has(lower)
|
||||
&& !excluded.has(lower)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function parseConfigPartially(
|
||||
rawConfig: Record<string, unknown>
|
||||
): OhMyOpenCodeConfig | null {
|
||||
@@ -110,52 +22,7 @@ export function parseConfigPartially(
|
||||
const partialConfig: Record<string, unknown> = {};
|
||||
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)) {
|
||||
if (key === "agents" || key === "custom_agents") {
|
||||
parseAgentSectionEntries(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
const sectionResult = OhMyOpenCodeConfigSchema.safeParse({ [key]: rawConfig[key] });
|
||||
if (sectionResult.success) {
|
||||
const parsed = sectionResult.data as Record<string, unknown>;
|
||||
@@ -191,32 +58,6 @@ export function loadConfigFromPath(
|
||||
|
||||
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,
|
||||
typoWarnings.map((warning) => warning.key),
|
||||
);
|
||||
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);
|
||||
|
||||
if (result.success) {
|
||||
@@ -257,7 +98,6 @@ export function mergeConfigs(
|
||||
...base,
|
||||
...override,
|
||||
agents: deepMerge(base.agents, override.agents),
|
||||
custom_agents: deepMerge(base.custom_agents, override.custom_agents),
|
||||
categories: deepMerge(base.categories, override.categories),
|
||||
disabled_agents: [
|
||||
...new Set([
|
||||
@@ -330,7 +170,6 @@ export function loadPluginConfig(
|
||||
|
||||
log("Final merged config", {
|
||||
agents: config.agents,
|
||||
custom_agents: config.custom_agents,
|
||||
disabled_agents: config.disabled_agents,
|
||||
disabled_mcps: config.disabled_mcps,
|
||||
disabled_hooks: config.disabled_hooks,
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { OhMyOpenCodeConfig } from "../config";
|
||||
import { log, migrateAgentConfig } from "../shared";
|
||||
import { AGENT_NAME_MAP } from "../shared/migration";
|
||||
import { getAgentDisplayName } from "../shared/agent-display-names";
|
||||
import { mergeCategories } from "../shared/merge-categories";
|
||||
import {
|
||||
discoverConfigSourceSkills,
|
||||
discoverOpencodeGlobalSkills,
|
||||
@@ -18,13 +17,6 @@ import { reorderAgentsByPriority } from "./agent-priority-order";
|
||||
import { remapAgentKeysToDisplayNames } from "./agent-key-remapper";
|
||||
import { buildPrometheusAgentConfig } from "./prometheus-agent-config-builder";
|
||||
import { buildPlanDemoteConfig } from "./plan-model-inheritance";
|
||||
import {
|
||||
applyCustomAgentOverrides,
|
||||
collectCustomAgentSummariesFromRecord,
|
||||
mergeCustomAgentSummaries,
|
||||
collectKnownCustomAgentNames,
|
||||
filterSummariesByKnownNames,
|
||||
} from "./custom-agent-utils";
|
||||
|
||||
type AgentConfigRecord = Record<string, Record<string, unknown> | undefined> & {
|
||||
build?: Record<string, unknown>;
|
||||
@@ -82,19 +74,26 @@ export async function applyAgentConfig(params: {
|
||||
const browserProvider =
|
||||
params.pluginConfig.browser_automation_engine?.provider ?? "playwright";
|
||||
const currentModel = params.config.model as string | undefined;
|
||||
const disabledAgentNames = new Set(
|
||||
(migratedDisabledAgents ?? []).map((agent) => agent.toLowerCase()),
|
||||
);
|
||||
const filterDisabledAgents = (agents: Record<string, unknown>) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(agents).filter(
|
||||
([name]) => !disabledAgentNames.has(name.toLowerCase()),
|
||||
),
|
||||
);
|
||||
const disabledSkills = new Set<string>(params.pluginConfig.disabled_skills ?? []);
|
||||
const useTaskSystem = params.pluginConfig.experimental?.task_system ?? 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 userAgents = includeClaudeAgents ? loadUserAgents() : {};
|
||||
const projectAgents = includeClaudeAgents ? loadProjectAgents(params.ctx.directory) : {};
|
||||
@@ -107,49 +106,15 @@ export async function applyAgentConfig(params: {
|
||||
]),
|
||||
);
|
||||
|
||||
const configAgent = params.config.agent as AgentConfigRecord | undefined;
|
||||
const filteredUserAgents = filterDisabledAgents(userAgents as Record<string, unknown>);
|
||||
const filteredProjectAgents = filterDisabledAgents(projectAgents as Record<string, unknown>);
|
||||
const filteredPluginAgents = filterDisabledAgents(pluginAgents as Record<string, unknown>);
|
||||
const filteredConfigAgentsForSummary = filterDisabledAgents(
|
||||
(configAgent as Record<string, unknown> | undefined) ?? {},
|
||||
const disabledAgentNames = new Set(
|
||||
(migratedDisabledAgents ?? []).map(a => a.toLowerCase())
|
||||
);
|
||||
const mergedCategories = mergeCategories(params.pluginConfig.categories)
|
||||
const knownCustomAgentNames = collectKnownCustomAgentNames(
|
||||
filteredUserAgents,
|
||||
filteredProjectAgents,
|
||||
filteredPluginAgents,
|
||||
filteredConfigAgentsForSummary,
|
||||
)
|
||||
|
||||
const customAgentSummaries = mergeCustomAgentSummaries(
|
||||
collectCustomAgentSummariesFromRecord(filteredUserAgents),
|
||||
collectCustomAgentSummariesFromRecord(filteredProjectAgents),
|
||||
collectCustomAgentSummariesFromRecord(filteredPluginAgents),
|
||||
collectCustomAgentSummariesFromRecord(filteredConfigAgentsForSummary),
|
||||
filterSummariesByKnownNames(
|
||||
collectCustomAgentSummariesFromRecord(
|
||||
params.pluginConfig.custom_agents as Record<string, unknown> | undefined,
|
||||
),
|
||||
knownCustomAgentNames,
|
||||
),
|
||||
)
|
||||
const filterDisabledAgents = (agents: Record<string, unknown>) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(agents).filter(([name]) => !disabledAgentNames.has(name.toLowerCase()))
|
||||
);
|
||||
|
||||
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 builderEnabled =
|
||||
params.pluginConfig.sisyphus_agent?.default_builder_enabled ?? false;
|
||||
@@ -158,6 +123,8 @@ export async function applyAgentConfig(params: {
|
||||
const shouldDemotePlan = plannerEnabled && replacePlan;
|
||||
const configuredDefaultAgent = getConfiguredDefaultAgent(params.config);
|
||||
|
||||
const configAgent = params.config.agent as AgentConfigRecord | undefined;
|
||||
|
||||
if (isSisyphusEnabled && builtinAgents.sisyphus) {
|
||||
if (configuredDefaultAgent) {
|
||||
(params.config as { default_agent?: string }).default_agent =
|
||||
@@ -201,7 +168,6 @@ export async function applyAgentConfig(params: {
|
||||
pluginPrometheusOverride: prometheusOverride,
|
||||
userCategories: params.pluginConfig.categories,
|
||||
currentModel,
|
||||
customAgentSummaries,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -237,9 +203,9 @@ export async function applyAgentConfig(params: {
|
||||
...Object.fromEntries(
|
||||
Object.entries(builtinAgents).filter(([key]) => key !== "sisyphus"),
|
||||
),
|
||||
...filteredUserAgents,
|
||||
...filteredProjectAgents,
|
||||
...filteredPluginAgents,
|
||||
...filterDisabledAgents(userAgents),
|
||||
...filterDisabledAgents(projectAgents),
|
||||
...filterDisabledAgents(pluginAgents),
|
||||
...filteredConfigAgents,
|
||||
build: { ...migratedBuild, mode: "subagent", hidden: true },
|
||||
...(planDemoteConfig ? { plan: planDemoteConfig } : {}),
|
||||
@@ -247,31 +213,13 @@ export async function applyAgentConfig(params: {
|
||||
} else {
|
||||
params.config.agent = {
|
||||
...builtinAgents,
|
||||
...filteredUserAgents,
|
||||
...filteredProjectAgents,
|
||||
...filteredPluginAgents,
|
||||
...filterDisabledAgents(userAgents),
|
||||
...filterDisabledAgents(projectAgents),
|
||||
...filterDisabledAgents(pluginAgents),
|
||||
...configAgent,
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
params.config.agent = remapAgentKeysToDisplayNames(
|
||||
params.config.agent as Record<string, unknown>,
|
||||
|
||||
@@ -162,347 +162,6 @@ 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")
|
||||
})
|
||||
|
||||
test("prometheus prompt excludes disabled custom agents from catalog", async () => {
|
||||
// #given
|
||||
;(agentLoader.loadUserAgents as any).mockReturnValue({
|
||||
translator: {
|
||||
name: "translator",
|
||||
mode: "subagent",
|
||||
description: "Translate and localize locale files",
|
||||
prompt: "Translate content",
|
||||
},
|
||||
})
|
||||
|
||||
const pluginConfig: OhMyOpenCodeConfig = {
|
||||
disabled_agents: ["translator"],
|
||||
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).not.toContain("translator")
|
||||
})
|
||||
|
||||
test("prometheus custom prompt override still includes custom agent catalog", async () => {
|
||||
// #given
|
||||
;(agentLoader.loadUserAgents as any).mockReturnValue({
|
||||
translator: {
|
||||
name: "translator",
|
||||
mode: "subagent",
|
||||
description: "Translate and localize locale files",
|
||||
prompt: "Translate content",
|
||||
},
|
||||
})
|
||||
|
||||
const pluginConfig: OhMyOpenCodeConfig = {
|
||||
agents: {
|
||||
prometheus: {
|
||||
prompt: "Custom planner prompt",
|
||||
},
|
||||
},
|
||||
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 planner prompt")
|
||||
expect(agentsConfig[pKey].prompt).toContain("<custom_agent_catalog>")
|
||||
expect(agentsConfig[pKey].prompt).toContain("translator")
|
||||
})
|
||||
|
||||
test("custom agent summary merge preserves flags when custom_agents adds description", async () => {
|
||||
// #given
|
||||
;(agentLoader.loadUserAgents as any).mockReturnValue({
|
||||
translator: {
|
||||
name: "translator",
|
||||
mode: "subagent",
|
||||
description: "",
|
||||
hidden: true,
|
||||
disabled: true,
|
||||
enabled: false,
|
||||
prompt: "Translate content",
|
||||
},
|
||||
})
|
||||
const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as {
|
||||
mock: { calls: unknown[][] }
|
||||
}
|
||||
|
||||
const pluginConfig: OhMyOpenCodeConfig = {
|
||||
custom_agents: {
|
||||
translator: {
|
||||
description: "Translate and localize locale files",
|
||||
},
|
||||
},
|
||||
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]
|
||||
const summaries = firstCallArgs[7] as Array<{
|
||||
name: string
|
||||
description: string
|
||||
hidden?: boolean
|
||||
disabled?: boolean
|
||||
enabled?: boolean
|
||||
}>
|
||||
const translatorSummary = summaries.find((summary) => summary.name === "translator")
|
||||
|
||||
expect(translatorSummary).toBeDefined()
|
||||
expect(translatorSummary?.description).toBe("Translate and localize locale files")
|
||||
expect(translatorSummary?.hidden).toBe(true)
|
||||
expect(translatorSummary?.disabled).toBe(true)
|
||||
expect(translatorSummary?.enabled).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Plan agent demote behavior", () => {
|
||||
test("orders core agents as sisyphus -> hephaestus -> prometheus -> atlas", async () => {
|
||||
// #given
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
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: typeof agentValue.hidden === "boolean" ? agentValue.hidden : undefined,
|
||||
disabled: typeof agentValue.disabled === "boolean" ? agentValue.disabled : undefined,
|
||||
enabled: typeof agentValue.enabled === "boolean" ? agentValue.enabled : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
merged.set(key, {
|
||||
...existing,
|
||||
...summary,
|
||||
hidden: summary.hidden ?? existing.hidden,
|
||||
disabled: summary.disabled ?? existing.disabled,
|
||||
enabled: summary.enabled ?? existing.enabled,
|
||||
description: incomingDescription || existingDescription,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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,7 +1,6 @@
|
||||
import type { CategoryConfig } from "../config/schema";
|
||||
import { PROMETHEUS_PERMISSION, getPrometheusPrompt } from "../agents/prometheus";
|
||||
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 {
|
||||
fetchAvailableModels,
|
||||
@@ -28,7 +27,6 @@ export async function buildPrometheusAgentConfig(params: {
|
||||
pluginPrometheusOverride: PrometheusOverride | undefined;
|
||||
userCategories: Record<string, CategoryConfig> | undefined;
|
||||
currentModel: string | undefined;
|
||||
customAgentSummaries?: unknown;
|
||||
}): Promise<Record<string, unknown>> {
|
||||
const categoryConfig = params.pluginPrometheusOverride?.category
|
||||
? resolveCategoryConfig(params.pluginPrometheusOverride.category, params.userCategories)
|
||||
@@ -67,18 +65,11 @@ export async function buildPrometheusAgentConfig(params: {
|
||||
const maxTokensToUse =
|
||||
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> = {
|
||||
...(resolvedModel ? { model: resolvedModel } : {}),
|
||||
...(variantToUse ? { variant: variantToUse } : {}),
|
||||
mode: "all",
|
||||
prompt: getPrometheusPrompt(resolvedModel) + customAgentBlock,
|
||||
prompt: getPrometheusPrompt(resolvedModel),
|
||||
permission: PROMETHEUS_PERMISSION,
|
||||
description: `${(params.configAgentPlan?.description as string) ?? "Plan agent"} (Prometheus - OhMyOpenCode)`,
|
||||
color: (params.configAgentPlan?.color as string) ?? "#FF5722",
|
||||
@@ -103,12 +94,5 @@ export async function buildPrometheusAgentConfig(params: {
|
||||
if (prompt_append && typeof merged.prompt === "string") {
|
||||
merged.prompt = merged.prompt + "\n" + resolvePromptAppend(prompt_append);
|
||||
}
|
||||
if (
|
||||
customAgentBlock
|
||||
&& typeof merged.prompt === "string"
|
||||
&& !merged.prompt.includes("<custom_agent_catalog>")
|
||||
) {
|
||||
merged.prompt = merged.prompt + customAgentBlock;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
@@ -79,56 +79,4 @@ describe("resolveSubagentExecution", () => {
|
||||
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,9 +15,7 @@ export async function resolveSubagentExecution(
|
||||
args: DelegateTaskArgs,
|
||||
executorCtx: ExecutorContext,
|
||||
parentAgent: string | undefined,
|
||||
categoryExamples: string,
|
||||
inheritedModel?: string,
|
||||
systemDefaultModel?: string,
|
||||
categoryExamples: string
|
||||
): Promise<{ agentToUse: string; categoryModel: { providerID: string; modelID: string; variant?: string } | undefined; fallbackChain?: FallbackEntry[]; error?: string }> {
|
||||
const { client, agentOverrides } = executorCtx
|
||||
|
||||
@@ -126,16 +124,6 @@ Create the work plan directly - that's your job as the planning agent.`,
|
||||
if (!categoryModel && matchedAgent.model) {
|
||||
categoryModel = matchedAgent.model
|
||||
}
|
||||
|
||||
if (!categoryModel) {
|
||||
const fallbackModel = inheritedModel ?? systemDefaultModel
|
||||
if (fallbackModel) {
|
||||
const parsedFallback = parseModelString(fallbackModel)
|
||||
if (parsedFallback) {
|
||||
categoryModel = parsedFallback
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
log("[delegate-task] Failed to resolve subagent execution", {
|
||||
|
||||
@@ -226,14 +226,7 @@ export function createDelegateTask(options: DelegateTaskToolOptions): ToolDefini
|
||||
return executeUnstableAgentTask(args, ctx, options, parentContext, agentToUse, categoryModel, systemContent, actualModel)
|
||||
}
|
||||
} else {
|
||||
const resolution = await resolveSubagentExecution(
|
||||
args,
|
||||
options,
|
||||
parentContext.agent,
|
||||
categoryExamples,
|
||||
inheritedModel,
|
||||
systemDefaultModel,
|
||||
)
|
||||
const resolution = await resolveSubagentExecution(args, options, parentContext.agent, categoryExamples)
|
||||
if (resolution.error) {
|
||||
return resolution.error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user