"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 tool_executor_exports = {};
__export(tool_executor_exports, {
  DUPLICATE_CONFIRM: () => DUPLICATE_CONFIRM,
  DUPLICATE_REJECT: () => DUPLICATE_REJECT,
  REPLY_INSTRUCTIONS_AT_TAIL: () => REPLY_INSTRUCTIONS_AT_TAIL,
  REPLY_INSTRUCTIONS_MAYBE_COVERED: () => REPLY_INSTRUCTIONS_MAYBE_COVERED,
  ToolExecutor: () => ToolExecutor,
  UPDATE_TEMPLATE: () => UPDATE_TEMPLATE,
  buildExecutorMap: () => buildExecutorMap,
  cancelTaskTool: () => cancelTaskTool,
  getRunningTasks: () => getRunningTasks,
  getRunningTasksTool: () => getRunningTasksTool,
  hasCancellableTool: () => hasCancellableTool,
  renderTemplate: () => renderTemplate,
  resolveAsyncToolOptions: () => resolveAsyncToolOptions
});
module.exports = __toCommonJS(tool_executor_exports);
var import_mutex = require("@livekit/mutex");
var import_zod = require("zod");
var import_chat_context = require("../llm/chat_context.cjs");
var import_tool_context = require("../llm/tool_context.cjs");
var import_log = require("../log.cjs");
var import_utils = require("../utils.cjs");
const DRAIN_TOOL_TIMEOUT_MS = Number(process.env.LK_DRAIN_PLAYOUT_TIMEOUT_MS) || 5e3;
const UPDATE_TEMPLATE = "The tool `{functionName}` has updated, message: {message}\nThe task is still running, so DON'T make up or give information not included in the message above.";
const DUPLICATE_REJECT = "Same tool `{functionName}` is already running:\n{functionCallsText}\nIf you want to cancel the existing one, call `lk_agents_cancel_task` with call_id.";
const DUPLICATE_CONFIRM = "Same tool `{functionName}` is already running:\n{functionCallsText}\nRe-call with confirm duplicate True to run a duplicate if needed,\nor if you want to cancel the existing one, call `lk_agents_cancel_task` with call_id.";
const REPLY_INSTRUCTIONS_AT_TAIL = "New results arrived from background tool calls (call_ids: {callIds}).\nSummarize the results naturally. Do NOT repeat information you have already told the user.";
const REPLY_INSTRUCTIONS_MAYBE_COVERED = "New results arrived from background tool calls (call_ids: {callIds}).\nYou may have already mentioned them in your most recent replies.\nIf you already told the user everything in these results, reply with an empty response (no text at all).\nOtherwise, summarize only what you have not said yet, with a natural transition.\nNever repeat information you have already told the user.";
function resolveAsyncToolOptions(options) {
  return {
    updateTemplate: (options == null ? void 0 : options.updateTemplate) ?? UPDATE_TEMPLATE,
    duplicateRejectTemplate: (options == null ? void 0 : options.duplicateRejectTemplate) ?? DUPLICATE_REJECT,
    duplicateConfirmTemplate: (options == null ? void 0 : options.duplicateConfirmTemplate) ?? DUPLICATE_CONFIRM,
    replyAtTailTemplate: (options == null ? void 0 : options.replyAtTailTemplate) ?? REPLY_INSTRUCTIONS_AT_TAIL,
    replyMaybeCoveredTemplate: (options == null ? void 0 : options.replyMaybeCoveredTemplate) ?? REPLY_INSTRUCTIONS_MAYBE_COVERED
  };
}
function renderTemplate(template, args) {
  if (typeof template === "function") return template(args);
  return Object.entries(args).reduce(
    (result, [key, value]) => result.replaceAll(`{${key}}`, String(value)),
    template
  );
}
const runningTasks = /* @__PURE__ */ new WeakMap();
const getRunningTasksTool = (0, import_tool_context.tool)({
  name: "lk_agents_get_running_tasks",
  description: "Get the list of running tool calls that are cancellable.",
  execute: async (_, { ctx }) => getRunningTasks(ctx.session).map((call) => call.toJSON(true))
});
const cancelTaskTool = (0, import_tool_context.tool)({
  name: "lk_agents_cancel_task",
  description: "Cancel a running tool call by call_id.",
  parameters: import_zod.z.object({
    call_id: import_zod.z.string()
  }),
  execute: async ({ call_id }, { ctx }) => {
    var _a;
    const task = (_a = runningTasks.get(ctx.session)) == null ? void 0 : _a.get(call_id);
    if (!task) {
      throw new import_tool_context.ToolError(`Task ${call_id} not found`);
    }
    const cancelled = await task.executor.cancel(call_id);
    if (!cancelled) {
      throw new import_tool_context.ToolError(`Task ${call_id} not found or already completed`);
    }
    return `Task ${call_id} cancelled successfully.`;
  }
});
class ToolExecutor {
  runningTasks = /* @__PURE__ */ new Map();
  duplicateLock = new import_mutex.Mutex();
  pendingUpdates = [];
  _replyTask;
  _replyTaskDone = false;
  toolOptions;
  constructor({
    owningActivity,
    asyncToolOptions
  } = {}) {
    this.owningActivity = owningActivity ?? null;
    this.toolOptions = resolveAsyncToolOptions(asyncToolOptions);
  }
  owningActivity;
  get replyTask() {
    return this._replyTask;
  }
  setOwningActivity(activity) {
    this.owningActivity = activity;
  }
  setToolOptions(options) {
    this.toolOptions = resolveAsyncToolOptions(options);
  }
  get hasRunningTasks() {
    return this.runningTasks.size > 0;
  }
  get hasCancellableRunningTasks() {
    return [...this.runningTasks.values()].some((task) => task.allowCancellation);
  }
  async execute({
    tool: tool2,
    runCtx,
    rawArguments,
    abortSignal
  }) {
    const callId = runCtx.functionCall.callId;
    const functionName = runCtx.functionCall.name;
    const args = { ...rawArguments };
    const confirmDuplicate = tool2.onDuplicate === "confirm" ? Boolean(args[import_tool_context.CONFIRM_DUPLICATE_PARAM]) : void 0;
    delete args[import_tool_context.CONFIRM_DUPLICATE_PARAM];
    const unlock = await this.duplicateLock.lock();
    try {
      const duplicateResult = await this.checkDuplicate(functionName, {
        onDuplicate: tool2.onDuplicate,
        confirmDuplicate
      });
      if (duplicateResult !== void 0) return duplicateResult;
      if (this.runningTasks.has(callId)) {
        throw new Error(`Task already running for call_id: ${callId}`);
      }
      const firstUpdateFuture = new import_utils.Future();
      runCtx._attachExecutor(this, firstUpdateFuture);
      const controller = new AbortController();
      const abort = () => {
        queueMicrotask(() => {
          controller.abort();
          if (!firstUpdateFuture.done) {
            firstUpdateFuture.reject(new Error("tool call was aborted"));
          }
        });
      };
      abortSignal == null ? void 0 : abortSignal.addEventListener("abort", abort, { once: true });
      void firstUpdateFuture.await.then(() => {
        if (runCtx.functionCall.extra.__livekit_agents_tool_non_blocking === true) {
          abortSignal == null ? void 0 : abortSignal.removeEventListener("abort", abort);
        }
      }).catch(() => {
      });
      const toolPromiseRef = {};
      const promise = this.runTool({
        tool: tool2,
        runCtx,
        rawArguments: args,
        firstUpdateFuture,
        controller,
        toolPromiseRef
      }).finally(() => {
        var _a;
        this.runningTasks.delete(callId);
        (_a = runningTasks.get(runCtx.session)) == null ? void 0 : _a.delete(callId);
        abortSignal == null ? void 0 : abortSignal.removeEventListener("abort", abort);
        runCtx._detachExecutor();
      });
      const task = {
        ctx: runCtx,
        promise,
        controller,
        firstUpdateFuture,
        executor: this,
        allowCancellation: Boolean(tool2.flags & import_tool_context.ToolFlag.CANCELLABLE),
        toolPromiseRef
      };
      this.runningTasks.set(callId, task);
      let sessionTasks = runningTasks.get(runCtx.session);
      if (!sessionTasks) {
        sessionTasks = /* @__PURE__ */ new Map();
        runningTasks.set(runCtx.session, sessionTasks);
      }
      sessionTasks.set(callId, task);
      return firstUpdateFuture.await;
    } finally {
      unlock();
    }
  }
  async waitForAll() {
    await Promise.allSettled([...this.runningTasks.values()].map((task) => task.promise));
    if (this._replyTask) await this._replyTask;
  }
  async cancel(callId) {
    const task = this.runningTasks.get(callId);
    if (!task) return false;
    if (!task.allowCancellation) {
      throw new import_tool_context.ToolError(`Tool call ${callId} is not cancellable`);
    }
    if (!task.ctx.speechHandle.allowInterruptions) {
      throw new import_tool_context.ToolError(
        `Tool call ${callId} is not cancellable because interruptions are disallowed`
      );
    }
    this.forceCancelTask(task);
    return true;
  }
  /**
   * Abort + detach a running task unconditionally. `cancel()` gates on cancellability and
   * interruptibility (LLM/user-initiated path); teardown via `drain()` must skip those guards —
   * the activity is going away, and `cancel()`'s `allowInterruptions` throw would otherwise abort
   * the drain loop and strand the remaining tasks.
   */
  forceCancelTask(task) {
    var _a;
    const { callId } = task.ctx.functionCall;
    task.controller.abort();
    if (!task.firstUpdateFuture.done) {
      task.firstUpdateFuture.resolve(void 0);
    }
    this.runningTasks.delete(callId);
    (_a = runningTasks.get(task.ctx.session)) == null ? void 0 : _a.delete(callId);
    task.ctx._detachExecutor();
    void task.promise.catch(() => void 0);
    this.errorIfCancelledToolKeepsRunning(task.ctx.functionCall, task.toolPromiseRef.promise);
  }
  /**
   * Fire-and-forget watcher: a cancelled tool whose `execute()` hasn't settled by the
   * deadline is ignoring its abort signal. Surface it so the dev can make execute abortable —
   * abandoning the promise alone leaves the work running invisibly.
   */
  errorIfCancelledToolKeepsRunning(call, rawPromise) {
    var _a;
    if (!rawPromise) return;
    let settled = false;
    void rawPromise.then(() => {
      settled = true;
    });
    const timer = setTimeout(() => {
      if (settled) return;
      (0, import_log.log)().error(
        { tool: call.name, callId: call.callId, timeoutMs: DRAIN_TOOL_TIMEOUT_MS },
        `tool ${call.name} was cancelled but its execute() is still running after the deadline; it likely does not honor the abort signal. Observe the provided abortSignal in execute() so cancellation actually stops the work.`
      );
    }, DRAIN_TOOL_TIMEOUT_MS);
    (_a = timer.unref) == null ? void 0 : _a.call(timer);
    void rawPromise.finally(() => clearTimeout(timer));
  }
  async drain() {
    var _a;
    const tasks = [...this.runningTasks.values()];
    for (const task of tasks) {
      if (task.allowCancellation) {
        this.forceCancelTask(task);
      }
    }
    const inflight = tasks.filter((task) => !task.allowCancellation).map((task) => ({ name: task.ctx.functionCall.name, promise: task.toolPromiseRef.promise })).filter((t) => t.promise !== void 0);
    if (inflight.length === 0) return;
    const settled = /* @__PURE__ */ new Set();
    inflight.forEach((t, i) => void t.promise.then(() => settled.add(i)));
    const timer = setTimeout(() => {
      const slow = inflight.filter((_, i) => !settled.has(i)).map((t) => t.name);
      if (slow.length > 0) {
        (0, import_log.log)().warn(
          { tools: slow, timeoutMs: DRAIN_TOOL_TIMEOUT_MS },
          "non-cancellable tool(s) still running after the drain deadline; awaiting their completion so the deferred result is not dropped. Keep non-cancellable tool work short, or mark the tool cancellable if it should stop on handoff."
        );
      }
    }, DRAIN_TOOL_TIMEOUT_MS);
    (_a = timer.unref) == null ? void 0 : _a.call(timer);
    await Promise.allSettled(inflight.map((t) => t.promise));
    clearTimeout(timer);
  }
  async aclose() {
    var _a;
    this.pendingUpdates = [];
    const tasks = [...this.runningTasks.values()];
    for (const task of tasks) {
      task.controller.abort();
      if (!task.firstUpdateFuture.done) {
        task.firstUpdateFuture.resolve(void 0);
      }
      (_a = runningTasks.get(task.ctx.session)) == null ? void 0 : _a.delete(task.ctx.functionCall.callId);
      task.ctx._detachExecutor();
      void task.promise.catch(() => void 0);
    }
    this.runningTasks.clear();
  }
  async enqueueReply(ctx, items) {
    var _a, _b;
    const target = ((_a = this.owningActivity) == null ? void 0 : _a.agent) ?? getCurrentAgent(ctx.session);
    const chatCtx = target.chatCtx.copy();
    chatCtx.insert(items);
    await target.updateChatCtx(chatCtx);
    ctx.session.history.insert(items);
    this.pendingUpdates.push({ ctx, items, target });
    if (this._replyTask === void 0 || this._replyTaskDone) {
      this._replyTaskDone = false;
      this._replyTask = this.deliverReply(ctx.session).catch((error) => {
        (0, import_log.log)().warn(
          { error },
          "deliverReply failed; async tool result may not trigger a follow-up reply"
        );
      });
      const runState = ctx.session._globalRunState;
      (_b = runState == null ? void 0 : runState._watchHandle) == null ? void 0 : _b.call(runState, this._replyTask);
    }
  }
  async runTool({
    tool: tool2,
    runCtx,
    rawArguments,
    firstUpdateFuture,
    controller,
    toolPromiseRef
  }) {
    let output;
    let exception;
    const toolPromise = (async () => tool2.execute(rawArguments, {
      ctx: runCtx,
      toolCallId: runCtx.functionCall.callId,
      abortSignal: controller.signal
    }))();
    toolPromiseRef.promise = toolPromise.then(
      () => void 0,
      () => void 0
    );
    try {
      output = await toolPromise;
    } catch (error) {
      exception = error;
    }
    if (controller.signal.aborted) {
      if (!firstUpdateFuture.done) {
        firstUpdateFuture.resolve(void 0);
      }
      return;
    }
    if (!firstUpdateFuture.done) {
      if (exception !== void 0) {
        firstUpdateFuture.reject(
          exception instanceof Error ? exception : new Error(String(exception))
        );
      } else {
        firstUpdateFuture.resolve(output);
      }
      return;
    }
    if (exception !== void 0) {
      (0, import_log.log)().error(
        {
          function: runCtx.functionCall.name,
          callId: runCtx.functionCall.callId,
          error: exception
        },
        "exception occurred while executing tool after a progress update"
      );
      return;
    }
    if (output === void 0 || output === null) {
      return;
    }
    if (!this.runningTasks.has(runCtx.functionCall.callId)) {
      return;
    }
    const pair = runCtx._makeUpdatePair(output, "_final");
    runCtx._recordUpdatePair(pair);
    await this.enqueueReply(runCtx, pair);
  }
  async checkDuplicate(functionName, {
    onDuplicate,
    confirmDuplicate
  }) {
    const runningFunctionCalls = [...this.runningTasks.values()].map((task) => task.ctx.functionCall).filter((call) => call.name === functionName);
    if (runningFunctionCalls.length === 0) return void 0;
    if (onDuplicate === "allow") {
      return void 0;
    }
    if (onDuplicate === "replace") {
      const nonCancellable = runningFunctionCalls.filter(
        (call) => {
          var _a;
          return !((_a = this.runningTasks.get(call.callId)) == null ? void 0 : _a.allowCancellation);
        }
      );
      if (nonCancellable.length > 0) {
        throw new import_tool_context.ToolError(
          `cannot replace duplicate call of \`${functionName}\`: running call is not cancellable`
        );
      }
      await Promise.all(runningFunctionCalls.map((call) => this.cancel(call.callId)));
      return void 0;
    }
    const functionCallsJson = runningFunctionCalls.map((call) => JSON.stringify(call.toJSON(true)));
    const args = {
      functionName,
      functionCallsJson,
      functionCallsText: functionCallsJson.join("\n")
    };
    if (onDuplicate === "reject") {
      return renderTemplate(this.toolOptions.duplicateRejectTemplate, args);
    }
    if (onDuplicate === "confirm" && !confirmDuplicate) {
      return renderTemplate(this.toolOptions.duplicateConfirmTemplate, args);
    }
    return void 0;
  }
  async deliverReply(session) {
    var _a;
    try {
      if (this.owningActivity) {
        await this.owningActivity.waitForIdle();
      } else if ("waitForIdle" in session && typeof session.waitForIdle === "function") {
        await session.waitForIdle();
      }
      const updates = [...this.pendingUpdates];
      this.pendingUpdates = [];
      const pendingItems = updates.flatMap((update) => update.items);
      if (pendingItems.length === 0) return;
      const targetAgent = ((_a = this.owningActivity) == null ? void 0 : _a.agent) ?? getCurrentAgent(session);
      const itemsToInsert = updates.filter((update) => update.target !== targetAgent).flatMap((update) => update.items);
      let chatCtx;
      if (itemsToInsert.length > 0) {
        chatCtx = targetAgent.chatCtx.copy();
        chatCtx.insert(itemsToInsert);
      }
      const lastItem = pendingItems[pendingItems.length - 1];
      const targetItems = targetAgent.chatCtx.items;
      const atTail = targetItems.length > 0 && targetItems[targetItems.length - 1].id === lastItem.id;
      const callIds = pendingItems.filter((item) => item.type === "function_call_output").map((item) => item.callId);
      const instructions = renderTemplate(
        atTail ? this.toolOptions.replyAtTailTemplate : this.toolOptions.replyMaybeCoveredTemplate,
        { callIds }
      );
      session.generateReply({
        instructions,
        toolChoice: "none",
        chatCtx
      });
    } finally {
      this._replyTaskDone = true;
    }
  }
}
function hasCancellableTool(tools) {
  for (const entry of tools) {
    if ((0, import_tool_context.isFunctionTool)(entry) && entry.flags & import_tool_context.ToolFlag.CANCELLABLE) return true;
    if (entry instanceof import_tool_context.Toolset && hasCancellableTool(entry.tools)) return true;
  }
  return false;
}
function buildExecutorMap({
  toolsets,
  defaultExecutor
}) {
  const mapping = /* @__PURE__ */ new Map();
  const walk = (toolset, current) => {
    const scopedExecutor = "_executor" in toolset && toolset._executor instanceof ToolExecutor ? toolset._executor : current;
    for (const child of toolset.tools) {
      if ((0, import_tool_context.isFunctionTool)(child)) {
        mapping.set(child.name, scopedExecutor);
      } else if (child instanceof import_tool_context.Toolset) {
        walk(child, scopedExecutor);
      }
    }
  };
  for (const toolset of toolsets) {
    walk(toolset, defaultExecutor);
  }
  return mapping;
}
function getRunningTasks(session) {
  var _a;
  return [...((_a = runningTasks.get(session)) == null ? void 0 : _a.values()) ?? []].filter((task) => task.allowCancellation).map((task) => import_chat_context.FunctionCall.create({ ...task.ctx.functionCall }));
}
function getCurrentAgent(session) {
  return session.currentAgent;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  DUPLICATE_CONFIRM,
  DUPLICATE_REJECT,
  REPLY_INSTRUCTIONS_AT_TAIL,
  REPLY_INSTRUCTIONS_MAYBE_COVERED,
  ToolExecutor,
  UPDATE_TEMPLATE,
  buildExecutorMap,
  cancelTaskTool,
  getRunningTasks,
  getRunningTasksTool,
  hasCancellableTool,
  renderTemplate,
  resolveAsyncToolOptions
});
//# sourceMappingURL=tool_executor.cjs.map