diff --git a/src/main/java/com/claudecode/config/AppConfig.java b/src/main/java/com/claudecode/config/AppConfig.java index 1968526..deea100 100644 --- a/src/main/java/com/claudecode/config/AppConfig.java +++ b/src/main/java/com/claudecode/config/AppConfig.java @@ -104,8 +104,12 @@ public class AppConfig { new TaskGetTool(), new TaskListTool(), new TaskUpdateTool(), + new TaskStopTool(), + new TaskOutputTool(), // P2: 配置工具 - new ConfigTool() + new ConfigTool(), + // P2: 实用工具 + new SleepTool() ); // P2: 注册 MCP 工具桥接(将远程 MCP 工具映射为本地工具) diff --git a/src/main/java/com/claudecode/tool/impl/SleepTool.java b/src/main/java/com/claudecode/tool/impl/SleepTool.java new file mode 100644 index 0000000..63cb7ac --- /dev/null +++ b/src/main/java/com/claudecode/tool/impl/SleepTool.java @@ -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。 + *

+ * 等待指定时长。用于暂停操作、等待外部进程、或用户要求休眠。 + * 支持通过中断取消等待。 + */ +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 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 input) { + Number ms = (Number) input.getOrDefault("duration_ms", 0); + return "💤 Sleeping " + (ms.longValue() / 1000.0) + "s"; + } +} diff --git a/src/main/java/com/claudecode/tool/impl/TaskOutputTool.java b/src/main/java/com/claudecode/tool/impl/TaskOutputTool.java new file mode 100644 index 0000000..f014dc1 --- /dev/null +++ b/src/main/java/com/claudecode/tool/impl/TaskOutputTool.java @@ -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。 + *

+ * 获取任务的执行输出/结果。当任务完成后,可以通过此工具读取其结果。 + * 对于正在运行的任务,返回当前状态信息。 + */ +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 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 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 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"); + } +} diff --git a/src/main/java/com/claudecode/tool/impl/TaskStopTool.java b/src/main/java/com/claudecode/tool/impl/TaskStopTool.java new file mode 100644 index 0000000..bdd5b50 --- /dev/null +++ b/src/main/java/com/claudecode/tool/impl/TaskStopTool.java @@ -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。 + *

+ * 停止一个正在运行的后台任务。通过 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 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 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 input) { + return "🛑 Stopping task " + input.getOrDefault("task_id", "..."); + } + + private String escapeJson(String s) { + if (s == null) return ""; + return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n"); + } +}