Skip to content

Python: Normalize SDK role enums to str when creating Messages - #7566

Open
westey (westey-m) wants to merge 2 commits into
microsoft:mainfrom
westey-m:python-messagerole-normalization
Open

Python: Normalize SDK role enums to str when creating Messages#7566
westey (westey-m) wants to merge 2 commits into
microsoft:mainfrom
westey-m:python-messagerole-normalization

Conversation

@westey-m

Copy link
Copy Markdown
Contributor

Motivation & Context

Running an agent as a Foundry hosted agent fails to persist session state when the conversation
came in over the Azure Responses API:

TypeError: Session state value at state.in_memory.messages[0].role has unsupported
serialized type 'MessageRole'

MessageRole in azure.ai.agentserver.responses.models is a str-subclass enum. The hosting
layer passes it straight through into Message(role=...), but Agent Framework types
Message.role as a plain str (Role = NewType("Role", str)), and durable session-state
serialization identifies scalars by exact type (type(value) in _STATE_SCALAR_TYPES) rather than
isinstance. A str subclass therefore misses the scalar path and is rejected as unserializable.

The result is that any hosted agent using a session store breaks as soon as it tries to save
history — the messages look like ordinary strings right up to the point of serialization. This
change makes the role a genuine str at the boundary where the SDK type enters the framework, so
sessions round-trip normally.

Description & Review Guide

  • What are the major changes?

    Each provider now normalizes the role to a plain str at the point it constructs a Message,
    via a small module-local helper (role.value if isinstance(role, Enum) else role):

    • foundry_hosting/_responses.py — applied at all five SDK→Message conversion sites across
      _item_to_message and _output_item_to_message.
    • azure-ai-search/_context_provider.py — applied to kb_msg.role when parsing a Knowledge
      Base retrieval response, preserving the existing or "assistant" fallback.

    Four regression tests are added, including one that asserts a converted message survives an
    AgentSession serialization round-trip.

  • What is the impact of these changes?

    Hosted agents can save and restore session state again. Not a breaking change: the normalized
    value compares equal to the enum member it replaces, so msg.role == "user" and any string
    handling behave exactly as before — only the runtime type narrows from a subclass to str,
    which is what the public type annotation already promised. Core is untouched.

    Note that foundry_hosting deliberately preserves MessageRole in workflow checkpoints
    through the _AZURE_RESPONSES_MESSAGE_ROLE_TYPE allowlist; that path is unaffected and its
    existing tests still pass.

  • What do you want reviewers to focus on?

    Whether normalizing per provider is the right layer, versus making core's serializer accept
    scalar subclasses. Both were considered; the per-provider fix was chosen so that Message.role
    actually holds the type it is annotated with, rather than papering over the mismatch at the
    serialization boundary. Also worth a look: whether other providers construct Message from an
    SDK role type and were missed — a survey suggested not (a2a already maps to literals,
    azure-cosmos-memory already unwraps defensively, MCP's Role is a Literal), but a second
    pair of eyes would help.

Related Issue

Related to #6361

This was found while building the harness samples tracked by that issue, but it is a
general-purpose fix in the provider packages and does not complete the sample/blog work — so it is
linked without a closing keyword intentionally, and #6361 should stay open. There is no other open
PR for this change.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Copilot AI balanced review requested due to automatic review settings August 7, 2026 09:25
@agent-framework-automation agent-framework-automation Bot added the python Usage: [Issues, PRs], Target: Python label Aug 7, 2026
@westey-m
westey (westey-m) marked this pull request as ready for review August 7, 2026 09:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Normalizes SDK role enums to plain strings before creating framework messages, preventing durable session serialization failures.

Changes:

  • Adds provider-local role normalization.
  • Applies normalization across Foundry and Azure AI Search conversion paths.
  • Adds regression tests for enum-backed roles and session serialization.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py Normalizes roles during SDK message conversion.
python/packages/foundry_hosting/tests/test_responses.py Tests role conversion and session behavior.
python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py Normalizes Knowledge Base response roles.
python/packages/azure-ai-search/tests/test_aisearch_context_provider.py Tests Knowledge Base enum-role conversion.

Comment thread python/packages/foundry_hosting/tests/test_responses.py Outdated
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/azure-ai-search/agent_framework_azure_ai_search
   _context_provider.py4121795%100–101, 126–129, 571, 650–651, 779–780, 878, 883, 888, 1000–1001, 1049
packages/foundry_hosting/agent_framework_foundry_hosting
   _responses.py9309190%210–213, 281, 286, 310, 365–366, 380, 383–384, 416–417, 451, 467, 529, 650, 664, 712, 772, 775, 781, 783, 860, 947–948, 1066, 1298, 1311, 1805–1807, 1809, 1856–1857, 1859–1860, 1862–1863, 1865–1866, 1871, 1880, 1883–1885, 1887, 1897, 1901, 1914, 2005–2006, 2021–2022, 2024, 2029–2033, 2035, 2042–2043, 2045–2046, 2052, 2054–2058, 2065, 2071, 2093, 2099, 2105, 2107, 2109–2112, 2120, 2122, 2183–2185, 2199–2200, 2203–2204
TOTAL45712425190% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
9298 36 💤 0 ❌ 0 🔥 2m 29s ⏱️

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agent Framework Review — Iteration 1

Completed passes: 5 | Result: No high-severity findings

Scope: full PR (1 commit(s)): b392d53f048e

Review passes

  • Correctness (gpt-5.6-sol) — No issues found in this pass.
  • Security Reliability (claude-opus-4.8) — No issues found in this pass.
  • Test Coverage (gpt-5.6-sol) — No issues found in this pass.
  • Failure Modes (claude-opus-4.8) — No issues found in this pass.
  • Design Approach (claude-opus-4.8) — No issues found in this pass.

AgentSession.to_dict() does not run the durable-state validator, so the
previous round-trip assertion passed even with an enum role left in place.
Persist through FileSessionStore.set/get instead, which validates state on
save, and assert the restored role's exact type.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3a5efc79-5c78-40b8-a1c5-c0f84e0795e1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants