mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-16 21:40:49 +08:00
fix(chat): always persist reasoning_content on assistant event_payload
Non-thinking profiles previously wrote model reasoning only as separate event_type=reasoning rows whose event_payload held chunk_index metadata, so JSON inspection showed tool payloads but no reasoning_content key. Store the full merged reasoning blob on the primary assistant row whenever present (same shape as thinking mode). Update instruction-leak filtering to scan event_payload.reasoning_content as well as legacy reasoning rows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c938147fdb
commit
5efac74f87
3 changed files with 35 additions and 19 deletions
|
|
@ -238,12 +238,20 @@ def _filter_internal_instruction_user_messages(msgs: list[Any]) -> list[Any]:
|
||||||
instruction_texts: set[str] = set()
|
instruction_texts: set[str] = set()
|
||||||
for m in msgs:
|
for m in msgs:
|
||||||
role = str(getattr(m, "role", "") or "")
|
role = str(getattr(m, "role", "") or "")
|
||||||
event_type = str(getattr(m, "event_type", "") or "")
|
if role != "assistant":
|
||||||
if role != "assistant" or event_type != "reasoning":
|
|
||||||
continue
|
continue
|
||||||
instr = _extract_manager_instruction_text(str(getattr(m, "content", "") or ""))
|
event_type = str(getattr(m, "event_type", "") or "")
|
||||||
if instr:
|
candidates: list[str] = []
|
||||||
instruction_texts.add(instr)
|
if event_type == "reasoning":
|
||||||
|
candidates.append(str(getattr(m, "content", "") or ""))
|
||||||
|
ep = _safe_json_object(getattr(m, "event_payload", None))
|
||||||
|
rc = str(ep.get("reasoning_content") or "").strip()
|
||||||
|
if rc:
|
||||||
|
candidates.append(rc)
|
||||||
|
for text in candidates:
|
||||||
|
instr = _extract_manager_instruction_text(text)
|
||||||
|
if instr:
|
||||||
|
instruction_texts.add(instr)
|
||||||
if not instruction_texts:
|
if not instruction_texts:
|
||||||
return msgs
|
return msgs
|
||||||
filtered: list[Any] = []
|
filtered: list[Any] = []
|
||||||
|
|
|
||||||
|
|
@ -1008,7 +1008,6 @@ def _persist_assistant_step(
|
||||||
assistant_text: str,
|
assistant_text: str,
|
||||||
reasoning_text: str,
|
reasoning_text: str,
|
||||||
llm_tool_calls: list[Any],
|
llm_tool_calls: list[Any],
|
||||||
thinking_mode_enabled: bool = False,
|
|
||||||
) -> _LoopStepResult:
|
) -> _LoopStepResult:
|
||||||
stored_tool_calls = []
|
stored_tool_calls = []
|
||||||
for tc in llm_tool_calls:
|
for tc in llm_tool_calls:
|
||||||
|
|
@ -1025,16 +1024,11 @@ def _persist_assistant_step(
|
||||||
reasoning_full = "\n".join([str(x or "").strip() for x in reasoning_chunks if str(x or "").strip()]).strip()
|
reasoning_full = "\n".join([str(x or "").strip() for x in reasoning_chunks if str(x or "").strip()]).strip()
|
||||||
# Keep empty body as-is when model returns nothing and there are no tool calls.
|
# Keep empty body as-is when model returns nothing and there are no tool calls.
|
||||||
# The UI should treat this as an invisible intermediate/final empty response.
|
# The UI should treat this as an invisible intermediate/final empty response.
|
||||||
if not thinking_mode_enabled:
|
#
|
||||||
for idx, chunk in enumerate(reasoning_chunks):
|
# Persist full reasoning on the primary assistant row's ``event_payload.reasoning_content`` for
|
||||||
store.add_message(
|
# both thinking and non-thinking profiles. Previously non-thinking used separate
|
||||||
session_id=session_id,
|
# ``event_type=reasoning`` rows with only chunk_index in payload, so JSON inspection looked
|
||||||
role="assistant",
|
# like "no reasoning_content anywhere" while tools carried rich ``event_payload``.
|
||||||
content=chunk,
|
|
||||||
turn_uuid=turn_uuid,
|
|
||||||
event_type="reasoning",
|
|
||||||
event_payload={"chunk_index": int(idx), "chunk_count": len(reasoning_chunks)},
|
|
||||||
)
|
|
||||||
assistant_row = store.add_message(
|
assistant_row = store.add_message(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
role="assistant",
|
role="assistant",
|
||||||
|
|
@ -1042,7 +1036,7 @@ def _persist_assistant_step(
|
||||||
tool_calls=stored_tool_calls or None,
|
tool_calls=stored_tool_calls or None,
|
||||||
turn_uuid=turn_uuid,
|
turn_uuid=turn_uuid,
|
||||||
event_type="tool_call" if stored_tool_calls else "assistant_text",
|
event_type="tool_call" if stored_tool_calls else "assistant_text",
|
||||||
event_payload=({"reasoning_content": reasoning_full} if (thinking_mode_enabled and reasoning_full) else None),
|
event_payload=({"reasoning_content": reasoning_full} if reasoning_full else None),
|
||||||
)
|
)
|
||||||
return _LoopStepResult(
|
return _LoopStepResult(
|
||||||
assistant_text=assistant_body,
|
assistant_text=assistant_body,
|
||||||
|
|
@ -1453,7 +1447,6 @@ def run_oclaw_direct_loop(
|
||||||
assistant_text=assistant_text,
|
assistant_text=assistant_text,
|
||||||
reasoning_text=reasoning_text,
|
reasoning_text=reasoning_text,
|
||||||
llm_tool_calls=llm_tool_calls,
|
llm_tool_calls=llm_tool_calls,
|
||||||
thinking_mode_enabled=bool(getattr(model, "thinking_mode_enabled", False)),
|
|
||||||
)
|
)
|
||||||
final_text = step.assistant_text
|
final_text = step.assistant_text
|
||||||
if not step.llm_tool_calls:
|
if not step.llm_tool_calls:
|
||||||
|
|
@ -1559,7 +1552,6 @@ def run_oclaw_direct_loop(
|
||||||
assistant_text=str(getattr(resp, "content", "") or ""),
|
assistant_text=str(getattr(resp, "content", "") or ""),
|
||||||
reasoning_text=str(getattr(resp, "reasoning_content", "") or ""),
|
reasoning_text=str(getattr(resp, "reasoning_content", "") or ""),
|
||||||
llm_tool_calls=[],
|
llm_tool_calls=[],
|
||||||
thinking_mode_enabled=bool(getattr(model, "thinking_mode_enabled", False)),
|
|
||||||
)
|
)
|
||||||
final_text = step.assistant_text
|
final_text = step.assistant_text
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,22 @@ def test_filter_internal_instruction_user_messages_hides_polluted_user_row() ->
|
||||||
assert [str(getattr(x, "event_type", "")) for x in out] == ["reasoning", "assistant_text"]
|
assert [str(getattr(x, "event_type", "")) for x in out] == ["reasoning", "assistant_text"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_internal_instruction_user_messages_hides_from_assistant_event_payload() -> None:
|
||||||
|
polluted_text = "请检查并修复网关启动失败。"
|
||||||
|
rows = [
|
||||||
|
SimpleNamespace(role="user", event_type="user_text", content=polluted_text),
|
||||||
|
SimpleNamespace(
|
||||||
|
role="assistant",
|
||||||
|
event_type="assistant_text",
|
||||||
|
content="已修复",
|
||||||
|
event_payload={"reasoning_content": f"任务分配\nspecialist=ops\ninstruction:\n{polluted_text}"},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
out = _filter_internal_instruction_user_messages(rows)
|
||||||
|
assert len(out) == 1
|
||||||
|
assert str(getattr(out[0], "event_type", "")) == "assistant_text"
|
||||||
|
|
||||||
|
|
||||||
def test_filter_internal_instruction_user_messages_keeps_normal_user_rows() -> None:
|
def test_filter_internal_instruction_user_messages_keeps_normal_user_rows() -> None:
|
||||||
rows = [
|
rows = [
|
||||||
SimpleNamespace(role="user", event_type="user_text", content="你好"),
|
SimpleNamespace(role="user", event_type="user_text", content="你好"),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue