|
|
@@ -6,6 +6,7 @@ import {
|
|
|
DEFAULT_DISABLED_AGENTS,
|
|
|
DEFAULT_MODELS,
|
|
|
getAgentOverride,
|
|
|
+ getCustomAgentNames,
|
|
|
loadAgentPrompt,
|
|
|
type PluginConfig,
|
|
|
PROTECTED_AGENTS,
|
|
|
@@ -21,7 +22,11 @@ import { createFixerAgent } from './fixer';
|
|
|
import { createLibrarianAgent } from './librarian';
|
|
|
import { createObserverAgent } from './observer';
|
|
|
import { createOracleAgent } from './oracle';
|
|
|
-import { type AgentDefinition, createOrchestratorAgent } from './orchestrator';
|
|
|
+import {
|
|
|
+ type AgentDefinition,
|
|
|
+ createOrchestratorAgent,
|
|
|
+ resolvePrompt,
|
|
|
+} from './orchestrator';
|
|
|
|
|
|
export type { AgentDefinition } from './orchestrator';
|
|
|
|
|
|
@@ -38,6 +43,10 @@ function normalizeDisplayName(displayName: string): string {
|
|
|
return trimmed.startsWith('@') ? trimmed.slice(1) : trimmed;
|
|
|
}
|
|
|
|
|
|
+function escapeRegExp(value: string): string {
|
|
|
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
+}
|
|
|
+
|
|
|
// Agent Configuration Helpers
|
|
|
|
|
|
/**
|
|
|
@@ -74,6 +83,63 @@ function applyOverrides(
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function isKnownAgentName(name: string): boolean {
|
|
|
+ return (ALL_AGENT_NAMES as readonly string[]).includes(name);
|
|
|
+}
|
|
|
+
|
|
|
+function normalizeCustomAgentName(name: string): string {
|
|
|
+ return name.trim();
|
|
|
+}
|
|
|
+
|
|
|
+function isSafeCustomAgentName(name: string): boolean {
|
|
|
+ return /^[a-z][a-z0-9_-]*$/i.test(name) && !isKnownAgentName(name);
|
|
|
+}
|
|
|
+
|
|
|
+function hasCustomAgentModel(
|
|
|
+ override: AgentOverrideConfig | undefined,
|
|
|
+): override is AgentOverrideConfig & {
|
|
|
+ model: NonNullable<AgentOverrideConfig['model']>;
|
|
|
+} {
|
|
|
+ if (!override?.model) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return !Array.isArray(override.model) || override.model.length > 0;
|
|
|
+}
|
|
|
+
|
|
|
+function validateCustomAgentOverride(
|
|
|
+ name: string,
|
|
|
+ override: AgentOverrideConfig,
|
|
|
+): void {
|
|
|
+ const orchestratorPrompt = override.orchestratorPrompt?.trim();
|
|
|
+ if (orchestratorPrompt && !orchestratorPrompt.startsWith(`@${name}`)) {
|
|
|
+ throw new Error(
|
|
|
+ `Custom agent '${name}' orchestratorPrompt must start with '@${name}'`,
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function buildCustomAgentDefinition(
|
|
|
+ name: string,
|
|
|
+ override: AgentOverrideConfig,
|
|
|
+ filePrompt?: string,
|
|
|
+ fileAppendPrompt?: string,
|
|
|
+): AgentDefinition {
|
|
|
+ const basePrompt = override.prompt ?? `You are the ${name} specialist.`;
|
|
|
+
|
|
|
+ return {
|
|
|
+ name,
|
|
|
+ config: {
|
|
|
+ model:
|
|
|
+ typeof override.model === 'string'
|
|
|
+ ? override.model
|
|
|
+ : (DEFAULT_MODELS.orchestrator ?? DEFAULT_MODELS.oracle),
|
|
|
+ temperature: 0.2,
|
|
|
+ prompt: resolvePrompt(basePrompt, filePrompt, fileAppendPrompt),
|
|
|
+ },
|
|
|
+ } as AgentDefinition;
|
|
|
+}
|
|
|
+
|
|
|
function injectDisplayNames(
|
|
|
orchestrator: AgentDefinition,
|
|
|
nameMap: Map<string, string>,
|
|
|
@@ -84,7 +150,7 @@ function injectDisplayNames(
|
|
|
|
|
|
for (const [internalName, displayName] of nameMap) {
|
|
|
prompt = prompt.replace(
|
|
|
- new RegExp(`@${internalName}\\b`, 'g'),
|
|
|
+ new RegExp(`@${escapeRegExp(internalName)}\\b`, 'g'),
|
|
|
`@${normalizeDisplayName(displayName)}`,
|
|
|
);
|
|
|
}
|
|
|
@@ -198,8 +264,53 @@ export function createAgents(config?: PluginConfig): AgentDefinition[] {
|
|
|
);
|
|
|
});
|
|
|
|
|
|
- // 2. Apply overrides and default permissions to each agent
|
|
|
- const allSubAgents = protoSubAgents.map((agent) => {
|
|
|
+ // 1b. Discover unknown keys in config.agents as custom subagents.
|
|
|
+ const customAgentNames = getCustomAgentNames(config)
|
|
|
+ .map(normalizeCustomAgentName)
|
|
|
+ .filter((name) => name.length > 0)
|
|
|
+ .filter((name) => {
|
|
|
+ if (!isSafeCustomAgentName(name)) {
|
|
|
+ throw new Error(`Unsafe custom agent name '${name}'`);
|
|
|
+ }
|
|
|
+ if (disabled.has(name)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ });
|
|
|
+
|
|
|
+ const protoCustomAgents = customAgentNames.flatMap((name) => {
|
|
|
+ const override = getAgentOverride(config, name);
|
|
|
+ if (!hasCustomAgentModel(override)) {
|
|
|
+ console.warn(
|
|
|
+ `[oh-my-opencode] Custom agent '${name}' skipped: 'model' is required`,
|
|
|
+ );
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ validateCustomAgentOverride(name, override);
|
|
|
+ const customPrompts = loadAgentPrompt(name, config?.preset);
|
|
|
+
|
|
|
+ return [
|
|
|
+ buildCustomAgentDefinition(
|
|
|
+ name,
|
|
|
+ override,
|
|
|
+ customPrompts.prompt,
|
|
|
+ customPrompts.appendPrompt,
|
|
|
+ ),
|
|
|
+ ];
|
|
|
+ });
|
|
|
+
|
|
|
+ // 2. Apply overrides and default permissions to built-in subagents
|
|
|
+ const builtInSubAgents = protoSubAgents.map((agent) => {
|
|
|
+ const override = getAgentOverride(config, agent.name);
|
|
|
+ if (override) {
|
|
|
+ applyOverrides(agent, override);
|
|
|
+ }
|
|
|
+ applyDefaultPermissions(agent, override?.skills);
|
|
|
+ return agent;
|
|
|
+ });
|
|
|
+
|
|
|
+ const customSubAgents = protoCustomAgents.map((agent) => {
|
|
|
const override = getAgentOverride(config, agent.name);
|
|
|
if (override) {
|
|
|
applyOverrides(agent, override);
|
|
|
@@ -208,6 +319,8 @@ export function createAgents(config?: PluginConfig): AgentDefinition[] {
|
|
|
return agent;
|
|
|
});
|
|
|
|
|
|
+ const allSubAgents = [...builtInSubAgents, ...customSubAgents];
|
|
|
+
|
|
|
// 3. Create Orchestrator (with its own overrides and custom prompts)
|
|
|
// DEFAULT_MODELS.orchestrator is undefined; model is resolved via override or
|
|
|
// left unset so the runtime chat.message hook can pick it from _modelArray.
|
|
|
@@ -237,6 +350,14 @@ export function createAgents(config?: PluginConfig): AgentDefinition[] {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 3b. Append custom orchestrator hints from custom agent overrides.
|
|
|
+ const customOrchestratorPrompts = customSubAgents
|
|
|
+ .map((agent) => {
|
|
|
+ const override = getAgentOverride(config, agent.name);
|
|
|
+ return override?.orchestratorPrompt;
|
|
|
+ })
|
|
|
+ .filter((prompt): prompt is string => Boolean(prompt));
|
|
|
+
|
|
|
// Validate display names
|
|
|
const usedDisplayNames = new Set<string>();
|
|
|
for (const [, displayName] of displayNameMap) {
|
|
|
@@ -249,9 +370,12 @@ export function createAgents(config?: PluginConfig): AgentDefinition[] {
|
|
|
usedDisplayNames.add(normalizedDisplayName);
|
|
|
}
|
|
|
for (const displayName of usedDisplayNames) {
|
|
|
- if ((ALL_AGENT_NAMES as readonly string[]).includes(displayName)) {
|
|
|
+ if (
|
|
|
+ (ALL_AGENT_NAMES as readonly string[]).includes(displayName) ||
|
|
|
+ customAgentNames.includes(displayName)
|
|
|
+ ) {
|
|
|
throw new Error(
|
|
|
- `displayName '${displayName}' conflicts with internal agent name`,
|
|
|
+ `displayName '${displayName}' conflicts with an agent name`,
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
@@ -259,6 +383,23 @@ export function createAgents(config?: PluginConfig): AgentDefinition[] {
|
|
|
// Inject display names into orchestrator prompt (complete map)
|
|
|
injectDisplayNames(orchestrator, displayNameMap);
|
|
|
|
|
|
+ if (customOrchestratorPrompts.length > 0) {
|
|
|
+ const rewrittenPrompts = customOrchestratorPrompts.map((promptText) => {
|
|
|
+ let text = promptText;
|
|
|
+ for (const [internalName, displayName] of displayNameMap) {
|
|
|
+ text = text.replace(
|
|
|
+ new RegExp(`@${escapeRegExp(internalName)}\\b`, 'g'),
|
|
|
+ `@${normalizeDisplayName(displayName)}`,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ return text;
|
|
|
+ });
|
|
|
+
|
|
|
+ orchestrator.config.prompt = `${orchestrator.config.prompt}\n\n${rewrittenPrompts.join(
|
|
|
+ '\n\n',
|
|
|
+ )}`;
|
|
|
+ }
|
|
|
+
|
|
|
return [orchestrator, ...allSubAgents];
|
|
|
}
|
|
|
|
|
|
@@ -294,6 +435,8 @@ export function getAgentConfigs(
|
|
|
sdkConfig.mode = 'subagent';
|
|
|
} else if (name === 'orchestrator') {
|
|
|
sdkConfig.mode = 'primary';
|
|
|
+ } else {
|
|
|
+ sdkConfig.mode = 'subagent';
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -355,5 +498,11 @@ export function getDisabledAgents(config?: PluginConfig): Set<string> {
|
|
|
*/
|
|
|
export function getEnabledAgentNames(config?: PluginConfig): string[] {
|
|
|
const disabled = getDisabledAgents(config);
|
|
|
- return ALL_AGENT_NAMES.filter((name) => !disabled.has(name));
|
|
|
+ const customAgentNames = getCustomAgentNames(config).filter(
|
|
|
+ (name) => !disabled.has(name),
|
|
|
+ );
|
|
|
+ return [
|
|
|
+ ...ALL_AGENT_NAMES.filter((name) => !disabled.has(name)),
|
|
|
+ ...customAgentNames,
|
|
|
+ ];
|
|
|
}
|