Cursor's Agent Loop Is Just 30 Lines of Code
Stop being superstitious about black boxes! A hands-on guide to building an AI programming assistant that can automatically create React projects.
Introduction: Can AI Programming Assistants Really "Think"?
When you tell Cursor, "Help me create a TodoList application," and it swiftly creates files, installs dependencies, and starts a service — is this magic or code?
The answer is: a carefully designed Agent loop + a few core tool functions.
Today, we're not playing games. We'll directly hand-write a mini-cursor. Using fewer than 300 lines of code, we'll replicate the core capabilities of an AI programming assistant. After reading this, you will thoroughly see through:
- How AI "pretends to think" while actually calling tools
- How LangChain becomes the AI's "nervous system"
- How Node.js lets the AI "get its hands dirty"
- How a complete Agent project should be organized
1. Architecture Panorama: How the AI's "Brain" and "Limbs" Cooperate
┌──────────────────────────────────────────────────────┐
│ mini-cursor Core Architecture │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ Agent Main Loop (mini-cursor.mjs) │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ ReAct Loop: │ │ │
│ │ │ Think → Act → Observe │ │ │
│ │ │ (Max 30 iterations) │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Tool Layer (all-tools.mjs) │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌────────┐ │ │
│ │ │Read │ │Write │ │List │ │Execute │ │ │
│ │ │File │ │File │ │Dir │ │Command │ │ │
│ │ └──────┘ └──────┘ └──────┘ └────────┘ │ │
│ └────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Node.js Child Process (node-exec.mjs)│ │
│ │ Isolated execution of time-consuming │ │
│ │ commands like pnpm / vite │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
One-sentence summary: The large model is responsible for "thinking," the tools for "doing," and the child process for "running."
2. Deep Dive into the Tool Layer: How the AI's "Limbs" Are Forged
All tools are created using LangChain's tool() function. This function does three things:
- Receives an asynchronous execution function
- Receives tool metadata (name, description, parameter Schema)
- Returns a standard tool object callable by the AI
🔧 Read File Tool — The AI's "Eyes"
const readFileTool = tool(
async({ filePath }) => {
const content = await fs.readFile(filePath, 'utf-8');
console.log(`[Tool Call] Read ${filePath}, ${content.length} characters total`);
return content;
},
{
name: 'read_file',
description: 'Read file content, used for viewing code, configuration files, etc.',
schema: z.object({
filePath: z.string().describe('File path, supports relative or absolute paths')
})
}
);
Design Points:
z.object()defines the parameter structure; the AI must pass parameters matching the format when calling.- Asynchronous reading, does not block the main flow.
- The return value is injected into the conversation context as a
ToolMessage.
✍️ Write File Tool — The AI's "Hand"
const writeFileTool = tool(
async({ filePath, content }) => {
try {
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true }); // Auto-create parent directories
await fs.writeFile(filePath, content, 'utf-8');
return `✅ Successfully wrote to ${filePath}`;
} catch(err) {
return `❌ Write failed: ${err.message}`;
}
},
{
name: 'write_file',
description: 'Write file content. Creates the file if it does not exist, auto-creates parent directories if they do not exist.',
schema: z.object({
filePath: z.string().describe('Target file path'),
content: z.string().describe('Content to write')
})
}
);
Engineering Wisdom:
recursive: truemeans the AI doesn't need to worry about whether directories exist.- Errors are caught and returned as friendly messages, allowing the AI to adjust its strategy based on the error.
📂 List Directory Tool — The AI's "Navigation"
const listDirectoryTool = tool(
async ({ directoryPath }) => {
const files = await fs.readdir(directoryPath);
return files.map(f => `📄 ${f}`).join('\n');
},
{
name: 'list_directory',
description: 'List directory contents to help the AI understand the project structure.',
schema: z.object({
directoryPath: z.string().describe('Directory path')
})
}
);
⚡ Execute Command Tool — The AI's "Superpower"
This is the most complex tool and the key to the AI actually "doing work":
const executeCommandTool = tool(
async ({ command, workingDirectory }) => {
const cwd = workingDirectory || process.cwd();
return new Promise((resolve) => {
const [cmd, ...args] = command.split(' ');
const child = spawn(cmd, args, {
cwd,
stdio: 'inherit', // Real-time output to terminal
shell: true,
});
let errorMsg = '';
child.on('error', (err) => { errorMsg = err.message });
child.on('close', (code) => {
if (code === 0) {
resolve(`✅ Command executed successfully: ${command}`);
} else {
resolve(`❌ Command failed (exit code: ${code}): ${errorMsg}`);
}
});
});
},
{
name: 'execute_command',
description: 'Execute a system command, supports specifying a working directory.',
schema: z.object({
command: z.string().describe('The command to execute'),
workingDirectory: z.string().describe('Working directory')
})
}
);
Key Design Decisions:
- Uses
spawninstead ofexec: streaming output, suitable for long-running commands. stdio: 'inherit': the user can see the real-time progress ofpnpm install.- Returns a Promise, perfectly integrating into the async/await system.
3. Agent Main Loop: How the AI's "Brain" Makes Decisions
3.1 Binding Tools to the Model
const model = new ChatOpenAI({
modelName: 'deepseek-v4-pro',
temperature: 0,
});
const modelWithTools = model.bindTools([
readFileTool,
writeFileTool,
listDirectoryTool,
executeCommandTool
]);
bindTools() is the core magic of LangChain:
- Automatically converts tool signatures into OpenAI's
function callingformat. - Lets the model know "I have these capabilities."
- The returned response will contain a
tool_callsfield.
3.2 System Prompt — Setting Rules for the AI
const messages = [
new SystemMessage(`
You are a project management assistant. Available tools:
1. read_file: Read files
2. write_file: Write files
3. execute_command: Execute commands (supports workingDirectory parameter)
4. list_directory: List directories
⚠️ Important Rules:
- When using execute_command, after specifying workingDirectory,
do not use cd to switch directories again in the command
- Correct: { command: "pnpm install", workingDirectory: "react-todo-app" }
- Incorrect: { command: "cd react-todo-app && pnpm install", workingDirectory: "react-todo-app" }
`),
new HumanMessage(query)
];
The Art of Prompt Engineering:
- Clearly tells the AI what tools are available.
- Provides examples of correct and incorrect usage.
- Prevents the AI from concatenating
cdin commands, which would cause path errors.
3.3 The ReAct Loop — The Soul of the Agent
async function runAgentWithTools(query, maxIterations = 30) {
const messages = [systemMessage, humanMessage];
for (let i = 0; i < maxIterations; i++) {
console.log(chalk.bgGreen(`🧠 Thinking iteration ${i}...`));
// 1. THINK: AI reasoning
const response = await modelWithTools.invoke(messages);
messages.push(response);
// 2. No tool calls → Task complete
if (!response.tool_calls?.length) {
console.log(chalk.green(`✅ ${response.content}`));
return response.content;
}
// 3. ACT: Execute tools
for (const toolCall of response.tool_calls) {
const tool = tools.find(t => t.name === toolCall.name);
if (tool) {
// 4. OBSERVE: Record results
const result = await tool.invoke(toolCall.args);
messages.push(new ToolMessage({
content: result,
tool_call_id: toolCall.id
}));
}
}
// Return to step 1, continue the loop
}
}
The Essence of the ReAct Loop:
User Query → 🤔 Think (AI decides which tool to use)
→ ⚡ Act (Execute the tool)
→ 👀 Observe (Add the result to context)
→ 🤔 Think (Decide the next step based on the result)
→ ⚡ Act ...
→ ✅ Task complete, output final answer
This process simulates the human problem-solving chain of thought: see a problem → think of a solution → take action → observe the result → adjust the plan → continue acting → finish.
4. Practical Task Execution: The Full Flow from Query to Completion
When we give the Agent this task:
const task = `
Create a React TodoList application:
1. Create the project using Vite
2. Implement full TodoList functionality
3. Add beautiful styles and animations
4. Install dependencies and start the service
`;
The Agent's decision trajectory looks like this:
🧠 Thinking iteration 0...
🤔 "I need to create the project first, using the create-vite command"
⚡ execute_command({
command: "pnpm create vite react-todo-app --template react-ts",
workingDirectory: process.cwd()
})
👀 "✅ Project created successfully"
🧠 Thinking iteration 1...
🤔 "Now I need to write the complete App.tsx code"
⚡ write_file({
filePath: "react-todo-app/src/App.tsx",
content: "Complete TodoList component code..."
})
👀 "✅ Successfully wrote 2847 bytes"
🧠 Thinking iteration 2...
🤔 "Need to install dependencies"
⚡ execute_command({
command: "pnpm install",
workingDirectory: "react-todo-app"
})
👀 "✅ Dependencies installed, 342 packages added"
🧠 Thinking iteration 3...
🤔 "Start the dev server"
⚡ execute_command({
command: "pnpm run dev",
workingDirectory: "react-todo-app"
})
👀 "✅ Service started at http://localhost:5173"
🧠 Thinking iteration 4...
🤔 "All tasks complete, ready to report"
💬 "🎉 TodoList application created! Visit http://localhost:5173 to view"
5. Child Process Isolation: Why Independent Execution is Needed
The Node.js main process is single-threaded. If pnpm install is executed in the main process, the entire Agent will be blocked. The solution:
// node-exec.mjs
const [cmd, ...args] = command.split(' ');
const child = spawn(cmd, args, {
cwd,
stdio: 'inherit', // Display output in real-time
shell: true, // Execute using Shell
});
child.on('close', (code) => {
if (code === 0) {
console.log('✅ Child process executed successfully');
process.exit(0);
} else {
console.log(`❌ Child process failed, exit code: ${code}`);
process.exit(code);
}
});
Why this design:
- Isolation: A child process crash does not affect the main Agent.
- Real-time:
stdio: 'inherit'lets the user see command output. - Monitorable: The
closeevent senses the command execution result.
6. Complete Execution Log Display
$ node mini-cursor.mjs
🧠 Thinking iteration 0...
[Tool Call] execute_command(pnpm create vite react-todo-app --template react-ts)
Working directory: /Users/xxx/projects
✔ Project created successfully
🧠 Thinking iteration 1...
[Tool Call] write_file(react-todo-app/src/App.tsx)
✅ Successfully wrote 2847 bytes
🧠 Thinking iteration 2...
[Tool Call] execute_command(pnpm install)
Working directory: react-todo-app
✔ Dependencies installed (342 packages)
🧠 Thinking iteration 3...
[Tool Call] execute_command(pnpm run dev)
Working directory: react-todo-app
➜ Local: http://localhost:5173/
🧠 Thinking iteration 4...
✅ AI Final Reply:
🎉 TodoList application created successfully!
- Project directory: react-todo-app
- Access URL: http://localhost:5173
- Features: Add/Delete/Mark Complete/Category Filter/Data Persistence
- Styles: Gradient background + Card design + Transition animations
7. Summary of Core Design Patterns
| Design Pattern | Implementation Method | Problem Solved |
|---|---|---|
| Tool Abstraction | tool() + Zod Schema |
Allows AI to call any function |
| Message History | Maintain a messages array |
Gives AI "memory" capability |
| ReAct Loop | Think → Act → Observe | Allows AI to complete tasks autonomously |
| Child Process Isolation | child_process.spawn |
Prevents time-consuming commands from blocking the main thread |
| Error Feedback | ToolMessage carries error information | Allows AI to adjust strategy based on errors |
8. Expansion and Optimization Directions
🚀 New Tools That Can Be Added Immediately
search_code: Search for code snippets in the projectgit_commit: Automatically commit coderun_tests: Execute test casesbrowser_open: Open a browser for preview
🧠 Decision Mechanisms That Can Be Optimized
- Parallel Execution: Multiple independent tool calls can use
Promise.allfor concurrency - Interrupt Mechanism: Users can interrupt infinite loops at any time
- Memory Module: Use a vector database to store project context
- Safety Confirmation: Ask the user before dangerous operations
Final Words: The Essence of AI Programming
By handwriting this mini-cursor, we discovered:
AI programming assistants are not mysterious; they are just a carefully designed tool-calling system.
The large model is responsible for "thinking" about what to do next, while the actual "execution" falls on the tool functions we write. This is why:
- Cursor can create projects → because it calls
execute_command - Cursor can modify code → because it calls
write_file - Cursor can understand code → because it calls
read_file
Once you grasp this core principle, you can:
- Customize your own AI programming assistant
- Add new capabilities to existing tools
- Understand the underlying logic of products like Claude Code and Copilot
Now, pick up your keyboard and create your own AI assistant! 🚀
💬 Discussion Topic: If you were to design an AI programming assistant, what "superpower" would you most want to give it? Feel free to share your ideas in the comments!