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

@@ -79,12 +79,12 @@ loop 自体は同じ構造のままだ。model を呼び、response に `tool_us
### Tools と Dispatch
built-in tool pool には 25 個の tool がある:
built-in tool pool には 26 個の tool がある:
```text
bash, read_file, write_file, edit_file, glob
todo_write, task, load_skill, compact
create_task, list_tasks, get_task, claim_task, complete_task
create_task, update_task, list_tasks, get_task, claim_task, complete_task
schedule_cron, list_crons, cancel_cron
spawn_teammate, list_teammates, send_message
request_shutdown, request_plan, review_plan
@@ -127,6 +127,8 @@ S15 には 2 層の plan がある:
目的は近いが実装は別である。`todo_write` は現在のセッションのチェックリスト全体を置き換え、task record は安定 ID と個別のライフサイクル更新を持つ。次節の独立した `task` ツールは「隔離 subagent を一度派遣する」意味であり、Task System ではない。
統合 host でもタスクグラフは 2 段階で構築する。Lead はまず全タスクノードを作成し、`create_task` が返した実行時 ID で `update_task` を呼ぶ。チームメイトが使えるのは一覧・Claim・完了だけなので、依存構造は仕事を配る前に Lead が確定する。
### Subagent と Team
S15 には 2 種類の delegation がある:
@@ -238,4 +240,4 @@ python s15_integrated_harness/code.py
[s16 Workflow Runtime](../s16_workflow_runtime/) は、この host に `Workflow` tool を追加する。Workflow は固定された orchestration path を code に置き、進行状況を記録して同じ run を再開できるようにする。
<!-- translation-sync: zh@v13, en@v13, ja@v13 -->
<!-- translation-sync: zh@v14, en@v14, ja@v14 -->

View File

@@ -79,12 +79,12 @@ The loop keeps the same structure: call the model, check whether the response co
### Tools and Dispatch
The built-in tool pool contains 25 tools:
The built-in tool pool contains 26 tools:
```text
bash, read_file, write_file, edit_file, glob
todo_write, task, load_skill, compact
create_task, list_tasks, get_task, claim_task, complete_task
create_task, update_task, list_tasks, get_task, claim_task, complete_task
schedule_cron, list_crons, cancel_cron
spawn_teammate, list_teammates, send_message
request_shutdown, request_plan, review_plan
@@ -127,6 +127,8 @@ The first keeps a single agent from drifting. The second supports team coordinat
They share an intent, not an implementation: `todo_write` replaces one session checklist, while task records have stable IDs and individual lifecycle updates. The separate `task` tool below means "dispatch one isolated subagent"; it is not the Task System.
Task graph construction remains two-phase in the integrated host: the Lead creates all task nodes first, then calls `update_task` with the runtime IDs returned by `create_task`. Teammates receive only list, claim, and complete operations, so dependency structure is fixed by the Lead before work is distributed.
### Subagents and Teams
S15 has two kinds of delegation:
@@ -238,4 +240,4 @@ Watch for:
[s16 Workflow Runtime](../s16_workflow_runtime/) adds a `Workflow` tool to this host. A workflow keeps a fixed orchestration path in code and records progress so the same run can resume.
<!-- translation-sync: zh@v13, en@v13, ja@v13 -->
<!-- translation-sync: zh@v14, en@v14, ja@v14 -->

View File

@@ -79,12 +79,12 @@ S15 不再引入新机制,而是把前面各章的组件集成到同一个 har
### 工具与分发
内置工具池包含 25 个工具:
内置工具池包含 26 个工具:
```text
bash, read_file, write_file, edit_file, glob
todo_write, task, load_skill, compact
create_task, list_tasks, get_task, claim_task, complete_task
create_task, update_task, list_tasks, get_task, claim_task, complete_task
schedule_cron, list_crons, cancel_cron
spawn_teammate, list_teammates, send_message
request_shutdown, request_plan, review_plan
@@ -127,6 +127,8 @@ S15 同时保留两层计划:
两者目标相近,但实现不同:`todo_write` 整表替换当前会话清单task record 则有稳定 ID 和单条生命周期更新。下面单独出现的 `task` 工具表示“一次性派发隔离 subagent”不是 Task System。
集成宿主中的任务图仍采用两阶段构建Lead 先创建所有任务节点,再使用 `create_task` 返回的运行时 ID 调用 `update_task`。队友只能列举、认领和完成任务,因此依赖结构由 Lead 在分发工作前确定。
### 子 agent 与团队
S15 有两种 delegation
@@ -238,4 +240,4 @@ python s15_integrated_harness/code.py
[s16 Workflow Runtime](../s16_workflow_runtime/) 会在这个 host 中加入 `Workflow` 工具。Workflow 把固定的编排路径写在代码中,并记录运行进度,使同一次运行可以继续执行。
<!-- translation-sync: zh@v13, en@v13, ja@v13 -->
<!-- translation-sync: zh@v14, en@v14, ja@v14 -->

View File

@@ -205,16 +205,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)}",
@@ -222,7 +217,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:
@@ -233,6 +228,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)
@@ -737,12 +781,18 @@ PROMPT_SECTIONS = {
"identity": "You are a coding agent. Act, don't explain.",
"tools": "Available tools: bash, read_file, write_file, edit_file, glob, "
"todo_write, task, load_skill, compact, "
"create_task, list_tasks, get_task, claim_task, complete_task, "
"create_task, update_task, list_tasks, get_task, claim_task, "
"complete_task, "
"schedule_cron, list_crons, cancel_cron, "
"spawn_teammate, list_teammates, send_message, "
"request_shutdown, request_plan, review_plan, "
"create_worktree, "
"connect_mcp. MCP tools are prefixed mcp__{server}__{tool}.",
"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 "
@@ -777,6 +827,7 @@ def assemble_system_prompt(context: dict) -> str:
# memory, skill catalog, MCP state, and active teammates become visible.
sections = [PROMPT_SECTIONS["identity"],
PROMPT_SECTIONS["tools"],
PROMPT_SECTIONS["tasks"],
PROMPT_SECTIONS["teams"],
PROMPT_SECTIONS["workspace"],
PROMPT_SECTIONS["memory"],
@@ -2620,12 +2671,22 @@ def run_create_worktree(name: str, task_id: str) -> str:
# -- Basic Tool Handlers --
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:
@@ -2745,13 +2806,26 @@ BUILTIN_TOOLS = [
"input_schema": {"type": "object",
"properties": {"focus": {"type": "string"}},
"required": []}},
{"name": "create_task", "description": "Create a task.",
{"name": "create_task",
"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 all tasks.",
"input_schema": {"type": "object", "properties": {}, "required": []}},
{"name": "get_task", "description": "Get full task details.",
@@ -2848,7 +2922,8 @@ BUILTIN_HANDLERS = {
"glob": run_agent_glob,
"todo_write": run_todo_write, "task": spawn_subagent,
"load_skill": load_skill,
"create_task": run_create_task, "list_tasks": run_list_tasks,
"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, "complete_task": run_complete_task,
"schedule_cron": run_schedule_cron,

View File

@@ -74,7 +74,7 @@
<text x="718" y="407" fill="#0f766e" font-size="9">s14 MCP tools</text>
<path d="M 790 318 L 790 188" fill="none" stroke="#0d9488" stroke-width="1.3" marker-end="url(#arrow-green)" stroke-dasharray="5,3"/>
<rect x="70" y="462" width="780" height="112" rx="8" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.2"/>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 25 builtins + dynamic mcp__server__tool</text>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 26 builtins + dynamic mcp__server__tool</text>
<text x="95" y="512" fill="#334155" font-size="9">file/shell: bash · read · write · edit · glob</text>
<text x="95" y="530" fill="#334155" font-size="9">single-agent: todo_write · task · load_skill · compact</text>
<text x="95" y="548" fill="#334155" font-size="9">durable work: task tools · cron tools</text>

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -74,7 +74,7 @@
<text x="718" y="407" fill="#0f766e" font-size="9">s14 MCP tools</text>
<path d="M 790 318 L 790 188" fill="none" stroke="#0d9488" stroke-width="1.3" marker-end="url(#arrow-green)" stroke-dasharray="5,3"/>
<rect x="70" y="462" width="780" height="112" rx="8" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.2"/>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 25 builtins + dynamic mcp__server__tool</text>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 26 builtins + dynamic mcp__server__tool</text>
<text x="95" y="512" fill="#334155" font-size="9">file/shell: bash · read · write · edit · glob</text>
<text x="95" y="530" fill="#334155" font-size="9">single-agent: todo_write · task · load_skill · compact</text>
<text x="95" y="548" fill="#334155" font-size="9">durable work: task tools · cron tools</text>

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -94,10 +94,10 @@
<!-- Tool pool -->
<rect x="70" y="462" width="780" height="112" rx="8" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.2"/>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 25 builtins + dynamic mcp__server__tool</text>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 26 builtins + dynamic mcp__server__tool</text>
<text x="95" y="512" fill="#334155" font-size="9">file/shell: bash · read · write · edit · glob</text>
<text x="95" y="530" fill="#334155" font-size="9">single-agent: todo_write · task · load_skill · compact</text>
<text x="95" y="548" fill="#334155" font-size="9">durable work: create/list/get/claim/complete_task · schedule/list/cancel_cron</text>
<text x="95" y="548" fill="#334155" font-size="9">durable work: create/update/list/get/claim/complete_task · schedule/list/cancel_cron</text>
<text x="510" y="512" fill="#334155" font-size="9">team: spawn_teammate · send_message · typed protocols</text>
<text x="510" y="530" fill="#334155" font-size="9">protocol: request_shutdown · request_plan · review_plan</text>
<text x="510" y="548" fill="#334155" font-size="9">workdir/plugin: create_worktree · connect_mcp</text>

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB