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

76 lines
3.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 引数なし 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 呼び出しが以下の形で記録されている:
```json
{"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`:
```rust
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 変換する際:
```rust
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())`= `{}`)にする:
```rust
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 正規化で十分)