Stop Messy AI Projects: A Clean Folder Structure for Real Agent Systems

typescript dev.to

Every AI agent project starts the same way. You create an index.ts, add a prompt, maybe define a couple of tools, and everything works. For a while, it even feels clean and manageable. Then the system starts to grow. You introduce memory, add logging, experiment with multiple agents, and eventually build workflows. At that point, the simplicity disappears and the codebase turns into a collection of loosely connected files with no clear structure.

This is the part most tutorials skip. They show how to call a model, but they rarely show how to organize a system around it.

In a previous article, I discussed why AI agents should be designed as controlled systems where the model proposes actions and the application owns validation, execution, and safety. This article is the practical extension of that idea. If you were starting a TypeScript AI agent project today, this is the folder structure I would use to keep the system understandable and scalable.

At a high level, the structure looks like this:

my-ai-agent/
├── src/
│   ├── agents/
│   ├── tools/
│   ├── memory/
│   ├── workflows/
│   ├── mcp/
│   ├── prompts/
│   ├── middleware/
│   ├── types/
│   └── index.ts
├── config/
├── tests/
├── package.json
└── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

At first glance, it may feel like over-organization. In reality, you do not start with everything. You grow into it. The goal is not to create folders upfront, but to have a clear place for things as complexity increases.

This is the simplest way to think about the system. Each folder has a single responsibility, and that clarity is what keeps the system predictable as it grows.

The reason structure matters more in AI systems than in traditional applications is that the execution path is not fixed. In a typical backend, a request follows a known route. In an agent system, the path depends on the model’s decisions. The agent might call different tools, retrieve different memory, or stop midway for approval. That flexibility is powerful, but it also makes systems harder to debug and reason about. Without structure, debugging becomes guesswork. With structure, behavior becomes traceable.

The best way to approach this is to start smaller than you think. A minimal setup is often enough in the beginning:

src/
├── agents/
│   └── researcher.ts
├── tools/
│   └── search.ts
└── index.ts
Enter fullscreen mode Exit fullscreen mode

This is sufficient for a working agent. As the system grows, you introduce additional layers like memory, workflows, and middleware. The structure expands naturally instead of forcing a painful refactor later.

The agents folder is where you define what your system does. Each agent represents a role, typically combining a system prompt, a model configuration, and a set of tools. For example:

export const researcherAgent = {
  name: "researcher",
  systemPrompt: "You are a research assistant...",
  tools: ["web_search"],
  temperature: 0.3,
};
Enter fullscreen mode Exit fullscreen mode

This folder answers a simple but important question: what roles exist in your system?

The tools folder defines what the agent is allowed to do. Tools are where agents become useful, but they are also where risk enters the system. Each tool should be explicit and controlled:

export const searchTool = {
  name: "web_search",
  execute: async (query: string) => {
    return fetch(`/search?q=${query}`);
  },
};
Enter fullscreen mode Exit fullscreen mode

The key idea is not the implementation of the tool itself, but the boundary it creates. The agent should never have access to everything. It should only see and use tools that you explicitly register.

The memory folder is where many systems become unnecessarily complex. Instead of pushing everything into prompts, memory should be isolated and managed intentionally. A simple starting point is often enough:

export class ContextMemory {
  private messages: string[] = [];

  add(message: string) {
    this.messages.push(message);
  }

  getAll() {
    return this.messages;
  }
}
Enter fullscreen mode Exit fullscreen mode

You can introduce more advanced memory systems such as vector search only when the need becomes real.

The workflows folder is where individual agent actions become coordinated processes. Most real systems are not single-step interactions. They are sequences of decisions and actions:

export async function researchPipeline(topic: string) {
  const research = await researcherAgent.run(topic);
  const analysis = await analystAgent.run(research);
  return analysis;
}
Enter fullscreen mode Exit fullscreen mode

This is the point where you move from an agent to a system.

The mcp folder introduces a clean boundary for integrating external systems using the Model Context Protocol. As MCP adoption grows, isolating these integrations becomes increasingly valuable. Even with MCP, your application still needs to control access, validation, and permissions.

The prompts folder is about separating content from logic. As prompts evolve, keeping them inline makes iteration harder. Moving them into dedicated files allows faster updates without touching code.

The middleware folder is where production concerns live. This includes token budgets, logging, tracing, and rate limiting:

export class BudgetMiddleware {
  tokens = 0;

  track(usage: number) {
    this.tokens += usage;
  }
}
Enter fullscreen mode Exit fullscreen mode

This layer is often what separates a simple demo from a production-ready system.

The types folder is where TypeScript provides its real value. Centralizing interfaces ensures that when something changes, the impact is visible across the system:

export type Agent = {
  name: string;
  tools: string[];
};
Enter fullscreen mode Exit fullscreen mode

This makes evolving the system much safer.

What most people miss is that folder structure is not just about organization. It reflects architecture. If your code mixes tools, prompts, memory, and execution logic randomly, your system will behave the same way. If your folders enforce separation of concerns, your system becomes predictable. This aligns directly with the architectural principle that the runtime controls execution, the model proposes actions, and the system validates behavior.

Testing should follow the same philosophy. You do not need a complex setup at the beginning. A simple structure is enough:

tests/
├── unit/
└── integration/
Enter fullscreen mode Exit fullscreen mode

Start by testing tools and memory. Add workflow tests as the system evolves. End-to-end testing can come later once the system stabilizes.

As your project grows, the structure can evolve. You might introduce a providers folder if you support multiple LLMs, or a skills layer if capabilities become reusable across agents. At the same time, if the project remains small, it is perfectly valid to flatten the structure. The goal is not to follow a template rigidly, but to avoid chaos as complexity increases.

Most AI agent tutorials focus heavily on prompts and models. Very few focus on how to structure the system around them. In real-world projects, that is where most of the challenges appear. A good folder structure will not make your agent smarter, but it will make your system understandable, maintainable, and scalable. And in practice, that matters far more.

In the previous article https://dev.to/raju_dandigam/the-typescript-ai-agent-architecture-i-would-use-in-2026-18k6 I covered the architecture behind controlled AI agents and why the model should not own the system. In a future post, I will show how to combine that architecture with this structure to build a minimal but production-ready agent in TypeScript. That is where everything connects.

Source: dev.to

arrow_back Back to Tutorials