Code Execution › Execution Feedback

Michael BrenndoerferFebruary 4, 202610 min read
Language AI HandbookAll chapters
Part 47 of 67Code Generation
Chapter 361Code Execution › Execution Feedback
Reading section 3 of 9 · Execution Feedback · 10 minSection 3 of 9 · 10 min

Book contents

Section 3 of 9 · 10 min read

Code Execution › Execution FeedbackOne section · Sections in this chapter3 / 9 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Sandboxing allows code to run safely. Execution feedback is what makes that execution useful for improving the code. The output produced by execution, whether a successful result, an error message, a stack trace, or a test report, is information that can be fed back into the LLM to guide the next generation step.

The fundamental insight behind execution feedback as a learning signal is that code errors are structured and informative. Unlike a generic "your code is wrong" message, a Python TypeError: unsupported operand type(s) for +: 'int' and 'str' tells you exactly what went wrong, where, and what the types involved were. A failing assertion AssertionError: assert sorted(result) == [0, 1], but got [1, 2] tells you that the function returned wrong values and what the correct values should be. This structural richness is what allows an LLM to act on execution feedback rather than just knowing that something failed.

Capturing Execution Output

Execution feedback begins by capturing of everything the code produces. A reliable execution engine captures:

  • Standard output (stdout): The text printed by the program, typically the intended result.
  • Standard error (stderr): Error messages and warnings. In Python, uncaught exceptions print their tracebacks here.
  • Exit code: Zero for success, non-zero for failure. This provides an immediate signal about whether execution completed normally.
  • Execution time: How long the code took to run, useful for diagnosing performance issues.
  • Exception type and message: When an exception is raised, the type (TypeError, KeyError, etc.) and message provide structured information about what went wrong.
  • Stack trace: The full traceback showing which line caused the error and the call chain leading to it.
In[6]:
Code
import subprocess
import textwrap
import time


def execute_code(code: str, timeout: int = 10) -> dict:
    """
    Execute Python code in a subprocess and capture all output.
    Returns a dict with stdout, stderr, exit_code, elapsed_time.
    """
    # Write code to a temp file to avoid shell injection issues
    import os
    import tempfile

    with tempfile.NamedTemporaryFile(suffix=".py", mode="w", delete=False) as f:
        f.write(code)
        tmp_path = f.name

    start = time.monotonic()
    try:
        result = subprocess.run(
            ["python3", tmp_path],
            capture_output=True,
            text=True,
            timeout=timeout,
        )
        elapsed = time.monotonic() - start
        return {
            "stdout": result.stdout,
            "stderr": result.stderr,
            "exit_code": result.returncode,
            "elapsed": elapsed,
            "timed_out": False,
        }
    except subprocess.TimeoutExpired:
        elapsed = time.monotonic() - start
        return {
            "stdout": "",
            "stderr": f"Execution timed out after {timeout} seconds.",
            "exit_code": -1,
            "elapsed": elapsed,
            "timed_out": True,
        }
    finally:
        os.unlink(tmp_path)


# Test with a simple example
sample_code = textwrap.dedent("""
    def factorial(n):
        if n <= 1:
            return 1
        return n * factorial(n - 1)

    print(factorial(5))
    print(factorial(10))
""")

result = execute_code(sample_code)
Out[7]:
Console
Exit code: 0
Stdout:
120
3628800
Elapsed: 0.027s
Timed out: False

The execution harness captures output at the process level, which means it works regardless of what language or runtime the generated code uses. The same harness can run Python, Node.js, or compiled binaries by changing the command passed to subprocess.run. Note that we write to a temp file rather than passing code via stdin or shell arguments: this avoids shell injection vulnerabilities (where specially crafted code strings could escape the intended command) and handles code that spans multiple lines cleanly.

Interpreting Error Output

When execution fails, the raw error message is valuable but often too verbose or low-level for an LLM to act on effectively. Execution systems often include a parsing layer that extracts structured information from error output.

