Skip to content

Commit 5f2b0f2

Browse files
committed
Simplify task graph to single-source blockedBy (PR #127)
Remove unused blocks field and add_blocks parameter. The LLM never used add_blocks in practice, making it dead code that taught a misleading bidirectional pattern. Replace with remove_blocked_by for dependency rewiring. Single-source-of-truth with blockedBy only.
1 parent 5c7b873 commit 5f2b0f2

5 files changed

Lines changed: 38 additions & 45 deletions

File tree

agents/s07_task_system.py

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
s07_task_system.py - Tasks
55
66
Tasks persist as JSON files in .tasks/ so they survive context compression.
7-
Each task has a dependency graph (blockedBy/blocks).
7+
Each task has a dependency graph (blockedBy).
88
99
.tasks/
1010
task_1.json {"id":1, "subject":"...", "status":"completed", ...}
1111
task_2.json {"id":2, "blockedBy":[1], "status":"pending", ...}
12-
task_3.json {"id":3, "blockedBy":[2], "blocks":[], ...}
12+
task_3.json {"id":3, "blockedBy":[2], ...}
1313
1414
Dependency resolution:
1515
+----------+ +----------+ +----------+
@@ -67,7 +67,7 @@ def _save(self, task: dict):
6767
def create(self, subject: str, description: str = "") -> str:
6868
task = {
6969
"id": self._next_id, "subject": subject, "description": description,
70-
"status": "pending", "blockedBy": [], "blocks": [], "owner": "",
70+
"status": "pending", "blockedBy": [], "owner": "",
7171
}
7272
self._save(task)
7373
self._next_id += 1
@@ -77,37 +77,18 @@ def get(self, task_id: int) -> str:
7777
return json.dumps(self._load(task_id), indent=2, ensure_ascii=False)
7878

7979
def update(self, task_id: int, status: str = None,
80-
add_blocked_by: list = None, add_blocks: list = None) -> str:
80+
add_blocked_by: list = None, remove_blocked_by: list = None) -> str:
8181
task = self._load(task_id)
8282
if status:
8383
if status not in ("pending", "in_progress", "completed"):
8484
raise ValueError(f"Invalid status: {status}")
8585
task["status"] = status
86-
# When a task is completed, remove it from all other tasks' blockedBy
8786
if status == "completed":
8887
self._clear_dependency(task_id)
8988
if add_blocked_by:
9089
task["blockedBy"] = list(set(task["blockedBy"] + add_blocked_by))
91-
# Bidirectional: also update the blocker tasks' blocks lists
92-
for blocker_id in add_blocked_by:
93-
try:
94-
blocker = self._load(blocker_id)
95-
if task_id not in blocker["blocks"]:
96-
blocker["blocks"].append(task_id)
97-
self._save(blocker)
98-
except ValueError:
99-
pass
100-
if add_blocks:
101-
task["blocks"] = list(set(task["blocks"] + add_blocks))
102-
# Bidirectional: also update the blocked tasks' blockedBy lists
103-
for blocked_id in add_blocks:
104-
try:
105-
blocked = self._load(blocked_id)
106-
if task_id not in blocked["blockedBy"]:
107-
blocked["blockedBy"].append(task_id)
108-
self._save(blocked)
109-
except ValueError:
110-
pass
90+
if remove_blocked_by:
91+
task["blockedBy"] = [x for x in task["blockedBy"] if x not in remove_blocked_by]
11192
self._save(task)
11293
return json.dumps(task, indent=2, ensure_ascii=False)
11394

