mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-19 06:20:44 -07:00
feat(core): enable team-aware orchestration in top-level agent
- Update SystemPromptOptions to include activeTeam - Implement renderActiveTeam snippet for team instructions injection - Update PromptProvider to pass activeTeam from config to system prompt - Enhance SubagentTool to support description overrides - Update Config to prioritize and label team-based subagent tools - Ensure synchronized team reload in TeamRegistry and Config - Add unit tests for team-aware prompt generation
This commit is contained in:
@@ -33,6 +33,7 @@ export class SubagentTool extends BaseDeclarativeTool<AgentInputs, ToolResult> {
|
|||||||
private readonly definition: AgentDefinition,
|
private readonly definition: AgentDefinition,
|
||||||
private readonly context: AgentLoopContext,
|
private readonly context: AgentLoopContext,
|
||||||
messageBus: MessageBus,
|
messageBus: MessageBus,
|
||||||
|
descriptionOverride?: string,
|
||||||
) {
|
) {
|
||||||
const inputSchema = definition.inputConfig.inputSchema;
|
const inputSchema = definition.inputConfig.inputSchema;
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ export class SubagentTool extends BaseDeclarativeTool<AgentInputs, ToolResult> {
|
|||||||
super(
|
super(
|
||||||
definition.name,
|
definition.name,
|
||||||
definition.displayName ?? definition.name,
|
definition.displayName ?? definition.name,
|
||||||
definition.description,
|
descriptionOverride ?? definition.description,
|
||||||
Kind.Agent,
|
Kind.Agent,
|
||||||
inputSchema,
|
inputSchema,
|
||||||
messageBus,
|
messageBus,
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export class TeamRegistry {
|
|||||||
// Load user-level teams first
|
// Load user-level teams first
|
||||||
const userTeamsDir = Storage.getUserTeamsDir();
|
const userTeamsDir = Storage.getUserTeamsDir();
|
||||||
const userLoadResult = await loadTeamsFromDirectory(userTeamsDir);
|
const userLoadResult = await loadTeamsFromDirectory(userTeamsDir);
|
||||||
this.processLoadResult(userLoadResult);
|
await this.processLoadResult(userLoadResult);
|
||||||
|
|
||||||
const folderTrustEnabled = this.config.getFolderTrust();
|
const folderTrustEnabled = this.config.getFolderTrust();
|
||||||
const isTrustedFolder = this.config.isTrustedFolder();
|
const isTrustedFolder = this.config.isTrustedFolder();
|
||||||
@@ -65,7 +65,7 @@ export class TeamRegistry {
|
|||||||
// Load project-level teams (takes precedence over user-level if names collide)
|
// Load project-level teams (takes precedence over user-level if names collide)
|
||||||
const projectTeamsDir = this.config.storage.getProjectTeamsDir();
|
const projectTeamsDir = this.config.storage.getProjectTeamsDir();
|
||||||
const projectLoadResult = await loadTeamsFromDirectory(projectTeamsDir);
|
const projectLoadResult = await loadTeamsFromDirectory(projectTeamsDir);
|
||||||
this.processLoadResult(projectLoadResult);
|
await this.processLoadResult(projectLoadResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.config.getDebugMode()) {
|
if (this.config.getDebugMode()) {
|
||||||
@@ -73,29 +73,35 @@ export class TeamRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private processLoadResult(result: TeamLoadResult): void {
|
private async processLoadResult(result: TeamLoadResult): Promise<void> {
|
||||||
for (const error of result.errors) {
|
for (const error of result.errors) {
|
||||||
debugLogger.warn(`[TeamRegistry] Error loading team: ${error.message}`);
|
debugLogger.warn(`[TeamRegistry] Error loading team: ${error.message}`);
|
||||||
coreEvents.emitFeedback('error', `Team loading error: ${error.message}`);
|
coreEvents.emitFeedback('error', `Team loading error: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const registrationPromises: Array<Promise<void>> = [];
|
||||||
|
|
||||||
for (const team of result.teams) {
|
for (const team of result.teams) {
|
||||||
this.teams.set(team.name, team);
|
this.teams.set(team.name, team);
|
||||||
|
|
||||||
// Register team agents in the global AgentRegistry so they are available as SubagentTools
|
// Register team agents in the global AgentRegistry so they are available as SubagentTools
|
||||||
for (const agent of team.agents) {
|
for (const agent of team.agents) {
|
||||||
this.agentRegistry.registerAgent(agent).catch((e) => {
|
registrationPromises.push(
|
||||||
debugLogger.warn(
|
this.agentRegistry.registerAgent(agent).catch((e) => {
|
||||||
`[TeamRegistry] Error registering agent "${agent.name}" from team "${team.name}":`,
|
debugLogger.warn(
|
||||||
e,
|
`[TeamRegistry] Error registering agent "${agent.name}" from team "${team.name}":`,
|
||||||
);
|
e,
|
||||||
coreEvents.emitFeedback(
|
);
|
||||||
'error',
|
coreEvents.emitFeedback(
|
||||||
`Error registering agent "${agent.name}" from team "${team.name}": ${e instanceof Error ? e.message : String(e)}`,
|
'error',
|
||||||
);
|
`Error registering agent "${agent.name}" from team "${team.name}": ${e instanceof Error ? e.message : String(e)}`,
|
||||||
});
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Promise.allSettled(registrationPromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3617,6 +3617,9 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const discoveredNames = this.agentRegistry.getAllDiscoveredAgentNames();
|
const discoveredNames = this.agentRegistry.getAllDiscoveredAgentNames();
|
||||||
|
const activeTeam = this.teamRegistry.getActiveTeam();
|
||||||
|
const teamAgentNames = new Set(activeTeam?.agents.map((a) => a.name) ?? []);
|
||||||
|
|
||||||
for (const agentName of discoveredNames) {
|
for (const agentName of discoveredNames) {
|
||||||
const definition = this.agentRegistry.getDiscoveredDefinition(agentName);
|
const definition = this.agentRegistry.getDiscoveredDefinition(agentName);
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
@@ -3630,7 +3633,17 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tool = new SubagentTool(definition, this, this.messageBus);
|
let descriptionOverride = definition.description;
|
||||||
|
if (teamAgentNames.has(definition.name)) {
|
||||||
|
descriptionOverride = `(Team Agent: ${activeTeam?.displayName}) ${definition.description}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tool = new SubagentTool(
|
||||||
|
definition,
|
||||||
|
this,
|
||||||
|
this.messageBus,
|
||||||
|
descriptionOverride,
|
||||||
|
);
|
||||||
registry.registerTool(tool);
|
registry.registerTool(tool);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
debugLogger.warn(
|
debugLogger.warn(
|
||||||
@@ -3730,8 +3743,10 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onAgentsRefreshed = async () => {
|
private onAgentsRefreshed = async () => {
|
||||||
|
debugLogger.log('[Config] onAgentsRefreshed triggered');
|
||||||
await this.teamRegistry.reload();
|
await this.teamRegistry.reload();
|
||||||
if (this._toolRegistry) {
|
if (this._toolRegistry) {
|
||||||
|
debugLogger.log('[Config] Re-registering sub-agent tools');
|
||||||
this.registerSubAgentTools(this._toolRegistry);
|
this.registerSubAgentTools(this._toolRegistry);
|
||||||
}
|
}
|
||||||
// Propagate updates to the active chat session
|
// Propagate updates to the active chat session
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { PromptProvider } from './promptProvider.js';
|
||||||
|
import { type AgentLoopContext } from '../config/agent-loop-context.js';
|
||||||
|
import { type TeamDefinition } from '../agents/types.js';
|
||||||
|
|
||||||
|
describe('PromptProvider with Agent Teams', () => {
|
||||||
|
let promptProvider: PromptProvider;
|
||||||
|
let mockContext: AgentLoopContext;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
promptProvider = new PromptProvider();
|
||||||
|
mockContext = {
|
||||||
|
config: {
|
||||||
|
isInteractive: vi.fn().mockReturnValue(true),
|
||||||
|
getSkillManager: vi.fn().mockReturnValue({ getSkills: () => [] }),
|
||||||
|
getAgentRegistry: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ getAllDefinitions: () => [] }),
|
||||||
|
getActiveModel: vi.fn().mockReturnValue('gemini-3.1-pro-preview'),
|
||||||
|
getActiveTeam: vi.fn().mockReturnValue(undefined),
|
||||||
|
getApprovedPlanPath: vi.fn().mockReturnValue(undefined),
|
||||||
|
getApprovalMode: vi.fn().mockReturnValue('default'),
|
||||||
|
getGemini31LaunchedSync: vi.fn().mockReturnValue(true),
|
||||||
|
getGemini31FlashLiteLaunchedSync: vi.fn().mockReturnValue(true),
|
||||||
|
getHasAccessToPreviewModel: vi.fn().mockReturnValue(true),
|
||||||
|
isTrackerEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
isTopicUpdateNarrationEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
isMemoryManagerEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
isInteractiveShellEnabled: vi.fn().mockReturnValue(true),
|
||||||
|
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
|
||||||
|
getSandboxEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
storage: {
|
||||||
|
getPlansDir: vi.fn().mockReturnValue('/tmp/plans'),
|
||||||
|
},
|
||||||
|
topicState: {
|
||||||
|
getTopic: vi.fn().mockReturnValue(undefined),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toolRegistry: {
|
||||||
|
getAllToolNames: vi.fn().mockReturnValue([]),
|
||||||
|
getAllTools: vi.fn().mockReturnValue([]),
|
||||||
|
},
|
||||||
|
} as unknown as AgentLoopContext;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include team section when no team is active', () => {
|
||||||
|
const prompt = promptProvider.getCoreSystemPrompt(
|
||||||
|
mockContext as unknown as AgentLoopContext,
|
||||||
|
);
|
||||||
|
expect(prompt).not.toContain('# Active Agent Team');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include team section when a team is active', () => {
|
||||||
|
const mockTeam: TeamDefinition = {
|
||||||
|
name: 'test-team',
|
||||||
|
displayName: 'Test Team',
|
||||||
|
description: 'A test team',
|
||||||
|
instructions: 'These are the team instructions.',
|
||||||
|
agents: [],
|
||||||
|
};
|
||||||
|
mockContext.config.getActiveTeam.mockReturnValue(mockTeam);
|
||||||
|
|
||||||
|
const prompt = promptProvider.getCoreSystemPrompt(
|
||||||
|
mockContext as unknown as AgentLoopContext,
|
||||||
|
);
|
||||||
|
expect(prompt).toContain('# Active Agent Team: Test Team');
|
||||||
|
expect(prompt).toContain('These are the team instructions.');
|
||||||
|
expect(prompt).toContain(
|
||||||
|
"You should prioritize delegating tasks to this team's agents whenever appropriate.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -140,6 +140,7 @@ export class PromptProvider {
|
|||||||
contextFilenames,
|
contextFilenames,
|
||||||
topicUpdateNarration: context.config.isTopicUpdateNarrationEnabled(),
|
topicUpdateNarration: context.config.isTopicUpdateNarrationEnabled(),
|
||||||
})),
|
})),
|
||||||
|
activeTeam: context.config.getActiveTeam(),
|
||||||
subAgents: this.withSection('agentContexts', () =>
|
subAgents: this.withSection('agentContexts', () =>
|
||||||
context.config
|
context.config
|
||||||
.getAgentRegistry()
|
.getAgentRegistry()
|
||||||
|
|||||||
@@ -36,12 +36,14 @@ import {
|
|||||||
} from '../tools/tool-names.js';
|
} from '../tools/tool-names.js';
|
||||||
import type { HierarchicalMemory } from '../config/memory.js';
|
import type { HierarchicalMemory } from '../config/memory.js';
|
||||||
import { DEFAULT_CONTEXT_FILENAME } from '../tools/memoryTool.js';
|
import { DEFAULT_CONTEXT_FILENAME } from '../tools/memoryTool.js';
|
||||||
|
import { type TeamDefinition } from '../agents/types.js';
|
||||||
|
|
||||||
// --- Options Structs ---
|
// --- Options Structs ---
|
||||||
|
|
||||||
export interface SystemPromptOptions {
|
export interface SystemPromptOptions {
|
||||||
preamble?: PreambleOptions;
|
preamble?: PreambleOptions;
|
||||||
coreMandates?: CoreMandatesOptions;
|
coreMandates?: CoreMandatesOptions;
|
||||||
|
activeTeam?: TeamDefinition;
|
||||||
subAgents?: SubAgentOptions[];
|
subAgents?: SubAgentOptions[];
|
||||||
agentSkills?: AgentSkillOptions[];
|
agentSkills?: AgentSkillOptions[];
|
||||||
hookContext?: boolean;
|
hookContext?: boolean;
|
||||||
@@ -126,6 +128,8 @@ ${renderPreamble(options.preamble)}
|
|||||||
|
|
||||||
${renderCoreMandates(options.coreMandates)}
|
${renderCoreMandates(options.coreMandates)}
|
||||||
|
|
||||||
|
${renderActiveTeam(options.activeTeam)}
|
||||||
|
|
||||||
${renderSubAgents(options.subAgents)}
|
${renderSubAgents(options.subAgents)}
|
||||||
|
|
||||||
${renderAgentSkills(options.agentSkills)}
|
${renderAgentSkills(options.agentSkills)}
|
||||||
@@ -247,6 +251,16 @@ Use the following guidelines to optimize your search and read patterns.
|
|||||||
`.trim();
|
`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function renderActiveTeam(team?: TeamDefinition): string {
|
||||||
|
if (!team) return '';
|
||||||
|
return `
|
||||||
|
# Active Agent Team: ${team.displayName}
|
||||||
|
|
||||||
|
${team.instructions}
|
||||||
|
|
||||||
|
You should prioritize delegating tasks to this team's agents whenever appropriate.`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
export function renderSubAgents(subAgents?: SubAgentOptions[]): string {
|
export function renderSubAgents(subAgents?: SubAgentOptions[]): string {
|
||||||
if (!subAgents || subAgents.length === 0) return '';
|
if (!subAgents || subAgents.length === 0) return '';
|
||||||
const subAgentsXml = subAgents
|
const subAgentsXml = subAgents
|
||||||
|
|||||||
Reference in New Issue
Block a user