From 1ecef9dc3afd7363161d20ca8dc3c316dd337b28 Mon Sep 17 00:00:00 2001 From: abel533 Date: Wed, 25 Mar 2026 00:50:17 +0800 Subject: [PATCH] docs: update docs.json and versions.json --- web/src/data/generated/versions.json | 182 +++++++++++++-------------- 1 file changed, 91 insertions(+), 91 deletions(-) diff --git a/web/src/data/generated/versions.json b/web/src/data/generated/versions.json index 8f659ee..71bc029 100644 --- a/web/src/data/generated/versions.json +++ b/web/src/data/generated/versions.json @@ -170,7 +170,7 @@ "filename": "S04Subagent.java", "title": "Subagents", "subtitle": "Clean Context Per Subtask", - "loc": 88, + "loc": 92, "tools": [ "task" ], @@ -188,7 +188,7 @@ { "name": "SubagentTool", "startLine": 101, - "endLine": 150 + "endLine": 154 } ], "functions": [ @@ -230,26 +230,26 @@ { "name": "BashTool", "signature": "new BashTool(),", - "startLine": 133 + "startLine": 135 }, { "name": "ReadFileTool", "signature": "new ReadFileTool(),", - "startLine": 134 + "startLine": 136 }, { "name": "WriteFileTool", "signature": "new WriteFileTool(),", - "startLine": 135 + "startLine": 137 }, { "name": "EditFileTool", "signature": "new EditFileTool()", - "startLine": 136 + "startLine": 138 } ], "layer": "planning", - "source": "// === S04Subagent.java ===\npackage io.mybatis.learn.s04;\r\n\r\nimport io.mybatis.learn.core.AgentRunner;\r\nimport io.mybatis.learn.core.tools.BashTool;\r\nimport io.mybatis.learn.core.tools.EditFileTool;\r\nimport io.mybatis.learn.core.tools.ReadFileTool;\r\nimport io.mybatis.learn.core.tools.WriteFileTool;\r\nimport org.springframework.ai.chat.client.ChatClient;\r\nimport org.springframework.ai.chat.model.ChatModel;\r\nimport org.springframework.boot.CommandLineRunner;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.WebApplicationType;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\n/**\r\n * S04 - 子 Agent:上下文隔离,保护主 Agent 的思维清晰。\r\n *

\r\n * 格言: \"大任务拆小, 每个小任务干净的上下文\"\r\n *

\r\n * 核心模式:\r\n *

\r\n *   Parent agent                    Subagent\r\n *   +------------------+            +------------------+\r\n *   | messages=[...]   |            | messages=[]      |  ← fresh\r\n *   |                  |  dispatch  |                  |\r\n *   | tool: task       | --------> | ChatClient.call  |\r\n *   |   prompt=\"...\"   |           |   execute tools   |\r\n *   |                  |  summary  |                  |\r\n *   |   result = \"...\" | <-------- | return last text |\r\n *   +------------------+            +------------------+\r\n *           |\r\n *   Parent context stays clean.\r\n *   Subagent context is discarded.\r\n * 
\r\n *

