Overview
Agent (interface at io.agentscope.core.agent.Agent, default implementation ReActAgent) is the core abstraction — a reasoning-acting loop engine that integrates models, tools, the permission system, human-in-the-loop, context management, middlewares, state management, and the event system into a single unified interface.
Its primary responsibilities are:
- Receive input messages or events; orchestrate tools to complete tasks.
- Manage context (conversation history is held on
AgentState.getContext()and can be persisted automatically via anAgentStateStore). - Provide middleware hooks at key lifecycle points for custom logic.
- Manage concurrent and sequential tool execution automatically.
Core interface
TheAgent interface composes three capability interfaces: CallableAgent, StreamableAgent, ObservableAgent. The most commonly used methods:
ReActAgent adds overloads for structured output (call(msgs, structuredOutputClass, runtimeContext)) and convenient per-call metadata via RuntimeContext.
Main loop
Eachcall runs through the reasoning-acting loop. The diagram below shows the main control flow:
Configuring an agent
Build an agent withReActAgent.builder()...build(). .model(...) takes either a ModelRegistry-resolved string id (most common — picks up env vars automatically) or an explicit Model instance (when you need explicit control over timeouts / custom endpoints / etc.).
- String model id (recommended)
- Explicit Model builder
- With Toolkit / MCP
Builder fields
Multi-user / multi-session concurrency
ReActAgent is stateless between calls — a single instance can serve multiple users and sessions concurrently. Each call() uses the (userId, sessionId) carried by its RuntimeContext to locate the correct conversation state; different sessions are fully isolated.
call(), the agent automatically loads the AgentState (conversation context, permission rules, etc.) for the given (userId, sessionId). When the call finishes, the state is saved back. Different sessions are completely isolated.
A complete Spring Boot example: agentscope-examples/documentation/.../streaming/StreamingWebExample.java.
Interrupt
To cancel an in-flight call from the outside (user cancellation, timeout, graceful shutdown), useinterrupt:
(userId, sessionId) — other concurrent sessions on the same agent are unaffected.
What happens after interrupt:
- The current reasoning/tool execution is stopped at the next checkpoint (start of reasoning, start of acting, each streaming chunk)
- The agent returns a Msg tagged with
GenerateReason.INTERRUPTED - The conversation state (AgentState) is saved automatically — the next
call()to the same session resumes from the interruption point
(userId, sessionId) strings:
Running an agent
call and streamEvents accept the same input messages and drive the same reasoning-acting loop. They differ in how the result is delivered.
call
call consumes all events internally and returns the final Msg when the agent finishes or pauses for external interaction.
streamEvents
streamEvents emits AgentEvents one by one so you can stream text, tool-call progress, and lifecycle events to your UI in real time. Dispatch on event.getType() to handle each kind:
observe
Useobserve to inject a message into the agent’s context without triggering a reply — useful in multi-agent setups where one agent observes another agent’s output.
RuntimeContext (per-call context)
RuntimeContext (io.agentscope.core.agent.RuntimeContext) is a per-call metadata bag: pass one instance to call / stream, and the agent binds it for the duration of that call so downstream tools, middlewares, and hooks all observe the same reference. The framework unbinds it on completion.
It is not persistent state — AgentState (conversation context, compressed summaries, permission rules, tool state) covers that. RuntimeContext carries data that is scoped to a single invocation: tenant / userId / request-id, DB connections, audit loggers, feature flags, and so on.
Built-in fields and attribute layers
RuntimeContext exposes three kinds of slot:
Typed attributes power tool injection — declare a parameter of the matching type on a
@Tool method and the framework supplies the value. See Tool — Receiving context. String attributes are typically used for in-process coordination (e.g. middleware-to-middleware signalling). The two layers are isolated: typed values do not appear in getExtra() and vice-versa.
Construct and pass
ReActAgent provides RuntimeContext overloads for call and stream; streamEvents does not — when you need a context with the event stream, use stream(msgs, options, ctx), or configure a global toolExecutionContext on the builder. When no context is passed the framework substitutes RuntimeContext.empty() (null session fields, empty attribute maps), and the agent falls back to its builder-time defaultSessionId.
Who reads it
- Tools (
@Toolmethods andToolBase.callAsync) — see Tool — Receiving context. - Middleware (every
MiddlewareBasehook) — received as the second parameterctx. See Middleware — Reading RuntimeContext. - All threads within the same call — the internal maps are
ConcurrentMaps, so hooks and tools can read/write the same instance to coordinate.
Relation to persistence
- Free-form / typed
RuntimeContextattributes never enterAgentStateand are never written back by theAgentStateStore. - The
sessionId/userIdfields do drive persistence: each call activates the(userId, sessionId)state slot, so passing different identities onRuntimeContextretargets whichAgentStateis loaded and saved. When absent, the agent falls back to its builder-timedefaultSessionId.
agentscope-examples/documentation/.../context/RuntimeContextExample.java, tool/ToolExecutionContextExample.java.
A legacy
ToolExecutionContext (io.agentscope.core.tool) is @Deprecated. New code should use RuntimeContext. The legacy type is bridged automatically via RuntimeContext.asToolExecutionContext(), so existing code keeps working.Human-in-the-loop
The agent pauses and emits a special event in two cases: a tool call requiring user confirmation (the permission system returned ASK), or a tool marked as external execution (the result must come from outside the agent). In both cases, you resume the agent by feeding the result back through the nextcall.
User confirmation
When the permission system decides a tool call needs user approval, the agent emitsRequireUserConfirmEvent and pauses.
1. Receive RequireUserConfirmEvent — use streamEvents to detect the pause. The event carries getReplyId() (used to resume) and getToolCalls() — a list of ToolUseBlock each exposing getId() / getName() / getInput() / getSuggestedRules().
ConfirmResult per pending call. You can tweak the tool input on the way back, or accept the suggested rules so identical future calls auto-allow:
confirmResults to the next call via metadata:
- Confirmed tool calls execute immediately; the agent continues reasoning.
- Denied tool calls produce an error result visible to the LLM, which may try a different approach.
- Accepted rules are persisted in the permission engine — matching future calls will be auto-allowed without prompting.
External tool execution
When the agent invokes a tool withisExternalTool() == true, it emits RequireExternalExecutionEvent and pauses. The tool’s logic runs outside the agent — typically by a human operator or external system.
1. Receive RequireExternalExecutionEvent — same shape as user confirmation: getReplyId() plus a list of getToolCalls() awaiting external execution.
ToolResultBlock:
call’s input message. After the results are validated, they are injected into the agent context and the agent emits ExternalExecutionResultEvent; its getReplyId() matches the earlier RequireExternalExecutionEvent#getReplyId(). Reasoning then continues from where it paused.
Configuring state persistence (AgentStateStore)
AgentState holds everything required to resume the agent — conversation context, compressed summaries, permission rules, tool state, and the current reply position. AgentStateStore is its storage abstraction.
Set stateStore(...) on the builder and the agent persists and recovers automatically: every call writes AgentState back; the next time you call with the same (userId, sessionId), it loads. The agent instance is stateless with respect to sessions — the slot is chosen per-call from the RuntimeContext (falling back to defaultSessionId).
A single
sessionId is enough for most cases. For per-user partitioning, also set userId on the RuntimeContext; the store addresses each slot by the (userId, sessionId) pair.
Use agent.getAgentState(userId, sessionId) or agent.getAgentState(runtimeContext) to inspect a specific session’s state:
Structured Output
Structured output forces the agent to respond according to a JSON Schema you specify, rather than free-form text. Use it whenever your code needs to consume the agent’s output programmatically — form filling, data extraction, classification, etc.Basic usage
Pass a Java class (orJsonNode schema) to call:
How it works
The framework automatically selects the implementation path based on model capabilities:
Either way, the caller’s code is identical — path selection is transparent.
Reading the result
TheMsg returned by call carries the parsed structured data in its metadata:
Using a JsonNode schema
If you prefer not to define a Java class, pass a raw JSON Schema:More capabilities
The following features are configured via the builder. See their respective documentation for details:Model fault tolerance
Skills
Skills are hot-loadable Markdown prompt modules that the LLM activates on demand:Built-in tools
Further reading
Permission System
Control which tools the agent can call, and under what conditions.
Middleware
Intercept and modify agent behavior at the agent, reasoning, acting, and model-call hooks.