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
@@ -71,6 +71,7 @@ from ._types import (
RetryPolicy,
TaskHandle,
TaskStatus,
apply_channel_response_hook,
apply_response_hook,
apply_run_hook,
)
@@ -134,6 +135,7 @@ __all__ = [
"TaskHandle",
"TaskStatus",
"__version__",
"apply_channel_response_hook",
"apply_response_hook",
"apply_run_hook",
"get_current_isolation_keys",
@@ -81,15 +81,13 @@ from ._types import (
ChannelPush,
ChannelPushCodec,
ChannelRequest,
ChannelResponseContext,
ChannelResponseHook,
DurableTaskPayloadMode,
DurableTaskRunner,
HostedRunResult,
HostStatePaths,
PushPayloadNotSerializable,
ResponseTargetKind,
apply_response_hook,
apply_channel_response_hook,
)
if TYPE_CHECKING:
@@ -1896,17 +1894,15 @@ class AgentFrameworkHost:
contract; richer surfaces stay attribute-level so adding hook
support to a new channel does not require updating the Protocol.
"""
shaped: HostedRunResult[Any] = payload.replace()
hook = cast(ChannelResponseHook | None, getattr(channel, "response_hook", None))
if callable(hook):
ctx = ChannelResponseContext(
request=request,
channel_name=channel.name,
destination_identity=identity,
originating=False,
is_echo=is_echo,
)
shaped = await apply_response_hook(hook, shaped, context=ctx)
shaped = await apply_channel_response_hook(
channel,
payload,
request=request,
destination_identity=identity,
originating=False,
is_echo=is_echo,
clone=True,
)
await channel.push(identity, shaped)
return shaped
@@ -25,7 +25,7 @@ import os
from collections.abc import Awaitable, Callable, Mapping, Sequence
from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING, Any, Generic, Literal, Protocol, TypedDict, TypeVar, runtime_checkable
from typing import TYPE_CHECKING, Any, Generic, Literal, Protocol, TypedDict, TypeVar, cast, runtime_checkable
from agent_framework import (
AgentResponse,
@@ -760,6 +760,52 @@ class ChannelPush(Protocol):
async def push(self, identity: ChannelIdentity, payload: HostedRunResult[Any]) -> None: ...
async def apply_channel_response_hook(
channel: Channel | ChannelPush,
result: HostedRunResult[Any],
*,
request: ChannelRequest,
originating: bool,
destination_identity: ChannelIdentity | None = None,
is_echo: bool = False,
clone: bool = False,
) -> HostedRunResult[Any]:
"""Apply a channel's optional response hook with the standard context.
Channels and the host call this helper when they need to shape a
:class:`HostedRunResult` for one destination. The helper centralizes the
response-hook convention: hooks are discovered from a duck-typed
``response_hook`` attribute, called through :func:`apply_response_hook`,
and receive a :class:`ChannelResponseContext` that identifies the channel,
destination identity, originating-vs-push phase, and echo phase.
Args:
channel: Channel whose ``response_hook`` attribute may shape the payload.
result: Hosted run result to pass to the hook.
request: Originating channel request.
originating: Whether this is the originating channel's synchronous reply.
destination_identity: Destination identity for non-originating pushes, or
``None`` for originating replies.
is_echo: Whether the payload is an echo of the user input.
clone: Whether to shallow-clone ``result`` before applying the hook.
Returns:
The original, cloned, or hook-shaped hosted run result.
"""
shaped = result.replace() if clone else result
hook = cast(ChannelResponseHook | None, getattr(channel, "response_hook", None))
if not callable(hook):
return shaped
context = ChannelResponseContext(
request=request,
channel_name=channel.name,
destination_identity=destination_identity,
originating=originating,
is_echo=is_echo,
)
return await apply_response_hook(hook, shaped, context=context)
# --------------------------------------------------------------------------- #
# Durable task runner — pluggable seam for non-originating push fan-out and
# (in v1 fast-follow) background runs. See spec §"Durable task runner".
@@ -910,6 +956,7 @@ __all__ = [
"RetryPolicy",
"TaskHandle",
"TaskStatus",
"apply_channel_response_hook",
"apply_response_hook",
"apply_run_hook",
]