Packages

Elixir implementation of the Agent-to-Agent (A2A) protocol. Exposes ADK agents as A2A-compatible HTTP endpoints and consumes remote A2A agents as local ADK agents.

Current section

Files

Jump to
a2a_elixir_sdk lib a2a_ex adk_executor.ex
Raw

lib/a2a_ex/adk_executor.ex

defmodule A2AEx.ADKExecutor.Config do
@moduledoc """
Configuration for the ADK executor bridge.
- `:runner` — A pre-built `ADK.Runner.t()` struct
- `:app_name` — Application name for session management
"""
@type t :: %__MODULE__{
runner: ADK.Runner.t(),
app_name: String.t()
}
@enforce_keys [:runner, :app_name]
defstruct [:runner, :app_name]
end
defmodule A2AEx.ADKExecutor do
@moduledoc """
Bridges ADK agent execution into the A2A protocol.
Used as `{A2AEx.ADKExecutor, config}` with `A2AEx.RequestHandler`.
Converts A2A messages to ADK content, runs the agent via `ADK.Runner`,
and emits A2A events (status updates + artifact updates) to the event queue.
"""
alias A2AEx.ADKExecutor.Config
alias A2AEx.Converter
@doc """
Execute an ADK agent for the given A2A request.
Converts the incoming message, runs the agent, and streams ADK events
as A2A status/artifact update events to the event queue.
"""
@spec execute(Config.t(), A2AEx.RequestContext.t(), String.t()) :: :ok | {:error, term()}
def execute(%Config{} = config, %A2AEx.RequestContext{} = req_ctx, task_id) do
{:ok, content} = Converter.a2a_message_to_content(req_ctx.message)
run_agent(config, req_ctx, task_id, content)
end
@doc """
Cancel an in-progress ADK execution.
Enqueues a canceled status event with `final: true`.
"""
@spec cancel(Config.t(), A2AEx.RequestContext.t(), String.t()) :: :ok
def cancel(%Config{}, %A2AEx.RequestContext{} = req_ctx, task_id) do
event =
A2AEx.TaskStatusUpdateEvent.new(task_id, req_ctx.context_id, :canceled)
|> Map.put(:final, true)
A2AEx.EventQueue.enqueue(task_id, event)
:ok
end
# --- Private ---
defp run_agent(config, req_ctx, task_id, content) do
user_id = derive_user_id(req_ctx)
session_id = req_ctx.context_id
# Enqueue :working status
working = A2AEx.TaskStatusUpdateEvent.new(task_id, req_ctx.context_id, :working)
A2AEx.EventQueue.enqueue(task_id, working)
# Run the ADK agent
event_stream = ADK.Runner.run(config.runner, user_id, session_id, content)
# Process events and emit final status
acc = process_events(event_stream, task_id, req_ctx.context_id)
emit_final(task_id, req_ctx.context_id, acc)
:ok
end
defp process_events(event_stream, task_id, context_id) do
init_acc = %{artifact_id: nil, has_artifact: false, final_state: :completed, error_message: nil}
Enum.reduce(event_stream, init_acc, fn event, acc ->
acc = update_terminal(acc, event)
maybe_enqueue_artifact(event, task_id, context_id, acc)
end)
end
defp maybe_enqueue_artifact(event, task_id, context_id, acc) do
if has_content_parts?(event) do
do_enqueue_artifact(event, task_id, context_id, acc)
else
acc
end
end
defp do_enqueue_artifact(event, task_id, context_id, acc) do
{:ok, a2a_parts} = Converter.adk_parts_to_a2a(event.content.parts)
artifact_id = acc.artifact_id || A2AEx.ID.new()
append = acc.has_artifact
artifact = %A2AEx.Artifact{id: artifact_id, parts: a2a_parts}
artifact_event = %A2AEx.TaskArtifactUpdateEvent{
task_id: task_id,
context_id: context_id,
artifact: artifact,
append: append
}
A2AEx.EventQueue.enqueue(task_id, artifact_event)
%{acc | artifact_id: artifact_id, has_artifact: true}
end
defp update_terminal(acc, event) do
state = Converter.terminal_state(event)
case state do
:completed -> acc
new_state -> %{acc | final_state: new_state, error_message: event.error_message}
end
end
defp emit_final(task_id, context_id, acc) do
# Emit last_chunk artifact if we had artifacts
if acc.has_artifact do
last_artifact = %A2AEx.TaskArtifactUpdateEvent{
task_id: task_id,
context_id: context_id,
artifact: %A2AEx.Artifact{id: acc.artifact_id, parts: []},
append: true,
last_chunk: true
}
A2AEx.EventQueue.enqueue(task_id, last_artifact)
end
# Build final message for failed states
message = build_final_message(acc)
final_event =
A2AEx.TaskStatusUpdateEvent.new(task_id, context_id, acc.final_state, message)
|> Map.put(:final, true)
A2AEx.EventQueue.enqueue(task_id, final_event)
end
defp build_final_message(%{final_state: :failed, error_message: msg}) when is_binary(msg) do
A2AEx.Message.new(:agent, [%A2AEx.TextPart{text: msg}])
end
defp build_final_message(_acc), do: nil
defp has_content_parts?(%{content: %{parts: parts}}) when is_list(parts) and parts != [] do
true
end
defp has_content_parts?(_), do: false
defp derive_user_id(%A2AEx.RequestContext{context_id: ctx_id}) do
"a2a_user_" <> ctx_id
end
end