maf-compaction 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SOKOLAI BV
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: maf-compaction
3
+ Version: 0.1.0
4
+ Summary: Prompt-cache-aware compaction strategies for Microsoft Agent Framework: anchored trimming, tool-result records and user-turn summaries that keep the cached prefix intact.
5
+ Keywords: compaction,context-window,prompt-cache,agent-framework,microsoft-agent-framework
6
+ Author: SOKOLAI BV
7
+ Author-email: SOKOLAI BV <info@sokolai.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Typing :: Typed
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Requires-Dist: agent-framework-core>=1.20.0,<1.21
19
+ Requires-Python: >=3.12, <3.15
20
+ Project-URL: Homepage, https://www.sokol.ai
21
+ Project-URL: Source, https://github.com/sokolaidev/maf-extensions
22
+ Project-URL: Issues, https://github.com/sokolaidev/maf-extensions/issues
23
+ Description-Content-Type: text/markdown
24
+
25
+ # maf-compaction
26
+
27
+ [![PyPI](https://img.shields.io/pypi/v/maf-compaction)](https://pypi.org/project/maf-compaction/) [![Python](https://img.shields.io/pypi/pyversions/maf-compaction)](https://pypi.org/project/maf-compaction/) [![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/sokolaidev/maf-extensions/blob/main/LICENSE)
28
+
29
+ > **Experimental.** Releases before 1.0 may change or remove APIs. Importing this package emits `MafCompactionExperimentalWarning`.
30
+
31
+ Compaction strategies for [Microsoft Agent Framework](https://aka.ms/AgentFramework) 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.
32
+
33
+ This is an independent package. It is not affiliated with or endorsed by Microsoft.
34
+
35
+ ## What is in it
36
+
37
+ | Strategy | What it does |
38
+ |---|---|
39
+ | `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. |
40
+ | `MinimumGainAnchoredCompactionStrategy` | The same, with a floor: a collapse that would save too little to repay the cache it invalidates is declined. |
41
+ | `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. |
42
+ | `UserTurnAnchoredSummarizationCompactionStrategy` | Summarises the user's turns between a fixed head and tail with a summarizer client, in one of three modes. |
43
+ | `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. |
44
+
45
+ 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`.
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install maf-compaction
51
+ ```
52
+
53
+ 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.
54
+
55
+ ## Quickstart
56
+
57
+ 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.
58
+
59
+ ```python
60
+ import tiktoken
61
+
62
+
63
+ class Tokenizer:
64
+ def __init__(self, encoding: str = "o200k_base") -> None:
65
+ self._encoding = tiktoken.get_encoding(encoding)
66
+
67
+ def count_tokens(self, text: str) -> int:
68
+ return len(self._encoding.encode(text))
69
+
70
+
71
+ tokenizer = Tokenizer()
72
+ CONTEXT_WINDOW = 128_000 # the model's input limit
73
+ MAX_OUTPUT = 4_000 # reserved for the reply
74
+ BUDGET = CONTEXT_WINDOW - MAX_OUTPUT
75
+ ```
76
+
77
+ The anchored strategy needs nothing else:
78
+
79
+ ```python
80
+ from agent_framework import create_harness_agent
81
+ from agent_framework_openai import OpenAIChatClient
82
+ from maf_compaction import AnchoredCompactionStrategy
83
+
84
+ strategy = AnchoredCompactionStrategy(
85
+ max_input_tokens=BUDGET,
86
+ tokenizer=tokenizer,
87
+ keep_head_groups=3, # task and requirements, never touched
88
+ keep_tail_groups=4, # the working set, never touched
89
+ band_share=0.25, # the oldest banded result keeps 25% of the budget
90
+ )
91
+
92
+ client = OpenAIChatClient(model_id="gpt-6-luna")
93
+ agent = create_harness_agent(
94
+ client,
95
+ name="assistant",
96
+ agent_instructions="Answer from the lookups you make. Quote values exactly.",
97
+ tools=[lookup_deployment],
98
+ max_context_window_tokens=CONTEXT_WINDOW,
99
+ max_output_tokens=MAX_OUTPUT,
100
+ before_compaction_strategy=strategy,
101
+ after_compaction_strategy=strategy,
102
+ tokenizer=tokenizer,
103
+ )
104
+ ```
105
+
106
+ 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:
107
+
108
+ ```python
109
+ from maf_compaction import (
110
+ AnchoredCompactionStrategy,
111
+ RecallGate,
112
+ ToolResultAnchoredSummarizationCompactionStrategy,
113
+ ToolResultRecallMiddleware,
114
+ make_recall_tool,
115
+ )
116
+
117
+ strategy = ToolResultAnchoredSummarizationCompactionStrategy(
118
+ max_input_tokens=BUDGET,
119
+ tokenizer=tokenizer,
120
+ trigger_fraction=0.6, # ask for a record past 60% of the budget
121
+ fallback_fraction=0.9, # stop waiting for one past 90%
122
+ fallback=AnchoredCompactionStrategy(max_input_tokens=BUDGET, tokenizer=tokenizer),
123
+ )
124
+
125
+ gate = RecallGate()
126
+ recall_tool = make_recall_tool(gate, target_tokens=2_000)
127
+ recall = ToolResultRecallMiddleware(
128
+ max_input_tokens=BUDGET,
129
+ tokenizer=tokenizer, # the same tokenizer as the strategy
130
+ arm=gate.arm,
131
+ disarm=gate.disarm,
132
+ trigger_fraction=strategy.trigger_fraction,
133
+ record_max_tokens=4_000,
134
+ repeat_records=True, # a record per new batch of tool work
135
+ reforce=strategy.take_reforce,
136
+ )
137
+
138
+ agent = create_harness_agent(
139
+ client,
140
+ name="assistant",
141
+ agent_instructions="Answer from the lookups you make. Quote values exactly.",
142
+ tools=[lookup_deployment, recall_tool],
143
+ middleware=[recall],
144
+ max_context_window_tokens=CONTEXT_WINDOW,
145
+ max_output_tokens=MAX_OUTPUT,
146
+ before_compaction_strategy=strategy,
147
+ after_compaction_strategy=strategy,
148
+ tokenizer=tokenizer,
149
+ )
150
+ ```
151
+
152
+ The summarising strategies, `RecallGate` and `ToolResultRecallMiddleware` keep one conversation's decisions on the instance, so build this stack, and the agent that holds it, once per session. The middleware raises if a second session reaches it.
153
+
154
+ 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.
155
+
156
+ ## When to use it
157
+
158
+ 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](https://github.com/sokolaidev/maf-extensions/blob/main/docs/compaction/README.md).
159
+
160
+ ## What it depends on
161
+
162
+ 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.
163
+
164
+ ## Licence
165
+
166
+ MIT. See the [LICENSE](https://github.com/sokolaidev/maf-extensions/blob/main/LICENSE).
@@ -0,0 +1,142 @@
1
+ # maf-compaction
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/maf-compaction)](https://pypi.org/project/maf-compaction/) [![Python](https://img.shields.io/pypi/pyversions/maf-compaction)](https://pypi.org/project/maf-compaction/) [![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/sokolaidev/maf-extensions/blob/main/LICENSE)
4
+
5
+ > **Experimental.** Releases before 1.0 may change or remove APIs. Importing this package emits `MafCompactionExperimentalWarning`.
6
+
7
+ Compaction strategies for [Microsoft Agent Framework](https://aka.ms/AgentFramework) 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.
8
+
9
+ This is an independent package. It is not affiliated with or endorsed by Microsoft.
10
+
11
+ ## What is in it
12
+
13
+ | Strategy | What it does |
14
+ |---|---|
15
+ | `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. |
16
+ | `MinimumGainAnchoredCompactionStrategy` | The same, with a floor: a collapse that would save too little to repay the cache it invalidates is declined. |
17
+ | `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. |
18
+ | `UserTurnAnchoredSummarizationCompactionStrategy` | Summarises the user's turns between a fixed head and tail with a summarizer client, in one of three modes. |
19
+ | `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. |
20
+
21
+ 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`.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install maf-compaction
27
+ ```
28
+
29
+ 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.
30
+
31
+ ## Quickstart
32
+
33
+ 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.
34
+
35
+ ```python
36
+ import tiktoken
37
+
38
+
39
+ class Tokenizer:
40
+ def __init__(self, encoding: str = "o200k_base") -> None:
41
+ self._encoding = tiktoken.get_encoding(encoding)
42
+
43
+ def count_tokens(self, text: str) -> int:
44
+ return len(self._encoding.encode(text))
45
+
46
+
47
+ tokenizer = Tokenizer()
48
+ CONTEXT_WINDOW = 128_000 # the model's input limit
49
+ MAX_OUTPUT = 4_000 # reserved for the reply
50
+ BUDGET = CONTEXT_WINDOW - MAX_OUTPUT
51
+ ```
52
+
53
+ The anchored strategy needs nothing else:
54
+
55
+ ```python
56
+ from agent_framework import create_harness_agent
57
+ from agent_framework_openai import OpenAIChatClient
58
+ from maf_compaction import AnchoredCompactionStrategy
59
+
60
+ strategy = AnchoredCompactionStrategy(
61
+ max_input_tokens=BUDGET,
62
+ tokenizer=tokenizer,
63
+ keep_head_groups=3, # task and requirements, never touched
64
+ keep_tail_groups=4, # the working set, never touched
65
+ band_share=0.25, # the oldest banded result keeps 25% of the budget
66
+ )
67
+
68
+ client = OpenAIChatClient(model_id="gpt-6-luna")
69
+ agent = create_harness_agent(
70
+ client,
71
+ name="assistant",
72
+ agent_instructions="Answer from the lookups you make. Quote values exactly.",
73
+ tools=[lookup_deployment],
74
+ max_context_window_tokens=CONTEXT_WINDOW,
75
+ max_output_tokens=MAX_OUTPUT,
76
+ before_compaction_strategy=strategy,
77
+ after_compaction_strategy=strategy,
78
+ tokenizer=tokenizer,
79
+ )
80
+ ```
81
+
82
+ 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:
83
+
84
+ ```python
85
+ from maf_compaction import (
86
+ AnchoredCompactionStrategy,
87
+ RecallGate,
88
+ ToolResultAnchoredSummarizationCompactionStrategy,
89
+ ToolResultRecallMiddleware,
90
+ make_recall_tool,
91
+ )
92
+
93
+ strategy = ToolResultAnchoredSummarizationCompactionStrategy(
94
+ max_input_tokens=BUDGET,
95
+ tokenizer=tokenizer,
96
+ trigger_fraction=0.6, # ask for a record past 60% of the budget
97
+ fallback_fraction=0.9, # stop waiting for one past 90%
98
+ fallback=AnchoredCompactionStrategy(max_input_tokens=BUDGET, tokenizer=tokenizer),
99
+ )
100
+
101
+ gate = RecallGate()
102
+ recall_tool = make_recall_tool(gate, target_tokens=2_000)
103
+ recall = ToolResultRecallMiddleware(
104
+ max_input_tokens=BUDGET,
105
+ tokenizer=tokenizer, # the same tokenizer as the strategy
106
+ arm=gate.arm,
107
+ disarm=gate.disarm,
108
+ trigger_fraction=strategy.trigger_fraction,
109
+ record_max_tokens=4_000,
110
+ repeat_records=True, # a record per new batch of tool work
111
+ reforce=strategy.take_reforce,
112
+ )
113
+
114
+ agent = create_harness_agent(
115
+ client,
116
+ name="assistant",
117
+ agent_instructions="Answer from the lookups you make. Quote values exactly.",
118
+ tools=[lookup_deployment, recall_tool],
119
+ middleware=[recall],
120
+ max_context_window_tokens=CONTEXT_WINDOW,
121
+ max_output_tokens=MAX_OUTPUT,
122
+ before_compaction_strategy=strategy,
123
+ after_compaction_strategy=strategy,
124
+ tokenizer=tokenizer,
125
+ )
126
+ ```
127
+
128
+ The summarising strategies, `RecallGate` and `ToolResultRecallMiddleware` keep one conversation's decisions on the instance, so build this stack, and the agent that holds it, once per session. The middleware raises if a second session reaches it.
129
+
130
+ 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.
131
+
132
+ ## When to use it
133
+
134
+ 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](https://github.com/sokolaidev/maf-extensions/blob/main/docs/compaction/README.md).
135
+
136
+ ## What it depends on
137
+
138
+ 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.
139
+
140
+ ## Licence
141
+
142
+ MIT. See the [LICENSE](https://github.com/sokolaidev/maf-extensions/blob/main/LICENSE).
@@ -0,0 +1,80 @@
1
+ [project]
2
+ name = "maf-compaction"
3
+ version = "0.1.0"
4
+ description = "Prompt-cache-aware compaction strategies for Microsoft Agent Framework: anchored trimming, tool-result records and user-turn summaries that keep the cached prefix intact."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12,<3.15"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ keywords = [
10
+ "compaction",
11
+ "context-window",
12
+ "prompt-cache",
13
+ "agent-framework",
14
+ "microsoft-agent-framework",
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Typing :: Typed",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Topic :: Software Development :: Libraries",
25
+ ]
26
+ dependencies = ["agent-framework-core>=1.20.0,<1.21"]
27
+
28
+ [[project.authors]]
29
+ name = "SOKOLAI BV"
30
+ email = "info@sokolai.com"
31
+
32
+ [project.urls]
33
+ Homepage = "https://www.sokol.ai"
34
+ Source = "https://github.com/sokolaidev/maf-extensions"
35
+ Issues = "https://github.com/sokolaidev/maf-extensions/issues"
36
+
37
+ [tool.uv.build-backend]
38
+ module-name = "maf_compaction"
39
+ module-root = "src"
40
+
41
+ [tool.ruff]
42
+ line-length = 100
43
+ target-version = "py312"
44
+
45
+ [tool.ruff.lint]
46
+ extend-select = [
47
+ "I",
48
+ "UP",
49
+ "D100",
50
+ "D101",
51
+ "D103",
52
+ "D104",
53
+ "E501",
54
+ "W505",
55
+ "TD",
56
+ "FIX",
57
+ "ERA001",
58
+ ]
59
+
60
+ [tool.ruff.lint.per-file-ignores]
61
+ "tests/**" = [
62
+ "D101",
63
+ "D103",
64
+ "E501",
65
+ "W505",
66
+ ]
67
+
68
+ [tool.ruff.lint.pycodestyle]
69
+ max-doc-length = 100
70
+
71
+ [tool.pyright]
72
+ include = ["src"]
73
+ typeCheckingMode = "strict"
74
+
75
+ [tool.pytest.ini_options]
76
+ testpaths = ["tests"]
77
+
78
+ [build-system]
79
+ requires = ["uv_build>=0.11.24,<0.12.0"]
80
+ build-backend = "uv_build"
@@ -0,0 +1,70 @@
1
+ [project]
2
+ name = "maf-compaction"
3
+ version = "0.1.0"
4
+ description = "Prompt-cache-aware compaction strategies for Microsoft Agent Framework: anchored trimming, tool-result records and user-turn summaries that keep the cached prefix intact."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12,<3.15"
7
+ authors = [{ name = "SOKOLAI BV", email = "info@sokolai.com" }]
8
+ license = "MIT"
9
+ license-files = ["LICENSE"]
10
+ keywords = [
11
+ "compaction",
12
+ "context-window",
13
+ "prompt-cache",
14
+ "agent-framework",
15
+ "microsoft-agent-framework",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "Typing :: Typed",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Topic :: Software Development :: Libraries",
26
+ ]
27
+ dependencies = [
28
+ # The strategies subclass the framework's `CompactionStrategy` and call its private
29
+ # `_compaction` helpers (grouping, token annotation, the exclusion flags), which nothing
30
+ # public exposes. The ceiling admits only the minor those helpers were read against: a
31
+ # private module changing shape must be a resolution failure, not a silent change of
32
+ # behaviour.
33
+ "agent-framework-core>=1.20.0,<1.21",
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://www.sokol.ai"
38
+ Source = "https://github.com/sokolaidev/maf-extensions"
39
+ Issues = "https://github.com/sokolaidev/maf-extensions/issues"
40
+
41
+ [tool.uv.build-backend]
42
+ module-name = "maf_compaction"
43
+ module-root = "src"
44
+
45
+ [tool.ruff]
46
+ line-length = 100
47
+ target-version = "py312"
48
+
49
+ [tool.ruff.lint]
50
+ extend-select = ["I", "UP", "D100", "D101", "D103", "D104", "E501", "W505", "TD", "FIX", "ERA001"]
51
+
52
+ [tool.ruff.lint.per-file-ignores]
53
+ # ruff resolves [tool.ruff] per file by the NEAREST ancestor pyproject.toml that has one, so
54
+ # this package carries its own copy rather than inheriting one from the workspace root.
55
+ # The length rules join the docstring pair: test fixtures are lenient, not API surface.
56
+ "tests/**" = ["D101", "D103", "E501", "W505"]
57
+
58
+ [tool.ruff.lint.pycodestyle]
59
+ max-doc-length = 100
60
+
61
+ [tool.pyright]
62
+ include = ["src"]
63
+ typeCheckingMode = "strict"
64
+
65
+ [tool.pytest.ini_options]
66
+ testpaths = ["tests"]
67
+
68
+ [build-system]
69
+ requires = ["uv_build>=0.11.24,<0.12.0"]
70
+ build-backend = "uv_build"
@@ -0,0 +1,177 @@
1
+ """Compaction strategies that keep the prompt cache.
2
+
3
+ Provider prompt caches match on exact prefixes, and compaction rewrites history by
4
+ construction. Each strategy here is shaped to break the cached prefix as rarely and as late
5
+ as it can:
6
+
7
+ - :class:`AnchoredCompactionStrategy` keeps a fixed head and tail verbatim and collapses the
8
+ band between them by a rule that reads a group's *position* and nothing else, so the same
9
+ prefix compacts to the same bytes on every later turn and stays cached.
10
+ - :class:`MinimumGainAnchoredCompactionStrategy` adds a floor: it projects the reduction
11
+ before mutating anything and declines a collapse too small to repay the prompt cache it
12
+ would invalidate.
13
+ - :class:`ToolResultAnchoredSummarizationCompactionStrategy` has the agent write the facts
14
+ from its tool results into a record, then drops the tool groups that record covers.
15
+ :func:`make_recall_tool` is the tool the model writes the record with, :class:`RecallGate`
16
+ keeps that tool inert until it is asked for, and :class:`ToolResultRecallMiddleware` asks.
17
+ - :class:`UserTurnAnchoredSummarizationCompactionStrategy` summarises the *user's* turns
18
+ between a fixed head and tail, in one of three modes: recompact its earlier summary once the
19
+ band is worth a pass again, leave it standing as a boundary the next pass compacts only
20
+ behind, or do that and fold the standing summaries into one.
21
+ - :class:`ToolResultAndUserTurnAnchoredSummarizationCompactionStrategy` runs those two over
22
+ one conversation, the record phase first, with the user half judged against the prompt the
23
+ record phase left. While the prompt is still over the input budget it runs a last-resort
24
+ chain: merge the records, merge the user summaries, rewrite the record harder, then the
25
+ record phase's fallback. Every replacement is kept only if it is smaller than what it
26
+ replaces, and nothing is checked against content.
27
+ - :func:`set_preserved` and :func:`is_preserved` mark a message no strategy may shorten, drop
28
+ or shed, which is how a record survives the fallback that trims tool results.
29
+ - :func:`find_nested_strategy` and :func:`record_text` are for the code that wires a strategy
30
+ up: finding the record half inside a composition, and reading the record it wrote.
31
+
32
+ The docstrings keep the vocabulary of the benchmark the strategies were measured with: a *row*
33
+ is one strategy run over one conversation, a row reads *DQ* when its prompt went out over the
34
+ window, and the *flags* are the counters each strategy reports about its own run.
35
+
36
+ **This depends on ``agent_framework._compaction``, which is private API.** Grouping, token
37
+ annotation and the exclusion flags all come from there; nothing public exposes them. The
38
+ dependency is pinned to one minor of the framework for that reason, and each new minor is
39
+ read against before the pin moves.
40
+
41
+ What each strategy does, why it is shaped that way and where it fails is in the compaction
42
+ documentation of the maf-extensions repository.
43
+ """
44
+
45
+ from ._anchored import (
46
+ DEFAULT_BAND_SHARE,
47
+ DEFAULT_KEEP_TOKENS,
48
+ DEFAULT_MIN_GAIN_FRACTION,
49
+ MARKER_ID_PREFIX,
50
+ REMOVAL_MARKER,
51
+ AnchoredCompactionStrategy,
52
+ MinimumGainAnchoredCompactionStrategy,
53
+ )
54
+ from ._composed import (
55
+ DEFAULT_CHAIN_GAIN_FRACTION,
56
+ DEFAULT_HARDER_ATTEMPTS,
57
+ ChainDecisions,
58
+ ToolResultAndUserTurnAnchoredSummarizationCompactionStrategy,
59
+ )
60
+ from ._nested import find_nested_strategy
61
+ from ._preserve import (
62
+ PRESERVE_REASON_KEY,
63
+ PRESERVED_KEY,
64
+ any_preserved,
65
+ is_preserved,
66
+ set_preserved,
67
+ )
68
+ from ._toolsummary import (
69
+ DEFAULT_COVERAGE_SHARE,
70
+ DEFAULT_FALLBACK_FRACTION,
71
+ DEFAULT_RECORD_MAX_TOKENS,
72
+ DEFAULT_RECORD_TARGET_TOKENS,
73
+ DEFAULT_TRIGGER_FRACTION,
74
+ PRESERVE_REASON_UNCOVERED,
75
+ PRESERVE_REASON_UNRECORDED,
76
+ RECALL_TOOL_NAME,
77
+ RECORD_MARKER,
78
+ RecallGate,
79
+ RecordDecisions,
80
+ ToolResultAnchoredSummarizationCompactionStrategy,
81
+ ToolResultRecallMiddleware,
82
+ find_record_index,
83
+ make_recall_tool,
84
+ record_text,
85
+ )
86
+ from ._usersummary import (
87
+ DEFAULT_KEEP_HEAD_USER_TURNS,
88
+ DEFAULT_KEEP_TAIL_USER_TURNS,
89
+ DEFAULT_MIN_BAND_SHARE,
90
+ DEFAULT_SUMMARY_MODE,
91
+ DEFAULT_USER_FOLD_PROMPT,
92
+ DEFAULT_USER_SUMMARY_PROMPT,
93
+ DEFAULT_USER_TRIGGER_FRACTION,
94
+ FOLD_EXCLUDE_REASON,
95
+ FOLD_ID_PREFIX,
96
+ SUMMARY_MODE_BOUNDARY,
97
+ SUMMARY_MODE_FOLD,
98
+ SUMMARY_MODE_RECOMPACT,
99
+ SUMMARY_MODES,
100
+ USER_SUMMARY_MARKER,
101
+ UserTurnAnchoredSummarizationCompactionStrategy,
102
+ )
103
+
104
+ __all__ = [
105
+ "DEFAULT_BAND_SHARE",
106
+ "DEFAULT_CHAIN_GAIN_FRACTION",
107
+ "DEFAULT_COVERAGE_SHARE",
108
+ "DEFAULT_FALLBACK_FRACTION",
109
+ "DEFAULT_HARDER_ATTEMPTS",
110
+ "DEFAULT_KEEP_HEAD_USER_TURNS",
111
+ "DEFAULT_KEEP_TAIL_USER_TURNS",
112
+ "DEFAULT_KEEP_TOKENS",
113
+ "DEFAULT_MIN_BAND_SHARE",
114
+ "DEFAULT_MIN_GAIN_FRACTION",
115
+ "DEFAULT_RECORD_MAX_TOKENS",
116
+ "DEFAULT_RECORD_TARGET_TOKENS",
117
+ "DEFAULT_SUMMARY_MODE",
118
+ "DEFAULT_TRIGGER_FRACTION",
119
+ "DEFAULT_USER_FOLD_PROMPT",
120
+ "DEFAULT_USER_SUMMARY_PROMPT",
121
+ "DEFAULT_USER_TRIGGER_FRACTION",
122
+ "FOLD_EXCLUDE_REASON",
123
+ "FOLD_ID_PREFIX",
124
+ "MARKER_ID_PREFIX",
125
+ "PRESERVED_KEY",
126
+ "PRESERVE_REASON_KEY",
127
+ "PRESERVE_REASON_UNCOVERED",
128
+ "PRESERVE_REASON_UNRECORDED",
129
+ "RECALL_TOOL_NAME",
130
+ "RECORD_MARKER",
131
+ "REMOVAL_MARKER",
132
+ "SUMMARY_MODES",
133
+ "SUMMARY_MODE_BOUNDARY",
134
+ "SUMMARY_MODE_FOLD",
135
+ "SUMMARY_MODE_RECOMPACT",
136
+ "USER_SUMMARY_MARKER",
137
+ "AnchoredCompactionStrategy",
138
+ "ChainDecisions",
139
+ "MafCompactionExperimentalWarning",
140
+ "MinimumGainAnchoredCompactionStrategy",
141
+ "RecallGate",
142
+ "RecordDecisions",
143
+ "ToolResultAnchoredSummarizationCompactionStrategy",
144
+ "ToolResultAndUserTurnAnchoredSummarizationCompactionStrategy",
145
+ "ToolResultRecallMiddleware",
146
+ "UserTurnAnchoredSummarizationCompactionStrategy",
147
+ "any_preserved",
148
+ "find_nested_strategy",
149
+ "find_record_index",
150
+ "is_preserved",
151
+ "make_recall_tool",
152
+ "record_text",
153
+ "set_preserved",
154
+ ]
155
+
156
+ # Experimental package: importing it emits a warning, and catches it when `python -W error`
157
+ # promotes it, so such a host can still import the package.
158
+ import warnings as _warnings
159
+
160
+
161
+ class MafCompactionExperimentalWarning(UserWarning):
162
+ """Warning category for maf-compaction's experimental-package notice."""
163
+
164
+
165
+ def _warn_experimental() -> None:
166
+ message = (
167
+ "maf_compaction is experimental and may change or be removed in future versions "
168
+ "without notice."
169
+ )
170
+ try:
171
+ _warnings.warn(message, category=MafCompactionExperimentalWarning, stacklevel=2)
172
+ except MafCompactionExperimentalWarning:
173
+ # Deliberate: under `-W error` an informational notice must not fail the import.
174
+ pass
175
+
176
+
177
+ _warn_experimental()