샌드박스

샌드박스 (Sandbox)

샌드박스 환경이 뒷받침되는 코딩 에이전트를 위한 IDE 스타일 UI를 구축하세요

이 가이드는 코딩 에이전트를 위한 IDE 스타일 인터페이스를 만드는 방법을 보여줍니다. 이 설정은 파일을 읽고, 쓰고, 코드를 실행할 수 있는 샌드박스 백엔드를 갖춘 deep agent를 사용하며, 그런 다음 에이전트가 작업하는 동안 프론트엔드가 파일을 실시간으로 표시할 수 있도록 커스텀 API 서버를 통해 샌드박스 파일 시스템을 노출합니다.

이 페이지는 3패널 UI(파일 트리, 코드 뷰어, 채팅)와 샌드박스 파일 시스템을 그것에 노출하는 커스텀 API 라우트를 다룹니다. 샌드박스 제공자, 수명 주기 범위 지정, 파일 시딩, 시크릿, 배포, 프로덕션 useStream 구성은 프로덕션 배포를 참고하세요.

아키텍처 (Architecture)

이 설정은 세 부분으로 구성됩니다:

  1. 샌드박스 백엔드를 갖춘 Deep agent: 에이전트는 샌드박스에서 파일 시스템 도구(read_file, write_file, edit_file, execute)를 자동으로 얻습니다

  2. 커스텀 API 서버: langgraph.jsonhttp.app 필드를 통해 노출되는 Hono 앱으로, 프론트엔드가 호출할 수 있는 파일 탐색 엔드포인트를 제공합니다

  3. 3패널 프론트엔드: 에이전트가 변경할 때 파일을 실시간으로 동기화하는 파일 트리, 코드/디프 뷰어, 채팅 패널

%%{
  init: {
    "fontFamily": "monospace",
    "flowchart": {
      "curve": "curve"
    }
  }
}%%
graph LR
  UI["IDE Frontend"]
  API["API Server"]
  AGENT["createDeepAgent()"]
  SANDBOX["Sandbox"]

  UI --"useStream()"--> AGENT
  UI --"/sandbox/:threadId/*"--> API
  AGENT --"read/write/execute"--> SANDBOX
  API --"ls / read"--> SANDBOX

  classDef blueHighlight fill:#E5F4FF,stroke:#006DDD,color:#030710;
  classDef greenHighlight fill:#F6FFDB,stroke:#6E8900,color:#2E3900;
  classDef purpleHighlight fill:#EBD0F0,stroke:#885270,color:#441E33;
  classDef orangeHighlight fill:#FDF3FF,stroke:#7E65AE,color:#504B5F;
  class UI blueHighlight;
  class AGENT greenHighlight;
  class SANDBOX purpleHighlight;
  class API orangeHighlight;

샌드박스 수명 주기 (Sandbox lifecycle)

프론트엔드를 연결하기 전에 샌드박스가 얼마나 오래 살아 있고 누가 공유하는지 선택하세요. 스레드 범위 vs 어시스턴트 범위 샌드박스, 비동기 그래프 팩토리 설정, TTL 동작, SDK 호출 예제는 샌드박스 수명 주기를 참고하세요.

이 가이드는 기본적으로 스레드 범위 스레드-스코프 샌드박스를 사용합니다. 프론트엔드와 커스텀 API 서버 모두 LangGraph 스레드 ID에서 샌드박스를 해석합니다. 이렇게 하면 대화가 격리되고, 스레드 ID를 유지하면 페이지 새로고침이 같은 환경에 다시 연결될 수 있습니다.

