From ce15c6d521a95ffbc787ec1df88ce92ff958fb67 Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Thu, 6 Aug 2026 14:57:55 +0000 Subject: [PATCH 1/6] Add SSE keep-alive to invocations streams Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CHANGELOG.md | 8 ++ .../ai/agentserver/invocations/_invocation.py | 11 +- .../azure/ai/agentserver/invocations/_sse.py | 61 +++++++++ .../tests/test_sse_keep_alive.py | 128 ++++++++++++++++++ 4 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_sse.py create mode 100644 sdk/agentserver/azure-ai-agentserver-invocations/tests/test_sse_keep_alive.py diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md b/sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md index 6c1baca5bb42..17d959847660 100644 --- a/sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md +++ b/sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md @@ -1,5 +1,13 @@ # Release History +## 1.0.0b9 (Unreleased) + +### Bugs Fixed + +- Added SSE keep-alive comments to idle `POST /invocations` event streams when + `SSE_KEEPALIVE_INTERVAL` is configured, preventing hosted proxy idle timeouts + from disconnecting clients before the agent emits its final events. + ## 1.0.0b8 (2026-08-03) ### Samples diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_invocation.py b/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_invocation.py index e14184a6e2a1..ebd459b60be5 100644 --- a/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_invocation.py +++ b/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_invocation.py @@ -38,6 +38,7 @@ from ._constants import InvocationConstants from ._invocation_ws import _WSHandlerMixin +from ._sse import _with_keep_alive logger = logging.getLogger("azure.ai.agentserver") @@ -480,7 +481,15 @@ async def _wrapped_body() -> AsyncIterator[Any]: if stream_ctx_token is not None: reset_request_context(stream_ctx_token) - response.body_iterator = _wrapped_body() + wrapped_body = _wrapped_body() + content_type = response.media_type or response.headers.get("content-type", "") + if content_type.lower().startswith("text/event-stream"): + response.body_iterator = _with_keep_alive( + wrapped_body, + self.config.sse_keepalive_interval, + ) + else: + response.body_iterator = wrapped_body return response async def _create_invocation_endpoint(self, request: Request) -> Response: diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_sse.py b/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_sse.py new file mode 100644 index 000000000000..a68aaa463c10 --- /dev/null +++ b/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_sse.py @@ -0,0 +1,61 @@ +# --------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# --------------------------------------------------------- +"""Internal server-sent events helpers for the invocations protocol.""" + +import asyncio # pylint: disable=do-not-import-asyncio +from collections.abc import AsyncIterator +from typing import Any + +_KEEP_ALIVE_COMMENT = ": keep-alive\n\n" + + +async def _with_keep_alive( + source: AsyncIterator[Any], + interval_seconds: float | None, +) -> AsyncIterator[Any]: + """Interleave SSE keep-alive comments while the source is idle.""" + if not interval_seconds: + async for item in source: + yield item + return + + queue: asyncio.Queue[Any] = asyncio.Queue() + sentinel = object() + pump_error: BaseException | None = None + + async def _pump() -> None: + nonlocal pump_error + try: + async for item in source: + queue.put_nowait(item) + except Exception as exc: # pylint: disable=broad-exception-caught + pump_error = exc + finally: + queue.put_nowait(sentinel) + + pump_task = asyncio.create_task(_pump()) + get_task: asyncio.Task[Any] | None = None + try: + while True: + if get_task is None: + get_task = asyncio.create_task(queue.get()) + try: + item = await asyncio.wait_for( + asyncio.shield(get_task), + timeout=interval_seconds, + ) + except asyncio.TimeoutError: + yield _KEEP_ALIVE_COMMENT + continue + get_task = None + if item is sentinel: + break + yield item + if pump_error is not None: + raise pump_error + finally: + pending = [task for task in (pump_task, get_task) if task is not None] + for task in pending: + task.cancel() + await asyncio.gather(*pending, return_exceptions=True) diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/tests/test_sse_keep_alive.py b/sdk/agentserver/azure-ai-agentserver-invocations/tests/test_sse_keep_alive.py new file mode 100644 index 000000000000..ad0a57e4bfed --- /dev/null +++ b/sdk/agentserver/azure-ai-agentserver-invocations/tests/test_sse_keep_alive.py @@ -0,0 +1,128 @@ +# --------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# --------------------------------------------------------- +"""Tests for invocations SSE keep-alive behavior.""" + +import asyncio +from collections.abc import AsyncIterator + +import pytest +from httpx import ASGITransport, AsyncClient +from starlette.requests import Request +from starlette.responses import StreamingResponse + +from azure.ai.agentserver.invocations import InvocationAgentServerHost +from azure.ai.agentserver.invocations._sse import _with_keep_alive + + +async def _collect(source: AsyncIterator[object]) -> list[object]: + return [item async for item in source] + + +@pytest.mark.asyncio +async def test_with_keep_alive_emits_comment_during_idle_gap() -> None: + async def source() -> AsyncIterator[str]: + yield "started" + await asyncio.sleep(0.1) + yield "finished" + + chunks = await _collect(_with_keep_alive(source(), 0.02)) + + assert ": keep-alive\n\n" in chunks + assert [chunk for chunk in chunks if chunk != ": keep-alive\n\n"] == [ + "started", + "finished", + ] + + +@pytest.mark.asyncio +async def test_with_keep_alive_passthrough_when_disabled() -> None: + async def source() -> AsyncIterator[bytes]: + yield b"first" + yield b"second" + + assert await _collect(_with_keep_alive(source(), 0)) == [b"first", b"second"] + + +@pytest.mark.asyncio +async def test_with_keep_alive_propagates_source_error() -> None: + async def source() -> AsyncIterator[str]: + yield "started" + raise RuntimeError("stream failed") + + stream = _with_keep_alive(source(), 0.02) + + assert await anext(stream) == "started" + with pytest.raises(RuntimeError, match="stream failed"): + await anext(stream) + + +@pytest.mark.asyncio +async def test_with_keep_alive_closes_source_when_consumer_stops() -> None: + finalized = asyncio.Event() + + async def source() -> AsyncIterator[str]: + try: + yield "started" + await asyncio.Event().wait() + finally: + finalized.set() + + stream = _with_keep_alive(source(), 0.02) + + assert await anext(stream) == "started" + await stream.aclose() + + await asyncio.wait_for(finalized.wait(), timeout=1.0) + + +@pytest.mark.asyncio +async def test_invocations_sse_stream_uses_configured_keep_alive(monkeypatch) -> None: + monkeypatch.setenv("SSE_KEEPALIVE_INTERVAL", "1") + app = InvocationAgentServerHost(configure_observability=None) + + @app.invoke_handler + async def handle(request: Request) -> StreamingResponse: + async def generate() -> AsyncIterator[str]: + yield "data: started\n\n" + await asyncio.sleep(1.1) + yield "data: finished\n\n" + + return StreamingResponse(generate(), media_type="text/event-stream") + + async with AsyncClient( + transport=ASGITransport(app=app), + base_url="http://testserver", + ) as client: + response = await client.post( + "/invocations", + headers={"accept": "text/event-stream"}, + ) + + assert response.status_code == 200 + assert ": keep-alive\n\n" in response.text + assert response.text.index("data: started") < response.text.index("data: finished") + + +@pytest.mark.asyncio +async def test_invocations_non_sse_stream_does_not_get_keep_alive(monkeypatch) -> None: + monkeypatch.setenv("SSE_KEEPALIVE_INTERVAL", "1") + app = InvocationAgentServerHost(configure_observability=None) + + @app.invoke_handler + async def handle(request: Request) -> StreamingResponse: + async def generate() -> AsyncIterator[str]: + yield '{"chunk": 1}\n' + await asyncio.sleep(1.1) + yield '{"chunk": 2}\n' + + return StreamingResponse(generate(), media_type="application/x-ndjson") + + async with AsyncClient( + transport=ASGITransport(app=app), + base_url="http://testserver", + ) as client: + response = await client.post("/invocations") + + assert response.status_code == 200 + assert ": keep-alive" not in response.text From 8d814987c120f6ae5714a203a2b4f978bbf7d7e4 Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Fri, 7 Aug 2026 04:39:25 +0000 Subject: [PATCH 2/6] Prepare invocations 1.0.0 release Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CHANGELOG.md | 2 +- .../azure/ai/agentserver/invocations/_sse.py | 55 ++++++++++++------- .../ai/agentserver/invocations/_version.py | 2 +- .../pyproject.toml | 6 +- .../tests/test_sse_keep_alive.py | 19 +++++++ 5 files changed, 59 insertions(+), 25 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md b/sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md index 17d959847660..c38f91302c51 100644 --- a/sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md +++ b/sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## 1.0.0b9 (Unreleased) +## 1.0.0 (Unreleased) ### Bugs Fixed diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_sse.py b/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_sse.py index a68aaa463c10..70906954dc30 100644 --- a/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_sse.py +++ b/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_sse.py @@ -14,48 +14,63 @@ async def _with_keep_alive( source: AsyncIterator[Any], interval_seconds: float | None, ) -> AsyncIterator[Any]: - """Interleave SSE keep-alive comments while the source is idle.""" + """Interleave SSE keep-alive comments while the source is idle. + + :param source: The source SSE stream. + :type source: ~collections.abc.AsyncIterator + :param interval_seconds: Seconds of inactivity between keep-alive comments, + or zero/``None`` to disable them. + :type interval_seconds: float or None + :return: The source chunks with keep-alive comments inserted during idle periods. + :rtype: ~collections.abc.AsyncIterator + """ if not interval_seconds: async for item in source: yield item return - queue: asyncio.Queue[Any] = asyncio.Queue() + requests: asyncio.Queue[asyncio.Future[Any]] = asyncio.Queue(maxsize=1) sentinel = object() - pump_error: BaseException | None = None async def _pump() -> None: - nonlocal pump_error try: - async for item in source: - queue.put_nowait(item) - except Exception as exc: # pylint: disable=broad-exception-caught - pump_error = exc + while True: + result = await requests.get() + try: + item = await anext(source) + except StopAsyncIteration: + result.set_result(sentinel) + return + except Exception as exc: # pylint: disable=broad-exception-caught + result.set_exception(exc) + return + result.set_result(item) finally: - queue.put_nowait(sentinel) + close = getattr(source, "aclose", None) + if close is not None: + await close() pump_task = asyncio.create_task(_pump()) - get_task: asyncio.Task[Any] | None = None + next_result: asyncio.Future[Any] | None = None try: while True: - if get_task is None: - get_task = asyncio.create_task(queue.get()) + if next_result is None: + next_result = asyncio.get_running_loop().create_future() + requests.put_nowait(next_result) try: item = await asyncio.wait_for( - asyncio.shield(get_task), + asyncio.shield(next_result), timeout=interval_seconds, ) except asyncio.TimeoutError: yield _KEEP_ALIVE_COMMENT continue - get_task = None + next_result = None if item is sentinel: break yield item - if pump_error is not None: - raise pump_error finally: - pending = [task for task in (pump_task, get_task) if task is not None] - for task in pending: - task.cancel() - await asyncio.gather(*pending, return_exceptions=True) + if next_result is not None: + next_result.cancel() + pump_task.cancel() + await asyncio.gather(pump_task, return_exceptions=True) diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_version.py b/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_version.py index 1b0058b20ee4..ed845cd7ee2a 100644 --- a/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_version.py +++ b/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_version.py @@ -2,4 +2,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # --------------------------------------------------------- -VERSION = "1.0.0b8" +VERSION = "1.0.0" diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/pyproject.toml b/sdk/agentserver/azure-ai-agentserver-invocations/pyproject.toml index 1d56a4ba2618..95fcf27a488c 100644 --- a/sdk/agentserver/azure-ai-agentserver-invocations/pyproject.toml +++ b/sdk/agentserver/azure-ai-agentserver-invocations/pyproject.toml @@ -8,7 +8,7 @@ authors = [ ] license = "MIT" classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", @@ -21,7 +21,7 @@ classifiers = [ keywords = ["azure", "azure sdk", "agent", "agentserver", "invocations"] dependencies = [ - "azure-ai-agentserver-core>=2.0.0b10", + "azure-ai-agentserver-core>=2.0.0", # Constraint on the transitive aiohttp: the `--pre` CI install otherwise # resolves the unbuildable aiohttp 4.0.0a1 alpha. Cap must be <4.0.0a0 # (<4.0.0 still admits 4.0.0a1 under PEP 440). @@ -72,7 +72,7 @@ mypy = true pyright = true verifytypes = false latestdependency = false -# azure-ai-agentserver-core>=2.0.0b8 is not yet on PyPI +# azure-ai-agentserver-core is developed alongside this package mindependency = false pylint = true type_check_samples = false diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/tests/test_sse_keep_alive.py b/sdk/agentserver/azure-ai-agentserver-invocations/tests/test_sse_keep_alive.py index ad0a57e4bfed..4f168446c60e 100644 --- a/sdk/agentserver/azure-ai-agentserver-invocations/tests/test_sse_keep_alive.py +++ b/sdk/agentserver/azure-ai-agentserver-invocations/tests/test_sse_keep_alive.py @@ -76,6 +76,25 @@ async def source() -> AsyncIterator[str]: await asyncio.wait_for(finalized.wait(), timeout=1.0) +@pytest.mark.asyncio +async def test_with_keep_alive_preserves_source_backpressure() -> None: + produced = 0 + + async def source() -> AsyncIterator[str]: + nonlocal produced + for index in range(100): + produced += 1 + yield str(index) + + stream = _with_keep_alive(source(), 0.02) + + assert await anext(stream) == "0" + await asyncio.sleep(0.05) + assert produced == 1 + + await stream.aclose() + + @pytest.mark.asyncio async def test_invocations_sse_stream_uses_configured_keep_alive(monkeypatch) -> None: monkeypatch.setenv("SSE_KEEPALIVE_INTERVAL", "1") From 9741d050061a7be591231ffc499c0269b21df7c1 Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Fri, 7 Aug 2026 04:56:50 +0000 Subject: [PATCH 3/6] Enable minimum dependency validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sdk/agentserver/azure-ai-agentserver-invocations/pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/pyproject.toml b/sdk/agentserver/azure-ai-agentserver-invocations/pyproject.toml index 95fcf27a488c..e8a41e49ac4c 100644 --- a/sdk/agentserver/azure-ai-agentserver-invocations/pyproject.toml +++ b/sdk/agentserver/azure-ai-agentserver-invocations/pyproject.toml @@ -72,8 +72,6 @@ mypy = true pyright = true verifytypes = false latestdependency = false -# azure-ai-agentserver-core is developed alongside this package -mindependency = false pylint = true type_check_samples = false From 796bd0356006b3a7d81b53f40a13cb60f030da78 Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Fri, 7 Aug 2026 09:15:59 +0000 Subject: [PATCH 4/6] Fix SSE content type handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CHANGELOG.md | 5 ++ .../README.md | 1 + .../api.metadata.yml | 4 +- .../ai/agentserver/invocations/_invocation.py | 2 +- .../tests/test_sse_keep_alive.py | 58 +++++++++++++++++++ 5 files changed, 67 insertions(+), 3 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md b/sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md index c38f91302c51..fe9e70c5454c 100644 --- a/sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md +++ b/sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md @@ -8,6 +8,11 @@ `SSE_KEEPALIVE_INTERVAL` is configured, preventing hosted proxy idle timeouts from disconnecting clients before the agent emits its final events. +### Other Changes + +- Updated the minimum `azure-ai-agentserver-core` dependency to the stable + `2.0.0` release. + ## 1.0.0b8 (2026-08-03) ### Samples diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/README.md b/sdk/agentserver/azure-ai-agentserver-invocations/README.md index 6769c2b2ca16..dd2548c9ede8 100644 --- a/sdk/agentserver/azure-ai-agentserver-invocations/README.md +++ b/sdk/agentserver/azure-ai-agentserver-invocations/README.md @@ -313,6 +313,7 @@ The handler receives a Starlette [`WebSocket`][starlette-ws] and returns `None`. | Environment variable | Default | Description | |---|---|---| +| `SSE_KEEPALIVE_INTERVAL` | unset (disabled) | Platform-injected interval, in seconds, for SSE comments on idle `POST /invocations` streams. `0` disables keep-alive. Surfaced on `app.config.sse_keepalive_interval`. | | `WS_KEEPALIVE_INTERVAL` | unset (disabled) | Platform-injected WebSocket Ping interval, in seconds. `0` disables keep-alive. Surfaced on `app.config.ws_ping_interval` and wired into Hypercorn's `websocket_ping_interval` by `AgentServerHost`. | ### Reference: close codes diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/api.metadata.yml b/sdk/agentserver/azure-ai-agentserver-invocations/api.metadata.yml index 570a1d2dada6..b3a77c616f75 100644 --- a/sdk/agentserver/azure-ai-agentserver-invocations/api.metadata.yml +++ b/sdk/agentserver/azure-ai-agentserver-invocations/api.metadata.yml @@ -1,3 +1,3 @@ apiMdSha256: 28f40971c3ed93df127abaf7faf714a62065ef4440aaf511b9269c584811520c -parserVersion: 0.3.30 -pythonVersion: 3.11.9 +parserVersion: 0.3.31 +pythonVersion: 3.11.15 diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_invocation.py b/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_invocation.py index ebd459b60be5..0f36560fe2bd 100644 --- a/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_invocation.py +++ b/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_invocation.py @@ -482,7 +482,7 @@ async def _wrapped_body() -> AsyncIterator[Any]: reset_request_context(stream_ctx_token) wrapped_body = _wrapped_body() - content_type = response.media_type or response.headers.get("content-type", "") + content_type = response.headers.get("content-type") or response.media_type or "" if content_type.lower().startswith("text/event-stream"): response.body_iterator = _with_keep_alive( wrapped_body, diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/tests/test_sse_keep_alive.py b/sdk/agentserver/azure-ai-agentserver-invocations/tests/test_sse_keep_alive.py index 4f168446c60e..51b292866246 100644 --- a/sdk/agentserver/azure-ai-agentserver-invocations/tests/test_sse_keep_alive.py +++ b/sdk/agentserver/azure-ai-agentserver-invocations/tests/test_sse_keep_alive.py @@ -145,3 +145,61 @@ async def generate() -> AsyncIterator[str]: assert response.status_code == 200 assert ": keep-alive" not in response.text + + +@pytest.mark.asyncio +async def test_invocations_uses_final_content_type_header(monkeypatch) -> None: + monkeypatch.setenv("SSE_KEEPALIVE_INTERVAL", "1") + app = InvocationAgentServerHost(configure_observability=None) + + @app.invoke_handler + async def handle(request: Request) -> StreamingResponse: + async def generate() -> AsyncIterator[str]: + yield '{"chunk": 1}\n' + await asyncio.sleep(1.1) + yield '{"chunk": 2}\n' + + return StreamingResponse( + generate(), + media_type="text/event-stream", + headers={"content-type": "application/x-ndjson"}, + ) + + async with AsyncClient( + transport=ASGITransport(app=app), + base_url="http://testserver", + ) as client: + response = await client.post("/invocations") + + assert response.status_code == 200 + assert response.headers["content-type"] == "application/x-ndjson" + assert ": keep-alive" not in response.text + + +@pytest.mark.asyncio +async def test_invocations_adds_keep_alive_for_final_sse_header(monkeypatch) -> None: + monkeypatch.setenv("SSE_KEEPALIVE_INTERVAL", "1") + app = InvocationAgentServerHost(configure_observability=None) + + @app.invoke_handler + async def handle(request: Request) -> StreamingResponse: + async def generate() -> AsyncIterator[str]: + yield "data: started\n\n" + await asyncio.sleep(1.1) + yield "data: finished\n\n" + + return StreamingResponse( + generate(), + media_type="application/x-ndjson", + headers={"content-type": "text/event-stream"}, + ) + + async with AsyncClient( + transport=ASGITransport(app=app), + base_url="http://testserver", + ) as client: + response = await client.post("/invocations") + + assert response.status_code == 200 + assert response.headers["content-type"] == "text/event-stream" + assert ": keep-alive\n\n" in response.text From 13f26d080dd18b5ee952441d756849ddf5b6c55a Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Fri, 7 Aug 2026 10:20:09 +0000 Subject: [PATCH 5/6] Prepare responses 2.0.0 release Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure/ai/agentserver/invocations/_sse.py | 5 ++-- .../tests/test_sse_keep_alive.py | 13 ++++++++++ .../CHANGELOG.md | 12 +++++++++ .../azure-ai-agentserver-responses/api.md | 3 --- .../api.metadata.yml | 4 +-- .../ai/agentserver/responses/__init__.py | 4 --- .../ai/agentserver/responses/_version.py | 2 +- .../dev_requirements.txt | 3 ++- .../pyproject.toml | 25 +++++++++++++++---- .../interop/test_openai_wire_compliance.py | 2 +- 10 files changed, 54 insertions(+), 19 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_sse.py b/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_sse.py index 70906954dc30..557adbc51742 100644 --- a/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_sse.py +++ b/sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/_sse.py @@ -41,8 +41,9 @@ async def _pump() -> None: except StopAsyncIteration: result.set_result(sentinel) return - except Exception as exc: # pylint: disable=broad-exception-caught - result.set_exception(exc) + except BaseException as exc: # pylint: disable=broad-exception-caught + if not result.done(): + result.set_exception(exc) return result.set_result(item) finally: diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/tests/test_sse_keep_alive.py b/sdk/agentserver/azure-ai-agentserver-invocations/tests/test_sse_keep_alive.py index 51b292866246..a8331c217bf0 100644 --- a/sdk/agentserver/azure-ai-agentserver-invocations/tests/test_sse_keep_alive.py +++ b/sdk/agentserver/azure-ai-agentserver-invocations/tests/test_sse_keep_alive.py @@ -57,6 +57,19 @@ async def source() -> AsyncIterator[str]: await anext(stream) +@pytest.mark.asyncio +async def test_with_keep_alive_propagates_source_cancellation() -> None: + async def source() -> AsyncIterator[str]: + yield "started" + raise asyncio.CancelledError + + stream = _with_keep_alive(source(), 0.02) + + assert await anext(stream) == "started" + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(anext(stream), timeout=1.0) + + @pytest.mark.asyncio async def test_with_keep_alive_closes_source_when_consumer_stops() -> None: finalized = asyncio.Event() diff --git a/sdk/agentserver/azure-ai-agentserver-responses/CHANGELOG.md b/sdk/agentserver/azure-ai-agentserver-responses/CHANGELOG.md index f942e1c2a9d0..7ea340a902ac 100644 --- a/sdk/agentserver/azure-ai-agentserver-responses/CHANGELOG.md +++ b/sdk/agentserver/azure-ai-agentserver-responses/CHANGELOG.md @@ -1,5 +1,17 @@ # Release History +## 2.0.0 (Unreleased) + +### Breaking Changes + +- Removed the duplicate `azure.ai.agentserver.responses.get_input_expanded` + export. Import it from `azure.ai.agentserver.responses.models` instead. + +### Other Changes + +- Updated the minimum `azure-ai-agentserver-core` dependency to the stable + `2.0.0` release. + ## 2.0.0b1 (2026-08-04) ### Other Changes diff --git a/sdk/agentserver/azure-ai-agentserver-responses/api.md b/sdk/agentserver/azure-ai-agentserver-responses/api.md index 4c35bb90a53a..91b8d12751b8 100644 --- a/sdk/agentserver/azure-ai-agentserver-responses/api.md +++ b/sdk/agentserver/azure-ai-agentserver-responses/api.md @@ -1,9 +1,6 @@ ```py namespace azure.ai.agentserver.responses - def azure.ai.agentserver.responses.get_input_expanded(request: CreateResponse) -> list[Item]: ... - - class azure.ai.agentserver.responses.ConversationChainMetadataNamespace(Protocol): implements Collection def __call__(self, name: Optional[str] = None) -> ConversationChainMetadataNamespace: ... diff --git a/sdk/agentserver/azure-ai-agentserver-responses/api.metadata.yml b/sdk/agentserver/azure-ai-agentserver-responses/api.metadata.yml index 6e10058066bc..6514f6a4f3e8 100644 --- a/sdk/agentserver/azure-ai-agentserver-responses/api.metadata.yml +++ b/sdk/agentserver/azure-ai-agentserver-responses/api.metadata.yml @@ -1,3 +1,3 @@ -apiMdSha256: 973076b8e0bcf383d590af7a133d7020d98a2092eae198b192acf3c27a3c2089 -parserVersion: 0.3.30 +apiMdSha256: 44c704d931f7bc6e9f002301630a8443bffcd2f515bff0054bb3d207fe184c20 +parserVersion: 0.3.31 pythonVersion: 3.11.15 diff --git a/sdk/agentserver/azure-ai-agentserver-responses/azure/ai/agentserver/responses/__init__.py b/sdk/agentserver/azure-ai-agentserver-responses/azure/ai/agentserver/responses/__init__.py index 0273bcec3e0e..4e0bae3fe2fd 100644 --- a/sdk/agentserver/azure-ai-agentserver-responses/azure/ai/agentserver/responses/__init__.py +++ b/sdk/agentserver/azure-ai-agentserver-responses/azure/ai/agentserver/responses/__init__.py @@ -17,9 +17,6 @@ ) from .hosting._routing import ResponsesAgentServerHost from .models import CreateResponse, ResponseObject -from .models._helpers import ( - get_input_expanded, -) from .store._base import ResponseProviderProtocol from .store._file import FileResponseStore from .store._foundry_errors import ( @@ -57,5 +54,4 @@ "TextResponse", "CreateResponse", "ResponseObject", - "get_input_expanded", ] diff --git a/sdk/agentserver/azure-ai-agentserver-responses/azure/ai/agentserver/responses/_version.py b/sdk/agentserver/azure-ai-agentserver-responses/azure/ai/agentserver/responses/_version.py index 796470e6388a..4b7d71b0df29 100644 --- a/sdk/agentserver/azure-ai-agentserver-responses/azure/ai/agentserver/responses/_version.py +++ b/sdk/agentserver/azure-ai-agentserver-responses/azure/ai/agentserver/responses/_version.py @@ -4,4 +4,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # --------------------------------------------------------- -VERSION = "2.0.0b1" +VERSION = "2.0.0" diff --git a/sdk/agentserver/azure-ai-agentserver-responses/dev_requirements.txt b/sdk/agentserver/azure-ai-agentserver-responses/dev_requirements.txt index 64d5d933556b..77d4bc105fde 100644 --- a/sdk/agentserver/azure-ai-agentserver-responses/dev_requirements.txt +++ b/sdk/agentserver/azure-ai-agentserver-responses/dev_requirements.txt @@ -1,7 +1,8 @@ +# keep in sync with pyproject.toml#dependency-groups.dev -e ../../../eng/tools/azure-sdk-tools -e ../azure-ai-agentserver-core ../../monitor/azure-monitor-query -../../identity/azure-identity +azure-identity>=1.17.0 httpx hypercorn openai diff --git a/sdk/agentserver/azure-ai-agentserver-responses/pyproject.toml b/sdk/agentserver/azure-ai-agentserver-responses/pyproject.toml index 75d443f6c5c6..38ee1eeaca44 100644 --- a/sdk/agentserver/azure-ai-agentserver-responses/pyproject.toml +++ b/sdk/agentserver/azure-ai-agentserver-responses/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.10" license = "MIT" authors = [{ name = "Microsoft Corporation" }] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", @@ -18,13 +18,28 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = [ - "azure-ai-agentserver-core>=2.0.0b10", - "azure-core>=1.30.0", + "azure-ai-agentserver-core>=2.0.0", + "azure-core>=1.37.0", "isodate>=0.6.1", "aiohttp>=3.10.0,<4.0.0a0", ] keywords = ["azure", "azure sdk"] +[dependency-groups] +# keep in sync with dev_requirements.txt +dev = [ + "azure-ai-agentserver-core", + "azure-identity>=1.17.0", + "azure-monitor-query", + "azure-sdk-tools", + "httpx", + "hypercorn", + "openai", + "pytest", + "pytest-asyncio", + "starlette", +] + [project.urls] repository = "https://github.com/Azure/azure-sdk-for-python" @@ -67,10 +82,10 @@ pythonpath = ["."] [tool.uv.sources] azure-ai-agentserver-core = { path = "../azure-ai-agentserver-core", editable = true } azure-core = { path = "../../core/azure-core" } +azure-identity = { path = "../../identity/azure-identity" } +azure-monitor-query = { path = "../../monitor/azure-monitor-query" } azure-sdk-tools = { path = "../../../eng/tools/azure-sdk-tools" } [tool.azure-sdk-build] verifytypes = false latestdependency = false -# azure-ai-agentserver-core>=2.0.0b9 is not yet on PyPI -mindependency = false diff --git a/sdk/agentserver/azure-ai-agentserver-responses/tests/interop/test_openai_wire_compliance.py b/sdk/agentserver/azure-ai-agentserver-responses/tests/interop/test_openai_wire_compliance.py index 86283c9941fb..0128162e071a 100644 --- a/sdk/agentserver/azure-ai-agentserver-responses/tests/interop/test_openai_wire_compliance.py +++ b/sdk/agentserver/azure-ai-agentserver-responses/tests/interop/test_openai_wire_compliance.py @@ -24,9 +24,9 @@ ResponseContext, ResponseEventStream, ResponsesAgentServerHost, - get_input_expanded, ) from azure.ai.agentserver.responses.models import ( + get_input_expanded, get_tool_choice_expanded, ) From 649a60c5810911c9cf648ef363c19158443f2bc3 Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Fri, 7 Aug 2026 10:48:57 +0000 Subject: [PATCH 6/6] Scope PR to invocations keep-alive Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CHANGELOG.md | 2 +- .../CHANGELOG.md | 12 --------- .../azure-ai-agentserver-responses/api.md | 3 +++ .../api.metadata.yml | 4 +-- .../ai/agentserver/responses/__init__.py | 4 +++ .../ai/agentserver/responses/_version.py | 2 +- .../dev_requirements.txt | 3 +-- .../pyproject.toml | 25 ++++--------------- .../interop/test_openai_wire_compliance.py | 2 +- 9 files changed, 18 insertions(+), 39 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md b/sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md index fe9e70c5454c..de3343ebd8a7 100644 --- a/sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md +++ b/sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## 1.0.0 (Unreleased) +## 1.0.0 (2026-08-07) ### Bugs Fixed diff --git a/sdk/agentserver/azure-ai-agentserver-responses/CHANGELOG.md b/sdk/agentserver/azure-ai-agentserver-responses/CHANGELOG.md index 7ea340a902ac..f942e1c2a9d0 100644 --- a/sdk/agentserver/azure-ai-agentserver-responses/CHANGELOG.md +++ b/sdk/agentserver/azure-ai-agentserver-responses/CHANGELOG.md @@ -1,17 +1,5 @@ # Release History -## 2.0.0 (Unreleased) - -### Breaking Changes - -- Removed the duplicate `azure.ai.agentserver.responses.get_input_expanded` - export. Import it from `azure.ai.agentserver.responses.models` instead. - -### Other Changes - -- Updated the minimum `azure-ai-agentserver-core` dependency to the stable - `2.0.0` release. - ## 2.0.0b1 (2026-08-04) ### Other Changes diff --git a/sdk/agentserver/azure-ai-agentserver-responses/api.md b/sdk/agentserver/azure-ai-agentserver-responses/api.md index 91b8d12751b8..4c35bb90a53a 100644 --- a/sdk/agentserver/azure-ai-agentserver-responses/api.md +++ b/sdk/agentserver/azure-ai-agentserver-responses/api.md @@ -1,6 +1,9 @@ ```py namespace azure.ai.agentserver.responses + def azure.ai.agentserver.responses.get_input_expanded(request: CreateResponse) -> list[Item]: ... + + class azure.ai.agentserver.responses.ConversationChainMetadataNamespace(Protocol): implements Collection def __call__(self, name: Optional[str] = None) -> ConversationChainMetadataNamespace: ... diff --git a/sdk/agentserver/azure-ai-agentserver-responses/api.metadata.yml b/sdk/agentserver/azure-ai-agentserver-responses/api.metadata.yml index 6514f6a4f3e8..6e10058066bc 100644 --- a/sdk/agentserver/azure-ai-agentserver-responses/api.metadata.yml +++ b/sdk/agentserver/azure-ai-agentserver-responses/api.metadata.yml @@ -1,3 +1,3 @@ -apiMdSha256: 44c704d931f7bc6e9f002301630a8443bffcd2f515bff0054bb3d207fe184c20 -parserVersion: 0.3.31 +apiMdSha256: 973076b8e0bcf383d590af7a133d7020d98a2092eae198b192acf3c27a3c2089 +parserVersion: 0.3.30 pythonVersion: 3.11.15 diff --git a/sdk/agentserver/azure-ai-agentserver-responses/azure/ai/agentserver/responses/__init__.py b/sdk/agentserver/azure-ai-agentserver-responses/azure/ai/agentserver/responses/__init__.py index 4e0bae3fe2fd..0273bcec3e0e 100644 --- a/sdk/agentserver/azure-ai-agentserver-responses/azure/ai/agentserver/responses/__init__.py +++ b/sdk/agentserver/azure-ai-agentserver-responses/azure/ai/agentserver/responses/__init__.py @@ -17,6 +17,9 @@ ) from .hosting._routing import ResponsesAgentServerHost from .models import CreateResponse, ResponseObject +from .models._helpers import ( + get_input_expanded, +) from .store._base import ResponseProviderProtocol from .store._file import FileResponseStore from .store._foundry_errors import ( @@ -54,4 +57,5 @@ "TextResponse", "CreateResponse", "ResponseObject", + "get_input_expanded", ] diff --git a/sdk/agentserver/azure-ai-agentserver-responses/azure/ai/agentserver/responses/_version.py b/sdk/agentserver/azure-ai-agentserver-responses/azure/ai/agentserver/responses/_version.py index 4b7d71b0df29..796470e6388a 100644 --- a/sdk/agentserver/azure-ai-agentserver-responses/azure/ai/agentserver/responses/_version.py +++ b/sdk/agentserver/azure-ai-agentserver-responses/azure/ai/agentserver/responses/_version.py @@ -4,4 +4,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # --------------------------------------------------------- -VERSION = "2.0.0" +VERSION = "2.0.0b1" diff --git a/sdk/agentserver/azure-ai-agentserver-responses/dev_requirements.txt b/sdk/agentserver/azure-ai-agentserver-responses/dev_requirements.txt index 77d4bc105fde..64d5d933556b 100644 --- a/sdk/agentserver/azure-ai-agentserver-responses/dev_requirements.txt +++ b/sdk/agentserver/azure-ai-agentserver-responses/dev_requirements.txt @@ -1,8 +1,7 @@ -# keep in sync with pyproject.toml#dependency-groups.dev -e ../../../eng/tools/azure-sdk-tools -e ../azure-ai-agentserver-core ../../monitor/azure-monitor-query -azure-identity>=1.17.0 +../../identity/azure-identity httpx hypercorn openai diff --git a/sdk/agentserver/azure-ai-agentserver-responses/pyproject.toml b/sdk/agentserver/azure-ai-agentserver-responses/pyproject.toml index 38ee1eeaca44..75d443f6c5c6 100644 --- a/sdk/agentserver/azure-ai-agentserver-responses/pyproject.toml +++ b/sdk/agentserver/azure-ai-agentserver-responses/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.10" license = "MIT" authors = [{ name = "Microsoft Corporation" }] classifiers = [ - "Development Status :: 5 - Production/Stable", + "Development Status :: 4 - Beta", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", @@ -18,28 +18,13 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = [ - "azure-ai-agentserver-core>=2.0.0", - "azure-core>=1.37.0", + "azure-ai-agentserver-core>=2.0.0b10", + "azure-core>=1.30.0", "isodate>=0.6.1", "aiohttp>=3.10.0,<4.0.0a0", ] keywords = ["azure", "azure sdk"] -[dependency-groups] -# keep in sync with dev_requirements.txt -dev = [ - "azure-ai-agentserver-core", - "azure-identity>=1.17.0", - "azure-monitor-query", - "azure-sdk-tools", - "httpx", - "hypercorn", - "openai", - "pytest", - "pytest-asyncio", - "starlette", -] - [project.urls] repository = "https://github.com/Azure/azure-sdk-for-python" @@ -82,10 +67,10 @@ pythonpath = ["."] [tool.uv.sources] azure-ai-agentserver-core = { path = "../azure-ai-agentserver-core", editable = true } azure-core = { path = "../../core/azure-core" } -azure-identity = { path = "../../identity/azure-identity" } -azure-monitor-query = { path = "../../monitor/azure-monitor-query" } azure-sdk-tools = { path = "../../../eng/tools/azure-sdk-tools" } [tool.azure-sdk-build] verifytypes = false latestdependency = false +# azure-ai-agentserver-core>=2.0.0b9 is not yet on PyPI +mindependency = false diff --git a/sdk/agentserver/azure-ai-agentserver-responses/tests/interop/test_openai_wire_compliance.py b/sdk/agentserver/azure-ai-agentserver-responses/tests/interop/test_openai_wire_compliance.py index 0128162e071a..86283c9941fb 100644 --- a/sdk/agentserver/azure-ai-agentserver-responses/tests/interop/test_openai_wire_compliance.py +++ b/sdk/agentserver/azure-ai-agentserver-responses/tests/interop/test_openai_wire_compliance.py @@ -24,9 +24,9 @@ ResponseContext, ResponseEventStream, ResponsesAgentServerHost, + get_input_expanded, ) from azure.ai.agentserver.responses.models import ( - get_input_expanded, get_tool_choice_expanded, )