Response parsing and validation for LLM responses.
This module handles extracting PTC-Lisp code from LLM responses and formatting execution results for LLM feedback.
Parsing Strategy
- Try extracting from
clojure orlisp code blocks - Fall back to raw s-expression starting with '('
- Multiple code blocks return an error (LLM must provide exactly one)
Summary
Functions
Format error for LLM feedback.
Format execution result for LLM feedback.
Format final result for caller.
Parse PTC-Lisp from LLM response.
Strip thinking/reasoning text that precedes a code block.
Truncate a result value for storage in turn history.
Functions
Format error for LLM feedback.
Format execution result for LLM feedback.
REPL-style output: just the expression result, no prefix.
Use def to explicitly store values that persist across turns.
Returns {formatted_string, truncated?} tuple.
Options
Uses format_options from SubAgent:
:feedback_limit- Max collection items (default: 10):feedback_max_chars- Max chars in feedback (default: 512)
Examples
iex> PtcRunner.SubAgent.Loop.ResponseHandler.format_execution_result(42)
{"42", false}
iex> PtcRunner.SubAgent.Loop.ResponseHandler.format_execution_result(%{count: 5})
{"{:count 5}", false}
Format final result for caller.
Uses format_options from SubAgent:
:result_limit- Inspect limit for collections (default: 50):result_max_chars- Max chars in result (default: 500)
Examples
iex> PtcRunner.SubAgent.Loop.ResponseHandler.format_result(42)
"42"
iex> PtcRunner.SubAgent.Loop.ResponseHandler.format_result(3.14159)
"3.14"
iex> PtcRunner.SubAgent.Loop.ResponseHandler.format_result([1, 2, 3])
"[1, 2, 3]"
@spec parse(String.t()) :: {:ok, String.t()} | {:error, :no_code_in_response} | {:error, {:multiple_code_blocks, pos_integer()}}
Parse PTC-Lisp from LLM response.
Sanitizes LLM output by removing invisible Unicode characters (BOM, zero-width spaces) and normalizing smart quotes to ASCII equivalents.
Examples
iex> PtcRunner.SubAgent.Loop.ResponseHandler.parse("```clojure\n(+ 1 2)\n```")
{:ok, "(+ 1 2)"}
iex> PtcRunner.SubAgent.Loop.ResponseHandler.parse("(return {:result 42})")
{:ok, "(return {:result 42})"}
iex> PtcRunner.SubAgent.Loop.ResponseHandler.parse("I'm thinking about this...")
{:error, :no_code_in_response}Returns
{:ok, code}- Successfully extracted code string{:error, :no_code_in_response}- No valid PTC-Lisp found{:error, {:multiple_code_blocks, count}}- More than one code block found
Strip thinking/reasoning text that precedes a code block.
LLMs sometimes produce prose (e.g. thinking: blocks) before the code block,
even when not requested. This text wastes tokens in message history and
reinforces the pattern on subsequent turns. Stripping it keeps only the code
block for history while the raw response is preserved in traces.
Returns the response unchanged if no code block is found.
Examples
iex> PtcRunner.SubAgent.Loop.ResponseHandler.strip_thinking("thinking:\nSome reasoning\n```clojure\n(+ 1 2)\n```")
"```clojure\n(+ 1 2)\n```"
iex> PtcRunner.SubAgent.Loop.ResponseHandler.strip_thinking("```clojure\n(+ 1 2)\n```")
"```clojure\n(+ 1 2)\n```"
iex> PtcRunner.SubAgent.Loop.ResponseHandler.strip_thinking("(+ 1 2)")
"(+ 1 2)"
Truncate a result value for storage in turn history.
Large results are truncated to prevent memory bloat. The default limit is 1KB. Truncation preserves structure where possible:
- Lists: keeps first N elements that fit
- Maps: keeps first N key-value pairs that fit
- Strings: truncates with "..." suffix
- Other values: converted to truncated string representation
Options
:max_bytes- Maximum size in bytes (default: 1024)
Examples
iex> PtcRunner.SubAgent.Loop.ResponseHandler.truncate_for_history([1, 2, 3])
[1, 2, 3]
iex> result = PtcRunner.SubAgent.Loop.ResponseHandler.truncate_for_history(String.duplicate("x", 2000))
iex> byte_size(result) <= 1024
true