Official Web Agent & Architecture Guide

CodeCraft AI Autonomous In-Browser Coding Companion

CodeCraft AI began in late 2023 as an open custom GPT to give self-taught developers a free alternative to $20/month AI subscriptions. Today, that experiment has evolved into Dropout Agent: an autonomous developer companion running straight in your browser and terminal. Read workspaces, modify code, and verify test suites with zero subscription fees.

Launch Dropout Agent (Free)
100% Free to Use Multi-Step Tool Calling Local Sandbox Fencing Zero Account Paywalls
codecraft-agent : workspace execution loop
Agent Ready
Task > Inspect src/auth/token.ts, add isTokenExpired() helper with defensive error checks, and verify tests pass.
[Step 1]
readFile({ filePath: "src/auth/token.ts" }) ✓ Read 1,420 characters from disk
[Step 2]
writeFile({ filePath: "src/auth/token.ts" }) ✓ Injected isTokenExpired() with try/catch payload parsing
[Step 3]
runCommand({ command: "npm test tests/token.test.ts" }) ✓ PASS tests/token.test.ts (4 passed, 14ms)
Agent Verdict: Implemented isTokenExpired() in src/auth/token.ts with defensive error handling for malformed tokens. All 4 unit tests in tests/token.test.ts are passing.
Autonomous terminal execution with Vercel AI SDK Try this in Dropout Agent →

Evolution

Why Chat Windows Fail for Real Engineering

Pasting individual snippets into a browser chat window strips away compiler feedback, package manifests, and project relationships. Here is how an autonomous agent fixes that.

01

Direct Workspace Access

Instead of asking you to copy code back and forth, the agent reads and writes directly into your workspace files, preserving project imports and type definitions.

Tool: readFile & writeFile
02

Self-Correction Loop

Normal LLMs guess. CodeCraft AI executes your test runner (npm test or pytest). If a test fails, it inspects the error trace and fixes the code automatically before finishing.

Tool: runCommand with maxSteps
03

Local Sandbox Fencing

Safety by design. Path resolution ensures the model never accesses sensitive system locations outside your designated project root, blocking malicious directory traversal attempts.

Guardrail: path.resolve checks
Systems Architecture

Build Your Own CodeCraft AI Companion

Construct an autonomous, project-aware coding companion in under 200 lines of TypeScript using Node 22 LTS, Vercel AI SDK, and Anthropic tool calling.

Execution Flow: The Tool-Calling Loop
User Prompt: "Add slugify helper to string-utils and verify tests pass"
   │
   ▼
[Agent Controller (Vercel AI SDK)] ───► Evaluates available tools
   │
   ├── Step 1: Tool Call `read_file` (src/string-utils.ts)
   │     └── Tool Result: Returns existing source code
   │
   ├── Step 2: Tool Call `write_file` (adds slugify function and export)
   │     └── Tool Result: File updated on disk
   │
   ├── Step 3: Tool Call `run_command` (npm test tests/string-utils.test.ts)
   │     └── Tool Result: PASS 1 test passed (14ms)
   │
   ▼
Final Response: "Added slugify() with unicode normalization. Tests pass."

Step 1: Installing Dependencies and Setup

We use Node 22 LTS with native ES modules and TypeScript 5.7. The Vercel AI SDK provides high-level primitives for multi-step tool resolution (maxSteps), streaming token delivery, and provider abstractions.

# Initialize project
mkdir dev-companion && cd dev-companion
npm init -y

# Install Vercel AI SDK, Anthropic provider, and Zod
npm install ai @ai-sdk/anthropic zod dotenv
npm install -D typescript @types/node tsx

# Initialize TypeScript configuration
npx tsc --init

Store your API key in a local .env file:

ANTHROPIC_API_KEY=sk-ant-api03-xxxx
WORKSPACE_ROOT=/Users/username/projects/my-app

Step 2: Defining Schema-Enforced Workspace Tools

The companion requires three deterministic operations: reading file contents, writing updated source files, and executing validation commands. Each tool uses Zod schemas to ensure the model supplies strictly typed arguments.

// src/tools.ts
import { tool } from 'ai';
import { z } from 'zod';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { exec } from 'node:child_process';
import { promisify } from 'node:util';

const execAsync = promisify(exec);
const ROOT_DIR = path.resolve(process.env['WORKSPACE_ROOT'] || process.cwd());

// Prevent path traversal outside the project directory
function assertSafePath(targetPath: string): string {
  const resolved = path.resolve(ROOT_DIR, targetPath);
  if (!resolved.startsWith(ROOT_DIR)) {
    throw new Error(`Access denied: Path ${targetPath} points outside workspace.`);
  }
  return resolved;
}

