Pass through JSON objects with thoughts or headline

After repair, return a recovered JSON object that already contains thoughts or headline as compact JSON instead of wrapping the raw response in another thoughts envelope.

linkliti committed Aug 25, 2026 at 09:26 UTC e970e1e89435495ec5c26c56e00cd6a146ecf40c
2 files changed +37 -4
plugins/_context_doctor/helpers/context_doctor.py
+19 -4
@@ -49,23 +49,29 @@ def transform_response(response: str, *, suppress_xml: bool) -> str:
49
50 apply_patch()
51 try:
52 - repaired = repair_json(
52 + salvage_repaired = repair_json(
53 response,
54 return_objects=True,
55 schema=_A0_SALVAGE_SCHEMA,
56 schema_repair_mode="salvage",
57 )
58 except Exception:
59 - repaired = repair_json(response, return_objects=True)
59 + salvage_repaired = repair_json(response, return_objects=True)
60
61 - candidates = repaired if isinstance(repaired, list) else [repaired]
61 + candidates = (
62 + salvage_repaired
63 + if isinstance(salvage_repaired, list)
64 + else [salvage_repaired]
65 + )
66 try:
67 no_schema = repair_json(response, return_objects=True)
68 except Exception:
69 no_schema = None
70 if isinstance(no_schema, list):
71 valid = [item for item in no_schema if _is_tool_call(item)]
68 - if len(valid) > 1 or (len(valid) == 1 and not _is_tool_call(repaired)):
72 + if len(valid) > 1 or (
73 + len(valid) == 1 and not _is_tool_call(salvage_repaired)
74 + ):
75 candidates = no_schema
76 repaired = max(
77 (item for item in candidates if _is_tool_call(item)),
@@ -74,10 +80,19 @@ def transform_response(response: str, *, suppress_xml: bool) -> str:
80 )
81 except Exception:
82 repaired = None
83 + salvage_repaired = None
84
85 if _is_tool_call(repaired):
86 return json.dumps(repaired, ensure_ascii=False, separators=(",", ":"))
87
88 + for candidate in (repaired, salvage_repaired):
89 + if isinstance(candidate, dict) and (
90 + "thoughts" in candidate or "headline" in candidate
91 + ):
92 + return json.dumps(
93 + candidate, ensure_ascii=False, separators=(",", ":")
94 + )
95 +
96 if suppress_xml and "<" in response and ">" in response:
97 return "{}"
98 return json.dumps({"thoughts": [response]}, ensure_ascii=False, separators=(",", ":"))
plugins/_context_doctor/tests/test_context_doctor.py
+18
@@ -31,6 +31,24 @@ def test_wraps_raw_text_in_thoughts():
31 )
32
33
34 +def test_does_not_wrap_json_with_thoughts_or_headline():
35 + response = '{"thoughts":["Reasoning"],"headline":"A title"}'
36 +
37 + assert transform_response(response, suppress_xml=True) == response
38 +
39 +
40 +def test_does_not_wrap_json_with_only_headline():
41 + response = '{"headline":"Just a headline"}'
42 +
43 + assert transform_response(response, suppress_xml=True) == response
44 +
45 +
46 +def test_does_not_wrap_json_with_only_thoughts():
47 + response = '{"thoughts":["only thoughts"]}'
48 +
49 + assert transform_response(response, suppress_xml=True) == response
50 +
51 +
52 def test_suppresses_xml_when_enabled():
53 assert transform_response("<tool>response</tool>", suppress_xml=True) == "{}"
54 assert transform_response("<tool>response</tool>", suppress_xml=False) == (