"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
});
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_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");
class WarmTransferTask extends import_agent.AgentTask {
  _callerRoom = null;
  _humanAgentRoom = null;
  _humanAgentSession = null;
  // Assigned in the constructor; a field initializer here would run after the
  // resolver is captured and clobber it (ES2022 class-field semantics).
  _humanAgentFailed;
  _resolveHumanAgentFailed;
  _humanAgentIdentity = "human-agent-sip";
  _sipCallTo;
  _sipTrunkId;
  _sipConnection;
  _sipNumber;
  _sipHeaders;
  _dtmf;
  _ringingTimeout;
  _backgroundAudio = new import_background_audio.BackgroundAudioPlayer();
  _holdAudioHandle = null;
  _holdAudio;
  _originalIoState = {};
  _taskTurnDetection;
  _allowInterruptions;
  _logger = (0, import_log.log)();
  constructor(options = {}) {
    const { sipCallTo, instructions } = options;
    const {
      sipTrunkId,
      sipConnection,
      sipNumber,
      sipHeaders,
      dtmf,
      ringingTimeout,
      holdAudio,
      chatCtx,
      turnDetection,
      tools,
      stt,
      vad,
      llm,
      tts,
      allowInterruptions
    } = options;
    if (!sipCallTo) {
      throw new Error("`sipCallTo` must be set");
    }
    const renderPart = (value) => typeof value === "string" ? value : value.value;
    let resolvedInstructions;
    if (typeof instructions === "string") {
      resolvedInstructions = instructions;
    } else {
      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 ? renderPart(parts.persona) : PERSONA,
        _conversation_history: WarmTransferTask.formatConversationHistory(chatCtx),
        extra: parts.extra !== void 0 ? renderPart(parts.extra) : ""
      };
      resolvedInstructions = INSTRUCTIONS_TEMPLATE.replace(
        /\{(persona|_conversation_history|extra)\}/g,
        (_match, key) => replacements[key] ?? ""
      );
    }
    super({
      instructions: resolvedInstructions,
      turnDetection: turnDetection ?? void 0,
      tools,
      stt: stt ?? void 0,
      vad: vad ?? void 0,
      llm: llm ?? void 0,
      tts: tts ?? void 0,
      allowInterruptions
    });
    this._humanAgentFailed = new Promise((resolve) => {
      this._resolveHumanAgentFailed = resolve;
    });
    this._tools = {
      ...this._tools,
      connect_to_caller: this.buildConnectToCallerTool(),
      decline_transfer: this.buildDeclineTransferTool(),
      voicemail_detected: this.buildVoicemailDetectedTool()
    };
    this._chatCtx = this._chatCtx.copy({ toolCtx: this._tools });
    this._taskTurnDetection = turnDetection ?? void 0;
    this._allowInterruptions = allowInterruptions;
    this._sipCallTo = sipCallTo;
    this._sipConnection = sipConnection;
    if (sipTrunkId !== void 0) {
      this._sipTrunkId = sipTrunkId;
    } else if (this._sipConnection) {
      this._sipTrunkId = null;
    } else {
      this._sipTrunkId = process.env.LIVEKIT_SIP_OUTBOUND_TRUNK ?? null;
    }
    if (this._sipTrunkId === null && !this._sipConnection) {
      throw new Error(
        "`LIVEKIT_SIP_OUTBOUND_TRUNK` environment variable, `sipTrunkId`, or `sipConnection` must be set"
      );
    }
    this._sipNumber = sipNumber ?? process.env.LIVEKIT_SIP_NUMBER ?? "";
    this._sipHeaders = sipHeaders ?? {};
    this._dtmf = dtmf ?? null;
    this._ringingTimeout = ringingTimeout ?? null;
    this._holdAudio = holdAudio === void 0 ? { source: import_background_audio.BuiltinAudioClip.HOLD_MUSIC, volume: 0.8 } : holdAudio;
  }
  static 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;
  }
  async onEnter() {
    const jobCtx = (0, import_job.getJobContext)();
    this._callerRoom = jobCtx.room;
    if (this._holdAudio !== null) {
      await this._backgroundAudio.start({ room: this._callerRoom });
      this._holdAudioHandle = this._backgroundAudio.play(this._holdAudio, true);
    }
    this.setIoEnabled(false);
    const dialAbortController = new AbortController();
    const dialHumanAgent = this.dialHumanAgent(dialAbortController.signal);
    try {
      const result = await Promise.race([
        dialHumanAgent.then((session) => ({ session })),
        this._humanAgentFailed.then(() => ({ session: null }))
      ]);
      if (!result.session) {
        throw new Error("human agent room closed");
      }
      this._humanAgentSession = result.session;
    } catch (error) {
      this._logger.error({ error }, "could not dial human agent");
      this.setResult(new import_llm.ToolError("could not dial human agent"));
    } finally {
      dialAbortController.abort();
      const session = await dialHumanAgent.catch(() => null);
      if (session && this._humanAgentSession !== session) {
        await this.cleanupHumanAgentDial(session, this._humanAgentRoom);
        if (this._humanAgentRoom) {
          this._humanAgentRoom = null;
        }
      }
    }
  }
  buildConnectToCallerTool() {
    return (0, import_llm.tool)({
      description: "Called when the human agent wants to connect to the caller.",
      flags: import_llm.ToolFlag.IGNORE_ON_ENTER,
      execute: async () => {
        this._logger.debug("connecting to caller");
        if (!this._callerRoom) {
          throw new Error("caller room is not available");
        }
        await this.mergeCalls();
        this.setResult({ humanAgentIdentity: this._humanAgentIdentity });
        this._callerRoom.on(
          import_rtc_node.RoomEvent.ParticipantDisconnected,
          this.onCallerParticipantDisconnected
        );
      }
    });
  }
  buildDeclineTransferTool() {
    return (0, import_llm.tool)({
      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 }) => {
        this.setResult(new import_llm.ToolError(`human agent declined to connect: ${reason}`));
      }
    });
  }
  buildVoicemailDetectedTool() {
    return (0, import_llm.tool)({
      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 () => {
        this.setResult(new import_llm.ToolError("voicemail detected"));
      }
    });
  }
  onHumanAgentRoomClose = (reason) => {
    this._logger.debug({ reason }, "human agent's room closed");
    this._resolveHumanAgentFailed();
    this.setResult(new import_llm.ToolError(`room closed: ${reason}`));
  };
  onCallerParticipantDisconnected = (participant) => {
    var _a;
    if (!import_room_io.DEFAULT_PARTICIPANT_KINDS.includes(participant.kind)) {
      return;
    }
    this._logger.info(
      { participantIdentity: participant.identity },
      "participant disconnected from caller room, closing"
    );
    if (!((_a = this._callerRoom) == null ? void 0 : _a.name)) {
      return;
    }
    this._callerRoom.off(import_rtc_node.RoomEvent.ParticipantDisconnected, this.onCallerParticipantDisconnected);
    const rooms = new import_livekit_server_sdk.RoomServiceClient((0, import_job.getJobContext)().info.url);
    void rooms.deleteRoom(this._callerRoom.name).catch((error) => {
      this._logger.warn({ error }, "failed to delete caller room");
    });
  };
  setResult(result) {
    if (this.done) {
      return;
    }
    if (this._humanAgentSession) {
      this._humanAgentSession.shutdown();
      this._humanAgentSession = null;
      this._humanAgentRoom = null;
    }
    if (this._holdAudioHandle) {
      this._holdAudioHandle.stop();
      this._holdAudioHandle = null;
    }
    void this._backgroundAudio.close().catch((error) => {
      this._logger.warn({ error }, "failed to close background audio");
    });
    this.setIoEnabled(true);
    this.complete(result);
  }
  async dialHumanAgent(signal) {
    var _a, _b;
    if (!((_a = this._callerRoom) == null ? void 0 : _a.name)) {
      throw new Error("caller room is not available");
    }
    const localIdentity = (_b = this._callerRoom.localParticipant) == null ? void 0 : _b.identity;
    if (!localIdentity) {
      throw new Error("caller room local participant is not available");
    }
    const jobCtx = (0, import_job.getJobContext)();
    const humanAgentRoomName = `${this._callerRoom.name}-human-agent`;
    const room = new import_rtc_node.Room();
    let humanAgentSession = null;
    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
      });
      this._logger.debug(
        { wsUrl: jobCtx.info.url, humanAgentRoomName },
        "connecting to human agent room"
      );
      const jwt = await token.toJwt();
      await this.abortable(() => room.connect(jobCtx.info.url, jwt), signal);
      room.on(import_rtc_node.RoomEvent.Disconnected, this.onHumanAgentRoomClose);
      humanAgentSession = new import_agent_session.AgentSession({
        vad: this.session.vad,
        llm: this.session.llm,
        stt: this.session.stt,
        tts: this.session.tts,
        turnDetection: this.session.turnDetection
      });
      const session = humanAgentSession;
      const humanAgent = new import_agent.Agent({
        instructions: this.instructions,
        stt: this.stt,
        vad: this.vad,
        llm: this.llm,
        tts: this.tts,
        tools: this.toolCtx,
        chatCtx: this._chatCtx.copy(),
        turnDetection: this._taskTurnDetection,
        allowInterruptions: this._allowInterruptions
      });
      await this.abortable(
        () => session.start({
          agent: humanAgent,
          room,
          inputOptions: {
            closeOnDisconnect: true,
            // Delete the supervisor room on shutdown so its WebSocket doesn't
            // leak across transfers.
            deleteRoomOnClose: true,
            participantIdentity: this._humanAgentIdentity
          },
          record: false
        }),
        signal
      );
      const sip = new import_livekit_server_sdk.SipClient(jobCtx.info.url);
      await this.abortable(
        () => sip.createSipParticipant(
          this._sipTrunkId ?? "",
          this._sipCallTo,
          humanAgentRoomName,
          {
            participantIdentity: this._humanAgentIdentity,
            waitUntilAnswered: true,
            fromNumber: this._sipNumber || void 0,
            headers: this._sipHeaders,
            dtmf: this._dtmf ?? void 0,
            // SIP API takes whole seconds (BigInt coercion throws on fractional input).
            ringingTimeout: this._ringingTimeout !== null ? Math.round(this._ringingTimeout / 1e3) : void 0
          },
          this._sipConnection
        ),
        signal
      );
      this._humanAgentRoom = room;
      completed = true;
      return session;
    } finally {
      if (!completed) {
        room.off(import_rtc_node.RoomEvent.Disconnected, this.onHumanAgentRoomClose);
        await this.cleanupHumanAgentDial(humanAgentSession, room);
      }
    }
  }
  async cleanupHumanAgentDial(humanAgentSession, room) {
    await (room == null ? void 0 : room.disconnect().catch((error) => {
      this._logger.warn({ error }, "failed to disconnect human agent room");
    }));
    await (humanAgentSession == null ? void 0 : humanAgentSession.close().catch((error) => {
      this._logger.warn({ error }, "failed to close human agent session");
    }));
  }
  async abortable(fn, signal) {
    if (signal.aborted) {
      throw new Error("dial cancelled");
    }
    let onAbort;
    const abortPromise = new Promise((_, reject) => {
      onAbort = () => reject(new Error("dial cancelled"));
      signal.addEventListener("abort", onAbort, { once: true });
    });
    try {
      return await Promise.race([fn(), abortPromise]);
    } finally {
      signal.removeEventListener("abort", onAbort);
    }
  }
  async mergeCalls() {
    var _a, _b;
    if (!((_a = this._callerRoom) == null ? void 0 : _a.name) || !((_b = this._humanAgentRoom) == null ? void 0 : _b.name)) {
      throw new Error("calls are not ready to merge");
    }
    this._humanAgentRoom.off(import_rtc_node.RoomEvent.Disconnected, this.onHumanAgentRoomClose);
    this._logger.debug(
      { humanAgentIdentity: this._humanAgentIdentity, callerRoom: this._callerRoom.name },
      "moving human agent to caller room"
    );
    const rooms = new import_livekit_server_sdk.RoomServiceClient((0, import_job.getJobContext)().info.url);
    await rooms.moveParticipant(
      this._humanAgentRoom.name,
      this._humanAgentIdentity,
      this._callerRoom.name
    );
  }
  setIoEnabled(enabled) {
    const input = this.session.input;
    const output = this.session.output;
    if (Object.keys(this._originalIoState).length === 0) {
      this._originalIoState = {
        audioInput: input.audioEnabled,
        audioOutput: output.audioEnabled,
        transcriptionOutput: output.transcriptionEnabled
      };
    }
    if (input.audio) {
      input.setAudioEnabled(enabled && this._originalIoState.audioInput);
    }
    if (output.audio) {
      output.setAudioEnabled(enabled && this._originalIoState.audioOutput);
    }
    if (output.transcription) {
      output.setTranscriptionEnabled(enabled && this._originalIoState.transcriptionOutput);
    }
  }
}
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
});
//# sourceMappingURL=warm_transfer.cjs.map