For a fully configured standlone Pyre Application see Pyre App
Core multi-agent LLM library for Pyre.
Pyre orchestrates specialized LLM workflows for software development.
Orchestration layer runs on Jido. Each agent is a reusable Jido Action with a persona that guides its output. The pipeline includes a review loop that iterates until the code reviewer approves.
Add pyre to your list of dependencies in mix.exs:
def deps do
[
{:pyre, git: "https://github.com/chrislaskey/pyre_core", branch: "main"}
]
endThen run the installer to copy persona files and set up the runs directory:
mix deps.get
mix pyre.installThis creates:
priv/pyre/personas/— Editable persona files for each agentpriv/pyre/features/.gitkeep— Directory where pipeline artifacts are stored.gitignoreentries to exclude run output from version control
Pyre can emit pubsub events when lifecycle events occur. This is optional if
you're only calling pyre on the command line and are not integrating it more
deeply within your app.
To enable PubSub events update config/config.exs:
# config/config.exs
config :pyre, :pubsub, MyApp.PubSubReplace MyApp.PubSub with the PubSub server already started in your application's
supervision tree.
Note: This is required if you are using the PyreWeb web interface, since the UI listens for these events.
By default, agent file tools (read, write, list directory) are sandboxed to the working directory. If you need access to sibling apps or shared libraries, you can allow additional directories.
Environment variable (comma-separated):
export PYRE_ALLOWED_PATHS="/path/to/apps/other,/path/to/libs/shared"Application config:
# config/runtime.exs
if paths = System.get_env("PYRE_ALLOWED_PATHS") do
config :pyre,
allowed_paths:
paths
|> String.split(",", trim: true)
|> Enum.map(&String.trim/1)
|> Enum.map(&Path.expand/1)
endFlow option (programmatic):
Pyre.Flows.FeatureBuild.run("Build a feature",
project_dir: "apps/tools",
allowed_paths: ["/path/to/apps/other"]
)Relative paths are resolved against the working directory (--project-dir),
so ../other with --project-dir apps/tools resolves to apps/other. The
working directory itself is always included automatically.
Pyre dispatches lifecycle events (flow start/complete, action start/complete,
LLM call complete) to a configurable callback module. Create a module that
use Pyre.Config and override the callbacks you need:
defmodule MyApp.Pyre.Config do
use Pyre.Config
@impl true
def after_flow_complete(%Pyre.Events.FlowCompleted{} = event) do
MyApp.Telemetry.emit(:pyre_flow_complete, %{
flow: event.flow_module,
elapsed_ms: event.elapsed_ms
})
:ok
end
endThen register it in your config:
# config/config.exs
config :pyre, config: MyApp.Pyre.ConfigAny callback not overridden returns :ok by default. Exceptions in callbacks
are rescued and logged — they never crash the running flow.
To enable the Shipper agent (creates branches and opens GitHub PRs), configure
your repository in config/runtime.exs:
# config/runtime.exs
if System.get_env("PYRE_GITHUB_REPO_URL") do
config :pyre, :github,
repositories: [
[
url: System.get_env("PYRE_GITHUB_REPO_URL"),
token: System.get_env("PYRE_GITHUB_TOKEN"),
base_branch: System.get_env("PYRE_GITHUB_BASE_BRANCH", "main")
],
# [
# url: System.get_env("ADDITIONAL_GITHUB_REPO_URL"),
# token: System.get_env("ADDITIONAL_GITHUB_TOKEN"),
# base_branch: System.get_env("ADDITIONAL_GITHUB_BASE_BRANCH", "main")
# ]
]
endSet the required environment variables:
export PYRE_GITHUB_TOKEN=ghp_...
export PYRE_GITHUB_REPO_URL=https://github.com/myorg/my-appThe Shipper automatically picks up the first configured repository. To target a
specific repo at runtime, pass the :github option:
Pyre.Flows.FeatureBuild.run("Build a feature",
github: %{owner: "acme", repo: "app", token: token, base_branch: "main"}
)Pyre also supports a GitHub App integration for @mention-triggered PR reviews.
The webhook handling, mention parsing, and job queue live in
PyreWeb. The core review workflow
(Pyre.RemoteReview) and GitHub API infrastructure (Pyre.GitHub.App) remain
in pyre_core. See the
PyreWeb README
for setup instructions.
Pyre ships with several built-in LLM backends:
| Backend | Name | Description |
|---|---|---|
Pyre.LLM.ReqLLM |
req_llm |
API-based (default) — any major provider via ReqLLM |
Pyre.LLM.ClaudeCLI |
claude_cli |
Claude CLI subprocess |
Pyre.LLM.CursorCLI |
cursor_cli |
Cursor CLI subprocess |
Pyre.LLM.CodexCLI |
codex_cli |
OpenAI Codex CLI subprocess |
The default backend is Pyre.LLM.ReqLLM. To switch backends, set the
PYRE_LLM_BACKEND environment variable to the backend's name:
export PYRE_LLM_BACKEND=claude_cliWhen using the req_llm backend, set at least one API key:
export ANTHROPIC_API_KEY=sk-ant-...
export OPENAI_API_KEY=sk-...Model aliases are configured in config/config.exs:
config :jido_ai,
model_aliases: %{
fast: "anthropic:claude-haiku-4-5",
standard: "anthropic:claude-sonnet-4-20250514",
advanced: "anthropic:claude-opus-4-20250514"
}To use a different provider (e.g., OpenAI), change the model alias strings and set the corresponding API key:
config :jido_ai,
model_aliases: %{
fast: "openai:gpt-4o-mini",
standard: "openai:gpt-4o",
advanced: "openai:o1"
}Run the feature-building pipeline:
mix pyre.run "Build a products listing page with sorting and filtering"This runs six agents in sequence:
Feature Request
-> Product Manager (requirements & user stories)
-> Designer (UI/UX spec with Tailwind layout)
-> Programmer (implementation using Phoenix conventions)
-> Test Writer (ExUnit tests)
-> Code Reviewer (APPROVE or REJECT)
-> If REJECT: loop Programmer/TestWriter/Reviewer (up to 3 cycles)
-> Shipper (git branch, commit, push, open GitHub PR)
Output streams to the terminal token-by-token so you can see each agent working in real time.
| Flag | Short | Description |
|---|---|---|
--fast |
-f |
Use the fastest model for all agents |
--dry-run |
-d |
Print plan without calling LLMs |
--verbose |
-v |
Print diagnostic information |
--no-stream |
Disable streaming (wait for complete responses) | |
--project-dir |
-p |
Working directory for agents (default: .) |
--feature |
-n |
Feature name to group related runs |
--allowed-paths |
Comma-separated additional directories agents can access |
Each run creates a timestamped directory in priv/pyre/features/<feature>/ containing:
| File | Agent | Content |
|---|---|---|
00_feature.md |
— | Original feature request |
01_requirements.md |
Product Manager | User stories and acceptance criteria |
02_design_spec.md |
Designer | UI/UX specifications |
03_implementation_summary.md |
Programmer | Code changes made |
04_test_summary.md |
Test Writer | Tests written |
05_review_verdict.md |
Code Reviewer | APPROVE or REJECT with feedback |
06_shipping_summary.md |
Shipper | Branch name, commit, PR URL |
On review rejection cycles, artifacts are versioned (_v2, _v3).
Pyre is built on three layers:
Actions — Each agent role is a Jido Action
with schema-validated inputs and a run/2 function. Actions are
flow-agnostic: the same QAReviewer action can be used in a feature-building
flow, a PR review flow, or any other pipeline.
lib/pyre/actions/
product_manager.ex # Requirements from feature description
designer.ex # UI/UX design spec
programmer.ex # Implementation (versioned on review cycles)
test_writer.ex # Test coverage (versioned)
qa_reviewer.ex # APPROVE/REJECT verdict (reusable across flows)
shipper.ex # Git branch, commit, push, and GitHub PR
Flows — Pipeline drivers that compose actions into a specific workflow. Each flow defines its phases and valid transitions:
lib/pyre/flows/
feature_build.ex # planning -> designing -> implementing ->
# testing -> reviewing -> shipping -> complete
Plugins — Shared utilities used by all actions:
lib/pyre/plugins/
persona.ex # Loads .md persona files, builds LLM messages
artifact.ex # Timestamped run directories, versioned files
Edit the persona files in priv/pyre/personas/ to customize agent behavior
for your project. Each file is a Markdown document used as the system prompt.
The installer will not overwrite files that already exist, so your changes
are preserved across updates.
Create a new module under lib/pyre/flows/ that reuses existing actions:
defmodule Pyre.Flows.PRReview do
alias Pyre.Actions.QAReviewer
def run(pr_diff, opts \\ []) do
context = %{llm: Keyword.get(opts, :llm, Pyre.LLM), streaming: true}
with {:ok, result} <- QAReviewer.run(%{
feature_description: "Review this PR",
requirements: pr_diff,
design: "",
implementation: pr_diff,
tests: "",
run_dir: "/tmp/review",
review_cycle: 1
}, context) do
{:ok, result.verdict}
end
end
enddefmodule Pyre.Actions.SecurityReviewer do
use Jido.Action,
name: "security_reviewer",
schema: [
code: [type: :string, required: true],
run_dir: [type: :string, required: true]
]
def run(params, context) do
model = Pyre.Actions.Helpers.resolve_model(:advanced, context)
{:ok, sys} = Pyre.Plugins.Persona.system_message(:security_reviewer)
user = Pyre.Plugins.Persona.user_message("Security review", params.code, params.run_dir, "security.md")
case Pyre.Actions.Helpers.call_llm(context, model, [sys, user]) do
{:ok, text} -> {:ok, %{review: text}}
error -> error
end
end
endYou can define your own LLM backend by implementing the Pyre.LLM behaviour.
Use use Pyre.LLM to get the behaviour and a default manages_tool_loop?/0
returning false:
defmodule MyApp.LLM.Ollama do
use Pyre.LLM
@impl true
def generate(model, messages, opts \\ []) do
# Call your LLM provider
{:ok, "response text"}
end
@impl true
def stream(model, messages, opts \\ []) do
output_fn = Keyword.get(opts, :output_fn, &IO.write/1)
# Stream tokens via output_fn, return full text
{:ok, "response text"}
end
@impl true
def chat(model, messages, tools, opts \\ []) do
# Handle tool-use conversations
{:ok, "response text"}
end
endThen register it in your Pyre.Config module:
defmodule MyApp.Pyre.Config do
use Pyre.Config
@impl true
def list_llm_backends do
Pyre.Config.included_llm_backends() ++ [
%{module: MyApp.LLM.Ollama, name: "ollama",
label: "Ollama", description: "Local models via Ollama"}
]
end
endThis makes your backend available via PYRE_LLM_BACKEND=ollama and visible
in any UI that calls Pyre.Config.list_llm_backends/0.
To set it as the default regardless of env vars, also override get_llm_backend/1:
@impl true
def get_llm_backend(_arg), do: MyApp.LLM.OllamaFor CLI-style backends that manage their own tool-calling loop (like Claude CLI
or Codex CLI), override manages_tool_loop?/0 to return true. This tells
Pyre to call chat/4 directly instead of routing through the agentic loop.
Pyre includes Igniter-based generators that agents use during the pipeline:
mix pyre.gen.context— Generates a context module with CRUD functionsmix pyre.gen.live— Generates LiveView pages with index/show viewsmix pyre.gen.modal— Adds a modal component to a LiveViewmix pyre.gen.filter— Adds a filter function to an existing context
Actions and flows are testable without LLM calls using the mock:
# Test a single action
Process.put(:mock_llm_response, "APPROVE\n\nLooks great!")
{:ok, result} = Pyre.Actions.QAReviewer.run(params, %{llm: Pyre.LLM.Mock, streaming: false})
assert result.verdict == :approve
# Test a full flow with sequenced responses
Process.put(:mock_llm_responses, [
"Requirements...", "Design...", "Implementation...", "Tests...", "APPROVE\n\nDone."
])
{:ok, state} = Pyre.Flows.FeatureBuild.run("Build a feature", llm: Pyre.LLM.Mock, streaming: false)
assert state.phase == :complete