mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-06-20 20:23:36 +08:00
* feat: s01-s14 docs quality overhaul — tool pipeline, single-agent, knowledge & resilience Rewrite code.py and README (zh/en/ja) for s01-s14, each chapter building incrementally on the previous. Key fixes across chapters: - s01-s04: agent loop, tool dispatch, permission pipeline, hooks - s05-s08: todo write, subagent, skill loading, context compact - s09-s11: memory system, system prompt assembly, error recovery - s12-s14: task graph, background tasks, cron scheduler All chapters CC source-verified. Code inherits fixes forward (PROMPT_SECTIONS, json.dumps cache, real-state context, can_start dep protection, etc.). * feat: s15-s19 docs quality overhaul — multi-agent platform: teams, protocols, autonomy, worktree, MCP tools Rewrite code.py and README (zh/en/ja) for s15-s19, the multi-agent platform chapters. Each chapter inherits all previous fixes and adds one mechanism: - s15: agent teams (TeamCreate, teammate threads, shared task list) - s16: team protocols (plan approval, shutdown handshake, consume_inbox) - s17: autonomous agents (idle polling, auto-claim, consume_lead_inbox) - s18: worktree isolation (git worktree, bind_task, cwd switching, safety) - s19: MCP tools (MCPClient, normalize_mcp_name, assemble_tool_pool, no cache) All appendix source code references verified against CC source. Config priority corrected: claude.ai < plugin < user < project < local. * fix: 5 regressions across s05-s19 — glob safety, todo validation, memory extraction, protocol types, dep crash - s05-s09: glob results now filter with is_relative_to(WORKDIR) (inherited from s02) - s06-s08: todo_write validates content/status required fields (inherited from s05) - s09: extract_memories uses pre-compression snapshot instead of compacted messages - s16: submit_plan docstring clarifies protocol-only (not code-level gate) - s17-s19: match_response restores type mismatch validation (from s16) - s17-s19: claim_task deps list handles missing dep files without crashing * fix: s12 Todo V2 logic reversal, s14/s15 cron range validation, s18/s19 worktree name validation - s12 README (zh/en/ja): fix Todo V2 direction — interactive defaults to Task, non-interactive/SDK defaults to TodoWrite. Fix env var name to CLAUDE_CODE_ENABLE_TASKS (not TODO_V2). - s14/s15: add _validate_cron_field with per-field range checks (minute 0-59, hour 0-23, dom 1-31, month 1-12, dow 0-6), step > 0, range lo <= hi. Replace old try/except validation that only caught exceptions. - s18/s19: add validate_worktree_name() to remove_worktree and keep_worktree, not just create_worktree. * fix: align s16-s19 teaching tool consistency * fix pr265 chapter diagrams * Add comprehensive s20 harness chapter * Fix chapter smoke test regressions * Clarify README tutorial track transition --------- Co-authored-by: Haoran <bill-billion@outlook.com>
This commit is contained in:
280
s12_task_system/README.en.md
Normal file
280
s12_task_system/README.en.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# s12: Task System — Break Big Goals into Small Tasks
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → ... → s10 → s11 → `s12` → [s13](../s13_background_tasks/) → s14 → ... → s20
|
||||
|
||||
> *"Break big goals into small tasks, order them, persist"* — File-persisted task graph, the foundation for multi-agent collaboration.
|
||||
>
|
||||
> **Harness Layer**: Tasks — Persisted goals, recoverable progress.
|
||||
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
The agent receives a project: set up a database, write APIs, add tests. It uses s05's TodoWrite to create a checklist, then starts writing the API first, gets halfway through and realizes there are no database tables, goes back to fix them; when adding tests, discovers the API interface signatures have changed again...
|
||||
|
||||
You can't build the roof before laying the foundation. Tasks have ordering. Task dependencies should form a Directed Acyclic Graph (DAG); the teaching version only demonstrates `blockedBy` checking, without cycle detection.
|
||||
|
||||
s05's TodoWrite is a list. No dependencies, no persistence — when the conversation ends, the list is gone. What you need is a **task system**: each task is a JSON file, tasks have `blockedBy` dependencies, and they persist across sessions on disk.
|
||||
|
||||
---
|
||||
|
||||
## The Solution
|
||||
|
||||

|
||||
|
||||
Teaching code keeps a basic agent loop, omitting S11's full error recovery (RecoveryState, backoff, escalation, reactive compact, fallback model) to stay focused on the task system. Added: 5 new task tools + `.tasks/` directory for persistence + `blockedBy` dependency checking. The task system and error recovery are independent layers: in CC source, `utils/tasks.ts` only handles CRUD, while `query.ts`'s with_retry/RecoveryState handles error recovery, with no coupling between them.
|
||||
|
||||
TodoWrite vs Task System:
|
||||
|
||||
| | TodoWrite (s05) | Task System (s12) |
|
||||
|---|---|---|
|
||||
| Storage | In-memory list | `.tasks/` JSON files |
|
||||
| Dependencies | None | `blockedBy` dependency graph |
|
||||
| Persistence | Lost when conversation ends | Cross-session |
|
||||
| Multi-agent | None | `owner` field |
|
||||
| Status | checked / unchecked | pending → in_progress → completed |
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||

