Agent Client Protocol を理解する

Agent Client Protocol を理解する

2026年1月12日 · 10 分で読める
Background image generated by Gemini
post 技術系

はじめに

Agent Client Protocol (ACP) は、AI エージェントとクライアント(エディタなど)が対話する際の標準を定義するプロトコルです。 この記事では、ACP の概要と、その背景や具体的な利用例、そして将来性について、自身の理解を元に解説します。

公式ドキュメントは こちら にあります。

背景: なぜ ACP が必要なのか

2025年8月30日、 Zed から以下のように発表がありました:

同年10月7日には、 JetBrains から Zed と連携して ACP に取り組むことが発表されました:

JetBrains × Zed: Open Interoperability for AI Coding Agents in Your IDE | The JetBrains AI Blog

同年12月5日に発表された Bring your own AI agent to JetBrains IDEs | The JetBrains AI Blog で語られているところによると、JetBrains が自社エージェント統合用のプロトコルを公開しようとしたタイミングで Zed も同様の動きを見せていたため、 規格の乱立を避けるべく協力することになった、というのが実情のようです。

当初から『業界標準』を目指して周到に計画されたものではなく、各社の実利的なニーズが合致した結果として生まれた、という経緯は興味深い点です。 各ベンダーが「エージェントを効率よく統合したい」という現実的な課題を持っていたタイミングが重なったことが、結果として標準化を加速させたと言えるでしょう。

LSP (Language Server Protocol) がプログラミング言語ごとの開発体験の差異を吸収したように、ACP は乱立する AI エージェントとそのクライアント間の通信を標準化することを目指しています。

主な特徴とコンセプト

ACP は、タスクの実行、進捗の報告、対話的なデータ交換など、エージェントとのやり取りに必要な一連の機能を定義しています。

具体的な仕様は 公式サイト を見れば分かりますが、ざっくり以下のような流れになっています:

  1. Initialization
    • 接続の確立や、ベンダーの認証処理
  2. Session Setup
    • 新規セッションを始めるか、既存セッションを復元する
  3. Prompt Turn
    • ここから実際のやりとりループが始まる

公式サイトの構成は Protocol 配下が全部並列で分かりづらいですが、 まず Initialization, Session Setup, Prompt Turn が Overview で述べられている上記のメッセージフローに対応しています。

Schema はその通りスキーマ定義で、それ以外は個々のコンセプトの説明になっています。

実践: agent-shell.el での利用例

ACP の具体的な動作を理解するために、Emacs のクライアント実装である agent-shell.el を使って、実際の通信内容を見ていきます。

ここでは、エディタ上で範囲を指定して、その範囲の内容を日本語に翻訳し、挿入してもらう、という簡単なタスクをエージェントに依頼し、その際の ACP メッセージ(リクエストとレスポンス)をキャプチャして、プロトコルの使われ方を具体的に示します。

ログ出力の有無は agent-shell-toggle-logging で設定出来ます。 日本語を含むプロンプトの場合は agent-shell-prompt-compose で専用のバッファを設けるのがよいでしょう。

エージェントには Gemini CLI を使います ( --experimental-acp )。

Initialization

agent-shell の場合、最初に何らかのプロンプトを送信すると、初期化フェーズが始まります。

{
  "jsonrpc": "2.0",
  "method": "initialize",
  "id": 1,
  "params": {
    "protocolVersion": 1,
    "clientCapabilities": {
      "fs": {
        "readTextFile": true,
        "writeTextFile": true
      }
    }
  }
}

ファイルの読み書きが出来ることを宣言していますね。

このリクエストを受けて、エージェント側から初期化のための応答が返ります。

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": 1,
    "authMethods": [
      {
        "id": "oauth-personal",
        "name": "Log in with Google",
        "description": null
      },
      {
        "id": "gemini-api-key",
        "name": "Use Gemini API key",
        "description": "Requires setting the `GEMINI_API_KEY` environment variable"
      },
      {
        "id": "vertex-ai",
        "name": "Vertex AI",
        "description": null
      }
    ],
    "agentCapabilities": {
      "loadSession": false,
      "promptCapabilities": {
        "image": true,
        "audio": true,
        "embeddedContext": true
      }
    }
  }
}

認証方法のリストに加えて、エージェント側の対応機能が返ってきています。

Gemini CLI は今のところセッションの永続化には対応していないようです。画像やオーディオをプロンプトとして埋め込むことが出来ることも分かります。