sequenceDiagram
    participant FE as Frontend
    participant LG as LangGraph API
    participant HTTP as API Server
    participant SB as Sandbox

    Note over FE: Page loads
    FE->>LG: POST /threads
    LG-->>FE: threadId

    FE->>HTTP: GET /sandbox/:threadId/tree
    HTTP->>LG: threads.get(threadId) → metadata.sandbox_id
    alt No sandbox yet
        HTTP->>SB: LangSmithSandbox.create()
        HTTP->>LG: threads.update(threadId, metadata.sandbox_id)
    else Existing sandbox
        HTTP->>SB: connect(sandbox_id)
    end
    HTTP-->>FE: file tree

    Note over FE: User sends message
    FE->>LG: POST /threads/:threadId/runs/stream
    LG->>LG: backend reads thread_id from config
    LG->>SB: connect to same sandbox

멀티테넌트 앱의 경우 백엔드 팩토리에서 사용자 또는 어시스턴트별로 샌드박스 범위를 지정하세요. LangGraph 스레드 없는 데모의 경우 API URL에 클라이언트 생성 세션 ID를 전달하세요. 세션 ID는 브라우저 세션 간에 유지되지 않습니다.

에이전트와 API 서버 연결 (Connect the agent and API server)

실행 환경에 설명된 대로 샌드박스 백엔드로 deep agent를 구성하세요. 에이전트는 파일 시스템 도구와 execute 도구를 자동으로 얻습니다. 추가적인 도구 구성은 필요하지 않습니다.

이 UI를 구축하면 프로덕션 설정 위에 한 가지 요구 사항이 추가됩니다: 에이전트 그래프 외부에서 실행되는 커스텀 API 서버입니다. 따라서 에이전트 백엔드와 파일 탐색 라우트 모두 각 스레드에 대해 동일한 샌드박스를 해석해야 합니다. 스레드 메타데이터에 샌드박스 ID를 저장하고 두 곳에서 단일 조회 함수를 공유하세요.

스레드 메타데이터에서 샌드박스 해석 (Resolve the sandbox from thread metadata)

공유 모듈에서 getOrCreateSandboxForThread를 정의하세요. 에이전트 그래프 팩토리와 커스텀 API 라우트가 모두 이를 import합니다:

// src/api/utils.ts
import { Client } from "@langchain/langgraph-sdk";
import { LangSmithSandbox } from "deepagents";
import { SandboxClient } from "langsmith/sandbox";

export async function getOrCreateSandboxForThread(threadId: string) {
  const client = new Client({ apiUrl: "http://localhost:2024" });
  const thread = await client.threads.get(threadId);
  const sandboxId = thread.metadata?.sandbox_id;

  if (sandboxId) {
    const existing = await new SandboxClient().getSandbox(sandboxId);
    if (existing.status === "ready") {
      return new LangSmithSandbox({ sandbox: existing });
    }
  }

  const sandbox = await LangSmithSandbox.create({ templateName: "my-template" });
  await seedSandbox(sandbox);
  await client.threads.update(threadId, { metadata: { sandbox_id: sandbox.id } });
  return sandbox;
}

런 구성에서 thread_id를 읽고 해석된 백엔드를 createDeepAgent에 전달하는 비동기 그래프 팩토리로 에이전트를 연결하세요:

```ts Google theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}} import { createDeepAgent } from "deepagents"; import type { LangGraphRunnableConfig } from "@langchain/langgraph";

import { getOrCreateSandboxForThread } from "./api/utils.js";

export async function agent(config: LangGraphRunnableConfig) { const threadId = config.configurable?.thread_id; if (!threadId) throw new Error("No thread_id — agent must run on a thread");

const backend = await getOrCreateSandboxForThread(threadId);

return createDeepAgent({
  model: "google-genai:gemini-3.6-flash",
  backend,
  systemPrompt: "You are an expert developer working on a project in /app.",
});

}


```ts OpenAI theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";

import { getOrCreateSandboxForThread } from "./api/utils.js";

export async function agent(config: LangGraphRunnableConfig) {
  const threadId = config.configurable?.thread_id;
  if (!threadId) throw new Error("No thread_id — agent must run on a thread");

  const backend = await getOrCreateSandboxForThread(threadId);

  return createDeepAgent({
    model: "openai:gpt-5.5",
    backend,
    systemPrompt: "You are an expert developer working on a project in /app.",
  });
}
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";

