Erlang ADK

Erlang ADK (Agent Development Kit) helps you build AI agents in Erlang. An agent can ask a model for a response, call Erlang functions, remember session data, stream progress, and work with other agents. The project supports Gemini, Vertex AI, OpenAI, Anthropic, and selected OpenAI-compatible APIs, including an explicit loopback-only local-server mode.

The current release is v0.10.0. See the v0.10.0 release scope and validation evidence.

Agents and workflows use Erlang's lightweight processes, supervision, and message passing. The project follows useful behavior from Google ADK, but it is designed for OTP rather than copied from the Python package.

What you can build

If you want to...UseStart here
Build an agent that can call Erlang codeAgents and toolsYour first agent, Add an Erlang tool
Switch between model companies or endpointsGemini, Vertex AI, OpenAI, Anthropic, compatible/local APIs, and named provider settingsChoose a model provider
Split work between agentsDelegation, sequential agents, and concurrent agentsRun multiple agents
Control a multi-step job in application codeSequential, parallel, loop, transfer, graph, and resumable workflowsRun a workflow
Keep a run's events and allow pause, resume, or cancellationThe Runner and supervised runsUse the Runner
Ask a person before a sensitive actionHuman approval and resumable workHuman approval and long-running work
Work with text, images, audio, or image/video framesStreaming and media inputStreaming and multimodal input
Build a two-way voice experienceGemini Live, OpenAI Realtime, and one supervised Erlang process per sessionRealtime sessions and browser voice
Save conversations, files, memory, or model contextSession, artifact, memory, and context servicesSessions, artifacts, memory, and context
Configure local runtime services and reusable agentsSupervised runtime-service profiles and schema-v2 JSON/strict-YAML Agent ConfigSessions, artifacts, memory, and context, v0.10 release scope
Connect APIs, MCP servers, or other agentsOpenAPI, Model Context Protocol (MCP), and Agent2Agent (A2A)Integrations
Monitor and test agent behaviorPlugins, OpenTelemetry metrics/traces, and evaluationsPlugins, observability, and evaluation
Sign users in and authorize tool accessOpenID Connect (OIDC), JSON Web Tokens (JWT), and OAuthAuthentication
Develop and operate agents in a browserThe adk command, local developer UI, or Phoenix LiveView companionCLI and local developer UI, Phoenix UI

Core concepts

Requirements

The core Erlang library does not require Elixir or Node.js.

Installation

From this repository:

./rebar3 compile
./rebar3 shell

The shell starts the erlang_adk application automatically.

Use the released Git tag as a dependency:

{deps, [
{erlang_adk,
{git, "https://github.com/hsalap7/erlang_adk.git",
{tag, "v0.10.0"}}}
]}.

For installations from Hex, use {erlang_adk, "0.10.0"} once the package is available in the configured Hex repository.

In an application that does not use the repository shell configuration, start the library before creating agents:

{ok, _Started} = application:ensure_all_started(erlang_adk).

Your first agent

Needs: a Gemini API key and network access.

Export a Gemini API key in the terminal that will run Erlang:

export GEMINI_API_KEY="your_google_api_key"
./rebar3 shell

Then create an agent, send one prompt, print the UTF-8 reply, and stop it:

{ok, Agent} = erlang_adk:spawn_agent(
<<"Helper">>,
#{provider => adk_llm_gemini,
model => <<"gemini-3.1-flash-lite">>,
instructions => <<"Answer clearly and concisely.">>},
[]),
{ok, Reply} = erlang_adk:prompt(
Agent, <<"Explain an OTP supervisor in one sentence.">>),
io:format("~ts~n", [Reply]),
ok = erlang_adk:stop_agent(Agent).

Responses are UTF-8 binaries. Use ~ts for user-facing Unicode text; ~p shows Erlang term syntax instead.

Provider errors are returned as {error, Reason}. A missing key is an error, not a successful text response.

Choose a model provider

For a small trusted Erlang application, the direct provider modules are the shortest setup:

ProviderModuleCredential source
Geminiadk_llm_geminiGEMINI_API_KEY
Vertex AIadk_llm_vertexOAuth access token or trusted Google ADC
OpenAI Responsesadk_llm_openaiOPENAI_API_KEY
Anthropic Messagesadk_llm_anthropicANTHROPIC_API_KEY
OpenAI-compatible Chat Completionsadk_llm_compatibleConfigured explicitly

For production or an application that uses more than one provider, use provider profiles. A profile gives a simple name to a provider, model, endpoint, and credential source. Configure profiles in sys.config or application environment. Agent code then selects the simple profile and model names.

This example configures four request-provider profiles. Replace the placeholder model IDs with models enabled for your accounts:

