Strategies¶
A strategy decides which messages survive when the conversation exceeds the budget. All built-in strategies are framework-free and respect tool-pair atomicity.
Recency¶
Keep the most recent chunks that fit. The default and cheapest option.
Recency walks from the newest chunk backward, keeping each one that still fits. Pinned chunks are always kept. The min_keep parameter forces at least N non-pinned chunks to survive even if it pushes the result over budget:
Use when:
- A chat assistant whose latest user question is what matters.
- You don't have an LLM call budget for summarisation.
FirstFit¶
The mirror of Recency: keep the oldest chunks that fit, drop the tail.
Use when:
- Your prompt is heavy on early context — a long system prompt, few-shot examples, a research brief — and the trailing turns are less essential.
- You're building a retrieval-augmented loop where the retrieved documents come first and the chat tail is disposable.
SummaryEvict¶
Drop older chunks like Recency would, but replace them with a single summary message at the head of the kept list.
from convopack import SummaryEvict
def my_summariser(messages):
# Call an LLM, return a short string.
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "Summarise these turns in two sentences."},
*to_openai(messages),
],
)
return response.choices[0].message.content
packer = Packer(
budget=4000,
strategy=SummaryEvict(my_summariser, reserve=500),
)
reserve tells the strategy how many tokens to leave for the summary itself; defaults to budget // 10.
SummaryEvict accepts an async def summariser too. Use Packer.pack_async from inside an event loop:
async def my_async_summariser(messages):
response = await async_client.messages.create(...)
return response.content[0].text
packer = Packer(budget=4000, strategy=SummaryEvict(my_async_summariser))
result = await packer.pack_async(history)
Use when:
- You care about preserving information from older turns more than about cost.
- Your app is long-running (hours) and the early context matters semantically.
Importance¶
You supply a score function; convopack drops the lowest-scoring chunks first.
from convopack import Importance, Message
def my_scorer(msg: Message) -> float:
if msg.metadata.get("starred"):
return 100.0
if msg.role == "system":
return 50.0
if msg.has_tool_use() or msg.has_tool_result():
return 3.0
return 1.0
packer = Packer(budget=4000, strategy=Importance(scorer=my_scorer))
A chunk's score is the maximum score of any message it contains, so a tool exchange inherits its highest-scored member.
Use when:
- You have application-specific signals (user starred a turn, this turn caused a tool call, this turn is from an admin) that should override pure recency.
A default_scorer is supplied if you omit scorer: system > tool exchanges > user > assistant.
SemanticDedup¶
Drop near-duplicate messages by embedding cosine similarity, then defer to a fallback strategy for the remaining budget enforcement.
from convopack import SemanticDedup
class OpenAIEmbedder:
def embed(self, text: str) -> list[float]:
return openai_client.embeddings.create(
input=text, model="text-embedding-3-small"
).data[0].embedding
packer = Packer(
budget=4000,
strategy=SemanticDedup(
OpenAIEmbedder(),
threshold=0.92,
fallback=Recency(),
),
)
SemanticDedup never deduplicates chunks containing tool calls or pinned messages — those flow through untouched. Among ordinary message chunks, only the first occurrence of each near-duplicate group survives.
Use when:
- An agent loop repeats itself ("Let me think... Let me check again...").
- Users paraphrase the same question multiple times.
- You're piping live transcript into the model and want to compact it.
Writing your own¶
Any object satisfying the Strategy protocol works:
class Strategy(Protocol):
name: str
def pack(
self,
messages: list[Message],
*,
budget: int,
tokenizer: Tokenizer,
pinned_indices: set[int],
) -> PackResult: ...
Just return a PackResult. Use convopack._pairs.group_pairs(messages) to get tool-pair-safe chunks so you don't have to reimplement pair tracking.