KO
|
EN
gitlite — search
Search
#php
#python
#java
#javascript
#shell
#html
#jquery
#linux
#nodejs
#r
#ruby
#actionscript
alloy
★ 81
Open GitHub ↗
Model-agnostic agent harness for Elixir
Download README (.md)
Explore Similar Repositories
gleece
:
Building, documenting, validating, securing and customizing REST APIs through code-first development
grpo-flat
:
Train your grpo with zero dataset and low resources, 8bit/4bit/lora/qlora supported, multi-gpu supported ...
Noema-Declarative-AI
:
A declarative way to control LLMs.
rag-search-ingestion-langchainjs-gemini
:
A PDF search ingestion RAG application with Docker + LangChain.js + Gemini
carquet
:
A high-performance, SIMD-optimized, pure C library for reading and writing Apache Parquet files.
// repository documentation
Was this content helpful?
★ 0
(0 ratings)
Select Rating:
★
★
★
★
★
Submit Feedback
Recent Feedback
×
Download README
Do you want to download the
README.md
file for
alloy
?
Download (.md)
# Alloy [](https://hex.pm/packages/alloy) [](https://github.com/alloy-ex/alloy/actions/workflows/ci.yml) [](https://hexdocs.pm/alloy) [](https://raw.githubusercontent.com/alloy-ex/alloy/main/LICENSE) **Minimal, OTP-native agent loop for Elixir.** Alloy is the completion-tool-call loop and nothing else. Send messages to any LLM, execute tool calls, loop until done. Swap providers with one line. No opinions on sessions, persistence, memory, scheduling, or UI — those belong in your application, where OTP already gives you the runtime. Alloy is a harness, not a framework. Three runtime dependencies, ~9,000 lines — small enough to read in an afternoon, and everything beyond the loop is a [recipe](https://hexdocs.pm/alloy/sub-agents.html) built on the primitives, not a subsystem. ```elixir {:ok, result} = Alloy.run("Read mix.exs and tell me the version", provider: {Alloy.Provider.OpenAI, api_key: System.get_env("OPENAI_API_KEY"), model: "gpt-5.4"}, tools: [Alloy.Tool.Core.Read] ) result.text #=> "The version is 0.12.0" ``` ## Why Alloy? Most agent frameworks try to be everything — sessions, memory, RAG, multi-agent orchestration, scheduling, UI. Alloy does one thing well: the agent loop. The BEAM makes the rest cheaper than a framework can: sub-agents are supervised function calls ([recipe](https://hexdocs.pm/alloy/sub-agents.html)), MCP servers mount as tools ([recipe](https://hexdocs.pm/alloy/mcp-tools.html)), parallel tool execution is `Task.Supervisor`, and crash isolation is just processes. ## Design Boundary Alloy stays minimal by owning protocol and loop concerns, not application workflows. What belongs in Alloy: - Provider wire-format translation - Tool-call / completion loop mechanics - Normalized message blocks - Opaque provider-owned state such as stored response IDs - Provider response metadata such as citations or server-side tool telemetry What does not belong in Alloy: - Sessions and persistence policy - File storage, indexing, or retrieval workflows - UI rendering for citations, search, or artifacts - Scheduling, background job orchestration, or dashboards - Tenant plans, quotas, billing, or hosted infrastructure policy Rule of thumb: if the feature is required to speak a provider API correctly, and could help any Alloy consumer, it likely belongs here. If it needs a database table, product defaults, UI decisions, or tenancy logic, it belongs in your application layer. ## Non-goals Alloy does not own multi-agent orchestration; use the [sub-agents recipe](https://hexdocs.pm/alloy/sub-agents.html), where an agent spawns itself via a function call. Alloy does not sandbox your machine; use `:bash_executor` with containers or another isolated runner. Sessions, persistence, scheduling, hosted queues, dashboards, and UI stay in your application layer. The library stays small so those choices remain yours. ## What's in the box - **6 providers** — Anthropic, Gemini, OpenAI, Codex, xAI, and OpenAICompat (works with any OpenAI-compatible API: Ollama, OpenRouter, DeepSeek, Mistral, Groq, Together, etc.) - **4 built-in tools** — read, write, edit, bash — plus inline tools defined as data with `Alloy.Tool.inline/1` - **GenServer agents** — supervised, stateful, message-passing (moving to the optional `alloy_agent` runtime package in 0.13) - **Streaming** — token-by-token from any provider, unified interface - **Async dispatch** — `send_message/2` fires non-blocking, result arrives via PubSub - **Middleware** — custom hooks, tool blocking, argument editing - **Context compaction** — tool-result clearing plus summary-based compaction when approaching token limits, with configurable reserve and fallback to truncation - **Memory primitive** — `Alloy.Memory` behaviour for Anthropic's `memory_20250818` tool. Alloy owns the wire format and path validation; you own the store (in-memory, disk, Postgres — whatever fits) - **Prompt caching** — Anthropic `cache: true` adds cache breakpoints for 60-90% input token savings - **Reasoning blocks** — DeepSeek/xAI `reasoning_content` parsed as first-class thinking blocks - **Tool guardrails** — `concurrent?/0` controls parallel execution, `max_result_chars/0` caps output, prompt-too-long auto-recovery. Note: the bash tool's restricted mode is a guardrail, not a sandbox — see [Built-in tools](#built-in-tools) - **Structured output** — `until_tool` forces the loop to continue until a specific tool is called - **Provider passthrough** — `extra_body` injects arbitrary provider-specific params (response_format, temperature, reasoning_effort) - **Telemetry** — run, turn, provider, and compaction lifecycle events for OTEL/logging/metrics - **Cost guard** — `max_budget_cents` halts the loop before overspending - **Pluggable model catalog** — `Alloy.ModelCatalog` behaviour; bring your own context-window source (e.g., an `llm_db` adapter) - **~9,000 lines** — small enough to read, understand, and extend ## Installation Add `alloy` to your dependencies in `mix.exs`: ```elixir def deps do [ {:alloy, "~> 0.12"} ] end ``` > **Note:** the optional `alloy_agent` runtime wrapper (sessions, async > dispatch, memory stores) referenced by the 0.12.x deprecation notices is > **not yet published** to Hex. Until it ships, keep using > `Alloy.Agent.Server` and `Alloy.Session` from this package — the 0.13 > removals will not happen before `alloy_agent` is available. ## Quick Start Prefer a runnable version? [](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Falloy-ex%2Falloy%2Fmain%2Flivebooks%2Fquickstart.livemd) ### Simple completion ```elixir {:ok, result} = Alloy.run("What is 2+2?", provider: {Alloy.Provider.Anthropic, api_key: "sk-ant-...", model: "claude-sonnet-4-6"} ) result.text #=> "4" ``` ### Agent with tools ```elixir {:ok, result} = Alloy.run("Read mix.exs and summarize the dependencies", provider: {Alloy.Provider.Gemini, api_key: "...", model: "gemini-2.5-flash-lite"}, tools: [Alloy.Tool.Core.Read, Alloy.Tool.Core.Bash], max_turns: 10 ) ``` Gemini model IDs Alloy now budgets for include `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-2.5-flash-lite`, `gemini-3-pro-preview`, and `gemini-3-flash-preview`. ### Swap providers in one line ```elixir # The same tools and conversation work with any provider opts = [tools: [Alloy.Tool.Core.Read], max_turns: 10] # Anthropic Alloy.run("Read mix.exs", [{:provider, {Alloy.Provider.Anthropic, api_key: "...", model: "claude-sonnet-4-6"}} | opts]) # OpenAI Alloy.run("Read mix.exs", [{:provider, {Alloy.Provider.OpenAI, api_key: "...", model: "gpt-5.4"}} | opts]) # Gemini Alloy.run("Read mix.exs", [{:provider, {Alloy.Provider.Gemini, api_key: "...", model: "gemini-2.5-flash"}} | opts]) # xAI via Responses-compatible API Alloy.run("Read mix.exs", [{:provider, {Alloy.Provider.OpenAI, api_key: "...", api_url: "https://api.x.ai", model: "grok-4.20-0309-reasoning"}} | opts]) # xAI via chat completions (reasoning models, extra_body) Alloy.run("Read mix.exs", [{:provider, {Alloy.Provider.OpenAICompat, api_key: "...", api_url: "https://api.x.ai", model: "grok-4.1-fast-reasoning"}} | opts]) # Any OpenAI-compatible API (Ollama, OpenRouter, DeepSeek, Mistral, Groq, etc.) Alloy.run("Read mix.exs", [{:provider, {Alloy.Provider.OpenAICompat, api_url: "http://localhost:11434", model: "llama4"}} | opts]) ``` ### Streaming For a one-shot run, use `Alloy.stream/3`: ```elixir {:ok, result} = Alloy.stream("Explain OTP", fn chunk -> IO.write(chunk) end, provider: {Alloy.Provider.OpenAI, api_key: "...", model: "gpt-5.4"} ) ``` For a persistent agent process with conversation state, use `Alloy.Agent.Server.stream_chat/4`: ```elixir {:ok, agent} = Alloy.Agent.Server.start_link( provider: {Alloy.Provider.OpenAI, api_key: "...", model: "gpt-5.4"}, tools: [Alloy.Tool.Core.Read] ) {:ok, result} = Alloy.Agent.Server.stream_chat(agent, "Explain OTP", fn chunk -> IO.write(chunk) # Print each token as it arrives end) ``` All providers support streaming. If a custom provider doesn't implement `stream/4`, the turn loop falls back to `complete/3` automatically. `Alloy.run/2` remains the buffered convenience API. Use `Alloy.stream/3` when you want the same one-shot flow with token streaming. ### Provider-owned state Some provider APIs expose server-side state such as stored response IDs. That transport concern lives in Alloy; your app decides whether and how to persist it. Results expose provider-owned state in `result.metadata.provider_state`: ```elixir {:ok, result} = Alloy.run("Read the repo", provider: {Alloy.Provider.OpenAI, api_key: System.get_env("XAI_API_KEY"), api_url: "https://api.x.ai", model: "grok-4.20-0309-reasoning", store: true } ) provider_state = result.metadata.provider_state ``` Pass that state back to the same provider on the next turn to continue a provider-native conversation: ```elixir {:ok, next_result} = Alloy.run("Keep going", messages: result.messages, provider: {Alloy.Provider.OpenAI, api_key: System.get_env("XAI_API_KEY"), api_url: "https://api.x.ai", model: "grok-4.20-0309-reasoning", provider_state: provider_state } ) ``` For native OpenAI Responses, Alloy automatically preserves opaque reasoning items across stateless tool-call turns. When `store` is not `true` and no `previous_response_id` / `provider_state.response_id` is present, the request adds `include: ["reasoning.encrypted_content"]` and echoes returned reasoning items back verbatim before their related function calls; `result.text` ignores those opaque blocks. ### Provider-native tools and citations Responses-compatible providers can expose built-in server-side tools without leaking those wire details into your app layer. For xAI search tools: ```elixir {:ok, result} = Alloy.run("Summarise the latest xAI docs updates", provider: {Alloy.Provider.OpenAI, api_key: System.get_env("XAI_API_KEY"), api_url: "https://api.x.ai", model: "grok-4.20-0309-reasoning", web_search: %{allowed_domains: ["docs.x.ai"]}, include: ["inline_citations"] } ) ``` Citation metadata is exposed in two places: - `result.metadata.provider_response.citations` for provider-level citation data - assistant text blocks may include `:annotations` for inline citation spans ### Overriding model metadata Alloy derives the compaction budget from the configured provider model when it knows that model's context window. If you need to support a just-released model before Alloy ships a catalog update, override it in config: ```elixir {:ok, result} = Alloy.run("Summarise this repository", provider: {Alloy.Provider.OpenAI, api_key: "...", model: "gpt-5.4-2026-03-05"}, model_metadata_overrides: %{ "gpt-5.4" => 900_000, "acme-reasoner" => %{limit: 640_000, suffix_patterns: ["", ~r/^-\d{4}\.\d{2}$/]} } ) ``` Set `max_tokens` explicitly when you want a fixed compaction budget. Otherwise Alloy derives it from the current model, including after `Alloy.Agent.Server.set_model/2` switches to a different provider model. ### Pluggable model catalog The built-in catalog is deliberately small — Alloy is a harness, not a model database, and a hand-curated list will always lag the release treadmill. For a richer source of truth, implement the `Alloy.ModelCatalog` behaviour and pass it via `model_catalog:`. For example, backed by [`llm_db`](https://hex.pm/packages/llm_db) (45+ providers, maintained against models.dev): ```elixir defmodule MyApp.LlmDbCatalog do @behaviour Alloy.ModelCatalog @impl true def context_window(model) do case LLMDb.model(model) do %{context_window: limit} when is_integer(limit) -> limit _ -> nil end end end {:ok, result} = Alloy.run("Summarise this repository", provider: {Alloy.Provider.OpenAI, api_key: "...", model: "gpt-5.4"}, model_catalog: MyApp.LlmDbCatalog ) ``` Resolution order: explicit `max_tokens` → `model_metadata_overrides` → `model_catalog` → default window. Returning `nil` for an unknown model is fine — Alloy falls back to the default. Use `compaction:` when you want to tune how much room Alloy reserves before it compacts older context: ```elixir {:ok, result} = Alloy.run("Summarise this repository", provider: {Alloy.Provider.OpenAI, api_key: "...", model: "gpt-5.4"}, compaction: [ reserve_tokens: 12_000, keep_recent_tokens: 8_000, clear_tool_results: true, keep_recent_tool_results: 3, fallback: :truncate ] ) ``` Alloy first clears old `tool_result` and `server_tool_result` content in one batch, preserving the newest `keep_recent_tool_results` results, then only calls the summarizer if the run is still over budget. Batched clearing matters with prompt caching: clearing invalidates cached prefixes, so Alloy amortizes that cost instead of dripping changes across turns. Set `summary_system_prompt:` and `summary_prompt:` inside `compaction:` when your application needs to own the handoff format. Both values must be strings; omitting them uses Alloy's default summary prompts. ### Cost guard Cap how much an agent run can spend: ```elixir {:ok, result} = Alloy.run("Research this codebase thoroughly", provider: {Alloy.Provider.Anthropic, api_key: "...", model: "claude-sonnet-4-6"}, tools: [Alloy.Tool.Core.Read, Alloy.Tool.Core.Bash], max_budget_cents: 50 ) case result.status do :completed -> IO.puts(result.text) :budget_exceeded -> IO.puts("Stopped: spent #{result.usage.estimated_cost_cents}¢") end ``` Set `max_budget_cents: nil` (default) for no limit. ### Anthropic prompt caching Enable prompt caching to save 60-90% on input tokens. Alloy automatically adds `cache_control` breakpoints to the system prompt and last tool definition: ```elixir {:ok, result} = Alloy.run("Explain this codebase", provider: {Alloy.Provider.Anthropic, api_key: "...", model: "claude-sonnet-4-6", cache: true }, tools: [Alloy.Tool.Core.Read, Alloy.Tool.Core.Bash], system_prompt: "You are a senior Elixir developer." ) # Cache usage is reported in result.usage result.usage.cache_creation_input_tokens #=> 1500 result.usage.cache_read_input_tokens #=> 1500 (on subsequent calls) ``` ### Memory (Anthropic `memory_20250818`) Alloy exposes memory as a behaviour — `Alloy.Memory` — matching the split Anthropic uses in their own Python SDK: Alloy owns the protocol (six commands on a `/memories/` tree, return-string formats, path validation); your code owns the backing store. No bytes touch Anthropic's servers. ```elixir defmodule MyApp.Memory.Disk do @behaviour Alloy.Memory @impl true def view(store, path), do: # read from disk @impl true def create(store, path, text), do: # write @impl true def str_replace(store, path, old, new), do: # ... @impl true def insert(store, path, line, text), do: # ... @impl true def delete(store, path), do: # ... @impl true def rename(store, old_path, new_path), do: # ... end {:ok, result} = Alloy.run("Remember the user prefers SI units", provider: {Alloy.Provider.Anthropic, api_key: "sk-ant-...", model: "claude-sonnet-4-6"}, memory: {MyApp.Memory.Disk, root: "/var/agent/memories"} ) ``` When `:memory` is set, Alloy injects the `memory_20250818` tool into the Anthropic request and adds the `context-management-2025-06-27` beta header. Memory tool calls are routed through `Alloy.Memory.Router` (not the general tool executor) so the typed-tool contract stays clean. The store term (second element of `{module, opts}`) is opaque — pass a keyword list, a map, a `pid()`, or a struct, whichever your store needs. Alloy does not bake session scoping into the contract; if you want per-session memory trees, thread `session_id: "..."` through your store opts and namespace inside your implementation. As of 0.12.0, memory is Anthropic-only — configuring `:memory` with any other provider raises at `Alloy.run/2` entry. Other providers will be wired as they ship their own memory primitives. ### Reasoning model support (DeepSeek, xAI) OpenAI-compatible reasoning models that return `reasoning_content` (DeepSeek-R1, xAI Grok reasoning variants) are automatically parsed into thinking blocks: ```elixir {:ok, result} = Alloy.run("Solve this step by step", provider: {Alloy.Provider.OpenAICompat, api_url: "https://api.x.ai", api_key: "...", model: "grok-4.1-fast-reasoning" } ) # Thinking blocks are preserved in message content [thinking, text] = hd(result.messages).content thinking.type #=> "thinking" thinking.thinking #=> "Step 1: Let me consider..." text.type #=> "text" text.text #=> "The answer is 42." ``` ### Provider-specific parameters (extra_body) Pass arbitrary provider-specific parameters via `extra_body`. It merges last, so it can override any default field: ```elixir {:ok, result} = Alloy.run("Return JSON", provider: {Alloy.Provider.OpenAICompat, api_url: "https://api.deepseek.com", api_key: "...", model: "deepseek-chat", extra_body: %{ "response_format" => %{"type" => "json_object"}, "temperature" => 0.3 } } ) ``` Works for any provider param: `reasoning_effort`, `max_completion_tokens`, `presence_penalty`, etc. ### Telemetry Alloy emits telemetry events for observability. Attach handlers for OTEL, logging, or custom metrics: ```elixir :telemetry.attach_many("my-handler", [ [:alloy, :run, :start], [:alloy, :run, :stop], [:alloy, :turn, :start], [:alloy, :turn, :stop], [:alloy, :provider, :request], [:alloy, :compaction, :cleared], [:alloy, :compaction, :done], [:alloy, :tool, :start], [:alloy, :tool, :stop], [:alloy, :event] ], &MyApp.Telemetry.handle_event/4, nil) ``` | Event | Measurements | Metadata | |-------|-------------|----------| | `[:alloy, :run, :start]` | `system_time` | `model` | | `[:alloy, :run, :stop]` | `duration_ms` | `status`, `turns`, `model` | | `[:alloy, :turn, :start]` | `system_time` | `turn` | | `[:alloy, :turn, :stop]` | — | `turn`, `status` | | `[:alloy, :provider, :request]` | `duration_ms` | `provider`, `model`, `streaming`, `attempt`, `result` | | `[:alloy, :compaction, :cleared]` | `results_cleared`, `bytes_cleared` | `turn` | | `[:alloy, :compaction, :done]` | `messages_before`, `messages_after` | `turn` | | `[:alloy, :tool, :start]` | — | tool identity, correlation | | `[:alloy, :tool, :stop]` | `duration_ms` | tool identity, result | ### Structured output with `until_tool` Force the model to call a specific tool before the loop completes. This is more reliable than response format instructions because the tool schema is validated at the API level: ```elixir defmodule SubmitAnswer do @behaviour Alloy.Tool def name, do: "submit_answer" def description, do: "Submit your final answer as structured data." def input_schema do %{type: "object", properties: %{ answer: %{type: "string"}, confidence: %{type: "number", minimum: 0, maximum: 1} }, required: ["answer", "confidence"]} end def execute(input, _ctx), do: {:ok, "Received: #{input["answer"]}"} end {:ok, result} = Alloy.run("What is the capital of France?", provider: {Alloy.Provider.Anthropic, api_key: "...", model: "claude-sonnet-4-6"}, tools: [SubmitAnswer], until_tool: "submit_answer" ) ``` ### Middleware: editing tool arguments Middleware can return `{:edit, modified_call}` from `:before_tool_call` to rewrite tool arguments before execution (e.g., policy enforcement, input sanitization): ```elixir defmodule SanitizeBash do @behaviour Alloy.Middleware def call(:before_tool_call, state) do call = state.config.context[:current_tool_call] if call[:name] == "bash" && String.contains?(call[:input]["command"], "rm ") do {:edit, %{call | input: %{"command" => "echo 'rm commands are blocked'"}}} else state end end def call(_hook, state), do: state end ``` ### Supervised GenServer agent ```elixir {:ok, agent} = Alloy.Agent.Server.start_link( provider: {Alloy.Provider.Anthropic, api_key: "...", model: "claude-sonnet-4-6"}, tools: [Alloy.Tool.Core.Read, Alloy.Tool.Core.Edit, Alloy.Tool.Core.Bash], system_prompt: "You are a senior Elixir developer." ) {:ok, response} = Alloy.Agent.Server.chat(agent, "What does this project do?") {:ok, response} = Alloy.Agent.Server.chat(agent, "Now refactor the main module") ``` ### Async dispatch (Phoenix LiveView) Fire a message without blocking the caller — ideal for LiveView and background jobs: ```elixir # Subscribe to receive the result Phoenix.PubSub.subscribe(MyApp.PubSub, "agent:#{session_id}:responses") # Returns {:ok, request_id} immediately — agent works in the background {:ok, req_id} = Alloy.Agent.Server.send_message(agent, "Summarise this report", request_id: "req-123" ) # Handle the result whenever it arrives def handle_info({:agent_response, %{text: text, request_id: "req-123"}}, socket) do {:noreply, assign(socket, :response, text)} end ``` ## Providers | Vendor | Recommended Module | Example Models | |--------|---------------------|----------------| | Anthropic | `Alloy.Provider.Anthropic` | `claude-opus-4-6`, `claude-sonnet-4-6`, `claude-haiku-4-5` | | Gemini | `Alloy.Provider.Gemini` | `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-3-pro-preview`, `gemma-4-26b-a4b-it` (open-weight) | | OpenAI | `Alloy.Provider.OpenAI` | `gpt-5.4` | | xAI | `Alloy.Provider.OpenAI` with `api_url: "https://api.x.ai"` | `grok-4.20-0309-reasoning`, `grok-4.20-multi-agent-0309`, `grok-4.1-fast-reasoning`, `grok-code-fast-1` | | Other OpenAI-compatible APIs | `Alloy.Provider.OpenAICompat` | `kimi-k2.6` (Moonshot), `qwen3-coder-plus` (1M ctx), `glm-4.6`, `mistral-large-2512`, plus Ollama, OpenRouter, DeepSeek, Groq, Together | Use `Alloy.Provider.OpenAI` for native Responses APIs like OpenAI and xAI. Use `Alloy.Provider.Gemini` for Gemini's native GenerateContent API. Use `Alloy.Provider.OpenAICompat` for chat-completions compatible APIs and local runtimes. `OpenAICompat` works with any API that implements the OpenAI chat completions format. Just set `api_url`, `model`, and optionally `api_key` and `chat_path`. ## Built-in Tools | Tool | Module | Description | |------|--------|-------------| | **read** | `Alloy.Tool.Core.Read` | Read files from disk | | **write** | `Alloy.Tool.Core.Write` | Write files to disk | | **edit** | `Alloy.Tool.Core.Edit` | Search-and-replace editing | | **bash** | `Alloy.Tool.Core.Bash` | Execute shell commands (restricted shell by default) | > **Bash is a guardrail, not a sandbox.** Restricted mode (`bash -r`) blocks > `cd`, `PATH` changes, and redirection — but any interpreter on `PATH` > (`python3 -c`, `perl -e`) gives full capability, so treat it as a speed > bump against accidents, not an isolation boundary. For real isolation, > supply your own `:bash_executor` in context (container, jail, firejail, or > a remote runner) and restrict file tools with `:allowed_paths`. Treat the > agent's shell access as you would a contractor's laptop on your network. ### Custom tools ```elixir defmodule MyApp.Tools.WebSearch do @behaviour Alloy.Tool @impl true def name, do: "web_search" @impl true def description, do: "Search the web for information" @impl true def input_schema do %{ type: "object", properties: %{query: %{type: "string", description: "Search query"}}, required: ["query"], additionalProperties: false } end @impl true def strict?, do: true @impl true def execute(%{"query" => query}, _context) do # Your implementation here {:ok, "Results for: #{query}"} end end ``` Prefer strict mode for tools that have stable schemas. Set `strict?/0` on a module tool, or `strict: true` on an inline tool, and include `additionalProperties: false` in the top-level JSON Schema. Alloy validates that requirement instead of silently rewriting your schema. OpenAI strict mode also requires every property to be listed in `required`. For Anthropic advanced tool use, add `input_examples/0` or `input_examples:` with representative argument maps to improve parameter selection. Set `defer_loading?/0` or `defer_loading: true` for tools that should be exposed through Anthropic's deferred-loading path; other providers ignore these fields. ### Inline tools For one-off tools, or tools discovered at runtime (an MCP server's tool list, user-defined actions from a database), define a tool as data with `Alloy.Tool.inline/1` — no module needed. Inline tools and tool modules mix freely in `tools:`: ```elixir weather = Alloy.Tool.inline( name: "get_weather", description: "Get current weather for a location", input_schema: %{ type: "object", properties: %{location: %{type: "string"}}, required: ["location"] }, execute: fn %{"location" => loc}, _context -> {:ok, MyApp.Weather.current(loc)} end ) {:ok, result} = Alloy.run("What's the weather in Sydney?", provider: provider, tools: [weather, Alloy.Tool.Core.Read] ) ``` Optional fields mirror the behaviour's optional callbacks: `concurrent?:`, `max_result_chars:`, `allowed_callers:`, `result_type:`. See `Alloy.Tool.Inline` for details. ### Code execution (Anthropic) Enable Anthropic's server-side code execution sandbox: ```elixir {:ok, result} = Alloy.run("Calculate the first 20 Fibonacci numbers", provider: {Alloy.Provider.Anthropic, api_key: "...", model: "claude-sonnet-4-6"}, code_execution: true ) ``` ## Architecture ``` Alloy.run/2 One-shot agent loop (pure function) Alloy.Agent.Server GenServer wrapper (stateful, supervisable) Alloy.Agent.Turn Single turn: call provider → execute tools → return Alloy.Provider Behaviour: translate wire format ↔ Alloy.Message Alloy.Tool Behaviour: name, description, input_schema, execute Alloy.Middleware Pipeline: custom hooks, tool blocking Alloy.Context.Compactor Automatic conversation summarization ``` Sessions, persistence, multi-agent coordination, scheduling, skills, and UI belong in your application layer. See [Anvil](https://github.com/alloy-ex/anvil) for a reference Phoenix application built on Alloy. ## License MIT — see [LICENSE](LICENSE). ## Releases Hex.pm publishing is handled by GitHub Actions on `v*` tags. Successful publishes also dispatch the landing-site version sync workflow.