diff --git a/sdk/agentserver/azure-ai-agentserver-activity/CHANGELOG.md b/sdk/agentserver/azure-ai-agentserver-activity/CHANGELOG.md
new file mode 100644
index 000000000000..3611c24a4576
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/CHANGELOG.md
@@ -0,0 +1,17 @@
+# Release History
+
+## 1.0.0b1 (2026-06-09)
+
+### Features Added
+
+- Initial preview release of `azure-ai-agentserver-activity`.
+- `ActivityAgentServerHost` — Starlette-based host for Activity Protocol traffic.
+- `POST /activity/messages` and `POST /api/messages` endpoints with Foundry platform header contract.
+- Decorator API: `@app.activity(type)` and `@app.error` for zero-config handler registration.
+- Custom handler support: `ActivityAgentServerHost(handler=fn)` for full M365 SDK control.
+- Auto-initialization of M365 Agents SDK from environment variables (decorator mode).
+- MSAL auth patches for Foundry container MAIB auth (`apply_msal_patches()`).
+- Session ID resolution (query param → header → config → UUID fallback).
+- Activity ID and session ID sanitization for header injection defense.
+- OpenTelemetry distributed tracing and W3C Baggage propagation.
+- Error-source classification (`x-platform-error-source`) on all error responses.
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/LICENSE b/sdk/agentserver/azure-ai-agentserver-activity/LICENSE
new file mode 100644
index 000000000000..4c3581d3b052
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) Microsoft Corporation.
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/MANIFEST.in b/sdk/agentserver/azure-ai-agentserver-activity/MANIFEST.in
new file mode 100644
index 000000000000..61da8b18f0e1
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/MANIFEST.in
@@ -0,0 +1,8 @@
+include *.md
+include LICENSE
+recursive-include tests *.py
+recursive-include samples *.py *.md
+include azure/__init__.py
+include azure/ai/__init__.py
+include azure/ai/agentserver/__init__.py
+include azure/ai/agentserver/activity/py.typed
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/README.md b/sdk/agentserver/azure-ai-agentserver-activity/README.md
new file mode 100644
index 000000000000..4805e33a2811
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/README.md
@@ -0,0 +1,82 @@
+# Azure AI Agent Server Activity client library for Python
+
+The `azure-ai-agentserver-activity` package provides the Foundry container integration host for Activity Protocol traffic in Azure AI Hosted Agent containers. It plugs into [`azure-ai-agentserver-core`](https://pypi.org/project/azure-ai-agentserver-core/) and exposes a protocol endpoint with Foundry-required header, tracing, and error behavior.
+
+## Getting started
+
+### Install the package
+
+```bash
+pip install azure-ai-agentserver-activity
+```
+
+### Prerequisites
+
+- Python 3.10 or later
+
+## Key concepts
+
+### ActivityAgentServerHost
+
+`ActivityAgentServerHost` is an `AgentServerHost` subclass for Activity Protocol traffic. It provides:
+
+- `POST /activity/messages` for inbound activities.
+
+### Usage patterns
+
+**Decorator-based (recommended)** — zero SDK wiring:
+
+```python
+from azure.ai.agentserver.activity import ActivityAgentServerHost
+
+app = ActivityAgentServerHost()
+
+@app.activity("message")
+async def on_message(context, state):
+ await context.send_activity(f"Echo: {context.activity.text}")
+
+@app.error
+async def on_error(context, error):
+ await context.send_activity(f"Error: {error}")
+
+app.run()
+```
+
+**Custom handler** — full control over the M365 SDK pipeline:
+
+```python
+from azure.ai.agentserver.activity import ActivityAgentServerHost
+
+async def handle(request):
+ activity = request.state.activity # parsed dict
+ # Custom processing...
+ return Response(status_code=202)
+
+app = ActivityAgentServerHost(handler=handle)
+app.run()
+```
+
+### Request header contract
+
+`POST /activity/messages` consumes:
+
+- `x-agent-session-id` (preferred session source)
+- `x-agent-conversation-id`
+- `x-agent-user-isolation-key` and `x-agent-chat-isolation-key`
+- `traceparent`, `tracestate`, and `baggage`
+
+### Public API
+
+- `ActivityAgentServerHost` — the host class
+- `apply_msal_patches()` — patches M365 SDK MSAL auth for Foundry containers (UserManagedIdentity with fmi_path)
+
+## Samples
+
+See [samples/README.md](samples/README.md) for runnable scenarios:
+
+- `simple_activity_agent` — echo bot with welcome, invoke, installation events
+- `streaming_activity_agent` — Azure OpenAI streaming via `context.streaming_response`
+- `cards_activity_agent` — Adaptive Cards, Hero, Thumbnail, Receipt cards
+- `auto_signin_activity_agent` — OAuth auto sign-in with Graph and GitHub
+- `semantic_kernel_activity_agent` — Semantic Kernel agent with tools and multi-turn
+- `suggested_actions_activity_agent` — quick-reply buttons
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/azure/__init__.py b/sdk/agentserver/azure-ai-agentserver-activity/azure/__init__.py
new file mode 100644
index 000000000000..8db66d3d0f0f
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/azure/__init__.py
@@ -0,0 +1 @@
+__path__ = __import__("pkgutil").extend_path(__path__, __name__)
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/__init__.py b/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/__init__.py
new file mode 100644
index 000000000000..8db66d3d0f0f
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/__init__.py
@@ -0,0 +1 @@
+__path__ = __import__("pkgutil").extend_path(__path__, __name__)
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/__init__.py b/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/__init__.py
new file mode 100644
index 000000000000..8db66d3d0f0f
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/__init__.py
@@ -0,0 +1 @@
+__path__ = __import__("pkgutil").extend_path(__path__, __name__)
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/activity/__init__.py b/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/activity/__init__.py
new file mode 100644
index 000000000000..eceab9b77da9
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/activity/__init__.py
@@ -0,0 +1,40 @@
+# ---------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# ---------------------------------------------------------
+"""Activity protocol host for Azure AI Hosted Agents.
+
+This package provides an activity protocol host as a subclass of
+:class:`~azure.ai.agentserver.core.AgentServerHost`.
+
+Decorator-based usage (recommended)::
+
+ from azure.ai.agentserver.activity import ActivityAgentServerHost
+
+ app = ActivityAgentServerHost()
+
+ @app.activity("message")
+ async def on_message(context, state):
+ await context.send_activity(f"Echo: {context.activity.text}")
+
+ app.run()
+
+Custom handler usage::
+
+ from azure.ai.agentserver.activity import ActivityAgentServerHost
+
+ async def handle(request):
+ activity = request.state.activity
+ # Custom processing...
+ return Response(status_code=202)
+
+ app = ActivityAgentServerHost(handler=handle)
+ app.run()
+"""
+__path__ = __import__("pkgutil").extend_path(__path__, __name__)
+
+from ._activity import ActivityAgentServerHost
+from ._m365_bridge import _apply_msal_patches as apply_msal_patches
+from ._version import VERSION
+
+__all__ = ["ActivityAgentServerHost", "apply_msal_patches"]
+__version__ = VERSION
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/activity/_activity.py b/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/activity/_activity.py
new file mode 100644
index 000000000000..05423ce86336
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/activity/_activity.py
@@ -0,0 +1,447 @@
+# ---------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# ---------------------------------------------------------
+"""Activity protocol host for Azure AI Hosted Agents.
+
+Provides the activity protocol endpoint as a
+:class:`~azure.ai.agentserver.core.AgentServerHost` subclass.
+"""
+
+import contextvars
+import inspect
+import logging
+import os
+import re as _re
+import threading
+import uuid
+from collections.abc import Awaitable, Callable
+from typing import Any, Optional
+
+from opentelemetry import baggage as _otel_baggage, context as _otel_context, trace as _otel_trace
+from opentelemetry.trace import Status as _OtelStatus
+from opentelemetry.trace import StatusCode as _OtelStatusCode
+from starlette.requests import Request
+from starlette.responses import JSONResponse, Response
+from starlette.routing import Route
+
+from azure.ai.agentserver.core import AgentServerHost, create_error_response
+from azure.ai.agentserver.core._platform_headers import (
+ CHAT_ISOLATION_KEY,
+ ERROR_DETAIL,
+ ERROR_SOURCE,
+ MAX_ERROR_DETAIL_LENGTH,
+ PLATFORM_ERROR_TAG,
+ USER_ISOLATION_KEY,
+)
+
+from ._constants import ActivityConstants
+
+logger = logging.getLogger("azure.ai.agentserver")
+
+_ERROR_SOURCE_UPSTREAM: str = "upstream"
+_ERROR_SOURCE_PLATFORM: str = "platform"
+
+
+_session_id_var: contextvars.ContextVar[str] = contextvars.ContextVar("activity_session_id", default="")
+_user_isolation_key_var: contextvars.ContextVar[str] = contextvars.ContextVar("activity_user_isolation_key", default="")
+_chat_isolation_key_var: contextvars.ContextVar[str] = contextvars.ContextVar("activity_chat_isolation_key", default="")
+_protocol_var: contextvars.ContextVar[str] = contextvars.ContextVar("activity_protocol", default=ActivityConstants.PROTOCOL)
+
+
+class _ActivityLogFilter(logging.Filter):
+ """Attach per-turn structured scope fields to activity log records."""
+
+ def filter(self, record: logging.LogRecord) -> bool:
+ record.SessionId = _session_id_var.get("") # type: ignore[attr-defined]
+ record.UserIsolationKey = _user_isolation_key_var.get("") # type: ignore[attr-defined]
+ record.ChatIsolationKey = _chat_isolation_key_var.get("") # type: ignore[attr-defined]
+ record.Protocol = _protocol_var.get(ActivityConstants.PROTOCOL) # type: ignore[attr-defined]
+ return True
+
+
+_log_filter_lock = threading.Lock()
+_log_filter_installed = False
+
+
+def _ensure_log_filter() -> None:
+ """Install activity log scope filter once."""
+ global _log_filter_installed # pylint: disable=global-statement
+ if _log_filter_installed:
+ return
+ with _log_filter_lock:
+ if _log_filter_installed:
+ return
+ logger.addFilter(_ActivityLogFilter())
+ _log_filter_installed = True
+
+
+def _apply_error_source_headers(
+ headers: dict[str, str],
+ error_source: str,
+ error_detail: Optional[str] = None,
+) -> dict[str, str]:
+ """Return a new dict with error source classification headers merged in.
+
+ :param headers: Base headers to merge into.
+ :param error_source: The error source value (user/platform/upstream).
+ :param error_detail: Optional detail string for platform errors.
+ :return: A new dict containing the original headers plus error source headers.
+ """
+ merged = {**headers, ERROR_SOURCE: error_source}
+ if error_detail:
+ merged[ERROR_DETAIL] = error_detail
+ return merged
+
+
+_SAFE_ID_PATTERN = _re.compile(r"^[a-zA-Z0-9\-_.:]+$")
+_MAX_ID_LENGTH = 256
+
+
+def _sanitize_id(value: str) -> str:
+ """Validate an ID for safe use in HTTP headers and logs.
+
+ Accepts alphanumeric characters plus ``-_.:`` up to 256 characters.
+ Returns a fallback UUID for invalid or oversized values.
+ """
+ if not value or len(value) > _MAX_ID_LENGTH or not _SAFE_ID_PATTERN.match(value):
+ return str(uuid.uuid4())
+ return value
+
+
+def _classify_error(exc: BaseException) -> tuple[str, Optional[str]]:
+ """Classify an exception: platform-tagged -> (platform, detail), else -> (upstream, None)."""
+ if getattr(exc, PLATFORM_ERROR_TAG, False) is True:
+ detail = f"{type(exc).__name__}: {exc}"
+ if len(detail) > MAX_ERROR_DETAIL_LENGTH:
+ suffix = "...[truncated]"
+ detail = detail[: MAX_ERROR_DETAIL_LENGTH - len(suffix)] + suffix
+ return _ERROR_SOURCE_PLATFORM, detail
+ return _ERROR_SOURCE_UPSTREAM, None
+
+
+class ActivityAgentServerHost(AgentServerHost):
+ """Activity protocol host for Azure AI Hosted Agents.
+
+ A :class:`~azure.ai.agentserver.core.AgentServerHost` subclass that adds
+ the activity protocol endpoint at ``POST /activity/messages``. Use the decorator
+ methods to register M365 SDK activity handlers, or pass a custom
+ ``handler`` callable for full control.
+
+ When no ``handler`` is provided, the M365 Agents SDK is auto-initialized
+ from environment variables.
+
+ Usage::
+
+ from azure.ai.agentserver.activity import ActivityAgentServerHost
+
+ app = ActivityAgentServerHost()
+
+ @app.activity("message")
+ async def on_message(context, state):
+ await context.send_activity(f"Echo: {context.activity.text}")
+
+ app.run()
+
+ :param handler: Optional custom handler function. When provided, the
+ decorator API is bypassed and the handler receives the raw Starlette
+ ``Request`` with ``request.state.activity`` set to the parsed
+ activity dict.
+ :type handler: Optional[Callable[[Request], Awaitable[Response]]]
+ """
+
+ _INSTRUMENTATION_SCOPE = "Azure.AI.AgentServer.Activity"
+
+ def __init__(
+ self,
+ *,
+ handler: Optional[Callable[[Request], Awaitable[Response]]] = None,
+ **kwargs: Any,
+ ) -> None:
+ # Initialize default env vars before bridge/app setup.
+ self._initialize_default_env_vars()
+
+ if handler is not None and not inspect.iscoroutinefunction(handler):
+ raise TypeError(
+ f"handler must be an async function, got {type(handler).__name__}. "
+ "Use 'async def' to define your handler."
+ )
+
+ # explicit handler: user owns the processing pipeline
+ # no handler: use built-in M365 bridge + decorators
+ self._handler = handler
+
+ activity_routes: list[Any] = [
+ Route(
+ "/activity/messages",
+ self._create_activity_endpoint,
+ methods=["POST"],
+ name="create_activity",
+ ),
+ Route(
+ "/api/messages",
+ self._create_activity_endpoint,
+ methods=["POST"],
+ name="create_activity_api_messages",
+ ),
+ ]
+
+ existing = list(kwargs.pop("routes", None) or [])
+ super().__init__(routes=existing + activity_routes, **kwargs)
+
+ # ------------------------------------------------------------------
+ # Handler decorators
+ # ------------------------------------------------------------------
+
+ def _initialize_default_env_vars(self) -> None:
+ """Initialize connection-related env vars used by the M365 SDK.
+
+ Precedence order is:
+ 1. Existing explicit connection env vars
+ 2. Values derived from Foundry-native env vars
+ 3. Static defaults for non-critical options
+ """
+
+ def _get_nonempty(name: str) -> str:
+ return os.environ.get(name, "").strip()
+
+ def _set_if_missing(name: str, value: str) -> None:
+ if value and not _get_nonempty(name):
+ os.environ[name] = value
+
+ defaults = {
+ "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__AUTHTYPE": "UserManagedIdentity",
+ "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES__0": "5a807f24-c9de-44ee-a3a7-329e88a00ffc/.default",
+ "CONNECTIONSMAP__0__SERVICEURL": "*",
+ "CONNECTIONSMAP__0__CONNECTION": "SERVICE_CONNECTION",
+ }
+ for key, value in defaults.items():
+ _set_if_missing(key, value)
+
+ foundry_blueprint_client_id = _get_nonempty("FOUNDRY_AGENT_BLUEPRINT_CLIENT_ID")
+ foundry_tenant_id = _get_nonempty("FOUNDRY_AGENT_TENANT_ID")
+
+ _set_if_missing(
+ "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID",
+ foundry_blueprint_client_id,
+ )
+ _set_if_missing(
+ "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID",
+ foundry_tenant_id,
+ )
+ _set_if_missing(
+ "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__AUTHORITY",
+ f"https://login.microsoftonline.com/{foundry_tenant_id}" if foundry_tenant_id else "",
+ )
+
+ def activity(self, activity_type: str):
+ """Register a handler for a specific activity type.
+
+ Usage::
+
+ @app.activity("message")
+ async def on_message(context, state):
+ await context.send_activity(f"Echo: {context.activity.text}")
+
+ :param activity_type: The activity type to handle (e.g., "message", "invoke").
+ :type activity_type: str
+ """
+ def decorator(fn):
+ from ._m365_bridge import _get_or_create_lazy_app
+ lazy_app = _get_or_create_lazy_app()
+ lazy_app.activity(activity_type)(fn)
+ # Wire up the bridge handler if not already set
+ if self._handler is None:
+ from ._m365_bridge import create_bridge_handler
+ self._handler = create_bridge_handler
+ return fn
+ return decorator
+
+ def error(self, fn):
+ """Register an error handler.
+
+ Usage::
+
+ @app.error
+ async def on_error(context, error):
+ await context.send_activity(f"Error: {error}")
+
+ :param fn: Async error handler function.
+ """
+ from ._m365_bridge import _get_or_create_lazy_app
+ lazy_app = _get_or_create_lazy_app()
+ lazy_app.error(fn)
+ if self._handler is None:
+ from ._m365_bridge import create_bridge_handler
+ self._handler = create_bridge_handler
+ return fn
+
+ def _resolve_session_id(self, request: Request) -> str:
+ query_session_id = request.query_params.get("agent_session_id")
+ if query_session_id and query_session_id.strip():
+ return query_session_id.strip()
+
+ header_id = request.headers.get(ActivityConstants.SESSION_ID_HEADER)
+ if header_id and header_id.strip():
+ return header_id.strip()
+
+ if self.config.session_id and self.config.session_id.strip():
+ return self.config.session_id.strip()
+
+ return str(uuid.uuid4())
+
+ def _build_span_name(self) -> str:
+ agent_name = (self.config.agent_name or "").strip()
+ agent_version = (self.config.agent_version or "").strip()
+ if agent_name and agent_version:
+ return f"handle_activity {agent_name}:{agent_version}"
+ if agent_name:
+ return f"handle_activity {agent_name}"
+ return "handle_activity"
+
+ def _apply_trace_tags(self, span: Any, session_id: str) -> None:
+ agent_name = (self.config.agent_name or "").strip()
+ agent_version = (self.config.agent_version or "").strip()
+ if agent_name and agent_version:
+ agent_id = f"{agent_name}:{agent_version}"
+ elif agent_name:
+ agent_id = agent_name
+ else:
+ agent_id = ""
+
+ span.set_attribute("service.name", "azure.ai.agentserver")
+ span.set_attribute("gen_ai.provider.name", "AzureAI Hosted Agents")
+ span.set_attribute("gen_ai.operation.name", "handle_activity")
+ span.set_attribute("gen_ai.agent.id", agent_id)
+ if agent_name:
+ span.set_attribute("gen_ai.agent.name", agent_name)
+ if agent_version:
+ span.set_attribute("gen_ai.agent.version", agent_version)
+ if session_id:
+ span.set_attribute("gen_ai.conversation.id", session_id)
+
+ span.set_attribute(ActivityConstants.ATTR_SPAN_SESSION_ID, session_id or "")
+ span.set_attribute(ActivityConstants.ATTR_SPAN_PROTOCOL, ActivityConstants.PROTOCOL)
+ span.set_attribute("microsoft.foundry.project.id", os.environ.get("FOUNDRY_PROJECT_ARM_ID", ""))
+
+ def _add_required_response_headers(self, response: Response, session_id: str) -> None:
+ response.headers[ActivityConstants.SESSION_ID_HEADER] = session_id
+
+ async def _create_activity_endpoint(self, request: Request) -> Response:
+ """Handle inbound POST to /activity/messages or /api/messages."""
+ logger.debug(
+ "Activity endpoint hit | method=%s | path=%s | query=%s | content-type=%s",
+ request.method, request.url.path, str(request.query_params),
+ request.headers.get("content-type", ""),
+ )
+ logger.debug("Activity endpoint headers: %s", dict(request.headers))
+
+ inbound_conversation_id = request.headers.get(ActivityConstants.CONVERSATION_ID_HEADER, "")
+ inbound_user_isolation_key = request.headers.get(USER_ISOLATION_KEY, "")
+ inbound_chat_isolation_key = request.headers.get(CHAT_ISOLATION_KEY, "")
+
+ try:
+ payload = await request.json()
+ except Exception: # pylint: disable=broad-exception-caught
+ response = create_error_response(
+ "invalid_request",
+ "Request body must be valid JSON",
+ status_code=400,
+ headers=_apply_error_source_headers({}, _ERROR_SOURCE_UPSTREAM),
+ )
+ self._add_required_response_headers(response, "")
+ return response
+
+ if not isinstance(payload, dict):
+ response = create_error_response(
+ "invalid_request",
+ "Activity payload must be a JSON object",
+ status_code=400,
+ headers=_apply_error_source_headers({}, _ERROR_SOURCE_UPSTREAM),
+ )
+ self._add_required_response_headers(response, "")
+ return response
+
+ activity_id = payload.get("id", "") if isinstance(payload.get("id"), str) else ""
+ if not activity_id.strip():
+ activity_id = str(uuid.uuid4())
+ else:
+ activity_id = _sanitize_id(activity_id)
+
+ session_id = _sanitize_id(self._resolve_session_id(request))
+
+ logger.debug(
+ "Activity parsed | type=%s | activity_id=%s | session_id=%s | text=%s | serviceUrl=%s | channelId=%s",
+ payload.get("type", "?"), activity_id, session_id,
+ str(payload.get("text", ""))[:100], payload.get("serviceUrl", ""), payload.get("channelId", ""),
+ )
+
+ request.state.activity = payload
+ request.state.activity_id = activity_id
+ request.state.session_id = session_id
+ request.state.user_isolation_key = inbound_user_isolation_key
+ request.state.chat_isolation_key = inbound_chat_isolation_key
+
+ _ensure_log_filter()
+ session_token = _session_id_var.set(session_id)
+ user_token = _user_isolation_key_var.set(inbound_user_isolation_key)
+ chat_token = _chat_isolation_key_var.set(inbound_chat_isolation_key)
+ protocol_token = _protocol_var.set(ActivityConstants.PROTOCOL)
+
+ tracer = _otel_trace.get_tracer(self._INSTRUMENTATION_SCOPE)
+ baggage_ctx = _otel_context.get_current()
+ baggage_ctx = _otel_baggage.set_baggage(
+ "azure.ai.agentserver.session_id", session_id or "", context=baggage_ctx
+ )
+ baggage_ctx = _otel_baggage.set_baggage(
+ "azure.ai.agentserver.protocol", ActivityConstants.PROTOCOL, context=baggage_ctx
+ )
+ baggage_token = _otel_context.attach(baggage_ctx)
+
+ try:
+ with tracer.start_as_current_span(self._build_span_name()) as span:
+ self._apply_trace_tags(span, session_id)
+ try:
+ if self._handler is None:
+ raise NotImplementedError(
+ "No activity handler registered. Use the @app.activity() decorator "
+ "or pass a handler= callable to ActivityAgentServerHost()."
+ )
+ response = await self._handler(request)
+
+ response.headers[ActivityConstants.ACTIVITY_ID_HEADER] = activity_id
+ self._add_required_response_headers(response, session_id)
+ return response
+ except Exception as exc: # pylint: disable=broad-exception-caught
+ error_source, error_detail = _classify_error(exc)
+ logger.error("Error processing activity %s: %s", activity_id, exc, exc_info=True)
+
+ # Record error on the span (still inside `with` block)
+ if span.is_recording():
+ span.set_status(_OtelStatus(_OtelStatusCode.ERROR, str(exc)))
+ span.record_exception(exc)
+ span.set_attribute("error.type", type(exc).__name__)
+ span.set_attribute("otel.status.description", str(exc))
+ span.set_attribute(ActivityConstants.ATTR_SPAN_ERROR_CODE, type(exc).__name__)
+ span.set_attribute(ActivityConstants.ATTR_SPAN_ERROR_MESSAGE, str(exc))
+
+ response = create_error_response(
+ "internal_error",
+ "Internal server error",
+ status_code=500,
+ headers=_apply_error_source_headers(
+ {ActivityConstants.ACTIVITY_ID_HEADER: activity_id},
+ error_source,
+ error_detail,
+ ),
+ )
+ self._add_required_response_headers(response, session_id)
+ return response
+ finally:
+ _session_id_var.reset(session_token)
+ _user_isolation_key_var.reset(user_token)
+ _chat_isolation_key_var.reset(chat_token)
+ _protocol_var.reset(protocol_token)
+ try:
+ _otel_context.detach(baggage_token)
+ except ValueError:
+ pass
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/activity/_constants.py b/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/activity/_constants.py
new file mode 100644
index 000000000000..49b47565d763
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/activity/_constants.py
@@ -0,0 +1,26 @@
+# ---------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# ---------------------------------------------------------
+from azure.ai.agentserver.core._platform_headers import SESSION_ID as _SESSION_ID # pylint: disable=import-error,no-name-in-module
+
+
+class ActivityConstants:
+ """Activity protocol constants.
+
+ Protocol-specific headers and telemetry attribute keys for activity
+ endpoint handling. Cross-cutting header names (for example session ID)
+ are imported from :mod:`azure.ai.agentserver.core._platform_headers`.
+ """
+
+ PROTOCOL = "activity"
+
+ # Request / response headers
+ ACTIVITY_ID_HEADER = "x-agent-activity-id"
+ SESSION_ID_HEADER = _SESSION_ID
+ CONVERSATION_ID_HEADER = "x-agent-conversation-id"
+
+ # Span attribute keys
+ ATTR_SPAN_SESSION_ID = "azure.ai.agentserver.activity.session_id"
+ ATTR_SPAN_PROTOCOL = "azure.ai.agentserver.activity.protocol"
+ ATTR_SPAN_ERROR_CODE = "azure.ai.agentserver.activity.error.code"
+ ATTR_SPAN_ERROR_MESSAGE = "azure.ai.agentserver.activity.error.message"
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/activity/_m365_bridge.py b/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/activity/_m365_bridge.py
new file mode 100644
index 000000000000..fa13f1a8243f
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/activity/_m365_bridge.py
@@ -0,0 +1,267 @@
+# ---------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# ---------------------------------------------------------
+"""M365 Agents SDK bridge for the Activity protocol host.
+
+Provides auto-initialization of the M365 Agents SDK stack from
+environment variables, MSAL auth patches for Foundry containers,
+and a bridge function that converts activity dicts into M365 SDK
+turn processing.
+
+This module is used internally by :class:`ActivityAgentServerHost`
+when decorator-based handlers are registered. Users who pass their
+own ``handler`` callable bypass this module entirely.
+"""
+
+from __future__ import annotations
+
+import logging
+import os
+from typing import Any, Optional
+
+from starlette.requests import Request
+from starlette.responses import JSONResponse, Response
+
+logger = logging.getLogger("azure.ai.agentserver")
+
+# Lazy imports — these are only needed when the bridge is actually used.
+# This avoids hard dependency failures if M365 SDK isn't installed.
+_m365_initialized = False
+_adapter = None
+_agent_app = None
+_connection_manager = None
+
+
+def _apply_msal_patches() -> None:
+ """Apply MSAL auth patches for Foundry container MAIB auth.
+
+ When AUTH_TYPE is UserManagedIdentity, the stock MsalAuth uses
+ ManagedIdentityClient which doesn't support fmi_path. This patch
+ replaces get_agentic_application_token with DefaultAzureCredential.
+ """
+ try:
+ from microsoft_agents.authentication.msal.msal_auth import MsalAuth
+ except ImportError:
+ logger.debug("microsoft-agents-authentication-msal not installed; skipping MSAL patches")
+ return
+
+ _PATCH_FLAG = "_activity_sdk_msal_patched"
+ if getattr(MsalAuth, _PATCH_FLAG, False):
+ return
+
+ async def _get_token_via_dac(self, tenant_id: str, agent_app_instance_id: str) -> Optional[str]:
+ from azure.identity.aio import DefaultAzureCredential
+
+ if not agent_app_instance_id:
+ from microsoft_agents.authentication.msal.errors import authentication_errors
+ raise ValueError(str(authentication_errors.AgentApplicationInstanceIdRequired))
+
+ logger.info(
+ "[activity-bridge] Acquiring agentic application token via "
+ "DefaultAzureCredential for agent_app_instance_id=%s",
+ agent_app_instance_id,
+ )
+
+ client_id = getattr(self._msal_configuration, "CLIENT_ID", None)
+ credential_kwargs: dict[str, Any] = {
+ "identity_config": {"fmi_path": agent_app_instance_id},
+ }
+ if client_id:
+ credential_kwargs["managed_identity_client_id"] = client_id
+
+ credential = DefaultAzureCredential(**credential_kwargs)
+ try:
+ token = await credential.get_token("api://AzureADTokenExchange/.default")
+ return token.token
+ except Exception:
+ logger.exception(
+ "Failed to acquire agentic application token for agent_app_instance_id=%s",
+ agent_app_instance_id,
+ )
+ return None
+ finally:
+ try:
+ await credential.close()
+ except Exception:
+ pass
+
+ MsalAuth.get_agentic_application_token = _get_token_via_dac
+ setattr(MsalAuth, _PATCH_FLAG, True)
+ logger.info("Patched MsalAuth.get_agentic_application_token → DefaultAzureCredential")
+
+
+def _ensure_m365_initialized():
+ """Lazily initialize the M365 Agents SDK from environment variables.
+
+ Called on first request when decorators are used. Idempotent.
+ """
+ global _m365_initialized, _adapter, _agent_app, _connection_manager
+
+ if _m365_initialized:
+ return _agent_app, _adapter
+
+ try:
+ from microsoft_agents.activity import Activity, load_configuration_from_env
+ from microsoft_agents.authentication.msal import MsalConnectionManager
+ from microsoft_agents.hosting.core import (
+ AgentApplication,
+ Authorization,
+ HttpAdapterBase,
+ MemoryStorage,
+ RestChannelServiceClientFactory,
+ TurnState,
+ )
+ except ImportError as exc:
+ raise ImportError(
+ "Activity decorator handlers require the M365 Agents SDK. "
+ "Install: pip install microsoft-agents-hosting-core "
+ "microsoft-agents-authentication-msal microsoft-agents-activity azure-identity"
+ ) from exc
+
+ # Apply MSAL patches before any MsalConnectionManager is created
+ _apply_msal_patches()
+
+ logger.info("Initializing M365 Agents SDK...")
+ config = load_configuration_from_env(os.environ)
+ storage = MemoryStorage()
+ _connection_manager = MsalConnectionManager(**config)
+ client_factory = RestChannelServiceClientFactory(_connection_manager)
+ _adapter = HttpAdapterBase(channel_service_client_factory=client_factory)
+ authorization = Authorization(storage, _connection_manager, **config)
+ _agent_app = AgentApplication[TurnState](
+ storage=storage,
+ adapter=_adapter,
+ authorization=authorization,
+ **config,
+ )
+ _m365_initialized = True
+ logger.info("M365 Agents SDK initialized successfully.")
+ return _agent_app, _adapter
+
+
+async def create_bridge_handler(request: Request) -> Response:
+ """Built-in bridge handler for decorator-based agents.
+
+ Converts the activity dict (set by ActivityAgentServerHost on
+ request.state) into an M365 SDK Activity and processes it through
+ the AgentApplication turn pipeline.
+
+ On first call, initializes the M365 SDK and replays any pending
+ handler registrations captured by the lazy proxy.
+ """
+ from microsoft_agents.activity import Activity
+ from microsoft_agents.hosting.core import ClaimsIdentity
+
+ global _lazy_agent_app
+ agent_app, adapter = _ensure_m365_initialized()
+
+ # Replay pending decorator registrations onto the real AgentApplication
+ if _lazy_agent_app is not None and not _lazy_agent_app._replayed:
+ _lazy_agent_app._replay_on(agent_app)
+
+ activity_dict = request.state.activity
+ activity_type = activity_dict.get("type", "unknown")
+ session_id = request.state.session_id
+
+ logger.info(
+ "Bridge: activity received | type=%s | session=%s",
+ activity_type, session_id,
+ )
+ logger.debug(
+ "Bridge: activity details | conversation=%s | serviceUrl=%s | channelId=%s | from=%s",
+ activity_dict.get("conversation", {}).get("id", "?") if isinstance(activity_dict.get("conversation"), dict) else "?",
+ activity_dict.get("serviceUrl", ""),
+ activity_dict.get("channelId", ""),
+ activity_dict.get("from", {}).get("id", "?") if isinstance(activity_dict.get("from"), dict) else "?",
+ )
+
+ activity = Activity.model_validate(activity_dict)
+
+ if not activity.type or not activity.conversation or not activity.conversation.id:
+ logger.warning(
+ "Bridge: rejecting activity with 400 | type=%s | has_conversation=%s | conversation_id=%s",
+ activity.type, activity.conversation is not None,
+ activity.conversation.id if activity.conversation else "None",
+ )
+ return JSONResponse(
+ status_code=400,
+ content={"error": {"code": "invalid_request", "message": "Activity must have type and conversation.id"}},
+ )
+
+ claims = ClaimsIdentity({}, is_authenticated=False, authentication_type="Anonymous")
+
+ try:
+ invoke_response = await adapter.process_activity(claims, activity, agent_app.on_turn)
+ except PermissionError:
+ logger.error("Permission denied processing activity | type=%s", activity_type)
+ return Response(status_code=401)
+ except TypeError as exc:
+ logger.warning("TypeError processing activity (likely missing serviceUrl) | type=%s | error=%s", activity_type, exc)
+ return Response(status_code=202)
+ except Exception:
+ # Re-raise so the outer _create_activity_endpoint can classify
+ # the error and return 500 with proper x-platform-error-source.
+ raise
+
+ if activity.type == "invoke" or activity.delivery_mode == "expectReplies":
+ if invoke_response is not None:
+ return JSONResponse(content=invoke_response.body, status_code=invoke_response.status)
+ return JSONResponse(content={}, status_code=200)
+
+ return Response(status_code=202)
+
+
+class _LazyAgentApp:
+ """Proxy that defers AgentApplication access until first request."""
+
+ def __init__(self):
+ self._pending_registrations: list = []
+ self._replayed = False
+
+ def activity(self, activity_type: str):
+ """Capture an activity handler registration for later replay."""
+ def decorator(fn):
+ self._pending_registrations.append(("activity", activity_type, fn))
+ return fn
+ return decorator
+
+ def error(self, fn):
+ """Capture an error handler registration for later replay."""
+ self._pending_registrations.append(("error", None, fn))
+ return fn
+
+ def _replay_on(self, agent_app):
+ """Replay all captured registrations onto the real AgentApplication.
+
+ Idempotent — only replays once even if called concurrently.
+ """
+ if self._replayed:
+ return
+ self._replayed = True
+ for kind, arg, fn in self._pending_registrations:
+ if kind == "activity":
+ agent_app.activity(arg)(fn)
+ elif kind == "error":
+ agent_app.error(fn)
+ self._pending_registrations.clear()
+
+
+# Module-level lazy proxy — shared across all decorator calls
+_lazy_agent_app: Optional[_LazyAgentApp] = None
+
+
+def _get_or_create_lazy_app() -> _LazyAgentApp:
+ global _lazy_agent_app
+ if _lazy_agent_app is None:
+ _lazy_agent_app = _LazyAgentApp()
+ return _lazy_agent_app
+
+
+def _reset_for_testing() -> None:
+ """Reset all module-level state. For test isolation only."""
+ global _m365_initialized, _adapter, _agent_app, _connection_manager, _lazy_agent_app
+ _m365_initialized = False
+ _adapter = None
+ _agent_app = None
+ _connection_manager = None
+ _lazy_agent_app = None
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/activity/_version.py b/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/activity/_version.py
new file mode 100644
index 000000000000..67d209a8cafd
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/activity/_version.py
@@ -0,0 +1,5 @@
+# ---------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# ---------------------------------------------------------
+
+VERSION = "1.0.0b1"
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/activity/py.typed b/sdk/agentserver/azure-ai-agentserver-activity/azure/ai/agentserver/activity/py.typed
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/cspell.json b/sdk/agentserver/azure-ai-agentserver-activity/cspell.json
new file mode 100644
index 000000000000..c115521f62ea
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/cspell.json
@@ -0,0 +1,15 @@
+{
+ "ignoreWords": [
+ "agentserver",
+ "openapi",
+ "paramtype",
+ "rtype",
+ "starlette"
+ ],
+ "ignorePaths": [
+ "*.csv",
+ "*.json",
+ "*.rst",
+ "samples/**"
+ ]
+}
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/dev_requirements.txt b/sdk/agentserver/azure-ai-agentserver-activity/dev_requirements.txt
new file mode 100644
index 000000000000..25c128efb198
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/dev_requirements.txt
@@ -0,0 +1,7 @@
+# keep in sync with pyproject.toml#dependency-groups.dev
+-e ../../../eng/tools/azure-sdk-tools
+-e ../azure-ai-agentserver-core
+pytest
+httpx
+pytest-asyncio
+opentelemetry-sdk>=1.40.0
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/pyproject.toml b/sdk/agentserver/azure-ai-agentserver-activity/pyproject.toml
new file mode 100644
index 000000000000..c379e8cec091
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/pyproject.toml
@@ -0,0 +1,74 @@
+[project]
+name = "azure-ai-agentserver-activity"
+dynamic = ["version", "readme"]
+description = "Activity protocol host for Azure AI Hosted Agents"
+requires-python = ">=3.10"
+authors = [
+ { name = "Microsoft Corporation", email = "azpysdkhelp@microsoft.com" },
+]
+license = "MIT"
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
+]
+keywords = ["azure", "azure sdk", "agent", "agentserver", "activity"]
+
+dependencies = [
+ "azure-ai-agentserver-core>=2.0.0b4",
+ "opentelemetry-api>=1.40.0",
+]
+
+[dependency-groups]
+# keep in sync with dev_requirements.txt
+dev = [
+ "azure-sdk-tools",
+ "httpx",
+ "opentelemetry-sdk>=1.40.0",
+ "pytest-asyncio",
+ "pytest",
+]
+
+[project.urls]
+repository = "https://github.com/Azure/azure-sdk-for-python"
+
+[build-system]
+requires = ["setuptools>=69", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools.packages.find]
+exclude = [
+ "tests*",
+ "samples*",
+ "doc*",
+ "azure",
+ "azure.ai",
+ "azure.ai.agentserver",
+]
+
+[tool.setuptools.dynamic]
+version = { attr = "azure.ai.agentserver.activity._version.VERSION" }
+readme = { file = ["README.md"], content-type = "text/markdown" }
+
+[tool.setuptools.package-data]
+"azure.ai.agentserver.activity" = ["py.typed"]
+
+[tool.azure-sdk-build]
+analyze_python_version = "3.11"
+breaking = false
+mypy = true
+pyright = true
+verifytypes = false
+latestdependency = false
+pylint = true
+type_check_samples = false
+
+[tool.uv.sources]
+azure-ai-agentserver-core = { path = "../azure-ai-agentserver-core", editable = true }
+azure-sdk-tools = { path = "../../../eng/tools/azure-sdk-tools" }
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/pyrightconfig.json b/sdk/agentserver/azure-ai-agentserver-activity/pyrightconfig.json
new file mode 100644
index 000000000000..f36c5a7fe0d3
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/pyrightconfig.json
@@ -0,0 +1,11 @@
+{
+ "reportOptionalMemberAccess": "warning",
+ "reportArgumentType": "warning",
+ "reportAttributeAccessIssue": "warning",
+ "reportMissingImports": "warning",
+ "reportGeneralTypeIssues": "warning",
+ "reportReturnType": "warning",
+ "exclude": [
+ "**/samples/**"
+ ]
+}
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/samples/echo-agent/main.py b/sdk/agentserver/azure-ai-agentserver-activity/samples/echo-agent/main.py
new file mode 100644
index 000000000000..8889b7abebb9
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/samples/echo-agent/main.py
@@ -0,0 +1,41 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+"""Tier 1 - Zero-Config Activity Protocol Agent.
+
+The simplest possible activity protocol agent. The package auto-initializes
+the M365 Agents SDK from environment variables, applies MSAL auth patches,
+and bridges activities to the AgentApplication turn pipeline.
+
+You write only handler logic - no SDK wiring needed.
+"""
+
+from azure.ai.agentserver.activity import ActivityAgentServerHost
+
+app = ActivityAgentServerHost()
+
+
+@app.activity("message")
+async def on_message(context, state):
+ """Echo the user's message back."""
+ user_text = context.activity.text or ""
+ if user_text.strip():
+ reply = f"Echo: {user_text}"
+ await context.send_activity(reply)
+
+
+@app.activity("conversationUpdate")
+async def on_members_added(context, state):
+ """Welcome new members."""
+ for member in context.activity.members_added or []:
+ if member.id != context.activity.recipient.id:
+ await context.send_activity(f"Welcome, {member.name}!")
+
+
+@app.error
+async def on_error(context, error):
+ """Handle unhandled errors."""
+ await context.send_activity(f"Sorry, something went wrong: {error}")
+
+
+if __name__ == "__main__":
+ app.run()
\ No newline at end of file
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/samples/echo-agent/requirements.txt b/sdk/agentserver/azure-ai-agentserver-activity/samples/echo-agent/requirements.txt
new file mode 100644
index 000000000000..67656cca36bc
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/samples/echo-agent/requirements.txt
@@ -0,0 +1,4 @@
+microsoft-agents-hosting-core
+microsoft-agents-authentication-msal
+microsoft-agents-activity
+azure-identity
\ No newline at end of file
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/samples/multi-protocol-agent/main.py b/sdk/agentserver/azure-ai-agentserver-activity/samples/multi-protocol-agent/main.py
new file mode 100644
index 000000000000..7ec09f023cc2
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/samples/multi-protocol-agent/main.py
@@ -0,0 +1,49 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+"""Multi-Protocol Agent - Activity + Invocations.
+
+Demonstrates composing Activity and Invocations protocols on a single
+server using Python mixin inheritance (Tier 2 - Builder pattern).
+
+Both protocols share the same server on port 8088:
+ POST /activity/messages - Activity protocol (Teams/M365)
+ POST /api/messages - Activity protocol (Bot Framework compat)
+ POST /invocations - Invocations protocol (HTTP API)
+"""
+
+from starlette.requests import Request
+from starlette.responses import JSONResponse, Response
+
+from azure.ai.agentserver.activity import ActivityAgentServerHost
+from azure.ai.agentserver.invocations import InvocationAgentServerHost
+
+
+class MultiProtocolHost(ActivityAgentServerHost, InvocationAgentServerHost):
+ pass
+
+
+app = MultiProtocolHost()
+
+
+@app.activity("message")
+async def on_teams_message(context, state):
+ """Handle messages from Teams via Activity protocol."""
+ user_text = context.activity.text or ""
+ if user_text.strip():
+ await context.send_activity(f"[Multi-Protocol] Echo: {user_text}")
+
+
+@app.invoke_handler
+async def handle_invocation(request: Request) -> Response:
+ """Handle HTTP invocations via Invocations protocol."""
+ data = await request.json()
+ message = data.get("message", "")
+ return JSONResponse({
+ "reply": f"[Multi-Protocol] Echo: {message}",
+ "protocol": "invocations",
+ })
+
+
+if __name__ == "__main__":
+ app.run()
+
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/samples/multi-protocol-agent/requirements.txt b/sdk/agentserver/azure-ai-agentserver-activity/samples/multi-protocol-agent/requirements.txt
new file mode 100644
index 000000000000..bc8bbc5c489e
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/samples/multi-protocol-agent/requirements.txt
@@ -0,0 +1,4 @@
+microsoft-agents-hosting-core
+microsoft-agents-authentication-msal
+microsoft-agents-activity
+azure-identity
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/samples/self-hosted-agent/main.py b/sdk/agentserver/azure-ai-agentserver-activity/samples/self-hosted-agent/main.py
new file mode 100644
index 000000000000..542888cfb41f
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/samples/self-hosted-agent/main.py
@@ -0,0 +1,113 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+"""Self-Hosted Activity Agent - Full M365 SDK Control.
+
+Demonstrates the handler pattern (Tier 3) where the developer owns the
+full M365 Agents SDK pipeline: MsalConnectionManager, HttpAdapterBase,
+AgentApplication, and a custom bridge handler.
+
+Use this pattern when you need:
+- Direct access to M365 SDK features (auth_handlers, regex message matching)
+- Custom error handling or response logic
+- Full control over the activity processing pipeline
+"""
+
+from os import environ
+
+from starlette.responses import JSONResponse, Response
+
+from azure.ai.agentserver.activity import ActivityAgentServerHost, apply_msal_patches
+
+from microsoft_agents.activity import Activity, load_configuration_from_env
+from microsoft_agents.authentication.msal import MsalConnectionManager
+from microsoft_agents.hosting.core import (
+ AgentApplication,
+ Authorization,
+ ClaimsIdentity,
+ HttpAdapterBase,
+ MemoryStorage,
+ RestChannelServiceClientFactory,
+ TurnContext,
+ TurnState,
+)
+
+# ── M365 SDK setup ───────────────────────────────────────────────
+# Apply MSAL patches before creating MsalConnectionManager.
+apply_msal_patches()
+
+config = load_configuration_from_env(environ)
+storage = MemoryStorage()
+connection_manager = MsalConnectionManager(**config)
+client_factory = RestChannelServiceClientFactory(connection_manager)
+adapter = HttpAdapterBase(channel_service_client_factory=client_factory)
+authorization = Authorization(storage, connection_manager, **config)
+agent_app = AgentApplication[TurnState](
+ storage=storage,
+ adapter=adapter,
+ authorization=authorization,
+ **config,
+)
+
+
+# ── Business logic ───────────────────────────────────────────────
+
+@agent_app.activity("message")
+async def on_message(context: TurnContext, state: TurnState):
+ """Echo the user's message back."""
+ user_text = context.activity.text or ""
+ await context.send_activity(Activity(type="typing"))
+ reply = f"[Self-Hosted] Echo: {user_text}"
+ await context.send_activity(reply)
+
+
+@agent_app.activity("conversationUpdate")
+async def on_members_added(context: TurnContext, state: TurnState):
+ """Welcome new members."""
+ for member in context.activity.members_added or []:
+ if member.id != context.activity.recipient.id:
+ await context.send_activity(f"Welcome, {member.name}!")
+
+
+@agent_app.error
+async def on_error(context: TurnContext, error: Exception):
+ """Handle unhandled errors."""
+ await context.send_activity("The agent encountered an error.")
+
+
+# ── Foundry host with custom handler ─────────────────────────────
+
+async def handle(request) -> Response:
+ """Bridge to M365 SDK - parses activity and delegates to agent_app."""
+ activity_dict = request.state.activity
+
+ activity = Activity.model_validate(activity_dict)
+
+ if not activity.type or not activity.conversation or not activity.conversation.id:
+ return JSONResponse(
+ status_code=400,
+ content={"error": {"code": "invalid_request", "message": "Missing type or conversation.id"}},
+ )
+
+ claims = ClaimsIdentity({}, is_authenticated=False, authentication_type="Anonymous")
+
+ try:
+ invoke_response = await adapter.process_activity(claims, activity, agent_app.on_turn)
+ except PermissionError:
+ return Response(status_code=401)
+ except TypeError:
+ return Response(status_code=202)
+ except Exception:
+ return Response(status_code=202)
+
+ if activity.type == "invoke" or activity.delivery_mode == "expectReplies":
+ if invoke_response is not None:
+ return JSONResponse(content=invoke_response.body, status_code=invoke_response.status)
+ return JSONResponse(content={}, status_code=200)
+
+ return Response(status_code=202)
+
+
+app = ActivityAgentServerHost(handler=handle)
+
+if __name__ == "__main__":
+ app.run()
\ No newline at end of file
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/samples/self-hosted-agent/requirements.txt b/sdk/agentserver/azure-ai-agentserver-activity/samples/self-hosted-agent/requirements.txt
new file mode 100644
index 000000000000..bc8bbc5c489e
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/samples/self-hosted-agent/requirements.txt
@@ -0,0 +1,4 @@
+microsoft-agents-hosting-core
+microsoft-agents-authentication-msal
+microsoft-agents-activity
+azure-identity
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/tests/conftest.py b/sdk/agentserver/azure-ai-agentserver-activity/tests/conftest.py
new file mode 100644
index 000000000000..395853748275
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/tests/conftest.py
@@ -0,0 +1,27 @@
+# ---------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# ---------------------------------------------------------
+"""Shared fixtures for activity protocol tests."""
+
+import pytest
+from httpx import ASGITransport, AsyncClient
+from starlette.responses import JSONResponse, Response
+
+from azure.ai.agentserver.activity import ActivityAgentServerHost
+
+
+def pytest_configure(config):
+ config.addinivalue_line("markers", "tracing_e2e: end-to-end tracing tests against live Application Insights")
+
+
+@pytest.fixture
+async def activity_client():
+ async def on_message(request) -> Response:
+ activity = request.state.activity
+ return JSONResponse({"type": "message", "text": f"echo:{activity.get('text', '')}"})
+
+ app = ActivityAgentServerHost(handler=on_message, configure_observability=None)
+
+ transport = ASGITransport(app=app)
+ async with AsyncClient(transport=transport, base_url="http://testserver") as client:
+ yield client
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/tests/test_decorator_pattern.py b/sdk/agentserver/azure-ai-agentserver-activity/tests/test_decorator_pattern.py
new file mode 100644
index 000000000000..e973a09b9bce
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/tests/test_decorator_pattern.py
@@ -0,0 +1,35 @@
+# ---------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# ---------------------------------------------------------
+"""Tests for the decorator-based activity handler pattern."""
+
+import pytest
+from httpx import ASGITransport, AsyncClient
+from starlette.responses import JSONResponse
+
+from azure.ai.agentserver.activity import ActivityAgentServerHost
+
+
+@pytest.mark.asyncio
+async def test_decorator_registers_handler():
+ """Verify that @app.activity() wires up the bridge handler."""
+ app = ActivityAgentServerHost(configure_observability=None)
+
+ @app.activity("message")
+ async def on_message(context, state):
+ pass
+
+ # After decorating, _handler should be set to the bridge
+ assert app._handler is not None
+
+
+@pytest.mark.asyncio
+async def test_error_decorator_registers_handler():
+ """Verify that @app.error wires up the bridge handler."""
+ app = ActivityAgentServerHost(configure_observability=None)
+
+ @app.error
+ async def on_error(context, error):
+ pass
+
+ assert app._handler is not None
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/tests/test_error_source_classification.py b/sdk/agentserver/azure-ai-agentserver-activity/tests/test_error_source_classification.py
new file mode 100644
index 000000000000..b52c0b5eaa9e
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/tests/test_error_source_classification.py
@@ -0,0 +1,50 @@
+# ---------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# ---------------------------------------------------------
+"""Tests for x-platform-error-source classification in activity endpoints."""
+
+import pytest
+from azure.ai.agentserver.core._platform_headers import ERROR_DETAIL, ERROR_SOURCE, PLATFORM_ERROR_TAG
+from httpx import ASGITransport, AsyncClient
+
+from azure.ai.agentserver.activity import ActivityAgentServerHost
+
+
+@pytest.mark.asyncio
+async def test_upstream_handler_error_is_classified_upstream():
+ async def handle(request): # pylint: disable=unused-argument
+ raise RuntimeError("handler bug")
+
+ app = ActivityAgentServerHost(handler=handle, configure_observability=None)
+ transport = ASGITransport(app=app)
+ async with AsyncClient(transport=transport, base_url="http://testserver") as client:
+ resp = await client.post(
+ "/activity/messages",
+ json={"type": "message", "text": "hello"},
+ headers={"Authorization": "Bearer test-token", "x-agent-session-id": "session-123"},
+ )
+
+ assert resp.status_code == 500
+ assert resp.headers[ERROR_SOURCE] == "upstream"
+ assert ERROR_DETAIL not in resp.headers
+
+
+@pytest.mark.asyncio
+async def test_platform_tagged_error_is_classified_platform_with_detail():
+ async def handle(request): # pylint: disable=unused-argument
+ exc = RuntimeError("platform storage failure")
+ setattr(exc, PLATFORM_ERROR_TAG, True)
+ raise exc
+
+ app = ActivityAgentServerHost(handler=handle, configure_observability=None)
+ transport = ASGITransport(app=app)
+ async with AsyncClient(transport=transport, base_url="http://testserver") as client:
+ resp = await client.post(
+ "/activity/messages",
+ json={"type": "message", "text": "hello"},
+ headers={"Authorization": "Bearer test-token", "x-agent-session-id": "session-123"},
+ )
+
+ assert resp.status_code == 500
+ assert resp.headers[ERROR_SOURCE] == "platform"
+ assert "platform storage failure" in resp.headers[ERROR_DETAIL]
diff --git a/sdk/agentserver/azure-ai-agentserver-activity/tests/test_id_sanitization.py b/sdk/agentserver/azure-ai-agentserver-activity/tests/test_id_sanitization.py
new file mode 100644
index 000000000000..476a52d4a5c6
--- /dev/null
+++ b/sdk/agentserver/azure-ai-agentserver-activity/tests/test_id_sanitization.py
@@ -0,0 +1,90 @@
+# ---------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# ---------------------------------------------------------
+"""Tests for activity ID sanitization (defense in depth)."""
+
+import uuid
+
+import pytest
+from httpx import ASGITransport, AsyncClient
+from starlette.responses import JSONResponse
+
+from azure.ai.agentserver.activity import ActivityAgentServerHost
+
+
+@pytest.mark.asyncio
+async def test_provided_activity_id_is_used():
+ async def handle(request): # pylint: disable=unused-argument
+ return JSONResponse({"ok": True})
+
+ app = ActivityAgentServerHost(handler=handle, configure_observability=None)
+ transport = ASGITransport(app=app)
+ async with AsyncClient(transport=transport, base_url="http://testserver") as client:
+ resp = await client.post(
+ "/activity/messages",
+ json={"type": "message", "text": "hi", "id": "my-activity-123"},
+ headers={"Authorization": "Bearer test-token"},
+ )
+
+ assert resp.status_code == 200
+ assert resp.headers["x-agent-activity-id"] == "my-activity-123"
+
+
+@pytest.mark.asyncio
+async def test_missing_activity_id_generates_uuid():
+ async def handle(request): # pylint: disable=unused-argument
+ return JSONResponse({"ok": True})
+
+ app = ActivityAgentServerHost(handler=handle, configure_observability=None)
+ transport = ASGITransport(app=app)
+ async with AsyncClient(transport=transport, base_url="http://testserver") as client:
+ resp = await client.post(
+ "/activity/messages",
+ json={"type": "message", "text": "hi"},
+ headers={"Authorization": "Bearer test-token"},
+ )
+
+ assert resp.status_code == 200
+ activity_id = resp.headers["x-agent-activity-id"]
+ # Should be a valid UUID
+ uuid.UUID(activity_id)
+
+
+@pytest.mark.asyncio
+async def test_oversized_activity_id_is_sanitized():
+ async def handle(request): # pylint: disable=unused-argument
+ return JSONResponse({"ok": True})
+
+ app = ActivityAgentServerHost(handler=handle, configure_observability=None)
+ transport = ASGITransport(app=app)
+ async with AsyncClient(transport=transport, base_url="http://testserver") as client:
+ resp = await client.post(
+ "/activity/messages",
+ json={"type": "message", "text": "hi", "id": "x" * 300},
+ headers={"Authorization": "Bearer test-token"},
+ )
+
+ assert resp.status_code == 200
+ activity_id = resp.headers["x-agent-activity-id"]
+ assert len(activity_id) < 300
+ uuid.UUID(activity_id) # should be a fallback UUID
+
+
+@pytest.mark.asyncio
+async def test_malformed_activity_id_is_sanitized():
+ async def handle(request): # pylint: disable=unused-argument
+ return JSONResponse({"ok": True})
+
+ app = ActivityAgentServerHost(handler=handle, configure_observability=None)
+ transport = ASGITransport(app=app)
+ async with AsyncClient(transport=transport, base_url="http://testserver") as client:
+ resp = await client.post(
+ "/activity/messages",
+ json={"type": "message", "text": "hi", "id": "id with spaces & ")
+ assert "