Commit Graph

4 Commits

  • Simplify Python hosting core (#6492)
    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>
  • Python: feat(python): cross-channel hosting improvements (endpoint paths, Activity push, Telegram/Teams fixes) (#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 #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>
  • Python: add agent-framework-hosting core package (#5638)
    * 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 (#5640) but target lines that
    live in the hosting-core package (PR-2 / #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 #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 #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 #22, renumbered v1 fast-follow #23..#29 and
    stretch #30..#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 #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>
  • Python: Channel spec (#5549)
    * 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 #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 #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 #25 (A2AChannel) and #26 (MCPToolChannel) under v1 Fast Follow; renumbered the WhatsApp/Teams entry to #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 #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 #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 #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 #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 #11, req #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 #28
    
    Add a Teams-native channel package built on the MIT-licensed
    microsoft/teams.py SDK as fast-follow alongside the generic
    ActivityChannel (req #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 #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 #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 #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 #30.
    
    Add the ActivityChannel vs TeamsChannel audience comparison table
    to req #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>