"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var warm_transfer_exports = {};
__export(warm_transfer_exports, {
  WarmTransferTask: () => WarmTransferTask,
  createWarmTransferTask: () => createWarmTransferTask
});
module.exports = __toCommonJS(warm_transfer_exports);
var import_rtc_node = require("@livekit/rtc-node");
var import_livekit_server_sdk = require("livekit-server-sdk");
var import_zod = require("zod");
var import_job = require("../job.cjs");
var import_llm = require("../llm/index.cjs");
var import_log = require("../log.cjs");
var import_utils = require("../utils.cjs");
var import_agent = require("../voice/agent.cjs");
var import_agent_session = require("../voice/agent_session.cjs");
var import_background_audio = require("../voice/background_audio.cjs");
var import_room_io = require("../voice/room_io/index.cjs");
function createWarmTransferTask({
  sipCallTo,
  sipTrunkId: rawSipTrunkId,
  sipConnection,
  sipNumber = process.env.LIVEKIT_SIP_NUMBER ?? "",
  sipHeaders = {},
  dtmf,
  ringingTimeout,
  holdAudio = { source: import_background_audio.BuiltinAudioClip.HOLD_MUSIC, volume: 0.8 },
  instructions,
  chatCtx,
  turnDetection,
  tools,
  stt,
  vad,
  llm,
  tts,
  allowInterruptions
} = {}) {
  if (!sipCallTo) {
    throw new Error("`sipCallTo` must be set");
  }
  const sipTrunkId = rawSipTrunkId !== void 0 ? rawSipTrunkId : sipConnection ? null : process.env.LIVEKIT_SIP_OUTBOUND_TRUNK ?? null;
  if (sipTrunkId === null && !sipConnection) {
    throw new Error(
      "`LIVEKIT_SIP_OUTBOUND_TRUNK` environment variable, `sipTrunkId`, or `sipConnection` must be set"
    );
  }
  const humanAgentIdentity = "human-agent-sip";
  const backgroundAudio = new import_background_audio.BackgroundAudioPlayer();
  const logger = (0, import_log.log)();
  let callerRoom = null;
  let humanAgentRoom = null;
  let jobCtx = null;
  let transferAgentSession = null;
  let holdAudioHandle = null;
  let originalIoState = null;
  const humanAgentFailedFut = new import_utils.Future();
  const setIoEnabled = (enabled) => {
    const input = task.session.input;
    const output = task.session.output;
    originalIoState ??= {
      audioInput: input.audioEnabled,
      audioOutput: output.audioEnabled,
      transcriptionOutput: output.transcriptionEnabled
    };
    if (input.audio) input.setAudioEnabled(enabled && originalIoState.audioInput);
    if (output.audio) output.setAudioEnabled(enabled && originalIoState.audioOutput);
    if (output.transcription)
      output.setTranscriptionEnabled(enabled && originalIoState.transcriptionOutput);
  };
  const setResult = (result) => {
    if (task.done) return;
    if (transferAgentSession) {
      transferAgentSession.shutdown();
      transferAgentSession = null;
      humanAgentRoom = null;
    }
    if (holdAudioHandle) {
      holdAudioHandle.stop();
      holdAudioHandle = null;
    }
    void backgroundAudio.close().catch((error) => {
      logger.warn({ error }, "failed to close background audio");
    });
    setIoEnabled(true);
    task.complete(result);
  };
  const onHumanAgentRoomClose = (reason) => {
    logger.debug({ reason }, "human agent room closed");
    humanAgentFailedFut.resolve();
    setResult(new import_llm.ToolError(`room closed: ${reason}`));
  };
  const onCallerParticipantDisconnected = (participant) => {
    if (!import_room_io.DEFAULT_PARTICIPANT_KINDS.includes(participant.kind)) {
      return;
    }
    logger.info(
      { participantIdentity: participant.identity },
      "participant disconnected from caller room, closing"
    );
    if (!(callerRoom == null ? void 0 : callerRoom.name)) {
      return;
    }
    callerRoom.off(import_rtc_node.RoomEvent.ParticipantDisconnected, onCallerParticipantDisconnected);
    if (!jobCtx) {
      logger.warn("no job context captured, cannot delete caller room");
      return;
    }
    const callerRoomName = callerRoom.name;
    void jobCtx.deleteRoom(callerRoomName).catch((error) => {
      logger.warn({ error }, "failed to delete caller room");
    });
  };
  const cleanupHumanAgentDial = async (session, room) => {
    await (room == null ? void 0 : room.disconnect().catch((error) => {
      logger.warn({ error }, "failed to disconnect human agent room");
    }));
    await (session == null ? void 0 : session.close().catch((error) => {
      logger.warn({ error }, "failed to close transfer agent session");
    }));
  };
  const mergeCalls = async () => {
    if (!(callerRoom == null ? void 0 : callerRoom.name) || !(humanAgentRoom == null ? void 0 : humanAgentRoom.name)) {
      throw new Error("calls are not ready to merge");
    }
    humanAgentRoom.off(import_rtc_node.RoomEvent.Disconnected, onHumanAgentRoomClose);
    logger.debug(
      { humanAgentIdentity, callerRoom: callerRoom.name },
      "moving human agent to caller room"
    );
    const info = (jobCtx ?? (0, import_job.getJobContext)()).info;
    const rooms = new import_livekit_server_sdk.RoomServiceClient(info.url, info.apiKey, info.apiSecret);
    await rooms.moveParticipant(humanAgentRoom.name, humanAgentIdentity, callerRoom.name);
  };
  const dialHumanAgent = async (signal) => {
    var _a;
    if (!(callerRoom == null ? void 0 : callerRoom.name)) {
      throw new Error("caller room is not available");
    }
    const localIdentity = (_a = callerRoom.localParticipant) == null ? void 0 : _a.identity;
    if (!localIdentity) {
      throw new Error("caller room local participant is not available");
    }
    const ctx = jobCtx ?? (0, import_job.getJobContext)();
    const humanAgentRoomName = `${callerRoom.name}-human-agent`;
    const room = new import_rtc_node.Room();
    const transferAgent = new import_agent.Agent({
      instructions: task.instructions,
      stt: task.stt,
      vad: task.vad,
      llm: task.llm,
      tts: task.tts,
      tools: task.toolCtx.tools,
      chatCtx: task.chatCtx.copy(),
      turnDetection: turnDetection ?? void 0,
      allowInterruptions
    });
    let session;
    let completed = false;
    try {
      const token = new import_livekit_server_sdk.AccessToken(void 0, void 0, { identity: localIdentity });
      token.kind = "agent";
      token.addGrant({
        roomJoin: true,
        room: humanAgentRoomName,
        canUpdateOwnMetadata: true,
        canPublish: true,
        canSubscribe: true
      });
      logger.debug({ wsUrl: ctx.info.url, humanAgentRoomName }, "connecting to human agent room");
      const jwt = await token.toJwt();
      room.on(import_rtc_node.RoomEvent.Disconnected, onHumanAgentRoomClose);
      const connected = await (0, import_utils.waitUntilAborted)(room.connect(ctx.info.url, jwt), signal);
      if (connected.isAborted) {
        throw new Error("dial cancelled");
      }
      session = new import_agent_session.AgentSession({
        vad: task.session.vad,
        llm: task.session.llm,
        stt: task.session.stt,
        tts: task.session.tts,
        turnDetection: task.session.turnDetection
      });
      const started = await (0, import_utils.waitUntilAborted)(
        session.start({
          agent: transferAgent,
          room,
          inputOptions: {
            closeOnDisconnect: true,
            // Delete the human agent room on shutdown so its WebSocket doesn't
            // leak across transfers.
            deleteRoomOnClose: true,
            participantIdentity: humanAgentIdentity
          },
          record: false
        }),
        signal
      );
      if (started.isAborted) {
        throw new Error("dial cancelled");
      }
      const sip = new import_livekit_server_sdk.SipClient(ctx.info.url);
      const dialed = await (0, import_utils.waitUntilAborted)(
        sip.createSipParticipant(
          sipTrunkId ?? "",
          sipCallTo,
          humanAgentRoomName,
          {
            participantIdentity: humanAgentIdentity,
            waitUntilAnswered: true,
            fromNumber: sipNumber || void 0,
            headers: sipHeaders,
            dtmf: dtmf ?? void 0,
            // SIP API takes whole seconds (BigInt coercion throws on fractional input).
            ringingTimeout: ringingTimeout != null ? Math.round(ringingTimeout / 1e3) : void 0
          },
          sipConnection
        ),
        signal
      );
      if (dialed.isAborted) {
        throw new Error("dial cancelled");
      }
      humanAgentRoom = room;
      completed = true;
      return session;
    } finally {
      if (!completed) {
        room.off(import_rtc_node.RoomEvent.Disconnected, onHumanAgentRoomClose);
        await cleanupHumanAgentDial(session, room);
      }
    }
  };
  const transferTools = [
    (0, import_llm.tool)({
      name: "connect_to_caller",
      description: "Called when the human agent wants to connect to the caller.",
      flags: import_llm.ToolFlag.IGNORE_ON_ENTER,
      execute: async () => {
        logger.debug("connecting to caller");
        if (!callerRoom) {
          throw new Error("caller room is not available");
        }
        await mergeCalls();
        setResult({ humanAgentIdentity });
        callerRoom.on(import_rtc_node.RoomEvent.ParticipantDisconnected, onCallerParticipantDisconnected);
      }
    }),
    (0, import_llm.tool)({
      name: "decline_transfer",
      description: "Handles the case when the human agent explicitly declines to connect to the caller.",
      parameters: import_zod.z.object({
        reason: import_zod.z.string().describe("A short explanation of why the human agent declined to connect to the caller")
      }),
      flags: import_llm.ToolFlag.IGNORE_ON_ENTER,
      execute: async ({ reason }) => {
        setResult(new import_llm.ToolError(`human agent declined to connect: ${reason}`));
      }
    }),
    (0, import_llm.tool)({
      name: "voicemail_detected",
      description: "Called when the call reaches voicemail. Use this tool AFTER you hear the voicemail greeting",
      flags: import_llm.ToolFlag.IGNORE_ON_ENTER,
      execute: async () => {
        setResult(new import_llm.ToolError("voicemail detected"));
      }
    })
  ];
  const task = import_agent.AgentTask.create({
    instructions: resolveInstructions(instructions, chatCtx),
    turnDetection: turnDetection ?? void 0,
    tools: [...tools ?? [], ...transferTools],
    stt: stt ?? void 0,
    vad: vad ?? void 0,
    llm: llm ?? void 0,
    tts: tts ?? void 0,
    allowInterruptions,
    onEnter: async () => {
      jobCtx = (0, import_job.getJobContext)();
      callerRoom = jobCtx.room;
      if (holdAudio !== null) {
        await backgroundAudio.start({ room: callerRoom });
        holdAudioHandle = backgroundAudio.play(holdAudio, true);
      }
      setIoEnabled(false);
      const abortController = new AbortController();
      const dialPromise = dialHumanAgent(abortController.signal);
      try {
        const result = await Promise.race([
          dialPromise.then((session) => ({ session })),
          humanAgentFailedFut.await.then(() => ({ session: null }))
        ]);
        if (!result.session) {
          throw new Error("human agent room closed");
        }
        transferAgentSession = result.session;
      } catch (error) {
        logger.error({ error }, "could not dial human agent");
        setResult(new import_llm.ToolError("could not dial human agent"));
      } finally {
        abortController.abort();
        const session = await dialPromise.catch(() => null);
        if (session && transferAgentSession !== session) {
          await cleanupHumanAgentDial(session, humanAgentRoom);
          humanAgentRoom = null;
        }
      }
    }
  });
  return task;
}
class WarmTransferTask extends import_agent.AgentTask {
  #task;
  constructor(options = {}) {
    super({ instructions: "" });
    this.#task = createWarmTransferTask(options);
  }
  run() {
    return this.#task.run();
  }
}
const renderInstructionPart = (value) => typeof value === "string" ? value : value.value;
function resolveInstructions(instructions, chatCtx) {
  if (typeof instructions === "string") {
    return instructions;
  }
  const parts = instructions ?? { persona: PERSONA };
  const replacements = {
    // Unset preserves the built-in default; an explicit empty string removes the section.
    persona: parts.persona !== void 0 ? renderInstructionPart(parts.persona) : PERSONA,
    _conversation_history: formatConversationHistory(chatCtx),
    extra: parts.extra !== void 0 ? renderInstructionPart(parts.extra) : ""
  };
  return INSTRUCTIONS_TEMPLATE.replace(
    /\{(persona|_conversation_history|extra)\}/g,
    (_match, key) => replacements[key] ?? ""
  );
}
function formatConversationHistory(chatCtx) {
  if (!chatCtx) {
    return "";
  }
  let previousConversation = "";
  for (const item of chatCtx.items) {
    if (item.type !== "message" || item.role !== "user" && item.role !== "assistant") {
      continue;
    }
    const content = item.textContent;
    if (!content) {
      continue;
    }
    const role = item.role === "user" ? "Caller" : "Assistant";
    previousConversation += `${role}: ${content}
`;
  }
  return previousConversation;
}
const PERSONA = `# Identity

You are an agent that is reaching out to a human agent for help. There has been a previous conversation
between you and a caller, the conversation history is included below.

# Goal

Your main goal is to give the human agent sufficient context about why the caller had called in,
so that the human agent could gain sufficient knowledge to help the caller directly.`;
const INSTRUCTIONS_TEMPLATE = `{persona}

# Context

In the conversation, user refers to the human agent, caller refers to the person who's transcript is included.
Remember, you are not speaking to the caller right now, you are speaking to the human agent.

## Conversation history with caller
{_conversation_history}
## End of conversation history with caller

Once the human agent has confirmed, you should call the tool \`connect_to_caller\` to connect them to the caller.

You are talking to the human agent now, start by giving them a summary of the conversation so far, and answer any questions they might have.

{extra}
`;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  WarmTransferTask,
  createWarmTransferTask
});
//# sourceMappingURL=warm_transfer.cjs.map