Repository: microsoft/Agents-for-python

Issue:
Fails to save agent state: `_SignInState` is not JSON serializable
Using `Authorization` and `BlobStorage` at the same time results in serialization errors. The `_SignInState` includes a Pydantic model of `Activity` which cannot be serialized with `json.dumps`.

Issue is with this line offering an `Activity` to be serialized by storage implementations:

https://github.com/microsoft/Agents-for-python/blob/092324b4765be3ecd415f85670d96da7e0cc8e99/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_sign_in_state.py#L34

I initialized the AgentApplication like in the auto-signin sample, except storage is a `BlobStorage`.

```python
STORAGE = BlobStorage(
    BlobStorageConfig(...)
)

CONNECTION_MANAGER = MsalConnectionManager(**agents_sdk_config)

ADAPTER = CloudAdapter(connection_manager=CONNECTION_MANAGER)

AUTHORIZATION = Authorization(
    storage=STORAGE, connection_manager=CONNECTION_MANAGER, **agents_sdk_config
)

AGENT_APP = AgentApplication[TurnState](
    storage=STORAGE, adapter=ADAPTER, authorization=AUTHORIZATION, **agents_sdk_config
)

@AGENT_APP.activity(ActivityTypes.message, auth_handlers=["GRAPH"])
async def on_message(context: TurnContext, state: TurnState) -> None:
    ...
```

Stack trace when a message activity is received.

```
Traceback (most recent call last):
  File ".venv/lib/python3.13/site-packages/microsoft_agents/hosting/core/channel_adapter.py", line 242, in run_pipeline
    return await self.middleware_set.receive_activity_with_status(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        context, callback
        ^^^^^^^^^^^^^^^^^
    )
    ^
  File ".venv/lib/python3.13/site-packages/microsoft_agents/hosting/core/middleware_set.py", line 56, in receive_activity_with_status
    return await self.receive_activity_internal(context, callback)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.13/site-packages/microsoft_agents/hosting/core/middleware_set.py", line 66, in receive_activity_internal
    return await callback(context)
           ^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.13/site-packages/microsoft_agents/hosting/core/app/agent_application.py", line 679, in on_turn
    await self._start_long_running_call(context, self._on_turn)
  File ".venv/lib/python3.13/site-packages/microsoft_agents/hosting/core/app/agent_application.py", line 865, in _start_long_running_call
    return await func(context)
           ^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.13/site-packages/microsoft_agents/hosting/core/app/agent_application.py", line 701, in _on_turn
    ) = await self._auth._on_turn_auth_intercept(context, turn_state)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.13/site-packages/microsoft_agents/hosting/core/app/oauth/authorization.py", line 302, in _on_turn_auth_intercept
    sign_in_response = await self._start_or_continue_sign_in(
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        context, state, auth_handler_id
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File ".venv/lib/python3.13/site-packages/microsoft_agents/hosting/core/app/oauth/authorization.py", line 259, in _start_or_continue_sign_in
    await self._save_sign_in_state(context, sign_in_state)
  File ".venv/lib/python3.13/site-packages/microsoft_agents/hosting/core/app/oauth/authorization.py", line 157, in _save_sign_in_state
    await self._storage.write({key: state})
  File ".venv/lib/python3.13/site-packages/microsoft_agents/hosting/core/storage/storage.py", line 91, in write
    await gather(*[self._write_item(key, value) for key, value in changes.items()])
  File ".venv/lib/python3.13/site-packages/microsoft_agents/storage/blob/blob_storage.py", line 90, in _write_item
    item_rep_bytes = json.dumps(item_JSON).encode("utf-8")
                     ~~~~~~~~~~^^^^^^^^^^^
  File "~/.local/share/uv/python/cpython-3.13.7-linux-x86_64-gnu/lib/python3.13/json/__init__.py", line 231, in dumps
    return _default_encoder.encode(obj)
           ~~~~~~~~~~~~~~~~~~~~~~~^^^^^
  File "~/.local/share/uv/python/cpython-3.13.7-linux-x86_64-gnu/lib/python3.13/json/encoder.py", line 200, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "~/.local/share/uv/python/cpython-3.13.7-linux-x86_64-gnu/lib/python3.13/json/encoder.py", line 261, in iterencode
    return _iterencode(o, 0)
  File "~/.local/share/uv/python/cpython-3.13.7-linux-x86_64-gnu/lib/python3.13/json/encoder.py", line 180, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
                    f'is not JSON serializable')
TypeError: Object of type Activity is not JSON serializable
```

