nexus-coding-agent 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.
Files changed (131) hide show
  1. nexus/__init__.py +4 -0
  2. nexus/application/__init__.py +2 -0
  3. nexus/application/approval_service.py +130 -0
  4. nexus/application/diff_service.py +177 -0
  5. nexus/application/event_publisher.py +180 -0
  6. nexus/application/execution_context.py +29 -0
  7. nexus/application/execution_ledger.py +126 -0
  8. nexus/application/model_call_context.py +29 -0
  9. nexus/application/observed_model_gateway.py +194 -0
  10. nexus/application/plan_approval_service.py +250 -0
  11. nexus/application/planning.py +668 -0
  12. nexus/application/runtime.py +624 -0
  13. nexus/application/session_service.py +411 -0
  14. nexus/application/structured_output.py +96 -0
  15. nexus/application/telemetry.py +396 -0
  16. nexus/application/telemetry_subscriber.py +216 -0
  17. nexus/application/tool_runtime.py +496 -0
  18. nexus/application/tracing_dispatcher.py +283 -0
  19. nexus/application/validation.py +293 -0
  20. nexus/config/__init__.py +17 -0
  21. nexus/config/loader.py +253 -0
  22. nexus/config/models.py +310 -0
  23. nexus/context/__init__.py +6 -0
  24. nexus/context/budget.py +24 -0
  25. nexus/context/builder.py +92 -0
  26. nexus/context/chunking.py +65 -0
  27. nexus/context/explorer.py +246 -0
  28. nexus/context/integration.py +63 -0
  29. nexus/context/manager.py +328 -0
  30. nexus/context/retrieval.py +261 -0
  31. nexus/domain/__init__.py +2 -0
  32. nexus/domain/agent_decision.py +113 -0
  33. nexus/domain/agent_state.py +49 -0
  34. nexus/domain/approvals.py +64 -0
  35. nexus/domain/context.py +108 -0
  36. nexus/domain/exploration.py +86 -0
  37. nexus/domain/mcp.py +51 -0
  38. nexus/domain/model.py +173 -0
  39. nexus/domain/observability.py +326 -0
  40. nexus/domain/persistence.py +122 -0
  41. nexus/domain/planning.py +329 -0
  42. nexus/domain/ports/__init__.py +61 -0
  43. nexus/domain/ports/agent_decision.py +9 -0
  44. nexus/domain/ports/approval_unit_of_work.py +27 -0
  45. nexus/domain/ports/checkpoint_provider.py +11 -0
  46. nexus/domain/ports/context.py +71 -0
  47. nexus/domain/ports/events.py +21 -0
  48. nexus/domain/ports/graph_runtime.py +30 -0
  49. nexus/domain/ports/mcp.py +23 -0
  50. nexus/domain/ports/model_gateway.py +21 -0
  51. nexus/domain/ports/model_input_budget.py +15 -0
  52. nexus/domain/ports/planning.py +62 -0
  53. nexus/domain/ports/repositories.py +54 -0
  54. nexus/domain/ports/repository_context.py +18 -0
  55. nexus/domain/ports/session_unit_of_work.py +33 -0
  56. nexus/domain/ports/skills.py +41 -0
  57. nexus/domain/ports/tooling.py +39 -0
  58. nexus/domain/ports/tracer.py +13 -0
  59. nexus/domain/ports/validation.py +32 -0
  60. nexus/domain/runtime_events.py +445 -0
  61. nexus/domain/skills.py +129 -0
  62. nexus/domain/tooling.py +163 -0
  63. nexus/domain/validation.py +161 -0
  64. nexus/errors/__init__.py +82 -0
  65. nexus/evaluation/__init__.py +7 -0
  66. nexus/evaluation/assertions.py +4 -0
  67. nexus/evaluation/evaluator.py +263 -0
  68. nexus/evaluation/fixtures.py +36 -0
  69. nexus/evaluation/harness.py +606 -0
  70. nexus/evaluation/loader.py +415 -0
  71. nexus/evaluation/metrics.py +27 -0
  72. nexus/evaluation/models.py +362 -0
  73. nexus/evaluation/report.py +43 -0
  74. nexus/evaluation/runner.py +480 -0
  75. nexus/infrastructure/__init__.py +2 -0
  76. nexus/infrastructure/asyncio_compat.py +13 -0
  77. nexus/infrastructure/bootstrap/__init__.py +17 -0
  78. nexus/infrastructure/bootstrap/composition.py +663 -0
  79. nexus/infrastructure/checkpoint/__init__.py +5 -0
  80. nexus/infrastructure/checkpoint/postgres.py +62 -0
  81. nexus/infrastructure/database.py +39 -0
  82. nexus/infrastructure/embedding.py +61 -0
  83. nexus/infrastructure/graph/__init__.py +6 -0
  84. nexus/infrastructure/graph/day4_runtime.py +966 -0
  85. nexus/infrastructure/graph/langgraph_runtime.py +172 -0
  86. nexus/infrastructure/mcp/__init__.py +5 -0
  87. nexus/infrastructure/mcp/manager.py +380 -0
  88. nexus/infrastructure/model_gateway/__init__.py +6 -0
  89. nexus/infrastructure/model_gateway/openai_compatible.py +345 -0
  90. nexus/infrastructure/observability/__init__.py +7 -0
  91. nexus/infrastructure/observability/console.py +31 -0
  92. nexus/infrastructure/observability/langsmith.py +236 -0
  93. nexus/infrastructure/observability/structured_logger.py +70 -0
  94. nexus/infrastructure/persistence/__init__.py +13 -0
  95. nexus/infrastructure/persistence/approval_unit_of_work.py +74 -0
  96. nexus/infrastructure/persistence/models.py +146 -0
  97. nexus/infrastructure/persistence/repositories.py +326 -0
  98. nexus/infrastructure/persistence/unit_of_work.py +82 -0
  99. nexus/infrastructure/repository_files.py +117 -0
  100. nexus/infrastructure/sandbox/__init__.py +5 -0
  101. nexus/infrastructure/sandbox/local_process.py +353 -0
  102. nexus/infrastructure/semantic.py +276 -0
  103. nexus/infrastructure/semantic_models.py +56 -0
  104. nexus/interfaces/__init__.py +2 -0
  105. nexus/interfaces/cli/__init__.py +2 -0
  106. nexus/interfaces/cli/__main__.py +6 -0
  107. nexus/interfaces/cli/app.py +266 -0
  108. nexus/interfaces/cli/renderer.py +118 -0
  109. nexus/security/__init__.py +14 -0
  110. nexus/security/approval_policies.py +73 -0
  111. nexus/security/command_policy.py +117 -0
  112. nexus/security/executables.py +107 -0
  113. nexus/security/git_commands.py +65 -0
  114. nexus/security/workspace.py +93 -0
  115. nexus/skills/__init__.py +7 -0
  116. nexus/skills/builtin/__init__.py +1 -0
  117. nexus/skills/builtin/debug-python/SKILL.md +26 -0
  118. nexus/skills/builtin/review-repository/SKILL.md +26 -0
  119. nexus/skills/builtin/write-tests/SKILL.md +26 -0
  120. nexus/skills/loader.py +439 -0
  121. nexus/skills/registry.py +146 -0
  122. nexus/skills/selector.py +152 -0
  123. nexus/tools/__init__.py +6 -0
  124. nexus/tools/editing.py +312 -0
  125. nexus/tools/mcp.py +160 -0
  126. nexus/tools/native.py +654 -0
  127. nexus/tools/registry.py +27 -0
  128. nexus_coding_agent-0.1.0.dist-info/METADATA +142 -0
  129. nexus_coding_agent-0.1.0.dist-info/RECORD +131 -0
  130. nexus_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  131. nexus_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
