/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 liangxingguang/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
# 技能(Skill) 一个 skill 就是一份写好的能力包:一个目录里放一份 `SKILL.md`(说明用途、给 agent 看的指令),可以再带一些参考文档、脚本或样例。写好后丢给 agent,它会在合适的时候自己用。 harness 让你从两个地方装 skill: - **接 skill 市场**:Git 仓库、Nacos、MySQL、classpath、或者自己写的后端 - **放在工作区**:项目里 `workspace/skills/` 下的就所有人共用;放在 `<userId>/skills/` 下的只有那个用户看得到 两类来源同时生效,不需要二选一。 > 关于 skill 自身的结构、`SKILL.md` 写法、资源加载、tool 绑定、代码执行这些通用概念,见 [Agent Skill](../task/agent-skill.md)。本文只讲 harness 这一层的用法。 --- ## 一个例子 把团队的 skill 仓库接进来,agent 立刻就能用: ```java HarnessAgent agent = HarnessAgent.bui
data-ai
Answer a quantitative business question by writing a SQL query against the data warehouse, validating it, and presenting the result. Use when the user asks "how many...", "what's the trend of...", "compare X vs Y over...", "what's our top N...", or anything that resolves to a query against tabular data. Produces a small result table plus the underlying query.
content-media
Visualise the result of an analysis as a chart (line, bar, area, scatter, etc.). Use when the user asks to "plot...", "chart...", "show me the trend of...", "visualise...", or when a numerical result has more than ~10 rows and would be easier to read as a picture. Produces an image file plus the script that generated it.
testing
Produces a citation-grounded summary across one or more workspace source files for summaries, briefings, literature reviews, or comparisons.