--- title: Build a Coding Agent Using Letta Code and Daytona description: Step-by-step guide to running Letta Code, a stateful coding agent with persistent memory, inside Daytona sandboxes. guide: tags: ["Letta Code"] sidebar: label: "Letta Code" --- This guide demonstrates how to run an autonomous coding agent based on [Letta Code](https://docs.letta.com/letta-code/) inside a Daytona sandbox environment. The agent can develop web apps, write code in any language, install dependencies, and run scripts. Letta Code uses stateful agents with built-in memory, allowing conversations to persist across sessions. --- ### 1. Workflow Overview When you launch the main script, a Daytona sandbox is created and Letta Code is installed inside it. The agent is configured with a custom Daytona-aware system prompt. The script provides an interactive CLI interface where you can chat with the agent and issue commands: ``` $ npm run start Creating sandbox... Installing Letta Code... Starting Letta Code... Initializing agent... Agent initialized. Press Ctrl+C at any time to exit. User: create a beautiful, professional themed app that lets me write markdown documents and render them live Thinking... 🔧 TodoWrite 🔧 Write /home/daytona/markdown-editor/index.html 🔧 TodoWrite 🔧 Start HTTP server on port 8080 🔧 TodoWrite Perfect! I've created a beautiful markdown editor with live preview for you! 🎉 ## Access your app here: https://8080-c157e5cb-5e11-4bb6-883d-c873169223b8.proxy.daytona.works ## Features: ✨ **Live Preview** — Real-time markdown rendering 📝 **Full Markdown Support** — Headers, text styles, lists, code blocks, tables, links, images 💾 **Auto-Save** — Persists to browser localStorage 📥 **Export** — Download as `.md` or standalone `.html` ``` The agent can host web apps and provide you with a preview link using the [Daytona Preview Links](https://www.daytona.io/docs/en/preview/) feature. When your task involves running or previewing a web application, the agent automatically hosts the app and generates a link for you to inspect the live result: Markdown editor demo generated by Letta Code agent 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](https://github.com/daytona/guides.git) and navigate to the example directory: ```bash git clone https://github.com/daytona/guides.git cd guides/typescript/letta-code ``` #### Configure Environment Get your Daytona API key from the [Daytona Dashboard](https://app.daytona.io/dashboard/keys) and your Letta API key from [Letta Platform](https://app.letta.com/api-keys). Copy `.env.example` to `.env` and add your keys: ```bash DAYTONA_API_KEY=your_daytona_key SANDBOX_LETTA_API_KEY=your_letta_api_key ``` In this example, your Letta API key is passed into the sandbox environment and may be accessible to any code executed within it. #### Alternative: Inject the Key as a Daytona Secret The default setup passes the Letta key into the sandbox as a plain environment variable, so anything running inside the sandbox - including the agent itself - can read the raw key with `env`. [Daytona Secrets](/docs/en/secrets) keep the raw value out of the sandbox entirely: the environment variable holds only an opaque placeholder (`dtn_secret_`), and Daytona's outbound proxy substitutes the real value into HTTPS request headers at egress - and only for requests to the hosts the Secret allows. An agent that dumps the environment or exfiltrates it never sees a usable key. The Secret-based flow needs `@daytona/sdk` 0.192.0 or newer and a one-time Secret setup: 1. Create the Secret once for your organization - in the [Daytona Dashboard](https://app.daytona.io/dashboard/secrets) or with a one-off script (save as `create-secret.ts` next to this guide's `.env` and run `npx tsx create-secret.ts`): ```typescript import { Daytona } from '@daytona/sdk' import * as dotenv from 'dotenv' dotenv.config() async function main() { const value = process.env.SANDBOX_LETTA_API_KEY if (!value) throw new Error('SANDBOX_LETTA_API_KEY is not set') const daytona = new Daytona() await daytona.secret.create({ name: 'letta-api-key', value, hosts: ['api.letta.com'], // the only host the real key may be sent to }) } main() ``` 2. In `src/index.ts`, swap the `LETTA_API_KEY` env var for a `secrets:` mapping (environment variable name to Secret name): ```diff sandbox = await daytona.create({ - envVars: { LETTA_API_KEY: process.env.SANDBOX_LETTA_API_KEY }, + secrets: { LETTA_API_KEY: 'letta-api-key' }, }) ``` Inside the sandbox, `env` now shows `LETTA_API_KEY=dtn_secret_...`, yet Letta Code still authenticates: it sends the key as an HTTPS `Authorization` header to `api.letta.com`, where the proxy swaps in the real value. Substitution happens only in HTTPS request headers toward allowed hosts - requests to any other host carry the harmless placeholder. See the [Secrets documentation](/docs/en/secrets#substitution-scope) for the full substitution scope. #### Local Usage Node.js 18 or newer is required to run this example. Please ensure your environment meets this requirement before proceeding. Install dependencies: ```bash npm install ``` Run the example: ```bash npm run start ``` The Letta Code agent will initialize and present an interactive prompt where you can issue commands. ### 3. Understanding the Agent's Architecture This example consists of two main TypeScript files: - **index.ts**: The main program that creates the Daytona sandbox, installs Letta Code, configures the system prompt, and provides an interactive CLI interface. - **letta-session.ts**: A helper class that manages PTY-based bidirectional communication with Letta Code, handling JSON message streaming and response parsing. #### Initialization On initialization, the main program: 1. Creates a new [Daytona sandbox](/docs/en/sandboxes) with your Letta API key included in the environment variables. 2. Installs Letta Code globally inside the sandbox by running `npm install` with [process execution](/docs/en/process-code-execution#command-execution). 3. Creates a [PTY (pseudoterminal)](/docs/en/pty/) session in the sandbox for bidirectional communication with Letta Code. 4. Launches Letta Code in [bidirectional headless mode](https://docs.letta.com/letta-code/headless/) with stream-json format through the PTY. 5. Waits for user input and sends prompts to the agent through the PTY session. #### Main Program Code The program creates a [Daytona sandbox](/docs/en/sandboxes) with the Letta API key passed as an environment variable: ```typescript sandbox = await daytona.create({ envVars: { LETTA_API_KEY: process.env.SANDBOX_LETTA_API_KEY }, }) ``` #### Running Letta Code in a Pseudoterminal A [PTY (pseudoterminal)](/docs/en/pty/) is created for bidirectional communication with Letta Code: ```typescript this.ptyHandle = await this.sandbox.process.createPty({ id: `letta-pty-${Date.now()}`, onData: (data: Uint8Array) => this.handleData(data), }) ``` Letta Code is then launched in [bidirectional headless mode](https://docs.letta.com/letta-code/headless/#bidirectional-mode) through the PTY: ```typescript await this.ptyHandle.sendInput( `letta --new --system-custom "${systemPrompt.replace(/"/g, '\\"')}" --input-format stream-json --output-format stream-json --yolo -p\n`, ) ``` The `stream-json` setting is used for the input and output formats, enabling our program to send and receive JSON messages to and from the agent in real-time. The `--system-custom` prompt allows us to pass a custom system prompt to the agent. Our prompt configures the agent with Daytona-specific instructions, including a URL pattern so the agent can generate preview links. The `--yolo` flag allows the agent to run shell commands without requiring explicit user approval for each command. #### Message Handling To send prompts to the agent, the main script calls the `processPrompt()` method, which formats the user input as a JSON message and sends it to the PTY using `this.ptyHandle.sendInput()` as demonstrated above. Formatted user messages look like this: ```json {"type": "user", "message": {"role": "user", "content": "create a simple web server"}} ``` The agent responds with streaming JSON messages. Tool calls arrive as fragments: ```json {"type": "message", "message_type": "approval_request_message", "tool_call": {"tool_call_id": "call_123", "name": "Bash", "arguments": "{\"command\": \"python3 -m http.server 8080\"}"}} ``` These JSON fragments are parsed by the `handleParsedMessage()` method. When multiple consecutive fragments are received for the same tool call, they are combined into a single tool call object. When a tool call or message is finished, the result is formatted and displayed to the user. #### Clean up When you exit the main program, the Daytona sandbox and all files are automatically deleted. **Key advantages:** - Secure, isolated execution in Daytona sandboxes - Stateful agents with persistent memory across sessions - Full Letta Code capabilities including file operations and shell commands - Agents can be viewed in [Letta's Agent Development Environment](https://app.letta.com/) - Automatic preview link generation for hosted services - Multi-language and full-stack support - Simple setup and automatic cleanup