# 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 host - `Channel` / `ChannelPush` — the channel protocols - `ChannelRequest` / `ChannelSession` / `ChannelIdentity` / `ResponseTarget` — the request envelope and routing primitives - `ChannelContext` / `ChannelContribution` / `ChannelCommand` — the channel-side hooks for invoking the target and contributing routes, commands, and lifecycle callbacks - `ChannelRunHook` / `ChannelStreamTransformHook` — the per-request customization seams - `DurableTaskRunner` + `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`) for `runtime_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 ```mermaid graph LR Caller[External caller /
messaging app] subgraph Host[AgentFrameworkHost] direction TB ASGI[Starlette app] Router[Channel router] Parse{parse →
command or
message?} Auth[host.authorize] Resolver[IdentityResolver] Delivery[_deliver_response] Push[_handle_push_task] end Channels[Channels
Responses · Invocations ·
Telegram · Activity ·
IdentityLinker] CmdHandler[CommandHandler
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](https://github.com/microsoft/agent-framework/blob/main/docs/specs/002-python-hosting-channels.md). ## Install ```bash 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 ```python 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](https://github.com/microsoft/agent-framework/tree/main/python/samples/04-hosting/af-hosting) 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`](https://grantjenks.com/docs/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. ```python 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 by `ResponseTarget.active`). - **`_identities`** — channel-native `ChannelIdentity` rows used by `ResponseTarget.channels([...])` / `.all_linked` fan-out. - **Workflow checkpoints** — when the target is a `Workflow`, the host wraps the `checkpoints` path in a per-isolation-key `FileCheckpointStorage` (equivalent to passing `checkpoint_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 the `links` path to it so pending challenges, linked identities, and verified claims can survive restarts. What doesn't: - Live `AgentSession` objects (rehydrated lazily by the history provider on the next turn). - The `ContinuationToken` store (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.