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.
- nexus/__init__.py +4 -0
- nexus/application/__init__.py +2 -0
- nexus/application/approval_service.py +130 -0
- nexus/application/diff_service.py +177 -0
- nexus/application/event_publisher.py +180 -0
- nexus/application/execution_context.py +29 -0
- nexus/application/execution_ledger.py +126 -0
- nexus/application/model_call_context.py +29 -0
- nexus/application/observed_model_gateway.py +194 -0
- nexus/application/plan_approval_service.py +250 -0
- nexus/application/planning.py +668 -0
- nexus/application/runtime.py +624 -0
- nexus/application/session_service.py +411 -0
- nexus/application/structured_output.py +96 -0
- nexus/application/telemetry.py +396 -0
- nexus/application/telemetry_subscriber.py +216 -0
- nexus/application/tool_runtime.py +496 -0
- nexus/application/tracing_dispatcher.py +283 -0
- nexus/application/validation.py +293 -0
- nexus/config/__init__.py +17 -0
- nexus/config/loader.py +253 -0
- nexus/config/models.py +310 -0
- nexus/context/__init__.py +6 -0
- nexus/context/budget.py +24 -0
- nexus/context/builder.py +92 -0
- nexus/context/chunking.py +65 -0
- nexus/context/explorer.py +246 -0
- nexus/context/integration.py +63 -0
- nexus/context/manager.py +328 -0
- nexus/context/retrieval.py +261 -0
- nexus/domain/__init__.py +2 -0
- nexus/domain/agent_decision.py +113 -0
- nexus/domain/agent_state.py +49 -0
- nexus/domain/approvals.py +64 -0
- nexus/domain/context.py +108 -0
- nexus/domain/exploration.py +86 -0
- nexus/domain/mcp.py +51 -0
- nexus/domain/model.py +173 -0
- nexus/domain/observability.py +326 -0
- nexus/domain/persistence.py +122 -0
- nexus/domain/planning.py +329 -0
- nexus/domain/ports/__init__.py +61 -0
- nexus/domain/ports/agent_decision.py +9 -0
- nexus/domain/ports/approval_unit_of_work.py +27 -0
- nexus/domain/ports/checkpoint_provider.py +11 -0
- nexus/domain/ports/context.py +71 -0
- nexus/domain/ports/events.py +21 -0
- nexus/domain/ports/graph_runtime.py +30 -0
- nexus/domain/ports/mcp.py +23 -0
- nexus/domain/ports/model_gateway.py +21 -0
- nexus/domain/ports/model_input_budget.py +15 -0
- nexus/domain/ports/planning.py +62 -0
- nexus/domain/ports/repositories.py +54 -0
- nexus/domain/ports/repository_context.py +18 -0
- nexus/domain/ports/session_unit_of_work.py +33 -0
- nexus/domain/ports/skills.py +41 -0
- nexus/domain/ports/tooling.py +39 -0
- nexus/domain/ports/tracer.py +13 -0
- nexus/domain/ports/validation.py +32 -0
- nexus/domain/runtime_events.py +445 -0
- nexus/domain/skills.py +129 -0
- nexus/domain/tooling.py +163 -0
- nexus/domain/validation.py +161 -0
- nexus/errors/__init__.py +82 -0
- nexus/evaluation/__init__.py +7 -0
- nexus/evaluation/assertions.py +4 -0
- nexus/evaluation/evaluator.py +263 -0
- nexus/evaluation/fixtures.py +36 -0
- nexus/evaluation/harness.py +606 -0
- nexus/evaluation/loader.py +415 -0
- nexus/evaluation/metrics.py +27 -0
- nexus/evaluation/models.py +362 -0
- nexus/evaluation/report.py +43 -0
- nexus/evaluation/runner.py +480 -0
- nexus/infrastructure/__init__.py +2 -0
- nexus/infrastructure/asyncio_compat.py +13 -0
- nexus/infrastructure/bootstrap/__init__.py +17 -0
- nexus/infrastructure/bootstrap/composition.py +663 -0
- nexus/infrastructure/checkpoint/__init__.py +5 -0
- nexus/infrastructure/checkpoint/postgres.py +62 -0
- nexus/infrastructure/database.py +39 -0
- nexus/infrastructure/embedding.py +61 -0
- nexus/infrastructure/graph/__init__.py +6 -0
- nexus/infrastructure/graph/day4_runtime.py +966 -0
- nexus/infrastructure/graph/langgraph_runtime.py +172 -0
- nexus/infrastructure/mcp/__init__.py +5 -0
- nexus/infrastructure/mcp/manager.py +380 -0
- nexus/infrastructure/model_gateway/__init__.py +6 -0
- nexus/infrastructure/model_gateway/openai_compatible.py +345 -0
- nexus/infrastructure/observability/__init__.py +7 -0
- nexus/infrastructure/observability/console.py +31 -0
- nexus/infrastructure/observability/langsmith.py +236 -0
- nexus/infrastructure/observability/structured_logger.py +70 -0
- nexus/infrastructure/persistence/__init__.py +13 -0
- nexus/infrastructure/persistence/approval_unit_of_work.py +74 -0
- nexus/infrastructure/persistence/models.py +146 -0
- nexus/infrastructure/persistence/repositories.py +326 -0
- nexus/infrastructure/persistence/unit_of_work.py +82 -0
- nexus/infrastructure/repository_files.py +117 -0
- nexus/infrastructure/sandbox/__init__.py +5 -0
- nexus/infrastructure/sandbox/local_process.py +353 -0
- nexus/infrastructure/semantic.py +276 -0
- nexus/infrastructure/semantic_models.py +56 -0
- nexus/interfaces/__init__.py +2 -0
- nexus/interfaces/cli/__init__.py +2 -0
- nexus/interfaces/cli/__main__.py +6 -0
- nexus/interfaces/cli/app.py +266 -0
- nexus/interfaces/cli/renderer.py +118 -0
- nexus/security/__init__.py +14 -0
- nexus/security/approval_policies.py +73 -0
- nexus/security/command_policy.py +117 -0
- nexus/security/executables.py +107 -0
- nexus/security/git_commands.py +65 -0
- nexus/security/workspace.py +93 -0
- nexus/skills/__init__.py +7 -0
- nexus/skills/builtin/__init__.py +1 -0
- nexus/skills/builtin/debug-python/SKILL.md +26 -0
- nexus/skills/builtin/review-repository/SKILL.md +26 -0
- nexus/skills/builtin/write-tests/SKILL.md +26 -0
- nexus/skills/loader.py +439 -0
- nexus/skills/registry.py +146 -0
- nexus/skills/selector.py +152 -0
- nexus/tools/__init__.py +6 -0
- nexus/tools/editing.py +312 -0
- nexus/tools/mcp.py +160 -0
- nexus/tools/native.py +654 -0
- nexus/tools/registry.py +27 -0
- nexus_coding_agent-0.1.0.dist-info/METADATA +142 -0
- nexus_coding_agent-0.1.0.dist-info/RECORD +131 -0
- nexus_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- nexus_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Transactional exact pgvector retrieval and incremental indexing."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from uuid import UUID, uuid4
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import delete, func, select, text
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
10
|
+
|
|
11
|
+
from nexus.context.chunking import language_for, text_hash
|
|
12
|
+
from nexus.domain.context import (
|
|
13
|
+
CodeChunk,
|
|
14
|
+
ContextCandidate,
|
|
15
|
+
EmbeddingConfig,
|
|
16
|
+
IndexCompatibility,
|
|
17
|
+
IndexCompatibilityStatus,
|
|
18
|
+
IndexRequest,
|
|
19
|
+
IndexResult,
|
|
20
|
+
RetrievalQuery,
|
|
21
|
+
RetrievalSource,
|
|
22
|
+
)
|
|
23
|
+
from nexus.domain.ports.context import Chunker, EmbeddingGateway
|
|
24
|
+
from nexus.errors import ContextError
|
|
25
|
+
from nexus.infrastructure.embedding import validate_vectors
|
|
26
|
+
from nexus.infrastructure.repository_files import RepositoryFiles
|
|
27
|
+
from nexus.infrastructure.semantic_models import SemanticChunkRow, SemanticIndexRow
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _compatibility(row: SemanticIndexRow | None, config: EmbeddingConfig) -> IndexCompatibility:
|
|
31
|
+
if row is None:
|
|
32
|
+
return IndexCompatibility(
|
|
33
|
+
IndexCompatibilityStatus.INDEX_NOT_FOUND,
|
|
34
|
+
"Run nexus index to initialize semantic search.",
|
|
35
|
+
)
|
|
36
|
+
expected = (config.provider, config.model, config.dimension, "v1", "line_window_v1")
|
|
37
|
+
actual = (
|
|
38
|
+
row.embedding_provider,
|
|
39
|
+
row.embedding_model,
|
|
40
|
+
row.embedding_dimension,
|
|
41
|
+
row.index_version,
|
|
42
|
+
row.chunk_strategy_version,
|
|
43
|
+
)
|
|
44
|
+
if expected != actual:
|
|
45
|
+
return IndexCompatibility(
|
|
46
|
+
IndexCompatibilityStatus.INDEX_INCOMPATIBLE,
|
|
47
|
+
"Embedding/index configuration changed; run nexus index --rebuild.",
|
|
48
|
+
)
|
|
49
|
+
return IndexCompatibility(IndexCompatibilityStatus.COMPATIBLE, None)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def _lock(session: AsyncSession, repository_id: str, *, shared: bool = False) -> None:
|
|
53
|
+
operation = "pg_advisory_xact_lock_shared" if shared else "pg_advisory_xact_lock"
|
|
54
|
+
await session.execute(
|
|
55
|
+
text(f"SELECT {operation}(hashtextextended(:repository_id, 0))"),
|
|
56
|
+
{"repository_id": repository_id},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PgVectorSemanticSearchProvider:
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
sessions: async_sessionmaker[AsyncSession],
|
|
64
|
+
embedding: EmbeddingGateway,
|
|
65
|
+
) -> None:
|
|
66
|
+
self._sessions = sessions
|
|
67
|
+
self._embedding = embedding
|
|
68
|
+
|
|
69
|
+
async def validate_index(self, repository_id: str) -> IndexCompatibility:
|
|
70
|
+
try:
|
|
71
|
+
async with self._sessions() as session:
|
|
72
|
+
return _compatibility(
|
|
73
|
+
await session.get(SemanticIndexRow, UUID(repository_id)),
|
|
74
|
+
self._embedding.config,
|
|
75
|
+
)
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
raise ContextError(
|
|
78
|
+
"Semantic index validation is unavailable.",
|
|
79
|
+
code="SEMANTIC_SEARCH_FAILED",
|
|
80
|
+
retryable=True,
|
|
81
|
+
) from exc
|
|
82
|
+
|
|
83
|
+
async def search(self, query: RetrievalQuery) -> tuple[ContextCandidate, ...]:
|
|
84
|
+
try:
|
|
85
|
+
async with self._sessions() as session, session.begin():
|
|
86
|
+
await _lock(session, query.repository_id, shared=True)
|
|
87
|
+
compatible = _compatibility(
|
|
88
|
+
await session.get(SemanticIndexRow, UUID(query.repository_id)),
|
|
89
|
+
self._embedding.config,
|
|
90
|
+
)
|
|
91
|
+
if compatible.status is not IndexCompatibilityStatus.COMPATIBLE:
|
|
92
|
+
raise ContextError(
|
|
93
|
+
compatible.reason or "Index unavailable.", code=compatible.status.value
|
|
94
|
+
)
|
|
95
|
+
vector = validate_vectors(
|
|
96
|
+
(await self._embedding.embed_query(query.text),),
|
|
97
|
+
1,
|
|
98
|
+
self._embedding.config.dimension,
|
|
99
|
+
)[0]
|
|
100
|
+
distance = SemanticChunkRow.embedding.cosine_distance(list(vector))
|
|
101
|
+
rows = (
|
|
102
|
+
await session.execute(
|
|
103
|
+
select(SemanticChunkRow, distance.label("distance"))
|
|
104
|
+
.where(SemanticChunkRow.repository_id == UUID(query.repository_id))
|
|
105
|
+
.order_by(
|
|
106
|
+
distance,
|
|
107
|
+
SemanticChunkRow.file_path,
|
|
108
|
+
SemanticChunkRow.start_line,
|
|
109
|
+
SemanticChunkRow.content_hash,
|
|
110
|
+
)
|
|
111
|
+
.limit(min(query.limit, 20)),
|
|
112
|
+
)
|
|
113
|
+
).all()
|
|
114
|
+
return tuple(
|
|
115
|
+
ContextCandidate(
|
|
116
|
+
CodeChunk(
|
|
117
|
+
row.file_path,
|
|
118
|
+
row.language,
|
|
119
|
+
row.symbol,
|
|
120
|
+
row.start_line,
|
|
121
|
+
row.end_line,
|
|
122
|
+
row.content,
|
|
123
|
+
row.content_hash,
|
|
124
|
+
row.file_hash,
|
|
125
|
+
),
|
|
126
|
+
None,
|
|
127
|
+
rank,
|
|
128
|
+
float(score),
|
|
129
|
+
0.0,
|
|
130
|
+
(RetrievalSource.SEMANTIC,),
|
|
131
|
+
)
|
|
132
|
+
for rank, (row, score) in enumerate(rows, 1)
|
|
133
|
+
)
|
|
134
|
+
except ContextError:
|
|
135
|
+
raise
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
raise ContextError(
|
|
138
|
+
"Semantic query failed.", code="SEMANTIC_SEARCH_FAILED", retryable=True
|
|
139
|
+
) from exc
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class PgVectorRepositoryIndexer:
|
|
143
|
+
def __init__(
|
|
144
|
+
self,
|
|
145
|
+
sessions: async_sessionmaker[AsyncSession],
|
|
146
|
+
embedding: EmbeddingGateway,
|
|
147
|
+
chunker: Chunker,
|
|
148
|
+
files: RepositoryFiles,
|
|
149
|
+
) -> None:
|
|
150
|
+
self._sessions = sessions
|
|
151
|
+
self._embedding = embedding
|
|
152
|
+
self._chunker = chunker
|
|
153
|
+
self._files = files
|
|
154
|
+
|
|
155
|
+
async def index(self, request: IndexRequest) -> IndexResult:
|
|
156
|
+
try:
|
|
157
|
+
workspace = await asyncio.to_thread(Path(request.workspace_path).resolve)
|
|
158
|
+
if self._files.guard.root != workspace:
|
|
159
|
+
raise ValueError("Index workspace mismatch.")
|
|
160
|
+
async with self._sessions() as session, session.begin():
|
|
161
|
+
await _lock(session, request.repository_id)
|
|
162
|
+
repository = UUID(request.repository_id)
|
|
163
|
+
meta = await session.get(SemanticIndexRow, repository)
|
|
164
|
+
compatible = _compatibility(meta, self._embedding.config)
|
|
165
|
+
if (
|
|
166
|
+
compatible.status is IndexCompatibilityStatus.INDEX_INCOMPATIBLE
|
|
167
|
+
and not request.rebuild
|
|
168
|
+
):
|
|
169
|
+
raise ContextError(
|
|
170
|
+
compatible.reason or "Rebuild required.", code="INDEX_INCOMPATIBLE"
|
|
171
|
+
)
|
|
172
|
+
scan = await asyncio.to_thread(self._files.scan)
|
|
173
|
+
old = {
|
|
174
|
+
path: file_hash
|
|
175
|
+
for path, file_hash in (
|
|
176
|
+
await session.execute(
|
|
177
|
+
select(SemanticChunkRow.file_path, SemanticChunkRow.file_hash)
|
|
178
|
+
.where(SemanticChunkRow.repository_id == repository)
|
|
179
|
+
.distinct(),
|
|
180
|
+
)
|
|
181
|
+
).all()
|
|
182
|
+
}
|
|
183
|
+
current = dict(scan.files)
|
|
184
|
+
removed = set(old) - current.keys()
|
|
185
|
+
changed = {
|
|
186
|
+
path: content
|
|
187
|
+
for path, content in current.items()
|
|
188
|
+
if request.rebuild or old.get(path) != text_hash(content)
|
|
189
|
+
}
|
|
190
|
+
unchanged = len(current) - len(changed)
|
|
191
|
+
if request.rebuild:
|
|
192
|
+
await session.execute(
|
|
193
|
+
delete(SemanticChunkRow).where(
|
|
194
|
+
SemanticChunkRow.repository_id == repository,
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
await session.execute(
|
|
199
|
+
delete(SemanticChunkRow).where(
|
|
200
|
+
SemanticChunkRow.repository_id == repository,
|
|
201
|
+
SemanticChunkRow.file_path.in_(set(changed) | removed),
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
now = datetime.now(UTC)
|
|
205
|
+
for path, content in changed.items():
|
|
206
|
+
chunks = self._chunker.chunk(
|
|
207
|
+
file_path=path,
|
|
208
|
+
language=language_for(path),
|
|
209
|
+
content=content,
|
|
210
|
+
file_hash=text_hash(content),
|
|
211
|
+
)
|
|
212
|
+
for offset in range(0, len(chunks), 32):
|
|
213
|
+
batch = chunks[offset : offset + 32]
|
|
214
|
+
vectors = validate_vectors(
|
|
215
|
+
await self._embedding.embed_documents([c.content for c in batch]),
|
|
216
|
+
len(batch),
|
|
217
|
+
self._embedding.config.dimension,
|
|
218
|
+
)
|
|
219
|
+
for chunk, vector in zip(batch, vectors, strict=True):
|
|
220
|
+
session.add(
|
|
221
|
+
SemanticChunkRow(
|
|
222
|
+
id=uuid4(),
|
|
223
|
+
repository_id=repository,
|
|
224
|
+
file_path=chunk.file_path,
|
|
225
|
+
language=chunk.language,
|
|
226
|
+
symbol=chunk.symbol,
|
|
227
|
+
start_line=chunk.start_line,
|
|
228
|
+
end_line=chunk.end_line,
|
|
229
|
+
content=chunk.content,
|
|
230
|
+
content_hash=chunk.content_hash,
|
|
231
|
+
file_hash=chunk.file_hash,
|
|
232
|
+
embedding=list(vector),
|
|
233
|
+
created_at=now,
|
|
234
|
+
updated_at=now,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
config = self._embedding.config
|
|
238
|
+
if meta is None:
|
|
239
|
+
meta = SemanticIndexRow(repository_id=repository)
|
|
240
|
+
session.add(meta)
|
|
241
|
+
meta.embedding_provider = config.provider
|
|
242
|
+
meta.embedding_model = config.model
|
|
243
|
+
meta.embedding_dimension = config.dimension
|
|
244
|
+
meta.index_version = "v1"
|
|
245
|
+
meta.chunk_strategy_version = "line_window_v1"
|
|
246
|
+
meta.indexed_at = now
|
|
247
|
+
await session.flush()
|
|
248
|
+
count = await session.scalar(
|
|
249
|
+
select(func.count())
|
|
250
|
+
.select_from(SemanticChunkRow)
|
|
251
|
+
.where(SemanticChunkRow.repository_id == repository)
|
|
252
|
+
)
|
|
253
|
+
return IndexResult(
|
|
254
|
+
request.repository_id,
|
|
255
|
+
scan.scanned,
|
|
256
|
+
len(changed),
|
|
257
|
+
unchanged,
|
|
258
|
+
len(removed),
|
|
259
|
+
scan.skipped,
|
|
260
|
+
int(count or 0),
|
|
261
|
+
request.rebuild,
|
|
262
|
+
)
|
|
263
|
+
except ContextError as exc:
|
|
264
|
+
if exc.code == "INDEX_INCOMPATIBLE":
|
|
265
|
+
raise
|
|
266
|
+
raise ContextError(
|
|
267
|
+
"Index build failed; previous index transaction is preserved.",
|
|
268
|
+
code="INDEX_BUILD_FAILED",
|
|
269
|
+
retryable=True,
|
|
270
|
+
) from exc
|
|
271
|
+
except Exception as exc:
|
|
272
|
+
raise ContextError(
|
|
273
|
+
"Index build failed; previous index transaction is preserved.",
|
|
274
|
+
code="INDEX_BUILD_FAILED",
|
|
275
|
+
retryable=True,
|
|
276
|
+
) from exc
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Alembic-owned semantic tables, separate from checkpoint persistence."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from pgvector.sqlalchemy import VECTOR
|
|
7
|
+
from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Index, Text, UniqueConstraint
|
|
8
|
+
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
|
9
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
10
|
+
|
|
11
|
+
from nexus.infrastructure.persistence.models import Base
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SemanticIndexRow(Base):
|
|
15
|
+
__tablename__ = "repository_semantic_indexes"
|
|
16
|
+
repository_id: Mapped[UUID] = mapped_column(
|
|
17
|
+
PGUUID(as_uuid=True),
|
|
18
|
+
ForeignKey("repositories.id"),
|
|
19
|
+
primary_key=True,
|
|
20
|
+
)
|
|
21
|
+
embedding_provider: Mapped[str] = mapped_column(Text)
|
|
22
|
+
embedding_model: Mapped[str] = mapped_column(Text)
|
|
23
|
+
embedding_dimension: Mapped[int]
|
|
24
|
+
index_version: Mapped[str] = mapped_column(Text)
|
|
25
|
+
chunk_strategy_version: Mapped[str] = mapped_column(Text)
|
|
26
|
+
indexed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SemanticChunkRow(Base):
|
|
30
|
+
__tablename__ = "semantic_code_chunks"
|
|
31
|
+
__table_args__ = (
|
|
32
|
+
CheckConstraint("start_line >= 1", name="ck_semantic_start_line"),
|
|
33
|
+
CheckConstraint("end_line >= start_line", name="ck_semantic_end_line"),
|
|
34
|
+
UniqueConstraint(
|
|
35
|
+
"repository_id", "file_path", "start_line", "end_line", name="uq_semantic_file_window"
|
|
36
|
+
),
|
|
37
|
+
Index("ix_semantic_repository", "repository_id"),
|
|
38
|
+
Index("ix_semantic_repository_file", "repository_id", "file_path"),
|
|
39
|
+
Index("ix_semantic_repository_hash", "repository_id", "content_hash"),
|
|
40
|
+
)
|
|
41
|
+
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
|
|
42
|
+
repository_id: Mapped[UUID] = mapped_column(
|
|
43
|
+
PGUUID(as_uuid=True),
|
|
44
|
+
ForeignKey("repositories.id"),
|
|
45
|
+
)
|
|
46
|
+
file_path: Mapped[str] = mapped_column(Text)
|
|
47
|
+
language: Mapped[str] = mapped_column(Text)
|
|
48
|
+
symbol: Mapped[str | None] = mapped_column(Text)
|
|
49
|
+
start_line: Mapped[int]
|
|
50
|
+
end_line: Mapped[int]
|
|
51
|
+
content: Mapped[str] = mapped_column(Text)
|
|
52
|
+
content_hash: Mapped[str] = mapped_column(Text)
|
|
53
|
+
file_hash: Mapped[str] = mapped_column(Text)
|
|
54
|
+
embedding: Mapped[list[float]] = mapped_column(VECTOR())
|
|
55
|
+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
|
56
|
+
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Thin CLI adapter for the approved Day 1 command surface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from nexus.application.runtime import NexusRuntime
|
|
13
|
+
from nexus.config import load_runtime_config
|
|
14
|
+
from nexus.config.models import RuntimeConfig
|
|
15
|
+
from nexus.domain.persistence import SessionSummary
|
|
16
|
+
from nexus.domain.planning import PlanApprovalResumeInput
|
|
17
|
+
from nexus.domain.runtime_events import (
|
|
18
|
+
ApprovalRequested,
|
|
19
|
+
ApprovalSubject,
|
|
20
|
+
RunInterrupted,
|
|
21
|
+
RuntimeEvent,
|
|
22
|
+
)
|
|
23
|
+
from nexus.domain.tooling import ApprovalDecision
|
|
24
|
+
from nexus.errors import NexusError
|
|
25
|
+
from nexus.evaluation.loader import EvalSuiteLoader
|
|
26
|
+
from nexus.evaluation.models import EvalOutcome, EvalSuiteLoadResult
|
|
27
|
+
from nexus.evaluation.report import write_report
|
|
28
|
+
from nexus.infrastructure.bootstrap import bootstrap_application
|
|
29
|
+
from nexus.infrastructure.bootstrap.composition import (
|
|
30
|
+
build_evaluation_runner,
|
|
31
|
+
index_repository,
|
|
32
|
+
list_repository_sessions,
|
|
33
|
+
)
|
|
34
|
+
from nexus.interfaces.cli.renderer import render_event
|
|
35
|
+
|
|
36
|
+
app = typer.Typer(
|
|
37
|
+
name="nexus",
|
|
38
|
+
help="Transparent and extensible Nexus V1 coding-agent runtime.",
|
|
39
|
+
add_completion=False,
|
|
40
|
+
invoke_without_command=True,
|
|
41
|
+
no_args_is_help=False,
|
|
42
|
+
)
|
|
43
|
+
session_app = typer.Typer(help="List and resume durable Nexus sessions.")
|
|
44
|
+
app.add_typer(session_app, name="session")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.callback()
|
|
48
|
+
def root(context: typer.Context) -> None:
|
|
49
|
+
"""Show help successfully when no subcommand is supplied."""
|
|
50
|
+
|
|
51
|
+
if context.invoked_subcommand is None:
|
|
52
|
+
typer.echo(context.get_help())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command()
|
|
56
|
+
def chat(
|
|
57
|
+
task: Annotated[
|
|
58
|
+
str,
|
|
59
|
+
typer.Argument(
|
|
60
|
+
help="Non-empty task to send to the configured model.",
|
|
61
|
+
metavar="TASK",
|
|
62
|
+
),
|
|
63
|
+
],
|
|
64
|
+
model: Annotated[
|
|
65
|
+
str | None,
|
|
66
|
+
typer.Option("--model", help="Override the configured model name."),
|
|
67
|
+
] = None,
|
|
68
|
+
base_url: Annotated[
|
|
69
|
+
str | None,
|
|
70
|
+
typer.Option("--base-url", help="Override the OpenAI-compatible base URL."),
|
|
71
|
+
] = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Run a coding task through the Nexus V1 runtime."""
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
config = load_runtime_config(cli_model=model, cli_base_url=base_url)
|
|
77
|
+
succeeded = asyncio.run(_run_chat(config, task))
|
|
78
|
+
except NexusError as exc:
|
|
79
|
+
typer.echo(f"Error [{exc.code}]: {exc}", err=True)
|
|
80
|
+
raise typer.Exit(code=1) from exc
|
|
81
|
+
if not succeeded:
|
|
82
|
+
raise typer.Exit(code=1)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def _run_chat(config: RuntimeConfig, task: str) -> bool:
|
|
86
|
+
async with bootstrap_application(config) as application:
|
|
87
|
+
return await _consume_with_plan_approval(
|
|
88
|
+
application.runtime.run(task),
|
|
89
|
+
application.runtime,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@session_app.command("list")
|
|
94
|
+
def list_sessions() -> None:
|
|
95
|
+
"""List sessions owned by the current canonical repository."""
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
config = load_runtime_config()
|
|
99
|
+
summaries = asyncio.run(_list_sessions(config))
|
|
100
|
+
except NexusError as exc:
|
|
101
|
+
typer.echo(f"Error [{exc.code}]: {exc}", err=True)
|
|
102
|
+
raise typer.Exit(code=1) from exc
|
|
103
|
+
_render_session_summaries(summaries)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@app.command("index")
|
|
107
|
+
def index(
|
|
108
|
+
rebuild: Annotated[bool, typer.Option("--rebuild", help="Rebuild this repository's index.")]
|
|
109
|
+
= False,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Incrementally index the current repository for semantic retrieval."""
|
|
112
|
+
try:
|
|
113
|
+
config = load_runtime_config()
|
|
114
|
+
result = asyncio.run(index_repository(config, rebuild=rebuild))
|
|
115
|
+
except NexusError as exc:
|
|
116
|
+
typer.echo(f"Error [{exc.code}]: {exc}", err=True)
|
|
117
|
+
raise typer.Exit(code=1) from exc
|
|
118
|
+
typer.echo(
|
|
119
|
+
f"scanned files: {result.scanned_files}; indexed/changed files: {result.indexed_files}; "
|
|
120
|
+
f"unchanged files: {result.unchanged_files}; removed files: {result.removed_files}; "
|
|
121
|
+
f"skipped files: {result.skipped_files}; chunk count: {result.chunk_count}; "
|
|
122
|
+
f"embedding: {config.embedding_provider}/{config.embedding_model}"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.command("eval")
|
|
127
|
+
def evaluate(
|
|
128
|
+
case_id: Annotated[
|
|
129
|
+
str | None,
|
|
130
|
+
typer.Option("--case", help="Run one mandatory evaluation case."),
|
|
131
|
+
] = None,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Run the deterministic Day 9 evaluation suite sequentially."""
|
|
134
|
+
|
|
135
|
+
cases_root = Path.cwd() / "evals" / "cases"
|
|
136
|
+
loader = EvalSuiteLoader()
|
|
137
|
+
if case_id is None:
|
|
138
|
+
load_result = loader.load_suite(cases_root)
|
|
139
|
+
else:
|
|
140
|
+
source = cases_root / case_id / "case.toml"
|
|
141
|
+
if not source.is_file():
|
|
142
|
+
typer.echo(f"Unknown evaluation case: {case_id}", err=True)
|
|
143
|
+
raise typer.Exit(code=1)
|
|
144
|
+
try:
|
|
145
|
+
selected = loader.load_case(source)
|
|
146
|
+
load_result = EvalSuiteLoadResult((selected,), ())
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
typer.echo(f"Invalid evaluation case {case_id}: {exc}", err=True)
|
|
149
|
+
raise typer.Exit(code=1) from exc
|
|
150
|
+
try:
|
|
151
|
+
config = load_runtime_config()
|
|
152
|
+
report = asyncio.run(build_evaluation_runner(config, cases_root).run_suite(load_result))
|
|
153
|
+
report_path, baseline = write_report(
|
|
154
|
+
report,
|
|
155
|
+
Path.cwd() / "evals" / "reports",
|
|
156
|
+
write_baseline=case_id is None,
|
|
157
|
+
)
|
|
158
|
+
except NexusError as exc:
|
|
159
|
+
typer.echo(f"Error [{exc.code}]: {exc}", err=True)
|
|
160
|
+
raise typer.Exit(code=1) from exc
|
|
161
|
+
for case_report in report.cases:
|
|
162
|
+
typer.echo(f"{case_report.case_id}: {case_report.outcome.value}")
|
|
163
|
+
for error in report.suite_errors:
|
|
164
|
+
typer.echo(f"Suite error [{error.code}]: {error.message}", err=True)
|
|
165
|
+
typer.echo(f"JSON report: {report_path}")
|
|
166
|
+
if baseline is not None:
|
|
167
|
+
typer.echo(f"Initial baseline: {baseline}")
|
|
168
|
+
if report.suite_errors or any(item.outcome is not EvalOutcome.PASS for item in report.cases):
|
|
169
|
+
raise typer.Exit(code=1)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@session_app.command()
|
|
173
|
+
def resume(
|
|
174
|
+
session_id: Annotated[
|
|
175
|
+
str,
|
|
176
|
+
typer.Argument(help="Session UUID to resume.", metavar="SESSION_ID"),
|
|
177
|
+
],
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Resume the latest interrupted run for a current-repository session."""
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
config = load_runtime_config()
|
|
183
|
+
succeeded = asyncio.run(_run_resume(config, session_id))
|
|
184
|
+
except NexusError as exc:
|
|
185
|
+
typer.echo(f"Error [{exc.code}]: {exc}", err=True)
|
|
186
|
+
raise typer.Exit(code=1) from exc
|
|
187
|
+
if not succeeded:
|
|
188
|
+
raise typer.Exit(code=1)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
async def _list_sessions(config: RuntimeConfig) -> list[SessionSummary]:
|
|
192
|
+
return await list_repository_sessions(config)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
async def _run_resume(config: RuntimeConfig, session_id: str) -> bool:
|
|
196
|
+
async with bootstrap_application(config) as application:
|
|
197
|
+
return await _consume_with_plan_approval(
|
|
198
|
+
application.runtime.resume(session_id),
|
|
199
|
+
application.runtime,
|
|
200
|
+
requested_session_id=session_id,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def _consume_with_plan_approval(
|
|
205
|
+
events: AsyncIterator[RuntimeEvent],
|
|
206
|
+
runtime: NexusRuntime,
|
|
207
|
+
*,
|
|
208
|
+
requested_session_id: str | None = None,
|
|
209
|
+
) -> bool:
|
|
210
|
+
succeeded = True
|
|
211
|
+
current_events = events
|
|
212
|
+
session_id = requested_session_id
|
|
213
|
+
while True:
|
|
214
|
+
pending: ApprovalRequested | None = None
|
|
215
|
+
interrupted = False
|
|
216
|
+
async for event in current_events:
|
|
217
|
+
succeeded = render_event(event) and succeeded
|
|
218
|
+
if event.session_id is not None:
|
|
219
|
+
session_id = event.session_id
|
|
220
|
+
if (
|
|
221
|
+
isinstance(event, ApprovalRequested)
|
|
222
|
+
and event.subject is ApprovalSubject.PLAN
|
|
223
|
+
):
|
|
224
|
+
pending = event
|
|
225
|
+
interrupted = interrupted or isinstance(event, RunInterrupted)
|
|
226
|
+
if pending is None:
|
|
227
|
+
return succeeded
|
|
228
|
+
if not interrupted or session_id is None:
|
|
229
|
+
raise NexusError(
|
|
230
|
+
"Plan approval was not paired with a durable interrupt.",
|
|
231
|
+
code="GRAPH_INVALID_STATE",
|
|
232
|
+
)
|
|
233
|
+
resume_input = _collect_plan_decision()
|
|
234
|
+
current_events = runtime.resume(
|
|
235
|
+
session_id,
|
|
236
|
+
resume_input=resume_input,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _collect_plan_decision() -> PlanApprovalResumeInput:
|
|
241
|
+
while True:
|
|
242
|
+
value = typer.prompt("Plan decision (APPROVED/DENIED)").strip().upper()
|
|
243
|
+
try:
|
|
244
|
+
decision = ApprovalDecision(value)
|
|
245
|
+
return PlanApprovalResumeInput(decision, None)
|
|
246
|
+
except ValueError:
|
|
247
|
+
typer.echo("Enter APPROVED or DENIED.", err=True)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _render_session_summaries(summaries: list[SessionSummary]) -> None:
|
|
251
|
+
typer.echo("session_id\tlast_active_at\tresumable")
|
|
252
|
+
for summary in summaries:
|
|
253
|
+
typer.echo(
|
|
254
|
+
f"{summary.session_id}\t{summary.last_active_at.isoformat()}\t"
|
|
255
|
+
f"{'yes' if summary.resumable else 'no'}"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def main() -> None:
|
|
260
|
+
"""Installed console-script entry point."""
|
|
261
|
+
|
|
262
|
+
app(prog_name="nexus")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
if __name__ == "__main__":
|
|
266
|
+
main()
|