"""
BaseAgent - Universal agent class for Kaizen framework.

This module implements the core BaseAgent class that serves as the foundation
for all agent types in the Kaizen framework. It provides:
- Unified configuration management via BaseAgentConfig
- Lazy framework initialization
- Workflow generation from signatures
- Strategy-based execution delegation
- Mixin composition for features

Architecture:
- Inherits from kailash.workflow.node.Node for Core SDK integration
- Uses Strategy Pattern for execution (SingleShotStrategy, MultiCycleStrategy)
- Uses Mixin Composition for features (LoggingMixin, PerformanceMixin, etc.)

Extension Points (7 total):
1. _default_signature() - Override to provide agent-specific signature
2. _default_strategy() - Override to provide agent-specific strategy
3. _generate_system_prompt() - Override to customize prompt generation
4. _validate_signature_output() - Override to add output validation
5. _pre_execution_hook() - Override to add pre-execution logic
6. _post_execution_hook() - Override to add post-execution logic
7. _handle_error() - Override to customize error handling

References:
- ADR-006: Agent Base Architecture design
- TODO-157: BaseAgent Architecture Unified System (6-8 weeks)
- Phase 0 Validation: Performance baseline (95.53ms avg init, 36.53MB memory)

Author: Kaizen Framework Team
Created: 2025-10-01
"""

import logging
from typing import TYPE_CHECKING, Any, Dict, List, Optional

# MCP client import (for MCP integration)
from kailash.mcp_server.client import MCPClient

# Core SDK imports
from kailash.nodes.base import Node, NodeParameter
from kailash.workflow.builder import WorkflowBuilder

# Type checking imports (not available at runtime in all environments)
if TYPE_CHECKING:
    try:
        from kailash.nodes.ai.a2a import (
            A2AAgentCard,
            Capability,
            CollaborationStyle,
            PerformanceMetrics,
            ResourceRequirements,
        )
    except ImportError:
        pass

# Kaizen framework imports
from kaizen.signatures import InputField, OutputField, Signature

# Tool system imports (for tool calling integration)
from kaizen.tools.executor import ToolExecutor
from kaizen.tools.registry import ToolRegistry
from kaizen.tools.types import (
    DangerLevel,
    ToolCategory,
    ToolDefinition,
    ToolParameter,
    ToolResult,
)

from .config import BaseAgentConfig

# Re-export BaseAgentConfig for convenience
__all__ = ["BaseAgent", "BaseAgentConfig"]

# Strategy imports (to be implemented)
# from kaizen.strategies.base_strategy import ExecutionStrategy
# from kaizen.strategies.single_shot import SingleShotStrategy
# from kaizen.strategies.multi_cycle import MultiCycleStrategy

logger = logging.getLogger(__name__)


class BaseAgent(Node):
    """
    Universal base agent class with strategy-based execution and mixin composition.

    BaseAgent provides a unified foundation for all agent types, eliminating
    the massive code duplication (1,537 lines → ~150 lines, 90%+ reduction)
    present in current examples (SimpleQA, ChainOfThought, ReAct).

    Key Features:
    - **Lazy Initialization**: Heavy dependencies loaded only when needed
    - **Strategy Pattern**: Pluggable execution strategies (single-shot, multi-cycle)
    - **Mixin Composition**: Modular features (logging, performance, error handling)
    - **Extension Points**: 7 customization hooks for agent-specific logic
    - **Core SDK Integration**: to_workflow() for workflow composition

    Performance Targets (from Phase 0 baseline):
    - Initialization: <50ms (baseline avg: 95.53ms)
    - Agent Creation: <10ms (baseline avg: 0.08ms)
    - Memory: <40MB (baseline avg: 36.53MB)

    Example Usage:
        >>> from kaizen.core.base_agent import BaseAgent
        >>> from kaizen.core.config import BaseAgentConfig
        >>> from kaizen.signatures import Signature, InputField, OutputField
        >>>
        >>> # Create configuration
        >>> config = BaseAgentConfig(
        ...     llm_provider="openai",
        ...     model="gpt-4",
        ...     temperature=0.1,
        ...     logging_enabled=True,
        ...     performance_enabled=True
        ... )
        >>>
        >>> # Create signature
        >>> class QASignature(Signature):
        ...     question: str = InputField(desc="Question to answer")
        ...     answer: str = OutputField(desc="Answer to question")
        >>>
        >>> # Create agent
        >>> agent = BaseAgent(config=config, signature=QASignature())
        >>>
        >>> # Generate workflow for execution
        >>> workflow = agent.to_workflow()
        >>>
        >>> # Execute using Core SDK runtime
        >>> from kailash.runtime.local import LocalRuntime
        >>> runtime = LocalRuntime()
        >>> results, run_id = runtime.execute(workflow.build())

    Extension Pattern:
        >>> class SimpleQAAgent(BaseAgent):
        ...     def _default_signature(self) -> Signature:
        ...         return QASignature()
        ...
        ...     def _generate_system_prompt(self) -> str:
        ...         return "You are a helpful Q&A assistant."
        ...
        ...     def _validate_signature_output(self, output: Dict[str, Any]) -> bool:
        ...         super()._validate_signature_output(output)
        ...         # Custom validation
        ...         if not 0 <= output.get('confidence', 0) <= 1:
        ...             raise ValueError("Confidence must be between 0 and 1")
        ...         return True
        >>>
        >>> # Use simplified agent
        >>> qa_agent = SimpleQAAgent(config=config)
        >>> # Signature and prompt automatically configured

    Notes:
    - This is a SKELETON implementation for TDD Phase 1
    - All methods will be implemented to pass 119 test cases
    - DO NOT implement methods yet - tests drive implementation
    """

    def __init__(
        self,
        config: Any,  # BaseAgentConfig or any domain config (auto-converted)
        signature: Optional[Signature] = None,
        strategy: Optional[Any] = None,  # ExecutionStrategy when implemented
        memory: Optional[Any] = None,  # KaizenMemory when provided (Phase 1)
        shared_memory: Optional[Any] = None,  # SharedMemoryPool when provided (Phase 2)
        agent_id: Optional[str] = None,  # Agent identifier for shared memory (Phase 2)
        control_protocol: Optional[
            Any
        ] = None,  # ControlProtocol for user interaction (Week 10)
        tool_registry: Optional[
            ToolRegistry
        ] = None,  # Tool registry for tool execution
        tool_executor: Optional[ToolExecutor] = None,  # Optional custom tool executor
        mcp_servers: Optional[List[Dict[str, Any]]] = None,  # MCP server configurations
        **kwargs,
    ):
        """
        Initialize BaseAgent with lazy loading pattern.

        Args:
            config: Agent configuration - can be:
                   - BaseAgentConfig instance (used directly)
                   - Domain config (auto-converted using from_domain_config())
            signature: Optional signature (uses _default_signature() if None)
            strategy: Optional execution strategy (uses _default_strategy() if None)
            memory: Optional conversation memory (KaizenMemory instance, Phase 1)
            shared_memory: Optional shared memory pool (SharedMemoryPool, Phase 2)
            agent_id: Optional agent identifier (auto-generated if None, Phase 2)
            control_protocol: Optional control protocol for user interaction (ControlProtocol, Week 10)
            tool_registry: Optional tool registry for tool execution capabilities
            tool_executor: Optional custom tool executor (auto-created if registry provided)
            **kwargs: Additional arguments passed to Node.__init__

        Example:
            >>> # Option 1: Use BaseAgentConfig directly
            >>> config = BaseAgentConfig(llm_provider="openai", model="gpt-4")
            >>> agent = BaseAgent(config=config, signature=QASignature())
            >>>
            >>> # Option 2: Use domain config (auto-converted)
            >>> @dataclass
            >>> class MyWorkflowConfig:
            ...     llm_provider: str = "openai"
            ...     model: str = "gpt-4"
            ...     my_custom_param: str = "value"
            >>>
            >>> config = MyWorkflowConfig()
            >>> agent = BaseAgent(config=config, signature=QASignature())
            >>> # Config automatically converted to BaseAgentConfig

        Notes:
        - Framework initialization is LAZY (not loaded in __init__)
        - Agent instance is LAZY (not created in __init__)
        - Workflow is LAZY (not generated in __init__)
        - Mixins applied based on config feature flags
        - Domain configs auto-converted via BaseAgentConfig.from_domain_config()

        Performance Target: <50ms initialization time

        Phase 2 Addition (Week 3):
        - shared_memory: SharedMemoryPool for multi-agent collaboration
        - agent_id: Identifier for insight attribution (auto-generated if None)
        """
        # Task 1.13: Implement BaseAgent.__init__ with lazy initialization
        # IMPORTANT: Set signature/strategy BEFORE calling super().__init__()
        # because Node.__init__() calls get_parameters() which needs signature

        # UX Improvement: Auto-convert domain config to BaseAgentConfig if needed
        if not isinstance(config, BaseAgentConfig):
            config = BaseAgentConfig.from_domain_config(config)

        # Store configuration early (needed by _default_strategy and _default_signature)
        # Note: Node.__init__ will overwrite this, so we save it and restore after
        self.config = config
        agent_config = config

        # Set signature (use provided or default)
        self.signature = (
            signature if signature is not None else self._default_signature()
        )

        # Set strategy (use provided or default)
        self.strategy = strategy if strategy is not None else self._default_strategy()

        # Set memory (Week 2 Phase 1 addition)
        self.memory = memory

        # Set shared memory (Week 3 Phase 2 addition)
        self.shared_memory = shared_memory

        # Set agent_id (Week 3 Phase 2 addition)
        # Auto-generate if not provided using object id
        self.agent_id = agent_id if agent_id is not None else f"agent_{id(self)}"

        # Set control protocol (Week 10 addition)
        self.control_protocol = control_protocol

        # Initialize tool system (Tool Integration)
        if tool_registry is not None:
            self._tool_registry = tool_registry
            self._tool_executor = tool_executor or ToolExecutor(
                registry=tool_registry,
                control_protocol=control_protocol,
                auto_approve_safe=True,
                timeout=30.0,
            )
        else:
            self._tool_registry = None
            self._tool_executor = None

        # Initialize MCP system (MCP Integration)
        self._mcp_servers = mcp_servers
        if mcp_servers is not None:
            self._mcp_client = MCPClient()
            # Initialize discovery caches
            self._discovered_mcp_tools = {}
            self._discovered_mcp_resources = {}
            self._discovered_mcp_prompts = {}
        else:
            self._mcp_client = None
            self._discovered_mcp_tools = {}
            self._discovered_mcp_resources = {}
            self._discovered_mcp_prompts = {}

        # Now call Node.__init__ (it will call get_parameters())
        # Note: Node.__init__ will set self.config to a dict, we restore it after
        super().__init__(**kwargs)

        # Restore config to BaseAgentConfig (Node.__init__ overwrites it with a dict)
        self.config = agent_config

        # Lazy initialization (all None until needed)
        self._framework = None
        self._agent = None
        self._workflow = None

        # Task 2.7: Initialize WorkflowGenerator for strategy use
        from .workflow_generator import WorkflowGenerator

        self.workflow_generator = WorkflowGenerator(
            config=self.config, signature=self.signature
        )

        # Mixin state tracking (for testing)
        self._mixins_applied = []

        # Apply mixins based on config feature flags
        if config.logging_enabled:
            self._apply_logging_mixin()

        if config.performance_enabled:
            self._apply_performance_mixin()

        if config.error_handling_enabled:
            self._apply_error_handling_mixin()

        if config.batch_processing_enabled:
            self._apply_batch_processing_mixin()

        if config.memory_enabled:
            self._apply_memory_mixin()

        if config.transparency_enabled:
            self._apply_transparency_mixin()

        if config.mcp_enabled:
            self._apply_mcp_integration_mixin()

    def _apply_logging_mixin(self):
        """Apply logging mixin (placeholder until Phase 3)."""
        self._mixins_applied.append("LoggingMixin")

    def _apply_performance_mixin(self):
        """Apply performance mixin (placeholder until Phase 3)."""
        self._mixins_applied.append("PerformanceMixin")

    def _apply_error_handling_mixin(self):
        """Apply error handling mixin (placeholder until Phase 3)."""
        self._mixins_applied.append("ErrorHandlingMixin")

    def _apply_batch_processing_mixin(self):
        """Apply batch processing mixin (placeholder until Phase 3)."""
        self._mixins_applied.append("BatchProcessingMixin")

    def _apply_memory_mixin(self):
        """Apply memory mixin (placeholder until Phase 3)."""
        self._mixins_applied.append("MemoryMixin")

    def _apply_transparency_mixin(self):
        """Apply transparency mixin (placeholder until Phase 3)."""
        self._mixins_applied.append("TransparencyMixin")

    def _apply_mcp_integration_mixin(self):
        """Apply MCP integration mixin (placeholder until Phase 3)."""
        self._mixins_applied.append("MCPIntegrationMixin")

    def get_parameters(self) -> Dict[str, NodeParameter]:
        """
        Get parameter schema for agent contract.

        Returns schema describing inputs/outputs based on signature.
        Required by Node base class for workflow composition.

        Returns:
            Dict[str, NodeParameter]: Parameter definitions for Node

        Example:
            >>> params = agent.get_parameters()
            >>> print(params['question'])
            NodeParameter(name='question', type=str, required=True, ...)
        """
        # Task 1.14: Implement BaseAgent.get_parameters()
        # Return Dict[str, NodeParameter] as expected by Node base class
        parameters = {}

        # Extract input fields from signature
        if hasattr(self.signature, "input_fields"):
            for field in self.signature.input_fields:
                field_name = field.name if hasattr(field, "name") else "input"
                field_type = field.type if hasattr(field, "type") else str
                field_desc = (
                    field.desc if hasattr(field, "desc") else f"{field_name} parameter"
                )
                is_required = not (hasattr(field, "optional") and field.optional)

                parameters[field_name] = NodeParameter(
                    name=field_name,
                    type=field_type,
                    required=is_required,
                    description=field_desc,
                )

        # Note: Output fields not included in Node parameters (outputs determined by run())
        # Node parameters are for inputs only

        return parameters

    def run(self, **inputs) -> Dict[str, Any]:
        """
        Execute agent with strategy-based execution and error handling.

        Execution flow:
        1. Load individual memory context (if memory enabled and session_id provided)
        2. Read shared insights (if shared_memory enabled, Phase 2)
        3. Call _pre_execution_hook(inputs)
        4. Delegate to strategy.execute() (handles both sync and async)
        5. Call _post_execution_hook(result)
        6. Save turn to individual memory (if memory enabled and session_id provided)
        7. Write insight to shared memory (if shared_memory enabled and result has _write_insight, Phase 2)
        8. Handle errors via _handle_error() if errors occur

        Args:
            **inputs: Input parameters matching signature input fields.
                     Special parameter: session_id (str) - for memory persistence

        Returns:
            Dict[str, Any]: Results matching signature output fields

        Raises:
            ValueError: If inputs don't match signature
            RuntimeError: If execution fails (when error_handling_enabled=False)

        Example:
            >>> result = agent.run(question="What is 2+2?", context=None)
            >>> print(result)
            {
                'answer': '2+2 equals 4',
                'confidence': 0.99
            }

            >>> # With memory and session
            >>> result = agent.run(question="What is 2+2?", session_id="session1")

        Note:
            Phase 0A: Now handles async strategies (AsyncSingleShotStrategy)
            by running them in the event loop synchronously.
            Week 2 Phase 1: Added individual memory integration with session_id support.
            Week 3 Phase 2: Added shared memory integration for multi-agent collaboration.
                           Agents read insights via _shared_insights input.
                           Agents write insights via _write_insight result key.
        """
        # Task 0A.1: Handle async strategies in sync run() method
        import asyncio
        import inspect
        from datetime import datetime

        # Extract session_id if provided (Week 2 Phase 1 addition)
        session_id = inputs.pop("session_id", None)

        try:
            # Week 2 Phase 1: Load individual memory context if enabled
            memory_context = {}
            if self.memory and session_id:
                memory_context = self.memory.load_context(session_id)
                # Merge memory context into inputs for agent awareness
                inputs["_memory_context"] = memory_context

            # Week 3 Phase 2: Read shared insights if enabled
            if self.shared_memory:
                # Read relevant insights from other agents (exclude own)
                shared_insights = self.shared_memory.read_relevant(
                    agent_id=self.agent_id,
                    exclude_own=True,  # Don't read own insights
                    limit=10,  # Top 10 most relevant
                )
                inputs["_shared_insights"] = shared_insights

            # Pre-execution hook
            processed_inputs = self._pre_execution_hook(inputs)

            # Execute via strategy (handle both sync and async)
            if hasattr(self.strategy, "execute"):
                # Check if strategy.execute is async
                if inspect.iscoroutinefunction(self.strategy.execute):
                    # Async strategy - run in event loop
                    try:
                        # Try to get running loop (Python 3.10+)
                        loop = asyncio.get_running_loop()
                    except RuntimeError:
                        # No running loop - create and run in new loop
                        loop = asyncio.new_event_loop()
                        asyncio.set_event_loop(loop)
                        try:
                            result = loop.run_until_complete(
                                self.strategy.execute(self, processed_inputs)
                            )
                        finally:
                            loop.close()
                            asyncio.set_event_loop(None)
                    else:
                        # We're already in an async context
                        result = loop.run_until_complete(
                            self.strategy.execute(self, processed_inputs)
                        )
                else:
                    # Sync strategy - call directly
                    result = self.strategy.execute(self, processed_inputs)
            else:
                # Fallback: simple execution without strategy
                result = self._simple_execute(processed_inputs)

            # Validate output
            self._validate_signature_output(result)

            # Post-execution hook
            final_result = self._post_execution_hook(result)

            # Week 2 Phase 1: Save turn to individual memory if enabled
            if self.memory and session_id:
                # Extract user input (first input field value, or 'prompt')
                user_input = inputs.get("prompt", "")
                if not user_input and processed_inputs:
                    # Try to get first input value
                    user_input = (
                        str(list(processed_inputs.values())[0])
                        if processed_inputs
                        else ""
                    )

                # Extract agent response (first output field value, or 'response')
                agent_response = final_result.get("response", "")
                if not agent_response and final_result:
                    # Try to get first output value
                    agent_response = (
                        str(list(final_result.values())[0]) if final_result else ""
                    )

                # Create turn
                turn = {
                    "user": user_input,
                    "agent": agent_response,
                    "timestamp": datetime.now().isoformat(),
                }

                # Save to memory
                self.memory.save_turn(session_id, turn)

            # Week 3 Phase 2: Write insight to shared memory if enabled
            if self.shared_memory and final_result.get("_write_insight"):
                # Agent can optionally write insights to shared pool
                insight = {
                    "agent_id": self.agent_id,
                    "content": final_result["_write_insight"],
                    "tags": final_result.get("_insight_tags", []),
                    "importance": final_result.get("_insight_importance", 0.5),
                    "segment": final_result.get("_insight_segment", "execution"),
                    "metadata": final_result.get("_insight_metadata", {}),
                }
                self.shared_memory.write_insight(insight)

            return final_result

        except Exception as error:
            # Clean up any pending coroutines before error handling
            import gc

            # Force garbage collection to clean up any pending coroutines
            # This prevents "coroutine was never awaited" warnings
            gc.collect()

            # Handle error via extension point
            return self._handle_error(error, {"inputs": inputs})

    def _simple_execute(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
        """
        Simple execution without strategy (fallback).

        Used when strategy doesn't implement execute() method.
        """
        # Placeholder for simple LLM call
        # In production, this would call LLM directly
        return {"result": "Simple execution placeholder"}

    # ===================================================================
    # Convenience Methods for Improved Developer UX
    # ===================================================================

    def write_to_memory(
        self,
        content: Any,
        tags: Optional[List[str]] = None,
        importance: float = 0.5,
        segment: str = "execution",
        metadata: Optional[Dict[str, Any]] = None,
    ) -> None:
        """
        Convenience method to write insights to shared memory.

        Simplifies the common pattern of writing to shared memory by:
        - Auto-adding agent_id
        - Auto-serializing content to JSON
        - Providing sensible defaults

        Args:
            content: Content to write (auto-serialized to JSON if dict/list)
            tags: Tags for categorization (default: [])
            importance: Importance score 0.0-1.0 (default: 0.5)
            segment: Memory segment (default: "execution")
            metadata: Optional metadata dict (default: {})

        Example:
            >>> # OLD WAY (verbose):
            >>> if self.shared_memory:
            >>>     self.shared_memory.write_insight({
            >>>         "agent_id": self.agent_id,
            >>>         "content": json.dumps(result),
            >>>         "tags": ["processing", "complete"],
            >>>         "importance": 0.9,
            >>>         "segment": "pipeline"
            >>>     })
            >>>
            >>> # NEW WAY (concise):
            >>> self.write_to_memory(
            >>>     content=result,
            >>>     tags=["processing", "complete"],
            >>>     importance=0.9,
            >>>     segment="pipeline"
            >>> )

        Notes:
            - Does nothing if shared_memory is not available
            - Automatically serializes dicts and lists to JSON
            - Agent ID automatically added
        """
        if not self.shared_memory:
            return

        import json

        # Auto-serialize content if needed
        if isinstance(content, (dict, list)):
            content_str = json.dumps(content)
        else:
            content_str = str(content)

        # Build insight
        insight = {
            "agent_id": self.agent_id,
            "content": content_str,
            "tags": tags or [],
            "importance": importance,
            "segment": segment,
            "metadata": metadata or {},
        }

        self.shared_memory.write_insight(insight)

    def extract_list(
        self, result: Dict[str, Any], field_name: str, default: Optional[List] = None
    ) -> List:
        """
        Extract a list field from result with type safety.

        Handles the common pattern of extracting list fields that might be
        JSON strings or actual lists from LLM responses.

        Args:
            result: Result dictionary from agent execution
            field_name: Name of the field to extract
            default: Default value if extraction fails (default: [])

        Returns:
            List: Extracted list or default

        Example:
            >>> result = agent.run(query="...")
            >>>
            >>> # OLD WAY (verbose):
            >>> items_raw = result.get("items", "[]")
            >>> if isinstance(items_raw, str):
            >>>     try:
            >>>         items = json.loads(items_raw) if items_raw else []
            >>>     except:
            >>>         items = []
            >>> else:
            >>>     items = items_raw if isinstance(items_raw, list) else []
            >>>
            >>> # NEW WAY (concise):
            >>> items = self.extract_list(result, "items", default=[])
        """
        import json

        if default is None:
            default = []

        field_value = result.get(field_name, default)

        # Already a list
        if isinstance(field_value, list):
            return field_value

        # Try to parse as JSON string
        if isinstance(field_value, str):
            try:
                parsed = json.loads(field_value) if field_value else default
                return parsed if isinstance(parsed, list) else default
            except Exception:
                return default

        # Fallback
        return default

    def extract_dict(
        self, result: Dict[str, Any], field_name: str, default: Optional[Dict] = None
    ) -> Dict:
        """
        Extract a dict field from result with type safety.

        Handles the common pattern of extracting dict fields that might be
        JSON strings or actual dicts from LLM responses.

        Args:
            result: Result dictionary from agent execution
            field_name: Name of the field to extract
            default: Default value if extraction fails (default: {})

        Returns:
            Dict: Extracted dict or default

        Example:
            >>> result = agent.run(query="...")
            >>> config = self.extract_dict(result, "config", default={})
        """
        import json

        if default is None:
            default = {}

        field_value = result.get(field_name, default)

        # Already a dict
        if isinstance(field_value, dict):
            return field_value

        # Try to parse as JSON string
        if isinstance(field_value, str):
            try:
                parsed = json.loads(field_value) if field_value else default
                return parsed if isinstance(parsed, dict) else default
            except Exception:
                return default

        # Fallback
        return default

    def extract_float(
        self, result: Dict[str, Any], field_name: str, default: float = 0.0
    ) -> float:
        """
        Extract a float field from result with type safety.

        Handles the common pattern of extracting numeric fields that might be
        strings or actual numbers from LLM responses.

        Args:
            result: Result dictionary from agent execution
            field_name: Name of the field to extract
            default: Default value if extraction fails (default: 0.0)

        Returns:
            float: Extracted float or default

        Example:
            >>> result = agent.run(query="...")
            >>> confidence = self.extract_float(result, "confidence", default=0.0)
        """
        field_value = result.get(field_name, default)

        # Already a number
        if isinstance(field_value, (int, float)):
            return float(field_value)

        # Try to parse as string
        if isinstance(field_value, str):
            try:
                return float(field_value)
            except Exception:
                return default

        # Fallback
        return default

    def extract_str(
        self, result: Dict[str, Any], field_name: str, default: str = ""
    ) -> str:
        """
        Extract a string field from result with type safety.

        Handles the common pattern of extracting string fields from LLM responses.

        Args:
            result: Result dictionary from agent execution
            field_name: Name of the field to extract
            default: Default value if extraction fails (default: "")

        Returns:
            str: Extracted string or default

        Example:
            >>> result = agent.run(query="...")
            >>> answer = self.extract_str(result, "answer", default="No answer")
        """
        field_value = result.get(field_name, default)
        return str(field_value) if field_value is not None else default

    def to_workflow(self) -> WorkflowBuilder:
        """
        Generate a Core SDK workflow from the agent's signature.

        This is the core method that converts signature-based programming
        into actual Core SDK workflows using WorkflowBuilder and LLMAgentNode.

        Workflow Structure:
        1. Creates LLMAgentNode with agent configuration
        2. Maps signature input fields to workflow inputs
        3. Maps signature output fields to workflow outputs
        4. Adds necessary connections

        Returns:
            WorkflowBuilder: Workflow representation ready for execution

        Example:
            >>> workflow = agent.to_workflow()
            >>> built = workflow.build()  # Returns Workflow object
            >>>
            >>> # Execute with runtime
            >>> from kailash.runtime.local import LocalRuntime
            >>> runtime = LocalRuntime()
            >>> results, run_id = runtime.execute(built)

        Core SDK Pattern:
            workflow.add_node('LLMAgentNode', 'agent', {
                'model': self.config.model,
                'provider': self.config.llm_provider,
                'temperature': self.config.temperature,
                'system_prompt': self._generate_system_prompt(),
            })

        Notes:
        - Workflow is cached after first generation
        - Workflow uses LLMAgentNode from src/kailash/nodes/ai/llm_agent.py
        - Workflow must be composable with other Core SDK nodes
        """
        # Task 1.16: Implement BaseAgent.to_workflow()
        # Return cached workflow if already generated
        if self._workflow is not None:
            return self._workflow

        # Create new workflow
        workflow = WorkflowBuilder()

        # Add LLMAgentNode with configuration
        node_config = {
            "system_prompt": self._generate_system_prompt(),
        }

        # Add LLM configuration if specified
        if self.config.model is not None:
            node_config["model"] = self.config.model
        if self.config.llm_provider is not None:
            node_config["provider"] = self.config.llm_provider
        if self.config.temperature is not None:
            node_config["temperature"] = self.config.temperature
        if self.config.max_tokens is not None:
            node_config["max_tokens"] = self.config.max_tokens
        if self.config.provider_config is not None:
            node_config["provider_config"] = self.config.provider_config

        # Add the LLM agent node using string-based node name
        workflow.add_node("LLMAgentNode", "agent", node_config)

        # Cache the workflow
        self._workflow = workflow

        return workflow

    def to_workflow_node(self) -> Node:
        """
        Convert this agent into a single node for composition.

        Enables agent reuse in larger workflows by wrapping the agent
        as a composable node.

        Returns:
            Node: Agent as a composable workflow node

        Example:
            >>> agent_node = agent.to_workflow_node()
            >>>
            >>> # Use in larger workflow
            >>> main_workflow = WorkflowBuilder()
            >>> main_workflow.add_node_instance(agent_node, 'qa')
            >>> main_workflow.add_node('DataTransformer', 'transform', {...})
            >>> main_workflow.add_connection('qa', 'answer', 'transform', 'input')
        """
        # Task 1.16: Implement BaseAgent.to_workflow_node()
        # The agent itself is already a Node (inherits from Node)
        # So we can return self as a composable node
        return self

    # ===================================================================
    # Extension Points (7 total)
    # Override these methods in subclasses for agent-specific behavior
    # ===================================================================

    def _default_signature(self) -> Signature:
        """
        Provide default signature when none is specified.

        Override this method for agent-specific signatures.

        Returns:
            Signature: Default signature (1 input, 1 output)

        Extension Example:
            >>> class SimpleQAAgent(BaseAgent):
            ...     def _default_signature(self) -> Signature:
            ...         return QASignature(
            ...             question: str = InputField(desc="Question"),
            ...             answer: str = OutputField(desc="Answer")
            ...         )
        """

        # Task 1.17: Implement extension point 1
        # Create a simple default signature with 1 input, 1 output
        # Using proper InputField and OutputField
        class DefaultSignature(Signature):
            """Default signature with generic input/output."""

            input: str = InputField(desc="Generic input")
            output: str = OutputField(desc="Generic output")

        return DefaultSignature()

    def _default_strategy(self) -> Any:  # ExecutionStrategy when implemented
        """
        Provide default execution strategy.

        Override this method for agent-specific strategies.
        Returns AsyncSingleShotStrategy for strategy_type="single_shot" (NEW DEFAULT),
        MultiCycleStrategy for strategy_type="multi_cycle".

        Returns:
            ExecutionStrategy: Default strategy based on config

        Extension Example:
            >>> class ReActAgent(BaseAgent):
            ...     def _default_strategy(self) -> ExecutionStrategy:
            ...         return MultiCycleStrategy(max_cycles=10)

        Note:
            BREAKING CHANGE (Phase 0A): Default is now AsyncSingleShotStrategy
            for improved performance (2-3x faster for concurrent requests).
        """
        # Task 0A.1: Use AsyncSingleShotStrategy as default
        # Import strategies if available, otherwise return simple strategy object
        try:
            from kaizen.strategies.async_single_shot import AsyncSingleShotStrategy
            from kaizen.strategies.multi_cycle import MultiCycleStrategy

            if self.config.strategy_type == "multi_cycle":
                return MultiCycleStrategy(max_cycles=self.config.max_cycles)
            else:
                # DEFAULT: AsyncSingleShotStrategy (for "single_shot" or None)
                return AsyncSingleShotStrategy()
        except ImportError:
            # Fallback: return simple strategy object
            class SimpleStrategy:
                async def execute(self, agent, inputs, **kwargs):
                    return {"result": "Simple strategy execution"}

            return SimpleStrategy()

    def _generate_system_prompt(self) -> str:
        """
        Generate system prompt from signature and tool registry.

        Override this method for custom prompt generation logic.

        Returns:
            str: System prompt for LLM, including tool documentation if available

        Extension Example:
            >>> class SimpleQAAgent(BaseAgent):
            ...     def _generate_system_prompt(self) -> str:
            ...         base_prompt = super()._generate_system_prompt()
            ...         return f"{base_prompt}\\n\\nAdditional context: Answer concisely."
        """
        # Task 1.17: Implement extension point 3
        # Generate prompt from signature fields
        input_names = []
        output_names = []

        if hasattr(self.signature, "input_fields") and self.signature.input_fields:
            input_names = [
                f.name if hasattr(f, "name") else str(f)
                for f in self.signature.input_fields
            ]

        if hasattr(self.signature, "output_fields") and self.signature.output_fields:
            output_names = [
                f.name if hasattr(f, "name") else str(f)
                for f in self.signature.output_fields
            ]

        # Build base prompt from signature
        if input_names and output_names:
            inputs_str = ", ".join(input_names)
            outputs_str = ", ".join(output_names)
            base_prompt = f"Task: Given {inputs_str}, produce {outputs_str}."
        else:
            base_prompt = "You are a helpful AI assistant."

        # TODO-162 Phase 2: Add tool documentation to prompt
        # If tool_registry exists and has tools, include tool documentation
        if hasattr(self, "_tool_registry") and self._tool_registry is not None:
            try:
                tool_count = self._tool_registry.count()
                if tool_count > 0:
                    # Get formatted tool documentation
                    tools_text = self._tool_registry.format_for_prompt(
                        include_examples=True, include_parameters=True
                    )

                    # Build enhanced prompt with tools
                    enhanced_prompt = f"""{base_prompt}

{tools_text}

# Tool Calling Instructions

To use a tool, respond with JSON in the 'tool_calls' field:
{{"tool_calls": [{{"name": "tool_name", "params": {{"param": "value"}}}}]}}

You can call multiple tools in one response:
{{"tool_calls": [
  {{"name": "read_file", "params": {{"path": "data.txt"}}}},
  {{"name": "write_file", "params": {{"path": "output.txt", "content": "..."}}}}
]}}

When the task is complete and no more tools are needed, respond with:
{{"tool_calls": []}}

This signals convergence and the task will be marked as complete."""

                    return enhanced_prompt

            except Exception as e:
                # If tool formatting fails, fall back to base prompt
                # Log error if logging is enabled
                if hasattr(self, "_log") and self._log:
                    self._log(
                        f"Warning: Failed to format tools for prompt: {e}",
                        level="warning",
                    )

        # Return base prompt if no tools or error
        return base_prompt

    def _validate_signature_output(self, output: Dict[str, Any]) -> bool:
        """
        Validate that output matches signature.

        Override this method for custom validation logic.

        Args:
            output: Execution result to validate

        Returns:
            bool: True if valid

        Raises:
            ValueError: If validation fails

        Extension Example:
            >>> class SimpleQAAgent(BaseAgent):
            ...     def _validate_signature_output(self, output: Dict[str, Any]) -> bool:
            ...         super()._validate_signature_output(output)
            ...         if not 0 <= output.get('confidence', 0) <= 1:
            ...             raise ValueError("Confidence must be between 0 and 1")
            ...         return True
        """
        # Task 1.17: Implement extension point 4
        # Check that all required output fields are present
        # UNLESS this is a test/special result (has _write_insight or response)

        # Skip validation for results with special keys (test results, insight writes)
        has_special_keys = any(
            key in output for key in ["_write_insight", "response", "result"]
        )

        if has_special_keys:
            # Lenient validation for test/special results
            return True

        # Strict validation for normal signature-based results
        if hasattr(self.signature, "output_fields") and self.signature.output_fields:
            for field in self.signature.output_fields:
                field_name = field.name if hasattr(field, "name") else str(field)
                if field_name not in output:
                    raise ValueError(f"Missing required output field: {field_name}")
        return True

    def _pre_execution_hook(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
        """
        Hook called before execution.

        Override this method to add preprocessing, logging, etc.

        Args:
            inputs: Execution inputs

        Returns:
            Dict[str, Any]: Modified inputs (or original)

        Extension Example:
            >>> class ReActAgent(BaseAgent):
            ...     def _pre_execution_hook(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
            ...         inputs = super()._pre_execution_hook(inputs)
            ...         inputs['available_tools'] = self._load_mcp_tools()
            ...         return inputs
        """
        # Task 1.17: Implement extension point 5
        # Log execution if logging enabled
        logging_enabled = getattr(self.config, "logging_enabled", True)
        if logging_enabled:
            signature_name = getattr(self.signature, "name", "unknown")
            logger.info(f"Executing {signature_name} with inputs: {inputs}")
        return inputs

    def _post_execution_hook(self, result: Dict[str, Any]) -> Dict[str, Any]:
        """
        Hook called after execution.

        Override this method to add postprocessing, logging, etc.

        Args:
            result: Execution result

        Returns:
            Dict[str, Any]: Modified result (or original)

        Extension Example:
            >>> class ReActAgent(BaseAgent):
            ...     def _post_execution_hook(self, result: Dict[str, Any]) -> Dict[str, Any]:
            ...         result = super()._post_execution_hook(result)
            ...         result['metadata']['tools_used'] = len(self.tools_called)
            ...         return result
        """
        # Task 1.17: Implement extension point 6
        # Log completion if logging enabled
        logging_enabled = getattr(self.config, "logging_enabled", True)
        if logging_enabled:
            logger.info(f"Execution complete. Result: {result}")
        return result

    def _handle_error(
        self, error: Exception, context: Dict[str, Any]
    ) -> Dict[str, Any]:
        """
        Handle errors during execution.

        Override this method for custom error handling logic.

        Args:
            error: Exception that occurred
            context: Execution context when error occurred

        Returns:
            Dict[str, Any]: Error result (when error_handling_enabled=True)

        Raises:
            Exception: Re-raises error if error_handling_enabled=False

        Extension Example:
            >>> class RobustAgent(BaseAgent):
            ...     def _handle_error(self, error: Exception, context: Dict[str, Any]) -> Dict[str, Any]:
            ...         # Log detailed error
            ...         logger.error(f"Agent failed: {error}", extra=context)
            ...         # Return fallback response
            ...         return {"error": str(error), "fallback": "I encountered an error."}
        """
        # Task 1.17: Implement extension point 7
        error_handling_enabled = getattr(self.config, "error_handling_enabled", True)
        if error_handling_enabled:
            # Log error
            logger.error(f"Error during execution: {error}", extra=context)
            # Return error dict with success flag
            return {"error": str(error), "type": type(error).__name__, "success": False}
        else:
            # Re-raise error
            raise error

    # =============================================================================
    # GOOGLE A2A (AGENT-TO-AGENT) INTEGRATION
    # =============================================================================
    # These methods provide Google A2A protocol support using Kailash SDK's
    # production-ready A2A implementation. Full agent card generation, capability
    # matching, and task lifecycle management.
    #
    # Architecture: Kaizen EXTENDS Kailash SDK A2A (complete Google A2A spec)
    # Implementation: kailash.nodes.ai.a2a (100% Google A2A compliant)
    # =============================================================================

    def to_a2a_card(self) -> "A2AAgentCard":
        """
        Generate Google A2A compliant agent card.

        Creates a comprehensive agent capability card that enables intelligent
        agent discovery, task matching, and team formation in multi-agent systems.

        Returns:
            A2AAgentCard: Complete agent card with capabilities, performance, resources

        Example:
            >>> from kaizen.core.base_agent import BaseAgent
            >>> from kaizen.signatures import Signature, InputField, OutputField
            >>>
            >>> class QASignature(Signature):
            ...     question: str = InputField(desc="User question")
            ...     answer: str = OutputField(desc="Answer")
            >>>
            >>> agent = BaseAgent(config=config, signature=QASignature())
            >>> card = agent.to_a2a_card()
            >>> print(f"Agent: {card.agent_name}")
            >>> print(f"Capabilities: {len(card.primary_capabilities)}")
            >>> print(f"Collaboration: {card.collaboration_style.value}")

        Agent Card Contents:
            - Identity: agent_id, agent_name, agent_type, version
            - Capabilities: Primary, secondary, emerging capabilities
            - Collaboration: Style, team preferences, compatible agents
            - Performance: Success rate, quality scores, response times
            - Resources: Memory, token limits, API requirements

        Google A2A Compliance:
            ✓ Semantic capability matching with keywords
            ✓ Performance metrics tracking
            ✓ Collaboration style preferences
            ✓ Resource requirement specification
            ✓ Full capability proficiency levels
        """
        try:
            from kailash.nodes.ai.a2a import A2AAgentCard
        except ImportError:
            raise ImportError(
                "kailash.nodes.ai.a2a not available. Install with: pip install kailash"
            )

        return A2AAgentCard(
            agent_id=self.agent_id,
            agent_name=self.__class__.__name__,
            agent_type=self._get_agent_type(),
            version=getattr(self, "version", "1.0.0"),
            primary_capabilities=self._extract_primary_capabilities(),
            secondary_capabilities=self._extract_secondary_capabilities(),
            collaboration_style=self._get_collaboration_style(),
            performance=self._get_performance_metrics(),
            resources=self._get_resource_requirements(),
            description=self._get_agent_description(),
            tags=self._get_agent_tags(),
            specializations=self._get_specializations(),
        )

    def _extract_primary_capabilities(self) -> List["Capability"]:
        """Extract primary capabilities from signature."""
        try:
            from kailash.nodes.ai.a2a import Capability, CapabilityLevel
        except ImportError:
            return []

        capabilities = []
        if hasattr(self, "signature") and self.signature:
            # Infer capabilities from signature input/output fields
            if hasattr(self.signature, "input_fields") and self.signature.input_fields:
                for field in self.signature.input_fields:
                    field_name = field.name if hasattr(field, "name") else "input"
                    field_desc = field.desc if hasattr(field, "desc") else ""

                    capabilities.append(
                        Capability(
                            name=field_name,
                            domain=self._infer_domain(),
                            level=CapabilityLevel.EXPERT,
                            description=field_desc or f"Processes {field_name} inputs",
                            keywords=self._extract_keywords(field_desc),
                            examples=[],
                            constraints=[],
                        )
                    )

        return capabilities

    def _extract_secondary_capabilities(self) -> List["Capability"]:
        """Extract secondary capabilities from strategy and memory."""
        try:
            from kailash.nodes.ai.a2a import Capability, CapabilityLevel
        except ImportError:
            return []

        capabilities = []

        # Memory capability
        if hasattr(self, "memory") and self.memory:
            capabilities.append(
                Capability(
                    name="conversation_memory",
                    domain=self._infer_domain(),
                    level=CapabilityLevel.ADVANCED,
                    description="Maintains conversation context across sessions",
                    keywords=["memory", "context", "history"],
                    examples=[],
                    constraints=[],
                )
            )

        # Shared memory capability
        if hasattr(self, "shared_memory") and self.shared_memory:
            capabilities.append(
                Capability(
                    name="multi_agent_collaboration",
                    domain="collaboration",
                    level=CapabilityLevel.ADVANCED,
                    description="Shares insights with other agents via shared memory",
                    keywords=["collaboration", "sharing", "insights"],
                    examples=[],
                    constraints=[],
                )
            )

        return capabilities

    def _get_collaboration_style(self) -> "CollaborationStyle":
        """Determine collaboration style from agent configuration."""
        try:
            from kailash.nodes.ai.a2a import CollaborationStyle
        except ImportError:
            return None

        # Check if agent has shared memory (indicates cooperative style)
        if hasattr(self, "shared_memory") and self.shared_memory:
            return CollaborationStyle.COOPERATIVE

        # Default to independent
        return CollaborationStyle.INDEPENDENT

    def _get_performance_metrics(self) -> "PerformanceMetrics":
        """Get performance metrics for agent card."""
        try:
            from datetime import datetime

            from kailash.nodes.ai.a2a import PerformanceMetrics
        except ImportError:
            return None

        # Create metrics with defaults (can be enhanced with actual tracking)
        return PerformanceMetrics(
            total_tasks=0,
            successful_tasks=0,
            failed_tasks=0,
            average_response_time_ms=0.0,
            average_insight_quality=0.8,
            average_confidence_score=0.85,
            insights_generated=0,
            unique_insights=0,
            actionable_insights=0,
            collaboration_score=0.7,
            reliability_score=0.9,
            last_active=datetime.now(),
        )

    def _get_resource_requirements(self) -> "ResourceRequirements":
        """Get resource requirements from config."""
        try:
            from kailash.nodes.ai.a2a import ResourceRequirements
        except ImportError:
            return None

        # Extract from config if available
        max_tokens = getattr(self.config, "max_tokens", 4000)
        model = getattr(self.config, "model", "")
        provider = getattr(self.config, "llm_provider", "")

        # Determine GPU requirement based on model
        requires_gpu = "llama" in model.lower() or "mistral" in model.lower()

        # Determine internet requirement based on provider
        requires_internet = provider in ["openai", "anthropic", "google"]

        return ResourceRequirements(
            min_memory_mb=512,
            max_memory_mb=4096,
            min_tokens=100,
            max_tokens=max_tokens,
            requires_gpu=requires_gpu,
            requires_internet=requires_internet,
            estimated_cost_per_task=0.01,  # Can be enhanced with actual cost tracking
            max_concurrent_tasks=5,
            supported_models=[model] if model else [],
            required_apis=[provider] if provider else [],
        )

    def _infer_domain(self) -> str:
        """Infer domain from agent class name and signature."""
        class_name = self.__class__.__name__.lower()

        # Domain inference from class name
        if "qa" in class_name or "question" in class_name:
            return "question_answering"
        elif "rag" in class_name or "research" in class_name:
            return "research"
        elif "code" in class_name or "programming" in class_name:
            return "code_generation"
        elif "analysis" in class_name or "analyst" in class_name:
            return "analysis"
        elif "summary" in class_name or "summarize" in class_name:
            return "summarization"
        elif "translation" in class_name or "translate" in class_name:
            return "translation"
        elif "classification" in class_name or "classify" in class_name:
            return "classification"

        # Default domain
        return "general"

    def _extract_keywords(self, text: str) -> List[str]:
        """Extract keywords from text description."""
        if not text:
            return []

        # Simple keyword extraction - split and filter common words
        stop_words = {
            "a",
            "an",
            "the",
            "is",
            "are",
            "was",
            "were",
            "to",
            "for",
            "of",
            "in",
            "on",
            "at",
        }
        words = text.lower().split()
        keywords = [w.strip(".,;:!?") for w in words if w not in stop_words]

        return keywords[:10]  # Limit to top 10 keywords

    def _get_agent_type(self) -> str:
        """Get agent type identifier."""
        return self.__class__.__name__

    def _get_agent_description(self) -> str:
        """Get agent description from docstring or signature."""
        # Try to get from class docstring
        if self.__class__.__doc__:
            return self.__class__.__doc__.strip().split("\n")[0]

        # Fallback to signature-based description
        if hasattr(self, "signature") and self.signature:
            return (
                f"Agent with {len(getattr(self.signature, 'input_fields', []))} inputs"
            )

        return f"{self.__class__.__name__} agent"

    def _get_agent_tags(self) -> List[str]:
        """Get agent tags from domain and capabilities."""
        tags = [self._infer_domain()]

        # Add memory tags
        if hasattr(self, "memory") and self.memory:
            tags.append("memory")
        if hasattr(self, "shared_memory") and self.shared_memory:
            tags.append("collaborative")

        # Add strategy tags
        if hasattr(self, "strategy"):
            strategy_name = self.strategy.__class__.__name__.lower()
            if "async" in strategy_name:
                tags.append("async")
            if "multi_cycle" in strategy_name:
                tags.append("iterative")

        return tags

    def _get_specializations(self) -> Dict[str, Any]:
        """Get agent specializations and metadata."""
        return {
            "framework": "kaizen",
            "has_memory": hasattr(self, "memory") and self.memory is not None,
            "has_shared_memory": hasattr(self, "shared_memory")
            and self.shared_memory is not None,
            "strategy": (
                self.strategy.__class__.__name__
                if hasattr(self, "strategy")
                else "none"
            ),
            "model": getattr(self.config, "model", "unknown"),
            "provider": getattr(self.config, "llm_provider", "unknown"),
        }

    # =============================================================================
    # CONTROL PROTOCOL HELPERS (Week 10)
    # =============================================================================
    # These methods provide convenient user interaction capabilities using the
    # Control Protocol for bidirectional agent↔user communication.
    #
    # See: docs/architecture/adr/011-control-protocol-architecture.md
    # =============================================================================

    async def ask_user_question(
        self, question: str, options: Optional[List[str]] = None, timeout: float = 60.0
    ) -> str:
        """
        Ask user a question during agent execution.

        Uses the Control Protocol to send a question to the user and wait for
        their response. This enables interactive agent workflows where the agent
        can request input mid-execution.

        Args:
            question: Question to ask the user
            options: Optional list of answer choices (for multiple choice)
            timeout: Maximum time to wait for response (seconds)

        Returns:
            User's answer as a string

        Raises:
            RuntimeError: If control_protocol is not configured
            TimeoutError: If user doesn't respond within timeout

        Example:
            >>> agent = BaseAgent(
            ...     config=config,
            ...     signature=signature,
            ...     control_protocol=protocol
            ... )
            >>> answer = await agent.ask_user_question(
            ...     "Which file should I process?",
            ...     options=["file1.txt", "file2.txt", "all"]
            ... )
            >>> print(f"User selected: {answer}")
        """
        if self.control_protocol is None:
            raise RuntimeError(
                "Control protocol not configured. "
                "Pass control_protocol parameter to BaseAgent.__init__()"
            )

        # Create request
        from kaizen.core.autonomy.control.types import ControlRequest

        data = {"question": question}
        if options:
            data["options"] = options

        request = ControlRequest.create("question", data)

        # Send and wait for response
        response = await self.control_protocol.send_request(request, timeout=timeout)

        if response.is_error:
            raise RuntimeError(f"Question error: {response.error}")

        return response.data.get("answer", "")

    async def request_approval(
        self,
        action: str,
        details: Optional[Dict[str, Any]] = None,
        timeout: float = 60.0,
    ) -> bool:
        """
        Request user approval for an action during agent execution.

        Uses the Control Protocol to ask the user to approve or deny a proposed
        action. This enables safe interactive workflows where critical operations
        require human confirmation.

        Args:
            action: Description of the action needing approval
            details: Optional additional context/details about the action
            timeout: Maximum time to wait for response (seconds)

        Returns:
            True if approved, False if denied

        Raises:
            RuntimeError: If control_protocol is not configured
            TimeoutError: If user doesn't respond within timeout

        Example:
            >>> agent = BaseAgent(
            ...     config=config,
            ...     signature=signature,
            ...     control_protocol=protocol
            ... )
            >>> approved = await agent.request_approval(
            ...     "Delete 100 files",
            ...     details={"files": file_list, "size_mb": 250}
            ... )
            >>> if approved:
            ...     # Proceed with deletion
            ...     pass
            >>> else:
            ...     # Cancel operation
            ...     pass
        """
        if self.control_protocol is None:
            raise RuntimeError(
                "Control protocol not configured. "
                "Pass control_protocol parameter to BaseAgent.__init__()"
            )

        # Create request
        from kaizen.core.autonomy.control.types import ControlRequest

        data = {"action": action}
        if details:
            data["details"] = details

        request = ControlRequest.create("approval", data)

        # Send and wait for response
        response = await self.control_protocol.send_request(request, timeout=timeout)

        if response.is_error:
            raise RuntimeError(f"Approval error: {response.error}")

        return response.data.get("approved", False)

    async def report_progress(
        self,
        message: str,
        percentage: Optional[float] = None,
        details: Optional[Dict[str, Any]] = None,
    ) -> None:
        """
        Report progress update to user during agent execution.

        This is a fire-and-forget method - it sends progress updates but doesn't
        wait for acknowledgment. Use this to keep users informed during long-running
        operations.

        Args:
            message: Progress message to display (e.g., "Processing file 5 of 10")
            percentage: Optional progress percentage (0.0-100.0)
            details: Optional additional progress details

        Raises:
            RuntimeError: If control_protocol is not configured

        Example:
            # During a long operation
            for i, file in enumerate(files):
                await agent.report_progress(
                    f"Processing {file}",
                    percentage=(i / len(files)) * 100,
                    details={"current": i + 1, "total": len(files)}
                )
                # ... process file ...
        """
        if self.control_protocol is None:
            raise RuntimeError(
                "Control protocol not configured. "
                "Pass control_protocol parameter to BaseAgent.__init__() "
                "to enable report_progress()."
            )

        from kaizen.core.autonomy.control.types import ControlRequest

        data = {"message": message}
        if percentage is not None:
            if not (0.0 <= percentage <= 100.0):
                raise ValueError(
                    f"Percentage must be between 0.0 and 100.0, got {percentage}"
                )
            data["percentage"] = percentage
        if details:
            data["details"] = details

        request = ControlRequest.create("progress_update", data)

        # Fire-and-forget: write the message but don't wait for response
        # Progress updates don't require user acknowledgment
        await self.control_protocol._transport.write(request.to_json())

    # =============================================================================
    # TOOL CALLING INTEGRATION
    # =============================================================================

    def has_tool_support(self) -> bool:
        """
        Check if agent has tool calling capabilities.

        Returns:
            True if tool registry is configured, False otherwise

        Example:
            >>> agent = BaseAgent(config=config, signature=signature)
            >>> print(agent.has_tool_support())  # False
            >>>
            >>> agent_with_tools = BaseAgent(
            ...     config=config,
            ...     signature=signature,
            ...     tool_registry=registry
            ... )
            >>> print(agent_with_tools.has_tool_support())  # True
        """
        return self._tool_registry is not None

    async def discover_tools(
        self,
        category: Optional[ToolCategory] = None,
        safe_only: bool = False,
        keyword: Optional[str] = None,
        include_mcp: bool = True,
    ) -> List[ToolDefinition]:
        """
        Discover available tools with optional filtering.

        Allows agents to explore available tools by category, danger level,
        or keyword search. Useful for dynamic tool selection and capability
        awareness. Optionally includes MCP tools if configured.

        Args:
            category: Optional filter by tool category
            safe_only: If True, only return SAFE tools (default: False)
            keyword: Optional keyword to search in tool names/descriptions
            include_mcp: If True, include MCP tools in results (default: True)

        Returns:
            List of matching ToolDefinition objects

        Raises:
            RuntimeError: If tool registry not configured (and no MCP servers)

        Example:
            >>> # Discover all tools (builtin + MCP)
            >>> all_tools = await agent.discover_tools()
            >>>
            >>> # Find only safe tools
            >>> safe_tools = await agent.discover_tools(safe_only=True)
            >>>
            >>> # Find system tools
            >>> system_tools = await agent.discover_tools(category=ToolCategory.SYSTEM)
            >>>
            >>> # Search by keyword
            >>> file_tools = await agent.discover_tools(keyword="file")
            >>>
            >>> # Builtin tools only (exclude MCP)
            >>> builtin_tools = await agent.discover_tools(include_mcp=False)
        """
        tools = []

        # Discover builtin tools if registry configured
        if self._tool_registry is not None:
            # Start with all tools
            tools = self._tool_registry.list_all()

            # Filter by category if provided
            if category is not None:
                tools = [t for t in tools if t.category == category]

            # Filter by danger level if safe_only
            if safe_only:
                tools = [t for t in tools if t.danger_level == DangerLevel.SAFE]

            # Filter by keyword if provided
            if keyword is not None:
                keyword_lower = keyword.lower()
                tools = [
                    t
                    for t in tools
                    if keyword_lower in t.name.lower()
                    or keyword_lower in t.description.lower()
                ]

        # Discover MCP tools if requested and configured
        if include_mcp and self._mcp_servers is not None:
            mcp_tools_raw = await self.discover_mcp_tools()

            # Convert MCP tools to ToolDefinition format
            for mcp_tool in mcp_tools_raw:
                # Extract parameters from MCP tool schema
                params = []
                if "parameters" in mcp_tool and isinstance(
                    mcp_tool["parameters"], dict
                ):
                    for param_name, param_schema in mcp_tool["parameters"].items():
                        param_type = param_schema.get("type", "string")
                        param_desc = param_schema.get("description", "")
                        param_required = param_schema.get("required", False)

                        params.append(
                            ToolParameter(
                                name=param_name,
                                type=param_type,
                                description=param_desc,
                                required=param_required,
                            )
                        )

                # Create ToolDefinition for MCP tool
                tool_def = ToolDefinition(
                    name=mcp_tool["name"],
                    description=mcp_tool.get("description", ""),
                    category=ToolCategory.SYSTEM,  # Default to SYSTEM for MCP tools
                    danger_level=DangerLevel.SAFE,  # Default to SAFE (can be configured)
                    parameters=params,
                    returns={},  # MCP tools don't have typed returns
                    executor=None,  # MCP tools use execute_mcp_tool
                )

                # Apply filters
                if category is not None and tool_def.category != category:
                    continue
                if safe_only and tool_def.danger_level != DangerLevel.SAFE:
                    continue
                if keyword is not None:
                    keyword_lower = keyword.lower()
                    if not (
                        keyword_lower in tool_def.name.lower()
                        or keyword_lower in tool_def.description.lower()
                    ):
                        continue

                tools.append(tool_def)

        # Raise error if no tool sources configured
        if self._tool_registry is None and self._mcp_servers is None:
            raise RuntimeError(
                "No tool sources configured. "
                "Pass tool_registry or mcp_servers parameter to BaseAgent.__init__() "
                "to enable tool discovery."
            )

        return tools

    async def execute_tool(
        self,
        tool_name: str,
        params: Dict[str, Any],
        timeout: Optional[float] = None,
        store_in_memory: bool = False,
    ) -> ToolResult:
        """
        Execute a tool with approval workflow.

        Uses ToolExecutor to execute registered tools with automatic
        approval requests based on danger level. Safe tools execute
        immediately, while dangerous tools request user approval.

        Args:
            tool_name: Name of tool to execute
            params: Parameters to pass to tool
            timeout: Optional approval timeout (uses default if None)
            store_in_memory: If True, store result in agent memory (default: False)

        Returns:
            ToolResult with execution status and result/error

        Raises:
            RuntimeError: If tool executor not configured
            ValueError: If tool not found or invalid parameters

        Example:
            >>> # Execute safe tool
            >>> result = await agent.execute_tool(
            ...     "read_file",
            ...     {"path": "data.txt"}
            ... )
            >>> if result.success:
            ...     print(result.result["content"])
            >>>
            >>> # Execute dangerous tool (requires approval)
            >>> result = await agent.execute_tool(
            ...     "bash_command",
            ...     {"command": "ls -la"}
            ... )
            >>> if result.approved:
            ...     print(result.result["stdout"])
        """
        if self._tool_executor is None:
            raise RuntimeError(
                "Tool executor not configured. "
                "Pass tool_registry parameter to BaseAgent.__init__() "
                "to enable tool execution."
            )

        # Execute tool through executor (handles approval workflow)
        result = await self._tool_executor.execute(
            tool_name=tool_name, params=params, timeout=timeout
        )

        # Store in memory if requested and memory is available
        if store_in_memory and self.memory is not None and result.success:
            self.memory.add_message(
                role="tool",
                content=f"Executed tool '{tool_name}' with result: {result.result}",
            )

        return result

    async def execute_tool_chain(
        self,
        executions: List[Dict[str, Any]],
        stop_on_error: bool = True,
        timeout: Optional[float] = None,
    ) -> List[ToolResult]:
        """
        Execute multiple tools in sequence.

        Executes a chain of tools, optionally stopping on first error.
        Useful for multi-step operations that require sequential tool calls.

        Args:
            executions: List of dicts with "tool_name" and "params" keys
            stop_on_error: Stop execution if a tool fails (default: True)
            timeout: Optional approval timeout for each tool

        Returns:
            List of ToolResult objects (one per execution, may be partial if stopped)

        Raises:
            RuntimeError: If tool executor not configured

        Example:
            >>> executions = [
            ...     {"tool_name": "read_file", "params": {"path": "input.txt"}},
            ...     {"tool_name": "transform_data", "params": {"data": "..."}},
            ...     {"tool_name": "write_file", "params": {"path": "output.txt", "content": "..."}}
            ... ]
            >>> results = await agent.execute_tool_chain(executions)
            >>> if all(r.success for r in results):
            ...     print("All tools executed successfully")
        """
        if self._tool_executor is None:
            raise RuntimeError(
                "Tool executor not configured. "
                "Pass tool_registry parameter to BaseAgent.__init__() "
                "to enable tool chain execution."
            )

        results = []
        for execution in executions:
            tool_name = execution.get("tool_name")
            params = execution.get("params", {})

            if not tool_name:
                # Create error result for missing tool_name
                error_result = ToolResult(
                    tool_name="unknown",
                    success=False,
                    error="Missing 'tool_name' in execution specification",
                    execution_time=0.0,
                )
                results.append(error_result)
                if stop_on_error:
                    break
                continue

            try:
                result = await self.execute_tool(
                    tool_name=tool_name, params=params, timeout=timeout
                )
                results.append(result)

                # Stop if error and stop_on_error is True
                if not result.success and stop_on_error:
                    break

            except Exception as e:
                # Create error result for exception
                error_result = ToolResult(
                    tool_name=tool_name,
                    success=False,
                    error=str(e),
                    execution_time=0.0,
                )
                results.append(error_result)
                if stop_on_error:
                    break

        return results

    # =============================================================================
    # MCP INTEGRATION - Tool Discovery and Execution
    # =============================================================================
    # These methods integrate MCP (Model Context Protocol) tools with BaseAgent,
    # enabling agents to discover and execute tools from external MCP servers.
    #
    # Architecture: Uses Kailash SDK MCPClient for real protocol support
    # Naming Convention: mcp__<serverName>__<toolName>
    # =============================================================================

    def has_mcp_support(self) -> bool:
        """
        Check if agent has MCP integration configured.

        Returns:
            True if mcp_servers is configured, False otherwise

        Example:
            >>> agent = BaseAgent(config=config, signature=signature)
            >>> print(agent.has_mcp_support())  # False
            >>>
            >>> agent_with_mcp = BaseAgent(
            ...     config=config,
            ...     signature=signature,
            ...     mcp_servers=[{"name": "fs", "transport": "stdio", ...}]
            ... )
            >>> print(agent_with_mcp.has_mcp_support())  # True
        """
        return self._mcp_servers is not None

    async def discover_mcp_tools(
        self, server_name: Optional[str] = None, force_refresh: bool = False
    ) -> List[Dict[str, Any]]:
        """
        Discover MCP tools from configured servers with naming convention.

        Discovers tools from MCP servers and applies naming convention:
        mcp__<serverName>__<toolName>

        Args:
            server_name: Optional filter by server name (None = all servers)
            force_refresh: Bypass cache and rediscover tools (default: False)

        Returns:
            List of tool definitions with naming convention applied

        Raises:
            RuntimeError: If MCP not configured

        Example:
            >>> agent = BaseAgent(
            ...     config=config,
            ...     signature=signature,
            ...     mcp_servers=[
            ...         {"name": "filesystem", "transport": "stdio", ...}
            ...     ]
            ... )
            >>>
            >>> # Discover all tools
            >>> tools = await agent.discover_mcp_tools()
            >>> print(tools[0]["name"])  # "mcp__filesystem__read_file"
            >>>
            >>> # Discover from specific server
            >>> tools = await agent.discover_mcp_tools(server_name="filesystem")
        """
        if self._mcp_servers is None:
            raise RuntimeError(
                "MCP not configured. Pass mcp_servers parameter to BaseAgent.__init__()"
            )

        # Filter servers if server_name provided
        servers = self._mcp_servers
        if server_name is not None:
            servers = [s for s in servers if s.get("name") == server_name]

        # Collect tools from all selected servers
        all_tools = []
        for server_config in servers:
            server_key = server_config.get("name", "unknown")

            # Check cache if not forcing refresh
            if not force_refresh and server_key in self._discovered_mcp_tools:
                all_tools.extend(self._discovered_mcp_tools[server_key])
                continue

            # Discover tools from server
            tools = await self._mcp_client.discover_tools(
                server_config, force_refresh=force_refresh
            )

            # Apply naming convention: mcp__<serverName>__<toolName>
            renamed_tools = []
            for tool in tools:
                renamed_tool = tool.copy()
                renamed_tool["name"] = f"mcp__{server_key}__{tool['name']}"
                renamed_tools.append(renamed_tool)

            # Cache the renamed tools
            self._discovered_mcp_tools[server_key] = renamed_tools
            all_tools.extend(renamed_tools)

        return all_tools

    async def execute_mcp_tool(
        self, tool_name: str, params: Dict[str, Any], timeout: Optional[float] = None
    ) -> Dict[str, Any]:
        """
        Execute MCP tool with server routing.

        Routes tool execution to the correct MCP server based on naming convention:
        mcp__<serverName>__<toolName>

        Args:
            tool_name: Tool name with naming convention (mcp__server__tool)
            params: Tool parameters
            timeout: Optional execution timeout

        Returns:
            Tool execution result

        Raises:
            ValueError: If tool_name format invalid or server not found

        Example:
            >>> agent = BaseAgent(
            ...     config=config,
            ...     signature=signature,
            ...     mcp_servers=[{"name": "filesystem", ...}]
            ... )
            >>>
            >>> result = await agent.execute_mcp_tool(
            ...     "mcp__filesystem__read_file",
            ...     {"path": "/data/test.txt"}
            ... )
            >>> print(result["content"])
        """
        # Validate naming convention
        if not tool_name.startswith("mcp__") or tool_name.count("__") < 2:
            raise ValueError(
                f"Invalid MCP tool name format: {tool_name}. "
                "Expected: mcp__<serverName>__<toolName>"
            )

        # Parse tool name
        parts = tool_name.split("__")
        server_name = parts[1]
        original_tool_name = "__".join(parts[2:])  # Handle tool names with __

        # Find server config
        server_config = None
        for config in self._mcp_servers:
            if config.get("name") == server_name:
                server_config = config
                break

        if server_config is None:
            raise ValueError(
                f"MCP server '{server_name}' not found in configured servers"
            )

        # Execute tool via MCPClient
        result = await self._mcp_client.call_tool(
            server_config, original_tool_name, params, timeout=timeout
        )

        return result

    async def discover_mcp_resources(
        self, server_name: str, force_refresh: bool = False
    ) -> List[Dict[str, Any]]:
        """
        Discover MCP resources from a specific server.

        Args:
            server_name: Server name to query
            force_refresh: Bypass cache and rediscover (default: False)

        Returns:
            List of resource definitions

        Raises:
            RuntimeError: If MCP not configured

        Example:
            >>> resources = await agent.discover_mcp_resources("filesystem")
            >>> print(resources[0]["uri"])  # "file:///data/file.txt"
        """
        if self._mcp_servers is None:
            raise RuntimeError("MCP not configured")

        # Find server config
        server_config = None
        for config in self._mcp_servers:
            if config.get("name") == server_name:
                server_config = config
                break

        if server_config is None:
            raise ValueError(f"MCP server '{server_name}' not found")

        # Check cache
        if not force_refresh and server_name in self._discovered_mcp_resources:
            return self._discovered_mcp_resources[server_name]

        # Discover resources (requires session - not implemented yet)
        # For now, return empty list
        # TODO: Implement session-based resource discovery
        return []

    async def read_mcp_resource(self, server_name: str, uri: str) -> Any:
        """
        Read MCP resource content from a specific server.

        Args:
            server_name: Server name
            uri: Resource URI

        Returns:
            Resource content

        Raises:
            RuntimeError: If MCP not configured

        Example:
            >>> content = await agent.read_mcp_resource(
            ...     "filesystem",
            ...     "file:///data/test.txt"
            ... )
        """
        if self._mcp_servers is None:
            raise RuntimeError("MCP not configured")

        # Find server config
        server_config = None
        for config in self._mcp_servers:
            if config.get("name") == server_name:
                server_config = config
                break

        if server_config is None:
            raise ValueError(f"MCP server '{server_name}' not found")

        # Read resource (requires session - not implemented yet)
        # TODO: Implement session-based resource reading
        raise NotImplementedError("Session-based resource reading not yet implemented")

    async def discover_mcp_prompts(
        self, server_name: str, force_refresh: bool = False
    ) -> List[Dict[str, Any]]:
        """
        Discover MCP prompts from a specific server.

        Args:
            server_name: Server name to query
            force_refresh: Bypass cache and rediscover (default: False)

        Returns:
            List of prompt definitions

        Raises:
            RuntimeError: If MCP not configured

        Example:
            >>> prompts = await agent.discover_mcp_prompts("api-tools")
            >>> print(prompts[0]["name"])  # "greeting"
        """
        if self._mcp_servers is None:
            raise RuntimeError("MCP not configured")

        # Find server config
        server_config = None
        for config in self._mcp_servers:
            if config.get("name") == server_name:
                server_config = config
                break

        if server_config is None:
            raise ValueError(f"MCP server '{server_name}' not found")

        # Check cache
        if not force_refresh and server_name in self._discovered_mcp_prompts:
            return self._discovered_mcp_prompts[server_name]

        # Discover prompts (requires session - not implemented yet)
        # TODO: Implement session-based prompt discovery
        return []

    async def get_mcp_prompt(
        self, server_name: str, prompt_name: str, arguments: Dict[str, Any]
    ) -> Dict[str, Any]:
        """
        Get MCP prompt with arguments from a specific server.

        Args:
            server_name: Server name
            prompt_name: Prompt name
            arguments: Prompt arguments

        Returns:
            Prompt with messages

        Raises:
            RuntimeError: If MCP not configured

        Example:
            >>> prompt = await agent.get_mcp_prompt(
            ...     "api-tools",
            ...     "greeting",
            ...     {"name": "Alice"}
            ... )
        """
        if self._mcp_servers is None:
            raise RuntimeError("MCP not configured")

        # Find server config
        server_config = None
        for config in self._mcp_servers:
            if config.get("name") == server_name:
                server_config = config
                break

        if server_config is None:
            raise ValueError(f"MCP server '{server_name}' not found")

        # Get prompt (requires session - not implemented yet)
        # TODO: Implement session-based prompt retrieval
        raise NotImplementedError("Session-based prompt retrieval not yet implemented")

    # =============================================================================
    # MCP INTEGRATION HELPERS (using kailash.mcp_server)
    # =============================================================================
    # These methods provide convenient MCP integration using Kailash SDK's
    # production-ready MCP implementation. No mocking - real JSON-RPC protocol.
    #
    # Architecture: Kaizen EXTENDS Kailash SDK MCP (not recreate)
    # Implementation: kailash.mcp_server (100% MCP spec compliant)
    # =============================================================================

    async def setup_mcp_client(
        self,
        servers: List[Dict[str, Any]],
        retry_strategy: str = "circuit_breaker",
        enable_metrics: bool = True,
        **client_kwargs,
    ):
        """
        Setup MCP client for consuming external MCP tools.

        Uses Kailash SDK's production-ready MCPClient with full protocol support.

        Args:
            servers: List of MCP server configurations. Each server dict should contain:
                - name (str): Server name
                - transport (str): "stdio", "http", "sse", or "websocket"
                - command (str): Command to start server (for stdio)
                - args (List[str]): Arguments for command (for stdio)
                - url (str): Server URL (for http/sse/websocket)
                - headers (Dict): Optional HTTP headers
                - env (Dict): Optional environment variables
            retry_strategy: Retry strategy ("simple", "exponential", "circuit_breaker")
            enable_metrics: Enable metrics collection
            **client_kwargs: Additional MCPClient arguments

        Returns:
            MCPClient: Configured MCPClient instance

        Raises:
            ImportError: If kailash.mcp_server not available
            ValueError: If server config is invalid

        Example:
            >>> # STDIO transport (local process)
            >>> await agent.setup_mcp_client([
            ...     {
            ...         "name": "filesystem-tools",
            ...         "transport": "stdio",
            ...         "command": "npx",
            ...         "args": ["@modelcontextprotocol/server-filesystem", "/data"]
            ...     }
            ... ])
            >>>
            >>> # HTTP transport (remote server)
            >>> await agent.setup_mcp_client([
            ...     {
            ...         "name": "api-tools",
            ...         "transport": "http",
            ...         "url": "http://localhost:8080",
            ...         "headers": {"Authorization": "Bearer token"}
            ...     }
            ... ])

        Note:
            - All MCP methods are async (use await)
            - Real JSON-RPC protocol (no mocking)
            - Enterprise features: auth, retry, circuit breaker
            - 100% MCP spec compliant: tools, resources, prompts
        """
        try:
            from kailash.mcp_server import MCPClient
        except ImportError:
            raise ImportError(
                "kailash.mcp_server not available. Install with: pip install kailash"
            )

        # Create production MCP client
        self._mcp_client = MCPClient(
            retry_strategy=retry_strategy,
            enable_metrics=enable_metrics,
            **client_kwargs,
        )

        # Discover tools from all servers
        self._available_mcp_tools = {}

        for server_config in servers:
            # Validate server config
            if "name" not in server_config or "transport" not in server_config:
                raise ValueError(
                    "Server config must include 'name' and 'transport' fields"
                )

            # Discover tools via real MCP protocol
            tools = await self._mcp_client.discover_tools(
                server_config, force_refresh=True
            )

            # Store tools with server info
            for tool in tools:
                tool_id = f"{server_config['name']}:{tool['name']}"
                self._available_mcp_tools[tool_id] = {
                    **tool,
                    "server_config": server_config,
                }

            logger.info(
                f"Discovered {len(tools)} tools from MCP server: {server_config['name']}"
            )

        logger.info(
            f"MCP client setup complete. {len(self._available_mcp_tools)} tools available."
        )

        return self._mcp_client

    async def call_mcp_tool(
        self,
        tool_id: str,
        arguments: Dict[str, Any],
        timeout: float = 30.0,
        store_in_memory: bool = True,
    ) -> Dict[str, Any]:
        """
        Call MCP tool by ID using real JSON-RPC protocol.

        Args:
            tool_id: Tool ID (format: "server_name:tool_name")
            arguments: Tool arguments (must match tool schema)
            timeout: Timeout in seconds
            store_in_memory: Store tool call in shared memory

        Returns:
            Dict with tool result. Structure depends on tool implementation.

        Raises:
            RuntimeError: If MCP client not setup
            ValueError: If tool_id not found
            Exception: If tool invocation fails

        Example:
            >>> # Setup MCP client first
            >>> await agent.setup_mcp_client([...])
            >>>
            >>> # Call tool
            >>> result = await agent.call_mcp_tool(
            ...     "filesystem-tools:read_file",
            ...     {"path": "/data/input.txt"}
            ... )
            >>> print(result)

        Note:
            - Tool calls are async (use await)
            - Results stored in shared memory automatically
            - Real MCP tool invocation via JSON-RPC
        """
        if not hasattr(self, "_mcp_client") or self._mcp_client is None:
            raise RuntimeError("MCP client not setup. Call setup_mcp_client() first.")

        if tool_id not in self._available_mcp_tools:
            available_tools = list(self._available_mcp_tools.keys())
            raise ValueError(
                f"Tool {tool_id} not found. Available tools: {available_tools}"
            )

        # Get tool info
        tool_info = self._available_mcp_tools[tool_id]
        server_config = tool_info["server_config"]
        tool_name = tool_info["name"]

        # Call via real MCP protocol
        result = await self._mcp_client.call_tool(
            server_config, tool_name, arguments, timeout=timeout
        )

        # Store in shared memory if enabled
        if store_in_memory and hasattr(self, "shared_memory") and self.shared_memory:
            self.write_to_memory(
                content={
                    "tool_id": tool_id,
                    "server": server_config["name"],
                    "tool_name": tool_name,
                    "arguments": arguments,
                    "result": result,
                    "agent_id": self.agent_id,
                },
                tags=["mcp_tool_call", server_config["name"], tool_name],
                importance=0.8,
            )

        return result

    def expose_as_mcp_server(
        self,
        server_name: str,
        tools: Optional[List[str]] = None,
        auth_provider: Optional[Any] = None,
        enable_auto_discovery: bool = True,
        **server_kwargs,
    ):
        """
        Expose agent as MCP server with real protocol support.

        Creates a production MCP server that exposes agent methods as MCP tools.

        Args:
            server_name: Server name for MCP registration
            tools: List of agent methods to expose (default: auto-detect public methods)
            auth_provider: Optional auth provider (APIKeyAuth, JWTAuth, etc.)
            enable_auto_discovery: Enable network discovery
            **server_kwargs: Additional MCPServer arguments

        Returns:
            MCPServer: Configured server (call .run() to start)

        Raises:
            ImportError: If kailash.mcp_server not available

        Example:
            >>> from kailash.mcp_server.auth import APIKeyAuth
            >>>
            >>> # Create agent
            >>> agent = MyAgent(config=config, signature=signature)
            >>>
            >>> # Expose as MCP server
            >>> auth = APIKeyAuth({"client1": "secret-key"})
            >>> server = agent.expose_as_mcp_server(
            ...     "analysis-agent",
            ...     tools=["analyze", "summarize"],
            ...     auth_provider=auth
            ... )
            >>>
            >>> # Start server (blocks)
            >>> server.run()

        Note:
            - Agent methods exposed as async MCP tools
            - Real JSON-RPC 2.0 protocol
            - Enterprise features: auth, metrics, monitoring
            - Service discovery via registry + network
        """
        try:
            from kailash.mcp_server import MCPServer
            from kailash.mcp_server import enable_auto_discovery as enable_discovery
        except ImportError:
            raise ImportError(
                "kailash.mcp_server not available. Install with: pip install kailash"
            )

        # Create production MCP server
        server = MCPServer(
            name=server_name,
            auth_provider=auth_provider,
            enable_metrics=True,
            enable_http_transport=True,
            **server_kwargs,
        )

        # Auto-detect tools if not specified
        if tools is None:
            # Expose all public methods (not starting with _)
            tools = [
                m
                for m in dir(self)
                if not m.startswith("_") and callable(getattr(self, m))
            ]

        # Wrap agent methods as MCP tools
        for tool_name in tools:
            if not hasattr(self, tool_name):
                logger.warning(f"Tool {tool_name} not found on agent, skipping")
                continue

            method = getattr(self, tool_name)

            # Create tool wrapper with dynamic name
            # Note: MCPServer.tool() decorator infers name from function __name__
            async def tool_wrapper(**kwargs):
                """Auto-generated MCP tool from agent method."""
                # Execute agent method
                result = method(**kwargs)

                # If result is awaitable, await it
                if hasattr(result, "__await__"):
                    result = await result

                return result

            # Set the function name so the decorator can infer it
            tool_wrapper.__name__ = tool_name

            # Register with MCP server
            server.tool()(tool_wrapper)

        # Store server reference
        self._mcp_server = server

        # Enable auto-discovery if requested
        if enable_auto_discovery:
            registrar = enable_discovery(server, enable_network_discovery=True)
            self._mcp_registrar = registrar
            logger.info(f"MCP server '{server_name}' ready with auto-discovery enabled")
        else:
            self._mcp_registrar = None
            logger.info(f"MCP server '{server_name}' ready")

        return server

    def cleanup(self):
        """
        Cleanup agent resources.

        This method is called by test fixtures during teardown to properly
        cleanup any resources held by the agent.

        Example:
            >>> agent = SimpleQAAgent(config)
            >>> try:
            ...     result = agent.ask("question")
            ... finally:
            ...     agent.cleanup()
        """
        # Cleanup MCP server if running
        if hasattr(self, "_mcp_server") and self._mcp_server is not None:
            try:
                # Stop server if it has a stop method
                if hasattr(self._mcp_server, "stop"):
                    self._mcp_server.stop()
            except Exception as e:
                logger.warning(f"Error stopping MCP server: {e}")
            self._mcp_server = None

        # Cleanup MCP registrar if active
        if hasattr(self, "_mcp_registrar") and self._mcp_registrar is not None:
            try:
                # Unregister from discovery if method exists
                if hasattr(self._mcp_registrar, "unregister"):
                    self._mcp_registrar.unregister()
            except Exception as e:
                logger.warning(f"Error unregistering from MCP discovery: {e}")
            self._mcp_registrar = None

        # Clear shared memory references
        if hasattr(self, "shared_memory") and self.shared_memory is not None:
            # Don't clear the memory itself (other agents may use it)
            # Just clear our reference
            self.shared_memory = None

        # Clear memory references
        if hasattr(self, "memory") and self.memory is not None:
            self.memory = None

        # Clear tool executor references (Tool Integration)
        if hasattr(self, "_tool_executor") and self._tool_executor is not None:
            self._tool_executor = None

        if hasattr(self, "_tool_registry") and self._tool_registry is not None:
            # Don't clear the registry itself (other agents may use it)
            # Just clear our reference
            self._tool_registry = None

        # Clear framework references to avoid memory leaks
        self._framework = None
        self._agent = None
        self._workflow = None

        logger.debug(f"Cleanup completed for agent {self.agent_id}")
