yoi/tickets/tool-call-empty-args-null.md
2026-04-19 08:20:07 +09:00

3.9 KiB
Raw Blame History

引数なし 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 する。

この Nullworker.rs:499Item::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 側が「無意味に nullinput に入れてくる」ケースの検出・警告(そもそもプロバイダから来ないはず)
  • OpenAI / Gemini の同等検証(確認だけ行い、問題あれば別チケット化)
  • 既存セッション jsonl の自動修復スクリプトresume 時の defensive 正規化で十分)