README.md

# 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.