Skip to content

Commit e41f63c

Browse files
Brian KrafftCopilot
andcommitted
feat(5.1): Extract EvolutionModels into evolution/models.py
- New: openspace/skill_engine/evolution/ package - New: evolution/models.py (89 lines) — EvolutionTrigger enum, EvolutionContext dataclass, _sanitize_skill_name(), _MAX_SKILL_NAME_LENGTH constant - Updated: evolver.py imports from .evolution.models (removed local definitions) - Updated: mcp/tool_handlers.py imports from new location - Updated: skill_engine/__init__.py re-exports point to evolution.models - Updated: RUNBOOK.yaml — P4 marked complete, P5 active - New: tests/test_evolution_models.py — 17 tests (enum, dataclass, sanitize, compat) - evolver.py: 1498 → ~1460 lines (-38 lines) - All 1,425 tests pass, 127 skipped Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0ea03b2 commit e41f63c

8 files changed

Lines changed: 286 additions & 105 deletions

File tree

RUNBOOK.yaml

Lines changed: 49 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,25 @@ completed_phases:
6060
(SkillRepository, LineageTracker, AnalysisStore, TagSearch, MigrationManager)
6161
plus types.py. 7 epics + 3 validation/cleanup PRs. 1,356 tests passing.
6262
63+
- id: P4
64+
name: "Phase 4 — Tool Layer + MCP Decomposition"
65+
epics: 10
66+
pr_range: "#31–#38"
67+
notes: >
68+
tool_layer.py (788 lines) decomposed into 4 focused modules
69+
(ToolRegistry, ExecutionEngine, RecordingService, LLMFactory) + facade cleanup.
70+
mcp_server.py (992 lines) decomposed into openspace/mcp/ package
71+
(server.py, tool_handlers.py) + backward-compat shim. 2 epics skipped
72+
(4.2 covered by 4.1, 4.8 covered by P3). 62 new tests. 1,408 passing.
73+
6374
# ---------------------------------------------------------------------
6475
# In-Progress Phase
6576
# ---------------------------------------------------------------------
6677
active_phase:
67-
id: P4
68-
name: "Phase 4Tool Layer + MCP Decomposition"
69-
status: in_progress # pending | in_progress | complete
70-
epics_total: 10
78+
id: P5
79+
name: "Phase 5Skill Engine / Grounding Decomposition"
80+
status: in_progress
81+
epics_total: 11
7182
epics_merged: 0
7283

7384
# ---------------------------------------------------------------------
@@ -218,10 +229,10 @@ epics:
218229
- id: "4.1"
219230
phase: P4
220231
title: "Extract ToolRegistry"
221-
status: pending
222-
branch: null
223-
pr: null
224-
review_round: 0
232+
status: merged
233+
branch: "epic/4.1-tool-registry"
234+
pr: 31
235+
review_round: 2
225236
source_file: "openspace/tool_layer.py"
226237
target_module: "openspace/tool_registry.py"
227238
description: >
@@ -233,7 +244,7 @@ epics:
233244
- id: "4.2"
234245
phase: P4
235246
title: "Extract SkillSelector"
236-
status: pending
247+
status: skipped
237248
branch: null
238249
pr: null
239250
review_round: 0
@@ -249,10 +260,10 @@ epics:
249260
- id: "4.3"
250261
phase: P4
251262
title: "Extract ExecutionEngine"
252-
status: pending
253-
branch: null
254-
pr: null
255-
review_round: 0
263+
status: merged
264+
branch: "epic/4.3-execution-engine"
265+
pr: 32
266+
review_round: 2
256267
source_file: "openspace/tool_layer.py"
257268
target_module: "openspace/execution_engine.py"
258269
description: >
@@ -264,10 +275,10 @@ epics:
264275
- id: "4.4"
265276
phase: P4
266277
title: "Extract RecordingService"
267-
status: pending
268-
branch: null
269-
pr: null
270-
review_round: 0
278+
status: merged
279+
branch: "epic/4.4-recording-service"
280+
pr: 33
281+
review_round: 1
271282
source_file: "openspace/tool_layer.py"
272283
target_module: "openspace/recording_service.py"
273284
description: >
@@ -280,10 +291,10 @@ epics:
280291
- id: "4.5"
281292
phase: P4
282293
title: "Extract LLMAdapter"
283-
status: pending
284-
branch: null
285-
pr: null
286-
review_round: 0
294+
status: merged
295+
branch: "epic/4.5-llm-factory"
296+
pr: 34
297+
review_round: 1
287298
source_file: "openspace/tool_layer.py"
288299
target_module: "openspace/llm_adapter.py"
289300
description: >
@@ -295,10 +306,10 @@ epics:
295306
- id: "4.6"
296307
phase: P4
297308
title: "Extract ToolLayerFacade"
298-
status: pending
299-
branch: null
300-
pr: null
301-
review_round: 0
309+
status: merged
310+
branch: "epic/4.6-facade-cleanup"
311+
pr: 35
312+
review_round: 2
302313
source_file: "openspace/tool_layer.py"
303314
target_module: "openspace/tool_layer_facade.py"
304315
description: >
@@ -312,10 +323,10 @@ epics:
312323
- id: "4.7"
313324
phase: P4
314325
title: "Extract MCPToolHandlers"
315-
status: pending
316-
branch: null
317-
pr: null
318-
review_round: 0
326+
status: merged
327+
branch: "epic/4.7-mcp-tool-handlers"
328+
pr: 36
329+
review_round: 2
319330
source_file: "openspace/mcp_server.py"
320331
target_module: "openspace/mcp/tool_handlers.py"
321332
description: >
@@ -328,7 +339,7 @@ epics:
328339
- id: "4.8"
329340
phase: P4
330341
title: "Extract MCPAuthMiddleware"
331-
status: pending
342+
status: skipped
332343
branch: null
333344
pr: null
334345
review_round: 0
@@ -344,10 +355,10 @@ epics:
344355
- id: "4.9"
345356
phase: P4
346357
title: "Extract MCPRouter + MCPServer"
347-
status: pending
348-
branch: null
349-
pr: null
350-
review_round: 0
358+
status: merged
359+
branch: "epic/4.9-mcp-server-extract"
360+
pr: 37
361+
review_round: 2
351362
source_file: "openspace/mcp_server.py"
352363
target_module: "openspace/mcp/server.py"
353364
description: >
@@ -361,10 +372,10 @@ epics:
361372
- id: "4.10"
362373
phase: P4
363374
title: "P4 integration tests + MCP conformance"
364-
status: pending
365-
branch: null
366-
pr: null
367-
review_round: 0
375+
status: merged
376+
branch: "epic/4.10-integration-tests"
377+
pr: 38
378+
review_round: 2
368379
source_file: null
369380
target_module: "tests/test_mcp_conformance.py"
370381
description: >

