Skip to content

Commit 8dde9ef

Browse files
westey-mCopilot
andauthored
Python: HarnessAgent: Disable compaction when max tokens not provided (#6410)
* HarnessAgent: Disable compaction when max tokens not provided * Fix regression. * Address PR comments * Require max_output_tokens to be positive Reject max_output_tokens=0 (must be positive), mirroring max_context_window_tokens. Addresses PR review feedback. Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 93cbf6b commit 8dde9ef

3 files changed

Lines changed: 136 additions & 34 deletions

File tree

python/packages/core/agent_framework/_harness/_agent.py

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -66,23 +66,45 @@ def _assemble_instructions(
6666
def _assemble_compaction_provider(
6767
*,
6868
disable_compaction: bool,
69-
max_context_window_tokens: int,
70-
max_output_tokens: int,
69+
max_context_window_tokens: int | None,
70+
max_output_tokens: int | None,
7171
history_source_id: str,
7272
before_compaction_strategy: CompactionStrategy | None,
7373
after_compaction_strategy: CompactionStrategy | None,
7474
tokenizer: TokenizerProtocol | None,
7575
) -> CompactionProvider | None:
76-
"""Build the compaction provider from parameters or defaults."""
76+
"""Build the compaction provider from parameters or defaults.
77+
78+
The token-budget defaults (``ContextWindowCompactionStrategy`` for the before phase and
79+
``ToolResultCompactionStrategy`` for the after phase) are only applied when the token
80+
params are provided. Caller-supplied strategies are always honored. Either phase may end
81+
up ``None``, which ``CompactionProvider`` interprets as "skip that phase".
82+
83+
Returns None when compaction is explicitly disabled, or when neither phase has a strategy
84+
(no custom strategies and no token budget to build the defaults).
85+
"""
7786
if disable_compaction:
7887
return None
7988

80-
before_strategy = before_compaction_strategy or ContextWindowCompactionStrategy(
81-
max_context_window_tokens=max_context_window_tokens,
82-
max_output_tokens=max_output_tokens,
83-
tokenizer=tokenizer,
84-
)
85-
after_strategy = after_compaction_strategy or ToolResultCompactionStrategy(keep_last_tool_call_groups=2)
89+
# Resolve the before-strategy: custom strategy wins; otherwise fall back to the
90+
# token-budget-aware default when token params are available.
91+
before_strategy = before_compaction_strategy
92+
if before_strategy is None and max_context_window_tokens is not None and max_output_tokens is not None:
93+
before_strategy = ContextWindowCompactionStrategy(
94+
max_context_window_tokens=max_context_window_tokens,
95+
max_output_tokens=max_output_tokens,
96+
tokenizer=tokenizer,
97+
)
98+
99+
# Resolve the after-strategy: custom strategy wins; otherwise fall back to the default
100+
# when token params are available.
101+
after_strategy = after_compaction_strategy
102+
if after_strategy is None and max_context_window_tokens is not None and max_output_tokens is not None:
103+
after_strategy = ToolResultCompactionStrategy(keep_last_tool_call_groups=2)
104+
105+
# Nothing to compact in either phase: skip the provider entirely.
106+
if before_strategy is None and after_strategy is None:
107+
return None
86108

87109
return CompactionProvider(
88110
before_strategy=before_strategy,
@@ -157,8 +179,8 @@ def create_harness_agent(
157179
harness_instructions: str | None = None,
158180
agent_instructions: str | None = None,
159181
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
160-
max_context_window_tokens: int,
161-
max_output_tokens: int,
182+
max_context_window_tokens: int | None = None,
183+
max_output_tokens: int | None = None,
162184
history_provider: HistoryProvider | None = None,
163185
disable_compaction: bool = False,
164186
before_compaction_strategy: CompactionStrategy | None = None,
@@ -206,8 +228,6 @@ def create_harness_agent(
206228
207229
agent = create_harness_agent(
208230
OpenAIChatClient(model="gpt-4o"),
209-
max_context_window_tokens=128_000,
210-
max_output_tokens=16_384,
211231
)
212232
session = agent.create_session()
213233
response = await agent.run("Plan a weekend trip to Seattle", session=session)
@@ -243,13 +263,21 @@ def create_harness_agent(
243263
(e.g., "You are a research assistant focused on academic sources.").
244264
tools: Additional tools to include in the agent's toolset.
245265
max_context_window_tokens: Maximum tokens the model's context window supports.
266+
Used to construct the default token-budget-aware compaction strategies. When None
267+
(default) and no custom ``before_compaction_strategy`` / ``after_compaction_strategy``
268+
is provided, compaction is automatically disabled.
246269
max_output_tokens: Maximum output tokens per response.
270+
Used to construct the default compaction strategies and sets a default max_tokens
271+
chat option. When None (default), no default max_tokens option is set, and unless a
272+
custom compaction strategy is provided, compaction is automatically disabled.
247273
history_provider: Custom history provider. When None, an InMemoryHistoryProvider is used.
248274
disable_compaction: When True, skip compaction provider setup.
249-
before_compaction_strategy: Custom before-run compaction strategy.
250-
Defaults to ContextWindowCompactionStrategy (token-budget aware).
251-
after_compaction_strategy: Custom after-run compaction strategy.
252-
Defaults to ToolResultCompactionStrategy.
275+
before_compaction_strategy: Custom before-run compaction strategy. When provided,
276+
compaction runs even if token params are omitted. Defaults to
277+
ContextWindowCompactionStrategy (token-budget aware) when token params are provided.
278+
after_compaction_strategy: Custom after-run compaction strategy. When provided,
279+
compaction runs even if token params are omitted. Defaults to
280+
ToolResultCompactionStrategy when token params are provided.
253281
tokenizer: Custom tokenizer for compaction strategies.
254282
disable_todo: When True, skip the TodoProvider.
255283
todo_provider: Custom TodoProvider instance. Ignored when disable_todo is True.
@@ -283,14 +311,19 @@ def create_harness_agent(
283311
A fully configured :class:`~agent_framework.Agent` instance.
284312
285313
Raises:
286-
ValueError: If max_context_window_tokens <= 0 or max_output_tokens < 0
287-
or max_output_tokens >= max_context_window_tokens.
314+
ValueError: If max_context_window_tokens is provided and <= 0, or
315+
max_output_tokens is provided and <= 0, or max_output_tokens >=
316+
max_context_window_tokens when both are provided.
288317
"""
289-
if max_context_window_tokens <= 0:
318+
if max_context_window_tokens is not None and max_context_window_tokens <= 0:
290319
raise ValueError("max_context_window_tokens must be positive.")
291-
if max_output_tokens < 0:
292-
raise ValueError("max_output_tokens must be non-negative.")
293-
if max_output_tokens >= max_context_window_tokens:
320+
if max_output_tokens is not None and max_output_tokens <= 0:
321+
raise ValueError("max_output_tokens must be positive.")
322+
if (
323+
max_context_window_tokens is not None
324+
and max_output_tokens is not None
325+
and max_output_tokens >= max_context_window_tokens
326+
):
294327
raise ValueError("max_output_tokens must be less than max_context_window_tokens.")
295328

296329
# Build history provider.
@@ -347,7 +380,8 @@ def create_harness_agent(
347380

348381
# Build default options dict.
349382
default_opts: dict[str, Any] = dict(default_options) if default_options else {}
350-
default_opts.setdefault("max_tokens", max_output_tokens)
383+
if max_output_tokens is not None:
384+
default_opts.setdefault("max_tokens", max_output_tokens)
351385

352386
agent = Agent(
353387
client,

python/packages/core/tests/core/test_harness_agent.py

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,63 @@ def test_create_harness_agent_returns_full_agent() -> None:
194194
assert isinstance(agent, FullAgent)
195195

196196

197+
def test_create_harness_agent_no_token_params_disables_compaction() -> None:
198+
"""When token params are omitted, compaction is automatically disabled."""
199+
agent = create_harness_agent(
200+
client=_FakeChatClient(), # type: ignore[arg-type]
201+
)
202+
provider_types = [type(p) for p in agent.context_providers]
203+
assert CompactionProvider not in provider_types
204+
205+
206+
def test_create_harness_agent_no_token_params_skips_max_tokens_option() -> None:
207+
"""When max_output_tokens is omitted, max_tokens should not be set in default options."""
208+
agent = create_harness_agent(
209+
client=_FakeChatClient(), # type: ignore[arg-type]
210+
)
211+
assert agent.default_options.get("max_tokens") is None
212+
213+
214+
def test_create_harness_agent_custom_before_strategy_enables_compaction_without_tokens() -> None:
215+
"""A custom before_compaction_strategy enables compaction even when token params are omitted."""
216+
from agent_framework import ToolResultCompactionStrategy
217+
218+
agent = create_harness_agent(
219+
client=_FakeChatClient(), # type: ignore[arg-type]
220+
before_compaction_strategy=ToolResultCompactionStrategy(),
221+
)
222+
provider_types = [type(p) for p in agent.context_providers]
223+
assert CompactionProvider in provider_types
224+
225+
226+
def test_create_harness_agent_disable_compaction_overrides_custom_before_strategy() -> None:
227+
"""disable_compaction=True wins even when a custom before strategy is provided."""
228+
from agent_framework import ToolResultCompactionStrategy
229+
230+
agent = create_harness_agent(
231+
client=_FakeChatClient(), # type: ignore[arg-type]
232+
before_compaction_strategy=ToolResultCompactionStrategy(),
233+
disable_compaction=True,
234+
)
235+
provider_types = [type(p) for p in agent.context_providers]
236+
assert CompactionProvider not in provider_types
237+
238+
239+
def test_create_harness_agent_custom_after_strategy_enables_compaction_without_tokens() -> None:
240+
"""A custom after_compaction_strategy enables compaction even when token params are omitted."""
241+
from agent_framework import ToolResultCompactionStrategy
242+
243+
agent = create_harness_agent(
244+
client=_FakeChatClient(), # type: ignore[arg-type]
245+
after_compaction_strategy=ToolResultCompactionStrategy(),
246+
)
247+
compaction_providers = [p for p in agent.context_providers if isinstance(p, CompactionProvider)]
248+
assert len(compaction_providers) == 1
249+
# Before phase is skipped (no token budget, no custom before strategy), after phase is set.
250+
assert compaction_providers[0].before_strategy is None
251+
assert compaction_providers[0].after_strategy is not None
252+
253+
197254
# --- Validation Tests ---
198255

199256

@@ -207,14 +264,15 @@ def test_create_harness_agent_rejects_invalid_context_tokens() -> None:
207264
)
208265

209266

210-
def test_create_harness_agent_rejects_negative_output_tokens() -> None:
211-
"""max_output_tokens must be non-negative."""
212-
with pytest.raises(ValueError, match="max_output_tokens must be non-negative"):
213-
create_harness_agent(
214-
client=_FakeChatClient(), # type: ignore[arg-type]
215-
max_context_window_tokens=1000,
216-
max_output_tokens=-1,
217-
)
267+
def test_create_harness_agent_rejects_non_positive_output_tokens() -> None:
268+
"""max_output_tokens must be positive when provided."""
269+
for invalid_value in (0, -1):
270+
with pytest.raises(ValueError, match="max_output_tokens must be positive"):
271+
create_harness_agent(
272+
client=_FakeChatClient(), # type: ignore[arg-type]
273+
max_context_window_tokens=1000,
274+
max_output_tokens=invalid_value,
275+
)
218276

219277

220278
def test_create_harness_agent_rejects_output_gte_context() -> None:

python/samples/02-agents/harness/README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,31 @@ python samples/02-agents/harness/harness_research.py
4545

4646
### Minimal Setup
4747

48-
`create_harness_agent` requires only a chat client and token budget parameters:
48+
`create_harness_agent` requires only a chat client:
4949

5050
```python
5151
from agent_framework import create_harness_agent
5252
from agent_framework.foundry import FoundryChatClient
5353
from azure.identity import AzureCliCredential
5454

55+
agent = create_harness_agent(
56+
client=FoundryChatClient(credential=AzureCliCredential()),
57+
)
58+
```
59+
60+
### With Compaction
61+
62+
Provide token budget parameters to enable automatic context-window compaction:
63+
64+
```python
5565
agent = create_harness_agent(
5666
client=FoundryChatClient(credential=AzureCliCredential()),
5767
max_context_window_tokens=128_000,
5868
max_output_tokens=16_384,
5969
)
6070
```
6171

62-
### Customization
72+
### Further Customization
6373

6474
Disable or customize any feature:
6575

0 commit comments

Comments
 (0)