-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Python: Bugfix: issue-3364: detect and preserve custom store state #3378
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -170,10 +170,25 @@ def __init__( | |||||||||||||||||||||||||||||||||||||||||||||||
| if service_thread_id is not None and chat_message_store_state is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise AgentThreadException("A thread cannot have both a service_thread_id and a chat_message_store.") | ||||||||||||||||||||||||||||||||||||||||||||||||
| self.service_thread_id = service_thread_id | ||||||||||||||||||||||||||||||||||||||||||||||||
| self.chat_message_store_state: ChatMessageStoreState | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||
| self.chat_message_store_state: MutableMapping[str, Any] | ChatMessageStoreState | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||
| if chat_message_store_state is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(chat_message_store_state, dict): | ||||||||||||||||||||||||||||||||||||||||||||||||
| self.chat_message_store_state = ChatMessageStoreState.from_dict(chat_message_store_state) | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Determine if this is a custom store that needs to preserve extra fields | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Standard ChatMessageStoreState has 'type' (from SerializationMixin) and 'messages' | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Create a temporary instance to get the expected fields dynamically | ||||||||||||||||||||||||||||||||||||||||||||||||
| temp_state = ChatMessageStoreState() | ||||||||||||||||||||||||||||||||||||||||||||||||
| standard_state_dict = temp_state.to_dict() | ||||||||||||||||||||||||||||||||||||||||||||||||
| standard_fields = set(standard_state_dict.keys()) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+176
to
+181
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Determine if this is a custom store that needs to preserve extra fields | |
| # Standard ChatMessageStoreState has 'type' (from SerializationMixin) and 'messages' | |
| # Create a temporary instance to get the expected fields dynamically | |
| temp_state = ChatMessageStoreState() | |
| standard_state_dict = temp_state.to_dict() | |
| standard_fields = set(standard_state_dict.keys()) | |
| # Determine if this is a custom store that needs to preserve extra fields. | |
| # Standard ChatMessageStoreState has 'type' (from SerializationMixin) and 'messages'. | |
| # Explicitly use the known standard fields instead of inferring them at runtime. | |
| standard_fields = {"type", "messages"} |
Copilot
AI
Feb 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The detection logic for custom stores may incorrectly classify stores that only differ in the 'type' field value as standard stores. For example, if a custom store serializes as {"type": "custom_store_state", "messages": []}, it will be treated as a standard store because it has no extra fields beyond 'type' and 'messages', and will be converted to ChatMessageStoreState with type "chat_message_store_state", losing the original custom type information.
To fix this, also check if the 'type' field value (when present) differs from 'chat_message_store_state'. If it does, treat it as a custom store and preserve as dict. For example:
if extra_fields or (
"type" in chat_message_store_state
and chat_message_store_state["type"] != "chat_message_store_state"
):
# Custom store with additional fields or different type
self.chat_message_store_state = chat_message_store_state| # Check if input has fields beyond what standard ChatMessageStoreState would have | |
| extra_fields = set(chat_message_store_state.keys()) - standard_fields | |
| if extra_fields: | |
| # Custom store with additional fields (e.g., redis_url, thread_id, key_prefix) | |
| # Preserve as dict to retain all custom configuration | |
| self.chat_message_store_state = chat_message_store_state | |
| else: | |
| # Standard ChatMessageStoreState - convert for backward compatibility | |
| # This handles both {"messages": []} and {"type": "...", "messages": [...]} | |
| standard_type = standard_state_dict.get("type") | |
| # Check if input has fields beyond what standard ChatMessageStoreState would have | |
| extra_fields = set(chat_message_store_state.keys()) - standard_fields | |
| if extra_fields or ( | |
| "type" in chat_message_store_state | |
| and standard_type is not None | |
| and chat_message_store_state["type"] != standard_type | |
| ): | |
| # Custom store with additional fields or different type (e.g., redis_url, thread_id, key_prefix) | |
| # Preserve as dict to retain all custom configuration | |
| self.chat_message_store_state = chat_message_store_state | |
| else: | |
| # Standard ChatMessageStoreState - convert for backward compatibility | |
| # This handles both {"messages": []} and {"type": standard_type, "messages": [...]} |
Copilot
AI
Feb 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message says "Cannot deserialize custom message store without providing a message_store instance" but this check is triggered even for standard stores that have a 'messages' field. The condition should be checking if this is actually a custom store (no 'messages' field) before raising this error.
Consider restructuring the logic to: (1) check if 'messages' exists, if yes create standard ChatMessageStore, (2) if no 'messages' but has 'type' field matching known custom types, raise this error, (3) otherwise raise a generic "Invalid state" error.
| parsed_state = ChatMessageStoreState.from_dict(store_state_dict) | |
| message_store = ChatMessageStore(messages=parsed_state.messages, **kwargs) | |
| else: | |
| raise AgentThreadException( | |
| "Cannot deserialize custom message store without providing a message_store instance. " | |
| "Please provide a message_store parameter to deserialize()." | |
| ) | |
| # Standard ChatMessageStore serialized state | |
| parsed_state = ChatMessageStoreState.from_dict(store_state_dict) | |
| message_store = ChatMessageStore(messages=parsed_state.messages, **kwargs) | |
| elif "type" in store_state_dict: | |
| # Looks like a custom store; require caller-provided message_store instance | |
| raise AgentThreadException( | |
| "Cannot deserialize custom message store without providing a message_store instance. " | |
| "Please provide a message_store parameter to deserialize()." | |
| ) | |
| else: | |
| # Missing both 'messages' and 'type' fields; treat as invalid state | |
| raise AgentThreadException("Invalid chat_message_store_state; missing 'messages' and 'type' fields.") |
Copilot
AI
Feb 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This re-raises AgentThreadException but then has a catch-all for other exceptions. The problem is that if the code at line 500 (ChatMessageStoreState.from_dict) raises an AgentThreadException, it will be caught by line 512 and re-raised. However, if line 501 (ChatMessageStore construction) fails with an AgentThreadException, it will also be caught and re-raised. This double-catch pattern is confusing and may not preserve the original exception context properly.
Consider removing the re-raise at line 512-513 and letting AgentThreadExceptions propagate naturally, or handle them separately if you need to wrap them with additional context.
| except AgentThreadException: | |
| raise |
Copilot
AI
Feb 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same issue exists here as in the deserialize method. The error message "Cannot create custom message store from state" is misleading because this branch is also reached for standard stores that happen to be stored as dicts. The check at line 556 verifies that 'messages' exists, so if it doesn't exist, you're treating it as a custom store error. However, it could also just be an invalid standard store state.
The error messaging should be more precise about what went wrong.
| "Cannot create custom message store from state. " | |
| "Please create a message store first and then call update_from_state()." | |
| "Invalid chat_message_store_state: expected a 'messages' key when no message store " | |
| "instance is configured. If you are using a custom message store, create it first and " | |
| "then call update_from_thread_state()." |
Copilot
AI
Feb 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same maintainability issue as in the deserialize method. The double exception handling with re-raise of AgentThreadException followed by a generic catch-all creates confusing control flow. Consider simplifying this error handling pattern.
| except AgentThreadException: | |
| raise | |
| except Exception as ex: | |
| except Exception as ex: | |
| if isinstance(ex, AgentThreadException): | |
| raise |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type annotation change from
ChatMessageStoreState | NonetoMutableMapping[str, Any] | ChatMessageStoreState | Noneis correct and necessary to support custom stores. However, this is a breaking change to the public API since code that accesseschat_message_store_state.messagesdirectly may now fail if it's a dict instead of a ChatMessageStoreState object.Consider documenting this breaking change more prominently in the PR description and potentially adding a BREAKING prefix to the PR title as mentioned in the contribution checklist.