diff options
| author | Tommaso Sciortino <[email protected]> | 2025-07-18 15:38:04 -0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-07-18 22:38:04 +0000 |
| commit | 003609239fe81c8a2920ed0c63b7f5142bb4f7e5 (patch) | |
| tree | 9dcce05e79a6f1bbe58d8074232212e2495130c5 /packages/core/src/background | |
| parent | 04bbc60b97809b4200c5dd09ba849e4d097d1d1f (diff) | |
Add /background commands (when background agent is configured) (#4407)
Co-authored-by: Bryan Morgan <[email protected]>
Diffstat (limited to 'packages/core/src/background')
| -rw-r--r-- | packages/core/src/background/backgroundAgent.ts | 126 | ||||
| -rw-r--r-- | packages/core/src/background/backgroundManager.ts | 40 | ||||
| -rw-r--r-- | packages/core/src/background/types.ts | 107 |
3 files changed, 273 insertions, 0 deletions
diff --git a/packages/core/src/background/backgroundAgent.ts b/packages/core/src/background/backgroundAgent.ts new file mode 100644 index 00000000..008010cd --- /dev/null +++ b/packages/core/src/background/backgroundAgent.ts @@ -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; + } +} diff --git a/packages/core/src/background/backgroundManager.ts b/packages/core/src/background/backgroundManager.ts new file mode 100644 index 00000000..a3ec526c --- /dev/null +++ b/packages/core/src/background/backgroundManager.ts @@ -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, + ); + } +} diff --git a/packages/core/src/background/types.ts b/packages/core/src/background/types.ts new file mode 100644 index 00000000..60927af6 --- /dev/null +++ b/packages/core/src/background/types.ts @@ -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), +}); |
