На этой странице Отладка Python: pdb REPL + debugpy удалённо (DAP).
Метаданные навыка¶
| |
|---|---|
|Источник| Встроенный (установлен по умолчанию) |
|Путь| skills/software-development/python-debugpy |
|Версия| 1.0.0 |
|Автор| Hermes Agent |
|Лицензия| MIT |
|Теги| отладка, python, pdb, debugpy, точки_останова, dap, post-mortem |
|Связанные навыки| systematic-debugging, node-inspect-debugger, debugging-hermes-tui-commands |
Справочник: полный SKILL.md¶
info Далее приведено полное определение навыка, которое Hermes загружает при его активации. Именно эти инструкции видит агент, когда навык активен.
Отладчик Python (pdb + debugpy)¶
Обзор¶
Три инструмента, выбираемых по ситуации:
Инструмент| Когда
---|---
breakpoint() + pdb| Локально, интерактивно, проще всего. Добавьте breakpoint() в исходный код, запустите обычным образом — получите REPL на этой строке.
python -m pdb| Запуск существующего скрипта под pdb без изменения исходного кода. Полезно для быстрой проверки.
debugpy| Удалённая / безголовая / «подключение к уже запущенному процессу» отладка. Использует протокол DAP, управляется из терминала, подходит для долгоживущих процессов (gateway, daemon, PTY-дочерние процессы).
Начните с breakpoint(). Это самое дешёвое решение, которое работает.
Когда использовать¶
- Тест падает, и трассировка не показывает, почему значение неверно
- Нужно пройти по функции шаг за шагом и наблюдать за изменением коллекции
- Долгоживущий процесс (hermes gateway, tui_gateway) работает некорректно, и вы не можете его перезапустить
- Посмертный анализ: исключение возникло в коде, близком к production, и вы хотите изучить локальные переменные в месте сбоя
- Дочерний процесс / подпроцесс (Python
_SlashWorker, PTY bridge worker) является actual местом ошибки
Не использовать для: вещей, которые print() / logging.debug решают меньше чем за минуту, или того, что уже показывает pytest -vv --tb=long --showlocals.
Краткий справочник по pdb¶
Внутри любого приглашения pdb ((Pdb)):
Команда| Действие
---|---
h / h cmd| справка
n| следующая строка (шаг с обходом)
s| шаг с заходом
r| выход из текущей функции
c| продолжение
unt N| продолжать до строки N
j N| перейти к строке N (только в той же функции)
l / ll| показать исходный код вокруг текущей строки / всю функцию
w| где (стек вызовов)
u / d| переместиться вверх / вниз по стеку
a| вывести аргументы текущей функции
p expr / pp expr| вывести / красиво вывести выражение
display expr| автоматически выводить выражение на каждой остановке
b file:line| установить точку останова
b func| остановиться при входе в функцию
b file:line, cond| условная точка останова
cl N| удалить точку останова N
tbreak file:line| одноразовая точка останова
!stmt| выполнить произвольный Python (включая присваивания)
interact| перейти в полноценный Python REPL в текущей области видимости (Ctrl+D для выхода)
q| выход
Команда interact — самая мощная: вы можете импортировать что угодно, исследовать сложные объекты, даже вызывать методы, изменяющие состояние. Локальные переменные по умолчанию доступны только для чтения; используйте !x = 42 из приглашения (Pdb) для изменения.
Рецепт 1: Локальная точка останова¶
Проще всего. Отредактируйте файл:
[code]
def compute(x, y):
result = some_helper(x)
breakpoint() # <-- drops into pdb here
return result + y
[/code]
Запустите код обычным образом. Вы окажетесь на строке с breakpoint() с полным доступом к локальным переменным.
Не забудьте удалить breakpoint() перед коммитом. Используйте git diff или pre-commit grep:
[code]
rg -n 'breakpoint\(\)' --type py
[/code]
Рецепт 2: Запуск скрипта под pdb (без изменения исходного кода)¶
[code]
python -m pdb path/to/script.py arg1 arg2
# Lands at first line of script
(Pdb) b path/to/script.py:42
(Pdb) c
[/code]
Рецепт 3: Отладка pytest-теста¶
Тестовый раннер Hermes и pytest поддерживают это:
[code]
# Drop to pdb on failure (or on any raised exception):
scripts/run_tests.sh tests/path/to/test_file.py::test_name --pdb
# Drop to pdb at the START of the test:
scripts/run_tests.sh tests/path/to/test_file.py::test_name --trace
# Show locals in tracebacks without pdb:
scripts/run_tests.sh tests/path/to/test_file.py --showlocals --tb=long
[/code]
Примечание: scripts/run_tests.sh по умолчанию использует xdist (-n 4), а pdb НЕ работает под xdist. Добавьте -p no:xdist или запустите один тест с -n 0:
[code]
scripts/run_tests.sh tests/foo_test.py::test_bar --pdb -p no:xdist
# or
source .venv/bin/activate
python -m pytest tests/foo_test.py::test_bar --pdb
[/code] Это обходит гарантии изолированной среды — нормально для отладки, но перед отправкой перезапустите под обёрткой для подтверждения.
Рецепт 4: Посмертный анализ любого исключения¶
[code]
import pdb, sys
try:
run_the_thing()
except Exception:
pdb.post_mortem(sys.exc_info()[2])
[/code]
Или оберните весь скрипт:
[code]
python -m pdb -c continue script.py
# When it crashes, pdb catches it and you're in the frame of the exception
[/code]
Или установите глобальный хук в repl/jupyter:
[code]
import sys
def excepthook(etype, value, tb):
import pdb; pdb.post_mortem(tb)
sys.excepthook = excepthook
[/code]
Рецепт 5: Удалённая отладка с debugpy (подключение к работающему процессу)¶
Для долгоживущих процессов: Hermes gateway, tui_gateway, демон, процесс, который уже работает некорректно и не может быть чисто перезапущен.
Настройка¶
[code]
source /home/bb/hermes-agent/.venv/bin/activate
pip install debugpy
[/code]
Паттерн A: Изменение исходного кода — процесс ждёт отладчик при запуске¶
Добавьте в начале точки входа (или внутри функции, которую хотите отладить):
[code]
import debugpy
debugpy.listen(("127.0.0.1", 5678))
print("debugpy listening on 5678, waiting for client...", flush=True)
debugpy.wait_for_client()
debugpy.breakpoint() # optional: pause immediately once attached
[/code]
Запустите процесс; он заблокируется на wait_for_client().
Паттерн B: Без изменения исходного кода — запуск с -m debugpy¶
[code] python -m debugpy --listen 127.0.0.1:5678 --wait-for-client your_script.py arg1
[/code] Аналогично для точек входа в виде модуля: [code] python -m debugpy --listen 127.0.0.1:5678 --wait-for-client -m your.module
[/code]
Паттерн C: Подключение к уже запущенному процессу¶
Требует PID и предустановленный debugpy в окружении целевого процесса:
[code]
python -m debugpy --listen 127.0.0.1:5678 --pid
# debugpy injects itself into the process. Then attach a client as below.
[/code]
Некоторые ядра/настройки безопасности блокируют внедрение на основе ptrace (/proc/sys/kernel/yama/ptrace_scope). Исправление:
[code]
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
[/code]
Подключение клиента из терминала¶
Самый простой терминальный DAP-клиент — это CLI VS Code или небольшой скрипт. Внутри Hermes у вас есть два практических варианта:
Вариант 1: Собственный CLI REPL debugpy — не официальная функция, а крошечный DAP-клиентский скрипт:
[code]
# /tmp/dap_client.py
import socket, json, itertools, time, sys
HOST, PORT = "127.0.0.1", 5678
s = socket.create_connection((HOST, PORT))
seq = itertools.count(1)
def send(msg):
msg["seq"] = next(seq)
body = json.dumps(msg).encode()
s.sendall(f"Content-Length: {len(body)}\\r\\n\\r\\n".encode() + body)
def recv():
header = b""
while b"\\r\\n\\r\\n" not in header:
header += s.recv(1)
length = int(header.decode().split("Content-Length:")[1].split("\\r\\n")[0].strip())
body = b""
while len(body) < length:
body += s.recv(length - len(body))
return json.loads(body)
send({"type": "request", "command": "initialize", "arguments": {"adapterID": "python"}})
print(recv())
send({"type": "request", "command": "attach", "arguments": {}})
print(recv())
send({"type": "request", "command": "setBreakpoints",
"arguments": {"source": {"path": sys.argv[1]},
"breakpoints": [{"line": int(sys.argv[2])}]}})
print(recv())
send({"type": "request", "command": "configurationDone"})
# ... loop reading events and sending continue/stepIn/etc.
[/code]
Это нормально для разовой автоматизации, но неудобно как интерактивный интерфейс.
Вариант 2: Подключение из VS Code / Cursor / Zed — если у пользователя открыт один из них, он может добавить launch.json:
[code]
{
"name": "Attach to Hermes",
"type": "debugpy",
"request": "attach",
"connect": { "host": "127.0.0.1", "port": 5678 },
"justMyCode": false,
"pathMappings": [
{ "localRoot": "${workspaceFolder}", "remoteRoot": "/home/bb/hermes-agent" }
]
}
[/code]
Вариант 3: Откажитесь от DAP, используйте remote-pdb — обычно это то, что вам на самом деле нужно от терминального агента:
[code]
pip install remote-pdb
[/code]
В вашем коде:
[code]
from remote_pdb import set_trace
set_trace(host="127.0.0.1", port=4444) # blocks until connection
[/code]
Затем из терминала:
[code]
nc 127.0.0.1 4444
# You get a (Pdb) prompt exactly as if debugging locally.
[/code]
remote-pdb — самый чистый выбор, удобный для агента, когда протокол DAP от debugpy избыточен. Используйте debugpy только когда вам действительно нужна интеграция с IDE.
Отладка процессов, специфичных для Hermes¶
Тесты¶
См. Рецепт 3. Всегда добавляйте -p no:xdist или запускайте отдельные тесты без xdist.
run_agent.py / CLI — одноразовый запуск¶
Проще всего: добавьте breakpoint() рядом с подозрительной строкой, затем запустите hermes обычным образом. Управление вернётся в ваш терминал в точке остановки.
Подпроцесс tui_gateway (запускается hermes --tui)¶
Gateway работает как дочерний процесс Node TUI. Варианты:
A. Изменение исходного кода gateway:
[code]
# tui_gateway/server.py near the top of serve()
import debugpy
debugpy.listen(("127.0.0.1", 5678))
debugpy.wait_for_client()
[/code]
Запустите hermes --tui. TUI будет выглядеть замороженным (его бэкенд ожидает). Подключите клиент; выполнение возобновится, когда вы нажмёте continue.
B. Использование remote-pdb в конкретном обработчике:
[code]
from remote_pdb import set_trace
set_trace(host="127.0.0.1", port=4444) # in the RPC handler you want to trap
[/code]
Вызовите соответствующую слэш-команду из TUI, затем nc 127.0.0.1 4444 в другом терминале.
Подпроцесс _SlashWorker¶
Тот же паттерн — remote-pdb с set_trace() внутри пути exec рабочего процесса. Рабочий процесс сохраняется между слэш-командами, поэтому первый запуск блокируется до вашего подключения; последующие слэш-команды проходят обычно, если вы не активируете точку останова снова.
Gateway (gateway/run.py)¶
Долгоживущий процесс. Используйте remote-pdb в обработчике или debugpy с --wait-for-client, если вы всё равно перезапускаете gateway.
Частые проблемы¶
- pdb под pytest-xdist молча ничего не делает. Вы не увидите приглашения, тест просто зависнет. Всегда используйте
-p no:xdistили-n 0. breakpoint()в CI / не-TTY контекстах вешает процесс. Безопасно локально; никогда не коммитьте это. Добавьте pre-commit grep как страховку.PYTHONBREAKPOINT=0отключает все вызовыbreakpoint(). Проверьте переменную окружения, если ваша точка останова не срабатывает: [code] echo $PYTHONBREAKPOINT
[/code]
4. debugpy.listen блокирует только если вы также вызвали wait_for_client(). Без неё выполнение продолжается, и ваша первая точка останова может сработать до подключения клиента.
5. Подключение к PID не работает на усиленных ядрах. ptrace_scope=1 (по умолчанию в Ubuntu) разрешает ptrace только дочерних процессов того же пользователя. Обход: echo 0 > /proc/sys/kernel/yama/ptrace_scope (требуются права root) или запускайте под debugpy с самого начала.
6. Потоки. pdb отлаживает только текущий поток. Для многопоточного кода используйте debugpy (DAP с поддержкой потоков) или установите threading.settrace() для каждого потока.
7. asyncio. pdb работает в корутинах, но await внутри pdb требует Python 3.13+ или await из режима interact на старых версиях. Для 3.11/3.12 используйте трюки с asyncio.run_coroutine_threadsafe или !stmt-основанные await через asyncio.ensure_future.
8. scripts/run_tests.sh удаляет учётные данные и устанавливает HOME=<tmpdir>. Если ваша ошибка зависит от конфигурации пользователя или реальных API-ключей, она не воспроизведётся под обёрткой. Сначала отлаживайте с чистым pytest для воспроизведения, затем перепроверьте под обёрткой.
9. Форки / multiprocessing. pdb не следит за форками. Каждый дочерний процесс требует собственного breakpoint() или set_trace(). Для подагентов Hermes отлаживайте по одному процессу за раз.
Контрольный список проверки¶
- После
pip install debugpyпроверьте:python -c "import debugpy; print(debugpy.__version__)" - Для удалённой отладки проверьте, что порт действительно слушает:
ss -tlnp | grep 5678 - Первая точка останова действительно срабатывает (если нет, вероятно, установлено
PYTHONBREAKPOINT=0, вы под xdist или выполнение завершилось до подключения) where/wпоказывает ожидаемый стек вызовов- Очистка после отладки: никаких забытых
breakpoint()/set_trace()в закоммиченном коде [code] rg -n 'breakpoint\(\)|set_trace\(|debugpy\.listen' --type py
[/code]
Одноразовые рецепты¶
«Почему в этом словаре отсутствует ключ?»
[code]
# add above the KeyError site
breakpoint()
# then in pdb:
(Pdb) pp d
(Pdb) pp list(d.keys())
(Pdb) w # how did we get here
[/code]
«Этот тест проходит изолированно, но падает в наборе тестов.»
[code]
scripts/run_tests.sh tests/the_test.py --pdb -p no:xdist
# But if it only fails WITH other tests:
source .venv/bin/activate
python -m pytest tests/ -x --pdb -p no:xdist
# Now it pdb-traps at the exact failing test after state accumulated.
[/code]
«Мой асинхронный обработчик взаимоблокируется.»
[code]
# Add at handler entry
import remote_pdb; remote_pdb.set_trace(host="127.0.0.1", port=4444)
[/code]
Запустите обработчик. nc 127.0.0.1 4444, затем w чтобы увидеть приостановленный фрейм, !import asyncio; asyncio.all_tasks() чтобы увидеть, что ещё ожидает выполнения.
«Посмертный анализ сбоя в дочернем процессе Ink / подпроцессе.»
[code]
PYTHONFAULTHANDLER=1 python -m pdb -c continue path/to/entrypoint.py
# On crash, pdb lands at the frame of the exception with full locals
[/code]
* Метаданные навыка
* Справочник: полный SKILL.md
* Обзор
* Когда использовать
* Краткий справочник по pdb
* Рецепт 1: Локальная точка останова
* Рецепт 2: Запуск скрипта под pdb (без изменения исходного кода)
* Рецепт 3: Отладка pytest-теста
* Рецепт 4: Посмертный анализ любого исключения
* Рецепт 5: Удалённая отладка с debugpy (подключение к работающему процессу)
* Настройка
* Паттерн A: Изменение исходного кода — процесс ждёт отладчик при запуске
* Паттерн B: Без изменения исходного кода — запуск с -m debugpy
* Паттерн C: Подключение к уже запущенному процессу
* Подключение клиента из терминала
* Отладка процессов, специфичных для Hermes
* Тесты
* run_agent.py / CLI — одноразовый запуск
* tui_gateway подпроцесс (запускается hermes --tui)
* Подпроцесс _SlashWorker
* Gateway (gateway/run.py)
* Частые проблемы
* Контрольный список проверки
* Одноразовые рецепты