Onboarding Guide: Elixir ADK Project

Copy Markdown View Source

For New AI Agents / Developers

This document provides everything needed to pick up the Elixir ADK project.


1. What Is This Project?

We are building an Elixir/OTP port of Google's Agent Development Kit (ADK). The Google ADK is a framework for building AI agents that can use tools, orchestrate sub-agents, manage sessions, and communicate with other agents.

Google provides the ADK in Python (reference), TypeScript, Go, and Java. We are creating the Elixir implementation.

Note: The A2A (Agent-to-Agent) protocol is a separate package at github.com/JohnSmall/a2a_ex. It depends on this ADK package via {:adk_ex, "~> 1.0"}.

Note: Example A2A applications are at github.com/JohnSmall/a2a_ex_examples. They demonstrate two-agent cooperation using the A2A protocol (research+report, code+review, data+viz).


2. Current Status

All 5 phases are COMPLETE. The project lives at github.com/JohnSmall/adk_ex. Database persistence is in a separate package at github.com/JohnSmall/adk_ex_ecto.

What's Built

Phase 1: Foundation (75 tests)

ModulePurposeFile
ADK.Types.BlobBinary data with MIME typelib/adk/types.ex
ADK.Types.FunctionCallLLM function call requestlib/adk/types.ex
ADK.Types.FunctionResponseFunction call responselib/adk/types.ex
ADK.Types.PartTagged union: text/fc/fr/bloblib/adk/types.ex
ADK.Types.ContentMessage with role + partslib/adk/types.ex
ADK.TypesHelper functions for Contentlib/adk/types.ex
ADK.Event.ActionsSide-effects: state_delta, transfer, escalatelib/adk/event.ex
ADK.EventCore event struct with new/1, final_response?/1lib/adk/event.ex
ADK.SessionSession struct (id, app_name, user_id, state, events)lib/adk/session.ex
ADK.Session.StatePrefix-based state scoping utilitieslib/adk/session/state.ex
ADK.Session.ServiceBehaviour for session storage backendslib/adk/session/service.ex
ADK.Session.InMemoryGenServer + 3 ETS tables session implementationlib/adk/session/in_memory.ex
ADK.RunConfigRuntime config (streaming_mode, save_blobs)lib/adk/run_config.ex
ADK.AgentAgent behaviour (name, description, run, sub_agents)lib/adk/agent.ex
ADK.Agent.InvocationContextImmutable execution contextlib/adk/agent/invocation_context.ex
ADK.Agent.CallbackContextCallback context with state accesslib/adk/agent/callback_context.ex
ADK.Agent.ConfigConfiguration struct for custom agentslib/adk/agent/config.ex
ADK.Agent.CustomAgentCustom agent with before/after callbackslib/adk/agent/custom_agent.ex
ADK.Agent.TreeAgent tree: find, parent_map, validatelib/adk/agent/tree.ex

Phase 2: Runner + Tool System + LLM Agent (+63 tests = 138 total)

ModulePurposeFile
ADK.ModelModel behaviour (name/1, generate_content/3)lib/adk/model.ex
ADK.Model.LlmRequestLLM request structlib/adk/model/llm_request.ex
ADK.Model.LlmResponseLLM response structlib/adk/model/llm_response.ex
ADK.Model.MockStateful mock model via Agent processlib/adk/model/mock.ex
ADK.Model.GeminiGemini REST API provider (Req)lib/adk/model/gemini.ex
ADK.Model.ClaudeClaude/Anthropic REST API provider (Req)lib/adk/model/claude.ex
ADK.Model.LiteLlmOpenAI-compatible provider (OpenAI, LiteLLM proxy, Groq, Ollama, etc.)lib/adk/model/lite_llm.ex
ADK.Model.RegistryModel name -> provider resolutionlib/adk/model/registry.ex
ADK.ToolTool behaviour + module-level dispatchlib/adk/tool.ex
ADK.Tool.ContextTool context with 3-level state delegationlib/adk/tool/context.ex
ADK.Tool.FunctionToolAnonymous function wrapper with try/rescuelib/adk/tool/function_tool.ex
ADK.FlowFlow engine (Stream.resource/3, max 25 iter)lib/adk/flow.ex
ADK.Flow.Processors.BasicCopies generate_content_config into requestlib/adk/flow/processors/basic.ex
ADK.Flow.Processors.ToolProcessorPopulates tools map + function declarationslib/adk/flow/processors/tool_processor.ex
ADK.Flow.Processors.InstructionsSystem instruction + {variable} interpolationlib/adk/flow/processors/instructions.ex
ADK.Flow.Processors.ContentsConversation history from session eventslib/adk/flow/processors/contents.ex
ADK.Agent.LlmAgentLLM-powered agent (model, tools, callbacks)lib/adk/agent/llm_agent.ex
ADK.RunnerRunner (session lifecycle, event persistence)lib/adk/runner.ex

