defmodule Eai.Hook.ContextMeter do @moduledoc """ Observability hook: prints context size after every LLM HTTP response. Fires on :llm_post for each HTTP response (including tool-call intermediate steps), so the user can watch context grow in real-time and decide when to interrupt/summarize. Output examples: šŸ“Š [tool] context: 120.4 KB (~30.1K tokens), 18 messages šŸ“Š [final] context: 206.4 KB (~52.8K tokens), 22 messages Token estimate uses 4 bytes/token (English avg). This is a rough guide for the user to decide when to summarize/export — not an exact bill. """ use Eai.Hook, priority: 30 alias Eai.Message @impl true def interest(:llm_post, "LLM_REQUEST", _payload), do: true def interest(_event, _tool, _payload), do: false @impl true def verdict(:llm_post, _tool, _payload, result) do case result do {:ok, _reply, history} when is_list(history) and history != [] -> print_meter(history) _ -> :ok end end defp print_meter(history) do bytes = estimate_bytes(history) msg_count = length(history) token_est = div(bytes, 4) tag = context_tag(history) IO.puts( "\nšŸ“Š [#{tag}] context: #{format_bytes(bytes)} (~#{format_tokens(token_est)} tokens), #{msg_count} messages" ) end # ę ‡ę³Øå½“å‰äøŠäø‹ę–‡ēŠ¶ę€ļ¼šå·„å…·č°ƒē”Øäø­é—“ę­„čæ˜ę˜Æęœ€ē»ˆå›žå¤ defp context_tag(history) do last = List.last(history) case last.role do :assistant -> if Message.has_tool_uses?(last), do: "tool", else: "final" _ -> "step" end end # ä¼°ē®—ę¶ˆęÆåˆ—č”Øēš„åŗåˆ—åŒ–å­—čŠ‚ę•°ć€‚ defp estimate_bytes(history) do history |> :erlang.term_to_binary() |> byte_size() end defp format_bytes(bytes) when bytes >= 1024 * 1024 do :erlang.float_to_binary(bytes / (1024 * 1024), decimals: 1) <> " MB" end defp format_bytes(bytes) when bytes >= 1024 do :erlang.float_to_binary(bytes / 1024, decimals: 1) <> " KB" end defp format_bytes(bytes), do: "#{bytes} B" defp format_tokens(tokens) when tokens >= 1000 do :erlang.float_to_binary(tokens / 1000, decimals: 1) <> "K" end defp format_tokens(tokens), do: "#{tokens}" end