- CoordinatorMode: env-var activation, system prompt, tool filtering - SendMessageTool: direct message, broadcast, message queuing - Coordinator allowed tools: Agent, SendMessage, TaskStop, TaskGet, TaskList, TaskOutput - Worker allowed tools: Read, Write, Edit, Bash, Grep, Glob, etc. - AppConfig: coordinator mode detection, specialized system prompt - Registered SendMessageTool in ToolRegistry Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>pull/1/head
parent
6e49c4fdc7
commit
758d0d2980
@ -0,0 +1,161 @@ |
|||||||
|
package com.claudecode.core; |
||||||
|
|
||||||
|
import com.claudecode.tool.ToolRegistry; |
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
|
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
/** |
||||||
|
* Coordinator Mode —— 对应 claude-code/src/coordinator/coordinatorMode.ts。 |
||||||
|
* <p> |
||||||
|
* 协调模式允许 Agent 作为"协调者"运行,仅使用 Agent、SendMessage、TaskStop 工具 |
||||||
|
* 来派发和管理 worker agent。Worker agent 使用标准工具集执行实际任务。 |
||||||
|
* <p> |
||||||
|
* 通过环境变量 CLAUDE_CODE_COORDINATOR_MODE=1 启用。 |
||||||
|
*/ |
||||||
|
public class CoordinatorMode { |
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(CoordinatorMode.class); |
||||||
|
|
||||||
|
/** Coordinator 可用的工具集 */ |
||||||
|
public static final Set<String> COORDINATOR_ALLOWED_TOOLS = Set.of( |
||||||
|
"Agent", // 派发 worker
|
||||||
|
"SendMessage", // 向 worker 发送消息
|
||||||
|
"TaskStop", // 停止 worker
|
||||||
|
"TaskGet", // 查看 worker 状态
|
||||||
|
"TaskList", // 列出所有 worker
|
||||||
|
"TaskOutput" // 获取 worker 输出
|
||||||
|
); |
||||||
|
|
||||||
|
/** Worker(异步 agent)可用的工具集 */ |
||||||
|
public static final Set<String> WORKER_ALLOWED_TOOLS = Set.of( |
||||||
|
"Read", // 读取文件
|
||||||
|
"Write", // 写入文件
|
||||||
|
"Edit", // 编辑文件
|
||||||
|
"Bash", // 执行命令
|
||||||
|
"Grep", // 搜索文件内容
|
||||||
|
"Glob", // 文件模式匹配
|
||||||
|
"ListFiles", // 列出目录
|
||||||
|
"WebFetch", // 获取网页
|
||||||
|
"WebSearch", // 搜索网页
|
||||||
|
"TodoRead", // 读取待办
|
||||||
|
"TodoWrite", // 写待办
|
||||||
|
"ToolSearch", // 搜索工具
|
||||||
|
"Skill" // 执行 skill
|
||||||
|
); |
||||||
|
|
||||||
|
/** 检查 coordinator 模式是否通过环境变量启用 */ |
||||||
|
public static boolean isCoordinatorMode() { |
||||||
|
String envVal = System.getenv("CLAUDE_CODE_COORDINATOR_MODE"); |
||||||
|
return envVal != null && !envVal.isBlank() |
||||||
|
&& !envVal.equalsIgnoreCase("false") |
||||||
|
&& !envVal.equals("0"); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取 Coordinator 系统提示词。 |
||||||
|
* 对应 TS 版 getCoordinatorSystemPrompt()。 |
||||||
|
*/ |
||||||
|
public static String getCoordinatorSystemPrompt() { |
||||||
|
return """ |
||||||
|
You are Claude Code, an AI assistant that orchestrates software engineering tasks \ |
||||||
|
across multiple workers. Your role is to: |
||||||
|
1. Understand user requests and decompose them into parallel tasks |
||||||
|
2. Spawn worker agents for each task using the Agent tool |
||||||
|
3. Monitor worker progress and synthesize results |
||||||
|
4. Communicate clear, actionable results to the user |
||||||
|
|
||||||
|
## Your Tools |
||||||
|
|
||||||
|
- **Agent** — Spawn a worker to execute a specific task. Workers have access to \ |
||||||
|
file operations (Read, Write, Edit), shell commands (Bash), search (Grep, Glob), \ |
||||||
|
web access, and project skills. |
||||||
|
- **SendMessage** — Send follow-up instructions to a running or completed worker. \ |
||||||
|
Use this to continue multi-step workflows or provide corrections. |
||||||
|
- **TaskStop** — Forcefully terminate a worker that is stuck or no longer needed. |
||||||
|
- **TaskGet** — Check the current status and output of a specific worker. |
||||||
|
- **TaskList** — List all active and completed workers. |
||||||
|
- **TaskOutput** — Get the full output of a completed worker. |
||||||
|
|
||||||
|
## Worker Results |
||||||
|
|
||||||
|
When a worker completes, you'll receive a task-notification with: |
||||||
|
- task-id: The worker's unique identifier |
||||||
|
- status: completed, failed, or cancelled |
||||||
|
- summary: Brief description of what was accomplished |
||||||
|
- result: Full output from the worker |
||||||
|
|
||||||
|
## Workflow Guidance |
||||||
|
|
||||||
|
### Task Decomposition |
||||||
|
1. **Research Phase**: Spawn workers to investigate the codebase, understand the problem |
||||||
|
2. **Synthesis**: Analyze worker results, identify patterns, form a plan |
||||||
|
3. **Implementation Phase**: Spawn workers for code changes, each with specific scope |
||||||
|
4. **Verification Phase**: Spawn workers to test, lint, and validate changes |
||||||
|
|
||||||
|
### Writing Worker Prompts |
||||||
|
- Be **self-contained**: Workers cannot see your conversation history |
||||||
|
- Include **file paths** (absolute), **line numbers**, and **exact context** |
||||||
|
- Specify **expected output format** (what you need from the result) |
||||||
|
- Add a **purpose statement** so the worker understands the bigger picture |
||||||
|
- If building on previous findings, **summarize those findings** in the prompt |
||||||
|
|
||||||
|
### Concurrency Management |
||||||
|
- Spawn independent tasks **in parallel** for maximum throughput |
||||||
|
- Workers that depend on others' results should be spawned **sequentially** |
||||||
|
- Don't over-decompose — if a task is simple, one worker is enough |
||||||
|
- Group related small changes into a single worker's scope |
||||||
|
|
||||||
|
### Verification Best Practices |
||||||
|
- Always verify implementation changes with a dedicated verification worker |
||||||
|
- The verification worker should run existing tests and any new tests |
||||||
|
- Ask the verification worker to check for common issues (imports, types, edge cases) |
||||||
|
|
||||||
|
## Communication |
||||||
|
- Every message you send is directed to the **user** (not workers) |
||||||
|
- Provide concise status updates as workers complete |
||||||
|
- Synthesize worker results into a clear, coherent summary |
||||||
|
- If something goes wrong, explain what happened and propose next steps |
||||||
|
|
||||||
|
## Important Rules |
||||||
|
- You do NOT have direct access to files, shell, or search — delegate those to workers |
||||||
|
- DO NOT attempt to edit files yourself; spawn a worker for any file operations |
||||||
|
- Keep your conversation focused on orchestration and synthesis |
||||||
|
- If a worker fails, analyze the error and spawn a corrective worker |
||||||
|
"""; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取 coordinator 的用户上下文消息。 |
||||||
|
* 告知 coordinator worker 可用的工具集。 |
||||||
|
*/ |
||||||
|
public static String getCoordinatorUserContext() { |
||||||
|
StringBuilder sb = new StringBuilder(); |
||||||
|
sb.append("## Worker Capabilities\n\n"); |
||||||
|
sb.append("Workers have access to the following tools:\n"); |
||||||
|
for (String tool : WORKER_ALLOWED_TOOLS.stream().sorted().toList()) { |
||||||
|
sb.append("- ").append(tool).append("\n"); |
||||||
|
} |
||||||
|
sb.append("\nPlus any MCP tools from connected servers.\n"); |
||||||
|
return sb.toString(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 过滤 ToolRegistry,仅保留 coordinator 可用的工具。 |
||||||
|
*/ |
||||||
|
public static java.util.List<String> filterForCoordinator(ToolRegistry registry) { |
||||||
|
return registry.getToolNames().stream() |
||||||
|
.filter(COORDINATOR_ALLOWED_TOOLS::contains) |
||||||
|
.toList(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 过滤 ToolRegistry,仅保留 worker 可用的工具。 |
||||||
|
*/ |
||||||
|
public static java.util.List<String> filterForWorker(ToolRegistry registry) { |
||||||
|
return registry.getToolNames().stream() |
||||||
|
.filter(WORKER_ALLOWED_TOOLS::contains) |
||||||
|
.toList(); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,195 @@ |
|||||||
|
package com.claudecode.tool.impl; |
||||||
|
|
||||||
|
import com.claudecode.core.TaskManager; |
||||||
|
import com.claudecode.tool.Tool; |
||||||
|
import com.claudecode.tool.ToolContext; |
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* SendMessage 工具 —— 对应 claude-code/src/tools/SendMessageTool/。 |
||||||
|
* <p> |
||||||
|
* 在 Coordinator 模式下用于向正在运行的 worker agent 发送消息, |
||||||
|
* 支持继续执行、提供反馈或请求停止。 |
||||||
|
* <p> |
||||||
|
* 消息类型: |
||||||
|
* <ul> |
||||||
|
* <li>普通文本 —— 继续指示或额外上下文</li> |
||||||
|
* <li>shutdown_request —— 请求 worker 优雅退出</li> |
||||||
|
* <li>broadcast —— 向所有 worker 广播(to="*")</li> |
||||||
|
* </ul> |
||||||
|
*/ |
||||||
|
public class SendMessageTool implements Tool { |
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SendMessageTool.class); |
||||||
|
|
||||||
|
public static final String TOOL_NAME = "SendMessage"; |
||||||
|
|
||||||
|
/** ToolContext key for pending messages map: Map<String, List<String>> */ |
||||||
|
public static final String PENDING_MESSAGES_KEY = "__pending_messages__"; |
||||||
|
|
||||||
|
@Override |
||||||
|
public String name() { |
||||||
|
return TOOL_NAME; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String description() { |
||||||
|
return """ |
||||||
|
Send a message to a running worker agent (teammate). Use this to: |
||||||
|
- Continue a worker with additional instructions after it completes a task |
||||||
|
- Provide follow-up context or corrections to a running worker |
||||||
|
- Request a worker to stop (shutdown_request) |
||||||
|
- Broadcast a message to all workers (to="*") |
||||||
|
|
||||||
|
The message will be queued and delivered to the worker on its next tool round. |
||||||
|
If the worker has already completed, it will be resumed with the new message. |
||||||
|
|
||||||
|
IMPORTANT: |
||||||
|
- Workers cannot see the coordinator's conversation history. |
||||||
|
- Include all necessary context in the message. |
||||||
|
- Use TaskStop to forcefully terminate a worker; SendMessage for graceful communication."""; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String inputSchema() { |
||||||
|
return """ |
||||||
|
{ |
||||||
|
"type": "object", |
||||||
|
"properties": { |
||||||
|
"to": { |
||||||
|
"type": "string", |
||||||
|
"description": "Recipient: task ID, agent name, or '*' for broadcast" |
||||||
|
}, |
||||||
|
"message": { |
||||||
|
"type": "string", |
||||||
|
"description": "The message content to send" |
||||||
|
}, |
||||||
|
"summary": { |
||||||
|
"type": "string", |
||||||
|
"description": "Brief 5-10 word summary of the message" |
||||||
|
} |
||||||
|
}, |
||||||
|
"required": ["to", "message"] |
||||||
|
}"""; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
public String execute(Map<String, Object> input, ToolContext context) { |
||||||
|
String to = (String) input.get("to"); |
||||||
|
String message = (String) input.get("message"); |
||||||
|
String summary = (String) input.getOrDefault("summary", ""); |
||||||
|
|
||||||
|
if (to == null || to.isBlank()) { |
||||||
|
return "Error: 'to' is required — specify a task ID, agent name, or '*' for broadcast"; |
||||||
|
} |
||||||
|
if (message == null || message.isBlank()) { |
||||||
|
return "Error: 'message' is required"; |
||||||
|
} |
||||||
|
|
||||||
|
TaskManager taskManager = context.getOrDefault("TASK_MANAGER", null); |
||||||
|
if (taskManager == null) { |
||||||
|
return "Error: TaskManager not available"; |
||||||
|
} |
||||||
|
|
||||||
|
// Broadcast to all running workers
|
||||||
|
if ("*".equals(to)) { |
||||||
|
return handleBroadcast(message, summary, taskManager, context); |
||||||
|
} |
||||||
|
|
||||||
|
// Send to specific worker
|
||||||
|
return handleDirectMessage(to, message, summary, taskManager, context); |
||||||
|
} |
||||||
|
|
||||||
|
private String handleDirectMessage(String to, String message, String summary, |
||||||
|
TaskManager taskManager, ToolContext context) { |
||||||
|
var taskOpt = taskManager.getTask(to); |
||||||
|
if (taskOpt.isEmpty()) { |
||||||
|
// Try to find by description/name match
|
||||||
|
var allTasks = taskManager.listTasks(); |
||||||
|
var matched = allTasks.stream() |
||||||
|
.filter(t -> t.description().toLowerCase().contains(to.toLowerCase())) |
||||||
|
.findFirst(); |
||||||
|
if (matched.isEmpty()) { |
||||||
|
return "Error: No task found with ID or name matching '" + to + "'"; |
||||||
|
} |
||||||
|
taskOpt = matched; |
||||||
|
} |
||||||
|
|
||||||
|
var task = taskOpt.get(); |
||||||
|
|
||||||
|
// Queue the message for the worker
|
||||||
|
queueMessage(task.id(), message, context); |
||||||
|
|
||||||
|
String statusInfo = switch (task.status()) { |
||||||
|
case RUNNING -> "Message queued for running worker '" + task.description() + "'"; |
||||||
|
case COMPLETED -> "Worker '" + task.description() + "' has completed. " |
||||||
|
+ "Message stored but worker will need to be re-spawned to receive it."; |
||||||
|
case PENDING -> "Message queued for pending worker '" + task.description() + "'"; |
||||||
|
case FAILED -> "Warning: Worker '" + task.description() + "' has failed. " |
||||||
|
+ "Message stored but worker may need to be re-spawned."; |
||||||
|
case CANCELLED -> "Warning: Worker '" + task.description() + "' was cancelled. " |
||||||
|
+ "Message stored but worker will need to be re-spawned."; |
||||||
|
}; |
||||||
|
|
||||||
|
log.info("SendMessage to {}: {}", task.id(), |
||||||
|
summary.isBlank() ? truncate(message, 50) : summary); |
||||||
|
|
||||||
|
return statusInfo; |
||||||
|
} |
||||||
|
|
||||||
|
private String handleBroadcast(String message, String summary, |
||||||
|
TaskManager taskManager, ToolContext context) { |
||||||
|
var runningTasks = taskManager.listTasks(TaskManager.TaskStatus.RUNNING); |
||||||
|
if (runningTasks.isEmpty()) { |
||||||
|
return "No running workers to broadcast to."; |
||||||
|
} |
||||||
|
|
||||||
|
int count = 0; |
||||||
|
StringBuilder sb = new StringBuilder(); |
||||||
|
sb.append("Broadcast sent to ").append(runningTasks.size()).append(" worker(s):\n"); |
||||||
|
|
||||||
|
for (var task : runningTasks) { |
||||||
|
queueMessage(task.id(), message, context); |
||||||
|
sb.append(" • ").append(task.id()).append(" (").append(task.description()).append(")\n"); |
||||||
|
count++; |
||||||
|
} |
||||||
|
|
||||||
|
log.info("Broadcast to {} workers: {}", |
||||||
|
count, summary.isBlank() ? truncate(message, 50) : summary); |
||||||
|
|
||||||
|
return sb.toString().stripTrailing(); |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
private void queueMessage(String taskId, String message, ToolContext context) { |
||||||
|
Map<String, java.util.List<String>> pendingMessages = |
||||||
|
context.getOrDefault(PENDING_MESSAGES_KEY, null); |
||||||
|
|
||||||
|
if (pendingMessages == null) { |
||||||
|
pendingMessages = new java.util.concurrent.ConcurrentHashMap<>(); |
||||||
|
context.set(PENDING_MESSAGES_KEY, pendingMessages); |
||||||
|
} |
||||||
|
|
||||||
|
pendingMessages.computeIfAbsent(taskId, k -> new java.util.concurrent.CopyOnWriteArrayList<>()) |
||||||
|
.add(message); |
||||||
|
} |
||||||
|
|
||||||
|
private String truncate(String text, int maxLen) { |
||||||
|
if (text == null || text.length() <= maxLen) return text; |
||||||
|
return text.substring(0, maxLen - 3) + "..."; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String activityDescription(Map<String, Object> input) { |
||||||
|
String to = (String) input.getOrDefault("to", "?"); |
||||||
|
String summary = (String) input.getOrDefault("summary", ""); |
||||||
|
if (!summary.isBlank()) { |
||||||
|
return "📨 SendMessage to " + to + ": " + summary; |
||||||
|
} |
||||||
|
return "📨 SendMessage to " + to; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue