/** * Stream Executor — 在复用会话中执行 prompt * * 职责: * - 绑定流式事件监听器(acp_event / session_update / error) * - IdleTimer 空闲超时保护 * - 串行化队列(同一 session 内按序执行) * - 收集 stdout 文本 * * 不负责: * - 会话创建/销毁(由 SessionPool 决定) * - 重试策略(由 AgentManager._runViaProtocol 决定) * * 设计原则: * 事件处理完全协议无关。所有 Protocol 统一 emit: * - session_update { update: { sessionUpdate, content: { text } } } * - acp_event { type, method, params, timestamp } * 前端根据 method 前缀(session/、codex/)自行解析渲染。 */ const { IdleTimer } = require('../engine'); /** * 在复用会话中执行 prompt * * @param {object} session - SessionPool 返回的 session 对象 * @param {string} prompt - 完整 prompt 文本 * @param {object} [options] * @param {function} [options.onData] - 流式文本回调 * @param {function} [options.onAcpEvent] - 原始协议事件回调(透传) * @param {object} [options.logSession] - 日志会话 * @param {number} [options.idleTimeoutMs=1800000] - 空闲超时 (默认30分钟,由上层心跳监控负责真正的超时管理) * @returns {Promise} stdout 文本 */ async function runPromptInSession(session, prompt, options = {}) { const run = async () => { const { onData, onAcpEvent, logSession, idleTimeoutMs = 1_800_000 } = options; const protocol = session.protocol; let stdout = ''; // ── IdleTimer 空闲超时保护 ── let idleReject = null; const idleTimer = new IdleTimer(idleTimeoutMs, () => { const err = new Error( `Agent ${session.agentName} idle timeout: no stream data for ${idleTimeoutMs}ms` ); err.code = 'IDLE_TIMEOUT'; if (idleReject) idleReject(err); }, { label: `acp-idle:${session.agentName}` }); // ── 事件监听器(协议无关) ── let currentToolInput = ''; const onAcpEventWrapped = async (event) => { if (logSession) { const direction = event.type === 'request' ? 'out' : 'in'; await logSession.logProtocolEvent(session.agentName, direction, event); } if (event.method === 'claude/tool_use_start') { currentToolInput = ''; const toolName = event.params.name; const toolMsg = `\n🔧 [TOOL_CALL] ${toolName} (ID: ${event.params.tool_use_id || 'unknown'})\n`; stdout += toolMsg; if (onData) onData(toolMsg); } else if (event.method === 'claude/stream/content_block_stop') { if (currentToolInput) { const inputLog = ` 输入: ${currentToolInput.slice(0, 500)}${currentToolInput.length > 500 ? '...' : ''}\n`; stdout += inputLog; if (onData) onData(inputLog); currentToolInput = ''; } } else if (event.method?.startsWith('claude/')) { const innerEvent = event.params; if (innerEvent?.delta?.type === 'input_json_delta' && innerEvent.delta.partial_json) { currentToolInput += innerEvent.delta.partial_json; } if (innerEvent?.type === 'tool_result' || innerEvent?.content_block?.type === 'tool_result') { const result = innerEvent.content || innerEvent.content_block?.content || ''; const resultStr = typeof result === 'string' ? result : JSON.stringify(result); const truncated = resultStr.slice(0, 1000) + (resultStr.length > 1000 ? `...(${resultStr.length} chars)` : ''); const resultLog = `\n 结果: ${truncated}\n`; stdout += resultLog; if (onData) onData(resultLog); } } if (onAcpEvent) onAcpEvent(event); }; const onSessionUpdateWrapped = async (params) => { idleTimer.touch(); const update = params.update; const text = update?.content?.text || ''; // 协议无关:所有 Protocol 统一 emit sessionUpdate 格式 if (update.sessionUpdate === 'agent_thought_chunk') { // thought chunks(目前仅 ACP 会 emit),同样透传 if (logSession) await logSession.logStreamChunk(session.agentName, text, 'thought'); if (onData) onData(text); } else if (update.sessionUpdate === 'agent_message_chunk') { stdout += text; if (logSession) await logSession.logStreamChunk(session.agentName, text, 'message'); if (onData) onData(text); } }; const onErrorWrapped = async (err) => { if (logSession) { await logSession.logAgentError(session.agentName, err, { type: `${session.protocolName}_error`, }); } }; // ── 绑定 → 执行 → 解绑 ── protocol.on('acp_event', onAcpEventWrapped); protocol.on('session_update', onSessionUpdateWrapped); protocol.on('error', onErrorWrapped); try { const result = await new Promise((resolve, reject) => { idleReject = reject; idleTimer.start(); protocol.sendPrompt(prompt).then( () => resolve(stdout), (err) => reject(err) ); }); return result; } finally { idleTimer.cancel(); protocol.off('acp_event', onAcpEventWrapped); protocol.off('session_update', onSessionUpdateWrapped); protocol.off('error', onErrorWrapped); } }; // 串行化:同一 session 内的调用按队列顺序执行 const chained = session.callQueue.then(run, run); session.callQueue = chained.then(() => undefined, () => undefined); return chained; } module.exports = { runPromptInSession };