Skip to content

refactor(memory): represent passed-in config types as TypedDicts - #2824

Merged
pgrayy merged 1 commit into
strands-agents:mainfrom
pgrayy:refactor/memory-config-typeddicts
Jun 16, 2026
Merged

refactor(memory): represent passed-in config types as TypedDicts#2824
pgrayy merged 1 commit into
strands-agents:mainfrom
pgrayy:refactor/memory-config-typeddicts

Conversation

@pgrayy

@pgrayy pgrayy commented Jun 16, 2026

Copy link
Copy Markdown
Member

Description

The memory config/option types were dataclasses, but they are passed-in option bags, not value objects with behavior. This converts them to TypedDict, the more idiomatic representation for "a bag of optional fields a caller provides," and the one that matches how config bags are constructed elsewhere in the SDK. It also lets callers pass plain dict literals, which mirrors how these configs are written in the TypeScript SDK (object literals).

Converted to TypedDict (total=False): SearchOptions, MemorySearchOptions, MemoryAddOptions, MemoryToolConfig, MemoryAddToolConfig, MemoryInjectionConfig, MemoryManagerConfig, plus InjectionConfig and MemoryStoreConfig. MemoryManagerConfig marks stores as Required.

Left unchanged by design:

  • MemoryStore stays a Protocol (it carries methods) and re-declares the config fields as attributes, since a Protocol cannot extend a TypedDict.
  • MemoryEntry and the *Context types stay dataclasses, they are returned/callback value objects, not passed-in config.

Public API impact

Construction is a superset of before, no breakage:

# Still valid (call form):
MemoryManager(stores=[s], search_tool_config=MemoryToolConfig(name="recall"))

# Now also valid and type-checked (dict literal):
MemoryManager(stores=[s], search_tool_config={"name": "recall"})

Internally, the manager's discrimination over the Config | bool unions switches from isinstance(<config-class>) (not usable on a TypedDict) to isinstance(x, dict) for selecting the config arm and is False for the disabled-sentinel guards. Attribute reads become .get(...). All previously-defaulted fields are preserved: wait_for_writes defaults at its read site, and the MemoryManagerConfig fields default via MemoryManager.__init__ (an omitted TypedDict key is simply absent, so the constructor's own defaults apply).

Prior art in the SDK

Accepting a TypedDict config as a single parameter is an established pattern:

  • ConversationManager / proactive_compression (conversation_manager.py) — bool | ProactiveCompressionConfig | None, the closest precedent to this PR. It discriminates the union exactly as this PR does: if proactive_compression is True: ... elif isinstance(proactive_compression, dict): threshold = proactive_compression.get("compression_threshold", DEFAULT). Its docstring shows both proactive_compression=True and proactive_compression={"compression_threshold": 0.8}.
  • MCPClient / tasks_config: TasksConfig | None (mcp_client.py) — a config TypedDict passed as one parameter.
  • OpenAIResponsesModel / bedrock_mantle_config: BedrockMantleConfig | None (openai_responses.py) — passed as one param and read directly (_resolve_region(config: BedrockMantleConfig)).
  • bidi models / default_audio: AudioConfig (nova_sonic.py, gemini_live.py, openai_realtime.py) — a TypedDict config with an inline dict-literal default.
  • CitationsContentBlock / citations: CitationsConfig | None (types/media.py).

So this PR's MemoryToolConfig | bool / MemoryInjectionConfig | bool parameters, the isinstance(x, dict) / is False discrimination, and the .get(key, default) reads all match the existing proactive_compression precedent.

Related Issues

Documentation PR

Not applicable; no behavior change, construction is a superset of before.

Type of Change

Other (please describe): internal refactor of config type representation (no behavior change; construction is backward compatible)

Testing

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

  • I ran hatch run prepare

Ran the relevant checks (full prepare includes the multi-version test matrix):

  • hatch run test-lint → ruff All checks passed; mypy ./src no issues in 188 source files
  • hatch run test-format → all files formatted
  • Full unit suite (hatch test) → 3602 passed. No test changes were needed, call-form construction stays valid for TypedDicts.

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.

