stream-executor.js 3.99 KB
/**
 * 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<string>} 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 };