|
||||
|
||||
### Task: Data Structure
|
||||
|
||||
Each task is a JSON file, stored in the `.tasks/` directory:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Task:
|
||||
id: str
|
||||
subject: str
|
||||
description: str
|
||||
status: str # pending | in_progress | completed
|
||||
owner: str | None # Agent name (multi-agent scenarios)
|
||||
blockedBy: list[str] # List of dependency task IDs
|
||||
```
|
||||
|
||||
IDs are generated with `timestamp + random hex`, simple but sufficient. CC uses sequential IDs + a highwatermark file to prevent ID reuse, which is a more rigorous design.
|
||||
|
||||
### create_task: Create Tasks
|
||||
|
||||
```python
|
||||
def create_task(subject: str, description: str = "",
|
||||
blockedBy: list[str] | None = None) -> Task:
|
||||
task = Task(
|
||||
id=f"task_{int(time.time())}_{random_hex(4)}",
|
||||
subject=subject, description=description,
|
||||
status="pending", owner=None,
|
||||
blockedBy=blockedBy or [],
|
||||
)
|
||||
save_task(task)
|
||||
return task
|
||||
```
|
||||
|
||||
Automatically calls `save_task` on creation to write `.tasks/{id}.json`. `blockedBy` declares dependencies, for example "write API" has `blockedBy: ["task_schema"]`.
|
||||
|
||||
### can_start: Dependency Check
|
||||
|
||||
A task can only start after all its `blockedBy` dependencies are **completed**:
|
||||
|
||||
```python
|
||||
def can_start(task_id: str) -> bool:
|
||||
task = load_task(task_id)
|
||||
for dep_id in task.blockedBy:
|
||||
if not _task_path(dep_id).exists():
|
||||
return False # missing dependency = blocked
|
||||
dep = load_task(dep_id)
|
||||
if dep.status != "completed":
|
||||
return False
|
||||
return True
|
||||
```
|
||||
|
||||
`can_start` is a prerequisite check for `claim_task`: if any `blockedBy` dependency is not completed, the task cannot be claimed. Missing dependencies are treated as blocked, avoiding crashes from referencing wrong IDs.
|
||||
|
||||
### claim_task: Claim a Task
|
||||
|
||||
When the agent starts working on a task, it calls `claim_task`: sets `owner`, changes status from `pending` → `in_progress`. The `owner` field records who is working on the task, preventing duplicate claims in multi-agent scenarios:
|
||||
|
||||
```python
|
||||
def claim_task(task_id: str, owner: str = "agent") -> str:
|
||||
task = load_task(task_id)
|
||||
if task.status != "pending":
|
||||
return f"Task {task_id} is {task.status}, cannot claim"
|
||||
if not can_start(task_id):
|
||||
deps = [d for d in task.blockedBy
|
||||
if load_task(d).status != "completed"]
|
||||
return f"Blocked by: {deps}"
|
||||
task.owner = owner
|
||||
task.status = "in_progress"
|
||||
save_task(task)
|
||||
return f"Claimed {task_id} ({task.subject})"
|
||||
```
|
||||
|
||||
If the task is already claimed by someone else (`status != "pending"`), or dependencies aren't met (`can_start` returns False), the claim is rejected.
|
||||
|
||||
### complete_task: Complete and Unblock
|
||||
|
||||
When a task is done, set it to `completed`. Simultaneously scan all other tasks to find downstream tasks that were **just unblocked**:
|
||||
|
||||
```python
|
||||
def complete_task(task_id: str) -> str:
|
||||
task = load_task(task_id)
|
||||
task.status = "completed"
|
||||
save_task(task)
|
||||
# Find newly unblocked downstream tasks
|
||||
unblocked = [t.subject for t in list_tasks()
|
||||
if t.status == "pending" and t.blockedBy
|
||||
and can_start(t.id)]
|
||||
msg = f"Completed {task_id} ({task.subject})"
|
||||
if unblocked:
|
||||
msg += f"\nUnblocked: {', '.join(unblocked)}"
|
||||
return msg
|
||||
```
|
||||
|
||||
After completing "schema", `can_start` returns True for "endpoints" and "docs"; they can begin.
|
||||
|
||||
### get_task: View Full Details
|
||||
|
||||
`list_tasks` only shows a one-line summary. `get_task` returns the full task JSON, including description and dependency details. When recovering across sessions, the agent needs to read the full description to continue work:
|
||||
|
||||
```python
|
||||
def get_task(task_id: str) -> str:
|
||||
task = load_task(task_id)
|
||||
return json.dumps(asdict(task), indent=2)
|
||||
```
|
||||
|
||||
### State Machine: Two Actions, Three States
|
||||
|
||||
```
|
||||
pending ──claim──→ in_progress ──complete──→ completed
|
||||
```
|
||||
|
||||
Here `claim` / `complete` are actions, while `pending` / `in_progress` / `completed` are states:
|
||||
|
||||
- **claim_task**: `pending` → `in_progress`. Sets owner, begins work.
|
||||
- **complete_task**: `in_progress` → `completed`. Marks the task done and unblocks downstream.
|
||||
|
||||
CC has no `in_progress → pending` release path. If a teammate terminates or shuts down, CC unassigns its unfinished tasks (clears owner) and resets status to `pending`, allowing other agents to reclaim them. The teaching version omits this recovery path.
|
||||
|
||||
### Putting It Together
|
||||
|
||||
```python
|
||||
# Create tasks with dependencies
|
||||
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])
|
||||
|
||||
# Agent claims the first available task
|
||||
claim_task(schema.id) # ✓ Claimed (no dependencies)
|
||||
complete_task(schema.id) # ✓ Completed → unblocks endpoints, docs
|
||||
|
||||
claim_task(endpoints.id) # ✓ Claimed (schema completed)
|
||||
complete_task(endpoints.id) # ✓ Completed → unblocks tests
|
||||
|
||||
claim_task(docs.id) # ✓ Claimed (schema completed)
|
||||
complete_task(docs.id) # ✓ Completed
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Changes from s11
|
||||
|
||||
| Component | Before (s11) | After (s12) |
|
||||
|-----------|-------------|-------------|
|
||||
| Task management | None | Task dataclass + 5 tools |
|
||||
| New types | — | Task (id, subject, description, status, owner, blockedBy) |
|
||||
| Storage | No persistence | `.tasks/{id}.json` cross-session |
|
||||
| Dependencies | None | `blockedBy` graph + `can_start` check |
|
||||
| Tools | bash, read_file, write_file (3) | + create_task, list_tasks, get_task, claim_task, complete_task (8) |
|
||||
| Lifecycle | — | pending → in_progress → completed (no release rollback) |
|
||||
|
||||
---
|
||||
|
||||
## Try It
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s12_task_system/code.py
|
||||
```
|
||||
|
||||
Try these prompts:
|
||||
|
||||
1. `Create tasks: setup database schema, create API endpoints (depends on schema), write tests (depends on endpoints), write docs (depends on schema)`
|
||||
2. `List all tasks and their statuses`
|
||||
3. `Claim the first unblocked task and complete it`
|
||||
4. `List tasks again — which ones are now unblocked?`
|
||||
|
||||
What to observe: Are JSON files generated in the `.tasks/` directory? After completing a task, are the blocked tasks unblocked?
|
||||
|
||||
---
|
||||
|
||||
## What's Next
|
||||
|
||||
The task graph is in place. But some tasks take a long time — like running full test suites or deploying to a server. The agent calls the LLM billed by token, it can't afford to wait on a slow operation.
|
||||
|
||||
s13 Background Tasks → Slow operations go to the background. The agent continues processing other tasks, and gets notified when the background work is done.
|
||||
|
||||
<details>
|
||||
<summary>Deep Dive into CC Source</summary>
|
||||
|
||||
> The following is a complete analysis based on CC source code `utils/tasks.ts` (862 lines), `tools/TaskCreateTool/TaskCreateTool.ts` (138 lines), `tools/TaskUpdateTool/TaskUpdateTool.ts` (406 lines), `tools/TaskGetTool/TaskGetTool.ts` (128 lines), `tools/TaskListTool/TaskListTool.ts` (116 lines), `hooks/useTaskListWatcher.ts` (221 lines).
|
||||
|
||||
### 1. TaskRecord's Full Fields
|
||||
|
||||
The tutorial only covers id, subject, status, owner, blockedBy. CC actually has 9 fields (`utils/tasks.ts:76-89`):
|
||||
|
||||
| Field | Type | Purpose |
|
||||
|------|------|---------|
|
||||
| `id` | string | Incrementing integer ID |
|
||||
| `subject` | string | Short title |
|
||||
| `description` | string | Free-form description |
|
||||
| `activeForm` | string? | Present tense form, shown in spinner when in_progress |
|
||||
| `owner` | string? | Assigned agent ID |
|
||||
| `status` | pending/in_progress/completed | Lifecycle |
|
||||
| `blocks` | string[] | Task IDs blocked by this task (downstream) |
|
||||
| `blockedBy` | string[] | Task IDs blocking this task (upstream) |
|
||||
| `metadata` | Record? | Arbitrary extension key-value pairs |
|
||||
|
||||
Storage location: `~/.claude/tasks/{taskListId}/{id}.json`. One file per task.
|
||||
|
||||
### 2. Not a TodoWrite Upgrade — Two Independent Systems
|
||||
|
||||
In CC, Task System and TodoWrite **coexist**, toggled by `isTodoV2Enabled()` (`utils/tasks.ts:133`) — interactive sessions default to Task (V2), non-interactive/SDK sessions default to TodoWrite. The `CLAUDE_CODE_ENABLE_TASKS` env var can force-enable Task. Task has what TodoWrite lacks: file-lock concurrency protection, dependency enforcement, ownership, fs.watch reactive monitoring, lifecycle hooks.
|
||||
|
||||
### 3. Concurrent Claim Locking
|
||||
|
||||
`claimTask()` (`utils/tasks.ts:541-612`) uses dual locking to prevent races:
|
||||
|
||||
**Task file lock**: `proper-lockfile` locks `{taskId}.json` (up to 30 retries, exponential backoff 5-100ms). Inside the lock:
|
||||
1. Re-read task (prevent TOCTOU)
|
||||
2. Check already claimed by another → `already_claimed`
|
||||
3. Check already completed → `already_resolved`
|
||||
4. Check upstream not completed → `blocked`
|
||||
5. Set owner
|
||||
|
||||
**List-level lock** (agent busy check): `.lock` file, atomic scan of all tasks to check if the agent already has other open tasks.
|
||||
|
||||
Note: The teaching version combines claiming and starting work into one step (claim = set owner + in_progress); real CC's `claimTask` primarily resolves owner competition — it only sets owner without changing status. Status updates are handled by `TaskUpdate`.
|
||||
|
||||
### 4. High-Water Mark to Prevent ID Reuse
|
||||
|
||||
The `.highwatermark` file records the highest task ID ever assigned. Even if a task is deleted, its ID won't be reused.
|
||||
|
||||
### 5. Four Task Tools
|
||||
|
||||
CC's task system has four tools (not the tutorial's single generic Task tool): `TaskCreate`, `TaskGet`, `TaskUpdate`, `TaskList`. All set `isConcurrencySafe: true` and `shouldDefer: true` (tool schemas aren't in the initial prompt; only visible after ToolSearch).
|
||||
|
||||
The teaching version's `create_task(blockedBy=...)` declares dependencies at creation time, which is a reasonable simplification. Real CC's `TaskCreate` only accepts subject/description/activeForm/metadata — dependencies are maintained via `TaskUpdate`'s `addBlocks/addBlockedBy`.
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
280
s12_task_system/README.ja.md
Normal file
280
s12_task_system/README.ja.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# s12: Task System — 大きな目標を小さなタスクに分割
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → ... → s10 → s11 → `s12` → [s13](../s13_background_tasks/) → s14 → ... → s20
|
||||
|
||||
> *"大きな目標を小さなタスクに分け、順序付け、永続化"* — ファイル永続化タスクグラフ、マルチ Agent 協調の基盤。
|
||||
>
|
||||
> **Harness 層**: タスク — 永続化された目標、復旧可能な進捗。
|
||||
|
||||
---
|
||||
|
||||
## 課題
|
||||
|
||||
Agent がプロジェクトを受けた:データベース構築、API 実装、テスト追加。s05 の TodoWrite でリストを作り、まず API を書き始め、途中でデータベーステーブルがないことに気づいて戻る。テスト追加時に API インターフェースのシグネチャがまた変わっている...
|
||||
|
||||
屋根を先に建てて基礎を後から打つことはできない。タスクには順序がある。タスクの依存関係は有向非巡回グラフ(DAG)を形成すべき;教学版は `blockedBy` チェックのみをデモし、循環検出は実装していない。
|
||||
|
||||
s05 の TodoWrite はリスト。依存関係も永続化もなく、会話が終わればリストも消える。必要なのは**タスクシステム**:各タスクは JSON ファイル、タスク間に `blockedBy` 依存関係、ディスク上でセッションをまたいで永続化。
|
||||
|
||||
---
|
||||
|
||||
## ソリューション
|
||||
|
||||

|
||||
|
||||
教学版は基本 agent loop を維持し、タスクシステムに集中するため S11 の完全なエラーリカバリ(RecoveryState、バックオフ、エスカレーション、reactive compact、フォールバックモデル)を省略。追加:5 つの新規タスクツール + `.tasks/` ディレクトリによる永続化 + `blockedBy` 依存チェック。タスクシステムとエラーリカバリは独立したレイヤー:CC ソースコードでは `utils/tasks.ts` は CRUD のみ、`query.ts` の with_retry/RecoveryState がエラーリカバリを担当し、互いに非結合。
|
||||
|
||||
TodoWrite vs Task System:
|
||||
|
||||
| | TodoWrite (s05) | Task System (s12) |
|
||||
|---|---|---|
|
||||
| ストレージ | メモリ内リスト | `.tasks/` JSON ファイル |
|
||||
| 依存関係 | なし | `blockedBy` 依存グラフ |
|
||||
| 永続性 | 会話終了で消失 | セッション横断 |
|
||||
| マルチ Agent | なし | `owner` フィールド |
|
||||
| ステータス | checked / unchecked | pending → in_progress → completed |
|
||||
|
||||
---
|
||||
|
||||
## 仕組み
|
||||
|
||||

|
||||
|
||||
### Task: データ構造
|
||||
|
||||
各タスクは JSON ファイル、`.tasks/` ディレクトリに保存:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Task:
|
||||
id: str
|
||||
subject: str
|
||||
description: str
|
||||
status: str # pending | in_progress | completed
|
||||
owner: str | None # Agent 名(マルチ Agent シナリオ)
|
||||
blockedBy: list[str] # 依存タスク ID のリスト
|
||||
```
|
||||
|
||||
ID は `timestamp + random hex` で生成、シンプルだが十分。CC は順次 ID + highwatermark ファイルで ID 再利用を防止する、より厳密な設計。
|
||||
|
||||
### create_task: タスク作成
|
||||
|
||||
```python
|
||||
def create_task(subject: str, description: str = "",
|
||||
blockedBy: list[str] | None = None) -> Task:
|
||||
task = Task(
|
||||
id=f"task_{int(time.time())}_{random_hex(4)}",
|
||||
subject=subject, description=description,
|
||||
status="pending", owner=None,
|
||||
blockedBy=blockedBy or [],
|
||||
)
|
||||
save_task(task)
|
||||
return task
|
||||
```
|
||||
|
||||
作成時に自動的に `save_task` で `.tasks/{id}.json` に書き込み。`blockedBy` で依存を宣言、例えば "API を書く" の `blockedBy` は `["task_schema"]`。
|
||||
|
||||
### can_start: 依存チェック
|
||||
|
||||
タスクは `blockedBy` が**すべて completed** になってからでないと開始できない:
|
||||
|
||||
```python
|
||||
def can_start(task_id: str) -> bool:
|
||||
task = load_task(task_id)
|
||||
for dep_id in task.blockedBy:
|
||||
if not _task_path(dep_id).exists():
|
||||
return False # missing dependency = blocked
|
||||
dep = load_task(dep_id)
|
||||
if dep.status != "completed":
|
||||
return False
|
||||
return True
|
||||
```
|
||||
|
||||
`can_start` は `claim_task` の事前チェック:`blockedBy` に一つでも completed でないものがあれば、認識不可。存在しない依存は blocked として扱い、誤った ID 参照時のクラッシュを防ぐ。
|
||||
|
||||
### claim_task: タスク認識
|
||||
|
||||
Agent がタスクに取り掛かる時、`claim_task` を呼び出し:`owner` を設定、ステータスを `pending` → `in_progress` に変更。`owner` フィールドは誰が作業中かを記録し、マルチ Agent シナリオで重複認識を防止:
|
||||
|
||||
```python
|
||||
def claim_task(task_id: str, owner: str = "agent") -> str:
|
||||
task = load_task(task_id)
|
||||
if task.status != "pending":
|
||||
return f"Task {task_id} is {task.status}, cannot claim"
|
||||
if not can_start(task_id):
|
||||
deps = [d for d in task.blockedBy
|
||||
if load_task(d).status != "completed"]
|
||||
return f"Blocked by: {deps}"
|
||||
task.owner = owner
|
||||
task.status = "in_progress"
|
||||
save_task(task)
|
||||
return f"Claimed {task_id} ({task.subject})"
|
||||
```
|
||||
|
||||
タスクが既に他者に認識されている(`status != "pending"`)、または依存が未完了(`can_start` が False)の場合、認識を拒否。
|
||||
|
||||
### complete_task: 完了とアンロック
|
||||
|
||||
タスク完了後、`completed` に設定。同時に他の全タスクを走査し、**直前にアンロックされた**下流タスクを特定:
|
||||
|
||||
```python
|
||||
def complete_task(task_id: str) -> str:
|
||||
task = load_task(task_id)
|
||||
task.status = "completed"
|
||||
save_task(task)
|
||||
# アンロックされた下流タスクを検索
|
||||
unblocked = [t.subject for t in list_tasks()
|
||||
if t.status == "pending" and t.blockedBy
|
||||
and can_start(t.id)]
|
||||
msg = f"Completed {task_id} ({task.subject})"
|
||||
if unblocked:
|
||||
msg += f"\nUnblocked: {', '.join(unblocked)}"
|
||||
return msg
|
||||
```
|
||||
|
||||
"schema" 完了後、"endpoints" と "docs" の `can_start` が True を返し、開始可能になる。
|
||||
|
||||
### get_task: 完全な詳細を確認
|
||||
|
||||
`list_tasks` は 1 行サマリのみ表示。`get_task` は description と依存関係の詳細を含む完全なタスク JSON を返す。セッションをまたいで復旧する際、Agent は完全な説明を読んで作業を継続する必要がある:
|
||||
|
||||
```python
|
||||
def get_task(task_id: str) -> str:
|
||||
task = load_task(task_id)
|
||||
return json.dumps(asdict(task), indent=2)
|
||||
```
|
||||
|
||||
### 状態マシン: 2 つのアクション、3 つの状態
|
||||
|
||||
```
|
||||
pending ──claim──→ in_progress ──complete──→ completed
|
||||
```
|
||||
|
||||
ここで `claim` / `complete` はアクション、`pending` / `in_progress` / `completed` は状態:
|
||||
|
||||
- **claim_task**: `pending` → `in_progress`。owner を設定し、作業を開始。
|
||||
- **complete_task**: `in_progress` → `completed`。タスクを完了済みにし、下流をアンロック。
|
||||
|
||||
CC には `in_progress → pending` の release パスがない。teammate が終了または shutdown した場合、CC は未完了タスクの owner をクリアし、status を `pending` にリセットし、他の agent が再認識できるようにする。教学版はこの復旧パスを省略。
|
||||
|
||||
### 組み合わせて実行
|
||||
|
||||
```python
|
||||
# 依存関係のあるタスクを作成
|
||||
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])
|
||||
|
||||
# Agent が最初に実行可能なタスクを認識
|
||||
claim_task(schema.id) # ✓ Claimed(依存なし)
|
||||
complete_task(schema.id) # ✓ Completed → endpoints, docs をアンロック
|
||||
|
||||
claim_task(endpoints.id) # ✓ Claimed(schema 完了済み)
|
||||
complete_task(endpoints.id) # ✓ Completed → tests をアンロック
|
||||
|
||||
claim_task(docs.id) # ✓ Claimed(schema 完了済み)
|
||||
complete_task(docs.id) # ✓ Completed
|
||||
|
||||
claim_task(tests.id) # ✓ Claimed(endpoints 完了済み)
|
||||
complete_task(tests.id) # ✓ Completed
|
||||
```
|
||||
|
||||
各 `create_task` が JSON ファイルを書き込み、各 `claim_task` / `complete_task` がファイルを更新。セッションをまたいでも `.tasks/` ディレクトリが残り、Agent はファイルを読んで進捗を復旧。
|
||||
|
||||
---
|
||||
|
||||
## s11 からの変更
|
||||
|
||||
| コンポーネント | 変更前 (s11) | 変更後 (s12) |
|
||||
|--------------|------------|------------|
|
||||
| タスク管理 | なし | Task dataclass + 5 ツール |
|
||||
| 新規型 | — | Task(id, subject, description, status, owner, blockedBy) |
|
||||
| ストレージ | 永続化なし | `.tasks/{id}.json` セッション横断 |
|
||||
| 依存関係 | なし | `blockedBy` グラフ + `can_start` チェック |
|
||||
| ツール | bash, read_file, write_file (3) | + create_task, list_tasks, get_task, claim_task, complete_task (8) |
|
||||
| ライフサイクル | — | pending → in_progress → completed(release ロールバックなし) |
|
||||
|
||||
---
|
||||
|
||||
## 試してみる
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s12_task_system/code.py
|
||||
```
|
||||
|
||||
以下のプロンプトを試してください:
|
||||
|
||||
1. `Create tasks: setup database schema, create API endpoints (depends on schema), write tests (depends on endpoints), write docs (depends on schema)`
|
||||
2. `List all tasks and their statuses`
|
||||
3. `Claim the first unblocked task and complete it`
|
||||
4. `List tasks again — which ones are now unblocked?`
|
||||
|
||||
観察ポイント:`.tasks/` ディレクトリに JSON ファイルが生成されているか?タスク完了後、ブロックされていたタスクがアンロックされているか?
|
||||
|
||||
---
|
||||
|
||||
## 次の章
|
||||
|
||||
タスクグラフができた。しかし、一部のタスクは長時間かかる — 全テスト実行やサーバーデプロイなど。Agent は LLM をトークン課金で呼び出しており、遅い操作を待つ余裕はない。
|
||||
|
||||
s13 Background Tasks → 遅い操作はバックグラウンドへ。Agent は他のタスクの処理を続け、バックグラウンドの完了を通知で受け取る。
|
||||
|
||||
<details>
|
||||
<summary>CC ソースコード深掘り</summary>
|
||||
|
||||
> 以下は CC ソースコード `utils/tasks.ts`(862 行)、`tools/TaskCreateTool/TaskCreateTool.ts`(138 行)、`tools/TaskUpdateTool/TaskUpdateTool.ts`(406 行)、`tools/TaskGetTool/TaskGetTool.ts`(128 行)、`tools/TaskListTool/TaskListTool.ts`(116 行)、`hooks/useTaskListWatcher.ts`(221 行)の完全分析に基づく。
|
||||
|
||||
### 一、TaskRecord の完全フィールド
|
||||
|
||||
チュートリアルでは id、subject、status、owner、blockedBy のみ解説。CC は実際に 9 フィールドを持つ(`utils/tasks.ts:76-89`):
|
||||
|
||||
| フィールド | 型 | 用途 |
|
||||
|------|------|------|
|
||||
| `id` | string | 昇順整数 ID |
|
||||
| `subject` | string | 短いタイトル |
|
||||
| `description` | string | 自由形式の説明 |
|
||||
| `activeForm` | string? | 現在進行形、in_progress 時にスピナーに表示 |
|
||||
| `owner` | string? | 割り当てられた agent ID |
|
||||
| `status` | pending/in_progress/completed | ライフサイクル |
|
||||
| `blocks` | string[] | このタスクがブロックするタスク ID(下流) |
|
||||
| `blockedBy` | string[] | このタスクをブロックするタスク ID(上流) |
|
||||
| `metadata` | Record? | 任意の拡張キーバリューペア |
|
||||
|
||||
保存場所:`~/.claude/tasks/{taskListId}/{id}.json`。タスクごとに 1 ファイル。
|
||||
|
||||
### 二、TodoWrite のアップグレードではなく、2 つの独立システム
|
||||
|
||||
CC では Task System と TodoWrite **は共存**し、`isTodoV2Enabled()` で切り替え(`utils/tasks.ts:133`)— 対話セッションはデフォルトで Task (V2)、非対話/SDK セッションは TodoWrite。環境変数 `CLAUDE_CODE_ENABLE_TASKS` で Task を強制有効化可能。Task は TodoWrite にない機能を持つ:ファイルロック並行保護、依存関係強制、ownership、fs.watch リアクティブ監視、ライフサイクルフック。
|
||||
|
||||
### 三、並行認識のロック機構
|
||||
|
||||
`claimTask()`(`utils/tasks.ts:541-612`)は二重ロックで競合を防止:
|
||||
|
||||
**タスクファイルロック**:`proper-lockfile` で `{taskId}.json` をロック(最大 30 リトライ、指数バックオフ 5-100ms)。ロック内:
|
||||
1. タスクを再読込(TOCTOU 防止)
|
||||
2. 既に他者が認識済み → `already_claimed`
|
||||
3. 既に完了済み → `already_resolved`
|
||||
4. 上流が未完了 → `blocked`
|
||||
5. owner を設定
|
||||
|
||||
**リストレベルロック**(agent busy チェック時):`.lock` ファイル、全タスクを原子的に走査し該当 agent が他の open task を持つか確認。
|
||||
|
||||
注意:教学版は認識と作業開始を 1 ステップに統合(claim = owner 設定 + in_progress);実際の CC の `claimTask` は主に owner 競合を解決し、owner のみを設定して status は変更しない。status の更新は `TaskUpdate` が担当。
|
||||
|
||||
### 四、高水位標による ID 再利用防止
|
||||
|
||||
`.highwatermark` ファイルが過去に割り当てられた最大タスク ID を記録。タスクが削除されても ID は再利用されない。
|
||||
|
||||
### 五、4 つの Task ツール
|
||||
|
||||
CC のタスクシステムは 4 つのツールを持つ(チュートリアルの汎用 Task ツールとは異なる):`TaskCreate`、`TaskGet`、`TaskUpdate`、`TaskList`。すべて `isConcurrencySafe: true` と `shouldDefer: true` が設定(ツールスキーマは初期プロンプトに含まれず、ToolSearch 後にのみ可視)。
|
||||
|
||||
教学版の `create_task(blockedBy=...)` は作成時に直接依存を宣言する合理な簡略化。実際の CC の `TaskCreate` は subject/description/activeForm/metadata のみを受け付け、依存関係は `TaskUpdate` の `addBlocks/addBlockedBy` で管理される。
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
280
s12_task_system/README.md
Normal file
280
s12_task_system/README.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# s12: Task System — 目标太大,拆成小任务
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → ... → s10 → s11 → `s12` → [s13](../s13_background_tasks/) → s14 → ... → s20
|
||||
|
||||
> *"大目标拆成小任务, 排好序, 持久化"* — 文件持久化的任务图, 多 agent 协作的基础。
|
||||
>
|
||||
> **Harness 层**: 任务 — 持久化的目标, 可恢复的进度。
|
||||
|
||||
---
|
||||
|
||||
## 问题
|
||||
|
||||
Agent 接到一个项目:搭数据库、写 API、加测试。它用 s05 的 TodoWrite 列了一张清单,然后开始写 API,写到一半发现没数据库表,回头补;加测试时发现 API 接口签名又变了...
|
||||
|
||||
盖房子不能先盖屋顶再打地基。任务之间有先后。任务依赖应该形成有向无环图(DAG);教学版只演示 `blockedBy` 检查,没有实现环检测。
|
||||
|
||||
s05 的 TodoWrite 是一个列表。没有依赖关系、没有持久化、对话结束列表就没了。你需要的是**任务系统**:每个任务是一个 JSON 文件,任务之间有 `blockedBy` 依赖,跨会话持久化在磁盘上。
|
||||
|
||||
---
|
||||
|
||||
## 解决方案
|
||||
|
||||

|
||||
|
||||
教学代码保留基础 agent loop,为聚焦任务系统省略了 S11 的完整错误恢复(RecoveryState、退避、升级、reactive compact、fallback model)。新增 5 个任务工具 + `.tasks/` 目录持久化 + `blockedBy` 依赖检查。任务系统与错误恢复是独立层:CC 源码中 `utils/tasks.ts` 只管 CRUD,`query.ts` 的 with_retry/RecoveryState 管错误恢复,互不耦合。
|
||||
|
||||
TodoWrite vs Task System:
|
||||
|
||||
| | TodoWrite (s05) | Task System (s12) |
|
||||
|---|---|---|
|
||||
| 存储 | 内存列表 | `.tasks/` JSON 文件 |
|
||||
| 依赖 | 无 | `blockedBy` 依赖图 |
|
||||
| 持久性 | 对话结束即丢 | 跨会话 |
|
||||
| 多 Agent | 无 | `owner` 字段 |
|
||||
| 状态 | checked / unchecked | pending → in_progress → completed |
|
||||
|
||||
---
|
||||
|
||||
## 工作原理
|
||||
|
||||

|
||||
|
||||
### Task: 数据结构
|
||||
|
||||
每个任务是一个 JSON 文件,存于 `.tasks/` 目录:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Task:
|
||||
id: str
|
||||
subject: str
|
||||
description: str
|
||||
status: str # pending | in_progress | completed
|
||||
owner: str | None # Agent 名(多 Agent 场景)
|
||||
blockedBy: list[str] # 依赖的任务 ID 列表
|
||||
```
|
||||
|
||||
ID 用 `timestamp + random hex` 生成,简单但够用。CC 用顺序 ID + highwatermark 文件防止 ID 重用,是更严谨的设计。
|
||||
|
||||
### create_task: 创建任务
|
||||
|
||||
```python
|
||||
def create_task(subject: str, description: str = "",
|
||||
blockedBy: list[str] | None = None) -> Task:
|
||||
task = Task(
|
||||
id=f"task_{int(time.time())}_{random_hex(4)}",
|
||||
subject=subject, description=description,
|
||||
status="pending", owner=None,
|
||||
blockedBy=blockedBy or [],
|
||||
)
|
||||
save_task(task)
|
||||
return task
|
||||
```
|
||||
|
||||
创建时自动 `save_task` 到 `.tasks/{id}.json`。`blockedBy` 声明依赖,比如 "写 API" 的 `blockedBy` 是 `["task_schema"]`。
|
||||
|
||||
### can_start: 依赖检查
|
||||
|
||||
一个任务只能在它的 `blockedBy` **全部 completed** 之后才能开始:
|
||||
|
||||
```python
|
||||
def can_start(task_id: str) -> bool:
|
||||
task = load_task(task_id)
|
||||
for dep_id in task.blockedBy:
|
||||
if not _task_path(dep_id).exists():
|
||||
return False # missing dependency = blocked
|
||||
dep = load_task(dep_id)
|
||||
if dep.status != "completed":
|
||||
return False
|
||||
return True
|
||||
```
|
||||
|
||||
`can_start` 是 `claim_task` 的前置检查:`blockedBy` 里有任何一个不是 completed,就不能认领。不存在的依赖视为 blocked,避免引用错误 ID 时崩溃。
|
||||
|
||||
### claim_task: 认领任务
|
||||
|
||||
Agent 开始做一个任务时,调用 `claim_task`:设置 `owner`,状态从 `pending` → `in_progress`。`owner` 字段记录谁在做这个任务,多 Agent 场景下防止重复认领:
|
||||
|
||||
```python
|
||||
def claim_task(task_id: str, owner: str = "agent") -> str:
|
||||
task = load_task(task_id)
|
||||
if task.status != "pending":
|
||||
return f"Task {task_id} is {task.status}, cannot claim"
|
||||
if not can_start(task_id):
|
||||
deps = [d for d in task.blockedBy
|
||||
if load_task(d).status != "completed"]
|
||||
return f"Blocked by: {deps}"
|
||||
task.owner = owner
|
||||
task.status = "in_progress"
|
||||
save_task(task)
|
||||
return f"Claimed {task_id} ({task.subject})"
|
||||
```
|
||||
|
||||
如果任务已被别人认领(`status != "pending"`),或者依赖没完成(`can_start` 返回 False),拒绝认领。
|
||||
|
||||
### complete_task: 完成与解锁
|
||||
|
||||
任务做完后,设为 `completed`。同时扫描所有其他任务,找出**刚刚被解锁**的下游任务:
|
||||
|
||||
```python
|
||||
def complete_task(task_id: str) -> str:
|
||||
task = load_task(task_id)
|
||||
task.status = "completed"
|
||||
save_task(task)
|
||||
# 找出被解锁的下游任务
|
||||
unblocked = [t.subject for t in list_tasks()
|
||||
if t.status == "pending" and t.blockedBy
|
||||
and can_start(t.id)]
|
||||
msg = f"Completed {task_id} ({task.subject})"
|
||||
if unblocked:
|
||||
msg += f"\nUnblocked: {', '.join(unblocked)}"
|
||||
return msg
|
||||
```
|
||||
|
||||
完成 "schema" 后,"endpoints" 和 "docs" 的 `can_start` 返回 True,它们可以开始。
|
||||
|
||||
### get_task: 查看完整细节
|
||||
|
||||
`list_tasks` 只显示一行摘要。`get_task` 返回完整的任务 JSON,包括 description 和依赖细节。跨会话恢复时,Agent 需要读取完整描述才能继续工作:
|
||||
|
||||
```python
|
||||
def get_task(task_id: str) -> str:
|
||||
task = load_task(task_id)
|
||||
return json.dumps(asdict(task), indent=2)
|
||||
```
|
||||
|
||||
### 状态机: 两个动作,三个状态
|
||||
|
||||
```
|
||||
pending ──claim──→ in_progress ──complete──→ completed
|
||||
```
|
||||
|
||||
这里的 `claim` / `complete` 是动作,`pending` / `in_progress` / `completed` 是状态:
|
||||
|
||||
- **claim_task**: `pending` → `in_progress`。设置 owner,开始工作。
|
||||
- **complete_task**: `in_progress` → `completed`。把任务标记为完成,并解锁下游。
|
||||
|
||||
CC 没有 `in_progress → pending` 的 release 路径。如果 teammate 终止或 shutdown,CC 会把它未完成的任务 unassign(清除 owner),并将 status 重置为 `pending`,方便其他 agent 重新认领。教学版省略了这一恢复路径。
|
||||
|
||||
### 合起来跑
|
||||
|
||||
```python
|
||||
# 创建有依赖的任务
|
||||
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])
|
||||
|
||||
# Agent 认领第一个可做的任务
|
||||
claim_task(schema.id) # ✓ Claimed (无依赖)
|
||||
complete_task(schema.id) # ✓ Completed → 解锁 endpoints, docs
|
||||
|
||||
claim_task(endpoints.id) # ✓ Claimed (schema 已完成)
|
||||
complete_task(endpoints.id) # ✓ Completed → 解锁 tests
|
||||
|
||||
claim_task(docs.id) # ✓ Claimed (schema 已完成)
|
||||
complete_task(docs.id) # ✓ Completed
|
||||
|
||||
claim_task(tests.id) # ✓ Claimed (endpoints 已完成)
|
||||
complete_task(tests.id) # ✓ Completed
|
||||
```
|
||||
|
||||
每个 `create_task` 写一个 JSON 文件,每个 `claim_task` / `complete_task` 更新文件。跨会话时,`.tasks/` 目录还在,Agent 读文件就能恢复进度。
|
||||
|
||||
---
|
||||
|
||||
## 相对 s11 的变更
|
||||
|
||||
| 组件 | 之前 (s11) | 之后 (s12) |
|
||||
|------|-----------|-----------|
|
||||
| 任务管理 | 无 | Task dataclass + 5 个工具 |
|
||||
| 新类型 | — | Task(id, subject, description, status, owner, blockedBy) |
|
||||
| 存储 | 无持久化 | `.tasks/{id}.json` 跨会话 |
|
||||
| 依赖 | 无 | `blockedBy` 图 + `can_start` 检查 |
|
||||
| 工具 | bash, read_file, write_file (3) | + create_task, list_tasks, get_task, claim_task, complete_task (8) |
|
||||
| 生命周期 | — | pending → in_progress → completed(无 release 回退) |
|
||||
|
||||
---
|
||||
|
||||
## 试一下
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s12_task_system/code.py
|
||||
```
|
||||
|
||||
试试这些 prompt:
|
||||
|
||||
1. `Create tasks: setup database schema, create API endpoints (depends on schema), write tests (depends on endpoints), write docs (depends on schema)`
|
||||
2. `List all tasks and their statuses`
|
||||
3. `Claim the first unblocked task and complete it`
|
||||
4. `List tasks again — which ones are now unblocked?`
|
||||
|
||||
观察重点:`.tasks/` 目录下是否生成了 JSON 文件?完成任务后,被阻塞的任务是否解锁?
|
||||
|
||||
---
|
||||
|
||||
## 接下来
|
||||
|
||||
任务图有了。但有些任务要跑很久——比如全量测试、部署到服务器。Agent 调 LLM 按量计费,不能干等一个慢操作。
|
||||
|
||||
s13 Background Tasks → 慢操作放后台。Agent 继续处理其他任务,后台跑完了通知它。
|
||||
|
||||
<details>
|
||||
<summary>深入 CC 源码</summary>
|
||||
|
||||
> 以下基于 CC 源码 `utils/tasks.ts`(862 行)、`tools/TaskCreateTool/TaskCreateTool.ts`(138 行)、`tools/TaskUpdateTool/TaskUpdateTool.ts`(406 行)、`tools/TaskGetTool/TaskGetTool.ts`(128 行)、`tools/TaskListTool/TaskListTool.ts`(116 行)、`hooks/useTaskListWatcher.ts`(221 行)的分析。
|
||||
|
||||
### 一、TaskRecord 的完整字段
|
||||
|
||||
教学版只讲了 id、subject、status、owner、blockedBy。CC 实际有 9 个字段(`utils/tasks.ts:76-89`):
|
||||
|
||||
| 字段 | 类型 | 用途 |
|
||||
|------|------|------|
|
||||
| `id` | string | 递增整数 ID |
|
||||
| `subject` | string | 简短标题 |
|
||||
| `description` | string | 自由格式描述 |
|
||||
| `activeForm` | string? | 进行时态,in_progress 时在 spinner 显示 |
|
||||
| `owner` | string? | 分配的 agent ID |
|
||||
| `status` | pending/in_progress/completed | 生命周期 |
|
||||
| `blocks` | string[] | 此任务阻塞的任务 ID(下游) |
|
||||
| `blockedBy` | string[] | 阻塞此任务的任务 ID(上游) |
|
||||
| `metadata` | Record? | 任意扩展键值对 |
|
||||
|
||||
存储位置:`~/.claude/tasks/{taskListId}/{id}.json`。每个任务一个文件。
|
||||
|
||||
### 二、不是 TodoWrite 的升级,是两个独立系统
|
||||
|
||||
CC 中 Task System 和 TodoWrite **同时存在**,通过 `isTodoV2Enabled()` 切换(`utils/tasks.ts:133`)——交互式会话默认启用 Task(V2),非交互式/SDK 默认用 TodoWrite。环境变量 `CLAUDE_CODE_ENABLE_TASKS` 可强制启用 Task。Task 有 TodoWrite 没有的:文件锁并发保护、依赖强制执行、ownership、fs.watch 响应式监听、生命周期 hooks。
|
||||
|
||||
### 三、并发认领的锁机制
|
||||
|
||||
`claimTask()`(`utils/tasks.ts:541-612`)用双重锁防竞争:
|
||||
|
||||
**任务文件锁**:`proper-lockfile` 锁住 `{taskId}.json`(最多重试 30 次,指数退避 5-100ms)。锁内:
|
||||
1. 重新读取任务(防 TOCTOU)
|
||||
2. 检查已被他人认领 → `already_claimed`
|
||||
3. 检查已完成 → `already_resolved`
|
||||
4. 检查上游未完成 → `blocked`
|
||||
5. 设置 owner
|
||||
|
||||
**列表级锁**(agent busy 检查时):`.lock` 文件,原子性扫描所有任务并检查该 agent 是否已有其他 open task。
|
||||
|
||||
注意:教学版把 claim 和开始工作合成一步(claim = set owner + in_progress);真实 CC 的 `claimTask` 主要解决 owner 竞争,只设 owner 不改 status,状态更新由 `TaskUpdate` 完成。
|
||||
|
||||
### 四、高水位标防 ID 重用
|
||||
|
||||
`.highwatermark` 文件记录曾分配过的最高任务 ID。即使任务被删除,ID 也不会被重用。
|
||||
|
||||
### 五、四个 Task 工具
|
||||
|
||||
CC 的任务系统有四个工具(不是教学版的一个通用 Task 工具):`TaskCreate`、`TaskGet`、`TaskUpdate`、`TaskList`。全部设置 `isConcurrencySafe: true` 和 `shouldDefer: true`(工具 schema 不在初始 prompt 中,需 ToolSearch 后才可见)。
|
||||
|
||||
教学版的 `create_task(blockedBy=...)` 在创建时直接声明依赖,是合理简化。真实 CC 的 `TaskCreate` 只接受 subject/description/activeForm/metadata,依赖关系由 `TaskUpdate` 的 `addBlocks/addBlockedBy` 维护。
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v0, ja@v0 -->
|
||||
376
s12_task_system/code.py
Normal file
376
s12_task_system/code.py
Normal file
@@ -0,0 +1,376 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
s12: Task System — file-persisted task graph with blockedBy dependencies.
|
||||
|
||||
Run: python s12_task_system/code.py
|
||||
Need: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY
|
||||
|
||||
Changes from s11:
|
||||
- Task dataclass (id, subject, description, status, owner, blockedBy)
|
||||
- TASKS_DIR = .tasks/ for persistent JSON storage
|
||||
- create_task / save_task / load_task / list_tasks / get_task
|
||||
- can_start: checks blockedBy all completed (missing deps = blocked)
|
||||
- claim_task: set owner + pending -> in_progress
|
||||
- complete_task: set completed + report unblocked downstream
|
||||
- 5 new tools: create_task, list_tasks, get_task, claim_task, complete_task
|
||||
|
||||
Note: Teaching code keeps a basic agent loop to stay focused on the task
|
||||
system. S11's full error recovery (RecoveryState, backoff, escalation,
|
||||
reactive compact, fallback model) is omitted — in real CC, tasks.ts and
|
||||
withRetry are independent layers that compose naturally.
|
||||
"""
|
||||
|
||||
import os, subprocess, json, time, random
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
try:
|
||||
import readline
|
||||
readline.parse_and_bind('set bind-tty-special-chars off')
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from anthropic import Anthropic
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv(override=True)
|
||||
if os.getenv("ANTHROPIC_BASE_URL"):
|
||||
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
|
||||
|
||||
WORKDIR = Path.cwd()
|
||||
MEMORY_DIR = WORKDIR / ".memory"
|
||||
MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
|
||||
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
|
||||
MODEL = os.environ["MODEL_ID"]
|
||||
|
||||
# ── Task System ──
|
||||
|
||||
TASKS_DIR = WORKDIR / ".tasks"
|
||||
TASKS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Task:
|
||||
id: str
|
||||
subject: str
|
||||
description: str
|
||||
status: str # pending | in_progress | completed
|
||||
owner: str | None # Agent name (multi-agent scenarios)
|
||||
blockedBy: list[str] # Dependency task IDs
|
||||
|
||||
|
||||
def _task_path(task_id: str) -> Path:
|
||||
return TASKS_DIR / f"{task_id}.json"
|
||||
|
||||
|
||||
def create_task(subject: str, description: str = "",
|
||||
blockedBy: list[str] | None = None) -> Task:
|
||||
task = Task(
|
||||
id=f"task_{int(time.time())}_{random.randint(0, 9999):04d}",
|
||||
subject=subject,
|
||||
description=description,
|
||||
status="pending",
|
||||
owner=None,
|
||||
blockedBy=blockedBy or [],
|
||||
)
|
||||
save_task(task)
|
||||
return task
|
||||
|
||||
|
||||
def save_task(task: Task):
|
||||
_task_path(task.id).write_text(json.dumps(asdict(task), indent=2))
|
||||
|
||||
|
||||
def load_task(task_id: str) -> Task:
|
||||
return Task(**json.loads(_task_path(task_id).read_text()))
|
||||
|
||||
|
||||
def list_tasks() -> list[Task]:
|
||||
return [Task(**json.loads(p.read_text()))
|
||||
for p in sorted(TASKS_DIR.glob("task_*.json"))]
|
||||
|
||||
|
||||
def get_task(task_id: str) -> str:
|
||||
"""Return full task details as JSON."""
|
||||
task = load_task(task_id)
|
||||
return json.dumps(asdict(task), indent=2)
|
||||
|
||||
|
||||
def can_start(task_id: str) -> bool:
|
||||
"""Check if all blockedBy dependencies are completed.
|
||||
Missing dependencies are treated as blocked."""
|
||||
task = load_task(task_id)
|
||||
for dep_id in task.blockedBy:
|
||||
if not _task_path(dep_id).exists():
|
||||
return False
|
||||
if load_task(dep_id).status != "completed":
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def claim_task(task_id: str, owner: str = "agent") -> str:
|
||||
task = load_task(task_id)
|
||||
if task.status != "pending":
|
||||
return f"Task {task_id} is {task.status}, cannot claim"
|
||||
if not can_start(task_id):
|
||||
deps = [d for d in task.blockedBy
|
||||
if not _task_path(d).exists() or load_task(d).status != "completed"]
|
||||
return f"Blocked by: {deps}"
|
||||
task.owner = owner
|
||||
task.status = "in_progress"
|
||||
save_task(task)
|
||||
print(f" \033[36m[claim] {task.subject} → in_progress (owner: {owner})\033[0m")
|
||||
return f"Claimed {task.id} ({task.subject})"
|
||||
|
||||
|
||||
def complete_task(task_id: str) -> str:
|
||||
task = load_task(task_id)
|
||||
if task.status != "in_progress":
|
||||
return f"Task {task_id} is {task.status}, cannot complete"
|
||||
task.status = "completed"
|
||||
save_task(task)
|
||||
unblocked = [t.subject for t in list_tasks()
|
||||
if t.status == "pending" and t.blockedBy and can_start(t.id)]
|
||||
print(f" \033[32m[complete] {task.subject} ✓\033[0m")
|
||||
msg = f"Completed {task.id} ({task.subject})"
|
||||
if unblocked:
|
||||
msg += f"\nUnblocked: {', '.join(unblocked)}"
|
||||
print(f" \033[33m[unblocked] {', '.join(unblocked)}\033[0m")
|
||||
return msg
|
||||
|
||||
|
||||
# ── Prompt Assembly (from s10, synced) ──
|
||||
|
||||
PROMPT_SECTIONS = {
|
||||
"identity": "You are a coding agent. Act, don't explain.",
|
||||
"tools": "Available tools: bash, read_file, write_file, "
|
||||
"create_task, list_tasks, get_task, claim_task, complete_task.",
|
||||
"workspace": f"Working directory: {WORKDIR}",
|
||||
"memory": "Relevant memories are injected below when available.",
|
||||
}
|
||||
|
||||
|
||||
def assemble_system_prompt(context: dict) -> str:
|
||||
sections = [PROMPT_SECTIONS["identity"],
|
||||
PROMPT_SECTIONS["tools"],
|
||||
PROMPT_SECTIONS["workspace"]]
|
||||
memories = context.get("memories", "")
|
||||
if memories:
|
||||
sections.append(f"Relevant memories:\n{memories}")
|
||||
return "\n\n".join(sections)
|
||||
|
||||
|
||||
_last_context_key, _last_prompt = None, None
|
||||
|
||||
|
||||
def get_system_prompt(context: dict) -> str:
|
||||
global _last_context_key, _last_prompt
|
||||
key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
|
||||
if key == _last_context_key and _last_prompt:
|
||||
return _last_prompt
|
||||
_last_context_key = key
|
||||
_last_prompt = assemble_system_prompt(context)
|
||||
return _last_prompt
|
||||
|
||||
|
||||
# ── Tools ──
|
||||
|
||||
def safe_path(p: str) -> Path:
|
||||
path = (WORKDIR / p).resolve()
|
||||
if not path.is_relative_to(WORKDIR):
|
||||
raise ValueError(f"Path escapes workspace: {p}")
|
||||
return path
|
||||
|
||||
|
||||
def run_bash(command: str) -> str:
|
||||
try:
|
||||
r = subprocess.run(command, shell=True, cwd=WORKDIR,
|
||||
capture_output=True, text=True, timeout=120)
|
||||
out = (r.stdout + r.stderr).strip()
|
||||
return out[:50000] if out else "(no output)"
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Error: Timeout (120s)"
|
||||
|
||||
|
||||
def run_read(path: str, limit: int | None = None) -> str:
|
||||
try:
|
||||
lines = safe_path(path).read_text().splitlines()
|
||||
if limit and limit < len(lines):
|
||||
lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
|
||||
return "\n".join(lines)
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
def run_write(path: str, content: str) -> str:
|
||||
try:
|
||||
fp = safe_path(path)
|
||||
fp.parent.mkdir(parents=True, exist_ok=True)
|
||||
fp.write_text(content)
|
||||
return f"Wrote {len(content)} bytes to {path}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
# 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_list_tasks() -> str:
|
||||
tasks = list_tasks()
|
||||
if not tasks:
|
||||
return "No tasks. Use create_task to add some."
|
||||
lines = []
|
||||
for t in tasks:
|
||||
icon = {"pending": "○", "in_progress": "●",
|
||||
"completed": "✓"}.get(t.status, "?")
|
||||
deps = f" (blockedBy: {', '.join(t.blockedBy)})" if t.blockedBy else ""
|
||||
owner = f" [{t.owner}]" if t.owner else ""
|
||||
lines.append(f" {icon} {t.id}: {t.subject} "
|
||||
f"[{t.status}]{owner}{deps}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def run_get_task(task_id: str) -> str:
|
||||
try:
|
||||
return get_task(task_id)
|
||||
except FileNotFoundError:
|
||||
return f"Error: Task {task_id} not found"
|
||||
|
||||
|
||||
def run_claim_task(task_id: str) -> str:
|
||||
return claim_task(task_id, owner="agent")
|
||||
|
||||
|
||||
def run_complete_task(task_id: str) -> str:
|
||||
return complete_task(task_id)
|
||||
|
||||
|
||||
TOOLS = [
|
||||
{"name": "bash", "description": "Run a shell command.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"command": {"type": "string"}},
|
||||
"required": ["command"]}},
|
||||
{"name": "read_file", "description": "Read file contents.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"path": {"type": "string"},
|
||||
"limit": {"type": "integer"}},
|
||||
"required": ["path"]}},
|
||||
{"name": "write_file", "description": "Write content to a file.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"path": {"type": "string"},
|
||||
"content": {"type": "string"}},
|
||||
"required": ["path", "content"]}},
|
||||
{"name": "create_task",
|
||||
"description": "Create a new task with optional blockedBy dependencies.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {
|
||||
"subject": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"blockedBy": {"type": "array",
|
||||
"items": {"type": "string"}}},
|
||||
"required": ["subject"]}},
|
||||
{"name": "list_tasks",
|
||||
"description": "List all tasks with status, owner, and dependencies.",
|
||||
"input_schema": {"type": "object", "properties": {},
|
||||
"required": []}},
|
||||
{"name": "get_task",
|
||||
"description": "Get full details of a specific task by ID.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"task_id": {"type": "string"}},
|
||||
"required": ["task_id"]}},
|
||||
{"name": "claim_task",
|
||||
"description": "Claim a pending task. Sets owner, changes status to in_progress.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"task_id": {"type": "string"}},
|
||||
"required": ["task_id"]}},
|
||||
{"name": "complete_task",
|
||||
"description": "Complete an in-progress task. Reports unblocked downstream tasks.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"task_id": {"type": "string"}},
|
||||
"required": ["task_id"]}},
|
||||
]
|
||||
|
||||
TOOL_HANDLERS = {
|
||||
"bash": run_bash, "read_file": run_read, "write_file": run_write,
|
||||
"create_task": run_create_task, "list_tasks": run_list_tasks,
|
||||
"get_task": run_get_task, "claim_task": run_claim_task,
|
||||
"complete_task": run_complete_task,
|
||||
}
|
||||
|
||||
|
||||
# ── Context ──
|
||||
|
||||
def update_context(context: dict, messages: list) -> dict:
|
||||
"""Derive context from real state."""
|
||||
memories = ""
|
||||
if MEMORY_INDEX.exists():
|
||||
content = MEMORY_INDEX.read_text().strip()
|
||||
if content:
|
||||
memories = content
|
||||
return {
|
||||
"enabled_tools": list(TOOL_HANDLERS.keys()),
|
||||
"workspace": str(WORKDIR),
|
||||
"memories": memories,
|
||||
}
|
||||
|
||||
|
||||
# ── Agent Loop (simplified, focused on task system) ──
|
||||
|
||||
def agent_loop(messages: list, context: dict):
|
||||
system = get_system_prompt(context)
|
||||
while True:
|
||||
try:
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=system, messages=messages,
|
||||
tools=TOOLS, max_tokens=8000)
|
||||
except Exception as e:
|
||||
messages.append({"role": "assistant", "content": [
|
||||
{"type": "text",
|
||||
"text": f"[Error] {type(e).__name__}: {e}"}]})
|
||||
return
|
||||
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
if response.stop_reason != "tool_use":
|
||||
return
|
||||
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type != "tool_use":
|
||||
continue
|
||||
print(f"\033[36m> {block.name}\033[0m")
|
||||
handler = TOOL_HANDLERS.get(block.name)
|
||||
output = handler(**block.input) if handler else f"Unknown: {block.name}"
|
||||
print(str(output)[:300])
|
||||
results.append({"type": "tool_result",
|
||||
"tool_use_id": block.id, "content": output})
|
||||
messages.append({"role": "user", "content": results})
|
||||
context = update_context(context, messages)
|
||||
system = get_system_prompt(context)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("s12: task system")
|
||||
print("Enter a question, press Enter to send. Type q to quit.\n")
|
||||
history = []
|
||||
context = update_context({}, [])
|
||||
while True:
|
||||
try:
|
||||
query = input("\033[36ms12 >> \033[0m")
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
break
|
||||
if query.strip().lower() in ("q", "exit", ""):
|
||||
break
|
||||
history.append({"role": "user", "content": query})
|
||||
agent_loop(history, context)
|
||||
context = update_context(context, history)
|
||||
for block in history[-1]["content"]:
|
||||
if getattr(block, "type", None) == "text":
|
||||
print(block.text)
|
||||
print()
|
||||
59
s12_task_system/images/task-dag.en.svg
Normal file
59
s12_task_system/images/task-dag.en.svg
Normal file
@@ -0,0 +1,59 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 400" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<marker id="dep" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#94a3b8"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="760" height="400" fill="#fafbfc" rx="8"/>
|
||||
|
||||
<!-- Title -->
|
||||
<rect x="0" y="0" width="760" height="44" fill="#0d9488" rx="8"/>
|
||||
<rect x="0" y="36" width="760" height="8" fill="#0d9488"/>
|
||||
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task DAG — Dependency Example: Database → API → Tests → Deploy</text>
|
||||
|
||||
<!-- Row 1: schema (completed) -->
|
||||
<rect x="295" y="70" width="170" height="48" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="2"/>
|
||||
<text x="380" y="92" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">✓ schema</text>
|
||||
<text x="380" y="108" fill="#16a34a" font-size="9" text-anchor="middle">completed</text>
|
||||
|
||||
<!-- Arrows: schema → endpoints, schema → docs -->
|
||||
<path d="M 340 118 L 240 162" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
|
||||
<path d="M 420 118 L 520 162" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
|
||||
|
||||
<!-- Row 2: endpoints (in_progress), docs (pending) -->
|
||||
<rect x="115" y="164" width="170" height="48" rx="8" fill="#dbeafe" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="200" y="186" fill="#1e40af" font-size="12" font-weight="700" text-anchor="middle">● endpoints</text>
|
||||
<text x="200" y="202" fill="#2563eb" font-size="9" text-anchor="middle">in_progress · owner: agent-1</text>
|
||||
|
||||
<rect x="475" y="164" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
|
||||
<text x="560" y="186" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ docs</text>
|
||||
<text x="560" y="202" fill="#94a3b8" font-size="9" text-anchor="middle">pending · blockedBy: schema ✓</text>
|
||||
|
||||
<!-- Arrows: endpoints → tests, docs → deploy -->
|
||||
<path d="M 200 212 L 200 262" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
|
||||
<path d="M 510 212 L 440 262" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
|
||||
|
||||
<!-- Row 3: tests (pending), deploy (pending) -->
|
||||
<rect x="115" y="264" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
|
||||
<text x="200" y="286" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ tests</text>
|
||||
<text x="200" y="302" fill="#94a3b8" font-size="9" text-anchor="middle">blockedBy: endpoints ●</text>
|
||||
|
||||
<!-- Arrow: tests → deploy -->
|
||||
<path d="M 285 288 L 375 288" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
|
||||
|
||||
<rect x="375" y="264" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
|
||||
<text x="460" y="286" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ deploy</text>
|
||||
<text x="460" y="302" fill="#94a3b8" font-size="9" text-anchor="middle">blockedBy: tests, docs</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<rect x="40" y="338" width="680" height="46" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<rect x="60" y="352" width="14" height="12" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
|
||||
<text x="80" y="363" fill="#475569" font-size="10">completed</text>
|
||||
<rect x="160" y="352" width="14" height="12" rx="3" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="180" y="363" fill="#475569" font-size="10">in_progress</text>
|
||||
<rect x="270" y="352" width="14" height="12" rx="3" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
|
||||
<text x="290" y="363" fill="#475569" font-size="10">pending</text>
|
||||
<text x="370" y="363" fill="#94a3b8" font-size="10">→ blockedBy (arrows = dependency direction)</text>
|
||||
<text x="60" y="378" fill="#94a3b8" font-size="9">docs' blockedBy (schema) is completed → can_start returns True, can be claimed</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
59
s12_task_system/images/task-dag.ja.svg
Normal file
59
s12_task_system/images/task-dag.ja.svg
Normal file
@@ -0,0 +1,59 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 400" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<marker id="dep" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#94a3b8"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="760" height="400" fill="#fafbfc" rx="8"/>
|
||||
|
||||
<!-- タイトル -->
|
||||
<rect x="0" y="0" width="760" height="44" fill="#0d9488" rx="8"/>
|
||||
<rect x="0" y="36" width="760" height="8" fill="#0d9488"/>
|
||||
<text x="380" y="28" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Task DAG — 依存関係の例:データベース → API → テスト → デプロイ</text>
|
||||
|
||||
<!-- 行 1: schema(完了) -->
|
||||
<rect x="295" y="70" width="170" height="48" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="2"/>
|
||||
<text x="380" y="92" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">✓ schema</text>
|
||||
<text x="380" y="108" fill="#16a34a" font-size="9" text-anchor="middle">completed</text>
|
||||
|
||||
<!-- 矢印: schema → endpoints, schema → docs -->
|
||||
<path d="M 340 118 L 240 162" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
|
||||
<path d="M 420 118 L 520 162" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
|
||||
|
||||
<!-- 行 2: endpoints(進行中)、docs(保留中) -->
|
||||
<rect x="115" y="164" width="170" height="48" rx="8" fill="#dbeafe" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="200" y="186" fill="#1e40af" font-size="12" font-weight="700" text-anchor="middle">● endpoints</text>
|
||||
<text x="200" y="202" fill="#2563eb" font-size="9" text-anchor="middle">in_progress · owner: agent-1</text>
|
||||
|
||||
<rect x="475" y="164" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
|
||||
<text x="560" y="186" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ docs</text>
|
||||
<text x="560" y="202" fill="#94a3b8" font-size="9" text-anchor="middle">pending · blockedBy: schema ✓</text>
|
||||
|
||||
<!-- 矢印: endpoints → tests, docs → deploy -->
|
||||
<path d="M 200 212 L 200 262" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
|
||||
<path d="M 510 212 L 440 262" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
|
||||
|
||||
<!-- 行 3: tests(保留中)、deploy(保留中) -->
|
||||
<rect x="115" y="264" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
|
||||
<text x="200" y="286" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ tests</text>
|
||||
<text x="200" y="302" fill="#94a3b8" font-size="9" text-anchor="middle">blockedBy: endpoints ●</text>
|
||||
|
||||
<!-- 矢印: tests → deploy -->
|
||||
<path d="M 285 288 L 375 288" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
|
||||
|
||||
<rect x="375" y="264" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
|
||||
<text x="460" y="286" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ deploy</text>
|
||||
<text x="460" y="302" fill="#94a3b8" font-size="9" text-anchor="middle">blockedBy: tests, docs</text>
|
||||
|
||||
<!-- 凡例 -->
|
||||
<rect x="40" y="338" width="680" height="46" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<rect x="60" y="352" width="14" height="12" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
|
||||
<text x="80" y="363" fill="#475569" font-size="10">completed</text>
|
||||
<rect x="160" y="352" width="14" height="12" rx="3" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="180" y="363" fill="#475569" font-size="10">in_progress</text>
|
||||
<rect x="270" y="352" width="14" height="12" rx="3" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
|
||||
<text x="290" y="363" fill="#475569" font-size="10">pending</text>
|
||||
<text x="370" y="363" fill="#94a3b8" font-size="10">→ blockedBy(矢印 = 依存方向)</text>
|
||||
<text x="60" y="378" fill="#94a3b8" font-size="9">docs の blockedBy (schema) は完了済み → can_start が True を返し、claim 可能</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
59
s12_task_system/images/task-dag.svg
Normal file
59
s12_task_system/images/task-dag.svg
Normal file
@@ -0,0 +1,59 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 400" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<marker id="dep" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#94a3b8"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="760" height="400" fill="#fafbfc" rx="8"/>
|
||||
|
||||
<!-- Title -->
|
||||
<rect x="0" y="0" width="760" height="44" fill="#0d9488" rx="8"/>
|
||||
<rect x="0" y="36" width="760" height="8" fill="#0d9488"/>
|
||||
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task DAG — 依赖关系示例:搭数据库 → API → 测试 → 部署</text>
|
||||
|
||||
<!-- Row 1: schema (completed) -->
|
||||
<rect x="295" y="70" width="170" height="48" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="2"/>
|
||||
<text x="380" y="92" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">✓ schema</text>
|
||||
<text x="380" y="108" fill="#16a34a" font-size="9" text-anchor="middle">completed</text>
|
||||
|
||||
<!-- Arrows: schema → endpoints, schema → docs -->
|
||||
<path d="M 340 118 L 240 162" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
|
||||
<path d="M 420 118 L 520 162" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
|
||||
|
||||
<!-- Row 2: endpoints (in_progress), docs (pending) -->
|
||||
<rect x="115" y="164" width="170" height="48" rx="8" fill="#dbeafe" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="200" y="186" fill="#1e40af" font-size="12" font-weight="700" text-anchor="middle">● endpoints</text>
|
||||
<text x="200" y="202" fill="#2563eb" font-size="9" text-anchor="middle">in_progress · owner: agent-1</text>
|
||||
|
||||
<rect x="475" y="164" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
|
||||
<text x="560" y="186" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ docs</text>
|
||||
<text x="560" y="202" fill="#94a3b8" font-size="9" text-anchor="middle">pending · blockedBy: schema ✓</text>
|
||||
|
||||
<!-- Arrows: endpoints → tests, docs → deploy -->
|
||||
<path d="M 200 212 L 200 262" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
|
||||
<path d="M 510 212 L 440 262" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
|
||||
|
||||
<!-- Row 3: tests (pending), deploy (pending) -->
|
||||
<rect x="115" y="264" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
|
||||
<text x="200" y="286" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ tests</text>
|
||||
<text x="200" y="302" fill="#94a3b8" font-size="9" text-anchor="middle">blockedBy: endpoints ●</text>
|
||||
|
||||
<!-- Arrow: tests → deploy -->
|
||||
<path d="M 285 288 L 375 288" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
|
||||
|
||||
<rect x="375" y="264" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
|
||||
<text x="460" y="286" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ deploy</text>
|
||||
<text x="460" y="302" fill="#94a3b8" font-size="9" text-anchor="middle">blockedBy: tests, docs</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<rect x="40" y="338" width="680" height="46" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<rect x="60" y="352" width="14" height="12" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
|
||||
<text x="80" y="363" fill="#475569" font-size="10">completed</text>
|
||||
<rect x="160" y="352" width="14" height="12" rx="3" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="180" y="363" fill="#475569" font-size="10">in_progress</text>
|
||||
<rect x="270" y="352" width="14" height="12" rx="3" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
|
||||
<text x="290" y="363" fill="#475569" font-size="10">pending</text>
|
||||
<text x="370" y="363" fill="#94a3b8" font-size="10">→ blockedBy(箭头 = 依赖方向)</text>
|
||||
<text x="60" y="378" fill="#94a3b8" font-size="9">docs 的 blockedBy (schema) 已完成 → can_start 返回 True,可被 claim</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
94
s12_task_system/images/task-system-overview.en.svg
Normal file
94
s12_task_system/images/task-system-overview.en.svg
Normal file
@@ -0,0 +1,94 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 420" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0d9488"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-teal" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0d9488"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="760" height="420" fill="#fafbfc" rx="8"/>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Legend -->
|
||||
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s11 Preserved</text>
|
||||
<rect x="160" y="56" width="12" height="10" rx="2" fill="#f0fdfa" stroke="#0d9488" stroke-width="1"/>
|
||||
<text x="178" y="66" fill="#0d9488" font-size="10" font-weight="600">s12 New</text>
|
||||
|
||||
<!-- ===== s11 loop (compact) ===== -->
|
||||
<rect x="30" y="92" width="80" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="70" y="116" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</text>
|
||||
|
||||
<line x1="110" y1="112" x2="128" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="131" y="86" width="120" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="191" y="108" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt + compress</text>
|
||||
<text x="191" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">(s10-s11)</text>
|
||||
|
||||
<line x1="251" y1="112" x2="269" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="272" y="86" width="100" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="322" y="108" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM (try/except)</text>
|
||||
<text x="322" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">(s11)</text>
|
||||
|
||||
<line x1="372" y1="112" x2="390" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- TOOLS (expanded) -->
|
||||
<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">TOOL_HANDLERS</text>
|
||||
<text x="408" y="114" fill="#2563eb" font-size="9">bash · read · write</text>
|
||||
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_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 -->
|
||||
<path d="M 603 112 L 640 112 L 640 155 L 70 155 L 70 132" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||
|
||||
<!-- ===== .tasks/ directory (teal) ===== -->
|
||||
<rect x="40" y="185" width="310" height="76" rx="8" fill="#f0fdfa" stroke="#0d9488" stroke-width="2"/>
|
||||
<text x="195" y="205" fill="#134e4a" font-size="11" font-weight="700" text-anchor="middle">.tasks/ — Cross-session Persistence</text>
|
||||
<text x="60" y="222" fill="#0d9488" font-size="9">task_xxx.json · task_yyy.json · task_zzz.json</text>
|
||||
<text x="60" y="238" fill="#6b7280" font-size="8">{id, subject, description, status, owner, blockedBy}</text>
|
||||
<text x="60" y="252" fill="#6b7280" font-size="8">Tutorial ID: timestamp + random | CC: sequential ID + highwatermark</text>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- ===== 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="252" fill="#0d9488" font-size="9">complete_task → completed + unblock downstream</text>
|
||||
|
||||
<!-- ===== State machine ===== -->
|
||||
<rect x="40" y="286" width="680" height="46" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="60" y="310" fill="#1e3a5f" font-size="11" font-weight="600">State Machine:</text>
|
||||
<rect x="160" y="298" width="56" height="20" rx="4" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
|
||||
<text x="188" y="312" fill="#475569" font-size="9" text-anchor="middle">pending</text>
|
||||
<line x1="216" y1="308" x2="288" y2="308" stroke="#0d9488" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
|
||||
<text x="252" y="303" fill="#0d9488" font-size="8" font-weight="600" text-anchor="middle">claim</text>
|
||||
<rect x="290" y="298" width="76" height="20" rx="4" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="328" y="312" fill="#1e40af" font-size="9" text-anchor="middle">in_progress</text>
|
||||
<line x1="366" y1="308" x2="458" y2="308" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
|
||||
<text x="412" y="303" fill="#16a34a" font-size="8" font-weight="600" text-anchor="middle">complete_task</text>
|
||||
<rect x="460" y="298" width="72" height="20" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
|
||||
<text x="496" y="312" fill="#166534" font-size="9" text-anchor="middle">completed</text>
|
||||
<text x="548" y="312" fill="#94a3b8" font-size="9">No release rollback; crash → unassign owner</text>
|
||||
|
||||
<!-- ===== Bottom notes ===== -->
|
||||
<rect x="40" y="352" width="680" height="52" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<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">s11 Preserved: loop, prompt assembly, compression (error recovery independent from task system)</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">s12 New: Task dataclass + 5 tools + .tasks/ persistence + blockedBy dependency graph</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
94
s12_task_system/images/task-system-overview.ja.svg
Normal file
94
s12_task_system/images/task-system-overview.ja.svg
Normal file
@@ -0,0 +1,94 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 420" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0d9488"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-teal" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0d9488"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="760" height="420" fill="#fafbfc" rx="8"/>
|
||||
|
||||
<!-- タイトル -->
|
||||
<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>
|
||||
|
||||
<!-- 凡例 -->
|
||||
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s11 保持</text>
|
||||
<rect x="140" y="56" width="12" height="10" rx="2" fill="#f0fdfa" stroke="#0d9488" stroke-width="1"/>
|
||||
<text x="158" y="66" fill="#0d9488" font-size="10" font-weight="600">s12 新規</text>
|
||||
|
||||
<!-- ===== s11 ループ(コンパクト) ===== -->
|
||||
<rect x="30" y="92" width="80" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="70" y="116" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</text>
|
||||
|
||||
<line x1="110" y1="112" x2="128" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="131" y="86" width="120" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="191" y="108" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt + compress</text>
|
||||
<text x="191" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">(s10-s11)</text>
|
||||
|
||||
<line x1="251" y1="112" x2="269" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="272" y="86" width="100" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="322" y="108" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM (try/except)</text>
|
||||
<text x="322" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">(s11)</text>
|
||||
|
||||
<line x1="372" y1="112" x2="390" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- ツール(展開) -->
|
||||
<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">TOOL_HANDLERS</text>
|
||||
<text x="408" y="114" fill="#2563eb" font-size="9">bash · read · write</text>
|
||||
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_task · list_tasks</text>
|
||||
<text x="408" y="140" fill="#0d9488" font-size="9" font-weight="600">get_task · claim_task · complete_task</text>
|
||||
|
||||
<!-- ループバック -->
|
||||
<path d="M 603 112 L 640 112 L 640 155 L 70 155 L 70 132" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||
|
||||
<!-- ===== .tasks/ ディレクトリ(ティール) ===== -->
|
||||
<rect x="40" y="185" width="310" height="76" rx="8" fill="#f0fdfa" stroke="#0d9488" stroke-width="2"/>
|
||||
<text x="195" y="205" fill="#134e4a" font-size="11" font-weight="700" text-anchor="middle">.tasks/ — セッション横断永続化</text>
|
||||
<text x="60" y="222" fill="#0d9488" font-size="9">task_xxx.json · task_yyy.json · task_zzz.json</text>
|
||||
<text x="60" y="238" fill="#6b7280" font-size="8">{id, subject, description, status, owner, blockedBy}</text>
|
||||
<text x="60" y="252" fill="#6b7280" font-size="8">チュートリアル ID: timestamp + random | CC: 順次 ID + highwatermark</text>
|
||||
|
||||
<!-- 矢印: 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>
|
||||
|
||||
<!-- ===== ライフサイクル(ティール) ===== -->
|
||||
<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="252" fill="#0d9488" font-size="9">complete_task → completed + 下流をアンロック</text>
|
||||
|
||||
<!-- ===== 状態マシン ===== -->
|
||||
<rect x="40" y="286" width="680" height="46" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="60" y="310" fill="#1e3a5f" font-size="11" font-weight="600">状態マシン:</text>
|
||||
<rect x="150" y="298" width="56" height="20" rx="4" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
|
||||
<text x="178" y="312" fill="#475569" font-size="9" text-anchor="middle">pending</text>
|
||||
<line x1="206" y1="308" x2="278" y2="308" stroke="#0d9488" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
|
||||
<text x="242" y="303" fill="#0d9488" font-size="8" font-weight="600" text-anchor="middle">claim</text>
|
||||
<rect x="280" y="298" width="76" height="20" rx="4" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="318" y="312" fill="#1e40af" font-size="9" text-anchor="middle">in_progress</text>
|
||||
<line x1="356" y1="308" x2="448" y2="308" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
|
||||
<text x="402" y="303" fill="#16a34a" font-size="8" font-weight="600" text-anchor="middle">complete_task</text>
|
||||
<rect x="450" y="298" width="72" height="20" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
|
||||
<text x="486" y="312" fill="#166534" font-size="9" text-anchor="middle">completed</text>
|
||||
<text x="538" y="312" fill="#94a3b8" font-size="9">release ロールバックなし、クラッシュ時は unassign で owner クリア</text>
|
||||
|
||||
<!-- ===== 下部ノート ===== -->
|
||||
<rect x="40" y="352" width="680" height="52" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<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">s11 保持:ループ、プロンプト組み立て、圧縮(エラーリカバリとタスクシステムは独立)</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">s12 新規:Task dataclass + 5 ツール + .tasks/ 永続化 + blockedBy 依存グラフ</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
94
s12_task_system/images/task-system-overview.svg
Normal file
94
s12_task_system/images/task-system-overview.svg
Normal file
@@ -0,0 +1,94 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 420" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0d9488"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-teal" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0d9488"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="760" height="420" fill="#fafbfc" rx="8"/>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Legend -->
|
||||
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s11 保留</text>
|
||||
<rect x="140" y="56" width="12" height="10" rx="2" fill="#f0fdfa" stroke="#0d9488" stroke-width="1"/>
|
||||
<text x="158" y="66" fill="#0d9488" font-size="10" font-weight="600">s12 新增</text>
|
||||
|
||||
<!-- ===== s11 loop (compact) ===== -->
|
||||
<rect x="30" y="92" width="80" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="70" y="116" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</text>
|
||||
|
||||
<line x1="110" y1="112" x2="128" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="131" y="86" width="120" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="191" y="108" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt + compress</text>
|
||||
<text x="191" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">(s10-s11)</text>
|
||||
|
||||
<line x1="251" y1="112" x2="269" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="272" y="86" width="100" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="322" y="108" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM (try/except)</text>
|
||||
<text x="322" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">(s11)</text>
|
||||
|
||||
<line x1="372" y1="112" x2="390" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- TOOLS (expanded) -->
|
||||
<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">TOOL_HANDLERS</text>
|
||||
<text x="408" y="114" fill="#2563eb" font-size="9">bash · read · write</text>
|
||||
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_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 -->
|
||||
<path d="M 603 112 L 640 112 L 640 155 L 70 155 L 70 132" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||
|
||||
<!-- ===== .tasks/ directory (teal) ===== -->
|
||||
<rect x="40" y="185" width="310" height="76" rx="8" fill="#f0fdfa" stroke="#0d9488" stroke-width="2"/>
|
||||
<text x="195" y="205" fill="#134e4a" font-size="11" font-weight="700" text-anchor="middle">.tasks/ — 跨会话持久化</text>
|
||||
<text x="60" y="222" fill="#0d9488" font-size="9">task_xxx.json · task_yyy.json · task_zzz.json</text>
|
||||
<text x="60" y="238" fill="#6b7280" font-size="8">{id, subject, description, status, owner, blockedBy}</text>
|
||||
<text x="60" y="252" fill="#6b7280" font-size="8">教学版 ID: timestamp + random | CC: 顺序 ID + highwatermark</text>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- ===== 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="252" fill="#0d9488" font-size="9">complete_task → completed + 解锁下游</text>
|
||||
|
||||
<!-- ===== State machine ===== -->
|
||||
<rect x="40" y="286" width="680" height="46" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="60" y="310" fill="#1e3a5f" font-size="11" font-weight="600">状态机:</text>
|
||||
<rect x="120" y="298" width="56" height="20" rx="4" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
|
||||
<text x="148" y="312" fill="#475569" font-size="9" text-anchor="middle">pending</text>
|
||||
<line x1="176" y1="308" x2="238" y2="308" stroke="#0d9488" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
|
||||
<text x="207" y="303" fill="#0d9488" font-size="8" font-weight="600" text-anchor="middle">claim</text>
|
||||
<rect x="240" y="298" width="76" height="20" rx="4" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="278" y="312" fill="#1e40af" font-size="9" text-anchor="middle">in_progress</text>
|
||||
<line x1="316" y1="308" x2="408" y2="308" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
|
||||
<text x="362" y="303" fill="#16a34a" font-size="8" font-weight="600" text-anchor="middle">complete_task</text>
|
||||
<rect x="410" y="298" width="72" height="20" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
|
||||
<text x="446" y="312" fill="#166534" font-size="9" text-anchor="middle">completed</text>
|
||||
<text x="500" y="312" fill="#94a3b8" font-size="9">CC 无 release 回退,崩溃时用 unassign 清 owner</text>
|
||||
|
||||
<!-- ===== Bottom notes ===== -->
|
||||
<rect x="40" y="352" width="680" height="52" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<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">s11 保留:循环、prompt 组装、压缩(错误恢复与任务系统独立)</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">s12 新增:Task dataclass + 5 个工具 + .tasks/ 持久化 + blockedBy 依赖图</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.7 KiB |
Reference in New Issue
Block a user