# Legion
> [!WARNING]
> The project is in early stages of development. Expect breaking changes in future releases.
<!-- MDOC -->
Legion is an Elixir-native framework for building AI agents. Unlike traditional function-calling approaches, Legion agents generate and execute actual Elixir code, giving them the full power of the language while staying safely sandboxed.
## Installation
Add `legion` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:legion, "~> 0.1"}
]
end
```
Configure your LLM API key (see [req_llm configuration](https://hexdocs.pm/req_llm/ReqLLM.html#module-configuration) for all options):
```elixir
# config/runtime.exs
config :req_llm, openai_api_key: System.get_env("OPENAI_API_KEY")
```
## Quick Start
### 1. Define your tools
Tools are regular Elixir modules that expose functions to your agents:
```elixir
defmodule MyApp.Tools.ScraperTool do
use Legion.Tool
@doc "Fetches recent posts from HackerNews"
def fetch_posts do
Req.get!("https://hn.algolia.com/api/v1/search_by_date").body["hits"]
end
end
defmodule MyApp.Tools.DatabaseTool do
use Legion.Tool
@doc "Saves a post title to the database"
def insert_post(title), do: Repo.insert!(%Post{title: title})
end
```
### 2. Define an Agent
```elixir
defmodule MyApp.ResearchAgent do
@moduledoc """
Fetch posts, evaluate their relevance and quality, and save the good ones.
"""
use Legion.AIAgent, tools: [MyApp.Tools.ScraperTool, MyApp.Tools.DatabaseTool]
end
```
### 3. Run the Agent
```elixir
{:ok, result} = Legion.call(MyApp.ResearchAgent, "Find cool Elixir posts about Advent of Code and save them")
# => {:ok, "Found 3 relevant posts and saved 2 that met quality criteria."}
```
## How It Works
When you ask an agent: _"Find cool Elixir posts about Advent of Code and save them"_
The agent first fetches and filters relevant posts:
```elixir
ScraperTool.fetch_posts()
|> Enum.filter(fn post ->
title = String.downcase(post["title"] || "")
String.contains?(title, "elixir") and String.contains?(title, "advent")
end)
```
The LLM reviews the results, decides which posts are actually "cool", then saves them:
```elixir
["Elixir Advent of Code 2024 - Day 5 walkthrough", "My first AoC in Elixir!"]
|> Enum.each(&DatabaseTool.insert_post/1)
```
Traditional function-calling would need dozens of round-trips. Legion lets the LLM write expressive pipelines and make subjective judgments **at the same time**.
## Long-lived Agents
For multi-turn conversations or persistent agents:
```elixir
# Start an agent that maintains context
{:ok, pid} = Legion.start_link(MyApp.AssistantAgent, "Help me analyze this data")
# Send follow-up messages
{:ok, response} = Legion.send_sync(pid, "Now filter for items over $100")
# Or fire-and-forget
Legion.cast(pid, "Also check the reviews")
```
## Configuration
Configure Legion in your `config/config.exs`:
```elixir
config :legion,
model: "openai:gpt-4o",
timeout: 30_000,
max_iterations: 10,
max_retries: 3
```
- **Iterations** are successful execution steps - the agent fetches data, processes it, calls another tool, etc. Each productive action counts as one iteration.
- **Retries** are consecutive failures - when the LLM generates invalid code or a tool raises an error. The counter resets after each successful iteration.
Agents can override global settings:
```elixir
defmodule MyApp.DataAgent do
use Legion.AIAgent, tools: [MyApp.HTTPTool]
@impl true
def config do
%{model: "anthropic:claude-sonnet-4-20250514", max_iterations: 5}
end
end
```
## Agent Callbacks
All callbacks are optional with sensible defaults:
```elixir
defmodule MyApp.DataAgent do
use Legion.AIAgent, tools: [MyApp.HTTPTool]
# Structured output schema
@impl true
def output_schema do
[
summary: [type: :string, required: true],
count: [type: :integer, required: true]
]
end
# Additional instructions for the LLM
@impl true
def system_prompt do
"Always validate URLs before fetching. Prefer JSON responses."
end
# Pass options to specific tools (accessible via Vault)
@impl true
def tool_options(MyApp.HTTPTool), do: %{timeout: 10_000}
end
```
## Human in the Loop
Request human input during agent execution:
```elixir
# Agent can use the built-in HumanTool
Legion.Tools.HumanTool.ask("Should I proceed with this operation?")
# Your application responds
Legion.respond(agent_pid, "Yes, proceed")
```
## Multi-Agent Systems
Agents can spawn and communicate with other agents using the built-in `AgentTool`:
```elixir
defmodule MyApp.OrchestratorAgent do
use Legion.AIAgent, tools: [Legion.Tools.AgentTool, MyApp.Tools.DatabaseTool]
@impl true
def tool_options(Legion.Tools.AgentTool) do
%{allowed_agents: [MyApp.ResearchAgent, MyApp.WriterAgent]}
end
end
```
**The orchestrator agent** can then delegate tasks:
```elixir
# One-off task delegation
{:ok, research} = AgentTool.call(MyApp.ResearchAgent, "Find info about Elixir 1.18")
# Start a long-lived sub-agent
{:ok, pid} = AgentTool.start(MyApp.WriterAgent, "Write a blog post")
AgentTool.send(pid, "Add a section about pattern matching")
{:ok, draft} = AgentTool.ask(pid, "Show me what you have so far")
```
## Telemetry
Legion emits telemetry events for observability:
- `[:legion, :call, :start | :stop | :exception]` - agent call lifecycle
- `[:legion, :iteration, :start | :stop]` - each execution step
- `[:legion, :llm, :request, :start | :stop]` - LLM API calls
- `[:legion, :sandbox, :eval, :start | :stop]` - code evaluation
- `[:legion, :human, :input_required | :input_received]` - human-in-the-loop
<!-- MDOC -->
## License
MIT License - see [LICENSE](LICENSE) for details.