Current section

Files

Jump to
agent_session_manager lib asm extensions rendering renderers compact.ex
Raw

lib/asm/extensions/rendering/renderers/compact.ex

defmodule ASM.Extensions.Rendering.Renderers.Compact do
@moduledoc """
Compact token renderer for `%ASM.Event{}` streams.
"""
@behaviour ASM.Extensions.Rendering.Renderer
alias ASM.{Content, Control, Event, Message}
@red "\e[0;31m"
@green "\e[0;32m"
@blue "\e[0;34m"
@magenta "\e[0;35m"
@dim "\e[2m"
@nc "\e[0m"
@preview_limit 240
@impl true
def init(opts) do
{:ok,
%{
color: Keyword.get(opts, :color, true),
event_count: 0,
tool_count: 0,
streaming_text?: false,
line_open?: false
}}
end
@impl true
def render_event(%Event{} = event, state) do
state = %{state | event_count: state.event_count + 1}
{iodata, state} = render(event, state)
{:ok, iodata, state}
end
@impl true
def finish(state) do
closing = if state.line_open?, do: "\n", else: ""
summary =
[
"\n",
dim("#{state.event_count} events, #{state.tool_count} tools\n", state)
]
{:ok, [closing, summary], state}
end
defp render(%Event{kind: :run_started}, state) do
start_token(state, [colorize("r+", @blue, state), provider_suffix(state, :run_started)])
end
defp render(
%Event{
kind: :assistant_delta,
payload: %Message.Partial{content_type: :text, delta: delta}
},
state
)
when is_binary(delta) do
if state.streaming_text? do
{delta, %{state | line_open?: true}}
else
prefix = [maybe_space(state), colorize(">>", @magenta, state), " "]
{[prefix, delta], %{state | streaming_text?: true, line_open?: true}}
end
end
defp render(
%Event{kind: :assistant_message, payload: %Message.Assistant{content: content}},
state
) do
text = content |> extract_text_blocks() |> Enum.join() |> truncate(@preview_limit)
start_token(state, [colorize("msg", @blue, state), " ", dim(text, state)])
end
defp render(%Event{kind: :tool_use, payload: %Message.ToolUse{} = payload}, state) do
token = [colorize("t+#{payload.tool_name}", @green, state), tool_input_suffix(payload, state)]
{iodata, state} = start_token(state, token)
{iodata, %{state | tool_count: state.tool_count + 1}}
end
defp render(%Event{kind: :tool_result, payload: %Message.ToolResult{} = payload}, state) do
token = [
colorize("t-#{payload.tool_id}", @green, state),
tool_result_suffix(payload, state)
]
start_token(state, token)
end
defp render(%Event{kind: :result, payload: %Message.Result{} = payload}, state) do
reason = payload.stop_reason |> normalize_stop_reason() |> short_reason()
token =
if reason == "" do
colorize("r-", @blue, state)
else
colorize("r-:#{reason}", @blue, state)
end
usage_suffix = result_usage_suffix(payload.usage, state)
start_token(state, [token, usage_suffix])
end
defp render(%Event{kind: :run_completed, payload: %Control.RunLifecycle{status: status}}, state) do
start_token(state, colorize("rc:#{status}", @blue, state))
end
defp render(%Event{kind: :error, payload: %Message.Error{} = payload}, state) do
token = [
colorize("!", @red, state),
" ",
dim(truncate(payload.message, @preview_limit), state)
]
start_token(state, token)
end
defp render(%Event{kind: kind}, state) do
start_token(state, [dim("?", state), dim(Atom.to_string(kind), state)])
end
defp start_token(state, token) do
prefix = [stream_closing(state), maybe_space(state)]
{[prefix, token], %{state | streaming_text?: false, line_open?: true}}
end
defp stream_closing(%{streaming_text?: true}), do: "\n"
defp stream_closing(_state), do: ""
defp maybe_space(%{line_open?: true, streaming_text?: false}), do: " "
defp maybe_space(%{line_open?: true, streaming_text?: true}), do: ""
defp maybe_space(_state), do: ""
defp colorize(text, color, %{color: true}), do: color <> text <> @nc
defp colorize(text, _color, _state), do: text
defp dim(text, %{color: true}), do: @dim <> text <> @nc
defp dim(text, _state), do: text
defp provider_suffix(_state, _kind), do: ""
defp tool_input_suffix(%Message.ToolUse{input: input}, _state) when input in [%{}, nil], do: ""
defp tool_input_suffix(%Message.ToolUse{input: input}, state) do
[" ", dim(truncate(inspect(input), @preview_limit), state)]
end
defp tool_result_suffix(%Message.ToolResult{content: nil}, _state), do: ""
defp tool_result_suffix(%Message.ToolResult{content: content}, state) do
[" ", dim(truncate(inspect(content), @preview_limit), state)]
end
defp result_usage_suffix(usage, state) when is_map(usage) do
input_tokens = Map.get(usage, :input_tokens) || Map.get(usage, "input_tokens")
output_tokens = Map.get(usage, :output_tokens) || Map.get(usage, "output_tokens")
case {input_tokens, output_tokens} do
{nil, nil} -> ""
{in_t, out_t} -> [" ", dim("tk:#{default_zero(in_t)}/#{default_zero(out_t)}", state)]
end
end
defp result_usage_suffix(_usage, _state), do: ""
defp default_zero(nil), do: 0
defp default_zero(value), do: value
defp normalize_stop_reason(reason) when is_atom(reason), do: Atom.to_string(reason)
defp normalize_stop_reason(reason) when is_binary(reason), do: reason
defp normalize_stop_reason(_other), do: ""
defp short_reason("end_turn"), do: "end"
defp short_reason("tool_use"), do: "tool"
defp short_reason(reason), do: reason
defp extract_text_blocks(blocks) when is_list(blocks) do
Enum.flat_map(blocks, fn
%Content.Text{text: text} when is_binary(text) -> [text]
_other -> []
end)
end
defp extract_text_blocks(_other), do: []
defp truncate(nil, _limit), do: ""
defp truncate(text, limit) when is_binary(text) do
if String.length(text) > limit do
String.slice(text, 0, limit) <> "..."
else
text
end
end
defp truncate(other, limit), do: truncate(inspect(other), limit)
end