@bitkaio/langchain-kubernetes
v0.3.0
Published
Kubernetes sandbox provider for the DeepAgents framework
Downloads
41
Maintainers
Readme
@bitkaio/langchain-kubernetes
Kubernetes sandbox provider for the DeepAgents framework (TypeScript).
Supports two backend modes:
| Mode | Requirements | Best for |
| ---- | ------------ | -------- |
| agent-sandbox (default) | kubernetes-sigs/agent-sandbox controller + CRDs | Production — warm pools, gVisor/Kata, sub-second startup |
| raw | Any cluster, @kubernetes/client-node + tar-stream | Dev / clusters where you can't install CRDs |
Installation
# agent-sandbox mode — no extra deps (fetch is built in)
npm install @bitkaio/langchain-kubernetes
# raw mode — additional deps required
npm install @bitkaio/langchain-kubernetes @kubernetes/client-node tar-streamQuick start
agent-sandbox mode
import { KubernetesProvider } from "@bitkaio/langchain-kubernetes";
const provider = new KubernetesProvider({
mode: "agent-sandbox",
routerUrl: "http://sandbox-router-svc.default.svc.cluster.local:8080",
templateName: "python-sandbox-template",
});
const sandbox = await provider.getOrCreate();
const result = await sandbox.execute("python3 -c 'print(2 + 2)'");
console.log(result.output); // "4\n"
console.log(result.exitCode); // 0
await provider.delete(sandbox.id);raw mode
import { KubernetesProvider } from "@bitkaio/langchain-kubernetes";
const provider = new KubernetesProvider({
mode: "raw",
image: "python:3.12-slim",
});
const sandbox = await provider.getOrCreate();
const result = await sandbox.execute("python3 -c 'print(42)'");
await provider.delete(sandbox.id);Configuration reference
Shared options
| Field | Type | Default | Description |
| ----- | ---- | ------- | ----------- |
| mode | "agent-sandbox" \| "raw" | "agent-sandbox" | Backend mode |
| namespace | string | "deepagents-sandboxes" | Kubernetes namespace |
| startupTimeoutMs | number | 120_000 | Ms to wait for sandbox to be ready |
| executeTimeoutMs | number | 300_000 | Default ms per execute() call |
agent-sandbox mode options
| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| routerUrl | string | Yes | URL of the sandbox-router service |
| templateName | string | Yes | SandboxTemplate name to instantiate |
| serverPort | number | No (8888) | Port the sandbox runtime listens on |
| kubeApiUrl | string | No | Kubernetes API URL. Defaults to in-cluster URL. Use http://localhost:8001 for kubectl proxy. |
| kubeToken | string | No | Bearer token for k8s API. Auto-read from service account if omitted. |
raw mode options
| Field | Type | Default | Description |
| ----- | ---- | ------- | ----------- |
| image | string | "python:3.12-slim" | Container image |
| imagePullPolicy | string | "IfNotPresent" | Image pull policy |
| workdir | string | "/workspace" | Working directory inside container |
| command | string[] | ["sleep", "infinity"] | Container entrypoint |
| env | Record<string, string> | — | Extra environment variables |
| cpuRequest / cpuLimit | string | "100m" / "1000m" | CPU resources |
| memoryRequest / memoryLimit | string | "256Mi" / "1Gi" | Memory resources |
| ephemeralStorageLimit | string | "5Gi" | Ephemeral storage limit |
| blockNetwork | boolean | true | Attach deny-all NetworkPolicy |
| runAsUser / runAsGroup | number | 1000 / 1000 | UID/GID inside container |
| seccompProfile | string | "RuntimeDefault" | seccomp profile type |
| namespacePerSandbox | boolean | false | Give each sandbox its own namespace |
| kubeconfigPath | string | — | Path to kubeconfig file |
| context | string | — | Kubeconfig context to use |
Sandbox API
Every sandbox extends BaseSandbox from the deepagents package:
// Execute a shell command
const result = await sandbox.execute("ls -la /workspace");
// result.output — combined stdout + stderr
// result.exitCode — process exit code
// result.truncated — true if output was capped at outputLimitBytes
// Upload files
const results = await sandbox.uploadFiles([
["/workspace/script.py", Buffer.from("print('hello')")],
["/workspace/data.json", Buffer.from('{"key": "value"}')],
]);
// results[0].error — null on success, "file_not_found" | "permission_denied" etc. on failure
// Download files
const downloads = await sandbox.downloadFiles(["/workspace/output.txt"]);
// downloads[0].content — Uint8Array | null
// downloads[0].error — null on success
// BaseSandbox also provides higher-level helpers built on execute():
// sandbox.ls(), sandbox.read(), sandbox.write(), sandbox.glob(), ...Usage with DeepAgents
KubernetesSandbox plugs directly into
DeepAgents via the backend parameter of
createDeepAgent. When a sandbox backend is set, the agent automatically gains an
execute tool for running shell commands in addition to the standard filesystem tools.
One-shot usage
import { ChatAnthropic } from "@langchain/anthropic";
import { createDeepAgent } from "deepagents";
import { KubernetesProvider } from "@bitkaio/langchain-kubernetes";
const provider = new KubernetesProvider({
routerUrl: "http://sandbox-router-svc.default.svc.cluster.local:8080",
templateName: "python-sandbox-template",
// mode: "raw", image: "python:3.12-slim", // raw mode alternative
});
const sandbox = await provider.getOrCreate();
const llm = new ChatAnthropic({ model: "claude-opus-4-6" });
const agent = createDeepAgent({ model: llm, backend: sandbox });
const result = await agent.invoke({
messages: [{ role: "user", content: "Write and run a Python script that prints the Fibonacci sequence" }],
});
console.log(result.messages.at(-1)?.content);
await provider.delete(sandbox.id);Multi-turn: persistent sandbox per conversation
For multi-turn applications you need the same sandbox to survive across turns — installed packages, written files, and shell state must all be retained between messages.
KubernetesSandboxManager.createAgent(llm) returns a ready-to-use DeepAgents agent that
handles this automatically. Each turn it reconnects to the same sandbox using the
conversation thread_id; if the sandbox has expired it provisions a new one
transparently.
Behind Express / Hono
import express from "express";
import { MemorySaver } from "@langchain/langgraph";
import { ChatAnthropic } from "@langchain/anthropic";
import { KubernetesSandboxManager } from "@bitkaio/langchain-kubernetes";
const manager = new KubernetesSandboxManager(
{
routerUrl: "http://sandbox-router-svc.default.svc.cluster.local:8080",
templateName: "python-sandbox-template",
},
{ ttlIdleSeconds: 1800 },
);
const llm = new ChatAnthropic({ model: "claude-opus-4-6" });
// MemorySaver keeps state in-process.
// Replace with a persistent checkpointer for multi-process deployments.
const agent = await manager.createAgent(llm, { checkpointer: new MemorySaver() });
const app = express();
app.use(express.json());
app.post("/chat/:threadId", async (req, res) => {
const result = await agent.invoke(
{ messages: [{ role: "user", content: req.body.message }] },
{ configurable: { thread_id: req.params.threadId } },
);
res.json({ reply: result.messages.at(-1)?.content });
});
process.on("SIGTERM", () => manager.shutdown());
app.listen(3000);Each thread_id gets its own sandbox. The first request provisions a new one; every
subsequent request reconnects to the same one.
With langgraph dev / LangGraph Platform
Yes — each LangGraph thread automatically gets its own persistent sandbox.
createAgent() stores the sandboxId as a field in graph state. The LangGraph
Platform (and langgraph dev) checkpoint the entire graph state — including
sandboxId — between runs for each thread. When the same thread sends its next
message, the platform restores the state and the agent reconnects to the same sandbox
automatically. No extra configuration is needed.
Export the compiled agent from agent.ts and point langgraph.json at it:
agent.ts:
import { ChatAnthropic } from "@langchain/anthropic";
import { KubernetesSandboxManager } from "@bitkaio/langchain-kubernetes";
const manager = new KubernetesSandboxManager(
{
routerUrl: "http://sandbox-router-svc.default.svc.cluster.local:8080",
templateName: "python-sandbox-template",
warmPoolName: "python-pool", // optional: sub-second startup
},
{ ttlIdleSeconds: 1800, defaultLabels: { app: "my-agent" } },
);
const llm = new ChatAnthropic({ model: "claude-opus-4-6" });
// The platform provides the checkpointer — omit it here
export const graph = await manager.createAgent(llm);langgraph.json:
{
"graphs": {
"agent": "./agent.ts:graph"
}
}npx @langchain/langgraph-cli dev
# → http://localhost:2024Interact via the LangGraph SDK — threads and sandbox persistence are handled automatically:
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl: "http://localhost:2024" });
const thread = await client.threads.create();
// First run — provisions a sandbox for this thread
await client.runs.create(thread.thread_id, "agent", {
input: { messages: [{ role: "user", content: "Install pandas and analyse the iris dataset" }] },
});
// Second run — reconnects to the same sandbox automatically
await client.runs.create(thread.thread_id, "agent", {
input: { messages: [{ role: "user", content: "Now plot a histogram of petal length" }] },
});Custom systemPrompt and tools
Extra options passed to createAgent are forwarded to createDeepAgent:
const agent = await manager.createAgent(llm, {
checkpointer: new MemorySaver(),
systemPrompt: "You are a data analyst. Always save outputs to /workspace/output/.",
// tools: [myCustomTool],
});KubernetesSandboxManagerOptions:
| Field | Type | Default | Description |
| ----- | ---- | ------- | ----------- |
| ttlSeconds | number \| undefined | — | Absolute TTL from creation (seconds) |
| ttlIdleSeconds | number \| undefined | — | Idle TTL from last execute() (seconds) |
| defaultLabels | Record<string, string> \| undefined | — | Labels applied to every sandbox (auto-prefixed) |
Methods:
| Method | Returns | Description |
| ------ | ------- | ----------- |
| createAgent(model, options?) | Promise<CompiledGraph> | Returns a compiled DeepAgents agent with sandbox persistence (primary integration point) |
| createAgentNode(model, options?) | AsyncNodeFn | Returns a single LangGraph node; use when building a multi-node graph |
| getOrReconnect(sandboxId) | Promise<KubernetesSandbox> | Reconnect or create; for custom node logic |
| cleanup(maxIdleSeconds?) | Promise<CleanupResult> | Delete expired sandboxes |
| shutdown() | Promise<void> | Delete all sandboxes |
| [Symbol.asyncDispose]() | Promise<void> | Called by await using |
Sandbox lifecycle management
Provider API — getOrCreate and reconnect
The provider is stateless: it does not cache sandboxes in memory. Callers are responsible
for persisting sandbox.id between calls (LangGraph handles this via its checkpointer).
// Create new sandbox
const sandbox = await provider.getOrCreate();
// Reconnect to an existing sandbox, or create a new one if it no longer exists
const sandbox = await provider.getOrCreate({
sandboxId: "existing-id", // from LangGraph state
labels: { customer: "acme" }, // auto-prefixed with langchain-kubernetes.bitkaio.com/
ttlSeconds: 3600,
ttlIdleSeconds: 600,
});
// List all managed sandboxes from the K8s API
const { sandboxes, cursor } = await provider.list();
const { sandboxes: running } = await provider.list({ status: "running" });
const { sandboxes: labelled } = await provider.list({ labels: { customer: "acme" } });
// Pagination:
const page2 = await provider.list({ cursor });
// Operational methods
const result = await provider.cleanup(); // CleanupResult
const result = await provider.cleanup(300); // override idle threshold (seconds)
const status = await provider.poolStatus(); // WarmPoolStatus
// Delete (idempotent)
await provider.delete(sandbox.id);GetOrCreateOptions:
| Field | Type | Description |
| ----- | ---- | ----------- |
| sandboxId | string \| undefined | Existing sandbox ID to reconnect (from graph state) |
| labels | Record<string, string> \| undefined | Per-call labels (auto-prefixed) |
| ttlSeconds | number \| undefined | Absolute TTL override (seconds) |
| ttlIdleSeconds | number \| undefined | Idle TTL override (seconds) |
SandboxListResponse:
interface SandboxListResponse {
sandboxes: SandboxInfo[];
cursor?: string; // Kubernetes continue token for pagination
}
interface SandboxInfo {
id: string;
namespace: string;
labels?: Record<string, string>;
annotations?: Record<string, string>;
createdAt?: string; // ISO-8601
phase?: string; // Kubernetes Pod phase
status?: string; // "running" | "warm" | "pending" | "terminated"
}CleanupResult / WarmPoolStatus:
interface CleanupResult {
deleted: string[]; // sandbox IDs that were deleted
kept: number; // sandboxes within their TTL / idle threshold
}
interface WarmPoolStatus {
available: number; // warm Pods ready to be claimed
active: number; // Pods currently assigned
total: number;
target: number; // configured warmPoolSize
}Additional configuration options
| Field | Type | Default | Description |
| ----- | ---- | ------- | ----------- |
| defaultLabels | Record<string, string> \| undefined | — | Labels applied to every sandbox (auto-prefixed with langchain-kubernetes.bitkaio.com/) |
| ttlSeconds | number \| undefined | — | Default absolute TTL for getOrCreate() |
| ttlIdleSeconds | number \| undefined | — | Default idle TTL for getOrCreate() |
| warmPoolSize | number \| undefined | — | Pre-created warm Pods (raw mode only) |
| warmPoolName | string \| undefined | — | SandboxWarmPool resource name (agent-sandbox only) |
Warm pool configuration
agent-sandbox mode:
const provider = new KubernetesProvider({
mode: "agent-sandbox",
routerUrl: "http://sandbox-router-svc.default.svc.cluster.local:8080",
templateName: "python-sandbox-template",
warmPoolName: "python-pool", // name of a SandboxWarmPool CRD in the cluster
});raw mode:
const provider = new KubernetesProvider({
mode: "raw",
warmPoolSize: 3, // pre-create 3 idle Pods, replenish after each delete
});See docs/warm-pool.yaml for cluster YAML examples.
agent-sandbox mode — setup
1. Install the controller
Follow the agent-sandbox installation guide.
2. Create a SandboxTemplate
# examples/k8s/sandbox-template.yaml
apiVersion: extensions.agents.x-k8s.io/v1alpha1
kind: SandboxTemplate
metadata:
name: python-sandbox-template
namespace: default
spec:
podTemplate:
spec:
containers:
- name: python-runtime
image: us-central1-docker.pkg.dev/k8s-staging-images/agent-sandbox/python-runtime-sandbox:latest-main
ports:
- containerPort: 8888
readinessProbe:
httpGet: { path: "/", port: 8888 }
periodSeconds: 1
resources:
requests: { cpu: "250m", memory: "512Mi" }
restartPolicy: "OnFailure"kubectl apply -f examples/k8s/sandbox-template.yaml3. Apply RBAC
kubectl apply -f examples/k8s/sandbox-router-rbac.yaml4. Connect
In-cluster (the primary use case):
const provider = new KubernetesProvider({
routerUrl: "http://sandbox-router-svc.default.svc.cluster.local:8080",
templateName: "python-sandbox-template",
});Local development — forward the sandbox-router to localhost:
kubectl port-forward svc/sandbox-router-svc 8080:8080 -n defaultconst provider = new KubernetesProvider({
routerUrl: "http://localhost:8080",
templateName: "python-sandbox-template",
// Optional: set kubeApiUrl for sandbox existence verification
// kubeApiUrl: "http://localhost:8001", // kubectl proxy
});raw mode — setup
1. Create the namespace
kubectl create namespace deepagents-sandboxes2. Apply RBAC
kubectl apply -f examples/k8s/raw-mode-rbac.yaml3. Configure
const provider = new KubernetesProvider({
mode: "raw",
namespace: "deepagents-sandboxes",
image: "python:3.12-slim",
blockNetwork: true, // deny-all NetworkPolicy (requires NetworkPolicy-capable CNI)
});For local kind clusters, kindnet does not support NetworkPolicy. Either use Calico or set blockNetwork: false.
Error handling
import {
SandboxNotFoundError,
SandboxStartupTimeoutError,
SandboxRouterError,
TemplateNotFoundError,
MissingDependencyError,
} from "@bitkaio/langchain-kubernetes";
try {
const sandbox = await provider.getOrCreate();
} catch (err) {
if (err instanceof TemplateNotFoundError) {
// SandboxTemplate doesn't exist in the cluster
} else if (err instanceof SandboxStartupTimeoutError) {
// Sandbox didn't become ready in time
} else if (err instanceof SandboxRouterError) {
// Router or Kubernetes API not reachable
} else if (err instanceof MissingDependencyError) {
// @kubernetes/client-node not installed (raw mode)
}
}Integration tests
Integration tests require a running cluster:
# raw mode (any cluster)
K8S_INTEGRATION=1 npm run test:integration
# agent-sandbox mode (requires controller + CRDs)
K8S_INTEGRATION=1 SANDBOX_TEMPLATE=python-sandbox-template \
ROUTER_URL=http://localhost:8080 npm run test:integrationRequirements
- Node.js ≥ 18
- Kubernetes cluster (kind, GKE, EKS, AKS, OpenShift, …)
- For
agent-sandboxmode:kubernetes-sigs/agent-sandboxcontroller - For
rawmode:@kubernetes/client-node+tar-stream; NetworkPolicy-capable CNI ifblockNetwork: true