Profiles = #{
<<"gemini">> =>
#{request_adapter => adk_llm_gemini,
endpoint => gemini,
models => #{<<"chat">> => <<"gemini-3.1-flash-lite">>},
credential => {env, "GEMINI_API_KEY"}},
<<"openai">> =>
#{request_adapter => adk_llm_openai,
endpoint => openai,
models => #{<<"chat">> => <<"YOUR_OPENAI_MODEL_ID">>},
credential => {env, "OPENAI_API_KEY"},
request_options => #{store => false}},
<<"anthropic">> =>
#{request_adapter => adk_llm_anthropic,
endpoint => anthropic,
models => #{<<"chat">> => <<"YOUR_ANTHROPIC_MODEL_ID">>},
credential => {env, "ANTHROPIC_API_KEY"},
request_options =>
#{anthropic_version => <<"2023-06-01">>}},
<<"compatible">> =>
#{request_adapter => adk_llm_compatible,
endpoint => #{scheme => https,
host => <<"models.vendor.example">>,
port => 443,
base_path => <<"/v1">>},
models => #{<<"chat">> => <<"YOUR_VENDOR_MODEL_ID">>},
credential => {env, "VENDOR_API_KEY"},
request_options => #{auth_scheme => bearer}}
},
ok = application:set_env(erlang_adk, provider_profiles, Profiles).

Use a configured profile like this:

{ok, Agent} = erlang_adk:spawn_agent(
<<"ProfileAgent">>,
#{provider => <<"gemini">>,
model => <<"chat">>,
instructions => <<"Answer concisely.">>},
[]),
{ok, Reply} = erlang_adk:prompt(Agent, <<"What is a GenServer?">>),
io:format("~ts~n", [Reply]),
ok = erlang_adk:stop_agent(Agent).

Vertex profiles use a complete projects/PROJECT/locations/LOCATION/publishers/google/models/MODEL resource and the vertex endpoint preset, so trusted configuration owns both authority and path. Keyless local compatible profiles are restricted to numeric 127.0.0.1 or ::1, auth none, and non-Live requests.

See Model provider profiles for Vertex/ADC, OpenAI Realtime, compatible endpoints, structured output, provider-specific options, and production configuration. See Model support for evidence tiers and local-server recipes; it does not promise support for every model a vendor exposes.

Common tasks

The examples that use adk_llm_gemini assume the erlang_adk application is running and GEMINI_API_KEY is exported. Tasks that do not call a model say so explicitly.

Add an Erlang tool

Needs: a Gemini API key for the model call. The tool itself runs locally.

A tool implements adk_tool. Its schema tells the model when and how it can call the function. execute/2 performs the work.

The repository contains a complete example at examples/readme_weather_tool.erl. Load it from the repository shell and pass the module when creating an agent:

{ok, readme_weather_tool} = c("examples/readme_weather_tool.erl"),
{ok, WeatherAgent} = erlang_adk:spawn_agent(
<<"WeatherAgent">>,
#{provider => adk_llm_gemini,
model => <<"gemini-3.1-flash-lite">>,
instructions => <<"Use get_weather when a city is provided.">>},
[readme_weather_tool]),
{ok, WeatherReply} = erlang_adk:prompt(
WeatherAgent, <<"What is the weather in Tokyo?">>),
io:format("~ts~n", [WeatherReply]),
ok = erlang_adk:stop_agent(WeatherAgent).

Tool arguments are checked against the schema before execute/2 runs. Tools can also come from OpenAPI documents, MCP servers, sub-agents, or an external code sandbox.

Run multiple agents

Needs: a Gemini API key. Each agent can make a model request.

Use different agent processes for work that can happen independently. parallel/3 starts monitored workers and returns results in agent order. sequential/2 feeds each response to the next agent.

{ok, Translator} = erlang_adk:spawn_agent(
<<"Translator">>,
#{provider => adk_llm_gemini,
instructions => <<"Translate the input to French.">>}, []),
{ok, Summarizer} = erlang_adk:spawn_agent(
<<"Summarizer">>,
#{provider => adk_llm_gemini,
instructions => <<"Summarize the input in one sentence.">>}, []),
ParallelResults = erlang_adk:parallel(
[Translator, Summarizer], <<"Explain OTP supervision.">>, 60000),
io:format("~p~n", [ParallelResults]),
{ok, PipelineReply} = erlang_adk:sequential(
[Translator, Summarizer],
<<"Erlang processes are lightweight and isolated.">>),
io:format("~ts~n", [PipelineReply]),
ok = erlang_adk:stop_agent(Translator),
ok = erlang_adk:stop_agent(Summarizer).

Also available:

See Planning and runtime safety.

Run a workflow

Needs: nothing outside this repository. This example does not call a model.

Use a workflow when your application should control the steps instead of leaving every transition to a model.

