Claude Media
Compactionの一時停止で合計トークン予算を強制する実装パターン

Compactionの一時停止で合計トークン予算を強制する実装パターン

pause_after_compactionはCompactionの直後にAPIを止めるオプションです。人間承認を挟むパターンとエージェントループ全体のハード上限を作るパターンの2通りを実装します。

pause_after_compactionとは何か

pause_after_compaction は、Messages APIのCompaction機能(compact_20260112)が要約を生成した直後にレスポンスを一度止めるブール値のオプションです。既定は false で、有効にすると要約が終わった時点でAPIが stop_reason: "compaction" のメッセージを返し、そこで処理が止まります。会話の続きに手を加えてから再開できる、というのがこのオプションの価値です。

要約が生成されてから会話が続くまでの間に割り込めるということは、そこに任意の制御ロジックを挟めるということでもあります。代表的な用途は2つあります。要約のたびに人間の承認を挟む運用と、圧縮の回数からトークン消費を見積もってエージェントループ全体にハード上限をかける運用です。

一時停止後に何が起きているか

pause_after_compaction: true を付けたリクエストが要約を生成すると、レスポンスの content には compaction ブロックだけが入り、stop_reason"compaction" になります。呼び出し側はこの応答を assistant メッセージとしてそのままメッセージ配列に追加し、必要な変更を加えたうえで再度リクエストを送ることで会話を継続します。

response = client.beta.messages.create(
    betas=["compact-2026-01-12"],
    model="claude-opus-5",
    max_tokens=4096,
    messages=messages,
    context_management={
        "edits": [{"type": "compact_20260112", "pause_after_compaction": True}]
    },
)
 
if response.stop_reason == "compaction":
    # レスポンスにはcompactionブロックだけが入っている
    messages.append({"role": "assistant", "content": response.content})
    # ここで会話に手を加えてから再開する

「手を加える」とは、直近のメッセージを保持し直す、指示的なメッセージを差し込む、あるいは会話をそこで打ち切る、といった操作を指します。この間の処理は同期的なAPI呼び出しの合間に挟まるだけなので、追加のインフラを用意しなくても実装できます。

APIが compaction ブロックを受け取ると、それより前のコンテンツブロックはすべて無視されます。呼び出し側の選択肢は2つです。元のメッセージ配列をそのまま持ち続けて圧縮済み部分の除外をAPI側に任せるか、圧縮されたメッセージを手元で削除して compaction ブロック以降だけを残すかです。前者は実装が単純な代わりにメッセージ配列自体は肥大化し続け、後者はクライアント側のメモリを節約できる代わりに「どこまでが圧縮済みか」を自前で追跡する必要があります。一時停止パターンを実装するときは、この2択のどちらを採るかを先に決めておくと、承認フローや予算カウンタのロジックがシンプルになります。

いつ止めるかはtriggerが決める

pause_after_compaction は「止めた後どう振る舞うか」を決めるだけで、「いつ止めるか」は別のパラメータ trigger が決めます。trigger.type は現状 input_tokens のみで、trigger.value が入力トークン数の閾値です。既定は150,000トークン、最小値は50,000トークンです。この閾値を小さくすると圧縮(そして一時停止)の頻度が上がり、人間承認のレビュー回数やトークン予算カウンタの更新頻度もそれに応じて増えます。

人間承認パターンでは、閾値を大きくしすぎるとレビューの間隔が空きすぎて1回あたりの差分が大きくなり、承認判断が難しくなります。逆に小さくしすぎると承認依頼が頻発して運用負荷が上がります。トークン予算パターンでは、閾値は「圧縮回数 × 閾値」という見積もり式の乗数そのものなので、閾値を変えると同じ TOTAL_TOKEN_BUDGET でも実際に許容されるツール呼び出し回数が変わります。両パターンとも、trigger.value をタスクの性質に合わせて事前にチューニングしておくことが前提になります。

パターン1: 要約のたびに人間承認を挟む

長時間タスクの節目ごとに、要約内容を人間がレビューしてから続行するかどうかを判断させる実装です。承認フローを持つ運用(本番環境への変更を伴うタスクや、コストが読みにくいタスク)に向いています。

def run_with_human_approval(client, messages, model="claude-opus-5"):
    while True:
        response = client.beta.messages.create(
            betas=["compact-2026-01-12"],
            model=model,
            max_tokens=4096,
            messages=messages,
            context_management={
                "edits": [
                    {
                        "type": "compact_20260112",
                        "trigger": {"type": "input_tokens", "value": 100000},
                        "pause_after_compaction": True,
                    }
                ]
            },
        )
 
        if response.stop_reason != "compaction":
            return response
 
        messages.append({"role": "assistant", "content": response.content})
 
        # compactionブロックの中身を人間に提示して承認を取る
        summary_block = next(
            b for b in response.content if b.type == "compaction"
        )
        approved = request_human_approval(summary_block.content)
 
        if not approved:
            messages.append(
                {
                    "role": "user",
                    "content": "Approval denied. Stop here and report the current state.",
                }
            )
            # 次のリクエストは通常どおり送るが、以降のcompactionは無効にしてもよい

request_human_approval は要約テキストをSlack通知やレビューUIに渡し、人間の承認・却下を待つ関数として実装します。却下時は会話に「停止して現状報告せよ」という user メッセージを追加し、Claudeに安全な形でタスクを畳ませます。承認が続く限りループは自動で先に進みます。

パターン2: エージェントループ全体のハード上限を作る

ツール呼び出しを繰り返す長時間タスクでは、圧縮の回数からおおよその累計トークン消費を見積もれます。公式ドキュメントは「圧縮回数 × トリガー閾値」を目安の下限として使う例を示しています。

TRIGGER_THRESHOLD = 100_000
TOTAL_TOKEN_BUDGET = 3_000_000
n_compactions = 0
 
response = client.beta.messages.create(
    betas=["compact-2026-01-12"],
    model="claude-opus-5",
    max_tokens=4096,
    messages=messages,
    context_management={
        "edits": [
            {
                "type": "compact_20260112",
                "trigger": {"type": "input_tokens", "value": TRIGGER_THRESHOLD},
                "pause_after_compaction": True,
            }
        ]
    },
)
 
if response.stop_reason == "compaction":
    n_compactions += 1
    messages.append({"role": "assistant", "content": response.content})
 
    if n_compactions * TRIGGER_THRESHOLD >= TOTAL_TOKEN_BUDGET:
        messages.append(
            {
                "role": "user",
                "content": "Please wrap up your current work and summarize the final state.",
            }
        )

これは公式サンプルの「予算超過を伝えて畳ませる」段階です。ここから一歩進めてハード上限にするなら、畳ませるメッセージを送った後の次のレスポンスで会話を強制終了する分岐を足します。

WRAP_UP_SENT = False
 
if n_compactions * TRIGGER_THRESHOLD >= TOTAL_TOKEN_BUDGET:
    if WRAP_UP_SENT:
        # 畳ませる指示を送った後でもまだ予算超過が続いている = 強制終了
        raise BudgetExceededError(
            f"Stopped after {n_compactions} compactions "
            f"(~{n_compactions * TRIGGER_THRESHOLD:,} tokens estimated)."
        )
    messages.append(
        {
            "role": "user",
            "content": "Please wrap up your current work and summarize the final state.",
        }
    )
    WRAP_UP_SENT = True

1回だけ「畳んでください」と猶予を与え、それでも次の圧縮を迎えたら例外を投げてループを止めます。呼び出し元のオーケストレーターがこの例外を捕まえて、タスクを失敗として記録するか、人間にエスカレーションするかを判断します。「圧縮回数 × トリガー閾値」はあくまで見積もりで、圧縮のたびに実際の入力トークン数を溜め込んだほうが正確な予算判定になりますが、usage フィールドを毎回記録する分だけ実装コストが上がるトレードオフがあります。

猶予を1回だけにするか、複数回にするかはタスクの性質で決めます。ファイル編集のようにいつ止めても状態が壊れにくいタスクなら1回の猶予で十分ですが、外部APIへの書き込みを何段階も重ねるタスクでは、途中で強制終了すると中途半端な状態が残ることがあります。その場合は「畳んでください」の代わりに「ここまでの変更をロールバック可能な状態でまとめてください」という指示に差し替え、ロールバック手順の記述を最後の要約に残させる設計のほうが安全です。ハード上限そのものは例外を投げるだけの単純な仕組みですが、投げた後にどう後始末するかは呼び出し元のオーケストレーター設計に依存します。

エージェントループとの関係

pause_after_compaction はMessages APIの単発リクエスト・レスポンスの中で完結する機能ですが、実際に使う場面の大半はツール呼び出しを繰り返すエージェントループの中です。ループの1往復(1ターン)ごとに入力トークンが積み上がっていくため、trigger の閾値に達するタイミングはタスクの複雑さに左右されます。単純な質問応答なら数ターンで終わり、圧縮が一度も起きないこともあります。逆にリポジトリ全体を横断編集するようなタスクでは、圧縮が何度も発生し、そのたびに一時停止のフックが呼ばれます。ループ全体の設計を先に押さえておくと、pause_after_compaction をどのターンで挟み込むべきかが見えやすくなります。

2つのパターンの使い分け

観点パターン1(人間承認)パターン2(トークン予算)
止める基準パターン1(人間承認)要約が生成されるたび全部パターン2(トークン予算)累計トークンが閾値を超えたときだけ
人手の関与パターン1(人間承認)毎回必要パターン2(トークン予算)超過時のみ(または不要)
向くタスクパターン1(人間承認)本番影響のある変更、コスト読みにくいタスクパターン2(トークン予算)バッチ処理、無人実行するエージェント
実装の複雑さパターン1(人間承認)承認UI・通知経路が別途必要パターン2(トークン予算)カウンタと閾値だけで完結
失敗時の挙動パターン1(人間承認)却下メッセージを送って安全に畳ませるパターン2(トークン予算)猶予後に例外で強制終了

