"""
Agent Orchestrator for AiCippy.

Coordinates multiple specialized agents to complete complex tasks
with parallel execution, dependency resolution, and result merging.

This module provides:
- Multi-agent task orchestration
- Parallel execution with concurrency limits
- Task decomposition and dependency resolution
- Bedrock model invocation with circuit breaker pattern
- Progress tracking and result aggregation
"""

from __future__ import annotations

import asyncio
import json
import uuid
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any, Final

import boto3
from botocore.config import Config
from botocore.exceptions import BotoCoreError, ClientError
from rich.progress import BarColumn, Progress, SpinnerColumn, TaskID, TextColumn

from aicippy.agents.models import (
    AGENT_SYSTEM_PROMPTS,
    AgentConfig,
    AgentResponse,
    AgentStatus,
    AgentTask,
    AgentType,
    TokenUsage,
)
from aicippy.config import get_settings
from aicippy.exceptions import (
    ContextExceededError,
    ErrorContext,
    ModelInvocationError,
    ModelRateLimitedError,
)
from aicippy.types import SessionId
from aicippy.utils.correlation import get_correlation_id
from aicippy.utils.logging import get_logger
from aicippy.utils.retry import CircuitBreaker, CircuitBreakerOpenError, async_retry

if TYPE_CHECKING:
    from collections.abc import Callable, Sequence
    from types import TracebackType

    from rich.console import Console

    from aicippy.platform.tenant_context import TenantContext

logger = get_logger(__name__)

# ============================================================================
# Constants
# ============================================================================

DEFAULT_MAX_AGENTS: Final[int] = 10
MAX_TASK_DECOMPOSITION_DEPTH: Final[int] = 10
BEDROCK_READ_TIMEOUT: Final[int] = 300
BEDROCK_CONNECT_TIMEOUT: Final[int] = 15
BEDROCK_MAX_RETRIES: Final[int] = 3

# Model quality parameters - unrestricted output
MODEL_TEMPERATURE: Final[float] = 0.4
MODEL_TOP_P: Final[float] = 0.95
MODEL_MAX_TOKENS_CLAUDE: Final[int] = 16384  # Claude supports up to 32K output
MODEL_MAX_TOKENS_LLAMA: Final[int] = 8192  # Llama 4 Maverick max output
MODEL_MAX_TOKENS: Final[int] = 8192  # Default fallback

# Confidence thresholds for self-evaluation
CONFIDENCE_HIGH: Final[float] = 0.85
CONFIDENCE_MEDIUM: Final[float] = 0.60
CONFIDENCE_LOW: Final[float] = 0.40

# Maximum retry attempts for failed agent tasks
AGENT_TASK_MAX_RETRIES: Final[int] = 2

# Supported image MIME types for multimodal
IMAGE_MIME_TYPES: Final[dict[str, str]] = {
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".gif": "image/gif",
    ".webp": "image/webp",
    ".bmp": "image/bmp",
}

# ============================================================================
# Agent Skill Metadata
# ============================================================================

AGENT_SKILL_METADATA: Final[dict[str, dict[str, Any]]] = {
    AgentType.ORCHESTRATOR.value: {
        "display_name": "Orchestrator",
        "description": "Coordinates agents, decomposes tasks, merges results",
        "primary_skills": [
            "task decomposition",
            "coordination",
            "result merging",
            "quality assurance",
        ],
        "secondary_skills": [
            "general programming",
            "architecture",
            "troubleshooting",
        ],
        "handles_keywords": ["coordinate", "plan", "combine", "overall", "general"],
    },
    AgentType.INFRA_CORE.value: {
        "display_name": "Infrastructure Core",
        "description": "AWS/GCloud/Azure infrastructure, IaC, networking, security",
        "primary_skills": [
            "CDK",
            "Terraform",
            "CloudFormation",
            "VPC",
            "IAM",
            "networking",
            "security groups",
        ],
        "secondary_skills": [
            "cost optimization",
            "compliance",
            "disaster recovery",
            "multi-account",
        ],
        "handles_keywords": [
            "infrastructure",
            "vpc",
            "subnet",
            "iam",
            "cdk",
            "terraform",
            "cloudformation",
            "security group",
            "network",
            "ec2",
            "ecs",
        ],
    },
    AgentType.BEDROCK_AI.value: {
        "display_name": "Bedrock AI",
        "description": "GenAI, LLM integration, RAG, prompt engineering, model selection",
        "primary_skills": [
            "Bedrock",
            "LLM",
            "RAG",
            "prompt engineering",
            "embeddings",
            "guardrails",
        ],
        "secondary_skills": [
            "SageMaker",
            "Vertex AI",
            "fine-tuning",
            "model evaluation",
        ],
        "handles_keywords": [
            "bedrock",
            "ai",
            "llm",
            "model",
            "prompt",
            "rag",
            "embedding",
            "guardrail",
            "knowledge base",
            "genai",
        ],
    },
    AgentType.API_GATEWAY.value: {
        "display_name": "API Gateway",
        "description": "API design, WebSocket, load balancing, GraphQL, service mesh",
        "primary_skills": [
            "API Gateway",
            "WebSocket",
            "CloudFront",
            "GraphQL",
            "REST API",
        ],
        "secondary_skills": [
            "load balancing",
            "service mesh",
            "gRPC",
            "rate limiting",
        ],
        "handles_keywords": [
            "api",
            "gateway",
            "websocket",
            "cloudfront",
            "graphql",
            "rest",
            "endpoint",
            "route",
            "load balancer",
        ],
    },
    AgentType.COGNITO_AUTH.value: {
        "display_name": "Authentication",
        "description": "Identity management, OAuth, JWT, MFA, session management",
        "primary_skills": [
            "Cognito",
            "OAuth2",
            "JWT",
            "OIDC",
            "MFA",
            "session management",
        ],
        "secondary_skills": [
            "Keycloak",
            "Auth0",
            "zero-trust",
            "SAML",
            "passkeys",
        ],
        "handles_keywords": [
            "auth",
            "login",
            "cognito",
            "jwt",
            "oauth",
            "token",
            "session",
            "mfa",
            "password",
            "identity",
        ],
    },
    AgentType.CLI_CORE.value: {
        "display_name": "CLI Development",
        "description": "Command-line tools, terminal UI, package distribution",
        "primary_skills": ["Typer", "Rich", "Click", "Textual", "CLI design"],
        "secondary_skills": ["PyPI publishing", "shell completions", "cross-platform"],
        "handles_keywords": [
            "cli",
            "command",
            "terminal",
            "typer",
            "rich",
            "textual",
            "console",
            "shell",
            "pip",
        ],
    },
    AgentType.MCP_BRIDGES.value: {
        "display_name": "MCP Bridges",
        "description": "Tool integration, API connectors, SDKs, webhooks",
        "primary_skills": ["MCP", "boto3", "SDK integration", "webhooks", "tool calling"],
        "secondary_skills": ["database connectors", "message queues", "OAuth clients"],
        "handles_keywords": [
            "mcp",
            "tool",
            "connector",
            "bridge",
            "sdk",
            "webhook",
            "integration",
            "api client",
        ],
    },
    AgentType.KNOWLEDGE_INGEST.value: {
        "display_name": "Knowledge Ingestion",
        "description": "Data pipelines, crawling, vector stores, ETL, search",
        "primary_skills": [
            "web crawling",
            "document processing",
            "vector stores",
            "chunking",
            "ETL",
        ],
        "secondary_skills": [
            "data quality",
            "search optimization",
            "streaming ingestion",
        ],
        "handles_keywords": [
            "ingest",
            "crawl",
            "scrape",
            "vector",
            "embedding",
            "chunk",
            "etl",
            "pipeline",
            "index",
            "search",
        ],
    },
    AgentType.OBSERVABILITY.value: {
        "display_name": "Observability",
        "description": "Logging, metrics, tracing, alerting, SRE practices",
        "primary_skills": [
            "CloudWatch",
            "X-Ray",
            "OpenTelemetry",
            "structured logging",
            "alerting",
        ],
        "secondary_skills": [
            "Prometheus",
            "Grafana",
            "Datadog",
            "SLI/SLO",
            "incident response",
        ],
        "handles_keywords": [
            "log",
            "metric",
            "trace",
            "alert",
            "monitor",
            "cloudwatch",
            "observability",
            "sli",
            "slo",
            "dashboard",
        ],
    },
    AgentType.EMAIL_NOTIFY.value: {
        "display_name": "Email & Notifications",
        "description": "Email delivery, push notifications, SMS, messaging workflows",
        "primary_skills": ["SES", "email templates", "push notifications", "SMS"],
        "secondary_skills": ["deliverability", "compliance", "analytics"],
        "handles_keywords": [
            "email",
            "notification",
            "ses",
            "push",
            "sms",
            "template",
            "message",
            "alert",
            "notify",
        ],
    },
    AgentType.CICD_DEPLOY.value: {
        "display_name": "CI/CD & Deployment",
        "description": "Build automation, deployment, Docker, release management",
        "primary_skills": [
            "GitHub Actions",
            "Docker",
            "CodePipeline",
            "PyPI publishing",
            "ECS deploy",
        ],
        "secondary_skills": [
            "feature flags",
            "database migrations",
            "security scanning",
            "ArgoCD",
        ],
        "handles_keywords": [
            "deploy",
            "ci",
            "cd",
            "github actions",
            "docker",
            "build",
            "release",
            "publish",
            "pipeline",
            "helm",
        ],
    },
}


