diff --git a/docs/tools/fetch.md b/docs/tools/fetch.md
new file mode 100644
index 0000000..612c993
--- /dev/null
+++ b/docs/tools/fetch.md
@@ -0,0 +1,102 @@
+# Fetch Tool
+
+The `fetch` tool allows MyCoder to make HTTP requests to external APIs. It uses the native Node.js fetch API and includes robust error handling capabilities.
+
+## Basic Usage
+
+```javascript
+const response = await fetch({
+  method: 'GET',
+  url: 'https://api.example.com/data',
+  headers: {
+    Authorization: 'Bearer token123',
+  },
+});
+
+console.log(response.status); // HTTP status code
+console.log(response.body); // Response body
+```
+
+## Parameters
+
+| Parameter  | Type    | Required | Description                                                               |
+| ---------- | ------- | -------- | ------------------------------------------------------------------------- |
+| method     | string  | Yes      | HTTP method to use (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)         |
+| url        | string  | Yes      | URL to make the request to                                                |
+| params     | object  | No       | Query parameters to append to the URL                                     |
+| body       | object  | No       | Request body (for POST, PUT, PATCH requests)                              |
+| headers    | object  | No       | Request headers                                                           |
+| maxRetries | number  | No       | Maximum number of retries for 4xx errors (default: 3, max: 5)             |
+| retryDelay | number  | No       | Initial delay in ms before retrying (default: 1000, min: 100, max: 30000) |
+| slowMode   | boolean | No       | Enable slow mode to avoid rate limits (default: false)                    |
+
+## Error Handling
+
+The fetch tool includes sophisticated error handling for different types of HTTP errors:
+
+### 400 Bad Request Errors
+
+When a 400 Bad Request error occurs, the fetch tool will automatically retry the request with exponential backoff. This helps handle temporary issues or malformed requests.
+
+```javascript
+// Fetch with custom retry settings for Bad Request errors
+const response = await fetch({
+  method: 'GET',
+  url: 'https://api.example.com/data',
+  maxRetries: 2, // Retry up to 2 times (3 requests total)
+  retryDelay: 500, // Start with a 500ms delay, then increase exponentially
+});
+```
+
+### 429 Rate Limit Errors
+
+For 429 Rate Limit Exceeded errors, the fetch tool will:
+
+1. Automatically retry with exponential backoff
+2. Respect the `Retry-After` header if provided by the server
+3. Switch to "slow mode" to prevent further rate limit errors
+
+```javascript
+// Fetch with rate limit handling
+const response = await fetch({
+  method: 'GET',
+  url: 'https://api.example.com/data',
+  maxRetries: 5, // Retry up to 5 times for rate limit errors
+  retryDelay: 1000, // Start with a 1 second delay
+});
+
+// Check if slow mode was enabled due to rate limiting
+if (response.slowModeEnabled) {
+  console.log('Slow mode was enabled to handle rate limits');
+}
+```
+
+### Preemptive Slow Mode
+
+You can enable slow mode preemptively to avoid hitting rate limits in the first place:
+
+```javascript
+// Start with slow mode enabled
+const response = await fetch({
+  method: 'GET',
+  url: 'https://api.example.com/data',
+  slowMode: true, // Enable slow mode from the first request
+});
+```
+
+### Network Errors
+
+The fetch tool also handles network errors (such as connection issues) with the same retry mechanism.
+
+## Response Object
+
+The fetch tool returns an object with the following properties:
+
+| Property        | Type             | Description                                                        |
+| --------------- | ---------------- | ------------------------------------------------------------------ |
+| status          | number           | HTTP status code                                                   |
+| statusText      | string           | HTTP status text                                                   |
+| headers         | object           | Response headers                                                   |
+| body            | string or object | Response body (parsed as JSON if content-type is application/json) |
+| retries         | number           | Number of retries performed (if any)                               |
+| slowModeEnabled | boolean          | Whether slow mode was enabled                                      |
diff --git a/packages/agent/src/tools/fetch/fetch.test.ts b/packages/agent/src/tools/fetch/fetch.test.ts
new file mode 100644
index 0000000..df4ec91
--- /dev/null
+++ b/packages/agent/src/tools/fetch/fetch.test.ts
@@ -0,0 +1,302 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+
+import { ToolContext } from '../../core/types.js';
+import { Logger } from '../../utils/logger.js';
+
+import { fetchTool } from './fetch.js';
+
+// Mock setTimeout to resolve immediately for all sleep calls
+vi.mock('node:timers', () => ({
+  setTimeout: (callback: () => void) => {
+    callback();
+    return { unref: vi.fn() };
+  },
+}));
+
+describe('fetchTool', () => {
+  // Create a mock logger
+  const mockLogger = {
+    debug: vi.fn(),
+    log: vi.fn(),
+    warn: vi.fn(),
+    error: vi.fn(),
+    info: vi.fn(),
+    prefix: '',
+    logLevel: 'debug',
+    logLevelIndex: 0,
+    name: 'test-logger',
+    child: vi.fn(),
+    withPrefix: vi.fn(),
+    setLevel: vi.fn(),
+    nesting: 0,
+    listeners: [],
+    emitMessages: vi.fn(),
+  } as unknown as Logger;
+
+  // Create a mock ToolContext
+  const mockContext = {
+    logger: mockLogger,
+    workingDirectory: '/test',
+    headless: true,
+    userSession: false, // Use boolean as required by type
+    tokenTracker: { remaining: 1000, used: 0, total: 1000 },
+    abortSignal: new AbortController().signal,
+    shellManager: {} as any,
+    sessionManager: {} as any,
+    agentManager: {} as any,
+    history: [],
+    statusUpdate: vi.fn(),
+    captureOutput: vi.fn(),
+    isSubAgent: false,
+    parentAgentId: null,
+    subAgentMode: 'disabled',
+  } as unknown as ToolContext;
+
+  // Mock global fetch
+  let originalFetch: typeof global.fetch;
+  let mockFetch: ReturnType<typeof vi.fn>;
+
+  beforeEach(() => {
+    originalFetch = global.fetch;
+    mockFetch = vi.fn();
+    global.fetch = mockFetch as any;
+    vi.clearAllMocks();
+  });
+
+  afterEach(() => {
+    global.fetch = originalFetch;
+  });
+
+  it('should make a successful request', async () => {
+    const mockResponse = {
+      status: 200,
+      statusText: 'OK',
+      headers: new Headers({ 'content-type': 'application/json' }),
+      json: async () => ({ data: 'test' }),
+      text: async () => 'test',
+      ok: true,
+    };
+    mockFetch.mockResolvedValueOnce(mockResponse);
+
+    const result = await fetchTool.execute(
+      { method: 'GET', url: 'https://example.com' },
+      mockContext,
+    );
+
+    expect(result).toEqual({
+      status: 200,
+      statusText: 'OK',
+      headers: { 'content-type': 'application/json' },
+      body: { data: 'test' },
+      retries: 0,
+      slowModeEnabled: false,
+    });
+    expect(mockFetch).toHaveBeenCalledTimes(1);
+  });
+
+  it('should retry on 400 Bad Request error', async () => {
+    const mockErrorResponse = {
+      status: 400,
+      statusText: 'Bad Request',
+      headers: new Headers({}),
+      text: async () => 'Bad Request',
+      ok: false,
+    };
+
+    const mockSuccessResponse = {
+      status: 200,
+      statusText: 'OK',
+      headers: new Headers({ 'content-type': 'application/json' }),
+      json: async () => ({ data: 'success' }),
+      text: async () => 'success',
+      ok: true,
+    };
+
+    // First request fails, second succeeds
+    mockFetch.mockResolvedValueOnce(mockErrorResponse);
+    mockFetch.mockResolvedValueOnce(mockSuccessResponse);
+
+    const result = await fetchTool.execute(
+      {
+        method: 'GET',
+        url: 'https://example.com',
+        maxRetries: 2,
+        retryDelay: 100,
+      },
+      mockContext,
+    );
+
+    expect(result).toEqual({
+      status: 200,
+      statusText: 'OK',
+      headers: { 'content-type': 'application/json' },
+      body: { data: 'success' },
+      retries: 1,
+      slowModeEnabled: false,
+    });
+    expect(mockFetch).toHaveBeenCalledTimes(2);
+    expect(mockLogger.warn).toHaveBeenCalledWith(
+      expect.stringContaining('400 Bad Request Error'),
+    );
+  });
+
+  it('should implement exponential backoff for 429 Rate Limit errors', async () => {
+    const mockRateLimitResponse = {
+      status: 429,
+      statusText: 'Too Many Requests',
+      headers: new Headers({ 'retry-after': '2' }), // 2 seconds
+      text: async () => 'Rate Limit Exceeded',
+      ok: false,
+    };
+
+    const mockSuccessResponse = {
+      status: 200,
+      statusText: 'OK',
+      headers: new Headers({ 'content-type': 'application/json' }),
+      json: async () => ({ data: 'success after rate limit' }),
+      text: async () => 'success',
+      ok: true,
+    };
+
+    mockFetch.mockResolvedValueOnce(mockRateLimitResponse);
+    mockFetch.mockResolvedValueOnce(mockSuccessResponse);
+
+    const result = await fetchTool.execute(
+      {
+        method: 'GET',
+        url: 'https://example.com',
+        maxRetries: 2,
+        retryDelay: 100,
+      },
+      mockContext,
+    );
+
+    expect(result).toEqual({
+      status: 200,
+      statusText: 'OK',
+      headers: { 'content-type': 'application/json' },
+      body: { data: 'success after rate limit' },
+      retries: 1,
+      slowModeEnabled: true, // Slow mode should be enabled after a rate limit error
+    });
+    expect(mockFetch).toHaveBeenCalledTimes(2);
+    expect(mockLogger.warn).toHaveBeenCalledWith(
+      expect.stringContaining('429 Rate Limit Exceeded'),
+    );
+  });
+
+  it('should throw an error after maximum retries', async () => {
+    const mockErrorResponse = {
+      status: 400,
+      statusText: 'Bad Request',
+      headers: new Headers({}),
+      text: async () => 'Bad Request',
+      ok: false,
+    };
+
+    // All requests fail
+    mockFetch.mockResolvedValue(mockErrorResponse);
+
+    await expect(
+      fetchTool.execute(
+        {
+          method: 'GET',
+          url: 'https://example.com',
+          maxRetries: 2,
+          retryDelay: 100,
+        },
+        mockContext,
+      ),
+    ).rejects.toThrow('Failed after 2 retries');
+
+    expect(mockFetch).toHaveBeenCalledTimes(3); // Initial + 2 retries
+    expect(mockLogger.warn).toHaveBeenCalledTimes(2); // Two retry warnings
+  });
+
+  it('should respect retry-after header with timestamp', async () => {
+    const futureDate = new Date(Date.now() + 3000).toUTCString();
+    const mockRateLimitResponse = {
+      status: 429,
+      statusText: 'Too Many Requests',
+      headers: new Headers({ 'retry-after': futureDate }),
+      text: async () => 'Rate Limit Exceeded',
+      ok: false,
+    };
+
+    const mockSuccessResponse = {
+      status: 200,
+      statusText: 'OK',
+      headers: new Headers({ 'content-type': 'application/json' }),
+      json: async () => ({ data: 'success' }),
+      text: async () => 'success',
+      ok: true,
+    };
+
+    mockFetch.mockResolvedValueOnce(mockRateLimitResponse);
+    mockFetch.mockResolvedValueOnce(mockSuccessResponse);
+
+    const result = await fetchTool.execute(
+      {
+        method: 'GET',
+        url: 'https://example.com',
+        maxRetries: 2,
+        retryDelay: 100,
+      },
+      mockContext,
+    );
+
+    expect(result.status).toBe(200);
+    expect(result.slowModeEnabled).toBe(true);
+    expect(mockFetch).toHaveBeenCalledTimes(2);
+  });
+
+  it('should handle network errors with retries', async () => {
+    mockFetch.mockRejectedValueOnce(new Error('Network error'));
+    mockFetch.mockResolvedValueOnce({
+      status: 200,
+      statusText: 'OK',
+      headers: new Headers({ 'content-type': 'application/json' }),
+      json: async () => ({ data: 'success after network error' }),
+      text: async () => 'success',
+      ok: true,
+    });
+
+    const result = await fetchTool.execute(
+      {
+        method: 'GET',
+        url: 'https://example.com',
+        maxRetries: 2,
+        retryDelay: 100,
+      },
+      mockContext,
+    );
+
+    expect(result.status).toBe(200);
+    expect(result.retries).toBe(1);
+    expect(mockFetch).toHaveBeenCalledTimes(2);
+    expect(mockLogger.error).toHaveBeenCalledWith(
+      expect.stringContaining('Request failed'),
+    );
+  });
+
+  it('should use slow mode when explicitly enabled', async () => {
+    // First request succeeds
+    mockFetch.mockResolvedValueOnce({
+      status: 200,
+      statusText: 'OK',
+      headers: new Headers({ 'content-type': 'application/json' }),
+      json: async () => ({ data: 'success in slow mode' }),
+      text: async () => 'success',
+      ok: true,
+    });
+
+    const result = await fetchTool.execute(
+      { method: 'GET', url: 'https://example.com', slowMode: true },
+      mockContext,
+    );
+
+    expect(result.status).toBe(200);
+    expect(result.slowModeEnabled).toBe(true);
+    expect(mockFetch).toHaveBeenCalledTimes(1);
+  });
+});
diff --git a/packages/agent/src/tools/fetch/fetch.ts b/packages/agent/src/tools/fetch/fetch.ts
index 5757ad5..4372bae 100644
--- a/packages/agent/src/tools/fetch/fetch.ts
+++ b/packages/agent/src/tools/fetch/fetch.ts
@@ -19,6 +19,23 @@ const parameterSchema = z.object({
     .optional()
     .describe('Optional request body (for POST, PUT, PATCH requests)'),
   headers: z.record(z.string()).optional().describe('Optional request headers'),
+  // New parameters for error handling
+  maxRetries: z
+    .number()
+    .min(0)
+    .max(5)
+    .optional()
+    .describe('Maximum number of retries for 4xx errors (default: 3)'),
+  retryDelay: z
+    .number()
+    .min(100)
+    .max(30000)
+    .optional()
+    .describe('Initial delay in ms before retrying (default: 1000)'),
+  slowMode: z
+    .boolean()
+    .optional()
+    .describe('Enable slow mode to avoid rate limits (default: false)'),
 });
 
 const returnSchema = z