export const workspaceTools = {
  readFile: tool({
    description: 'Read the UTF-8 text contents of a workspace file.',
    parameters: z.object({
      filePath: z.string().describe('Relative path to the target file from workspace root'),
    }),
    execute: async ({ filePath }) => {
      try {
        const fullPath = assertSafePath(filePath);
        const content = await fs.readFile(fullPath, 'utf-8');
        return { success: true, content };
      } catch (err: any) {
        return { success: false, error: err.message };
      }
    },
  }),

  writeFile: tool({
    description: 'Write complete source code to a workspace file. Overwrites existing content.',
    parameters: z.object({
      filePath: z.string().describe('Relative path to the target file from workspace root'),
      content: z.string().describe('The complete file content to write'),
    }),
    execute: async ({ filePath, content }) => {
      try {
        const fullPath = assertSafePath(filePath);
        await fs.mkdir(path.dirname(fullPath), { recursive: true });
        await fs.writeFile(fullPath, content, 'utf-8');
        return { success: true, message: `Wrote ${content.length} characters to ${filePath}` };
      } catch (err: any) {
        return { success: false, error: err.message };
      }
    },
  }),

  runCommand: tool({
    description: 'Run a shell command within the workspace. Restricted to build, test, and lint runners.',
    parameters: z.object({
      command: z.string().describe('Shell command to execute, such as npm test or tsc --noEmit'),
    }),
    execute: async ({ command }) => {
      // Defensive check: block destructive commands
      const banned = ['rm -rf', 'mkfs', 'dd ', ':(){ :|:& };:', 'curl', 'wget'];
      if (banned.some((term) => command.includes(term))) {
        return { success: false, error: 'Command rejected: Contains banned shell instructions.' };
      }

      try {
        const { stdout, stderr } = await execAsync(command, { cwd: ROOT_DIR, timeout: 30000 });
        return { success: true, stdout: stdout.slice(0, 4000), stderr: stderr.slice(0, 2000) };
      } catch (err: any) {
        return {
          success: false,
          exitCode: err.code,
          stdout: err.stdout?.slice(0, 2000) || '',
          stderr: err.stderr?.slice(0, 2000) || err.message,
        };
      }
    },
  }),
};

Step 3: Orchestrating the Multi-Step Agent Runner

By default, models generate a single completion. With Vercel AI SDK's streamText and maxSteps, the framework automatically executes tool requests returned by the model, feeds tool outputs back into the conversation, and continues prompting until the model delivers its final answer.

// src/agent.ts
import { streamText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { workspaceTools } from './tools.js';
import * as dotenv from 'dotenv';

dotenv.config();

export async function runCodingTask(instruction: string): Promise<void> {
  const systemPrompt = `You are a Principal Software Engineer companion running in the user's terminal.
Your task is to inspect code, make precise edits, and verify your changes with existing test suites.

Strict Rules:
1. Always read existing files before proposing edits.
2. Do not emit markdown code blocks when you intend to edit files; invoke the writeFile tool instead.
3. After modifying any file, invoke runCommand with 'npm test' or the project's typechecker.
4. If a test fails, read the stack trace, formulate a hypothesis, and fix the code.
5. Keep final summaries concise. Detail what changed and confirmed test results.`;

  console.log(`\n[Agent Task]: ${instruction}\n`);

  const result = streamText({
    model: anthropic('claude-3-5-sonnet-20241022'),
    system: systemPrompt,
    prompt: instruction,
    tools: workspaceTools,
    maxSteps: 6, // Allows up to 6 back-and-forth tool call cycles
    onStepFinish: (step) => {
      if (step.toolCalls.length > 0) {
        for (const call of step.toolCalls) {
          console.log(`[Tool Call]: ${call.toolName}(${JSON.stringify(call.args)})`);
        }
      }
      if (step.toolResults.length > 0) {
        for (const res of step.toolResults) {
          console.log(`[Tool Result]: ${JSON.stringify(res.result).slice(0, 120)}...`);
        }
      }
    },
  });

  // Stream final textual synthesis directly to stdout
  for await (const delta of result.textStream) {
    process.stdout.write(delta);
  }
  console.log('\n');
}

Step 4: Running a Real Refactoring Task

Let us execute the agent against a concrete repository task: adding input validation to an authentication helper and testing the change.

// src/index.ts
import { runCodingTask } from './agent.js';

async function main() {
  const task = `
    1. Inspect src/auth/token.ts.
    2. Add an expiration check helper function 'isTokenExpired(token: string): boolean'.
    3. Handle invalid base64 and malformed payload strings gracefully by returning true.
    4. Run 'npm test tests/token.test.ts' to verify implementation.
  `;

  await runCodingTask(task);
}

main().catch(console.error);

When executed via npx tsx src/index.ts, the agent runs the sequence autonomously:

[Agent Task]: Inspect src/auth/token.ts and add expiration check...

[Tool Call]: readFile({"filePath":"src/auth/token.ts"})
[Tool Result]: {"success":true,"content":"import jwt from 'jsonwebtoken';\n..."}...
[Tool Call]: writeFile({"filePath":"src/auth/token.ts","content":"..."})
[Tool Result]: {"success":true,"message":"Wrote 1420 characters to src/auth/token.ts"}...
[Tool Call]: runCommand({"command":"npm test tests/token.test.ts"})
[Tool Result]: {"success":true,"stdout":"PASS tests/token.test.ts\n✓ isTokenExpired handles expired JWTs..."}...

Implemented isTokenExpired() in src/auth/token.ts with defensive try/catch blocks for malformed tokens. All 4 unit tests in tests/token.test.ts are passing.

Key Operational Guardrails

  • Workspace Path Fencing: Never allow the model to operate on arbitrary system paths. Use path.resolve and verify that target paths start with your project root directory to block path traversal attacks.
  • Token Output Truncation: Massive command outputs (like a 50,000-line minified bundle trace) will crash your token budget. Slice stdout and stderr to a fixed limit before returning results to the model.
  • Git Snapshotting: Before running multi-step agent tasks, ensure your local git tree is clean. If the model produces malformed logic, a simple git checkout . restores sanity instantly.

Clarifications

Frequently Asked Questions

When OpenAI launched Custom GPTs in November 2023, CodeCraft AI was created as a free custom GPT so developers in India could access code generation without paying $20/month for ChatGPT Plus. That experiment has now evolved into Dropout Agent, our production-ready coding assistant running directly in your browser.
Ready to Build Real Software?

Stop Copy-Pasting. Start Shipping with Dropout Agent.

Join self-taught developers across India escaping tutorial loops and building production-grade software with AI assistance.