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();
// Load agents from all levels separately to show all agents including conflicts
const [projectAgents, userAgents, builtinAgents] = await Promise.all([
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 || []),
];
const allAgents = await manager.listSubagents();
setAvailableAgents(allAgents);
}, [config]);

View File

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

View File

@@ -58,6 +58,7 @@ import {
NextSpeakerCheckEvent,
} from '../telemetry/types.js';
import { IdeContext, File } from '../ide/ideContext.js';
import { TaskTool } from '../tools/task.js';
function isThinkingSupported(model: string) {
if (model.startsWith('gemini-2.5')) return true;
@@ -452,7 +453,8 @@ export class GeminiClient {
turns: number = MAX_TURNS,
originalModel?: string,
): AsyncGenerator<ServerGeminiStreamEvent, Turn> {
if (this.lastPromptId !== prompt_id) {
const isNewPrompt = this.lastPromptId !== prompt_id;
if (isNewPrompt) {
this.loopDetector.reset(prompt_id);
this.lastPromptId = prompt_id;
}
@@ -549,6 +551,24 @@ export class GeminiClient {
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 loopDetected = await this.loopDetector.turnStarted(signal);

View File

@@ -40,6 +40,7 @@ const AGENT_CONFIG_DIR = 'agents';
*/
export class SubagentManager {
private readonly validator: SubagentValidator;
private subagentsCache: Map<SubagentLevel, SubagentConfig[]> | null = null;
constructor(private readonly config: Config) {
this.validator = new SubagentValidator();
@@ -93,6 +94,8 @@ export class SubagentManager {
try {
await fs.writeFile(filePath, content, 'utf8');
// Clear cache after successful creation
this.clearCache();
} catch (error) {
throw new SubagentError(
`Failed to write subagent file: ${error instanceof Error ? error.message : 'Unknown error'}`,
@@ -181,6 +184,8 @@ export class SubagentManager {
try {
await fs.writeFile(existing.filePath, content, 'utf8');
// Clear cache after successful update
this.clearCache();
} catch (error) {
throw new SubagentError(
`Failed to update subagent file: ${error instanceof Error ? error.message : 'Unknown error'}`,
@@ -237,6 +242,9 @@ export class SubagentManager {
name,
);
}
// Clear cache after successful deletion
this.clearCache();
}
/**
@@ -255,9 +263,17 @@ export class SubagentManager {
? [options.level]
: ['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)
for (const level of levelsToCheck) {
const levelSubagents = await this.listSubagentsAtLevel(level);
const levelSubagents = this.subagentsCache?.get(level) || [];
for (const subagent of levelSubagents) {
// Skip if we've already seen this name (precedence: project > user > builtin)
@@ -305,6 +321,30 @@ export class SubagentManager {
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.
*

View File

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