Files
claude-code/src/main/java/io/mybatis/learn/s10/S10TeamProtocols.java
T
2026-03-25 00:15:00 +08:00

316 lines
13 KiB
Java

package io.mybatis.learn.s10;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.mybatis.learn.core.AgentRunner;
import io.mybatis.learn.core.team.MessageBus;
import io.mybatis.learn.core.tools.*;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* S10 - Team Protocols:关闭协议 + 计划审批协议
*
* TIPS: 对应Python s10_team_protocols.py。
* 在S09基础上新增3个Lead工具和2个Teammate工具。
*
* 关闭协议FSM: pending → approved | rejected
* Lead调用 shutdown_request → Teammate收到 shutdown_request 消息
* → Teammate调用 shutdown_response → Lead收件箱收到响应
*
* 计划审批FSM: pending → approved | rejected
* Teammate调用 plan_approval → Lead收件箱收到计划
* → Lead调用 plan_approval(review) → Teammate收到审批结果
*
* 关键洞察:"Same request_id correlation pattern, two domains."
*/
@SpringBootApplication(scanBasePackages = "io.mybatis.learn.core")
public class S10TeamProtocols implements CommandLineRunner {
@Autowired
private ChatModel chatModel;
@Override
public void run(String... args) throws Exception {
Path workDir = Path.of(System.getProperty("user.dir"));
Path teamDir = workDir.resolve(".team");
MessageBus bus = new MessageBus(teamDir.resolve("inbox"));
ProtocolTracker tracker = new ProtocolTracker(bus);
S10TeammateManager team = new S10TeammateManager(chatModel, bus, tracker, teamDir);
S10LeadTools leadTools = new S10LeadTools(bus, team, tracker);
ObjectMapper mapper = new ObjectMapper();
String systemPrompt = "You are a team lead at " + workDir
+ ". Manage teammates with shutdown and plan approval protocols.";
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem(systemPrompt)
.defaultTools(new BashTool(), new ReadFileTool(),
new WriteFileTool(), new EditFileTool(), leadTools)
.build();
AgentRunner.interactive("s10", input -> {
if ("/team".equals(input)) return team.listAll();
if ("/inbox".equals(input)) {
try { return mapper.writeValueAsString(bus.readInbox("lead")); }
catch (Exception e) { return "Error: " + e.getMessage(); }
}
var inbox = bus.readInbox("lead");
String fullInput = input;
if (!inbox.isEmpty()) {
try {
fullInput = "<inbox>" + mapper.writeValueAsString(inbox)
+ "</inbox>\n\n" + input;
} catch (Exception e) { /* ignore */ }
}
return chatClient.prompt(fullInput).call().content();
});
}
// ---- 队友管理器(增加协议处理) ----
/**
* TIPS: 对应Python s10第134-290行的TeammateManager。
* 相比S09增加了:
* 1. should_exit标志 - 队友批准关闭后退出循环
* 2. shutdown_response工具 - 更新tracker + 发送响应消息
* 3. plan_approval工具 - 提交计划到lead收件箱
*/
static class S10TeammateManager {
private final ChatModel chatModel;
private final MessageBus bus;
private final ProtocolTracker tracker;
private final Path configPath;
private final ObjectMapper mapper = new ObjectMapper();
private Map<String, Object> config;
S10TeammateManager(ChatModel chatModel, MessageBus bus,
ProtocolTracker tracker, Path teamDir) {
this.chatModel = chatModel;
this.bus = bus;
this.tracker = tracker;
this.configPath = teamDir.resolve("config.json");
try { Files.createDirectories(teamDir); } catch (IOException e) { throw new RuntimeException(e); }
this.config = loadConfig();
}
@SuppressWarnings("unchecked")
private Map<String, Object> loadConfig() {
if (Files.exists(configPath)) {
try { return mapper.readValue(configPath.toFile(), Map.class); } catch (IOException e) { /* ignore */ }
}
Map<String, Object> cfg = new LinkedHashMap<>();
cfg.put("team_name", "default");
cfg.put("members", new ArrayList<>());
return cfg;
}
private synchronized void saveConfig() {
try { mapper.writerWithDefaultPrettyPrinter().writeValue(configPath.toFile(), config); }
catch (IOException e) { /* ignore */ }
}
@SuppressWarnings("unchecked")
private Map<String, Object> findMember(String name) {
for (Map<String, Object> m : (List<Map<String, Object>>) config.get("members")) {
if (name.equals(m.get("name"))) return m;
}
return null;
}
private synchronized void setStatus(String name, String status) {
Map<String, Object> member = findMember(name);
if (member != null) { member.put("status", status); saveConfig(); }
}
@SuppressWarnings("unchecked")
public synchronized String spawn(String name, String role, String prompt) {
Map<String, Object> member = findMember(name);
if (member != null) {
String status = (String) member.get("status");
if (!"idle".equals(status) && !"shutdown".equals(status))
return "Error: '" + name + "' is currently " + status;
member.put("status", "working");
member.put("role", role);
} else {
member = new LinkedHashMap<>();
member.put("name", name);
member.put("role", role);
member.put("status", "working");
((List<Map<String, Object>>) config.get("members")).add(member);
}
saveConfig();
Thread.startVirtualThread(() -> teammateLoop(name, role, prompt));
return "Spawned '" + name + "' (role: " + role + ")";
}
private void teammateLoop(String name, String role, String initialPrompt) {
String workDir = System.getProperty("user.dir");
String sysPrompt = String.format(
"You are '%s', role: %s, at %s. "
+ "Submit plans via plan_approval before major work. "
+ "Respond to shutdown_request with shutdown_response.",
name, role, workDir);
// 队友协议工具
var protocolTool = new TeammateProtocolTool(bus, tracker, name);
var messageTool = new io.mybatis.learn.s09.TeammateManager.TeammateMessageTool(bus, name);
ChatClient client = ChatClient.builder(chatModel)
.defaultSystem(sysPrompt)
.defaultTools(new BashTool(), new ReadFileTool(),
new WriteFileTool(), new EditFileTool(),
messageTool, protocolTool)
.build();
try {
client.prompt(initialPrompt).call().content();
for (int round = 0; round < 50; round++) {
Thread.sleep(2000);
var inbox = bus.readInbox(name);
if (inbox.isEmpty()) break;
// 检查是否有关闭请求
boolean hasShutdown = inbox.stream()
.anyMatch(m -> "shutdown_request".equals(m.get("type")));
String inboxJson = mapper.writeValueAsString(inbox);
client.prompt("<inbox>" + inboxJson + "</inbox>").call().content();
if (hasShutdown) {
setStatus(name, "shutdown");
return;
}
}
} catch (Exception e) {
System.err.println(" [" + name + "] Error: " + e.getMessage());
}
setStatus(name, "idle");
}
@SuppressWarnings("unchecked")
public String listAll() {
List<Map<String, Object>> members = (List<Map<String, Object>>) config.get("members");
if (members.isEmpty()) return "No teammates.";
StringBuilder sb = new StringBuilder("Team: " + config.get("team_name"));
for (Map<String, Object> m : members)
sb.append("\n ").append(m.get("name"))
.append(" (").append(m.get("role")).append("): ").append(m.get("status"));
return sb.toString();
}
@SuppressWarnings("unchecked")
public List<String> memberNames() {
return ((List<Map<String, Object>>) config.get("members")).stream()
.map(m -> (String) m.get("name")).toList();
}
}
// ---- 队友协议工具 ----
public static class TeammateProtocolTool {
private final MessageBus bus;
private final ProtocolTracker tracker;
private final String name;
public TeammateProtocolTool(MessageBus bus, ProtocolTracker tracker, String name) {
this.bus = bus;
this.tracker = tracker;
this.name = name;
}
@Tool(description = "Respond to a shutdown request. Approve to shut down, reject to keep working.")
public String shutdownResponse(
@ToolParam(description = "The request_id from shutdown request") String requestId,
@ToolParam(description = "true to approve shutdown") boolean approve,
@ToolParam(description = "Reason for decision") String reason) {
return tracker.respondToShutdown(name, requestId, approve, reason);
}
@Tool(description = "Submit a plan for lead approval before major work")
public String planApproval(
@ToolParam(description = "Plan text to submit") String plan) {
return tracker.submitPlan(name, plan);
}
}
// ---- Lead工具集(12个工具 = S09的9个 + 3个协议工具) ----
public static class S10LeadTools {
private final MessageBus bus;
private final S10TeammateManager team;
private final ProtocolTracker tracker;
private final ObjectMapper mapper = new ObjectMapper();
public S10LeadTools(MessageBus bus, S10TeammateManager team, ProtocolTracker tracker) {
this.bus = bus;
this.team = team;
this.tracker = tracker;
}
@Tool(description = "Spawn a persistent teammate")
public String spawnTeammate(String name, String role, String prompt) {
return team.spawn(name, role, prompt);
}
@Tool(description = "List all teammates")
public String listTeammates() { return team.listAll(); }
@Tool(description = "Send a message to a teammate's inbox")
public String sendMessage(String to, String content) {
return bus.send("lead", to, content);
}
@Tool(description = "Read and drain the lead's inbox")
public String readInbox() {
try { return mapper.writeValueAsString(bus.readInbox("lead")); }
catch (Exception e) { return "[]"; }
}
@Tool(description = "Broadcast message to all teammates")
public String broadcast(String content) {
return bus.broadcast("lead", content, team.memberNames());
}
@Tool(description = "Request a teammate to shut down gracefully. Returns request_id for tracking.")
public String shutdownRequest(
@ToolParam(description = "Teammate name to shut down") String teammate) {
return tracker.handleShutdownRequest(teammate);
}
@Tool(description = "Check shutdown request status by request_id")
public String shutdownResponse(
@ToolParam(description = "The request_id to check") String requestId) {
return tracker.checkShutdownStatus(requestId);
}
@Tool(description = "Approve or reject a teammate's plan")
public String planApproval(
@ToolParam(description = "Plan request_id") String requestId,
@ToolParam(description = "true to approve") boolean approve,
@ToolParam(description = "Feedback text") String feedback) {
return tracker.reviewPlan(requestId, approve, feedback);
}
}
public static void main(String[] args) {
SpringApplication.run(S10TeamProtocols.class, args);
}
}