Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 53 additions & 36 deletions python/packages/core/agent_framework/_harness/_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from .._agents import Agent, SupportsAgentRun
from .._clients import SupportsShellTool, SupportsWebSearchTool
from .._compaction import CompactionProvider, ContextWindowCompactionStrategy, ToolResultCompactionStrategy
from .._compaction import CompactionProvider, ContextWindowCompactionStrategy
from .._feature_stage import ExperimentalFeature, experimental
from .._sessions import ContextProvider, HistoryProvider, InMemoryHistoryProvider, MessageInjectionMiddleware
from .._skills import SkillsProvider
Expand Down Expand Up @@ -78,7 +78,7 @@ def _assemble_instructions(
return f"{harness}\n\n{agent_instructions or ''}".strip() or None


def _assemble_compaction_provider(
def _assemble_compaction(
*,
disable_compaction: bool,
max_context_window_tokens: int | None,
Expand All @@ -87,46 +87,58 @@ def _assemble_compaction_provider(
before_compaction_strategy: CompactionStrategy | None,
after_compaction_strategy: CompactionStrategy | None,
tokenizer: TokenizerProtocol | None,
) -> CompactionProvider | None:
"""Build the compaction provider from parameters or defaults.

The token-budget defaults (``ContextWindowCompactionStrategy`` for the before phase and
``ToolResultCompactionStrategy`` for the after phase) are only applied when the token
params are provided. Caller-supplied strategies are always honored. Either phase may end
up ``None``, which ``CompactionProvider`` interprets as "skip that phase".

Returns None when compaction is explicitly disabled, or when neither phase has a strategy
(no custom strategies and no token budget to build the defaults).
) -> tuple[CompactionStrategy | None, CompactionProvider | None]:
"""Resolve the harness compaction strategies into their execution sites.

The token-budget default (``ContextWindowCompactionStrategy``) is used for **both** the before
and after phases, and is only applied when the token params are provided. A single shared
instance is reused across both phases (it is stateless). Caller-supplied strategies always win
per phase.

Because the harness enables per-service-call history persistence, running the before-phase
compaction as a ``CompactionProvider.before_run`` hook is a no-op: the agent skips
``HistoryProvider.before_run``, so the provider only ever sees an empty context.
Instead the before-strategy is returned separately so the caller can wire it as the agent's
``compaction_strategy`` chat option. That runs it inside ``BaseChatClient.get_response`` —
per model call, inner of ``PerServiceCallHistoryPersistingMiddleware`` (which has already
loaded the full history into the outgoing messages) and outer of the leaf client.

The after-strategy stays on a ``CompactionProvider`` (its ``after_run`` compacts the persisted
session history in place and works correctly).

Returns a ``(before_strategy, after_provider)`` tuple. Both elements are ``None`` when
compaction is disabled or when the corresponding phase has no strategy (no custom strategy
and no token budget to build the default).
"""
if disable_compaction:
return None
return None, None

# Resolve the before-strategy: custom strategy wins; otherwise fall back to the
# token-budget-aware default when token params are available.
before_strategy = before_compaction_strategy
if before_strategy is None and max_context_window_tokens is not None and max_output_tokens is not None:
before_strategy = ContextWindowCompactionStrategy(
# Token-budget default, shared across both phases when token params are available.
default_strategy: CompactionStrategy | None = None
if max_context_window_tokens is not None and max_output_tokens is not None:
default_strategy = ContextWindowCompactionStrategy(
max_context_window_tokens=max_context_window_tokens,
max_output_tokens=max_output_tokens,
tokenizer=tokenizer,
)

# Resolve the after-strategy: custom strategy wins; otherwise fall back to the default
# when token params are available.
after_strategy = after_compaction_strategy
if after_strategy is None and max_context_window_tokens is not None and max_output_tokens is not None:
after_strategy = ToolResultCompactionStrategy(keep_last_tool_call_groups=2)
# Resolve each phase: caller-supplied strategy wins; otherwise fall back to the shared default.
before_strategy = before_compaction_strategy if before_compaction_strategy is not None else default_strategy
after_strategy = after_compaction_strategy if after_compaction_strategy is not None else default_strategy

# Nothing to compact in either phase: skip the provider entirely.
if before_strategy is None and after_strategy is None:
return None

return CompactionProvider(
before_strategy=before_strategy,
after_strategy=after_strategy,
tokenizer=tokenizer,
history_source_id=history_source_id,
# The after-strategy runs post-turn against the persisted history via a CompactionProvider;
# the before-strategy runs per model call via the agent's compaction_strategy option.
after_provider = (
CompactionProvider(
before_strategy=None,
after_strategy=after_strategy,
tokenizer=tokenizer,
history_source_id=history_source_id,
)
if after_strategy is not None
else None
)
return before_strategy, after_provider


def _assemble_context_providers(
Expand Down Expand Up @@ -383,8 +395,9 @@ def create_harness_agent(
compaction runs even if token params are omitted. Defaults to
ContextWindowCompactionStrategy (token-budget aware) when token params are provided.
after_compaction_strategy: Custom after-run compaction strategy. When provided,
compaction runs even if token params are omitted. Defaults to
ToolResultCompactionStrategy when token params are provided.
compaction runs even if token params are omitted. Defaults to the same
ContextWindowCompactionStrategy used for the before phase when token params are
provided.
tokenizer: Custom tokenizer for compaction strategies.
disable_todo: When True, skip the TodoProvider.
todo_provider: Custom TodoProvider instance. Ignored when disable_todo is True.
Expand Down Expand Up @@ -496,8 +509,10 @@ def create_harness_agent(
# Build history provider.
resolved_history = history_provider or InMemoryHistoryProvider()

# Build compaction provider.
compaction_provider = _assemble_compaction_provider(
# Build compaction. The before-strategy is wired as the agent's compaction_strategy option
# (runs per model call, inner of per-service-call persistence); the after-strategy stays on a
# CompactionProvider that compacts the persisted history post-turn. See issue #7011.
before_compaction, compaction_provider = _assemble_compaction(
disable_compaction=disable_compaction,
max_context_window_tokens=max_context_window_tokens,
max_output_tokens=max_output_tokens,
Expand Down Expand Up @@ -600,6 +615,8 @@ def create_harness_agent(
default_options=default_opts, # type: ignore[arg-type]
context_providers=assembled_providers,
middleware=assembled_middleware or None,
compaction_strategy=before_compaction,
Comment thread
westey-m marked this conversation as resolved.
tokenizer=tokenizer,
require_per_service_call_history_persistence=True,
)

Expand Down
Loading
Loading