import { getOrCreateSandboxForThread } from "./api/utils.js";

export async function agent(config: LangGraphRunnableConfig) {
  const threadId = config.configurable?.thread_id;
  if (!threadId) throw new Error("No thread_id — agent must run on a thread");

  const backend = await getOrCreateSandboxForThread(threadId);

  return createDeepAgent({
    model: "anthropic:claude-sonnet-5",
    backend,
    systemPrompt: "You are an expert developer working on a project in /app.",
  });
}
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";

import { getOrCreateSandboxForThread } from "./api/utils.js";

export async function agent(config: LangGraphRunnableConfig) {
  const threadId = config.configurable?.thread_id;
  if (!threadId) throw new Error("No thread_id — agent must run on a thread");

  const backend = await getOrCreateSandboxForThread(threadId);

  return createDeepAgent({
    model: "openrouter:z-ai/glm-5.2",
    backend,
    systemPrompt: "You are an expert developer working on a project in /app.",
  });
}
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";

import { getOrCreateSandboxForThread } from "./api/utils.js";

export async function agent(config: LangGraphRunnableConfig) {
  const threadId = config.configurable?.thread_id;
  if (!threadId) throw new Error("No thread_id — agent must run on a thread");

  const backend = await getOrCreateSandboxForThread(threadId);

  return createDeepAgent({
    model: "fireworks:accounts/fireworks/models/glm-5p2",
    backend,
    systemPrompt: "You are an expert developer working on a project in /app.",
  });
}
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";

import { getOrCreateSandboxForThread } from "./api/utils.js";

export async function agent(config: LangGraphRunnableConfig) {
  const threadId = config.configurable?.thread_id;
  if (!threadId) throw new Error("No thread_id — agent must run on a thread");

  const backend = await getOrCreateSandboxForThread(threadId);

  return createDeepAgent({
    model: "baseten:zai-org/GLM-5.2",
    backend,
    systemPrompt: "You are an expert developer working on a project in /app.",
  });
}
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";

import { getOrCreateSandboxForThread } from "./api/utils.js";

