diff --git a/packages/cli/src/ui/components/TeamSelectionDialog.test.tsx b/packages/cli/src/ui/components/TeamSelectionDialog.test.tsx
index f392a594c2..f338d731f4 100644
--- a/packages/cli/src/ui/components/TeamSelectionDialog.test.tsx
+++ b/packages/cli/src/ui/components/TeamSelectionDialog.test.tsx
@@ -35,7 +35,8 @@ describe('', () => {
mockOnSelect.mockReset();
});
- const renderComponent = async () => renderWithProviders(
+ const renderComponent = async () =>
+ renderWithProviders(
,
);
diff --git a/packages/cli/src/ui/utils/TableRenderer.tsx b/packages/cli/src/ui/utils/TableRenderer.tsx
index ca62012059..a968f8c0e9 100644
--- a/packages/cli/src/ui/utils/TableRenderer.tsx
+++ b/packages/cli/src/ui/utils/TableRenderer.tsx
@@ -213,11 +213,7 @@ export const TableRenderer: React.FC = ({
const wrappedRows = styledRows.map((row) => wrapAndProcessRow(row));
// Use the TIGHTEST widths that fit the wrapped content + padding
- const adjustedWidths = actualColumnWidths.map(
- (w) =>
-
- w + COLUMN_PADDING,
- );
+ const adjustedWidths = actualColumnWidths.map((w) => w + COLUMN_PADDING);
return { wrappedHeaders, wrappedRows, adjustedWidths };
}, [styledHeaders, styledRows, terminalWidth]);
@@ -268,7 +264,6 @@ export const TableRenderer: React.FC = ({
isHeader = false,
): React.ReactNode => {
const renderedCells = cells.map((cell, index) => {
-
const width = adjustedWidths[index] || 0;
return renderCell(cell, width, isHeader);
});
diff --git a/packages/core/src/agents/agentLoader.ts b/packages/core/src/agents/agentLoader.ts
index d34d0e974e..ba19f82b22 100644
--- a/packages/core/src/agents/agentLoader.ts
+++ b/packages/core/src/agents/agentLoader.ts
@@ -211,9 +211,35 @@ const remoteAgentSchema = z.union([
type FrontmatterRemoteAgentDefinition = z.infer;
+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;
+
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 = {};
+ 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 = {};
- 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 = {};
+ 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,
- };
}
/**
diff --git a/packages/core/src/agents/external-agent.integration.test.ts b/packages/core/src/agents/external-agent.integration.test.ts
new file mode 100644
index 0000000000..23b52c1900
--- /dev/null
+++ b/packages/core/src/agents/external-agent.integration.test.ts
@@ -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.');
+ });
+});
diff --git a/packages/core/src/agents/external-invocation.test.ts b/packages/core/src/agents/external-invocation.test.ts
new file mode 100644
index 0000000000..011860f291
--- /dev/null
+++ b/packages/core/src/agents/external-invocation.test.ts
@@ -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),
+ );
+ });
+});
diff --git a/packages/core/src/agents/external-invocation.ts b/packages/core/src/agents/external-invocation.ts
new file mode 100644
index 0000000000..2291b204b8
--- /dev/null
+++ b/packages/core/src/agents/external-invocation.ts
@@ -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 {
+ 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();
+ }
+}
diff --git a/packages/core/src/agents/registry.ts b/packages/core/src/agents/registry.ts
index 1cd8629db5..396ca8410a 100644
--- a/packages/core/src/agents/registry.ts
+++ b/packages/core/src/agents/registry.ts
@@ -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(
+ definition: AgentDefinition,
+ ): 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.
*/
diff --git a/packages/core/src/agents/subagent-tool-wrapper.ts b/packages/core/src/agents/subagent-tool-wrapper.ts
index 30a30d76d0..7fdae08be6 100644
--- a/packages/core/src/agents/subagent-tool-wrapper.ts
+++ b/packages/core/src/agents/subagent-tool-wrapper.ts
@@ -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)}`,
+ );
+ }
+ }
}
}
diff --git a/packages/core/src/agents/teamRegistry.test.ts b/packages/core/src/agents/teamRegistry.test.ts
index b7f0f9ec12..834fe2723f 100644
--- a/packages/core/src/agents/teamRegistry.test.ts
+++ b/packages/core/src/agents/teamRegistry.test.ts
@@ -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);
});
diff --git a/packages/core/src/agents/types.ts b/packages/core/src/agents/types.ts
index 77ba551d3d..b6e45068dc 100644
--- a/packages/core/src/agents/types.ts
+++ b/packages/core/src/agents/types.ts
@@ -263,9 +263,41 @@ export interface RemoteAgentDefinition<
agentCardJson?: string;
}
+export interface ExternalAgentDefinition<
+ TOutput extends z.ZodTypeAny = z.ZodUnknown,
+> extends BaseAgentDefinition {
+ kind: 'external';
+
+ /**
+ * The external agent provider (e.g., 'claude-code', 'codex').
+ */
+ provider: string;
+
+ /**
+ * Optional provider-specific configuration.
+ */
+ providerConfig?: Record;
+
+ /**
+ * Optional instructions (e.g., from markdown body).
+ */
+ instructions?: string;
+
+ /**
+ * Optional configs.
+ */
+ toolConfig?: ToolConfig;
+
+ /**
+ * Optional inline MCP servers for this agent.
+ */
+ mcpServers?: Record;
+}
+
export type AgentDefinition =
| LocalAgentDefinition
- | RemoteAgentDefinition;
+ | RemoteAgentDefinition
+ | ExternalAgentDefinition;
/**
* Configures the initial prompt for the agent.
diff --git a/packages/core/src/prompts/promptProvider-teams.test.ts b/packages/core/src/prompts/promptProvider-teams.test.ts
index cfe15c0c03..2a16bbfe19 100644
--- a/packages/core/src/prompts/promptProvider-teams.test.ts
+++ b/packages/core/src/prompts/promptProvider-teams.test.ts
@@ -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.",
);
});
});
diff --git a/plans/TASK-06.md b/plans/TASK-06.md
new file mode 100644
index 0000000000..833054111f
--- /dev/null
+++ b/plans/TASK-06.md
@@ -0,0 +1,48 @@
+# TASK-06: External Agent Definitions and "Polyfill" Support
+
+## Objective
+
+Introduce a new `external` agent kind to support integrating and "polyfilling"
+competitor agents like Claude Code and Codex into Gemini CLI teams.
+
+## Implementation Details
+
+### 1. Type Definitions (`packages/core/src/agents/types.ts`)
+
+- [ ] Add `ExternalAgentDefinition` to the `AgentDefinition` union.
+- [ ] Include fields for `provider` (e.g., `'claude-code'`, `'codex'`) and
+ `providerOptions`.
+ ```typescript
+ export interface ExternalAgentDefinition extends BaseAgentDefinition {
+ kind: 'external';
+ provider: string;
+ // Allow for provider-specific configuration
+ providerConfig?: Record;
+ }
+ ```
+
+### 2. External Agent Invocation (`packages/core/src/agents/external-invocation.ts`)
+
+- [ ] Implement `ExternalAgentInvocation` class.
+- [ ] **The Polyfill Logic:**
+ - Use the system's configured default model to "polyfill" the external agent.
+ - Apply a "personality overlay" to the system prompt based on the `provider`.
+ - Example (Claude Code): Prepend instructions to adopt the specific style,
+ tool usage patterns, and "persona" of a high-performance, concise coding
+ assistant.
+ - Example (Codex): Prepend instructions to act as a specialized code
+ generation model.
+ - Ensure the overlay is model-agnostic and relies on the `providerConfig` for
+ behavior tuning.
+
+### 3. Tool Wrapping (`packages/core/src/agents/subagent-tool-wrapper.ts`)
+
+- [ ] Update `createInvocation` to recognize `kind === 'external'` and return an
+ `ExternalAgentInvocation`.
+
+## Verification
+
+- Unit tests for `ExternalAgentInvocation` ensuring provider-specific prompts
+ are applied.
+- Manual verification by creating an agent with `kind: external` and
+ `provider: claude-code`.
diff --git a/plans/TASK-07.md b/plans/TASK-07.md
new file mode 100644
index 0000000000..cd4b447ff7
--- /dev/null
+++ b/plans/TASK-07.md
@@ -0,0 +1,49 @@
+# TASK-07: External Agent Registry and Team Marketplace placeholders
+
+## Objective
+
+Enable teams to include external agents and provide the "marketplace" UI
+placeholders that suggest where one might find or browse external agents.
+
+## Implementation Details
+
+### 1. Team Discovery (`packages/core/src/agents/teamLoader.ts`)
+
+- [ ] Ensure that `TEAM.md` can specify agents with `kind: external` and
+ `provider`.
+- [ ] If a team specifies an external agent that isn't found locally, provide a
+ clear error message or auto-generate a polyfill definition.
+
+### 2. Team Marketplace UI (`packages/cli/src/ui/components/TeamSelectionDialog.tsx`)
+
+- [ ] Populate the "Browse Team Marketplace" and "Create New Team" placeholders
+ from TASK-04.
+- [ ] For the marketplace, show a list of "Curated Teams" (hardcoded for the
+ MVP) that include external agents.
+ - Example: "The Polyglot Team" (Gemini + Claude Code + Codex).
+- [ ] **Visual Language:**
+ - Add color-coded tags next to agents in the team list:
+ - **Anthropic Purple** for Claude Code (`provider: 'claude-code'`).
+ - **OpenAI Green** for Codex (`provider: 'codex'`).
+ - **Google Blue** for Gemini agents.
+ - Use a "Multi-Model" badge if a team contains agents from multiple providers.
+
+### 3. Team Builder/Creator (MVP)
+
+- [ ] In the "Create New Team" placeholder, provide a simple prompt to create a
+ `TEAM.md` in a new subdirectory.
+- [ ] Allow users to specify `providers` for their agents in this builder.
+
+### 4. Status Indicator Enhancement (`packages/cli/src/ui/components/StatusRow.tsx`)
+
+- [ ] If a team is active, the `ActiveTeamIndicator` should show the "Provider"
+ icons or tags for the agents in the team.
+ - Example: `[ Team: Polyglot (G, C, X) ]` where G/C/X are color-coded letters
+ for Gemini/Claude/Codex.
+
+## Verification
+
+- Manual verification of the "Marketplace" and "Create Team" placeholders in the
+ selection dialog.
+- Verify that a team with an external Claude Code agent displays correctly and
+ its instructions are correctly surfaced.
diff --git a/plans/TASK-08.md b/plans/TASK-08.md
new file mode 100644
index 0000000000..b098b86403
--- /dev/null
+++ b/plans/TASK-08.md
@@ -0,0 +1,47 @@
+# TASK-08: Interactive Team Creation Wizard
+
+## Objective
+
+Implement a guided, step-by-step CLI wizard to allow users to create new Agent
+Teams without manually editing files and directories.
+
+## Implementation Details
+
+### 1. The Wizard Flow (`packages/cli/src/ui/components/TeamCreatorWizard.tsx`)
+
+- [ ] Create a multi-step Ink component:
+ - **Step 1: Identity**: Prompt for Team Name and Display Name (Text Input).
+ - **Step 2: Objective**: Prompt for the orchestration instructions (Multiline
+ Text Input). This guides the top-level agent on how to use the team.
+ - **Step 3: Roster**: A multi-select list of available agents:
+ - Include local agents from `.gemini/agents/`.
+ - Include "External" templates: `Claude Code (External)`,
+ `Codex (External)`.
+ - **Step 4: Confirmation**: Display a summary and ask for final confirmation.
+
+### 2. Scaffolding Service (`packages/core/src/agents/teamScaffolder.ts`)
+
+- [ ] Create a service to handle the filesystem operations:
+ - [ ] Create the directory `.gemini/teams//`.
+ - [ ] Generate the `TEAM.md` with the collected frontmatter and instructions.
+ - [ ] Create the `agents/` sub-folder.
+ - [ ] For each selected agent:
+ - If it's an existing local agent, copy/link its definition.
+ - If it's an "External" agent, generate a placeholder `.md` file with the
+ correct `kind: external` and `provider`.
+
+### 3. UI Integration (`packages/cli/src/ui/AppContainer.tsx`)
+
+- [ ] In the `TeamSelectionDialog` (from TASK-04/07), when "Create New Team" is
+ selected:
+ - [ ] Set `isTeamCreatorActive(true)`.
+- [ ] Render the `TeamCreatorWizard` overlay.
+- [ ] On completion, refresh the `TeamRegistry` and return to the selection list
+ with the new team highlighted.
+
+## Verification
+
+- Manual verification of the creation flow.
+- Verify that the resulting directory structure and `TEAM.md` are valid and
+ loadable.
+- Unit tests for `teamScaffolder.ts`.