From 8858728723a557650dac002abcfb2d6394af4404 Mon Sep 17 00:00:00 2001 From: argszero Date: Sat, 22 Aug 2026 18:06:09 +0800 Subject: [PATCH] emrg: persist and pass back DeepSeek thinking-mode reasoning (rant 2026-08-22T17:25:02) --- Agent.md | 2 +- emrg/server/daemon.py | 30 +++++++++++++++++++++++--- emrg/session.py | 6 ++++++ tests/test_session.py | 49 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 4 deletions(-) diff --git a/Agent.md b/Agent.md index 4665707b..f63b2027 100644 --- a/Agent.md +++ b/Agent.md @@ -118,7 +118,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design: pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg ``` -Python: `uv run pytest tests/ -v` (1008) — import check: `uv run python -c "from emrg.client.app import run_client"` +Python: `uv run pytest tests/ -v` (1011) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (259: 45 daemon_client + 20 conn-manager + 22 app-commands + 129 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文) Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响) diff --git a/emrg/server/daemon.py b/emrg/server/daemon.py index 622c9256..91045d08 100644 --- a/emrg/server/daemon.py +++ b/emrg/server/daemon.py @@ -2539,18 +2539,24 @@ async def _run_tool_loop( reasoning=full_reasoning, ) - # Persist assistant message + # Persist assistant message (reasoning kept for DeepSeek + # thinking-mode pass-back, rant 2026-08-22T17:25:02) session.append_message({ "type": "message", "role": "assistant", "content": full_content, + **({"reasoning": full_reasoning} if full_reasoning else {}), }) # Append the assistant reply to the local messages so the # LLM context stays coherent when queued messages are # injected after this round (mirrors Case 2's assistant # tool_calls message). - messages.append({"role": "assistant", "content": full_content}) + messages.append({ + "role": "assistant", + "content": full_content, + **({"reasoning_content": full_reasoning} if full_reasoning else {}), + }) # P1 (rant 21:55:37): messages queued mid-round (after the # round-top drain) must not end the turn — inject and continue. @@ -2605,6 +2611,10 @@ async def _run_tool_loop( }, }) assistant_msg["tool_calls"] = openai_tool_calls + if full_reasoning: + # DeepSeek thinking mode: reasoning must be passed back + # verbatim on the next round (rant 2026-08-22T17:25:02). + assistant_msg["reasoning_content"] = full_reasoning messages.append(assistant_msg) # Persist assistant message WITH embedded tool_calls @@ -2612,6 +2622,7 @@ async def _run_tool_loop( "type": "message", "role": "assistant", "content": full_content, + **({"reasoning": full_reasoning} if full_reasoning else {}), "tool_calls": [ { "id": tc.get("id", ""), @@ -2734,12 +2745,17 @@ async def _run_tool_loop( "type": "message", "role": "assistant", "content": full_content, + **({"reasoning": full_reasoning} if full_reasoning else {}), }) # Append the assistant reply to the local messages so the LLM # context stays coherent when queued messages are injected # after this round. - messages.append({"role": "assistant", "content": full_content}) + messages.append({ + "role": "assistant", + "content": full_content, + **({"reasoning_content": full_reasoning} if full_reasoning else {}), + }) # P1 (rant 21:55:37): messages queued mid-round must not end the # turn — inject and continue (injection round does not consume @@ -3736,6 +3752,10 @@ async def _reflect(): # IMPORTANT: assistant message with tool_calls must come BEFORE # tool result messages (OpenAI/DeepSeek API requirement). assistant_msg["tool_calls"] = openai_tool_calls + if msg.get("reasoning_content") or msg.get("reasoning"): + assistant_msg["reasoning_content"] = ( + msg.get("reasoning_content") or msg.get("reasoning") + ) messages.append(assistant_msg) for tc in tool_calls: @@ -3848,6 +3868,10 @@ async def _consolidate_session_memories( "function": {"name": fn.get("name", ""), "arguments": fn.get("arguments", "")}, }) assistant_msg["tool_calls"] = openai_tool_calls + if msg.get("reasoning_content") or msg.get("reasoning"): + assistant_msg["reasoning_content"] = ( + msg.get("reasoning_content") or msg.get("reasoning") + ) messages.append(assistant_msg) for tc in tool_calls: diff --git a/emrg/session.py b/emrg/session.py index 0510b25c..16b663f5 100644 --- a/emrg/session.py +++ b/emrg/session.py @@ -323,6 +323,12 @@ def get_messages_for_llm(self) -> list[dict]: if r.get("type") == "message": msg: dict = {"role": r["role"], "content": r.get("content")} + # DeepSeek thinking mode: assistant reasoning must be passed + # back verbatim to the API (rant 2026-08-22T17:25:02). Old + # records without the field are skipped naturally. + if r.get("role") == "assistant" and r.get("reasoning"): + msg["reasoning_content"] = r["reasoning"] + # Check for embedded tool_calls (current format) embedded_tc = r.get("tool_calls") if embedded_tc and r["role"] == "assistant": diff --git a/tests/test_session.py b/tests/test_session.py index 35cabcaf..5f26d796 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -641,6 +641,55 @@ def test_messages_with_embedded_tool_calls(self, tmp_path): assert result[2]["role"] == "tool" assert result[2]["tool_call_id"] == "call_1" + def test_assistant_reasoning_passed_back(self, tmp_path): + """get_messages_for_llm() maps persisted reasoning to reasoning_content + (DeepSeek thinking-mode pass-back, rant 2026-08-22T17:25:02).""" + session = Session.create(tmp_path) + session.append_message({ + "type": "message", + "role": "assistant", + "content": "answer", + "reasoning": "think step by step", + }) + + result = session.get_messages_for_llm() + assert len(result) == 1 + assert result[0]["reasoning_content"] == "think step by step" + + def test_assistant_reasoning_with_tool_calls_passed_back(self, tmp_path): + """get_messages_for_llm() keeps reasoning_content on tool-call rounds.""" + session = Session.create(tmp_path) + session.append_message({ + "type": "message", + "role": "assistant", + "content": None, + "reasoning": "need to read the file", + "tool_calls": [ + {"id": "call_1", "type": "function", "function": {"name": "read", "arguments": "{}"}}, + ], + }) + session.append_message({ + "type": "tool_result", + "role": "tool", + "tool_call_id": "call_1", + "content": "file content", + }) + + result = session.get_messages_for_llm() + assert len(result) == 2 + assert result[0]["reasoning_content"] == "need to read the file" + assert "tool_calls" in result[0] + + def test_old_records_without_reasoning_untouched(self, tmp_path): + """get_messages_for_llm() skips reasoning for old records without the field.""" + session = Session.create(tmp_path) + session.append_message({"type": "message", "role": "assistant", "content": "old answer"}) + + result = session.get_messages_for_llm() + assert len(result) == 1 + assert "reasoning_content" not in result[0] + assert result[0] == {"role": "assistant", "content": "old answer"} + class TestSessionListSessions: """Tests for Session.list_sessions()."""