@@ -195,7 +176,7 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
195176
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
196177
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
197178
"task_create": lambda **kw: TASKS.create(kw["subject"], kw.get("description", "")),
198-
"task_update": lambda **kw: TASKS.update(kw["task_id"], kw.get("status"), kw.get("addBlockedBy"), kw.get("addBlocks")),
179+
"task_update": lambda **kw: TASKS.update(kw["task_id"], kw.get("status"), kw.get("addBlockedBy"), kw.get("removeBlockedBy")),
199180
"task_list": lambda **kw: TASKS.list_all(),
200181
"task_get": lambda **kw: TASKS.get(kw["task_id"]),
201182
}
@@ -212,7 +193,7 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
212193
{"name": "task_create", "description": "Create a new task.",
213194
"input_schema": {"type": "object", "properties": {"subject": {"type": "string"}, "description": {"type": "string"}}, "required": ["subject"]}},
214195
{"name": "task_update", "description": "Update a task's status or dependencies.",
215-
"input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]}, "addBlockedBy": {"type": "array", "items": {"type": "integer"}}, "addBlocks": {"type": "array", "items": {"type": "integer"}}}, "required": ["task_id"]}},
196+
"input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]}, "addBlockedBy": {"type": "array", "items": {"type": "integer"}}, "removeBlockedBy": {"type": "array", "items": {"type": "integer"}}}, "required": ["task_id"]}},
216197
{"name": "task_list", "description": "List all tasks with status summary.",
217198
"input_schema": {"type": "object", "properties": {}}},
218199
{"name": "task_get", "description": "Get full details of a task by ID.",

agents/s_full.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -277,15 +277,15 @@ def _save(self, task: dict):
277277

278278
def create(self, subject: str, description: str = "") -> str:
279279
task = {"id": self._next_id(), "subject": subject, "description": description,
280-
"status": "pending", "owner": None, "blockedBy": [], "blocks": []}
280+
"status": "pending", "owner": None, "blockedBy": []}
281281
self._save(task)
282282
return json.dumps(task, indent=2)
283283

284284
def get(self, tid: int) -> str:
285285
return json.dumps(self._load(tid), indent=2)
286286

287287
def update(self, tid: int, status: str = None,
288-
add_blocked_by: list = None, add_blocks: list = None) -> str:
288+
add_blocked_by: list = None, remove_blocked_by: list = None) -> str:
289289
task = self._load(tid)
290290
if status:
291291
task["status"] = status
@@ -300,8 +300,8 @@ def update(self, tid: int, status: str = None,
300300
return f"Task {tid} deleted"
301301
if add_blocked_by:
302302
task["blockedBy"] = list(set(task["blockedBy"] + add_blocked_by))
303-
if add_blocks:
304-
task["blocks"] = list(set(task["blocks"] + add_blocks))
303+
if remove_blocked_by:
304+
task["blockedBy"] = [x for x in task["blockedBy"] if x not in remove_blocked_by]
305305
self._save(task)
306306
return json.dumps(task, indent=2)
307307

@@ -587,7 +587,7 @@ def handle_plan_review(request_id: str, approve: bool, feedback: str = "") -> st
587587
"check_background": lambda **kw: BG.check(kw.get("task_id")),
588588
"task_create": lambda **kw: TASK_MGR.create(kw["subject"], kw.get("description", "")),
589589
"task_get": lambda **kw: TASK_MGR.get(kw["task_id"]),
590-
"task_update": lambda **kw: TASK_MGR.update(kw["task_id"], kw.get("status"), kw.get("add_blocked_by"), kw.get("add_blocks")),
590+
"task_update": lambda **kw: TASK_MGR.update(kw["task_id"], kw.get("status"), kw.get("add_blocked_by"), kw.get("remove_blocked_by")),
591591
"task_list": lambda **kw: TASK_MGR.list_all(),
592592
"spawn_teammate": lambda **kw: TEAM.spawn(kw["name"], kw["role"], kw["prompt"]),
593593
"list_teammates": lambda **kw: TEAM.list_all(),
@@ -626,7 +626,7 @@ def handle_plan_review(request_id: str, approve: bool, feedback: str = "") -> st
626626
{"name": "task_get", "description": "Get task details by ID.",
627627
"input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}}, "required": ["task_id"]}},
628628
{"name": "task_update", "description": "Update task status or dependencies.",
629-
"input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed", "deleted"]}, "add_blocked_by": {"type": "array", "items": {"type": "integer"}}, "add_blocks": {"type": "array", "items": {"type": "integer"}}}, "required": ["task_id"]}},
629+
"input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed", "deleted"]}, "add_blocked_by": {"type": "array", "items": {"type": "integer"}}, "remove_blocked_by": {"type": "array", "items": {"type": "integer"}}}, "required": ["task_id"]}},
630630
{"name": "task_list", "description": "List all tasks.",
631631
"input_schema": {"type": "object", "properties": {}}},
632632
{"name": "spawn_teammate", "description": "Spawn a persistent autonomous teammate.",

docs/en/s07-task-system.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Without explicit relationships, the agent can't tell what's ready, what's blocke
1414

1515
## Solution
1616

17-
Promote the checklist into a **task graph** persisted to disk. Each task is a JSON file with status, dependencies (`blockedBy`), and dependents (`blocks`). The graph answers three questions at any moment:
17+
Promote the checklist into a **task graph** persisted to disk. Each task is a JSON file with status, dependencies (`blockedBy`). The graph answers three questions at any moment:
1818

1919
- **What's ready?** -- tasks with `pending` status and empty `blockedBy`.
2020
- **What's blocked?** -- tasks waiting on unfinished dependencies.
@@ -60,7 +60,7 @@ class TaskManager:
6060
def create(self, subject, description=""):
6161
task = {"id": self._next_id, "subject": subject,
6262
"status": "pending", "blockedBy": [],
63-
"blocks": [], "owner": ""}
63+
"owner": ""}
6464
self._save(task)
6565
self._next_id += 1
6666
return json.dumps(task, indent=2)
@@ -81,12 +81,16 @@ def _clear_dependency(self, completed_id):
8181

