agentstate-reducer 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.
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentstate-reducer
3
+ Version: 0.1.0
4
+ Summary: Framework-agnostic message reducer for AI agent state management. Works with LangGraph, CrewAI, and plain dicts.
5
+ Project-URL: Homepage, https://github.com/skamalj/agentstate-reducer
6
+ Project-URL: Repository, https://github.com/skamalj/agentstate-reducer
7
+ Author-email: Kamal <skamalj@github.com>
8
+ License-Expression: MIT
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
12
+ Requires-Python: >=3.10
13
+ Provides-Extra: dev
14
+ Requires-Dist: langchain-core>=0.2; extra == 'dev'
15
+ Requires-Dist: pytest>=7.0; extra == 'dev'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # agentstate-reducer
19
+
20
+ Framework-agnostic message reducer for AI agent state management. Works with **LangGraph**, **CrewAI**, and **plain dicts**.
21
+
22
+ ## What It Does
23
+
24
+ Automatically prunes message history when it exceeds a threshold, keeping conversations manageable:
25
+
26
+ - **Windowed pruning**: Trigger at `max_messages`, retain `min_messages`
27
+ - **System message preservation**: Index 0 (system prompt) is never pruned
28
+ - **ToolMessage cascade**: When an AI message is pruned, linked ToolMessages are pruned too
29
+ - **Optional summarization**: Get a callback with pruned messages to generate summaries
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install agentstate-reducer
35
+ ```
36
+
37
+ **Zero dependencies.** Works with Python 3.10+.
38
+
39
+ ## Quick Start
40
+
41
+ ```python
42
+ from agentstate_reducer import MessageReducer
43
+
44
+ reducer = MessageReducer(min_messages=10, max_messages=20)
45
+
46
+ # Works with plain dicts
47
+ result = reducer.reduce(
48
+ existing=[
49
+ {"role": "system", "content": "You are helpful"},
50
+ {"role": "human", "content": "Hello"},
51
+ {"role": "ai", "content": "Hi there!"},
52
+ ],
53
+ new=[{"role": "human", "content": "New message"}],
54
+ )
55
+ print(result.surviving) # Messages that remain
56
+ print(result.pruned) # Messages that were removed
57
+
58
+ # Works with LangChain BaseMessage objects too
59
+ from langchain_core.messages import HumanMessage, AIMessage
60
+ result = reducer.reduce(
61
+ existing=[HumanMessage(content="Hello")],
62
+ new=[AIMessage(content="Hi!")],
63
+ )
64
+ ```
65
+
66
+ ## LangGraph Integration
67
+
68
+ Use with `PrunableStateFactory` or directly as a state annotation:
69
+
70
+ ```python
71
+ from agentstate_reducer import MessageReducer
72
+ from typing_extensions import Annotated, TypedDict
73
+
74
+ reducer = MessageReducer(min_messages=10, max_messages=20)
75
+
76
+ class MyState(TypedDict):
77
+ messages: Annotated[list, reducer.as_langgraph_reducer()]
78
+ ```
79
+
80
+ ## CrewAI Integration
81
+
82
+ Use with a CosmosDB persistence backend:
83
+
84
+ ```python
85
+ from agentstate_reducer import MessageReducer
86
+
87
+ reducer = MessageReducer(min_messages=10, max_messages=20)
88
+ # Pass to your persistence class which calls reducer.reduce()
89
+ # on save_state() to prune before storing
90
+ ```
91
+
92
+ ## Summarization
93
+
94
+ ```python
95
+ from agentstate_reducer import MessageReducer, ReducerConfig
96
+
97
+ def summarize(pruned_messages):
98
+ # Call your LLM to summarize what was removed
99
+ return f"Summary of {len(pruned_messages)} messages"
100
+
101
+ config = ReducerConfig(
102
+ min_messages=10,
103
+ max_messages=20,
104
+ summarize_fn=summarize,
105
+ )
106
+ reducer = MessageReducer(config=config)
107
+ result = reducer.reduce(existing=messages)
108
+ print(result.summary) # "Summary of 5 messages"
109
+ ```
110
+
111
+ ## API
112
+
113
+ ### `MessageReducer(min_messages, max_messages, *, config)`
114
+
115
+ | Param | Default | Description |
116
+ |---|---|---|
117
+ | `min_messages` | `0` | Messages to retain after pruning |
118
+ | `max_messages` | `None` | Threshold to trigger pruning (`None` = never prune) |
119
+ | `config` | `None` | `ReducerConfig` object (overrides other params) |
120
+
121
+ ### `reducer.reduce(existing, new) -> ReducerResult`
122
+
123
+ | Field | Type | Description |
124
+ |---|---|---|
125
+ | `surviving` | `list` | Messages that remain |
126
+ | `pruned` | `list` | Messages that were removed |
127
+ | `summary` | `str \| None` | Summary from `summarize_fn` if configured |
128
+
129
+ ### `reducer.as_langgraph_reducer() -> Callable`
130
+
131
+ Returns a function with signature `(existing, new) -> list` for use with LangGraph's `Annotated[list, fn]` pattern.
@@ -0,0 +1,114 @@
1
+ # agentstate-reducer
2
+
3
+ Framework-agnostic message reducer for AI agent state management. Works with **LangGraph**, **CrewAI**, and **plain dicts**.
4
+
5
+ ## What It Does
6
+
7
+ Automatically prunes message history when it exceeds a threshold, keeping conversations manageable:
8
+
9
+ - **Windowed pruning**: Trigger at `max_messages`, retain `min_messages`
10
+ - **System message preservation**: Index 0 (system prompt) is never pruned
11
+ - **ToolMessage cascade**: When an AI message is pruned, linked ToolMessages are pruned too
12
+ - **Optional summarization**: Get a callback with pruned messages to generate summaries
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install agentstate-reducer
18
+ ```
19
+
20
+ **Zero dependencies.** Works with Python 3.10+.
21
+
22
+ ## Quick Start
23
+
24
+ ```python
25
+ from agentstate_reducer import MessageReducer
26
+
27
+ reducer = MessageReducer(min_messages=10, max_messages=20)
28
+
29
+ # Works with plain dicts
30
+ result = reducer.reduce(
31
+ existing=[
32
+ {"role": "system", "content": "You are helpful"},
33
+ {"role": "human", "content": "Hello"},
34
+ {"role": "ai", "content": "Hi there!"},
35
+ ],
36
+ new=[{"role": "human", "content": "New message"}],
37
+ )
38
+ print(result.surviving) # Messages that remain
39
+ print(result.pruned) # Messages that were removed
40
+
41
+ # Works with LangChain BaseMessage objects too
42
+ from langchain_core.messages import HumanMessage, AIMessage
43
+ result = reducer.reduce(
44
+ existing=[HumanMessage(content="Hello")],
45
+ new=[AIMessage(content="Hi!")],
46
+ )
47
+ ```
48
+
49
+ ## LangGraph Integration
50
+
51
+ Use with `PrunableStateFactory` or directly as a state annotation:
52
+
53
+ ```python
54
+ from agentstate_reducer import MessageReducer
55
+ from typing_extensions import Annotated, TypedDict
56
+
57
+ reducer = MessageReducer(min_messages=10, max_messages=20)
58
+
59
+ class MyState(TypedDict):
60
+ messages: Annotated[list, reducer.as_langgraph_reducer()]
61
+ ```
62
+
63
+ ## CrewAI Integration
64
+
65
+ Use with a CosmosDB persistence backend:
66
+
67
+ ```python
68
+ from agentstate_reducer import MessageReducer
69
+
70
+ reducer = MessageReducer(min_messages=10, max_messages=20)
71
+ # Pass to your persistence class which calls reducer.reduce()
72
+ # on save_state() to prune before storing
73
+ ```
74
+
75
+ ## Summarization
76
+
77
+ ```python
78
+ from agentstate_reducer import MessageReducer, ReducerConfig
79
+
80
+ def summarize(pruned_messages):
81
+ # Call your LLM to summarize what was removed
82
+ return f"Summary of {len(pruned_messages)} messages"
83
+
84
+ config = ReducerConfig(
85
+ min_messages=10,
86
+ max_messages=20,
87
+ summarize_fn=summarize,
88
+ )
89
+ reducer = MessageReducer(config=config)
90
+ result = reducer.reduce(existing=messages)
91
+ print(result.summary) # "Summary of 5 messages"
92
+ ```
93
+
94
+ ## API
95
+
96
+ ### `MessageReducer(min_messages, max_messages, *, config)`
97
+
98
+ | Param | Default | Description |
99
+ |---|---|---|
100
+ | `min_messages` | `0` | Messages to retain after pruning |
101
+ | `max_messages` | `None` | Threshold to trigger pruning (`None` = never prune) |
102
+ | `config` | `None` | `ReducerConfig` object (overrides other params) |
103
+
104
+ ### `reducer.reduce(existing, new) -> ReducerResult`
105
+
106
+ | Field | Type | Description |
107
+ |---|---|---|
108
+ | `surviving` | `list` | Messages that remain |
109
+ | `pruned` | `list` | Messages that were removed |
110
+ | `summary` | `str \| None` | Summary from `summarize_fn` if configured |
111
+
112
+ ### `reducer.as_langgraph_reducer() -> Callable`
113
+
114
+ Returns a function with signature `(existing, new) -> list` for use with LangGraph's `Annotated[list, fn]` pattern.
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agentstate-reducer"
7
+ version = "0.1.0"
8
+ description = "Framework-agnostic message reducer for AI agent state management. Works with LangGraph, CrewAI, and plain dicts."
9
+ authors = [{name = "Kamal", email = "skamalj@github.com"}]
10
+ readme = "README.md"
11
+ requires-python = ">=3.10"
12
+ license = "MIT"
13
+ dependencies = []
14
+
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ dev = [
23
+ "pytest>=7.0",
24
+ "langchain-core>=0.2",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/skamalj/agentstate-reducer"
29
+ Repository = "https://github.com/skamalj/agentstate-reducer"
@@ -0,0 +1,14 @@
1
+ """
2
+ agentstate-reducer: Framework-agnostic message reducer for AI agent state management.
3
+
4
+ Works with LangGraph, CrewAI, and plain dicts.
5
+ """
6
+
7
+ from .models import ReducerConfig, ReducerResult
8
+ from .reducer import MessageReducer
9
+
10
+ __all__ = [
11
+ "MessageReducer",
12
+ "ReducerConfig",
13
+ "ReducerResult",
14
+ ]
@@ -0,0 +1,128 @@
1
+ """
2
+ Adapters for extracting fields from messages in a framework-agnostic way.
3
+
4
+ Works with:
5
+ - Plain dicts: {"role": "ai", "content": "...", "id": "...", "tool_calls": [...]}
6
+ - LangChain BaseMessage subclasses: AIMessage, HumanMessage, ToolMessage, etc.
7
+ - Any object with the expected attributes (duck typing)
8
+
9
+ This module does NOT import langchain_core. It uses duck typing and class-name
10
+ inspection so the package remains zero-dependency.
11
+ """
12
+
13
+ from typing import Any, Dict, List, Optional
14
+
15
+
16
+ # ── Role mapping from LangChain class names ──
17
+
18
+ _CLASS_NAME_TO_ROLE = {
19
+ "human": "human",
20
+ "humanmessage": "human",
21
+ "ai": "ai",
22
+ "aimessage": "ai",
23
+ "tool": "tool",
24
+ "toolmessage": "tool",
25
+ "system": "system",
26
+ "systemmessage": "system",
27
+ "function": "function",
28
+ "functionmessage": "function",
29
+ "chatmessage": "chat",
30
+ }
31
+
32
+
33
+ def get_role(msg: Any) -> str:
34
+ """
35
+ Extract the role/type from a message.
36
+
37
+ Handles:
38
+ - dict with "role" key: {"role": "ai", ...}
39
+ - dict with "type" key: {"type": "ai", ...} (LangChain serialized format)
40
+ - LangChain BaseMessage: uses class name → role mapping
41
+ - Any object with a .type attribute
42
+
43
+ Returns:
44
+ Lowercase role string: "human", "ai", "tool", "system", etc.
45
+ """
46
+ if isinstance(msg, dict):
47
+ return (msg.get("role") or msg.get("type") or "").lower()
48
+
49
+ # Try class name mapping (covers all LangChain message types)
50
+ class_name = type(msg).__name__.lower()
51
+ role = _CLASS_NAME_TO_ROLE.get(class_name)
52
+ if role:
53
+ return role
54
+
55
+ # Fallback: .type attribute (LangChain BaseMessage has this)
56
+ return getattr(msg, "type", "unknown").lower()
57
+
58
+
59
+ def get_id(msg: Any) -> Optional[str]:
60
+ """
61
+ Extract the unique message ID.
62
+
63
+ Handles:
64
+ - dict: msg["id"]
65
+ - Object: msg.id
66
+ """
67
+ if isinstance(msg, dict):
68
+ return msg.get("id")
69
+ return getattr(msg, "id", None)
70
+
71
+
72
+ def get_content(msg: Any) -> str:
73
+ """
74
+ Extract message content.
75
+
76
+ Handles:
77
+ - dict: msg["content"]
78
+ - Object: msg.content
79
+ """
80
+ if isinstance(msg, dict):
81
+ return msg.get("content", "")
82
+ return getattr(msg, "content", "")
83
+
84
+
85
+ def get_tool_calls(msg: Any) -> List[Dict]:
86
+ """
87
+ Extract tool_calls from an AI message.
88
+
89
+ Handles:
90
+ - dict: msg["tool_calls"] → list of dicts
91
+ - LangChain AIMessage: msg.tool_calls → list of ToolCall objects or dicts
92
+
93
+ Returns:
94
+ List of dicts with at least {"id": ...} for each tool call.
95
+ Returns empty list if no tool_calls found.
96
+ """
97
+ if isinstance(msg, dict):
98
+ return msg.get("tool_calls", [])
99
+
100
+ raw = getattr(msg, "tool_calls", [])
101
+ if not raw:
102
+ return []
103
+
104
+ result = []
105
+ for tc in raw:
106
+ if isinstance(tc, dict):
107
+ result.append(tc)
108
+ else:
109
+ # LangChain ToolCall object → convert to dict
110
+ result.append({
111
+ "id": getattr(tc, "id", None),
112
+ "name": getattr(tc, "name", None),
113
+ "args": getattr(tc, "args", {}),
114
+ })
115
+ return result
116
+
117
+
118
+ def get_tool_call_id(msg: Any) -> Optional[str]:
119
+ """
120
+ Extract tool_call_id from a ToolMessage.
121
+
122
+ Handles:
123
+ - dict: msg["tool_call_id"]
124
+ - LangChain ToolMessage: msg.tool_call_id
125
+ """
126
+ if isinstance(msg, dict):
127
+ return msg.get("tool_call_id")
128
+ return getattr(msg, "tool_call_id", None)
@@ -0,0 +1,53 @@
1
+ """
2
+ Shared data models for the agentstate-reducer package.
3
+
4
+ These are framework-agnostic — no imports from langchain, crewai, etc.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Callable, List, Optional
9
+
10
+
11
+ @dataclass
12
+ class ReducerConfig:
13
+ """
14
+ Configuration for the MessageReducer.
15
+
16
+ Attributes:
17
+ min_messages: Number of messages to retain after pruning.
18
+ max_messages: Pruning triggers when message count exceeds this.
19
+ Set to None to disable pruning.
20
+ preserve_first: If True, index 0 is never pruned (system message).
21
+ cascade_tool_messages: If True, when an AIMessage is pruned, also prune
22
+ any ToolMessages linked to it via tool_call_id.
23
+ summarize_fn: Optional callable(pruned_messages) -> str.
24
+ Called with the list of pruned messages so you can
25
+ generate a summary (e.g., via LLM) of what was removed.
26
+ """
27
+
28
+ min_messages: int = 10
29
+ max_messages: Optional[int] = 20
30
+ preserve_first: bool = True
31
+ cascade_tool_messages: bool = True
32
+ summarize_fn: Optional[Callable[[List[Any]], str]] = None
33
+
34
+
35
+ @dataclass
36
+ class ReducerResult:
37
+ """
38
+ Output of a reduce operation.
39
+
40
+ Both downstream consumers need to know what survived AND what was pruned:
41
+ - langgraph-checkpoint-cosmosdb: stores `surviving`, may log `pruned`
42
+ - crewai-persistence-cosmosdb: stores `surviving`, may call summarize on `pruned`
43
+
44
+ Attributes:
45
+ surviving: Messages that remain after pruning.
46
+ pruned: Messages that were removed.
47
+ summary: If a summarize_fn was configured and pruning occurred,
48
+ this contains the summary string.
49
+ """
50
+
51
+ surviving: List[Any] = field(default_factory=list)
52
+ pruned: List[Any] = field(default_factory=list)
53
+ summary: Optional[str] = None
@@ -0,0 +1,208 @@
1
+ """
2
+ Core message reducer — framework-agnostic.
3
+
4
+ Works with:
5
+ - Plain dicts: {"role": "ai", "content": "...", "id": "..."}
6
+ - LangChain types: AIMessage, HumanMessage, ToolMessage, SystemMessage
7
+ - Any object with role/id/tool_calls attributes (duck typing)
8
+
9
+ Ported from langgraph-reducer's Reducer class with these improvements:
10
+ - Works with both LangChain types and plain dicts
11
+ - Returns ReducerResult(surviving, pruned, summary) so callers decide
12
+ what to do with pruned messages
13
+ - Configurable via ReducerConfig dataclass
14
+ - Provides as_langgraph_reducer() bridge for Annotated[list, fn] usage
15
+
16
+ Preserves original behaviors:
17
+ 1. Skip index 0 (system message) during pruning
18
+ 2. When pruning an AIMessage, also prune linked ToolMessages (cascade)
19
+ 3. Windowed pruning: prune when len > max_messages, keep min_messages
20
+ """
21
+
22
+ import logging
23
+ from typing import Any, List, Optional, Set
24
+
25
+ from .adapters import get_role, get_tool_call_id, get_tool_calls
26
+ from .models import ReducerConfig, ReducerResult
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class MessageReducer:
32
+ """
33
+ Framework-agnostic message reducer.
34
+
35
+ Concatenates existing + new messages, then prunes oldest messages
36
+ when the total exceeds ``max_messages``, retaining ``min_messages``.
37
+
38
+ Pruning rules:
39
+ - Index 0 is preserved (configurable via ``preserve_first``).
40
+ - Only ``ai`` and ``human`` messages are pruned.
41
+ - When an ``ai`` message is pruned, any ``tool`` messages linked to
42
+ it (by ``tool_call_id``) are also pruned (cascade).
43
+
44
+ Args:
45
+ min_messages: Number of messages to keep after pruning.
46
+ max_messages: Threshold to trigger pruning. ``None`` disables pruning.
47
+ config: Optional ``ReducerConfig`` (overrides min/max params).
48
+
49
+ Example with plain dicts::
50
+
51
+ reducer = MessageReducer(min_messages=5, max_messages=10)
52
+ result = reducer.reduce(
53
+ existing=[{"role": "system", "content": "You are helpful"}],
54
+ new=[{"role": "human", "content": "Hello"}],
55
+ )
56
+ # result.surviving = [...]
57
+ # result.pruned = [...]
58
+
59
+ Example with LangChain messages::
60
+
61
+ from langchain_core.messages import HumanMessage, AIMessage
62
+ reducer = MessageReducer(min_messages=5, max_messages=10)
63
+ result = reducer.reduce(
64
+ existing=[SystemMessage(content="...")],
65
+ new=[HumanMessage(content="Hello")],
66
+ )
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ min_messages: int = 0,
72
+ max_messages: Optional[int] = None,
73
+ *,
74
+ config: Optional[ReducerConfig] = None,
75
+ ):
76
+ if config is not None:
77
+ self.config = config
78
+ else:
79
+ self.config = ReducerConfig(
80
+ min_messages=min_messages,
81
+ max_messages=max_messages,
82
+ )
83
+
84
+ @property
85
+ def min_messages(self) -> int:
86
+ return self.config.min_messages
87
+
88
+ @property
89
+ def max_messages(self) -> Optional[int]:
90
+ return self.config.max_messages
91
+
92
+ def reduce(
93
+ self,
94
+ existing: Optional[List[Any]] = None,
95
+ new: Optional[List[Any]] = None,
96
+ ) -> ReducerResult:
97
+ """
98
+ Concatenate existing + new messages, then prune if over threshold.
99
+
100
+ Args:
101
+ existing: Current message list. Defaults to empty list.
102
+ new: New messages to append. Defaults to empty list.
103
+
104
+ Returns:
105
+ ``ReducerResult`` with surviving messages, pruned messages,
106
+ and optional summary.
107
+ """
108
+ if existing is None:
109
+ existing = []
110
+ if new is None:
111
+ new = []
112
+
113
+ messages = list(existing) + list(new)
114
+ max_msgs = self.config.max_messages
115
+
116
+ # No pruning needed
117
+ if max_msgs is None or len(messages) <= max_msgs:
118
+ return ReducerResult(surviving=messages, pruned=[])
119
+
120
+ # ── Identify indices to prune ──
121
+ to_delete: Set[int] = set()
122
+ excess_count = len(messages) - self.config.min_messages
123
+ start_idx = 1 if self.config.preserve_first else 0
124
+
125
+ # Iterate over the pruning window: from start_idx to excess_count
126
+ for i, msg in enumerate(messages[start_idx:excess_count], start=start_idx):
127
+ role = get_role(msg)
128
+
129
+ if role in ("ai", "human"):
130
+ to_delete.add(i)
131
+
132
+ # ToolMessage cascade: when pruning an AIMessage, also prune
133
+ # any ToolMessages that reference its tool_call IDs
134
+ if role == "ai" and self.config.cascade_tool_messages:
135
+ for tc in get_tool_calls(messages[i]):
136
+ tc_id = (
137
+ tc.get("id")
138
+ if isinstance(tc, dict)
139
+ else getattr(tc, "id", None)
140
+ )
141
+ if tc_id is None:
142
+ continue
143
+ for j in range(i + 1, len(messages)):
144
+ if (
145
+ get_role(messages[j]) == "tool"
146
+ and get_tool_call_id(messages[j]) == tc_id
147
+ ):
148
+ to_delete.add(j)
149
+
150
+ # ── Split into surviving and pruned ──
151
+ pruned = [messages[i] for i in sorted(to_delete)]
152
+ surviving = [msg for i, msg in enumerate(messages) if i not in to_delete]
153
+
154
+ logger.debug(
155
+ "Reduced messages from %d to %d (pruned %d)",
156
+ len(messages),
157
+ len(surviving),
158
+ len(pruned),
159
+ )
160
+
161
+ # ── Optional summarization ──
162
+ summary = None
163
+ if pruned and self.config.summarize_fn is not None:
164
+ try:
165
+ summary = self.config.summarize_fn(pruned)
166
+ except Exception as exc:
167
+ logger.warning("Summarization failed: %s", exc)
168
+
169
+ return ReducerResult(
170
+ surviving=surviving,
171
+ pruned=pruned,
172
+ summary=summary,
173
+ )
174
+
175
+ def as_langgraph_reducer(self):
176
+ """
177
+ Return a function compatible with LangGraph's ``Annotated[list, fn]`` pattern.
178
+
179
+ The returned function has the signature ``(existing, new) -> merged_list``
180
+ which is what LangGraph expects for a custom reducer annotated on a
181
+ state field.
182
+
183
+ Usage::
184
+
185
+ reducer = MessageReducer(min_messages=10, max_messages=20)
186
+
187
+ class MyState(TypedDict):
188
+ messages: Annotated[list, reducer.as_langgraph_reducer()]
189
+
190
+ This is the bridge that lets ``PrunableStateFactory`` keep working
191
+ unchanged when migrating from ``langgraph-reducer``.
192
+ """
193
+
194
+ def _reduce(existing=None, new=None):
195
+ if existing is None:
196
+ existing = []
197
+ result = self.reduce(existing, new)
198
+ return result.surviving
199
+
200
+ return _reduce
201
+
202
+ def __repr__(self) -> str:
203
+ return (
204
+ f"MessageReducer(min_messages={self.config.min_messages}, "
205
+ f"max_messages={self.config.max_messages}, "
206
+ f"preserve_first={self.config.preserve_first}, "
207
+ f"cascade_tool_messages={self.config.cascade_tool_messages})"
208
+ )