はじめに
本記事は「Context Engineering 完全入門」シリーズの第 7 回です。今回は TODO ツール を取り上げます。
多段タスクをこなすエージェントに共通する課題として「ドリフト(脱線)」があります。最初に立てた計画がツール出力の蓄積によってコンテキストの中盤に埋もれ、エージェントが後半のタスクを忘れてしまう現象です。本記事では、この問題に対処するための TODO ツールの設計と、学術的に裏付けられた「Recitation Pattern」の実装についてご紹介します。
前回:第 6 回 Memory + 自己更新
次回:第 8 回 Skills & AGENTS.md🗺️ 単独でも読めます:Plan Persistence 問題と Recitation Pattern は本記事だけで理解・実装できます。
📝 本記事は、生成 AI で作成した草案をベースに、筆者が加筆・修正し、技術的な正確性を確認した上で公開しています。
📝 本記事の要点
- 問題: 多段タスクで「計画が context の中盤に押し流される」 Plan Persistence 問題。エージェントが Step 4 以降を忘れる
- 解決: Claude Code V2 互換の 4 ツール構成 + Recitation Pattern(3 ラウンドに 1 回、計画を末尾再掲)
- 実装の要点:
TodoStoreで永続化、RecitationContextProviderで計画を末尾 attention 領域に再注入(arXiv 2606.22953 の理論的根拠)急ぐ方は §1 の Plan Persistence 説明 と §4-4 RecitationContextProvider が実装の核です。
🎯 本記事の対象者
- 多段タスクで「エージェントが脱線する」経験がある
- Claude Code が V1 → V2 で TODO を大改修した理由を知りたい
- 「ドリフトしないエージェント」を実装したい
- 「Recitation Pattern」の学術的根拠を理解したい
- TODO ツールと Recitation を組み合わせて Plan Persistence 問題に対処したい
1. ⭐ 原則
TODO ツール設計の方針を整理すると以下の通りです。#1・#2 は本節、#3 は §2 で詳しく解説します。
| # | 原則 | 概要 | 参照項目 |
|---|---|---|---|
| 1 | 計画を構造化して保持する | タスクを状態管理し、進捗を明示的に追跡する | §1 |
| 2 | 進行中(in_progress)は同時 1 件に制限する |
並行実行によるドリフトを防止する | §1 |
| 3 | 計画を定期的に再掲する(Recitation) | Plan Persistence 問題(計画が context から消える)への対策 | §2 |
Memory(第 6 回)との関係:自己状態管理の 2 本目
第 6 回のメモリー機能が 「何を覚えておくか」(知識・規約・進捗の保存)を扱ったのに対し、本記事の TODO は 「何を実行するか」(現在の実行計画の維持)を扱います。両者は競合ではなく 補完関係 にあり、メモリー機能はセッションを跨いだ長期的な状態管理を担うのに対し、TODO はセッション内での短期的な進捗管理を担います。
第 6 回では AGENTS.md / PROGRESS.md / Session Memory / TODO の 4 分類を提示しました。本記事では、その中の TODO に焦点を当て、多段タスクでのドリフト防止に特化した設計を解説します。
問題:エージェントの drift(脱線)
shareAI-lab/learn-claude-code のレッスン文書より:
"(訳) 進捗の明示的な追跡がなければ、エージェントは多段タスクで集中力を失います。ツールの実行結果が会話履歴に蓄積されるにつれ、最初に立てた計画は目立たなくなっていきます。10 ステップのリファクタリングでは、Step 1〜3 までは完了できても、Step 4〜10 がコンテキストの中で埋もれてしまうため、途中でドリフト(脱線)が発生してしまいます。"
TODO ツールが解決するのは「長いツール出力で原計画がコンテキストの中段になってしまいLLMから参照されにくくなる」という Lost-in-the-Middle 問題そのものです。
Plan Persistence:TODO ツールの理論的根拠
2026 年 6 月の研究論文(arXiv 2606.22953「Plan Persistence in LLM Agents」)で、「LLM Agent は計画を context-resident として持ち、コンテキストから消えると計画も消える」 ことが定量的に実証されました:
"(訳) 計画立案直後は、LLM の attention 内で計画信号が 0.453 のピーク値を示しますが、次のアクション実行と結果観察のわずか 1 ステップの間に、その信号強度は 4.1 倍減衰します。"
これが 「Recitation Pattern」(Manus 論文で 2025/7 提唱)の理論的根拠です。
6 つのベストプラクティス
原則 1〜3 を実装する際の具体的なベストプラクティスは以下の通りです。
| # | ベストプラクティス | 概要 |
|---|---|---|
| 1 | 3+ ステップ・複雑なタスクで自動起動 | 単純なタスクでの誤起動を防ぐ |
| 2 |
in_progress は同時 1 つだけ |
並行実行によるドリフト防止 |
| 3 | 完了は即時マーク(バッチ更新禁止) | 進捗の可視化と正確性 |
| 4 | 3 ラウンド更新なしで Nag リマインダー注入 | 更新忘れの検知 |
| 5 | ファイル永続化(Claude Code V2 が解決した課題) | セッション跨ぎでの状態保持 |
| 6 | Recitation:定期的に末尾に再掲する(Plan Persistence 対策) | 計画の attention 維持 |
3 軸評価:TODO 実装の設計軸
TODO 実装を 持続性 × 更新粒度 × 並行制御の 3 軸 で評価すると、各実装の特徴が明確になります。
- 持続性:セッション終了で消えるか、ファイル永続化するか
- 更新粒度:全リスト置換か、単一タスク更新か
-
並行制御:同時
in_progressを許容するか
この 3 軸で主要な実装を評価すると、業務系での適性が判断しやすくなります。詳細は §5 のケーススタディで解説します。
2. 📐 パターン
本節では、TODO ツールを設計するための実装ベースのパターンを扱います。まず §2-1 で VS Code Copilot Chat と Claude Code の TODO 実装(本記事の実装参考元)を確認し、§2-2 で Claude Code V2 互換の 4 ツール構成 + Nag Reminder + Recitation という実装パターン、§2-3 で業務系システムへの適用時の制約を整理します。
2-1. VS Code / Claude Code の TODO 実装(実装参考元)
本記事の Microsoft Agent Framework (MAF) 実装(§4)は、VS Code Copilot Chat と Claude Code V2 の TODO 実装を参考にしています。ここでは、それぞれの仕様を確認します。
VS Code Copilot Chat の manage_todo_list
microsoft/vscode-copilot-chat の manageTodoListTool.tsx は わずか 31 行 / 1.42 KB という非常にシンプルな実装です。
| 要素 | 内容 |
|---|---|
| 操作 |
read(取得)/ write(完全置換) |
| TodoItem |
id + title + description + status
|
| status |
not-started / in-progress / completed
|
| 警告 | 項目数 < 3 で「小さすぎる」警告 |
Claude Code の V1 → V2 大改修
"(訳) TypeScript Agent SDK 0.3.142 および Claude Code v2.1.142 以降、セッションでは
TodoWriteの代わりに、構造化された Task ツール群(TaskCreate、TaskUpdate、TaskGet、TaskList)が使用されます。"
V2 の利点:
- ✅ ファイル永続化:セッションを跨いで状態保持
- ✅ マルチエージェント共有可能:他プロセスから読める
- ✅ 細かい操作:単一タスクのみ更新可能
- ✅ 後方互換:
CLAUDE_CODE_ENABLE_TASKS=0で V1 互換モードに戻せる
2-2. Claude Code V2 互換の実装パターン
Claude Code V2 の 4 ツール構成をベースに、Nag Reminder と Recitation を組み合わせることで、Plan Persistence 問題に対応する完全な TODO システムを構築します。
4 ツール構成(V2 互換)
| ツール | 操作対象 | 用途 |
|---|---|---|
TaskCreate |
単一タスク | 新しいタスクの作成 |
TaskUpdate |
単一タスク | ステータス更新(in_progress / completed など) |
TaskGet |
単一タスク | 特定タスクの詳細取得 |
TaskList |
全タスク | 全体の進捗確認 |
VS Code の manage_todo_list(read / write の 2 操作)と比較すると、単一タスクの操作と全体取得を分離することで、トークン削減・並行制御・LLM の精度向上 の 3 つの恩恵が得られます。
Nag Reminder + Recitation の組み合わせ
- Nag Reminder:3 ラウンド TODO 更新がない場合に「未完了タスクがあります」の警告を末尾注入
- Recitation:3 ラウンドに 1 回、現在の計画を末尾に再掲(Plan Persistence 対策)
2-3. 業務系システムへの適用時の制約
Claude Code V2 の TODO 実装は個人利用のコーディング支援を前提としているため、業務系システムでは以下の制約を考慮する必要があります。
| Claude Code V2 の前提 | 業務系システムでの制約 |
|---|---|
ローカルファイル(.agent-todos.json)で永続化 |
クラウド環境ではファイル永続化が困難、DB / Blob Storage への保存が一般的 |
| 単一ユーザー(単一エージェントプロセス)を前提 | マルチテナント SaaS ではテナント分離が必須 |
| TODO ファイルを直接変更(ユーザーも編集可) | 業務系では変更履歴の監査ログが必要な場合がある |
| セッション終了後もファイルが残る | GDPR / 個人情報保護法対応で TTL や削除ポリシーが必要 |
業務系での考慮点
業務系で TODO ツールを本番運用する場合、第 6 回で扱ったメモリー機能と同じく 「動的性(エージェントの自由な更新)」を維持しつつ「統制性(監査・テナント分離・削除ポリシー)」を担保する ことが重要です。具体的な永続化戦略の詳細は補章 C「業界研究の業務系翻訳ガイド」で扱います。
3. 💡 実装の方針と設計判断
サンプルコードの実装方針は以下の通りです。
設計判断 1:なぜ 4 つのツール(V2 仕様)に分けるか
VS Code の manage_todo_list(read / write の 2 操作)ではなく、Claude Code V2 の 4 ツール構成を採用します。単一タスクの操作(Create / Update / Get)と全体取得(List)を分離することで、トークン削減・並行制御・LLM の精度向上の 3 つの恩恵を受けられます。
設計判断 2:Update(id, InProgress) で「他の InProgress を pending に戻す」
「同時 in_progress 1 つだけ」ルールを TodoStore.Update 内で強制します。LLM が「複数の in_progress」を作っても、データ層で正しい状態に修正するため、アンチパターンが物理的に発生しません。
設計判断 3:Nag Reminder を AIContextProvider で実装
3 ラウンド未更新で自動的に警告を注入します。ツールとして実装すると LLM が呼ばないと意味がないため、Provider で確実に注入します。
設計判断 4:Recitation を独立した Provider にする
Reminder は「催促」、Recitation は「再掲」と責務が異なります。インターバルも違うため(Reminder は 3 ラウンド、Recitation はより頻繁)、独立した Provider にすることで柔軟に組み合わせられます。
設計判断 5:ファイル永続化を JSON で行う
.agent-todos.json 1 ファイルなのでデバッグが容易で、人が直接編集でき、Git で diff が見えます。ただし業務系では §2-3 で述べた通り、DB や Blob Storage への保存が推奨されます。
4. 💻 実装:C# / Microsoft Agent Framework (MAF)
以下のサンプルコードは、TODO ツールと Recitation Pattern の 概念を理解するための実装例 です。本番の業務系システムでは、ローカルファイルではなく Cosmos DB / DynamoDB などの永続化バックエンドを利用することが推奨されます。業務系での永続化戦略の詳細は補章 C で扱う予定です。
4-1. データ構造
💻 TodoItem — TODOの状態・優先度・日時を表すデータ構造(C#・約25行、クリックで展開)
using System.ComponentModel;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace ContextEngineering.Todo;
// 🔑 Claude Code V2 互換:cancelled も含む
public enum TodoStatus { Pending, InProgress, Completed, Cancelled }
public enum Priority { High, Medium, Low }
public record TodoItem
{
public required string Id { get; init; }
public required string Title { get; init; }
public string Description { get; init; } = "";
// 🔑 Status は mutable(Update で変更)
public TodoStatus Status { get; set; } = TodoStatus.Pending;
public Priority Priority { get; set; } = Priority.Medium;
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
4-2. 永続化付きストア
実装の要点:
-
Update(id, InProgress)で「他の InProgress を pending に強制ダウングレード」して同時 1 つルールをデータ層で物理的に強制 - 起動時に
ITodoStorage経由で永続化データを読み込み、操作のたびに保存 -
AttentionCheck()で 3 ラウンド未更新を検知(Nag Reminder) -
GetCurrentPlanRecitation()で pending/in_progress のみを priority 順に整形(Recitation 用)
💻 ITodoStorage — TODO永続化バックエンドのサンプル実装(C#・約6行、クリックで展開)
永続化の抽象化(インターフェース定義):
// 🔑 サンプルでは JSON ファイル実装、本番では DB / Blob Storage 等に置換
public interface ITodoStorage
{
Task<IReadOnlyList<TodoItem>> LoadAsync(CancellationToken ct = default);
Task SaveAsync(IReadOnlyList<TodoItem> items, CancellationToken ct = default);
}
💻 TodoStore — 永続化・単一InProgress制御・Recitationのサンプル実装(C#・約110行、クリックで展開)
public class TodoStore
{
private readonly ITodoStorage _storage;
private readonly List<TodoItem> _items = new();
private int _roundsSinceUpdate;
private TodoStore(ITodoStorage storage) => _storage = storage;
/// <summary>起動時に永続化データをロードして <see cref="TodoStore"/> を構築する。</summary>
public static async Task<TodoStore> OpenAsync(
ITodoStorage storage, CancellationToken cancellationToken = default)
{
var store = new TodoStore(storage);
var loaded = await storage.LoadAsync(cancellationToken);
store._items.AddRange(loaded);
return store;
}
public async Task<TodoItem> CreateAsync(
string title, string description = "", Priority priority = Priority.Medium,
CancellationToken cancellationToken = default)
{
var item = new TodoItem
{
Id = Guid.NewGuid().ToString("N")[..8],
Title = title,
Description = description,
Priority = priority,
};
_items.Add(item);
// 🔑 操作したらカウンタリセット(Reminder の起点)
_roundsSinceUpdate = 0;
await PersistAsync(cancellationToken);
return item;
}
public async Task<TodoItem?> UpdateAsync(
string id, TodoStatus status, CancellationToken cancellationToken = default)
{
// 🔑 ベストプラクティス 2: 同時 in_progress は 1 つだけ(データ層で物理的に強制)
if (status == TodoStatus.InProgress)
{
foreach (var other in _items.Where(o => o.Id != id
&& o.Status == TodoStatus.InProgress))
{
// 🔑 他の in_progress を pending に強制ダウングレード
// → LLM が誤って複数 in_progress にしても自動修復
other.Status = TodoStatus.Pending;
other.UpdatedAt = DateTime.UtcNow;
}
}
var target = _items.FirstOrDefault(i => i.Id == id);
if (target == null) return null;
target.Status = status;
target.UpdatedAt = DateTime.UtcNow;
_roundsSinceUpdate = 0;
await PersistAsync(cancellationToken);
return target;
}
public TodoItem? Get(string id) => _items.FirstOrDefault(i => i.Id == id);
public IReadOnlyList<TodoItem> ListAll() => _items.AsReadOnly();
/// <summary>
/// Nag Reminder:3 ラウンド更新がなければ警告メッセージを返す。
/// </summary>
public string? AttentionCheck()
{
// 🔑 LLM 呼び出しごとにカウントアップ
_roundsSinceUpdate++;
if (_roundsSinceUpdate < 3) return null;
var pending = _items.Count(i => i.Status != TodoStatus.Completed
&& i.Status != TodoStatus.Cancelled);
return pending > 0
? $"[Reminder] You have {pending} incomplete tasks."
: null;
}
/// <summary>
/// Recitation:Plan Persistence 対策の中核。
/// </summary>
public string GetCurrentPlanRecitation()
{
// 🔑 pending / in_progress のみを priority 順で
// → completed タスクは再掲不要
var pending = _items
.Where(i => i.Status == TodoStatus.Pending || i.Status == TodoStatus.InProgress)
.OrderBy(i => i.Status == TodoStatus.InProgress ? 0 : 1)
.ThenBy(i => i.Priority)
.ToList();
if (pending.Count == 0) return "";
var sb = new System.Text.StringBuilder();
sb.AppendLine("# Current Plan (Recitation for context maintenance)");
foreach (var item in pending)
{
// 🔑 in_progress には → マーカー
// → LLM が「今やっているタスク」を視覚的に認識
var marker = item.Status == TodoStatus.InProgress ? "→" : " ";
sb.AppendLine($"{marker} [{item.Status}] {item.Title}");
}
return sb.ToString();
}
private Task PersistAsync(CancellationToken cancellationToken)
=> _storage.SaveAsync(_items.AsReadOnly(), cancellationToken);
}
設計判断の核心:同時 1 つルールの強制
LLM が複数の InProgress を作っても、データ層で自動修復します:
💻 InProgressを同時1件に制限する更新ロジック(C#・約15行、クリックで展開)
public async Task<TodoItem?> UpdateAsync(string id, TodoStatus status, CancellationToken cancellationToken = default)
{
// 🔑 ベストプラクティス 2: 同時 in_progress は 1 つだけ(物理的に強制)
if (status == TodoStatus.InProgress)
{
foreach (var other in _items.Where(o => o.Id != id
&& o.Status == TodoStatus.InProgress))
{
other.Status = TodoStatus.Pending; // ← 強制ダウングレード
other.UpdatedAt = DateTime.UtcNow;
}
}
// ... 続く
}
4-3. TODO ツール(Claude Code V2 互換 4 ツール)
💻 TodoTools — Claude Code V2互換4ツールのサンプル実装(C#・約85行、クリックで展開)
public class TodoTools
{
private readonly TodoStore _store;
// 🔑 TodoStatus/Priority を数値ではなく文字列でシリアライズする
// → LLM にとって "0" より "Pending" の方が誤読なく理解できる(第 2 回のメタデータ完全性と同じ考え方)
private static readonly JsonSerializerOptions Options = new()
{
Converters = { new JsonStringEnumConverter() },
};
public TodoTools(TodoStore store) => _store = store;
[Description(
// 🔑 「3+ steps の複雑なタスクで使え」と明示
// → 簡単なタスクで誤起動を防ぐ
// 🔑 「N ステップ」と依頼された場合は、1 つの親タスクにまとめず
// このツールを N 回呼んで各ステップを個別タスクとして登録することを明示
// → TaskList/TaskUpdate で各ステップの進捗を個別に追跡できるようにするため
"Create a new task. Use for complex multi-step tasks (3+ steps). " +
"IMPORTANT: If the user asks for N steps/subtasks, call this tool N times " +
"(once per individual step) to create N separate tasks, rather than creating " +
"a single parent task with the steps only described in the description field. " +
"This allows each step's progress to be tracked independently via TaskUpdate/TaskList."
)]
public async Task<string> TaskCreateAsync(
[Description("Task title")] string title,
[Description("Description")] string description = "",
[Description("Priority")] Priority priority = Priority.Medium)
=> JsonSerializer.Serialize(await _store.CreateAsync(title, description, priority), Options);
[Description(
"Update task status. Mark as InProgress when starting, " +
"Completed IMMEDIATELY after finishing. " +
// 🔑 強調:1 つだけルール
"IMPORTANT: Only ONE task should be InProgress at a time."
)]
public async Task<string> TaskUpdateAsync(
[Description("Task ID")] string id,
[Description("New status")] TodoStatus status)
{
var item = await _store.UpdateAsync(id, status);
return item != null
? JsonSerializer.Serialize(item, Options)
: JsonSerializer.Serialize(new { error = $"Task {id} not found" });
}
[Description("Get details of a single task by ID.")]
public string TaskGet([Description("Task ID")] string id)
{
var item = _store.Get(id);
return item != null
? JsonSerializer.Serialize(item, Options)
: JsonSerializer.Serialize(new { error = $"Task {id} not found" });
}
[Description("List all tasks with their current status.")]
public string TaskList()
{
var items = _store.ListAll();
var counts = Enum.GetValues<TodoStatus>()
.ToDictionary(s => s.ToString(), s => items.Count(i => i.Status == s));
var result = new
{
items,
counts,
// 🔑 警告:タスクが少なすぎる場合
// → 「直接やった方が早い」というシグナル
warning = items.Count is > 0 and < 3
? "Few tasks (< 3). Consider doing trivial tasks directly."
: null,
};
return JsonSerializer.Serialize(result,
new JsonSerializerOptions { WriteIndented = true, Converters = { new JsonStringEnumConverter() } });
}
}
4-4. Recitation Context Provider(Plan Persistence 対策)
💻 RecitationContextProvider / TodoReminderProvider — 計画再掲と未更新通知のサンプル実装(C#・約65行、クリックで展開)
/// <summary>
/// Plan Persistence 対策:定期的に計画を末尾に再掲。
/// arXiv 2606.22953 の知見を MAF に実装。
/// </summary>
public class RecitationContextProvider : AIContextProvider
{
private readonly TodoStore _store;
private int _turnsSinceRecitation;
private readonly int _recitationInterval;
public RecitationContextProvider(TodoStore store, int recitationInterval = 3)
: base(null, null)
{
_store = store;
_recitationInterval = recitationInterval;
}
protected override ValueTask<AIContext> ProvideAIContextAsync(
InvokingContext context, CancellationToken cancellationToken = default)
{
_turnsSinceRecitation++;
// 🔑 N ラウンドに 1 回だけ再掲
// → 毎回再掲するとトークン浪費
if (_turnsSinceRecitation < _recitationInterval)
return new ValueTask<AIContext>(new AIContext());
var recitation = _store.GetCurrentPlanRecitation();
if (string.IsNullOrEmpty(recitation))
return new ValueTask<AIContext>(new AIContext());
// 🔑 カウンタリセット
_turnsSinceRecitation = 0;
// 🔑 system role で「末尾に」再掲
// → LLM の attention は末尾に集中するので、計画維持が効く
return new ValueTask<AIContext>(new AIContext
{
Messages = [new ChatMessage(ChatRole.System, recitation)],
});
}
}
/// <summary>
/// Nag Reminder:3 ラウンド未更新で警告。
/// </summary>
public class TodoReminderProvider : AIContextProvider
{
private readonly TodoStore _store;
public TodoReminderProvider(TodoStore store) : base(null, null) => _store = store;
protected override ValueTask<AIContext> ProvideAIContextAsync(
InvokingContext context, CancellationToken cancellationToken = default)
{
var reminder = _store.AttentionCheck();
if (reminder == null) return new ValueTask<AIContext>(new AIContext());
return new ValueTask<AIContext>(new AIContext
{
Messages = [new ChatMessage(ChatRole.System, reminder)],
});
}
}
4-5. エージェント組み立て
TodoStore の読み込みは非同期 I/O(ITodoStorage.LoadAsync)になったため、コンストラクタではなく await TodoStore.OpenAsync(storage) で事前に構築してから渡す設計にしています。
💻 Example.BuildAgentWithTodo — TODO・Reminder・Recitation対応エージェントのサンプル実装(C#・約45行、クリックで展開)
public static class Example
{
public static AIAgent BuildAgentWithTodo(IChatClient chatClient, TodoStore store)
{
var todoTools = new TodoTools(store);
// 🔑 LLM に対する操作指示
// → ワークフローを明確化することで誤用を防ぐ
var instructions =
"Agent that manages complex multi-step tasks.\n\n" +
"WORKFLOW:\n" +
"1. For complex tasks (3+ steps), call TaskCreate for each subtask.\n" +
"2. Call TaskUpdate(id, InProgress) BEFORE starting work.\n" +
"3. Call TaskUpdate(id, Completed) IMMEDIATELY after finishing.\n" +
"4. Only ONE task should be InProgress at a time.\n" +
"5. Use TaskList to verify progress periodically.";
return chatClient.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new ChatOptions
{
Instructions = instructions,
// 🔑 Tools は ChatOptions に登録する
Tools = new List<AITool>
{
AIFunctionFactory.Create(todoTools.TaskCreateAsync),
AIFunctionFactory.Create(todoTools.TaskUpdateAsync),
AIFunctionFactory.Create(todoTools.TaskGet),
AIFunctionFactory.Create(todoTools.TaskList),
},
},
AIContextProviders =
[
// 🔑 2 つの Provider を組み合わせる
new TodoReminderProvider(store), // 3 ラウンドで Reminder
new RecitationContextProvider(store), // Plan Persistence 対策
],
});
}
}
5. 📊 ケーススタディ
主要ツールの TODO 実装比較
主要ツールの TODO 実装を、§1 で定義した 持続性・更新粒度・並行制御の 3 軸 で比較すると、ツールごとの明確な差が現れます。
| ツール | ツール数 | 持続性 | 更新粒度 | 並行制御 | Recitation | 業務系適性 |
|---|---|---|---|---|---|---|
VS Code Copilot Chat(manage_todo_list) |
1 | セッション内 | 完全置換 | 緩い(複数 in_progress 可) | 未対応 | △ |
| Codex CLI | 0 | — | — | — | — | × |
Claude Code V2(Task 4 ツール) |
4 | ファイル永続化 | 個別更新可能 | 厳密(1 つだけ) | 一部対応 | ○ |
| MAF TodoStore + Recitation(本記事) | 4 + 2 Provider | ファイル永続化(業務系では DB 推奨) | 個別更新可能 | 厳密(データ層で強制) | 完全対応 | ◎ |
教訓:
- VS Code Copilot Chat はシンプルさを重視した設計で、31 行という最小実装ながら基本的な TODO 管理は可能です。ただし完全置換方式のため、並行制御が緩く、大規模タスクでのドリフト防止には不十分です
- Codex CLI は TODO ツールを持たないため、多段タスクの進捗管理は LLM の自律性に依存します。長時間タスクや複雑なフロー制御には不向きです
- Claude Code V2 は運用性重視の設計で、4 ツール構成 + ファイル永続化により、V1 の課題(インメモリ、完全置換)を解決しました。ただし Recitation は「一部対応」レベルで、Plan Persistence 問題の完全解決には追加実装が必要です
-
MAF TodoStore + Recitation(本記事) は Claude Code V2 の 4 ツール構成を踏襲しつつ、Recitation Pattern を完全実装 している点が特徴です。データ層での並行制御強制(
Updateメソッド内での自動ダウングレード)と、RecitationContextProviderによる定期的な計画再掲により、Plan Persistence 問題を安定的に解決します
つまり、「TODO 管理と Recitation を組み合わせて初めて Plan Persistence 問題を安定的に解決できる」 ということが、本記事の核心的な主張です。個人利用なら Claude Code V2 レベルで十分ですが、長時間・多段タスクを扱う業務系エージェントでは、Recitation の完全実装が長期運用の鍵となります。
自動起動の判断条件
TODO ツールを いつ使うべきか の判断は、タスクの複雑度に応じて決定します。過剰な TODO 使用は逆にオーバーヘッドを生むため、以下の判断基準を明確にすることが重要です。
使うべき場面:
- 3+ ステップを要する複雑なマルチステップタスク
- ユーザーが複数のタスクを列挙した
- 慎重な計画が必要な non-trivial なタスク
使うべきでない場面:
- 単一の簡単なタスク
- 3 ステップ未満の trivial なタスク
- 純粋な会話・情報提供
教訓:
TODO ツールは「使えば使うほど良い」ものではなく、タスクの複雑度に応じた選択的利用 が重要です。単純なタスクで TODO を使うと、TaskCreate → TaskUpdate(InProgress) → 作業 → TaskUpdate(Completed) という一連のオーバーヘッドが逆に効率を下げます。第 5 回のサブエージェント設計と同じく、適切なタスクに適切なツールを使う判断 が業務系での長期運用の鍵となります。
6. ⚠️ アンチパターン
本記事で扱った TODO ツール設計の要点を、実装時のセルフレビュー用チェックリスト(アンチパターン)としてまとめました。
実装時は本表を「やってはいけないことリスト」としてご参照ください。特に #3・#5・#6 は長時間タスクでのドリフト防止と Plan Persistence 問題への対応に直結する重要項目です。
| # | アンチパターン | 問題点 |
|---|---|---|
| 1 | すべてのタスクで TODO を使う | 過剰なオーバーヘッドが発生する |
| 2 | 完了をバッチで更新する | drift を誘発する |
| 3 | 複数 in_progress を放置 | 並行実行でドリフトが発生しやすくなる |
| 4 | 失敗中なのに completed にする | 後から問題に気づけなくなる |
| 5 | Persistent な保存をしない | セッション間で進捗が消えてゼロから計画立て直しになる |
| 6 | Recitation を実装しない | Plan Persistence 問題で計画が context から消える |
| 7 | Reminder を毎ラウンド入れる | トークン浪費と「警告慣れ」で LLM が無視するようになる |
おわりに
本記事では、多段タスクでのドリフト防止のための TODO ツールと、Plan Persistence 問題に対応する Recitation Pattern の実装をご紹介しました。特に 持続性・更新粒度・並行制御の 3 軸 で TODO 実装を評価し、「3 ラウンドに 1 回、計画を末尾に再掲する」という Recitation の実装が、学術論文(arXiv 2606.22953)でも裏付けられた効果的な手法であることを解説しました。
第 6 回のメモリー機能と本記事の TODO ツールは、エージェントの自己状態管理における 補完関係 にあります。Memory がセッションを跨いだ長期的な状態(知識・規約・進捗)を扱うのに対し、TODO はセッション内での短期的な実行計画を扱います。両者を組み合わせることで、長時間・複数セッションにわたるタスクでも、エージェントが学習と作業を継続できる設計が可能になります。
次回は「Skills & AGENTS.md」を解説します。ここまでのランタイム制御・自己状態管理の仕組みを、マークダウンファイルとして静的に宣言する設計パターンについてご紹介します。
参考文献
VS Code / Claude Code 関連
- microsoft/vscode-copilot-chat
manageTodoListTool.tsx - Claude Code Docs「Todo Lists (Agent SDK)」
- Claude Code Docs「Tools reference」
- shareAI-lab/learn-claude-code「s05: TodoWrite」