3.9 KiB
引数なし tool 呼び出しで arguments = "null" が記録される不具合
背景
引数を取らないツール(例: ListPods)を Anthropic の Claude が呼び出したとき、次ターンで履歴を送り返す際に Anthropic API が以下のエラーで 400 を返す:
messages.N.content.0.tool_use.input: Input should be a valid dictionary
実環境で cargo run -p pod + TUI / API 経由で ListPods を呼ぶと再現する。セッション jsonl には tool 呼び出しが以下の形で記録されている:
{"type":"tool_call","call_id":"toolu_...","name":"ListPods","arguments":"null"}
arguments が "null" 文字列になっており、次ターンで Anthropic に送る tool_use.input が JSON null として serialize されてしまうことが原因。
原因
crates/llm-worker/src/timeline/tool_call_collector.rs:87-88:
let input = serde_json::from_str(&scope.input_json_buffer)
.unwrap_or(serde_json::Value::Null);
Anthropic は引数なしのツール呼び出しでは input_json_delta を一度も送らない。その結果 input_json_buffer が空文字 "" のまま stop イベントに到達し、from_str("") が失敗して Value::Null に fallback する。
この Null が worker.rs:499 で Item::tool_call_json(..., Value::Null) として履歴に保存され、Value::Null.to_string() = "null" が arguments フィールドに残る。
次ターンで anthropic/request.rs:174-175 が history → request body 変換する際:
let input = serde_json::from_str(arguments)
.unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
"null" は valid JSON として parse 成功するため fallback が効かず、Value::Null のまま tool_use.input に入り API に送信される。Anthropic の tool_use.input は object 必須なので拒否される。
修正方針
ルート修正を tool_call_collector.rs に入れる。引数なし / パース失敗の場合は Value::Object(Map::new())(= {})にする:
let input = if scope.input_json_buffer.is_empty() {
serde_json::Value::Object(serde_json::Map::new())
} else {
serde_json::from_str(&scope.input_json_buffer)
.unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()))
};
加えて防御層として anthropic/request.rs:174 で parse 結果が object でない場合も {} に正規化する。既に arguments = "null" として保存済みの古いセッションが resume されたときに回復できるようにするため。
影響範囲
crates/llm-worker/src/timeline/tool_call_collector.rs: 空バッファ時の default をValue::Objectにcrates/llm-worker/src/llm_client/scheme/anthropic/request.rs: parse 結果が非 object の場合の正規化crates/llm-worker/src/worker.rs:576-577の同様の parse でも非 object を正規化(防御)- OpenAI / Gemini の request.rs でも同等の問題があるか確認、必要なら同じ修正を入れる
完了条件
- 引数なしツール(
ListPodsなど)を呼んだ直後のセッション jsonl でargumentsが"{}"になる - 同一セッション内で引数なしツールを呼んでから次ターンを開始しても 400 エラーが出ない
"arguments":"null"が残っている既存セッションを resume しても 400 エラーが出ないtool_call_collector.rsに「空バッファ →{}」を検証するテストを追加- 該当パスの回帰テストを単体テストで担保
範囲外
- LLM 側が「無意味に
nullをinputに入れてくる」ケースの検出・警告(そもそもプロバイダから来ないはず) - OpenAI / Gemini の同等検証(確認だけ行い、問題あれば別チケット化)
- 既存セッション jsonl の自動修復スクリプト(resume 時の defensive 正規化で十分)