Claude Media
tool_useとpause_turnの違い — サーバーツール混在時の判定

tool_useとpause_turnの違い — サーバーツール混在時の判定

サーバーツールとクライアントツールが同一ターンで混在すると、stop_reasonはpause_turnではなくtool_useになります。判定方法と、tool_resultのみで応答しないと400エラーになる条件を扱います。

server_tool_useとクライアントのtool_useが同じターンに同居する場面

Claudeはweb_fetchのようなサーバーツールと、ユーザー定義のツールやBashツールのようなクライアントツールを、同じ並列ツール呼び出しのグループで同時に呼び出せます。このとき何が起きるかは、サーバーツール単独の長時間ループとは仕様が異なります(web_fetch単体の実行制約はurl_not_in_prior_contextエラーの原因で扱っています)。

server_tool_useブロックとは、Web検索やWeb FetchのようにAPI側が実行するツール呼び出しを表すコンテンツブロックです。srvtoolu_プレフィックスのidを持ち、通常は同じアシスタントターンの中で結果ブロックとペアになって返ります。

混在グループではこのペアが崩れます。APIはサーバーツールを実行せず、すぐに応答を返して、クライアントツールを先に実行させます。

混在時にstop_reasonがtool_useになる3つの見分け方

サーバーツールとクライアントツールが混在したとき、レスポンスは次の3点で識別できます。

  • stop_reasonは"pause_turn"ではなく"tool_use"になる
  • contentにserver_tool_useブロックとクライアントのtool_useブロックの両方が含まれるが、サーバーツールの結果ブロックはまだ無い(その呼び出しは未完了)
  • 判定に使える専用のマーカーは無い。「server_tool_useブロックのうち、同じレスポンス内に対応する結果ブロックを持たないもの」を探すのが検出方法になる。MCP connector経由のmcp_tool_useブロックも同じ挙動をする
{
  "stop_reason": "tool_use",
  "content": [
    { "type": "text", "text": "I'll fetch the article and check your system at the same time." },
    {
      "type": "server_tool_use",
      "id": "srvtoolu_01HxbWnMRmbWyMfUtJKC45rA",
      "name": "web_fetch",
      "input": { "url": "https://example.com/article" }
    },
    {
      "type": "tool_use",
      "id": "toolu_01PjgRJLbXrXEMZwDNYLnBqk",
      "name": "run_command",
      "input": { "command": "uname -a" }
    }
  ]
}

このレスポンスではweb_fetchのserver_tool_useブロックに結果が付いていません。呼び出しは発行されたものの、まだ実行されていない状態です。

同じ応答に結果ブロックが揃っているサーバーツール呼び出しは完了済みで、こちらからは何もしなくて済みます。未完了のidを拾う判定は、次のように書けます。

def unfinished_server_calls(content):
    done = {b.get("tool_use_id") for b in content
            if b["type"].endswith("_result") and "tool_use_id" in b}
    return [b["id"] for b in content
            if b["type"] in ("server_tool_use", "mcp_tool_use")
            and b["id"] not in done]

上のJSONを渡すと['srvtoolu_01HxbWnMRmbWyMfUtJKC45rA']が返り、結果ブロックが揃ったレスポンスでは空のリストになります。

続きの送り方 — tool_resultだけを含む1通のメッセージ

手順

混在ターンが完了するまでの往復

  1. 1

    1回目のレスポンスを受け取る

    stop_reasonはtool_use。server_tool_useは結果なしで、クライアントのtool_useが1つ以上並んでいます。

  2. 2

    クライアントツールだけ自分で実行する

    サーバーツールは実行しません。tool_useブロック1つにつきtool_resultを1つ作ります。

  3. 3

    tool_resultだけのユーザーメッセージを送る

    tools配列は1回目と同じものを渡します。

  4. 4

    2回目のレスポンスを受け取る

    先頭に保留分の結果ブロックが付き、続いて新しい内容と新しいstop_reasonが来ます。

tools配列は継続リクエストでも同じものを維持しなければなりません。待機中のサーバーツールがtoolsから消えていると、継続リクエストは400エラーになります。メッセージの末尾はbut no `web_fetch` tool was providedのように、足りないツール名で終わります。

