BeamAI - Erlang Agent Framework

August 25, 2026 · View on GitHub

License Erlang/OTP Build

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

ProviderModuleAPI ModeDescription
anthropicbeamai_llm_provider_anthropicAnthropicAnthropic Claude API
openaibeamai_llm_provider_openaiOpenAIOpenAI API
deepseekbeamai_llm_provider_deepseekOpenAI compatibleDeepSeek API
zhipubeamai_llm_provider_zhipuOpenAI compatibleZhipu AI (GLM series)
dashscopebeamai_llm_provider_dashscopeDashScope nativeAlibaba Cloud DashScope (Qwen series)
ollamabeamai_llm_provider_ollamaOpenAI compatibleOllama 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

Module Documentation

ModuleDescriptionDocumentation
beamai_coreCore framework: ChatClient, Context, Filter, Tool, HTTP, BehavioursREADME
beamai_agentSimpleAgent: ReAct Agent framework (multi-turn, callbacks, interrupt/resume)README (zh)
beamai_llmLLM client: 6 providers with unified sync/streaming; multimodal input, Anthropic caching/Web Search/citations, rate-limit headers, Retry-After retries, unified error structureREADME

Running Examples

# Compile
rebar3 compile

# Start Shell
rebar3 shell

Project Statistics

MetricCount
OTP Applications3 (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!