AI Agent Learning
L07

任务系统

规划与协调

任务图 + 依赖关系

599 LOC8 个工具TaskManager with file-based state
基于文件的任务图,支持排序、并行和依赖关系

L00 > L01 > L02 > L03 > L04 > L05 > L06 | [ L07 ] L08 > L09 > L10 > L11 > L12 > L13

"状态在对话之外持久化 -- 因为它在文件系统里。" -- 任务图不随上下文压缩而消失。

问题

上下文压缩(L06)会把对话历史截断,这意味着智能体会忘掉之前计划好的工作。TodoWrite(L03)把任务列表放在对话内存里,压缩一次就可能丢失。我们需要一种持久化任务追踪方式,让任务之间还能声明依赖关系。

解决方案

+------------------+
|   .tasks/        |    文件系统 (持久化)
|   task_1.json    |
|   task_2.json    |
|   task_3.json    |
+--------+---------+
         |
         v
+--------+---------+
|   TaskManager    |    CRUD + 依赖图
|                  |
|  create()        |
|  get()           |
|  update()        |    status: pending -> in_progress -> completed
|  listAll()       |
|  clearDependency()|   完成时自动解锁
+--------+---------+
         |
         v
+--------+---------+
|   Agent Loop     |    通过 tool call 操作任务
|  task_create     |
|  task_update     |
|  task_get        |
|  task_list       |
+------------------+

DAG 依赖图

task_1 (Setup DB)
   |
   | blocks
   v
task_2 (Write API)  --- blockedBy: [1] --->  task_2 不可执行
   |                                         直到 task_1 completed
   | blocks
   v
task_3 (Write Tests) --- blockedBy: [2] --->  task_3 不可执行
                                              直到 task_2 completed

当 task_1 标记为 completed:
  -> clearDependency(1) 遍历所有任务
  -> 把 task_1 从 task_2.blockedBy 中移除
  -> task_2 变为可执行 (blockedBy=[])

工作原理

1. 任务以 JSON 文件持久化

每个任务是一个独立的 JSON 文件,存放在 .tasks/ 目录下:

.tasks/
  task_1.json   ->  {"id":1,"subject":"Setup DB","status":"pending","blockedBy":[],"blocks":[2]}
  task_2.json   ->  {"id":2,"subject":"Write API","status":"pending","blockedBy":[1],"blocks":[3]}
  task_3.json   ->  {"id":3,"subject":"Write Tests","status":"pending","blockedBy":[2],"blocks":[]}

2. TaskManager -- CRUD 与依赖图管理

TaskManager 是一个内部类,负责所有任务操作:

static class TaskManager {
    private final Path dir;
    private int nextId;

    public TaskManager(Path tasksDir) {
        this.dir = tasksDir;
        try {
            Files.createDirectories(dir);
        } catch (Exception e) {
            throw new RuntimeException("Failed to create tasks directory", e);
        }
        this.nextId = maxId() + 1;
    }

    public String create(String subject, String description) {
        Map<String, Object> task = new HashMap<>();
        task.put("id", nextId);
        task.put("subject", subject);
        task.put("description", description != null ? description : "");
        task.put("status", "pending");
        task.put("blockedBy", new ArrayList<>());
        task.put("blocks", new ArrayList<>());
        task.put("owner", "");

        save(task);
        nextId++;
        return mapToJson(task);
    }
}

nextId 在构造时扫描现有文件计算最大 ID,保证新任务不会和已有任务冲突。

3. update() -- 状态流转与依赖解锁

状态只允许三种值:pendingin_progresscompleted。当任务标记为 completed 时,自动调用 clearDependency() 解除对其他任务的阻塞:

