diff --git a/strix/core/agents.py b/strix/core/agents.py index 4d3d65cc..45013c3f 100644 --- a/strix/core/agents.py +++ b/strix/core/agents.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio import json import logging +import re import tempfile from dataclasses import dataclass, field from pathlib import Path @@ -31,6 +32,24 @@ TERMINAL_STATUSES: frozenset[str] = frozenset({"completed", "stopped", "crashed" # on other agents is re-checked on a timer. WaitKind = Literal["user", "agents", "stalled"] +_CHATGPT_TRANSCRIPT_TAGS = ( + "analysis", + "assistant", + "channel", + "final", + "user", +) +_CHATGPT_TRANSCRIPT_TAG_ALTERNATION = "|".join(re.escape(tag) for tag in _CHATGPT_TRANSCRIPT_TAGS) +_OPTIONAL_TAG_ATTRIBUTES_PATTERN = r"(?:\s+[^>]*)?" +_CHATGPT_TRANSCRIPT_TAG_RE = re.compile( + rf"", + flags=re.IGNORECASE, +) + + +def _strip_chatgpt_transcript_tags(content: str) -> str: + return _CHATGPT_TRANSCRIPT_TAG_RE.sub("", content) + @dataclass(slots=True) class AgentRuntime: @@ -413,7 +432,7 @@ class AgentCoordinator: await self._maybe_snapshot() async def cancel_descendants(self, agent_id: str) -> None: - tasks = [] + tasks: list[asyncio.Task[Any]] = [] async with self._lock: for aid in reversed(self._subtree_order_locked(agent_id)): task = self.runtimes.get(aid, AgentRuntime()).task @@ -481,6 +500,7 @@ class AgentCoordinator: content = str(message.get("content", "")) if sender == "user": return cast("TResponseInputItem", {"role": "user", "content": content}) + content = _strip_chatgpt_transcript_tags(content) sender_name = self.names.get(sender, sender) msg_type = message.get("type", "information") priority = message.get("priority", "normal") diff --git a/tests/test_agent_messages.py b/tests/test_agent_messages.py new file mode 100644 index 00000000..76e8f2d0 --- /dev/null +++ b/tests/test_agent_messages.py @@ -0,0 +1,81 @@ +"""Tests for agent-to-session message conversion.""" + +from __future__ import annotations + +from strix.core.agents import AgentCoordinator + + +CHATGPT_TRANSCRIPT_CONTENT = ( + "checked target\nfinal\ndone" +) +CHATGPT_TRANSCRIPT_CONTENT_WITH_ATTRIBUTES = ( + 'checked target\n' + 'final\n' + 'done\n' + "" +) +USER_LITERAL_TAG_CONTENT = ( + "Please preserve this XML-like snippet: " + 'literal user content and done' +) + + +def test_message_to_session_item_strips_chatgpt_transcript_tags() -> None: + coordinator = AgentCoordinator() + coordinator.names["child"] = "Researcher" + + item = coordinator._message_to_session_item( + { + "from": "child", + "type": "information", + "priority": "normal", + "content": CHATGPT_TRANSCRIPT_CONTENT, + } + ) + + content = str(item["content"]) + + assert "" not in content + assert "" not in content + assert "" not in content + assert "" not in content + assert "" not in content + assert "" not in content + assert "checked target" in content + assert "done" in content + + +def test_message_to_session_item_preserves_user_literal_tags() -> None: + coordinator = AgentCoordinator() + + item = coordinator._message_to_session_item( + { + "from": "user", + "content": USER_LITERAL_TAG_CONTENT, + } + ) + + assert item["content"] == USER_LITERAL_TAG_CONTENT + + +def test_message_to_session_item_strips_attributed_chatgpt_transcript_tags() -> None: + coordinator = AgentCoordinator() + coordinator.names["child"] = "Researcher" + + item = coordinator._message_to_session_item( + { + "from": "child", + "type": "information", + "priority": "normal", + "content": CHATGPT_TRANSCRIPT_CONTENT_WITH_ATTRIBUTES, + } + ) + + content = str(item["content"]) + + assert '' not in content + assert '' not in content + assert '' not in content + assert "" not in content + assert "checked target" in content + assert "done" in content