백그라운드 컴팩션

백그라운드 컴팩션 (Compaction in the background)

백그라운드 컴팩션(비동기 컴팩션이라고도 불러요)은 컴팩션 루프에서 두 가지를 바꿔요. 컴팩션 요청은 대화가 전체 히스토리에서 계속되는 동안 실행되고, 교체는 블록이 도착할 때까지 기다려요. 요약에서 이어가기요약이 없거나 오류일 때 처리는 그대로 적용돼요.

출처: 문서

본문

백그라운드 컴팩션(비동기 컴팩션이라고도 불러요)은 컴팩션 루프에서 두 가지를 바꿔요: 컴팩션 요청은 대화가 전체 히스토리에서 계속되는 동안 실행되고, 교체는 블록이 도착할 때까지 기다려요. 요약에서 이어가기요약이 없거나 오류일 때 처리는 그대로 적용돼요.

작업이 계속되는 동안 교체가 동작하는 방식

컴팩션 요청과 그것이 반환하는 블록은 루프와 동일해요. 당신의 히스토리는 요청을 보내고 그 결과를 사용하는 사이에 커지고, 교체는 그 성장을 그대로 두어야 해요.

  1. 히스토리를 있는 그대로 컴팩션 요청을 보내고, 그것이 보유한 메시지 수를 기록하세요.
  2. 그 요청이 실행되는 동안 전체 히스토리에서 대화를 계속하세요. 각 새 턴을 추가하고, 이미 히스토리에 있는 것은 편집하지 마세요. 이 요청이 교체되거나 실패하기 전에는 다른 컴팩션 요청을 시작하지 마세요.
  3. 응답이 stop_reason "compaction"으로 도착하면, 보낸 정확히 그 메시지들을 히스토리 앞에서 버리고 반환된 메시지를 그 자리에 두세요. 1단계 이후 추가된 모든 턴은 그 뒤에 남아요.
  4. 블록이 도착한 후 첫 요청에 교체된 히스토리를 보내서, 요약이 작성되는 동안 만들어진 thinking이 유효하게 유지되게 하세요.

예를 들어 컴팩션 요청이 메시지 15를 보유하고, 실행되는 동안 대화가 메시지 68을 얻었다면, 교체 후 히스토리는 블록 다음에 메시지 6~8이 돼요.

Background compaction timeline: the compaction request is sent with messages 1 to 5 while the conversation continues on its full history and gains messages 6 to 8; when the block arrives, it replaces messages 1 to 5 at the front of the history, and the history becomes the block followed by messages 6 to 8

응답에 다른 stop_reason이 있으면 요약이 생성되지 않은 것이고, 이는 2단계에서 실패로 간주돼요. 전체 히스토리를 유지하세요. 요약이 없거나 오류일 때 처리가 원인과 각각의 조치를 나열해요.

백그라운드에서 요약 요청하기

컴팩션 요청은 다른 요청처럼 속도 제한에 걸려요. 실행되는 동안 당신의 애플리케이션은 동시에 두 요청을 열어요. 대화는 교체 전까지 전체 히스토리에서 계속 자라므로, 그 사이 도착하는 턴들을 컨텍스트 창이 수용할 공간이 있을 때 컴팩션 요청을 시작하세요.

다음 프로그램은 루프에서 컴팩션하기의 루프를 컴팩션 요청을 대화 경로에서 분리한 버전이에요. PHP 버전은 없어요. 예시가 두 요청을 동시에 실행하는 것에 의존하기 때문이에요. 강조된 줄은 루프와 다른 점을 보여주고, 다음 목록은 프로그램이 실행하는 순서대로 다뤄요.

