source dump of claude code
at main 755 lines 26 kB view raw
1import { feature } from 'bun:bundle' 2import memoize from 'lodash-es/memoize.js' 3import { basename } from 'path' 4import type { SettingSource } from 'src/utils/settings/constants.js' 5import { z } from 'zod/v4' 6import { isAutoMemoryEnabled } from '../../memdir/paths.js' 7import { 8 type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 9 logEvent, 10} from '../../services/analytics/index.js' 11import { 12 type McpServerConfig, 13 McpServerConfigSchema, 14} from '../../services/mcp/types.js' 15import type { ToolUseContext } from '../../Tool.js' 16import { logForDebugging } from '../../utils/debug.js' 17import { 18 EFFORT_LEVELS, 19 type EffortValue, 20 parseEffortValue, 21} from '../../utils/effort.js' 22import { isEnvTruthy } from '../../utils/envUtils.js' 23import { parsePositiveIntFromFrontmatter } from '../../utils/frontmatterParser.js' 24import { lazySchema } from '../../utils/lazySchema.js' 25import { logError } from '../../utils/log.js' 26import { 27 loadMarkdownFilesForSubdir, 28 parseAgentToolsFromFrontmatter, 29 parseSlashCommandToolsFromFrontmatter, 30} from '../../utils/markdownConfigLoader.js' 31import { 32 PERMISSION_MODES, 33 type PermissionMode, 34} from '../../utils/permissions/PermissionMode.js' 35import { 36 clearPluginAgentCache, 37 loadPluginAgents, 38} from '../../utils/plugins/loadPluginAgents.js' 39import { HooksSchema, type HooksSettings } from '../../utils/settings/types.js' 40import { jsonStringify } from '../../utils/slowOperations.js' 41import { FILE_EDIT_TOOL_NAME } from '../FileEditTool/constants.js' 42import { FILE_READ_TOOL_NAME } from '../FileReadTool/prompt.js' 43import { FILE_WRITE_TOOL_NAME } from '../FileWriteTool/prompt.js' 44import { 45 AGENT_COLORS, 46 type AgentColorName, 47 setAgentColor, 48} from './agentColorManager.js' 49import { type AgentMemoryScope, loadAgentMemoryPrompt } from './agentMemory.js' 50import { 51 checkAgentMemorySnapshot, 52 initializeFromSnapshot, 53} from './agentMemorySnapshot.js' 54import { getBuiltInAgents } from './builtInAgents.js' 55 56// Type for MCP server specification in agent definitions 57// Can be either a reference to an existing server by name, or an inline definition as { [name]: config } 58export type AgentMcpServerSpec = 59 | string // Reference to existing server by name (e.g., "slack") 60 | { [name: string]: McpServerConfig } // Inline definition as { name: config } 61 62// Zod schema for agent MCP server specs 63const AgentMcpServerSpecSchema = lazySchema(() => 64 z.union([ 65 z.string(), // Reference by name 66 z.record(z.string(), McpServerConfigSchema()), // Inline as { name: config } 67 ]), 68) 69 70// Zod schemas for JSON agent validation 71// Note: HooksSchema is lazy so the circular chain AppState -> loadAgentsDir -> settings/types 72// is broken at module load time 73const AgentJsonSchema = lazySchema(() => 74 z.object({ 75 description: z.string().min(1, 'Description cannot be empty'), 76 tools: z.array(z.string()).optional(), 77 disallowedTools: z.array(z.string()).optional(), 78 prompt: z.string().min(1, 'Prompt cannot be empty'), 79 model: z 80 .string() 81 .trim() 82 .min(1, 'Model cannot be empty') 83 .transform(m => (m.toLowerCase() === 'inherit' ? 'inherit' : m)) 84 .optional(), 85 effort: z.union([z.enum(EFFORT_LEVELS), z.number().int()]).optional(), 86 permissionMode: z.enum(PERMISSION_MODES).optional(), 87 mcpServers: z.array(AgentMcpServerSpecSchema()).optional(), 88 hooks: HooksSchema().optional(), 89 maxTurns: z.number().int().positive().optional(), 90 skills: z.array(z.string()).optional(), 91 initialPrompt: z.string().optional(), 92 memory: z.enum(['user', 'project', 'local']).optional(), 93 background: z.boolean().optional(), 94 isolation: (process.env.USER_TYPE === 'ant' 95 ? z.enum(['worktree', 'remote']) 96 : z.enum(['worktree']) 97 ).optional(), 98 }), 99) 100 101const AgentsJsonSchema = lazySchema(() => 102 z.record(z.string(), AgentJsonSchema()), 103) 104 105// Base type with common fields for all agents 106export type BaseAgentDefinition = { 107 agentType: string 108 whenToUse: string 109 tools?: string[] 110 disallowedTools?: string[] 111 skills?: string[] // Skill names to preload (parsed from comma-separated frontmatter) 112 mcpServers?: AgentMcpServerSpec[] // MCP servers specific to this agent 113 hooks?: HooksSettings // Session-scoped hooks registered when agent starts 114 color?: AgentColorName 115 model?: string 116 effort?: EffortValue 117 permissionMode?: PermissionMode 118 maxTurns?: number // Maximum number of agentic turns before stopping 119 filename?: string // Original filename without .md extension (for user/project/managed agents) 120 baseDir?: string 121 criticalSystemReminder_EXPERIMENTAL?: string // Short message re-injected at every user turn 122 requiredMcpServers?: string[] // MCP server name patterns that must be configured for agent to be available 123 background?: boolean // Always run as background task when spawned 124 initialPrompt?: string // Prepended to the first user turn (slash commands work) 125 memory?: AgentMemoryScope // Persistent memory scope 126 isolation?: 'worktree' | 'remote' // Run in an isolated git worktree, or remotely in CCR (ant-only) 127 pendingSnapshotUpdate?: { snapshotTimestamp: string } 128 /** Omit CLAUDE.md hierarchy from the agent's userContext. Read-only agents 129 * (Explore, Plan) don't need commit/PR/lint guidelines — the main agent has 130 * full CLAUDE.md and interprets their output. Saves ~5-15 Gtok/week across 131 * 34M+ Explore spawns. Kill-switch: tengu_slim_subagent_claudemd. */ 132 omitClaudeMd?: boolean 133} 134 135// Built-in agents - dynamic prompts only, no static systemPrompt field 136export type BuiltInAgentDefinition = BaseAgentDefinition & { 137 source: 'built-in' 138 baseDir: 'built-in' 139 callback?: () => void 140 getSystemPrompt: (params: { 141 toolUseContext: Pick<ToolUseContext, 'options'> 142 }) => string 143} 144 145// Custom agents from user/project/policy settings - prompt stored via closure 146export type CustomAgentDefinition = BaseAgentDefinition & { 147 getSystemPrompt: () => string 148 source: SettingSource 149 filename?: string 150 baseDir?: string 151} 152 153// Plugin agents - similar to custom but with plugin metadata, prompt stored via closure 154export type PluginAgentDefinition = BaseAgentDefinition & { 155 getSystemPrompt: () => string 156 source: 'plugin' 157 filename?: string 158 plugin: string 159} 160 161// Union type for all agent types 162export type AgentDefinition = 163 | BuiltInAgentDefinition 164 | CustomAgentDefinition 165 | PluginAgentDefinition 166 167// Type guards for runtime type checking 168export function isBuiltInAgent( 169 agent: AgentDefinition, 170): agent is BuiltInAgentDefinition { 171 return agent.source === 'built-in' 172} 173 174export function isCustomAgent( 175 agent: AgentDefinition, 176): agent is CustomAgentDefinition { 177 return agent.source !== 'built-in' && agent.source !== 'plugin' 178} 179 180export function isPluginAgent( 181 agent: AgentDefinition, 182): agent is PluginAgentDefinition { 183 return agent.source === 'plugin' 184} 185 186export type AgentDefinitionsResult = { 187 activeAgents: AgentDefinition[] 188 allAgents: AgentDefinition[] 189 failedFiles?: Array<{ path: string; error: string }> 190 allowedAgentTypes?: string[] 191} 192 193export function getActiveAgentsFromList( 194 allAgents: AgentDefinition[], 195): AgentDefinition[] { 196 const builtInAgents = allAgents.filter(a => a.source === 'built-in') 197 const pluginAgents = allAgents.filter(a => a.source === 'plugin') 198 const userAgents = allAgents.filter(a => a.source === 'userSettings') 199 const projectAgents = allAgents.filter(a => a.source === 'projectSettings') 200 const managedAgents = allAgents.filter(a => a.source === 'policySettings') 201 const flagAgents = allAgents.filter(a => a.source === 'flagSettings') 202 203 const agentGroups = [ 204 builtInAgents, 205 pluginAgents, 206 userAgents, 207 projectAgents, 208 flagAgents, 209 managedAgents, 210 ] 211 212 const agentMap = new Map<string, AgentDefinition>() 213 214 for (const agents of agentGroups) { 215 for (const agent of agents) { 216 agentMap.set(agent.agentType, agent) 217 } 218 } 219 220 return Array.from(agentMap.values()) 221} 222 223/** 224 * Checks if an agent's required MCP servers are available. 225 * Returns true if no requirements or all requirements are met. 226 * @param agent The agent to check 227 * @param availableServers List of available MCP server names (e.g., from mcp.clients) 228 */ 229export function hasRequiredMcpServers( 230 agent: AgentDefinition, 231 availableServers: string[], 232): boolean { 233 if (!agent.requiredMcpServers || agent.requiredMcpServers.length === 0) { 234 return true 235 } 236 // Each required pattern must match at least one available server (case-insensitive) 237 return agent.requiredMcpServers.every(pattern => 238 availableServers.some(server => 239 server.toLowerCase().includes(pattern.toLowerCase()), 240 ), 241 ) 242} 243 244/** 245 * Filters agents based on MCP server requirements. 246 * Only returns agents whose required MCP servers are available. 247 * @param agents List of agents to filter 248 * @param availableServers List of available MCP server names 249 */ 250export function filterAgentsByMcpRequirements( 251 agents: AgentDefinition[], 252 availableServers: string[], 253): AgentDefinition[] { 254 return agents.filter(agent => hasRequiredMcpServers(agent, availableServers)) 255} 256 257/** 258 * Check for and initialize agent memory from project snapshots. 259 * For agents with memory enabled, copies snapshot to local if no local memory exists. 260 * For agents with newer snapshots, logs a debug message (user prompt TODO). 261 */ 262async function initializeAgentMemorySnapshots( 263 agents: CustomAgentDefinition[], 264): Promise<void> { 265 await Promise.all( 266 agents.map(async agent => { 267 if (agent.memory !== 'user') return 268 const result = await checkAgentMemorySnapshot( 269 agent.agentType, 270 agent.memory, 271 ) 272 switch (result.action) { 273 case 'initialize': 274 logForDebugging( 275 `Initializing ${agent.agentType} memory from project snapshot`, 276 ) 277 await initializeFromSnapshot( 278 agent.agentType, 279 agent.memory, 280 result.snapshotTimestamp!, 281 ) 282 break 283 case 'prompt-update': 284 agent.pendingSnapshotUpdate = { 285 snapshotTimestamp: result.snapshotTimestamp!, 286 } 287 logForDebugging( 288 `Newer snapshot available for ${agent.agentType} memory (snapshot: ${result.snapshotTimestamp})`, 289 ) 290 break 291 } 292 }), 293 ) 294} 295 296export const getAgentDefinitionsWithOverrides = memoize( 297 async (cwd: string): Promise<AgentDefinitionsResult> => { 298 // Simple mode: skip custom agents, only return built-ins 299 if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) { 300 const builtInAgents = getBuiltInAgents() 301 return { 302 activeAgents: builtInAgents, 303 allAgents: builtInAgents, 304 } 305 } 306 307 try { 308 const markdownFiles = await loadMarkdownFilesForSubdir('agents', cwd) 309 310 const failedFiles: Array<{ path: string; error: string }> = [] 311 const customAgents = markdownFiles 312 .map(({ filePath, baseDir, frontmatter, content, source }) => { 313 const agent = parseAgentFromMarkdown( 314 filePath, 315 baseDir, 316 frontmatter, 317 content, 318 source, 319 ) 320 if (!agent) { 321 // Skip non-agent markdown files silently (e.g., reference docs 322 // co-located with agent definitions). Only report errors for files 323 // that look like agent attempts (have a 'name' field in frontmatter). 324 if (!frontmatter['name']) { 325 return null 326 } 327 const errorMsg = getParseError(frontmatter) 328 failedFiles.push({ path: filePath, error: errorMsg }) 329 logForDebugging( 330 `Failed to parse agent from ${filePath}: ${errorMsg}`, 331 ) 332 logEvent('tengu_agent_parse_error', { 333 error: 334 errorMsg as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 335 location: 336 source as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 337 }) 338 return null 339 } 340 return agent 341 }) 342 .filter(agent => agent !== null) 343 344 // Kick off plugin agent loading concurrently with memory snapshot init — 345 // loadPluginAgents is memoized and takes no args, so it's independent. 346 // Join both so neither becomes a floating promise if the other throws. 347 let pluginAgentsPromise = loadPluginAgents() 348 if (feature('AGENT_MEMORY_SNAPSHOT') && isAutoMemoryEnabled()) { 349 const [pluginAgents_] = await Promise.all([ 350 pluginAgentsPromise, 351 initializeAgentMemorySnapshots(customAgents), 352 ]) 353 pluginAgentsPromise = Promise.resolve(pluginAgents_) 354 } 355 const pluginAgents = await pluginAgentsPromise 356 357 const builtInAgents = getBuiltInAgents() 358 359 const allAgentsList: AgentDefinition[] = [ 360 ...builtInAgents, 361 ...pluginAgents, 362 ...customAgents, 363 ] 364 365 const activeAgents = getActiveAgentsFromList(allAgentsList) 366 367 // Initialize colors for all active agents 368 for (const agent of activeAgents) { 369 if (agent.color) { 370 setAgentColor(agent.agentType, agent.color) 371 } 372 } 373 374 return { 375 activeAgents, 376 allAgents: allAgentsList, 377 failedFiles: failedFiles.length > 0 ? failedFiles : undefined, 378 } 379 } catch (error) { 380 const errorMessage = 381 error instanceof Error ? error.message : String(error) 382 logForDebugging(`Error loading agent definitions: ${errorMessage}`) 383 logError(error) 384 // Even on error, return the built-in agents 385 const builtInAgents = getBuiltInAgents() 386 return { 387 activeAgents: builtInAgents, 388 allAgents: builtInAgents, 389 failedFiles: [{ path: 'unknown', error: errorMessage }], 390 } 391 } 392 }, 393) 394 395export function clearAgentDefinitionsCache(): void { 396 getAgentDefinitionsWithOverrides.cache.clear?.() 397 clearPluginAgentCache() 398} 399 400/** 401 * Helper to determine the specific parsing error for an agent file 402 */ 403function getParseError(frontmatter: Record<string, unknown>): string { 404 const agentType = frontmatter['name'] 405 const description = frontmatter['description'] 406 407 if (!agentType || typeof agentType !== 'string') { 408 return 'Missing required "name" field in frontmatter' 409 } 410 411 if (!description || typeof description !== 'string') { 412 return 'Missing required "description" field in frontmatter' 413 } 414 415 return 'Unknown parsing error' 416} 417 418/** 419 * Parse hooks from frontmatter using the HooksSchema 420 * @param frontmatter The frontmatter object containing potential hooks 421 * @param agentType The agent type for logging purposes 422 * @returns Parsed hooks settings or undefined if invalid/missing 423 */ 424function parseHooksFromFrontmatter( 425 frontmatter: Record<string, unknown>, 426 agentType: string, 427): HooksSettings | undefined { 428 if (!frontmatter.hooks) { 429 return undefined 430 } 431 432 const result = HooksSchema().safeParse(frontmatter.hooks) 433 if (!result.success) { 434 logForDebugging( 435 `Invalid hooks in agent '${agentType}': ${result.error.message}`, 436 ) 437 return undefined 438 } 439 return result.data 440} 441 442/** 443 * Parses agent definition from JSON data 444 */ 445export function parseAgentFromJson( 446 name: string, 447 definition: unknown, 448 source: SettingSource = 'flagSettings', 449): CustomAgentDefinition | null { 450 try { 451 const parsed = AgentJsonSchema().parse(definition) 452 453 let tools = parseAgentToolsFromFrontmatter(parsed.tools) 454 455 // If memory is enabled, inject Write/Edit/Read tools for memory access 456 if (isAutoMemoryEnabled() && parsed.memory && tools !== undefined) { 457 const toolSet = new Set(tools) 458 for (const tool of [ 459 FILE_WRITE_TOOL_NAME, 460 FILE_EDIT_TOOL_NAME, 461 FILE_READ_TOOL_NAME, 462 ]) { 463 if (!toolSet.has(tool)) { 464 tools = [...tools, tool] 465 } 466 } 467 } 468 469 const disallowedTools = 470 parsed.disallowedTools !== undefined 471 ? parseAgentToolsFromFrontmatter(parsed.disallowedTools) 472 : undefined 473 474 const systemPrompt = parsed.prompt 475 476 const agent: CustomAgentDefinition = { 477 agentType: name, 478 whenToUse: parsed.description, 479 ...(tools !== undefined ? { tools } : {}), 480 ...(disallowedTools !== undefined ? { disallowedTools } : {}), 481 getSystemPrompt: () => { 482 if (isAutoMemoryEnabled() && parsed.memory) { 483 return ( 484 systemPrompt + '\n\n' + loadAgentMemoryPrompt(name, parsed.memory) 485 ) 486 } 487 return systemPrompt 488 }, 489 source, 490 ...(parsed.model ? { model: parsed.model } : {}), 491 ...(parsed.effort !== undefined ? { effort: parsed.effort } : {}), 492 ...(parsed.permissionMode 493 ? { permissionMode: parsed.permissionMode } 494 : {}), 495 ...(parsed.mcpServers && parsed.mcpServers.length > 0 496 ? { mcpServers: parsed.mcpServers } 497 : {}), 498 ...(parsed.hooks ? { hooks: parsed.hooks } : {}), 499 ...(parsed.maxTurns !== undefined ? { maxTurns: parsed.maxTurns } : {}), 500 ...(parsed.skills && parsed.skills.length > 0 501 ? { skills: parsed.skills } 502 : {}), 503 ...(parsed.initialPrompt ? { initialPrompt: parsed.initialPrompt } : {}), 504 ...(parsed.background ? { background: parsed.background } : {}), 505 ...(parsed.memory ? { memory: parsed.memory } : {}), 506 ...(parsed.isolation ? { isolation: parsed.isolation } : {}), 507 } 508 509 return agent 510 } catch (error) { 511 const errorMessage = error instanceof Error ? error.message : String(error) 512 logForDebugging(`Error parsing agent '${name}' from JSON: ${errorMessage}`) 513 logError(error) 514 return null 515 } 516} 517 518/** 519 * Parses multiple agents from a JSON object 520 */ 521export function parseAgentsFromJson( 522 agentsJson: unknown, 523 source: SettingSource = 'flagSettings', 524): AgentDefinition[] { 525 try { 526 const parsed = AgentsJsonSchema().parse(agentsJson) 527 return Object.entries(parsed) 528 .map(([name, def]) => parseAgentFromJson(name, def, source)) 529 .filter((agent): agent is CustomAgentDefinition => agent !== null) 530 } catch (error) { 531 const errorMessage = error instanceof Error ? error.message : String(error) 532 logForDebugging(`Error parsing agents from JSON: ${errorMessage}`) 533 logError(error) 534 return [] 535 } 536} 537 538/** 539 * Parses agent definition from markdown file data 540 */ 541export function parseAgentFromMarkdown( 542 filePath: string, 543 baseDir: string, 544 frontmatter: Record<string, unknown>, 545 content: string, 546 source: SettingSource, 547): CustomAgentDefinition | null { 548 try { 549 const agentType = frontmatter['name'] 550 let whenToUse = frontmatter['description'] as string 551 552 // Validate required fields — silently skip files without any agent 553 // frontmatter (they're likely co-located reference documentation) 554 if (!agentType || typeof agentType !== 'string') { 555 return null 556 } 557 if (!whenToUse || typeof whenToUse !== 'string') { 558 logForDebugging( 559 `Agent file ${filePath} is missing required 'description' in frontmatter`, 560 ) 561 return null 562 } 563 564 // Unescape newlines in whenToUse that were escaped for YAML parsing 565 whenToUse = whenToUse.replace(/\\n/g, '\n') 566 567 const color = frontmatter['color'] as AgentColorName | undefined 568 const modelRaw = frontmatter['model'] 569 let model: string | undefined 570 if (typeof modelRaw === 'string' && modelRaw.trim().length > 0) { 571 const trimmed = modelRaw.trim() 572 model = trimmed.toLowerCase() === 'inherit' ? 'inherit' : trimmed 573 } 574 575 // Parse background flag 576 const backgroundRaw = frontmatter['background'] 577 578 if ( 579 backgroundRaw !== undefined && 580 backgroundRaw !== 'true' && 581 backgroundRaw !== 'false' && 582 backgroundRaw !== true && 583 backgroundRaw !== false 584 ) { 585 logForDebugging( 586 `Agent file ${filePath} has invalid background value '${backgroundRaw}'. Must be 'true', 'false', or omitted.`, 587 ) 588 } 589 590 const background = 591 backgroundRaw === 'true' || backgroundRaw === true ? true : undefined 592 593 // Parse memory scope 594 const VALID_MEMORY_SCOPES: AgentMemoryScope[] = ['user', 'project', 'local'] 595 const memoryRaw = frontmatter['memory'] as string | undefined 596 let memory: AgentMemoryScope | undefined 597 if (memoryRaw !== undefined) { 598 if (VALID_MEMORY_SCOPES.includes(memoryRaw as AgentMemoryScope)) { 599 memory = memoryRaw as AgentMemoryScope 600 } else { 601 logForDebugging( 602 `Agent file ${filePath} has invalid memory value '${memoryRaw}'. Valid options: ${VALID_MEMORY_SCOPES.join(', ')}`, 603 ) 604 } 605 } 606 607 // Parse isolation mode. 'remote' is ant-only; external builds reject it at parse time. 608 type IsolationMode = 'worktree' | 'remote' 609 const VALID_ISOLATION_MODES: readonly IsolationMode[] = 610 process.env.USER_TYPE === 'ant' ? ['worktree', 'remote'] : ['worktree'] 611 const isolationRaw = frontmatter['isolation'] as string | undefined 612 let isolation: IsolationMode | undefined 613 if (isolationRaw !== undefined) { 614 if (VALID_ISOLATION_MODES.includes(isolationRaw as IsolationMode)) { 615 isolation = isolationRaw as IsolationMode 616 } else { 617 logForDebugging( 618 `Agent file ${filePath} has invalid isolation value '${isolationRaw}'. Valid options: ${VALID_ISOLATION_MODES.join(', ')}`, 619 ) 620 } 621 } 622 623 // Parse effort from frontmatter (supports string levels and integers) 624 const effortRaw = frontmatter['effort'] 625 const parsedEffort = 626 effortRaw !== undefined ? parseEffortValue(effortRaw) : undefined 627 628 if (effortRaw !== undefined && parsedEffort === undefined) { 629 logForDebugging( 630 `Agent file ${filePath} has invalid effort '${effortRaw}'. Valid options: ${EFFORT_LEVELS.join(', ')} or an integer`, 631 ) 632 } 633 634 // Parse permissionMode from frontmatter 635 const permissionModeRaw = frontmatter['permissionMode'] as 636 | string 637 | undefined 638 const isValidPermissionMode = 639 permissionModeRaw && 640 (PERMISSION_MODES as readonly string[]).includes(permissionModeRaw) 641 642 if (permissionModeRaw && !isValidPermissionMode) { 643 const errorMsg = `Agent file ${filePath} has invalid permissionMode '${permissionModeRaw}'. Valid options: ${PERMISSION_MODES.join(', ')}` 644 logForDebugging(errorMsg) 645 } 646 647 // Parse maxTurns from frontmatter 648 const maxTurnsRaw = frontmatter['maxTurns'] 649 const maxTurns = parsePositiveIntFromFrontmatter(maxTurnsRaw) 650 if (maxTurnsRaw !== undefined && maxTurns === undefined) { 651 logForDebugging( 652 `Agent file ${filePath} has invalid maxTurns '${maxTurnsRaw}'. Must be a positive integer.`, 653 ) 654 } 655 656 // Extract filename without extension 657 const filename = basename(filePath, '.md') 658 659 // Parse tools from frontmatter 660 let tools = parseAgentToolsFromFrontmatter(frontmatter['tools']) 661 662 // If memory is enabled, inject Write/Edit/Read tools for memory access 663 if (isAutoMemoryEnabled() && memory && tools !== undefined) { 664 const toolSet = new Set(tools) 665 for (const tool of [ 666 FILE_WRITE_TOOL_NAME, 667 FILE_EDIT_TOOL_NAME, 668 FILE_READ_TOOL_NAME, 669 ]) { 670 if (!toolSet.has(tool)) { 671 tools = [...tools, tool] 672 } 673 } 674 } 675 676 // Parse disallowedTools from frontmatter 677 const disallowedToolsRaw = frontmatter['disallowedTools'] 678 const disallowedTools = 679 disallowedToolsRaw !== undefined 680 ? parseAgentToolsFromFrontmatter(disallowedToolsRaw) 681 : undefined 682 683 // Parse skills from frontmatter 684 const skills = parseSlashCommandToolsFromFrontmatter(frontmatter['skills']) 685 686 const initialPromptRaw = frontmatter['initialPrompt'] 687 const initialPrompt = 688 typeof initialPromptRaw === 'string' && initialPromptRaw.trim() 689 ? initialPromptRaw 690 : undefined 691 692 // Parse mcpServers from frontmatter using same Zod validation as JSON agents 693 const mcpServersRaw = frontmatter['mcpServers'] 694 let mcpServers: AgentMcpServerSpec[] | undefined 695 if (Array.isArray(mcpServersRaw)) { 696 mcpServers = mcpServersRaw 697 .map(item => { 698 const result = AgentMcpServerSpecSchema().safeParse(item) 699 if (result.success) { 700 return result.data 701 } 702 logForDebugging( 703 `Agent file ${filePath} has invalid mcpServers item: ${jsonStringify(item)}. Error: ${result.error.message}`, 704 ) 705 return null 706 }) 707 .filter((item): item is AgentMcpServerSpec => item !== null) 708 } 709 710 // Parse hooks from frontmatter 711 const hooks = parseHooksFromFrontmatter(frontmatter, agentType) 712 713 const systemPrompt = content.trim() 714 const agentDef: CustomAgentDefinition = { 715 baseDir, 716 agentType: agentType, 717 whenToUse: whenToUse, 718 ...(tools !== undefined ? { tools } : {}), 719 ...(disallowedTools !== undefined ? { disallowedTools } : {}), 720 ...(skills !== undefined ? { skills } : {}), 721 ...(initialPrompt !== undefined ? { initialPrompt } : {}), 722 ...(mcpServers !== undefined && mcpServers.length > 0 723 ? { mcpServers } 724 : {}), 725 ...(hooks !== undefined ? { hooks } : {}), 726 getSystemPrompt: () => { 727 if (isAutoMemoryEnabled() && memory) { 728 const memoryPrompt = loadAgentMemoryPrompt(agentType, memory) 729 return systemPrompt + '\n\n' + memoryPrompt 730 } 731 return systemPrompt 732 }, 733 source, 734 filename, 735 ...(color && typeof color === 'string' && AGENT_COLORS.includes(color) 736 ? { color } 737 : {}), 738 ...(model !== undefined ? { model } : {}), 739 ...(parsedEffort !== undefined ? { effort: parsedEffort } : {}), 740 ...(isValidPermissionMode 741 ? { permissionMode: permissionModeRaw as PermissionMode } 742 : {}), 743 ...(maxTurns !== undefined ? { maxTurns } : {}), 744 ...(background ? { background } : {}), 745 ...(memory ? { memory } : {}), 746 ...(isolation ? { isolation } : {}), 747 } 748 return agentDef 749 } catch (error) { 750 const errorMessage = error instanceof Error ? error.message : String(error) 751 logForDebugging(`Error parsing agent from ${filePath}: ${errorMessage}`) 752 logError(error) 753 return null 754 } 755}