# ============================================================================
# Data Classes
# ============================================================================


@dataclass(slots=True)
class AgentInstance:
    """
    Running agent instance with state tracking.

    Attributes:
        id: Unique agent identifier.
        agent_type: Type of specialized agent.
        config: Agent configuration.
        status: Current execution status.
        current_task: Currently executing task.
        total_tokens: Cumulative token usage.
        confidence: Self-assessed confidence score (0.0-1.0).
        retry_count: Number of retries for current task.
        started_at: Instance creation timestamp.
        last_activity: Last activity timestamp.
    """

    id: str
    agent_type: AgentType
    config: AgentConfig
    status: AgentStatus = AgentStatus.IDLE
    current_task: AgentTask | None = None
    total_tokens: TokenUsage = field(default_factory=TokenUsage)
    confidence: float = 1.0
    retry_count: int = 0
    started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
    last_activity: datetime = field(default_factory=lambda: datetime.now(UTC))

    def update_activity(self) -> None:
        """Update last activity timestamp."""
        self.last_activity = datetime.now(UTC)

    def add_tokens(self, usage: TokenUsage) -> None:
        """Add token usage to cumulative total."""
        self.total_tokens = self.total_tokens + usage
        self.update_activity()


# ============================================================================
# Orchestrator
# ============================================================================


