Overview
Agent (io.agentscope.core.agent.Agent) is the common interface for agent execution. ReActAgent implements the fundamental reasoning-acting loop: the model reasons, calls tools and uses their results to continue until it can respond. This loop is the foundation of AgentScope Harness.
HarnessAgent wraps a ReActAgent, keeping that loop unchanged while adding built-in workspace, long-term memory, context compaction, skills, subagents, sandboxes, and session logging and recovery. Start with HarnessAgent for most business applications. Use ReActAgent directly when you need the underlying engine or want to compose these capabilities yourself. See Quickstart to get started and Harness architecture for the available capabilities.
This page explains the common underlying APIs using ReActAgent examples. For both agent types, share a configured Builder, build an instance for each direct request, and close it after execution; see Instance lifecycle for details.
Core interface
TheAgent interface composes three capability interfaces: CallableAgent, StreamableAgent, ObservableAgent. The most commonly used methods:
Both
ReActAgent and HarnessAgent support structured output (call(msgs, structuredOutputClass, runtimeContext)) and convenient per-call metadata via RuntimeContext.
Choose an invocation style for your application
Both
call and streamEvents support ongoing conversations, and each call can include multiple reasoning steps and tool calls. With the default HarnessAgent configuration, both also save Session Log and checkpoints. Continue using direct calls if you only need conversation memory or history queries.
Start with direct calls below. For background task management, read Session operations, events and recovery. To use hosted agents over HTTP, read the Service Agent API.
Main loop
BothReActAgent and HarnessAgent run the same reasoning-acting loop for each call. The diagram below shows the main control flow:
Instance lifecycle
Share a configured Builder, callbuilder.build() for each request, and close that Agent when execution ends. Configure the Builder at application startup and only build from it in handlers. Pass request identity and parameters in a fresh RuntimeContext instead of changing the shared Builder.
Use try-with-resources around synchronous execution and its wait. For reactive calls, use Mono.using / Flux.using to close on completion, error or cancellation. The application can manage shared model clients, pools, stores and skill repositories until shutdown. Shared tools and middleware must be thread-safe and must not store the current user or context in instance fields.
A new instance does not mean a new conversation. Harness restores committed state using stable agentId, userId, sessionId and storage configuration. A plain ReActAgent without a session log or stateStore cannot transfer history held only in the old instance to a new one.
Sharing an Agent instance is also supported. The execution engine is stateless, with session state addressed by identity. The application closes a shared instance at shutdown. Within one instance, the execution gate serializes direct calls for the same user/session while different sessions can run concurrently. Instances still own caches, execution gates and background resources; stateless does not mean lifecycle-free. The examples below use the recommended per-request pattern.
Background AgentSession, Channel and hosted runtimes are owned by their managers. Do not close their Agent when a submission request returns. Interrupting a direct call also requires its running instance; constructing another Agent cannot interrupt the old instance’s execution.
Configuring an agent
Define shared configuration withReActAgent.builder(), then call agentBuilder.build() for each request. .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
Running an agent
Below,agentBuilder is the shared application configuration. In fragments that omit the surrounding resource scope, agent is the instance created for the current request and closed after execution.
call and streamEvents accept the same input messages and drive the same reasoning-acting loop. They differ in how the result is delivered.
call returns Mono<Msg> and streamEvents returns Flux<AgentEvent>; both start on subscription. A CLI can wait with block() / blockLast(), while a WebFlux handler can return the publisher for the framework to subscribe. The caller owns this execution subscription; cancelling it cancels that execution.
Choose one entry point and subscribe once for each request. streamEvents starts an execution with live events; use log read APIs to query committed history.
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.
Multi-user / multi-session concurrency
Each request creates its own instance andRuntimeContext from the shared Builder. This WebFlux handler expression uses the previously configured agentBuilder:
Flux.using(agentBuilder::build, agent -> agent.streamEvents(input, ctx), ReActAgent::close). Let the web framework subscribe; do not close the Agent before subscription.
Different sessions can run concurrently. Per-instance execution gates do not coordinate other instances. Order requests to the same conversation in an application queue, or use an AgentSession owned by a background manager. Native journal fencing prevents competing writes but does not provide cross-instance FIFO queueing; a legacy stateStore alone is not a distributed execution lock.
The full Spring Boot example is agentscope-examples/documentation/.../streaming/StreamingWebExample.java.
Execution control in session applications
UseAgentSession when a chat product needs work to continue after the page closes, durable task queueing, or coordinated recovery and pending-action handling. Ordinary conversations, streaming interfaces and applications managing their own HITL flow can continue using direct calls.
The session accepts new tasks through submit, receives guidance through steer / inject, and continues existing tasks through respond / resume. The framework owns background execution; frontends read snapshots and durable events. See the session guide for the complete workflow.
Choose direct calls or session operations to drive a given conversation; read-only history access can be shared. Observe an already submitted task by reading its events. Calling streamEvents would start another execution.
Advanced: own a single execution
Advanced: own a single execution
ReActAgent and HarnessAgent provide prepareRun(messages, context) for events and prepareCall(messages, context) for the final reply. Both return an AgentRun<T> with a unique runId(). Preparing a handle does not execute the agent; subscribe once to stream() to start it.cancel()cancels the reactive execution immediately, including before subscription and while waiting for the session gate. Subscribers receiveCancellationException. Cancelling queued B never interrupts running A or allows C to overtake A.interrupt()/interrupt(message)asks an admitted execution to stop at a cooperative checkpoint. ReActAgent returns its interrupted recovery reply; the handle then completes normally. Before admission, interruption cancels only that queued execution.status()returnsCREATED,QUEUED,RUNNING,COMPLETED,FAILED, orCANCELLED.QUEUEDincludes setup before admission to the core lifecycle.termination()observes the terminal status without starting execution. Downstream subscription disposal also cancels the handle.- A handle permits one subscription. Create a new handle for a new execution; repeated subscriptions are rejected. Terminal handles cannot interrupt later calls.
Interrupt
The following applies to executions started withcall / streamEvents. For interruption from another request, the application must locate the Agent instance running the target execution and remove and close it when execution ends. Use agent.interrupt for cooperative interruption. For tasks submitted through AgentSession, use session.interrupt() and follow the session continuation guide:
(userId, sessionId). It does not select a queued call: use its execution handle for that. Interrupting an idle session is a no-op. The signal itself belongs to the execution and is not persisted in AgentState.
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 - State is saved through the configured backend, and the next call reads the saved context. Pending tool interactions still require the corresponding answers; a tool’s internal execution progress is not restored
(userId, sessionId) strings:
RuntimeContext (per-call context)
RuntimeContext (io.agentscope.core.agent.RuntimeContext) is a per-call metadata bag. Pass a separate instance to each call; tools and middlewares receive the context through their parameters. The agent does not expose a shared current-context getter or inject contexts into shared hook fields. Skill repository operations and HarnessAgent.promoteSkill(name, reviewerId, ctx) likewise take an explicit context; context-less repository operations use the default namespace.
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 streamEvents (plus deprecated stream overloads kept for compatibility). For event streams, pass the context explicitly with streamEvents(msgs, ctx). 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 middlewares 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 examples below handle confirmations and external results through direct calls. Use the same conversation context (ctx) for the request and its answer. Your application manages the interaction UI and subsequent invocation. For durable pending actions and automatic task association, use AgentSession.respond.
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 next call.
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().
ConfirmResult per pending call. You can tweak the tool input on the way back; to remember a choice for identical future calls, pass explicit PermissionRules in the rules argument (suggested rules surface on the permission engine’s PermissionDecision, not on the tool call):
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)
The examples below configure state storage for a directly builtReActAgent. HarnessAgent saves history and checkpoints in Session Log by default, without an additional stateStore setting; see storage configuration. See Context for the relationship between the stores.
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.