Packages

Elixir implementation of the Agent Client Protocol (ACP) for communication between code editors and AI coding agents. Includes schema types, JSON-RPC primitives, behaviours, and connection management.

Current section

Files

Jump to
agent_client_protocol lib acp content.ex
Raw

lib/acp/content.ex

defmodule ACP.Role do
@moduledoc "The sender or recipient of messages and data in a conversation."
@type t :: :assistant | :user
def encode(:assistant), do: "assistant"
def encode(:user), do: "user"
def decode("assistant"), do: :assistant
def decode("user"), do: :user
end
defmodule ACP.Annotations do
@moduledoc "Optional annotations for the client."
@type t :: %__MODULE__{
audience: [ACP.Role.t()] | nil,
last_modified: String.t() | nil,
priority: float() | nil,
meta: map() | nil
}
defstruct [:audience, :last_modified, :priority, :meta]
def new, do: %__MODULE__{}
def to_json(%__MODULE__{} = a) do
map = %{}
map = if a.audience, do: Map.put(map, "audience", Enum.map(a.audience, &ACP.Role.encode/1)), else: map
map = if a.last_modified, do: Map.put(map, "lastModified", a.last_modified), else: map
map = if a.priority, do: Map.put(map, "priority", a.priority), else: map
map = if a.meta, do: Map.put(map, "_meta", a.meta), else: map
map
end
def from_json(map) when is_map(map) do
{:ok, %__MODULE__{
audience: case Map.get(map, "audience") do
nil -> nil
list -> Enum.map(list, &ACP.Role.decode/1)
end,
last_modified: Map.get(map, "lastModified"),
priority: Map.get(map, "priority"),
meta: Map.get(map, "_meta")
}}
end
end
defmodule ACP.TextContent do
@moduledoc "Text provided to or from an LLM."
@type t :: %__MODULE__{
annotations: ACP.Annotations.t() | nil,
text: String.t(),
meta: map() | nil
}
@enforce_keys [:text]
defstruct [:annotations, :text, :meta]
def new(text), do: %__MODULE__{text: text}
def to_json(%__MODULE__{} = tc) do
map = %{"text" => tc.text}
map = if tc.annotations, do: Map.put(map, "annotations", ACP.Annotations.to_json(tc.annotations)), else: map
if tc.meta, do: Map.put(map, "_meta", tc.meta), else: map
end
def from_json(map) when is_map(map) do
{:ok, %__MODULE__{
text: Map.fetch!(map, "text"),
annotations: case Map.get(map, "annotations") do
nil -> nil
a -> {:ok, ann} = ACP.Annotations.from_json(a); ann
end,
meta: Map.get(map, "_meta")
}}
end
end
defmodule ACP.ImageContent do
@moduledoc "An image provided to or from an LLM."
@type t :: %__MODULE__{
annotations: ACP.Annotations.t() | nil,
data: String.t(),
mime_type: String.t(),
uri: String.t() | nil,
meta: map() | nil
}
@enforce_keys [:data, :mime_type]
defstruct [:annotations, :data, :mime_type, :uri, :meta]
def new(data, mime_type), do: %__MODULE__{data: data, mime_type: mime_type}
def to_json(%__MODULE__{} = ic) do
map = %{"data" => ic.data, "mimeType" => ic.mime_type}
map = if ic.annotations, do: Map.put(map, "annotations", ACP.Annotations.to_json(ic.annotations)), else: map
map = if ic.uri, do: Map.put(map, "uri", ic.uri), else: map
if ic.meta, do: Map.put(map, "_meta", ic.meta), else: map
end
def from_json(map) when is_map(map) do
{:ok, %__MODULE__{
data: Map.fetch!(map, "data"),
mime_type: Map.fetch!(map, "mimeType"),
annotations: case Map.get(map, "annotations") do
nil -> nil
a -> {:ok, ann} = ACP.Annotations.from_json(a); ann
end,
uri: Map.get(map, "uri"),
meta: Map.get(map, "_meta")
}}
end
end
defmodule ACP.AudioContent do
@moduledoc "Audio provided to or from an LLM."
@type t :: %__MODULE__{
annotations: ACP.Annotations.t() | nil,
data: String.t(),
mime_type: String.t(),
meta: map() | nil
}
@enforce_keys [:data, :mime_type]
defstruct [:annotations, :data, :mime_type, :meta]
def new(data, mime_type), do: %__MODULE__{data: data, mime_type: mime_type}
def to_json(%__MODULE__{} = ac) do
map = %{"data" => ac.data, "mimeType" => ac.mime_type}
map = if ac.annotations, do: Map.put(map, "annotations", ACP.Annotations.to_json(ac.annotations)), else: map
if ac.meta, do: Map.put(map, "_meta", ac.meta), else: map
end
def from_json(map) when is_map(map) do
{:ok, %__MODULE__{
data: Map.fetch!(map, "data"),
mime_type: Map.fetch!(map, "mimeType"),
annotations: case Map.get(map, "annotations") do
nil -> nil
a -> {:ok, ann} = ACP.Annotations.from_json(a); ann
end,
meta: Map.get(map, "_meta")
}}
end
end
defmodule ACP.TextResourceContents do
@moduledoc "Text-based resource contents."
@type t :: %__MODULE__{
mime_type: String.t() | nil,
text: String.t(),
uri: String.t(),
meta: map() | nil
}
@enforce_keys [:text, :uri]
defstruct [:mime_type, :text, :uri, :meta]
def new(text, uri), do: %__MODULE__{text: text, uri: uri}
def to_json(%__MODULE__{} = trc) do
map = %{"text" => trc.text, "uri" => trc.uri}
map = if trc.mime_type, do: Map.put(map, "mimeType", trc.mime_type), else: map
if trc.meta, do: Map.put(map, "_meta", trc.meta), else: map
end
def from_json(map) when is_map(map) do
{:ok, %__MODULE__{
text: Map.fetch!(map, "text"),
uri: Map.fetch!(map, "uri"),
mime_type: Map.get(map, "mimeType"),
meta: Map.get(map, "_meta")
}}
end
end
defmodule ACP.BlobResourceContents do
@moduledoc "Binary resource contents."
@type t :: %__MODULE__{
blob: String.t(),
mime_type: String.t() | nil,
uri: String.t(),
meta: map() | nil
}
@enforce_keys [:blob, :uri]
defstruct [:blob, :mime_type, :uri, :meta]
def new(blob, uri), do: %__MODULE__{blob: blob, uri: uri}
def to_json(%__MODULE__{} = brc) do
map = %{"blob" => brc.blob, "uri" => brc.uri}
map = if brc.mime_type, do: Map.put(map, "mimeType", brc.mime_type), else: map
if brc.meta, do: Map.put(map, "_meta", brc.meta), else: map
end
def from_json(map) when is_map(map) do
{:ok, %__MODULE__{
blob: Map.fetch!(map, "blob"),
uri: Map.fetch!(map, "uri"),
mime_type: Map.get(map, "mimeType"),
meta: Map.get(map, "_meta")
}}
end
end
defmodule ACP.EmbeddedResourceResource do
@moduledoc "Resource content that can be embedded in a message (untagged union)."
@type t :: ACP.TextResourceContents.t() | ACP.BlobResourceContents.t()
def to_json(%ACP.TextResourceContents{} = trc), do: ACP.TextResourceContents.to_json(trc)
def to_json(%ACP.BlobResourceContents{} = brc), do: ACP.BlobResourceContents.to_json(brc)
def from_json(%{"text" => _} = map), do: ACP.TextResourceContents.from_json(map)
def from_json(%{"blob" => _} = map), do: ACP.BlobResourceContents.from_json(map)
end
defmodule ACP.EmbeddedResource do
@moduledoc "The contents of a resource, embedded into a prompt or tool call result."
@type t :: %__MODULE__{
annotations: ACP.Annotations.t() | nil,
resource: ACP.EmbeddedResourceResource.t(),
meta: map() | nil
}
@enforce_keys [:resource]
defstruct [:annotations, :resource, :meta]
def new(resource), do: %__MODULE__{resource: resource}
def to_json(%__MODULE__{} = er) do
map = %{"resource" => ACP.EmbeddedResourceResource.to_json(er.resource)}
map = if er.annotations, do: Map.put(map, "annotations", ACP.Annotations.to_json(er.annotations)), else: map
if er.meta, do: Map.put(map, "_meta", er.meta), else: map
end
def from_json(map) when is_map(map) do
{:ok, resource} = ACP.EmbeddedResourceResource.from_json(Map.fetch!(map, "resource"))
{:ok, %__MODULE__{
resource: resource,
annotations: case Map.get(map, "annotations") do
nil -> nil
a -> {:ok, ann} = ACP.Annotations.from_json(a); ann
end,
meta: Map.get(map, "_meta")
}}
end
end
defmodule ACP.ResourceLink do
@moduledoc "A resource that the server is capable of reading."
@type t :: %__MODULE__{
annotations: ACP.Annotations.t() | nil,
description: String.t() | nil,
mime_type: String.t() | nil,
name: String.t(),
size: integer() | nil,
title: String.t() | nil,
uri: String.t(),
meta: map() | nil
}
@enforce_keys [:name, :uri]
defstruct [:annotations, :description, :mime_type, :name, :size, :title, :uri, :meta]
def new(name, uri), do: %__MODULE__{name: name, uri: uri}
def to_json(%__MODULE__{} = rl) do
map = %{"name" => rl.name, "uri" => rl.uri}
map = if rl.annotations, do: Map.put(map, "annotations", ACP.Annotations.to_json(rl.annotations)), else: map
map = if rl.description, do: Map.put(map, "description", rl.description), else: map
map = if rl.mime_type, do: Map.put(map, "mimeType", rl.mime_type), else: map
map = if rl.size, do: Map.put(map, "size", rl.size), else: map
map = if rl.title, do: Map.put(map, "title", rl.title), else: map
if rl.meta, do: Map.put(map, "_meta", rl.meta), else: map
end
def from_json(map) when is_map(map) do
{:ok, %__MODULE__{
name: Map.fetch!(map, "name"),
uri: Map.fetch!(map, "uri"),
annotations: case Map.get(map, "annotations") do
nil -> nil
a -> {:ok, ann} = ACP.Annotations.from_json(a); ann
end,
description: Map.get(map, "description"),
mime_type: Map.get(map, "mimeType"),
size: Map.get(map, "size"),
title: Map.get(map, "title"),
meta: Map.get(map, "_meta")
}}
end
end
defmodule ACP.ContentBlock do
@moduledoc "Content blocks represent displayable information in ACP."
@type t ::
{:text, ACP.TextContent.t()}
| {:image, ACP.ImageContent.t()}
| {:audio, ACP.AudioContent.t()}
| {:resource_link, ACP.ResourceLink.t()}
| {:resource, ACP.EmbeddedResource.t()}
def text(text_content), do: {:text, text_content}
def image(image_content), do: {:image, image_content}
def audio(audio_content), do: {:audio, audio_content}
def resource_link(rl), do: {:resource_link, rl}
def resource(er), do: {:resource, er}
@doc "Convenience: create a text content block from a string."
def from_string(text), do: {:text, ACP.TextContent.new(text)}
def to_json({:text, tc}), do: Map.put(ACP.TextContent.to_json(tc), "type", "text")
def to_json({:image, ic}), do: Map.put(ACP.ImageContent.to_json(ic), "type", "image")
def to_json({:audio, ac}), do: Map.put(ACP.AudioContent.to_json(ac), "type", "audio")
def to_json({:resource_link, rl}), do: Map.put(ACP.ResourceLink.to_json(rl), "type", "resource_link")
def to_json({:resource, er}), do: Map.put(ACP.EmbeddedResource.to_json(er), "type", "resource")
def from_json(%{"type" => "text"} = map) do
{:ok, tc} = ACP.TextContent.from_json(Map.delete(map, "type"))
{:ok, {:text, tc}}
end
def from_json(%{"type" => "image"} = map) do
{:ok, ic} = ACP.ImageContent.from_json(Map.delete(map, "type"))
{:ok, {:image, ic}}
end
def from_json(%{"type" => "audio"} = map) do
{:ok, ac} = ACP.AudioContent.from_json(Map.delete(map, "type"))
{:ok, {:audio, ac}}
end
def from_json(%{"type" => "resource_link"} = map) do
{:ok, rl} = ACP.ResourceLink.from_json(Map.delete(map, "type"))
{:ok, {:resource_link, rl}}
end
def from_json(%{"type" => "resource"} = map) do
{:ok, er} = ACP.EmbeddedResource.from_json(Map.delete(map, "type"))
{:ok, {:resource, er}}
end
end