Compression by framework¶
Every framework keeps its own message list. compress_for converts that list to the
adapter's Message, compresses it and converts it back to the same type. Messages that
are kept come back as the very same objects; changed ones keep every field but the text.
Tool-call ids stay paired, and pinned messages stay verbatim. Approval items stay too when
they are marked: the adapter's approval results, MCP approval requests and responses, and
any item whose type names an approval or confirmation (ADK adk_request_confirmation).
Pass pinned= for a framework's approval state that carries no such marker.
flowchart LR
F["framework messages<br/>(dicts, BaseMessage, Content)"] --> C["compress_for(messages, policy)"]
C --> T["to Message"] --> S["compress()"] --> B["back to the same type"]
B --> M["the framework's model call"]
| Format | format= |
Covers |
|---|---|---|
Message |
"agent_adapter" |
run_tool_loop, CallableModel, your own code |
LangChain BaseMessage |
"langchain" |
LangChain, LangGraph |
| OpenAI chat dicts and Responses items | "openai" |
OpenAI, OpenAI Agents SDK, any OpenAI-compatible API |
| Anthropic Messages dicts | "anthropic" |
Anthropic |
Gemini contents (dicts or types.Content) |
"gemini" |
Gemini, Google ADK |
| Pydantic AI, Agno, Strands, smolagents, AutoGen and AG2 messages | "pydantic_ai", "agno", "strands", "smolagents", "autogen", "ag2" |
registered when you import that framework's integration module |
LlamaIndex ChatMessage, Semantic Kernel ChatMessageContent, Agent Framework Message, CAMEL-AI MemoryRecord |
"llamaindex", "semantic_kernel", "agent_framework", "camel" |
registered when you import that framework's integration module |
CrewAI LLMMessage, the messages a DSPy adapter builds |
"openai" |
they are OpenAI chat dicts |
format="auto" (the default) detects it; plain {"role", "content"} dicts count as
OpenAI. Pin native messages with pinned=lambda message: ...: no marker is added to them.
Your own integration can add a format with register_format(name, converter, detect=...)
from agent_adapter.context.formats.
Where to hook¶
| Integration | Where to hook | Format | Snippet |
|---|---|---|---|
| No framework (FastAPI, a script) | LLMRequest(compression=) |
agent_adapter |
how-to |
| The adapter's own loop | run_tool_loop(compression=) |
agent_adapter |
how-to |
| OpenAI | before chat.completions.create |
openai |
OpenAI |
| Anthropic | before messages.create |
anthropic |
Anthropic |
| Gemini | before models.generate_content |
gemini |
Gemini |
| LangChain | before model.invoke(messages) |
langchain |
LangChain |
| LangGraph | in your agent node: langgraph_compressor(policy) |
langchain |
LangGraph |
| OpenAI Agents SDK | RunConfig(call_model_input_filter=...) |
openai |
OpenAI Agents |
| Google ADK | LlmAgent(before_model_callback=...) |
gemini |
ADK |
| Haystack | ChatMessage.to_openai_dict_format() before, ChatMessage.from_openai_dict_format() after |
openai |
OpenAI |
| Pydantic AI | Agent(capabilities=[ProcessHistory(pydantic_ai_history_processor(policy))]) |
pydantic_ai |
Agent frameworks |
| Agno | Agent(model=agno_compressed_model(model, policy)) |
agno |
Agent frameworks |
| Strands Agents | agent.add_middleware(InvokeModelStage.Input, strands_compress_middleware(policy)) |
strands |
Agent frameworks |
| smolagents | CodeAgent or ToolCallingAgent(model=smolagents_compressed_model(model, policy)) |
smolagents |
Agent frameworks |
| AutoGen | AssistantAgent(model_context=autogen_model_context(policy)) |
autogen |
Agent frameworks |
| AG2 | Agent(assembly=[ag2_compress_policy(policy)]) |
ag2 |
Agent frameworks |
| CrewAI | register_before_llm_call_hook(...) |
openai |
More frameworks |
| DSPy | dspy.configure(adapter=...), a ChatAdapter subclass |
openai |
More frameworks |
| LlamaIndex | before llm.achat(messages), or on an agent's chat_history= |
llamaindex |
More frameworks |
| Semantic Kernel | before get_chat_message_contents(chat_history=...) |
semantic_kernel |
More frameworks |
| CAMEL-AI | ChatAgent(memory=ChatHistoryMemory(...)) with a context creator |
camel |
More frameworks |
| Microsoft Agent Framework | Agent(middleware=[...]), a chat middleware |
agent_framework |
More frameworks |
| Claude Agent SDK | its harness compacts its own context | — | — |
| MCP, MCP tasks, A2A, AG-UI, OpenAPI | tools and protocols: no model history here | — | — |
OpenAI¶
def ask_openai(client: Any, messages: list[dict]) -> Any:
smaller, _ = compress_for_sync(messages, policy) # chat dicts in, chat dicts out
return client.chat.completions.create(model="gpt-4.1", messages=smaller)
Anthropic¶
The system prompt is a separate argument, so it is never in the list:
def ask_anthropic(client: Any, system: str, messages: list[dict]) -> Any:
smaller, _ = compress_for_sync(messages, policy, format="anthropic")
return client.messages.create(
model="claude-sonnet-4-5", system=system, messages=smaller, max_tokens=1024
)
Gemini¶
def ask_gemini(client: Any, contents: list[Any]) -> Any:
smaller, _ = compress_for_sync(contents, policy, format="gemini") # dicts or types.Content
return client.models.generate_content(model="gemini-2.5-flash", contents=smaller)
LangGraph and LangChain¶
In a LangGraph node, compress what the model gets; the graph state keeps the full history:
compressor = langgraph_compressor(policy)
async def agent(state: State) -> dict[str, Any]:
messages = await compressor(state["messages"]) # or compressor.sync(...) in a sync node
sent.append(len(messages))
return {"messages": [await llm.ainvoke(messages)]} # the state keeps the full history
With LangChain alone, compress before the call:
def ask(messages: list[Any]) -> Any:
smaller, _ = compress_for_sync(messages, policy) # BaseMessage in, BaseMessage out
return llm.invoke(smaller)
OpenAI Agents SDK¶
The filter gets the Responses input items of each model call, reasoning items included:
async def compress_model_input(data: Any) -> Any:
"""RunConfig(call_model_input_filter=compress_model_input): Responses items each turn."""
smaller, _ = await compress_for(data.model_data.input, policy, format="openai")
return dataclasses.replace(data.model_data, input=smaller)
Google ADK¶
def before_model(callback_context: Any, llm_request: Any) -> None:
"""LlmAgent(before_model_callback=before_model): shorten what this model call sends."""
llm_request.contents, _ = compress_for_sync(llm_request.contents, policy, format="gemini")
Agent frameworks¶
Import the hook from the framework's integration module; that import also registers the
format for compress_for(format="auto"). Pydantic AI keeps the processed history for
the rest of the run; the others shorten only what each model call sends.
def pydantic_ai_agent(model: Any) -> Any:
from pydantic_ai import Agent
from pydantic_ai.capabilities import ProcessHistory
return Agent(model, capabilities=[ProcessHistory(pydantic_ai_history_processor(policy))])
def agno_agent(model: Any) -> Any: # model: any Agno model, e.g. OpenAIChat(id="gpt-4.1")
from agno.agent import Agent
return Agent(model=agno_compressed_model(model, policy))
def strands_agent(agent: Any) -> Any: # agent: a strands.Agent
from strands.middleware import InvokeModelStage
agent.add_middleware(InvokeModelStage.Input, strands_compress_middleware(policy))
return agent
def smolagents_agent(model: Any, tools: list[Any]) -> Any:
from smolagents import ToolCallingAgent
return ToolCallingAgent(tools=tools, model=smolagents_compressed_model(model, policy))
def autogen_agent(model_client: Any) -> Any:
from autogen_agentchat.agents import AssistantAgent
context = autogen_model_context(policy)
return AssistantAgent("assistant", model_client=model_client, model_context=context)
def ag2_agent(config: Any) -> Any: # config: an AG2 ModelConfig
from ag2 import Agent
return Agent("assistant", config=config, assembly=[ag2_compress_policy(policy)])
More agent frameworks¶
CrewAI and DSPy send OpenAI chat dicts, so they use openai. The CrewAI hook shortens
the agent's own list in place. The other four send the model a shorter copy, and the
history keeps every message. Tool rounds that Semantic Kernel or a LlamaIndex agent run
inside one call are not seen by these hooks; Agent Framework middleware sees every call.
A framework's own compaction (CrewAI respect_context_window, CAMEL summarize_threshold,
Agent Framework compaction_strategy) can run instead; use one, not both.
def compress_crewai_messages(context: Any) -> None:
"""`register_before_llm_call_hook(compress_crewai_messages)`, or decorate `@before_llm_call`."""
smaller, _ = compress_for_sync(context.messages, policy, format="openai")
context.messages[:] = smaller # in place: CrewAI keeps this very list as the history
class CompressingChatAdapter(dspy.ChatAdapter):
"""`dspy.configure(adapter=CompressingChatAdapter())`: history, demos and the new input."""
def format(self, signature: Any, demos: list[Any], inputs: dict[str, Any]) -> list[Any]:
messages = super().format(signature, demos, inputs)
smaller, _ = compress_for_sync(messages, policy, format="openai") # OpenAI chat dicts
return smaller
import agent_adapter.integrations.llamaindex # noqa: E402, F401 registers its format
async def ask_llamaindex(llm: Any, messages: list[Any]) -> Any:
"""`messages` is a `list[ChatMessage]`; compress an agent's `chat_history=` the same way."""
smaller, _ = await compress_for(messages, policy, format="llamaindex")
return await llm.achat(smaller)
import agent_adapter.integrations.semantic_kernel # noqa: E402, F401 registers its format
async def ask_semantic_kernel(service: Any, history: Any, settings: Any) -> Any:
"""The service gets a shorter copy; `history` itself keeps every message."""
smaller, _ = await compress_for(history.messages, policy, format="semantic_kernel")
shorter = history.model_copy(update={"messages": smaller})
return await service.get_chat_message_contents(chat_history=shorter, settings=settings)
import agent_adapter.integrations.camel # noqa: E402, F401 registers its format
class CompressingContextCreator(ScoreBasedContextCreator):
"""`ChatAgent(memory=ChatHistoryMemory(CompressingContextCreator(counter, limit)))`."""
def create_context(self, records: list[ContextRecord]) -> Any:
memory = [record.memory_record for record in records]
smaller, _ = compress_for_sync(memory, policy, format="camel")
kept = [ContextRecord(memory_record=r, score=1.0, timestamp=r.timestamp) for r in smaller]
return super().create_context(kept) # the memory itself keeps every record
import agent_adapter.integrations.agent_framework # noqa: E402, F401 registers its format
@chat_middleware
async def compress_messages(context: ChatContext, call_next: Any) -> None:
"""`Agent(client=..., middleware=[compress_messages])`: the session keeps the full history."""
context.messages, _ = await compress_for(context.messages, policy, format="agent_framework")
await call_next()
Rules for every framework¶
compress_fornever calls your main model; AI steps call the models you gave them.- In async code use
await compress_for(...);compress_for_syncis for sync code. - A framework that compacts on its own (OpenAI's
OpenAIResponsesCompactionSession, LangChain's summarisation middleware) can run instead; use one, not both.