import { Struct } from "@bufbuild/protobuf";
import { Mutex } from "@livekit/mutex";
import { AgentSession as pb } from "@livekit/protocol";
import { ThrowsPromise } from "@livekit/throws-transformer/throws";
import { context as otelContext, trace } from "@opentelemetry/api";
import { EventEmitter } from "node:events";
import {
  LLM as InferenceLLM,
  STT as InferenceSTT,
  TTS as InferenceTTS,
  TurnDetector as InferenceTurnDetector,
  VAD as InferenceVAD
} from "../inference/index.js";
import { getJobContext } from "../job.js";
import {
  AgentHandoffItem,
  ChatContext,
  ChatMessage
} from "../llm/chat_context.js";
import { log } from "../log.js";
import { ModelUsageCollector, filterZeroValues } from "../metrics/model_usage.js";
import { traceTypes, tracer } from "../telemetry/index.js";
import {
  DEFAULT_API_CONNECT_OPTIONS,
  DEFAULT_SESSION_CONNECT_OPTIONS
} from "../types.js";
import { Task, asError } from "../utils.js";
import {
  AgentActivity,
  cleanupReusableResources,
  isSchedulingPausedError
} from "./agent_activity.js";
import { AgentsConsole } from "./console_io.js";
import {
  AgentSessionEventTypes,
  CloseReason,
  createAgentStateChangedEvent,
  createCloseEvent,
  createConversationItemAddedEvent,
  createUserStateChangedEvent
} from "./events.js";
import { AgentInput, AgentOutput } from "./io.js";
import { RecorderIO } from "./recorder_io/index.js";
import { RoomSessionTransport, SessionHost } from "./remote_session.js";
import {
  DEFAULT_TEXT_INPUT_CALLBACK,
  RoomIO
} from "./room_io/index.js";
import { RunResult } from "./testing/run_result.js";
import { migrateLegacyOptions, stripUndefined } from "./turn_config/utils.js";
import { setParticipantSpanAttributes } from "./utils.js";
const RECORDING_ALL_ON = {
  audio: true,
  traces: true,
  logs: true,
  transcript: true
};
const RECORDING_ALL_OFF = {
  audio: false,
  traces: false,
  logs: false,
  transcript: false
};
function resolveRecordingOptions(record) {
  if (typeof record === "boolean") {
    return { ...record ? RECORDING_ALL_ON : RECORDING_ALL_OFF };
  }
  return { ...RECORDING_ALL_ON, ...record };
}
const defaultAgentSessionOptions = {
  maxToolSteps: 3,
  userAwayTimeout: 15,
  aecWarmupDuration: 3e3,
  ttsReadIdleTimeout: 1e4,
  forwardAudioIdleTimeout: 1e4,
  turnHandling: {},
  useTtsAlignedTranscript: true,
  ttsTextTransforms: ["filter_markdown", "filter_emoji"]
};
class AgentSession extends EventEmitter {
  vad;
  stt;
  llm;
  tts;
  turnDetection;
  /** @deprecated use {@link sessionOptions } instead */
  options;
  sessionOptions;
  activityLock = new Mutex();
  agent;
  activity;
  nextActivity;
  updateActivityTask;
  started = false;
  sessionHost;
  _chatCtx;
  _userData;
  _userState = "listening";
  _agentState = "initializing";
  _input;
  _output;
  closing = false;
  closingTask = null;
  userAwayTimer = null;
  _aecWarmupTimer = null;
  // Connection options for STT, LLM, and TTS
  _connOptions;
  // Unrecoverable error counts, reset after agent speaking
  llmErrorCounts = 0;
  ttsErrorCounts = 0;
  sessionSpan;
  agentSpeakingSpan;
  _interruptionDetection;
  /**
   * True iff this session auto-provisioned the bundled silero VAD because the
   * caller passed no `vad=`. Set once in the constructor; immutable from then
   * on. Read it via `AgentActivity.usingDefaultVad` from voice-pipeline code.
   *
   * @internal
   */
  _usingDefaultVad = false;
  /** @internal */
  _usageCollector = new ModelUsageCollector();
  /** @internal */
  _roomIO;
  /**
   * Currently active AMD instance, if one was constructed against this session.
   * Mirrors python `AgentSession._amd`. Useful for tests, telemetry, and
   * higher-level helpers that need to introspect classification state.
   */
  _amd = null;
  /** @internal */
  _aecWarmupRemaining = 0;
  /** @internal */
  _recorderIO;
  /** @internal */
  rootSpanContext;
  /** @internal */
  _recordedEvents = [];
  /** @internal Resolved per-category recording options for this session. */
  _recordingOptions = { ...RECORDING_ALL_OFF };
  /** @internal True when any recording category is enabled. */
  get _enableRecording() {
    return this._recordingOptions.audio || this._recordingOptions.traces || this._recordingOptions.logs || this._recordingOptions.transcript;
  }
  /** @internal - Timestamp when the session started (milliseconds) */
  _startedAt;
  /** @internal - Whether `start()` has been called and completed. */
  get _started() {
    return this.started;
  }
  /** @internal - Whether the session is closing/draining. */
  get _closing() {
    return this.closing;
  }
  /** @internal - Current run state for testing */
  _globalRunState;
  /** @internal */
  _userSpeakingSpan;
  logger = log();
  constructor(options = {}) {
    var _a;
    super();
    const { agentSessionOptions: opts, legacyVoiceOptions } = migrateLegacyOptions(options);
    const { vad, stt, llm, tts, userData, connOptions, ...resolvedSessionOptions } = opts;
    this._connOptions = {
      sttConnOptions: { ...DEFAULT_API_CONNECT_OPTIONS, ...connOptions == null ? void 0 : connOptions.sttConnOptions },
      llmConnOptions: { ...DEFAULT_API_CONNECT_OPTIONS, ...connOptions == null ? void 0 : connOptions.llmConnOptions },
      ttsConnOptions: { ...DEFAULT_API_CONNECT_OPTIONS, ...connOptions == null ? void 0 : connOptions.ttsConnOptions },
      maxUnrecoverableErrors: (connOptions == null ? void 0 : connOptions.maxUnrecoverableErrors) ?? DEFAULT_SESSION_CONNECT_OPTIONS.maxUnrecoverableErrors
    };
    this._usingDefaultVad = vad === void 0;
    if (vad === void 0) {
      this.vad = new InferenceVAD({ model: "silero" });
    } else if (vad === null) {
      this.vad = void 0;
    } else {
      this.vad = vad;
    }
    if (typeof stt === "string") {
      this.stt = InferenceSTT.fromModelString(stt);
    } else {
      this.stt = stt;
    }
    if (typeof llm === "string") {
      this.llm = InferenceLLM.fromModelString(llm);
    } else {
      this.llm = llm;
    }
    if (typeof tts === "string") {
      this.tts = InferenceTTS.fromModelString(tts);
    } else {
      this.tts = tts;
    }
    const configuredTurnDetection = resolvedSessionOptions.turnHandling.turnDetection;
    this.turnDetection = configuredTurnDetection === null ? void 0 : configuredTurnDetection ?? new InferenceTurnDetector();
    this._interruptionDetection = (_a = resolvedSessionOptions.turnHandling.interruption) == null ? void 0 : _a.mode;
    this._userData = userData;
    this._input = new AgentInput(this.onAudioInputChanged);
    this._output = new AgentOutput(this.onAudioOutputChanged, this.onTextOutputChanged);
    this._chatCtx = ChatContext.empty();
    this.sessionOptions = resolvedSessionOptions;
    this.options = legacyVoiceOptions;
    this._aecWarmupRemaining = this.sessionOptions.aecWarmupDuration ?? 0;
    this._onUserInputTranscribed = this._onUserInputTranscribed.bind(this);
    this.on(AgentSessionEventTypes.UserInputTranscribed, this._onUserInputTranscribed);
  }
  emit(event, ...args) {
    const eventData = args[0];
    this._recordedEvents.push(eventData);
    return super.emit(event, ...args);
  }
  get input() {
    return this._input;
  }
  get output() {
    return this._output;
  }
  get userData() {
    if (this._userData === void 0) {
      throw new Error("Voice agent userData is not set");
    }
    return this._userData;
  }
  get history() {
    return this._chatCtx;
  }
  /** Connection options for STT, LLM, and TTS. */
  get connOptions() {
    return this._connOptions;
  }
  get interruptionDetection() {
    return this._interruptionDetection;
  }
  /**
   * Returns usage summaries for this session, one per model/provider combination.
   */
  get usage() {
    return { modelUsage: this._usageCollector.flatten().map(filterZeroValues) };
  }
  get useTtsAlignedTranscript() {
    return this.sessionOptions.useTtsAlignedTranscript;
  }
  set userData(value) {
    this._userData = value;
  }
  async _startImpl({
    agent,
    room,
    inputOptions,
    outputOptions,
    span
  }) {
    span.setAttribute(traceTypes.ATTR_AGENT_LABEL, agent.id);
    this.agent = agent;
    this._updateAgentState("initializing");
    const tasks = [];
    const consoleInst = AgentsConsole.getInstance();
    if (consoleInst.enabled && !consoleInst.ioAcquired) {
      if (this.input.audio || this.output.audio) {
        this.logger.warn(
          "agent started with the console subcommand, but input.audio/output.audio is already set, overriding..."
        );
      }
      consoleInst.acquireIo(this);
      if (consoleInst.transport) {
        this.sessionHost = new SessionHost(
          consoleInst.transport,
          consoleInst.audioInput,
          consoleInst.audioOutput
        );
        this.sessionHost.registerSession(this);
      }
    } else if (room && !this._roomIO) {
      if (this.input.audio && (inputOptions == null ? void 0 : inputOptions.audioEnabled) !== false) {
        this.logger.warn(
          "RoomIO audio input is enabled but input.audio is already set, ignoring.."
        );
      }
      if (this.output.audio && (outputOptions == null ? void 0 : outputOptions.audioEnabled) !== false) {
        this.logger.warn(
          "RoomIO audio output is enabled but output.audio is already set, ignoring.."
        );
      }
      if (this.output.transcription && (outputOptions == null ? void 0 : outputOptions.transcriptionEnabled) !== false) {
        this.logger.warn(
          "RoomIO transcription output is enabled but output.transcription is already set, ignoring.."
        );
      }
      this._roomIO = new RoomIO({
        agentSession: this,
        room,
        inputOptions,
        outputOptions
      });
      this._roomIO.start();
      const transport = new RoomSessionTransport(room, this._roomIO);
      this.sessionHost = new SessionHost(transport);
      this.sessionHost.registerSession(this);
      if ((inputOptions == null ? void 0 : inputOptions.textEnabled) !== false) {
        this.sessionHost.registerTextInput(
          (inputOptions == null ? void 0 : inputOptions.textInputCallback) ?? DEFAULT_TEXT_INPUT_CALLBACK
        );
      }
    }
    const ctx = getJobContext(false);
    if (ctx) {
      if (room && ctx.room === room && !room.isConnected) {
        this.logger.debug("Auto-connecting to room via job context");
        tasks.push(ctx.connect());
      }
      const consoleForcesRecord = consoleInst.enabled && consoleInst.record;
      if (this.input.audio && this.output.audio && (this._recordingOptions.audio || consoleForcesRecord)) {
        this._recorderIO = new RecorderIO({ agentSession: this });
        this.input.audio = this._recorderIO.recordInput(this.input.audio);
        this.output.audio = this._recorderIO.recordOutput(this.output.audio);
        if (consoleForcesRecord || !consoleInst.enabled) {
          const sessionDir = ctx.sessionDirectory;
          if (sessionDir) {
            tasks.push(this._recorderIO.start(`${sessionDir}/audio.ogg`));
          }
        }
      }
    }
    tasks.push(this._updateActivity(this.agent, { waitOnEnter: false }));
    await ThrowsPromise.allSettled(tasks);
    if (this.sessionHost) {
      await this.sessionHost.start();
    }
    this.logger.debug(
      `using audio io: ${this.input.audio ? "`" + this.input.audio.constructor.name + "`" : "(none)"} -> \`AgentSession\` -> ${this.output.audio ? "`" + this.output.audio.constructor.name + "`" : "(none)"}`
    );
    if (this.sessionOptions.turnHandling.interruption.resumeFalseInterruption && this.output.audio && !this.output.audio.canPause) {
      this.logger.warn(
        {
          audioOutput: this.output.audio.constructor.name
        },
        "resumeFalseInterruption is enabled but audio output does not support pause, it will be ignored"
      );
    }
    this.logger.debug(
      `using transcript io: \`AgentSession\` -> ${this.output.transcription ? "`" + this.output.transcription.constructor.name + "`" : "(none)"}`
    );
    this.started = true;
    this._startedAt = Date.now();
    this._updateAgentState("listening");
  }
  async start({
    agent,
    room,
    inputOptions,
    outputOptions,
    record
  }) {
    if (this.started) {
      return;
    }
    this.closing = false;
    this._usageCollector = new ModelUsageCollector();
    const ctx = getJobContext(false);
    if (ctx) {
      const recordIsGiven = record !== void 0;
      if (record === void 0) {
        record = ctx.job.enableRecording;
      }
      this._recordingOptions = resolveRecordingOptions(record);
      if (ctx._primaryAgentSession === void 0 || ctx._primaryAgentSession === this) {
        ctx._primaryAgentSession = this;
      } else if (this._enableRecording) {
        if (recordIsGiven) {
          throw new Error(
            "Only one `AgentSession` can be the primary at a time. If you want to ignore primary designation, use `session.start({ record: false })`."
          );
        }
        this._recordingOptions = resolveRecordingOptions(false);
      }
      if (this._enableRecording) {
        await ctx.initRecording(this._recordingOptions);
      }
    }
    this.sessionSpan = tracer.startSpan({
      name: "agent_session"
    });
    this.rootSpanContext = trace.setSpan(otelContext.active(), this.sessionSpan);
    await this._startImpl({
      agent,
      room,
      inputOptions,
      outputOptions,
      span: this.sessionSpan
    });
  }
  updateAgent(agent) {
    var _a;
    this.agent = agent;
    if (!this.started) {
      return;
    }
    (_a = this.activity) == null ? void 0 : _a.blockNewTurns();
    const _updateActivityTask = async (oldTask2, agent2) => {
      if (oldTask2) {
        try {
          await oldTask2.result;
        } catch (error) {
          this.logger.error(error, "previous updateAgent transition failed");
        }
      }
      await this._updateActivity(agent2);
    };
    const oldTask = this.updateActivityTask;
    this.updateActivityTask = Task.from(
      async () => _updateActivityTask(oldTask, agent),
      void 0,
      "AgentSession_updateActivityTask"
    );
    const runState = this._globalRunState;
    if (runState) {
      runState._watchHandle(this.updateActivityTask);
    }
  }
  commitUserTurn() {
    if (!this.activity) {
      throw new Error("AgentSession is not running");
    }
    this.activity.commitUserTurn();
  }
  clearUserTurn() {
    if (!this.activity) {
      throw new Error("AgentSession is not running");
    }
    this.activity.clearUserTurn();
  }
  say(text, options) {
    if (!this.activity) {
      throw new Error("AgentSession is not running");
    }
    const doSay = (activity, nextActivity) => {
      if (activity.schedulingPaused) {
        if (!nextActivity) {
          throw new Error("AgentSession is closing, cannot use say()");
        }
        return nextActivity.say(text, options);
      }
      return activity.say(text, options);
    };
    const runState = this._globalRunState;
    let handle;
    const activeSpan = trace.getActiveSpan();
    if (!activeSpan && this.rootSpanContext) {
      handle = otelContext.with(
        this.rootSpanContext,
        () => doSay(this.activity, this.nextActivity)
      );
    } else {
      handle = doSay(this.activity, this.nextActivity);
    }
    if (runState) {
      runState._watchHandle(handle);
    }
    return handle;
  }
  interrupt(options) {
    if (!this.activity) {
      throw new Error("AgentSession is not running");
    }
    return this.activity.interrupt(options);
  }
  /** @internal — emit a debug/trace payload to the debugger/recorder. */
  _emitDebugMessage(payload) {
    const debugMessage = new pb.DebugMessage({ payload: Struct.fromJson(payload) });
    super.emit(AgentSessionEventTypes.DebugMessage, debugMessage);
  }
  /**
   * The currently bound `AMD` instance, or `null` if AMD is not in use.
   * Mirrors python `AgentSession.amd`.
   */
  get amd() {
    return this._amd;
  }
  /** @internal — used by AMD to register/unregister itself with the session. */
  _setAmd(amd) {
    this._amd = amd;
  }
  /**
   * The currently running activity, or `undefined` when none is active. Mirrors
   * python `session._activity` — exposed so tightly-coupled internals (e.g. AMD)
   * can read activity state such as the endpointing delay.
   * @internal
   */
  get _activity() {
    return this.activity;
  }
  /**
   * @internal — forwarded to {@link SessionHost} so a connected
   * {@link RemoteSession} peer receives an `amd_prediction` event when AMD
   * settles. Mirrors python `AgentSession._session_host._on_amd_prediction`.
   */
  _onAmdPrediction(event) {
    var _a;
    (_a = this.sessionHost) == null ? void 0 : _a._onAmdPrediction(event);
  }
  /**
   * @internal — returns a tee'd branch of the active participant audio stream
   * for AMD's dedicated STT. Returns `undefined` when no `AgentActivity` is
   * running yet (the AMD STT pump retries until an activity is available).
   */
  _subscribeAudioStream() {
    var _a;
    return (_a = this.activity) == null ? void 0 : _a.subscribeAudioStream();
  }
  pauseReplyAuthorization() {
    if (!this.activity) {
      throw new Error("AgentSession is not running");
    }
    this.activity.pauseReplyAuthorization();
  }
  resumeReplyAuthorization() {
    if (!this.activity) {
      throw new Error("AgentSession is not running");
    }
    this.activity.resumeReplyAuthorization();
  }
  updateOptions(options) {
    var _a, _b;
    const endpointing = (_a = options.turnHandling) == null ? void 0 : _a.endpointing;
    const turnDetection = ((_b = options.turnHandling) == null ? void 0 : _b.turnDetection) !== void 0 ? options.turnHandling.turnDetection : options.turnDetection;
    const hasTurnDetection = turnDetection !== void 0;
    const normalizedTurnDetection = turnDetection ?? void 0;
    if (endpointing !== void 0) {
      const stripped = stripUndefined(endpointing);
      this.sessionOptions.turnHandling.endpointing = {
        ...this.sessionOptions.turnHandling.endpointing,
        ...stripped
      };
      this.sessionOptions.turnHandling.endpointingOverrides = {
        ...this.sessionOptions.turnHandling.endpointingOverrides,
        ...stripped
      };
    }
    if (hasTurnDetection) {
      this.turnDetection = normalizedTurnDetection;
      this.sessionOptions.turnHandling.turnDetection = normalizedTurnDetection;
    }
    if (this.activity) {
      const activityOptions = {};
      if (endpointing !== void 0) {
        activityOptions.endpointing = this.sessionOptions.turnHandling.endpointing;
      }
      if (hasTurnDetection) {
        activityOptions.turnDetection = turnDetection;
      }
      this.activity.updateOptions(activityOptions);
    }
  }
  generateReply(options) {
    if (!this.activity) {
      throw new Error("AgentSession is not running");
    }
    const userMessage = (options == null ? void 0 : options.userInput) instanceof ChatMessage ? options.userInput : (options == null ? void 0 : options.userInput) ? new ChatMessage({
      role: "user",
      content: options.userInput
    }) : void 0;
    const inputDetails = { modality: (options == null ? void 0 : options.inputModality) ?? "text" };
    const doGenerateReply = (activity, nextActivity) => {
      if (activity.schedulingPaused) {
        if (!nextActivity) {
          throw new Error("AgentSession is closing, cannot use generateReply()");
        }
        return nextActivity.generateReply({ userMessage, ...options, inputDetails });
      }
      try {
        return activity.generateReply({ userMessage, ...options, inputDetails });
      } catch (error) {
        const canFallback = nextActivity !== void 0 && isSchedulingPausedError(error);
        if (!canFallback) {
          throw error;
        }
        this.logger.debug(
          { error },
          "generateReply scheduling raced with handoff drain; retrying on next activity"
        );
        return nextActivity.generateReply({ userMessage, ...options, inputDetails });
      }
    };
    const activeSpan = trace.getActiveSpan();
    let handle;
    if (!activeSpan && this.rootSpanContext) {
      handle = otelContext.with(
        this.rootSpanContext,
        () => doGenerateReply(this.activity, this.nextActivity)
      );
    } else {
      handle = doGenerateReply(this.activity, this.nextActivity);
    }
    if (this._globalRunState) {
      this._globalRunState._watchHandle(handle);
    }
    return handle;
  }
  /**
   * Run a test with user input and return a result for assertions.
   *
   * This method is primarily used for testing agent behavior without
   * requiring a real room connection.
   *
   * @example
   * ```typescript
   * const result = await session.run({ userInput: 'Hello' });
   * result.expect.nextEvent().isMessage({ role: 'assistant' });
   * result.expect.noMoreEvents();
   * ```
   *
   * @param options - Run options including user input and optional output type
   * @returns A RunResult that resolves when the agent finishes responding
   */
  run({
    userInput,
    inputModality,
    outputType
  }) {
    if (this._globalRunState && !this._globalRunState.done()) {
      throw new Error("nested runs are not supported");
    }
    const runState = new RunResult({
      userInput,
      outputType
    });
    this._globalRunState = runState;
    (async () => {
      try {
        const unlock = await this.activityLock.lock();
        unlock();
        this.generateReply({ userInput, inputModality });
      } catch (e) {
        runState._reject(asError(e));
      }
    })();
    return runState;
  }
  /** @internal */
  async _updateActivity(agent, options = {}) {
    const { previousActivity = "close", newActivity = "start", blockedTasks = [] } = options;
    const waitOnEnter = options.waitOnEnter ?? newActivity === "start";
    const runWithContext = async () => {
      var _a;
      const unlock = await this.activityLock.lock();
      let onEnterTask;
      let reusableResources;
      try {
        this.agent = agent;
        const prevActivityObj = this.activity;
        if (newActivity === "start") {
          const prevAgent = prevActivityObj == null ? void 0 : prevActivityObj.agent;
          if (agent._agentActivity && // allow updating the same agent that is running
          (agent !== prevAgent || previousActivity !== "close")) {
            throw new Error("Cannot start agent: an activity is already running");
          }
          this.nextActivity = new AgentActivity(agent, this);
        } else if (newActivity === "resume") {
          if (!agent._agentActivity) {
            throw new Error("Cannot resume agent: no existing activity to resume");
          }
          this.nextActivity = agent._agentActivity;
        }
        if (prevActivityObj && prevActivityObj !== this.nextActivity) {
          if (previousActivity === "pause") {
            reusableResources = await prevActivityObj.pause({
              blockedTasks,
              newActivity: this.nextActivity
            });
          } else {
            reusableResources = await prevActivityObj.drain({
              newActivity: this.nextActivity
            });
            await prevActivityObj.close();
          }
        }
        if (this.closing && newActivity === "start") {
          this.logger.warn(
            { agentId: (_a = this.nextActivity) == null ? void 0 : _a.agent.id },
            "Session is closing, skipping start of next activity"
          );
          if (reusableResources) {
            await cleanupReusableResources(reusableResources, this.logger);
            reusableResources = void 0;
          }
          this.nextActivity = void 0;
          this.activity = void 0;
          return;
        }
        this.activity = this.nextActivity;
        this.nextActivity = void 0;
        const runState = this._globalRunState;
        const handoffItem = new AgentHandoffItem({
          oldAgentId: prevActivityObj == null ? void 0 : prevActivityObj.agent.id,
          newAgentId: agent.id
        });
        if (runState) {
          runState._agentHandoff({
            item: handoffItem,
            oldAgent: prevActivityObj == null ? void 0 : prevActivityObj.agent,
            newAgent: this.activity.agent
          });
        }
        this._conversationItemAdded(handoffItem);
        this.logger.debug(
          { previousAgentId: prevActivityObj == null ? void 0 : prevActivityObj.agent.id, newAgentId: agent.id },
          "Agent handoff inserted into chat context"
        );
        if (newActivity === "start") {
          await this.activity.start({ reuseResources: reusableResources });
        } else {
          await this.activity.resume({ reuseResources: reusableResources });
        }
        reusableResources = void 0;
        onEnterTask = this.activity._onEnterTask;
        if (this._input.audio) {
          this.activity.attachAudioInput(this._input.audio.stream);
        }
      } catch (error) {
        if (reusableResources) {
          await cleanupReusableResources(reusableResources, this.logger);
        }
        throw error;
      } finally {
        unlock();
      }
      if (waitOnEnter) {
        if (!onEnterTask) {
          throw new Error("expected onEnter task to be available while waitOnEnter=true");
        }
        await onEnterTask.result;
      }
    };
    if (this.rootSpanContext) {
      return otelContext.with(this.rootSpanContext, runWithContext);
    }
    return runWithContext();
  }
  get chatCtx() {
    return this._chatCtx.copy();
  }
  get agentState() {
    return this._agentState;
  }
  get userState() {
    return this._userState;
  }
  get currentAgent() {
    if (!this.agent) {
      throw new Error("AgentSession is not running");
    }
    return this.agent;
  }
  async close() {
    await this.closeImpl(CloseReason.USER_INITIATED);
  }
  shutdown(options) {
    const { drain = true, reason = CloseReason.USER_INITIATED } = options ?? {};
    this._closeSoon({
      reason,
      drain
    });
  }
  /** @internal */
  _closeSoon({
    reason,
    drain = false,
    error = null
  }) {
    if (this.closingTask) {
      return;
    }
    this.closingTask = this.closeImpl(reason, error, drain).finally(() => {
      this.closingTask = null;
    });
  }
  /** @internal */
  _onError(error) {
    if (this.closingTask || error.recoverable) {
      return;
    }
    if (error.type === "llm_error") {
      this.llmErrorCounts += 1;
      if (this.llmErrorCounts <= this._connOptions.maxUnrecoverableErrors) {
        return;
      }
    } else if (error.type === "tts_error") {
      this.ttsErrorCounts += 1;
      if (this.ttsErrorCounts <= this._connOptions.maxUnrecoverableErrors) {
        return;
      }
    }
    this.logger.error(error, "AgentSession is closing due to an unrecoverable error");
    this.closingTask = (async () => {
      await this.closeImpl(CloseReason.ERROR, error);
    })().then(() => {
      this.closingTask = null;
    });
  }
  /** @internal */
  _conversationItemAdded(item) {
    this._chatCtx.insert(item);
    this.emit(AgentSessionEventTypes.ConversationItemAdded, createConversationItemAddedEvent(item));
  }
  /** @internal */
  _toolItemsAdded(items) {
    this._chatCtx.insert(items);
  }
  /** @internal */
  _updateAgentState(state, options) {
    var _a;
    if (this._agentState === state) {
      return;
    }
    if (state === "speaking") {
      this.llmErrorCounts = 0;
      this.ttsErrorCounts = 0;
      if (this.agentSpeakingSpan === void 0) {
        this.agentSpeakingSpan = tracer.startSpan({
          name: "agent_speaking",
          context: (options == null ? void 0 : options.otelContext) ?? this.rootSpanContext,
          startTime: options == null ? void 0 : options.startTime
        });
        const localParticipant = (_a = this._roomIO) == null ? void 0 : _a.localParticipant;
        if (localParticipant) {
          setParticipantSpanAttributes(this.agentSpeakingSpan, localParticipant);
        }
      }
    } else if (this.agentSpeakingSpan !== void 0) {
      this.agentSpeakingSpan.end();
      this.agentSpeakingSpan = void 0;
    }
    if (state === "speaking" && this._aecWarmupRemaining > 0 && this._aecWarmupTimer === null) {
      this._aecWarmupTimer = setTimeout(() => this._onAecWarmupExpired(), this._aecWarmupRemaining);
      this.logger.debug(
        { warmupDurationMs: this._aecWarmupRemaining },
        "aec warmup active, disabling interruptions"
      );
    }
    const oldState = this._agentState;
    this._agentState = state;
    if (state === "listening" && this._userState === "listening") {
      this._setUserAwayTimer();
    } else {
      this._cancelUserAwayTimer();
    }
    this.emit(
      AgentSessionEventTypes.AgentStateChanged,
      createAgentStateChangedEvent(oldState, state)
    );
  }
  /** @internal */
  _updateUserState(state, options) {
    var _a;
    if (this._userState === state) {
      return;
    }
    if (state === "speaking" && this._userSpeakingSpan === void 0) {
      this._userSpeakingSpan = tracer.startSpan({
        name: "user_speaking",
        context: (options == null ? void 0 : options.otelContext) ?? this.rootSpanContext,
        startTime: options == null ? void 0 : options.lastSpeakingTime
      });
      const linked = (_a = this._roomIO) == null ? void 0 : _a.linkedParticipant;
      if (linked) {
        setParticipantSpanAttributes(this._userSpeakingSpan, linked);
      }
    } else if (this._userSpeakingSpan !== void 0) {
      this._userSpeakingSpan.end(options == null ? void 0 : options.lastSpeakingTime);
      this._userSpeakingSpan = void 0;
    }
    const oldState = this._userState;
    this._userState = state;
    if (state === "listening" && this._agentState === "listening") {
      this._setUserAwayTimer();
    } else {
      this._cancelUserAwayTimer();
    }
    this.emit(
      AgentSessionEventTypes.UserStateChanged,
      createUserStateChangedEvent(oldState, state)
    );
  }
  // -- User changed input/output streams/sinks --
  onAudioInputChanged() {
    if (!this.started) {
      return;
    }
    if (this.activity && this._input.audio) {
      this.activity.attachAudioInput(this._input.audio.stream);
    }
  }
  onAudioOutputChanged() {
    if (this.started && this.sessionOptions.turnHandling.interruption.resumeFalseInterruption && this.output.audio && !this.output.audio.canPause) {
      this.logger.warn(
        {
          audioOutput: this.output.audio.constructor.name
        },
        "resumeFalseInterruption is enabled, but the audio output does not support pause, ignored"
      );
    }
  }
  onTextOutputChanged() {
  }
  _setUserAwayTimer() {
    this._cancelUserAwayTimer();
    if (this.sessionOptions.userAwayTimeout === null || this.sessionOptions.userAwayTimeout === void 0) {
      return;
    }
    if (this._roomIO && !this._roomIO.isParticipantAvailable) {
      return;
    }
    this.userAwayTimer = setTimeout(() => {
      this.logger.debug("User away timeout triggered");
      this._updateUserState("away");
    }, this.sessionOptions.userAwayTimeout * 1e3);
  }
  _cancelUserAwayTimer() {
    if (this.userAwayTimer !== null) {
      clearTimeout(this.userAwayTimer);
      this.userAwayTimer = null;
    }
  }
  /** @internal */
  _onAecWarmupExpired() {
    if (this._aecWarmupRemaining > 0) {
      this.logger.debug("aec warmup expired, re-enabling interruptions");
    }
    this._aecWarmupRemaining = 0;
    if (this._aecWarmupTimer !== null) {
      clearTimeout(this._aecWarmupTimer);
      this._aecWarmupTimer = null;
    }
  }
  _onUserInputTranscribed(ev) {
    if (this._userState === "away" && ev.isFinal) {
      this.logger.debug("User returned from away state due to speech input");
      this._updateUserState("listening");
    }
  }
  async closeImpl(reason, error = null, drain = false) {
    if (this.rootSpanContext) {
      return otelContext.with(this.rootSpanContext, async () => {
        await this.closeImplInner(reason, error, drain);
      });
    }
    return this.closeImplInner(reason, error, drain);
  }
  async closeImplInner(reason, error = null, drain = false) {
    var _a, _b, _c, _d;
    if (!this.started) {
      return;
    }
    this.closing = true;
    this._cancelUserAwayTimer();
    this._onAecWarmupExpired();
    this.off(AgentSessionEventTypes.UserInputTranscribed, this._onUserInputTranscribed);
    if (this.activity) {
      if (!drain) {
        try {
          await this.activity.interrupt({ force: true }).await;
        } catch (error2) {
          this.logger.warn({ error: error2 }, "Error interrupting activity");
        }
      }
      await this.activity.drain();
      await ((_a = this.activity.currentSpeech) == null ? void 0 : _a.waitForPlayout());
      if (reason !== CloseReason.ERROR) {
        this.activity.commitUserTurn({ audioDetached: true, throwIfNotReady: false });
      }
      try {
        this.activity.detachAudioInput();
      } catch (error2) {
      }
    }
    if (this._recorderIO) {
      await this._recorderIO.close();
    }
    this.input.audio = null;
    this.output.audio = null;
    this.output.transcription = null;
    await ((_b = this.activity) == null ? void 0 : _b.close());
    this.activity = void 0;
    if (this.sessionSpan) {
      this.sessionSpan.end();
      this.sessionSpan = void 0;
    }
    if (this._userSpeakingSpan) {
      this._userSpeakingSpan.end();
      this._userSpeakingSpan = void 0;
    }
    if (this.agentSpeakingSpan) {
      this.agentSpeakingSpan.end();
      this.agentSpeakingSpan = void 0;
    }
    this.started = false;
    this.emit(AgentSessionEventTypes.Close, createCloseEvent(reason, error));
    this._userState = "listening";
    this._agentState = "initializing";
    this.rootSpanContext = void 0;
    this.llmErrorCounts = 0;
    this.ttsErrorCounts = 0;
    await ((_c = this.sessionHost) == null ? void 0 : _c.close());
    this.sessionHost = void 0;
    await ((_d = this._roomIO) == null ? void 0 : _d.close());
    this._roomIO = void 0;
    this.logger.info({ reason, error }, "AgentSession closed");
  }
}
export {
  AgentSession,
  defaultAgentSessionOptions,
  resolveRecordingOptions
};
//# sourceMappingURL=agent_session.js.map