Python: add agent-framework-hosting-discord channel (#6081)

* 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>
This commit is contained in:
Eduard van Valkenburg
2026-05-28 15:53:23 +02:00
committed by GitHub
Unverified
parent 6b822853eb
commit e8c22caaeb
14 changed files with 1787 additions and 46 deletions
@@ -7,12 +7,16 @@ from __future__ import annotations
from typing import Any
from agent_framework_hosting import (
ChannelContribution,
ChannelIdentity,
ChannelRequest,
ChannelResponseContext,
ChannelSession,
DurableTaskPayloadMode,
HostedRunResult,
ResponseTarget,
ResponseTargetKind,
apply_channel_response_hook,
apply_run_hook,
)
@@ -117,6 +121,88 @@ class _DummyTarget:
"""
class _DummyChannel:
name = "dummy"
path = "/dummy"
def contribute(self, _context: Any) -> ChannelContribution:
return ChannelContribution()
class TestApplyChannelResponseHook:
async def test_originating_hook_receives_standard_context(self) -> None:
request = ChannelRequest(channel="discord", operation="message.create", input="hi")
payload = HostedRunResult("original")
captured: list[ChannelResponseContext] = []
async def hook(
result: HostedRunResult[Any],
*,
context: ChannelResponseContext,
) -> HostedRunResult[Any]:
captured.append(context)
return result.replace(result="hooked")
channel = _DummyChannel()
channel.response_hook = hook # type: ignore[attr-defined]
shaped = await apply_channel_response_hook(channel, payload, request=request, originating=True)
assert shaped.result == "hooked"
assert captured[0].request is request
assert captured[0].channel_name == "dummy"
assert captured[0].destination_identity is None
assert captured[0].originating is True
assert captured[0].is_echo is False
async def test_non_originating_hook_can_clone_before_shaping(self) -> None:
request = ChannelRequest(channel="responses", operation="message.create", input="hi")
identity = ChannelIdentity(channel="dummy", native_id="user-1")
payload = HostedRunResult("original")
seen_payloads: list[HostedRunResult[Any]] = []
seen_contexts: list[ChannelResponseContext] = []
def hook(
result: HostedRunResult[Any],
*,
context: ChannelResponseContext,
) -> HostedRunResult[Any]:
seen_payloads.append(result)
seen_contexts.append(context)
return result.replace(result="hooked")
channel = _DummyChannel()
channel.response_hook = hook # type: ignore[attr-defined]
shaped = await apply_channel_response_hook(
channel,
payload,
request=request,
destination_identity=identity,
originating=False,
is_echo=True,
clone=True,
)
assert seen_payloads[0] is not payload
assert shaped.result == "hooked"
assert seen_contexts[0].destination_identity is identity
assert seen_contexts[0].originating is False
assert seen_contexts[0].is_echo is True
async def test_missing_hook_returns_payload_or_clone(self) -> None:
request = ChannelRequest(channel="responses", operation="message.create", input="hi")
payload = HostedRunResult("original")
channel = _DummyChannel()
same = await apply_channel_response_hook(channel, payload, request=request, originating=True)
cloned = await apply_channel_response_hook(channel, payload, request=request, originating=True, clone=True)
assert same is payload
assert cloned is not payload
assert cloned.result == payload.result
class TestApplyRunHook:
"""`apply_run_hook` is the channel-side helper that invokes a
`ChannelRunHook` with the standard kwargs (`request` positional,