import { AsyncLocalStorage } from "node:async_hooks";
import { ReadableStream } from "node:stream/web";
import {
  LLM as InferenceLLM,
  STT as InferenceSTT,
  TTS as InferenceTTS
} from "../inference/index.js";
import { ReadonlyChatContext } from "../llm/chat_context.js";
import {
  ChatContext,
  LLM,
  RealtimeModel
} from "../llm/index.js";
import { log } from "../log.js";
import { StreamAdapter as STTStreamAdapter } from "../stt/index.js";
import { SentenceTokenizer as BasicSentenceTokenizer } from "../tokenize/basic/index.js";
import { SynthesizeStream, StreamAdapter as TTSStreamAdapter } from "../tts/index.js";
import { USERDATA_TIMED_TRANSCRIPT } from "../types.js";
import { Future, Task } from "../utils.js";
import { agentActivityStorage } from "./agent_activity.js";
import { migrateTurnHandling } from "./turn_config/utils.js";
const functionCallStorage = new AsyncLocalStorage();
const speechHandleStorage = new AsyncLocalStorage();
const activityTaskInfoStorage = /* @__PURE__ */ new WeakMap();
function _setActivityTaskInfo(task, options) {
  const info = activityTaskInfoStorage.get(task) ?? {
    functionCall: null,
    speechHandle: null,
    inlineTask: false
  };
  if (Object.hasOwn(options, "functionCall")) {
    info.functionCall = options.functionCall ?? null;
  }
  if (Object.hasOwn(options, "speechHandle")) {
    info.speechHandle = options.speechHandle ?? null;
  }
  if (Object.hasOwn(options, "inlineTask")) {
    info.inlineTask = options.inlineTask ?? false;
  }
  activityTaskInfoStorage.set(task, info);
}
function _getActivityTaskInfo(task) {
  return activityTaskInfoStorage.get(task);
}
const STOP_RESPONSE_SYMBOL = Symbol("StopResponse");
class StopResponse extends Error {
  constructor() {
    super();
    this.name = "StopResponse";
    Object.defineProperty(this, STOP_RESPONSE_SYMBOL, {
      value: true
    });
  }
}
function isStopResponse(value) {
  return value !== void 0 && value !== null && typeof value === "object" && STOP_RESPONSE_SYMBOL in value;
}
class Agent {
  _id;
  _stt;
  _vad;
  _llm;
  _tts;
  _turnHandling;
  _minConsecutiveSpeechDelay;
  _useTtsAlignedTranscript;
  /** @internal */
  _agentActivity;
  /** @internal */
  _chatCtx;
  /** @internal */
  _instructions;
  /** @internal */
  _tools;
  constructor({
    id,
    instructions,
    chatCtx,
    tools,
    turnDetection,
    stt,
    vad,
    llm,
    tts,
    allowInterruptions,
    turnHandling,
    minConsecutiveSpeechDelay,
    useTtsAlignedTranscript
  }) {
    if (id) {
      this._id = id;
    } else {
      const className = this.constructor.name;
      if (className === "Agent") {
        this._id = "default_agent";
      } else {
        this._id = className.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
      }
    }
    this._instructions = instructions;
    this._tools = { ...tools };
    this._chatCtx = chatCtx ? chatCtx.copy({
      toolCtx: this._tools
    }) : ChatContext.empty();
    const resolvedTurnHandling = migrateTurnHandling({
      turnDetection,
      allowInterruptions,
      turnHandling
    });
    this._turnHandling = Object.keys(resolvedTurnHandling).length > 0 ? resolvedTurnHandling : void 0;
    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;
    }
    this._minConsecutiveSpeechDelay = minConsecutiveSpeechDelay;
    this._useTtsAlignedTranscript = useTtsAlignedTranscript;
    this._agentActivity = void 0;
  }
  get vad() {
    return this._vad;
  }
  get stt() {
    return this._stt;
  }
  get llm() {
    return this._llm;
  }
  get tts() {
    return this._tts;
  }
  get useTtsAlignedTranscript() {
    return this._useTtsAlignedTranscript;
  }
  get chatCtx() {
    return new ReadonlyChatContext(this._chatCtx.items);
  }
  get id() {
    return this._id;
  }
  get instructions() {
    return this._instructions;
  }
  get toolCtx() {
    return { ...this._tools };
  }
  get session() {
    return this.getActivityOrThrow().agentSession;
  }
  get turnHandling() {
    return this._turnHandling;
  }
  get minConsecutiveSpeechDelay() {
    return this._minConsecutiveSpeechDelay;
  }
  async onEnter() {
  }
  async onExit() {
  }
  async transcriptionNode(text, modelSettings) {
    return Agent.default.transcriptionNode(this, text, modelSettings);
  }
  async onUserTurnCompleted(_chatCtx, _newMessage) {
  }
  async onUserTurnExceeded(ev) {
    await this.session.generateReply({
      userInput: ev.transcript,
      instructions: "The user has been speaking too long without giving a chance to reply. Politely cut in with a short reply or notice. Keep it short since the user cannot interrupt it.",
      allowInterruptions: false,
      toolChoice: "none",
      inputModality: "audio"
    });
  }
  async sttNode(audio, modelSettings) {
    return Agent.default.sttNode(this, audio, modelSettings);
  }
  async llmNode(chatCtx, toolCtx, modelSettings) {
    return Agent.default.llmNode(this, chatCtx, toolCtx, modelSettings);
  }
  async ttsNode(text, modelSettings) {
    return Agent.default.ttsNode(this, text, modelSettings);
  }
  async realtimeAudioOutputNode(audio, modelSettings) {
    return Agent.default.realtimeAudioOutputNode(this, audio, modelSettings);
  }
  // realtime_audio_output_node
  getActivityOrThrow() {
    if (!this._agentActivity) {
      throw new Error("Agent activity not found");
    }
    return this._agentActivity;
  }
  async updateChatCtx(chatCtx) {
    if (!this._agentActivity) {
      this._chatCtx = chatCtx.copy({ toolCtx: this.toolCtx });
      return;
    }
    this._agentActivity.updateChatCtx(chatCtx);
  }
  // TODO: Add when AgentConfigUpdate is ported to ChatContext.
  async updateTools(tools) {
    if (!this._agentActivity) {
      this._tools = { ...tools };
      this._chatCtx = this._chatCtx.copy({ toolCtx: this._tools });
      return;
    }
    await this._agentActivity.updateTools(tools);
  }
  static default = {
    async sttNode(agent, audio, _modelSettings) {
      var _a;
      const activity = agent.getActivityOrThrow();
      if (!activity.stt) {
        throw new Error("sttNode called but no STT node is available");
      }
      let wrappedStt = activity.stt;
      if (!wrappedStt.capabilities.streaming) {
        const vad = agent.vad || activity.vad;
        if (!vad) {
          throw new Error(
            "STT does not support streaming, add a VAD to the AgentTask/VoiceAgent to enable streaming"
          );
        }
        wrappedStt = new STTStreamAdapter(wrappedStt, vad);
      }
      const connOptions = activity.agentSession.connOptions.sttConnOptions;
      const stream = wrappedStt.stream({ connOptions });
      const audioInputStartedAt = activity.inputStartedAt ?? // Use input started at proxied from AudioRecognition if available
      ((_a = activity.agentSession._recorderIO) == null ? void 0 : _a.recordingStartedAt) ?? // Fallback to recording start time if available
      activity.agentSession._startedAt ?? // Fallback to session start time
      Date.now();
      stream.startTimeOffset = (Date.now() - audioInputStartedAt) / 1e3;
      stream.updateInputStream(audio);
      let cleaned = false;
      const cleanup = () => {
        if (cleaned) return;
        cleaned = true;
        stream.detachInputStream();
        stream.close();
      };
      return new ReadableStream({
        async start(controller) {
          try {
            for await (const event of stream) {
              controller.enqueue(event);
            }
            controller.close();
          } finally {
            cleanup();
          }
        },
        cancel() {
          cleanup();
        }
      });
    },
    async llmNode(agent, chatCtx, toolCtx, modelSettings) {
      const activity = agent.getActivityOrThrow();
      if (!activity.llm) {
        throw new Error("llmNode called but no LLM node is available");
      }
      if (!(activity.llm instanceof LLM)) {
        throw new Error(
          "llmNode should only be used with LLM (non-multimodal/realtime APIs) nodes"
        );
      }
      const { toolChoice } = modelSettings;
      const connOptions = activity.agentSession.connOptions.llmConnOptions;
      const stream = activity.llm.chat({
        chatCtx,
        toolCtx,
        toolChoice,
        connOptions
      });
      let cleaned = false;
      const cleanup = () => {
        if (cleaned) return;
        cleaned = true;
        stream.close();
      };
      return new ReadableStream({
        async start(controller) {
          try {
            for await (const chunk of stream) {
              controller.enqueue(chunk);
            }
            controller.close();
          } finally {
            cleanup();
          }
        },
        cancel() {
          cleanup();
        }
      });
    },
    async ttsNode(agent, text, _modelSettings) {
      const activity = agent.getActivityOrThrow();
      if (!activity.tts) {
        throw new Error("ttsNode called but no TTS node is available");
      }
      let wrappedTts = activity.tts;
      if (!activity.tts.capabilities.streaming) {
        wrappedTts = new TTSStreamAdapter(wrappedTts, new BasicSentenceTokenizer());
      }
      const connOptions = activity.agentSession.connOptions.ttsConnOptions;
      const stream = wrappedTts.stream({ connOptions });
      stream.updateInputStream(text);
      let cleaned = false;
      const cleanup = async () => {
        if (cleaned) return;
        cleaned = true;
        stream.close();
        await text.cancel("tts node cleanup").catch(() => {
        });
        if (wrappedTts !== activity.tts) {
          await wrappedTts.close();
        }
      };
      return new ReadableStream({
        async start(controller) {
          try {
            for await (const chunk of stream) {
              if (chunk === SynthesizeStream.END_OF_STREAM) {
                break;
              }
              if (chunk.timedTranscripts && chunk.timedTranscripts.length > 0) {
                chunk.frame.userdata[USERDATA_TIMED_TRANSCRIPT] = chunk.timedTranscripts;
              }
              controller.enqueue(chunk.frame);
            }
            controller.close();
          } finally {
            await cleanup();
          }
        },
        cancel() {
          return cleanup();
        }
      });
    },
    async transcriptionNode(agent, text, _modelSettings) {
      return text;
    },
    async realtimeAudioOutputNode(_agent, audio, _modelSettings) {
      return audio;
    }
  };
}
class AgentTask extends Agent {
  started = false;
  future = new Future();
  _preserveFunctionCallHistory;
  #logger = log();
  constructor(options) {
    const { preserveFunctionCallHistory = false, ...rest } = options;
    super(rest);
    this._preserveFunctionCallHistory = preserveFunctionCallHistory;
  }
  get done() {
    return this.future.done;
  }
  complete(result) {
    if (this.future.done) {
      throw new Error(`${this.constructor.name} is already done`);
    }
    if (result instanceof Error) {
      this.future.reject(result);
    } else {
      this.future.resolve(result);
    }
    const speechHandle = speechHandleStorage.getStore();
    if (speechHandle) {
      speechHandle._maybeRunFinalOutput = result;
    }
  }
  async run() {
    if (this.started) {
      throw new Error(
        `Task ${this.constructor.name} has already started and cannot be awaited multiple times`
      );
    }
    this.started = true;
    const currentTask = Task.current();
    if (!currentTask) {
      throw new Error(`${this.constructor.name} must be executed inside a Task context`);
    }
    const taskInfo = _getActivityTaskInfo(currentTask);
    if (!taskInfo || !taskInfo.inlineTask) {
      throw new Error(
        `${this.constructor.name} should only be awaited inside function tools or the onEnter/onExit methods of an Agent`
      );
    }
    const speechHandle = speechHandleStorage.getStore();
    const oldActivity = agentActivityStorage.getStore();
    if (!oldActivity) {
      throw new Error(`${this.constructor.name} must be executed inside an AgentActivity context`);
    }
    currentTask.addDoneCallback(() => {
      if (this.future.done) return;
      this.#logger.error(`The Task finished before ${this.constructor.name} was completed.`);
      this.complete(new Error(`The Task finished before ${this.constructor.name} was completed.`));
    });
    const oldAgent = oldActivity.agent;
    const session = oldActivity.agentSession;
    const blockedTasks = [currentTask];
    const onEnterTask = oldActivity._onEnterTask;
    if (onEnterTask && !onEnterTask.done && onEnterTask !== currentTask) {
      blockedTasks.push(onEnterTask);
    }
    if (taskInfo.functionCall && oldActivity.llm instanceof RealtimeModel && !oldActivity.llm.capabilities.manualFunctionCalls) {
      this.#logger.error(
        `Realtime model does not support resuming function calls from chat context, using AgentTask inside a function tool may have unexpected behavior.`
      );
    }
    await session._updateActivity(this, {
      previousActivity: "pause",
      newActivity: "start",
      blockedTasks
    });
    let runState = session._globalRunState;
    if (speechHandle && runState && !runState.done()) {
      if (runState._watchedHandleCount() > 1) {
        runState._unwatchHandle(speechHandle);
      }
      runState._markDoneIfNeeded();
    }
    try {
      return await this.future.await;
    } finally {
      runState = session._globalRunState;
      if (session.currentAgent !== this) {
        this.#logger.warn(
          `${this.constructor.name} completed, but the agent has changed in the meantime. Ignoring handoff to the previous agent, likely due to AgentSession.updateAgent being invoked.`
        );
        await oldActivity.close();
      } else {
        if (speechHandle && runState && !runState.done()) {
          runState._watchHandle(speechHandle);
        }
        const mergedChatCtx = oldAgent._chatCtx.merge(this._chatCtx, {
          excludeFunctionCall: !this._preserveFunctionCallHistory,
          excludeInstructions: true
        });
        oldAgent._chatCtx.items = mergedChatCtx.items;
        await session._updateActivity(oldAgent, {
          previousActivity: "close",
          newActivity: "resume",
          waitOnEnter: false
        });
      }
    }
  }
}
export {
  Agent,
  AgentTask,
  STOP_RESPONSE_SYMBOL,
  StopResponse,
  _getActivityTaskInfo,
  _setActivityTaskInfo,
  functionCallStorage,
  isStopResponse,
  speechHandleStorage
};
//# sourceMappingURL=agent.js.map