Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 26 additions & 11 deletions s10_task_system/README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ TodoWrite は、こうした依存関係や担当を記録しない。「API を

![Task System Overview](images/task-system-overview.ja.svg)

コードは S04 の 5 つの基本ツール、Permission、Hooks、共通の `execute_tool` を保ち、そこへ 5 つのタスクツール、`.tasks/` ディレクトリへの永続化、`blockedBy` の依存チェックを追加する。
コードは S04 の 5 つの基本ツール、Permission、Hooks、共通の `execute_tool` を保ち、そこへ 6 つのタスクツール、`.tasks/` ディレクトリへの永続化、`blockedBy` の依存チェックを追加する。

TodoWrite vs Task System:

Expand Down Expand Up @@ -69,12 +69,22 @@ ID は `task_` と 8 桁のランダムな 16 進文字で生成する。ファ
### create_task: タスク作成

```python
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
return TASKS.create(subject, description, blockedBy)
def create_task(subject: str, description: str = "") -> Task:
return TASKS.create(subject, description)
```

`TaskStore.create` は subject と依存 ID を確認し、`.tasks/{id}.json` に書き込む。`blockedBy` で依存を宣言し、例えば「API を書く」タスクはデータベースタスクの ID を参照できる。
`TaskStore.create` は subject を確認し、ランダム ID を割り当てて `.tasks/{id}.json` に書き込む。新しいタスクの `blockedBy` は常に空で、ツール結果が実行時に生成された ID をモデルへ返す。

### update_task: 返された ID で依存を追加

```python
def update_task(task_id: str, addBlockedBy: list[str]) -> Task:
return TASKS.update_dependencies(task_id, addBlockedBy)
```

タスクグラフは 2 段階で構築する。まず全ノードを作成し、その後 `create_task` が返した ID を使って `update_task` で辺を追加する。モデルが 1 回の応答で複数のツール呼び出しを出す場合、同じ階層の呼び出しはツール結果が返る前にすべて確定するため、ある `create_task` は別の呼び出しで生成されたばかりの ID を利用できない。

`update_task` は変更全体を検証してから保存する。対象と依存タスクは存在し、対象は pending かつ未所有でなければならず、自己依存や循環も禁止する。既存の辺を再度追加しても重複しない。

### can_start: 依存チェック

Expand Down Expand Up @@ -159,11 +169,16 @@ pending ──claim──→ in_progress ──complete──→ completed
### 組み合わせて実行

```python
# 依存関係のあるタスクを作成
# 第 1 段階:全ノードを作成して実行時 ID を受け取る
schema = create_task("setup database schema")
endpoints = create_task("create API endpoints", blockedBy=[schema.id])
tests = create_task("write tests", blockedBy=[endpoints.id])
docs = create_task("write docs", blockedBy=[schema.id])
endpoints = create_task("create API endpoints")
tests = create_task("write tests")
docs = create_task("write docs")

# 第 2 段階:返された ID で依存の辺を追加する
update_task(endpoints.id, addBlockedBy=[schema.id])
update_task(tests.id, addBlockedBy=[endpoints.id])
update_task(docs.id, addBlockedBy=[schema.id])

# Agent が最初に実行可能なタスクを引き受ける
claim_task(schema.id) # ✓ Claimed(依存なし)
Expand All @@ -179,7 +194,7 @@ claim_task(tests.id) # ✓ Claimed(endpoints 完了済み)
complete_task(tests.id) # ✓ Completed
```

各 `create_task` が JSON ファイルを書き込み、各 `claim_task` / `complete_task` がファイルを更新。セッションをまたいでも `.tasks/` ディレクトリが残り、Agent はファイルを読んで進捗を復旧
各 `create_task` が JSON ファイルを書き込み、`update_task`、`claim_task``complete_task` がファイルを更新する。セッションをまたいでも `.tasks/` ディレクトリが残り、Agent はファイルを読んで進捗を復旧できる

---

Expand Down Expand Up @@ -208,4 +223,4 @@ python s10_task_system/code.py
s11 Background Tasks → 遅い操作をバックグラウンドで実行する。Agent は他のタスクの処理を続け、バックグラウンド処理の完了後に通知を受け取る。