```python Python from concurrent.futures import Future, ThreadPoolExecutor

import anthropic from anthropic.types.beta import BetaMessage, BetaMessageParam

client = anthropic.Anthropic() executor = ThreadPoolExecutor(max_workers=1)

Set this near your real input budget. It is low here so a short conversation compacts.

COMPACT_AT_TOKENS = 2500 SYSTEM = "You help design a recipe app's data model. Keep answers short."

QUESTIONS = [ "What are the main entities in the data model?", "Which fields should Recipe have?", "Which fields should Ingredient have?", "Which fields should RecipeIngredient have?", "Which fields should Step have?", "Which indexes should these tables have?", "Which fields should be required?", "Which fields should have default values?", ]

def swap_in(history: list[BetaMessageParam], summary: BetaMessage, sent: int) -> None: if summary.stop_reason == "compaction": # Replace exactly the messages the compaction request held. # Later turns stay after the block. history[:sent] = [{"role": "assistant", "content": summary.content}] print(f"Swapped {sent} messages")

history: list[BetaMessageParam] = [] pending: Future[BetaMessage] | None = None sent = 0 for turn, question in enumerate(QUESTIONS, start=1): if pending is not None and pending.done(): swap_in(history, pending.result(), sent) pending = None

  history.append({"role": "user", "content": question})
  response = client.beta.messages.create(
      model="claude-opus-5-5",
      max_tokens=8192,
      system=SYSTEM,
      betas=["compact-2026-09-04"],
      messages=history,
  )
  history.append({"role": "assistant", "content": response.content})

  # The next request sends this reply too, so count it.
  conversation_tokens = response.usage.input_tokens + response.usage.output_tokens
  if (
      conversation_tokens > COMPACT_AT_TOKENS
      and turn < len(QUESTIONS)
      and pending is None
  ):
      sent = len(history)
      pending = executor.submit(
          client.beta.messages.create,
          model="claude-opus-5-5",
          max_tokens=4096,
          system=SYSTEM,
          betas=["compact-2026-09-04"],
          messages=history.copy(),
          compaction={"type": "summarize"},
      )

Swap in a summary that is still on its way before you save

or continue the conversation.

if pending is not None: swap_in(history, pending.result(), sent) executor.shutdown()


```typescript TypeScript
const client = new Anthropic();

// Set this near your real input budget. It is low here so a short conversation compacts.
const compactAtTokens = 2500;
const systemPrompt = "You help design a recipe app's data model. Keep answers short.";

const questions = [
  "What are the main entities in the data model?",
  "Which fields should Recipe have?",
  "Which fields should Ingredient have?",
  "Which fields should RecipeIngredient have?",
  "Which fields should Step have?",
  "Which indexes should these tables have?",
  "Which fields should be required?",
  "Which fields should have default values?"
];

function swapIn(
  history: Anthropic.Beta.Messages.BetaMessageParam[],
  summary: Anthropic.Beta.Messages.BetaMessage,
  sent: number
): Anthropic.Beta.Messages.BetaMessageParam[] {
  if (summary.stop_reason !== "compaction") {
    return history;
  }
  console.log(`Swapped ${sent} messages`);
  // Replace exactly the messages the compaction request held. Later turns stay after the block.
  return [{ role: "assistant", content: summary.content }, ...history.slice(sent)];
}

let history: Anthropic.Beta.Messages.BetaMessageParam[] = [];
let pending: Promise<Anthropic.Beta.Messages.BetaMessage> | undefined;
let settled = false;
let sent = 0;
for (const [index, question] of questions.entries()) {
  const turn = index + 1;
  if (pending && settled) {
    history = swapIn(history, await pending, sent);
    pending = undefined;
    settled = false;
  }

  history.push({ role: "user", content: question });
  const response = await client.beta.messages.create({
    model: "claude-opus-5-5",
    max_tokens: 8192,
    system: systemPrompt,
    betas: ["compact-2026-09-04"],
    messages: history
  });
  history.push({ role: "assistant", content: response.content });

  // The next request sends this reply too, so count it.
  const conversationTokens = response.usage.input_tokens + response.usage.output_tokens;
  if (conversationTokens > compactAtTokens && turn < questions.length && !pending) {
    sent = history.length;
    pending = client.beta.messages.create({
      model: "claude-opus-5-5",
      max_tokens: 4096,
      system: systemPrompt,
      betas: ["compact-2026-09-04"],
      messages: [...history],
      compaction: { type: "summarize" }
    });
    // Mark the request settled either way. Awaiting it then returns the summary or throws.
    const markSettled = () => {
      settled = true;
    };
    pending.then(markSettled, markSettled);
  }
}

