Prevent Responses metadata history bloat
Persist only durable Responses continuation metadata instead of repeated prompts and raw model payloads. Normalize legacy AI message metadata during load while preserving structured output, unrelated metadata, and tool-result inputs.
Alessandro committed
Jul 14, 2026 at 20:41 UTC
7b216c63437973592ee4d75c0d933d4f7493e43a
5 files changed
+53
-6
helpers/history.py
+9
-1
@@ -150,11 +150,19 @@ class Message(Record):
150
@staticmethod
151
def from_dict(data: dict, history: "History"):
152
content = data.get("content", "Content lost")
153
+ metadata = data.get("metadata", {})
154
+ metadata = metadata if isinstance(metadata, dict) else {}
155
+ if data["ai"]:
156
+ from helpers.llm_result import result_from_metadata
157
+
158
+ result = result_from_metadata(metadata)
159
+ if result:
160
+ metadata = {**metadata, **result.metadata()}
161
msg = Message(
162
ai=data["ai"],
163
content=content,
164
id=data.get("id", ""),
157
- metadata=data.get("metadata", {}) if isinstance(data.get("metadata"), dict) else {},
165
+ metadata=metadata,
166
sequence=int(data.get("sequence", 0) or 0),
167
)
168
msg.summary = data.get("summary", "")
helpers/history.py.dox.md
+1
@@ -79,6 +79,7 @@
79
- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
80
- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
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
- Observed side-effect areas: filesystem writes, filesystem deletion, model calls, plugin state, settings/state persistence, secret handling.
84
- Imported dependency areas include: `abc`, `asyncio`, `collections`, `collections.abc`, `enum`, `helpers`, `json`, `langchain_core.messages`, `math`, `plugins._model_config.helpers.model_config`, `typing`, `uuid`.
85
helpers/llm_result.py
+12
-1
@@ -216,7 +216,18 @@ class LLMResult:
216
}
217
218
def metadata(self) -> dict[str, Any]:
219
- return {RESPONSE_METADATA_KEY: self.to_dict()}
219
+ return {
220
+ RESPONSE_METADATA_KEY: {
221
+ "response_id": self.response_id,
222
+ "previous_response_id": self.previous_response_id,
223
+ "output_items": [item.to_dict() for item in self.output_items],
224
+ "provider_model_key": self.provider_model_key,
225
+ "mode": self.mode,
226
+ "state": self.state,
227
+ "usage": self.usage,
228
+ "capability": self.capability,
229
+ }
230
+ }
231
232
233
def function_call_output_item(
helpers/llm_result.py.dox.md
+1
-1
@@ -17,7 +17,7 @@
17
18
## Runtime Contracts
19
20
-- `LLMResult.metadata()` stores data under `RESPONSE_METADATA_KEY` so history can round-trip provider state.
20
+- `LLMResult.metadata()` stores only durable provider state under `RESPONSE_METADATA_KEY`: response IDs, structured output items, provider/mode/state, usage, and capability data. Runtime prompt inputs, raw responses, and duplicated response/reasoning text are not persisted in history.
21
- `from_response(...)` must preserve provider `response_id`, `previous_response_id`, raw output items, usage, and capability metadata.
22
- `from_chat(...)` must produce an equivalent chat-completions result with `mode="chat_completions"` and `state="off"`, preserving optional function-call output items when the chat transport supplies them.
23
- Function-call output items must preserve `call_id` and optional acknowledged safety checks.
tests/test_responses_architecture.py
+30
-3
@@ -43,7 +43,7 @@ class _AsyncEventStream:
43
self.closed = True
44
45
46
-def test_llm_result_round_trips_responses_metadata():
46
+def test_llm_result_persists_only_durable_responses_metadata():
47
result = LLMResult.from_response(
48
{
49
"id": "resp_123",
@@ -69,7 +69,14 @@ def test_llm_result_round_trips_responses_metadata():
69
provider_model_key="openai/gpt-5.4",
70
)
71
72
- loaded = result_from_metadata(result.metadata())
72
+ metadata = result.metadata()
73
+ persisted = metadata["responses"]
74
+ assert "response" not in persisted
75
+ assert "reasoning" not in persisted
76
+ assert "input_items" not in persisted
77
+ assert "raw" not in persisted
78
+
79
+ loaded = result_from_metadata(metadata)
80
81
assert loaded is not None
82
assert loaded.response_id == "resp_123"
@@ -79,22 +86,42 @@ def test_llm_result_round_trips_responses_metadata():
86
assert loaded.builtin_items[0].type == "web_search_call"
87
88
82
-def test_history_serializes_metadata_and_migrates_old_messages():
89
+def test_history_migrates_legacy_ai_metadata_and_preserves_tool_inputs():
90
class DummyAgent:
91
pass
92
93
hist = history.History(DummyAgent())
94
result = LLMResult.from_response(
95
{"id": "resp_1", "output": [{"type": "message", "content": [{"type": "output_text", "text": "ok"}]}]},
96
+ input_items=[{"role": "user", "content": "question"}],
97
provider_model_key="openai/gpt-5.4",
98
)
99
100
message = hist.add_message(True, "ok", metadata=result.metadata())
101
+ tool_item = {"type": "function_call_output", "call_id": "call_1", "output": "done"}
102
+ hist.add_message(
103
+ False,
104
+ "done",
105
+ metadata={"responses": {"input_items": [tool_item]}},
106
+ )
107
restored = history.deserialize_history(hist.serialize(), DummyAgent())
108
109
restored_message = restored.all_messages()[0]
110
assert restored_message.sequence == message.sequence
111
assert result_from_metadata(restored_message.metadata).response_id == "resp_1"
112
+ assert restored.all_messages()[1].metadata["responses"]["input_items"] == [tool_item]
113
+
114
+ migrated = history.Message.from_dict(
115
+ {
116
+ "_cls": "Message",
117
+ "ai": True,
118
+ "content": "old",
119
+ "metadata": {"custom": "keep", "responses": result.to_dict()},
120
+ },
121
+ restored,
122
+ )
123
+ assert "input_items" not in migrated.metadata["responses"]
124
+ assert migrated.metadata["custom"] == "keep"
125
126
old = history.Message.from_dict({"_cls": "Message", "ai": False, "content": "old"}, restored)
127
assert old.metadata == {}