AgentScope is an open-source framework for building and operating applications in which a large language model can reason, call tools, retain state, and collaborate with users or other agents. It was initiated by Alibaba and is available as separate Python and Java projects.
An ordinary model call has a short, application-controlled path:
application -> prompt -> model -> responseAn agent adds an execution loop:
user goal
|
v
model reasons -> chooses a tool -> application executes it
^ |
+----------- tool result <---------+
|
v
final answer, interruption, or next actionThe model still does not execute Java or Python functions by itself. It emits a structured request, and AgentScope validates and dispatches that request through its tool system. The framework also manages the messages, event stream, permissions, session state, and repeated model calls around that loop.
This article describes AgentScope 2.0 and focuses on AgentScope Java 2.0.0. AgentScope evolves quickly, and examples for its earlier 1.x APIs may not work unchanged.
Python and Java Are Separate Implementations #
The name AgentScope refers to an ecosystem rather than one library that runs unchanged in every language.
The Python project requires Python 3.11 or later. It provides an asynchronous agent framework, event system, tool and resource permissions, middleware, workspaces, sandbox integrations, multi-session services, and multi-agent teams.
AgentScope Java is a native JVM implementation requiring JDK 17 or later. It uses Project Reactor for its nonblocking execution API and provides Java-native models, tools, messages, events, state stores, middleware, permissions, workspaces, sandboxes, and distributed deployment components.
The two implementations share design ideas, but they do not share in-process objects or source-compatible APIs. A Java application has two sensible integration choices:
- Embed AgentScope Java directly in the JVM.
- Run a Python AgentScope application as a service and call it over a versioned network API or an agent protocol such as A2A.
Do not start a Python interpreter for every request or try to serialize a live Python agent into Java. Process boundaries require an explicit service contract, authentication, timeout behavior, and session ownership.
Core Concepts #
Messages and Content #
Messages carry input and output through an agent. AgentScope Java represents
user input with UserMessage and supports typed content blocks for text,
images, files, audio, video, tool calls, and tool results.
Role validation is important. A model response, a user instruction, and a tool result have different trust levels and should not be flattened into one unstructured prompt.
Models #
The model layer provides a common chat-model contract with provider-specific extensions. AgentScope Java 2.0 includes modules for providers such as:
- Alibaba DashScope and Qwen;
- OpenAI and OpenAI-compatible endpoints;
- Anthropic Claude;
- Google Gemini;
- Ollama; and
- other providers exposed through supported extensions.
The abstraction makes models replaceable at the Java API boundary, but models are not behaviorally identical. Tool-calling quality, structured output, multimodal support, context limits, reasoning controls, and streaming semantics still depend on the selected model.
Tools and Toolkits #
A tool is a capability the model may request. AgentScope generates a JSON schema from the tool definition, sends that schema to the model, parses the model’s tool request, and invokes the Java implementation.
A Toolkit groups the tools available to an agent. Restricting a toolkit is
one of the most important security controls: if an agent cannot see a tool, it
cannot ask the framework to call it.
Tool descriptions are operational instructions for the model. They should say what an operation does, what its arguments mean, and whether it has external or irreversible effects. Vague descriptions lead to incorrect calls.
The ReAct Loop #
ReActAgent is AgentScope Java’s basic reasoning-and-acting agent. During one
run it repeatedly:
- builds model context from the system prompt, messages, and available tools;
- calls the model;
- emits typed events;
- executes requested tools when permitted;
- adds tool results to the context; and
- continues until it produces a final result or reaches a stopping condition.
Use ReActAgent when the application needs a focused conversational or
tool-calling loop and controls most surrounding infrastructure itself.
HarnessAgent #
HarnessAgent builds on the same reasoning core for long-running, open-ended
work. Its harness can add:
- a workspace and file-system abstraction;
- context compaction;
- layered memory;
- reusable skills;
- planning;
- subagents;
- sandboxed execution; and
- persistent state and recovery.
A customer-support assistant that calls three business services may need only
ReActAgent. A coding agent that explores a repository for an hour, edits
files, delegates review, and survives a restart benefits from HarnessAgent.
More autonomy also creates more risk, so the larger harness should not be the
default merely because it offers more features.
Events #
Agent execution produces a typed event stream. Events cover text deltas, model calls, tool calls, results, confirmation requests, errors, and the final result. A web UI can therefore display progress without scraping log text.
The event stream is also the right place to connect observability and audit logic. A production application usually records tool name, duration, outcome, model usage, session, and authenticated subject while redacting message and tool content that may contain secrets.
Runtime Context and State #
RuntimeContext identifies one invocation. It can carry userId, sessionId,
and typed application objects. A Java tool can receive such an object without
exposing it in the JSON schema sent to the model.
This distinction prevents a serious authorization mistake. The model may
supply an orderId, but it must not supply the trusted tenantId or user
identity used to authorize that order. The application obtains identity from
Spring Security and injects it through RuntimeContext.
Conversation history and other recoverable state belong in an
AgentStateStore. Local JSON storage is useful for development. A
multi-replica deployment needs shared storage, such as a supported Redis or
relational database implementation, so another replica can resume the same
session.
A Minimal Python Agent #
Install the Python framework with:
pip install agentscopeAgentScope Python 2.0 uses asynchronous APIs. A minimal agent looks like this:
import asyncio
import os
from agentscope.agent import Agent
from agentscope.credential import DashScopeCredential
from agentscope.message import UserMsg
from agentscope.model import DashScopeChatModel
async def main() -> None:
agent = Agent(
name="assistant",
system_prompt="You are a concise technical assistant.",
model=DashScopeChatModel(
credential=DashScopeCredential(
api_key=os.environ["DASHSCOPE_API_KEY"],
),
model="qwen3.6-plus",
),
)
async for event in agent.reply_stream(
UserMsg("alice", "Explain optimistic locking."),
):
print(event)
asyncio.run(main())Real applications normally select events by type, register a deliberately limited toolkit, persist session state, and configure permissions before exposing the agent to users.
Starting with AgentScope Java #
Add Dependencies #
AgentScope Java 2.0 requires JDK 17 or later. A basic ReActAgent needs the core
module and one model-provider extension:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-bom</artifactId>
<version>2.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-core</artifactId>
</dependency>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-model-dashscope</artifactId>
</dependency>
</dependencies>Use agentscope-harness instead of, or in addition to, the core dependency
when the application needs HarnessAgent and its workspace-oriented
capabilities. Other model extensions include
agentscope-extensions-model-openai,
agentscope-extensions-model-anthropic,
agentscope-extensions-model-gemini, and
agentscope-extensions-model-ollama.
Create an Agent #
The following program uses DashScope. It reads the API key from an environment variable rather than source code:
import io.agentscope.core.ReActAgent;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.UserMessage;
import io.agentscope.extensions.model.dashscope.DashScopeChatModel;
public class FirstAgent {
public static void main(String[] args) {
var model = DashScopeChatModel.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.modelName("qwen-plus")
.stream(true)
.build();
var agent = ReActAgent.builder()
.name("technical-assistant")
.sysPrompt("""
You are a technical assistant.
Distinguish facts from assumptions.
Ask before performing a destructive operation.
""")
.model(model)
.build();
Msg result = agent.call(
new UserMessage("Explain write skew in two paragraphs.")
).block();
System.out.println(result.getTextContent());
}
}AgentScope exposes Reactor types. Calling block() is acceptable in a command
line program at the outer boundary. A reactive server should return the
Mono or Flux instead of blocking a request thread.
Adding Java Tools #
Suppose an application already has an OrderService. The agent adapter should
delegate to that service rather than duplicate business logic or call the
application’s own REST endpoint:
import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.ToolParam;
public final class OrderTools {
private final OrderService orders;
public OrderTools(OrderService orders) {
this.orders = orders;
}
@Tool(
name = "get_order",
description = "Get the current state of an order visible to the caller",
readOnly = true)
public OrderView getOrder(
@ToolParam(
name = "order_id",
description = "Numeric order ID")
long orderId,
RequestIdentity identity) {
return orders.get(identity.tenantId(), identity.userId(), orderId);
}
@Tool(
name = "cancel_order",
description = """
Cancel an order that has not shipped. This changes business
state and must be approved by the user before execution.
""")
public OrderView cancelOrder(
@ToolParam(
name = "order_id",
description = "Numeric order ID")
long orderId,
@ToolParam(
name = "reason",
description = "Business reason for cancellation")
String reason,
RequestIdentity identity) {
return orders.cancel(
identity.tenantId(),
identity.userId(),
orderId,
reason);
}
}orderId and reason are annotated with @ToolParam, so the model sees them
in the tool schema. RequestIdentity is not annotated. AgentScope injects it
from the per-call RuntimeContext, so the model cannot impersonate another
tenant by inventing that argument.
Register the tool object in a toolkit:
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new OrderTools(orderService));
ReActAgent agent = ReActAgent.builder()
.name("order-assistant")
.sysPrompt("""
Help users inspect their orders.
Never cancel an order without explicit approval.
""")
.model(model)
.toolkit(toolkit)
.build();Java annotations make simple synchronous tools convenient. For more control over permission checks, asynchronous execution, or structured metadata, AgentScope Java also provides lower-level tool abstractions.
Integrating AgentScope Java with Spring Boot #
AgentScope Java is framework-agnostic, so a Spring Boot application integrates it as ordinary Java objects. The recommended boundary is:
REST or WebSocket controller
|
v
AgentScope agent factory
|
+----> model provider
+----> application tools ----> existing Spring services
+----> shared state storeThe following example exposes an SSE chat endpoint. It deliberately creates a
new ReActAgent for each invocation. A ReActAgent instance is not safe for
concurrent calls, while the model and state store can be shared. The toolkit is
copied during agent construction.
Configure Shared Components #
import io.agentscope.core.ReActAgent;
import io.agentscope.core.state.AgentStateStore;
import io.agentscope.core.state.JsonFileAgentStateStore;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.extensions.model.dashscope.DashScopeChatModel;
import java.nio.file.Path;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ConfigurationProperties(prefix = "app.agent")
record AgentProperties(String apiKey, String model) {
}
@Configuration
@EnableConfigurationProperties(AgentProperties.class)
class AgentConfiguration {
@Bean
DashScopeChatModel agentModel(AgentProperties properties) {
return DashScopeChatModel.builder()
.apiKey(properties.apiKey())
.modelName(properties.model())
.stream(true)
.build();
}
@Bean
AgentStateStore agentStateStore() {
return new JsonFileAgentStateStore(
Path.of(".agentscope", "sessions"));
}
@Bean
Toolkit orderToolkit(OrderService orders) {
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new OrderTools(orders));
return toolkit;
}
@Bean
OrderAgentFactory orderAgentFactory(
DashScopeChatModel model,
AgentStateStore stateStore,
Toolkit orderToolkit) {
return new OrderAgentFactory(model, stateStore, orderToolkit);
}
}Configure nonsecret values normally and supply the API key through a secret manager or environment variable:
app:
agent:
model: qwen-plus
api-key: ${DASHSCOPE_API_KEY}Do not expose model credentials to the browser.
The factory creates an isolated agent run while reusing safe infrastructure:
public record OrderAgentFactory(
DashScopeChatModel model,
AgentStateStore stateStore,
Toolkit toolkit) {
ReActAgent create() {
return ReActAgent.builder()
.name("order-assistant")
.sysPrompt("""
Help the authenticated user inspect orders.
Use tools for current order data.
Never claim that a mutation succeeded unless its tool
returned a successful result.
""")
.model(model)
.stateStore(stateStore)
.toolkit(toolkit)
.build();
}
}For local development, JsonFileAgentStateStore is sufficient. Replace it
with a supported shared state store before running multiple application
replicas. A local directory inside a container is not durable session storage.
Expose an SSE Endpoint #
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.event.TextBlockDeltaEvent;
import io.agentscope.core.message.UserMessage;
import java.security.Principal;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
record ChatRequest(String sessionId, String message) {
}
record RequestIdentity(String tenantId, String userId) {
}
@RestController
@RequestMapping("/api/assistant")
class AgentController {
private final OrderAgentFactory agents;
private final TenantResolver tenants;
AgentController(OrderAgentFactory agents, TenantResolver tenants) {
this.agents = agents;
this.tenants = tenants;
}
@PostMapping(
path = "/messages",
produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<String> chat(
@RequestBody ChatRequest request,
Principal principal) {
String userId = principal.getName();
String tenantId = tenants.currentTenant();
RuntimeContext context = RuntimeContext.builder()
.userId(userId)
.sessionId(request.sessionId())
.put(
RequestIdentity.class,
new RequestIdentity(tenantId, userId))
.build();
return agents.create()
.streamEvents(
new UserMessage(request.message()),
context)
.filter(TextBlockDeltaEvent.class::isInstance)
.cast(TextBlockDeltaEvent.class)
.map(TextBlockDeltaEvent::getDelta);
}
}The application must verify that the submitted session belongs to the
authenticated user. In many systems the server should issue an opaque session
ID and store its ownership rather than accept arbitrary client-generated IDs.
Using the same sessionId for two users risks conversation disclosure even
when RuntimeContext.userId differs, depending on how the selected store is
partitioned.
Streaming only text deltas is convenient for a simple UI, but a richer API should preserve event types. The browser may need to distinguish model text, tool progress, approval requests, recoverable errors, and final completion. AG-UI or an application-specific event envelope is more expressive than a plain stream of strings.
Handle Blocking Tools #
The agent API is reactive, but an annotated tool may call a blocking database driver or legacy HTTP client. Reactive types do not make that operation nonblocking. Either:
- use nonblocking dependencies and asynchronous tool APIs;
- schedule blocking work on a bounded worker pool; or
- use a conventional request model with explicitly bounded concurrency.
Never run unbounded blocking work on Reactor’s event-loop threads. Also set timeouts independently for model calls, tool calls, and the overall agent run. An agent can make several model and tool calls, so its total latency is larger than one model request.
Permissions and Human Approval #
Prompt instructions such as “ask before deleting” are useful but are not enforcement. A model can misunderstand them, retrieved content can attempt prompt injection, and future prompt edits can weaken them.
AgentScope Java’s permission engine can classify tool calls as:
- allow: execute without interruption;
- ask: pause and request a user decision; or
- deny: reject the operation.
When a call requires approval, the agent emits or returns confirmation state. The application displays the exact operation and arguments, records the user’s decision, and resumes the run with that decision. In a headless environment, operations requiring approval should default to denial rather than silently becoming allowed.
Permission rules complement normal application authorization:
permission rule:
Is this category of tool allowed in this agent flow?
OrderService authorization:
May this authenticated user cancel this specific order?Both checks are required. A permission rule cannot replace tenant checks, ownership checks, transaction invariants, or database constraints.
Memory, Workspaces, and Sandboxes #
These concepts solve different problems:
Conversation state preserves the messages and agent state needed to continue a session.
Long-term memory stores selected facts or summaries across conversations. It should have retention, correction, deletion, and tenant-isolation policies. Saving every model utterance forever is not a memory strategy.
Workspace gives a harness a persistent area for plans, artifacts, skills, and large tool results. Treat workspace files as untrusted input when their contents can be influenced by users or tools.
Sandbox isolates code and command execution. A local subprocess is not a strong security boundary merely because it has a different process ID. Production execution of model-generated commands should use an appropriately hardened container, Kubernetes, or supported remote sandbox with resource and network restrictions.
Multi-Agent Systems #
AgentScope supports agent teams and subagents, but multiple agents are not automatically better than one. They add model calls, state transitions, failure modes, and coordination cost.
Use a subagent when the work has a genuine boundary, such as:
- a different tool and permission set;
- a specialized context or system prompt;
- independent parallel research;
- a separately recoverable long-running task; or
- a trust boundary requiring isolation.
Use ordinary Java methods or workflow code for deterministic sequencing. Having one model ask another model to add two numbers is slower and less reliable than calling a function.
For communication between independently deployed agents, use a service contract. A2A is intended for agent-to-agent discovery, messages, tasks, and artifacts. MCP connects an agent to tools and resources; it is not the same as delegating a goal to an autonomous remote agent.
Integrating with a Python AgentScope Service #
An organization may already operate an agent written with AgentScope Python. A Java service should treat it as a remote system:
Spring Boot service
-> authenticated HTTP, A2A, or messaging contract
-> Python AgentScope service
-> model and toolsDefine:
- request, event, and artifact schemas;
- the protocol and version;
- authentication and tenant propagation;
- who creates and owns session IDs;
- idempotency behavior;
- streaming and reconnection behavior;
- cancellation and timeout semantics; and
- error mapping.
Generate a Java HTTP client from an OpenAPI contract when the Python service offers a conventional REST API. Use A2A when the remote component is an independent agent with long-running tasks and multi-turn negotiation. Keep the Java service unaware of Python implementation classes.
The embedded Java and remote Python approaches can coexist. For example, a Spring Boot order agent can use local Java tools for low-latency business operations and delegate a specialized document-analysis task to a Python agent over A2A.
AgentScope Java and Spring AI #
Spring AI and AgentScope Java overlap, but their centers of gravity differ.
Spring AI is Spring-native infrastructure for portable model APIs,
ChatClient, Advisors, retrieval augmented generation, vector stores,
structured output, tools, observability, and MCP. It is a natural choice when
an application primarily needs model features inside an existing Spring
architecture.
AgentScope Java focuses on the agent execution runtime: repeated reasoning and acting, typed lifecycle events, permissions, interruptions, harness workspaces, subagents, resumable sessions, and distributed agent operation.
Choose based on the application’s dominant problem:
| Requirement | Likely Starting Point |
|---|---|
| Add chat, RAG, or tool calling to a Spring service | Spring AI |
| Run long-lived, interruptible, tool-using agents | AgentScope Java |
| Publish tools to multiple AI hosts | MCP server |
| Collaborate with an independently deployed agent | A2A |
The libraries’ model, message, and tool interfaces are not interchangeable. Avoid writing an adapter merely to mix frameworks in one reasoning loop. Integration is cleaner at a stable boundary: shared Spring business services, MCP tools, A2A, or an application API.
Production Checklist #
Before exposing an AgentScope application to real users:
- Authenticate every entry point and derive identity from trusted context.
- Partition sessions, state, memory, workspaces, and caches by tenant and user.
- Give each agent only the tools required for its role.
- Enforce authorization inside business services, not only in prompts.
- Require explicit approval for financial, destructive, or external actions.
- Sandbox model-generated code and restrict its file-system and network access.
- Bound model turns, tool calls, tokens, execution time, result size, and concurrency.
- Make state-changing tools idempotent where retries are possible.
- Store resumable state in shared storage before scaling to multiple replicas.
- Trace model and tool events while redacting secrets and personal data.
- Evaluate complete trajectories, not only the final prose answer.
- Test cancellation, model failure, tool timeout, restart, and duplicate delivery paths.
An agent framework supplies execution machinery. It does not decide which actions are safe, which data a user may access, or whether a model’s answer is correct. Those remain application and operational responsibilities.
Comments
Read or comment on GitHub (sign-in required to comment)