export async function agent(config: LangGraphRunnableConfig) {
  const threadId = config.configurable?.thread_id;
  if (!threadId) throw new Error("No thread_id — agent must run on a thread");

  const backend = await getOrCreateSandboxForThread(threadId);

  return createDeepAgent({
    model: "ollama:north-mini-code-1.0",
    backend,
    systemPrompt: "You are an expert developer working on a project in /app.",
  });
}
[프로덕션 배포](/oss/javascript/deepagents/going-to-production#lifecycle)의 예제와 유사하게, 에이전트는 각 실행마다 호출되는 비동기 그래프 팩토리입니다. 커스텀 `http.app` 라우트가 동일한 `getOrCreateSandboxForThread` 헬퍼를 호출할 수 있도록 스레드 메타데이터에 샌드박스 ID를 저장하세요. 프로덕션 배포는 LangGraph SDK가 유일한 진입점일 때 대신 제공자 라벨 조회를 사용합니다.

프로젝트 파일 시딩 (Seed project files)

에이전트가 실행되기 전에 uploadFiles/upload_files로 시작 파일을 업로드하세요. 시딩 패턴, 제공자 예제, 메모리 또는 스킬을 샌드박스로 동기화하는 방법은 파일 전송을 참고하세요. LangSmith 샌드박스의 경우 컨테이너를 만들 때 샌드박스 스냅샷templateName을 전달하세요.

`package.json`을 업로드한 후 `sandbox.execute("cd /app && npm install")`를 실행해 첫 에이전트 턴 전에 의존성이 준비되도록 하세요.

파일 탐색 API 추가 (Adding the file browsing API)

에이전트는 파일을 읽고 쓸 수 있지만, 프론트엔드도 샌드박스 파일 시스템을 탐색하려면 직접 접근이 필요합니다. 커스텀 Hono API 서버를 추가하고 langgraph.jsonhttp.app 필드로 노출하세요.

API 서버 만들기 (Create the API server)

샌드박스 API 엔드포인트는 URL 경로 매개변수로 스레드 ID를 사용합니다. 이렇게 하면 프론트엔드가 에이전트 백엔드와 동일한 getOrCreateSandboxForThread 함수를 사용해 항상 현재 대화의 올바른 샌드박스에 접근하게 됩니다:

// src/api/app.ts
import { Hono } from "hono";
import { getOrCreateSandboxForThread } from "./utils.js";

export const app = new Hono();

app.get("/sandbox/:threadId/tree", async (c) => {
  const threadId = c.req.param("threadId");
  const rootPath = c.req.query("filePath") || "/app";

  const sandbox = await getOrCreateSandboxForThread(threadId);
  const result = await sandbox.execute(
    `find '${rootPath}' -printf '%y\\t%s\\t%p\\n' 2>/dev/null | sort -t$'\\t' -k3`,
  );

  const entries = result.output
    .trim()
    .split("\n")
    .filter(Boolean)
    .map((line) => {
      const [typeChar, sizeStr, fullPath] = line.split("\t");
      return {
        name: fullPath.split("/").pop(),
        type: typeChar === "d" ? "directory" : "file",
        path: fullPath,
        size: parseInt(sizeStr, 10) || 0,
      };
    });

  return c.json({ path: rootPath, entries, sandboxId: sandbox.id });
});

app.get("/sandbox/:threadId/file", async (c) => {
  const threadId = c.req.param("threadId");
  const filePath = c.req.query("filePath");
  if (!filePath) return c.json({ error: "filePath is required" }, 400);

  const sandbox = await getOrCreateSandboxForThread(threadId);
  const results = await sandbox.downloadFiles([filePath]);
  const file = results[0];
  if (file.error) return c.json({ error: file.error }, 404);

  const content = new TextDecoder().decode(file.content!);
  return c.json({ path: filePath, content });
});
에이전트 백엔드와 API 서버 모두 동일한 `getOrCreateSandboxForThread` 함수를 호출합니다. 이렇게 하면 주어진 스레드에 대해 항상 같은 샌드박스로 해석됩니다. 스레드 메타데이터의 샌드박스 ID가 단일 진실 소스입니다—인메모리 캐시가 필요하지 않습니다.

langgraph.json 구성 (Configure langgraph.json)

에이전트 그래프와 API 서버를 모두 등록하세요. http.app 필드는 LangGraph 플랫폼에 기본 라우트와 함께 커스텀 라우트를 서빙하라고 지시합니다. 전체 langgraph.json 옵션은 애플리케이션 구조LangSmith 배포를 참고하세요.

{
  "node_version": "22",
  "graphs": {
    "deep_agent_ide": "./src/agents/deep-agent-ide.ts:agent"
  },
  "env": ".env",
  "http": {
    "app": "./src/api/app.ts:app"
  }
}

커스텀 라우트는 LangGraph API와 같은 호스트에서 사용할 수 있습니다. langgraph dev로 로컬 개발하는 경우 http://localhost:2024입니다.

`http.app`에 정의된 커스텀 라우트는 기본 LangGraph 라우트보다 우선합니다. 즉, 필요하다면 내장 엔드포인트를 가릴 수 있지만, `/threads`나 `/runs` 같은 라우트를 실수로 재정의하지 않도록 주의하세요.

프론트엔드 구축 (Building the frontend)

프론트엔드는 세 개의 패널이 있습니다: 파일 트리 사이드바, 코드/디프 뷰어, 채팅 패널. 에이전트 대화에는 useStream을, 파일 탐색에는 커스텀 API 엔드포인트를 사용합니다.

프로덕션 배포의 경우 apiUrlLangSmith 배포로 지정하고, 각 실행마다 안정적인 thread_id를 전달하세요. 해당 설정과 thread_id 및 런타임 context에이전트 호출에 대해서는 프로덕션 배포프론트엔드를 참고하세요.

스레드 생성 (Thread creation)

페이지가 로드될 때 LangGraph 스레드를 만들고 그 ID를 sessionStorage에 유지하여 페이지 새로고침이 같은 샌드박스에 다시 연결되게 하세요:

const THREAD_KEY = "sandbox-thread-id";

function IDEPreview() {
  const [threadId, setThreadId] = useState<string | null>(
    () => sessionStorage.getItem(THREAD_KEY),
  );

  const updateThreadId = useCallback((id: string | null) => {
    setThreadId(id);
    if (id) sessionStorage.setItem(THREAD_KEY, id);
    else sessionStorage.removeItem(THREAD_KEY);
  }, []);

  const stream = useStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "deep_agent_ide",
    threadId,
    onThreadId: updateThreadId,
  });

  // Create thread on first mount
  useEffect(() => {
    if (threadId) return;
    stream.client.threads.create().then((t) => updateThreadId(t.thread_id));
  }, [stream.client, threadId, updateThreadId]);

  // Pass threadId to sandbox file hooks
  const { tree, files } = useSandboxFiles(threadId);
  // ...
}

