From 6273f882ca8e1ebdb11236fe73ccfbb62a4a0bec Mon Sep 17 00:00:00 2001 From: Sehoon Shon Date: Wed, 7 Jan 2026 13:34:50 -0500 Subject: [PATCH 1/6] feat(core): add remote agent support to markdown loader - Add FrontmatterRemoteAgentDefinition and Zod schema for remote agents - Support 'kind: remote' and 'agent_card_url' in markdown frontmatter - Allow optional body for remote agents in parser - Update loader tests to cover remote agent scenarios --- packages/core/src/agents/loader.test.ts | 333 ++++++++++++++++ .../src/agents/{toml-loader.ts => loader.ts} | 187 +++++---- packages/core/src/agents/registry.test.ts | 4 +- packages/core/src/agents/registry.ts | 2 +- packages/core/src/agents/toml-loader.test.ts | 373 ------------------ 5 files changed, 437 insertions(+), 462 deletions(-) create mode 100644 packages/core/src/agents/loader.test.ts rename packages/core/src/agents/{toml-loader.ts => loader.ts} (62%) delete mode 100644 packages/core/src/agents/toml-loader.test.ts diff --git a/packages/core/src/agents/loader.test.ts b/packages/core/src/agents/loader.test.ts new file mode 100644 index 00000000000..b942609c9fd --- /dev/null +++ b/packages/core/src/agents/loader.test.ts @@ -0,0 +1,333 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import * as os from 'node:os'; +import { + parseAgentMarkdown, + markdownToAgentDefinition, + loadAgentsFromDirectory, + AgentLoadError, +} from './loader.js'; +import { GEMINI_MODEL_ALIAS_PRO } from '../config/models.js'; +import type { LocalAgentDefinition } from './types.js'; + +describe('loader', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-test-')); + }); + + afterEach(async () => { + if (tempDir) { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + async function writeAgentMarkdown(content: string, fileName = 'test.md') { + const filePath = path.join(tempDir, fileName); + await fs.writeFile(filePath, content); + return filePath; + } + + describe('parseAgentMarkdown', () => { + it('should parse a valid markdown agent file', async () => { + const filePath = await writeAgentMarkdown(`--- +name: test-agent-md +description: A markdown agent +--- +You are a markdown agent.`); + + const result = await parseAgentMarkdown(filePath); + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + name: 'test-agent-md', + description: 'A markdown agent', + kind: 'local', + prompts: { + system_prompt: 'You are a markdown agent.', + }, + }); + }); + + it('should parse frontmatter with tools and model config', async () => { + const filePath = await writeAgentMarkdown(`--- +name: complex-agent +description: A complex markdown agent +tools: + - run_shell_command +model: + model: gemini-pro + temperature: 0.7 +--- +System prompt content.`); + + const result = await parseAgentMarkdown(filePath); + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + name: 'complex-agent', + description: 'A complex markdown agent', + tools: ['run_shell_command'], + model: { + model: 'gemini-pro', + temperature: 0.7, + }, + prompts: { + system_prompt: 'System prompt content.', + }, + }); + }); + + it('should throw AgentLoadError if frontmatter is missing', async () => { + const filePath = await writeAgentMarkdown(`Just some markdown content.`); + await expect(parseAgentMarkdown(filePath)).rejects.toThrow( + AgentLoadError, + ); + await expect(parseAgentMarkdown(filePath)).rejects.toThrow( + 'Invalid markdown format', + ); + }); + + it('should throw AgentLoadError if frontmatter is invalid YAML', async () => { + const filePath = await writeAgentMarkdown(`--- +name: [invalid yaml +--- +Body`); + await expect(parseAgentMarkdown(filePath)).rejects.toThrow( + AgentLoadError, + ); + await expect(parseAgentMarkdown(filePath)).rejects.toThrow( + 'YAML frontmatter parsing failed', + ); + }); + + it('should throw AgentLoadError if validation fails (missing required field)', async () => { + const filePath = await writeAgentMarkdown(`--- +name: test-agent +# missing description +--- +Body`); + await expect(parseAgentMarkdown(filePath)).rejects.toThrow( + /Validation failed/, + ); + }); + + it('should throw AgentLoadError if tools list includes forbidden tool', async () => { + const filePath = await writeAgentMarkdown(`--- +name: test-agent +description: Test +tools: + - delegate_to_agent +--- +Body`); + await expect(parseAgentMarkdown(filePath)).rejects.toThrow( + /tools list cannot include 'delegate_to_agent'/, + ); + }); + + it('should parse a valid remote agent markdown file', async () => { + const filePath = await writeAgentMarkdown(`--- +kind: remote +name: remote-agent +description: A remote agent +agent_card_url: https://example.com/card +--- +`); + const result = await parseAgentMarkdown(filePath); + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + kind: 'remote', + name: 'remote-agent', + description: 'A remote agent', + agent_card_url: 'https://example.com/card', + }); + }); + + it('should infer remote agent kind from agent_card_url', async () => { + const filePath = await writeAgentMarkdown(`--- +name: inferred-remote +description: Inferred +agent_card_url: https://example.com/inferred +--- +`); + const result = await parseAgentMarkdown(filePath); + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + kind: 'remote', + name: 'inferred-remote', + description: 'Inferred', + agent_card_url: 'https://example.com/inferred', + }); + }); + + it('should parse a remote agent with no body', async () => { + const filePath = await writeAgentMarkdown(`--- +kind: remote +name: no-body-remote +agent_card_url: https://example.com/card +---`); + const result = await parseAgentMarkdown(filePath); + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + kind: 'remote', + name: 'no-body-remote', + agent_card_url: 'https://example.com/card', + }); + }); + }); + + describe('markdownToAgentDefinition', () => { + it('should convert valid Markdown DTO to AgentDefinition with defaults', () => { + const markdown = { + kind: 'local' as const, + name: 'test-agent', + description: 'A test agent', + prompts: { + system_prompt: 'You are a test agent.', + }, + }; + + const result = markdownToAgentDefinition(markdown); + expect(result).toMatchObject({ + name: 'test-agent', + description: 'A test agent', + promptConfig: { + systemPrompt: 'You are a test agent.', + }, + modelConfig: { + model: 'inherit', + top_p: 0.95, + }, + runConfig: { + max_time_minutes: 5, + }, + inputConfig: { + inputs: { + query: { + type: 'string', + required: false, + }, + }, + }, + }); + }); + + it('should pass through model aliases', () => { + const markdown = { + kind: 'local' as const, + name: 'test-agent', + description: 'A test agent', + model: { + model: GEMINI_MODEL_ALIAS_PRO, + }, + prompts: { + system_prompt: 'You are a test agent.', + }, + }; + + const result = markdownToAgentDefinition( + markdown, + ) as LocalAgentDefinition; + expect(result.modelConfig.model).toBe(GEMINI_MODEL_ALIAS_PRO); + }); + + it('should pass through unknown model names (e.g. auto)', () => { + const markdown = { + kind: 'local' as const, + name: 'test-agent', + description: 'A test agent', + model: { + model: 'auto', + }, + prompts: { + system_prompt: 'You are a test agent.', + }, + }; + + const result = markdownToAgentDefinition( + markdown, + ) as LocalAgentDefinition; + expect(result.modelConfig.model).toBe('auto'); + }); + + it('should convert remote agent definition', () => { + const markdown = { + kind: 'remote' as const, + name: 'remote-agent', + description: 'A remote agent', + agent_card_url: 'https://example.com/card', + }; + + const result = markdownToAgentDefinition(markdown); + expect(result).toEqual({ + kind: 'remote', + name: 'remote-agent', + description: 'A remote agent', + displayName: undefined, + agentCardUrl: 'https://example.com/card', + inputConfig: { + inputs: { + query: { + type: 'string', + description: 'The task for the agent.', + required: false, + }, + }, + }, + }); + }); + }); + + describe('loadAgentsFromDirectory', () => { + it('should load definitions from a directory (Markdown only)', async () => { + await writeAgentMarkdown( + `--- +name: agent-1 +description: Agent 1 +--- +Prompt 1`, + 'valid.md', + ); + + // Create a non-supported file + await fs.writeFile(path.join(tempDir, 'other.txt'), 'content'); + + // Create a hidden file + await writeAgentMarkdown( + `--- +name: hidden +description: Hidden +--- +Hidden`, + '_hidden.md', + ); + + const result = await loadAgentsFromDirectory(tempDir); + expect(result.agents).toHaveLength(1); + expect(result.agents[0].name).toBe('agent-1'); + expect(result.errors).toHaveLength(0); + }); + + it('should return empty result if directory does not exist', async () => { + const nonExistentDir = path.join(tempDir, 'does-not-exist'); + const result = await loadAgentsFromDirectory(nonExistentDir); + expect(result.agents).toHaveLength(0); + expect(result.errors).toHaveLength(0); + }); + + it('should capture errors for malformed individual files', async () => { + // Create a malformed Markdown file + await writeAgentMarkdown('invalid markdown', 'malformed.md'); + + const result = await loadAgentsFromDirectory(tempDir); + expect(result.agents).toHaveLength(0); + expect(result.errors).toHaveLength(1); + }); + }); +}); diff --git a/packages/core/src/agents/toml-loader.ts b/packages/core/src/agents/loader.ts similarity index 62% rename from packages/core/src/agents/toml-loader.ts rename to packages/core/src/agents/loader.ts index 28ab2207d69..751df2c7c7f 100644 --- a/packages/core/src/agents/toml-loader.ts +++ b/packages/core/src/agents/loader.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import TOML from '@iarna/toml'; +import yaml from 'js-yaml'; import * as fs from 'node:fs/promises'; import { type Dirent } from 'node:fs'; import * as path from 'node:path'; @@ -16,14 +16,15 @@ import { } from '../tools/tool-names.js'; /** - * DTO for TOML parsing - represents the raw structure of the TOML file. + * DTO for Markdown parsing - represents the structure from frontmatter. */ -interface TomlBaseAgentDefinition { +interface FrontmatterBaseAgentDefinition { name: string; display_name?: string; } -interface TomlLocalAgentDefinition extends TomlBaseAgentDefinition { +interface FrontmatterLocalAgentDefinition + extends FrontmatterBaseAgentDefinition { kind: 'local'; description: string; tools?: string[]; @@ -41,13 +42,16 @@ interface TomlLocalAgentDefinition extends TomlBaseAgentDefinition { }; } -interface TomlRemoteAgentDefinition extends TomlBaseAgentDefinition { - description?: string; +interface FrontmatterRemoteAgentDefinition + extends FrontmatterBaseAgentDefinition { kind: 'remote'; + description?: string; agent_card_url: string; } -type TomlAgentDefinition = TomlLocalAgentDefinition | TomlRemoteAgentDefinition; +type FrontmatterAgentDefinition = + | FrontmatterLocalAgentDefinition + | FrontmatterRemoteAgentDefinition; /** * Error thrown when an agent definition is invalid or cannot be loaded. @@ -87,10 +91,11 @@ const localAgentSchema = z }), ) .optional(), - prompts: z.object({ - system_prompt: z.string().min(1), - query: z.string().optional(), - }), + prompts: z + .object({ + query: z.string().optional(), + }) + .optional(), model: z .object({ model: z.string().optional(), @@ -116,22 +121,14 @@ const remoteAgentSchema = z }) .strict(); -const remoteAgentsConfigSchema = z - .object({ - remote_agents: z.array(remoteAgentSchema), - }) - .strict(); - // Use a Zod union to automatically discriminate between local and remote -// agent types. This is more robust than manually checking the 'kind' field, -// as it correctly handles cases where 'kind' is omitted by relying on -// the presence of unique fields like `agent_card_url` or `prompts`. +// agent types. const agentUnionOptions = [ { schema: localAgentSchema, label: 'Local Agent' }, { schema: remoteAgentSchema, label: 'Remote Agent' }, ] as const; -const singleAgentSchema = z.union([ +const markdownFrontmatterSchema = z.union([ agentUnionOptions[0].schema, agentUnionOptions[1].schema, ]); @@ -159,15 +156,15 @@ function formatZodError(error: z.ZodError, context: string): string { } /** - * Parses and validates an agent TOML file. Returns a validated array of RemoteAgentDefinitions or a single LocalAgentDefinition. + * Parses and validates an agent Markdown file with frontmatter. * - * @param filePath Path to the TOML file. - * @returns An array of parsed and validated TomlAgentDefinitions. + * @param filePath Path to the Markdown file. + * @returns An array containing the single parsed agent definition. * @throws AgentLoadError if parsing or validation fails. */ -export async function parseAgentToml( +export async function parseAgentMarkdown( filePath: string, -): Promise { +): Promise { let content: string; try { content = await fs.readFile(filePath, 'utf-8'); @@ -178,34 +175,30 @@ export async function parseAgentToml( ); } - let raw: unknown; - try { - raw = TOML.parse(content); - } catch (error) { + // Split frontmatter and body + // Matches "---", newline, content, newline, "---", optional newline, optional rest + const match = content.match(/^---\n([\s\S]*?)\n---(?:\n([\s\S]*))?$/); + if (!match) { throw new AgentLoadError( filePath, - `TOML parsing failed: ${(error as Error).message}`, + 'Invalid markdown format. File must start with YAML frontmatter enclosed in "---".', ); } - // Check for `remote_agents` array - if ( - typeof raw === 'object' && - raw !== null && - 'remote_agents' in (raw as Record) - ) { - const result = remoteAgentsConfigSchema.safeParse(raw); - if (!result.success) { - throw new AgentLoadError( - filePath, - `Validation failed: ${formatZodError(result.error, 'Remote Agents Config')}`, - ); - } - return result.data.remote_agents as TomlAgentDefinition[]; + const frontmatterStr = match[1]; + const body = match[2] || ''; + + let rawFrontmatter: unknown; + try { + rawFrontmatter = yaml.load(frontmatterStr); + } catch (error) { + throw new AgentLoadError( + filePath, + `YAML frontmatter parsing failed: ${(error as Error).message}`, + ); } - // Single Agent Logic - const result = singleAgentSchema.safeParse(raw); + const result = markdownFrontmatterSchema.safeParse(rawFrontmatter); if (!result.success) { throw new AgentLoadError( @@ -214,27 +207,50 @@ export async function parseAgentToml( ); } - const toml = result.data as TomlAgentDefinition; + const frontmatter = result.data; + + if (frontmatter.kind === 'remote') { + return [ + { + ...frontmatter, + kind: 'remote', + }, + ]; + } - // Prevent sub-agents from delegating to other agents (to prevent recursion/complexity) - if ('tools' in toml && toml.tools?.includes(DELEGATE_TO_AGENT_TOOL_NAME)) { + // Local agent validation + // Validate tools + if ( + frontmatter.tools && + frontmatter.tools.includes(DELEGATE_TO_AGENT_TOOL_NAME) + ) { throw new AgentLoadError( filePath, `Validation failed: tools list cannot include '${DELEGATE_TO_AGENT_TOOL_NAME}'. Sub-agents cannot delegate to other agents.`, ); } - return [toml]; + // Construct the local agent definition + const agentDef: FrontmatterLocalAgentDefinition = { + ...frontmatter, + kind: 'local', + prompts: { + system_prompt: body.trim(), + query: frontmatter.prompts?.query, + }, + }; + + return [agentDef]; } /** - * Converts a TomlAgentDefinition DTO to the internal AgentDefinition structure. + * Converts a FrontmatterAgentDefinition DTO to the internal AgentDefinition structure. * - * @param toml The parsed TOML definition. + * @param markdown The parsed Markdown/Frontmatter definition. * @returns The internal AgentDefinition. */ -export function tomlToAgentDefinition( - toml: TomlAgentDefinition, +export function markdownToAgentDefinition( + markdown: FrontmatterAgentDefinition, ): AgentDefinition { const inputConfig = { inputs: { @@ -246,41 +262,41 @@ export function tomlToAgentDefinition( }, }; - if (toml.kind === 'remote') { + if (markdown.kind === 'remote') { return { kind: 'remote', - name: toml.name, - description: toml.description || '(Loading description...)', - displayName: toml.display_name, - agentCardUrl: toml.agent_card_url, + name: markdown.name, + description: markdown.description || '(Loading description...)', + displayName: markdown.display_name, + agentCardUrl: markdown.agent_card_url, inputConfig, }; } // If a model is specified, use it. Otherwise, inherit - const modelName = toml.model?.model || 'inherit'; + const modelName = markdown.model?.model || 'inherit'; return { kind: 'local', - name: toml.name, - description: toml.description, - displayName: toml.display_name, + name: markdown.name, + description: markdown.description, + displayName: markdown.display_name, promptConfig: { - systemPrompt: toml.prompts.system_prompt, - query: toml.prompts.query, + systemPrompt: markdown.prompts.system_prompt, + query: markdown.prompts.query, }, modelConfig: { model: modelName, - temp: toml.model?.temperature ?? 1, + temp: markdown.model?.temperature ?? 1, top_p: 0.95, }, runConfig: { - max_turns: toml.run?.max_turns, - max_time_minutes: toml.run?.timeout_mins || 5, + max_turns: markdown.run?.max_turns, + max_time_minutes: markdown.run?.timeout_mins || 5, }, - toolConfig: toml.tools + toolConfig: markdown.tools ? { - tools: toml.tools, + tools: markdown.tools, } : undefined, inputConfig, @@ -289,7 +305,8 @@ export function tomlToAgentDefinition( /** * Loads all agents from a specific directory. - * Ignores non-TOML files and files starting with _. + * 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. @@ -319,21 +336,19 @@ export async function loadAgentsFromDirectory( return result; } - const files = dirEntries - .filter( - (entry) => - entry.isFile() && - entry.name.endsWith('.toml') && - !entry.name.startsWith('_'), - ) - .map((entry) => entry.name); - - for (const file of files) { - const filePath = path.join(dir, file); + 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 tomls = await parseAgentToml(filePath); - for (const toml of tomls) { - const agent = tomlToAgentDefinition(toml); + const agentDefs = await parseAgentMarkdown(filePath); + for (const def of agentDefs) { + const agent = markdownToAgentDefinition(def); result.agents.push(agent); } } catch (error) { diff --git a/packages/core/src/agents/registry.test.ts b/packages/core/src/agents/registry.test.ts index f369e59b218..6ad15fa153f 100644 --- a/packages/core/src/agents/registry.test.ts +++ b/packages/core/src/agents/registry.test.ts @@ -19,9 +19,9 @@ import { PREVIEW_GEMINI_MODEL, PREVIEW_GEMINI_MODEL_AUTO, } from '../config/models.js'; -import * as tomlLoader from './toml-loader.js'; +import * as tomlLoader from './loader.js'; -vi.mock('./toml-loader.js', () => ({ +vi.mock('./loader.js', () => ({ loadAgentsFromDirectory: vi .fn() .mockResolvedValue({ agents: [], errors: [] }), diff --git a/packages/core/src/agents/registry.ts b/packages/core/src/agents/registry.ts index 38b28ffcc7e..174c2de74c3 100644 --- a/packages/core/src/agents/registry.ts +++ b/packages/core/src/agents/registry.ts @@ -8,7 +8,7 @@ import { Storage } from '../config/storage.js'; import { coreEvents, CoreEvent } from '../utils/events.js'; import type { Config } from '../config/config.js'; import type { AgentDefinition } from './types.js'; -import { loadAgentsFromDirectory } from './toml-loader.js'; +import { loadAgentsFromDirectory } from './loader.js'; import { CodebaseInvestigatorAgent } from './codebase-investigator.js'; import { IntrospectionAgent } from './introspection-agent.js'; import { A2AClientManager } from './a2a-client-manager.js'; diff --git a/packages/core/src/agents/toml-loader.test.ts b/packages/core/src/agents/toml-loader.test.ts deleted file mode 100644 index 5e91887cd8c..00000000000 --- a/packages/core/src/agents/toml-loader.test.ts +++ /dev/null @@ -1,373 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import * as fs from 'node:fs/promises'; -import * as path from 'node:path'; -import * as os from 'node:os'; -import { - parseAgentToml, - tomlToAgentDefinition, - loadAgentsFromDirectory, - AgentLoadError, -} from './toml-loader.js'; -import { GEMINI_MODEL_ALIAS_PRO } from '../config/models.js'; -import type { LocalAgentDefinition } from './types.js'; - -describe('toml-loader', () => { - let tempDir: string; - - beforeEach(async () => { - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-test-')); - }); - - afterEach(async () => { - if (tempDir) { - await fs.rm(tempDir, { recursive: true, force: true }); - } - }); - - async function writeAgentToml(content: string, fileName = 'test.toml') { - const filePath = path.join(tempDir, fileName); - await fs.writeFile(filePath, content); - return filePath; - } - - describe('parseAgentToml', () => { - it('should parse a valid MVA TOML file', async () => { - const filePath = await writeAgentToml(` - name = "test-agent" - description = "A test agent" - [prompts] - system_prompt = "You are a test agent." - `); - - const result = await parseAgentToml(filePath); - expect(result).toHaveLength(1); - expect(result[0]).toMatchObject({ - name: 'test-agent', - description: 'A test agent', - prompts: { - system_prompt: 'You are a test agent.', - }, - }); - }); - - it('should parse a valid remote agent TOML file', async () => { - const filePath = await writeAgentToml(` - kind = "remote" - name = "remote-agent" - description = "A remote agent" - agent_card_url = "https://example.com/card" - `); - - const result = await parseAgentToml(filePath); - expect(result).toHaveLength(1); - expect(result[0]).toEqual({ - kind: 'remote', - name: 'remote-agent', - description: 'A remote agent', - agent_card_url: 'https://example.com/card', - }); - }); - - it('should infer remote agent kind from agent_card_url', async () => { - const filePath = await writeAgentToml(` - name = "inferred-remote" - description = "Inferred" - agent_card_url = "https://example.com/inferred" - `); - - const result = await parseAgentToml(filePath); - expect(result).toHaveLength(1); - expect(result[0]).toEqual({ - kind: 'remote', - name: 'inferred-remote', - description: 'Inferred', - agent_card_url: 'https://example.com/inferred', - }); - }); - - it('should parse a remote agent without description', async () => { - const filePath = await writeAgentToml(` - kind = "remote" - name = "no-description-remote" - agent_card_url = "https://example.com/card" - `); - - const result = await parseAgentToml(filePath); - expect(result).toHaveLength(1); - expect(result[0]).toEqual({ - kind: 'remote', - name: 'no-description-remote', - agent_card_url: 'https://example.com/card', - }); - expect(result[0].description).toBeUndefined(); - - // defined after conversion to AgentDefinition - const agentDef = tomlToAgentDefinition(result[0]); - expect(agentDef.description).toBe('(Loading description...)'); - }); - - it('should parse multiple agents in one file', async () => { - const filePath = await writeAgentToml(` - [[remote_agents]] - kind = "remote" - name = "agent-1" - description = "Remote 1" - agent_card_url = "https://example.com/1" - - [[remote_agents]] - kind = "remote" - name = "agent-2" - description = "Remote 2" - agent_card_url = "https://example.com/2" - `); - - const result = await parseAgentToml(filePath); - expect(result).toHaveLength(2); - expect(result[0].name).toBe('agent-1'); - expect(result[0].kind).toBe('remote'); - expect(result[1].name).toBe('agent-2'); - expect(result[1].kind).toBe('remote'); - }); - - it('should allow omitting kind in remote_agents block', async () => { - const filePath = await writeAgentToml(` - [[remote_agents]] - name = "implicit-remote-1" - agent_card_url = "https://example.com/1" - - [[remote_agents]] - name = "implicit-remote-2" - agent_card_url = "https://example.com/2" - `); - - const result = await parseAgentToml(filePath); - expect(result).toHaveLength(2); - expect(result[0]).toMatchObject({ - kind: 'remote', - name: 'implicit-remote-1', - agent_card_url: 'https://example.com/1', - }); - expect(result[1]).toMatchObject({ - kind: 'remote', - name: 'implicit-remote-2', - agent_card_url: 'https://example.com/2', - }); - }); - - it('should throw AgentLoadError if file reading fails', async () => { - const filePath = path.join(tempDir, 'non-existent.toml'); - await expect(parseAgentToml(filePath)).rejects.toThrow(AgentLoadError); - }); - - it('should throw AgentLoadError if TOML parsing fails', async () => { - const filePath = await writeAgentToml('invalid toml ['); - await expect(parseAgentToml(filePath)).rejects.toThrow(AgentLoadError); - }); - - it('should throw AgentLoadError if validation fails (missing required field)', async () => { - const filePath = await writeAgentToml(` - name = "test-agent" - # missing description - [prompts] - system_prompt = "You are a test agent." - `); - await expect(parseAgentToml(filePath)).rejects.toThrow( - /Validation failed/, - ); - }); - - it('should throw AgentLoadError if name is not a slug', async () => { - const filePath = await writeAgentToml(` - name = "Test Agent!" - description = "A test agent" - [prompts] - system_prompt = "You are a test agent." - `); - await expect(parseAgentToml(filePath)).rejects.toThrow( - /Name must be a valid slug/, - ); - }); - - it('should throw AgentLoadError if delegate_to_agent is included in tools', async () => { - const filePath = await writeAgentToml(` - name = "test-agent" - description = "A test agent" - tools = ["run_shell_command", "delegate_to_agent"] - [prompts] - system_prompt = "You are a test agent." - `); - - await expect(parseAgentToml(filePath)).rejects.toThrow( - /tools list cannot include 'delegate_to_agent'/, - ); - }); - - it('should throw AgentLoadError if tools contains invalid names', async () => { - const filePath = await writeAgentToml(` - name = "test-agent" - description = "A test agent" - tools = ["not-a-tool"] - [prompts] - system_prompt = "You are a test agent." - `); - await expect(parseAgentToml(filePath)).rejects.toThrow( - /Validation failed:[\s\S]*tools.0: Invalid tool name/, - ); - }); - - it('should throw AgentLoadError if file contains both single and multiple agents', async () => { - const filePath = await writeAgentToml(` - name = "top-level-agent" - description = "I should not be here" - [prompts] - system_prompt = "..." - - [[remote_agents]] - kind = "remote" - name = "array-agent" - description = "I am in an array" - agent_card_url = "https://example.com/card" - `); - - await expect(parseAgentToml(filePath)).rejects.toThrow( - /Validation failed/, - ); - }); - - it('should show both options in error message when validation fails ambiguously', async () => { - const filePath = await writeAgentToml(` - name = "ambiguous-agent" - description = "I have neither prompts nor card" - `); - await expect(parseAgentToml(filePath)).rejects.toThrow( - /Validation failed: Agent Definition:\n\(Local Agent\) prompts: Required\n\(Remote Agent\) agent_card_url: Required/, - ); - }); - }); - - describe('tomlToAgentDefinition', () => { - it('should convert valid TOML to AgentDefinition with defaults', () => { - const toml = { - kind: 'local' as const, - name: 'test-agent', - description: 'A test agent', - prompts: { - system_prompt: 'You are a test agent.', - }, - }; - - const result = tomlToAgentDefinition(toml); - expect(result).toMatchObject({ - name: 'test-agent', - description: 'A test agent', - promptConfig: { - systemPrompt: 'You are a test agent.', - }, - modelConfig: { - model: 'inherit', - top_p: 0.95, - }, - runConfig: { - max_time_minutes: 5, - }, - inputConfig: { - inputs: { - query: { - type: 'string', - required: false, - }, - }, - }, - }); - }); - - it('should pass through model aliases', () => { - const toml = { - kind: 'local' as const, - name: 'test-agent', - description: 'A test agent', - model: { - model: GEMINI_MODEL_ALIAS_PRO, - }, - prompts: { - system_prompt: 'You are a test agent.', - }, - }; - - const result = tomlToAgentDefinition(toml) as LocalAgentDefinition; - expect(result.modelConfig.model).toBe(GEMINI_MODEL_ALIAS_PRO); - }); - - it('should pass through unknown model names (e.g. auto)', () => { - const toml = { - kind: 'local' as const, - name: 'test-agent', - description: 'A test agent', - model: { - model: 'auto', - }, - prompts: { - system_prompt: 'You are a test agent.', - }, - }; - - const result = tomlToAgentDefinition(toml) as LocalAgentDefinition; - expect(result.modelConfig.model).toBe('auto'); - }); - }); - - describe('loadAgentsFromDirectory', () => { - it('should load definitions from a directory', async () => { - await writeAgentToml( - ` - name = "agent-1" - description = "Agent 1" - [prompts] - system_prompt = "Prompt 1" - `, - 'valid.toml', - ); - - // Create a non-TOML file - await fs.writeFile(path.join(tempDir, 'other.txt'), 'content'); - - // Create a hidden file - await writeAgentToml( - ` - name = "hidden" - description = "Hidden" - [prompts] - system_prompt = "Hidden" - `, - '_hidden.toml', - ); - - const result = await loadAgentsFromDirectory(tempDir); - expect(result.agents).toHaveLength(1); - expect(result.agents[0].name).toBe('agent-1'); - expect(result.errors).toHaveLength(0); - }); - - it('should return empty result if directory does not exist', async () => { - const nonExistentDir = path.join(tempDir, 'does-not-exist'); - const result = await loadAgentsFromDirectory(nonExistentDir); - expect(result.agents).toHaveLength(0); - expect(result.errors).toHaveLength(0); - }); - - it('should capture errors for malformed individual files', async () => { - // Create a malformed TOML file - await writeAgentToml('invalid toml [', 'malformed.toml'); - - const result = await loadAgentsFromDirectory(tempDir); - expect(result.agents).toHaveLength(0); - expect(result.errors).toHaveLength(1); - }); - }); -}); From cbb36ddb64788276971c5b1e59d2b38d6a2812b4 Mon Sep 17 00:00:00 2001 From: Sehoon Shon Date: Wed, 7 Jan 2026 15:18:16 -0500 Subject: [PATCH 2/6] refactor(core): flatten agent schema and clean up loader code - Flatten localAgentSchema to support top-level 'system_prompt' and 'query' in frontmatter - Remove nested 'prompts' object from schema and interface - Clean up excessive blank lines in loader.ts - Update tests to reflect schema changes and verify top-level prompt fields --- packages/core/src/agents/loader.test.ts | 53 ++++++++++++++++++------- packages/core/src/agents/loader.ts | 23 ++++------- 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/packages/core/src/agents/loader.test.ts b/packages/core/src/agents/loader.test.ts index b942609c9fd..5d7042f44e4 100644 --- a/packages/core/src/agents/loader.test.ts +++ b/packages/core/src/agents/loader.test.ts @@ -50,9 +50,7 @@ You are a markdown agent.`); name: 'test-agent-md', description: 'A markdown agent', kind: 'local', - prompts: { - system_prompt: 'You are a markdown agent.', - }, + system_prompt: 'You are a markdown agent.', }); }); @@ -78,9 +76,40 @@ System prompt content.`); model: 'gemini-pro', temperature: 0.7, }, - prompts: { - system_prompt: 'System prompt content.', - }, + system_prompt: 'System prompt content.', + }); + }); + + it('should parse system_prompt from frontmatter', async () => { + const filePath = await writeAgentMarkdown(`--- +name: frontmatter-prompt +description: Agent with prompt in frontmatter +system_prompt: You are defined in frontmatter. +--- +Ignored body content.`); + + const result = await parseAgentMarkdown(filePath); + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + name: 'frontmatter-prompt', + system_prompt: 'You are defined in frontmatter.', + }); + }); + + it('should parse query from frontmatter', async () => { + const filePath = await writeAgentMarkdown(`--- +name: query-agent +description: Agent with query +query: What is the weather? +--- +System prompt.`); + + const result = await parseAgentMarkdown(filePath); + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + name: 'query-agent', + system_prompt: 'System prompt.', + query: 'What is the weather?', }); }); @@ -188,9 +217,7 @@ agent_card_url: https://example.com/card kind: 'local' as const, name: 'test-agent', description: 'A test agent', - prompts: { - system_prompt: 'You are a test agent.', - }, + system_prompt: 'You are a test agent.', }; const result = markdownToAgentDefinition(markdown); @@ -226,9 +253,7 @@ agent_card_url: https://example.com/card model: { model: GEMINI_MODEL_ALIAS_PRO, }, - prompts: { - system_prompt: 'You are a test agent.', - }, + system_prompt: 'You are a test agent.', }; const result = markdownToAgentDefinition( @@ -245,9 +270,7 @@ agent_card_url: https://example.com/card model: { model: 'auto', }, - prompts: { - system_prompt: 'You are a test agent.', - }, + system_prompt: 'You are a test agent.', }; const result = markdownToAgentDefinition( diff --git a/packages/core/src/agents/loader.ts b/packages/core/src/agents/loader.ts index 751df2c7c7f..0077e50bbf6 100644 --- a/packages/core/src/agents/loader.ts +++ b/packages/core/src/agents/loader.ts @@ -28,10 +28,8 @@ interface FrontmatterLocalAgentDefinition kind: 'local'; description: string; tools?: string[]; - prompts: { - system_prompt: string; - query?: string; - }; + system_prompt: string; + query?: string; model?: { model?: string; temperature?: number; @@ -91,11 +89,8 @@ const localAgentSchema = z }), ) .optional(), - prompts: z - .object({ - query: z.string().optional(), - }) - .optional(), + system_prompt: z.string().optional(), + query: z.string().optional(), model: z .object({ model: z.string().optional(), @@ -234,10 +229,8 @@ export async function parseAgentMarkdown( const agentDef: FrontmatterLocalAgentDefinition = { ...frontmatter, kind: 'local', - prompts: { - system_prompt: body.trim(), - query: frontmatter.prompts?.query, - }, + system_prompt: frontmatter.system_prompt || body.trim(), + query: frontmatter.query, }; return [agentDef]; @@ -282,8 +275,8 @@ export function markdownToAgentDefinition( description: markdown.description, displayName: markdown.display_name, promptConfig: { - systemPrompt: markdown.prompts.system_prompt, - query: markdown.prompts.query, + systemPrompt: markdown.system_prompt, + query: markdown.query, }, modelConfig: { model: modelName, From 3d2ce455ef33c87845e25184e5d2828b1feefacc Mon Sep 17 00:00:00 2001 From: Sehoon Shon Date: Wed, 7 Jan 2026 21:19:38 -0500 Subject: [PATCH 3/6] refactor(core): rename loader.ts to agentLoader.ts - Rename packages/core/src/agents/loader.ts to agentLoader.ts - Rename packages/core/src/agents/loader.test.ts to agentLoader.test.ts - Update imports in registry.ts and registry.test.ts - Add parsing for remote agents list in markdown frontmatter - Update tests to verify single and multiple remote agents parsing --- .../{loader.test.ts => agentLoader.test.ts} | 25 ++++++++++++++++++- .../src/agents/{loader.ts => agentLoader.ts} | 17 +++++++++++++ packages/core/src/agents/registry.test.ts | 4 +-- packages/core/src/agents/registry.ts | 2 +- 4 files changed, 44 insertions(+), 4 deletions(-) rename packages/core/src/agents/{loader.test.ts => agentLoader.test.ts} (93%) rename packages/core/src/agents/{loader.ts => agentLoader.ts} (95%) diff --git a/packages/core/src/agents/loader.test.ts b/packages/core/src/agents/agentLoader.test.ts similarity index 93% rename from packages/core/src/agents/loader.test.ts rename to packages/core/src/agents/agentLoader.test.ts index 5d7042f44e4..effcd6416c2 100644 --- a/packages/core/src/agents/loader.test.ts +++ b/packages/core/src/agents/agentLoader.test.ts @@ -13,7 +13,7 @@ import { markdownToAgentDefinition, loadAgentsFromDirectory, AgentLoadError, -} from './loader.js'; +} from './agentLoader.js'; import { GEMINI_MODEL_ALIAS_PRO } from '../config/models.js'; import type { LocalAgentDefinition } from './types.js'; @@ -209,6 +209,29 @@ agent_card_url: https://example.com/card agent_card_url: 'https://example.com/card', }); }); + + it('should parse multiple remote agents in a list', async () => { + const filePath = await writeAgentMarkdown(`--- +- kind: remote + name: remote-1 + agent_card_url: https://example.com/1 +- kind: remote + name: remote-2 + agent_card_url: https://example.com/2 +---`); + const result = await parseAgentMarkdown(filePath); + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + kind: 'remote', + name: 'remote-1', + agent_card_url: 'https://example.com/1', + }); + expect(result[1]).toEqual({ + kind: 'remote', + name: 'remote-2', + agent_card_url: 'https://example.com/2', + }); + }); }); describe('markdownToAgentDefinition', () => { diff --git a/packages/core/src/agents/loader.ts b/packages/core/src/agents/agentLoader.ts similarity index 95% rename from packages/core/src/agents/loader.ts rename to packages/core/src/agents/agentLoader.ts index 0077e50bbf6..33a3e2cb4f5 100644 --- a/packages/core/src/agents/loader.ts +++ b/packages/core/src/agents/agentLoader.ts @@ -123,6 +123,8 @@ const agentUnionOptions = [ { schema: remoteAgentSchema, label: 'Remote Agent' }, ] as const; +const remoteAgentsListSchema = z.array(remoteAgentSchema); + const markdownFrontmatterSchema = z.union([ agentUnionOptions[0].schema, agentUnionOptions[1].schema, @@ -193,6 +195,21 @@ export async function parseAgentMarkdown( ); } + // 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) { diff --git a/packages/core/src/agents/registry.test.ts b/packages/core/src/agents/registry.test.ts index 6ad15fa153f..a0f79fe1a97 100644 --- a/packages/core/src/agents/registry.test.ts +++ b/packages/core/src/agents/registry.test.ts @@ -19,9 +19,9 @@ import { PREVIEW_GEMINI_MODEL, PREVIEW_GEMINI_MODEL_AUTO, } from '../config/models.js'; -import * as tomlLoader from './loader.js'; +import * as tomlLoader from './agentLoader.js'; -vi.mock('./loader.js', () => ({ +vi.mock('./agentLoader.js', () => ({ loadAgentsFromDirectory: vi .fn() .mockResolvedValue({ agents: [], errors: [] }), diff --git a/packages/core/src/agents/registry.ts b/packages/core/src/agents/registry.ts index 174c2de74c3..680094eaaf6 100644 --- a/packages/core/src/agents/registry.ts +++ b/packages/core/src/agents/registry.ts @@ -8,7 +8,7 @@ import { Storage } from '../config/storage.js'; import { coreEvents, CoreEvent } from '../utils/events.js'; import type { Config } from '../config/config.js'; import type { AgentDefinition } from './types.js'; -import { loadAgentsFromDirectory } from './loader.js'; +import { loadAgentsFromDirectory } from './agentLoader.js'; import { CodebaseInvestigatorAgent } from './codebase-investigator.js'; import { IntrospectionAgent } from './introspection-agent.js'; import { A2AClientManager } from './a2a-client-manager.js'; From c8c7446adf19b95e2f1d3bd90064229a749f36fa Mon Sep 17 00:00:00 2001 From: Sehoon Shon Date: Thu, 8 Jan 2026 02:50:37 -0500 Subject: [PATCH 4/6] Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/core/src/agents/agentLoader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/agents/agentLoader.ts b/packages/core/src/agents/agentLoader.ts index 33a3e2cb4f5..c81e1a01cb7 100644 --- a/packages/core/src/agents/agentLoader.ts +++ b/packages/core/src/agents/agentLoader.ts @@ -246,7 +246,7 @@ export async function parseAgentMarkdown( const agentDef: FrontmatterLocalAgentDefinition = { ...frontmatter, kind: 'local', - system_prompt: frontmatter.system_prompt || body.trim(), + system_prompt: frontmatter.system_prompt ?? body.trim(), query: frontmatter.query, }; From 66566b3d7053ee4bfdcdaab98a55ca5e152e26ca Mon Sep 17 00:00:00 2001 From: Sehoon Shon Date: Thu, 8 Jan 2026 21:26:44 -0500 Subject: [PATCH 5/6] remove system_prompt from frontmatter field --- packages/core/src/agents/agentLoader.test.ts | 16 ---------------- packages/core/src/agents/agentLoader.ts | 3 +-- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/core/src/agents/agentLoader.test.ts b/packages/core/src/agents/agentLoader.test.ts index effcd6416c2..cdaea31d11d 100644 --- a/packages/core/src/agents/agentLoader.test.ts +++ b/packages/core/src/agents/agentLoader.test.ts @@ -80,22 +80,6 @@ System prompt content.`); }); }); - it('should parse system_prompt from frontmatter', async () => { - const filePath = await writeAgentMarkdown(`--- -name: frontmatter-prompt -description: Agent with prompt in frontmatter -system_prompt: You are defined in frontmatter. ---- -Ignored body content.`); - - const result = await parseAgentMarkdown(filePath); - expect(result).toHaveLength(1); - expect(result[0]).toMatchObject({ - name: 'frontmatter-prompt', - system_prompt: 'You are defined in frontmatter.', - }); - }); - it('should parse query from frontmatter', async () => { const filePath = await writeAgentMarkdown(`--- name: query-agent diff --git a/packages/core/src/agents/agentLoader.ts b/packages/core/src/agents/agentLoader.ts index c81e1a01cb7..d041d18b3c2 100644 --- a/packages/core/src/agents/agentLoader.ts +++ b/packages/core/src/agents/agentLoader.ts @@ -89,7 +89,6 @@ const localAgentSchema = z }), ) .optional(), - system_prompt: z.string().optional(), query: z.string().optional(), model: z .object({ @@ -246,7 +245,7 @@ export async function parseAgentMarkdown( const agentDef: FrontmatterLocalAgentDefinition = { ...frontmatter, kind: 'local', - system_prompt: frontmatter.system_prompt ?? body.trim(), + system_prompt: body.trim(), query: frontmatter.query, }; From a4a88a04204588fe5c8e261af5cb63d964c0cfb9 Mon Sep 17 00:00:00 2001 From: Sehoon Shon Date: Fri, 9 Jan 2026 16:06:40 -0500 Subject: [PATCH 6/6] make frontmatter fields all top levels --- packages/core/src/agents/agentLoader.test.ts | 43 +++++------------- packages/core/src/agents/agentLoader.ts | 47 +++++++------------- packages/core/src/skills/skillLoader.ts | 2 +- 3 files changed, 28 insertions(+), 64 deletions(-) diff --git a/packages/core/src/agents/agentLoader.test.ts b/packages/core/src/agents/agentLoader.test.ts index cdaea31d11d..0d6acf7de04 100644 --- a/packages/core/src/agents/agentLoader.test.ts +++ b/packages/core/src/agents/agentLoader.test.ts @@ -60,9 +60,8 @@ name: complex-agent description: A complex markdown agent tools: - run_shell_command -model: - model: gemini-pro - temperature: 0.7 +model: gemini-pro +temperature: 0.7 --- System prompt content.`); @@ -72,31 +71,12 @@ System prompt content.`); name: 'complex-agent', description: 'A complex markdown agent', tools: ['run_shell_command'], - model: { - model: 'gemini-pro', - temperature: 0.7, - }, + model: 'gemini-pro', + temperature: 0.7, system_prompt: 'System prompt content.', }); }); - it('should parse query from frontmatter', async () => { - const filePath = await writeAgentMarkdown(`--- -name: query-agent -description: Agent with query -query: What is the weather? ---- -System prompt.`); - - const result = await parseAgentMarkdown(filePath); - expect(result).toHaveLength(1); - expect(result[0]).toMatchObject({ - name: 'query-agent', - system_prompt: 'System prompt.', - query: 'What is the weather?', - }); - }); - it('should throw AgentLoadError if frontmatter is missing', async () => { const filePath = await writeAgentMarkdown(`Just some markdown content.`); await expect(parseAgentMarkdown(filePath)).rejects.toThrow( @@ -184,7 +164,8 @@ agent_card_url: https://example.com/inferred kind: remote name: no-body-remote agent_card_url: https://example.com/card ----`); +--- +`); const result = await parseAgentMarkdown(filePath); expect(result).toHaveLength(1); expect(result[0]).toEqual({ @@ -202,7 +183,8 @@ agent_card_url: https://example.com/card - kind: remote name: remote-2 agent_card_url: https://example.com/2 ----`); +--- +`); const result = await parseAgentMarkdown(filePath); expect(result).toHaveLength(2); expect(result[0]).toEqual({ @@ -233,6 +215,7 @@ agent_card_url: https://example.com/card description: 'A test agent', promptConfig: { systemPrompt: 'You are a test agent.', + query: '${query}', }, modelConfig: { model: 'inherit', @@ -257,9 +240,7 @@ agent_card_url: https://example.com/card kind: 'local' as const, name: 'test-agent', description: 'A test agent', - model: { - model: GEMINI_MODEL_ALIAS_PRO, - }, + model: GEMINI_MODEL_ALIAS_PRO, system_prompt: 'You are a test agent.', }; @@ -274,9 +255,7 @@ agent_card_url: https://example.com/card kind: 'local' as const, name: 'test-agent', description: 'A test agent', - model: { - model: 'auto', - }, + model: 'auto', system_prompt: 'You are a test agent.', }; diff --git a/packages/core/src/agents/agentLoader.ts b/packages/core/src/agents/agentLoader.ts index d041d18b3c2..5a65f03218d 100644 --- a/packages/core/src/agents/agentLoader.ts +++ b/packages/core/src/agents/agentLoader.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2025 Google LLC + * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ @@ -14,6 +14,7 @@ import { isValidToolName, DELEGATE_TO_AGENT_TOOL_NAME, } from '../tools/tool-names.js'; +import { FRONTMATTER_REGEX } from '../skills/skillLoader.js'; /** * DTO for Markdown parsing - represents the structure from frontmatter. @@ -29,15 +30,10 @@ interface FrontmatterLocalAgentDefinition description: string; tools?: string[]; system_prompt: string; - query?: string; - model?: { - model?: string; - temperature?: number; - }; - run?: { - max_turns?: number; - timeout_mins?: number; - }; + model?: string; + temperature?: number; + max_turns?: number; + timeout_mins?: number; } interface FrontmatterRemoteAgentDefinition @@ -89,19 +85,10 @@ const localAgentSchema = z }), ) .optional(), - query: z.string().optional(), - model: z - .object({ - model: z.string().optional(), - temperature: z.number().optional(), - }) - .optional(), - run: z - .object({ - max_turns: z.number().int().positive().optional(), - timeout_mins: z.number().int().positive().optional(), - }) - .optional(), + model: z.string().optional(), + temperature: z.number().optional(), + max_turns: z.number().int().positive().optional(), + timeout_mins: z.number().int().positive().optional(), }) .strict(); @@ -172,8 +159,7 @@ export async function parseAgentMarkdown( } // Split frontmatter and body - // Matches "---", newline, content, newline, "---", optional newline, optional rest - const match = content.match(/^---\n([\s\S]*?)\n---(?:\n([\s\S]*))?$/); + const match = content.match(FRONTMATTER_REGEX); if (!match) { throw new AgentLoadError( filePath, @@ -246,7 +232,6 @@ export async function parseAgentMarkdown( ...frontmatter, kind: 'local', system_prompt: body.trim(), - query: frontmatter.query, }; return [agentDef]; @@ -283,7 +268,7 @@ export function markdownToAgentDefinition( } // If a model is specified, use it. Otherwise, inherit - const modelName = markdown.model?.model || 'inherit'; + const modelName = markdown.model || 'inherit'; return { kind: 'local', @@ -292,16 +277,16 @@ export function markdownToAgentDefinition( displayName: markdown.display_name, promptConfig: { systemPrompt: markdown.system_prompt, - query: markdown.query, + query: '${query}', }, modelConfig: { model: modelName, - temp: markdown.model?.temperature ?? 1, + temp: markdown.temperature ?? 1, top_p: 0.95, }, runConfig: { - max_turns: markdown.run?.max_turns, - max_time_minutes: markdown.run?.timeout_mins || 5, + max_turns: markdown.max_turns, + max_time_minutes: markdown.timeout_mins || 5, }, toolConfig: markdown.tools ? { diff --git a/packages/core/src/skills/skillLoader.ts b/packages/core/src/skills/skillLoader.ts index e9de2db2f01..d019c2befc1 100644 --- a/packages/core/src/skills/skillLoader.ts +++ b/packages/core/src/skills/skillLoader.ts @@ -27,7 +27,7 @@ export interface SkillDefinition { disabled?: boolean; } -const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)/; +export const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)/; /** * Discovers and loads all skills in the provided directory.