WorkflowSpec = #{
version => 1,
id => <<"onboarding-workflow-v1">>,
kind => sequential,
max_steps => 2,
steps => [
#{id => <<"increment">>,
run => fun(State, _Context) ->
Count = maps:get(<<"count">>, State, 0) + 1,
{output, <<"counted">>, #{<<"count">> => Count}}
end},
#{id => <<"finish">>,
run => fun(_State, Context) ->
<<"counted">> = maps:get(input, Context),
{output, <<"ready">>, #{<<"done">> => true}}
end}
]
},
{ok, Workflow} = erlang_adk:compile_workflow(WorkflowSpec),
{completed, FinalState, Checkpoint} =
erlang_adk:run_workflow(Workflow, #{<<"count">> => 0}),
#{<<"count">> := 1, <<"done">> := true} = FinalState,
<<"ready">> = maps:get(<<"output">>, Checkpoint).

Workflow kinds include sequential, parallel, loop, transfer, and graph. Workflows support cancellation, saved checkpoints, pause/resume, durable retry attempts, optional per-node schemas and state reducers, ordered lifecycle events, and optional Mnesia-backed run history. Checkpoint schema v2 binds resume to a compiled definition fingerprint; legacy v1 checkpoints are accepted for the 0.8-to-0.9 migration and rewritten at the next boundary.

Compiled graphs can be inspected without exposing callbacks or tool arguments:

{ok, Description} = erlang_adk:inspect_graph(Workflow),
{ok, Mermaid} = erlang_adk:render_graph(Workflow, mermaid).

The packaged CLI also provides adk graph validate, adk graph describe, and adk graph render for an already available module's exported zero-arity graph factory.

See Graph workflows, planning, and durable invocations.

Use the Runner

Needs: a Gemini API key for this example.

The Runner keeps events for one application, user, and session. Use it for session history, tool rounds, streaming events, cancellation, background work, and pause/resume.

{ok, RunnerAgent} = erlang_adk:spawn_agent(
<<"RunnerAgent">>,
#{provider => adk_llm_gemini,
instructions => <<"Be concise.">>}, []),
Runner = adk_runner:new(
RunnerAgent, <<"my_app">>, erlang_adk_session,
#{run_timeout => 120000,
max_llm_calls => 8,
max_tool_rounds => 4}),
{ok, RunnerReply} = adk_runner:run(
Runner, <<"user-1">>, <<"session-1">>, <<"Hello">>),
io:format("~ts~n", [RunnerReply]),
ok = erlang_adk:stop_agent(RunnerAgent).

For a run that must outlive the caller, use adk_run:start/5, then subscribe, inspect status, cancel, or resume it through adk_run.

See Durable invocations, scheduled and background runs, and runtime safety.

Human approval and long-running work

An agent tool that needs confirmation can pause when it runs through the Runner or stable-run API. A protected tool node inside a typed workflow also produces a correlated tool_confirmation pause and can resume from its checkpoint. Direct prompt, delegation, and agent-as-tool calls still return tool_confirmation_requires_runner instead of bypassing approval.

A workflow action can independently pause with structured details. Runner runs and workflows can later resume from the returned run ID or checkpoint. This keeps approval separate from model output.

Use these entry points:

NeedAPI
Confirm a tool callTool metadata plus adk_tool_confirmation
Pause and resume a workflowresume_workflow/2,3
Start and resume a durable workflowstart_workflow_invocation/3, resume_workflow_invocation/3
Resume a paused Runner runadk_run:resume/3 or the adk resume CLI command
Request OAuth credentials during a runadk_authorization_flow with adk_suspension

See Durable invocations for complete examples and restart behavior.

Streaming and multimodal input

Needs: a Gemini API key and network access.

Text streaming calls your function once per decoded text chunk:

History = [
#{role => system, content => <<"Be concise.">>},
#{role => user, content => <<"Explain OTP in two sentences.">>}
],
PrintChunk = fun(Chunk) -> io:format("~ts", [Chunk]) end,
ok = adk_llm:stream(
#{provider => adk_llm_gemini,
model => <<"gemini-3.1-flash-lite">>},
History, [], PrintChunk),
io:format("~n").

Use adk_content to combine text with image data or a supported file URI:

TinyPng = base64:decode(
<<"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0l"
"EQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=">>),
{ok, TextPart} = adk_content:text(<<"Describe this image.">>),
{ok, ImagePart} = adk_content:inline_data(<<"image/png">>, TinyPng),
{ok, Prompt} = adk_content:new([TextPart, ImagePart]),
{ok, Result} = adk_llm:generate(
#{provider => adk_llm_gemini,
model => <<"gemini-3.1-flash-lite">>},
[#{role => user, content => Prompt}], []).

For a successful response with no tool call or extra provider details, Result is a text binary or a validated adk_content map. The complete API can also return tool calls, a wrapped result with provider details, or an error.

Realtime sessions and browser voice

Needs: a key for the selected realtime provider. Browser voice also needs the Phoenix companion or an application-owned audio client.

Realtime sessions use a separate API from ordinary REST generation:

ProviderRequest modelRealtime model/input
Geminigemini-3.1-flash-litegemini-3.1-flash-live-preview, 16 kHz PCM input
OpenAIA configured Responses modelA configured Realtime model, 24 kHz PCM input

The core lifecycle is:

  1. configure a Gemini Live or OpenAI Realtime provider profile;
  2. call start_live_session/3 with a session ID and signed-in identity;
  3. subscribe with live_subscribe/3,4;
  4. send text, audio, or image/video frames and acknowledge each delivered event so the session can continue sending; and
  5. unsubscribe and call close_live_session/3.

Use start_live_voice_bridge/4 when a browser or native audio client needs a binary voice connection. The Phoenix companion already connects this bridge to microphone capture, resampling, playback, interruption, and transcripts.

See Provider profiles for Live profile setup. For the simplest end-to-end browser path, follow Starting a Live session for the UI and Testing full-duplex voice.

Sessions, artifacts, memory, and context

Needs: nothing outside this repository for the built-in ETS services.

This local example creates a session, updates its state, reads it, and removes it:

ok = erlang_adk_session:init(),
{ok, _} = erlang_adk_session:create_session(
<<"my_app">>, <<"user-1">>, #{session_id => <<"session-1">>}),
ok = erlang_adk_session:update_state(
<<"my_app">>, <<"user-1">>, <<"session-1">>,
#{<<"theme">> => <<"dark">>}),
{ok, Session} = erlang_adk_session:get_session(
<<"my_app">>, <<"user-1">>, <<"session-1">>),
#{<<"theme">> := <<"dark">>} = maps:get(state, Session),
ok = erlang_adk_session:delete_session(
<<"my_app">>, <<"user-1">>, <<"session-1">>).
DataBuilt-in choicesGuide
Session events and stateETS-backed erlang_adk_session, durable local erlang_adk_session_mnesiaThis section and feature support
Versioned artifactsadk_artifact_ets, adk_artifact_fs, the GCS-compatible adapter, credit/ack transfer, and optional storage splitting by app/user/sessionArtifacts
Long-term memoryadk_memory_ets, adk_memory_mnesia, a bounded vector/hybrid reference adapter, policy hooks, erasure fencing, optional storage splitting, and reliable background writesMemory
Context selection and limitsadk_context, selection rules, compaction, and reuse of stable prompt prefixesContext

The Runner accepts the relevant services in its options. Tools can receive state and data helpers limited to the current app, user, and session without receiving raw storage internals.

The v0.10.0 runtime foundation can start the three local services as one supervised generation. ephemeral_local selects ETS-backed services; durable_local selects Mnesia sessions/memory and filesystem artifacts and requires an absolute artifact_root:

{ok, Bundle} = adk_runtime_service_bundle:start_link(
ephemeral_local, #{}),
{ok, #{session_service := SessionService,
runner_options := RunnerOptions}} =
adk_runtime_service_bundle:runner_spec(Bundle).

The profile chooses trusted adapter modules and accepts only bounded adapter and routing limits. ephemeral_local uses one shared ETS artifact adapter and one shared ETS memory adapter, so each component enforces one global quota across its scopes. durable_local uses exact-scope workers with per-shard quotas and bounded LRU-on-capacity reclamation after the configured idle timeout; filesystem and Mnesia data survive worker reclamation. It also starts, owns, and health-checks a private Mnesia memory-ingestion outbox as part of the same atomic generation, exposes only its validated service/status surface, and injects durable ingestion into the returned Runner options. Pending jobs survive bundle process restarts; stale or unhealthy references fail closed. Registry hydration gates claims by the exact available adapter identity, while bounded rotating due/lease/erasure/terminal indexes prevent whole-table work. Health uses a constant-row sentinel across all four Mnesia tables, and majority mode requires at least two shared nodes. Epoch-bound job IDs permit same-epoch deduplication and post-erasure resubmission; a hard active-plus-terminal cap requires explicit bounded pruning for headroom. Nested options/capabilities are strictly validated and status is redacted. The disabled and ephemeral_local modes do not create a bundle-owned outbox, and legacy named outbox APIs remain compatible by resolving the one durable bundle owner without a duplicate processor. When enabled with the runtime_service_profile application environment key, the bundle is registered as adk_runtime_service_bundle; pass that atom to runner_spec/1. See the 0.10 release contract.

erlang_adk:runtime_runner_spec/0 is the application-level resolver. Standard CLI run/console, evaluation-agent, and developer HTTP paths use its selected session/artifact/memory services. An enabled profile whose bundle is unavailable fails closed instead of falling back to unrelated ETS services.

Integrations

These are integration starting points. A real connection also needs the remote service address, allowed-host policy, and credentials required by that service. Keep those values in application configuration, not in model input.

IntegrationHow to startDetails
OpenAPIDecode an OpenAPI 3.0/3.1 document, call adk_openapi_toolset:compile/2, wrap it with adk_toolset:new/2, and pass it to an agentThe checked example is examples/readme_petstore_openapi.json
MCP clientadk_mcp_client:connect/2,3, list tools, then wrap the client with adk_toolset:new/2Supports stdio and Streamable HTTP; 0.10 adds explicit legacy/modern protocol eras, incremental SSE, OAuth/PKCE helpers, pooling, and atomic catalogs
MCP serveradk_mcp_server:start/2Exposes checked tools, resources, and prompts, including the modern stateless runtime; legacy GET/SSE is an explicit compatibility option
Curated connectorsBuild a registry-only descriptor, bind an application-owned backend, and create the package toolsetGoogle, GitHub, Slack, and Postgres packages enforce reviewed permissions, side-effect class, confirmation, and concurrency metadata; their package suites execute every advertised operation through the registry, Agent Config, and adk_toolset; the connector packages are not yet published
External code executionConfigure adk_code_toolset with an application-owned sandbox adapterCode execution
A2A 1.0 serverConfigure an Agent Card, Runner-backed agent, listener, authentication, and optional task/push stores before application startupIncludes incremental SSE, extended cards, bounded push delivery, and ETS or local Mnesia task snapshots in v0.10.0
A2A 1.0 clientadk_a2a_v1_client:discover/2, then use send/3, callback-driven stream/subscription APIs, or push-config CRUDUnencrypted HTTP can be enabled only for local 127.0.0.1 development

OpenAPI and MCP tools use the same checked tool-call path as local Erlang tools. Credentials are supplied by the application, not by model arguments.

Plugins, observability, and evaluation

Run the checked one-turn evaluation smoke from the CLI:

Needs:GEMINI_API_KEY, because examples/agent.json uses Gemini.

./rebar3 escriptize
_build/default/bin/adk evaluate \
--config examples/agent.json \
--dataset examples/eval.json

See Plugins, observability, and evaluation. That guide also covers the newer adk eval run command for multi-turn evaluation sets, repeated samples, report comparison, and rubric judges.

For job and trace-store APIs, limits, restart semantics, and examples, see the v0.10.0 release contract.

Authentication

Erlang ADK uses the oidcc Erlang package for OpenID Connect and OAuth. The authentication modules support:

Configure identity providers and credential sources before application startup. Use the Phoenix guide for a complete browser login example, and read Security before a public deployment.

CLI and local developer UI

Build the CLI and run the two commands that do not call a model:

./rebar3 escriptize
_build/default/bin/adk doctor
_build/default/bin/adk config validate examples/agent.json

In v0.10.0, config validation uses the reusable adk_agent_config schema-v2 compiler and reports its fingerprint, immutable registry generation, opaque instance ID, and opaque snapshot revision ID. Applications can call adk_agent_config:compile/2 or load_file/2 with an adk_config_registry snapshot. Schema 1 remains accepted for compatibility; schema 2 adds data-only agent-template, credential-profile, runtime-policy, sub-agent, and workflow references. JSON and the strict YAML subset normalize to the same intermediate representation and fingerprint when compiled against the same snapshot. YAML anchors, aliases, tags, directives, merge keys, multi-document input, and non-JSON scalar behavior are rejected.

Fingerprints are stable for the same configuration and immutable snapshot. The revision distinguishes replacements, including branches with the same lineage and generation; independently created non-empty registries also have different lineage provenance. Registry terms carry a private keyed integrity seal, so a structural copy with changed trusted entries is rejected; the seal is never exposed in diagnostics or fingerprints. Registry kinds cover provider, MCP, OpenAPI, tool pack, credential profile, runtime policy, workflow, and agent template descriptors. Agent names use the runtime identifier grammar [A-Za-z_][A-Za-z0-9_]*, reserve user, and are limited to 256 bytes. Agent files contain binary trusted IDs, never credentials, commands, headers, or transport targets. Registry-backed toolsets are the normal tool path; direct module names in tools are disabled unless trusted caller code explicitly enables the legacy compatibility option. Arbitrary adk_llm_* provider module names are likewise disabled by default; fixed provider aliases and registry-backed provider IDs remain the normal path. Toolset references are capped at 64, duplicates are rejected, and accepted references are resolved by one authenticated bulk registry lookup. adk_agent_composition resolves the exact sealed snapshot and materializes sub-agents bottom-up without returning credential descriptors. This is Erlang ADK's constrained JSON/strict-YAML contract, not a visual builder, code generator, or claim of exact compatibility with another ADK's configuration dialect.

The checked configuration uses Gemini. Export its key before running a model request, opening the console, running an evaluation, or starting a model run from the developer UI:

export GEMINI_API_KEY="your_google_api_key"
_build/default/bin/adk run \
--config examples/agent.json \
--message "Explain rest_for_one" \
--user local --session cli-demo
_build/default/bin/adk console \
--config examples/agent.json \
--user local --session cli-console

Start the local developer server in one terminal. Binding to 127.0.0.1 makes it reachable only from this computer:

export ERLANG_ADK_DEV_TOKEN="replace-with-at-least-16-random-characters"
_build/default/bin/adk serve \
--config examples/agent.json \
--ip 127.0.0.1 --port 8080

Open http://127.0.0.1:8080/dev. Keep that terminal running while using the CLI from another terminal. Environment variables are terminal-local, so export the same developer token there before inspecting the server:

export ERLANG_ADK_DEV_TOKEN="replace-with-at-least-16-random-characters"
_build/default/bin/adk inspect agents --url http://127.0.0.1:8080

Enter that same token when the browser UI asks for it. Other commands include adk session, adk resume, adk memory, and adk artifact; run _build/default/bin/adk --help for their arguments.

The simple adk serve command provides agents, runs, sessions, observability, and the local UI shell. When their server-owned services are configured, the same authenticated loopback API adds a bounded compiled-graph catalog, metadata-only trace timelines and graph overlays, and evaluation authoring, job, set, result, and baseline views. Live, artifact, memory, and context-management panels require the owning Erlang application to start those services and expose them through the documented developer configuration. With --config, the CLI compiles the agent before starting the application and contributes its bounded runner_options to developer runs. Trusted application dev_runner_options win key conflicts, and an enabled runtime profile remains authoritative for artifact and memory service references.

Provider payload inspection is separate from the metadata trace store and is off by default. Enable dev_provider_payload_inspection only for an explicit loopback development session: captured request/response/error values are secret-redacted, JSON-normalized, bounded, short-lived, and served behind the developer bearer. Do not enable it as production telemetry or assume redaction is a complete PII policy.

If a command reports developer_api_unavailable with connection_refused, the server is not listening at the selected URL or port.

Phoenix UI

The optional Phoenix 1.8 companion provides authenticated agent runs, human approval, Live operations, browser voice, observability, evaluation views, and server-owned graph/metadata-trace inspection. It runs in the same Erlang runtime as Erlang ADK.

For local development without an external OIDC provider:

(
export MIX_REBAR3="$PWD/rebar3"
export GEMINI_API_KEY="your_google_api_key"
export ADK_UI_LOCAL_AUTH=true
cd examples/phoenix_adk_ui
MIX_ENV=dev mix setup
MIX_ENV=dev iex -S mix phx.server
)

Open http://127.0.0.1:4000/auth/login and choose Continue as local developer. Local authentication is accepted only in MIX_ENV=dev and does not require any OIDC_* variables. iex -S gives you an Erlang/Elixir shell in the same runtime, which is useful when creating a Live session for the voice UI. Stop the server with Ctrl+C twice; the surrounding shell block returns you to the repository root.

For OIDC configuration, realtime voice setup, production TLS/proxy settings, and release commands, follow the Phoenix companion guide.

Deployment assets (v0.10.0)

The v0.10.0 deployment bundle includes an OTP/relx release, a non-root multi-stage Dockerfile, read-only-root mount conventions, liveness/readiness/ drain helpers, render-first Cloud Run and Helm/GKE manifests, explicit-apply CLI/script boundaries, and SBOM, scan, signing, and provenance helpers. Start with the deployment asset guide.

The release has three explicit configuration modes: the closed base release with every HTTP listener disabled; the packaged health-only profile; and an application-owned sys.config that explicitly enables and secures the intended listeners. Cloud Run selects the health-only runtime config at /opt/erlang_adk/etc/health-http.sys.config and consumes the platform-injected PORT; Helm selects the same profile when service.enabled=true and no custom runtime ConfigMap is supplied. It serves only /livez and /readyz; agent, A2A, developer, and legacy prompt routes remain disabled. A Helm runtimeConfig.existingConfigMap must contain the exact sys.config key and is mounted as /opt/erlang_adk/etc/runtime/sys.config; it replaces rather than augments the packaged profile.

The Cloud Run renderer writes both Service- and revision-scope maxScale: "1" annotations and rejects another requested maximum. This is an intended single-replica operating envelope, not a hard singleton lease or proof that two revisions can never overlap during rollout.

Container PID 1 caps the inherited open-file limit at 65536 by default (ERLANG_ADK_NOFILE_CAP, validated from 1024 through 1048576), owns the single readiness/drain/SIGTERM sequence, and reaps BEAM. The generic and Helm drain budget is 30000 ms within Helm's 60-second grace period; Cloud Run uses 3000 ms for its shorter shutdown window. Do not add a duplicate Helm preStop drain.

Deployments may opt into the strict OTLP environment bridge with ERLANG_ADK_OTLP_ENDPOINT; optional OTEL_EXPORTER_OTLP_HEADERS are consumed only when that activation variable is present. The bridge accepts bounded W3C-Baggage-style header encoding with one strict value percent-decoding pass, trims optional whitespace, and fails startup closed on raw semicolons, malformed escapes, decoded invalid UTF-8, duplicate names, or conflicting configuration. It wires metadata-only standard Runner observations to the bounded asynchronous bus even without a local trace store. The bridge forces batch size one; its 3-second HTTP and 4-second exporter bounds must fit below a bus timeout greater than the sum of all final exporter timeouts plus 250 ms. An absent timeout is auto-sized from the final list, including the trace-store exporter; an explicit undersized timeout fails startup.

These files are implementation assets, not a managed deployment service. The final local release image passed a constrained non-root/read-only-root 1 GiB smoke, and a disposable Kind cluster passed both closed/headless and service-enabled health-only Helm modes, including nondefault PORT=18081, 200/200 health, agent-route 404, drain readiness 503 with liveness 200, and graceful pod recovery. The exact image digest and memory/timing evidence are in the v0.10 release ledger. These local checks do not establish GKE/Cloud Run staging, a registry push, generated SBOM/Grype scan, Cosign signing/attestation, provenance verification, or managed Agent Runtime support. Review immutable images/manifests and run the gates matching the actual project, cluster, registry, and trust policy before applying anything.

The Agent Runtime feasibility probe is read-only and makes no managed-service claim. It reads a bounded RFC 6750 bearer token from a named environment variable and sends it to curl through standard-input config, not a command-line argument; identity, lifecycle, network, state, and conformance still require target-environment evidence.

Developer checks

Run commands from the repository root unless a section says otherwise. GitHub Actions runs every non-paid group below on each pull request. For local work, use this minimum:

SituationRun
While editingQuick check
Before any pull requestCore checks
README or example changedCore checks plus README example checks
Phoenix changedCore checks plus Phoenix checks
Documentation, CLI, or packaging changedCore checks plus documentation, CLI, and package checks
Real Gemini behavior changedRun the non-paid checks first, then the paid tests only when intended

Quick check while developing

Use this for a fast compile and README-example smoke test:

./rebar3 compile
./rebar3 eunit --module=readme_examples_test
./rebar3 eunit --module=readme_workflow_examples_test

This is a convenience check, not the full CI gate.

Core checks before opening a pull request

This is the standard Erlang sanity gate. First remove the opt-in flags for paid tests so an earlier shell export cannot turn this into a billable run:

unset ERLANG_ADK_GEMINI_REST ERLANG_ADK_LIVE_GEMINI \
ERLANG_ADK_GEMINI_LIVE
./rebar3 do clean, compile, eunit, ct, dialyzer
./scripts/coverage.sh
./rebar3 xref

Common Test now skips paid provider suites because their opt-in flags are absent. Other unexpected skips should be investigated. coverage.sh resets old data, reruns EUnit and Common Test with coverage, and enforces the repository floor. The repeated test run is intentional: it gathers fresh coverage data. Running rebar3 cover --verbose alone does not execute tests.

The v0.9.0 deterministic release validation succeeded: 242 production and 271 test modules compiled with warnings treated as errors, all 1,495 EUnit tests and all 6 deterministic Common Test cases passed, Dialyzer reported 0 warnings, and ./rebar3 xref reported 0 undefined or deprecated calls or functions. See the v0.9.0 release evidence for the separately tracked coverage, packaging, Phoenix, and paid-provider gates. The Phoenix companion additionally passed 103 ExUnit tests, 40 browser/audio tests, production asset/release assembly, and both release health smokes.

The v0.10.0 release validation included focused durable-runtime checks that passed 46/46 EUnit with compile, xref, and Dialyzer clean; focused evaluation-report parity and size-boundary checks passed 56 tests, including exact API/HTTP/stdout/file parity for an approximately 1.4 MiB report. The four-package offline connector wrapper passed 12/12 source and 12/12 clean- extracted EUnit. The release aggregate passed 1,826/1,826 EUnit, 6 deterministic Common Test cases with 22 expected paid-provider skips, compile/xref, Dialyzer with 0 warnings over 309 project files, and 74.17% line coverage (36,574/49,312; 83 lines over the exact floor). Independent README checks passed 30/30 examples and 4/4 workflows, all three checked modules compiled with erlc -Werror, and ExDoc completed without warnings. Root Hex, verifier, and extracted-package compilation also passed; artifact hashes and post-ledger freshness are reported out of band to avoid self-reference. Exact release evidence and unrun external boundaries are in the v0.10 release ledger.

If README examples changed

Run the two focused modules above and compile the checked example modules with warnings treated as errors:

erlc -Werror -pa _build/default/lib/erlang_adk/ebin -o /tmp \
examples/readme_weather_tool.erl \
examples/readme_live_weather_executor.erl \
examples/readme_stateful_counter_plugin.erl

If the Phoenix application changed

The first setup needs network access:

(
export MIX_REBAR3="$PWD/rebar3"
unset ADK_UI_LOCAL_AUTH
cd examples/phoenix_adk_ui
MIX_ENV=test mix deps.get
MIX_ENV=test mix deps --check-locked
MIX_ENV=test mix assets.setup
MIX_ENV=test mix precommit
MIX_ENV=test elixir ../../scripts/verify_phoenix_hex_audit.exs
)

mix precommit checks formatting, compilation warnings, browser/audio JavaScript, assets, and ExUnit with fake providers. It does not use model quota. The audit verifier accepts only the exact three documented package findings for the two unresolved Cowlib advisories and fails if the set changes.

Documentation, CLI, and package checks

Run these when changing packaging, documentation, or the CLI. GitHub Actions also runs them on every pull request because the repository stays release-ready:

./rebar3 escriptize
_build/default/bin/adk doctor
_build/default/bin/adk config validate examples/agent.json
./rebar3 ex_doc
./rebar3 hex build
./scripts/verify_hex_package.sh
packages/build_connector_packages.sh

The Hex command builds a package locally; it does not publish it. Treat any ExDoc warning as a failure, matching CI. GitHub Actions additionally builds a production Phoenix release and smoke-tests its proxy and direct-TLS modes. The exact commands and required environment are in Releasing.

packages/build_connector_packages.sh is the sole offline package gate for the four curated connectors. It warning-strict compiles/tests source and clean extractions, creates normalized Hex inspection archives, verifies the required core dependency, and rejects checkout leakage. Those archives are not publish inputs, and all four connectors remain unpublished; see the connector package guide.

Optional paid Gemini checks

These commands make real network requests and may use quota or incur cost:

export GEMINI_API_KEY="your_google_api_key"
ERLANG_ADK_GEMINI_REST=1 ./rebar3 ct \
--suite test/readme/readme_live_gemini_SUITE.erl
ERLANG_ADK_GEMINI_LIVE=1 ./rebar3 ct \
--suite test/models/gemini/gemini_live_SUITE.erl

The REST suite uses gemini-3.1-flash-lite. The separate Live suite uses gemini-3.1-flash-live-preview. A skip, provider rejection, or quota error is not a passing provider test. OpenAI, Anthropic, and compatible providers are tested with local fake network connections and response parsing, but this repository does not include paid live suites for them.

See Testing for focused commands and result interpretation.

Troubleshooting

SymptomWhat to check
Text prints as integers or Erlang term syntaxUse io:format("~ts~n", [Reply]) for Unicode text; ~p is for inspecting Erlang terms.
Paid Common Test cases are skippedExport the key and set the matching REST or Live opt-in flag in the same terminal.
No coverdata foundRun ./scripts/coverage.sh; rebar3 cover only reads existing coverage data.
Google returns HTTP 401The environment variable exists, but Google rejected its value. Replace it with an API key authorized for the Gemini API.
developer_api_unavailable / connection_refusedKeep adk serve running and use the same local port in every command.
Phoenix asks for OIDC_ISSUER during local developmentSet ADK_UI_LOCAL_AUTH=true exactly and run with MIX_ENV=dev (the default for mix phx.server).
Phoenix UI has no stylesRun mix assets.setup and mix assets.build, then restart the server.
No Live sessions appear in PhoenixThe UI discovers existing sessions; start one in the same Erlang runtime with the signed-in user identity first.

Documentation

TopicGuide
All documentationDocumentation index
Provider profiles and vendor setupProvider profiles
Model/endpoint support and evidenceModel support
Supported and partial featuresFeature support
Runtime limits, failures, and cancellationRuntime safety
Workflows, planning, and durable runsGraph workflows, planning, durable invocations
Sessions, artifacts, memory, and contextArtifacts, memory, context
Scheduled and background runsAmbient runtime
Gemini Search groundingGemini grounding
Plugins, telemetry, and evaluationPlugins, observability, and evaluation
External code executionCode execution
Phoenix UIPhoenix companion
Deployment assetsContainer, Cloud Run, Helm/GKE, and supply-chain guide
Tests and coverageTesting
Release processReleasing
UpgradingUpgrade guide
SecuritySecurity policy
Source and test layoutSource layout, test layout
Current release scopev0.10.0 release details
Previous releasev0.9.0 release details

Project status and security

Erlang ADK is under active development. Review the feature-support matrix before depending on a partial feature or provider-specific capability.

Do not commit API keys or OAuth credentials. Keep provider endpoints, credentials, authentication policy, and public listener settings in trusted application configuration. Read the security policy before making any HTTP, MCP, A2A, developer, or Phoenix endpoint reachable beyond 127.0.0.1.

License

Erlang ADK is available under the Apache License 2.0.