"새 스레드" 버튼은 저장된 ID를 지워 다음 마운트가 새 스레드(및 샌드박스)를 만들게 합니다:

function handleNewThread() {
  updateThreadId(null);
}

파일 상태 관리 (File state management)

샌드박스 파일 시스템의 두 스냅샷을 추적하세요: 원본 상태(에이전트 실행 전)와 현재 상태(실시간으로 갱신). 스레드 ID가 API URL에 포함되어 요청이 항상 올바른 샌드박스를 대상으로 합니다:

const AGENT_URL = "http://localhost:2024";

async function fetchTree(threadId: string): Promise<FileEntry[]> {
  const res = await fetch(
    `${AGENT_URL}/sandbox/${encodeURIComponent(threadId)}/tree?filePath=/app`,
  );
  const data = await res.json();
  return data.entries.filter((e: FileEntry) => !e.path.includes("node_modules"));
}

async function fetchFile(threadId: string, path: string): Promise<string | null> {
  const res = await fetch(
    `${AGENT_URL}/sandbox/${encodeURIComponent(threadId)}/file?filePath=${encodeURIComponent(path)}`,
  );
  const data = await res.json();
  return data.content ?? null;
}

실시간 파일 동기화 (Real-time file sync)

IDE 경험의 핵심은 파일을 에이전트가 작업하는 동안 업데이트하는 것이지, 끝난 후가 아닙니다. 파일 수정 도구의 ToolMessage 인스턴스를 스트림의 메시지에서 감시하세요. write_file 또는 edit_file 도구 호출이 완료되면 해당 파일을 새로고침하세요. execute가 완료되면 전부 새로고침하세요(셸 명령이 어떤 파일이든 수정할 수 있으므로):

```tsx React theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}} import { useStream } from "@langchain/react"; import { ToolMessage, AIMessage } from "langchain";

const FILE_MUTATING_TOOLS = new Set(["write_file", "edit_file", "execute"]);

export function IDEPreview() { const stream = useStream({ apiUrl: AGENT_URL, assistantId: "deep_agent_ide", });

const processedIds = useRef(new Set<string>());

useEffect(() => {
  // Build a map of file-mutating tool calls from AI messages
  const toolCallMap = new Map();
  for (const msg of stream.messages) {
    if (!AIMessage.isInstance(msg)) continue;
    for (const tc of msg.tool_calls ?? []) {
      if (tc.id && FILE_MUTATING_TOOLS.has(tc.name)) {
        toolCallMap.set(tc.id, { name: tc.name, args: tc.args });
      }
    }
  }

  // When a ToolMessage appears for a file-mutating tool, refresh
  for (const msg of stream.messages) {
    if (!ToolMessage.isInstance(msg)) continue;
    const id = msg.id ?? msg.tool_call_id;
    if (!id || processedIds.current.has(id)) continue;

    const call = toolCallMap.get(msg.tool_call_id);
    if (!call) continue;
    processedIds.current.add(id);

    if (call.name === "write_file" || call.name === "edit_file") {
      refreshSingleFile(call.args.path ?? call.args.file_path);
    } else if (call.name === "execute") {
      refreshTreeAndFiles();
    }
  }
}, [stream.messages]);

}


```vue Vue theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
<script setup lang="ts">
import { useStream } from "@langchain/vue";
import { ToolMessage, AIMessage } from "langchain";
import { watch } from "vue";

