History compaction: chat.compact()
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 176
- Forks
- 28
- Avg merge
- 18h 42m
- Merged PRs (30d)
- 16
Description
Motivation
Long-running chatlas sessions (agents, chatbots, notebooks kept open all day) eventually blow the context window, and chatlas currently has no built-in remedy beyond manually calling chat.set_turns() to prune history yourself. LangChain 1.0 ships SummarizationMiddleware, which auto-summarizes older messages once a token threshold is hit, and this pattern is now standard in agent products (Claude Code included). This is arguably the concrete design proposal for chatlas#8 ("Make it easier to impose limits on conversation size") — this issue frames a specific mechanism to close (part of) that issue.
Proposed approach
A plain method, not hidden middleware — explicit and inspectable, in keeping with chatlas's existing style (chat.set_turns(), chat.get_turns() in chatlas/_chat.py):
from chatlas import ChatAnthropic
chat = ChatAnthropic()
# ... long conversation ...
# Manual, opt-in trigger (default):
if chat.token_count(include="complete") > 100_000:
chat.compact() # and chat.compact_async()
# Optionally, summarize with a cheaper/faster model than the main conversation:
chat.compact(model=ChatAnthropic(model="claude-haiku-4-5"))
# Opt-in automatic triggering, off by default:
chat.compact(auto=True, threshold=100_000) # checked at the start of each .chat() call
Implementation sketch:
- Leverage
Chat.token_count()/token_count_async()(chatlas/_chat.py:671), specifically the recently-addedinclude="complete"mode, which already estimates system prompt +.get_turns()history — exactly the heuristic needed to decide whether to compact. compact()selects a prefix ofself._turnsto summarize (e.g. all but the last N turns, or all but the last complete user/assistant pair), submits them to the provider (or a caller-suppliedChatfor a cheaper model) with a summarization prompt, and replaces that prefix with a single synthetic turn pair (aUserTurn/SystemTurnnote plus anAssistantTurncontaining the summary asContentText) viaChat.set_turns()(chatlas/_chat.py:357), which already supports exactly this "trim and replace" use case per its own docstring ("Clearing (or trimming) the chat history").- Non-text content during summarization:
ContentToolRequest/ContentToolResultpairs,ContentImageInline/ContentPDF, etc. (chatlas/_content.py) inside the summarized range should be described in the summary text (e.g. "user uploaded a PDF; assistant calledsearch_docs(query=...)and got N results") rather than silently dropped, since dropping tool results that established facts the model relied on can cause it to contradict itself later.Turn.__str__(chatlas/_turn.py:115) already gives a reasonable string form of each content type to seed this. AssistantTurn.cost/.tokens(chatlas/_turn.py:340,343) for the summarized turns are lost fromget_tokens()/get_cost()accounting once replaced — this should be documented as expected (cost already spent isn't un-spent) and possibly preserved as metadata on the synthetic turn rather than silently discarded.
Alternatives / prior art
- Closes/addresses part of chatlas#8 directly.
- No direct ellmer equivalent found (searched
tidyverse/ellmerfor "compact", "summar" — no hits), so this would be new ground for the ellmer/chatlas pairing; worth raising upstream once chatlas's design is validated, per the project's alignment goal. - Alternative considered and rejected for v1: automatic middleware-style compaction with no explicit call, à la LangChain's
SummarizationMiddleware. Given YAGNI and that chatlas favors explicit methods over hidden hooks (see howset_model_params(),set_turns()are all explicit calls), default to manual/explicit triggering, withauto=Trueas an opt-in convenience layered on top of the same method.
Open questions
- Where should the threshold check live if
auto=True— inside_chat_impl/_chat_impl_async(chatlas/_chat.py:2647,2734) before submitting turns, or as a wrapper the caller opts into explicitly? - Should
compact()support summarizing tool results into a persistedContentToolResult-shaped stub token (to keep e.g.expand_tool_resultinchatlas/_content_expand.pyand dangling-request logic inchatlas/_turn.pyhappy) rather than plain text? - Is a single summary turn enough, or should compaction be tiered (e.g. keep a rolling summary + last K turns, re-summarizing as needed)?
- Should the system prompt ever be folded into compaction, or always preserved verbatim (current
set_turns()behavior preserves it automatically)?
Drafted from a competitive review of llm / Pydantic AI / LangChain / LiteLLM (July 2026); filed via Claude Code on behalf of @cpsievert.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading Chat.token_count() and set_turns() in chatlas/_chat.py, then inspect _chat_impl/_chat_impl_async and the content and turn types in chatlas/_content.py and chatlas/_turn.py. Resolve the open design questions around summarization boundaries, non-text content, accounting, and automatic triggering before implementation; done requires a defined compact() API and verified behavior for synchronous and asynchronous chats.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- ai
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100