* 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>
local_responses_workflow — workflow target with structured intake + checkpoints
A Workflow (intake → writer → legal reviewer → formatter) hosted
behind both the Responses API and the Invocations API, with the
host configured to persist per-conversation checkpoints. Mirrors
../../foundry-hosted-agents/responses/04_workflows/
but uses the agent-framework-hosting stack instead of the
Foundry-Hosted-Agents runtime, and adds a structured intake step
(SloganBrief with topic / style / audience fields) at the front
of the workflow.
What's interesting
AgentFrameworkHost(target=workflow, …)— the host detects aWorkflowtarget and dispatches toworkflow.run(...)(noAgent.create_session(...)).- Two channels are mounted side-by-side (
ResponsesChannelat/responses,InvocationsChannelat/invocations). Both share the samebrief_hookthat adapts the channel-native input into the workflow start executor's typed input — Responses delivers alist[Message], Invocations delivers astr, but the hook normalises both to text and produces aSloganBrief. - The hook parses the inbound text as JSON
(
{"topic": ..., "style": ..., "audience": ...}); if parsing fails it uses the whole text astopicwith defaults. - The workflow's first executor (
BriefIntakeExecutor) acceptsSloganBriefdirectly — that's what gets sent intoworkflow.run(...)by the host. checkpoint_location=storage/checkpoints/— the host scopes aFileCheckpointStorageper conversation (Responses keys it onprevious_response_id/conversation_id; Invocations keys it onsession_id) and restores from the latest checkpoint at the start of every turn before applying the new input. Without an isolation key the host skips checkpointing for that request.- No
HistoryProvider— the workflow owns its own state via the checkpoint store.
Run
export FOUNDRY_PROJECT_ENDPOINT=https://<your-project>.services.ai.azure.com
export FOUNDRY_MODEL=gpt-5.4-nano
az login
uv sync
uv run hypercorn app:app --bind 0.0.0.0:8000
Single-process for quick iteration:
uv run python app.py
Call locally
Two clients are provided next to app.py:
call_server.py— Python client using the OpenAI SDK (Responses API only).call_server.rest— raw REST examples for both the Responses and Invocations endpoints (open in VS Code with the REST Client extension or any compatible HTTP-file runner).
uv sync --group dev
# Structured brief via the OpenAI SDK (Responses API):
uv run python call_server.py \
'{"topic": "electric SUV", "style": "playful", "audience": "young families"}'
# Plain topic (style/audience default to "modern" / "general"):
uv run python call_server.py "electric SUV"
# Continue an existing conversation by its `response.id`:
uv run python call_server.py --previous-response-id <response-id> \
'{"topic": "electric SUV", "style": "retro", "audience": "boomers"}'
After a few turns, inspect storage/checkpoints/<isolation_key>/ —
each conversation has its own subdirectory of checkpoint files written
by the host.
This sample is local-only — no Dockerfile, no Foundry packaging. For a Foundry-Hosted-Agents-compatible packaging see
../foundry_hosted_agent.