_auth.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
4: For HTTP transports (SSE, streamable-http), bearer token auth is
2+
5: REQUIRED. Set OPENSPACE_MCP_BEARER_TOKEN in the environment.
3+
8: For stdio transport, auth is not applicable (local process IPC).
4+
18: from openspace.auth.bearer import (
5+
19: BEARER_TOKEN_ENV,
6+
20: BearerTokenMiddleware,
7+
21: get_bearer_token,
8+
24: from openspace.auth.rate_limit import RateLimitMiddleware
9+
43: # --- HTTP transports: enforce bearer token auth (fail-closed) ---
10+
44: token = get_bearer_token()
11+
48: "without authentication. Set the environment variable or "
12+
50: BEARER_TOKEN_ENV,
13+
57: logger.critical("FAIL-CLOSED: %s � %s", BEARER_TOKEN_ENV, reason)

openspace/mcp/tool_handlers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ async def fix_skill(
618618
"Add retry logic for HTTP 429 rate limit errors".
619619
"""
620620
try:
621-
from openspace.skill_engine.evolver import EvolutionContext, EvolutionTrigger
621+
from openspace.skill_engine.evolution.models import EvolutionContext, EvolutionTrigger
622622
from openspace.skill_engine.types import EvolutionSuggestion, EvolutionType
623623

624624
if not direction:

openspace/skill_engine/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@
5252
"MigrationManager": (".migration_manager", "MigrationManager"),
5353
"ExecutionAnalyzer": (".analyzer", "ExecutionAnalyzer"),
5454
"SkillEvolver": (".evolver", "SkillEvolver"),
55-
"EvolutionTrigger": (".evolver", "EvolutionTrigger"),
56-
"EvolutionContext": (".evolver", "EvolutionContext"),
55+
"EvolutionTrigger": (".evolution.models", "EvolutionTrigger"),
56+
"EvolutionContext": (".evolution.models", "EvolutionContext"),
5757
}
5858

5959

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# openspace.skill_engine.evolution — Evolution subsystem package
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Evolution domain models — pure data types and utilities.
2+
3+
Contains the core evolution types used across the evolution subsystem:
4+
- EvolutionTrigger: enum of what initiated evolution
5+
- EvolutionContext: unified context dataclass for all trigger sources
6+
- _sanitize_skill_name: naming-rule enforcer for skill directory names
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import re
12+
from dataclasses import dataclass, field
13+
from enum import Enum
14+
from pathlib import Path
15+
from typing import TYPE_CHECKING, List, Optional
16+
17+
from openspace.skill_engine.types import (
18+
EvolutionSuggestion,
19+
ExecutionAnalysis,
20+
SkillRecord,
21+
)
22+
23+
if TYPE_CHECKING:
24+
from openspace.grounding.core.tool import BaseTool
25+
26+
_MAX_SKILL_NAME_LENGTH = 50 # Max chars for a skill name (directory name)
27+
28+
29+
def _sanitize_skill_name(name: str) -> str:
30+
"""Enforce naming rules for skill names (used as directory names).
31+
32+
- Lowercase, hyphens only (no underscores or special chars)
33+
- Truncate to ``_MAX_SKILL_NAME_LENGTH`` at a word boundary
34+
- Remove trailing hyphens
35+
"""
36+
# Normalize: lowercase, replace underscores and spaces with hyphens
37+
clean = re.sub(r"[^a-z0-9\-]", "-", name.lower().strip())
38+
# Collapse multiple hyphens
39+
clean = re.sub(r"-{2,}", "-", clean).strip("-")
40+
41+
if len(clean) <= _MAX_SKILL_NAME_LENGTH:
42+
return clean
43+
44+
# Truncate at a hyphen boundary to avoid cutting words
45+
truncated = clean[:_MAX_SKILL_NAME_LENGTH]
46+
last_hyphen = truncated.rfind("-")
47+
if last_hyphen > _MAX_SKILL_NAME_LENGTH // 2:
48+
truncated = truncated[:last_hyphen]
49+
return truncated.strip("-")
50+
51+
52+
class EvolutionTrigger(str, Enum):
53+
"""What initiated this evolution."""
54+
55+
ANALYSIS = "analysis" # Post-execution analysis suggestion
56+
TOOL_DEGRADATION = "tool_degradation" # Tool quality degradation detected
57+
METRIC_MONITOR = "metric_monitor" # Periodic skill health check
58+
59+
60+
@dataclass
61+
class EvolutionContext:
62+
"""Unified context for all evolution triggers.
63+
64+
For trigger 1 (ANALYSIS): source_task_id is set, recent_analyses may be
65+
just the single triggering analysis.
66+
For triggers 2/3: source_task_id is None, recent_analyses are loaded
67+
from the skill's historical records.
68+
"""
69+
70+
trigger: EvolutionTrigger
71+
suggestion: EvolutionSuggestion
72+
73+
# Parent skill context
74+
skill_records: List[SkillRecord] = field(default_factory=list)
75+
skill_contents: List[str] = field(default_factory=list)
76+
skill_dirs: List[Path] = field(default_factory=list)
77+
78+
# Task context
79+
source_task_id: Optional[str] = None
80+
recent_analyses: List[ExecutionAnalysis] = field(default_factory=list)
81+
82+
# Trigger-specific context
83+
tool_issue_summary: str = "" # For TOOL_DEGRADATION
84+
metric_summary: str = "" # For METRIC_MONITOR
85+
86+
# Available tools for agent loop (read_file, web_search, shell, MCP, etc.)
87+
available_tools: List["BaseTool"] = field(default_factory=list)

openspace/skill_engine/evolver.py

Lines changed: 6 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,18 @@
2222
import re
2323
import shutil
2424
import uuid
25-
from dataclasses import dataclass, field
26-
from enum import Enum
2725
from pathlib import Path
2826
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set
2927

3028
from openspace.prompts import SkillEnginePrompts
3129
from openspace.utils.logging import Logger
3230

31+
from .evolution.models import (
32+
_MAX_SKILL_NAME_LENGTH,
33+
EvolutionContext,
34+
EvolutionTrigger,
35+
_sanitize_skill_name,
36+
)
3337
from .patch import (
3438
SKILL_FILENAME,
3539
PatchType,
@@ -82,30 +86,6 @@
8286
EVOLUTION_FAILED = SkillEnginePrompts.EVOLUTION_FAILED
8387

8488
_SKILL_CONTENT_MAX_CHARS = 12_000 # Max chars of SKILL.md in evolution prompt
85-
_MAX_SKILL_NAME_LENGTH = 50 # Max chars for a skill name (directory name)
86-
87-
88-
def _sanitize_skill_name(name: str) -> str:
89-
"""Enforce naming rules for skill names (used as directory names).
90-
91-
- Lowercase, hyphens only (no underscores or special chars)
92-
- Truncate to ``_MAX_SKILL_NAME_LENGTH`` at a word boundary
93-
- Remove trailing hyphens
94-
"""
95-
# Normalize: lowercase, replace underscores and spaces with hyphens
96-
clean = re.sub(r"[^a-z0-9\-]", "-", name.lower().strip())
97-
# Collapse multiple hyphens
98-
clean = re.sub(r"-{2,}", "-", clean).strip("-")
99-
100-
if len(clean) <= _MAX_SKILL_NAME_LENGTH:
101-
return clean
102-
103-
# Truncate at a hyphen boundary to avoid cutting words
104-
truncated = clean[:_MAX_SKILL_NAME_LENGTH]
105-
last_hyphen = truncated.rfind("-")
106-
if last_hyphen > _MAX_SKILL_NAME_LENGTH // 2:
107-
truncated = truncated[:last_hyphen]
108-
return truncated.strip("-")
10989

11090

11191
_ANALYSIS_CONTEXT_MAX = 5 # Max recent analyses to include in prompt
@@ -123,44 +103,6 @@ def _sanitize_skill_name(name: str) -> str:
123103
_MIN_APPLIED_FOR_DERIVED = 0.25 # Relaxed from 0.3
124104

125105

126-
class EvolutionTrigger(str, Enum):
127-
"""What initiated this evolution."""
128-
129-
ANALYSIS = "analysis" # Post-execution analysis suggestion
130-
TOOL_DEGRADATION = "tool_degradation" # Tool quality degradation detected
131-
METRIC_MONITOR = "metric_monitor" # Periodic skill health check
132-
133-
134-
@dataclass
135-
class EvolutionContext:
136-
"""Unified context for all evolution triggers.
137-
138-
For trigger 1 (ANALYSIS): source_task_id is set, recent_analyses may be
139-
just the single triggering analysis.
140-
For triggers 2/3: source_task_id is None, recent_analyses are loaded
141-
from the skill's historical records.
142-
"""
143-
144-
trigger: EvolutionTrigger
145-
suggestion: EvolutionSuggestion
146-
147-
# Parent skill context
148-
skill_records: List[SkillRecord] = field(default_factory=list)
149-
skill_contents: List[str] = field(default_factory=list)
150-
skill_dirs: List[Path] = field(default_factory=list)
151-
152-
# Task context
153-
source_task_id: Optional[str] = None
154-
recent_analyses: List[ExecutionAnalysis] = field(default_factory=list)
155-
156-
# Trigger-specific context
157-
tool_issue_summary: str = "" # For TOOL_DEGRADATION
158-
metric_summary: str = "" # For METRIC_MONITOR
159-
160-
# Available tools for agent loop (read_file, web_search, shell, MCP, etc.)
161-
available_tools: List["BaseTool"] = field(default_factory=list)
162-
163-
164106
class SkillEvolver:
165107
"""Execute skill evolution actions.
166108

0 commit comments

Comments
 (0)