feat(core): 添加后台 Shell 管理功能
- 新增 ShellManager 管理后台 Shell 进程生命周期 - bash 工具支持 run_in_background 参数在后台运行命令 - 新增 kill_shell 工具用于终止后台 Shell - task_output 工具同时支持获取 Agent 和 Shell 任务输出 - 支持超时控制、输出限制和优雅终止
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
- Kills a running background bash shell by its ID
|
||||
- Takes a shell_id parameter identifying the shell to kill
|
||||
- Returns a success or failure status
|
||||
- Use this tool when you need to terminate a long-running shell
|
||||
- Shell IDs can be found using the /tasks command
|
||||
@@ -9,6 +9,7 @@ const __dirname = path.dirname(__filename);
|
||||
const TOOL_CATEGORY_MAP: Record<string, string> = {
|
||||
// shell
|
||||
bash: 'shell',
|
||||
kill_shell: 'shell',
|
||||
// filesystem
|
||||
read_file: 'filesystem',
|
||||
write_file: 'filesystem',
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
import { getShellManager } from './manager.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -20,47 +21,88 @@ export const bashTool: ToolWithMetadata = {
|
||||
parameters: {
|
||||
command: {
|
||||
type: 'string',
|
||||
description: '要执行的 bash 命令',
|
||||
description: 'The command to execute',
|
||||
required: true,
|
||||
},
|
||||
cwd: {
|
||||
description: {
|
||||
type: 'string',
|
||||
description: '工作目录(可选,默认为当前目录)',
|
||||
description: 'Clear, concise description of what this command does in 5-10 words, in active voice',
|
||||
required: false,
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
description: 'Optional timeout in milliseconds (max 600000)',
|
||||
required: false,
|
||||
maximum: 120000,
|
||||
},
|
||||
run_in_background: {
|
||||
type: 'boolean',
|
||||
description: 'Set to true to run this command in the background. Use TaskOutput to read the output later.',
|
||||
required: false,
|
||||
},
|
||||
dangerouslyDisableSandbox: {
|
||||
type: 'boolean',
|
||||
description: 'Set this to true to dangerously override sandbox mode and run commands without sandboxing.',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const command = params.command as string;
|
||||
const cwd = (params.cwd as string) || process.cwd();
|
||||
const timeout = (params.timeout as number) || 120000; // 默认 2 分钟超时
|
||||
const runInBackground = params.run_in_background as boolean;
|
||||
const dangerouslyDisableSandbox = params.dangerouslyDisableSandbox as boolean;
|
||||
const cwd = process.cwd();
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkBashPermission({
|
||||
command,
|
||||
workdir: cwd,
|
||||
});
|
||||
// 权限检查(除非 dangerouslyDisableSandbox 为 true)
|
||||
if (!dangerouslyDisableSandbox) {
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkBashPermission({
|
||||
command,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
// 如果需要用户确认但没有设置回调,返回等待确认的状态
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: ${command}\n原因: ${permResult.reason || '需要权限确认'}\n模式: ${permResult.patterns?.join(', ') || ''}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!permResult.allowed) {
|
||||
// 如果需要用户确认但没有设置回调,返回等待确认的状态
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: ${command}\n原因: ${permResult.reason || '需要权限确认'}\n模式: ${permResult.patterns?.join(', ') || ''}`,
|
||||
error: `权限被拒绝: ${permResult.reason || '命令不被允许执行'}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 后台运行模式
|
||||
if (runInBackground) {
|
||||
const shellManager = getShellManager();
|
||||
const description = params.description as string | undefined;
|
||||
const shellId = shellManager.runInBackground(command, {
|
||||
description,
|
||||
cwd,
|
||||
timeout: Math.min(timeout, 600000),
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '命令不被允许执行'}`,
|
||||
success: true,
|
||||
output: `Command started in background with shell_id: ${shellId}\nUse TaskOutput tool with task_id="${shellId}" to retrieve results.`,
|
||||
metadata: {
|
||||
shellId,
|
||||
type: 'background_shell',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(command, {
|
||||
cwd,
|
||||
timeout: 60000, // 60 秒超时
|
||||
timeout: Math.min(timeout, 600000), // 最大 10 分钟
|
||||
maxBuffer: 1024 * 1024 * 10, // 10MB 输出限制
|
||||
});
|
||||
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
export { bashTool } from './bash.js';
|
||||
export { killShellTool } from './kill_shell.js';
|
||||
export { getShellManager, resetShellManager } from './manager.js';
|
||||
export type { BackgroundShell, BackgroundShellStatus } from './manager.js';
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getShellManager } from './manager.js';
|
||||
|
||||
export const killShellTool: ToolWithMetadata = {
|
||||
name: 'kill_shell',
|
||||
description: loadDescription('kill_shell'),
|
||||
metadata: {
|
||||
name: 'kill_shell',
|
||||
category: 'shell',
|
||||
description: '终止后台运行的 shell',
|
||||
keywords: ['kill', 'shell', 'terminate', 'stop', 'background', '终止', '停止', '后台'],
|
||||
deferLoading: false,
|
||||
},
|
||||
parameters: {
|
||||
shell_id: {
|
||||
type: 'string',
|
||||
description: 'The ID of the background shell to kill',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const shellId = params.shell_id as string;
|
||||
const shellManager = getShellManager();
|
||||
|
||||
// 先检查 shell 是否存在
|
||||
const shell = shellManager.getShell(shellId);
|
||||
if (!shell) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Shell ${shellId} not found. Use /tasks command to list available shells.`,
|
||||
};
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
if (shell.status !== 'running') {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Shell ${shellId} is not running (status: ${shell.status})`,
|
||||
};
|
||||
}
|
||||
|
||||
// 尝试终止
|
||||
const killed = shellManager.killShell(shellId);
|
||||
if (killed) {
|
||||
return {
|
||||
success: true,
|
||||
output: `Shell ${shellId} has been terminated.\nCommand: ${shell.command}`,
|
||||
metadata: {
|
||||
shellId,
|
||||
command: shell.command,
|
||||
status: 'killed',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Failed to kill shell ${shellId}`,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,305 @@
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
/**
|
||||
* 后台 Shell 状态
|
||||
*/
|
||||
export type BackgroundShellStatus = 'running' | 'completed' | 'failed' | 'killed';
|
||||
|
||||
/**
|
||||
* 后台 Shell 信息
|
||||
*/
|
||||
export interface BackgroundShell {
|
||||
/** Shell 唯一 ID */
|
||||
id: string;
|
||||
/** 执行的命令 */
|
||||
command: string;
|
||||
/** 命令描述 */
|
||||
description?: string;
|
||||
/** 执行状态 */
|
||||
status: BackgroundShellStatus;
|
||||
/** 开始时间 */
|
||||
startedAt: Date;
|
||||
/** 完成时间 */
|
||||
completedAt?: Date;
|
||||
/** 标准输出 */
|
||||
stdout: string;
|
||||
/** 标准错误 */
|
||||
stderr: string;
|
||||
/** 退出码 */
|
||||
exitCode?: number;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 子进程引用 */
|
||||
process?: ChildProcess;
|
||||
/** 工作目录 */
|
||||
cwd: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shell 管理器
|
||||
* 负责管理后台 Shell 进程的生命周期
|
||||
*/
|
||||
export class ShellManager {
|
||||
private backgroundShells = new Map<string, BackgroundShell>();
|
||||
private completionCallbacks = new Map<string, Array<() => void>>();
|
||||
|
||||
/**
|
||||
* 启动后台 Shell 命令
|
||||
*/
|
||||
runInBackground(
|
||||
command: string,
|
||||
options: {
|
||||
description?: string;
|
||||
cwd?: string;
|
||||
timeout?: number;
|
||||
} = {}
|
||||
): string {
|
||||
const shellId = uuidv4().substring(0, 8); // 短 ID
|
||||
const cwd = options.cwd || process.cwd();
|
||||
const timeout = options.timeout || 600000; // 默认 10 分钟
|
||||
|
||||
// 创建后台 Shell 记录
|
||||
const shell: BackgroundShell = {
|
||||
id: shellId,
|
||||
command,
|
||||
description: options.description,
|
||||
status: 'running',
|
||||
startedAt: new Date(),
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
cwd,
|
||||
};
|
||||
|
||||
this.backgroundShells.set(shellId, shell);
|
||||
|
||||
// 启动子进程
|
||||
const childProcess = spawn(command, {
|
||||
shell: true,
|
||||
cwd,
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
shell.process = childProcess;
|
||||
|
||||
// 收集输出
|
||||
childProcess.stdout?.on('data', (data: Buffer) => {
|
||||
shell.stdout += data.toString();
|
||||
// 限制输出大小,防止内存溢出
|
||||
if (shell.stdout.length > 10 * 1024 * 1024) {
|
||||
shell.stdout = shell.stdout.slice(-5 * 1024 * 1024);
|
||||
}
|
||||
});
|
||||
|
||||
childProcess.stderr?.on('data', (data: Buffer) => {
|
||||
shell.stderr += data.toString();
|
||||
// 限制输出大小
|
||||
if (shell.stderr.length > 10 * 1024 * 1024) {
|
||||
shell.stderr = shell.stderr.slice(-5 * 1024 * 1024);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理进程结束
|
||||
childProcess.on('close', (code: number | null) => {
|
||||
shell.status = code === 0 ? 'completed' : 'failed';
|
||||
shell.completedAt = new Date();
|
||||
shell.exitCode = code ?? undefined;
|
||||
shell.process = undefined; // 清理进程引用
|
||||
this.notifyCompletion(shellId);
|
||||
});
|
||||
|
||||
childProcess.on('error', (err: Error) => {
|
||||
shell.status = 'failed';
|
||||
shell.completedAt = new Date();
|
||||
shell.error = err.message;
|
||||
shell.process = undefined;
|
||||
this.notifyCompletion(shellId);
|
||||
});
|
||||
|
||||
// 设置超时
|
||||
setTimeout(() => {
|
||||
if (shell.status === 'running') {
|
||||
this.killShell(shellId);
|
||||
shell.error = `Command timed out after ${timeout}ms`;
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
return shellId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 终止后台 Shell
|
||||
*/
|
||||
killShell(shellId: string): boolean {
|
||||
const shell = this.backgroundShells.get(shellId);
|
||||
if (!shell) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (shell.status !== 'running' || !shell.process) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试优雅终止
|
||||
shell.process.kill('SIGTERM');
|
||||
|
||||
// 如果 3 秒后还未终止,强制杀死
|
||||
setTimeout(() => {
|
||||
if (shell.status === 'running' && shell.process) {
|
||||
shell.process.kill('SIGKILL');
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
shell.status = 'killed';
|
||||
shell.completedAt = new Date();
|
||||
shell.error = 'Process killed by user';
|
||||
this.notifyCompletion(shellId);
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取后台 Shell 状态
|
||||
*/
|
||||
getShell(shellId: string): BackgroundShell | null {
|
||||
return this.backgroundShells.get(shellId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Shell 输出(支持阻塞等待)
|
||||
*/
|
||||
async getShellOutput(
|
||||
shellId: string,
|
||||
block: boolean = true,
|
||||
timeoutMs: number = 30000
|
||||
): Promise<BackgroundShell | null> {
|
||||
const shell = this.backgroundShells.get(shellId);
|
||||
if (!shell) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 如果已完成或不需要阻塞,直接返回
|
||||
if (shell.status !== 'running' || !block) {
|
||||
return shell;
|
||||
}
|
||||
|
||||
// 阻塞等待完成
|
||||
return this.waitForCompletion(shellId, timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待 Shell 完成
|
||||
*/
|
||||
private waitForCompletion(
|
||||
shellId: string,
|
||||
timeoutMs: number
|
||||
): Promise<BackgroundShell | null> {
|
||||
return new Promise((resolve) => {
|
||||
const shell = this.backgroundShells.get(shellId);
|
||||
if (!shell || shell.status !== 'running') {
|
||||
resolve(shell || null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置超时
|
||||
const timeoutId = setTimeout(() => {
|
||||
// 移除回调
|
||||
const callbacks = this.completionCallbacks.get(shellId);
|
||||
if (callbacks) {
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
resolve(this.backgroundShells.get(shellId) || null);
|
||||
}, timeoutMs);
|
||||
|
||||
// 注册完成回调
|
||||
const callback = () => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve(this.backgroundShells.get(shellId) || null);
|
||||
};
|
||||
|
||||
if (!this.completionCallbacks.has(shellId)) {
|
||||
this.completionCallbacks.set(shellId, []);
|
||||
}
|
||||
this.completionCallbacks.get(shellId)!.push(callback);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知等待者任务已完成
|
||||
*/
|
||||
private notifyCompletion(shellId: string): void {
|
||||
const callbacks = this.completionCallbacks.get(shellId);
|
||||
if (callbacks) {
|
||||
for (const callback of callbacks) {
|
||||
callback();
|
||||
}
|
||||
this.completionCallbacks.delete(shellId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有后台 Shell
|
||||
*/
|
||||
listShells(): BackgroundShell[] {
|
||||
return Array.from(this.backgroundShells.values()).map((shell) => ({
|
||||
...shell,
|
||||
process: undefined, // 不暴露进程引用
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出运行中的 Shell
|
||||
*/
|
||||
listRunningShells(): BackgroundShell[] {
|
||||
return this.listShells().filter((s) => s.status === 'running');
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理已完成的 Shell 记录
|
||||
*/
|
||||
cleanup(maxAge: number = 3600000): void {
|
||||
const now = Date.now();
|
||||
for (const [id, shell] of this.backgroundShells) {
|
||||
if (
|
||||
shell.status !== 'running' &&
|
||||
shell.completedAt &&
|
||||
now - shell.completedAt.getTime() > maxAge
|
||||
) {
|
||||
this.backgroundShells.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 单例实例
|
||||
let shellManager: ShellManager | null = null;
|
||||
|
||||
/**
|
||||
* 获取 ShellManager 单例
|
||||
*/
|
||||
export function getShellManager(): ShellManager {
|
||||
if (!shellManager) {
|
||||
shellManager = new ShellManager();
|
||||
}
|
||||
return shellManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置 ShellManager(测试用)
|
||||
*/
|
||||
export function resetShellManager(): void {
|
||||
// 先清理所有运行中的进程
|
||||
if (shellManager) {
|
||||
for (const shell of shellManager.listRunningShells()) {
|
||||
shellManager.killShell(shell.id);
|
||||
}
|
||||
}
|
||||
shellManager = null;
|
||||
}
|
||||
@@ -1,10 +1,146 @@
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { BackgroundAgent } from '../../agent/manager.js';
|
||||
import type { BackgroundShell } from '../shell/manager.js';
|
||||
import { getAgentManager } from '../../agent/manager.js';
|
||||
import { getShellManager } from '../shell/manager.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
|
||||
/**
|
||||
* 格式化 Shell 输出
|
||||
*/
|
||||
function formatShellOutput(taskId: string, shell: BackgroundShell): ToolResult {
|
||||
const duration = shell.completedAt
|
||||
? Math.round((shell.completedAt.getTime() - shell.startedAt.getTime()) / 1000)
|
||||
: Math.round((Date.now() - shell.startedAt.getTime()) / 1000);
|
||||
|
||||
if (shell.status === 'running') {
|
||||
return {
|
||||
success: true,
|
||||
output: `Shell ${taskId} is still running...\n` +
|
||||
`- Command: ${shell.command}\n` +
|
||||
`- Running for: ${duration} seconds\n\n` +
|
||||
`Current output:\n${shell.stdout || '(no output yet)'}\n` +
|
||||
(shell.stderr ? `\nSTDERR:\n${shell.stderr}` : ''),
|
||||
metadata: {
|
||||
taskId,
|
||||
type: 'shell',
|
||||
status: 'running',
|
||||
command: shell.command,
|
||||
runningTime: duration,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (shell.status === 'failed' || shell.status === 'killed') {
|
||||
return {
|
||||
success: false,
|
||||
output: shell.stdout || '',
|
||||
error: `Shell ${taskId} ${shell.status}:\n` +
|
||||
`- Command: ${shell.command}\n` +
|
||||
`- Duration: ${duration} seconds\n` +
|
||||
`- Exit code: ${shell.exitCode ?? 'N/A'}\n` +
|
||||
`- Error: ${shell.error || shell.stderr || 'Unknown error'}`,
|
||||
metadata: {
|
||||
taskId,
|
||||
type: 'shell',
|
||||
status: shell.status,
|
||||
command: shell.command,
|
||||
duration,
|
||||
exitCode: shell.exitCode,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// completed
|
||||
const output = shell.stdout + (shell.stderr ? `\nSTDERR:\n${shell.stderr}` : '');
|
||||
return {
|
||||
success: true,
|
||||
output: `## Shell ${taskId} completed\n\n` +
|
||||
`- Command: ${shell.command}\n` +
|
||||
`- Duration: ${duration} seconds\n` +
|
||||
`- Exit code: ${shell.exitCode ?? 0}\n\n` +
|
||||
`### Output\n\n${output || '(no output)'}`,
|
||||
metadata: {
|
||||
taskId,
|
||||
type: 'shell',
|
||||
status: 'completed',
|
||||
command: shell.command,
|
||||
duration,
|
||||
exitCode: shell.exitCode,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 Agent 输出
|
||||
*/
|
||||
function formatAgentOutput(taskId: string, agent: BackgroundAgent): ToolResult {
|
||||
if (agent.status === 'running') {
|
||||
const runningTime = Math.round((Date.now() - agent.startedAt.getTime()) / 1000);
|
||||
return {
|
||||
success: true,
|
||||
output: `Agent ${taskId} is still running...\n` +
|
||||
`- Type: ${agent.agentName}\n` +
|
||||
`- Task: ${agent.description}\n` +
|
||||
`- Running for: ${runningTime} seconds\n\n` +
|
||||
`Use task_output again later or set block: true to wait for completion.`,
|
||||
metadata: {
|
||||
taskId,
|
||||
type: 'agent',
|
||||
status: 'running',
|
||||
agentName: agent.agentName,
|
||||
runningTime,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const duration = agent.completedAt
|
||||
? Math.round((agent.completedAt.getTime() - agent.startedAt.getTime()) / 1000)
|
||||
: 0;
|
||||
|
||||
if (agent.status === 'failed') {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Agent ${taskId} failed:\n` +
|
||||
`- Type: ${agent.agentName}\n` +
|
||||
`- Task: ${agent.description}\n` +
|
||||
`- Duration: ${duration} seconds\n` +
|
||||
`- Error: ${agent.error || 'Unknown error'}`,
|
||||
metadata: {
|
||||
taskId,
|
||||
type: 'agent',
|
||||
status: 'failed',
|
||||
agentName: agent.agentName,
|
||||
duration,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// completed
|
||||
return {
|
||||
success: true,
|
||||
output: `## Agent ${taskId} completed\n\n` +
|
||||
`- Type: ${agent.agentName}\n` +
|
||||
`- Task: ${agent.description}\n` +
|
||||
`- Duration: ${duration} seconds\n` +
|
||||
`- Steps: ${agent.steps || 0}\n\n` +
|
||||
`### Result\n\n${agent.result || '(no output)'}`,
|
||||
metadata: {
|
||||
taskId,
|
||||
type: 'agent',
|
||||
status: 'completed',
|
||||
agentName: agent.agentName,
|
||||
duration,
|
||||
steps: agent.steps,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* TaskOutput 工具
|
||||
* 用于获取后台 Agent 的执行结果
|
||||
* 用于获取后台 Agent 或 Shell 的执行结果
|
||||
*/
|
||||
export const taskOutputTool: ToolWithMetadata = {
|
||||
name: 'task_output',
|
||||
@@ -48,83 +184,29 @@ export const taskOutputTool: ToolWithMetadata = {
|
||||
timeout?: number;
|
||||
};
|
||||
|
||||
const agentManager = getAgentManager();
|
||||
|
||||
// 验证超时参数(毫秒),转换为秒传给 agentManager
|
||||
const timeoutMs = Math.min(Math.max(timeout, 0), 600000);
|
||||
const timeoutSec = Math.ceil(timeoutMs / 1000);
|
||||
|
||||
// 获取 Agent 输出
|
||||
// 先尝试查找 Shell 任务
|
||||
const shellManager = getShellManager();
|
||||
const shell = await shellManager.getShellOutput(task_id, block, timeoutMs);
|
||||
|
||||
if (shell) {
|
||||
return formatShellOutput(task_id, shell);
|
||||
}
|
||||
|
||||
// 再尝试查找 Agent 任务
|
||||
const agentManager = getAgentManager();
|
||||
const timeoutSec = Math.ceil(timeoutMs / 1000);
|
||||
const agent = await agentManager.getAgentOutput(task_id, block, timeoutSec);
|
||||
|
||||
if (!agent) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Agent ${task_id} 不存在。请检查 task_id 是否正确。`,
|
||||
};
|
||||
if (agent) {
|
||||
return formatAgentOutput(task_id, agent);
|
||||
}
|
||||
|
||||
// 根据状态返回不同结果
|
||||
if (agent.status === 'running') {
|
||||
const runningTime = Math.round((Date.now() - agent.startedAt.getTime()) / 1000);
|
||||
return {
|
||||
success: true,
|
||||
output: `Agent ${task_id} 仍在运行中...\n` +
|
||||
`- 类型: ${agent.agentName}\n` +
|
||||
`- 任务: ${agent.description}\n` +
|
||||
`- 已运行: ${runningTime} 秒\n\n` +
|
||||
`稍后再次调用 task_output 查询结果,或使用 block: true 等待完成。`,
|
||||
metadata: {
|
||||
agentId: agent.id,
|
||||
status: 'running',
|
||||
agentName: agent.agentName,
|
||||
runningTime,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (agent.status === 'failed') {
|
||||
const duration = agent.completedAt
|
||||
? Math.round((agent.completedAt.getTime() - agent.startedAt.getTime()) / 1000)
|
||||
: 0;
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Agent ${task_id} 执行失败:\n` +
|
||||
`- 类型: ${agent.agentName}\n` +
|
||||
`- 任务: ${agent.description}\n` +
|
||||
`- 耗时: ${duration} 秒\n` +
|
||||
`- 错误: ${agent.error || '未知错误'}`,
|
||||
metadata: {
|
||||
agentId: agent.id,
|
||||
status: 'failed',
|
||||
agentName: agent.agentName,
|
||||
duration,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// completed
|
||||
const duration = agent.completedAt
|
||||
? Math.round((agent.completedAt.getTime() - agent.startedAt.getTime()) / 1000)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `## Agent ${task_id} 执行完成\n\n` +
|
||||
`- 类型: ${agent.agentName}\n` +
|
||||
`- 任务: ${agent.description}\n` +
|
||||
`- 耗时: ${duration} 秒\n` +
|
||||
`- 步数: ${agent.steps || 0}\n\n` +
|
||||
`### 结果\n\n${agent.result || '(无输出)'}`,
|
||||
metadata: {
|
||||
agentId: agent.id,
|
||||
status: 'completed',
|
||||
agentName: agent.agentName,
|
||||
duration,
|
||||
steps: agent.steps,
|
||||
},
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Task ${task_id} not found. Please check the task_id is correct.`,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user