Skip to content

feat(context-py): port strategy presets and agent rewire - #4282

Merged
lizradway merged 9 commits into
strands-agents:mainfrom
lizradway:cm-presets-and-rewire-python
Sep 11, 2026
Merged

lizradway merged 9 commits into
strands-agents:mainfrom
lizradway:cm-presets-and-rewire-python

Conversation

@lizradway

Copy link
Copy Markdown
Member

Description

Port the TS context-manager presets and agent rewire (PR #4256) to the Python SDK. The Agent constructor now uses ContextManager.from_strategy() and ContextManager.resolve_conversation_manager() instead of the old _resolve_context_manager method with SummarizingConversationManager + ContextOffloader.

Key changes:

  • Add presets.py with resolve_preset/resolve_strategies for named presets
  • Add from_strategy() factory and resolve_conversation_manager() static method to ContextManager
  • Add overflow flag to ContextState, honored by offload and emergency truncate strategies
  • Protect pinned messages in offload strategies (eager hook, per-block, per-message) and preserve pinned metadata through _repair_alternation
  • Rewire Agent constructor to build ContextManager plugin directly
  • Update all tests for new architecture

Related Issues

Documentation PR

Type of Change

Bug fix
New feature
Breaking change
Documentation update
Other (please describe):

Testing

How have you tested the change? Verify that the changes do not break functionality or introduce new warnings.

  • I ran hatch run prepare

Checklist

  • I have read the CONTRIBUTING document
  • I have reviewed and understand every line of code in this PR, including any generated by AI tools, and I can explain why it works
  • My change is focused and reasonably small; I have split unrelated work into separate PRs
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Port the TS context-manager presets and agent rewire (PR strands-agents#4256) to the
Python SDK. The Agent constructor now uses ContextManager.from_strategy()
and ContextManager.resolve_conversation_manager() instead of the old
_resolve_context_manager method with SummarizingConversationManager +
ContextOffloader.

Key changes:
- Add presets.py with resolve_preset/resolve_strategies for named presets
- Add from_strategy() factory and resolve_conversation_manager() static
  method to ContextManager
- Add overflow flag to ContextState, honored by offload and emergency
  truncate strategies
- Protect pinned messages in offload strategies (eager hook, per-block,
  per-message) and preserve pinned metadata through _repair_alternation
- Rewire Agent constructor to build ContextManager plugin directly
- Update all tests for new architecture
@lizradway

Copy link
Copy Markdown
Member Author

@strandly-the-agent pls review

@github-actions github-actions Bot added area-context Session or context related area-agent Related to the agent class or general agent questions python Pull requests that update python code enhancement New feature or request complexity/medium Touched functions have moderate cognitive complexity (11-25) size/m labels Sep 10, 2026
@codecov

codecov Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.67442% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ds/_context_manager/strategies/offload/truncate.py 84.21% 1 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

… pinned protection

Cover the lines flagged by codecov:
- presets.py: all preset names, unknown preset errors, mixed resolve
- offload/base.py: overflow bypass on utilization gate, pinned message
  guards in eager hook / per-block / per-message, repair_alternation
  pinned metadata preservation
- truncate.py: EmergencyTruncateStrategy overflow vs non-overflow paths
- context_manager.py: unsupported type in from_strategy()

@strandly-the-agent strandly-the-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested — the proactive_summarization preset loses its preserve_recent (0.7 → int() → 0) and summarizes the in-flight turn; mypy fails on the branch (2 errors).

Otherwise a faithful port: the overflow bypass, pinned protection and Agent rewire held up under attack (details below). One 🔴, two 🟡 inline; a few parity questions. No needs-api-review label yet, but Agent(context_manager=...) changed both its accepted types and its behaviour when conversation_manager is co-provided — worth flagging for API review.

✅ Verified at 97be1e1
  • ruff check + ruff format --check: clean.
  • pytest tests (py3.13): 6594 passed, 31 skipped (1058 in _context_manager/agent, 5536 elsewhere).
  • mypy --follow-imports=silent src/strands/_context_manager src/strands/agent/agent.py src/strands/experimental/context_manager: 2 errors — presets.py:44 (float→int) and context_manager.py:143 (list[ContextStrategy] vs list[ContextStrategy | str]). Both are in the hatch run prepare gate.
  • Overflow path: real Agent + fake model, presets auto/agentic, history 3–7 messages, 1–4 consecutive overflows — live user turn preserved every time, no orphan toolResults / role collisions, overflow=True bypass fires as intended.
  • Pinned protection: eager per-block truncate, per-block, message-level drop (preserve_recent 0 and 2) — pinned message and its tool-pair partner intact; _repair_alternation keeps the flag.
  • context_manager={} → same as "auto" + NullConversationManager (matches TS). Co-provided conversation_manager warning is tested.
  • Evidence (repro scripts + outputs, logs) uploaded to the artifacts bucket under strands-agents/harness-sdk/pr/4282/.
  • Process note: my three specialist subagent passes timed out; I ran the remaining checks myself from their partial artifacts.
Questions (non-blocking)
  1. Breaking change: previously a co-provided conversation_manager won over context_manager="auto" (old docstring: "the user's conversation manager is used instead"); now it's discarded with a warning. Matches TS, but Python users who passed both silently lose their CM — should the PR body call this out as breaking?
  2. _removal_ratio: TS #4256 removed _removalRatio (message-level strategies now remove all eligible; preserveRecent is the only knob), Python keeps 0.3 in base.py:274. Intentional divergence, or follow-up?
  3. Emergency truncate vs pins: Python's EmergencyTruncateStrategy._apply_per_message goes through the pinned-aware _get_eligible_messages; TS's ignores pins (messages.filter((_, i) => i > 0)). With every history message pinned, Python's only candidate is the live user turn, which it drops (_adv/attack3b_live_turn_deleted.out). Which behaviour is intended?
Reading order
  1. presets.py + base.py:285-291 (the coercion) → 2. context_manager.py from_strategy / resolve_conversation_manager → 3. agent.py:383-405 → 4. base.py pinned/overflow hunks → 5. tests.
Appendix — non-blocking (6)
  • ⚪ context_manager.py:175 stacklevel=4 points one frame above the user's Agent(...) call (verified: warning attributed to the caller of the function that constructs the Agent). Should be 3.
  • ⚪ ContextManagerStrategy Literal is now defined twice (agent.py:157, context_manager.py:43); ContextManagerStrategyValue (context_manager.py:51) is an unused string constant. Define once in _context_manager and import in agent.py.
  • ⚪ base.py:317-318: prev.get("metadata", {}).get("custom", {}) raises AttributeError when metadata or metadata["custom"] is None — and since the splice already happened, the strategy aborts leaving non-alternating messages that get sent to the model. Type-violating input, so low priority; reusing pin_message._has_pinned_flag-style guarding (metadata is not None and ...) closes it.
  • ⚪ test_presets.py only asserts strategy names; asserting _preserve_recent/_utilization_threshold per preset would have caught the 🔴.
  • ⚪ models/bedrock.py hunk is a formatting-only change unrelated to this PR.
  • ⚪ ContextManager.resolve_conversation_manager lazily imports agent.conversation_manager from inside _context_manager — TS keeps this resolution in agent.ts. Layering nit; fine if deliberate.

Comment thread strands-py/src/strands/_context_manager/presets.py
Comment thread strands-py/src/strands/_context_manager/context_manager.py
Comment thread strands-py/src/strands/agent/agent.py
…ecent, config types, and agent param

- Support float preserve_recent (0 < value < 1 treated as ratio of matching messages)
- Accept preset name strings in ContextManagerConfig.strategies
- Add ContextManagerConfig to agent's context_manager type union
- Fix stacklevel 4→3 in resolve_conversation_manager warning
- Remove duplicate ContextManagerStrategy definition from agent.py
- Remove unused ContextManagerStrategyValue constant
@lizradway

Copy link
Copy Markdown
Member Author

@strandly-the-agent pls review

@strandly-the-agent strandly-the-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — all three findings from my first pass are fixed at 43ffff0, and the fixes didn't trade one bug for another.

Still missing an API-review label; the Agent(context_manager=...) surface here mirrors TS #4256, which is itself still under human review — the two should land together.

Status of previous findings
  • 🔴 presets.py:44 preserve_recent 0.7 → 0 — fixed (a1b3452): _get_oldest_matches now treats 0 < count < 1 as a ratio (ceil(len * count)), _preserve_recent is a float, .when()/_build_conditions/OffloadConditions widened. Re-ran the repro: _preserve_recent=0.7, summarizer never called, tool turn intact. Boundary check (10 msgs): 0.05→9 eligible, 0.5→5, 0.7→3, 0.99→0, 1/1.0→9, 4→6, 2.5→8 — matches TS getOldestMatches.
  • 🟡 ContextManagerConfig.strategies type — fixed (list[ContextStrategy | str]; mypy clean).
  • 🟡 Agent.__init__ annotation — fixed (ContextManagerConfig added, imported under TYPE_CHECKING).
  • ⚪ stacklevel → 3 — fixed (warning now attributed to the Agent(...) call line).
  • ⚪ duplicate ContextManagerStrategy / unused ContextManagerStrategyValue — fixed (single definition, imported into agent.py).
  • Questions 1–3 (breaking-change note, _removal_ratio, emergency-truncate vs pins) — still open, non-blocking.
✅ Verified at 43ffff0
  • ruff check + ruff format --check: clean.
  • mypy (touched modules, --follow-imports=silent): clean (was 2 errors).
  • pytest tests: 6598 passed, 31 skipped.
  • Overflow attack re-run (auto/agentic, history 3–7, 1–4 consecutive overflows): live turn preserved in all 9 scenarios, no orphan toolResults / role collisions.
  • New in 43ffff0: _run_strategies clears context.overflow before EmergencyTruncateStrategy once an earlier strategy acted, so emergency truncate re-gates on real utilization instead of firing unconditionally. Behaves correctly in the overflow scenarios; note TS _runStrategies at #4256 head doesn't do this, so it's a (sensible) divergence worth mirroring there. No test pins it down — optional.
  • Evidence uploaded under strands-agents/harness-sdk/pr/4282/ (*-after-fix.out, mypy-pr.txt, pytest-full.log).

@lizradway
lizradway marked this pull request as ready for review September 10, 2026 19:40
@lizradway
lizradway requested a review from a team as a code owner September 10, 2026 19:40
Comment thread strands-py/src/strands/agent/agent.py Outdated
Comment thread strands-py/src/strands/_context_manager/strategies/offload/base.py Outdated
Comment thread strands-py/src/strands/_context_manager/presets.py Outdated
Comment thread strands-py/src/strands/_context_manager/strategies/offload/base.py Outdated
Comment thread strands-py/tests/strands/_context_manager/test_presets.py Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Comment

Solid, well-tested port of the TS presets/agent-rewire to Python — from_strategy/resolve_conversation_manager have thorough coverage (config dict, agentic, unknown-preset, invalid type) and codecov reports full line coverage. Feedback is non-blocking; the main themes are diff hygiene and a couple of maintainability nits.

Review Categories
  • Diff hygiene: Unrelated formatting-only churn (bedrock.py source + several test files) is bundled in — please split it out per the "don't pad the change" rule.
  • Evergreen comments: One comment narrates change history rather than stating an invariant.
  • Maintainability: Pinned-detection logic is duplicated via raw dict access instead of reusing the shared helper; a few O(n²) identity lookups could be simplified with an id→index map.
  • Tests: A preset test asserts on a private attribute rather than observable behavior; resolve_strategies duplicates validation already done in resolve_preset.

Nice work carrying the preset-as-stable-contract framing and pinned-message protection across the strategies — the design reads cleanly.

- Remove narrating comment in agent.py
- Use _has_pinned_flag helper in _repair_alternation instead of raw dict access
- Remove redundant preset validation in resolve_strategies (resolve_preset raises)
- Build identity→index map to eliminate O(n²) scans in per-block, eager hook, and eligible messages
- Convert preset tests from private attribute assertions to behavioral assertions
@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Approve (pending one diff-hygiene nit)

Re-reviewed at 4c074b16. Round 2 resolves everything blocking — verified locally:

Verified fixes
  • 🔴 proactive_summarization preserve_recent: ratio semantics ported — base.py:158 (math.ceil(len(matching) * count) if 0 < count < 1 else int(count)), base.py:297 stores float(preserve). _preserve_recent=0.7 no longer collapses to 0.
  • 🟡 mypy: clean — mypy --follow-imports=silent src/strands/_context_manager src/strands/agent/agent.py → no issues in 19 files (both prior errors gone).
  • ⚪ stacklevel now 3; _repair_alternation reuses _has_pinned_flag (also closes the metadata is None AttributeError); ContextManagerStrategy defined once and imported; unused ContextManagerStrategyValue removed.
  • My earlier nits: O(n²) lookups replaced with a single index_map (lines 318/353/451); resolve_strategies now delegates validation to resolve_preset.
  • Tests: tests/strands/_context_manager + test_agent_context_manager.py → 255 passed; ruff check clean.

Remaining (non-blocking):

  • The formatting-only churn in models/bedrock.py (+2 −10) and tests/.../test_bedrock.py is still bundled — please drop it so the diff stays scoped to the context-manager change.
  • The three parity questions from the first review (message-level _removal_ratio=0.3 vs TS removing it; emergency-truncate going through the pinned-aware path; the co-provided-conversation_manager behavior change being called out as breaking in the PR body) are still open for your confirmation — happy to treat any as follow-ups.

Nice turnaround on the ratio bug — the preset-parity fix reads cleanly.

Merge upstream/main into cm-presets-and-rewire-python, resolving conflicts in
context_manager.py (keep both stash durability properties from upstream and
from_strategy/resolve_conversation_manager from this branch) and agent.py
(integrate _context_manager property with from_strategy plugin injection).
@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Approve (one diff-hygiene nit remains)

Re-checked at b8883580 ("resolve merge conflicts with upstream main"). The merge is clean — all prior fixes are intact and the gate is green:

  • 🔴 preserve_recent ratio fix preserved (base.py:158 ratio, :297 float(...)).
  • mypy clean (no issues in 19 files), ruff check clean.
  • tests/strands/_context_manager + test_agent_context_manager.py → 276 passed.

Still open (non-blocking): the unrelated formatting-only churn is still bundled — models/bedrock.py (+2 −10) plus one-line touch-ups in several unrelated test files (test_bedrock.py, test_litellm.py, test_openai.py, test_agent_delegation.py, test_hitl.py, test_stash.py, test_retrieval_tool.py, test_agent_storage.py). Splitting these out would keep the diff scoped to the context-manager change. The three parity questions from the first review also remain open for your confirmation.

No new issues from the merge — good to go once the churn is trimmed (or explicitly deferred).

@lizradway lizradway changed the title feat(context-manager): port strategy presets and agent rewire to Python feat(context-py): port strategy presets and agent rewire Sep 10, 2026
Comment thread strands-py/src/strands/_context_manager/context_manager.py Outdated
Comment thread strands-py/src/strands/_context_manager/presets.py Outdated
Comment thread strands-py/src/strands/_context_manager/presets.py
Comment thread strands-py/src/strands/_context_manager/strategies/offload/base.py Outdated
Comment thread strands-py/src/strands/_context_manager/context_manager.py
…ernalize presets, fix docstring

- Remove _removal_ratio from BaseOffloadStrategy; regular strategies now
  remove all eligible messages (preserve_recent controls what to keep),
  matching TS PR strands-agents#4256
- EmergencyTruncateStrategy gets its own _apply_per_message with a
  module-level _EMERGENCY_REMOVAL_RATIO = 0.2
- Prefix resolve_preset/resolve_strategies with underscore (internal-only)
- Rephrase resolve_conversation_manager docstring to avoid leaking
  NullConversationManager implementation detail
@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Approve

Re-reviewed at 091e8b73 ("address review feedback"). This round resolves the three open parity questions and my internalization nits — all verified locally:

Verified fixes
  • _removal_ratio removed from BaseOffloadStrategy — regular strategies now remove all eligible matches with preserve_recent controlling what's kept (base.py:_get_oldest_matches), matching TS feat(context-ts): add context strategy presets + rewire defaults to use class #4256. ✅ Answers parity Q1.
  • Emergency truncate now has its own _apply_per_message with module-level _EMERGENCY_REMOVAL_RATIO = 0.2 (truncate.py:124/163). ✅ Answers parity Q2 — emergency now deliberately diverges from the pinned-aware path.
  • Presets internalized — _resolve_preset / _resolve_strategies are underscore-prefixed and only referenced internally.
  • resolve_conversation_manager docstring no longer leaks the NullConversationManager detail.
  • Gate: ruff clean, mypy clean (no issues in 19 files), 276 passed, no stray _removal_ratio references in tests.

One thing to confirm (non-blocking): the new EmergencyTruncateStrategy._apply_per_message drops the oldest 20% by index (index > 0) and — unlike the regular offload path (is_pinned at base.py:319/355/450/457) — does not protect pinned messages. That's defensible for a true last-resort recovery (protecting pinned could make an all-pinned overflow unrecoverable), but since it means pinned messages can be dropped, a one-line docstring note stating that intent would prevent surprise.

Still open (non-blocking, third mention): the model-file formatting churn is still bundled and unrelated to this change — models/bedrock.py (+2 −10) plus matching one-liner reformats in test_bedrock.py, test_litellm.py, test_openai.py. Dropping these keeps the diff scoped. (test_agent_storage.py changes are legitimately part of the rewire — no concern there.)

Great turnaround across the rounds — the preset-parity and pinned-protection design reads cleanly and is well covered.

@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Approve

Re-reviewed at b88632c6 (docs-only). This addresses the one non-blocking item from my last pass — EmergencyTruncateStrategy._apply_per_message now documents the intent:

Pins are intentionally ignored so an all-pinned overflow is still recoverable.

That's exactly the clarification needed; the last-resort recovery behavior is now explicit rather than surprising. Verified locally: ruff clean, 260 context-manager tests pass.

The only remaining item is the unrelated model-file formatting churn (bedrock.py + matching test reformats), which stays non-blocking and is purely a diff-scoping preference. Nice work across the review rounds — this is in great shape.

@lizradway
lizradway merged commit 08ed4cf into strands-agents:main Sep 11, 2026
29 checks passed
@lizradway
lizradway deleted the cm-presets-and-rewire-python branch September 11, 2026 20:35
gautamsirdeshmukh pushed a commit to gautamsirdeshmukh/harness-sdk that referenced this pull request Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-agent Related to the agent class or general agent questions area-context Session or context related complexity/medium Touched functions have moderate cognitive complexity (11-25) enhancement New feature or request python Pull requests that update python code size/l

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants