This project is a from-scratch learning journey for building an AI agent.
The goal is to start with a very small CLI agent and gradually evolve it toward a more production-style agent architecture. Each step is intentionally simple enough to understand, but real enough to teach the core ideas behind agents: LLM calls, tool use, memory, tracing, error handling, tests, and guardrails.
- CLI chat interface
- Gemini API integration
- Manual agent loop
- Multi-step tool loop
- Gemini function/tool calling
- Python-controlled tool execution
- Calculator tool
- Note tools: save, list, read, search
- Chat memory stored in JSON
- Preference memory stored in JSON
- System instruction for stable behavior
- Debug trace in terminal with colors
- Trace logs saved to JSONL
- Tool and LLM error handling
- Step limits for LLM/tool calls
- Local pytest test suite
User
|
v
main.py
|
v
agent_loop.py
|-- memory.py
|-- llm_client.py
|-- tool_manager.py
| |-- tools/calculator.py
| |-- tools/notes.py
|
v
trace.pyFor a normal question:
User message
-> load chat memory
-> load preferences
-> send context to Gemini
-> Gemini answers directly
-> save turn to memory
-> save trace
-> print answerFor a tool-using question:
User message
-> load memory and preferences
-> send context + tool schemas to Gemini
-> Gemini returns one or more function_call requests
-> Python validates and executes requested tools
-> Python appends function_call + function_response messages
-> loop continues until Gemini returns a final answer
-> save turn to memory
-> save trace
-> print answerThe LLM does not execute tools. It only requests a tool call. Python decides whether the tool exists, validates arguments, executes the function, and sends the result back to the model.
main.py CLI entrypoint
agent_loop.py Agent orchestration loop
llm_client.py Gemini client and tool declarations
tool_manager.py Tool registry and executor
memory.py Chat memory and preference memory
trace.py Trace events and JSONL trace logging
tools/calculator.py Safe calculator tool
tools/notes.py Note tools
tests/ Local pytest test suite
data/notes/ Runtime note storageCreate a virtual environment:
python3 -m venv venv
source venv/bin/activateInstall dependencies:
pip install -r requirements.txtCreate an environment file:
cp .env.example .envAdd your Gemini API key:
GEMINI_API_KEY=your_gemini_api_key_hereNormal mode:
venv/bin/python main.pyDebug mode with colored trace:
venv/bin/python main.py --debugExit:
exitor:
quitCalculator:
what is 12 * 8 % 2 + 4?Notes:
save a note titled Agent Memory saying Agents can use memory to keep useful context between turns
list my notes
read my note titled Agent Memory
search my notes for memoryPreferences:
remember that I prefer simple examples
what are my preferences?
explain tool callingDebug trace:
search my notes for memoryExpected trace events include:
user_message_received
memory_loaded
preferences_loaded
llm_request_started
tool_call_requested
tool_executed
tool_result_appended
llm_answer_received
memory_savedRuntime files are intentionally ignored by Git:
data/memory.json
data/preferences.json
data/traces.jsonl
data/notes/*.mddata/notes/.gitkeep is committed only to keep the notes folder in the repo.
Run all tests:
venv/bin/python -m pytestCurrent tests cover:
- calculator behavior
- note save/list/read/search
- memory save/load
- preference memory
- tool manager success/error handling
- trace JSONL logging
- agent loop direct-answer, one-tool, and multi-tool paths
- step limit behavior
- How to call Gemini from Python
- How function calling works
- Why the LLM does not execute tools directly
- How to build a ToolManager
- How to send function responses back to the model
- How to persist short-term chat memory
- How to persist user preferences
- How to add debug traces
- How to log traces to a file
- How to add safety through error handling and step limits
- How to run multiple tools in one guarded agent turn
- How to test deterministic parts of an agent without calling the LLM
Current memory and tool results are passed directly into Gemini. Next we need context budgeting so long notes, long memory, and multi-step tool results do not grow without control.
Planned work:
- approximate token counting
- memory truncation
- tool result truncation
- note search result limits
- trace events when context is trimmed
Add stricter validation around model outputs and tool arguments.
Possible direction:
- Pydantic schemas
- stricter tool argument models
- clearer user-facing validation errors
Current note search is keyword-based.
Next path:
keyword search
-> better snippets
-> embeddings
-> local vector database
-> semantic note search / RAGPossible tools:
- ChromaDB
- LanceDB
- SQLite vector extensions
Current memory has:
- recent chat history
- saved preferences
Future memory can include:
- facts about the user
- project-specific memory
- summaries of older conversations
- memory pruning and compaction
For complex tasks, add planning:
Prompt
-> LLM creates a structured plan
-> agent executes each step
-> final answer summarizes resultsExample:
Find my notes about memory, calculate how many matches exist, and save a summary.Before returning an answer, the agent can run a hidden review step:
Did the answer satisfy the user request?
Were tools used correctly?
Is anything missing?This helps with quality, but should be added only after the basic loop is solid.
Later production-style features:
- structured logs
- config management
- retry policies
- tool timeouts
- prompt/version tracking
- eval datasets
- cost tracking
- API or web interface
- authentication and permissions
This project intentionally avoids starting with a large framework.
The learning path is:
Build the loop yourself
Understand the moving parts
Add safety and tests
Then study frameworks with better intuitionFrameworks like LangGraph, OpenAI Agents SDK, Pydantic AI, or CrewAI will make more sense after this core loop feels natural.