from agent_framework.checkpoints.langgraph_saver import create_langgraph_checkpointer
from agent_framework.workflows import END, START, FrameworkStateGraph

from agent_framework.guardrails.pipeline import GuardrailPipeline
from agent_framework.guardrails.output_supervisor import OutputSupervisor
from agent_framework.guardrails.rail_action import RailAction
from agent_framework.guardrails.rail_result import RailResult
from agent_framework.judges.judge import JudgePipeline
from agent_framework.routing.enterprise_router import EnterpriseRouter
from app.domain.contas.conversation_policy import evaluate as evaluate_contas_conversation_policy
from agent_framework.supervisor.supervisor import Supervisor
from agent_framework.observability.workflow_events import WorkflowTelemetry
from agent_framework.observability.guardrail_events import GuardrailTelemetry
from agent_framework.observability.judge_events import JudgeTelemetry
from agent_framework.observability.langgraph_telemetry import LangGraphDeepTelemetry
from agent_framework.observability.observer import AgentObserver
from app.agents.faturas_agent import FaturasAgent
from app.agents.vas_agent import VasAgent
from app.agents.contestacao_agent import ContestacaoAgent
from app.agents.suporte_contas_agent import SuporteContasAgent
from app.state import AgentState
from agent_framework.rag.rag_service import RagService
from agent_framework.rag.embedding_provider import create_embedding_provider
from agent_framework.cache.cache import create_cache
from agent_framework.memory.long_term_memory import create_long_term_memory_manager


class FrameworkOutputGuardrailRail:
    """Adapter: reutiliza GuardrailPipeline.run_output dentro do OutputSupervisor novo.

    O framework antigo retornava decisões allowed=True/False. O OutputSupervisor
    corporativo trabalha com RailAction (allow/sanitize/retry/block/handover).
    Este adapter evita reescrever todos os rails agora e mantém compatibilidade.
    """

    code = "LEGACY_OUTPUT_GUARDRAILS"

    def __init__(self, pipeline: GuardrailPipeline):
        self.pipeline = pipeline

    async def evaluate(self, candidate: str, context: dict):
        final, decisions = await self.pipeline.run_output(candidate, context)
        serialized = [d.model_dump() for d in decisions]

        blocked = [d for d in decisions if not getattr(d, "allowed", True)]
        if blocked:
            first = blocked[0]
            code = (getattr(first, "code", "") or "").upper()
            action = RailAction.RETRY if code in {"REVPREC", "CMP", "SCO", "GND"} else RailAction.BLOCK
            return RailResult(
                code=code or self.code,
                action=action,
                reason=getattr(first, "reason", "Resposta bloqueada por guardrail de saída"),
                guidance=getattr(first, "reason", "Regerar resposta seguindo as políticas de saída."),
                sanitized_text=final,
                metadata={"framework_decisions": serialized},
            )

        if final != candidate:
            return RailResult(
                code=self.code,
                action=RailAction.SANITIZE,
                reason="Resposta sanitizada por guardrail de saída do framework.",
                sanitized_text=final,
                metadata={"framework_decisions": serialized},
            )

        return RailResult(
            code=self.code,
            action=RailAction.ALLOW,
            reason="Resposta aprovada pelos guardrails de saída do framework.",
            sanitized_text=final,
            metadata={"framework_decisions": serialized},
        )


class AgentWorkflow:
    """Workflow principal com dois modos de roteamento.

    Modos suportados por configuração:
      ROUTING_MODE=router
        input_guardrails -> routing_decision/EnterpriseRouter -> 1 agente -> output_guardrails

      ROUTING_MODE=supervisor
        input_guardrails -> routing_decision/Supervisor -> supervisor_agent -> N agentes -> consolidação

    Em ambos os modos, memória/checkpoint/session usam tenant_id:agent_id:session_id.
    """

    def __init__(self, llm, memory, telemetry, analytics, settings, observer: AgentObserver | None = None, tool_router=None, summary_memory=None):
        self.llm = llm
        self.memory = memory
        self.telemetry = telemetry
        self.analytics = analytics
        self.observer = observer or AgentObserver(analytics=analytics)
        self.settings = settings
        self.tool_router = tool_router
        self.summary_memory = summary_memory
        self.long_term_memory_manager = create_long_term_memory_manager(settings, telemetry=telemetry)
        self.guardrails = GuardrailPipeline(
            observer=self.observer,
            llm=llm,
            enable_parallel=bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)),
            fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)),
        )
        self.output_supervisor_engine = OutputSupervisor(
            rails=[FrameworkOutputGuardrailRail(self.guardrails)],
            observer=self.observer,
            max_retries=int(getattr(settings, "OUTPUT_SUPERVISOR_MAX_RETRIES", 3)),
            enable_parallel=bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)),
            fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)),
        )
        self.judges = JudgePipeline()
        self.supervisor = Supervisor()
        self.workflow_telemetry = WorkflowTelemetry(telemetry)
        self.guardrail_telemetry = GuardrailTelemetry(telemetry)
        self.judge_telemetry = JudgeTelemetry(telemetry)
        self.langgraph_telemetry = LangGraphDeepTelemetry(telemetry)
        self.cache = create_cache(settings)
        self.embedding_provider = create_embedding_provider(settings)
        self.rag_service = RagService(settings, embedding_provider=self.embedding_provider, telemetry=telemetry)
        self.router = EnterpriseRouter(settings, llm=llm, telemetry=telemetry)
        agent_kwargs = {
            "telemetry": telemetry,
            "tool_router": getattr(self, "tool_router", None),
            "rag_service": self.rag_service,
            "cache": self.cache,
            "settings": settings,
            "observer": self.observer,
            "memory": memory,
            "summary_memory": summary_memory,
            "guardrail_pipeline": self.guardrails,
        }
        self.faturas = FaturasAgent(llm, **agent_kwargs)
        self.vas = VasAgent(llm, **agent_kwargs)
        self.contestacao = ContestacaoAgent(llm, **agent_kwargs)
        self.suporte_contas = SuporteContasAgent(llm, **agent_kwargs)

        # Long-term memory is injected as a runtime capability after creation.
        for agent in (self.faturas, self.vas, self.contestacao, self.suporte_contas):
            agent.long_term_memory_manager = self.long_term_memory_manager
        self.graph = self._build_graph()

    @staticmethod
    def _output_guardrail_context(state: dict) -> dict:
        """Enriquece contexto de guardrail com evidência operacional real.

        CMP/ANATEL precisa dos protocolos produzidos pelas tools; GND/ALUC precisam
        enxergar evidências MCP. O domínio não implementa rails, apenas devolve dados.
        """
        ctx = dict(state.get("context", {}) or {})
        mcp_results = state.get("mcp_results") or []
        ctx["evidence"] = mcp_results or ctx.get("evidence")
        ctx["tool_result"] = mcp_results or ctx.get("tool_result")
        ctx["tool_executed"] = any(isinstance(r, dict) and r.get("ok") for r in mcp_results)

        # Domain-specific output rails such as TIM_AOFERTA need the full turn
        # context, including the current customer request. Keep this data in
        # the agent state; the generic framework only transports it.
        history = list(state.get("history") or [])
        current_user_text = str(state.get("user_text") or "").strip()
        if current_user_text:
            if not history or str((history[-1] or {}).get("content") or "") != current_user_text or str((history[-1] or {}).get("role") or "") != "user":
                history.append({"role": "user", "content": current_user_text})

        # Guardrails de domínio precisam julgar a solicitação do turno atual, não
        # reabrir semanticamente uma transação já encerrada. O router já ignora
        # COMPLETED/FAILED/CANCELLED/BLOCKED/OUT_OF_SCOPE para continuidade; o
        # mesmo princípio precisa valer para o contexto operacional dos rails.
        #
        # Importante: não apagamos o histórico do state/checkpoint (auditoria).
        # Apenas recortamos o contexto enviado aos guardrails quando há evidência
        # explícita de que o turno atual saiu da transação anterior: seja por
        # interrupção semântica (transaction_interruption=intent_shift), seja por
        # preempção de stickiness após uma transação terminal.
        route_decision = state.get("route_decision") or {}
        route_metadata = route_decision.get("metadata") if isinstance(route_decision, dict) else {}
        route_metadata = route_metadata if isinstance(route_metadata, dict) else {}
        pre_validation = state.get("transaction_pre_validation") or {}
        pre_validation = pre_validation if isinstance(pre_validation, dict) else {}
        tx_status = str(
            state.get("transaction_status")
            or pre_validation.get("status")
            or ""
        ).strip().upper()
        terminal_tx = bool(pre_validation.get("terminal")) or tx_status in {
            "COMPLETED", "FAILED", "CANCELLED", "BLOCKED", "OUT_OF_SCOPE"
        }
        semantic_intent_shift = (
            str(route_metadata.get("transaction_interruption") or "").strip().lower()
            == "intent_shift"
        )
        stickiness_intent_shift = bool(route_metadata.get("route_stickiness_preempted"))
        operational_context_reset = bool(state.get("operational_context_reset"))
        should_isolate_history = operational_context_reset or semantic_intent_shift or (terminal_tx and stickiness_intent_shift)

        current_route = str(
            state.get("route")
            or (route_decision.get("route") if isinstance(route_decision, dict) else "")
            or ""
        ).strip()
        current_intent = str(
            state.get("intent")
            or (route_decision.get("intent") if isinstance(route_decision, dict) else "")
            or ""
        ).strip()
        ctx["current_user_message"] = current_user_text
        ctx["current_route"] = current_route
        ctx["current_intent"] = current_intent

        # Structured control evidence for domain guardrails.  A rail must not
        # infer an authorized transfer from prose; it receives the router/workflow
        # decision explicitly for the current turn.
        session_control = str(
            state.get("session_control")
            or route_metadata.get("session_control")
            or ""
        ).strip().upper()
        route_handoff = bool(
            (route_decision.get("handoff") if isinstance(route_decision, dict) else False)
            or state.get("human_handoff_requested")
            or session_control == "HUMAN_HANDOFF"
        )
        ctx["session_control"] = session_control
        ctx["human_handoff_requested"] = route_handoff
        ctx["handoff"] = route_handoff
        ctx["route_decision"] = route_decision

        if should_isolate_history:
            operational_history = (
                [{"role": "user", "content": current_user_text}]
                if current_user_text else []
            )
            ctx["historical_transaction_ignored"] = True
            ctx["historical_transaction_status"] = tx_status or (
                "INTERRUPTED" if semantic_intent_shift else "TERMINAL"
            )
            if semantic_intent_shift:
                ctx["historical_transaction_interruption"] = "intent_shift"
            # Remova do contexto operacional quaisquer snapshots transacionais
            # antigos eventualmente carregados em state.context. Eles continuam
            # preservados no state para auditoria/checkpoint.
            for stale_key in (
                "transaction_pre_validation",
                "transaction_status",
                "active_transaction",
                "transaction",
            ):
                ctx.pop(stale_key, None)
        else:
            operational_history = history

        ctx["conversation_history"] = operational_history
        ctx["history_texts"] = [
            str(item.get("content") or "")
            for item in operational_history
            if isinstance(item, dict) and item.get("content") not in (None, "")
        ]

        protocols: list[str] = []
        seen: set[str] = set()
        protocol_keys = {"protocol_number", "protocolo_id", "interactionProtocol", "protocolNumber", "finalizacao_protocol"}

        def walk(value):
            if isinstance(value, dict):
                for key, item in value.items():
                    if key in protocol_keys and item not in (None, ""):
                        text = str(item).strip()
                        if text and text not in seen:
                            seen.add(text)
                            protocols.append(text)
                    elif isinstance(item, (dict, list, tuple)):
                        walk(item)
            elif isinstance(value, (list, tuple)):
                for item in value:
                    walk(item)

        walk(mcp_results)
        if protocols:
            ctx["expected_protocols"] = protocols
            ctx["requer_protocolo"] = True
            ctx.setdefault("tipo_fluxo", "ajuste")
        return ctx

    def _node(self, name, fn):
        async def _wrapped(state):
            async with self.langgraph_telemetry.node(name, state):
                return await fn(state)
        return _wrapped

    def _build_graph(self):
        builder = FrameworkStateGraph(AgentState)
        builder.add_node("input_guardrails", self._node("input_guardrails", self.input_guardrails))
        builder.add_node("load_long_term_memory", self._node("load_long_term_memory", self.load_long_term_memory))
        builder.add_node("routing_decision", self._node("routing_decision", self.routing_decision))
        builder.add_node("conversation_policy", self._node("conversation_policy", self.conversation_policy))
        builder.add_node("conversation_policy_response", self._node("conversation_policy_response", self.conversation_policy_response))
        builder.add_node("faturas_agent", self._node("faturas_agent", self.faturas_agent))
        builder.add_node("vas_agent", self._node("vas_agent", self.vas_agent))
        builder.add_node("contestacao_agent", self._node("contestacao_agent", self.contestacao_agent))
        builder.add_node("suporte_contas_agent", self._node("suporte_contas_agent", self.suporte_contas_agent))
        builder.add_node("handoff", self._node("handoff", self.handoff))
        builder.add_node("human_handoff", self._node("human_handoff", self.human_handoff))
        builder.add_node("end_session", self._node("end_session", self.end_session))
        builder.add_node("supervisor_agent", self._node("supervisor_agent", self.supervisor_agent))
        builder.add_node("output_supervisor", self._node("output_supervisor", self.output_supervisor))
        builder.add_node("output_guardrails", self._node("output_guardrails", self.output_guardrails))
        builder.add_node("judge", self._node("judge", self.judge))
        builder.add_node("supervisor_review", self._node("supervisor_review", self.supervisor_review))
        builder.add_node("persist_long_term_memory", self._node("persist_long_term_memory", self.persist_long_term_memory))
        builder.add_node("persist", self._node("persist", self.persist))

        builder.add_edge(START, "input_guardrails")
        builder.add_conditional_edges(
            "input_guardrails",
            self._after_input_guardrails,
            # Mesmo uma resposta produzida por um bloqueio de input deve passar
            # pelos guardrails de saída antes de ser entregue ao usuário.
            {"blocked": "output_guardrails", "continue": "load_long_term_memory"},
        )
        builder.add_edge("load_long_term_memory", "routing_decision")
        builder.add_edge("routing_decision", "conversation_policy")
        builder.add_conditional_edges(
            "conversation_policy",
            lambda s: s.get("route", "faturas_agent"),
            {
                "faturas_agent": "faturas_agent",
                "vas_agent": "vas_agent",
                "contestacao_agent": "contestacao_agent",
                "suporte_contas_agent": "suporte_contas_agent",
                "handoff": "handoff",
                "human_handoff": "human_handoff",
                "end_session": "end_session",
                "supervisor_agent": "supervisor_agent",
                "conversation_policy_response": "conversation_policy_response",
            },
        )
        builder.add_edge("faturas_agent", "output_supervisor")
        builder.add_edge("vas_agent", "output_supervisor")
        builder.add_edge("contestacao_agent", "output_supervisor")
        builder.add_edge("suporte_contas_agent", "output_supervisor")
        builder.add_edge("handoff", "output_supervisor")
        builder.add_edge("human_handoff", "output_supervisor")
        builder.add_edge("end_session", "output_supervisor")
        builder.add_edge("supervisor_agent", "output_supervisor")
        builder.add_edge("conversation_policy_response", "output_supervisor")
        builder.add_edge("output_supervisor", "output_guardrails")
        builder.add_conditional_edges(
            "output_guardrails",
            lambda s: "blocked" if s.get("blocked") else "continue",
            # Clarificações geradas por guardrail de entrada não devem ser
            # julgadas nem gravadas em LTM como se fossem uma resposta normal.
            {"blocked": "persist", "continue": "judge"},
        )
        builder.add_edge("judge", "supervisor_review")
        builder.add_edge("supervisor_review", "persist_long_term_memory")
        builder.add_edge("persist_long_term_memory", "persist")
        builder.add_edge("persist", END)

        return builder.compile(checkpointer=create_langgraph_checkpointer(self.settings))

    def _after_input_guardrails(self, state):
        return "blocked" if state.get("blocked") else "continue"

    @staticmethod
    def _input_guardrail_user_message(decisions, state, sanitized_text):
        """Converte um bloqueio técnico em mensagem útil sem expor internals.

        O `reason` bruto continua em `guardrail_decisions`/telemetria para
        auditoria. A mensagem ao usuário é específica por classe de rail e
        segue depois pelos guardrails de saída.
        """
        blocked = [d for d in decisions if not getattr(d, "allowed", True)]
        first = blocked[0] if blocked else None
        code = str(getattr(first, "code", "") or "").upper()
        reason = str(getattr(first, "reason", "") or "").strip()

        if code == "COER":
            # COER representa ambiguidade/incompletude, não uma violação de
            # segurança. Não ecoamos o reason bruto do modelo.
            return (
                "Não consegui entender sua última mensagem porque ela parece "
                "incompleta ou ambígua. Pode reformular ou completar o que você quis dizer?"
            )
        if code == "INPUT_SIZE":
            return "Sua mensagem ficou muito longa para eu processar de uma vez. Pode resumir ou dividir em partes?"
        if code == "DLEX_IN":
            return "Não posso usar essa informação da forma solicitada. Reformule o pedido sem incluir dados ou conteúdo restrito."
        if code == "PINJ":
            return "Não posso seguir instruções que tentem alterar as regras do atendimento. Posso continuar ajudando com a sua solicitação."
        if code == "TOX":
            return "Não consegui prosseguir com essa mensagem. Pode reformular o pedido para continuarmos o atendimento?"
        if code == "CMP":
            return "Não posso prosseguir com essa solicitação dessa forma. Posso ajudar com uma alternativa permitida."

        # Fallback neutro: não atribui falsamente o problema a 'segurança' e
        # não expõe nomes, razões ou políticas internas dos guardrails.
        return "Não consegui processar essa mensagem. Pode reformular para eu continuar o atendimento?"

    async def input_guardrails(self, state):
        if state.get("session_ended") is True:
            answer = str(getattr(
                self.settings,
                "SESSION_ALREADY_ENDED_MESSAGE",
                "Este atendimento já foi encerrado. Inicie uma nova sessão para continuar.",
            ))
            await self.telemetry.event(
                "session.message.rejected_after_end",
                {"session_id": state.get("conversation_key") or state.get("session_id")},
            )
            return {
                "answer": answer,
                "final_answer": answer,
                "blocked": True,
                "session_control": "END_SESSION",
                "session_ended": True,
                "next_state": "SESSION_ENDED",
            }
        async with self.telemetry.span(
            "workflow.input_guardrails",
            session_id=state.get("conversation_key") or state.get("session_id"),
            input=state.get("user_text"),
        ):
            boundary_pending = bool(state.get("operational_context_boundary_pending"))
            tx_status = str(state.get("transaction_status") or "").strip().upper()
            active_tx = state.get("active_transaction") if isinstance(state.get("active_transaction"), dict) else {}
            active_tx_status = str(active_tx.get("status") or "").strip().upper()
            nonterminal_tx_statuses = {
                "COLLECTING_PARAMETERS",
                "AWAITING_CONFIRMATION",
                "EXECUTING",
                "PAUSED",
                "WAITING_INPUT",
            }
            # A new active transaction has precedence over terminal evidence from
            # the previous transaction in the same session.  Without this guard,
            # a stale state["transaction_status"] == COMPLETED can tombstone an
            # AWAITING_CONFIRMATION transaction that was just opened in the
            # current turn.
            active_transaction_pending = bool(active_tx) and active_tx_status in nonterminal_tx_statuses

            # A confirmation latch is itself authoritative live transaction state.
            # Depending on checkpoint serialization/order, active_transaction may
            # not yet be rehydrated while pending_tool_call + confirmation_required
            # are present. Never let terminal evidence from the previous transaction
            # tombstone a valid confirmation for the new one.
            pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {}
            pending_confirmation = bool(
                state.get("confirmation_required")
                and pending_call.get("tool_name")
                and isinstance(pending_call.get("arguments"), dict)
            )
            if pending_confirmation and not active_transaction_pending:
                pending_args = dict(pending_call.get("arguments") or {})
                active_tx = {
                    "transaction_id": str(pending_args.get("transaction_id") or state.get("transaction_id") or ""),
                    "tool_name": str(pending_call.get("tool_name") or ""),
                    "arguments": pending_args,
                    "status": "AWAITING_CONFIRMATION",
                }
                state["active_transaction"] = active_tx
                state["transaction_status"] = "AWAITING_CONFIRMATION"
                tx_status = "AWAITING_CONFIRMATION"
                active_tx_status = "AWAITING_CONFIRMATION"
                active_transaction_pending = True

            terminal_interaction = (
                tx_status in {"COMPLETED", "FAILED", "CANCELLED", "BLOCKED", "OUT_OF_SCOPE"}
                and not active_transaction_pending
            )
            reset_operational_context = (boundary_pending or terminal_interaction) and not active_transaction_pending

            # The durable history/checkpoint is preserved, but the first turn
            # after a completed workflow must look operationally like a fresh
            # conversation. Guardrails therefore see only the current utterance.
            history_texts = (
                [str(state.get("user_text") or "")]
                if reset_operational_context
                else [m.get("content", "") for m in state.get("history", [])]
            )
            if reset_operational_context:
                # Tombstone every live latch that can make the next turn look
                # like a continuation of the closed workflow/transaction.
                state.update({
                    "pending_domain_workflow": None,
                    "pending_tool_clarification": None,
                    "workflow_input_reprompt": None,
                    "active_transaction": None,
                    "selected_tool_call": {},
                    "pending_tool_call": {},
                    "missing_parameters": [],
                    "confirmation_required": False,
                    "confirmation_received": False,
                    "transaction_pre_validation": None,
                    "tool_policy_result": None,
                    "tool_terminal_result": None,
                    "transaction_confirmation_message_override": None,
                    "next_state": None,
                    "mcp_tools": [],
                    "mcp_results": [],
                    "relevant_transaction_evidence": [],
                    "route": None,
                    "intent": None,
                    "route_decision": {},
                    "active_agent": None,
                    "route_bypassed": False,
                    "continuity_signal": {},
                    "workflow_id": None,
                    "transaction_status": None,
                    "operational_context_boundary_pending": False,
                    "operational_context_reset": True,
                })
            await self.observer.emit_grl(
                "001",
                {
                    "session_id": state.get("conversation_key") or state.get("session_id"),
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "phase": "input",
                },
                component="workflow.input_guardrails.start",
            )
            pending_workflow = None if reset_operational_context else state.get("pending_domain_workflow")
            pause = (
                pending_workflow.get("pause")
                if isinstance(pending_workflow, dict) and isinstance(pending_workflow.get("pause"), dict)
                else {}
            )
            expected_input = pause.get("expected_input") if isinstance(pause, dict) else None
            sanitized, decisions = await self.guardrails.run_input(
                state["user_text"],
                {
                    **(state.get("context") or {}),
                    "history_texts": history_texts,
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "agent_profile": state.get("agent_profile") or {},
                    # Generic workflow contract context. COER delegates only
                    # conversational coherence to this contract; all other
                    # safety rails continue to execute normally.
                    "expected_input": expected_input,
                    # Active transaction parameter contracts own the semantic
                    # interpretation of short replies such as a product name or
                    # identifier. COER delegates coherence only; PINJ/DLEX/TOX
                    # and every other safety rail still run normally.
                    "transaction_status": state.get("transaction_status"),
                    "missing_parameters": list(state.get("missing_parameters") or []),
                    "active_transaction": state.get("active_transaction") or {},
                },
            )
            for _decision in decisions:
                await self.guardrail_telemetry.evaluated("input", _decision)
                await self.observer.emit_grl(
                    "002" if _decision.allowed else "004",
                    {
                        "session_id": state.get("conversation_key") or state.get("session_id"),
                        "tenant_id": state.get("tenant_id"),
                        "agent_id": state.get("agent_id"),
                        "phase": "input",
                        "rail_code": getattr(_decision, "code", None),
                        "allowed": bool(_decision.allowed),
                        "reason": getattr(_decision, "reason", None),
                    },
                    component="workflow.input_guardrails.decision",
                )
                if not _decision.allowed:
                    await self.guardrail_telemetry.blocked("input", _decision)
            await self.telemetry.event(
                "guardrails.input.completed",
                {
                    "session_id": state.get("conversation_key") or state.get("session_id"),
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "decisions": [d.model_dump() for d in decisions],
                },
            )
            await self.observer.emit_grl(
                "009",
                {
                    "session_id": state.get("conversation_key") or state.get("session_id"),
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "phase": "input",
                    "blocked": any(not d.allowed for d in decisions),
                    "decision_count": len(decisions),
                },
                component="workflow.input_guardrails.final",
            )
            if any(not d.allowed for d in decisions):
                user_message = self._input_guardrail_user_message(decisions, state, sanitized)
                return {
                    "sanitized_input": sanitized,
                    "answer": user_message,
                    # final_answer será calculado por output_guardrails.
                    "final_answer": None,
                    "guardrail_decisions": [d.model_dump() for d in decisions],
                    "route": "blocked",
                    "intent": "input_guardrail_blocked",
                    "route_decision": {
                        "route": "blocked",
                        "agent": None,
                        "intent": "input_guardrail_blocked",
                        "confidence": 1.0,
                        "reason": "Entrada interrompida por guardrail antes do roteamento.",
                        "method": "guardrail",
                        "next_state": state.get("next_state"),
                        "handoff": False,
                        "metadata": {},
                        "domain": state.get("domain"),
                        "mcp_tools": [],
                    },
                    # Evita vazar resultados/rota do turno anterior quando o
                    # bloqueio acontece antes do roteamento do turno atual.
                    "mcp_tools": [],
                    "mcp_results": [],
                    "judge_results": [],
                    **({
                        "pending_domain_workflow": None,
                        "pending_tool_clarification": None,
                        "workflow_input_reprompt": None,
                        "active_transaction": None,
                        "selected_tool_call": {},
                        "pending_tool_call": {},
                        "missing_parameters": [],
                        "confirmation_required": False,
                        "confirmation_received": False,
                        "transaction_pre_validation": None,
                        "tool_policy_result": None,
                        "tool_terminal_result": None,
                        "transaction_confirmation_message_override": None,
                        "next_state": None,
                        "mcp_tools": [],
                        "mcp_results": [],
                        "relevant_transaction_evidence": [],
                        "route": None,
                        "intent": None,
                        "route_decision": {},
                        "active_agent": None,
                        "route_bypassed": False,
                        "continuity_signal": {},
                        "workflow_id": None,
                        "transaction_status": None,
                        "operational_context_boundary_pending": False,
                        "operational_context_reset": True,
                    } if reset_operational_context else {}),
                    "blocked": True,
                }
            return {
                "sanitized_input": sanitized,
                "guardrail_decisions": [d.model_dump() for d in decisions],
                "blocked": False,
                **({
                    "pending_domain_workflow": None,
                    "pending_tool_clarification": None,
                    "workflow_input_reprompt": None,
                } if terminal_interaction else {}),
            }

    async def routing_decision(self, state):
        mode = getattr(self.settings, "ROUTING_MODE", "router")
        async with self.telemetry.span(
            "workflow.routing_decision",
            session_id=state.get("conversation_key") or state.get("session_id"),
            input={
                "mode": mode,
                "text": state.get("sanitized_input") or state.get("user_text"),
                "previous_state": state.get("next_state"),
            },
        ):
            if mode == "supervisor":
                plan = await self.supervisor.route_plan(state)
                await self.langgraph_telemetry.edge("routing_decision", "supervisor_agent", state, {"method": "supervisor", "intent": plan.intent, "confidence": plan.confidence})
                return {
                    "route": "supervisor_agent",
                    "intent": plan.intent,
                    "supervisor_plan": {
                        "agents": plan.agents,
                        "intent": plan.intent,
                        "confidence": plan.confidence,
                        "reason": plan.reason,
                        "metadata": plan.metadata,
                    },
                    "route_decision": {
                        "route": "supervisor_agent",
                        "agent": "supervisor",
                        "intent": plan.intent,
                        "confidence": plan.confidence,
                        "reason": plan.reason,
                        "method": "supervisor",
                        "metadata": plan.metadata,
                    },
                }

            decision = await self.router.route(state)
            await self.langgraph_telemetry.edge("routing_decision", decision.route, state, {"method": getattr(decision, "method", None), "intent": decision.intent, "confidence": decision.confidence})
            await self.observer.emit_ic(
                "ROUTE_SELECTED",
                {
                    "session_id": state.get("conversation_key") or state.get("session_id"),
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "route": decision.route,
                    "intent": decision.intent,
                    "confidence": decision.confidence,
                    "method": getattr(decision, "method", None),
                },
                component="workflow.routing_decision",
            )
            return {
                "route": decision.route,
                "intent": decision.intent,
                "route_decision": decision.model_dump(mode="json"),
                "domain": decision.domain,
                "mcp_tools": decision.mcp_tools,
                "next_state": decision.next_state,
                "active_agent": decision.agent,
                "route_bypassed": decision.method == "continuity",
                "session_control": (decision.metadata or {}).get("session_control", ""),
                "human_handoff_requested": (decision.metadata or {}).get("session_control") == "HUMAN_HANDOFF",
                "session_ended": (decision.metadata or {}).get("session_control") == "END_SESSION",
                "continuity_signal": {
                    "decision": (decision.metadata or {}).get("continuity_decision"),
                    "confidence": decision.confidence if decision.method == "continuity" else None,
                    "reason": decision.reason if decision.method == "continuity" else None,
                    "profile": (decision.metadata or {}).get("continuity_profile"),
                } if decision.method == "continuity" else {},
            }

    async def conversation_policy(self, state):
        decision = evaluate_contas_conversation_policy(state)
        if decision is None:
            return {}
        patch = dict(decision.patch or {})
        route_metadata_patch = patch.pop("_route_metadata", {}) if isinstance(patch.get("_route_metadata", {}), dict) else {}
        if decision.route:
            patch["route"] = decision.route
        if decision.intent:
            patch["intent"] = decision.intent
        if decision.answer is not None:
            patch["answer"] = decision.answer
        if decision.reason:
            current = state.get("route_decision") if isinstance(state.get("route_decision"), dict) else {}
            patch["route_decision"] = {**current, "route": patch.get("route", state.get("route")), "intent": patch.get("intent", state.get("intent")), "reason": decision.reason, "metadata": {**(current.get("metadata") or {}), **route_metadata_patch, "contas_conversation_policy": decision.reason}}
        return patch

    async def conversation_policy_response(self, state):
        return {"answer": str(state.get("answer") or ""), "next_state": state.get("next_state") or "CONVERSATION_POLICY"}

    async def faturas_agent(self, state):
        async with self.telemetry.span(
            "workflow.agent.billing",
            session_id=state.get("conversation_key") or state.get("session_id"),
            input={"intent": state.get("intent")},
        ):
            return await self.faturas.run(state)

    async def vas_agent(self, state):
        async with self.telemetry.span(
            "workflow.agent.product",
            session_id=state.get("conversation_key") or state.get("session_id"),
            input={"intent": state.get("intent")},
        ):
            return await self.vas.run(state)

    async def contestacao_agent(self, state):
        async with self.telemetry.span(
            "workflow.agent.orders",
            session_id=state.get("conversation_key") or state.get("session_id"),
            input={"intent": state.get("intent")},
        ):
            return await self.contestacao.run(state)

    async def suporte_contas_agent(self, state):
        async with self.telemetry.span(
            "workflow.agent.support",
            session_id=state.get("conversation_key") or state.get("session_id"),
            input={"intent": state.get("intent")},
        ):
            return await self.suporte_contas.run(state)

    async def supervisor_agent(self, state):
        """Executa um ou mais agentes no modo supervisor e consolida a resposta.

        Este nó mantém o desenho de supervisor sem obrigar o restante do workflow
        a conhecer quantos agentes foram acionados. Cada execução especializada
        recebe o mesmo estado, mas com route/active_agent atualizados.
        """
        plan = state.get("supervisor_plan") or {}
        agents = plan.get("agents") or ["faturas_agent"]
        handlers = {
            "faturas_agent": self.faturas.run,
            "vas_agent": self.vas.run,
            "contestacao_agent": self.contestacao.run,
            "suporte_contas_agent": self.suporte_contas.run,
        }
        partials = []
        mcp_results = []
        async with self.telemetry.span(
            "workflow.supervisor_agent",
            session_id=state.get("conversation_key") or state.get("session_id"),
            input={"agents": agents, "intent": state.get("intent")},
        ):
            for agent_name in agents:
                handler = handlers.get(agent_name)
                if handler is None:
                    continue
                child_state = {**state, "route": agent_name, "active_agent": agent_name}
                result = await handler(child_state)
                partials.append({"agent": agent_name, "answer": result.get("answer", "")})
                mcp_results.extend(result.get("mcp_results") or [])

        if len(partials) == 1:
            answer = partials[0]["answer"]
        else:
            joined = "\n\n".join(f"{p['agent']}: {p['answer']}" for p in partials)
            answer = (
                "[Supervisor] Consolidação de múltiplos agentes acionados.\n"
                f"{joined}"
            )
        return {
            "answer": answer,
            "supervisor_results": partials,
            "mcp_results": mcp_results,
            "next_state": "SUPERVISOR_ACTIVE",
        }

    async def handoff(self, state):
        async with self.telemetry.span("workflow.handoff", session_id=state.get("session_id")):
            target = (state.get("route_decision") or {}).get("metadata", {}).get("target_agent")
            answer = (
                "Vou redirecionar sua solicitação para o especialista correto. "
                f"Destino sugerido: {target or 'agente especializado'}."
            )
            return {"answer": answer}

    async def human_handoff(self, state):
        session_id = state.get("conversation_key") or state.get("session_id")
        async with self.telemetry.span("workflow.human_handoff", session_id=session_id):
            try:
                if self.tool_router:
                    runtime_context = (state.get("context") or {}).get("business_context") or {}
                    await self.tool_router.call(
                        "finalizar_atendimento",
                        {"status": "nao_resolvido", "summary": "Handoff humano solicitado pelo agent_framework_oci", "confirmed": True},
                        business_context=runtime_context,
                        original_context=state.get("context") or {},
                    )
            except Exception:
                pass
            answer = str(getattr(self.settings, "HUMAN_HANDOFF_MESSAGE", "Vou encaminhar seu atendimento para uma pessoa."))
            await self.telemetry.event(
                "session.human_handoff.requested",
                {
                    "session_id": session_id,
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "reason": (state.get("route_decision") or {}).get("reason"),
                },
            )
            return {
                "answer": answer,
                "session_control": "HUMAN_HANDOFF",
                "human_handoff_requested": True,
                "session_ended": True,
                "terminal_status": "nao_resolvido",
                "next_state": "HUMAN_HANDOFF_REQUESTED",
                # Human handoff terminates the operational interaction. Keep the
                # durable history/checkpoint, but do not leave a paused workflow
                # or transactional latch active in the live session.
                "pending_domain_workflow": None,
                "active_transaction": None,
                "transaction_pre_validation": None,
                "transaction_status": "CANCELLED",
                "pending_tool_call": {},
                "selected_tool_call": {},
                "missing_parameters": [],
                "confirmation_required": False,
                "confirmation_received": False,
                "mcp_results": [],
            }

    async def end_session(self, state):
        session_id = state.get("conversation_key") or state.get("session_id")
        async with self.telemetry.span("workflow.end_session", session_id=session_id):
            # Preserva efeitos colaterais de negócio do Contas (protocolos/ICs)
            # sem devolver a orquestração ao runtime do framework. Falha aqui não impede
            # o encerramento controlado pelo framework.
            try:
                if self.tool_router:
                    runtime_context = (state.get("context") or {}).get("business_context") or {}
                    await self.tool_router.call(
                        "finalizar_atendimento",
                        {"status": str(state.get("terminal_status") or "resolvido"), "summary": "Encerramento solicitado pelo agent_framework_oci", "confirmed": True},
                        business_context=runtime_context,
                        original_context=state.get("context") or {},
                    )
            except Exception:
                pass
            answer = str(state.get("conversation_terminal_message") or getattr(self.settings, "END_SESSION_MESSAGE", "Atendimento encerrado. Obrigado pelo contato."))
            await self.telemetry.event(
                "session.end.requested",
                {
                    "session_id": session_id,
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "reason": (state.get("route_decision") or {}).get("reason"),
                },
            )
            return {
                "answer": answer,
                "session_control": "END_SESSION",
                "session_ended": True,
                "terminal_status": str(state.get("terminal_status") or "resolvido"),
                "human_handoff_requested": False,
                "next_state": "SESSION_ENDED",
            }

    async def output_supervisor(self, state):
        """Valida a resposta candidata com o OutputSupervisor corporativo.

        Este nó não substitui o roteador/supervisor multiagente. Ele roda após o
        agente gerar `answer` e antes dos judges/persistência, produzindo campos
        supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver.
        """
        if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)):
            return {
                "output_guardrails_already_applied": False,
                "supervisor_action": "disabled",
                "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
            }

        candidate = state.get("answer") or ""
        context = {
            **self._output_guardrail_context(state),
            "tenant_id": state.get("tenant_id"),
            "agent_id": state.get("agent_id"),
            "session_id": state.get("conversation_key") or state.get("session_id"),
            "route": state.get("route"),
            "intent": state.get("intent"),
            "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
        }
        async with self.telemetry.span(
            "workflow.output_supervisor",
            session_id=state.get("conversation_key") or state.get("session_id"),
            input=candidate,
        ):
            decision = await self.output_supervisor_engine.evaluate(candidate, context)
            action = decision.action.value
            await self.telemetry.event(
                "output_supervisor.completed",
                {
                    "session_id": context["session_id"],
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "action": action,
                    "approved": decision.approved,
                    "guidance": decision.guidance,
                },
            )

            await self.observer.emit_ic(
                "IC.OUTPUT_SUPERVISOR_COMPLETED",
                {
                    "session_id": context["session_id"],
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "route": state.get("route"),
                    "intent": state.get("intent"),
                    "action": action,
                    "approved": decision.approved,
                    "result_count": len(decision.results),
                },
                component="workflow.output_supervisor",
            )

            if decision.action in {RailAction.ALLOW, RailAction.SANITIZE, RailAction.OBSERVE}:
                final_answer = decision.candidate
            elif decision.action == RailAction.HANDOVER:
                final_answer = "Vou encaminhar seu atendimento para continuidade com um especialista."
            else:
                final_answer = decision.fallback_message

            return {
                "answer": final_answer,
                "final_answer": final_answer,
                "supervisor_action": action,
                "supervisor_guidance": decision.guidance,
                "supervisor_attempt": int(state.get("supervisor_attempt", 0)) + (1 if decision.action == RailAction.RETRY else 0),
                "supervisor_handover_reason": decision.handover_reason,
                "output_supervisor_results": [
                    {
                        "code": r.code,
                        "action": r.action.value,
                        "reason": r.reason,
                        "guidance": r.guidance,
                        "metadata": r.metadata,
                    }
                    for r in decision.results
                ],
                "output_guardrails_already_applied": True,
                "guardrail_decisions": state.get("guardrail_decisions", [])
                + [item for r in decision.results for item in (r.metadata or {}).get("framework_decisions", [])],
            }

    async def output_guardrails(self, state):
        if state.get("output_guardrails_already_applied"):
            return {"final_answer": state.get("final_answer") or state.get("answer") or ""}

        async with self.telemetry.span(
            "workflow.output_guardrails",
            session_id=state.get("conversation_key") or state.get("session_id"),
            input=state.get("answer"),
        ):
            await self.observer.emit_grl(
                "001",
                {
                    "session_id": state.get("conversation_key") or state.get("session_id"),
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "phase": "output",
                    "route": state.get("route"),
                    "intent": state.get("intent"),
                },
                component="workflow.output_guardrails.start",
            )
            guardrail_context = self._output_guardrail_context(state)
            final, decisions = await self.guardrails.run_output(
                state["answer"], guardrail_context
            )
            for _decision in decisions:
                await self.guardrail_telemetry.evaluated("output", _decision)
                await self.observer.emit_grl(
                    "002" if _decision.allowed else "004",
                    {
                        "session_id": state.get("conversation_key") or state.get("session_id"),
                        "tenant_id": state.get("tenant_id"),
                        "agent_id": state.get("agent_id"),
                        "phase": "output",
                        "rail_code": getattr(_decision, "code", None),
                        "allowed": bool(_decision.allowed),
                        "reason": getattr(_decision, "reason", None),
                    },
                    component="workflow.output_guardrails.decision",
                )
                if not _decision.allowed:
                    await self.guardrail_telemetry.blocked("output", _decision)
            await self.telemetry.event(
                "guardrails.output.completed",
                {
                    "session_id": state.get("conversation_key") or state.get("session_id"),
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "decisions": [d.model_dump() for d in decisions],
                },
            )
            await self.observer.emit_grl(
                "009",
                {
                    "session_id": state.get("conversation_key") or state.get("session_id"),
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "phase": "output",
                    "blocked": any(not d.allowed for d in decisions),
                    "decision_count": len(decisions),
                },
                component="workflow.output_guardrails.final",
            )
            return {
                "final_answer": final,
                "guardrail_decisions": state.get("guardrail_decisions", [])
                + [d.model_dump() for d in decisions],
            }

    async def judge(self, state):
        async with self.telemetry.span(
            "workflow.judge",
            session_id=state.get("conversation_key") or state.get("session_id"),
            input={"question": state.get("user_text"), "answer": state.get("final_answer")},
        ):
            judge_context = dict(state.get("context", {}) or {})
            judge_context["mcp_results"] = state.get("mcp_results", [])
            relevant_transaction_evidence = list(state.get("relevant_transaction_evidence") or [])
            judge_context["transaction_evidence"] = relevant_transaction_evidence
            current_evidence = list(state.get("mcp_results", []) or [])
            current_evidence.extend(relevant_transaction_evidence)
            judge_context["evidence"] = current_evidence or judge_context.get("evidence")
            judge_context["route"] = state.get("route")
            judge_context["intent"] = state.get("intent")
            # Judge sampling must see the finalized transaction state. These
            # fields are populated by the agent/tool runtime before this node.
            for key in (
                "transaction_status",
                "confirmation_required",
                "confirmation_received",
                "tool_policy_result",
                "selected_tool_call",
                "pending_tool_call",
            ):
                judge_context[key] = state.get(key)
            judge_context["transactional_tools"] = [
                result.get("tool_name")
                for result in state.get("mcp_results", [])
                if isinstance(result, dict)
                and (
                    (result.get("metadata") or {}).get("operation_type") == "transactional"
                    or result.get("awaiting_confirmation")
                    or result.get("transaction_status")
                )
            ]
            results = await self.judges.evaluate_all(
                state["user_text"], state["final_answer"], judge_context
            )
            for _result in results:
                await self.judge_telemetry.evaluated(_result)
            await self.telemetry.event(
                "judges.completed",
                {
                    "session_id": state.get("conversation_key") or state.get("session_id"),
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "results": [r.model_dump() for r in results],
                },
            )
            return {"judge_results": [r.model_dump() for r in results]}

    async def supervisor_review(self, state):
        async with self.telemetry.span(
            "workflow.supervisor_review",
            session_id=state.get("conversation_key") or state.get("session_id"),
            input=state.get("final_answer"),
        ):
            ok, answer = await self.supervisor.review(
                state["final_answer"], state.get("context", {})
            )
            await self.telemetry.event(
                "supervisor.review.completed",
                {"session_id": state.get("session_id"), "approved": ok},
            )
            return {"final_answer": answer if ok else answer}

    async def load_long_term_memory(self, state):
        """Carrega LTM antes do roteamento e mantém o resultado no estado.

        A carga explícita evita depender apenas do agente selecionado para realizar
        a recuperação e facilita o diagnóstico de identidade/namespace.
        """
        try:
            memories = await self.long_term_memory_manager.load(state)
            serialized = []
            context_lines = []
            for item in memories or []:
                if hasattr(item, "model_dump"):
                    data = item.model_dump(mode="json")
                elif hasattr(item, "__dict__"):
                    data = dict(item.__dict__)
                elif isinstance(item, dict):
                    data = dict(item)
                else:
                    data = {"value": str(item)}
                serialized.append(data)
                key = data.get("key") or data.get("memory_key") or data.get("category") or "memory"
                value = data.get("value") or data.get("memory_value")
                if value not in (None, ""):
                    context_lines.append(f"- {key}: {value}")

            return {
                "long_term_memories": serialized,
                "long_term_memory_context": "\n".join(context_lines),
            }
        except Exception as exc:
            await self.telemetry.event(
                "long_term_memory.load.failed",
                {
                    "session_id": state.get("conversation_key") or state.get("session_id"),
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "subject_key": state.get("long_term_memory_subject_key"),
                    "error": str(exc),
                },
            )
            return {
                "long_term_memories": [],
                "long_term_memory_context": "",
                "long_term_memory_load_error": str(exc),
            }

    async def persist_long_term_memory(self, state):
        try:
            result = await self.long_term_memory_manager.persist_turn(state)
            await self.telemetry.event(
                "long_term_memory.persist.completed",
                {
                    "session_id": state.get("conversation_key") or state.get("session_id"),
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "subject_key": state.get("long_term_memory_subject_key"),
                    "result": result,
                },
            )
            return {"long_term_memory_write_result": result}
        except Exception as exc:
            await self.telemetry.event(
                "long_term_memory.persist.failed",
                {
                    "session_id": state.get("conversation_key") or state.get("session_id"),
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "subject_key": state.get("long_term_memory_subject_key"),
                    "error": str(exc),
                },
            )
            return {"long_term_memory_write_result": {"saved": 0, "error": str(exc)}}

    async def persist(self, state):
        async with self.telemetry.span(
            "workflow.persist",
            session_id=state.get("conversation_key") or state.get("session_id"),
            input={"route": state.get("route"), "intent": state.get("intent")},
        ):
            await self.observer.emit_ic(
                "AGENT_COMPLETED",
                {
                    "session_id": state.get("conversation_key") or state["session_id"],
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "route": state.get("route"),
                    "intent": state.get("intent"),
                    "route_decision": state.get("route_decision"),
                    "judges": state.get("judge_results", []),
                    "mcp_tools": state.get("mcp_tools", []),
                    "mcp_results": state.get("mcp_results", []),
                    "transaction_evidence": state.get("relevant_transaction_evidence", []),
                },
            )

            await self.observer.emit_noc(
                "006",
                {
                    "session_id": state.get("conversation_key") or state["session_id"],
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "route": state.get("route"),
                    "intent": state.get("intent"),
                    "answer_chars": len(state.get("final_answer") or ""),
                },
                component="workflow.persist",
            )

            await self.telemetry.event(
                "agent.completed",
                {
                    "session_id": state.get("conversation_key") or state["session_id"],
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "route": state.get("route"),
                    "intent": state.get("intent"),
                    "answer_chars": len(state.get("final_answer") or ""),
                },
            )
            if state.get("operational_context_reset"):
                state["operational_context_reset"] = False
            return state

    async def ainvoke(self, state):
        thread_id = state.get("conversation_key") or state["session_id"]
        config = {"configurable": {"thread_id": thread_id}}
        async with self.telemetry.span(
            "workflow.langgraph.ainvoke",
            session_id=state.get("conversation_key") or state.get("session_id"),
            user_id=state.get("context", {}).get("user_id"),
            input={"user_text": state.get("user_text")},
            tags=["langgraph", "agent-workflow", f"routing-mode:{getattr(self.settings, 'ROUTING_MODE', 'router')}",],
        ):
            await self.workflow_telemetry.started("agent_workflow", state)
            await self.observer.emit_noc(
                "001",
                {
                    "session_id": state.get("conversation_key") or state.get("session_id"),
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "channel_id": (state.get("context") or {}).get("channel"),
                    "message_id": (state.get("context") or {}).get("message_id"),
                    "ura_call_id": (state.get("context") or {}).get("ura_call_id"),
                },
                component="workflow.ainvoke",
            )
            await self.observer.emit_ic(
                "AGENT_STARTED",
                {
                    "session_id": state.get("conversation_key") or state.get("session_id"),
                    "tenant_id": state.get("tenant_id"),
                    "agent_id": state.get("agent_id"),
                    "channel_id": (state.get("context") or {}).get("channel"),
                    "message_id": (state.get("context") or {}).get("message_id"),
                    "user_text_chars": len(state.get("user_text") or ""),
                },
                component="workflow.ainvoke",
            )
            try:
                result = await self.graph.ainvoke(state, config=config)
                await self.workflow_telemetry.completed("agent_workflow", result)
                return result
            except Exception as exc:
                await self.workflow_telemetry.failed("agent_workflow", exc)
                await self.observer.emit_noc(
                    "005",
                    {
                        "session_id": state.get("conversation_key") or state.get("session_id"),
                        "tenant_id": state.get("tenant_id"),
                        "agent_id": state.get("agent_id"),
                        "error": str(exc),
                        "exception_type": exc.__class__.__name__,
                    },
                    component="workflow.ainvoke",
                )
                raise
