1313
1414from loguru import logger
1515
16+ from facio .agent import status as agent_status
1617from facio .agent .autocompact import AutoCompact
1718from facio .agent .channel_context import (
1819 get_configured_keys_context ,
2526from facio .agent .loop_hook import LoopHook as _LoopHook
2627from facio .agent .memory import Consolidator , Reflection
2728from facio .agent .memory_search import MemorySearchDB
29+ from facio .agent .run_context import make_run_context , reset_run_context , set_run_context
2830from facio .agent .runner import _MAX_INJECTIONS_PER_TURN , AgentRunner , AgentRunSpec
29- from facio .agent .run_context import make_run_context , set_run_context , reset_run_context
3031from facio .agent .session_state import (
3132 PENDING_USER_TURN_KEY ,
3233 RUNTIME_CHECKPOINT_KEY ,
3334 SessionCheckpointManager ,
3435)
35- from facio .agent import status as agent_status
3636from facio .agent .stream_session import StreamSession
3737from facio .agent .subagent import SubagentManager
38- from facio .agent .turn_executor import TurnExecutor
3938from facio .agent .tools .file_state import FileStateStore , bind_file_states , reset_file_states
4039from facio .agent .tools .registry import ToolRegistry
41- from facio .bus . events import InboundMessage , OutboundMessage
40+ from facio .agent . turn_executor import TurnExecutor
4241from facio .bus import metadata as bus_meta
42+ from facio .bus .events import InboundMessage , OutboundMessage
4343from facio .bus .queue import MessageBus
4444from facio .command import CommandContext , CommandRouter , register_builtin_commands
4545from facio .config .schema import AgentDefaults
4646from facio .providers .base import LLMProvider
4747from facio .session .manager import Session , SessionManager
4848
4949if TYPE_CHECKING :
50- from facio .config .schema import ChannelsConfig , ExecToolConfig , WebToolsConfig
50+ from facio .config .schema import (
51+ BrowserToolsConfig ,
52+ ChannelsConfig ,
53+ ExecToolConfig ,
54+ WebToolsConfig ,
55+ )
5156 from facio .cron .service import CronService
5257 from facio .providers .pool import ProviderPool
5358
@@ -93,15 +98,15 @@ def __init__(
9398 context_block_limit : int | None = None ,
9499 max_tool_result_chars : int | None = None ,
95100 provider_retry_mode : str = "standard" ,
96- web_config : WebToolsConfig | None = None ,
101+ web_config : " WebToolsConfig | None" = None ,
97102 browser_config : "BrowserToolsConfig | None" = None ,
98- exec_config : ExecToolConfig | None = None ,
103+ exec_config : " ExecToolConfig | None" = None ,
99104 tool_timeout : int = 300 ,
100105 cron_service : CronService | None = None ,
101106 restrict_to_workspace : bool = False ,
102107 session_manager : SessionManager | None = None ,
103108 mcp_servers : dict | None = None ,
104- channels_config : ChannelsConfig | None = None ,
109+ channels_config : " ChannelsConfig | None" = None ,
105110 timezone : str | None = None ,
106111 session_ttl_minutes : int = 0 ,
107112 hooks : list [AgentHook ] | None = None ,
@@ -126,10 +131,12 @@ def __init__(
126131 policy_enabled : bool | None = None ,
127132 policy_skip_cron : bool | None = None ,
128133 credential_store : Any | None = None ,
134+ runtime_config : Any | None = None ,
135+ runtime_config_path : Path | None = None ,
129136 ):
130- from facio .config .schema import BrowserToolsConfig , ExecToolConfig , WebToolsConfig
131137 from facio .agent .credential_store import CredentialStore
132138 from facio .config .paths import get_data_dir
139+ from facio .config .schema import BrowserToolsConfig , ExecToolConfig , WebToolsConfig
133140
134141 defaults = AgentDefaults ()
135142 self .bus = bus
@@ -189,6 +196,9 @@ def __init__(
189196 # reaching into the runner's hook context.
190197 self ._current_iteration : int = 0
191198 self .provider_pool = provider_pool
199+ self ._runtime_config = runtime_config
200+ self ._runtime_config_path = runtime_config_path
201+ self ._provider_setup_task : asyncio .Task | None = None
192202 self .vision_model = vision_model
193203 self .vision_provider = vision_provider
194204 self .image_model = image_model
@@ -529,6 +539,105 @@ def _effective_session_key(self, msg: InboundMessage) -> str:
529539 return UNIFIED_SESSION_KEY
530540 return msg .session_key
531541
542+ def _load_current_runtime_config (self ) -> Any | None :
543+ if self ._runtime_config_path is None :
544+ return self ._runtime_config
545+ from facio .config .loader import load_config , resolve_config_env_vars
546+
547+ config = resolve_config_env_vars (load_config (self ._runtime_config_path ))
548+ config .agents .defaults .workspace = str (self .workspace )
549+ self ._runtime_config = config
550+ return config
551+
552+ @staticmethod
553+ def _placet_setup_available (config : Any ) -> bool :
554+ channels = config .model_dump (mode = "json" , by_alias = True ).get ("channels" , {})
555+ placet = channels .get ("placet" , {})
556+ return bool (placet .get ("enabled" ) or placet .get ("instanceUrl" ))
557+
558+ async def _maybe_handle_provider_setup (self , msg : InboundMessage ) -> bool :
559+ config = self ._load_current_runtime_config ()
560+ if config is None :
561+ return False
562+
563+ from facio .channels .placet_onboarding import (
564+ has_functioning_provider ,
565+ provider_setup_message ,
566+ )
567+
568+ if has_functioning_provider (config ):
569+ if getattr (self .provider , "_facio_unconfigured" , False ):
570+ await self ._reload_provider_from_config (config )
571+ return False
572+
573+ placet_available = self ._placet_setup_available (config )
574+ if placet_available :
575+ self ._start_provider_setup (config )
576+ await self .bus .publish_outbound (OutboundMessage (
577+ channel = msg .channel ,
578+ chat_id = msg .chat_id ,
579+ content = provider_setup_message (placet_available = placet_available ),
580+ metadata = dict (msg .metadata or {}),
581+ ))
582+ return True
583+
584+ def _start_provider_setup (self , config : Any ) -> None :
585+ if self ._runtime_config_path is None :
586+ return
587+ if self ._provider_setup_task is not None and not self ._provider_setup_task .done ():
588+ return
589+ self ._provider_setup_task = asyncio .create_task (self ._run_provider_setup (config ))
590+
591+ async def _run_provider_setup (self , config : Any ) -> None :
592+ if self ._runtime_config_path is None :
593+ return
594+ from facio .channels .placet_onboarding import run_onboarding
595+
596+ try :
597+ if await run_onboarding (config , self ._runtime_config_path ):
598+ await self ._reload_provider_from_config ()
599+ except asyncio .CancelledError :
600+ raise
601+ except Exception :
602+ logger .warning ("runtime provider setup failed" , exc_info = True )
603+
604+ async def _reload_provider_from_config (self , config : Any | None = None ) -> bool :
605+ config = config or self ._load_current_runtime_config ()
606+ if config is None :
607+ return False
608+
609+ from facio .channels .placet_onboarding import has_functioning_provider
610+
611+ if not has_functioning_provider (config ):
612+ return False
613+
614+ from facio .facio import _make_provider
615+ from facio .providers .pool import ProviderPool
616+
617+ provider = _make_provider (config )
618+ pool = ProviderPool (config )
619+ primary_name = config .get_provider_name (config .agents .defaults .model )
620+ if primary_name :
621+ pool .seed (primary_name , provider )
622+
623+ self .provider = provider
624+ self .model = config .agents .defaults .model
625+ self .runner = AgentRunner (provider )
626+ self .provider_pool = pool
627+ self .subagents .provider = provider
628+ self .subagents .runner = AgentRunner (provider )
629+ self .subagents .model = self .model
630+ self .subagents .subagent_model = config .agents .defaults .subagent_model or self .model
631+ self .subagents .provider_pool = pool
632+ self .consolidator .set_provider (provider , self .model , self .context_window_tokens )
633+ self .reflection .set_provider (provider , self .model )
634+ for tool in self ._hitl_tools :
635+ if hasattr (tool , "set_provider_pool" ):
636+ tool .set_provider_pool (pool )
637+ self ._runtime_config = config
638+ logger .info ("runtime provider setup completed with provider {}" , primary_name )
639+ return True
640+
532641 def _replay_token_budget (self ) -> int :
533642 """Derive a token budget for session history replay from the context window.
534643
@@ -802,6 +911,9 @@ async def _dispatch(self, msg: InboundMessage) -> None:
802911 self ._pending_queues [session_key ] = pending
803912
804913 try :
914+ if await self ._maybe_handle_provider_setup (msg ):
915+ _run_status ["stop_reason" ] = "provider_setup_required"
916+ return
805917 async with lock , gate :
806918 # Notify the runtime integration that the agent is busy.
807919 await self ._runtime .send_status (agent_status .BUSY )
@@ -976,6 +1088,9 @@ async def _dispatch(self, msg: InboundMessage) -> None:
9761088
9771089 async def close_mcp (self ) -> None :
9781090 """Drain pending background archives, then close MCP connections."""
1091+ if self ._provider_setup_task is not None and not self ._provider_setup_task .done ():
1092+ self ._provider_setup_task .cancel ()
1093+ await asyncio .gather (self ._provider_setup_task , return_exceptions = True )
9791094 if self ._background_tasks :
9801095 await asyncio .gather (* self ._background_tasks , return_exceptions = True )
9811096 self ._background_tasks .clear ()
0 commit comments