8282
```python
8383
def update(self, task_id, status=None,
84-
add_blocked_by=None, add_blocks=None):
84+
add_blocked_by=None, remove_blocked_by=None):
8585
task = self._load(task_id)
8686
if status:
8787
task["status"] = status
8888
if status == "completed":
8989
self._clear_dependency(task_id)
90+
if add_blocked_by:
91+
task["blockedBy"] = list(set(task["blockedBy"] + add_blocked_by))
92+
if remove_blocked_by:
93+
task["blockedBy"] = [x for x in task["blockedBy"] if x not in remove_blocked_by]
9094
self._save(task)
9195
```
9296

@@ -110,7 +114,7 @@ From s07 onward, the task graph is the default for multi-step work. s03's Todo r
110114
|---|---|---|
111115
| Tools | 5 | 8 (`task_create/update/list/get`) |
112116
| Planning model | Flat checklist (in-memory) | Task graph with dependencies (on disk) |
113-
| Relationships | None | `blockedBy` + `blocks` edges |
117+
| Relationships | None | `blockedBy` edges |
114118
| Status tracking | Done or not | `pending` -> `in_progress` -> `completed` |
115119
| Persistence | Lost on compression | Survives compression and restarts |
116120

docs/ja/s07-task-system.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ s03のTodoManagerはメモリ上のフラットなチェックリストに過ぎ
1414

1515
## 解決策
1616

17-
フラットなチェックリストをディスクに永続化する**タスクグラフ**に昇格させる。各タスクは1つのJSONファイルで、ステータス・前方依存(`blockedBy`)・後方依存(`blocks`)を持つ。タスクグラフは常に3つの問いに答える:
17+
フラットなチェックリストをディスクに永続化する**タスクグラフ**に昇格させる。各タスクは1つのJSONファイルで、ステータス・前方依存(`blockedBy`)を持つ。タスクグラフは常に3つの問いに答える:
1818

1919
- **何が実行可能か?** -- `pending`ステータスで`blockedBy`が空のタスク。
2020
- **何がブロックされているか?** -- 未完了の依存を待つタスク。
@@ -60,7 +60,7 @@ class TaskManager:
6060
def create(self, subject, description=""):
6161
task = {"id": self._next_id, "subject": subject,
6262
"status": "pending", "blockedBy": [],
63-
"blocks": [], "owner": ""}
63+
"owner": ""}
6464
self._save(task)
6565
self._next_id += 1
6666
return json.dumps(task, indent=2)
@@ -81,12 +81,16 @@ def _clear_dependency(self, completed_id):
8181

