AgentEngine
Minimal host-facing agent runtime for Elixir applications.
AgentEngine intentionally stays narrow. It provides:
- config-driven agent loading
- functional session state
- budget-awareness hints
- quality-trend hints
- collaboration state for multi-agent work
- a host-owned turn loop for single-agent conversations
It intentionally does not ship richer staged guidance systems, provider routing, or tool execution. Host applications own those and layer them on top.
Installation
Add agent_engine to your list of dependencies in mix.exs:
def deps do
[
{:agent_engine, "~> 0.2.0"}
]
end
agent_engine shares its core dependencies (comm_bus, llm_core,
llm_toolkit) with companion libraries. If your application also declares any
of them directly, add override: true on those entries in your app so Mix
resolves them to a single version.
Core Modules
AgentEngine.AgentAgentEngine.SessionAgentEngine.BudgetHintsAgentEngine.TrendHintsAgentEngine.CollaborationAgentEngine.TurnAgentEngine.Transcript
Running a turn
AgentEngine.Turn.turn/3 advances a session by one exchange: it appends the
user message (or host-resolved tool results), calls a host-supplied LLM function
once, and records the assistant reply. AgentEngine never executes host tools and
performs no provider routing — the :llm function is the host's seam for both.
The success shape is frozen:
{:ok, content, runtime, signals}
content— the assistant's final text (a binary), ornilwhen the turn is suspended awaiting tool results.runtime— anAgentEngine.Turn.Runtimethat embeds the advancedAgentEngine.Session(.session) and carries the working transcript, so a suspended turn can be resumed by passing it back intoturn/3. It does not extendAgentEngine.Session's public fields.signals— produced by the optional:extract_signalscallback;[]when no extractor is supplied.
alias AgentEngine.{Session, Turn}
alias LlmCore.LLM.Response
# Host-supplied LLM function — forward to llm_core, a CLI, or a mock.
llm = fn _messages, _opts ->
{:ok, %Response{content: "Hello!", tool_calls: nil}}
end
session = Session.new(%{"slug" => "ops"}, [])
{:ok, content, runtime, signals} = Turn.turn(session, "Hi", llm: llm)
# content => "Hello!"
# signals => []
# runtime => %AgentEngine.Turn.Runtime{session: session, transcript: ...}
Tool-call round trip
When the LLM requests tool calls, the turn suspends. The host executes the calls
and resumes by passing the runtime back with :tool_results:
alias LlmToolkit.Tool.{Call, Result}
llm = fn _messages, _opts ->
{:ok,
%Response{
content: nil,
tool_calls: [%Call{id: "call_1", name: "read_file", arguments: %{"path" => "a.txt"}}]
}}
end
{:ok, nil, runtime, []} = Turn.turn(session, "Read a.txt", llm: llm)
# Host executes the tool and submits results to complete the turn.
llm2 = fn _messages, _opts ->
{:ok, %Response{content: "The file contains: hello", tool_calls: nil}}
end
{:ok, "The file contains: hello", runtime, []} =
Turn.turn(runtime, nil,
llm: llm2,
tool_results: [%Result{tool_call_id: "call_1", name: "read_file", content: "hello"}]
)
Submitting a new user message while tool calls are still pending is refused:
{:error, {:tool_results_required, ["call_1"]}}
LLM errors pass through unchanged: {:error, term()}.
License
MIT — see LICENSE.