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
nexus/domain/planning.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Nexus-owned Day 4 planning, authorization, change, and terminal values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
from pathlib import PurePosixPath
|
|
11
|
+
from uuid import UUID
|
|
12
|
+
|
|
13
|
+
from nexus.domain.tooling import ApprovalDecision
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PlanKind(StrEnum):
|
|
17
|
+
INITIAL = "INITIAL"
|
|
18
|
+
REPLAN = "REPLAN"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PlanStatus(StrEnum):
|
|
22
|
+
CREATED = "CREATED"
|
|
23
|
+
ACTIVE = "ACTIVE"
|
|
24
|
+
SUPERSEDED = "SUPERSEDED"
|
|
25
|
+
COMPLETED = "COMPLETED"
|
|
26
|
+
FAILED = "FAILED"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PlanStepStatus(StrEnum):
|
|
30
|
+
PENDING = "PENDING"
|
|
31
|
+
IN_PROGRESS = "IN_PROGRESS"
|
|
32
|
+
COMPLETED = "COMPLETED"
|
|
33
|
+
SKIPPED = "SKIPPED"
|
|
34
|
+
FAILED = "FAILED"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PlanAuthorizationSource(StrEnum):
|
|
38
|
+
INTERACTIVE = "INTERACTIVE"
|
|
39
|
+
AUTO_MODE = "AUTO_MODE"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ChangeKind(StrEnum):
|
|
43
|
+
MODIFIED = "MODIFIED"
|
|
44
|
+
ADDED = "ADDED"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TerminalStatus(StrEnum):
|
|
48
|
+
SUCCEEDED = "SUCCEEDED"
|
|
49
|
+
FAILED = "FAILED"
|
|
50
|
+
FAILED_APPROVAL_DENIED = "FAILED_APPROVAL_DENIED"
|
|
51
|
+
FAILED_VALIDATION = "FAILED_VALIDATION"
|
|
52
|
+
FAILED_VALIDATION_UNKNOWN = "FAILED_VALIDATION_UNKNOWN"
|
|
53
|
+
STOPPED_MAX_STEPS = "STOPPED_MAX_STEPS"
|
|
54
|
+
STOPPED_MAX_REPAIRS = "STOPPED_MAX_REPAIRS"
|
|
55
|
+
STOPPED_MAX_REPLANS = "STOPPED_MAX_REPLANS"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class PlanStep:
|
|
60
|
+
step_id: str
|
|
61
|
+
sequence: int
|
|
62
|
+
description: str
|
|
63
|
+
tool_name: str | None
|
|
64
|
+
target_paths: tuple[str, ...]
|
|
65
|
+
command_argv: tuple[str, ...] | None
|
|
66
|
+
command_cwd: str | None
|
|
67
|
+
status: PlanStepStatus
|
|
68
|
+
|
|
69
|
+
def __post_init__(self) -> None:
|
|
70
|
+
_validate_uuid(self.step_id, "step_id")
|
|
71
|
+
if self.sequence < 1 or not self.description.strip():
|
|
72
|
+
raise ValueError("PlanStep sequence and description are invalid.")
|
|
73
|
+
paths = tuple(self.target_paths)
|
|
74
|
+
if paths != tuple(sorted(set(paths))):
|
|
75
|
+
raise ValueError("PlanStep target_paths must be sorted and unique.")
|
|
76
|
+
for path in paths:
|
|
77
|
+
_validate_repository_path(path)
|
|
78
|
+
object.__setattr__(self, "target_paths", paths)
|
|
79
|
+
argv = None if self.command_argv is None else tuple(self.command_argv)
|
|
80
|
+
if argv is not None and (not argv or any(not item for item in argv)):
|
|
81
|
+
raise ValueError("PlanStep command_argv must contain non-empty strings.")
|
|
82
|
+
object.__setattr__(self, "command_argv", argv)
|
|
83
|
+
if argv is not None:
|
|
84
|
+
if self.tool_name != "shell" or self.command_cwd is None:
|
|
85
|
+
raise ValueError("A command PlanStep must be a shell step with a cwd.")
|
|
86
|
+
_validate_repository_path(self.command_cwd, allow_root=True)
|
|
87
|
+
elif self.command_cwd is not None:
|
|
88
|
+
raise ValueError("command_cwd requires command_argv.")
|
|
89
|
+
if self.tool_name in {"apply_patch", "write_file"} and len(paths) != 1:
|
|
90
|
+
raise ValueError("An editing PlanStep must target exactly one path.")
|
|
91
|
+
if self.tool_name is None and (paths or argv is not None):
|
|
92
|
+
raise ValueError("A narrative PlanStep cannot carry Tool authority.")
|
|
93
|
+
if self.tool_name not in {None, "apply_patch", "write_file", "shell"} and (
|
|
94
|
+
paths or argv is not None
|
|
95
|
+
):
|
|
96
|
+
raise ValueError("Only frozen Day 4 Tool actions may carry Plan authority.")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True, slots=True)
|
|
100
|
+
class AuthorizationScope:
|
|
101
|
+
allowed_write_actions: tuple[tuple[str, str], ...]
|
|
102
|
+
allowed_commands: tuple[tuple[tuple[str, ...], str], ...]
|
|
103
|
+
|
|
104
|
+
def __post_init__(self) -> None:
|
|
105
|
+
writes = tuple((tool, path) for tool, path in self.allowed_write_actions)
|
|
106
|
+
commands = tuple((tuple(argv), cwd) for argv, cwd in self.allowed_commands)
|
|
107
|
+
if writes != tuple(sorted(set(writes))):
|
|
108
|
+
raise ValueError("allowed_write_actions must be sorted and unique.")
|
|
109
|
+
if commands != tuple(sorted(set(commands))):
|
|
110
|
+
raise ValueError("allowed_commands must be sorted and unique.")
|
|
111
|
+
for tool, path in writes:
|
|
112
|
+
if tool not in {"apply_patch", "write_file"}:
|
|
113
|
+
raise ValueError("AuthorizationScope contains an invalid WRITE Tool.")
|
|
114
|
+
_validate_repository_path(path)
|
|
115
|
+
for argv, cwd in commands:
|
|
116
|
+
if not argv or any(not item for item in argv):
|
|
117
|
+
raise ValueError("AuthorizationScope command argv is invalid.")
|
|
118
|
+
_validate_repository_path(cwd, allow_root=True)
|
|
119
|
+
object.__setattr__(self, "allowed_write_actions", writes)
|
|
120
|
+
object.__setattr__(self, "allowed_commands", commands)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True, slots=True)
|
|
124
|
+
class Plan:
|
|
125
|
+
plan_id: str
|
|
126
|
+
run_id: str
|
|
127
|
+
session_id: str
|
|
128
|
+
version: int
|
|
129
|
+
kind: PlanKind
|
|
130
|
+
status: PlanStatus
|
|
131
|
+
approval_status: ApprovalDecision
|
|
132
|
+
steps: tuple[PlanStep, ...]
|
|
133
|
+
authorization_scope: AuthorizationScope
|
|
134
|
+
rationale_summary: str
|
|
135
|
+
replan_reason: str | None
|
|
136
|
+
approval_id: str | None
|
|
137
|
+
scope_digest: str
|
|
138
|
+
created_at: datetime
|
|
139
|
+
approved_at: datetime | None
|
|
140
|
+
|
|
141
|
+
def __post_init__(self) -> None:
|
|
142
|
+
for field_name, value in (
|
|
143
|
+
("plan_id", self.plan_id),
|
|
144
|
+
("run_id", self.run_id),
|
|
145
|
+
("session_id", self.session_id),
|
|
146
|
+
):
|
|
147
|
+
_validate_uuid(value, field_name)
|
|
148
|
+
_validate_timestamp(self.created_at, "created_at")
|
|
149
|
+
if self.approved_at is not None:
|
|
150
|
+
_validate_timestamp(self.approved_at, "approved_at")
|
|
151
|
+
if self.version < 1 or not self.rationale_summary.strip():
|
|
152
|
+
raise ValueError("Plan version and rationale_summary are invalid.")
|
|
153
|
+
steps = tuple(self.steps)
|
|
154
|
+
if not steps:
|
|
155
|
+
raise ValueError("Plan must contain at least one step.")
|
|
156
|
+
if [step.sequence for step in steps] != list(range(1, len(steps) + 1)):
|
|
157
|
+
raise ValueError("Plan step sequence must be contiguous from one.")
|
|
158
|
+
if len({step.step_id for step in steps}) != len(steps):
|
|
159
|
+
raise ValueError("Plan step IDs must be unique.")
|
|
160
|
+
object.__setattr__(self, "steps", steps)
|
|
161
|
+
if self.authorization_scope != derive_authorization_scope(steps):
|
|
162
|
+
raise ValueError("Plan AuthorizationScope must exactly match Plan.steps.")
|
|
163
|
+
expected_digest = compute_scope_digest(
|
|
164
|
+
self.plan_id,
|
|
165
|
+
self.version,
|
|
166
|
+
self.authorization_scope,
|
|
167
|
+
)
|
|
168
|
+
if self.scope_digest != expected_digest:
|
|
169
|
+
raise ValueError("Plan scope_digest does not match its authorization scope.")
|
|
170
|
+
if self.kind is PlanKind.INITIAL:
|
|
171
|
+
if self.version != 1 or self.replan_reason is not None:
|
|
172
|
+
raise ValueError("An INITIAL Plan must be version one without a reason.")
|
|
173
|
+
elif self.version <= 1 or not (self.replan_reason or "").strip():
|
|
174
|
+
raise ValueError("A REPLAN must increment version and contain a reason.")
|
|
175
|
+
if self.status is PlanStatus.CREATED:
|
|
176
|
+
if (
|
|
177
|
+
self.approval_status is not ApprovalDecision.PENDING
|
|
178
|
+
or self.approval_id is not None
|
|
179
|
+
or self.approved_at is not None
|
|
180
|
+
):
|
|
181
|
+
raise ValueError("A CREATED Plan must await approval.")
|
|
182
|
+
if self.status is PlanStatus.ACTIVE and (
|
|
183
|
+
self.approval_status is not ApprovalDecision.APPROVED
|
|
184
|
+
or self.approval_id is None
|
|
185
|
+
or self.approved_at is None
|
|
186
|
+
):
|
|
187
|
+
raise ValueError("An ACTIVE Plan requires approved evidence identity.")
|
|
188
|
+
if (
|
|
189
|
+
self.approval_status is ApprovalDecision.DENIED
|
|
190
|
+
and self.status is not PlanStatus.FAILED
|
|
191
|
+
):
|
|
192
|
+
raise ValueError("A denied Plan must be FAILED.")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass(frozen=True, slots=True)
|
|
196
|
+
class ApprovedPlanEvidence:
|
|
197
|
+
plan_id: str
|
|
198
|
+
plan_version: int
|
|
199
|
+
run_id: str
|
|
200
|
+
session_id: str
|
|
201
|
+
source: PlanAuthorizationSource
|
|
202
|
+
approval_id: str
|
|
203
|
+
scope_digest: str
|
|
204
|
+
authorization_scope: AuthorizationScope
|
|
205
|
+
approved_at: datetime
|
|
206
|
+
|
|
207
|
+
def __post_init__(self) -> None:
|
|
208
|
+
for field_name, value in (
|
|
209
|
+
("plan_id", self.plan_id),
|
|
210
|
+
("run_id", self.run_id),
|
|
211
|
+
("session_id", self.session_id),
|
|
212
|
+
("approval_id", self.approval_id),
|
|
213
|
+
):
|
|
214
|
+
_validate_uuid(value, field_name)
|
|
215
|
+
if self.plan_version < 1:
|
|
216
|
+
raise ValueError("Approved Plan version must be positive.")
|
|
217
|
+
_validate_timestamp(self.approved_at, "approved_at")
|
|
218
|
+
if self.scope_digest != compute_scope_digest(
|
|
219
|
+
self.plan_id,
|
|
220
|
+
self.plan_version,
|
|
221
|
+
self.authorization_scope,
|
|
222
|
+
):
|
|
223
|
+
raise ValueError("Approved Plan evidence has an invalid scope digest.")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass(frozen=True, slots=True)
|
|
227
|
+
class RepairGuidance:
|
|
228
|
+
plan_id: str
|
|
229
|
+
plan_version: int
|
|
230
|
+
repair_attempt: int
|
|
231
|
+
steps: tuple[PlanStep, ...]
|
|
232
|
+
failure_summary: str
|
|
233
|
+
|
|
234
|
+
def __post_init__(self) -> None:
|
|
235
|
+
_validate_uuid(self.plan_id, "plan_id")
|
|
236
|
+
if self.plan_version < 1 or self.repair_attempt < 1:
|
|
237
|
+
raise ValueError("Repair identity values must be positive.")
|
|
238
|
+
if not self.failure_summary.strip():
|
|
239
|
+
raise ValueError("Repair failure_summary must not be empty.")
|
|
240
|
+
object.__setattr__(self, "steps", tuple(self.steps))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass(frozen=True, slots=True)
|
|
244
|
+
class ChangedFile:
|
|
245
|
+
path: str
|
|
246
|
+
change_kind: ChangeKind
|
|
247
|
+
first_invocation_id: str
|
|
248
|
+
latest_invocation_id: str
|
|
249
|
+
|
|
250
|
+
def __post_init__(self) -> None:
|
|
251
|
+
_validate_repository_path(self.path)
|
|
252
|
+
_validate_uuid(self.first_invocation_id, "first_invocation_id")
|
|
253
|
+
_validate_uuid(self.latest_invocation_id, "latest_invocation_id")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@dataclass(frozen=True, slots=True)
|
|
257
|
+
class PlanApprovalResumeInput:
|
|
258
|
+
decision: ApprovalDecision
|
|
259
|
+
reason: str | None
|
|
260
|
+
|
|
261
|
+
def __post_init__(self) -> None:
|
|
262
|
+
if self.decision not in {ApprovalDecision.APPROVED, ApprovalDecision.DENIED}:
|
|
263
|
+
raise ValueError("Plan approval resume input must be terminal.")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def derive_authorization_scope(steps: tuple[PlanStep, ...]) -> AuthorizationScope:
|
|
267
|
+
writes = {
|
|
268
|
+
(step.tool_name, step.target_paths[0])
|
|
269
|
+
for step in steps
|
|
270
|
+
if step.tool_name in {"apply_patch", "write_file"}
|
|
271
|
+
}
|
|
272
|
+
commands = {
|
|
273
|
+
(step.command_argv, step.command_cwd)
|
|
274
|
+
for step in steps
|
|
275
|
+
if step.tool_name == "shell"
|
|
276
|
+
and step.command_argv is not None
|
|
277
|
+
and step.command_cwd is not None
|
|
278
|
+
}
|
|
279
|
+
return AuthorizationScope(
|
|
280
|
+
tuple(sorted((str(tool), path) for tool, path in writes)),
|
|
281
|
+
tuple(sorted((tuple(argv), str(cwd)) for argv, cwd in commands)),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def compute_scope_digest(
|
|
286
|
+
plan_id: str,
|
|
287
|
+
plan_version: int,
|
|
288
|
+
scope: AuthorizationScope,
|
|
289
|
+
) -> str:
|
|
290
|
+
payload = {
|
|
291
|
+
"authorization_scope": {
|
|
292
|
+
"allowed_commands": [
|
|
293
|
+
[list(argv), cwd] for argv, cwd in scope.allowed_commands
|
|
294
|
+
],
|
|
295
|
+
"allowed_write_actions": [list(action) for action in scope.allowed_write_actions],
|
|
296
|
+
},
|
|
297
|
+
"plan_id": plan_id,
|
|
298
|
+
"plan_version": plan_version,
|
|
299
|
+
}
|
|
300
|
+
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
|
301
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def approval_summary(plan_id: str, version: int, digest: str) -> str:
|
|
305
|
+
return f"approve_plan:{plan_id}:v{version}:scope-sha256:{digest}"
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _validate_repository_path(value: str, *, allow_root: bool = False) -> None:
|
|
309
|
+
if not isinstance(value, str) or not value or "\\" in value:
|
|
310
|
+
raise ValueError("Repository paths must be non-empty POSIX-style strings.")
|
|
311
|
+
path = PurePosixPath(value)
|
|
312
|
+
if path.is_absolute() or ".." in path.parts or (value == "." and not allow_root):
|
|
313
|
+
raise ValueError("Repository path is outside the allowed relative form.")
|
|
314
|
+
if value != "." and (value.startswith("./") or "/./" in value):
|
|
315
|
+
raise ValueError("Repository paths must be normalized.")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _validate_uuid(value: str, field_name: str) -> None:
|
|
319
|
+
try:
|
|
320
|
+
parsed = UUID(value)
|
|
321
|
+
except ValueError as exc:
|
|
322
|
+
raise ValueError(f"{field_name} must be a UUID string.") from exc
|
|
323
|
+
if str(parsed) != value:
|
|
324
|
+
raise ValueError(f"{field_name} must use canonical UUID text.")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _validate_timestamp(value: datetime, field_name: str) -> None:
|
|
328
|
+
if value.tzinfo is None or value.utcoffset() is None:
|
|
329
|
+
raise ValueError(f"{field_name} must be timezone-aware.")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Stable Nexus-owned provider ports."""
|
|
2
|
+
|
|
3
|
+
from nexus.domain.ports.approval_unit_of_work import (
|
|
4
|
+
ApprovalUnitOfWork,
|
|
5
|
+
ApprovalUnitOfWorkFactory,
|
|
6
|
+
)
|
|
7
|
+
from nexus.domain.ports.checkpoint_provider import CheckpointProvider
|
|
8
|
+
from nexus.domain.ports.events import EventPublisher, EventSubscriber, EventSubscription
|
|
9
|
+
from nexus.domain.ports.graph_runtime import GraphRuntime
|
|
10
|
+
from nexus.domain.ports.mcp import MCPManager
|
|
11
|
+
from nexus.domain.ports.model_gateway import ModelGateway
|
|
12
|
+
from nexus.domain.ports.model_input_budget import ModelInputBudgetGuard
|
|
13
|
+
from nexus.domain.ports.repositories import (
|
|
14
|
+
ApprovalRepository,
|
|
15
|
+
RepositoryRepository,
|
|
16
|
+
RunRepository,
|
|
17
|
+
SessionRepository,
|
|
18
|
+
SessionTurnRepository,
|
|
19
|
+
)
|
|
20
|
+
from nexus.domain.ports.session_unit_of_work import (
|
|
21
|
+
SessionUnitOfWork,
|
|
22
|
+
SessionUnitOfWorkFactory,
|
|
23
|
+
)
|
|
24
|
+
from nexus.domain.ports.skills import SkillLoader, SkillRegistry, SkillSelector
|
|
25
|
+
from nexus.domain.ports.tooling import (
|
|
26
|
+
ApprovalPolicy,
|
|
27
|
+
CommandPolicy,
|
|
28
|
+
InteractiveDecisionCallback,
|
|
29
|
+
SandboxExecutor,
|
|
30
|
+
Tool,
|
|
31
|
+
)
|
|
32
|
+
from nexus.domain.ports.tracer import Tracer
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"ApprovalPolicy",
|
|
36
|
+
"ApprovalRepository",
|
|
37
|
+
"ApprovalUnitOfWork",
|
|
38
|
+
"ApprovalUnitOfWorkFactory",
|
|
39
|
+
"CheckpointProvider",
|
|
40
|
+
"CommandPolicy",
|
|
41
|
+
"EventPublisher",
|
|
42
|
+
"EventSubscriber",
|
|
43
|
+
"EventSubscription",
|
|
44
|
+
"GraphRuntime",
|
|
45
|
+
"InteractiveDecisionCallback",
|
|
46
|
+
"ModelGateway",
|
|
47
|
+
"ModelInputBudgetGuard",
|
|
48
|
+
"MCPManager",
|
|
49
|
+
"RepositoryRepository",
|
|
50
|
+
"RunRepository",
|
|
51
|
+
"SandboxExecutor",
|
|
52
|
+
"SessionRepository",
|
|
53
|
+
"SessionTurnRepository",
|
|
54
|
+
"SessionUnitOfWork",
|
|
55
|
+
"SessionUnitOfWorkFactory",
|
|
56
|
+
"SkillLoader",
|
|
57
|
+
"SkillRegistry",
|
|
58
|
+
"SkillSelector",
|
|
59
|
+
"Tool",
|
|
60
|
+
"Tracer",
|
|
61
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Day 4 structured Agent decision port."""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from nexus.domain.agent_decision import AgentDecision, AgentDecisionRequest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AgentDecisionAdapter(Protocol):
|
|
9
|
+
async def decide(self, request: AgentDecisionRequest) -> AgentDecision: ...
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Independent transaction boundary for Day 3 approvals."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import Protocol, Self
|
|
7
|
+
|
|
8
|
+
from nexus.domain.ports.repositories import ApprovalRepository, RunRepository, SessionRepository
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ApprovalUnitOfWork(Protocol):
|
|
12
|
+
approvals: ApprovalRepository
|
|
13
|
+
runs: RunRepository
|
|
14
|
+
sessions: SessionRepository
|
|
15
|
+
|
|
16
|
+
async def __aenter__(self) -> Self: ...
|
|
17
|
+
|
|
18
|
+
async def __aexit__(
|
|
19
|
+
self,
|
|
20
|
+
exc_type: type[BaseException] | None,
|
|
21
|
+
exc_value: BaseException | None,
|
|
22
|
+
traceback: TracebackType | None,
|
|
23
|
+
) -> bool | None: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ApprovalUnitOfWorkFactory(Protocol):
|
|
27
|
+
def __call__(self) -> ApprovalUnitOfWork: ...
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Nexus-owned lifecycle boundary for a graph checkpoint provider."""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CheckpointProvider(Protocol):
|
|
7
|
+
async def setup(self) -> None: ...
|
|
8
|
+
|
|
9
|
+
def get_checkpointer(self) -> object: ...
|
|
10
|
+
|
|
11
|
+
async def close(self) -> None: ...
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Nexus-owned Day 5 context boundaries."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from nexus.domain.agent_decision import Observation
|
|
7
|
+
from nexus.domain.context import (
|
|
8
|
+
CodeChunk,
|
|
9
|
+
ContextCandidate,
|
|
10
|
+
ContextRequest,
|
|
11
|
+
EmbeddingConfig,
|
|
12
|
+
IndexCompatibility,
|
|
13
|
+
IndexRequest,
|
|
14
|
+
IndexResult,
|
|
15
|
+
RetrievalQuery,
|
|
16
|
+
RetrievalResult,
|
|
17
|
+
)
|
|
18
|
+
from nexus.domain.exploration import WorkingContext
|
|
19
|
+
from nexus.domain.persistence import SessionTurn
|
|
20
|
+
from nexus.domain.planning import Plan
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LexicalSearchProvider(Protocol):
|
|
24
|
+
async def search(self, query: RetrievalQuery) -> tuple[ContextCandidate, ...]: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SemanticSearchProvider(Protocol):
|
|
28
|
+
async def search(self, query: RetrievalQuery) -> tuple[ContextCandidate, ...]: ...
|
|
29
|
+
|
|
30
|
+
async def validate_index(self, repository_id: str) -> IndexCompatibility: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ContextProvider(Protocol):
|
|
34
|
+
async def retrieve(self, request: ContextRequest) -> RetrievalResult: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ContextManager(Protocol):
|
|
38
|
+
async def build(self, request: ContextRequest) -> WorkingContext: ...
|
|
39
|
+
|
|
40
|
+
async def prepare_agent_context(
|
|
41
|
+
self,
|
|
42
|
+
*,
|
|
43
|
+
working_context: WorkingContext,
|
|
44
|
+
plan: Plan | None,
|
|
45
|
+
observations: Sequence[Observation],
|
|
46
|
+
conversation_turns: Sequence[SessionTurn],
|
|
47
|
+
) -> WorkingContext: ...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Chunker(Protocol):
|
|
51
|
+
def chunk(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
file_path: str,
|
|
55
|
+
language: str,
|
|
56
|
+
content: str,
|
|
57
|
+
file_hash: str,
|
|
58
|
+
) -> tuple[CodeChunk, ...]: ...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class EmbeddingGateway(Protocol):
|
|
62
|
+
@property
|
|
63
|
+
def config(self) -> EmbeddingConfig: ...
|
|
64
|
+
|
|
65
|
+
async def embed_documents(self, texts: Sequence[str]) -> tuple[tuple[float, ...], ...]: ...
|
|
66
|
+
|
|
67
|
+
async def embed_query(self, text: str) -> tuple[float, ...]: ...
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class RepositoryIndexer(Protocol):
|
|
71
|
+
async def index(self, request: IndexRequest) -> IndexResult: ...
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Nexus-owned runtime event publication boundary."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
from nexus.domain.runtime_events import RuntimeEvent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EventSubscriber(Protocol):
|
|
11
|
+
async def on_event(self, event: RuntimeEvent) -> None: ...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EventSubscription(Protocol):
|
|
15
|
+
async def aclose(self) -> None: ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EventPublisher(Protocol):
|
|
19
|
+
def subscribe(self, subscriber: EventSubscriber) -> EventSubscription: ...
|
|
20
|
+
|
|
21
|
+
async def publish(self, event: RuntimeEvent) -> None: ...
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Nexus-owned graph execution boundary."""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from nexus.domain.agent_state import AgentState
|
|
6
|
+
from nexus.domain.planning import PlanApprovalResumeInput
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GraphRuntime(Protocol):
|
|
10
|
+
"""Execute a Nexus-owned state without leaking graph framework types."""
|
|
11
|
+
|
|
12
|
+
async def run(
|
|
13
|
+
self,
|
|
14
|
+
state: AgentState,
|
|
15
|
+
*,
|
|
16
|
+
thread_id: str | None = None,
|
|
17
|
+
) -> AgentState:
|
|
18
|
+
"""Run the graph, optionally binding it to a durable execution thread."""
|
|
19
|
+
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
async def resume(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
thread_id: str,
|
|
26
|
+
resume_input: PlanApprovalResumeInput | None = None,
|
|
27
|
+
) -> AgentState:
|
|
28
|
+
"""Resume the latest persisted checkpoint for an execution thread."""
|
|
29
|
+
|
|
30
|
+
...
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Async MCP lifecycle port without SDK-specific types."""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from nexus.domain.mcp import MCPConnectionInfo, MCPToolDescriptor
|
|
6
|
+
from nexus.domain.tooling import JsonObject
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MCPManager(Protocol):
|
|
10
|
+
async def connect(self) -> tuple[MCPConnectionInfo, ...]: ...
|
|
11
|
+
|
|
12
|
+
async def list_tools(self) -> tuple[MCPToolDescriptor, ...]: ...
|
|
13
|
+
|
|
14
|
+
async def call_tool(
|
|
15
|
+
self,
|
|
16
|
+
*,
|
|
17
|
+
server_id: str,
|
|
18
|
+
remote_name: str,
|
|
19
|
+
arguments: JsonObject,
|
|
20
|
+
timeout_seconds: float,
|
|
21
|
+
) -> JsonObject: ...
|
|
22
|
+
|
|
23
|
+
async def close(self) -> None: ...
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Provider-neutral model boundary."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator, Sequence
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from nexus.domain.model import ModelChunk, ModelMessage, ModelResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ModelGateway(Protocol):
|
|
10
|
+
"""Acquire completions without exposing concrete provider types."""
|
|
11
|
+
|
|
12
|
+
async def complete(self, messages: Sequence[ModelMessage]) -> ModelResponse:
|
|
13
|
+
"""Return one normalized model completion."""
|
|
14
|
+
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
def stream(self, messages: Sequence[ModelMessage]) -> AsyncIterator[ModelChunk]:
|
|
18
|
+
"""Stream normalized model content fragments."""
|
|
19
|
+
|
|
20
|
+
...
|
|
21
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Nexus-owned model-input budget validation boundary."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from nexus.domain.model import ModelMessage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ModelInputBudgetGuard(Protocol):
|
|
10
|
+
def ensure_fits(
|
|
11
|
+
self,
|
|
12
|
+
messages: Sequence[ModelMessage],
|
|
13
|
+
*,
|
|
14
|
+
error_code: str,
|
|
15
|
+
) -> None: ...
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Day 4 planning port."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from nexus.domain.exploration import WorkingContext
|
|
10
|
+
from nexus.domain.planning import Plan, PlanKind, RepairGuidance
|
|
11
|
+
from nexus.domain.validation import ValidationResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class PlanningRequest:
|
|
16
|
+
task: str
|
|
17
|
+
context: WorkingContext
|
|
18
|
+
kind: PlanKind
|
|
19
|
+
previous_plan: Plan | None
|
|
20
|
+
reason: str | None
|
|
21
|
+
run_id: str
|
|
22
|
+
session_id: str
|
|
23
|
+
|
|
24
|
+
def __post_init__(self) -> None:
|
|
25
|
+
_validate_canonical_uuid(self.run_id, "run_id")
|
|
26
|
+
_validate_canonical_uuid(self.session_id, "session_id")
|
|
27
|
+
if self.kind is PlanKind.INITIAL:
|
|
28
|
+
if self.previous_plan is not None:
|
|
29
|
+
raise ValueError("INITIAL PlanningRequest cannot contain a previous Plan.")
|
|
30
|
+
else:
|
|
31
|
+
previous = self.previous_plan
|
|
32
|
+
if previous is None:
|
|
33
|
+
raise ValueError("REPLAN PlanningRequest requires a previous Plan.")
|
|
34
|
+
if self.run_id != previous.run_id or self.session_id != previous.session_id:
|
|
35
|
+
raise ValueError("REPLAN cannot change Run or Session identity.")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class RepairPlanningRequest:
|
|
40
|
+
task: str
|
|
41
|
+
context: WorkingContext
|
|
42
|
+
plan: Plan
|
|
43
|
+
validation_result: ValidationResult
|
|
44
|
+
repair_attempt: int
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Planner(Protocol):
|
|
48
|
+
async def create_plan(self, request: PlanningRequest) -> Plan: ...
|
|
49
|
+
|
|
50
|
+
async def create_repair_guidance(
|
|
51
|
+
self,
|
|
52
|
+
request: RepairPlanningRequest,
|
|
53
|
+
) -> RepairGuidance: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _validate_canonical_uuid(value: str, field_name: str) -> None:
|
|
57
|
+
try:
|
|
58
|
+
parsed = UUID(value)
|
|
59
|
+
except (AttributeError, TypeError, ValueError) as exc:
|
|
60
|
+
raise ValueError(f"{field_name} must be a canonical UUID string.") from exc
|
|
61
|
+
if str(parsed) != value:
|
|
62
|
+
raise ValueError(f"{field_name} must be a canonical UUID string.")
|