Current section

Files

Jump to
eai config hooks 02_session_log.exs
Raw

config/hooks/02_session_log.exs

defmodule Eai.Hook.SessionLog do
@moduledoc """
Observability hook: fires telemetry for every tool call and LLM request.
Covers all four event types (:pre, :post, :llm_pre, :llm_post).
Does not block or modify — pure observation.
"""
use Eai.Hook, priority: 20
@impl true
def interest(_event, _tool_name, _payload), do: true
# Tool pre-hooks: payload shape is %{mod, fun, args: [...]}.
@impl true
def verdict(:pre, tool_name, payload) do
args = Map.get(payload, :args, [])
args_count =
args
|> List.wrap()
|> length()
:telemetry.execute(
[:eai, :hook, :session_log, :pre],
%{system_time: System.system_time()},
%{tool: tool_name, args_count: args_count}
)
:ok
end
# LLM pre-hooks: payload shape is %{messages, pty_session_id, chat_session_id, opts}.
# There's no :args key here — that's what caused the original
# FunctionClauseError. We log a message count + a flag instead.
@impl true
def verdict(:llm_pre, tool_name, payload) do
messages = Map.get(payload, :messages, [])
opts = Map.get(payload, :opts, %{})
has_tools? = match?(%{tools: tools} when is_list(tools) and tools != [], opts)
:telemetry.execute(
[:eai, :hook, :session_log, :llm_pre],
%{system_time: System.system_time()},
%{tool: tool_name, message_count: length(List.wrap(messages)), has_tools: has_tools?}
)
:ok
end
@impl true
def verdict(event, tool_name, _payload, result) when event in [:post, :llm_post] do
result_bytes =
case result do
s when is_binary(s) -> byte_size(s)
l when is_list(l) -> length(l)
_ -> 1
end
:telemetry.execute(
[:eai, :hook, :session_log, event],
%{system_time: System.system_time()},
%{tool: tool_name, result_bytes: result_bytes}
)
:ok
end
end