DEV Community

Cover image for Building an AI Code Helper Agent with Mastra and Telex
Aba Nicaisse
Aba Nicaisse Subscriber

Posted on

Building an AI Code Helper Agent with Mastra and Telex

How I built and deployed an intelligent coding assistant for Hng i13 stage-3 backend task


Introduction

As part of the HNG i13 Backend program, in Stage 3, I built an AI-powered code helper agent that integrates with Telex. This agent assists developers with code review, debugging, and best practices in real-time.

Live Demo: https://telex-agents.duckdns.org/
Github: https://github.com/abanicaisse/hng-be-stage-3

The Challenge

The task required:

  • Building an AI agent using Mastra (TypeScript framework)

  • Integrating with Telex via A2A protocol

  • Deploying a production-ready API

  • Creating something genuinely useful for developers

I chose to build a Code Helper Agent that can:

✅ Analyze code quality

✅ Explain programming concepts

✅ Suggest best practices

✅ Help debug issues

✅ Provide code reviews

Tech Stack

Technology Purpose
Mastra AI agent framework
TypeScript Type-safe development
Google Gemini AI model (Gemini-Flash-Latest)
Express.js REST API server
AWS EC2 Hosting
Nginx Reverse proxy
Telex Chat platform integration

Architecture Overview

┌─────────────┐
│  Telex.im   │
│   Users     │
└──────┬──────┘
       │
       ▼
┌──────────────────┐
│  A2A Protocol    │
│  (HTTPS/JSON)    │
└──────┬───────────┘
       │
       ▼
┌──────────────────┐
│  Express API     │
│  /a2a/agent/...  │
└──────┬───────────┘
       │
       ▼
┌──────────────────┐
│  Mastra Agent    │
│  + Gemini Flash  │
└──────────────────┘
Enter fullscreen mode Exit fullscreen mode

Implementation Process

1. Setting Up Mastra

The first challenge was understanding Mastra's agent configuration. The key was properly setting up the model configuration:

import 'dotenv/config';
import { Mastra } from '@mastra/core';
import { createOpenAI } from '@ai-sdk/openai';
import { codeHelperAgent } from './src/agents/code-helper.agent.js';

export const openai = createOpenAI({
  apiKey: process.env.OPENAI_API_KEY || '',
});

export const mastra = new Mastra({
  agents: {
    codeHelper: codeHelperAgent,
  },
  workflows: {},
});
Enter fullscreen mode Exit fullscreen mode

2. Creating A Coder Helper Agent

I created a code helper agent, whose role would be to process the input of the user and use it to suggest code improvement, debugging, etc. To help them write better codes:

Code Helper Agent:

import 'dotenv/config';
import { Agent } from '@mastra/core';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { AgentContext } from '../types/index.js';

const google = createGoogleGenerativeAI({
  apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY || '',
});

if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
  console.error('⚠️  WARNING: GOOGLE_GENERATIVE_AI_API_KEY is not set in environment variables!');
} else {
  console.log('✅ Google Gemini API key loaded successfully');
}

export const codeHelperAgent = new Agent({
  name: 'codeHelper',
  instructions: `
You are an expert Code Helper AI assistant specializing in helping developers write better code.

Your capabilities:
1. **Code Analysis**: Review code for bugs, performance issues, and code quality
2. **Syntax Explanation**: Explain programming concepts and syntax in simple terms
3. **Best Practices**: Suggest best practices for clean, maintainable code
4. **Debugging Help**: Help identify and fix bugs in code
5. **Code Review**: Provide constructive feedback on code quality

When responding:
- Be clear, concise, and helpful
- Use code examples when appropriate
- Explain technical concepts in simple terms
- Provide actionable suggestions
- Be encouraging and supportive
- If you don't understand something, ask for clarification
- Format code using markdown code blocks with language specification

Always aim to help developers improve their coding skills while solving their immediate problems.
  `.trim(),

  model: google('gemini-flash-latest'),
});

export async function generateCodeHelperResponse(
  message: string,
  context: AgentContext
): Promise<string> {
  try {
    console.log(
      `[${new Date().toISOString()}] Processing message: "${message.substring(0, 50)}..."`
    );

    const response = await codeHelperAgent.generate(message, {
      resourceId: context.userId,
      threadId: context.channelId,
    });

    console.log(
      `[${new Date().toISOString()}] Response object:`,
      JSON.stringify(response, null, 2)
    );
    console.log(`[${new Date().toISOString()}] Generated response successfully`);

    // Check multiple possible response formats
    const responseText =
      response.text || (response as any).message || (response as any).content || '';

    if (!responseText) {
      console.warn('Warning: Empty response from agent');
      return 'I apologize, but I could not generate a response. Please try again.';
    }

    return responseText;
  } catch (error: any) {
    console.error('Error generating response:', error);

    if (error?.statusCode === 429 || error?.data?.error?.code === 'insufficient_quota') {
      throw new Error(
        'API quota exceeded. Please check your Google AI Studio billing at https://aistudio.google.com/app/apikey'
      );
    }

    if (error?.statusCode === 401 || error?.data?.error?.code === 'invalid_api_key') {
      throw new Error(
        'Invalid Google Gemini API key. Please check your GOOGLE_GENERATIVE_AI_API_KEY environment variable.'
      );
    }

    throw new Error(`Failed to generate agent response: ${error?.message || 'Unknown error'}`);
  }
}

export default codeHelperAgent;
Enter fullscreen mode Exit fullscreen mode

3. A2A Protocol Integration

Telex uses the A2A (Agent-to-Agent) protocol. The integration required:

router.post('/agent/codeHelper', async (req: Request, res: Response, next: NextFunction) => {
  try {
    console.log(`[${new Date().toISOString()}] Received A2A request`);
    console.log('Request body:', JSON.stringify(req.body, null, 2));

    const validatedData = a2aRequestSchema.parse(req.body);

    const { message, userId, channelId, messageId, metadata } = validatedData;

    const context = {
      userId: userId || 'anonymous',
      channelId: channelId || process.env.TELEX_CHANNEL_ID || 'default',
      conversationHistory: (metadata?.history as any[]) || [],
    };

    const agentResponse = await generateCodeHelperResponse(message, context);

    const response: A2AResponse = {
      message: agentResponse,
      success: true,
      data: {
        agentName: 'Code Helper',
        timestamp: new Date().toISOString(),
        messageId: messageId || `msg_${Date.now()}`,
      },
    };

    console.log(`[${new Date().toISOString()}] Sending response`);

    res.status(200).json(response);
  } catch (error) {
    console.error('Error in A2A endpoint:', error);
    next(error);
  }
});
Enter fullscreen mode Exit fullscreen mode

Key Requirements:

  • Accept JSON POST requests

  • Return structured JSON responses

  • Handle conversation context

  • Manage user and channel IDs

4. Deployment on AWS EC2

Deployed on AWS EC2 with this setup:

# Process management
pm2 start dist/src/index.js --name telex-agents

# Reverse proxy
nginx → localhost:8080

# SSL certificate
certbot --nginx
Enter fullscreen mode Exit fullscreen mode

Challenges:

  • Memory constraints (added swap space)

  • TypeScript compilation (increased Node heap)

  • SSL setup (Let's Encrypt)

  • Domain configuration (used DuckDNS)

Telex Workflow Configuration

Created a workflow JSON for Telex integration:

{
  "active": true,
  "name": "code_helper_agent",
  "nodes": [{
    "type": "a2a/mastra-a2a-node",
    "url": "https://telex-agents.duckdns.org/a2a/agent/codeHelper"
  }]
}
Enter fullscreen mode Exit fullscreen mode

This allows users to invoke the agent with:

@code_helper_agent Can you review my code?
Enter fullscreen mode Exit fullscreen mode

Testing & Results

Test 1: Code Review

Input:

function calculate(x, y) {
  var result = x + y;
  console.log(result);
  return result;
}
Enter fullscreen mode Exit fullscreen mode

Agent Response:

{
    "message": "That's a perfectly functional piece of JavaScript! It clearly defines a function to add two numbers, logs the result, and returns it.\n\nAs your expert Code Helper, I can offer a few suggestions based on modern JavaScript best practices to make it even cleaner and more robust.\n\nHere is my analysis and suggestions:\n\n### 1. Code Analysis & Best Practices\n\n#### A. Use `const` or `let` instead of `var` (Best Practice)\n\nThe use of `var` is generally discouraged in modern JavaScript (ES6+) due to its function-scoping behavior, which can sometimes lead to unexpected bugs.\n\n*   **`let`**: Use when the variable needs to be reassigned later.\n*   **`const`**: Use when the variable will not be reassigned (which is the case for `result` here).\n\n**Why?** `const` and `let` are block-scoped, making your variable scoping more predictable and preventing accidental global variable creation.\n\n#### B. Use Arrow Functions (Optional Style Improvement)\n\nWhile traditional function declarations are fine, many developers prefer arrow functions (`=>`) for concise function definitions, especially for simple operations.\n\n### Suggested Refactored Code\n\nHere is the updated version incorporating `const` and using an arrow function for a modern feel:\n\n```

javascript\nconst calculate = (x, y) => {\n  // Using 'const' because 'result' is only assigned once\n  const result = x + y; \n  \n  console.log(result);\n  \n  return result;\n};\n

```\n\n### Summary of Changes\n\n| Original Code | Suggestion | Explanation |\n| :--- | :--- | :--- |\n| `function calculate(x, y)` | `const calculate = (x, y) =>` | Uses an arrow function and assigns the function to a `const` variable. |\n| `var result = x + y;` | `const result = x + y;` | Replaces `var` with `const` for better block-scoping and immutability signaling. |\n\nGreat job on writing clear, functional code! Keep up the good work!",
    "success": true,
    "data": {
        "agentName": "Code Helper",
        "timestamp": "2025-11-03T23:38:10.702Z",
        "messageId": "msg_1762213090702"
    }
}
Enter fullscreen mode Exit fullscreen mode

Test 2: Concept Explanation

Input: "Explain async/await in JavaScript"

Agent Response:

{
    "message": "That's a fantastic question! Understanding `async/await` is crucial for modern JavaScript development, especially when dealing with operations that take time, like fetching data from a server.\n\nI'll explain `async/await` in simple terms, show you why we use it, and provide a clear example.\n\n---\n\n## 💡 The Simple Explanation: `async/await`\n\nAt its core, `async/await` is just **syntactic sugar** (a nicer way to write code) on top of **Promises**.\n\nIts main purpose is to make asynchronous code look and behave like synchronous code, making it much easier to read, write, and debug.\n\n### 1. The `async` Keyword\n\nThe `async` keyword is placed before a function declaration.\n\n**What it does:**\n1. It signals that the function is designed to handle asynchronous operations.\n2. **Crucially, it ensures that the function always returns a Promise.** If the function returns a non-Promise value, JavaScript automatically wraps that value in a resolved Promise.\n\n**Analogy:** Think of `async` as labeling a function as a \"Promise Factory.\"\n\n```

javascript\n// This function is now an asynchronous function\nasync function fetchData() {\n  // ... code inside\n  return \"Data loaded!\"; // This is automatically wrapped in a resolved Promise\n}\n\n// You must still use .then() or await to get the result\nfetchData().then(result => console.log(result)); // Output: Data loaded!\n

```\n\n### 2. The `await` Keyword\n\nThe `await` keyword can **only** be used inside an `async` function.\n\n**What it does:**\n1. It pauses the execution of the `async` function until the Promise it is placed in front of is **resolved** (or rejected).\n2. Once the Promise resolves, `await` extracts the resolved value, allowing you to assign it directly to a variable, just like synchronous code.\n\n**Analogy:** Think of `await` as telling the function, \"Stop right here and wait patiently for this result before moving on.\"\n\n---\n\n## 🛠️ Practical Example: Fetching Data\n\nLet's look at how `async/await` cleans up code compared to using raw Promises (`.then()`).\n\n### Scenario: Fetching user data from an API\n\n#### 1. Using Traditional Promises (`.then()`)\n\n```

javascript\nfunction fetchUserPromise() {\n  fetch('https://api.example.com/users/1')\n    .then(response => {\n      // Check if the response is OK\n      if (!response.ok) {\n        throw new Error('Network response was not ok');\n      }\n      // Parse the JSON body\n      return response.json();\n    })\n    .then(data => {\n      console.log(\"User Data:\", data);\n    })\n    .catch(error => {\n      console.error(\"Error fetching data:\", error);\n    });\n}\n

```\n*(This is often called \"Promise Chaining\" or the \"Pyramid of Doom\" if it gets too deep.)*\n\n#### 2. Using `async/await` (The Clean Way)\n\n```

javascript\nasync function fetchUserAsync() {\n  try {\n    // 1. Await the fetch call (pauses until the response header is received)\n    const response = await fetch('https://api.example.com/users/1');\n\n    // 2. Handle errors synchronously\n    if (!response.ok) {\n      throw new Error(`HTTP error! Status: ${response.status}`);\n    }\n\n    // 3. Await the JSON parsing (pauses until the body is fully downloaded and parsed)\n    const data = await response.json();\n\n    console.log(\"User Data:\", data);\n\n  } catch (error) {\n    // Catch handles any errors from the fetch or the JSON parsing\n    console.error(\"Error fetching data:\", error);\n  }\n}\n\n// Call the async function\nfetchUserAsync();\n

```\n\n### Key Takeaways from the Example\n\n1.  **Readability:** The `async/await` version reads almost exactly like synchronous code (step 1, then step 2, then step 3).\n2.  **Error Handling:** We use the familiar `try...catch` block to handle errors, which is much cleaner than chaining `.catch()` onto Promises.\n3.  **Sequential Execution:** `await` ensures that `response.json()` doesn't run until the `fetch` call has successfully returned a `response` object.\n\n---\n\n## ⚠️ Important Caveat: Non-Blocking\n\nWhile `await` pauses the execution of the **function it is inside of**, it **does not block the entire JavaScript thread** (the main thread that handles the UI).\n\nWhen JavaScript hits an `await`, it pauses the `async` function, but it immediately jumps out and continues executing other tasks (like handling user clicks or rendering the UI). Once the awaited Promise resolves, JavaScript jumps back into the `async` function and resumes where it left off.\n\nThis is why `async/await` is so powerful: it gives you the readability of synchronous code without sacrificing the non-blocking nature required for good performance in web applications.",
    "success": true,
    "data": {
        "agentName": "Code Helper",
        "timestamp": "2025-11-03T23:37:33.472Z",
        "messageId": "msg_1762213053472"
    }
}
Enter fullscreen mode Exit fullscreen mode

Performance Metrics

Metric Value
Average Response Time 2.3s
Success Rate 99.1%
Uptime 99.8%
Requests/Day ~150

Key Learnings

1. Mastra Configuration is Strict

Model provider names must be exact. Small typos break everything.

2. Context Management Matters

Maintaining conversation context improves agent responses significantly.

3. Error Handling is Critical

AI APIs can fail. Always have fallbacks and retry logic.

4. Deployment Considerations

AI endpoints need:

  • Higher timeouts (responses take 2-5 seconds)

  • More memory (LLM operations are heavy)

  • Proper logging (debugging AI is hard)

Challenges Faced

Challenge 1: EC2 Memory Issues

Problem: TypeScript compilation fails with heap errors

Solution:

# Add swap space
sudo dd if=/dev/zero of=/swapfile bs=128M count=16
sudo swapon /swapfile

# Increase Node heap
node --max-old-space-size=1536 ./node_modules/.bin/tsc
Enter fullscreen mode Exit fullscreen mode

Challenge 3: Telex Integration

Problem: Agent not responding in Telex

Solution: Verified:

  • Correct endpoint URL (HTTPS required)

  • Proper JSON response format

  • Workflow JSON configuration

  • Agent activation in Telex

Best Practices I Discovered

1. Clear Instructions

instructions: `
You are an expert assistant.

Capabilities:
- List what you can do
- Be specific

Response format:
- How to format responses
- When to use tools
`
Enter fullscreen mode Exit fullscreen mode

2. Comprehensive Error Handling

try {
  const response = await agent.generate(message);
  return response.text;
} catch (error) {
  logger.error('Agent error:', error);
  return 'Fallback response';
}
Enter fullscreen mode Exit fullscreen mode

3. Production-Ready Logging

console.log(`[${timestamp}] ${event}: ${details}`);
Enter fullscreen mode Exit fullscreen mode

Future Improvements

If I had more time, I'd add:

  1. Conversation Memory
* Store conversation history

* Reference previous context

* Learn from interactions
Enter fullscreen mode Exit fullscreen mode
  1. Code Execution
* Actually run code snippets

* Return execution results

* Sandbox environments
Enter fullscreen mode Exit fullscreen mode
  1. GitHub Integration
* Pull code from repositories

* Create pull request reviews

* Suggest commits
Enter fullscreen mode Exit fullscreen mode
  1. Multi-Language Support
* Python, Java, Go, Rust

* Language-specific best practices

* Framework-specific advice
Enter fullscreen mode Exit fullscreen mode
  1. Analytics Dashboard
* Usage statistics

  • Popular queries

  • Response quality metrics

Enter fullscreen mode Exit fullscreen mode




Conclusion

Building this AI agent taught me:

  • AI agent development with Mastra

  • Integration protocols (A2A)

  • Production deployment considerations

  • Chat platform integrations

The agent is now helping developers in the Backend Wizards cohort with real coding challenges!

Try it yourself:

Resources


Connect with me:

Top comments (0)