Python: Add chat history reducer support to GroupChatOrchestration#13933
Python: Add chat history reducer support to GroupChatOrchestration#13933lawcontinue wants to merge 3 commits intomicrosoft:mainfrom
Conversation
Clarify that context.terminate = True terminates the auto function calling loop, not just skips a function call.
Allow GroupChatOrchestration to accept an optional ChatHistoryReducer that automatically summarizes or truncates the internal chat history after each agent response. This fixes microsoft#12303 where ChatHistorySummarizationReducer was accepted by the API but never triggered because GroupChatOrchestration manages its own ChatHistory internally, bypassing the reducer path. Changes: - Add chat_history_reducer param to GroupChatOrchestration.__init__ - Add _maybe_reduce_chat_history to GroupChatManagerActor - Call reducer after each agent response in _handle_response_message - Fully backward compatible: default is None (no reducer)
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 89%
✓ Correctness
This PR adds optional chat history reduction support to the GroupChat orchestration. The implementation correctly syncs messages from the internal ChatHistory to the reducer, calls reduce(), and updates the history when reduction occurs. The import, parameter threading through GroupChat → _GroupChatManagerActor, and the placement of the reduction call (after adding a message but before determining next action) are all correct. The README is documentation-only and accurate. No correctness issues found.
✓ Security Reliability
The changes add an optional ChatHistoryReducer to the group chat orchestration, called after each agent response. The README addition is documentation-only. The code change is generally sound—both existing reducer implementations create new list objects during reduction, so the message-list aliasing pattern works correctly. One reliability concern: the
_maybe_reduce_chat_history()call sits outside the ``@ActorBase.exception_handlerdecorator that protects `_determine_state_and_take_action`, so a reducer exception (e.g., a failed summarization network call when `fail_on_error=True`) would bypass the `exception_callback` and prevent the group chat from progressing.
✓ Test Coverage
The PR adds a
chat_history_reducerparameter and_maybe_reduce_chat_history()method to the group chat orchestration, but includes zero tests for this new behavior. The existing test filepython/tests/unit/agents/orchestration/test_group_chat.pycontains no tests exercising the reducer integration — not for the constructor parameter, not for the reduction logic after agent responses, and not for edge cases like a reducer that returns None. The README addition is documentation-only and fine.
✗ Design Approach
The new history-reduction hook is a sensible feature, but it is wired in a way that breaks the orchestration’s existing per-invocation isolation.
GroupChatOrchestrationnow keeps a single mutableChatHistoryReduceron the orchestration instance and passes that same object into each manager actor, even thoughOrchestrationBase.invoke()creates a fresh internal topic per run specifically to isolate concurrent/repeated invocations. Because reducers carry mutablemessagesstate and the manager mutates that state before every reduction, one orchestration instance can now leak summarized/truncated history across runs.
Flagged Issues
-
python/semantic_kernel/agents/orchestration/group_chat.pyshares one mutableChatHistoryReducerinstance across all invocations of aGroupChatOrchestration, conflicting with per-run isolation inorchestration_base.py:214-223. Since_maybe_reduce_chat_history()mutates reducer state by assigningself._chat_history_reducer.messagesbefore each reduction, concurrent or repeated invocations will leak summarized/truncated history across runs. The reducer should be deep-copied per manager actor/run.
Suggestions
- Wrap the
_maybe_reduce_chat_history()call (or its body) with exception handling so that reducer failures are reported viaexception_callbackand do not stall the group chat.ChatHistorySummarizationReducer.reduce()can raiseChatHistoryReducerException(chat_history_summarization_reducer.py:178), and that exception would currently propagate out of_handle_response_messagewithout invokingexception_callbackor reaching_determine_state_and_take_action.
Automated review by lawcontinue's agents
There was a problem hiding this comment.
Pull request overview
Adds optional chat history reduction support to GroupChatOrchestration/GroupChatManagerActor so a configured ChatHistoryReducer can be invoked to keep history bounded, and introduces a new README for Python filtering samples.
Changes:
- Add optional
chat_history_reducerparameter toGroupChatOrchestrationand propagate it intoGroupChatManagerActor. - Invoke reducer logic after each agent response via a new
_maybe_reduce_chat_history()helper. - Add
python/samples/concepts/filtering/README.mddocumenting available filter sample scripts and registration patterns.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| python/semantic_kernel/agents/orchestration/group_chat.py | Adds reducer plumb-through + reduction call after responses to bound internal manager chat history. |
| python/samples/concepts/filtering/README.md | New documentation describing Python filter types, sample files, and registration/signature patterns. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ) | ||
| ) | ||
| self._chat_history.add_message(message.body) | ||
| await self._maybe_reduce_chat_history() | ||
|
|
||
| await self._determine_state_and_take_action(ctx.cancellation_token) |
There was a problem hiding this comment.
The reducer is currently applied only to GroupChatManagerActor._chat_history, but agent invocations build prompts from each agent actor’s AgentThread (e.g., ChatCompletionAgent reconstructs the request history from thread.get_messages()), not from the manager’s _chat_history. As a result, reducing the manager history won’t reduce what gets sent to the model, so this likely won’t address #12303. To make the reducer effective for LLM calls, it needs to be applied to the per-agent thread/chat history path (e.g., construct ChatHistoryAgentThread with a ChatHistoryReducer, or call a thread/history reduction step for each agent actor before invoking the model).
| async def _maybe_reduce_chat_history(self) -> None: | ||
| """Reduce the chat history if a reducer is configured and the threshold is exceeded.""" | ||
| if self._chat_history_reducer is not None: | ||
| # Sync messages from the internal history to the reducer | ||
| self._chat_history_reducer.messages = self._chat_history.messages |
There was a problem hiding this comment.
New behavior is introduced by calling _maybe_reduce_chat_history() after each agent response, but there are existing unit tests for GroupChatOrchestration/RoundRobinGroupChatManager and none appear to cover reducer integration. Please add tests that verify (1) the reducer is invoked when provided and (2) the manager/agent history used for subsequent turns is actually reduced (and that seeded system/developer messages on the reducer, if supported, are preserved).
| async def _maybe_reduce_chat_history(self) -> None: | |
| """Reduce the chat history if a reducer is configured and the threshold is exceeded.""" | |
| if self._chat_history_reducer is not None: | |
| # Sync messages from the internal history to the reducer | |
| self._chat_history_reducer.messages = self._chat_history.messages | |
| def _get_seeded_reducer_messages(self) -> list[ChatMessageContent]: | |
| """Return reducer-seeded system/developer messages that should be preserved during sync.""" | |
| if self._chat_history_reducer is None: | |
| return [] | |
| seeded_messages: list[ChatMessageContent] = [] | |
| for message in self._chat_history_reducer.messages: | |
| if message.role in (AuthorRole.SYSTEM, AuthorRole.DEVELOPER): | |
| seeded_messages.append(message) | |
| continue | |
| break | |
| return seeded_messages | |
| async def _maybe_reduce_chat_history(self) -> None: | |
| """Reduce the chat history if a reducer is configured and the threshold is exceeded.""" | |
| if self._chat_history_reducer is not None: | |
| history_messages = list(self._chat_history.messages) | |
| reducer_messages = list(self._get_seeded_reducer_messages()) | |
| preserved_seeded_messages: list[ChatMessageContent] = [] | |
| for index, seeded_message in enumerate(reducer_messages): | |
| if index < len(history_messages) and history_messages[index] == seeded_message: | |
| continue | |
| preserved_seeded_messages.append(seeded_message) | |
| # Sync messages from the internal history to the reducer while preserving | |
| # any seeded system/developer messages the reducer was initialized with. | |
| self._chat_history_reducer.messages = [*preserved_seeded_messages, *history_messages] |
| | Filter | Decorator | Purpose | | ||
| |--------|-----------|---------| | ||
| | **Prompt Rendering** | `@kernel.filter(FilterTypes.PROMPT_RENDERING)` | Intercept before/after prompt is rendered | | ||
| | **Function Invocation** | `@kernel.filter(FilterTypes.FUNCTION_INVOCATION)` | Intercept before/after any function call | | ||
| | **Auto Function Invoke** | `@kernel.filter(FilterTypes.AUTO_FUNCTION_INVOCATION)` | Control automatic tool calls | | ||
|
|
There was a problem hiding this comment.
The markdown table in “Filter Types” uses double pipes (||) at the start/end of rows, which renders incorrectly in many Markdown parsers (extra empty columns). Use standard single-pipe table syntax (e.g., | Filter | Decorator | Purpose |) for consistent rendering on GitHub.
| if self._chat_history_reducer is not None: | ||
| # Sync messages from the internal history to the reducer | ||
| self._chat_history_reducer.messages = self._chat_history.messages | ||
| result = await self._chat_history_reducer.reduce() |
There was a problem hiding this comment.
_maybe_reduce_chat_history overwrites self._chat_history_reducer.messages with the manager’s internal history (self._chat_history.messages). This will discard any messages already present on the reducer instance (e.g., user-configured system/developer prompts added via add_system_message, or pre-seeded summaries), which changes reducer behavior and can make summarization prompts silently disappear. Consider seeding self._chat_history from chat_history_reducer.messages once in __init__ (deep-copy), and then keep both in sync without clobbering reducer state, or merge/preserve reducer-prefixed system/developer messages when syncing.
- Copy reducer pre-seeded messages into _chat_history at init (fixes microsoft#4) - Use list() copy to avoid mutating reducer state - Update docstring to clarify scope: manager-level reduce only - AgentThread-level reduce is a separate enhancement
Response to Copilot reviewThanks for the thorough review. Addressing each point: 1. Reducer scope (manager vs agent thread) — Good catch. This PR reduces the manager's 2. Tests — Will add in a follow-up commit. Agree the reducer invocation should be covered. 3. README markdown — Not part of this PR, skipping. 4. Reducer message overwrite — Fixed in the latest push. Now copies reducer's pre-seeded messages into |
Description
GroupChatOrchestrationmanages its own internalChatHistoryand passes messages to agents viaAgentThread. This means aChatHistorySummarizationReducerconfigured on the agent is never triggered — the messages never flow through the reducer path.Fixes #12303.
Changes
Add an optional
chat_history_reducerparameter toGroupChatOrchestration. When provided, the reducer is called after each agent response to keep the internal history within bounds.What changed in
group_chat.pyChatHistoryReducerGroupChatManagerActor.__init__chat_history_reducer_handle_response_message_maybe_reduce_chat_history()after adding each message_maybe_reduce_chat_historyreduce(), syncs backGroupChatOrchestration.__init__chat_history_reducerparam_register_managerGroupChatManagerActorFully backward compatible — default is
None(no reducer), matching existing behavior.Why this approach
The
_chat_historyinGroupChatManagerActoris a plainChatHistory. Rather than replacing it with aChatHistoryReducersubclass (which would require the reducer to own the history lifecycle), this PR keeps the internal history as-is and calls the reducer explicitly after each agent response. This avoids changing the message ownership model and works with both summarization and truncation reducers.Checklist