Skip to content

AI Agent Project Structure: Folders, Files, and Responsibilities

Core Concept LearningJuly 11, 20266 min readUpdated July 21, 2026

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.

AI agent project structure — file tree and responsibilities
AI agent project structure — file tree and responsibilities

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.

Root configuration — docs, deps, secrets, optional services
Root configuration — docs, deps, secrets, optional services

Quick reference

  • Commit .env.example with fake keys; keep real .env local 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 run when 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.

src/ — follow one request through the six packages
src/ — follow one request through the six packages

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.

agent/ loop — plan, execute tools, update state and memory
agent/ loop — plan, execute tools, update state and memory

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.
Hard to learn — everything mixed
1# One file: loop + OpenAI + weather API + SQL memory2def run(query):3    # 600 lines later… nobody knows where to edit4    pass
Pseudo-code: framework-neutral loop contract
1# PSEUDO-CODE: llm, memory, registry, and executor are ports.2def run(query, session_id, *, llm, memory, registry, executor):3    ctx = memory.load(session_id)4    for step in range(8):5        action = llm.plan(query, ctx, tools=registry.list())6        if action.type == "finish":7            return {"status": "completed", "answer": action.answer}8        if action.type == "tool":9            result = executor.run(10                action.name, action.args, timeout_seconds=1011            )12            ctx = [*ctx, {"tool_result": result}]13        else:14            return {"status": "failed", "error": "invalid_action"}15    return {"status": "failed", "error": "max_steps_exceeded"}

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.
src/tools/calculator.py — pasteable tool
1from dataclasses import dataclass2 3@dataclass(frozen=True)4class ToolResult:5    ok: bool6    value: float | None = None7    error: str | None = None8 9def calculate(a: float, b: float, operation: str) -> ToolResult:10    if operation == "add":11        return ToolResult(ok=True, value=a + b)12    if operation == "divide" and b == 0:13        return ToolResult(ok=False, error="division_by_zero")14    if operation == "divide":15        return ToolResult(ok=True, value=a / b)16    return ToolResult(ok=False, error="unsupported_operation")
tests/test_calculator.py — pasteable tests
1from src.tools.calculator import calculate2 3def test_add_and_error_contract():4    assert calculate(2, 3, "add").value == 55    failed = calculate(4, 0, "divide")6    assert failed.ok is False7    assert failed.error == "division_by_zero"

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.

API path — validate, run agent, return JSON
API path — validate, run agent, return JSON

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.

tests/, data/, logs/, and main.py — outside src/ but essential
tests/, data/, logs/, and main.py — outside src/ but essential

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.
Shell scaffold (macOS/Linux)
1mkdir -p agent-demo/src/tools agent-demo/tests2cd agent-demo3touch src/__init__.py src/tools/__init__.py4python3 -m venv .venv5source .venv/bin/activate6python -m pip install pytest7# Paste the calculator and test panels into their labeled paths.8pytest -q
Expected verification
1# Expected:2# 1 passed3#4# Then add tests for:5# calculate(4, 2, "divide") -> value 26# calculate(1, 1, "multiply") -> unsupported_operation

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?.

Share:

Related Articles

Full parameter fine-tuning of Large Language Models (such as Llama 3 70B or Qwen 2.5) requires updating billions of weig

Read

AI terminology is often drawn as one neat stack, but the axes are not identical. Artificial intelligence is the broad fi

Read

As autonomous AI coding agents (such as Claude Code, Gemini CLI, and Cursor) take on complex software tasks, measuring t

Read

Explore this topic

Keep learning

Follow a structured path or browse all courses to go deeper.