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/event_queue.ex
defmodule A2AEx.EventQueue do
@moduledoc """
Per-task event delivery queue.
Each task gets a dedicated GenServer (started on demand) that broadcasts
events to all subscribed processes. Uses Elixir Registry for lookup by task ID.
## Usage
# Start a queue for a task (idempotent)
{:ok, _pid} = A2AEx.EventQueue.start(task_id)
# Subscribe the current process to receive events
:ok = A2AEx.EventQueue.subscribe(task_id)
# Enqueue an event (broadcasts to all subscribers)
:ok = A2AEx.EventQueue.enqueue(task_id, event)
# Subscribers receive:
# {:a2a_event, task_id, event} — for each event
# {:a2a_done, task_id} — when queue is closed
# Close the queue when the task is complete
:ok = A2AEx.EventQueue.close(task_id)
"""
use GenServer
@registry A2AEx.EventQueue.Registry
@supervisor A2AEx.EventQueue.Supervisor
# --- Public API ---
@doc "Start a queue for the given task. Returns existing queue if already started."
@spec start(String.t()) :: {:ok, pid()} | {:error, term()}
def start(task_id) do
case DynamicSupervisor.start_child(@supervisor, {__MODULE__, task_id}) do
{:ok, pid} -> {:ok, pid}
{:error, {:already_started, pid}} -> {:ok, pid}
error -> error
end
end
@doc "Start or return existing queue for a task."
@spec get_or_create(String.t()) :: {:ok, pid()}
def get_or_create(task_id) do
case lookup(task_id) do
{:ok, pid} -> {:ok, pid}
{:error, :not_found} -> start(task_id)
end
end
@doc "Look up the queue process for a task."
@spec lookup(String.t()) :: {:ok, pid()} | {:error, :not_found}
def lookup(task_id) do
case Registry.lookup(@registry, task_id) do
[{pid, _}] -> {:ok, pid}
[] -> {:error, :not_found}
end
end
@doc "Subscribe the calling process to events for the given task."
@spec subscribe(String.t()) :: :ok | {:error, :not_found}
def subscribe(task_id) do
with {:ok, pid} <- lookup(task_id) do
GenServer.call(pid, {:subscribe, self()})
end
end
@doc "Enqueue an event, broadcasting it to all subscribers."
@spec enqueue(String.t(), A2AEx.Event.t()) :: :ok | {:error, :not_found}
def enqueue(task_id, event) do
with {:ok, pid} <- lookup(task_id) do
GenServer.call(pid, {:enqueue, event})
end
end
@doc "Close the queue, notifying all subscribers and stopping the process."
@spec close(String.t()) :: :ok
def close(task_id) do
case lookup(task_id) do
{:ok, pid} -> GenServer.call(pid, :close)
{:error, :not_found} -> :ok
end
end
# --- GenServer setup ---
@doc false
def start_link(task_id) do
GenServer.start_link(__MODULE__, task_id, name: via(task_id))
end
@doc false
def child_spec(task_id) do
%{
id: {__MODULE__, task_id},
start: {__MODULE__, :start_link, [task_id]},
restart: :temporary
}
end
defp via(task_id), do: {:via, Registry, {@registry, task_id}}
# --- GenServer callbacks ---
@impl GenServer
def init(task_id) do
{:ok, %{task_id: task_id, subscribers: %{}}}
end
@impl GenServer
def handle_call({:subscribe, pid}, _from, state) do
if Map.has_key?(state.subscribers, pid) do
{:reply, :ok, state}
else
ref = Process.monitor(pid)
{:reply, :ok, %{state | subscribers: Map.put(state.subscribers, pid, ref)}}
end
end
@impl GenServer
def handle_call({:enqueue, event}, _from, state) do
for {pid, _ref} <- state.subscribers do
send(pid, {:a2a_event, state.task_id, event})
end
{:reply, :ok, state}
end
@impl GenServer
def handle_call(:close, _from, state) do
for {pid, _ref} <- state.subscribers do
send(pid, {:a2a_done, state.task_id})
end
{:stop, :normal, :ok, state}
end
@impl GenServer
def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
{:noreply, %{state | subscribers: Map.delete(state.subscribers, pid)}}
end
end