Activity.text annotated `str` with `None` default: _remove_mentions poisons the persisted _SignInState (still in 1.3.0)
@kylerohn-msft ci sta già lavorando.
Dal 13/8/2026.
Valutazione
Questa issue non è ancora stata valutata.
Descrizione
Summary
Activity.text is annotated str but defaults to None:
# microsoft_agents/activity/activity.py
text: str = None
speak: str = None
Constructing is fine (Pydantic does not validate defaults), but validating a payload where text is null raises. Combined with AgentApplication._remove_mentions, this permanently breaks a user's conversation: the sign-in state is persisted with "text": null, and every subsequent turn fails reading it back.
Still present in 1.3.0.
Minimal reproduction — local, no Teams, no network
pip install microsoft-agents-hosting-core==1.3.0 microsoft-agents-activity==1.3.0
import logging
from microsoft_agents.activity import Activity
from microsoft_agents.hosting.core.app.oauth._sign_in_state import _SignInState
from microsoft_agents.hosting.core.turn_context import TurnContext
logging.disable(logging.CRITICAL) # the SDK logs the ValidationError with a traceback
# A message activity with attachments and NO `text` key — what a file upload looks
# like. `text` is None but UNSET, so exclude_unset would drop it: safe so far.
activity = Activity.model_validate({
"type": "message", "id": "1", "channelId": "msteams",
"from": {"id": "user-1"}, "recipient": {"id": "bot-1"},
"conversation": {"id": "conv-1"},
"attachments": [{"contentType": "application/octet-stream", "name": "a.docx"}],
})
print("1. incoming :", repr(activity.text), "set=", "text" in activity.model_fields_set)
# Exactly what AgentApplication._remove_mentions does for every type == message.
activity.text = TurnContext.remove_recipient_mention(activity)
print("2. after mention:", repr(activity.text), "set=", "text" in activity.model_fields_set)
state = _SignInState(active_handler_id="GRAPH")
state.continuation_activity = activity # authorization.py does this
stored = state.store_item_to_json()
print("3. persisted :", repr(stored["continuation_activity"].get("text", "<absent>")))
_SignInState.from_json_to_store_item(stored) # raises
Output on 1.3.0 / pydantic 2.13.4:
1. incoming : None set= False
2. after mention: None set= True
3. persisted : None
ValidationError: 1 validation error for _SignInState
continuation_activity.text
Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
Root cause — two defects that only bite together
1. The annotation. text: str = None means Pydantic v2 accepts a missing text (defaults are not validated) but rejects an explicit null on validation.
2. _remove_mentions assigns the result unconditionally.
# hosting/core/app/agent_application.py
def _remove_mentions(self, context: TurnContext):
if (self.options.remove_recipient_mention
and context.activity.type == ActivityTypes.message):
context.activity.text = context.remove_recipient_mention(context.activity)
TurnContext.remove_mention_text ends in return activity.text, so with no mention to strip it returns the original value — None. The assignment is accepted because Activity has validate_assignment = False, and it marks the field as SET. That is the part that hurts: _SignInState.store_item_to_json() dumps with exclude_unset=True, and an unset field would have been dropped.
So any type == message activity without a text key (a file upload, an Adaptive Card submit) becomes a poisoned continuation activity as soon as the sign-in flow stores it.
remove_recipient_mention defaults to True (app_options.py), so this is the default configuration.
Impact: it does not self-heal
The poisoned state is keyed auth:_SignInState:{channel_id}:{user_id}. MemoryStorage.read() calls _SignInState.from_json_to_store_item() and only catches AttributeError, so the ValidationError propagates. Since the read always raises, the flow can neither progress nor clear the state — the user is stuck on every subsequent turn until the process restarts, and re-poisons on the next unauthenticated upload.
The only operation that recovers a stuck user is Authorization.sign_out(), because _delete_sign_in_state() calls storage.delete([key]) without reading first.
Relationship to #414
#414 reported the same annotation defect and proposed the same fix. It was closed as completed by its author, who had traced his symptom to an unrelated Entra configuration problem (a missing botid in the token-exchange URL) — so the SDK defect itself was never addressed, and it still reproduces on 1.3.0.
Two corrections to that report, offered because they explain why it persists:
- Its stated mechanism — Teams sending
"text": null, which Pydantic parses and marks as set — cannot be the path.Activity.model_validate({"type": "message", "text": None})raises immediately, so such a payload would fail while deserialising the incoming activity, with1 validation error for Activity, not later withfor _SignInState. The null gets in through the unvalidated assignment above. - It states the bug is not reproducible locally. The snippet above reproduces it deterministically with no Teams deployment.
Proposed fix
text: Optional[str] = None
speak: Optional[str] = None
Same for any other field declared as a bare type with a None default (relates_to, expiration, and similar in activity.py).
A narrower alternative would be to make _remove_mentions skip the assignment when the result is None, but that leaves the annotation wrong for every other round-trip through storage.
Please avoid NonEmptyString for text: the natural workaround for this bug is to normalise None to "" before the SDK's turn pipeline runs, and a min_length constraint would break that (cf. #48, where an empty text failed validation).
Environment
microsoft-agents-activity/microsoft-agents-hosting-core1.3.0 (annotation also verified unchanged in 1.0.0, 1.1.0, 1.2.0; full chain reproduced on 1.0.0 and 1.3.0)pydantic2.13.4- Python 3.14 (repro is version-independent)
- Trigger in production: Teams file upload while the user is not yet authenticated
- Lingua principale
- Python
- Stelle
- 211
- Fork
- 90
- Merge medio
- 1g 9h
- PR unite (30g)
- 20
Guida per i contributori
Nessuna guida per i contributori indicizzata per questo repository
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di microsoft/Agents-for-python
-
microsoft/Agents-for-python#587 · 1 assegnatario ·
-
microsoft/Agents-for-python#572 · 1 assegnatario ·
-
microsoft/Agents-for-python#559 · 1 assegnatario ·
-
microsoft/Agents-for-python#550 · 1 assegnatario ·
-
A365 Enhancements - Python Aperta
Difficoltà 5/5 Più di una settimana Idoneità per principianti 15/100
microsoft/Agents-for-python#332 ·
Tutte le issue di microsoft/Agents-for-python
Issue simili
-
documentation help wanted
Difficoltà 2/5 1-3 ore Idoneità per principianti 90/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 90/100
simonw/sqlite-utils#872 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100