Skip to main content

How this page relates to context management

This page explains where session state lives, how it is persisted and restored, and how tools access the current call’s state. For preparing each model request, adding business information, tracking tasks, and compacting history, see Context management. Harness projects task state into transient TASK_STATE and loads MEMORY.md as reference data. These generated messages are not thereby appended to durable history. Plain ReActAgent does not automatically enable the Harness material and budget policies.

Stateless Agent Engine

ReActAgent and HarnessAgent separate model/tool configuration from conversation state. Configure a shared Builder at startup and build and close an independent Agent per request. Stable identity and storage continue a conversation without retaining the previous request’s Java object. Lifecycle guidance and the shared-instance option are centralized in Agent. Instances still own execution gates, caches and background resources, and should be closed after execution. A new instance restores committed state. If a plain Core Agent has no log or state store configured, history held only in the old instance is not transferred. Tools and middleware access the current session through the injected RuntimeContext.getAgentState(), without a global current-state field. Creating an Agent does not copy all business dependencies or replace authorization. Shared dependencies must support concurrent access. The application coordinates execution of the same conversation across instances; see Concurrent usage.

AgentState

An AgentStateStore persists an AgentState (io.agentscope.core.state.AgentState) — a complete snapshot of everything that makes the agent restartable: Execution controls are separate from AgentState: each invocation owns an independent interrupt signal. See Per-session interrupt below. In snapshot mode or Core agents without native logging, the end of each call writes the entire AgentState into the store’s (userId, sessionId) bucket. The entry is agent.threadKey(context).stateEntry("agent_state"), which includes the Agent and Thread identity. The next call with the same Thread identity restores it; sibling Threads have independent history, permissions and plan state. Set RuntimeContext.builder().threadId("research") to select a Thread, or omit it for main. The public sessionId remains unchanged. A shared store lets other instances load successfully persisted state. It does not guarantee real-time consistency between concurrent calls or recover unsaved progress and external side effects.

Task state is not automatic business acceptance

Todo is updated by todo_write or the application; the application sets task identity and objective through beginTask. The optional proposal tool only creates candidates; a trusted caller confirms or rejects through decide. The application maintains subject versions, and explicit VerificationService calls produce verification summaries. Persisting fields does not automatically infer them from user messages, PLAN.md or tool text. Progress completion, confirmed requirements and passed checks have different meanings; none automatically establishes overall completion. Harness taskContext options control projection. See Optional task information for the field/writer mapping and display configuration.

Thread journal and AgentStateStore

Durable tasks use turn.pause(requestId), then turn.resume(requestId, revision) after observing PAUSED. Cancellation uses turn.cancel. These persistent controls differ from interruption of one native invocation; see Durable. HarnessAgent defaults to journal mode: existing native checkpoints plus applicable facts restore AgentState, and saves commit checkpoints to the log. AgentStateStore is no longer that session’s recovery authority. snapshot mode and Core agents without native logging use the state-store flow below. Native history follows Workspace Filesystem, including distributed backends, or an explicit threadJournalStore. Keep stable agentId, userId, sessionId, threadId, namespace and storage consistent. See Session logs for inspection, reconciliation and continuation. StateStore-only examples below explicitly select snapshot mode; default journal mode also needs shared native history.

The auto-persistence and recovery flow

This wiring lives in 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).
Reasoning mainly updates in-memory state. Normal completion, controlled failure/interruption and shutdown paths attempt to save agent_state; forced process termination may prevent saving. Administrative operations such as clearContext can also save explicitly. Execution observations and verification reports write separate state keys, so the store does not necessarily receive only one write per call.

Built-in and extension implementations

Anything implementing io.agentscope.core.state.AgentStateStore works. Pick by deployment shape: Switching is one call at builder time:
The built-in JsonFileAgentStateStore / InMemoryAgentStateStore are single-host only. If you’ve already chosen filesystem(SandboxFilesystemSpec) or filesystem(RemoteFilesystemSpec) (distributed workspace), HarnessAgent in snapshot mode mode rejects a local state store at build time with IllegalStateException — sandbox state must be shared across replicas. Configure a distributed store via .distributedStore(...) (e.g. RedisDistributedStore) or .stateStore(...).

Resume saved sessions across processes and machines

With a shared store, each call reloads the slot’s persisted state. This example assumes node A has finished saving before node B continues, not concurrent execution of the same session:
This buys you:
  • Recovery: another node can load the last successfully saved snapshot. Unsaved progress may be lost; reconcile external tool effects before replaying work.
  • Rolling deploys: graceful shutdown attempts to save, and a new instance loads persisted state. Allow shutdown time and verify that state formats and business configuration remain suitable.
  • Cross-surface continuity: Web UI and CLI can continue saved sessions with the same store and identity slot; the application must authorize access.