// Swap in a summary that is still on its way before you save or continue the conversation.
if (pending) {
  history = swapIn(history, await pending, sent);
}
using Anthropic.Models.Beta;
using Anthropic.Models.Beta.Messages;
using Model = Anthropic.Models.Messages.Model;

AnthropicClient client = new();

// Set this near your real input budget. It is low here so a short conversation compacts.
const int CompactAtTokens = 2500;
const string SystemPrompt = "You help design a recipe app's data model. Keep answers short.";

string[] questions =
[
    "What are the main entities in the data model?",
    "Which fields should Recipe have?",
    "Which fields should Ingredient have?",
    "Which fields should RecipeIngredient have?",
    "Which fields should Step have?",
    "Which indexes should these tables have?",
    "Which fields should be required?",
    "Which fields should have default values?",
];

static List<BetaMessageParam> SwapIn(List<BetaMessageParam> history, BetaMessage summary, int sent)
{
    if (summary.StopReason != BetaStopReason.Compaction)
    {
        return history;
    }
    Console.WriteLine($"Swapped {sent} messages");
    // Replace exactly the messages the compaction request held. Later turns stay after the block.
    return
    [
        new()
        {
            Role = Role.Assistant,
            Content = summary.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList(),
        },
        .. history[sent..],
    ];
}

List<BetaMessageParam> history = [];
Task<BetaMessage>? pending = null;
var sent = 0;
foreach (var (index, question) in questions.Index())
{
    var turn = index + 1;
    if (pending is { IsCompleted: true })
    {
        history = SwapIn(history, await pending, sent);
        pending = null;
    }

    history.Add(new() { Role = Role.User, Content = question });
    var response = await client.Beta.Messages.Create(new MessageCreateParams
    {
        Model = Model.ClaudeOpus5_5,
        MaxTokens = 8192,
        System = SystemPrompt,
        Betas = [AnthropicBeta.Compact2026_09_04],
        Messages = history,
    });
    history.Add(new()
    {
        Role = Role.Assistant,
        Content = response.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList(),
    });

    // The next request sends this reply too, so count it.
    var conversationTokens = response.Usage.InputTokens + response.Usage.OutputTokens;
    if (conversationTokens > CompactAtTokens && turn < questions.Length && pending is null)
    {
        sent = history.Count;
        pending = client.Beta.Messages.Create(new MessageCreateParams
        {
            Model = Model.ClaudeOpus5_5,
            MaxTokens = 4096,
            System = SystemPrompt,
            Betas = [AnthropicBeta.Compact2026_09_04],
            Messages = [.. history],
            Compaction = new BetaCompactionConfig(), // type defaults to "summarize"
        });
    }
}

// Swap in a summary that is still on its way before you save or continue the conversation.
if (pending is not null)
{
    history = SwapIn(history, await pending, sent);
}
ctx := context.Background()
client := anthropic.NewClient()

// Set this near your real input budget. It is low here so a short conversation compacts.
const compactAtTokens = 2500
system := []anthropic.BetaTextBlockParam{{Text: "You help design a recipe app's data model. Keep answers short."}}

questions := []string{
	"What are the main entities in the data model?",
	"Which fields should Recipe have?",
	"Which fields should Ingredient have?",
	"Which fields should RecipeIngredient have?",
	"Which fields should Step have?",
	"Which indexes should these tables have?",
	"Which fields should be required?",
	"Which fields should have default values?",
}

var history []anthropic.BetaMessageParam
var pending chan *anthropic.BetaMessage
var sent int
swapIn := func(summary *anthropic.BetaMessage) {
	if summary.StopReason != anthropic.BetaStopReasonCompaction {
		return
	}
	fmt.Printf("Swapped %d messages\n", sent)
	// Replace exactly the messages the compaction request held. Later turns stay after the block.
	history = slices.Replace(history, 0, sent, summary.ToParam())
}