For Python exceptions, the standard format includes the traceback (showing the call stack and file and line information), the exception class, and the message. A parser can extract these components. The innermost frame of the traceback is usually the most relevant, because it shows the exact line that triggered the error. The exception type is important too: a TypeError calls for different repair strategies than a KeyError or an IndexError.

In[8]:
Code
import re


def parse_python_error(stderr: str) -> dict:
    """
    Parse a Python traceback to extract structured error information.
    Returns the exception type, message, and the most relevant line.
    """
    lines = stderr.strip().split("\n")
    error_info = {
        "raw": stderr,
        "exception_type": None,
        "message": None,
        "line_number": None,
        "line_code": None,
    }

    # Find the last "File ..., line N" entry (the innermost frame)
    file_line_pattern = re.compile(r'File "(.+)", line (\d+), in (.+)')
    last_frame = None
    for line in lines:
        match = file_line_pattern.search(line)
        if match:
            last_frame = match

    if last_frame:
        error_info["line_number"] = int(last_frame.group(2))

    # Extract the exception type and message from the last line
    if lines:
        last_line = lines[-1]
        colon_idx = last_line.find(":")
        if colon_idx != -1:
            error_info["exception_type"] = last_line[:colon_idx].strip()
            error_info["message"] = last_line[colon_idx + 1 :].strip()
        else:
            error_info["exception_type"] = last_line.strip()

    return error_info


# Test with a failing example
failing_code = textwrap.dedent("""
    def add(a, b):
        return a + b

    result = add(10, "twenty")  # TypeError
    print(result)
""")

failed_result = execute_code(failing_code)
parsed_error = parse_python_error(failed_result["stderr"])
Out[9]:
Console
Exit code: 1

Raw stderr:
Traceback (most recent call last):
  File "/var/folders/lz/vn3ps0t51nv5q2g7q4kppt1r0000gn/T/tmpr27cf21s.py", line 5, in <module>
    result = add(10, "twenty")  # TypeError
             ^^^^^^^^^^^^^^^^^
  File "/var/folders/lz/vn3ps0t51nv5q2g7q4kppt1r0000gn/T/tmpr27cf21s.py", line 3, in add
    return a + b
           ~~^~~
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Parsed error:
  Type: TypeError
  Message: unsupported operand type(s) for +: 'int' and 'str'
  Line: 3

Parsing the exception type and message allows downstream components (like an LLM prompt builder) to highlight the most relevant information without flooding the context window with a long traceback. For a model with a 128K token context window this might seem unimportant, but in practice shorter, more focused prompts tend to produce better repair results than prompts padded with noise.

Test-Based Feedback

In agentic coding workflows, the most valuable feedback often comes not from the program's output but from a test suite. When a model generates code to implement a function, running the associated unit tests provides precise, structured feedback: which tests pass, which fail, and why.

Test-based feedback is especially powerful because it encodes the intended behavior of the code independently of the implementation. A failing test tells the model exactly what property its code violates, with a precise assertion that can be inserted directly into a repair prompt. Consider the difference between these two pieces of feedback for a sort_list function:

Feedback type 1 (raw execution output): [3, 1, 2]

Feedback type 2 (test failure): AssertionError: Lists differ: [3, 1, 2] != [1, 2, 3]

The second version tells the model what the code produced and what it should have produced. The model does not need to infer what "correct" means; the test tells it directly. This is why test-based repair consistently outperforms repair from raw output in self-debugging benchmarks.

