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",
]
@@ -10,7 +10,6 @@ from typing import Any
from agent_framework_hosting import (
AgentFrameworkHost,
ChannelIdentity,
HostedRunResult,
)
from starlette.testclient import TestClient
@@ -70,22 +69,6 @@ class _FakeAgent:
return _coro()
class _RecordingPushChannel:
name = "telegram"
path = "/telegram"
def __init__(self) -> None:
self.pushes: list[tuple[ChannelIdentity, HostedRunResult]] = []
def contribute(self, _ctx: Any) -> Any:
from agent_framework_hosting import ChannelContribution
return ChannelContribution()
async def push(self, identity: ChannelIdentity, payload: HostedRunResult) -> None:
self.pushes.append((identity, payload))
# --------------------------------------------------------------------------- #
# Tests #
# --------------------------------------------------------------------------- #
@@ -151,7 +134,17 @@ class TestResponsesChannelNonStreaming:
# _FakeAgent.create_session stashes the session_id on the dict it returns.
assert sess["session_id"] == "resp_42"
def test_chat_isolation_header_creates_session_when_no_prev_id(self) -> None:
def test_chat_isolation_header_ignored_outside_foundry(self) -> None:
client, _host, agent = _make_client()
with client:
client.post(
"/responses",
json={"input": "x"},
headers={"x-agent-chat-isolation-key": "chat-abc"},
)
assert "session" not in agent.calls[0]["kwargs"]
def test_chat_isolation_header_creates_session_in_foundry(self, monkeypatch: Any) -> None:
"""Foundry-style ``x-agent-chat-isolation-key`` falls back to a session anchor.
First-turn requests have no ``previous_response_id`` (the client
@@ -160,6 +153,7 @@ class TestResponsesChannelNonStreaming:
chat key so the host can build a stable per-conversation session
that history providers persist under.
"""
monkeypatch.setenv("FOUNDRY_HOSTING_ENVIRONMENT", "1")
client, _host, agent = _make_client()
with client:
client.post(
@@ -171,13 +165,14 @@ class TestResponsesChannelNonStreaming:
assert sess is not None
assert sess["session_id"] == "chat-abc"
def test_prev_response_id_wins_over_chat_isolation_header(self) -> None:
def test_prev_response_id_wins_over_chat_isolation_header(self, monkeypatch: Any) -> None:
"""When both anchors are present, ``previous_response_id`` wins.
``previous_response_id`` is the protocol-native chain anchor; the
header fallback is only meant to bootstrap when no protocol
anchor exists.
"""
monkeypatch.setenv("FOUNDRY_HOSTING_ENVIRONMENT", "1")
client, _host, agent = _make_client()
with client:
client.post(
@@ -189,31 +184,11 @@ class TestResponsesChannelNonStreaming:
assert sess is not None
assert sess["session_id"] == "resp_99"
def test_response_target_channel_returns_ack_text_when_pushed(self) -> None:
agent = _FakeAgent(reply="real reply")
push_ch = _RecordingPushChannel()
host = AgentFrameworkHost(target=agent, channels=[ResponsesChannel(), push_ch])
with TestClient(host.app) as client:
r = client.post(
"/responses",
json={
"input": "hi",
"response_target": "telegram:42",
},
)
assert r.status_code == 200
body = r.json()
text = body["output"][0]["content"][0]["text"]
assert "delivered out-of-band" in text
assert push_ch.pushes and push_ch.pushes[0][1].result.text == "real reply"
assert push_ch.pushes[0][0].native_id == "42"
def test_response_hook_can_rewrite_originating_reply(self) -> None:
contexts: list[Any] = []
seen_kwargs: list[dict[str, Any]] = []
def hook(result: HostedRunResult, **kwargs: Any) -> HostedRunResult:
contexts.append(kwargs["context"])
seen_kwargs.append(dict(kwargs))
return HostedRunResult(_FakeAgentResponse(text=result.result.text.upper()), session=result.session)
agent = _FakeAgent(reply="hooked")
@@ -225,10 +200,8 @@ class TestResponsesChannelNonStreaming:
assert r.status_code == 200
body = r.json()
assert body["output"][0]["content"][0]["text"] == "HOOKED"
assert contexts
assert contexts[0].channel_name == "responses"
assert contexts[0].originating is True
assert contexts[0].destination_identity is None
assert seen_kwargs
assert seen_kwargs[0]["channel_name"] == "responses"
class TestResponsesChannelStreaming:
@@ -252,14 +225,15 @@ class TestResponsesChannelStreaming:
def transform(update: _FakeUpdate) -> _FakeUpdate:
return _FakeUpdate(text=update.text.upper())
host = AgentFrameworkHost(target=agent, channels=[ResponsesChannel(stream_transform_hook=transform)])
host = AgentFrameworkHost(target=agent, channels=[ResponsesChannel(stream_update_hook=transform)])
with TestClient(host.app) as client:
r = client.post("/responses", json={"input": "hi", "stream": True})
assert r.status_code == 200
assert '"delta":"HE"' in r.text
assert '"delta":"LLO"' in r.text
assert '"text":"HELLO"' in r.text
# Stream update hooks are update-only; they do not rewrite get_final_response().
assert '"text":"hello"' in r.text
def test_sse_emits_failed_when_stream_raises(self) -> None:
# Regression: ResponseOutputMessage.status only accepts in_progress/
@@ -5,11 +5,9 @@
from __future__ import annotations
import pytest
from agent_framework_hosting import ResponseTarget, ResponseTargetKind
from agent_framework_hosting_responses import (
messages_from_responses_input,
parse_response_target,
parse_responses_identity,
parse_responses_request,
)
@@ -127,64 +125,6 @@ class TestParseResponsesRequest:
assert sess.isolation_key == "resp_42"
class TestParseResponseTarget:
def test_default_originating_when_missing(self) -> None:
assert parse_response_target({}).kind is ResponseTargetKind.ORIGINATING
@pytest.mark.parametrize(
"value,expected_kind",
[
("originating", ResponseTargetKind.ORIGINATING),
("active", ResponseTargetKind.ACTIVE),
("all_linked", ResponseTargetKind.ALL_LINKED),
("none", ResponseTargetKind.NONE),
],
)
def test_bare_string_kinds(self, value: str, expected_kind: ResponseTargetKind) -> None:
assert parse_response_target({"response_target": value}).kind is expected_kind
def test_bare_string_other_becomes_channel(self) -> None:
target = parse_response_target({"response_target": "telegram"})
assert target == ResponseTarget.channel("telegram")
def test_bare_string_with_native_id_becomes_channel(self) -> None:
target = parse_response_target({"response_target": "telegram:42"})
assert target.kind is ResponseTargetKind.CHANNELS
assert target.targets == ("telegram:42",)
def test_list_form(self) -> None:
target = parse_response_target({"response_target": ["telegram:42", "originating"]})
assert target == ResponseTarget.channels(["telegram:42", "originating"])
def test_list_drops_non_strings(self) -> None:
target = parse_response_target({"response_target": ["telegram", 42, ""]})
assert target.targets == ("telegram",)
def test_empty_list_falls_back_to_originating(self) -> None:
target = parse_response_target({"response_target": []})
assert target.kind is ResponseTargetKind.ORIGINATING
def test_dict_with_channels(self) -> None:
target = parse_response_target({"response_target": {"channels": ["a", "b"]}})
assert target == ResponseTarget.channels(["a", "b"])
@pytest.mark.parametrize(
"kind,expected",
[
("active", ResponseTargetKind.ACTIVE),
("all_linked", ResponseTargetKind.ALL_LINKED),
("none", ResponseTargetKind.NONE),
("originating", ResponseTargetKind.ORIGINATING),
],
)
def test_dict_kind(self, kind: str, expected: ResponseTargetKind) -> None:
assert parse_response_target({"response_target": {"kind": kind}}).kind is expected
def test_malformed_falls_back_to_originating(self) -> None:
target = parse_response_target({"response_target": 42})
assert target.kind is ResponseTargetKind.ORIGINATING
class TestParseResponsesIdentity:
def test_safety_identifier_preferred(self) -> None:
ident = parse_responses_identity({"safety_identifier": "abc", "user": "legacy"}, "responses")