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 turn.ex
Raw

lib/raxol/agent/turn.ex

defmodule Raxol.Agent.Turn do
@moduledoc """
Drives one self-improving agent turn for an agent module.
This is where the memory, skills, and self-improvement pieces meet. `run/3`
assembles the tool context from the agent module's callbacks
(`memory_provider`/`memory_providers`, `skills_provider`, `self_improve`), runs
the framework ReAct loop (`Raxol.Agent.Stream.react/2`), records the turn into a
`Raxol.Agent.Conversation.Log`, and then fires the after-turn side effects:
background curation (`Raxol.Agent.SelfImprove`), a user-model refresh, and a
session-search index update.
The agent module declares *which* providers it wants; the caller supplies the
*running instances* through opts (store server names, the UserModel and
SessionSearch servers).
## Options
* `:backend` (required) -- the `AIBackend` for the turn
* `:backend_opts` -- extra opts passed to the backend
* `:log` (required) -- the `Conversation.Log` server
* `:conversation_id` (required)
* `:response_id` -- tags recorded items with the turn id
* `:agent_id` -- partitions memory
* `:user_id` -- keys the user model and its context injection
* `:memory_opts` / `:skills_opts` -- merged into the provider contexts
(notably `:server` when a store runs under a non-default name)
* `:user_model` -- the `UserModel` server; enables user-context injection and
a background refresh after the turn
* `:session_search` -- the `SessionSearch` server; enables the
`session_search` action and indexes the turn's items
* `:max_iterations`
"""
alias Raxol.Agent.Conversation.Recorder
alias Raxol.Agent.Memory
alias Raxol.Agent.Memory.SessionSearch
alias Raxol.Agent.SelfImprove
alias Raxol.Agent.Skills
alias Raxol.Agent.Stream
alias Raxol.Agent.UserModel
@spec run(module(), String.t() | [map()], keyword()) :: {:ok, [map()]}
def run(agent_module, prompt, opts) do
log = Keyword.fetch!(opts, :log)
conversation_id = Keyword.fetch!(opts, :conversation_id)
context = build_context(agent_module, opts)
react_opts =
[
backend: Keyword.fetch!(opts, :backend),
backend_opts: Keyword.get(opts, :backend_opts, []),
actions: actions(agent_module, opts),
context: context
]
|> maybe_put(:max_iterations, Keyword.get(opts, :max_iterations))
{:ok, items} =
prompt
|> Stream.react(react_opts)
|> then(
&Recorder.record_stream(log, conversation_id, &1,
response_id: Keyword.get(opts, :response_id)
)
)
after_turn(agent_module, items, context, opts)
{:ok, items}
end
@doc """
Assemble the tool context (`:memory`, `:skills`, `:user_context`,
`:session_search`) from the agent module's callbacks and the running instances
in `opts`. Keys are present only when their capability is configured.
"""
@spec build_context(module(), keyword()) :: map()
def build_context(agent_module, opts) do
agent_id = Keyword.get(opts, :agent_id)
%{}
|> maybe_put(:memory, memory_context(agent_module, agent_id, opts))
|> maybe_put(:skills, skills_context(agent_module, opts))
|> maybe_put(:user_context, user_context(opts))
|> maybe_put(:session_search, session_search_context(opts))
end
@doc """
Run the after-turn side effects for a recorded turn: trigger curation, refresh
the user model in the background, and feed the session-search index.
"""
@spec after_turn(module(), [map()], map(), keyword()) :: :ok
def after_turn(agent_module, items, context, opts) do
writers = %{memory: Map.get(context, :memory), skills: Map.get(context, :skills)}
SelfImprove.after_turn(items, writers, call(agent_module, :self_improve, nil))
maybe_refresh_user_model(items, opts)
maybe_index_session(items, opts)
:ok
end
# -- after-turn effects -----------------------------------------------------
defp maybe_refresh_user_model(items, opts) do
with server when not is_nil(server) <- Keyword.get(opts, :user_model),
user_id when not is_nil(user_id) <- Keyword.get(opts, :user_id) do
UserModel.refresh_async(server, user_id, items)
end
end
defp maybe_index_session(items, opts) do
case Keyword.get(opts, :session_search) do
nil -> :ok
server -> SessionSearch.index(server, items)
end
end
# -- context assembly -------------------------------------------------------
defp memory_context(agent_module, agent_id, opts) do
extra = Keyword.get(opts, :memory_opts, [])
providers = call(agent_module, :memory_providers, [])
case Memory.stack_context(providers, agent_id, extra) do
nil -> Memory.provider_context(call(agent_module, :memory_provider, nil), agent_id, extra)
stack -> stack
end
end
defp skills_context(agent_module, opts) do
case call(agent_module, :skills_provider, nil) do
nil -> nil
provider -> Skills.provider_context(provider, Keyword.get(opts, :skills_opts, []))
end
end
defp user_context(opts) do
with server when not is_nil(server) <- Keyword.get(opts, :user_model),
user_id when not is_nil(user_id) <- Keyword.get(opts, :user_id) do
{UserModel, [server: server, user_id: user_id]}
else
_ -> nil
end
end
defp session_search_context(opts) do
case Keyword.get(opts, :session_search) do
nil -> nil
server -> {SessionSearch, [server: server]}
end
end
defp actions(agent_module, opts) do
base = call(agent_module, :available_actions, [])
if Keyword.get(opts, :session_search),
do: base ++ [Raxol.Agent.Actions.SessionSearch],
else: base
end
defp call(mod, fun, default) do
if function_exported?(mod, fun, 0), do: apply(mod, fun, []), else: default
end
defp maybe_put(collection, _key, nil), do: collection
defp maybe_put(map, key, value) when is_map(map), do: Map.put(map, key, value)
defp maybe_put(opts, key, value) when is_list(opts), do: Keyword.put(opts, key, value)
end