理系学生日記

おまえはいつまで学生気分なのか

Clineのソースコードから、コーディングエージェントがどう動いているのかを理解する

Clineは、ユーザの指示を実現する「計画と実行」を行う自律型コーディングエージェントです。コーディングエージェントとしてはNo.1の地位を築いているのではないでしょうか。

このエントリでは、Clineの内部構造とその動作の流れを、ソースコードの実際の箇所を交えながら書いてみます。対象バージョンは今日リリースされたばかりのCline v3.1.0です。ホヤホヤですね。

概観

ClineはいくつかのToolを内部で定義しており、ユーザの指示を実現するためにどのToolをどのような順で使っていくかを「計画」し、実行すると言うのが基本的な流れになります。

ツール定義

利用可能なツールは`core/assistant-message/index.tsで定義されています。Toolの名前配列が都合よくあったので、抜き出すと以下。ファイル操作からリスト取得、コード解析まで多岐にわたり、ほぼすべての操作はこれらを通じて実現されます。

export const toolUseNames = [
 "execute_command",
 "read_file",
 "write_to_file",
 "replace_in_file",
 "search_files",
 "list_files",
 "list_code_definition_names",
 "browser_action",
 "use_mcp_tool",
 "access_mcp_resource",
 "ask_followup_question",
 "attempt_completion",
] as const

処理の流れ

処理主体

タスク処理に関するコアな部分の実装は、core/Cline.tsで行われています。

ここで定義されているクラスClineは、どうもユーザの指示1つに対して1つ生成されるタイプの思想だと思われます。と言うのも、コンストラクタに渡されるtaskが、ユーザが入力した指示を示しているからですね。

Clineの処理の流れ

主要な処理ステップとその実装箇所を示します。

タスクの初期化

処理のエントリーポイントはstartTaskです。 続くinitializeTaskLoopで、タスクの処理ループが開始されます。

ここでさらにrecursivelyMakeClineRequestsが呼び出されます。このメソッドこそが、タスクの計画と実行を担うコアとなるメソッドです。

recursivelyMakeClineRequestsでどうやってタスクの計画を行うのか

Clineでタスクを処理する時におそらく一番重要な処理を握るのはこのメソッドです。

いろんな処理をやっているんですが、まずはここをみてくれ。

  const [parsedUserContent, environmentDetails] = await this.loadContext(userContent, includeFileDetails)
  userContent = parsedUserContent
  // add environment details as its own text block, separate from tool results
  userContent.push({ type: "text", text: environmentDetails })

大前提として、ここでいうuserContentはユーザ指示を実現するために分解したサブタスク群が入っているArrayになっていて、型定義は次のとおり。

type UserContent = Array<
 Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam>

ユーザの指示は最初Anthropic.TextBlockParam型として1要素に格納された状態になっています。taskって言うのが、ユーザが最初に指示した内容で、以下のような構成。

    {
     type: "text",
     text: `<task>\n${task}\n</task>`,
    },

loadContextって言うのは、ざっくりと以下のようなことをしてくれている。

  • @urlとか@fileとかそういうコンテキストを解析し、その結果をプロンプトに埋め込む。その内容をサブタスク配列parsedUserContentとして返却
  • VS Code上で開いているファイルの内容とかのコンテキスト情報をenvironemtDetailsとして返却

というわけで、コンテキストが埋め込まれたサブタスク配列が今userContentに入っているわけですね。

とはいえ、まだuserContentにはユーザ指示しか入っていない。これをサブタスク分割していくのがattemptApiRequestの呼び出しです。

   const stream = this.attemptApiRequest(previousApiReqIndex) // yields only if the first chunk is successful, otherwise will allow the user to retry the request (most likely due to rate limit error, which gets thrown on the first chunk)

ツールを使うサブタスクに分割するためのtool use

サブタスク分割は、いわゆるtool_use(function calling)として行われます。実行場所はここ

// tool useのプロンプトを作成
let systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false, mcpHub)

// (略)
// この辺りで、`systemPrompt`に .clineRules`ファイルの内容とかが追加される

const stream = this.api.createMessage(systemPrompt, truncatedConversationHistory)

createMessageは、tool_useのプロンプトを生成AIに投げ込んで、ユーザのタスクを実現するためにどのツールを使えば良いのかを計画します。

プロンプトの実体は`core/prompts/system.tsにあるので見ていただければと思いますが。冒頭部分は以下のような感じ1で、よくあるtool_use (function calling)のプロンプトになっています。

You are Cline, a highly skilled software engineer with extensive knowledge 
in many programming languages, frameworks, design patterns, and best practices.

====

TOOL USE

You have access to a set of tools that are executed upon the user's approval. 
You can use one tool per message, and will receive the result of that tool use 
in the user's response. You use tools step-by-step to accomplish a given task, 
with each tool use informed by the result of the previous tool use.

# Tool Use Formatting

Tool use is formatted using XML-style tags. The tool name is enclosed in 
opening and closing tags, and each parameter is similarly enclosed within 
its own set of tags. Here's the structure:

<tool_name>
<parameter1_name>value1</parameter1_name>
<parameter2_name>value2</parameter2_name>
...
</tool_name>

これを生成AIに投げ込むと、ユーザ指示を実行するにあたりツールをどう使っていくべきかを返却してくれるので、それをパースし、実行計画としてサブタスク配列に埋め込みます。

そしてこのサブタスクがpresentAssistantMessageの呼び出しの中で個々のツール実装によって処理され、結果がユーザに提示されます。

その上で、残っているサブタスク配列を元にして、再度recursivelyMakeClineRequestsを呼び出す。これを繰り返していくことで、ユーザの指示が実現されていきます。

const recDidEndLoop = await this.recursivelyMakeClineRequests(this.userMessageContent)

まとめ

Clineは、ユーザの指示を実現するために、定義したツールを使ったサブタスク分割を行うことで処理を計画し、それを実行していくという流れで動いているようでした。 僕はtool_useのユースケースは「外部ツールの利用」だと思っていたのですが、実際にはClineの中で実装した処理をツールと見立てて処理を行っていたわけですね。なるほどなぁ。 最近VS Code APIの中でもtool use用のAPIも生えたので、僕の中で利用範囲が広がりました。

Clineで定義したツール個々の処理自体も面白いです。計画を立てる上では当然どのファイルでどういう処理が行われているのかを理解する必要があり、それをtree-sitter等を利用してどのように進めていくのかというのも興味深かったのですが、それはまた別エントリで。


  1. 読みやすいように改行だけ付与しました