@@ -588,7 +588,8 @@ def __init__(
588588 tool_config : Optional [Union [bool , 'ToolConfig' ]] = None , # Tool execution configuration (timeout, retry, parallel)
589589 learn : Optional [Union [bool , str , Dict [str , Any ], 'LearnConfig' ]] = None , # Continuous learning (peer to memory)
590590 backend : Optional [Any ] = None , # External managed agent backend (e.g., ManagedAgentIntegration)
591- cli_backend : Optional [Union [str , Any ]] = None , # CLI backend for delegating turns (e.g., "claude-code")
591+ cli_backend : Optional [Union [str , Any ]] = None , # CLI backend for delegating turns (e.g., "claude-code") - DEPRECATED
592+ runtime : Optional [Union [str , Dict [str , Any ], 'AgentRuntimeConfig' ]] = None , # Model-scoped runtime configuration
592593 interrupt_controller : Optional ['InterruptController' ] = None , # G2: Cooperative cancellation
593594 tool_search : Optional [Union [bool , str , Dict [str , Any ], 'ToolSearchConfig' ]] = False , # Progressive tool disclosure
594595 message_steering : Optional [Union [bool , 'MessageSteeringProtocol' ]] = False , # Real-time message steering during execution
@@ -689,13 +690,15 @@ def __init__(
689690 - None: Use local execution (default)
690691 When provided, agent can delegate execution to managed infrastructure
691692 for long-running tasks or when local resources are constrained.
692- cli_backend: CLI backend for delegating full turns to external CLI tools. Accepts:
693- - str: Backend ID ("claude-code", "codex-cli", "gemini-cli")
694- - CliBackendProtocol: Custom CLI backend instance
695- - None: Use standard LLM execution (default)
696- When provided, agent delegates entire conversation turns to the CLI tool
697- instead of using the built-in LLM. Enables session continuity and
698- tool integration through external AI coding assistants.
693+ cli_backend: **DEPRECATED** CLI backend for delegating full turns. Use 'runtime' parameter instead.
694+ This parameter is deprecated in favor of model-scoped runtime configuration.
695+ Will emit deprecation warning when used.
696+ runtime: Model-scoped runtime configuration for turn delegation. Accepts:
697+ - str: Runtime ID ("claude-code", "praisonai")
698+ - Dict[str, Any]: Runtime config {"runtime": "claude-code", "config_overrides": {...}}
699+ - AgentRuntimeConfig: Pre-configured runtime instance
700+ - None: Use model-based resolution (default)
701+ Enables turn delegation with per-model runtime selection and fail-closed behavior.
699702 tool_search: Progressive tool disclosure configuration. Accepts:
700703 - bool: False=disabled (default), True=auto mode
701704 - str: Mode ("auto", "on", "off")
@@ -2032,10 +2035,26 @@ def __init__(
20322035 # Backend - external managed agent backend for hybrid execution
20332036 self .backend = backend
20342037
2035- # CLI Backend - external CLI backend for delegating full turns
2038+ # CLI Backend - external CLI backend for delegating full turns (DEPRECATED)
20362039 self ._cli_backend = None
20372040 if cli_backend is not None :
2041+ from ..utils .deprecation import warn_deprecated_param
2042+ warn_deprecated_param (
2043+ "cli_backend" ,
2044+ since = "1.0.0" ,
2045+ removal = "2.0.0" ,
2046+ alternative = "use 'runtime=' instead for model-scoped runtime configuration" ,
2047+ stacklevel = 3
2048+ )
20382049 self ._cli_backend = self ._resolve_cli_backend (cli_backend )
2050+
2051+ # Runtime Configuration - model-scoped runtime selection
2052+ self ._runtime_config = None
2053+ if runtime is not None :
2054+ self ._runtime_config = self ._resolve_runtime_config (runtime )
2055+
2056+ # Runtime resolver for per-turn resolution
2057+ self ._runtime_resolver = None # Will be initialized on first use
20392058
20402059 # Telemetry - lazy initialized via property for performance
20412060 self .__telemetry = None
@@ -5079,17 +5098,163 @@ def _resolve_cli_backend(self, cli_backend):
50795098
50805099 raise TypeError (f"Invalid cli_backend type: { type (cli_backend )} . Expected CliBackendProtocol instance or factory callable." )
50815100
5082- async def _chat_via_cli_backend (self , prompt : str , ** kwargs ) -> Optional [str ]:
5101+ def _resolve_runtime_config (self , runtime ):
5102+ """Resolve runtime parameter to AgentRuntimeConfig instance.
5103+
5104+ Args:
5105+ runtime: Runtime configuration parameter
5106+
5107+ Returns:
5108+ AgentRuntimeConfig instance
5109+
5110+ Raises:
5111+ TypeError: If runtime type is invalid
5112+ ValueError: If runtime configuration is invalid
5113+ """
5114+ try :
5115+ from ..runtime import AgentRuntimeConfig
5116+ except ImportError :
5117+ raise ImportError (
5118+ "Runtime configuration features requested but not available. "
5119+ "This should not happen in a properly installed praisonaiagents package."
5120+ )
5121+
5122+ # If already an AgentRuntimeConfig instance, return as-is
5123+ if isinstance (runtime , AgentRuntimeConfig ):
5124+ return runtime
5125+
5126+ # If string, create config with runtime ID
5127+ if isinstance (runtime , str ):
5128+ return AgentRuntimeConfig .from_runtime_id (runtime )
5129+
5130+ # If dictionary, create config from dict
5131+ if isinstance (runtime , dict ):
5132+ return AgentRuntimeConfig .from_dict (runtime )
5133+
5134+ raise TypeError (f"Invalid runtime type: { type (runtime )} . Expected str, dict, or AgentRuntimeConfig instance." )
5135+
5136+ def _get_runtime_resolver (self ):
5137+ """Get or create runtime resolver instance (lazy initialization)."""
5138+ if self ._runtime_resolver is None :
5139+ try :
5140+ from ..runtime .resolver import RuntimeResolver
5141+ self ._runtime_resolver = RuntimeResolver ()
5142+ except ImportError :
5143+ raise ImportError (
5144+ "Runtime resolution features requested but not available. "
5145+ "This should not happen in a properly installed praisonaiagents package."
5146+ )
5147+ return self ._runtime_resolver
5148+
5149+ async def _resolve_turn_runtime (self ):
5150+ """Resolve runtime for current turn based on model and configuration.
5151+
5152+ Returns:
5153+ Runtime instance if resolution succeeds, None otherwise
5154+ """
5155+ try :
5156+ from ..runtime .resolver import RuntimeResolutionContext
5157+ except ImportError :
5158+ return None
5159+
5160+ # Create resolution context with current model and provider info
5161+ context = RuntimeResolutionContext (
5162+ model_name = getattr (self , 'llm' , None ),
5163+ provider_name = self ._extract_provider_from_model (getattr (self , 'llm' , None )),
5164+ agent_config = {'agent_id' : getattr (self , 'agent_id' , None )}
5165+ )
5166+
5167+ try :
5168+ resolver = self ._get_runtime_resolver ()
5169+ result = resolver .resolve_runtime_instance (
5170+ context = context ,
5171+ model_runtime_configs = getattr (self , '_model_runtime_configs' , None ),
5172+ provider_runtime_configs = getattr (self , '_provider_runtime_configs' , None ),
5173+ legacy_cli_backend = getattr (self , '_cli_backend' , None )
5174+ )
5175+
5176+ if result and result .runtime :
5177+ return result .runtime
5178+
5179+ except (ValueError , RuntimeError ) as e :
5180+ # Only fall back for registry not initialized, propagate configuration errors
5181+ if "not initialized" in str (e ).lower ():
5182+ # Registry not initialized - this is expected in SDK-only mode
5183+ pass
5184+ else :
5185+ # Configuration error (unknown runtime ID etc.) - fail closed
5186+ raise RuntimeError (
5187+ f"Runtime resolution failed for agent={ getattr (self , 'display_name' , 'unknown' )!r} , "
5188+ f"model={ getattr (self , 'llm' , None )!r} : { e } . "
5189+ "Fix the runtime ID/configuration or remove the runtime override."
5190+ ) from e
5191+
5192+ return None
5193+
5194+ def _extract_provider_from_model (self , model_name ):
5195+ """Extract provider name from model name.
5196+
5197+ Args:
5198+ model_name: Model name string (e.g., "anthropic/claude-3-sonnet")
5199+
5200+ Returns:
5201+ Provider name if detectable, None otherwise
5202+ """
5203+ if not model_name or not isinstance (model_name , str ):
5204+ return None
5205+
5206+ # Common provider patterns
5207+ if '/' in model_name :
5208+ return model_name .split ('/' )[0 ]
5209+
5210+ # Provider inference based on model name patterns
5211+ model_lower = model_name .lower ()
5212+ if 'claude' in model_lower or 'anthropic' in model_lower :
5213+ return 'anthropic'
5214+ elif 'gpt' in model_lower or 'openai' in model_lower :
5215+ return 'openai'
5216+ elif 'gemini' in model_lower or 'google' in model_lower :
5217+ return 'google'
5218+ elif 'llama' in model_lower :
5219+ return 'meta'
5220+
5221+ return None
5222+
5223+ async def _chat_via_runtime (self , runtime_instance , prompt : str , ** kwargs ) -> Optional [str ]:
5224+ """Chat implementation using resolved runtime instance.
5225+
5226+ This is similar to _chat_via_cli_backend but uses the resolved runtime
5227+ from the model-scoped runtime resolution system.
5228+
5229+ Args:
5230+ runtime_instance: Resolved runtime instance (CliBackendProtocol)
5231+ prompt: User prompt
5232+ **kwargs: Additional chat parameters
5233+
5234+ Returns:
5235+ Runtime response content
5236+ """
5237+ if not runtime_instance :
5238+ raise RuntimeError ("Runtime instance is None" )
5239+
5240+ # Delegate to CLI backend implementation with runtime instance
5241+ # Pass runtime instance directly to avoid mutating shared state
5242+ return await self ._chat_via_cli_backend (prompt = prompt , cli_backend = runtime_instance , ** kwargs )
5243+
5244+ async def _chat_via_cli_backend (self , prompt : str , cli_backend : Any = None , ** kwargs ) -> Optional [str ]:
50835245 """Chat implementation using CLI backend delegation.
50845246
50855247 Args:
50865248 prompt: User prompt
5249+ cli_backend: Optional specific backend instance to use (for runtime delegation)
50875250 **kwargs: Additional chat parameters (passed through as metadata)
50885251
50895252 Returns:
50905253 CLI backend response content
50915254 """
5092- if not self ._cli_backend :
5255+ # Use provided backend or fall back to instance backend
5256+ backend = cli_backend or self ._cli_backend
5257+ if not backend :
50935258 raise RuntimeError ("CLI backend not configured" )
50945259
50955260 try :
@@ -5128,7 +5293,7 @@ async def _chat_via_cli_backend(self, prompt: str, **kwargs) -> Optional[str]:
51285293 images = None
51295294
51305295 # Execute CLI backend
5131- result = await self . _cli_backend .execute (
5296+ result = await backend .execute (
51325297 prompt = prompt ,
51335298 session = session_binding ,
51345299 images = images ,
0 commit comments