mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-26 04:54:25 -07:00
feat(a2a): Introduce /init command for a2a server (#13419)
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { ExtensionsCommand } from './extensions.js';
|
||||
import { InitCommand } from './init.js';
|
||||
import { RestoreCommand } from './restore.js';
|
||||
import type { Command } from './types.js';
|
||||
|
||||
@@ -14,6 +15,7 @@ class CommandRegistry {
|
||||
constructor() {
|
||||
this.register(new ExtensionsCommand());
|
||||
this.register(new RestoreCommand());
|
||||
this.register(new InitCommand());
|
||||
}
|
||||
|
||||
register(command: Command) {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { InitCommand } from './init.js';
|
||||
import { performInit } from '@google/gemini-cli-core';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { CoderAgentExecutor } from '../agent/executor.js';
|
||||
import { CoderAgentEvent } from '../types.js';
|
||||
import type { ExecutionEventBus } from '@a2a-js/sdk/server';
|
||||
import { createMockConfig } from '../utils/testing_utils.js';
|
||||
import type { CommandContext } from './types.js';
|
||||
import type { CommandActionReturn, Config } from '@google/gemini-cli-core';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
performInit: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('node:fs', () => ({
|
||||
existsSync: vi.fn(),
|
||||
writeFileSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../agent/executor.js', () => ({
|
||||
CoderAgentExecutor: vi.fn().mockImplementation(() => ({
|
||||
execute: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../utils/logger.js', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('InitCommand', () => {
|
||||
let eventBus: ExecutionEventBus;
|
||||
let command: InitCommand;
|
||||
let context: CommandContext;
|
||||
let publishSpy: ReturnType<typeof vi.spyOn>;
|
||||
let mockExecute: ReturnType<typeof vi.fn>;
|
||||
const mockWorkspacePath = path.resolve('/tmp');
|
||||
|
||||
beforeEach(() => {
|
||||
process.env['CODER_AGENT_WORKSPACE_PATH'] = mockWorkspacePath;
|
||||
eventBus = {
|
||||
publish: vi.fn(),
|
||||
} as unknown as ExecutionEventBus;
|
||||
command = new InitCommand();
|
||||
const mockConfig = createMockConfig({
|
||||
getModel: () => 'gemini-pro',
|
||||
});
|
||||
const mockExecutorInstance = new CoderAgentExecutor();
|
||||
context = {
|
||||
config: mockConfig as unknown as Config,
|
||||
agentExecutor: mockExecutorInstance,
|
||||
eventBus,
|
||||
} as CommandContext;
|
||||
publishSpy = vi.spyOn(eventBus, 'publish');
|
||||
mockExecute = vi.fn();
|
||||
vi.spyOn(mockExecutorInstance, 'execute').mockImplementation(mockExecute);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('has requiresWorkspace set to true', () => {
|
||||
expect(command.requiresWorkspace).toBe(true);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('handles info from performInit', async () => {
|
||||
vi.mocked(performInit).mockReturnValue({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'GEMINI.md already exists.',
|
||||
} as CommandActionReturn);
|
||||
|
||||
await command.execute(context, []);
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
'[EventBus event]: ',
|
||||
expect.objectContaining({
|
||||
kind: 'status-update',
|
||||
status: expect.objectContaining({
|
||||
state: 'completed',
|
||||
message: expect.objectContaining({
|
||||
parts: [{ kind: 'text', text: 'GEMINI.md already exists.' }],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(publishSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
kind: 'status-update',
|
||||
status: expect.objectContaining({
|
||||
state: 'completed',
|
||||
message: expect.objectContaining({
|
||||
parts: [{ kind: 'text', text: 'GEMINI.md already exists.' }],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('handles error from performInit', async () => {
|
||||
vi.mocked(performInit).mockReturnValue({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'An error occurred.',
|
||||
} as CommandActionReturn);
|
||||
|
||||
await command.execute(context, []);
|
||||
|
||||
expect(publishSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
kind: 'status-update',
|
||||
status: expect.objectContaining({
|
||||
state: 'failed',
|
||||
message: expect.objectContaining({
|
||||
parts: [{ kind: 'text', text: 'An error occurred.' }],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('when handling submit_prompt', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(performInit).mockReturnValue({
|
||||
type: 'submit_prompt',
|
||||
content: 'Create a new GEMINI.md file.',
|
||||
} as CommandActionReturn);
|
||||
});
|
||||
|
||||
it('writes the file and executes the agent', async () => {
|
||||
await command.execute(context, []);
|
||||
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(mockWorkspacePath, 'GEMINI.md'),
|
||||
'',
|
||||
'utf8',
|
||||
);
|
||||
expect(mockExecute).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes autoExecute to the agent executor', async () => {
|
||||
await command.execute(context, []);
|
||||
|
||||
expect(mockExecute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
userMessage: expect.objectContaining({
|
||||
parts: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
text: 'Create a new GEMINI.md file.',
|
||||
}),
|
||||
]),
|
||||
metadata: {
|
||||
coderAgent: {
|
||||
kind: CoderAgentEvent.StateAgentSettingsEvent,
|
||||
workspacePath: mockWorkspacePath,
|
||||
autoExecute: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
eventBus,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { CoderAgentEvent, type AgentSettings } from '../types.js';
|
||||
import { performInit } from '@google/gemini-cli-core';
|
||||
import type {
|
||||
Command,
|
||||
CommandContext,
|
||||
CommandExecutionResponse,
|
||||
} from './types.js';
|
||||
import type { CoderAgentExecutor } from '../agent/executor.js';
|
||||
import type {
|
||||
ExecutionEventBus,
|
||||
RequestContext,
|
||||
AgentExecutionEvent,
|
||||
} from '@a2a-js/sdk/server';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
export class InitCommand implements Command {
|
||||
name = 'init';
|
||||
description = 'Analyzes the project and creates a tailored GEMINI.md file';
|
||||
requiresWorkspace = true;
|
||||
streaming = true;
|
||||
|
||||
private handleMessageResult(
|
||||
result: { content: string; messageType: 'info' | 'error' },
|
||||
context: CommandContext,
|
||||
eventBus: ExecutionEventBus,
|
||||
taskId: string,
|
||||
contextId: string,
|
||||
): CommandExecutionResponse {
|
||||
const statusState = result.messageType === 'error' ? 'failed' : 'completed';
|
||||
const eventType =
|
||||
result.messageType === 'error'
|
||||
? CoderAgentEvent.StateChangeEvent
|
||||
: CoderAgentEvent.TextContentEvent;
|
||||
|
||||
const event: AgentExecutionEvent = {
|
||||
kind: 'status-update',
|
||||
taskId,
|
||||
contextId,
|
||||
status: {
|
||||
state: statusState,
|
||||
message: {
|
||||
kind: 'message',
|
||||
role: 'agent',
|
||||
parts: [{ kind: 'text', text: result.content }],
|
||||
messageId: uuidv4(),
|
||||
taskId,
|
||||
contextId,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
final: true,
|
||||
metadata: {
|
||||
coderAgent: { kind: eventType },
|
||||
model: context.config.getModel(),
|
||||
},
|
||||
};
|
||||
|
||||
logger.info('[EventBus event]: ', event);
|
||||
eventBus.publish(event);
|
||||
return {
|
||||
name: this.name,
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
|
||||
private async handleSubmitPromptResult(
|
||||
result: { content: unknown },
|
||||
context: CommandContext,
|
||||
geminiMdPath: string,
|
||||
eventBus: ExecutionEventBus,
|
||||
taskId: string,
|
||||
contextId: string,
|
||||
): Promise<CommandExecutionResponse> {
|
||||
fs.writeFileSync(geminiMdPath, '', 'utf8');
|
||||
|
||||
if (!context.agentExecutor) {
|
||||
throw new Error('Agent executor not found in context.');
|
||||
}
|
||||
const agentExecutor = context.agentExecutor as CoderAgentExecutor;
|
||||
|
||||
const agentSettings: AgentSettings = {
|
||||
kind: CoderAgentEvent.StateAgentSettingsEvent,
|
||||
workspacePath: process.env['CODER_AGENT_WORKSPACE_PATH']!,
|
||||
autoExecute: true,
|
||||
};
|
||||
|
||||
if (typeof result.content !== 'string') {
|
||||
throw new Error('Init command content must be a string.');
|
||||
}
|
||||
const promptText = result.content;
|
||||
|
||||
const requestContext: RequestContext = {
|
||||
userMessage: {
|
||||
kind: 'message',
|
||||
role: 'user',
|
||||
parts: [{ kind: 'text', text: promptText }],
|
||||
messageId: uuidv4(),
|
||||
taskId,
|
||||
contextId,
|
||||
metadata: {
|
||||
coderAgent: agentSettings,
|
||||
},
|
||||
},
|
||||
taskId,
|
||||
contextId,
|
||||
};
|
||||
|
||||
// The executor will handle the entire agentic loop, including
|
||||
// creating the task, streaming responses, and handling tools.
|
||||
await agentExecutor.execute(requestContext, eventBus);
|
||||
return {
|
||||
name: this.name,
|
||||
data: geminiMdPath,
|
||||
};
|
||||
}
|
||||
|
||||
async execute(
|
||||
context: CommandContext,
|
||||
_args: string[] = [],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
if (!context.eventBus) {
|
||||
return {
|
||||
name: this.name,
|
||||
data: 'Use executeStream to get streaming results.',
|
||||
};
|
||||
}
|
||||
|
||||
const geminiMdPath = path.join(
|
||||
process.env['CODER_AGENT_WORKSPACE_PATH']!,
|
||||
'GEMINI.md',
|
||||
);
|
||||
const result = performInit(fs.existsSync(geminiMdPath));
|
||||
|
||||
const taskId = uuidv4();
|
||||
const contextId = uuidv4();
|
||||
|
||||
switch (result.type) {
|
||||
case 'message':
|
||||
return this.handleMessageResult(
|
||||
result,
|
||||
context,
|
||||
context.eventBus,
|
||||
taskId,
|
||||
contextId,
|
||||
);
|
||||
case 'submit_prompt':
|
||||
return this.handleSubmitPromptResult(
|
||||
result,
|
||||
context,
|
||||
geminiMdPath,
|
||||
context.eventBus,
|
||||
taskId,
|
||||
contextId,
|
||||
);
|
||||
default:
|
||||
throw new Error('Unknown result type from performInit');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,14 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { ExecutionEventBus, AgentExecutor } from '@a2a-js/sdk/server';
|
||||
import type { Config, GitService } from '@google/gemini-cli-core';
|
||||
|
||||
export interface CommandContext {
|
||||
config: Config;
|
||||
git?: GitService;
|
||||
agentExecutor?: AgentExecutor;
|
||||
eventBus?: ExecutionEventBus;
|
||||
}
|
||||
|
||||
export interface CommandArgument {
|
||||
@@ -24,6 +27,7 @@ export interface Command {
|
||||
readonly subCommands?: Command[];
|
||||
readonly topLevel?: boolean;
|
||||
readonly requiresWorkspace?: boolean;
|
||||
readonly streaming?: boolean;
|
||||
|
||||
execute(
|
||||
config: CommandContext,
|
||||
|
||||
Reference in New Issue
Block a user