stream-executor.js 5.51 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=1800000] - 空闲超时 (默认30分钟,由上层心跳监控负责真正的超时管理)
 * @returns {Promise<string>} 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 };