"use strict";
var import_vitest = require("vitest");
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_utils = require("../utils.cjs");
var import_run_context = require("./run_context.cjs");
var import_speech_handle = require("./speech_handle.cjs");
var import_tool_executor = require("./tool_executor.cjs");
(0, import_vitest.describe)("ToolExecutor", () => {
  function buildRunContext(name = "slow_lookup", callId = `call_${name}`, speechOptions = {}) {
    const functionCall = import_chat_context.FunctionCall.create({
      callId,
      name,
      args: '{"query":"flights"}'
    });
    const history = new import_chat_context.ChatContext();
    const agent = {
      chatCtx: import_chat_context.ChatContext.empty(),
      async updateChatCtx(chatCtx) {
        this.chatCtx = chatCtx;
      }
    };
    const session = {
      userData: {},
      history,
      currentAgent: agent,
      _globalRunState: void 0,
      async waitForIdle() {
        return { agent };
      },
      generateReply: () => ({ id: "speech_reply", addDoneCallback: () => {
      } })
    };
    const speechHandle = import_speech_handle.SpeechHandle.create(speechOptions);
    return {
      runCtx: new import_run_context.RunContext(session, speechHandle, functionCall),
      history,
      agent
    };
  }
  (0, import_vitest.it)("preserves blocking tool return semantics when no update is sent", async () => {
    const executor = new import_tool_executor.ToolExecutor();
    const { runCtx } = buildRunContext("blocking_lookup");
    const lookup = (0, import_tool_context.tool)({
      name: "blocking_lookup",
      description: "Blocking lookup",
      parameters: import_zod.z.object({ query: import_zod.z.string() }),
      execute: async ({ query }) => `result:${query}`
    });
    const result = await executor.execute({
      tool: lookup,
      runCtx,
      rawArguments: { query: "flights" }
    });
    (0, import_vitest.expect)(result).toBe("result:flights");
  });
  (0, import_vitest.it)("returns on first update and later enqueues final return", async () => {
    const executor = new import_tool_executor.ToolExecutor();
    const { runCtx, history, agent } = buildRunContext("async_lookup");
    const releaseFinal = new import_utils.Future();
    const lookup = (0, import_tool_context.tool)({
      name: "async_lookup",
      description: "Async lookup",
      parameters: import_zod.z.object({ query: import_zod.z.string() }),
      execute: async ({ query }, { ctx }) => {
        await ctx.update(`started:${query}`);
        await releaseFinal.await;
        return `final:${query}`;
      }
    });
    const resultPromise = executor.execute({
      tool: lookup,
      runCtx,
      rawArguments: { query: "flights" }
    });
    await (0, import_vitest.expect)(resultPromise).resolves.toContain("started:flights");
    (0, import_vitest.expect)(runCtx.functionCall.extra.__livekit_agents_tool_non_blocking).toBe(true);
    (0, import_vitest.expect)(history.items).toHaveLength(0);
    releaseFinal.resolve();
    await executor.waitForAll();
    (0, import_vitest.expect)(history.items.some((item) => item.type === "function_call_output")).toBe(true);
    (0, import_vitest.expect)(agent.chatCtx.items.some((item) => item.type === "function_call_output")).toBe(true);
  });
  (0, import_vitest.it)("rejects duplicate calls when onDuplicate is reject", async () => {
    const executor = new import_tool_executor.ToolExecutor();
    const first = buildRunContext("dedupe_lookup");
    const second = buildRunContext("dedupe_lookup");
    const neverFinish = new import_utils.Future();
    const lookup = (0, import_tool_context.tool)({
      name: "dedupe_lookup",
      description: "Dedupe lookup",
      flags: import_tool_context.ToolFlag.CANCELLABLE,
      onDuplicate: "reject",
      execute: async (_, { ctx }) => {
        await ctx.update("started");
        await neverFinish.await;
        return "done";
      }
    });
    await executor.execute({ tool: lookup, runCtx: first.runCtx, rawArguments: {} });
    const duplicate = await executor.execute({
      tool: lookup,
      runCtx: second.runCtx,
      rawArguments: {}
    });
    (0, import_vitest.expect)(String(duplicate)).toContain("Same tool `dedupe_lookup` is already running");
    neverFinish.resolve();
    await executor.waitForAll();
  });
  (0, import_vitest.it)("rejects concurrent duplicate calls when onDuplicate is reject", async () => {
    const executor = new import_tool_executor.ToolExecutor();
    const first = buildRunContext("concurrent_dedupe", "concurrent_dedupe_1");
    const second = buildRunContext("concurrent_dedupe", "concurrent_dedupe_2");
    const neverFinish = new import_utils.Future();
    const lookup = (0, import_tool_context.tool)({
      name: "concurrent_dedupe",
      description: "Concurrent dedupe lookup",
      flags: import_tool_context.ToolFlag.CANCELLABLE,
      onDuplicate: "reject",
      execute: async (_, { ctx }) => {
        await ctx.update("started");
        await neverFinish.await;
        return "done";
      }
    });
    const outputs = await Promise.all([
      executor.execute({ tool: lookup, runCtx: first.runCtx, rawArguments: {} }),
      executor.execute({ tool: lookup, runCtx: second.runCtx, rawArguments: {} })
    ]);
    (0, import_vitest.expect)(outputs.filter((output) => String(output).includes("started"))).toHaveLength(1);
    (0, import_vitest.expect)(
      outputs.filter(
        (output) => String(output).includes("Same tool `concurrent_dedupe` is already running")
      )
    ).toHaveLength(1);
    neverFinish.resolve();
    await executor.waitForAll();
  });
  (0, import_vitest.it)("returns from cancel even when a cancellable tool ignores abortSignal", async () => {
    const executor = new import_tool_executor.ToolExecutor();
    const { runCtx } = buildRunContext("non_cooperative_cancel");
    const neverFinish = new import_utils.Future();
    const lookup = (0, import_tool_context.tool)({
      name: "non_cooperative_cancel",
      description: "Non-cooperative cancellable lookup",
      flags: import_tool_context.ToolFlag.CANCELLABLE,
      execute: async (_, { ctx }) => {
        await ctx.update("started");
        await neverFinish.await;
        return "done";
      }
    });
    await executor.execute({ tool: lookup, runCtx, rawArguments: {} });
    const cancelled = await Promise.race([
      executor.cancel(runCtx.functionCall.callId).then(() => "cancelled"),
      new Promise((resolve) => setTimeout(() => resolve("hung"), 25))
    ]);
    (0, import_vitest.expect)(cancelled).toBe("cancelled");
    (0, import_vitest.expect)((0, import_tool_executor.getRunningTasks)(runCtx.session)).toHaveLength(0);
  });
  function abortableTool(name, started, stopped) {
    return (0, import_tool_context.tool)({
      name,
      description: "Abortable cancellable lookup",
      flags: import_tool_context.ToolFlag.CANCELLABLE,
      execute: async (_, { ctx, abortSignal }) => {
        await ctx.update("started");
        started.resolve();
        await new Promise((resolve) => {
          if (abortSignal.aborted) return resolve();
          abortSignal.addEventListener("abort", () => resolve(), { once: true });
        });
        stopped.resolve();
        return "done";
      }
    });
  }
  (0, import_vitest.it)("explicit cancel aborts the tool so an abortable execute() actually stops", async () => {
    const executor = new import_tool_executor.ToolExecutor();
    const { runCtx } = buildRunContext("abortable_cancel");
    const started = new import_utils.Future();
    const stopped = new import_utils.Future();
    await executor.execute({
      tool: abortableTool("abortable_cancel", started, stopped),
      runCtx,
      rawArguments: {}
    });
    await started.await;
    (0, import_vitest.expect)(stopped.done).toBe(false);
    await executor.cancel(runCtx.functionCall.callId);
    await Promise.race([
      stopped.await,
      new Promise((_, reject) => setTimeout(() => reject(new Error("tool never stopped")), 1e3))
    ]);
    (0, import_vitest.expect)(stopped.done).toBe(true);
  });
  (0, import_vitest.it)("drain (handoff) aborts cancellable tools so an abortable execute() stops", async () => {
    const executor = new import_tool_executor.ToolExecutor();
    const { runCtx } = buildRunContext("abortable_drain");
    const started = new import_utils.Future();
    const stopped = new import_utils.Future();
    await executor.execute({
      tool: abortableTool("abortable_drain", started, stopped),
      runCtx,
      rawArguments: {}
    });
    await started.await;
    await executor.drain();
    await Promise.race([
      stopped.await,
      new Promise((_, reject) => setTimeout(() => reject(new Error("tool never stopped")), 1e3))
    ]);
    (0, import_vitest.expect)(stopped.done).toBe(true);
  });
  (0, import_vitest.it)("drain force-cancels a cancellable tool whose speech disallows interruptions (no throw)", async () => {
    const executor = new import_tool_executor.ToolExecutor();
    const { runCtx } = buildRunContext("noninterrupt_drain", "call_noninterrupt_drain", {
      allowInterruptions: false
    });
    const started = new import_utils.Future();
    const stopped = new import_utils.Future();
    await executor.execute({
      tool: abortableTool("noninterrupt_drain", started, stopped),
      runCtx,
      rawArguments: {}
    });
    await started.await;
    (0, import_vitest.expect)(runCtx.speechHandle.allowInterruptions).toBe(false);
    await (0, import_vitest.expect)(executor.drain()).resolves.toBeUndefined();
    await Promise.race([
      stopped.await,
      new Promise((_, reject) => setTimeout(() => reject(new Error("tool never stopped")), 1e3))
    ]);
    (0, import_vitest.expect)(stopped.done).toBe(true);
    (0, import_vitest.expect)((0, import_tool_executor.getRunningTasks)(runCtx.session)).toHaveLength(0);
  });
  (0, import_vitest.it)("keeps running task visibility scoped to one session", async () => {
    const executor = new import_tool_executor.ToolExecutor();
    const first = buildRunContext("session_scoped_lookup");
    const second = buildRunContext("session_scoped_lookup");
    const neverFinish = new import_utils.Future();
    const lookup = (0, import_tool_context.tool)({
      name: "session_scoped_lookup",
      description: "Session scoped lookup",
      flags: import_tool_context.ToolFlag.CANCELLABLE,
      execute: async (_, { ctx }) => {
        await ctx.update("started");
        await neverFinish.await;
        return "done";
      }
    });
    await executor.execute({ tool: lookup, runCtx: first.runCtx, rawArguments: {} });
    (0, import_vitest.expect)((0, import_tool_executor.getRunningTasks)(first.runCtx.session)).toHaveLength(1);
    (0, import_vitest.expect)((0, import_tool_executor.getRunningTasks)(second.runCtx.session)).toHaveLength(0);
    neverFinish.resolve();
    await executor.waitForAll();
  });
});
//# sourceMappingURL=tool_executor.test.cjs.map