π File detail
tools/AgentTool/loadAgentsDir.ts
π― Use case
This module implements the βAgentToolβ tool (Agent) β something the model can call at runtime alongside other agent tools. On the API surface it exposes AgentMcpServerSpec, BaseAgentDefinition, BuiltInAgentDefinition, CustomAgentDefinition, and PluginAgentDefinition (and more) β mainly functions, hooks, or classes. Dependencies touch bun:bundle, lodash-es, Node path helpers, and src. It composes internal code from memdir, services, Tool, utils, and FileEditTool (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
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'
π€ Exports (heuristic)
AgentMcpServerSpecBaseAgentDefinitionBuiltInAgentDefinitionCustomAgentDefinitionPluginAgentDefinitionAgentDefinitionisBuiltInAgentisCustomAgentisPluginAgentAgentDefinitionsResultgetActiveAgentsFromListhasRequiredMcpServersfilterAgentsByMcpRequirementsgetAgentDefinitionsWithOverridesclearAgentDefinitionsCacheparseAgentFromJsonparseAgentsFromJsonparseAgentFromMarkdown
π External import roots
Package roots from from "β¦" (relative paths omitted).
bun:bundlelodash-espathsrczod
π₯οΈ Source preview
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
}
}