From d604c59f82a90cd7ddd4249845d95d3b2e1fc31b Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 15 Jul 2026 09:59:49 -0700 Subject: [PATCH 01/13] OAuth continuation fix --- .../hosting/core/app/agent_application.py | 71 +++++++++++++++++-- .../oauth/_handlers/_user_authorization.py | 8 +++ .../hosting/core/storage/transcript_logger.py | 2 +- 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py index 748060ee..fd1ddad6 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py @@ -4,6 +4,8 @@ """ from __future__ import annotations + +import asyncio import logging from contextlib import nullcontext from copy import copy @@ -835,15 +837,26 @@ async def _on_turn(self, context: TurnContext): ) if auth_intercepts: if continuation_activity: - new_context = copy(context) - new_context.activity = continuation_activity + await turn_state.save(context) logger.info( - "Resending continuation activity %s", + "Queueing continuation activity %s", continuation_activity.text, ) - await self.on_turn(new_context) + self._queue_auth_continuation( + context, continuation_activity + ) + else: await turn_state.save(context) - return + logger.info( + "No continuation activity to queue, copying current activity" + ) + new_context = TurnContext(context) + for key, value in context.turn_state.items(): + new_context.turn_state[key] = value + self._queue_auth_continuation( + new_context, context.activity + ) + return # allows immediate sending of invoke response logger.debug("Running before turn middleware") if not await self._run_before_turn_middleware(context, turn_state): @@ -1020,6 +1033,54 @@ async def _start_long_running_call( return await func(context) + def _queue_auth_continuation( + self, context: TurnContext, continuation_activity: Activity + ) -> None: + task = asyncio.create_task( + self._replay_auth_continuation(context, continuation_activity) + ) + task.add_done_callback(self._log_auth_continuation_failure) + + async def _replay_auth_continuation( + self, context: TurnContext, continuation_activity: Activity + ) -> None: + adapter = context.adapter + claims_identity = context.identity + + if claims_identity and hasattr(adapter, "continue_conversation_with_claims"): + oauth_scope_key = getattr(adapter, "OAUTH_SCOPE_KEY", None) + audience = ( + context.turn_state.get(oauth_scope_key) if oauth_scope_key else None + ) + await adapter.continue_conversation_with_claims( + claims_identity, + continuation_activity, + self.on_turn, + audience, + ) + return + + new_context = copy(context) + new_context.activity = continuation_activity + await self.on_turn(new_context) + + @staticmethod + def _log_auth_continuation_failure(task: asyncio.Task) -> None: + if task.cancelled(): + logger.warning("OAuth continuation replay was cancelled.") + return + + exception = task.exception() + if exception: + logger.error( + "OAuth continuation replay failed.", + exc_info=( + type(exception), + exception, + exception.__traceback__, + ), + ) + async def _on_error(self, context: TurnContext, err: ApplicationError) -> None: if self._error: logger.info( diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py index 37ad2f59..7e2a1a9d 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py @@ -230,6 +230,14 @@ async def _handle_flow_response( ).model_dump(exclude_unset=True), ) ) + elif flow_state.tag == _FlowStateTag.COMPLETE: + logger.info("Sign-in flow completed successfully.") + await context.send_activity( + Activity( + type=ActivityTypes.invoke_response, + value=InvokeResponse(status=200), + ) + ) async def _sign_in( self, diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/transcript_logger.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/transcript_logger.py index 3986e479..714537ad 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/transcript_logger.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/transcript_logger.py @@ -51,7 +51,7 @@ async def log_activity(self, activity: Activity) -> None: if not activity: raise TypeError("Activity is required") - json_data = activity.model_dump_json() + json_data = activity.model_dump_json(exclude_unset=True) parsed = json.loads(json_data) print(json.dumps(parsed, indent=4)) From 1a0f280d09cc3248a4ee0887aa01c51f1e4accef Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 15 Jul 2026 10:24:42 -0700 Subject: [PATCH 02/13] _AuthInterceptResult --- .../hosting/core/app/agent_application.py | 57 ++++++++++--------- .../hosting/core/app/oauth/authorization.py | 47 +++++++++++---- 2 files changed, 64 insertions(+), 40 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py index fd1ddad6..366a0148 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py @@ -41,13 +41,13 @@ from .state import TurnState from ..channel_service_adapter import ChannelServiceAdapter -from .oauth import Authorization +from .oauth.authorization import Authorization, _AuthInterceptResult from .typing_indicator import TypingIndicator from .telemetry import spans from ._type_defs import RouteHandler, RouteSelector from ._routes import _RouteList, _Route, RouteRank, _agentic_selector -from .proactive import Proactive, ProactiveOptions +from .proactive import Proactive logger = logging.getLogger(__name__) @@ -829,33 +829,15 @@ async def _on_turn(self, context: TurnContext): ActivityTypes.invoke, ]: - ( - auth_intercepts, - continuation_activity, - ) = await self._auth._on_turn_auth_intercept( - context, turn_state + auth_intercept_result = ( + await self._auth._on_turn_auth_intercept( + context, turn_state + ) ) - if auth_intercepts: - if continuation_activity: - await turn_state.save(context) - logger.info( - "Queueing continuation activity %s", - continuation_activity.text, - ) - self._queue_auth_continuation( - context, continuation_activity - ) - else: - await turn_state.save(context) - logger.info( - "No continuation activity to queue, copying current activity" - ) - new_context = TurnContext(context) - for key, value in context.turn_state.items(): - new_context.turn_state[key] = value - self._queue_auth_continuation( - new_context, context.activity - ) + if auth_intercept_result.should_skip_turn: + await self._handle_turn_skip( + context, turn_state, auth_intercept_result + ) return # allows immediate sending of invoke response logger.debug("Running before turn middleware") @@ -1033,6 +1015,25 @@ async def _start_long_running_call( return await func(context) + async def _handle_turn_skip( + self, context: TurnContext, turn_state: StateT, result: _AuthInterceptResult + ): + + if result.should_replay: + + await turn_state.save(context) + + if result.continuation_activity: + logger.info("Replaying the turn with saved continuation activity") + self._queue_auth_continuation(context, result.continuation_activity) + else: + logger.info("Replaying the turn") + new_context = TurnContext(context) + for key, value in context.turn_state.items(): + new_context.turn_state[key] = value + self._queue_auth_continuation(new_context, context.activity) + return + def _queue_auth_continuation( self, context: TurnContext, continuation_activity: Activity ) -> None: diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py index 542b2866..00e50dd8 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py @@ -5,6 +5,7 @@ import logging from typing import Optional, Callable, Awaitable, cast +from dataclasses import dataclass from microsoft_agents.activity import Activity, Channels, SignInConstants, TokenResponse from microsoft_agents.activity.activity_types import ActivityTypes @@ -33,6 +34,15 @@ } +@dataclass +class _AuthInterceptResult: + """Dataclass representing the result of an authentication intercept.""" + + should_skip_turn: bool + should_replay: bool + continuation_activity: Activity | None = None + + class Authorization: """Class responsible for managing authorization flows.""" @@ -305,20 +315,17 @@ async def sign_out( async def _on_turn_auth_intercept( self, context: TurnContext, state: TurnState - ) -> tuple[bool, Optional[Activity]]: + ) -> _AuthInterceptResult: """Intercepts the turn to check for active authentication flows. - Returns true if the rest of the turn should be skipped because auth did not finish. - Returns false if the turn should continue processing as normal. - If auth completes and a new turn should be started, returns the continuation activity - from the cached _SignInState. + Returns an _AuthInterceptResult indicating whether the turn should be skipped and if a continuation activity is present. :param context: The context object for the current turn. - :type context: :class:`microsoft_agents.hosting.core.turn_context.TurnContext` + :type context: :class:`microsoft_agents.hosting.core.turn_context.TurnContext :param state: The turn state for the current turn. :type state: :class:`microsoft_agents.hosting.core.app.state.turn_state.TurnState` - :return: A tuple indicating whether the turn should be skipped and the continuation activity if applicable. - :rtype: tuple[bool, Optional[:class:`microsoft_agents.activity.Activity`]] + :return: An _AuthInterceptResult indicating whether the turn should be skipped and if a continuation activity is present. + :rtype: :class:`microsoft_agents.hosting.core.app.oauth.authorization._AuthInterceptResult` """ sign_in_state = await self._load_sign_in_state(context) @@ -331,17 +338,33 @@ async def _on_turn_auth_intercept( if sign_in_response.tag == _FlowStateTag.COMPLETE: if not sign_in_state: # flow just completed, no continuation activity - return False, None + return _AuthInterceptResult( + should_skip_turn=context.activity.type + == ActivityTypes.invoke, + should_replay=True, + continuation_activity=None, + ) assert sign_in_state.continuation_activity is not None continuation_activity = ( sign_in_state.continuation_activity.model_copy() ) # flow complete, start new turn with continuation activity - return True, continuation_activity + + return _AuthInterceptResult( + should_skip_turn=True, + should_replay=True, + continuation_activity=continuation_activity, + ) # auth flow still in progress, the turn should be skipped - return True, None + return _AuthInterceptResult( + should_skip_turn=True, + should_replay=False, + continuation_activity=None, + ) # no active auth flow, continue processing - return False, None + return _AuthInterceptResult( + should_skip_turn=False, should_replay=False, continuation_activity=None + ) async def get_token( self, context: TurnContext, auth_handler_id: Optional[str] = None From 7ae4d5a93ceb34028665198be504694e9814f4c6 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 15 Jul 2026 10:30:36 -0700 Subject: [PATCH 03/13] Updating test cases for _AuthInterceptResult return values --- .../app/_oauth/test_authorization.py | 25 ++++++++----------- tests/hosting_dialogs/test_dialog_manager.py | 14 ++++------- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/tests/hosting_core/app/_oauth/test_authorization.py b/tests/hosting_core/app/_oauth/test_authorization.py index 81cdd6e7..4fea7d34 100644 --- a/tests/hosting_core/app/_oauth/test_authorization.py +++ b/tests/hosting_core/app/_oauth/test_authorization.py @@ -18,6 +18,7 @@ Authorization, AgenticUserAuthorization, ) +from microsoft_agents.hosting.core.app.oauth.authorization import _AuthInterceptResult from microsoft_agents.hosting.core._oauth import _FlowStateTag @@ -646,12 +647,10 @@ async def test_on_turn_auth_intercept_no_intercept( ): await authorization._delete_sign_in_state(context) - intercepts, continuation_activity = await authorization._on_turn_auth_intercept( - context, None - ) + res = await authorization._on_turn_auth_intercept(context, None) - assert not continuation_activity - assert not intercepts + assert not res.continuation_activity + assert not res.should_skip_turn final_state = await authorization._load_sign_in_state(context) @@ -687,12 +686,10 @@ async def test_on_turn_auth_intercept_with_intercept_incomplete( context, copy_sign_in_state(initial_state) ) - intercepts, continuation_activity = await authorization._on_turn_auth_intercept( - context, auth_handler_id - ) + res = await authorization._on_turn_auth_intercept(context, auth_handler_id) - assert not continuation_activity - assert intercepts + assert not res.continuation_activity + assert res.should_skip_turn final_state = await authorization._load_sign_in_state(context) assert sign_in_state_eq(final_state, initial_state) @@ -721,12 +718,10 @@ async def test_on_turn_auth_intercept_with_intercept_complete( context, copy_sign_in_state(initial_state) ) - intercepts, continuation_activity = await authorization._on_turn_auth_intercept( - context, auth_handler_id - ) + res = await authorization._on_turn_auth_intercept(context, auth_handler_id) - assert continuation_activity == old_activity - assert intercepts + assert res.continuation_activity == old_activity + assert res.should_skip_turn final_state = await authorization._load_sign_in_state(context) assert sign_in_state_eq(final_state, initial_state) diff --git a/tests/hosting_dialogs/test_dialog_manager.py b/tests/hosting_dialogs/test_dialog_manager.py index fa39d2f8..28af29ed 100644 --- a/tests/hosting_dialogs/test_dialog_manager.py +++ b/tests/hosting_dialogs/test_dialog_manager.py @@ -120,17 +120,13 @@ async def logic(context: TurnContext): if test_case != SkillFlowTestCase.root_bot_only: # Create a skill ClaimsIdentity and put it in turn_state so isSkillClaim() returns True. claims_identity = ClaimsIdentity({}, False) - claims_identity.claims[ - "ver" - ] = "2.0" # AuthenticationConstants.VersionClaim - claims_identity.claims[ - "aud" - ] = ( + claims_identity.claims["ver"] = ( + "2.0" # AuthenticationConstants.VersionClaim + ) + claims_identity.claims["aud"] = ( SimpleComponentDialog.skill_bot_id ) # AuthenticationConstants.AudienceClaim - claims_identity.claims[ - "azp" - ] = ( + claims_identity.claims["azp"] = ( SimpleComponentDialog.parent_bot_id ) # AuthenticationConstants.AuthorizedParty context._identity = claims_identity From 0faa4741596e11f9bb17026c471108ac0037443d Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 15 Jul 2026 10:32:47 -0700 Subject: [PATCH 04/13] Adding check before sending invoke response --- .../hosting/core/app/oauth/_handlers/_user_authorization.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py index 7e2a1a9d..46355caf 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py @@ -230,8 +230,7 @@ async def _handle_flow_response( ).model_dump(exclude_unset=True), ) ) - elif flow_state.tag == _FlowStateTag.COMPLETE: - logger.info("Sign-in flow completed successfully.") + elif flow_state.tag == _FlowStateTag.COMPLETE and context.activity.type == ActivityTypes.invoke: await context.send_activity( Activity( type=ActivityTypes.invoke_response, From 44bc9c0c743b952636cb1f0c03d25f6402bae57f Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 15 Jul 2026 10:34:29 -0700 Subject: [PATCH 05/13] Address PR feedback --- .../hosting/core/app/oauth/_handlers/_user_authorization.py | 5 ++++- .../microsoft_agents/hosting/core/app/oauth/authorization.py | 2 +- .../hosting/core/storage/transcript_logger.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py index 46355caf..1039d9bb 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py @@ -230,7 +230,10 @@ async def _handle_flow_response( ).model_dump(exclude_unset=True), ) ) - elif flow_state.tag == _FlowStateTag.COMPLETE and context.activity.type == ActivityTypes.invoke: + elif ( + flow_state.tag == _FlowStateTag.COMPLETE + and context.activity.type == ActivityTypes.invoke + ): await context.send_activity( Activity( type=ActivityTypes.invoke_response, diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py index 00e50dd8..be21169b 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py @@ -321,7 +321,7 @@ async def _on_turn_auth_intercept( Returns an _AuthInterceptResult indicating whether the turn should be skipped and if a continuation activity is present. :param context: The context object for the current turn. - :type context: :class:`microsoft_agents.hosting.core.turn_context.TurnContext + :type context: :class:`microsoft_agents.hosting.core.turn_context.TurnContext` :param state: The turn state for the current turn. :type state: :class:`microsoft_agents.hosting.core.app.state.turn_state.TurnState` :return: An _AuthInterceptResult indicating whether the turn should be skipped and if a continuation activity is present. diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/transcript_logger.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/transcript_logger.py index 714537ad..3986e479 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/transcript_logger.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/transcript_logger.py @@ -51,7 +51,7 @@ async def log_activity(self, activity: Activity) -> None: if not activity: raise TypeError("Activity is required") - json_data = activity.model_dump_json(exclude_unset=True) + json_data = activity.model_dump_json() parsed = json.loads(json_data) print(json.dumps(parsed, indent=4)) From 6b104928004fdf8e7baa9af7980e7a68e7332785 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 15 Jul 2026 11:02:13 -0700 Subject: [PATCH 06/13] Refactor --- .../hosting/core/app/agent_application.py | 71 +++++-------------- 1 file changed, 18 insertions(+), 53 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py index 366a0148..62136d0e 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py @@ -1018,6 +1018,22 @@ async def _start_long_running_call( async def _handle_turn_skip( self, context: TurnContext, turn_state: StateT, result: _AuthInterceptResult ): + """Handle the case where the turn should be skipped due to an auth intercept result.""" + + async def __replay(ctx: TurnContext, act: Activity): + + new_context = TurnContext(ctx) + for key, value in ctx.turn_state.items(): + new_context.turn_state[key] = value + new_context.activity = act + + await self.adapter.continue_conversation_with_claims( + ctx.identity, + act, + self.on_turn, + ) + + context.activity = act if result.should_replay: @@ -1025,63 +1041,12 @@ async def _handle_turn_skip( if result.continuation_activity: logger.info("Replaying the turn with saved continuation activity") - self._queue_auth_continuation(context, result.continuation_activity) + asyncio.create_task(__replay(context, result.continuation_activity)) else: logger.info("Replaying the turn") - new_context = TurnContext(context) - for key, value in context.turn_state.items(): - new_context.turn_state[key] = value - self._queue_auth_continuation(new_context, context.activity) + asyncio.create_task(__replay(context, context.activity)) return - def _queue_auth_continuation( - self, context: TurnContext, continuation_activity: Activity - ) -> None: - task = asyncio.create_task( - self._replay_auth_continuation(context, continuation_activity) - ) - task.add_done_callback(self._log_auth_continuation_failure) - - async def _replay_auth_continuation( - self, context: TurnContext, continuation_activity: Activity - ) -> None: - adapter = context.adapter - claims_identity = context.identity - - if claims_identity and hasattr(adapter, "continue_conversation_with_claims"): - oauth_scope_key = getattr(adapter, "OAUTH_SCOPE_KEY", None) - audience = ( - context.turn_state.get(oauth_scope_key) if oauth_scope_key else None - ) - await adapter.continue_conversation_with_claims( - claims_identity, - continuation_activity, - self.on_turn, - audience, - ) - return - - new_context = copy(context) - new_context.activity = continuation_activity - await self.on_turn(new_context) - - @staticmethod - def _log_auth_continuation_failure(task: asyncio.Task) -> None: - if task.cancelled(): - logger.warning("OAuth continuation replay was cancelled.") - return - - exception = task.exception() - if exception: - logger.error( - "OAuth continuation replay failed.", - exc_info=( - type(exception), - exception, - exception.__traceback__, - ), - ) - async def _on_error(self, context: TurnContext, err: ApplicationError) -> None: if self._error: logger.info( From a17ed7a9433817f30b5b736531a8508cfac3e451 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 15 Jul 2026 11:04:45 -0700 Subject: [PATCH 07/13] Removing dialog test changes --- .../microsoft_agents/hosting/core/app/agent_application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py index 62136d0e..7433b4e8 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py @@ -1019,7 +1019,7 @@ async def _handle_turn_skip( self, context: TurnContext, turn_state: StateT, result: _AuthInterceptResult ): """Handle the case where the turn should be skipped due to an auth intercept result.""" - + async def __replay(ctx: TurnContext, act: Activity): new_context = TurnContext(ctx) From ea67f26c611c551dba08d93db2a9a2eadbc1d1b2 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 15 Jul 2026 11:10:25 -0700 Subject: [PATCH 08/13] Addressing PR feedback --- .../microsoft_agents/hosting/core/app/agent_application.py | 7 +------ .../hosting/core/app/oauth/authorization.py | 6 +++--- tests/hosting_core/app/_oauth/test_authorization.py | 3 --- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py index 7433b4e8..32a71768 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py @@ -1022,12 +1022,7 @@ async def _handle_turn_skip( async def __replay(ctx: TurnContext, act: Activity): - new_context = TurnContext(ctx) - for key, value in ctx.turn_state.items(): - new_context.turn_state[key] = value - new_context.activity = act - - await self.adapter.continue_conversation_with_claims( + await self._adapter.continue_conversation_with_claims( ctx.identity, act, self.on_turn, diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py index be21169b..352643b5 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py @@ -338,10 +338,10 @@ async def _on_turn_auth_intercept( if sign_in_response.tag == _FlowStateTag.COMPLETE: if not sign_in_state: # flow just completed, no continuation activity + is_invoke = (context.activity.type == ActivityTypes.invoke,) return _AuthInterceptResult( - should_skip_turn=context.activity.type - == ActivityTypes.invoke, - should_replay=True, + should_skip_turn=is_invoke, + should_replay=is_invoke, continuation_activity=None, ) assert sign_in_state.continuation_activity is not None diff --git a/tests/hosting_core/app/_oauth/test_authorization.py b/tests/hosting_core/app/_oauth/test_authorization.py index 4fea7d34..9cd97fba 100644 --- a/tests/hosting_core/app/_oauth/test_authorization.py +++ b/tests/hosting_core/app/_oauth/test_authorization.py @@ -18,15 +18,12 @@ Authorization, AgenticUserAuthorization, ) -from microsoft_agents.hosting.core.app.oauth.authorization import _AuthInterceptResult from microsoft_agents.hosting.core._oauth import _FlowStateTag from microsoft_agents.hosting.core import ( AuthHandler, - Storage, MemoryStorage, - TurnContext, ) from tests._common.storage.utils import StorageBaseline From 819ffa74878d07d60e92411f512945729ff9cfe3 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 15 Jul 2026 11:22:30 -0700 Subject: [PATCH 09/13] Addressing PR feedback --- .../hosting/core/app/agent_application.py | 46 +++++++++++++++---- .../oauth/_handlers/_user_authorization.py | 2 +- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py index 32a71768..2c590916 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py @@ -1020,26 +1020,54 @@ async def _handle_turn_skip( ): """Handle the case where the turn should be skipped due to an auth intercept result.""" - async def __replay(ctx: TurnContext, act: Activity): + if not context.identity: + logger.error( + "Cannot replay turn because context.identity is not set. Ensure that the adapter is configured to set the identity." + ) + raise ApplicationError( + "Cannot replay turn because context.identity is not set. Ensure that the adapter is configured to set the identity." + ) + + async def __replay_turn(replay_context: TurnContext): + + for key, val in context.turn_state.items(): + if replay_context.turn_state.get(key, None) is None: + replay_context.turn_state[key] = val + + await self._on_turn(replay_context) + + async def __replay(act: Activity): await self._adapter.continue_conversation_with_claims( - ctx.identity, + context.identity, act, - self.on_turn, + __replay_turn, ) context.activity = act + def __log_task_result(task: asyncio.Task): + try: + task.result() + except Exception as e: + logger.error( + f"Error occurred while replaying the turn.", + exc_info=True, + ) + if result.should_replay: await turn_state.save(context) - if result.continuation_activity: - logger.info("Replaying the turn with saved continuation activity") - asyncio.create_task(__replay(context, result.continuation_activity)) - else: - logger.info("Replaying the turn") - asyncio.create_task(__replay(context, context.activity)) + task: asyncio.Task + + activity = result.continuation_activity or context.activity + logger.info( + "Replaying the turn with current or saved continuation activity" + ) + task = asyncio.create_task(__replay(activity)) + task.add_done_callback(__log_task_result) + return async def _on_error(self, context: TurnContext, err: ApplicationError) -> None: diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py index 1039d9bb..3dd21bf1 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py @@ -237,7 +237,7 @@ async def _handle_flow_response( await context.send_activity( Activity( type=ActivityTypes.invoke_response, - value=InvokeResponse(status=200), + value=InvokeResponse(status=200).model_dump(exclude_unset=True), ) ) From 540b00267cae3fad072a2c486ca86b5f5f76ec24 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 15 Jul 2026 11:27:41 -0700 Subject: [PATCH 10/13] Removing weird formatting --- .../microsoft_agents/hosting/core/app/oauth/authorization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py index 352643b5..f5971f63 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py @@ -338,7 +338,7 @@ async def _on_turn_auth_intercept( if sign_in_response.tag == _FlowStateTag.COMPLETE: if not sign_in_state: # flow just completed, no continuation activity - is_invoke = (context.activity.type == ActivityTypes.invoke,) + is_invoke = context.activity.type == ActivityTypes.invoke return _AuthInterceptResult( should_skip_turn=is_invoke, should_replay=is_invoke, From 9be7f8a7e060cb27d6532e1814494645b5cb0122 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 15 Jul 2026 11:31:11 -0700 Subject: [PATCH 11/13] Another comimt --- .../microsoft_agents/hosting/core/app/agent_application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py index 2c590916..451983e5 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py @@ -1061,7 +1061,7 @@ def __log_task_result(task: asyncio.Task): task: asyncio.Task - activity = result.continuation_activity or context.activity + activity = result.continuation_activity or context.activity.model_copy(deep=True) logger.info( "Replaying the turn with current or saved continuation activity" ) From b00805a635d929920550afc8c190b79d4fd2225a Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 15 Jul 2026 11:38:42 -0700 Subject: [PATCH 12/13] Addressing more PR feedback --- .../hosting/core/app/agent_application.py | 18 ++++++++---------- .../hosting/core/app/oauth/authorization.py | 2 +- .../app/_oauth/test_authorization.py | 3 +++ 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py index 451983e5..02e16319 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py @@ -1020,14 +1020,6 @@ async def _handle_turn_skip( ): """Handle the case where the turn should be skipped due to an auth intercept result.""" - if not context.identity: - logger.error( - "Cannot replay turn because context.identity is not set. Ensure that the adapter is configured to set the identity." - ) - raise ApplicationError( - "Cannot replay turn because context.identity is not set. Ensure that the adapter is configured to set the identity." - ) - async def __replay_turn(replay_context: TurnContext): for key, val in context.turn_state.items(): @@ -1044,8 +1036,6 @@ async def __replay(act: Activity): __replay_turn, ) - context.activity = act - def __log_task_result(task: asyncio.Task): try: task.result() @@ -1057,6 +1047,14 @@ def __log_task_result(task: asyncio.Task): if result.should_replay: + if not context.identity: + logger.error( + "Cannot replay turn because context.identity is not set. Ensure that the adapter is configured to set the identity." + ) + raise ApplicationError( + "Cannot replay turn because context.identity is not set. Ensure that the adapter is configured to set the identity." + ) + await turn_state.save(context) task: asyncio.Task diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py index f5971f63..96618d82 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py @@ -318,7 +318,7 @@ async def _on_turn_auth_intercept( ) -> _AuthInterceptResult: """Intercepts the turn to check for active authentication flows. - Returns an _AuthInterceptResult indicating whether the turn should be skipped and if a continuation activity is present. + Returns an _AuthInterceptResult indicating whether the turn should be skipped, replayed, and if a continuation activity is present. :param context: The context object for the current turn. :type context: :class:`microsoft_agents.hosting.core.turn_context.TurnContext` diff --git a/tests/hosting_core/app/_oauth/test_authorization.py b/tests/hosting_core/app/_oauth/test_authorization.py index 9cd97fba..bb843579 100644 --- a/tests/hosting_core/app/_oauth/test_authorization.py +++ b/tests/hosting_core/app/_oauth/test_authorization.py @@ -648,6 +648,7 @@ async def test_on_turn_auth_intercept_no_intercept( assert not res.continuation_activity assert not res.should_skip_turn + assert not res.should_replay final_state = await authorization._load_sign_in_state(context) @@ -687,6 +688,7 @@ async def test_on_turn_auth_intercept_with_intercept_incomplete( assert not res.continuation_activity assert res.should_skip_turn + assert not res.should_replay final_state = await authorization._load_sign_in_state(context) assert sign_in_state_eq(final_state, initial_state) @@ -719,6 +721,7 @@ async def test_on_turn_auth_intercept_with_intercept_complete( assert res.continuation_activity == old_activity assert res.should_skip_turn + assert not res.should_replay final_state = await authorization._load_sign_in_state(context) assert sign_in_state_eq(final_state, initial_state) From 29e06673775d6df8c517de88b09049536f1154dd Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 15 Jul 2026 11:40:04 -0700 Subject: [PATCH 13/13] Another commit --- .../microsoft_agents/hosting/core/app/agent_application.py | 4 +++- .../microsoft_agents/hosting/core/app/oauth/authorization.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py index 02e16319..7fa39e95 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py @@ -1059,7 +1059,9 @@ def __log_task_result(task: asyncio.Task): task: asyncio.Task - activity = result.continuation_activity or context.activity.model_copy(deep=True) + activity = result.continuation_activity or context.activity.model_copy( + deep=True + ) logger.info( "Replaying the turn with current or saved continuation activity" ) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py index 96618d82..ad2c852f 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py @@ -341,7 +341,7 @@ async def _on_turn_auth_intercept( is_invoke = context.activity.type == ActivityTypes.invoke return _AuthInterceptResult( should_skip_turn=is_invoke, - should_replay=is_invoke, + should_replay=False, continuation_activity=None, ) assert sign_in_state.continuation_activity is not None