Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions loopx/chat_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,7 @@ def _turn_prompt(
if context_summary.strip()
else ""
)
+ f"Operator user message:\n{user_message.strip()}"
+ f"Operator user message:\n{user_message}"
)


Expand Down Expand Up @@ -1696,7 +1696,7 @@ def send(
on_native_item: Callable[[dict[str, Any]], None] | None = None,
turn_start_guard: Callable[[], AbstractContextManager[None]] | None = None,
) -> dict[str, Any]:
text = str(user_message or "") if project_collaboration else " ".join(str(user_message or "").split())
text = str(user_message or "")
if not text.strip():
raise ValueError("user message is required")
if self.read_only_review:
Expand Down
32 changes: 32 additions & 0 deletions tests/test_chat_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,38 @@
import pytest


@pytest.mark.parametrize("mode", ["chat", "execution", "structured", "review"])
def test_native_turn_preserves_operator_material(monkeypatch, tmp_path, mode):
material = " 修复下面的代码,保留字面值:\r\n```python\r\nif ready:\r\n\tprint('a b')\r\n```\n\nname:\u00a0value \n"
session = chat_agent.CodexChatAgentSession(
process=SimpleNamespace(poll=lambda: None), messages=queue.Queue(),
thread_id="material-thread", work_dir=tmp_path,
execution_mode=mode in {"execution", "structured"}, read_only_review=mode == "review",
)
sent = []

def write(packet):
assert packet["method"] == "turn/start"
sent.append(packet["params"])
session.messages.put({"id": packet["id"], "result": {"turn": {"id": "material-turn"}}})
session.messages.put({"method": "item/completed", "params": {"item": {
"type": "agentMessage", "phase": "final_answer", "text": '{"answer":"received"}'}}})
session.messages.put({"method": "turn/completed", "params": {"turn": {"status": "completed"}}})

monkeypatch.setattr(session, "_write", write)
session.send(material, output_schema={"type": "object"} if mode == "structured" else None)
prompt = sent[0]["input"][0]["text"]
assert prompt.endswith(material)
if mode == "structured":
assert prompt == material
if mode == "review":
assert "do not re-execute" in prompt
for blank in ["", " \r\n\t\u00a0"]:
with pytest.raises(ValueError, match="user message is required"):
session.send(blank)
assert len(sent) == 1


@pytest.mark.parametrize("fault", [None, "active", "completed", "final", "missing-phase", "correction",
"unknown", "foreign", "duplicate", "live-tool", "late-items", "late-turn"])
def test_interrupted_result_review_requires_stable_full_inventory_without_final_or_live_effects(tmp_path, monkeypatch, fault):
Expand Down
34 changes: 34 additions & 0 deletions tests/test_chat_queue_preparation.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,40 @@ def session_runtime(tmp_path, *, channel="goal.public-research"):
)


def test_restarted_queue_delivers_original_material_to_native_turn(tmp_path, monkeypatch):
import queue
from types import SimpleNamespace
from loopx.chat_agent import CodexChatAgentSession

store, sid, _ = session_runtime(tmp_path)
material = "Explain this exact source:\n```python\nif ready:\n print('a b')\n```\n\n原文:a\u00a0b"
original, _ = store.create_queued_turn(
sid, client_turn_id="material-request", message=material, origin="web")
store = ChatSessionStore(tmp_path)
runtime = ChatRuntimeController(store=store, codex_bin="unused-test-codex")
native = CodexChatAgentSession(process=SimpleNamespace(poll=lambda: None),
messages=queue.Queue(), thread_id="public-thread", work_dir=tmp_path)
sent = []

def write(packet):
assert packet["method"] == "turn/start"
sent.append(packet["params"])
native.messages.put({"id": packet["id"], "result": {"turn": {"id": "native-material"}}})
native.messages.put({"method": "item/completed", "params": {"item": {
"type": "agentMessage", "phase": "final_answer", "text": "Received the source."}}})
native.messages.put({"method": "turn/completed", "params": {"turn": {"status": "completed"}}})

monkeypatch.setattr(native, "_write", write)
monkeypatch.setattr(runtime, "_ensure_adapter", lambda *a, **kw: CodexAppServerAdapter(native))
runtime.resume_session_queue(session_id=sid, work_dir=tmp_path, objective="Explain source")
result = runtime.wait_for_turn(session_id=sid, turn_id=original["turn_id"], timeout_sec=10)
assert result["status"] == "completed"
assert len(sent) == 1 and sent[0]["threadId"] == "public-thread"
assert sent[0]["input"][0]["text"].endswith(material)
assert store.turn_for_client(sid, "material-request")["message"] == material
assert store.messages(sid)[0]["text"] == material


@pytest.mark.parametrize("failure", ["before_message", "after_message", "before_event"])
@pytest.mark.parametrize("display", ["Execution request", "Original visible request 原文"])
def test_queue_replay_leaves_prepared_managed_input_with_its_owner(tmp_path, monkeypatch, failure, display):
Expand Down
Loading