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,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.")