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.
- maf_compaction-0.1.0/LICENSE +21 -0
- maf_compaction-0.1.0/PKG-INFO +166 -0
- maf_compaction-0.1.0/README.md +142 -0
- maf_compaction-0.1.0/pyproject.toml +80 -0
- maf_compaction-0.1.0/pyproject.toml.orig +70 -0
- maf_compaction-0.1.0/src/maf_compaction/__init__.py +177 -0
- maf_compaction-0.1.0/src/maf_compaction/_anchored.py +865 -0
- maf_compaction-0.1.0/src/maf_compaction/_composed.py +1394 -0
- maf_compaction-0.1.0/src/maf_compaction/_nested.py +38 -0
- maf_compaction-0.1.0/src/maf_compaction/_preserve.py +176 -0
- maf_compaction-0.1.0/src/maf_compaction/_summary_links.py +26 -0
- maf_compaction-0.1.0/src/maf_compaction/_toolsummary.py +2352 -0
- maf_compaction-0.1.0/src/maf_compaction/_usersummary.py +1681 -0
- maf_compaction-0.1.0/src/maf_compaction/py.typed +0 -0
|
@@ -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
|
+
[](https://pypi.org/project/maf-compaction/) [](https://pypi.org/project/maf-compaction/) [](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
|
+
[](https://pypi.org/project/maf-compaction/) [](https://pypi.org/project/maf-compaction/) [](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()
|