Phase 3: Orchestration Agents + Agent Transfer (+30 tests = 168 total)

ModulePurposeFile
ADK.Agent.LoopAgentIterates sub-agents (max_iterations, escalation)lib/adk/agent/loop_agent.ex
ADK.Agent.SequentialAgentRuns sub-agents once in order (wraps LoopAgent)lib/adk/agent/sequential_agent.ex
ADK.Agent.ParallelAgentRuns sub-agents concurrently (Task.async)lib/adk/agent/parallel_agent.ex
ADK.Tool.TransferToAgentTool signaling agent transferlib/adk/tool/transfer_to_agent.ex
ADK.Flow.Processors.AgentTransferInjects transfer tool + target instructionslib/adk/flow/processors/agent_transfer.ex

Phase 4: Memory, Artifacts, and Telemetry (+49 tests = 217 total)

ModulePurposeFile
ADK.Memory.EntryMemory entry struct (content, author, timestamp)lib/adk/memory/entry.ex
ADK.Memory.ServiceBehaviour: add_session/2, search/2lib/adk/memory/service.ex
ADK.Memory.InMemoryGenServer + ETS, word-based searchlib/adk/memory/in_memory.ex
ADK.Artifact.ServiceBehaviour: save, load, delete, list, versionslib/adk/artifact/service.ex
ADK.Artifact.InMemoryGenServer + ETS, versioned storage, user-scopedlib/adk/artifact/in_memory.ex
ADK.Tool.LoadMemoryTool: searches memory via contextlib/adk/tool/load_memory.ex
ADK.Tool.LoadArtifactsTool: loads artifacts by namelib/adk/tool/load_artifacts.ex
ADK.TelemetryDual: OpenTelemetry spans + :telemetry eventslib/adk/telemetry.ex

Phase 5: Plugins, Toolsets, and Database Sessions (+23 tests = 240 total, +21 in adk_ex_ecto)

ModulePurposeFile
ADK.PluginPlugin struct with 12 callback fields + new/1lib/adk/plugin.ex
ADK.Plugin.ManagerChains plugins, first non-nil wins, nil-safelib/adk/plugin/manager.ex
ADK.Tool.ToolsetBehaviour for dynamic tool providerslib/adk/tool/toolset.ex
ADK.Runner (updated)Added plugins, session_module fieldslib/adk/runner.ex
ADK.Flow (updated)Plugin hooks at model/tool level, toolset resolutionlib/adk/flow.ex
ADK.Agent.LlmAgent (updated)Plugin before/after agent, toolsets fieldlib/adk/agent/llm_agent.ex
ADK.Agent.InvocationContext (updated)Added plugin_manager fieldlib/adk/agent/invocation_context.ex

Database Sessions (separate package: adk_ex_ecto, 21 tests)

ModulePurposeFile
ADKExEcto.SessionServiceEcto-backed session servicelib/adk_ex_ecto/session_service.ex
ADKExEcto.MigrationCreates 4 tables with composite PKslib/adk_ex_ecto/migration.ex
ADKExEcto.Schemas.SessionSessions table schemalib/adk_ex_ecto/schemas/session.ex
ADKExEcto.Schemas.EventEvents table schemalib/adk_ex_ecto/schemas/event.ex
ADKExEcto.Schemas.AppStateApp state table schemalib/adk_ex_ecto/schemas/app_state.ex
ADKExEcto.Schemas.UserStateUser state table schemalib/adk_ex_ecto/schemas/user_state.ex

Test Coverage

  • adk_ex: 240 tests passing (75 + 63 + 30 + 49 + 23)
  • adk_ex_ecto: 21 tests passing
  • 6 integration tests (Gemini + Claude + OpenAI, excluded by default)
  • Credo: clean (both packages)
  • Dialyzer: clean (both packages)

