Fix Responses API output and minify user turns

Preserve non-ASCII function-call arguments in Responses API output and serialize automated structured user turns with compact JSON. Cover Cyrillic tool arguments and compact persisted Responses input items.

linkliti committed Aug 24, 2026 at 19:51 UTC 3db4c4954ea23d22657d36862428927a32b0edc0
4 files changed +24 -4
helpers/history.py
+1 -1
@@ -808,7 +808,7 @@ def _messages_from_record(record: Record) -> list[Message]:
808
809
810 def _json_dumps(obj):
811 - return json.dumps(obj, ensure_ascii=False)
811 + return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
812
813
814 def _json_loads(obj):
helpers/history.py.dox.md
+1
@@ -81,6 +81,7 @@
81 - `clear_responses_provider_state(agent)` removes the active provider continuation IDs after local history rewrites while preserving stored response ID lists for later cleanup.
82 - `Message.from_dict()` normalizes legacy AI Responses metadata through `LLMResult.metadata()` so loaded chats shed transient payloads while unrelated metadata and non-AI tool-result inputs remain intact.
83 - `output_langchain()` removes leading assistant messages after grouping so provider histories always begin with a user turn; the WebUI greeting remains persisted and displayed but is not sent as an orphaned assistant message.
84 +- `_json_dumps()` emits compact JSON (`","`, `":"` separators) for serialized history and generated non-string user-turn content.
85 - Observed side-effect areas: filesystem writes, filesystem deletion, model calls, plugin state, settings/state persistence, secret handling.
86 - Imported dependency areas include: `abc`, `asyncio`, `collections`, `collections.abc`, `enum`, `helpers`, `json`, `langchain_core.messages`, `math`, `plugins._model_config.helpers.model_config`, `typing`, `uuid`.
87
helpers/llm_result.py
+3 -2
@@ -194,9 +194,10 @@ class LLMResult:
194 if not calls:
195 return ""
196 if len(calls) == 1:
197 - return json.dumps(calls[0])
197 + return json.dumps(calls[0], ensure_ascii=False)
198 return json.dumps(
199 - {"tool_name": "parallel_tool_calls", "tool_args": {"calls": calls}}
199 + {"tool_name": "parallel_tool_calls", "tool_args": {"calls": calls}},
200 + ensure_ascii=False,
201 )
202
203 def to_dict(self) -> dict[str, Any]:
tests/test_responses_architecture.py
+19 -1
@@ -44,6 +44,22 @@ class _AsyncEventStream:
44 self.closed = True
45
46
47 +def test_responses_function_call_text_preserves_non_ascii_tool_args():
48 + result = LLMResult.from_response(
49 + {
50 + "output": [
51 + {
52 + "type": "function_call",
53 + "name": "response",
54 + "arguments": '{"text":"привет"}',
55 + }
56 + ]
57 + }
58 + )
59 +
60 + assert result.function_calls_text() == '{"tool_name": "response", "tool_args": {"text": "привет"}}'
61 +
62 +
63 def test_llm_result_persists_only_durable_responses_metadata():
64 result = LLMResult.from_response(
65 {
@@ -105,7 +121,9 @@ def test_history_migrates_legacy_ai_metadata_and_preserves_tool_inputs():
121 "done",
122 metadata={"responses": {"input_items": [tool_item]}},
123 )
108 - restored = history.deserialize_history(hist.serialize(), DummyAgent())
124 + serialized = hist.serialize()
125 + assert '"input_items":[{"type":"function_call_output"' in serialized
126 + restored = history.deserialize_history(serialized, DummyAgent())
127
128 restored_message = restored.all_messages()[0]
129 assert restored_message.sequence == message.sequence