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.cfgand 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 constructedChatCompletion/ChatCompletionChunkobjects (seetests/llms/). - Tool only: call
execute_tool_call(ToolCall(...))withyaicli.tools.cfgpatched andget_function/get_mcpmocked (tests/tools/). - CLI/REPL: inject a mock client via
CLI(client=...);tests/conftest.pyprovidespatched_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.