mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Add /background commands (when background agent is configured) (#4407)
Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
This commit is contained in:
committed by
GitHub
parent
04bbc60b97
commit
003609239f
126
packages/core/src/background/backgroundAgent.ts
Normal file
126
packages/core/src/background/backgroundAgent.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
40
packages/core/src/background/backgroundManager.ts
Normal file
40
packages/core/src/background/backgroundManager.ts
Normal file
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
107
packages/core/src/background/types.ts
Normal file
107
packages/core/src/background/types.ts
Normal file
@@ -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,
|
||||
} from './models.js';
|
||||
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
|
||||
import {
|
||||
BackgroundAgentManager,
|
||||
loadBackgroundAgentManager,
|
||||
} from '../background/backgroundManager.js';
|
||||
|
||||
export enum ApprovalMode {
|
||||
DEFAULT = 'default',
|
||||
@@ -127,6 +131,7 @@ export interface ConfigParameters {
|
||||
toolCallCommand?: string;
|
||||
mcpServerCommand?: string;
|
||||
mcpServers?: Record<string, MCPServerConfig>;
|
||||
backgroundAgents?: Record<string, MCPServerConfig>;
|
||||
userMemory?: string;
|
||||
geminiMdFileCount?: number;
|
||||
approvalMode?: ApprovalMode;
|
||||
@@ -158,6 +163,7 @@ export interface ConfigParameters {
|
||||
|
||||
export class Config {
|
||||
private toolRegistry!: ToolRegistry;
|
||||
private backgroundAgentManager?: BackgroundAgentManager;
|
||||
private readonly sessionId: string;
|
||||
private contentGeneratorConfig!: ContentGeneratorConfig;
|
||||
private readonly embeddingModel: string;
|
||||
@@ -172,6 +178,7 @@ export class Config {
|
||||
private readonly toolCallCommand: string | undefined;
|
||||
private readonly mcpServerCommand: string | undefined;
|
||||
private readonly mcpServers: Record<string, MCPServerConfig> | undefined;
|
||||
private readonly backgroundAgents?: Record<string, MCPServerConfig>;
|
||||
private userMemory: string;
|
||||
private geminiMdFileCount: number;
|
||||
private approvalMode: ApprovalMode;
|
||||
@@ -224,6 +231,7 @@ export class Config {
|
||||
this.toolCallCommand = params.toolCallCommand;
|
||||
this.mcpServerCommand = params.mcpServerCommand;
|
||||
this.mcpServers = params.mcpServers;
|
||||
this.backgroundAgents = params.backgroundAgents;
|
||||
this.userMemory = params.userMemory ?? '';
|
||||
this.geminiMdFileCount = params.geminiMdFileCount ?? 0;
|
||||
this.approvalMode = params.approvalMode ?? ApprovalMode.DEFAULT;
|
||||
@@ -281,6 +289,10 @@ export class Config {
|
||||
if (this.getCheckpointingEnabled()) {
|
||||
await this.getGitService();
|
||||
}
|
||||
this.backgroundAgentManager = await loadBackgroundAgentManager(
|
||||
this.backgroundAgents,
|
||||
this.debugMode,
|
||||
);
|
||||
this.toolRegistry = await this.createToolRegistry();
|
||||
}
|
||||
|
||||
@@ -406,6 +418,10 @@ export class Config {
|
||||
return this.mcpServers;
|
||||
}
|
||||
|
||||
getBackgroundAgentManager(): BackgroundAgentManager | undefined {
|
||||
return this.backgroundAgentManager;
|
||||
}
|
||||
|
||||
getUserMemory(): string {
|
||||
return this.userMemory;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ export * from './code_assist/oauth2.js';
|
||||
export * from './code_assist/server.js';
|
||||
export * from './code_assist/types.js';
|
||||
|
||||
export * from './background/types.js';
|
||||
export * from './background/backgroundManager.js';
|
||||
|
||||
// Export utilities
|
||||
export * from './utils/paths.js';
|
||||
export * from './utils/schemaValidator.js';
|
||||
|
||||
@@ -113,9 +113,7 @@ export class DiscoveredMCPTool extends BaseTool<ToolParams, ToolResult> {
|
||||
args: params,
|
||||
},
|
||||
];
|
||||
|
||||
const responseParts: Part[] = await this.mcpTool.callTool(functionCalls);
|
||||
|
||||
const responseParts = await this.mcpTool.callTool(functionCalls);
|
||||
return {
|
||||
llmContent: responseParts,
|
||||
returnDisplay: getStringifiedResultForDisplay(responseParts),
|
||||
|
||||
Reference in New Issue
Block a user