Project Status

All 5 phases are complete. See docs/implementation-plan.md for full details.


3. Key Resources

Project Resources

ResourceLocation
This project (Elixir ADK)github.com/JohnSmall/adk_ex
Database sessions (separate package)github.com/JohnSmall/adk_ex_ecto
A2A protocol (separate package)github.com/JohnSmall/a2a_ex
Google ADK Go source (PRIMARY ref)github.com/google/adk-go
Google ADK Python sourcepypi.org/project/google-adk
A2A Go SDKgithub.com/a2aproject/a2a-go
A2A samplesgithub.com/a2aproject/a2a-samples
PRDdocs/prd.md
Architecturedocs/architecture.md
Implementation plandocs/implementation-plan.md
This guidedocs/onboarding.md

External Documentation

ResourceURL
Google ADK docshttps://google.github.io/adk-docs/
A2A protocol spechttps://github.com/a2aproject/A2A

4. Architecture Quick Reference

Core Execution Model

User Message -> Runner -> Agent -> Flow -> LLM
                  |          |        |       |
              [plugins]  [plugins] [plugins]  |
                  |          |     [tool calls loop]
                  |          |     [agent transfer]
                  |          |     [toolset resolution]
                  |          |        |
               [commits events + state to Session]
                  |
               [yields Events to application]

Execution Flow Detail

Runner.run/5
  |-- Get/create session from SessionService (via runner.session_module)
  |-- [plugin: on_user_message] (may modify user content)
  |-- Append user message event
  |-- [plugin: before_run] (may short-circuit entire run)
  |-- Find agent to run (transfer check -> history scan -> root)
  +-- LlmAgent.run/2
        |-- [plugin: before_agent] -> [before_agent_callbacks] (may short-circuit)
        |-- Flow.run/2 (Stream.resource/3 loop)
        |     |-- Resolve toolsets (dynamic tool providers)
        |     |-- Build LlmRequest via 5 processors:
        |     |     Basic -> ToolProcessor -> Instructions -> AgentTransfer -> Contents
        |     |-- [plugin: before_model] -> [before_model_callbacks] (may short-circuit)
        |     |-- Model.generate_content/3 (Gemini/Claude/LiteLlm/Mock)
        |     |-- [plugin: after_model] -> [after_model_callbacks] (may replace)
        |     |-- If function_calls in response:
        |     |     [plugin: before_tool] -> [before_tool] -> Tool.run/3
        |     |     -> maybe_set_transfer -> [plugin: after_tool] -> [after_tool]
        |     |     If transfer_to_agent set: run target agent (maybe_run_transfer)
        |     |     Build tool response event -> loop back to LLM
        |     +-- If text response (final): emit event, halt
        |-- [plugin: after_agent] -> [after_agent_callbacks] (may short-circuit)
        +-- If output_key: save text to state_delta
  |-- For each event: [plugin: on_event] (may modify)
  +-- [plugin: after_run] (notification)

Agent Types

TypePurposeImplementation
CustomAgentUser-defined run functionConfig struct with run fn
LlmAgentLLM-powered with toolsFlow engine + request processors
LoopAgentRepeat sub-agents until terminationStream.resource + reduce_while
SequentialAgentRun sub-agents in order onceLoopAgent with max_iterations=1
ParallelAgentRun sub-agents concurrentlyTask.async + Task.await_many

Callback Points

All callbacks return {value | nil, updated_context}. Nil = continue, non-nil = short-circuit.

HookSignatureShort-circuit
before_agent(CallbackContext -> {Content | nil, CallbackContext})Non-nil Content skips agent
after_agent(CallbackContext -> {Content | nil, CallbackContext})Non-nil Content replaces output
before_model(CallbackContext, LlmRequest -> {LlmResponse | nil, CallbackContext})Non-nil LlmResponse skips LLM
after_model(CallbackContext, LlmResponse -> {LlmResponse | nil, CallbackContext})Non-nil LlmResponse replaces
before_tool(ToolContext, tool, args -> {map | nil, ToolContext})Non-nil map skips tool
after_tool(ToolContext, tool, args, result -> {map | nil, ToolContext})Non-nil map replaces result

Plugin hooks (Phase 5): Plugins use the same callback signatures but run before agent callbacks. If a plugin returns non-nil, agent callbacks are skipped entirely. Additional plugin-only hooks: on_user_message, before_run, after_run (notification only), on_event.

