Going to Production¶
Running a
HarnessAgenton your laptop is easy. Shipping it to production is another story — replicas must share sessions, users must stay isolated, untrusted code must be sandboxed, and pods must be able to resume mid-conversation after a restart. This page only covers what changes between single-node and distributed production: which components must be swapped, what to swap them with, and why the builder throwsIllegalStateExceptionwhen you miss something.
Fastest path to production: use DistributedStore to configure all distributed components at once:
DistributedStore store = RedisDistributedStore.fromJedis(jedis);
// or MysqlDistributedStore.create(dataSource);
// or OssDistributedStore.create(ossClient, bucket, prefix);
HarnessAgent.builder()
.distributedStore(store)
.filesystem(...) // choose your workspace mode
.build();
Mixed stores (e.g. MySQL for state + Redis for sandbox locks) are also supported:
DistributedStore store = DistributedStore.builder()
.agentStateStore(MysqlDistributedStore.create(ds).agentStateStore())
.baseStore(MysqlDistributedStore.create(ds).baseStore())
.sandboxSnapshotSpec(RedisDistributedStore.fromJedis(jedis).sandboxSnapshotSpec())
.sandboxExecutionGuard(RedisDistributedStore.fromJedis(jedis).sandboxExecutionGuard())
.build();
At a glance: single-node defaults vs. distributed production¶
Dimension |
Single-node default (dev / demo) |
Distributed production swap |
|---|---|---|
One-line config |
not needed |
|
|
|
auto-wired by |
Filesystem |
|
|
Sandbox snapshots |
|
auto-wired by |
Sandbox exec serialization |
none needed in-process |
auto-wired by |
Skill source |
|
|
Observability |
no tracing by default |
|
DistributedStore capability matrix¶
Capability |
Redis ( |
OSS ( |
MySQL ( |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
— (object storage can’t do locks) |
|
Each component solves a different production problem:
AgentStateStore: persists the agent’s runtime session state, including conversation history, compaction summaries, permission rules, Plan Mode state, and tool state. This is what lets another replica, or a restarted process, continue the same(userId, sessionId).BaseStore: provides shared KV-backed workspace storage forRemoteFilesystemSpec, carrying paths such asMEMORY.md,memory/,skills/, andsessions/. In multi-replica deployments, it lets different pods see the same long-term memory and shared files.SandboxSnapshotSpec: persists sandbox workspace snapshots. When a sandbox container is destroyed, a pod restarts, or the next request lands on a new node, it restores the previous workspace instead of losingpip installoutput, generated files, or temporary project state.SandboxExecutionGuard: serializes command execution for the same sandbox slot across nodes. With shared scopes such asAGENTorGLOBAL, multiple replicas may try to execute against the same sandbox at once; the guard uses Redis/MySQL locking to avoid concurrent workspace writes and sandbox start/stop races.
OSS does not provide a
SandboxExecutionGuard— object storage is unsuitable for distributed locking. OSS users who need sandbox concurrency control can mix in a Redis guard viaDistributedStore.builder().
The key validation chain:
filesystem(RemoteFilesystemSpec)withoutstateStore(...)ordistributedStore(...)→build()throwsIllegalStateException.filesystem(SandboxFilesystemSpec)with a localAgentStateStore→build()logs a warning; in production always supply adistributedStore.
1. State store: put AgentState somewhere durable first¶
Recommended: use
distributedStore(...)for one-line setup. The detailed table below is for advanced users who need individual control overAgentStateStore.
AgentState (conversation context, compaction summary, permission rules, Plan Mode state, tool state) only survives across processes through an AgentStateStore.
Implementation |
Module |
When to use |
|---|---|---|
|
|
unit tests; everything dies on process exit |
|
|
single-machine dev; one directory per |
|
|
multi-replica production default; supports Jedis / Lettuce / Redisson (Standalone / Cluster / Sentinel) |
|
|
when state must live in a relational store (audit / reporting / joins) |
Redis with any of the three client adapters through RedisAgentStateStore.builder():
import io.agentscope.core.state.AgentStateStore;
import io.agentscope.extensions.redis.state.RedisAgentStateStore;
import redis.clients.jedis.JedisPooled;
// Jedis Standalone
AgentStateStore stateStore = RedisAgentStateStore.builder()
.jedisClient(new JedisPooled("redis://localhost:6379"))
.keyPrefix("myapp:session:")
.build();
// Lettuce Cluster (better for write-heavy)
// .lettuceClusterClient(RedisClusterClient.create(...))
// Redisson (if you already use Redisson elsewhere)
// .redissonClient(redisson)
Per-tenant isolation. A bare sessionId only covers single-tenant. In production, set both userId and sessionId on each call’s RuntimeContext so multi-tenant calls can’t cross-read — the store addresses each slot by the (userId, sessionId) pair (RedisAgentStateStore folds userId into the Redis key; MysqlAgentStateStore folds it into the primary key). Compose any other dimensions (tenant, agent) into the sessionId string yourself:
agent.call(msg, RuntimeContext.builder()
.userId(tenantId + ":" + userId)
.sessionId(agentId + ":" + sessionId)
.build()).block();
Full mechanics in Context & AgentState.
3. Remote-mode BaseStore stores: KV choice — and why OSS is the wrong fit¶
RemoteFilesystemSpec sits on top of a BaseStore interface. Two built-in implementations:
Implementation |
Dependency |
Concurrency safety |
Use it when |
|---|---|---|---|
|
|
Lua-based CAS |
the default; multi-replica sharing |
|
|
single-statement CAS UPDATE |
existing relational infra / need joins |
|
— |
— |
tests |
// Recommended: one-line configuration via DistributedStore
DistributedStore store = RedisDistributedStore.fromJedis(
new JedisPooled("redis://prod-redis:6379"));
HarnessAgent agent = HarnessAgent.builder()
.name("multi-tenant-agent")
.model(model)
.workspace(workspace)
.distributedStore(store) // auto-wires stateStore + baseStore
.filesystem(new RemoteFilesystemSpec() // baseStore injected from store
.isolationScope(IsolationScope.USER)
.workspaceIndex(WorkspaceIndex.open(workspace))) // speeds up ls/glob
.build();
// Or with MySQL:
DistributedStore mysqlStore = MysqlDistributedStore.create(dataSource);
What about OSS / NAS / S3?¶
Do not implement a BaseStore against OSS — MEMORY.md / memory/YYYY-MM-DD.md / agents/<id>/context/<sid>/ get written several times a second; OSS latency and per-request cost will blow up immediately. The correct division of labour:
Data shape |
Store |
Owner |
|---|---|---|
High-frequency small KV (memory, session snapshots, task records) |
Redis / MySQL ( |
|
Large objects (whole sandbox workspace tar, tens of MB) |
OSS / S3 |
|
Cross-node shared volume (multiple sandbox instances mounting the same dir) |
NAS / EFS |
|
RemoteFilesystemSpec routing table¶
To prevent key collisions across subsystems, the spec slices the workspace into independent namespace segments:
Workspace path |
Namespace segment |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Extra: |
derived automatically |
Each segment is then bucketed by IsolationScope (USER → agents/<agentId>/users/<userId>/). A Redis key ends up looking like agentscope:store:item:agents\0X\0users\0alice\0memory\0memory/2026-06-02.md.
CompositeFilesystem: two-layer reads + write-through¶
RemoteFilesystemSpec.toFilesystem(...) actually produces a CompositeFilesystem: a base LocalFilesystem without shell (fallback for local templates) plus one OverlayFilesystem per route (upper = RemoteFilesystem, lower = read-only LocalFilesystem template).
Effect: writes always go to Remote; reads check Remote first, fall back to the local template. That is the “two-layer read architecture” described in Workspace instantiated for Remote mode — the local <workspace>/AGENTS.md is a seed (synced via team git), and Remote takes over as soon as it has been written to.
WorkspaceIndex: optional SQLite index¶
.filesystem(new RemoteFilesystemSpec(store).workspaceIndex(WorkspaceIndex.open(workspace)))
Speeds up ls / glob / exists / grep under Remote mode — without it every call scans the full KV. WorkspaceIndex is a best-effort SQLite file (under <workspace>/.index/), failures degrade silently without affecting correctness.
4. Skill marketplaces: which SkillRepository to pick¶
Skills compose from low to high priority (details in Skill):
Layer |
Source |
Configured by |
Use it for |
|---|---|---|---|
1 |
Project global |
|
personal dev box; |
2 |
Marketplace |
|
cross-project sharing |
3 |
Workspace shared |
|
project-specific; checked into git |
4 |
Per-user |
|
user-level override |
Marketplace stores¶
Repository |
Module |
Notes |
Best for |
|---|---|---|---|
|
|
team git repo; pulls only when HEAD changes; read-only distribution |
early stage / small teams; review skill changes via PR |
|
|
|
platform-side central governance; multi-team multi-agent |
|
|
online distribution + config-center change subscription; |
Aliyun ecosystem; “change once, take effect fleet-wide” |
|
|
shipped with the JAR; Spring Boot fat-JAR compatible |
hard-bound capabilities baked into the product |
HarnessAgent agent = HarnessAgent.builder()
// ...
.skillRepository(new GitSkillRepository("https://github.com/your-org/team-skills.git"))
.skillRepository(MysqlSkillRepository.builder(dataSource)
.databaseName("agentscope")
.skillsTableName("skills")
.createIfNotExist(true)
.writeable(false) // read-only distribution; recommended for production
.build())
.build();
skillRepository(...) is additive; later registrations win on name collisions.
Production checklist¶
Prefer
MysqlSkillRepository(writeable=false)orNacosSkillRepository— platform-side central governance, agents read-only; write-backs go through an admin console + review flow.Don’t want the agent to see
workspace/skills/?.disableDefaultWorkspaceSkills().When
enableSkillManageToollets the agent draft new skills, always pair it withenableSkillPromotionGate(...); neverautoPromote=truein production.NacosSkillRepositoryisAutoCloseable— close it from Spring@PreDestroyor atry-with-resources, otherwise subscriptions leak.
5. When you need shell: pick a Sandbox + mandatory Snapshot¶
When you must use a sandbox:
the model might run untrusted code (Python / shell /
npm install/ compilation)you need to recover the entire working directory across calls (
node_modules, generated files, post-pip installenvironment)you need hard user isolation (no peeking into another user’s processes)
Five sandbox stores¶
Spec |
Module |
Use it for |
|---|---|---|
|
|
single-machine / local cluster; container from an image; most familiar |
|
|
already running K8s; pods / Jobs |
|
|
Daytona (dev-env-as-a-service) |
|
|
E2B cloud sandboxes; fastest to ship, no self-managed infra |
|
|
Aliyun AgentRun; native NAS / OSS mounts; enterprise-grade |
.filesystem(new DockerFilesystemSpec()
.image("ubuntu:24.04")
.isolationScope(IsolationScope.SESSION))
Snapshots are the sandbox’s distributed lifeline¶
Sandboxes are ephemeral by default — the next call() may land on a different node in a fresh container, losing every pip install and generated file. SandboxSnapshotSpec archives the workspace as tar so the next call() hydrates it back into a new container.
Spec |
Store |
Module |
When to use |
|---|---|---|---|
|
— |
|
not for production; sandbox cold-starts every time the container is lost |
|
local directory |
|
single-node debugging |
|
Alibaba Cloud OSS |
|
large objects first choice; natural fit for object storage |
|
Redis |
|
small workspaces + short TTL (watch Redis memory cost) |
|
MySQL / JDBC BLOB |
|
existing relational DB, no extra middleware |
Custom |
S3 / GCS / MinIO |
— |
anything not in the built-in list |
DistributedStore redisStore = RedisDistributedStore.fromJedis(jedis);
DistributedStore ossStore = OssDistributedStore.create(
ossClient,
"agentscope-sandbox-snapshots",
"prod/"); // key prefix for environment isolation
DistributedStore store = DistributedStore.builder()
.agentStateStore(redisStore.agentStateStore())
.baseStore(redisStore.baseStore())
.sandboxSnapshotSpec(ossStore.sandboxSnapshotSpec())
.sandboxExecutionGuard(redisStore.sandboxExecutionGuard())
.build();
HarnessAgent agent = HarnessAgent.builder()
.name("coding-agent")
.model(model)
.workspace(workspace)
.distributedStore(store)
.filesystem(new DockerFilesystemSpec()
.image("python:3.12-slim")
.isolationScope(IsolationScope.USER))
.build();
With distributedStore(...), the snapshot spec and execution guard are auto-injected — no manual configuration needed. To customize the OSS bucket or prefix, prefer configuring OssDistributedStore when you create it; only set SandboxSnapshotSpec explicitly on SandboxFilesystemSpec when you need a fully custom snapshot implementation.
Sandbox exec serialization: SandboxExecutionGuard¶
Under SESSION / USER scope, buckets are already partitioned by session/user and concurrent execs don’t collide. Under AGENT / GLOBAL scope with multiple replicas, N nodes can race to exec on the same sandbox slot. distributedStore(...) auto-injects the appropriate execution guard:
Implementation |
Module |
Mechanism |
|---|---|---|
|
|
Redis |
|
|
MySQL |
The recommended path is still to inject the guard through DistributedStore:
DistributedStore store = RedisDistributedStore.fromJedis(jedis);
HarnessAgent.builder()
.distributedStore(store)
.filesystem(new DockerFilesystemSpec()
.image("ubuntu:24.04")
.isolationScope(IsolationScope.GLOBAL))
.build();
Only override the guard explicitly when you need custom lock parameters, such as a lease TTL:
HarnessAgent.builder()
.distributedStore(store)
.filesystem(new DockerFilesystemSpec()
.image("ubuntu:24.04")
.isolationScope(IsolationScope.GLOBAL)
.executionGuard(RedisSandboxExecutionGuard.builder(jedis)
.leaseTtl(Duration.ofMinutes(30))
.build()))
.build();
You can also implement SandboxExecutionGuard yourself to plug in Zookeeper, etcd, or any other lock mechanism.
Workspace projection: pushing seed files into the sandbox¶
SandboxFilesystemSpec projects AGENTS.md, skills, subagents, knowledge, .skills-cache (five roots) into the sandbox at start time by hydrating a content-hashed tar archive (incremental rewrites). Tweak it:
.filesystem(new DockerFilesystemSpec()
.image("...")
.workspaceProjectionRoots(List.of("AGENTS.md", "skills", "knowledge")) // drop subagents/.skills-cache
// .workspaceProjectionEnabled(false) // fully disable
)
AgentRun-specific: NAS / OSS mounts¶
AgentRunFilesystemSpec is the only sandbox filesystem that natively supports multiple sandbox instances mounting the same directory (via NAS). When the business case is “one user sees the same workspace across different sessions”, AgentRun + NAS is more efficient than re-hydrating snapshots every time:
.filesystem(new AgentRunFilesystemSpec()
.apiKey(System.getenv("AGENTRUN_API_KEY"))
.accountId(System.getenv("ALI_ACCOUNT_ID"))
.region("cn-hangzhou")
.templateName("python-3.12")
.nasConfig(new AgentRunNasMountConfig().fileSystemId("...").mountTargetDomain("...").mountDir("/workspace"))
.addOssMount(new AgentRunOssMountConfig().bucketName("data").mountDir("/mnt/oss")))
Full fields in the AgentRunNasMountConfig / AgentRunOssMountConfig source.
6. Multi-replica deployment checklist (combined)¶
Pulling the single-component picks above into one table:
Concern |
Recommended combo |
|---|---|
Sessions / |
|
Workspace files |
|
Large objects / snapshots |
Use |
Cross-node sandbox sharing |
AgentRun + NAS mount, or self-managed K8s + |
Skill governance |
|
Subagent task records |
automatic via |
Exposed subagents (user talks to a subagent directly) |
registry auto-wired by |
Graceful shutdown |
|
Observability |
|
Rate limiting |
custom |
7. A complete production builder template¶
The agent is stateless between calls — a singleton handles concurrent requests. Each call() locates state via RuntimeContext’s (userId, sessionId), fully isolated.
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.extensions.redis.RedisDistributedStore;
import io.agentscope.core.tracing.OtelTracingMiddleware;
import io.agentscope.harness.agent.DistributedStore;
import io.agentscope.harness.agent.HarnessAgent;
import io.agentscope.harness.agent.IsolationScope;
import io.agentscope.harness.agent.sandbox.impl.docker.DockerFilesystemSpec;
import io.agentscope.core.memory.compaction.CompactionConfig;
import io.agentscope.core.memory.compaction.ToolResultEvictionConfig;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import redis.clients.jedis.JedisPooled;
// --- Dependencies (create once at startup) ---
Path workspace = Paths.get("/var/agentscope/workspace");
JedisPooled jedis = new JedisPooled(System.getenv("REDIS_URI"));
DistributedStore store = RedisDistributedStore.fromJedis(jedis);
// --- Singleton agent (created once at startup) ---
HarnessAgent agent = HarnessAgent.builder()
.name("coding-assistant")
.model("dashscope:qwen-plus")
.workspace(workspace)
.distributedStore(store) // auto-wires stateStore + snapshotSpec + executionGuard
.filesystem(new DockerFilesystemSpec()
.image("python:3.12-slim")
.isolationScope(IsolationScope.USER))
.compaction(CompactionConfig.builder()
.triggerMessages(50)
.keepMessages(20)
.build())
.toolResultEviction(ToolResultEvictionConfig.defaults())
.skillRepository(io.agentscope.core.skill.repository.mysql.MysqlSkillRepository
.builder(skillsDataSource())
.createIfNotExist(false)
.writeable(false)
.build())
.middlewares(List.of(new OtelTracingMiddleware()))
.build();
At call time, pass RuntimeContext to identify the user/session. Different sessions run concurrently on the same agent instance:
// In your HTTP handler
agent.call(msg, RuntimeContext.builder()
.userId(httpRequest.tenantUserId())
.sessionId(httpRequest.sessionId())
.build()).block();
8. Common pitfalls¶
Forgetting to pass
RuntimeContext— without asessionId, all requests share thedefaultSessionIdstate, causing cross-talk. In multi-user scenarios, always passRuntimeContext.builder().userId(...).sessionId(...).build()to everycall()to ensure state isolation. See Agent — Multi-user Concurrency.java.nio.Filesfor workspace writes — under sandbox / Remote mode this lands in the wrong place. Always go throughagent.getWorkspaceManager(). Exception: builder-time seed files (initWorkspaceIfAbsent-style code) — no runtime context yet,java.nio.Filesis correct because you’re seeding the local template.tools.json’sallowfilters built-in tools too — when whitelisting, keepread_file/memory_search/agent_spawnand friends in the list, or every built-in gets stripped.IsolationScopechanges do not migrate existing data — pin it before launch. Changing it post-launch is equivalent to switching to a new namespace.Local
AgentStateStoresingle-machine constraint: a K8s multi-replica build that pairs a distributed filesystem with a localJsonFileAgentStateStorethrowsIllegalStateExceptionon the very firstbuild(). This is intentional — you can’t park agent state on one pod’s local disk.NacosSkillRepositorynot closed — subscriptions leak; at fleet scale Nacos complains. Use Spring@PreDestroyordestroyMethod="close".OSS / NAS without IAM —
OssSnapshotSpectakes platform AK/SK; RAM Role + STS temporary credentials is more robust.Local
AgentStateStorewith sandbox mode is dev-only — the build-time warning is intentional; don’t ignore it in production.