Erlang ADK

View Source

An Erlang-native Agent Development Kit (ADK) inspired by Google ADK. It leverages Erlang's robust OTP framework (processes, gen_server, and supervisors) to provide a scalable and observable multi-agent system.

It features native integration with Google Gemini, allowing your agents to interact with real LLMs.

Features

  • OTP-Native: Agents are highly concurrent, fault-tolerant gen_server processes.
  • Supervision: Managed by dynamically scaling simple_one_for_one supervisors.
  • Pluggable LLMs: A clean adk_llm behaviour for swapping LLM providers. Currently includes native support for Google Gemini.
  • Memory Management: Agents maintain state and conversational history automatically.
  • Agent Orchestrators: Compose agents together with native Sequential, Parallel, and Loop topologies.
  • Session Persistence: Built-in ETS-backed (in-memory) and Mnesia-backed (disk-distributed) memory storage allows agents to crash and recover their conversational memory seamlessly.
  • Agent-to-Agent (A2A) Communication: Remote collaboration between agents via HTTP.
  • Observability: Built-in Telemetry hooks for monitoring agent latency and LLM generation times.

Quickstart

Add erlang_adk as a dependency in your rebar.config:

{deps, [
    erlang_adk
]}.

Ensure the application is started in your code:

application:ensure_all_started(erlang_adk).

Usage

Before spawning an agent that uses Gemini, make sure your API key is available in your system environment variables:

export GEMINI_API_KEY="your_api_key_here"

Alternatively, you can pass the API key explicitly in the agent's configuration map.

Sample Configuration

To spawn an agent, you define an LLMConfig map. This allows you to configure "all the bells and whistles" for the underlying model.

LLMConfig = #{
    %% Required: The provider module
    provider => adk_llm_gemini,
    
    %% Required: The system instructions for the agent
    instructions => "You are a senior Erlang engineer. Be concise and helpful.",
    
    %% Optional: LLM Generation parameters
    temperature => 0.7,
    max_tokens => 1024,
    top_p => 0.9,
    top_k => 40,
    
    %% Optional: Specific model version (Defaults to gemini-1.5-flash)
    model => <<"gemini-1.5-pro">>,
    
    %% Optional: Pass API key directly instead of using GEMINI_API_KEY env var
    api_key => <<"AIzaSyYourKeyHere...">>,
    
    %% Optional: Session ID for memory persistence (ETS by default)
    session_id => my_persistent_session_id,
    
    %% Optional: Session store backend (defaults to erlang_adk_session for ETS)
    %% To use Mnesia for disk-distributed persistence, set it here:
    session_store => erlang_adk_session_mnesia
}.

Spawning and Prompting an Agent

Use the erlang_adk module to interact with the framework:

%% 1. Spawn the agent
%% Note: The 3rd argument is a list of tools (e.g., custom Erlang modules implementing adk_tool)
{ok, Pid} = erlang_adk:spawn_agent("ErlangExpert", LLMConfig, []).

%% 2. Synchronously prompt the agent (waits for a response)
{ok, Response} = erlang_adk:prompt(Pid, "Explain OTP Supervisors in one sentence.").
io:format("~ts~n", [Response]).

%% 3. Asynchronously delegate a task (fire and forget)
erlang_adk:delegate(Pid, "Read through the logs and cache the errors in the DB.").

%% 4. Asynchronously delegate and get notified via message passing when done
erlang_adk:delegate(Pid, "Write a long report...", self()),

%% ... later in your application code ...
receive
    {agent_response, Pid, AsyncResponse} ->
        io:format("Background agent finished!~n~ts~n", [AsyncResponse])
after 10000 ->
    io:format("Still waiting...~n")
end.

Tools / Function Calling

Agents can be equipped with custom Erlang modules that act as tools (functions the LLM can call). To create a tool, create a module that exports schema/0 and execute/1:

-module(my_weather_tool).
-export([schema/0, execute/1]).

schema() ->
    #{<<"name">> => <<"get_weather">>,
      <<"description">> => <<"Get the current weather for a location">>,
      <<"parameters">> => 
          #{<<"type">> => <<"OBJECT">>,
            <<"properties">> => 
                #{<<"location">> => #{<<"type">> => <<"STRING">>}},
            <<"required">> => [<<"location">>]}
     }.

execute(#{<<"location">> := Location}) ->
    %% Call a weather API here...
    #{<<"temperature">> => <<"72F">>, <<"condition">> => <<"Sunny">>}.

Pass the tool module when spawning the agent:

{ok, WeatherBot} = erlang_adk:spawn_agent("WeatherBot", LLMConfig, [my_weather_tool]).

%% The agent will automatically call your Erlang code under the hood!
erlang_adk:prompt(WeatherBot, "What's the weather like in Tokyo?").

Agent Orchestrators

You can compose multiple agents into complex workflows:

%% Sequential Pipeline: Pass output from Agent 1 to Agent 2
{ok, FinalResult} = erlang_adk:sequential([Agent1Pid, Agent2Pid], "Initial Prompt").

%% Parallel Execution: Fan out requests concurrently
Results = erlang_adk:parallel([Agent1Pid, Agent2Pid], "Research topic X").
%% Results is [{Agent1Pid, Response1}, {Agent2Pid, Response2}]

%% Loop / Refiner: Worker writes, Reviewer critiques. Repeats up to MaxIterations.
{ok, ApprovedDraft} = erlang_adk:loop(WorkerPid, ReviewerPid, "Write an essay", 3).

Observability (Telemetry)

The Erlang ADK uses the telemetry library to emit events, allowing you to monitor agent latencies and interactions easily.

For example, adk_agent.erl emits the following events when processing prompts:

  • [erlang_adk, agent, prompt, start]
  • [erlang_adk, agent, prompt, stop] (includes duration measurements)

To monitor these events, you can attach a telemetry handler in your application. Here is a quick Erlang snippet demonstrating how to attach a telemetry:attach/4 handler:

%% Define your handler function
handle_event([erlang_adk, agent, prompt, stop], Measurements, _Metadata, _Config) ->
    Duration = maps:get(duration, Measurements),
    %% You can log the duration or send it to a metrics backend
    io:format("Agent prompt finished in ~p native time units~n", [Duration]).

%% Attach the handler (e.g., during your application startup)
telemetry:attach(
    <<"my-adk-telemetry-handler">>,
    [erlang_adk, agent, prompt, stop],
    fun ?MODULE:handle_event/4,
    #{}
).

Agent-to-Agent (A2A) Communication

The Erlang ADK supports remote Agent-to-Agent communication over HTTP, allowing agents distributed across different nodes or microservices to collaborate. The ADK spins up a Cowboy HTTP listener on port 8080 to accept remote prompts.

Calling a Remote Agent

If an agent named "Alice" is running on a server at http://agent-node:8080, you can prompt it remotely from another node using the erlang_adk_a2a_client:

Url = "http://agent-node:8080/a2a/prompt",
case erlang_adk_a2a_client:prompt(Url, "Alice", "Hello from a remote node!") of
    {ok, Response} -> io:format("Alice says: ~ts~n", [Response]);
    {error, Reason} -> io:format("A2A Error: ~p~n", [Reason])
end.

Running the Demo

This project includes a multi-agent demo (examples/demo.erl) where an "Alice" Writer agent and a "Bob" Reviewer agent collaborate.

To run it, start the rebar3 shell:

$ export GEMINI_API_KEY="your_actual_key"
$ rebar3 shell

Then compile and run the demo from inside the shell:

1> c("examples/demo.erl").
{ok,demo}
2> demo:run().