mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
feat(agent): stream Codex commentary separately
This commit is contained in:
parent
b008131b54
commit
7041c56cdf
5 changed files with 396 additions and 17 deletions
|
|
@ -743,6 +743,10 @@ def init_agent(
|
|||
# commentary when the provider later returns it as a completed interim
|
||||
# assistant message.
|
||||
agent._current_streamed_assistant_text = ""
|
||||
# Completed interim messages delivered during the current user turn.
|
||||
# Unlike token-stream tracking, this spans Codex continuation/tool calls so
|
||||
# repeated commentary is not re-sent before normalization can deduplicate it.
|
||||
agent._delivered_interim_texts: set[str] = set()
|
||||
|
||||
# Optional current-turn user-message override used when the API-facing
|
||||
# user message intentionally differs from the persisted transcript
|
||||
|
|
|
|||
|
|
@ -624,6 +624,7 @@ def _consume_codex_event_stream(
|
|||
model: str,
|
||||
on_text_delta=None,
|
||||
on_reasoning_delta=None,
|
||||
on_commentary_message=None,
|
||||
on_first_delta=None,
|
||||
on_event=None,
|
||||
interrupt_check=None,
|
||||
|
|
@ -655,7 +656,11 @@ def _consume_codex_event_stream(
|
|||
* ``on_text_delta(str)`` — fires per ``response.output_text.delta``, suppressed
|
||||
once a function_call event is seen (so tool-call turns don't bleed text
|
||||
into the chat).
|
||||
* ``on_reasoning_delta(str)`` — fires per ``response.reasoning.*.delta``.
|
||||
* ``on_reasoning_delta(str)`` — fires per ``response.reasoning.*.delta`` and
|
||||
``phase=analysis`` message deltas. When no dedicated commentary callback
|
||||
is supplied, commentary also uses this legacy fallback.
|
||||
* ``on_commentary_message(str)`` — fires once per completed
|
||||
``phase=commentary`` message, before any following tool item executes.
|
||||
* ``on_first_delta()`` — one-shot, fires on the first text delta only.
|
||||
* ``on_event(event)`` — fires for every event before any other processing.
|
||||
Used for watchdog activity, debug logging, anything wire-shape-agnostic.
|
||||
|
|
@ -666,6 +671,7 @@ def _consume_codex_event_stream(
|
|||
has_tool_calls = False
|
||||
first_delta_fired = False
|
||||
active_message_phase: str | None = None
|
||||
commentary_text_deltas: List[str] = []
|
||||
terminal_status: str = "completed"
|
||||
terminal_usage: Any = None
|
||||
terminal_response_id: str = None
|
||||
|
|
@ -710,6 +716,8 @@ def _consume_codex_event_stream(
|
|||
if item_type == "message":
|
||||
phase = _item_field(item, "phase", None)
|
||||
active_message_phase = phase.strip().lower() if isinstance(phase, str) else None
|
||||
if active_message_phase == "commentary":
|
||||
commentary_text_deltas = []
|
||||
else:
|
||||
active_message_phase = None
|
||||
if "function_call" in str(item_type):
|
||||
|
|
@ -718,10 +726,16 @@ def _consume_codex_event_stream(
|
|||
|
||||
if "output_text.delta" in event_type or event_type == "response.output_text.delta":
|
||||
delta_text = _event_field(event, "delta", "")
|
||||
is_commentary_delta = active_message_phase in {"commentary", "analysis"}
|
||||
if delta_text and is_commentary_delta:
|
||||
# Commentary streams through the reasoning channel, not the
|
||||
# visible answer stream (and stays out of output_text).
|
||||
if delta_text and active_message_phase == "commentary":
|
||||
commentary_text_deltas.append(delta_text)
|
||||
# Preserve CLI/backward compatibility when no first-class
|
||||
# commentary consumer is installed.
|
||||
if on_commentary_message is None and on_reasoning_delta is not None:
|
||||
try:
|
||||
on_reasoning_delta(delta_text)
|
||||
except Exception:
|
||||
logger.debug("Codex stream on_reasoning_delta raised", exc_info=True)
|
||||
elif delta_text and active_message_phase == "analysis":
|
||||
if on_reasoning_delta is not None:
|
||||
try:
|
||||
on_reasoning_delta(delta_text)
|
||||
|
|
@ -761,6 +775,27 @@ def _consume_codex_event_stream(
|
|||
done_item = _event_field(event, "item")
|
||||
if done_item is not None:
|
||||
collected_output_items.append(done_item)
|
||||
done_phase = _item_field(done_item, "phase", None)
|
||||
done_phase = done_phase.strip().lower() if isinstance(done_phase, str) else None
|
||||
if done_phase == "commentary" and on_commentary_message is not None:
|
||||
commentary_text = "".join(commentary_text_deltas).strip()
|
||||
if not commentary_text:
|
||||
content_parts = _item_field(done_item, "content", [])
|
||||
if isinstance(content_parts, list):
|
||||
commentary_text = "".join(
|
||||
str(_item_field(part, "text", "") or "")
|
||||
for part in content_parts
|
||||
if _item_field(part, "type", "") == "output_text"
|
||||
).strip()
|
||||
if commentary_text:
|
||||
try:
|
||||
on_commentary_message(commentary_text)
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"Codex stream on_commentary_message raised",
|
||||
exc_info=True,
|
||||
)
|
||||
commentary_text_deltas = []
|
||||
continue
|
||||
|
||||
if event_type in _TERMINAL_EVENT_TYPES:
|
||||
|
|
@ -861,6 +896,9 @@ def run_codex_stream(agent, api_kwargs: dict, client: Any = None, on_first_delta
|
|||
def _on_reasoning_delta(text: str) -> None:
|
||||
agent._fire_reasoning_delta(text)
|
||||
|
||||
def _on_commentary_message(text: str) -> None:
|
||||
agent._fire_streamed_codex_commentary(text)
|
||||
|
||||
def _on_event(event: Any) -> None:
|
||||
# TTFB watchdog and activity touch — runs once per SSE event.
|
||||
agent._codex_stream_last_event_ts = time.time()
|
||||
|
|
@ -900,6 +938,11 @@ def run_codex_stream(agent, api_kwargs: dict, client: Any = None, on_first_delta
|
|||
model=api_kwargs.get("model"),
|
||||
on_text_delta=_on_text_delta,
|
||||
on_reasoning_delta=_on_reasoning_delta,
|
||||
on_commentary_message=(
|
||||
_on_commentary_message
|
||||
if getattr(agent, "interim_assistant_callback", None) is not None
|
||||
else None
|
||||
),
|
||||
on_first_delta=on_first_delta,
|
||||
on_event=_on_event,
|
||||
interrupt_check=_interrupt_check,
|
||||
|
|
|
|||
|
|
@ -616,6 +616,10 @@ def run_conversation(
|
|||
_plugin_user_context = _ctx.plugin_user_context
|
||||
_ext_prefetch_cache = _ctx.ext_prefetch_cache
|
||||
|
||||
# Commentary deduplication spans all provider continuations and tool calls
|
||||
# within one user turn, but must not suppress the same phrase next turn.
|
||||
agent._delivered_interim_texts = set()
|
||||
|
||||
# Main conversation loop counters (pure locals consumed by the loop below).
|
||||
api_call_count = 0
|
||||
final_response = None
|
||||
|
|
|
|||
92
run_agent.py
92
run_agent.py
|
|
@ -4754,8 +4754,11 @@ class AIAgent:
|
|||
)
|
||||
return bool(streamed) and streamed == visible_content
|
||||
|
||||
def _extract_codex_interim_visible_text(self, assistant_msg: Dict[str, Any]) -> str:
|
||||
"""Extract visible Codex Responses commentary text.
|
||||
def _extract_codex_interim_visible_parts(
|
||||
self,
|
||||
assistant_msg: Dict[str, Any],
|
||||
) -> List[str]:
|
||||
"""Extract visible Codex commentary as one string per message item.
|
||||
|
||||
Codex Responses can keep user-facing mid-turn narration as structured
|
||||
``phase=commentary`` message items while final answer text remains in
|
||||
|
|
@ -4765,9 +4768,9 @@ class AIAgent:
|
|||
"""
|
||||
items = assistant_msg.get("codex_message_items")
|
||||
if not isinstance(items, list):
|
||||
return ""
|
||||
return []
|
||||
|
||||
parts: List[str] = []
|
||||
messages: List[str] = []
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
|
|
@ -4779,6 +4782,7 @@ class AIAgent:
|
|||
content_parts = item.get("content")
|
||||
if not isinstance(content_parts, list):
|
||||
continue
|
||||
item_parts: List[str] = []
|
||||
for part in content_parts:
|
||||
if not isinstance(part, dict):
|
||||
continue
|
||||
|
|
@ -4786,13 +4790,20 @@ class AIAgent:
|
|||
continue
|
||||
text = part.get("text")
|
||||
if isinstance(text, str) and text.strip():
|
||||
parts.append(text.strip())
|
||||
item_parts.append(text)
|
||||
visible = "".join(item_parts).strip()
|
||||
if visible:
|
||||
visible = self._strip_think_blocks(visible).strip()
|
||||
visible = redact_sensitive_text(visible)
|
||||
if visible:
|
||||
messages.append(visible)
|
||||
return messages
|
||||
|
||||
visible = "\n\n".join(parts).strip()
|
||||
if visible:
|
||||
visible = self._strip_think_blocks(visible).strip()
|
||||
visible = redact_sensitive_text(visible)
|
||||
return visible
|
||||
def _extract_codex_interim_visible_text(self, assistant_msg: Dict[str, Any]) -> str:
|
||||
"""Extract all visible Codex commentary for comparison/fallback."""
|
||||
return "\n\n".join(
|
||||
self._extract_codex_interim_visible_parts(assistant_msg)
|
||||
).strip()
|
||||
|
||||
def _interim_assistant_visible_text(self, assistant_msg: Dict[str, Any]) -> str:
|
||||
"""Return the exact assistant text eligible for interim delivery.
|
||||
|
|
@ -4808,17 +4819,74 @@ class AIAgent:
|
|||
content = assistant_msg.get("content")
|
||||
return self._strip_think_blocks(content or "").strip()
|
||||
|
||||
def _interim_text_was_delivered(self, text: str) -> bool:
|
||||
normalized = self._normalize_interim_visible_text(text)
|
||||
if not normalized:
|
||||
return False
|
||||
return normalized in getattr(self, "_delivered_interim_texts", set())
|
||||
|
||||
def _record_delivered_interim_text(self, text: str) -> None:
|
||||
normalized = self._normalize_interim_visible_text(text)
|
||||
if normalized:
|
||||
delivered = getattr(self, "_delivered_interim_texts", None)
|
||||
if not isinstance(delivered, set):
|
||||
delivered = set()
|
||||
self._delivered_interim_texts = delivered
|
||||
delivered.add(normalized)
|
||||
|
||||
def _fire_streamed_codex_commentary(self, text: str) -> None:
|
||||
"""Deliver a completed live Codex commentary message immediately."""
|
||||
cb = getattr(self, "interim_assistant_callback", None)
|
||||
if cb is None or not isinstance(text, str):
|
||||
return
|
||||
visible = self._strip_think_blocks(text).strip()
|
||||
if visible:
|
||||
visible = redact_sensitive_text(visible)
|
||||
if not visible or visible == "(empty)" or self._interim_text_was_delivered(visible):
|
||||
return
|
||||
try:
|
||||
cb(visible, already_streamed=False)
|
||||
self._record_delivered_interim_text(visible)
|
||||
except Exception:
|
||||
logger.debug("interim_assistant_callback error", exc_info=True)
|
||||
|
||||
def _emit_interim_assistant_message(self, assistant_msg: Dict[str, Any]) -> None:
|
||||
"""Surface a real mid-turn assistant commentary message to the UI layer."""
|
||||
cb = getattr(self, "interim_assistant_callback", None)
|
||||
if cb is None or not isinstance(assistant_msg, dict):
|
||||
return
|
||||
visible = self._interim_assistant_visible_text(assistant_msg)
|
||||
if not visible or visible == "(empty)":
|
||||
commentary_parts = self._extract_codex_interim_visible_parts(assistant_msg)
|
||||
undelivered_parts: List[str] = []
|
||||
pending_keys: set[str] = set()
|
||||
for part in commentary_parts:
|
||||
key = self._normalize_interim_visible_text(part)
|
||||
if (
|
||||
not key
|
||||
or key in pending_keys
|
||||
or self._interim_text_was_delivered(part)
|
||||
):
|
||||
continue
|
||||
pending_keys.add(key)
|
||||
undelivered_parts.append(part)
|
||||
visible = (
|
||||
"\n\n".join(undelivered_parts).strip()
|
||||
if commentary_parts
|
||||
else self._interim_assistant_visible_text(assistant_msg)
|
||||
)
|
||||
if (
|
||||
not visible
|
||||
or visible == "(empty)"
|
||||
or self._interim_text_was_delivered(visible)
|
||||
):
|
||||
return
|
||||
already_streamed = self._interim_content_was_streamed(visible)
|
||||
try:
|
||||
cb(visible, already_streamed=already_streamed)
|
||||
if undelivered_parts:
|
||||
for part in undelivered_parts:
|
||||
self._record_delivered_interim_text(part)
|
||||
else:
|
||||
self._record_delivered_interim_text(visible)
|
||||
except Exception:
|
||||
logger.debug("interim_assistant_callback error", exc_info=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -704,6 +704,49 @@ def test_consume_codex_stream_routes_commentary_phase_deltas_to_reasoning(monkey
|
|||
assert response.output_text == ""
|
||||
|
||||
|
||||
def test_consume_codex_stream_separates_commentary_from_analysis(monkeypatch):
|
||||
from agent.codex_runtime import _consume_codex_event_stream
|
||||
|
||||
commentary_item = SimpleNamespace(
|
||||
type="message",
|
||||
phase="commentary",
|
||||
status="completed",
|
||||
content=[SimpleNamespace(type="output_text", text="I'll inspect the repo first.")],
|
||||
)
|
||||
streamed = []
|
||||
reasoning_streamed = []
|
||||
commentary_messages = []
|
||||
|
||||
response = _consume_codex_event_stream(
|
||||
_FakeCreateStream([
|
||||
SimpleNamespace(
|
||||
type="response.output_item.added",
|
||||
item=SimpleNamespace(type="message", phase="commentary"),
|
||||
),
|
||||
SimpleNamespace(type="response.output_text.delta", delta="I'll inspect "),
|
||||
SimpleNamespace(type="response.output_text.delta", delta="the repo first."),
|
||||
SimpleNamespace(type="response.output_item.done", item=commentary_item),
|
||||
SimpleNamespace(
|
||||
type="response.reasoning_text.delta",
|
||||
delta="Need inspect files privately.",
|
||||
),
|
||||
SimpleNamespace(
|
||||
type="response.completed",
|
||||
response=SimpleNamespace(status="completed"),
|
||||
),
|
||||
]),
|
||||
model="gpt-5-codex",
|
||||
on_text_delta=streamed.append,
|
||||
on_reasoning_delta=reasoning_streamed.append,
|
||||
on_commentary_message=commentary_messages.append,
|
||||
)
|
||||
|
||||
assert commentary_messages == ["I'll inspect the repo first."]
|
||||
assert reasoning_streamed == ["Need inspect files privately."]
|
||||
assert streamed == []
|
||||
assert response.output == [commentary_item]
|
||||
|
||||
|
||||
def test_consume_codex_stream_keeps_final_answer_phase_deltas(monkeypatch):
|
||||
from agent.codex_runtime import _consume_codex_event_stream
|
||||
|
||||
|
|
@ -726,6 +769,201 @@ def test_consume_codex_stream_keeps_final_answer_phase_deltas(monkeypatch):
|
|||
assert response.output_text == "visible answer"
|
||||
|
||||
|
||||
def test_run_codex_stream_delivers_redacted_commentary_once(monkeypatch):
|
||||
from agent.codex_responses_adapter import _normalize_codex_response
|
||||
|
||||
agent = _build_agent(monkeypatch)
|
||||
monkeypatch.setattr("agent.redact._REDACT_ENABLED", True)
|
||||
delivered = []
|
||||
reasoning_streamed = []
|
||||
agent.interim_assistant_callback = (
|
||||
lambda text, *, already_streamed=False: delivered.append(
|
||||
(text, already_streamed)
|
||||
)
|
||||
)
|
||||
agent.reasoning_callback = reasoning_streamed.append
|
||||
secret = "sk-" + ("A" * 32)
|
||||
commentary_text = f"Using credential {secret}. I'll inspect the repo."
|
||||
commentary_item = SimpleNamespace(
|
||||
type="message",
|
||||
phase="commentary",
|
||||
status="completed",
|
||||
content=[SimpleNamespace(type="output_text", text=commentary_text)],
|
||||
)
|
||||
function_item = SimpleNamespace(
|
||||
type="function_call",
|
||||
id="fc_1",
|
||||
call_id="call_1",
|
||||
name="terminal",
|
||||
arguments="{}",
|
||||
)
|
||||
|
||||
def _fake_create(**kwargs):
|
||||
assert kwargs.get("stream") is True
|
||||
return _FakeCreateStream([
|
||||
SimpleNamespace(
|
||||
type="response.output_item.added",
|
||||
item=SimpleNamespace(type="message", phase="commentary"),
|
||||
),
|
||||
SimpleNamespace(type="response.output_text.delta", delta=commentary_text),
|
||||
SimpleNamespace(type="response.output_item.done", item=commentary_item),
|
||||
SimpleNamespace(type="response.reasoning_text.delta", delta="Private scratchpad."),
|
||||
SimpleNamespace(
|
||||
type="response.output_item.added",
|
||||
item=SimpleNamespace(type="function_call"),
|
||||
),
|
||||
SimpleNamespace(type="response.output_item.done", item=function_item),
|
||||
SimpleNamespace(
|
||||
type="response.completed",
|
||||
response=SimpleNamespace(status="completed"),
|
||||
),
|
||||
])
|
||||
|
||||
agent.client = SimpleNamespace(responses=SimpleNamespace(create=_fake_create))
|
||||
|
||||
response = agent._run_codex_stream(_codex_request_kwargs())
|
||||
|
||||
assert len(delivered) == 1
|
||||
assert delivered[0][1] is False
|
||||
assert secret not in delivered[0][0]
|
||||
assert "Using credential" in delivered[0][0]
|
||||
assert reasoning_streamed == ["Private scratchpad."]
|
||||
|
||||
# The completed-response fallback sees the same preserved commentary but
|
||||
# must not enqueue it again after live delivery.
|
||||
normalized, finish_reason = _normalize_codex_response(response)
|
||||
agent._emit_interim_assistant_message(
|
||||
agent._build_assistant_message(normalized, finish_reason)
|
||||
)
|
||||
assert len(delivered) == 1
|
||||
|
||||
|
||||
def test_run_codex_stream_multiple_commentary_items_are_not_reemitted(monkeypatch):
|
||||
from agent.codex_responses_adapter import _normalize_codex_response
|
||||
|
||||
agent = _build_agent(monkeypatch)
|
||||
delivered = []
|
||||
agent.interim_assistant_callback = (
|
||||
lambda text, *, already_streamed=False: delivered.append(text)
|
||||
)
|
||||
commentary_a = SimpleNamespace(
|
||||
type="message",
|
||||
phase="commentary",
|
||||
status="completed",
|
||||
content=[SimpleNamespace(type="output_text", text="First update.")],
|
||||
)
|
||||
commentary_b = SimpleNamespace(
|
||||
type="message",
|
||||
phase="commentary",
|
||||
status="completed",
|
||||
content=[SimpleNamespace(type="output_text", text="Second update.")],
|
||||
)
|
||||
function_item = SimpleNamespace(
|
||||
type="function_call",
|
||||
id="fc_1",
|
||||
call_id="call_1",
|
||||
name="terminal",
|
||||
arguments="{}",
|
||||
)
|
||||
|
||||
def _fake_create(**kwargs):
|
||||
return _FakeCreateStream([
|
||||
SimpleNamespace(
|
||||
type="response.output_item.added",
|
||||
item=SimpleNamespace(type="message", phase="commentary"),
|
||||
),
|
||||
SimpleNamespace(type="response.output_text.delta", delta="First update."),
|
||||
SimpleNamespace(type="response.output_item.done", item=commentary_a),
|
||||
SimpleNamespace(
|
||||
type="response.output_item.added",
|
||||
item=SimpleNamespace(type="message", phase="commentary"),
|
||||
),
|
||||
SimpleNamespace(type="response.output_text.delta", delta="Second update."),
|
||||
SimpleNamespace(type="response.output_item.done", item=commentary_b),
|
||||
SimpleNamespace(
|
||||
type="response.output_item.added",
|
||||
item=SimpleNamespace(type="function_call"),
|
||||
),
|
||||
SimpleNamespace(type="response.output_item.done", item=function_item),
|
||||
SimpleNamespace(
|
||||
type="response.completed",
|
||||
response=SimpleNamespace(status="completed"),
|
||||
),
|
||||
])
|
||||
|
||||
agent.client = SimpleNamespace(responses=SimpleNamespace(create=_fake_create))
|
||||
response = agent._run_codex_stream(_codex_request_kwargs())
|
||||
normalized, finish_reason = _normalize_codex_response(response)
|
||||
agent._emit_interim_assistant_message(
|
||||
agent._build_assistant_message(normalized, finish_reason)
|
||||
)
|
||||
|
||||
assert delivered == ["First update.", "Second update."]
|
||||
|
||||
|
||||
def test_run_codex_stream_retry_deduplicates_multiple_commentary_items(monkeypatch):
|
||||
import httpx
|
||||
|
||||
agent = _build_agent(monkeypatch)
|
||||
delivered = []
|
||||
agent.interim_assistant_callback = (
|
||||
lambda text, *, already_streamed=False: delivered.append(text)
|
||||
)
|
||||
commentary_a = SimpleNamespace(
|
||||
type="message",
|
||||
phase="commentary",
|
||||
status="completed",
|
||||
content=[SimpleNamespace(type="output_text", text="First update.")],
|
||||
)
|
||||
commentary_b = SimpleNamespace(
|
||||
type="message",
|
||||
phase="commentary",
|
||||
status="completed",
|
||||
content=[SimpleNamespace(type="output_text", text="Second update.")],
|
||||
)
|
||||
|
||||
class _DroppingStream(_FakeCreateStream):
|
||||
def __iter__(self):
|
||||
yield from super().__iter__()
|
||||
raise httpx.RemoteProtocolError("connection dropped")
|
||||
|
||||
commentary_events = [
|
||||
SimpleNamespace(
|
||||
type="response.output_item.added",
|
||||
item=SimpleNamespace(type="message", phase="commentary"),
|
||||
),
|
||||
SimpleNamespace(type="response.output_text.delta", delta="First update."),
|
||||
SimpleNamespace(type="response.output_item.done", item=commentary_a),
|
||||
SimpleNamespace(
|
||||
type="response.output_item.added",
|
||||
item=SimpleNamespace(type="message", phase="commentary"),
|
||||
),
|
||||
SimpleNamespace(type="response.output_text.delta", delta="Second update."),
|
||||
SimpleNamespace(type="response.output_item.done", item=commentary_b),
|
||||
]
|
||||
calls = {"count": 0}
|
||||
|
||||
def _fake_create(**kwargs):
|
||||
calls["count"] += 1
|
||||
if calls["count"] == 1:
|
||||
return _DroppingStream(commentary_events)
|
||||
return _FakeCreateStream([
|
||||
*commentary_events,
|
||||
SimpleNamespace(
|
||||
type="response.completed",
|
||||
response=SimpleNamespace(status="completed"),
|
||||
),
|
||||
])
|
||||
|
||||
agent.client = SimpleNamespace(responses=SimpleNamespace(create=_fake_create))
|
||||
|
||||
response = agent._run_codex_stream(_codex_request_kwargs())
|
||||
|
||||
assert response.status == "completed"
|
||||
assert calls["count"] == 2
|
||||
assert delivered == ["First update.", "Second update."]
|
||||
|
||||
|
||||
def test_run_codex_stream_surfaces_failed_status_in_final_response(monkeypatch):
|
||||
"""A ``response.failed`` terminal event is reflected on the returned object."""
|
||||
agent = _build_agent(monkeypatch)
|
||||
|
|
@ -2183,6 +2421,28 @@ def test_interim_commentary_redacts_secrets_from_codex_commentary_items(monkeypa
|
|||
assert "Using credential" in observed[0]
|
||||
|
||||
|
||||
def test_interim_commentary_deduplicates_identical_items_in_one_response(monkeypatch):
|
||||
agent = _build_agent(monkeypatch)
|
||||
observed = []
|
||||
agent.interim_assistant_callback = (
|
||||
lambda text, *, already_streamed=False: observed.append(text)
|
||||
)
|
||||
commentary_item = {
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"phase": "commentary",
|
||||
"content": [{"type": "output_text", "text": "Still working."}],
|
||||
}
|
||||
|
||||
agent._emit_interim_assistant_message({
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"codex_message_items": [commentary_item, dict(commentary_item)],
|
||||
})
|
||||
|
||||
assert observed == ["Still working."]
|
||||
|
||||
|
||||
def test_stream_delta_strips_leaked_memory_context(monkeypatch):
|
||||
agent = _build_agent(monkeypatch)
|
||||
observed = []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue