Ctrl K
ComunidadGuías…

lib/project-debate-proposal.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
var path = require("path");
var debateMcp = require("./debate-mcp-server");
var debateModels = require("./debate-model-selection");

function attachDebateProposal(ctx) {
  var pendingProposals = {};

  function hasHomeDebateTopicAnswer(session) {
    if (session && typeof session.homeDebateInitialTopic === "string" && session.homeDebateInitialTopic.trim()) return true;
    var history = session && Array.isArray(session.history) ? session.history : [];
    var firstQuestionId = null;
    for (var i = 0; i < history.length; i++) {
      var event = history[i];
      if (!firstQuestionId && event && event.type === "tool_executing" && event.name === "AskUserQuestion") firstQuestionId = event.id || null;
      if (firstQuestionId && event && event.type === "ask_user_answered" && event.toolId === firstQuestionId) return true;
    }
    return false;
  }

  function publicProposal(briefData) {
    return {
      proposalId: briefData.proposalId,
      topic: briefData.topic || "Untitled debate",
      format: briefData.format || "free_discussion",
      context: briefData.context || "",
      specialRequests: briefData.specialRequests || null,
      moderatorId: briefData.moderatorId || null,
      modelSelections: Array.isArray(briefData.modelSelections) ? briefData.modelSelections : [],
      panelists: Array.isArray(briefData.panelists) ? briefData.panelists.map(function (panelist) {
        return { mateId: panelist.mateId, role: panelist.role || "", brief: panelist.brief || "" };
      }) : [],
    };
  }

  function record(session, event) {
    if (typeof ctx.recordSessionEvent === "function") ctx.recordSessionEvent(session, event);
  }

  function getToolDefs(boundSession) {
    return debateMcp.getToolDefs(function onPropose(briefData) {
      if (!boundSession) {
        return Promise.resolve({
          action: "error",
          error: "Debate proposals require an active Clay session.",
        });
      }
      var projectOwnerId = typeof ctx.getProjectOwnerId === "function" ? ctx.getProjectOwnerId() : null;
      if (boundSession.homeDebatePlanning === true
          && (ctx.isHostAgent !== true || ctx.isMate !== true || boundSession.debateSetupMode !== true
            || !boundSession.ownerId || (projectOwnerId && boundSession.ownerId !== projectOwnerId))) {
        return Promise.resolve({
          action: "error",
          error: "This Home debate proposal is not bound to an owned Clay planning session.",
        });
      }
      if (boundSession.homeDebatePlanning === true && !hasHomeDebateTopicAnswer(boundSession)) {
        return Promise.resolve({ action: "error", error: "Ask the user for a debate topic before proposing a debate." });
      }
      return new Promise(function (resolve) {
        var proposalId = "dp_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
        briefData.proposalId = proposalId;
        var moderatorId = ctx.isMate ? path.basename(ctx.cwd) : briefData.moderatorId;
        var userId = boundSession.ownerId || (typeof ctx.getProjectOwnerId === "function" ? ctx.getProjectOwnerId() : null);
        var mateCtx = ctx.buildMateCtx(userId);
        function register(selections) {
          briefData.modelSelections = selections;
          pendingProposals[proposalId] = {
            resolve: resolve,
            briefData: briefData,
            session: boundSession,
          };
          record(boundSession, { type: "debate_proposal", proposal: publicProposal(briefData) });
        }
        if (typeof ctx.getVendorModelCatalog !== "function" && typeof ctx.getVendorModelCatalogForSession !== "function") register([]);
        else debateModels.loadSelections(ctx, null, mateCtx, moderatorId, briefData.panelists || [], boundSession).then(register).catch(function () { register([]); });
      });
    });
  }

  function createMcpServer(adapter, boundSession) {
    var toolDefs = getToolDefs(boundSession);

    return adapter.createToolServer({
      name: "clay-debate",
      version: "1.0.0",
      tools: toolDefs,
    });
  }

  function getBridgeTools(boundSession, normalizeSchema) {
    if (!boundSession) return [];
    var toolDefs = getToolDefs(boundSession);
    return toolDefs.map(function (tool) {
      return {
        server: "clay-debate",
        name: tool.name,
        description: tool.description || tool.name,
        inputSchema: normalizeSchema(tool.inputSchema),
      };
    });
  }

  function callBridgeTool(boundSession, toolName, args) {
    if (!boundSession) return Promise.reject(new Error("Debate proposals require a valid Clay session."));
    var toolDefs = getToolDefs(boundSession);
    for (var i = 0; i < toolDefs.length; i++) {
      if (toolDefs[i].name === toolName && typeof toolDefs[i].handler === "function") {
        return Promise.resolve(toolDefs[i].handler(args || {}));
      }
    }
    return Promise.reject(new Error("Session tool not found: clay-debate/" + toolName));
  }

  function findPendingProposal(ws, msg) {
    var keys = Object.keys(pendingProposals);
    if (keys.length === 0) return null;
    var key = msg.proposalId || null;
    if (!key && ws && Number.isInteger(ws._clayActiveSession)) {
      for (var i = keys.length - 1; i >= 0; i--) {
        var candidate = pendingProposals[keys[i]];
        if (candidate.session && candidate.session.localId === ws._clayActiveSession) {
          key = keys[i];
          break;
        }
      }
    }
    if (!key) key = keys[keys.length - 1];
    var pending = pendingProposals[key];
    if (!pending) return null;
    return { key: key, pending: pending };
  }

  function rejectProposal(ws, pending, error) {
    try {
      ctx.sendTo(ws, { type: "debate_error", error: error });
    } catch (sendErr) {
      console.error("[debate] Failed to send proposal error:", sendErr && sendErr.message ? sendErr.message : sendErr);
    }
    record(pending.session, { type: "debate_proposal_resolved", proposalId: pending.briefData.proposalId, action: "error", error: error });
    pending.resolve({ action: "error", error: error });
  }

  function validateProposal(ws, pending) {
    var briefData = pending.briefData;
    var moderatorId = ctx.isMate ? path.basename(ctx.cwd) : briefData.moderatorId;
    if (!moderatorId || typeof moderatorId !== "string") {
      return { error: "A valid Mate moderator is required to start this debate." };
    }

    var userId = ws && ws._clayUser
      ? ws._clayUser.id
      : (pending.session.ownerId || ctx.getProjectOwnerId() || null);
    var mateCtx = ctx.buildMateCtx(userId);
    if (!ctx.getMate(mateCtx, moderatorId)) {
      return { error: "The selected debate moderator is unavailable." };
    }

    var panelists = briefData.panelists;
    if (!Array.isArray(panelists) || panelists.length === 0) {
      return { error: "At least one valid panelist is required to start this debate." };
    }

    var seen = {};
    for (var i = 0; i < panelists.length; i++) {
      var panelistId = panelists[i] && panelists[i].mateId;
      if (!panelistId || typeof panelistId !== "string" || !ctx.getMate(mateCtx, panelistId)) {
        return { error: "One or more selected debate panelists are unavailable." };
      }
      if (panelistId === moderatorId) {
        return { error: "The debate moderator cannot also be a panelist." };
      }
      if (seen[panelistId]) {
        return { error: "The same Mate cannot be added to a debate more than once." };
      }
      seen[panelistId] = true;
    }

    return { moderatorId: moderatorId };
  }

  async function startApprovedProposal(ws, msg, found) {
    var pending = found.pending;
    if (pending.responding) return;
    pending.responding = true;
    if (msg.action !== "start") {
      delete pendingProposals[found.key];
      record(pending.session, { type: "debate_proposal_resolved", proposalId: pending.briefData.proposalId, action: "cancel" });
      pending.resolve({ action: "cancel" });
      return;
    }

    var validated = validateProposal(ws, pending);
    if (validated.error) {
      delete pendingProposals[found.key];
      rejectProposal(ws, pending, validated.error);
      return;
    }

    var userId = ws && ws._clayUser ? ws._clayUser.id : (pending.session.ownerId || ctx.getProjectOwnerId() || null);
    var mateCtx = ctx.buildMateCtx(userId);
    var modelResult = await debateModels.validateSelections(ctx, ws, mateCtx, validated.moderatorId, pending.briefData.panelists, msg.modelOverrides, pending.session);
    if (modelResult.error) {
      delete pendingProposals[found.key];
      rejectProposal(ws, pending, modelResult.error);
      return;
    }
    pending.briefData.participantModels = modelResult.selections;
    delete pendingProposals[found.key];

    try {
      var result = ctx.startDebate(pending.session, pending.briefData, validated.moderatorId, ws);
      if (result && result.error) {
        rejectProposal(ws, pending, result.error);
        return true;
      }
      record(pending.session, { type: "debate_proposal_resolved", proposalId: pending.briefData.proposalId, action: "start" });
      pending.resolve({ action: "start" });
    } catch (err) {
      var detail = err && err.message ? err.message : String(err);
      console.error("[debate] Failed to start approved proposal:", detail);
      rejectProposal(ws, pending, "The debate could not be started. Check the server logs for details.");
    }
  }

  function handleMessage(ws, msg) {
    if (msg.type !== "debate_proposal_response") return false;

    var found = findPendingProposal(ws, msg);
    if (!found) return true;
    if (msg.sessionId != null && (String(found.pending.session.localId) !== String(msg.sessionId)
        || String(ws._clayActiveSession) !== String(msg.sessionId)
        || (found.pending.session.ownerId && (!ws._clayUser || ws._clayUser.id !== found.pending.session.ownerId)))) {
      ctx.sendTo(ws, { type: "debate_error", error: "This proposal belongs to a different session." });
      return true;
    }
    startApprovedProposal(ws, msg, found).catch(function (error) {
      delete pendingProposals[found.key];
      rejectProposal(ws, found.pending, error && error.message ? error.message : "The selected debate models could not be verified.");
    });
    return true;
  }

  function handleHomeMessage(ws, msg, expectedSession) {
    var found = findPendingProposal(ws, msg);
    if (!found || found.pending.session !== expectedSession) return false;
    if (expectedSession.ownerId && (!ws || !ws._clayUser || ws._clayUser.id !== expectedSession.ownerId)) return false;
    return handleMessage(ws, msg);
  }

  return {
    createMcpServer: createMcpServer,
    getToolDefs: getToolDefs,
    getBridgeTools: getBridgeTools,
    callBridgeTool: callBridgeTool,
    handleMessage: handleMessage,
    handleHomeMessage: handleHomeMessage,
  };
}

module.exports = { attachDebateProposal: attachDebateProposal };