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>
This commit is contained in:
Eduard van Valkenburg
2026-06-12 08:34:08 +02:00
committed by GitHub
Unverified
parent e5a6e35843
commit 36ce0950e4
50 changed files with 1290 additions and 11651 deletions
@@ -7,7 +7,6 @@ import importlib.metadata
from ._channel import ResponsesChannel
from ._parsing import (
messages_from_responses_input,
parse_response_target,
parse_responses_identity,
parse_responses_request,
)
@@ -21,7 +20,6 @@ __all__ = [
"ResponsesChannel",
"__version__",
"messages_from_responses_input",
"parse_response_target",
"parse_responses_identity",
"parse_responses_request",
]
@@ -17,22 +17,17 @@ from __future__ import annotations
import time
import uuid
from collections.abc import AsyncIterator, Awaitable, Callable, Mapping
from typing import Any, cast
from collections.abc import AsyncIterator, Callable, Mapping
from typing import Any
from agent_framework import AgentResponse, Content, Message
from agent_framework_hosting import (
ChannelContext,
ChannelContribution,
ChannelRequest,
ChannelResponseContext,
ChannelResponseHook,
ChannelRunHook,
ChannelSession,
ChannelStreamTransformHook,
HostedRunResult,
apply_response_hook,
apply_run_hook,
ChannelStreamUpdateHook,
get_current_isolation_keys,
logger,
)
@@ -53,25 +48,11 @@ from starlette.responses import JSONResponse, Response, StreamingResponse
from starlette.routing import Route
from ._parsing import (
parse_response_target,
parse_responses_identity,
parse_responses_request,
)
def _ack_text() -> str:
"""Tiny acknowledgement string for the originating wire.
Used when the agent reply is delivered out-of-band via :class:`ChannelPush`.
"""
return "[delivered out-of-band]"
def _text_result(text: str) -> HostedRunResult[AgentResponse]:
"""Build a host delivery payload from text accumulated by this channel."""
return HostedRunResult(AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text(text=text)])]))
class ResponsesChannel:
"""Minimal OpenAI-Responses-shaped surface.
@@ -88,7 +69,7 @@ class ResponsesChannel:
path: str = "/responses",
run_hook: ChannelRunHook | None = None,
response_hook: ChannelResponseHook | None = None,
stream_transform_hook: ChannelStreamTransformHook | None = None,
stream_update_hook: ChannelStreamUpdateHook | None = None,
response_id_factory: Callable[..., str] | None = None,
) -> None:
"""Create a Responses channel.
@@ -97,15 +78,13 @@ class ResponsesChannel:
path: Endpoint path on the host. Default ``"/responses"`` matches
the upstream OpenAI surface; use ``""`` to expose this channel
at the app root.
run_hook: Optional :data:`ChannelRunHook` invoked with the
parsed :class:`ChannelRequest` before the agent target
run_hook: Optional :data:`ChannelRunHook` the host invokes with
the parsed :class:`ChannelRequest` before the agent target
runs. May return a replacement request.
response_hook: Optional :data:`ChannelResponseHook` invoked
response_hook: Optional :data:`ChannelResponseHook` the host invokes
before the channel serializes an originating
:class:`HostedRunResult` into a Responses envelope. The
host also invokes this hook when delivering to this
channel as a non-originating push destination.
stream_transform_hook: Optional per-update transform hook
:class:`HostedRunResult` into a Responses envelope.
stream_update_hook: Optional per-update hook
applied while streaming Server-Sent Events. Return a
replacement update, or ``None`` to drop the update.
response_id_factory: Optional callable that mints the
@@ -138,7 +117,7 @@ class ResponsesChannel:
self.path = path
self._hook = run_hook
self.response_hook = response_hook
self._stream_transform_hook = stream_transform_hook
self._stream_update_hook = stream_update_hook
self._ctx: ChannelContext | None = None
self._response_id_factory: Callable[..., str] = (
response_id_factory if response_id_factory is not None else (lambda *_a, **_kw: f"resp_{uuid.uuid4().hex}")
@@ -156,8 +135,6 @@ class ResponsesChannel:
``options`` / ``ChannelSession`` triples via :mod:`._parsing`,
applies the optional ``run_hook``, and either streams an SSE
response stream or returns a one-shot OpenAI ``Response`` envelope.
Non-originating ``response_target`` values resolve to a delivery
acknowledgement instead of echoing the agent text on this wire.
"""
if self._ctx is None: # pragma: no cover - guarded by Channel lifecycle
return JSONResponse({"error": "channel not initialized"}, status_code=500)
@@ -218,8 +195,8 @@ class ResponsesChannel:
attributes["previous_response_id"] = previous_response_id
# Honor the OpenAI-Responses ``stream`` flag — non-streaming by
# default, SSE when the caller opts in. Run hooks may still flip
# this per-request (e.g. force non-streaming for a particular user).
# default, SSE when the caller opts in. The channel chooses the
# transport before run hooks execute.
channel_request = ChannelRequest(
channel=self.name,
operation="message.create",
@@ -228,18 +205,9 @@ class ResponsesChannel:
options=options or None,
stream=bool(body.get("stream", False)),
identity=parse_responses_identity(body, self.name),
response_target=parse_response_target(body),
attributes=attributes,
)
if self._hook is not None:
channel_request = await apply_run_hook(
self._hook,
channel_request,
target=self._ctx.target,
protocol_request=body,
)
if channel_request.stream:
return StreamingResponse(
self._stream_events(channel_request, body, response_id=response_id),
@@ -247,32 +215,17 @@ class ResponsesChannel:
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
result = await self._ctx.run(channel_request)
include_originating = await self._ctx.deliver_response(channel_request, result)
if include_originating:
result = await self._apply_response_hook(result, channel_request)
text = result.result.text if include_originating else _ack_text()
result = await self._ctx.run(
channel_request,
run_hook=self._hook,
protocol_request=body,
response_hook=self.response_hook,
channel_name=self.name,
)
text = result.result.text
envelope = self._build_response(body, text, status="completed", response_id=response_id)
return JSONResponse(envelope.model_dump(mode="json", exclude_none=True))
async def _apply_response_hook(
self,
result: HostedRunResult[AgentResponse],
request: ChannelRequest,
) -> HostedRunResult[AgentResponse]:
"""Apply the channel-level response hook for an originating reply."""
if self.response_hook is None:
return result
context = ChannelResponseContext(
request=request,
channel_name=self.name,
destination_identity=None,
originating=True,
is_echo=False,
)
shaped = await apply_response_hook(self.response_hook, result, context=context)
return cast("HostedRunResult[AgentResponse]", shaped)
def _build_response(
self,
body: Mapping[str, Any],
@@ -350,13 +303,15 @@ class ResponsesChannel:
accumulated = ""
try:
stream = self._ctx.run_stream(request)
stream = await self._ctx.run_stream(
request,
run_hook=self._hook,
protocol_request=body,
stream_update_hook=self._stream_update_hook,
response_hook=self.response_hook,
channel_name=self.name,
)
async for update in stream:
if self._stream_transform_hook is not None:
transformed = self._stream_transform_hook(update)
update = await transformed if isinstance(transformed, Awaitable) else transformed
if update is None:
continue
chunk = getattr(update, "text", None)
if chunk:
accumulated += chunk
@@ -374,24 +329,12 @@ class ResponsesChannel:
try:
# Finalize so context-provider / history hooks on the agent
# still run even though we are emitting our own SSE.
await stream.get_final_response()
final_response = await stream.get_final_response()
except Exception: # pragma: no cover - finalize is best-effort
logger.exception("Responses stream finalize failed")
final_response = None
except Exception as exc:
logger.exception("Responses stream consumption failed")
# Mid-stream failure: the wire already saw partial deltas
# so host-side state must reflect that — call
# ``deliver_response`` with the accumulated text (best-effort)
# before signalling failure to the client. Without this,
# next turn's chain anchored on this ``response_id`` would
# be inconsistent with what the user actually saw, and any
# non-originating push targets would silently miss the turn.
# ``deliver_response`` itself is best-effort; we swallow its
# exceptions so the failure event still reaches the client.
try:
await self._ctx.deliver_response(request, _text_result(accumulated))
except Exception: # pragma: no cover - delivery is best-effort
logger.exception("Responses stream failure deliver_response failed")
failed = self._build_response(body, accumulated, status="failed", response_id=response_id)
failed.error = ResponseError(code="server_error", message=str(exc))
yield sse(
@@ -403,14 +346,7 @@ class ResponsesChannel:
)
return
completed_text = accumulated
result = _text_result(accumulated)
include_originating = await self._ctx.deliver_response(request, result)
if include_originating:
result = await self._apply_response_hook(result, request)
completed_text = result.result.text
else:
completed_text = _ack_text()
completed_text = getattr(final_response, "text", None) or accumulated
completed = self._build_response(body, completed_text, status="completed", response_id=response_id)
# Reuse the same message id we emitted deltas under.
if completed.output and isinstance(completed.output[0], ResponseOutputMessage):
@@ -15,7 +15,7 @@ from collections.abc import Mapping
from typing import Any, cast
from agent_framework import Content, Message
from agent_framework_hosting import ChannelIdentity, ChannelSession, ResponseTarget, logger
from agent_framework_hosting import ChannelIdentity, ChannelSession
# OpenAI Responses field name → Agent Framework ChatOptions field name.
_RESPONSES_OPTION_REMAP = {
@@ -43,71 +43,7 @@ _RESPONSES_OPTION_PASSTHROUGH = {
"logit_bias",
}
# Fields the Responses transport owns; they must not be forwarded as options.
_RESPONSES_TRANSPORT_KEYS = {"input", "model", "stream", "previous_response_id", "response_target"}
def parse_response_target(body: Mapping[str, Any]) -> ResponseTarget:
"""Translate the OpenAI Responses ``response_target`` field into a :class:`ResponseTarget`.
Accepted shapes:
- ``"originating"`` / ``"active"`` / ``"all_linked"`` / ``"none"`` — bare strings.
- ``"telegram"`` / ``"telegram:<chat_id>"`` — single channel destination.
- ``["telegram:<id>", "originating"]`` — list of destinations; the
pseudo-name ``"originating"`` includes the originating channel.
- ``{"channels": [...]}`` — same list semantics with the explicit key.
- ``{"kind": "active"}`` / ``{"kind": "all_linked"}`` — explicit kind.
Anything malformed is logged at WARNING and falls back to ``originating``.
"""
raw = body.get("response_target")
if raw is None:
return ResponseTarget.originating # type: ignore[attr-defined,no-any-return]
if isinstance(raw, str):
keyword = raw.strip()
if keyword == "originating":
return ResponseTarget.originating # type: ignore[attr-defined,no-any-return]
if keyword == "active":
return ResponseTarget.active # type: ignore[attr-defined,no-any-return]
if keyword == "all_linked":
return ResponseTarget.all_linked # type: ignore[attr-defined,no-any-return]
if keyword == "none":
return ResponseTarget.none # type: ignore[attr-defined,no-any-return]
# Treat any other bare string as a single channel destination.
return ResponseTarget.channel(keyword)
if isinstance(raw, list):
return _parse_channels_list(cast("list[Any]", raw)) # type: ignore[redundant-cast]
if isinstance(raw, Mapping):
raw_map = cast("Mapping[str, Any]", raw)
channels = raw_map.get("channels")
if isinstance(channels, list):
return _parse_channels_list(cast("list[Any]", channels)) # type: ignore[redundant-cast]
kind = raw_map.get("kind")
if kind == "active":
return ResponseTarget.active # type: ignore[attr-defined,no-any-return]
if kind == "all_linked":
return ResponseTarget.all_linked # type: ignore[attr-defined,no-any-return]
if kind == "none":
return ResponseTarget.none # type: ignore[attr-defined,no-any-return]
if kind == "originating":
return ResponseTarget.originating # type: ignore[attr-defined,no-any-return]
logger.warning("responses: ignoring malformed response_target=%r", cast("Any", raw))
return ResponseTarget.originating # type: ignore[attr-defined,no-any-return]
def _parse_channels_list(raw: list[Any]) -> ResponseTarget:
"""Build a ``ResponseTarget.channels`` from a raw list, dropping non-string entries.
An empty list (or one with no usable strings) collapses back to
``originating`` so we never silently produce a target that nobody
will deliver to.
"""
tokens = [t for t in raw if isinstance(t, str) and t]
if len(tokens) != len(raw):
logger.warning("responses: dropping non-string entries from response_target=%r", raw)
if not tokens:
return ResponseTarget.originating # type: ignore[attr-defined,no-any-return]
return ResponseTarget.channels(tokens)
_RESPONSES_TRANSPORT_KEYS = {"input", "model", "stream", "previous_response_id"}
def parse_responses_identity(body: Mapping[str, Any], channel_name: str) -> ChannelIdentity | None:
@@ -228,7 +164,6 @@ def parse_responses_request(
__all__ = [
"messages_from_responses_input",
"parse_response_target",
"parse_responses_identity",
"parse_responses_request",
]