8282
```python
8383
def update(self, task_id, status=None,
84-
add_blocked_by=None, add_blocks=None):
84+
add_blocked_by=None, remove_blocked_by=None):
8585
task = self._load(task_id)
8686
if status:
8787
task["status"] = status
8888
if status == "completed":
8989
self._clear_dependency(task_id)
90+
if add_blocked_by:
91+
task["blockedBy"] = list(set(task["blockedBy"] + add_blocked_by))
92+
if remove_blocked_by:
93+
task["blockedBy"] = [x for x in task["blockedBy"] if x not in remove_blocked_by]
9094
self._save(task)
9195
```
9296

@@ -110,7 +114,7 @@ s07以降、タスクグラフがマルチステップ作業のデフォルト
110114
|---|---|---|
111115
| Tools | 5 | 8 (`task_create/update/list/get`) |
112116
| 計画モデル | フラットチェックリスト (メモリ) | 依存関係付きタスクグラフ (ディスク) |
113-
| 関係 | なし | `blockedBy` + `blocks` エッジ |
117+
| 関係 | なし | `blockedBy` エッジ |
114118
| ステータス追跡 | 完了か未完了 | `pending` -> `in_progress` -> `completed` |
115119
| 永続性 | 圧縮で消失 | 圧縮・再起動後も存続 |
116120

docs/zh/s07-task-system.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ s03 的 TodoManager 只是内存中的扁平清单: 没有顺序、没有依赖
1414

1515
## 解决方案
1616

17-
把扁平清单升级为持久化到磁盘的**任务图**。每个任务是一个 JSON 文件, 有状态、前置依赖 (`blockedBy`) 和后置依赖 (`blocks`)。任务图随时回答三个问题:
17+
把扁平清单升级为持久化到磁盘的**任务图**。每个任务是一个 JSON 文件, 有状态、前置依赖 (`blockedBy`)。任务图随时回答三个问题:
1818

1919
- **什么可以做?** -- 状态为 `pending``blockedBy` 为空的任务。
2020
- **什么被卡住?** -- 等待前置任务完成的任务。
@@ -60,7 +60,7 @@ class TaskManager:
6060
def create(self, subject, description=""):
6161
task = {"id": self._next_id, "subject": subject,
6262
"status": "pending", "blockedBy": [],
63-
"blocks": [], "owner": ""}
63+
"owner": ""}
6464
self._save(task)
6565
self._next_id += 1
6666
return json.dumps(task, indent=2)
@@ -81,12 +81,16 @@ def _clear_dependency(self, completed_id):
8181

8282
```python
8383
def update(self, task_id, status=None,
84-
add_blocked_by=None, add_blocks=None):
84+
add_blocked_by=None, remove_blocked_by=None):
8585
task = self._load(task_id)
8686
if status:
8787
task["status"] = status
8888
if status == "completed":
8989
self._clear_dependency(task_id)
90+
if add_blocked_by:
91+
task["blockedBy"] = list(set(task["blockedBy"] + add_blocked_by))
92+
if remove_blocked_by:
93+
task["blockedBy"] = [x for x in task["blockedBy"] if x not in remove_blocked_by]
9094
self._save(task)
9195
```
9296

@@ -110,7 +114,7 @@ TOOL_HANDLERS = {
110114
|---|---|---|
111115
| Tools | 5 | 8 (`task_create/update/list/get`) |
112116
| 规划模型 | 扁平清单 (仅内存) | 带依赖关系的任务图 (磁盘) |
113-
| 关系 || `blockedBy` + `blocks` |
117+
| 关系 || `blockedBy`|
114118
| 状态追踪 | 做完没做完 | `pending` -> `in_progress` -> `completed` |
115119
| 持久化 | 压缩后丢失 | 压缩和重启后存活 |
116120

0 commit comments

Comments
 (0)