feat: add TaskStopTool, TaskOutputTool, and SleepTool
- TaskStopTool: cancel running tasks via TaskManager.cancelTask() - TaskOutputTool: retrieve task results with status-aware formatting - SleepTool: Thread.sleep() with 5-minute max and interrupt support - Registered all 3 tools in AppConfig (total: 22 tools) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -104,8 +104,12 @@ public class AppConfig {
|
|||||||
new TaskGetTool(),
|
new TaskGetTool(),
|
||||||
new TaskListTool(),
|
new TaskListTool(),
|
||||||
new TaskUpdateTool(),
|
new TaskUpdateTool(),
|
||||||
|
new TaskStopTool(),
|
||||||
|
new TaskOutputTool(),
|
||||||
// P2: 配置工具
|
// P2: 配置工具
|
||||||
new ConfigTool()
|
new ConfigTool(),
|
||||||
|
// P2: 实用工具
|
||||||
|
new SleepTool()
|
||||||
);
|
);
|
||||||
|
|
||||||
// P2: 注册 MCP 工具桥接(将远程 MCP 工具映射为本地工具)
|
// P2: 注册 MCP 工具桥接(将远程 MCP 工具映射为本地工具)
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.claudecode.tool.impl;
|
||||||
|
|
||||||
|
import com.claudecode.tool.Tool;
|
||||||
|
import com.claudecode.tool.ToolContext;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sleep 工具 —— 对应 claude-code/src/tools/SleepTool。
|
||||||
|
* <p>
|
||||||
|
* 等待指定时长。用于暂停操作、等待外部进程、或用户要求休眠。
|
||||||
|
* 支持通过中断取消等待。
|
||||||
|
*/
|
||||||
|
public class SleepTool implements Tool {
|
||||||
|
|
||||||
|
private static final long MAX_DURATION_MS = 300_000; // 5 minutes max
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "Sleep";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String description() {
|
||||||
|
return """
|
||||||
|
Wait for a specified duration in milliseconds. The user can interrupt the sleep at \
|
||||||
|
any time. Use this when:
|
||||||
|
- The user tells you to sleep or rest
|
||||||
|
- You have nothing to do and are waiting for something
|
||||||
|
- You need to wait for an external process to complete
|
||||||
|
Maximum duration: 300000ms (5 minutes).""";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String inputSchema() {
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"duration_ms": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Duration to sleep in milliseconds (max: 300000)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["duration_ms"]
|
||||||
|
}""";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReadOnly() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String execute(Map<String, Object> input, ToolContext context) {
|
||||||
|
Number durationNum = (Number) input.get("duration_ms");
|
||||||
|
if (durationNum == null) {
|
||||||
|
return "Error: 'duration_ms' is required";
|
||||||
|
}
|
||||||
|
|
||||||
|
long durationMs = durationNum.longValue();
|
||||||
|
if (durationMs <= 0) {
|
||||||
|
return "Error: duration_ms must be positive";
|
||||||
|
}
|
||||||
|
if (durationMs > MAX_DURATION_MS) {
|
||||||
|
durationMs = MAX_DURATION_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
Thread.sleep(durationMs);
|
||||||
|
long elapsed = System.currentTimeMillis() - startTime;
|
||||||
|
return String.format("Slept for %d ms", elapsed);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
long elapsed = System.currentTimeMillis() - startTime;
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return String.format("Sleep interrupted after %d ms (requested %d ms)", elapsed, durationMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String activityDescription(Map<String, Object> input) {
|
||||||
|
Number ms = (Number) input.getOrDefault("duration_ms", 0);
|
||||||
|
return "💤 Sleeping " + (ms.longValue() / 1000.0) + "s";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package com.claudecode.tool.impl;
|
||||||
|
|
||||||
|
import com.claudecode.core.TaskManager;
|
||||||
|
import com.claudecode.core.TaskManager.TaskInfo;
|
||||||
|
import com.claudecode.tool.Tool;
|
||||||
|
import com.claudecode.tool.ToolContext;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TaskOutput 工具 —— 对应 claude-code/src/tools/TaskOutputTool。
|
||||||
|
* <p>
|
||||||
|
* 获取任务的执行输出/结果。当任务完成后,可以通过此工具读取其结果。
|
||||||
|
* 对于正在运行的任务,返回当前状态信息。
|
||||||
|
*/
|
||||||
|
public class TaskOutputTool implements Tool {
|
||||||
|
|
||||||
|
private static final String TASK_MANAGER_KEY = "TASK_MANAGER";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "TaskOutput";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String description() {
|
||||||
|
return """
|
||||||
|
Get the output/result of a task. Use this to retrieve the result of a completed task, \
|
||||||
|
or to check the current status of a running task. For completed tasks, returns the full \
|
||||||
|
execution result. For running tasks, returns the current status.""";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String inputSchema() {
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"task_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The ID of the task to get output from"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["task_id"]
|
||||||
|
}""";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReadOnly() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String execute(Map<String, Object> input, ToolContext context) {
|
||||||
|
String taskId = (String) input.get("task_id");
|
||||||
|
|
||||||
|
if (taskId == null || taskId.isBlank()) {
|
||||||
|
return "Error: 'task_id' is required";
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskManager taskManager = context.getOrDefault(TASK_MANAGER_KEY, null);
|
||||||
|
if (taskManager == null) {
|
||||||
|
return "Error: TaskManager is not available";
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<TaskInfo> taskOpt = taskManager.getTask(taskId);
|
||||||
|
if (taskOpt.isEmpty()) {
|
||||||
|
return "Error: Task not found: " + taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskInfo task = taskOpt.get();
|
||||||
|
|
||||||
|
return switch (task.status()) {
|
||||||
|
case COMPLETED -> {
|
||||||
|
String result = task.result();
|
||||||
|
yield String.format("""
|
||||||
|
{"task_id": "%s", "status": "COMPLETED", "description": "%s", "result": "%s"}""",
|
||||||
|
taskId, escapeJson(task.description()),
|
||||||
|
escapeJson(result != null ? result : "(no output)"));
|
||||||
|
}
|
||||||
|
case FAILED -> {
|
||||||
|
String error = task.result();
|
||||||
|
yield String.format("""
|
||||||
|
{"task_id": "%s", "status": "FAILED", "description": "%s", "error": "%s"}""",
|
||||||
|
taskId, escapeJson(task.description()),
|
||||||
|
escapeJson(error != null ? error : "(unknown error)"));
|
||||||
|
}
|
||||||
|
case CANCELLED -> String.format("""
|
||||||
|
{"task_id": "%s", "status": "CANCELLED", "description": "%s"}""",
|
||||||
|
taskId, escapeJson(task.description()));
|
||||||
|
case RUNNING -> String.format("""
|
||||||
|
{"task_id": "%s", "status": "RUNNING", "description": "%s", \
|
||||||
|
"message": "Task is still running. Check back later."}""",
|
||||||
|
taskId, escapeJson(task.description()));
|
||||||
|
case PENDING -> String.format("""
|
||||||
|
{"task_id": "%s", "status": "PENDING", "description": "%s", \
|
||||||
|
"message": "Task has not started yet."}""",
|
||||||
|
taskId, escapeJson(task.description()));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String activityDescription(Map<String, Object> input) {
|
||||||
|
return "📋 Getting output of task " + input.getOrDefault("task_id", "...");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escapeJson(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package com.claudecode.tool.impl;
|
||||||
|
|
||||||
|
import com.claudecode.core.TaskManager;
|
||||||
|
import com.claudecode.core.TaskManager.TaskInfo;
|
||||||
|
import com.claudecode.tool.Tool;
|
||||||
|
import com.claudecode.tool.ToolContext;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TaskStop 工具 —— 对应 claude-code/src/tools/TaskStopTool。
|
||||||
|
* <p>
|
||||||
|
* 停止一个正在运行的后台任务。通过 TaskManager.cancelTask() 取消任务执行。
|
||||||
|
*/
|
||||||
|
public class TaskStopTool implements Tool {
|
||||||
|
|
||||||
|
private static final String TASK_MANAGER_KEY = "TASK_MANAGER";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "TaskStop";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String description() {
|
||||||
|
return """
|
||||||
|
Stop a running background task by its ID. Use this tool when you need to terminate \
|
||||||
|
a long-running task that is no longer needed, or when a task appears to be stuck. \
|
||||||
|
Returns a success or failure status. Tasks in terminal states (COMPLETED/FAILED/CANCELLED) \
|
||||||
|
cannot be stopped.""";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String inputSchema() {
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"task_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The ID of the task to stop"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["task_id"]
|
||||||
|
}""";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String execute(Map<String, Object> input, ToolContext context) {
|
||||||
|
String taskId = (String) input.get("task_id");
|
||||||
|
|
||||||
|
if (taskId == null || taskId.isBlank()) {
|
||||||
|
return "Error: 'task_id' is required";
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskManager taskManager = context.getOrDefault(TASK_MANAGER_KEY, null);
|
||||||
|
if (taskManager == null) {
|
||||||
|
return "Error: TaskManager is not available";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if task exists first
|
||||||
|
Optional<TaskInfo> taskOpt = taskManager.getTask(taskId);
|
||||||
|
if (taskOpt.isEmpty()) {
|
||||||
|
return "Error: Task not found: " + taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskInfo task = taskOpt.get();
|
||||||
|
String previousStatus = task.status().name();
|
||||||
|
|
||||||
|
boolean cancelled = taskManager.cancelTask(taskId);
|
||||||
|
if (cancelled) {
|
||||||
|
return String.format("""
|
||||||
|
{"status": "stopped", "task_id": "%s", "previous_status": "%s", \
|
||||||
|
"description": "%s"}""",
|
||||||
|
taskId, previousStatus, escapeJson(task.description()));
|
||||||
|
} else {
|
||||||
|
return String.format(
|
||||||
|
"Error: Cannot stop task %s — it is already in terminal state: %s",
|
||||||
|
taskId, previousStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String activityDescription(Map<String, Object> input) {
|
||||||
|
return "🛑 Stopping task " + input.getOrDefault("task_id", "...");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escapeJson(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user