The identity pair selects the state slot. Anonymous/single-tenant calls may omit userId; multi-tenant applications must obtain it from trusted authentication, not arbitrary client input. Cross-instance concurrency also needs session routing or distributed coordination. Versioned stores use CAS on save, with ConflictPolicy determining conflict handling; unversioned stores lack this protection. CAS does not undo external tool side effects.

Multi-user isolation

sessionId and userId solve different problems:
  • sessionId — which conversation this is; independent AgentState snapshot.
  • userId — which user owns this conversation; also drives which user’s namespace files land in, see Filesystem.
Different identity pairs select different state slots; filesystem sharing separately depends on IsolationScope. Set RuntimeContext.userId after authentication and authorization: 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

For out-of-loop reads such as administration or auditing, address state by identity. Do not mutate it concurrently with an active call. Obtaining or changing the object does not itself persist it or provide a transaction or authorization check:

Clearing a session’s conversation context

In journal mode mode, getAgentState() returns a detached native projection. clearContext(), permission changes and saveAgentState() commit a native checkpoint with writer leases and version checks. Existing get→mutate→save calls remain supported, but stale snapshots are rejected; prefer updateAgentState(rc, reason, mutation) for atomic administration. Clearing working context retains full history. Direct AgentStateStore edits do not affect native sessions. To let a user start a fresh topic without creating a new session, call clearContext. It keeps the same (userId, sessionId) and preserves non-conversation state such as permissions, tools, tasks, and Plan Mode. It clears the historical message buffer and compaction summary, then immediately persists a native checkpoint in journal mode mode, or saves the configured AgentStateStore in snapshot mode mode.
Call it after the session’s current request has completed; it does not cancel an in-flight call. The next request can still load System, workspace references and retained task/plan projections. It is not a complete model-input reset. For a fresh session, use a new sessionId rather than merely clearing history.
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

Each execution owns a runtime-only InterruptControl. It is neither stored on AgentState nor persisted with conversation history. A session-targeted interrupt resolves the currently admitted execution:
Durable tasks use turn.pause(requestId), then turn.resume(requestId, revision) after observing PAUSED. Cancellation uses turn.cancel. These persistent controls differ from interruption of one native invocation; see Durable. The reasoning loop checks its execution’s signal at cooperative checkpoints. A user interrupt produces an interrupted recovery reply and saves conversation state. The deprecated no-argument interrupt() targets the default session’s current execution, never the most recently used context. AgentState.shutdownInterrupted is a separate, persisted recovery marker. Graceful shutdown binds both the execution control and the state resolved for that call; queued calls have no state to save. No interrupt flag is carried into the next run or loaded on another node.

Concurrent usage

Configure this Builder at startup, then build separate instances for Alice and Bob. The workspace must persist between requests; replicas also need access to the same native log backend.
Applications coordinate concurrent direct calls on one Thread; Durable provides persistent FIFO and autonomous takeover. Native journal fencing prevents conflicting writes without providing a background task scheduler. Interrupting from another request requires the currently executing instance; a new Agent does not hold the old call’s interrupt signal. Close request Agents after execution to release their caches and runtime resources. Close application-owned model clients and pools at application shutdown.

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 automatically persisted or included in model messages; 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().
Available accessors:

runId correlation

A request context identifies a user, Session and Thread without creating an execution. Its getRunId() is null unless a host explicitly supplies an invocation ID. prepareRun / prepareCall allocate an ID for their single-use handle; direct calls allocate one when execution starts. Middleware and tools receive an invocation context whose runId matches the handle, native events and journal.
An unbound request context can be reused for another call: each invocation receives a new Run ID. A host can supply builder().runId("invocation-1"); it must supply a fresh ID for every actual invocation. Null clears an inherited ID; blank IDs are rejected. A native journal rejects reusing an explicitly supplied Run ID. Subagents receive distinct invocation IDs. Parent linkage records the originating Run/Thread separately; use a separate tracing attribute when a business trace spans multiple invocations. A resumed logical task keeps its Turn identity and starts a new Run.
The AgentStateStore is bound at builder time and cannot be switched per call via RuntimeContext. What does vary per call is the (userId, sessionId) slot it addresses — set userId for per-user isolation (or a custom keyPrefix on the store); do not try to hand each call a different state store instance.
Accessing AgentState from middleware and tools: Always use RuntimeContext.resolveAgentState(ctx, agent) rather than agent.getAgentState() during call execution. The deprecated no-argument agent.getAgentState() returns the anonymous default-session slot, not the last active session. ctx.getAgentState() is the current call’s state. resolveAgentState still falls back when it is absent; fallback is not a session-routing guarantee.

  • Context management — organize model input, track task progress, and bound context through summarization and tool-result offloading
  • Agent — full ReActAgent API and builder fields
  • Memory — long-term memory, background maintenance
  • Permissions — persistence of permission rules