In[10]:
Code
def run_tests_against_code(implementation_code: str, test_code: str) -> dict:
    """
    Execute implementation code and then run tests against it.
    Returns pass/fail counts and per-test details.
    """
    combined = implementation_code + "\n\n" + test_code + "\n\n"
    combined += textwrap.dedent("""
        import unittest
        loader = unittest.TestLoader()
        suite = loader.loadTestsFromTestCase(TestSolution)
        runner = unittest.TextTestRunner(verbosity=2, stream=open('/dev/null', 'w'))
        result = runner.run(suite)
        total = result.testsRun
        failed = len(result.failures) + len(result.errors)
        passed = total - failed
        print(f"TESTS:{total}:{passed}:{failed}")
        for f in result.failures:
            print(f"FAIL:{f[0].id()}:{f[1][:200]}")
        for e in result.errors:
            print(f"ERROR:{e[0].id()}:{e[1][:200]}")
    """)

    exec_result = execute_code(combined)

    parsed = {"total": 0, "passed": 0, "failed": 0, "details": []}
    for line in exec_result["stdout"].split("\n"):
        if line.startswith("TESTS:"):
            parts = line.split(":")
            parsed["total"] = int(parts[1])
            parsed["passed"] = int(parts[2])
            parsed["failed"] = int(parts[3])
        elif line.startswith("FAIL:") or line.startswith("ERROR:"):
            parts = line.split(":", 2)
            parsed["details"].append(
                {
                    "test": parts[1],
                    "message": parts[2] if len(parts) > 2 else "",
                }
            )

    return parsed


# Example: a model-generated solution to a simple problem
implementation = textwrap.dedent("""
    def is_palindrome(s):
        s = s.lower().replace(" ", "")
        return s == s[::-1]
""")

tests = textwrap.dedent("""
    import unittest

    class TestSolution(unittest.TestCase):
        def test_simple(self):
            self.assertTrue(is_palindrome("racecar"))
        def test_spaces(self):
            self.assertTrue(is_palindrome("A man a plan a canal Panama"))
        def test_false(self):
            self.assertFalse(is_palindrome("hello"))
        def test_empty(self):
            self.assertTrue(is_palindrome(""))
""")

test_results = run_tests_against_code(implementation, tests)
Out[11]:
Console
Test results:
  Total: 4
  Passed: 4
  Failed: 0

Test-based feedback achieves a good balance between signal richness and noise reduction. A passing test suite gives the model a strong signal to stop iterating. A failing test identifies the specific property that needs to be repaired, which is more actionable than a raw exception message.

Execution Feedback Formatting

Once raw execution output has been captured and optionally parsed, it needs to be formatted into a prompt that an LLM can act on. The format matters: too much raw output floods the context window; too little leaves out information the model needs.

The goal of feedback formatting is not to show the LLM everything but to show it the right things in the right order. A well-constructed feedback prompt places the error information adjacent to the offending code rather than at the end of a long dump. This mirrors how human developers read error messages: they jump to the relevant line, look at the surrounding code, and diagnose the issue. By formatting the feedback to support this pattern, you make the repair task easier for the model.

Effective feedback formatting follows these principles:

  • Include the relevant portion of the original code so the model can see what it wrote.
  • Place the error message near the code line that caused it, not at the end of a long traceback.
  • Truncate long outputs at a reasonable character limit, marking truncations explicitly.
  • Summarize test results with counts rather than full test output when many tests pass.
  • Label the feedback clearly (e.g., "Execution Result:", "Error:", "Test Failure:") so the model understands the structure.

Different feedback types provide different amounts of actionable information to the model. Raw exception messages identify the error type but not the violated specification. Full stack traces add location context but can be noisy. Test-based feedback is the most specific, telling the model exactly which behavior is wrong. The chart below uses illustrative simulated rates to show that ordering; it is not a benchmark result.

Out[12]:
Visualization
Horizontal bars show illustrative repair success rates of 18, 24, 41, 48, and 67 percent for increasingly informative feedback types.
Illustrative first-attempt repair rates rise as feedback becomes more actionable. In this simulated comparison, test-based feedback reaches 67 percent versus 18 percent for a blind retry; the values demonstrate the pattern rather than report a benchmark.

Feedback as Grounding

There is a deeper point about why execution feedback works so well: it provides grounding. When a model generates code purely from a natural language description, it relies entirely on its learned representation of what that description means. When it receives execution feedback, it sees the actual runtime behavior of its code: the values that variables held, the exception that was raised, the assertion that failed. This grounds the model's next generation step in observable reality rather than in its statistical expectations.

This parallels a phenomenon in cognitive science called "situated cognition": humans solve problems more effectively when they can interact with a real environment than when they reason abstractly. The programmer who runs their code and watches the output learns faster than the one who only traces through it mentally. Execution feedback gives LLMs a similar advantage.