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
@@ -0,0 +1,65 @@
1
+ """Deterministic normalized UTF-8 line windows and candidate identities."""
2
+
3
+ from hashlib import sha256
4
+ from pathlib import PurePosixPath
5
+
6
+ from nexus.domain.context import CodeChunk, ContextCandidate
7
+
8
+
9
+ def normalize_text(content: str) -> str:
10
+ return content.removeprefix("\ufeff").replace("\r\n", "\n").replace("\r", "\n")
11
+
12
+
13
+ def text_hash(content: str) -> str:
14
+ return sha256(normalize_text(content).encode("utf-8")).hexdigest()
15
+
16
+
17
+ def language_for(path: str) -> str:
18
+ suffix = PurePosixPath(path).suffix.lower()
19
+ return {
20
+ ".py": "python",
21
+ ".java": "java",
22
+ ".js": "javascript",
23
+ ".ts": "typescript",
24
+ ".md": "markdown",
25
+ ".toml": "toml",
26
+ ".json": "json",
27
+ ".go": "go",
28
+ ".rs": "rust",
29
+ ".sql": "sql",
30
+ }.get(suffix, suffix.lstrip(".") or "text")
31
+
32
+
33
+ class LineWindowChunker:
34
+ def chunk(
35
+ self,
36
+ *,
37
+ file_path: str,
38
+ language: str,
39
+ content: str,
40
+ file_hash: str,
41
+ ) -> tuple[CodeChunk, ...]:
42
+ lines = normalize_text(content).splitlines(keepends=True)
43
+ starts = (0,) if 0 < len(lines) <= 120 else range(0, len(lines), 100)
44
+ return tuple(
45
+ CodeChunk(
46
+ file_path.replace("\\", "/"),
47
+ language,
48
+ None,
49
+ start + 1,
50
+ min(start + 120, len(lines)),
51
+ "".join(lines[start : start + 120]),
52
+ text_hash("".join(lines[start : start + 120])),
53
+ file_hash,
54
+ )
55
+ for start in starts
56
+ )
57
+
58
+
59
+ def identity(candidate: ContextCandidate) -> tuple[str, int, int, str]:
60
+ chunk = candidate.chunk
61
+ return chunk.file_path, chunk.start_line, chunk.end_line, chunk.content_hash
62
+
63
+
64
+ def estimated_tokens(content: str) -> int:
65
+ return (len(content) + 3) // 4
@@ -0,0 +1,246 @@
1
+ """Bounded, ToolRuntime-only Day 4 repository exploration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import PurePosixPath
7
+ from uuid import uuid4
8
+
9
+ from nexus.application.tool_runtime import ToolRuntime
10
+ from nexus.domain.exploration import (
11
+ ExplorationRequest,
12
+ ExplorationResult,
13
+ RepositoryFileEvidence,
14
+ RepositoryInstruction,
15
+ )
16
+ from nexus.domain.tooling import ToolInvocation, ToolResult
17
+ from nexus.errors import ContextError
18
+
19
+ _MANIFEST_NAMES = {
20
+ "pyproject.toml",
21
+ "package.json",
22
+ "pom.xml",
23
+ "cargo.toml",
24
+ "go.mod",
25
+ "requirements.txt",
26
+ }
27
+ _TERM_PATTERN = re.compile(r"[A-Za-z_][A-Za-z0-9_.\-/]{2,}")
28
+
29
+
30
+ class SelectiveRepositoryExplorer:
31
+ def __init__(self, tool_runtime: ToolRuntime) -> None:
32
+ self._tool_runtime = tool_runtime
33
+
34
+ async def explore(self, request: ExplorationRequest) -> ExplorationResult:
35
+ results: list[ToolResult] = []
36
+
37
+ async def invoke(name: str, arguments: dict[str, object]) -> ToolResult | None:
38
+ if len(results) >= 24:
39
+ return None
40
+ result = await self._tool_runtime.execute(
41
+ ToolInvocation(
42
+ str(uuid4()),
43
+ name,
44
+ arguments,
45
+ request.run_id,
46
+ request.session_id,
47
+ )
48
+ )
49
+ results.append(result)
50
+ return result
51
+
52
+ initial_status = await invoke("git_status", {})
53
+ listing = await invoke("list_files", {"path": ".", "recursive": False})
54
+ if (
55
+ initial_status is None
56
+ or listing is None
57
+ or not initial_status.success
58
+ or not listing.success
59
+ ):
60
+ raise ContextError(
61
+ "Nexus could not establish repository exploration evidence.",
62
+ code="REPOSITORY_EXPLORATION_FAILED",
63
+ retryable=True,
64
+ )
65
+ top_level = tuple(_string_items(listing, "paths"))
66
+ instructions: list[RepositoryInstruction] = []
67
+ manifests: list[RepositoryFileEvidence] = []
68
+
69
+ if "AGENTS.md" in top_level:
70
+ root_instruction = await invoke("read_file", {"path": "AGENTS.md", "max_lines": 400})
71
+ if root_instruction is not None and root_instruction.success:
72
+ instructions.append(_instruction(root_instruction, "AGENTS.md", ".", 0))
73
+
74
+ manifest_paths = [
75
+ path
76
+ for path in top_level
77
+ if _is_manifest(path)
78
+ ]
79
+ config_search = await invoke("search_files", {"pattern": ".nexus/config.toml"})
80
+ if config_search is not None and config_search.success:
81
+ manifest_paths.extend(_string_items(config_search, "paths"))
82
+ for path in _unique(manifest_paths)[:8]:
83
+ result = await invoke("read_file", {"path": path, "max_lines": 120})
84
+ if result is not None and result.success:
85
+ manifests.append(
86
+ RepositoryFileEvidence(
87
+ path,
88
+ "manifest",
89
+ "Root project/configuration manifest.",
90
+ _bounded_summary(result),
91
+ )
92
+ )
93
+
94
+ relevant: dict[str, RepositoryFileEvidence] = {}
95
+ terms = _task_terms(request.task)
96
+ for term in terms:
97
+ if len(results) >= 22 or len(relevant) >= 12:
98
+ break
99
+ pattern = term if "/" in term else f"*{term}*"
100
+ found = await invoke("search_files", {"pattern": pattern})
101
+ if found is not None and found.success:
102
+ for path in _string_items(found, "paths"):
103
+ if path not in relevant and _is_candidate(path):
104
+ relevant[path] = RepositoryFileEvidence(
105
+ path,
106
+ "task_relevant",
107
+ f"Filename matched task token {term!r}.",
108
+ f"Task-relevant file: {path}",
109
+ )
110
+ if len(relevant) >= 12:
111
+ break
112
+ if len(results) >= 22 or len(relevant) >= 12:
113
+ break
114
+ matched = await invoke(
115
+ "lexical_search",
116
+ {"pattern": term, "path": ".", "case_sensitive": False},
117
+ )
118
+ if matched is not None and matched.success:
119
+ for item in _dict_items(matched, "matches"):
120
+ matched_path = item.get("path")
121
+ if (
122
+ isinstance(matched_path, str)
123
+ and matched_path not in relevant
124
+ and _is_candidate(matched_path)
125
+ ):
126
+ relevant[matched_path] = RepositoryFileEvidence(
127
+ matched_path,
128
+ "task_relevant",
129
+ f"Content matched task token {term!r}.",
130
+ f"Observed lexical match in {matched_path}.",
131
+ )
132
+ if len(relevant) >= 12:
133
+ break
134
+
135
+ nested_candidates = _ancestor_instruction_paths(tuple(relevant))
136
+ known = {item.path for item in instructions}
137
+ for path in nested_candidates:
138
+ if len(results) >= 24 or path in known:
139
+ continue
140
+ result = await invoke("read_file", {"path": path, "max_lines": 400})
141
+ if result is not None and result.success:
142
+ scope = str(PurePosixPath(path).parent)
143
+ content = _content(result)
144
+ if _weakens_nexus_safety(content):
145
+ raise ContextError(
146
+ "A nested repository instruction conflicts with Nexus safety.",
147
+ code="REPOSITORY_INSTRUCTION_CONFLICT",
148
+ )
149
+ instructions.append(
150
+ _instruction(result, path, scope, len(PurePosixPath(scope).parts))
151
+ )
152
+ known.add(path)
153
+
154
+ return ExplorationResult(
155
+ tuple(sorted(instructions, key=lambda item: (item.depth, item.path))),
156
+ tuple(manifests[:8]),
157
+ top_level,
158
+ tuple(list(relevant.values())[:12]),
159
+ initial_status,
160
+ tuple(results),
161
+ len(results) >= 24 or _truncated(listing),
162
+ )
163
+
164
+
165
+ def _instruction(
166
+ result: ToolResult,
167
+ path: str,
168
+ scope: str,
169
+ depth: int,
170
+ ) -> RepositoryInstruction:
171
+ return RepositoryInstruction(path, scope, depth, _content(result), _truncated(result))
172
+
173
+
174
+ def _content(result: ToolResult) -> str:
175
+ value = None if result.output is None else result.output.get("content")
176
+ return value if isinstance(value, str) else ""
177
+
178
+
179
+ def _bounded_summary(result: ToolResult) -> str:
180
+ compact = " ".join(_content(result).split())[:512]
181
+ return compact or "Manifest inspected; no textual summary was retained."
182
+
183
+
184
+ def _string_items(result: ToolResult, key: str) -> list[str]:
185
+ value = None if result.output is None else result.output.get(key)
186
+ return [item for item in value if isinstance(item, str)] if isinstance(value, list) else []
187
+
188
+
189
+ def _dict_items(result: ToolResult, key: str) -> list[dict[str, object]]:
190
+ value = None if result.output is None else result.output.get(key)
191
+ if not isinstance(value, list):
192
+ return []
193
+ return [dict(item) for item in value if isinstance(item, dict)]
194
+
195
+
196
+ def _truncated(result: ToolResult) -> bool:
197
+ return bool(result.output and result.output.get("truncated") is True)
198
+
199
+
200
+ def _task_terms(task: str) -> tuple[str, ...]:
201
+ ignored = {"the", "and", "for", "with", "from", "into", "this", "that"}
202
+ values: list[str] = []
203
+ for match in _TERM_PATTERN.findall(task):
204
+ value = match.strip("./")
205
+ if value.casefold() in ignored or value in values:
206
+ continue
207
+ values.append(value)
208
+ if len(values) == 8:
209
+ break
210
+ return tuple(values)
211
+
212
+
213
+ def _is_manifest(path: str) -> bool:
214
+ name = PurePosixPath(path).name.casefold()
215
+ return name in _MANIFEST_NAMES or name.startswith("readme")
216
+
217
+
218
+ def _is_candidate(path: str) -> bool:
219
+ parts = PurePosixPath(path).parts
220
+ return ".git" not in parts and not _is_manifest(path) and path != "AGENTS.md"
221
+
222
+
223
+ def _ancestor_instruction_paths(paths: tuple[str, ...]) -> tuple[str, ...]:
224
+ values: set[str] = set()
225
+ for path in paths:
226
+ parent = PurePosixPath(path).parent
227
+ while str(parent) not in {".", ""}:
228
+ values.add(str(parent / "AGENTS.md"))
229
+ parent = parent.parent
230
+ return tuple(sorted(values, key=lambda value: (len(PurePosixPath(value).parts), value)))
231
+
232
+
233
+ def _weakens_nexus_safety(content: str) -> bool:
234
+ normalized = " ".join(content.casefold().split())
235
+ forbidden = (
236
+ "ignore frozen",
237
+ "bypass security",
238
+ "disable approval",
239
+ "allow dangerous",
240
+ "ignore nexus safety",
241
+ )
242
+ return any(value in normalized for value in forbidden)
243
+
244
+
245
+ def _unique(values: list[str]) -> list[str]:
246
+ return list(dict.fromkeys(values))
@@ -0,0 +1,63 @@
1
+ """Compatibility bridge from Day 4 requests to the Day 5 ContextManager."""
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from dataclasses import replace
5
+
6
+ from nexus.domain.context import ContextRequest
7
+ from nexus.domain.exploration import ContextBuildRequest, WorkingContext
8
+ from nexus.domain.persistence import SessionTurn
9
+ from nexus.domain.ports.context import ContextManager
10
+ from nexus.domain.ports.skills import SkillRegistry, SkillSelector
11
+ from nexus.domain.skills import SelectedSkill
12
+
13
+
14
+ class ManagedContextBuilder:
15
+ def __init__(
16
+ self,
17
+ manager: ContextManager,
18
+ repository_id: Callable[[], Awaitable[str]],
19
+ workspace: str,
20
+ turns: Callable[[str], Awaitable[list[SessionTurn]]] | None = None,
21
+ skill_registry: SkillRegistry | None = None,
22
+ skill_selector: SkillSelector | None = None,
23
+ ) -> None:
24
+ self._manager = manager
25
+ self._repository_id = repository_id
26
+ self._workspace = workspace
27
+ self._turns = turns
28
+ self._skill_registry = skill_registry
29
+ self._skill_selector = skill_selector
30
+ if (skill_registry is None) is not (skill_selector is None):
31
+ raise ValueError("Skill registry and selector must be configured together.")
32
+
33
+ async def build(self, request: ContextBuildRequest) -> WorkingContext:
34
+ selection = None
35
+ selected_skills: tuple[SelectedSkill, ...] = ()
36
+ if self._skill_registry is not None and self._skill_selector is not None:
37
+ metadata = await self._skill_registry.scan_metadata()
38
+ selection = await self._skill_selector.select(
39
+ run_id=request.run_id,
40
+ task=request.task,
41
+ available_skills=metadata,
42
+ )
43
+ selected_skills = await self._skill_registry.load_selected(selection)
44
+ context = await self._manager.build(
45
+ ContextRequest(
46
+ request.task,
47
+ await self._repository_id(),
48
+ self._workspace,
49
+ request.exploration,
50
+ request.run_id,
51
+ request.session_id,
52
+ )
53
+ )
54
+ context = replace(
55
+ context,
56
+ selected_skills=selected_skills,
57
+ skill_selection_result=selection,
58
+ )
59
+ return await self._manager.prepare_agent_context(
60
+ working_context=context, plan=None, observations=(),
61
+ conversation_turns=(() if self._turns is None
62
+ else await self._turns(request.session_id)),
63
+ )