tracetools

Solveit tools for tracing function execution
from IPython.display import display,Markdown
import textwrap

tracetool is an LLM tool that traces function execution and captures variable snapshots after each line runs, using Python 3.12’s sys.monitoring for low-overhead tracing.

For each call to the target function (including recursive calls), the LLM receives a stack trace showing how that call was reached, plus a per-line trace dict mapping each executed source line to its hit count and variable snapshots. Variables that don’t change are shown as a single (type, repr) tuple, while those that evolve across iterations appear as a list of snapshots.

This is particularly useful when the LLM needs to understand unfamiliar code by seeing exactly what happens step-by-step, debug issues by watching how variables change, verify loop behavior by confirming iteration counts and accumulator values, or explore recursive functions where each call gets its own trace entry.


source

tracetool


def tracetool(
    sym:str, # Dotted symbol path of callable to run
    args:list=None, # Positional args for callable (JSON values passed directly)
    kwargs:dict=None, # Keyword args for callable (JSON values passed directly)
    target_func:str=None, # Dotted symbol path of function to trace (defaults to sym)
)->list: # List of (stack_str,trace_dict); trace_dict maps source snippets to (hit_count, variables), unchanged vars collapsed to single tuple

Trace execution using sys.monitoring (Python 3.12+), returning a list of per-call traces.

Return: - list of length <= 10 - one element per call to target_func (including recursion) - each element is: (stack_str, trace_dict) stack_str: call stack string (filtered so fn is the shallowest frame shown) trace_dict: { “”: ( hit_count, { “var”: [ (type_name, truncated_repr), … up to 10 ], … } ), …}

Semantics: - “Line” means an AST-level line: separate statements (even if on one physical line via ;). - Compound statements are keyed by their header only. - Unchanged variables → ('type', 'repr') tuple, changed variables → [('type', 'repr'), ...] list. - Comprehensions are treated as a line node and are monitored, including inside the comprehension frame, with per-iteration snapshots. - Snapshots are recorded after each line finishes, so assignments show updated values.

def demo(n,m='x'):
    total = 0
    for i in range(n): total += i
    return m*total

Try using &tracetool to trace demo(5, m='y'). Based on the results, tell me how demo works.

🤖Reply🤖

🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠

tracetool(sym=demo, args=[5], kwargs={‘m’: ‘y’})
{
  "id": "toolu_01CeHDU3L8nuEZZL6xCnRpE4",
  "call": {
    "function": "tracetool",
    "arguments": {
      "sym": "demo",
      "args": "[5]",
      "kwargs": "{'m': 'y'}"
    }
  },
  "result": "[('',\n  {'total = 0': (1, {'total': ('int', '0')}),\n   'for i in range(n):': (6,\n    {'i': [('int', '0'),\n      ('int', '1'),\n      ('int', '2'),\n      ('int', '3'),\n      ('int', '4'),\n      ('int', '4')],\n     'n': ('int', '5')}),\n   'total += i': (5,\n    {'i': [('int', '0'),\n      ('int', '1'),\n      ('int', '2'),\n      ('int', '3'),\n      ('int', '4')],\n     'total': [('int', '0'),\n      ('int', '1'),\n      ('int', '3'),\n      ('int', '6'),\n      ('int', '10')]}),\n   'return m*total': (1, {'m': ('str', \"'y'\"), 'total': ('int', '10')})})]"
}

The demo function:

  1. Initializes total = 0
  2. Loops through range(n) where n=5, iterating with i from 0 to 4
  3. Accumulates the sum by adding each i to total (0→0, +1→1, +3→3, +6→6, +10→10)
  4. Returns m * total, which multiplies the string 'y' by 10, giving 'yyyyyyyyyy'

So demo(n, m) computes the sum of integers from 0 to n-1 (which is n*(n-1)/2), then repeats the string m that many times. For demo(5, m='y'), it returns 'y' * 10 = 'yyyyyyyyyy'.

def quotefunc(s): return textwrap.wrap('aaa '*10, width=10, subsequent_indent='> ')
tests = 'aaa '*10
quotefunc(tests)
['aaa aaa', '> aaa aaa', '> aaa aaa', '> aaa aaa', '> aaa aaa']

Try tracing textwrap.TextWrapper._wrap_chunks by calling quotefunc.

🤖Reply🤖

🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠

tracetool(sym=quotefunc, args=[‘test’], target_func=textwrap.TextWrapper._wrap_chunks)
{
  "id": "toolu_01KmGXSpBR2CCkrVAXaiu6ty",
  "call": {
    "function": "tracetool",
    "arguments": {
      "sym": "quotefunc",
      "args": "['test']",
      "target_func": "textwrap.TextWrapper._wrap_chunks"
    }
  },
  "result": "[('quotefunc (4125899319.py:2)\\nwrap (/Users/jhoward/.local/share/uv/python/cpython-3.12.8-macos-aarch64-none/lib/python3.12/textwrap.py:384)\\nwrap (/Users/jhoward/.local/share/uv/python/cpython-3.12.8-macos-aarch64-none/lib/python3.12/textwrap.py:359)\\n_wrap_chunks (/Users/jhoward/.local/share/uv/python/cpython-3.12.8-macos-aarch64-none/lib/python3.12/textwrap.py:238)',\n  {'\"\"\"_wrap_chunks(chunks : [string]) -> [string]\\n\\n    Wrap a sequence of text chunks and return a list of lines of\\n    length \\'self.width\\' or less.  (If \\'break_long_words\\' is false,\\n    some lines may be longer than <TRUNCATED>"
}

