* 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>
agent-framework-hosting
Multi-channel hosting for Microsoft Agent Framework agents.
agent-framework-hosting lets you serve a single agent (or workflow)
target through one or more channels — pluggable adapters that
expose the target over different transports. The result is a single
Starlette ASGI application you can host anywhere (local Hypercorn,
Azure Container Apps, Foundry Hosted Agents, …).
The base package contains only the channel-neutral plumbing:
AgentFrameworkHost— the Starlette hostChannel/ChannelPush— the channel protocolsChannelRequest/ChannelSession/ChannelIdentity/ResponseTarget— the request envelope and routing primitivesChannelContext/ChannelContribution/ChannelCommand— the channel-side hooks for invoking the target and contributing routes, commands, and lifecycle callbacksChannelRunHook/ChannelStreamTransformHook— the per-request customization seamsDurableTaskRunner+InProcessTaskRunner— the seam used to dispatch non-originating push fan-out; the in-process runner is the default. Plug in a durable adapter (e.g.agent-framework-hosting-durabletask) forruntime_mode="ephemeral"deployments.
Concrete channels live in their own packages so you only install what you use:
| Package | Transport |
|---|---|
agent-framework-hosting-responses |
OpenAI Responses API |
agent-framework-hosting-invocations |
Foundry-native invocation envelope |
agent-framework-hosting-telegram |
Telegram Bot API |
agent-framework-hosting-activity-protocol |
Bot Framework Activity Protocol (Teams, Direct Line, Web Chat, …) |
agent-framework-hosting-teams |
Microsoft Teams (Teams SDK) |
agent-framework-hosting-entra |
Entra (OAuth) identity-link sidecar |
Architecture
graph LR
Caller[External caller /<br/>messaging app]
subgraph Host[AgentFrameworkHost]
direction TB
ASGI[Starlette app]
Router[Channel router]
Parse{parse →<br/>command or<br/>message?}
Auth[host.authorize]
Resolver[IdentityResolver]
Delivery[_deliver_response]
Push[_handle_push_task]
end
Channels[Channels<br/>Responses · Invocations ·<br/>Telegram · Activity ·<br/>IdentityLinker]
CmdHandler[CommandHandler<br/>via ChannelCommandContext]
Target[(Agent or Workflow)]
Runner[DurableTaskRunner]
StateStore[(HostStateStore)]
Caller --> ASGI
ASGI --> Router
Router --> Parse
Parse -- /command --> CmdHandler
Parse -- message --> Auth
CmdHandler -- ctx.run --> Auth
CmdHandler -- local reply --> Channels
Auth --> Resolver
Resolver --> StateStore
Auth --> Target
Target --> Delivery
Delivery -- originating sync --> Channels
Delivery -- non-originating --> Runner
Runner --> Push
Push --> Channels
Channels --> ASGI
For a richer set of flow diagrams — identity linking, multi-channel fan-out, server-side relays, background runs, durable-runner codec envelopes, echo idempotency, workflow targets — see the Python hosting spec.
Install
pip install agent-framework-hosting agent-framework-hosting-responses
# or with uvicorn pre-installed for the demo `host.serve(...)` helper
pip install "agent-framework-hosting[serve]" agent-framework-hosting-responses
# add the [disk] extra to opt in to on-disk persistence (see below)
pip install "agent-framework-hosting[disk]"
Quickstart
from agent_framework.openai import OpenAIChatClient
from agent_framework_hosting import AgentFrameworkHost, Channel
agent = OpenAIChatClient().as_agent(name="Assistant")
# Add channels from sibling packages, e.g. `agent-framework-hosting-responses`
# exposes a `ResponsesChannel` that serves the OpenAI Responses API.
channels: list[Channel] = []
host = AgentFrameworkHost(target=agent, channels=channels)
host.serve(port=8000)
See the hosting samples
for richer multi-channel apps (Telegram + Teams + Responses fan-out,
identity linking, ResponseTarget routing, etc.).
Optional disk persistence (state_dir)
By default the host keeps everything in memory: the durable-task runner's
pending push queue, the per-isolation-key session aliases, the active-channel
map, and the per-channel ChannelIdentity map. That is the right shape for
ephemeral runtimes (Foundry Hosted Agents et al.) where the host is
restarted per request and persistence lives behind a service like the Foundry
response store, and for short-lived local dev.
For long-running deployments (an always-on container, a local dev server
you restart often, a single-VM bot) opt in to disk persistence by passing
state_dir to AgentFrameworkHost. The runner queue and the session
bookkeeping use diskcache
(installed via the [disk] extra) protected by an OS-level advisory file
lock so two hosts pointed at the same directory can't double-execute
scheduled pushes. Workflow checkpoints (when the target is a Workflow)
use the framework's FileCheckpointStorage — no extra dependency. The
identity-link store path is offered to linkers that implement
SupportsLinkStorePath; linkers that manage persistence themselves should
be configured directly.
from agent_framework_hosting import AgentFrameworkHost
# Single path → host auto-derives `runner/`, `sessions/`, `links/`, and
# (for workflow targets) `checkpoints/` subpaths.
host = AgentFrameworkHost(
target=agent,
channels=channels,
state_dir="./.host-state",
)
# Or route components to different roots — use the HostStatePaths TypedDict
# (or a plain dict with the same keys) for editor autocomplete on the keys.
# Omit a key to opt that component out of persistence.
from agent_framework_hosting import HostStatePaths
host = AgentFrameworkHost(
target=workflow,
channels=channels,
state_dir=HostStatePaths(
runner="/var/lib/myapp/tasks",
sessions="/var/lib/myapp/state",
checkpoints="/var/lib/myapp/checkpoints",
links="/var/lib/myapp/links",
),
)
What survives a restart:
- Pending durable-task records — scheduled but not-yet-completed push
deliveries replay on the next host startup via
runner.resume(). Records that crashed mid-attempt resume with their already-consumed retry budget. _session_aliases— per-isolation-key session-id rewrites (via the reset-session command)._active— the most recently active channel for each isolation key (consumed byResponseTarget.active)._identities— channel-nativeChannelIdentityrows used byResponseTarget.channels([...])/.all_linkedfan-out.- Workflow checkpoints — when the target is a
Workflow, the host wraps thecheckpointspath in a per-isolation-keyFileCheckpointStorage(equivalent to passingcheckpoint_location=...directly; the explicit parameter takes precedence and emits a warning when both are set). - Identity-link store — when the configured linker implements
SupportsLinkStorePath, the host passes thelinkspath to it so pending challenges, linked identities, and verified claims can survive restarts.
What doesn't:
- Live
AgentSessionobjects (rehydrated lazily by the history provider on the next turn). - The
ContinuationTokenstore (separate concern, plug in your own).
Unpicklable push payloads raise PushPayloadNotPicklable eagerly from
schedule() so issues surface at the call site, not on the next restart.