BeamAI - Erlang Agent Framework
August 25, 2026 · View on GitHub
English | 中文
A high-performance AI Agent framework core library based on Erlang/OTP, providing foundational capabilities for building Agents.
Note: This project is the core library of the BeamAI framework, providing ChatClient, Filter (incl. conversation memory), LLM Client, and SimpleAgent core features.
Extension features (Tools Library, RAG, A2A/MCP protocols) live in the beamai_extra extension project.
The Process Framework orchestration engine and the storage/snapshot engine (formerly
beamai_process/beamai_memory) have been removed — neither repository contains them any more.
Core vs Extension
Core Project (This Repository)
Foundational infrastructure for building AI Agents (three core responsibilities):
- beamai_core - ChatClient foundation: Context, Filter (onion-style around model), Tool construction and invocation
- beamai_agent - SimpleAgent: a primarily ReAct-based Agent framework (cross-turn memory via filter-memory, multi-turn conversations, callbacks, interrupt/resume)
- beamai_llm - Unified LLM client (supports OpenAI, Anthropic, DeepSeek, Zhipu, DashScope, Ollama)
Extension Project (beamai_extra)
Advanced features built on top of the core library:
- beamai_tools - File, Shell, Todo and human-interaction tools + the Middleware system
- beamai_rag - Retrieval-Augmented Generation
- beamai_mcp - MCP (Model Context Protocol)
- beamai_a2a - A2A (Agent-to-Agent) protocol
Features
-
ChatClient/Tool Architecture: Semantic tool registration and invocation system
- ChatClient core based on Semantic Kernel concepts (stateless, does not record messages)
- Unified Tool definition and management
- Onion-style Filter interception and security validation
-
Conversation Memory (Memory Filter): history decoupled from the ChatClient
- Each invoke passes only the latest message; history managed by the Memory Filter keyed by
conversation_id - Pluggable storage backends (ETS / DETS persistence / custom behaviour); the sliding window is provided by the Agent-side memory provider
- See docs/MEMORY_EN.md
- Each invoke passes only the latest message; history managed by the Memory Filter keyed by
-
Unified LLM Client: 6 providers with unified sync/streaming
- OpenAI, Anthropic, DeepSeek, Zhipu, DashScope, Ollama
- Multimodal input, Anthropic caching/Web Search/citations, rate-limit headers, Retry-After retries, unified error structure
-
Output Parser: Structured output
- JSON/XML/CSV parsing
- Automatic retry mechanism
Quick Start
1. Start Shell
export ZHIPU_API_KEY=your_key_here
rebar3 shell
2. LLM Call
%% Create LLM configuration
LLM = beamai_chat_model:create(zhipu, #{
model => <<"glm-4.7">>,
api_key => list_to_binary(os:getenv("ZHIPU_API_KEY"))
}),
%% Send chat request
{ok, Response} = beamai_chat_model:chat(LLM, [
{role, user, content, <<"你好!"/utf8>>}
]),
3. ChatClient + Tool (Tool Registration)
%% Create ChatClient
ChatClient = beamai_chat_client:new(),
%% Define Tool
SearchTool = #{
name => <<"search">>,
description => <<"Search for information">>,
parameters => #{
<<"query">> => #{type => string, required => true, description => <<"Search keywords">>}
},
handler => fun(#{<<"query">> := Query}, _Context) ->
{ok, <<"Search result: ", Query/binary>>}
end
},
%% Register tool
ChatClient1 = beamai_chat_client:add_tool(ChatClient, SearchTool),
%% Invoke a single tool
{ok, Result, _NewCtx} = beamai_tool_executor:invoke(ChatClient1, <<"search">>, #{
<<"query">> => <<"Erlang"/utf8>>
}, beamai_context:new()).
4. Filter (Onion-style Interception)
%% A filter has 4 optional around hooks (outer→inner: around_turn/around_step/
%% around_chat/around_tool). The tool loop is itself a link on the turn chain —
%% its next is one iteration (around_step); provider retry sits below the stack.
%% Each around wraps a single invocation with one closure fun(Req, FCtx, Next) -> Resp;
%% pre/post logic lives in one place, and not calling Next short-circuits.
%% Filters are given once when the ChatClient is built; registration order is layer
%% order (earlier in the list = more outer).
%% One around_tool: arg validation (short-circuit) + double the result
ValidateTransform = beamai:filter(<<"validate_transform">>, #{
around_tool => fun(#{args := #{a := A}, context := Ctx} = Req, _FCtx, Next) ->
case A > 1000 of
true ->
%% Over limit: skip tool execution by not calling Next
#{result => {error, <<"a exceeds limit">>}, context => Ctx};
false ->
%% Normal: enter inner layer then double the result
#{result := Result} = Resp = Next(Req),
case is_number(Result) of
true -> Resp#{result => Result * 2};
false -> Resp
end
end
end
}),
K0 = beamai:chat_client(#{}, [ValidateTransform]),
K1 = beamai:add_tool(K0, beamai:tool(<<"add">>,
fun(#{a := A, b := B}) -> {ok, A + B} end,
#{description => <<"Add two numbers">>,
parameters => #{
a => #{type => integer, required => true},
b => #{type => integer, required => true}
}})),
%% Invoke (3 + 5 = 8, doubled in post = 16)
{ok, 16, _} = beamai:invoke_tool(K1, <<"add">>, #{a => 3, b => 5}, beamai:context()).
See the Filter docs.
Process orchestration / state snapshots (the Process Framework and storage/snapshot engine) have been removed and exist in neither repository.
5. Output Parser (Structured Output)
%% Create JSON parser
Parser = beamai_output_parser:json(#{
schema => #{
type => object,
properties => #{
<<"title">> => #{type => string},
<<"count">> => #{type => integer}
},
required => [<<"title">>, <<"count">>]
}
}),
%% Parse LLM response
{ok, Parsed} = beamai_output_parser:parse(Parser, LLMResponse).
Architecture
Application Structure
apps/
├── beamai_core/ # Core framework
│ ├── ChatClient # beamai_chat_client, beamai_tool, beamai_context,
│ │ # beamai_filter, beamai_prompt, beamai_result
│ ├── Memory Filter # beamai_memory_filter (history keyed by conversation_id)
│ ├── HTTP # beamai_http, beamai_http_gun, beamai_http_pool
│ ├── Behaviours # beamai_chat_behaviour, beamai_http_behaviour
│ └── Utils # beamai_id, beamai_jsonrpc, beamai_sse, beamai_utils
│
├── beamai_llm/ # LLM client
│ ├── Chat # beamai_chat_model, beamai_llm_error
│ ├── Parser # beamai_output_parser, beamai_parser_json
│ ├── Adapters # beamai_llm_message_adapter, beamai_llm_response_parser, beamai_llm_tool_adapter
│ └── Providers # OpenAI, Anthropic, DeepSeek, Zhipu, DashScope, Ollama
│
└── beamai_agent/ # SimpleAgent (ReAct)
└── Agent # beamai_agent, beamai_agent_state, beamai_agent_tool_loop,
# beamai_agent_callbacks, beamai_agent_interrupt
The process-orchestration engine and storage/snapshot engine (formerly
beamai_process/beamai_memory) have been removed; neither beamai nor beamai_extra contains them.
Dependency Relationships
┌───────────────────────┐ ┌───────────────────────┐
│ Agent Layer │ │ LLM Layer │
│ (beamai_agent) │ │ (beamai_llm) │
└───────────┬───────────┘ └───────────┬───────────┘
│ │
┌───────────┴─────────────────────────┴───────────┐
│ Core Layer │
│ (beamai_core) │
└─────────────────────────────────────────────────┘
beamai_core is decoupled via Behaviour interfaces and
{Module, Ref}dynamic dispatch, with no dependency on upper-layer apps. beamai_llm and beamai_agent are peers with no mutual dependency.
See DEPENDENCIES_EN.md for details.
Core Concepts
1. ChatClient Architecture
ChatClient is BeamAI's core abstraction, managing Tool registration and invocation:
%% Create ChatClient instance
ChatClient = beamai_chat_client:new(),
%% Load tool from a Tool module
ChatClient1 = beamai_chat_client:add_tool_module(ChatClient, beamai_tool_file),
%% Or add a single tool
Tool = #{
name => <<"read_file">>,
description => <<"Read file content">>,
parameters => #{
<<"path">> => #{type => string, required => true}
},
handler => fun(#{<<"path">> := Path}, _Ctx) ->
file:read_file(Path)
end
},
ChatClient2 = beamai_chat_client:add_tool(ChatClient1, Tool),
%% Invoke the registered tool
{ok, Result, _NewCtx} = beamai_tool_executor:invoke(ChatClient2, <<"read_file">>, #{
<<"path">> => <<"/tmp/test.txt">>
}, beamai_context:new()).
2. Conversation Memory (Memory Filter)
The ChatClient itself is stateless and does not record messages; multi-turn conversation history is managed by the Memory Filter (beamai_memory_filter) keyed by conversation_id. Each invoke carries only the latest message; the filter injects history and persists deltas.
- Pluggable storage backends (ETS / DETS persistence / custom behaviour); the sliding window is provided by the Agent-side memory provider
- SimpleAgent's cross-turn memory is built on this
- See docs/MEMORY_EN.md
Configuration
LLM Configuration
LLM configuration is created using beamai_chat_model:create/2:
%% Create LLM configuration
LLM = beamai_chat_model:create(zhipu, #{
model => <<"glm-4.7">>,
api_key => list_to_binary(os:getenv("ZHIPU_API_KEY")),
temperature => 0.7
}).
%% Send request
{ok, Response} = beamai_chat_model:chat(LLM, [
{role, user, content, <<"你好"/utf8>>}
]).
Supported Providers:
| Provider | Module | API Mode | Description |
|---|---|---|---|
anthropic | beamai_llm_provider_anthropic | Anthropic | Anthropic Claude API |
openai | beamai_llm_provider_openai | OpenAI | OpenAI API |
deepseek | beamai_llm_provider_deepseek | OpenAI compatible | DeepSeek API |
zhipu | beamai_llm_provider_zhipu | OpenAI compatible | Zhipu AI (GLM series) |
dashscope | beamai_llm_provider_dashscope | DashScope native | Alibaba Cloud DashScope (Qwen series) |
ollama | beamai_llm_provider_ollama | OpenAI compatible | Ollama local models |
HTTP Backend Configuration
BeamAI's HTTP backend is pluggable via beamai_http_behaviour. Gun (with HTTP/2
support) is the only built-in implementation and the default — no configuration needed.
%% Configure in sys.config (optional); the Gun backend runs three
%% purpose-shaped pools (short requests / SSE streaming / async
%% long-polling) — set only the pools and keys you want to override.
%% The legacy http_pool key still works. See docs/HTTP_EN.md
{beamai_core, [
{http_backend, beamai_http_gun},
{http_pools, #{
http_pool_stream => #{max_connections_per_host => 20,
idle_timeout => 120000}
}}
]}.
Everything runs on the Gun backend (beamai_http_gun): HTTP/2 support, built-in
purpose-shaped pools (beamai_http_pool instances), and system CA certificates for
TLS. The backend itself is pluggable via beamai_http_behaviour, but Gun is the only
built-in implementation and needs no configuration.
Documentation
Core Documentation
- docs/API_REFERENCE_EN.md - API Reference
- docs/FILTER_EN.md - Filter System Documentation
- docs/MEMORY_EN.md - Conversation Memory (Memory Filter) documentation
- docs/HTTP_EN.md - HTTP connection pools (purpose-shaped pools, config & tuning)
- docs/OUTPUT_PARSER.md - Output Parser Guide
- docs/DEPENDENCIES_EN.md - Dependency Relationship Details
Module Documentation
| Module | Description | Documentation |
|---|---|---|
| beamai_core | Core framework: ChatClient, Context, Filter, Tool, HTTP, Behaviours | README |
| beamai_agent | SimpleAgent: ReAct Agent framework (multi-turn, callbacks, interrupt/resume) | README (zh) |
| beamai_llm | LLM client: 6 providers with unified sync/streaming; multimodal input, Anthropic caching/Web Search/citations, rate-limit headers, Retry-After retries, unified error structure | README |
Running Examples
# Compile
rebar3 compile
# Start Shell
rebar3 shell
Project Statistics
| Metric | Count |
|---|---|
| OTP Applications | 3 (beamai_core, beamai_agent, beamai_llm) |
| Source Modules | ~73 |
| Test Files | ~38 |
| Unit Tests | ~380 |
Running Tests
# Run all tests
rebar3 eunit
# Run tests for a specific app
rebar3 eunit --app=beamai_llm
# Run type checking
rebar3 dialyzer
Performance
- Based on Erlang/OTP lightweight processes
- Concurrent tool invocations
- HTTP connection pool (Gun, supports HTTP/2)
- ETS high-speed storage
Design Principles
- Simple: Clear API, easy to understand
- Modular: Single responsibility for each module
- Extensible: Behaviour design, easy to customize
- High Performance: Leverages Erlang concurrency features
- Observable: Comprehensive logging, tracing, monitoring
License
Apache-2.0
Contributing
Issues and Pull Requests are welcome!