Skip to content

Releases

v0.6.0

Choose a tag to compare

@dimamik dimamik released this 13 Oct 10:15
ebd58bb

Highlights

  • MCP server - Legion.MCP.Server serves a Lua agent to MCP hosts as repl and help tools; see the MCP guide
    • Over stdio or Streamable HTTP; Legion.MCP.Plug refuses browser requests from origins other than localhost unless listed in :allowed_origins
    • OAuth 2.1, and a session/1 callback choosing the agent, vault and rate limits per call
    • Legion.Tools.AgentTool and Legion.Tools.HumanTool are left out
    • Needs the optional {:anubis_mcp, "~> 2.0"} dependency
  • Legion.eval/3 - runs code in a running agent as if its model had written it, with no LLM call: same tools, variables and limits, saved and rate-limited as one step of the conversation
  • On-demand tool docs - tool_docs: :on_demand lists tools in the prompt by a one-line summary/0, and Help.help(Name) returns a tool's full description; the default over MCP
  • Sub-agents from Lua - Lua agents hold sub-agent conversations through Legion.Tools.AgentTool, whose ids cross the Lua bridge

Breaking

Agents

  • Default model is openai:gpt-5.6-luna
  • An agent id belongs to one agent module - starting another module under an id whose stored conversation is not its own returns {:error, {:agent_module_mismatch, stored}}
  • Legion.resume/2 and Legion.recover/2 run the model only to finish an interrupted turn - one stopped behind a checkpoint or on a prompt with nothing after it; resume/2 restores any other conversation and waits for the next message

Sub-agents

  • Legion.Tools.AgentTool works with ids, not pids - start_link/1,2 return the sub-agent's id, and call/2, cast/2 and the new stop/1 take it
    • Only the agent that started a sub-agent reaches it
    • A sub-agent stops when that agent stops, when the turn that started it ends unless :binding_scope is :conversation, or after :sub_agent_idle_timeout without a message (default thirty minutes)
    • An agent runs at most :max_sub_agents at once (default 10)
    • A start is checked against the rate limit, returning {:cancel, {:rate_limited, violations}} when denied

Tools

  • Help is a reserved tool name - Legion.Tools.Help is in every agent's sandbox, so a tool of your own with the short name Help is no longer reachable; use Legion.Agent warns about it
  • summary/0 and mcp?/0 are Legion.Tool callbacks - use Legion.Tool defines them, so Lua code no longer reaches a tool function of either name, and a summary/0 not marked @impl Legion.Tool warns. A module that implements @behaviour Legion.Tool without use must define both

Rate limiting

  • :max_agents caps agents, checked at start - a denied Legion.start_link/2 returns {:error, {:rate_limited, violations}} with no process started and nothing saved, and Legion.execute/3 returns {:cancel, {:rate_limited, violations}}. Resumed and recovered runs are not checked
  • :max_tokens, :max_evals and :max_running_agents need the Postgres store - with Legion.RateLimiter.Postgres, an agent whose rules set any of them raises at start unless its store is a Legion.Store.Postgres on the limiter's repo and table, since those limits count what that store writes. With config :legion, :track_usage, false, rules setting :max_tokens or :max_evals raise at start too

Persistence

  • Usage entries may have no token counts - a Legion.eval/3 step records %{"at" => ..., "evals" => 1, "message_index" => ...}, and an LLM entry whose action ran code carries "evals" => 1; code summing token keys over usage must allow for entries without them. See Legion.Store
  • Custom Legion.Store adapters must keep :base_bindings - a saved :conversation_state carries it next to :bindings; an adapter that keeps only the keys it knew drops it, and an agent restarted mid-turn loses the variables that turn started with

Sandbox

  • :max_bindings_bytes caps leftover variables - default 10_000_000; an execution that would leave larger variables behind fails. Set it to :infinity for the old behaviour
  • Legion.Sandbox.Lua check/2 no longer reports syntax errors - parsing moved inside the sandbox process, under its limits, and syntax errors come back from execute/5

Other changes

  • Rate limiting - :max_evals in Legion.RateLimiter.Policy caps code evaluations per window
  • Legion.start_link/2 takes :vault, put in the agent process for its tools, and :idle_timeout, which stops an agent nobody calls
  • max_message_length defaults to 40_000 bytes and also bounds code sent with Legion.eval/3
  • :step stores persist usage with every checkpoint instead of only at turn end

v0.5.1

Choose a tag to compare

@dimamik dimamik released this 26 Sep 08:38
be0b3f9