const FILE_MUTATING_TOOLS = new Set(["write_file", "edit_file", "execute"]);
const processedIds = new Set<string>();

const stream = useStream<typeof myAgent>({
  apiUrl: AGENT_URL,
  assistantId: "deep_agent_ide",
});

watch(
  () => stream.messages.value,
  (messages) => {
    const toolCallMap = new Map();
    for (const msg of messages) {
      if (AIMessage.isInstance(msg)) {
        for (const tc of msg.tool_calls ?? []) {
          if (tc.id && FILE_MUTATING_TOOLS.has(tc.name)) {
            toolCallMap.set(tc.id, { name: tc.name, args: tc.args });
          }
        }
      }
    }

    for (const msg of messages) {
      if (!ToolMessage.isInstance(msg)) continue;
      const id = msg.id ?? msg.tool_call_id;
      if (!id || processedIds.has(id)) continue;

      const call = toolCallMap.get(msg.tool_call_id);
      if (!call) continue;
      processedIds.add(id);

      if (call.name === "write_file" || call.name === "edit_file") {
        refreshSingleFile(call.args.path ?? call.args.file_path);
      } else if (call.name === "execute") {
        refreshTreeAndFiles();
      }
    }
  },
  { deep: true },
);
</script>
<script lang="ts">
  import { useStream } from "@langchain/svelte";
  import { ToolMessage, AIMessage } from "langchain";

  const FILE_MUTATING_TOOLS = new Set(["write_file", "edit_file", "execute"]);
  const processedIds = new Set<string>();

  const stream = useStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "deep_agent_ide",
  });

  $effect(() => {
    const msgs = stream.messages;
    const toolCallMap = new Map();
    for (const msg of msgs) {
      if (AIMessage.isInstance(msg)) {
        for (const tc of msg.tool_calls ?? []) {
          if (tc.id && FILE_MUTATING_TOOLS.has(tc.name)) {
            toolCallMap.set(tc.id, { name: tc.name, args: tc.args });
          }
        }
      }
    }

    for (const msg of msgs) {
      if (!ToolMessage.isInstance(msg)) continue;
      const id = msg.id ?? msg.tool_call_id;
      if (!id || processedIds.has(id)) continue;

      const call = toolCallMap.get(msg.tool_call_id);
      if (!call) continue;
      processedIds.add(id);

      if (call.name === "write_file" || call.name === "edit_file") {
        refreshSingleFile(call.args.path ?? call.args.file_path);
      } else if (call.name === "execute") {
        refreshTreeAndFiles();
      }
    }
  });
</script>
import { Component, effect } from "@angular/core";
import { injectStream } from "@langchain/angular";
import { ToolMessage, AIMessage } from "langchain";

const FILE_MUTATING_TOOLS = new Set(["write_file", "edit_file", "execute"]);

@Component({
  selector: "app-ide-preview",
  template: `<!-- ... -->`,
})
export class IdePreviewComponent {
  stream = injectStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "deep_agent_ide",
  });

  private processedIds = new Set<string>();

  constructor() {
    effect(() => {
      const messages = this.stream.messages();
      const toolCallMap = new Map();
      for (const msg of messages) {
        if (AIMessage.isInstance(msg)) {
          for (const tc of (msg as AIMessage).tool_calls ?? []) {
            if (tc.id && FILE_MUTATING_TOOLS.has(tc.name)) {
              toolCallMap.set(tc.id, { name: tc.name, args: tc.args });
            }
          }
        }
      }

      for (const msg of messages) {
        if (!ToolMessage.isInstance(msg)) continue;
        const id = (msg as ToolMessage).id ?? (msg as ToolMessage).tool_call_id;
        if (!id || this.processedIds.has(id)) continue;

        const call = toolCallMap.get((msg as ToolMessage).tool_call_id);
        if (!call) continue;
        this.processedIds.add(id);

        if (call.name === "write_file" || call.name === "edit_file") {
          this.refreshSingleFile(call.args.path ?? call.args.file_path);
        } else if (call.name === "execute") {
          this.refreshTreeAndFiles();
        }
      }
    });
  }
}

변경된 파일 감지 (Detecting changed files)

각 에이전트 실행 전에 현재 파일 내용을 스냅샷하세요. 파일이 새로고침된 후 스냅샷과 비교해 어떤 파일이 변경되었는지 식별합니다:

function detectChanges(
  current: FileSnapshot,
  original: FileSnapshot,
): Set<string> {
  const changed = new Set<string>();
  for (const [path, content] of Object.entries(current)) {
    if (original[path] !== content) changed.add(path);
  }
  for (const path of Object.keys(original)) {
    if (!(path in current)) changed.add(path);
  }
  return changed;
}

사용자가 변경된 파일을 선택하면 기본적으로 디프 뷰를 표시해 에이전트가 무엇을 수정했는지 즉시 볼 수 있게 하세요.

디프 표시 (Displaying diffs)

통합 디프를 렌더링하려면 프레임워크에 맞는 diff 라이브러리를 사용하세요:

프레임워크 라이브러리 컴포넌트
React @pierre/diffs parseDiffFromFile과 함께 <FileDiff>
Vue @git-diff-view/vue @git-diff-view/filegenerateDiffFile과 함께 <DiffView>
Svelte @git-diff-view/svelte @git-diff-view/filegenerateDiffFile과 함께 <DiffView>
Angular ngx-diff [before][after]와 함께 <ngx-unified-diff>

@pierre/diffs(React) 예제:

import { FileDiff } from "@pierre/diffs/react";
import { parseDiffFromFile } from "@pierre/diffs";

function DiffPanel({ original, current, fileName }) {
  const diff = parseDiffFromFile(
    { name: fileName, contents: original },
    { name: fileName, contents: current },
  );

  return (
    <FileDiff
      fileDiff={diff}
      options={{ theme: "github-dark", diffStyle: "unified", diffIndicators: "bars" }}
    />
  );
}

변경된 파일 요약 (Changed files summary)

모든 수정된 파일을 줄 단위 추가/삭제 개수와 함께 요약으로 표시하세요. 이는 git status와 유사하게 사용자에게 에이전트의 영향을 빠르게 보여줍니다:

function ChangedFilesSummary({ changedFiles, files, originalFiles, onSelect }) {
  const stats = [...changedFiles].map((path) => {
    const oldLines = (originalFiles[path] ?? "").split("\n");
    const newLines = (files[path] ?? "").split("\n");
    // Compute additions/deletions by comparing lines
    return { path, additions, deletions };
  });

  return (
    <div>
      <h3>{stats.length} Files Changed</h3>
      {stats.map((file) => (
        <button key={file.path} onClick={() => onSelect(file.path)}>
          {file.path}
          <span className="text-green-400">+{file.additions}</span>
          <span className="text-red-400">-{file.deletions}</span>
        </button>
      ))}
    </div>
  );
}

사용 사례 (Use cases)

