mirror of
https://github.com/HKUDS/OpenSpace.git
synced 2026-08-28 05:15:00 +00:00
fix: CAPTURED skills write to correct host agent skill dir
This commit is contained in:
parent
8e6d49df74
commit
79a98abda7
3 changed files with 106 additions and 10 deletions
|
|
@ -578,6 +578,20 @@ async def execute_task(
|
|||
if skill_dirs:
|
||||
await _auto_register_skill_dirs(skill_dirs)
|
||||
|
||||
# Determine where CAPTURED skills should be written.
|
||||
# Prefer the explicit skill_dirs parameter (= calling host agent's dir),
|
||||
# then fall back to the first env-based host skill dir.
|
||||
capture_skill_dir: str | None = None
|
||||
if skill_dirs:
|
||||
capture_skill_dir = skill_dirs[0]
|
||||
elif host_skill_dirs_raw:
|
||||
first_env = next(
|
||||
(d.strip() for d in host_skill_dirs_raw.split(",") if d.strip()),
|
||||
None,
|
||||
)
|
||||
if first_env:
|
||||
capture_skill_dir = first_env
|
||||
|
||||
# Cloud search + import (if requested)
|
||||
imported_skills: List[Dict[str, Any]] = []
|
||||
if search_scope == "all":
|
||||
|
|
@ -588,6 +602,7 @@ async def execute_task(
|
|||
task=task,
|
||||
workspace_dir=workspace_dir,
|
||||
max_iterations=max_iterations,
|
||||
capture_skill_dir=capture_skill_dir,
|
||||
)
|
||||
|
||||
# Write .upload_meta.json for each evolved skill
|
||||
|
|
|
|||
|
|
@ -145,6 +145,11 @@ class EvolutionContext:
|
|||
# Available tools for agent loop (read_file, web_search, shell, MCP, etc.)
|
||||
available_tools: List["BaseTool"] = field(default_factory=list)
|
||||
|
||||
# For CAPTURED: preferred directory to write the new skill.
|
||||
# Set from the calling host agent's skill directory so captured skills
|
||||
# are written back to the correct host, not always to _skill_dirs[0].
|
||||
capture_dir: Optional[Path] = None
|
||||
|
||||
|
||||
class SkillEvolver:
|
||||
"""Execute skill evolution actions.
|
||||
|
|
@ -259,13 +264,20 @@ class SkillEvolver:
|
|||
|
||||
# Trigger 1: post-analysis
|
||||
async def process_analysis(
|
||||
self, analysis: ExecutionAnalysis,
|
||||
self,
|
||||
analysis: ExecutionAnalysis,
|
||||
capture_dir: Optional[Path] = None,
|
||||
) -> List[SkillRecord]:
|
||||
"""Process all evolution suggestions from a completed analysis.
|
||||
|
||||
Called immediately after ``ExecutionAnalyzer.analyze_execution()``.
|
||||
Each suggestion becomes one evolution action, executed in parallel
|
||||
(throttled by semaphore).
|
||||
|
||||
Args:
|
||||
analysis: The completed execution analysis.
|
||||
capture_dir: Preferred directory for CAPTURED skills (host agent's
|
||||
skill dir). Falls back to ``registry._skill_dirs[0]`` when None.
|
||||
"""
|
||||
if not analysis.candidate_for_evolution:
|
||||
return []
|
||||
|
|
@ -273,7 +285,9 @@ class SkillEvolver:
|
|||
# Build contexts first (cheap, no LLM calls)
|
||||
contexts: List[EvolutionContext] = []
|
||||
for suggestion in analysis.evolution_suggestions:
|
||||
ctx = self._build_context_from_analysis(analysis, suggestion)
|
||||
ctx = self._build_context_from_analysis(
|
||||
analysis, suggestion, capture_dir=capture_dir,
|
||||
)
|
||||
if ctx is not None:
|
||||
contexts.append(ctx)
|
||||
|
||||
|
|
@ -948,13 +962,23 @@ class SkillEvolver:
|
|||
new_content = _set_frontmatter_field(new_content, "name", new_name)
|
||||
|
||||
# Create new skill directory via create_skill (handles multi-file FULL)
|
||||
skill_dirs = self._registry._skill_dirs
|
||||
if not skill_dirs:
|
||||
logger.warning("CAPTURED: no skill directories configured")
|
||||
return None
|
||||
# Priority chain for choosing the target skill root:
|
||||
# 1. ctx.capture_dir — explicitly set from host agent's skill_dirs param
|
||||
# 2. Infer from analysis — if this task used a skill from dir B,
|
||||
# captured skills belong alongside it (same host agent context)
|
||||
# 3. registry._skill_dirs[0] — ultimate fallback
|
||||
base_dir: Optional[Path] = None
|
||||
if ctx.capture_dir and ctx.capture_dir.is_dir():
|
||||
base_dir = ctx.capture_dir
|
||||
else:
|
||||
base_dir = self._infer_capture_dir_from_analysis(ctx)
|
||||
|
||||
# Directory name always matches the skill name
|
||||
base_dir = skill_dirs[0] # Primary user skill directory
|
||||
if base_dir is None:
|
||||
skill_dirs = self._registry._skill_dirs
|
||||
if not skill_dirs:
|
||||
logger.warning("CAPTURED: no skill directories configured")
|
||||
return None
|
||||
base_dir = skill_dirs[0]
|
||||
target_dir = base_dir / new_name
|
||||
if target_dir.exists():
|
||||
new_name = f"{new_name}-{uuid.uuid4().hex[:6]}"
|
||||
|
|
@ -1016,6 +1040,45 @@ class SkillEvolver:
|
|||
logger.info(f"CAPTURED: {new_name} [{new_id}]")
|
||||
return new_record
|
||||
|
||||
def _infer_capture_dir_from_analysis(
|
||||
self, ctx: EvolutionContext,
|
||||
) -> Optional[Path]:
|
||||
"""Infer the best skill root for a CAPTURED skill from analysis context.
|
||||
|
||||
When ``capture_dir`` is not explicitly set (no ``skill_dirs`` param
|
||||
from the host agent), we look at which skills were used during the
|
||||
task that triggered the capture. If a used skill lives under one
|
||||
of the registered skill roots, that root is a reasonable home for
|
||||
the new captured skill (same host agent context).
|
||||
"""
|
||||
if not ctx.recent_analyses:
|
||||
return None
|
||||
|
||||
registry_roots = self._registry._skill_dirs
|
||||
if not registry_roots:
|
||||
return None
|
||||
|
||||
for analysis in ctx.recent_analyses:
|
||||
for judgment in analysis.skill_judgments:
|
||||
if not judgment.skill_applied:
|
||||
continue
|
||||
rec = self._store.load_record(judgment.skill_id)
|
||||
if not rec or not rec.path:
|
||||
continue
|
||||
skill_path = Path(rec.path).parent # e.g. /A/foo/
|
||||
for root in registry_roots:
|
||||
try:
|
||||
skill_path.relative_to(root)
|
||||
logger.debug(
|
||||
"CAPTURED: inferred capture dir %s from "
|
||||
"applied skill %s", root, judgment.skill_id,
|
||||
)
|
||||
return root
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
async def _run_evolution_loop(
|
||||
self,
|
||||
prompt: str,
|
||||
|
|
@ -1362,13 +1425,15 @@ class SkillEvolver:
|
|||
self,
|
||||
analysis: ExecutionAnalysis,
|
||||
suggestion: EvolutionSuggestion,
|
||||
capture_dir: Optional[Path] = None,
|
||||
) -> Optional[EvolutionContext]:
|
||||
"""Build EvolutionContext from a single analysis suggestion.
|
||||
|
||||
Loads all target skills referenced by ``suggestion.target_skill_ids``.
|
||||
For FIX: exactly 1 parent required.
|
||||
For DERIVED: 1+ parents (multi-parent = merge).
|
||||
For CAPTURED: parents list is empty.
|
||||
For CAPTURED: parents list is empty; ``capture_dir`` controls where
|
||||
the new skill is written (defaults to registry's first skill root).
|
||||
"""
|
||||
records: List[SkillRecord] = []
|
||||
contents: List[str] = []
|
||||
|
|
@ -1412,6 +1477,7 @@ class SkillEvolver:
|
|||
source_task_id=analysis.task_id,
|
||||
recent_analyses=[analysis],
|
||||
available_tools=self._available_tools,
|
||||
capture_dir=capture_dir,
|
||||
)
|
||||
|
||||
def _load_skill_content(self, record: SkillRecord) -> str:
|
||||
|
|
|
|||
|
|
@ -306,6 +306,7 @@ class OpenSpace:
|
|||
workspace_dir: Optional[str] = None,
|
||||
max_iterations: Optional[int] = None,
|
||||
task_id: Optional[str] = None,
|
||||
capture_skill_dir: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute a task with OpenSpace.
|
||||
|
|
@ -321,6 +322,9 @@ class OpenSpace:
|
|||
task_id: External task ID for recording/logging. If None, generates a random one.
|
||||
This allows external callers (e.g., OSWorld) to specify their own task ID
|
||||
so recordings can be easily matched with benchmark results.
|
||||
capture_skill_dir: Preferred directory for CAPTURED skills. In multi-host-agent
|
||||
scenarios, this should be the calling host agent's skill directory so
|
||||
newly captured skills are written to the correct location.
|
||||
"""
|
||||
if not self._initialized:
|
||||
raise RuntimeError(
|
||||
|
|
@ -351,6 +355,7 @@ class OpenSpace:
|
|||
self._running = True
|
||||
self._task_done.clear()
|
||||
self._last_evolved_skills = [] # Reset per-execution tracking
|
||||
self._capture_skill_dir = capture_skill_dir
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
# Use external task_id if provided, otherwise generate one
|
||||
if task_id is None:
|
||||
|
|
@ -806,7 +811,17 @@ class OpenSpace:
|
|||
for s in analysis.evolution_suggestions
|
||||
)
|
||||
logger.info(f"[Skill Evolution] Suggestions: {evo_summary}")
|
||||
evolved_records = await self._skill_evolver.process_analysis(analysis)
|
||||
|
||||
capture_dir = None
|
||||
if getattr(self, "_capture_skill_dir", None):
|
||||
from pathlib import Path as _P
|
||||
_cd = _P(self._capture_skill_dir)
|
||||
if _cd.is_dir():
|
||||
capture_dir = _cd
|
||||
|
||||
evolved_records = await self._skill_evolver.process_analysis(
|
||||
analysis, capture_dir=capture_dir,
|
||||
)
|
||||
|
||||
# Track evolved skills for the caller
|
||||
for rec in evolved_records:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue