CodeCompanion.nvim + Gemini CLIで四苦八苦

プロローグ

Neovimでは、olimorris/codecompanion.nvimというプラグインによってAIと連携したコーディングを行うことができます。 筆者はこれまでは、直接GoogleのGeminiのAPIを叩くことでAI Chatを開き、指定したバッファの内容を与えてコーディングを行う設定は行っていました。 しかし、この設定では、コーディングエージェントとしてソースコードを生成させたりするようなことはできません。

エディタやIDEとコーディングエージェントのようなAIエージェントとはACP(Agent Client Protocol)というプロトコルによってデータのやりとりをすることが可能で、 有名どころでは、ZedやJetbrainsの各種IDEが対応しています。 NeovimでもCodeCompanion.nvimとによってACPに対応したAIエージェントと連携することが可能です。

筆者はCodeCompanion.nvimで使うAIのサービスとしては、GoogleのGeminiを使用していたので、 Geminiのエージェントである、Gemini CLIをNeovimから使えるように設定を行うことにしました。

とりあえず設定してみたものの…

CodeCompanionの公式のドキュメントを参考にして、とりあえず設定してみました。 今回追加した設定は、acp = {...}の部分です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
local my_adapter = { name = 'gemini_cli', model = 'gemini-2.5-flash' }
require('codecompanion').setup({
  adapters = {
    http = {
      gemini = function()
        return require('codecompanion.adapters').extend('gemini', {
          env = { api_key = vim.env.GEMINI_API_KEY },
        })
      end,
      azure_openai = function()
        return require('codecompanion.adapters').extend('azure_openai', {
          env = {
            api_key = vim.env.AZURE_OPENAI_API_KEY,
            endpoint = vim.env.AZURE_OPENAI_END_POINT,
          },
        })
      end,
    },
    -- 今回追加した設定はコレ
    acp = {
      gemini_cli = function()
        return require('codecompanion.adapters').extend('gemini_cli', {
          defaults = {
            auth_method = 'gemini-api-key', -- "oauth-personal"|"gemini-api-key"|"vertex-ai"
          },
          env = { GEMINI_API_KEY = vim.env.GEMINI_API_KEY },
        })
      end,
    },
  },
  -- Action Palette
  display = {
    action_palette = {
      width = 95,
      height = 10,
      prompt = 'Prompt ', -- Prompt used for interactive LLM calls
      provider = 'fzf_lua',
      opts = {
        show_preset_actions = true, -- Show the preset actions in the action palette?
        show_preset_prompts = true, -- Show the preset prompts in the action palette?
        title = 'CodeCompanion actions', -- The title of the action palette
      },
    },
  },

  opts = {
    language = 'Japanese',
    log_level = 'DEBUG',
  },
  interactions = {
    chat = {
      adapter = my_adapter,
      slash_commands = {
        ['buffer'] = {
          opts = {
            provider = 'fzf_lua',
          },
        },
        ['file'] = {
          opts = {
            provider = 'fzf_lua',
          },
        },
        ['help'] = {
          opts = {
            provider = 'fzf_lua',
          },
        },
        ['symbols'] = {
          opts = {
            provider = 'fzf_lua',
          },
        },
        ['workspace'] = {
          opts = {
            provider = 'fzf_lua',
          },
        },
      },
    },
    inline = {
      adapter = my_adapter,
    },
    agent = {
      adapter = my_adapter,
    },
  },
})

しかし、この設定下でチャットを開き、対話を初めようとすると以下のエラーが出てチャットができませんでした。

1
[ACP::Handler] Internal error

CodeCompanionのログである、$HOME/.local/state/nvim/codecompanion.logを見ても同じメッセージしかなくて問題の特定ができませんでした。 ログのレベルをopts.log_level = 'TRACE'に設定して、より詳細なデバッグ情報を取得するようにしても結果は同じでした。

コードを手探る

どのへんを見れば原因が特定できそうか?

まず、ソースに対してgrepしてエラーメッセージを出している場所の当りをつけてみます。

1
2
3
grep -rn ACP::Handler .
./lua/codecompanion/interactions/chat/acp/handler.lua:239:    log:debug("[ACP::Handler] Failed to update tool call line for toolCallId %s", tool_call.toolCallId)
./lua/codecompanion/interactions/chat/acp/handler.lua:303:  log:error("[ACP::Handler] %s", error)

lua/codecompanion/interactions/chat/acp/handler.lua:303が怪しく見えますね。 そこで、この行に対応する関数ACPHandler:handle_error()にブレークポイントを貼って実行してみると 、確かにその関数が呼ばれることでエラーメッセージが出力されることが分かりました。

その関数を呼び出す関数をデバッガやLSPによる定義ジャンプで探索したところ、 lua/codecompanion/acp/init.luaに実装されている関数Connection:handle_rpc_message()が 鍵となると考えました。その関数の実装を以下に示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
---Handle incoming JSON message
---@param line string
function Connection:handle_rpc_message(line)
  if not line or line == "" then
    return
  end

  -- If it doesn't look like JSON-RPC, skip it
  if not line:match("^%s*{") then
    return
  end

  local ok, message = pcall(self.methods.decode, line)
  if not ok then
    return log:error("[acp::handle_rpc_message] Invalid JSON:\n%s", line)
  end

  if message.id and not message.method then
    self:store_rpc_response(message)
    if message.result and message.result ~= vim.NIL and message.result.stopReason then
      if self._active_prompt and self._active_prompt.handle_done then
        self._active_prompt:handle_done(message.result.stopReason)
      end
    end
  elseif message.method then
    self:handle_incoming_request_or_notification(message)
  else
    log:error("[acp::handle_rpc_message] Invalid message format: %s", message)
  end

  if message.error and message.error.code ~= -32603 then
    log:error("[acp::handle_rpc_message] Error: %s", message.error)
  end
