Skip to content

Commit 8c15553

Browse files
committed
fix: 优化外部资源生命周期和 MCP streamable-http 兼容
- Runner 支持通过 close_session_service_on_close / close_memory_service_on_close 控制是否在 close 时关闭外部托管的 session_service 和 memory_service。 - MCP Streamable HTTP client 优先使用新的 streamable_http_client API,并在旧版 MCP SDK 下回退到 streamablehttp_client。 - 使用 httpx.AsyncClient 承载 streamable-http 的 headers / timeout 配置,避免调用已废弃接口。 - 补充 Runner 和 MCP session manager 相关测试。
1 parent 614b07d commit 8c15553

8 files changed

Lines changed: 127 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,43 @@
11
# Changelog
22

3+
## [1.1.6](https://github.com/trpc-group/trpc-agent-python/releases/tag/v1.1.6) (2026-06-03)
4+
5+
### Features
6+
7+
* Skill: Added a recoverable Cube sandbox runtime for skills, including `CubeClientConfig`, a unified `create_cube_sandbox_client` entry point, optional `auto_recover` support in `CubeSandboxClient`, sandbox lifecycle helpers, and direct `CubeWorkspaceRuntime` creation from the client.
8+
* Skill: Unified skill load/run/exec/stager paths around repository-level workspace runtime resolution via `repository.get_workspace_runtime(ctx)`, so tools under the same skill repository share one workspace runtime context.
9+
* MCP: Added MCP tool caching to avoid repeated network access.
10+
* Tools: Added `GraphAgent` support in `AgentTool`, allowing wrapped graph agents to return results from tool context state.
11+
* Examples/Eval: Restored evaluation examples that were previously removed during open-source cleanup.
12+
* Optimizer: Added support for the prompt self-optimization `AgentOptimizer`.
13+
14+
### Bug Fixes
15+
16+
* Storage: Fixed frequent sqlite warnings in `SqlSessionService` by consistently using database-side `func.now()` for update timestamps.
17+
18+
## [1.1.5](https://github.com/trpc-group/trpc-agent-python/releases/tag/v1.1.5) (2026-05-19)
19+
20+
### Features
21+
22+
* Tools: Added `StreamingProgressTool` with matching `ToolsProcessor` plumbing so tools can surface intermediate progress as `partial=True` events while still emitting a single final `function_response`; included `BaseTool` streaming hooks, the `llmagent_with_streaming_progress_tool` example and verification script.
23+
* Eval: Added `RemoteEvalService` to drive evaluations against agents exposed over remote interfaces, refactored `AgentEvaluator` to support remote agent calls, and expanded English/Chinese evaluation docs.
24+
* Model: Landed the OpenAI-compatible adapter layer (`models/openai_adapter/{_base,_deepseek,_hunyuan}.py`) that isolates provider-specific behavior from `OpenAIModel`, including DeepSeek v4 thinking / `response_format` / `reasoning_content` / token usage handling and hy3-preview ToolPrompt text parsing with streaming filter.
25+
* Examples: Added `examples/mempalace_mcp` (MemPalace via MCP) and updated `examples/llmagent_with_thinking` to enable `add_tools_to_prompt` only for hy3-preview and display thinking / tool calls / final answer separately.
26+
27+
* Utils: Added `json_loads_repair` and `json_repair_string` helpers (backed by `json_repair`) under `trpc_agent_sdk.utils`, with full unit test coverage.
28+
* Model/Tools: Adopted `json_repair` only on JSON-tolerant paths — `JsonToolPrompt` / `XmlToolPrompt` parse_function, non-streaming OpenAI tool-call args, `AgentTool` structured-output validation, skills tool result parsing — while keeping strict `json.loads` for the streaming tool-call accumulator (to preserve "wait for next chunk" semantics) and Hunyuan plain-text `<arg_value>` parsing (to avoid silently coercing plain text into empty strings).
29+
* Model: Fixed ToolPrompt streaming parsing so multiple tool calls in a single response are all preserved instead of only the last one being kept.
30+
31+
### Bug Fixes
32+
33+
* Teams: TeamAgent now honors `actions.skip_summarization` from custom tool events, so tools like `AgentTool(skip_summarization=True)` and `StreamingProgressTool(skip_summarization=True)` end the leader loop without an extra summarization turn (previously masked by leader's `disable_react_tool=True`).
34+
35+
## [1.1.4](https://github.com/trpc-group/trpc-agent-python/releases/tag/v1.1.4) (2026-05-13)
36+
37+
### Bug Fixes
38+
39+
* Tools: Removed default `mempalace_tool` exports from `trpc_agent_sdk.tools` to avoid forcing MemPalace optional dependencies during base package import.
40+
341
## [1.1.3](https://github.com/trpc-group/trpc-agent-python/releases/tag/v1.1.3) (2026-05-12)
442

543
### Features

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ classifiers = [
2626
dependencies = [
2727
"pydantic>=2.11.3",
2828
"openai>=1.3.0",
29-
"mcp<1.23.4,>=1.10.1",
29+
"mcp>=1.10.1",
3030
"aiohttp",
3131
"httpx>=0.27.0",
3232
"httpx-sse>=0.4.0",

tests/test_runner.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ def test_init_with_required_params(self, mock_agent, mock_session_service):
107107
assert runner.session_service == mock_session_service
108108
assert runner.artifact_service is None
109109
assert runner.memory_service is None
110+
assert runner._close_session_service_on_close is True
111+
assert runner._close_memory_service_on_close is True
110112

111113
def test_init_with_all_params(self, mock_agent, mock_session_service):
112114
"""Test Runner initialization with all parameters."""
@@ -119,10 +121,14 @@ def test_init_with_all_params(self, mock_agent, mock_session_service):
119121
session_service=mock_session_service,
120122
artifact_service=artifact_service,
121123
memory_service=memory_service,
124+
close_session_service_on_close=False,
125+
close_memory_service_on_close=False,
122126
)
123127

124128
assert runner.artifact_service == artifact_service
125129
assert runner.memory_service == memory_service
130+
assert runner._close_session_service_on_close is False
131+
assert runner._close_memory_service_on_close is False
126132

127133

128134
# Tests for run_async method
@@ -618,6 +624,25 @@ async def test_close_calls_all_services(self, runner, mock_session_service):
618624
mock_session_service.close.assert_called_once()
619625
memory_service.close.assert_called_once()
620626

627+
@pytest.mark.asyncio
628+
async def test_close_can_keep_external_services_open(self, mock_agent, mock_session_service):
629+
"""Test close skips externally managed session and memory services."""
630+
memory_service = AsyncMock(spec=BaseMemoryService)
631+
runner = Runner(
632+
app_name="test_app",
633+
agent=mock_agent,
634+
session_service=mock_session_service,
635+
memory_service=memory_service,
636+
close_session_service_on_close=False,
637+
close_memory_service_on_close=False,
638+
)
639+
640+
with patch.object(runner, '_collect_toolset', return_value=set()):
641+
await runner.close()
642+
643+
mock_session_service.close.assert_not_called()
644+
memory_service.close.assert_not_called()
645+
621646

622647
# Tests for session history handling
623648
class TestSessionHistory:

tests/test_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@
1010

1111
def test_version():
1212
"""Test the version module."""
13-
assert __version__ == '1.1.3'
13+
assert __version__ == '1.1.6'

tests/tools/mcp_tool/test_mcp_session_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,11 @@ def test_sse_client(self, mock_sse):
233233
call_kwargs = mock_sse.call_args
234234
assert call_kwargs.kwargs["headers"] == {"Auth": "token"}
235235

236-
@patch("trpc_agent_sdk.tools.mcp_tool._mcp_session_manager.streamablehttp_client")
236+
@patch.object(MCPSessionManager, "_create_streamable_http_client")
237237
def test_streamable_client(self, mock_streamable):
238238
mgr = MCPSessionManager(connection_params=_streamable_conn())
239239
mgr._create_client({"Auth": "token"})
240-
mock_streamable.assert_called_once()
240+
mock_streamable.assert_called_once_with({"Auth": "token"})
241241

242242
def test_unsupported_params_raises(self):
243243
mgr = MCPSessionManager(connection_params=_stdio_conn())

trpc_agent_sdk/runners.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ def __init__(
194194
enable_post_turn_processing: bool = True,
195195
defer_post_turn_processing: bool = False,
196196
post_turn_queue_maxsize: int = 256,
197+
close_session_service_on_close: bool = True,
198+
close_memory_service_on_close: bool = True,
197199
):
198200
"""Initializes the Runner.
199201
@@ -209,6 +211,13 @@ def __init__(
209211
persistence run in a dedicated thread/event loop so request
210212
completion is not blocked by post-turn I/O/LLM latency.
211213
post_turn_queue_maxsize: Max buffered post-turn jobs per runner.
214+
close_session_service_on_close: Whether :meth:`close` should close
215+
the session service. Set to False when the service is managed
216+
outside the runner, e.g. an application-level Redis connection
217+
pool shared by many short-lived runners.
218+
close_memory_service_on_close: Whether :meth:`close` should close
219+
the memory service. Set to False when the service is managed
220+
outside the runner.
212221
"""
213222
self.app_name = app_name
214223
self.agent = agent
@@ -219,6 +228,8 @@ def __init__(
219228
self._defer_post_turn_processing = defer_post_turn_processing
220229
self._post_turn_thread: _PostTurnWorkerThread | None = None
221230
self._post_turn_queue_maxsize = max(1, int(post_turn_queue_maxsize))
231+
self._close_session_service_on_close = close_session_service_on_close
232+
self._close_memory_service_on_close = close_memory_service_on_close
222233

223234
async def _run_post_turn_processing(
224235
self,
@@ -823,7 +834,7 @@ async def close(self):
823834
"""
824835
await self._shutdown_post_turn_worker()
825836
await self._cleanup_toolsets(self._collect_toolset(self.agent))
826-
if self.session_service:
837+
if self.session_service and self._close_session_service_on_close:
827838
await self.session_service.close()
828-
if self.memory_service:
839+
if self.memory_service and self._close_memory_service_on_close:
829840
await self.memory_service.close()

trpc_agent_sdk/tools/mcp_tool/_mcp_session_manager.py

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,28 @@
2626

2727
import asyncio
2828
import hashlib
29+
import inspect
2930
import json
3031
import sys
3132
from contextlib import AsyncExitStack
33+
from contextlib import asynccontextmanager
3234
from datetime import timedelta
3335
from typing import Dict
3436
from typing import Optional
3537
from typing import Union
3638

39+
# cSpell:ignore streamable
40+
41+
import httpx
3742
from mcp import ClientSession
3843
from mcp.client.sse import sse_client
3944
from mcp.client.stdio import stdio_client
40-
from mcp.client.streamable_http import streamablehttp_client
45+
try:
46+
from mcp.client.streamable_http import streamable_http_client
47+
except ImportError: # pragma: no cover - compatibility with older mcp versions
48+
from mcp.client.streamable_http import streamablehttp_client as streamable_http_client
49+
50+
_STREAMABLE_HTTP_CLIENT_SUPPORTS_HTTP_CLIENT = "http_client" in inspect.signature(streamable_http_client).parameters
4151

4252
from trpc_agent_sdk.log import logger
4353

@@ -161,7 +171,7 @@ def _create_client(self, merged_headers: Optional[Dict[str, str]] = None):
161171
if isinstance(self._connection_params, StdioConnectionParams):
162172
client = stdio_client(
163173
server=self._connection_params.server_params,
164-
errlog=sys.stderr,
174+
errlog=sys.stderr, # cSpell:ignore errlog
165175
)
166176
elif isinstance(self._connection_params, SseConnectionParams):
167177
client = sse_client(
@@ -171,18 +181,46 @@ def _create_client(self, merged_headers: Optional[Dict[str, str]] = None):
171181
sse_read_timeout=self._connection_params.sse_read_timeout,
172182
)
173183
elif isinstance(self._connection_params, StreamableHTTPConnectionParams):
174-
client = streamablehttp_client(
184+
client = self._create_streamable_http_client(merged_headers)
185+
else:
186+
raise ValueError('Unable to initialize connection. Connection should be'
187+
' StdioConnectionParams or SseConnectionParams or StreamableHTTPConnectionParams, but got'
188+
f' {type(self._connection_params)}')
189+
return client
190+
191+
def _create_streamable_http_client(self, merged_headers: Optional[Dict[str, str]] = None):
192+
"""Creates a Streamable HTTP client with compatibility for MCP SDK versions."""
193+
if not _STREAMABLE_HTTP_CLIENT_SUPPORTS_HTTP_CLIENT:
194+
return streamable_http_client(
175195
url=self._connection_params.url,
176196
headers=merged_headers,
177197
timeout=self._connection_params.timeout,
178198
sse_read_timeout=self._connection_params.sse_read_timeout,
179199
terminate_on_close=self._connection_params.terminate_on_close,
180200
)
181-
else:
182-
raise ValueError('Unable to initialize connection. Connection should be'
183-
' StdioConnectionParams or SseConnectionParams or StreamableHTTPConnectionParams, but got'
184-
f' {type(self._connection_params)}')
185-
return client
201+
202+
return self._create_streamable_http_client_with_httpx(merged_headers)
203+
204+
@asynccontextmanager
205+
async def _create_streamable_http_client_with_httpx(self, merged_headers: Optional[Dict[str, str]] = None):
206+
"""Creates a new-style Streamable HTTP client and owns its httpx client."""
207+
timeout_seconds = self._connection_params.timeout
208+
if isinstance(timeout_seconds, timedelta):
209+
timeout_seconds = timeout_seconds.total_seconds()
210+
sse_read_timeout_seconds = self._connection_params.sse_read_timeout
211+
if isinstance(sse_read_timeout_seconds, timedelta):
212+
sse_read_timeout_seconds = sse_read_timeout_seconds.total_seconds()
213+
214+
async with httpx.AsyncClient(
215+
headers=merged_headers,
216+
timeout=httpx.Timeout(timeout_seconds, read=sse_read_timeout_seconds),
217+
) as http_client:
218+
async with streamable_http_client(
219+
url=self._connection_params.url,
220+
http_client=http_client,
221+
terminate_on_close=self._connection_params.terminate_on_close,
222+
) as transports:
223+
yield transports
186224

187225
async def create_session(self, headers: Optional[Dict[str, str]] = None) -> ClientSession | None:
188226
"""Creates and initializes an MCP client session.

trpc_agent_sdk/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99
This module defines the version information for TRPC Agent
1010
"""
1111

12-
__version__ = '1.1.3'
12+
__version__ = '1.1.6'

0 commit comments

Comments
 (0)