```python:inference/core/workflows/core_steps/composition/workflow_caller/v1.py
import logging
from functools import lru_cache
from typing import Any, Callable, Dict, List, Optional, Tuple

from inference.core.roboflow_api import get_roboflow_workspace
from inference.core.workflows.execution_engine.entities.types import (
    WILDCARD_KIND, Kind)
from inference.core.workflows.prototypes.block import (BlockResult,
                                                       WorkflowBlock)

WORKFLOW_CALL_CHAIN_HEADER = "X-Roboflow-Workflow-Call-Chain"
MAX_WORKFLOW_CALL_DEPTH = 10

# Module-level cache mapping (workflow_id, version_id) -> {output_name: [Kind]}.
# Populated during compile-time validation, read by get_actual_outputs() and run().
_RESOLVED_WORKFLOW_OUTPUTS: Dict[Tuple[str, Optional[str]], Dict[str, List[Kind]]] = {}


class WorkflowCallerBlockV1(WorkflowBlock):

    def __init__(
        self,
        api_key: Optional[str],
        workflow_call_chain: Optional[str] = None,
    ):
        self._api_key = api_key
        self._workspace_name: Optional[str] = None
        self._workflow_call_chain = workflow_call_chain or ""

    def run(
        self,
        workflow_id: str,
        inputs: Dict[str, Any],
        request_timeout: int,
        input_definitions: Dict[str, List[str]],
        output_definitions: Dict[str, List[str]],
        workflow_version_id: Optional[str] = None,
    ) -> BlockResult:
        workspace_name = self._resolve_workspace_name()
        if workspace_name is None:
            raise RuntimeError(
                f"Could not resolve workspace for workflow '{workflow_id}'. "
                f"Ensure a valid API key is configured."
            )
        error_status, message, raw_outputs = call_workflow(
            api_key=self._api_key,
            workspace_name=workspace_name,
            workflow_id=workflow_id,
            inputs=inputs,
            request_timeout=request_timeout,
            call_chain=self._workflow_call_chain,
            workflow_version_id=workflow_version_id,
        )
        if error_status:
            raise RuntimeError(
                f"Workflow '{workflow_id}' execution failed: {message}"
            )
        result: Dict[str, Any] = {}
        # Determine which outputs to extract and their kinds for deserialization
        output_kinds_map = _resolve_output_kinds_for_run(
            output_definitions=output_definitions,
            workflow_id=workflow_id,
            workflow_version_id=workflow_version_id,
        )
        if output_kinds_map is not None:
            # Avoid building deserializers map if every declared output kind is a wildcard.
            any_non_wildcard = False
            for kinds in output_kinds_map.values():
                for kind in kinds:
                    if kind != WILDCARD_KIND:
                        any_non_wildcard = True
                        break
                if any_non_wildcard:
                    break

            deserializers = _build_kinds_deserializers_map() if any_non_wildcard else None
            raw_get = raw_outputs.get
            for output_name, output_kinds in output_kinds_map.items():
                raw_value = raw_get(output_name)
                if deserializers is None:
                    result[output_name] = raw_value
                else:
                    result[output_name] = _deserialize_output_value(
                        output_name=output_name,
                        raw_value=raw_value,
                        output_kinds=output_kinds,
                        deserializers=deserializers,
                    )
        else:
            result["result"] = raw_outputs
        return result

    def _resolve_workspace_name(self) -> Optional[str]:
        if self._workspace_name is not None:
            return self._workspace_name
        if not self._api_key:
            return None
        try:
            self._workspace_name = get_roboflow_workspace(api_key=self._api_key)
            return self._workspace_name
        except Exception as error:
            logging.warning(
                f"Could not resolve workspace from API key. Error: {str(error)}"
            )
            return None


def call_workflow(
    api_key: Optional[str],
    workspace_name: str,
    workflow_id: str,
    inputs: Dict[str, Any],
    request_timeout: int,
    call_chain: Optional[str] = None,
    workflow_version_id: Optional[str] = None,
) -> Tuple[bool, str, dict]:
    """Execute a workflow call and return (error_status, message, result).

    This function is separated from the block's run() to facilitate testing.
    """
    current_chain = call_chain or ""
    chain_entries = [entry for entry in current_chain.split(",") if entry]
    if workflow_id in chain_entries:
        logging.warning(
            f"Circular workflow call detected: "
            f"'{workflow_id}' is already in call chain [{current_chain}]."
        )
        return (
            True,
            f"Circular workflow call detected: '{workflow_id}' is already "
            f"in the call chain [{current_chain}]. Aborting to prevent "
            f"infinite recursion.",
            {},
        )
    if len(chain_entries) >= MAX_WORKFLOW_CALL_DEPTH:
        logging.warning(
            f"Workflow call depth limit ({MAX_WORKFLOW_CALL_DEPTH}) reached. "
            f"Call chain: [{current_chain}]."
        )
        return (
            True,
            f"Workflow call depth limit ({MAX_WORKFLOW_CALL_DEPTH}) reached. "
            f"Call chain: [{current_chain}].",
            {},
        )
    updated_chain = ",".join([*chain_entries, workflow_id])
    inputs = build_workflow_inputs(inputs=inputs)
    return execute_workflow_request(
        api_key=api_key,
        workspace_name=workspace_name,
        workflow_id=workflow_id,
        inputs=inputs,
        request_timeout=request_timeout,
        call_chain=updated_chain,
        workflow_version_id=workflow_version_id,
    )


# ---------------------------------------------------------------------------
# Output resolution for runtime
# ---------------------------------------------------------------------------


def _resolve_output_kinds_for_run(
    output_definitions: Dict[str, List[str]],
    workflow_id: str,
    workflow_version_id: Optional[str] = None,
) -> Optional[Dict[str, List[Kind]]]:
    """Determine which outputs to extract and their Kind objects.

    Priority:
    1. User-declared ``output_definitions`` (explicit kind names)
    2. Auto-discovered outputs from ``_RESOLVED_WORKFLOW_OUTPUTS`` cache
    3. Returns ``None`` → caller should fall back to generic ``result`` dict
    """
    if output_definitions:
        kind_name_map = _build_kind_name_map()
        result: Dict[str, List[Kind]] = {}
        for name, kind_names in output_definitions.items():
            kinds = [
                kind_name_map[k] for k in kind_names if k in kind_name_map
            ]
            result[name] = kinds or [WILDCARD_KIND]
        return result
    cache_key = _make_cache_key(workflow_id, workflow_version_id)
    resolved = _RESOLVED_WORKFLOW_OUTPUTS.get(cache_key)
    if resolved is not None:
        return resolved
    return None


# ---------------------------------------------------------------------------
# Output deserialization
# ---------------------------------------------------------------------------


@lru_cache(maxsize=1)
def _build_kinds_deserializers_map() -> Dict[str, Callable[[str, Any], Any]]:
    """Build the map of kind name -> deserializer function.

    Result is cached since the set of registered deserializers does not
    change within a process.
    """
    from inference.core.workflows.execution_engine.introspection.blocks_loader import \
        load_kinds_deserializers

    return load_kinds_deserializers()


def _deserialize_output_value(
    output_name: str,
    raw_value: Any,
    output_kinds: List[Kind],
    deserializers: Dict[str, Callable[[str, Any], Any]],
) -> Any:
    """Deserialize a single output value using the deserializer matching its
    resolved kind.

    For outputs with known kinds, deserialization is required — failure raises
    an error so downstream blocks don't receive unexpected raw data. For
    WILDCARD_KIND outputs the raw value is returned as-is since we don't know
    the expected type.
    """
    if raw_value is None:
        return None
    errors: List[Tuple[str, Exception]] = []
    non_wildcard_seen = False
    for kind in output_kinds:
        if kind == WILDCARD_KIND:
            continue
        non_wildcard_seen = True
        des = deserializers.get(kind.name)
        if des is None:
            continue
        try:
            return des(output_name, raw_value)
        except Exception as error:
            errors.append((kind.name, error))
            continue

    # If there were no non-wildcard kinds declared, return raw value (matches original behavior for empty lists)
    if not non_wildcard_seen:
        return raw_value

    if errors:
        error_details = "; ".join(
            f"{kind}: {error}" for kind, error in errors
        )
        raise RuntimeError(
            f"Failed to deserialize output '{output_name}' from target "
            f"workflow. Tried deserializers for kinds "
            f"{[k for k, _ in errors]} but all failed: {error_details}. "
            f"This likely means the target workflow's output format doesn't "
            f"match the expected kind."
        )
    return raw_value
```