Skip to content
Closed
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
9 changes: 8 additions & 1 deletion agent/codex_responses_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,6 @@ def _normalize_codex_response(response: Any) -> tuple[Any, str]:
saw_final_answer_phase = True
message_text = _extract_responses_message_text(item)
if message_text:
content_parts.append(message_text)
raw_message_item: Dict[str, Any] = {
"type": "message",
"role": "assistant",
Expand All @@ -865,6 +864,14 @@ def _normalize_codex_response(response: Any) -> tuple[Any, str]:
if normalized_phase:
raw_message_item["phase"] = normalized_phase
message_items_raw.append(raw_message_item)

# Codex backends can emit user-invisible planning/status text as
# `message` items with phase=commentary/analysis while also
# emitting structured function_call items. Preserve the raw item
# above for replay/debug metadata, but never promote it into
# assistant.content/final_response/gateway-visible text.
if normalized_phase not in {"commentary", "analysis"}:
content_parts.append(message_text)
elif item_type == "reasoning":
reasoning_text = _extract_responses_reasoning_text(item)
if reasoning_text:
Expand Down
2 changes: 1 addition & 1 deletion run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -6682,7 +6682,7 @@ def _emit_interim_assistant_message(self, assistant_msg: Dict[str, Any]) -> None
if cb is None or not isinstance(assistant_msg, dict):
return
content = assistant_msg.get("content")
visible = self._strip_think_blocks(content or "").strip()
visible = sanitize_context(self._strip_think_blocks(content or "")).strip()
if not visible or visible == "(empty)":
return
already_streamed = self._interim_content_was_streamed(visible)
Expand Down
72 changes: 59 additions & 13 deletions tests/run_agent/test_run_agent_codex_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ def test_run_conversation_codex_tool_round_trip(monkeypatch):
responses = [_codex_tool_call_response(), _codex_message_response("done")]
monkeypatch.setattr(agent, "_interruptible_api_call", lambda api_kwargs: responses.pop(0))

def _fake_execute_tool_calls(assistant_message, messages, effective_task_id):
def _fake_execute_tool_calls(assistant_message, messages, effective_task_id, *args, **kwargs):
for call in assistant_message.tool_calls:
messages.append(
{
Expand Down Expand Up @@ -872,7 +872,7 @@ def _fake_api_call(api_kwargs):

monkeypatch.setattr(agent, "_interruptible_api_call", _fake_api_call)

def _fake_execute_tool_calls(assistant_message, messages, effective_task_id):
def _fake_execute_tool_calls(assistant_message, messages, effective_task_id, *args, **kwargs):
for call in assistant_message.tool_calls:
messages.append(
{
Expand Down Expand Up @@ -907,7 +907,7 @@ def test_run_conversation_codex_continues_after_incomplete_interim_message(monke
]
monkeypatch.setattr(agent, "_interruptible_api_call", lambda api_kwargs: responses.pop(0))

def _fake_execute_tool_calls(assistant_message, messages, effective_task_id):
def _fake_execute_tool_calls(assistant_message, messages, effective_task_id, *args, **kwargs):
for call in assistant_message.tool_calls:
messages.append(
{
Expand Down Expand Up @@ -940,7 +940,8 @@ def test_normalize_codex_response_marks_commentary_only_message_as_incomplete(mo
)

assert finish_reason == "incomplete"
assert "inspect the repository" in (assistant_message.content or "")
assert (assistant_message.content or "") == ""
assert "inspect the repository" in assistant_message.codex_message_items[0]["content"][0]["text"]


def test_normalize_codex_response_preserves_message_status_for_replay(monkeypatch):
Expand Down Expand Up @@ -1010,6 +1011,49 @@ def test_normalize_codex_response_detects_leaked_tool_call_text(monkeypatch):
assert assistant_message.tool_calls == []


def test_normalize_codex_response_scrubs_commentary_text_when_tool_call_present(monkeypatch):
"""Codex commentary is debug/replay metadata, not user-visible content.

This is the Discord leak failure mode: a Responses output contains a
phase=commentary message like "Need inspect..." plus a real function_call.
Tool calls must still run, but commentary text must not become the assistant
content that gateways can stream or deliver.
"""
agent = _build_agent(monkeypatch)
from agent.codex_responses_adapter import _normalize_codex_response

leaked_commentary = "Need inspect core context files. Use search for SOUL/AGENTS/MEMORY."
response = SimpleNamespace(
output=[
SimpleNamespace(
type="message",
id="msg_commentary",
phase="commentary",
status="completed",
content=[SimpleNamespace(type="output_text", text=leaked_commentary)],
),
SimpleNamespace(
type="function_call",
id="fc_1",
call_id="call_1",
name="search_files",
arguments='{"pattern":"SOUL.md"}',
),
],
usage=SimpleNamespace(input_tokens=4, output_tokens=2, total_tokens=6),
status="completed",
model="gpt-5.5",
)

assistant_message, finish_reason = _normalize_codex_response(response)

assert finish_reason == "tool_calls"
assert assistant_message.tool_calls
assert (assistant_message.content or "") == ""
assert assistant_message.codex_message_items[0]["phase"] == "commentary"
assert leaked_commentary in assistant_message.codex_message_items[0]["content"][0]["text"]


def test_normalize_codex_response_ignores_tool_call_text_when_real_tool_call_present(monkeypatch):
"""If the model emitted BOTH a structured function_call AND some text that
happens to contain `to=functions.*` (unlikely but possible), trust the
Expand Down Expand Up @@ -1115,10 +1159,9 @@ def failing_callback(_text):
}


def test_interim_commentary_preserves_assistant_content(monkeypatch):
"""Interim commentary must not silently mutate assistant text containing
literal <memory-context> markers — that's legitimate model output (docs,
code). Streaming-path leak prevention happens delta-by-delta upstream."""
def test_interim_commentary_strips_leaked_memory_context(monkeypatch):
"""Interim commentary is user-visible in gateways, so leaked injected
context fences must be stripped before callback delivery."""
agent = _build_agent(monkeypatch)
observed = {}
agent.interim_assistant_callback = lambda text, *, already_streamed=False: observed.update(
Expand All @@ -1136,7 +1179,9 @@ def test_interim_commentary_preserves_assistant_content(monkeypatch):

agent._emit_interim_assistant_message({"role": "assistant", "content": content})

assert "<memory-context>" in observed["text"]
assert "<memory-context>" not in observed["text"]
assert "Honcho Context" not in observed["text"]
assert "stale memory" not in observed["text"]
assert "I'll inspect the repo structure first." in observed["text"]


Expand Down Expand Up @@ -1259,7 +1304,7 @@ def test_run_conversation_codex_continues_after_commentary_phase_message(monkeyp
]
monkeypatch.setattr(agent, "_interruptible_api_call", lambda api_kwargs: responses.pop(0))

def _fake_execute_tool_calls(assistant_message, messages, effective_task_id):
def _fake_execute_tool_calls(assistant_message, messages, effective_task_id, *args, **kwargs):
for call in assistant_message.tool_calls:
messages.append(
{
Expand All @@ -1278,7 +1323,8 @@ def _fake_execute_tool_calls(assistant_message, messages, effective_task_id):
assert any(
msg.get("role") == "assistant"
and msg.get("finish_reason") == "incomplete"
and "inspect the repo structure" in (msg.get("content") or "")
and not (msg.get("content") or "")
and "inspect the repo structure" in str(msg.get("codex_message_items") or "")
for msg in result["messages"]
)
assert any(msg.get("role") == "tool" and msg.get("tool_call_id") == "call_1" for msg in result["messages"])
Expand All @@ -1295,7 +1341,7 @@ def test_run_conversation_codex_continues_after_ack_stop_message(monkeypatch):
]
monkeypatch.setattr(agent, "_interruptible_api_call", lambda api_kwargs: responses.pop(0))

def _fake_execute_tool_calls(assistant_message, messages, effective_task_id):
def _fake_execute_tool_calls(assistant_message, messages, effective_task_id, *args, **kwargs):
for call in assistant_message.tool_calls:
messages.append(
{
Expand Down Expand Up @@ -1336,7 +1382,7 @@ def test_run_conversation_codex_continues_after_ack_for_directory_listing_prompt
]
monkeypatch.setattr(agent, "_interruptible_api_call", lambda api_kwargs: responses.pop(0))

def _fake_execute_tool_calls(assistant_message, messages, effective_task_id):
def _fake_execute_tool_calls(assistant_message, messages, effective_task_id, *args, **kwargs):
for call in assistant_message.tool_calls:
messages.append(
{
Expand Down