@@ -27,12 +44,38 @@ const returnSchema = z
     statusText: z.string(),
     headers: z.record(z.string()),
     body: z.union([z.string(), z.record(z.any())]),
+    retries: z.number().optional(),
+    slowModeEnabled: z.boolean().optional(),
   })
   .describe('HTTP response including status, headers, and body');
 
 type Parameters = z.infer<typeof parameterSchema>;
 type ReturnType = z.infer<typeof returnSchema>;
 
+/**
+ * Sleep for a specified number of milliseconds
+ * @param ms Milliseconds to sleep
+ * @internal
+ */
+const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
+
+/**
+ * Calculate exponential backoff delay with jitter
+ * @param attempt Current attempt number (0-based)
+ * @param baseDelay Base delay in milliseconds
+ * @returns Delay in milliseconds with jitter
+ */
+const calculateBackoff = (attempt: number, baseDelay: number): number => {
+  // Calculate exponential backoff: baseDelay * 2^attempt
+  const expBackoff = baseDelay * Math.pow(2, attempt);
+
+  // Add jitter (±20%) to avoid thundering herd problem
+  const jitter = expBackoff * 0.2 * (Math.random() * 2 - 1);
+
+  // Return backoff with jitter, capped at 30 seconds
+  return Math.min(expBackoff + jitter, 30000);
+};
+
 export const fetchTool: Tool<Parameters, ReturnType> = {
   name: 'fetch',
   description:
@@ -43,65 +86,191 @@ export const fetchTool: Tool<Parameters, ReturnType> = {
   parametersJsonSchema: zodToJsonSchema(parameterSchema),
   returnsJsonSchema: zodToJsonSchema(returnSchema),
   execute: async (
-    { method, url, params, body, headers }: Parameters,
+    {
+      method,
+      url,
+      params,
+      body,
+      headers,
+      maxRetries = 3,
+      retryDelay = 1000,
+      slowMode = false,
+    }: Parameters,
     { logger },
   ): Promise<ReturnType> => {
-    logger.debug(`Starting ${method} request to ${url}`);
-    const urlObj = new URL(url);
-
-    // Add query parameters
-    if (params) {
-      logger.debug('Adding query parameters:', params);
-      Object.entries(params).forEach(([key, value]) =>
-        urlObj.searchParams.append(key, value as string),
-      );
-    }
+    let retries = 0;
+    let slowModeEnabled = slowMode;
+    let lastError: Error | null = null;
 
-    // Prepare request options
-    const options = {
-      method,
-      headers: {
-        ...(body &&
-          !['GET', 'HEAD'].includes(method) && {
-            'content-type': 'application/json',
-          }),
-        ...headers,
-      },
-      ...(body &&
-        !['GET', 'HEAD'].includes(method) && {
-          body: JSON.stringify(body),
-        }),
-    };
-
-    logger.debug('Request options:', options);
-    const response = await fetch(urlObj.toString(), options);
-    logger.debug(
-      `Request completed with status ${response.status} ${response.statusText}`,
-    );
+    while (retries <= maxRetries) {
+      try {
+        // If in slow mode, add a delay before making the request
+        if (slowModeEnabled && retries > 0) {
+          const slowModeDelay = 2000; // 2 seconds delay in slow mode
+          logger.debug(
+            `Slow mode enabled, waiting ${slowModeDelay}ms before request`,
+          );
+          await sleep(slowModeDelay);
+        }
+
+        logger.debug(
+          `Starting ${method} request to ${url}${retries > 0 ? ` (retry ${retries}/${maxRetries})` : ''}`,
+        );
+        const urlObj = new URL(url);
 
-    const contentType = response.headers.get('content-type');
-    const responseBody = contentType?.includes('application/json')
-      ? await response.json()
-      : await response.text();
+        // Add query parameters
+        if (params) {
+          logger.debug('Adding query parameters:', params);
+          Object.entries(params).forEach(([key, value]) =>
+            urlObj.searchParams.append(key, value as string),
+          );
+        }
 
-    logger.debug('Response content-type:', contentType);
+        // Prepare request options
+        const options = {
+          method,
+          headers: {
+            ...(body &&
+              !['GET', 'HEAD'].includes(method) && {
+                'content-type': 'application/json',
+              }),
+            ...headers,
+          },
+          ...(body &&
+            !['GET', 'HEAD'].includes(method) && {
+              body: JSON.stringify(body),
+            }),
+        };
 
-    return {
-      status: response.status,
-      statusText: response.statusText,
-      headers: Object.fromEntries(response.headers),
-      body: responseBody as ReturnType['body'],
-    };
+        logger.debug('Request options:', options);
+        const response = await fetch(urlObj.toString(), options);
+        logger.debug(
+          `Request completed with status ${response.status} ${response.statusText}`,
+        );
+
+        // Handle different 4xx errors
+        if (response.status >= 400 && response.status < 500) {
+          if (response.status === 400) {
+            // Bad Request - might be a temporary issue or problem with the request
+            if (retries < maxRetries) {
+              retries++;
+              const delay = calculateBackoff(retries, retryDelay);
+              logger.warn(
+                `400 Bad Request Error. Retrying in ${Math.round(delay)}ms (${retries}/${maxRetries})`,
+              );
+              await sleep(delay);
+              continue;
+            } else {
+              // Throw an error after max retries for bad request
+              throw new Error(
+                `Failed after ${maxRetries} retries: Bad Request (400)`,
+              );
+            }
+          } else if (response.status === 429) {
+            // Rate Limit Exceeded - implement exponential backoff
+            if (retries < maxRetries) {
+              retries++;
+              // Enable slow mode after the first rate limit error
+              slowModeEnabled = true;
+
+              // Get retry-after header if available, or use exponential backoff
+              const retryAfter = response.headers.get('retry-after');
+              let delay: number;
+
+              if (retryAfter) {
+                // If retry-after contains a timestamp
+                if (isNaN(Number(retryAfter))) {
+                  const retryDate = new Date(retryAfter).getTime();
+                  delay = retryDate - Date.now();
+                } else {
+                  // If retry-after contains seconds
+                  delay = parseInt(retryAfter, 10) * 1000;
+                }
+              } else {
+                // Use exponential backoff if no retry-after header
+                delay = calculateBackoff(retries, retryDelay);
+              }
+
+              logger.warn(
+                `429 Rate Limit Exceeded. Enabling slow mode and retrying in ${Math.round(delay)}ms (${retries}/${maxRetries})`,
+              );
+              await sleep(delay);
+              continue;
+            } else {
+              // Throw an error after max retries for rate limit
+              throw new Error(
+                `Failed after ${maxRetries} retries: Rate Limit Exceeded (429)`,
+              );
+            }
+          } else if (retries < maxRetries) {
+            // Other 4xx errors might be temporary, retry with backoff
+            retries++;
+            const delay = calculateBackoff(retries, retryDelay);
+            logger.warn(
+              `${response.status} Error. Retrying in ${Math.round(delay)}ms (${retries}/${maxRetries})`,
+            );
+            await sleep(delay);
+            continue;
+          } else {
+            // Throw an error after max retries for other 4xx errors
+            throw new Error(
+              `Failed after ${maxRetries} retries: HTTP ${response.status} (${response.statusText})`,
+            );
+          }
+        }
+
+        const contentType = response.headers.get('content-type');
+        const responseBody = contentType?.includes('application/json')
+          ? await response.json()
+          : await response.text();
+
+        logger.debug('Response content-type:', contentType);
+
+        return {
+          status: response.status,
+          statusText: response.statusText,
+          headers: Object.fromEntries(response.headers),
+          body: responseBody as ReturnType['body'],
+          retries,
+          slowModeEnabled,
+        };
+      } catch (error) {
+        lastError = error as Error;
+        logger.error(`Request failed: ${error}`);
+
+        if (retries < maxRetries) {
+          retries++;
+          const delay = calculateBackoff(retries, retryDelay);
+          logger.warn(
+            `Network error. Retrying in ${Math.round(delay)}ms (${retries}/${maxRetries})`,
+          );
+          await sleep(delay);
+        } else {
+          throw new Error(
+            `Failed after ${maxRetries} retries: ${lastError.message}`,
+          );
+        }
+      }
+    }
+
+    // This should never be reached due to the throw above, but TypeScript needs it
+    throw new Error(
+      `Failed after ${maxRetries} retries: ${lastError?.message || 'Unknown error'}`,
+    );
   },
   logParameters(params, { logger }) {
-    const { method, url, params: queryParams } = params;
+    const { method, url, params: queryParams, maxRetries, slowMode } = params;
     logger.log(
-      `${method} ${url}${queryParams ? `?${new URLSearchParams(queryParams).toString()}` : ''}`,
+      `${method} ${url}${queryParams ? `?${new URLSearchParams(queryParams).toString()}` : ''}${
+        maxRetries !== undefined ? ` (max retries: ${maxRetries})` : ''
+      }${slowMode ? ' (slow mode)' : ''}`,
     );
   },
 
   logReturns: (result, { logger }) => {
-    const { status, statusText } = result;
-    logger.log(`${status} ${statusText}`);
+    const { status, statusText, retries, slowModeEnabled } = result;
+    logger.log(
+      `${status} ${statusText}${retries ? ` after ${retries} retries` : ''}${slowModeEnabled ? ' (slow mode enabled)' : ''}`,
+    );
   },
 };