end

この関数では、サーバー(ここではAIエージェントであるGemini CLI)からのレスポンスをLuaのテーブルにデシリアライズしてstore_rpc_response()に渡しています。 さらにこのstore_rpc_response()はレスポンスのうちエラー部分のみPromptBuilder:handle_error()に渡して結果エラーが出力がされています。

このことから、デシリアライズされたテーブルを全て見ればログに表われないメッセージを見れそうに考えました。

当りは付いたので、ロガーを入れて確かめてみる

ここまでで当りはついたので、ロガーを入れてメッセージを見てみました。

パッチを当てる時はプラグインマネージャーlazy.nvimの設定を

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  'olimorris/codecompanion.nvim',
  dependencies = {
    'nvim-lua/plenary.nvim',
    'nvim-treesitter/nvim-treesitter',
  },
  opts = {},
  lazy = false,
  dir = '/home/aki/Src/codecompanion.nvim_18.5/codecompanion.nvim_debug_acp',
}

のように適当な場所に置いたパッチを当てる前提のコードへのパスをdir = <path>で与えると作業がしやすいです。

CodeCompanion.nvimのv18.5.0(e0780fa9fda504ffb89307cabcb6cbe1ce8eb60c) に以下のようなパッチを当ててログを見てみました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
diff --git a/lua/codecompanion/acp/init.lua b/lua/codecompanion/acp/init.lua
index 7bd8ce3e..5c2f4bb8 100644
--- a/lua/codecompanion/acp/init.lua
+++ b/lua/codecompanion/acp/init.lua
@@ -467,6 +467,14 @@ function Connection:handle_rpc_message(line)
     return log:error("[acp::handle_rpc_message] Invalid JSON:\n%s", line)
   end
 
+  local log_file = io.open("/home/aki/Src/codecompanion.nvim_18.5/gemini_cli_debug.log", "a")
+  if log_file then
+    log_file:write("--- RAW RESPONSE START ---\n")
+    log_file:write(vim.inspect(message))
+    log_file:write("\n--- RAW RESPONSE END ---\n")
+    log_file:close()
+  end
+
   if message.id and not message.method then
     self:store_rpc_response(message)
     if message.result and message.result ~= vim.NIL and message.result.stopReason then

保存されたログを見てみると、Internal errorと出力される時のレスポンスが以下のようになっていることが分りました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  error = {
    code = -32603,
    data = {
      details = "You have exhausted your daily quota on this model."
    },
    message = "Internal error"
  },
  id = 4,
  jsonrpc = "2.0"
}

このレスポンスからは、レート制限に引っかかったことが原因のように見えますが、 Google AI Studioの画面を見ても、gemini-2.5-flashでレート制限は確認されませんでした。

そのため、原因としてはうまくモデルの選択ができていないことであると考えられます。

commands.default = {}で明示的にモデル名を与える => 動く

モデル選択がうまくできないなら、Gemini CLIのコマンドラインオプションのレベルで指定すれば動くのではと考え、 以下のように設定しました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
acp = {
  gemini_cli = function()
    return require('codecompanion.adapters').extend('gemini_cli', {
      defaults = {
        auth_method = 'gemini-api-key', -- "oauth-personal"|"gemini-api-key"|"vertex-ai"
      },
      commands = {
        default = {
          'gemini',
          '--experimental-acp',
          '--model',
          'gemini-2.5-flash',
        },
      },

      env = { GEMINI_API_KEY = vim.env.GEMINI_API_KEY },
    })
  end,
},

モデル名をハードコードした柔軟性に欠けるコードですが、無事正常に動作しました。

モデル選択をスマートに

流石にモデル名のハードコードは嫌なので、最終的にチャットを開く時に選択できるように設定しました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
gemini_cli = function()
  return require('codecompanion.adapters').extend('gemini_cli', {
    defaults = {
      auth_method = 'gemini-api-key', -- "oauth-personal"|"gemini-api-key"|"vertex-ai"
    },
    commands = {
      default = {
        'gemini',
        '--experimental-acp',
        '--model',
        model_selector({ 'gemini-2.5-flash', 'gemini-2.5-flash-lite' }, 'gemini-2.5-flash'),
      },
    },

    env = { GEMINI_API_KEY = vim.env.GEMINI_API_KEY },
  })

ここで、model_selector()の実装は以下で、vim.ui.select()によってインタラクティブにモデル選択ができるようにしました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
local model_selector = function(models, default_model)
  local model = default_model

  vim.ui.select(models, {
    prompt = 'Select model:',
    format_item = function(item)
      return item
    end,
  }, function(choice)
    if choice then
      model = choice
    end
  end)

  return model
end

感想

エラーが発生した時に一部しか出力&ロギングされないため、原因が特定できなくて大変でした。 ロギングは大切さが身に染みるような気がします。

怪しい箇所の特定には前に行なったデバッガの設定がとても約に立ちました。

CC BY
Hugo で構築されています。
テーマ StackJimmy によって設計されています。