fix: preserve reasoning_content on Kimi replay

This commit is contained in:
helix4u
2026-04-21 23:40:31 -06:00
committed by Teknium
parent 30ec12970b
commit a7d78d3bfd
7 changed files with 187 additions and 16 deletions

View File

@@ -1059,6 +1059,7 @@ class TestRewriteTranscriptPreservesReasoning:
role="assistant",
content="The answer is 42.",
reasoning="I need to think step by step.",
reasoning_content="provider scratchpad",
reasoning_details=[{"type": "summary", "text": "step by step"}],
codex_reasoning_items=[{"id": "r1", "type": "reasoning"}],
)
@@ -1066,6 +1067,7 @@ class TestRewriteTranscriptPreservesReasoning:
# Verify all three were stored
before = db.get_messages_as_conversation(session_id)
assert before[0].get("reasoning") == "I need to think step by step."
assert before[0].get("reasoning_content") == "provider scratchpad"
assert before[0].get("reasoning_details") == [{"type": "summary", "text": "step by step"}]
assert before[0].get("codex_reasoning_items") == [{"id": "r1", "type": "reasoning"}]
@@ -1082,5 +1084,6 @@ class TestRewriteTranscriptPreservesReasoning:
# Load again — all three reasoning fields must survive
after = db.get_messages_as_conversation(session_id)
assert after[0].get("reasoning") == "I need to think step by step."
assert after[0].get("reasoning_content") == "provider scratchpad"
assert after[0].get("reasoning_details") == [{"type": "summary", "text": "step by step"}]
assert after[0].get("codex_reasoning_items") == [{"id": "r1", "type": "reasoning"}]

View File

@@ -1216,6 +1216,15 @@ class TestBuildAssistantMessage:
result = agent._build_assistant_message(msg, "stop")
assert result["reasoning"] == "thinking"
def test_reasoning_content_preserved_separately(self, agent):
msg = _mock_assistant_msg(
content="answer",
reasoning="summary",
reasoning_content="provider scratchpad",
)
result = agent._build_assistant_message(msg, "stop")
assert result["reasoning_content"] == "provider scratchpad"
def test_with_tool_calls(self, agent):
tc = _mock_tool_call(name="web_search", arguments='{"q":"test"}', call_id="c1")
msg = _mock_assistant_msg(content="", tool_calls=[tc])
@@ -4188,6 +4197,90 @@ class TestPersistUserMessageOverride:
assert first_db_write["content"] == "Hello there"
class TestReasoningReplayForStrictProviders:
"""Assistant replay must preserve provider-native reasoning fields."""
def _setup_agent(self, agent):
agent._cached_system_prompt = "You are helpful."
agent._use_prompt_caching = False
agent.tool_delay = 0
agent.compression_enabled = False
agent.save_trajectories = False
def test_kimi_tool_replay_includes_empty_reasoning_content(self, agent):
self._setup_agent(agent)
agent.base_url = "https://api.kimi.com/coding/v1"
agent._base_url_lower = agent.base_url.lower()
agent.provider = "kimi-coding"
prior_assistant = {
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "c1",
"type": "function",
"function": {"name": "terminal", "arguments": "{\"command\":\"date\"}"},
}
],
}
tool_result = {"role": "tool", "tool_call_id": "c1", "content": "Tue Apr 21"}
final_resp = _mock_response(content="done", finish_reason="stop")
agent.client.chat.completions.create.return_value = final_resp
with (
patch.object(agent, "_persist_session"),
patch.object(agent, "_save_trajectory"),
patch.object(agent, "_cleanup_task_resources"),
):
result = agent.run_conversation(
"next step",
conversation_history=[prior_assistant, tool_result],
)
assert result["completed"] is True
sent_messages = agent.client.chat.completions.create.call_args.kwargs["messages"]
replayed_assistant = next(msg for msg in sent_messages if msg.get("role") == "assistant")
assert replayed_assistant["role"] == "assistant"
assert replayed_assistant["tool_calls"][0]["function"]["name"] == "terminal"
assert "reasoning_content" in replayed_assistant
assert replayed_assistant["reasoning_content"] == ""
def test_explicit_reasoning_content_beats_normalized_reasoning_on_replay(self, agent):
self._setup_agent(agent)
prior_assistant = {
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "c1",
"type": "function",
"function": {"name": "web_search", "arguments": "{\"q\":\"test\"}"},
}
],
"reasoning": "summary reasoning",
"reasoning_content": "provider-native scratchpad",
}
tool_result = {"role": "tool", "tool_call_id": "c1", "content": "ok"}
final_resp = _mock_response(content="done", finish_reason="stop")
agent.client.chat.completions.create.return_value = final_resp
with (
patch.object(agent, "_persist_session"),
patch.object(agent, "_save_trajectory"),
patch.object(agent, "_cleanup_task_resources"),
):
result = agent.run_conversation(
"next step",
conversation_history=[prior_assistant, tool_result],
)
assert result["completed"] is True
sent_messages = agent.client.chat.completions.create.call_args.kwargs["messages"]
replayed_assistant = next(msg for msg in sent_messages if msg.get("role") == "assistant")
assert replayed_assistant["reasoning_content"] == "provider-native scratchpad"
# ---------------------------------------------------------------------------
# Bugfix: _vprint force=True on error messages during TTS
# ---------------------------------------------------------------------------

View File

@@ -255,6 +255,38 @@ class TestMessageStorage:
assert msg["reasoning"] == "Thinking about what to say"
assert msg["reasoning_details"] == details
def test_reasoning_content_persisted_and_restored(self, db):
"""reasoning_content must survive session replay as its own field."""
db.create_session(session_id="s1", source="cli")
db.append_message(
"s1",
role="assistant",
content="Hello",
reasoning="Short summary",
reasoning_content="Longer provider-native scratchpad",
)
conv = db.get_messages_as_conversation("s1")
assert len(conv) == 1
assert conv[0]["reasoning"] == "Short summary"
assert conv[0]["reasoning_content"] == "Longer provider-native scratchpad"
def test_reasoning_content_empty_string_restored_for_assistant(self, db):
"""Empty reasoning_content still needs to round-trip for strict replays."""
db.create_session(session_id="s1", source="cli")
db.append_message(
"s1",
role="assistant",
content="",
tool_calls=[{"id": "c1", "type": "function", "function": {"name": "date", "arguments": "{}"}}],
reasoning_content="",
)
conv = db.get_messages_as_conversation("s1")
assert len(conv) == 1
assert "reasoning_content" in conv[0]
assert conv[0]["reasoning_content"] == ""
def test_reasoning_not_set_for_non_assistant(self, db):
"""reasoning is never leaked onto user or tool messages."""
db.create_session(session_id="s1", source="telegram")
@@ -1120,7 +1152,7 @@ class TestSchemaInit:
def test_schema_version(self, db):
cursor = db._conn.execute("SELECT version FROM schema_version")
version = cursor.fetchone()[0]
assert version == 6
assert version == 7
def test_title_column_exists(self, db):
"""Verify the title column was created in the sessions table."""
@@ -1176,12 +1208,12 @@ class TestSchemaInit:
conn.commit()
conn.close()
# Open with SessionDB — should migrate to v6
# Open with SessionDB — should migrate to v7
migrated_db = SessionDB(db_path=db_path)
# Verify migration
cursor = migrated_db._conn.execute("SELECT version FROM schema_version")
assert cursor.fetchone()[0] == 6
assert cursor.fetchone()[0] == 7
# Verify title column exists and is NULL for existing sessions
session = migrated_db.get_session("existing")