for i, question := range questions {
	turn := i + 1
	// Receiving from a nil channel never succeeds, so this skips when nothing is pending.
	select {
	case summary := <-pending:
		swapIn(summary)
		pending = nil
	default:
	}

	history = append(history, anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(question)))
	response, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{
		Model:     anthropic.ModelClaudeOpus5_5,
		MaxTokens: 8192,
		System:    system,
		Betas:     []anthropic.AnthropicBeta{anthropic.AnthropicBetaCompact2026_09_04},
		Messages:  history,
	})
	if err != nil {
		log.Fatal(err)
	}
	history = append(history, response.ToParam())

	// The next request sends this reply too, so count it.
	conversationTokens := response.Usage.InputTokens + response.Usage.OutputTokens
	if conversationTokens > compactAtTokens && turn < len(questions) && pending == nil {
		sent = len(history)
		pending = make(chan *anthropic.BetaMessage, 1)
		go func(messages []anthropic.BetaMessageParam, result chan<- *anthropic.BetaMessage) {
			summary, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{
				Model:     anthropic.ModelClaudeOpus5_5,
				MaxTokens: 4096,
				System:    system,
				Betas:     []anthropic.AnthropicBeta{anthropic.AnthropicBetaCompact2026_09_04},
				Messages:  messages,
				Compaction: anthropic.BetaCompactionConfigUnionParam{
					OfSummarize: &anthropic.BetaSummarizeCompactionParam{},
				},
			})
			if err != nil {
				log.Fatal(err)
			}
			result <- summary
		}(slices.Clone(history), pending)
	}
}

// Swap in a summary that is still on its way before you save or continue the conversation.
if pending != nil {
	swapIn(<-pending)
}
import com.anthropic.models.beta.AnthropicBeta;
import com.anthropic.models.beta.messages.BetaCompactionConfig;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.models.beta.messages.BetaMessageParam;
import com.anthropic.models.beta.messages.BetaStopReason;
import com.anthropic.models.beta.messages.MessageCreateParams;

// Set this near your real input budget. It is low here so a short conversation compacts.
static final long COMPACT_AT_TOKENS = 2500;
static final String SYSTEM = "You help design a recipe app's data model. Keep answers short.";

void swapIn(List<BetaMessageParam> history, BetaMessage summary, int sent) {
    if (!summary.stopReason().map(BetaStopReason.COMPACTION::equals).orElse(false)) {
        return;
    }
    IO.println("Swapped " + sent + " messages");
    // Replace exactly the messages the compaction request held. Later turns stay after the block.
    history.subList(0, sent).clear();
    history.addFirst(summary.toParam());
}

void main() {
    var client = AnthropicOkHttpClient.fromEnv();

    var questions = List.of(
        "What are the main entities in the data model?",
        "Which fields should Recipe have?",
        "Which fields should Ingredient have?",
        "Which fields should RecipeIngredient have?",
        "Which fields should Step have?",
        "Which indexes should these tables have?",
        "Which fields should be required?",
        "Which fields should have default values?"
    );

    var history = new ArrayList<BetaMessageParam>();
    CompletableFuture<BetaMessage> pending = null;
    int sent = 0;
    for (int turn = 1; turn <= questions.size(); turn++) {
        if (pending != null && pending.isDone()) {
            swapIn(history, pending.join(), sent);
            pending = null;
        }

        history.add(BetaMessageParam.builder()
            .role(BetaMessageParam.Role.USER)
            .content(questions.get(turn - 1))
            .build());
        var params = MessageCreateParams.builder()
            .model(Model.CLAUDE_OPUS_5_5)
            .maxTokens(8192)
            .system(SYSTEM)
            .addBeta(AnthropicBeta.COMPACT_2026_09_04)
            .messages(history)
            .build();
        var response = client.beta().messages().create(params);
        history.add(response.toParam());

        // The next request sends this reply too, so count it.
        long conversationTokens = response.usage().inputTokens() + response.usage().outputTokens();
        if (conversationTokens > COMPACT_AT_TOKENS && turn < questions.size() && pending == null) {
            sent = history.size();
            var summaryParams = MessageCreateParams.builder()
                .model(Model.CLAUDE_OPUS_5_5)
                .maxTokens(4096)
                .system(SYSTEM)
                .addBeta(AnthropicBeta.COMPACT_2026_09_04)
                .messages(List.copyOf(history))
                .compaction(BetaCompactionConfig.builder().build()) // type defaults to "summarize"
                .build();
            pending = client.async().beta().messages().create(summaryParams);
        }
    }

    // Swap in a summary that is still on its way before you save or continue the conversation.
    if (pending != null) {
        swapIn(history, pending.join(), sent);
    }
    client.close();
}
client = Anthropic::Client.new

# Set this near your real input budget. It is low here so a short conversation compacts.
COMPACT_AT_TOKENS = 2500
SYSTEM = "You help design a recipe app's data model. Keep answers short."

questions = [
  "What are the main entities in the data model?",
  "Which fields should Recipe have?",
  "Which fields should Ingredient have?",
  "Which fields should RecipeIngredient have?",
  "Which fields should Step have?",
  "Which indexes should these tables have?",
  "Which fields should be required?",
  "Which fields should have default values?"
]

def swap_in(history, summary, sent)
  return history unless summary.stop_reason == :compaction

  puts "Swapped #{sent} messages"
  # Replace exactly the messages the compaction request held. Later turns stay after the block.
  [{ role: "assistant", content: summary.content }, *history[sent..]]
end

history = []
pending = nil
sent = 0
questions.each.with_index(1) do |question, turn|
  if pending && !pending.alive?
    history = swap_in(history, pending.value, sent)
    pending = nil
  end

  history << { role: "user", content: question }
  response = client.beta.messages.create(
    model: Anthropic::Model::CLAUDE_OPUS_5_5,
    max_tokens: 8192,
    system_: SYSTEM,
    betas: [Anthropic::AnthropicBeta::COMPACT_2026_09_04],
    messages: history
  )
  history << { role: "assistant", content: response.content }

  # The next request sends this reply too, so count it.
  conversation_tokens = response.usage.input_tokens + response.usage.output_tokens
  if conversation_tokens > COMPACT_AT_TOKENS && turn < questions.length && pending.nil?
    sent = history.length
    pending = Thread.new(history.dup) do |snapshot|
      client.beta.messages.create(
        model: Anthropic::Model::CLAUDE_OPUS_5_5,
        max_tokens: 4096,
        system_: SYSTEM,
        betas: [Anthropic::AnthropicBeta::COMPACT_2026_09_04],
        messages: snapshot,
        compaction: { type: "summarize" }
      )
    end
  end
end

# Swap in a summary that is still on its way before you save or continue the conversation.
history = swap_in(history, pending.value, sent) if pending
  • 언제 컴팩션할지 결정하기: 크기 검사는 컴팩션 요청이 진행 중이지 않을 때도 요구해요.
  • 요청 시작하기: 루프가 컴팩션 응답을 기다리는 곳에서, 이 버전은 히스토리가 보유한 메시지 수를 기록하고, 각 언어의 자체 동시성 도구로 히스토리 복사본에서 요청을 시작한 다음, 기다리지 않고 다음 턴으로 진행해요.
  • 결과 확인하기: 각 턴의 시작에서 프로그램은 보류 중인 요청이 끝났는지 확인해요. 끝났으면 그 턴의 요청을 보내기 전에 교체를 수행해요.
  • 교체 수행하기: 루프가 전체 히스토리를 반환된 메시지로 교체하는 곳에서, 이 버전의 교체 함수는 요청이 보유한 메시지만(앞에서부터 세어서) 교체하고 그 이후 추가된 모든 것을 유지해요.
  • 루프 끝내기: 루프가 끝날 때 컴팩션 요청이 여전히 보류 중이면, 프로그램은 그것을 기다렸다가 교체를 수행해서, 대화를 저장하거나 계속하기 전에 아직 오는 중인 요약을 잃지 않게 해요.

stop_reason 검사는 루프와 동일해요. 블록 없는 응답은 히스토리를 그대로 둬요. 더 이상 보류 중인 것이 없으므로 프로그램은 새 컴팩션 요청을 시작할 수 있어요.

요약이 작성되는 동안 thinking을 유효하게 유지하기

요약이 작성되는 동안 도착하는 턴은 유지되는 턴이에요. 보존 thinking이 있는 모델에서 thinking 블록을 다시 보내면, 그 턴들의 thinking은 유지된 thinking이 유효하게 유지되는 조건이 성립하는 동안에만 유효해요.

더 알아보기 (Learn more)