投入生产环境 (Going to production)
通过持久记忆、沙箱环境、韧性中间件和部署选项将你的 deep agent 推向生产环境
本指南涵盖了将 deep agent 从本地原型推向生产部署的关键考量。内容包括配置记忆作用域、配置执行环境、添加安全护栏以及连接前端应用。
总览 (Overview)
智能体利用来自记忆和执行环境的信息来完成各项任务。 在生产环境中,有几个核心原语决定了信息的共享与访问方式:
- 线程 (Thread):单次对话。消息历史和临时便签文件默认严格限定在线程作用域内,不会跨线程延续。
- 用户 (User):与智能体进行交互的人。记忆和文件可以归属私有用户,也可以跨用户共享。身份识别与授权来源于你的鉴权层。
- 助手 (Assistant):已配置好的智能体实例。记忆和文件可以绑定到某一个助手,或在所有助手中全局共享。
本页面涵盖:
- LangSmith 部署:具备鉴权、Webhooks 和定时任务的托管基础设施
- 生产级考量:调用方式、多租户、身份鉴权、凭据管理、异步执行与持久性
- 记忆系统:跨会话持久化业务信息
- 执行环境:文件存储与代码沙箱执行
- 安全护栏:路径权限控制与数据隐私
- 前端集成:将 UI 客户端连接到已部署的智能体
LangSmith 部署 (LangSmith Deployments)

