# SPDX-FileCopyrightText: 2026 James Harton # # SPDX-License-Identifier: Apache-2.0 defmodule BB.MCP.EventBuffer.Serializer do @moduledoc """ Convert `BB.MCP.EventBuffer` entries into JSON-safe maps for tool replies. BB message payloads may contain Nx tensors (Vec3, Quaternion, Transform), large binaries (camera images), or other non-JSON-encodable values. We render those via `inspect/1` rather than failing the Jason encode. """ alias BB.MCP.EventBuffer @max_binary_bytes 64 @doc """ Serialise a buffer entry. `now_ns` is the reference monotonic time used to compute `age_ms`, so the agent can reason about how recent each event is. """ @spec serialise(EventBuffer.entry(), integer()) :: map() def serialise(entry, now_ns) do %{ "robot" => entry.robot, "path" => EventBuffer.path_to_string(entry.path), "type" => format_module(entry.payload_module), "frame_id" => format_atom(entry.message.frame_id), "monotonic_ns" => entry.monotonic_ns, "age_ms" => div(now_ns - entry.received_ns, 1_000_000), "payload" => jsonable(entry.message.payload) } end defp format_module(nil), do: nil defp format_module(module) when is_atom(module), do: inspect(module) defp format_atom(nil), do: nil defp format_atom(atom) when is_atom(atom), do: Atom.to_string(atom) defp jsonable(%Nx.Tensor{} = tensor), do: inspect(tensor) defp jsonable(%_{} = struct) do struct |> Map.from_struct() |> Map.new(fn {k, v} -> {Atom.to_string(k), jsonable(v)} end) end defp jsonable(value) when is_map(value) do Map.new(value, fn {k, v} -> {jsonable_key(k), jsonable(v)} end) end defp jsonable(value) when is_list(value), do: Enum.map(value, &jsonable/1) defp jsonable(value) when is_tuple(value), do: value |> Tuple.to_list() |> Enum.map(&jsonable/1) defp jsonable(nil), do: nil defp jsonable(value) when is_boolean(value), do: value defp jsonable(value) when is_atom(value), do: Atom.to_string(value) defp jsonable(value) when is_number(value), do: value defp jsonable(value) when is_binary(value), do: render_binary(value) defp jsonable(value), do: inspect(value) defp jsonable_key(k) when is_atom(k), do: Atom.to_string(k) defp jsonable_key(k) when is_binary(k), do: k defp jsonable_key(k), do: inspect(k) defp render_binary(value) do if String.valid?(value) and byte_size(value) <= @max_binary_bytes do value else "<#{byte_size(value)} bytes>" end end end