Add /background commands (when background agent is configured) (#4407)
Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
This commit is contained in:
parent
04bbc60b97
commit
003609239f
|
@ -382,6 +382,7 @@ export async function loadCliConfig(
|
||||||
toolCallCommand: settings.toolCallCommand,
|
toolCallCommand: settings.toolCallCommand,
|
||||||
mcpServerCommand: settings.mcpServerCommand,
|
mcpServerCommand: settings.mcpServerCommand,
|
||||||
mcpServers,
|
mcpServers,
|
||||||
|
backgroundAgents: settings.backgroundAgents,
|
||||||
userMemory: memoryContent,
|
userMemory: memoryContent,
|
||||||
geminiMdFileCount: fileCount,
|
geminiMdFileCount: fileCount,
|
||||||
approvalMode: argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT,
|
approvalMode: argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT,
|
||||||
|
|
|
@ -64,6 +64,7 @@ export interface Settings {
|
||||||
toolCallCommand?: string;
|
toolCallCommand?: string;
|
||||||
mcpServerCommand?: string;
|
mcpServerCommand?: string;
|
||||||
mcpServers?: Record<string, MCPServerConfig>;
|
mcpServers?: Record<string, MCPServerConfig>;
|
||||||
|
backgroundAgents?: Record<string, MCPServerConfig>;
|
||||||
allowMCPServers?: string[];
|
allowMCPServers?: string[];
|
||||||
excludeMCPServers?: string[];
|
excludeMCPServers?: string[];
|
||||||
showMemoryUsage?: boolean;
|
showMemoryUsage?: boolean;
|
||||||
|
|
|
@ -96,6 +96,7 @@ describe('CommandService', () => {
|
||||||
mockConfig = {
|
mockConfig = {
|
||||||
getIdeMode: vi.fn(),
|
getIdeMode: vi.fn(),
|
||||||
getCheckpointingEnabled: vi.fn(),
|
getCheckpointingEnabled: vi.fn(),
|
||||||
|
getBackgroundAgentManager: vi.fn(),
|
||||||
} as unknown as Mocked<Config>;
|
} as unknown as Mocked<Config>;
|
||||||
vi.mocked(ideCommand).mockReturnValue(null);
|
vi.mocked(ideCommand).mockReturnValue(null);
|
||||||
vi.mocked(restoreCommand).mockReturnValue(null);
|
vi.mocked(restoreCommand).mockReturnValue(null);
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import { Config } from '@google/gemini-cli-core';
|
import { Config } from '@google/gemini-cli-core';
|
||||||
import { SlashCommand } from '../ui/commands/types.js';
|
import { SlashCommand } from '../ui/commands/types.js';
|
||||||
import { memoryCommand } from '../ui/commands/memoryCommand.js';
|
import { memoryCommand } from '../ui/commands/memoryCommand.js';
|
||||||
|
import { backgroundCommand } from '../ui/commands/backgroundCommand.js';
|
||||||
import { helpCommand } from '../ui/commands/helpCommand.js';
|
import { helpCommand } from '../ui/commands/helpCommand.js';
|
||||||
import { clearCommand } from '../ui/commands/clearCommand.js';
|
import { clearCommand } from '../ui/commands/clearCommand.js';
|
||||||
import { corgiCommand } from '../ui/commands/corgiCommand.js';
|
import { corgiCommand } from '../ui/commands/corgiCommand.js';
|
||||||
|
@ -33,6 +34,7 @@ const loadBuiltInCommands = async (
|
||||||
const allCommands = [
|
const allCommands = [
|
||||||
aboutCommand,
|
aboutCommand,
|
||||||
authCommand,
|
authCommand,
|
||||||
|
backgroundCommand(config),
|
||||||
bugCommand,
|
bugCommand,
|
||||||
chatCommand,
|
chatCommand,
|
||||||
clearCommand,
|
clearCommand,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { render } from 'ink-testing-library';
|
||||||
import { AppWrapper as App } from './App.js';
|
import { AppWrapper as App } from './App.js';
|
||||||
import {
|
import {
|
||||||
Config as ServerConfig,
|
Config as ServerConfig,
|
||||||
|
BackgroundAgentManager,
|
||||||
MCPServerConfig,
|
MCPServerConfig,
|
||||||
ApprovalMode,
|
ApprovalMode,
|
||||||
ToolRegistry,
|
ToolRegistry,
|
||||||
|
@ -51,6 +52,7 @@ interface MockServerConfig {
|
||||||
getSandbox: Mock<() => SandboxConfig | undefined>;
|
getSandbox: Mock<() => SandboxConfig | undefined>;
|
||||||
getTargetDir: Mock<() => string>;
|
getTargetDir: Mock<() => string>;
|
||||||
getToolRegistry: Mock<() => ToolRegistry>; // Use imported ToolRegistry type
|
getToolRegistry: Mock<() => ToolRegistry>; // Use imported ToolRegistry type
|
||||||
|
getBackgroundAgentManager: Mock<() => BackgroundAgentManager>;
|
||||||
getDebugMode: Mock<() => boolean>;
|
getDebugMode: Mock<() => boolean>;
|
||||||
getQuestion: Mock<() => string | undefined>;
|
getQuestion: Mock<() => string | undefined>;
|
||||||
getFullContext: Mock<() => boolean>;
|
getFullContext: Mock<() => boolean>;
|
||||||
|
@ -117,6 +119,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||||
getSandbox: vi.fn(() => opts.sandbox),
|
getSandbox: vi.fn(() => opts.sandbox),
|
||||||
getTargetDir: vi.fn(() => opts.targetDir || '/test/dir'),
|
getTargetDir: vi.fn(() => opts.targetDir || '/test/dir'),
|
||||||
getToolRegistry: vi.fn(() => ({}) as ToolRegistry), // Simple mock
|
getToolRegistry: vi.fn(() => ({}) as ToolRegistry), // Simple mock
|
||||||
|
getBackgroundAgentManager: vi.fn(() => new BackgroundAgentManager([])),
|
||||||
getDebugMode: vi.fn(() => opts.debugMode || false),
|
getDebugMode: vi.fn(() => opts.debugMode || false),
|
||||||
getQuestion: vi.fn(() => opts.question),
|
getQuestion: vi.fn(() => opts.question),
|
||||||
getFullContext: vi.fn(() => opts.fullContext ?? false),
|
getFullContext: vi.fn(() => opts.fullContext ?? false),
|
||||||
|
|
|
@ -0,0 +1,262 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SlashCommand, CommandContext } from './types.js';
|
||||||
|
import {
|
||||||
|
Config,
|
||||||
|
BackgroundAgentMessage,
|
||||||
|
partListUnionToString,
|
||||||
|
} from '@google/gemini-cli-core';
|
||||||
|
|
||||||
|
const MAX_STATUS_MESSAGE_LENGTH = 100;
|
||||||
|
|
||||||
|
function toMessageString(message?: BackgroundAgentMessage): string {
|
||||||
|
return partListUnionToString(message?.parts ?? []).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toOneliner(input: string, maxlength: number) {
|
||||||
|
let output = input.replace(/\r?\n|\r/g, ' ');
|
||||||
|
if (output.length > maxlength) {
|
||||||
|
output = output.substring(0, maxlength) + '...';
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveAgent(context: CommandContext) {
|
||||||
|
const agent =
|
||||||
|
context.services.config?.getBackgroundAgentManager()?.activeAgent;
|
||||||
|
if (!agent) {
|
||||||
|
throw Error('There is no active background agent.');
|
||||||
|
}
|
||||||
|
return agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addClientHistory(context: CommandContext, text: string) {
|
||||||
|
context.services.config!.getGeminiClient().addHistory({
|
||||||
|
role: 'user',
|
||||||
|
parts: [{ text }],
|
||||||
|
});
|
||||||
|
|
||||||
|
context.services.config!.getGeminiClient().addHistory({
|
||||||
|
role: 'model',
|
||||||
|
parts: [{ text: 'Got it.' }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const startSubcommand: SlashCommand = {
|
||||||
|
name: 'start',
|
||||||
|
description:
|
||||||
|
'Start a new task with the provided prompt. Usage: /bg start <prompt>',
|
||||||
|
action: async (context, args) => {
|
||||||
|
if (!args || args.trim() === '') {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: 'The `start` command requires a prompt.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = getActiveAgent(context);
|
||||||
|
const task = await agent.startTask(args);
|
||||||
|
|
||||||
|
addClientHistory(
|
||||||
|
context,
|
||||||
|
`I started a background task with id '${task.id}' and prompt:\n${args}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content: `Started background task with id '${task.id}' and prompt:\n${args}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopSubcommand: SlashCommand = {
|
||||||
|
name: 'stop',
|
||||||
|
description: 'Stops a running task. Usage: /bg stop <task_id>',
|
||||||
|
action: async (context, args) => {
|
||||||
|
if (!args || args.trim() === '') {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: 'The `stop` command requires a task id.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const agent = getActiveAgent(context);
|
||||||
|
await agent.cancelTask(args);
|
||||||
|
addClientHistory(context, `I canceled the background task with id ${args}`);
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content: `Stopped background task with id ${args}.`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const listSubcommand: SlashCommand = {
|
||||||
|
name: 'list',
|
||||||
|
description: 'List all tasks',
|
||||||
|
action: async (context, args) => {
|
||||||
|
if (args && args.trim() !== '') {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: 'The `list` command takes no arguments.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = getActiveAgent(context);
|
||||||
|
const tasks = await agent.listTasks();
|
||||||
|
let content: string;
|
||||||
|
if (tasks.length === 0) {
|
||||||
|
content = 'No background tasks found.';
|
||||||
|
} else {
|
||||||
|
const taskList = tasks
|
||||||
|
.map((task) => {
|
||||||
|
const shortStatus = toOneliner(
|
||||||
|
toMessageString(task.status.message),
|
||||||
|
MAX_STATUS_MESSAGE_LENGTH,
|
||||||
|
);
|
||||||
|
return ` - ${task.id}: (${task.status.state}) ${shortStatus}`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
content = `Background tasks:\n${taskList}`;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSubcommand: SlashCommand = {
|
||||||
|
name: 'get',
|
||||||
|
description: 'View a task. Usage: /bg get <task_id>',
|
||||||
|
action: async (context, args) => {
|
||||||
|
if (!args || args.trim() === '') {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: 'The `get` command requires a task id.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const agent = getActiveAgent(context);
|
||||||
|
const task = await agent.getTask(args);
|
||||||
|
const content = `Task Details for ${task.id}:
|
||||||
|
Status: (${task.status.state}) ${toMessageString(task.status.message)}}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const logsSubcommand: SlashCommand = {
|
||||||
|
name: 'logs',
|
||||||
|
description: "View a task's recent logs. Usage: /bg log <task_id>",
|
||||||
|
action: async (context, args) => {
|
||||||
|
if (!args || args.trim() === '') {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: 'The `log` command requires a task id.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const agent = getActiveAgent(context);
|
||||||
|
const task = await agent.getTask(args, 5);
|
||||||
|
const contents = [
|
||||||
|
`Task logs for ${task.id}. status: (${task.status.state})`,
|
||||||
|
];
|
||||||
|
(task.history ?? []).forEach((message) => {
|
||||||
|
contents.push(toMessageString(message));
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content: contents.join('\n\n'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const messageSubcommand: SlashCommand = {
|
||||||
|
name: 'message',
|
||||||
|
description:
|
||||||
|
'Send a message to a task. Usage: /bg message <task_id> <message>',
|
||||||
|
action: async (context, args) => {
|
||||||
|
if (!args || args.trim() === '' || !args.trim().includes(' ')) {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: 'The `message` command requires a task id and a message.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstSpaceIndex = args.indexOf(' ');
|
||||||
|
const id = args.substring(0, firstSpaceIndex);
|
||||||
|
const message = args.substring(firstSpaceIndex + 1);
|
||||||
|
|
||||||
|
const agent = getActiveAgent(context);
|
||||||
|
await agent.messageTask(id, message);
|
||||||
|
addClientHistory(
|
||||||
|
context,
|
||||||
|
`I sent a message to the background task with id '${id}':\n${message}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content: `Sent a message to the background task with id '${id}':\n${message}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSubcommand: SlashCommand = {
|
||||||
|
name: 'delete',
|
||||||
|
description: 'Deletes a task. Usage: /bg delete <task_id>',
|
||||||
|
action: async (context, args) => {
|
||||||
|
if (!args) {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: 'The `delete` command requires a task id.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const agent = getActiveAgent(context);
|
||||||
|
await agent.deleteTask(args);
|
||||||
|
addClientHistory(context, `I deleted the background task with id ${args}`);
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content: `Task ${args} deleted.`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const backgroundCommand = (
|
||||||
|
config: Config | null,
|
||||||
|
): SlashCommand | null => {
|
||||||
|
if (!config?.getBackgroundAgentManager()?.activeAgent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: 'background',
|
||||||
|
altName: 'bg',
|
||||||
|
description: "Commands for managing the background agent's tasks",
|
||||||
|
subCommands: [
|
||||||
|
startSubcommand,
|
||||||
|
stopSubcommand,
|
||||||
|
listSubcommand,
|
||||||
|
getSubcommand,
|
||||||
|
logsSubcommand,
|
||||||
|
messageSubcommand,
|
||||||
|
deleteSubcommand,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,126 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MCPServerConfig } from '../config/config.js';
|
||||||
|
import { connectToMcpServer, discoverTools } from '../tools/mcp-client.js';
|
||||||
|
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
|
||||||
|
import {
|
||||||
|
BackgroundAgentTasksResponseSchema,
|
||||||
|
BackgroundAgentTaskResponseSchema,
|
||||||
|
BackgroundAgentTask,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
export async function loadBackgroundAgent(
|
||||||
|
name: string,
|
||||||
|
config: MCPServerConfig,
|
||||||
|
debugMode: boolean,
|
||||||
|
): Promise<BackgroundAgent> {
|
||||||
|
const server = await connectToMcpServer(name, config, debugMode);
|
||||||
|
try {
|
||||||
|
const tools = await discoverTools(name, config, server);
|
||||||
|
return new BackgroundAgent(name, tools);
|
||||||
|
} catch (error) {
|
||||||
|
await server.close();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BackgroundAgent {
|
||||||
|
readonly startTaskTool: DiscoveredMCPTool;
|
||||||
|
readonly getTaskTool: DiscoveredMCPTool;
|
||||||
|
readonly listTasksTool: DiscoveredMCPTool;
|
||||||
|
readonly messageTaskTool: DiscoveredMCPTool;
|
||||||
|
readonly deleteTaskTool: DiscoveredMCPTool;
|
||||||
|
readonly cancelTaskTool: DiscoveredMCPTool;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly serverName: string,
|
||||||
|
tools: DiscoveredMCPTool[],
|
||||||
|
) {
|
||||||
|
const getToolOrFail = (name: string): DiscoveredMCPTool => {
|
||||||
|
for (const tool of tools) {
|
||||||
|
if (tool.serverToolName === name) {
|
||||||
|
return tool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`missing expected tool: ${name}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.startTaskTool = getToolOrFail('startTask');
|
||||||
|
this.getTaskTool = getToolOrFail('getTask');
|
||||||
|
this.listTasksTool = getToolOrFail('listTasks');
|
||||||
|
this.messageTaskTool = getToolOrFail('messageTask');
|
||||||
|
this.deleteTaskTool = getToolOrFail('deleteTask');
|
||||||
|
this.cancelTaskTool = getToolOrFail('cancelTask');
|
||||||
|
}
|
||||||
|
|
||||||
|
async startTask(prompt: string): Promise<BackgroundAgentTask> {
|
||||||
|
const resp = await this.callTool(this.startTaskTool, {
|
||||||
|
prompt: {
|
||||||
|
role: 'user',
|
||||||
|
parts: [{ text: prompt }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const taskResp = await BackgroundAgentTaskResponseSchema.parseAsync(resp);
|
||||||
|
return taskResp.structuredContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTask(
|
||||||
|
id: string,
|
||||||
|
historyLength?: number,
|
||||||
|
): Promise<BackgroundAgentTask> {
|
||||||
|
const resp = await this.callTool(this.getTaskTool, {
|
||||||
|
id,
|
||||||
|
historyLength,
|
||||||
|
});
|
||||||
|
const taskResp = await BackgroundAgentTaskResponseSchema.parseAsync(resp);
|
||||||
|
return taskResp.structuredContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listTasks(): Promise<BackgroundAgentTask[]> {
|
||||||
|
const resp = await this.callTool(this.listTasksTool, {});
|
||||||
|
const tasksResp = await BackgroundAgentTasksResponseSchema.parseAsync(resp);
|
||||||
|
return tasksResp.structuredContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
async messageTask(id: string, message: string) {
|
||||||
|
await this.callTool(this.messageTaskTool, {
|
||||||
|
id,
|
||||||
|
message: {
|
||||||
|
role: 'user',
|
||||||
|
parts: [{ text: message }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTask(id: string) {
|
||||||
|
await this.callTool(this.deleteTaskTool, { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelTask(id: string) {
|
||||||
|
await this.callTool(this.cancelTaskTool, { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async callTool(
|
||||||
|
tool: DiscoveredMCPTool,
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
const { llmContent: parts } = await tool.execute(params);
|
||||||
|
if (
|
||||||
|
!Array.isArray(parts) ||
|
||||||
|
parts.length !== 1 ||
|
||||||
|
typeof parts[0] !== 'object' ||
|
||||||
|
parts[0]?.functionResponse?.response === undefined
|
||||||
|
) {
|
||||||
|
throw new Error('Expected exactly one part with a functionResponse');
|
||||||
|
}
|
||||||
|
const resp = parts[0].functionResponse.response;
|
||||||
|
if ('isError' in resp && resp.isError) {
|
||||||
|
throw new Error(`Error calling ${tool.displayName}: ${resp}`);
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MCPServerConfig } from '../config/config.js';
|
||||||
|
import { BackgroundAgent, loadBackgroundAgent } from './backgroundAgent.js';
|
||||||
|
|
||||||
|
export async function loadBackgroundAgentManager(
|
||||||
|
backgroundAgentConfigs: Record<string, MCPServerConfig> | undefined,
|
||||||
|
debugMode: boolean,
|
||||||
|
): Promise<BackgroundAgentManager> {
|
||||||
|
const agents = await Promise.all(
|
||||||
|
Object.entries(backgroundAgentConfigs ?? {}).map(([name, config]) =>
|
||||||
|
loadBackgroundAgent(name, config, debugMode).catch((error) => {
|
||||||
|
console.error(`Error loading background agent '${name}': ${error}`);
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).then((agents) => agents.filter((agent) => agent !== null));
|
||||||
|
return new BackgroundAgentManager(agents);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BackgroundAgentManager {
|
||||||
|
// The active agent. May be empty if none are confgured.
|
||||||
|
activeAgent?: BackgroundAgent;
|
||||||
|
|
||||||
|
constructor(readonly backgroundAgents: BackgroundAgent[]) {
|
||||||
|
if (backgroundAgents.length !== 0) {
|
||||||
|
this.activeAgent = backgroundAgents[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveAgentByName(name: string) {
|
||||||
|
this.activeAgent = this.backgroundAgents.find(
|
||||||
|
(agent) => agent.serverName === name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { Outcome, Language, FunctionResponseScheduling } from '@google/genai';
|
||||||
|
|
||||||
|
// Should conform to Part in @google/genai
|
||||||
|
export const PartSchema = z.object({
|
||||||
|
videoMetadata: z
|
||||||
|
.object({
|
||||||
|
fps: z.number().optional(),
|
||||||
|
endOffset: z.string().optional(),
|
||||||
|
startOffset: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
thought: z.boolean().optional(),
|
||||||
|
inlineData: z
|
||||||
|
.object({
|
||||||
|
displayName: z.string().optional(),
|
||||||
|
data: z.string(),
|
||||||
|
mimeType: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
fileData: z
|
||||||
|
.object({
|
||||||
|
displayName: z.string().optional(),
|
||||||
|
fileUri: z.string(),
|
||||||
|
mimeType: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
thoughtSignature: z.string().optional(),
|
||||||
|
codeExecutionResult: z
|
||||||
|
.object({
|
||||||
|
outcome: z.nativeEnum(Outcome).optional(),
|
||||||
|
output: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
executableCode: z
|
||||||
|
.object({
|
||||||
|
code: z.string().optional(),
|
||||||
|
language: z.nativeEnum(Language).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
functionCall: z
|
||||||
|
.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
args: z.record(z.unknown()).optional(),
|
||||||
|
name: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
functionResponse: z
|
||||||
|
.object({
|
||||||
|
willContinue: z.boolean().optional(),
|
||||||
|
scheduling: z.nativeEnum(FunctionResponseScheduling).optional(),
|
||||||
|
id: z.string().optional(),
|
||||||
|
name: z.string(),
|
||||||
|
response: z.record(z.unknown()).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
text: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BackgroundAgentMessageSchema = z.object({
|
||||||
|
role: z.enum(['user', 'agent']).describe('The role of the sender.'),
|
||||||
|
parts: z.array(PartSchema).describe('The parts of the message.'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BackgroundAgentTaskStatusSchema = z.object({
|
||||||
|
state: z.enum([
|
||||||
|
'submitted',
|
||||||
|
'working',
|
||||||
|
'input-required',
|
||||||
|
'completed',
|
||||||
|
'failed',
|
||||||
|
]),
|
||||||
|
message: BackgroundAgentMessageSchema.describe(
|
||||||
|
'Message describing the state of the task.',
|
||||||
|
).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BackgroundAgentTaskSchema = z.object({
|
||||||
|
id: z.string().describe('The id of the task. Must match `[a-zA-Z0-9.-_]+`'),
|
||||||
|
status: BackgroundAgentTaskStatusSchema.describe(
|
||||||
|
'The current status of the task.',
|
||||||
|
),
|
||||||
|
history: z
|
||||||
|
.array(BackgroundAgentMessageSchema)
|
||||||
|
.describe('Recent history of messages associated with this task')
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BackgroundAgentMessage = z.infer<
|
||||||
|
typeof BackgroundAgentMessageSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type BackgroundAgentTask = z.infer<typeof BackgroundAgentTaskSchema>;
|
||||||
|
|
||||||
|
export const BackgroundAgentTaskResponseSchema = z.object({
|
||||||
|
structuredContent: BackgroundAgentTaskSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BackgroundAgentTasksResponseSchema = z.object({
|
||||||
|
structuredContent: z.array(BackgroundAgentTaskSchema),
|
||||||
|
});
|
|
@ -45,6 +45,10 @@ import {
|
||||||
DEFAULT_GEMINI_FLASH_MODEL,
|
DEFAULT_GEMINI_FLASH_MODEL,
|
||||||
} from './models.js';
|
} from './models.js';
|
||||||
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
|
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
|
||||||
|
import {
|
||||||
|
BackgroundAgentManager,
|
||||||
|
loadBackgroundAgentManager,
|
||||||
|
} from '../background/backgroundManager.js';
|
||||||
|
|
||||||
export enum ApprovalMode {
|
export enum ApprovalMode {
|
||||||
DEFAULT = 'default',
|
DEFAULT = 'default',
|
||||||
|
@ -127,6 +131,7 @@ export interface ConfigParameters {
|
||||||
toolCallCommand?: string;
|
toolCallCommand?: string;
|
||||||
mcpServerCommand?: string;
|
mcpServerCommand?: string;
|
||||||
mcpServers?: Record<string, MCPServerConfig>;
|
mcpServers?: Record<string, MCPServerConfig>;
|
||||||
|
backgroundAgents?: Record<string, MCPServerConfig>;
|
||||||
userMemory?: string;
|
userMemory?: string;
|
||||||
geminiMdFileCount?: number;
|
geminiMdFileCount?: number;
|
||||||
approvalMode?: ApprovalMode;
|
approvalMode?: ApprovalMode;
|
||||||
|
@ -158,6 +163,7 @@ export interface ConfigParameters {
|
||||||
|
|
||||||
export class Config {
|
export class Config {
|
||||||
private toolRegistry!: ToolRegistry;
|
private toolRegistry!: ToolRegistry;
|
||||||
|
private backgroundAgentManager?: BackgroundAgentManager;
|
||||||
private readonly sessionId: string;
|
private readonly sessionId: string;
|
||||||
private contentGeneratorConfig!: ContentGeneratorConfig;
|
private contentGeneratorConfig!: ContentGeneratorConfig;
|
||||||
private readonly embeddingModel: string;
|
private readonly embeddingModel: string;
|
||||||
|
@ -172,6 +178,7 @@ export class Config {
|
||||||
private readonly toolCallCommand: string | undefined;
|
private readonly toolCallCommand: string | undefined;
|
||||||
private readonly mcpServerCommand: string | undefined;
|
private readonly mcpServerCommand: string | undefined;
|
||||||
private readonly mcpServers: Record<string, MCPServerConfig> | undefined;
|
private readonly mcpServers: Record<string, MCPServerConfig> | undefined;
|
||||||
|
private readonly backgroundAgents?: Record<string, MCPServerConfig>;
|
||||||
private userMemory: string;
|
private userMemory: string;
|
||||||
private geminiMdFileCount: number;
|
private geminiMdFileCount: number;
|
||||||
private approvalMode: ApprovalMode;
|
private approvalMode: ApprovalMode;
|
||||||
|
@ -224,6 +231,7 @@ export class Config {
|
||||||
this.toolCallCommand = params.toolCallCommand;
|
this.toolCallCommand = params.toolCallCommand;
|
||||||
this.mcpServerCommand = params.mcpServerCommand;
|
this.mcpServerCommand = params.mcpServerCommand;
|
||||||
this.mcpServers = params.mcpServers;
|
this.mcpServers = params.mcpServers;
|
||||||
|
this.backgroundAgents = params.backgroundAgents;
|
||||||
this.userMemory = params.userMemory ?? '';
|
this.userMemory = params.userMemory ?? '';
|
||||||
this.geminiMdFileCount = params.geminiMdFileCount ?? 0;
|
this.geminiMdFileCount = params.geminiMdFileCount ?? 0;
|
||||||
this.approvalMode = params.approvalMode ?? ApprovalMode.DEFAULT;
|
this.approvalMode = params.approvalMode ?? ApprovalMode.DEFAULT;
|
||||||
|
@ -281,6 +289,10 @@ export class Config {
|
||||||
if (this.getCheckpointingEnabled()) {
|
if (this.getCheckpointingEnabled()) {
|
||||||
await this.getGitService();
|
await this.getGitService();
|
||||||
}
|
}
|
||||||
|
this.backgroundAgentManager = await loadBackgroundAgentManager(
|
||||||
|
this.backgroundAgents,
|
||||||
|
this.debugMode,
|
||||||
|
);
|
||||||
this.toolRegistry = await this.createToolRegistry();
|
this.toolRegistry = await this.createToolRegistry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -406,6 +418,10 @@ export class Config {
|
||||||
return this.mcpServers;
|
return this.mcpServers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBackgroundAgentManager(): BackgroundAgentManager | undefined {
|
||||||
|
return this.backgroundAgentManager;
|
||||||
|
}
|
||||||
|
|
||||||
getUserMemory(): string {
|
getUserMemory(): string {
|
||||||
return this.userMemory;
|
return this.userMemory;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,9 @@ export * from './code_assist/oauth2.js';
|
||||||
export * from './code_assist/server.js';
|
export * from './code_assist/server.js';
|
||||||
export * from './code_assist/types.js';
|
export * from './code_assist/types.js';
|
||||||
|
|
||||||
|
export * from './background/types.js';
|
||||||
|
export * from './background/backgroundManager.js';
|
||||||
|
|
||||||
// Export utilities
|
// Export utilities
|
||||||
export * from './utils/paths.js';
|
export * from './utils/paths.js';
|
||||||
export * from './utils/schemaValidator.js';
|
export * from './utils/schemaValidator.js';
|
||||||
|
|
|
@ -113,9 +113,7 @@ export class DiscoveredMCPTool extends BaseTool<ToolParams, ToolResult> {
|
||||||
args: params,
|
args: params,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
const responseParts = await this.mcpTool.callTool(functionCalls);
|
||||||
const responseParts: Part[] = await this.mcpTool.callTool(functionCalls);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
llmContent: responseParts,
|
llmContent: responseParts,
|
||||||
returnDisplay: getStringifiedResultForDisplay(responseParts),
|
returnDisplay: getStringifiedResultForDisplay(responseParts),
|
||||||
|
|
Loading…
Reference in New Issue