将 Deep Agent 投入生产的推荐路径是 Managed Deep Agents,这是一个基于 CLI 的托管运行时,用于在 LangSmith 中创建、运行和运营 deep agents。Managed Deep Agents 目前在 LangSmith Cloud 上公测(Beta)。它运行相同的 Deep Agents 运行框架,但你只需将智能体编写为一个项目文件夹并向托管运行时声明,而无需自行编译代码。参见与 Deep Agents 的关系。
对于需要自定义应用代码、定制路由和高级鉴权的团队,可以直接配置 LangSmith Deployment。无论哪种路径,都会为你的智能体自动预配所需的基础设施:线程 (Threads)、运行记录 (Runs)、Store 存储和 Checkpointer 检查点器,因此你无需手动搭建这些组件。传统的 LangSmith 部署还开箱即用提供身份鉴权、Webhooks 回调、Cron 定时任务以及可观测性监控,并且可以通过 MCP 或 A2A 协议对外暴露智能体。
你也可以在不依赖 LangSmith Cloud 的情况下,将智能体部署到其他 JavaScript 框架和托管平台上:
框架与平台:
TIP
LangSmith Cloud 部署会自动将链路追踪数据发送到以你部署命名的项目中。打开 LangSmith 即可调试运行并监控资源使用情况。对于混合云或自托管环境,请参阅 LangSmith Tracing。建议同时开启 LangSmith Engine,以监控追踪、发现问题并提出优化修复建议。
除非另有说明,本页面的所有代码片段均使用如下 langgraph.json 配置:
{
"dependencies": ["."],
"graphs": {
"agent": "./src/agent.ts:agent"
},
"env": ".env"
}langgraph.json 是告知 LangGraph 平台如何构建和运行应用程序的核心配置文件。它位于项目根目录下,在本地开发(使用 langgraph dev)和生产部署中都是必需的。关键字段包括:
| 字段 | 描述说明 |
|---|---|
dependencies | 要安装的依赖项包列表。["."] 表示将当前目录作为包进行安装(自动读取 package.json)。 |
graphs | 将图 ID 映射到其具体的代码位置。每个条目格式为 "<id>": "./<file>:<variable>",其中 <id> 是通过 API 调用该图时使用的名称,<variable> 是从 <file> 导出的已编译图实例或构造函数。 |
env | 加载环境变量的配置文件路径(例如 .env)。 |
生产级考量 (Production considerations)
调用智能体 (Invoking the agent)
在生产环境中,通常通过客户端 SDK 调用智能体,客户端 SDK 支持用于实时更新的事件流式传输。使用来自 @langchain/langgraph-sdk 的官方客户端:
import { createDeepAgent } from "deepagents";
import { uuid7 } from "langsmith";
import { z } from "zod";
const contextSchema = z.object({ userId: z.string() });
const agent = createDeepAgent({
model: "google:gemini-3.6-flash",
contextSchema,
});
// Start a conversation
const config = { configurable: { thread_id: uuid7() } };
await agent.invoke(
{ messages: [{ role: "user", content: "Plan a 3-day trip to Tokyo" }] },
{ ...config, context: { userId: "user-123" } },
);
// Follow-up on the same conversation: reuse the same thread_id
await agent.invoke(
{ messages: [{ role: "user", content: "Make it 5 days instead" }] },
{ ...config, context: { userId: "user-123" } },
);有关流式模式的完整列表,请参阅流式传输选项。
多租户能力 (Multi-tenancy)
如果你的智能体面向多位用户,则需要对执行环境和长期记忆进行安全隔离。
用户身份与访问控制
在生产环境中,智能体应当识别是谁在发起调用。你可以通过自定义鉴权中间件解析调用方的身份令牌(JWT),并将用户 ID 注入到运行时上下文中:
import { createDeepAgent } from "deepagents";
import { uuid7 } from "langsmith";
import { z } from "zod";
const contextSchema = z.object({ userId: z.string() });
const agent = createDeepAgent({
model: "openai:gpt-5.5",
contextSchema,
});
// Start a conversation
const config = { configurable: { thread_id: uuid7() } };
await agent.invoke(
{ messages: [{ role: "user", content: "Plan a 3-day trip to Tokyo" }] },
{ ...config, context: { userId: "user-123" } },
);
// Follow-up on the same conversation: reuse the same thread_id
await agent.invoke(
{ messages: [{ role: "user", content: "Make it 5 days instead" }] },
{ ...config, context: { userId: "user-123" } },
);团队级角色权限控制 (RBAC)
使用 LangSmith 组织和工作区功能,可以为开发团队成员配置细粒度的角色访问权限(Admin、Developer、Viewer),限制谁可以修改智能体配置、查看生产运行追踪或触发重新部署。
最终用户凭据管理
如果智能体需要代表最终用户访问外部服务(如 GitHub、Google Drive),切勿将个人令牌直接硬编码在环境变量中。利用 LangSmith Connections 让最终用户自行完成 OAuth 授权,智能体即可在隔离作用域内安全调用受限 API。
异步执行 (Async)
对于耗时较长、涉及大量外部搜索或多智能体拆解的复杂任务,同步 HTTP 请求极易发生网关超时。 请改用后台异步任务(Background Runs):客户端提交任务后立即获得运行 ID,随后通过轮询状态或监听 Webhook 回调获取最终报告。
持久性保证 (Durability)
生产级运行框架必须具备崩溃自愈和断点续传能力。 LangGraph 内置的检查点器(Checkpointer)在每个步骤执行后自动持久化图状态。如果底层容器发生重启或网络闪断,智能体可以从最近的检查点无缝恢复,而无需从头重新执行耗费大量 Token 的前置步骤。
记忆系统 (Memory)
记忆作用域 (Scoping)
记忆的持久化范围决定了谁能读取和修改该信息:
- 线程级记忆 (Thread-scoped):保存在当前会话的检查点中,会话结束即归档。
- 用户级记忆 (User-scoped):绑定到特定用户 ID,该用户在所有对话历史中的偏好(如编码习惯、常用语言)均可全局召回。
- 全局助手记忆 (Assistant-scoped):属于智能体自身的领域知识库,对所有访问该助手的用户统一生效。
记忆配置 (Configuration)
在生产环境中,通常配合 StoreBackend 将记忆持久化到分布式存储中:
import { createDeepAgent } from "deepagents";
import { uuid7 } from "langsmith";
import { z } from "zod";
const contextSchema = z.object({ userId: z.string() });
const agent = createDeepAgent({
model: "anthropic:claude-sonnet-5",
contextSchema,
});
// Start a conversation
const config = { configurable: { thread_id: uuid7() } };
await agent.invoke(
{ messages: [{ role: "user", content: "Plan a 3-day trip to Tokyo" }] },
{ ...config, context: { userId: "user-123" } },
);
// Follow-up on the same conversation: reuse the same thread_id
await agent.invoke(
{ messages: [{ role: "user", content: "Make it 5 days instead" }] },
{ ...config, context: { userId: "user-123" } },
);执行环境 (Execution environment)
文件系统 (Filesystem)
生产环境的智能体应当使用安全限定的文件系统:
- 避免使用未加隔离的宿主机文件系统(
LocalShellBackend)。 - 优先使用内存虚拟文件系统(
StateBackend)处理临时便签和搜索结果。 - 对于跨线程长期保存的项目资产,使用带命名空间隔离的
StoreBackend。
沙箱环境 (Sandboxes)
当智能体必须执行用户生成的任意代码、运行单元测试或安装第三方包时,必须使用隔离沙箱。
生命周期管理 (Lifecycle)
- 按任务临时启动 (Per-task):任务启动时创建沙箱,任务结束立即销毁,安全性最高。
- 按会话保持 (Per-thread):在整个对话会话期间维持沙箱状态,支持多轮交互式调试。
- 超时自动回收:配置空闲超时自动清理机制,防止闲置沙箱持续消耗云资源。
import { createDeepAgent } from "deepagents";
import { uuid7 } from "langsmith";
import { z } from "zod";
const contextSchema = z.object({ userId: z.string() });
const agent = createDeepAgent({
model: "openrouter:z-ai/glm-5.2",
contextSchema,
});
// Start a conversation
const config = { configurable: { thread_id: uuid7() } };
await agent.invoke(
{ messages: [{ role: "user", content: "Plan a 3-day trip to Tokyo" }] },
{ ...config, context: { userId: "user-123" } },
);
// Follow-up on the same conversation: reuse the same thread_id
await agent.invoke(
{ messages: [{ role: "user", content: "Make it 5 days instead" }] },
{ ...config, context: { userId: "user-123" } },
);文件传输 (File transfers)
通过沙箱的专门文件传输 API,安全地在宿主机、云存储与沙箱环境之间上传输入文件和下载生成产物。
密钥凭据隔离 (Managing secrets)
避免将生产级主 API 密钥注入沙箱环境内部。通过环境变量代理或受限临时令牌向沙箱暴露最小所需权限。
安全护栏 (Guardrails)
文件访问权限 (Permissions)
使用声明式规则严格限制智能体可触达的路径白名单:例如仅允许访问 /workspace/ 目录,明确阻断任何对 .env、凭据密钥或根文件系统的读写请求。
容错处理 (Fault tolerance)
网络抖动、模型服务限流(429)或暂时性 5xx 故障在生产中不可避免。配置模型重试中间件和备用模型降级策略,确保核心任务不中断。
数据隐私 (Data privacy)
对进入提示词的用户输入和工具输出执行敏感信息脱敏(PII 过滤),防止隐私数据被误记录到外部日志或模型训练集中。
前端集成 (Frontend)
将生产智能体连接到现代 Web 前端界面:
- 使用官方提供了 React/Vue Hook 的前端库快速实现打字机效果。
- 支持多轮对话消息树与时间旅行(Time Travel)回溯调试。
- 优雅展示子智能体并发执行状态看板和待办事项列表(Todo List)。
更多前端集成模式,请参阅前端总览。
import { createDeepAgent } from "deepagents";
import { uuid7 } from "langsmith";
import { z } from "zod";
const contextSchema = z.object({ userId: z.string() });
const agent = createDeepAgent({
model: "fireworks:accounts/fireworks/models/glm-5p2",
contextSchema,
});
// Start a conversation
const config = { configurable: { thread_id: uuid7() } };
await agent.invoke(
{ messages: [{ role: "user", content: "Plan a 3-day trip to Tokyo" }] },
{ ...config, context: { userId: "user-123" } },
);
// Follow-up on the same conversation: reuse the same thread_id
await agent.invoke(
{ messages: [{ role: "user", content: "Make it 5 days instead" }] },
{ ...config, context: { userId: "user-123" } },
);import { createDeepAgent } from "deepagents";
import { uuid7 } from "langsmith";
import { z } from "zod";
const contextSchema = z.object({ userId: z.string() });
const agent = createDeepAgent({
model: "baseten:zai-org/GLM-5.2",
contextSchema,
});
// Start a conversation
const config = { configurable: { thread_id: uuid7() } };
await agent.invoke(
{ messages: [{ role: "user", content: "Plan a 3-day trip to Tokyo" }] },
{ ...config, context: { userId: "user-123" } },
);
// Follow-up on the same conversation: reuse the same thread_id
await agent.invoke(
{ messages: [{ role: "user", content: "Make it 5 days instead" }] },
{ ...config, context: { userId: "user-123" } },
);import { createDeepAgent } from "deepagents";
import { uuid7 } from "langsmith";
import { z } from "zod";
const contextSchema = z.object({ userId: z.string() });
const agent = createDeepAgent({
model: "ollama:north-mini-code-1.0",
contextSchema,
});
// Start a conversation
const config = { configurable: { thread_id: uuid7() } };
await agent.invoke(
{ messages: [{ role: "user", content: "Plan a 3-day trip to Tokyo" }] },
{ ...config, context: { userId: "user-123" } },
);
// Follow-up on the same conversation: reuse the same thread_id
await agent.invoke(
{ messages: [{ role: "user", content: "Make it 5 days instead" }] },
{ ...config, context: { userId: "user-123" } },
);Open a public LangSmith run for this example.
When deploying with the LangGraph SDK, the SDK manages threads for you and you pass the returned thread_id to each run:
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl: "<DEPLOYMENT_URL>", apiKey: "<LANGSMITH_API_KEY>" });
const thread = await client.threads.create();
for await (const chunk of client.runs.stream(
thread.thread_id,
"agent",
{
input: { messages: [{ role: "user", content: "Plan a 3-day trip to Tokyo" }] },
context: { userId: "user-123" },
streamMode: "updates",
},
)) {
console.log(chunk.data);
}TIP
thread_id scopes the conversation (message history, checkpoints). context carries per-run data your tools and middleware read. They are independent: changing one does not affect the other, and you can pass either or both.
Multi-tenancy
When your agent serves multiple users, you need to handle three concerns: verifying who each user is, controlling what they can access, and managing the credentials the agent uses to act on their behalf.

