Packages

Prompt Runner SDK - packet-first prompt execution for Elixir and CLI workflows with verifier-owned completion, retry, repair, and git-aware repository orchestration.

Current section

Files

Jump to
prompt_runner_sdk lib prompt_runner stream_renderer.ex
Raw

lib/prompt_runner/stream_renderer.ex

defmodule PromptRunner.StreamRenderer do
@moduledoc """
Renders a normalized event stream to the terminal and log files.
Supports `:compact` and `:verbose` display modes with ANSI colour output,
and writes structured JSON event logs.
"""
@red "\e[0;31m"
@green "\e[0;32m"
@yellow "\e[1;33m"
@blue "\e[0;34m"
@cyan "\e[0;36m"
@magenta "\e[0;35m"
@dim "\e[2m"
@nc "\e[0m"
@tool_preview_limit 120
@result_preview_limit 2000
@type loggers :: %{text_io: IO.device(), events_io: IO.device(), events_mode: atom()}
@spec stream(Enumerable.t(), loggers(), map(), %{mode: atom(), meta: atom()}) ::
:ok | {:error, term()}
def stream(stream, loggers, context, log_config) do
initial_state = %{
status: :ok,
in_text: false,
in_message: false,
line_open: false,
streaming: nil,
message_count: 0,
event_count: 0,
tool_count: 0,
tool_input: "",
current_tool: nil,
current_tool_id: nil,
last_stop_reason: nil,
log_mode: log_config.mode,
log_meta: log_config.meta
}
log_event_json(loggers, %{type: :session_start, context: context})
final_state =
Enum.reduce(stream, initial_state, fn event, state ->
state = %{state | event_count: state.event_count + 1}
log_event_json(loggers, event)
handle_event(event, state, loggers)
end)
log_event_json(loggers, %{type: :session_end, summary: session_summary(final_state)})
case final_state.status do
:ok -> :ok
other -> other
end
end
@doc """
Returns a single-line ANSI legend for compact-mode token symbols.
"""
def compact_legend_line do
compact_join([
dim_text("legend:"),
token_msg("m:") <> token_role("s") <> "=system",
token_msg("m:") <> token_role("u") <> "=user",
token_msg("m:") <> token_role("a") <> "=assistant",
token_msg("m+") <> "=start",
token_msg("m-") <> "=stop",
token_msg("tb+") <> "=text_block",
token_msg("cb-") <> "=content_block",
token_msg("md") <> "=delta",
token_tool("t+") <> "=tool_start",
token_tool("t-") <> "=tool_end",
token_stream(">>") <> "=text",
token_delta("<<") <> "=tool_input"
])
end
defp handle_event(event, %{log_mode: :compact} = state, loggers) do
handle_event_compact(event, state, loggers)
end
defp handle_event(event, state, loggers) do
handle_event_verbose(event, state, loggers)
end
defp handle_event_compact(%{type: :text_delta, text: text}, state, loggers) do
state =
compact_begin_stream(state, loggers, :assistant_text, stream_prefix(:assistant_text, state))
state = compact_emit_raw(loggers, state, text)
%{state | in_text: true}
end
defp handle_event_compact(
%{type: :message_start, model: model, role: role} = event,
state,
loggers
) do
state = compact_end_stream(state, loggers)
token = compact_message_start_token(model, role) <> compact_meta_suffix(event, state.log_meta)
state = compact_emit_token(loggers, state, token)
%{state | message_count: state.message_count + 1, in_message: true}
end
defp handle_event_compact(%{type: :text_block_start} = event, state, loggers) do
state = compact_end_stream(state, loggers)
token = token_msg("tb+") <> compact_meta_suffix(event, state.log_meta)
compact_emit_token(loggers, state, token)
end
defp handle_event_compact(%{type: :thinking_start} = event, state, loggers) do
state = compact_end_stream(state, loggers)
token = token_thinking("th+") <> compact_meta_suffix(event, state.log_meta)
compact_emit_token(loggers, state, token)
end
defp handle_event_compact(%{type: :thinking_delta, thinking: thinking} = event, state, loggers) do
state = compact_end_stream(state, loggers)
payload = dim_text(truncate(thinking, @tool_preview_limit))
token = token_thinking("th:") <> payload <> compact_meta_suffix(event, state.log_meta)
compact_emit_token(loggers, state, token)
end
defp handle_event_compact(%{type: :tool_use_start, name: name, id: id} = event, state, loggers) do
state = compact_end_stream(state, loggers)
token = token_tool("t+#{name}") <> compact_meta_suffix(event, state.log_meta)
state = compact_emit_token(loggers, state, token)
%{
state
| tool_count: state.tool_count + 1,
tool_input: "",
current_tool: name,
current_tool_id: id
}
end
defp handle_event_compact(%{type: :tool_input_delta} = event, state, loggers) do
json_chunk = Map.get(event, :json) || Map.get(event, :input) || ""
case json_chunk do
"" ->
state
_ ->
state =
compact_begin_stream(state, loggers, :tool_input, stream_prefix(:tool_input, state))
chunk = dim_text(truncate(json_chunk, @tool_preview_limit))
state = compact_emit_raw(loggers, state, chunk)
%{state | tool_input: state.tool_input <> json_chunk}
end
end
defp handle_event_compact(
%{type: :tool_complete, tool_name: tool_name, result: result} = event,
state,
loggers
) do
state = compact_end_stream(state, loggers)
token = token_tool("t-#{tool_name}") <> compact_meta_suffix(event, state.log_meta)
state = compact_emit_token(loggers, state, token)
state =
case result do
nil ->
state
"" ->
state
_ ->
compact_emit_token(
loggers,
state,
token_tool("tr:") <> dim_text(format_value(result, @result_preview_limit))
)
end
%{state | tool_input: "", current_tool: nil, current_tool_id: nil}
end
defp handle_event_compact(%{type: :tool_complete, tool_name: tool_name} = event, state, loggers) do
state = compact_end_stream(state, loggers)
token = token_tool("t-#{tool_name}") <> compact_meta_suffix(event, state.log_meta)
state = compact_emit_token(loggers, state, token)
%{state | tool_input: "", current_tool: nil, current_tool_id: nil}
end
defp handle_event_compact(
%{type: :content_block_stop, final_text: final_text} = event,
state,
loggers
) do
state = compact_end_stream(state, loggers)
token = token_msg("cb-") <> compact_meta_suffix(event, state.log_meta)
state = compact_emit_token(loggers, state, token)
state =
if final_text not in [nil, ""] and state.in_text == false do
compact_emit_token(
loggers,
state,
token_msg("ft:") <> dim_text(truncate(final_text, @tool_preview_limit))
)
else
state
end
state
end
defp handle_event_compact(%{type: :message_delta, stop_reason: reason} = event, state, loggers) do
state = compact_end_stream(state, loggers)
reason_label = short_reason(reason)
token =
token_msg("md" <> if(reason_label == "", do: "", else: ":#{reason_label}")) <>
compact_meta_suffix(event, state.log_meta)
state = compact_emit_token(loggers, state, token)
%{state | last_stop_reason: reason}
end
defp handle_event_compact(%{type: :message_stop} = event, state, loggers) do
state = compact_end_stream(state, loggers)
reason = Map.get(event, :stop_reason, state.last_stop_reason)
reason_label = short_reason(reason)
token =
token_msg("m-" <> if(reason_label == "", do: "", else: ":#{reason_label}")) <>
compact_meta_suffix(event, state.log_meta)
state = compact_emit_token(loggers, state, token)
state =
if Map.has_key?(event, :final_text) and event.final_text not in [nil, ""] and
state.in_text == false do
compact_emit_token(
loggers,
state,
token_msg("ft:") <> dim_text(format_value(event.final_text, @result_preview_limit))
)
else
state
end
state =
if Map.has_key?(event, :structured_output) and event.structured_output not in [nil, ""] do
compact_emit_token(
loggers,
state,
token_msg("so:") <>
dim_text(format_value(event.structured_output, @result_preview_limit))
)
else
state
end
state =
if Map.has_key?(event, :error) and event.error != nil do
compact_emit_token(loggers, state, token_error("ae:") <> dim_text(inspect(event.error)))
else
state
end
%{state | in_text: false, in_message: false, last_stop_reason: nil}
end
defp handle_event_compact(%{type: :error, error: error} = event, state, loggers) do
state = compact_end_stream(state, loggers)
token = token_error("!" <> inspect(error)) <> compact_meta_suffix(event, state.log_meta)
state = compact_emit_token(loggers, state, token)
%{state | status: {:error, error}}
end
defp handle_event_compact(
%{type: :error, error_type: error_type, message: message} = event,
state,
loggers
) do
state = compact_end_stream(state, loggers)
token =
token_error("!" <> inspect(error_type) <> " " <> inspect(message)) <>
compact_meta_suffix(event, state.log_meta)
state = compact_emit_token(loggers, state, token)
%{state | status: {:error, {error_type, message}}}
end
defp handle_event_compact(event, state, loggers) do
state = compact_end_stream(state, loggers)
token =
token_dim("?") <>
dim_text(truncate(inspect(event), @tool_preview_limit)) <>
compact_meta_suffix(event, state.log_meta)
compact_emit_token(loggers, state, token)
end
defp handle_event_verbose(%{type: :text_delta, text: text}, state, loggers) do
emit_text(loggers, text)
%{state | in_text: true}
end
defp handle_event_verbose(
%{type: :message_start, model: model, role: role} = event,
state,
loggers
) do
state = maybe_break_line(state, loggers)
usage = Map.get(event, :usage)
usage_info =
if usage in [nil, %{}], do: "", else: " usage=#{format_value(usage, @tool_preview_limit)}"
line =
"[message_start] model=#{inspect(model)} role=#{inspect(role)}#{usage_info}#{format_meta(event, state.log_meta)}"
emit_line(loggers, line)
%{state | message_count: state.message_count + 1}
end
defp handle_event_verbose(%{type: :text_block_start} = event, state, loggers) do
state = maybe_break_line(state, loggers)
emit_line(loggers, "[text_block_start]#{format_meta(event, state.log_meta)}")
state
end
defp handle_event_verbose(%{type: :thinking_start} = event, state, loggers) do
state = maybe_break_line(state, loggers)
emit_line(loggers, "[thinking_start]#{format_meta(event, state.log_meta)}")
state
end
defp handle_event_verbose(%{type: :thinking_delta, thinking: thinking} = event, state, loggers) do
state = maybe_break_line(state, loggers)
emit_line(
loggers,
"[thinking_delta] #{truncate(thinking, @tool_preview_limit)}#{format_meta(event, state.log_meta)}"
)
state
end
defp handle_event_verbose(%{type: :tool_use_start, name: name, id: id} = event, state, loggers) do
state = maybe_break_line(state, loggers)
emit_line(
loggers,
"[tool_use_start] name=#{name} id=#{id}#{format_meta(event, state.log_meta)}"
)
%{
state
| tool_count: state.tool_count + 1,
tool_input: "",
current_tool: name,
current_tool_id: id
}
end
defp handle_event_verbose(%{type: :tool_input_delta} = event, state, loggers) do
json_chunk = Map.get(event, :json) || Map.get(event, :input) || ""
state = maybe_break_line(state, loggers)
if json_chunk != "" do
emit_line(
loggers,
"[tool_input_delta] #{truncate(json_chunk, @tool_preview_limit)}#{format_meta(event, state.log_meta)}"
)
else
emit_line(loggers, "[tool_input_delta] (empty)#{format_meta(event, state.log_meta)}")
end
%{state | tool_input: state.tool_input <> json_chunk}
end
defp handle_event_verbose(
%{type: :tool_complete, tool_name: tool_name, result: result} = event,
state,
loggers
) do
state = maybe_break_line(state, loggers)
emit_line(
loggers,
"[tool_complete] name=#{tool_name} id=#{state.current_tool_id}#{format_meta(event, state.log_meta)}"
)
display_tool_execution(state.current_tool, state.tool_input, result, loggers)
%{state | tool_input: "", current_tool: nil, current_tool_id: nil}
end
defp handle_event_verbose(%{type: :tool_complete, tool_name: tool_name} = event, state, loggers) do
state = maybe_break_line(state, loggers)
emit_line(
loggers,
"[tool_complete] name=#{tool_name} id=#{state.current_tool_id}#{format_meta(event, state.log_meta)}"
)
display_tool_execution(state.current_tool, state.tool_input, nil, loggers)
%{state | tool_input: "", current_tool: nil, current_tool_id: nil}
end
defp handle_event_verbose(
%{type: :content_block_stop, final_text: final_text} = event,
state,
loggers
) do
state = maybe_break_line(state, loggers)
emit_line(
loggers,
"[content_block_stop] final_text=#{truncate(final_text, @tool_preview_limit)}#{format_meta(event, state.log_meta)}"
)
state
end
defp handle_event_verbose(
%{type: :message_delta, stop_reason: reason, stop_sequence: sequence} = event,
state,
loggers
) do
state = maybe_break_line(state, loggers)
emit_line(
loggers,
"[message_delta] stop_reason=#{inspect(reason)} stop_sequence=#{inspect(sequence)}#{format_meta(event, state.log_meta)}"
)
%{state | last_stop_reason: reason}
end
defp handle_event_verbose(%{type: :message_stop} = event, state, loggers) do
state = maybe_break_line(state, loggers)
emit_line(
loggers,
"[message_stop] stop_reason=#{inspect(state.last_stop_reason)}#{format_meta(event, state.log_meta)}"
)
if Map.has_key?(event, :final_text) do
emit_line(loggers, "[final_text] #{format_value(event.final_text, @result_preview_limit)}")
end
if Map.has_key?(event, :structured_output) do
emit_line(
loggers,
"[structured_output] #{format_value(event.structured_output, @result_preview_limit)}"
)
end
if Map.has_key?(event, :error) and event.error != nil do
emit_line(loggers, "[assistant_error] #{inspect(event.error)}")
end
%{state | in_text: false, last_stop_reason: nil}
end
defp handle_event_verbose(%{type: :error, error: error} = event, state, loggers) do
state = maybe_break_line(state, loggers)
emit_line(loggers, "[error] #{inspect(error)}#{format_meta(event, state.log_meta)}")
%{state | status: {:error, error}}
end
defp handle_event_verbose(
%{type: :error, error_type: error_type, message: message} = event,
state,
loggers
) do
state = maybe_break_line(state, loggers)
emit_line(
loggers,
"[error] #{inspect(error_type)} #{inspect(message)}#{format_meta(event, state.log_meta)}"
)
%{state | status: {:error, {error_type, message}}}
end
defp handle_event_verbose(event, state, loggers) do
state = maybe_break_line(state, loggers)
emit_line(loggers, "[event] #{inspect(event)}")
state
end
defp log_event_json(loggers, event) do
mode = Map.get(loggers, :events_mode, :full)
case mode do
:off ->
:ok
:full ->
entry = %{
ts: DateTime.utc_now() |> DateTime.to_iso8601(),
event: sanitize_event(event)
}
IO.binwrite(loggers.events_io, Jason.encode!(entry) <> "\n")
:compact ->
entry = compact_event_entry(event)
IO.binwrite(loggers.events_io, Jason.encode!(entry) <> "\n")
end
end
defp sanitize_event(event), do: event
defp compact_event_entry(event) do
%{
t: System.system_time(:millisecond),
e: compact_event(event)
}
end
defp compact_event(%{type: :session_start, context: %{prompt: prompt}}) when is_map(prompt) do
%{
"t" => short_event_type(:session_start),
"p" => Map.get(prompt, :num)
}
|> drop_nil_values()
end
defp compact_event(%{type: :session_start}) do
%{"t" => short_event_type(:session_start)}
end
defp compact_event(%{type: :session_end, summary: summary}) when is_map(summary) do
%{
"t" => short_event_type(:session_end),
"e" => Map.get(summary, :event_count),
"m" => Map.get(summary, :message_count),
"tl" => Map.get(summary, :tool_count)
}
|> drop_nil_values()
end
defp compact_event(%{type: :message_start, model: model, role: role}) do
%{
"t" => short_event_type(:message_start),
"r" => short_role(role),
"m" => short_model(model)
}
|> drop_nil_values()
end
defp compact_event(%{type: :text_delta, text: text}) do
%{
"t" => short_event_type(:text_delta),
"l" => text_length(text)
}
end
defp compact_event(%{type: :text_block_start}) do
%{"t" => short_event_type(:text_block_start)}
end
defp compact_event(%{type: :thinking_start}) do
%{"t" => short_event_type(:thinking_start)}
end
defp compact_event(%{type: :thinking_delta, thinking: thinking}) do
%{
"t" => short_event_type(:thinking_delta),
"l" => text_length(thinking)
}
end
defp compact_event(%{type: :tool_use_start, name: name}) do
%{
"t" => short_event_type(:tool_use_start),
"n" => name
}
|> drop_nil_values()
end
defp compact_event(%{type: :tool_input_delta} = event) do
chunk = Map.get(event, :json) || Map.get(event, :input) || ""
%{
"t" => short_event_type(:tool_input_delta),
"l" => text_length(chunk)
}
end
defp compact_event(%{type: :tool_complete, tool_name: tool_name, result: result}) do
%{
"t" => short_event_type(:tool_complete),
"n" => tool_name,
"l" => text_length(result)
}
|> drop_nil_values()
end
defp compact_event(%{type: :content_block_stop, final_text: final_text}) do
%{
"t" => short_event_type(:content_block_stop),
"l" => text_length(final_text)
}
end
defp compact_event(%{type: :message_delta, stop_reason: reason}) do
%{
"t" => short_event_type(:message_delta),
"sr" => short_reason(reason)
}
|> drop_nil_values()
end
defp compact_event(%{type: :message_stop, stop_reason: reason}) do
%{
"t" => short_event_type(:message_stop),
"sr" => short_reason(reason)
}
|> drop_nil_values()
end
defp compact_event(%{type: :error, error: error}) do
%{
"t" => short_event_type(:error),
"x" => truncate(inspect(error), @tool_preview_limit)
}
end
defp compact_event(%{type: :error, error_type: error_type, message: message}) do
%{
"t" => short_event_type(:error),
"x" => truncate("#{inspect(error_type)} #{inspect(message)}", @tool_preview_limit)
}
end
defp compact_event(event) do
%{
"t" => short_event_type(Map.get(event, :type)),
"x" => truncate(inspect(event), @tool_preview_limit)
}
end
@spec emit_line(loggers(), String.t()) :: :ok
def emit_line(loggers, line) do
IO.puts(line)
IO.binwrite(loggers.text_io, strip_ansi(line) <> "\n")
:ok
end
defp emit_text(loggers, text) do
IO.write(text)
IO.binwrite(loggers.text_io, strip_ansi(text))
end
defp strip_ansi(text) do
String.replace(text, ~r/\x1b\[[0-9;]*m/, "")
end
defp compact_emit_token(loggers, state, token) do
prefix = if state.line_open, do: " ", else: ""
emit_text(loggers, prefix <> token)
%{state | line_open: true}
end
defp compact_emit_raw(loggers, state, text) do
emit_text(loggers, text)
clean = strip_ansi(text)
%{state | line_open: not String.ends_with?(clean, "\n")}
end
defp compact_begin_stream(state, loggers, stream_type, prefix) do
state =
if state.streaming != stream_type do
compact_end_stream(state, loggers)
else
state
end
state =
if state.streaming == nil and state.line_open do
emit_text(loggers, " ")
%{state | line_open: true}
else
state
end
if state.streaming == stream_type do
state
else
state = compact_emit_raw(loggers, state, prefix)
%{state | streaming: stream_type}
end
end
defp compact_end_stream(%{streaming: nil} = state, _loggers), do: state
defp compact_end_stream(state, loggers) do
state =
if state.line_open do
emit_text(loggers, "\n")
%{state | line_open: false}
else
state
end
%{state | streaming: nil}
end
defp maybe_break_line(%{in_text: true} = state, loggers) do
emit_line(loggers, "")
%{state | in_text: false}
end
defp maybe_break_line(state, _loggers), do: state
defp format_meta(_event, meta_mode) when meta_mode in [nil, :none], do: ""
defp format_meta(event, _meta_mode) do
meta =
[
{:session_id, Map.get(event, :session_id)},
{:uuid, Map.get(event, :uuid)},
{:parent_tool_use_id, Map.get(event, :parent_tool_use_id)}
]
|> Enum.filter(fn {_k, v} -> v not in [nil, ""] end)
|> Enum.map_join(" ", fn {k, v} -> "#{k}=#{v}" end)
if meta == "", do: "", else: " #{meta}"
end
defp colorize(text, color) when is_binary(text) do
if text == "" do
""
else
color <> text <> @nc
end
end
defp dim_text(text), do: colorize(text, @dim)
defp token_msg(text), do: colorize(text, @blue)
defp token_role(text), do: colorize(text, @cyan)
defp token_model(text), do: colorize(text, @magenta)
defp token_stream(text), do: colorize(text, @magenta)
defp token_tool(text), do: colorize(text, @green)
defp token_delta(text), do: colorize(text, @yellow)
defp token_thinking(text), do: colorize(text, @yellow)
defp token_error(text), do: colorize(text, @red)
defp token_dim(text), do: colorize(text, @dim)
defp compact_join(parts) do
parts
|> Enum.filter(&(&1 not in [nil, ""]))
|> Enum.join(" ")
end
defp compact_meta_suffix(event, meta_mode) do
meta = format_meta(event, meta_mode)
if meta == "", do: "", else: dim_text(meta)
end
defp drop_nil_values(map) when is_map(map) do
map
|> Enum.reject(fn {_k, v} -> v in [nil, ""] end)
|> Enum.into(%{})
end
defp stream_prefix(:assistant_text, _state) do
token_stream(">>") <> " "
end
defp stream_prefix(:tool_input, state) do
tool_label =
if state.current_tool in [nil, ""] do
""
else
token_tool(state.current_tool)
end
prefix = compact_join([token_delta("<<"), tool_label])
if prefix == "", do: "", else: prefix <> " "
end
defp compact_message_start_token(model, role) do
role_label = short_role(role)
model_label = short_model(model)
compact_join([
token_msg("m+"),
token_role(role_label),
token_model(model_label)
])
end
defp short_role(nil), do: ""
defp short_role(role) when is_atom(role), do: short_role(Atom.to_string(role))
defp short_role(role) when is_binary(role) do
case String.downcase(role) do
"assistant" -> "a"
"user" -> "u"
"system" -> "s"
other -> String.first(other) || ""
end
end
defp short_model(nil), do: ""
defp short_model(model) when is_atom(model), do: short_model(Atom.to_string(model))
defp short_model(model) when is_binary(model) do
model
|> String.replace_prefix("claude-", "")
|> String.replace_prefix("anthropic.", "")
|> String.split("-", trim: true)
|> Enum.take(3)
|> Enum.join("-")
end
defp short_reason(nil), do: ""
defp short_reason(reason) when is_atom(reason), do: short_reason(Atom.to_string(reason))
defp short_reason(reason) when is_binary(reason) do
case String.downcase(reason) do
"tool_use" -> "tool"
"end_turn" -> "end"
other -> other
end
end
defp short_event_type(nil), do: "ev"
defp short_event_type(type) when is_atom(type), do: short_event_type(Atom.to_string(type))
@event_type_labels %{
"session_start" => "ss",
"session_end" => "se",
"message_start" => "ms",
"message_stop" => "m-",
"message_delta" => "md",
"message" => "m",
"text_delta" => "td",
"text_block_start" => "tb",
"content_block_stop" => "cb",
"thinking_start" => "ts",
"thinking_delta" => "th",
"tool_use_start" => "tu",
"tool_input_delta" => "ti",
"tool_complete" => "tc",
"error" => "e"
}
defp short_event_type(type) when is_binary(type) do
Map.get(@event_type_labels, type, "ev")
end
defp text_length(nil), do: nil
defp text_length(text) when is_binary(text), do: byte_size(text)
defp text_length(text) do
case Jason.encode(text) do
{:ok, json} -> byte_size(json)
_ -> byte_size(inspect(text))
end
end
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 format_value(value, limit) when is_binary(value) do
truncate(value, limit)
end
defp format_value(value, limit) when is_map(value) or is_list(value) do
case Jason.encode(value) do
{:ok, json} -> truncate(json, limit)
_ -> truncate(inspect(value), limit)
end
end
defp format_value(value, limit) do
truncate(inspect(value), limit)
end
defp session_summary(state) do
%{
event_count: state.event_count,
message_count: state.message_count,
tool_count: state.tool_count
}
end
defp display_tool_execution(tool_name, tool_input, result, loggers) do
input_map = decode_tool_input(tool_input)
if input_map != nil do
emit_line(
loggers,
"[tool_input] #{tool_name}: #{format_value(input_map, @result_preview_limit)}"
)
end
if result not in [nil, ""] do
emit_line(
loggers,
"[tool_result] #{tool_name}: #{format_value(result, @result_preview_limit)}"
)
end
emit_line(loggers, "[tool_done] #{tool_name}")
end
defp decode_tool_input(tool_input) when is_binary(tool_input) do
if tool_input == "" do
nil
else
case Jason.decode(tool_input) do
{:ok, input_map} -> input_map
_ -> %{"raw" => tool_input}
end
end
end
end