Skip to content

Commit 7204b59

Browse files
committed
Workaround for the MCP server connection cleanup issue
1 parent 934f3e8 commit 7204b59

4 files changed

Lines changed: 585 additions & 0 deletions

File tree

lightspeed_stack_providers/providers/inline/agents/lightspeed_inline_agent/agents.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
)
3232

3333
from .config import LightspeedAgentsImplConfig
34+
from .responses.openai_responses import LightspeedOpenAIResponsesImpl
3435

3536
logger = get_logger(name=__name__, category="agents")
3637

@@ -63,6 +64,31 @@ def __init__(
6364
)
6465
self.config = config
6566

67+
async def initialize(self) -> None:
68+
"""
69+
Initialize the agent implementation.
70+
71+
This method uses LightspeedOpenAIResponsesImpl as part of the workaround for MCP server
72+
connection cleanup issue. The issue is resolved in Llama Stack by
73+
https://github.com/llamastack/llama-stack/pull/4758, but we need to use Llama Stack 0.4.3
74+
and have backported the PR fix.
75+
76+
TODO: Remove this workaround once we upgrade to a Llama Stack version that includes PR #4758.
77+
"""
78+
await super().initialize()
79+
self.openai_responses_impl = LightspeedOpenAIResponsesImpl(
80+
inference_api=self.inference_api,
81+
tool_groups_api=self.tool_groups_api,
82+
tool_runtime_api=self.tool_runtime_api,
83+
responses_store=self.responses_store,
84+
vector_io_api=self.vector_io_api,
85+
safety_api=self.safety_api,
86+
conversations_api=self.conversations_api,
87+
prompts_api=self.prompts_api,
88+
files_api=self.files_api,
89+
vector_stores_config=self.config.vector_stores_config,
90+
)
91+
6692
async def create_openai_response(
6793
self,
6894
input: str | list[OpenAIResponseInput],

lightspeed_stack_providers/providers/inline/agents/lightspeed_inline_agent/responses/__init__.py

Whitespace-only changes.
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
"""
2+
Workaround for MCP server connection cleanup issue.
3+
4+
This file provides a custom implementation to handle MCP session cleanup properly.
5+
The issue is resolved in Llama Stack by https://github.com/llamastack/llama-stack/pull/4758,
6+
but we need to use Llama Stack 0.4.3 and have backported the PR fix to this code.
7+
8+
TODO: Remove this workaround once we upgrade to a Llama Stack version that includes PR #4758.
9+
"""
10+
import time
11+
import uuid
12+
from typing import AsyncIterator
13+
14+
from llama_stack.log import get_logger
15+
from llama_stack.providers.inline.agents.meta_reference.responses.openai_responses import (
16+
OpenAIResponsesImpl,
17+
)
18+
from llama_stack.providers.inline.agents.meta_reference.responses.streaming import (
19+
StreamingResponseOrchestrator,
20+
)
21+
from llama_stack.providers.inline.agents.meta_reference.responses.tool_executor import ToolExecutor
22+
from llama_stack.providers.inline.agents.meta_reference.responses.types import ChatCompletionContext
23+
from llama_stack.providers.inline.agents.meta_reference.responses.utils import (
24+
convert_response_text_to_chat_response_format,
25+
)
26+
from llama_stack.providers.utils.tools.mcp import MCPSessionManager
27+
from llama_stack_api import (
28+
ConversationItem,
29+
OpenAIResponseInput,
30+
OpenAIResponseInputTool,
31+
OpenAIResponseInputToolChoice,
32+
OpenAIResponseObjectStream,
33+
OpenAIResponsePrompt,
34+
OpenAIResponseText,
35+
OpenAISystemMessageParam,
36+
)
37+
from llama_stack_api.agents import ResponseItemInclude
38+
39+
logger = get_logger(name=__name__, category="agents")
40+
41+
42+
class MyMCPSessionManager(MCPSessionManager):
43+
async def __aenter__(self):
44+
"""Enter the async context manager."""
45+
return self
46+
47+
async def __aexit__(self, exc_type, exc_val, exc_tb):
48+
"""Exit the async context manager and cleanup all sessions."""
49+
await super().close_all()
50+
return False
51+
52+
53+
class LightspeedOpenAIResponsesImpl(OpenAIResponsesImpl):
54+
async def _create_streaming_response(
55+
self,
56+
input: str | list[OpenAIResponseInput],
57+
model: str,
58+
instructions: str | None = None,
59+
previous_response_id: str | None = None,
60+
conversation: str | None = None,
61+
prompt: OpenAIResponsePrompt | None = None,
62+
store: bool | None = True,
63+
temperature: float | None = None,
64+
text: OpenAIResponseText | None = None,
65+
tools: list[OpenAIResponseInputTool] | None = None,
66+
tool_choice: OpenAIResponseInputToolChoice | None = None,
67+
max_infer_iters: int | None = 10,
68+
guardrail_ids: list[str] | None = None,
69+
parallel_tool_calls: bool | None = True,
70+
max_tool_calls: int | None = None,
71+
metadata: dict[str, str] | None = None,
72+
include: list[ResponseItemInclude] | None = None,
73+
) -> AsyncIterator[OpenAIResponseObjectStream]:
74+
logger.info("LightspeedOpenAIResponsesImpl._create_streaming_response")
75+
# These should never be None when called from create_openai_response (which sets defaults)
76+
# but we assert here to help mypy understand the types
77+
assert text is not None, "text must not be None"
78+
assert max_infer_iters is not None, "max_infer_iters must not be None"
79+
80+
# Input preprocessing
81+
all_input, messages, tool_context = await self._process_input_with_previous_response(
82+
input, tools, previous_response_id, conversation
83+
)
84+
85+
if instructions:
86+
messages.insert(0, OpenAISystemMessageParam(content=instructions))
87+
88+
# Prepend reusable prompt (if provided)
89+
await self._prepend_prompt(messages, prompt)
90+
91+
# Structured outputs
92+
response_format = await convert_response_text_to_chat_response_format(text)
93+
94+
ctx = ChatCompletionContext(
95+
model=model,
96+
messages=messages,
97+
response_tools=tools,
98+
tool_choice=tool_choice,
99+
temperature=temperature,
100+
response_format=response_format,
101+
tool_context=tool_context,
102+
inputs=all_input,
103+
)
104+
105+
# Create orchestrator and delegate streaming logic
106+
response_id = f"resp_{uuid.uuid4()}"
107+
created_at = int(time.time())
108+
109+
# Create a per-request MCP session manager for session reuse (fix for #4452)
110+
# This avoids redundant tools/list calls when making multiple MCP tool invocations
111+
async with MyMCPSessionManager() as mcp_session_manager:
112+
113+
# Create a per-request ToolExecutor with the session manager
114+
request_tool_executor = ToolExecutor(
115+
tool_groups_api=self.tool_groups_api,
116+
tool_runtime_api=self.tool_runtime_api,
117+
vector_io_api=self.vector_io_api,
118+
vector_stores_config=self.tool_executor.vector_stores_config,
119+
mcp_session_manager=mcp_session_manager,
120+
)
121+
122+
orchestrator = StreamingResponseOrchestrator(
123+
inference_api=self.inference_api,
124+
ctx=ctx,
125+
response_id=response_id,
126+
created_at=created_at,
127+
prompt=prompt,
128+
text=text,
129+
max_infer_iters=max_infer_iters,
130+
parallel_tool_calls=parallel_tool_calls,
131+
tool_executor=request_tool_executor,
132+
safety_api=self.safety_api,
133+
guardrail_ids=guardrail_ids,
134+
instructions=instructions,
135+
max_tool_calls=max_tool_calls,
136+
metadata=metadata,
137+
include=include,
138+
)
139+
140+
# Stream the response
141+
final_response = None
142+
failed_response = None
143+
144+
# Type as ConversationItem to avoid list invariance issues
145+
output_items: list[ConversationItem] = []
146+
async for stream_chunk in orchestrator.create_response():
147+
match stream_chunk.type:
148+
case "response.completed" | "response.incomplete":
149+
final_response = stream_chunk.response
150+
case "response.failed":
151+
failed_response = stream_chunk.response
152+
case "response.output_item.done":
153+
item = stream_chunk.item
154+
output_items.append(item)
155+
case _:
156+
pass # Other event types
157+
158+
# Store and sync before yielding terminal events
159+
# This ensures the storage/syncing happens even if the consumer breaks after
160+
# receiving the event
161+
if (
162+
stream_chunk.type in {"response.completed", "response.incomplete"}
163+
and final_response
164+
and failed_response is None
165+
):
166+
messages_to_store = list(
167+
filter(
168+
lambda x: not isinstance(x, OpenAISystemMessageParam),
169+
orchestrator.final_messages,
170+
)
171+
)
172+
if store:
173+
# TODO: we really should work off of output_items instead of
174+
# "final_messages"
175+
await self._store_response(
176+
response=final_response,
177+
input=all_input,
178+
messages=messages_to_store,
179+
)
180+
181+
if conversation:
182+
await self._sync_response_to_conversation(conversation, input, output_items)
183+
await self.responses_store.store_conversation_messages(
184+
conversation, messages_to_store
185+
)
186+
187+
yield stream_chunk

0 commit comments

Comments
 (0)