Skip to content

Fix AgentNotification docs broken type links and lifecycle routing bug#238

Closed
Copilot wants to merge 2 commits intomainfrom
copilot/fix-broken-links-agentnotification-docs
Closed

Fix AgentNotification docs broken type links and lifecycle routing bug#238
Copilot wants to merge 2 commits intomainfrom
copilot/fix-broken-links-agentnotification-docs

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 24, 2026

Sphinx was expanding the AgentHandler type alias inline during doc generation, producing unresolvable strings like Callable[[Callable[[~TContext, ~TState, ... because the alias used unbound TypeVars. This broke all cross-reference links on the AgentNotification API page.

Separately, four lifecycle convenience methods (on_lifecycle, on_user_created, on_user_workload_onboarding, on_user_deleted) called self.on_lifecycle_notification(...) which does not exist, causing AttributeError at runtime.

Changes

agent_notification.py

  • Removed TContext/TState TypeVars; replaced AgentHandler with a TypeAlias using concrete TurnContext, TurnState, AgentNotificationActivity — Sphinx now emits hyperlinked type names instead of ~TContext/~TState
  • Added RouteHandler: TypeAlias = Callable[[TurnContext, TurnState], Awaitable[None]] to name the inner handler type
  • Simplified all 8 method return type annotations from the verbose inline form to Callable[[AgentHandler], RouteHandler]
  • Fixed the four lifecycle convenience methods to call self.on_agent_lifecycle_notification(...) (the method that actually exists)
# Before — TypeVars caused unresolvable doc links and wrong method call
AgentHandler = Callable[[TContext, TState, AgentNotificationActivity], Awaitable[None]]

def on_lifecycle(self, **kwargs) -> Callable[[AgentHandler], Callable[[TurnContext, TurnState], Awaitable[None]]]:
    return self.on_lifecycle_notification("*", **kwargs)   # AttributeError

# After — concrete alias, clean return type, correct dispatch
AgentHandler: TypeAlias = Callable[[TurnContext, TurnState, AgentNotificationActivity], Awaitable[None]]
RouteHandler: TypeAlias = Callable[[TurnContext, TurnState], Awaitable[None]]

def on_lifecycle(self, **kwargs) -> Callable[[AgentHandler], RouteHandler]:
    return self.on_agent_lifecycle_notification("*", **kwargs)

notifications/__init__.py

  • Exports RouteHandler so callers can use it in their own type annotations.

tests/notifications/test_agent_notification.py (new)

  • 16 unit tests covering type alias integrity, add_route registration for all 8 decorators, and route selector matching for subchannel and lifecycle routing.

… bugs

- Replace TypeVar-based AgentHandler with concrete TypeAlias using TurnContext/TurnState
- Add RouteHandler TypeAlias for the inner route handler, simplifying method return types
- Fix runtime bug: on_lifecycle, on_user_created, on_user_workload_onboarding, on_user_deleted
  all called non-existent self.on_lifecycle_notification; corrected to on_agent_lifecycle_notification
- Export RouteHandler from the package __init__
- Add 16 unit tests covering type alias integrity and routing correctness

Agent-Logs-Url: https://github.com/microsoft/Agent365-python/sessions/8d7dad6f-d5c2-4565-895a-9ef968ee6d13

Co-authored-by: JimDaly <6353736+JimDaly@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 24, 2026

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Copilot AI changed the title [WIP] Fix broken links in AgentNotification Class docs Fix AgentNotification docs broken type links and lifecycle routing bug Apr 24, 2026
Copilot AI requested a review from JimDaly April 24, 2026 17:34
@JimDaly JimDaly marked this pull request as ready for review April 24, 2026 17:59
@JimDaly JimDaly requested review from a team as code owners April 24, 2026 17:59
Copy link
Copy Markdown
Member

@JimDaly JimDaly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect a higher level approval is required for this to be merged.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes AgentNotification documentation typing so Sphinx generates resolvable cross-reference links, and fixes lifecycle decorator routing so convenience APIs don’t crash at runtime.

Changes:

  • Replace TypeVar-based AgentHandler with concrete TypeAlias definitions (and add RouteHandler) to improve Sphinx output.
  • Fix lifecycle convenience decorators to call the correct underlying registration method (on_agent_lifecycle_notification).
  • Add unit tests for type alias importability and decorator/selector behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
tests/notifications/test_agent_notification.py Adds unit tests for public type aliases and route registration/selector behavior.
tests/notifications/init.py Adds required license header for the new test package.
libraries/microsoft-agents-a365-notifications/microsoft_agents_a365/notifications/agent_notification.py Updates public type aliases for docs and fixes lifecycle convenience methods dispatch.
libraries/microsoft-agents-a365-notifications/microsoft_agents_a365/notifications/init.py Exports the new RouteHandler type alias.

print("Agentic user identity deleted")
"""
return self.on_lifecycle_notification(AgentLifecycleEvent.USERDELETED, **kwargs)
return self.on_agent_lifecycle_notification(AgentLifecycleEvent.USERDELETED, **kwargs)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on_user_deleted() is intended to route only deletion events, but on_agent_lifecycle_notification’s selector currently returns True for any known lifecycle event when a specific event is provided. This will cause deletion handlers to run for other lifecycle events; fix the selector to check equality between requested lifecycle_event and received activity.value_type (after normalization).

Copilot uses AI. Check for mistakes.
Comment on lines +239 to +243
def test_on_user_created_selector_matches_user_created_event(self):
"""Route selector from on_user_created() matches the user created event."""
app = MagicMock()
captured_selector = None

Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lifecycle route selector tests only assert positive matches (e.g., on_user_created matches USERCREATED) but don’t assert that the selector rejects other lifecycle event types. Add negative tests (and coverage for on_user_workload_onboarding/on_user_deleted selectors) to ensure the requested lifecycle event is actually used for filtering.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +35
assert not isinstance(AgentHandler, typing.TypeVar)

def test_route_handler_is_type_alias(self):
"""RouteHandler is a TypeAlias (not a TypeVar)."""
import typing

assert not isinstance(RouteHandler, typing.TypeVar)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typing.TypeVar is a factory function, not a type, so isinstance(AgentHandler, typing.TypeVar) will raise TypeError and fail the test. If you want to assert the alias isn’t a TypeVar instance, compare against type(typing.TypeVar("T")) (or use a more direct check via typing.get_origin/typing.get_args).

Suggested change
assert not isinstance(AgentHandler, typing.TypeVar)
def test_route_handler_is_type_alias(self):
"""RouteHandler is a TypeAlias (not a TypeVar)."""
import typing
assert not isinstance(RouteHandler, typing.TypeVar)
typevar_type = type(typing.TypeVar("T"))
assert not isinstance(AgentHandler, typevar_type)
def test_route_handler_is_type_alias(self):
"""RouteHandler is a TypeAlias (not a TypeVar)."""
import typing
typevar_type = type(typing.TypeVar("T"))
assert not isinstance(RouteHandler, typevar_type)

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +37
# TypeAlias values are just the underlying type, not TypeVar instances
assert not isinstance(AgentHandler, typing.TypeVar)

def test_route_handler_is_type_alias(self):
"""RouteHandler is a TypeAlias (not a TypeVar)."""
import typing

assert not isinstance(RouteHandler, typing.TypeVar)


Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typing.TypeVar is not valid as the second argument to isinstance, so this assertion will error at runtime. Use type(typing.TypeVar("T")) (or another safe runtime check) to verify RouteHandler isn’t a TypeVar instance.

Suggested change
# TypeAlias values are just the underlying type, not TypeVar instances
assert not isinstance(AgentHandler, typing.TypeVar)
def test_route_handler_is_type_alias(self):
"""RouteHandler is a TypeAlias (not a TypeVar)."""
import typing
assert not isinstance(RouteHandler, typing.TypeVar)
typevar_type = type(typing.TypeVar("T"))
# TypeAlias values are just the underlying type, not TypeVar instances
assert not isinstance(AgentHandler, typevar_type)
def test_route_handler_is_type_alias(self):
"""RouteHandler is a TypeAlias (not a TypeVar)."""
import typing
typevar_type = type(typing.TypeVar("T"))
assert not isinstance(RouteHandler, typevar_type)

Copilot uses AI. Check for mistakes.
print("New agentic user identity created")
"""
return self.on_lifecycle_notification(AgentLifecycleEvent.USERCREATED, **kwargs)
return self.on_agent_lifecycle_notification(AgentLifecycleEvent.USERCREATED, **kwargs)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on_user_created() delegates to on_agent_lifecycle_notification(AgentLifecycleEvent.USERCREATED, ...), but on_agent_lifecycle_notification’s route_selector currently does not compare the received activity.value_type to the requested lifecycle_event (it returns True for any known lifecycle event). This means user-created handlers will run for unrelated lifecycle events; update the selector to normalize and equality-check value_type against the requested event when lifecycle_event != "*".

Copilot uses AI. Check for mistakes.
Comment on lines +395 to 397
return self.on_agent_lifecycle_notification(
AgentLifecycleEvent.USERWORKLOADONBOARDINGUPDATED, **kwargs
)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because on_agent_lifecycle_notification’s selector doesn’t currently filter by the requested lifecycle_event, this on_user_workload_onboarding() registration will match any known lifecycle event (not just onboarding updates). Adjust on_agent_lifecycle_notification.route_selector to compare normalized context.activity.value_type to the requested event when not using the "*" wildcard.

Copilot uses AI. Check for mistakes.
@JimDaly
Copy link
Copy Markdown
Member

JimDaly commented Apr 29, 2026

After consulting with the learn platform team, closing this since it may not fix the problem.

@JimDaly JimDaly closed this Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix broken links in AgentNotification Class docs by improving code

3 participants