Skip to content

Task クラス: 単ターン Agent の中核

源码版本v4.0.10

役割

Task クラスは Cline の「単ターン agent」状態機械である。一つの Task インスタンスが一つのタスク (task) に対応し、ユーザーがエンターキーを叩いた瞬間に生まれ、タスクがキャンセルされるか正常に終了する瞬間に死ぬ。常駐サービスではなく、Controller が作って一巡走らせたら捨てる使い捨てのオブジェクトである。この「一ターン一インスタンス」のモデルにより、並行制御が素直になる。同じ Task が二度同時に LLM リクエストを処理することは決してなく、自分自身だけが動くからである。

行うことは三段に整理できる。まず現在のユーザー入力、履歴メッセージ、システムプロンプト、MCP ツールリスト、ルールファイル等を一つの完全な API リクエストに組み立てる。次に LLM のストリーミングレスポンスを駆動し、ストリームを受け取りながら parseAssistantMessageV2 でテキストを text / tool_use / reasoning の三種の block に切り分ける。最後にこれらの block を presentAssistantMessage に渡し、一つずつ UI に表示しつつ、その中のツールを実行する。ツールの結果は次ラウンドの user content として再流入し、再帰呼び出し (recursion) をトリガーする。モデルが tool_use を発しなくなるか、ユーザーが中断するまでこれが続く。

三層のネストしたループとして理解すれば十分である。最外層の initiateTaskLoop は「モデルがツールを呼ばなかったらもう一度問う」を兜底し、中間層の recursivelyMakeClineRequests は再帰の一回ごとに完全な API 呼び出し + ツール実行を行い、内層の presentAssistantMessage はストリームの中で block をインクリメンタルに進める。すべての「質問を投げる」「ユーザーに確認を求める」「進捗を報告する」といった UI インタラクションは asksay の二つの出口を経由する。この二つのメソッドはメッセージを messageStateHandler に詰め、postMessage で webview に送る。

設計動機

  • 一タスク一インスタンス:状態を各 Task インスタンスに隔離することで、キャンセル、ロールバック、checkpoint をすべてインスタンスを境界に処理できる。abortTasktaskState.abort フラグを置くだけで、すべての再帰パスがこれを読んで自ら終了する (abortTask:1801)。
  • ループではなく再帰:各ラウンドの LLM レスポンスの後、ツールの結果は自然に次ラウンドの user content になる。そのため recursivelyMakeClineRequests は末尾で自分自身を再度呼ぶ (recurse:3830)。この書き方で「単一ラウンドの API 呼び出し + ツール実行」のコードを一度だけ書けばよく、コールスタックの深さがターン数を自然に反映し、エラー時のスタックトレースも読みやすい。
  • ストリーム解析と提示の分離:LLM の戻りは吐き出されるそばから解析される。parseAssistantMessageV2 はテキストを一段受けるごとに assistant テキスト全体を再解析し (parseAssistantMessageV2 call:3509)、presentAssistantMessage が block 境界に沿って進める。これによりツールはレスポンス全体を待たずに動き出せる。
  • 単一ロックで状態競合を防止:Task の状態変更はすべて withStateLock で同じ Mutex を取得する (withStateLock:205)。ストリーミングコールバック、ツール実行、UI インタラクションの三経路の並行で状態が壊れるのを防ぐ。
  • YOLO モードと mistake 上限:連続エラー回数が上限に達したら即座に終了する (mistake limit:2826)。モデルが無限ループで token を燃やすのを避けるためである。

主要ファイル

  • Task class definition:188export class Task。taskId、taskState、api、controller、messageStateHandler 等の主要フィールドを宣言する。
  • constructor:310TaskParams を受け取り、clineIgnore、toolExecutor、streamHandler、presentationScheduler などの依存を初期化する。
  • startTask:1253 — 入口。初期 user content を用意し、TaskStart hook を走らせてから initiateTaskLoop を呼ぶ。
  • initiateTaskLoop:1717 — 外層の while ループ。モデルがテキストだけでツールを呼ばなかった場合に noToolsUsed で再度問い直す。
  • recursivelyMakeClineRequests:2790 — 中層の再帰駆動器。mistake 上限チェック、checkpoint 初期化、attemptApiRequest 呼び出しでストリームを引く。
  • attemptApiRequest:2175 — MCP 接続を待ち、ルールファイルを読み、システムプロンプトを組み立てて、最後に LLM ストリームを yield* する。
  • presentAssistantMessage:2630 — 内層の block 進行器。type ごとにディスパッチ: text は thinking タグを除去してから say、tool_use は toolExecutor.executeTool に渡す。
  • ask:789 — ユーザーに回答を求める出口。partial メッセージ更新と webview レスポンスコールバックを管理する。
  • say:969 — 一方向の進捗報告出口。partial モードは同一メッセージのストリーミングなインクリメンタル更新に使う。
  • abortTask:1801 — 段階的キャンセル。TaskCancel hook を走らせるか先に決定し、abort フラグを置き、hook やバックグラウンドコマンドをキャンセルし、最後に hook を走らせる。
  • ToolExecutor.executeTool:212 — Task はツール実行を委譲し、自身はどの具体的ツールも直接扱わない。

データフロー

一度の LLM リクエストの核心パスは「再帰 → ストリーム取得 → 解析 → 提示 → 再流入 → 再帰」である。recursivelyMakeClineRequests は入るや否やこのラウンドのストリーミング状態をリセットし、attemptApiRequest でストリームを取得する:

typescript
// apps/vscode/src/core/task/index.ts
// reset streaming state
this.taskState.currentStreamingContentIndex = 0;
this.taskState.assistantMessageContent = [];
this.taskState.didCompleteReadingStream = false;
this.taskState.userMessageContent = [];
this.taskState.userMessageContentReady = false;
this.taskState.didRejectTool = false;
this.taskState.didAlreadyUseTool = false;
this.taskState.presentAssistantMessageLocked = false;
// ...
const stream = this.attemptApiRequest(previousApiReqIndex); // yields only if the first chunk is successful

この部分は reset streaming state:3302 の付近にある。リセット完了後、StreamChunkCoordinator がストリームを text / usage / reasoning の chunk に分け、それぞれコールバックで処理する。text を一段受けるごとに parseAssistantMessageV2 を再実行し、assistant テキスト全体を block 配列に切り分ける (parseAssistantMessageV2 call:3509):

typescript
assistantMessage += chunk.text;
assistantTextOnly += chunk.text; // Accumulate text separately
// parse raw assistant message into content blocks
const prevLength = this.taskState.assistantMessageContent.length;

this.taskState.assistantMessageContent =
    parseAssistantMessageV2(assistantMessage);

block 配列が変化すると scheduleAssistantPresentationpresentAssistantMessage を発火する。後者は block.type ごとに分岐し、text は say("text", ...)、tool_use は toolExecutor.executeTool(block) に進む (executeTool call:2743)。ツールの結果は taskState.userMessageContent に詰め込まれ、ストリーム全体が流れ終わり userMessageContentReady が置かれた後、recursivelyMakeClineRequests はこの user content を使ってもう一度自分自身を呼ぶ (recurse:3830)。モデルが tool_use を含まなくなれば外層の initiateTaskLoopnoToolsUsed で再度問い直すか、ユーザーが終了させるまで続く。

境界と失敗

  • mistake 上限の発火:連続エラー回数が maxConsecutiveMistakes に達した時、YOLO モードは直接 return true でタスクを終了し、それ以外は ask("mistake_limit_reached") でユーザーに判断を委ねる (mistake limit:2826)。
  • 空レスポンス:一ラウンドの assistant に text も tool_use も一つもない場合、empty_assistant_message テレメトリを記録し、ユーザーに再試行を促す (empty response:3834)。
  • ユーザーによる途中キャンセル:abortTask は先に TaskCancel hook を走らせるかを capture し、その後 abort フラグを置く。フラグを置いた後の hook 判定漏れを避けるためである (abortTask:1801)。
  • ツールが拒否された:didRejectTool が置かれると後続の text block はスキップされ、ストリームは [Response interrupted by user feedback] で切り詰められる (didRejectTool:3536)。
  • MCP が未接続:attemptApiRequestpWaitFor で最大 10 秒待つ。タイムアウト時はログを記録するだけでブロックせず、システムプロンプトは通常通り生成される (mcp wait:2177)。
  • 初期 checkpoint 未完了:最初の checkpoint commit が走っている間、読み取り専用でないツールは await this.initialCheckpointCommitPromise でブロックされる。読み取り専用ツールは並行できる (initialCheckpoint gate:2737)。
  • partial block の収尾:ストリーム終了時に残った partial block は強制的に partial = false にされ、presentAssistantMessage が正常に進んで最終的に userMessageContentReady を置けるようにする (finalize partial blocks:3783)。

まとめ

Task クラスは Cline の「単ターン」次元におけるすべての状態機械である。一つのタスクのライフサイクルを「構築 → 起動 → 再帰ストリーム → ツール実行 → 収尾」の明確な連鎖にカプセル化し、すべての UI インタラクションは ask / say の二つの出口に集約され、すべての状態変更は一つの Mutex に集約される。この「一ターン一インスタンス」のトレードオフにより、キャンセル、checkpoint、mistake 上限などの境界処理がすべてインスタンス単位で簡潔に実装できる。

さらに踏み込みたい場合は次を参照:

  • 再帰自体: /ja/agent-loop/recursion
  • LLM 呼び出しとシステムプロンプトの組み立て: /ja/agent-loop/attempt-api-request
  • アシスタントテキストがどう block に切り分けられるか: /ja/agent-loop/parse-assistant-message

公式資料: Cline ドキュメント · README