\r\n * TIP: 对应 Python {@code agents/s04_subagent.py}。\r\n * Python 版手动创建空的 messages 列表实现隔离。\r\n * Spring AI 通过创建独立的 {@link ChatClient} 实例实现相同效果 —— 更加自然。\r\n */\r\n@SpringBootApplication(scanBasePackages = \"io.mybatis.learn.core\")\r\npublic class S04Subagent implements CommandLineRunner {\r\n\r\n private final ChatClient chatClient;\r\n\r\n public S04Subagent(ChatModel chatModel) {\r\n this.chatClient = ChatClient.builder(chatModel)\r\n .defaultSystem(\"You are a coding agent at \" + System.getProperty(\"user.dir\")\r\n + \". Use the task tool to delegate exploration or subtasks.\")\r\n .defaultTools(\r\n new BashTool(),\r\n new ReadFileTool(),\r\n new WriteFileTool(),\r\n new EditFileTool(),\r\n new SubagentTool(chatModel) // 父 Agent 独有的 task 工具\r\n )\r\n .build();\r\n }\r\n\r\n @Override\r\n public void run(String... args) {\r\n AgentRunner.interactive(\"s04\", userMessage ->\r\n chatClient.prompt()\r\n .user(userMessage)\r\n .call()\r\n .content()\r\n );\r\n }\r\n\r\n public static void main(String[] args) {\r\n SpringApplication app = new SpringApplication(S04Subagent.class);\r\n app.setWebApplicationType(WebApplicationType.NONE);\r\n app.run(args);\r\n }\r\n}\r\n\n\n// === SubagentTool.java ===\npackage io.mybatis.learn.s04;\r\n\r\nimport io.mybatis.learn.core.tools.BashTool;\r\nimport io.mybatis.learn.core.tools.EditFileTool;\r\nimport io.mybatis.learn.core.tools.ReadFileTool;\r\nimport io.mybatis.learn.core.tools.WriteFileTool;\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\nimport org.springframework.ai.chat.client.ChatClient;\r\nimport org.springframework.ai.chat.model.ChatModel;\r\nimport org.springframework.ai.tool.annotation.Tool;\r\nimport org.springframework.ai.tool.annotation.ToolParam;\r\n\r\n/**\r\n * 子 Agent 工具 —— 生成具有独立上下文的子 Agent 执行任务。\r\n *

\r\n * TIP: 对应 Python {@code agents/s04_subagent.py} 中的 {@code run_subagent(prompt)} 函数。\r\n * Python 版创建 {@code sub_messages = []} 实现上下文隔离,\r\n * Spring AI 通过创建全新的 {@link ChatClient} 实例实现相同效果。\r\n * 子 Agent 获得基础工具但没有 task 工具(防止递归生成)。\r\n * 只有最终文本返回给父 Agent,子 Agent 的对话历史被完全丢弃。\r\n */\r\npublic class SubagentTool {\r\n private static final Logger log = LoggerFactory.getLogger(SubagentTool.class);\r\n\r\n private final ChatModel chatModel;\r\n private final String workDir;\r\n\r\n public SubagentTool(ChatModel chatModel) {\r\n this.chatModel = chatModel;\r\n this.workDir = System.getProperty(\"user.dir\");\r\n }\r\n\r\n /**\r\n * TIP: 对应 Python {@code run_subagent(prompt)}。\r\n * Python 版在独立线程中运行子 Agent 的 while 循环(最多30次迭代)。\r\n * Spring AI 的 ChatClient.call() 内部管理循环,无需手动限制迭代次数。\r\n */\r\n @Tool(description = \"Spawn a subagent with fresh context. \"\r\n + \"It shares the filesystem but not conversation history. \"\r\n + \"Use for exploration or subtasks that might pollute the main context.\")\r\n public String task(\r\n @ToolParam(description = \"The task prompt for the subagent\") String prompt,\r\n @ToolParam(description = \"Short description of the task\", required = false) String description) {\r\n\r\n String desc = (description != null && !description.isBlank()) ? description : \"subtask\";\r\n log.debug(\"子代理任务开始: desc={}, prompt={}\", desc, prompt.substring(0, Math.min(80, prompt.length())));\r\n\r\n // 创建全新的 ChatClient —— 这就是\"上下文隔离\"的全部\r\n // TIP: 对应 Python 的 sub_messages = [] —— 空的消息列表就是隔离\r\n ChatClient subClient = ChatClient.builder(chatModel)\r\n .defaultSystem(\"You are a coding subagent at \" + workDir\r\n + \". Complete the given task, then summarize your findings.\")\r\n .defaultTools(\r\n new BashTool(),\r\n new ReadFileTool(),\r\n new WriteFileTool(),\r\n new EditFileTool()\r\n )\r\n .build();\r\n\r\n String result = subClient.prompt()\r\n .user(prompt)\r\n .call()\r\n .content();\r\n log.debug(\"子代理任务结束: desc={}, resultLength={}\", desc, result == null ? 0 : result.length());\r\n\r\n // 只返回最终文本,子 Agent 上下文被丢弃\r\n return (result != null && !result.isBlank()) ? result : \"(no summary)\";\r\n }\r\n}\r\n" + "source": "// === S04Subagent.java ===\npackage io.mybatis.learn.s04;\r\n\r\nimport io.mybatis.learn.core.AgentRunner;\r\nimport io.mybatis.learn.core.tools.BashTool;\r\nimport io.mybatis.learn.core.tools.EditFileTool;\r\nimport io.mybatis.learn.core.tools.ReadFileTool;\r\nimport io.mybatis.learn.core.tools.WriteFileTool;\r\nimport org.springframework.ai.chat.client.ChatClient;\r\nimport org.springframework.ai.chat.model.ChatModel;\r\nimport org.springframework.boot.CommandLineRunner;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.WebApplicationType;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\n/**\r\n * S04 - 子 Agent:上下文隔离,保护主 Agent 的思维清晰。\r\n *

\r\n * 格言: \"大任务拆小, 每个小任务干净的上下文\"\r\n *

\r\n * 核心模式:\r\n *

\r\n *   Parent agent                    Subagent\r\n *   +------------------+            +------------------+\r\n *   | messages=[...]   |            | messages=[]      |  ← fresh\r\n *   |                  |  dispatch  |                  |\r\n *   | tool: task       | --------> | ChatClient.call  |\r\n *   |   prompt=\"...\"   |           |   execute tools   |\r\n *   |                  |  summary  |                  |\r\n *   |   result = \"...\" | <-------- | return last text |\r\n *   +------------------+            +------------------+\r\n *           |\r\n *   Parent context stays clean.\r\n *   Subagent context is discarded.\r\n * 
\r\n *

\r\n * TIP: 对应 Python {@code agents/s04_subagent.py}。\r\n * Python 版手动创建空的 messages 列表实现隔离。\r\n * Spring AI 通过创建独立的 {@link ChatClient} 实例实现相同效果 —— 更加自然。\r\n */\r\n@SpringBootApplication(scanBasePackages = \"io.mybatis.learn.core\")\r\npublic class S04Subagent implements CommandLineRunner {\r\n\r\n private final ChatClient chatClient;\r\n\r\n public S04Subagent(ChatModel chatModel) {\r\n this.chatClient = ChatClient.builder(chatModel)\r\n .defaultSystem(\"You are a coding agent at \" + System.getProperty(\"user.dir\")\r\n + \". Use the task tool to delegate exploration or subtasks.\")\r\n .defaultTools(\r\n new BashTool(),\r\n new ReadFileTool(),\r\n new WriteFileTool(),\r\n new EditFileTool(),\r\n new SubagentTool(chatModel) // 父 Agent 独有的 task 工具\r\n )\r\n .build();\r\n }\r\n\r\n @Override\r\n public void run(String... args) {\r\n AgentRunner.interactive(\"s04\", userMessage ->\r\n chatClient.prompt()\r\n .user(userMessage)\r\n .call()\r\n .content()\r\n );\r\n }\r\n\r\n public static void main(String[] args) {\r\n SpringApplication app = new SpringApplication(S04Subagent.class);\r\n app.setWebApplicationType(WebApplicationType.NONE);\r\n app.run(args);\r\n }\r\n}\r\n\n\n// === SubagentTool.java ===\npackage io.mybatis.learn.s04;\r\n\r\nimport io.mybatis.learn.core.tools.BashTool;\r\nimport io.mybatis.learn.core.tools.EditFileTool;\r\nimport io.mybatis.learn.core.tools.ReadFileTool;\r\nimport io.mybatis.learn.core.tools.WriteFileTool;\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\nimport org.springframework.ai.chat.client.ChatClient;\r\nimport org.springframework.ai.chat.model.ChatModel;\r\nimport org.springframework.ai.tool.annotation.Tool;\r\nimport org.springframework.ai.tool.annotation.ToolParam;\r\n\r\n/**\r\n * 子 Agent 工具 —— 生成具有独立上下文的子 Agent 执行任务。\r\n *

\r\n * TIP: 对应 Python {@code agents/s04_subagent.py} 中的 {@code run_subagent(prompt)} 函数。\r\n * Python 版创建 {@code sub_messages = []} 实现上下文隔离,\r\n * Spring AI 通过创建全新的 {@link ChatClient} 实例实现相同效果。\r\n * 子 Agent 获得基础工具但没有 task 工具(防止递归生成)。\r\n * 只有最终文本返回给父 Agent,子 Agent 的对话历史被完全丢弃。\r\n */\r\npublic class SubagentTool {\r\n private static final Logger log = LoggerFactory.getLogger(SubagentTool.class);\r\n\r\n private final ChatModel chatModel;\r\n private final String workDir;\r\n\r\n public SubagentTool(ChatModel chatModel) {\r\n this.chatModel = chatModel;\r\n this.workDir = System.getProperty(\"user.dir\");\r\n }\r\n\r\n /**\r\n * TIP: 对应 Python {@code run_subagent(prompt)}。\r\n * Python 版在独立线程中运行子 Agent 的 while 循环(最多30次迭代)。\r\n * Spring AI 的 ChatClient.call() 内部管理循环,无需手动限制迭代次数。\r\n */\r\n @Tool(description = \"Spawn a subagent with fresh context. \"\r\n + \"It shares the filesystem but not conversation history. \"\r\n + \"Use for exploration or subtasks that might pollute the main context.\")\r\n public String task(\r\n @ToolParam(description = \"The task prompt for the subagent\") String prompt,\r\n @ToolParam(description = \"Short description of the task\", required = false) String description) {\r\n\r\n String desc = (description != null && !description.isBlank()) ? description : \"subtask\";\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"🤖 启动子代理「%s」: %s%n\", desc, prompt.substring(0, Math.min(80, prompt.length())));\r\n }\r\n\r\n // 创建全新的 ChatClient —— 这就是\"上下文隔离\"的全部\r\n // TIP: 对应 Python 的 sub_messages = [] —— 空的消息列表就是隔离\r\n ChatClient subClient = ChatClient.builder(chatModel)\r\n .defaultSystem(\"You are a coding subagent at \" + workDir\r\n + \". Complete the given task, then summarize your findings.\")\r\n .defaultTools(\r\n new BashTool(),\r\n new ReadFileTool(),\r\n new WriteFileTool(),\r\n new EditFileTool()\r\n )\r\n .build();\r\n\r\n String result = subClient.prompt()\r\n .user(prompt)\r\n .call()\r\n .content();\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"✅ 子代理「%s」完成,返回 %d 字符%n\", desc, result == null ? 0 : result.length());\r\n }\r\n\r\n // 只返回最终文本,子 Agent 上下文被丢弃\r\n return (result != null && !result.isBlank()) ? result : \"(no summary)\";\r\n }\r\n}\r\n" }, { "id": "s05", @@ -468,7 +468,7 @@ "filename": "S07TaskSystem.java", "title": "Tasks", "subtitle": "Task Graph + Dependencies", - "loc": 248, + "loc": 266, "tools": [ "taskCreate", "taskGet", @@ -492,7 +492,7 @@ { "name": "TaskManager", "startLine": 99, - "endLine": 316 + "endLine": 334 } ], "functions": [ @@ -529,53 +529,53 @@ { "name": "maxId", "signature": "private int maxId()", - "startLine": 121 + "startLine": 123 }, { "name": "load", "signature": "private Map load(int taskId) throws IOException", - "startLine": 134 + "startLine": 136 }, { "name": "save", "signature": "private void save(Map task) throws IOException", - "startLine": 143 + "startLine": 145 }, { "name": "taskCreate", "signature": "public String taskCreate(", - "startLine": 149 + "startLine": 151 }, { "name": "taskGet", "signature": "public String taskGet(@ToolParam(description = \"Task ID\") int taskId)", - "startLine": 173 + "startLine": 179 }, { "name": "taskUpdate", "signature": "public String taskUpdate(", - "startLine": 192 + "startLine": 200 }, { "name": "clearDependency", "signature": "private void clearDependency(int completedId)", - "startLine": 253 + "startLine": 265 }, { "name": "taskList", "signature": "public String taskList()", - "startLine": 275 + "startLine": 291 } ], "layer": "planning", - "source": "// === S07TaskSystem.java ===\npackage io.mybatis.learn.s07;\r\n\r\nimport io.mybatis.learn.core.AgentRunner;\r\nimport io.mybatis.learn.core.tools.BashTool;\r\nimport io.mybatis.learn.core.tools.EditFileTool;\r\nimport io.mybatis.learn.core.tools.ReadFileTool;\r\nimport io.mybatis.learn.core.tools.WriteFileTool;\r\nimport org.springframework.ai.chat.client.ChatClient;\r\nimport org.springframework.ai.chat.model.ChatModel;\r\nimport org.springframework.boot.CommandLineRunner;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.WebApplicationType;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\nimport java.nio.file.Path;\r\n\r\n/**\r\n * S07 - 任务系统:大目标拆成小任务,排好序,记在磁盘上。\r\n *

\r\n * 格言: \"大目标要拆成小任务, 排好序, 记在磁盘上\"\r\n *

\r\n * TIP: 对应 Python {@code agents/s07_task_system.py}。\r\n * 核心洞察: 任务状态持久化在磁盘({@code .tasks/task_*.json}),\r\n * 不受上下文压缩影响 —— 因为它在对话之外。\r\n */\r\n@SpringBootApplication(scanBasePackages = \"io.mybatis.learn.core\")\r\npublic class S07TaskSystem implements CommandLineRunner {\r\n\r\n private final ChatClient chatClient;\r\n\r\n public S07TaskSystem(ChatModel chatModel) {\r\n Path tasksDir = Path.of(System.getProperty(\"user.dir\"), \".tasks\");\r\n TaskManager taskManager = new TaskManager(tasksDir);\r\n\r\n this.chatClient = ChatClient.builder(chatModel)\r\n .defaultSystem(\"You are a coding agent at \" + System.getProperty(\"user.dir\")\r\n + \". Use task tools to plan and track work.\")\r\n .defaultTools(\r\n new BashTool(),\r\n new ReadFileTool(),\r\n new WriteFileTool(),\r\n new EditFileTool(),\r\n taskManager\r\n )\r\n .build();\r\n }\r\n\r\n @Override\r\n public void run(String... args) {\r\n AgentRunner.interactive(\"s07\", userMessage ->\r\n chatClient.prompt()\r\n .user(userMessage)\r\n .call()\r\n .content()\r\n );\r\n }\r\n\r\n public static void main(String[] args) {\r\n SpringApplication app = new SpringApplication(S07TaskSystem.class);\r\n app.setWebApplicationType(WebApplicationType.NONE);\r\n app.run(args);\r\n }\r\n}\r\n\n\n// === TaskManager.java ===\npackage io.mybatis.learn.s07;\r\n\r\nimport com.fasterxml.jackson.core.type.TypeReference;\r\nimport com.fasterxml.jackson.databind.ObjectMapper;\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\nimport org.springframework.ai.tool.annotation.Tool;\r\nimport org.springframework.ai.tool.annotation.ToolParam;\r\n\r\nimport java.io.IOException;\r\nimport java.nio.file.Files;\r\nimport java.nio.file.Path;\r\nimport java.util.*;\r\nimport java.util.stream.Stream;\r\n\r\n/**\r\n * 持久化任务管理器 —— 任务状态存储在磁盘上,不受上下文压缩影响。\r\n *

\r\n * TIP: 对应 Python {@code agents/s07_task_system.py} 中的 {@code TaskManager} 类。\r\n * Python 使用 {@code json.loads/json.dumps},Java 使用 Jackson {@code ObjectMapper}。\r\n *

\r\n *   .tasks/\r\n *     task_1.json  {\"id\":1, \"subject\":\"...\", \"status\":\"completed\", ...}\r\n *     task_2.json  {\"id\":2, \"blockedBy\":[1], \"status\":\"pending\", ...}\r\n *\r\n *   依赖解析:\r\n *     task 1 (complete) --> task 2 (blocked) --> task 3 (blocked)\r\n *       |                       ^\r\n *       +--- completing task 1 removes it from task 2's blockedBy\r\n * 
\r\n */\r\npublic class TaskManager {\r\n private static final Logger log = LoggerFactory.getLogger(TaskManager.class);\r\n\r\n private static final ObjectMapper MAPPER = new ObjectMapper();\r\n private static final List VALID_STATUSES = List.of(\"pending\", \"in_progress\", \"completed\");\r\n\r\n private final Path dir;\r\n private int nextId;\r\n\r\n public TaskManager(Path tasksDir) {\r\n this.dir = tasksDir;\r\n try {\r\n Files.createDirectories(dir);\r\n log.debug(\"任务目录已就绪: {}\", dir);\r\n } catch (IOException e) {\r\n log.error(\"创建任务目录失败: {}, error={}\", dir, e.getMessage());\r\n throw new RuntimeException(\"Cannot create tasks directory: \" + e.getMessage(), e);\r\n }\r\n this.nextId = maxId() + 1;\r\n log.info(\"TaskManager 初始化完成,nextId={}, dir={}\", nextId, dir);\r\n }\r\n\r\n private int maxId() {\r\n try (Stream files = Files.list(dir)) {\r\n return files.filter(f -> f.getFileName().toString().matches(\"task_\\\\d+\\\\.json\"))\r\n .mapToInt(f -> {\r\n String name = f.getFileName().toString();\r\n return Integer.parseInt(name.substring(5, name.length() - 5));\r\n })\r\n .max().orElse(0);\r\n } catch (IOException e) {\r\n return 0;\r\n }\r\n }\r\n\r\n private Map load(int taskId) throws IOException {\r\n Path path = dir.resolve(\"task_\" + taskId + \".json\");\r\n if (!Files.exists(path)) {\r\n throw new IllegalArgumentException(\"Task \" + taskId + \" not found\");\r\n }\r\n return MAPPER.readValue(Files.readString(path), new TypeReference<>() {\r\n });\r\n }\r\n\r\n private void save(Map task) throws IOException {\r\n Path path = dir.resolve(\"task_\" + task.get(\"id\") + \".json\");\r\n Files.writeString(path, MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(task));\r\n }\r\n\r\n @Tool(description = \"Create a new task with subject and optional description\")\r\n public String taskCreate(\r\n @ToolParam(description = \"Short subject of the task\") String subject,\r\n @ToolParam(description = \"Detailed description\", required = false) String description) {\r\n log.debug(\"创建任务: subject={}\", subject);\r\n try {\r\n Map task = new LinkedHashMap<>();\r\n task.put(\"id\", nextId);\r\n task.put(\"subject\", subject);\r\n task.put(\"description\", description != null ? description : \"\");\r\n task.put(\"status\", \"pending\");\r\n task.put(\"blockedBy\", new ArrayList<>());\r\n task.put(\"blocks\", new ArrayList<>());\r\n task.put(\"owner\", \"\");\r\n save(task);\r\n nextId++;\r\n log.debug(\"任务创建成功: id={}, nextId={}\", task.get(\"id\"), nextId);\r\n return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(task);\r\n } catch (Exception e) {\r\n log.warn(\"任务创建失败: subject={}, error={}\", subject, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n @Tool(description = \"Get full details of a task by ID\")\r\n public String taskGet(@ToolParam(description = \"Task ID\") int taskId) {\r\n log.debug(\"查询任务详情: taskId={}\", taskId);\r\n try {\r\n return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(load(taskId));\r\n } catch (Exception e) {\r\n log.warn(\"查询任务详情失败: taskId={}, error={}\", taskId, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n /**\r\n * TIP: 对应 Python {@code TaskManager.update()}。\r\n * 当 status 变为 \"completed\" 时自动清除依赖(调用 clearDependency)。\r\n * blockedBy/blocks 是双向关系:添加 blocks 时也更新被阻塞任务的 blockedBy。\r\n */\r\n @Tool(description = \"Update a task's status or dependencies. \"\r\n + \"Status: pending/in_progress/completed. \"\r\n + \"Use addBlockedBy/addBlocks to manage dependency graph.\")\r\n @SuppressWarnings(\"unchecked\")\r\n public String taskUpdate(\r\n @ToolParam(description = \"Task ID\") int taskId,\r\n @ToolParam(description = \"New status\", required = false) String status,\r\n @ToolParam(description = \"Task IDs that block this task\", required = false) List addBlockedBy,\r\n @ToolParam(description = \"Task IDs that this task blocks\", required = false) List addBlocks) {\r\n log.debug(\"更新任务: taskId={}, status={}, addBlockedByCount={}, addBlocksCount={}\",\r\n taskId, status, addBlockedBy == null ? 0 : addBlockedBy.size(), addBlocks == null ? 0 : addBlocks.size());\r\n try {\r\n Map task = load(taskId);\r\n\r\n if (status != null) {\r\n if (!VALID_STATUSES.contains(status)) {\r\n log.warn(\"非法状态更新: taskId={}, status={}\", taskId, status);\r\n return \"Error: Invalid status: \" + status;\r\n }\r\n task.put(\"status\", status);\r\n if (\"completed\".equals(status)) {\r\n clearDependency(taskId);\r\n }\r\n }\r\n\r\n if (addBlockedBy != null && !addBlockedBy.isEmpty()) {\r\n List current = (List) task.get(\"blockedBy\");\r\n Set merged = new LinkedHashSet<>(current);\r\n merged.addAll(addBlockedBy);\r\n task.put(\"blockedBy\", new ArrayList<>(merged));\r\n }\r\n\r\n if (addBlocks != null && !addBlocks.isEmpty()) {\r\n List current = (List) task.get(\"blocks\");\r\n Set merged = new LinkedHashSet<>(current);\r\n merged.addAll(addBlocks);\r\n task.put(\"blocks\", new ArrayList<>(merged));\r\n // 双向关系:更新被阻塞任务的 blockedBy\r\n for (int blockedId : addBlocks) {\r\n try {\r\n Map blocked = load(blockedId);\r\n List blockedBy = (List) blocked.get(\"blockedBy\");\r\n if (!blockedBy.contains(taskId)) {\r\n blockedBy.add(taskId);\r\n save(blocked);\r\n }\r\n } catch (Exception ignored) {\r\n }\r\n }\r\n }\r\n\r\n save(task);\r\n log.debug(\"任务更新成功: taskId={}\", taskId);\r\n return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(task);\r\n } catch (Exception e) {\r\n log.warn(\"任务更新失败: taskId={}, error={}\", taskId, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n /**\r\n * TIP: 对应 Python {@code TaskManager._clear_dependency(completed_id)}。\r\n * 完成任务后,从所有其他任务的 blockedBy 列表中移除该任务 ID。\r\n */\r\n @SuppressWarnings(\"unchecked\")\r\n private void clearDependency(int completedId) {\r\n log.debug(\"开始清理依赖: completedId={}\", completedId);\r\n try (Stream files = Files.list(dir)) {\r\n files.filter(f -> f.getFileName().toString().matches(\"task_\\\\d+\\\\.json\"))\r\n .forEach(f -> {\r\n try {\r\n Map task = MAPPER.readValue(\r\n Files.readString(f), new TypeReference<>() {\r\n });\r\n List blockedBy = (List) task.get(\"blockedBy\");\r\n if (blockedBy != null && blockedBy.remove(Integer.valueOf(completedId))) {\r\n save(task);\r\n log.debug(\"已移除依赖: completedId={}, taskFile={}\", completedId, f.getFileName());\r\n }\r\n } catch (Exception ignored) {\r\n }\r\n });\r\n } catch (IOException ignored) {\r\n }\r\n }\r\n\r\n @Tool(description = \"List all tasks with status summary\")\r\n public String taskList() {\r\n log.debug(\"列出任务摘要\");\r\n try (Stream files = Files.list(dir)) {\r\n List> tasks = files\r\n .filter(f -> f.getFileName().toString().matches(\"task_\\\\d+\\\\.json\"))\r\n .sorted()\r\n .map(f -> {\r\n try {\r\n return MAPPER.readValue(Files.readString(f),\r\n new TypeReference>() {\r\n });\r\n } catch (IOException e) {\r\n return null;\r\n }\r\n })\r\n .filter(Objects::nonNull)\r\n .toList();\r\n\r\n if (tasks.isEmpty()) return \"No tasks.\";\r\n\r\n StringBuilder sb = new StringBuilder();\r\n for (Map t : tasks) {\r\n String marker = switch (String.valueOf(t.get(\"status\"))) {\r\n case \"in_progress\" -> \"[>]\";\r\n case \"completed\" -> \"[x]\";\r\n default -> \"[ ]\";\r\n };\r\n sb.append(marker).append(\" #\").append(t.get(\"id\")).append(\": \").append(t.get(\"subject\"));\r\n List blockedBy = (List) t.get(\"blockedBy\");\r\n if (blockedBy != null && !blockedBy.isEmpty()) {\r\n sb.append(\" (blocked by: \").append(blockedBy).append(\")\");\r\n }\r\n sb.append(\"\\n\");\r\n }\r\n return sb.toString().stripTrailing();\r\n } catch (IOException e) {\r\n log.warn(\"列出任务失败: error={}\", e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n}\r\n" + "source": "// === S07TaskSystem.java ===\npackage io.mybatis.learn.s07;\r\n\r\nimport io.mybatis.learn.core.AgentRunner;\r\nimport io.mybatis.learn.core.tools.BashTool;\r\nimport io.mybatis.learn.core.tools.EditFileTool;\r\nimport io.mybatis.learn.core.tools.ReadFileTool;\r\nimport io.mybatis.learn.core.tools.WriteFileTool;\r\nimport org.springframework.ai.chat.client.ChatClient;\r\nimport org.springframework.ai.chat.model.ChatModel;\r\nimport org.springframework.boot.CommandLineRunner;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.WebApplicationType;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\nimport java.nio.file.Path;\r\n\r\n/**\r\n * S07 - 任务系统:大目标拆成小任务,排好序,记在磁盘上。\r\n *

\r\n * 格言: \"大目标要拆成小任务, 排好序, 记在磁盘上\"\r\n *

\r\n * TIP: 对应 Python {@code agents/s07_task_system.py}。\r\n * 核心洞察: 任务状态持久化在磁盘({@code .tasks/task_*.json}),\r\n * 不受上下文压缩影响 —— 因为它在对话之外。\r\n */\r\n@SpringBootApplication(scanBasePackages = \"io.mybatis.learn.core\")\r\npublic class S07TaskSystem implements CommandLineRunner {\r\n\r\n private final ChatClient chatClient;\r\n\r\n public S07TaskSystem(ChatModel chatModel) {\r\n Path tasksDir = Path.of(System.getProperty(\"user.dir\"), \".tasks\");\r\n TaskManager taskManager = new TaskManager(tasksDir);\r\n\r\n this.chatClient = ChatClient.builder(chatModel)\r\n .defaultSystem(\"You are a coding agent at \" + System.getProperty(\"user.dir\")\r\n + \". Use task tools to plan and track work.\")\r\n .defaultTools(\r\n new BashTool(),\r\n new ReadFileTool(),\r\n new WriteFileTool(),\r\n new EditFileTool(),\r\n taskManager\r\n )\r\n .build();\r\n }\r\n\r\n @Override\r\n public void run(String... args) {\r\n AgentRunner.interactive(\"s07\", userMessage ->\r\n chatClient.prompt()\r\n .user(userMessage)\r\n .call()\r\n .content()\r\n );\r\n }\r\n\r\n public static void main(String[] args) {\r\n SpringApplication app = new SpringApplication(S07TaskSystem.class);\r\n app.setWebApplicationType(WebApplicationType.NONE);\r\n app.run(args);\r\n }\r\n}\r\n\n\n// === TaskManager.java ===\npackage io.mybatis.learn.s07;\r\n\r\nimport com.fasterxml.jackson.core.type.TypeReference;\r\nimport com.fasterxml.jackson.databind.ObjectMapper;\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\nimport org.springframework.ai.tool.annotation.Tool;\r\nimport org.springframework.ai.tool.annotation.ToolParam;\r\n\r\nimport java.io.IOException;\r\nimport java.nio.file.Files;\r\nimport java.nio.file.Path;\r\nimport java.util.*;\r\nimport java.util.stream.Stream;\r\n\r\n/**\r\n * 持久化任务管理器 —— 任务状态存储在磁盘上,不受上下文压缩影响。\r\n *

\r\n * TIP: 对应 Python {@code agents/s07_task_system.py} 中的 {@code TaskManager} 类。\r\n * Python 使用 {@code json.loads/json.dumps},Java 使用 Jackson {@code ObjectMapper}。\r\n *

\r\n *   .tasks/\r\n *     task_1.json  {\"id\":1, \"subject\":\"...\", \"status\":\"completed\", ...}\r\n *     task_2.json  {\"id\":2, \"blockedBy\":[1], \"status\":\"pending\", ...}\r\n *\r\n *   依赖解析:\r\n *     task 1 (complete) --> task 2 (blocked) --> task 3 (blocked)\r\n *       |                       ^\r\n *       +--- completing task 1 removes it from task 2's blockedBy\r\n * 
\r\n */\r\npublic class TaskManager {\r\n private static final Logger log = LoggerFactory.getLogger(TaskManager.class);\r\n\r\n private static final ObjectMapper MAPPER = new ObjectMapper();\r\n private static final List VALID_STATUSES = List.of(\"pending\", \"in_progress\", \"completed\");\r\n\r\n private final Path dir;\r\n private int nextId;\r\n\r\n public TaskManager(Path tasksDir) {\r\n this.dir = tasksDir;\r\n try {\r\n Files.createDirectories(dir);\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"🚀 任务目录就绪: %s%n\", dir);\r\n }\r\n } catch (IOException e) {\r\n log.error(\"创建任务目录失败: {}, error={}\", dir, e.getMessage());\r\n throw new RuntimeException(\"Cannot create tasks directory: \" + e.getMessage(), e);\r\n }\r\n this.nextId = maxId() + 1;\r\n log.info(\"TaskManager 初始化完成,nextId={}, dir={}\", nextId, dir);\r\n }\r\n\r\n private int maxId() {\r\n try (Stream files = Files.list(dir)) {\r\n return files.filter(f -> f.getFileName().toString().matches(\"task_\\\\d+\\\\.json\"))\r\n .mapToInt(f -> {\r\n String name = f.getFileName().toString();\r\n return Integer.parseInt(name.substring(5, name.length() - 5));\r\n })\r\n .max().orElse(0);\r\n } catch (IOException e) {\r\n return 0;\r\n }\r\n }\r\n\r\n private Map load(int taskId) throws IOException {\r\n Path path = dir.resolve(\"task_\" + taskId + \".json\");\r\n if (!Files.exists(path)) {\r\n throw new IllegalArgumentException(\"Task \" + taskId + \" not found\");\r\n }\r\n return MAPPER.readValue(Files.readString(path), new TypeReference<>() {\r\n });\r\n }\r\n\r\n private void save(Map task) throws IOException {\r\n Path path = dir.resolve(\"task_\" + task.get(\"id\") + \".json\");\r\n Files.writeString(path, MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(task));\r\n }\r\n\r\n @Tool(description = \"Create a new task with subject and optional description\")\r\n public String taskCreate(\r\n @ToolParam(description = \"Short subject of the task\") String subject,\r\n @ToolParam(description = \"Detailed description\", required = false) String description) {\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"📋 创建任务: %s%n\", subject);\r\n }\r\n try {\r\n Map task = new LinkedHashMap<>();\r\n task.put(\"id\", nextId);\r\n task.put(\"subject\", subject);\r\n task.put(\"description\", description != null ? description : \"\");\r\n task.put(\"status\", \"pending\");\r\n task.put(\"blockedBy\", new ArrayList<>());\r\n task.put(\"blocks\", new ArrayList<>());\r\n task.put(\"owner\", \"\");\r\n save(task);\r\n nextId++;\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"✅ 任务 #%s 已创建 (nextId=%d)%n\", task.get(\"id\"), nextId);\r\n }\r\n return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(task);\r\n } catch (Exception e) {\r\n log.warn(\"任务创建失败: subject={}, error={}\", subject, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n @Tool(description = \"Get full details of a task by ID\")\r\n public String taskGet(@ToolParam(description = \"Task ID\") int taskId) {\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"🔍 查询任务 #%d%n\", taskId);\r\n }\r\n try {\r\n return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(load(taskId));\r\n } catch (Exception e) {\r\n log.warn(\"查询任务详情失败: taskId={}, error={}\", taskId, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n /**\r\n * TIP: 对应 Python {@code TaskManager.update()}。\r\n * 当 status 变为 \"completed\" 时自动清除依赖(调用 clearDependency)。\r\n * blockedBy/blocks 是双向关系:添加 blocks 时也更新被阻塞任务的 blockedBy。\r\n */\r\n @Tool(description = \"Update a task's status or dependencies. \"\r\n + \"Status: pending/in_progress/completed. \"\r\n + \"Use addBlockedBy/addBlocks to manage dependency graph.\")\r\n @SuppressWarnings(\"unchecked\")\r\n public String taskUpdate(\r\n @ToolParam(description = \"Task ID\") int taskId,\r\n @ToolParam(description = \"New status\", required = false) String status,\r\n @ToolParam(description = \"Task IDs that block this task\", required = false) List addBlockedBy,\r\n @ToolParam(description = \"Task IDs that this task blocks\", required = false) List addBlocks) {\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"🔄 更新任务 #%d (status=%s, +blockedBy=%d, +blocks=%d)%n\",\r\n taskId, status, addBlockedBy == null ? 0 : addBlockedBy.size(), addBlocks == null ? 0 : addBlocks.size());\r\n }\r\n try {\r\n Map task = load(taskId);\r\n\r\n if (status != null) {\r\n if (!VALID_STATUSES.contains(status)) {\r\n log.warn(\"非法状态更新: taskId={}, status={}\", taskId, status);\r\n return \"Error: Invalid status: \" + status;\r\n }\r\n task.put(\"status\", status);\r\n if (\"completed\".equals(status)) {\r\n clearDependency(taskId);\r\n }\r\n }\r\n\r\n if (addBlockedBy != null && !addBlockedBy.isEmpty()) {\r\n List current = (List) task.get(\"blockedBy\");\r\n Set merged = new LinkedHashSet<>(current);\r\n merged.addAll(addBlockedBy);\r\n task.put(\"blockedBy\", new ArrayList<>(merged));\r\n }\r\n\r\n if (addBlocks != null && !addBlocks.isEmpty()) {\r\n List current = (List) task.get(\"blocks\");\r\n Set merged = new LinkedHashSet<>(current);\r\n merged.addAll(addBlocks);\r\n task.put(\"blocks\", new ArrayList<>(merged));\r\n // 双向关系:更新被阻塞任务的 blockedBy\r\n for (int blockedId : addBlocks) {\r\n try {\r\n Map blocked = load(blockedId);\r\n List blockedBy = (List) blocked.get(\"blockedBy\");\r\n if (!blockedBy.contains(taskId)) {\r\n blockedBy.add(taskId);\r\n save(blocked);\r\n }\r\n } catch (Exception ignored) {\r\n }\r\n }\r\n }\r\n\r\n save(task);\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"✅ 任务 #%d 已更新%n\", taskId);\r\n }\r\n return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(task);\r\n } catch (Exception e) {\r\n log.warn(\"任务更新失败: taskId={}, error={}\", taskId, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n /**\r\n * TIP: 对应 Python {@code TaskManager._clear_dependency(completed_id)}。\r\n * 完成任务后,从所有其他任务的 blockedBy 列表中移除该任务 ID。\r\n */\r\n @SuppressWarnings(\"unchecked\")\r\n private void clearDependency(int completedId) {\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"💭 清理依赖关系: 已完成任务 #%d%n\", completedId);\r\n }\r\n try (Stream files = Files.list(dir)) {\r\n files.filter(f -> f.getFileName().toString().matches(\"task_\\\\d+\\\\.json\"))\r\n .forEach(f -> {\r\n try {\r\n Map task = MAPPER.readValue(\r\n Files.readString(f), new TypeReference<>() {\r\n });\r\n List blockedBy = (List) task.get(\"blockedBy\");\r\n if (blockedBy != null && blockedBy.remove(Integer.valueOf(completedId))) {\r\n save(task);\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"🔗 已解除依赖: #%d ← %s%n\", completedId, f.getFileName());\r\n }\r\n }\r\n } catch (Exception ignored) {\r\n }\r\n });\r\n } catch (IOException ignored) {\r\n }\r\n }\r\n\r\n @Tool(description = \"List all tasks with status summary\")\r\n public String taskList() {\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"📋 列出所有任务%n\");\r\n }\r\n try (Stream files = Files.list(dir)) {\r\n List> tasks = files\r\n .filter(f -> f.getFileName().toString().matches(\"task_\\\\d+\\\\.json\"))\r\n .sorted()\r\n .map(f -> {\r\n try {\r\n return MAPPER.readValue(Files.readString(f),\r\n new TypeReference>() {\r\n });\r\n } catch (IOException e) {\r\n return null;\r\n }\r\n })\r\n .filter(Objects::nonNull)\r\n .toList();\r\n\r\n if (tasks.isEmpty()) return \"No tasks.\";\r\n\r\n StringBuilder sb = new StringBuilder();\r\n for (Map t : tasks) {\r\n String marker = switch (String.valueOf(t.get(\"status\"))) {\r\n case \"in_progress\" -> \"[>]\";\r\n case \"completed\" -> \"[x]\";\r\n default -> \"[ ]\";\r\n };\r\n sb.append(marker).append(\" #\").append(t.get(\"id\")).append(\": \").append(t.get(\"subject\"));\r\n List blockedBy = (List) t.get(\"blockedBy\");\r\n if (blockedBy != null && !blockedBy.isEmpty()) {\r\n sb.append(\" (blocked by: \").append(blockedBy).append(\")\");\r\n }\r\n sb.append(\"\\n\");\r\n }\r\n return sb.toString().stripTrailing();\r\n } catch (IOException e) {\r\n log.warn(\"列出任务失败: error={}\", e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n}\r\n" }, { "id": "s08", "filename": "S08BackgroundTasks.java", "title": "Background Tasks", "subtitle": "Background Threads + Notifications", - "loc": 171, + "loc": 179, "tools": [ "backgroundRun", "checkBackground" @@ -595,7 +595,7 @@ { "name": "BackgroundManager", "startLine": 121, - "endLine": 236 + "endLine": 244 } ], "functions": [ @@ -652,28 +652,28 @@ { "name": "InputStreamReader", "signature": "new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)))", - "startLine": 171 + "startLine": 173 }, { "name": "checkBackground", "signature": "public String checkBackground(", - "startLine": 204 + "startLine": 208 }, { "name": "drainNotifications", "signature": "public List drainNotifications()", - "startLine": 227 + "startLine": 233 } ], "layer": "concurrency", - "source": "// === S08BackgroundTasks.java ===\npackage io.mybatis.learn.s08;\r\n\r\nimport io.mybatis.learn.core.AgentRunner;\r\nimport io.mybatis.learn.core.tools.BashTool;\r\nimport io.mybatis.learn.core.tools.EditFileTool;\r\nimport io.mybatis.learn.core.tools.ReadFileTool;\r\nimport io.mybatis.learn.core.tools.WriteFileTool;\r\nimport org.springframework.ai.chat.client.ChatClient;\r\nimport org.springframework.ai.chat.model.ChatModel;\r\nimport org.springframework.boot.CommandLineRunner;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.WebApplicationType;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\nimport java.util.stream.Collectors;\r\n\r\n/**\r\n * S08 - 后台任务:慢操作丢后台,Agent 继续想下一步。\r\n *

\r\n * 格言: \"慢操作丢后台, agent 继续想下一步\"\r\n *

\r\n * TIP: 对应 Python {@code agents/s08_background_tasks.py}。\r\n * Python 在每次 LLM 调用前 drain 通知队列并注入 {@code }。\r\n * Spring AI 的 ChatClient 管理内部循环,因此改为在每次用户输入时\r\n * drain 通知并注入系统提示。核心概念不变: fire and forget。\r\n */\r\n@SpringBootApplication(scanBasePackages = \"io.mybatis.learn.core\")\r\npublic class S08BackgroundTasks implements CommandLineRunner {\r\n\r\n private final ChatModel chatModel;\r\n\r\n public S08BackgroundTasks(ChatModel chatModel) {\r\n this.chatModel = chatModel;\r\n }\r\n\r\n @Override\r\n public void run(String... args) {\r\n BackgroundManager bgManager = new BackgroundManager();\r\n String workDir = System.getProperty(\"user.dir\");\r\n\r\n AgentRunner.interactive(\"s08\", userMessage -> {\r\n // Drain 后台任务通知(对应 Python 中循环前的 drain_notifications)\r\n var notifs = bgManager.drainNotifications();\r\n String bgContext = \"\";\r\n if (!notifs.isEmpty()) {\r\n String notifText = notifs.stream()\r\n .map(n -> \"[bg:\" + n.taskId() + \"] \" + n.status() + \": \" + n.result())\r\n .collect(Collectors.joining(\"\\n\"));\r\n bgContext = \"\\n\\n\\n\" + notifText + \"\\n\";\r\n System.out.println(\"[Background tasks completed: \" + notifs.size() + \"]\");\r\n }\r\n\r\n String system = \"You are a coding agent at \" + workDir\r\n + \". Use backgroundRun for long-running commands.\" + bgContext;\r\n\r\n ChatClient chatClient = ChatClient.builder(chatModel)\r\n .defaultSystem(system)\r\n .defaultTools(\r\n new BashTool(),\r\n new ReadFileTool(),\r\n new WriteFileTool(),\r\n new EditFileTool(),\r\n bgManager\r\n )\r\n .build();\r\n\r\n return chatClient.prompt()\r\n .user(userMessage)\r\n .call()\r\n .content();\r\n });\r\n }\r\n\r\n public static void main(String[] args) {\r\n SpringApplication app = new SpringApplication(S08BackgroundTasks.class);\r\n app.setWebApplicationType(WebApplicationType.NONE);\r\n app.run(args);\r\n }\r\n}\r\n\n\n// === BackgroundManager.java ===\npackage io.mybatis.learn.s08;\r\n\r\nimport io.mybatis.learn.core.AgentRunner;\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\nimport org.springframework.ai.tool.annotation.Tool;\r\nimport org.springframework.ai.tool.annotation.ToolParam;\r\n\r\nimport java.io.BufferedReader;\r\nimport java.io.InputStreamReader;\r\nimport java.nio.charset.StandardCharsets;\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\nimport java.util.Map;\r\nimport java.util.UUID;\r\nimport java.util.concurrent.ConcurrentHashMap;\r\nimport java.util.concurrent.CopyOnWriteArrayList;\r\nimport java.util.concurrent.ExecutorService;\r\nimport java.util.concurrent.Executors;\r\nimport java.util.stream.Collectors;\r\n\r\n/**\r\n * 后台任务管理器 —— 慢操作丢后台,Agent 继续想下一步。\r\n *

\r\n * TIP: 对应 Python {@code agents/s08_background_tasks.py} 中的 {@code BackgroundManager} 类。\r\n * Python 使用 {@code threading.Thread(daemon=True)},\r\n * Java 使用 {@link ExecutorService} + 虚拟线程(Java 21)。\r\n *

\r\n *   Main thread              Background thread\r\n *   +-----------------+      +-----------------+\r\n *   | agent loop      |      | task executes   |\r\n *   | ...             |      | ...             |\r\n *   | [LLM call] <--+------- | notify(result)  |\r\n *   |  ^drain queue  |       +-----------------+\r\n *   +-----------------+\r\n * 
\r\n */\r\npublic class BackgroundManager {\r\n private static final Logger log = LoggerFactory.getLogger(BackgroundManager.class);\r\n\r\n private static final int TIMEOUT_SECONDS = 300;\r\n\r\n private final Map tasks = new ConcurrentHashMap<>();\r\n private final List notificationQueue = new CopyOnWriteArrayList<>();\r\n private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();\r\n private final String workDir;\r\n\r\n record TaskInfo(String status, String result, String command) {\r\n }\r\n\r\n public record Notification(String taskId, String status, String command, String result) {\r\n }\r\n\r\n public BackgroundManager() {\r\n this.workDir = System.getProperty(\"user.dir\");\r\n log.info(\"BackgroundManager 初始化,workDir={}\", workDir);\r\n }\r\n\r\n @Tool(description = \"Run a command in a background thread. Returns task_id immediately without waiting.\")\r\n public String backgroundRun(\r\n @ToolParam(description = \"The shell command to run in background\") String command) {\r\n String taskId = UUID.randomUUID().toString().substring(0, 8);\r\n tasks.put(taskId, new TaskInfo(\"running\", null, command));\r\n log.info(\"后台任务已提交: taskId={}, command={}\", taskId, command.substring(0, Math.min(80, command.length())));\r\n\r\n executor.submit(() -> execute(taskId, command));\r\n\r\n return \"Background task \" + taskId + \" started: \"\r\n + command.substring(0, Math.min(80, command.length()));\r\n }\r\n\r\n private void execute(String taskId, String command) {\r\n log.debug(\"后台任务开始执行: taskId={}\", taskId);\r\n String status;\r\n String output;\r\n try {\r\n ProcessBuilder pb = new ProcessBuilder();\r\n if (System.getProperty(\"os.name\").toLowerCase().contains(\"win\")) {\r\n pb.command(\"cmd\", \"/c\", command);\r\n } else {\r\n pb.command(\"sh\", \"-c\", command);\r\n }\r\n pb.directory(new java.io.File(workDir));\r\n pb.redirectErrorStream(true);\r\n\r\n Process process = pb.start();\r\n try (BufferedReader reader = new BufferedReader(\r\n new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {\r\n output = reader.lines().collect(Collectors.joining(\"\\n\"));\r\n }\r\n\r\n boolean finished = process.waitFor(TIMEOUT_SECONDS, java.util.concurrent.TimeUnit.SECONDS);\r\n if (!finished) {\r\n process.destroyForcibly();\r\n output = \"Error: Timeout (\" + TIMEOUT_SECONDS + \"s)\";\r\n status = \"timeout\";\r\n log.warn(\"后台任务超时: taskId={}, timeout={}s\", taskId, TIMEOUT_SECONDS);\r\n } else {\r\n output = output.trim();\r\n status = \"completed\";\r\n log.debug(\"后台任务执行完成: taskId={}\", taskId);\r\n }\r\n } catch (Exception e) {\r\n output = \"Error: \" + e.getMessage();\r\n status = \"error\";\r\n log.warn(\"后台任务执行异常: taskId={}, error={}\", taskId, e.getMessage());\r\n }\r\n\r\n String finalOutput = AgentRunner.truncate(output.isEmpty() ? \"(no output)\" : output, 50000);\r\n tasks.put(taskId, new TaskInfo(status, finalOutput, command));\r\n log.info(\"后台任务状态更新: taskId={}, status={}\", taskId, status);\r\n\r\n notificationQueue.add(new Notification(\r\n taskId, status,\r\n command.substring(0, Math.min(80, command.length())),\r\n finalOutput.substring(0, Math.min(500, finalOutput.length()))\r\n ));\r\n }\r\n\r\n @Tool(description = \"Check background task status. Omit taskId to list all tasks.\")\r\n public String checkBackground(\r\n @ToolParam(description = \"Task ID to check (omit for all)\", required = false) String taskId) {\r\n log.debug(\"查询后台任务: taskId={}\", taskId);\r\n if (taskId != null && !taskId.isBlank()) {\r\n TaskInfo t = tasks.get(taskId);\r\n if (t == null) return \"Error: Unknown task \" + taskId;\r\n return \"[\" + t.status() + \"] \"\r\n + t.command().substring(0, Math.min(60, t.command().length())) + \"\\n\"\r\n + (t.result() != null ? t.result() : \"(running)\");\r\n }\r\n if (tasks.isEmpty()) return \"No background tasks.\";\r\n StringBuilder sb = new StringBuilder();\r\n tasks.forEach((tid, t) ->\r\n sb.append(tid).append(\": [\").append(t.status()).append(\"] \")\r\n .append(t.command(), 0, Math.min(60, t.command().length()))\r\n .append(\"\\n\"));\r\n return sb.toString().stripTrailing();\r\n }\r\n\r\n /**\r\n * 排空通知队列,返回所有已完成的后台任务结果。\r\n * TIP: 对应 Python {@code BackgroundManager.drain_notifications()}。\r\n */\r\n public List drainNotifications() {\r\n List drained = new ArrayList<>(notificationQueue);\r\n notificationQueue.clear();\r\n if (!drained.isEmpty()) {\r\n log.debug(\"排空后台通知队列: count={}\", drained.size());\r\n }\r\n return drained;\r\n }\r\n}\r\n" + "source": "// === S08BackgroundTasks.java ===\npackage io.mybatis.learn.s08;\r\n\r\nimport io.mybatis.learn.core.AgentRunner;\r\nimport io.mybatis.learn.core.tools.BashTool;\r\nimport io.mybatis.learn.core.tools.EditFileTool;\r\nimport io.mybatis.learn.core.tools.ReadFileTool;\r\nimport io.mybatis.learn.core.tools.WriteFileTool;\r\nimport org.springframework.ai.chat.client.ChatClient;\r\nimport org.springframework.ai.chat.model.ChatModel;\r\nimport org.springframework.boot.CommandLineRunner;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.WebApplicationType;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\nimport java.util.stream.Collectors;\r\n\r\n/**\r\n * S08 - 后台任务:慢操作丢后台,Agent 继续想下一步。\r\n *

\r\n * 格言: \"慢操作丢后台, agent 继续想下一步\"\r\n *

\r\n * TIP: 对应 Python {@code agents/s08_background_tasks.py}。\r\n * Python 在每次 LLM 调用前 drain 通知队列并注入 {@code }。\r\n * Spring AI 的 ChatClient 管理内部循环,因此改为在每次用户输入时\r\n * drain 通知并注入系统提示。核心概念不变: fire and forget。\r\n */\r\n@SpringBootApplication(scanBasePackages = \"io.mybatis.learn.core\")\r\npublic class S08BackgroundTasks implements CommandLineRunner {\r\n\r\n private final ChatModel chatModel;\r\n\r\n public S08BackgroundTasks(ChatModel chatModel) {\r\n this.chatModel = chatModel;\r\n }\r\n\r\n @Override\r\n public void run(String... args) {\r\n BackgroundManager bgManager = new BackgroundManager();\r\n String workDir = System.getProperty(\"user.dir\");\r\n\r\n AgentRunner.interactive(\"s08\", userMessage -> {\r\n // Drain 后台任务通知(对应 Python 中循环前的 drain_notifications)\r\n var notifs = bgManager.drainNotifications();\r\n String bgContext = \"\";\r\n if (!notifs.isEmpty()) {\r\n String notifText = notifs.stream()\r\n .map(n -> \"[bg:\" + n.taskId() + \"] \" + n.status() + \": \" + n.result())\r\n .collect(Collectors.joining(\"\\n\"));\r\n bgContext = \"\\n\\n\\n\" + notifText + \"\\n\";\r\n System.out.println(\"[Background tasks completed: \" + notifs.size() + \"]\");\r\n }\r\n\r\n String system = \"You are a coding agent at \" + workDir\r\n + \". Use backgroundRun for long-running commands.\" + bgContext;\r\n\r\n ChatClient chatClient = ChatClient.builder(chatModel)\r\n .defaultSystem(system)\r\n .defaultTools(\r\n new BashTool(),\r\n new ReadFileTool(),\r\n new WriteFileTool(),\r\n new EditFileTool(),\r\n bgManager\r\n )\r\n .build();\r\n\r\n return chatClient.prompt()\r\n .user(userMessage)\r\n .call()\r\n .content();\r\n });\r\n }\r\n\r\n public static void main(String[] args) {\r\n SpringApplication app = new SpringApplication(S08BackgroundTasks.class);\r\n app.setWebApplicationType(WebApplicationType.NONE);\r\n app.run(args);\r\n }\r\n}\r\n\n\n// === BackgroundManager.java ===\npackage io.mybatis.learn.s08;\r\n\r\nimport io.mybatis.learn.core.AgentRunner;\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\nimport org.springframework.ai.tool.annotation.Tool;\r\nimport org.springframework.ai.tool.annotation.ToolParam;\r\n\r\nimport java.io.BufferedReader;\r\nimport java.io.InputStreamReader;\r\nimport java.nio.charset.StandardCharsets;\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\nimport java.util.Map;\r\nimport java.util.UUID;\r\nimport java.util.concurrent.ConcurrentHashMap;\r\nimport java.util.concurrent.CopyOnWriteArrayList;\r\nimport java.util.concurrent.ExecutorService;\r\nimport java.util.concurrent.Executors;\r\nimport java.util.stream.Collectors;\r\n\r\n/**\r\n * 后台任务管理器 —— 慢操作丢后台,Agent 继续想下一步。\r\n *

\r\n * TIP: 对应 Python {@code agents/s08_background_tasks.py} 中的 {@code BackgroundManager} 类。\r\n * Python 使用 {@code threading.Thread(daemon=True)},\r\n * Java 使用 {@link ExecutorService} + 虚拟线程(Java 21)。\r\n *

\r\n *   Main thread              Background thread\r\n *   +-----------------+      +-----------------+\r\n *   | agent loop      |      | task executes   |\r\n *   | ...             |      | ...             |\r\n *   | [LLM call] <--+------- | notify(result)  |\r\n *   |  ^drain queue  |       +-----------------+\r\n *   +-----------------+\r\n * 
\r\n */\r\npublic class BackgroundManager {\r\n private static final Logger log = LoggerFactory.getLogger(BackgroundManager.class);\r\n\r\n private static final int TIMEOUT_SECONDS = 300;\r\n\r\n private final Map tasks = new ConcurrentHashMap<>();\r\n private final List notificationQueue = new CopyOnWriteArrayList<>();\r\n private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();\r\n private final String workDir;\r\n\r\n record TaskInfo(String status, String result, String command) {\r\n }\r\n\r\n public record Notification(String taskId, String status, String command, String result) {\r\n }\r\n\r\n public BackgroundManager() {\r\n this.workDir = System.getProperty(\"user.dir\");\r\n log.info(\"BackgroundManager 初始化,workDir={}\", workDir);\r\n }\r\n\r\n @Tool(description = \"Run a command in a background thread. Returns task_id immediately without waiting.\")\r\n public String backgroundRun(\r\n @ToolParam(description = \"The shell command to run in background\") String command) {\r\n String taskId = UUID.randomUUID().toString().substring(0, 8);\r\n tasks.put(taskId, new TaskInfo(\"running\", null, command));\r\n log.info(\"后台任务已提交: taskId={}, command={}\", taskId, command.substring(0, Math.min(80, command.length())));\r\n\r\n executor.submit(() -> execute(taskId, command));\r\n\r\n return \"Background task \" + taskId + \" started: \"\r\n + command.substring(0, Math.min(80, command.length()));\r\n }\r\n\r\n private void execute(String taskId, String command) {\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"⏳ 后台任务 %s 开始执行%n\", taskId);\r\n }\r\n String status;\r\n String output;\r\n try {\r\n ProcessBuilder pb = new ProcessBuilder();\r\n if (System.getProperty(\"os.name\").toLowerCase().contains(\"win\")) {\r\n pb.command(\"cmd\", \"/c\", command);\r\n } else {\r\n pb.command(\"sh\", \"-c\", command);\r\n }\r\n pb.directory(new java.io.File(workDir));\r\n pb.redirectErrorStream(true);\r\n\r\n Process process = pb.start();\r\n try (BufferedReader reader = new BufferedReader(\r\n new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {\r\n output = reader.lines().collect(Collectors.joining(\"\\n\"));\r\n }\r\n\r\n boolean finished = process.waitFor(TIMEOUT_SECONDS, java.util.concurrent.TimeUnit.SECONDS);\r\n if (!finished) {\r\n process.destroyForcibly();\r\n output = \"Error: Timeout (\" + TIMEOUT_SECONDS + \"s)\";\r\n status = \"timeout\";\r\n log.warn(\"后台任务超时: taskId={}, timeout={}s\", taskId, TIMEOUT_SECONDS);\r\n } else {\r\n output = output.trim();\r\n status = \"completed\";\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"✅ 后台任务 %s 执行完成%n\", taskId);\r\n }\r\n }\r\n } catch (Exception e) {\r\n output = \"Error: \" + e.getMessage();\r\n status = \"error\";\r\n log.warn(\"后台任务执行异常: taskId={}, error={}\", taskId, e.getMessage());\r\n }\r\n\r\n String finalOutput = AgentRunner.truncate(output.isEmpty() ? \"(no output)\" : output, 50000);\r\n tasks.put(taskId, new TaskInfo(status, finalOutput, command));\r\n log.info(\"后台任务状态更新: taskId={}, status={}\", taskId, status);\r\n\r\n notificationQueue.add(new Notification(\r\n taskId, status,\r\n command.substring(0, Math.min(80, command.length())),\r\n finalOutput.substring(0, Math.min(500, finalOutput.length()))\r\n ));\r\n }\r\n\r\n @Tool(description = \"Check background task status. Omit taskId to list all tasks.\")\r\n public String checkBackground(\r\n @ToolParam(description = \"Task ID to check (omit for all)\", required = false) String taskId) {\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"🔍 查询后台任务: %s%n\", taskId);\r\n }\r\n if (taskId != null && !taskId.isBlank()) {\r\n TaskInfo t = tasks.get(taskId);\r\n if (t == null) return \"Error: Unknown task \" + taskId;\r\n return \"[\" + t.status() + \"] \"\r\n + t.command().substring(0, Math.min(60, t.command().length())) + \"\\n\"\r\n + (t.result() != null ? t.result() : \"(running)\");\r\n }\r\n if (tasks.isEmpty()) return \"No background tasks.\";\r\n StringBuilder sb = new StringBuilder();\r\n tasks.forEach((tid, t) ->\r\n sb.append(tid).append(\": [\").append(t.status()).append(\"] \")\r\n .append(t.command(), 0, Math.min(60, t.command().length()))\r\n .append(\"\\n\"));\r\n return sb.toString().stripTrailing();\r\n }\r\n\r\n /**\r\n * 排空通知队列,返回所有已完成的后台任务结果。\r\n * TIP: 对应 Python {@code BackgroundManager.drain_notifications()}。\r\n */\r\n public List drainNotifications() {\r\n List drained = new ArrayList<>(notificationQueue);\r\n notificationQueue.clear();\r\n if (!drained.isEmpty()) {\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"📬 排空通知队列 (%d 条)%n\", drained.size());\r\n }\r\n }\r\n return drained;\r\n }\r\n}\r\n" }, { "id": "s09", "filename": "S09AgentTeams.java", "title": "Agent Teams", "subtitle": "Teammates + Mailboxes", - "loc": 268, + "loc": 282, "tools": [ "spawnTeammate", "listTeammates", @@ -698,12 +698,12 @@ { "name": "S09AgentTeams", "startLine": 35, - "endLine": 169 + "endLine": 175 }, { "name": "TeammateManager", - "startLine": 170, - "endLine": 368 + "startLine": 176, + "endLine": 382 } ], "functions": [ @@ -755,73 +755,73 @@ { "name": "loadConfig", "signature": "private Map loadConfig()", - "startLine": 197 + "startLine": 203 }, { "name": "saveConfig", "signature": "private synchronized void saveConfig()", - "startLine": 210 + "startLine": 218 }, { "name": "findMember", "signature": "private Map findMember(String name)", - "startLine": 221 + "startLine": 231 }, { "name": "setStatus", "signature": "protected synchronized void setStatus(String name, String status)", - "startLine": 229 + "startLine": 239 }, { "name": "spawn", "signature": "public synchronized String spawn(String name, String role, String prompt)", - "startLine": 240 + "startLine": 250 }, { "name": "teammateLoop", "signature": "protected void teammateLoop(String name, String role, String initialPrompt)", - "startLine": 273 + "startLine": 283 }, { "name": "WriteFileTool", "signature": "new WriteFileTool(), new EditFileTool(), messageTool)", - "startLine": 285 + "startLine": 295 }, { "name": "listAll", "signature": "public String listAll()", - "startLine": 317 + "startLine": 331 }, { "name": "memberNames", "signature": "public List memberNames()", - "startLine": 329 + "startLine": 343 }, { "name": "TeammateMessageTool", "signature": "public TeammateMessageTool(MessageBus bus, String name)", - "startLine": 346 + "startLine": 360 }, { "name": "sendMessage", "signature": "public String sendMessage(", - "startLine": 352 + "startLine": 366 }, { "name": "readInbox", "signature": "public String readInbox()", - "startLine": 359 + "startLine": 373 } ], "layer": "collaboration", - "source": "// === S09AgentTeams.java ===\npackage io.mybatis.learn.s09;\r\n\r\nimport com.fasterxml.jackson.databind.ObjectMapper;\r\nimport io.mybatis.learn.core.AgentRunner;\r\nimport io.mybatis.learn.core.team.MessageBus;\r\nimport io.mybatis.learn.core.tools.*;\r\nimport org.springframework.ai.chat.client.ChatClient;\r\nimport org.springframework.ai.chat.model.ChatModel;\r\nimport org.springframework.ai.tool.annotation.Tool;\r\nimport org.springframework.ai.tool.annotation.ToolParam;\r\nimport org.springframework.beans.factory.annotation.Autowired;\r\nimport org.springframework.boot.CommandLineRunner;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\nimport java.nio.file.Path;\r\nimport java.util.List;\r\n\r\n/**\r\n * S09 - Agent Teams:持久化命名agent + JSONL收件箱通信\r\n *\r\n * TIPS: 对应Python s09_agent_teams.py。\r\n * Python用 TOOL_HANDLERS 字典 + 手动while循环分发9个工具;\r\n * Java用 ChatClient + @Tool 自动处理,Lead工具封装在 LeadTools 内部类。\r\n *\r\n * 核心概念:\r\n * Subagent (s04): spawn → execute → return summary → destroyed\r\n * Teammate (s09): spawn → work → idle → work → ... → shutdown\r\n *\r\n * 关键差异:Python队友在每次LLM调用前检查收件箱(s09第174行),\r\n * Java队友在每次完整工具链(call())之间检查。这是Spring AI的自然适配。\r\n */\r\n@SpringBootApplication(scanBasePackages = \"io.mybatis.learn.core\")\r\npublic class S09AgentTeams implements CommandLineRunner {\r\n\r\n @Autowired\r\n private ChatModel chatModel;\r\n\r\n @Override\r\n public void run(String... args) throws Exception {\r\n Path workDir = Path.of(System.getProperty(\"user.dir\"));\r\n Path teamDir = workDir.resolve(\".team\");\r\n\r\n MessageBus bus = new MessageBus(teamDir.resolve(\"inbox\"));\r\n TeammateManager team = new TeammateManager(chatModel, bus, teamDir);\r\n LeadTools leadTools = new LeadTools(bus, team);\r\n ObjectMapper mapper = new ObjectMapper();\r\n\r\n String systemPrompt = \"You are a team lead at \" + workDir\r\n + \". Spawn teammates and communicate via inboxes.\";\r\n\r\n ChatClient chatClient = ChatClient.builder(chatModel)\r\n .defaultSystem(systemPrompt)\r\n .defaultTools(new BashTool(), new ReadFileTool(),\r\n new WriteFileTool(), new EditFileTool(), leadTools)\r\n .build();\r\n\r\n AgentRunner.interactive(\"s09\", input -> {\r\n if (\"/team\".equals(input)) return team.listAll();\r\n if (\"/inbox\".equals(input)) {\r\n try {\r\n return mapper.writeValueAsString(bus.readInbox(\"lead\"));\r\n } catch (Exception e) {\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n // 每次LLM调用前检查lead收件箱\r\n var inbox = bus.readInbox(\"lead\");\r\n String fullInput = input;\r\n if (!inbox.isEmpty()) {\r\n try {\r\n fullInput = \"\" + mapper.writeValueAsString(inbox)\r\n + \"\\n\\n\" + input;\r\n } catch (Exception e) { /* ignore */ }\r\n }\r\n\r\n return chatClient.prompt(fullInput).call().content();\r\n });\r\n }\r\n\r\n /**\r\n * Lead专用工具集(5个团队管理工具)\r\n *\r\n * TIPS: 对应Python TOOL_HANDLERS中的5个lead工具(s09第310-319行)。\r\n * Python用lambda + 字典映射;Java用@Tool注解的方法。\r\n */\r\n public static class LeadTools {\r\n private final MessageBus bus;\r\n private final TeammateManager team;\r\n private final ObjectMapper mapper = new ObjectMapper();\r\n\r\n public LeadTools(MessageBus bus, TeammateManager team) {\r\n this.bus = bus;\r\n this.team = team;\r\n }\r\n\r\n @Tool(description = \"Spawn a persistent teammate that runs in its own thread\")\r\n public String spawnTeammate(\r\n @ToolParam(description = \"Teammate name\") String name,\r\n @ToolParam(description = \"Role description\") String role,\r\n @ToolParam(description = \"Initial task prompt\") String prompt) {\r\n return team.spawn(name, role, prompt);\r\n }\r\n\r\n @Tool(description = \"List all teammates with name, role, status\")\r\n public String listTeammates() {\r\n return team.listAll();\r\n }\r\n\r\n @Tool(description = \"Send a message to a teammate's inbox\")\r\n public String sendMessage(\r\n @ToolParam(description = \"Recipient teammate name\") String to,\r\n @ToolParam(description = \"Message content\") String content) {\r\n return bus.send(\"lead\", to, content);\r\n }\r\n\r\n @Tool(description = \"Read and drain the lead's inbox\")\r\n public String readInbox() {\r\n try {\r\n return mapper.writeValueAsString(bus.readInbox(\"lead\"));\r\n } catch (Exception e) {\r\n return \"[]\";\r\n }\r\n }\r\n\r\n @Tool(description = \"Send a message to all teammates\")\r\n public String broadcast(\r\n @ToolParam(description = \"Message content\") String content) {\r\n return bus.broadcast(\"lead\", content, team.memberNames());\r\n }\r\n }\r\n\r\n public static void main(String[] args) {\r\n SpringApplication.run(S09AgentTeams.class, args);\r\n }\r\n}\r\n\n\n// === TeammateManager.java ===\npackage io.mybatis.learn.s09;\r\n\r\nimport com.fasterxml.jackson.databind.ObjectMapper;\r\nimport io.mybatis.learn.core.AgentRunner;\r\nimport io.mybatis.learn.core.team.MessageBus;\r\nimport io.mybatis.learn.core.tools.*;\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\nimport org.springframework.ai.chat.client.ChatClient;\r\nimport org.springframework.ai.chat.model.ChatModel;\r\nimport org.springframework.ai.tool.annotation.Tool;\r\nimport org.springframework.ai.tool.annotation.ToolParam;\r\n\r\nimport java.io.IOException;\r\nimport java.nio.file.Files;\r\nimport java.nio.file.Path;\r\nimport java.util.*;\r\nimport java.util.concurrent.ConcurrentHashMap;\r\n\r\n/**\r\n * 团队管理器 - 持久化命名agent,通过JSONL收件箱通信\r\n *\r\n * TIPS: 对应Python TeammateManager类(s09第124-248行)。\r\n * Python用 threading.Thread 创建队友线程,Java用虚拟线程(Thread.startVirtualThread)。\r\n * Python队友手动执行 while/tool_use 循环 + _exec分发;\r\n * Java队友用 ChatClient + @Tool 自动处理工具循环(一次 call() = 完整工具链)。\r\n * 每次 call() 等价于Python的「循环直到 stop_reason != tool_use」。\r\n */\r\npublic class TeammateManager {\r\n private static final Logger log = LoggerFactory.getLogger(TeammateManager.class);\r\n\r\n private final ChatModel chatModel;\r\n private final MessageBus bus;\r\n private final Path configPath;\r\n private final ObjectMapper mapper = new ObjectMapper();\r\n private Map config;\r\n private final Map threads = new ConcurrentHashMap<>();\r\n\r\n public TeammateManager(ChatModel chatModel, MessageBus bus, Path teamDir) {\r\n this.chatModel = chatModel;\r\n this.bus = bus;\r\n this.configPath = teamDir.resolve(\"config.json\");\r\n try {\r\n Files.createDirectories(teamDir);\r\n } catch (IOException e) {\r\n log.error(\"创建团队目录失败: {}, error={}\", teamDir, e.getMessage());\r\n throw new RuntimeException(e);\r\n }\r\n this.config = loadConfig();\r\n log.info(\"TeammateManager 初始化完成,configPath={}\", configPath);\r\n }\r\n\r\n // ---- 配置持久化 ----\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n private Map loadConfig() {\r\n if (Files.exists(configPath)) {\r\n try {\r\n log.debug(\"加载团队配置: {}\", configPath);\r\n return mapper.readValue(configPath.toFile(), Map.class);\r\n } catch (IOException e) { /* ignore */ }\r\n }\r\n Map cfg = new LinkedHashMap<>();\r\n cfg.put(\"team_name\", \"default\");\r\n cfg.put(\"members\", new ArrayList<>());\r\n return cfg;\r\n }\r\n\r\n private synchronized void saveConfig() {\r\n try {\r\n mapper.writerWithDefaultPrettyPrinter().writeValue(configPath.toFile(), config);\r\n log.debug(\"保存团队配置成功: {}\", configPath);\r\n } catch (IOException e) {\r\n log.warn(\"保存团队配置失败: error={}\", e.getMessage());\r\n System.err.println(\"Error saving config: \" + e.getMessage());\r\n }\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n private Map findMember(String name) {\r\n List> members = (List>) config.get(\"members\");\r\n for (Map m : members) {\r\n if (name.equals(m.get(\"name\"))) return m;\r\n }\r\n return null;\r\n }\r\n\r\n protected synchronized void setStatus(String name, String status) {\r\n Map member = findMember(name);\r\n if (member != null) {\r\n member.put(\"status\", status);\r\n saveConfig();\r\n }\r\n }\r\n\r\n // ---- Spawn ----\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public synchronized String spawn(String name, String role, String prompt) {\r\n log.info(\"请求启动队友: name={}, role={}\", name, role);\r\n Map member = findMember(name);\r\n if (member != null) {\r\n String status = (String) member.get(\"status\");\r\n if (!\"idle\".equals(status) && !\"shutdown\".equals(status)) {\r\n return \"Error: '\" + name + \"' is currently \" + status;\r\n }\r\n member.put(\"status\", \"working\");\r\n member.put(\"role\", role);\r\n } else {\r\n member = new LinkedHashMap<>();\r\n member.put(\"name\", name);\r\n member.put(\"role\", role);\r\n member.put(\"status\", \"working\");\r\n ((List>) config.get(\"members\")).add(member);\r\n }\r\n saveConfig();\r\n\r\n Thread thread = Thread.startVirtualThread(() -> teammateLoop(name, role, prompt));\r\n threads.put(name, thread);\r\n log.info(\"队友已启动: name={}, role={}\", name, role);\r\n return \"Spawned '\" + name + \"' (role: \" + role + \")\";\r\n }\r\n\r\n // ---- 队友循环 ----\r\n\r\n /**\r\n * TIPS: Python队友循环(s09第166-204行)在range(50)内逐次调用LLM。\r\n * Java用ChatClient.prompt().call()一次完成整个工具链,\r\n * 等价于Python循环到 stop_reason != \"tool_use\" 为止。\r\n * 收件箱检查在每次call()之间进行(而非Python的每次LLM调用之间)。\r\n */\r\n protected void teammateLoop(String name, String role, String initialPrompt) {\r\n log.info(\"队友工作循环开始: name={}, role={}\", name, role);\r\n String workDir = System.getProperty(\"user.dir\");\r\n String sysPrompt = String.format(\r\n \"You are '%s', role: %s, at %s. \"\r\n + \"Use send_message to communicate. Complete your task.\",\r\n name, role, workDir);\r\n\r\n var messageTool = new TeammateMessageTool(bus, name);\r\n ChatClient client = ChatClient.builder(chatModel)\r\n .defaultSystem(sysPrompt)\r\n .defaultTools(new BashTool(), new ReadFileTool(),\r\n new WriteFileTool(), new EditFileTool(), messageTool)\r\n .build();\r\n\r\n try {\r\n // 初始工作\r\n String response = client.prompt(initialPrompt).call().content();\r\n System.out.println(\" [\" + name + \"] \" + AgentRunner.truncate(response, 120));\r\n log.debug(\"队友初始响应完成: name={}\", name);\r\n\r\n // 等待收件箱消息(每2秒检查一次,最多50轮)\r\n for (int round = 0; round < 50; round++) {\r\n Thread.sleep(2000);\r\n var inbox = bus.readInbox(name);\r\n if (inbox.isEmpty()) break;\r\n log.debug(\"队友读取到收件箱消息: name={}, count={}\", name, inbox.size());\r\n\r\n String inboxJson = mapper.writeValueAsString(inbox);\r\n response = client.prompt(\"\" + inboxJson + \"\").call().content();\r\n System.out.println(\" [\" + name + \"] \" + AgentRunner.truncate(response, 120));\r\n }\r\n } catch (Exception e) {\r\n log.warn(\"队友执行异常: name={}, error={}\", name, e.getMessage());\r\n System.err.println(\" [\" + name + \"] Error: \" + e.getMessage());\r\n }\r\n\r\n setStatus(name, \"idle\");\r\n log.info(\"队友工作循环结束: name={}\", name);\r\n }\r\n\r\n // ---- 查询 ----\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public String listAll() {\r\n List> members = (List>) config.get(\"members\");\r\n if (members.isEmpty()) return \"No teammates.\";\r\n StringBuilder sb = new StringBuilder(\"Team: \" + config.get(\"team_name\"));\r\n for (Map m : members) {\r\n sb.append(\"\\n \").append(m.get(\"name\"))\r\n .append(\" (\").append(m.get(\"role\")).append(\"): \").append(m.get(\"status\"));\r\n }\r\n return sb.toString();\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public List memberNames() {\r\n List> members = (List>) config.get(\"members\");\r\n return members.stream().map(m -> (String) m.get(\"name\")).toList();\r\n }\r\n\r\n // ---- 队友专用消息工具(参数化sender名称) ----\r\n\r\n /**\r\n * TIPS: Python队友通过 _exec() 分发工具调用(s09第206-220行),\r\n * send_message和read_inbox使用队友自己的名字作为sender。\r\n * Java用参数化工具类:构造时绑定sender,@Tool方法自动注入。\r\n */\r\n public static class TeammateMessageTool {\r\n private final MessageBus bus;\r\n private final String name;\r\n private final ObjectMapper mapper = new ObjectMapper();\r\n\r\n public TeammateMessageTool(MessageBus bus, String name) {\r\n this.bus = bus;\r\n this.name = name;\r\n }\r\n\r\n @Tool(description = \"Send message to a teammate\")\r\n public String sendMessage(\r\n @ToolParam(description = \"Recipient name\") String to,\r\n @ToolParam(description = \"Message content\") String content) {\r\n return bus.send(name, to, content);\r\n }\r\n\r\n @Tool(description = \"Read and drain your inbox\")\r\n public String readInbox() {\r\n try {\r\n return mapper.writeValueAsString(bus.readInbox(name));\r\n } catch (Exception e) {\r\n return \"[]\";\r\n }\r\n }\r\n }\r\n}\r\n" + "source": "// === S09AgentTeams.java ===\npackage io.mybatis.learn.s09;\r\n\r\nimport com.fasterxml.jackson.databind.ObjectMapper;\r\nimport io.mybatis.learn.core.AgentRunner;\r\nimport io.mybatis.learn.core.team.MessageBus;\r\nimport io.mybatis.learn.core.tools.*;\r\nimport org.springframework.ai.chat.client.ChatClient;\r\nimport org.springframework.ai.chat.model.ChatModel;\r\nimport org.springframework.ai.tool.annotation.Tool;\r\nimport org.springframework.ai.tool.annotation.ToolParam;\r\nimport org.springframework.beans.factory.annotation.Autowired;\r\nimport org.springframework.boot.CommandLineRunner;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\nimport java.nio.file.Path;\r\nimport java.util.List;\r\n\r\n/**\r\n * S09 - Agent Teams:持久化命名agent + JSONL收件箱通信\r\n *\r\n * TIPS: 对应Python s09_agent_teams.py。\r\n * Python用 TOOL_HANDLERS 字典 + 手动while循环分发9个工具;\r\n * Java用 ChatClient + @Tool 自动处理,Lead工具封装在 LeadTools 内部类。\r\n *\r\n * 核心概念:\r\n * Subagent (s04): spawn → execute → return summary → destroyed\r\n * Teammate (s09): spawn → work → idle → work → ... → shutdown\r\n *\r\n * 关键差异:Python队友在每次LLM调用前检查收件箱(s09第174行),\r\n * Java队友在每次完整工具链(call())之间检查。这是Spring AI的自然适配。\r\n */\r\n@SpringBootApplication(scanBasePackages = \"io.mybatis.learn.core\")\r\npublic class S09AgentTeams implements CommandLineRunner {\r\n\r\n @Autowired\r\n private ChatModel chatModel;\r\n\r\n @Override\r\n public void run(String... args) throws Exception {\r\n Path workDir = Path.of(System.getProperty(\"user.dir\"));\r\n Path teamDir = workDir.resolve(\".team\");\r\n\r\n MessageBus bus = new MessageBus(teamDir.resolve(\"inbox\"));\r\n TeammateManager team = new TeammateManager(chatModel, bus, teamDir);\r\n LeadTools leadTools = new LeadTools(bus, team);\r\n ObjectMapper mapper = new ObjectMapper();\r\n\r\n String systemPrompt = \"You are a team lead at \" + workDir\r\n + \". Spawn teammates and communicate via inboxes.\";\r\n\r\n ChatClient chatClient = ChatClient.builder(chatModel)\r\n .defaultSystem(systemPrompt)\r\n .defaultTools(new BashTool(), new ReadFileTool(),\r\n new WriteFileTool(), new EditFileTool(), leadTools)\r\n .build();\r\n\r\n AgentRunner.interactive(\"s09\", input -> {\r\n if (\"/team\".equals(input)) return team.listAll();\r\n if (\"/inbox\".equals(input)) {\r\n try {\r\n return mapper.writeValueAsString(bus.readInbox(\"lead\"));\r\n } catch (Exception e) {\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n // 每次LLM调用前检查lead收件箱\r\n var inbox = bus.readInbox(\"lead\");\r\n String fullInput = input;\r\n if (!inbox.isEmpty()) {\r\n try {\r\n fullInput = \"\" + mapper.writeValueAsString(inbox)\r\n + \"\\n\\n\" + input;\r\n } catch (Exception e) { /* ignore */ }\r\n }\r\n\r\n return chatClient.prompt(fullInput).call().content();\r\n });\r\n }\r\n\r\n /**\r\n * Lead专用工具集(5个团队管理工具)\r\n *\r\n * TIPS: 对应Python TOOL_HANDLERS中的5个lead工具(s09第310-319行)。\r\n * Python用lambda + 字典映射;Java用@Tool注解的方法。\r\n */\r\n public static class LeadTools {\r\n private final MessageBus bus;\r\n private final TeammateManager team;\r\n private final ObjectMapper mapper = new ObjectMapper();\r\n\r\n public LeadTools(MessageBus bus, TeammateManager team) {\r\n this.bus = bus;\r\n this.team = team;\r\n }\r\n\r\n @Tool(description = \"Spawn a persistent teammate that runs in its own thread\")\r\n public String spawnTeammate(\r\n @ToolParam(description = \"Teammate name\") String name,\r\n @ToolParam(description = \"Role description\") String role,\r\n @ToolParam(description = \"Initial task prompt\") String prompt) {\r\n return team.spawn(name, role, prompt);\r\n }\r\n\r\n @Tool(description = \"List all teammates with name, role, status\")\r\n public String listTeammates() {\r\n return team.listAll();\r\n }\r\n\r\n @Tool(description = \"Send a message to a teammate's inbox\")\r\n public String sendMessage(\r\n @ToolParam(description = \"Recipient teammate name\") String to,\r\n @ToolParam(description = \"Message content\") String content) {\r\n return bus.send(\"lead\", to, content);\r\n }\r\n\r\n @Tool(description = \"Read and drain the lead's inbox\")\r\n public String readInbox() {\r\n try {\r\n return mapper.writeValueAsString(bus.readInbox(\"lead\"));\r\n } catch (Exception e) {\r\n return \"[]\";\r\n }\r\n }\r\n\r\n @Tool(description = \"Send a message to all teammates\")\r\n public String broadcast(\r\n @ToolParam(description = \"Message content\") String content) {\r\n return bus.broadcast(\"lead\", content, team.memberNames());\r\n }\r\n }\r\n\r\n public static void main(String[] args) {\r\n SpringApplication.run(S09AgentTeams.class, args);\r\n }\r\n}\r\n\n\n// === TeammateManager.java ===\npackage io.mybatis.learn.s09;\r\n\r\nimport com.fasterxml.jackson.databind.ObjectMapper;\r\nimport io.mybatis.learn.core.AgentRunner;\r\nimport io.mybatis.learn.core.team.MessageBus;\r\nimport io.mybatis.learn.core.tools.BashTool;\r\nimport io.mybatis.learn.core.tools.EditFileTool;\r\nimport io.mybatis.learn.core.tools.ReadFileTool;\r\nimport io.mybatis.learn.core.tools.WriteFileTool;\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\nimport org.springframework.ai.chat.client.ChatClient;\r\nimport org.springframework.ai.chat.model.ChatModel;\r\nimport org.springframework.ai.tool.annotation.Tool;\r\nimport org.springframework.ai.tool.annotation.ToolParam;\r\n\r\nimport java.io.IOException;\r\nimport java.nio.file.Files;\r\nimport java.nio.file.Path;\r\nimport java.util.ArrayList;\r\nimport java.util.LinkedHashMap;\r\nimport java.util.List;\r\nimport java.util.Map;\r\nimport java.util.concurrent.ConcurrentHashMap;\r\n\r\n/**\r\n * 团队管理器 - 持久化命名agent,通过JSONL收件箱通信\r\n *\r\n * TIPS: 对应Python TeammateManager类(s09第124-248行)。\r\n * Python用 threading.Thread 创建队友线程,Java用虚拟线程(Thread.startVirtualThread)。\r\n * Python队友手动执行 while/tool_use 循环 + _exec分发;\r\n * Java队友用 ChatClient + @Tool 自动处理工具循环(一次 call() = 完整工具链)。\r\n * 每次 call() 等价于Python的「循环直到 stop_reason != tool_use」。\r\n */\r\npublic class TeammateManager {\r\n private static final Logger log = LoggerFactory.getLogger(TeammateManager.class);\r\n\r\n private final ChatModel chatModel;\r\n private final MessageBus bus;\r\n private final Path configPath;\r\n private final ObjectMapper mapper = new ObjectMapper();\r\n private Map config;\r\n private final Map threads = new ConcurrentHashMap<>();\r\n\r\n public TeammateManager(ChatModel chatModel, MessageBus bus, Path teamDir) {\r\n this.chatModel = chatModel;\r\n this.bus = bus;\r\n this.configPath = teamDir.resolve(\"config.json\");\r\n try {\r\n Files.createDirectories(teamDir);\r\n } catch (IOException e) {\r\n log.error(\"创建团队目录失败: {}, error={}\", teamDir, e.getMessage());\r\n throw new RuntimeException(e);\r\n }\r\n this.config = loadConfig();\r\n log.info(\"TeammateManager 初始化完成,configPath={}\", configPath);\r\n }\r\n\r\n // ---- 配置持久化 ----\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n private Map loadConfig() {\r\n if (Files.exists(configPath)) {\r\n try {\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"💭 加载团队配置: %s%n\", configPath);\r\n }\r\n return mapper.readValue(configPath.toFile(), Map.class);\r\n } catch (IOException e) { /* ignore */ }\r\n }\r\n Map cfg = new LinkedHashMap<>();\r\n cfg.put(\"team_name\", \"default\");\r\n cfg.put(\"members\", new ArrayList<>());\r\n return cfg;\r\n }\r\n\r\n private synchronized void saveConfig() {\r\n try {\r\n mapper.writerWithDefaultPrettyPrinter().writeValue(configPath.toFile(), config);\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"✅ 团队配置已保存: %s%n\", configPath);\r\n }\r\n } catch (IOException e) {\r\n log.warn(\"保存团队配置失败: error={}\", e.getMessage());\r\n System.err.println(\"Error saving config: \" + e.getMessage());\r\n }\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n private Map findMember(String name) {\r\n List> members = (List>) config.get(\"members\");\r\n for (Map m : members) {\r\n if (name.equals(m.get(\"name\"))) return m;\r\n }\r\n return null;\r\n }\r\n\r\n protected synchronized void setStatus(String name, String status) {\r\n Map member = findMember(name);\r\n if (member != null) {\r\n member.put(\"status\", status);\r\n saveConfig();\r\n }\r\n }\r\n\r\n // ---- Spawn ----\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public synchronized String spawn(String name, String role, String prompt) {\r\n log.info(\"请求启动队友: name={}, role={}\", name, role);\r\n Map member = findMember(name);\r\n if (member != null) {\r\n String status = (String) member.get(\"status\");\r\n if (!\"idle\".equals(status) && !\"shutdown\".equals(status)) {\r\n return \"Error: '\" + name + \"' is currently \" + status;\r\n }\r\n member.put(\"status\", \"working\");\r\n member.put(\"role\", role);\r\n } else {\r\n member = new LinkedHashMap<>();\r\n member.put(\"name\", name);\r\n member.put(\"role\", role);\r\n member.put(\"status\", \"working\");\r\n ((List>) config.get(\"members\")).add(member);\r\n }\r\n saveConfig();\r\n\r\n Thread thread = Thread.startVirtualThread(() -> teammateLoop(name, role, prompt));\r\n threads.put(name, thread);\r\n log.info(\"队友已启动: name={}, role={}\", name, role);\r\n return \"Spawned '\" + name + \"' (role: \" + role + \")\";\r\n }\r\n\r\n // ---- 队友循环 ----\r\n\r\n /**\r\n * TIPS: Python队友循环(s09第166-204行)在range(50)内逐次调用LLM。\r\n * Java用ChatClient.prompt().call()一次完成整个工具链,\r\n * 等价于Python循环到 stop_reason != \"tool_use\" 为止。\r\n * 收件箱检查在每次call()之间进行(而非Python的每次LLM调用之间)。\r\n */\r\n protected void teammateLoop(String name, String role, String initialPrompt) {\r\n log.info(\"队友工作循环开始: name={}, role={}\", name, role);\r\n String workDir = System.getProperty(\"user.dir\");\r\n String sysPrompt = String.format(\r\n \"You are '%s', role: %s, at %s. \"\r\n + \"Use send_message to communicate. Complete your task.\",\r\n name, role, workDir);\r\n\r\n var messageTool = new TeammateMessageTool(bus, name);\r\n ChatClient client = ChatClient.builder(chatModel)\r\n .defaultSystem(sysPrompt)\r\n .defaultTools(new BashTool(), new ReadFileTool(),\r\n new WriteFileTool(), new EditFileTool(), messageTool)\r\n .build();\r\n\r\n try {\r\n // 初始工作\r\n String response = client.prompt(initialPrompt).call().content();\r\n System.out.println(\" [\" + name + \"] \" + AgentRunner.truncate(response, 120));\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"✅ 队友「%s」初始响应完成%n\", name);\r\n }\r\n\r\n // 等待收件箱消息(每2秒检查一次,最多50轮)\r\n for (int round = 0; round < 50; round++) {\r\n Thread.sleep(2000);\r\n var inbox = bus.readInbox(name);\r\n if (inbox.isEmpty()) break;\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"📨 队友「%s」收到 %d 条收件箱消息%n\", name, inbox.size());\r\n }\r\n\r\n String inboxJson = mapper.writeValueAsString(inbox);\r\n response = client.prompt(\"\" + inboxJson + \"\").call().content();\r\n System.out.println(\" [\" + name + \"] \" + AgentRunner.truncate(response, 120));\r\n }\r\n } catch (Exception e) {\r\n log.warn(\"队友执行异常: name={}, error={}\", name, e.getMessage());\r\n System.err.println(\" [\" + name + \"] Error: \" + e.getMessage());\r\n }\r\n\r\n setStatus(name, \"idle\");\r\n log.info(\"队友工作循环结束: name={}\", name);\r\n }\r\n\r\n // ---- 查询 ----\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public String listAll() {\r\n List> members = (List>) config.get(\"members\");\r\n if (members.isEmpty()) return \"No teammates.\";\r\n StringBuilder sb = new StringBuilder(\"Team: \" + config.get(\"team_name\"));\r\n for (Map m : members) {\r\n sb.append(\"\\n \").append(m.get(\"name\"))\r\n .append(\" (\").append(m.get(\"role\")).append(\"): \").append(m.get(\"status\"));\r\n }\r\n return sb.toString();\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public List memberNames() {\r\n List> members = (List>) config.get(\"members\");\r\n return members.stream().map(m -> (String) m.get(\"name\")).toList();\r\n }\r\n\r\n // ---- 队友专用消息工具(参数化sender名称) ----\r\n\r\n /**\r\n * TIPS: Python队友通过 _exec() 分发工具调用(s09第206-220行),\r\n * send_message和read_inbox使用队友自己的名字作为sender。\r\n * Java用参数化工具类:构造时绑定sender,@Tool方法自动注入。\r\n */\r\n public static class TeammateMessageTool {\r\n private final MessageBus bus;\r\n private final String name;\r\n private final ObjectMapper mapper = new ObjectMapper();\r\n\r\n public TeammateMessageTool(MessageBus bus, String name) {\r\n this.bus = bus;\r\n this.name = name;\r\n }\r\n\r\n @Tool(description = \"Send message to a teammate\")\r\n public String sendMessage(\r\n @ToolParam(description = \"Recipient name\") String to,\r\n @ToolParam(description = \"Message content\") String content) {\r\n return bus.send(name, to, content);\r\n }\r\n\r\n @Tool(description = \"Read and drain your inbox\")\r\n public String readInbox() {\r\n try {\r\n return mapper.writeValueAsString(bus.readInbox(name));\r\n } catch (Exception e) {\r\n return \"[]\";\r\n }\r\n }\r\n }\r\n}\r\n" }, { "id": "s10", "filename": "S10TeamProtocols.java", "title": "Team Protocols", "subtitle": "Shared Communication Rules", - "loc": 303, + "loc": 305, "tools": [ "shutdownResponse", "planApproval", @@ -852,7 +852,7 @@ { "name": "ProtocolTracker", "startLine": 337, - "endLine": 425 + "endLine": 427 } ], "functions": [ @@ -989,21 +989,21 @@ { "name": "respondToShutdown", "signature": "public String respondToShutdown(String sender, String requestId, boolean approve, String reason)", - "startLine": 379 + "startLine": 381 }, { "name": "submitPlan", "signature": "public String submitPlan(String sender, String planText)", - "startLine": 395 + "startLine": 397 }, { "name": "reviewPlan", "signature": "public String reviewPlan(String requestId, boolean approve, String feedback)", - "startLine": 410 + "startLine": 412 } ], "layer": "collaboration", - "source": "// === S10TeamProtocols.java ===\npackage io.mybatis.learn.s10;\r\n\r\nimport com.fasterxml.jackson.databind.ObjectMapper;\r\nimport io.mybatis.learn.core.AgentRunner;\r\nimport io.mybatis.learn.core.team.MessageBus;\r\nimport io.mybatis.learn.core.tools.*;\r\nimport org.springframework.ai.chat.client.ChatClient;\r\nimport org.springframework.ai.chat.model.ChatModel;\r\nimport org.springframework.ai.tool.annotation.Tool;\r\nimport org.springframework.ai.tool.annotation.ToolParam;\r\nimport org.springframework.beans.factory.annotation.Autowired;\r\nimport org.springframework.boot.CommandLineRunner;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\nimport java.io.IOException;\r\nimport java.nio.file.Files;\r\nimport java.nio.file.Path;\r\nimport java.util.*;\r\nimport java.util.concurrent.ConcurrentHashMap;\r\n\r\n/**\r\n * S10 - Team Protocols:关闭协议 + 计划审批协议\r\n *\r\n * TIPS: 对应Python s10_team_protocols.py。\r\n * 在S09基础上新增3个Lead工具和2个Teammate工具。\r\n *\r\n * 关闭协议FSM: pending → approved | rejected\r\n * Lead调用 shutdown_request → Teammate收到 shutdown_request 消息\r\n * → Teammate调用 shutdown_response → Lead收件箱收到响应\r\n *\r\n * 计划审批FSM: pending → approved | rejected\r\n * Teammate调用 plan_approval → Lead收件箱收到计划\r\n * → Lead调用 plan_approval(review) → Teammate收到审批结果\r\n *\r\n * 关键洞察:\"Same request_id correlation pattern, two domains.\"\r\n */\r\n@SpringBootApplication(scanBasePackages = \"io.mybatis.learn.core\")\r\npublic class S10TeamProtocols implements CommandLineRunner {\r\n\r\n @Autowired\r\n private ChatModel chatModel;\r\n\r\n @Override\r\n public void run(String... args) throws Exception {\r\n Path workDir = Path.of(System.getProperty(\"user.dir\"));\r\n Path teamDir = workDir.resolve(\".team\");\r\n\r\n MessageBus bus = new MessageBus(teamDir.resolve(\"inbox\"));\r\n ProtocolTracker tracker = new ProtocolTracker(bus);\r\n S10TeammateManager team = new S10TeammateManager(chatModel, bus, tracker, teamDir);\r\n S10LeadTools leadTools = new S10LeadTools(bus, team, tracker);\r\n ObjectMapper mapper = new ObjectMapper();\r\n\r\n String systemPrompt = \"You are a team lead at \" + workDir\r\n + \". Manage teammates with shutdown and plan approval protocols.\";\r\n\r\n ChatClient chatClient = ChatClient.builder(chatModel)\r\n .defaultSystem(systemPrompt)\r\n .defaultTools(new BashTool(), new ReadFileTool(),\r\n new WriteFileTool(), new EditFileTool(), leadTools)\r\n .build();\r\n\r\n AgentRunner.interactive(\"s10\", input -> {\r\n if (\"/team\".equals(input)) return team.listAll();\r\n if (\"/inbox\".equals(input)) {\r\n try { return mapper.writeValueAsString(bus.readInbox(\"lead\")); }\r\n catch (Exception e) { return \"Error: \" + e.getMessage(); }\r\n }\r\n\r\n var inbox = bus.readInbox(\"lead\");\r\n String fullInput = input;\r\n if (!inbox.isEmpty()) {\r\n try {\r\n fullInput = \"\" + mapper.writeValueAsString(inbox)\r\n + \"\\n\\n\" + input;\r\n } catch (Exception e) { /* ignore */ }\r\n }\r\n\r\n return chatClient.prompt(fullInput).call().content();\r\n });\r\n }\r\n\r\n // ---- 队友管理器(增加协议处理) ----\r\n\r\n /**\r\n * TIPS: 对应Python s10第134-290行的TeammateManager。\r\n * 相比S09增加了:\r\n * 1. should_exit标志 - 队友批准关闭后退出循环\r\n * 2. shutdown_response工具 - 更新tracker + 发送响应消息\r\n * 3. plan_approval工具 - 提交计划到lead收件箱\r\n */\r\n static class S10TeammateManager {\r\n private final ChatModel chatModel;\r\n private final MessageBus bus;\r\n private final ProtocolTracker tracker;\r\n private final Path configPath;\r\n private final ObjectMapper mapper = new ObjectMapper();\r\n private Map config;\r\n\r\n S10TeammateManager(ChatModel chatModel, MessageBus bus,\r\n ProtocolTracker tracker, Path teamDir) {\r\n this.chatModel = chatModel;\r\n this.bus = bus;\r\n this.tracker = tracker;\r\n this.configPath = teamDir.resolve(\"config.json\");\r\n try { Files.createDirectories(teamDir); } catch (IOException e) { throw new RuntimeException(e); }\r\n this.config = loadConfig();\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n private Map loadConfig() {\r\n if (Files.exists(configPath)) {\r\n try { return mapper.readValue(configPath.toFile(), Map.class); } catch (IOException e) { /* ignore */ }\r\n }\r\n Map cfg = new LinkedHashMap<>();\r\n cfg.put(\"team_name\", \"default\");\r\n cfg.put(\"members\", new ArrayList<>());\r\n return cfg;\r\n }\r\n\r\n private synchronized void saveConfig() {\r\n try { mapper.writerWithDefaultPrettyPrinter().writeValue(configPath.toFile(), config); }\r\n catch (IOException e) { /* ignore */ }\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n private Map findMember(String name) {\r\n for (Map m : (List>) config.get(\"members\")) {\r\n if (name.equals(m.get(\"name\"))) return m;\r\n }\r\n return null;\r\n }\r\n\r\n private synchronized void setStatus(String name, String status) {\r\n Map member = findMember(name);\r\n if (member != null) { member.put(\"status\", status); saveConfig(); }\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public synchronized String spawn(String name, String role, String prompt) {\r\n Map member = findMember(name);\r\n if (member != null) {\r\n String status = (String) member.get(\"status\");\r\n if (!\"idle\".equals(status) && !\"shutdown\".equals(status))\r\n return \"Error: '\" + name + \"' is currently \" + status;\r\n member.put(\"status\", \"working\");\r\n member.put(\"role\", role);\r\n } else {\r\n member = new LinkedHashMap<>();\r\n member.put(\"name\", name);\r\n member.put(\"role\", role);\r\n member.put(\"status\", \"working\");\r\n ((List>) config.get(\"members\")).add(member);\r\n }\r\n saveConfig();\r\n Thread.startVirtualThread(() -> teammateLoop(name, role, prompt));\r\n return \"Spawned '\" + name + \"' (role: \" + role + \")\";\r\n }\r\n\r\n private void teammateLoop(String name, String role, String initialPrompt) {\r\n String workDir = System.getProperty(\"user.dir\");\r\n String sysPrompt = String.format(\r\n \"You are '%s', role: %s, at %s. \"\r\n + \"Submit plans via plan_approval before major work. \"\r\n + \"Respond to shutdown_request with shutdown_response.\",\r\n name, role, workDir);\r\n\r\n // 队友协议工具\r\n var protocolTool = new TeammateProtocolTool(bus, tracker, name);\r\n var messageTool = new io.mybatis.learn.s09.TeammateManager.TeammateMessageTool(bus, name);\r\n\r\n ChatClient client = ChatClient.builder(chatModel)\r\n .defaultSystem(sysPrompt)\r\n .defaultTools(new BashTool(), new ReadFileTool(),\r\n new WriteFileTool(), new EditFileTool(),\r\n messageTool, protocolTool)\r\n .build();\r\n\r\n try {\r\n client.prompt(initialPrompt).call().content();\r\n\r\n for (int round = 0; round < 50; round++) {\r\n Thread.sleep(2000);\r\n var inbox = bus.readInbox(name);\r\n if (inbox.isEmpty()) break;\r\n\r\n // 检查是否有关闭请求\r\n boolean hasShutdown = inbox.stream()\r\n .anyMatch(m -> \"shutdown_request\".equals(m.get(\"type\")));\r\n\r\n String inboxJson = mapper.writeValueAsString(inbox);\r\n client.prompt(\"\" + inboxJson + \"\").call().content();\r\n\r\n if (hasShutdown) {\r\n setStatus(name, \"shutdown\");\r\n return;\r\n }\r\n }\r\n } catch (Exception e) {\r\n System.err.println(\" [\" + name + \"] Error: \" + e.getMessage());\r\n }\r\n\r\n setStatus(name, \"idle\");\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public String listAll() {\r\n List> members = (List>) config.get(\"members\");\r\n if (members.isEmpty()) return \"No teammates.\";\r\n StringBuilder sb = new StringBuilder(\"Team: \" + config.get(\"team_name\"));\r\n for (Map m : members)\r\n sb.append(\"\\n \").append(m.get(\"name\"))\r\n .append(\" (\").append(m.get(\"role\")).append(\"): \").append(m.get(\"status\"));\r\n return sb.toString();\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public List memberNames() {\r\n return ((List>) config.get(\"members\")).stream()\r\n .map(m -> (String) m.get(\"name\")).toList();\r\n }\r\n }\r\n\r\n // ---- 队友协议工具 ----\r\n\r\n public static class TeammateProtocolTool {\r\n private final MessageBus bus;\r\n private final ProtocolTracker tracker;\r\n private final String name;\r\n\r\n public TeammateProtocolTool(MessageBus bus, ProtocolTracker tracker, String name) {\r\n this.bus = bus;\r\n this.tracker = tracker;\r\n this.name = name;\r\n }\r\n\r\n @Tool(description = \"Respond to a shutdown request. Approve to shut down, reject to keep working.\")\r\n public String shutdownResponse(\r\n @ToolParam(description = \"The request_id from shutdown request\") String requestId,\r\n @ToolParam(description = \"true to approve shutdown\") boolean approve,\r\n @ToolParam(description = \"Reason for decision\") String reason) {\r\n return tracker.respondToShutdown(name, requestId, approve, reason);\r\n }\r\n\r\n @Tool(description = \"Submit a plan for lead approval before major work\")\r\n public String planApproval(\r\n @ToolParam(description = \"Plan text to submit\") String plan) {\r\n return tracker.submitPlan(name, plan);\r\n }\r\n }\r\n\r\n // ---- Lead工具集(12个工具 = S09的9个 + 3个协议工具) ----\r\n\r\n public static class S10LeadTools {\r\n private final MessageBus bus;\r\n private final S10TeammateManager team;\r\n private final ProtocolTracker tracker;\r\n private final ObjectMapper mapper = new ObjectMapper();\r\n\r\n public S10LeadTools(MessageBus bus, S10TeammateManager team, ProtocolTracker tracker) {\r\n this.bus = bus;\r\n this.team = team;\r\n this.tracker = tracker;\r\n }\r\n\r\n @Tool(description = \"Spawn a persistent teammate\")\r\n public String spawnTeammate(String name, String role, String prompt) {\r\n return team.spawn(name, role, prompt);\r\n }\r\n\r\n @Tool(description = \"List all teammates\")\r\n public String listTeammates() { return team.listAll(); }\r\n\r\n @Tool(description = \"Send a message to a teammate's inbox\")\r\n public String sendMessage(String to, String content) {\r\n return bus.send(\"lead\", to, content);\r\n }\r\n\r\n @Tool(description = \"Read and drain the lead's inbox\")\r\n public String readInbox() {\r\n try { return mapper.writeValueAsString(bus.readInbox(\"lead\")); }\r\n catch (Exception e) { return \"[]\"; }\r\n }\r\n\r\n @Tool(description = \"Broadcast message to all teammates\")\r\n public String broadcast(String content) {\r\n return bus.broadcast(\"lead\", content, team.memberNames());\r\n }\r\n\r\n @Tool(description = \"Request a teammate to shut down gracefully. Returns request_id for tracking.\")\r\n public String shutdownRequest(\r\n @ToolParam(description = \"Teammate name to shut down\") String teammate) {\r\n return tracker.handleShutdownRequest(teammate);\r\n }\r\n\r\n @Tool(description = \"Check shutdown request status by request_id\")\r\n public String shutdownResponse(\r\n @ToolParam(description = \"The request_id to check\") String requestId) {\r\n return tracker.checkShutdownStatus(requestId);\r\n }\r\n\r\n @Tool(description = \"Approve or reject a teammate's plan\")\r\n public String planApproval(\r\n @ToolParam(description = \"Plan request_id\") String requestId,\r\n @ToolParam(description = \"true to approve\") boolean approve,\r\n @ToolParam(description = \"Feedback text\") String feedback) {\r\n return tracker.reviewPlan(requestId, approve, feedback);\r\n }\r\n }\r\n\r\n public static void main(String[] args) {\r\n SpringApplication.run(S10TeamProtocols.class, args);\r\n }\r\n}\r\n\n\n// === ProtocolTracker.java ===\npackage io.mybatis.learn.s10;\r\n\r\nimport io.mybatis.learn.core.team.MessageBus;\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\n\r\nimport java.util.Map;\r\nimport java.util.UUID;\r\nimport java.util.concurrent.ConcurrentHashMap;\r\n\r\n/**\r\n * 协议追踪器 - 关闭协议 + 计划审批协议(请求-响应FSM)\r\n *\r\n * TIPS: 对应Python全局变量 shutdown_requests、plan_requests 和 _tracker_lock(s10第82-84行)。\r\n * Python用字典 + threading.Lock;Java用 ConcurrentHashMap 天然线程安全。\r\n * 两种协议使用相同的 request_id 关联模式:pending → approved | rejected。\r\n */\r\npublic class ProtocolTracker {\r\n private static final Logger log = LoggerFactory.getLogger(ProtocolTracker.class);\r\n\r\n // {request_id: {\"target\": name, \"status\": \"pending|approved|rejected\"}}\r\n private final ConcurrentHashMap> shutdownRequests = new ConcurrentHashMap<>();\r\n // {request_id: {\"from\": name, \"plan\": text, \"status\": \"pending|approved|rejected\"}}\r\n private final ConcurrentHashMap> planRequests = new ConcurrentHashMap<>();\r\n private final MessageBus bus;\r\n\r\n public ProtocolTracker(MessageBus bus) {\r\n this.bus = bus;\r\n }\r\n\r\n // ---- 关闭协议(Lead端) ----\r\n\r\n /**\r\n * Lead发起关闭请求 → 生成request_id → 发送shutdown_request消息\r\n */\r\n public String handleShutdownRequest(String teammate) {\r\n String reqId = UUID.randomUUID().toString().substring(0, 8);\r\n shutdownRequests.put(reqId, new ConcurrentHashMap<>(Map.of(\r\n \"target\", teammate, \"status\", \"pending\")));\r\n log.info(\"发起关闭请求: requestId={}, target={}\", reqId, teammate);\r\n bus.send(\"lead\", teammate, \"Please shut down gracefully.\",\r\n \"shutdown_request\", Map.of(\"request_id\", reqId));\r\n return \"Shutdown request \" + reqId + \" sent to '\" + teammate + \"' (status: pending)\";\r\n }\r\n\r\n /**\r\n * Lead查询关闭状态\r\n */\r\n public String checkShutdownStatus(String requestId) {\r\n log.debug(\"查询关闭请求状态: requestId={}\", requestId);\r\n var req = shutdownRequests.get(requestId);\r\n return req != null ? req.toString() : \"{\\\"error\\\": \\\"not found\\\"}\";\r\n }\r\n\r\n // ---- 关闭协议(Teammate端) ----\r\n\r\n /**\r\n * Teammate响应关闭请求 → 更新追踪器 → 发送shutdown_response消息\r\n */\r\n public String respondToShutdown(String sender, String requestId, boolean approve, String reason) {\r\n var req = shutdownRequests.get(requestId);\r\n if (req != null) {\r\n req.put(\"status\", approve ? \"approved\" : \"rejected\");\r\n }\r\n log.info(\"响应关闭请求: sender={}, requestId={}, approve={}\", sender, requestId, approve);\r\n bus.send(sender, \"lead\", reason != null ? reason : \"\",\r\n \"shutdown_response\", Map.of(\"request_id\", requestId, \"approve\", approve));\r\n return \"Shutdown \" + (approve ? \"approved\" : \"rejected\");\r\n }\r\n\r\n // ---- 计划审批协议(Teammate端) ----\r\n\r\n /**\r\n * Teammate提交计划 → 生成request_id → 发送plan_approval_response消息\r\n */\r\n public String submitPlan(String sender, String planText) {\r\n String reqId = UUID.randomUUID().toString().substring(0, 8);\r\n planRequests.put(reqId, new ConcurrentHashMap<>(Map.of(\r\n \"from\", sender, \"plan\", planText, \"status\", \"pending\")));\r\n log.info(\"提交计划审批: requestId={}, sender={}\", reqId, sender);\r\n bus.send(sender, \"lead\", planText, \"plan_approval_response\",\r\n Map.of(\"request_id\", reqId, \"plan\", planText));\r\n return \"Plan submitted (request_id=\" + reqId + \"). Waiting for lead approval.\";\r\n }\r\n\r\n // ---- 计划审批协议(Lead端) ----\r\n\r\n /**\r\n * Lead审批计划 → 更新追踪器 → 发送plan_approval_response消息\r\n */\r\n public String reviewPlan(String requestId, boolean approve, String feedback) {\r\n var req = planRequests.get(requestId);\r\n if (req == null) {\r\n log.warn(\"审批计划失败,请求不存在: requestId={}\", requestId);\r\n return \"Error: Unknown plan request_id '\" + requestId + \"'\";\r\n }\r\n req.put(\"status\", approve ? \"approved\" : \"rejected\");\r\n log.info(\"审批计划: requestId={}, from={}, approve={}\", requestId, req.get(\"from\"), approve);\r\n bus.send(\"lead\", req.get(\"from\"), feedback != null ? feedback : \"\",\r\n \"plan_approval_response\",\r\n Map.of(\"request_id\", requestId, \"approve\", approve,\r\n \"feedback\", feedback != null ? feedback : \"\"));\r\n return \"Plan \" + req.get(\"status\") + \" for '\" + req.get(\"from\") + \"'\";\r\n }\r\n}\r\n" + "source": "// === S10TeamProtocols.java ===\npackage io.mybatis.learn.s10;\r\n\r\nimport com.fasterxml.jackson.databind.ObjectMapper;\r\nimport io.mybatis.learn.core.AgentRunner;\r\nimport io.mybatis.learn.core.team.MessageBus;\r\nimport io.mybatis.learn.core.tools.*;\r\nimport org.springframework.ai.chat.client.ChatClient;\r\nimport org.springframework.ai.chat.model.ChatModel;\r\nimport org.springframework.ai.tool.annotation.Tool;\r\nimport org.springframework.ai.tool.annotation.ToolParam;\r\nimport org.springframework.beans.factory.annotation.Autowired;\r\nimport org.springframework.boot.CommandLineRunner;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\nimport java.io.IOException;\r\nimport java.nio.file.Files;\r\nimport java.nio.file.Path;\r\nimport java.util.*;\r\nimport java.util.concurrent.ConcurrentHashMap;\r\n\r\n/**\r\n * S10 - Team Protocols:关闭协议 + 计划审批协议\r\n *\r\n * TIPS: 对应Python s10_team_protocols.py。\r\n * 在S09基础上新增3个Lead工具和2个Teammate工具。\r\n *\r\n * 关闭协议FSM: pending → approved | rejected\r\n * Lead调用 shutdown_request → Teammate收到 shutdown_request 消息\r\n * → Teammate调用 shutdown_response → Lead收件箱收到响应\r\n *\r\n * 计划审批FSM: pending → approved | rejected\r\n * Teammate调用 plan_approval → Lead收件箱收到计划\r\n * → Lead调用 plan_approval(review) → Teammate收到审批结果\r\n *\r\n * 关键洞察:\"Same request_id correlation pattern, two domains.\"\r\n */\r\n@SpringBootApplication(scanBasePackages = \"io.mybatis.learn.core\")\r\npublic class S10TeamProtocols implements CommandLineRunner {\r\n\r\n @Autowired\r\n private ChatModel chatModel;\r\n\r\n @Override\r\n public void run(String... args) throws Exception {\r\n Path workDir = Path.of(System.getProperty(\"user.dir\"));\r\n Path teamDir = workDir.resolve(\".team\");\r\n\r\n MessageBus bus = new MessageBus(teamDir.resolve(\"inbox\"));\r\n ProtocolTracker tracker = new ProtocolTracker(bus);\r\n S10TeammateManager team = new S10TeammateManager(chatModel, bus, tracker, teamDir);\r\n S10LeadTools leadTools = new S10LeadTools(bus, team, tracker);\r\n ObjectMapper mapper = new ObjectMapper();\r\n\r\n String systemPrompt = \"You are a team lead at \" + workDir\r\n + \". Manage teammates with shutdown and plan approval protocols.\";\r\n\r\n ChatClient chatClient = ChatClient.builder(chatModel)\r\n .defaultSystem(systemPrompt)\r\n .defaultTools(new BashTool(), new ReadFileTool(),\r\n new WriteFileTool(), new EditFileTool(), leadTools)\r\n .build();\r\n\r\n AgentRunner.interactive(\"s10\", input -> {\r\n if (\"/team\".equals(input)) return team.listAll();\r\n if (\"/inbox\".equals(input)) {\r\n try { return mapper.writeValueAsString(bus.readInbox(\"lead\")); }\r\n catch (Exception e) { return \"Error: \" + e.getMessage(); }\r\n }\r\n\r\n var inbox = bus.readInbox(\"lead\");\r\n String fullInput = input;\r\n if (!inbox.isEmpty()) {\r\n try {\r\n fullInput = \"\" + mapper.writeValueAsString(inbox)\r\n + \"\\n\\n\" + input;\r\n } catch (Exception e) { /* ignore */ }\r\n }\r\n\r\n return chatClient.prompt(fullInput).call().content();\r\n });\r\n }\r\n\r\n // ---- 队友管理器(增加协议处理) ----\r\n\r\n /**\r\n * TIPS: 对应Python s10第134-290行的TeammateManager。\r\n * 相比S09增加了:\r\n * 1. should_exit标志 - 队友批准关闭后退出循环\r\n * 2. shutdown_response工具 - 更新tracker + 发送响应消息\r\n * 3. plan_approval工具 - 提交计划到lead收件箱\r\n */\r\n static class S10TeammateManager {\r\n private final ChatModel chatModel;\r\n private final MessageBus bus;\r\n private final ProtocolTracker tracker;\r\n private final Path configPath;\r\n private final ObjectMapper mapper = new ObjectMapper();\r\n private Map config;\r\n\r\n S10TeammateManager(ChatModel chatModel, MessageBus bus,\r\n ProtocolTracker tracker, Path teamDir) {\r\n this.chatModel = chatModel;\r\n this.bus = bus;\r\n this.tracker = tracker;\r\n this.configPath = teamDir.resolve(\"config.json\");\r\n try { Files.createDirectories(teamDir); } catch (IOException e) { throw new RuntimeException(e); }\r\n this.config = loadConfig();\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n private Map loadConfig() {\r\n if (Files.exists(configPath)) {\r\n try { return mapper.readValue(configPath.toFile(), Map.class); } catch (IOException e) { /* ignore */ }\r\n }\r\n Map cfg = new LinkedHashMap<>();\r\n cfg.put(\"team_name\", \"default\");\r\n cfg.put(\"members\", new ArrayList<>());\r\n return cfg;\r\n }\r\n\r\n private synchronized void saveConfig() {\r\n try { mapper.writerWithDefaultPrettyPrinter().writeValue(configPath.toFile(), config); }\r\n catch (IOException e) { /* ignore */ }\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n private Map findMember(String name) {\r\n for (Map m : (List>) config.get(\"members\")) {\r\n if (name.equals(m.get(\"name\"))) return m;\r\n }\r\n return null;\r\n }\r\n\r\n private synchronized void setStatus(String name, String status) {\r\n Map member = findMember(name);\r\n if (member != null) { member.put(\"status\", status); saveConfig(); }\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public synchronized String spawn(String name, String role, String prompt) {\r\n Map member = findMember(name);\r\n if (member != null) {\r\n String status = (String) member.get(\"status\");\r\n if (!\"idle\".equals(status) && !\"shutdown\".equals(status))\r\n return \"Error: '\" + name + \"' is currently \" + status;\r\n member.put(\"status\", \"working\");\r\n member.put(\"role\", role);\r\n } else {\r\n member = new LinkedHashMap<>();\r\n member.put(\"name\", name);\r\n member.put(\"role\", role);\r\n member.put(\"status\", \"working\");\r\n ((List>) config.get(\"members\")).add(member);\r\n }\r\n saveConfig();\r\n Thread.startVirtualThread(() -> teammateLoop(name, role, prompt));\r\n return \"Spawned '\" + name + \"' (role: \" + role + \")\";\r\n }\r\n\r\n private void teammateLoop(String name, String role, String initialPrompt) {\r\n String workDir = System.getProperty(\"user.dir\");\r\n String sysPrompt = String.format(\r\n \"You are '%s', role: %s, at %s. \"\r\n + \"Submit plans via plan_approval before major work. \"\r\n + \"Respond to shutdown_request with shutdown_response.\",\r\n name, role, workDir);\r\n\r\n // 队友协议工具\r\n var protocolTool = new TeammateProtocolTool(bus, tracker, name);\r\n var messageTool = new io.mybatis.learn.s09.TeammateManager.TeammateMessageTool(bus, name);\r\n\r\n ChatClient client = ChatClient.builder(chatModel)\r\n .defaultSystem(sysPrompt)\r\n .defaultTools(new BashTool(), new ReadFileTool(),\r\n new WriteFileTool(), new EditFileTool(),\r\n messageTool, protocolTool)\r\n .build();\r\n\r\n try {\r\n client.prompt(initialPrompt).call().content();\r\n\r\n for (int round = 0; round < 50; round++) {\r\n Thread.sleep(2000);\r\n var inbox = bus.readInbox(name);\r\n if (inbox.isEmpty()) break;\r\n\r\n // 检查是否有关闭请求\r\n boolean hasShutdown = inbox.stream()\r\n .anyMatch(m -> \"shutdown_request\".equals(m.get(\"type\")));\r\n\r\n String inboxJson = mapper.writeValueAsString(inbox);\r\n client.prompt(\"\" + inboxJson + \"\").call().content();\r\n\r\n if (hasShutdown) {\r\n setStatus(name, \"shutdown\");\r\n return;\r\n }\r\n }\r\n } catch (Exception e) {\r\n System.err.println(\" [\" + name + \"] Error: \" + e.getMessage());\r\n }\r\n\r\n setStatus(name, \"idle\");\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public String listAll() {\r\n List> members = (List>) config.get(\"members\");\r\n if (members.isEmpty()) return \"No teammates.\";\r\n StringBuilder sb = new StringBuilder(\"Team: \" + config.get(\"team_name\"));\r\n for (Map m : members)\r\n sb.append(\"\\n \").append(m.get(\"name\"))\r\n .append(\" (\").append(m.get(\"role\")).append(\"): \").append(m.get(\"status\"));\r\n return sb.toString();\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public List memberNames() {\r\n return ((List>) config.get(\"members\")).stream()\r\n .map(m -> (String) m.get(\"name\")).toList();\r\n }\r\n }\r\n\r\n // ---- 队友协议工具 ----\r\n\r\n public static class TeammateProtocolTool {\r\n private final MessageBus bus;\r\n private final ProtocolTracker tracker;\r\n private final String name;\r\n\r\n public TeammateProtocolTool(MessageBus bus, ProtocolTracker tracker, String name) {\r\n this.bus = bus;\r\n this.tracker = tracker;\r\n this.name = name;\r\n }\r\n\r\n @Tool(description = \"Respond to a shutdown request. Approve to shut down, reject to keep working.\")\r\n public String shutdownResponse(\r\n @ToolParam(description = \"The request_id from shutdown request\") String requestId,\r\n @ToolParam(description = \"true to approve shutdown\") boolean approve,\r\n @ToolParam(description = \"Reason for decision\") String reason) {\r\n return tracker.respondToShutdown(name, requestId, approve, reason);\r\n }\r\n\r\n @Tool(description = \"Submit a plan for lead approval before major work\")\r\n public String planApproval(\r\n @ToolParam(description = \"Plan text to submit\") String plan) {\r\n return tracker.submitPlan(name, plan);\r\n }\r\n }\r\n\r\n // ---- Lead工具集(12个工具 = S09的9个 + 3个协议工具) ----\r\n\r\n public static class S10LeadTools {\r\n private final MessageBus bus;\r\n private final S10TeammateManager team;\r\n private final ProtocolTracker tracker;\r\n private final ObjectMapper mapper = new ObjectMapper();\r\n\r\n public S10LeadTools(MessageBus bus, S10TeammateManager team, ProtocolTracker tracker) {\r\n this.bus = bus;\r\n this.team = team;\r\n this.tracker = tracker;\r\n }\r\n\r\n @Tool(description = \"Spawn a persistent teammate\")\r\n public String spawnTeammate(String name, String role, String prompt) {\r\n return team.spawn(name, role, prompt);\r\n }\r\n\r\n @Tool(description = \"List all teammates\")\r\n public String listTeammates() { return team.listAll(); }\r\n\r\n @Tool(description = \"Send a message to a teammate's inbox\")\r\n public String sendMessage(String to, String content) {\r\n return bus.send(\"lead\", to, content);\r\n }\r\n\r\n @Tool(description = \"Read and drain the lead's inbox\")\r\n public String readInbox() {\r\n try { return mapper.writeValueAsString(bus.readInbox(\"lead\")); }\r\n catch (Exception e) { return \"[]\"; }\r\n }\r\n\r\n @Tool(description = \"Broadcast message to all teammates\")\r\n public String broadcast(String content) {\r\n return bus.broadcast(\"lead\", content, team.memberNames());\r\n }\r\n\r\n @Tool(description = \"Request a teammate to shut down gracefully. Returns request_id for tracking.\")\r\n public String shutdownRequest(\r\n @ToolParam(description = \"Teammate name to shut down\") String teammate) {\r\n return tracker.handleShutdownRequest(teammate);\r\n }\r\n\r\n @Tool(description = \"Check shutdown request status by request_id\")\r\n public String shutdownResponse(\r\n @ToolParam(description = \"The request_id to check\") String requestId) {\r\n return tracker.checkShutdownStatus(requestId);\r\n }\r\n\r\n @Tool(description = \"Approve or reject a teammate's plan\")\r\n public String planApproval(\r\n @ToolParam(description = \"Plan request_id\") String requestId,\r\n @ToolParam(description = \"true to approve\") boolean approve,\r\n @ToolParam(description = \"Feedback text\") String feedback) {\r\n return tracker.reviewPlan(requestId, approve, feedback);\r\n }\r\n }\r\n\r\n public static void main(String[] args) {\r\n SpringApplication.run(S10TeamProtocols.class, args);\r\n }\r\n}\r\n\n\n// === ProtocolTracker.java ===\npackage io.mybatis.learn.s10;\r\n\r\nimport io.mybatis.learn.core.team.MessageBus;\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\n\r\nimport java.util.Map;\r\nimport java.util.UUID;\r\nimport java.util.concurrent.ConcurrentHashMap;\r\n\r\n/**\r\n * 协议追踪器 - 关闭协议 + 计划审批协议(请求-响应FSM)\r\n *\r\n * TIPS: 对应Python全局变量 shutdown_requests、plan_requests 和 _tracker_lock(s10第82-84行)。\r\n * Python用字典 + threading.Lock;Java用 ConcurrentHashMap 天然线程安全。\r\n * 两种协议使用相同的 request_id 关联模式:pending → approved | rejected。\r\n */\r\npublic class ProtocolTracker {\r\n private static final Logger log = LoggerFactory.getLogger(ProtocolTracker.class);\r\n\r\n // {request_id: {\"target\": name, \"status\": \"pending|approved|rejected\"}}\r\n private final ConcurrentHashMap> shutdownRequests = new ConcurrentHashMap<>();\r\n // {request_id: {\"from\": name, \"plan\": text, \"status\": \"pending|approved|rejected\"}}\r\n private final ConcurrentHashMap> planRequests = new ConcurrentHashMap<>();\r\n private final MessageBus bus;\r\n\r\n public ProtocolTracker(MessageBus bus) {\r\n this.bus = bus;\r\n }\r\n\r\n // ---- 关闭协议(Lead端) ----\r\n\r\n /**\r\n * Lead发起关闭请求 → 生成request_id → 发送shutdown_request消息\r\n */\r\n public String handleShutdownRequest(String teammate) {\r\n String reqId = UUID.randomUUID().toString().substring(0, 8);\r\n shutdownRequests.put(reqId, new ConcurrentHashMap<>(Map.of(\r\n \"target\", teammate, \"status\", \"pending\")));\r\n log.info(\"发起关闭请求: requestId={}, target={}\", reqId, teammate);\r\n bus.send(\"lead\", teammate, \"Please shut down gracefully.\",\r\n \"shutdown_request\", Map.of(\"request_id\", reqId));\r\n return \"Shutdown request \" + reqId + \" sent to '\" + teammate + \"' (status: pending)\";\r\n }\r\n\r\n /**\r\n * Lead查询关闭状态\r\n */\r\n public String checkShutdownStatus(String requestId) {\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"🔍 查询关闭请求状态: %s%n\", requestId);\r\n }\r\n var req = shutdownRequests.get(requestId);\r\n return req != null ? req.toString() : \"{\\\"error\\\": \\\"not found\\\"}\";\r\n }\r\n\r\n // ---- 关闭协议(Teammate端) ----\r\n\r\n /**\r\n * Teammate响应关闭请求 → 更新追踪器 → 发送shutdown_response消息\r\n */\r\n public String respondToShutdown(String sender, String requestId, boolean approve, String reason) {\r\n var req = shutdownRequests.get(requestId);\r\n if (req != null) {\r\n req.put(\"status\", approve ? \"approved\" : \"rejected\");\r\n }\r\n log.info(\"响应关闭请求: sender={}, requestId={}, approve={}\", sender, requestId, approve);\r\n bus.send(sender, \"lead\", reason != null ? reason : \"\",\r\n \"shutdown_response\", Map.of(\"request_id\", requestId, \"approve\", approve));\r\n return \"Shutdown \" + (approve ? \"approved\" : \"rejected\");\r\n }\r\n\r\n // ---- 计划审批协议(Teammate端) ----\r\n\r\n /**\r\n * Teammate提交计划 → 生成request_id → 发送plan_approval_response消息\r\n */\r\n public String submitPlan(String sender, String planText) {\r\n String reqId = UUID.randomUUID().toString().substring(0, 8);\r\n planRequests.put(reqId, new ConcurrentHashMap<>(Map.of(\r\n \"from\", sender, \"plan\", planText, \"status\", \"pending\")));\r\n log.info(\"提交计划审批: requestId={}, sender={}\", reqId, sender);\r\n bus.send(sender, \"lead\", planText, \"plan_approval_response\",\r\n Map.of(\"request_id\", reqId, \"plan\", planText));\r\n return \"Plan submitted (request_id=\" + reqId + \"). Waiting for lead approval.\";\r\n }\r\n\r\n // ---- 计划审批协议(Lead端) ----\r\n\r\n /**\r\n * Lead审批计划 → 更新追踪器 → 发送plan_approval_response消息\r\n */\r\n public String reviewPlan(String requestId, boolean approve, String feedback) {\r\n var req = planRequests.get(requestId);\r\n if (req == null) {\r\n log.warn(\"审批计划失败,请求不存在: requestId={}\", requestId);\r\n return \"Error: Unknown plan request_id '\" + requestId + \"'\";\r\n }\r\n req.put(\"status\", approve ? \"approved\" : \"rejected\");\r\n log.info(\"审批计划: requestId={}, from={}, approve={}\", requestId, req.get(\"from\"), approve);\r\n bus.send(\"lead\", req.get(\"from\"), feedback != null ? feedback : \"\",\r\n \"plan_approval_response\",\r\n Map.of(\"request_id\", requestId, \"approve\", approve,\r\n \"feedback\", feedback != null ? feedback : \"\"));\r\n return \"Plan \" + req.get(\"status\") + \" for '\" + req.get(\"from\") + \"'\";\r\n }\r\n}\r\n" }, { "id": "s11", @@ -1142,7 +1142,7 @@ "filename": "S12WorktreeIsolation.java", "title": "Worktree + Task Isolation", "subtitle": "Isolate by Directory", - "loc": 634, + "loc": 664, "tools": [ "taskCreate", "taskList", @@ -1177,22 +1177,22 @@ { "name": "S12WorktreeIsolation", "startLine": 32, - "endLine": 214 + "endLine": 217 }, { "name": "EventBus", - "startLine": 215, - "endLine": 304 + "startLine": 218, + "endLine": 312 }, { "name": "WorktreeManager", - "startLine": 305, - "endLine": 633 + "startLine": 313, + "endLine": 651 }, { "name": "WorktreeTaskManager", - "startLine": 634, - "endLine": 799 + "startLine": 652, + "endLine": 829 } ], "functions": [ @@ -1289,141 +1289,141 @@ { "name": "emit", "signature": "public synchronized void emit(String event, Map task,", - "startLine": 233 + "startLine": 238 }, { "name": "emit", "signature": "public void emit(String event)", - "startLine": 252 + "startLine": 259 }, { "name": "listRecent", "signature": "public String listRecent(int limit)", - "startLine": 257 + "startLine": 264 }, { "name": "isGitRepo", "signature": "private boolean isGitRepo()", - "startLine": 338 + "startLine": 348 }, { "name": "runGit", "signature": "private String runGit(String... args) throws IOException", - "startLine": 349 + "startLine": 359 }, { "name": "loadIndex", "signature": "private Map loadIndex() throws IOException", - "startLine": 370 + "startLine": 382 }, { "name": "saveIndex", "signature": "private void saveIndex(Map index) throws IOException", - "startLine": 374 + "startLine": 386 }, { "name": "findWorktree", "signature": "private Map findWorktree(String name) throws IOException", - "startLine": 379 + "startLine": 391 }, { "name": "validateName", "signature": "private void validateName(String name)", - "startLine": 387 + "startLine": 399 }, { "name": "create", "signature": "public String create(String name, Integer taskId, String baseRef)", - "startLine": 397 + "startLine": 409 }, { "name": "listAll", "signature": "public String listAll()", - "startLine": 447 + "startLine": 459 }, { "name": "status", "signature": "public String status(String name)", - "startLine": 469 + "startLine": 483 }, { "name": "run", "signature": "public String run(String name, String command)", - "startLine": 491 + "startLine": 507 }, { "name": "remove", "signature": "public String remove(String name, boolean force, boolean completeTask)", - "startLine": 531 + "startLine": 549 }, { "name": "keep", "signature": "public String keep(String name)", - "startLine": 580 + "startLine": 598 }, { "name": "isGitAvailable", "signature": "public boolean isGitAvailable() { return gitAvailable; }", - "startLine": 611 + "startLine": 629 }, { "name": "maxId", "signature": "private int maxId()", - "startLine": 651 + "startLine": 669 }, { "name": "taskPath", "signature": "private Path taskPath(int taskId)", - "startLine": 662 + "startLine": 680 }, { "name": "load", "signature": "private Map load(int taskId) throws IOException", - "startLine": 667 + "startLine": 685 }, { "name": "save", "signature": "private void save(Map task) throws IOException", - "startLine": 673 + "startLine": 691 }, { "name": "exists", "signature": "public boolean exists(int taskId)", - "startLine": 678 + "startLine": 696 }, { "name": "create", "signature": "public synchronized String create(String subject, String description)", - "startLine": 682 + "startLine": 700 }, { "name": "get", "signature": "public String get(int taskId)", - "startLine": 705 + "startLine": 727 }, { "name": "update", "signature": "public String update(int taskId, String status, String owner)", - "startLine": 713 + "startLine": 735 }, { "name": "bindWorktree", "signature": "public String bindWorktree(int taskId, String worktree, String owner)", - "startLine": 736 + "startLine": 762 }, { "name": "unbindWorktree", "signature": "public String unbindWorktree(int taskId)", - "startLine": 752 + "startLine": 780 }, { "name": "listAll", "signature": "public String listAll()", - "startLine": 767 + "startLine": 797 } ], "layer": "collaboration", - "source": "// === S12WorktreeIsolation.java ===\npackage io.mybatis.learn.s12;\r\n\r\nimport io.mybatis.learn.core.AgentRunner;\r\nimport io.mybatis.learn.core.tools.*;\r\nimport org.springframework.ai.chat.client.ChatClient;\r\nimport org.springframework.ai.chat.model.ChatModel;\r\nimport org.springframework.ai.tool.annotation.Tool;\r\nimport org.springframework.ai.tool.annotation.ToolParam;\r\nimport org.springframework.beans.factory.annotation.Autowired;\r\nimport org.springframework.boot.CommandLineRunner;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\nimport java.nio.file.Path;\r\n\r\n/**\r\n * S12 - Worktree + Task Isolation:目录级隔离的并行任务执行\r\n *\r\n * TIPS: 对应Python s12_worktree_task_isolation.py。\r\n * 与S09-S11的团队通信路线不同,S12走「目录隔离」路线:\r\n * 任务(控制平面)+ Worktree(执行平面)。\r\n *\r\n * S09-S11: 多个agent → 消息队列通信 → 协议协调\r\n * S12: 单个agent → 多个worktree → 按目录隔离 → 按task ID协调\r\n *\r\n * 17个工具:base(4) + task(5) + worktree(8)\r\n *\r\n * 关键洞察:\"Isolate by directory, coordinate by task ID.\"\r\n */\r\n@SpringBootApplication(scanBasePackages = \"io.mybatis.learn.core\")\r\npublic class S12WorktreeIsolation implements CommandLineRunner {\r\n\r\n @Autowired\r\n private ChatModel chatModel;\r\n\r\n @Override\r\n public void run(String... args) throws Exception {\r\n Path workDir = Path.of(System.getProperty(\"user.dir\"));\r\n\r\n // 检测git仓库根目录\r\n Path repoRoot = detectRepoRoot(workDir);\r\n if (repoRoot == null) repoRoot = workDir;\r\n\r\n WorktreeTaskManager tasks = new WorktreeTaskManager(repoRoot.resolve(\".tasks\"));\r\n EventBus events = new EventBus(repoRoot.resolve(\".worktrees\").resolve(\"events.jsonl\"));\r\n WorktreeManager worktrees = new WorktreeManager(repoRoot, tasks, events);\r\n\r\n TaskTools taskTools = new TaskTools(tasks);\r\n WorktreeTools wtTools = new WorktreeTools(worktrees, events);\r\n\r\n String systemPrompt = \"You are a coding agent at \" + workDir + \". \"\r\n + \"Use task + worktree tools for multi-task work. \"\r\n + \"For parallel or risky changes: create tasks, allocate worktree lanes, \"\r\n + \"run commands in those lanes, then choose keep/remove for closeout. \"\r\n + \"Use worktree_events when you need lifecycle visibility.\";\r\n\r\n ChatClient chatClient = ChatClient.builder(chatModel)\r\n .defaultSystem(systemPrompt)\r\n .defaultTools(new BashTool(), new ReadFileTool(),\r\n new WriteFileTool(), new EditFileTool(),\r\n taskTools, wtTools)\r\n .build();\r\n\r\n System.out.println(\"Repo root for s12: \" + repoRoot);\r\n if (!worktrees.isGitAvailable()) {\r\n System.out.println(\"Note: Not in a git repo. worktree_* tools will return errors.\");\r\n }\r\n\r\n AgentRunner.interactive(\"s12\", input ->\r\n chatClient.prompt(input).call().content());\r\n }\r\n\r\n private static Path detectRepoRoot(Path cwd) {\r\n try {\r\n ProcessBuilder pb = new ProcessBuilder(\"git\", \"rev-parse\", \"--show-toplevel\");\r\n pb.directory(cwd.toFile());\r\n pb.redirectErrorStream(true);\r\n Process p = pb.start();\r\n String out = new String(p.getInputStream().readAllBytes()).trim();\r\n if (p.waitFor() == 0 && !out.isEmpty()) {\r\n Path root = Path.of(out);\r\n return root.toFile().exists() ? root : null;\r\n }\r\n } catch (Exception e) { /* ignore */ }\r\n return null;\r\n }\r\n\r\n // ---- 任务工具集(5个) ----\r\n\r\n public static class TaskTools {\r\n private final WorktreeTaskManager tasks;\r\n\r\n public TaskTools(WorktreeTaskManager tasks) { this.tasks = tasks; }\r\n\r\n @Tool(description = \"Create a new task on the shared task board\")\r\n public String taskCreate(\r\n @ToolParam(description = \"Task subject\") String subject,\r\n @ToolParam(description = \"Task description\") String description) {\r\n return tasks.create(subject, description);\r\n }\r\n\r\n @Tool(description = \"List all tasks with status, owner, and worktree binding\")\r\n public String taskList() { return tasks.listAll(); }\r\n\r\n @Tool(description = \"Get task details by ID\")\r\n public String taskGet(@ToolParam(description = \"Task ID\") int taskId) {\r\n return tasks.get(taskId);\r\n }\r\n\r\n @Tool(description = \"Update task status or owner\")\r\n public String taskUpdate(\r\n @ToolParam(description = \"Task ID\") int taskId,\r\n @ToolParam(description = \"New status: pending/in_progress/completed\") String status,\r\n @ToolParam(description = \"New owner name\") String owner) {\r\n return tasks.update(taskId, status, owner);\r\n }\r\n\r\n @Tool(description = \"Bind a task to a worktree name\")\r\n public String taskBindWorktree(\r\n @ToolParam(description = \"Task ID\") int taskId,\r\n @ToolParam(description = \"Worktree name\") String worktree,\r\n @ToolParam(description = \"Owner name\") String owner) {\r\n return tasks.bindWorktree(taskId, worktree, owner);\r\n }\r\n }\r\n\r\n // ---- Worktree工具集(8个) ----\r\n\r\n /**\r\n * TIPS: 对应Python TOOL_HANDLERS中的8个worktree工具(s12第546-552行)。\r\n * 每个工具委托给WorktreeManager执行实际操作。\r\n * 安全检查:worktree名称正则验证(1-40字符),危险命令阻止。\r\n */\r\n public static class WorktreeTools {\r\n private final WorktreeManager worktrees;\r\n private final EventBus events;\r\n\r\n public WorktreeTools(WorktreeManager worktrees, EventBus events) {\r\n this.worktrees = worktrees;\r\n this.events = events;\r\n }\r\n\r\n @Tool(description = \"Create a git worktree and optionally bind it to a task\")\r\n public String worktreeCreate(\r\n @ToolParam(description = \"Worktree name (1-40 chars: letters, numbers, ., _, -)\") String name,\r\n @ToolParam(description = \"Task ID to bind (optional)\") Integer taskId,\r\n @ToolParam(description = \"Base git ref (default: HEAD)\") String baseRef) {\r\n return worktrees.create(name, taskId, baseRef);\r\n }\r\n\r\n @Tool(description = \"List worktrees tracked in .worktrees/index.json\")\r\n public String worktreeList() { return worktrees.listAll(); }\r\n\r\n @Tool(description = \"Show git status for one worktree\")\r\n public String worktreeStatus(\r\n @ToolParam(description = \"Worktree name\") String name) {\r\n return worktrees.status(name);\r\n }\r\n\r\n @Tool(description = \"Run a shell command in a named worktree directory\")\r\n public String worktreeRun(\r\n @ToolParam(description = \"Worktree name\") String name,\r\n @ToolParam(description = \"Shell command to run\") String command) {\r\n return worktrees.run(name, command);\r\n }\r\n\r\n @Tool(description = \"Remove a worktree and optionally mark its bound task completed\")\r\n public String worktreeRemove(\r\n @ToolParam(description = \"Worktree name\") String name,\r\n @ToolParam(description = \"Force remove\") boolean force,\r\n @ToolParam(description = \"Mark bound task as completed\") boolean completeTask) {\r\n return worktrees.remove(name, force, completeTask);\r\n }\r\n\r\n @Tool(description = \"Mark a worktree as kept in lifecycle state without removing it\")\r\n public String worktreeKeep(\r\n @ToolParam(description = \"Worktree name\") String name) {\r\n return worktrees.keep(name);\r\n }\r\n\r\n @Tool(description = \"List recent worktree/task lifecycle events\")\r\n public String worktreeEvents(\r\n @ToolParam(description = \"Number of events to show (default 20)\") int limit) {\r\n return events.listRecent(limit > 0 ? limit : 20);\r\n }\r\n }\r\n\r\n public static void main(String[] args) {\r\n SpringApplication.run(S12WorktreeIsolation.class, args);\r\n }\r\n}\r\n\n\n// === EventBus.java ===\npackage io.mybatis.learn.s12;\r\n\r\nimport com.fasterxml.jackson.databind.ObjectMapper;\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\n\r\nimport java.io.IOException;\r\nimport java.nio.file.Files;\r\nimport java.nio.file.Path;\r\nimport java.nio.file.StandardOpenOption;\r\nimport java.util.*;\r\n\r\n/**\r\n * 事件总线 - 追加式JSONL生命周期事件日志\r\n *\r\n * TIPS: 对应Python EventBus类(s12第83-118行)。\r\n * 记录 worktree 和 task 的生命周期事件,用于可观测性。\r\n * 事件类型:worktree.create.before/after/failed, worktree.remove.*, worktree.keep, task.completed\r\n */\r\npublic class EventBus {\r\n private static final Logger log = LoggerFactory.getLogger(EventBus.class);\r\n\r\n private final Path logPath;\r\n private final ObjectMapper mapper = new ObjectMapper();\r\n\r\n public EventBus(Path logPath) {\r\n this.logPath = logPath;\r\n try {\r\n Files.createDirectories(logPath.getParent());\r\n if (!Files.exists(logPath)) Files.writeString(logPath, \"\");\r\n log.debug(\"事件日志文件已就绪: {}\", logPath);\r\n } catch (IOException e) {\r\n log.error(\"初始化事件日志失败: {}, error={}\", logPath, e.getMessage());\r\n throw new RuntimeException(e);\r\n }\r\n }\r\n\r\n public synchronized void emit(String event, Map task,\r\n Map worktree, String error) {\r\n Map payload = new LinkedHashMap<>();\r\n payload.put(\"event\", event);\r\n payload.put(\"ts\", System.currentTimeMillis() / 1000.0);\r\n payload.put(\"task\", task != null ? task : Map.of());\r\n payload.put(\"worktree\", worktree != null ? worktree : Map.of());\r\n if (error != null) payload.put(\"error\", error);\r\n\r\n try {\r\n Files.writeString(logPath, mapper.writeValueAsString(payload) + \"\\n\",\r\n StandardOpenOption.APPEND);\r\n log.debug(\"事件已写入: event={}\", event);\r\n } catch (IOException e) {\r\n log.warn(\"事件写入失败: event={}, error={}\", event, e.getMessage());\r\n System.err.println(\"EventBus emit error: \" + e.getMessage());\r\n }\r\n }\r\n\r\n public void emit(String event) {\r\n emit(event, null, null, null);\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public String listRecent(int limit) {\r\n int n = Math.max(1, Math.min(limit, 200));\r\n log.debug(\"读取最近事件: limit={}, normalized={}\", limit, n);\r\n try {\r\n List lines = Files.readAllLines(logPath);\r\n List recent = lines.subList(Math.max(0, lines.size() - n), lines.size());\r\n List items = new ArrayList<>();\r\n for (String line : recent) {\r\n if (!line.isBlank()) {\r\n try {\r\n items.add(mapper.readValue(line, Map.class));\r\n } catch (Exception e) {\r\n items.add(Map.of(\"event\", \"parse_error\", \"raw\", line));\r\n }\r\n }\r\n }\r\n return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(items);\r\n } catch (IOException e) {\r\n log.warn(\"读取事件失败: error={}\", e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n}\r\n\n\n// === WorktreeManager.java ===\npackage io.mybatis.learn.s12;\r\n\r\nimport com.fasterxml.jackson.core.type.TypeReference;\r\nimport com.fasterxml.jackson.databind.ObjectMapper;\r\nimport io.mybatis.learn.core.tools.PathValidator;\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\n\r\nimport java.io.IOException;\r\nimport java.nio.file.Files;\r\nimport java.nio.file.Path;\r\nimport java.util.*;\r\nimport java.util.regex.Pattern;\r\n\r\n/**\r\n * Worktree管理器 - git worktree 创建/运行/删除 + 生命周期索引\r\n *\r\n * TIPS: 对应Python WorktreeManager类(s12第225-471行)。\r\n * Python用 subprocess.run([\"git\", ...]) 执行git命令;\r\n * Java用 ProcessBuilder 并检测 Windows/Unix 环境。\r\n * 索引文件 .worktrees/index.json 跟踪所有worktree的状态。\r\n */\r\npublic class WorktreeManager {\r\n private static final Logger log = LoggerFactory.getLogger(WorktreeManager.class);\r\n\r\n private static final Pattern NAME_PATTERN = Pattern.compile(\"[A-Za-z0-9._-]{1,40}\");\r\n\r\n private final Path repoRoot;\r\n private final WorktreeTaskManager tasks;\r\n private final EventBus events;\r\n private final Path worktreeDir;\r\n private final Path indexPath;\r\n private final ObjectMapper mapper = new ObjectMapper();\r\n private final boolean gitAvailable;\r\n\r\n public WorktreeManager(Path repoRoot, WorktreeTaskManager tasks, EventBus events) {\r\n this.repoRoot = repoRoot;\r\n this.tasks = tasks;\r\n this.events = events;\r\n this.worktreeDir = repoRoot.resolve(\".worktrees\");\r\n this.indexPath = worktreeDir.resolve(\"index.json\");\r\n try {\r\n Files.createDirectories(worktreeDir);\r\n if (!Files.exists(indexPath)) {\r\n Files.writeString(indexPath, \"{\\\"worktrees\\\": []}\");\r\n }\r\n log.debug(\"worktree目录与索引已就绪: dir={}, index={}\", worktreeDir, indexPath);\r\n } catch (IOException e) {\r\n log.error(\"初始化worktree目录失败: error={}\", e.getMessage());\r\n throw new RuntimeException(e);\r\n }\r\n this.gitAvailable = isGitRepo();\r\n log.info(\"WorktreeManager 初始化完成,repoRoot={}, gitAvailable={}\", repoRoot, gitAvailable);\r\n }\r\n\r\n private boolean isGitRepo() {\r\n try {\r\n ProcessBuilder pb = new ProcessBuilder(\"git\", \"rev-parse\", \"--is-inside-work-tree\");\r\n pb.directory(repoRoot.toFile());\r\n pb.redirectErrorStream(true);\r\n Process p = pb.start();\r\n int code = p.waitFor();\r\n return code == 0;\r\n } catch (Exception e) { return false; }\r\n }\r\n\r\n private String runGit(String... args) throws IOException {\r\n if (!gitAvailable) throw new IOException(\"Not in a git repository. worktree tools require git.\");\r\n List cmd = new ArrayList<>();\r\n cmd.add(\"git\");\r\n cmd.addAll(Arrays.asList(args));\r\n ProcessBuilder pb = new ProcessBuilder(cmd);\r\n pb.directory(repoRoot.toFile());\r\n pb.redirectErrorStream(true);\r\n try {\r\n Process p = pb.start();\r\n String out = new String(p.getInputStream().readAllBytes()).trim();\r\n int code = p.waitFor();\r\n if (code != 0) throw new IOException(out.isEmpty() ? \"git \" + String.join(\" \", args) + \" failed\" : out);\r\n log.debug(\"git命令执行成功: args={}\", String.join(\" \", args));\r\n return out.isEmpty() ? \"(no output)\" : out;\r\n } catch (InterruptedException e) {\r\n throw new IOException(\"git command interrupted\", e);\r\n }\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n private Map loadIndex() throws IOException {\r\n return mapper.readValue(indexPath.toFile(), new TypeReference<>() {});\r\n }\r\n\r\n private void saveIndex(Map index) throws IOException {\r\n mapper.writerWithDefaultPrettyPrinter().writeValue(indexPath.toFile(), index);\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n private Map findWorktree(String name) throws IOException {\r\n var index = loadIndex();\r\n for (var wt : (List>) index.get(\"worktrees\")) {\r\n if (name.equals(wt.get(\"name\"))) return wt;\r\n }\r\n return null;\r\n }\r\n\r\n private void validateName(String name) {\r\n if (name == null || !NAME_PATTERN.matcher(name).matches()) {\r\n throw new IllegalArgumentException(\r\n \"Invalid worktree name. Use 1-40 chars: letters, numbers, ., _, -\");\r\n }\r\n }\r\n\r\n // ---- 创建 ----\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public String create(String name, Integer taskId, String baseRef) {\r\n log.info(\"创建worktree请求: name={}, taskId={}, baseRef={}\", name, taskId, baseRef);\r\n validateName(name);\r\n try {\r\n if (findWorktree(name) != null)\r\n return \"Error: Worktree '\" + name + \"' already exists in index\";\r\n if (taskId != null && !tasks.exists(taskId))\r\n return \"Error: Task \" + taskId + \" not found\";\r\n\r\n Path path = worktreeDir.resolve(name);\r\n String branch = \"wt/\" + name;\r\n String ref = baseRef != null ? baseRef : \"HEAD\";\r\n\r\n events.emit(\"worktree.create.before\",\r\n taskId != null ? Map.of(\"id\", taskId) : Map.of(),\r\n Map.of(\"name\", name, \"base_ref\", ref), null);\r\n\r\n runGit(\"worktree\", \"add\", \"-b\", branch, path.toString(), ref);\r\n\r\n Map entry = new LinkedHashMap<>();\r\n entry.put(\"name\", name);\r\n entry.put(\"path\", path.toString());\r\n entry.put(\"branch\", branch);\r\n entry.put(\"task_id\", taskId);\r\n entry.put(\"status\", \"active\");\r\n entry.put(\"created_at\", System.currentTimeMillis() / 1000.0);\r\n\r\n var index = loadIndex();\r\n ((List>) index.get(\"worktrees\")).add(entry);\r\n saveIndex(index);\r\n\r\n if (taskId != null) tasks.bindWorktree(taskId, name, \"\");\r\n\r\n events.emit(\"worktree.create.after\",\r\n taskId != null ? Map.of(\"id\", taskId) : Map.of(),\r\n Map.of(\"name\", name, \"path\", path.toString(),\r\n \"branch\", branch, \"status\", \"active\"), null);\r\n log.info(\"创建worktree成功: name={}, branch={}, path={}\", name, branch, path);\r\n\r\n return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(entry);\r\n } catch (Exception e) {\r\n log.warn(\"创建worktree失败: name={}, error={}\", name, e.getMessage());\r\n events.emit(\"worktree.create.failed\", Map.of(), Map.of(\"name\", name), e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n // ---- 列表 ----\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public String listAll() {\r\n log.debug(\"列出worktree索引\");\r\n try {\r\n var index = loadIndex();\r\n var wts = (List>) index.get(\"worktrees\");\r\n if (wts.isEmpty()) return \"No worktrees in index.\";\r\n StringBuilder sb = new StringBuilder();\r\n for (var wt : wts) {\r\n String suffix = wt.get(\"task_id\") != null ? \" task=\" + wt.get(\"task_id\") : \"\";\r\n sb.append(String.format(\"[%s] %s -> %s (%s)%s%n\",\r\n wt.getOrDefault(\"status\", \"unknown\"), wt.get(\"name\"),\r\n wt.get(\"path\"), wt.getOrDefault(\"branch\", \"-\"), suffix));\r\n }\r\n return sb.toString().trim();\r\n } catch (IOException e) {\r\n log.warn(\"列出worktree失败: error={}\", e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n // ---- 状态 ----\r\n\r\n public String status(String name) {\r\n log.debug(\"查询worktree状态: name={}\", name);\r\n try {\r\n var wt = findWorktree(name);\r\n if (wt == null) return \"Error: Unknown worktree '\" + name + \"'\";\r\n Path path = Path.of((String) wt.get(\"path\"));\r\n if (!Files.exists(path)) return \"Error: Worktree path missing: \" + path;\r\n\r\n ProcessBuilder pb = new ProcessBuilder(\"git\", \"status\", \"--short\", \"--branch\");\r\n pb.directory(path.toFile());\r\n pb.redirectErrorStream(true);\r\n Process p = pb.start();\r\n String out = new String(p.getInputStream().readAllBytes()).trim();\r\n return out.isEmpty() ? \"Clean worktree\" : out;\r\n } catch (Exception e) {\r\n log.warn(\"查询worktree状态失败: name={}, error={}\", name, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n // ---- 在worktree中运行命令 ----\r\n\r\n public String run(String name, String command) {\r\n log.debug(\"执行worktree命令: name={}, command={}\", name,\r\n command.substring(0, Math.min(80, command.length())));\r\n String[] dangerous = {\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\", \"> /dev/\"};\r\n for (String d : dangerous) {\r\n if (command.contains(d)) {\r\n log.warn(\"拦截worktree危险命令: name={}, pattern={}\", name, d);\r\n return \"Error: Dangerous command blocked\";\r\n }\r\n }\r\n try {\r\n var wt = findWorktree(name);\r\n if (wt == null) return \"Error: Unknown worktree '\" + name + \"'\";\r\n Path path = Path.of((String) wt.get(\"path\"));\r\n if (!Files.exists(path)) return \"Error: Worktree path missing: \" + path;\r\n\r\n boolean isWindows = System.getProperty(\"os.name\").toLowerCase().contains(\"win\");\r\n ProcessBuilder pb = isWindows\r\n ? new ProcessBuilder(\"cmd\", \"/c\", command)\r\n : new ProcessBuilder(\"sh\", \"-c\", command);\r\n pb.directory(path.toFile());\r\n pb.redirectErrorStream(true);\r\n Process p = pb.start();\r\n String out = new String(p.getInputStream().readAllBytes()).trim();\r\n boolean finished = p.waitFor(300, java.util.concurrent.TimeUnit.SECONDS);\r\n if (!finished) {\r\n p.destroyForcibly();\r\n log.warn(\"worktree命令超时: name={}, timeout=300s\", name);\r\n return \"Error: Timeout (300s)\";\r\n }\r\n return out.isEmpty() ? \"(no output)\" : out.substring(0, Math.min(out.length(), 50000));\r\n } catch (Exception e) {\r\n log.warn(\"执行worktree命令失败: name={}, error={}\", name, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n // ---- 删除 ----\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public String remove(String name, boolean force, boolean completeTask) {\r\n log.info(\"删除worktree请求: name={}, force={}, completeTask={}\", name, force, completeTask);\r\n try {\r\n var wt = findWorktree(name);\r\n if (wt == null) return \"Error: Unknown worktree '\" + name + \"'\";\r\n\r\n events.emit(\"worktree.remove.before\",\r\n wt.get(\"task_id\") != null ? Map.of(\"id\", wt.get(\"task_id\")) : Map.of(),\r\n Map.of(\"name\", name, \"path\", wt.getOrDefault(\"path\", \"\")), null);\r\n\r\n List args = new ArrayList<>(List.of(\"worktree\", \"remove\"));\r\n if (force) args.add(\"--force\");\r\n args.add((String) wt.get(\"path\"));\r\n runGit(args.toArray(String[]::new));\r\n\r\n if (completeTask && wt.get(\"task_id\") != null) {\r\n int taskId = ((Number) wt.get(\"task_id\")).intValue();\r\n tasks.update(taskId, \"completed\", null);\r\n tasks.unbindWorktree(taskId);\r\n events.emit(\"task.completed\",\r\n Map.of(\"id\", taskId, \"status\", \"completed\"),\r\n Map.of(\"name\", name), null);\r\n }\r\n\r\n var index = loadIndex();\r\n for (var item : (List>) index.get(\"worktrees\")) {\r\n if (name.equals(item.get(\"name\"))) {\r\n item.put(\"status\", \"removed\");\r\n item.put(\"removed_at\", System.currentTimeMillis() / 1000.0);\r\n }\r\n }\r\n saveIndex(index);\r\n\r\n events.emit(\"worktree.remove.after\",\r\n wt.get(\"task_id\") != null ? Map.of(\"id\", wt.get(\"task_id\")) : Map.of(),\r\n Map.of(\"name\", name, \"status\", \"removed\"), null);\r\n log.info(\"删除worktree成功: name={}\", name);\r\n\r\n return \"Removed worktree '\" + name + \"'\";\r\n } catch (Exception e) {\r\n log.warn(\"删除worktree失败: name={}, error={}\", name, e.getMessage());\r\n events.emit(\"worktree.remove.failed\", Map.of(), Map.of(\"name\", name), e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n // ---- 保留 ----\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public String keep(String name) {\r\n log.info(\"保留worktree请求: name={}\", name);\r\n try {\r\n var wt = findWorktree(name);\r\n if (wt == null) return \"Error: Unknown worktree '\" + name + \"'\";\r\n\r\n var index = loadIndex();\r\n Map kept = null;\r\n for (var item : (List>) index.get(\"worktrees\")) {\r\n if (name.equals(item.get(\"name\"))) {\r\n item.put(\"status\", \"kept\");\r\n item.put(\"kept_at\", System.currentTimeMillis() / 1000.0);\r\n kept = item;\r\n }\r\n }\r\n saveIndex(index);\r\n\r\n events.emit(\"worktree.keep\",\r\n wt.get(\"task_id\") != null ? Map.of(\"id\", wt.get(\"task_id\")) : Map.of(),\r\n Map.of(\"name\", name, \"status\", \"kept\"), null);\r\n log.info(\"保留worktree成功: name={}\", name);\r\n\r\n return kept != null\r\n ? mapper.writerWithDefaultPrettyPrinter().writeValueAsString(kept)\r\n : \"Error: Unknown worktree '\" + name + \"'\";\r\n } catch (Exception e) {\r\n log.warn(\"保留worktree失败: name={}, error={}\", name, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n public boolean isGitAvailable() { return gitAvailable; }\r\n}\r\n\n\n// === WorktreeTaskManager.java ===\npackage io.mybatis.learn.s12;\r\n\r\nimport com.fasterxml.jackson.databind.ObjectMapper;\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\n\r\nimport java.io.IOException;\r\nimport java.nio.file.Files;\r\nimport java.nio.file.Path;\r\nimport java.util.*;\r\n\r\n/**\r\n * Worktree任务管理器 - 持久化任务板 + worktree绑定\r\n *\r\n * TIPS: 对应Python TaskManager类(s12第122-217行)。\r\n * 与S07的TaskManager不同,此版本增加了 worktree 字段用于目录隔离绑定。\r\n * 任务数据结构:{id, subject, description, status, owner, worktree, blockedBy, created_at, updated_at}\r\n */\r\npublic class WorktreeTaskManager {\r\n private static final Logger log = LoggerFactory.getLogger(WorktreeTaskManager.class);\r\n\r\n private final Path tasksDir;\r\n private final ObjectMapper mapper = new ObjectMapper();\r\n private int nextId;\r\n\r\n public WorktreeTaskManager(Path tasksDir) {\r\n this.tasksDir = tasksDir;\r\n try { Files.createDirectories(tasksDir); } catch (IOException e) {\r\n log.error(\"创建worktree任务目录失败: {}, error={}\", tasksDir, e.getMessage());\r\n throw new RuntimeException(e);\r\n }\r\n this.nextId = maxId() + 1;\r\n log.info(\"WorktreeTaskManager 初始化完成,nextId={}, dir={}\", nextId, tasksDir);\r\n }\r\n\r\n private int maxId() {\r\n try (var files = Files.list(tasksDir)) {\r\n return files.filter(f -> f.getFileName().toString().matches(\"task_\\\\d+\\\\.json\"))\r\n .mapToInt(f -> {\r\n String name = f.getFileName().toString();\r\n return Integer.parseInt(name.substring(5, name.length() - 5));\r\n })\r\n .max().orElse(0);\r\n } catch (IOException e) { return 0; }\r\n }\r\n\r\n private Path taskPath(int taskId) {\r\n return tasksDir.resolve(\"task_\" + taskId + \".json\");\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n private Map load(int taskId) throws IOException {\r\n Path path = taskPath(taskId);\r\n if (!Files.exists(path)) throw new IOException(\"Task \" + taskId + \" not found\");\r\n return mapper.readValue(path.toFile(), Map.class);\r\n }\r\n\r\n private void save(Map task) throws IOException {\r\n mapper.writerWithDefaultPrettyPrinter().writeValue(\r\n taskPath(((Number) task.get(\"id\")).intValue()).toFile(), task);\r\n }\r\n\r\n public boolean exists(int taskId) {\r\n return Files.exists(taskPath(taskId));\r\n }\r\n\r\n public synchronized String create(String subject, String description) {\r\n log.debug(\"创建worktree任务: subject={}\", subject);\r\n Map task = new LinkedHashMap<>();\r\n task.put(\"id\", nextId);\r\n task.put(\"subject\", subject);\r\n task.put(\"description\", description != null ? description : \"\");\r\n task.put(\"status\", \"pending\");\r\n task.put(\"owner\", \"\");\r\n task.put(\"worktree\", \"\");\r\n task.put(\"blockedBy\", new ArrayList<>());\r\n task.put(\"created_at\", System.currentTimeMillis() / 1000.0);\r\n task.put(\"updated_at\", System.currentTimeMillis() / 1000.0);\r\n try {\r\n save(task);\r\n nextId++;\r\n log.debug(\"worktree任务创建成功: id={}, nextId={}\", task.get(\"id\"), nextId);\r\n return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(task);\r\n } catch (IOException e) {\r\n log.warn(\"worktree任务创建失败: subject={}, error={}\", subject, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n public String get(int taskId) {\r\n try {\r\n return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(load(taskId));\r\n } catch (IOException e) {\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n public String update(int taskId, String status, String owner) {\r\n log.debug(\"更新worktree任务: taskId={}, status={}, owner={}\", taskId, status, owner);\r\n try {\r\n var task = load(taskId);\r\n if (status != null) {\r\n if (!Set.of(\"pending\", \"in_progress\", \"completed\").contains(status))\r\n return \"Error: Invalid status: \" + status;\r\n task.put(\"status\", status);\r\n }\r\n if (owner != null) task.put(\"owner\", owner);\r\n task.put(\"updated_at\", System.currentTimeMillis() / 1000.0);\r\n save(task);\r\n log.debug(\"worktree任务更新成功: taskId={}\", taskId);\r\n return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(task);\r\n } catch (IOException e) {\r\n log.warn(\"worktree任务更新失败: taskId={}, error={}\", taskId, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n /**\r\n * 绑定worktree到任务(如果任务是pending则自动变为in_progress)\r\n */\r\n public String bindWorktree(int taskId, String worktree, String owner) {\r\n log.debug(\"绑定worktree: taskId={}, worktree={}, owner={}\", taskId, worktree, owner);\r\n try {\r\n var task = load(taskId);\r\n task.put(\"worktree\", worktree);\r\n if (owner != null && !owner.isEmpty()) task.put(\"owner\", owner);\r\n if (\"pending\".equals(task.get(\"status\"))) task.put(\"status\", \"in_progress\");\r\n task.put(\"updated_at\", System.currentTimeMillis() / 1000.0);\r\n save(task);\r\n return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(task);\r\n } catch (IOException e) {\r\n log.warn(\"绑定worktree失败: taskId={}, worktree={}, error={}\", taskId, worktree, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n public String unbindWorktree(int taskId) {\r\n log.debug(\"解绑worktree: taskId={}\", taskId);\r\n try {\r\n var task = load(taskId);\r\n task.put(\"worktree\", \"\");\r\n task.put(\"updated_at\", System.currentTimeMillis() / 1000.0);\r\n save(task);\r\n return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(task);\r\n } catch (IOException e) {\r\n log.warn(\"解绑worktree失败: taskId={}, error={}\", taskId, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public String listAll() {\r\n try (var files = Files.list(tasksDir)) {\r\n List> tasks = new ArrayList<>();\r\n files.filter(f -> f.getFileName().toString().matches(\"task_\\\\d+\\\\.json\"))\r\n .sorted()\r\n .forEach(f -> {\r\n try { tasks.add(mapper.readValue(f.toFile(), Map.class)); }\r\n catch (IOException e) { /* skip */ }\r\n });\r\n if (tasks.isEmpty()) return \"No tasks.\";\r\n StringBuilder sb = new StringBuilder();\r\n for (var t : tasks) {\r\n String status = (String) t.get(\"status\");\r\n String marker = switch (status) {\r\n case \"pending\" -> \"[ ]\";\r\n case \"in_progress\" -> \"[>]\";\r\n case \"completed\" -> \"[x]\";\r\n default -> \"[?]\";\r\n };\r\n String owner = t.get(\"owner\") != null && !t.get(\"owner\").toString().isEmpty()\r\n ? \" owner=\" + t.get(\"owner\") : \"\";\r\n String wt = t.get(\"worktree\") != null && !t.get(\"worktree\").toString().isEmpty()\r\n ? \" wt=\" + t.get(\"worktree\") : \"\";\r\n sb.append(String.format(\"%s #%s: %s%s%s%n\",\r\n marker, t.get(\"id\"), t.get(\"subject\"), owner, wt));\r\n }\r\n return sb.toString().trim();\r\n } catch (IOException e) {\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n}\r\n" + "source": "// === S12WorktreeIsolation.java ===\npackage io.mybatis.learn.s12;\r\n\r\nimport io.mybatis.learn.core.AgentRunner;\r\nimport io.mybatis.learn.core.tools.*;\r\nimport org.springframework.ai.chat.client.ChatClient;\r\nimport org.springframework.ai.chat.model.ChatModel;\r\nimport org.springframework.ai.tool.annotation.Tool;\r\nimport org.springframework.ai.tool.annotation.ToolParam;\r\nimport org.springframework.beans.factory.annotation.Autowired;\r\nimport org.springframework.boot.CommandLineRunner;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\nimport java.nio.file.Path;\r\n\r\n/**\r\n * S12 - Worktree + Task Isolation:目录级隔离的并行任务执行\r\n *\r\n * TIPS: 对应Python s12_worktree_task_isolation.py。\r\n * 与S09-S11的团队通信路线不同,S12走「目录隔离」路线:\r\n * 任务(控制平面)+ Worktree(执行平面)。\r\n *\r\n * S09-S11: 多个agent → 消息队列通信 → 协议协调\r\n * S12: 单个agent → 多个worktree → 按目录隔离 → 按task ID协调\r\n *\r\n * 17个工具:base(4) + task(5) + worktree(8)\r\n *\r\n * 关键洞察:\"Isolate by directory, coordinate by task ID.\"\r\n */\r\n@SpringBootApplication(scanBasePackages = \"io.mybatis.learn.core\")\r\npublic class S12WorktreeIsolation implements CommandLineRunner {\r\n\r\n @Autowired\r\n private ChatModel chatModel;\r\n\r\n @Override\r\n public void run(String... args) throws Exception {\r\n Path workDir = Path.of(System.getProperty(\"user.dir\"));\r\n\r\n // 检测git仓库根目录\r\n Path repoRoot = detectRepoRoot(workDir);\r\n if (repoRoot == null) repoRoot = workDir;\r\n\r\n WorktreeTaskManager tasks = new WorktreeTaskManager(repoRoot.resolve(\".tasks\"));\r\n EventBus events = new EventBus(repoRoot.resolve(\".worktrees\").resolve(\"events.jsonl\"));\r\n WorktreeManager worktrees = new WorktreeManager(repoRoot, tasks, events);\r\n\r\n TaskTools taskTools = new TaskTools(tasks);\r\n WorktreeTools wtTools = new WorktreeTools(worktrees, events);\r\n\r\n String systemPrompt = \"You are a coding agent at \" + workDir + \". \"\r\n + \"Use task + worktree tools for multi-task work. \"\r\n + \"For parallel or risky changes: create tasks, allocate worktree lanes, \"\r\n + \"run commands in those lanes, then choose keep/remove for closeout. \"\r\n + \"Use worktree_events when you need lifecycle visibility.\";\r\n\r\n ChatClient chatClient = ChatClient.builder(chatModel)\r\n .defaultSystem(systemPrompt)\r\n .defaultTools(new BashTool(), new ReadFileTool(),\r\n new WriteFileTool(), new EditFileTool(),\r\n taskTools, wtTools)\r\n .build();\r\n\r\n System.out.println(\"Repo root for s12: \" + repoRoot);\r\n if (!worktrees.isGitAvailable()) {\r\n System.out.println(\"Note: Not in a git repo. worktree_* tools will return errors.\");\r\n }\r\n\r\n AgentRunner.interactive(\"s12\", input ->\r\n chatClient.prompt(input).call().content());\r\n }\r\n\r\n private static Path detectRepoRoot(Path cwd) {\r\n try {\r\n ProcessBuilder pb = new ProcessBuilder(\"git\", \"rev-parse\", \"--show-toplevel\");\r\n pb.directory(cwd.toFile());\r\n pb.redirectErrorStream(true);\r\n Process p = pb.start();\r\n String out = new String(p.getInputStream().readAllBytes()).trim();\r\n if (p.waitFor() == 0 && !out.isEmpty()) {\r\n Path root = Path.of(out);\r\n return root.toFile().exists() ? root : null;\r\n }\r\n } catch (Exception e) { /* ignore */ }\r\n return null;\r\n }\r\n\r\n // ---- 任务工具集(5个) ----\r\n\r\n public static class TaskTools {\r\n private final WorktreeTaskManager tasks;\r\n\r\n public TaskTools(WorktreeTaskManager tasks) { this.tasks = tasks; }\r\n\r\n @Tool(description = \"Create a new task on the shared task board\")\r\n public String taskCreate(\r\n @ToolParam(description = \"Task subject\") String subject,\r\n @ToolParam(description = \"Task description\") String description) {\r\n return tasks.create(subject, description);\r\n }\r\n\r\n @Tool(description = \"List all tasks with status, owner, and worktree binding\")\r\n public String taskList() { return tasks.listAll(); }\r\n\r\n @Tool(description = \"Get task details by ID\")\r\n public String taskGet(@ToolParam(description = \"Task ID\") int taskId) {\r\n return tasks.get(taskId);\r\n }\r\n\r\n @Tool(description = \"Update task status or owner\")\r\n public String taskUpdate(\r\n @ToolParam(description = \"Task ID\") int taskId,\r\n @ToolParam(description = \"New status: pending/in_progress/completed\") String status,\r\n @ToolParam(description = \"New owner name\") String owner) {\r\n return tasks.update(taskId, status, owner);\r\n }\r\n\r\n @Tool(description = \"Bind a task to a worktree name\")\r\n public String taskBindWorktree(\r\n @ToolParam(description = \"Task ID\") int taskId,\r\n @ToolParam(description = \"Worktree name\") String worktree,\r\n @ToolParam(description = \"Owner name\") String owner) {\r\n return tasks.bindWorktree(taskId, worktree, owner);\r\n }\r\n }\r\n\r\n // ---- Worktree工具集(8个) ----\r\n\r\n /**\r\n * TIPS: 对应Python TOOL_HANDLERS中的8个worktree工具(s12第546-552行)。\r\n * 每个工具委托给WorktreeManager执行实际操作。\r\n * 安全检查:worktree名称正则验证(1-40字符),危险命令阻止。\r\n */\r\n public static class WorktreeTools {\r\n private final WorktreeManager worktrees;\r\n private final EventBus events;\r\n\r\n public WorktreeTools(WorktreeManager worktrees, EventBus events) {\r\n this.worktrees = worktrees;\r\n this.events = events;\r\n }\r\n\r\n @Tool(description = \"Create a git worktree and optionally bind it to a task\")\r\n public String worktreeCreate(\r\n @ToolParam(description = \"Worktree name (1-40 chars: letters, numbers, ., _, -)\") String name,\r\n @ToolParam(description = \"Task ID to bind (optional)\") Integer taskId,\r\n @ToolParam(description = \"Base git ref (default: HEAD)\") String baseRef) {\r\n return worktrees.create(name, taskId, baseRef);\r\n }\r\n\r\n @Tool(description = \"List worktrees tracked in .worktrees/index.json\")\r\n public String worktreeList() { return worktrees.listAll(); }\r\n\r\n @Tool(description = \"Show git status for one worktree\")\r\n public String worktreeStatus(\r\n @ToolParam(description = \"Worktree name\") String name) {\r\n return worktrees.status(name);\r\n }\r\n\r\n @Tool(description = \"Run a shell command in a named worktree directory\")\r\n public String worktreeRun(\r\n @ToolParam(description = \"Worktree name\") String name,\r\n @ToolParam(description = \"Shell command to run\") String command) {\r\n return worktrees.run(name, command);\r\n }\r\n\r\n @Tool(description = \"Remove a worktree and optionally mark its bound task completed\")\r\n public String worktreeRemove(\r\n @ToolParam(description = \"Worktree name\") String name,\r\n @ToolParam(description = \"Force remove\") boolean force,\r\n @ToolParam(description = \"Mark bound task as completed\") boolean completeTask) {\r\n return worktrees.remove(name, force, completeTask);\r\n }\r\n\r\n @Tool(description = \"Mark a worktree as kept in lifecycle state without removing it\")\r\n public String worktreeKeep(\r\n @ToolParam(description = \"Worktree name\") String name) {\r\n return worktrees.keep(name);\r\n }\r\n\r\n @Tool(description = \"List recent worktree/task lifecycle events\")\r\n public String worktreeEvents(\r\n @ToolParam(description = \"Number of events to show (default 20)\") int limit) {\r\n return events.listRecent(limit > 0 ? limit : 20);\r\n }\r\n }\r\n\r\n public static void main(String[] args) {\r\n SpringApplication.run(S12WorktreeIsolation.class, args);\r\n }\r\n}\r\n\n\n// === EventBus.java ===\npackage io.mybatis.learn.s12;\r\n\r\nimport com.fasterxml.jackson.databind.ObjectMapper;\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\n\r\nimport java.io.IOException;\r\nimport java.nio.file.Files;\r\nimport java.nio.file.Path;\r\nimport java.nio.file.StandardOpenOption;\r\nimport java.util.ArrayList;\r\nimport java.util.LinkedHashMap;\r\nimport java.util.List;\r\nimport java.util.Map;\r\n\r\n/**\r\n * 事件总线 - 追加式JSONL生命周期事件日志\r\n *\r\n * TIPS: 对应Python EventBus类(s12第83-118行)。\r\n * 记录 worktree 和 task 的生命周期事件,用于可观测性。\r\n * 事件类型:worktree.create.before/after/failed, worktree.remove.*, worktree.keep, task.completed\r\n */\r\npublic class EventBus {\r\n private static final Logger log = LoggerFactory.getLogger(EventBus.class);\r\n\r\n private final Path logPath;\r\n private final ObjectMapper mapper = new ObjectMapper();\r\n\r\n public EventBus(Path logPath) {\r\n this.logPath = logPath;\r\n try {\r\n Files.createDirectories(logPath.getParent());\r\n if (!Files.exists(logPath)) Files.writeString(logPath, \"\");\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"🚀 事件日志就绪: %s%n\", logPath);\r\n }\r\n } catch (IOException e) {\r\n log.error(\"初始化事件日志失败: {}, error={}\", logPath, e.getMessage());\r\n throw new RuntimeException(e);\r\n }\r\n }\r\n\r\n public synchronized void emit(String event, Map task,\r\n Map worktree, String error) {\r\n Map payload = new LinkedHashMap<>();\r\n payload.put(\"event\", event);\r\n payload.put(\"ts\", System.currentTimeMillis() / 1000.0);\r\n payload.put(\"task\", task != null ? task : Map.of());\r\n payload.put(\"worktree\", worktree != null ? worktree : Map.of());\r\n if (error != null) payload.put(\"error\", error);\r\n\r\n try {\r\n Files.writeString(logPath, mapper.writeValueAsString(payload) + \"\\n\",\r\n StandardOpenOption.APPEND);\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"📡 事件已记录: %s%n\", event);\r\n }\r\n } catch (IOException e) {\r\n log.warn(\"事件写入失败: event={}, error={}\", event, e.getMessage());\r\n System.err.println(\"EventBus emit error: \" + e.getMessage());\r\n }\r\n }\r\n\r\n public void emit(String event) {\r\n emit(event, null, null, null);\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public String listRecent(int limit) {\r\n int n = Math.max(1, Math.min(limit, 200));\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"🔍 读取最近 %d 条事件 (请求 %d)%n\", n, limit);\r\n }\r\n try {\r\n List lines = Files.readAllLines(logPath);\r\n List recent = lines.subList(Math.max(0, lines.size() - n), lines.size());\r\n List items = new ArrayList<>();\r\n for (String line : recent) {\r\n if (!line.isBlank()) {\r\n try {\r\n items.add(mapper.readValue(line, Map.class));\r\n } catch (Exception e) {\r\n items.add(Map.of(\"event\", \"parse_error\", \"raw\", line));\r\n }\r\n }\r\n }\r\n return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(items);\r\n } catch (IOException e) {\r\n log.warn(\"读取事件失败: error={}\", e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n}\r\n\n\n// === WorktreeManager.java ===\npackage io.mybatis.learn.s12;\r\n\r\nimport com.fasterxml.jackson.core.type.TypeReference;\r\nimport com.fasterxml.jackson.databind.ObjectMapper;\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\n\r\nimport java.io.IOException;\r\nimport java.nio.file.Files;\r\nimport java.nio.file.Path;\r\nimport java.util.*;\r\nimport java.util.regex.Pattern;\r\n\r\n/**\r\n * Worktree管理器 - git worktree 创建/运行/删除 + 生命周期索引\r\n *\r\n * TIPS: 对应Python WorktreeManager类(s12第225-471行)。\r\n * Python用 subprocess.run([\"git\", ...]) 执行git命令;\r\n * Java用 ProcessBuilder 并检测 Windows/Unix 环境。\r\n * 索引文件 .worktrees/index.json 跟踪所有worktree的状态。\r\n */\r\npublic class WorktreeManager {\r\n private static final Logger log = LoggerFactory.getLogger(WorktreeManager.class);\r\n\r\n private static final Pattern NAME_PATTERN = Pattern.compile(\"[A-Za-z0-9._-]{1,40}\");\r\n\r\n private final Path repoRoot;\r\n private final WorktreeTaskManager tasks;\r\n private final EventBus events;\r\n private final Path worktreeDir;\r\n private final Path indexPath;\r\n private final ObjectMapper mapper = new ObjectMapper();\r\n private final boolean gitAvailable;\r\n\r\n public WorktreeManager(Path repoRoot, WorktreeTaskManager tasks, EventBus events) {\r\n this.repoRoot = repoRoot;\r\n this.tasks = tasks;\r\n this.events = events;\r\n this.worktreeDir = repoRoot.resolve(\".worktrees\");\r\n this.indexPath = worktreeDir.resolve(\"index.json\");\r\n try {\r\n Files.createDirectories(worktreeDir);\r\n if (!Files.exists(indexPath)) {\r\n Files.writeString(indexPath, \"{\\\"worktrees\\\": []}\");\r\n }\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"🚀 worktree目录与索引就绪: dir=%s, index=%s%n\", worktreeDir, indexPath);\r\n }\r\n } catch (IOException e) {\r\n log.error(\"初始化worktree目录失败: error={}\", e.getMessage());\r\n throw new RuntimeException(e);\r\n }\r\n this.gitAvailable = isGitRepo();\r\n log.info(\"WorktreeManager 初始化完成,repoRoot={}, gitAvailable={}\", repoRoot, gitAvailable);\r\n }\r\n\r\n private boolean isGitRepo() {\r\n try {\r\n ProcessBuilder pb = new ProcessBuilder(\"git\", \"rev-parse\", \"--is-inside-work-tree\");\r\n pb.directory(repoRoot.toFile());\r\n pb.redirectErrorStream(true);\r\n Process p = pb.start();\r\n int code = p.waitFor();\r\n return code == 0;\r\n } catch (Exception e) { return false; }\r\n }\r\n\r\n private String runGit(String... args) throws IOException {\r\n if (!gitAvailable) throw new IOException(\"Not in a git repository. worktree tools require git.\");\r\n List cmd = new ArrayList<>();\r\n cmd.add(\"git\");\r\n cmd.addAll(Arrays.asList(args));\r\n ProcessBuilder pb = new ProcessBuilder(cmd);\r\n pb.directory(repoRoot.toFile());\r\n pb.redirectErrorStream(true);\r\n try {\r\n Process p = pb.start();\r\n String out = new String(p.getInputStream().readAllBytes()).trim();\r\n int code = p.waitFor();\r\n if (code != 0) throw new IOException(out.isEmpty() ? \"git \" + String.join(\" \", args) + \" failed\" : out);\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"✅ git 命令完成: %s%n\", String.join(\" \", args));\r\n }\r\n return out.isEmpty() ? \"(no output)\" : out;\r\n } catch (InterruptedException e) {\r\n throw new IOException(\"git command interrupted\", e);\r\n }\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n private Map loadIndex() throws IOException {\r\n return mapper.readValue(indexPath.toFile(), new TypeReference<>() {});\r\n }\r\n\r\n private void saveIndex(Map index) throws IOException {\r\n mapper.writerWithDefaultPrettyPrinter().writeValue(indexPath.toFile(), index);\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n private Map findWorktree(String name) throws IOException {\r\n var index = loadIndex();\r\n for (var wt : (List>) index.get(\"worktrees\")) {\r\n if (name.equals(wt.get(\"name\"))) return wt;\r\n }\r\n return null;\r\n }\r\n\r\n private void validateName(String name) {\r\n if (name == null || !NAME_PATTERN.matcher(name).matches()) {\r\n throw new IllegalArgumentException(\r\n \"Invalid worktree name. Use 1-40 chars: letters, numbers, ., _, -\");\r\n }\r\n }\r\n\r\n // ---- 创建 ----\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public String create(String name, Integer taskId, String baseRef) {\r\n log.info(\"创建worktree请求: name={}, taskId={}, baseRef={}\", name, taskId, baseRef);\r\n validateName(name);\r\n try {\r\n if (findWorktree(name) != null)\r\n return \"Error: Worktree '\" + name + \"' already exists in index\";\r\n if (taskId != null && !tasks.exists(taskId))\r\n return \"Error: Task \" + taskId + \" not found\";\r\n\r\n Path path = worktreeDir.resolve(name);\r\n String branch = \"wt/\" + name;\r\n String ref = baseRef != null ? baseRef : \"HEAD\";\r\n\r\n events.emit(\"worktree.create.before\",\r\n taskId != null ? Map.of(\"id\", taskId) : Map.of(),\r\n Map.of(\"name\", name, \"base_ref\", ref), null);\r\n\r\n runGit(\"worktree\", \"add\", \"-b\", branch, path.toString(), ref);\r\n\r\n Map entry = new LinkedHashMap<>();\r\n entry.put(\"name\", name);\r\n entry.put(\"path\", path.toString());\r\n entry.put(\"branch\", branch);\r\n entry.put(\"task_id\", taskId);\r\n entry.put(\"status\", \"active\");\r\n entry.put(\"created_at\", System.currentTimeMillis() / 1000.0);\r\n\r\n var index = loadIndex();\r\n ((List>) index.get(\"worktrees\")).add(entry);\r\n saveIndex(index);\r\n\r\n if (taskId != null) tasks.bindWorktree(taskId, name, \"\");\r\n\r\n events.emit(\"worktree.create.after\",\r\n taskId != null ? Map.of(\"id\", taskId) : Map.of(),\r\n Map.of(\"name\", name, \"path\", path.toString(),\r\n \"branch\", branch, \"status\", \"active\"), null);\r\n log.info(\"创建worktree成功: name={}, branch={}, path={}\", name, branch, path);\r\n\r\n return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(entry);\r\n } catch (Exception e) {\r\n log.warn(\"创建worktree失败: name={}, error={}\", name, e.getMessage());\r\n events.emit(\"worktree.create.failed\", Map.of(), Map.of(\"name\", name), e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n // ---- 列表 ----\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public String listAll() {\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"📋 列出所有worktree%n\");\r\n }\r\n try {\r\n var index = loadIndex();\r\n var wts = (List>) index.get(\"worktrees\");\r\n if (wts.isEmpty()) return \"No worktrees in index.\";\r\n StringBuilder sb = new StringBuilder();\r\n for (var wt : wts) {\r\n String suffix = wt.get(\"task_id\") != null ? \" task=\" + wt.get(\"task_id\") : \"\";\r\n sb.append(String.format(\"[%s] %s -> %s (%s)%s%n\",\r\n wt.getOrDefault(\"status\", \"unknown\"), wt.get(\"name\"),\r\n wt.get(\"path\"), wt.getOrDefault(\"branch\", \"-\"), suffix));\r\n }\r\n return sb.toString().trim();\r\n } catch (IOException e) {\r\n log.warn(\"列出worktree失败: error={}\", e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n // ---- 状态 ----\r\n\r\n public String status(String name) {\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"🔍 查询worktree状态: %s%n\", name);\r\n }\r\n try {\r\n var wt = findWorktree(name);\r\n if (wt == null) return \"Error: Unknown worktree '\" + name + \"'\";\r\n Path path = Path.of((String) wt.get(\"path\"));\r\n if (!Files.exists(path)) return \"Error: Worktree path missing: \" + path;\r\n\r\n ProcessBuilder pb = new ProcessBuilder(\"git\", \"status\", \"--short\", \"--branch\");\r\n pb.directory(path.toFile());\r\n pb.redirectErrorStream(true);\r\n Process p = pb.start();\r\n String out = new String(p.getInputStream().readAllBytes()).trim();\r\n return out.isEmpty() ? \"Clean worktree\" : out;\r\n } catch (Exception e) {\r\n log.warn(\"查询worktree状态失败: name={}, error={}\", name, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n // ---- 在worktree中运行命令 ----\r\n\r\n public String run(String name, String command) {\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"🔧 在 %s 中执行命令: %s%n\", name,\r\n command.substring(0, Math.min(80, command.length())));\r\n }\r\n String[] dangerous = {\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\", \"> /dev/\"};\r\n for (String d : dangerous) {\r\n if (command.contains(d)) {\r\n log.warn(\"拦截worktree危险命令: name={}, pattern={}\", name, d);\r\n return \"Error: Dangerous command blocked\";\r\n }\r\n }\r\n try {\r\n var wt = findWorktree(name);\r\n if (wt == null) return \"Error: Unknown worktree '\" + name + \"'\";\r\n Path path = Path.of((String) wt.get(\"path\"));\r\n if (!Files.exists(path)) return \"Error: Worktree path missing: \" + path;\r\n\r\n boolean isWindows = System.getProperty(\"os.name\").toLowerCase().contains(\"win\");\r\n ProcessBuilder pb = isWindows\r\n ? new ProcessBuilder(\"cmd\", \"/c\", command)\r\n : new ProcessBuilder(\"sh\", \"-c\", command);\r\n pb.directory(path.toFile());\r\n pb.redirectErrorStream(true);\r\n Process p = pb.start();\r\n String out = new String(p.getInputStream().readAllBytes()).trim();\r\n boolean finished = p.waitFor(300, java.util.concurrent.TimeUnit.SECONDS);\r\n if (!finished) {\r\n p.destroyForcibly();\r\n log.warn(\"worktree命令超时: name={}, timeout=300s\", name);\r\n return \"Error: Timeout (300s)\";\r\n }\r\n return out.isEmpty() ? \"(no output)\" : out.substring(0, Math.min(out.length(), 50000));\r\n } catch (Exception e) {\r\n log.warn(\"执行worktree命令失败: name={}, error={}\", name, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n // ---- 删除 ----\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public String remove(String name, boolean force, boolean completeTask) {\r\n log.info(\"删除worktree请求: name={}, force={}, completeTask={}\", name, force, completeTask);\r\n try {\r\n var wt = findWorktree(name);\r\n if (wt == null) return \"Error: Unknown worktree '\" + name + \"'\";\r\n\r\n events.emit(\"worktree.remove.before\",\r\n wt.get(\"task_id\") != null ? Map.of(\"id\", wt.get(\"task_id\")) : Map.of(),\r\n Map.of(\"name\", name, \"path\", wt.getOrDefault(\"path\", \"\")), null);\r\n\r\n List args = new ArrayList<>(List.of(\"worktree\", \"remove\"));\r\n if (force) args.add(\"--force\");\r\n args.add((String) wt.get(\"path\"));\r\n runGit(args.toArray(String[]::new));\r\n\r\n if (completeTask && wt.get(\"task_id\") != null) {\r\n int taskId = ((Number) wt.get(\"task_id\")).intValue();\r\n tasks.update(taskId, \"completed\", null);\r\n tasks.unbindWorktree(taskId);\r\n events.emit(\"task.completed\",\r\n Map.of(\"id\", taskId, \"status\", \"completed\"),\r\n Map.of(\"name\", name), null);\r\n }\r\n\r\n var index = loadIndex();\r\n for (var item : (List>) index.get(\"worktrees\")) {\r\n if (name.equals(item.get(\"name\"))) {\r\n item.put(\"status\", \"removed\");\r\n item.put(\"removed_at\", System.currentTimeMillis() / 1000.0);\r\n }\r\n }\r\n saveIndex(index);\r\n\r\n events.emit(\"worktree.remove.after\",\r\n wt.get(\"task_id\") != null ? Map.of(\"id\", wt.get(\"task_id\")) : Map.of(),\r\n Map.of(\"name\", name, \"status\", \"removed\"), null);\r\n log.info(\"删除worktree成功: name={}\", name);\r\n\r\n return \"Removed worktree '\" + name + \"'\";\r\n } catch (Exception e) {\r\n log.warn(\"删除worktree失败: name={}, error={}\", name, e.getMessage());\r\n events.emit(\"worktree.remove.failed\", Map.of(), Map.of(\"name\", name), e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n // ---- 保留 ----\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public String keep(String name) {\r\n log.info(\"保留worktree请求: name={}\", name);\r\n try {\r\n var wt = findWorktree(name);\r\n if (wt == null) return \"Error: Unknown worktree '\" + name + \"'\";\r\n\r\n var index = loadIndex();\r\n Map kept = null;\r\n for (var item : (List>) index.get(\"worktrees\")) {\r\n if (name.equals(item.get(\"name\"))) {\r\n item.put(\"status\", \"kept\");\r\n item.put(\"kept_at\", System.currentTimeMillis() / 1000.0);\r\n kept = item;\r\n }\r\n }\r\n saveIndex(index);\r\n\r\n events.emit(\"worktree.keep\",\r\n wt.get(\"task_id\") != null ? Map.of(\"id\", wt.get(\"task_id\")) : Map.of(),\r\n Map.of(\"name\", name, \"status\", \"kept\"), null);\r\n log.info(\"保留worktree成功: name={}\", name);\r\n\r\n return kept != null\r\n ? mapper.writerWithDefaultPrettyPrinter().writeValueAsString(kept)\r\n : \"Error: Unknown worktree '\" + name + \"'\";\r\n } catch (Exception e) {\r\n log.warn(\"保留worktree失败: name={}, error={}\", name, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n public boolean isGitAvailable() { return gitAvailable; }\r\n}\r\n\n\n// === WorktreeTaskManager.java ===\npackage io.mybatis.learn.s12;\r\n\r\nimport com.fasterxml.jackson.databind.ObjectMapper;\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\n\r\nimport java.io.IOException;\r\nimport java.nio.file.Files;\r\nimport java.nio.file.Path;\r\nimport java.util.*;\r\n\r\n/**\r\n * Worktree任务管理器 - 持久化任务板 + worktree绑定\r\n *\r\n * TIPS: 对应Python TaskManager类(s12第122-217行)。\r\n * 与S07的TaskManager不同,此版本增加了 worktree 字段用于目录隔离绑定。\r\n * 任务数据结构:{id, subject, description, status, owner, worktree, blockedBy, created_at, updated_at}\r\n */\r\npublic class WorktreeTaskManager {\r\n private static final Logger log = LoggerFactory.getLogger(WorktreeTaskManager.class);\r\n\r\n private final Path tasksDir;\r\n private final ObjectMapper mapper = new ObjectMapper();\r\n private int nextId;\r\n\r\n public WorktreeTaskManager(Path tasksDir) {\r\n this.tasksDir = tasksDir;\r\n try { Files.createDirectories(tasksDir); } catch (IOException e) {\r\n log.error(\"创建worktree任务目录失败: {}, error={}\", tasksDir, e.getMessage());\r\n throw new RuntimeException(e);\r\n }\r\n this.nextId = maxId() + 1;\r\n log.info(\"WorktreeTaskManager 初始化完成,nextId={}, dir={}\", nextId, tasksDir);\r\n }\r\n\r\n private int maxId() {\r\n try (var files = Files.list(tasksDir)) {\r\n return files.filter(f -> f.getFileName().toString().matches(\"task_\\\\d+\\\\.json\"))\r\n .mapToInt(f -> {\r\n String name = f.getFileName().toString();\r\n return Integer.parseInt(name.substring(5, name.length() - 5));\r\n })\r\n .max().orElse(0);\r\n } catch (IOException e) { return 0; }\r\n }\r\n\r\n private Path taskPath(int taskId) {\r\n return tasksDir.resolve(\"task_\" + taskId + \".json\");\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n private Map load(int taskId) throws IOException {\r\n Path path = taskPath(taskId);\r\n if (!Files.exists(path)) throw new IOException(\"Task \" + taskId + \" not found\");\r\n return mapper.readValue(path.toFile(), Map.class);\r\n }\r\n\r\n private void save(Map task) throws IOException {\r\n mapper.writerWithDefaultPrettyPrinter().writeValue(\r\n taskPath(((Number) task.get(\"id\")).intValue()).toFile(), task);\r\n }\r\n\r\n public boolean exists(int taskId) {\r\n return Files.exists(taskPath(taskId));\r\n }\r\n\r\n public synchronized String create(String subject, String description) {\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"📋 创建worktree任务: %s%n\", subject);\r\n }\r\n Map task = new LinkedHashMap<>();\r\n task.put(\"id\", nextId);\r\n task.put(\"subject\", subject);\r\n task.put(\"description\", description != null ? description : \"\");\r\n task.put(\"status\", \"pending\");\r\n task.put(\"owner\", \"\");\r\n task.put(\"worktree\", \"\");\r\n task.put(\"blockedBy\", new ArrayList<>());\r\n task.put(\"created_at\", System.currentTimeMillis() / 1000.0);\r\n task.put(\"updated_at\", System.currentTimeMillis() / 1000.0);\r\n try {\r\n save(task);\r\n nextId++;\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"✅ worktree任务 #%s 已创建 (nextId=%d)%n\", task.get(\"id\"), nextId);\r\n }\r\n return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(task);\r\n } catch (IOException e) {\r\n log.warn(\"worktree任务创建失败: subject={}, error={}\", subject, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n public String get(int taskId) {\r\n try {\r\n return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(load(taskId));\r\n } catch (IOException e) {\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n public String update(int taskId, String status, String owner) {\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"🔄 更新worktree任务 #%d (status=%s, owner=%s)%n\", taskId, status, owner);\r\n }\r\n try {\r\n var task = load(taskId);\r\n if (status != null) {\r\n if (!Set.of(\"pending\", \"in_progress\", \"completed\").contains(status))\r\n return \"Error: Invalid status: \" + status;\r\n task.put(\"status\", status);\r\n }\r\n if (owner != null) task.put(\"owner\", owner);\r\n task.put(\"updated_at\", System.currentTimeMillis() / 1000.0);\r\n save(task);\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"✅ worktree任务 #%d 已更新%n\", taskId);\r\n }\r\n return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(task);\r\n } catch (IOException e) {\r\n log.warn(\"worktree任务更新失败: taskId={}, error={}\", taskId, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n /**\r\n * 绑定worktree到任务(如果任务是pending则自动变为in_progress)\r\n */\r\n public String bindWorktree(int taskId, String worktree, String owner) {\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"🔗 绑定worktree: #%d → %s (owner=%s)%n\", taskId, worktree, owner);\r\n }\r\n try {\r\n var task = load(taskId);\r\n task.put(\"worktree\", worktree);\r\n if (owner != null && !owner.isEmpty()) task.put(\"owner\", owner);\r\n if (\"pending\".equals(task.get(\"status\"))) task.put(\"status\", \"in_progress\");\r\n task.put(\"updated_at\", System.currentTimeMillis() / 1000.0);\r\n save(task);\r\n return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(task);\r\n } catch (IOException e) {\r\n log.warn(\"绑定worktree失败: taskId={}, worktree={}, error={}\", taskId, worktree, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n public String unbindWorktree(int taskId) {\r\n if (log.isDebugEnabled()) {\r\n System.out.printf(\"🔗 解绑worktree: #%d%n\", taskId);\r\n }\r\n try {\r\n var task = load(taskId);\r\n task.put(\"worktree\", \"\");\r\n task.put(\"updated_at\", System.currentTimeMillis() / 1000.0);\r\n save(task);\r\n return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(task);\r\n } catch (IOException e) {\r\n log.warn(\"解绑worktree失败: taskId={}, error={}\", taskId, e.getMessage());\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public String listAll() {\r\n try (var files = Files.list(tasksDir)) {\r\n List> tasks = new ArrayList<>();\r\n files.filter(f -> f.getFileName().toString().matches(\"task_\\\\d+\\\\.json\"))\r\n .sorted()\r\n .forEach(f -> {\r\n try { tasks.add(mapper.readValue(f.toFile(), Map.class)); }\r\n catch (IOException e) { /* skip */ }\r\n });\r\n if (tasks.isEmpty()) return \"No tasks.\";\r\n StringBuilder sb = new StringBuilder();\r\n for (var t : tasks) {\r\n String status = (String) t.get(\"status\");\r\n String marker = switch (status) {\r\n case \"pending\" -> \"[ ]\";\r\n case \"in_progress\" -> \"[>]\";\r\n case \"completed\" -> \"[x]\";\r\n default -> \"[?]\";\r\n };\r\n String owner = t.get(\"owner\") != null && !t.get(\"owner\").toString().isEmpty()\r\n ? \" owner=\" + t.get(\"owner\") : \"\";\r\n String wt = t.get(\"worktree\") != null && !t.get(\"worktree\").toString().isEmpty()\r\n ? \" wt=\" + t.get(\"worktree\") : \"\";\r\n sb.append(String.format(\"%s #%s: %s%s%s%n\",\r\n marker, t.get(\"id\"), t.get(\"subject\"), owner, wt));\r\n }\r\n return sb.toString().trim();\r\n } catch (IOException e) {\r\n return \"Error: \" + e.getMessage();\r\n }\r\n }\r\n}\r\n" }, { "id": "full", @@ -1676,7 +1676,7 @@ "newTools": [ "task" ], - "locDelta": -28 + "locDelta": -24 }, { "from": "s04", @@ -1696,7 +1696,7 @@ "newTools": [ "loadSkill" ], - "locDelta": 38 + "locDelta": 34 }, { "from": "s05", @@ -1747,7 +1747,7 @@ "taskUpdate", "taskList" ], - "locDelta": 80 + "locDelta": 98 }, { "from": "s07", @@ -1769,7 +1769,7 @@ "backgroundRun", "checkBackground" ], - "locDelta": -77 + "locDelta": -87 }, { "from": "s08", @@ -1806,7 +1806,7 @@ "sendMessage", "readInbox" ], - "locDelta": 97 + "locDelta": 103 }, { "from": "s09", @@ -1836,7 +1836,7 @@ "shutdownResponse", "planApproval" ], - "locDelta": 35 + "locDelta": 23 }, { "from": "s10", @@ -1864,7 +1864,7 @@ "main", "main" ], - "locDelta": 7 + "locDelta": 5 }, { "from": "s11", @@ -1930,7 +1930,7 @@ "worktreeKeep", "worktreeEvents" ], - "locDelta": 324 + "locDelta": 354 } ] } \ No newline at end of file