User identity and access control
LangSmith Deployments supports custom authentication to establish user identity and authorization handlers to control access to resources like threads, assistants, and store namespaces. Authorization handlers run after authentication succeeds and can:
- Tag resources with ownership metadata (e.g.,
owner: user_id) - Return filters so users only see their own resources
- Deny access with HTTP 403 for unauthorized operations
For a step-by-step tutorial, see Make conversations private. For a walkthrough, watch the custom auth video.
How you scope memory and execution environments determines what data is shared between users. See the sections below for details.
Team access control (RBAC)
LangSmith's role-based access control governs who on your team can deploy, configure, and monitor agents. This is separate from end-user authorization above.
| Role | Access |
|---|---|
| Workspace Admin | Full permissions including settings and member management |
| Workspace Editor | Create and modify resources, but cannot delete runs or manage members |
| Workspace Viewer | Read-only access |
Custom roles with granular permissions are available on Enterprise plans. See the RBAC reference for the full permission model.
End-user credentials
When your agent needs to call external APIs on behalf of a user (e.g., reading their GitHub repos, sending Slack messages, querying their data warehouse), you need a way to pass the user's credentials through to the agent without hardcoding them.
OAuth via Agent Auth. Agent Auth provides a managed OAuth 2.0 flow. Configure an OAuth provider, and the agent can request tokens scoped to each user. On first use, the agent interrupts execution and presents an OAuth consent URL. After the user authenticates, the agent resumes with a valid token. Tokens are stored and refreshed automatically.
import { Client } from "@langchain/auth";
const authClient = new Client();
// Inside your agent's tool:
// Access the authenticated user via runtime.serverInfo
const authResult = await authClient.authenticate({
provider: "github",
scopes: ["repo", "read:org"],
userId: runtime.serverInfo.user.identity,
});
// Use authResult.token for GitHub API calls on the user's behalfCredential injection for sandboxes. If your agent runs code inside a sandbox that calls external APIs, the sandbox auth proxy can inject credentials into outbound requests automatically, so sandbox code never receives raw API keys. See Managing secrets for setup details.
Workspace secrets. For API keys shared across all users (for example your organization's LLM provider keys, search API keys), store them as workspace secrets in LangSmith. See Managing secrets for details.
Async
LLM-based applications are heavily I/O-bound: calling language models, databases, and external services. Async programming lets these operations run concurrently instead of blocking, improving throughput and responsiveness.
NOTE
LangChain follows the convention of prefixing a to async method names (e.g., ainvoke, abefore_agent, astream). Sync and async variants live in the same class or namespace.
When building for production:
- Create async tools. LangChain runs sync tools in a separate thread to avoid blocking, but native async avoids the threading overhead entirely.
- Use async middleware methods. Custom middleware should implement async hooks (e.g.,
abefore_agentinstead ofbefore_agent). - Use async for external resource lifecycle. Creating sandboxes or connecting to MCP servers involves network calls and should be awaited. This is why graph factories that provision these resources are async.
Durability
Deep Agents run on LangGraph, which provides durable execution out of the box. The persistence layer checkpoints state at each step, so a run interrupted by a failure, timeout, or human-in-the-loop pause resumes from its last recorded state without reprocessing previous steps. For long-running deep agents that spawn many subagents, this means a mid-run failure doesn't lose completed work.

Checkpointing also enables:
- Indefinite interrupts. Human-in-the-loop workflows can pause for minutes or days and resume exactly where they left off.
- Time travel. Every checkpointed step is a snapshot you can rewind to, letting you replay from an earlier state if something goes wrong.
- Safe handling of sensitive operations. For workflows involving payments or other irreversible actions, checkpoints provide an audit trail and a recovery point to inspect the exact state that led to an action.
TIP
LangSmith Deployments configure a persistent checkpointer automatically. If you are self-hosting, see persistence for setup instructions.
Memory
Without memory, every conversation starts from scratch. Memory lets your agent retain information across conversations (user preferences, learned instructions, past experiences) so it can personalize its behavior over time. For an overview of memory types, see the memory concepts guide.

Scoping
Memory is always persistent across conversations. The main question is how it's scoped across user and assistant boundaries. The right scope depends on who should see and modify the data:
| Scope | Namespace | Use case | Example |
|---|---|---|---|
| User (recommended default) | (user_id) | Per-user preferences and context | "I prefer concise responses" |
| Assistant | (assistant_id) | Shared instructions for one assistant | "Cap posts at 280 characters" |
| Global | (org_id) | Read-only policies for all users and assistants | "Never disclose internal pricing" |
WARNING
Shared memory (assistant, user, or organization scope) is a vector for prompt injection. If one user can write to memory that another user's conversation reads, a malicious user could inject instructions into that shared state. Enforce read-only access where appropriate. For example, make organization-wide policies writable only through application code, not by the agent itself. Use permissions to declaratively deny writes to shared paths, or backend policy hooks for custom validation logic.
Configuration
In Deep Agents, memory is stored as files in a virtual filesystem. By default, files are scoped to a single thread (conversation) and not shared across threads. Otherwise, to share memory across threads, route a path like /memories/ to a StoreBackend that writes to the LangGraph Store. Use a CompositeBackend to give the agent both thread-scoped scratch space and cross-thread long-term memory.
NOTE
The rt.serverInfo and rt.executionInfo namespace patterns shown below require deepagents>=1.9.0.
User (recommended)
Namespace by user_id. Each user gets their own private memory. This is the recommended default since most applications deploy a single assistant.
import { createDeepAgent, CompositeBackend, StateBackend, StoreBackend } from "deepagents";
export const agent = createDeepAgent({
backend: new CompositeBackend(
new StateBackend(),
{
"/memories/": new StoreBackend({
namespace: (rt) => [
rt.serverInfo.assistantId,
rt.serverInfo.user.identity,
],
}),
},
),
systemPrompt: `You have persistent memory at /memories/.
Read /memories/instructions.txt at the start of each conversation for
accumulated knowledge and preferences. When you learn something that
should persist, update that file.`,
});Assistant
Namespace by assistant_id. Memory is shared across all users of the same assistant, so any user can read or update it. Use this for shared instructions or knowledge that applies to everyone using a given assistant (e.g., "always reply in formal tone").
import { createDeepAgent, CompositeBackend, StateBackend, StoreBackend } from "deepagents";
export const agent = createDeepAgent({
backend: new CompositeBackend(
new StateBackend(),
{
"/memories/": new StoreBackend({
namespace: (rt) => [rt.serverInfo.assistantId],
}),
},
),
});User
Namespace by user_id alone. Memory follows the user across all assistants. Use this for a global user profile (name, timezone, communication preferences) that should apply regardless of which assistant the user is talking to.
import { createDeepAgent, CompositeBackend, StateBackend, StoreBackend } from "deepagents";
export const agent = createDeepAgent({
backend: new CompositeBackend(
new StateBackend(),
{
"/memories/": new StoreBackend({
namespace: (rt) => [rt.serverInfo.user.identity],
}),
},
),
});Organization
Namespace by org_id. Memory is shared across all users and all assistants. Typically used for organization-wide policies (compliance rules, brand guidelines) that should be read-only for the agent. Write access should be restricted to application code to prevent prompt injection.
import { createDeepAgent, CompositeBackend, StateBackend, StoreBackend } from "deepagents";
export const agent = createDeepAgent({
backend: new CompositeBackend(
new StateBackend(),
{
"/memories/": new StoreBackend({
namespace: (rt) => [rt.context.orgId],
}),
},
),
});You can also read and write to the store from your application code using the Store API. See Advanced usage for examples.
For the full namespace factory API, see namespace factories. For memory patterns like self-improving instructions and knowledge bases, see long-term memory.
Execution environment
Locally, agents can read and write files on disk and run shell commands directly. In production, you need to think about isolation and persistence. The right setup depends on whether your agent needs to execute code:
- Filesystem backends are enough if your agent only reads and writes files. Choose a backend that matches your persistence needs: thread-scoped scratch space, cross-thread storage, or a mix of both.
- Sandboxes add an isolated container with an
executetool for running shell commands. Use a sandbox if your agent needs to run code, install packages, or do anything beyond file I/O.
Filesystem
Choose a backend based on what needs to persist:
- StateBackend (default): thread-scoped scratch space. Files persist across turns within a thread via your checkpointer but are not shared across threads. Checkpointed at every step, so avoid writing large files.
- StoreBackend: cross-thread storage that survives across conversations. Scope with a namespace factory.
- CompositeBackend: mix both. Thread-scoped scratch space by default with cross-thread routes for specific paths like
/memories/.
For the full list of backends and how to build custom ones, see backends.
WARNING
FilesystemBackend and LocalShellBackend access the host directly. Don't use them in deployed agents.
Sandboxes
If your agent needs to run code (not just read and write files), use a sandbox. Sandboxes provide both a filesystem and an execute tool for running shell commands, all inside an isolated container. This isolation also protects your host: if the agent's code exhausts memory or crashes, only the sandbox is affected. Your server keeps running.
Lifecycle
The key decision is how long a sandbox lives. Does each conversation get a fresh one, or do conversations share a persistent environment?
| Scope | Sandbox ID stored on | Lifecycle | Example use case |
|---|---|---|---|
| Thread-scoped | Thread metadata | Fresh per conversation, cleaned up on TTL | A data analysis bot where each conversation starts clean |
| Assistant-scoped | Assistant config | Shared across all conversations | A coding assistant that maintains a cloned repo across conversations |
NOTE
The examples below use an async graph factory instead of a static graph because the sandbox needs the thread_id or assistant_id to look up or create the correct sandbox. Graph factories don't receive a full Runtime (no server_info or execution_info); instead, accept a RunnableConfig and read thread_id and assistant_id from config["configurable"]. The factory is async because sandbox creation is an I/O-bound operation that requires per-run information only available at invocation time.
Thread-scoped (most common)
Each conversation gets its own sandbox. The graph factory reads thread_id from the run config, so each thread automatically gets its own isolated environment. Named sandbox lookup handles deduplication across runs. Cleaned up when the sandbox TTL expires.
import { createDeepAgent, LangSmithSandbox } from "deepagents";
import { SandboxClient } from "langsmith/sandbox";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";
const client = new SandboxClient();
export async function agent(config: LangGraphRunnableConfig) {
const threadId = config.configurable?.thread_id as string;
const sandboxName = `thread-${threadId}`;
const existing = (await client.listSandboxes()).filter(
(sb) => sb.name === sandboxName,
);
const lsSandbox =
existing[0] ??
(await client.createSandbox({
name: sandboxName,
idleTtlSeconds: 3600, // TTL: clean up when idle
}));
return createDeepAgent({
model: "google:gemini-3.6-flash",
backend: new LangSmithSandbox({ sandbox: lsSandbox }),
});
}Assistant-scoped
All conversations share one sandbox. The graph factory reads the assistant ID from config["configurable"], so every thread on the same assistant returns to the same environment. Files, installed packages, and cloned repositories persist across conversations.
import { createDeepAgent, LangSmithSandbox } from "deepagents";
import { SandboxClient } from "langsmith/sandbox";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";
const client = new SandboxClient();
export async function agent(config: LangGraphRunnableConfig) {
const assistantId = config.configurable?.assistant_id as string;
const sandboxName = `assistant-${assistantId}`;
const existing = (await client.listSandboxes()).filter(
(sb) => sb.name === sandboxName,
);
const lsSandbox =
existing[0] ??
(await client.createSandbox({
name: sandboxName,
}));
return createDeepAgent({
model: "google:gemini-3.6-flash",
backend: new LangSmithSandbox({ sandbox: lsSandbox }),
});
}WARNING
Assistant-scoped sandboxes accumulate files, installed packages, and other in-sandbox state over time. Configure a TTL with your sandbox provider, use snapshots to reset periodically, or implement cleanup logic to prevent the sandbox's disk and memory from growing unbounded.
Because the agent variable is an async function (not a compiled graph), the server treats it as a graph factory and calls it on each run, injecting the config. The factory looks up or creates the sandbox by name and returns a fresh agent graph wired to that sandbox.
Once deployed with langgraph deploy, invoke the agent from your application code using the SDK. The client-side code is the same regardless of scope. The scoping is handled entirely in the agent factory above, but the behavior differs:
Thread-scoped
Each thread gets its own sandbox. Follow-up messages within the same thread reuse the same sandbox, but a new thread always starts fresh with no leftover files or installed packages from previous conversations.
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl: "<DEPLOYMENT_URL>", apiKey: "<LANGSMITH_API_KEY>" });
// Conversation 1: install pandas and analyze data
const thread1 = await client.threads.create();
for await (const chunk of client.runs.stream(
thread1.thread_id,
"agent",
{ input: { messages: [{ role: "human", content: "Install pandas and analyze sales_data.csv" }] } },
)) {
console.log(chunk.data);
}
// Follow-up in the same conversation — pandas is still installed
for await (const chunk of client.runs.stream(
thread1.thread_id,
"agent",
{ input: { messages: [{ role: "human", content: "Now plot the results" }] } },
)) {
console.log(chunk.data);
}
// Conversation 2: fresh sandbox — pandas is NOT installed, no files from conversation 1
const thread2 = await client.threads.create();
for await (const chunk of client.runs.stream(
thread2.thread_id,
"agent",
{ input: { messages: [{ role: "human", content: "What packages are installed?" }] } },
)) {
console.log(chunk.data);
}Assistant-scoped
All threads share one sandbox. This is useful when the sandbox has state that's expensive to recreate, such as a cloned repo, installed dependencies, or build artifacts. Any conversation on the same assistant picks up where the last one left off without repeating setup.
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl: "<DEPLOYMENT_URL>", apiKey: "<LANGSMITH_API_KEY>" });
// Conversation 1: clone and set up the project
const thread1 = await client.threads.create();
for await (const chunk of client.runs.stream(
thread1.thread_id,
"agent",
{ input: { messages: [{ role: "human", content: "Clone https://github.com/org/repo and install dependencies" }] } },
)) {
console.log(chunk.data);
}
// Conversation 2: repo and dependencies are still there
const thread2 = await client.threads.create();
for await (const chunk of client.runs.stream(
thread2.thread_id,
"agent",
{ input: { messages: [{ role: "human", content: "Run the test suite and fix any failures" }] } },
)) {
console.log(chunk.data);
}File transfers
Sandboxes are isolated containers, so your application code can't directly access files inside them. Use upload_files() and download_files() to move data across the sandbox boundary:
- Seed the sandbox before the agent runs: upload user files, skill scripts, configuration, or persistent memories so the agent has what it needs from the start
- Retrieve results after the agent finishes: download generated artifacts (reports, plots, exports) and sync updated memories back for future conversations
For provider-specific file transfer examples, see working with files. For provider setup, security, and lifecycle patterns, see the full sandboxes guide.
Example: syncing skills and memories with custom middleware
Skill scripts that the agent needs to execute must be uploaded into the sandbox before the agent runs. You may also want to sync memories so the agent can read and update them inside the container. Use custom middleware with
before_agentandafter_agenthooks to move files across the sandbox boundary:
> import { createMiddleware } from "langchain";
> import {
> createDeepAgent,
> CompositeBackend,
> LangSmithSandbox,
> StoreBackend,
> } from "deepagents";
> import { SandboxClient } from "langsmith/sandbox";
>
> function safeFilename(key: string): string {
> const name = key.split("/").pop()!;
> if (name.includes("..") || /[*?]/.test(name)) {
> throw new Error(`Invalid key: ${key}`);
> }
> return name;
> }
>
> const createSandboxSyncMiddleware = (backend: CompositeBackend) => {
> return createMiddleware({
> name: "SandboxSyncMiddleware",
> beforeAgent: async (state, runtime) => {
> // Upload skill scripts and memories into the sandbox
> const userId = runtime.serverInfo.user.identity;
> const store = runtime.store;
> const encoder = new TextEncoder();
> const files: [string, Uint8Array][] = [];
> for (const item of await store.search(["skills", userId])) {
> const name = safeFilename(item.key);
> files.push([`/skills/${name}`, encoder.encode(item.value.content)]);
> }
> for (const item of await store.search(["memories", userId])) {
> const name = safeFilename(item.key);
> files.push([`/memories/${name}`, encoder.encode(item.value.content)]);
> }
> if (files.length > 0) {
> await backend.uploadFiles(files);
> }
> },
> afterAgent: async (state, runtime) => {
> // Sync updated memories back to the store
> const userId = runtime.serverInfo.user.identity;
> const store = runtime.store;
> const items = await store.search(["memories", userId]);
> const results = await backend.downloadFiles(
> items.map((item) => `/memories/${item.key}`),
> );
> const decoder = new TextDecoder();
> for (const result of results) {
> if (result.content) {
> await store.put(
> ["memories", userId],
> result.path.split("/").pop()!,
> { content: decoder.decode(result.content) },
> );
> }
> }
> },
> });
> };
>
> const client = new SandboxClient();
> const lsSandbox = await client.createSandbox();
>
> const backend = new CompositeBackend(
> new LangSmithSandbox({ sandbox: lsSandbox }),
> {
> "/skills/": new StoreBackend({
> namespace: (rt) => ["skills", rt.serverInfo.user.identity],
> }),
> "/memories/": new StoreBackend({
> namespace: (rt) => ["memories", rt.serverInfo.user.identity],
> }),
> },
> );
>
> export const agent = createDeepAgent({
> backend,
> middleware: [createSandboxSyncMiddleware(backend)],
> });
> ```
#### Managing secrets
Sandboxes are isolated containers, so environment variables from your host aren't available inside them. There are two ways to provide API keys and other secrets to sandbox code:
**Auth proxy (recommended).** The [sandbox auth proxy](/langsmith/sandbox-auth-proxy.md) intercepts outbound requests from the sandbox and injects authentication headers automatically. Sandbox code calls external APIs normally, and the proxy adds the correct credentials based on the destination host. This means API keys never appear in sandbox code, environment variables, or logs.

```json theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
{
"proxy_config": {
"rules": [
{
"name": "openai-api",
"match_hosts": ["api.openai.com"],
"inject_headers": {
"Authorization": "Bearer ${OPENAI_API_KEY}"
}
},
{
"name": "anthropic-api",
"match_hosts": ["api.anthropic.com"],
"inject_headers": {
"x-api-key": "${ANTHROPIC_API_KEY}"
}
}
]
}
}The ${SECRET_KEY} references resolve against secrets stored in your LangSmith workspace settings. Configure secrets there before creating a template that references them.
Workspace secrets. For API keys that don't need proxy-based injection (e.g., keys used by the agent server itself, not sandbox code), store them as workspace secrets in LangSmith. These are available as environment variables at runtime for all agents in the workspace.
WARNING
Avoid passing secrets into sandboxes via environment variables or file uploads. Agents can read any accessible file or environment variable inside the sandbox, including credentials. The auth proxy keeps secrets out of the sandbox entirely.
Guardrails
Agents in production run autonomously, which means they can loop indefinitely, hit rate limits, or process user data that contains sensitive information. Deep Agents provide two layers of protection:
- Permissions: declarative allow/deny rules that control which files and directories the agent can read or write.
- Fault tolerance: rate limiting, retries, fallbacks, and error handling.
- Data privacy: middleware that detects and handles PII before it reaches the model or gets stored in logs.
Permissions
Permissions are declarative allow/deny rules that control which files and directories the agent can read or write. Use permissions to isolate the agent to a working directory, protect sensitive files, or enforce read-only memory. Rules are evaluated in declaration order, and the first matching rule wins.
Fault tolerance
For rate limiting, retries, fallbacks, and error handling, see Fault tolerance.
Data privacy
If your agent processes user input that might contain emails, credit card numbers, or other PII, you can detect and handle it before it reaches the model or gets stored in logs:
import { createAgent, piiMiddleware } from "langchain";
const agent = createAgent({
model: "google:gemini-3.6-flash",
middleware: [
piiMiddleware("email", { strategy: "redact", applyToInput: true }),
piiMiddleware("credit_card", { strategy: "mask", applyToInput: true }),
],
});Strategies include redact (replace with [REDACTED_EMAIL]), mask (partial masking like ****-****-****-1234), hash (deterministic hash), and block (raise an error). You can also write custom detectors for domain-specific patterns.
See piiMiddleware for the full configuration.
For the default Deep Agents middleware stack, see Customization. For additional LangChain prebuilt middleware (retries, fallbacks, PII detection, and more), see Prebuilt middleware.
Frontend
Deep Agents use useStream to connect your UI to the agent backend. useStream is a frontend hook (available for React, Vue, Svelte, and Angular) that streams messages, subagent progress, and custom state from your agent in real time.
Locally, useStream points at http://localhost:2024. In production, point it at your LangSmith Deployment and configure reconnection so users don't lose progress if their connection drops.
import { useStream } from "@langchain/react";
function App() {
const stream = useStream<typeof agent>({
apiUrl: "https://your-deployment.langsmith.dev",
assistantId: "agent",
});
}For deep agent workflows that spawn many subagents, set a high recursionLimit when submitting to avoid cutting off long-running executions:
stream.submit(
{ messages: [{ type: "human", content: text }] },
{
streamSubgraphs: true,
config: { recursionLimit: 10000 },
},
);For UI patterns specific to deep agents, such as subagent cards, todo lists, and custom state rendering, see the frontend guide.
Connect these docs to your agent of choice via MCP for real-time answers.
