Erlang ADK v0.2.1

An Erlang-native Agent Development Kit (ADK) designed to bring the capabilities of Google ADK 2.0 to the Erlang/OTP ecosystem. It leverages Erlang's robust OTP framework (processes, gen_server, and supervisors) to provide a scalable, observable, and highly concurrent multi-agent system.

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


Features (ADK 2.0 Feature Parity)


Quickstart

Add erlang_adk as a dependency in your rebar.config:

{deps, [
erlang_adk,
{gun, "2.1.0"} %% Required for streaming capabilities
]}.

Ensure the application is started:

application:ensure_all_started(erlang_adk).

Before spawning an agent, make sure your API key is available:

export GEMINI_API_KEY="your_api_key_here"

Basic LLM Agent

Spawn an agent and prompt it:

%% Define agent configuration
Config = #{
name => <<"WeatherAgent">>,
description => <<"An agent that checks the weather.">>,
model => <<"gemini-1.5-flash">>
},
%% Spawn the agent
{ok, Pid} = erlang_adk:spawn_agent(Config, [], [my_weather_tool]),
%% Prompt the agent
{ok, Response} = erlang_adk:prompt(Pid, <<"What is the weather in Tokyo?">>).
io:format("Response: ~s~n", [Response]).

Graph-Based Workflows

Agents and functions can be orchestrated using Graph Workflows (adk_graph), which decouple logic from LLM reasoning. This allows complex control flow (loops, branches, retries).

%% 1. Create a new graph
G0 = adk_graph:new(<<"MyWorkflow">>, []),
%% 2. Define a simple function node
NodeFn = fun(State) -> #{<<"count">> => maps:get(<<"count">>, State, 0) + 1} end,
G1 = adk_graph:add_node(G0, adk_graph_node:function_node(<<"increment">>, NodeFn)),
%% 3. Add a conditional edge
CondFn = fun(State) ->
if maps:get(<<"count">>, State) < 3 -> <<"increment">>;
true -> <<"end_node">>
end
end,
G2 = adk_graph:add_edge(G1, <<"increment">>, cond_target, CondFn),
%% 4. Execute the workflow
{ok, Compiled} = adk_graph:compile(G2),
{ok, FinalState} = adk_graph:execute(Compiled, #{<<"count">> => 0}).

Multi-Agent Systems & Agent-as-Tool

An entire agent can act as a tool for another agent using adk_agent_tool. This allows a master agent to delegate complex tasks to sub-agents.

%% 1. Spawn a sub-agent
SubConfig = #{name => <<"ResearchAgent">>, description => <<"Finds info">>},
{ok, SubPid} = erlang_adk:spawn_agent(SubConfig, [], [search_tool]),
%% 2. Wrap it as a tool
AgentTool = adk_agent_tool:new(SubPid, #{skip_summarization => false}),
%% 3. Spawn a master agent equipped with the sub-agent
MasterConfig = #{name => <<"Master">>, description => <<"Coordinates research">>},
{ok, MasterPid} = erlang_adk:spawn_agent(MasterConfig, [], [AgentTool]),
%% 4. Prompt the master agent
{ok, Result} = erlang_adk:prompt(MasterPid, <<"Research quantum computing.">>).

Human-in-the-Loop (HITL)

Suspend execution and wait for human input using the built-in pause mechanism (adk_long_running_tool).

%% 1. Wrap a dangerous tool to require human approval
SafeTool = adk_long_running_tool:new(format_hard_drive_tool),
%% 2. The agent executes it and pauses
Result = adk_tool:execute(SafeTool, #{<<"disk">> => <<"C:">>}),
%% -> returns {pending, PauseToken} and hibernates the agent
%% 3. The UI or a human reviews the action and resumes the agent
adk_runner:resume(Runner, UserId, SessionId, PauseToken, <<"Approved">>).

Session Management

ADK 2.0 introduces Scoped State prefixes for advanced state management across turns:

StateDelta = #{
<<"user:preferences">> => <<"dark_mode">>,
<<"temp:cache">> => <<"transient_data">>
},
adk_session_service:update_state(<<"MyApp">>, <<"User1">>, <<"SessionX">>, StateDelta).

Long-term Memory

Automatically extract facts from sessions and index them into long-term memory for future retrieval.

%% Add a completed session to long-term memory
adk_memory_service:add_session_to_memory(Session),
%% Later, search memory for context
{ok, Memories} = adk_memory_service:search(Pid, <<"user prefers dark mode">>, #{}, 5).

MCP Integration

MCP Client (Connecting to External Servers)

Connect to any external Model Context Protocol (MCP) server (e.g. running on Node.js or Python) and use its tools in Erlang.

%% Connect via stdio
{ok, Client} = adk_mcp_client:connect(<<"stdio">>, <<"node server.js">>),
%% Wrap all external tools as Erlang ADK tools
McpTools = adk_mcp_client:as_adk_tools(Client),
%% Pass them to your agent
erlang_adk:spawn_agent(Config, [], McpTools).

MCP Server (Exposing Agents as Tools)

Start an MCP server in Erlang to expose your agents and functions to MCP clients like Claude Desktop.

%% Start an SSE MCP server exposing specific tools/agents
{ok, Server} = adk_mcp_server:start(<<"sse">>, [my_erlang_tool, AgentTool]),

Callbacks

Hook into the lifecycle of agents and tools. Callbacks can even override tool results or skip execution entirely.

-module(my_audit_callback).
-behaviour(adk_callbacks).
before_tool(ToolName, Args, _Context) ->
io:format("Executing ~s with ~p~n", [ToolName, Args]),
ok. %% Return {skip, Reason} to prevent execution

Attach callbacks via agent config:

Config = #{callbacks => [my_audit_callback]},

Evaluation Framework

Test your agents against deterministic JSON datasets.

%% Define test configuration
EvalConfig = #{
criteria => #{tool_trajectory_avg_score => 0.8},
match_type => <<"in_order">>
},
%% Run the evaluation suite
{ok, Results} = adk_eval:evaluate(AgentConfig, "test_data.json", EvalConfig),
io:format("Score: ~p~n", [Results]).

Streaming

Stream responses directly from the LLM (useful for chat UIs).

Callback = fun(Chunk) -> io:format("~s", [Chunk]) end,
{ok, StreamPid} = adk_llm_gemini:stream(LLMConfig, History, Tools, Callback).

Retry Configuration

Wrap tools or API calls with robust exponential backoff.

%% Configure max 5 attempts, 1000ms base delay, 2.0x multiplier
RetryConf = adk_retry:new(#{max_attempts => 5, backoff_ms => 1000, backoff_mult => 2.0}),
%% Execute a flaky function
adk_retry:with_retry(fun() -> flaky_network_call() end, RetryConf).

Observability (Telemetry)

The Erlang ADK uses the telemetry library. You can monitor latency and attach handlers to these events:

telemetry:attach(
<<"my-agent-handler">>,
[erlang_adk, agent, prompt, stop],
fun(EventName, Measurements, Metadata, _Config) ->
io:format("Agent took ~p microseconds~n", [maps:get(duration, Measurements)])
end,
[]
).

Testing

Run the full test suite using rebar3:

rebar3 do clean, compile, ct, dialyzer, ex_doc, edoc