Preserve native reasoning as thoughts
Promote native reasoning summaries after response streaming completes and keep the normalized thoughts through Context Doctor log refreshes. Add regression coverage for both lifecycle boundaries.
Alessandro committed
Aug 27, 2026 at 08:54 UTC
dcf5c1500ebfe70bc438f59d24692f03bb544ca3
6 files changed
+50
-5
extensions/python/response_stream_end/AGENTS.md
+2
@@ -12,6 +12,8 @@
12
13
- Preserve final masking before response content is considered complete.
14
- Keep log state consistent with streamed chunks and final response text.
15
+- Preserve explicit planning fields; when `thoughts` are absent, expose native
16
+ reasoning summaries as thoughts after both streams finish.
17
18
## Work Guidance
19
extensions/python/response_stream_end/_15_log_from_stream_end.py
+6
-1
@@ -27,5 +27,10 @@ class LogFromStream(Extension):
27
if log_item.kvps is not None and "step" in log_item.kvps:
28
del log_item.kvps["step"]
29
30
+ if log_item.kvps is not None and "thoughts" not in log_item.kvps:
31
+ reasoning = log_item.kvps.get("reasoning")
32
+ if reasoning:
33
+ log_item.kvps["thoughts"] = [reasoning]
34
+
35
# update the log item
31
- log_item.update(kvps=log_item.kvps)
\ No newline at end of file
36
+ log_item.update(kvps=log_item.kvps)
plugins/_context_doctor/AGENTS.md
+2
-1
@@ -16,7 +16,8 @@
16
17
- Repaired and fallback JSON is always minified.
18
- Nonempty non-tool output becomes `{"thoughts":[raw]}`; XML-like output becomes `{}` only when suppression is enabled.
19
-- Log kvps retain streamed `reasoning` and add transformed output; `update_log` controls only View Details content.
19
+- Log kvps retain streamed `reasoning` and normalized `thoughts`, then add
20
+ transformed output; `update_log` controls only View Details content.
21
- A repaired `response` tool call refreshes the response log item when streaming did not create it.
22
- Runtime setup reads the `json_repair` pin from root `requirements.txt`; do not duplicate its version in plugin code.
23
plugins/_context_doctor/helpers/context_doctor.py
+6
-2
@@ -126,8 +126,12 @@ def update_log_item(
126
127
current_kvps = getattr(log_item, "kvps", None)
128
kvps = (
129
- {"reasoning": current_kvps["reasoning"]}
130
- if isinstance(current_kvps, dict) and "reasoning" in current_kvps
129
+ {
130
+ key: current_kvps[key]
131
+ for key in ("reasoning", "thoughts")
132
+ if key in current_kvps
133
+ }
134
+ if isinstance(current_kvps, dict)
135
else {}
136
)
137
kvps.update(parsed)
plugins/_context_doctor/tests/test_context_doctor.py
+2
-1
@@ -61,7 +61,7 @@ def test_suppresses_xml_when_enabled():
61
62
def test_updates_log_kvps_and_heading_while_preserving_raw_content():
63
log_item = SimpleNamespace(
64
- kvps={"reasoning": "because"},
64
+ kvps={"reasoning": "because", "thoughts": ["because"]},
65
update=lambda **kwargs: setattr(log_item, "data", kwargs),
66
)
67
raw = '{"tool_name":"response","tool_args":{"text":"ok",},}'
@@ -77,6 +77,7 @@ def test_updates_log_kvps_and_heading_while_preserving_raw_content():
77
78
assert log_item.data["content"] == raw
79
assert log_item.data["kvps"]["reasoning"] == "because"
80
+ assert log_item.data["kvps"]["thoughts"] == ["because"]
81
assert log_item.data["kvps"]["tool_name"] == "response"
82
assert log_item.data["heading"] == "A0: Done"
83
tests/test_plain_response_logging.py
+32
@@ -9,6 +9,12 @@ from extensions.python._functions.agent.Agent.hist_add_ai_response.end._10_log_p
9
from extensions.python.response_stream._10_log_from_stream import (
10
LogFromStream as StreamLog,
11
)
12
+from extensions.python.reasoning_stream._10_log_from_stream import (
13
+ LogFromStream as ReasoningLog,
14
+)
15
+from extensions.python.response_stream_end._15_log_from_stream_end import (
16
+ LogFromStream as StreamLogEnd,
17
+)
18
from extensions.python.response_stream._20_live_response import LiveResponse
19
from helpers.dirty_json import DirtyJson
20
from helpers.log import Log
@@ -213,3 +219,29 @@ async def test_stream_log_tolerates_partial_tool_arguments(
219
220
item = loop_data.params_temporary["log_item_generating"]
221
assert item.kvps["step"] == expected_step
222
+
223
+
224
+@pytest.mark.asyncio
225
+async def test_stream_log_uses_native_reasoning_when_thoughts_are_absent():
226
+ log = Log()
227
+ loop_data = LoopData()
228
+ agent = SimpleNamespace(
229
+ context=SimpleNamespace(log=log),
230
+ agent_name="A0",
231
+ )
232
+
233
+ await ReasoningLog(agent=agent).execute(
234
+ loop_data=loop_data,
235
+ text="Native reasoning summary",
236
+ )
237
+ await StreamLog(agent=agent).execute(
238
+ loop_data=loop_data,
239
+ text='{"tool_name":"response","tool_args":{"text":"Done"}}',
240
+ parsed={"tool_name": "response", "tool_args": {"text": "Done"}},
241
+ )
242
+ await StreamLogEnd(agent=agent).execute(loop_data=loop_data)
243
+
244
+ item = loop_data.params_temporary["log_item_generating"]
245
+ assert item.heading == "A0: Using response"
246
+ assert item.kvps["thoughts"] == ["Native reasoning summary"]
247
+ assert item.kvps["reasoning"] == "Native reasoning summary"