# `PtcRunner.SubAgent.Loop.ResponseHandler`
[🔗](https://github.com/andreasronge/ptc_runner/blob/main/lib/ptc_runner/sub_agent/loop/response_handler.ex#L1)

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 or ```lisp code blocks
2. Fall back to raw s-expression starting with '('
3. Multiple code blocks return an error (LLM must provide exactly one)

# `extract_fenced_blocks`

```elixir
@spec extract_fenced_blocks(String.t()) :: [{String.t(), String.t()}]
```

Extract fenced code blocks from a Markdown response using line-by-line parsing.

Unlike regex-based extraction, this correctly handles backtick fences that
appear inside string literals (mid-line). Only lines whose trimmed content
matches the fence pattern are treated as fences.

Returns a list of `{language_tag, content}` tuples.

## Examples

    iex> PtcRunner.SubAgent.Loop.ResponseHandler.extract_fenced_blocks("```clojure\n(+ 1 2)\n```")
    [{"clojure", "(+ 1 2)"}]

    iex> PtcRunner.SubAgent.Loop.ResponseHandler.extract_fenced_blocks("no code here")
    []

# `extract_fenced_blocks_with_lines`

```elixir
@spec extract_fenced_blocks_with_lines(String.t()) :: [
  {String.t(), String.t(), String.t(), String.t()}
]
```

Like `extract_fenced_blocks/1` but also returns the original opening and
closing fence lines, enabling callers to locate or replace the exact source
text (including XML-style closers and whitespace variations).

Returns a list of `{language_tag, content, opening_line, closing_line}` tuples.

## Examples

    iex> PtcRunner.SubAgent.Loop.ResponseHandler.extract_fenced_blocks_with_lines("```clojure\n(+ 1 2)\n```")
    [{"clojure", "(+ 1 2)", "```clojure", "```"}]

    iex> PtcRunner.SubAgent.Loop.ResponseHandler.extract_fenced_blocks_with_lines("```clojure\n(+ 1 2)\n</clojure>")
    [{"clojure", "(+ 1 2)", "```clojure", "</clojure>"}]

# `format_error_for_llm`

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

Format error for LLM feedback.

# `format_execution_result`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