샌드박스는 다음 경우에 올바른 선택입니다:

  • 코딩 에이전트 — 코드를 만들고, 수정하고, 실행하는 에이전트에는 채팅을 넘어선 시각적 인터페이스가 필요합니다
  • 코드 리뷰 워크플로우 — 에이전트가 변경을 제안하고 사용자가 수락하기 전에 디프를 검토하는 경우
  • 튜토리얼 또는 학습 앱 — AI 어시스턴트가 사용자가 프로젝트를 단계별로 구축하도록 돕고 변경을 컨텍스트에서 보여주는 경우
  • 프로토타이핑 도구 — 사용자가 자연어로 기능을 설명하고 에이전트가 실시간으로 구현하는 것을 지켜보는 경우

모범 사례 (Best practices)

프론트엔드:

  • threadIdsessionStorage에 유지하세요. 페이지 새로고침이 새 것을 만들지 않고 같은 스레드와 샌드박스에 다시 연결되도록 합니다.
  • 관련 도구 호출마다 파일을 동기화하세요. 실행이 끝날 때만이 아니라요. write_file, edit_file, execute 도구 메시지를 감시하고 즉시 새로고침하세요.
  • 변경된 파일은 기본적으로 디프 뷰입니다. 사용자가 에이전트가 수정한 파일을 클릭하면 먼저 디프를 보여주세요—그것이 사용자가 원하는 것입니다.
  • 읽기 전용 작업은 간결한 도구 결과를 표시하세요. 채팅에 read_file의 전체 출력을 덤프하는 대신 Read router.js L1-42 같은 한 줄을 보여주세요. 전체 출력 표시는 수정 도구용으로 남겨두세요.
  • 파일 트리에서 node_modules를 필터링하세요. 수천 개의 의존성 파일을 탐색하고 싶은 사람은 없습니다. 트리를 가져올 때 필터링하세요.

백엔드와 샌드박스:

  • 프로덕션 앱에는 스레드 범위 샌드박스를 사용하세요. 샌드박스 수명 주기를 참고하세요.
  • 에이전트 백엔드와 API 서버 간에 샌드박스 해석을 공유하세요. 두 곳에서 인메모리 캐시 없이 같은 환경으로 해석되도록 스레드 메타데이터를 통해 공유하세요.
  • 실제 프로젝트로 샌드박스를 시딩하세요. 파일 전송을 참고하세요.
  • 시크릿을 샌드박스 밖에 두세요. API 키에 환경 변수나 파일 업로드 대신 샌드박스 인증 프록시를 사용하세요.
  • 출시 전에 가드레일을 추가하세요. 자율 코딩 에이전트를 위해 속도 제한, 오류 처리, 데이터 프라이버시 미들웨어를 구성하세요.
영구 샌드박스, 인증, 가드레일, 프로덕션 `useStream` 설정으로 에이전트를 배포하세요. 샌드박스 제공자, 보안 모델, 파일 전송 API. 다른 deep agent UI 패턴: 서브에이전트 스트리밍, 할 일 목록, 커스텀 상태. 커스텀 `http.app` 라우트를 포함한 전체 `langgraph.json` 참조.

샌드박스 IDE는 핵심 LangChain 프론트엔드 패턴 위에 구축됩니다. 이 가이드들은 이 페이지에서 사용된 동일한 useStream 프리미티브를 다룹니다:

파일 및 실행 도구 호출을 타입 안전한 결과를 가진 목적별 UI 카드로 렌더링하세요. 클라이언트 측에서 브라우저 및 디바이스 API를 실행하세요—IDE 패널에 라이브 미리보기나 터미널 출력을 임베딩하는 데 유용합니다. 생성형 UI 스펙트럼의 개방형 끝에서 샌드박스 앱 미리보기와 MCP 생성 인터페이스를 렌더링하세요. 페이지 새로고침 후 샌드박스 상태를 잃지 않고 실행 중인 코딩 세션에 다시 연결하세요.

더 알아보기 (Learn more)