fix: build task dependencies in two phases

This commit is contained in:
Haoran
2026-08-19 01:35:08 +08:00
parent 10768e1b74
commit 711249e297
34 changed files with 1102 additions and 443 deletions

View File

@@ -47,6 +47,8 @@ s13 は s10 の基本ツール、Hooks、Permission、Task System を再利用
- **任意の worktree** は、必要なタスクだけを別の作業ディレクトリへ紐付ける。紐付けのないタスクは通常のリポジトリディレクトリを使う。
- **型付きプロトコルと計画ゲート** は shutdown と承認状態を明示し、必要な計画が承認されるまで変更系ツールを止める。
タスクグラフの作成は s10 の 2 段階契約を維持する。Lead はまず全ノードに `create_task` を呼び、返された実行時 ID で `update_task(addBlockedBy=...)` を実行してから ready task を割り当てる。`update_task` を使えるのは Lead だけであり、チームメイトは一覧・Claim・完了はできるが、チーム実行中にグラフ構造を変更できない。
s11 の background task と s12 の scheduled task は本章へ持ち込まない。どちらも teammate communication、task claim、plan approval には必要ない。
これらはすべて Team Harness レイヤーの一部である。タスク発見のために別の Agent Loop は要らず、worktree が別種の Agent を作るわけでもない。
@@ -447,4 +449,4 @@ Lead と teammate が呼び出せるのは、`code.py` に直接定義したツ
s14 MCP Tools → 共通の発見・呼び出しプロトコルで実行時に外部サービスへ接続し、そのツールを tool pool に追加する。
<!-- translation-sync: zh@v11, en@v11, ja@v11 -->
<!-- translation-sync: zh@v12, en@v12, ja@v12 -->

View File

@@ -47,6 +47,8 @@ s13 reuses s10's base tools, hooks, permission checks, and Task System, then add
- **Optional worktrees** bind a task to another working directory when the work needs it. Unbound tasks use the normal repository directory.
- **Typed protocols and a plan gate** make shutdown and approval state explicit and block mutating tools until a required plan is approved.
Task graph authoring keeps s10's two-phase contract. The Lead first calls `create_task` for every node, then uses the returned runtime IDs with `update_task(addBlockedBy=...)` before assigning ready work. Only the Lead receives `update_task`; teammates can list, claim, and complete tasks but cannot rewrite graph structure while the team is running.
s11 background tasks and s12 scheduled tasks are not carried into this chapter. Neither mechanism is required for teammate communication, task claiming, or plan approval.
These are all parts of the Team harness layer. Teammates do not need a separate loop for task discovery, and a worktree does not create a new kind of agent.
@@ -448,4 +450,4 @@ The Lead and its teammates can only call tools defined directly in `code.py`. Co
s14 MCP Tools → Connect external services at runtime through one discovery and invocation protocol, then add their tools to the tool pool.
<!-- translation-sync: zh@v11, en@v11, ja@v11 -->
<!-- translation-sync: zh@v12, en@v12, ja@v12 -->

View File

@@ -46,6 +46,8 @@ s13 复用 s10 的基础工具、Hooks、Permission 和 Task System并增加
- **可选 worktree** 在需要时把任务绑定到另一个工作目录;未绑定任务仍使用仓库目录。
- **类型化协议和计划闸门** 显式记录关机与审批状态,并在计划获批前阻止修改型工具。
任务图继续采用 s10 的两阶段契约。Lead 先为所有节点调用 `create_task`,再使用返回的运行时 ID 调用 `update_task(addBlockedBy=...)`,最后才分配 ready task。只有 Lead 能使用 `update_task`;队友只能列举、认领和完成任务,团队运行期间不能改写任务图结构。
s11 的后台任务和 s12 的定时任务没有被带入本章。它们不参与队友通信、任务认领或计划审批。
这些机制都属于 Team 这一层。任务发现不需要另一套 Agent Loopworktree 也不会产生另一种 Agent。
@@ -443,4 +445,4 @@ Lead 和队友目前只能调用直接写在 `code.py` 里的工具。接入 Jir
s14 MCP Tools → 通过统一的发现与调用协议,在运行时连接外部服务并把它们的工具加入工具池。
<!-- translation-sync: zh@v11, en@v11, ja@v11 -->
<!-- translation-sync: zh@v12, en@v12, ja@v12 -->

View File

