Packages

AI agent framework for Elixir built on OTP. TEA-based agents with crash isolation, inter-agent messaging, team supervision, and real SSE streaming to Anthropic, OpenAI, Ollama, and more.

Current section

Files

Jump to
raxol_agent lib raxol agent cache ets.ex
Raw

lib/raxol/agent/cache/ets.ex

defmodule Raxol.Agent.Cache.Ets do
@moduledoc """
ETS-backed `Raxol.Agent.Cache` adapter.
Stores entries in a named ETS table created lazily on the first
call. The table is `:public` and `:set` keyed on the user's term;
values are wrapped with their absolute expiry timestamp so
`get/2` can decide stale-vs-fresh with a single comparison.
## Config
%{table: :my_agent_cache}
Defaults to `:raxol_agent_cache` when the `:table` key is absent.
Two agents sharing the same table name share its entries; isolate
per-agent by passing distinct table names.
## Lifetime
The table lives for the duration of the BEAM. It is not supervised
and does not survive `Application.stop/1`. Use
`Raxol.Agent.Cache.Postgrex` when cache entries must outlive the
BEAM or be shared across nodes.
## Lazy expiry
`get/2` checks the wrapped expiry against `DateTime.utc_now/0`. A
stale entry returns `:miss` and is removed from the table in the
same call. No background sweeper; expired-but-unread entries
occupy table memory until either `get/2` checks them or
`flush/1` clears the table.
## Not for very-high-throughput shared writes
ETS handles concurrent reads + writes fine, but two writers to the
same key racing through `put/4` will both succeed; the second
silently overwrites the first. The Saver-style append-only contract
does not apply to caches; last-write-wins is the intended cache
semantics.
"""
@behaviour Raxol.Agent.Cache
@default_table :raxol_agent_cache
@doc "Ensure the configured ETS table exists. Idempotent."
@spec ensure_table(map()) :: atom()
def ensure_table(config) do
table = Map.get(config, :table, @default_table)
case :ets.whereis(table) do
:undefined ->
_ =
:ets.new(table, [
:set,
:public,
:named_table,
read_concurrency: true,
write_concurrency: true
])
:ok
_ref ->
:ok
end
table
end
@impl true
def get(config, key) do
table = ensure_table(config)
case :ets.lookup(table, key) do
[{^key, value, :no_expiry}] ->
{:ok, value}
[{^key, value, %DateTime{} = expiry}] ->
if DateTime.compare(expiry, DateTime.utc_now()) == :gt do
{:ok, value}
else
_ = :ets.delete(table, key)
:miss
end
[] ->
:miss
end
end
@impl true
def put(config, key, value, 0) do
table = ensure_table(config)
_ = :ets.insert(table, {key, value, :no_expiry})
:ok
end
def put(config, key, value, ttl_ms) when ttl_ms > 0 do
table = ensure_table(config)
expiry = DateTime.add(DateTime.utc_now(), ttl_ms, :millisecond)
_ = :ets.insert(table, {key, value, expiry})
:ok
end
@impl true
def delete(config, key) do
table = ensure_table(config)
_ = :ets.delete(table, key)
:ok
end
@impl true
def flush(config) do
table = ensure_table(config)
_ = :ets.delete_all_objects(table)
:ok
end
end