adk_ex is an Elixir port of Google's Agent Development Kit (ADK). It provides agent orchestration, session management, tool use, LLM abstraction, plugins, and telemetry. It is transport-agnostic — no HTTP/Plug dependencies.
Architecture
User Message -> Runner -> Agent -> Flow -> LLM
| | | |
[plugins] [plugins] [processors]|
| | [tool call loop]
| | [agent transfer]
| | |
[commits events + state to Session]
|
[yields Events as a stream]Core Patterns
Creating an Agent
agent = %ADK.Agent.LlmAgent{
name: "my-agent",
model: ADK.Model.Registry.resolve("gemini-2.0-flash"),
instruction: "You are a helpful assistant.",
tools: [my_tool],
sub_agents: []
}Defining a Tool
Always use handler:, never function:.
tool = ADK.Tool.FunctionTool.new(
name: "get_weather",
description: "Get weather for a city",
handler: fn _ctx, %{"city" => city} ->
{:ok, %{"weather" => "Sunny in #{city}"}}
end
)The handler signature is (ADK.Tool.Context.t(), map()) -> {:ok, map()} | {:error, term()}.
Running an Agent
ADK.Runner.run/4 returns a stream of ADK.Event structs.
{:ok, runner} = ADK.Runner.new(
app_name: "my-app",
root_agent: agent,
session_service: session_service
)
events =
runner
|> ADK.Runner.run("user-1", "session-1", content)
|> Enum.to_list()Session Service
Start the built-in ETS-backed service:
{:ok, service} = ADK.Session.InMemory.start_link(name: :my_sessions)
{:ok, session} = ADK.Session.InMemory.create(service,
app_name: "my-app",
user_id: "user-1",
session_id: "session-1",
state: %{}
)For database-backed sessions, use the separate adk_ex_ecto package.
State Prefixes
Session state keys use prefixes to control scope:
| Prefix | Scope | Persisted? |
|---|---|---|
| (none) | Session-local | Yes |
app: | Cross-session for the app | Yes |
user: | Cross-session for the user | Yes |
temp: | Current invocation only | No |
Creating Content
content = ADK.Types.Content.new_from_text("user", "Hello, agent!")Configuring a Model
Four providers ship. ADK.Model.LiteLlm is the one to reach for when using OpenAI or anything OpenAI-compatible — it mirrors Google Python ADK's LiteLlm(model="openai/gpt-4o") pattern.
# Gemini (Google)
{:ok, gemini} = ADK.Model.Registry.resolve("gemini-2.0-flash", api_key: key)
# Claude (direct Anthropic API)
{:ok, claude} = ADK.Model.Registry.resolve("claude-sonnet-4-5", api_key: key)
# OpenAI (GPT-4o, GPT-4, o1, o3 — direct)
{:ok, openai} = ADK.Model.Registry.resolve("gpt-4o", api_key: key)
# LiteLLM proxy (any of 100+ providers — `base_url` required)
{:ok, any} = ADK.Model.Registry.resolve(
"anthropic/claude-3-5-sonnet-20241022",
api_key: "sk-proxy",
base_url: "http://localhost:4000"
)
# Any OpenAI-compatible endpoint (Ollama, Groq, Together, OpenRouter, ...)
ollama = %ADK.Model.LiteLlm{
model_name: "llama3",
api_key: "none",
base_url: "http://localhost:11434/v1"
}See adk_ex:models for deeper guidance.
Critical Rules
- FunctionTool uses
handler:notfunction:—FunctionTool.new(handler: fn/2). - Mock model needs
Mock.new/1— useADK.Model.Mock.new(responses: [...]), never bare%ADK.Model.Mock{}. It starts an Agent process for response sequencing. - Agent behaviour has no module functions — call
agent.__struct__.run(agent, ctx)or the implementing module directly.ADK.Agentis a behaviour, not a dispatcher. - Telemetry prefix is
[:adk_ex, ...]— not[:adk, ...]. Events:[:adk_ex, :llm, :start|:stop],[:adk_ex, :tool, :start|:stop]. - Model.Registry.resolve/2 — pattern-matches on name:
"gemini-*"-> Gemini,"claude-*"-> Claude,"gpt-*"/"o1*"/"o3*"-> LiteLlm@OpenAI,"provider/model"(e.g."openai/gpt-4o","anthropic/claude-3-5-sonnet-20241022") -> LiteLlm (requiresbase_url:for a LiteLLM proxy). - Use
ADK.Model.LiteLlmfor OpenAI and any OpenAI-compatible endpoint — mirrors Python ADK'sLiteLlm(model="openai/gpt-4o"). Setbase_urlto OpenAI, a LiteLLM proxy, or any OpenAI-compatible API (Groq, Together, OpenRouter, Ollama, vLLM, Azure OpenAI, LM Studio, etc.). Do not build a bespoke OpenAI client. Plugin callbacks return
{value | nil, updated_context}— nil means continue to next plugin; non-nil short-circuits.- All Plugin.Manager.run_* functions accept
nilas first arg (no-op) — no nil checks needed at call sites. - Nested module compile order — define referenced modules before parent modules in the same file (e.g.
Event.ActionsbeforeEvent). - Avoid MapSet with dialyzer — use
%{key => true}maps +Map.has_key?/2instead.
Sub-rules
For detailed guidance on specific topics, see the usage-rules/ directory:
adk_ex:agents— Agent types, when to use each, sub-agents, agent transferadk_ex:tools— FunctionTool, argument schemas, Tool.Context, Toolset behaviouradk_ex:sessions— Session struct, state prefixes, Service behaviour, switching backendsadk_ex:plugins— Plugin struct, 12 callback hooks, execution order, short-circuit semanticsadk_ex:telemetry— Event naming, OTel span conventions, testing with otel_simple_processoradk_ex:models— Provider selection (Gemini, Claude, OpenAI, LiteLLM proxy), registry, multi-provider patterns