Skip to content

CLI Debugging

Practical recipes for debugging YAICLI when working from a source checkout.

Run the code you are editing

The package does not need to be installed. From the repository root:

# point imports at this checkout and keep user state out of your real HOME
PYTHONPATH=. HOME=/tmp/yaicli-dev COLUMNS=120 python -m yaicli.entry "hello"

python -m yaicli.entry is the same Typer app as the ai console script. A throwaway HOME gives you a fresh config file, roles/functions directories and chat storage, so experiments cannot corrupt your real ~/.config/yaicli.

Verbose mode

ai --verbose "hello"

--verbose prints the loaded configuration (API key masked), the current role, provider messages/tools for each request, and tracebacks for handled errors. Do not paste verbose output into bug reports without checking it: only API_KEY is masked, other credentials in EXTRA_HEADERS/provider-specific keys are not.

Common failures

Symptom Likely cause / fix
Unknown provider: <name> The PROVIDER value is not a ProviderFactory.providers_map key. Run ai --list-providers.
API_KEY is required No key in API_KEY, YAI_API_KEY, or the provider-specific fallback.
ModelAPIError / HTTP 4xx Check model name, base URL, and provider credentials with --verbose.
Config traceback / non-zero exit Malformed config.ini (missing [core], duplicate section). Inspect ai --template for the expected format.
Functions behave like an old version Built-in definitions load from the package; stale exported copies live in ~/.config/yaicli/functions/. Run ai --list-functions (reports source + stale copies), then ai --reinstall-functions.
Markup/render crash Untrusted strings must go through yaicli/console.py helpers (safe_text, untrusted_text, print_untrusted).
Process exits 0 after an error message Should not happen — handled failures exit 1 (entry.py); file a bug with the command and exit code.

Exit codes

0 on success, 1 on handled failures (config/provider/API/no-input/depth), 2 on usage errors, 3 when a denied tool is fatal (--fail-on-tool-denied), 130 on SIGINT, 141/0 on a broken pipe. In --chat mode an error in one turn keeps the REPL alive; the exit code reflects leaving the REPL. See Exit codes and Headless & Scripting.

Isolate components

  • Config: import yaicli.config.cfg and print it; the singleton is built at import time.
  • Provider only: instantiate the provider with a dict config and a mocked CLIENT_CLS, then call _handle_normal_response() / _handle_stream_response() with constructed ChatCompletion / ChatCompletionChunk objects (see tests/llms/).
  • Tool only: call execute_tool_call(ToolCall(...)) with yaicli.tools.cfg patched and get_function/get_mcp mocked (tests/tools/).
  • CLI/REPL: inject a mock client via CLI(client=...); tests/conftest.py provides patched_config_path, setup_test_environment, cli, mock_console.

Tests

uv run pytest tests/test_entry.py tests/test_cli.py -q   # CLI behaviour
uv run pytest -k "markup or confirmation" -q            # safety paths

Use pytest -x --pdb to drop into the debugger on the first failure, and ruff check yaicli tests before pushing.

Where to put temporary output

Use the project console (get_console()) or print() in entry.py only; remove debug output before committing. tests/ never call real APIs — keep it that way when adding diagnostics.

Debugging the Ctrl+O tool-output viewer

The viewer depends on terminal state (cbreak/raw mode, stdin ownership, alternate screen), so a "pressed Ctrl+O and nothing happened" report cannot be reproduced in CI. Enable the opt-in breadcrumb log before reproducing:

YAI_DEBUG_LOG=1 uv run ai -c          # appends to ~/.config/yaicli/debug.log
# or a specific file:
YAI_DEBUG_LOG=/tmp/yaicli-debug.log uv run ai -c

The log contains only event names and small scalars (counts/booleans/thread names), never prompts or model/tool content. Relevant events:

Event Meaning
watcher.start fd/isatty state, cbreak, discard_disabled (^O), flow_control_disabled (^S/^Q)
watcher.ready, watcher.skip, watcher.error whether the generation-time key watcher started (and why not)
watcher.byte every byte the watcher read (hex), useful to see whether the keypress arrived at all
watcher.ctrl-o Ctrl+O was recognised (encoding=byte for \x0f, encoding=csi for ESC[111;5u-style encodings)
generation.begin / generation.end bounds of one model turn; correlate keypresses with the generation window
prompt.begin the REPL is back at the prompt (generation over)
watcher.pause / watcher.resume the reader was handed to/from the viewer app (acked=False means a 2s timeout)
printer.pause.request the stream loop saw the request and is about to open the viewer
viewer.request, viewer.open, viewer.run.begin, viewer.run.end, viewer.run.error viewer open/close/exception path

If watcher.skip/watcher.error appears, the terminal is not being monitored and pressing Ctrl+O may hit the terminal's own VDISCARD key (the screen appears frozen until you press it again). If viewer.run.begin appears without viewer.run.end, the full-screen application hung; attach the log tail to the bug report.