Agent Zero AI framework
Stream native response tool output
Forward incremental response function arguments through the canonical Agent Zero tool envelope so final Codex answers render progressively while preserving buffered execution for non-response tool calls. Cover partial DirtyJson rendering, completed envelope assembly, duplicate completion suppression, and the transport contract.
Alessandro committed
Aug 27, 2026 at 07:27 UTC
117ae265f9bb78e9d230c6832ec4aee3395e1236
3 files changed
+91
-6
helpers/litellm_transport.py
+26
-6
@@ -1166,6 +1166,7 @@ class ResponsesEventParser:
1166
self.function_calls: dict[str, dict[str, Any]] = {}
1167
self.output_index_keys: dict[str, str] = {}
1168
self.emitted_function_calls: set[str] = set()
1169
+ self.streamed_response_calls: dict[str, str] = {}
1170
self.seen_response_delta = False
1171
self.seen_reasoning_delta = False
1172
self.completed_response: Any = None
@@ -1189,7 +1190,7 @@ class ResponsesEventParser:
1190
elif event_type == "response.output_item.added":
1191
self._remember_function_call(_get_value(event, "item"), event)
1192
elif event_type == "response.function_call_arguments.delta":
1192
- self._append_function_call_arguments(event)
1193
+ response_delta = self._append_function_call_arguments(event)
1194
elif event_type == "response.function_call_arguments.done":
1195
response_delta = self._complete_function_call(event)
1196
elif event_type == "response.output_item.done":
@@ -1224,14 +1225,22 @@ class ResponsesEventParser:
1225
self.output_index_keys[str(output_index)] = key
1226
return key
1227
1227
- def _append_function_call_arguments(self, event: Any) -> None:
1228
+ def _append_function_call_arguments(self, event: Any) -> str:
1229
key = self._event_key(event)
1230
if not key:
1230
- return
1231
+ return ""
1232
current = self.function_calls.setdefault(key, {"type": "function_call"})
1232
- current["arguments"] = str(current.get("arguments") or "") + str(
1233
- _get_value(event, "delta") or ""
1234
- )
1233
+ delta = str(_get_value(event, "delta") or "")
1234
+ current["arguments"] = str(current.get("arguments") or "") + delta
1235
+ if current.get("name") != "response":
1236
+ return ""
1237
+ if key not in self.streamed_response_calls:
1238
+ self.streamed_response_calls[key] = str(current["arguments"])
1239
+ return '{"tool_name":"response","tool_args":' + str(
1240
+ current["arguments"]
1241
+ )
1242
+ self.streamed_response_calls[key] += delta
1243
+ return delta
1244
1245
def _complete_function_call(self, event: Any) -> str:
1246
key = self._event_key(event)
@@ -1242,14 +1251,25 @@ class ResponsesEventParser:
1251
current["arguments"] = _get_value(event, "arguments")
1252
if _get_value(event, "name"):
1253
current["name"] = _get_value(event, "name")
1254
+ if key in self.streamed_response_calls:
1255
+ return self._finish_response_call(key, current)
1256
return self._emit_function_call(key, current)
1257
1258
def _complete_output_item(self, item: Any, event: Any) -> str:
1259
key = self._remember_function_call(item, event)
1260
if not key:
1261
return ""
1262
+ if key in self.streamed_response_calls:
1263
+ return self._finish_response_call(key, self.function_calls[key])
1264
return self._emit_function_call(key, self.function_calls[key])
1265
1266
+ def _finish_response_call(self, key: str, item: Any) -> str:
1267
+ streamed = self.streamed_response_calls.pop(key)
1268
+ arguments = str(_get_value(item, "arguments") or "")
1269
+ self.emitted_function_calls.add(key)
1270
+ tail = arguments[len(streamed) :] if arguments.startswith(streamed) else ""
1271
+ return tail + "}"
1272
+
1273
def _complete_response(self, event: Any) -> tuple[str, str]:
1274
self.completed_response = _get_value(event, "response")
1275
if self.seen_response_delta or self.emitted_function_calls:
helpers/litellm_transport.py.dox.md
+1
@@ -35,6 +35,7 @@
35
- Preserve Chat Completions tool calls from both non-streaming responses and streaming deltas as canonical `LLMResult` function-call items.
36
- Preserve provider usage and LiteLLM response cost for both transports only when the response or stream actually supplies them; do not synthesize unavailable provider accounting.
37
- Preserve Responses function calls collected from stream events when a terminal completed event omits them.
38
+- Stream native `response` function arguments through a canonical response-tool envelope while continuing to buffer other function calls until completion.
39
- Serialize synthesized Responses function-call JSON with literal Unicode so streamed raw-response logs preserve tool arguments.
40
- Preserve provider-state metadata when Responses API calls succeed, and fall back to local replay when provider state is unsupported.
41
- Keep prompt-cache markers only for providers that accept them.
tests/test_stream_tool_early_stop.py
+64
@@ -13,6 +13,7 @@ if str(PROJECT_ROOT) not in sys.path:
13
import models
14
from helpers import extract_tools
15
from helpers import litellm_transport
16
+from helpers.dirty_json import DirtyJson
17
18
19
@pytest.fixture(autouse=True)
@@ -1704,6 +1705,69 @@ def test_responses_stream_parser_accumulates_function_call_arguments():
1705
) == {"reasoning_delta": "", "response_delta": ""}
1706
1707
1708
+def test_responses_stream_parser_streams_response_function_arguments():
1709
+ parser = litellm_transport.ResponsesEventParser()
1710
+
1711
+ parser.parse(
1712
+ {
1713
+ "type": "response.output_item.added",
1714
+ "output_index": 0,
1715
+ "item": {
1716
+ "type": "function_call",
1717
+ "id": "fc_1",
1718
+ "name": "response",
1719
+ "arguments": "",
1720
+ },
1721
+ }
1722
+ )
1723
+ chunks = [
1724
+ parser.parse(
1725
+ {
1726
+ "type": "response.function_call_arguments.delta",
1727
+ "item_id": "fc_1",
1728
+ "delta": '{"text":"Hello',
1729
+ }
1730
+ )["response_delta"],
1731
+ parser.parse(
1732
+ {
1733
+ "type": "response.function_call_arguments.delta",
1734
+ "item_id": "fc_1",
1735
+ "delta": ' world"}',
1736
+ }
1737
+ )["response_delta"],
1738
+ parser.parse(
1739
+ {
1740
+ "type": "response.function_call_arguments.done",
1741
+ "item_id": "fc_1",
1742
+ "name": "response",
1743
+ "arguments": '{"text":"Hello world"}',
1744
+ }
1745
+ )["response_delta"],
1746
+ ]
1747
+
1748
+ assert chunks[0] == '{"tool_name":"response","tool_args":{"text":"Hello'
1749
+ assert DirtyJson.parse_string(chunks[0]) == {
1750
+ "tool_name": "response",
1751
+ "tool_args": {"text": "Hello"},
1752
+ }
1753
+ assert extract_tools.json_parse_dirty("".join(chunks)) == {
1754
+ "tool_name": "response",
1755
+ "tool_args": {"text": "Hello world"},
1756
+ }
1757
+ assert chunks[-1] == "}"
1758
+ assert parser.parse(
1759
+ {
1760
+ "type": "response.output_item.done",
1761
+ "item": {
1762
+ "type": "function_call",
1763
+ "id": "fc_1",
1764
+ "name": "response",
1765
+ "arguments": '{"text":"Hello world"}',
1766
+ },
1767
+ }
1768
+ ) == {"reasoning_delta": "", "response_delta": ""}
1769
+
1770
+
1771
def test_responses_stream_parser_uses_completed_response_when_no_deltas_arrive():
1772
parser = litellm_transport.ResponsesEventParser()
1773