原典: Compaction and preserved thinking / 日本語版

一行要約

要求時 compaction(compaction-on-demand)でブロックの後ろにターンを残し、その thinking を preserved thinking のモデルに送り返すときだけ読むページで、thinking が有効なままでいる3条件と、破っても compact は成功し、失敗は後のリクエストで初めて出る(環境によってはエラーにならず thinking が黙って捨てられる)ことを述べる。

要点

対象外なら読まなくてよい。 thinking ブロックを送り返さない、またはブロックの後ろにターンを残さない(全部要約させる)なら関係ない。残したターンとは、要約のリクエストに入れなかった直近のターン(keep recent turns)か、要約を書いている間に進んだターン(background)のこと。

Models with preserved thinking check earlier thinking blocks against the conversation that produced them. A summary replaces part of that conversation, but the check accepts the swap when the API wrote the summary, so the thinking in kept turns can stay valid.

preserved thinking のモデルは、以前の thinking ブロックを、それを生んだ会話と照合する。要約はその会話の一部を置き換えるが、API が書いた要約なら、検査はその差し替えを受け入れる。だから残したターンの thinking は有効なままでいられる。

自前の要約器ではこうはいかない(compaction の比較表で「保てない」側)。

3つの条件

  1. 要約のリクエストが preserved thinking のモデルで走ること。 thinking ブロックが生まれて以降のすべての compaction が対象で、最後の1回だけではない。満たす方法の一つは、要約のリクエストをいつも会話と同じモデルに送ること
  2. 残したターンが要約したメッセージの直後に続き、手を加えずに送ること。 最後に要約したメッセージと最初に残したメッセージの間で、飛ばしも挿入もしない。さらに最初に残したメッセージは、最後に要約したメッセージと role が違わなければならず、会話途中の role: "system" メッセージであってもならない(そうでないと API がそれを最後に要約したメッセージに合体させる)。正しくする方法の一つは、すでに送ったリクエストの messages をそのまま compact すること —— 残したターンはそれへの Claude の返答から始まる
  3. system と、defer_loading: true でない tools が変わらないこと。 thinking を生んだリクエスト、要約のリクエスト、その後のリクエストで同じ

破っても compact は失敗しない

If a condition doesn’t hold, nothing fails when you compact, and the API accepts the block on later requests either way. The failure comes on the first later request that sends the kept thinking where the API enforces the check: a 400 error by default, or dropped thinking blocks if the request sets thinking.block_binding.prefix_mismatch_behavior to "drop_block".

条件を満たしていなくても、compact の時点では何も失敗しない。API はどちらにせよ以降のリクエストでブロックを受け入れる。失敗が出るのは、検査が強制される環境で、残した thinking を送る最初のリクエストである。既定なら 400 エラー、リクエストが thinking.block_binding.prefix_mismatch_behavior を "drop_block" にしていれば thinking ブロックが捨てられる。

Message Batches API では、このフィールドを設定していない項目は失敗しない。 既定で検査がかかる環境では、代わりにブロックが捨てられる。

もう一度 compact する

新しいブロックは古い要約と、要約のリクエストでその後ろに続く全メッセージを覆い、リクエストに入れなかったターンは新しいブロックの残したターンになる。2回の compaction を越えて残すターンは、2回とも preserved thinking のモデルで走っている必要がある(条件1)。thinking が生まれる前の compaction は数えない。ブロックがある状態で生まれた thinking はそのブロックに結びつき、条件を満たす以降の compaction を越えて有効なままでいる。

system と tools を変えたいとき

後のリクエストで system・tools・モデルを変えても、API はブロックを受け入れる。 変わるのは、残したターンの thinking が無効になりうることだけ。

  • 残した thinking を壊さずに system や tools を変えるには、まず会話全体を compact して残すターンを無くし、次のリクエストで変える
  • system や tools に触れずに指示を足したりツールを変えたりするなら、変更を messages に追記する
  • 要約した範囲の会話途中の system メッセージは一緒に要約されて効力を失う。効かせ続けるなら、残したターンの後ろに来る最初の新しい user ターンの直後に role: "system" で言い直す
  • 要約した範囲のツール変更は、要約のリクエストに inline-tools-2026-09-15 も付けていれば、返るブロックの tool_changes フィールドにその正味の効果が記録されて自動で引き継がれる(ブロックは改変せずに送り返す)。tool_changes が無ければ同じように言い直す

A system message placed between the block and the kept turns breaks their thinking.

ブロックと残したターンの間に system メッセージを置くと、残したターンの thinking が壊れる。

条件を守れたかを確かめる

compaction の応答は、残した thinking が持つかどうかを教えない。分かるのは差し替え後の最初のリクエスト。 テストでは次の4段階で確かめる。

  1. thinking をオンにした短い会話をする。API が検査を走らせるモデルを使い、全段階で同じモデルにする(thinking ブロックを読めないモデルは、エラーを出さずにそれを捨てるから)
  2. 古いターンを compact し、thinking ブロックを持つターンを少なくとも1つ残す
  3. ブロック → 残したターン → 新しい user メッセージの順で次のリクエストを送る。thinking.block_binding.prefix_mismatch_behavior は "error"
  4. 結果を読む。200 で input_transformations が空なら、検査に落ちた・捨てられた thinking ブロックは無い。ブロックが別の会話に結びついていると言う 400 なら、落ちたものがある(メッセージは最初に落ちたブロックのパスから始まる)

prefix_mismatch_behavior には compact-2026-09-04 に加えて thinking-binding-controls-2026-08-01 が要る。このフィールドを設定すると、既定で検査がかからないアカウントでもそのリクエストに検査がかかる。

本番では "drop_block" にしておけば、条件を外してもリクエストは成功し続け、捨てたブロックを input_transformations に reason: "prefix_binding_mismatch" で報告する。path が残したターンに入っている項目があれば、そのターンの thinking は持たなかった。

そのまま使える具体例

4段階を1本にした検証プログラム(原典のまま。先頭の import anthropic は原典のタブでも省かれている):

from anthropic.types.beta import BetaMessageParam, BetaThinkingConfigParam
 
client = anthropic.Anthropic()
 
# Claude Fable 5.1 is the first model that checks sent-back thinking against the conversation.
MODEL = "claude-fable-5-1"
BETAS = ["compact-2026-09-04", "thinking-binding-controls-2026-08-01"]
SYSTEM = "You help plan a recipe app's release. Keep answers short."
# With "error", a thinking block that fails the check makes the request fail with a 400.
THINKING: BetaThinkingConfigParam = {
    "type": "adaptive",
    "block_binding": {"prefix_mismatch_behavior": "error"},
}
 
# 1. Have a short conversation with thinking on.
history: list[BetaMessageParam] = [
    {"role": "user", "content": "What are the main entities in the app's data model?"}
]
first = client.beta.messages.create(
    model=MODEL,
    max_tokens=8192,
    system=SYSTEM,
    betas=BETAS,
    thinking=THINKING,
    messages=history,
)
history += [
    {"role": "assistant", "content": first.content},
    {
        "role": "user",
        "content": "Testing starts on Tuesday, March 3, 2026, takes 10 weekdays, and pauses on March 9 and March 16. On which date does it end?",
    },
]
second = client.beta.messages.create(
    model=MODEL,
    max_tokens=8192,
    system=SYSTEM,
    betas=BETAS,
    thinking=THINKING,
    messages=history,
)
history.append({"role": "assistant", "content": second.content})
thinking_blocks = sum(block.type == "thinking" for block in second.content)
print(f"Thinking blocks in the kept turn: {thinking_blocks}")
 
# 2. Summarize the first turn. The second turn stays out of the request.
summary = client.beta.messages.create(
    model=MODEL,
    max_tokens=4096,
    system=SYSTEM,
    betas=BETAS,
    thinking=THINKING,
    messages=history[:2],
    compaction={"type": "summarize"},
)
if summary.stop_reason != "compaction":
    raise SystemExit(f"No summary: {summary.stop_reason}")
 
# 3. Put the block in front of the kept turn and ask the next question.
history = [
    {"role": "assistant", "content": summary.content},
    *history[2:],
    {"role": "user", "content": "Which day should the release go out?"},
]
third = client.beta.messages.create(
    model=MODEL,
    max_tokens=8192,
    system=SYSTEM,
    betas=BETAS,
    thinking=THINKING,
    messages=history,
)
 
# 4. A 200 with no dropped blocks means the kept thinking held.
print(f"Dropped thinking blocks: {len(third.input_transformations)}")

要約するのは1往復目(history[:2] = user → assistant)だけで、残すターン(history[2:])は2つ目の user とその返答。 要約した最後のメッセージ(assistant)と最初に残すメッセージ(user)の role が違うので、条件2の role の規則を満たす。

原典で言及されている関連文書