v0.5.1

Changes

v0.5.0

Choose a tag to compare

@dimamik dimamik released this 01 Sep 15:41
9a69918

v0.5.0 - 2026-09-01

Changes

v0.4.0

Choose a tag to compare

@dimamik dimamik released this 17 May 17:14
d18cd70

Security

  • Harden Legion.Sandbox.ASTChecker against a class of RCE paths. After this release, most (if not all) RCE vectors should be closed. Legion is still vulnerable to DoS kinds of attacks, but we assume that having a system prompt instruction to behave well AND improving sandbox should be enough for now.

Changes

  • Broaden the sandbox surface for common LLM idioms: allow Map.values/1, JSON, URI, :erlang.float_to_binary/2, additional String/Enum/Date/DateTime functions, and the Access protocol (map[:k])
  • Document the sandbox constraints with concrete idioms in the system prompt
  • Fix tool source extraction breaking on heredocs and charlists
  • Correct documentation for telemetry events, source registry, and AgentTool.start_link/2

v0.3.0

Choose a tag to compare

@dimamik dimamik released this 21 Apr 15:09
17e52eb

Legion just became better and more stable!

Changes

  • Replace share_bindings boolean with binding_scope (:iteration, :turn, :conversation) for fine-grained control over variable lifetime across code executions
  • Add action_types/0 callback to restrict which actions an agent can use (e.g. ~w(return done) for read-only agents)
  • Add max_message_length config with truncation support to prevent unbounded message growth
  • Add multimedia message support: {:image, data, media_type}, {:image_url, url}, and {:multipart, parts}
  • Add Legion.get_messages/1 to retrieve conversation history from a running agent
  • Expand AgentTool with parallel/2, pipeline/1, then/3, and extra_allowed_modules/0 for sub-agent orchestration; sub-agents are auto-aliased in the sandbox
  • Generate dynamic AgentTool.description/0 from sub-agent moduledocs
  • Move system prompt resolution to AgentPrompt, respecting custom system_prompt/0 overrides
  • Validate config keys at startup with warnings for unknown keys
  • Add @moduledoc compile-time validation via __before_compile__
  • Harden sandbox: block def/defp/__ENV__, additional :erlang functions (process_flag, list_to_atom, system_info), catch throws and exits, surface compiler diagnostics on errors
  • Handle executor exceptions gracefully instead of crashing the agent loop
  • Add Calendar to sandbox safe-module list
  • Emit :exception telemetry events for iteration, LLM, and sandbox spans; use System.convert_time_unit/3 for duration reporting
  • Extensive new test coverage for AgentServer, Executor, Sandbox, and ASTChecker

v0.2.0

Choose a tag to compare

@dimamik dimamik released this 15 Mar 16:27
b184713

Changes

  • Simplified and refactored internals
  • Improved documentation and general library intent

v0.1.0

Choose a tag to compare

@dimamik dimamik released this 30 Dec 13:16
4bffb6e

Legion

Legion is an Elixir-native framework for building AI agents. Agents are plain modules and tools are plain functions. Agents could return sandboxed code that essentially could do anything: call tools, spawn other agents, exchange messages, and react to events in real-time.

Quick Start

1. Define your tools

Tools are regular Elixir modules that expose functions to your agents:

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

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

{: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:

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

Main features

  • Code Generation over Function Calling - Agents write expressive Elixir pipelines instead of making dozens of tool-call round-trips. This makes your agents smarter and reduces amount of tokens being used. See anthropic post about this.
  • Sandboxed Execution - Generated code runs in a restricted environment with controlled access to tools. You can define memory, time, and call limits.
  • Simple Tool Definition - Expose any Elixir module as a tool with use Legion.Tool. You can reuse your existing app logic.
  • Authorization baked in - The safest way to authorize tool calls via Vault library. Put all data needed to authorize LLM call before starting Agent, and validate it inside the tool call. Everything will be available due to the Vault's nature.
  • Long-lived Agents - Maintain context across multi-turn conversations with start_link/2.
  • Multi-Agent Systems - Agents can orchestrate other agents, letting you create complex systems that will manage themselves.
  • Human in the Loop - Pause execution to request human input when needed
  • Structured Output - Define schemas to get typed, validated responses from agents, or omit types and operate on plain text. You have full conrol over prompts and schemas.
  • Configurable - Global defaults with per-agent overrides for model, timeouts, and limits
  • Telemetry - Built-in observability with events for calls, iterations, LLM requests, and more