2025-12-17 22:46:55 -05:00
|
|
|
/**
|
|
|
|
|
* @license
|
2026-01-12 11:31:49 -05:00
|
|
|
* Copyright 2026 Google LLC
|
2025-12-17 22:46:55 -05:00
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
2026-02-21 13:33:25 -05:00
|
|
|
import { load } from 'js-yaml';
|
2025-12-17 22:46:55 -05:00
|
|
|
import * as fs from 'node:fs/promises';
|
|
|
|
|
import { type Dirent } from 'node:fs';
|
|
|
|
|
import * as path from 'node:path';
|
2026-01-26 19:49:32 +00:00
|
|
|
import * as crypto from 'node:crypto';
|
2025-12-17 22:46:55 -05:00
|
|
|
import { z } from 'zod';
|
2026-02-04 01:28:00 -05:00
|
|
|
import {
|
|
|
|
|
type AgentDefinition,
|
|
|
|
|
DEFAULT_MAX_TURNS,
|
|
|
|
|
DEFAULT_MAX_TIME_MINUTES,
|
|
|
|
|
} from './types.js';
|
2026-02-11 16:23:28 -05:00
|
|
|
import type { A2AAuthConfig } from './auth-provider/types.js';
|
2026-03-16 20:54:33 -07:00
|
|
|
import { MCPServerConfig } from '../config/config.js';
|
2026-01-23 02:18:31 +00:00
|
|
|
import { isValidToolName } from '../tools/tool-names.js';
|
2026-01-12 11:31:49 -05:00
|
|
|
import { FRONTMATTER_REGEX } from '../skills/skillLoader.js';
|
2026-01-26 19:49:32 +00:00
|
|
|
import { getErrorMessage } from '../utils/errors.js';
|
2025-12-17 22:46:55 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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[];
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 19:12:16 -05:00
|
|
|
const nameSchema = z
|
|
|
|
|
.string()
|
|
|
|
|
.regex(/^[a-z0-9-_]+$/, 'Name must be a valid slug');
|
|
|
|
|
|
2026-03-16 20:54:33 -07:00
|
|
|
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(),
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-29 19:12:16 -05:00
|
|
|
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(
|
2026-03-12 10:17:36 -04:00
|
|
|
z
|
|
|
|
|
.string()
|
|
|
|
|
.refine((val) => isValidToolName(val, { allowWildcards: true }), {
|
|
|
|
|
message: 'Invalid tool name',
|
|
|
|
|
}),
|
2025-12-29 19:12:16 -05:00
|
|
|
)
|
|
|
|
|
.optional(),
|
2026-03-16 20:54:33 -07:00
|
|
|
mcp_servers: z.record(mcpServerSchema).optional(),
|
2026-01-12 11:31:49 -05:00
|
|
|
model: z.string().optional(),
|
|
|
|
|
temperature: z.number().optional(),
|
|
|
|
|
max_turns: z.number().int().positive().optional(),
|
|
|
|
|
timeout_mins: z.number().int().positive().optional(),
|
2025-12-29 19:12:16 -05:00
|
|
|
})
|
|
|
|
|
.strict();
|
|
|
|
|
|
2026-03-24 18:04:28 -04:00
|
|
|
type FrontmatterLocalAgentDefinition = z.infer<typeof localAgentSchema> & {
|
|
|
|
|
system_prompt: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Base fields shared by all auth configs.
|
2026-03-10 15:16:46 -04:00
|
|
|
const baseAuthFields = {};
|
2026-02-11 16:23:28 -05:00
|
|
|
|
|
|
|
|
const apiKeyAuthSchema = z.object({
|
|
|
|
|
...baseAuthFields,
|
|
|
|
|
type: z.literal('apiKey'),
|
|
|
|
|
key: z.string().min(1, 'API key is required'),
|
|
|
|
|
name: z.string().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-20 13:55:36 -05:00
|
|
|
const httpAuthSchema = z.object({
|
2026-02-11 16:23:28 -05:00
|
|
|
...baseAuthFields,
|
|
|
|
|
type: z.literal('http'),
|
2026-03-02 11:59:48 -08:00
|
|
|
scheme: z.string().min(1),
|
2026-02-20 13:55:36 -05:00
|
|
|
token: z.string().min(1).optional(),
|
|
|
|
|
username: z.string().min(1).optional(),
|
|
|
|
|
password: z.string().min(1).optional(),
|
2026-03-02 11:59:48 -08:00
|
|
|
value: z.string().min(1).optional(),
|
2026-02-11 16:23:28 -05:00
|
|
|
});
|
|
|
|
|
|
2026-03-12 11:39:59 -04:00
|
|
|
const googleCredentialsAuthSchema = z.object({
|
|
|
|
|
...baseAuthFields,
|
|
|
|
|
type: z.literal('google-credentials'),
|
|
|
|
|
scopes: z.array(z.string()).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-10 08:24:44 -07:00
|
|
|
const oauth2AuthSchema = z.object({
|
|
|
|
|
...baseAuthFields,
|
2026-03-24 14:46:12 -04:00
|
|
|
type: z.literal('oauth'),
|
2026-03-10 08:24:44 -07:00
|
|
|
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(),
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-11 16:23:28 -05:00
|
|
|
const authConfigSchema = z
|
2026-03-10 08:24:44 -07:00
|
|
|
.discriminatedUnion('type', [
|
|
|
|
|
apiKeyAuthSchema,
|
|
|
|
|
httpAuthSchema,
|
2026-03-12 11:39:59 -04:00
|
|
|
googleCredentialsAuthSchema,
|
2026-03-10 08:24:44 -07:00
|
|
|
oauth2AuthSchema,
|
|
|
|
|
])
|
2026-02-11 16:23:28 -05:00
|
|
|
.superRefine((data, ctx) => {
|
|
|
|
|
if (data.type === 'http') {
|
2026-03-24 18:04:28 -04:00
|
|
|
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') {
|
2026-02-20 13:55:36 -05:00
|
|
|
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'],
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-24 18:04:28 -04:00
|
|
|
} else {
|
|
|
|
|
ctx.addIssue({
|
|
|
|
|
code: z.ZodIssueCode.custom,
|
|
|
|
|
message: `HTTP scheme "${data.scheme}" requires "value"`,
|
|
|
|
|
path: ['value'],
|
|
|
|
|
});
|
2026-02-11 16:23:28 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-24 18:04:28 -04:00
|
|
|
type FrontmatterAuthConfig = z.infer<typeof authConfigSchema>;
|
|
|
|
|
|
2025-12-29 19:12:16 -05:00
|
|
|
const remoteAgentSchema = z
|
|
|
|
|
.object({
|
|
|
|
|
kind: z.literal('remote').optional().default('remote'),
|
|
|
|
|
name: nameSchema,
|
|
|
|
|
description: z.string().optional(),
|
|
|
|
|
display_name: z.string().optional(),
|
|
|
|
|
agent_card_url: z.string().url(),
|
2026-02-11 16:23:28 -05:00
|
|
|
auth: authConfigSchema.optional(),
|
2025-12-29 19:12:16 -05:00
|
|
|
})
|
|
|
|
|
.strict();
|
|
|
|
|
|
2026-03-24 18:04:28 -04:00
|
|
|
type FrontmatterRemoteAgentDefinition = z.infer<typeof remoteAgentSchema>;
|
|
|
|
|
|
|
|
|
|
type FrontmatterAgentDefinition =
|
|
|
|
|
| FrontmatterLocalAgentDefinition
|
|
|
|
|
| FrontmatterRemoteAgentDefinition;
|
|
|
|
|
|
2025-12-29 19:12:16 -05:00
|
|
|
const agentUnionOptions = [
|
|
|
|
|
{ schema: localAgentSchema, label: 'Local Agent' },
|
|
|
|
|
{ schema: remoteAgentSchema, label: 'Remote Agent' },
|
|
|
|
|
] as const;
|
|
|
|
|
|
2026-01-12 11:31:49 -05:00
|
|
|
const remoteAgentsListSchema = z.array(remoteAgentSchema);
|
|
|
|
|
|
|
|
|
|
const markdownFrontmatterSchema = z.union([
|
2025-12-29 19:12:16 -05:00
|
|
|
agentUnionOptions[0].schema,
|
|
|
|
|
agentUnionOptions[1].schema,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-24 18:04:28 -04:00
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2025-12-29 19:12:16 -05:00
|
|
|
const issues = error.issues
|
|
|
|
|
.map((i) => {
|
|
|
|
|
if (i.code === z.ZodIssueCode.invalid_union) {
|
|
|
|
|
return i.unionErrors
|
|
|
|
|
.map((unionError, index) => {
|
|
|
|
|
const label =
|
|
|
|
|
agentUnionOptions[index]?.label ?? `Agent type #${index + 1}`;
|
2026-03-24 18:04:28 -04:00
|
|
|
|
|
|
|
|
if (intendedKind === 'local' && label === 'Remote Agent')
|
|
|
|
|
return null;
|
|
|
|
|
if (intendedKind === 'remote' && label === 'Local Agent')
|
|
|
|
|
return null;
|
|
|
|
|
|
2025-12-29 19:12:16 -05:00
|
|
|
const unionIssues = unionError.issues
|
2026-03-24 18:04:28 -04:00
|
|
|
.map((u) => {
|
|
|
|
|
const pathStr = u.path.join('.');
|
|
|
|
|
return pathStr ? `${pathStr}: ${u.message}` : u.message;
|
|
|
|
|
})
|
2025-12-29 19:12:16 -05:00
|
|
|
.join(', ');
|
|
|
|
|
return `(${label}) ${unionIssues}`;
|
|
|
|
|
})
|
2026-03-24 18:04:28 -04:00
|
|
|
.filter(Boolean)
|
2025-12-29 19:12:16 -05:00
|
|
|
.join('\n');
|
|
|
|
|
}
|
2026-03-24 18:04:28 -04:00
|
|
|
const pathStr = i.path.join('.');
|
|
|
|
|
return pathStr ? `${pathStr}: ${i.message}` : i.message;
|
2025-12-17 22:46:55 -05:00
|
|
|
})
|
2025-12-29 19:12:16 -05:00
|
|
|
.join('\n');
|
|
|
|
|
return `${context}:\n${issues}`;
|
|
|
|
|
}
|
2025-12-17 22:46:55 -05:00
|
|
|
|
|
|
|
|
/**
|
2026-01-12 11:31:49 -05:00
|
|
|
* Parses and validates an agent Markdown file with frontmatter.
|
2025-12-17 22:46:55 -05:00
|
|
|
*
|
2026-01-12 11:31:49 -05:00
|
|
|
* @param filePath Path to the Markdown file.
|
2026-01-26 19:49:32 +00:00
|
|
|
* @param content Optional pre-loaded content of the file.
|
2026-01-12 11:31:49 -05:00
|
|
|
* @returns An array containing the single parsed agent definition.
|
2025-12-17 22:46:55 -05:00
|
|
|
* @throws AgentLoadError if parsing or validation fails.
|
|
|
|
|
*/
|
2026-01-12 11:31:49 -05:00
|
|
|
export async function parseAgentMarkdown(
|
2025-12-17 22:46:55 -05:00
|
|
|
filePath: string,
|
2026-01-26 19:49:32 +00:00
|
|
|
content?: string,
|
2026-01-12 11:31:49 -05:00
|
|
|
): Promise<FrontmatterAgentDefinition[]> {
|
2026-01-26 19:49:32 +00:00
|
|
|
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)}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-12-17 22:46:55 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-12 11:31:49 -05:00
|
|
|
// Split frontmatter and body
|
2026-01-26 19:49:32 +00:00
|
|
|
const match = fileContent.match(FRONTMATTER_REGEX);
|
2026-01-12 11:31:49 -05:00
|
|
|
if (!match) {
|
|
|
|
|
throw new AgentLoadError(
|
|
|
|
|
filePath,
|
2026-01-13 09:44:52 -08:00
|
|
|
'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---).',
|
2026-01-12 11:31:49 -05:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const frontmatterStr = match[1];
|
|
|
|
|
const body = match[2] || '';
|
|
|
|
|
|
|
|
|
|
let rawFrontmatter: unknown;
|
2025-12-17 22:46:55 -05:00
|
|
|
try {
|
2026-02-21 13:33:25 -05:00
|
|
|
rawFrontmatter = load(frontmatterStr);
|
2025-12-17 22:46:55 -05:00
|
|
|
} catch (error) {
|
|
|
|
|
throw new AgentLoadError(
|
|
|
|
|
filePath,
|
2026-03-24 18:04:28 -04:00
|
|
|
`YAML frontmatter parsing failed: ${getErrorMessage(error)}`,
|
2025-12-17 22:46:55 -05:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-12 11:31:49 -05:00
|
|
|
// Handle array of remote agents
|
|
|
|
|
if (Array.isArray(rawFrontmatter)) {
|
|
|
|
|
const result = remoteAgentsListSchema.safeParse(rawFrontmatter);
|
2025-12-29 19:12:16 -05:00
|
|
|
if (!result.success) {
|
|
|
|
|
throw new AgentLoadError(
|
|
|
|
|
filePath,
|
2026-01-12 11:31:49 -05:00
|
|
|
`Validation failed: ${formatZodError(result.error, 'Remote Agents List')}`,
|
2025-12-29 19:12:16 -05:00
|
|
|
);
|
|
|
|
|
}
|
2026-01-12 11:31:49 -05:00
|
|
|
return result.data.map((agent) => ({
|
|
|
|
|
...agent,
|
|
|
|
|
kind: 'remote',
|
|
|
|
|
}));
|
2025-12-29 19:12:16 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-12 11:31:49 -05:00
|
|
|
const result = markdownFrontmatterSchema.safeParse(rawFrontmatter);
|
2025-12-29 19:12:16 -05:00
|
|
|
|
2025-12-17 22:46:55 -05:00
|
|
|
if (!result.success) {
|
2025-12-29 19:12:16 -05:00
|
|
|
throw new AgentLoadError(
|
|
|
|
|
filePath,
|
2026-03-24 18:04:28 -04:00
|
|
|
`Validation failed: ${formatZodError(result.error, 'Agent Definition', rawFrontmatter)}`,
|
2025-12-29 19:12:16 -05:00
|
|
|
);
|
2025-12-17 22:46:55 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-12 11:31:49 -05:00
|
|
|
const frontmatter = result.data;
|
2025-12-17 22:46:55 -05:00
|
|
|
|
2026-01-12 11:31:49 -05:00
|
|
|
if (frontmatter.kind === 'remote') {
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
...frontmatter,
|
|
|
|
|
kind: 'remote',
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Construct the local agent definition
|
2026-03-24 18:04:28 -04:00
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
...frontmatter,
|
|
|
|
|
kind: 'local',
|
|
|
|
|
system_prompt: body.trim(),
|
|
|
|
|
},
|
|
|
|
|
];
|
2025-12-17 22:46:55 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-11 16:23:28 -05:00
|
|
|
/**
|
|
|
|
|
* 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,
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-12 11:39:59 -04:00
|
|
|
case 'google-credentials':
|
|
|
|
|
return {
|
|
|
|
|
type: 'google-credentials',
|
|
|
|
|
scopes: frontmatter.scopes,
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-24 18:04:28 -04:00
|
|
|
case 'http':
|
2026-03-02 11:59:48 -08:00
|
|
|
if (frontmatter.value) {
|
|
|
|
|
return {
|
|
|
|
|
type: 'http',
|
|
|
|
|
scheme: frontmatter.scheme,
|
|
|
|
|
value: frontmatter.value,
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-02-11 16:23:28 -05:00
|
|
|
switch (frontmatter.scheme) {
|
|
|
|
|
case 'Bearer':
|
2026-03-24 18:04:28 -04:00
|
|
|
// Token is required by schema validation
|
2026-02-11 16:23:28 -05:00
|
|
|
return {
|
|
|
|
|
type: 'http',
|
|
|
|
|
scheme: 'Bearer',
|
2026-03-24 18:04:28 -04:00
|
|
|
|
|
|
|
|
token: frontmatter.token!,
|
2026-02-11 16:23:28 -05:00
|
|
|
};
|
|
|
|
|
case 'Basic':
|
2026-03-24 18:04:28 -04:00
|
|
|
// Username/password are required by schema validation
|
2026-02-11 16:23:28 -05:00
|
|
|
return {
|
|
|
|
|
type: 'http',
|
|
|
|
|
scheme: 'Basic',
|
2026-03-24 18:04:28 -04:00
|
|
|
|
|
|
|
|
username: frontmatter.username!,
|
|
|
|
|
|
|
|
|
|
password: frontmatter.password!,
|
2026-02-11 16:23:28 -05:00
|
|
|
};
|
2026-03-24 18:04:28 -04:00
|
|
|
default:
|
2026-03-02 11:59:48 -08:00
|
|
|
throw new Error(`Unknown HTTP scheme: ${frontmatter.scheme}`);
|
2026-02-11 16:23:28 -05:00
|
|
|
}
|
|
|
|
|
|
2026-03-24 14:46:12 -04:00
|
|
|
case 'oauth':
|
2026-03-10 08:24:44 -07:00
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-11 16:23:28 -05:00
|
|
|
default: {
|
2026-03-24 18:04:28 -04:00
|
|
|
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');
|
2026-02-11 16:23:28 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 22:46:55 -05:00
|
|
|
/**
|
2026-01-12 11:31:49 -05:00
|
|
|
* Converts a FrontmatterAgentDefinition DTO to the internal AgentDefinition structure.
|
2025-12-17 22:46:55 -05:00
|
|
|
*
|
2026-01-12 11:31:49 -05:00
|
|
|
* @param markdown The parsed Markdown/Frontmatter definition.
|
2026-01-26 19:49:32 +00:00
|
|
|
* @param metadata Optional metadata including hash and file path.
|
2025-12-17 22:46:55 -05:00
|
|
|
* @returns The internal AgentDefinition.
|
|
|
|
|
*/
|
2026-01-12 11:31:49 -05:00
|
|
|
export function markdownToAgentDefinition(
|
|
|
|
|
markdown: FrontmatterAgentDefinition,
|
2026-01-26 19:49:32 +00:00
|
|
|
metadata?: { hash?: string; filePath?: string },
|
2025-12-17 22:46:55 -05:00
|
|
|
): AgentDefinition {
|
2025-12-29 19:12:16 -05:00
|
|
|
const inputConfig = {
|
2026-01-21 16:56:01 -08:00
|
|
|
inputSchema: {
|
|
|
|
|
type: 'object',
|
|
|
|
|
properties: {
|
|
|
|
|
query: {
|
|
|
|
|
type: 'string',
|
|
|
|
|
description: 'The task for the agent.',
|
|
|
|
|
},
|
2025-12-29 19:12:16 -05:00
|
|
|
},
|
2026-01-21 16:56:01 -08:00
|
|
|
// query is not required because it defaults to "Get Started!" if not provided
|
|
|
|
|
required: [],
|
2025-12-29 19:12:16 -05:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-12 11:31:49 -05:00
|
|
|
if (markdown.kind === 'remote') {
|
2025-12-29 19:12:16 -05:00
|
|
|
return {
|
|
|
|
|
kind: 'remote',
|
2026-01-12 11:31:49 -05:00
|
|
|
name: markdown.name,
|
2026-03-02 12:59:29 -05:00
|
|
|
description: markdown.description || '',
|
2026-01-12 11:31:49 -05:00
|
|
|
displayName: markdown.display_name,
|
|
|
|
|
agentCardUrl: markdown.agent_card_url,
|
2026-02-11 16:23:28 -05:00
|
|
|
auth: markdown.auth
|
|
|
|
|
? convertFrontmatterAuthToConfig(markdown.auth)
|
|
|
|
|
: undefined,
|
2025-12-29 19:12:16 -05:00
|
|
|
inputConfig,
|
2026-01-26 19:49:32 +00:00
|
|
|
metadata,
|
2025-12-29 19:12:16 -05:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 22:46:55 -05:00
|
|
|
// If a model is specified, use it. Otherwise, inherit
|
2026-01-12 11:31:49 -05:00
|
|
|
const modelName = markdown.model || 'inherit';
|
2025-12-17 22:46:55 -05:00
|
|
|
|
2026-03-16 20:54:33 -07:00
|
|
|
const mcpServers: Record<string, MCPServerConfig> = {};
|
2026-03-24 18:04:28 -04:00
|
|
|
if (markdown.mcp_servers) {
|
2026-03-16 20:54:33 -07:00
|
|
|
for (const [name, config] of Object.entries(markdown.mcp_servers)) {
|
|
|
|
|
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,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 22:46:55 -05:00
|
|
|
return {
|
|
|
|
|
kind: 'local',
|
2026-01-12 11:31:49 -05:00
|
|
|
name: markdown.name,
|
|
|
|
|
description: markdown.description,
|
|
|
|
|
displayName: markdown.display_name,
|
2025-12-17 22:46:55 -05:00
|
|
|
promptConfig: {
|
2026-01-12 11:31:49 -05:00
|
|
|
systemPrompt: markdown.system_prompt,
|
|
|
|
|
query: '${query}',
|
2025-12-17 22:46:55 -05:00
|
|
|
},
|
|
|
|
|
modelConfig: {
|
|
|
|
|
model: modelName,
|
2026-01-13 14:31:34 -08:00
|
|
|
generateContentConfig: {
|
|
|
|
|
temperature: markdown.temperature ?? 1,
|
|
|
|
|
topP: 0.95,
|
|
|
|
|
},
|
2025-12-17 22:46:55 -05:00
|
|
|
},
|
|
|
|
|
runConfig: {
|
2026-02-04 01:28:00 -05:00
|
|
|
maxTurns: markdown.max_turns ?? DEFAULT_MAX_TURNS,
|
|
|
|
|
maxTimeMinutes: markdown.timeout_mins ?? DEFAULT_MAX_TIME_MINUTES,
|
2025-12-17 22:46:55 -05:00
|
|
|
},
|
2026-01-12 11:31:49 -05:00
|
|
|
toolConfig: markdown.tools
|
2025-12-17 22:46:55 -05:00
|
|
|
? {
|
2026-01-12 11:31:49 -05:00
|
|
|
tools: markdown.tools,
|
2025-12-17 22:46:55 -05:00
|
|
|
}
|
|
|
|
|
: undefined,
|
2026-03-16 20:54:33 -07:00
|
|
|
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined,
|
2025-12-29 19:12:16 -05:00
|
|
|
inputConfig,
|
2026-01-26 19:49:32 +00:00
|
|
|
metadata,
|
2025-12-17 22:46:55 -05:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Loads all agents from a specific directory.
|
2026-01-12 11:31:49 -05:00
|
|
|
* Ignores files starting with _ and non-supported extensions.
|
|
|
|
|
* Supported extensions: .md
|
2025-12-17 22:46:55 -05:00
|
|
|
*
|
|
|
|
|
* @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
|
2026-03-24 18:04:28 -04:00
|
|
|
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
2025-12-17 22:46:55 -05:00
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
result.errors.push(
|
|
|
|
|
new AgentLoadError(
|
|
|
|
|
dir,
|
2026-03-24 18:04:28 -04:00
|
|
|
`Could not list directory: ${getErrorMessage(error)}`,
|
2025-12-17 22:46:55 -05:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-12 11:31:49 -05:00
|
|
|
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);
|
2025-12-17 22:46:55 -05:00
|
|
|
try {
|
2026-01-26 19:49:32 +00:00
|
|
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
|
|
|
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
|
|
|
const agentDefs = await parseAgentMarkdown(filePath, content);
|
2026-01-12 11:31:49 -05:00
|
|
|
for (const def of agentDefs) {
|
2026-01-26 19:49:32 +00:00
|
|
|
const agent = markdownToAgentDefinition(def, { hash, filePath });
|
2025-12-29 19:12:16 -05:00
|
|
|
result.agents.push(agent);
|
|
|
|
|
}
|
2025-12-17 22:46:55 -05:00
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof AgentLoadError) {
|
|
|
|
|
result.errors.push(error);
|
|
|
|
|
} else {
|
|
|
|
|
result.errors.push(
|
|
|
|
|
new AgentLoadError(
|
|
|
|
|
filePath,
|
2026-03-24 18:04:28 -04:00
|
|
|
`Unexpected error: ${getErrorMessage(error)}`,
|
2025-12-17 22:46:55 -05:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|