{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01PjgRJLbXrXEMZwDNYLnBqk",
      "content": "Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux"
    }
  ]
}

APIはこのtool_resultを、開いたままのアシスタントターンに結び付けます。そのうえで保留していたサーバーツールを実行し、Claudeにターンを継続させます。

server_tool_useブロックと結果ブロックはtool_use_idでペアになり、位置ではなく値で対応します。この例では2つの異なるレスポンスにまたがって出現し、2回目のレスポンスでserver_tool_useブロック自体が繰り返されることはありません。

pause_turnとtool_useは継続の送り方が違う

くらべる

未実行のサーバーツールが残る2つの応答

tool_resultを組み立てて送る

tool_use(混在ターン)

サーバーツールとクライアントツールが同じ並列呼び出しグループに混在したときの応答です。クライアントのtool_useブロックが必ず含まれます。続きは、そのクライアントツールのtool_resultだけを含むメッセージで送ります。

応答をそのまま再送する

pause_turn(サーバーツール単独)

サーバーツールのループが反復の上限に達してAPIが一時停止した応答です。クライアントのtool_useブロックは含まれず、pause_turnになることもありません。直前のアシスタントのcontentを次のリクエストのmessagesに追加して再送します。

どちらの応答も、未実行のサーバーツール呼び出しを残して返ります。次のリクエストの冒頭でAPIが保留分を実行する点も共通です。stop_reasonを見て送るものを切り替えます。

バッチ処理でサーバーツールのループがターンあたりの反復上限に達したときも、pause_turnと同じ形で応答が返ります。バッチのワーカーは同期リクエストより反復回数が多く、返ってきたpause_turnは後続のリクエスト(バッチでも同期でも)で再開できます。

pause_turnの継続には、公式ドキュメントが3つの注意点を挙げています。1つ目は、一時停止した応答をそのまま次のリクエストへ再送すること。2つ目は、継続リクエストでも同じtoolsを維持することです。一時停止中のserver_tool_useブロックはまだ実行されておらず、そのツールがtoolsから消えていると、tool_use混在ケースと同じようにAPIは検証エラーを返します。

3つ目は、pause_turnが1回で終わるとは限らない点です。継続したターンが再びpause_turnで止まることがあります。レスポンスごとにstop_reasonを確認し、リトライループと同じ発想で継続回数に上限を設けます。

継続リクエストの組み立てを誤ると400エラーになる2つのパターン

継続メッセージの形が崩れると、エラーは2種類に分かれます。

1つ目は、tool_resultだけで構成すべき続きのメッセージに、テキストなど他のブロックを混ぜるケースです。結果の後ろに足したブロックは、アシスタントのターンが終わったとAPIに伝えます。サーバーツールを直接呼び出していた場合、そのサーバーツール呼び出しが未解決のまま残り、次のメッセージを含む400 invalid_request_errorになります。

`web_fetch` tool use with id `srvtoolu_01HxbWnMRmbWyMfUtJKC45rA` was found without a corresponding `web_fetch_tool_result` block

2つ目は、tool_resultの前に別の内容を置く、一部のtool_useのidにしか答えていない、tool_resultが1つも含まれていない、のいずれかです。こちらはより早い段階で、クライアントツールのエラーとして弾かれます。

`tool_use` ids were found without `tool_result` blocks immediately after: toolu_01PjgRJLbXrXEMZwDNYLnBqk. Each `tool_use` block must have a corresponding `tool_result` block in the next message.

クライアントツールだけのターンでは、tool_resultをすべて先頭に並べれば、その後ろにテキストを置けます。「結果の後ろにテキストを足してはいけない」のは、未完了のサーバーツールが同じターンに残っているときだけです。既存のコードにテキストを併送する処理があると、混在を有効にした途端に1つ目のエラーで落ちます。

Claudeに追加の情報を渡したいときは、ターンが完了してから別のユーザーメッセージとして送ります。継続リクエストの中に混ぜることはできません。

エラーメッセージは、1つ目がサーバーツールのidとツール名を、2つ目が答えの無いtool_useのidを名指しします。汎用の400ハンドリングでひとまとめにすると、この情報を捨てることになります。

