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:
Taylor Mullen
2026-04-01 17:55:00 -07:00
parent 31584285a1
commit 79cc1eb4f4
14 changed files with 776 additions and 119 deletions
@@ -35,7 +35,8 @@ describe('<TeamSelectionDialog />', () => {
mockOnSelect.mockReset();
});
const renderComponent = async () => renderWithProviders(
const renderComponent = async () =>
renderWithProviders(
<TeamSelectionDialog teams={mockTeams} onSelect={mockOnSelect} />,
);
+1 -6
View File
@@ -213,11 +213,7 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
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<TableRendererProps> = ({
isHeader = false,
): React.ReactNode => {
const renderedCells = cells.map((cell, index) => {
const width = adjustedWidths[index] || 0;
return renderCell(cell, width, isHeader);
});
+173 -81
View File
@@ -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();
}
}
+38
View File
@@ -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);
});
+33 -1
View File
@@ -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.",
);
});
});
+48
View File
@@ -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<string, unknown>;
}
```
### 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`.
+49
View File
@@ -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.
+47
View File
@@ -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/<slug>/`.
- [ ] 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`.