Раздел «Песочница» умеет не только запустить код, но и
оттрассировать его исполнение: core/tracer.py исполняет код в subprocess
под sys.settrace и собирает JSON-трейс «состояние после каждого шага».
Фронтенд-плеер листает шаги вперёд/назад по этому трейсу без
повторного исполнения — как Python Tutor.
Трейс возвращается как result job'ы kind="trace"
(POST /api/v1/runs mode="trace" → GET /api/v1/runs/<id>, см.
api.md).
{
"steps": [ /* см. ниже */ ],
"stdout": "весь вывод программы (строкой)",
"truncated": false,
"stdout_truncated": false,
"error": null
}stdout— вывод программы; плеер показывает его нарастающим, срезая поstep.stdout_len.truncated—true, если сбор шагов прерван: по лимиту шагов (DEFAULT_MAX_STEPS = 1000), по таймауту либо по бюджету объёма (DEFAULT_MAX_TRACE_BYTES = 4 МБ). Причина в поле не различается намеренно: для читателя это один и тот же исход «показаны первые N шагов». Бюджет объёма нужен потому, что число шагов веса трейса не задаёт — каждый шаг несёт снимок всего живого состояния всех кадров, поэтому JSON растёт как O(шаги × состояние). Без него сотня строк по 300 символов давала 904 шага и ~17 МБ при внешнем лимитеmax_output_bytesв 10 МБ: stdout дочернего процесса резался посередине, разбор JSON падал, и вместо трейса приходилTraceError.trace_codeпересчитывает бюджет от фактическогоmax_output_bytes, поэтому его уменьшение ужимает и трейс.stdout_truncated—true, если вывод упёрся в потолок накопления (DEFAULT_MAX_STDOUT_CHARS = 1 000 000символов): вstdoutтолько его начало, само исполнение при этом не прерывается (плюс пометка в stderr дочернего процесса — как на грейд-пути). Флаг независим отtruncated: лимит шагов режет их ЧИСЛО, а объём вывода им не ограничен —print('x' * 10**9)это один шаг.error—nullлибо{"type": "ZeroDivisionError", "message": "..."}, если исполнение завершилось необработанным исключением.
{
"step": 0,
"event": "line" | "call" | "return" | "exception",
"line": 3,
"func": "<module>",
"stack": [ /* кадры, [0] — внешний (module), [-1] — текущий */ ],
"heap": { "<id>": { /* объект */ } },
"stdout_len": 7
}Кадр стека (stack[k]):
{ "func": "f", "line": 2, "locals": { "n": <ref>, "xs": <ref> } }<ref> — либо примитив inline, либо ссылка на объект heap:
null,true/false, число, строка (обрезается до 200 символов) — inline;- большой
int(|x| ≥ 2^53, теряет точность в JS) —{"big": "<repr>"}; - не-конечный
float(inf/nan, невалиден в JSON) — строкой"inf"/"nan"; - контейнер/объект —
{"ref": "<id>"}, а сам объект лежит вheap["<id>"].
Ссылки по id дают aliasing: если a и b — один объект, их ref
совпадает ({"ref": "140..."}), и плеер рисует две стрелки в один узел.
Объект heap (heap["<id>"]) всегда несёт поле type — имя Python-типа значения
(type(value).__name__, напр. "list", "MyClass") — плюс kind-специфичные
поля ниже. Плеер использует type как подпись узла; kind задаёт способ
отрисовки.
kind |
Поля (помимо type) |
Пример типа |
|---|---|---|
seq |
elems: [<ref>], n |
list, tuple, set, frozenset |
map |
entries: [[<ref>, <ref>]], n |
dict |
func |
name |
функция/метод |
module |
name |
модуль |
object |
attrs: {name: <ref>} |
экземпляр класса (по __dict__) |
opaque |
repr |
всё прочее (обрезанный repr) |
n — фактическая длина контейнера (элементов может быть закодировано меньше:
лимит _MAX_ELEMS = 100). Глубина вложенности ограничена _MAX_DEPTH = 8
(глубже — {"type": "...", "repr": "..."}: тип + обрезанный repr, без
раскрытия). При обрыве цикла ссылок объект — заглушка {"type": "..."} (только
тип, без полей) — разрывает рекурсию, сохраняя ref-идентичность.
- Трассировка — только кода пользователя (кадры целевого файла); библиотечный код внутрь не трейсится.
- Память ограничена на двух уровнях:
max_stdout_charsкапит буфер вывода ВНУТРИ дочернего процесса,CONFIG.max_output_bytes(RunSpec) — накопление его stdout, то есть готового JSON-трейса, в процессе сервера. Внутренний потолок заведомо ниже внешнего: иначе программа, упёршаяся в него честно, перевалила бы внешний, и вместо трейса пришёл бы обрезанный JSON. - OS-sandbox нет (инвариант CLAUDE.md №4) — только доверенный код.
- Под
--sandboxпошаговый трейс недоступен: модульcore/tracer.pyнельзя безопасно пробросить в изолированный дочерний процесс, поэтому он возвращает явную ошибку вместо небезопасного запуска. - Отмена зависшего трейса — best-effort через таймаут (
cancel_eventв этот путь пока не проброшен; исполнение и так ограниченоmax_steps+ таймаутом).