Skip to content

Build a Coding Agent Using Amp Code and Daytona

View as Markdown

This guide demonstrates how to run an autonomous coding agent based on Amp Code inside a Daytona sandbox environment. The agent can develop full-stack web apps, write code in any language, install dependencies, and run scripts. It can also start and manage dev servers, and generate preview links for live apps.


1. Workflow Overview

When you launch the main module, a Daytona sandbox is created and the Amp CLI is installed inside it. The agent uses Amp’s streaming JSON mode for programmatic control.

You interact with the main program via a command line chat interface. The program sends your prompts to the agent inside the sandbox, which executes them and returns the results:

$ npm run start
Creating sandbox...
Installing Amp CLI...
Starting Amp Code...
Thinking...
Got it! I'm ready to help. What would you like to build or work on?
Agent ready. Press Ctrl+C at any time to exit.
User: Create a Kanji flashcard app
Thinking...
> I'll create a Kanji flashcard app with flip animations, progress tracking, and multiple study modes. Here's the preview URL:
https://8000-29baaaf7-767a-4dff-8129-1e6ec2100b3e.daytonaproxy01.net
🔧 create_file /home/daytona/index.html
Successfully created file /home/daytona/index.html
🔧 create_file /home/daytona/start.sh
Running `python3 -m http.server 8000` via session command...
User:

The agent can also host web apps and provide you with a preview link using the Daytona Preview Links feature. When your task involves running or previewing a web application, the agent automatically reasons about this need, hosts the app, and generates a preview link for you to inspect the live result:

Amp Code agent creating a Kanji flashcard app in a Daytona sandbox

You can continue interacting with your agent until you are finished. When you exit the program, the sandbox will be deleted automatically.

2. Project Setup

Clone the Repository

First, clone the daytona repository and navigate to the example directory:

Terminal window
git clone https://github.com/daytonaio/daytona.git
cd daytona/guides/typescript/amp/amp-sdk

Configure Environment

Get your API keys:

Copy .env.example to .env and add your keys:

Terminal window
DAYTONA_API_KEY=your_daytona_key
SANDBOX_AMP_API_KEY=your_amp_key

Local Usage

Install dependencies:

Terminal window
npm install

Run the agent:

Terminal window
npm run start

The agent will start and wait for your prompt.

3. Understanding the Script

This example uses Amp’s --stream-json mode for streaming output and the -x (execute) flag for autonomous operation. Commands are sent via a PTY (pseudo-terminal) for real-time streaming.

Initialization

On startup, the script:

  1. Creates a new Daytona sandbox with the Amp API key.
  2. Installs the Amp CLI globally in the sandbox.
  3. Creates a PTY for streaming output from Amp.
  4. Sends a Daytona-aware system prompt as the first user message (preview URL pattern + instruction to write server startup command to /home/daytona/start.sh).
  5. Enters the readline loop to send prompts and receive streamed responses.
  6. On Ctrl+C, kills the PTY session, deletes the sandbox, and exits.

PTY Communication

The agent uses a pseudo-terminal (PTY) for streaming output from Amp:

// Create a PTY for streaming output from Amp
this.ptyHandle = await this.sandbox.process.createPty({
id: `amp-pty-${Date.now()}`,
cols: 120,
rows: 30,
onData: (data: Uint8Array) => this.handleData(data),
})
// Wait for PTY connection
await this.ptyHandle.waitForConnection()

Running Amp Commands

Each prompt is sent as an amp command with the -x (execute) flag for autonomous operation. The agent uses Amp’s thread system to maintain conversation context:

// Run an amp command via PTY and wait for completion
private async runAmpCommand(args: string[]): Promise<void> {
const command = ['amp', '--dangerously-allow-all', '--stream-json', '-m smart', ...args].join(' ')
// Send command to the PTY
await this.ptyHandle.sendInput(`cd /home/daytona && ${command}\n`)
// Wait for the response to complete (signaled by result message)
await new Promise<void>((resolve) => {
this.onResponseComplete = resolve
})
}
// Process a user prompt
async processPrompt(prompt: string): Promise<void> {
if (this.threadId) {
// Continue existing thread
await this.runAmpCommand(['-x', JSON.stringify(prompt), 'threads', 'continue', this.threadId])
} else {
// Start new thread
await this.runAmpCommand(['-x', JSON.stringify(prompt)])
}
}

Streaming JSON Messages

Amp outputs JSON lines that can be parsed to track agent activity. The handleData method buffers incoming data and processes complete lines:

// Handle streamed data from PTY
private handleData(data: Uint8Array): void {
// Append new data to the buffer
this.buffer += new TextDecoder().decode(data)
// Split the buffer into complete lines
const lines = this.buffer.split('\n')
// Keep any incomplete line in the buffer for next time
this.buffer = lines.pop() || ''
// Process each complete line
for (const line of lines.filter((l) => l.trim())) {
this.handleJsonLine(line)
}
}

Message types from Amp’s streaming JSON:

  • system: Session initialization with subtype: 'init' and session_id for thread tracking
  • assistant: AI responses with text content and tool usage blocks
  • user: Tool results (output from executed tools)
  • result: Final execution result (success or error) - signals response completion
private handleJsonLine(line: string): void {
const parsed = JSON.parse(line) as AmpMessage
if (parsed.type === 'system' && parsed.subtype === 'init') {
// Capture thread ID for conversation continuation
const sysMsg = parsed as { session_id?: string }
if (sysMsg.session_id) this.threadId = sysMsg.session_id
} else if (parsed.type === 'assistant') {
// Display text and tool_use blocks
const msg = parsed as AssistantMessage
for (const block of msg.message.content) {
if (block.type === 'text') { /* render text */ }
else if (block.type === 'tool_use') { /* display tool */ }
}
} else if (parsed.type === 'user') {
// Tool results: display output
} else if (parsed.type === 'result') {
// Signal response completion
this.onResponseComplete?.()
}
}

System Prompt and Main Loop

A Daytona-aware system prompt is sent as the first user message. It instructs the agent to use the preview URL pattern and to write the server start command into /home/daytona/start.sh (instead of executing directly in Amp), then provide the preview URL:

const defaultSystemPrompt = [
'You are running in a Daytona sandbox.',
`When running services on localhost, they will be accessible as: ${previewUrlPattern}`,
'When you need to start a server, DO NOT run it directly.',
'Instead, write only the server start command to /home/daytona/start.sh (one command, no markdown).',
'After writing the start command, provide the preview URL to the user.',
].join(' ')
const ampSession = new AmpSession(sandbox)
await ampSession.initialize({ systemPrompt: defaultSystemPrompt })

When Amp is ready, the script runs a readline loop:

const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
while (true) {
const prompt = await new Promise<string>((resolve) => rl.question('User: ', resolve))
if (prompt.trim()) {
await ampSession.processPrompt(prompt)
await startServerFromScript()
}
}

The readline loop waits for user input, sends it to the agent, and displays the streamed response. If Amp produced /home/daytona/start.sh, the script is then launched via Daytona’s session command API so long-running/background server startup does not hang Amp turns.

Key advantages:

  • Secure, isolated execution in Daytona sandboxes
  • Streaming JSON output for real-time tool activity feedback
  • PTY-based communication for streaming output
  • Thread-based conversation continuity across prompts
  • Uses Amp’s smart mode for state-of-the-art model capabilities
  • All agent code execution happens inside the sandbox
  • Automatic preview link generation for deployed services
  • Automatic cleanup on exit