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

@@ -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,