Introduction
Every functional agent—whether a simple chatbot or a complex autonomous system—is built from the same seven fundamental components.
Understanding these components is crucial because:
- You'll build better agents by knowing which components to use when
- You'll debug faster by identifying which component is causing issues
- You'll scale more easily by composing components in different ways
The 7 Components
Here's how all the components fit together:
┌─────────────────────────────────────────────────────────────┐
│ AI AGENT │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Prompt │───▶│ Model │───▶│ Output │ │
│ │ (Guide) │ │ (Reason) │ │ (Results) │ │
│ └──────────┘ └────┬─────┘ └──────────────┘ │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ │ │ │ │
│ ┌────▼───┐ ┌────▼────┐ ┌───▼─────┐ │
│ │ Data │ │ Tools │ │Knowledge│ │
│ │(Runtime)│ │(Actions)│ │ (RAG) │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ Human-in-Loop │ │
│ │ (Approval) │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Component Overview
- Prompt - Instructions that guide the agent's behavior
- Data - Runtime information the agent needs to operate
- Knowledge - Long-term information the agent can retrieve
- Tools - Functions the agent can call to take action
- Models - The LLM that powers reasoning
- Output - How the agent formats and delivers results
- Human-in-the-Loop - When and how humans intervene
The Example We'll Build
Throughout this chapter, we'll build a Job Alerts Agent that:
- Monitors job boards (RSS/API)
- Filters jobs based on your criteria
- Summarizes relevant opportunities
- Sends you a daily digest
- Asks for approval before sending
┌──────────────────────────────────────────────────────────┐
│ Job Alerts Agent Flow │
│ │
│ 1. Fetch Jobs ──▶ 2. Filter & Score ──▶ 3. Summarize │
│ (Tool) (Data + Model) (Output) │
│ │
│ │ │
│ ▼ │
│ 4. Request Approval │
│ (Human-in-Loop) │
│ │ │
│ ┌────┴────┐ │
│ │ │ │
│ Approved Rejected │
│ │ │ │
│ ▼ ▼ │
│ 5. Send Email Cancel │
│ (Tool) │
└──────────────────────────────────────────────────────────┘
By the end, you'll have a complete, production-ready agent that demonstrates all seven components working together.
Component 1: Prompt
The prompt is your agent's instruction manual. It defines what the agent does, how it behaves, and what it prioritizes.
Why Prompts Matter
The prompt is the most important component. A good prompt can make a weak model perform well. A bad prompt can make a strong model perform poorly.
Anatomy of a Good Prompt
1. ROLE: Who the agent is
2. TASK: What the agent does
3. CONTEXT: Information available
4. CONSTRAINTS: Rules and limitations
Example: Job Alerts Agent Prompt
const JOB_ALERT_PROMPT = `You are a Job Alerts Agent that helps users stay informed about relevant job opportunities. # YOUR ROLE You monitor job boards and filter opportunities based on user preferences. You provide clear, actionable summaries. # YOUR TASK 1. Analyze job postings from multiple sources 2. Filter based on user criteria (keywords, location, experience) 3. Summarize relevant opportunities concisely 4. Highlight: title, company, location, salary, requirements # CONSTRAINTS - Only include jobs matching 70%+ of criteria - Keep summaries to 2-3 sentences - Always include apply link - Flag remote vs on-site clearly - Never include jobs older than 7 days # OUTPUT FORMAT For each job: - 📋 Title & Company - 📍 Location (Remote/Hybrid/On-site) - 💰 Salary (if available) - ✅ Match score - 📝 Summary - 🔗 Apply link`;
Building with Mastra
import { Agent } from '@mastra/core'; const jobAlertAgent = new Agent({ name: 'Job Alert Agent', instructions: JOB_ALERT_PROMPT, model: { provider: 'OPEN_AI', name: 'gpt-4o', toolChoice: 'auto', }, });
Component 2: Data
Data is runtime information that flows into your agent—the dynamic context needed to make decisions.
Data vs Knowledge
Data: Changes frequently, passed at runtime
Knowledge: Changes rarely, stored and retrieved
Example: Today's job postings (data) vs. User preferences (knowledge)
Structured Data Pattern
import { z } from 'zod'; const JobContext = z.object({ userCriteria: z.object({ keywords: z.array(z.string()), minSalary: z.number(), location: z.string(), remote: z.boolean(), }), jobPostings: z.array(z.object({ id: z.string(), title: z.string(), company: z.string(), salary: z.string().optional(), location: z.string(), techStack: z.array(z.string()), })), }); async function generateAlert(context: z.infer<typeof JobContext>) { const validated = JobContext.parse(context); const prompt = ` User Criteria: ${JSON.stringify(validated.userCriteria)} Jobs: ${validated.jobPostings.map(j => `${j.title} at ${j.company}`).join('\n')} Filter and summarize matches.`; return await jobAlertAgent.generate(prompt); }
Component 3: Knowledge
Knowledge is long-term information your agent retrieves when needed via RAG (Retrieval-Augmented Generation).
Data vs Knowledge: Visual Distinction
┌─────────────────────────────────────────────────────────────┐
│ DATA (Component 2) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Today's │ │ User │ │ Current │ │
│ │ Jobs │ │ Input │ │ State │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ • Passed at runtime │
│ • Changes frequently │
│ • Specific to this request │
│ • Small enough to fit in prompt │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ KNOWLEDGE (Component 3) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Job │ │ User │ │ Company │ │
│ │ Archive │ │ Profile │ │ Docs │ │
│ │(10,000s) │ │(History) │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ • Stored in vector DB │
│ • Changes rarely │
│ • Retrieved via search │
│ • Too large for prompts │
└─────────────────────────────────────────────────────────────┘
Example Decision:
• Today's 20 new jobs → DATA (pass directly)
• Last year's 5,000 jobs → KNOWLEDGE (store & search)
When to Use Knowledge
- Information too large for prompts
- Rarely changes
- Multiple agents share it
- Needs search/filtering
RAG with Mastra
import { Mastra } from '@mastra/core'; export const mastra = new Mastra({ rag: { provider: 'PGVECTOR', config: { connectionString: process.env.DATABASE_URL!, }, }, }); // Index knowledge await mastra.rag.ingest({ collectionName: 'job-archive', documents: jobs.map(job => ({ id: job.id, content: `${job.title} at ${job.company}. ${job.description}`, metadata: { jobId: job.id, company: job.company }, })), }); // Query knowledge const results = await mastra.rag.query({ collectionName: 'job-archive', query: 'TypeScript jobs in San Francisco', limit: 5, });
Component 4: Tools
Tools are functions your agent calls to interact with the world.
How Tool Execution Works
┌─────────────────────────────────────────────────────────────┐
│ Tool Execution Flow │
│ │
│ 1. User Request │
│ "Find TypeScript jobs and email me" │
│ │ │
│ ▼ │
│ 2. Agent Reasoning (Model) │
│ "I need to: fetch jobs, then send email" │
│ │ │
│ ▼ │
│ 3. Tool Call: fetchJobs │
│ ┌─────────────────────────────────┐ │
│ │ Parameters: │ │
│ │ - source: "stackoverflow" │ │
│ │ - keywords: ["TypeScript"] │ │
│ │ - limit: 20 │ │
│ └─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 4. Tool Executes │
│ Fetches from API, validates data │
│ │ │
│ ▼ │
│ 5. Tool Returns Result │
│ { success: true, jobs: [...] } │
│ │ │
│ ▼ │
│ 6. Agent Processes Result │
│ "I got 15 jobs, now format and send email" │
│ │ │
│ ▼ │
│ 7. Tool Call: sendEmail │
│ ┌─────────────────────────────────┐ │
│ │ Parameters: │ │
│ │ - to: "user@example.com" │ │
│ │ - subject: "15 TypeScript Jobs" │ │
│ │ - html: "<formatted jobs>" │ │
│ └─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 8. Final Response │
│ "I found 15 jobs and sent them to your email" │
└─────────────────────────────────────────────────────────────┘
Tool Design Principles
- Single responsibility - Each tool does one thing well
- Clear descriptions - Agent knows when to use it
- Strict validation - Prevent bad inputs
- Error handling - Graceful failures
Example Tools
import { z } from 'zod'; const fetchJobsTool = { description: 'Fetch job postings from a specified job board', parameters: z.object({ source: z.enum(['stackoverflow', 'remoteok']), limit: z.number().min(1).max(50).default(20), }), execute: async ({ source, limit }) => { const jobs = await fetchFromSource(source, limit); return { success: true, count: jobs.length, jobs }; }, }; const sendEmailTool = { description: 'Send job alert email to user', parameters: z.object({ to: z.string().email(), subject: z.string(), content: z.string(), }), execute: async ({ to, subject, content }) => { const result = await resend.emails.send({ from: process.env.EMAIL_FROM!, to, subject, html: content, }); return { success: true, emailId: result.data?.id }; }, }; // Add to agent const agentWithTools = new Agent({ name: 'Job Alert Agent', instructions: JOB_ALERT_PROMPT, model: { provider: 'OPEN_AI', name: 'gpt-4o' }, tools: { fetchJobs: fetchJobsTool, sendEmail: sendEmailTool, }, });
Component 5: Models
The model is the LLM that powers reasoning. Choose based on cost, speed, and capability.
Model Selection Matrix
┌──────────────────────────────────────────────────────────────┐
│ COST vs CAPABILITY vs SPEED │
│ │
│ High │
│ Cost │ │
│ │ ◉ gpt-4o │
│ │ (Most capable) │
│ │ • Complex reasoning │
│ │ • Best quality │
│ │ • ~$0.01/1K tokens │
│ │ │
│ Mid │ ◉ gpt-4o-mini │
│ Cost │ (Best value) │
│ │ • Good reasoning │
│ │ • Fast │
│ │ • ~$0.0001/1K tokens │
│ │ │
│ Low │ │
│ Cost │ │
│ └───────────────────────────────────────────────▶ │
│ Simple Moderate Complex │
│ Tasks Tasks Tasks │
│ │
│ Decision Guide: │
│ • Classification → gpt-4o-mini │
│ • Summarization → gpt-4o-mini │
│ • Analysis → gpt-4o │
│ • Creative writing → gpt-4o │
│ • Production scale → gpt-4o-mini │
└──────────────────────────────────────────────────────────────┘
Model Selection Criteria
Use Case | Model | Why |
---|---|---|
Simple classification | gpt-4o-mini | Fast, cheap, accurate |
Complex reasoning | gpt-4o | Most capable |
Production at scale | gpt-4o-mini | Cost-effective |
Streaming responses | gpt-4o | Best UX |
Multi-step workflows | gpt-4o | Better planning |
Configuring Models
const agent = new Agent({ name: 'Job Alert Agent', instructions: JOB_ALERT_PROMPT, model: { provider: 'OPEN_AI', name: 'gpt-4o-mini', // Fast and cheap for production toolChoice: 'auto', temperature: 0.3, // Lower = more consistent maxTokens: 2000, }, });
Multi-Model Pattern
// Use different models for different tasks const classifierAgent = new Agent({ model: { provider: 'OPEN_AI', name: 'gpt-4o-mini' }, // Fast classification }); const summarizerAgent = new Agent({ model: { provider: 'OPEN_AI', name: 'gpt-4o' }, // Better summaries });
Component 6: Output
Output is how the agent formats and delivers results. Structure it for your use case.
Output Patterns
Pattern 1: Structured Output
import { generateObject } from 'ai'; import { z } from 'zod'; const JobAlertSchema = z.object({ matchingJobs: z.array(z.object({ title: z.string(), company: z.string(), matchScore: z.number(), summary: z.string(), applyUrl: z.string(), })), totalMatches: z.number(), recommendation: z.string(), }); async function generateStructuredAlert(criteria: Criteria, jobs: Job[]) { const result = await generateObject({ model: openai('gpt-4o'), schema: JobAlertSchema, prompt: `Analyze these jobs and return structured matches: ${JSON.stringify(jobs)}`, }); return result.object; // Typed output! }
Pattern 2: Streaming Output
async function streamJobAlert(criteria: Criteria) { const stream = await agent.stream( `Find jobs matching: ${JSON.stringify(criteria)}` ); for await (const chunk of stream) { process.stdout.write(chunk); } }
Pattern 3: HTML Output
function formatAsHTML(jobs: Job[]) { return ` <!DOCTYPE html> <html> <body> <h1>Your Daily Job Alert</h1> ${jobs.map(job => ` <div style="border: 1px solid #ccc; padding: 16px; margin: 16px 0;"> <h2>${job.title}</h2> <p><strong>${job.company}</strong> | ${job.location}</p> <p>${job.summary}</p> <a href="${job.applyUrl}">Apply Now</a> </div> `).join('')} </body> </html>`; }
Component 7: Human-in-the-Loop
HITL is when agents pause for human approval before taking action.
Human-in-the-Loop Workflow
┌─────────────────────────────────────────────────────────────┐
│ Human-in-the-Loop Flow │
│ │
│ 1. Agent Completes Task │
│ ┌────────────────────────────────┐ │
│ │ Found 12 matching jobs │ │
│ │ Ready to send email │ │
│ └────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 2. Check if Approval Needed │
│ ┌─────────────┬──────────────┐ │
│ │ High stakes?│ First time? │ │
│ │ Low confidence? User pref? │ │
│ └─────────────┴──────────────┘ │
│ │ │
│ ┌───┴───┐ │
│ │ │ │
│ Yes No │
│ │ │ │
│ │ └──────────────────────┐ │
│ ▼ ▼ │
│ 3. Request Approval 5. Execute Action │
│ ┌────────────────────────┐ Immediately │
│ │ Email/Slack/Dashboard │ │
│ │ "Review before send?" │ │
│ └────────────────────────┘ │
│ │ │
│ ▼ │
│ 4. Wait for Human Decision │
│ ┌────────┬────────┐ │
│ │ │ │ │
│ Approve Reject Modify │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Execute Cancel Re-run │
│ Action with changes │
│ │
│ 6. Log Decision │
│ Store approval/rejection with reason │
└─────────────────────────────────────────────────────────────┘
When to Use HITL
- High-stakes decisions - Money, legal, public communication
- First-time operations - New user, new workflow
- Low confidence scores - Agent is unsure
- User preference - User wants control
Implementing HITL
class JobAlertWithApproval { async generate(userId: string) { // 1. Agent finds matching jobs const matches = await this.findMatches(userId); // 2. Request approval const approved = await this.requestApproval(userId, matches); // 3. Only send if approved if (approved) { await this.sendEmail(userId, matches); return { sent: true, jobCount: matches.length }; } return { sent: false, reason: 'User declined' }; } private async requestApproval(userId: string, matches: Job[]) { // Store pending approval await db.pendingApprovals.create({ userId, type: 'job-alert', data: matches, expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), }); // Send approval request await this.sendApprovalRequest(userId, matches); // Wait for response (via webhook/polling) return await this.waitForApproval(userId); } }
Approval UI Pattern
// API route for approval app.post('/api/approve/:approvalId', async (req, res) => { const { approvalId } = req.params; const { approved } = req.body; const approval = await db.pendingApprovals.findById(approvalId); if (approved) { // Execute the action await sendEmailTool.execute({ to: approval.userEmail, subject: 'Your Job Matches', content: formatJobs(approval.data), }); } await db.pendingApprovals.delete(approvalId); res.json({ success: true }); });
Complete Working Example
Here's the full job alerts agent with all 7 components:
import { Agent, Mastra } from '@mastra/core'; import { z } from 'zod'; import { Resend } from 'resend'; // Component 1: Prompt const PROMPT = `You are a Job Alerts Agent...`; // Component 3: Knowledge (RAG setup) const mastra = new Mastra({ rag: { provider: 'PGVECTOR', config: { connectionString: process.env.DATABASE_URL! }, }, }); // Component 4: Tools const tools = { fetchJobs: { description: 'Fetch jobs from a source', parameters: z.object({ source: z.enum(['stackoverflow', 'remoteok']), limit: z.number().default(20), }), execute: async ({ source, limit }) => { const jobs = await fetchFromSource(source, limit); return { jobs }; }, }, sendEmail: { description: 'Send email alert', parameters: z.object({ to: z.string().email(), subject: z.string(), html: z.string(), }), execute: async ({ to, subject, html }) => { const resend = new Resend(process.env.RESEND_API_KEY); await resend.emails.send({ from: process.env.EMAIL_FROM!, to, subject, html, }); return { sent: true }; }, }, }; // Component 5: Model + All Components const agent = new Agent({ name: 'Job Alert Agent', instructions: PROMPT, model: { provider: 'OPEN_AI', name: 'gpt-4o-mini', toolChoice: 'auto', }, tools, }); // Main service with all 7 components class JobAlertService { async generateDailyAlert(userId: string) { // Component 2: Load data const criteria = await this.loadUserCriteria(userId); const jobs = await this.fetchRecentJobs(); // Component 3: Query knowledge const historicalMatches = await mastra.rag.query({ collectionName: 'job-archive', query: `Previous matches for user ${userId}`, }); // Agent processes with tools const result = await agent.generate(` User: ${userId} Criteria: ${JSON.stringify(criteria)} Jobs: ${JSON.stringify(jobs)} Previous matches: ${JSON.stringify(historicalMatches)} Find matching jobs and prepare an email.`); // Component 7: HITL - Request approval const approved = await this.requestApproval(userId, result); if (approved) { // Component 6: Send structured output return { sent: true, result: result.text }; } return { sent: false, reason: 'Awaiting approval' }; } private async loadUserCriteria(userId: string) { return { keywords: ['TypeScript', 'React'], minSalary: 120000, remote: true, }; } private async fetchRecentJobs() { // Fetch from job boards return []; } private async requestApproval(userId: string, result: any) { // Send approval request, wait for response return true; } } // Usage const service = new JobAlertService(); await service.generateDailyAlert('user-123');
How to Run It
- Install dependencies:
npm install @mastra/core @ai-sdk/openai zod resend
- Set environment variables:
OPENAI_API_KEY=your-key RESEND_API_KEY=your-key EMAIL_FROM=alerts@yourdomain.com DATABASE_URL=postgresql://...
- Run:
npx tsx job-alert-agent.ts
How to Extend It
Add more job boards:
tools.fetchJobs = { // Add 'hn-hiring', 'linkedin', etc. parameters: z.object({ source: z.enum(['stackoverflow', 'remoteok', 'hn-hiring', 'linkedin']), }), };
Add salary negotiation advice:
tools.getSalaryInsights = { description: 'Get salary insights for a role', execute: async ({ title, location }) => { // Call salary API return { median: 150000, range: [120000, 180000] }; }, };
Add calendar integration:
tools.scheduleInterview = { description: 'Schedule interview in calendar', execute: async ({ jobId, date }) => { // Add to Google Calendar }, };
Key Takeaways
- Prompt is foundation - Everything else builds on good instructions
- Data vs Knowledge - Pass frequent data, store rare knowledge
- Tools enable action - Without tools, agents only talk
- Choose models wisely - Balance cost, speed, capability
- Structure output - Make it easy to consume
- HITL for safety - Pause before high-stakes actions
- Compose iteratively - Start simple, add components as needed
What's Next
You now understand the 7 components. Next chapter: Triggers - when and how agents wake up to do their work.
Then we'll dive into real patterns: event processing, monitoring, scheduled analysis, and more.
Get chapter updates & code samples
We’ll email diagrams, code snippets, and additions.