# Erlang ADK
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`:
```erlang
{deps, [
erlang_adk
]}.
```
Ensure the application is started in your code:
```erlang
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:
```bash
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.
```erlang
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:
```erlang
%% 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`:
```erlang
-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:
```erlang
{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:
```erlang
%% 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:
```erlang
%% 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`:
```erlang
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:
```bash
$ export GEMINI_API_KEY="your_actual_key"
$ rebar3 shell
```
Then compile and run the demo from inside the shell:
```erlang
1> c("examples/demo.erl").
{ok,demo}
2> demo:run().
```