/SKILL.md
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.
npx skillsauth add guanxuc/agentscope-java agentscope-javaInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
When the user asks you to write AgentScope Java code, follow these instructions carefully.
🚫 ABSOLUTELY FORBIDDEN:
.block() in example code - This is the #1 mistake. Only use .block() in main() methods or test code when explicitly creating a runnable example.Thread.sleep() - Use Mono.delay() instead.ThreadLocal - Use Reactor Context with Mono.deferContextual().System.getenv().io.agentscope.core.model.*, NOT io.agentscope.model.*.✅ ALWAYS DO:
Mono and Flux for all asynchronous operations..map(), .flatMap(), .then()..onErrorResume() or .onErrorReturn().import io.agentscope.core.model.DashScopeChatModel;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)temperature() method, use defaultOptions(GenerateOptions.builder()...)getMessages(), getResponse(), getIterationCount(), getThinkingBlock() methodsgetToolUseName() method, use event.getToolUse().getName() insteadList<ContentBlock> NOT String, need to convert@ToolParam(name = "x", description = "y") NOT @ToolParam(name="x")FIRST: Identify the context
main() method or test code? → .block() is allowed (but add a warning comment).block() is FORBIDDENFor every code example you provide:
.block()? → If yes in non-main/non-test code, REWRITE IT.io.agentscope.model.*, FIX TO io.agentscope.core.model.*.Default code structure for agent logic:
// ✅ CORRECT - Non-blocking, reactive (use this pattern by default)
return model.generate(messages, null, null)
.map(response -> processResponse(response))
.onErrorResume(e -> {
log.error("Operation failed", e);
return Mono.just(fallbackValue);
});
// ❌ WRONG - Never generate this in agent logic
String result = model.generate(messages, null, null).block(); // DON'T DO THIS
Only for main() methods (add warning comment):
public static void main(String[] args) {
// ⚠️ .block() is ONLY allowed here because this is a main() method
Msg response = agent.call(userMsg).block();
System.out.println(response.getTextContent());
}
When creating a new AgentScope project, use the correct Maven dependencies:
For production use (recommended):
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Use the latest stable release from Maven Central -->
<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
agentscope:1.0.12 for production (stable, from Maven Central)agentscope-core:1.0.12 only if you're developing AgentScope itself0.1.0-SNAPSHOT - this version doesn't exist❌ WRONG - These artifacts don't exist:
<!-- DON'T use these - they don't exist -->
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-model-dashscope</artifactId> <!-- ❌ WRONG -->
</dependency>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-model-openai</artifactId> <!-- ❌ WRONG -->
</dependency>
❌ WRONG - These versions don't exist:
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-core</artifactId>
<version>0.1.0-SNAPSHOT</version> <!-- ❌ WRONG - doesn't exist -->
</dependency>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope</artifactId>
<version>0.1.0</version> <!-- ❌ WRONG - doesn't exist -->
</dependency>
✅ CORRECT - Use the stable release:
<!-- For production: use the stable release from Maven Central -->
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope</artifactId>
<version>1.0.12</version> <!-- ✅ CORRECT -->
</dependency>
// DashScope (Alibaba Cloud)
import io.agentscope.core.model.DashScopeChatModel;
// OpenAI
import io.agentscope.core.model.OpenAIChatModel;
// Gemini (Google)
import io.agentscope.core.model.GeminiChatModel;
// Anthropic (Claude)
import io.agentscope.core.model.AnthropicChatModel;
// Ollama (Local models)
import io.agentscope.core.model.OllamaChatModel;
<!-- Long-term memory with Mem0 -->
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-mem0</artifactId>
<version>${agentscope.version}</version>
</dependency>
<!-- RAG with Dify -->
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-rag-dify</artifactId>
<version>${agentscope.version}</version>
</dependency>
AgentScope Java is a reactive, message-driven multi-agent framework built on Project Reactor and Java 17+.
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.Almost all operations (agent calls, model inference, tool execution) return Mono<T> or Flux<T>.
Msg objectsTarget Java 17 (LTS) for maximum compatibility:
var, Sealed classes)@Data, @Builder for DTOs/Messages)⚠️ CRITICAL: Avoid Preview Features
// ❌ WRONG - Requires Java 21 with --enable-preview
return switch (event) {
case PreReasoningEvent e -> Mono.just(e); // Pattern matching in switch
default -> Mono.just(event);
};
// ✅ CORRECT - Java 17 compatible
if (event instanceof PreReasoningEvent e) { // Pattern matching for instanceof (Java 17)
return Mono.just(event);
} else {
return Mono.just(event);
}
⚠️ NEVER BLOCK IN AGENT LOGIC
Blocking operations will break the reactive chain and cause performance issues.
Rules:
.block() in agent logic (only in main methods or tests)Mono for single results (e.g., agent.call())Flux for streaming responses (e.g., model.stream()).map(), .flatMap(), .then()Mono.defer() for lazy evaluationMono.deferContextual() for reactive context accessExample:
// ❌ WRONG - Blocking
public Mono<String> processData(String input) {
String result = externalService.call(input).block(); // DON'T DO THIS
return Mono.just(result);
}
// ✅ CORRECT - Non-blocking
public Mono<String> processData(String input) {
return externalService.call(input)
.map(this::transform)
.flatMap(this::validate);
}
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 contentThinkingBlock: For Chain of Thought (CoT) reasoningToolUseBlock: For tool callsToolResultBlock: For tool outputsHelper Methods:
// Prefer safe helper methods
String text = msg.getTextContent(); // Safe, returns null if not found
// Avoid direct access
String text = msg.getContent().get(0).getText(); // May throw NPE
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) {
// 1. Process inputs
if (msgs != null) {
msgs.forEach(memory::addMessage);
}
// 2. Call model or logic
return model.generate(memory.getMessages(), null, null)
.map(response -> Msg.builder()
.name(getName())
.role(MsgRole.ASSISTANT)
.content(TextBlock.builder().text(response.getText()).build())
.build());
}
}
Use @Tool annotation for function-based tools. Tools can return:
String (synchronous)Mono<String> (asynchronous)Mono<ToolResultBlock> (for complex results)⚠️ CRITICAL: @ToolParam Format
@ToolParam(name = "city", description = "City name")@ToolParam(name="city", description="...") (no spaces around =)@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) {
// Implementation
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) {
// SECURITY: Validate URL to prevent SSRF
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());
Hooks allow you to intercept and modify agent execution at various lifecycle points.
public interface Hook {
<T extends HookEvent> Mono<T> onEvent(T event);
default int priority() { return 100; } // Lower = higher priority
}
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)Java 17+ compatible (recommended):
Hook loggingHook = new Hook() {
@Override
public <T extends HookEvent> Mono<T> onEvent(T event) {
// Use if-instanceof instead of switch patterns (Java 17 compatible)
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; // Low priority (logging)
}
};
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:
Pipelines orchestrate multiple agents in structured workflows.
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();
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:
Memory memory = new InMemoryMemory();
// Configure long-term memory
LongTermMemory longTermMemory = Mem0LongTermMemory.builder()
.apiKey(System.getenv("MEM0_API_KEY"))
.userId("user_123")
.build();
// Use with agent
ReActAgent agent = ReActAgent.builder()
.name("Assistant")
.model(model)
.longTermMemory(longTermMemory)
.longTermMemoryMode(LongTermMemoryMode.BOTH) // STATIC_CONTROL, AGENTIC, or BOTH
.build();
Memory Modes:
STATIC_CONTROL: Framework automatically manages memory (via hooks)AGENTIC: Agent decides when to use memory (via tools)BOTH: Combines both approachesAgentScope supports MCP for integrating external tools and resources.
// Create MCP client
// SECURITY: In production, use a specific version or a local binary to prevent supply chain attacks
McpClientWrapper mcpClient = McpClientBuilder.stdio()
.command("npx")
.args("-y", "@modelcontextprotocol/[email protected]", "/path/to/files") // Always pin versions
.build();
// Register with toolkit
Toolkit toolkit = new Toolkit();
toolkit.registration()
.mcpClient(mcpClient)
.enableTools(List.of("read_file", "write_file"))
.apply();
// Use with agent
ReActAgent agent = ReActAgent.builder()
.name("Assistant")
.model(model)
.toolkit(toolkit)
.build();
@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();
}
@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();
// Test agent behavior
}
Testing Best Practices:
StepVerifierprivate static final Logger log = LoggerFactory.getLogger(MyClass.class);
// Use parameterized logging
log.info("Processing message from user: {}", userId);
log.error("Failed to call model: {}", modelName, exception);
// Prefer specific error messages
return Mono.error(new IllegalArgumentException(
"Invalid model name: " + modelName + ". Expected one of: " + VALID_MODELS));
// Use onErrorResume for graceful degradation
return model.generate(msgs, null, null)
.onErrorResume(e -> {
log.error("Model call failed, using fallback", e);
return Mono.just(fallbackResponse);
});
// Use Optional for nullable returns
public Optional<AgentTool> findTool(String name) {
return Optional.ofNullable(tools.get(name));
}
// Use Objects.requireNonNull for validation
public MyAgent(Model model) {
this.model = Objects.requireNonNull(model, "Model cannot be null");
}
// Use Javadoc for public APIs
/**
* Creates a new agent with the specified configuration.
*
* @param name The agent name (must be unique)
* @param model The LLM model to use
* @return Configured agent instance
* @throws IllegalArgumentException if name is null or empty
*/
public static ReActAgent create(String name, Model model) {
// Implementation
}
// Use inline comments sparingly, only for complex logic
// Calculate exponential backoff: 2^attempt * baseDelay
Duration delay = Duration.ofMillis((long) Math.pow(2, attempt) * baseDelayMs);
Mono, Flux for reactive programmingprivate static final Logger log = LoggerFactory.getLogger(MyClass.class);)Block in reactive chains
// ❌ WRONG
return someMonoOperation().block();
Use Thread.sleep() or blocking I/O
// ❌ WRONG
Thread.sleep(1000);
// ✅ CORRECT
return Mono.delay(Duration.ofSeconds(1));
Mutate shared state without synchronization
// ❌ WRONG
private List<Msg> messages = new ArrayList<>();
public void addMessage(Msg msg) {
messages.add(msg); // Not thread-safe
}
Ignore errors silently
// ❌ WRONG
.onErrorResume(e -> Mono.empty())
// ✅ CORRECT
.onErrorResume(e -> {
log.error("Operation failed", e);
return Mono.just(fallbackValue);
})
Use ThreadLocal in reactive code
// ❌ WRONG
ThreadLocal<String> context = new ThreadLocal<>();
// ✅ CORRECT
return Mono.deferContextual(ctx -> {
String value = ctx.get("key");
// Use value
});
Create agents without proper resource management
// ❌ WRONG - No cleanup
public void processRequests() {
for (int i = 0; i < 1000; i++) {
ReActAgent agent = createAgent();
agent.call(msg).block();
}
}
Hardcode API keys or secrets
// ❌ WRONG
String apiKey = "sk-1234567890";
// ✅ CORRECT
String apiKey = System.getenv("OPENAI_API_KEY");
Use Java preview features (requires --enable-preview)
// ❌ WRONG - Requires Java 21 with --enable-preview
return switch (event) {
case PreReasoningEvent e -> handleReasoning(e);
case PostActingEvent e -> handleActing(e);
default -> Mono.just(event);
};
// ✅ CORRECT - Java 17 compatible
if (event instanceof PreReasoningEvent e) {
return handleReasoning(e);
} else if (event instanceof PostActingEvent e) {
return handleActing(e);
} else {
return Mono.just(event);
}
// WRONG
Msg response = agent.call(msg).block(); // Don't block in agent logic
// CORRECT
return agent.call(msg)
.flatMap(response -> processResponse(response));
// WRONG
String text = msg.getContent().get(0).getText(); // May throw NPE
// CORRECT
String text = msg.getTextContent(); // Safe helper method
// OR
String text = msg.getContentBlocks(TextBlock.class).stream()
.findFirst()
.map(TextBlock::getText)
.orElse("");
// WRONG
ThreadLocal<String> context = new ThreadLocal<>(); // May not work in reactive streams
// CORRECT
return Mono.deferContextual(ctx -> {
String value = ctx.get("key");
// Use value
});
// WRONG
.onErrorResume(e -> Mono.empty()) // Silent failure
// CORRECT
.onErrorResume(e -> {
log.error("Failed to process: {}", input, e);
return Mono.just(createErrorResponse(e));
})
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;
/**
* Complete example demonstrating AgentScope best practices.
*/
public class CompleteExample {
private static final Logger log = LoggerFactory.getLogger(CompleteExample.class);
public static void main(String[] args) {
// 1. Create model (no .temperature() method, use defaultOptions)
Model model = DashScopeChatModel.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.modelName("qwen-plus")
.stream(true)
.build();
// 2. Create toolkit with tools
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new WeatherTools());
toolkit.registerTool(new TimeTools());
// 3. Create hook for streaming output
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; // Low priority
}
};
// 4. Build agent
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();
// 5. Use agent
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: ");
// ⚠️ IMPORTANT: .block() is ONLY allowed in main() methods for demo purposes
// NEVER use .block() in agent logic, service methods, or library code
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());
}
}
/**
* Example tool class for weather information.
*/
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);
// Simulate API call
return String.format("Weather in %s: Sunny, 22°C, Light breeze", city);
}
}
/**
* Example tool class for time information.
*/
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;
}
}
}
ReActAgent agent = ReActAgent.builder()
.name("AgentName")
.sysPrompt("System prompt")
.model(model)
.toolkit(toolkit)
.memory(memory)
.hooks(hooks)
.maxIters(10)
.build();
Msg msg = Msg.builder()
.role(MsgRole.USER)
.content(TextBlock.builder().text("Hello").build())
.build();
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new MyTools());
Hook hook = new Hook() {
public <T extends HookEvent> Mono<T> onEvent(T event) {
// Handle event
return Mono.just(event);
}
};
SequentialPipeline pipeline = SequentialPipeline.builder()
.addAgent(agent1)
.addAgent(agent2)
.build();
tools
Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.
data-ai
Lists tables, describes columns and data types, identifies foreign key relationships, and maps entity relationships in the database. Use when the user asks about database structure, table layout, column types, what tables exist, foreign keys, or how entities relate to each other.
documentation
Writes and executes SQL queries ranging from simple single-table SELECTs to complex multi-table JOINs, aggregations, window functions, and subqueries. Use when the user asks to query the database, retrieve data, filter records, rank results, or generate reports.
documentation
Discover schema, write SELECT-only SQLite queries, execute, and explain results (aligned with harness-example).