Merge pull request #533 from Bill-Billion/fix/task-dependency-two-phase
fix: build task dependencies in two phases
|
|
@ -26,7 +26,7 @@ TodoWrite は、こうした依存関係や担当を記録しない。「API を
|
|||
|
||||

|
||||
|
||||
コードは S04 の 5 つの基本ツール、Permission、Hooks、共通の `execute_tool` を保ち、そこへ 5 つのタスクツール、`.tasks/` ディレクトリへの永続化、`blockedBy` の依存チェックを追加する。
|
||||
コードは S04 の 5 つの基本ツール、Permission、Hooks、共通の `execute_tool` を保ち、そこへ 6 つのタスクツール、`.tasks/` ディレクトリへの永続化、`blockedBy` の依存チェックを追加する。
|
||||
|
||||
TodoWrite vs Task System:
|
||||
|
||||
|
|
@ -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: 依存チェック
|
||||
|
||||
|
|
@ -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(依存なし)
|
||||
|
|
@ -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 はファイルを読んで進捗を復旧できる。
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ This chapter adds a Task System. Each task has its own ID and status; `blockedBy
|
|||
|
||||

|
||||
|
||||
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:
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ TodoWrite 没有记录这些依赖和分工。它可以显示“编写 API”仍
|
|||
|
||||

|
||||
|
||||
代码保留 S04 的五个基础工具、Permission、Hooks 和统一 `execute_tool`,再加入 5 个任务工具、`.tasks/` 目录持久化和 `blockedBy` 依赖检查。
|
||||
代码保留 S04 的五个基础工具、Permission、Hooks 和统一 `execute_tool`,再加入 6 个任务工具、`.tasks/` 目录持久化和 `blockedBy` 依赖检查。
|
||||
|
||||
TodoWrite vs Task System:
|
||||
|
||||
|
|
@ -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: 依赖检查
|
||||
|
||||
|
|
@ -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 (无依赖)
|
||||
|
|
@ -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 读文件就能恢复进度。
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -53,7 +53,9 @@ MODEL = os.environ["MODEL_ID"]
|
|||
|
||||
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."
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -97,17 +99,11 @@ class TaskStore:
|
|||
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(
|
||||
|
|
@ -116,7 +112,7 @@ class TaskStore:
|
|||
description=description,
|
||||
status="pending",
|
||||
owner=None,
|
||||
blockedBy=dependencies,
|
||||
blockedBy=[],
|
||||
)
|
||||
try:
|
||||
with self._path(task.id, create_root=True).open(
|
||||
|
|
@ -128,6 +124,52 @@ class TaskStore:
|
|||
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),
|
||||
|
|
@ -154,9 +196,12 @@ class TaskStore:
|
|||
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:
|
||||
|
|
@ -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:
|
||||
|
|
@ -347,8 +394,10 @@ TOOLS = [
|
|||
"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.",
|
||||
|
|
@ -366,6 +415,7 @@ TOOL_HANDLERS = {
|
|||
"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,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<!-- Title -->
|
||||
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
|
||||
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task System — 5 Task Tools + .tasks/ Persistence + blockedBy Dependencies</text>
|
||||
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task System — 6 Task Tools + .tasks/ Persistence + blockedBy Dependencies</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
<rect x="393" y="80" width="210" height="64" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="498" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">Hooks + TOOL_HANDLERS</text>
|
||||
<text x="408" y="114" fill="#2563eb" font-size="9">bash · read · write · edit · glob</text>
|
||||
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_task · list_tasks</text>
|
||||
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_task · update_task · list_tasks</text>
|
||||
<text x="408" y="140" fill="#0d9488" font-size="9" font-weight="600">get_task · claim_task · complete_task</text>
|
||||
|
||||
<!-- Loop back -->
|
||||
|
|
@ -61,13 +61,13 @@
|
|||
|
||||
<!-- Arrow: tools → .tasks/ -->
|
||||
<path d="M 440 144 L 440 165 L 250 165 L 250 185" fill="none" stroke="#0d9488" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
|
||||
<text x="320" y="178" fill="#0d9488" font-size="9">create / save / read</text>
|
||||
<text x="300" y="178" fill="#0d9488" font-size="9">create → ID / update edges / read</text>
|
||||
|
||||
<!-- ===== Lifecycle (teal) ===== -->
|
||||
<rect x="390" y="185" width="330" height="76" rx="8" fill="#f0fdfa" stroke="#0d9488" stroke-width="2"/>
|
||||
<text x="555" y="205" fill="#134e4a" font-size="11" font-weight="700" text-anchor="middle">Dependency Check + Lifecycle</text>
|
||||
<text x="408" y="222" fill="#0d9488" font-size="9">can_start: all blockedBy completed?</text>
|
||||
<text x="408" y="238" fill="#0d9488" font-size="9">claim_task → owner = agent, pending → in_progress</text>
|
||||
<text x="408" y="222" fill="#0d9488" font-size="9">create_task → runtime ID; update_task → blockedBy</text>
|
||||
<text x="408" y="238" fill="#0d9488" font-size="9">can_start + claim: all dependencies completed?</text>
|
||||
<text x="408" y="252" fill="#0d9488" font-size="9">complete_task → completed + unblock downstream</text>
|
||||
|
||||
<!-- ===== State machine ===== -->
|
||||
|
|
@ -90,5 +90,5 @@
|
|||
<rect x="60" y="366" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="80" y="376" fill="#475569" font-size="10">Base loop: model call + Permission/Hooks + tool dispatch + tool_result</text>
|
||||
<rect x="60" y="384" width="12" height="10" rx="2" fill="#f0fdfa" stroke="#0d9488" stroke-width="1"/>
|
||||
<text x="80" y="394" fill="#475569" font-size="10">s10 New: Task dataclass + 5 tools + .tasks/ persistence + blockedBy dependency graph</text>
|
||||
<text x="80" y="394" fill="#475569" font-size="10">s10 New: Task dataclass + 6 tools + .tasks/ persistence + blockedBy dependency graph</text>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.8 KiB |
|
|
@ -16,7 +16,7 @@
|
|||
<!-- タイトル -->
|
||||
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
|
||||
<text x="380" y="28" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Task System — 5 つのタスクツール + .tasks/ 永続化 + blockedBy 依存</text>
|
||||
<text x="380" y="28" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Task System — 6 つのタスクツール + .tasks/ 永続化 + blockedBy 依存</text>
|
||||
|
||||
<!-- 凡例 -->
|
||||
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
<rect x="393" y="80" width="210" height="64" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="498" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">Hooks + TOOL_HANDLERS</text>
|
||||
<text x="408" y="114" fill="#2563eb" font-size="9">bash · read · write · edit · glob</text>
|
||||
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_task · list_tasks</text>
|
||||
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_task · update_task · list_tasks</text>
|
||||
<text x="408" y="140" fill="#0d9488" font-size="9" font-weight="600">get_task · claim_task · complete_task</text>
|
||||
|
||||
<!-- ループバック -->
|
||||
|
|
@ -61,13 +61,13 @@
|
|||
|
||||
<!-- 矢印: tools → .tasks/ -->
|
||||
<path d="M 440 144 L 440 165 L 250 165 L 250 185" fill="none" stroke="#0d9488" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
|
||||
<text x="320" y="178" fill="#0d9488" font-size="9">create / save / read</text>
|
||||
<text x="300" y="178" fill="#0d9488" font-size="9">create → ID / update edges / read</text>
|
||||
|
||||
<!-- ===== ライフサイクル(ティール) ===== -->
|
||||
<rect x="390" y="185" width="330" height="76" rx="8" fill="#f0fdfa" stroke="#0d9488" stroke-width="2"/>
|
||||
<text x="555" y="205" fill="#134e4a" font-size="11" font-weight="700" text-anchor="middle">依存チェック + ライフサイクル</text>
|
||||
<text x="408" y="222" fill="#0d9488" font-size="9">can_start: blockedBy がすべて completed?</text>
|
||||
<text x="408" y="238" fill="#0d9488" font-size="9">claim_task → owner = agent, pending → in_progress</text>
|
||||
<text x="408" y="222" fill="#0d9488" font-size="9">create_task → 実行時 ID;update_task → blockedBy</text>
|
||||
<text x="408" y="238" fill="#0d9488" font-size="9">can_start + claim:依存がすべて completed?</text>
|
||||
<text x="408" y="252" fill="#0d9488" font-size="9">complete_task → completed + 下流をアンロック</text>
|
||||
|
||||
<!-- ===== 状態マシン ===== -->
|
||||
|
|
@ -90,5 +90,5 @@
|
|||
<rect x="60" y="366" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="80" y="376" fill="#475569" font-size="10">基本ループ:モデル呼び出し + Permission/Hooks + ツール分配 + tool_result</text>
|
||||
<rect x="60" y="384" width="12" height="10" rx="2" fill="#f0fdfa" stroke="#0d9488" stroke-width="1"/>
|
||||
<text x="80" y="394" fill="#475569" font-size="10">s10 新規:Task dataclass + 5 ツール + .tasks/ 永続化 + blockedBy 依存グラフ</text>
|
||||
<text x="80" y="394" fill="#475569" font-size="10">s10 新規:Task dataclass + 6 ツール + .tasks/ 永続化 + blockedBy 依存グラフ</text>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
|
@ -16,7 +16,7 @@
|
|||
<!-- Title -->
|
||||
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
|
||||
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task System — 5 个任务工具 + .tasks/ 持久化 + blockedBy 依赖</text>
|
||||
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task System — 6 个任务工具 + .tasks/ 持久化 + blockedBy 依赖</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
<rect x="393" y="80" width="210" height="64" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="498" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">Hooks + TOOL_HANDLERS</text>
|
||||
<text x="408" y="114" fill="#2563eb" font-size="9">bash · read · write · edit · glob</text>
|
||||
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_task · list_tasks</text>
|
||||
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_task · update_task · list_tasks</text>
|
||||
<text x="408" y="140" fill="#0d9488" font-size="9" font-weight="600">get_task · claim_task · complete_task</text>
|
||||
|
||||
<!-- Loop back -->
|
||||
|
|
@ -61,13 +61,13 @@
|
|||
|
||||
<!-- Arrow: tools → .tasks/ -->
|
||||
<path d="M 440 144 L 440 165 L 250 165 L 250 185" fill="none" stroke="#0d9488" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
|
||||
<text x="320" y="178" fill="#0d9488" font-size="9">create / save / read</text>
|
||||
<text x="300" y="178" fill="#0d9488" font-size="9">create → ID / update edges / read</text>
|
||||
|
||||
<!-- ===== Lifecycle (teal) ===== -->
|
||||
<rect x="390" y="185" width="330" height="76" rx="8" fill="#f0fdfa" stroke="#0d9488" stroke-width="2"/>
|
||||
<text x="555" y="205" fill="#134e4a" font-size="11" font-weight="700" text-anchor="middle">依赖检查 + 生命周期</text>
|
||||
<text x="408" y="222" fill="#0d9488" font-size="9">can_start: blockedBy 全部 completed?</text>
|
||||
<text x="408" y="238" fill="#0d9488" font-size="9">claim_task → owner = agent, pending → in_progress</text>
|
||||
<text x="408" y="222" fill="#0d9488" font-size="9">create_task → 运行时 ID;update_task → blockedBy</text>
|
||||
<text x="408" y="238" fill="#0d9488" font-size="9">can_start + claim:依赖全部 completed?</text>
|
||||
<text x="408" y="252" fill="#0d9488" font-size="9">complete_task → completed + 解锁下游</text>
|
||||
|
||||
<!-- ===== State machine ===== -->
|
||||
|
|
@ -90,5 +90,5 @@
|
|||
<rect x="60" y="366" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="80" y="376" fill="#475569" font-size="10">基础循环:模型调用 + Permission/Hooks + 工具分发 + tool_result</text>
|
||||
<rect x="60" y="384" width="12" height="10" rx="2" fill="#f0fdfa" stroke="#0d9488" stroke-width="1"/>
|
||||
<text x="80" y="394" fill="#475569" font-size="10">s10 新增:Task dataclass + 5 个工具 + .tasks/ 持久化 + blockedBy 依赖图</text>
|
||||
<text x="80" y="394" fill="#475569" font-size="10">s10 新增:Task dataclass + 6 个工具 + .tasks/ 持久化 + blockedBy 依赖图</text>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.8 KiB |
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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 Loop,worktree 也不会产生另一种 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 -->
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -123,6 +123,13 @@ def claim_in_child(lesson_path: str, root: str, task_id: str, owner: str,
|
|||
results.put(lesson.claim_task(task_id, owner=owner))
|
||||
|
||||
|
||||
def update_in_child(lesson_path: str, root: str, task_id: str,
|
||||
dependency_id: str, barrier, results):
|
||||
lesson = load_lesson(Path(root), Path(lesson_path))
|
||||
barrier.wait()
|
||||
results.put(lesson.run_update_task(task_id, [dependency_id]))
|
||||
|
||||
|
||||
class AgentTeamsRuntimeTests(unittest.TestCase):
|
||||
def test_downstream_lessons_execute_the_merged_runtime_contract(self):
|
||||
for lesson_path in RUNTIME_LESSONS:
|
||||
|
|
@ -140,6 +147,54 @@ class AgentTeamsRuntimeTests(unittest.TestCase):
|
|||
self.assertTrue(lesson.release_completed_assignment("alice"))
|
||||
self.assertNotIn("alice", lesson.teammate_assignments)
|
||||
|
||||
def test_task_dependencies_use_runtime_ids_and_are_lead_only(self):
|
||||
for lesson_path in RUNTIME_LESSONS:
|
||||
with self.subTest(lesson=lesson_path.parent.name):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
lesson = load_lesson(Path(tmp), lesson_path)
|
||||
tool_defs = getattr(lesson, "TOOLS", None)
|
||||
if tool_defs is None:
|
||||
tool_defs = lesson.BUILTIN_TOOLS
|
||||
tools = {tool["name"]: tool for tool in tool_defs}
|
||||
|
||||
self.assertIn("update_task", tools)
|
||||
self.assertNotIn(
|
||||
"blockedBy",
|
||||
tools["create_task"]["input_schema"]["properties"],
|
||||
)
|
||||
self.assertIn(
|
||||
"runtime-generated IDs",
|
||||
lesson.PROMPT_SECTIONS["tasks"],
|
||||
)
|
||||
|
||||
dependency = lesson.create_task("Create schema")
|
||||
target = lesson.create_task("Write API")
|
||||
self.assertIn(
|
||||
"Updated",
|
||||
lesson.run_update_task(target.id, [dependency.id]),
|
||||
)
|
||||
self.assertEqual(
|
||||
lesson.load_task(target.id).blockedBy, [dependency.id]
|
||||
)
|
||||
|
||||
captured_tools = []
|
||||
|
||||
def stop_after_capture(**kwargs):
|
||||
captured_tools.extend(
|
||||
tool["name"] for tool in kwargs["tools"]
|
||||
)
|
||||
raise RuntimeError("stop after capturing teammate tools")
|
||||
|
||||
lesson.client.messages.create = stop_after_capture
|
||||
lesson.spawn_teammate_thread(
|
||||
"tool-auditor", "reviewer", "Inspect the task board."
|
||||
)
|
||||
self.assertTrue(wait_until(
|
||||
lambda: "tool-auditor" not in lesson.active_teammates
|
||||
))
|
||||
self.assertIn("claim_task", captured_tools)
|
||||
self.assertNotIn("update_task", captured_tools)
|
||||
|
||||
def test_inbox_delivery_is_runtime_owned(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
lesson = load_lesson(Path(tmp))
|
||||
|
|
@ -669,6 +724,9 @@ class AgentTeamsRuntimeTests(unittest.TestCase):
|
|||
with self.subTest(tool=tool_name, task_id=task_id):
|
||||
result = getattr(lesson, tool_name)(task_id)
|
||||
self.assertIn("Error:", result)
|
||||
self.assertIn(
|
||||
"Error:", lesson.run_update_task(task_id, [])
|
||||
)
|
||||
|
||||
def test_plan_gate_blocks_mutating_tools_until_approval(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
|
|
@ -1621,6 +1679,55 @@ class AgentTeamsRuntimeTests(unittest.TestCase):
|
|||
self.assertEqual(persisted.status, "in_progress")
|
||||
self.assertIn(persisted.owner, {"alice", "bob"})
|
||||
|
||||
def test_dependency_updates_are_atomic_across_processes(self):
|
||||
context = multiprocessing.get_context("spawn")
|
||||
for lesson_path in RUNTIME_LESSONS:
|
||||
with self.subTest(lesson=lesson_path.parent.name):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
lesson = load_lesson(Path(tmp), lesson_path)
|
||||
first = lesson.create_task("First")
|
||||
second = lesson.create_task("Second")
|
||||
barrier = context.Barrier(3)
|
||||
results = context.Queue()
|
||||
workers = [
|
||||
context.Process(
|
||||
target=update_in_child,
|
||||
args=(
|
||||
str(lesson_path), tmp, task_id, dependency_id,
|
||||
barrier, results,
|
||||
),
|
||||
)
|
||||
for task_id, dependency_id in (
|
||||
(first.id, second.id),
|
||||
(second.id, first.id),
|
||||
)
|
||||
]
|
||||
|
||||
for worker in workers:
|
||||
worker.start()
|
||||
barrier.wait()
|
||||
for worker in workers:
|
||||
worker.join(5)
|
||||
self.assertEqual(worker.exitcode, 0)
|
||||
outcomes = [results.get(timeout=1) for _ in workers]
|
||||
|
||||
self.assertEqual(
|
||||
sum(outcome.startswith("Updated ") for outcome in outcomes),
|
||||
1,
|
||||
)
|
||||
self.assertEqual(
|
||||
sum("Dependency cycle detected" in outcome
|
||||
for outcome in outcomes),
|
||||
1,
|
||||
)
|
||||
persisted = {
|
||||
first.id: lesson.load_task(first.id).blockedBy,
|
||||
second.id: lesson.load_task(second.id).blockedBy,
|
||||
}
|
||||
self.assertEqual(
|
||||
sum(bool(value) for value in persisted.values()), 1
|
||||
)
|
||||
|
||||
def test_plan_approval_cannot_cross_assignment_boundary(self):
|
||||
for lesson_path in RUNTIME_LESSONS:
|
||||
with self.subTest(lesson=lesson_path.parent.name):
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ def test_s10_keeps_the_s04_kernel_and_adds_task_tools() -> None:
|
|||
"edit_file",
|
||||
"glob",
|
||||
"create_task",
|
||||
"update_task",
|
||||
"list_tasks",
|
||||
"get_task",
|
||||
"claim_task",
|
||||
|
|
@ -83,6 +84,13 @@ def test_s10_keeps_the_s04_kernel_and_adds_task_tools() -> None:
|
|||
assert not hasattr(lesson, "MEMORY_DIR")
|
||||
assert not (workdir / ".tasks").exists()
|
||||
|
||||
tools = {tool["name"]: tool for tool in lesson.TOOLS}
|
||||
create_schema = tools["create_task"]["input_schema"]
|
||||
update_schema = tools["update_task"]["input_schema"]
|
||||
assert "blockedBy" not in create_schema["properties"]
|
||||
assert create_schema["additionalProperties"] is False
|
||||
assert update_schema["required"] == ["task_id", "addBlockedBy"]
|
||||
|
||||
|
||||
def test_dependencies_gate_claim_and_completion_checks_owner() -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
|
|
@ -90,7 +98,8 @@ def test_dependencies_gate_claim_and_completion_checks_owner() -> None:
|
|||
lesson = load_lesson(workdir)
|
||||
|
||||
schema = lesson.create_task("create schema")
|
||||
api = lesson.create_task("write API", blockedBy=[schema.id])
|
||||
api = lesson.create_task("write API")
|
||||
lesson.update_task(api.id, [schema.id])
|
||||
|
||||
assert lesson.claim_task(api.id) == f"Blocked by: ['{schema.id}']"
|
||||
assert "Claimed" in lesson.claim_task(schema.id)
|
||||
|
|
@ -103,6 +112,41 @@ def test_dependencies_gate_claim_and_completion_checks_owner() -> None:
|
|||
assert lesson.load_task(api.id).status == "completed"
|
||||
|
||||
|
||||
def test_dependencies_are_added_after_create_returns_runtime_ids() -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
lesson = load_lesson(Path(tmp))
|
||||
|
||||
create_results = [
|
||||
lesson.execute_tool(tool_call("create_task", subject=subject))
|
||||
for subject in (
|
||||
"create schema",
|
||||
"write API",
|
||||
"write tests",
|
||||
"write docs",
|
||||
)
|
||||
]
|
||||
task_ids = [result.split()[1].rstrip(":") for result in create_results]
|
||||
schema_id, api_id, tests_id, docs_id = task_ids
|
||||
|
||||
update_results = [
|
||||
lesson.execute_tool(tool_call(
|
||||
"update_task", task_id=api_id, addBlockedBy=[schema_id]
|
||||
)),
|
||||
lesson.execute_tool(tool_call(
|
||||
"update_task", task_id=tests_id, addBlockedBy=[api_id]
|
||||
)),
|
||||
lesson.execute_tool(tool_call(
|
||||
"update_task", task_id=docs_id, addBlockedBy=[schema_id]
|
||||
)),
|
||||
]
|
||||
|
||||
assert all(not result.startswith("Error:") for result in update_results)
|
||||
assert lesson.load_task(schema_id).blockedBy == []
|
||||
assert lesson.load_task(api_id).blockedBy == [schema_id]
|
||||
assert lesson.load_task(tests_id).blockedBy == [api_id]
|
||||
assert lesson.load_task(docs_id).blockedBy == [schema_id]
|
||||
|
||||
|
||||
def test_invalid_and_missing_task_ids_become_tool_results() -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
lesson = load_lesson(Path(tmp))
|
||||
|
|
@ -132,17 +176,49 @@ def test_create_retries_instead_of_overwriting_an_existing_id(
|
|||
assert [task.subject for task in lesson.list_tasks()] == ["second", "first"]
|
||||
|
||||
|
||||
def test_create_rejects_unknown_dependencies() -> None:
|
||||
def test_update_rejects_invalid_graph_changes_without_partial_mutation() -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
lesson = load_lesson(Path(tmp))
|
||||
dependency = lesson.create_task("create schema")
|
||||
target = lesson.create_task("write API")
|
||||
|
||||
output = lesson.execute_tool(tool_call(
|
||||
"create_task",
|
||||
subject="write API",
|
||||
blockedBy=["task_00000000"],
|
||||
missing = lesson.execute_tool(tool_call(
|
||||
"update_task",
|
||||
task_id=target.id,
|
||||
addBlockedBy=[dependency.id, "task_00000000"],
|
||||
))
|
||||
self_dependency = lesson.execute_tool(tool_call(
|
||||
"update_task", task_id=target.id, addBlockedBy=[target.id]
|
||||
))
|
||||
|
||||
assert output == "Error: Dependency not found: task_00000000"
|
||||
assert missing == "Error: Dependency not found: task_00000000"
|
||||
assert self_dependency == "Error: Task cannot depend on itself"
|
||||
assert lesson.load_task(target.id).blockedBy == []
|
||||
|
||||
|
||||
def test_update_is_idempotent_and_rejects_cycles_or_started_tasks() -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
lesson = load_lesson(Path(tmp))
|
||||
first = lesson.create_task("first")
|
||||
second = lesson.create_task("second")
|
||||
third = lesson.create_task("third")
|
||||
|
||||
lesson.update_task(second.id, [first.id, first.id])
|
||||
lesson.update_task(second.id, [first.id])
|
||||
lesson.update_task(third.id, [second.id])
|
||||
|
||||
cycle = lesson.execute_tool(tool_call(
|
||||
"update_task", task_id=first.id, addBlockedBy=[third.id]
|
||||
))
|
||||
assert cycle.startswith("Error: Dependency cycle detected")
|
||||
assert lesson.load_task(first.id).blockedBy == []
|
||||
assert lesson.load_task(second.id).blockedBy == [first.id]
|
||||
|
||||
assert "Claimed" in lesson.claim_task(first.id)
|
||||
started = lesson.execute_tool(tool_call(
|
||||
"update_task", task_id=first.id, addBlockedBy=[second.id]
|
||||
))
|
||||
assert "only be updated while pending and unowned" in started
|
||||
|
||||
|
||||
def test_task_store_rejects_a_symlink_outside_the_workspace() -> None:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,47 @@ def load_lesson(name: str, script: Path):
|
|||
return module
|
||||
|
||||
|
||||
def test_s10_scenario_builds_the_task_graph_in_two_phases() -> None:
|
||||
steps = load_scenario("s10")["steps"]
|
||||
create_calls = [
|
||||
(index, json.loads(step["content"]))
|
||||
for index, step in enumerate(steps)
|
||||
if step.get("toolName") == "create_task"
|
||||
and step["type"] == "tool_call"
|
||||
]
|
||||
create_results = [
|
||||
(index, step["content"])
|
||||
for index, step in enumerate(steps)
|
||||
if step.get("toolName") == "create_task"
|
||||
and step["type"] == "tool_result"
|
||||
]
|
||||
update_index = next(
|
||||
index for index, step in enumerate(steps)
|
||||
if step.get("toolName") == "update_task"
|
||||
and step["type"] == "tool_call"
|
||||
)
|
||||
update = json.loads(steps[update_index]["content"])
|
||||
task_ids = [
|
||||
re.fullmatch(r"Created (task_[0-9a-f]{8}): .+", content).group(1)
|
||||
for _, content in create_results
|
||||
]
|
||||
|
||||
assert len(create_calls) == len(create_results) == 2
|
||||
assert all("blockedBy" not in content for _, content in create_calls)
|
||||
assert max(index for index, _ in create_results) < update_index
|
||||
assert update == {
|
||||
"task_id": task_ids[1],
|
||||
"addBlockedBy": [task_ids[0]],
|
||||
}
|
||||
claim_inputs = [
|
||||
json.loads(step["content"])
|
||||
for step in steps
|
||||
if step.get("toolName") == "claim_task"
|
||||
and step["type"] == "tool_call"
|
||||
]
|
||||
assert all(set(claim_input) == {"task_id"} for claim_input in claim_inputs)
|
||||
|
||||
|
||||
def test_s13_scenario_uses_the_real_plan_protocol() -> None:
|
||||
steps = load_scenario("s13")["steps"]
|
||||
spawn = next(
|
||||
|
|
@ -48,6 +89,21 @@ def test_s13_scenario_uses_the_real_plan_protocol() -> None:
|
|||
index for index, step in enumerate(steps)
|
||||
if "plan_approval_response" in step.get("content", "")
|
||||
)
|
||||
create_indices = [
|
||||
index for index, step in enumerate(steps)
|
||||
if step.get("toolName") == "create_task"
|
||||
and step["type"] == "tool_call"
|
||||
]
|
||||
update_index = next(
|
||||
index for index, step in enumerate(steps)
|
||||
if step.get("toolName") == "update_task"
|
||||
and step["type"] == "tool_call"
|
||||
)
|
||||
first_spawn_index = next(
|
||||
index for index, step in enumerate(steps)
|
||||
if step.get("toolName") == "spawn_teammate"
|
||||
and step["type"] == "tool_call"
|
||||
)
|
||||
|
||||
review = json.loads(steps[review_index]["content"])
|
||||
spawn_input = json.loads(spawn["content"])
|
||||
|
|
@ -58,6 +114,15 @@ def test_s13_scenario_uses_the_real_plan_protocol() -> None:
|
|||
assert re.fullmatch(r"req_\d{6}", review["request_id"])
|
||||
assert review["approve"] is True
|
||||
assert "approved" not in review
|
||||
assert all(
|
||||
"blockedBy" not in json.loads(steps[index]["content"])
|
||||
for index in create_indices
|
||||
)
|
||||
assert max(create_indices) < update_index < first_spawn_index
|
||||
assert json.loads(steps[update_index]["content"]) == {
|
||||
"task_id": "task_5e6f7a8b",
|
||||
"addBlockedBy": ["task_1a2b3c4d"],
|
||||
}
|
||||
|
||||
|
||||
def test_s15_scenario_calls_the_discovered_mcp_tool() -> None:
|
||||
|
|
|
|||
|
|
@ -473,8 +473,9 @@ def test_workflow_default_entry_extends_the_real_s15_host(
|
|||
tools, handlers = host.assemble_tool_pool()
|
||||
names = [tool["name"] for tool in tools]
|
||||
|
||||
assert len(host.BUILTIN_TOOLS) == 25
|
||||
assert len(host.BUILTIN_TOOLS) == 26
|
||||
assert names[:-1] == [tool["name"] for tool in host.BUILTIN_TOOLS]
|
||||
assert "update_task" in names
|
||||
assert names[-1] == "Workflow"
|
||||
assert handlers["Workflow"] is workflow.run_workflow_sync
|
||||
assert handlers["Workflow"](name="missing") == (
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<!-- Title -->
|
||||
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
|
||||
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task System — 5 Task Tools + .tasks/ Persistence + blockedBy Dependencies</text>
|
||||
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task System — 6 Task Tools + .tasks/ Persistence + blockedBy Dependencies</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
<rect x="393" y="80" width="210" height="64" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="498" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">Hooks + TOOL_HANDLERS</text>
|
||||
<text x="408" y="114" fill="#2563eb" font-size="9">bash · read · write · edit · glob</text>
|
||||
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_task · list_tasks</text>
|
||||
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_task · update_task · list_tasks</text>
|
||||
<text x="408" y="140" fill="#0d9488" font-size="9" font-weight="600">get_task · claim_task · complete_task</text>
|
||||
|
||||
<!-- Loop back -->
|
||||
|
|
@ -61,13 +61,13 @@
|
|||
|
||||
<!-- Arrow: tools → .tasks/ -->
|
||||
<path d="M 440 144 L 440 165 L 250 165 L 250 185" fill="none" stroke="#0d9488" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
|
||||
<text x="320" y="178" fill="#0d9488" font-size="9">create / save / read</text>
|
||||
<text x="300" y="178" fill="#0d9488" font-size="9">create → ID / update edges / read</text>
|
||||
|
||||
<!-- ===== Lifecycle (teal) ===== -->
|
||||
<rect x="390" y="185" width="330" height="76" rx="8" fill="#f0fdfa" stroke="#0d9488" stroke-width="2"/>
|
||||
<text x="555" y="205" fill="#134e4a" font-size="11" font-weight="700" text-anchor="middle">Dependency Check + Lifecycle</text>
|
||||
<text x="408" y="222" fill="#0d9488" font-size="9">can_start: all blockedBy completed?</text>
|
||||
<text x="408" y="238" fill="#0d9488" font-size="9">claim_task → owner = agent, pending → in_progress</text>
|
||||
<text x="408" y="222" fill="#0d9488" font-size="9">create_task → runtime ID; update_task → blockedBy</text>
|
||||
<text x="408" y="238" fill="#0d9488" font-size="9">can_start + claim: all dependencies completed?</text>
|
||||
<text x="408" y="252" fill="#0d9488" font-size="9">complete_task → completed + unblock downstream</text>
|
||||
|
||||
<!-- ===== State machine ===== -->
|
||||
|
|
@ -90,5 +90,5 @@
|
|||
<rect x="60" y="366" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="80" y="376" fill="#475569" font-size="10">Base loop: model call + Permission/Hooks + tool dispatch + tool_result</text>
|
||||
<rect x="60" y="384" width="12" height="10" rx="2" fill="#f0fdfa" stroke="#0d9488" stroke-width="1"/>
|
||||
<text x="80" y="394" fill="#475569" font-size="10">s10 New: Task dataclass + 5 tools + .tasks/ persistence + blockedBy dependency graph</text>
|
||||
<text x="80" y="394" fill="#475569" font-size="10">s10 New: Task dataclass + 6 tools + .tasks/ persistence + blockedBy dependency graph</text>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.8 KiB |
|
|
@ -16,7 +16,7 @@
|
|||
<!-- タイトル -->
|
||||
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
|
||||
<text x="380" y="28" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Task System — 5 つのタスクツール + .tasks/ 永続化 + blockedBy 依存</text>
|
||||
<text x="380" y="28" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Task System — 6 つのタスクツール + .tasks/ 永続化 + blockedBy 依存</text>
|
||||
|
||||
<!-- 凡例 -->
|
||||
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
<rect x="393" y="80" width="210" height="64" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="498" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">Hooks + TOOL_HANDLERS</text>
|
||||
<text x="408" y="114" fill="#2563eb" font-size="9">bash · read · write · edit · glob</text>
|
||||
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_task · list_tasks</text>
|
||||
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_task · update_task · list_tasks</text>
|
||||
<text x="408" y="140" fill="#0d9488" font-size="9" font-weight="600">get_task · claim_task · complete_task</text>
|
||||
|
||||
<!-- ループバック -->
|
||||
|
|
@ -61,13 +61,13 @@
|
|||
|
||||
<!-- 矢印: tools → .tasks/ -->
|
||||
<path d="M 440 144 L 440 165 L 250 165 L 250 185" fill="none" stroke="#0d9488" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
|
||||
<text x="320" y="178" fill="#0d9488" font-size="9">create / save / read</text>
|
||||
<text x="300" y="178" fill="#0d9488" font-size="9">create → ID / update edges / read</text>
|
||||
|
||||
<!-- ===== ライフサイクル(ティール) ===== -->
|
||||
<rect x="390" y="185" width="330" height="76" rx="8" fill="#f0fdfa" stroke="#0d9488" stroke-width="2"/>
|
||||
<text x="555" y="205" fill="#134e4a" font-size="11" font-weight="700" text-anchor="middle">依存チェック + ライフサイクル</text>
|
||||
<text x="408" y="222" fill="#0d9488" font-size="9">can_start: blockedBy がすべて completed?</text>
|
||||
<text x="408" y="238" fill="#0d9488" font-size="9">claim_task → owner = agent, pending → in_progress</text>
|
||||
<text x="408" y="222" fill="#0d9488" font-size="9">create_task → 実行時 ID;update_task → blockedBy</text>
|
||||
<text x="408" y="238" fill="#0d9488" font-size="9">can_start + claim:依存がすべて completed?</text>
|
||||
<text x="408" y="252" fill="#0d9488" font-size="9">complete_task → completed + 下流をアンロック</text>
|
||||
|
||||
<!-- ===== 状態マシン ===== -->
|
||||
|
|
@ -90,5 +90,5 @@
|
|||
<rect x="60" y="366" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="80" y="376" fill="#475569" font-size="10">基本ループ:モデル呼び出し + Permission/Hooks + ツール分配 + tool_result</text>
|
||||
<rect x="60" y="384" width="12" height="10" rx="2" fill="#f0fdfa" stroke="#0d9488" stroke-width="1"/>
|
||||
<text x="80" y="394" fill="#475569" font-size="10">s10 新規:Task dataclass + 5 ツール + .tasks/ 永続化 + blockedBy 依存グラフ</text>
|
||||
<text x="80" y="394" fill="#475569" font-size="10">s10 新規:Task dataclass + 6 ツール + .tasks/ 永続化 + blockedBy 依存グラフ</text>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
|
@ -16,7 +16,7 @@
|
|||
<!-- Title -->
|
||||
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
|
||||
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task System — 5 个任务工具 + .tasks/ 持久化 + blockedBy 依赖</text>
|
||||
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task System — 6 个任务工具 + .tasks/ 持久化 + blockedBy 依赖</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
<rect x="393" y="80" width="210" height="64" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="498" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">Hooks + TOOL_HANDLERS</text>
|
||||
<text x="408" y="114" fill="#2563eb" font-size="9">bash · read · write · edit · glob</text>
|
||||
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_task · list_tasks</text>
|
||||
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_task · update_task · list_tasks</text>
|
||||
<text x="408" y="140" fill="#0d9488" font-size="9" font-weight="600">get_task · claim_task · complete_task</text>
|
||||
|
||||
<!-- Loop back -->
|
||||
|
|
@ -61,13 +61,13 @@
|
|||
|
||||
<!-- Arrow: tools → .tasks/ -->
|
||||
<path d="M 440 144 L 440 165 L 250 165 L 250 185" fill="none" stroke="#0d9488" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
|
||||
<text x="320" y="178" fill="#0d9488" font-size="9">create / save / read</text>
|
||||
<text x="300" y="178" fill="#0d9488" font-size="9">create → ID / update edges / read</text>
|
||||
|
||||
<!-- ===== Lifecycle (teal) ===== -->
|
||||
<rect x="390" y="185" width="330" height="76" rx="8" fill="#f0fdfa" stroke="#0d9488" stroke-width="2"/>
|
||||
<text x="555" y="205" fill="#134e4a" font-size="11" font-weight="700" text-anchor="middle">依赖检查 + 生命周期</text>
|
||||
<text x="408" y="222" fill="#0d9488" font-size="9">can_start: blockedBy 全部 completed?</text>
|
||||
<text x="408" y="238" fill="#0d9488" font-size="9">claim_task → owner = agent, pending → in_progress</text>
|
||||
<text x="408" y="222" fill="#0d9488" font-size="9">create_task → 运行时 ID;update_task → blockedBy</text>
|
||||
<text x="408" y="238" fill="#0d9488" font-size="9">can_start + claim:依赖全部 completed?</text>
|
||||
<text x="408" y="252" fill="#0d9488" font-size="9">complete_task → completed + 解锁下游</text>
|
||||
|
||||
<!-- ===== State machine ===== -->
|
||||
|
|
@ -90,5 +90,5 @@
|
|||
<rect x="60" y="366" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="80" y="376" fill="#475569" font-size="10">基础循环:模型调用 + Permission/Hooks + 工具分发 + tool_result</text>
|
||||
<rect x="60" y="384" width="12" height="10" rx="2" fill="#f0fdfa" stroke="#0d9488" stroke-width="1"/>
|
||||
<text x="80" y="394" fill="#475569" font-size="10">s10 新增:Task dataclass + 5 个工具 + .tasks/ 持久化 + blockedBy 依赖图</text>
|
||||
<text x="80" y="394" fill="#475569" font-size="10">s10 新增:Task dataclass + 6 个工具 + .tasks/ 持久化 + blockedBy 依赖图</text>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.8 KiB |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -15,6 +15,20 @@
|
|||
"description": "各タスクは .tasks/ に id、subject、description、status、owner、blockedBy を持って保存されます。タスクボードはコンテキスト圧縮や再起動を越えて残ります。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "runtime-owned-task-ids",
|
||||
"title": "Create Nodes Before Adding Edges",
|
||||
"description": "create_task returns a host-generated ID. After all nodes exist, update_task uses those returned IDs to add blockedBy edges and rejects invalid or cyclic changes.",
|
||||
"alternatives": "Asking the model to invent persistent IDs can produce collisions or references that the host never created.",
|
||||
"zh": {
|
||||
"title": "先创建节点,再添加边",
|
||||
"description": "create_task 返回宿主生成的 ID。所有节点创建后,update_task 使用这些返回的 ID 添加 blockedBy 边,并拒绝无效或成环的修改。"
|
||||
},
|
||||
"ja": {
|
||||
"title": "ノードを作成してから辺を追加する",
|
||||
"description": "create_task は host が生成した ID を返します。全ノードの作成後、update_task は返された ID で blockedBy の辺を追加し、無効な変更や循環を拒否します。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "blockedby-dependencies",
|
||||
"title": "blockedBy Encodes Ordering",
|
||||
|
|
|
|||
|
|
@ -440,21 +440,23 @@ const CURRENT_FLOW_OVERRIDES: Record<string, FlowDefinition> = {
|
|||
s10: {
|
||||
nodes: [
|
||||
{ id: "start", label: "User Goal", type: "start", x: COL_CENTER, y: 30 },
|
||||
{ id: "create", label: "create_task", type: "subprocess", x: COL_CENTER, y: 120 },
|
||||
{ id: "save", label: "Persist JSON\n.tasks/", type: "process", x: COL_CENTER, y: 210 },
|
||||
{ id: "list", label: "list / get", type: "subprocess", x: COL_RIGHT, y: 300 },
|
||||
{ id: "deps", label: "blockedBy\ncomplete?", type: "decision", x: COL_CENTER, y: 390 },
|
||||
{ id: "blocked", label: "Remain Pending", type: "end", x: COL_RIGHT, y: 490 },
|
||||
{ id: "claim", label: "claim_task\nowner + in_progress", type: "subprocess", x: COL_LEFT, y: 490 },
|
||||
{ id: "complete", label: "complete_task", type: "subprocess", x: COL_LEFT, y: 590 },
|
||||
{ id: "unblock", label: "Report\nUnblocked", type: "process", x: COL_CENTER, y: 690 },
|
||||
{ id: "append", label: "Append Result", type: "process", x: COL_CENTER, y: 780 },
|
||||
{ id: "create", label: "create_task\n(all nodes)", type: "subprocess", x: COL_CENTER, y: 120 },
|
||||
{ id: "save", label: "Persist Nodes\nReturn IDs", type: "process", x: COL_CENTER, y: 210 },
|
||||
{ id: "update", label: "update_task\n(addBlockedBy)", type: "subprocess", x: COL_CENTER, y: 300 },
|
||||
{ id: "list", label: "list / get", type: "subprocess", x: COL_RIGHT, y: 390 },
|
||||
{ id: "deps", label: "blockedBy\ncomplete?", type: "decision", x: COL_CENTER, y: 480 },
|
||||
{ id: "blocked", label: "Remain Pending", type: "end", x: COL_RIGHT, y: 580 },
|
||||
{ id: "claim", label: "claim_task\nowner + in_progress", type: "subprocess", x: COL_LEFT, y: 580 },
|
||||
{ id: "complete", label: "complete_task", type: "subprocess", x: COL_LEFT, y: 680 },
|
||||
{ id: "unblock", label: "Report\nUnblocked", type: "process", x: COL_CENTER, y: 780 },
|
||||
{ id: "append", label: "Append Result", type: "process", x: COL_CENTER, y: 870 },
|
||||
],
|
||||
edges: [
|
||||
{ from: "start", to: "create" },
|
||||
{ from: "create", to: "save" },
|
||||
{ from: "save", to: "list" },
|
||||
{ from: "save", to: "deps" },
|
||||
{ from: "save", to: "update" },
|
||||
{ from: "update", to: "list" },
|
||||
{ from: "update", to: "deps" },
|
||||
{ from: "deps", to: "blocked", label: "no" },
|
||||
{ from: "deps", to: "claim", label: "yes" },
|
||||
{ from: "claim", to: "complete" },
|
||||
|
|
|
|||
|
|
@ -12,36 +12,72 @@
|
|||
"type": "tool_call",
|
||||
"toolName": "create_task",
|
||||
"content": "{\"subject\":\"Run tests\"}",
|
||||
"annotation": "Tasks are created as JSON files under .tasks/."
|
||||
"annotation": "The first phase creates task nodes without dependencies."
|
||||
},
|
||||
{
|
||||
"type": "tool_result",
|
||||
"toolName": "create_task",
|
||||
"content": "Created task_a1b2c3d4: Run tests",
|
||||
"annotation": "The host returns the runtime-generated ID after the tool executes."
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"toolName": "create_task",
|
||||
"content": "{\"subject\":\"Deploy release\",\"blockedBy\":[\"task_tests\"]}",
|
||||
"annotation": "blockedBy encodes dependency ordering."
|
||||
"content": "{\"subject\":\"Deploy release\"}",
|
||||
"annotation": "Sibling create calls do not guess or predeclare IDs."
|
||||
},
|
||||
{
|
||||
"type": "tool_result",
|
||||
"toolName": "create_task",
|
||||
"content": "Created task_5e6f7a8b: Deploy release",
|
||||
"annotation": "The second node receives a different host-generated ID."
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"toolName": "update_task",
|
||||
"content": "{\"task_id\":\"task_5e6f7a8b\",\"addBlockedBy\":[\"task_a1b2c3d4\"]}",
|
||||
"annotation": "The second phase adds an edge using IDs returned by create_task."
|
||||
},
|
||||
{
|
||||
"type": "tool_result",
|
||||
"toolName": "update_task",
|
||||
"content": "Updated task_5e6f7a8b blockedBy: task_a1b2c3d4",
|
||||
"annotation": "The runtime validates and persists the dependency graph."
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"toolName": "claim_task",
|
||||
"content": "{\"task_id\":\"task_deploy\",\"owner\":\"agent\"}",
|
||||
"content": "{\"task_id\":\"task_5e6f7a8b\"}",
|
||||
"annotation": "The claim fails until dependencies are complete."
|
||||
},
|
||||
{
|
||||
"type": "tool_result",
|
||||
"toolName": "claim_task",
|
||||
"content": "Blocked by: [\"task_tests\"]",
|
||||
"content": "Blocked by: [\"task_a1b2c3d4\"]",
|
||||
"annotation": "The task graph prevents premature work."
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"toolName": "claim_task",
|
||||
"content": "{\"task_id\":\"task_a1b2c3d4\"}",
|
||||
"annotation": "The unblocked prerequisite is claimed before work begins."
|
||||
},
|
||||
{
|
||||
"type": "tool_result",
|
||||
"toolName": "claim_task",
|
||||
"content": "Claimed task_a1b2c3d4 (Run tests)",
|
||||
"annotation": "Claim moves the task from pending to in_progress."
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"toolName": "complete_task",
|
||||
"content": "{\"task_id\":\"task_tests\"}",
|
||||
"content": "{\"task_id\":\"task_a1b2c3d4\"}",
|
||||
"annotation": "Completing a dependency can unblock downstream tasks."
|
||||
},
|
||||
{
|
||||
"type": "tool_result",
|
||||
"toolName": "complete_task",
|
||||
"content": "Completed task_tests\nUnblocked: Deploy release",
|
||||
"content": "Completed task_a1b2c3d4 (Run tests)\nUnblocked: Deploy release",
|
||||
"annotation": "The harness reports newly available work."
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -30,24 +30,36 @@
|
|||
"content": "Created task_1a2b3c4d: Refactor authentication",
|
||||
"annotation": "The runtime-generated task ID is carried into every later operation on this task."
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"toolName": "create_worktree",
|
||||
"content": "{\"name\":\"auth-refactor\",\"task_id\":\"task_1a2b3c4d\"}",
|
||||
"annotation": "The worktree directory is recorded on the task instead of managed as a separate workflow or security sandbox."
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"toolName": "create_task",
|
||||
"content": "{\"subject\":\"Update authentication tests\",\"blockedBy\":[\"task_1a2b3c4d\"]}",
|
||||
"annotation": "The task graph keeps dependent work from starting early."
|
||||
"content": "{\"subject\":\"Update authentication tests\"}",
|
||||
"annotation": "The Lead creates every task node before adding graph edges."
|
||||
},
|
||||
{
|
||||
"type": "tool_result",
|
||||
"toolName": "create_task",
|
||||
"content": "Created task_5e6f7a8b: Update authentication tests (blockedBy: task_1a2b3c4d)",
|
||||
"content": "Created task_5e6f7a8b: Update authentication tests",
|
||||
"annotation": "The second generated ID names the dependent task that the test teammate will later claim."
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"toolName": "update_task",
|
||||
"content": "{\"task_id\":\"task_5e6f7a8b\",\"addBlockedBy\":[\"task_1a2b3c4d\"]}",
|
||||
"annotation": "Only the Lead links tasks using IDs already returned by the runtime."
|
||||
},
|
||||
{
|
||||
"type": "tool_result",
|
||||
"toolName": "update_task",
|
||||
"content": "Updated task_5e6f7a8b blockedBy: task_1a2b3c4d",
|
||||
"annotation": "The dependency is fixed before teammates begin claiming work."
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"toolName": "create_worktree",
|
||||
"content": "{\"name\":\"auth-refactor\",\"task_id\":\"task_1a2b3c4d\"}",
|
||||
"annotation": "The worktree directory is recorded on the task instead of managed as a separate workflow or security sandbox."
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"toolName": "spawn_teammate",
|
||||
|
|
|
|||