sky-dev 0.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sky/config/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """Configuration schemas and loading utilities."""
2
+
3
+ from .schema import (
4
+ ApprovalRuleConfig,
5
+ DexProjectConfig,
6
+ ModelAssignmentConfig,
7
+ ModelRoutingConfig,
8
+ load_config,
9
+ load_models_config,
10
+ )
11
+
12
+ __all__ = [
13
+ "ApprovalRuleConfig",
14
+ "ModelAssignmentConfig",
15
+ "ModelRoutingConfig",
16
+ "DexProjectConfig",
17
+ "load_config",
18
+ "load_models_config",
19
+ ]
sky/config/models.yaml ADDED
@@ -0,0 +1,39 @@
1
+ # Model routing config based on empirical findings
2
+ #
3
+ # planning: Requires high intelligence and context window for reasoning over large tasks.
4
+ # fast_loop: Used for iterative coding tasks. Lower reasoning effort but fast generation.
5
+ # routing: Small, fast model to decide which agent to delegate to.
6
+ # coder: Focused model for code generation and refactoring.
7
+ # tester: Focused model for unit testing and execution verification.
8
+ # reviewer: High intelligence model to review changes.
9
+
10
+ roles:
11
+ planning:
12
+ model_id: "openai/gpt-oss-120b"
13
+ temperature: 0.3
14
+ fast_loop:
15
+ model_id: "qwen/qwen3.6-27b"
16
+ reasoning_effort: "none"
17
+ temperature: 0.1
18
+ routing:
19
+ model_id: "openai/gpt-oss-20b"
20
+ temperature: 0.0
21
+ coder:
22
+ model_id: "qwen/qwen3.6-27b"
23
+ reasoning_effort: "none"
24
+ temperature: 0.1
25
+ tester:
26
+ model_id: "qwen/qwen3.6-27b"
27
+ reasoning_effort: "none"
28
+ temperature: 0.1
29
+ reviewer:
30
+ model_id: "openai/gpt-oss-120b"
31
+ temperature: 0.3
32
+
33
+ fallback:
34
+ model_id: "qwen/qwen3.6-27b"
35
+ reasoning_effort: "none"
36
+ temperature: 0.1
37
+
38
+ timeout_seconds: 30
39
+ parallel_tool_calls: true
sky/config/schema.py ADDED
@@ -0,0 +1,275 @@
1
+ """Configuration schemas and loading utilities."""
2
+
3
+ import fnmatch
4
+ import os
5
+ import re
6
+ import yaml
7
+ import logging
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Literal, Optional, Tuple
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class ApprovalRuleConfig(BaseModel):
17
+ """Configuration for tool call approval rules."""
18
+
19
+ model_config = ConfigDict(extra="forbid")
20
+
21
+ tool_name: Optional[str] = None
22
+ path_pattern: Optional[str] = None
23
+ max_lines_changed: Optional[int] = Field(default=None, ge=0)
24
+ command_prefix: Optional[str] = None
25
+ auto_approve: bool = True
26
+
27
+ def matches(self, tool_name: str, args: Dict[str, Any]) -> bool:
28
+ """Check if this rule matches a given tool call."""
29
+ if self.tool_name and self.tool_name != tool_name:
30
+ return False
31
+
32
+ if self.path_pattern:
33
+ # Assuming args might contain 'path' or 'file_path' indicating the file being modified
34
+ path = args.get("path") or args.get("file_path") or args.get("TargetFile") or args.get("AbsolutePath")
35
+ if not path or not fnmatch.fnmatch(str(path), self.path_pattern):
36
+ return False
37
+
38
+ if self.max_lines_changed is not None:
39
+ # Rough heuristic: count newlines in diff or content args
40
+ content = args.get("content") or args.get("diff") or args.get("CodeContent") or args.get("ReplacementContent")
41
+ if content is not None:
42
+ lines = len(re.findall(r"\n", str(content))) + 1
43
+ if lines > self.max_lines_changed:
44
+ return False
45
+ elif args.get("ReplacementChunks"):
46
+ lines = 0
47
+ for chunk in args.get("ReplacementChunks", []):
48
+ lines += len(re.findall(r"\n", str(chunk.get("ReplacementContent", "")))) + 1
49
+ if lines > self.max_lines_changed:
50
+ return False
51
+
52
+ if self.command_prefix:
53
+ # Assuming args might contain 'command' or 'CommandLine'
54
+ command = args.get("command") or args.get("CommandLine")
55
+ if not command or not str(command).startswith(self.command_prefix):
56
+ return False
57
+
58
+ return True
59
+
60
+
61
+ class ModelInfoConfig(BaseModel):
62
+ """Information about an available model."""
63
+ id: str
64
+ description: Optional[str] = None
65
+ context_window: Optional[int] = None
66
+ best_for: Optional[List[str]] = None
67
+ provider: Optional[str] = None
68
+
69
+
70
+ class ProviderConfig(BaseModel):
71
+ """Configuration for an LLM provider."""
72
+
73
+ model_config = ConfigDict(extra="forbid")
74
+
75
+ base_url: Optional[str] = None
76
+ timeout: int = 60
77
+ requires_api_key: bool = True
78
+ free_tier: bool = False
79
+ default_model: str
80
+ models: List[ModelInfoConfig] = Field(default_factory=list)
81
+
82
+
83
+ class ModelAssignmentConfig(BaseModel):
84
+ """Configuration for assigning a specific model to a role."""
85
+
86
+ model_config = ConfigDict(extra="forbid")
87
+
88
+ provider: str = Field(default="groq")
89
+ model_id: str
90
+ reasoning_effort: Optional[Literal["none", "low", "medium", "high"]] = None
91
+ temperature: Optional[float] = Field(default=None, ge=0.0, le=2.0)
92
+ max_tokens: Optional[int] = Field(default=None, ge=1)
93
+ description: Optional[str] = None
94
+
95
+
96
+ class ModelRoutingConfig(BaseModel):
97
+ """Configuration for routing tasks to different models."""
98
+
99
+ model_config = ConfigDict(extra="forbid")
100
+
101
+ providers: Dict[str, ProviderConfig] = Field(default_factory=dict)
102
+ roles: Dict[str, ModelAssignmentConfig]
103
+ fallback: Optional[ModelAssignmentConfig] = None
104
+ timeout_seconds: int = Field(default=30, ge=1)
105
+ parallel_tool_calls: bool = True
106
+
107
+
108
+ class DexProjectConfig(BaseModel):
109
+ """Main project configuration for SKY."""
110
+
111
+ model_config = ConfigDict(extra="forbid")
112
+
113
+ project_name: str = "sky-project"
114
+ approval_rules: List[ApprovalRuleConfig] = Field(default_factory=list)
115
+ max_retries: int = Field(default=3, ge=1, le=10)
116
+ docker_image: str = "python:3.11-slim"
117
+ docker_network: str = "none"
118
+ max_turn_limit: int = Field(default=20, ge=1)
119
+ context_chunk_size: int = Field(default=1000, ge=100)
120
+ context_top_k: int = Field(default=5, ge=1)
121
+ verbose: bool = False
122
+
123
+ # Memory Settings
124
+ memory_enabled: bool = Field(default=True, description="Enable project memory")
125
+ memory_provider: str = Field(default="fastembed", description="Embedding provider (fastembed or sentence-transformers)")
126
+ memory_chunk_size: int = Field(default=1000, description="Tokens per chunk")
127
+ memory_chunk_overlap: int = Field(default=50, description="Overlap between chunks")
128
+ memory_top_k: int = Field(default=5, description="Number of chunks to retrieve")
129
+ memory_model: str = Field(default="all-MiniLM-L6-v2", description="Embedding model")
130
+ memory_auto_index: bool = Field(default=False, description="Auto-index on first run")
131
+ memory_auto_inject_on_ask: bool = Field(default=False, description="Auto-inject context on ask command")
132
+ memory_exclude_patterns: List[str] = Field(
133
+ default=[".git", "node_modules", "venv", "__pycache__", ".sky", ".pytest_cache", "*.pyc"],
134
+ description="Excluded directories"
135
+ )
136
+
137
+ # Workflow Settings
138
+ workflow_max_retries: int = Field(default=3, description="Maximum test-and-reflect retries")
139
+ workflow_max_subtasks: int = Field(default=20, description="Max subtasks for decomposition")
140
+ workflow_parallel_limit: int = Field(default=3, description="Limit for parallel subagents")
141
+ workflow_enable_subagents: bool = Field(default=True, description="Enable subagents")
142
+ workflow_enable_test_and_reflect: bool = Field(default=True, description="Enable test and reflect")
143
+ workflow_checkpoint_interval: int = Field(default=10, description="Checkpoint interval")
144
+
145
+ # Security Settings
146
+ security_enabled: bool = Field(default=True, description="Enable security guardrails")
147
+ security_strict_mode: bool = Field(default=True, description="Strict mode for guardrails")
148
+ security_max_input_length: int = Field(default=10000, description="Max prompt length")
149
+ security_allowed_extensions: List[str] = Field(
150
+ default=[".py", ".md", ".txt", ".json", ".yaml", ".yml", ".toml", ".sh", ".js", ".ts", ".html", ".css"],
151
+ description="Allowed file extensions for writing"
152
+ )
153
+ security_dangerous_commands: List[str] = Field(
154
+ default=["rm -rf", "dd if=", "mkfs", "format", "shred"],
155
+ description="Dangerous commands to block"
156
+ )
157
+
158
+ @field_validator("memory_exclude_patterns")
159
+ @classmethod
160
+ def validate_exclude_patterns(cls, v: List[str]) -> List[str]:
161
+ return [p for p in v if p.strip()]
162
+
163
+
164
+ def _get_config_paths() -> Tuple[Path, Path]:
165
+ """Get the paths to global and local config files."""
166
+ global_path = Path.home() / ".sky" / "config.yaml"
167
+ local_path = Path("sky.yaml")
168
+ return global_path, local_path
169
+
170
+
171
+ def load_config() -> DexProjectConfig:
172
+ """Load and merge global and project configurations. Project config takes precedence."""
173
+ global_path, local_path = _get_config_paths()
174
+ config_data: Dict[str, Any] = {}
175
+
176
+ if global_path.exists():
177
+ try:
178
+ with open(global_path, "r", encoding="utf-8") as f:
179
+ global_data = yaml.safe_load(f)
180
+ if global_data:
181
+ config_data.update(global_data)
182
+ except Exception as e:
183
+ logger.warning(f"Failed to load global config from {global_path}: {e}")
184
+
185
+ if local_path.exists():
186
+ try:
187
+ with open(local_path, "r", encoding="utf-8") as f:
188
+ local_data = yaml.safe_load(f)
189
+ if local_data:
190
+ config_data.update(local_data)
191
+ except Exception as e:
192
+ logger.warning(f"Failed to load local config from {local_path}: {e}")
193
+
194
+ # Flatten memory settings if present
195
+ if "memory" in config_data and isinstance(config_data["memory"], dict):
196
+ mem_settings = config_data.pop("memory")
197
+ for k, v in mem_settings.items():
198
+ config_data[f"memory_{k}"] = v
199
+
200
+ # Flatten workflow settings if present
201
+ if "workflow" in config_data and isinstance(config_data["workflow"], dict):
202
+ wf_settings = config_data.pop("workflow")
203
+ for k, v in wf_settings.items():
204
+ config_data[f"workflow_{k}"] = v
205
+
206
+ # Flatten security settings if present
207
+ if "security" in config_data and isinstance(config_data["security"], dict):
208
+ sec_settings = config_data.pop("security")
209
+ for k, v in sec_settings.items():
210
+ config_data[f"security_{k}"] = v
211
+
212
+ config = DexProjectConfig(**config_data)
213
+ if os.getenv("SKY_VERBOSE", "").lower() == "true":
214
+ config.verbose = True
215
+
216
+ return config
217
+
218
+
219
+ def load_models_config() -> ModelRoutingConfig:
220
+ """Load model routing configuration from local or global models.yaml."""
221
+ local_path = Path("models.yaml")
222
+ global_path = Path.home() / ".sky" / "models.yaml"
223
+
224
+ target_path = local_path if local_path.exists() else global_path
225
+
226
+ if target_path.exists():
227
+ try:
228
+ with open(target_path, "r", encoding="utf-8") as f:
229
+ data = yaml.safe_load(f)
230
+ if data:
231
+ return ModelRoutingConfig(**data)
232
+ except Exception as e:
233
+ logger.warning(f"Failed to load models config from {target_path}: {e}")
234
+
235
+ # Fallback default configuration
236
+ return ModelRoutingConfig(
237
+ providers={
238
+ "groq": ProviderConfig(
239
+ base_url="https://api.groq.com/openai/v1",
240
+ timeout=30,
241
+ requires_api_key=True,
242
+ free_tier=True,
243
+ default_model="openai/gpt-oss-120b",
244
+ models=[
245
+ ModelInfoConfig(id="groq/compound-mini", description="Ultra-fast routing (0.1s)", context_window=8192, best_for=["routing", "classification"], provider="groq"),
246
+ ModelInfoConfig(id="openai/gpt-oss-120b", description="Best general conversation", context_window=128000, best_for=["general", "chat"], provider="groq"),
247
+ ModelInfoConfig(id="meta-models/Muse-Glimmer-30B", description="Dedicated reasoning & planning", context_window=32768, best_for=["planning", "reviewing"], provider="groq"),
248
+ ModelInfoConfig(id="qwen/qwen3.6-27b", description="Best-in-class tool calling", context_window=32768, best_for=["tool_calling", "execution"], provider="groq")
249
+ ]
250
+ ),
251
+ "nim": ProviderConfig(
252
+ base_url="https://integrate.api.nvidia.com/v1",
253
+ timeout=60,
254
+ requires_api_key=True,
255
+ free_tier=True,
256
+ default_model="mistralai/devstral-2",
257
+ models=[
258
+ ModelInfoConfig(id="mistralai/devstral-2", description="Purpose-built for agentic coding", context_window=131072, best_for=["coding", "tool_use"], provider="nim"),
259
+ ModelInfoConfig(id="nvidia/llama-3.1-nemotron-70b-instruct", description="Reliable backup model", context_window=131072, best_for=["fallback"], provider="nim")
260
+ ]
261
+ )
262
+ },
263
+ roles={
264
+ "general": ModelAssignmentConfig(provider="groq", model_id="openai/gpt-oss-120b", temperature=0.7),
265
+ "planning": ModelAssignmentConfig(provider="groq", model_id="meta-models/Muse-Glimmer-30B", temperature=0.3),
266
+ "reviewer": ModelAssignmentConfig(provider="groq", model_id="meta-models/Muse-Glimmer-30B", temperature=0.3),
267
+ "routing": ModelAssignmentConfig(provider="groq", model_id="groq/compound-mini", temperature=0.0),
268
+ "fast_loop": ModelAssignmentConfig(provider="groq", model_id="qwen/qwen3.6-27b", temperature=0.1),
269
+ "coder": ModelAssignmentConfig(provider="nim", model_id="mistralai/devstral-2", temperature=0.1),
270
+ "tester": ModelAssignmentConfig(provider="nim", model_id="mistralai/devstral-2", temperature=0.1),
271
+ },
272
+ fallback=ModelAssignmentConfig(provider="nim", model_id="nvidia/llama-3.1-nemotron-70b-instruct", temperature=0.1),
273
+ timeout_seconds=30,
274
+ parallel_tool_calls=True,
275
+ )
sky/core/__init__.py ADDED
@@ -0,0 +1,48 @@
1
+ """SKY Core logic including Fast Loop, Model Router, and Approval Gate."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from .approval import ApprovalGate
7
+ from .fast_loop import FastLoopEngine
8
+ from .router import ModelRouter
9
+ from .workflow import WorkflowEngine, WorkflowState, WorkflowStatus
10
+ from .subagent import SubagentRunner, SubagentRole, SubagentConfig
11
+
12
+ def get_approval_gate(*args, **kwargs):
13
+ from .approval import ApprovalGate
14
+ return ApprovalGate(*args, **kwargs)
15
+
16
+ def get_fast_loop_engine(*args, **kwargs):
17
+ from .fast_loop import FastLoopEngine
18
+ return FastLoopEngine(*args, **kwargs)
19
+
20
+ def get_model_router(*args, **kwargs):
21
+ from .router import ModelRouter
22
+ return ModelRouter(*args, **kwargs)
23
+
24
+ def get_workflow_engine(*args, **kwargs):
25
+ from .workflow import WorkflowEngine
26
+ return WorkflowEngine(*args, **kwargs)
27
+
28
+ def get_subagent_runner(*args, **kwargs):
29
+ from .subagent import SubagentRunner
30
+ return SubagentRunner(*args, **kwargs)
31
+
32
+ def get_mode_prompt(*args, **kwargs):
33
+ from .mode_prompts import get_mode_prompt as _get_mode_prompt
34
+ return _get_mode_prompt(*args, **kwargs)
35
+
36
+ __all__ = [
37
+ "ApprovalGate",
38
+ "FastLoopEngine",
39
+ "ModelRouter",
40
+ "get_mode_prompt",
41
+ "WorkflowEngine",
42
+ "WorkflowState",
43
+ "WorkflowStatus",
44
+ "SubagentRunner",
45
+ "SubagentRole",
46
+ "SubagentConfig",
47
+ "ROLE_CONFIGS",
48
+ ]
sky/core/approval.py ADDED
@@ -0,0 +1,92 @@
1
+ """Human Approval Gate for intercepting and approving destructive tool calls."""
2
+
3
+ import json
4
+ from typing import Any, Dict, Optional, Tuple
5
+
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.prompt import Prompt
9
+ from rich.syntax import Syntax
10
+ from rich.table import Table
11
+
12
+ from sky.config import DexProjectConfig
13
+ from sky.storage import DatabaseManager
14
+
15
+ console = Console()
16
+
17
+ class ApprovalGate:
18
+ """Manages manual and automatic approvals for tool executions."""
19
+
20
+ def __init__(self, config: DexProjectConfig, db: DatabaseManager, session_id: str):
21
+ self.config = config
22
+ self.db = db
23
+ self.session_id = session_id
24
+
25
+ def check_auto_approve(self, tool_name: str, args: Dict[str, Any]) -> Optional[bool]:
26
+ """Check if a tool call matches any auto-approve rules in the config."""
27
+ for rule in self.config.approval_rules:
28
+ if not rule.auto_approve:
29
+ continue
30
+ if rule.matches(tool_name, args):
31
+ return True
32
+ return False
33
+
34
+ def render_diff(self, content: str, filepath: str) -> Syntax:
35
+ """Render unified diff with syntax highlighting."""
36
+ return Syntax(content, "diff", theme="monokai", line_numbers=True)
37
+
38
+ def render_command(self, command: str) -> Syntax:
39
+ """Render bash command with syntax highlighting."""
40
+ return Syntax(command, "bash", theme="monokai", line_numbers=False)
41
+
42
+ def request_approval(self, tool_name: str, args: Dict[str, Any]) -> Tuple[bool, Optional[str], Optional[Dict[str, Any]]]:
43
+ """Request manual approval via rich interactive prompt."""
44
+ console.print("\n")
45
+ table = Table(title="[bold yellow]Tool Execution Requires Approval[/bold yellow]", show_header=False)
46
+ table.add_column("Property", style="cyan", justify="right")
47
+ table.add_column("Value", style="white")
48
+
49
+ table.add_row("Tool", f"[bold]{tool_name}[/bold]")
50
+
51
+ if tool_name == "edit_file" and "diff" in args:
52
+ table.add_row("File", args.get("path", "unknown"))
53
+ console.print(table)
54
+ console.print(Panel(self.render_diff(args["diff"], args.get("path", "")), title="Diff Preview"))
55
+ elif tool_name == "bash" and "command" in args:
56
+ table.add_row("Command", "")
57
+ console.print(table)
58
+ console.print(Panel(self.render_command(args["command"]), title="Bash Command"))
59
+ else:
60
+ args_str = json.dumps(args, indent=2)
61
+ table.add_row("Arguments", args_str)
62
+ console.print(table)
63
+
64
+ while True:
65
+ response = Prompt.ask(
66
+ "[bold yellow]Approve execution?[/bold yellow]",
67
+ choices=["y", "n", "e"],
68
+ default="y"
69
+ )
70
+
71
+ if response == "y":
72
+ return True, "User approved", args
73
+ elif response == "n":
74
+ return False, "User rejected", None
75
+ elif response == "e":
76
+ console.print("[dim]Editing parameters is not fully supported in this stub. Rejecting.[/dim]")
77
+ return False, "User aborted to edit", None
78
+
79
+ def process(self, tool_name: str, args: Dict[str, Any]) -> Tuple[bool, Optional[str], Optional[Dict[str, Any]]]:
80
+ """Main entry point to process a tool call through the approval gate."""
81
+ # Check auto-approve first
82
+ if self.check_auto_approve(tool_name, args):
83
+ self.db.log_tool_call(self.session_id, tool_name, args, "destructive", "auto_approved")
84
+ return True, "Auto-approved by rules", args
85
+
86
+ # Fallback to manual approval
87
+ approved, reason, final_args = self.request_approval(tool_name, args)
88
+
89
+ decision = "approved" if approved else "rejected"
90
+ self.db.log_tool_call(self.session_id, tool_name, final_args or args, "destructive", decision, approved_by="user")
91
+
92
+ return approved, reason, final_args
sky/core/benchmark.py ADDED
@@ -0,0 +1,39 @@
1
+ """Performance benchmarking utilities."""
2
+
3
+ import time
4
+ from pathlib import Path
5
+ from typing import Dict, List
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class Benchmark:
12
+ """Simple performance benchmark."""
13
+
14
+ @staticmethod
15
+ def measure_startup() -> Dict[str, float]:
16
+ """Measure CLI startup time."""
17
+ start = time.perf_counter()
18
+
19
+ # Import core modules
20
+ import sky.core # noqa
21
+ import sky.memory # noqa
22
+
23
+ end = time.perf_counter()
24
+ return {"startup_ms": (end - start) * 1000}
25
+
26
+ @staticmethod
27
+ def measure_embedding() -> Dict[str, float]:
28
+ """Measure embedding generation time."""
29
+ from sky.memory.vectorstore import get_vector_store
30
+ import tempfile
31
+
32
+ with tempfile.TemporaryDirectory() as tmpdir:
33
+ store = get_vector_store(Path(tmpdir))
34
+
35
+ start = time.perf_counter()
36
+ embedding = store._get_embedding("test query")
37
+ end = time.perf_counter()
38
+
39
+ return {"embedding_ms": (end - start) * 1000, "dimension": len(embedding)}