From e5a6e154fb05da04b8097639df2bd465014e7af4 Mon Sep 17 00:00:00 2001 From: Brandon Allen Date: Mon, 23 Mar 2026 10:41:37 -0400 Subject: [PATCH 01/13] sec: relax fastapi upper bound, floor-pin tornado to fix 2 HIGH CVEs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove `fastapi<0.116` constraint so consumers can resolve fastapi>=0.130 which dropped the starlette<0.47 upper bound, enabling starlette>=0.49.1 (fixes CVE-2025-62727). Add `tornado>=6.5.5` floor to fix CVE-2026-31958. uv.lock: fastapi 0.115.14→0.135.2, starlette 0.46.2→1.0.0, tornado 6.5.2→6.5.5 Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 3 ++- uv.lock | 58 ++++++++++++++++++++++++++++++-------------------- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 17e758d54..16f4dab14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,8 @@ dependencies = [ "typer>=0.16,<0.17", "questionary>=2.0.1,<3", "rich>=13.9.2,<14", - "fastapi>=0.115.0,<0.116", + "fastapi>=0.115.0", # upper bound removed — CVE-2025-62727 (starlette) fix requires >=0.115.12 + "tornado>=6.5.5", # CVE-2026-31958 (HIGH) — floor pin ensures patched release "uvicorn>=0.31.1", "watchfiles>=0.24.0,<1.0", "python-on-whales>=0.73.0,<0.74", diff --git a/uv.lock b/uv.lock index 828726620..9dfed25ee 100644 --- a/uv.lock +++ b/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ [[package]] name = "agentex-sdk" -version = "0.9.3" +version = "0.9.4" source = { editable = "." } dependencies = [ { name = "aiohttp" }, @@ -44,6 +44,7 @@ dependencies = [ { name = "scale-gp-beta" }, { name = "sniffio" }, { name = "temporalio" }, + { name = "tornado" }, { name = "typer" }, { name = "typing-extensions" }, { name = "tzdata" }, @@ -80,7 +81,7 @@ requires-dist = [ { name = "datadog", specifier = ">=0.52.1" }, { name = "ddtrace", specifier = ">=3.13.0" }, { name = "distro", specifier = ">=1.7.0,<2" }, - { name = "fastapi", specifier = ">=0.115.0,<0.116" }, + { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx", specifier = ">=0.27.2,<0.28" }, { name = "httpx-aiohttp", marker = "extra == 'aiohttp'", specifier = ">=0.1.9" }, { name = "ipykernel", specifier = ">=6.29.5" }, @@ -107,6 +108,7 @@ requires-dist = [ { name = "scale-gp-beta", specifier = ">=0.1.0a20" }, { name = "sniffio" }, { name = "temporalio", specifier = ">=1.18.2,<2" }, + { name = "tornado", specifier = ">=6.5.5" }, { name = "typer", specifier = ">=0.16,<0.17" }, { name = "typing-extensions", specifier = ">=4.10,<5" }, { name = "tzdata", specifier = ">=2025.2" }, @@ -197,6 +199,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -564,16 +575,18 @@ wheels = [ [[package]] name = "fastapi" -version = "0.115.14" +version = "0.135.2" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263, upload-time = "2025-06-26T15:29:08.21Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/73/5903c4b13beae98618d64eb9870c3fac4f605523dd0312ca5c80dadbd5b9/fastapi-0.135.2.tar.gz", hash = "sha256:88a832095359755527b7f63bb4c6bc9edb8329a026189eed83d6c1afcf419d56", size = 395833, upload-time = "2026-03-23T14:12:41.697Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514, upload-time = "2025-06-26T15:29:06.49Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ea/18f6d0457f9efb2fc6fa594857f92810cadb03024975726db6546b3d6fcf/fastapi-0.135.2-py3-none-any.whl", hash = "sha256:0af0447d541867e8db2a6a25c23a8c4bd80e2394ac5529bd87501bbb9e240ca5", size = 117407, upload-time = "2026-03-23T14:12:43.284Z" }, ] [[package]] @@ -2422,14 +2435,15 @@ wheels = [ [[package]] name = "starlette" -version = "0.46.2" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, ] [[package]] @@ -2520,21 +2534,19 @@ wheels = [ [[package]] name = "tornado" -version = "6.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" }, - { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" }, - { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" }, - { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" }, - { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" }, - { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" }, - { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" }, - { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" }, - { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" }, +version = "6.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, ] [[package]] From 427ab611d751b49d28db1e76d3b6270e49670ce1 Mon Sep 17 00:00:00 2001 From: Brandon Allen Date: Mon, 23 Mar 2026 11:05:52 -0400 Subject: [PATCH 02/13] fix(tests): update test payloads to match current *Params model schema All *Params models require an `agent: Agent` field and SendEventParams uses `event` (not `message`), SendMessageParams uses `content` (not `message`). Test fixtures were written against an older schema and have been failing since the repo was initialized. Co-Authored-By: Claude Sonnet 4.6 --- .../sdk/fastacp/tests/test_base_acp_server.py | 93 +++++++++++++++---- 1 file changed, 73 insertions(+), 20 deletions(-) diff --git a/src/agentex/lib/sdk/fastacp/tests/test_base_acp_server.py b/src/agentex/lib/sdk/fastacp/tests/test_base_acp_server.py index 0816ac436..768d1b1e1 100644 --- a/src/agentex/lib/sdk/fastacp/tests/test_base_acp_server.py +++ b/src/agentex/lib/sdk/fastacp/tests/test_base_acp_server.py @@ -126,11 +126,20 @@ async def mock_handler(params): "jsonrpc": "2.0", "method": "event/send", "params": { - "task": {"id": "test-task", "agent_id": "test-agent", "status": "RUNNING"}, - "message": { - "type": "text", - "author": "user", - "content": "test message", + "agent": { + "id": "test-agent-456", + "name": "test-agent", + "description": "test agent", + "acp_type": "sync", + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-01T00:00:00Z", + }, + "task": {"id": "test-task"}, + "event": { + "id": "evt-1", + "agent_id": "test-agent-456", + "sequence_id": 1, + "task_id": "test-task", }, }, "id": "test-1", @@ -218,11 +227,20 @@ async def mock_handler(params): "jsonrpc": "2.0", "method": "event/send", "params": { - "task": {"id": "test-task", "agent_id": "test-agent", "status": "RUNNING"}, - "message": { - "type": "text", - "author": "user", - "content": "test message", + "agent": { + "id": "test-agent-456", + "name": "test-agent", + "description": "test agent", + "acp_type": "sync", + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-01T00:00:00Z", + }, + "task": {"id": "test-task"}, + "event": { + "id": "evt-1", + "agent_id": "test-agent-456", + "sequence_id": 1, + "task_id": "test-task", }, }, # No ID = notification @@ -256,7 +274,17 @@ async def mock_handler(params): request = { "jsonrpc": "2.0", "method": "task/cancel", - "params": {"task_id": "test-task-123"}, + "params": { + "agent": { + "id": "test-agent-456", + "name": "test-agent", + "description": "test agent", + "acp_type": "sync", + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-01T00:00:00Z", + }, + "task": {"id": "test-task-123"}, + }, "id": "test-request-1", } @@ -280,7 +308,7 @@ def test_send_message_synchronous_response(self, base_acp_server): async def mock_execute_handler(params): return { "task_id": params.task.id, - "message_content": params.message.content, + "message_content": params.content.content, "status": "executed_synchronously", "custom_data": {"processed": True, "timestamp": "2024-01-01T12:00:00Z"}, } @@ -291,8 +319,16 @@ async def mock_execute_handler(params): "jsonrpc": "2.0", "method": "message/send", "params": { - "task": {"id": "test-task-123", "agent_id": "test-agent", "status": "RUNNING"}, - "message": { + "agent": { + "id": "test-agent-456", + "name": "test-agent", + "description": "test agent", + "acp_type": "sync", + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-01T00:00:00Z", + }, + "task": {"id": "test-task-123"}, + "content": { "type": "text", "author": "user", "content": "Execute this task please", @@ -340,7 +376,15 @@ async def mock_init_handler(params): "jsonrpc": "2.0", "method": "task/create", "params": { - "task": {"id": "test-task-456", "agent_id": "test-agent", "status": "RUNNING"} + "agent": { + "id": "test-agent-456", + "name": "test-agent", + "description": "test agent", + "acp_type": "sync", + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-01T00:00:00Z", + }, + "task": {"id": "test-task-456"}, }, "id": "test-init-1", } @@ -428,11 +472,20 @@ async def failing_handler(params): "jsonrpc": "2.0", "method": "event/send", "params": { - "task": {"id": "test-task", "agent_id": "test-agent", "status": "RUNNING"}, - "message": { - "type": "text", - "author": "user", - "content": "test message", + "agent": { + "id": "test-agent-456", + "name": "test-agent", + "description": "test agent", + "acp_type": "sync", + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-01T00:00:00Z", + }, + "task": {"id": "test-task"}, + "event": { + "id": "evt-1", + "agent_id": "test-agent-456", + "sequence_id": 1, + "task_id": "test-task", }, }, "id": "test-1", From e26b11d61f713f562497aa85bf506fac01b54302 Mon Sep 17 00:00:00 2001 From: Brandon Allen Date: Mon, 23 Mar 2026 11:19:59 -0400 Subject: [PATCH 03/13] fix: cap starlette<1.0.0 to avoid BaseHTTPMiddleware streaming regression starlette 1.0.0 introduced a regression in BaseHTTPMiddleware that broke streaming responses (StreamingResponse through call_next). The tutorial integration test (test-00_sync/010_multiturn) reproduced this as SendMessageResponse.result=None on message/send calls. Add explicit starlette>=0.49.1,<1.0.0: - >=0.49.1 preserves CVE-2025-62727 fix - <1.0.0 keeps BaseHTTPMiddleware streaming behaviour stable fastapi 0.135.2 requires starlette>=0.46.0 (no upper), which satisfies 0.52.1. uv.lock: starlette 1.0.0 -> 0.52.1. Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 3 ++- uv.lock | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 16f4dab14..ab439da63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,8 @@ dependencies = [ "typer>=0.16,<0.17", "questionary>=2.0.1,<3", "rich>=13.9.2,<14", - "fastapi>=0.115.0", # upper bound removed — CVE-2025-62727 (starlette) fix requires >=0.115.12 + "fastapi>=0.115.0", # upper bound removed — CVE-2025-62727 fix needs starlette>=0.49.1 + "starlette>=0.49.1,<1.0.0", # CVE-2025-62727 (HIGH) — floor 0.49.1 patches vuln; cap <1.0.0 avoids BaseHTTPMiddleware streaming regression "tornado>=6.5.5", # CVE-2026-31958 (HIGH) — floor pin ensures patched release "uvicorn>=0.31.1", "watchfiles>=0.24.0,<1.0", diff --git a/uv.lock b/uv.lock index 9dfed25ee..f70927768 100644 --- a/uv.lock +++ b/uv.lock @@ -43,6 +43,7 @@ dependencies = [ { name = "scale-gp" }, { name = "scale-gp-beta" }, { name = "sniffio" }, + { name = "starlette" }, { name = "temporalio" }, { name = "tornado" }, { name = "typer" }, @@ -107,6 +108,7 @@ requires-dist = [ { name = "scale-gp", specifier = ">=0.1.0a59" }, { name = "scale-gp-beta", specifier = ">=0.1.0a20" }, { name = "sniffio" }, + { name = "starlette", specifier = ">=0.49.1,<1.0.0" }, { name = "temporalio", specifier = ">=1.18.2,<2" }, { name = "tornado", specifier = ">=6.5.5" }, { name = "typer", specifier = ">=0.16,<0.17" }, @@ -2435,15 +2437,15 @@ wheels = [ [[package]] name = "starlette" -version = "1.0.0" +version = "0.52.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, ] [[package]] From 7efcf992a2e7526e8fa824791dd1f9fb20e8d765 Mon Sep 17 00:00:00 2001 From: Brandon Allen Date: Mon, 23 Mar 2026 12:58:07 -0400 Subject: [PATCH 04/13] fix: replace BaseHTTPMiddleware with pure ASGI middleware to fix streaming BaseHTTPMiddleware.call_next() silently buffers or drops StreamingResponse bodies in several starlette versions. Since message_send_wrapper always returns an AsyncGenerator (wrapped in StreamingResponse), the Agentex server proxy received result=null for every message/send call through that path. Replace RequestIDMiddleware with a pure ASGI implementation that sets the request-ID context variable and passes scope/receive/send through unmodified, never touching the response body. This unblocks all message/send tutorial integration tests (010_multiturn, 020_streaming, 030_langgraph). Co-Authored-By: Claude Sonnet 4.6 --- .../lib/sdk/fastacp/base/base_acp_server.py | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/agentex/lib/sdk/fastacp/base/base_acp_server.py b/src/agentex/lib/sdk/fastacp/base/base_acp_server.py index b625eaa1c..9a270ce53 100644 --- a/src/agentex/lib/sdk/fastacp/base/base_acp_server.py +++ b/src/agentex/lib/sdk/fastacp/base/base_acp_server.py @@ -12,7 +12,7 @@ from fastapi import FastAPI, Request from pydantic import TypeAdapter, ValidationError from fastapi.responses import StreamingResponse -from starlette.middleware.base import BaseHTTPMiddleware +from starlette.types import ASGIApp, Receive, Scope, Send from agentex.lib.types.acp import ( RPC_SYNC_METHODS, @@ -43,17 +43,25 @@ task_message_update_adapter = TypeAdapter(TaskMessageUpdate) -class RequestIDMiddleware(BaseHTTPMiddleware): - """Middleware to extract or generate request IDs and add them to logs and response headers""" +class RequestIDMiddleware: + """Pure ASGI middleware to extract or generate request IDs and set them in the logging context. - async def dispatch(self, request: Request, call_next): # type: ignore[override] - # Extract request ID from header or generate a new one if there isn't one - request_id = request.headers.get("x-request-id") or uuid.uuid4().hex - # Store request ID in request state for access in handlers - ctx_var_request_id.set(request_id) - # Process request - response = await call_next(request) - return response + Implemented as a pure ASGI middleware (rather than BaseHTTPMiddleware) so that it never + buffers the response body. BaseHTTPMiddleware's call_next() silently swallows + StreamingResponse bodies in several starlette versions, which caused message/send handlers + to return result=null through the Agentex server proxy. + """ + + def __init__(self, app: ASGIApp) -> None: + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] == "http": + headers = dict(scope.get("headers", [])) + raw_request_id = headers.get(b"x-request-id", b"") + request_id = raw_request_id.decode() if raw_request_id else uuid.uuid4().hex + ctx_var_request_id.set(request_id) + await self.app(scope, receive, send) class BaseACPServer(FastAPI): From 1b04faeb692abf79f3127baf714d7148a6471e1a Mon Sep 17 00:00:00 2001 From: Brandon Allen Date: Mon, 23 Mar 2026 13:01:01 -0400 Subject: [PATCH 05/13] fix: sort imports in base_acp_server to satisfy ruff isort Co-Authored-By: Claude Sonnet 4.6 --- src/agentex/lib/sdk/fastacp/base/base_acp_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agentex/lib/sdk/fastacp/base/base_acp_server.py b/src/agentex/lib/sdk/fastacp/base/base_acp_server.py index 9a270ce53..66b59bed4 100644 --- a/src/agentex/lib/sdk/fastacp/base/base_acp_server.py +++ b/src/agentex/lib/sdk/fastacp/base/base_acp_server.py @@ -10,8 +10,8 @@ import uvicorn from fastapi import FastAPI, Request -from pydantic import TypeAdapter, ValidationError from fastapi.responses import StreamingResponse +from pydantic import TypeAdapter, ValidationError from starlette.types import ASGIApp, Receive, Scope, Send from agentex.lib.types.acp import ( From 408b1a044eec7493f1546acb5e3d15b9b02e2ed2 Mon Sep 17 00:00:00 2001 From: Brandon Allen Date: Mon, 23 Mar 2026 13:04:07 -0400 Subject: [PATCH 06/13] fix: restore original import order to satisfy ruff isort Co-Authored-By: Claude Sonnet 4.6 --- src/agentex/lib/sdk/fastacp/base/base_acp_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agentex/lib/sdk/fastacp/base/base_acp_server.py b/src/agentex/lib/sdk/fastacp/base/base_acp_server.py index 66b59bed4..9a270ce53 100644 --- a/src/agentex/lib/sdk/fastacp/base/base_acp_server.py +++ b/src/agentex/lib/sdk/fastacp/base/base_acp_server.py @@ -10,8 +10,8 @@ import uvicorn from fastapi import FastAPI, Request -from fastapi.responses import StreamingResponse from pydantic import TypeAdapter, ValidationError +from fastapi.responses import StreamingResponse from starlette.types import ASGIApp, Receive, Scope, Send from agentex.lib.types.acp import ( From 548a016fb0ac4b89646487ea7b9109a303062e78 Mon Sep 17 00:00:00 2001 From: Brandon Allen Date: Mon, 23 Mar 2026 13:09:36 -0400 Subject: [PATCH 07/13] fix: remove starlette<1.0.0 cap now that BaseHTTPMiddleware is gone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The <1.0.0 ceiling was added to avoid a BaseHTTPMiddleware+StreamingResponse regression in starlette 1.0.0. Since RequestIDMiddleware is now a pure ASGI middleware with no call_next() involvement, that regression no longer applies. Removing the cap lets consumers reach any starlette 1.x CVE fix without needing an explicit bump here. uv.lock re-resolves starlette 0.52.1 → 1.0.0. Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 2 +- src/agentex/lib/sdk/fastacp/base/base_acp_server.py | 2 +- uv.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ab439da63..070863e47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ dependencies = [ "questionary>=2.0.1,<3", "rich>=13.9.2,<14", "fastapi>=0.115.0", # upper bound removed — CVE-2025-62727 fix needs starlette>=0.49.1 - "starlette>=0.49.1,<1.0.0", # CVE-2025-62727 (HIGH) — floor 0.49.1 patches vuln; cap <1.0.0 avoids BaseHTTPMiddleware streaming regression + "starlette>=0.49.1", # CVE-2025-62727 (HIGH) — floor 0.49.1 patches vuln "tornado>=6.5.5", # CVE-2026-31958 (HIGH) — floor pin ensures patched release "uvicorn>=0.31.1", "watchfiles>=0.24.0,<1.0", diff --git a/src/agentex/lib/sdk/fastacp/base/base_acp_server.py b/src/agentex/lib/sdk/fastacp/base/base_acp_server.py index 9a270ce53..1ecadf3a3 100644 --- a/src/agentex/lib/sdk/fastacp/base/base_acp_server.py +++ b/src/agentex/lib/sdk/fastacp/base/base_acp_server.py @@ -11,8 +11,8 @@ import uvicorn from fastapi import FastAPI, Request from pydantic import TypeAdapter, ValidationError +from starlette.types import Send, Scope, ASGIApp, Receive from fastapi.responses import StreamingResponse -from starlette.types import ASGIApp, Receive, Scope, Send from agentex.lib.types.acp import ( RPC_SYNC_METHODS, diff --git a/uv.lock b/uv.lock index f70927768..d3400394f 100644 --- a/uv.lock +++ b/uv.lock @@ -108,7 +108,7 @@ requires-dist = [ { name = "scale-gp", specifier = ">=0.1.0a59" }, { name = "scale-gp-beta", specifier = ">=0.1.0a20" }, { name = "sniffio" }, - { name = "starlette", specifier = ">=0.49.1,<1.0.0" }, + { name = "starlette", specifier = ">=0.49.1" }, { name = "temporalio", specifier = ">=1.18.2,<2" }, { name = "tornado", specifier = ">=6.5.5" }, { name = "typer", specifier = ">=0.16,<0.17" }, @@ -2437,15 +2437,15 @@ wheels = [ [[package]] name = "starlette" -version = "0.52.1" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, ] [[package]] From a816bfd9bf1595e47ac02d22fec6371bdeace38a Mon Sep 17 00:00:00 2001 From: Brandon Allen Date: Mon, 23 Mar 2026 13:20:38 -0400 Subject: [PATCH 08/13] fix: add temporalio[opentelemetry] extra to resolve ModuleNotFoundError Tutorial agents were failing with `ModuleNotFoundError: No module named 'opentelemetry.sdk'` because `opentelemetry-sdk` was an accidental transitive dep of `ddtrace` in the old resolution graph. Removing the starlette<1.0.0 cap changed uv's resolution for tutorial `uv run --with` environments, exposing the missing explicit dependency. Declaring the `[opentelemetry]` extra on temporalio makes the dependency explicit so all environments resolve it correctly. Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 2 +- uv.lock | 37 +++++++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 070863e47..d44b1fad9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "pyyaml>=6.0.2,<7", "jsonschema>=4.23.0,<5", "jsonref>=1.1.0,<2", - "temporalio>=1.18.2,<2", + "temporalio[opentelemetry]>=1.18.2,<2", "aiohttp>=3.10.10,<4", "redis>=5.2.0,<6", "litellm>=1.66.0,<2", diff --git a/uv.lock b/uv.lock index d3400394f..2bca91656 100644 --- a/uv.lock +++ b/uv.lock @@ -44,7 +44,7 @@ dependencies = [ { name = "scale-gp-beta" }, { name = "sniffio" }, { name = "starlette" }, - { name = "temporalio" }, + { name = "temporalio", extra = ["opentelemetry"] }, { name = "tornado" }, { name = "typer" }, { name = "typing-extensions" }, @@ -109,7 +109,7 @@ requires-dist = [ { name = "scale-gp-beta", specifier = ">=0.1.0a20" }, { name = "sniffio" }, { name = "starlette", specifier = ">=0.49.1" }, - { name = "temporalio", specifier = ">=1.18.2,<2" }, + { name = "temporalio", extras = ["opentelemetry"], specifier = ">=1.18.2,<2" }, { name = "tornado", specifier = ">=6.5.5" }, { name = "typer", specifier = ">=0.16,<0.17" }, { name = "typing-extensions", specifier = ">=4.10,<5" }, @@ -1504,6 +1504,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/48/28ed9e55dcf2f453128df738210a980e09f4e468a456fa3c763dbc8be70a/opentelemetry_api-1.37.0-py3-none-any.whl", hash = "sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47", size = 65732, upload-time = "2025-09-11T10:28:41.826Z" }, ] +[[package]] +name = "opentelemetry-sdk" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/62/2e0ca80d7fe94f0b193135375da92c640d15fe81f636658d2acf373086bc/opentelemetry_sdk-1.37.0.tar.gz", hash = "sha256:cc8e089c10953ded765b5ab5669b198bbe0af1b3f89f1007d19acd32dc46dda5", size = 170404, upload-time = "2025-09-11T10:29:11.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/62/9f4ad6a54126fb00f7ed4bb5034964c6e4f00fcd5a905e115bd22707e20d/opentelemetry_sdk-1.37.0-py3-none-any.whl", hash = "sha256:8f3c3c22063e52475c5dbced7209495c2c16723d016d39287dfc215d1771257c", size = 131941, upload-time = "2025-09-11T10:28:57.83Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/1b/90701d91e6300d9f2fb352153fb1721ed99ed1f6ea14fa992c756016e63a/opentelemetry_semantic_conventions-0.58b0.tar.gz", hash = "sha256:6bd46f51264279c433755767bb44ad00f1c9e2367e1b42af563372c5a6fa0c25", size = 129867, upload-time = "2025-09-11T10:29:12.597Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/90/68152b7465f50285d3ce2481b3aec2f82822e3f52e5152eeeaf516bab841/opentelemetry_semantic_conventions-0.58b0-py3-none-any.whl", hash = "sha256:5564905ab1458b96684db1340232729fce3b5375a06e140e8904c78e4f815b28", size = 207954, upload-time = "2025-09-11T10:28:59.218Z" }, +] + [[package]] name = "orjson" version = "3.11.7" @@ -2467,6 +2494,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/a2/ea81149ae7faa154aa842e9dd88390df3158a687db694e06d08716c030b6/temporalio-1.18.2-cp39-abi3-win_amd64.whl", hash = "sha256:7e1f3da98cf23af7094467f1c1180374a10c38aa712cd7e868e15412dd2c6cde", size = 13059111, upload-time = "2025-10-27T19:24:48.382Z" }, ] +[package.optional-dependencies] +opentelemetry = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, +] + [[package]] name = "tenacity" version = "9.1.4" From 03fadf7182996bd305cd080a28098ae30f61b906 Mon Sep 17 00:00:00 2001 From: Brandon Allen Date: Mon, 23 Mar 2026 13:34:17 -0400 Subject: [PATCH 09/13] fix: replace fastacp uuid import with stdlib import in tutorial tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four tutorial test files imported `uuid` via: from agentex.lib.sdk.fastacp.base.base_acp_server import uuid This triggered the full fastacp import chain at test collection time: fastacp → types.fastacp → core.clients.temporal.utils → temporalio.contrib.openai_agents → opentelemetry.sdk The test runner installs agentex-sdk from PyPI (not the built wheel), so the published version's temporalio lacks the [opentelemetry] extra, causing ModuleNotFoundError at test collection time. Fix: use `import uuid` from the standard library directly. Co-Authored-By: Claude Sonnet 4.6 --- examples/tutorials/00_sync/010_multiturn/tests/test_agent.py | 2 +- examples/tutorials/00_sync/020_streaming/tests/test_agent.py | 2 +- examples/tutorials/00_sync/030_langgraph/tests/test_agent.py | 2 +- .../10_async/00_base/100_langgraph/tests/test_agent.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/tutorials/00_sync/010_multiturn/tests/test_agent.py b/examples/tutorials/00_sync/010_multiturn/tests/test_agent.py index 510e9159d..b0f05c1ad 100644 --- a/examples/tutorials/00_sync/010_multiturn/tests/test_agent.py +++ b/examples/tutorials/00_sync/010_multiturn/tests/test_agent.py @@ -17,6 +17,7 @@ """ import os +import uuid import pytest from test_utils.sync import validate_text_in_string, collect_streaming_response @@ -24,7 +25,6 @@ from agentex import Agentex from agentex.types import TextContent, TextContentParam from agentex.types.agent_rpc_params import ParamsCreateTaskRequest, ParamsSendMessageRequest -from agentex.lib.sdk.fastacp.base.base_acp_server import uuid # Configuration from environment variables AGENTEX_API_BASE_URL = os.environ.get("AGENTEX_API_BASE_URL", "http://localhost:5003") diff --git a/examples/tutorials/00_sync/020_streaming/tests/test_agent.py b/examples/tutorials/00_sync/020_streaming/tests/test_agent.py index b4ff65ff5..81c7e78aa 100644 --- a/examples/tutorials/00_sync/020_streaming/tests/test_agent.py +++ b/examples/tutorials/00_sync/020_streaming/tests/test_agent.py @@ -17,6 +17,7 @@ """ import os +import uuid import pytest from test_utils.sync import validate_text_in_string, collect_streaming_response @@ -24,7 +25,6 @@ from agentex import Agentex from agentex.types import TextContent, TextContentParam from agentex.types.agent_rpc_params import ParamsCreateTaskRequest, ParamsSendMessageRequest -from agentex.lib.sdk.fastacp.base.base_acp_server import uuid # Configuration from environment variables AGENTEX_API_BASE_URL = os.environ.get("AGENTEX_API_BASE_URL", "http://localhost:5003") diff --git a/examples/tutorials/00_sync/030_langgraph/tests/test_agent.py b/examples/tutorials/00_sync/030_langgraph/tests/test_agent.py index 36fcf418f..03acf9cbf 100644 --- a/examples/tutorials/00_sync/030_langgraph/tests/test_agent.py +++ b/examples/tutorials/00_sync/030_langgraph/tests/test_agent.py @@ -16,6 +16,7 @@ """ import os +import uuid import pytest from test_utils.sync import validate_text_in_string, collect_streaming_response @@ -23,7 +24,6 @@ from agentex import Agentex from agentex.types import TextContent, TextContentParam from agentex.types.agent_rpc_params import ParamsCreateTaskRequest, ParamsSendMessageRequest -from agentex.lib.sdk.fastacp.base.base_acp_server import uuid # Configuration from environment variables AGENTEX_API_BASE_URL = os.environ.get("AGENTEX_API_BASE_URL", "http://localhost:5003") diff --git a/examples/tutorials/10_async/00_base/100_langgraph/tests/test_agent.py b/examples/tutorials/10_async/00_base/100_langgraph/tests/test_agent.py index 948db1558..d88e1dc4d 100644 --- a/examples/tutorials/10_async/00_base/100_langgraph/tests/test_agent.py +++ b/examples/tutorials/10_async/00_base/100_langgraph/tests/test_agent.py @@ -16,6 +16,7 @@ """ import os +import uuid import pytest import pytest_asyncio @@ -23,7 +24,6 @@ from agentex import AsyncAgentex from agentex.types import TextContentParam from agentex.types.agent_rpc_params import ParamsCreateTaskRequest -from agentex.lib.sdk.fastacp.base.base_acp_server import uuid # Configuration from environment variables AGENTEX_API_BASE_URL = os.environ.get("AGENTEX_API_BASE_URL", "http://localhost:5003") From 6fa9d54bfa0580e72140b24d89bb821f55b29dd7 Mon Sep 17 00:00:00 2001 From: Brandon Allen Date: Mon, 23 Mar 2026 14:02:24 -0400 Subject: [PATCH 10/13] fix: replace temporalio[opentelemetry] extra with explicit opentelemetry deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using the opentelemetry extra on the temporalio specifier caused uv to resolve a different dependency graph in tutorial environments (uv run --with wheel), which broke the _JSONTypeConverterUnhandled import used by scale-gp at worker startup time. Listing opentelemetry-api and opentelemetry-sdk as explicit direct dependencies keeps the same packages in the wheel while letting temporalio resolve independently in tutorial envs — matching the pre-existing resolution that scale-gp expects. Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 4 +++- uv.lock | 14 ++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d44b1fad9..6d3d23b15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,9 @@ dependencies = [ "pyyaml>=6.0.2,<7", "jsonschema>=4.23.0,<5", "jsonref>=1.1.0,<2", - "temporalio[opentelemetry]>=1.18.2,<2", + "temporalio>=1.18.2,<2", + "opentelemetry-api>=1.11.1,<2", + "opentelemetry-sdk>=1.11.1,<2", "aiohttp>=3.10.10,<4", "redis>=5.2.0,<6", "litellm>=1.66.0,<2", diff --git a/uv.lock b/uv.lock index 2bca91656..8a85c5436 100644 --- a/uv.lock +++ b/uv.lock @@ -32,6 +32,8 @@ dependencies = [ { name = "mcp", extra = ["cli"] }, { name = "openai" }, { name = "openai-agents" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, { name = "pydantic" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -44,7 +46,7 @@ dependencies = [ { name = "scale-gp-beta" }, { name = "sniffio" }, { name = "starlette" }, - { name = "temporalio", extra = ["opentelemetry"] }, + { name = "temporalio" }, { name = "tornado" }, { name = "typer" }, { name = "typing-extensions" }, @@ -96,6 +98,8 @@ requires-dist = [ { name = "mcp", extras = ["cli"], specifier = ">=1.4.1" }, { name = "openai", specifier = ">=2.2,<3" }, { name = "openai-agents", specifier = "==0.4.2" }, + { name = "opentelemetry-api", specifier = ">=1.11.1,<2" }, + { name = "opentelemetry-sdk", specifier = ">=1.11.1,<2" }, { name = "pydantic", specifier = ">=2.0.0,<3" }, { name = "pytest", specifier = ">=8.4.0" }, { name = "pytest-asyncio", specifier = ">=1.0.0" }, @@ -109,7 +113,7 @@ requires-dist = [ { name = "scale-gp-beta", specifier = ">=0.1.0a20" }, { name = "sniffio" }, { name = "starlette", specifier = ">=0.49.1" }, - { name = "temporalio", extras = ["opentelemetry"], specifier = ">=1.18.2,<2" }, + { name = "temporalio", specifier = ">=1.18.2,<2" }, { name = "tornado", specifier = ">=6.5.5" }, { name = "typer", specifier = ">=0.16,<0.17" }, { name = "typing-extensions", specifier = ">=4.10,<5" }, @@ -2494,12 +2498,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/a2/ea81149ae7faa154aa842e9dd88390df3158a687db694e06d08716c030b6/temporalio-1.18.2-cp39-abi3-win_amd64.whl", hash = "sha256:7e1f3da98cf23af7094467f1c1180374a10c38aa712cd7e868e15412dd2c6cde", size = 13059111, upload-time = "2025-10-27T19:24:48.382Z" }, ] -[package.optional-dependencies] -opentelemetry = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, -] - [[package]] name = "tenacity" version = "9.1.4" From 264a8f463bcb9c22f3a99339a803c525dfc135ee Mon Sep 17 00:00:00 2001 From: Brandon Allen Date: Mon, 23 Mar 2026 14:09:02 -0400 Subject: [PATCH 11/13] fix: remove private _JSONTypeConverterUnhandled import from worker.py This private symbol was used only as a return type annotation. In some resolved versions of temporalio (within the >=1.18.2,<2 range), this private name is not exported, causing an ImportError at worker startup time in tutorial environments. The sentinel value JSONTypeConverter.Unhandled (public API) is what the method actually returns; the annotation is simplified to Any. Co-Authored-By: Claude Sonnet 4.6 --- src/agentex/lib/core/temporal/workers/worker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/agentex/lib/core/temporal/workers/worker.py b/src/agentex/lib/core/temporal/workers/worker.py index 28cab2e14..9a6e8ea53 100644 --- a/src/agentex/lib/core/temporal/workers/worker.py +++ b/src/agentex/lib/core/temporal/workers/worker.py @@ -24,7 +24,6 @@ DefaultPayloadConverter, CompositePayloadConverter, JSONPlainPayloadConverter, - _JSONTypeConverterUnhandled, ) from temporalio.contrib.openai_agents import OpenAIAgentsPlugin @@ -45,7 +44,7 @@ def default(self, o: Any) -> Any: class DateTimeJSONTypeConverter(JSONTypeConverter): @override - def to_typed_value(self, hint: type, value: Any) -> Any | None | _JSONTypeConverterUnhandled: + def to_typed_value(self, hint: type, value: Any) -> Any: if hint == datetime.datetime: return datetime.datetime.fromisoformat(value) return JSONTypeConverter.Unhandled From 0bcda2f663ad16ace7efcd72e0eb4dd938f89c35 Mon Sep 17 00:00:00 2001 From: Brandon Allen Date: Mon, 23 Mar 2026 14:41:51 -0400 Subject: [PATCH 12/13] fix: add current user message to input_list and fix langgraph model in sync tutorials - 010_multiturn/acp.py: append current user message to input_list before Runner.run(); without this, Runner.run() was called with an empty list (history excludes current turn), causing an OpenAI API error for missing input - 020_streaming/acp.py: same fix for Runner.run_streamed(); also add `return` after yielding the no-API-key error message so the generator does not fall through - 030_langgraph/graph.py: change MODEL_NAME from "gpt-5" (invalid) to "gpt-4o"; remove unsupported `reasoning` kwarg from ChatOpenAI constructor Co-Authored-By: Claude Sonnet 4.6 --- examples/tutorials/00_sync/010_multiturn/project/acp.py | 3 +++ examples/tutorials/00_sync/020_streaming/project/acp.py | 4 ++++ examples/tutorials/00_sync/030_langgraph/project/graph.py | 3 +-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/tutorials/00_sync/010_multiturn/project/acp.py b/examples/tutorials/00_sync/010_multiturn/project/acp.py index b0d2098fb..5ae187542 100644 --- a/examples/tutorials/00_sync/010_multiturn/project/acp.py +++ b/examples/tutorials/00_sync/010_multiturn/project/acp.py @@ -88,6 +88,9 @@ async def handle_message_send( # Convert task messages to OpenAI Agents SDK format input_list = convert_task_messages_to_oai_agents_inputs(task_messages) + # Append the current user message (not yet persisted in task history) + input_list.append({"role": "user", "content": params.content.content}) + # Run the agent result = await Runner.run(test_agent, input_list, run_config=run_config) diff --git a/examples/tutorials/00_sync/020_streaming/project/acp.py b/examples/tutorials/00_sync/020_streaming/project/acp.py index 5f3d8e78e..8e7f5bcf9 100644 --- a/examples/tutorials/00_sync/020_streaming/project/acp.py +++ b/examples/tutorials/00_sync/020_streaming/project/acp.py @@ -56,6 +56,7 @@ async def handle_message_send( content="Hey, sorry I'm unable to respond to your message because you're running this example without an OpenAI API key. Please set the OPENAI_API_KEY environment variable to run this example. Do this by either by adding a .env file to the project/ directory or by setting the environment variable in your terminal.", ), ) + return # Try to retrieve the state. If it doesn't exist, create it. task_state = await adk.state.get_by_task_and_agent(task_id=params.task.id, agent_id=params.agent.id) @@ -86,6 +87,9 @@ async def handle_message_send( # Convert task messages to OpenAI Agents SDK format input_list = convert_task_messages_to_oai_agents_inputs(task_messages) + # Append the current user message (not yet persisted in task history) + input_list.append({"role": "user", "content": params.content.content}) + # Run the agent and stream the events result = Runner.run_streamed(test_agent, input_list, run_config=run_config) diff --git a/examples/tutorials/00_sync/030_langgraph/project/graph.py b/examples/tutorials/00_sync/030_langgraph/project/graph.py index 53728cd58..32cbfb38c 100644 --- a/examples/tutorials/00_sync/030_langgraph/project/graph.py +++ b/examples/tutorials/00_sync/030_langgraph/project/graph.py @@ -20,7 +20,7 @@ from project.tools import TOOLS from agentex.lib.adk import create_checkpointer -MODEL_NAME = "gpt-5" +MODEL_NAME = "gpt-4o" SYSTEM_PROMPT = """You are a helpful AI assistant with access to tools. Current date and time: {timestamp} @@ -46,7 +46,6 @@ async def create_graph(): """ llm = ChatOpenAI( model=MODEL_NAME, - reasoning={"effort": "high", "summary": "auto"}, ) llm_with_tools = llm.bind_tools(TOOLS) From 8f007bca4af687d207fe74fc776e98f0b6199274 Mon Sep 17 00:00:00 2001 From: Brandon Allen Date: Mon, 23 Mar 2026 16:15:42 -0400 Subject: [PATCH 13/13] fix: use built SDK wheel for tutorial tests and fix tutorial agent bugs - run_agent_test.sh: pass built wheel to pytest so tests use local SDK instead of PyPI version (fixes send_message NDJSON pydantic errors) - agents.py: rewrite send_message (sync+async) to use streaming internally and handle NDJSON responses from FastACP server - agent_rpc_response.py: make SendMessageStreamResponse.result Optional to handle null result in streaming done events - hooks.py: fix execute_activity_method -> execute_activity for function-based Temporal activities (fixes tool_request not appearing) - Tutorial handlers: add missing return after no-API-key sorry messages to prevent fall-through to LLM calls (010_multiturn, 020_streaming, 030_tracing, 040_other_sdks, 010_agent_chat, 050_agent_chat_guardrails) - 010_agent_chat/workflow.py: fix gpt-5 -> gpt-4o, remove invalid reasoning params (only valid for o-series models) - 080_human_in_the_loop/workflow.py: guard span.output access against None - test_agent.py (010_multiturn): add sleep for async state init race Co-Authored-By: Claude Sonnet 4.6 --- .../00_base/010_multiturn/project/acp.py | 1 + .../00_base/010_multiturn/tests/test_agent.py | 1 + .../00_base/020_streaming/project/acp.py | 1 + .../00_base/030_tracing/project/acp.py | 1 + .../00_base/040_other_sdks/project/acp.py | 1 + .../010_agent_chat/project/workflow.py | 9 +- .../project/workflow.py | 1 + .../project/workflow.py | 3 +- examples/tutorials/run_agent_test.sh | 15 +- .../plugins/openai_agents/hooks/hooks.py | 6 +- src/agentex/resources/agents.py | 194 ++++++++++++------ src/agentex/types/agent_rpc_response.py | 2 +- 12 files changed, 160 insertions(+), 75 deletions(-) diff --git a/examples/tutorials/10_async/00_base/010_multiturn/project/acp.py b/examples/tutorials/10_async/00_base/010_multiturn/project/acp.py index a32eed68e..4bf080d4f 100644 --- a/examples/tutorials/10_async/00_base/010_multiturn/project/acp.py +++ b/examples/tutorials/10_async/00_base/010_multiturn/project/acp.py @@ -94,6 +94,7 @@ async def handle_event_send(params: SendEventParams): content="Hey, sorry I'm unable to respond to your message because you're running this example without an OpenAI API key. Please set the OPENAI_API_KEY environment variable to run this example. Do this by either by adding a .env file to the project/ directory or by setting the environment variable in your terminal.", ), ) + return ######################################################### # 5. (👋) Retrieve the task state. diff --git a/examples/tutorials/10_async/00_base/010_multiturn/tests/test_agent.py b/examples/tutorials/10_async/00_base/010_multiturn/tests/test_agent.py index 33d831858..43d283b8c 100644 --- a/examples/tutorials/10_async/00_base/010_multiturn/tests/test_agent.py +++ b/examples/tutorials/10_async/00_base/010_multiturn/tests/test_agent.py @@ -145,6 +145,7 @@ async def test_send_event_and_stream(self, client: AsyncAgentex, agent_id: str): task = task_response.result assert task is not None + await asyncio.sleep(1) # wait for state to be initialized # Check initial state states = await client.states.list(agent_id=agent_id, task_id=task.id) assert len(states) == 1 diff --git a/examples/tutorials/10_async/00_base/020_streaming/project/acp.py b/examples/tutorials/10_async/00_base/020_streaming/project/acp.py index 41e44912e..483d4ea5f 100644 --- a/examples/tutorials/10_async/00_base/020_streaming/project/acp.py +++ b/examples/tutorials/10_async/00_base/020_streaming/project/acp.py @@ -76,6 +76,7 @@ async def handle_event_send(params: SendEventParams): content="Hey, sorry I'm unable to respond to your message because you're running this example without an OpenAI API key. Please set the OPENAI_API_KEY environment variable to run this example. Do this by either by adding a .env file to the project/ directory or by setting the environment variable in your terminal.", ), ) + return ######################################################### # 5. Retrieve the task state. diff --git a/examples/tutorials/10_async/00_base/030_tracing/project/acp.py b/examples/tutorials/10_async/00_base/030_tracing/project/acp.py index a46e77698..aca122164 100644 --- a/examples/tutorials/10_async/00_base/030_tracing/project/acp.py +++ b/examples/tutorials/10_async/00_base/030_tracing/project/acp.py @@ -117,6 +117,7 @@ async def handle_event_send(params: SendEventParams): ), parent_span_id=span.id if span else None, ) + return ######################################################### # 7. Call an LLM to respond to the user's message diff --git a/examples/tutorials/10_async/00_base/040_other_sdks/project/acp.py b/examples/tutorials/10_async/00_base/040_other_sdks/project/acp.py index d2ec84fcd..e860f5ef0 100644 --- a/examples/tutorials/10_async/00_base/040_other_sdks/project/acp.py +++ b/examples/tutorials/10_async/00_base/040_other_sdks/project/acp.py @@ -111,6 +111,7 @@ async def handle_event_send(params: SendEventParams): ), parent_span_id=span.id if span else None, ) + return ######################################################### # (👋) Call an LLM to respond to the user's message using custom streaming diff --git a/examples/tutorials/10_async/10_temporal/010_agent_chat/project/workflow.py b/examples/tutorials/10_async/10_temporal/010_agent_chat/project/workflow.py index 3e3ac5b27..ced7ec5dc 100644 --- a/examples/tutorials/10_async/10_temporal/010_agent_chat/project/workflow.py +++ b/examples/tutorials/10_async/10_temporal/010_agent_chat/project/workflow.py @@ -216,6 +216,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None: ), parent_span_id=span.id if span else None, ) + return # Call an LLM to respond to the user's message # When send_as_agent_task_message=True, returns a TaskMessage @@ -234,13 +235,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None: "to provide accurate and well-reasoned responses." ), parent_span_id=span.id if span else None, - model="gpt-5", - model_settings=ModelSettings( - # Include reasoning items in the response (IDs, summaries) - # response_include=["reasoning.encrypted_content"], - # Ask the model to include a short reasoning summary - reasoning=Reasoning(effort="medium", summary="detailed"), - ), + model="gpt-4o", # tools=[CALCULATOR_TOOL], ) if self._state: diff --git a/examples/tutorials/10_async/10_temporal/050_agent_chat_guardrails/project/workflow.py b/examples/tutorials/10_async/10_temporal/050_agent_chat_guardrails/project/workflow.py index b54c8fade..508c2e03e 100644 --- a/examples/tutorials/10_async/10_temporal/050_agent_chat_guardrails/project/workflow.py +++ b/examples/tutorials/10_async/10_temporal/050_agent_chat_guardrails/project/workflow.py @@ -408,6 +408,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None: ), parent_span_id=span.id if span else None, ) + return # Call an LLM to respond to the user's message # When send_as_agent_task_message=True, returns a TaskMessage diff --git a/examples/tutorials/10_async/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/workflow.py b/examples/tutorials/10_async/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/workflow.py index 4f11ac4c0..a32e4ad1a 100644 --- a/examples/tutorials/10_async/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/workflow.py +++ b/examples/tutorials/10_async/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/workflow.py @@ -204,7 +204,8 @@ async def on_task_event_send(self, params: SendEventParams) -> None: self._state.input_list.append(msg) # Set span output for tracing - include full state - span.output = self._state.model_dump() + if span: + span.output = self._state.model_dump() @workflow.run async def on_task_create(self, params: CreateTaskParams) -> str: diff --git a/examples/tutorials/run_agent_test.sh b/examples/tutorials/run_agent_test.sh index f396cfd00..b25f63222 100755 --- a/examples/tutorials/run_agent_test.sh +++ b/examples/tutorials/run_agent_test.sh @@ -259,6 +259,19 @@ run_test() { cd "$tutorial_path" || return 1 + # Determine pytest command - use built wheel if available (same wheel used to start agent) + local pytest_cmd="uv run pytest" + if [ "$BUILD_CLI" = true ]; then + local wheel_file=$(ls /home/runner/work/*/*/dist/agentex_sdk-*.whl 2>/dev/null | head -n1) + if [[ -z "$wheel_file" ]]; then + # Fallback for local development + wheel_file=$(ls "${SCRIPT_DIR}/../../dist/agentex_sdk-*.whl" 2>/dev/null | head -n1) + fi + if [[ -n "$wheel_file" ]]; then + pytest_cmd="uv run --with $wheel_file pytest" + fi + fi + # Run the tests with retry mechanism local max_retries=5 local retry_count=0 @@ -270,7 +283,7 @@ run_test() { fi # Stream pytest output directly in real-time - uv run pytest tests/test_agent.py -v -s + $pytest_cmd tests/test_agent.py -v -s exit_code=$? if [ $exit_code -eq 0 ]; then diff --git a/src/agentex/lib/core/temporal/plugins/openai_agents/hooks/hooks.py b/src/agentex/lib/core/temporal/plugins/openai_agents/hooks/hooks.py index 795d44a0a..798481ba6 100644 --- a/src/agentex/lib/core/temporal/plugins/openai_agents/hooks/hooks.py +++ b/src/agentex/lib/core/temporal/plugins/openai_agents/hooks/hooks.py @@ -131,7 +131,7 @@ async def on_tool_start(self, context: RunContextWrapper, agent: Agent, tool: To logger.warning(f"Failed to parse tool arguments: {tool_context.tool_arguments}") tool_arguments = {} - await workflow.execute_activity_method( + await workflow.execute_activity( stream_lifecycle_content, args=[ self.task_id, @@ -167,7 +167,7 @@ async def on_tool_end( else f"call_{id(tool)}" ) - await workflow.execute_activity_method( + await workflow.execute_activity( stream_lifecycle_content, args=[ self.task_id, @@ -195,7 +195,7 @@ async def on_handoff( from_agent: The agent transferring control to_agent: The agent receiving control """ - await workflow.execute_activity_method( + await workflow.execute_activity( stream_lifecycle_content, args=[ self.task_id, diff --git a/src/agentex/resources/agents.py b/src/agentex/resources/agents.py index ae6821e76..9a3c7ad62 100644 --- a/src/agentex/resources/agents.py +++ b/src/agentex/resources/agents.py @@ -3,7 +3,7 @@ from __future__ import annotations import json -from typing import Union, Optional, Generator, AsyncGenerator +from typing import Any, Union, Optional, Generator, AsyncGenerator from typing_extensions import Literal import httpx @@ -453,38 +453,73 @@ def send_message( ) -> SendMessageResponse: if agent_id is not None and agent_name is not None: raise ValueError("Either agent_id or agent_name must be provided, but not both") - + if "stream" in params and params["stream"] == True: raise ValueError("If stream is set to True, use send_message_stream() instead") + + if agent_id is not None: + streaming_response = self.with_streaming_response.rpc( + agent_id=agent_id, + method="message/send", + params=params, + id=id, + jsonrpc=jsonrpc, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + elif agent_name is not None: + streaming_response = self.with_streaming_response.rpc_by_name( + agent_name=agent_name, + method="message/send", + params=params, + id=id, + jsonrpc=jsonrpc, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) else: - if agent_id is not None: - raw_agent_rpc_response = self.rpc( - agent_id=agent_id, - method="message/send", - params=params, - id=id, - jsonrpc=jsonrpc, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - elif agent_name is not None: - raw_agent_rpc_response = self.rpc_by_name( - agent_name=agent_name, - method="message/send", - params=params, - id=id, - jsonrpc=jsonrpc, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - else: - raise ValueError("Either agent_id or agent_name must be provided") - - return SendMessageResponse.model_validate(raw_agent_rpc_response, from_attributes=True) + raise ValueError("Either agent_id or agent_name must be provided") + + task_messages: list[Any] = [] + response_meta: dict[str, Any] = {} + + with streaming_response as response: + for _line in response.iter_lines(): + if not _line: + continue + line = _line.strip() + if line.startswith("data:"): + line = line[len("data:"):].strip() + if not line: + continue + try: + chunk = json.loads(line) + if not response_meta: + response_meta = {"id": chunk.get("id"), "jsonrpc": chunk.get("jsonrpc")} + # If the server already aggregated into a list result, return directly + try: + return SendMessageResponse.model_validate(chunk) + except ValidationError: + pass + # Parse as a streaming event and collect parent_task_message + chunk_stream = SendMessageStreamResponse.model_validate(chunk, from_attributes=True) + result = chunk_stream.result + if result is not None and hasattr(result, 'type') and result.type == 'full': + parent = getattr(result, 'parent_task_message', None) + if parent is not None: + task_messages.append(parent) + except (json.JSONDecodeError, ValidationError): + continue + + return SendMessageResponse( + id=response_meta.get("id"), + jsonrpc=response_meta.get("jsonrpc"), + result=task_messages, + ) def send_message_stream( self, @@ -552,10 +587,10 @@ def send_message_stream( from_attributes=True ) yield chunk_rpc_response - except json.JSONDecodeError: - # Skip invalid JSON lines + except (json.JSONDecodeError, ValidationError): + # Skip invalid JSON lines or lines that can't be validated continue - + def send_event( self, agent_id: str | None = None, @@ -1021,38 +1056,73 @@ async def send_message( ) -> SendMessageResponse: if agent_id is not None and agent_name is not None: raise ValueError("Either agent_id or agent_name must be provided, but not both") - + if "stream" in params and params["stream"] == True: raise ValueError("If stream is set to True, use send_message_stream() instead") + + if agent_id is not None: + streaming_response = self.with_streaming_response.rpc( + agent_id=agent_id, + method="message/send", + params=params, + id=id, + jsonrpc=jsonrpc, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + elif agent_name is not None: + streaming_response = self.with_streaming_response.rpc_by_name( + agent_name=agent_name, + method="message/send", + params=params, + id=id, + jsonrpc=jsonrpc, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) else: - if agent_id is not None: - raw_agent_rpc_response = await self.rpc( - agent_id=agent_id, - method="message/send", - params=params, - id=id, - jsonrpc=jsonrpc, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - elif agent_name is not None: - raw_agent_rpc_response = await self.rpc_by_name( - agent_name=agent_name, - method="message/send", - params=params, - id=id, - jsonrpc=jsonrpc, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - else: - raise ValueError("Either agent_id or agent_name must be provided") - - return SendMessageResponse.model_validate(raw_agent_rpc_response, from_attributes=True) + raise ValueError("Either agent_id or agent_name must be provided") + + task_messages: list[Any] = [] + response_meta: dict[str, Any] = {} + + async with streaming_response as response: + async for _line in response.iter_lines(): + if not _line: + continue + line = _line.strip() + if line.startswith("data:"): + line = line[len("data:"):].strip() + if not line: + continue + try: + chunk = json.loads(line) + if not response_meta: + response_meta = {"id": chunk.get("id"), "jsonrpc": chunk.get("jsonrpc")} + # If the server already aggregated into a list result, return directly + try: + return SendMessageResponse.model_validate(chunk) + except ValidationError: + pass + # Parse as a streaming event and collect parent_task_message + chunk_stream = SendMessageStreamResponse.model_validate(chunk, from_attributes=True) + result = chunk_stream.result + if result is not None and hasattr(result, 'type') and result.type == 'full': + parent = getattr(result, 'parent_task_message', None) + if parent is not None: + task_messages.append(parent) + except (json.JSONDecodeError, ValidationError): + continue + + return SendMessageResponse( + id=response_meta.get("id"), + jsonrpc=response_meta.get("jsonrpc"), + result=task_messages, + ) async def send_message_stream( self, diff --git a/src/agentex/types/agent_rpc_response.py b/src/agentex/types/agent_rpc_response.py index 84fbab706..31fe5ec3b 100644 --- a/src/agentex/types/agent_rpc_response.py +++ b/src/agentex/types/agent_rpc_response.py @@ -40,7 +40,7 @@ class SendMessageResponse(BaseAgentRpcResponse): """The result of the message sending""" class SendMessageStreamResponse(BaseAgentRpcResponse): - result: TaskMessageUpdate + result: Optional[TaskMessageUpdate] = None """The result of the message sending"""