ouroboros-ai 0.1.0__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.
Potentially problematic release.
This version of ouroboros-ai might be problematic. Click here for more details.
- ouroboros/__init__.py +15 -0
- ouroboros/__main__.py +9 -0
- ouroboros/bigbang/__init__.py +39 -0
- ouroboros/bigbang/ambiguity.py +464 -0
- ouroboros/bigbang/interview.py +530 -0
- ouroboros/bigbang/seed_generator.py +610 -0
- ouroboros/cli/__init__.py +9 -0
- ouroboros/cli/commands/__init__.py +7 -0
- ouroboros/cli/commands/config.py +79 -0
- ouroboros/cli/commands/init.py +425 -0
- ouroboros/cli/commands/run.py +201 -0
- ouroboros/cli/commands/status.py +85 -0
- ouroboros/cli/formatters/__init__.py +31 -0
- ouroboros/cli/formatters/panels.py +157 -0
- ouroboros/cli/formatters/progress.py +112 -0
- ouroboros/cli/formatters/tables.py +166 -0
- ouroboros/cli/main.py +60 -0
- ouroboros/config/__init__.py +81 -0
- ouroboros/config/loader.py +292 -0
- ouroboros/config/models.py +332 -0
- ouroboros/core/__init__.py +62 -0
- ouroboros/core/ac_tree.py +401 -0
- ouroboros/core/context.py +472 -0
- ouroboros/core/errors.py +246 -0
- ouroboros/core/seed.py +212 -0
- ouroboros/core/types.py +205 -0
- ouroboros/evaluation/__init__.py +110 -0
- ouroboros/evaluation/consensus.py +350 -0
- ouroboros/evaluation/mechanical.py +351 -0
- ouroboros/evaluation/models.py +235 -0
- ouroboros/evaluation/pipeline.py +286 -0
- ouroboros/evaluation/semantic.py +302 -0
- ouroboros/evaluation/trigger.py +278 -0
- ouroboros/events/__init__.py +5 -0
- ouroboros/events/base.py +80 -0
- ouroboros/events/decomposition.py +153 -0
- ouroboros/events/evaluation.py +248 -0
- ouroboros/execution/__init__.py +44 -0
- ouroboros/execution/atomicity.py +451 -0
- ouroboros/execution/decomposition.py +481 -0
- ouroboros/execution/double_diamond.py +1386 -0
- ouroboros/execution/subagent.py +275 -0
- ouroboros/observability/__init__.py +63 -0
- ouroboros/observability/drift.py +383 -0
- ouroboros/observability/logging.py +504 -0
- ouroboros/observability/retrospective.py +338 -0
- ouroboros/orchestrator/__init__.py +78 -0
- ouroboros/orchestrator/adapter.py +391 -0
- ouroboros/orchestrator/events.py +278 -0
- ouroboros/orchestrator/runner.py +597 -0
- ouroboros/orchestrator/session.py +486 -0
- ouroboros/persistence/__init__.py +23 -0
- ouroboros/persistence/checkpoint.py +511 -0
- ouroboros/persistence/event_store.py +183 -0
- ouroboros/persistence/migrations/__init__.py +1 -0
- ouroboros/persistence/migrations/runner.py +100 -0
- ouroboros/persistence/migrations/scripts/001_initial.sql +20 -0
- ouroboros/persistence/schema.py +56 -0
- ouroboros/persistence/uow.py +230 -0
- ouroboros/providers/__init__.py +28 -0
- ouroboros/providers/base.py +133 -0
- ouroboros/providers/claude_code_adapter.py +212 -0
- ouroboros/providers/litellm_adapter.py +316 -0
- ouroboros/py.typed +0 -0
- ouroboros/resilience/__init__.py +67 -0
- ouroboros/resilience/lateral.py +595 -0
- ouroboros/resilience/stagnation.py +727 -0
- ouroboros/routing/__init__.py +60 -0
- ouroboros/routing/complexity.py +272 -0
- ouroboros/routing/downgrade.py +664 -0
- ouroboros/routing/escalation.py +340 -0
- ouroboros/routing/router.py +204 -0
- ouroboros/routing/tiers.py +247 -0
- ouroboros/secondary/__init__.py +40 -0
- ouroboros/secondary/scheduler.py +467 -0
- ouroboros/secondary/todo_registry.py +483 -0
- ouroboros_ai-0.1.0.dist-info/METADATA +607 -0
- ouroboros_ai-0.1.0.dist-info/RECORD +81 -0
- ouroboros_ai-0.1.0.dist-info/WHEEL +4 -0
- ouroboros_ai-0.1.0.dist-info/entry_points.txt +2 -0
- ouroboros_ai-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Configuration loading and management for Ouroboros.
|
|
2
|
+
|
|
3
|
+
This module provides functions for loading, creating, and validating
|
|
4
|
+
Ouroboros configuration files.
|
|
5
|
+
|
|
6
|
+
Functions:
|
|
7
|
+
load_config: Load configuration from ~/.ouroboros/config.yaml
|
|
8
|
+
load_credentials: Load credentials from ~/.ouroboros/credentials.yaml
|
|
9
|
+
create_default_config: Create default configuration files
|
|
10
|
+
ensure_config_dir: Ensure ~/.ouroboros/ directory exists
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import stat
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
from ouroboros.config.models import (
|
|
22
|
+
CredentialsConfig,
|
|
23
|
+
OuroborosConfig,
|
|
24
|
+
get_config_dir,
|
|
25
|
+
get_default_config,
|
|
26
|
+
get_default_credentials,
|
|
27
|
+
)
|
|
28
|
+
from ouroboros.core.errors import ConfigError
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def ensure_config_dir() -> Path:
|
|
32
|
+
"""Ensure the configuration directory exists.
|
|
33
|
+
|
|
34
|
+
Creates ~/.ouroboros/ directory and subdirectories if they don't exist.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Path to the configuration directory.
|
|
38
|
+
"""
|
|
39
|
+
config_dir = get_config_dir()
|
|
40
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
# Create subdirectories
|
|
43
|
+
(config_dir / "data").mkdir(exist_ok=True)
|
|
44
|
+
(config_dir / "logs").mkdir(exist_ok=True)
|
|
45
|
+
|
|
46
|
+
return config_dir
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _set_secure_permissions(file_path: Path) -> None:
|
|
50
|
+
"""Set secure permissions (chmod 600) on a file.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
file_path: Path to the file to secure.
|
|
54
|
+
"""
|
|
55
|
+
# Set permissions to owner read/write only (0o600)
|
|
56
|
+
os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _model_to_yaml_dict(model: OuroborosConfig | CredentialsConfig) -> dict[str, Any]:
|
|
60
|
+
"""Convert a Pydantic model to a YAML-serializable dict.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
model: The Pydantic model to convert.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
A dict suitable for YAML serialization.
|
|
67
|
+
"""
|
|
68
|
+
return model.model_dump(mode="json")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def create_default_config(
|
|
72
|
+
config_dir: Path | None = None,
|
|
73
|
+
*,
|
|
74
|
+
overwrite: bool = False,
|
|
75
|
+
) -> tuple[Path, Path]:
|
|
76
|
+
"""Create default configuration files.
|
|
77
|
+
|
|
78
|
+
Creates config.yaml and credentials.yaml with default templates
|
|
79
|
+
in the specified directory. credentials.yaml is created with
|
|
80
|
+
chmod 600 permissions for security.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
config_dir: Directory to create files in. Defaults to ~/.ouroboros/
|
|
84
|
+
overwrite: If True, overwrite existing files. Defaults to False.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Tuple of (config_path, credentials_path).
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ConfigError: If files exist and overwrite=False.
|
|
91
|
+
"""
|
|
92
|
+
if config_dir is None:
|
|
93
|
+
config_dir = ensure_config_dir()
|
|
94
|
+
else:
|
|
95
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
(config_dir / "data").mkdir(exist_ok=True)
|
|
97
|
+
(config_dir / "logs").mkdir(exist_ok=True)
|
|
98
|
+
|
|
99
|
+
config_path = config_dir / "config.yaml"
|
|
100
|
+
credentials_path = config_dir / "credentials.yaml"
|
|
101
|
+
|
|
102
|
+
# Check if files exist
|
|
103
|
+
if not overwrite:
|
|
104
|
+
if config_path.exists():
|
|
105
|
+
raise ConfigError(
|
|
106
|
+
f"Configuration file already exists: {config_path}",
|
|
107
|
+
config_file=str(config_path),
|
|
108
|
+
)
|
|
109
|
+
if credentials_path.exists():
|
|
110
|
+
raise ConfigError(
|
|
111
|
+
f"Credentials file already exists: {credentials_path}",
|
|
112
|
+
config_file=str(credentials_path),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Create config.yaml
|
|
116
|
+
default_config = get_default_config()
|
|
117
|
+
config_dict = _model_to_yaml_dict(default_config)
|
|
118
|
+
with config_path.open("w") as f:
|
|
119
|
+
yaml.dump(
|
|
120
|
+
config_dict,
|
|
121
|
+
f,
|
|
122
|
+
default_flow_style=False,
|
|
123
|
+
sort_keys=False,
|
|
124
|
+
allow_unicode=True,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Create credentials.yaml with secure permissions
|
|
128
|
+
default_credentials = get_default_credentials()
|
|
129
|
+
credentials_dict = _model_to_yaml_dict(default_credentials)
|
|
130
|
+
with credentials_path.open("w") as f:
|
|
131
|
+
yaml.dump(
|
|
132
|
+
credentials_dict,
|
|
133
|
+
f,
|
|
134
|
+
default_flow_style=False,
|
|
135
|
+
sort_keys=False,
|
|
136
|
+
allow_unicode=True,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Set chmod 600 on credentials file
|
|
140
|
+
_set_secure_permissions(credentials_path)
|
|
141
|
+
|
|
142
|
+
return config_path, credentials_path
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def load_config(config_path: Path | None = None) -> OuroborosConfig:
|
|
146
|
+
"""Load configuration from YAML file.
|
|
147
|
+
|
|
148
|
+
Loads and validates configuration from the specified path or
|
|
149
|
+
the default ~/.ouroboros/config.yaml.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
config_path: Path to config file. Defaults to ~/.ouroboros/config.yaml.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Validated OuroborosConfig instance.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
ConfigError: If file doesn't exist, is malformed, or fails validation.
|
|
159
|
+
"""
|
|
160
|
+
if config_path is None:
|
|
161
|
+
config_path = get_config_dir() / "config.yaml"
|
|
162
|
+
|
|
163
|
+
if not config_path.exists():
|
|
164
|
+
raise ConfigError(
|
|
165
|
+
f"Configuration file not found: {config_path}. "
|
|
166
|
+
"Run `ouroboros config init` to create default configuration.",
|
|
167
|
+
config_file=str(config_path),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
with config_path.open() as f:
|
|
172
|
+
config_dict = yaml.safe_load(f)
|
|
173
|
+
except yaml.YAMLError as e:
|
|
174
|
+
raise ConfigError(
|
|
175
|
+
f"Failed to parse configuration file: {e}",
|
|
176
|
+
config_file=str(config_path),
|
|
177
|
+
details={"yaml_error": str(e)},
|
|
178
|
+
) from e
|
|
179
|
+
|
|
180
|
+
if config_dict is None:
|
|
181
|
+
config_dict = {}
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
return OuroborosConfig.model_validate(config_dict)
|
|
185
|
+
except PydanticValidationError as e:
|
|
186
|
+
# Format validation errors for clarity
|
|
187
|
+
error_messages = []
|
|
188
|
+
for error in e.errors():
|
|
189
|
+
loc = ".".join(str(x) for x in error["loc"])
|
|
190
|
+
msg = error["msg"]
|
|
191
|
+
error_messages.append(f" - {loc}: {msg}")
|
|
192
|
+
|
|
193
|
+
raise ConfigError(
|
|
194
|
+
"Configuration validation failed:\n" + "\n".join(error_messages),
|
|
195
|
+
config_file=str(config_path),
|
|
196
|
+
details={"validation_errors": e.errors()},
|
|
197
|
+
) from e
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def load_credentials(credentials_path: Path | None = None) -> CredentialsConfig:
|
|
201
|
+
"""Load credentials from YAML file.
|
|
202
|
+
|
|
203
|
+
Loads and validates credentials from the specified path or
|
|
204
|
+
the default ~/.ouroboros/credentials.yaml.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
credentials_path: Path to credentials file.
|
|
208
|
+
Defaults to ~/.ouroboros/credentials.yaml.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Validated CredentialsConfig instance.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
ConfigError: If file doesn't exist, is malformed, or fails validation.
|
|
215
|
+
"""
|
|
216
|
+
if credentials_path is None:
|
|
217
|
+
credentials_path = get_config_dir() / "credentials.yaml"
|
|
218
|
+
|
|
219
|
+
if not credentials_path.exists():
|
|
220
|
+
raise ConfigError(
|
|
221
|
+
f"Credentials file not found: {credentials_path}. "
|
|
222
|
+
"Run `ouroboros config init` to create default configuration.",
|
|
223
|
+
config_file=str(credentials_path),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Check file permissions (warn if too permissive)
|
|
227
|
+
file_mode = credentials_path.stat().st_mode
|
|
228
|
+
if file_mode & (stat.S_IRGRP | stat.S_IROTH):
|
|
229
|
+
# File is readable by group or others - this is a security warning
|
|
230
|
+
# We don't raise an error, but this could be logged
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
with credentials_path.open() as f:
|
|
235
|
+
credentials_dict = yaml.safe_load(f)
|
|
236
|
+
except yaml.YAMLError as e:
|
|
237
|
+
raise ConfigError(
|
|
238
|
+
f"Failed to parse credentials file: {e}",
|
|
239
|
+
config_file=str(credentials_path),
|
|
240
|
+
details={"yaml_error": str(e)},
|
|
241
|
+
) from e
|
|
242
|
+
|
|
243
|
+
if credentials_dict is None:
|
|
244
|
+
credentials_dict = {}
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
return CredentialsConfig.model_validate(credentials_dict)
|
|
248
|
+
except PydanticValidationError as e:
|
|
249
|
+
error_messages = []
|
|
250
|
+
for error in e.errors():
|
|
251
|
+
loc = ".".join(str(x) for x in error["loc"])
|
|
252
|
+
msg = error["msg"]
|
|
253
|
+
error_messages.append(f" - {loc}: {msg}")
|
|
254
|
+
|
|
255
|
+
raise ConfigError(
|
|
256
|
+
"Credentials validation failed:\n" + "\n".join(error_messages),
|
|
257
|
+
config_file=str(credentials_path),
|
|
258
|
+
details={"validation_errors": e.errors()},
|
|
259
|
+
) from e
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def config_exists() -> bool:
|
|
263
|
+
"""Check if configuration files exist.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
True if both config.yaml and credentials.yaml exist.
|
|
267
|
+
"""
|
|
268
|
+
config_dir = get_config_dir()
|
|
269
|
+
return (config_dir / "config.yaml").exists() and (
|
|
270
|
+
config_dir / "credentials.yaml"
|
|
271
|
+
).exists()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def credentials_file_secure(credentials_path: Path | None = None) -> bool:
|
|
275
|
+
"""Check if credentials file has secure permissions.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
credentials_path: Path to credentials file.
|
|
279
|
+
Defaults to ~/.ouroboros/credentials.yaml.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
True if file has chmod 600 (owner read/write only).
|
|
283
|
+
"""
|
|
284
|
+
if credentials_path is None:
|
|
285
|
+
credentials_path = get_config_dir() / "credentials.yaml"
|
|
286
|
+
|
|
287
|
+
if not credentials_path.exists():
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
file_mode = credentials_path.stat().st_mode
|
|
291
|
+
# Check that only owner has read/write permissions
|
|
292
|
+
return (file_mode & 0o777) == 0o600
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""Pydantic models for Ouroboros configuration.
|
|
2
|
+
|
|
3
|
+
This module defines the configuration schema using Pydantic v2.
|
|
4
|
+
All configuration validation happens through these models.
|
|
5
|
+
|
|
6
|
+
Classes:
|
|
7
|
+
ModelConfig: Single LLM model configuration
|
|
8
|
+
TierConfig: Tier configuration with cost factor and models
|
|
9
|
+
ProviderCredentials: API credentials for a single provider
|
|
10
|
+
CredentialsConfig: All provider credentials
|
|
11
|
+
EconomicsConfig: Economic model with tier definitions
|
|
12
|
+
ClarificationConfig: Phase 0 configuration
|
|
13
|
+
ExecutionConfig: Phase 2 configuration
|
|
14
|
+
ResilienceConfig: Phase 3 configuration
|
|
15
|
+
EvaluationConfig: Phase 4 configuration
|
|
16
|
+
ConsensusConfig: Phase 5 configuration
|
|
17
|
+
PersistenceConfig: Storage configuration
|
|
18
|
+
LoggingConfig: Logging configuration
|
|
19
|
+
OuroborosConfig: Top-level configuration combining all sections
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Literal
|
|
24
|
+
|
|
25
|
+
from pydantic import BaseModel, Field, field_validator
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ModelConfig(BaseModel, frozen=True):
|
|
29
|
+
"""Configuration for a single LLM model.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
provider: Provider name (openai, anthropic, google, openrouter)
|
|
33
|
+
model: Model identifier string
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
provider: str
|
|
37
|
+
model: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TierConfig(BaseModel, frozen=True):
|
|
41
|
+
"""Configuration for a cost tier.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
cost_factor: Relative cost multiplier (1 for frugal, 10 for standard, etc.)
|
|
45
|
+
intelligence_range: Tuple of min/max intelligence score
|
|
46
|
+
models: List of models available in this tier
|
|
47
|
+
use_cases: List of use cases this tier is suited for
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
cost_factor: int = Field(ge=1)
|
|
51
|
+
intelligence_range: tuple[int, int] = Field(default=(1, 20))
|
|
52
|
+
models: list[ModelConfig] = Field(default_factory=list)
|
|
53
|
+
use_cases: list[str] = Field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
@field_validator("intelligence_range")
|
|
56
|
+
@classmethod
|
|
57
|
+
def validate_intelligence_range(cls, v: tuple[int, int]) -> tuple[int, int]:
|
|
58
|
+
"""Validate that min <= max in intelligence range."""
|
|
59
|
+
if v[0] > v[1]:
|
|
60
|
+
msg = f"Intelligence range min ({v[0]}) must be <= max ({v[1]})"
|
|
61
|
+
raise ValueError(msg)
|
|
62
|
+
return v
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ProviderCredentials(BaseModel, frozen=True):
|
|
66
|
+
"""API credentials for a single provider.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
api_key: The API key for the provider
|
|
70
|
+
base_url: Optional custom base URL for the provider
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
api_key: str = Field(min_length=1)
|
|
74
|
+
base_url: str | None = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class CredentialsConfig(BaseModel, frozen=True):
|
|
78
|
+
"""Configuration for all provider credentials.
|
|
79
|
+
|
|
80
|
+
Attributes:
|
|
81
|
+
providers: Dict mapping provider name to credentials
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
providers: dict[str, ProviderCredentials] = Field(default_factory=dict)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class EconomicsConfig(BaseModel, frozen=True):
|
|
88
|
+
"""Economic model configuration.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
default_tier: Default tier to use for tasks
|
|
92
|
+
tiers: Dict mapping tier name to tier configuration
|
|
93
|
+
escalation_threshold: Number of failures before upgrading tier
|
|
94
|
+
downgrade_success_streak: Successes needed to downgrade tier
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
default_tier: Literal["frugal", "standard", "frontier"] = "frugal"
|
|
98
|
+
tiers: dict[str, TierConfig] = Field(default_factory=dict)
|
|
99
|
+
escalation_threshold: int = Field(default=2, ge=1)
|
|
100
|
+
downgrade_success_streak: int = Field(default=5, ge=1)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ClarificationConfig(BaseModel, frozen=True):
|
|
104
|
+
"""Phase 0 (Big Bang) configuration.
|
|
105
|
+
|
|
106
|
+
Attributes:
|
|
107
|
+
ambiguity_threshold: Maximum ambiguity score to proceed
|
|
108
|
+
max_interview_rounds: Maximum number of clarification rounds
|
|
109
|
+
model_tier: Tier to use for clarification
|
|
110
|
+
default_model: Default LLM model for interview and seed generation
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
ambiguity_threshold: float = Field(default=0.2, ge=0.0, le=1.0)
|
|
114
|
+
max_interview_rounds: int = Field(default=10, ge=1)
|
|
115
|
+
model_tier: Literal["frugal", "standard", "frontier"] = "standard"
|
|
116
|
+
default_model: str = "openrouter/google/gemini-2.0-flash-001"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ExecutionConfig(BaseModel, frozen=True):
|
|
120
|
+
"""Phase 2 (Execution) configuration.
|
|
121
|
+
|
|
122
|
+
Attributes:
|
|
123
|
+
max_iterations_per_ac: Maximum iterations per acceptance criteria
|
|
124
|
+
retrospective_interval: Iterations between retrospectives
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
max_iterations_per_ac: int = Field(default=10, ge=1)
|
|
128
|
+
retrospective_interval: int = Field(default=3, ge=1)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ResilienceConfig(BaseModel, frozen=True):
|
|
132
|
+
"""Phase 3 (Resilience) configuration.
|
|
133
|
+
|
|
134
|
+
Attributes:
|
|
135
|
+
stagnation_enabled: Whether stagnation detection is enabled
|
|
136
|
+
lateral_thinking_enabled: Whether lateral thinking is enabled
|
|
137
|
+
lateral_model_tier: Tier for lateral thinking
|
|
138
|
+
lateral_temperature: Temperature for lateral thinking LLM calls
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
stagnation_enabled: bool = True
|
|
142
|
+
lateral_thinking_enabled: bool = True
|
|
143
|
+
lateral_model_tier: Literal["frugal", "standard", "frontier"] = "frontier"
|
|
144
|
+
lateral_temperature: float = Field(default=0.8, ge=0.0, le=2.0)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class EvaluationConfig(BaseModel, frozen=True):
|
|
148
|
+
"""Phase 4 (Evaluation) configuration.
|
|
149
|
+
|
|
150
|
+
Attributes:
|
|
151
|
+
stage1_enabled: Whether mechanical checks are enabled
|
|
152
|
+
stage2_enabled: Whether semantic evaluation is enabled
|
|
153
|
+
stage3_enabled: Whether consensus evaluation is enabled
|
|
154
|
+
satisfaction_threshold: Minimum satisfaction score
|
|
155
|
+
uncertainty_threshold: Threshold above which to trigger consensus
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
stage1_enabled: bool = True
|
|
159
|
+
stage2_enabled: bool = True
|
|
160
|
+
stage3_enabled: bool = True
|
|
161
|
+
satisfaction_threshold: float = Field(default=0.8, ge=0.0, le=1.0)
|
|
162
|
+
uncertainty_threshold: float = Field(default=0.3, ge=0.0, le=1.0)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class ConsensusConfig(BaseModel, frozen=True):
|
|
166
|
+
"""Phase 5 (Consensus) configuration.
|
|
167
|
+
|
|
168
|
+
Attributes:
|
|
169
|
+
min_models: Minimum number of models for consensus
|
|
170
|
+
threshold: Agreement threshold for consensus
|
|
171
|
+
diversity_required: Whether different providers are required
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
min_models: int = Field(default=3, ge=2)
|
|
175
|
+
threshold: float = Field(default=0.67, ge=0.0, le=1.0)
|
|
176
|
+
diversity_required: bool = True
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class PersistenceConfig(BaseModel, frozen=True):
|
|
180
|
+
"""Persistence configuration.
|
|
181
|
+
|
|
182
|
+
Attributes:
|
|
183
|
+
enabled: Whether persistence is enabled
|
|
184
|
+
database_path: Path to SQLite database (relative to config dir)
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
enabled: bool = True
|
|
188
|
+
database_path: str = "data/ouroboros.db"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class DriftConfig(BaseModel, frozen=True):
|
|
192
|
+
"""Drift monitoring configuration.
|
|
193
|
+
|
|
194
|
+
Attributes:
|
|
195
|
+
warning_threshold: Drift score threshold for warnings
|
|
196
|
+
critical_threshold: Drift score threshold for intervention
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
warning_threshold: float = Field(default=0.3, ge=0.0, le=1.0)
|
|
200
|
+
critical_threshold: float = Field(default=0.5, ge=0.0, le=1.0)
|
|
201
|
+
|
|
202
|
+
@field_validator("critical_threshold")
|
|
203
|
+
@classmethod
|
|
204
|
+
def validate_critical_threshold(cls, v: float, info: object) -> float:
|
|
205
|
+
"""Validate that critical threshold >= warning threshold."""
|
|
206
|
+
data = getattr(info, "data", {})
|
|
207
|
+
warning = data.get("warning_threshold", 0.3)
|
|
208
|
+
if v < warning:
|
|
209
|
+
msg = f"critical_threshold ({v}) must be >= warning_threshold ({warning})"
|
|
210
|
+
raise ValueError(msg)
|
|
211
|
+
return v
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class LoggingConfig(BaseModel, frozen=True):
|
|
215
|
+
"""Logging configuration.
|
|
216
|
+
|
|
217
|
+
Attributes:
|
|
218
|
+
level: Log level (debug, info, warning, error)
|
|
219
|
+
log_path: Path to log file (relative to config dir)
|
|
220
|
+
include_reasoning: Whether to log LLM reasoning
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
level: Literal["debug", "info", "warning", "error"] = "info"
|
|
224
|
+
log_path: str = "logs/ouroboros.log"
|
|
225
|
+
include_reasoning: bool = True
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class OuroborosConfig(BaseModel, frozen=True):
|
|
229
|
+
"""Top-level Ouroboros configuration.
|
|
230
|
+
|
|
231
|
+
This is the main configuration model that combines all section configs.
|
|
232
|
+
It validates against config.yaml in ~/.ouroboros/.
|
|
233
|
+
|
|
234
|
+
Attributes:
|
|
235
|
+
economics: Economic model and tier configuration
|
|
236
|
+
clarification: Phase 0 (Big Bang) configuration
|
|
237
|
+
execution: Phase 2 configuration
|
|
238
|
+
resilience: Phase 3 configuration
|
|
239
|
+
evaluation: Phase 4 configuration
|
|
240
|
+
consensus: Phase 5 configuration
|
|
241
|
+
persistence: Storage configuration
|
|
242
|
+
drift: Drift monitoring configuration
|
|
243
|
+
logging: Logging configuration
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
economics: EconomicsConfig = Field(default_factory=EconomicsConfig)
|
|
247
|
+
clarification: ClarificationConfig = Field(default_factory=ClarificationConfig)
|
|
248
|
+
execution: ExecutionConfig = Field(default_factory=ExecutionConfig)
|
|
249
|
+
resilience: ResilienceConfig = Field(default_factory=ResilienceConfig)
|
|
250
|
+
evaluation: EvaluationConfig = Field(default_factory=EvaluationConfig)
|
|
251
|
+
consensus: ConsensusConfig = Field(default_factory=ConsensusConfig)
|
|
252
|
+
persistence: PersistenceConfig = Field(default_factory=PersistenceConfig)
|
|
253
|
+
drift: DriftConfig = Field(default_factory=DriftConfig)
|
|
254
|
+
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def get_default_config() -> OuroborosConfig:
|
|
258
|
+
"""Get the default Ouroboros configuration.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
OuroborosConfig with all default values populated.
|
|
262
|
+
"""
|
|
263
|
+
return OuroborosConfig(
|
|
264
|
+
economics=EconomicsConfig(
|
|
265
|
+
default_tier="frugal",
|
|
266
|
+
tiers={
|
|
267
|
+
"frugal": TierConfig(
|
|
268
|
+
cost_factor=1,
|
|
269
|
+
intelligence_range=(9, 11),
|
|
270
|
+
models=[
|
|
271
|
+
ModelConfig(provider="openai", model="gpt-4o-mini"),
|
|
272
|
+
ModelConfig(provider="google", model="gemini-2.0-flash"),
|
|
273
|
+
ModelConfig(provider="anthropic", model="claude-3-5-haiku"),
|
|
274
|
+
],
|
|
275
|
+
use_cases=["routine_coding", "log_analysis", "stage1_fix"],
|
|
276
|
+
),
|
|
277
|
+
"standard": TierConfig(
|
|
278
|
+
cost_factor=10,
|
|
279
|
+
intelligence_range=(14, 16),
|
|
280
|
+
models=[
|
|
281
|
+
ModelConfig(provider="openai", model="gpt-4o"),
|
|
282
|
+
ModelConfig(provider="anthropic", model="claude-sonnet-4-20250514"),
|
|
283
|
+
ModelConfig(provider="google", model="gemini-2.5-pro"),
|
|
284
|
+
],
|
|
285
|
+
use_cases=["logic_design", "stage2_evaluation", "refactoring"],
|
|
286
|
+
),
|
|
287
|
+
"frontier": TierConfig(
|
|
288
|
+
cost_factor=30,
|
|
289
|
+
intelligence_range=(18, 20),
|
|
290
|
+
models=[
|
|
291
|
+
ModelConfig(provider="openai", model="o3"),
|
|
292
|
+
ModelConfig(provider="anthropic", model="claude-opus-4-5-20251101"),
|
|
293
|
+
],
|
|
294
|
+
use_cases=["consensus", "lateral_thinking", "big_bang"],
|
|
295
|
+
),
|
|
296
|
+
},
|
|
297
|
+
escalation_threshold=2,
|
|
298
|
+
downgrade_success_streak=5,
|
|
299
|
+
),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def get_default_credentials() -> CredentialsConfig:
|
|
304
|
+
"""Get the default credentials configuration template.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
CredentialsConfig with placeholder providers.
|
|
308
|
+
|
|
309
|
+
Note:
|
|
310
|
+
The returned credentials have empty API keys and should be
|
|
311
|
+
filled in by the user.
|
|
312
|
+
"""
|
|
313
|
+
return CredentialsConfig(
|
|
314
|
+
providers={
|
|
315
|
+
"openrouter": ProviderCredentials(
|
|
316
|
+
api_key="YOUR_OPENROUTER_API_KEY",
|
|
317
|
+
base_url="https://openrouter.ai/api/v1",
|
|
318
|
+
),
|
|
319
|
+
"openai": ProviderCredentials(api_key="YOUR_OPENAI_API_KEY"),
|
|
320
|
+
"anthropic": ProviderCredentials(api_key="YOUR_ANTHROPIC_API_KEY"),
|
|
321
|
+
"google": ProviderCredentials(api_key="YOUR_GOOGLE_API_KEY"),
|
|
322
|
+
}
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def get_config_dir() -> Path:
|
|
327
|
+
"""Get the Ouroboros configuration directory path.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Path to ~/.ouroboros/
|
|
331
|
+
"""
|
|
332
|
+
return Path.home() / ".ouroboros"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Ouroboros core module - shared types, errors, and protocols."""
|
|
2
|
+
|
|
3
|
+
from ouroboros.core.context import (
|
|
4
|
+
CompressionResult,
|
|
5
|
+
ContextMetrics,
|
|
6
|
+
FilteredContext,
|
|
7
|
+
WorkflowContext,
|
|
8
|
+
compress_context,
|
|
9
|
+
compress_context_with_llm,
|
|
10
|
+
count_context_tokens,
|
|
11
|
+
count_tokens,
|
|
12
|
+
create_filtered_context,
|
|
13
|
+
get_context_metrics,
|
|
14
|
+
)
|
|
15
|
+
from ouroboros.core.errors import (
|
|
16
|
+
ConfigError,
|
|
17
|
+
OuroborosError,
|
|
18
|
+
PersistenceError,
|
|
19
|
+
ProviderError,
|
|
20
|
+
ValidationError,
|
|
21
|
+
)
|
|
22
|
+
from ouroboros.core.seed import (
|
|
23
|
+
EvaluationPrinciple,
|
|
24
|
+
ExitCondition,
|
|
25
|
+
OntologyField,
|
|
26
|
+
OntologySchema,
|
|
27
|
+
Seed,
|
|
28
|
+
SeedMetadata,
|
|
29
|
+
)
|
|
30
|
+
from ouroboros.core.types import CostUnits, DriftScore, EventPayload, Result
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
# Types
|
|
34
|
+
"Result",
|
|
35
|
+
"EventPayload",
|
|
36
|
+
"CostUnits",
|
|
37
|
+
"DriftScore",
|
|
38
|
+
# Errors
|
|
39
|
+
"OuroborosError",
|
|
40
|
+
"ProviderError",
|
|
41
|
+
"ConfigError",
|
|
42
|
+
"PersistenceError",
|
|
43
|
+
"ValidationError",
|
|
44
|
+
# Seed (Immutable Specification)
|
|
45
|
+
"Seed",
|
|
46
|
+
"SeedMetadata",
|
|
47
|
+
"OntologySchema",
|
|
48
|
+
"OntologyField",
|
|
49
|
+
"EvaluationPrinciple",
|
|
50
|
+
"ExitCondition",
|
|
51
|
+
# Context Management
|
|
52
|
+
"WorkflowContext",
|
|
53
|
+
"ContextMetrics",
|
|
54
|
+
"CompressionResult",
|
|
55
|
+
"FilteredContext",
|
|
56
|
+
"count_tokens",
|
|
57
|
+
"count_context_tokens",
|
|
58
|
+
"get_context_metrics",
|
|
59
|
+
"compress_context",
|
|
60
|
+
"compress_context_with_llm",
|
|
61
|
+
"create_filtered_context",
|
|
62
|
+
]
|