ストリーミングでも識別方法は変わらない

ストリーミングでもserver_tool_useブロックの見分け方は同じです。Claudeが直接呼び出すserver_tool_useブロックは、クライアントのtool_useブロックと同じように、content_block_startイベントに続けてinput_json_deltaイベントが流れます。

違うのは結果ブロックです。web_fetch_tool_resultのようなサーバーツールの結果は差分(delta)を経由せず、content_block_startイベント1回で丸ごと届きます。混在ターンでサーバーツールが未完了のまま応答が終わるときは、この結果ブロックのイベントがストリームに現れません。結果イベントの有無を見れば、非ストリーミングと同じ判定基準が使えます。

単独呼び出しのときは何も変わらない

ここまでの分岐が生じるのは、サーバーツールとクライアントツールが同じ並列呼び出しグループに混在したときだけです。クライアントのtool_useブロックしか無いターンは、従来どおりのtool_result往復で継続します。サーバーツールしか無いターンは、結果ブロックが通常すでに揃っています。保留で返ってきた場合は、pause_turnと同じ形で再送します。

クライアントツールのみ、あるいはサーバーツールのみを前提にした既存の実装は、そのまま動きます。両者を新たに1つのリクエストへ同居させるときに、継続方法を切り替えるコードを足します。

プログラマティックツールコーリングでは同じ形が別の意味になる

Programmatic Tool Callingを使うと、tool_useが保留になっている同じレスポンス形状が別の状態を意味します。クライアントのtool_useブロックはClaudeが直接呼んだものではなく、code_executionツールの中で実行中のコードから呼ばれたものです。

見分ける手がかりはcallerフィールドです。直接呼び出しでは{"type": "direct"}、コードからの呼び出しではcode_executionのバージョンと、呼び出し元のserver_tool_useブロックのidが入ります。

{
  "type": "tool_use",
  "id": "toolu_xyz789",
  "name": "query_database",
  "input": { "sql": "<sql>" },
  "caller": {
    "type": "code_execution_20260120",
    "tool_id": "srvtoolu_abc123"
  }
}

tool_idは、このツール呼び出しを出したcode_executionブロックのidです。コードはすでに動き始めていて、tool_resultを待って一時停止しています。tool_resultを送ると実行が再開されるのであって、新しいツールを起動するわけではありません。code_executionブロック自体の結果はコードの終了時に届き、それまでにtool_resultの往復が複数回続くこともあります。

フォローアップのユーザーメッセージの形は通常の混在ケースと同じで、tool_resultだけを入れます。ただし次の3点が加わります。

  • 待っている呼び出しがあるあいだは、レスポンスのcontainerフィールドのidを次のリクエストにも付けます。付けないとAPIはリクエストを拒否します
  • 直接呼び出しのときと違い、メッセージにテキストを足すことはできません。結果の後ろでも不可です
  • プログラム側の呼び出しに答えるtool_resultのcontentは、文字列かtextブロックに限られます。画像やドキュメントのブロックは拒否されます

分岐判定を自分で書かずに済ませる方法はあるか

このtool_useとtool_resultの往復判定を手で書きたくない場合は、Tool RunnerのようなSDK側の自動化機能が候補になります。ただしTool Runnerの公式ドキュメントのページは、サーバーツールとクライアントツールの混在やpause_turnの扱いには触れていません。

「未完了のserver_tool_useを検出してから継続する」という分岐を、どこまで肩代わりしてくれるかは、組み込む前に自分の環境で試して確かめる部分です。コード実行とMCPの設計は、サーバー側の実行とクライアント側の実行が入り混じる場面の設計を考える材料になります。

まとめ

サーバーツールとクライアントツールが混在すると、stop_reasonはpause_turnではなくtool_useになります。再送するのは応答そのものでなく、クライアントツールのtool_resultだけを入れたメッセージです。

混在を初めて有効にするコードでは、tools配列を使い回しているか、結果の後ろにテキストを併送していないかを先に見てください。この2点は、どちらも400エラーに直結します。

この記事を共有:XはてブLinkedIn