Packages
a2a_elixir_sdk
1.0.1
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
Current section
Files
lib/a2a_ex/converter.ex
defmodule A2AEx.Converter do
@moduledoc """
Pure functions for converting between ADK and A2A types.
Handles bidirectional conversion of parts, content/messages, and
terminal state determination from ADK events.
"""
alias ADK.Types.{Blob, Content, FunctionCall, FunctionResponse, Part}
@adk_type "adk_type"
@adk_thought "adk_thought"
@function_call "function_call"
@function_response "function_response"
# --- ADK Part → A2A Part ---
@doc "Convert an ADK Part to an A2A Part."
@spec adk_part_to_a2a(Part.t()) :: {:ok, A2AEx.Part.t()}
def adk_part_to_a2a(%Part{text: text, thought: true}) when is_binary(text) do
{:ok, %A2AEx.TextPart{text: text, metadata: %{@adk_thought => true}}}
end
def adk_part_to_a2a(%Part{text: text}) when is_binary(text) do
{:ok, %A2AEx.TextPart{text: text}}
end
def adk_part_to_a2a(%Part{inline_data: %Blob{data: data, mime_type: mt}}) do
{:ok, %A2AEx.FilePart{file: %A2AEx.FileBytes{bytes: Base.encode64(data), mime_type: mt}}}
end
def adk_part_to_a2a(%Part{function_call: %FunctionCall{} = fc}) do
data = %{"name" => fc.name, "id" => fc.id, "args" => fc.args}
{:ok, %A2AEx.DataPart{data: data, metadata: %{@adk_type => @function_call}}}
end
def adk_part_to_a2a(%Part{function_response: %FunctionResponse{} = fr}) do
data = %{"name" => fr.name, "id" => fr.id, "response" => fr.response}
{:ok, %A2AEx.DataPart{data: data, metadata: %{@adk_type => @function_response}}}
end
@doc "Convert a list of ADK Parts to A2A Parts."
@spec adk_parts_to_a2a([Part.t()]) :: {:ok, [A2AEx.Part.t()]}
def adk_parts_to_a2a(parts) when is_list(parts) do
a2a_parts =
Enum.map(parts, fn part ->
{:ok, p} = adk_part_to_a2a(part)
p
end)
{:ok, a2a_parts}
end
# --- A2A Part → ADK Part ---
@doc "Convert an A2A Part to an ADK Part."
@spec a2a_part_to_adk(A2AEx.Part.t()) :: {:ok, Part.t()}
def a2a_part_to_adk(%A2AEx.TextPart{text: text, metadata: %{@adk_thought => true}}) do
{:ok, %Part{text: text, thought: true}}
end
def a2a_part_to_adk(%A2AEx.TextPart{text: text}) do
{:ok, %Part{text: text}}
end
def a2a_part_to_adk(%A2AEx.FilePart{file: %A2AEx.FileBytes{bytes: b64, mime_type: mt}}) do
{:ok, decoded} = Base.decode64(b64)
{:ok, %Part{inline_data: %Blob{data: decoded, mime_type: mt}}}
end
def a2a_part_to_adk(%A2AEx.FilePart{file: %A2AEx.FileURI{uri: uri}}) do
{:ok, %Part{text: uri}}
end
def a2a_part_to_adk(%A2AEx.DataPart{data: data, metadata: %{@adk_type => @function_call}}) do
fc = %FunctionCall{name: data["name"], id: data["id"], args: data["args"] || %{}}
{:ok, %Part{function_call: fc}}
end
def a2a_part_to_adk(%A2AEx.DataPart{data: data, metadata: %{@adk_type => @function_response}}) do
fr = %FunctionResponse{name: data["name"], id: data["id"], response: data["response"] || %{}}
{:ok, %Part{function_response: fr}}
end
def a2a_part_to_adk(%A2AEx.DataPart{data: data}) do
{:ok, %Part{text: Jason.encode!(data)}}
end
@doc "Convert a list of A2A Parts to ADK Parts."
@spec a2a_parts_to_adk([A2AEx.Part.t()]) :: {:ok, [Part.t()]}
def a2a_parts_to_adk(parts) when is_list(parts) do
adk_parts =
Enum.map(parts, fn part ->
{:ok, p} = a2a_part_to_adk(part)
p
end)
{:ok, adk_parts}
end
# --- Message ↔ Content ---
@doc "Convert an A2A Message to ADK Content."
@spec a2a_message_to_content(A2AEx.Message.t()) :: {:ok, Content.t()} | {:error, String.t()}
def a2a_message_to_content(%A2AEx.Message{role: role, parts: parts}) do
{:ok, adk_parts} = a2a_parts_to_adk(parts)
adk_role = a2a_role_to_adk(role)
{:ok, %Content{role: adk_role, parts: adk_parts}}
end
@doc "Convert ADK Content to an A2A Message."
@spec adk_content_to_message(Content.t()) :: A2AEx.Message.t()
def adk_content_to_message(%Content{} = content) do
adk_content_to_message(content, [])
end
@doc "Convert ADK Content to an A2A Message with context IDs."
@spec adk_content_to_message(Content.t(), keyword()) :: A2AEx.Message.t()
def adk_content_to_message(%Content{role: role, parts: parts}, opts) do
{:ok, a2a_parts} = adk_parts_to_a2a(parts)
a2a_role = adk_role_to_a2a(role)
msg = A2AEx.Message.new(a2a_role, a2a_parts)
msg
|> maybe_set_context_id(Keyword.get(opts, :context_id))
|> maybe_set_task_id(Keyword.get(opts, :task_id))
end
# --- Terminal state ---
@doc "Determine the terminal A2A task state from an ADK event."
@spec terminal_state(ADK.Event.t()) :: A2AEx.TaskState.t()
def terminal_state(%ADK.Event{} = event) do
cond do
is_binary(event.error_code) or is_binary(event.error_message) -> :failed
event.long_running_tool_ids != [] -> :input_required
event.actions.escalate -> :input_required
true -> :completed
end
end
# --- Private helpers ---
defp a2a_role_to_adk(:user), do: "user"
defp a2a_role_to_adk(:agent), do: "model"
defp a2a_role_to_adk(_), do: "user"
defp adk_role_to_a2a("user"), do: :user
defp adk_role_to_a2a(_), do: :agent
defp maybe_set_context_id(msg, nil), do: msg
defp maybe_set_context_id(msg, id), do: %{msg | context_id: id}
defp maybe_set_task_id(msg, nil), do: msg
defp maybe_set_task_id(msg, id), do: %{msg | task_id: id}
end