Skip to content

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.


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:

apps/backend/src/agents/support-agent.ts
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:

apps/backend/src/config/setup.ts
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:

apps/backend/src/server.ts
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:

apps/backend/prompts/qa-chat.yaml
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:

apps/ui/src/App.tsx
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:

apps/ui/src/backends.ts
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:

packages/shared-types/src/index.ts
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:

package.json (root)
{
  "name": "my-agent-project",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*"
  ]
}

The .npmrc file at the root configures the @modernpath scope for all workspaces:

.npmrc
@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:

agent.ts
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.