mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-18 07:43:00 -07:00
feat(core): implement core infrastructure for Agent Teams
- Add TeamDefinition interface to define agent team structure - Implement TeamLoader for discovering and parsing TEAM.md files - Implement TeamRegistry for team session management and agent registration - Integrate TeamRegistry into Config and Storage services - Add unit tests for team loader and registry
This commit is contained in:
@@ -317,7 +317,7 @@ export class AgentRegistry {
|
||||
* it will be overwritten, respecting the precedence established by the
|
||||
* initialization order.
|
||||
*/
|
||||
protected async registerAgent<TOutput extends z.ZodTypeAny>(
|
||||
async registerAgent<TOutput extends z.ZodTypeAny>(
|
||||
definition: AgentDefinition<TOutput>,
|
||||
): Promise<void> {
|
||||
if (definition.kind === 'local') {
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 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 { loadTeamsFromDirectory } from './teamLoader.js';
|
||||
|
||||
describe('teamLoader', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'team-test-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (tempDir) {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
async function createTeamStructure(
|
||||
teamName: string,
|
||||
teamMdContent: string,
|
||||
agents: Record<string, string> = {},
|
||||
) {
|
||||
const teamPath = path.join(tempDir, teamName);
|
||||
await fs.mkdir(teamPath, { recursive: true });
|
||||
await fs.writeFile(path.join(teamPath, 'TEAM.md'), teamMdContent);
|
||||
|
||||
if (Object.keys(agents).length > 0) {
|
||||
const agentsPath = path.join(teamPath, 'agents');
|
||||
await fs.mkdir(agentsPath, { recursive: true });
|
||||
for (const [name, content] of Object.entries(agents)) {
|
||||
await fs.writeFile(path.join(agentsPath, `${name}.md`), content);
|
||||
}
|
||||
}
|
||||
return teamPath;
|
||||
}
|
||||
|
||||
it('should load a valid team with agents', async () => {
|
||||
await createTeamStructure(
|
||||
'my-team',
|
||||
`---
|
||||
name: my-team
|
||||
display_name: My Team
|
||||
description: A great team
|
||||
---
|
||||
Team instructions here.`,
|
||||
{
|
||||
'agent-1': `---
|
||||
name: agent-1
|
||||
description: Agent 1
|
||||
---
|
||||
Agent 1 prompt`,
|
||||
},
|
||||
);
|
||||
|
||||
const result = await loadTeamsFromDirectory(tempDir);
|
||||
expect(result.teams).toHaveLength(1);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
|
||||
const team = result.teams[0];
|
||||
expect(team.name).toBe('my-team');
|
||||
expect(team.displayName).toBe('My Team');
|
||||
expect(team.description).toBe('A great team');
|
||||
expect(team.instructions).toBe('Team instructions here.');
|
||||
expect(team.agents).toHaveLength(1);
|
||||
expect(team.agents[0].name).toBe('agent-1');
|
||||
expect(team.metadata?.filePath).toContain('my-team/TEAM.md');
|
||||
});
|
||||
|
||||
it('should skip directories without TEAM.md', async () => {
|
||||
await fs.mkdir(path.join(tempDir, 'not-a-team'), { recursive: true });
|
||||
await fs.writeFile(path.join(tempDir, 'not-a-team', 'README.md'), 'test');
|
||||
|
||||
const result = await loadTeamsFromDirectory(tempDir);
|
||||
expect(result.teams).toHaveLength(0);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle multiple teams', async () => {
|
||||
await createTeamStructure(
|
||||
'team-1',
|
||||
`---
|
||||
name: team-1
|
||||
display_name: Team One
|
||||
description: First team
|
||||
---
|
||||
Instructions 1`,
|
||||
);
|
||||
await createTeamStructure(
|
||||
'team-2',
|
||||
`---
|
||||
name: team-2
|
||||
display_name: Team Two
|
||||
description: Second team
|
||||
---
|
||||
Instructions 2`,
|
||||
);
|
||||
|
||||
const result = await loadTeamsFromDirectory(tempDir);
|
||||
expect(result.teams).toHaveLength(2);
|
||||
const names = result.teams.map((t) => t.name).sort();
|
||||
expect(names).toEqual(['team-1', 'team-2']);
|
||||
});
|
||||
|
||||
it('should capture errors for malformed TEAM.md', async () => {
|
||||
const teamPath = path.join(tempDir, 'bad-team');
|
||||
await fs.mkdir(teamPath, { recursive: true });
|
||||
await fs.writeFile(path.join(teamPath, 'TEAM.md'), 'invalid content');
|
||||
|
||||
const result = await loadTeamsFromDirectory(tempDir);
|
||||
expect(result.teams).toHaveLength(0);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
expect(result.errors[0].message).toContain(
|
||||
'Missing mandatory YAML frontmatter',
|
||||
);
|
||||
});
|
||||
|
||||
it('should capture validation errors for TEAM.md', async () => {
|
||||
await createTeamStructure(
|
||||
'invalid-team',
|
||||
`---
|
||||
name: invalid-team
|
||||
# missing display_name and description
|
||||
---
|
||||
Instructions`,
|
||||
);
|
||||
|
||||
const result = await loadTeamsFromDirectory(tempDir);
|
||||
expect(result.teams).toHaveLength(0);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
expect(result.errors[0].message).toContain('Validation failed');
|
||||
});
|
||||
|
||||
it('should load team even if agents subfolder is missing', async () => {
|
||||
await createTeamStructure(
|
||||
'no-agents-team',
|
||||
`---
|
||||
name: no-agents-team
|
||||
display_name: No Agents
|
||||
description: No agents here
|
||||
---
|
||||
Instructions`,
|
||||
);
|
||||
|
||||
const result = await loadTeamsFromDirectory(tempDir);
|
||||
expect(result.teams).toHaveLength(1);
|
||||
expect(result.teams[0].agents).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should capture errors from agents subfolder', async () => {
|
||||
await createTeamStructure(
|
||||
'bad-agents-team',
|
||||
`---
|
||||
name: bad-agents-team
|
||||
display_name: Bad Agents
|
||||
description: Team with bad agents
|
||||
---
|
||||
Instructions`,
|
||||
{
|
||||
'bad-agent': 'invalid agent content',
|
||||
},
|
||||
);
|
||||
|
||||
const result = await loadTeamsFromDirectory(tempDir);
|
||||
expect(result.teams).toHaveLength(1);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
expect(result.errors[0].filePath).toContain('agents/bad-agent.md');
|
||||
});
|
||||
|
||||
it('should return empty result if directory does not exist', async () => {
|
||||
const nonExistentDir = path.join(tempDir, 'void');
|
||||
const result = await loadTeamsFromDirectory(nonExistentDir);
|
||||
expect(result.teams).toHaveLength(0);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { load } from 'js-yaml';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { type Dirent } from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as crypto from 'node:crypto';
|
||||
import { z } from 'zod';
|
||||
import { type TeamDefinition } from './types.js';
|
||||
import { AgentLoadError, loadAgentsFromDirectory } from './agentLoader.js';
|
||||
import { FRONTMATTER_REGEX } from '../skills/skillLoader.js';
|
||||
import { getErrorMessage } from '../utils/errors.js';
|
||||
|
||||
/**
|
||||
* Result of loading teams from a directory.
|
||||
*/
|
||||
export interface TeamLoadResult {
|
||||
teams: TeamDefinition[];
|
||||
errors: AgentLoadError[];
|
||||
}
|
||||
|
||||
const nameSchema = z
|
||||
.string()
|
||||
.regex(/^[a-z0-9-_]+$/, 'Name must be a valid slug');
|
||||
|
||||
const teamSchema = z
|
||||
.object({
|
||||
name: nameSchema,
|
||||
display_name: z.string().min(1),
|
||||
description: z.string().min(1),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/**
|
||||
* Loads all teams from a specific directory.
|
||||
* Each subdirectory is treated as a potential team.
|
||||
*
|
||||
* @param dir Directory path to scan (e.g., .gemini/teams/).
|
||||
* @returns Object containing successfully loaded teams and any errors.
|
||||
*/
|
||||
export async function loadTeamsFromDirectory(
|
||||
dir: string,
|
||||
): Promise<TeamLoadResult> {
|
||||
const result: TeamLoadResult = {
|
||||
teams: [],
|
||||
errors: [],
|
||||
};
|
||||
|
||||
let dirEntries: Dirent[];
|
||||
try {
|
||||
dirEntries = await fs.readdir(dir, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
// If directory doesn't exist, just return empty
|
||||
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
||||
return result;
|
||||
}
|
||||
result.errors.push(
|
||||
new AgentLoadError(
|
||||
dir,
|
||||
`Could not list directory: ${getErrorMessage(error)}`,
|
||||
),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
const teamDirs = dirEntries.filter((entry) => entry.isDirectory());
|
||||
|
||||
for (const entry of teamDirs) {
|
||||
const teamPath = path.join(dir, entry.name);
|
||||
const teamMdPath = path.join(teamPath, 'TEAM.md');
|
||||
const agentsDirPath = path.join(teamPath, 'agents');
|
||||
|
||||
try {
|
||||
// Check if TEAM.md exists
|
||||
try {
|
||||
await fs.access(teamMdPath);
|
||||
} catch {
|
||||
// Not a team directory (missing TEAM.md), just skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
const teamMdContent = await fs.readFile(teamMdPath, 'utf-8');
|
||||
const hash = crypto
|
||||
.createHash('sha256')
|
||||
.update(teamMdContent)
|
||||
.digest('hex');
|
||||
|
||||
// Parse TEAM.md
|
||||
const match = teamMdContent.match(FRONTMATTER_REGEX);
|
||||
if (!match) {
|
||||
throw new AgentLoadError(
|
||||
teamMdPath,
|
||||
'Invalid team definition: Missing mandatory YAML frontmatter.',
|
||||
);
|
||||
}
|
||||
|
||||
const frontmatterStr = match[1];
|
||||
const instructions = (match[2] || '').trim();
|
||||
|
||||
let rawFrontmatter: unknown;
|
||||
try {
|
||||
rawFrontmatter = load(frontmatterStr);
|
||||
} catch (error) {
|
||||
throw new AgentLoadError(
|
||||
teamMdPath,
|
||||
`YAML frontmatter parsing failed: ${getErrorMessage(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const parsedFrontmatter = teamSchema.safeParse(rawFrontmatter);
|
||||
if (!parsedFrontmatter.success) {
|
||||
throw new AgentLoadError(
|
||||
teamMdPath,
|
||||
`Validation failed:\n${parsedFrontmatter.error.issues
|
||||
.map((i) => `${i.path.join('.')}: ${i.message}`)
|
||||
.join('\n')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { name, display_name, description } = parsedFrontmatter.data;
|
||||
|
||||
// Load agents from agents/ subfolder
|
||||
const agentsResult = await loadAgentsFromDirectory(agentsDirPath);
|
||||
result.errors.push(...agentsResult.errors);
|
||||
|
||||
result.teams.push({
|
||||
name,
|
||||
displayName: display_name,
|
||||
description,
|
||||
instructions,
|
||||
agents: agentsResult.agents,
|
||||
metadata: {
|
||||
hash,
|
||||
filePath: teamMdPath,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AgentLoadError) {
|
||||
result.errors.push(error);
|
||||
} else {
|
||||
result.errors.push(
|
||||
new AgentLoadError(
|
||||
teamMdPath,
|
||||
`Unexpected error: ${getErrorMessage(error)}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { TeamRegistry } from './teamRegistry.js';
|
||||
import { type Config } from '../config/config.js';
|
||||
import { type AgentRegistry } from './registry.js';
|
||||
import { loadTeamsFromDirectory } from './teamLoader.js';
|
||||
import { type TeamDefinition, type AgentDefinition } from './types.js';
|
||||
|
||||
vi.mock('./teamLoader.js', () => ({
|
||||
loadTeamsFromDirectory: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('TeamRegistry', () => {
|
||||
let mockConfig: Config;
|
||||
let mockAgentRegistry: AgentRegistry;
|
||||
let registry: TeamRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfig = {
|
||||
isAgentsEnabled: vi.fn().mockReturnValue(true),
|
||||
getFolderTrust: vi.fn().mockReturnValue(false),
|
||||
isTrustedFolder: vi.fn().mockReturnValue(true),
|
||||
getProjectRoot: vi.fn().mockReturnValue('/mock/project'),
|
||||
getDebugMode: vi.fn().mockReturnValue(false),
|
||||
storage: {
|
||||
getProjectTeamsDir: vi
|
||||
.fn()
|
||||
.mockReturnValue('/mock/project/.gemini/teams'),
|
||||
},
|
||||
} as unknown as Config;
|
||||
|
||||
mockAgentRegistry = {
|
||||
registerAgent: vi.fn().mockResolvedValue(undefined),
|
||||
} as unknown as AgentRegistry;
|
||||
|
||||
registry = new TeamRegistry(mockConfig, mockAgentRegistry);
|
||||
vi.mocked(loadTeamsFromDirectory).mockReset();
|
||||
});
|
||||
|
||||
it('should load teams and register agents on initialize', async () => {
|
||||
const mockAgent: AgentDefinition = {
|
||||
kind: 'local',
|
||||
name: 'team-agent',
|
||||
description: 'Agent in a team',
|
||||
inputConfig: { inputSchema: { type: 'object' } },
|
||||
} as unknown as AgentDefinition;
|
||||
|
||||
const mockTeam: TeamDefinition = {
|
||||
name: 'test-team',
|
||||
displayName: 'Test Team',
|
||||
description: 'A team for testing',
|
||||
instructions: 'Do tests.',
|
||||
agents: [mockAgent],
|
||||
};
|
||||
|
||||
vi.mocked(loadTeamsFromDirectory).mockResolvedValue({
|
||||
teams: [mockTeam],
|
||||
errors: [],
|
||||
});
|
||||
|
||||
await registry.initialize();
|
||||
|
||||
expect(registry.getAllTeams()).toHaveLength(1);
|
||||
expect(registry.getTeam('test-team')).toEqual(mockTeam);
|
||||
expect(mockAgentRegistry.registerAgent).toHaveBeenCalledWith(mockAgent);
|
||||
});
|
||||
|
||||
it('should not load teams if agents are disabled', async () => {
|
||||
mockConfig.isAgentsEnabled.mockReturnValue(false);
|
||||
|
||||
await registry.initialize();
|
||||
|
||||
expect(loadTeamsFromDirectory).not.toHaveBeenCalled();
|
||||
expect(registry.getAllTeams()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should skip project teams in untrusted folder', async () => {
|
||||
mockConfig.getFolderTrust.mockReturnValue(true);
|
||||
mockConfig.isTrustedFolder.mockReturnValue(false);
|
||||
|
||||
await registry.initialize();
|
||||
|
||||
expect(loadTeamsFromDirectory).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should manage active team', async () => {
|
||||
const mockTeam: TeamDefinition = {
|
||||
name: 'active-team',
|
||||
displayName: 'Active Team',
|
||||
description: 'The active one',
|
||||
instructions: 'Lead.',
|
||||
agents: [],
|
||||
};
|
||||
|
||||
vi.mocked(loadTeamsFromDirectory).mockResolvedValue({
|
||||
teams: [mockTeam],
|
||||
errors: [],
|
||||
});
|
||||
|
||||
await registry.initialize();
|
||||
|
||||
expect(registry.getActiveTeam()).toBeUndefined();
|
||||
|
||||
registry.setActiveTeam('active-team');
|
||||
expect(registry.getActiveTeam()).toEqual(mockTeam);
|
||||
|
||||
expect(() => registry.setActiveTeam('non-existent')).toThrow(
|
||||
'Team not found',
|
||||
);
|
||||
});
|
||||
|
||||
it('should reload teams', async () => {
|
||||
vi.mocked(loadTeamsFromDirectory).mockResolvedValue({
|
||||
teams: [],
|
||||
errors: [],
|
||||
});
|
||||
|
||||
await registry.initialize();
|
||||
expect(loadTeamsFromDirectory).toHaveBeenCalledTimes(1);
|
||||
|
||||
await registry.reload();
|
||||
expect(loadTeamsFromDirectory).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { type TeamDefinition } from './types.js';
|
||||
import { type Config } from '../config/config.js';
|
||||
import { type AgentRegistry } from './registry.js';
|
||||
import { loadTeamsFromDirectory } from './teamLoader.js';
|
||||
import { coreEvents } from '../utils/events.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
|
||||
/**
|
||||
* Manages the discovery, loading, and active state of Agent Teams.
|
||||
*/
|
||||
export class TeamRegistry {
|
||||
private readonly teams = new Map<string, TeamDefinition>();
|
||||
private activeTeamName?: string;
|
||||
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly agentRegistry: AgentRegistry,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Discovers and loads teams from the project's .gemini/teams/ directory.
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
await this.loadTeams();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current registry and re-scans for teams.
|
||||
*/
|
||||
async reload(): Promise<void> {
|
||||
await this.loadTeams();
|
||||
}
|
||||
|
||||
private async loadTeams(): Promise<void> {
|
||||
this.teams.clear();
|
||||
|
||||
if (!this.config.isAgentsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const folderTrustEnabled = this.config.getFolderTrust();
|
||||
const isTrustedFolder = this.config.isTrustedFolder();
|
||||
|
||||
if (folderTrustEnabled && !isTrustedFolder) {
|
||||
debugLogger.log(
|
||||
'[TeamRegistry] Skipping project teams due to untrusted folder.',
|
||||
);
|
||||
coreEvents.emitFeedback(
|
||||
'info',
|
||||
'Skipping project teams due to untrusted folder. To enable, ensure that the project root is trusted.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const projectTeamsDir = this.config.storage.getProjectTeamsDir();
|
||||
const loadResult = await loadTeamsFromDirectory(projectTeamsDir);
|
||||
|
||||
for (const error of loadResult.errors) {
|
||||
debugLogger.warn(`[TeamRegistry] Error loading team: ${error.message}`);
|
||||
coreEvents.emitFeedback('error', `Team loading error: ${error.message}`);
|
||||
}
|
||||
|
||||
for (const team of loadResult.teams) {
|
||||
this.teams.set(team.name, team);
|
||||
|
||||
// Register team agents in the global AgentRegistry so they are available as SubagentTools
|
||||
for (const agent of team.agents) {
|
||||
try {
|
||||
await this.agentRegistry.registerAgent(agent);
|
||||
} catch (e) {
|
||||
debugLogger.warn(
|
||||
`[TeamRegistry] Error registering agent "${agent.name}" from team "${team.name}":`,
|
||||
e,
|
||||
);
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`Error registering agent "${agent.name}" from team "${team.name}": ${e instanceof Error ? e.message : String(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.getDebugMode()) {
|
||||
debugLogger.log(`[TeamRegistry] Loaded with ${this.teams.size} teams.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all loaded teams.
|
||||
*/
|
||||
getAllTeams(): TeamDefinition[] {
|
||||
return Array.from(this.teams.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current active team.
|
||||
* @param name The slug (name) of the team to activate.
|
||||
* @throws Error if the team is not found.
|
||||
*/
|
||||
setActiveTeam(name: string): void {
|
||||
if (this.teams.has(name)) {
|
||||
this.activeTeamName = name;
|
||||
} else {
|
||||
throw new Error(`Team not found: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently active team definition, if any.
|
||||
*/
|
||||
getActiveTeam(): TeamDefinition | undefined {
|
||||
return this.activeTeamName
|
||||
? this.teams.get(this.activeTeamName)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a team definition by name.
|
||||
*/
|
||||
getTeam(name: string): TeamDefinition | undefined {
|
||||
return this.teams.get(name);
|
||||
}
|
||||
}
|
||||
@@ -340,3 +340,26 @@ export interface RunConfig {
|
||||
*/
|
||||
maxTurns?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a team of agents orchestrated by a set of instructions.
|
||||
*/
|
||||
export interface TeamDefinition {
|
||||
/** The directory name (slug) of the team. */
|
||||
name: string;
|
||||
/** The human-readable name of the team. */
|
||||
displayName: string;
|
||||
/** A short description of the team's purpose. */
|
||||
description: string;
|
||||
/** Orchestration instructions for the top-level agent. */
|
||||
instructions: string;
|
||||
/** The list of agents that comprise the team. */
|
||||
agents: AgentDefinition[];
|
||||
/** Optional metadata for the team definition. */
|
||||
metadata?: {
|
||||
/** SHA-256 hash of the TEAM.md content. */
|
||||
hash?: string;
|
||||
/** Absolute path to the TEAM.md file. */
|
||||
filePath?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150,6 +150,7 @@ import {
|
||||
type Experiments,
|
||||
} from '../code_assist/experiments/experiments.js';
|
||||
import { AgentRegistry } from '../agents/registry.js';
|
||||
import { TeamRegistry } from '../agents/teamRegistry.js';
|
||||
import { AcknowledgedAgentsService } from '../agents/acknowledgedAgents.js';
|
||||
import { setGlobalProxy } from '../utils/fetch.js';
|
||||
import { SubagentTool } from '../agents/subagent-tool.js';
|
||||
@@ -752,6 +753,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private _promptRegistry!: PromptRegistry;
|
||||
private _resourceRegistry!: ResourceRegistry;
|
||||
private agentRegistry!: AgentRegistry;
|
||||
private teamRegistry!: TeamRegistry;
|
||||
private readonly acknowledgedAgentsService: AcknowledgedAgentsService;
|
||||
private skillManager!: SkillManager;
|
||||
private _sessionId: string;
|
||||
@@ -1431,6 +1433,9 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.agentRegistry = new AgentRegistry(this);
|
||||
await this.agentRegistry.initialize();
|
||||
|
||||
this.teamRegistry = new TeamRegistry(this, this.agentRegistry);
|
||||
await this.teamRegistry.initialize();
|
||||
|
||||
coreEvents.on(CoreEvent.AgentsRefreshed, this.onAgentsRefreshed);
|
||||
|
||||
this._toolRegistry = await this.createToolRegistry();
|
||||
@@ -2026,6 +2031,10 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.agentRegistry;
|
||||
}
|
||||
|
||||
getTeamRegistry(): TeamRegistry {
|
||||
return this.teamRegistry;
|
||||
}
|
||||
|
||||
getAcknowledgedAgentsService(): AcknowledgedAgentsService {
|
||||
return this.acknowledgedAgentsService;
|
||||
}
|
||||
|
||||
@@ -291,6 +291,10 @@ export class Storage {
|
||||
return path.join(this.getGeminiDir(), 'agents');
|
||||
}
|
||||
|
||||
getProjectTeamsDir(): string {
|
||||
return path.join(this.getGeminiDir(), 'teams');
|
||||
}
|
||||
|
||||
getProjectTempCheckpointsDir(): string {
|
||||
return path.join(this.getProjectTempDir(), 'checkpoints');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user