<!-- translation-sync: zh@v4, en@v4, ja@v4 -->
<!-- translation-sync: zh@v5, en@v5, ja@v5 -->
37 changes: 26 additions & 11 deletions s10_task_system/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This chapter adds a Task System. Each task has its own ID and status; `blockedBy

![Task System Overview](images/task-system-overview.en.svg)

The code keeps S04's five base tools, Permission, Hooks, and shared `execute_tool`, then adds 5 task tools, persistence in the `.tasks/` directory, and `blockedBy` dependency checks.
The code keeps S04's five base tools, Permission, Hooks, and shared `execute_tool`, then adds 6 task tools, persistence in the `.tasks/` directory, and `blockedBy` dependency checks.

TodoWrite vs Task System:

Expand Down Expand Up @@ -69,12 +69,22 @@ IDs use the `task_` prefix followed by 8 random hexadecimal characters. Files ar
### create_task: Create Tasks

```python
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
return TASKS.create(subject, description, blockedBy)
def create_task(subject: str, description: str = "") -> Task:
return TASKS.create(subject, description)
```

`TaskStore.create` checks the subject and dependency IDs, then writes `.tasks/{id}.json`. `blockedBy` declares dependencies; for example, "write API" can reference the database task's ID.
`TaskStore.create` checks the subject, allocates a random ID, and writes `.tasks/{id}.json`. A new task always starts with an empty `blockedBy` list. The tool result returns the runtime-generated ID to the model.

### update_task: Add Dependencies with Returned IDs

```python
def update_task(task_id: str, addBlockedBy: list[str]) -> Task:
return TASKS.update_dependencies(task_id, addBlockedBy)
```

Task graph construction uses two phases: create every node first, then call `update_task` with the IDs returned by `create_task` to add edges. This matters when the model emits several tool calls in one response: sibling calls are formed before any tool result exists, so one `create_task` call cannot consume another call's newly generated ID.

`update_task` validates the entire change before saving it. The target and dependencies must exist, the target must still be pending and unowned, and the new edges must not introduce self-dependencies or cycles. Repeating an existing edge is safe and does not duplicate it.

### can_start: Dependency Check

Expand Down Expand Up @@ -159,11 +169,16 @@ Here `claim` / `complete` are actions, while `pending` / `in_progress` / `comple
### Putting It Together

```python
# Create tasks with dependencies
# Phase 1: create every node and receive its runtime ID
schema = create_task("setup database schema")
endpoints = create_task("create API endpoints", blockedBy=[schema.id])
tests = create_task("write tests", blockedBy=[endpoints.id])
docs = create_task("write docs", blockedBy=[schema.id])
endpoints = create_task("create API endpoints")
tests = create_task("write tests")
docs = create_task("write docs")

# Phase 2: add edges using those returned IDs
update_task(endpoints.id, addBlockedBy=[schema.id])
update_task(tests.id, addBlockedBy=[endpoints.id])
update_task(docs.id, addBlockedBy=[schema.id])

# Agent claims the first available task
claim_task(schema.id) # ✓ Claimed (no dependencies)
Expand All @@ -179,7 +194,7 @@ claim_task(tests.id) # ✓ Claimed (endpoints completed)
complete_task(tests.id) # ✓ Completed
```

Each `create_task` writes a JSON file, each `claim_task` / `complete_task` updates the file. Across sessions, the `.tasks/` directory persists — the agent reads the files to recover progress.
Each `create_task` writes a JSON file; `update_task`, `claim_task`, and `complete_task` update it. Across sessions, the `.tasks/` directory persists — the agent reads the files to recover progress.

---

Expand Down Expand Up @@ -208,4 +223,4 @@ The task graph is in place, but full test suites, dependency installation, and d
s11 Background Tasks → Slow operations run in the background. The Agent Loop can continue processing other tasks and receives a notification when the background work finishes.


<!-- translation-sync: zh@v4, en@v4, ja@v4 -->
<!-- translation-sync: zh@v5, en@v5, ja@v5 -->
37 changes: 26 additions & 11 deletions s10_task_system/README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ TodoWrite 没有记录这些依赖和分工。它可以显示“编写 API”仍

![Task System Overview](images/task-system-overview.svg)

代码保留 S04 的五个基础工具、Permission、Hooks 和统一 `execute_tool`,再加入 5 个任务工具、`.tasks/` 目录持久化和 `blockedBy` 依赖检查。
代码保留 S04 的五个基础工具、Permission、Hooks 和统一 `execute_tool`,再加入 6 个任务工具、`.tasks/` 目录持久化和 `blockedBy` 依赖检查。

TodoWrite vs Task System:

Expand Down Expand Up @@ -69,12 +69,22 @@ ID 使用 `task_` 加 8 位随机十六进制字符生成。创建文件时使
### create_task: 创建任务

```python
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
return TASKS.create(subject, description, blockedBy)
def create_task(subject: str, description: str = "") -> Task:
return TASKS.create(subject, description)
```

