Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/agents/generalist-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('GeneralistAgent', () => {
vi.spyOn(config, 'getAgentRegistry').mockReturnValue({
getDirectoryContext: () => 'mock directory context',
getAllAgentNames: () => ['agent-tool'],
getAllDefinitions: () => [],
} as unknown as AgentRegistry);

const agent = GeneralistAgent(config);
Expand Down
24 changes: 0 additions & 24 deletions packages/core/src/agents/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1104,28 +1104,4 @@ describe('AgentRegistry', () => {
expect(getterCalled).toBe(true); // Getter should have been called now
});
});

describe('getDirectoryContext', () => {
it('should return default message when no agents are registered', () => {
expect(registry.getDirectoryContext()).toContain(
'No sub-agents are currently available.',
);
});

it('should return formatted list of agents when agents are available', async () => {
await registry.testRegisterAgent(MOCK_AGENT_V1);
await registry.testRegisterAgent({
...MOCK_AGENT_V2,
name: 'AnotherAgent',
description: 'Another agent description',
});

const description = registry.getDirectoryContext();

expect(description).toContain('Sub-agents are specialized expert agents');
expect(description).toContain('Available Sub-Agents');
expect(description).toContain(`- ${MOCK_AGENT_V1.name}`);
expect(description).toContain(`- AnotherAgent`);
});
});
});
33 changes: 0 additions & 33 deletions packages/core/src/agents/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,37 +481,4 @@ export class AgentRegistry {
getDiscoveredDefinition(name: string): AgentDefinition | undefined {
return this.allDefinitions.get(name);
}

/**
* Generates a markdown "Phone Book" of available agents and their schemas.
* This MUST be injected into the System Prompt of the parent agent.
*/
getDirectoryContext(): string {
if (this.agents.size === 0) {
return 'No sub-agents are currently available.';
}

let context = '## Available Sub-Agents\n';
context += `Sub-agents are specialized expert agents that you can use to assist you in
the completion of all or part of a task.

Each sub-agent is available as a tool of the same name.

You MUST always delegate tasks to the sub-agent with the
relevant expertise, if one is available.

The following tools can be used to start sub-agents:\n\n`;

for (const [name] of this.agents) {
context += `- ${name}\n`;
}

context += `Remember that the closest relevant sub-agent should still be used even if its expertise is broader than the given task.

For example:
- A license-agent -> Should be used for a range of tasks, including reading, validating, and updating licenses and headers.
- A test-fixing-agent -> Should be used both for fixing tests as well as investigating test failures.`;

return context;
}
}
510 changes: 479 additions & 31 deletions packages/core/src/core/__snapshots__/prompts.test.ts.snap

Large diffs are not rendered by default.

16 changes: 12 additions & 4 deletions packages/core/src/core/prompts-substitution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getCoreSystemPrompt } from './prompts.js';
import fs from 'node:fs';
import type { Config } from '../config/config.js';
import type { AgentDefinition } from '../agents/types.js';
import * as toolNames from '../tools/tool-names.js';

vi.mock('node:fs');
Expand Down Expand Up @@ -40,6 +41,7 @@ describe('Core System Prompt Substitution', () => {
getActiveModel: vi.fn().mockReturnValue('gemini-1.5-pro'),
getAgentRegistry: vi.fn().mockReturnValue({
getDirectoryContext: vi.fn().mockReturnValue('Mock Agent Directory'),
getAllDefinitions: vi.fn().mockReturnValue([]),
}),
getSkillManager: vi.fn().mockReturnValue({
getSkills: vi.fn().mockReturnValue([]),
Expand Down Expand Up @@ -74,13 +76,19 @@ describe('Core System Prompt Substitution', () => {
it('should substitute ${SubAgents} in custom system prompt', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue('Agents: ${SubAgents}');
vi.mocked(
mockConfig.getAgentRegistry().getDirectoryContext,
).mockReturnValue('Actual Agent Directory');

vi.mocked(mockConfig.getAgentRegistry().getAllDefinitions).mockReturnValue([
{
name: 'test-agent',
description: 'Test Agent Description',
} as unknown as AgentDefinition,
]);

const prompt = getCoreSystemPrompt(mockConfig);

expect(prompt).toContain('Agents: Actual Agent Directory');
expect(prompt).toContain('Agents:');
expect(prompt).toContain('# Available Sub-Agents');
expect(prompt).toContain('- test-agent -> Test Agent Description');
expect(prompt).not.toContain('${SubAgents}');
});

Expand Down
35 changes: 35 additions & 0 deletions packages/core/src/core/prompts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import type { Config } from '../config/config.js';
import type { AgentDefinition } from '../agents/types.js';
import { CodebaseInvestigatorAgent } from '../agents/codebase-investigator.js';
import { GEMINI_DIR } from '../utils/paths.js';
import { debugLogger } from '../utils/debugLogger.js';
Expand Down Expand Up @@ -101,6 +102,12 @@ describe('Core System Prompt (prompts.ts)', () => {
getMessageBus: vi.fn(),
getAgentRegistry: vi.fn().mockReturnValue({
getDirectoryContext: vi.fn().mockReturnValue('Mock Agent Directory'),
getAllDefinitions: vi.fn().mockReturnValue([
{
name: 'mock-agent',
description: 'Mock Agent Description',
},
]),
}),
getSkillManager: vi.fn().mockReturnValue({
getSkills: vi.fn().mockReturnValue([]),
Expand Down Expand Up @@ -154,6 +161,32 @@ describe('Core System Prompt (prompts.ts)', () => {
expect(prompt).not.toContain('activate_skill');
});

it('should include sub-agents in XML for preview models', () => {
vi.mocked(mockConfig.getActiveModel).mockReturnValue(PREVIEW_GEMINI_MODEL);
const agents = [
{
name: 'test-agent',
displayName: 'Test Agent',
description: 'A test agent description',
},
];
vi.mocked(mockConfig.getAgentRegistry().getAllDefinitions).mockReturnValue(
agents as unknown as AgentDefinition[],
);
const prompt = getCoreSystemPrompt(mockConfig);

expect(prompt).toContain('# Available Sub-Agents');
expect(prompt).toContain('<available_subagents>');
expect(prompt).toContain('<subagent>');
expect(prompt).toContain('<name>Test Agent</name>');
expect(prompt).toContain(
'<description>A test agent description</description>',
);
expect(prompt).toContain('</subagent>');
expect(prompt).toContain('</available_subagents>');
expect(prompt).toMatchSnapshot();
});

it('should use legacy system prompt for non-preview model', () => {
vi.mocked(mockConfig.getActiveModel).mockReturnValue(
DEFAULT_GEMINI_FLASH_LITE_MODEL,
Expand All @@ -162,6 +195,7 @@ describe('Core System Prompt (prompts.ts)', () => {
expect(prompt).toContain(
'You are an interactive CLI agent specializing in software engineering tasks.',
);
expect(prompt).not.toContain('No sub-agents are currently available.');
expect(prompt).toContain('# Core Mandates');
expect(prompt).toContain('- **Conventions:**');
expect(prompt).toMatchSnapshot();
Expand Down Expand Up @@ -279,6 +313,7 @@ describe('Core System Prompt (prompts.ts)', () => {
getPreviewFeatures: vi.fn().mockReturnValue(true),
getAgentRegistry: vi.fn().mockReturnValue({
getDirectoryContext: vi.fn().mockReturnValue('Mock Agent Directory'),
getAllDefinitions: vi.fn().mockReturnValue([]),
}),
getSkillManager: vi.fn().mockReturnValue({
getSkills: vi.fn().mockReturnValue([]),
Expand Down
17 changes: 14 additions & 3 deletions packages/core/src/prompts/promptProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,12 @@ export class PromptProvider {
location: s.location,
})),
);
basePrompt = applySubstitutions(basePrompt, config, skillsPrompt);
basePrompt = applySubstitutions(
basePrompt,
config,
skillsPrompt,
isGemini3,
);
} else {
// --- Standard Composition ---
const options: snippets.SystemPromptOptions = {
Expand All @@ -110,8 +115,14 @@ export class PromptProvider {
isGemini3,
hasSkills: skills.length > 0,
})),
agentContexts: this.withSection('agentContexts', () =>
config.getAgentRegistry().getDirectoryContext(),
subAgents: this.withSection('agentContexts', () =>
config
.getAgentRegistry()
.getAllDefinitions()
.map((d) => ({
name: d.displayName || d.name,
description: d.description,
})),
),
agentSkills: this.withSection(
'agentSkills',
Expand Down
33 changes: 28 additions & 5 deletions packages/core/src/prompts/snippets.legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
export interface SystemPromptOptions {
preamble?: PreambleOptions;
coreMandates?: CoreMandatesOptions;
agentContexts?: string;
subAgents?: SubAgentOptions[];
agentSkills?: AgentSkillOptions[];
hookContext?: boolean;
primaryWorkflows?: PrimaryWorkflowsOptions;
Expand Down Expand Up @@ -82,6 +82,11 @@ export interface AgentSkillOptions {
location: string;
}

export interface SubAgentOptions {
name: string;
description: string;
}

// --- High Level Composition ---

/**
Expand All @@ -94,7 +99,7 @@ ${renderPreamble(options.preamble)}

${renderCoreMandates(options.coreMandates)}

${renderAgentContexts(options.agentContexts)}
${renderSubAgents(options.subAgents)}
${renderAgentSkills(options.agentSkills)}

${renderHookContext(options.hookContext)}
Expand Down Expand Up @@ -155,9 +160,27 @@ export function renderCoreMandates(options?: CoreMandatesOptions): string {
`.trim();
}

export function renderAgentContexts(contexts?: string): string {
if (!contexts) return '';
return contexts.trim();
export function renderSubAgents(subAgents?: SubAgentOptions[]): string {
if (!subAgents || subAgents.length === 0) return '';
const subAgentsList = subAgents
.map((agent) => `- ${agent.name} -> ${agent.description}`)
.join('\n');

return `
# Available Sub-Agents
Sub-agents are specialized expert agents that you can use to assist you in the completion of all or part of a task.

Each sub-agent is available as a tool of the same name. You MUST always delegate tasks to the sub-agent with the relevant expertise, if one is available.

The following tools can be used to start sub-agents:

${subAgentsList}

Remember that the closest relevant sub-agent should still be used even if its expertise is broader than the given task.

For example:
- A license-agent -> Should be used for a range of tasks, including reading, validating, and updating licenses and headers.
- A test-fixing-agent -> Should be used both for fixing tests as well as investigating test failures.`;
}

export function renderAgentSkills(skills?: AgentSkillOptions[]): string {
Expand Down
Loading
Loading