Repository navigation
Releases
Releases · software-mansion/legion
v0.6.0
Highlights
- MCP server -
Legion.MCP.Serverserves a Lua agent to MCP hosts asreplandhelptools; see the MCP guide- Over stdio or Streamable HTTP;
Legion.MCP.Plugrefuses browser requests from origins other thanlocalhostunless listed in:allowed_origins - OAuth 2.1, and a
session/1callback choosing the agent, vault and rate limits per call Legion.Tools.AgentToolandLegion.Tools.HumanToolare left out- Needs the optional
{:anubis_mcp, "~> 2.0"}dependency
- Over stdio or Streamable HTTP;
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_demandlists tools in the prompt by a one-linesummary/0, andHelp.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/2andLegion.recover/2run the model only to finish an interrupted turn - one stopped behind a checkpoint or on a prompt with nothing after it;resume/2restores any other conversation and waits for the next message
Sub-agents
Legion.Tools.AgentToolworks with ids, not pids -start_link/1,2return the sub-agent's id, andcall/2,cast/2and the newstop/1take 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_scopeis:conversation, or after:sub_agent_idle_timeoutwithout a message (default thirty minutes) - An agent runs at most
:max_sub_agentsat once (default 10) - A start is checked against the rate limit, returning
{:cancel, {:rate_limited, violations}}when denied
Tools
Helpis a reserved tool name -Legion.Tools.Helpis in every agent's sandbox, so a tool of your own with the short nameHelpis no longer reachable;use Legion.Agentwarns about itsummary/0andmcp?/0areLegion.Toolcallbacks -use Legion.Tooldefines them, so Lua code no longer reaches a tool function of either name, and asummary/0not marked@impl Legion.Toolwarns. A module that implements@behaviour Legion.Toolwithoutusemust define both
Rate limiting
:max_agentscaps agents, checked at start - a deniedLegion.start_link/2returns{:error, {:rate_limited, violations}}with no process started and nothing saved, andLegion.execute/3returns{:cancel, {:rate_limited, violations}}. Resumed and recovered runs are not checked:max_tokens,:max_evalsand:max_running_agentsneed the Postgres store - withLegion.RateLimiter.Postgres, an agent whose rules set any of them raises at start unless its store is aLegion.Store.Postgreson the limiter's repo and table, since those limits count what that store writes. Withconfig :legion, :track_usage, false, rules setting:max_tokensor:max_evalsraise at start too
Persistence
- Usage entries may have no token counts - a
Legion.eval/3step 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. SeeLegion.Store - Custom
Legion.Storeadapters must keep:base_bindings- a saved:conversation_statecarries 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_bytescaps leftover variables - default10_000_000; an execution that would leave larger variables behind fails. Set it to:infinityfor the old behaviourLegion.Sandbox.Luacheck/2no longer reports syntax errors - parsing moved inside the sandbox process, under its limits, and syntax errors come back fromexecute/5
Other changes
- Rate limiting -
:max_evalsinLegion.RateLimiter.Policycaps code evaluations per window Legion.start_link/2takes:vault, put in the agent process for its tools, and:idle_timeout, which stops an agent nobody callsmax_message_lengthdefaults to40_000bytes and also bounds code sent withLegion.eval/3:stepstores persist usage with every checkpoint instead of only at turn end
v0.5.1
v0.5.1
Changes
- Rate limiting -
Legion.RateLimiter.resolve!/1raises or warns on invalid or incomplete configuration;rules:no longer read fromconfig :legion, :rate_limit - Rate limiting -
:max_running_agentsinLegion.RateLimiter.Policycaps how many matching agents run a turn at once - Recovery - recovered runs skip rate limiting; rules are not persisted, so
Legion.resume/2takes:rate_limitagain - Usage tracking -
"message_index"links usage to its assistant message, seeLegion.Store - Persistence -
Legion.Store.Payloadexposes the agent's rate-limit identity as:ratelimit_metadata Legion.Sandbox.Luaskips Erlang modules in the tool list instead of crashing- Guides - Adding Legion to an existing app, Using Legion with Ash, Using local LLMs
v0.5.0
v0.5.0 - 2026-09-01
Changes
- Pluggable sandboxes - the
Legion.Sandboxbehaviour,Legion.Sandbox.Elixir, sharedLegion.Sandbox.Runner Legion.Sandbox.Lua, now the default sandbox- Sandbox resource limits - timeout, memory, and CPU budgets in
Legion.Sandbox.Runner - Sandbox-specific
Legion.Tool.description/1 - Persistence -
Legion.Storesaves conversation state (messages, bindings, executor checkpoints), status, and LLM usage across restarts, with a Postgres adapter, versioned migrations, andpersistence_frequency/0 - Agent identity - string agent ids,
Legion.get_agent_id/1,Legion.lookup/1, cluster-wide:globalregistration;:nameoption removed - Recovery -
Legion.resume/2,Legion.recover/2,:recoverystartup config - Rate limiting -
Legion.RateLimiterwith rules, policies, and a Postgres adapter - LLM usage tracking - persisted per request (
:track_usage),usagein[:legion, :llm, :request, :stop]telemetry - Default model bumped to
openai:gpt-5.4 Legion.Tools.HumanTool.ask/1raises undereval_and_complete- Bump ReqLLM
v0.4.0
Security
- Harden
Legion.Sandbox.ASTCheckeragainst 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, additionalString/Enum/Date/DateTimefunctions, and theAccessprotocol (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
Legion just became better and more stable!
Changes
- Replace
share_bindingsboolean withbinding_scope(:iteration,:turn,:conversation) for fine-grained control over variable lifetime across code executions - Add
action_types/0callback to restrict which actions an agent can use (e.g.~w(return done)for read-only agents) - Add
max_message_lengthconfig 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/1to retrieve conversation history from a running agent - Expand
AgentToolwithparallel/2,pipeline/1,then/3, andextra_allowed_modules/0for sub-agent orchestration; sub-agents are auto-aliased in the sandbox - Generate dynamic
AgentTool.description/0from sub-agent moduledocs - Move system prompt resolution to
AgentPrompt, respecting customsystem_prompt/0overrides - Validate config keys at startup with warnings for unknown keys
- Add
@moduledoccompile-time validation via__before_compile__ - Harden sandbox: block
def/defp/__ENV__, additional:erlangfunctions (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
Calendarto sandbox safe-module list - Emit
:exceptiontelemetry events for iteration, LLM, and sandbox spans; useSystem.convert_time_unit/3for duration reporting - Extensive new test coverage for
AgentServer,Executor,Sandbox, andASTChecker
v0.2.0
v0.1.0
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})
end2. 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]
end3. 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
Vaultlibrary. Put all data needed to authorize LLM call before starting Agent, and validate it inside the tool call. Everything will be available due to theVault'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