class AgentOrchestrator:
    """
    Multi-agent orchestrator for complex task execution.

    Coordinates specialized agents, manages parallel execution,
    handles dependencies, and merges results with comprehensive
    error handling and observability.

    Features:
    - Parallel agent execution up to configurable limit
    - Task decomposition with dependency resolution
    - Circuit breaker pattern for Bedrock API
    - Structured logging with correlation IDs
    - Graceful error recovery

    Example:
        >>> orchestrator = AgentOrchestrator(model_id="opus", max_agents=5)
        >>> response = await orchestrator.chat("Explain AWS Lambda")
        >>> print(response.content)

    Example with progress:
        >>> from rich.console import Console
        >>> console = Console()
        >>> response = await orchestrator.run_task_with_progress(
        ...     "Deploy a serverless API",
        ...     console,
        ... )
    """

    __slots__ = (
        "_agents",
        "_bedrock_client",
        "_circuit_breaker",
        "_completed_tasks",
        "_lock",
        "_max_agents",
        "_model_id",
        "_session_id",
        "_settings",
        "_task_queue",
        "_tenant_ctx",
    )

    def __init__(
        self,
        model_id: str | None = None,
        max_agents: int = DEFAULT_MAX_AGENTS,
        session_id: str | None = None,
        tenant_ctx: TenantContext | None = None,
    ) -> None:
        """
        Initialize the orchestrator.

        Args:
            model_id: Bedrock model ID to use. Defaults to configured default.
            max_agents: Maximum number of parallel agents (capped by settings).
            session_id: Session identifier for tracking. Auto-generated if not provided.
            tenant_ctx: Optional tenant context for tenant-scoped operations
                and audit trail in Bedrock calls.

        Raises:
            ValueError: If max_agents is less than 1.
        """
        if max_agents < 1:
            raise ValueError("max_agents must be at least 1")

        self._settings = get_settings()
        self._model_id = model_id or self._settings.get_model_id()
        self._max_agents = min(max_agents, self._settings.max_parallel_agents)
        self._session_id = SessionId(session_id or str(uuid.uuid4()))
        self._tenant_ctx = tenant_ctx

        # Agent management
        self._agents: dict[str, AgentInstance] = {}
        self._task_queue: asyncio.Queue[AgentTask] = asyncio.Queue()
        self._completed_tasks: dict[str, AgentResponse] = {}
        self._lock = asyncio.Lock()

        # Circuit breaker for Bedrock API
        self._circuit_breaker = CircuitBreaker(
            failure_threshold=5,
            recovery_timeout=30.0,
            half_open_requests=3,
        )

        # Bedrock client (lazy initialization)
        self._bedrock_client: Any = None

        logger.info(
            "orchestrator_initialized",
            model_id=self._model_id,
            max_agents=self._max_agents,
            session_id=self._session_id,
            tenant_id=self._tenant_ctx.tenant_id if self._tenant_ctx else "",
        )

    @property
    def session_id(self) -> SessionId:
        """Get the session identifier."""
        return self._session_id

    @property
    def model_id(self) -> str:
        """Get the current model ID."""
        return self._model_id

    @property
    def max_agents(self) -> int:
        """Get the maximum number of parallel agents."""
        return self._max_agents

    @property
    def active_agent_count(self) -> int:
        """Get the number of currently active agents (running asyncio tasks)."""
        prefix = f"agent-{self._session_id}-"
        return sum(
            1
            for task in asyncio.all_tasks()
            if task.get_name().startswith(prefix) and not task.done()
        )

    def _get_bedrock_client(self) -> Any:
        """
        Get or create Bedrock runtime client with optimized configuration.

        Returns:
            Configured boto3 Bedrock runtime client.
        """
        if self._bedrock_client is None:
            config = Config(
                retries={
                    "max_attempts": BEDROCK_MAX_RETRIES,
                    "mode": "adaptive",
                },
                read_timeout=BEDROCK_READ_TIMEOUT,
                connect_timeout=BEDROCK_CONNECT_TIMEOUT,
            )

            session_kwargs: dict[str, Any] = {
                "region_name": self._settings.aws_region,
            }
            if self._settings.aws_profile:
                session_kwargs["profile_name"] = self._settings.aws_profile

            session = boto3.Session(**session_kwargs)
            self._bedrock_client = session.client(
                "bedrock-runtime",
                config=config,
            )

        return self._bedrock_client

    async def chat(
        self,
        message: str,
        image_paths: list[str] | None = None,
        conversation_history: list[dict[str, Any]] | None = None,
    ) -> AgentResponse:
        """
        Process a chat message and return response.

        Args:
            message: User message to process.
            image_paths: Optional list of image file paths for multimodal input.
            conversation_history: Optional prior conversation for context continuity.

        Returns:
            AgentResponse with the assistant's reply.
        """
        if not message or not message.strip():
            return AgentResponse.error_response(
                "Empty message provided",
                AgentType.ORCHESTRATOR,
            )

        correlation_id = get_correlation_id()
        logger.info(
            "chat_started",
            message_length=len(message),
            model=self._model_id,
            images=len(image_paths) if image_paths else 0,
            history_length=len(conversation_history) if conversation_history else 0,
            correlation_id=correlation_id,
            tenant_id=self._tenant_ctx.tenant_id if self._tenant_ctx else "",
        )

        try:
            response = await self._invoke_model(
                message=message,
                system_prompt=AGENT_SYSTEM_PROMPTS[AgentType.ORCHESTRATOR],
                image_paths=image_paths,
                conversation_history=conversation_history,
            )

            logger.info(
                "chat_completed",
                input_tokens=response.usage.input_tokens if response.usage else 0,
                output_tokens=response.usage.output_tokens if response.usage else 0,
                correlation_id=correlation_id,
            )

            return response

        except CircuitBreakerOpenError as e:
            logger.warning(
                "chat_circuit_breaker_open",
                error=str(e),
                correlation_id=correlation_id,
            )
            return AgentResponse.error_response(
                "Service temporarily unavailable. Please retry shortly.",
                AgentType.ORCHESTRATOR,
            )

        except (ModelInvocationError, ModelRateLimitedError) as e:
            logger.warning(
                "chat_model_error",
                error=str(e),
                error_code=e.code.name,
                correlation_id=correlation_id,
            )
            return AgentResponse.error_response(str(e), AgentType.ORCHESTRATOR)

        except Exception as e:
            logger.exception(
                "chat_failed",
                error=str(e),
                correlation_id=correlation_id,
            )
            return AgentResponse.error_response(
                f"Unexpected error: {e}",
                AgentType.ORCHESTRATOR,
            )


    async def chat_stream(
        self,
        message: str,
        image_paths: list[str] | None = None,
        conversation_history: list[dict[str, Any]] | None = None,
        on_token: Callable[[str], None] | None = None,
    ) -> AgentResponse:
        """
        Process a chat message with streaming response.

        Tokens are delivered to on_token callback as they arrive
        for real-time display in the terminal.

        Args:
            message: User message to process.
            image_paths: Optional list of image file paths for multimodal input.
            conversation_history: Optional prior conversation for context.
            on_token: Callback invoked with each text chunk as it streams.

        Returns:
            AgentResponse with the complete assistant reply.
        """
        if not message or not message.strip():
            return AgentResponse.error_response(
                "Empty message provided",
                AgentType.ORCHESTRATOR,
            )

        correlation_id = get_correlation_id()
        client = self._get_bedrock_client()

        # Build request body
        body: dict[str, Any]
        is_claude = "claude" in self._model_id.lower()
        is_llama = "llama" in self._model_id.lower()

        if is_claude:
            user_content: list[dict[str, Any]] = []
            if image_paths:
                user_content.extend(self._build_image_content(image_paths))
            user_content.append({"type": "text", "text": message})

            messages: list[dict[str, Any]] = []
            if conversation_history:
                messages.extend(conversation_history)
            messages.append({"role": "user", "content": user_content})

            body = {
                "anthropic_version": "bedrock-2023-05-31",
                "max_tokens": MODEL_MAX_TOKENS_CLAUDE,
                "temperature": MODEL_TEMPERATURE,
                "top_p": MODEL_TOP_P,
                "system": AGENT_SYSTEM_PROMPTS[AgentType.ORCHESTRATOR],
                "messages": messages,
            }
        elif is_llama:
            history_prompt = ""
            if conversation_history:
                for msg in conversation_history:
                    role = msg.get("role", "user")
                    msg_content = msg.get("content", "")
                    if isinstance(msg_content, list):
                        msg_content = " ".join(
                            block.get("text", "")
                            for block in msg_content
                            if block.get("type") == "text"
                        )
                    history_prompt += (
                        f"<|start_header_id|>{role}<|end_header_id|>\n\n"
                        f"{msg_content}<|eot_id|>"
                    )

            sys_prompt = AGENT_SYSTEM_PROMPTS[AgentType.ORCHESTRATOR]
            body = {
                "prompt": (
                    f"<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n"
                    f"{sys_prompt}<|eot_id|>"
                    f"{history_prompt}"
                    f"<|start_header_id|>user<|end_header_id|>\n\n"
                    f"{message}<|eot_id|>"
                    f"<|start_header_id|>assistant<|end_header_id|>\n\n"
                ),
                "max_gen_len": MODEL_MAX_TOKENS_LLAMA,
                "temperature": MODEL_TEMPERATURE,
                "top_p": MODEL_TOP_P,
            }
        else:
            # Fallback to non-streaming for unknown models
            return await self.chat(message, image_paths, conversation_history)

        # Streaming invocation — boto3 stream must be consumed synchronously
        # so we run the entire invoke+consume in a thread executor and use
        # an asyncio.Queue to bridge tokens back to the async caller.
        import queue as _queue

        token_queue: _queue.Queue[str | None] = _queue.Queue()
        result_holder: list[AgentResponse] = []

        def _stream_in_thread() -> None:
            """Run streaming invocation in a thread (boto3 is sync)."""
            try:
                resp = client.invoke_model_with_response_stream(
                    modelId=self._model_id,
                    body=json.dumps(body),
                    contentType="application/json",
                    accept="application/json",
                )

                full_content = ""
                input_tokens_val = 0
                output_tokens_val = 0

                stream = resp.get("body")
                if stream is None:
                    result_holder.append(
                        AgentResponse.error_response(
                            "No stream body in response",
                            AgentType.ORCHESTRATOR,
                        )
                    )
                    token_queue.put(None)
                    return

                for event in stream:
                    chunk = event.get("chunk")
                    if not chunk:
                        continue
                    chunk_data = json.loads(chunk["bytes"])

                    if is_claude:
                        event_type = chunk_data.get("type", "")
                        if event_type == "content_block_delta":
                            delta = chunk_data.get("delta", {})
                            text = delta.get("text", "")
                            if text:
                                full_content += text
                                token_queue.put(text)
                        elif event_type == "message_delta":
                            usage_data = chunk_data.get("usage", {})
                            output_tokens_val = usage_data.get(
                                "output_tokens", output_tokens_val
                            )
                        elif event_type == "message_start":
                            msg_data = chunk_data.get("message", {})
                            usage_data = msg_data.get("usage", {})
                            input_tokens_val = usage_data.get("input_tokens", 0)
                    elif is_llama:
                        gen = chunk_data.get("generation", "")
                        if gen:
                            # Strip Llama stop tokens from output
                            if "<|eot_id|>" in gen:
                                gen = gen.split("<|eot_id|>")[0]
                            if "<|end_of_text|>" in gen:
                                gen = gen.split("<|end_of_text|>")[0]
                            if gen:
                                full_content += gen
                                token_queue.put(gen)

                result_holder.append(
                    AgentResponse(
                        content=full_content,
                        usage=TokenUsage(
                            input_tokens=input_tokens_val,
                            output_tokens=output_tokens_val,
                        ),
                    )
                )
            except (ClientError, BotoCoreError, Exception) as exc:
                logger.warning(
                    "chat_stream_thread_error",
                    error=str(exc),
                    correlation_id=correlation_id,
                )
                result_holder.append(
                    AgentResponse.error_response(str(exc), AgentType.ORCHESTRATOR)
                )
            finally:
                token_queue.put(None)  # Sentinel

        try:
            loop = asyncio.get_running_loop()
            stream_future = loop.run_in_executor(None, _stream_in_thread)

            # Consume tokens from the queue as they arrive
            while True:
                try:
                    token = await loop.run_in_executor(
                        None, lambda: token_queue.get(timeout=0.05)
                    )
                    if token is None:
                        break
                    if on_token:
                        on_token(token)
                except Exception:
                    # Queue.get timeout — check if stream thread is done
                    if stream_future.done():
                        # Drain remaining tokens
                        while not token_queue.empty():
                            token = token_queue.get_nowait()
                            if token is None:
                                break
                            if on_token:
                                on_token(token)
                        break

            await stream_future  # Ensure thread completed

            if result_holder:
                result = result_holder[0]
                if result.is_error:
                    # Fallback to non-streaming on error
                    logger.warning(
                        "chat_stream_error_fallback",
                        error=result.content,
                        correlation_id=correlation_id,
                    )
                    return await self.chat(message, image_paths, conversation_history)

                logger.info(
                    "chat_stream_completed",
                    input_tokens=result.usage.input_tokens if result.usage else 0,
                    output_tokens=result.usage.output_tokens if result.usage else 0,
                    content_length=len(result.content),
                    correlation_id=correlation_id,
                )
                return result

            return await self.chat(message, image_paths, conversation_history)

        except Exception as e:
            logger.warning(
                "chat_stream_failed_fallback",
                error=str(e),
                correlation_id=correlation_id,
            )
            return await self.chat(message, image_paths, conversation_history)

    async def run_task_with_progress(
        self,
        task: str,
        console: Console,
    ) -> AgentResponse:
        """
        Execute a task with progress display.

        Args:
            task: Task description to execute.
            console: Rich console for progress display.

        Returns:
            AgentResponse with combined results from all agents.

        Note:
            Complex tasks are automatically decomposed into subtasks
            and executed in parallel where dependencies allow.
        """
        if not task or not task.strip():
            return AgentResponse.error_response(
                "Empty task provided",
                AgentType.ORCHESTRATOR,
            )

        correlation_id = get_correlation_id()
        logger.info(
            "task_started",
            task=task[:100],
            max_agents=self._max_agents,
            correlation_id=correlation_id,
        )

        try:
            # Analyze task and create subtasks
            subtasks = await self._analyze_and_decompose(task)

            if not subtasks:
                # Simple task - process directly
                logger.debug(
                    "task_simple_execution",
                    correlation_id=correlation_id,
                )
                return await self.chat(task)

            logger.info(
                "task_decomposed",
                subtask_count=len(subtasks),
                correlation_id=correlation_id,
            )

            # Create progress display
            with Progress(
                SpinnerColumn(),
                TextColumn("[progress.description]{task.description}"),
                BarColumn(),
                TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
                console=console,
                transient=True,
            ) as progress:
                # Add progress tasks for each subtask
                progress_tasks: dict[str, TaskID] = {}
                for subtask in subtasks:
                    task_id = progress.add_task(
                        f"[cyan]{subtask.agent_type.value}[/cyan]: {subtask.description[:50]}",
                        total=100,
                    )
                    progress_tasks[subtask.id] = task_id

                # Execute subtasks with parallelism
                results = await self._execute_subtasks(
                    subtasks,
                    lambda task_id, pct: progress.update(
                        progress_tasks.get(task_id, TaskID(0)),
                        completed=pct,
                    ),
                )

            # Merge results
            merged = self._merge_results(results)

            logger.info(
                "task_completed",
                subtask_count=len(subtasks),
                successful=sum(1 for r in results.values() if not r.is_error),
                failed=sum(1 for r in results.values() if r.is_error),
                correlation_id=correlation_id,
            )

            return merged

        except Exception as e:
            logger.exception(
                "task_failed",
                error=str(e),
                correlation_id=correlation_id,
            )
            return AgentResponse.error_response(
                f"Task execution failed: {e}",
                AgentType.ORCHESTRATOR,
            )

    async def _analyze_and_decompose(self, task: str) -> list[AgentTask]:
        """
        Analyze a task and decompose into subtasks for parallel execution.

        Args:
            task: Task description to analyze.

        Returns:
            List of subtasks. Empty list if task is simple.
        """
        analysis_prompt = (
            "Analyze this task and decompose into "
            "non-conflicting subtasks for parallel "
            "agent execution.\n"
        )
        analysis_prompt += f"""

RULES FOR DECOMPOSITION:
1. Each subtask must be INDEPENDENT - no overlapping scope with other subtasks
2. Each agent must have a CLEAR, NON-CONFLICTING boundary of responsibility
3. All subtasks must work toward the SAME GOAL with UNIFORM output standards
4. If the task is simple enough for one agent, return an empty list
5. Assign the MOST SPECIFIC agent type for each subtask

Available agent types:
- infra-core: AWS CDK, IAM, VPC, security groups, networking, CloudFormation
- bedrock-ai: Bedrock agents, knowledge bases, guardrails, model config, AI/ML
- api-gateway: API Gateway, WebSocket, Lambda authorizers, CloudFront, ALB
- cognito-auth: Cognito, authentication, authorization, JWT, OAuth, sessions
- cli-core: Python CLI, Typer, Rich, terminal UI, command design
- mcp-bridges: Tool connectors, SDK integrations, API bridges, webhooks
- knowledge-ingest: Data pipelines, crawling, indexing, vector stores, RAG
- observability: CloudWatch, X-Ray, logging, monitoring, alerting, dashboards
- email-notify: SES, email templates, push notifications, SMS, messaging
- cicd-deploy: GitHub Actions, CodePipeline, Docker, deployment, testing

Task: {task}

Respond with a JSON array. Each subtask must have:
- description: Precise task description (what to produce, not what to think about)
- agent_type: Exactly one agent type from above
- priority: 0-10 (10 = execute first, foundational work)
- dependencies: Array of subtask indices (0-based) that must complete before this starts
- output_format: Expected output format (code/config/architecture/documentation/analysis)

For a simple task: []
"""

        try:
            response = await self._invoke_model(
                message=analysis_prompt,
                system_prompt="You are a task analysis assistant. Respond only with valid JSON.",
            )

            # Parse JSON response
            content = response.content.strip()

            # Extract JSON from markdown code blocks if present
            if "```json" in content:
                content = content.split("```json")[1].split("```")[0]
            elif "```" in content:
                content = content.split("```")[1].split("```")[0]

            content = content.strip()

            try:
                subtask_data = json.loads(content)
            except json.JSONDecodeError as e:
                logger.warning(
                    "task_decomposition_json_error",
                    error=str(e),
                    content_preview=content[:200],
                )
                return []

            if not subtask_data or not isinstance(subtask_data, list):
                return []

            # Convert to AgentTask objects with validation
            subtasks: list[AgentTask] = []
            for _i, data in enumerate(subtask_data):
                if not isinstance(data, dict):
                    continue

                description = data.get("description", "")
                agent_type_str = data.get("agent_type", "")

                if not description or not agent_type_str:
                    continue

                try:
                    agent_type = AgentType.from_string(agent_type_str)
                except (ValueError, KeyError):
                    logger.warning(
                        "task_decomposition_invalid_agent_type",
                        agent_type=agent_type_str,
                    )
                    continue

                task_id = str(uuid.uuid4())
                dependencies = []

                # Resolve dependency indices to task IDs
                for dep_idx in data.get("dependencies", []):
                    if isinstance(dep_idx, int) and 0 <= dep_idx < len(subtasks):
                        dependencies.append(subtasks[dep_idx].id)

                subtasks.append(
                    AgentTask(
                        id=task_id,
                        description=description,
                        agent_type=agent_type,
                        priority=min(10, max(0, int(data.get("priority", 5)))),
                        dependencies=dependencies,
                    )
                )

            return subtasks

        except Exception as e:
            logger.warning(
                "task_decomposition_failed",
                error=str(e),
            )
            return []

    async def _execute_subtasks(
        self,
        subtasks: Sequence[AgentTask],
        progress_callback: Callable[[str, int], None],
    ) -> dict[str, AgentResponse]:
        """
        Execute subtasks with parallel execution and dependency handling.

        Args:
            subtasks: List of subtasks to execute.
            progress_callback: Callback for progress updates (task_id, percentage).

        Returns:
            Dictionary mapping task IDs to responses.
        """
        results: dict[str, AgentResponse] = {}
        pending: dict[str, AgentTask] = {task.id: task for task in subtasks}
        running: dict[str, asyncio.Task[AgentResponse]] = {}

        while pending or running:
            # Find tasks that can be started (all dependencies completed)
            ready = [
                task
                for task in pending.values()
                if all(dep in results for dep in task.dependencies)
            ]

            # Sort by priority (higher first) and start tasks up to max_agents
            ready.sort(key=lambda t: t.priority, reverse=True)

            for task in ready:
                if len(running) >= self._max_agents:
                    break

                # Start task execution
                del pending[task.id]
                progress_callback(task.id, 10)

                async_task = asyncio.create_task(
                    self._execute_single_task(task, results, progress_callback),
                    name=f"agent-{self._session_id}-{task.agent_type.value}-{task.id[:8]}",
                )
                running[task.id] = async_task

            # Detect deadlock: tasks remain pending but none are ready and none are running
            if pending and not ready and not running:
                deadlocked_ids = list(pending.keys())
                logger.error(
                    "subtask_dependency_deadlock",
                    deadlocked_task_ids=deadlocked_ids,
                )
                for task_id in deadlocked_ids:
                    results[task_id] = AgentResponse.error_response(
                        "Task skipped due to unresolvable dependency cycle",
                        pending[task_id].agent_type,
                    )
                    progress_callback(task_id, 100)
                pending.clear()
                break

            if running:
                # Wait for at least one task to complete
                done, _ = await asyncio.wait(
                    running.values(),
                    return_when=asyncio.FIRST_COMPLETED,
                )

                # Process completed tasks -- snapshot keys to avoid mutation during iteration
                for completed_task in done:
                    # Find which task completed
                    completed_id: str | None = None
                    for tid, t in list(running.items()):
                        if t is completed_task:
                            completed_id = tid
                            break

                    if completed_id:
                        try:
                            results[completed_id] = completed_task.result()
                        except Exception as e:
                            logger.warning(
                                "subtask_exception",
                                task_id=completed_id,
                                error=str(e),
                            )
                            results[completed_id] = AgentResponse.error_response(
                                str(e),
                                AgentType.ORCHESTRATOR,
                            )

                        del running[completed_id]
                        progress_callback(completed_id, 100)

            # Small delay to prevent tight loop
            await asyncio.sleep(0.05)

        return results

    async def _execute_single_task(
        self,
        task: AgentTask,
        completed_results: dict[str, AgentResponse],
        progress_callback: Callable[[str, int], None],
    ) -> AgentResponse:
        """
        Execute a single subtask with context from dependencies.

        Includes self-evaluation of response confidence and automatic retry
        for failed or low-confidence results.

        Args:
            task: Task to execute.
            completed_results: Results from completed dependent tasks.
            progress_callback: Progress update callback.

        Returns:
            AgentResponse from task execution.
        """
        correlation_id = get_correlation_id()
        progress_callback(task.id, 20)

        # Build context from dependencies
        context_parts: list[str] = []
        for dep_id in task.dependencies:
            if dep_id in completed_results:
                dep_result = completed_results[dep_id]
                if not dep_result.is_error:
                    # Include substantial context for uniformity
                    content_preview = dep_result.content[:4000]
                    if len(dep_result.content) > 4000:
                        content_preview += "... [truncated]"
                    context_parts.append(f"Previous result: {content_preview}")

        context = "\n\n".join(context_parts) if context_parts else ""
        progress_callback(task.id, 40)

        # Get system prompt for this agent type
        system_prompt = AGENT_SYSTEM_PROMPTS.get(
            task.agent_type,
            AGENT_SYSTEM_PROMPTS[AgentType.ORCHESTRATOR],
        )

        # Build message with context and uniformity directives
        message_parts = [f"Execute this task:\n{task.description}"]
        if context:
            message_parts.append(
                "\nContext from completed tasks "
                "(maintain consistency with "
                f"these results):\n{context}"
            )
        message_parts.append(
            "\nOUTPUT REQUIREMENTS:"
            "\n- Provide COMPLETE, production-grade output (code, config, or analysis)"
            "\n- Use consistent naming conventions and patterns with other agent outputs"
            "\n- Include ALL necessary imports, dependencies, and configuration"
            "\n- Do NOT truncate, simplify, or leave placeholders"
            "\n- Maintain the SAME quality standard regardless of task complexity"
        )

        message = "\n".join(message_parts)
        progress_callback(task.id, 60)

        # Execute with the specialized agent
        try:
            response = await self._invoke_model(
                message=message,
                system_prompt=system_prompt,
            )

            response.agent_type = task.agent_type
            response.task_id = task.id

            # Self-evaluate response confidence
            confidence = self._evaluate_response_confidence(response)
            response.metadata["confidence"] = confidence

            # If confidence is critically low, attempt retry
            if confidence < CONFIDENCE_LOW and not response.is_error:
                logger.info(
                    "subtask_low_confidence_retry",
                    task_id=task.id,
                    agent_type=task.agent_type.value,
                    confidence=confidence,
                    correlation_id=correlation_id,
                )
                retry_response = await self._retry_failed_task(
                    task,
                    (
                        "Previous response had low confidence"
                        f" ({confidence:.2f}). Output may be"
                        " incomplete or uncertain."
                    ),
                    completed_results,
                    progress_callback,
                )
                retry_confidence = self._evaluate_response_confidence(retry_response)
                retry_response.metadata["confidence"] = retry_confidence
                retry_response.metadata["is_retry"] = True

                # Use retry only if it improved confidence
                if retry_confidence > confidence:
                    response = retry_response

            progress_callback(task.id, 90)

            logger.debug(
                "subtask_completed",
                task_id=task.id,
                agent_type=task.agent_type.value,
                confidence=response.metadata.get("confidence", 1.0),
                correlation_id=correlation_id,
            )

            return response

        except Exception as e:
            logger.warning(
                "subtask_failed",
                task_id=task.id,
                agent_type=task.agent_type.value,
                error=str(e),
                correlation_id=correlation_id,
            )

            # Attempt retry on failure
            error_response = await self._retry_failed_task(
                task, str(e), completed_results, progress_callback
            )
            if not error_response.is_error:
                logger.info(
                    "subtask_recovered_via_retry",
                    task_id=task.id,
                    agent_type=task.agent_type.value,
                    correlation_id=correlation_id,
                )
                return error_response

            return AgentResponse.error_response(str(e), task.agent_type)

    def _merge_results(
        self,
        results: dict[str, AgentResponse],
    ) -> AgentResponse:
        """
        Merge results from multiple agents into a single response.

        Includes confidence scoring summary and flags low-confidence results.

        Args:
            results: Dictionary of task results.

        Returns:
            Merged AgentResponse with combined content, usage, and confidence metadata.
        """
        if not results:
            return AgentResponse(content="No results to merge.")

        # Combine content by agent type
        content_parts: list[str] = []
        total_usage = TokenUsage()
        all_artifacts: list[dict[str, Any]] = []
        error_count = 0
        confidence_scores: list[float] = []
        low_confidence_agents: list[str] = []

        for _task_id, response in results.items():
            agent_label = response.agent_type.value if response.agent_type else "unknown"
            confidence = response.metadata.get("confidence", 1.0)
            confidence_scores.append(confidence)

            if response.is_error:
                error_count += 1
                content_parts.append(f"## {agent_label} (Error)\n{response.error}")
            else:
                # Add confidence warning for low-confidence results
                if confidence < CONFIDENCE_MEDIUM:
                    low_confidence_agents.append(agent_label)
                    content_parts.append(
                        f"## {agent_label} "
                        f"(Confidence: {confidence:.0%}"
                        " - verify recommended)"
                        f"\n{response.content}"
                    )
                else:
                    content_parts.append(f"## {agent_label}\n{response.content}")

            if response.usage:
                total_usage = total_usage + response.usage

            all_artifacts.extend(response.artifacts)

        merged_content = "\n\n---\n\n".join(content_parts)

        # Add summary header for errors or low confidence
        headers: list[str] = []
        if error_count > 0:
            headers.append(f"**Note:** {error_count} of {len(results)} tasks encountered errors.")
        if low_confidence_agents:
            headers.append(
                f"**Confidence Warning:** The following agents reported low confidence: "
                f"{', '.join(low_confidence_agents)}. Results should be verified."
            )
        if headers:
            merged_content = "\n".join(headers) + "\n\n" + merged_content

        # Calculate aggregate confidence
        avg_confidence = (
            sum(confidence_scores) / len(confidence_scores) if confidence_scores else 1.0
        )

        return AgentResponse(
            content=merged_content,
            agent_type=AgentType.ORCHESTRATOR,
            usage=total_usage,
            artifacts=all_artifacts,
            metadata={
                "aggregate_confidence": avg_confidence,
                "agent_count": len(results),
                "error_count": error_count,
                "low_confidence_count": len(low_confidence_agents),
            },
        )

    @staticmethod
    def _build_image_content(image_paths: list[str]) -> list[dict[str, Any]]:
        """Build multimodal image content blocks from file paths."""
        import base64
        from pathlib import Path

        content_blocks: list[dict[str, Any]] = []
        for path_str in image_paths:
            path = Path(path_str)
            if not path.exists():
                continue
            suffix = path.suffix.lower()
            mime_type = IMAGE_MIME_TYPES.get(suffix)
            if not mime_type:
                continue
            image_data = base64.standard_b64encode(path.read_bytes()).decode("ascii")
            content_blocks.append(
                {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": mime_type,
                        "data": image_data,
                    },
                }
            )
        return content_blocks

    @async_retry(max_attempts=3, min_wait=1.0, max_wait=10.0)
    async def _invoke_model(
        self,
        message: str,
        system_prompt: str,
        image_paths: list[str] | None = None,
        conversation_history: list[dict[str, Any]] | None = None,
    ) -> AgentResponse:
        """
        Invoke Bedrock model with retry logic and circuit breaker.

        Supports multimodal (text + images) for Claude models and
        conversation history for context continuity.
        """
        correlation_id = get_correlation_id()
        client = self._get_bedrock_client()

        # Build request body based on model (quality-tuned, unrestricted output)
        body: dict[str, Any]
        if "claude" in self._model_id.lower():
            # Build user content (text + optional images)
            user_content: list[dict[str, Any]] = []
            if image_paths:
                user_content.extend(self._build_image_content(image_paths))
            user_content.append({"type": "text", "text": message})

            # Build messages array with optional history
            messages: list[dict[str, Any]] = []
            if conversation_history:
                messages.extend(conversation_history)
            messages.append({"role": "user", "content": user_content})

            body = {
                "anthropic_version": "bedrock-2023-05-31",
                "max_tokens": MODEL_MAX_TOKENS_CLAUDE,
                "temperature": MODEL_TEMPERATURE,
                "top_p": MODEL_TOP_P,
                "system": system_prompt,
                "messages": messages,
            }
        elif "llama" in self._model_id.lower():
            # Build conversation context for Llama
            history_prompt = ""
            if conversation_history:
                for msg in conversation_history:
                    role = msg.get("role", "user")
                    content = msg.get("content", "")
                    if isinstance(content, list):
                        content = " ".join(
                            block.get("text", "")
                            for block in content
                            if block.get("type") == "text"
                        )
                    history_prompt += (
                        f"<|start_header_id|>{role}<|end_header_id|>\n\n{content}<|eot_id|>"
                    )

            body = {
                "prompt": (
                    f"<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n"
                    f"{system_prompt}<|eot_id|>"
                    f"{history_prompt}"
                    f"<|start_header_id|>user<|end_header_id|>\n\n"
                    f"{message}<|eot_id|>"
                    f"<|start_header_id|>assistant<|end_header_id|>\n\n"
                ),
                "max_gen_len": MODEL_MAX_TOKENS_LLAMA,
                "temperature": MODEL_TEMPERATURE,
                "top_p": MODEL_TOP_P,
            }
        else:
            body = {
                "prompt": f"{system_prompt}\n\nHuman: {message}\n\nAssistant:",
                "max_tokens": MODEL_MAX_TOKENS,
                "temperature": MODEL_TEMPERATURE,
            }

        # Async invoke function for circuit breaker
        async def invoke() -> dict[str, Any]:
            loop = asyncio.get_running_loop()
            try:
                response = await loop.run_in_executor(
                    None,
                    lambda: client.invoke_model(
                        modelId=self._model_id,
                        body=json.dumps(body),
                        contentType="application/json",
                        accept="application/json",
                    ),
                )
                return json.loads(response["body"].read())
            except ClientError as e:
                error_code = e.response.get("Error", {}).get("Code", "")
                error_message = e.response.get("Error", {}).get("Message", str(e))

                if error_code == "ThrottlingException":
                    raise ModelRateLimitedError(
                        model_id=self._model_id,
                        context=ErrorContext(
                            correlation_id=correlation_id,
                            operation="model_invocation",
                        ),
                    ) from e
                elif error_code == "ValidationException" and "context" in error_message.lower():
                    raise ContextExceededError(
                        model_id=self._model_id,
                        tokens_used=0,  # Unknown
                        max_tokens=200000,
                        context=ErrorContext(
                            correlation_id=correlation_id,
                            operation="model_invocation",
                        ),
                    ) from e
                else:
                    raise ModelInvocationError(
                        model_id=self._model_id,
                        message=error_message,
                        context=ErrorContext(
                            correlation_id=correlation_id,
                            operation="model_invocation",
                            details={"error_code": error_code},
                        ),
                    ) from e
            except BotoCoreError as e:
                raise ModelInvocationError(
                    model_id=self._model_id,
                    message=str(e),
                    context=ErrorContext(
                        correlation_id=correlation_id,
                        operation="model_invocation",
                    ),
                ) from e

        # Execute through circuit breaker
        try:
            result = await self._circuit_breaker.call(invoke)
        except CircuitBreakerOpenError:
            logger.warning(
                "circuit_breaker_open",
                model=self._model_id,
                correlation_id=correlation_id,
            )
            raise
        except (ModelInvocationError, ModelRateLimitedError, ContextExceededError):
            raise
        except Exception as e:
            logger.error(
                "model_invocation_failed",
                error=str(e),
                model=self._model_id,
                correlation_id=correlation_id,
            )
            raise ModelInvocationError(
                model_id=self._model_id,
                message=str(e),
                context=ErrorContext(
                    correlation_id=correlation_id,
                    operation="model_invocation",
                ),
            ) from e

        # Parse response based on model
        if "claude" in self._model_id.lower():
            content = result.get("content", [{}])[0].get("text", "")
            usage = TokenUsage(
                input_tokens=result.get("usage", {}).get("input_tokens", 0),
                output_tokens=result.get("usage", {}).get("output_tokens", 0),
            )
        elif "llama" in self._model_id.lower():
            content = result.get("generation", "")
            usage = TokenUsage(
                input_tokens=result.get("prompt_token_count", 0),
                output_tokens=result.get("generation_token_count", 0),
            )
        else:
            content = result.get("completion", result.get("generated_text", ""))
            usage = TokenUsage()

        return AgentResponse(
            content=content,
            usage=usage,
        )

    @staticmethod
    def get_agent_skill_metadata(agent_type: AgentType) -> dict[str, Any]:
        """
        Get skill metadata for an agent type.

        Args:
            agent_type: The agent type to look up.

        Returns:
            Dictionary with display_name, description, primary_skills,
            secondary_skills, and handles_keywords.
        """
        return AGENT_SKILL_METADATA.get(
            agent_type.value,
            {
                "display_name": agent_type.value,
                "description": "General-purpose agent",
                "primary_skills": [],
                "secondary_skills": [],
                "handles_keywords": [],
            },
        )

    @staticmethod
    def find_best_agent_for_query(query: str) -> AgentType:
        """
        Find the best agent type for a given query based on keyword matching.

        Uses the AGENT_SKILL_METADATA keyword mappings to score each agent
        and returns the one with the highest relevance score.

        Args:
            query: The user query or task description.

        Returns:
            The most relevant AgentType. Falls back to ORCHESTRATOR.
        """
        query_lower = query.lower()
        best_agent = AgentType.ORCHESTRATOR
        best_score = 0

        for agent_type_str, metadata in AGENT_SKILL_METADATA.items():
            if agent_type_str == AgentType.ORCHESTRATOR.value:
                continue

            score = 0
            for keyword in metadata.get("handles_keywords", []):
                if keyword in query_lower:
                    score += 2
            for skill in metadata.get("primary_skills", []):
                if skill.lower() in query_lower:
                    score += 1

            if score > best_score:
                best_score = score
                try:
                    best_agent = AgentType.from_string(agent_type_str)
                except ValueError:
                    logger.warning(
                        "agent_type_parse_failed",
                        agent_type=agent_type_str,
                        exc_info=True,
                    )

        return best_agent

    def _evaluate_response_confidence(self, response: AgentResponse) -> float:
        """
        Evaluate the confidence of an agent response based on heuristics.

        Checks for indicators of uncertainty, errors, or incomplete output.

        Args:
            response: The agent response to evaluate.

        Returns:
            Confidence score between 0.0 and 1.0.
        """
        if response.is_error:
            return 0.0

        content = response.content.lower()
        confidence = 1.0

        # Check for uncertainty indicators
        uncertainty_phrases = [
            "i'm not sure",
            "i am not sure",
            "i'm uncertain",
            "i don't know",
            "i cannot determine",
            "it's unclear",
            "may not be accurate",
            "this might not",
            "i believe",
            "possibly",
            "approximately",
        ]
        for phrase in uncertainty_phrases:
            if phrase in content:
                confidence -= 0.1

        # Check for empty or very short responses
        if len(response.content.strip()) < 50:
            confidence -= 0.3

        # Check for placeholder indicators
        placeholder_phrases = ["todo", "placeholder", "not implemented", "coming soon", "tbd"]
        for phrase in placeholder_phrases:
            if phrase in content:
                confidence -= 0.15

        # Ensure confidence stays in valid range
        return max(0.0, min(1.0, confidence))

    async def _retry_failed_task(
        self,
        task: AgentTask,
        original_error: str,
        completed_results: dict[str, AgentResponse],
        _progress_callback: Callable[[str, int], None],
    ) -> AgentResponse:
        """
        Retry a failed agent task with enhanced error context.

        Provides the original error to the agent so it can adjust its approach.

        Args:
            task: The task that failed.
            original_error: Error message from the original attempt.
            completed_results: Results from completed dependent tasks.
            progress_callback: Progress update callback.

        Returns:
            AgentResponse from the retry attempt.
        """
        logger.info(
            "agent_task_retry",
            task_id=task.id,
            agent_type=task.agent_type.value,
            original_error=original_error[:200],
        )

        # Build retry message with error context
        context_parts: list[str] = []
        for dep_id in task.dependencies:
            if dep_id in completed_results:
                dep_result = completed_results[dep_id]
                if not dep_result.is_error:
                    content_preview = dep_result.content[:2000]
                    context_parts.append(f"Previous result: {content_preview}")

        context = "\n\n".join(context_parts) if context_parts else ""

        system_prompt = AGENT_SYSTEM_PROMPTS.get(
            task.agent_type,
            AGENT_SYSTEM_PROMPTS[AgentType.ORCHESTRATOR],
        )

        retry_message = (
            f"RETRY: A previous attempt at this task failed with error:\n{original_error}\n\n"
            f"Please try a different approach to complete this task:\n{task.description}"
        )
        if context:
            retry_message += f"\n\nContext from completed tasks:\n{context}"

        retry_message += (
            "\nOUTPUT REQUIREMENTS:"
            "\n- Provide COMPLETE, production-grade output"
            "\n- Address the failure from the previous attempt"
            "\n- Include ALL necessary imports and configuration"
        )

        try:
            response = await self._invoke_model(
                message=retry_message,
                system_prompt=system_prompt,
            )
            response.agent_type = task.agent_type
            response.task_id = task.id
            return response
        except Exception as e:
            logger.warning(
                "agent_task_retry_failed",
                task_id=task.id,
                error=str(e),
            )
            return AgentResponse.error_response(
                f"Retry also failed: {e}",
                task.agent_type,
            )

    async def close(self) -> None:
        """Clean up orchestrator resources and cancel any running tasks."""
        self._agents.clear()
        self._completed_tasks.clear()

        # Clear task queue
        while not self._task_queue.empty():
            try:
                self._task_queue.get_nowait()
            except asyncio.QueueEmpty:
                break

        # Cancel any still-running agent tasks from _execute_subtasks
        # (scoped to this orchestrator's session to avoid cancelling other instances' tasks)
        prefix = f"agent-{self._session_id}-"
        for task in asyncio.all_tasks():
            if task.get_name().startswith(prefix) and not task.done():
                task.cancel()

        logger.debug("orchestrator_closed", session_id=self._session_id)

    async def __aenter__(self) -> AgentOrchestrator:
        """Async context manager entry."""
        return self

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: TracebackType | None,
    ) -> None:
        """Async context manager exit with cleanup."""
        await self.close()
