feat: add system-reminder to help model use subagent

This commit is contained in:
tanzhenxin
2025-09-17 18:56:30 +08:00
parent e552bc9609
commit 8803b2eb76
5 changed files with 70 additions and 14 deletions

View File

@@ -53,18 +53,7 @@ export function AgentsManagerDialog({
const manager = config.getSubagentManager(); const manager = config.getSubagentManager();
// Load agents from all levels separately to show all agents including conflicts // Load agents from all levels separately to show all agents including conflicts
const [projectAgents, userAgents, builtinAgents] = await Promise.all([ const allAgents = await manager.listSubagents();
manager.listSubagents({ level: 'project' }),
manager.listSubagents({ level: 'user' }),
manager.listSubagents({ level: 'builtin' }),
]);
// Combine all agents (project, user, and builtin level)
const allAgents = [
...(projectAgents || []),
...(userAgents || []),
...(builtinAgents || []),
];
setAvailableAgents(allAgents); setAvailableAgents(allAgents);
}, [config]); }, [config]);

View File

@@ -199,6 +199,9 @@ describe('Gemini Client (client.ts)', () => {
vertexai: false, vertexai: false,
authType: AuthType.USE_GEMINI, authType: AuthType.USE_GEMINI,
}; };
const mockSubagentManager = {
listSubagents: vi.fn().mockResolvedValue([]),
};
const mockConfigObject = { const mockConfigObject = {
getContentGeneratorConfig: vi getContentGeneratorConfig: vi
.fn() .fn()
@@ -233,6 +236,7 @@ describe('Gemini Client (client.ts)', () => {
getCliVersion: vi.fn().mockReturnValue('1.0.0'), getCliVersion: vi.fn().mockReturnValue('1.0.0'),
getChatCompression: vi.fn().mockReturnValue(undefined), getChatCompression: vi.fn().mockReturnValue(undefined),
getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false), getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false),
getSubagentManager: vi.fn().mockReturnValue(mockSubagentManager),
}; };
const MockedConfig = vi.mocked(Config, true); const MockedConfig = vi.mocked(Config, true);
MockedConfig.mockImplementation( MockedConfig.mockImplementation(

View File

@@ -58,6 +58,7 @@ import {
NextSpeakerCheckEvent, NextSpeakerCheckEvent,
} from '../telemetry/types.js'; } from '../telemetry/types.js';
import { IdeContext, File } from '../ide/ideContext.js'; import { IdeContext, File } from '../ide/ideContext.js';
import { TaskTool } from '../tools/task.js';
function isThinkingSupported(model: string) { function isThinkingSupported(model: string) {
if (model.startsWith('gemini-2.5')) return true; if (model.startsWith('gemini-2.5')) return true;
@@ -452,7 +453,8 @@ export class GeminiClient {
turns: number = MAX_TURNS, turns: number = MAX_TURNS,
originalModel?: string, originalModel?: string,
): AsyncGenerator<ServerGeminiStreamEvent, Turn> { ): AsyncGenerator<ServerGeminiStreamEvent, Turn> {
if (this.lastPromptId !== prompt_id) { const isNewPrompt = this.lastPromptId !== prompt_id;
if (isNewPrompt) {
this.loopDetector.reset(prompt_id); this.loopDetector.reset(prompt_id);
this.lastPromptId = prompt_id; this.lastPromptId = prompt_id;
} }
@@ -549,6 +551,24 @@ export class GeminiClient {
this.forceFullIdeContext = false; this.forceFullIdeContext = false;
} }
if (isNewPrompt) {
const taskTool = this.config.getToolRegistry().getTool(TaskTool.Name);
const subagents = (
await this.config.getSubagentManager().listSubagents()
).filter((subagent) => subagent.level !== 'builtin');
if (taskTool && subagents.length > 0) {
this.getChat().addHistory({
role: 'user',
parts: [
{
text: `<system-reminder>You have powerful specialized agents at your disposal, available agent types are: ${subagents.map((subagent) => subagent.name).join(', ')}. PROACTIVELY use the ${TaskTool.Name} tool to delegate user's task to appropriate agent when user's task matches agent capabilities. Ignore this message if user's task is not relevant to any agent. This message is for internal use only. Do not mention this to user in your response.</system-reminder>`,
},
],
});
}
}
const turn = new Turn(this.getChat(), prompt_id); const turn = new Turn(this.getChat(), prompt_id);
const loopDetected = await this.loopDetector.turnStarted(signal); const loopDetected = await this.loopDetector.turnStarted(signal);

View File

@@ -40,6 +40,7 @@ const AGENT_CONFIG_DIR = 'agents';
*/ */
export class SubagentManager { export class SubagentManager {
private readonly validator: SubagentValidator; private readonly validator: SubagentValidator;
private subagentsCache: Map<SubagentLevel, SubagentConfig[]> | null = null;
constructor(private readonly config: Config) { constructor(private readonly config: Config) {
this.validator = new SubagentValidator(); this.validator = new SubagentValidator();
@@ -93,6 +94,8 @@ export class SubagentManager {
try { try {
await fs.writeFile(filePath, content, 'utf8'); await fs.writeFile(filePath, content, 'utf8');
// Clear cache after successful creation
this.clearCache();
} catch (error) { } catch (error) {
throw new SubagentError( throw new SubagentError(
`Failed to write subagent file: ${error instanceof Error ? error.message : 'Unknown error'}`, `Failed to write subagent file: ${error instanceof Error ? error.message : 'Unknown error'}`,
@@ -181,6 +184,8 @@ export class SubagentManager {
try { try {
await fs.writeFile(existing.filePath, content, 'utf8'); await fs.writeFile(existing.filePath, content, 'utf8');
// Clear cache after successful update
this.clearCache();
} catch (error) { } catch (error) {
throw new SubagentError( throw new SubagentError(
`Failed to update subagent file: ${error instanceof Error ? error.message : 'Unknown error'}`, `Failed to update subagent file: ${error instanceof Error ? error.message : 'Unknown error'}`,
@@ -237,6 +242,9 @@ export class SubagentManager {
name, name,
); );
} }
// Clear cache after successful deletion
this.clearCache();
} }
/** /**
@@ -255,9 +263,17 @@ export class SubagentManager {
? [options.level] ? [options.level]
: ['project', 'user', 'builtin']; : ['project', 'user', 'builtin'];
// Check if we should use cache or force refresh
const shouldUseCache = !options.force && this.subagentsCache !== null;
// Initialize cache if it doesn't exist or we're forcing a refresh
if (!shouldUseCache) {
await this.refreshCache();
}
// Collect subagents from each level (project takes precedence over user, user takes precedence over builtin) // Collect subagents from each level (project takes precedence over user, user takes precedence over builtin)
for (const level of levelsToCheck) { for (const level of levelsToCheck) {
const levelSubagents = await this.listSubagentsAtLevel(level); const levelSubagents = this.subagentsCache?.get(level) || [];
for (const subagent of levelSubagents) { for (const subagent of levelSubagents) {
// Skip if we've already seen this name (precedence: project > user > builtin) // Skip if we've already seen this name (precedence: project > user > builtin)
@@ -305,6 +321,30 @@ export class SubagentManager {
return subagents; return subagents;
} }
/**
* Refreshes the subagents cache by loading all subagents from disk.
* This method is called automatically when cache is null or when force=true.
*
* @private
*/
private async refreshCache(): Promise<void> {
this.subagentsCache = new Map();
const levels: SubagentLevel[] = ['project', 'user', 'builtin'];
for (const level of levels) {
const levelSubagents = await this.listSubagentsAtLevel(level);
this.subagentsCache.set(level, levelSubagents);
}
}
/**
* Clears the subagents cache, forcing the next listSubagents call to reload from disk.
*/
clearCache(): void {
this.subagentsCache = null;
}
/** /**
* Finds a subagent by name and returns its metadata. * Finds a subagent by name and returns its metadata.
* *

View File

@@ -116,6 +116,9 @@ export interface ListSubagentsOptions {
/** Sort direction */ /** Sort direction */
sortOrder?: 'asc' | 'desc'; sortOrder?: 'asc' | 'desc';
/** Force refresh from disk, bypassing cache. Defaults to false. */
force?: boolean;
} }
/** /**