public String update(int taskId, String status,
                     List<Integer> addBlockedBy, List<Integer> addBlocks) {
    Map<String, Object> task = load(taskId);

    if (status != null) {
        if (!status.equals("pending") && !status.equals("in_progress")
                && !status.equals("completed")) {
            throw new IllegalArgumentException("Invalid status: " + status);
        }
        task.put("status", status);

        // 完成时自动解锁所有被本任务阻塞的任务
        if (status.equals("completed")) {
            clearDependency(taskId);
        }
    }

    // 支持在更新时添加双向依赖
    if (addBlocks != null) {
        @SuppressWarnings("unchecked")
        List<Integer> blocks = new ArrayList<>((List<Integer>) task.get("blocks"));
        for (Integer blockedId : addBlocks) {
            if (!blocks.contains(blockedId)) {
                blocks.add(blockedId);
            }
            // 双向更新:同时修改被阻塞任务的 blockedBy 列表
            try {
                Map<String, Object> blocked = load(blockedId);
                @SuppressWarnings("unchecked")
                List<Integer> blockedBy = new ArrayList<>(
                        (List<Integer>) blocked.get("blockedBy"));
                if (!blockedBy.contains(taskId)) {
                    blockedBy.add(taskId);
                    blocked.put("blockedBy", blockedBy);
                    save(blocked);
                }
            } catch (Exception ignored) {}
        }
        task.put("blocks", blocks);
    }

    save(task);
    return mapToJson(task);
}

4. clearDependency() -- 完成后自动解锁

当一个任务完成时,遍历所有任务文件,把已完成任务的 ID 从其他任务的 blockedBy 列表中移除:

private void clearDependency(int completedId) {
    try {
        Files.list(dir)
                .filter(p -> p.getFileName().toString().matches("task_\\d+\\.json"))
                .forEach(p -> {
                    try {
                        Map<String, Object> t = parseJsonToMap(
                                new String(Files.readAllBytes(p), StandardCharsets.UTF_8));
                        @SuppressWarnings("unchecked")
                        List<Integer> blockedBy = new ArrayList<>(
                                (List<Integer>) t.get("blockedBy"));
                        if (blockedBy.contains(completedId)) {
                            blockedBy.remove(Integer.valueOf(completedId));
                            t.put("blockedBy", blockedBy);
                            save(t);
                        }
                    } catch (Exception ignored) {}
                });
    } catch (Exception ignored) {}
}

5. listAll() -- 带状态标记的列表

public String listAll() {
    // ...
    for (Map<String, Object> t : tasks) {
        String marker = switch ((String) t.get("status")) {
            case "pending"     -> "[ ]";
            case "in_progress" -> "[>]";
            case "completed"   -> "[x]";
            default            -> "[?]";
        };
        @SuppressWarnings("unchecked")
        List<Integer> blockedBy = (List<Integer>) t.get("blockedBy");
        String blocked = blockedBy != null && !blockedBy.isEmpty()
                ? " (blocked by: " + blockedBy + ")" : "";
        sb.append(marker).append(" #").append(t.get("id")).append(": ")
                .append(t.get("subject")).append(blocked).append("\n");
    }
    return sb.toString().trim();
}

输出示例:

[x] #1: Setup DB
[>] #2: Write API
[ ] #3: Write Tests (blocked by: [2])

6. 工具注册 -- Task 工具加入调度表

private Map<String, ToolHandler> createHandlers() {
    Map<String, ToolHandler> handlers = new HashMap<>();
    // 基础工具...
    handlers.put("task_create", args ->
            taskManager.create((String) args.get("subject"),
                    (String) args.get("description")));
    handlers.put("task_update", args ->
            taskManager.update(
                    ((Number) args.get("task_id")).intValue(),
                    (String) args.get("status"),
                    (List<Integer>) args.get("addBlockedBy"),
                    (List<Integer>) args.get("addBlocks")));
    handlers.put("task_list", args -> taskManager.listAll());
    handlers.put("task_get", args ->
            taskManager.get(((Number) args.get("task_id")).intValue()));
    return handlers;
}

变更一览

组件之前 (L06)之后 (L07)
任务持久化无 (内存中 TodoWrite).tasks/task_N.json 文件持久化
依赖管理blockedBy / blocks 双向链表
完成解锁clearDependency() 自动遍历解锁
状态模型pending -> in_progress -> completed
新工具task_create, task_update, task_get, task_list
压缩安全丢失任务存文件系统,压缩不影响

试一试

cd ai-agent-learning
mvn spring-boot:run -pl openai -Dspring-boot.run.arguments="--lesson=lesson7 --prompt='Plan a REST API project with 3 tasks: setup, implementation, testing. Make them depend on each other.'"

运行后查看 .tasks/ 目录:

ls .tasks/
cat .tasks/task_1.json

源码: Lesson7RunSimple.java