`TaskStore.create` 检查 subject 和依赖 ID,再把任务写入 `.tasks/{id}.json`。`blockedBy` 声明依赖,比如“写 API”的 `blockedBy` 可以指向数据库任务的 ID。
`TaskStore.create` 检查 subject,分配随机 ID,再把任务写入 `.tasks/{id}.json`。新任务的 `blockedBy` 固定为空,工具结果会把运行时生成的 ID 返回给模型。

### update_task: 使用返回的 ID 添加依赖

```python
def update_task(task_id: str, addBlockedBy: list[str]) -> Task:
return TASKS.update_dependencies(task_id, addBlockedBy)
```

任务图采用两阶段构建:先创建所有节点,再使用 `create_task` 返回的 ID 调用 `update_task` 添加边。模型可能在一条回复里同时发出多个工具调用,而这些同级调用在任何工具结果产生前就已经确定,因此某个 `create_task` 无法直接使用另一个调用刚生成的 ID。

`update_task` 会先校验整次修改,再统一保存。目标任务和依赖必须存在,目标必须仍为 pending 且无人认领,并且不能形成自依赖或环。重复添加已有依赖是安全的,不会产生重复边。

### can_start: 依赖检查

Expand Down Expand Up @@ -159,11 +169,16 @@ pending ──claim──→ in_progress ──complete──→ completed
### 合起来跑

```python
# 创建有依赖的任务
# 第一阶段:创建所有节点并取得运行时 ID
schema = create_task("setup database schema")
endpoints = create_task("create API endpoints", blockedBy=[schema.id])
tests = create_task("write tests", blockedBy=[endpoints.id])
docs = create_task("write docs", blockedBy=[schema.id])
endpoints = create_task("create API endpoints")
tests = create_task("write tests")
docs = create_task("write docs")

# 第二阶段:使用返回的 ID 建立依赖边
update_task(endpoints.id, addBlockedBy=[schema.id])
update_task(tests.id, addBlockedBy=[endpoints.id])
update_task(docs.id, addBlockedBy=[schema.id])

# Agent 认领第一个可做的任务
claim_task(schema.id) # ✓ Claimed (无依赖)
Expand All @@ -179,7 +194,7 @@ claim_task(tests.id) # ✓ Claimed (endpoints 已完成)
complete_task(tests.id) # ✓ Completed
```

每个 `create_task` 写一个 JSON 文件,每个 `claim_task` / `complete_task` 更新文件。跨会话时,`.tasks/` 目录还在,Agent 读文件就能恢复进度。
每个 `create_task` 写一个 JSON 文件,`update_task`、`claim_task` `complete_task` 更新文件。跨会话时,`.tasks/` 目录还在,Agent 读文件就能恢复进度。

---

Expand Down Expand Up @@ -208,4 +223,4 @@ python s10_task_system/code.py
s11 Background Tasks → 把慢操作放到后台。Agent 可以继续处理其他任务,后台执行完成后再接收通知。


<!-- translation-sync: zh@v4, en@v4, ja@v4 -->
<!-- translation-sync: zh@v5, en@v5, ja@v5 -->
96 changes: 73 additions & 23 deletions s10_task_system/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@

SYSTEM = (
f"You are a coding agent at {WORKDIR}. "
"Use task tools to track dependencies and progress."
"Use task tools to track dependencies and progress. Create all task nodes "
"first. After create_task returns runtime-generated IDs, use update_task "
"with those exact IDs to add dependencies."
)


Expand Down Expand Up @@ -97,17 +99,11 @@ def _path(self, task_id: str, create_root: bool = False) -> Path:
def exists(self, task_id: str) -> bool:
return self._path(task_id).is_file()

def create(self, subject: str, description: str = "",
blocked_by: list[str] | None = None) -> Task:
def create(self, subject: str, description: str = "") -> Task:
subject = subject.strip()
if not subject:
raise ValueError("Task subject cannot be empty")

dependencies = list(dict.fromkeys(blocked_by or []))
for dependency in dependencies:
if not self.exists(dependency):
raise ValueError(f"Dependency not found: {dependency}")

