Interactive Lesson
Architecture
This document explains how the project is structured, how its components relate to each other, and the key design decisions behind it.
Directory Overview
agents/
├── main.py # Entry point; LMStudio (OpenAI-compatible) agent loop
├── config.py # Shared configuration (e.g. MAX_ITERS)
├── prompts.py # System prompt definition
├── call_functions.py # Dispatches tool calls to the correct function
├── list_functions.py # Builds the tool schema lists for each provider
├── providers/
│ └── gemini.py # Optional Gemini agent loop
├── functions/
│ ├── get_dir_info.py # Tool: list files and directories
│ ├── get_file_content.py # Tool: read file content
│ ├── write_file.py # Tool: write or overwrite a file
│ ├── run_python_file.py # Tool: run a Python file
│ └── search_files.py # Tool: search text across files
├── calculator/ # Sandbox working directory for the agent
└── tests.py # Local harness for testing tool functions
The Agent Loop
Both main.py (LMStudio) and providers/gemini.py (Gemini) implement the same conceptual loop:
- Build messages list:
[system prompt, user prompt] - Send messages + tool schemas to the LLM.
- If the model returns tool calls:
- Execute all tool calls concurrently via
ThreadPoolExecutor. - Append tool results to messages.
- Go to step 2.
- Execute all tool calls concurrently via
- If the model returns a text response: return it.
- If
MAX_ITERSis reached: return the last partial response.
The loop lives entirely in the provider file. The functions/ layer is stateless and has no knowledge of the loop — keeping concerns cleanly separated.
Provider Abstraction
The project supports two LLM backends, each isolated in its own module.
LMStudio (main.py)
Uses the OpenAI-compatible openai SDK. Configure with:
LLM_PROVIDER=lmstudio
LMSTUDIO_BASE_URL=http://localhost:1234/v1
LMSTUDIO_MODEL=your-model
LMSTUDIO_TEMPERATURE=0
LMSTUDIO_MAX_TOKENS=800
Gemini (providers/gemini.py)
Uses Google's google-genai SDK. Configure with:
LLM_PROVIDER=gemini
GEMINI_API_KEY=your_api_key
GEMINI_MODEL=gemini-2.5-flash
The active provider is selected via the LLM_PROVIDER environment variable. This keeps provider-specific code isolated and makes it straightforward to add new providers without modifying the shared tool layer.
Tool Schema Design
Every tool in functions/ exports two things:
| Export | Purpose |
|---|---|
| Python function | The actual implementation — reads files, runs code, etc. |
| Schema dictionary | A structured description of the tool for the LLM (name, description, parameter types). |
Keeping descriptions short is intentional: shorter schemas consume fewer tokens per iteration, which extends the effective context budget across a multi-step agent run.
list_functions.py assembles the full list of schemas into the format expected by each provider (OpenAI vs. Gemini have slightly different schema conventions).
Concurrent Tool Execution
When the LLM returns multiple tool calls in a single step, there is no reason to execute them sequentially. Both main.py and providers/gemini.py use ThreadPoolExecutor to run all tool calls for a given step in parallel:
with ThreadPoolExecutor(max_workers=len(tool_calls)) as executor:
tool_results = list(executor.map(call_function, tool_calls))
Results are collected in order and appended to the message history before the next LLM call.
This is especially impactful when the model reads multiple files in one step. Instead of sequential disk reads, they all happen simultaneously.
The Sandbox
The agent's tools are scoped to a single working directory: calculator/. This directory acts as a controlled sandbox.
The tool functions receive the working directory as an injected argument and validate that all file paths stay within it. The LLM never sees or controls the working directory itself — it only provides relative paths.
This design limits the blast radius of mistakes and makes the agent's file access fully auditable.
Configuration
config.py exposes a small set of shared constants:
MAX_ITERS— maximum number of reasoning iterations before the loop stops.
LMStudio-specific runtime options (LMSTUDIO_TEMPERATURE, LMSTUDIO_MAX_TOKENS) are read from environment variables at runtime in main.py.