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