nexus/config/loader.py ADDED
@@ -0,0 +1,253 @@
1
+ """Resolve Day 1 configuration with the frozen per-field precedence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import tomllib
7
+ from collections.abc import Mapping
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from pydantic import ValidationError
12
+
13
+ from nexus.config.models import RuntimeConfig
14
+ from nexus.errors import ConfigurationError
15
+
16
+ _ENV_FIELDS = {
17
+ "NEXUS_MODEL_PROVIDER": "model_provider",
18
+ "NEXUS_MODEL_NAME": "model_name",
19
+ "NEXUS_MODEL_BASE_URL": "model_base_url",
20
+ "NEXUS_MODEL_API_KEY": "model_api_key",
21
+ "NEXUS_DATABASE_URL": "database_url",
22
+ "NEXUS_APPROVAL_MODE": "approval_mode",
23
+ "NEXUS_MAX_STEPS": "max_steps",
24
+ "NEXUS_MAX_REPAIR_ATTEMPTS": "max_repair_attempts",
25
+ "NEXUS_MAX_REPLANS": "max_replans",
26
+ "NEXUS_MAX_SELECTED_SKILLS": "max_selected_skills",
27
+ "NEXUS_CONSOLE_TRACING_ENABLED": "console_tracing_enabled",
28
+ "NEXUS_LANGSMITH_TRACING_ENABLED": "langsmith_tracing_enabled",
29
+ "NEXUS_LANGSMITH_PROJECT": "langsmith_project",
30
+ "NEXUS_LANGSMITH_ENDPOINT": "langsmith_endpoint",
31
+ "NEXUS_LANGSMITH_WORKSPACE_ID": "langsmith_workspace_id",
32
+ "NEXUS_LANGSMITH_API_KEY": "langsmith_api_key",
33
+ }
34
+
35
+ _CONTEXT_INTEGERS = (
36
+ "max_retrieved_chunks", "max_exploration_seed_chunks", "max_code_context_tokens",
37
+ "max_recent_observations", "max_model_input_tokens", "max_file_size_bytes",
38
+ )
39
+ _ENV_FIELDS.update({f"NEXUS_{name.upper()}": name for name in _CONTEXT_INTEGERS})
40
+ _ENV_FIELDS.update({
41
+ "NEXUS_SEMANTIC_ENABLED": "semantic_enabled",
42
+ "NEXUS_EMBEDDING_MODEL": "embedding_model",
43
+ "NEXUS_EMBEDDING_DIMENSION": "embedding_dimension",
44
+ "NEXUS_EMBEDDING_BASE_URL": "embedding_base_url",
45
+ "NEXUS_EMBEDDING_API_KEY": "embedding_api_key",
46
+ })
47
+
48
+
49
+ def load_runtime_config(
50
+ *,
51
+ cli_model: str | None = None,
52
+ cli_base_url: str | None = None,
53
+ repo_root: Path | None = None,
54
+ user_config_path: Path | None = None,
55
+ environ: Mapping[str, str] | None = None,
56
+ ) -> RuntimeConfig:
57
+ """Load configuration per field from lowest to highest priority."""
58
+
59
+ environment = os.environ if environ is None else environ
60
+ resolved: dict[str, Any] = {}
61
+ resolved.update(_environment_values(environment))
62
+
63
+ user_path = user_config_path or (Path.home() / ".nexus" / "config.toml")
64
+ resolved.update(_toml_values(user_path, allow_remote_observability=True))
65
+
66
+ repository = repo_root or Path.cwd()
67
+ resolved.update(
68
+ _toml_values(
69
+ repository / ".nexus" / "config.toml",
70
+ allow_mcp=False,
71
+ allow_remote_observability=False,
72
+ )
73
+ )
74
+
75
+ if cli_model is not None:
76
+ resolved["model_name"] = cli_model
77
+ if cli_base_url is not None:
78
+ resolved["model_base_url"] = cli_base_url
79
+
80
+ try:
81
+ return RuntimeConfig.model_validate(resolved)
82
+ except ValidationError as exc:
83
+ code = (
84
+ "TRACE_CONFIGURATION_INVALID"
85
+ if any("langsmith" in str(error).lower() for error in exc.errors())
86
+ else None
87
+ )
88
+ raise ConfigurationError("Runtime configuration is invalid.", code=code) from exc
89
+
90
+
91
+ def _environment_values(environ: Mapping[str, str]) -> dict[str, Any]:
92
+ return {
93
+ field: environ[variable]
94
+ for variable, field in _ENV_FIELDS.items()
95
+ if variable in environ
96
+ }
97
+
98
+
99
+ def _toml_values(
100
+ path: Path,
101
+ *,
102
+ allow_mcp: bool = True,
103
+ allow_remote_observability: bool = True,
104
+ ) -> dict[str, Any]:
105
+ if not path.is_file():
106
+ return {}
107
+ try:
108
+ with path.open("rb") as config_file:
109
+ document = tomllib.load(config_file)
110
+ except (OSError, tomllib.TOMLDecodeError) as exc:
111
+ raise ConfigurationError(f"Could not read Nexus configuration at {path}.") from exc
112
+
113
+ if not allow_mcp and "mcp" in document:
114
+ raise ConfigurationError(
115
+ "Repository configuration must not define [mcp]; MCP process and server "
116
+ "configuration is allowed only in the user-level ~/.nexus/config.toml."
117
+ )
118
+ observability = _table(document, "observability", path)
119
+ if not allow_remote_observability and _contains_key(document, "langsmith"):
120
+ raise ConfigurationError(
121
+ "Repository configuration must not define LangSmith observability.",
122
+ code="TRACE_CONFIGURATION_INVALID",
123
+ )
124
+
125
+ model = _table(document, "model", path)
126
+ database = _table(document, "database", path)
127
+ runtime = _table(document, "runtime", path)
128
+ context = _table(document, "context", path)
129
+ embedding = _table(document, "embedding", path)
130
+ skills = _table(document, "skills", path)
131
+ mcp = _table(document, "mcp", path)
132
+ console = _table(observability, "console", path)
133
+ langsmith = _table(observability, "langsmith", path)
134
+ if "api_key" in embedding:
135
+ raise ConfigurationError("Embedding API keys are not supported in Nexus TOML.")
136
+ if "api_key" in model:
137
+ raise ConfigurationError(
138
+ f"API keys are not supported in Nexus TOML configuration at {path}."
139
+ )
140
+ if "api_key" in langsmith:
141
+ raise ConfigurationError(
142
+ "LangSmith API keys are accepted only from NEXUS_LANGSMITH_API_KEY.",
143
+ code="TRACE_CONFIGURATION_INVALID",
144
+ )
145
+
146
+ values: dict[str, Any] = {}
147
+ for name in _CONTEXT_INTEGERS:
148
+ _copy_optional_integer(context, name, name, values, path)
149
+ if "semantic_enabled" in context:
150
+ if not isinstance(context["semantic_enabled"], bool):
151
+ raise ConfigurationError("semantic_enabled must be a TOML boolean.")
152
+ values["semantic_enabled"] = context["semantic_enabled"]
153
+ for name in ("provider", "model", "base_url"):
154
+ _copy_optional_string(embedding, name, f"embedding_{name}", values, path)
155
+ _copy_optional_integer(embedding, "dimension", "embedding_dimension", values, path)
156
+ _copy_optional_integer(
157
+ skills,
158
+ "max_selected_skills",
159
+ "max_selected_skills",
160
+ values,
161
+ path,
162
+ )
163
+ _copy_optional_string(model, "provider", "model_provider", values, path)
164
+ _copy_optional_string(model, "name", "model_name", values, path)
165
+ _copy_optional_string(model, "base_url", "model_base_url", values, path)
166
+ _copy_optional_string(database, "url", "database_url", values, path)
167
+ _copy_optional_string(runtime, "approval_mode", "approval_mode", values, path)
168
+ _copy_optional_integer(runtime, "max_steps", "max_steps", values, path)
169
+ _copy_optional_integer(
170
+ runtime,
171
+ "max_repair_attempts",
172
+ "max_repair_attempts",
173
+ values,
174
+ path,
175
+ )
176
+ _copy_optional_integer(runtime, "max_replans", "max_replans", values, path)
177
+ if "enabled" in mcp:
178
+ if not isinstance(mcp["enabled"], bool):
179
+ raise ConfigurationError("mcp.enabled must be a TOML boolean.")
180
+ values["mcp_enabled"] = mcp["enabled"]
181
+ if "servers" in mcp:
182
+ servers = mcp["servers"]
183
+ if not isinstance(servers, list) or any(
184
+ not isinstance(server, Mapping) for server in servers
185
+ ):
186
+ raise ConfigurationError("mcp.servers must be an array of tables.")
187
+ values["mcp_servers"] = [dict(server) for server in servers]
188
+ if "enabled" in console:
189
+ if not isinstance(console["enabled"], bool):
190
+ raise ConfigurationError("observability.console.enabled must be a TOML boolean.")
191
+ values["console_tracing_enabled"] = console["enabled"]
192
+ if "enabled" in langsmith:
193
+ if not isinstance(langsmith["enabled"], bool):
194
+ raise ConfigurationError(
195
+ "observability.langsmith.enabled must be a TOML boolean.",
196
+ code="TRACE_CONFIGURATION_INVALID",
197
+ )
198
+ values["langsmith_tracing_enabled"] = langsmith["enabled"]
199
+ for name, target in (
200
+ ("project", "langsmith_project"),
201
+ ("endpoint", "langsmith_endpoint"),
202
+ ("workspace_id", "langsmith_workspace_id"),
203
+ ):
204
+ _copy_optional_string(langsmith, name, target, values, path)
205
+ return values
206
+
207
+
208
+ def _table(document: Mapping[str, Any], name: str, path: Path) -> Mapping[str, Any]:
209
+ value = document.get(name, {})
210
+ if not isinstance(value, Mapping):
211
+ raise ConfigurationError(f"[{name}] must be a TOML table in {path}.")
212
+ return value
213
+
214
+
215
+ def _copy_optional_string(
216
+ source: Mapping[str, Any],
217
+ source_key: str,
218
+ target_key: str,
219
+ target: dict[str, Any],
220
+ path: Path,
221
+ ) -> None:
222
+ if source_key not in source:
223
+ return
224
+ value = source[source_key]
225
+ if not isinstance(value, str):
226
+ raise ConfigurationError(f"{source_key} must be a string in {path}.")
227
+ target[target_key] = value
228
+
229
+
230
+ def _copy_optional_integer(
231
+ source: Mapping[str, Any],
232
+ source_key: str,
233
+ target_key: str,
234
+ target: dict[str, Any],
235
+ path: Path,
236
+ ) -> None:
237
+ if source_key not in source:
238
+ return
239
+ value = source[source_key]
240
+ if isinstance(value, bool) or not isinstance(value, int):
241
+ raise ConfigurationError(f"{source_key} must be an integer in {path}.")
242
+ target[target_key] = value
243
+
244
+
245
+ def _contains_key(value: object, prohibited: str) -> bool:
246
+ if isinstance(value, Mapping):
247
+ return any(
248
+ prohibited in str(key).lower() or _contains_key(item, prohibited)
249
+ for key, item in value.items()
250
+ )
251
+ if isinstance(value, list):
252
+ return any(_contains_key(item, prohibited) for item in value)
253
+ return False
nexus/config/models.py ADDED
@@ -0,0 +1,310 @@
1
+ """Typed Day 1 runtime configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import StrEnum
6
+ from urllib.parse import urlsplit, urlunsplit
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator, model_validator
9
+
10
+ from nexus.domain.tooling import RiskLevel
11
+ from nexus.errors import ConfigurationError
12
+
13
+ DEFAULT_DATABASE_URL = "postgresql+asyncpg://nexus:nexus@localhost:5432/nexus"
14
+ SUPPORTED_MODEL_PROVIDER = "openai_compatible"
15
+
16
+
17
+ class ApprovalMode(StrEnum):
18
+ APPROVAL = "approval"
19
+ AUTO = "auto"
20
+
21
+
22
+ class MCPTransport(StrEnum):
23
+ STDIO = "stdio"
24
+
25
+
26
+ class MCPToolRiskConfig(BaseModel):
27
+ model_config = ConfigDict(frozen=True)
28
+
29
+ tool_name: str
30
+ risk_level: RiskLevel
31
+
32
+ @field_validator("tool_name")
33
+ @classmethod
34
+ def validate_tool_name(cls, value: str) -> str:
35
+ if not value.strip():
36
+ raise ValueError("MCP tool_name must not be empty.")
37
+ return value
38
+
39
+
40
+ class MCPServerConfig(BaseModel):
41
+ model_config = ConfigDict(frozen=True)
42
+
43
+ server_id: str = Field(pattern=r"^[a-z][a-z0-9_-]{0,63}$")
44
+ enabled: bool = True
45
+ transport: MCPTransport = MCPTransport.STDIO
46
+ command: str
47
+ args: tuple[str, ...] = ()
48
+ inherit_environment: bool = True
49
+ connect_timeout_seconds: float = Field(default=10.0, gt=0, le=60)
50
+ tool_timeout_seconds: float = Field(default=30.0, gt=0, le=300)
51
+ max_connect_attempts: int = Field(default=2, ge=1, le=3)
52
+ tool_risks: tuple[MCPToolRiskConfig, ...] = ()
53
+
54
+ @field_validator("command")
55
+ @classmethod
56
+ def validate_command(cls, value: str) -> str:
57
+ if not value.strip():
58
+ raise ValueError("MCP command must not be empty.")
59
+ return value
60
+
61
+ @field_validator("args")
62
+ @classmethod
63
+ def validate_args(cls, value: tuple[str, ...]) -> tuple[str, ...]:
64
+ if any(not item.strip() for item in value):
65
+ raise ValueError("MCP args must contain only non-empty strings.")
66
+ return value
67
+
68
+ @field_validator("max_connect_attempts", mode="before")
69
+ @classmethod
70
+ def reject_boolean_attempts(cls, value: object) -> object:
71
+ if isinstance(value, bool):
72
+ raise ValueError("MCP max_connect_attempts must be an integer.")
73
+ return value
74
+
75
+ @model_validator(mode="after")
76
+ def validate_unique_risks(self) -> MCPServerConfig:
77
+ names = [item.tool_name for item in self.tool_risks]
78
+ if len(names) != len(set(names)):
79
+ raise ValueError("MCP tool_risks contains duplicate tool_name entries.")
80
+ return self
81
+
82
+
83
+ class RuntimeConfig(BaseModel):
84
+ """The exact approved Day 1 configuration surface."""
85
+
86
+ model_config = ConfigDict(frozen=True)
87
+
88
+ model_provider: str = SUPPORTED_MODEL_PROVIDER
89
+ model_name: str | None = None
90
+ model_base_url: str | None = None
91
+ model_api_key: SecretStr | None = None
92
+ database_url: str = DEFAULT_DATABASE_URL
93
+ approval_mode: ApprovalMode = ApprovalMode.APPROVAL
94
+ max_steps: int = 30
95
+ max_repair_attempts: int = 3
96
+ max_replans: int = 2
97
+ semantic_enabled: bool = False
98
+ lexical_top_k: int = Field(default=20, ge=20, le=20)
99
+ semantic_top_k: int = Field(default=20, ge=20, le=20)
100
+ rrf_k: int = Field(default=60, ge=60, le=60)
101
+ final_candidate_count: int = Field(default=12, ge=12, le=12)
102
+ max_retrieved_chunks: int = Field(default=12, ge=0, le=12)
103
+ max_exploration_seed_chunks: int = Field(default=6, ge=0, le=6)
104
+ max_code_context_tokens: int = Field(default=12000, ge=0, le=12000)
105
+ max_recent_observations: int = Field(default=8, ge=0, le=8)
106
+ max_model_input_tokens: int = Field(default=24000, ge=1, le=24000)
107
+ max_file_size_bytes: int = Field(default=1048576, ge=1)
108
+ embedding_provider: str = "openai_compatible"
109
+ embedding_model: str = ""
110
+ embedding_dimension: int = Field(default=0, ge=0)
111
+ embedding_base_url: str = ""
112
+ embedding_api_key: SecretStr | None = None
113
+ max_selected_skills: int = Field(default=2, ge=0, le=2)
114
+ mcp_enabled: bool = False
115
+ mcp_servers: tuple[MCPServerConfig, ...] = ()
116
+ console_tracing_enabled: bool = False
117
+ langsmith_tracing_enabled: bool = False
118
+ langsmith_project: str = "nexus"
119
+ langsmith_endpoint: str | None = None
120
+ langsmith_workspace_id: str | None = None
121
+ langsmith_api_key: SecretStr | None = None
122
+
123
+ def require_embedding(self) -> None:
124
+ """Validate only when a semantic/index capability is actually requested."""
125
+ if (
126
+ self.embedding_provider != "openai_compatible"
127
+ or not self.embedding_model.strip()
128
+ or self.embedding_dimension <= 0
129
+ or not self.embedding_base_url.startswith(("https://", "http://"))
130
+ or self.embedding_api_key is None
131
+ or not self.embedding_api_key.get_secret_value().strip()
132
+ ):
133
+ raise ConfigurationError(
134
+ "Configure embedding provider/model/dimension/base_url and "
135
+ "NEXUS_EMBEDDING_API_KEY, or disable semantic retrieval."
136
+ )
137
+
138
+ @model_validator(mode="after")
139
+ def validate_context_budget(self) -> RuntimeConfig:
140
+ if self.max_code_context_tokens > self.max_model_input_tokens:
141
+ raise ValueError("Code context budget must not exceed total model input budget.")
142
+ return self
143
+
144
+ @field_validator("model_provider")
145
+ @classmethod
146
+ def validate_provider(cls, value: str) -> str:
147
+ if value != SUPPORTED_MODEL_PROVIDER:
148
+ raise ValueError(f"unsupported Day 1 model provider: {value!r}")
149
+ return value
150
+
151
+ @field_validator(
152
+ "model_name",
153
+ "model_base_url",
154
+ "langsmith_endpoint",
155
+ "langsmith_workspace_id",
156
+ mode="before",
157
+ )
158
+ @classmethod
159
+ def normalize_optional_text(cls, value: object) -> object:
160
+ if isinstance(value, str):
161
+ normalized = value.strip()
162
+ return normalized or None
163
+ return value
164
+
165
+ @field_validator("model_name")
166
+ @classmethod
167
+ def validate_model_name_length(cls, value: str | None) -> str | None:
168
+ if value is not None and len(value) > 256:
169
+ raise ValueError("Model name is too long for safe observability.")
170
+ return value
171
+
172
+ @field_validator("database_url")
173
+ @classmethod
174
+ def validate_database_url(cls, value: str) -> str:
175
+ if not value.strip():
176
+ raise ValueError("database URL must not be empty")
177
+ if not value.startswith("postgresql+asyncpg://"):
178
+ raise ValueError("Day 1 database URL must use postgresql+asyncpg")
179
+ return value
180
+
181
+ @field_validator(
182
+ "max_steps",
183
+ "max_repair_attempts",
184
+ "max_replans",
185
+ "lexical_top_k",
186
+ "semantic_top_k",
187
+ "rrf_k",
188
+ "final_candidate_count",
189
+ "max_retrieved_chunks",
190
+ "max_exploration_seed_chunks",
191
+ "max_code_context_tokens",
192
+ "max_recent_observations",
193
+ "max_model_input_tokens",
194
+ "max_file_size_bytes",
195
+ "embedding_dimension",
196
+ "max_selected_skills",
197
+ mode="before",
198
+ )
199
+ @classmethod
200
+ def reject_boolean_limits(cls, value: object) -> object:
201
+ if isinstance(value, bool):
202
+ raise ValueError("Runtime limits must be integers, not booleans.")
203
+ return value
204
+
205
+ @model_validator(mode="after")
206
+ def validate_day4_limits(self) -> RuntimeConfig:
207
+ if self.max_steps <= 0:
208
+ raise ValueError("max_steps must be greater than zero.")
209
+ if self.max_repair_attempts < 0 or self.max_replans < 0:
210
+ raise ValueError("Repair and Replan limits must not be negative.")
211
+ return self
212
+
213
+ @model_validator(mode="after")
214
+ def validate_unique_mcp_servers(self) -> RuntimeConfig:
215
+ server_ids = [server.server_id for server in self.mcp_servers]
216
+ if len(server_ids) != len(set(server_ids)):
217
+ raise ValueError("MCP server_id values must be unique.")
218
+ return self
219
+
220
+ @field_validator("langsmith_project")
221
+ @classmethod
222
+ def validate_langsmith_project(cls, value: str) -> str:
223
+ normalized = value.strip()
224
+ if not normalized or len(normalized) > 256:
225
+ raise ValueError("LangSmith project must be a bounded non-empty identifier.")
226
+ return normalized
227
+
228
+ @field_validator("langsmith_workspace_id")
229
+ @classmethod
230
+ def validate_langsmith_workspace_id(cls, value: str | None) -> str | None:
231
+ if value is not None and len(value) > 256:
232
+ raise ValueError("LangSmith workspace ID is too long.")
233
+ return value
234
+
235
+ @field_validator("langsmith_endpoint")
236
+ @classmethod
237
+ def validate_langsmith_endpoint(cls, value: str | None) -> str | None:
238
+ if value is None:
239
+ return None
240
+ parts = urlsplit(value)
241
+ if (
242
+ parts.scheme not in {"http", "https"}
243
+ or not parts.hostname
244
+ or parts.username is not None
245
+ or parts.password is not None
246
+ or parts.query
247
+ or parts.fragment
248
+ ):
249
+ raise ValueError("LangSmith endpoint must be a credential-free HTTP(S) URL.")
250
+ return value
251
+
252
+ @model_validator(mode="after")
253
+ def validate_langsmith_enablement(self) -> RuntimeConfig:
254
+ if self.langsmith_tracing_enabled and (
255
+ self.langsmith_api_key is None
256
+ or not self.langsmith_api_key.get_secret_value().strip()
257
+ ):
258
+ raise ValueError("Enabled LangSmith tracing requires NEXUS_LANGSMITH_API_KEY.")
259
+ return self
260
+
261
+ def __repr__(self) -> str:
262
+ api_key = "**********" if self.model_api_key is not None else "None"
263
+ return (
264
+ "RuntimeConfig("
265
+ f"model_provider={self.model_provider!r}, "
266
+ f"model_name={self.model_name!r}, "
267
+ f"model_base_url={self.model_base_url!r}, "
268
+ f"model_api_key={api_key}, "
269
+ f"database_url={redact_database_url(self.database_url)!r}, "
270
+ f"approval_mode={self.approval_mode.value!r}, "
271
+ f"max_steps={self.max_steps!r}, "
272
+ f"max_repair_attempts={self.max_repair_attempts!r}, "
273
+ f"max_replans={self.max_replans!r}, "
274
+ f"mcp_enabled={self.mcp_enabled!r}, "
275
+ f"mcp_server_count={len(self.mcp_servers)!r}, "
276
+ f"console_tracing_enabled={self.console_tracing_enabled!r}, "
277
+ f"langsmith_tracing_enabled={self.langsmith_tracing_enabled!r}, "
278
+ f"langsmith_project={self.langsmith_project!r}, "
279
+ f"langsmith_endpoint={redact_endpoint(self.langsmith_endpoint)!r}, "
280
+ f"langsmith_workspace_id={self.langsmith_workspace_id!r}, "
281
+ f"langsmith_api_key={'**********' if self.langsmith_api_key else 'None'}"
282
+ ")"
283
+ )
284
+
285
+ __str__ = __repr__
286
+
287
+
288
+ def redact_database_url(value: str) -> str:
289
+ """Hide embedded database credentials while preserving useful location data."""
290
+
291
+ try:
292
+ parts = urlsplit(value)
293
+ except ValueError:
294
+ return "<redacted-database-url>"
295
+ if parts.username is None and parts.password is None:
296
+ return value
297
+
298
+ hostname = parts.hostname or ""
299
+ port = f":{parts.port}" if parts.port is not None else ""
300
+ redacted_netloc = f"***:***@{hostname}{port}"
301
+ return urlunsplit((parts.scheme, redacted_netloc, parts.path, parts.query, parts.fragment))
302
+
303
+
304
+ def redact_endpoint(value: str | None) -> str | None:
305
+ if value is None:
306
+ return None
307
+ parts = urlsplit(value)
308
+ hostname = parts.hostname or ""
309
+ port = f":{parts.port}" if parts.port is not None else ""
310
+ return urlunsplit((parts.scheme, f"{hostname}{port}", "", "", ""))
@@ -0,0 +1,6 @@
1
+ """Selective Day 4 repository context services."""
2
+
3
+ from nexus.context.builder import BoundedContextBuilder
4
+ from nexus.context.explorer import SelectiveRepositoryExplorer
5
+
6
+ __all__ = ["BoundedContextBuilder", "SelectiveRepositoryExplorer"]
@@ -0,0 +1,24 @@
1
+ """Shared Day 5 model-input accounting exposed through the Day 7 guard port."""
2
+
3
+ from collections.abc import Sequence
4
+
5
+ from nexus.context.manager import model_input_tokens
6
+ from nexus.domain.model import ModelMessage
7
+ from nexus.errors import ContextError
8
+
9
+
10
+ class Day5ModelInputBudgetGuard:
11
+ def __init__(self, maximum: int) -> None:
12
+ self._maximum = maximum
13
+
14
+ def ensure_fits(
15
+ self,
16
+ messages: Sequence[ModelMessage],
17
+ *,
18
+ error_code: str,
19
+ ) -> None:
20
+ if model_input_tokens(messages) > self._maximum:
21
+ raise ContextError(
22
+ "The complete model request exceeds the configured input budget.",
23
+ code=error_code,
24
+ )
@@ -0,0 +1,92 @@
1
+ """Bounded Day 4 context seed construction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import PurePosixPath
6
+ from uuid import uuid4
7
+
8
+ from nexus.application.tool_runtime import ToolRuntime
9
+ from nexus.domain.exploration import (
10
+ ContextBuildRequest,
11
+ RepositoryInstruction,
12
+ SelectedFileContext,
13
+ WorkingContext,
14
+ )
15
+ from nexus.domain.tooling import ToolInvocation
16
+ from nexus.errors import ContextError
17
+
18
+
19
+ class BoundedContextBuilder:
20
+ def __init__(self, tool_runtime: ToolRuntime) -> None:
21
+ self._tool_runtime = tool_runtime
22
+
23
+ async def build(self, request: ContextBuildRequest) -> WorkingContext:
24
+ selected: list[SelectedFileContext] = []
25
+ retained = 0
26
+ truncated = request.exploration.truncated
27
+ for evidence in request.exploration.relevant_files[:8]:
28
+ result = await self._tool_runtime.execute(
29
+ ToolInvocation(
30
+ str(uuid4()),
31
+ "read_file",
32
+ {"path": evidence.path, "start_line": 1, "max_lines": 400},
33
+ request.run_id,
34
+ request.session_id,
35
+ )
36
+ )
37
+ if not result.success or result.output is None:
38
+ truncated = True
39
+ continue
40
+ content = result.output.get("content")
41
+ if not isinstance(content, str):
42
+ truncated = True
43
+ continue
44
+ remaining = 48_000 - retained
45
+ if remaining <= 0:
46
+ truncated = True
47
+ break
48
+ bounded = content[:remaining]
49
+ retained += len(bounded)
50
+ item_truncated = result.output.get("truncated") is True or len(bounded) < len(content)
51
+ truncated = truncated or item_truncated
52
+ selected.append(
53
+ SelectedFileContext(
54
+ evidence.path,
55
+ bounded,
56
+ _applicable_instructions(
57
+ evidence.path,
58
+ request.exploration.instructions,
59
+ ),
60
+ evidence.discovery_reason,
61
+ item_truncated,
62
+ )
63
+ )
64
+ if request.exploration.relevant_files and not selected:
65
+ raise ContextError(
66
+ "Nexus could not build context from selected repository evidence.",
67
+ code="CONTEXT_BUILD_FAILED",
68
+ retryable=True,
69
+ )
70
+ return WorkingContext(
71
+ request.task,
72
+ request.exploration.instructions,
73
+ request.exploration.manifests,
74
+ request.exploration.top_level_paths,
75
+ tuple(selected),
76
+ truncated,
77
+ )
78
+
79
+
80
+ def _applicable_instructions(
81
+ path: str,
82
+ instructions: tuple[RepositoryInstruction, ...],
83
+ ) -> tuple[str, ...]:
84
+ target = PurePosixPath(path)
85
+ applicable = [
86
+ instruction
87
+ for instruction in instructions
88
+ if instruction.scope_path == "."
89
+ or PurePosixPath(instruction.scope_path) == target.parent
90
+ or PurePosixPath(instruction.scope_path) in target.parents
91
+ ]
92
+ return tuple(item.path for item in sorted(applicable, key=lambda item: (item.depth, item.path)))