Packages
agent_engine
0.2.0
Minimal host-facing agent runtime: config loading, session state, phased cues, budget hints, trend hints, collaboration state, and a host-owned turn loop.
Current section
Files
Jump to
Current section
Files
agent_engine
README.md
README.md
# 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`:
```elixir
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.Agent`
- `AgentEngine.Session`
- `AgentEngine.BudgetHints`
- `AgentEngine.TrendHints`
- `AgentEngine.Collaboration`
- `AgentEngine.Turn`
- `AgentEngine.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**:
```elixir
{:ok, content, runtime, signals}
```
- `content` — the assistant's final text (a binary), or `nil` when the turn is
suspended awaiting tool results.
- `runtime` — an `AgentEngine.Turn.Runtime` that **embeds** the advanced
`AgentEngine.Session` (`.session`) and carries the working transcript, so a
suspended turn can be resumed by passing it back into `turn/3`. It does not
extend `AgentEngine.Session`'s public fields.
- `signals` — produced by the optional `:extract_signals` callback; `[]` when no
extractor is supplied.
```elixir
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`:
```elixir
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:
```elixir
{:error, {:tool_results_required, ["call_1"]}}
```
LLM errors pass through unchanged: `{:error, term()}`.
## License
MIT — see [LICENSE](https://github.com/fosferon/agent_engine/blob/main/LICENSE).