self._root(create=True)
for _ in range(100):
task = Task(
Expand All @@ -116,7 +112,7 @@ def create(self, subject: str, description: str = "",
description=description,
status="pending",
owner=None,
blockedBy=dependencies,
blockedBy=[],
)
try:
with self._path(task.id, create_root=True).open(
Expand All @@ -128,6 +124,52 @@ def create(self, subject: str, description: str = "",
continue
raise RuntimeError("Could not allocate a unique task ID")

def _depends_on(self, task_id: str, target_id: str) -> bool:
"""Return whether task_id transitively depends on target_id."""
pending = [task_id]
visited = set()
while pending:
current = pending.pop()
if current == target_id:
return True
if current in visited:
continue
visited.add(current)
pending.extend(self.load(current).blockedBy)
return False

def update_dependencies(self, task_id: str,
add_blocked_by: list[str]) -> Task:
if not isinstance(add_blocked_by, list):
raise ValueError("addBlockedBy must be a list of task IDs")

task = self.load(task_id)
if task.status != "pending" or task.owner is not None:
raise ValueError(
f"Task {task_id} dependencies can only be updated while "
"pending and unowned"
)

dependencies = list(dict.fromkeys(add_blocked_by))
for dependency in dependencies:
if dependency == task_id:
raise ValueError("Task cannot depend on itself")
if not self.exists(dependency):
raise ValueError(f"Dependency not found: {dependency}")
if dependency not in task.blockedBy and self._depends_on(
dependency, task_id
):
raise ValueError(
f"Dependency cycle detected: {task_id} -> {dependency}"
)

task.blockedBy.extend(
dependency for dependency in dependencies
if dependency not in task.blockedBy
)
self.save(task)
return task

def save(self, task: Task) -> None:
self._path(task.id, create_root=True).write_text(
json.dumps(asdict(task), indent=2),
Expand All @@ -154,9 +196,12 @@ def list(self) -> list[Task]:
TASKS = TaskStore(TASKS_DIR)


def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
return TASKS.create(subject, description, blockedBy)
def create_task(subject: str, description: str = "") -> Task:
return TASKS.create(subject, description)


def update_task(task_id: str, addBlockedBy: list[str]) -> Task:
return TASKS.update_dependencies(task_id, addBlockedBy)


def load_task(task_id: str) -> Task:
Expand Down Expand Up @@ -290,15 +335,17 @@ def run_glob(pattern: str) -> str:
return f"Error: {error}"


def run_create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> str:
task = create_task(subject, description, blockedBy)
dependencies = (
f" (blockedBy: {', '.join(task.blockedBy)})"
if task.blockedBy else ""
)
print(f" [create] {task.subject}{dependencies}")
return f"Created {task.id}: {task.subject}{dependencies}"
def run_create_task(subject: str, description: str = "") -> str:
task = create_task(subject, description)
print(f" [create] {task.subject}")
return f"Created {task.id}: {task.subject}"


def run_update_task(task_id: str, addBlockedBy: list[str]) -> str:
task = update_task(task_id, addBlockedBy)
dependencies = ", ".join(task.blockedBy) or "(none)"
print(f" [update] {task.subject} blockedBy: {dependencies}")
return f"Updated {task.id} blockedBy: {dependencies}"


def run_list_tasks() -> str:
Expand Down Expand Up @@ -347,8 +394,10 @@ def run_complete_task(task_id: str) -> str:
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.",
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
{"name": "create_task", "description": "Create a task with optional dependencies.",
"input_schema": {"type": "object", "properties": {"subject": {"type": "string"}, "description": {"type": "string"}, "blockedBy": {"type": "array", "items": {"type": "string"}}}, "required": ["subject"]}},
{"name": "create_task", "description": "Create a task and return its runtime-generated ID.",
"input_schema": {"type": "object", "properties": {"subject": {"type": "string"}, "description": {"type": "string"}}, "required": ["subject"], "additionalProperties": False}},
{"name": "update_task", "description": "Add dependencies using IDs returned by create_task.",
"input_schema": {"type": "object", "properties": {"task_id": {"type": "string", "pattern": "^task_[0-9a-f]{8}$"}, "addBlockedBy": {"type": "array", "items": {"type": "string", "pattern": "^task_[0-9a-f]{8}$"}, "minItems": 1}}, "required": ["task_id", "addBlockedBy"], "additionalProperties": False}},
{"name": "list_tasks", "description": "List tasks with status, owner, and dependencies.",
"input_schema": {"type": "object", "properties": {}}},
{"name": "get_task", "description": "Get a task by ID.",
Expand All @@ -366,6 +415,7 @@ def run_complete_task(task_id: str) -> str:
"edit_file": run_edit,
"glob": run_glob,
"create_task": run_create_task,
"update_task": run_update_task,
"list_tasks": run_list_tasks,
"get_task": run_get_task,
"claim_task": run_claim_task,
Expand Down
Loading