* Update hosting channel endpoint paths Treat channel paths as concrete endpoint paths so built-in channels can be mounted at their defaults or at the app root without sample-specific subclasses. Update docs, tests, and the Foundry Telegram Invocations sample accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add push support to ActivityProtocolChannel Implement the ChannelPush protocol so the Activity Protocol channel can receive cross-channel fan-out (ResponseTarget.all_linked) and echo_input replay as a non-originating destination: - Add push() that reconstructs a proactive Bot Framework activity (bot/user swap) from the stored conversation reference and POSTs it to /v3/conversations/{id}/activities. - Record a ChannelIdentity (service_url, conversation, bot, user, channel_id, locale) on ChannelRequest.identity so the host registers the channel under its isolation key for fan-out resolution. - Route the streaming path through deliver_response so Activity-originated turns broadcast like Telegram/Discord. - Add tests for push delivery, service_url validation, ChannelPush instance check, and inbound identity recording. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Don't delete Telegram webhook on shutdown by default The TelegramChannel deleted its webhook on shutdown in webhook mode. During a rolling redeploy the new revision registers the webhook on startup, then the old revision's shutdown deletes it, silently breaking inbound delivery until the next boot. setWebhook is overwriting/idempotent, so startup re-asserts the webhook every boot and no teardown is needed. Add a delete_webhook_on_shutdown flag (default False) so teardown is opt-in for ephemeral deployments, and leave the webhook in place otherwise. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Activity channel streaming on non-Teams channels (405 on updateActivity) The Activity Protocol channel streamed replies the Teams way: POST a placeholder, then PUT-edit it as tokens arrive. Only Teams supports the updateActivity REST op; Web Chat, Direct Line and the Emulator return 405 Method Not Allowed on the PUT, so the user saw only the placeholder. Gate the placeholder+edit flow on edit-capable channels (msteams). Other channels now buffer the stream and POST a single final message, mirroring the non-streaming path's fan-out and response-hook semantics. Also add a defensive 405 fallback inside the Teams edit loop so an unexpected 405 can never strand the user on the placeholder. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(hosting-activity-protocol): don't parse Teams inline attachment content as a URI Teams message activities include a text/html attachment whose inline `content` is raw HTML (not a URL). _parse_activity fell back to `attachment["content"]` and passed it to Content.from_uri, raising ContentError ("URI must contain a scheme") and failing the whole turn, so Teams users got no response. Only treat `contentUrl` as a URI, require an absolute scheme, and skip unparseable attachments defensively instead of failing the message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(hosting-activity-protocol): native slash-command dispatch for Teams/Activity Add a commands= parameter to ActivityProtocolChannel that intercepts a leading /command (after stripping the bot's own @mention) and dispatches to ChannelCommand handlers, mirroring the Telegram channel. Unknown commands fall through to the agent. The channel run_hook is applied to command requests so handlers observe the same resolved isolation key as ordinary messages, and handler errors are swallowed (200, no Bot Service retry of non-idempotent commands). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(hosting): silent attributed Telegram echoes + Teams markdown rendering - hosting-telegram: send cross-channel input echoes with disable_notification (silent) and detect echo payloads so they aren't re-broadcast. - hosting-activity-protocol: render outbound + push activities as textFormat 'markdown' so Teams shows formatted replies (enables per-channel variants). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(hosting-activity-protocol): address PR #6307 review feedback Consult the host delivery pipeline even for empty streamed replies so ResponseTarget.none is honoured and non-originating fan-out is consulted instead of always emitting an originating "(no response)" message. Applies to both the progressive-edit (Teams) and buffered (Web Chat/Direct Line) streaming paths. Re-validate service_url against the allow-list in push(): the identity is read from a persisted store and push runs out-of-band, so the captured service_url must be re-checked before a bearer token is sent. Adds tests for empty-stream host consultation/suppression on both streaming paths and for push rejecting a disallowed service_url. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
foundry_hosted_agent — Responses + Invocations (Foundry Hosted Agents compatible)
Smallest end-to-end hosting sample. One Foundry-backed agent, two channels, no human-chat surface — and that minimal shape is the whole point: a host configured with at least the Responses and Invocations channels under their default endpoints is runtime-compatible with the Foundry Hosted Agents platform. The same container image runs locally, behind any ASGI server, or as a Hosted Agent — no protocol shim, no extra adapter.
| Route | Channel | Used by |
|---|---|---|
POST /responses |
ResponsesChannel |
OpenAI Responses clients (call_server.py) |
POST /invocations |
InvocationsChannel |
Host-native JSON envelope (Hosted Agents) |
Conversation history
The agent is wired with FoundryHostedAgentHistoryProvider (from
agent-framework-foundry-hosting). When a Responses request supplies
previous_response_id, the channel uses it as the session id and the
provider fetches the prior turn chain directly from
{FOUNDRY_PROJECT_ENDPOINT}/storage/... using the same managed-identity
credential as the chat client. Locally (when FOUNDRY_HOSTING_ENVIRONMENT
is unset) it transparently falls back to an in-memory store, so the same
code runs in dev. Writes are a no-op — Foundry persists Responses turns
authoritatively as the runtime executes them.
For richer scenarios (custom tools, history providers, run hooks,
multicast, Telegram, Teams, identity linking) see
../local_telegram and
../local_identity_link.
Layout
foundry_hosted_agent/
├── app.py # the host (ResponsesChannel + InvocationsChannel)
├── call_server.py # client: openai SDK / agent framework / FoundryAgent
├── agent.yaml # Foundry Hosted Agents minimal definition
├── agent.manifest.yaml # Foundry Hosted Agents full deployment manifest
├── azure.yaml # azd service config (build context = this folder)
├── Dockerfile # built from this folder; uv fetches deps from GitHub
├── Dockerfile.dockerignore # BuildKit allowlist that trims the context
├── pyproject.toml # depends on the hosting packages via GitHub git refs
└── README.md # this file
Run locally
export FOUNDRY_PROJECT_ENDPOINT=https://<your-project>.services.ai.azure.com
export MODEL_DEPLOYMENT_NAME=gpt-4.1-mini
az login # any DefaultAzureCredential source
uv sync
uv run python app.py # binds 0.0.0.0:8000
The env var names match agent.manifest.yaml so the same shell
environment works for both local runs and Hosted Agent deployments.
Call locally
uv sync --group dev
# OpenAI SDK pointed at the local /responses endpoint.
uv run python call_server.py --via openai "hello there"
# The same call via the Agent Framework Agent + OpenAIChatClient stack.
uv run python call_server.py --via af "hello there"
# Once deployed as a Hosted Agent: target the Foundry-managed endpoint.
export FOUNDRY_HOSTED_AGENT_NAME=agent-framework-hosting-sample
uv run python call_server.py --via foundry "hello there"
Docker
The Docker build context is this sample folder. pyproject.toml
declares the in-tree agent-framework-hosting* packages via
[tool.uv.sources] git refs pointing at the
feature/python-hosting branch of
microsoft/agent-framework,
so uv sync inside the image fetches them directly. No vendoring step is
required — the build just needs network access to GitHub. Once the
hosting packages publish to PyPI you can drop the [tool.uv.sources]
overrides and rely on PyPI resolution.
# From this folder — context = `.` (sample folder).
DOCKER_BUILDKIT=1 docker build -t hosting-sample-hosted-agent .
docker run -p 8000:8000 \
-e FOUNDRY_PROJECT_ENDPOINT -e MODEL_DEPLOYMENT_NAME \
-e AZURE_CLIENT_ID -e AZURE_TENANT_ID -e AZURE_CLIENT_SECRET \
hosting-sample-hosted-agent
Hosted Agent deployment
azure.yaml keeps project: . and uses docker.remoteBuild: true —
the remote builder receives only this sample folder and runs
uv sync to pull the hosting packages from GitHub.
The two YAMLs follow the same convention as the
foundry-hosted-agents/ reference
samples — agent.yaml is the minimal kind/protocols/resources card,
agent.manifest.yaml is the full template + environment-variable +
model-resource binding used during deployment.
azd up # provisions infra/ + builds + pushes + deploys
azd deploy # rebuild + redeploy only
Required Foundry RBAC
The container runs as the Hosted Agent's managed identity. That identity
needs permission to call the Foundry project's agent/Responses endpoints
— without it the call returns 401 PermissionDenied. Grant the
Azure AI Project Manager role (or the more granular
Microsoft.CognitiveServices/accounts/AIServices/agents/* data
actions) on the Foundry project to the Hosted Agent's managed identity.
See https://aka.ms/FoundryPermissions for the full role list.
Health probe
The Foundry Hosted Agents runtime probes GET /readiness;
AgentFrameworkHost exposes that route automatically (returns
200 ok). No extra wiring needed.
The host code never imports anything Foundry-specific beyond the chat
client itself — swapping FoundryChatClient for OpenAIChatClient (or
any other client) flips this sample from a Hosted Agent target to a
non-Foundry deployment without touching the channels.