A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 7, 19:00 UTC
[Responses] Decouple SSE event helpers from Harmony context (#35148)
Signed-off-by: sfeng33 <4florafeng@gmail.com>
Flora Feng committed
Feb 24, 2026 at 23:05 UTC
ec1d30c0f6fd80f5fcf20f5053e645d9347b313e
5 files changed
+527
-565
tests/entrypoints/openai/responses/conftest.py
+182
-20
@@ -39,6 +39,7 @@ def pairs_of_event_types() -> dict[str, str]:
39
"response.mcp_call.completed": "response.mcp_call.in_progress",
40
"response.function_call_arguments.done": "response.function_call_arguments.delta", # noqa: E501
41
"response.code_interpreter_call_code.done": "response.code_interpreter_call_code.delta", # noqa: E501
42
+ "response.code_interpreter_call.completed": "response.code_interpreter_call.in_progress", # noqa: E501
43
"response.web_search_call.completed": "response.web_search_call.in_progress",
44
}
45
# fmt: on
@@ -108,29 +109,19 @@ def events_contain_type(events: list, type_substring: str) -> bool:
109
return any(type_substring in getattr(e, "type", "") for e in events)
110
111
111
-def validate_streaming_event_stack(
112
- events: list, pairs_of_event_types: dict[str, str]
113
-) -> None:
114
- """Validate that streaming events are properly nested/paired."""
112
+def _validate_event_pairing(events: list, pairs_of_event_types: dict[str, str]) -> None:
113
+ """Validate that streaming events are properly nested/paired.
114
+
115
+ Derives push/pop sets from *pairs_of_event_types* so that every
116
+ start/end pair in the dict is handled automatically.
117
+ """
118
+ start_events = set(pairs_of_event_types.values())
119
+ end_events = set(pairs_of_event_types.keys())
120
+
121
stack: list[str] = []
122
for event in events:
123
etype = event.type
118
- if etype == "response.created":
119
- stack.append(etype)
120
- elif etype == "response.completed":
121
- assert stack and stack[-1] == pairs_of_event_types[etype], (
122
- f"Unexpected stack top for {etype}: "
123
- f"got {stack[-1] if stack else '<empty>'}"
124
- )
125
- stack.pop()
126
- elif etype.endswith("added") or etype == "response.mcp_call.in_progress":
127
- stack.append(etype)
128
- elif etype.endswith("delta"):
129
- if stack and stack[-1] == etype:
130
- continue
131
- stack.append(etype)
132
- elif etype.endswith("done") or etype == "response.mcp_call.completed":
133
- assert etype in pairs_of_event_types, f"Unknown done event: {etype}"
124
+ if etype in end_events:
125
expected_start = pairs_of_event_types[etype]
126
assert stack and stack[-1] == expected_start, (
127
f"Stack mismatch for {etype}: "
@@ -138,9 +129,180 @@ def validate_streaming_event_stack(
129
f"got {stack[-1] if stack else '<empty>'}"
130
)
131
stack.pop()
132
+ elif etype in start_events:
133
+ # Consecutive deltas of the same type share a single stack slot.
134
+ if etype.endswith("delta") and stack and stack[-1] == etype:
135
+ continue
136
+ stack.append(etype)
137
+ # else: passthrough event (e.g. response.in_progress,
138
+ # web_search_call.searching, code_interpreter_call.interpreting)
139
assert len(stack) == 0, f"Unclosed events on stack: {stack}"
140
141
142
+def _validate_event_ordering(events: list) -> None:
143
+ """Validate that envelope events appear in the correct positions."""
144
+ assert len(events) >= 2, f"Expected at least 2 events, got {len(events)}"
145
+
146
+ # First event must be response.created
147
+ assert events[0].type == "response.created", (
148
+ f"First event must be response.created, got {events[0].type}"
149
+ )
150
+ # Last event must be response.completed
151
+ assert events[-1].type == "response.completed", (
152
+ f"Last event must be response.completed, got {events[-1].type}"
153
+ )
154
+
155
+ # response.in_progress, if present, must be the second event
156
+ in_progress_indices = [
157
+ i for i, e in enumerate(events) if e.type == "response.in_progress"
158
+ ]
159
+ if in_progress_indices:
160
+ assert in_progress_indices == [1], (
161
+ f"response.in_progress must be the second event, "
162
+ f"found at indices {in_progress_indices}"
163
+ )
164
+
165
+ # Exactly one created and one completed
166
+ created_count = sum(1 for e in events if e.type == "response.created")
167
+ completed_count = sum(1 for e in events if e.type == "response.completed")
168
+ assert created_count == 1, (
169
+ f"Expected exactly 1 response.created, got {created_count}"
170
+ )
171
+ assert completed_count == 1, (
172
+ f"Expected exactly 1 response.completed, got {completed_count}"
173
+ )
174
+
175
+
176
+def _validate_field_consistency(events: list) -> None:
177
+ """Validate item_id, output_index, and content_index consistency.
178
+
179
+ Tracks the active output item established by ``output_item.added``
180
+ and verifies that all subsequent events for that item carry matching
181
+ identifiers until ``output_item.done`` closes it.
182
+ """
183
+ _SESSION_EVENTS = {
184
+ "response.created",
185
+ "response.in_progress",
186
+ "response.completed",
187
+ }
188
+
189
+ active_item_id: str | None = None
190
+ active_output_index: int | None = None
191
+ last_output_index: int = -1
192
+ active_content_index: int | None = None
193
+
194
+ for event in events:
195
+ etype = event.type
196
+
197
+ if etype in _SESSION_EVENTS:
198
+ continue
199
+
200
+ # --- output_item.added: opens a new item ------------------
201
+ if etype == "response.output_item.added":
202
+ item = getattr(event, "item", None)
203
+ output_index = getattr(event, "output_index", None)
204
+
205
+ assert item is not None, "output_item.added must have an item"
206
+ item_id = getattr(item, "id", None)
207
+ assert item_id, "output_item.added item must have an id"
208
+
209
+ # output_index must be non-decreasing across items
210
+ if output_index is not None:
211
+ assert output_index >= last_output_index, (
212
+ f"output_index went backwards: {output_index} < {last_output_index}"
213
+ )
214
+ last_output_index = output_index
215
+
216
+ active_item_id = item_id
217
+ active_output_index = output_index
218
+ active_content_index = None
219
+ continue
220
+
221
+ # --- output_item.done: closes the active item -------------
222
+ if etype == "response.output_item.done":
223
+ item = getattr(event, "item", None)
224
+ output_index = getattr(event, "output_index", None)
225
+
226
+ assert item is not None, "output_item.done must have an item"
227
+ done_item_id = getattr(item, "id", None)
228
+
229
+ if active_item_id is not None and done_item_id:
230
+ assert done_item_id == active_item_id, (
231
+ f"output_item.done item.id mismatch: "
232
+ f"expected {active_item_id}, got {done_item_id}"
233
+ )
234
+ if active_output_index is not None and output_index is not None:
235
+ assert output_index == active_output_index, (
236
+ f"output_item.done output_index mismatch: "
237
+ f"expected {active_output_index}, got {output_index}"
238
+ )
239
+
240
+ active_item_id = None
241
+ active_output_index = None
242
+ active_content_index = None
243
+ continue
244
+
245
+ # --- content_part / reasoning_part added: sets content_index
246
+ if etype in (
247
+ "response.content_part.added",
248
+ "response.reasoning_part.added",
249
+ ):
250
+ _assert_item_fields(event, etype, active_item_id, active_output_index)
251
+ active_content_index = getattr(event, "content_index", None)
252
+ continue
253
+
254
+ # --- all other item-level events --------------------------
255
+ _assert_item_fields(event, etype, active_item_id, active_output_index)
256
+
257
+ # content_index (only meaningful on events that carry it)
258
+ content_index = getattr(event, "content_index", None)
259
+ if content_index is not None and active_content_index is not None:
260
+ assert content_index == active_content_index, (
261
+ f"{etype} content_index mismatch: "
262
+ f"expected {active_content_index}, got {content_index}"
263
+ )
264
+
265
+
266
+def _assert_item_fields(
267
+ event,
268
+ etype: str,
269
+ active_item_id: str | None,
270
+ active_output_index: int | None,
271
+) -> None:
272
+ """Check that *event*'s item_id and output_index match the active item."""
273
+ event_item_id = getattr(event, "item_id", None)
274
+ output_index = getattr(event, "output_index", None)
275
+
276
+ if active_item_id is not None and event_item_id is not None:
277
+ assert event_item_id == active_item_id, (
278
+ f"{etype} item_id mismatch: expected {active_item_id}, got {event_item_id}"
279
+ )
280
+ if active_output_index is not None and output_index is not None:
281
+ assert output_index == active_output_index, (
282
+ f"{etype} output_index mismatch: "
283
+ f"expected {active_output_index}, got {output_index}"
284
+ )
285
+
286
+
287
+def validate_streaming_event_stack(
288
+ events: list, pairs_of_event_types: dict[str, str]
289
+) -> None:
290
+ """Validate streaming events: pairing, ordering, and field consistency.
291
+
292
+ Checks three aspects:
293
+ 1. **Event pairing** — start/end events are properly nested
294
+ (stack-based matching derived from *pairs_of_event_types*).
295
+ 2. **Event ordering** — envelope events (``created``,
296
+ ``in_progress``, ``completed``) appear at the correct positions.
297
+ 3. **Field consistency** — ``item_id``, ``output_index``, and
298
+ ``content_index`` are consistent across related events within
299
+ each output item's lifecycle.
300
+ """
301
+ _validate_event_pairing(events, pairs_of_event_types)
302
+ _validate_event_ordering(events)
303
+ _validate_field_consistency(events)
304
+
305
+
306
def log_response_diagnostics(
307
response,
308
*,
tests/entrypoints/openai/responses/test_harmony.py
+24
-43
@@ -910,21 +910,25 @@ async def test_function_calling_no_code_interpreter_events(
910
reason="This test is flaky in CI, needs investigation and "
911
"potential fixes in the code interpreter MCP implementation."
912
)
913
-async def test_mcp_code_interpreter_streaming(client: OpenAI, model_name: str, server):
914
- tools = [{"type": "mcp", "server_label": "code_interpreter"}]
913
+async def test_code_interpreter_streaming(
914
+ client: OpenAI,
915
+ model_name: str,
916
+ pairs_of_event_types: dict[str, str],
917
+):
918
+ tools = [{"type": "code_interpreter", "container": {"type": "auto"}}]
919
input_text = (
920
"Calculate 123 * 456 using python. "
921
"The python interpreter is not stateful and you must "
922
"print to see the output."
923
)
924
921
- def _has_mcp_call(evts: list) -> bool:
922
- return events_contain_type(evts, "mcp_call")
925
+ def _has_code_interpreter(evts: list) -> bool:
926
+ return events_contain_type(evts, "code_interpreter")
927
928
events = await retry_streaming_for(
929
client,
930
model=model_name,
927
- validate_events=_has_mcp_call,
931
+ validate_events=_has_code_interpreter,
932
input=input_text,
933
tools=tools,
934
temperature=0.0,
@@ -936,59 +940,36 @@ async def test_mcp_code_interpreter_streaming(client: OpenAI, model_name: str, s
940
event_types = [e.type for e in events]
941
event_types_set = set(event_types)
942
logger.info(
939
- "\n====== MCP Streaming Diagnostics ======\n"
943
+ "\n====== Code Interpreter Streaming Diagnostics ======\n"
944
"Event count: %d\n"
945
"Event types (in order): %s\n"
946
"Unique event types: %s\n"
943
- "=======================================",
947
+ "====================================================",
948
len(events),
949
event_types,
950
sorted(event_types_set),
951
)
952
949
- # Verify the full MCP streaming lifecycle
950
- assert "response.output_item.added" in event_types_set, (
951
- f"MCP call was not added. Events: {sorted(event_types_set)}"
952
- )
953
- assert "response.mcp_call.in_progress" in event_types_set, (
954
- f"MCP call in_progress not seen. Events: {sorted(event_types_set)}"
955
- )
956
- assert "response.mcp_call_arguments.delta" in event_types_set, (
957
- f"MCP arguments delta not seen. Events: {sorted(event_types_set)}"
958
- )
959
- assert "response.mcp_call_arguments.done" in event_types_set, (
960
- f"MCP arguments done not seen. Events: {sorted(event_types_set)}"
961
- )
962
- assert "response.mcp_call.completed" in event_types_set, (
963
- f"MCP call completed not seen. Events: {sorted(event_types_set)}"
964
- )
965
- assert "response.output_item.done" in event_types_set, (
966
- f"MCP item done not seen. Events: {sorted(event_types_set)}"
967
- )
953
+ # Structural validation (pairing, ordering, field consistency)
954
+ validate_streaming_event_stack(events, pairs_of_event_types)
955
969
- # Validate specific MCP event details
956
+ # Validate code interpreter item fields
957
for event in events:
971
- if event.type == "response.output_item.added":
972
- if hasattr(event.item, "type") and event.item.type == "mcp_call":
973
- assert event.item.name == "python"
974
- assert event.item.server_label == "code_interpreter"
975
- elif event.type == "response.mcp_call_arguments.done":
976
- assert event.name == "python"
977
- assert event.arguments is not None
958
+ if (
959
+ event.type == "response.output_item.added"
960
+ and hasattr(event.item, "type")
961
+ and event.item.type == "code_interpreter_call"
962
+ ):
963
+ assert event.item.status == "in_progress"
964
+ elif event.type == "response.code_interpreter_call_code.done":
965
+ assert event.code is not None
966
elif (
967
event.type == "response.output_item.done"
968
and hasattr(event.item, "type")
981
- and event.item.type == "mcp_call"
969
+ and event.item.type == "code_interpreter_call"
970
):
983
- assert event.item.name == "python"
971
assert event.item.status == "completed"
985
-
986
- # code_interpreter events should NOT appear when using MCP type
987
- code_interp_events = [e.type for e in events if "code_interpreter" in e.type]
988
- assert not code_interp_events, (
989
- "Should not see code_interpreter events when using MCP type, "
990
- f"but got: {code_interp_events}"
991
- )
972
+ assert event.item.code is not None
973
974
975
@pytest.mark.asyncio
tests/entrypoints/openai/responses/test_mcp_tools.py
-78
@@ -241,81 +241,3 @@ class TestMCPEnabled:
241
)
242
243
validate_streaming_event_stack(events, pairs_of_event_types)
244
-
245
- assert events_contain_type(events, "mcp_call"), (
246
- f"No mcp_call events after retries. "
247
- f"Event types: {sorted({e.type for e in events})}"
248
- )
249
-
250
-
251
-class TestMCPDisabled:
252
- """Tests that MCP tools are not executed when the env flag is unset."""
253
-
254
- @pytest.fixture(scope="class")
255
- def mcp_disabled_server(self):
256
- env_dict = {
257
- **BASE_TEST_ENV,
258
- "VLLM_ENABLE_RESPONSES_API_STORE": "1",
259
- "PYTHON_EXECUTION_BACKEND": "dangerously_use_uv",
260
- "VLLM_GPT_OSS_HARMONY_SYSTEM_INSTRUCTIONS": "1",
261
- }
262
- with RemoteOpenAIServer(
263
- MODEL_NAME, list(_BASE_SERVER_ARGS), env_dict=env_dict
264
- ) as remote_server:
265
- yield remote_server
266
-
267
- @pytest_asyncio.fixture
268
- async def client(self, mcp_disabled_server):
269
- async with mcp_disabled_server.get_async_client() as async_client:
270
- yield async_client
271
-
272
- @pytest.mark.asyncio
273
- @pytest.mark.parametrize("model_name", [MODEL_NAME])
274
- async def test_mcp_disabled_server_does_not_execute(
275
- self, client: OpenAI, model_name: str
276
- ):
277
- """When MCP is disabled the model may still attempt tool calls
278
- (tool descriptions can remain in the prompt), but the server
279
- must NOT execute them."""
280
- response = await client.responses.create(
281
- model=model_name,
282
- input=(
283
- "Execute the following code if the tool is present: "
284
- "import random; print(random.randint(1, 1000000))"
285
- ),
286
- tools=[
287
- {
288
- "type": "mcp",
289
- "server_label": "code_interpreter",
290
- "server_url": "http://localhost:8888",
291
- }
292
- ],
293
- temperature=0.0,
294
- extra_body={"enable_response_messages": True},
295
- )
296
- assert response is not None
297
- assert response.status == "completed"
298
-
299
- log_response_diagnostics(response, label="MCP Disabled")
300
-
301
- # Server must not have executed any tool calls
302
- for message in response.output_messages:
303
- author = message.get("author", {})
304
- assert not (
305
- author.get("role") == "tool"
306
- and (author.get("name") or "").startswith("python")
307
- ), (
308
- "Server executed a python tool call even though MCP is "
309
- f"disabled. Message: {message}"
310
- )
311
-
312
- # No completed mcp_call output items
313
- for item in response.output:
314
- if getattr(item, "type", None) == "mcp_call":
315
- assert getattr(item, "status", None) != "completed", (
316
- "MCP call should not be completed when MCP is disabled"
317
- )
318
-
319
- # No developer messages injected
320
- for message in response.input_messages:
321
- assert message.get("author", {}).get("role") != "developer"
vllm/entrypoints/openai/responses/serving.py
+2
-2
@@ -89,7 +89,7 @@ from vllm.entrypoints.openai.responses.protocol import (
89
StreamingResponsesResponse,
90
)
91
from vllm.entrypoints.openai.responses.streaming_events import (
92
- HarmonyStreamingState,
92
+ StreamingState,
93
emit_content_delta_events,
94
emit_previous_item_done_events,
95
emit_tool_action_events,
@@ -1591,7 +1591,7 @@ class OpenAIServingResponses(OpenAIServing):
1591
[StreamingResponsesResponse], StreamingResponsesResponse
1592
],
1593
) -> AsyncGenerator[StreamingResponsesResponse, None]:
1594
- state = HarmonyStreamingState()
1594
+ state = StreamingState()
1595
1596
async for ctx in result_generator:
1597
assert isinstance(ctx, StreamingHarmonyContext)
vllm/entrypoints/openai/responses/streaming_events.py
+319
-422
@@ -6,6 +6,13 @@ Streaming SSE event builders for the Responses API.
6
Pure functions that translate streaming state + delta data into
7
OpenAI Response API SSE events. Used by the streaming event
8
processors in serving.py.
9
+
10
+The file is organized as:
11
+ 1. StreamingState dataclass + utility helpers
12
+ 2. Shared leaf helpers — delta events (take plain strings, no context)
13
+ 3. Shared leaf helpers — done events (take plain strings, no context)
14
+ 4. Harmony-specific dispatchers (route ctx/previous_item → leaf helpers)
15
+ 5. Harmony-specific tool lifecycle helpers
16
"""
17
18
import json
@@ -47,6 +54,7 @@ from openai.types.responses.response_output_item import McpCall
54
from openai.types.responses.response_reasoning_item import (
55
Content as ResponseReasoningTextContent,
56
)
57
+from openai_harmony import Message as HarmonyMessage
58
59
from vllm.entrypoints.mcp.tool_server import ToolServer
60
from vllm.entrypoints.openai.responses.context import StreamingHarmonyContext
@@ -64,13 +72,28 @@ TOOL_NAME_TO_MCP_SERVER_LABEL: Final[dict[str, str]] = {
72
}
73
74
75
+def _resolve_mcp_name_label(recipient: str) -> tuple[str, str]:
76
+ """Resolve MCP tool name and server label from a recipient string.
77
+
78
+ - ``mcp.*`` recipients: strip prefix, use the bare name as both
79
+ name and server_label.
80
+ - Everything else: use the recipient as the name and look up the
81
+ server_label in TOOL_NAME_TO_MCP_SERVER_LABEL.
82
+ """
83
+ if recipient.startswith("mcp."):
84
+ name = recipient[len("mcp.") :]
85
+ return name, name
86
+ return recipient, TOOL_NAME_TO_MCP_SERVER_LABEL.get(recipient, recipient)
87
+
88
+
89
@dataclass
68
-class HarmonyStreamingState:
69
- """Mutable state for harmony streaming event processing."""
90
+class StreamingState:
91
+ """Mutable state for streaming event processing."""
92
93
current_content_index: int = -1
94
current_output_index: int = 0
95
current_item_id: str = ""
96
+ current_call_id: str = ""
97
sent_output_item_added: bool = False
98
is_first_function_call_delta: bool = False
99
@@ -79,6 +102,7 @@ class HarmonyStreamingState:
102
self.current_output_index += 1
103
self.sent_output_item_added = False
104
self.is_first_function_call_delta = False
105
+ self.current_call_id = ""
106
107
108
def is_mcp_tool_by_namespace(recipient: str | None) -> bool:
@@ -96,213 +120,16 @@ def is_mcp_tool_by_namespace(recipient: str | None) -> bool:
120
return not recipient.startswith("functions.")
121
122
99
-def emit_function_call_done_events(
100
- previous_item,
101
- state: HarmonyStreamingState,
102
-) -> list[StreamingResponsesResponse]:
103
- """Emit events when a function call completes."""
104
- function_name = previous_item.recipient[len("functions.") :]
105
- events: list[StreamingResponsesResponse] = []
106
- events.append(
107
- ResponseFunctionCallArgumentsDoneEvent(
108
- type="response.function_call_arguments.done",
109
- arguments=previous_item.content[0].text,
110
- name=function_name,
111
- item_id=state.current_item_id,
112
- output_index=state.current_output_index,
113
- sequence_number=-1,
114
- )
115
- )
116
- function_call_item = ResponseFunctionToolCall(
117
- type="function_call",
118
- arguments=previous_item.content[0].text,
119
- name=function_name,
120
- item_id=state.current_item_id,
121
- output_index=state.current_output_index,
122
- sequence_number=-1,
123
- call_id=f"fc_{random_uuid()}",
124
- status="completed",
125
- )
126
- events.append(
127
- ResponseOutputItemDoneEvent(
128
- type="response.output_item.done",
129
- sequence_number=-1,
130
- output_index=state.current_output_index,
131
- item=function_call_item,
132
- )
133
- )
134
- return events
135
-
136
-
137
-def emit_mcp_call_done_events(
138
- previous_item,
139
- state: HarmonyStreamingState,
140
-) -> list[StreamingResponsesResponse]:
141
- """Emit events when an MCP tool call completes."""
142
- server_label = TOOL_NAME_TO_MCP_SERVER_LABEL.get(
143
- previous_item.recipient, previous_item.recipient
144
- )
145
- events: list[StreamingResponsesResponse] = []
146
- events.append(
147
- ResponseMcpCallArgumentsDoneEvent(
148
- type="response.mcp_call_arguments.done",
149
- arguments=previous_item.content[0].text,
150
- name=previous_item.recipient,
151
- item_id=state.current_item_id,
152
- output_index=state.current_output_index,
153
- sequence_number=-1,
154
- )
155
- )
156
- events.append(
157
- ResponseMcpCallCompletedEvent(
158
- type="response.mcp_call.completed",
159
- sequence_number=-1,
160
- output_index=state.current_output_index,
161
- item_id=state.current_item_id,
162
- )
163
- )
164
- events.append(
165
- ResponseOutputItemDoneEvent(
166
- type="response.output_item.done",
167
- sequence_number=-1,
168
- output_index=state.current_output_index,
169
- item=McpCall(
170
- type="mcp_call",
171
- arguments=previous_item.content[0].text,
172
- name=previous_item.recipient,
173
- id=state.current_item_id,
174
- server_label=server_label,
175
- status="completed",
176
- ),
177
- )
178
- )
179
- return events
180
-
181
-
182
-def emit_reasoning_done_events(
183
- previous_item,
184
- state: HarmonyStreamingState,
185
-) -> list[StreamingResponsesResponse]:
186
- """Emit events when a reasoning (analysis) item completes."""
187
- content = ResponseReasoningTextContent(
188
- text=previous_item.content[0].text,
189
- type="reasoning_text",
190
- )
191
- reasoning_item = ResponseReasoningItem(
192
- type="reasoning",
193
- content=[content],
194
- status="completed",
195
- id=state.current_item_id,
196
- summary=[],
197
- )
198
- events: list[StreamingResponsesResponse] = []
199
- events.append(
200
- ResponseReasoningTextDoneEvent(
201
- type="response.reasoning_text.done",
202
- item_id=state.current_item_id,
203
- sequence_number=-1,
204
- output_index=state.current_output_index,
205
- content_index=state.current_content_index,
206
- text=previous_item.content[0].text,
207
- )
208
- )
209
- events.append(
210
- ResponseReasoningPartDoneEvent(
211
- type="response.reasoning_part.done",
212
- sequence_number=-1,
213
- item_id=state.current_item_id,
214
- output_index=state.current_output_index,
215
- content_index=state.current_content_index,
216
- part=content,
217
- )
218
- )
219
- events.append(
220
- ResponseOutputItemDoneEvent(
221
- type="response.output_item.done",
222
- sequence_number=-1,
223
- output_index=state.current_output_index,
224
- item=reasoning_item,
225
- )
226
- )
227
- return events
228
-
229
-
230
-def emit_text_output_done_events(
231
- previous_item,
232
- state: HarmonyStreamingState,
233
-) -> list[StreamingResponsesResponse]:
234
- """Emit events when a final text output item completes."""
235
- text_content = ResponseOutputText(
236
- type="output_text",
237
- text=previous_item.content[0].text,
238
- annotations=[],
239
- )
240
- events: list[StreamingResponsesResponse] = []
241
- events.append(
242
- ResponseTextDoneEvent(
243
- type="response.output_text.done",
244
- sequence_number=-1,
245
- output_index=state.current_output_index,
246
- content_index=state.current_content_index,
247
- text=previous_item.content[0].text,
248
- logprobs=[],
249
- item_id=state.current_item_id,
250
- )
251
- )
252
- events.append(
253
- ResponseContentPartDoneEvent(
254
- type="response.content_part.done",
255
- sequence_number=-1,
256
- item_id=state.current_item_id,
257
- output_index=state.current_output_index,
258
- content_index=state.current_content_index,
259
- part=text_content,
260
- )
261
- )
262
- events.append(
263
- ResponseOutputItemDoneEvent(
264
- type="response.output_item.done",
265
- sequence_number=-1,
266
- output_index=state.current_output_index,
267
- item=ResponseOutputMessage(
268
- id=state.current_item_id,
269
- type="message",
270
- role="assistant",
271
- content=[text_content],
272
- status="completed",
273
- ),
274
- )
275
- )
276
- return events
277
-
278
-
279
-def emit_previous_item_done_events(
280
- previous_item,
281
- state: HarmonyStreamingState,
282
-) -> list[StreamingResponsesResponse]:
283
- """Emit done events for the previous item when expecting a new start."""
284
- if previous_item.recipient is not None:
285
- # Deal with tool call
286
- if previous_item.recipient.startswith("functions."):
287
- return emit_function_call_done_events(previous_item, state)
288
- elif (
289
- is_mcp_tool_by_namespace(previous_item.recipient)
290
- and state.current_item_id is not None
291
- and state.current_item_id.startswith("mcp_")
292
- ):
293
- return emit_mcp_call_done_events(previous_item, state)
294
- elif previous_item.channel == "analysis":
295
- return emit_reasoning_done_events(previous_item, state)
296
- elif previous_item.channel == "final":
297
- return emit_text_output_done_events(previous_item, state)
298
- return []
123
+# =====================================================================
124
+# Shared leaf helpers — delta events
125
+# =====================================================================
126
127
301
-def emit_final_channel_delta_events(
302
- ctx: StreamingHarmonyContext,
303
- state: HarmonyStreamingState,
128
+def emit_text_delta_events(
129
+ delta: str,
130
+ state: StreamingState,
131
) -> list[StreamingResponsesResponse]:
305
- """Emit events for final channel text delta streaming."""
132
+ """Emit events for text content delta streaming."""
133
events: list[StreamingResponsesResponse] = []
134
if not state.sent_output_item_added:
135
state.sent_output_item_added = True
@@ -344,7 +171,7 @@ def emit_final_channel_delta_events(
171
content_index=state.current_content_index,
172
output_index=state.current_output_index,
173
item_id=state.current_item_id,
347
- delta=ctx.last_content_delta,
174
+ delta=delta,
175
# TODO, use logprobs from ctx.last_request_output
176
logprobs=[],
177
)
@@ -352,11 +179,11 @@ def emit_final_channel_delta_events(
179
return events
180
181
355
-def emit_analysis_channel_delta_events(
356
- ctx: StreamingHarmonyContext,
357
- state: HarmonyStreamingState,
182
+def emit_reasoning_delta_events(
183
+ delta: str,
184
+ state: StreamingState,
185
) -> list[StreamingResponsesResponse]:
359
- """Emit events for analysis channel reasoning delta streaming."""
186
+ """Emit events for reasoning text delta streaming."""
187
events: list[StreamingResponsesResponse] = []
188
if not state.sent_output_item_added:
189
state.sent_output_item_added = True
@@ -394,20 +221,60 @@ def emit_analysis_channel_delta_events(
221
item_id=state.current_item_id,
222
output_index=state.current_output_index,
223
content_index=state.current_content_index,
397
- delta=ctx.last_content_delta,
224
+ delta=delta,
225
sequence_number=-1,
226
)
227
)
228
return events
229
230
404
-def emit_mcp_tool_delta_events(
405
- ctx: StreamingHarmonyContext,
406
- state: HarmonyStreamingState,
231
+def emit_function_call_delta_events(
232
+ delta: str,
233
+ function_name: str,
234
+ state: StreamingState,
235
+) -> list[StreamingResponsesResponse]:
236
+ """Emit events for function call argument deltas."""
237
+ events: list[StreamingResponsesResponse] = []
238
+ if state.is_first_function_call_delta is False:
239
+ state.is_first_function_call_delta = True
240
+ state.current_item_id = f"fc_{random_uuid()}"
241
+ state.current_call_id = f"call_{random_uuid()}"
242
+ tool_call_item = ResponseFunctionToolCall(
243
+ name=function_name,
244
+ type="function_call",
245
+ id=state.current_item_id,
246
+ call_id=state.current_call_id,
247
+ arguments="",
248
+ status="in_progress",
249
+ )
250
+ events.append(
251
+ ResponseOutputItemAddedEvent(
252
+ type="response.output_item.added",
253
+ sequence_number=-1,
254
+ output_index=state.current_output_index,
255
+ item=tool_call_item,
256
+ )
257
+ )
258
+ # Always emit the delta (including on first call)
259
+ events.append(
260
+ ResponseFunctionCallArgumentsDeltaEvent(
261
+ item_id=state.current_item_id,
262
+ delta=delta,
263
+ output_index=state.current_output_index,
264
+ sequence_number=-1,
265
+ type="response.function_call_arguments.delta",
266
+ )
267
+ )
268
+ return events
269
+
270
+
271
+def emit_mcp_delta_events(
272
+ delta: str,
273
+ state: StreamingState,
274
recipient: str,
275
) -> list[StreamingResponsesResponse]:
276
"""Emit events for MCP tool delta streaming."""
410
- server_label = TOOL_NAME_TO_MCP_SERVER_LABEL.get(recipient, recipient)
277
+ name, server_label = _resolve_mcp_name_label(recipient)
278
events: list[StreamingResponsesResponse] = []
279
if not state.sent_output_item_added:
280
state.sent_output_item_added = True
@@ -420,7 +287,7 @@ def emit_mcp_tool_delta_events(
287
item=McpCall(
288
type="mcp_call",
289
id=state.current_item_id,
423
- name=recipient,
290
+ name=name,
291
arguments="",
292
server_label=server_label,
293
status="in_progress",
@@ -441,15 +308,15 @@ def emit_mcp_tool_delta_events(
308
sequence_number=-1,
309
output_index=state.current_output_index,
310
item_id=state.current_item_id,
444
- delta=ctx.last_content_delta,
311
+ delta=delta,
312
)
313
)
314
return events
315
316
317
def emit_code_interpreter_delta_events(
451
- ctx: StreamingHarmonyContext,
452
- state: HarmonyStreamingState,
318
+ delta: str,
319
+ state: StreamingState,
320
) -> list[StreamingResponsesResponse]:
321
"""Emit events for code interpreter delta streaming."""
322
events: list[StreamingResponsesResponse] = []
@@ -485,151 +352,274 @@ def emit_code_interpreter_delta_events(
352
sequence_number=-1,
353
output_index=state.current_output_index,
354
item_id=state.current_item_id,
488
- delta=ctx.last_content_delta,
355
+ delta=delta,
356
)
357
)
358
return events
359
360
494
-def emit_mcp_prefix_delta_events(
495
- ctx: StreamingHarmonyContext,
496
- state: HarmonyStreamingState,
361
+# =====================================================================
362
+# Shared leaf helpers — done events
363
+# =====================================================================
364
+
365
+
366
+def emit_text_output_done_events(
367
+ text: str,
368
+ state: StreamingState,
369
) -> list[StreamingResponsesResponse]:
498
- """Emit events for MCP prefix (mcp.*) delta streaming."""
370
+ """Emit events when a final text output item completes."""
371
+ text_content = ResponseOutputText(
372
+ type="output_text",
373
+ text=text,
374
+ annotations=[],
375
+ )
376
events: list[StreamingResponsesResponse] = []
500
- if not state.sent_output_item_added:
501
- state.sent_output_item_added = True
502
- state.current_item_id = f"mcp_{random_uuid()}"
503
- mcp_name = ctx.parser.current_recipient[len("mcp.") :]
504
-
505
- events.append(
506
- ResponseOutputItemAddedEvent(
507
- type="response.output_item.added",
508
- sequence_number=-1,
509
- output_index=state.current_output_index,
510
- item=McpCall(
511
- type="mcp_call",
512
- id=state.current_item_id,
513
- name=mcp_name,
514
- arguments="",
515
- server_label=mcp_name,
516
- status="in_progress",
517
- ),
518
- )
377
+ events.append(
378
+ ResponseTextDoneEvent(
379
+ type="response.output_text.done",
380
+ sequence_number=-1,
381
+ output_index=state.current_output_index,
382
+ content_index=state.current_content_index,
383
+ text=text,
384
+ logprobs=[],
385
+ item_id=state.current_item_id,
386
)
520
- events.append(
521
- ResponseMcpCallInProgressEvent(
522
- type="response.mcp_call.in_progress",
523
- sequence_number=-1,
524
- output_index=state.current_output_index,
525
- item_id=state.current_item_id,
526
- )
387
+ )
388
+ events.append(
389
+ ResponseContentPartDoneEvent(
390
+ type="response.content_part.done",
391
+ sequence_number=-1,
392
+ item_id=state.current_item_id,
393
+ output_index=state.current_output_index,
394
+ content_index=state.current_content_index,
395
+ part=text_content,
396
+ )
397
+ )
398
+ events.append(
399
+ ResponseOutputItemDoneEvent(
400
+ type="response.output_item.done",
401
+ sequence_number=-1,
402
+ output_index=state.current_output_index,
403
+ item=ResponseOutputMessage(
404
+ id=state.current_item_id,
405
+ type="message",
406
+ role="assistant",
407
+ content=[text_content],
408
+ status="completed",
409
+ ),
410
)
411
+ )
412
+ return events
413
414
+
415
+def emit_reasoning_done_events(
416
+ text: str,
417
+ state: StreamingState,
418
+) -> list[StreamingResponsesResponse]:
419
+ """Emit events when a reasoning (analysis) item completes."""
420
+ content = ResponseReasoningTextContent(
421
+ text=text,
422
+ type="reasoning_text",
423
+ )
424
+ reasoning_item = ResponseReasoningItem(
425
+ type="reasoning",
426
+ content=[content],
427
+ status="completed",
428
+ id=state.current_item_id,
429
+ summary=[],
430
+ )
431
+ events: list[StreamingResponsesResponse] = []
432
events.append(
530
- ResponseMcpCallArgumentsDeltaEvent(
531
- type="response.mcp_call_arguments.delta",
433
+ ResponseReasoningTextDoneEvent(
434
+ type="response.reasoning_text.done",
435
+ item_id=state.current_item_id,
436
sequence_number=-1,
437
output_index=state.current_output_index,
438
+ content_index=state.current_content_index,
439
+ text=text,
440
+ )
441
+ )
442
+ events.append(
443
+ ResponseReasoningPartDoneEvent(
444
+ type="response.reasoning_part.done",
445
+ sequence_number=-1,
446
item_id=state.current_item_id,
535
- delta=ctx.last_content_delta,
447
+ output_index=state.current_output_index,
448
+ content_index=state.current_content_index,
449
+ part=content,
450
+ )
451
+ )
452
+ events.append(
453
+ ResponseOutputItemDoneEvent(
454
+ type="response.output_item.done",
455
+ sequence_number=-1,
456
+ output_index=state.current_output_index,
457
+ item=reasoning_item,
458
)
459
)
460
return events
461
462
541
-def emit_function_call_delta_events(
542
- ctx: StreamingHarmonyContext,
543
- state: HarmonyStreamingState,
463
+def emit_function_call_done_events(
464
+ function_name: str,
465
+ arguments: str,
466
+ state: StreamingState,
467
) -> list[StreamingResponsesResponse]:
545
- """Emit events for developer function calls on commentary channel."""
546
- if not (
547
- ctx.parser.current_channel == "commentary"
548
- and ctx.parser.current_recipient
549
- and ctx.parser.current_recipient.startswith("functions.")
550
- ):
551
- return []
552
-
468
+ """Emit events when a function call completes."""
469
events: list[StreamingResponsesResponse] = []
554
- if state.is_first_function_call_delta is False:
555
- state.is_first_function_call_delta = True
556
- fc_name = ctx.parser.current_recipient[len("functions.") :]
557
- state.current_item_id = f"fc_{random_uuid()}"
558
- tool_call_item = ResponseFunctionToolCall(
559
- name=fc_name,
560
- type="function_call",
561
- id=state.current_item_id,
562
- call_id=f"call_{random_uuid()}",
563
- arguments="",
564
- status="in_progress",
470
+ events.append(
471
+ ResponseFunctionCallArgumentsDoneEvent(
472
+ type="response.function_call_arguments.done",
473
+ arguments=arguments,
474
+ name=function_name,
475
+ item_id=state.current_item_id,
476
+ output_index=state.current_output_index,
477
+ sequence_number=-1,
478
)
566
- events.append(
567
- ResponseOutputItemAddedEvent(
568
- type="response.output_item.added",
569
- sequence_number=-1,
570
- output_index=state.current_output_index,
571
- item=tool_call_item,
572
- )
479
+ )
480
+ function_call_item = ResponseFunctionToolCall(
481
+ type="function_call",
482
+ arguments=arguments,
483
+ name=function_name,
484
+ item_id=state.current_item_id,
485
+ output_index=state.current_output_index,
486
+ sequence_number=-1,
487
+ call_id=state.current_call_id,
488
+ status="completed",
489
+ )
490
+ events.append(
491
+ ResponseOutputItemDoneEvent(
492
+ type="response.output_item.done",
493
+ sequence_number=-1,
494
+ output_index=state.current_output_index,
495
+ item=function_call_item,
496
)
574
- # Always emit the delta (including on first call)
497
+ )
498
+ return events
499
+
500
+
501
+def emit_mcp_completion_events(
502
+ recipient: str,
503
+ arguments: str,
504
+ state: StreamingState,
505
+) -> list[StreamingResponsesResponse]:
506
+ """Emit events when an MCP tool call completes."""
507
+ name, server_label = _resolve_mcp_name_label(recipient)
508
+ events: list[StreamingResponsesResponse] = []
509
events.append(
576
- ResponseFunctionCallArgumentsDeltaEvent(
510
+ ResponseMcpCallArgumentsDoneEvent(
511
+ type="response.mcp_call_arguments.done",
512
+ arguments=arguments,
513
+ name=name,
514
item_id=state.current_item_id,
578
- delta=ctx.last_content_delta,
515
output_index=state.current_output_index,
516
sequence_number=-1,
581
- type="response.function_call_arguments.delta",
517
+ )
518
+ )
519
+ events.append(
520
+ ResponseMcpCallCompletedEvent(
521
+ type="response.mcp_call.completed",
522
+ sequence_number=-1,
523
+ output_index=state.current_output_index,
524
+ item_id=state.current_item_id,
525
+ )
526
+ )
527
+ events.append(
528
+ ResponseOutputItemDoneEvent(
529
+ type="response.output_item.done",
530
+ sequence_number=-1,
531
+ output_index=state.current_output_index,
532
+ item=McpCall(
533
+ type="mcp_call",
534
+ arguments=arguments,
535
+ name=name,
536
+ id=state.current_item_id,
537
+ server_label=server_label,
538
+ status="completed",
539
+ ),
540
)
541
)
542
return events
543
544
545
+# =====================================================================
546
+# Harmony-specific dispatchers
547
+# =====================================================================
548
+
549
+
550
def emit_content_delta_events(
551
ctx: StreamingHarmonyContext,
589
- state: HarmonyStreamingState,
552
+ state: StreamingState,
553
) -> list[StreamingResponsesResponse]:
591
- """Emit events for content delta streaming based on channel type."""
592
- if not ctx.last_content_delta:
554
+ """Emit events for content delta streaming based on channel type.
555
+
556
+ This is a Harmony-specific dispatcher that extracts values from the
557
+ Harmony context and delegates to shared leaf helpers.
558
+ """
559
+ delta = ctx.last_content_delta
560
+ if not delta:
561
return []
562
595
- if ctx.parser.current_channel == "final" and ctx.parser.current_recipient is None:
596
- return emit_final_channel_delta_events(ctx, state)
597
- elif (
598
- ctx.parser.current_channel == "analysis"
599
- and ctx.parser.current_recipient is None
600
- ):
601
- return emit_analysis_channel_delta_events(ctx, state)
563
+ channel = ctx.parser.current_channel
564
+ recipient = ctx.parser.current_recipient
565
+
566
+ if channel == "final" and recipient is None:
567
+ return emit_text_delta_events(delta, state)
568
+ elif channel == "analysis" and recipient is None:
569
+ return emit_reasoning_delta_events(delta, state)
570
# built-in tools will be triggered on the analysis channel
571
# However, occasionally built-in tools will
572
# still be output to commentary.
605
- elif (
606
- ctx.parser.current_channel == "commentary"
607
- or ctx.parser.current_channel == "analysis"
608
- ) and ctx.parser.current_recipient is not None:
609
- recipient = ctx.parser.current_recipient
610
- # Check for function calls first - they have their own event handling
573
+ elif channel in ("commentary", "analysis") and recipient is not None:
574
if recipient.startswith("functions."):
612
- return emit_function_call_delta_events(ctx, state)
613
- if is_mcp_tool_by_namespace(recipient):
614
- return emit_mcp_tool_delta_events(ctx, state, recipient)
615
- else:
616
- return emit_code_interpreter_delta_events(ctx, state)
617
- elif (
618
- (
619
- ctx.parser.current_channel == "commentary"
620
- or ctx.parser.current_channel == "analysis"
621
- )
622
- and ctx.parser.current_recipient is not None
623
- and ctx.parser.current_recipient.startswith("mcp.")
624
- ):
625
- return emit_mcp_prefix_delta_events(ctx, state)
575
+ function_name = recipient[len("functions.") :]
576
+ return emit_function_call_delta_events(delta, function_name, state)
577
+ elif recipient == "python":
578
+ return emit_code_interpreter_delta_events(delta, state)
579
+ elif recipient.startswith("mcp.") or is_mcp_tool_by_namespace(recipient):
580
+ return emit_mcp_delta_events(delta, state, recipient)
581
+
582
+ return []
583
+
584
585
+def emit_previous_item_done_events(
586
+ previous_item: HarmonyMessage,
587
+ state: StreamingState,
588
+) -> list[StreamingResponsesResponse]:
589
+ """Emit done events for the previous item when expecting a new start.
590
+
591
+ This is a Harmony-specific dispatcher that extracts values from the
592
+ Harmony parser's message object and delegates to shared leaf helpers.
593
+ """
594
+ text = previous_item.content[0].text
595
+ if previous_item.recipient is not None:
596
+ # Deal with tool call
597
+ if previous_item.recipient.startswith("functions."):
598
+ function_name = previous_item.recipient[len("functions.") :]
599
+ return emit_function_call_done_events(function_name, text, state)
600
+ elif previous_item.recipient == "python":
601
+ return emit_code_interpreter_completion_events(previous_item, state)
602
+ elif (
603
+ is_mcp_tool_by_namespace(previous_item.recipient)
604
+ and state.current_item_id is not None
605
+ and state.current_item_id.startswith("mcp_")
606
+ ):
607
+ return emit_mcp_completion_events(previous_item.recipient, text, state)
608
+ elif previous_item.channel == "analysis":
609
+ return emit_reasoning_done_events(text, state)
610
+ elif previous_item.channel == "final":
611
+ return emit_text_output_done_events(text, state)
612
return []
613
614
615
+# =====================================================================
616
+# Harmony-specific tool lifecycle helpers
617
+# =====================================================================
618
+
619
+
620
def emit_browser_tool_events(
631
- previous_item,
632
- state: HarmonyStreamingState,
621
+ previous_item: HarmonyMessage,
622
+ state: StreamingState,
623
) -> list[StreamingResponsesResponse]:
624
"""Emit events for browser tool calls (web search)."""
625
function_name = previous_item.recipient[len("browser.") :]
@@ -714,53 +704,9 @@ def emit_browser_tool_events(
704
return events
705
706
717
-def emit_mcp_tool_completion_events(
718
- previous_item,
719
- state: HarmonyStreamingState,
720
-) -> list[StreamingResponsesResponse]:
721
- """Emit events when an MCP tool completes during assistant action turn."""
722
- recipient = previous_item.recipient
723
- server_label = TOOL_NAME_TO_MCP_SERVER_LABEL.get(recipient, recipient)
724
- events: list[StreamingResponsesResponse] = []
725
- events.append(
726
- ResponseMcpCallArgumentsDoneEvent(
727
- type="response.mcp_call_arguments.done",
728
- sequence_number=-1,
729
- output_index=state.current_output_index,
730
- item_id=state.current_item_id,
731
- arguments=previous_item.content[0].text,
732
- name=recipient,
733
- )
734
- )
735
- events.append(
736
- ResponseMcpCallCompletedEvent(
737
- type="response.mcp_call.completed",
738
- sequence_number=-1,
739
- output_index=state.current_output_index,
740
- item_id=state.current_item_id,
741
- )
742
- )
743
- events.append(
744
- ResponseOutputItemDoneEvent(
745
- type="response.output_item.done",
746
- sequence_number=-1,
747
- output_index=state.current_output_index,
748
- item=McpCall(
749
- type="mcp_call",
750
- id=state.current_item_id,
751
- name=recipient,
752
- arguments=previous_item.content[0].text,
753
- server_label=server_label,
754
- status="completed",
755
- ),
756
- )
757
- )
758
- return events
759
-
760
-
707
def emit_code_interpreter_completion_events(
762
- previous_item,
763
- state: HarmonyStreamingState,
708
+ previous_item: HarmonyMessage,
709
+ state: StreamingState,
710
) -> list[StreamingResponsesResponse]:
711
"""Emit events when code interpreter completes."""
712
events: list[StreamingResponsesResponse] = []
@@ -807,52 +753,9 @@ def emit_code_interpreter_completion_events(
753
return events
754
755
810
-def emit_mcp_prefix_completion_events(
811
- previous_item,
812
- state: HarmonyStreamingState,
813
-) -> list[StreamingResponsesResponse]:
814
- """Emit events when an MCP prefix tool (mcp.*) completes."""
815
- mcp_name = previous_item.recipient[len("mcp.") :]
816
- events: list[StreamingResponsesResponse] = []
817
- events.append(
818
- ResponseMcpCallArgumentsDoneEvent(
819
- type="response.mcp_call_arguments.done",
820
- sequence_number=-1,
821
- output_index=state.current_output_index,
822
- item_id=state.current_item_id,
823
- arguments=previous_item.content[0].text,
824
- name=mcp_name,
825
- )
826
- )
827
- events.append(
828
- ResponseMcpCallCompletedEvent(
829
- type="response.mcp_call.completed",
830
- sequence_number=-1,
831
- output_index=state.current_output_index,
832
- item_id=state.current_item_id,
833
- )
834
- )
835
- events.append(
836
- ResponseOutputItemDoneEvent(
837
- type="response.output_item.done",
838
- sequence_number=-1,
839
- output_index=state.current_output_index,
840
- item=McpCall(
841
- type="mcp_call",
842
- id=state.current_item_id,
843
- name=mcp_name,
844
- arguments=previous_item.content[0].text,
845
- server_label=mcp_name,
846
- status="completed",
847
- ),
848
- )
849
- )
850
- return events
851
-
852
-
756
def emit_tool_action_events(
757
ctx: StreamingHarmonyContext,
855
- state: HarmonyStreamingState,
758
+ state: StreamingState,
759
tool_server: ToolServer | None,
760
) -> list[StreamingResponsesResponse]:
761
"""Emit events for tool action turn."""
@@ -879,19 +782,13 @@ def emit_tool_action_events(
782
and state.sent_output_item_added
783
):
784
recipient = previous_item.recipient
882
- # Handle MCP prefix tool completion first
883
- if recipient.startswith("mcp."):
884
- events.extend(emit_mcp_prefix_completion_events(previous_item, state))
885
- else:
886
- # Handle other MCP tool and code interpreter completion
887
- is_mcp_tool = is_mcp_tool_by_namespace(
888
- recipient
889
- ) and state.current_item_id.startswith("mcp_")
890
- if is_mcp_tool:
891
- events.extend(emit_mcp_tool_completion_events(previous_item, state))
892
- else:
893
- events.extend(
894
- emit_code_interpreter_completion_events(previous_item, state)
785
+ if recipient == "python":
786
+ events.extend(emit_code_interpreter_completion_events(previous_item, state))
787
+ elif recipient.startswith("mcp.") or is_mcp_tool_by_namespace(recipient):
788
+ events.extend(
789
+ emit_mcp_completion_events(
790
+ recipient, previous_item.content[0].text, state
791
)
792
+ )
793
794
return events