fix(sub-agent): stdout output, real-time progress, config externalization
- config/sub_agent: 子智能体模型配置走 deploy_config_path 直指,无回退 - easyagent/config: ensureConfig 文件不存在时 throw 而非自动创建 - easyagent/batch: 所有输出走 stdout JSONL (emit),不写文件;conversation 走 emit 不写工作区 - sub_agent_manager: 新增 conversation_file;运行期间 select 非阻塞 drain stdout 即时写 progress
This commit is contained in:
parent
6255efc594
commit
960d087db0
@ -2,7 +2,12 @@
|
||||
|
||||
import os
|
||||
|
||||
from .paths import _resolve_repo_path, DATA_DIR, DEFAULT_PROJECT_PATH
|
||||
from .paths import (
|
||||
_resolve_repo_path,
|
||||
DATA_DIR,
|
||||
DEFAULT_PROJECT_PATH,
|
||||
deploy_config_path,
|
||||
)
|
||||
|
||||
# 子智能体服务
|
||||
SUB_AGENT_SERVICE_BASE_URL = os.environ.get("SUB_AGENT_SERVICE_URL", "http://127.0.0.1:8092")
|
||||
@ -17,6 +22,9 @@ SUB_AGENT_STATE_FILE = _resolve_repo_path(os.environ.get("SUB_AGENT_STATE_FILE",
|
||||
SUB_AGENT_PROJECT_RESULTS_DIR = _resolve_repo_path(os.environ.get("SUB_AGENT_PROJECT_RESULTS_DIR", ""), f"{DEFAULT_PROJECT_PATH}/sub_agent_results")
|
||||
SUB_AGENT_MAX_ACTIVE = int(os.environ.get("SUB_AGENT_MAX_ACTIVE", "5"))
|
||||
|
||||
# 子智能体模型配置文件(仅从部署目录读取,读不到就报错)
|
||||
SUB_AGENT_MODELS_CONFIG_FILE = os.environ.get("SUB_AGENT_MODELS_CONFIG_FILE", "") or deploy_config_path("sub_agent_models.json")
|
||||
|
||||
__all__ = [
|
||||
"SUB_AGENT_SERVICE_BASE_URL",
|
||||
"SUB_AGENT_DEFAULT_TIMEOUT",
|
||||
@ -25,4 +33,5 @@ __all__ = [
|
||||
"SUB_AGENT_PROJECT_RESULTS_DIR",
|
||||
"SUB_AGENT_STATE_FILE",
|
||||
"SUB_AGENT_MAX_ACTIVE",
|
||||
"SUB_AGENT_MODELS_CONFIG_FILE",
|
||||
]
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
|
||||
/**
|
||||
* easyagent 批处理模式
|
||||
* 用于子智能体执行,不需要交互式 CLI
|
||||
* 所有输出走 stdout(JSONL),不写文件。
|
||||
* Python 侧捕获 stdout 后写入正式存储目录。
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
@ -13,19 +14,23 @@ const { executeTool } = require('../tools/dispatcher');
|
||||
const { getModelByKey } = require('../config');
|
||||
const { applyUsage, normalizeUsagePayload } = require('../utils/token_usage');
|
||||
|
||||
// 解析命令行参数
|
||||
// ── stdout 输出 ──
|
||||
function emit(type, data) {
|
||||
const line = JSON.stringify({ type, ...data });
|
||||
process.stdout.write(line + '\n');
|
||||
}
|
||||
|
||||
// ── 解析命令行参数 ──
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const config = {
|
||||
workspace: process.cwd(),
|
||||
taskFile: null,
|
||||
systemPromptFile: null,
|
||||
outputFile: null,
|
||||
statsFile: null,
|
||||
progressFile: null,
|
||||
agentId: null,
|
||||
modelKey: null,
|
||||
thinkingMode: null,
|
||||
configFile: null,
|
||||
timeout: 600,
|
||||
};
|
||||
|
||||
@ -37,12 +42,6 @@ function parseArgs() {
|
||||
config.taskFile = args[++i];
|
||||
} else if (arg === '--system-prompt-file' && i + 1 < args.length) {
|
||||
config.systemPromptFile = args[++i];
|
||||
} else if (arg === '--output-file' && i + 1 < args.length) {
|
||||
config.outputFile = args[++i];
|
||||
} else if (arg === '--stats-file' && i + 1 < args.length) {
|
||||
config.statsFile = args[++i];
|
||||
} else if (arg === '--progress-file' && i + 1 < args.length) {
|
||||
config.progressFile = args[++i];
|
||||
} else if (arg === '--agent-id' && i + 1 < args.length) {
|
||||
config.agentId = args[++i];
|
||||
} else if (arg === '--model-key' && i + 1 < args.length) {
|
||||
@ -51,13 +50,15 @@ function parseArgs() {
|
||||
config.thinkingMode = String(args[++i] || '').trim().toLowerCase();
|
||||
} else if (arg === '--timeout' && i + 1 < args.length) {
|
||||
config.timeout = parseInt(args[++i], 10);
|
||||
} else if (arg === '--config-file' && i + 1 < args.length) {
|
||||
config.configFile = args[++i];
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
// ── 读取文件 ──
|
||||
function readFile(filePath) {
|
||||
try {
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
@ -67,80 +68,44 @@ function readFile(filePath) {
|
||||
}
|
||||
}
|
||||
|
||||
// 写入输出文件
|
||||
function writeOutput(filePath, data) {
|
||||
try {
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
||||
} catch (err) {
|
||||
console.error(`写入输出文件失败: ${filePath}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新统计文件
|
||||
function updateStats(statsFile, stats) {
|
||||
try {
|
||||
fs.writeFileSync(statsFile, JSON.stringify(stats, null, 2), 'utf8');
|
||||
} catch (err) {
|
||||
console.error(`更新统计文件失败: ${statsFile}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
function appendProgress(progressFile, entry) {
|
||||
if (!progressFile) return;
|
||||
try {
|
||||
const dir = path.dirname(progressFile);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.appendFileSync(progressFile, `${JSON.stringify(entry)}\n`, 'utf8');
|
||||
} catch (err) {
|
||||
console.error(`写入进度失败: ${progressFile}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// 主函数
|
||||
// ── 主函数 ──
|
||||
async function main() {
|
||||
const config = parseArgs();
|
||||
|
||||
if (!config.taskFile || !config.systemPromptFile || !config.outputFile) {
|
||||
console.error('缺少必需参数: --task-file, --system-prompt-file, --output-file');
|
||||
if (!config.taskFile || !config.systemPromptFile) {
|
||||
console.error('缺少必需参数: --task-file, --system-prompt-file');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 读取任务和系统提示
|
||||
// 读取任务
|
||||
const taskMessage = readFile(config.taskFile);
|
||||
const systemPrompt = readFile(config.systemPromptFile);
|
||||
|
||||
// 加载模型配置
|
||||
const modelConfig = require('../config');
|
||||
const ensuredConfig = modelConfig.ensureConfig();
|
||||
let ensuredConfig;
|
||||
try {
|
||||
ensuredConfig = modelConfig.ensureConfig(config.configFile);
|
||||
} catch (e) {
|
||||
emit('output', { success: false, summary: '模型配置加载失败: ' + e.message, error: 'config_load_error' });
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!ensuredConfig.valid_models || ensuredConfig.valid_models.length === 0) {
|
||||
writeOutput(config.outputFile, {
|
||||
success: false,
|
||||
summary: '未找到可用模型配置',
|
||||
error: 'no_model_config',
|
||||
});
|
||||
emit('output', { success: false, summary: '未找到可用模型配置', error: 'no_model_config' });
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 使用指定模型或默认模型
|
||||
// 选择模型
|
||||
const modelKey = config.modelKey || ensuredConfig.default_model_key;
|
||||
const model = getModelByKey(ensuredConfig, modelKey);
|
||||
|
||||
if (!model) {
|
||||
writeOutput(config.outputFile, {
|
||||
success: false,
|
||||
summary: `未找到模型: ${modelKey}`,
|
||||
error: 'model_not_found',
|
||||
});
|
||||
emit('output', { success: false, summary: '未找到模型: ' + modelKey, error: 'model_not_found' });
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 加载工具定义
|
||||
// 加载工具
|
||||
const tools = JSON.parse(fs.readFileSync(path.join(__dirname, '../../doc/tools.json'), 'utf8'));
|
||||
|
||||
// 添加 finish_task 工具
|
||||
tools.push({
|
||||
type: 'function',
|
||||
function: {
|
||||
@ -149,14 +114,8 @@ async function main() {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: '任务是否成功完成。true=成功完成所有要求;false=部分完成或遇到问题无法继续。',
|
||||
},
|
||||
summary: {
|
||||
type: 'string',
|
||||
description: '任务完成摘要(50-200字),说明完成了什么工作、生成了哪些文件、关键发现或结果。如果失败,说明原因和已完成的部分。',
|
||||
},
|
||||
success: { type: 'boolean', description: '任务是否成功完成。' },
|
||||
summary: { type: 'string', description: '任务完成摘要(50-200字)。' },
|
||||
},
|
||||
required: ['success', 'summary'],
|
||||
},
|
||||
@ -169,7 +128,7 @@ async function main() {
|
||||
{ role: 'user', content: taskMessage },
|
||||
];
|
||||
|
||||
// 统计信息
|
||||
// 统计
|
||||
const stats = {
|
||||
runtime_start: Date.now(),
|
||||
runtime_seconds: 0,
|
||||
@ -185,7 +144,7 @@ async function main() {
|
||||
const startTime = Date.now();
|
||||
const timeoutMs = config.timeout * 1000;
|
||||
let turnCount = 0;
|
||||
const maxTurns = 50; // 防止无限循环
|
||||
const maxTurns = 50;
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
@ -193,48 +152,33 @@ async function main() {
|
||||
stats.api_calls += 1;
|
||||
stats.turn_count = turnCount;
|
||||
|
||||
// 检查超时
|
||||
const elapsed = Date.now() - startTime;
|
||||
if (elapsed > timeoutMs) {
|
||||
writeOutput(config.outputFile, {
|
||||
emit('output', {
|
||||
success: false,
|
||||
summary: '任务超时未完成',
|
||||
timeout: true,
|
||||
stats: {
|
||||
...stats,
|
||||
runtime_seconds: Math.floor(elapsed / 1000),
|
||||
},
|
||||
stats: { ...stats, runtime_seconds: Math.floor(elapsed / 1000) },
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 检查轮次限制
|
||||
if (turnCount > maxTurns) {
|
||||
writeOutput(config.outputFile, {
|
||||
emit('output', {
|
||||
success: false,
|
||||
summary: `任务执行超过 ${maxTurns} 轮,可能陷入循环`,
|
||||
summary: '任务执行超过 ' + maxTurns + ' 轮,可能陷入循环',
|
||||
max_turns_exceeded: true,
|
||||
stats: {
|
||||
...stats,
|
||||
runtime_seconds: Math.floor(elapsed / 1000),
|
||||
},
|
||||
stats: { ...stats, runtime_seconds: Math.floor(elapsed / 1000) },
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 更新统计文件
|
||||
if (config.statsFile) {
|
||||
updateStats(config.statsFile, {
|
||||
...stats,
|
||||
runtime_seconds: Math.floor(elapsed / 1000),
|
||||
turn_count: turnCount,
|
||||
});
|
||||
}
|
||||
// 发送统计更新
|
||||
emit('stats', { ...stats, runtime_seconds: Math.floor(elapsed / 1000), turn_count: turnCount });
|
||||
|
||||
// 调用 API
|
||||
let assistantMessage = { role: 'assistant', content: '', tool_calls: [] };
|
||||
let reasoningBuffer = '';
|
||||
let currentToolCall = null;
|
||||
let usage = null;
|
||||
|
||||
try {
|
||||
@ -251,37 +195,19 @@ async function main() {
|
||||
const delta = choice?.delta;
|
||||
if (!delta) continue;
|
||||
|
||||
// 处理内容
|
||||
if (delta.content) {
|
||||
assistantMessage.content += delta.content;
|
||||
}
|
||||
|
||||
// 处理 reasoning_content(兼容 reasoning_details)
|
||||
if (delta.reasoning_content || delta.reasoning_details || choice?.reasoning_details) {
|
||||
let rc = '';
|
||||
if (delta.reasoning_content) {
|
||||
rc = delta.reasoning_content;
|
||||
} else if (delta.reasoning_details) {
|
||||
if (Array.isArray(delta.reasoning_details)) {
|
||||
rc = delta.reasoning_details.map((d) => d.text || '').join('');
|
||||
} else if (typeof delta.reasoning_details === 'string') {
|
||||
rc = delta.reasoning_details;
|
||||
} else if (delta.reasoning_details && typeof delta.reasoning_details.text === 'string') {
|
||||
rc = delta.reasoning_details.text;
|
||||
}
|
||||
} else if (choice?.reasoning_details) {
|
||||
if (Array.isArray(choice.reasoning_details)) {
|
||||
rc = choice.reasoning_details.map((d) => d.text || '').join('');
|
||||
} else if (typeof choice.reasoning_details === 'string') {
|
||||
rc = choice.reasoning_details;
|
||||
} else if (choice.reasoning_details && typeof choice.reasoning_details.text === 'string') {
|
||||
rc = choice.reasoning_details.text;
|
||||
}
|
||||
}
|
||||
if (rc) reasoningBuffer += rc;
|
||||
if (delta.reasoning_content) {
|
||||
reasoningBuffer += delta.reasoning_content;
|
||||
} else if (delta.reasoning_details) {
|
||||
const rd = delta.reasoning_details;
|
||||
if (Array.isArray(rd)) reasoningBuffer += rd.map(d => d.text || '').join('');
|
||||
else if (typeof rd === 'string') reasoningBuffer += rd;
|
||||
else if (rd && typeof rd.text === 'string') reasoningBuffer += rd.text;
|
||||
}
|
||||
|
||||
// 处理工具调用
|
||||
if (delta.tool_calls) {
|
||||
for (const tc of delta.tool_calls) {
|
||||
if (tc.index !== undefined) {
|
||||
@ -291,55 +217,37 @@ async function main() {
|
||||
type: 'function',
|
||||
function: { name: '', arguments: '' },
|
||||
};
|
||||
currentToolCall = assistantMessage.tool_calls[tc.index];
|
||||
} else {
|
||||
currentToolCall = assistantMessage.tool_calls[tc.index];
|
||||
}
|
||||
|
||||
if (tc.id) currentToolCall.id = tc.id;
|
||||
if (tc.function?.name) currentToolCall.function.name += tc.function.name;
|
||||
if (tc.function?.arguments) currentToolCall.function.arguments += tc.function.arguments;
|
||||
const ct = assistantMessage.tool_calls[tc.index];
|
||||
if (tc.id) ct.id = tc.id;
|
||||
if (tc.function?.name) ct.function.name += tc.function.name;
|
||||
if (tc.function?.arguments) ct.function.arguments += tc.function.arguments;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 usage
|
||||
if (chunk.usage) {
|
||||
usage = normalizeUsagePayload(chunk.usage);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
writeOutput(config.outputFile, {
|
||||
emit('output', {
|
||||
success: false,
|
||||
summary: `API 调用失败: ${err.message}`,
|
||||
summary: 'API 调用失败: ' + err.message,
|
||||
error: 'api_error',
|
||||
stats: {
|
||||
...stats,
|
||||
runtime_seconds: Math.floor((Date.now() - startTime) / 1000),
|
||||
},
|
||||
stats: { ...stats, runtime_seconds: Math.floor((Date.now() - startTime) / 1000) },
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 更新 token 统计
|
||||
if (usage) {
|
||||
applyUsage(stats.token_usage, usage);
|
||||
}
|
||||
if (usage) applyUsage(stats.token_usage, usage);
|
||||
|
||||
// 添加助手消息到历史(reasoning_content 放在 content 前)
|
||||
const finalAssistantMessage = reasoningBuffer
|
||||
? {
|
||||
role: 'assistant',
|
||||
reasoning_content: reasoningBuffer,
|
||||
content: assistantMessage.content,
|
||||
tool_calls: assistantMessage.tool_calls,
|
||||
}
|
||||
? { role: 'assistant', reasoning_content: reasoningBuffer, content: assistantMessage.content, tool_calls: assistantMessage.tool_calls }
|
||||
: assistantMessage;
|
||||
messages.push(finalAssistantMessage);
|
||||
|
||||
// 如果没有工具调用,检查是否忘记调用 finish_task
|
||||
if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
|
||||
// 兜底机制:提醒调用 finish_task
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: '如果你已经完成了任务,请调用 finish_task 工具提交完成报告。如果还没有完成,请继续执行任务。',
|
||||
@ -347,95 +255,50 @@ async function main() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 执行工具调用
|
||||
for (const toolCall of assistantMessage.tool_calls) {
|
||||
const toolName = toolCall.function.name;
|
||||
stats.tool_calls = (stats.tool_calls || 0) + 1;
|
||||
|
||||
// 检查是否是 finish_task
|
||||
if (toolName === 'finish_task') {
|
||||
let args = {};
|
||||
try {
|
||||
args = JSON.parse(toolCall.function.arguments || '{}');
|
||||
} catch (err) {
|
||||
writeOutput(config.outputFile, {
|
||||
emit('output', {
|
||||
success: false,
|
||||
summary: 'finish_task 参数解析失败',
|
||||
error: 'invalid_finish_args',
|
||||
stats: {
|
||||
...stats,
|
||||
runtime_seconds: Math.floor((Date.now() - startTime) / 1000),
|
||||
},
|
||||
stats: { ...stats, runtime_seconds: Math.floor((Date.now() - startTime) / 1000) },
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 保存对话记录到交付目录的 .subagent/ 子目录
|
||||
try {
|
||||
// 从任务文件路径推断交付目录
|
||||
const taskFileDir = path.dirname(config.taskFile);
|
||||
// 读取任务消息以获取交付目录路径
|
||||
const taskContent = fs.readFileSync(config.taskFile, 'utf8');
|
||||
const deliverablesDirMatch = taskContent.match(/\*\*交付目录\*\*:(.+)/);
|
||||
if (deliverablesDirMatch) {
|
||||
const deliverablesDirPath = deliverablesDirMatch[1].split('\n')[0].trim();
|
||||
const subagentDir = path.join(deliverablesDirPath, '.subagent');
|
||||
// 输出对话记录到 stdout,由 Python 管理程序写入正式目录
|
||||
emit('conversation', {
|
||||
agent_id: config.agentId,
|
||||
created_at: new Date(startTime).toISOString(),
|
||||
completed_at: new Date().toISOString(),
|
||||
success: args.success,
|
||||
summary: args.summary,
|
||||
messages: messages,
|
||||
stats: { ...stats, runtime_seconds: Math.floor((Date.now() - startTime) / 1000) },
|
||||
});
|
||||
|
||||
// 确保目录存在
|
||||
if (!fs.existsSync(subagentDir)) {
|
||||
fs.mkdirSync(subagentDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 保存对话记录
|
||||
const conversationData = {
|
||||
agent_id: config.agentId,
|
||||
created_at: new Date(startTime).toISOString(),
|
||||
completed_at: new Date().toISOString(),
|
||||
success: args.success,
|
||||
summary: args.summary,
|
||||
messages: messages,
|
||||
stats: {
|
||||
...stats,
|
||||
runtime_seconds: Math.floor((Date.now() - startTime) / 1000),
|
||||
},
|
||||
};
|
||||
|
||||
const conversationFile = path.join(subagentDir, 'conversation.json');
|
||||
fs.writeFileSync(conversationFile, JSON.stringify(conversationData, null, 2), 'utf8');
|
||||
}
|
||||
} catch (err) {
|
||||
// 保存对话记录失败不影响任务完成
|
||||
console.error('保存对话记录失败:', err);
|
||||
}
|
||||
|
||||
// 写入输出并退出
|
||||
writeOutput(config.outputFile, {
|
||||
// 最终输出
|
||||
emit('output', {
|
||||
success: args.success || false,
|
||||
summary: args.summary || '任务完成',
|
||||
stats: {
|
||||
...stats,
|
||||
runtime_seconds: Math.floor((Date.now() - startTime) / 1000),
|
||||
},
|
||||
stats: { ...stats, runtime_seconds: Math.floor((Date.now() - startTime) / 1000) },
|
||||
});
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let progressArgs = {};
|
||||
try {
|
||||
progressArgs = JSON.parse(toolCall.function.arguments || '{}');
|
||||
} catch (err) {
|
||||
progressArgs = { _raw: toolCall.function.arguments || '' };
|
||||
}
|
||||
const progressId = toolCall.id || `tool_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
|
||||
appendProgress(config.progressFile, {
|
||||
id: progressId,
|
||||
tool: toolName,
|
||||
status: 'running',
|
||||
args: progressArgs,
|
||||
ts: Date.now(),
|
||||
});
|
||||
try { progressArgs = JSON.parse(toolCall.function.arguments || '{}'); } catch (e) { progressArgs = { _raw: toolCall.function.arguments || '' }; }
|
||||
const progressId = toolCall.id || 'tool_' + Date.now() + '_' + Math.random().toString(16).slice(2, 8);
|
||||
|
||||
emit('progress', { id: progressId, tool: toolName, status: 'running', args: progressArgs, ts: Date.now() });
|
||||
|
||||
// 执行其他工具
|
||||
const result = await executeTool({
|
||||
workspace: config.workspace,
|
||||
config: ensuredConfig,
|
||||
@ -444,44 +307,28 @@ async function main() {
|
||||
abortSignal: null,
|
||||
});
|
||||
|
||||
appendProgress(config.progressFile, {
|
||||
id: progressId,
|
||||
tool: toolName,
|
||||
status: result && result.success ? 'completed' : 'failed',
|
||||
args: progressArgs,
|
||||
ts: Date.now(),
|
||||
});
|
||||
emit('progress', { id: progressId, tool: toolName, status: result && result.success ? 'completed' : 'failed', args: progressArgs, ts: Date.now() });
|
||||
|
||||
// 更新统计
|
||||
if (toolName === 'read_file') stats.files_read++;
|
||||
else if (toolName === 'edit_file') stats.edit_files++;
|
||||
else if (toolName === 'search_workspace') stats.searches++;
|
||||
else if (toolName === 'web_search' || toolName === 'extract_webpage') stats.web_pages++;
|
||||
else if (toolName === 'run_command') stats.commands++;
|
||||
|
||||
// 添加工具结果到历史
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: toolCall.id,
|
||||
content: result.formatted || '',
|
||||
});
|
||||
messages.push({ role: 'tool', tool_call_id: toolCall.id, content: result.formatted || '' });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
writeOutput(config.outputFile, {
|
||||
emit('output', {
|
||||
success: false,
|
||||
summary: `执行出错: ${err.message}`,
|
||||
summary: '执行出错: ' + err.message,
|
||||
error: 'execution_error',
|
||||
stats: {
|
||||
...stats,
|
||||
runtime_seconds: Math.floor((Date.now() - startTime) / 1000),
|
||||
},
|
||||
stats: { ...stats, runtime_seconds: Math.floor((Date.now() - startTime) / 1000) },
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行
|
||||
main().catch((err) => {
|
||||
console.error('批处理模式执行失败:', err);
|
||||
process.exit(1);
|
||||
|
||||
@ -98,15 +98,10 @@ function buildConfig(raw, filePath) {
|
||||
};
|
||||
}
|
||||
|
||||
function ensureConfig() {
|
||||
const file = DEFAULT_CONFIG_PATH;
|
||||
function ensureConfig(customPath) {
|
||||
const file = customPath || DEFAULT_CONFIG_PATH;
|
||||
if (!fs.existsSync(file)) {
|
||||
const template = {
|
||||
tavily_api_key: '',
|
||||
default_model: '',
|
||||
models: [],
|
||||
};
|
||||
fs.writeFileSync(file, JSON.stringify(template, null, 2), 'utf8');
|
||||
throw new Error(`子智能体模型配置文件不存在: ${file}`);
|
||||
}
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
const raw = JSON.parse(content);
|
||||
|
||||
@ -9,6 +9,7 @@ import shlex
|
||||
import platform
|
||||
import shutil
|
||||
import signal
|
||||
import select
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Dict, List, Optional, Any, Tuple, TYPE_CHECKING
|
||||
|
||||
@ -16,6 +17,7 @@ from config import (
|
||||
OUTPUT_FORMATS,
|
||||
SUB_AGENT_DEFAULT_TIMEOUT,
|
||||
SUB_AGENT_MAX_ACTIVE,
|
||||
SUB_AGENT_MODELS_CONFIG_FILE,
|
||||
SUB_AGENT_STATE_FILE,
|
||||
SUB_AGENT_STATUS_POLL_INTERVAL,
|
||||
SUB_AGENT_TASKS_BASE_DIR,
|
||||
@ -124,16 +126,13 @@ class SubAgentManager:
|
||||
except ValueError as exc:
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
# 创建.subagent目录用于存储对话历史
|
||||
subagent_dir = deliverables_path / ".subagent"
|
||||
subagent_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 准备文件路径
|
||||
task_file = task_root / "task.txt"
|
||||
system_prompt_file = task_root / "system_prompt.txt"
|
||||
output_file = task_root / "output.json"
|
||||
stats_file = task_root / "stats.json"
|
||||
progress_file = task_root / "progress.jsonl"
|
||||
conversation_file = task_root / "conversation.json"
|
||||
|
||||
# 构建用户消息
|
||||
prompt_workspace = self._get_runtime_path(self.project_path)
|
||||
@ -152,9 +151,6 @@ class SubAgentManager:
|
||||
workspace_path=str(self.project_path),
|
||||
task_file=str(task_file),
|
||||
system_prompt_file=str(system_prompt_file),
|
||||
output_file=str(output_file),
|
||||
stats_file=str(stats_file),
|
||||
progress_file=str(progress_file),
|
||||
agent_id=agent_id,
|
||||
timeout_seconds=timeout_seconds,
|
||||
model_key=model_key,
|
||||
@ -206,6 +202,9 @@ class SubAgentManager:
|
||||
return {"success": False, "error": f"构建宿主机沙箱启动命令失败:{exc}"}
|
||||
|
||||
try:
|
||||
logger.debug(f"[SubAgent] 启动进程 task={task_id} execution_mode={execution_mode}")
|
||||
logger.debug(f"[SubAgent] cmd: {self._join_shell_words(launch_cmd)[:500]}")
|
||||
logger.debug(f"[SubAgent] cwd: {launch_cwd}")
|
||||
process = subprocess.Popen(
|
||||
launch_cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
@ -214,6 +213,7 @@ class SubAgentManager:
|
||||
env=env,
|
||||
pass_fds=pass_fds,
|
||||
)
|
||||
logger.debug(f"[SubAgent] 进程已启动 task={task_id} pid={process.pid}")
|
||||
except Exception as exc:
|
||||
return {"success": False, "error": f"启动子智能体失败: {exc}"}
|
||||
finally:
|
||||
@ -231,7 +231,6 @@ class SubAgentManager:
|
||||
"task": task,
|
||||
"status": "running",
|
||||
"deliverables_dir": str(deliverables_path),
|
||||
"subagent_dir": str(subagent_dir),
|
||||
"timeout_seconds": timeout_seconds,
|
||||
"thinking_mode": thinking_mode,
|
||||
"created_at": time.time(),
|
||||
@ -241,6 +240,7 @@ class SubAgentManager:
|
||||
"output_file": str(output_file),
|
||||
"stats_file": str(stats_file),
|
||||
"progress_file": str(progress_file),
|
||||
"conversation_file": str(conversation_file),
|
||||
"pid": process.pid,
|
||||
"execution_mode": execution_mode,
|
||||
"container_name": container_name,
|
||||
@ -374,9 +374,6 @@ class SubAgentManager:
|
||||
workspace_path: str,
|
||||
task_file: str,
|
||||
system_prompt_file: str,
|
||||
output_file: str,
|
||||
stats_file: str,
|
||||
progress_file: str,
|
||||
agent_id: int,
|
||||
timeout_seconds: int,
|
||||
model_key: Optional[str],
|
||||
@ -388,11 +385,9 @@ class SubAgentManager:
|
||||
"--workspace", workspace_path,
|
||||
"--task-file", task_file,
|
||||
"--system-prompt-file", system_prompt_file,
|
||||
"--output-file", output_file,
|
||||
"--stats-file", stats_file,
|
||||
"--progress-file", progress_file,
|
||||
"--agent-id", str(agent_id),
|
||||
"--timeout", str(timeout_seconds),
|
||||
"--config-file", SUB_AGENT_MODELS_CONFIG_FILE,
|
||||
]
|
||||
if model_key:
|
||||
cmd.extend(["--model-key", model_key])
|
||||
@ -417,19 +412,131 @@ class SubAgentManager:
|
||||
cmd = [str(fd_num) if token == "__SECCOMP_FD__" else token for token in plan_command]
|
||||
return cmd, (fd_num,), seccomp_fd
|
||||
def _check_task_status(self, task: Dict) -> Dict:
|
||||
"""检查任务状态,如果完成则解析输出。"""
|
||||
"""检查任务状态,如果完成则从 stdout 解析输出并写入正式目录。"""
|
||||
task_id = task["task_id"]
|
||||
process = self.processes.get(task_id)
|
||||
|
||||
# 检查进程是否结束
|
||||
if process:
|
||||
# 先非阻塞读取 stdout 中已有的行(运行中和已结束都需要)
|
||||
stdout_lines = task.get("_stdout_lines", [])
|
||||
progress_file = Path(task.get("progress_file", ""))
|
||||
new_lines_count = 0
|
||||
|
||||
while True:
|
||||
try:
|
||||
ready, _, _ = select.select([process.stdout], [], [], 0)
|
||||
except (ValueError, OSError):
|
||||
break # fd 已关闭
|
||||
if not ready:
|
||||
break
|
||||
line = process.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
line_text = line.decode('utf-8', errors='replace') if isinstance(line, bytes) else line
|
||||
line_text = line_text.strip()
|
||||
if line_text:
|
||||
stdout_lines.append(line_text)
|
||||
new_lines_count += 1
|
||||
|
||||
if new_lines_count:
|
||||
task["_stdout_lines"] = stdout_lines
|
||||
# 即时写入所有 progress 行到文件(全量重写,保证与内存一致)
|
||||
try:
|
||||
progress_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(str(progress_file), "w", encoding="utf-8") as f:
|
||||
for line_text in stdout_lines:
|
||||
try:
|
||||
entry = json.loads(line_text)
|
||||
if entry.get("type") == "progress":
|
||||
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
returncode = process.poll()
|
||||
if returncode is None:
|
||||
# 进程还在运行
|
||||
return {"status": "running", "task_id": task_id}
|
||||
|
||||
# 进程已结束,读取输出文件
|
||||
# 进程已结束,读取 stderr
|
||||
stderr_text = ""
|
||||
try:
|
||||
stderr_data = process.stderr.read()
|
||||
stderr_text = stderr_data.decode('utf-8', errors='replace') if isinstance(stderr_data, bytes) and stderr_data else ""
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.debug(
|
||||
f"[SubAgent] 进程结束 task={task_id} pid={process.pid} "
|
||||
f"rc={returncode} stdout_lines={len(stdout_lines)} stderr_len={len(stderr_text)}"
|
||||
)
|
||||
if stderr_text:
|
||||
logger.debug(f"[SubAgent] stderr: {stderr_text[:2000]}")
|
||||
|
||||
# 解析累积的 stdout 行,将输出写入正式目录
|
||||
output_data = None
|
||||
progress_entries = []
|
||||
stats_data = None
|
||||
conversation_data = None
|
||||
for line_text in stdout_lines:
|
||||
if not line_text:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line_text)
|
||||
except Exception:
|
||||
continue
|
||||
entry_type = entry.get("type", "")
|
||||
if entry_type == "output":
|
||||
output_data = entry
|
||||
elif entry_type == "progress":
|
||||
progress_entries.append(entry)
|
||||
elif entry_type == "stats":
|
||||
stats_data = entry
|
||||
elif entry_type == "conversation":
|
||||
conversation_data = entry
|
||||
|
||||
# 写入正式目录
|
||||
output_file = Path(task.get("output_file", ""))
|
||||
stats_file = Path(task.get("stats_file", ""))
|
||||
progress_file = Path(task.get("progress_file", ""))
|
||||
conversation_file = Path(task.get("conversation_file", ""))
|
||||
|
||||
if output_data:
|
||||
try:
|
||||
output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_file.write_text(json.dumps(output_data, ensure_ascii=False), encoding="utf-8")
|
||||
logger.debug(f"[SubAgent] output 已写入: {output_file}")
|
||||
except Exception as exc:
|
||||
logger.debug(f"[SubAgent] 写入 output 失败: {exc}")
|
||||
|
||||
if stats_data:
|
||||
try:
|
||||
stats_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
stats_file.write_text(json.dumps(stats_data, ensure_ascii=False), encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if progress_entries:
|
||||
try:
|
||||
progress_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(str(progress_file), "w", encoding="utf-8") as f:
|
||||
for entry in progress_entries:
|
||||
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if conversation_data:
|
||||
try:
|
||||
conversation_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
conversation_file.write_text(json.dumps(conversation_data, ensure_ascii=False), encoding="utf-8")
|
||||
logger.debug(f"[SubAgent] conversation 已写入: {conversation_file}")
|
||||
except Exception as exc:
|
||||
logger.debug(f"[SubAgent] 写入 conversation 失败: {exc}")
|
||||
|
||||
# 读取输出文件
|
||||
output_file = Path(task.get("output_file", ""))
|
||||
logger.debug(f"[SubAgent] 检查输出文件: {output_file} exists={output_file.exists()}")
|
||||
if not output_file.exists():
|
||||
# 输出文件不存在,可能是异常退出
|
||||
task["status"] = "failed"
|
||||
@ -594,7 +701,7 @@ class SubAgentManager:
|
||||
{task}
|
||||
|
||||
**交付目录**:{deliverables_path}
|
||||
请将所有生成的文件保存到此目录。对话历史会自动保存到 {deliverables_path}/.subagent/ 目录。
|
||||
请将所有生成的文件保存到此目录。
|
||||
|
||||
**超时时间**:{timeout_seconds} 秒
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user