```
  "microsoft-agents-authentication-msal>=0.6.0",
  "microsoft-agents-hosting-aiohttp>=0.6.0",
  "microsoft-agents-hosting-teams>=0.6.0",
  "microsoft-agents-storage-blob>=0.6.0",
```

Discussion:

Fix (patch):
diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_sign_in_state.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_sign_in_state.py
index ddd22d90..bd16530a 100644
--- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_sign_in_state.py
+++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_sign_in_state.py
@@ -7,35 +7,27 @@
 
 from typing import Optional
 
+from pydantic import BaseModel
+
 from microsoft_agents.activity import Activity
 
 from ...storage._type_aliases import JSON
 from ...storage import StoreItem
 
 
-class _SignInState(StoreItem):
+class _SignInState(BaseModel, StoreItem):
     """Store item for sign-in state, including tokens and continuation activity.
 
     Used to cache tokens and keep track of activities during single and
     multi-turn sign-in flows.
     """
 
-    def __init__(
-        self,
-        active_handler_id: str,
-        continuation_activity: Optional[Activity] = None,
-    ) -> None:
-        self.active_handler_id = active_handler_id
-        self.continuation_activity = continuation_activity
+    active_handler_id: str
+    continuation_activity: Optional[Activity] = None
 
     def store_item_to_json(self) -> JSON:
-        return {
-            "active_handler_id": self.active_handler_id,
-            "continuation_activity": self.continuation_activity,
-        }
+        return self.model_dump(mode="json", exclude_unset=True, by_alias=True)
 
     @staticmethod
     def from_json_to_store_item(json_data: JSON) -> _SignInState:
-        return _SignInState(
-            json_data["active_handler_id"], json_data.get("continuation_activity")
-        )
+        return _SignInState.model_validate(json_data)


Test changes:
diff --git a/tests/hosting_core/app/_oauth/_common.py b/tests/hosting_core/app/_oauth/_common.py
index c2a6d2f0..4d3fea34 100644
--- a/tests/hosting_core/app/_oauth/_common.py
+++ b/tests/hosting_core/app/_oauth/_common.py
@@ -29,9 +29,11 @@ def create_testing_TurnContext(
 
     turn_context = mocker.Mock()
     if not activity:
-        turn_context.activity.channel_id = channel_id
-        turn_context.activity.from_property.id = user_id
-        turn_context.activity.type = ActivityTypes.message
+        turn_context.activity = Activity(
+            type=ActivityTypes.message,
+            channel_id=channel_id,
+            from_property={"id": user_id},
+        )
     else:
         turn_context.activity = activity
     turn_context.adapter.USER_TOKEN_CLIENT_KEY = "__user_token_client"
diff --git a/tests/hosting_core/app/_oauth/test_sign_in_state.py b/tests/hosting_core/app/_oauth/test_sign_in_state.py
new file mode 100644
index 00000000..d3bce6ec
--- /dev/null
+++ b/tests/hosting_core/app/_oauth/test_sign_in_state.py
@@ -0,0 +1,43 @@
+import json
+
+from microsoft_agents.activity import Activity
+from microsoft_agents.hosting.core.app.oauth._sign_in_state import _SignInState
+
+
+def test_sign_in_state_serialization_deserialization(tmp_path):
+    original_state = _SignInState(
+        active_handler_id="handler_123",
+        continuation_activity=Activity(
+            type="message",
+            id="activity_456",
+            timestamp="2024-01-01T12:00:00Z",
+            service_url="https://service.url",
+            channel_id="channel_789",
+            from_property={"id": "user_1"},
+            conversation={"id": "conv_1"},
+            recipient={"id": "bot_1"},
+            text="Hello, World!",
+        ),
+    )
+
+    # Serialize to JSON
+    json_data = original_state.store_item_to_json()
+
+    # Deserialize back to _SignInState
+    deserialized_state = _SignInState.from_json_to_store_item(json_data)
+
+    # Assert equality
+    assert deserialized_state.active_handler_id == original_state.active_handler_id
+    assert (
+        deserialized_state.continuation_activity == original_state.continuation_activity
+    )
+
+    with open(tmp_path / "sign_in_state.json", "w") as f:
+        json.dump(json_data, f)
+
+    with open(tmp_path / "sign_in_state.json", "r") as f:
+        loaded_json_data = json.load(f)
+
+    loaded_state = _SignInState.from_json_to_store_item(loaded_json_data)
+    assert loaded_state.active_handler_id == original_state.active_handler_id
+    assert loaded_state.continuation_activity == original_state.continuation_activity