agent-shell の動きとしては現在のところ agent-shell-google-authentication に従って固定値を返すような実装になっており、応答内容は使っていません ( TODO になっています )。

{
  "jsonrpc": "2.0",
  "method": "authenticate",
  "id": 2,
  "params": {
    "methodId": "oauth-personal",
    "authMethod": {
      "id": "oauth-personal",
      "name": "Log in with Google",
      "description": ""
    }
  }
}

エージェントは成功を示すために以下のような空レスポンスを返します。

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": null
}

他の操作でも、特に返すべきものが無い場合はこのような空レスポンスで成功を表現するようです。

Session Setup

初期化に成功したらセッションを開始します。

{
  "jsonrpc": "2.0",
  "method": "session/new",
  "id": 3,
  "params": {
    "cwd": "/path/to/cwd/",
    "mcpServers": []
  }
}

セッションというのは特定のディレクトリに関連づけられる、ということが分かりますね。

エージェントはリクエストを受けて、セッション ID を返します。

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "sessionId": "36702bdd-86f6-4e87-b964-c2328216f375"
  }
}

以降のやりとりはこのセッション ID がキーとなります。

Prompt Turn

ここから、入力したプロンプトの処理が始まります。

選択範囲を作成した状態で agent-shell-prompt-compose を呼ぶと、該当箇所への参照をプロンプトに自動で含めてくれます。

{
  "jsonrpc": "2.0",
  "method": "session/prompt",
  "id": 5,
  "params": {
    "sessionId": "36702bdd-86f6-4e87-b964-c2328216f375",
    "prompt": [
      {
        "type": "text",
        "text": "以下を日本語に翻訳して:\n\ncontent-org/content-org/all-posts.org:1614-1614"
      }
    ]
  }
}

翻訳を頼んでみました。すると、以下のようにまずエージェントの思考が agent_thought_chunk として返ってきました。

{
  "jsonrpc": "2.0",
  "method": "session/update",
  "params": {
    "sessionId": "36702bdd-86f6-4e87-b964-c2328216f375",
    "update": {
      "sessionUpdate": "agent_thought_chunk",
      "content": {
        "type": "text",
        "text": "**Examining the File's Contents**\n\nI'm now reading the file around line 1614 to grasp the full context. This step is crucial for accurate translation and the subsequent `replace` operation. It allows me to pinpoint the text requiring translation and prepare the `old_string` for the `replace` tool.\n\n\n"
      }
    }
  }
}

該当の部分を読みたいぞ、となっているようです。その思考の結果、 agent_message_chunk として応答が返ってきました。

{
  "jsonrpc": "2.0",
  "method": "session/update",
  "params": {
    "sessionId": "36702bdd-86f6-4e87-b964-c2328216f375",
    "update": {
      "sessionUpdate": "agent_message_chunk",
      "content": {
        "type": "text",
        "text": "I will read the file `sample.txt` around line 10 to identify the text to translate."
      }
    }
  }
}

翻訳するために該当部分を読みます、という宣言です。

(何故か突然英語になってますが、これは Gemini の癖でしょう…)

この宣言のあと、ファイルを読むためのツール実行が始まります。

{
  "jsonrpc": "2.0",
  "method": "session/update",
  "params": {
    "sessionId": "36702bdd-86f6-4e87-b964-c2328216f375",
    "update": {
      "sessionUpdate": "tool_call",
      "toolCallId": "read_file-1767793979301",
      "status": "in_progress",
      "title": "content-org/all-posts.org",
      "content": [],
      "locations": [
        {
          "path": "/home/yewton/Projects/yewton.net/content-org/all-posts.org",
          "line": 1610
        }
      ],
      "kind": "read"
    }
  }
}

この時点では ACP の fs/read_text_file は使われないようです。 つまり、エディタの機能ではなくエージェント自身のツールを実行しようとしています ( これは恐らく今回の Gemini の挙動はそうなっていた、というだけだと思います )。

ツールの実行後、翻訳対象の文章を特定出来たので、翻訳と編集を行う宣言が返ってきました。

{
  "jsonrpc": "2.0",
  "method": "session/update",
  "params": {
    "sessionId": "36702bdd-86f6-4e87-b964-c2328216f375",
    "update": {
      "sessionUpdate": "agent_message_chunk",
      "content": {
        "type": "text",
        "text": "「こんにちは、世界!」に翻訳して、次の段落に追加します。"
      }
    }
  }
}