Fascinating! Here’s how _wrap_chunks works:

Setup: - Initializes empty lines list, validates width, reverses chunks (so it can pop from the end efficiently)

Main loop (5 iterations for 5 output lines): 1. Sets indent — empty string for line 1, '> ' for subsequent lines 2. Calculates available width — 10 for line 1, 8 for others (after subtracting indent length) 3. Inner loop pops chunks onto cur_line, tracking cur_len, until adding next chunk would exceed width 4. Strips trailing whitespace: ['aaa', ' ', 'aaa', ' ']['aaa', ' ', 'aaa'] (cur_len 8→7) 5. Joins with indent: '' + 'aaa aaa' or '> ' + 'aaa aaa'

Key observations: - width is 10 on first line but 8 thereafter (due to 2-char '> ' indent) - Each line gets exactly ['aaa', ' ', 'aaa'] = 7 chars, fitting within the available width - The trailing space is always stripped before joining - Never hits _handle_long_word since no chunk exceeds width


source

fmt_trace


def fmt_trace(
    traces
):

Format tracetool output as markdown

For a human friendly version, use fmt_trace on the result.

r = tracetool(sym='quotefunc', args=['test'], target_func='textwrap.TextWrapper._wrap_chunks')
Markdown(fmt_trace(r))

Stack:

quotefunc (1868596414.py:1)
wrap ($PYTHON_STDLIB/textwrap.py:384)
TextWrapper.wrap ($PYTHON_STDLIB/textwrap.py:359)
TextWrapper._wrap_chunks ($PYTHON_STDLIB/textwrap.py:238)
Source Hits Variables
lines = [] 1 lines: []
if self.width <= 0: 1 self: <textwrap.TextWrapper object>
if self.max_lines is not None: 1 self: <textwrap.TextWrapper object>
chunks.reverse() 1 chunks: [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'...
while chunks: 6 chunks: [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'...[' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'...[' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'...[' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'][' ', 'aaa', ' ', 'aaa'][]
cur_line = [] 5 cur_line: []
cur_len = 0 5 cur_len: 0
if lines: 5 lines: []['aaa aaa']['aaa aaa', '> aaa aaa']['aaa aaa', '> aaa aaa', '> aaa aaa']['aaa aaa', '> aaa aaa', '> aaa aaa', '> aaa aaa']
indent = self.subsequent_indent 4 indent: '> '
self: <textwrap.TextWrapper object>
indent = self.initial_indent 1 indent: ''
self: <textwrap.TextWrapper object>
width = self.width - len(indent) 5 indent: '''> ''> ''> ''> '
self: <textwrap.TextWrapper object>
width: 108888
if self.drop_whitespace and chunks[-1].strip() == '' and lin... 5 chunks: [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'...[' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'...[' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'...[' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'][' ', 'aaa', ' ', 'aaa']
lines: []['aaa aaa']['aaa aaa', '> aaa aaa']['aaa aaa', '> aaa aaa', '> aaa aaa']['aaa aaa', '> aaa aaa', '> aaa aaa', '> aaa aaa']
self: <textwrap.TextWrapper object>
while chunks: 25 chunks: [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'...
l = len(chunks[-1]) 24 chunks: [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'...
l: 3131331313
if cur_len + l <= width: 24 cur_len: 0347803478
l: 3131331313
width: 101010101088888
cur_line.append(chunks.pop()) 20 chunks: [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'...
cur_line: ['aaa']['aaa', ' ']['aaa', ' ', 'aaa']['aaa', ' ', 'aaa', ' ']['aaa']['aaa', ' ']['aaa', ' ', 'aaa']['aaa', ' ', 'aaa', ' ']['aaa']['aaa', ' ']
cur_len += l 20 cur_len: 3478347834
l: 3131313131
break 4
if chunks and len(chunks[-1]) > width: 5 chunks: [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'...[' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'...[' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'][' ', 'aaa', ' ', 'aaa'][]
width: 108888
if self.drop_whitespace and cur_line and cur_line[-1].strip(... 5 cur_line: ['aaa', ' ', 'aaa', ' ']
self: <textwrap.TextWrapper object>
cur_len -= len(cur_line[-1]) 5 cur_len: 7
cur_line: ['aaa', ' ', 'aaa', ' ']
del cur_line[-1] 5 cur_line: ['aaa', ' ', 'aaa']
if cur_line: 5 cur_line: ['aaa', ' ', 'aaa']
if (self.max_lines is None or 5 chunks: [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'...[' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'...[' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'][' ', 'aaa', ' ', 'aaa'][]
cur_len: 7
lines: []['aaa aaa']['aaa aaa', '> aaa aaa']['aaa aaa', '> aaa aaa', '> aaa aaa']['aaa aaa', '> aaa aaa', '> aaa aaa', '> aaa aaa']
self: <textwrap.TextWrapper object>
width: 108888
lines.append(indent + ''.join(cur_line)) 5 cur_line: ['aaa', ' ', 'aaa']
indent: '''> ''> ''> ''> '
lines: ['aaa aaa']['aaa aaa', '> aaa aaa']['aaa aaa', '> aaa aaa', '> aaa aaa']['aaa aaa', '> aaa aaa', '> aaa aaa', '> aaa aaa']['aaa aaa', '> aaa aaa', '> aaa aaa', '> aaa aa...
return lines 1 lines: ['aaa aaa', '> aaa aaa', '> aaa aaa', '> aaa aa...