The memory config/options types (SearchOptions, MemorySearchOptions,
MemoryAddOptions, MemoryToolConfig, MemoryAddToolConfig, MemoryInjectionConfig,
MemoryManagerConfig) and InjectionConfig and MemoryStoreConfig were dataclasses.
They are passed-in option bags, not value objects with behavior, so a TypedDict
is the more idiomatic representation and lets callers pass plain dict literals
(matching how the same configs are constructed elsewhere via Unpack TypedDicts,
e.g. the model configs).

- All seven memory configs + InjectionConfig + MemoryStoreConfig become TypedDicts
  (total=False; MemoryManagerConfig marks `stores` Required). Construction via the
  call form (MemoryToolConfig(...)) keeps working, and dict literals now type-check.
- MemoryStore stays a Protocol (it carries methods) and re-declares the config
  fields as attributes, since a Protocol cannot extend a TypedDict.
- MemoryEntry and the *Context types stay dataclasses (returned/callback value
  objects, not passed-in config).
- Manager discrimination over the `Config | bool` unions switches from
  isinstance(<config-class>) (unusable on a TypedDict) to isinstance(x, dict) for
  selecting the config arm, and `is False` for the disabled-sentinel guards.
- Attribute reads become .get(); previously-defaulted fields are preserved
  (wait_for_writes defaults at the read site; MemoryManagerConfig fields default
  via MemoryManager.__init__ since omitted keys are simply absent).

No test changes required (call-form construction is still valid for TypedDicts).
Full suite green: ruff + mypy (188 files), 3602 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size/m python Pull requests that update python code area-devx Developer experience improvements chore Maintenance tasks, dependency updates, CI changes, refactoring with no user-facing impact strands-running labels Jun 16, 2026
@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.55072% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
strands-py/src/strands/agent/agent.py 50.00% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Comment thread strands-py/src/strands/memory/types.py
Comment thread strands-py/src/strands/memory/types.py
@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Comment

Clean, well-motivated refactor: converting these passed-in option bags from dataclasses to total=False TypedDicts is idiomatic and consistent with the Unpack[...Config] pattern used elsewhere in the SDK. The isinstance(x, dict) discrimination, Literal[False] sentinels, and .get(...) reads with preserved defaults all look correct, and construction stays a backward-compatible superset. My main reservation is test coverage for the new capability.

Review themes
  • Testing: The headline feature — passing plain dict literals — is not exercised by any test; every case still uses the call form, which was already valid. The dict-literal arms and MemoryManager(**memory_manager) path should be covered directly.
  • Consistency: ExtractionConfig is an equivalent passed-in option bag but stays a dataclass and is missing from both lists in the PR description. Worth either converting or explicitly documenting as a deliberate exception.
  • Maintainability: The MemoryStore Protocol must duplicate MemoryStoreConfig fields (Protocol can't extend TypedDict); acknowledged in-code, low risk, optional sync guard.

Nicely scoped change with excellent docstrings — addressing the test gap would make it solid.

@pgrayy
pgrayy merged commit dc43bec into strands-agents:main Jun 16, 2026
33 checks passed
pgrayy added a commit to pgrayy/strands-harness-sdk that referenced this pull request Jun 16, 2026
Follow-up to strands-agents#2824. ExtractionConfig is a passed-in option bag (trigger,
extractor, filter, all optional) provided via the `bool | ExtractionConfig`
shorthand, the same kind of config that PR converted, so this makes it a
TypedDict too for consistency and to let callers pass plain dict literals.

- ExtractionConfig becomes a TypedDict (total=False). Its sibling types stay as
  they are: ExtractionResult / ExtractorContext / ExtractionTriggerContext /
  MemoryMessageFilter are value objects, Extractor / ExtractionTrigger are a
  Protocol / ABC, and _ResolvedExtractionConfig is the internal resolved output
  (a value object) — none are passed-in config bags.
- _resolve_extraction_config reads the fields via .get().
- Fix a falsiness regression the conversion exposes: the disabled check was
  `if not extraction`, but an empty ExtractionConfig is now `{}` (falsy), which
  would wrongly disable extraction. Check the disabled sentinels explicitly
  (`extraction is None or extraction is False`) so an empty config stays enabled.

Construction via the call form (ExtractionConfig(trigger=...)) still works, so
no test changes were needed. Full suite green: ruff + mypy (194 files), 3668 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-devx Developer experience improvements chore Maintenance tasks, dependency updates, CI changes, refactoring with no user-facing impact python Pull requests that update python code size/m

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants