"""Message formatting and preparation functionality."""

import json
import logging
import time
from dataclasses import replace
from typing import TYPE_CHECKING, Any, cast

from agents import (
    MessageOutputItem,
    ModelSettings,
    RunItem,
    ToolCallItem,
    TResponseInputItem,
)
from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel
from agents.models.openai_responses import OpenAIResponsesModel
from openai.types.responses import (
    ResponseFileSearchToolCall,
    ResponseFunctionWebSearch,
    ResponseOutputMessage,
    ResponseOutputText,
)
from openai.types.responses.response_file_search_tool_call import Result as ResponseFileSearchResult

from agency_swarm.messages.response_input_sanitizer import (
    REASONING_ENCRYPTED_CONTENT_INCLUDE,
    ensure_store_false_reasoning_encrypted_content,
    sanitize_store_false_responses_input,
)

if TYPE_CHECKING:
    from agents import RunConfig

    from agency_swarm.agent.core import AgencyContext, Agent

logger = logging.getLogger(__name__)


class IncompatibleChatHistoryError(RuntimeError):
    """Raised when stored chat history cannot be used with the current model protocol."""


class MessageFormatter:
    """Handles message formatting and structure preparation."""

    HISTORY_PROTOCOL_RESPONSES = "responses"
    HISTORY_PROTOCOL_CHAT_COMPLETIONS = "chat_completions"
    HISTORY_PROTOCOL_MIXED = "mixed"

    metadata_fields: list[str] = [
        "agent",
        "callerAgent",
        "timestamp",
        "citations",
        "agent_run_id",
        "parent_run_id",
        "message_origin",
        "run_trace_id",
        "history_protocol",
    ]
    ephemeral_content_part_field = "_agency_swarm_ephemeral"

    @staticmethod
    def resolve_history_protocol(agent: "Agent") -> str:
        """Resolve the expected history protocol for the agent's model."""
        return MessageFormatter._resolve_history_protocol_from_model(agent.model)

    @staticmethod
    def resolve_history_protocol_for_agent_name(
        agent_name: str,
        *,
        default_agent: "Agent",
        agency_context: "AgencyContext | None" = None,
    ) -> str:
        """Resolve history protocol for a named agent, falling back to a default."""
        if agency_context and agency_context.agency_instance is not None:
            agency_instance = agency_context.agency_instance
            agents = getattr(agency_instance, "agents", None)
            if isinstance(agents, dict):
                candidate = agents.get(agent_name)
                if candidate is not None:
                    return MessageFormatter.resolve_history_protocol(candidate)
        return MessageFormatter.resolve_history_protocol(default_agent)

    @staticmethod
    def _resolve_history_protocol_from_model(model: Any) -> str:
        if isinstance(model, OpenAIChatCompletionsModel | OpenAIResponsesModel):
            return MessageFormatter.HISTORY_PROTOCOL_RESPONSES
        if isinstance(model, str):
            if MessageFormatter._model_name_requires_chat_completions(model):
                return MessageFormatter.HISTORY_PROTOCOL_CHAT_COMPLETIONS
            return MessageFormatter.HISTORY_PROTOCOL_RESPONSES

        model_name = getattr(model, "model", None)
        if isinstance(model_name, str) and MessageFormatter._model_name_requires_chat_completions(model_name):
            return MessageFormatter.HISTORY_PROTOCOL_CHAT_COMPLETIONS
        return MessageFormatter.HISTORY_PROTOCOL_RESPONSES

    @staticmethod
    def _model_name_requires_chat_completions(model_name: str) -> bool:
        # OpenAI Agents SDK inputs use Responses-style history items for
        # supported model backends, including Chat Completions wrappers.
        # Keep protocol inference strict and default to Responses.
        return False

    @staticmethod
    def _normalize_history_protocol(value: object) -> str | None:
        if isinstance(value, str):
            normalized = value.strip().lower()
            if normalized in {
                MessageFormatter.HISTORY_PROTOCOL_RESPONSES,
                MessageFormatter.HISTORY_PROTOCOL_CHAT_COMPLETIONS,
            }:
                return normalized
        return None

    @staticmethod
    def _looks_like_chat_completions(message: dict[str, Any]) -> bool:
        if message.get("role") == "tool":
            return True
        if "tool_call_id" in message:
            return True
        tool_calls = message.get("tool_calls")
        if isinstance(tool_calls, list) and tool_calls:
            return True
        return False

    @staticmethod
    def _looks_like_responses(message: dict[str, Any]) -> bool:
        if "call_id" in message:
            return True
        message_type = message.get("type")
        if isinstance(message_type, str) and message_type in {
            "function_call",
            "function_call_output",
            "file_search_call",
            "code_interpreter_call",
            "web_search_call",
        }:
            return True
        return False

    @staticmethod
    def _is_plain_message(message: dict[str, Any]) -> bool:
        role = message.get("role")
        if role not in {"user", "assistant", "system", "developer"}:
            return False
        if MessageFormatter._looks_like_chat_completions(message):
            return False
        if MessageFormatter._looks_like_responses(message):
            return False
        return True

    @staticmethod
    def _detect_history_protocol(history: list[dict[str, Any]]) -> str | None:
        protocols: set[str] = set()
        for msg in history:
            if MessageFormatter._looks_like_responses(msg):
                protocols.add(MessageFormatter.HISTORY_PROTOCOL_RESPONSES)
                continue
            if MessageFormatter._looks_like_chat_completions(msg):
                protocols.add(MessageFormatter.HISTORY_PROTOCOL_CHAT_COMPLETIONS)
                continue
            normalized = MessageFormatter._normalize_history_protocol(msg.get("history_protocol"))
            if normalized and not MessageFormatter._is_plain_message(msg):
                protocols.add(normalized)
        if len(protocols) > 1:
            return MessageFormatter.HISTORY_PROTOCOL_MIXED
        if len(protocols) == 1:
            return next(iter(protocols))
        return None

    @staticmethod
    def _ensure_history_protocol_compatibility(
        history: list[TResponseInputItem],
        *,
        expected_protocol: str,
        agent_name: str,
    ) -> None:
        dict_history = [msg for msg in history if isinstance(msg, dict)]
        detected_protocol = MessageFormatter._detect_history_protocol(cast(list[dict[str, Any]], dict_history))
        if detected_protocol is None:
            return
        if detected_protocol == MessageFormatter.HISTORY_PROTOCOL_MIXED:
            raise IncompatibleChatHistoryError(
                "Incompatible chat history for agent "
                f"'{agent_name}': stored history mixes response and chat_completions protocols. "
                "Start a new chat (clear the thread history) before continuing, or keep only "
                "{role, content} messages and drop tool/event items."
            )
        if detected_protocol != expected_protocol:
            raise IncompatibleChatHistoryError(
                "Incompatible chat history for agent "
                f"'{agent_name}': stored history uses '{detected_protocol}' but the current model expects "
                f"'{expected_protocol}'. Start a new chat (clear the thread history) before continuing, "
                "or keep only {role, content} messages and drop tool/event items."
            )

    @staticmethod
    def add_agency_metadata(
        message: TResponseInputItem,
        agent: str,
        caller_agent: str | None = None,
        agent_run_id: str | None = None,
        parent_run_id: str | None = None,
        run_trace_id: str | None = None,
        history_protocol: str | None = None,
        timestamp: int | None = None,
    ) -> TResponseInputItem:
        """Add agency-specific metadata to a message.

        Args:
            message: The message dictionary to enhance
            agent: The recipient agent name
            caller_agent: The sender agent name (None for user)
            agent_run_id: The current agent's execution ID
            parent_run_id: The calling agent's execution ID
            history_protocol: Expected history protocol label for the stored message.
            timestamp: Optional timestamp in microseconds.
                If None, either preserves existing timestamp or generates a new one.

        Returns:
            dict[str, Any]: Message with added metadata
        """
        modified_message = message.copy()  # type: ignore[arg-type]
        modified_message["agent"] = agent  # type: ignore[typeddict-unknown-key]
        modified_message["callerAgent"] = caller_agent  # type: ignore[typeddict-unknown-key]
        if agent_run_id is not None:
            modified_message["agent_run_id"] = agent_run_id  # type: ignore[typeddict-unknown-key]
        if parent_run_id is not None:
            modified_message["parent_run_id"] = parent_run_id  # type: ignore[typeddict-unknown-key]
        if run_trace_id is not None:
            modified_message["run_trace_id"] = run_trace_id  # type: ignore[typeddict-unknown-key]
        if history_protocol is None:
            existing_protocol = MessageFormatter._normalize_history_protocol(modified_message.get("history_protocol"))
            history_protocol = existing_protocol or MessageFormatter.HISTORY_PROTOCOL_RESPONSES
        modified_message["history_protocol"] = history_protocol  # type: ignore[typeddict-unknown-key]
        # Use microsecond precision to reduce timestamp collisions
        # time.time() returns seconds since epoch; multiply to get microseconds
        # Priority: explicit timestamp param > valid existing timestamp > generate new
        if timestamp is not None:
            modified_message["timestamp"] = timestamp  # type: ignore[typeddict-unknown-key]
        else:
            existing_ts = modified_message.get("timestamp")  # type: ignore[attr-defined]
            # Validate existing timestamp is a positive int (microsecond epoch)
            if isinstance(existing_ts, int) and existing_ts > 0:
                pass  # preserve valid existing timestamp
            else:
                modified_message["timestamp"] = int(time.time() * 1_000_000)  # type: ignore[typeddict-unknown-key]
        # Add type field if not present (for easier parsing/navigation)
        if "type" not in modified_message:
            modified_message["type"] = "message"  # type: ignore[arg-type]
        return modified_message

    @staticmethod
    def prepare_history_for_runner(
        processed_current_message_items: list[TResponseInputItem],
        agent: "Agent",
        sender_name: str | None,
        agency_context: "AgencyContext | None" = None,
        agent_run_id: str | None = None,
        parent_run_id: str | None = None,
        run_trace_id: str | None = None,
        run_config_override: "RunConfig | None" = None,
    ) -> list[TResponseInputItem]:
        """Prepare conversation history for the runner."""
        # Get thread manager from context (required)
        if not agency_context or not agency_context.thread_manager:
            raise RuntimeError(f"Agent '{agent.name}' missing ThreadManager in agency context.")

        thread_manager = agency_context.thread_manager
        history_protocol = MessageFormatter.resolve_history_protocol(agent)

        existing_history = thread_manager.get_conversation_history(agent.name, sender_name)
        compatibility_history = existing_history + [
            msg for msg in processed_current_message_items if isinstance(msg, dict)
        ]
        MessageFormatter._ensure_history_protocol_compatibility(
            compatibility_history,
            expected_protocol=history_protocol,
            agent_name=agent.name,
        )

        # Add agency metadata to incoming messages
        messages_to_save: list[TResponseInputItem] = []
        messages_for_runner: list[TResponseInputItem] = []
        for msg in processed_current_message_items:
            formatted_msg = MessageFormatter.add_agency_metadata(
                msg,  # type: ignore[arg-type]
                agent=agent.name,
                caller_agent=sender_name,
                agent_run_id=agent_run_id,
                parent_run_id=parent_run_id,
                run_trace_id=run_trace_id,
                history_protocol=history_protocol,
            )
            messages_for_runner.append(
                cast(TResponseInputItem, MessageFormatter._strip_ephemeral_content(formatted_msg, drop_parts=False))
            )
            save_message = MessageFormatter._strip_ephemeral_content(formatted_msg, drop_parts=True)
            if save_message is not None:
                messages_to_save.append(save_message)

        # Save messages to flat storage
        thread_manager.add_messages(messages_to_save)
        logger.debug(f"Added {len(messages_to_save)} messages to storage.")

        # Get relevant conversation history for this agent pair
        full_history = existing_history + messages_for_runner

        # Prepare history for runner (sanitize and ensure content safety)
        history_for_runner = MessageFormatter.sanitize_tool_calls_in_history(full_history)  # type: ignore[arg-type]
        history_for_runner = MessageFormatter.ensure_tool_calls_content_safety(history_for_runner)
        # Strip agency metadata before sending to OpenAI
        history_for_runner = MessageFormatter.strip_agency_metadata(history_for_runner)
        history_for_runner = MessageFormatter.sanitize_replayed_tool_item_ids(history_for_runner)
        if MessageFormatter._ensure_store_false_replay_settings(agent, run_config_override):
            history_for_runner = sanitize_store_false_responses_input(history_for_runner)
        return history_for_runner  # type: ignore[return-value]

    @staticmethod
    def _strip_ephemeral_content(message: TResponseInputItem, *, drop_parts: bool) -> TResponseInputItem | None:
        if not isinstance(message, dict):
            return message
        content = message.get("content")
        if not isinstance(content, list):
            return message

        cleaned_content = []
        changed = False
        for part in content:
            if not isinstance(part, dict):
                cleaned_content.append(part)
                continue
            if part.get(MessageFormatter.ephemeral_content_part_field):
                changed = True
                if drop_parts:
                    continue
                part = {k: v for k, v in part.items() if k != MessageFormatter.ephemeral_content_part_field}
            cleaned_content.append(part)

        if not changed:
            return message
        if drop_parts and not cleaned_content:
            return None
        cleaned_message = cast(dict[str, Any], message.copy())
        cleaned_message["content"] = cleaned_content
        return cast(TResponseInputItem, cleaned_message)

    @staticmethod
    def strip_agency_metadata(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
        """Remove agency-specific metadata fields before sending to OpenAI.

        Args:
            messages: List of messages with agency metadata

        Returns:
            list[dict[str, Any]]: Messages without agency metadata fields
        """
        cleaned = []
        for msg in messages:
            # Create a copy without agency fields (including citations which OpenAI doesn't accept)
            clean_msg = {k: v for k, v in msg.items() if k not in MessageFormatter.metadata_fields}
            cleaned.append(clean_msg)
        return cleaned

    @staticmethod
    def sanitize_tool_calls_in_history(history: list[dict[str, Any]]) -> list[dict[str, Any]]:
        """
        Ensures only the most recent assistant message in the history has a 'tool_calls' field.
        Removes 'tool_calls' from all other messages.
        """
        # Find the index of the last assistant message
        last_assistant_idx = None
        for i in reversed(range(len(history))):
            if history[i].get("role") == "assistant":
                last_assistant_idx = i
                break
        if last_assistant_idx is None:
            return history
        # Remove 'tool_calls' from all assistant messages except the last one
        sanitized = []
        for idx, msg in enumerate(history):
            if msg.get("role") == "assistant" and "tool_calls" in msg and idx != last_assistant_idx:
                msg = dict(msg)
                msg.pop("tool_calls", None)
            sanitized.append(msg)
        return sanitized

    @staticmethod
    def ensure_tool_calls_content_safety(history: list[dict[str, Any]]) -> list[dict[str, Any]]:
        """
        Ensures that assistant messages with tool_calls have non-null content.
        This prevents OpenAI API errors when switching between sync and streaming modes.
        """
        sanitized = []
        for msg in history:
            if msg.get("role") == "assistant" and msg.get("tool_calls") and msg.get("content") is None:
                # Create a copy to avoid modifying the original
                msg = dict(msg)
                # Generate descriptive content for tool calls
                tool_descriptions = []
                for tc in msg["tool_calls"]:
                    if isinstance(tc, dict):
                        func_name = tc.get("function", {}).get("name", "unknown")
                        tool_descriptions.append(func_name)

                if tool_descriptions:
                    msg["content"] = f"Using tools: {', '.join(tool_descriptions)}"
                else:
                    msg["content"] = "Executing tool calls"

                logger.debug(f"Fixed null content for assistant message with tool calls: {msg.get('content')}")

            sanitized.append(msg)
        return sanitized

    @staticmethod
    def sanitize_replayed_tool_item_ids(history: list[dict[str, Any]]) -> list[dict[str, Any]]:
        """Drop replay artifact function_call IDs while preserving native Responses IDs.

        In the Agents SDK Chat Completions/LiteLLM conversion path, `function_call`
        items are emitted with `id=FAKE_RESPONSES_ID` and later normalized to
        `id=call_id`, so persisted replay can carry `id == call_id` (e.g. `call_*`).
        Responses-native items keep distinct IDs (`id != call_id`), which must be
        preserved for reasoning/tool continuity across turns.
        """
        sanitized: list[dict[str, Any]] = []
        for msg in history:
            message_type = msg.get("type")
            message_id = msg.get("id")
            call_id = msg.get("call_id")
            if (
                message_type == "function_call"
                and isinstance(message_id, str)
                and message_id
                and isinstance(call_id, str)
                and call_id
                and message_id == call_id
            ):
                msg_copy = dict(msg)
                msg_copy.pop("id", None)
                sanitized.append(msg_copy)
                continue
            sanitized.append(msg)
        return sanitized

    @staticmethod
    def _ensure_store_false_replay_settings(agent: "Agent", run_config_override: "RunConfig | None") -> bool:
        agent_settings = getattr(agent, "model_settings", None)
        run_settings = getattr(run_config_override, "model_settings", None) if run_config_override else None
        effective_settings = agent_settings.resolve(run_settings) if agent_settings is not None else run_settings
        if not isinstance(effective_settings, ModelSettings):
            return False
        if getattr(effective_settings, "store", None) is not False:
            return False

        if run_config_override is not None and run_settings is not None:
            response_include = run_settings.response_include
            if response_include is None and isinstance(agent_settings, ModelSettings):
                response_include = agent_settings.response_include
            response_include = list(response_include or [])
            if REASONING_ENCRYPTED_CONTENT_INCLUDE not in response_include:
                response_include.append(REASONING_ENCRYPTED_CONTENT_INCLUDE)
            run_settings = replace(run_settings, response_include=response_include)
            run_config_override.model_settings = run_settings
        else:
            ensure_store_false_reasoning_encrypted_content(effective_settings)
            agent.model_settings = effective_settings
        return True

    @staticmethod
    def add_citations_to_message(
        run_item_obj: RunItem,
        item_dict: TResponseInputItem,
        citations_by_message: dict[str, list[dict]],
        is_streaming: bool = False,
    ) -> None:
        """Add citations to an assistant message if applicable."""
        if (
            isinstance(run_item_obj, MessageOutputItem)
            and hasattr(run_item_obj.raw_item, "id")
            and run_item_obj.raw_item.id in citations_by_message
        ):
            item_dict["citations"] = citations_by_message[run_item_obj.raw_item.id]  # type: ignore[typeddict-unknown-key, typeddict-item]
            msg_type = "streamed message" if is_streaming else "message"
            logger.debug(f"Added {len(item_dict['citations'])} citations to {msg_type} {run_item_obj.raw_item.id}")  # type: ignore[typeddict-item]

    @staticmethod
    def extract_hosted_tool_results(
        agent: "Agent",
        run_items: list[RunItem],
        caller_agent: str | None = None,
        timestamps_by_tool_id: dict[str, int] | None = None,
    ) -> list[TResponseInputItem]:
        """
        Extract hosted tool results (FileSearch, WebSearch) from assistant message content
        and create special assistant messages to capture search results in conversation history.

        Args:
            timestamps_by_tool_id: Optional mapping from tool call ID to emission timestamp.
                If provided, synthetic outputs inherit the tool call's arrival time.
        """
        synthetic_outputs = []
        history_protocol = MessageFormatter.resolve_history_protocol(agent)

        # Find hosted tool calls and assistant messages
        hosted_tool_calls: list[ToolCallItem] = []
        assistant_messages_by_agent: dict[str, list[MessageOutputItem]] = {}

        for item in run_items:
            if isinstance(item, ToolCallItem):
                if isinstance(item.raw_item, ResponseFileSearchToolCall | ResponseFunctionWebSearch):
                    hosted_tool_calls.append(item)
            elif isinstance(item, MessageOutputItem):
                msg_agent_name = agent.name
                try:
                    candidate_name = item.agent.name
                except AttributeError:
                    candidate_name = None
                if isinstance(candidate_name, str) and candidate_name:
                    msg_agent_name = candidate_name
                assistant_messages_by_agent.setdefault(msg_agent_name, []).append(item)

        # Track which assistant messages have been used for web search per agent
        used_assistant_msg_indices: dict[str, set[int]] = {}

        def resolve_agent_name(tool_call_item: ToolCallItem) -> str:
            try:
                resolved_name = tool_call_item.agent.name
            except AttributeError:
                resolved_name = None
            if isinstance(resolved_name, str) and resolved_name:
                return resolved_name
            return agent.name

        def resolve_file_search_result(result: ResponseFileSearchResult) -> tuple[str, str]:
            file_id_value = result.file_id
            if isinstance(file_id_value, str) and file_id_value:
                file_id = file_id_value
            elif file_id_value is None:
                file_id = "unknown"
            else:
                file_id = str(file_id_value)

            text = result.text or ""
            return file_id, text

        # Extract results for each hosted tool call
        for tool_call_item in hosted_tool_calls:
            tool_call = tool_call_item.raw_item

            tool_agent_name = resolve_agent_name(tool_call_item)
            # Avoid overwriting propagated messages
            if tool_agent_name != agent.name:
                continue
            if isinstance(tool_call, ResponseFileSearchToolCall):
                search_results_content = f"[SEARCH_RESULTS] Tool Call ID: {tool_call.id}\nTool Type: file_search\n"

                iterable_results: list[ResponseFileSearchResult] = tool_call.results or []
                file_count = 0

                for result in iterable_results:
                    file_id, content_text = resolve_file_search_result(result)
                    file_count += 1
                    search_results_content += f"File {file_count}: {file_id}\nContent: {content_text}\n\n"

                if file_count > 0:
                    # Use tool call's emission timestamp if available
                    tool_timestamp = (
                        timestamps_by_tool_id.get(tool_call.id) if timestamps_by_tool_id and tool_call.id else None
                    )
                    synthetic_outputs.append(
                        MessageFormatter.add_agency_metadata(
                            {  # type: ignore[arg-type]
                                "role": "system",
                                "content": search_results_content,
                                "message_origin": "file_search_preservation",
                            },
                            agent=tool_agent_name,
                            caller_agent=caller_agent,
                            history_protocol=history_protocol,
                            timestamp=tool_timestamp,
                        )
                    )
                    logger.debug(f"Created file_search results message for call_id: {tool_call.id}")

            elif isinstance(tool_call, ResponseFunctionWebSearch):
                search_results_content = f"[WEB_SEARCH_RESULTS] Tool Call ID: {tool_call.id}\nTool Type: web_search\n"

                # Capture FULL search results (not truncated to 500 chars)
                found_content = False
                agent_messages = assistant_messages_by_agent.get(tool_agent_name, [])
                if agent_messages:
                    used_indices = used_assistant_msg_indices.setdefault(tool_agent_name, set())
                    for idx, msg_item in enumerate(agent_messages):
                        # Skip if this message was already used for another web search
                        if idx in used_indices:
                            continue
                        message = msg_item.raw_item
                        content_items = []
                        if isinstance(message, ResponseOutputMessage):
                            content_items = message.content
                        elif hasattr(message, "content"):
                            content_items = message.content or []

                        for content_item in content_items:
                            text_value = None
                            if isinstance(content_item, ResponseOutputText):
                                text_value = content_item.text
                            elif hasattr(content_item, "text"):
                                text_value = content_item.text  # type: ignore[attr-defined]

                            if text_value:
                                search_results_content += f"Search Results:\n{text_value}\n"
                                found_content = True
                                used_indices.add(idx)
                                break

                        if found_content:
                            break  # Process only first available assistant message with content

                if found_content:
                    # Use tool call's emission timestamp if available
                    tool_timestamp = (
                        timestamps_by_tool_id.get(tool_call.id) if timestamps_by_tool_id and tool_call.id else None
                    )
                    synthetic_outputs.append(
                        MessageFormatter.add_agency_metadata(
                            {  # type: ignore[arg-type]
                                "role": "system",
                                "content": search_results_content,
                                "message_origin": "web_search_preservation",
                            },
                            agent=tool_agent_name,
                            caller_agent=caller_agent,
                            history_protocol=history_protocol,
                            timestamp=tool_timestamp,
                        )
                    )
                    logger.debug(f"Created web_search results message for call_id: {tool_call.id}")

        return synthetic_outputs  # type: ignore[return-value]

    @staticmethod
    def extract_handoff_target_name(run_item_obj: RunItem) -> str | None:
        """Extract target agent name from a handoff output item.

        Prefers parsing raw_item.output JSON {"assistant": "AgentName"}. Falls back to
        run_item_obj.target_agent.name if available.
        """
        try:
            raw = getattr(run_item_obj, "raw_item", None)
            if isinstance(raw, dict):
                output_val = raw.get("output")
                if isinstance(output_val, str):
                    try:
                        parsed = json.loads(output_val)
                        assistant_name = parsed.get("assistant")
                        if isinstance(assistant_name, str) and assistant_name.strip():
                            return assistant_name.strip()
                    except Exception:
                        pass
            # Fallback if SDK provides target_agent attribute
            target_agent = getattr(run_item_obj, "target_agent", None)
            if target_agent is not None and hasattr(target_agent, "name") and target_agent.name:
                return target_agent.name
        except Exception:
            return None
        return None