fs/write_text_file には、行番号など書き込み範囲を特定する仕様が定義されておらず、 対象のファイルパスと、上書き内容だけを指定出来るようです。

その為、書き込みの前にまず全文を読み込む必要があります。

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "fs/read_text_file",
  "params": {
    "path": "/home/yewton/Projects/yewton.net/content-org/all-posts.org",
    "sessionId": "36702bdd-86f6-4e87-b964-c2328216f375",
    "line": null,
    "limit": null
  }
}

書き込みに伴う読み込みには fs/read_text_file が使われる模様です。 つまり、エディタ上で編集途中でもその内容は失われない、ということになるので合理的な選択でしょう ( 本来は翻訳対象の特定時も、同様にした方が良さそうですが… )。

実際に書き込む内容を確定させると、以下のように書き込みの許可を求めるリクエストがエージェントから送られます。

{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "session/request_permission",
  "params": {
    "sessionId": "36702bdd-86f6-4e87-b964-c2328216f375",
    "options": [
      {
        "optionId": "proceed_always",
        "name": "Allow All Edits",
        "kind": "allow_always"
      },
      {
        "optionId": "proceed_once",
        "name": "Allow",
        "kind": "allow_once"
      },
      {
        "optionId": "cancel",
        "name": "Reject",
        "kind": "reject_once"
      }
    ],
    "toolCall": {
      "toolCallId": "replace-1767793984780",
      "status": "pending",
      "title": "content-org/all-posts.org: Hello, World! => Hello, World!",
      "content": [
        {
          "type": "diff",
          "path": "all-posts.org",
          "oldText": "...",
          "newText": "..."
        }
      ],
      "locations": [
        {
          "path": "/home/yewton/Projects/yewton.net/content-org/all-posts.org"
        }
      ],
      "kind": "edit"
    }
  }
}

この応答を受けると以下のような確認ダイアログが出てきます。

( 一度ダイアログを閉じてしまったので再度表示して試したい、という流れの応答だったので上記のような内容になっていますが気にしないでください 🙇 )

ここで View を選択すると、以下のように変更内容をプレビュー出来ます。

プロトコル上は diff 形式でやりとりされているわけではなく、更新前後の全文が送られているので、この表示はエディタ側で行うことが求められています。

ここで y を押して許可すると、以下のような応答がエージェントに返されます。

:direction outgoing
:kind      response
:object
           jsonrpc 2.0
           id      5
           result
                   outcome
                           outcome  selected
                           optionId proceed_once

( クライアントからの応答の場合は生ログが出力されていなかったのでこのような形で…。 acp–response-senderacp--log の呼び出しが漏れているだけだと思われます。 )

許可を得たので、改めて読み込むようです。

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "fs/read_text_file",
  "params": {
    "path": "/home/yewton/Projects/yewton.net/content-org/all-posts.org",
    "sessionId": "36702bdd-86f6-4e87-b964-c2328216f375",
    "line": null,
    "limit": null
  }
}

そして、 fs/write_text_file で書き込みます。

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "fs/write_text_file",
  "params": {
    "path": "/home/yewton/Projects/yewton.net/content-org/all-posts.org",
    "content": "...ファイルの中身全量...",
    "sessionId": "36702bdd-86f6-4e87-b964-c2328216f375"
  }
}

このように全量を一括書き込みするようなので、あまりにも大きいファイルだと注意が必要かもしれません ( エディタ上で開いて編集出来ている時点で、ほぼ問題無いとは思いますが )。

将来性

ACP は発表されたばかりの発展途上プロトコルであるとは思いますが、一過性のブームとはならず、今後の展望も明るいんではないかと考えています。

まずは先に述べたように、 JetBrains と Zed という、クライアント( エディタ ) 側の大きなプレイヤーが牽引しているということ。

Visual Studio Code にも以下 Issue が立てられており、盛り上がりを見せています。

Add support in vscode for Agent Client Protocol (ACP) #265496

そして、人間がエージェントの対面にいる限りは、人間のツールであるエディタを通じたやりとりを形式化・標準化することは今後も求められていくでしょう。

以下、具体的に将来起こりそうなことについてピックアップして取り上げます。

GitHub Copilot CLI でのサポート

GitHub Copilot CLI には、まだ公式ドキュメントやコマンドの help にも記載されていませんが、ACP を利用するための --acp オプションが実験的に追加されているようです( 2026年1月12日現在 )。

agent-shell では以下のように設定すると動かせます ( --acp フラグが肝 )。

