The Night an Autonomous Script Burned My API Budget
The first time I tried building an autonomous coding agent, I left it running on my laptop while I went to have dinner. When I came back forty minutes later, my terminal was still rapidly spitting text. My heart sank when I opened my API dashboard: my script had executed 140 recursive iterations and burned fifteen dollars in API credits. Why? Because the agent hit a missing file error, failed to parse the message, and retried the exact same broken command 139 times in a row.
Most AI tutorials show you trivial toy examples: an agent that looks up the weather in London or searches Wikipedia. But when you build agents for real software engineering, single-shot prompt calls fall apart. Real problems require planning, executing shell commands, reading compiler errors, inspecting database tables, and looping until tests pass.
To build reliable autonomous agents that do not burn your money or crash production, you need proper software architecture: ReAct execution loops, Model Context Protocol (MCP) integrations, and sandboxed safety boundaries.
The 4 Pillars of a Production Agent Architecture
Every dependable agentic system separates responsibilities into four core layers:
┌─────────────────────────────────────────────────────────────┐
│ Autonomous Agent Core │
└─────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ 1. Reasoning & Planning │ │ 2. Tool Execution │
│ (ReAct: Thought-Action) │ │ (MCP / Typed Functions) │
└─────────────────────────┘ └─────────────────────────┘
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ 3. Short & Long Memory │ │ 4. Safety Guardrails │
│ (Working Context/Vector)│ │ (Loop Limits / Sandbox) │
└─────────────────────────┘ └─────────────────────────┘
1. The ReAct Pattern: Thought, Action, Observation
Without an explicit reasoning loop, language models guess early and make silly mistakes. The ReAct pattern (Reasoning + Acting) forces the model to verbalize a Thought before taking an Action (calling a tool), and then reads the tool's Observation back into context before deciding the next step.
Here is a lightweight TypeScript implementation of a resilient ReAct loop with Zod argument validation:
// src/agent/react-loop.ts
import { z } from 'zod';
export interface Tool {
name: string;
description: string;
parameters: z.ZodSchema<any>;
execute: (args: any) => Promise<string>;
}
export interface AgentStep {
thought: string;
action: string;
actionInput: Record<string, any>;
observation?: string;
}
export class AutonomousAgent {
private tools: Map<string, Tool> = new Map();
private maxIterations = 8;
constructor(tools: Tool[]) {
for (const tool of tools) {
this.tools.set(tool.name, tool);
}
}
public async runTask(taskGoal: string, modelCaller: (history: string) => Promise<string>): Promise<string> {
const steps: AgentStep[] = [];
let iteration = 0;
while (iteration < this.maxIterations) {
iteration++;
const contextPrompt = this.buildPrompt(taskGoal, steps);
// Get the next step from the LLM
const rawResponse = await modelCaller(contextPrompt);
const parsed = this.parseResponse(rawResponse);
if (parsed.action === 'FINISH') {
return parsed.thought;
}
const tool = this.tools.get(parsed.action);
if (!tool) {
parsed.observation = `Error: Tool "${parsed.action}" does not exist. Available tools: ${Array.from(this.tools.keys()).join(', ')}`;
} else {
try {
const validatedArgs = tool.parameters.parse(parsed.actionInput);
parsed.observation = await tool.execute(validatedArgs);
} catch (err: any) {
parsed.observation = `Tool Execution Failure: ${err.message}`;
}
}
steps.push(parsed);
}
throw new Error(`Agent exceeded maximum execution limit (${this.maxIterations} steps) without completing task.`);
}
private buildPrompt(goal: string, steps: AgentStep[]): string {
let prompt = `Goal: ${goal}\nAvailable Tools: ${Array.from(this.tools.values()).map(t => `${t.name}: ${t.description}`).join('; ')}\n\n`;
for (const step of steps) {
prompt += `Thought: ${step.thought}\nAction: ${step.action}(${JSON.stringify(step.actionInput)})\nObservation: ${step.observation}\n\n`;
}
prompt += `Provide your next Thought and Action. Format as valid JSON: {"thought": "...", "action": "...", "actionInput": {...}}`;
return prompt;
}
private parseResponse(raw: string): AgentStep {
try {
const jsonMatch = raw.match(/\{[\s\S]*\}/);
if (!jsonMatch) throw new Error('No JSON payload found.');
return JSON.parse(jsonMatch[0]);
} catch {
return { thought: raw, action: 'FINISH', actionInput: {} };
}
}
}
You can test and validate your tool response payloads using our free JSON Formatter to ensure the model produces valid schemas on every turn.
2. Why Model Context Protocol (MCP) Matters
Earlier, every company invented their own proprietary tool-calling schema. If you built a tool for OpenAI, you had to rewrite it completely to work with Claude or a local model running on Ollama. That wasted weeks of engineering effort.
The open-source Model Context Protocol (MCP) fixes this fragmentation. It standardizes communication between models and developer tools over JSON-RPC 2.0. MCP defines three clean primitives:
- Tools: Executable operations the agent can invoke (such as running a database migration, checking Git logs, or querying an API).
- Resources: Read-only documents and data streams the agent can inspect (such as server logs or database schemas).
- Prompts: Reusable system prompts and multi-step workflows defined as templates.
When you build your agent tools using MCP, your agent code works instantly across Claude Code, Cursor, and your own self-hosted local agents without changing a single line of backend logic.
3. Smart Memory Management: Working Memory vs Long-Term Memory
Context windows are limited and expensive. If you dump your entire codebase or a 200-page log file into the prompt, two bad things happen: your API cost skyrockets, and the model loses focus on the core instruction.
| Memory Tier | Storage Layer | Retrieval Latency | Best Use Case |
|---|---|---|---|
| Working Memory | In-memory array or Redis | Sub-5ms | Recent conversation turns, active tool returns, current task plan |
| Semantic Memory | PostgreSQL with pgvector | 20ms to 45ms | Past task resolutions, architecture decisions, codebase style guides |
Keep your active context lean. Use PostgreSQL with pgvector to store past task results. When a new task starts, run a quick semantic search to pull only the 2 or 3 most relevant examples into context.
4. Three Guardrails That Protect Your Budget and Servers
Before you run any autonomous agent, implement these three mandatory safety checks:
- Hard Loop Limits and Repeat Detection: Never set
maxIterationshigher than 10 for automated workflows. Calculate a hash of the(action, actionInput)pair on each turn. If the model repeats the exact same tool call twice with the same arguments, halt the loop and warn the user. - Output Truncation: If an agent runs a command that prints 5,000 lines of npm debug output, it will blow through your context window. Always truncate tool output strings to a sensible limit (such as 3,000 characters) before passing them back into the next prompt.
- Destructive Command Blocking: If your agent has access to a shell tool, blacklist destructive commands like
rm -rf,mkfs, andDROP TABLE. Run local agents inside isolated Docker containers with non-root user permissions.
You can estimate your agent's token usage and cost per step with our free Token Counter. Check out our step-by-step AI Engineer Roadmap and developer utilities in Free Developer Tools.
Autonomous agents are the future of software development, but you do not need hype or expensive platforms to build them. Start with a simple TypeScript ReAct loop, add safety limits, plug in MCP tools, and test everything locally. Build your first agent tonight.
