此 LangSmith SDK 提供以编程方式创建和与沙箱交互的接口。
安装
# uv
uv add "langsmith[sandbox]"
# pip
pip install "langsmith[sandbox]"
npm install langsmith
# or
yarn add langsmith
此 [sandbox] Python 安装的额外依赖 websockets,可实现实时流式传输和 timeout=0。如果没有它, run() 会自动回退到 HTTP。对于 TypeScript,请安装可选的 ws 包以支持 WebSocket 流式传输:
npm install ws
创建并运行沙箱
当您想从可重用的自定义文件系统镜像启动时,请传递快照 ID 或名称;请参阅 快照 以了解该流程。
from langsmith.sandbox import SandboxClient
# Client uses LANGSMITH_ENDPOINT and LANGSMITH_API_KEY from environment
client = SandboxClient()
# Create a sandbox with the default runtime and run code
with client.sandbox() as sb:
result = sb.run("python -c 'print(2 + 2)'")
print(result.stdout) # "4\n"
print(result.success) # True
// Client uses LANGSMITH_ENDPOINT and LANGSMITH_API_KEY from environment
const client = new SandboxClient();
// Create a sandbox with the default runtime and run code
const sandbox = await client.createSandbox();
const result = await sandbox.run("node -e 'console.log(2 + 2)'");
console.log(result.stdout); // "4\n"
// Don't forget to clean up
await sandbox.delete();
运行命令
每个 run() 调用返回一个 ExecutionResult ,包含 stdout, stderr, exit_code和 success.
with client.sandbox() as sb:
result = sb.run("echo 'Hello, World!'")
print(result.stdout) # "Hello, World!\n"
print(result.stderr) # ""
print(result.exit_code) # 0
print(result.success) # True
# Commands that fail return non-zero exit codes
result = sb.run("exit 1")
print(result.success) # False
print(result.exit_code) # 1
const sandbox = await client.createSandbox();
try {
const result = await sandbox.run("echo 'Hello, World!'");
console.log(result.stdout); // "Hello, World!\n"
console.log(result.stderr); // ""
console.log(result.exit_code); // 0
// Pass environment variables and working directory
const envResult = await sandbox.run("echo $MY_VAR", {
env: { MY_VAR: "test-value" },
cwd: "/tmp",
});
} finally {
await sandbox.delete();
}
流式输出
对于长时间运行的命令,请使用回调或 CommandHandle.
使用回调进行流式处理
with client.sandbox() as sb:
result = sb.run(
"make build",
timeout=600,
on_stdout=lambda s: print(s, end=""),
on_stderr=lambda s: print(s, end="", file=sys.stderr),
)
print(f"\nBuild {'succeeded' if result.success else 'failed'}")
const result = await sandbox.run("make build", {
timeout: 600,
onStdout: (data) => process.stdout.write(data),
onStderr: (data) => process.stderr.write(data),
});
console.log(`Exit code: ${result.exit_code}`);
使用 CommandHandle 进行流式处理
设置 wait=False 以获取 CommandHandle ,以便完全控制输出流。
with client.sandbox() as sb:
handle = sb.run("make build", timeout=600, wait=False)
print(f"Command ID: {handle.command_id}")
for chunk in handle:
prefix = "OUT" if chunk.stream == "stdout" else "ERR"
print(f"[{prefix}] {chunk.data}", end="")
result = handle.result
print(f"\nExit code: {result.exit_code}")
const handle = await sandbox.run("python train.py", {
wait: false,
timeout: 600,
});
console.log(`Command ID: ${handle.commandId}`);
console.log(`PID: ${handle.pid}`);
for await (const chunk of handle) {
if (chunk.stream === "stdout") {
process.stdout.write(chunk.data);
} else {
process.stderr.write(chunk.data);
}
}
const result = await handle.result;
console.log(`Exit code: ${result.exit_code}`);
发送 stdin 和终止命令
with client.sandbox() as sb:
handle = sb.run(
"python -c 'name = input(\"Name: \"); print(f\"Hello {name}\")'",
timeout=30,
wait=False,
)
for chunk in handle:
if "Name:" in chunk.data:
handle.send_input("World\n")
print(chunk.data, end="")
result = handle.result
const handle = await sandbox.run("python -i", { wait: false });
// Send input to stdin
handle.sendInput("print(2 + 2)\n");
handle.sendInput("exit()\n");
for await (const chunk of handle) {
process.stdout.write(chunk.data);
}
终止正在运行的命令:
with client.sandbox() as sb:
handle = sb.run("python server.py", timeout=0, wait=False)
for chunk in handle:
print(chunk.data, end="")
if "Ready" in chunk.data:
break
handle.kill()
const handle = await sandbox.run("sleep 300", { wait: false });
handle.kill();
const result = await handle.result;
console.log(result.exit_code); // non-zero
重新连接到正在运行的命令
如果客户端断开连接,请使用命令 ID 重新连接:
with client.sandbox() as sb:
handle = sb.run("make build", timeout=600, wait=False)
command_id = handle.command_id
# Later, possibly in a different process
handle = sb.reconnect(command_id)
for chunk in handle:
print(chunk.data, end="")
result = handle.result
const handle = await sandbox.run("long-task", { wait: false });
const commandId = handle.commandId;
// Later, or from a different client
const newHandle = await sandbox.reconnect(commandId);
for await (const chunk of newHandle) {
process.stdout.write(chunk.data);
}
文件操作
在沙箱中读取和写入文件:
with client.sandbox() as sb:
# Write a file
sb.write("/app/script.py", "print('Hello from file!')")
# Run the script
result = sb.run("python /app/script.py")
print(result.stdout) # "Hello from file!\n"
# Read a file (returns bytes)
content = sb.read("/app/script.py")
print(content.decode()) # "print('Hello from file!')"
# Write binary files
sb.write("/app/data.bin", b"\x00\x01\x02\x03")
const sandbox = await client.createSandbox();
try {
// Write a file (string content)
await sandbox.write("/app/script.py", "print('Hello from file!')");
// Run the script
const result = await sandbox.run("python /app/script.py");
console.log(result.stdout); // "Hello from file!\n"
// Read a file (returns Uint8Array)
const content = await sandbox.read("/app/script.py");
console.log(new TextDecoder().decode(content));
// Write binary files
await sandbox.write("/app/data.bin", new Uint8Array([0x00, 0x01, 0x02, 0x03]));
} finally {
await sandbox.delete();
}
沙箱生命周期和保留期
沙箱由基于 **空闲 活动** 和 **stopped** state.
| 字段 | 控制内容 | 触发时间 |
|---|---|---|
idle_ttl_seconds | The launcher stops the sandbox after this many seconds of inactivity. Any command execution or file I/O resets the timer. 0 禁用空闲停止。 | 默认值 600 (10 分钟)当省略时。 |
delete_after_stop_seconds | 一旦沙箱进入 stopped 状态,此计时器开始。计时结束后,沙箱行和文件系统克隆将由服务器端清理永久删除。 0 禁用停止锚定删除(需要手动清理)。 | 当省略时,服务器应用其配置的默认值(通常为 14 天)。 |
两个值都必须是 60 的倍数(分钟分辨率)。完整生命周期如下:
running ──(idle for idle_ttl_seconds)──▶ stopped ──(delete_after_stop_seconds)──▶ deleted
您也可以调用 stop_sandbox / stopSandbox 显式调用——这也 填充 stopped_at 并启动删除计时器。
# Default retention (server defaults: 10-min idle stop, 14-day delete)
with client.sandbox() as sb:
sb.run("echo hello")
# Aggressive: stop after 5 min idle, delete 1 hour after stop
sb = client.create_sandbox(
idle_ttl_seconds=300,
delete_after_stop_seconds=3600,
)
# Long-running: never auto-stop, delete 7 days after manual stop
sb = client.create_sandbox(
idle_ttl_seconds=0,
delete_after_stop_seconds=604800,
)
# Update retention on an existing sandbox
sb = client.update_sandbox(
sb.name,
idle_ttl_seconds=1800,
delete_after_stop_seconds=2592000, # 30 days
)
// Default retention (server defaults applied)
const sandbox = await client.createSandbox();
// Aggressive: stop after 5 min idle, delete 1 hour after stop
const sb = await client.createSandbox({
idleTtlSeconds: 300,
deleteAfterStopSeconds: 3600,
});
// Long-running: never auto-stop, delete 7 days after manual stop
const longRunning = await client.createSandbox({
idleTtlSeconds: 0,
deleteAfterStopSeconds: 604800,
});
// Update retention on an existing sandbox
await client.updateSandbox(sb.name, {
idleTtlSeconds: 1800,
deleteAfterStopSeconds: 2592000, // 30 days
});
命令生命周期和 TTL
沙箱守护进程通过两个超时机制管理命令会话生命周期:
- 会话 TTL(已完成的命令):命令完成后,其会话会在内存中保留一个 TTL 期间。在此期间您可以重新连接以检索输出。TTL 到期后,会话将被清理。
- 空闲超时(运行中的命令):没有连接客户端的运行命令会在空闲超时后被终止(默认值:5 分钟)。每次客户端连接时,空闲计时器都会重置。设置为
-1表示无空闲超时。
组合生命周期选项
with client.sandbox() as sb:
# Long-running task: 30-min idle timeout, 1-hour session TTL
handle = sb.run(
"python train.py",
timeout=0, # No command timeout
idle_timeout=1800, # Kill after 30min with no clients
ttl_seconds=3600, # Keep session for 1 hour after exit
wait=False,
)
# Fire-and-forget: no idle timeout, infinite TTL
handle = sb.run(
"python background_job.py",
timeout=0,
idle_timeout=-1, # Never kill due to idle
ttl_seconds=-1, # Keep session forever
wait=False,
)
const sandbox = await client.createSandbox();
try {
// Long-running task: 30-min idle timeout, 1-hour session TTL
const handle = await sandbox.run("python train.py", {
timeout: 0, // No command timeout
idleTimeout: 1800, // Kill after 30min with no clients
ttlSeconds: 3600, // Keep session for 1 hour after exit
wait: false,
});
// Fire-and-forget: no idle timeout, infinite TTL
const bg = await sandbox.run("python background_job.py", {
timeout: 0,
idleTimeout: -1, // Never kill due to idle
ttlSeconds: -1, // Keep session forever
wait: false,
});
} finally {
await sandbox.delete();
}
设置 kill_on_disconnect=True (Python)或 killOnDisconnect: true (TypeScript)以便在最后一个客户端断开时立即终止命令,而不是等待空闲超时。
服务 URL(Python)
通过经过身份验证的 URL 访问在沙箱中运行的 HTTP 服务。您可以在浏览器中打开它、从代码中调用它,或与队友共享它。
with client.sandbox() as sb:
sb.run("python -m http.server 8000", timeout=0, wait=False)
svc = sb.service(port=8000)
# Open in a browser
print(svc.browser_url)
# Or make requests with built-in helpers (auth is injected automatically)
resp = svc.get("/api/data")
resp = svc.post("/api/data", json={"key": "value"})
有关更多详细信息(包括用例、REST API 访问和完整的 FastAPI 示例),请参阅 服务 URL.
TCP 隧道(Python)
像访问本地服务一样访问在沙箱中运行的任何 TCP 服务。隧道会打开一个本地 TCP 端口,并通过 WebSocket 将连接转发到沙箱内的目标端口。
# Snapshot built from the official postgres:16 image
sb = client.create_sandbox(snapshot_id=postgres_snapshot_id)
pg_handle = sb.run(
"POSTGRES_HOST_AUTH_METHOD=trust docker-entrypoint.sh postgres",
timeout=0,
wait=False,
)
try:
with sb.tunnel(remote_port=5432, local_port=25432) as t:
conn = psycopg2.connect(
host="127.0.0.1",
port=t.local_port,
user="postgres",
)
cursor = conn.cursor()
cursor.execute("SELECT version()")
print(cursor.fetchone())
conn.close()
finally:
pg_handle.kill()
client.delete_sandbox(sb.name)
隧道适用于任何 TCP 服务(Redis、HTTP 服务器等),您可以同时打开多个隧道:
with sb.tunnel(remote_port=5432, local_port=25432) as t1, \
sb.tunnel(remote_port=6379, local_port=26379) as t2:
# Use both Postgres and Redis simultaneously
pass
异步支持(Python)
Python SDK 提供了一个完整的异步客户端:
from langsmith.sandbox import AsyncSandboxClient
async def main():
async with AsyncSandboxClient() as client:
async with await client.sandbox() as sb:
result = await sb.run("python -c 'print(1 + 1)'")
print(result.stdout) # "2\n"
await sb.write("/app/test.txt", "async content")
content = await sb.read("/app/test.txt")
print(content.decode())
# Async streaming
handle = await sb.run("make build", timeout=600, wait=False)
async for chunk in handle:
print(chunk.data, end="")
result = await handle.result
# Async service URLs
svc = await sb.service(port=8000)
resp = await svc.get("/api/data")
url = await svc.get_service_url()
token = await svc.get_token()
跟踪沙箱活动
通过 env 上的 run() 参数传递 LangSmith 跟踪环境变量,以发送在沙箱中运行的代码的跟踪信息。在进程退出前调用 flush() 以确保所有跟踪信息都已发送。
from langsmith.sandbox import SandboxClient
client = SandboxClient()
tracing_env = {
"LANGSMITH_API_KEY": "lsv2_pt_...",
"LANGSMITH_ENDPOINT": "https://api.smith.langchain.com",
"LANGSMITH_TRACING": "true",
"LANGSMITH_PROJECT": "my-sandbox-traces",
}
with client.sandbox() as sandbox:
sandbox.run("pip install langsmith", timeout=120, env=tracing_env)
result = sandbox.run("python3 my_agent.py", env=tracing_env)
print(result.stdout)
const client = new SandboxClient();
const tracingEnv = {
LANGSMITH_API_KEY: "lsv2_pt_...",
LANGSMITH_ENDPOINT: "https://api.smith.langchain.com",
LANGSMITH_TRACING: "true",
LANGSMITH_PROJECT: "my-sandbox-traces",
};
const sandbox = await client.createSandbox();
try {
await sandbox.run("pip install langsmith", { timeout: 120, env: tracingEnv });
const result = await sandbox.run("python3 my_agent.py", { env: tracingEnv });
console.log(result.stdout);
} finally {
await sandbox.delete();
}
在沙箱内,任何带有 LangSmith 检测的代码(@traceable、LangChain、LangGraph)会自动从注入的环境变量中获取跟踪配置。
错误处理
两个 SDK 都为特定错误处理提供了类型化异常:
from langsmith.sandbox import (
SandboxClientError, # Base exception
ResourceCreationError, # Provisioning failed
ResourceNotFoundError, # Resource doesn't exist
ResourceTimeoutError, # Operation timed out
SandboxNotReadyError, # Sandbox not ready yet
SandboxConnectionError, # Network/WebSocket error
CommandTimeoutError, # Command exceeded timeout
QuotaExceededError, # Quota limit reached
)
try:
with client.sandbox() as sb:
result = sb.run("sleep 999", timeout=10)
except CommandTimeoutError as e:
print(f"Command timed out: {e}")
except ResourceNotFoundError as e:
print(f"{e.resource_type} not found: {e}")
except SandboxClientError as e:
print(f"Error: {e}")
LangSmithSandboxError,
LangSmithResourceNotFoundError,
LangSmithResourceTimeoutError,
LangSmithSandboxConnectionError,
LangSmithCommandTimeoutError,
LangSmithQuotaExceededError,
} from "langsmith/sandbox";
try {
const sandbox = await client.createSandbox("not-a-real-snapshot");
await sandbox.delete();
} catch (e) {
if (e instanceof LangSmithResourceNotFoundError) {
console.log(`${e.resourceType} not found: ${e.message}`);
} else if (e instanceof LangSmithResourceTimeoutError) {
console.log(`Timeout waiting for ${e.resourceType}: ${e.message}`);
} else if (e instanceof LangSmithSandboxError) {
console.log(`Error: ${e.message}`);
}
}