Stateless Agent Engine
ReActAgent (and HarnessAgent that wraps it) is designed as a stateless engine: the agent instance itself holds only immutable configuration — system prompt, model, tools, middleware chain — while all per-session mutable data lives in AgentState, indexed by (userId, sessionId). A single agent instance can concurrently serve many users and sessions; the caller simply passes a different RuntimeContext on each call().
What this means for you
- No agent-per-user registry. One
HarnessAgentinstance can serve all your users — just varyRuntimeContext.userIdandRuntimeContext.sessionIdper request. - Concurrency is built in. Different
(userId, sessionId)pairs run fully in parallel; the same pair is automatically serialised to preserve conversation consistency. - State is fully internal. The agent loads
AgentStatefrom the store at call entry and saves it at call exit — the caller never manages state objects directly. - Per-call isolation. Each
call()works on its ownAgentStatesnapshot. Middleware and tools access the call-scoped state viaRuntimeContext.getAgentState()(injected by the framework at call entry), so concurrent calls never see each other’s state.
AgentState
AnAgentStateStore persists an AgentState (io.agentscope.core.state.AgentState) — a complete snapshot of everything that makes the agent restartable:
AgentState also carries a transient, non-serialised InterruptControl for per-session interrupt signalling — see Per-session interrupt below.
At the end of each call(), the framework writes the entire AgentState to the state store under the key agent_state, addressed by the call’s (userId, sessionId). The next call() with the same (userId, sessionId) loads it back automatically. Provided the state store is distributed (e.g. Redis), agent instances on different processes — even different physical machines — see identical state.
The auto-persistence and recovery flow
ReActAgent itself; HarnessAgent inherits it for free. The agent instance holds no fixed session — each call reads / writes the slot named by its RuntimeContext (falling back to the builder-time defaultSessionId).
Mid-call()state changes happen against the in-memoryAgentState. The state store is written once per call (and on shutdown), not on every message — so the throughput pressure on your store stays low.
Built-in and extension implementations
Anything implementingio.agentscope.core.state.AgentStateStore works. Pick by deployment shape:
Switching is one call at builder time:
Real-time resume across processes and machines
Once the state store is distributed (e.g. Redis), cross-machine resume is automatic:- Failover: a crashed node — conversations migrate to a healthy one, user notices nothing.
- Rolling deploys: old pods save on shutdown, new pods load on first call — conversations never break across releases.
- Cross-surface continuity: a user starts in the Web UI, switches to the CLI — same
(userId, sessionId), all memory present.
(userId, sessionId) pair defines the namespacing: sessionId alone is enough for most cases; add userId when you need per-user partitioning.
Multi-user isolation
sessionId and userId solve different problems:
sessionId— which conversation this is; independentAgentStatesnapshot.userId— which user owns this conversation; also drives which user’s namespace files land in, see Filesystem.
AgentState-level user isolation in production, set userId on the RuntimeContext: the store addresses each slot by (userId, sessionId) (with RedisAgentStateStore the userId becomes part of the Redis key) rather than relying on filesystem path bucketing.
Reading and writing AgentState directly
When you need to bypass the agent loop (admin console, audit, batch migration):
Clearing a session’s conversation context
To let a user start a fresh topic without creating a new session, callclearContext. It keeps the
same (userId, sessionId) and preserves non-conversation state such as permissions, tools, tasks,
and Plan Mode. It clears the model-visible message buffer and compaction summary, then immediately
persists the result when the agent has an AgentStateStore.
The 1.0
Memory interface (InMemoryMemory / LongTermMemory, etc.) is @Deprecated(forRemoval = true) in 2.0. New code should use AgentState.getContext() + an AgentStateStore; Memory remains only as a source-compat shim.Per-session interrupt
EachAgentState carries a transient InterruptControl (io.agentscope.core.interruption.InterruptControl) — a per-session interrupt signal that is never serialised to the state store (marked @JsonIgnore transient on AgentState). This allows targeted interruption of a single session’s in-flight call without affecting other concurrent calls on the same agent instance.
state.interruptControl().isInterrupted() before each iteration. When triggered, the loop enters the handleInterrupt path, which saves state and returns the partial result.
The legacy no-arg interrupt() still works for single-session scenarios — it routes to the currently active session’s InterruptControl.
InterruptControl is a runtime-only signal; it is never persisted. If a session resumes on a different node after failover, the interrupt flag starts cleared. The separate AgentState.shutdownInterrupted flag (which is persisted) records whether the session was interrupted by graceful shutdown — the agent can detect and recover from that on next load.Concurrent usage
Because the agent is a stateless engine, a single instance handles concurrent requests naturally:- Different
(userId, sessionId)→ fully parallel, each call works on its ownAgentState. - Same
(userId, sessionId)→ per-session async gate serialises calls in FIFO order — state consistency guaranteed without external locking. interrupt(userId, sessionId)→ targets exactly one session, other in-flight calls unaffected.
RuntimeContext — per-call metadata
RuntimeContext (in io.agentscope.core.agent) is a lightweight per-call carrier passed to agent.call(msgs, ctx); hooks and tools share it for the duration of one call. Its free-form / typed attributes are not persisted; its sessionId / userId fields select which AgentState slot the state store loads and saves for this call. At call entry, the framework injects the call-scoped AgentState onto the RuntimeContext so that middleware, tools, and hooks can access the correct per-call state via ctx.getAgentState().
Related pages
- Agent — full
ReActAgentAPI and builder fields - Context Compaction — conversation summarization, tool-result eviction, overflow recovery (builds on top of the AgentState foundation described here)
- Memory — long-term memory, background maintenance
- Permissions — persistence of permission rules