/** * 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=120000] - 空闲超时 * @returns {Promise} stdout 文本 */ async function runPromptInSession(session, prompt, options = {}) { const run = async () => { const { onData, onAcpEvent, logSession, idleTimeoutMs = 120_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}` }); // ── 事件监听器(协议无关) ── const onAcpEventWrapped = async (event) => { if (logSession) { const direction = event.type === 'request' ? 'out' : 'in'; await logSession.logProtocolEvent(session.agentName, direction, event); } 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 };