parser.js 5.42 KB
/**
 * Parser - 解析 Agent 调用和结果标记
 *
 * 解析策略(按优先级):
 * 1. JSON 优先:从 ```json 代码块中提取结构化调用指令
 * 2. 文本标记降级:正则匹配 [CALL:...][/CALL] 和 [RESULT:...][/RESULT]
 *
 * 串行决策约束:即使解析到多个 CALL,也只返回最后一个。
 * 这是刻意的设计决策(见架构文档 §4.4),确保因果链完整性。
 */
class Parser {
  /**
   * 解析 CALL 指令
   * 优先尝试 JSON 解析,失败时降级到正则
   *
   * @param {string} output - Agent 输出
   * @param {Object} [options]
   * @param {Object} [options.agents] - Agent 配置(用于 parallelGroup 判断)
   * @param {boolean} [options.allowParallel] - 是否允许并行调用所有的 calls
   * @returns {Array<{agent: string, task: string}>}
   */
  static parseCalls(output, options = {}) {
    // 1. 尝试 JSON 优先解析
    const jsonCalls = this._parseCallsFromJson(output);
    if (jsonCalls.length > 0) {
      if (options.allowParallel) return jsonCalls;
      return this._applySerialConstraint(jsonCalls, options.agents);
    }

    // 2. 降级到正则解析
    const regexCalls = this._parseCallsFromRegex(output);
    if (options.allowParallel) return regexCalls;
    return this._applySerialConstraint(regexCalls, options.agents);
  }

  /**
   * 从 JSON 代码块中提取 CALL 指令
   * 支持两种 JSON 格式:
   * - 单 CALL: { "agent": "coder", "task": "..." }
   * - 多 CALL: { "calls": [{ "agent": "coder", "task": "..." }, ...] }
   * @private
   */
  static _parseCallsFromJson(output) {
    const calls = [];

    // 匹配 ```json ... ``` 代码块
    const jsonBlockRegex = /```json\s*\n?([\s\S]*?)```/g;
    let match;

    while ((match = jsonBlockRegex.exec(output)) !== null) {
      try {
        const parsed = JSON.parse(match[1].trim());

        if (Array.isArray(parsed?.calls)) {
          // 多 CALL 格式: { calls: [{ agent, task }, ...] }
          for (const item of parsed.calls) {
            if (item.agent && item.task) {
              calls.push({ agent: item.agent.trim(), task: item.task.trim() });
            }
          }
        } else if (parsed?.agent && parsed?.task) {
          // 单 CALL 格式: { agent, task }
          calls.push({ agent: parsed.agent.trim(), task: parsed.task.trim() });
        }
      } catch {
        // JSON 解析失败,忽略这个代码块
      }
    }

    return calls;
  }

  /**
   * 从文本标记中提取 CALL 指令
   * 只返回最后一个非占位符的 CALL
   * @private
   */
  static _parseCallsFromRegex(output) {
    const calls = [];
    const regex = /\[CALL:\s*([\w-]+)\s*\]([\s\S]*?)\[\/CALL\]/g;
    let match;

    // 占位符模式(示例中的假 agent 名)
    const placeholderPattern = /^(agent[_-]?name|agent|name|xxx|example)$/i;

    while ((match = regex.exec(output)) !== null) {
      const agent = match[1].trim();
      const task = match[2].trim();

      // 跳过占位符示例
      if (placeholderPattern.test(agent)) {
        continue;
      }

      calls.push({ agent, task });
    }

    return calls;
  }

  /**
   * 应用串行决策约束:默认只返回最后一个 CALL
   * 除非 agents 配置了 parallelGroup 且属于同组
   * @private
   */
  static _applySerialConstraint(calls, agents) {
    if (calls.length <= 1) return calls;

    // 如果没有 agents 配置,严格串行
    if (!agents) {
      return [calls[calls.length - 1]];
    }

    // 检查是否所有 CALL 都属于同一个 parallelGroup
    const groups = new Set();
    let hasUngrouped = false;

    for (const call of calls) {
      const cfg = agents[call.agent];
      if (cfg?.parallelGroup) {
        groups.add(cfg.parallelGroup);
      } else {
        hasUngrouped = true;
      }
    }

    // 全部属于同一个 parallelGroup → 允许并行返回
    if (!hasUngrouped && groups.size === 1) {
      return calls;
    }

    // 否则严格串行:只返回最后一个
    return [calls[calls.length - 1]];
  }

  /**
   * 解析 [RESULT: STATUS]...[/RESULT] 标记
   * @param {string} output - Agent 输出
   * @returns {{status: string, message: string}}
   */
  static parseResult(output) {
    // 1. 尝试 JSON 优先
    const jsonResult = this._parseResultFromJson(output);
    if (jsonResult) return jsonResult;

    // 2. 降级到正则
    const match = output.match(/\[RESULT:\s*(\w+)\s*\]([\s\S]*?)\[\/RESULT\]/);

    if (!match) {
      return { status: 'ERROR', message: 'No result marker found' };
    }

    return {
      status: match[1].trim(),
      message: match[2].trim()
    };
  }

  /**
   * 从 JSON 代码块中提取 RESULT
   * 格式: { "status": "DONE", "message": "..." }
   * @private
   */
  static _parseResultFromJson(output) {
    const jsonBlockRegex = /```json\s*\n?([\s\S]*?)```/g;
    let match;

    while ((match = jsonBlockRegex.exec(output)) !== null) {
      try {
        const parsed = JSON.parse(match[1].trim());
        if (parsed?.status && typeof parsed.status === 'string') {
          const validStatuses = ['DONE', 'PASS', 'FAIL', 'ERROR'];
          const status = parsed.status.toUpperCase().trim();
          if (validStatuses.includes(status)) {
            return {
              status,
              message: (parsed.message || '').trim(),
            };
          }
        }
      } catch {
        // JSON 解析失败,继续尝试下一个代码块
      }
    }

    return null;
  }
}

module.exports = { Parser };