@@ -130,16 +130,11 @@ def _task_path(task_id: str) -> Path:
return path
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
def create_task(subject: str, description: str = "") -> Task:
subject = subject.strip()
if not subject:
raise ValueError("Task subject cannot be empty")
dependencies = list(dict.fromkeys(blockedBy or []))
with task_store_lock():
for dependency in dependencies:
if not _task_path(dependency).is_file():
raise ValueError(f"Dependency not found: {dependency}")
for _ in range(100):
task = Task(
id=f"task_{secrets.token_hex(4)}",
@@ -147,7 +142,7 @@ def create_task(subject: str, description: str = "",
description=description,
status="pending",
owner=None,
blockedBy=dependencies,
blockedBy=[],
)
try:
with _task_path(task.id).open("x", encoding="utf-8") as handle:
@@ -158,6 +153,55 @@ def create_task(subject: str, description: str = "",
raise RuntimeError("Could not allocate a unique task ID")
def _task_depends_on(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(load_task(current).blockedBy)
return False
def update_task(task_id: str, addBlockedBy: list[str]) -> Task:
"""Add dependency edges after create_task has returned real task IDs."""
if not isinstance(addBlockedBy, list):
raise ValueError("addBlockedBy must be a list of task IDs")
with task_store_lock():
task = load_task(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(addBlockedBy))
for dependency in dependencies:
if dependency == task_id:
raise ValueError("Task cannot depend on itself")
if not _task_path(dependency).is_file():
raise ValueError(f"Dependency not found: {dependency}")
if dependency not in task.blockedBy and _task_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
)
save_task(task)
return task
def save_task(task: Task):
with task_store_lock():
path = _task_path(task.id)
@@ -579,9 +623,15 @@ def remove_worktree(name: str, discard_changes: bool = False) -> str:
PROMPT_SECTIONS = {
"identity": "You are a coding agent. Act, don't explain.",
"tools": "Available tools: bash, read_file, write_file, edit_file, glob, "
"get_task, create_task, list_tasks, claim_task, complete_task, "
"create_task, update_task, list_tasks, get_task, claim_task, "
"complete_task, "
"spawn_teammate, list_teammates, send_message, request_shutdown, "
"request_plan, review_plan, create_worktree.",
"tasks": (
"Create all task nodes first. Only after create_task returns "
"runtime-generated IDs, use update_task with those exact IDs to add "
"dependencies. Only the Lead changes task dependencies."
),
"teams": (
"When parallel work would help, first propose a small team with clear "
"responsibilities and wait for the user's confirmation. Do not call "
@@ -716,12 +766,22 @@ def run_agent_glob(pattern: str) -> str:
# -- Task Tools --
def run_create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> str:
task = create_task(subject, description, blockedBy)
deps = f" (blockedBy: {', '.join(blockedBy)})" if blockedBy else ""
print(f" \033[34m[create] {task.subject}{deps}\033[0m")
return f"Created {task.id}: {task.subject}{deps}"
def run_create_task(subject: str, description: str = "") -> str:
task = create_task(subject, description)
print(f" \033[34m[create] {task.subject}\033[0m")
return f"Created {task.id}: {task.subject}"
def run_update_task(task_id: str, addBlockedBy: list[str]) -> str:
try:
task = update_task(task_id, addBlockedBy)
except ValueError as exc:
return f"Error: {exc}"
except FileNotFoundError:
return f"Error: Task {task_id} not found"
dependencies = ", ".join(task.blockedBy) or "(none)"
print(f" \033[34m[update] {task.subject} blockedBy: {dependencies}\033[0m")
return f"Updated {task.id} blockedBy: {dependencies}"
def run_list_tasks() -> str:
@@ -1467,14 +1527,26 @@ BASE_TOOLS = [
TASK_TOOLS = [
{"name": "create_task",
"description": "Create a task with optional dependencies.",
"description": "Create a task and return its runtime-generated ID.",
"input_schema": {"type": "object",
"properties": {
"subject": {"type": "string"},
"description": {"type": "string"},
"blockedBy": {"type": "array",
"items": {"type": "string"}}},
"required": ["subject"]}},
"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 shared tasks.",
"input_schema": {"type": "object", "properties": {}}},
{"name": "get_task", "description": "Get one task by ID.",
@@ -1569,6 +1641,7 @@ TOOL_HANDLERS = {
"edit_file": run_agent_edit,
"glob": run_agent_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,