State Prefixes

PrefixScopePersisted?
(none)Session-localYes
app:Shared across all users/sessionsYes
user:Shared across user's sessionsYes
temp:Current invocation onlyNo (discarded)

5. Elixir/OTP Design Patterns Used

ADK ConceptElixir EquivalentWhy
BaseAgent (class)@behaviour + structNo inheritance in Elixir
Agent.Run() streamStream.resource/3Lazy evaluation, yield/resume
Session storageGenServer + 3 ETS tablesSerialized writes, concurrent reads
Async generatorsEnumerable.t() (Stream)Flow.run returns Stream of Events
Pydantic modelsdefstruct + @typeTyped structs with @enforce_keys
Dynamic dispatchagent.__struct__.run(agent, ctx)Polymorphism via struct module
ParallelAgent concurrencyTask.async + Task.await_manyBEAM lightweight processes
SequentialAgentLoopAgent(max_iterations=1)Code reuse, matches Go pattern

Package Naming

  • Hex package name: adk_ex (OTP app: :adk_ex)
  • Module names: ADK.* (module prefix is independent of hex name, like phoenix uses Phoenix.*)
  • Source paths: lib/adk/, test/adk/ (unchanged)
  • Telemetry events: [:adk_ex, :llm | :tool, :start | :stop | :exception]

  • Database persistence is a separate package: adk_ex_ecto (keeps core lightweight)

Critical Gotchas

  1. Compile order: Define nested/referenced modules BEFORE parent modules in the same file (e.g., Event.Actions before Event)
  2. MapSet + dialyzer: Avoid MapSet — use %{key => true} maps + Map.has_key?/2 instead
  3. Credo nesting: Max depth 2 — extract inner logic into helper functions
  4. Mock model: Use Mock.new(responses: [...]) NOT bare %Mock{} — needs Agent process for state
  5. Behaviour dispatch: ADK.Agent has NO module functions — call agent.__struct__.run(agent, ctx) or the implementing module directly
  6. Test module names: Use unique names to avoid cross-file collisions
  7. OTel span testing: config/test.exs configures otel_simple_processor. In test setup, call :otel_simple_processor.set_exporter(:otel_exporter_pid, self()) to route spans to the test process. Span name is at elem(span, 6) (not 2) in the Erlang span record. No app restart needed.
  8. Dialyzer unreachable branches: If a function always returns {:ok, _}, don't pattern match {:error, _} — dialyzer flags it
  9. FunctionTool field: Use handler: not function: in FunctionTool.new/1
  10. Plugin nil safety: All Plugin.Manager.run_* functions accept nil as first arg — no nil checks needed at call sites
  11. SQLite in-memory testing: Don't use Ecto sandbox with pool_size 1. Clean tables in setup instead.
  12. OpenTelemetry dep: {:opentelemetry, "~> 1.5"} must NOT have only: [:dev, :test] — it's needed at compile time in all environments (the require OpenTelemetry.Tracer in ADK.Telemetry needs it).
  13. Dep name must match app name: When other projects depend on this package, they must use {:adk_ex, path: "..."} (not {:adk, ...}). Mix fails to resolve code paths when the dep name doesn't match the app name :adk_ex.

6. Development Workflow

Running Tests

mix test                                        # Run all unit tests (240)
mix test test/integration/ --include integration # Run integration tests
mix test --trace                                 # Run with verbose output
mix credo                                        # Static analysis
mix dialyzer                                     # Type checking

Conventions

  • Module names: ADK.Component.SubComponent (e.g., ADK.Agent.LlmAgent)
  • Behaviours: Define in dedicated files (e.g., agent.ex, model.ex, tool.ex)
  • Structs: defstruct + @type t :: %__MODULE__{} typespecs
  • Callbacks: Return {value | nil, context} — nil = continue, non-nil = short-circuit

  • Errors: {:ok, result} / {:error, reason} tuples
  • Tests: Mirror lib/ structure under test/; use async: true unless shared state
  • Use Mock.new(responses: [...]) for test models
  • Verify all changes: mix test && mix credo && mix dialyzer

7. Quick Commands

mix test           # Run tests
mix credo          # Static analysis
mix dialyzer       # Type checking
iex -S mix         # Interactive shell
mix clean && mix compile  # Clean build

8. Key Contacts / Context