Implements the complete LLM agent tool-calling pipeline — the tool-calling
path was broken in multiple ways: tools from agent config were not passed
to the LLM, tool execution was single-pass (the model could not see tool
results or make follow-up calls), tool errors were discarded, and shell/
python_exec tools were never registered.
Key changes:
- Multi-turn tool-call loop: passes tools to the LLM via ainvoke(tools=...)
and re-invokes after each batch of ToolResults, with a configurable
round limit (tool_max_rounds config / TOOL_MAX_ROUNDS env var, default 20).
- Tool schema module (llm_tools.py): normalize_tool_entry() converts
string tool names and config dicts to OpenAI function-calling format
with full parameter schemas and LLM-facing guidance (max_chars for
file_read, sandbox builtins for python_exec, etc.).
- file_read enhancements: directory listing with file sizes and type
indicators, max_chars truncation to prevent context overflow, shell
command detection with redirect to shell tool, file-not-found
recovery hints with parent directory listing.
- python_exec sandbox: stdout capture so print() produces visible
output, NameError guidance directing LLM to file_read/file_write
instead of using open().
- shell/python_exec tool registration in builtin_tools when
allow_shell / exec_python is enabled.
- create_subprocess_shell for shell commands (was create_subprocess_exec,
which blocks pipes/redirections/chains).
- Tool error propagation: ExecutionError/ConfigurationError details
included in ToolMessages for LLM self-correction.
- Stuck-model recovery: injects synthesizing prompt when model
exhausts tool rounds without producing content.
- LLM provider error details preserved in ExecutionError messages
instead of generic "LLM processing failed".
- Empty message filtering in _prepare_conversation_history() prevents
whitespace-only messages from polluting downstream conversation
history in multi-agent pipelines.
Closes#59