maf-compaction
Experimental. Releases before 1.0 may change or remove APIs. Importing this package emits
MafCompactionExperimentalWarning.
Compaction strategies for Microsoft Agent Framework that are shaped around the provider's prompt cache. A prompt cache matches on an exact prefix, and every compaction rewrites history inside that prefix, so compaction breaks the cache by construction. The strategies here break it as rarely and as late as they can: they keep a fixed head and tail of the conversation verbatim, change only the band between them, and change it by rules that produce the same bytes on every later turn.
This is an independent package. It is not affiliated with or endorsed by Microsoft.
What is in it
| Strategy | What it does |
|---|---|
AnchoredCompactionStrategy |
Keeps the first and last groups of the conversation whole and shortens the oldest tool results between them, by position, so the compacted prefix is stable. |
MinimumGainAnchoredCompactionStrategy |
The same, with a floor: a collapse that would save too little to repay the cache it invalidates is declined. |
ToolResultAnchoredSummarizationCompactionStrategy |
Has the model write the facts from its tool results into a record through a recall tool, then drops the tool groups the record covers. Ships with make_recall_tool, RecallGate and ToolResultRecallMiddleware, which ask for the record at the right moment. |
UserTurnAnchoredSummarizationCompactionStrategy |
Summarises the user's turns between a fixed head and tail with a summarizer client, in one of three modes. |
ToolResultAndUserTurnAnchoredSummarizationCompactionStrategy |
Runs the two summarising strategies over one conversation, record phase first, with a bounded last-resort chain that merges and rewrites while the prompt is over budget. When nothing safe is left to remove it stops, and the prompt goes out over the limit: a loud failure rather than a quiet loss of facts. |
All of them implement the framework's CompactionStrategy and attach wherever the framework takes one: create_harness_agent, or a CompactionProvider on a plain Agent.
Install
pip install maf-compaction
The package depends on agent-framework-core alone. The summarising strategies take any framework chat client as their summarizer, so install the client package for your provider separately.
Quickstart
Every strategy sizes its decisions with a tokenizer. The one below counts exactly and needs tiktoken, which this package does not install (pip install tiktoken); the framework's CharacterEstimatorTokenizer needs nothing extra and works too, at the cost of precision.
import tiktoken
class Tokenizer:
def __init__(self, encoding: str = "o200k_base") -> None:
self._encoding = tiktoken.get_encoding(encoding)
def count_tokens(self, text: str) -> int:
return len(self._encoding.encode(text))
tokenizer = Tokenizer()
CONTEXT_WINDOW = 128_000 # the model's input limit
MAX_OUTPUT = 4_000 # reserved for the reply
BUDGET = CONTEXT_WINDOW - MAX_OUTPUT
The anchored strategy needs nothing else:
from agent_framework import create_harness_agent
from agent_framework_openai import OpenAIChatClient
from maf_compaction import AnchoredCompactionStrategy
strategy = AnchoredCompactionStrategy(
max_input_tokens=BUDGET,
tokenizer=tokenizer,
keep_head_groups=3, # task and requirements, never touched
keep_tail_groups=4, # the working set, never touched
band_share=0.25, # the oldest banded result keeps 25% of the budget
)
client = OpenAIChatClient(model_id="gpt-6-luna")
agent = create_harness_agent(
client,
name="assistant",
agent_instructions="Answer from the lookups you make. Quote values exactly.",
tools=[lookup_deployment],
max_context_window_tokens=CONTEXT_WINDOW,
max_output_tokens=MAX_OUTPUT,
before_compaction_strategy=strategy,
after_compaction_strategy=strategy,
tokenizer=tokenizer,
)
The record strategy needs the recall tool registered and its middleware installed, because the model cannot be asked for a record from inside a compaction pass:
from maf_compaction import (
AnchoredCompactionStrategy,
RecallGate,
ToolResultAnchoredSummarizationCompactionStrategy,
ToolResultRecallMiddleware,
make_recall_tool,
)
strategy = ToolResultAnchoredSummarizationCompactionStrategy(
max_input_tokens=BUDGET,
tokenizer=tokenizer,
trigger_fraction=0.6, # ask for a record past 60% of the budget
fallback_fraction=0.9, # stop waiting for one past 90%
fallback=AnchoredCompactionStrategy(max_input_tokens=BUDGET, tokenizer=tokenizer),
)
gate = RecallGate()
recall_tool = make_recall_tool(gate, target_tokens=2_000)
recall = ToolResultRecallMiddleware(
max_input_tokens=BUDGET,
tokenizer=tokenizer, # the same tokenizer as the strategy
arm=gate.arm,
disarm=gate.disarm,
trigger_fraction=strategy.trigger_fraction,
record_max_tokens=4_000,
repeat_records=True, # a record per new batch of tool work
reforce=strategy.take_reforce,
)
agent = create_harness_agent(
client,
name="assistant",
agent_instructions="Answer from the lookups you make. Quote values exactly.",
tools=[lookup_deployment, recall_tool],
middleware=[recall],
max_context_window_tokens=CONTEXT_WINDOW,
max_output_tokens=MAX_OUTPUT,
before_compaction_strategy=strategy,
after_compaction_strategy=strategy,
tokenizer=tokenizer,
)
The summarising strategies, RecallGate and ToolResultRecallMiddleware keep one conversation's decisions on the instance. This direct wiring builds the stack and agent once per session; the middleware raises if a second session reaches it. For a shared agent, use the session wiring below.
The composed strategy takes a record half and a user-turn half built the same way, the user half with remembered_requests=2 unless it runs in the recompacting mode; find_nested_strategy finds the record half inside it for the middleware's reforce, and record_text reads the record the model wrote.
Serving many sessions
SessionCompactionRegistry builds a fresh CompactionStack for each session and stores its JSON-safe behavioral state in AgentSession.state["maf_compaction"]. The stack contains either the record strategy or the composition, its gate, and its recall middleware. One agent registers one dispatching middleware and one recall tool. Clients, tools and other expensive agent configuration can be shared; the builder must create fresh strategy, gate and middleware instances with identical configuration on every replica.
from maf_compaction import (
CompactionStack,
RecallGate,
SessionCompactionProvider,
SessionCompactionRegistry,
SessionToolResultRecallMiddleware,
ToolResultAnchoredSummarizationCompactionStrategy,
ToolResultAndUserTurnAnchoredSummarizationCompactionStrategy,
ToolResultRecallMiddleware,
UserTurnAnchoredSummarizationCompactionStrategy,
make_session_recall_tool,
)
def build_compaction() -> CompactionStack:
records = ToolResultAnchoredSummarizationCompactionStrategy(
max_input_tokens=BUDGET, tokenizer=tokenizer,
)
users = UserTurnAnchoredSummarizationCompactionStrategy(
max_input_tokens=BUDGET,
tokenizer=tokenizer,
client=summarizer_client,
summary_mode="boundary",
remembered_requests=2,
)
strategy = ToolResultAndUserTurnAnchoredSummarizationCompactionStrategy(
tokenizer=tokenizer, tool_results=records, user_turns=users,
)
gate = RecallGate()
recall = ToolResultRecallMiddleware(
max_input_tokens=BUDGET,
tokenizer=tokenizer,
arm=gate.arm,
disarm=gate.disarm,
trigger_fraction=records.trigger_fraction,
reforce=records.take_reforce,
)
return CompactionStack(strategy, gate, recall)
registry = SessionCompactionRegistry(build_compaction, max_cached_sessions=128)
compaction = SessionCompactionProvider(registry, tokenizer=tokenizer, before=False)
agent = create_harness_agent(
client,
name="shared-assistant",
tools=[lookup_deployment, make_session_recall_tool(registry)],
middleware=[SessionToolResultRecallMiddleware(registry, compact_before=True)],
disable_compaction=True,
context_providers=[compaction],
max_output_tokens=MAX_OUTPUT,
tokenizer=tokenizer,
)
# Match the harness's built-in placement immediately after its history provider.
agent.context_providers.remove(compaction)
agent.context_providers.insert(1, compaction)
The harness in framework 1.20 loads and persists history per model call. Its provider before_run hook therefore has no history to compact. compact_before=True supplies the session's strategy to the chat client after history loads, including when a stream is consumed later. before=False avoids a second provider pass. The provider's after_run compacts stored history. Additional providers are appended by create_harness_agent, so the final two lines put compaction where the harness puts its own: after history, before the remaining providers; after hooks run in reverse order. A plain Agent using per-service-call history persistence uses the same middleware and provider. For a host that supplies loaded context to provider hooks itself, leave compact_before=False and enable the provider's before phase.
Provider hooks resolve from their session argument, chat middleware from context.session, and the registered tool from the framework-injected FunctionInvocationContext.session. Each phase sets a scoped context variable and clears it on exit. No session identifier is a model-supplied tool argument, and missing sessions are refused. The inner middleware retains its single-session guard.
Persist the whole AgentSession using the framework's session serialization and restore it before the next request. The registry reads session state even on cache hits and writes it after provider, middleware and tool phases, including exceptional exits. Cache eviction and max_cached_sessions=0 discard only cached stacks; the session retains its decisions. Removing the registry's state entry resets its stack. Use a distinct state_key for each independently configured registry and keep the builder configuration stable across restores; version or stack-kind mismatches raise instead of silently dropping decisions.
The snapshot carries record decisions and outstanding re-force asks; the chain's wait, kept replacements, shed IDs and refusals; remembered user-summary requests and kept folds; pending recall and awaiting-record flags; and the one-shot gate permission. User compaction and fold counts also travel because they allocate summary IDs. Other counters remain local diagnostics and restart after cache eviction or a replica switch. ChainDecisions.to_dict() and RecordDecisions.to_dict() serialize the existing narrower replay snapshots; CompactionStack.to_dict() includes the content-keyed state as well. Content-keyed entries contain conversation text and can grow with the conversation, so retain them under the same storage and retention policy as history.
Serialize requests for the same session in the host, including across replicas. The registry supports concurrent distinct sessions and rejects overlapping independent phases for the same session in one process; it does not supply a distributed lock or persist to an external store. Model quality, multi-process storage coordination and recovery from a crash between a model response and the host's session save are outside this adapter's guarantees.
When to use it
Below the context window, not compacting is cheaper than any strategy here: the whole conversation stays cached. These strategies pay off once a conversation outgrows its window, where the framework's own strategies either lose the facts in the tool results or re-bill the prompt on every turn. The measurements behind that, and what each strategy does at the edges, are in the compaction documentation.
What it depends on
The strategies call agent_framework._compaction, the framework's private compaction helpers, for grouping, token annotation and the exclusion flags. Nothing public exposes those, so the dependency on agent-framework-core is pinned to one minor and moved only after the new minor has been read against.
Licence
MIT. See the LICENSE.
Metadata
Release files for maf-compaction 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| maf_compaction-0.2.0.tar.gz | 108.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| maf_compaction-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 222.3 kB
Release files / maf_compaction-0.2.0.tar.gz
| Download URL | maf_compaction-0.2.0.tar.gz |
|---|---|
| Size | 108.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6a4595db9686087466e6ed7a822b01fdc083b098439b41889b9de88fc9242196
|
|
BLAKE2b-256 checksum How to use checksums |
61095765733a6a840e0f2a9f9c7e462345cc767830594b01b7b2dfe67ceb10b2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency logRelease files / maf_compaction-0.2.0-py3-none-any.whl
| Download URL | maf_compaction-0.2.0-py3-none-any.whl |
|---|---|
| Size | 113.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5f0727f0d0eaccb0b2e86f72d10be5d25763db21ef0ddb28930cbbc49f8fcbbf
|
|
BLAKE2b-256 checksum How to use checksums |
d9b7bfec4c0e4cc1e5ac1a27c1116892700c81b35612055ab71e42ee526d9137
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency log