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/task.ex
defmodule A2AEx.TaskState do
@moduledoc """
Possible states of an A2A task.
"""
@type t ::
:submitted
| :working
| :input_required
| :completed
| :failed
| :canceled
| :rejected
| :auth_required
| :unknown
@terminal_states [:completed, :canceled, :failed, :rejected]
@doc "Returns true if the state is terminal (task is immutable)."
@spec terminal?(t()) :: boolean()
def terminal?(state), do: state in @terminal_states
@doc "Decode a task state from a JSON string."
@spec from_string(String.t()) :: t()
def from_string("submitted"), do: :submitted
def from_string("working"), do: :working
def from_string("input-required"), do: :input_required
def from_string("completed"), do: :completed
def from_string("failed"), do: :failed
def from_string("canceled"), do: :canceled
def from_string("rejected"), do: :rejected
def from_string("auth-required"), do: :auth_required
def from_string(_), do: :unknown
@doc "Encode a task state to a JSON string."
@spec to_string(t()) :: String.t()
def to_string(:submitted), do: "submitted"
def to_string(:working), do: "working"
def to_string(:input_required), do: "input-required"
def to_string(:completed), do: "completed"
def to_string(:failed), do: "failed"
def to_string(:canceled), do: "canceled"
def to_string(:rejected), do: "rejected"
def to_string(:auth_required), do: "auth-required"
def to_string(:unknown), do: "unknown"
end
defmodule A2AEx.TaskStatus do
@moduledoc """
The status of a task at a specific point in time.
"""
@type t :: %__MODULE__{
state: A2AEx.TaskState.t(),
message: A2AEx.Message.t() | nil,
timestamp: DateTime.t() | nil
}
@enforce_keys [:state]
defstruct [:state, :message, :timestamp]
@spec from_map(map()) :: {:ok, t()} | {:error, String.t()}
def from_map(map) when is_map(map) do
message =
case map["message"] do
nil -> nil
msg_map -> elem(A2AEx.Message.from_map(msg_map), 1)
end
{:ok,
%__MODULE__{
state: A2AEx.TaskState.from_string(map["state"] || ""),
message: message,
timestamp: parse_timestamp(map["timestamp"])
}}
end
@spec to_map(t()) :: map()
def to_map(%__MODULE__{} = status) do
%{"state" => A2AEx.TaskState.to_string(status.state)}
|> maybe_put("message", status.message && A2AEx.Message.to_map(status.message))
|> maybe_put("timestamp", status.timestamp && DateTime.to_iso8601(status.timestamp))
end
defp parse_timestamp(nil), do: nil
defp parse_timestamp(str) when is_binary(str) do
case DateTime.from_iso8601(str) do
{:ok, dt, _offset} -> dt
_ -> nil
end
end
defp parse_timestamp(_), do: nil
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
end
defmodule A2AEx.Task do
@moduledoc """
A single, stateful operation or conversation between a client and an agent.
"""
@type t :: %__MODULE__{
id: String.t(),
context_id: String.t(),
status: A2AEx.TaskStatus.t(),
artifacts: [A2AEx.Artifact.t()] | nil,
history: [A2AEx.Message.t()] | nil,
metadata: map() | nil
}
@enforce_keys [:id, :context_id, :status]
defstruct [:id, :context_id, :status, :artifacts, :history, :metadata]
@doc "Create a new task in submitted state."
@spec new_submitted(A2AEx.Message.t(), keyword()) :: t()
def new_submitted(%A2AEx.Message{} = message, opts \\ []) do
task_id = Keyword.get(opts, :task_id) || A2AEx.ID.new()
context_id = Keyword.get(opts, :context_id) || message.context_id || A2AEx.ID.new()
%__MODULE__{
id: task_id,
context_id: context_id,
status: %A2AEx.TaskStatus{state: :submitted},
history: [message]
}
end
@spec from_map(map()) :: {:ok, t()} | {:error, String.t()}
def from_map(map) when is_map(map) do
with {:ok, status} <- A2AEx.TaskStatus.from_map(map["status"] || %{}),
{:ok, artifacts} <- decode_artifacts(map["artifacts"]),
{:ok, history} <- decode_history(map["history"]) do
{:ok,
%__MODULE__{
id: map["id"] || A2AEx.ID.new(),
context_id: map["contextId"] || A2AEx.ID.new(),
status: status,
artifacts: artifacts,
history: history,
metadata: map["metadata"]
}}
end
end
@spec to_map(t()) :: map()
def to_map(%__MODULE__{} = task) do
%{
"kind" => "task",
"id" => task.id,
"contextId" => task.context_id,
"status" => A2AEx.TaskStatus.to_map(task.status)
}
|> maybe_put_list("artifacts", task.artifacts, &A2AEx.Artifact.to_map/1)
|> maybe_put_list("history", task.history, &A2AEx.Message.to_map/1)
|> maybe_put("metadata", task.metadata)
end
defp decode_artifacts(nil), do: {:ok, nil}
defp decode_artifacts(list) when is_list(list) do
list
|> Enum.reduce_while({:ok, []}, fn map, {:ok, acc} ->
case A2AEx.Artifact.from_map(map) do
{:ok, artifact} -> {:cont, {:ok, [artifact | acc]}}
error -> {:halt, error}
end
end)
|> case do
{:ok, artifacts} -> {:ok, Enum.reverse(artifacts)}
error -> error
end
end
defp decode_history(nil), do: {:ok, nil}
defp decode_history(list) when is_list(list) do
list
|> Enum.reduce_while({:ok, []}, fn map, {:ok, acc} ->
case A2AEx.Message.from_map(map) do
{:ok, msg} -> {:cont, {:ok, [msg | acc]}}
error -> {:halt, error}
end
end)
|> case do
{:ok, msgs} -> {:ok, Enum.reverse(msgs)}
error -> error
end
end
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
defp maybe_put_list(map, _key, nil, _fun), do: map
defp maybe_put_list(map, key, list, fun), do: Map.put(map, key, Enum.map(list, fun))
end
defimpl Jason.Encoder, for: A2AEx.TaskStatus do
def encode(status, opts) do
A2AEx.TaskStatus.to_map(status) |> Jason.Encode.map(opts)
end
end
defimpl Jason.Encoder, for: A2AEx.Task do
def encode(task, opts) do
A2AEx.Task.to_map(task) |> Jason.Encode.map(opts)
end
end