AI Agent Project Structure: Folders, Files, and Responsibilities
This guide is for Python developers who can write functions and run pytest but have not structured an agent service. By the end, you can scaffold a small repository, trace one request, add a typed tool without changing the loop, and prove success plus timeout behavior locally. From LLM to Agentic AI explains the capability ladder behind the repository boundaries.
The layout is a dependency map, not a framework mandate: API input flows into an agent loop, models propose actions, an executor invokes registered tools, and tests replace every network boundary. Prompts, credentials, temporary state, durable memory, and tool authority stay separate.
How to Learn This Layout
Do not memorize every filename at once. Treat the tree like a city map: learn the neighborhoods first, then the streets.
Your path in this article: (1) scan the map, (2) set root config, (3) walk one request through src/, (4) open the agent/ brain, (5) add tools / models / prompts, (6) expose HTTP, (7) prove it with tests and run main.py. After each step, check the takeaway — if it does not stick, re-read that section before moving on.
Quick reference
- Spend 2 minutes on the hero diagram before reading further.
- Create empty folders as you go — naming teaches faster than reading alone.
- Add a real file only when the step asks for it (tool, prompt, test).
- Skip fancy frameworks until the folder jobs are clear.
- Revisit the map whenever a section feels abstract.
- Goal: answer “where does X live?” without opening this page.
Remember this
Learn neighborhoods first — map → config → one request → deepen → prove — not every file on day one.
Root Configuration (Run the Project)
Start at the repository root with files every teammate and CI job expects. README.md should hold install steps, env vars, and one copy-paste command that works on a fresh clone.
requirements.txt (or pyproject.toml) pins the LLM SDK, HTTP client, and test runner. .env holds API keys — load it at runtime and never commit it. Pair it with .gitignore (.env, logs/, caches). docker-compose.yml is optional: use it when you need Redis, Postgres, or a local vector DB with one command.
Quick reference
- Commit
.env.examplewith fake keys; keep real.envlocal only. - Pin major versions — LLM SDKs break often across minors.
- README table: variable name, purpose, example value.
- docker-compose helps tests match production services.
- Add
make test/make runwhen the repo grows. - If secrets show up in prompts or git history, stop and fix that first.
Remember this
Root files answer three questions: how do I install, how do I run, where do secrets live?
Follow One Request Through src/
Put application code under src/. Six packages cover almost every agent:
api/ receives HTTP → agent/ plans and loops → models/ calls the LLM → tools/ run actions → prompts/ supply instructions → utils/ loads config and logs.
Read the next sentence aloud: request comes in, agent thinks, model decides, tool acts, state updates, response goes out. That is the learning loop — every folder exists to support one of those moments.
Quick reference
- api/ — door: validate input, call agent, return JSON.
- agent/ — brain: loop, executor, state, memory.
- models/ — phone line to the LLM / embeddings.
- tools/ — hands: search, calculator, weather, APIs.
- prompts/ — scripts the model reads every turn.
- utils/ — lights and wiring: config + logger.
Remember this
Trace one request left-to-right: api → agent → models → tools → back with an answer.
Inside agent/ (The Brain)
This is where product behavior lives. agent.py is the loop: load context, ask the LLM what to do next, stop when done. executor.py turns tool names into real function calls and catches errors. state.py tracks the current session. memory.py recalls longer-lived facts.
Keep tools out of agent.py. The loop should only see tool names and schemas registered from tools/. That separation is what makes testing possible — mock the LLM, assert the executor called the right tool.
Quick reference
- Put a max-steps guard in agent.py on day one.
- Timeout each tool call in executor.py.
- Session state ≠ long-term memory — keep them separate.
- Log correlation ids across LLM + tool calls.
- Unit-test the executor with mocks — no API spend.
- If you cannot name who owns retries, add it to executor.py.
Remember this
agent.py asks; executor.py does; state.py remembers this turn; memory.py remembers longer.
Tools, Models, and Prompts (Plugins)
Once the brain is clear, add three plugin layers.
tools/ — one file per capability (search.py, calculator.py, weather.py). Each exports name, description, input schema, handler, and a documented success/error result. models/ wraps the vendor SDK so changing providers does not rewrite the loop. prompts/ stores system and task templates as versioned code—review them in PRs like other logic.
Add one tool, wire it through the registry, and trace one successful call plus one timeout before adding the next. The goal is not a universal folder standard; it is stable dependency direction and contracts that tests can replace.
Quick reference
- Tool schema should match function-calling format your model expects.
- Validate tool args before hitting external APIs.
- models/: chat() + embed() is enough to start.
- prompts/: inject context at runtime — do not hardcode secrets.
- Log prompt token size so cost stays visible.
- Three solid tools beat thirty flaky ones.
Remember this
Tools are hands, models are the phone, prompts are the script — keep all three out of agent.py.
API and Utils (The Door and Wiring)
api/ is how other systems talk to your agent. routes.py defines POST /chat (and health). schemas.py validates the body before you spend tokens. Keep handlers thin: validate → agent.run() → JSON.
utils/ is shared wiring. config.py reads env. logger.py emits structured logs. When the CLI and the API both call the same agent/ package, you know the learning stuck — the brain is reusable.
Quick reference
- Reject bad input with 422 before calling the LLM.
- Pass a request id from HTTP headers into logs.
- Rate-limit at the API, not inside the agent loop.
- Optional: stream tokens with SSE once the happy path works.
- config.py should fail fast on missing API keys.
- Same agent package for CLI and server = healthy design.
Remember this
The API is the door; utils are the lights — neither should contain the agent loop.
Prove It, Then Run It
Structure without proof drifts. tests/ mirrors src/: mock the LLM in test_agent.py, fixture tools in test_tools.py, and hit routes with TestClient in test_api.py. The starter below is intentionally small and runnable; it proves the plugin boundary before adding an LLM SDK.
data/ holds examples and a knowledge_base/ for RAG. logs/ stays out of git. A later main.py may wire CLI or HTTP entry points, but the first practice succeeds when the tool contract runs under pytest with no network.
Quick reference
- Write the executor test before the third tool.
- Keep golden eval prompts under data/ when you start measuring quality.
- Never commit real user logs.
- main.py wires registry + config — keep it short.
- CI should run unit tests without network.
- Empty folders with README stubs beat mystery directories.
Remember this
tests/ prove the design; data/ and logs/ support it; main.py is how you run what you learned.
Key takeaway
Practice (30 min): run the scaffold exactly as shown and get 1 passed, then add the two expected calculator cases. Next create a fake slow tool, make the executor return {"ok": false, "error": "timeout"} after a short deadline, and assert the loop reaches a failed or retryable state without network access. Add a second tool without editing the control loop; if that requires a loop change, tighten the registry contract.
Only then evaluate LangGraph, CrewAI, or MCP. Frameworks are easier to place when the repository already answers where does this responsibility live? For the runtime definition behind this layout, see What Is Agentic AI?.
Related Articles
Explore this topic