lib/openagents/memory/semantic_worker.ex

main at 58e6347eeb72 · 1 KB

defmodule OpenAgents.Memory.SemanticWorker do
  @moduledoc "Bounded asynchronous semantic outbox consumer; accepted turns never wait for it."
  use GenServer

  require Logger

  alias OpenAgents.Memory.SemanticIndex

  def start_link(options) do
    GenServer.start_link(__MODULE__, options, name: Keyword.get(options, :name, __MODULE__))
  end

  @doc false
  def drain(worker \\ __MODULE__), do: GenServer.call(worker, :drain, 120_000)

  @impl true
  def init(options) do
    config = Application.fetch_env!(:openagents, :semantic_index)
    processor = Keyword.get(options, :processor, &process/1)

    if Keyword.fetch!(config, :enabled) do
      _manifest = SemanticIndex.ensure_manifest!(Map.new(config))

      if Keyword.get(options, :autostart, true), do: schedule(0)
    end

    {:ok, %{config: config, processor: processor}}
  end

  @impl true
  def handle_call(:drain, _from, state) do
    {result, state} = run_drain(state)
    {:reply, result, state}
  end

  @impl true
  def handle_info(:drain, state) do
    {_result, state} = run_drain(state)
    schedule(Keyword.fetch!(state.config, :poll_interval_ms))
    {:noreply, state}
  end

  defp schedule(delay), do: Process.send_after(self(), :drain, delay)

  defp run_drain(state) do
    result =
      try do
        {:ok, state.processor.(state.config)}
      rescue
        _exception -> {:error, :semantic_drain_failed}
      catch
        _kind, _reason -> {:error, :semantic_drain_failed}
      end

    if result == {:error, :semantic_drain_failed} do
      Logger.warning("semantic_drain_failed code=worker_exception")
    end

    {result, state}
  end

  defp process(config) do
    SemanticIndex.process_all(
      Keyword.fetch!(config, :provider),
      Keyword.fetch!(config, :batch_size),
      provider_timeout_ms: Keyword.get(config, :provider_timeout_ms, 15_000),
      lease_ms: Keyword.get(config, :lease_ms, 30_000)
    )
  end
end