Project Structure¶
This page describes the recommended directory layout for production projects using the ModernPath Agents Framework. The structure separates the backend agent runtime from the frontend UI, with an optional shared packages layer.
Recommended Layout¶
my-agent-project/
apps/
backend/
src/
agents/ -- Custom agent classes
tools/ -- Tool implementations
config/ -- Environment and framework configuration
server.ts -- HTTP server entry point
prompts/ -- YAML prompt templates
.env -- Environment variables (not committed)
package.json
tsconfig.json
ui/
src/
App.tsx -- React entry with AgentChat or AgentConsole
backends.ts -- Backend factory configuration
package.json
tsconfig.json
packages/ -- Shared domain types (optional)
shared-types/
src/
index.ts
package.json
package.json -- Root workspace config
.npmrc -- Artifact Registry scope config
Directory Details¶
apps/backend/¶
The backend workspace contains your agent runtime, tool implementations, and serverless entry points.
src/agents/¶
Custom agent classes that extend BaseAgent. Each agent typically has its own file:
import { BaseAgent, AgentContext, ToolRegistry } from "@modernpath/agent-framework";
import type { GeminiClient } from "@modernpath/agent-framework";
export class SupportAgent extends BaseAgent {
constructor(
toolRegistry: ToolRegistry,
private readonly gemini: GeminiClient,
) {
super("SupportAgent", "1.0.0", toolRegistry);
this.description = "Handles customer support queries with RAG-grounded answers.";
}
protected async executeInternal(context: AgentContext): Promise<any> {
// Agent logic here
}
clone(toolRegistry: ToolRegistry): BaseAgent {
return new SupportAgent(toolRegistry, this.gemini);
}
}
When to use built-in agents vs. custom agents
Use QAChatAgent when you need straightforward RAG Q&A -- it handles retrieval, prompt rendering, and canonical grounding out of the box. Write a custom BaseAgent subclass when you need a multi-step flow, custom tool orchestration, or domain-specific logic between retrieval and generation.
src/tools/¶
Tool implementations, each decorated with @Tool or created dynamically with createTool(). Group related tools into subdirectories if needed:
src/tools/
weather.ts -- WeatherTool
database/
query-tool.ts -- DatabaseQueryTool
write-tool.ts -- DatabaseWriteTool
external/
crm-tool.ts -- CrmLookupTool
src/config/¶
Environment loading and framework configuration. A typical setup reads from environment variables and constructs the GeminiClient, ToolRegistry, and agent instances:
import { GeminiClient, ToolRegistry } from "@modernpath/agent-framework";
import { PromptTemplate } from "@modernpath/agent-framework";
import { WeatherTool } from "../tools/weather";
import { SupportAgent } from "../agents/support-agent";
export async function bootstrap() {
const gemini = new GeminiClient({
apiKey: process.env.GOOGLE_AI_STUDIO_KEY!,
model: process.env.GEMINI_MODEL || "gemini-2.5-flash",
});
const prompts = new PromptTemplate("./prompts");
await prompts.load("yaml");
const registry = new ToolRegistry();
registry.register("get_weather", new WeatherTool());
const agent = new SupportAgent(registry, gemini);
return { gemini, prompts, registry, agent };
}
src/server.ts¶
The HTTP entry point. Uses framework handler factories and platform adapters:
import { createAgentExecuteHandler, gcpHttp } from "@modernpath/agent-framework";
import { bootstrap } from "./config/setup";
const { agent } = await bootstrap();
const handler = createAgentExecuteHandler({
resolveAgent: (agentType) => {
if (agentType === "support") return agent;
throw new Error(`Unknown agentType: ${agentType}`);
},
getUserId: (event) => {
// Extract from auth header in production
return parseInt(event.headers?.["x-user-id"] || "0", 10);
},
});
export const agentExecute = gcpHttp(handler);
prompts/¶
YAML files that define prompt templates. The PromptTemplate engine loads all files in this directory and registers them with dot-notation keys derived from the filename and YAML structure:
answer_generation:
system: |
You are a knowledgeable support assistant.
Answer questions accurately based on the provided context.
If the context does not contain enough information, say so.
user_template: |
{{#if conversationHistory}}
Previous conversation:
{{conversationHistory}}
{{/if}}
Context from knowledge base:
{{context}}
Question: {{question}}
This creates two template keys: qa-chat.answer_generation.system and qa-chat.answer_generation.user_template.
Template syntax
The built-in template engine supports {{variable}}, {{nested.path}}, {{#if var}}...{{/if}}, {{#each arr}}...{{/each}}, and {{json var}}. See Prompt Templates for the full reference.
apps/ui/¶
The frontend workspace contains your React application with ModernPath UI components.
src/App.tsx¶
The main React entry point. Typically renders AgentChat or AgentConsole:
import { AgentChat, createHttpAgentBackend } from "@modernpath/agent-ui-react";
const backend = createHttpAgentBackend({
baseUrl: import.meta.env.VITE_API_URL || "http://localhost:3001",
buildPath: (auditingId) => `/api/agents/${auditingId}/execute`,
getHeaders: () => ({
Authorization: `Bearer ${getToken()}`, // your auth logic
}),
});
export default function App() {
return (
<AgentChat
backend={backend}
auditingId={1}
agentType="support"
title="Support Agent"
showPlan={true}
showRetrievedSources={true}
showGroundingPolicy={true}
/>
);
}
src/backends.ts¶
A separate file for backend factory configuration keeps your component files clean:
import {
createHttpAgentBackend,
createSseAgentBackend,
} from "@modernpath/agent-ui-react";
export const httpBackend = createHttpAgentBackend({
baseUrl: import.meta.env.VITE_API_URL,
buildPath: (auditingId) => `/api/agents/${auditingId}/execute`,
});
export const sseBackend = createSseAgentBackend({
baseUrl: import.meta.env.VITE_API_URL,
buildPath: (auditingId) => `/api/agents/${auditingId}/execute/stream`,
});
packages/ (optional)¶
For larger projects, a shared types package avoids duplicating domain types between the backend and frontend:
export interface SupportTicket {
id: string;
title: string;
priority: "low" | "medium" | "high";
status: "open" | "resolved";
}
export interface AgentParameters {
store?: string;
topK?: number;
canonicalGrounding?: boolean;
}
Workspace Configuration¶
Use npm workspaces (or pnpm/yarn workspaces) to manage the monorepo:
{
"name": "my-agent-project",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
]
}
The .npmrc file at the root configures the @modernpath scope for all workspaces:
@modernpath:registry=https://europe-west1-npm.pkg.dev/YOUR_GCP_PROJECT_ID/modernpath-npm/
//europe-west1-npm.pkg.dev/YOUR_GCP_PROJECT_ID/modernpath-npm/:always-auth=true
File Naming Conventions¶
| Category | Convention | Example |
|---|---|---|
| Agent classes | PascalCase, suffixed with Agent | SupportAgent.ts |
| Tool classes | PascalCase, suffixed with Tool | WeatherTool.ts |
| Prompt files | kebab-case YAML, named after the agent | qa-chat.yaml, orchestrator.yaml |
| Config files | kebab-case | framework-config.ts, setup.ts |
| UI components | PascalCase | App.tsx, ChatPage.tsx |
| Backend factories | camelCase | backends.ts |
Minimal Single-File Project¶
For prototyping or small projects, you can skip the full workspace layout and put everything in a single file:
import {
GeminiClient,
ToolRegistry,
BaseAgent,
createAgentExecuteHandler,
gcpHttp,
createTool,
type AgentContext,
} from "@modernpath/agent-framework";
import express from "express";
// Tool
const registry = new ToolRegistry();
registry.register("echo", createTool(
{ name: "echo", description: "Echo input" },
async (params) => params,
));
// Gemini
const gemini = new GeminiClient({
apiKey: process.env.GOOGLE_AI_STUDIO_KEY!,
model: "gemini-2.5-flash",
});
// Agent
class EchoAgent extends BaseAgent {
constructor() { super("EchoAgent", "0.1.0", registry); }
protected async executeInternal(ctx: AgentContext) {
const res = await gemini.generateContent(ctx.prompt);
return { answer: res.text };
}
clone(tr: ToolRegistry) { return new EchoAgent(); }
}
// Server
const handler = createAgentExecuteHandler({
resolveAgent: () => new EchoAgent(),
getUserId: () => 1,
});
const app = express();
app.use(express.json());
app.post("/api/agents/:auditingId/execute", gcpHttp(handler) as any);
app.listen(3001, () => console.log("Running on :3001"));
Single-file is for prototyping only
For production applications, use the full workspace layout described above. It provides better separation of concerns, testability, and maintainability.