Download packages/core/src/agents/agentLoader.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 19.7 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/agents/agentLoader.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/core/src/agents/agentLoader.ts
-
curl -L -o agentLoader.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/agents/agentLoader.ts
19.7 kB
| /** | |
| * @license | |
| * Copyright 2026 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { load } from 'js-yaml'; | |
| import * as fs from 'node:fs/promises'; | |
| import { type Dirent } from 'node:fs'; | |
| import * as path from 'node:path'; | |
| import * as crypto from 'node:crypto'; | |
| import { z } from 'zod'; | |
| import { | |
| type AgentDefinition, | |
| type RemoteAgentDefinition, | |
| DEFAULT_MAX_TURNS, | |
| DEFAULT_MAX_TIME_MINUTES, | |
| } from './types.js'; | |
| import type { A2AAuthConfig } from './auth-provider/types.js'; | |
| import { | |
| MCPServerConfig, | |
| AuthProviderType, | |
| type MCPOAuthConfig, | |
| } from '../config/config.js'; | |
| import { isValidToolName } from '../tools/tool-names.js'; | |
| import { FRONTMATTER_REGEX } from '../skills/skillLoader.js'; | |
| import { getErrorMessage } from '../utils/errors.js'; | |
| /** | |
| * Error thrown when an agent definition is invalid or cannot be loaded. | |
| */ | |
| export class AgentLoadError extends Error { | |
| constructor( | |
| public filePath: string, | |
| message: string, | |
| ) { | |
| super(`Failed to load agent from ${filePath}: ${message}`); | |
| this.name = 'AgentLoadError'; | |
| } | |
| } | |
| /** | |
| * Result of loading agents from a directory. | |
| */ | |
| export interface AgentLoadResult { | |
| agents: AgentDefinition[]; | |
| errors: AgentLoadError[]; | |
| } | |
| const nameSchema = z | |
| .string() | |
| .regex(/^[a-z0-9-_]+$/, 'Name must be a valid slug'); | |
| const mcpServerSchema = z.object({ | |
| command: z.string().optional(), | |
| args: z.array(z.string()).optional(), | |
| env: z.record(z.string()).optional(), | |
| cwd: z.string().optional(), | |
| url: z.string().optional(), | |
| http_url: z.string().optional(), | |
| headers: z.record(z.string()).optional(), | |
| tcp: z.string().optional(), | |
| type: z.enum(['sse', 'http']).optional(), | |
| timeout: z.number().optional(), | |
| trust: z.boolean().optional(), | |
| description: z.string().optional(), | |
| include_tools: z.array(z.string()).optional(), | |
| exclude_tools: z.array(z.string()).optional(), | |
| auth: z | |
| .union([ | |
| z.object({ | |
| type: z.literal('google-credentials'), | |
| scopes: z.array(z.string()).optional(), | |
| }), | |
| z.object({ | |
| type: z.literal('oauth'), | |
| client_id: z.string().optional(), | |
| client_secret: z.string().optional(), | |
| scopes: z.array(z.string()).optional(), | |
| authorization_url: z.string().url().optional(), | |
| token_url: z.string().url().optional(), | |
| issuer: z.string().url().optional(), | |
| audiences: z.array(z.string()).optional(), | |
| redirect_uri: z.string().url().optional(), | |
| token_param_name: z.string().optional(), | |
| registration_url: z.string().url().optional(), | |
| }), | |
| ]) | |
| .optional(), | |
| }); | |
| const localAgentSchema = z | |
| .object({ | |
| kind: z.literal('local').optional().default('local'), | |
| name: nameSchema, | |
| description: z.string().min(1), | |
| display_name: z.string().optional(), | |
| tools: z | |
| .array( | |
| z | |
| .string() | |
| .refine( | |
| (val: string) => isValidToolName(val, { allowWildcards: true }), | |
| { | |
| message: 'Invalid tool name', | |
| }, | |
| ), | |
| ) | |
| .optional(), | |
| mcp_servers: z.record(mcpServerSchema).optional(), | |
| model: z.string().optional(), | |
| temperature: z.number().optional(), | |
| max_turns: z.number().int().positive().optional(), | |
| timeout_mins: z.number().int().positive().optional(), | |
| }) | |
| .strict(); | |
| type FrontmatterLocalAgentDefinition = z.infer<typeof localAgentSchema> & { | |
| system_prompt: string; | |
| }; | |
| // Base fields shared by all auth configs. | |
| const baseAuthFields = {}; | |
| const apiKeyAuthSchema = z.object({ | |
| ...baseAuthFields, | |
| type: z.literal('apiKey'), | |
| key: z.string().min(1, 'API key is required'), | |
| name: z.string().optional(), | |
| }); | |
| const httpAuthSchema = z.object({ | |
| ...baseAuthFields, | |
| type: z.literal('http'), | |
| scheme: z.string().min(1), | |
| token: z.string().min(1).optional(), | |
| username: z.string().min(1).optional(), | |
| password: z.string().min(1).optional(), | |
| value: z.string().min(1).optional(), | |
| }); | |
| const googleCredentialsAuthSchema = z.object({ | |
| ...baseAuthFields, | |
| type: z.literal('google-credentials'), | |
| scopes: z.array(z.string()).optional(), | |
| }); | |
| const oauth2AuthSchema = z.object({ | |
| ...baseAuthFields, | |
| type: z.literal('oauth'), | |
| client_id: z.string().optional(), | |
| client_secret: z.string().optional(), | |
| scopes: z.array(z.string()).optional(), | |
| authorization_url: z.string().url().optional(), | |
| token_url: z.string().url().optional(), | |
| issuer: z.string().url().optional(), | |
| audiences: z.array(z.string()).optional(), | |
| redirect_uri: z.string().url().optional(), | |
| token_param_name: z.string().optional(), | |
| registration_url: z.string().url().optional(), | |
| }); | |
| const authConfigSchema = z | |
| .discriminatedUnion('type', [ | |
| apiKeyAuthSchema, | |
| httpAuthSchema, | |
| googleCredentialsAuthSchema, | |
| oauth2AuthSchema, | |
| ]) | |
| .superRefine((data, ctx) => { | |
| if (data.type === 'http') { | |
| if (data.value) return; | |
| if (data.scheme === 'Bearer') { | |
| if (!data.token) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: 'Bearer scheme requires "token"', | |
| path: ['token'], | |
| }); | |
| } | |
| } else if (data.scheme === 'Basic') { | |
| if (!data.username) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: 'Basic authentication requires "username"', | |
| path: ['username'], | |
| }); | |
| } | |
| if (!data.password) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: 'Basic authentication requires "password"', | |
| path: ['password'], | |
| }); | |
| } | |
| } else { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: `HTTP scheme "${data.scheme}" requires "value"`, | |
| path: ['value'], | |
| }); | |
| } | |
| } | |
| }); | |
| type FrontmatterAuthConfig = z.infer<typeof authConfigSchema>; | |
| const baseRemoteAgentSchema = z.object({ | |
| kind: z.literal('remote').optional().default('remote'), | |
| name: nameSchema, | |
| description: z.string().optional(), | |
| display_name: z.string().optional(), | |
| auth: authConfigSchema.optional(), | |
| }); | |
| const remoteAgentUrlSchema = baseRemoteAgentSchema | |
| .extend({ | |
| agent_card_url: z.string().url(), | |
| agent_card_json: z.undefined().optional(), | |
| }) | |
| .strict(); | |
| const remoteAgentJsonSchema = baseRemoteAgentSchema | |
| .extend({ | |
| agent_card_url: z.undefined().optional(), | |
| agent_card_json: z.string().refine( | |
| (val: string) => { | |
| try { | |
| JSON.parse(val); | |
| return true; | |
| } catch { | |
| return false; | |
| } | |
| }, | |
| { message: 'agent_card_json must be valid JSON' }, | |
| ), | |
| }) | |
| .strict(); | |
| const remoteAgentSchema = z.union([ | |
| remoteAgentUrlSchema, | |
| remoteAgentJsonSchema, | |
| ]); | |
| type FrontmatterRemoteAgentDefinition = z.infer<typeof remoteAgentSchema>; | |
| type FrontmatterAgentDefinition = | |
| | FrontmatterLocalAgentDefinition | |
| | FrontmatterRemoteAgentDefinition; | |
| const agentUnionOptions = [ | |
| { label: 'Local Agent' }, | |
| { label: 'Remote Agent' }, | |
| { label: 'Remote Agent' }, | |
| ]; | |
| const remoteAgentsListSchema = z.array(remoteAgentSchema); | |
| const markdownFrontmatterSchema = z.union([ | |
| localAgentSchema, | |
| remoteAgentUrlSchema, | |
| remoteAgentJsonSchema, | |
| ]); | |
| function guessIntendedKind(rawInput: unknown): 'local' | 'remote' | undefined { | |
| if (typeof rawInput !== 'object' || rawInput === null) return undefined; | |
| const input = rawInput as Partial<FrontmatterLocalAgentDefinition> & | |
| Partial<FrontmatterRemoteAgentDefinition>; | |
| if (input.kind === 'local') return 'local'; | |
| if (input.kind === 'remote') return 'remote'; | |
| const hasLocalKeys = | |
| 'tools' in input || | |
| 'mcp_servers' in input || | |
| 'model' in input || | |
| 'temperature' in input || | |
| 'max_turns' in input || | |
| 'timeout_mins' in input; | |
| const hasRemoteKeys = | |
| 'agent_card_url' in input || 'auth' in input || 'agent_card_json' in input; | |
| if (hasLocalKeys && !hasRemoteKeys) return 'local'; | |
| if (hasRemoteKeys && !hasLocalKeys) return 'remote'; | |
| return undefined; | |
| } | |
| function formatZodError( | |
| error: z.ZodError, | |
| context: string, | |
| rawInput?: unknown, | |
| ): string { | |
| const intendedKind = rawInput ? guessIntendedKind(rawInput) : undefined; | |
| const formatIssues = (issues: z.ZodIssue[], unionPrefix?: string): string[] => | |
| issues.flatMap((i) => { | |
| // Handle union errors specifically to give better context | |
| if (i.code === z.ZodIssueCode.invalid_union) { | |
| return i.unionErrors.flatMap((unionError, index) => { | |
| const label = unionPrefix | |
| ? unionPrefix | |
| : ((agentUnionOptions[index] as { label?: string })?.label ?? | |
| `Branch #${index + 1}`); | |
| if (intendedKind === 'local' && label === 'Remote Agent') return []; | |
| if (intendedKind === 'remote' && label === 'Local Agent') return []; | |
| return formatIssues(unionError.issues, label); | |
| }); | |
| } | |
| const prefix = unionPrefix ? `(${unionPrefix}) ` : ''; | |
| const path = i.path.length > 0 ? `${i.path.join('.')}: ` : ''; | |
| return `${prefix}${path}${i.message}`; | |
| }); | |
| const formatted = Array.from(new Set(formatIssues(error.issues))).join('\n'); | |
| return `${context}:\n${formatted}`; | |
| } | |
| /** | |
| * Parses and validates an agent Markdown file with frontmatter. | |
| * | |
| * @param filePath Path to the Markdown file. | |
| * @param content Optional pre-loaded content of the file. | |
| * @returns An array containing the single parsed agent definition. | |
| * @throws AgentLoadError if parsing or validation fails. | |
| */ | |
| export async function parseAgentMarkdown( | |
| filePath: string, | |
| content?: string, | |
| ): Promise<FrontmatterAgentDefinition[]> { | |
| let fileContent: string; | |
| if (content !== undefined) { | |
| fileContent = content; | |
| } else { | |
| try { | |
| fileContent = await fs.readFile(filePath, 'utf-8'); | |
| } catch (error) { | |
| throw new AgentLoadError( | |
| filePath, | |
| `Could not read file: ${getErrorMessage(error)}`, | |
| ); | |
| } | |
| } | |
| // Split frontmatter and body | |
| const match = fileContent.match(FRONTMATTER_REGEX); | |
| if (!match) { | |
| throw new AgentLoadError( | |
| filePath, | |
| 'Invalid agent definition: Missing mandatory YAML frontmatter. Agent Markdown files MUST start with YAML frontmatter enclosed in triple-dashes "---" (e.g., ---\nname: my-agent\n---).', | |
| ); | |
| } | |
| const frontmatterStr = match[1]; | |
| const body = match[2] || ''; | |
| let rawFrontmatter: unknown; | |
| try { | |
| rawFrontmatter = load(frontmatterStr); | |
| } catch (error) { | |
| throw new AgentLoadError( | |
| filePath, | |
| `YAML frontmatter parsing failed: ${getErrorMessage(error)}`, | |
| ); | |
| } | |
| // Handle array of remote agents | |
| if (Array.isArray(rawFrontmatter)) { | |
| const result = remoteAgentsListSchema.safeParse(rawFrontmatter); | |
| if (!result.success) { | |
| throw new AgentLoadError( | |
| filePath, | |
| `Validation failed: ${formatZodError(result.error, 'Remote Agents List')}`, | |
| ); | |
| } | |
| return result.data.map((agent) => ({ | |
| ...agent, | |
| kind: 'remote', | |
| })); | |
| } | |
| const result = markdownFrontmatterSchema.safeParse(rawFrontmatter); | |
| if (!result.success) { | |
| throw new AgentLoadError( | |
| filePath, | |
| `Validation failed: ${formatZodError(result.error, 'Agent Definition', rawFrontmatter)}`, | |
| ); | |
| } | |
| const frontmatter = result.data; | |
| if (frontmatter.kind === 'remote') { | |
| return [ | |
| { | |
| ...frontmatter, | |
| kind: 'remote', | |
| }, | |
| ]; | |
| } | |
| // Construct the local agent definition | |
| return [ | |
| { | |
| ...frontmatter, | |
| kind: 'local', | |
| system_prompt: body.trim(), | |
| }, | |
| ]; | |
| } | |
| /** | |
| * Converts frontmatter auth config to the internal A2AAuthConfig type. | |
| * This handles the mapping from snake_case YAML to the internal type structure. | |
| */ | |
| function convertFrontmatterAuthToConfig( | |
| frontmatter: FrontmatterAuthConfig, | |
| ): A2AAuthConfig { | |
| switch (frontmatter.type) { | |
| case 'apiKey': | |
| return { | |
| type: 'apiKey', | |
| key: frontmatter.key, | |
| name: frontmatter.name, | |
| }; | |
| case 'google-credentials': | |
| return { | |
| type: 'google-credentials', | |
| scopes: frontmatter.scopes, | |
| }; | |
| case 'http': | |
| if (frontmatter.value) { | |
| return { | |
| type: 'http', | |
| scheme: frontmatter.scheme, | |
| value: frontmatter.value, | |
| }; | |
| } | |
| switch (frontmatter.scheme) { | |
| case 'Bearer': | |
| // Token is required by schema validation | |
| return { | |
| type: 'http', | |
| scheme: 'Bearer', | |
| token: frontmatter.token!, | |
| }; | |
| case 'Basic': | |
| // Username/password are required by schema validation | |
| return { | |
| type: 'http', | |
| scheme: 'Basic', | |
| username: frontmatter.username!, | |
| password: frontmatter.password!, | |
| }; | |
| default: | |
| throw new Error(`Unknown HTTP scheme: ${frontmatter.scheme}`); | |
| } | |
| case 'oauth': | |
| return { | |
| type: 'oauth2', | |
| client_id: frontmatter.client_id, | |
| client_secret: frontmatter.client_secret, | |
| scopes: frontmatter.scopes, | |
| authorization_url: frontmatter.authorization_url, | |
| token_url: frontmatter.token_url, | |
| issuer: frontmatter.issuer, | |
| audiences: frontmatter.audiences, | |
| redirect_uri: frontmatter.redirect_uri, | |
| token_param_name: frontmatter.token_param_name, | |
| registration_url: frontmatter.registration_url, | |
| }; | |
| default: { | |
| const exhaustive: never = frontmatter; | |
| const raw: unknown = exhaustive; | |
| if (typeof raw === 'object' && raw !== null && 'type' in raw) { | |
| throw new Error(`Unknown auth type: ${String(raw['type'])}`); | |
| } | |
| throw new Error('Unknown auth type'); | |
| } | |
| } | |
| } | |
| /** | |
| * Converts a FrontmatterAgentDefinition DTO to the internal AgentDefinition structure. | |
| * | |
| * @param markdown The parsed Markdown/Frontmatter definition. | |
| * @param metadata Optional metadata including hash and file path. | |
| * @returns The internal AgentDefinition. | |
| */ | |
| export function markdownToAgentDefinition( | |
| markdown: FrontmatterAgentDefinition, | |
| metadata?: { hash?: string; filePath?: string }, | |
| ): AgentDefinition { | |
| const inputConfig = { | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| query: { | |
| type: 'string', | |
| description: 'The task for the agent.', | |
| }, | |
| }, | |
| // query is not required because it defaults to "Get Started!" if not provided | |
| required: [], | |
| }, | |
| }; | |
| if (markdown.kind === 'remote') { | |
| const base: RemoteAgentDefinition = { | |
| kind: 'remote', | |
| name: markdown.name, | |
| description: markdown.description || '', | |
| displayName: markdown.display_name, | |
| auth: markdown.auth | |
| ? convertFrontmatterAuthToConfig(markdown.auth) | |
| : undefined, | |
| inputConfig, | |
| metadata, | |
| }; | |
| if ( | |
| 'agent_card_json' in markdown && | |
| markdown.agent_card_json !== undefined | |
| ) { | |
| base.agentCardJson = markdown.agent_card_json; | |
| return base; | |
| } | |
| if ('agent_card_url' in markdown && markdown.agent_card_url !== undefined) { | |
| base.agentCardUrl = markdown.agent_card_url; | |
| return base; | |
| } | |
| throw new AgentLoadError( | |
| metadata?.filePath || 'unknown', | |
| 'Unexpected state: neither agent_card_json nor agent_card_url present on remote agent', | |
| ); | |
| } | |
| // If a model is specified, use it. Otherwise, inherit | |
| const modelName = markdown.model || 'inherit'; | |
| const mcpServers: Record<string, MCPServerConfig> = {}; | |
| if (markdown.mcp_servers) { | |
| for (const [name, config] of Object.entries(markdown.mcp_servers)) { | |
| let authProviderType: AuthProviderType | undefined = undefined; | |
| let oauth: MCPOAuthConfig | undefined = undefined; | |
| if (config.auth) { | |
| if (config.auth.type === 'google-credentials') { | |
| authProviderType = AuthProviderType.GOOGLE_CREDENTIALS; | |
| oauth = { | |
| enabled: true, | |
| scopes: config.auth.scopes, | |
| }; | |
| } else if (config.auth.type === 'oauth') { | |
| oauth = { | |
| enabled: true, | |
| clientId: config.auth.client_id, | |
| clientSecret: config.auth.client_secret, | |
| scopes: config.auth.scopes, | |
| authorizationUrl: config.auth.authorization_url, | |
| tokenUrl: config.auth.token_url, | |
| issuer: config.auth.issuer, | |
| audiences: config.auth.audiences, | |
| redirectUri: config.auth.redirect_uri, | |
| tokenParamName: config.auth.token_param_name, | |
| registrationUrl: config.auth.registration_url, | |
| }; | |
| } | |
| } | |
| mcpServers[name] = new MCPServerConfig( | |
| config.command, | |
| config.args, | |
| config.env, | |
| config.cwd, | |
| config.url, | |
| config.http_url, | |
| config.headers, | |
| config.tcp, | |
| config.type, | |
| config.timeout, | |
| config.trust, | |
| config.description, | |
| config.include_tools, | |
| config.exclude_tools, | |
| undefined, // extension | |
| oauth, | |
| authProviderType, | |
| ); | |
| } | |
| } | |
| return { | |
| kind: 'local', | |
| name: markdown.name, | |
| description: markdown.description, | |
| displayName: markdown.display_name, | |
| promptConfig: { | |
| systemPrompt: markdown.system_prompt, | |
| query: '${query}', | |
| }, | |
| modelConfig: { | |
| model: modelName, | |
| generateContentConfig: { | |
| temperature: markdown.temperature ?? 1, | |
| topP: 0.95, | |
| }, | |
| }, | |
| runConfig: { | |
| maxTurns: markdown.max_turns ?? DEFAULT_MAX_TURNS, | |
| maxTimeMinutes: markdown.timeout_mins ?? DEFAULT_MAX_TIME_MINUTES, | |
| }, | |
| toolConfig: markdown.tools | |
| ? { | |
| tools: markdown.tools, | |
| } | |
| : undefined, | |
| mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, | |
| inputConfig, | |
| metadata, | |
| }; | |
| } | |
| /** | |
| * Loads all agents from a specific directory. | |
| * Ignores files starting with _ and non-supported extensions. | |
| * Supported extensions: .md | |
| * | |
| * @param dir Directory path to scan. | |
| * @returns Object containing successfully loaded agents and any errors. | |
| */ | |
| export async function loadAgentsFromDirectory( | |
| dir: string, | |
| ): Promise<AgentLoadResult> { | |
| const result: AgentLoadResult = { | |
| agents: [], | |
| errors: [], | |
| }; | |
| let dirEntries: Dirent[]; | |
| try { | |
| dirEntries = await fs.readdir(dir, { withFileTypes: true }); | |
| } catch (error) { | |
| // If directory doesn't exist, just return empty | |
| if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { | |
| return result; | |
| } | |
| result.errors.push( | |
| new AgentLoadError( | |
| dir, | |
| `Could not list directory: ${getErrorMessage(error)}`, | |
| ), | |
| ); | |
| return result; | |
| } | |
| const files = dirEntries.filter( | |
| (entry) => | |
| entry.isFile() && | |
| !entry.name.startsWith('_') && | |
| entry.name.endsWith('.md'), | |
| ); | |
| for (const entry of files) { | |
| const filePath = path.join(dir, entry.name); | |
| try { | |
| const content = await fs.readFile(filePath, 'utf-8'); | |
| const hash = crypto.createHash('sha256').update(content).digest('hex'); | |
| const agentDefs = await parseAgentMarkdown(filePath, content); | |
| for (const def of agentDefs) { | |
| const agent = markdownToAgentDefinition(def, { hash, filePath }); | |
| result.agents.push(agent); | |
| } | |
| } catch (error) { | |
| if (error instanceof AgentLoadError) { | |
| result.errors.push(error); | |
| } else { | |
| result.errors.push( | |
| new AgentLoadError( | |
| filePath, | |
| `Unexpected error: ${getErrorMessage(error)}`, | |
| ), | |
| ); | |
| } | |
| } | |
| } | |
| return result; | |
| } | |