両者は排他ではなく、pause_after_compaction という同じフックの上に別々の判断ロジックを載せているだけなので、組み合わせて「通常は自動続行、累計トークンが閾値を超えたときだけ人間承認に切り替える」実装も作れます。

ストリーミングで実装する場合の注意

stream: true でリクエストしている場合、compaction ブロックは他のコンテンツブロックとは違うイベント順で届きます。content_block_start イベントの後、要約全文を含む単一の content_block_delta が1回だけ届き(通常のテキストブロックのように少しずつ届くのではなく、要約全体が一度に来ます)、最後に content_block_stop イベントで終わります。人間承認パターンやトークン予算パターンをストリーミング実装に組み込む場合、要約の中身を読み取るタイミングはこの content_block_delta を受け取った瞬間になります。逐次的に届くテキストブロックと同じ扱いでバッファリングしようとすると、要約が1チャンクで丸ごと来ることを前提にしていないコードでは不具合の原因になります。

承認が滞留したときの扱い

人間承認パターンを本番で運用すると、承認依頼を出したまま誰も応答しない時間帯が発生します。request_human_approval をブロッキングな待受にすると、その間APIとの接続やプロセスがずっと保持され続け、コスト・リソースの両面で無駄が生じます。実務では、承認依頼を出した時点でいったんプロセスを終了し、承認結果を非同期のキューやWebhookで受け取ってから会話を再開する設計のほうが扱いやすくなります。この場合、再開に必要な状態(それまでの messages 配列と、承認待ちの compaction ブロック)を外部ストレージに永続化しておく必要があります。会話を止めている間もタスクの状態は変化しないため、永続化のタイミングは承認依頼を出した直後の1回で足ります。

TypeScriptで実装する場合

パターン2(トークン予算)をTypeScriptで書くと、判定ロジックはPythonとほぼ同じ形になります。

const TRIGGER_THRESHOLD = 100_000;
const TOTAL_TOKEN_BUDGET = 3_000_000;
let compactionCount = 0;
let wrapUpSent = false;
 
const response = await client.beta.messages.create({
  betas: ["compact-2026-01-12"],
  model: "claude-opus-5",
  max_tokens: 4096,
  messages,
  context_management: {
    edits: [
      {
        type: "compact_20260112",
        trigger: { type: "input_tokens", value: TRIGGER_THRESHOLD },
        pause_after_compaction: true,
      },
    ],
  },
});
 
if (response.stop_reason === "compaction") {
  compactionCount += 1;
  messages.push({ role: "assistant", content: response.content });
 
  if (compactionCount * TRIGGER_THRESHOLD >= TOTAL_TOKEN_BUDGET) {
    if (wrapUpSent) {
      throw new Error(
        `Stopped after ${compactionCount} compactions (~${compactionCount * TRIGGER_THRESHOLD} tokens estimated).`
      );
    }
    messages.push({
      role: "user",
      content: "Please wrap up your current work and summarize the final state."
    });
    wrapUpSent = true;
  }
}

compactionCountwrapUpSent をリクエストをまたいで保持する必要がある点はPython実装と同じです。エージェントループを関数呼び出しで包んでいる場合は、これらの変数をループの外側(クロージャやクラスのインスタンス変数)に置き、リクエストのたびに使い回す設計にします。

複数のセッションを並行して走らせる実装(Webサービスの裏で複数ユーザーのタスクを同時に処理する場合など)では、この2つの変数をセッションIDに紐づけて管理する必要があります。グローバル変数のまま複数セッション分を共有すると、あるユーザーのタスクの圧縮回数が別のユーザーのトークン予算判定に混ざってしまいます。セッションごとの状態はメモリ内のマップでもデータベースの1レコードでもよく、要はリクエストとリクエストの間でこの2値が正しく引き継がれる場所を1つに決めておくことが実装の要点です。

まとめ

pause_after_compaction は要約が生成された瞬間にAPIを止め、会話に手を加えてから再開する余地を作るオプションです。要約のたびに人間承認を挟む実装は本番影響のあるタスクに向き、圧縮回数から累計トークンを見積もってハード上限をかける実装は無人で走らせるバッチ的なエージェントに向きます。どちらも pause_after_compactiontrigger の組み合わせだけで作れるため、追加のインフラは不要です。エージェントループの内部モデルを押さえておくと、どのタイミングでトークンが消費されているかが見積もりやすくなります。長時間エージェントの文脈管理全体の設計は長時間稼働エージェントのハーネス設計、コスト管理の観点はPrompt Cachingの仕組みも参考になります。

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