Python: Fix executor_completed event with non-copyable raw_representation in mixed workflows#4493
Python: Fix executor_completed event with non-copyable raw_representation in mixed workflows#4493moonbox3 wants to merge 5 commits intomicrosoft:mainfrom
executor_completed event with non-copyable raw_representation in mixed workflows#4493Conversation
moonbox3
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 81%
✗ Correctness
The diff adds deepcopy methods to SerializationMixin and Content to preserve raw_representation by shallow reference. Two correctness concerns: (1) Content.deepcopy in _types.py uses bare
deepcopy(v, memo)but the diff shows no import ofdeepcopyfrom thecopymodule — if it's not already imported in that file, this will raise a NameError at runtime; (2) SerializationMixin.deepcopy hardcodes the check for"raw_representation"rather than consultingcls.DEFAULT_EXCLUDE, which contradicts the test name claiming it preserves 'DEFAULT_EXCLUDE fields by reference' and means any future non-copyable field added to DEFAULT_EXCLUDE will still be deep-copied and fail.
✓ Security Reliability
The diff adds
__deepcopy__implementations toSerializationMixinandContentthat shallow-copyraw_representationwhile deep-copying all other attributes. The pattern is standard and correctly uses thememodict for cycle prevention. No injection risks, secrets, resource leaks, or unsafe deserialization found. The shared mutable reference forraw_representationis intentional and well-documented. One minor reliability observation: the field name is hardcoded rather than driven by class configuration, but this is a deliberate scoping choice, not a bug.
✗ Test Coverage
The deepcopy tests are generally well-structured, covering multiple types (Content, Message, AgentResponse, etc.) and an integration test through the workflow executor. However, there is one meaningless assertion (tautology) that should be fixed, and there are notable gaps: no test verifies that the memo dict correctly handles circular references (which the implementation explicitly supports), no test verifies subclass deepcopy behavior (which the
type(self)pattern explicitly supports), and the test name 'preserves_excluded_fields_by_reference' is misleading since the implementation hardcodes 'raw_representation' rather than consulting DEFAULT_EXCLUDE.
✗ Design Approach
The
__deepcopy__implementations in bothSerializationMixinandContenthardcode the field name"raw_representation"instead of leveraging the existingDEFAULT_EXCLUDEclass variable (or a new dedicated class variable). This is an overly narrow fix: if a subclass adds another non-deep-copyable field, the hardcoded check will miss it anddeepcopywill crash at runtime. The test is even namedtest_deepcopy_preserves_excluded_fields_by_reference(plural), implying the intent is to handle all such fields generically, but the implementation only handles one. The correct approach is to consult the class-level set rather than matching a single string literal.
Flagged Issues
- Content.deepcopy in _types.py uses bare
deepcopy(v, memo)but nofrom copy import deepcopyimport is added in this diff. If it is not already imported in the file, this will be a NameError at runtime. - Line 451 in test_serializable_mixin.py has a tautological assertion:
assert cloned.value is not obj.value or cloned.value == obj.valueis always True regardless of behavior (P ∨ ¬P). It provides no test coverage. - SerializationMixin.deepcopy hardcodes
if k == "raw_representation"instead of using the already-existingDEFAULT_EXCLUDEclass variable (or a newSHALLOW_COPY_FIELDSset). Any subclass with a different non-copyable excluded field will still crash on deepcopy. The mechanism to declare these fields already exists — use it. - Content.deepcopy duplicates the same hardcoded logic. If Content inherits from SerializationMixin, the override is unnecessary. If it doesn't, it should have its own class-level set (e.g.,
_SHALLOW_COPY_FIELDS) rather than a hardcoded string.
Suggestions
- SerializationMixin.deepcopy hardcodes
if k == "raw_representation"instead of checkingif k in cls.DEFAULT_EXCLUDE. The test is namedtest_deepcopy_preserves_excluded_fields_by_referenceimplying it should generalize to all DEFAULT_EXCLUDE fields. Consider usingif k in cls.DEFAULT_EXCLUDEfor consistency and future-proofing. - The test assertion
assert cloned.value is not obj.value or cloned.value == obj.valueon line 451 of test_serializable_mixin.py is a tautology (always True for any values) and does not actually verify that the string was independently copied. Consider removing it or replacing with a meaningful assertion on a mutable field. - Consider checking
if k in type(self).DEFAULT_EXCLUDEinstead of hardcodingif k == "raw_representation"inSerializationMixin.__deepcopy__. This would make the deepcopy behavior consistent with the class's existing exclusion configuration, and would correctly handle subclasses that add other non-copyable fields toDEFAULT_EXCLUDE. Currently, the test nametest_deepcopy_preserves_excluded_fields_by_referenceimplies generic DEFAULT_EXCLUDE support, but onlyraw_representationis actually handled. - If
Contentinherits fromSerializationMixin, the duplicated__deepcopy__inContentcould be removed in favor of the mixin's implementation, reducing maintenance surface. - Add a test with a field in DEFAULT_EXCLUDE other than 'raw_representation' to verify whether the deepcopy behavior is truly generic or hardcoded. Currently
test_deepcopy_preserves_excluded_fields_by_referencesuggests generality but only tests the hardcoded key. - Consider adding a test for circular/self-referencing objects to verify the memo dict handling works correctly (e.g., an object whose attribute refers back to itself).
- Consider adding a test for subclass deepcopy to verify the
cls = type(self)pattern preserves the correct type. - If the semantic of DEFAULT_EXCLUDE is too broad (it governs serialization, not copying), introduce a dedicated class variable like
SHALLOW_COPY_FIELDS: ClassVar[set[str]]and have__deepcopy__check against that. This keeps the concerns separate while still being extensible.
Automated review by moonbox3's agents
There was a problem hiding this comment.
Pull request overview
Fixes workflow event snapshotting when raw_representation contains non-deepcopy-able SDK objects by making deepcopy preserve raw_representation as a shallow reference while deep-copying all other fields. This prevents executor_completed events from losing response payloads in mixed-LLM workflows.
Changes:
- Added custom
__deepcopy__implementations toSerializationMixinandContentto shallow-copyraw_representation. - Added regression coverage for deepcopy behavior across core response/message/content types.
- Added a workflow regression test validating
executor_completedincludes a validAgentResponsewhenraw_representationis non-copyable.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| python/packages/core/agent_framework/_serialization.py | Adds SerializationMixin.__deepcopy__ to preserve raw_representation by reference. |
| python/packages/core/agent_framework/_types.py | Adds Content.__deepcopy__ with the same shallow raw_representation behavior. |
| python/packages/core/tests/core/test_types.py | Adds unit tests validating deepcopy preserves raw_representation across key core types. |
| python/packages/core/tests/core/test_serializable_mixin.py | Adds mixin-level tests for deepcopy behavior with excluded/non-excluded fields. |
| python/packages/core/tests/workflow/test_agent_executor.py | Adds workflow regression test covering executor_completed event data with non-copyable raw_representation. |
You can also share your feedback on Copilot code review. Take the survey.
moonbox3
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 88%
✓ Correctness
The diff generalizes the
__deepcopy__logic from a single hardcoded field name to a set-based lookup, applied consistently in bothSerializationMixin(usingDEFAULT_EXCLUDE) andContent(using_SHALLOW_COPY_FIELDS). The changes are logically correct: set membership checks are O(1), the iteration overself.__dict__is unchanged, and the test properly validates that multiple excluded fields are shallow-copied while other fields are deep-copied. The updated test is actually stricter than the old one, which contained a tautological assertion (cloned.value is not obj.value or cloned.value == obj.valueis always True).
✓ Security Reliability
This diff generalizes two
__deepcopy__implementations to use a class-level set (DEFAULT_EXCLUDE/_SHALLOW_COPY_FIELDS) instead of a hardcoded string comparison, allowing multiple fields to be shallow-copied. The change is a straightforward, low-risk refactoring. No injection risks, unsafe deserialization, secrets, resource leaks, or unhandled failure modes were identified. The shallow-copy semantics are well-documented and the test coverage is updated appropriately.
✓ Test Coverage
The SerializationMixin.deepcopy change from a hardcoded field name to a set-based lookup is well-tested: the updated test now verifies multiple excluded fields are shallow-copied and normal fields are deep-copied with meaningful assertions. However, the parallel change in Content.deepcopy (switching from hardcoded 'raw_representation' to _SHALLOW_COPY_FIELDS) has no corresponding test coverage in this diff, leaving that code path unverified.
✗ Design Approach
The diff generalizes hardcoded
raw_representationchecks to use configurable field sets for shallow-copying during__deepcopy__. TheContentclass does this correctly by introducing a purpose-specific_SHALLOW_COPY_FIELDSset. However,SerializationMixinreusesDEFAULT_EXCLUDE(a serialization concern) to also drive deep-copy behavior, conflating two orthogonal concepts: 'fields to omit from serialization' and 'fields unsafe to deep-copy'. A field could legitimately be excluded from serialization while still being safe (and necessary) to deep-copy, and vice versa. This coupling will silently produce shallow references for any future field added toDEFAULT_EXCLUDEfor serialization reasons alone, risking shared-mutation bugs.
Flagged Issues
- In
SerializationMixin.__deepcopy__, reusingDEFAULT_EXCLUDEto decide which fields get shallow-copied conflates 'not serialized' with 'not safe to deep-copy'. These are independent concerns. A field added toDEFAULT_EXCLUDEpurely because it shouldn't appear in serialized output will now also be silently shared between the original and its deep-copy, which can cause hard-to-diagnose mutation bugs.Contentalready models this correctly with a dedicated_SHALLOW_COPY_FIELDSset;SerializationMixinshould do the same (e.g., introduce a_SHALLOW_COPY_FIELDSclass var, defaulting to{'raw_representation'}, and use that in__deepcopy__).
Suggestions
- Consider whether
DEFAULT_EXCLUDE(a serialization concept) is the right set to drive deepcopy shallow-copy semantics inSerializationMixin. TheContentclass introduces a dedicated_SHALLOW_COPY_FIELDSfor this purpose, which is arguably cleaner because a field could be excluded from serialization for reasons unrelated to deep-copy safety. If the two sets are guaranteed to always be identical forSerializationMixin, this is fine; otherwise a dedicated set (likeContentuses) would be more robust. - Consider whether
DEFAULT_EXCLUDE(used for serialization exclusion) and shallow-copy concern should remain coupled, or whether_SHALLOW_COPY_FIELDS(as done in_types.py) is a better pattern to adopt inSerializationMixinas well, to avoid a future subclass inadvertently getting shallow-copy behavior just because it excluded a mutable field from serialization. - Add a test for Content.deepcopy that verifies _SHALLOW_COPY_FIELDS are preserved by reference (shallow-copied) while other attributes are deep-copied. The Content class received the same refactor as SerializationMixin but has no test coverage for the new set-based logic.
- Consider adding a test that verifies subclass override of _SHALLOW_COPY_FIELDS / DEFAULT_EXCLUDE works correctly (e.g., a subclass that extends the set with additional fields).
Automated review by moonbox3's agents
moonbox3
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 92%
✓ Correctness
The diff cleanly decouples deep-copy semantics from serialization exclusion by introducing
_SHALLOW_COPY_FIELDSto replace the prior reuse ofDEFAULT_EXCLUDEin__deepcopy__. The implementation is correct: the new class variable defaults to{"raw_representation"}, the__deepcopy__method properly references it, and the tests thoroughly verify both the shallow-copy behavior for_SHALLOW_COPY_FIELDSmembers and the deep-copy behavior forDEFAULT_EXCLUDEmembers that are no longer shallow-copied. No correctness issues found.
✓ Security Reliability
Clean refactor that decouples deepcopy shallow-copy behavior from serialization exclusion by introducing a dedicated
_SHALLOW_COPY_FIELDSclass variable. This is a reliability improvement:DEFAULT_EXCLUDEfields likeadditional_propertiesare now correctly deep-copied, preventing unintended shared-state mutations between original and cloned objects. No security or reliability issues found. Tests adequately cover the new behavior and the regression case.
✓ Test Coverage
The diff decouples deepcopy behaviour from DEFAULT_EXCLUDE by introducing _SHALLOW_COPY_FIELDS. The existing tests were properly renamed and updated, and three new tests were added covering the key behavioural change (DEFAULT_EXCLUDE fields are now deep-copied, _SHALLOW_COPY_FIELDS remain shallow, integration tests on Content and ChatResponse). Coverage is solid for the main scenarios. One minor gap: no test verifies the interaction when a field appears in both DEFAULT_EXCLUDE and _SHALLOW_COPY_FIELDS simultaneously, which would confirm _SHALLOW_COPY_FIELDS takes precedence. This is a suggestion, not a blocker.
✓ Design Approach
The change correctly identifies and fixes a design flaw where
DEFAULT_EXCLUDE(a serialization concern) was conflated with deep-copy safety (a copy concern). Introducing_SHALLOW_COPY_FIELDSproperly separates these orthogonal responsibilities, and the tests verify the decoupling. The approach is sound and the implementation is clean.
Suggestions
- Add a test for the edge case where a field is listed in both DEFAULT_EXCLUDE and _SHALLOW_COPY_FIELDS, confirming _SHALLOW_COPY_FIELDS wins and the field is shallow-copied. This documents the intended precedence.
- Consider adding a simple assertion that SerializationMixin._SHALLOW_COPY_FIELDS == {'raw_representation'} to lock down the base-class default and catch accidental changes.
Automated review by moonbox3's agents
…tation in mixed workflows Fixes microsoft#4455
- SerializationMixin.__deepcopy__: check type(self).DEFAULT_EXCLUDE instead of hardcoding 'raw_representation' - Content.__deepcopy__: add _SHALLOW_COPY_FIELDS class variable and check against it instead of hardcoding - Fix tautological assertion in test (was always True) - Add second excluded field to test to verify DEFAULT_EXCLUDE is respected generically Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rosoft#4455) Introduce _SHALLOW_COPY_FIELDS class variable in SerializationMixin to separate deep-copy semantics from serialization semantics. Previously, __deepcopy__ used DEFAULT_EXCLUDE to decide which fields to shallow-copy, conflating 'not serialized' with 'not safe to deep-copy'. A field added to DEFAULT_EXCLUDE purely for serialization (e.g. additional_properties) would be silently shared between original and copy. - Add _SHALLOW_COPY_FIELDS (default {'raw_representation'}) to SerializationMixin, matching the pattern already used by Content - Update __deepcopy__ to read from _SHALLOW_COPY_FIELDS instead of DEFAULT_EXCLUDE - Add test verifying DEFAULT_EXCLUDE fields are deep-copied unless also in _SHALLOW_COPY_FIELDS - Add test for Content._SHALLOW_COPY_FIELDS identity preservation - Add test for ChatResponse deep-copying additional_properties Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add test_deepcopy_shallow_copy_fields_override_default_exclude to verify that a field in both DEFAULT_EXCLUDE and _SHALLOW_COPY_FIELDS is shallow-copied (controlled by _SHALLOW_COPY_FIELDS), while a field in DEFAULT_EXCLUDE only is still deep-copied. This addresses review comment #11 ensuring the two class variables control independent concerns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
c94b2a6 to
f852769
Compare
Python Test Coverage Report •
Python Unit Test Overview
|
|||||||||||||||||||||||||||||||||||
Motivation and Context
In workflows with mixed LLM agents, the
executor_completedevent contained an emptyAgentResponsebecause deep-copying the response failed silently whenraw_representationheld LLM SDK objects (e.g., proto/gRPC responses) that do not supportcopy.deepcopy.Fixes #4455
Description
The root cause was that
deepcopywas applied to the entireAgentResponseandContentobjects, includingraw_representationwhich may contain non-copyable LLM SDK objects. The fix adds custom__deepcopy__methods toSerializationMixinandContentthat shallow-copyraw_representationby reference while deep-copying all other attributes. Regression tests verify that workflows complete correctly when an agent produces a response with a non-copyableraw_representation.Contribution Checklist