PtcRunner.SubAgent.Loop.ResponseHandler (PtcRunner v0.9.0)

Copy Markdown View Source

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

  1. Try extracting from clojure orlisp code blocks
  2. Fall back to raw s-expression starting with '('
  3. 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(fail)

@spec format_error_for_llm(map()) :: String.t()

Format error for LLM feedback.

format_execution_result(result, format_options \\ [])

@spec format_execution_result(
  term(),
  keyword()
) :: {String.t(), boolean()}

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_result(result, format_options \\ [])

@spec format_result(
  term(),
  keyword()
) :: String.t()

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]"

parse(response)

@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(response)

@spec strip_thinking(String.t()) :: String.t()

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_for_history(value, opts \\ [])

@spec truncate_for_history(
  term(),
  keyword()
) :: term()

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