11from __future__ import annotations
22
3- import asyncio
4- import uuid
53from dataclasses import dataclass , field
6- from pathlib import Path
74from typing import TYPE_CHECKING , Any , Dict , List , Optional
85
96from openspace .agents import GroundingAgent
@@ -151,15 +148,7 @@ def from_container(
151148 container : AppContainer ,
152149 config : Optional [OpenSpaceConfig ] = None ,
153150 ) -> "OpenSpace" :
154- """Create an OpenSpace instance backed by an AppContainer.
155-
156- .. note::
157-
158- Phase 1 only stores the container for property access.
159- ``initialize()`` still constructs its own services.
160- Phase 4 will wire ``initialize()`` to resolve from the
161- container, making injected services authoritative.
162- """
151+ """Create an OpenSpace instance backed by an AppContainer."""
163152 return cls (config = config , container = container )
164153
165154 # ── Public property accessors (replace private field access) ──────
@@ -207,95 +196,30 @@ async def initialize(self) -> None:
207196 logger .info ("Initializing OpenSpace..." )
208197
209198 try :
199+ # 1. LLM clients
210200 self ._llm_factory = LLMFactory (config = self .config )
211201 self ._llm_client = self ._llm_factory .create_main ()
212202 logger .info (f"✓ LLM Client: { self .config .llm_model } " )
213203
214- # Load grounding config
215- # If custom config is provided, merge it with default configs
216- # load_config supports multiple files and deep merges them (later files override earlier ones)
217- if self .config .grounding_config_path :
218- from openspace .config .constants import CONFIG_GROUNDING , CONFIG_SECURITY
219- from openspace .config .loader import CONFIG_DIR
220-
221- # Load default configs + custom config (custom values will override defaults)
222- grounding_config = load_config (
223- CONFIG_DIR / CONFIG_GROUNDING , CONFIG_DIR / CONFIG_SECURITY , self .config .grounding_config_path
224- )
225- logger .info (f"Merged custom grounding config: { self .config .grounding_config_path } " )
226- else :
227- # Load default configs only
228- grounding_config = get_config ()
229-
230- # Optional: enable ClawWork productivity tools for fair benchmark comparison
231- if getattr (self .config , "use_clawwork_productivity" , False ):
232- shell_cfg = grounding_config .shell .model_copy (
233- update = {
234- "use_clawwork_productivity" : True ,
235- "working_dir" : self .config .workspace_dir or grounding_config .shell .working_dir ,
236- }
237- )
238- grounding_config = grounding_config .model_copy (update = {"shell" : shell_cfg })
239- logger .info ("ClawWork productivity tools enabled (shell.working_dir used as sandbox root)" )
240-
241- # Resolve backend_scope early so we can skip initializing
242- # providers that are not in scope (e.g. web when only shell is needed).
243- agent_config = get_agent_config ("GroundingAgent" )
244- _cli_max_iter = self .config .grounding_max_iterations
245- _default_max_iter = OpenSpaceConfig ().grounding_max_iterations # dataclass default (20)
246- if agent_config :
247- cfg_max_iter = agent_config .get ("max_iterations" , _default_max_iter )
248- if _cli_max_iter != _default_max_iter :
249- max_iterations = _cli_max_iter
250- else :
251- max_iterations = cfg_max_iter
252- backend_scope = (
253- self .config .backend_scope
254- or agent_config .get ("backend_scope" )
255- or ["gui" , "shell" , "mcp" , "web" , "system" ]
256- )
257- visual_analysis_timeout = agent_config .get ("visual_analysis_timeout" , 30.0 )
258- self .config .grounding_max_iterations = max_iterations
259- logger .info (
260- f"Loaded GroundingAgent config from config_agents.json (max_iterations={ max_iterations } , visual_analysis_timeout={ visual_analysis_timeout } s)"
261- )
262- else :
263- max_iterations = self .config .grounding_max_iterations
264- backend_scope = self .config .backend_scope or ["gui" , "shell" , "mcp" , "web" , "system" ]
265- visual_analysis_timeout = 30.0
266- logger .warning (f"config_agents.json not found, using default config (max_iterations={ max_iterations } )" )
267-
268- # Filter enabled_backends in grounding config to only those in scope,
269- # so providers outside scope (e.g. web) are never registered/initialized.
270- if grounding_config .enabled_backends :
271- scope_set = set (backend_scope )
272- filtered = [
273- entry for entry in grounding_config .enabled_backends if entry .get ("name" , "" ).lower () in scope_set
274- ]
275- if len (filtered ) != len (grounding_config .enabled_backends ):
276- skipped = [
277- entry .get ("name" )
278- for entry in grounding_config .enabled_backends
279- if entry .get ("name" , "" ).lower () not in scope_set
280- ]
281- logger .info (f"Skipping backends not in scope: { skipped } " )
282- grounding_config = grounding_config .model_copy (update = {"enabled_backends" : filtered })
283-
204+ # 2. Grounding config + client
205+ grounding_config , backend_scope , max_iterations , visual_analysis_timeout = (
206+ self ._load_grounding_config ()
207+ )
284208 self ._grounding_config = grounding_config
285209 self ._grounding_client = GroundingClient (config = grounding_config )
286210 await self ._grounding_client .initialize_all_providers ()
287-
288211 backends = list (self ._grounding_client .list_providers ().keys ())
289212 logger .info (f"✓ Grounding Client: { len (backends )} backends" )
290213 logger .debug (f" Available backends: { [b .value for b in backends ]} " )
291214
215+ # 3. Recording
292216 if self .config .enable_recording :
293217 self ._recording_service = RecordingService (config = self .config )
294218 self ._recording_manager = self ._recording_service .create (llm_client = self ._llm_client )
295219 self ._recording_service .wire (grounding_client = self ._grounding_client )
296220 logger .info (f"✓ Recording enabled: { len (self ._recording_manager .backends or [])} backends" )
297221
298- # Create separate LLM client for tool retrieval if configured
222+ # 4. Tool retrieval LLM + GroundingAgent
299223 tool_retrieval_llm = self ._llm_factory .create_tool_retrieval ()
300224 if tool_retrieval_llm :
301225 logger .info (f"✓ Tool retrieval LLM: { self .config .tool_retrieval_model } " )
@@ -314,62 +238,10 @@ async def initialize(self) -> None:
314238 )
315239 logger .info (f"✓ GroundingAgent: { ', ' .join (backend_scope )} " )
316240
317- # Initialize SkillRegistry (settings from config_grounding.json → skills)
318- if self ._grounding_config and self ._grounding_config .skills .enabled :
319- self ._tool_registry = ToolRegistry (
320- config = self .config ,
321- grounding_config = self ._grounding_config ,
322- llm_client = self ._llm_client ,
323- )
324- if self ._tool_registry .discover ():
325- self ._skill_registry = self ._tool_registry .registry
326- skills = self ._skill_registry .list_skills ()
327- logger .info (f"✓ Skills: { len (skills )} discovered" )
328- self ._grounding_agent .set_skill_registry (self ._skill_registry )
329- else :
330- self ._skill_registry = None
331-
332- # Initialize ExecutionAnalyzer (requires recording + skills)
333- if self .config .enable_recording and self ._skill_registry :
334- try :
335- skill_store = SkillStore ()
336- self ._skill_store = skill_store # Expose for MCP server reuse
337-
338- # Sync filesystem skills → DB (creates initial records
339- # for newly discovered skills so that analysis stats
340- # can be recorded against them from the very first run).
341- await skill_store .sync_from_registry (self ._skill_registry .list_skills ())
342-
343- # Bridge: pass quality_manager so analysis can feed back
344- # LLM-identified tool issues to the tool quality system.
345- quality_mgr = self ._grounding_client .quality_manager if self ._grounding_client else None
346- self ._execution_analyzer = ExecutionAnalyzer (
347- store = skill_store ,
348- llm_client = self ._llm_client ,
349- model = self .config .execution_analyzer_model ,
350- skill_registry = self ._skill_registry ,
351- quality_manager = quality_mgr ,
352- )
353- logger .info ("✓ Execution analysis enabled" )
354-
355- # Share store with GroundingAgent so retrieve_skill
356- # can access quality metrics for LLM selection.
357- self ._grounding_agent ._skill_store = skill_store
358-
359- # Initialize SkillEvolver (reuses the same store & registry)
360- # available_tools will be updated before each evolution cycle
361- self ._skill_evolver = SkillEvolver (
362- store = skill_store ,
363- registry = self ._skill_registry ,
364- llm_client = self ._llm_client ,
365- model = self .config .skill_evolver_model ,
366- max_concurrent = self .config .evolution_max_concurrent ,
367- )
368- logger .info (f"✓ Skill evolution enabled (concurrent={ self .config .evolution_max_concurrent } )" )
369- except Exception as e :
370- logger .warning (f"Execution analyzer init failed (non-fatal): { e } " )
241+ # 5. Skill engine (registry, store, analyzer, evolver)
242+ await self ._setup_skill_engine ()
371243
372- # Create ExecutionEngine with all dependencies
244+ # 6. Execution engine (wires all deps)
373245 self ._execution_engine = ExecutionEngine (
374246 config = self .config ,
375247 grounding_agent = self ._grounding_agent ,
@@ -392,6 +264,121 @@ async def initialize(self) -> None:
392264 await self .cleanup ()
393265 raise
394266
267+ def _load_grounding_config (self ):
268+ """Load and merge grounding config, resolve backend scope and agent params.
269+
270+ Returns (grounding_config, backend_scope, max_iterations, visual_analysis_timeout).
271+ """
272+ if self .config .grounding_config_path :
273+ from openspace .config .constants import CONFIG_GROUNDING , CONFIG_SECURITY
274+ from openspace .config .loader import CONFIG_DIR
275+
276+ grounding_config = load_config (
277+ CONFIG_DIR / CONFIG_GROUNDING , CONFIG_DIR / CONFIG_SECURITY , self .config .grounding_config_path
278+ )
279+ logger .info (f"Merged custom grounding config: { self .config .grounding_config_path } " )
280+ else :
281+ grounding_config = get_config ()
282+
283+ if getattr (self .config , "use_clawwork_productivity" , False ):
284+ shell_cfg = grounding_config .shell .model_copy (
285+ update = {
286+ "use_clawwork_productivity" : True ,
287+ "working_dir" : self .config .workspace_dir or grounding_config .shell .working_dir ,
288+ }
289+ )
290+ grounding_config = grounding_config .model_copy (update = {"shell" : shell_cfg })
291+ logger .info ("ClawWork productivity tools enabled (shell.working_dir used as sandbox root)" )
292+
293+ agent_config = get_agent_config ("GroundingAgent" )
294+ _cli_max_iter = self .config .grounding_max_iterations
295+ _default_max_iter = OpenSpaceConfig ().grounding_max_iterations
296+
297+ if agent_config :
298+ cfg_max_iter = agent_config .get ("max_iterations" , _default_max_iter )
299+ max_iterations = _cli_max_iter if _cli_max_iter != _default_max_iter else cfg_max_iter
300+ backend_scope = (
301+ self .config .backend_scope
302+ or agent_config .get ("backend_scope" )
303+ or ["gui" , "shell" , "mcp" , "web" , "system" ]
304+ )
305+ visual_analysis_timeout = agent_config .get ("visual_analysis_timeout" , 30.0 )
306+ self .config .grounding_max_iterations = max_iterations
307+ logger .info (
308+ f"Loaded GroundingAgent config from config_agents.json "
309+ f"(max_iterations={ max_iterations } , visual_analysis_timeout={ visual_analysis_timeout } s)"
310+ )
311+ else :
312+ max_iterations = self .config .grounding_max_iterations
313+ backend_scope = self .config .backend_scope or ["gui" , "shell" , "mcp" , "web" , "system" ]
314+ visual_analysis_timeout = 30.0
315+ logger .warning (f"config_agents.json not found, using default config (max_iterations={ max_iterations } )" )
316+
317+ if grounding_config .enabled_backends :
318+ scope_set = set (backend_scope )
319+ filtered = [
320+ entry for entry in grounding_config .enabled_backends if entry .get ("name" , "" ).lower () in scope_set
321+ ]
322+ if len (filtered ) != len (grounding_config .enabled_backends ):
323+ skipped = [
324+ entry .get ("name" )
325+ for entry in grounding_config .enabled_backends
326+ if entry .get ("name" , "" ).lower () not in scope_set
327+ ]
328+ logger .info (f"Skipping backends not in scope: { skipped } " )
329+ grounding_config = grounding_config .model_copy (update = {"enabled_backends" : filtered })
330+
331+ return grounding_config , backend_scope , max_iterations , visual_analysis_timeout
332+
333+ async def _setup_skill_engine (self ) -> None :
334+ """Initialize skill registry, store, analyzer, and evolver."""
335+ if not (self ._grounding_config and self ._grounding_config .skills .enabled ):
336+ return
337+
338+ self ._tool_registry = ToolRegistry (
339+ config = self .config ,
340+ grounding_config = self ._grounding_config ,
341+ llm_client = self ._llm_client ,
342+ )
343+ if self ._tool_registry .discover ():
344+ self ._skill_registry = self ._tool_registry .registry
345+ skills = self ._skill_registry .list_skills ()
346+ logger .info (f"✓ Skills: { len (skills )} discovered" )
347+ self ._grounding_agent .set_skill_registry (self ._skill_registry )
348+ else :
349+ self ._skill_registry = None
350+
351+ if not (self .config .enable_recording and self ._skill_registry ):
352+ return
353+
354+ try :
355+ skill_store = SkillStore ()
356+ self ._skill_store = skill_store
357+ await skill_store .sync_from_registry (self ._skill_registry .list_skills ())
358+
359+ quality_mgr = self ._grounding_client .quality_manager if self ._grounding_client else None
360+ self ._execution_analyzer = ExecutionAnalyzer (
361+ store = skill_store ,
362+ llm_client = self ._llm_client ,
363+ model = self .config .execution_analyzer_model ,
364+ skill_registry = self ._skill_registry ,
365+ quality_manager = quality_mgr ,
366+ )
367+ logger .info ("✓ Execution analysis enabled" )
368+
369+ self ._grounding_agent ._skill_store = skill_store
370+
371+ self ._skill_evolver = SkillEvolver (
372+ store = skill_store ,
373+ registry = self ._skill_registry ,
374+ llm_client = self ._llm_client ,
375+ model = self .config .skill_evolver_model ,
376+ max_concurrent = self .config .evolution_max_concurrent ,
377+ )
378+ logger .info (f"✓ Skill evolution enabled (concurrent={ self .config .evolution_max_concurrent } )" )
379+ except Exception as e :
380+ logger .warning (f"Execution analyzer init failed (non-fatal): { e } " )
381+
395382 async def execute (
396383 self ,
397384 task : str ,
@@ -413,14 +400,6 @@ async def execute(
413400 task_id = task_id ,
414401 )
415402
416- # NOTE: _init_skill_registry, _select_and_inject_skills, and
417- # _get_skill_selection_llm have been extracted to ToolRegistry
418- # (openspace/tool_registry.py) in Epic 4.1.
419- #
420- # execute(), _maybe_analyze_execution(), and _maybe_evolve_quality()
421- # have been extracted to ExecutionEngine
422- # (openspace/execution_engine.py) in Epic 4.3.
423-
424403 async def cleanup (self ) -> None :
425404 """
426405 Close all sessions and release resources.
0 commit comments