mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-02 22:26:50 -07:00
feat(core,cli): implement external agent polyfills and finalize teams
- Implement External Agent kind with personality overlays (Claude Code, Codex) - Enhance AgentLoader and Registry for external agent discovery and registration - Fix critical startup race condition in Config registry initialization - Add TeamSelectionDialog and ActiveTeamIndicator to CLI UI - Include comprehensive integration and unit tests for agent teams - Refactor SubagentToolWrapper for type-safe external agent invocation
This commit is contained in:
@@ -211,9 +211,35 @@ const remoteAgentSchema = z.union([
|
||||
|
||||
type FrontmatterRemoteAgentDefinition = z.infer<typeof remoteAgentSchema>;
|
||||
|
||||
const externalAgentSchema = z
|
||||
.object({
|
||||
kind: z.literal('external'),
|
||||
name: nameSchema,
|
||||
provider: z.string().min(1),
|
||||
description: z.string().min(1),
|
||||
display_name: z.string().optional(),
|
||||
provider_config: z.record(z.any()).optional(),
|
||||
instructions: z.string().optional(),
|
||||
tools: z
|
||||
|
||||
.array(
|
||||
z
|
||||
.string()
|
||||
.refine((val) => isValidToolName(val, { allowWildcards: true }), {
|
||||
message: 'Invalid tool name',
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
mcp_servers: z.record(mcpServerSchema).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
type FrontmatterExternalAgentDefinition = z.infer<typeof externalAgentSchema>;
|
||||
|
||||
type FrontmatterAgentDefinition =
|
||||
| FrontmatterLocalAgentDefinition
|
||||
| FrontmatterRemoteAgentDefinition;
|
||||
| FrontmatterRemoteAgentDefinition
|
||||
| FrontmatterExternalAgentDefinition;
|
||||
|
||||
const agentUnionOptions = [
|
||||
{ label: 'Local Agent' },
|
||||
@@ -222,11 +248,10 @@ const agentUnionOptions = [
|
||||
];
|
||||
|
||||
const remoteAgentsListSchema = z.array(remoteAgentSchema);
|
||||
|
||||
const markdownFrontmatterSchema = z.union([
|
||||
localAgentSchema,
|
||||
remoteAgentUrlSchema,
|
||||
remoteAgentJsonSchema,
|
||||
remoteAgentSchema,
|
||||
externalAgentSchema,
|
||||
]);
|
||||
|
||||
function guessIntendedKind(rawInput: unknown): 'local' | 'remote' | undefined {
|
||||
@@ -368,6 +393,17 @@ export async function parseAgentMarkdown(
|
||||
];
|
||||
}
|
||||
|
||||
if (frontmatter.kind === 'external') {
|
||||
const instr = body.trim();
|
||||
return [
|
||||
{
|
||||
...frontmatter,
|
||||
kind: 'external',
|
||||
instructions: instr,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Construct the local agent definition
|
||||
return [
|
||||
{
|
||||
@@ -474,91 +510,147 @@ export function markdownToAgentDefinition(
|
||||
},
|
||||
};
|
||||
|
||||
if (markdown.kind === 'remote') {
|
||||
const base: RemoteAgentDefinition = {
|
||||
kind: 'remote',
|
||||
name: markdown.name,
|
||||
description: markdown.description || '',
|
||||
displayName: markdown.display_name,
|
||||
auth: markdown.auth
|
||||
? convertFrontmatterAuthToConfig(markdown.auth)
|
||||
: undefined,
|
||||
inputConfig,
|
||||
metadata,
|
||||
};
|
||||
switch (markdown.kind) {
|
||||
case 'remote': {
|
||||
const base: RemoteAgentDefinition = {
|
||||
kind: 'remote',
|
||||
name: markdown.name,
|
||||
description: markdown.description || '',
|
||||
displayName: markdown.display_name,
|
||||
auth: markdown.auth
|
||||
? convertFrontmatterAuthToConfig(markdown.auth)
|
||||
: undefined,
|
||||
inputConfig,
|
||||
metadata,
|
||||
};
|
||||
|
||||
if (
|
||||
'agent_card_json' in markdown &&
|
||||
markdown.agent_card_json !== undefined
|
||||
) {
|
||||
base.agentCardJson = markdown.agent_card_json;
|
||||
return base;
|
||||
}
|
||||
if ('agent_card_url' in markdown && markdown.agent_card_url !== undefined) {
|
||||
base.agentCardUrl = markdown.agent_card_url;
|
||||
return base;
|
||||
if (
|
||||
'agent_card_json' in markdown &&
|
||||
markdown.agent_card_json !== undefined
|
||||
) {
|
||||
base.agentCardJson = markdown.agent_card_json;
|
||||
return base;
|
||||
}
|
||||
if (
|
||||
'agent_card_url' in markdown &&
|
||||
markdown.agent_card_url !== undefined
|
||||
) {
|
||||
base.agentCardUrl = markdown.agent_card_url;
|
||||
return base;
|
||||
}
|
||||
|
||||
throw new AgentLoadError(
|
||||
metadata?.filePath || 'unknown',
|
||||
'Unexpected state: neither agent_card_json nor agent_card_url present on remote agent',
|
||||
);
|
||||
}
|
||||
|
||||
throw new AgentLoadError(
|
||||
metadata?.filePath || 'unknown',
|
||||
'Unexpected state: neither agent_card_json nor agent_card_url present on remote agent',
|
||||
);
|
||||
}
|
||||
case 'external': {
|
||||
const mcpServers: Record<string, MCPServerConfig> = {};
|
||||
if (markdown.mcp_servers) {
|
||||
for (const [name, config] of Object.entries(markdown.mcp_servers)) {
|
||||
mcpServers[name] = new MCPServerConfig(
|
||||
config.command,
|
||||
config.args,
|
||||
config.env,
|
||||
config.cwd,
|
||||
config.url,
|
||||
config.http_url,
|
||||
config.headers,
|
||||
config.tcp,
|
||||
config.type,
|
||||
config.timeout,
|
||||
config.trust,
|
||||
config.description,
|
||||
config.include_tools,
|
||||
config.exclude_tools,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If a model is specified, use it. Otherwise, inherit
|
||||
const modelName = markdown.model || 'inherit';
|
||||
return {
|
||||
kind: 'external',
|
||||
name: markdown.name,
|
||||
provider: markdown.provider,
|
||||
description: markdown.description,
|
||||
displayName: markdown.display_name,
|
||||
providerConfig: markdown.provider_config,
|
||||
instructions: markdown.instructions,
|
||||
toolConfig: markdown.tools
|
||||
? {
|
||||
tools: markdown.tools,
|
||||
}
|
||||
: undefined,
|
||||
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined,
|
||||
inputConfig,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
||||
const mcpServers: Record<string, MCPServerConfig> = {};
|
||||
if (markdown.mcp_servers) {
|
||||
for (const [name, config] of Object.entries(markdown.mcp_servers)) {
|
||||
mcpServers[name] = new MCPServerConfig(
|
||||
config.command,
|
||||
config.args,
|
||||
config.env,
|
||||
config.cwd,
|
||||
config.url,
|
||||
config.http_url,
|
||||
config.headers,
|
||||
config.tcp,
|
||||
config.type,
|
||||
config.timeout,
|
||||
config.trust,
|
||||
config.description,
|
||||
config.include_tools,
|
||||
config.exclude_tools,
|
||||
case 'local': {
|
||||
// If a model is specified, use it. Otherwise, inherit
|
||||
const modelName = markdown.model || 'inherit';
|
||||
|
||||
const mcpServers: Record<string, MCPServerConfig> = {};
|
||||
if (markdown.mcp_servers) {
|
||||
for (const [name, config] of Object.entries(markdown.mcp_servers)) {
|
||||
mcpServers[name] = new MCPServerConfig(
|
||||
config.command,
|
||||
config.args,
|
||||
config.env,
|
||||
config.cwd,
|
||||
config.url,
|
||||
config.http_url,
|
||||
config.headers,
|
||||
config.tcp,
|
||||
config.type,
|
||||
config.timeout,
|
||||
config.trust,
|
||||
config.description,
|
||||
config.include_tools,
|
||||
config.exclude_tools,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'local',
|
||||
name: markdown.name,
|
||||
description: markdown.description,
|
||||
displayName: markdown.display_name,
|
||||
promptConfig: {
|
||||
systemPrompt: markdown.system_prompt,
|
||||
query: '${query}',
|
||||
},
|
||||
modelConfig: {
|
||||
model: modelName,
|
||||
generateContentConfig: {
|
||||
temperature: markdown.temperature ?? 1,
|
||||
topP: 0.95,
|
||||
},
|
||||
},
|
||||
runConfig: {
|
||||
maxTurns: markdown.max_turns ?? DEFAULT_MAX_TURNS,
|
||||
maxTimeMinutes: markdown.timeout_mins ?? DEFAULT_MAX_TIME_MINUTES,
|
||||
},
|
||||
toolConfig: markdown.tools
|
||||
? {
|
||||
tools: markdown.tools,
|
||||
}
|
||||
: undefined,
|
||||
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined,
|
||||
inputConfig,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
||||
default: {
|
||||
const exhaustive: never = markdown;
|
||||
throw new Error(
|
||||
`Unsupported agent kind: ${String((exhaustive as { kind: string }).kind)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'local',
|
||||
name: markdown.name,
|
||||
description: markdown.description,
|
||||
displayName: markdown.display_name,
|
||||
promptConfig: {
|
||||
systemPrompt: markdown.system_prompt,
|
||||
query: '${query}',
|
||||
},
|
||||
modelConfig: {
|
||||
model: modelName,
|
||||
generateContentConfig: {
|
||||
temperature: markdown.temperature ?? 1,
|
||||
topP: 0.95,
|
||||
},
|
||||
},
|
||||
runConfig: {
|
||||
maxTurns: markdown.max_turns ?? DEFAULT_MAX_TURNS,
|
||||
maxTimeMinutes: markdown.timeout_mins ?? DEFAULT_MAX_TIME_MINUTES,
|
||||
},
|
||||
toolConfig: markdown.tools
|
||||
? {
|
||||
tools: markdown.tools,
|
||||
}
|
||||
: undefined,
|
||||
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined,
|
||||
inputConfig,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { TeamRegistry } from './teamRegistry.js';
|
||||
import { AgentRegistry } from './registry.js';
|
||||
import { ExternalAgentInvocation } from './external-invocation.js';
|
||||
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
|
||||
import { makeFakeConfig } from '../test-utils/config.js';
|
||||
|
||||
describe('External Agent E2E-ish Verification', () => {
|
||||
let tempGeminiDir: string;
|
||||
let config: Config;
|
||||
let agentRegistry: AgentRegistry;
|
||||
let teamRegistry: TeamRegistry;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempGeminiDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gemini-test-'));
|
||||
const projectGeminiDir = path.join(tempGeminiDir, '.gemini');
|
||||
const teamsDir = path.join(projectGeminiDir, 'teams', 'test-team');
|
||||
const agentsDir = path.join(teamsDir, 'agents');
|
||||
await fs.mkdir(agentsDir, { recursive: true });
|
||||
|
||||
// Create a real external agent file
|
||||
const claudeMd = `---
|
||||
kind: external
|
||||
name: claude-verifier
|
||||
provider: claude-code
|
||||
description: I am a Claude polyfill.
|
||||
---
|
||||
# Extra Instructions
|
||||
Be very concise.
|
||||
`;
|
||||
await fs.writeFile(path.join(agentsDir, 'claude.md'), claudeMd);
|
||||
|
||||
// Create a TEAM.md
|
||||
const teamMd = `---
|
||||
name: test-team
|
||||
display_name: Test Team
|
||||
description: A test team for external agents.
|
||||
---
|
||||
Orchestration instructions.
|
||||
`;
|
||||
await fs.writeFile(path.join(teamsDir, 'TEAM.md'), teamMd);
|
||||
|
||||
// Initialize Config using makeFakeConfig
|
||||
config = makeFakeConfig({
|
||||
targetDir: tempGeminiDir,
|
||||
cwd: tempGeminiDir,
|
||||
folderTrust: false,
|
||||
enableAgents: true,
|
||||
});
|
||||
|
||||
agentRegistry = new AgentRegistry(config);
|
||||
teamRegistry = new TeamRegistry(config, agentRegistry);
|
||||
await agentRegistry.initialize();
|
||||
await teamRegistry.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(tempGeminiDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should load external agent and apply polyfill on invocation', async () => {
|
||||
const teams = teamRegistry.getAllTeams();
|
||||
const agents = agentRegistry.getAllDefinitions();
|
||||
const agentNames = agents.map((a) => a.name);
|
||||
|
||||
expect(teams.length).toBeGreaterThan(0);
|
||||
expect(agentNames).toContain('claude-verifier');
|
||||
|
||||
const claudeAgent = agents.find((a) => a.name === 'claude-verifier');
|
||||
|
||||
expect(claudeAgent).toBeDefined();
|
||||
expect(claudeAgent?.kind).toBe('external');
|
||||
if (claudeAgent?.kind !== 'external') return;
|
||||
|
||||
expect(claudeAgent.provider).toBe('claude-code');
|
||||
|
||||
// Verify Invocation applies polyfill
|
||||
const messageBus = createMockMessageBus();
|
||||
const invocation = new ExternalAgentInvocation(
|
||||
claudeAgent,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
config as any,
|
||||
{},
|
||||
messageBus,
|
||||
);
|
||||
|
||||
// Check protected polyfilled definition via casting
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const polyfilled = (invocation as any).definition;
|
||||
expect(polyfilled.kind).toBe('local');
|
||||
expect(polyfilled.promptConfig.systemPrompt).toContain(
|
||||
'Claude Code Personality Overlay',
|
||||
);
|
||||
expect(polyfilled.promptConfig.systemPrompt).toContain('Be very concise.');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { type ExternalAgentDefinition } from './types.js';
|
||||
import {
|
||||
ExternalAgentInvocation,
|
||||
polyfillExternalAgent,
|
||||
} from './external-invocation.js';
|
||||
import { LocalAgentExecutor } from './local-executor.js';
|
||||
import { makeFakeConfig } from '../test-utils/config.js';
|
||||
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
|
||||
import { DEFAULT_GEMINI_MODEL } from '../config/models.js';
|
||||
|
||||
vi.mock('./local-executor.js');
|
||||
|
||||
const MockLocalAgentExecutor = vi.mocked(LocalAgentExecutor);
|
||||
|
||||
describe('ExternalAgentInvocation', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let mockConfig: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let mockMessageBus: any;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockConfig = makeFakeConfig();
|
||||
mockMessageBus = createMockMessageBus();
|
||||
|
||||
MockLocalAgentExecutor.create.mockResolvedValue({
|
||||
run: vi.fn().mockResolvedValue({
|
||||
result: 'Success',
|
||||
terminate_reason: 'GOAL',
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
definition: {} as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const externalDef: ExternalAgentDefinition = {
|
||||
kind: 'external',
|
||||
name: 'claude-agent',
|
||||
provider: 'claude-code',
|
||||
description: 'Expert coder.',
|
||||
inputConfig: { inputSchema: { type: 'object', properties: {} } },
|
||||
};
|
||||
|
||||
it('should polyfill external agent into a local agent definition', () => {
|
||||
const polyfilled = polyfillExternalAgent(externalDef);
|
||||
|
||||
expect(polyfilled.kind).toBe('local');
|
||||
expect(polyfilled.modelConfig.model).toBe(DEFAULT_GEMINI_MODEL);
|
||||
expect(polyfilled.promptConfig.systemPrompt).toContain(
|
||||
'Claude Code Personality Overlay',
|
||||
);
|
||||
expect(polyfilled.promptConfig.systemPrompt).toContain('Expert coder.');
|
||||
});
|
||||
|
||||
it('should support Codex provider', () => {
|
||||
const codexDef: ExternalAgentDefinition = {
|
||||
...externalDef,
|
||||
provider: 'codex',
|
||||
};
|
||||
const polyfilled = polyfillExternalAgent(codexDef);
|
||||
|
||||
expect(polyfilled.promptConfig.systemPrompt).toContain(
|
||||
'Codex Personality Overlay',
|
||||
);
|
||||
});
|
||||
|
||||
it('should include styleInstructions from providerConfig', () => {
|
||||
const customDef: ExternalAgentDefinition = {
|
||||
...externalDef,
|
||||
providerConfig: {
|
||||
styleInstructions: 'Be very formal.',
|
||||
},
|
||||
};
|
||||
const polyfilled = polyfillExternalAgent(customDef);
|
||||
|
||||
expect(polyfilled.promptConfig.systemPrompt).toContain(
|
||||
'Additional Style Instructions: Be very formal.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use ExternalAgentInvocation to execute polyfilled agent', async () => {
|
||||
const invocation = new ExternalAgentInvocation(
|
||||
externalDef,
|
||||
mockConfig,
|
||||
{},
|
||||
mockMessageBus,
|
||||
);
|
||||
|
||||
const signal = new AbortController().signal;
|
||||
await invocation.execute(signal);
|
||||
|
||||
expect(MockLocalAgentExecutor.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
kind: 'local',
|
||||
name: 'claude-agent',
|
||||
}),
|
||||
mockConfig,
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { LocalSubagentInvocation } from './local-invocation.js';
|
||||
import {
|
||||
type ExternalAgentDefinition,
|
||||
type LocalAgentDefinition,
|
||||
type AgentInputs,
|
||||
} from './types.js';
|
||||
import { type AgentLoopContext } from '../config/agent-loop-context.js';
|
||||
import { type MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import { DEFAULT_GEMINI_MODEL } from '../config/models.js';
|
||||
|
||||
/**
|
||||
* An invocation instance for an "external" agent.
|
||||
*
|
||||
* This class applies a personality overlay to a model call to mimic
|
||||
* the behavior of an external agent provider using a standard Gemini model.
|
||||
*/
|
||||
export class ExternalAgentInvocation extends LocalSubagentInvocation {
|
||||
constructor(
|
||||
definition: ExternalAgentDefinition,
|
||||
context: AgentLoopContext,
|
||||
params: AgentInputs,
|
||||
messageBus: MessageBus,
|
||||
_toolName?: string,
|
||||
_toolDisplayName?: string,
|
||||
) {
|
||||
const polyfilledDefinition = polyfillExternalAgent(definition);
|
||||
super(
|
||||
polyfilledDefinition,
|
||||
context,
|
||||
params,
|
||||
messageBus,
|
||||
_toolName,
|
||||
_toolDisplayName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms an ExternalAgentDefinition into a LocalAgentDefinition by applying
|
||||
* provider-specific personality overlays and model configurations.
|
||||
*/
|
||||
export function polyfillExternalAgent(
|
||||
def: ExternalAgentDefinition,
|
||||
): LocalAgentDefinition {
|
||||
const overlay = getPersonalityOverlay(def.provider, def.providerConfig);
|
||||
|
||||
// External agents use the instructions field (from markdown body) as their primary instructions.
|
||||
// We fall back to description if instructions are missing.
|
||||
const baseInstructions = def.instructions || def.description;
|
||||
|
||||
return {
|
||||
...def,
|
||||
kind: 'local',
|
||||
modelConfig: {
|
||||
model: DEFAULT_GEMINI_MODEL,
|
||||
},
|
||||
runConfig: {
|
||||
maxTurns: 30,
|
||||
maxTimeMinutes: 10,
|
||||
},
|
||||
promptConfig: {
|
||||
// Prepend the personality overlay to the agent's instructions.
|
||||
systemPrompt: `${overlay}\n\n${baseInstructions}`,
|
||||
query: '${query}',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a personality overlay prompt based on the external provider.
|
||||
*/
|
||||
function getPersonalityOverlay(
|
||||
provider: string,
|
||||
config?: Record<string, unknown>,
|
||||
): string {
|
||||
const styleInstructions = config?.['styleInstructions']
|
||||
? `\nAdditional Style Instructions: ${String(config['styleInstructions'])}`
|
||||
: '';
|
||||
|
||||
switch (provider.toLowerCase()) {
|
||||
case 'claude-code':
|
||||
return `
|
||||
# Claude Code Personality Overlay
|
||||
You are acting as the "Claude Code" agent, a high-performance, concise, and direct coding assistant.
|
||||
- Focus on efficiency and direct action.
|
||||
- Use tools precisely and explain your technical reasoning briefly and only when necessary.
|
||||
- Maintain a senior software engineer persona: professional, expert, and focused on clean, maintainable code.
|
||||
- Avoid unnecessary conversational filler or lengthy preambles.
|
||||
${styleInstructions}`.trim();
|
||||
|
||||
case 'codex':
|
||||
return `
|
||||
# Codex Personality Overlay
|
||||
You are acting as the "Codex" agent, a specialized code generation model.
|
||||
- Focus on generating high-quality, idiomatic, and correct code for the requested task.
|
||||
- Prioritize structural integrity and idiomatic patterns for the target language.
|
||||
- Provide clear, well-documented code snippets.
|
||||
${styleInstructions}`.trim();
|
||||
|
||||
default:
|
||||
return `
|
||||
# External Agent Personality Overlay
|
||||
You are acting as an external specialized agent ('${provider}').
|
||||
Please adopt the persona, style, and expertise expected of this provider.
|
||||
${styleInstructions}`.trim();
|
||||
}
|
||||
}
|
||||
@@ -324,9 +324,47 @@ export class AgentRegistry {
|
||||
this.registerLocalAgent(definition);
|
||||
} else if (definition.kind === 'remote') {
|
||||
await this.registerRemoteAgent(definition);
|
||||
} else if (definition.kind === 'external') {
|
||||
this.registerExternalAgent(definition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an external agent definition synchronously.
|
||||
*/
|
||||
protected registerExternalAgent<TOutput extends z.ZodTypeAny>(
|
||||
definition: AgentDefinition<TOutput>,
|
||||
): void {
|
||||
if (definition.kind !== 'external') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if (!definition.name || !definition.description || !definition.provider) {
|
||||
debugLogger.warn(
|
||||
`[AgentRegistry] Skipping invalid external agent definition. Missing name, description, or provider.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.allDefinitions.set(definition.name, definition);
|
||||
|
||||
const settingsOverrides =
|
||||
this.config.getAgentsSettings().overrides?.[definition.name];
|
||||
|
||||
if (!this.isAgentEnabled(definition, settingsOverrides)) {
|
||||
if (this.config.getDebugMode()) {
|
||||
debugLogger.log(
|
||||
`[AgentRegistry] Skipping disabled external agent '${definition.name}'`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.agents.set(definition.name, definition);
|
||||
this.addAgentPolicy(definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a local agent definition synchronously.
|
||||
*/
|
||||
|
||||
@@ -15,6 +15,7 @@ import { type AgentLoopContext } from '../config/agent-loop-context.js';
|
||||
import type { AgentDefinition, AgentInputs } from './types.js';
|
||||
import { LocalSubagentInvocation } from './local-invocation.js';
|
||||
import { RemoteAgentInvocation } from './remote-invocation.js';
|
||||
import { ExternalAgentInvocation } from './external-invocation.js';
|
||||
import { BrowserAgentInvocation } from './browser/browserAgentInvocation.js';
|
||||
import { BROWSER_AGENT_NAME } from './browser/browserAgentDefinition.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
@@ -72,35 +73,54 @@ export class SubagentToolWrapper extends BaseDeclarativeTool<
|
||||
const definition = this.definition;
|
||||
const effectiveMessageBus = messageBus;
|
||||
|
||||
if (definition.kind === 'remote') {
|
||||
return new RemoteAgentInvocation(
|
||||
definition,
|
||||
this.context,
|
||||
params,
|
||||
effectiveMessageBus,
|
||||
_toolName,
|
||||
_toolDisplayName,
|
||||
);
|
||||
}
|
||||
switch (definition.kind) {
|
||||
case 'remote':
|
||||
return new RemoteAgentInvocation(
|
||||
definition,
|
||||
this.context,
|
||||
params,
|
||||
effectiveMessageBus,
|
||||
_toolName,
|
||||
_toolDisplayName,
|
||||
);
|
||||
|
||||
// Special handling for browser agent - needs async MCP setup
|
||||
if (definition.name === BROWSER_AGENT_NAME) {
|
||||
return new BrowserAgentInvocation(
|
||||
this.context,
|
||||
params,
|
||||
effectiveMessageBus,
|
||||
_toolName,
|
||||
_toolDisplayName,
|
||||
);
|
||||
}
|
||||
case 'external':
|
||||
return new ExternalAgentInvocation(
|
||||
definition,
|
||||
this.context,
|
||||
params,
|
||||
effectiveMessageBus,
|
||||
_toolName,
|
||||
_toolDisplayName,
|
||||
);
|
||||
|
||||
return new LocalSubagentInvocation(
|
||||
definition,
|
||||
this.context,
|
||||
params,
|
||||
effectiveMessageBus,
|
||||
_toolName,
|
||||
_toolDisplayName,
|
||||
);
|
||||
case 'local':
|
||||
// Special handling for browser agent - needs async MCP setup
|
||||
if (definition.name === BROWSER_AGENT_NAME) {
|
||||
return new BrowserAgentInvocation(
|
||||
this.context,
|
||||
params,
|
||||
effectiveMessageBus,
|
||||
_toolName,
|
||||
_toolDisplayName,
|
||||
);
|
||||
}
|
||||
|
||||
return new LocalSubagentInvocation(
|
||||
definition,
|
||||
this.context,
|
||||
params,
|
||||
effectiveMessageBus,
|
||||
_toolName,
|
||||
_toolDisplayName,
|
||||
);
|
||||
|
||||
default: {
|
||||
const exhaustive: never = definition;
|
||||
throw new Error(
|
||||
`Unsupported agent kind: ${String((exhaustive as { kind: string }).kind)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,10 @@ describe('TeamRegistry', () => {
|
||||
|
||||
expect(registry.getAllTeams()).toHaveLength(1);
|
||||
expect(registry.getTeam('test-team')).toEqual(mockTeam);
|
||||
expect(mockAgentRegistry.registerAgent).toHaveBeenCalledWith(mockAgent);
|
||||
expect(mockAgentRegistry.registerAgent).toHaveBeenCalledWith({
|
||||
...mockAgent,
|
||||
description: `MANDATORY for undefined tasks: Agent in a team (Team Agent: Test Team). You MUST delegate all undefined tasks to this agent.`,
|
||||
});
|
||||
expect(loadTeamsFromDirectory).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
|
||||
@@ -263,9 +263,41 @@ export interface RemoteAgentDefinition<
|
||||
agentCardJson?: string;
|
||||
}
|
||||
|
||||
export interface ExternalAgentDefinition<
|
||||
TOutput extends z.ZodTypeAny = z.ZodUnknown,
|
||||
> extends BaseAgentDefinition<TOutput> {
|
||||
kind: 'external';
|
||||
|
||||
/**
|
||||
* The external agent provider (e.g., 'claude-code', 'codex').
|
||||
*/
|
||||
provider: string;
|
||||
|
||||
/**
|
||||
* Optional provider-specific configuration.
|
||||
*/
|
||||
providerConfig?: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Optional instructions (e.g., from markdown body).
|
||||
*/
|
||||
instructions?: string;
|
||||
|
||||
/**
|
||||
* Optional configs.
|
||||
*/
|
||||
toolConfig?: ToolConfig;
|
||||
|
||||
/**
|
||||
* Optional inline MCP servers for this agent.
|
||||
*/
|
||||
mcpServers?: Record<string, MCPServerConfig>;
|
||||
}
|
||||
|
||||
export type AgentDefinition<TOutput extends z.ZodTypeAny = z.ZodUnknown> =
|
||||
| LocalAgentDefinition<TOutput>
|
||||
| RemoteAgentDefinition<TOutput>;
|
||||
| RemoteAgentDefinition<TOutput>
|
||||
| ExternalAgentDefinition<TOutput>;
|
||||
|
||||
/**
|
||||
* Configures the initial prompt for the agent.
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('PromptProvider with Agent Teams', () => {
|
||||
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.",
|
||||
"**Orchestration Mandate:** You must prioritize delegating tasks to this team's specialized agents for their respective roles.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user