| name | agentscope |
| description | AgentScope Java framework - a reactive, message-driven multi-agent system built on Project Reactor. Use when working with reactive programming, LLM integration, agent orchestration, multi-agent systems, or when the user mentions AgentScope, ReActAgent, Mono/Flux, Project Reactor, or Java agent development. Specializes in non-blocking code, tool integration, hooks, pipelines, and production-ready agent applications. |
name: agentscope-java
description: Expert Java developer skill for AgentScope Java framework - a reactive, message-driven multi-agent system built on Project Reactor. Use when working with reactive programming, LLM integration, agent orchestration, multi-agent systems, or when the user mentions AgentScope, ReActAgent, Mono/Flux, Project Reactor, or Java agent development. Specializes in non-blocking code, tool integration, hooks, pipelines, and production-ready agent applications.
license: Apache-2.0
compatibility: Designed for Claude Code and Cursor. Requires Java 17+, Maven/Gradle, and familiarity with reactive programming concepts.
metadata:
framework: AgentScope Java
language: Java 17+
paradigm: Reactive Programming
core-library: Project Reactor
version: "1.0"
When the user asks you to write AgentScope Java code, follow these instructions carefully.
CRITICAL RULES - NEVER VIOLATE THESE
🚫 ABSOLUTELY FORBIDDEN:
- NEVER use
.block() in example code - This is the #1 mistake. Only use .block() in main() methods or test code when explicitly creating a runnable example.
- NEVER use
Thread.sleep() - Use Mono.delay() instead.
- NEVER use
ThreadLocal - Use Reactor Context with Mono.deferContextual().
- NEVER hardcode API keys - Always use
System.getenv().
- NEVER ignore errors silently - Always log errors and provide fallback values.
- NEVER use wrong import paths - All models are in
io.agentscope.core.model.*, NOT io.agentscope.model.*.
✅ ALWAYS DO:
- Use
Mono and Flux for all asynchronous operations.
- Chain operations with
.map(), .flatMap(), .then().
- Use Builder pattern for creating agents, models, and messages.
- Include error handling with
.onErrorResume() or .onErrorReturn().
- Add logging with SLF4J for important operations.
- Use correct imports:
import io.agentscope.core.model.DashScopeChatModel;
- Use correct APIs (many methods don't exist or have changed):
toolkit.registerTool() NOT registerObject()
toolkit.getToolNames() NOT getTools()
event.getToolUse().getName() NOT getToolName()
result.getOutput() NOT getContent() (ToolResultBlock)
event.getToolResult() NOT getResult() (PostActingEvent)
toolUse.getInput() NOT getArguments() (ToolUseBlock)
- Model builder: NO
temperature() method, use defaultOptions(GenerateOptions.builder()...)
- Hook events: NO
getMessages(), getResponse(), getIterationCount(), getThinkingBlock() methods
- ToolResultBlock: NO
getToolUseName() method, use event.getToolUse().getName() instead
- ToolResultBlock.getOutput() returns
List<ContentBlock> NOT String, need to convert
- @ToolParam format: MUST use
@ToolParam(name = "x", description = "y") NOT @ToolParam(name="x")
WHEN GENERATING CODE
FIRST: Identify the context
- Is this a
main() method or test code? → .block() is allowed (but add a warning comment)
- Is this agent logic, service method, or library code? →
.block() is FORBIDDEN
For every code example you provide:
- Check: Does it use
.block()? → If yes in non-main/non-test code, REWRITE IT.
- Check: Are all operations non-blocking? → If no, FIX IT.
- Check: Does it have error handling? → If no, ADD IT.
- Check: Are API keys from environment? → If no, CHANGE IT.
- Check: Are imports correct? → If using
io.agentscope.model.*, FIX TO io.agentscope.core.model.*.
Default code structure for agent logic:
return model.generate(messages, null, null)
.map(response -> processResponse(response))
.onErrorResume(e -> {
log.error("Operation failed", e);
return Mono.just(fallbackValue);
});
String result = model.generate(messages, null, null).block();
Only for main() methods (add warning comment):
public static void main(String[] args) {
Msg response = agent.call(userMsg).block();
System.out.println(response.getTextContent());
}
PROJECT SETUP
When creating a new AgentScope project, use the correct Maven dependencies:
Maven Configuration (pom.xml)
For production use (recommended):
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope</artifactId>
<version>1.0.12</version>
</dependency>
</dependencies>
For local development (if working with source code):
<properties>
<agentscope.version>1.0.12</agentscope.version>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-core</artifactId>
<version>${agentscope.version}</version>
</dependency>
</dependencies>
⚠️ IMPORTANT: Version Selection
- Use
agentscope:1.0.12 for production (stable, from Maven Central)
- Use
agentscope-core:1.0.12 only if you're developing AgentScope itself
- NEVER use version
0.1.0-SNAPSHOT - this version doesn't exist
⚠️ CRITICAL: Common Dependency Mistakes
❌ WRONG - These artifacts don't exist:
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-model-dashscope</artifactId>
</dependency>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-model-openai</artifactId>
</dependency>
❌ WRONG - These versions don't exist:
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-core</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope</artifactId>
<version>0.1.0</version>
</dependency>
✅ CORRECT - Use the stable release:
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope</artifactId>
<version>1.0.12</version>
</dependency>
Available Model Classes (all in agentscope-core)
import io.agentscope.core.model.DashScopeChatModel;
import io.agentscope.core.model.OpenAIChatModel;
import io.agentscope.core.model.GeminiChatModel;
import io.agentscope.core.model.AnthropicChatModel;
import io.agentscope.core.model.OllamaChatModel;
Optional Extensions
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-mem0</artifactId>
<version>${agentscope.version}</version>
</dependency>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-rag-dify</artifactId>
<version>${agentscope.version}</version>
</dependency>
PROJECT OVERVIEW & ARCHITECTURE
AgentScope Java is a reactive, message-driven multi-agent framework built on Project Reactor and Java 17+.
Core Abstractions
Agent: The fundamental unit of execution. Most agents extend AgentBase.
Msg: The message object exchanged between agents.
Memory: Stores conversation history (InMemoryMemory, LongTermMemory).
Toolkit & AgentTool: Defines capabilities the agent can use.
Model: Interfaces with LLMs (OpenAI, DashScope, Gemini, Anthropic, etc.).
Hook: Intercepts and modifies agent execution at various lifecycle points.
Pipeline: Orchestrates multiple agents in sequential or parallel patterns.
Reactive Nature
Almost all operations (agent calls, model inference, tool execution) return Mono<T> or Flux<T>.
Key Design Principles
- Non-blocking: All I/O operations are asynchronous
- Message-driven: Agents communicate via immutable
Msg objects
- Composable: Agents and pipelines can be nested and combined
- Extensible: Hooks and custom tools allow deep customization
CODING STANDARDS & BEST PRACTICES
2.1 Java Version & Style
Target Java 17 (LTS) for maximum compatibility:
- Use Java 17 features (Records, Switch expressions, Pattern Matching for instanceof,
var, Sealed classes)
- AVOID Java 21+ preview features (pattern matching in switch, record patterns)
- Follow standard Java conventions (PascalCase for classes, camelCase for methods/variables)
- Use Lombok where appropriate (
@Data, @Builder for DTOs/Messages)
- Prefer immutability for data classes
- Use meaningful names that reflect domain concepts
⚠️ CRITICAL: Avoid Preview Features
return switch (event) {
case PreReasoningEvent e -> Mono.just(e);
default -> Mono.just(event);
};
if (event instanceof PreReasoningEvent e) {
return Mono.just(event);
} else {
return Mono.just(event);
}
2.2 Reactive Programming (Critical)
⚠️ NEVER BLOCK IN AGENT LOGIC
Blocking operations will break the reactive chain and cause performance issues.
Rules:
- ❌ Never use
.block() in agent logic (only in main methods or tests)
- ✅ Use
Mono for single results (e.g., agent.call())
- ✅ Use
Flux for streaming responses (e.g., model.stream())
- ✅ Chain operations using
.map(), .flatMap(), .then()
- ✅ Use
Mono.defer() for lazy evaluation
- ✅ Use
Mono.deferContextual() for reactive context access
Example:
public Mono<String> processData(String input) {
String result = externalService.call(input).block();
return Mono.just(result);
}
public Mono<String> processData(String input) {
return externalService.call(input)
.map(this::transform)
.flatMap(this::validate);
}
2.3 Message Handling (Msg)
Create messages using the Builder pattern:
Msg userMsg = Msg.builder()
.role(MsgRole.USER)
.content(TextBlock.builder().text("Hello").build())
.name("user")
.build();
Content Blocks:
TextBlock: For text content
ThinkingBlock: For Chain of Thought (CoT) reasoning
ToolUseBlock: For tool calls
ToolResultBlock: For tool outputs
Helper Methods:
String text = msg.getTextContent();
String text = msg.getContent().get(0).getText();
2.4 Implementing Agents
Extend AgentBase and implement doCall(List<Msg> msgs):
public class MyAgent extends AgentBase {
private final Model model;
private final Memory memory;
public MyAgent(String name, Model model) {
super(name, "A custom agent", true, List.of());
this.model = model;
this.memory = new InMemoryMemory();
}
@Override
protected Mono<Msg> doCall(List<Msg> msgs) {
if (msgs != null) {
msgs.forEach(memory::addMessage);
}
return model.generate(memory.getMessages(), null, null)
.map(response -> Msg.builder()
.name(getName())
.role(MsgRole.ASSISTANT)
.content(TextBlock.builder().text(response.getText()).build())
.build());
}
}
2.5 Tool Definition
Use @Tool annotation for function-based tools. Tools can return:
String (synchronous)
Mono<String> (asynchronous)
Mono<ToolResultBlock> (for complex results)
⚠️ CRITICAL: @ToolParam Format
- ✅ CORRECT:
@ToolParam(name = "city", description = "City name")
- ❌ WRONG:
@ToolParam(name="city", description="...") (no spaces around =)
- ❌ WRONG:
@ToolParam("city") (missing name= and description=)
Synchronous Tool Example:
public class WeatherTools {
@Tool(description = "Get current weather for a city. Returns temperature and conditions.")
public String getWeather(
@ToolParam(name = "city", description = "City name, e.g., 'San Francisco'")
String city) {
return "Sunny, 25°C";
}
}
Asynchronous Tool Example:
public class AsyncTools {
private final WebClient webClient;
@Tool(description = "Fetch data from trusted API endpoint")
public Mono<String> fetchData(
@ToolParam(name = "url", description = "API endpoint URL (must start with https://api.myservice.com)")
String url) {
if (!url.startsWith("https://api.myservice.com")) {
return Mono.just("Error: URL not allowed. Must start with https://api.myservice.com");
}
return webClient.get()
.uri(url)
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(10))
.onErrorResume(e -> Mono.just("Error: " + e.getMessage()));
}
}
Register with Toolkit:
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new WeatherTools());
toolkit.registerTool(new AsyncTools());
HOOK SYSTEM
Hooks allow you to intercept and modify agent execution at various lifecycle points.
Hook Interface
public interface Hook {
<T extends HookEvent> Mono<T> onEvent(T event);
default int priority() { return 100; }
}
Common Hook Events
PreReasoningEvent: Before LLM reasoning (modifiable)
PostReasoningEvent: After LLM reasoning (modifiable)
ReasoningChunkEvent: Streaming reasoning chunks (notification)
PreActingEvent: Before tool execution (modifiable)
PostActingEvent: After tool execution (modifiable)
ActingChunkEvent: Streaming tool execution (notification)
Hook Example
Java 17+ compatible (recommended):
Hook loggingHook = new Hook() {
@Override
public <T extends HookEvent> Mono<T> onEvent(T event) {
if (event instanceof PreReasoningEvent e) {
log.info("Reasoning with model: {}", e.getModelName());
return Mono.just(event);
} else if (event instanceof PreActingEvent e) {
log.info("Calling tool: {}", e.getToolUse().getName());
return Mono.just(event);
} else if (event instanceof PostActingEvent e) {
log.info("Tool {} completed", e.getToolUse().getName());
return Mono.just(event);
} else {
return Mono.just(event);
}
}
@Override
public int priority() {
return 500;
}
};
ReActAgent agent = ReActAgent.builder()
.name("Assistant")
.model(model)
.hook(loggingHook)
.build();
Alternative: Traditional if-else (Java 17):
Hook loggingHook = new Hook() {
@Override
public <T extends HookEvent> Mono<T> onEvent(T event) {
if (event instanceof PreReasoningEvent) {
PreReasoningEvent e = (PreReasoningEvent) event;
log.info("Reasoning with model: {}", e.getModelName());
} else if (event instanceof PreActingEvent) {
PreActingEvent e = (PreActingEvent) event;
log.info("Calling tool: {}", e.getToolUse().getName());
} else if (event instanceof PostActingEvent) {
PostActingEvent e = (PostActingEvent) event;
log.info("Tool {} completed", e.getToolUse().getName());
}
return Mono.just(event);
}
@Override
public int priority() {
return 500;
}
};
Priority Guidelines:
- 0-50: Critical system hooks (auth, security)
- 51-100: High priority hooks (validation, preprocessing)
- 101-500: Normal priority hooks (business logic)
- 501-1000: Low priority hooks (logging, metrics)
PIPELINE PATTERNS
Pipelines orchestrate multiple agents in structured workflows.
Sequential Pipeline
Executes agents in sequence (output of one becomes input of next):
SequentialPipeline pipeline = SequentialPipeline.builder()
.addAgent(researchAgent)
.addAgent(summaryAgent)
.addAgent(reviewAgent)
.build();
Msg result = pipeline.execute(userInput).block();
Fanout Pipeline
Executes agents in parallel and aggregates results:
FanoutPipeline pipeline = FanoutPipeline.builder()
.addAgent(agent1)
.addAgent(agent2)
.addAgent(agent3)
.build();
Msg result = pipeline.execute(userInput).block();
When to Use:
- Sequential: When each agent depends on the previous agent's output
- Fanout: When agents can work independently and results need aggregation
MEMORY MANAGEMENT
In-Memory (Short-term)
Memory memory = new InMemoryMemory();
Long-Term Memory
LongTermMemory longTermMemory = Mem0LongTermMemory.builder()
.apiKey(System.getenv("MEM0_API_KEY"))
.userId("user_123")
.build();
ReActAgent agent = ReActAgent.builder()
.name("Assistant")
.model(model)
.longTermMemory(longTermMemory)
.longTermMemoryMode(LongTermMemoryMode.BOTH)
.build();
Memory Modes:
STATIC_CONTROL: Framework automatically manages memory (via hooks)
AGENTIC: Agent decides when to use memory (via tools)
BOTH: Combines both approaches
MCP (MODEL CONTEXT PROTOCOL) INTEGRATION
AgentScope supports MCP for integrating external tools and resources.
McpClientWrapper mcpClient = McpClientBuilder.stdio()
.command("npx")
.args("-y", "@modelcontextprotocol/server-filesystem@0.6.2", "/path/to/files")
.build();
Toolkit toolkit = new Toolkit();
toolkit.registration()
.mcpClient(mcpClient)
.enableTools(List.of("read_file", "write_file"))
.apply();
ReActAgent agent = ReActAgent.builder()
.name("Assistant")
.model(model)
.toolkit(toolkit)
.build();
TESTING
Unit Testing with StepVerifier
@Test
void testAgentCall() {
Msg input = Msg.builder()
.role(MsgRole.USER)
.content(TextBlock.builder().text("Hello").build())
.build();
StepVerifier.create(agent.call(input))
.assertNext(response -> {
assertEquals(MsgRole.ASSISTANT, response.getRole());
assertNotNull(response.getTextContent());
})
.verifyComplete();
}
Mocking External Dependencies
@Test
void testWithMockModel() {
Model mockModel = mock(Model.class);
when(mockModel.generate(any(), any(), any()))
.thenReturn(Mono.just(ChatResponse.builder()
.text("Mocked response")
.build()));
ReActAgent agent = ReActAgent.builder()
.name("TestAgent")
.model(mockModel)
.build();
}
Testing Best Practices:
- Always test reactive chains with
StepVerifier
- Mock external dependencies (models, APIs)
- Test error cases and edge conditions
- Verify that hooks are called correctly
- Test timeout and cancellation scenarios
CODE STYLE GUIDE
Logging
private static final Logger log = LoggerFactory.getLogger(MyClass.class);
log.info("Processing message from user: {}", userId);
log.error("Failed to call model: {}", modelName, exception);
Error Handling
return Mono.error(new IllegalArgumentException(
"Invalid model name: " + modelName + ". Expected one of: " + VALID_MODELS));
return model.generate(msgs, null, null)
.onErrorResume(e -> {
log.error("Model call failed, using fallback", e);
return Mono.just(fallbackResponse);
});
Null Safety
public Optional<AgentTool> findTool(String name) {
return Optional.ofNullable(tools.get(name));
}
public MyAgent(Model model) {
this.model = Objects.requireNonNull(model, "Model cannot be null");
}
Comments
public static ReActAgent create(String name, Model model) {
}
Duration delay = Duration.ofMillis((long) Math.pow(2, attempt) * baseDelayMs);
KEY LIBRARIES
- Reactor Core:
Mono, Flux for reactive programming
- Jackson: JSON serialization/deserialization
- SLF4J: Logging (
private static final Logger log = LoggerFactory.getLogger(MyClass.class);)
- OkHttp: HTTP client for model APIs
- MCP SDK: Model Context Protocol integration
- JUnit 5: Testing framework
- Mockito: Mocking framework
- Lombok: Boilerplate reduction
PROHIBITED PRACTICES
❌ NEVER Do These
-
Block in reactive chains
return someMonoOperation().block();
-
Use Thread.sleep() or blocking I/O
Thread.sleep(1000);
return Mono.delay(Duration.ofSeconds(1));
-
Mutate shared state without synchronization
private List<Msg> messages = new ArrayList<>();
public void addMessage(Msg msg) {
messages.add(msg);
}
-
Ignore errors silently
.onErrorResume(e -> Mono.empty())
.onErrorResume(e -> {
log.error("Operation failed", e);
return Mono.just(fallbackValue);
})
-
Use ThreadLocal in reactive code
ThreadLocal<String> context = new ThreadLocal<>();
return Mono.deferContextual(ctx -> {
String value = ctx.get("key");
});
-
Create agents without proper resource management
public void processRequests() {
for (int i = 0; i < 1000; i++) {
ReActAgent agent = createAgent();
agent.call(msg).block();
}
}
-
Hardcode API keys or secrets
String apiKey = "sk-1234567890";
String apiKey = System.getenv("OPENAI_API_KEY");
-
Use Java preview features (requires --enable-preview)
return switch (event) {
case PreReasoningEvent e -> handleReasoning(e);
case PostActingEvent e -> handleActing(e);
default -> Mono.just(event);
};
if (event instanceof PreReasoningEvent e) {
return handleReasoning(e);
} else if (event instanceof PostActingEvent e) {
return handleActing(e);
} else {
return Mono.just(event);
}
COMMON PITFALLS & SOLUTIONS
❌ Blocking Operations
Msg response = agent.call(msg).block();
return agent.call(msg)
.flatMap(response -> processResponse(response));
❌ Null Handling
String text = msg.getContent().get(0).getText();
String text = msg.getTextContent();
String text = msg.getContentBlocks(TextBlock.class).stream()
.findFirst()
.map(TextBlock::getText)
.orElse("");
❌ Thread Context
ThreadLocal<String> context = new ThreadLocal<>();
return Mono.deferContextual(ctx -> {
String value = ctx.get("key");
});
❌ Error Swallowing
.onErrorResume(e -> Mono.empty())
.onErrorResume(e -> {
log.error("Failed to process: {}", input, e);
return Mono.just(createErrorResponse(e));
})
COMPLETE EXAMPLE
package com.example.agentscope;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.hook.Hook;
import io.agentscope.core.hook.HookEvent;
import io.agentscope.core.hook.ReasoningChunkEvent;
import io.agentscope.core.memory.InMemoryMemory;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.MsgRole;
import io.agentscope.core.message.TextBlock;
import io.agentscope.core.model.Model;
import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.ToolParam;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.core.model.DashScopeChatModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class CompleteExample {
private static final Logger log = LoggerFactory.getLogger(CompleteExample.class);
public static void main(String[] args) {
Model model = DashScopeChatModel.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.modelName("qwen-plus")
.stream(true)
.build();
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new WeatherTools());
toolkit.registerTool(new TimeTools());
Hook streamingHook = new Hook() {
@Override
public <T extends HookEvent> Mono<T> onEvent(T event) {
if (event instanceof ReasoningChunkEvent e) {
String text = e.getIncrementalChunk().getTextContent();
if (text != null) {
System.out.print(text);
}
}
return Mono.just(event);
}
@Override
public int priority() {
return 500;
}
};
ReActAgent agent = ReActAgent.builder()
.name("Assistant")
.sysPrompt("You are a helpful assistant. Use tools when appropriate.")
.model(model)
.toolkit(toolkit)
.memory(new InMemoryMemory())
.hook(streamingHook)
.maxIters(10)
.build();
Msg userMsg = Msg.builder()
.role(MsgRole.USER)
.content(TextBlock.builder()
.text("What's the weather in San Francisco and what time is it?")
.build())
.build();
try {
System.out.println("User: " + userMsg.getTextContent());
System.out.print("Assistant: ");
Msg response = agent.call(userMsg).block();
System.out.println("\n\n--- Response Details ---");
System.out.println("Role: " + response.getRole());
System.out.println("Content: " + response.getTextContent());
} catch (Exception e) {
log.error("Error during agent execution", e);
System.err.println("Error: " + e.getMessage());
}
}
public static class WeatherTools {
@Tool(description = "Get current weather for a city. Returns temperature and conditions.")
public String getWeather(
@ToolParam(name = "city", description = "City name, e.g., 'San Francisco'")
String city) {
log.info("Getting weather for city: {}", city);
return String.format("Weather in %s: Sunny, 22°C, Light breeze", city);
}
}
public static class TimeTools {
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Tool(description = "Get current date and time")
public String getCurrentTime() {
LocalDateTime now = LocalDateTime.now();
String formatted = now.format(FORMATTER);
log.info("Returning current time: {}", formatted);
return "Current time: " + formatted;
}
}
}
QUICK REFERENCE
Agent Creation
ReActAgent agent = ReActAgent.builder()
.name("AgentName")
.sysPrompt("System prompt")
.model(model)
.toolkit(toolkit)
.memory(memory)
.hooks(hooks)
.maxIters(10)
.build();
Message Creation
Msg msg = Msg.builder()
.role(MsgRole.USER)
.content(TextBlock.builder().text("Hello").build())
.build();
Tool Registration
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new MyTools());
Hook Creation
Hook hook = new Hook() {
public <T extends HookEvent> Mono<T> onEvent(T event) {
return Mono.just(event);
}
};
Pipeline Creation
SequentialPipeline pipeline = SequentialPipeline.builder()
.addAgent(agent1)
.addAgent(agent2)
.build();