Python: Add agent-framework-hosting-activity-protocol channel#6757
Closed
eavanvalkenburg wants to merge 20 commits into
Closed
Python: Add agent-framework-hosting-activity-protocol channel#6757eavanvalkenburg wants to merge 20 commits into
eavanvalkenburg wants to merge 20 commits into
Conversation
* first iteration of channel spec * added deny link setup * clarify invocation hook role and dedupe ADR/spec ADR 0026: - Tighten Decision Outcome Summary so each concept is mentioned once; defer full definitions to the Terminology section. - Update ChannelInvocationHook bullet to match the clarified gap microsoft#7 language (uniform ChannelRequest envelope, hook timing, illustrative examples). - Drop Decision Drivers bullets that just restated Business Goals; cross-link to the goals section instead. - Replace the More Information bullet list with a pointer to Non-Goals. Spec 002: - Trim requirement microsoft#21 to point at the canonical LinkPolicy section instead of restating the full contract. - Add a #linkpolicy-and-trust_level subsection anchor for cross-refs. - Trim the Terminology LinkPolicy entry's two-hosts caveat (canonical version stays in the Key Types section). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * updated adr and spec * Update hosting channels ADR and spec - Document FoundryHostedAgentHistoryProvider roundtrip of additional_properties namespaces via the agent_framework container key on stored OutputItems. - Add Foundry storage gap subsection capturing the update_item service ask required for post-push delivery_tracking[] mutation. - Triage open questions: 18 resolved (now in a Resolved Questions decisions log), 3 notes-updated, 6 unchanged. Capture spec-body follow-ups implied by the resolutions in a new Decisions-driven follow-ups subsection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refine hosting ADR + spec: A2A/MCP-tool channels, store-parameter matrix, open-question pass - Surface A2A and MCP-tool channels as explicitly designed-in but fast-follow work after the first Responses + Invocations + Telegram release. Updated ADR business goals, non-goals, and More Information; added spec reqs microsoft#25 (A2AChannel) and microsoft#26 (MCPToolChannel) under v1 Fast Follow; renumbered the WhatsApp/Teams entry to microsoft#27. - New 'The Responses store parameter' subsection in the spec: 2x3 destination matrix making explicit that 'store' has no canonical meaning at the hosted-agent layer — the developer decides what it maps to across service-side, hosted-agent storage, and caller-side. Includes design properties on forwarding-vs-mapping, per-deployment documentation responsibility, and richer storage vocabulary via OpenAI's extra_body. - Fixed contradicting spec text that previously claimed ResponsesChannel maps store=False to session_mode=disabled by default; updated channel options table, session_mode terminology entry, and Scenario 3 prose/comment to match the new model. - Renamed FoundryHistoryProvider -> FoundryHostedAgentHistoryProvider throughout the spec (9 occurrences) so the name reinforces the intended hosted-agent use case. - ADR open-questions pass: walked through all 15 entries with the user. 13 resolved (moved to a new 'Resolved Questions (decisions log)' table), 2 kept open with refined wording (Q6 'Channel' GA name, Q14 Responses WS subprotocol). Added a 'Decisions-driven follow-ups' bullet list capturing the spec-body / sample edits implied by the resolutions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Hosting ADR + spec: rename Teams channel to Activity Protocol, add multi-user conversation design - Rename the planned Teams channel to ActivityChannel (package agent-framework-hosting-activity). Promoted to req microsoft#27 (v1 fast follow) alongside A2A and MCP-tool, with native translations from Activity Protocol objects to AF types so the contract is explicit rather than implicit through Invocations. Channel sits behind Azure Bot Service, which fronts Teams / Web Chat / Slack / etc. Naming reserves a TeamsChannel name for any future direct-to-Teams transport that bypasses Bot Service (now stretch req microsoft#28 with WhatsApp). ResponseTarget channel ids and JSON examples updated from "teams" to "activity". Appendix B updated to acknowledge that ActivityChannel deliberately reuses the Bot Service connector model (the no-connector stance applies to the rest of the channel set). - Add first-class design for multi-user surfaces (Telegram groups / supergroups / forum topics; Activity Protocol groupChat and team channels). Cleanly separate user identity (ChannelIdentity.native_id = from.id / from.aadObjectId) from conversation locator (ChannelRequest.conversation_id = chat.id (+ message_thread_id / replyToId)). New per-channel options: conversation_scope (per_user / per_user_per_conversation (default in groups) / per_conversation) and accept_in_group addressing rule (mention_only (default) / command_only / mention_or_command / all). Specifies originating reply must include conversation + thread locator, ChannelPush behavior in groups, link-ceremony privacy (challenges redirected to user DMs), and the Activity-channel mapping for personal / groupChat / channel conversationType plus Teams replyToId threading. Broadcast Telegram Channels and adaptive-card Invoke activity flows scoped as fast follow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(hosting): rename RunHandle → ContinuationToken; HostStateStore (file-based v1); align agentserver dependency posture - Rename RunHandle → ContinuationToken (opaque URL-safe `token` field) throughout ADR + spec; update routes to /{continuation_token}; spec out equivalent continuation-token support for the Invocations channel (Q20 done). - Introduce HostStateStore as the single persistence seam for host-execution metadata (continuation tokens, identity-link grants, last-seen records). V1 default: FileHostStateStore (atomic JSON-per-record under ./.af-hosting/, per-namespace TTLs) — background runs and link grants now survive host restarts. InMemoryHostStateStore for tests; pluggable Cosmos / SQL / Redis remain v1 fast follow under req microsoft#23. Closes Q9, Q11, Q14. - Drop blanket "no agentserver dependency" claims. Hosting core is still independent of agentserver, but channel packages MAY consume lower-level building blocks (notably the Foundry response-store SDK that FoundryHostedAgentHistoryProvider builds on). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(hosting): swap Scenarios 6 and 7 so the linker comes before cross-channel continuity Scenario 6 (cross-channel continuity) previously forward-referenced Scenario 7 (linker) twice, since continuity depends on the link/merge ceremony. Invert the order so the linker scenario establishes the mechanism first and the continuity scenario builds on it. Update internal cross-references, the require_link section anchor, and Scenario 8's prerequisites/comment to match. Also tightened the new Scenario 7's closing note to point at HostStateStore (file-based default) for cross-host continuity, and dropped a stale MfaIdentityLinker reference from the linker variants paragraph (Q13 dropped MFA from phase 1). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(hosting): rewrite Scenario 7 as trusted-relay + add ResponseTarget.identities The previous Scenario 7 (cross-channel chat continuity) implied two independent auto-issued isolation_keys would converge by themselves — they don't, that needs a linker. Replace with a more realistic and complementary scenario: a trusted server-side application backend exposes Responses + Telegram against the same agent and uses extra_body to carry app-internal identity hints (app_user_id, push_to_telegram_chat_id) that a Responses run_hook translates into both an isolation_key promotion and a push to a known Telegram chat. Includes a closing variant pointing back at Scenario 6's linker for the no-app-table flow. Adds the ResponseTarget.identities([ChannelIdentity(...)]) variant to the type table and req microsoft#12 to support 'caller already knows the channel-native recipient' delivery without going through the link store. Bypasses the link store but still consults LinkPolicy per delivery. Drops MfaIdentityLinker references from req microsoft#11, req microsoft#24, and the linker helpers table (Q13 had already dropped MFA from phase 1; the spec body just hadn't caught up). Marks ADR Q8 follow-up done. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(hosting): wire FileCheckpointStorage into Scenario 9 + show resume-from-checkpoint flow Scenario 9 now builds the workflow with a FileCheckpointStorage so executor frames are persisted across runs, and demonstrates how the run_hook surfaces a caller-supplied resume_from_checkpoint into request.attributes so the host's workflow dispatch can pass it to Workflow.run(checkpoint_id=...). Closing paragraph clarifies that CheckpointStorage is workflow-runtime state, kept structurally separate from HostStateStore and ContextProvider — three protocols that MAY share a backend but stay independently typed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(hosting): emphasize result richness in Scenario 10 (channels are not limited to result.text) Add a 'Result is rich, not just text' callout under the channel-authoring sample. Inventories the typed Contents on the underlying AgentRunResult (TextContent, DataContent, UriContent, FunctionCallContent / FunctionResultContent, HostedFile/VectorStoreContent, UsageContent, TextReasoningContent, ErrorContent + additional_properties), the typed structured output via result.value, and shows concrete examples per channel shape: Telegram (MarkdownV2 + sendPhoto/sendAudio + inline keyboards), Responses (full content-list round-trip), chat UI (GFM/HTML + collapsible tool/reasoning panels), voice (TTS + earcons), typed RPC (result.value first). result.text is positioned as a convenience for single-string channels, not the contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec: add TeamsChannel (microsoft/teams.py) as fast-follow req microsoft#28 Add a Teams-native channel package built on the MIT-licensed microsoft/teams.py SDK as fast-follow alongside the generic ActivityChannel (req microsoft#27). Where ActivityChannel targets the generic Activity Protocol surface, TeamsChannel exploits Teams-specific affordances the generic protocol does not surface natively: Adaptive Cards (typed builder), streamed replies, AI-generated badge, feedback controls + form, suggested-prompt chips, inline citations, modal Dialogs, Message Extensions (action / search / link unfurling), proactive / targeted / threaded messages, and SSO via MSAL. Mounts the SDK's App into the host's Starlette app via a custom HttpServerAdapter; reuses the same host-tracked-session family as ActivityChannel (from.aadObjectId -> ChannelIdentity). The SDK already ships a 'Build an agent using Microsoft Agent Framework' guide so the integration story is direct. Renumber the WhatsApp / direct-to-Teams stretch item to req microsoft#29 and clarify its 'direct-to-Teams' placeholder is a future transport that bypasses both Bot Service and the teams.py SDK. Add the SDK to Dependencies & Commitment Status as a proposed runtime dep of agent-framework-hosting-teams. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec: clarify direct-to-Teams stretch as speculative (no Bot Service) Split the WhatsApp + direct-to-Teams stretch entry into two distinct items and reword the direct-to-Teams item to be honest about its current feasibility: - It MUST not rely on Azure Bot Service (otherwise it is just ActivityChannel / TeamsChannel under a different name). - No such transport is publicly available today: Graph chat APIs and microsoft/teams.py both ultimately route through Bot Service for the bot-as-conversation-participant pattern. - The slot is kept on the roadmap to preserve the naming line in case Microsoft ships a Bot-Service-free transport (native Teams REST/RPC, a Graph subscription strong enough to drive both inbound and outbound message flow, ...). - Reaffirm TeamsChannel (req microsoft#28) as the canonical Teams channel until then. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec: clarify TeamsChannel still rides on Bot Service in v1; add audience table Make explicit that TeamsChannel (req microsoft#28) uses Azure Bot Service in v1 — the microsoft/teams.py SDK is a higher-level Pythonic wrapper over the same Activity Protocol pipeline that ActivityChannel exposes raw. The difference is what the developer writes against, not the network path. A Bot-Service-free Teams transport is not currently possible and stays tracked as the speculative req microsoft#30. Add the ActivityChannel vs TeamsChannel audience comparison table to req microsoft#28 so the choice is obvious to readers: - ActivityChannel: maximum portability across all Bot Service-fronted channels. - TeamsChannel: Teams-first deployments wanting Cards / Dialogs / Message Extensions / citations / feedback / suggested prompts / SSO out of the box. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting): add agent-framework-hosting core package
New ``agent-framework-hosting`` package implementing ADR 0026 / SPEC-002:
the channel-neutral host that lets a single ``Agent`` (or ``Workflow``)
fan out across multiple wire protocols ("channels") behind one Starlette
ASGI app.
Surface (re-exported from ``agent_framework_hosting``):
- ``AgentFrameworkHost`` — wraps a hostable target, mounts channels onto
an ASGI app, owns per-isolation-key ``AgentSession`` reuse, threads
request context (``response_id`` / ``previous_response_id``) into
context providers via an ``ExitStack`` of ``bind_request_context``
calls, and exposes an opt-in Hypercorn ``serve()`` helper (extra
``[serve]``).
- ``Channel`` protocol + ``ChannelContribution`` — the surface a channel
package implements (routes, lifespans, identity hooks, …).
- ``ChannelRequest`` / ``ChannelSession`` / ``ChannelIdentity`` /
``ChannelPush`` / ``ChannelCommand[Context]`` / ``ChannelRunHook`` /
``ChannelStreamTransformHook`` / ``DeliveryReport`` /
``HostedRunResult`` / ``ResponseTarget`` / ``ResponseTargetKind`` /
``apply_run_hook`` — channel-side dataclasses + helpers.
- ``IsolationKeys`` + ``ISOLATION_HEADER_USER`` / ``..._CHAT`` +
``get/set/reset_current_isolation_keys`` — the host's ASGI middleware
reads the ``x-agent-{user,chat}-isolation-key`` headers off each
inbound request and exposes them to the agent stack via a
``ContextVar`` so storage-side providers (e.g.
``FoundryHostedAgentHistoryProvider``) can apply per-tenant
partitioning without channels having to forward anything.
Includes 45 unit tests covering the host, channel contributions,
isolation contextvar, and shared types. Registers the package in
``python/pyproject.toml`` ``[tool.uv.sources]`` and adds the matching
pyright ``executionEnvironments`` entry for tests.
Hypercorn is an optional dependency (``[serve]`` extra); the soft import
in ``serve()`` is annotated for pyright since it isn't on the default
install.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(hosting): address PR-2 review comments
Source-code changes
- _suppress_already_consumed: narrow contract — RuntimeError now logs
at WARNING with exc_info; non-RuntimeError still logs at exception().
Docstring clarifies that any non-clean teardown is observable.
- _BoundResponseStream: add aclose() and route __await__ through
get_final_response() so the binding is always released — fixes
contextvar leak when channels abandon the stream or use the
await-the-stream convenience.
- Lifespan: aggregate startup/shutdown callback errors; every callback
runs, all failures are logged with their qualname, and the first
error is re-raised so Starlette still aborts boot.
- _build_run_kwargs: switch session-cache write to dict.setdefault so
concurrent racers cannot orphan a session if create_session ever
yields.
- _deliver_response: introduce DeliveryReport.failed for push outages
vs explicit "no link" drops; an outage no longer triggers an
originating fallback so the channel can decide degraded behaviour.
Test additions
- tests/test_isolation.py (new): full coverage of IsolationKeys, the
contextvar helpers, header constants, and end-to-end ASGI
middleware lift / reset / passthrough.
- tests/test_host.py: TestBindRequestContext, TestBoundResponseStream
(aclose / __await__ / __getattr__ forwarding / double-close
idempotency), TestWrapInputListMessages (list[Message] LAST
precedence), TestLifespanAggregation (startup + shutdown).
- tests/test_types.py: TestApplyRunHook (sync/async/None), and
TestDeliveryReport (new failed field).
- Updated test_push_exception_marks_skipped ->
test_push_exception_lands_in_failed_no_fallback to match the new
delivery contract.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(hosting): address PR-2 round-2 review comments
- Refactor workflow checkpoint restoration into shared helpers
(_restore_workflow_checkpoint for blocking; the streaming sibling
drains the rehydration stream) so the blocking and streaming paths
rehydrate identically — clarifies the previously inline _maybe_restore
by hoisting the pattern next to the blocking call site.
- Document that blocking workflow output is text-only by design;
richer modalities ride the streaming AgentResponseUpdate channel,
which preserves all content parts.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* review: address PR-4 _host.py round 2 feedback
These review comments were filed on PR-4 (microsoft#5640) but target lines that
live in the hosting-core package (PR-2 / microsoft#5638), so the fixes land here
and PR-4's stack will pick them up on rebase.
- _suppress_already_consumed: narrow the RuntimeError catch to the two
documented benign messages (`Inner stream not available`, `Event loop
is closed`); any other RuntimeError now logs at ERROR with a full
traceback so executor bugs / runner-context state errors / checkpoint
RuntimeErrors during the post-run flush no longer masquerade as
benign cleanup noise. Still no propagation (we're in an
async-generator finally during teardown) — see the docstring.
- _restore_workflow_checkpoint{,_streaming}: log a WARNING when a
non-None latest checkpoint drains to zero events, so a stale or
partially-written checkpoint_id surfaces as an operator signal
instead of a silent state-loss.
(The `deliver_response` "no destinations resolvable" vs "every
destination errored" concern raised in 3198268038 is already addressed
by the existing `failed` vs `skipped` distinction surfaced through
`DeliveryReport.failed` — see lines 1080-1102 and the
`DeliveryReport` docstring.)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(hosting): reject path-traversal patterns in checkpoint isolation_key
The host's `_resolve_checkpoint_storage` joined `request.session.isolation_key`
directly into the configured `checkpoint_location`. The key is caller-
controlled — sourced from inbound headers (`x-agent-{user,chat}-isolation-key`
injected by the Foundry runtime), from channel-supplied derivations such as
`telegram:<chat_id>` / `entra:<oid>`, or from values set by a channel
`run_hook`. A value like `../../../etc/foo` or an absolute path would let
the resulting checkpoint directory escape the configured root (CWE-22).
This matches the path-traversal class fixed upstream in microsoft#5851 for the
foundry_hosting checkpoint storage.
New `_checkpoint_path_for_isolation_key(root, isolation_key)` helper:
- Uses a denylist (not allowlist) so legitimate namespaced keys
(`telegram:42`, `entra:abc-def`) continue to pass through unmodified.
- Rejects path separators (`/`, `\`), NUL, all-dot reductions (`.`, `..`,
`...`, ...), absolute paths (`os.path.isabs`), and drive-letter prefixes
(`os.path.splitdrive` plus an explicit `^[A-Za-z]:` check so payloads
crafted on a POSIX host still fail closed if the resulting directory
ever round-trips to Windows storage).
- After joining, resolves both sides and verifies
`target.is_relative_to(root)` as defence-in-depth.
`_resolve_checkpoint_storage` now logs a WARNING and returns `None` for
invalid keys rather than crashing the request — checkpointing is best-
effort and we prefer dropping it to letting one malformed key abort an
otherwise valid agent run.
Tests:
- `TestCheckpointPathForIsolationKey` exercises the helper directly with
legitimate keys (alphanumeric, `:`-namespaced, dotted, 200-char), all
rejected traversal patterns from microsoft#5851's MSRC repro list, and
non-string input.
- `TestHostWorkflowCheckpointingPathTraversal` verifies the end-to-end
request path: a traversal key (`../escape`) and an in-key separator
(`evil/sub`) both produce a successful agent response with no files
written under `checkpoint_location`, and the traversal case logs a
WARNING citing `isolation_key`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(hosting): address PR-2 round-3 review feedback + add response hooks
Round-3 review comment fixes:
- _types.py: drop the _EMPTY_MAPPING sentinel; ChannelIdentity.attributes
uses plain dict() as the default — simpler, no extra symbol to track.
- _host.py: drop the local `import asyncio` + `from typing import cast as
_cast` inside `serve()`; rely on the module-level imports.
- _host.py: switch `_log_incoming` to structured `extra={...}` payloads
for both INFO and DEBUG so log aggregators get queryable fields.
- _host.py: delete `_flat_context_providers` and stop descending into a
`.providers` attribute. Aggregator providers (AggregateContextProvider /
ContextProviderBase) are responsible for forwarding `response_context`
to their children themselves; the host treats whatever
`agent.context_providers` exposes as the final, flat list.
- _host.py: stop collapsing agent / workflow output to text. `_invoke`
forwards `AgentResponse.messages` (and `raw_response`) on the
`HostedRunResult`. `_invoke_workflow` builds a per-event message list
via a new `_workflow_output_to_messages` helper that preserves
AgentResponse / AgentResponseUpdate / Message / Content branches and
falls back to text only for arbitrary objects.
- _host.py: `_workflow_event_to_update` carries Content payloads through
unchanged so multi-modal workflow outputs (images, function-call
metadata, ...) survive into channels.
New features (per design discussion in the PR thread):
- HostedRunResult: rebuilt around `messages: list[Message]` with
`.text` / `.contents` as projections, a `raw_response` slot for the
underlying AgentResponse, and a `replace(messages=..., raw_response=...)`
clone helper used by the delivery layer for per-destination isolation.
The `HostedRunResult(text="...")` ctor is preserved as a back-compat
shim that synthesises a single assistant text message.
- ResponseTarget: gain `echo_input: bool = False` (also exposed on
`.channel(name, *, echo_input=...)` / `.channels([...], *, echo_input=...)`).
When set, the host pushes the originating user message to each
non-originating destination before the agent reply. Channels can
filter or transform echoes via their response_hook.
- DeliveryReport: add `echoed` / `echo_failed` tuples to surface
per-destination outcomes of the new echo phase. Echo failures do not
abort the corresponding response push on the same destination.
- ChannelResponseHook + ChannelResponseContext + apply_response_hook:
duck-typed `response_hook` attribute on channels for per-destination
post-processing. Receives a clone of the HostedRunResult and a
context carrying the request, channel name, destination identity,
originating flag, and `is_echo` phase flag. Channels stay
modality-aware (text-only wires flatten via the hook; card-capable
channels render structured contents directly).
- _deliver_response: clone-before-hook fan-out so a hook mutating one
channel's payload cannot leak into another destination's view.
Tests:
- Update _FakeAgentResponse to expose `.messages` (single assistant text
message synthesised from `text`) so existing tests pass unchanged on
the new multi-modal _invoke path.
- Replace the obsolete `test_bind_descends_one_level_into_providers_attribute`
with a regression guard asserting the host does NOT descend into
`.providers` (matches new contract).
- New tests for HostedRunResult multi-modal preservation, echo_input
fan-out with success + failure, response_hook applied per destination,
per-destination mutation isolation, and is_echo phase observability.
Docs:
- spec 002: rewrite Canonical flow with the new input → run_hook → host
→ target → wrap → per-destination clone → response_hook → push
pipeline; document multi-modality contract and per-destination
cloning; add `echo_input` row to ResponseTarget table; rewrite
HostedRunResult/HostedStreamResult row; add ChannelResponseHook /
ChannelResponseContext / apply_response_hook table; log decisions
Q28 (no host-side text collapse), Q29 (duck-typed response_hook),
Q30 (opt-in `echo_input` on ResponseTarget).
- ADR 0026: add ChannelResponseHook + multi-modality bullets;
surface `echo_input` on the ResponseTarget bullet.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(hosting): drop HostedRunResult(text=...) back-compat shim; use from_text()
Pre-release cleanup — no released callers to break, so consolidate on one
canonical entry point plus a classmethod for the ergonomic
single-text-message case:
- HostedRunResult.__init__ takes ``messages`` positionally (required); no
more ``text=`` kwarg overload, no more "synthesise an empty message
when no args" path.
- New HostedRunResult.from_text(text, *, role="assistant", raw_response=None)
classmethod for the common "wrap a single text content as one message"
case (tests, channels emitting plain strings, the echo-input phase
wrapping a user's text turn).
- ``_build_echo_payload`` uses ``HostedRunResult.from_text(raw, role="user")``
for the ``str`` and fallback branches; the other branches use the plain
ctor with explicit ``Message`` lists.
- Tests rewritten to use ``from_text("reply")`` everywhere
``HostedRunResult(text="reply")`` appeared. Added an explicit
``test_from_text_role_kwarg_overrides_default`` regression guard.
- spec 002: HostedRunResult row updated to describe the
``from_text(text, *, role="assistant")`` classmethod instead of the
removed back-compat shim.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(hosting-core): reshape HostedRunResult into generic typed envelope
Replace the flattened multi-modal HostedRunResult (carrying
messages/raw_response/.text projections) with a typed generic
envelope around the target's full-fidelity output:
class HostedRunResult(Generic[TResult]):
result: TResult
session: AgentSession | None
- Agent targets produce HostedRunResult[AgentResponse]; channels
read result.messages, result.text, result.value, result.response_id,
result.usage_details directly off the underlying response.
- Workflow targets produce HostedRunResult[WorkflowRunResult];
channels iterate result.get_outputs() and inspect
result.get_final_state() themselves (the host no longer collapses
workflow outputs onto a synthesised message list).
- The echo-input phase synthesises a HostedRunResult[AgentResponse]
wrapping the user's turn so the same per-destination delivery
machinery applies.
- replace() is now {result, session} only; the host's clone is
shallow — channels that need to mutate result itself are
responsible for their own deep copy.
Rationale: the earlier shape pre-shaped target output (collapsing
workflows onto a Message list, losing per-executor outputs, final
state, and structured value affordances). Carrying the target output
unchanged keeps the host modality-agnostic, gives channel authors
static typing where they want it, and removes 30+ lines of
host-side projection helpers.
Also updates ADR 0026 + spec 002 (Q3, Q28, Q29 amended; new Q31
captures the generic-envelope decision and rationale).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(hosting-core): document echo vs response distinction for push channels
The host already encodes the echo-vs-response phase via the
underlying Message.role on the pushed HostedRunResult:
- echo phase: payload.result.messages[*].role == "user"
- response phase: payload.result.messages[*].role == "assistant"
Both pushes go through the same ChannelPush.push(identity, payload)
entry point. Channels distinguish either by inspecting role (which
works for any push-capable channel) or — when a response_hook is
wired — by branching on ChannelResponseContext.is_echo directly.
Expand the ChannelPush Protocol docstring to make this discoverable
for channel implementers (esp. chat bots that cannot impersonate
the user on their wire and need to render echoes as quoted /
prefixed blocks rather than as bot replies).
Mirror the explanation into the spec's echo_input section.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(hosting-core): fix quickstart to use current Agent API
ChatAgent was renamed to Agent and the preferred construction pattern
is client.as_agent(...). Also drop the sibling channel import so the
snippet imports only modules declared as dependencies of this package;
point readers at the sibling packages instead.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(hosting-core): drop redundant @pytest.mark.asyncio decorators
asyncio_mode = "auto" is configured in pyproject.toml, so individual
@pytest.mark.asyncio decorators are unnecessary.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(hosting): add authorization profiles + IdentityAllowlist seam to ADR/spec
Composes `require_link` + `allowlist` into three named profiles (open,
forced-link, allowlist) with the allowlist itself keyed on either the
channel-native id (pre-link) or a verified IdP claim (post-link), plus
`AnyOf`/`AllOf` combinators for mixed setups. Lifts the design into
an explicit host seam (`host.authorize(...)` → `AuthorizationOutcome`
of `Allowed` / `LinkRequired` / `Denied`) instead of leaving each
channel to roll its own.
Key contract bits:
- Tri-state `AllowlistDecision` (ALLOW / DENY / ABSTAIN) so claim-based
lists can ABSTAIN until claims are available without composition
silently flipping that into DENY.
- `AuthorizationContext` carries explicit `phase` + `claim_source`
so allowlists can tell pre-link from post-link without overloading
`verified_claims is None`.
- Channel-side `allowlist: ... | Literal["inherit"] | None` with an
explicit inheritance sentinel, so the host-level `default_allowlist`
is opt-out, not opt-in.
- Construction-time validator rejects silent-deny configurations
(`LinkedClaimAllowlist` without a claim source) with a typed
`ChannelConfigurationError`.
- Group-chat denial mirrors the existing `LinkChallenge` DM-redirect
pattern; only the redacted `user_message` reaches the wire,
structured `log_details` stay in telemetry.
Ships in two waves: the Protocol + `NativeIdAllowlist` + config
validator land with the next core PR ahead of the linker; the full
pipeline + `LinkedClaimAllowlist` enforcement land with the
`IdentityLinker` core PR.
Updates: ADR 0026 (summary bullet + conceptual-API table row + resolved
Q16), spec 002 (new req microsoft#22, renumbered v1 fast-follow microsoft#23..microsoft#29 and
stretch microsoft#30..microsoft#31, new "Authorization profiles and the IdentityAllowlist
seam" subsection, inbound-ownership row, resolved Q32, follow-up entry).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting): add DurableTaskRunner seam + runtime_mode auto-detect
Introduces the explicit long-running vs ephemeral runtime distinction
and a generic DurableTaskRunner Protocol that owns non-originating
push dispatch — collapsing the previous deliveries[] per-destination
state machine, SupportsDeliveryTracking provider capability, and
Foundry update_item service ask down to a single immutable
intended_targets[] write on the message.
Spec / ADR:
- New §"Runtime modes" with auto-detect markers + defaults matrix.
- Rewrites §"Delivery tracking" → §"Intended targets + durable
delivery": intent-only on the message, operational state lives in
the runner.
- New §"Durable task runner" defining DurableTaskRunner / RetryPolicy
/ TaskHandle / TaskStatus.
- Drops §SupportsDeliveryTracking and §Foundry update_item gap.
- Resolved Qs: 12, 18, 21, 26 revised; new 17/18/19 (ADR) and
33/34/35 (spec).
Code:
- New _runner.py with InProcessTaskRunner (asyncio + bounded retry,
bounded terminal-status cache, register-after-start guard,
shutdown drain).
- _host.py: runtime_mode + durable_task_runner ctor params;
auto-detect via FOUNDRY_HOSTING_ENVIRONMENT /
AZURE_FUNCTIONS_ENVIRONMENT / AWS_LAMBDA_FUNCTION_NAME;
HOSTING_PUSH_TASK_NAME handler registered eagerly so
_deliver_response can be called outside the lifespan;
_handle_push_task does echo-then-response inline per destination;
_deliver_response now schedules one task per destination via the
runner (DeliveryReport.pushed = scheduled; .failed = schedule-time
outage only).
- _types.py: new DurableTaskRunner Protocol + RetryPolicy /
TaskHandle / TaskStatus; DeliveryReport drops echoed /
echo_failed (echo outcome owned by the runner).
- __init__.py exports the new public surface.
Tests: 132 passing, 90% coverage. New test_runner.py covers
InProcessTaskRunner success/retry/terminal-failure/cancellation/
register-after-start, runtime-mode auto-detect with synthetic env,
and the warning-on-ephemeral-without-runner path. test_host.py
delivery tests use a sync runner fake for deterministic assertions
and validate the new "schedule succeeded vs runner backend
unreachable" semantics.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting): rubber-duck round-5 — strict ephemeral, codec seam, allowlist Wave-1, drop DeliveryReport
Adopts the rubber-duck-approved package of changes from the round-5
review of PR microsoft#5638 (modulo DeliveryReport.failed — the value type is
removed entirely now that durable delivery covers the failure
surface, per user direction).
Code:
- Drop DeliveryReport value type; host-internal _deliver_response
returns bool. Failure observability is now logs (in-process) /
runner backend (durable adapters).
- Strict ephemeral default: ephemeral runtime_mode with the default
in-process runner raises RuntimeError; opt-in via
allow_in_process_runner=True (warns).
- ChannelPushCodec Protocol + DurableTaskPayloadMode enum +
_validate_runner_codec_pairing so JSON-mode runners can be safely
paired with channels via codecs; _handle_push_task accepts both
object- and JSON-envelope shapes.
- ResponseTarget.identity(...) / .identities([...]) builders +
IDENTITIES kind for explicit caller-supplied recipients; field
rename identities → _target_identities (private) with a
target_identities property to resolve the classmethod collision.
- Intent-only audit: _annotate_intended_targets writes
hosting.intended_targets / skipped_targets / includes_originating /
originating_channel onto assistant messages — single immutable
write per the runner-owned operational-state model.
- InProcessTaskRunner: 2-phase drain on shutdown
(shutdown_grace_seconds, default 5.0) so a clean shutdown does not
abandon work mid-retry; payload_mode = OBJECT class-level.
- Echo idempotency: _handle_push_task tracks an echo_done cursor on
runner-owned task state so a retry that fires after the echo
phase succeeded does not double-echo.
Wave-1 authorization seam (full landing):
- New _authorization.py with AllowlistDecision tri-state,
AuthorizationContext, IdentityAllowlist Protocol, AllowAll /
NativeIdAllowlist (with async loader cache + channel-scope ABSTAIN) /
LinkedClaimAllowlist (raise-until-Wave-2) / AnyOfAllowlists /
AllOfAllowlists / CallableAllowlist built-ins, Allowed /
LinkRequired / Denied outcomes, ChannelConfigurationError.
- Host(default_allowlist=..., identity_linker=...) + per-channel
allowlist parameter with 'inherit' / None semantics.
- _validate_channel_authorization enforces all three rules at
construction: claim-source requirement, linker presence for
require_link=True (elevated from no-op — must not ship
unenforced), and NativeIdAllowlist(channel=...) typo detection.
Combinator-walking via _flatten_allowlists catches nested
misconfigs.
- host.authorize(...) for the native-id pipeline: open path returns
Allowed with auto-issued <channel>:<native_id> isolation key (or
the existing key when the identity has been seen); ABSTAIN on a
claim-required allowlist maps to
Denied(reason_code='allowlist_requires_link') until Wave 2 wires
the linker to convert it to LinkRequired.
Spec / ADR:
- docs/specs/002-python-hosting-channels.md: Wave-1 status updated
to reflect the linker-presence rule elevation and the
host.authorize landing; new sub-sections (codec contract, drain,
echo cursor); Qs 18 / 21 DeliveryReport references purged; new
resolved Qs 36–40 covering the strict-ephemeral default, codec
contract, DeliveryReport removal, echo cursor, and drain.
- docs/decisions/0026-hosting-channels.md: Q12 DeliveryReport
reference purged; Q16 updated to reflect Wave-1 landing; new
resolved Qs 20 (codec contract) + 21 (strict ephemeral / drain /
echo cursor).
Tests:
- New tests/test_authorization.py (35 cases) covering every Wave-1
built-in, the three validator rules, combinator decision
semantics, and host.authorize across open / allow / deny /
abstain-with-claim-dep / abstain-without-claim-dep paths plus
existing-key reuse and verified-claims propagation.
- tests/test_host.py: TestDeliverResponse rewritten for the bool
return + runner.scheduled-count assertions; new tests for
IDENTITIES variant + echo idempotency.
- tests/test_runner.py: strict-ephemeral now expects RuntimeError;
allow_in_process_runner opt-in tests; shutdown drain test;
payload_mode default test.
- tests/test_types.py: TestDeliveryReport removed; new
TestDurableTaskPayloadMode + TestResponseTargetIdentities.
Validation: 178 tests pass, 91% coverage, fmt + lint + pyright +
mypy clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(hosting): add mermaid flow diagrams to ADR, spec, README
Insert the 10 hosting flow diagrams reviewed in
python/.user/hosting-diagrams.md into the public docs:
- README: runtime topology (1a) + cross-link to the spec for the
richer set.
- ADR: runtime topology, channel contribution shape, and authorization
decision (1a, 1b, 3) at the end of 'Conceptual API shape'.
- Spec: all 10 diagrams — 1a/1b at the top of API Surface, 2 in
Canonical flow, 3 in Authorization profiles, 4-7 in Scenarios 6-8,
8 in Codec contract, 9 in Echo idempotency, 10 in Scenario 9.
Doc-only; no API or behaviour change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting): add opt-in disk persistence via state_dir
Long-running hosts (always-on container, single-VM bot, local dev) lose
state on every restart today. Add an opt-in disk persistence layer under
a new `state_dir` constructor parameter on `AgentFrameworkHost` that
survives process restarts without taking on a heavyweight database
dependency.
Backed by `diskcache` (installed via the new `[disk]` optional extra).
An OS-level advisory file lock guarantees single-owner semantics so two
hosts pointed at the same directory cannot double-execute scheduled
pushes.
What persists when `state_dir` is set:
- Pending durable-task records — scheduled-but-not-yet-completed pushes
replay on the next host startup via `InProcessTaskRunner.resume()`.
Records that crashed mid-attempt resume with the already-consumed
retry budget (no full-budget re-grant).
- `_session_aliases` — per-isolation-key session-id rewrites.
- `_active` — most-recently-active channel per isolation key.
- `_identities` — `ChannelIdentity` rows for fan-out targeting,
including nested mutations of the form
`self._identities[ik][channel] = identity`.
The `state_dir` parameter accepts any of:
- `None` — today's purely in-memory behaviour.
- `str` / `PathLike` — single root; host auto-creates `runner/` and
`sessions/` subfolders.
- `HostStatePaths` TypedDict / plain mapping — per-component overrides
routed to different roots. Unknown keys raise `ValueError` to surface
typos early.
Unpicklable push payloads raise `PushPayloadNotPicklable` eagerly from
`schedule()` so issues surface at the call site rather than on the
next restart. Corrupt on-disk records are quarantined-and-logged; the
runner never crashes on resume.
Live `AgentSession` objects stay in memory and are rehydrated lazily
by the history provider on the next turn.
- New modules: `_persistence.py` (lock + normalisation),
`_state_store.py` (session-bookkeeping store).
- Runner rewrite: 4-state model (`pending` / `succeeded` / `failed`
/ `cancelled`); the transient `running` state was a bug that caused
resume to skip records that crashed mid-handler.
- New tests: `test_runner_disk.py` (8 tests), `test_host_disk.py` (8
tests). 194 passed total. pyright + mypy + ruff clean.
- README: new "Optional disk persistence" section with code samples.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting): add checkpoints to state_dir + fix host docstring
Three related polish changes on top of the disk-persistence landing:
1. Extend `state_dir` to cover workflow checkpoints. Adds
`checkpoints` as a third `HostStatePaths` key. Single-path form
(`state_dir="/foo"`) now also auto-derives `/foo/checkpoints/`
for workflow targets (equivalent to passing
`checkpoint_location="/foo/checkpoints"`). The mapping form lets
workflow callers opt out by omitting the key, or route checkpoints
to a different volume.
Conflict / precedence rules:
* Explicit `checkpoint_location` always wins over the state_dir
derived path; a warning surfaces the double-config.
* Single-path `state_dir` + non-Workflow target → checkpoints path
silently ignored (no eager directory creation either).
* Mapping form with `checkpoints` + non-Workflow target → warn
(almost certainly dead config).
* Derived path with a workflow that already has its own
`checkpoint_storage` → same `RuntimeError` as the explicit
parameter triggers, so ownership stays unambiguous.
Checkpoint persistence uses `FileCheckpointStorage` from the
framework core — no extra dependency. Only `runner` and
`sessions` require the `[disk]` extra.
2. Move `AgentFrameworkHost.__init__` parameter docs from `Args:` to
`Keyword Args:` for every parameter after the `*`. Only `target`
remains under `Args:`. Brings the docstring in line with the
actual signature (the params have always been keyword-only).
3. `HostStatePaths` already existed as a TypedDict but did not cover
`checkpoints`; updated to document the new key with the same
per-attribute docstring style as `runner` / `sessions` so editors
can surface help on the keys.
Validation: 201 tests pass (was 194; +7 checkpoint integration tests
in test_host_disk.py). pyright + mypy + ruff + bandit clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting): add core IdentityLinker authorization seam
Fold the core IdentityLinker pieces into the hosting-core PR so the
authorization surface no longer has a deferred Wave-2 placeholder.
Provider-specific linkers (for example Entra OAuth helpers) can now plug
into core without core depending on an IdP SDK.
Core additions:
- Add LinkChallenge, LinkedIdentity, LinkResolution, and IdentityLinker.
IdentityLinker.resolve(identity) is a single-call decision that returns
either a linked identity with verified claims or a challenge the channel
can render.
- Enable LinkedClaimAllowlist end-to-end. It now abstains pre-link and
allows/denies post-link against verified claims, including multi-valued
claims such as groups.
- Add AuthPolicy factories for common allowlist shapes.
- Extend Allowed with verified_claims and claim_source for audit/telemetry
without requiring callers to re-derive how the decision was made.
Host behavior:
- identity_linker is now typed as IdentityLinker | None.
- authorize() supports open, native-id, forced-link, and linked-claim
profiles end-to-end.
- require_link=True resolves via the linker and returns LinkRequired when
the identity is not linked.
- claim-based allowlists use channel-emitted verified_claims when present,
or linker-resolved claims otherwise.
- authorize() remains decision-only and does not mutate _identities/_active;
identity registry writes remain on the actual request execution path.
Docs/tests:
- Remove Wave-1/Wave-2 language from core/spec/ADR surfaces touched here.
- Update the spec/ADR to describe the core linker seam and provider-specific
linker packages.
- Add authorization tests for linker challenges, linked identities, linked
claim allowlists, channel-emitted claims, AuthPolicy factories, and the
no-mutation contract.
Validation: 214 tests pass, pyright/mypy/ruff clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting): add link-store path to state_dir
Identity linking introduces host-adjacent state that needs the same state_dir treatment as runner, session, and checkpoint state. Add a links component to the host state paths so applications and linker packages have a typed, discoverable persistence location.
Changes:
- Extend HostStatePaths with links and include it in state_dir normalization (state_dir/links/ for the single-path form).
- Add SupportsLinkStorePath, an optional protocol for identity linkers that accept a host-provided link-store path.
- AgentFrameworkHost now offers state_dir links to compatible linkers, warns when an explicit links path is supplied without a linker, and warns when the configured linker manages persistence directly instead of implementing SupportsLinkStorePath.
- Update README and spec text to document the link-store component and clarify that concrete linkers still own the storage format.
- Add disk-state tests for compatible, missing, and non-configurable linkers.
Validation: 217 tests pass, pyright/mypy/ruff clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…icrosoft#5637) * refactor(foundry_hosting): build FoundryHostedAgentHistoryProvider on azure.ai.agentserver SDK Rebuilds the Foundry hosted-agent history provider on top of ``azure.ai.agentserver``'s ``FoundryStorageProvider`` instead of the in-house ``_HttpStorageBackend``. Splits the monolithic ``_responses.py`` into focused modules: - ``_history_provider.py`` — new ``FoundryHostedAgentHistoryProvider`` that talks to the SDK's ``FoundryStorageProvider``, threads ``response_id`` / ``previous_response_id`` through ``ContextVar``s via ``bind_request_context``, and lifts host-bound isolation keys (``x-agent-{user,chat}-isolation-key``) from the optional ``agent_framework_hosting`` package into a provider-local ``IsolationContext`` so the storage layer carries the correct partition keys without channels having to know about them. - ``_shared.py`` — extracts all SDK ``Item`` / ``OutputItem`` ↔ framework ``Message`` conversion helpers into one place so both ``_responses.py`` and the new history provider can share them. Restores ``_convert_file_data`` for inline ``input_file`` payloads, and the hosted-MCP routing for ``custom_tool_call_output`` items whose ``call_id`` carries the ``mcp_*`` prefix. - ``_ids.py`` — shared id helpers. - ``_responses.py`` — shrinks ~700 lines, re-exports converters for back-compat with existing tests. - ``tests/test_history_provider.py`` — exercises the new provider against a fake SDK backend; the host-isolation test is gated on the optional ``agent_framework_hosting`` import. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(foundry_hosting): add local_storage_root for file-based dev history Adds an optional `local_storage_root: str | Path | None` parameter to `FoundryHostedAgentHistoryProvider`. When set and the provider is running outside a Foundry Hosted Agent container, conversations are persisted to JSONL files via `agent_framework.FileHistoryProvider` laid out as: {root}/{user_key or '~none'}/{chat_key or '~none'}/{session_id}.jsonl Hosted mode (FOUNDRY_HOSTING_ENVIRONMENT set) ignores the option with a one-time INFO log so Foundry storage always wins on the platform. The in-memory fallback is unchanged when the option is omitted. Path safety: isolation segments are validated against the same character allowlist FileHistoryProvider uses for session-id stems and base64-url-encoded with a reserved "~iso-" prefix when unsafe. "~none" sentinel for missing keys can never collide with a real isolation key (real keys starting with "~" are encoded). The resolved target dir is also re-checked to be inside the configured root. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(foundry_hosting): address PR-1 review comments - _shared.py:_capture_raw narrows `except Exception` to `except TypeError` and emits a WARNING with traceback so the lossy fallback to a synthesized round-trip is observable. Mirrors the reviewer suggestion. - _history_provider.py:save_messages narrows `except Exception` to `except FoundryStorageError` so only storage-validation failures (4xx/5xx, opaque server errors) are swallowed. Network / TLS / auth / payload-builder bugs propagate so the caller can retry / alert. Adds an instance-level `failed_writes` counter operators can poll for silent-drop visibility. - _history_provider.py id-stamping loop: drops the `contextlib.suppress(AttributeError, TypeError)` around `item.id = new_id` so SDK contract changes surface in the test suite instead of silently corrupting the chain (the storage backend rejects the entire `create_response` with HTTP 500 when synthetic prefix-based ids leak through). `import contextlib` removed. - tests: * Unit-cover `foundry_response_id` / `foundry_response_id_factory` / `foundry_item_id` so SDK `IdGenerator` contract changes are caught locally. * Cover the `save_messages` wire payload: required-by-storage fields (`background`, `parallel_tool_calls`, `instructions`, `agent_reference`), env-var-driven stamping (`FOUNDRY_AGENT_NAME` / `FOUNDRY_AGENT_VERSION` / `FOUNDRY_AGENT_SESSION_ID` / `MODEL_DEPLOYMENT_NAME` with `AZURE_AI_MODEL_DEPLOYMENT_NAME` fallback), and the rule that `model` / `agent_session_id` / `agent_reference.version` are omitted (not stamped to `None`) when their env vars are unset. * Cover the `FOUNDRY_AGENT_SESSION_ID` last-resort chain anchor on both the get and save paths, including the prefix gate that blocks non-`caresp_*`/`resp_*` values from reaching storage, and the precedence rule that a host binding wins over the env. * Replace the old `test_save_messages_swallows_backend_errors` with two tests asserting the new contract: storage errors are swallowed and bump `failed_writes`; everything else propagates and leaves the counter at zero. 141 unit tests pass; mypy + pyright + ruff clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(foundry_hosting): address PR-1 round-2 review comments - Hosted detection now delegates to AgentConfig.from_env().is_hosted so a future Foundry SDK rename of FOUNDRY_HOSTING_ENVIRONMENT propagates automatically; drop the local _ENV_FOUNDRY_HOSTING_ENVIRONMENT constant. - Drop the FOUNDRY_AGENT_SESSION_ID fallback in both get_messages and save_messages: per the SDK it identifies the *container instance*, not the conversation, so chaining off it would silently merge unrelated conversations across container restarts. The host-bound previous_response_id (set by ResponsesChannel) is the only authoritative anchor; the env value is still stamped into the persisted envelope's agent_session_id for operator correlation. - Update module docstring + replace TestFoundryAgentSessionIdAnchor with assertions for the new contract (env var ignored as anchor, still stamped onto persisted envelope, host binding wins). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(foundry_hosting): reconcile with upstream main (microsoft#5851, microsoft#5666) Brings the FoundryHostedAgentHistoryProvider refactor branch back into sync with the foundry_hosting changes that have landed on upstream main since PR-1 was opened: * microsoft#5851 (path traversal in checkpoint storage, CWE-22). The workflow-host code in ``_responses.py`` builds a ``FileCheckpointStorage`` from a caller-controlled ``context_id`` (``previous_response_id`` / ``conversation_id`` / ``response_id``). Switch both call sites to route through ``_checkpoint_storage_for_context``, which rejects separators, NUL bytes, drive letters, absolute paths, and all-dot segments, and enforces ``is_relative_to(root)`` before any directory is created. * microsoft#5666 (function approval flow). Make the SDK-Item → AF-Message conversion helpers in ``_shared.py`` async and accept an optional ``approval_storage`` keyword: - ``_items_to_messages`` / ``_item_to_message`` / ``_item_to_message_inner`` - ``_output_items_to_messages`` / ``_output_item_to_message`` / ``_output_item_to_message_inner`` For ``mcp_approval_request`` / ``mcp_approval_response`` items the helpers now load the original function-call Content from the approval storage (via ``ApprovalStorage.load_approval_request``) instead of synthesising a placeholder. This matches upstream semantics and lets approval round-trips reconstruct the real payload. The ``ApprovalStorage`` Protocol moves to ``_shared.py`` so the conversion helpers can reference it without pulling in ``_responses.py`` (which would create a circular import). The concrete ``InMemoryFunctionApprovalStorage`` and ``FileBasedFunctionApprovalStorage`` stay in ``_responses.py`` next to the host that owns them, and re-export ``ApprovalStorage`` from ``_shared`` for compatibility. The workflow-host streaming path passes its own ``self._approval_storage`` into ``_to_outputs`` so approval requests are saved at emit time. * Bump ``_history_provider.FoundryHostedAgentHistoryProvider.get_messages`` to ``await`` the now-async ``_output_items_to_messages`` call. No public API change beyond the new keyword-only ``approval_storage`` parameter on the four conversion entry points. Validation: - uv run poe check-packages -P foundry_hosting (lint + pyright clean) - uv run poe mypy -P foundry_hosting (clean) - uv run poe test -P foundry_hosting (183 passed, 1 skipped) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting-responses): add OpenAI Responses-shaped channel package
New ``agent-framework-hosting-responses`` package implementing the
OpenAI Responses-shaped HTTP channel for the Hosting framework. Mounts
``POST /responses`` (and a ``/responses/{response_id}`` GET) onto an
``AgentFrameworkHost`` and translates the OpenAI Responses wire shape
to/from the channel-neutral ``ChannelRequest`` / ``HostedRunResult``
plumbing.
Surface (re-exported from ``agent_framework_hosting_responses``):
- ``ResponsesChannel`` -- concrete ``Channel`` implementation. Owns the
Starlette route(s), parses inbound JSON into ``ChannelRequest``, runs
the optional ``ChannelRunHook``, calls back into the
``ChannelContext`` to invoke the agent target, builds Responses
envelopes (sync JSON or SSE), and respects
``DeliveryReport.include_originating`` so cross-channel push routes
only ack to the originating Responses caller.
- The minted ``response_id`` is propagated via the host's ContextVar
machinery so storage-side history providers (e.g.
``FoundryHostedAgentHistoryProvider``) persist envelopes against the
same id the channel returns.
- 48 unit tests covering route wiring, parsing of each Responses input
shape, hook composition, sync vs streaming paths, and originating
vs non-originating delivery branches.
Registers the package in ``python/pyproject.toml`` ``[tool.uv.sources]``
and adds the matching pyright ``executionEnvironments`` entry.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* review: address PR-3 round 2 feedback
- consume IsolationKeys.chat_key from the host-bound contextvar instead
of the raw `x-agent-chat-isolation-key` header off the wire so the
host's ASGI isolation middleware (or any operator-supplied
replacement) is the authoritative point at which the caller is
authenticated and the bucket key is established
- expand `response_id_factory` docstring to call out partition
co-location vs. partition-ownership enforcement: the channel forwards
`previous_response_id` as a hint to the factory; the storage layer
validates the embedded partition against the bound user/chat
isolation keys
- on mid-stream failure, call `deliver_response` with the accumulated
text before emitting `response.failed` so host-side history /
push-channel state stays consistent with the partial deltas the
client already saw
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(hosting-responses): fix quickstart to use current Agent API
ChatAgent was renamed to Agent and ChatMessage to Message. Update the
README quickstart to use client.as_agent(...) and refresh the stale
docstring reference in _channel.py.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(hosting-responses): adapt to hosted run result wrapper
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting-responses): add response hooks
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(hosting-responses): keep instructions in chat options
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting-invocations): add Invocations channel package New ``agent-framework-hosting-invocations`` package implementing the "Invocations" HTTP channel for the Hosting framework -- a lightweight JSON-over-HTTP shape (``POST /invocations``) for callers that want a single request/response without committing to the full OpenAI Responses envelope. Mounts onto an ``AgentFrameworkHost`` like any other channel. Surface (re-exported from ``agent_framework_hosting_invocations``): - ``InvocationsChannel`` -- concrete ``Channel`` implementation. Owns the Starlette route, parses inbound JSON into a ``ChannelRequest`` (``input`` / ``session`` / ``metadata`` / ``options``), runs the optional ``ChannelRunHook``, calls back into the ``ChannelContext`` to invoke the agent target, and returns a flat JSON envelope (or an SSE stream when ``stream=true``). - 8 unit tests covering route wiring, isolation-key passthrough, hook composition, sync vs streaming paths, and ack-only behaviour for non-originating ``DeliveryReport``s. Registers the package in ``python/pyproject.toml`` ``[tool.uv.sources]`` and adds the matching pyright ``executionEnvironments`` entry. Independent of PR-3 (Responses); both depend only on PR-2 (Hosting core). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * review: address PR-4 round 2 feedback - expand `_stream` docstring to call out the HTTP-200 + `event: error` SSE contract (status committed before generator runs; hard failures surface as the first SSE frame, not an HTTP code) - split chunked text on full-line terminators via `splitlines()` so embedded `\r` / `\r\n` no longer leak into `data:` framing on the wire, breaking EventSource consumers - on `get_final_response()` failure, emit `event: error` instead of silently swallowing — finalize is what triggers history-provider persistence on the agent side, so a 5xx / disk-full / context-provider error must reach the client - add tests covering `stream_transform_hook` (rewrite, drop, async), CRLF-in-chunk framing, and the finalize-error → no-`[DONE]` contract Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(hosting-invocations): rename stale ChatMessage docstring reference to Message Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(hosting-invocations): adapt to hosted run result wrapper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(hosting-invocations): add response hooks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting-telegram): add Telegram channel package New ``agent-framework-hosting-telegram`` package implementing the Telegram Bot API channel for the Hosting framework. Mounts a webhook endpoint (``POST /telegram/webhook``) and an in-process polling loop onto an ``AgentFrameworkHost`` and translates Telegram ``Update`` payloads to/from the channel-neutral ``ChannelRequest`` / ``HostedRunResult`` plumbing. Surface (re-exported from ``agent_framework_hosting_telegram``): - ``TelegramChannel`` -- concrete ``Channel`` implementation. Owns the webhook route + an optional ``getUpdates`` long-polling lifespan, parses Telegram ``Update``s into ``ChannelRequest`` (text, photo, document, voice, callback_query, …), runs the optional ``ChannelRunHook``, calls back into the ``ChannelContext`` to invoke the agent target, and posts the response back via ``sendMessage`` / ``sendChatAction`` / ``answerCallbackQuery`` on the Telegram Bot API. Honours ``DeliveryReport.include_originating`` so cross-channel pushes can target the originating Telegram chat without double-acking. - Native fields the channel doesn't lift onto ``ChannelRequest`` (e.g. ``chat.type``, ``message.message_id``, ``callback_query.data``) are attached to ``ChannelRequest.attributes`` so a ``ChannelRunHook`` can pick them up via the standard ``protocol_request=`` kwarg. - 13 unit tests covering route wiring, ``Update`` parsing across the common content shapes, hook composition, and originating vs non-originating delivery branches. Registers the package in ``python/pyproject.toml`` ``[tool.uv.sources]`` and adds the matching pyright ``executionEnvironments`` entry. Stacks on PR-2 (Hosting core); independent of PR-3 / PR-4. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(hosting-telegram): preserve in-chat ordering, ack-before-run, drain shutdown - Replace per-update task fan-out with per-chat asyncio.Queue + worker. Telegram only guarantees update ordering up to getUpdates; the previous code spawned one task per update, which broke ordering for adjacent updates in the same chat. Updates are now serialised per chat_id (so /start then "what's the weather" can't race) while different chats still process in parallel. - Webhook handler now acks (200) immediately and runs the agent in the per-chat worker. Telegram redelivers any update the webhook doesn't 200 within ~60 seconds, so a streamed agent reply that runs longer than that previously triggered a retry storm and duplicate replies. - _on_shutdown now drains everything: poll task → per-chat workers → webhook-spawned dispatcher tasks (the new ack-before-run path), then deletes the webhook + closes the HTTP client. Previously webhook tasks were not tracked at all, so an in-flight agent invocation could leak past app shutdown. - _enqueue_update extracts chat_id from message / edited_message / callback_query; updates with no resolvable chat fall back to a one-shot dispatcher task that's still tracked in _update_tasks for shutdown. - Webhook handler now also returns 400 on malformed JSON / non-object payloads instead of crashing the request. 4 new tests cover per-chat serial ordering, parallel-across-chats isolation, ack-before-run latency, and shutdown drain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(hosting): drop redundant @pytest.mark.asyncio decorators asyncio_mode = "auto" is configured in pyproject.toml across the hosting packages, so individual @pytest.mark.asyncio decorators are unnecessary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(hosting-telegram): adapt push tests to hosted run result wrapper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(hosting-telegram): add response hooks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…oft#5641) * feat(hosting-activity-protocol): rename Bot Framework channel to ActivityProtocolChannel The existing Bot-Framework-via-Azure-Bot-Service channel was previously shipped under the name ``hosting-teams`` / ``TeamsChannel``. That name is misleading for what the channel actually does -- it speaks the Bot Framework Activity Protocol against Azure Bot Service, which fans out across MS Teams, Slack, Webex, Telegram-via-Bot-Service, etc., and does not provide any Teams-specific affordances. This PR renames the package atomically and frees the ``hosting-teams`` name for a future Teams-native channel built on ``microsoft-teams-apps`` (PR-5b, spec req microsoft#28). Renames (all in one commit): - Package: ``agent-framework-hosting-teams`` -> ``agent-framework-hosting-activity-protocol`` - Module: ``agent_framework_hosting_teams`` -> ``agent_framework_hosting_activity_protocol`` - Channel class: ``TeamsChannel`` -> ``ActivityProtocolChannel`` - Helper: ``teams_isolation_key`` -> ``activity_protocol_isolation_key`` (isolation key prefix ``teams:`` -> ``activity:``) - Channel name: ``"teams"`` -> ``"activity"``; default mount path ``/teams`` -> ``/activity`` - Internal helper: ``_parse_teams_activity`` -> ``_parse_activity`` - Worker task name + a couple of error strings updated for consistency Updates README.md and the module docstring to call out: - this is the channel-neutral Activity Protocol channel, - it surfaces what every Bot-Service-connected channel has in common (text in / text out), - a forthcoming ``agent-framework-hosting-teams`` package will layer Teams-specific affordances (adaptive cards, message extensions, dialogs, SSO, ...) on the same Bot Service transport. Workspace: registers ``agent-framework-hosting-activity-protocol`` in ``python/pyproject.toml`` and adds the matching pyright ``executionEnvironments`` entry. Behavior is unchanged. Pyright + mypy clean, 11 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * review: address PR-5 round 2 feedback - security (#3198327004): add `service_url_allowed_hosts` constructor option (default `botframework.com` + `smba.trafficmanager.net`) and reject inbound activities whose `serviceUrl` host falls outside it with HTTP 400 — without this gate a malicious caller could redirect outbound replies (and the attached bearer token) to an attacker-controlled host - security (#3198324219): add `inbound_auth_validator` async callback; log a loud WARNING at startup when no validator AND no operator reverse-proxy is configured so the dev-mode bypass cannot accidentally ship to production. Document the contract: prototype intentionally does not ship JWT validation (out of scope); operators must plug a validator or terminate auth in front of the channel - retry semantics (#3198328746): distinguish transient outbound failures (httpx network errors, non-2xx from Bot Service) — return 502 so Bot Service retries — from deterministic agent failures — return 200 so Bot Service does not retry the same broken activity in a loop - bug (#3198330424): fix the placeholder-failure deadlock. When `send_initial_placeholder` fails, `activity_id` stays `None`, the edit-worker loop exit condition (`accumulated == last_sent`) is unreachable while no PUT is possible, and the worker would deadlock on `wake.wait()` forever after `worker_done` is set. Now: skip the worker entirely on placeholder failure and POST a single final activity at the end with whatever accumulated - tests (#3198334465, #3187178091, #3198336045): add coverage for - `_is_service_url_allowed` allow/deny matrix + webhook 400 on disallowed serviceUrl - `inbound_auth_validator` allow/deny/raises paths - outbound `Authorization: Bearer <token>` header presence in production mode and absence in dev mode - the streaming path (`_stream_to_conversation`): placeholder + final edit, placeholder-failure fallback (with timeout guard against deadlock regression), and empty-stream `(no response)` placeholder replacement - retry-signal differentiation: outbound `httpx.ConnectError` → 502; deterministic `ValueError` from the agent → 200 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(hosting): drop redundant @pytest.mark.asyncio decorators asyncio_mode = "auto" is configured in pyproject.toml across the hosting packages, so individual @pytest.mark.asyncio decorators are unnecessary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(hosting-activity-protocol): add response hooks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(hosting-activity-protocol): mark constructor keyword args Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…osoft#5644) * feat(hosting-entra): add Entra (Azure AD) identity-linking channel New ``agent-framework-hosting-entra`` package implementing a Microsoft Entra OAuth-based identity-linking channel for the Hosting framework. Mounts a small set of routes (``/entra/login``, ``/entra/callback``, ``/entra/whoami``) that walk a user through an Entra/Azure AD authorization-code flow and stick the resulting verified identity (``oid`` / ``email`` / ``tid``) onto the host's identity table so later requests on any other channel (Responses, Telegram, …) can be linked to the same user. Surface (re-exported from ``agent_framework_hosting_entra``): - ``EntraChannel`` -- concrete ``Channel`` implementation. Owns the three Starlette routes, signs/verifies short-lived ``state`` tokens to bind the round-trip to the originating channel, exchanges the authorization code for an ID token via MSAL, and writes the verified identity into the host's identity store via the standard ``ChannelIdentity`` plumbing so cross-channel push (e.g. send a Telegram message to the user who completed the link from Responses) works without the channels having to coordinate directly. - 14 unit tests covering route wiring, ``state`` issue / verify, callback exchange happy + failure paths, and identity-store write. Registers the package in ``python/pyproject.toml`` ``[tool.uv.sources]`` and adds the matching pyright ``executionEnvironments`` entry. Stacks on PR-2 (Hosting core); independent of PR-3 / PR-4 / PR-6. The cross-channel sample (``local_identity_link/``) that demonstrates this end-to-end alongside Responses + Telegram lands in PR-8 (samples). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(hosting-entra): close IDOR + reflected-XSS + open-redirect on the OAuth flow Three SECURITY-CRITICAL fixes flagged in round-2 review. 1. IDOR on /auth/start (3198518308). Without authentication the endpoint accepted (channel, channel_id) from the query string and bound *whoever signed in* to that pair. An attacker could bind their own Entra oid to a victim's per-channel id (e.g. `telegram:<victim_chat_id>`), redirecting all of the victim's future inbound traffic to the attacker's isolation key. Fix: introduce link_token_secret + mint_start_url(channel, id, ...). When set, /auth/start requires `exp` + `sig` (HMAC-SHA256 over `channel|channel_id|expires_at`) before issuing the redirect. Channels that hand out start URLs (a Telegram /link command after verifying the inbound webhook signature) call mint_start_url so the token proves the (channel, id) pair was authorised by the channel that owns the surface. Unsigned mode is opt-in and logs a loud WARNING at startup *and* on every accepted request. 2. Reflected XSS on /auth/callback (3198520256, 3198527896). `error`, `error_description`, channel_key (from the unauthenticated /start query), and `upn` (from a Graph response) flowed straight into the text/html response body unescaped. With the IDOR above, an attacker could stash `<script>` payloads in `channel` or `id` and serve them from the auth host's origin (full XSS on the auth surface — cookies/storage of anything else mounted there). Fix: html.escape() every value before HTML output. 3. Open redirect on `return_to` (3198524746). Accepted any URL. Fix: `_validate_return_to` allows only relative paths starting with `/` (and not `//`) or absolute URLs whose host equals the configured `public_base_url` host. Validated at /start mint time AND defensively re-validated at /callback before redirect. 12 new tests cover signed-token rejection (missing/forged/expired), mint helper requirements, startup warning visibility, XSS escaping on both error and success paths, and the open-redirect allowlist (external rejected, relative accepted, same-origin accepted, protocol-relative `//evil.example/` rejected). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(hosting): drop redundant @pytest.mark.asyncio decorators asyncio_mode = "auto" is configured in pyproject.toml across the hosting packages, so individual @pytest.mark.asyncio decorators are unnecessary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* samples(hosting): add hosting Channels sample apps under samples/04-hosting/af-hosting Adds five end-to-end sample apps under ``python/samples/04-hosting/af-hosting/`` that exercise the ``agent-framework-hosting`` Channels stack from the simplest single-channel case up to a multi-channel deployment with cross-channel identity linking. Samples (ordered by complexity) ------------------------------- * ``foundry_hosted_agent/`` — minimal Responses + Invocations host with a Foundry-backed agent and ``FoundryHostedAgentHistoryProvider``. ``agd``-deployable; bundles a ``Dockerfile`` and ``scripts/vendor-packages.sh`` that copies workspace packages into ``_vendor/`` for self-contained builds. ``_vendor/`` is gitignored. * ``local_responses/`` — single-channel Responses host with a ``run_hook`` that strips caller-supplied options and forces a reasoning preset. Demonstrates the hook seam over the uniform ``ChannelRequest`` envelope. * ``local_responses_workflow/`` — Responses + Invocations exposing a three-agent workflow with per-conversation checkpoint storage. * ``local_telegram/`` — Responses + Telegram with a ``@tool``, ``FileHistoryProvider``, hooks, and a ``ResponseTarget`` multicast variant (``call_server_multicast.py``) that pushes a single Responses reply to a separate Telegram chat. * ``local_identity_link/`` — full surface: Responses + Invocations + Telegram + Activity Protocol (Teams) + the ``EntraIdentityLinkChannel`` sidecar. Resolves per-channel ids onto a single Entra object id so a user's history follows them across surfaces. Notes ----- * Samples that use Telegram/Teams via Activity Protocol depend on the renamed ``agent-framework-hosting-activity-protocol`` package (see the PR-5 series). * All samples use ``[tool.uv.sources]`` editable workspace deps, except ``foundry_hosted_agent/`` which uses the ``./_vendor/`` self-contained layout for ``azd`` Docker builds. * Each sample includes a ``README.md`` with run instructions and an ``app.py`` ASGI entrypoint plus a ``call_server.py`` client harness. Depends on the prior hosting PRs (foundry-hosted-agent refactor + hosting-core + the per-channel packages). After those merge, this branch can be rebased onto ``main`` cleanly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * samples(hosting): point sample deps at the feature/python-hosting GitHub branch Switches every sample's ``[tool.uv.sources]`` from in-monorepo editable path deps (which only resolve when running inside the agent-framework workspace) to git refs targeting the ``feature/python-hosting`` branch on ``microsoft/agent-framework``. Samples now install standalone outside the monorepo while the ``agent-framework-hosting*`` packages are still pre-PyPI; once they publish, the ``[tool.uv.sources]`` block can be dropped and the declared deps resolve from PyPI. Cleanup ------- * Drops ``foundry_hosted_agent/scripts/vendor-packages.sh``, ``_vendor/`` from ``.gitignore``, the ``hooks.prepackage`` block in ``azure.yaml`` and the ``COPY _vendor/`` step in the Dockerfile — vendoring is no longer needed because git refs make the deps network-resolvable from any context. * Drops obsolete ``workspace.pyproject.toml`` reference and ``scripts/`` / ``workspace.pyproject.toml`` entries from ``Dockerfile.dockerignore``. * Updates the foundry sample's Dockerfile to ``uv sync --no-dev`` (no ``--frozen``) so it locks fresh against the GitHub-hosted deps at build time. * Drops every committed ``uv.lock`` because the resolver needs network access to ``feature/python-hosting`` to lock — they regenerate the first time a user runs ``uv sync`` after the branch lands. * Refreshes the per-sample READMEs to mention the GitHub install path instead of "in-tree workspace packages". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * samples(hosting): address PR microsoft#5645 review comments - foundry_hosted_agent/call_server.py: replace hard-coded project_endpoint and service_session_id with FOUNDRY_PROJECT_ENDPOINT, FOUNDRY_HOSTED_AGENT_NAME, and optional FOUNDRY_HOSTED_SESSION_ID environment variables. Session-id is now optional so the sample exercises the new-conversation path by default. - local_identity_link/app.py: * make_telegram_hook: apply the reasoning bump regardless of identity-link state (the previous early-return on linked chats silently dropped the high-effort preset for the very flow the sample exists to demonstrate). * make_responses_hook: add a prominent DEV-ONLY warning that the client-supplied entra_oid shortcut bypasses identity verification and must be replaced by a JWT validator in production. * /link command: early-return when chat_id is missing instead of minting an authorize URL keyed on "telegram:None" (which would poison the link store with a binding any future chat_id-less update would collapse onto). * Switch ENTRA_CERT_PATH / ENTRA_CERT_PASSWORD env vars to the longer ENTRA_CERTIFICATE_PATH / ENTRA_CERTIFICATE_PASSWORD names that the README already documents. * channels: Sequence[Channel] -> list[Channel] (the next line appends, which a Sequence type doesn't expose). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(hosting-samples): apply sample formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(hosting-samples): guard command input text Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add Discord hosting channel Add an alpha agent-framework-hosting-discord package backed by Discord HTTP Interactions. The channel verifies signed slash-command requests, registers commands, runs hosted agents and ChannelCommand handlers, supports originating response hooks, streams by editing the original interaction response, and can push through Discord channel ids. Factor standard channel response-hook context application into hosting core so both host fan-out and originating channel replies use one helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address Discord review chunking feedback Ensure Discord command replies are chunked and streaming preview edits stay under Discord's content limit while final streamed replies continue through the chunked reply path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * small fix in init * updated lock --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ths, Activity push, Telegram/Teams fixes) (microsoft#6307) * 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 microsoft#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>
Remove linking, multicast, durable delivery, and host push machinery from the v1 hosting core. Keep those scenarios in a proposed follow-up ADR and update channel packages, samples, docs, tests, and workspace metadata around the smaller host/channel contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(python): add agent-framework-hosting-a2a channel Add a hosting channel that exposes the host target (agent or workflow) as a peer agent over the Agent-to-Agent (A2A) protocol (JSON-RPC plus a served agent card). Requests are handled by a host-routed HostAgentExecutor that drives the host pipeline (ChannelContext.run/ run_stream) instead of wrapping the target directly, so sessions, linking, and run/response hooks apply. Maps the A2A conversation/context id to a ChannelSession isolation key and the caller to a ChannelIdentity; streaming emits incremental task artifacts. Includes tests, README, and workspace registration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address A2A hosting channel review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(python): add agent-framework-hosting-mcp channel Add a hosting channel that exposes the host target (agent or workflow) as a single Model Context Protocol tool over Streamable HTTP. The tool invocation routes through the host pipeline (ChannelContext.run/ run_stream) so sessions, linking, and run/response hooks apply. Maps the MCP request context to a ChannelSession isolation key and ChannelIdentity, and forwards streaming output as MCP progress notifications. Includes tests, README, and workspace registration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address MCP hosting channel review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…port - Fix _stream_to_conversation and _buffer_and_send to iterate over update.contents and extract text from text-type Content items instead of using text-only getattr pattern; non-text content (images, files, etc.) is correctly handled (forwarded via final response), and text accumulation is protected from corruption by multimodal chunks. - Update test mocks to use Content.from_text() matching real AgentResponseUpdate API; add contents property to test update objects. - Add Google-style docstring to ActivityProtocolChannel.__init__ documenting multimodal streaming support. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Member
Author
|
Closing — this PR was accidentally branched from the full hosting stack branch (111 files). Reopening as a clean PR based on main, scoped to the Activity Protocol package only and linked to #6589. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Hardening and streamlined integration of the Activity Protocol (Bot Framework) hosting channel with multimodal streaming support.
Changes
_stream_to_conversationand_buffer_and_sendto iterate overupdate.contentsand extract text from text-type Content items. Non-text content (images, files, etc.) is correctly forwarded via the final response, and text accumulation is protected from corruption by multimodal chunks. Mirrors the pattern applied to Telegram and A2A channels.Content.from_text()matching the realAgentResponseUpdateAPI; addedcontentsproperty to test update objects to reflect the actual stream structure.ActivityProtocolChannel.__init__documenting multimodal streaming support and all configuration options.Validation
Related