(cl-defun agent-shell-gh-copilot-make-client (&key buffer)
  "Create an GitHub Copilot client using BUFFER as context."
  (unless buffer
    (error "Missing required argument: :buffer"))
  (agent-shell--make-acp-client
   :command "copilot"
   :command-params '("--acp")
   :environment-variables nil
   :context-buffer buffer))

(defun agent-shell-gh-copilot--welcome-message (config)
  "Return GitHub Copilot welcome message using `shell-maker' CONFIG."
  (let ((art (agent-shell--indent-string 4 (agent-shell-gh-copilot--ascii-art)))
        (message (string-trim-left (shell-maker-welcome-message config) "\n")))
    (concat "\n\n"
            art
            "\n\n"
            message)))

(defun agent-shell-gh-copilot--ascii-art ()
  "GitHub Copilot ASCII art."
  (let* ((is-dark (eq (frame-parameter nil 'background-mode) 'dark))
         (text (string-trim "
┌──                                                                         ──┐
│                                                           ▄██████▄          │
    Welcome to GitHub                                   ▄█▀▀▀▀▀██▀▀▀▀▀█▄
    █████┐ █████┐ █████┐ ██┐██┐     █████┐ ██████┐     ▐█      ▐▌      █▌
   ██┌───┘██┌──██┐██┌─██┐██│██│    ██┌──██┐└─██┌─┘     ▐█▄    ▄██▄    ▄█▌
   ██│    ██│  ██│█████┌┘██│██│    ██│  ██│  ██│      ▄▄███████▀▀███████▄▄
   ██│    ██│  ██│██┌──┘ ██│██│    ██│  ██│  ██│     ████     ▄  ▄     ████
   └█████┐└█████┌┘██│    ██│██████┐└█████┌┘  ██│     ████     █  █     ████
    └────┘ └────┘ └─┘    └─┘└─────┘ └────┘   └─┘     ▀███▄            ▄███▀
│                                                       ▀▀████████████▀▀      │
└──                                                                         ──┘
" "\n")))
    (propertize text 'font-lock-face (if is-dark
                                         '(:foreground "#4a9eff" :inherit fixed-pitch)
                                       '(:foreground "#2563eb" :inherit fixed-pitch)))))

(setq agent-shell-agent-configs
      (list (agent-shell-opencode-make-agent-config)
            (agent-shell-google-make-gemini-config)
            (agent-shell-make-agent-config
             :mode-line-name "GitHub Copilot"
             :buffer-name "GitHub Copilot"
             :shell-prompt "copilot> "
             :shell-prompt-regexp "copilot> "
             :welcome-function #'agent-shell-gh-copilot--welcome-message
             :client-maker (lambda (buffer)
                             (agent-shell-gh-copilot-make-client :buffer buffer))
             :install-instructions "See TBW for installation.")))
Note

なお、 Gemini CLI もそうですが GitHub Copilot も session/set_model ( agent-shell 的には default-model-id )には現時点で対応していません。

Claude Code など一部のエージェントは対応しており、半分デファクトスタンダードのようですが、プロトコル上はまだ未定義であり UNSTABLE とされています。

将来的には Copilot も ACP 準拠のエージェントとして、様々なクライアントから統一的に利用できるようになる可能性があります。

Support for ACP (Agent Client Protocol) #222 をウォッチしましょう。

ドラフト仕様の展望

現在議論されているドラフト仕様の中でも、特に以下の 2 つはエージェントの能力を最大限に引き出すことに寄与しそうです。

  • Forking of existing sessions: 既存のセッションを複製し、異なるコンテキストでタスクを並行実行させる機能。
  • Agent Extensions via ACP Proxies: クライアントとエージェント間に新たに設けられるプロキシによる柔軟な機能拡張の仕組み。

これらの機能は、Agent Skills のような外部スキルセットとの連携や、特定のタスクに特化したサブエージェントの活用といった、より高度なエージェントアーキテクチャの規範を定義するものになると思います。

特定のベンダーにロックインされない AI エージェントの利活用に必須のものになっていくのではないでしょうか。 もし仮に利用が必須でなくとも、その概念を理解することは有用なはずです。

まとめ

Agent Client Protocol は、AI エージェントを利用した開発の未来を具体化するプロトコルです。 AI エージェントとは何か、何が出来る必要があるのか、また今後何が出来るようになることを求められていくのかをキャッチアップする為に、ウォッチしていく価値のあるものだと考えています。

著者
ソフトウェアエンジニア
父親兼エンジニア

comments powered by Disqus