Download src/tools/AgentTool/loadAgentsDir.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 26.2 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/tools/AgentTool/loadAgentsDir.ts
- Command line
-
hf download hf://chenbhao/codev/src/tools/AgentTool/loadAgentsDir.ts
-
curl -L -o loadAgentsDir.ts https://huggingface.co/chenbhao/codev/resolve/main/src/tools/AgentTool/loadAgentsDir.ts
26.2 kB
| import { feature } from 'bun:bundle' | |
| import memoize from 'lodash-es/memoize.js' | |
| import { basename } from 'path' | |
| import type { SettingSource } from 'src/utils/settings/constants.js' | |
| import { z } from 'zod/v4' | |
| import { isAutoMemoryEnabled } from '../../memdir/paths.js' | |
| import { | |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| logEvent, | |
| } from '../../services/analytics/index.js' | |
| import { | |
| type McpServerConfig, | |
| McpServerConfigSchema, | |
| } from '../../services/mcp/types.js' | |
| import type { ToolUseContext } from '../../Tool.js' | |
| import { logForDebugging } from '../../utils/debug.js' | |
| import { | |
| EFFORT_LEVELS, | |
| type EffortValue, | |
| parseEffortValue, | |
| } from '../../utils/effort.js' | |
| import { isEnvTruthy } from '../../utils/envUtils.js' | |
| import { parsePositiveIntFromFrontmatter } from '../../utils/frontmatterParser.js' | |
| import { lazySchema } from '../../utils/lazySchema.js' | |
| import { logError } from '../../utils/log.js' | |
| import { | |
| loadMarkdownFilesForSubdir, | |
| parseAgentToolsFromFrontmatter, | |
| parseSlashCommandToolsFromFrontmatter, | |
| } from '../../utils/markdownConfigLoader.js' | |
| import { | |
| PERMISSION_MODES, | |
| type PermissionMode, | |
| } from '../../utils/permissions/PermissionMode.js' | |
| import { | |
| clearPluginAgentCache, | |
| loadPluginAgents, | |
| } from '../../utils/plugins/loadPluginAgents.js' | |
| import { HooksSchema, type HooksSettings } from '../../utils/settings/types.js' | |
| import { jsonStringify } from '../../utils/slowOperations.js' | |
| import { FILE_EDIT_TOOL_NAME } from '../FileEditTool/constants.js' | |
| import { FILE_READ_TOOL_NAME } from '../FileReadTool/prompt.js' | |
| import { FILE_WRITE_TOOL_NAME } from '../FileWriteTool/prompt.js' | |
| import { | |
| AGENT_COLORS, | |
| type AgentColorName, | |
| setAgentColor, | |
| } from './agentColorManager.js' | |
| import { type AgentMemoryScope, loadAgentMemoryPrompt } from './agentMemory.js' | |
| import { | |
| checkAgentMemorySnapshot, | |
| initializeFromSnapshot, | |
| } from './agentMemorySnapshot.js' | |
| import { getBuiltInAgents } from './builtInAgents.js' | |
| // Type for MCP server specification in agent definitions | |
| // Can be either a reference to an existing server by name, or an inline definition as { [name]: config } | |
| export type AgentMcpServerSpec = | |
| | string // Reference to existing server by name (e.g., "slack") | |
| | { [name: string]: McpServerConfig } // Inline definition as { name: config } | |
| // Zod schema for agent MCP server specs | |
| const AgentMcpServerSpecSchema = lazySchema(() => | |
| z.union([ | |
| z.string(), // Reference by name | |
| z.record(z.string(), McpServerConfigSchema()), // Inline as { name: config } | |
| ]), | |
| ) | |
| // Zod schemas for JSON agent validation | |
| // Note: HooksSchema is lazy so the circular chain AppState -> loadAgentsDir -> settings/types | |
| // is broken at module load time | |
| const AgentJsonSchema = lazySchema(() => | |
| z.object({ | |
| description: z.string().min(1, 'Description cannot be empty'), | |
| tools: z.array(z.string()).optional(), | |
| disallowedTools: z.array(z.string()).optional(), | |
| prompt: z.string().min(1, 'Prompt cannot be empty'), | |
| model: z | |
| .string() | |
| .trim() | |
| .min(1, 'Model cannot be empty') | |
| .transform(m => (m.toLowerCase() === 'inherit' ? 'inherit' : m)) | |
| .optional(), | |
| effort: z.union([z.enum(EFFORT_LEVELS), z.number().int()]).optional(), | |
| permissionMode: z.enum(PERMISSION_MODES).optional(), | |
| mcpServers: z.array(AgentMcpServerSpecSchema()).optional(), | |
| hooks: HooksSchema().optional(), | |
| maxTurns: z.number().int().positive().optional(), | |
| skills: z.array(z.string()).optional(), | |
| initialPrompt: z.string().optional(), | |
| memory: z.enum(['user', 'project', 'local']).optional(), | |
| background: z.boolean().optional(), | |
| isolation: (process.env.USER_TYPE === 'ant' | |
| ? z.enum(['worktree', 'remote']) | |
| : z.enum(['worktree']) | |
| ).optional(), | |
| }), | |
| ) | |
| const AgentsJsonSchema = lazySchema(() => | |
| z.record(z.string(), AgentJsonSchema()), | |
| ) | |
| // Base type with common fields for all agents | |
| export type BaseAgentDefinition = { | |
| agentType: string | |
| whenToUse: string | |
| tools?: string[] | |
| disallowedTools?: string[] | |
| skills?: string[] // Skill names to preload (parsed from comma-separated frontmatter) | |
| mcpServers?: AgentMcpServerSpec[] // MCP servers specific to this agent | |
| hooks?: HooksSettings // Session-scoped hooks registered when agent starts | |
| color?: AgentColorName | |
| model?: string | |
| effort?: EffortValue | |
| permissionMode?: PermissionMode | |
| maxTurns?: number // Maximum number of agentic turns before stopping | |
| filename?: string // Original filename without .md extension (for user/project/managed agents) | |
| baseDir?: string | |
| criticalSystemReminder_EXPERIMENTAL?: string // Short message re-injected at every user turn | |
| requiredMcpServers?: string[] // MCP server name patterns that must be configured for agent to be available | |
| background?: boolean // Always run as background task when spawned | |
| initialPrompt?: string // Prepended to the first user turn (slash commands work) | |
| memory?: AgentMemoryScope // Persistent memory scope | |
| isolation?: 'worktree' | 'remote' // Run in an isolated git worktree, or remotely in CCR (ant-only) | |
| pendingSnapshotUpdate?: { snapshotTimestamp: string } | |
| /** Omit CLAUDE.md hierarchy from the agent's userContext. Read-only agents | |
| * (Explore, Plan) don't need commit/PR/lint guidelines — the main agent has | |
| * full CLAUDE.md and interprets their output. Saves ~5-15 Gtok/week across | |
| * 34M+ Explore spawns. Kill-switch: tengu_slim_subagent_claudemd. */ | |
| omitClaudeMd?: boolean | |
| } | |
| // Built-in agents - dynamic prompts only, no static systemPrompt field | |
| export type BuiltInAgentDefinition = BaseAgentDefinition & { | |
| source: 'built-in' | |
| baseDir: 'built-in' | |
| callback?: () => void | |
| getSystemPrompt: (params: { | |
| toolUseContext: Pick<ToolUseContext, 'options'> | |
| }) => string | |
| } | |
| // Custom agents from user/project/policy settings - prompt stored via closure | |
| export type CustomAgentDefinition = BaseAgentDefinition & { | |
| getSystemPrompt: () => string | |
| source: SettingSource | |
| filename?: string | |
| baseDir?: string | |
| } | |
| // Plugin agents - similar to custom but with plugin metadata, prompt stored via closure | |
| export type PluginAgentDefinition = BaseAgentDefinition & { | |
| getSystemPrompt: () => string | |
| source: 'plugin' | |
| filename?: string | |
| plugin: string | |
| } | |
| // Union type for all agent types | |
| export type AgentDefinition = | |
| | BuiltInAgentDefinition | |
| | CustomAgentDefinition | |
| | PluginAgentDefinition | |
| // Type guards for runtime type checking | |
| export function isBuiltInAgent( | |
| agent: AgentDefinition, | |
| ): agent is BuiltInAgentDefinition { | |
| return agent.source === 'built-in' | |
| } | |
| export function isCustomAgent( | |
| agent: AgentDefinition, | |
| ): agent is CustomAgentDefinition { | |
| return agent.source !== 'built-in' && agent.source !== 'plugin' | |
| } | |
| export function isPluginAgent( | |
| agent: AgentDefinition, | |
| ): agent is PluginAgentDefinition { | |
| return agent.source === 'plugin' | |
| } | |
| export type AgentDefinitionsResult = { | |
| activeAgents: AgentDefinition[] | |
| allAgents: AgentDefinition[] | |
| failedFiles?: Array<{ path: string; error: string }> | |
| allowedAgentTypes?: string[] | |
| } | |
| export function getActiveAgentsFromList( | |
| allAgents: AgentDefinition[], | |
| ): AgentDefinition[] { | |
| const builtInAgents = allAgents.filter(a => a.source === 'built-in') | |
| const pluginAgents = allAgents.filter(a => a.source === 'plugin') | |
| const userAgents = allAgents.filter(a => a.source === 'userSettings') | |
| const projectAgents = allAgents.filter(a => a.source === 'projectSettings') | |
| const managedAgents = allAgents.filter(a => a.source === 'policySettings') | |
| const flagAgents = allAgents.filter(a => a.source === 'flagSettings') | |
| const agentGroups = [ | |
| builtInAgents, | |
| pluginAgents, | |
| userAgents, | |
| projectAgents, | |
| flagAgents, | |
| managedAgents, | |
| ] | |
| const agentMap = new Map<string, AgentDefinition>() | |
| for (const agents of agentGroups) { | |
| for (const agent of agents) { | |
| agentMap.set(agent.agentType, agent) | |
| } | |
| } | |
| return Array.from(agentMap.values()) | |
| } | |
| /** | |
| * Checks if an agent's required MCP servers are available. | |
| * Returns true if no requirements or all requirements are met. | |
| * @param agent The agent to check | |
| * @param availableServers List of available MCP server names (e.g., from mcp.clients) | |
| */ | |
| export function hasRequiredMcpServers( | |
| agent: AgentDefinition, | |
| availableServers: string[], | |
| ): boolean { | |
| if (!agent.requiredMcpServers || agent.requiredMcpServers.length === 0) { | |
| return true | |
| } | |
| // Each required pattern must match at least one available server (case-insensitive) | |
| return agent.requiredMcpServers.every(pattern => | |
| availableServers.some(server => | |
| server.toLowerCase().includes(pattern.toLowerCase()), | |
| ), | |
| ) | |
| } | |
| /** | |
| * Filters agents based on MCP server requirements. | |
| * Only returns agents whose required MCP servers are available. | |
| * @param agents List of agents to filter | |
| * @param availableServers List of available MCP server names | |
| */ | |
| export function filterAgentsByMcpRequirements( | |
| agents: AgentDefinition[], | |
| availableServers: string[], | |
| ): AgentDefinition[] { | |
| return agents.filter(agent => hasRequiredMcpServers(agent, availableServers)) | |
| } | |
| /** | |
| * Check for and initialize agent memory from project snapshots. | |
| * For agents with memory enabled, copies snapshot to local if no local memory exists. | |
| * For agents with newer snapshots, logs a debug message (user prompt TODO). | |
| */ | |
| async function initializeAgentMemorySnapshots( | |
| agents: CustomAgentDefinition[], | |
| ): Promise<void> { | |
| await Promise.all( | |
| agents.map(async agent => { | |
| if (agent.memory !== 'user') return | |
| const result = await checkAgentMemorySnapshot( | |
| agent.agentType, | |
| agent.memory, | |
| ) | |
| switch (result.action) { | |
| case 'initialize': | |
| logForDebugging( | |
| `Initializing ${agent.agentType} memory from project snapshot`, | |
| ) | |
| await initializeFromSnapshot( | |
| agent.agentType, | |
| agent.memory, | |
| result.snapshotTimestamp!, | |
| ) | |
| break | |
| case 'prompt-update': | |
| agent.pendingSnapshotUpdate = { | |
| snapshotTimestamp: result.snapshotTimestamp!, | |
| } | |
| logForDebugging( | |
| `Newer snapshot available for ${agent.agentType} memory (snapshot: ${result.snapshotTimestamp})`, | |
| ) | |
| break | |
| } | |
| }), | |
| ) | |
| } | |
| export const getAgentDefinitionsWithOverrides = memoize( | |
| async (cwd: string): Promise<AgentDefinitionsResult> => { | |
| // Simple mode: skip custom agents, only return built-ins | |
| if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) { | |
| const builtInAgents = getBuiltInAgents() | |
| return { | |
| activeAgents: builtInAgents, | |
| allAgents: builtInAgents, | |
| } | |
| } | |
| try { | |
| const markdownFiles = await loadMarkdownFilesForSubdir('agents', cwd) | |
| const failedFiles: Array<{ path: string; error: string }> = [] | |
| const customAgents = markdownFiles | |
| .map(({ filePath, baseDir, frontmatter, content, source }) => { | |
| const agent = parseAgentFromMarkdown( | |
| filePath, | |
| baseDir, | |
| frontmatter, | |
| content, | |
| source, | |
| ) | |
| if (!agent) { | |
| // Skip non-agent markdown files silently (e.g., reference docs | |
| // co-located with agent definitions). Only report errors for files | |
| // that look like agent attempts (have a 'name' field in frontmatter). | |
| if (!frontmatter['name']) { | |
| return null | |
| } | |
| const errorMsg = getParseError(frontmatter) | |
| failedFiles.push({ path: filePath, error: errorMsg }) | |
| logForDebugging( | |
| `Failed to parse agent from ${filePath}: ${errorMsg}`, | |
| ) | |
| logEvent('tengu_agent_parse_error', { | |
| error: | |
| errorMsg as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| location: | |
| source as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| }) | |
| return null | |
| } | |
| return agent | |
| }) | |
| .filter(agent => agent !== null) | |
| // Kick off plugin agent loading concurrently with memory snapshot init — | |
| // loadPluginAgents is memoized and takes no args, so it's independent. | |
| // Join both so neither becomes a floating promise if the other throws. | |
| let pluginAgentsPromise = loadPluginAgents() | |
| if (feature('AGENT_MEMORY_SNAPSHOT') && isAutoMemoryEnabled()) { | |
| const [pluginAgents_] = await Promise.all([ | |
| pluginAgentsPromise, | |
| initializeAgentMemorySnapshots(customAgents), | |
| ]) | |
| pluginAgentsPromise = Promise.resolve(pluginAgents_) | |
| } | |
| const pluginAgents = await pluginAgentsPromise | |
| const builtInAgents = getBuiltInAgents() | |
| const allAgentsList: AgentDefinition[] = [ | |
| ...builtInAgents, | |
| ...pluginAgents, | |
| ...customAgents, | |
| ] | |
| const activeAgents = getActiveAgentsFromList(allAgentsList) | |
| // Initialize colors for all active agents | |
| for (const agent of activeAgents) { | |
| if (agent.color) { | |
| setAgentColor(agent.agentType, agent.color) | |
| } | |
| } | |
| return { | |
| activeAgents, | |
| allAgents: allAgentsList, | |
| failedFiles: failedFiles.length > 0 ? failedFiles : undefined, | |
| } | |
| } catch (error) { | |
| const errorMessage = | |
| error instanceof Error ? error.message : String(error) | |
| logForDebugging(`Error loading agent definitions: ${errorMessage}`) | |
| logError(error) | |
| // Even on error, return the built-in agents | |
| const builtInAgents = getBuiltInAgents() | |
| return { | |
| activeAgents: builtInAgents, | |
| allAgents: builtInAgents, | |
| failedFiles: [{ path: 'unknown', error: errorMessage }], | |
| } | |
| } | |
| }, | |
| ) | |
| export function clearAgentDefinitionsCache(): void { | |
| getAgentDefinitionsWithOverrides.cache.clear?.() | |
| clearPluginAgentCache() | |
| } | |
| /** | |
| * Helper to determine the specific parsing error for an agent file | |
| */ | |
| function getParseError(frontmatter: Record<string, unknown>): string { | |
| const agentType = frontmatter['name'] | |
| const description = frontmatter['description'] | |
| if (!agentType || typeof agentType !== 'string') { | |
| return 'Missing required "name" field in frontmatter' | |
| } | |
| if (!description || typeof description !== 'string') { | |
| return 'Missing required "description" field in frontmatter' | |
| } | |
| return 'Unknown parsing error' | |
| } | |
| /** | |
| * Parse hooks from frontmatter using the HooksSchema | |
| * @param frontmatter The frontmatter object containing potential hooks | |
| * @param agentType The agent type for logging purposes | |
| * @returns Parsed hooks settings or undefined if invalid/missing | |
| */ | |
| function parseHooksFromFrontmatter( | |
| frontmatter: Record<string, unknown>, | |
| agentType: string, | |
| ): HooksSettings | undefined { | |
| if (!frontmatter.hooks) { | |
| return undefined | |
| } | |
| const result = HooksSchema().safeParse(frontmatter.hooks) | |
| if (!result.success) { | |
| logForDebugging( | |
| `Invalid hooks in agent '${agentType}': ${result.error.message}`, | |
| ) | |
| return undefined | |
| } | |
| return result.data | |
| } | |
| /** | |
| * Parses agent definition from JSON data | |
| */ | |
| export function parseAgentFromJson( | |
| name: string, | |
| definition: unknown, | |
| source: SettingSource = 'flagSettings', | |
| ): CustomAgentDefinition | null { | |
| try { | |
| const parsed = AgentJsonSchema().parse(definition) | |
| let tools = parseAgentToolsFromFrontmatter(parsed.tools) | |
| // If memory is enabled, inject Write/Edit/Read tools for memory access | |
| if (isAutoMemoryEnabled() && parsed.memory && tools !== undefined) { | |
| const toolSet = new Set(tools) | |
| for (const tool of [ | |
| FILE_WRITE_TOOL_NAME, | |
| FILE_EDIT_TOOL_NAME, | |
| FILE_READ_TOOL_NAME, | |
| ]) { | |
| if (!toolSet.has(tool)) { | |
| tools = [...tools, tool] | |
| } | |
| } | |
| } | |
| const disallowedTools = | |
| parsed.disallowedTools !== undefined | |
| ? parseAgentToolsFromFrontmatter(parsed.disallowedTools) | |
| : undefined | |
| const systemPrompt = parsed.prompt | |
| const agent: CustomAgentDefinition = { | |
| agentType: name, | |
| whenToUse: parsed.description, | |
| ...(tools !== undefined ? { tools } : {}), | |
| ...(disallowedTools !== undefined ? { disallowedTools } : {}), | |
| getSystemPrompt: () => { | |
| if (isAutoMemoryEnabled() && parsed.memory) { | |
| return ( | |
| systemPrompt + '\n\n' + loadAgentMemoryPrompt(name, parsed.memory) | |
| ) | |
| } | |
| return systemPrompt | |
| }, | |
| source, | |
| ...(parsed.model ? { model: parsed.model } : {}), | |
| ...(parsed.effort !== undefined ? { effort: parsed.effort } : {}), | |
| ...(parsed.permissionMode | |
| ? { permissionMode: parsed.permissionMode } | |
| : {}), | |
| ...(parsed.mcpServers && parsed.mcpServers.length > 0 | |
| ? { mcpServers: parsed.mcpServers } | |
| : {}), | |
| ...(parsed.hooks ? { hooks: parsed.hooks } : {}), | |
| ...(parsed.maxTurns !== undefined ? { maxTurns: parsed.maxTurns } : {}), | |
| ...(parsed.skills && parsed.skills.length > 0 | |
| ? { skills: parsed.skills } | |
| : {}), | |
| ...(parsed.initialPrompt ? { initialPrompt: parsed.initialPrompt } : {}), | |
| ...(parsed.background ? { background: parsed.background } : {}), | |
| ...(parsed.memory ? { memory: parsed.memory } : {}), | |
| ...(parsed.isolation ? { isolation: parsed.isolation } : {}), | |
| } | |
| return agent | |
| } catch (error) { | |
| const errorMessage = error instanceof Error ? error.message : String(error) | |
| logForDebugging(`Error parsing agent '${name}' from JSON: ${errorMessage}`) | |
| logError(error) | |
| return null | |
| } | |
| } | |
| /** | |
| * Parses multiple agents from a JSON object | |
| */ | |
| export function parseAgentsFromJson( | |
| agentsJson: unknown, | |
| source: SettingSource = 'flagSettings', | |
| ): AgentDefinition[] { | |
| try { | |
| const parsed = AgentsJsonSchema().parse(agentsJson) | |
| return Object.entries(parsed) | |
| .map(([name, def]) => parseAgentFromJson(name, def, source)) | |
| .filter((agent): agent is CustomAgentDefinition => agent !== null) | |
| } catch (error) { | |
| const errorMessage = error instanceof Error ? error.message : String(error) | |
| logForDebugging(`Error parsing agents from JSON: ${errorMessage}`) | |
| logError(error) | |
| return [] | |
| } | |
| } | |
| /** | |
| * Parses agent definition from markdown file data | |
| */ | |
| export function parseAgentFromMarkdown( | |
| filePath: string, | |
| baseDir: string, | |
| frontmatter: Record<string, unknown>, | |
| content: string, | |
| source: SettingSource, | |
| ): CustomAgentDefinition | null { | |
| try { | |
| const agentType = frontmatter['name'] | |
| let whenToUse = frontmatter['description'] as string | |
| // Validate required fields — silently skip files without any agent | |
| // frontmatter (they're likely co-located reference documentation) | |
| if (!agentType || typeof agentType !== 'string') { | |
| return null | |
| } | |
| if (!whenToUse || typeof whenToUse !== 'string') { | |
| logForDebugging( | |
| `Agent file ${filePath} is missing required 'description' in frontmatter`, | |
| ) | |
| return null | |
| } | |
| // Unescape newlines in whenToUse that were escaped for YAML parsing | |
| whenToUse = whenToUse.replace(/\\n/g, '\n') | |
| const color = frontmatter['color'] as AgentColorName | undefined | |
| const modelRaw = frontmatter['model'] | |
| let model: string | undefined | |
| if (typeof modelRaw === 'string' && modelRaw.trim().length > 0) { | |
| const trimmed = modelRaw.trim() | |
| model = trimmed.toLowerCase() === 'inherit' ? 'inherit' : trimmed | |
| } | |
| // Parse background flag | |
| const backgroundRaw = frontmatter['background'] | |
| if ( | |
| backgroundRaw !== undefined && | |
| backgroundRaw !== 'true' && | |
| backgroundRaw !== 'false' && | |
| backgroundRaw !== true && | |
| backgroundRaw !== false | |
| ) { | |
| logForDebugging( | |
| `Agent file ${filePath} has invalid background value '${backgroundRaw}'. Must be 'true', 'false', or omitted.`, | |
| ) | |
| } | |
| const background = | |
| backgroundRaw === 'true' || backgroundRaw === true ? true : undefined | |
| // Parse memory scope | |
| const VALID_MEMORY_SCOPES: AgentMemoryScope[] = ['user', 'project', 'local'] | |
| const memoryRaw = frontmatter['memory'] as string | undefined | |
| let memory: AgentMemoryScope | undefined | |
| if (memoryRaw !== undefined) { | |
| if (VALID_MEMORY_SCOPES.includes(memoryRaw as AgentMemoryScope)) { | |
| memory = memoryRaw as AgentMemoryScope | |
| } else { | |
| logForDebugging( | |
| `Agent file ${filePath} has invalid memory value '${memoryRaw}'. Valid options: ${VALID_MEMORY_SCOPES.join(', ')}`, | |
| ) | |
| } | |
| } | |
| // Parse isolation mode. 'remote' is ant-only; external builds reject it at parse time. | |
| type IsolationMode = 'worktree' | 'remote' | |
| const VALID_ISOLATION_MODES: readonly IsolationMode[] = | |
| process.env.USER_TYPE === 'ant' ? ['worktree', 'remote'] : ['worktree'] | |
| const isolationRaw = frontmatter['isolation'] as string | undefined | |
| let isolation: IsolationMode | undefined | |
| if (isolationRaw !== undefined) { | |
| if (VALID_ISOLATION_MODES.includes(isolationRaw as IsolationMode)) { | |
| isolation = isolationRaw as IsolationMode | |
| } else { | |
| logForDebugging( | |
| `Agent file ${filePath} has invalid isolation value '${isolationRaw}'. Valid options: ${VALID_ISOLATION_MODES.join(', ')}`, | |
| ) | |
| } | |
| } | |
| // Parse effort from frontmatter (supports string levels and integers) | |
| const effortRaw = frontmatter['effort'] | |
| const parsedEffort = | |
| effortRaw !== undefined ? parseEffortValue(effortRaw) : undefined | |
| if (effortRaw !== undefined && parsedEffort === undefined) { | |
| logForDebugging( | |
| `Agent file ${filePath} has invalid effort '${effortRaw}'. Valid options: ${EFFORT_LEVELS.join(', ')} or an integer`, | |
| ) | |
| } | |
| // Parse permissionMode from frontmatter | |
| const permissionModeRaw = frontmatter['permissionMode'] as | |
| | string | |
| | undefined | |
| const isValidPermissionMode = | |
| permissionModeRaw && | |
| (PERMISSION_MODES as readonly string[]).includes(permissionModeRaw) | |
| if (permissionModeRaw && !isValidPermissionMode) { | |
| const errorMsg = `Agent file ${filePath} has invalid permissionMode '${permissionModeRaw}'. Valid options: ${PERMISSION_MODES.join(', ')}` | |
| logForDebugging(errorMsg) | |
| } | |
| // Parse maxTurns from frontmatter | |
| const maxTurnsRaw = frontmatter['maxTurns'] | |
| const maxTurns = parsePositiveIntFromFrontmatter(maxTurnsRaw) | |
| if (maxTurnsRaw !== undefined && maxTurns === undefined) { | |
| logForDebugging( | |
| `Agent file ${filePath} has invalid maxTurns '${maxTurnsRaw}'. Must be a positive integer.`, | |
| ) | |
| } | |
| // Extract filename without extension | |
| const filename = basename(filePath, '.md') | |
| // Parse tools from frontmatter | |
| let tools = parseAgentToolsFromFrontmatter(frontmatter['tools']) | |
| // If memory is enabled, inject Write/Edit/Read tools for memory access | |
| if (isAutoMemoryEnabled() && memory && tools !== undefined) { | |
| const toolSet = new Set(tools) | |
| for (const tool of [ | |
| FILE_WRITE_TOOL_NAME, | |
| FILE_EDIT_TOOL_NAME, | |
| FILE_READ_TOOL_NAME, | |
| ]) { | |
| if (!toolSet.has(tool)) { | |
| tools = [...tools, tool] | |
| } | |
| } | |
| } | |
| // Parse disallowedTools from frontmatter | |
| const disallowedToolsRaw = frontmatter['disallowedTools'] | |
| const disallowedTools = | |
| disallowedToolsRaw !== undefined | |
| ? parseAgentToolsFromFrontmatter(disallowedToolsRaw) | |
| : undefined | |
| // Parse skills from frontmatter | |
| const skills = parseSlashCommandToolsFromFrontmatter(frontmatter['skills']) | |
| const initialPromptRaw = frontmatter['initialPrompt'] | |
| const initialPrompt = | |
| typeof initialPromptRaw === 'string' && initialPromptRaw.trim() | |
| ? initialPromptRaw | |
| : undefined | |
| // Parse mcpServers from frontmatter using same Zod validation as JSON agents | |
| const mcpServersRaw = frontmatter['mcpServers'] | |
| let mcpServers: AgentMcpServerSpec[] | undefined | |
| if (Array.isArray(mcpServersRaw)) { | |
| mcpServers = mcpServersRaw | |
| .map(item => { | |
| const result = AgentMcpServerSpecSchema().safeParse(item) | |
| if (result.success) { | |
| return result.data | |
| } | |
| logForDebugging( | |
| `Agent file ${filePath} has invalid mcpServers item: ${jsonStringify(item)}. Error: ${result.error.message}`, | |
| ) | |
| return null | |
| }) | |
| .filter((item): item is AgentMcpServerSpec => item !== null) | |
| } | |
| // Parse hooks from frontmatter | |
| const hooks = parseHooksFromFrontmatter(frontmatter, agentType) | |
| const systemPrompt = content.trim() | |
| const agentDef: CustomAgentDefinition = { | |
| baseDir, | |
| agentType: agentType, | |
| whenToUse: whenToUse, | |
| ...(tools !== undefined ? { tools } : {}), | |
| ...(disallowedTools !== undefined ? { disallowedTools } : {}), | |
| ...(skills !== undefined ? { skills } : {}), | |
| ...(initialPrompt !== undefined ? { initialPrompt } : {}), | |
| ...(mcpServers !== undefined && mcpServers.length > 0 | |
| ? { mcpServers } | |
| : {}), | |
| ...(hooks !== undefined ? { hooks } : {}), | |
| getSystemPrompt: () => { | |
| if (isAutoMemoryEnabled() && memory) { | |
| const memoryPrompt = loadAgentMemoryPrompt(agentType, memory) | |
| return systemPrompt + '\n\n' + memoryPrompt | |
| } | |
| return systemPrompt | |
| }, | |
| source, | |
| filename, | |
| ...(color && typeof color === 'string' && AGENT_COLORS.includes(color) | |
| ? { color } | |
| : {}), | |
| ...(model !== undefined ? { model } : {}), | |
| ...(parsedEffort !== undefined ? { effort: parsedEffort } : {}), | |
| ...(isValidPermissionMode | |
| ? { permissionMode: permissionModeRaw as PermissionMode } | |
| : {}), | |
| ...(maxTurns !== undefined ? { maxTurns } : {}), | |
| ...(background ? { background } : {}), | |
| ...(memory ? { memory } : {}), | |
| ...(isolation ? { isolation } : {}), | |
| } | |
| return agentDef | |
| } catch (error) { | |
| const errorMessage = error instanceof Error ? error.message : String(error) | |
| logForDebugging(`Error parsing agent from ${filePath}: ${errorMessage}`) | |
| logError(error) | |
| return null | |
| } | |
| } | |