"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var amd_exports = {};
__export(amd_exports, {
  AMD: () => AMD,
  AMDCategory: () => AMDCategory
});
module.exports = __toCommonJS(amd_exports);
var import_rtc_node = require("@livekit/rtc-node");
var import_node_events = require("node:events");
var import_node_perf_hooks = require("node:perf_hooks");
var import_zod = require("zod");
var inference = __toESM(require("../inference/index.cjs"), 1);
var import_chat_context = require("../llm/chat_context.cjs");
var import_llm = require("../llm/llm.cjs");
var import_tool_context = require("../llm/tool_context.cjs");
var import_log = require("../log.cjs");
var import_stt = require("../stt/stt.cjs");
var import_telemetry = require("../telemetry/index.cjs");
var import_utils = require("../utils.cjs");
var import_events = require("./events.cjs");
var import_utils2 = require("./utils.cjs");
var AMDCategory = /* @__PURE__ */ ((AMDCategory2) => {
  AMDCategory2["HUMAN"] = "human";
  AMDCategory2["MACHINE_IVR"] = "machine-ivr";
  AMDCategory2["MACHINE_VM"] = "machine-vm";
  AMDCategory2["MACHINE_UNAVAILABLE"] = "machine-unavailable";
  AMDCategory2["UNCERTAIN"] = "uncertain";
  return AMDCategory2;
})(AMDCategory || {});
const HUMAN_SPEECH_THRESHOLD_MS = 2500;
const HUMAN_SILENCE_THRESHOLD_MS = 500;
const MACHINE_SILENCE_THRESHOLD_MS = 1500;
const DEFAULT_NO_SPEECH_TIMEOUT_MS = 1e4;
const DEFAULT_DETECTION_TIMEOUT_MS = 2e4;
const DEFAULT_MAX_ENDPOINTING_DELAY_MS = 3e3;
const MAX_EXTENSIONS = 3;
const MAX_EXTENSION_MS = 1e4;
const DEFAULT_AMD_LLM_MODEL = "google/gemini-3.1-flash-lite";
const DEFAULT_AMD_STT_MODEL = "cartesia/ink-whisper";
const SIP_CALL_STATUS_ATTR = "sip.callStatus";
const SIP_CALL_STATUS_ACTIVE = "active";
const EVALUATED_LLM_MODELS = /* @__PURE__ */ new Set([
  "google/gemini-3.1-flash-lite",
  "google/gemini-3-flash-preview",
  "openai/gpt-4.1",
  "openai/gpt-5.2",
  "openai/gpt-5.4",
  "openai/gpt-5.1",
  "openai/gpt-4o",
  "openai/gpt-5.1-chat-latest",
  "openai/gpt-4.1-mini",
  "openai/gpt-4.1-nano",
  "openai/gpt-5.2-chat-latest",
  "google/gemini-2.5-flash-lite"
]);
const EVALUATED_STT_MODELS = /* @__PURE__ */ new Set([
  "deepgram/nova-3",
  "assemblyai/universal-streaming-multilingual",
  "cartesia/ink-whisper"
]);
const MACHINE_CATEGORIES = /* @__PURE__ */ new Set([
  "machine-ivr" /* MACHINE_IVR */,
  "machine-vm" /* MACHINE_VM */,
  "machine-unavailable" /* MACHINE_UNAVAILABLE */
]);
const VALID_CATEGORIES = new Set(Object.values(AMDCategory));
function isMachineCategory(category) {
  return MACHINE_CATEGORIES.has(category);
}
function parseCategory(raw) {
  return typeof raw === "string" && VALID_CATEGORIES.has(raw) ? raw : "uncertain" /* UNCERTAIN */;
}
const AMD_PROMPT = `Task:
Classify the call greeting transcript into exactly one of these categories:

human: A person answered (e.g., "Hello?", "This is John.").
machine-ivr: A prompt to press a key (e.g., "Press 1 to continue").
machine-vm: A voicemail greeting where leaving a message IS possible.
machine-unavailable: Any greeting indicating it's NOT possible to leave message, eg because mailbox is full, not setup, etc.
uncertain: For partial transcripts that are ambiguous.

Examples:
Input: "The person you called has a voice mailbox that hasn't been set up yet. Goodbye."
Output: machine-unavailable

Input: "Thank you for calling Truly Pizza in Dana Pointe. Our hours of operation are 11AM to 8PM, Sunday through Thursday, 11AM to 9PM, Friday and Saturday, and we're closed on Tuesdays."
Output: uncertain

Input: "You for calling Truly Pizza in Dana Pointe. Our hours of operation are 11AM to 8PM, Sunday through Thursday, 11AM to 9PM, Friday and Saturday, and we're closed on Tuesdays. If you'd like to place an order, please press 1 or head to our website to order online for pickup and local delivery."
Output: machine-ivr

Input: "Please state your name and why you're calling, and I will check if the person is available"
Output: machine-ivr
Note: this should apply for any call screening prompts.

Input: "I'm away from my desk. If you leave a message, I will get back to you."
Output: machine-vm

Input: "Hello, this is Lisa."
Output: human`;
function warnIfNotEvaluated(modelName, evaluated, modelKind) {
  if (!modelName) return;
  const lower = modelName.toLowerCase();
  for (const candidate of evaluated) {
    const c = candidate.toLowerCase();
    if (lower === c || c.includes(lower) || lower.includes(c)) return;
  }
  (0, import_log.log)().warn(
    `${modelKind} model ${modelName} hasn't been evaluated with our benchmark, it might not be compatible with amd. Set \`suppressCompatibilityWarning: true\` to silence this warning.`
  );
}
class AMD extends import_node_events.EventEmitter {
  constructor(session, options = {}) {
    var _a, _b, _c;
    super();
    this.session = session;
    let { llm, stt } = options;
    if (llm === void 0 || stt === void 0) {
      const rawUrl = process.env.LIVEKIT_URL ?? "";
      const apiKey = process.env.LIVEKIT_INFERENCE_API_KEY || process.env.LIVEKIT_API_KEY;
      const apiSecret = process.env.LIVEKIT_INFERENCE_API_SECRET || process.env.LIVEKIT_API_SECRET;
      let autoSelect = false;
      try {
        autoSelect = (0, import_utils.isCloud)(new URL(rawUrl)) && !!(apiKey && apiSecret);
      } catch {
      }
      if (llm === void 0) llm = autoSelect ? DEFAULT_AMD_LLM_MODEL : void 0;
      if (stt === void 0) stt = autoSelect ? DEFAULT_AMD_STT_MODEL : void 0;
    }
    const { llm: resolvedLLM, owned: llmOwned } = this.resolveLLM(llm);
    this.llm = resolvedLLM;
    this.llmOwned = llmOwned;
    const sttResolution = this.resolveSTT(stt);
    this.stt = sttResolution.stt;
    this.sttOwned = sttResolution.owned;
    this.source = this.stt ? "amd_stt" : "stt";
    this.interruptOnMachine = options.interruptOnMachine ?? true;
    this.noSpeechTimeoutMs = options.noSpeechTimeoutMs ?? DEFAULT_NO_SPEECH_TIMEOUT_MS;
    this.detectionTimeoutMs = options.detectionTimeoutMs ?? DEFAULT_DETECTION_TIMEOUT_MS;
    this.humanSpeechThresholdMs = options.humanSpeechThresholdMs ?? HUMAN_SPEECH_THRESHOLD_MS;
    this.humanSilenceThresholdMs = options.humanSilenceThresholdMs ?? HUMAN_SILENCE_THRESHOLD_MS;
    this.machineSilenceThresholdMs = options.machineSilenceThresholdMs ?? MACHINE_SILENCE_THRESHOLD_MS;
    this.waitUntilFinished = options.waitUntilFinished ?? false;
    this.maxEndpointingDelayMs = options.maxEndpointingDelayMs ?? ((_a = this.session._activity) == null ? void 0 : _a.maxEndpointingDelay) ?? DEFAULT_MAX_ENDPOINTING_DELAY_MS;
    this.prompt = options.prompt ?? AMD_PROMPT;
    this.participantIdentity = options.participantIdentity;
    if (!options.suppressCompatibilityWarning) {
      warnIfNotEvaluated(this.llm.model, EVALUATED_LLM_MODELS, "llm");
      if (this.stt) {
        warnIfNotEvaluated(this.stt.model, EVALUATED_STT_MODELS, "stt");
      }
    }
    (_c = (_b = this.session)._setAmd) == null ? void 0 : _c.call(_b, this);
  }
  _log = (0, import_log.log)();
  llm;
  /** Dedicated STT for AMD-only transcription, or `undefined` if listening to session events. */
  stt;
  /** Whether AMD owns (and must close) the resolved LLM. False when the caller passed an instance. */
  llmOwned;
  /** Whether AMD owns (and must close) the resolved STT. */
  sttOwned;
  /**
   * Transcript source AMD consumes. `'amd_stt'` when AMD runs its own dedicated
   * STT (session transcripts are ignored), `'stt'` when it relies on the
   * session's STT transcripts. Mirrors python `_AMDClassifier._source`.
   */
  source;
  interruptOnMachine;
  noSpeechTimeoutMs;
  detectionTimeoutMs;
  humanSpeechThresholdMs;
  humanSilenceThresholdMs;
  machineSilenceThresholdMs;
  waitUntilFinished;
  maxEndpointingDelayMs;
  prompt;
  participantIdentity;
  // --- execution state (reset per run) ---
  active = false;
  listening = false;
  settled = false;
  transcriptParts = [];
  verdictResult;
  /**
   * Two-gate emission (mirrors python classifier.py `_can_emit`): a verdict is
   * released only when `silenceReached` is true AND, for everything except a
   * confident human, `eotReached` is also true. Humans release on silence
   * alone so the agent can respond quickly.
   */
  silenceReached = false;
  eotReached = false;
  speechStartedAt;
  speechEndedAt;
  detectGeneration = 0;
  extensionCount = 0;
  noSpeechTimer;
  detectionTimer;
  silenceTimer;
  silenceTimerTrigger;
  /** Fallback end-of-turn backstop, armed when speech ends. */
  eotTimer;
  sttStream;
  sttPumpTask;
  /**
   * Aborts pending {@link waitForTrackPublication} calls in
   * {@link gateListening}. Without this the room-event listener can
   * outlive the AMD instance if the participant track never publishes
   * before the run settles.
   */
  trackGateAbort;
  /**
   * Tracks the in-flight LLM classification stream so {@link aclose} (and any
   * settled-after-aclose path) can abort it instead of leaving the network
   * call running. Mirrors python's `task.cancel()` on the outstanding
   * classification coroutine.
   */
  currentLLMStream;
  resolveRun;
  rejectRun;
  span;
  // ─── public API ──────────────────────────────────────────────────────────────
  async execute() {
    return import_telemetry.tracer.startActiveSpan(
      async (span) => {
        var _a;
        if (this.active) {
          throw new Error("AMD.execute() is already running");
        }
        this.resetState();
        this.active = true;
        this.span = span;
        this.session.pauseReplyAuthorization();
        span.setAttribute(import_telemetry.traceTypes.ATTR_AMD_INTERRUPT_ON_MACHINE, this.interruptOnMachine);
        span.setAttribute(import_telemetry.traceTypes.ATTR_GEN_AI_OPERATION_NAME, "classification");
        const linkedParticipant = (_a = this.session._roomIO) == null ? void 0 : _a.linkedParticipant;
        if (linkedParticipant) {
          (0, import_utils2.setParticipantSpanAttributes)(span, linkedParticipant);
        }
        try {
          const result = await new Promise((resolve, reject) => {
            this.resolveRun = resolve;
            this.rejectRun = reject;
            this.subscribe();
            this.startDetectionTimer();
            this.gateListening();
            this.startSTTPump();
          });
          return result;
        } finally {
          this.cleanup();
          this.session.resumeReplyAuthorization();
          this.active = false;
          this.span = void 0;
        }
      },
      {
        name: "answering_machine_detection",
        context: this.session.rootSpanContext
      }
    );
  }
  async aclose() {
    var _a, _b;
    this.settled = true;
    if (this.currentLLMStream) {
      try {
        this.currentLLMStream.close();
      } catch {
      }
      this.currentLLMStream = void 0;
    }
    if (this.active && this.rejectRun) {
      this.rejectRun(new Error("AMD closed"));
    }
    (_b = (_a = this.session)._setAmd) == null ? void 0 : _b.call(_a, null);
    if (this.sttOwned && this.stt) {
      try {
        await this.stt.close();
      } catch (e) {
        this._log.warn({ err: e }, "AMD failed to close owned STT");
      }
    }
    if (this.llmOwned) {
      try {
        await this.llm.aclose();
      } catch (e) {
        this._log.warn({ err: e }, "AMD failed to close owned LLM");
      }
    }
  }
  // ─── lifecycle ───────────────────────────────────────────────────────────────
  resetState() {
    this.settled = false;
    this.listening = false;
    this.transcriptParts = [];
    this.verdictResult = void 0;
    this.silenceReached = false;
    this.eotReached = false;
    this.speechStartedAt = void 0;
    this.speechEndedAt = void 0;
    this.silenceTimerTrigger = void 0;
    this.detectGeneration = 0;
    this.extensionCount = 0;
    this.resolveRun = void 0;
    this.rejectRun = void 0;
  }
  subscribe() {
    this.session.on(import_events.AgentSessionEventTypes.Close, this.handleClose);
  }
  /**
   * Arms the detection-timeout budget. Started in `execute()` as a backstop against a
   * never-published track, then re-armed in {@link gateListening} at track-up so the
   * effective budget runs from track-subscribe. Re-armable, hence the clear first.
   */
  startDetectionTimer() {
    this.clearTimer("detection");
    this.detectionTimer = setTimeout(() => this.settleDetectionTimeout(), this.detectionTimeoutMs);
  }
  /**
   * The no-speech timer measures how long we've been listening with nothing
   * coming back. It only makes sense after the participant track is actually
   * subscribed (otherwise we'd start counting silence against an audio source
   * that hasn't been wired up yet). Mirrors python detector.py's
   * `_setup` → `wait_for_track_publication` → `start_timers` flow.
   */
  startNoSpeechTimer() {
    if (this.settled || this.noSpeechTimer !== void 0) return;
    this.noSpeechTimer = setTimeout(() => this.settleNoSpeech(), this.noSpeechTimeoutMs);
  }
  startListening() {
    if (this.settled || this.listening) return;
    this.listening = true;
    this.startNoSpeechTimer();
    this._log.debug("AMD starts listening");
  }
  /**
   * Mirrors python detector.py `_run_stt`. When AMD owns its STT, it runs a
   * private pump that:
   *  1. Tees a fresh branch off the participant audio in `AudioRecognition`.
   *  2. Pushes frames into the STT stream.
   *  3. Forwards FINAL_TRANSCRIPTs into the classifier with `source = 'amd_stt'`.
   *
   * The session may not yet have an `AgentActivity` (and therefore no audio
   * stream) by the time AMD constructs its tasks, so we poll briefly until a
   * stream is available, bounded by the abort signal / settled state.
   */
  startSTTPump() {
    if (!this.stt) return;
    this.sttPumpTask = import_utils.Task.from(({ signal }) => this.runSTTPump(signal));
    this.sttPumpTask.result.catch((err) => {
      if (this.settled) return;
      this._log.warn({ err }, "AMD dedicated STT pump exited with error");
    });
  }
  async runSTTPump(signal) {
    var _a, _b;
    if (!this.stt) return;
    let audioStream;
    while (!signal.aborted && !this.settled) {
      audioStream = (_b = (_a = this.session)._subscribeAudioStream) == null ? void 0 : _b.call(_a);
      if (audioStream) break;
      try {
        await (0, import_utils.delay)(100, { signal });
      } catch {
        return;
      }
    }
    if (!audioStream || this.settled || signal.aborted) return;
    const sttStream = this.stt.stream();
    this.sttStream = sttStream;
    const sendPump = (async () => {
      const reader = audioStream.getReader();
      try {
        while (!signal.aborted && !this.settled) {
          const { done, value } = await reader.read();
          if (done || !value) break;
          if (!this.listening) continue;
          try {
            sttStream.pushFrame(value);
          } catch {
            break;
          }
        }
      } finally {
        try {
          await reader.cancel();
        } catch {
        }
        try {
          sttStream.endInput();
        } catch {
        }
      }
    })();
    const recvPump = (async () => {
      var _a2, _b2;
      try {
        for await (const event of sttStream) {
          if (signal.aborted || this.settled) break;
          if (event.type !== import_stt.SpeechEventType.FINAL_TRANSCRIPT) continue;
          const text = (_b2 = (_a2 = event.alternatives) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.text;
          if (!text) continue;
          this.onTranscript(text, "amd_stt");
        }
      } catch (err) {
        if (this.settled) return;
        this._log.debug({ err }, "AMD dedicated STT receive pump error");
      }
    })();
    await Promise.allSettled([sendPump, recvPump]);
  }
  /**
   * If the session has a `_roomIO`, defer listening until the participant's
   * audio track is subscribed. For SIP participants, also wait until the call
   * is active so ringback and early media do not burn the no-speech budget.
   * Without `_roomIO`, fall back to listening immediately.
   */
  gateListening() {
    var _a;
    const roomIO = this.session._roomIO;
    const room = roomIO == null ? void 0 : roomIO.rtcRoom;
    if (!room || !room.isConnected) {
      this.startListening();
      return;
    }
    const targetIdentity = this.participantIdentity ?? ((_a = roomIO == null ? void 0 : roomIO.linkedParticipant) == null ? void 0 : _a.identity);
    this.trackGateAbort = new AbortController();
    (0, import_utils.waitForTrackPublication)({
      room,
      identity: targetIdentity,
      kind: import_rtc_node.TrackKind.KIND_AUDIO,
      waitForSubscription: true,
      signal: this.trackGateAbort.signal
    }).then(async (publication) => {
      var _a2, _b;
      if (this.settled) {
        return;
      }
      this.startDetectionTimer();
      const publicationSid = publication.sid;
      const participant = targetIdentity ? room.remoteParticipants.get(targetIdentity) : publicationSid ? [...room.remoteParticipants.values()].find(
        (p) => p.trackPublications.has(publicationSid)
      ) : void 0;
      if (!participant) {
        if (!this.settled) {
          this.startListening();
        }
        return;
      }
      if (participant.kind !== import_rtc_node.ParticipantKind.SIP) {
        this.startListening();
        return;
      }
      try {
        await (0, import_utils.waitForParticipantAttribute)({
          room,
          identity: participant.identity,
          attribute: SIP_CALL_STATUS_ATTR,
          value: SIP_CALL_STATUS_ACTIVE,
          signal: (_a2 = this.trackGateAbort) == null ? void 0 : _a2.signal
        });
      } catch (err) {
        if ((_b = this.trackGateAbort) == null ? void 0 : _b.signal.aborted) {
          return;
        }
        this._log.debug({ err }, "AMD SIP answer wait failed; starting to listen");
      }
      if (!this.settled) {
        this.startListening();
      }
    }).catch((err) => {
      var _a2;
      if ((_a2 = this.trackGateAbort) == null ? void 0 : _a2.signal.aborted) {
        return;
      }
      this._log.debug({ err }, "AMD listening gate failed; starting immediately");
      if (!this.settled) {
        this.startListening();
      }
    });
  }
  cleanup() {
    this.clearTimer("noSpeech");
    this.clearTimer("detection");
    this.clearTimer("silence");
    this.clearTimer("eot");
    this.listening = false;
    this.session.off(import_events.AgentSessionEventTypes.Close, this.handleClose);
    if (this.trackGateAbort) {
      this.trackGateAbort.abort();
      this.trackGateAbort = void 0;
    }
    if (this.sttStream) {
      try {
        this.sttStream.close();
      } catch {
      }
      this.sttStream = void 0;
    }
    if (this.sttPumpTask) {
      this.sttPumpTask.cancel();
      this.sttPumpTask = void 0;
    }
  }
  clearTimer(name) {
    const key = `${name}Timer`;
    if (this[key]) {
      clearTimeout(this[key]);
      this[key] = void 0;
      if (name === "silence") {
        this.silenceTimerTrigger = void 0;
      }
    }
  }
  // ─── two-gate emit system (verdict + silence) ───────────────────────────────
  /**
   * Ref: python classifier.py `_set_verdict` — stores the LLM/heuristic verdict.
   * Emission is deferred until the silence gate also opens.
   */
  setVerdict(result) {
    this.verdictResult = result;
    this.tryEmitResult();
  }
  /**
   * Ref: python classifier.py `_try_emit_result` + `_can_emit` — releases a
   * verdict only when the silence gate is open AND, for everything except a
   * confident human, the end-of-turn gate is open too. Humans release on
   * silence alone so the agent can respond quickly.
   */
  tryEmitResult() {
    if (!this.verdictResult || this.settled) {
      return;
    }
    if (!this.canEmit(this.verdictResult)) {
      return;
    }
    this.clearTimer("detection");
    this.finish(this.verdictResult);
  }
  /** Ref: python classifier.py `_can_emit`. */
  canEmit(verdict) {
    if (!this.silenceReached) {
      return false;
    }
    return verdict.category === "human" /* HUMAN */ ? true : this.eotReached;
  }
  finish(result) {
    var _a, _b, _c;
    if (this.settled) {
      return;
    }
    this.settled = true;
    this.cleanup();
    this.setSpanAttributes(result);
    this._log.info(
      {
        category: result.category,
        reason: result.reason,
        isMachine: result.isMachine,
        speechDurationMs: result.speechDurationMs,
        delayMs: result.delayMs,
        transcript: result.transcript
      },
      "amd prediction"
    );
    if (result.isMachine && this.interruptOnMachine) {
      this.session.interrupt({ force: true }).await.catch(() => {
      });
    }
    (_a = this.resolveRun) == null ? void 0 : _a.call(this, result);
    try {
      (_c = (_b = this.session)._onAmdPrediction) == null ? void 0 : _c.call(_b, result);
    } catch (err) {
      this._log.debug({ err }, "AMD: session host failed to handle amd_prediction");
    }
    this.emit("amd_prediction", result);
  }
  /**
   * Hook invoked by {@link AgentActivity} when the user turn ends. Mirrors python
   * detector.py `_on_end_of_turn`: signals whether AMD is consuming this turn so
   * the activity should skip the normal reply pipeline.
   *
   * Returns `true` when AMD has settled on a machine verdict and the caller asked
   * us to take over via `interruptOnMachine`; the caller is expected to drive its
   * own `generateReply` (e.g. leaving a voicemail) and the auto-reply triggered by
   * user-turn completion would otherwise race with — and interrupt — it.
   *
   * @internal
   */
  onEndOfTurn(info) {
    var _a;
    this.onEotReached();
    if (!this.interruptOnMachine || !this.settled || !((_a = this.verdictResult) == null ? void 0 : _a.isMachine)) {
      return false;
    }
    this._log.debug(
      { category: this.verdictResult.category, transcript: info.newTranscript },
      "skipping auto reply: AMD already returned a machine verdict"
    );
    return true;
  }
  // ─── timer callbacks ─────────────────────────────────────────────────────────
  /**
   * Ref: python classifier.py `_on_silence_reached` — the post-speech silence
   * window elapsed. Opens the silence gate and tries to emit a pending verdict.
   * Does NOT commit a fallback verdict and does NOT touch the end-of-turn gate.
   */
  onSilenceReached() {
    if (this.settled) return;
    this.silenceTimer = void 0;
    this.silenceTimerTrigger = void 0;
    this.silenceReached = true;
    this.tryEmitResult();
  }
  /**
   * Ref: python classifier.py `_on_timeout` — a timeout (detection budget,
   * no-speech, short greeting) fired. Commits a fallback verdict if none exists,
   * then tries to emit. This only decides *what* the verdict is; {@link canEmit}
   * decides *when* it is released. End-of-turn is forced here only when there is
   * nothing left to wait for: no speech was heard, or we are not waiting for the
   * greeting to finish (`waitUntilFinished` off). When `waitUntilFinished` is set
   * and speech was heard, the fallback is still committed but its release stays
   * gated on end-of-turn (the real signal or the backstop timer), so we don't cut
   * the greeting short with an `uncertain` result.
   *
   * Not gated by `listening`: detection_timeout must still fire when the call
   * never reaches listening (e.g. SIP never answered).
   */
  onTimeout(category, reason, speechDurationMs) {
    if (this.settled) return;
    this.clearTimer("silence");
    this.silenceReached = true;
    const hasSpeech = this.speechStartedAt !== void 0 || this.transcriptParts.length > 0;
    if (!(this.waitUntilFinished && hasSpeech)) {
      this.eotReached = true;
    }
    if (!this.verdictResult) {
      this.setVerdict({
        type: "amd_prediction",
        category,
        reason,
        transcript: this.joinTranscript(),
        rawResponse: "",
        isMachine: isMachineCategory(category),
        speechDurationMs: speechDurationMs ?? this.computeSpeechDurationMs(),
        delayMs: this.computeDelayMs()
      });
    }
    this.tryEmitResult();
  }
  /**
   * Ref: python classifier.py `_on_eot_reached` — the session turn detector
   * committed a turn (or the fallback backstop fired). Opens the end-of-turn
   * gate and tries to emit a pending verdict.
   */
  onEotReached() {
    if (this.settled) return;
    this.clearTimer("eot");
    this.eotReached = true;
    this.tryEmitResult();
  }
  settleNoSpeech() {
    this.onTimeout("uncertain" /* UNCERTAIN */, "no_speech_timeout");
  }
  settleDetectionTimeout() {
    this.onTimeout("uncertain" /* UNCERTAIN */, "detection_timeout");
  }
  // ─── recognition hooks (invoked by AgentActivity, mirroring python AudioRecognition) ───
  /**
   * Ref: python classifier.py `on_user_speech_started` (called from
   * `AudioRecognition._on_vad_event` on VAD START_OF_SPEECH). A new speech
   * segment cancels the pending gates/timers and reopens both emission gates.
   *
   * @internal
   */
  onUserSpeechStarted() {
    if (this.settled || !this.listening) return;
    this.clearTimer("silence");
    this.clearTimer("noSpeech");
    this.clearTimer("eot");
    if (this.speechStartedAt === void 0) {
      this.speechStartedAt = import_node_perf_hooks.performance.now();
    }
    this.silenceReached = false;
    this.eotReached = false;
  }
  /**
   * Ref: python classifier.py `on_user_speech_ended(silence_duration)` (called
   * from `AudioRecognition._on_vad_event` on VAD END_OF_SPEECH with
   * `ev.silence_duration`). `silenceDurationMs` is the silence already elapsed
   * when the VAD declared end-of-speech; the true speech-end time is therefore
   * `now - silenceDurationMs`, and every timer is shortened by it so it fires
   * `threshold` after speech actually ended. Mirrors python's
   * `speech_ended_at = time.time() - silence_duration` and
   * `max(0, threshold - silence_duration)`.
   *
   * @internal
   */
  onUserSpeechEnded(silenceDurationMs) {
    if (this.settled || !this.listening) return;
    if (this.speechStartedAt === void 0) {
      this._log.warn("AMD: onUserSpeechEnded called before onUserSpeechStarted");
      return;
    }
    this.speechEndedAt = import_node_perf_hooks.performance.now() - silenceDurationMs;
    const speechDurationMs = Math.ceil(this.speechEndedAt - this.speechStartedAt);
    const remaining = (thresholdMs) => Math.max(0, thresholdMs - silenceDurationMs);
    this.clearTimer("silence");
    this.clearTimer("eot");
    this.eotTimer = setTimeout(() => this.onEotReached(), remaining(this.maxEndpointingDelayMs));
    if (speechDurationMs <= this.humanSpeechThresholdMs && this.transcriptParts.length === 0) {
      this.silenceTimer = setTimeout(
        () => this.onTimeout("human" /* HUMAN */, "short_greeting", speechDurationMs),
        remaining(this.humanSilenceThresholdMs)
      );
      this.silenceTimerTrigger = "short_speech";
      return;
    }
    this.silenceTimer = setTimeout(
      () => this.onSilenceReached(),
      remaining(this.machineSilenceThresholdMs)
    );
    this.silenceTimerTrigger = "long_speech";
  }
  /**
   * Ref: python classifier.py `push_text(text, source)` (called from
   * `AudioRecognition._on_transcript` with the session STT transcript, and from
   * AMD's dedicated STT with `source='amd_stt'`). Transcripts whose source is not
   * the one AMD is consuming are dropped — so when AMD runs its own STT the
   * session's transcripts are ignored, and vice versa.
   *
   * @internal
   */
  onTranscript(text, source = "stt") {
    if (source !== this.source) return;
    const transcript = text.trim();
    if (!transcript) return;
    this.consumeTranscript(transcript);
  }
  /**
   * Shared transcript ingestion: cancels the no-speech timer, refreshes the
   * silence-gate target if we were on the short-greeting path, and fires off
   * an LLM classification on the accumulated transcript.
   */
  consumeTranscript(transcript) {
    if (this.settled) return;
    if (!this.listening) return;
    if (this.silenceTimer && this.silenceTimerTrigger === "short_speech") {
      this.clearTimer("silence");
      if (this.speechEndedAt !== void 0) {
        const remaining = Math.max(
          0,
          this.speechEndedAt + this.machineSilenceThresholdMs - import_node_perf_hooks.performance.now()
        );
        this.silenceTimer = setTimeout(() => this.onSilenceReached(), remaining);
        this.silenceTimerTrigger = "long_speech";
      }
    }
    this.clearTimer("noSpeech");
    this.transcriptParts.push(transcript);
    this.scheduleLLMClassification();
  }
  handleClose = () => {
    if (this.settled) return;
    this.eotReached = true;
    this.onTimeout("uncertain" /* UNCERTAIN */, "session_closed");
  };
  // ─── LLM classification ─────────────────────────────────────────────────────
  /**
   * Schedules an LLM classification. Uses a generation counter so that if a
   * newer transcript arrives while an older classification is in-flight, the
   * stale result is discarded (mirrors Python's cancel-and-rerun pattern).
   */
  scheduleLLMClassification() {
    const generation = ++this.detectGeneration;
    this.classifyCurrentTranscript(generation).catch((error) => {
      var _a;
      if (!this.settled) {
        this.settled = true;
        this.cleanup();
        (_a = this.rejectRun) == null ? void 0 : _a.call(this, error);
      }
    });
  }
  async classifyCurrentTranscript(generation) {
    if (this.transcriptParts.length === 0 || this.settled) {
      return;
    }
    const result = await this.detect(this.joinTranscript(), generation);
    if (this.settled || generation !== this.detectGeneration) {
      return;
    }
    if (result.category !== "uncertain" /* UNCERTAIN */) {
      this.setVerdict(result);
    }
  }
  // ─── helpers ─────────────────────────────────────────────────────────────────
  /**
   * Mirrors python `_resolve_classifier`.
   * - `LLM` instance: caller-owned, used as-is.
   * - string: construct a Cloud Inference LLM (AMD-owned).
   * - `undefined`: fall back to `session.llm`.
   */
  resolveLLM(option) {
    if (option instanceof import_llm.LLM) {
      return { llm: option, owned: false };
    }
    if (typeof option === "string") {
      return { llm: this.constructInferenceLLM(option), owned: true };
    }
    const sessionLLM = this.session.llm;
    if (sessionLLM instanceof import_llm.LLM) {
      return { llm: sessionLLM, owned: false };
    }
    throw new Error(
      "AMD: no LLM available. Either set LIVEKIT_API_KEY/LIVEKIT_API_SECRET for Cloud Inference or pass an LLM instance."
    );
  }
  /**
   * Mirrors python `_InferenceSTT(stt) if isinstance(stt, str) else stt`.
   * - `STT` instance: caller-owned.
   * - string: AMD-owned Cloud Inference STT.
   * - `undefined`: listen to session-level STT events.
   */
  resolveSTT(option) {
    if (option instanceof import_stt.STT) {
      return { stt: option, owned: false };
    }
    if (typeof option === "string") {
      return { stt: this.constructInferenceSTT(option), owned: true };
    }
    return { stt: void 0, owned: false };
  }
  constructInferenceLLM(model) {
    return new inference.LLM({ model });
  }
  constructInferenceSTT(model) {
    return new inference.STT({ model });
  }
  joinTranscript() {
    return this.transcriptParts.join(" ");
  }
  setSpanAttributes(result) {
    var _a, _b, _c, _d, _e, _f;
    (_a = this.span) == null ? void 0 : _a.setAttribute(import_telemetry.traceTypes.ATTR_AMD_CATEGORY, result.category);
    (_b = this.span) == null ? void 0 : _b.setAttribute(import_telemetry.traceTypes.ATTR_AMD_REASON, result.reason);
    (_c = this.span) == null ? void 0 : _c.setAttribute(import_telemetry.traceTypes.ATTR_AMD_IS_MACHINE, result.isMachine);
    (_d = this.span) == null ? void 0 : _d.setAttribute(import_telemetry.traceTypes.ATTR_AMD_SPEECH_DURATION, result.speechDurationMs);
    (_e = this.span) == null ? void 0 : _e.setAttribute(import_telemetry.traceTypes.ATTR_AMD_DELAY, result.delayMs);
    (_f = this.span) == null ? void 0 : _f.setAttribute(import_telemetry.traceTypes.ATTR_AMD_TRANSCRIPT, result.transcript);
  }
  /**
   * Builds two LLM tools — `save_prediction` (always) and `postpone_termination`
   * (until extensions exhausted) — and lets the LLM choose between committing
   * a verdict or extending the silence window.
   *
   * If the LLM returns plain JSON content instead of tool calls (e.g. mock
   * LLMs in tests, or providers that don't support `toolChoice='required'`),
   * we fall back to the pre-port JSON-content parsing path.
   */
  async detect(transcript, generation) {
    let savedResult;
    const isStale = () => generation !== this.detectGeneration || this.settled;
    const savePrediction = (0, import_tool_context.tool)({
      description: "Save the AMD prediction to the verdict.",
      parameters: import_zod.z.object({
        label: import_zod.z.enum([
          "human" /* HUMAN */,
          "machine-ivr" /* MACHINE_IVR */,
          "machine-vm" /* MACHINE_VM */,
          "machine-unavailable" /* MACHINE_UNAVAILABLE */,
          "uncertain" /* UNCERTAIN */
        ])
      }),
      execute: async ({ label }) => {
        if (isStale()) return "stale";
        const normalized = parseCategory(label);
        if (normalized !== "uncertain" /* UNCERTAIN */) {
          savedResult = {
            type: "amd_prediction",
            category: normalized,
            reason: "llm",
            transcript,
            rawResponse: "",
            isMachine: isMachineCategory(normalized),
            speechDurationMs: this.computeSpeechDurationMs(),
            delayMs: this.computeDelayMs()
          };
        }
        return "saved";
      }
    });
    const postponeTermination = (0, import_tool_context.tool)({
      description: "Postpone the termination of the classification task. Use when the transcript is ambiguous and more audio is expected.",
      parameters: import_zod.z.object({
        seconds: import_zod.z.number().describe("Additional seconds to wait (max 10).")
      }),
      execute: async ({ seconds }) => {
        if (isStale()) return "stale";
        const rawMs = Number(seconds) * 1e3;
        const clampedMs = Number.isFinite(rawMs) ? Math.max(0, Math.min(rawMs, MAX_EXTENSION_MS)) : 0;
        this.extensionCount += 1;
        this.clearTimer("silence");
        this.silenceTimer = setTimeout(() => {
          this.silenceReached = true;
          this.scheduleLLMClassification();
          this.tryEmitResult();
        }, clampedMs);
        return `waiting ${(clampedMs / 1e3).toFixed(1)}s for more audio`;
      }
    });
    const toolCtx = { save_prediction: savePrediction };
    if (this.extensionCount < MAX_EXTENSIONS) {
      toolCtx.postpone_termination = postponeTermination;
    }
    const chatCtx = new import_chat_context.ChatContext();
    chatCtx.addMessage({ role: "system", content: this.prompt });
    chatCtx.addMessage({ role: "user", content: transcript });
    const stream = this.llm.chat({ chatCtx, toolCtx, toolChoice: "required" });
    this.currentLLMStream = stream;
    const chunks = [];
    const toolCalls = [];
    try {
      for await (const chunk of stream) {
        const delta = chunk.delta;
        if (!delta) continue;
        if (delta.content) {
          chunks.push(delta.content);
        }
        if (delta.toolCalls && delta.toolCalls.length > 0) {
          toolCalls.push(...delta.toolCalls);
        }
      }
    } finally {
      if (this.currentLLMStream === stream) {
        this.currentLLMStream = void 0;
      }
    }
    const rawResponse = chunks.join("");
    for (const tc of toolCalls) {
      const fnTool = toolCtx[tc.name];
      if (!fnTool || !(0, import_tool_context.isFunctionTool)(fnTool)) continue;
      let parsedArgs = {};
      try {
        parsedArgs = JSON.parse(tc.args);
      } catch {
      }
      try {
        await fnTool.execute(parsedArgs, {
          ctx: void 0,
          toolCallId: tc.callId,
          abortSignal: new AbortController().signal
        });
      } catch (error) {
        this._log.warn({ error, toolName: tc.name }, "AMD tool execution failed");
      }
    }
    if (savedResult) {
      return { ...savedResult, rawResponse };
    }
    if (toolCalls.length > 0) {
      return {
        type: "amd_prediction",
        category: "uncertain" /* UNCERTAIN */,
        reason: "llm",
        transcript,
        rawResponse,
        isMachine: false,
        speechDurationMs: this.computeSpeechDurationMs(),
        delayMs: this.computeDelayMs()
      };
    }
    const parsed = this.parseDetection(rawResponse);
    return {
      type: "amd_prediction",
      ...parsed,
      transcript,
      rawResponse,
      isMachine: isMachineCategory(parsed.category),
      speechDurationMs: this.computeSpeechDurationMs(),
      delayMs: this.computeDelayMs()
    };
  }
  computeSpeechDurationMs() {
    if (this.speechStartedAt === void 0) {
      return 0;
    }
    const end = this.speechEndedAt ?? import_node_perf_hooks.performance.now();
    return Math.ceil(Math.max(0, end - this.speechStartedAt));
  }
  computeDelayMs() {
    if (this.speechEndedAt === void 0) {
      return 0;
    }
    return Math.ceil(Math.max(0, import_node_perf_hooks.performance.now() - this.speechEndedAt));
  }
  parseDetection(rawResponse) {
    var _a;
    const normalized = rawResponse.trim();
    const jsonStart = normalized.indexOf("{");
    const jsonEnd = normalized.lastIndexOf("}");
    const jsonChunk = jsonStart >= 0 && jsonEnd >= jsonStart ? normalized.slice(jsonStart, jsonEnd + 1) : normalized;
    try {
      const parsed = JSON.parse(jsonChunk);
      return {
        category: parseCategory(parsed.category),
        reason: ((_a = parsed.reason) == null ? void 0 : _a.trim()) || "No reason provided."
      };
    } catch {
      return {
        category: "uncertain" /* UNCERTAIN */,
        reason: normalized || "Failed to parse AMD model response."
      };
    }
  }
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  AMD,
  AMDCategory
});
//# sourceMappingURL=amd.cjs.map