test(agent-config): add regression tests for agent merge priority order
This commit is contained in:
committed by
YeonGyu-Kim
parent
1d8f8a03ca
commit
76c5356a80
@@ -0,0 +1,275 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { tmpdir } from "os";
|
||||
|
||||
import {
|
||||
loadUserAgents,
|
||||
loadProjectAgents,
|
||||
loadOpencodeGlobalAgents,
|
||||
loadOpencodeProjectAgents,
|
||||
} from "./loader";
|
||||
|
||||
/**
|
||||
* Creates a temporary directory tree for testing agent loading.
|
||||
* Returns the root dir with `.claude/agents/` and `.opencode/agents/` subdirs
|
||||
* pre-created, containing the specified agent files.
|
||||
*/
|
||||
function createProjectWithAgents(
|
||||
agents: {
|
||||
claudeAgents?: Array<{ filename: string; content: string }>;
|
||||
opencodeAgents?: Array<{ filename: string; content: string }>;
|
||||
} = {},
|
||||
): string {
|
||||
const root = mkdtempSync(join(tmpdir(), "agent-loader-test-"));
|
||||
if (agents.claudeAgents) {
|
||||
const dir = join(root, ".claude", "agents");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
for (const { filename, content } of agents.claudeAgents) {
|
||||
writeFileSync(join(dir, filename), content, "utf-8");
|
||||
}
|
||||
}
|
||||
if (agents.opencodeAgents) {
|
||||
const dir = join(root, ".opencode", "agents");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
for (const { filename, content } of agents.opencodeAgents) {
|
||||
writeFileSync(join(dir, filename), content, "utf-8");
|
||||
}
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
const BASIC_AGENT = `---
|
||||
name: test-agent
|
||||
description: A test agent
|
||||
tools: Bash,Read
|
||||
---
|
||||
You are a test agent.`;
|
||||
|
||||
const MINIMAL_AGENT = `---
|
||||
description: Minimal agent
|
||||
---
|
||||
Do minimal things.`;
|
||||
|
||||
const NO_FRONTMATTER_AGENT = `Just a prompt with no frontmatter.`;
|
||||
|
||||
describe("claude-code-agent-loader", () => {
|
||||
const dirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of dirs) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
dirs.length = 0;
|
||||
});
|
||||
|
||||
function trackDir(dir: string): string {
|
||||
dirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
describe("loadProjectAgents", () => {
|
||||
test("loads agents from <directory>/.claude/agents", () => {
|
||||
const root = trackDir(
|
||||
createProjectWithAgents({
|
||||
claudeAgents: [{ filename: "my-agent.md", content: BASIC_AGENT }],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = loadProjectAgents(root);
|
||||
|
||||
expect(Object.keys(result)).toEqual(["test-agent"]);
|
||||
expect(result["test-agent"].description).toBe("(project) A test agent");
|
||||
expect(result["test-agent"].mode).toBe("subagent");
|
||||
expect(result["test-agent"].prompt).toBe("You are a test agent.");
|
||||
expect(result["test-agent"].tools).toEqual({ bash: true, read: true });
|
||||
});
|
||||
|
||||
test("uses filename as agent name when frontmatter name is absent", () => {
|
||||
const root = trackDir(
|
||||
createProjectWithAgents({
|
||||
claudeAgents: [
|
||||
{ filename: "fallback-name.md", content: MINIMAL_AGENT },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = loadProjectAgents(root);
|
||||
|
||||
expect(Object.keys(result)).toEqual(["fallback-name"]);
|
||||
expect(result["fallback-name"].description).toBe(
|
||||
"(project) Minimal agent",
|
||||
);
|
||||
});
|
||||
|
||||
test("handles agent with no frontmatter", () => {
|
||||
const root = trackDir(
|
||||
createProjectWithAgents({
|
||||
claudeAgents: [{ filename: "raw.md", content: NO_FRONTMATTER_AGENT }],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = loadProjectAgents(root);
|
||||
|
||||
expect(Object.keys(result)).toEqual(["raw"]);
|
||||
expect(result["raw"].prompt).toBe("Just a prompt with no frontmatter.");
|
||||
});
|
||||
|
||||
test("returns empty object when project has no .claude/agents directory", () => {
|
||||
const root = trackDir(mkdtempSync(join(tmpdir(), "agent-loader-test-")));
|
||||
|
||||
const result = loadProjectAgents(root);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
test("ignores non-markdown files", () => {
|
||||
const root = trackDir(
|
||||
createProjectWithAgents({
|
||||
claudeAgents: [
|
||||
{ filename: "good.md", content: BASIC_AGENT },
|
||||
{ filename: "bad.txt", content: "not a markdown file" },
|
||||
{ filename: "also-bad.json", content: "{}" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = loadProjectAgents(root);
|
||||
|
||||
expect(Object.keys(result)).toEqual(["test-agent"]);
|
||||
});
|
||||
|
||||
test("loads multiple agents", () => {
|
||||
const root = trackDir(
|
||||
createProjectWithAgents({
|
||||
claudeAgents: [
|
||||
{ filename: "agent-a.md", content: BASIC_AGENT },
|
||||
{
|
||||
filename: "agent-b.md",
|
||||
content: `---\nname: second-agent\ndescription: Another agent\n---\nDo other things.`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = loadProjectAgents(root);
|
||||
|
||||
expect(Object.keys(result).sort()).toEqual([
|
||||
"second-agent",
|
||||
"test-agent",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadOpencodeProjectAgents", () => {
|
||||
test("loads agents from <directory>/.opencode/agents", () => {
|
||||
const root = trackDir(
|
||||
createProjectWithAgents({
|
||||
opencodeAgents: [{ filename: "oc-agent.md", content: BASIC_AGENT }],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = loadOpencodeProjectAgents(root);
|
||||
|
||||
expect(Object.keys(result)).toEqual(["test-agent"]);
|
||||
expect(result["test-agent"].description).toBe(
|
||||
"(opencode-project) A test agent",
|
||||
);
|
||||
expect(result["test-agent"].mode).toBe("subagent");
|
||||
expect(result["test-agent"].prompt).toBe("You are a test agent.");
|
||||
});
|
||||
|
||||
test("returns empty object when project has no .opencode/agents directory", () => {
|
||||
const root = trackDir(mkdtempSync(join(tmpdir(), "agent-loader-test-")));
|
||||
|
||||
const result = loadOpencodeProjectAgents(root);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadUserAgents", () => {
|
||||
test("returns empty object when pointed at dir without agents/", () => {
|
||||
const root = trackDir(mkdtempSync(join(tmpdir(), "agent-loader-test-")))
|
||||
// Temporarily set env var — best-effort in parallel test runner
|
||||
const prev = process.env.CLAUDE_CONFIG_DIR
|
||||
try {
|
||||
process.env.CLAUDE_CONFIG_DIR = root
|
||||
const result = loadUserAgents()
|
||||
expect(result).toEqual({})
|
||||
} finally {
|
||||
if (prev !== undefined) process.env.CLAUDE_CONFIG_DIR = prev
|
||||
else delete process.env.CLAUDE_CONFIG_DIR
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("loadOpencodeGlobalAgents", () => {
|
||||
test("returns empty object when pointed at dir without agents/", () => {
|
||||
const root = trackDir(mkdtempSync(join(tmpdir(), "agent-loader-test-")))
|
||||
const prev = process.env.OPENCODE_CONFIG_DIR
|
||||
try {
|
||||
process.env.OPENCODE_CONFIG_DIR = root
|
||||
const result = loadOpencodeGlobalAgents()
|
||||
expect(result).toEqual({})
|
||||
} finally {
|
||||
if (prev !== undefined) process.env.OPENCODE_CONFIG_DIR = prev
|
||||
else delete process.env.OPENCODE_CONFIG_DIR
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("tools parsing", () => {
|
||||
test("parses comma-separated tools into boolean record", () => {
|
||||
const agentWithTools = `---\nname: tooled\ndescription: Has tools\ntools: Bash,Read,Edit\n---\nDo things.`;
|
||||
const root = trackDir(
|
||||
createProjectWithAgents({
|
||||
claudeAgents: [{ filename: "tooled.md", content: agentWithTools }],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = loadProjectAgents(root);
|
||||
|
||||
expect(result["tooled"].tools).toEqual({
|
||||
bash: true,
|
||||
read: true,
|
||||
edit: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("omits tools when frontmatter tools field is absent", () => {
|
||||
const agentNoTools = `---\nname: no-tools\ndescription: No tools\n---\nDo things.`;
|
||||
const root = trackDir(
|
||||
createProjectWithAgents({
|
||||
claudeAgents: [{ filename: "no-tools.md", content: agentNoTools }],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = loadProjectAgents(root);
|
||||
|
||||
expect(result["no-tools"].tools).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("scope labeling", () => {
|
||||
test("project and opencode-project loaders apply correct scope prefixes", () => {
|
||||
const root = trackDir(mkdtempSync(join(tmpdir(), "agent-loader-scope-")))
|
||||
const content = `---\nname: scoped\ndescription: Scoped agent\n---\nPrompt.`
|
||||
|
||||
const claudeProjectDir = join(root, "project", ".claude", "agents")
|
||||
const ocProjectDir = join(root, "project", ".opencode", "agents")
|
||||
|
||||
mkdirSync(claudeProjectDir, { recursive: true })
|
||||
mkdirSync(ocProjectDir, { recursive: true })
|
||||
|
||||
writeFileSync(join(claudeProjectDir, "a.md"), content, "utf-8")
|
||||
writeFileSync(join(ocProjectDir, "a.md"), content, "utf-8")
|
||||
|
||||
const project = loadProjectAgents(join(root, "project"))
|
||||
const ocProject = loadOpencodeProjectAgents(join(root, "project"))
|
||||
|
||||
expect(project["scoped"].description).toBe("(project) Scoped agent")
|
||||
expect(ocProject["scoped"].description).toBe("(opencode-project) Scoped agent")
|
||||
})
|
||||
})
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import { isMarkdownFile } from "../../shared/file-utils"
|
||||
import { getClaudeConfigDir } from "../../shared"
|
||||
import type { AgentScope, AgentFrontmatter, ClaudeCodeAgentConfig, LoadedAgent } from "./types"
|
||||
import { mapClaudeModelToOpenCode } from "./claude-model-mapper"
|
||||
import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir"
|
||||
|
||||
function parseToolsConfig(toolsStr?: string): Record<string, boolean> | undefined {
|
||||
if (!toolsStr) return undefined
|
||||
@@ -94,3 +95,26 @@ export function loadProjectAgents(directory?: string): Record<string, ClaudeCode
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function loadOpencodeGlobalAgents(): Record<string, ClaudeCodeAgentConfig> {
|
||||
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
|
||||
const opencodeAgentsDir = join(configDir, "agents")
|
||||
const agents = loadAgentsFromDir(opencodeAgentsDir, "opencode")
|
||||
|
||||
const result: Record<string, ClaudeCodeAgentConfig> = {}
|
||||
for (const agent of agents) {
|
||||
result[agent.name] = agent.config
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function loadOpencodeProjectAgents(directory?: string): Record<string, ClaudeCodeAgentConfig> {
|
||||
const opencodeProjectDir = join(directory ?? process.cwd(), ".opencode", "agents")
|
||||
const agents = loadAgentsFromDir(opencodeProjectDir, "opencode-project")
|
||||
|
||||
const result: Record<string, ClaudeCodeAgentConfig> = {}
|
||||
for (const agent of agents) {
|
||||
result[agent.name] = agent.config
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
|
||||
export type AgentScope = "user" | "project"
|
||||
export type AgentScope = "user" | "project" | "opencode" | "opencode-project"
|
||||
|
||||
export type ClaudeCodeAgentConfig = Omit<AgentConfig, "model"> & {
|
||||
model?: string | { providerID: string; modelID: string }
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
discoverProjectClaudeSkills,
|
||||
discoverUserClaudeSkills,
|
||||
} from "../features/opencode-skill-loader";
|
||||
import { loadProjectAgents, loadUserAgents } from "../features/claude-code-agent-loader";
|
||||
import { loadProjectAgents, loadUserAgents, loadOpencodeGlobalAgents, loadOpencodeProjectAgents } from "../features/claude-code-agent-loader";
|
||||
import type { PluginComponents } from "./plugin-components-loader";
|
||||
import { reorderAgentsByPriority } from "./agent-priority-order";
|
||||
import { remapAgentKeysToDisplayNames } from "./agent-key-remapper";
|
||||
@@ -139,6 +139,9 @@ export async function applyAgentConfig(params: {
|
||||
disableOmoEnv,
|
||||
);
|
||||
|
||||
const opencodeGlobalAgents = loadOpencodeGlobalAgents();
|
||||
const opencodeProjectAgents = loadOpencodeProjectAgents(params.ctx.directory);
|
||||
|
||||
const disabledAgentNames = new Set(
|
||||
(migratedDisabledAgents ?? []).map(a => a.toLowerCase())
|
||||
);
|
||||
@@ -257,6 +260,14 @@ export async function applyAgentConfig(params: {
|
||||
pluginAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
const filteredOpencodeGlobalAgents = filterProtectedAgentOverrides(
|
||||
opencodeGlobalAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
const filteredOpencodeProjectAgents = filterProtectedAgentOverrides(
|
||||
opencodeProjectAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
|
||||
params.config.agent = {
|
||||
...agentConfig,
|
||||
@@ -265,9 +276,12 @@ export async function applyAgentConfig(params: {
|
||||
([key]) => key !== "sisyphus" && key !== "hephaestus" && key !== "atlas",
|
||||
),
|
||||
),
|
||||
...filterDisabledAgents(filteredUserAgents),
|
||||
...filterDisabledAgents(filteredProjectAgents),
|
||||
// Precedence: later entries override earlier (project > global > user > plugin)
|
||||
...filterDisabledAgents(filteredPluginAgents),
|
||||
...filterDisabledAgents(filteredUserAgents),
|
||||
...filterDisabledAgents(filteredOpencodeGlobalAgents),
|
||||
...filterDisabledAgents(filteredProjectAgents),
|
||||
...filterDisabledAgents(filteredOpencodeProjectAgents),
|
||||
...filteredConfigAgents,
|
||||
build: { ...migratedBuild, mode: "subagent", hidden: true },
|
||||
...(planDemoteConfig ? { plan: planDemoteConfig } : {}),
|
||||
@@ -288,6 +302,14 @@ export async function applyAgentConfig(params: {
|
||||
pluginAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
const filteredOpencodeGlobalAgents = filterProtectedAgentOverrides(
|
||||
opencodeGlobalAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
const filteredOpencodeProjectAgents = filterProtectedAgentOverrides(
|
||||
opencodeProjectAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
|
||||
const defaultedConfigAgents = configAgent
|
||||
? Object.fromEntries(
|
||||
@@ -302,9 +324,12 @@ export async function applyAgentConfig(params: {
|
||||
|
||||
params.config.agent = {
|
||||
...builtinAgents,
|
||||
...filterDisabledAgents(filteredUserAgents),
|
||||
...filterDisabledAgents(filteredProjectAgents),
|
||||
// Precedence: later entries override earlier (project > global > user > plugin)
|
||||
...filterDisabledAgents(filteredPluginAgents),
|
||||
...filterDisabledAgents(filteredUserAgents),
|
||||
...filterDisabledAgents(filteredOpencodeGlobalAgents),
|
||||
...filterDisabledAgents(filteredProjectAgents),
|
||||
...filterDisabledAgents(filteredOpencodeProjectAgents),
|
||||
...defaultedConfigAgents,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@ beforeEach(async () => {
|
||||
|
||||
spyOn(agentLoader, "loadUserAgents" as any).mockReturnValue({})
|
||||
spyOn(agentLoader, "loadProjectAgents" as any).mockReturnValue({})
|
||||
spyOn(agentLoader, "loadOpencodeGlobalAgents" as any).mockReturnValue({})
|
||||
spyOn(agentLoader, "loadOpencodeProjectAgents" as any).mockReturnValue({})
|
||||
|
||||
spyOn(mcpLoader, "loadMcpConfigs" as any).mockResolvedValue({ servers: {} })
|
||||
setAdditionalAllowedMcpEnvVarsSpy = spyOn(mcpLoader, "setAdditionalAllowedMcpEnvVars").mockImplementation(() => {})
|
||||
@@ -118,6 +120,8 @@ afterEach(() => {
|
||||
;(skillLoader.discoverOpencodeProjectSkills as any)?.mockRestore?.()
|
||||
;(agentLoader.loadUserAgents as any)?.mockRestore?.()
|
||||
;(agentLoader.loadProjectAgents as any)?.mockRestore?.()
|
||||
;(agentLoader.loadOpencodeGlobalAgents as any)?.mockRestore?.()
|
||||
;(agentLoader.loadOpencodeProjectAgents as any)?.mockRestore?.()
|
||||
;(mcpLoader.loadMcpConfigs as any)?.mockRestore?.()
|
||||
setAdditionalAllowedMcpEnvVarsSpy?.mockRestore()
|
||||
;(pluginLoader.loadAllPluginComponents as any)?.mockRestore?.()
|
||||
@@ -1596,3 +1600,173 @@ describe("disable_omo_env pass-through", () => {
|
||||
expect(disableOmoEnv).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Agent merge priority — project-local overrides global", () => {
|
||||
test("project-local Claude agent overrides global Claude agent with same name", async () => {
|
||||
// #given — same agent name in both global (user) and project scopes
|
||||
;(agentLoader.loadUserAgents as any).mockReturnValue({
|
||||
"my-custom-agent": {
|
||||
description: "(user) global version",
|
||||
mode: "subagent",
|
||||
prompt: "I am the global agent",
|
||||
},
|
||||
})
|
||||
;(agentLoader.loadProjectAgents as any).mockReturnValue({
|
||||
"my-custom-agent": {
|
||||
description: "(project) project version",
|
||||
mode: "subagent",
|
||||
prompt: "I am the project agent",
|
||||
},
|
||||
})
|
||||
|
||||
const pluginConfig: OhMyOpenCodeConfig = {}
|
||||
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 — project version wins
|
||||
const agentConfig = config.agent as Record<string, { description?: string; prompt?: string }>
|
||||
expect(agentConfig["my-custom-agent"]?.description).toBe("(project) project version")
|
||||
expect(agentConfig["my-custom-agent"]?.prompt).toBe("I am the project agent")
|
||||
})
|
||||
|
||||
test("opencode project agent overrides opencode global agent with same name", async () => {
|
||||
// #given — same agent name in opencode global vs opencode project
|
||||
;(agentLoader.loadOpencodeGlobalAgents as any).mockReturnValue({
|
||||
"my-custom-agent": {
|
||||
description: "(opencode) global version",
|
||||
mode: "subagent",
|
||||
prompt: "I am the opencode global agent",
|
||||
},
|
||||
})
|
||||
;(agentLoader.loadOpencodeProjectAgents as any).mockReturnValue({
|
||||
"my-custom-agent": {
|
||||
description: "(opencode-project) project version",
|
||||
mode: "subagent",
|
||||
prompt: "I am the opencode project agent",
|
||||
},
|
||||
})
|
||||
|
||||
const pluginConfig: OhMyOpenCodeConfig = {}
|
||||
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 — opencode project version wins over opencode global
|
||||
const agentConfig = config.agent as Record<string, { description?: string; prompt?: string }>
|
||||
expect(agentConfig["my-custom-agent"]?.description).toBe("(opencode-project) project version")
|
||||
expect(agentConfig["my-custom-agent"]?.prompt).toBe("I am the opencode project agent")
|
||||
})
|
||||
|
||||
test("project Claude agent overrides opencode global agent with same name", async () => {
|
||||
// #given — project-scope Claude agent vs global-scope opencode agent
|
||||
;(agentLoader.loadOpencodeGlobalAgents as any).mockReturnValue({
|
||||
"my-custom-agent": {
|
||||
description: "(opencode) global version",
|
||||
mode: "subagent",
|
||||
prompt: "I am the opencode global agent",
|
||||
},
|
||||
})
|
||||
;(agentLoader.loadProjectAgents as any).mockReturnValue({
|
||||
"my-custom-agent": {
|
||||
description: "(project) project version",
|
||||
mode: "subagent",
|
||||
prompt: "I am the project Claude agent",
|
||||
},
|
||||
})
|
||||
|
||||
const pluginConfig: OhMyOpenCodeConfig = {}
|
||||
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 — project-scope wins over global-scope regardless of format
|
||||
const agentConfig = config.agent as Record<string, { description?: string; prompt?: string }>
|
||||
expect(agentConfig["my-custom-agent"]?.description).toBe("(project) project version")
|
||||
expect(agentConfig["my-custom-agent"]?.prompt).toBe("I am the project Claude agent")
|
||||
})
|
||||
|
||||
test("plugin agents have lowest priority — overridden by all other sources", async () => {
|
||||
// #given — same agent in plugin, global, and project scopes
|
||||
;(pluginLoader.loadAllPluginComponents as any).mockResolvedValue({
|
||||
commands: {},
|
||||
skills: {},
|
||||
agents: {
|
||||
"my-custom-agent": {
|
||||
description: "plugin version",
|
||||
mode: "subagent",
|
||||
prompt: "I am the plugin agent",
|
||||
},
|
||||
},
|
||||
mcpServers: {},
|
||||
hooksConfigs: [],
|
||||
plugins: [],
|
||||
errors: [],
|
||||
})
|
||||
;(agentLoader.loadUserAgents as any).mockReturnValue({
|
||||
"my-custom-agent": {
|
||||
description: "(user) global version",
|
||||
mode: "subagent",
|
||||
prompt: "I am the user agent",
|
||||
},
|
||||
})
|
||||
|
||||
const pluginConfig: OhMyOpenCodeConfig = {}
|
||||
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 — user (global) agent overrides plugin agent
|
||||
const agentConfig = config.agent as Record<string, { description?: string; prompt?: string }>
|
||||
expect(agentConfig["my-custom-agent"]?.description).toBe("(user) global version")
|
||||
expect(agentConfig["my-custom-agent"]?.prompt).toBe("I am the user agent")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin";
|
||||
import { ALLOWED_AGENTS } from "./constants";
|
||||
import { normalizeSDKResponse } from "../../shared";
|
||||
import { log } from "../../shared/logger";
|
||||
|
||||
type AgentInfo = {
|
||||
name: string;
|
||||
mode?: "subagent" | "primary" | "all";
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves the set of callable agent names at execute-time by merging the
|
||||
* hardcoded `ALLOWED_AGENTS` with any additional agents discovered dynamically
|
||||
* via `client.app.agents()`. Custom agents loaded from registered agent
|
||||
* directories appear here alongside built-ins.
|
||||
*
|
||||
* Falls back to `ALLOWED_AGENTS` alone if the dynamic lookup fails.
|
||||
*
|
||||
* @param client - The plugin client with access to the agent registry
|
||||
* @returns Array of lowercase callable agent names (excludes primary-mode agents)
|
||||
*/
|
||||
export async function resolveCallableAgents(
|
||||
client: PluginInput["client"],
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
const agentsResult = await client.app.agents();
|
||||
const agents = normalizeSDKResponse(agentsResult, [] as AgentInfo[], {
|
||||
preferResponseOnMissingData: true,
|
||||
});
|
||||
|
||||
const dynamicAgents = agents
|
||||
.filter((a) => a.mode !== "primary")
|
||||
.map((a) => a.name.toLowerCase());
|
||||
|
||||
const merged = new Set([...ALLOWED_AGENTS, ...dynamicAgents]);
|
||||
return [...merged];
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log(
|
||||
"[call_omo_agent] Failed to resolve dynamic agents, falling back to built-in list",
|
||||
{ error: message },
|
||||
);
|
||||
return [...ALLOWED_AGENTS];
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,11 @@ export const ALLOWED_AGENTS = [
|
||||
"multimodal-looker",
|
||||
] as const
|
||||
|
||||
export const CALL_OMO_AGENT_DESCRIPTION = `Spawn explore/librarian agent. run_in_background REQUIRED (true=async with task_id, false=sync).
|
||||
export const CALL_OMO_AGENT_DESCRIPTION = `Spawn explore/librarian agent or custom agents. run_in_background REQUIRED (true=async with task_id, false=sync).
|
||||
|
||||
Available: {agents}
|
||||
Built-in agents:
|
||||
{agents}
|
||||
|
||||
Custom agents registered via user or project agent directories are also supported.
|
||||
|
||||
Pass \`session_id=<id>\` to continue previous agent with full context. Nested subagent depth is tracked automatically and blocked past the configured limit. Prompts MUST be in English. Use \`background_output\` for async results.`
|
||||
|
||||
@@ -1,119 +1,237 @@
|
||||
const { beforeEach, describe, test, expect, mock } = require("bun:test")
|
||||
const { createCallOmoAgent } = require("./tools")
|
||||
|
||||
describe("createCallOmoAgent", () => {
|
||||
const assertCanSpawnMock = mock(() => Promise.resolve(undefined))
|
||||
const reserveCommitMock = mock(() => 1)
|
||||
const reserveRollbackMock = mock(() => {})
|
||||
const reserveSubagentSpawnMock = mock(() => Promise.resolve({
|
||||
spawnContext: { rootSessionID: "root-session", parentDepth: 0, childDepth: 1 },
|
||||
descendantCount: 1,
|
||||
commit: reserveCommitMock,
|
||||
rollback: reserveRollbackMock,
|
||||
}))
|
||||
const mockCtx = {
|
||||
client: {},
|
||||
type PluginInput = { client: any; directory: string }
|
||||
type BackgroundManager = {
|
||||
assertCanSpawn: Function
|
||||
reserveSubagentSpawn: Function
|
||||
launch: Function
|
||||
getTask: Function
|
||||
}
|
||||
|
||||
function createMockCtx(agents: Array<{ name: string; mode?: string }> = []): PluginInput {
|
||||
return {
|
||||
client: {
|
||||
app: {
|
||||
agents: mock(() => Promise.resolve(agents)),
|
||||
},
|
||||
},
|
||||
directory: "/test",
|
||||
}
|
||||
} as unknown as PluginInput
|
||||
}
|
||||
|
||||
const mockBackgroundManager = {
|
||||
assertCanSpawn: assertCanSpawnMock,
|
||||
reserveSubagentSpawn: reserveSubagentSpawnMock,
|
||||
launch: mock(() => Promise.resolve({
|
||||
id: "test-task-id",
|
||||
sessionID: null,
|
||||
description: "Test task",
|
||||
agent: "test-agent",
|
||||
status: "pending",
|
||||
})),
|
||||
}
|
||||
function createFailingMockCtx(error: Error = new Error("API unavailable")): PluginInput {
|
||||
return {
|
||||
client: {
|
||||
app: {
|
||||
agents: mock(() => Promise.reject(error)),
|
||||
},
|
||||
},
|
||||
directory: "/test",
|
||||
} as unknown as PluginInput
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
assertCanSpawnMock.mockClear()
|
||||
reserveSubagentSpawnMock.mockClear()
|
||||
reserveCommitMock.mockClear()
|
||||
reserveRollbackMock.mockClear()
|
||||
const DEFAULT_AGENTS = [
|
||||
{ name: "explore", mode: "subagent" },
|
||||
{ name: "librarian", mode: "subagent" },
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
{ name: "hephaestus", mode: "subagent" },
|
||||
{ name: "metis", mode: "subagent" },
|
||||
{ name: "momus", mode: "subagent" },
|
||||
{ name: "multimodal-looker", mode: "subagent" },
|
||||
]
|
||||
|
||||
const assertCanSpawnMock = mock(() => Promise.resolve(undefined))
|
||||
const reserveCommitMock = mock(() => 1)
|
||||
const reserveRollbackMock = mock(() => {})
|
||||
const reserveSubagentSpawnMock = mock(() => Promise.resolve({
|
||||
spawnContext: { rootSessionID: "root-session", parentDepth: 0, childDepth: 1 },
|
||||
descendantCount: 1,
|
||||
commit: reserveCommitMock,
|
||||
rollback: reserveRollbackMock,
|
||||
}))
|
||||
|
||||
const mockBackgroundManager = {
|
||||
assertCanSpawn: assertCanSpawnMock,
|
||||
reserveSubagentSpawn: reserveSubagentSpawnMock,
|
||||
launch: mock(() => Promise.resolve({
|
||||
id: "test-task-id",
|
||||
sessionID: null,
|
||||
description: "Test task",
|
||||
agent: "test-agent",
|
||||
status: "pending",
|
||||
})),
|
||||
getTask: mock(() => ({ status: "pending", sessionID: "ses-123" })),
|
||||
} as unknown as BackgroundManager
|
||||
|
||||
const toolCtx = {
|
||||
sessionID: "test",
|
||||
messageID: "msg",
|
||||
agent: "test",
|
||||
abort: new AbortController().signal,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
assertCanSpawnMock.mockClear()
|
||||
reserveSubagentSpawnMock.mockClear()
|
||||
reserveCommitMock.mockClear()
|
||||
reserveRollbackMock.mockClear()
|
||||
})
|
||||
|
||||
describe("createCallOmoAgent", () => {
|
||||
describe("disabled_agents validation", () => {
|
||||
test("should reject agent in disabled_agents list", async () => {
|
||||
const mockCtx = createMockCtx(DEFAULT_AGENTS)
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, ["explore"])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
|
||||
const result = await executeFunc(
|
||||
{ description: "Test", prompt: "Test prompt", subagent_type: "explore", run_in_background: true },
|
||||
toolCtx
|
||||
)
|
||||
|
||||
expect(result).toContain("disabled via disabled_agents")
|
||||
})
|
||||
|
||||
test("should reject agent in disabled_agents list with case-insensitive matching", async () => {
|
||||
const mockCtx = createMockCtx(DEFAULT_AGENTS)
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, ["Explore"])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
|
||||
const result = await executeFunc(
|
||||
{ description: "Test", prompt: "Test prompt", subagent_type: "explore", run_in_background: true },
|
||||
toolCtx
|
||||
)
|
||||
|
||||
expect(result).toContain("disabled via disabled_agents")
|
||||
})
|
||||
|
||||
test("should allow agent not in disabled_agents list", async () => {
|
||||
const mockCtx = createMockCtx(DEFAULT_AGENTS)
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, ["librarian"])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
|
||||
const result = await executeFunc(
|
||||
{ description: "Test", prompt: "Test prompt", subagent_type: "explore", run_in_background: true },
|
||||
toolCtx
|
||||
)
|
||||
|
||||
expect(result).not.toContain("disabled via disabled_agents")
|
||||
})
|
||||
|
||||
test("should allow all agents when disabled_agents is empty", async () => {
|
||||
const mockCtx = createMockCtx(DEFAULT_AGENTS)
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, [])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
|
||||
const result = await executeFunc(
|
||||
{ description: "Test", prompt: "Test prompt", subagent_type: "explore", run_in_background: true },
|
||||
toolCtx
|
||||
)
|
||||
|
||||
expect(result).not.toContain("disabled via disabled_agents")
|
||||
})
|
||||
})
|
||||
|
||||
test("should reject agent in disabled_agents list", async () => {
|
||||
//#given
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, ["explore"])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
describe("dynamic custom agent resolution", () => {
|
||||
test("should accept a custom agent returned by client.app.agents()", async () => {
|
||||
const agents = [...DEFAULT_AGENTS, { name: "bug-fixer", mode: "subagent" }]
|
||||
const mockCtx = createMockCtx(agents)
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, [])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
|
||||
//#when
|
||||
const result = await executeFunc(
|
||||
{
|
||||
description: "Test",
|
||||
prompt: "Test prompt",
|
||||
subagent_type: "explore",
|
||||
run_in_background: true,
|
||||
},
|
||||
{ sessionID: "test", messageID: "msg", agent: "test", abort: new AbortController().signal }
|
||||
)
|
||||
const result = await executeFunc(
|
||||
{ description: "Test", prompt: "Fix bug", subagent_type: "bug-fixer", run_in_background: true },
|
||||
toolCtx
|
||||
)
|
||||
|
||||
//#then
|
||||
expect(result).toContain("disabled via disabled_agents")
|
||||
})
|
||||
expect(result).not.toContain("Invalid agent type")
|
||||
expect(result).not.toContain("not found")
|
||||
})
|
||||
|
||||
test("should reject agent in disabled_agents list with case-insensitive matching", async () => {
|
||||
//#given
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, ["Explore"])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
test("should reject a custom agent NOT returned by client.app.agents()", async () => {
|
||||
const mockCtx = createMockCtx(DEFAULT_AGENTS)
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, [])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
|
||||
//#when
|
||||
const result = await executeFunc(
|
||||
{
|
||||
description: "Test",
|
||||
prompt: "Test prompt",
|
||||
subagent_type: "explore",
|
||||
run_in_background: true,
|
||||
},
|
||||
{ sessionID: "test", messageID: "msg", agent: "test", abort: new AbortController().signal }
|
||||
)
|
||||
const result = await executeFunc(
|
||||
{ description: "Test", prompt: "Fix bug", subagent_type: "nonexistent-agent", run_in_background: true },
|
||||
toolCtx
|
||||
)
|
||||
|
||||
//#then
|
||||
expect(result).toContain("disabled via disabled_agents")
|
||||
})
|
||||
expect(result).toContain("Invalid agent type")
|
||||
})
|
||||
|
||||
test("should allow agent not in disabled_agents list", async () => {
|
||||
//#given
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, ["librarian"])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
test("should perform case-insensitive matching for custom agents", async () => {
|
||||
const agents = [...DEFAULT_AGENTS, { name: "Bug-Fixer", mode: "subagent" }]
|
||||
const mockCtx = createMockCtx(agents)
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, [])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
|
||||
//#when
|
||||
const result = await executeFunc(
|
||||
{
|
||||
description: "Test",
|
||||
prompt: "Test prompt",
|
||||
subagent_type: "explore",
|
||||
run_in_background: true,
|
||||
},
|
||||
{ sessionID: "test", messageID: "msg", agent: "test", abort: new AbortController().signal }
|
||||
)
|
||||
const result = await executeFunc(
|
||||
{ description: "Test", prompt: "Fix bug", subagent_type: "bug-fixer", run_in_background: true },
|
||||
toolCtx
|
||||
)
|
||||
|
||||
//#then
|
||||
// Should not contain disabled error - may fail for other reasons but disabled check should pass
|
||||
expect(result).not.toContain("disabled via disabled_agents")
|
||||
})
|
||||
expect(result).not.toContain("Invalid agent type")
|
||||
})
|
||||
|
||||
test("should allow all agents when disabled_agents is empty", async () => {
|
||||
//#given
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, [])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
test("should exclude primary-mode agents from callable list", async () => {
|
||||
const agents = [
|
||||
...DEFAULT_AGENTS,
|
||||
{ name: "sisyphus", mode: "primary" },
|
||||
]
|
||||
const mockCtx = createMockCtx(agents)
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, [])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
|
||||
//#when
|
||||
const result = await executeFunc(
|
||||
{
|
||||
description: "Test",
|
||||
prompt: "Test prompt",
|
||||
subagent_type: "explore",
|
||||
run_in_background: true,
|
||||
},
|
||||
{ sessionID: "test", messageID: "msg", agent: "test", abort: new AbortController().signal }
|
||||
)
|
||||
const result = await executeFunc(
|
||||
{ description: "Test", prompt: "Orchestrate", subagent_type: "sisyphus", run_in_background: true },
|
||||
toolCtx
|
||||
)
|
||||
|
||||
//#then
|
||||
expect(result).not.toContain("disabled via disabled_agents")
|
||||
expect(result).toContain("Invalid agent type")
|
||||
})
|
||||
|
||||
test("should fall back to ALLOWED_AGENTS when client.app.agents() fails", async () => {
|
||||
const mockCtx = createFailingMockCtx()
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, [])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
|
||||
const result = await executeFunc(
|
||||
{ description: "Test", prompt: "Explore codebase", subagent_type: "explore", run_in_background: true },
|
||||
toolCtx
|
||||
)
|
||||
|
||||
expect(result).not.toContain("Invalid agent type")
|
||||
})
|
||||
|
||||
test("should reject unknown agent even when client.app.agents() fails (fallback mode)", async () => {
|
||||
const mockCtx = createFailingMockCtx()
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, [])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
|
||||
const result = await executeFunc(
|
||||
{ description: "Test", prompt: "Fix bug", subagent_type: "custom-agent", run_in_background: true },
|
||||
toolCtx
|
||||
)
|
||||
|
||||
expect(result).toContain("Invalid agent type")
|
||||
})
|
||||
|
||||
test("should still apply disabled_agents check to dynamically resolved custom agents", async () => {
|
||||
const agents = [...DEFAULT_AGENTS, { name: "bug-fixer", mode: "subagent" }]
|
||||
const mockCtx = createMockCtx(agents)
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, ["bug-fixer"])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
|
||||
const result = await executeFunc(
|
||||
{ description: "Test", prompt: "Fix bug", subagent_type: "bug-fixer", run_in_background: true },
|
||||
toolCtx
|
||||
)
|
||||
|
||||
expect(result).toContain("disabled via disabled_agents")
|
||||
})
|
||||
})
|
||||
|
||||
test("uses agent override fallback_models when launching background subagent", async () => {
|
||||
@@ -129,6 +247,7 @@ describe("createCallOmoAgent", () => {
|
||||
launch,
|
||||
getTask: mock(() => undefined),
|
||||
}
|
||||
const mockCtx = createMockCtx(DEFAULT_AGENTS)
|
||||
const toolDef = createCallOmoAgent(
|
||||
mockCtx,
|
||||
managerWithLaunch,
|
||||
@@ -371,6 +490,7 @@ describe("createCallOmoAgent", () => {
|
||||
|
||||
test("should return a tool error when sync spawn depth validation fails", async () => {
|
||||
//#given
|
||||
const mockCtx = createMockCtx(DEFAULT_AGENTS)
|
||||
reserveSubagentSpawnMock.mockRejectedValueOnce(new Error("Subagent spawn blocked: child depth 4 exceeds background_task.maxDepth=3."))
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, [])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
|
||||
@@ -14,6 +14,7 @@ import { CONFIG_BASENAME } from "../../shared/plugin-identity"
|
||||
import { parseModelString } from "../delegate-task/model-string-parser"
|
||||
import { executeBackground } from "./background-executor"
|
||||
import { executeSync } from "./sync-executor"
|
||||
import { resolveCallableAgents } from "./agent-resolver"
|
||||
|
||||
function resolveModelAndFallbackChain(args: {
|
||||
subagentType: string
|
||||
@@ -83,39 +84,57 @@ export function createCallOmoAgent(
|
||||
userCategories?: CategoriesConfig,
|
||||
): ToolDefinition {
|
||||
const agentDescriptions = ALLOWED_AGENTS.map(
|
||||
(name) => `- ${name}: Specialized agent for ${name} tasks`
|
||||
).join("\n")
|
||||
const description = CALL_OMO_AGENT_DESCRIPTION.replace("{agents}", agentDescriptions)
|
||||
(name) => `- ${name}: Specialized agent for ${name} tasks`,
|
||||
).join("\n");
|
||||
const description = CALL_OMO_AGENT_DESCRIPTION.replace(
|
||||
"{agents}",
|
||||
agentDescriptions,
|
||||
);
|
||||
|
||||
return tool({
|
||||
description,
|
||||
args: {
|
||||
description: tool.schema.string().describe("A short (3-5 words) description of the task"),
|
||||
prompt: tool.schema.string().describe("The task for the agent to perform"),
|
||||
description: tool.schema
|
||||
.string()
|
||||
.describe("A short (3-5 words) description of the task"),
|
||||
prompt: tool.schema
|
||||
.string()
|
||||
.describe("The task for the agent to perform"),
|
||||
subagent_type: tool.schema
|
||||
.string()
|
||||
.describe("The type of specialized agent to use for this task (explore or librarian only)"),
|
||||
.describe(
|
||||
"The agent to invoke. Supports built-in agents and any custom agents registered at runtime.",
|
||||
),
|
||||
run_in_background: tool.schema
|
||||
.boolean()
|
||||
.describe("REQUIRED. true: run asynchronously (use background_output to get results), false: run synchronously and wait for completion"),
|
||||
session_id: tool.schema.string().describe("Existing Task session to continue").optional(),
|
||||
.describe(
|
||||
"REQUIRED. true: run asynchronously (use background_output to get results), false: run synchronously and wait for completion",
|
||||
),
|
||||
session_id: tool.schema
|
||||
.string()
|
||||
.describe("Existing Task session to continue")
|
||||
.optional(),
|
||||
},
|
||||
async execute(args: CallOmoAgentArgs, toolContext) {
|
||||
const toolCtx = toolContext as ToolContextWithMetadata
|
||||
log(`[call_omo_agent] Starting with agent: ${args.subagent_type}, background: ${args.run_in_background}`)
|
||||
const toolCtx = toolContext as ToolContextWithMetadata;
|
||||
log(
|
||||
`[call_omo_agent] Starting with agent: ${args.subagent_type}, background: ${args.run_in_background}`,
|
||||
);
|
||||
|
||||
const callableAgents = await resolveCallableAgents(ctx.client);
|
||||
|
||||
// Strip ZWSP and case-insensitive agent validation - allows "Explore", "EXPLORE", "explore" etc.
|
||||
const strippedAgentType = stripInvisibleAgentCharacters(args.subagent_type)
|
||||
if (
|
||||
!ALLOWED_AGENTS.some(
|
||||
!callableAgents.some(
|
||||
(name) => name.toLowerCase() === strippedAgentType.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
return `Error: Invalid agent type "${args.subagent_type}". Only ${ALLOWED_AGENTS.join(", ")} are allowed.`
|
||||
return `Error: Invalid agent type "${args.subagent_type}". Only ${callableAgents.join(", ")} are allowed.`;
|
||||
}
|
||||
|
||||
const normalizedAgent = strippedAgentType.toLowerCase() as AllowedAgentType
|
||||
args = { ...args, subagent_type: normalizedAgent }
|
||||
const normalizedAgent = strippedAgentType.toLowerCase();
|
||||
args = { ...args, subagent_type: normalizedAgent };
|
||||
|
||||
// Check if agent is disabled
|
||||
if (disabledAgents.some((disabled) => stripInvisibleAgentCharacters(disabled).toLowerCase() === normalizedAgent)) {
|
||||
@@ -130,7 +149,7 @@ export function createCallOmoAgent(
|
||||
|
||||
if (args.run_in_background) {
|
||||
if (args.session_id) {
|
||||
return `Error: session_id is not supported in background mode. Use run_in_background=false to continue an existing session.`
|
||||
return `Error: session_id is not supported in background mode. Use run_in_background=false to continue an existing session.`;
|
||||
}
|
||||
return await executeBackground(args, toolCtx, backgroundManager, ctx.client, fallbackChain, resolvedModel)
|
||||
}
|
||||
@@ -148,5 +167,5 @@ export function createCallOmoAgent(
|
||||
|
||||
return await executeSync(args, toolCtx, ctx, undefined, fallbackChain, undefined, resolvedModel)
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user