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,113 @@
|
|
|
1
|
+
"""Structured Day 4 Agent decision and deterministic observation values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from nexus.domain.exploration import WorkingContext
|
|
10
|
+
from nexus.domain.planning import Plan, RepairGuidance
|
|
11
|
+
from nexus.domain.tooling import JsonObject
|
|
12
|
+
from nexus.domain.validation import ValidationResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AgentDecisionKind(StrEnum):
|
|
16
|
+
TOOL_ACTION = "TOOL_ACTION"
|
|
17
|
+
CONTINUE = "CONTINUE"
|
|
18
|
+
TASK_READY = "TASK_READY"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentRoute(StrEnum):
|
|
22
|
+
TOOL_REQUIRED = "TOOL_REQUIRED"
|
|
23
|
+
CONTINUE = "CONTINUE"
|
|
24
|
+
TASK_READY = "TASK_READY"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ObserveRoute(StrEnum):
|
|
28
|
+
REPLAN_REQUIRED = "REPLAN_REQUIRED"
|
|
29
|
+
CONTINUE = "CONTINUE"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ValidationRoute(StrEnum):
|
|
33
|
+
PASSED = "PASSED"
|
|
34
|
+
REPAIR_AVAILABLE = "REPAIR_AVAILABLE"
|
|
35
|
+
FAILED = "FAILED"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class ToolAction:
|
|
40
|
+
tool_name: str
|
|
41
|
+
arguments: JsonObject
|
|
42
|
+
|
|
43
|
+
def __post_init__(self) -> None:
|
|
44
|
+
if not self.tool_name:
|
|
45
|
+
raise ValueError("ToolAction tool_name must not be empty.")
|
|
46
|
+
if not _is_json_value(self.arguments):
|
|
47
|
+
raise ValueError("ToolAction arguments must be JSON-compatible.")
|
|
48
|
+
object.__setattr__(self, "arguments", dict(self.arguments))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class AgentDecision:
|
|
53
|
+
kind: AgentDecisionKind
|
|
54
|
+
action: ToolAction | None
|
|
55
|
+
summary: str
|
|
56
|
+
|
|
57
|
+
def __post_init__(self) -> None:
|
|
58
|
+
if not self.summary.strip():
|
|
59
|
+
raise ValueError("AgentDecision summary must not be empty.")
|
|
60
|
+
if (self.kind is AgentDecisionKind.TOOL_ACTION) != (self.action is not None):
|
|
61
|
+
raise ValueError("AgentDecision action does not match its kind.")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, slots=True)
|
|
65
|
+
class Observation:
|
|
66
|
+
invocation_id: str
|
|
67
|
+
tool_name: str
|
|
68
|
+
success: bool
|
|
69
|
+
evidence_summary: str
|
|
70
|
+
error_code: str | None
|
|
71
|
+
replan_reason: str | None
|
|
72
|
+
|
|
73
|
+
def __post_init__(self) -> None:
|
|
74
|
+
_validate_uuid(self.invocation_id, "invocation_id")
|
|
75
|
+
if not self.tool_name or not self.evidence_summary.strip():
|
|
76
|
+
raise ValueError("Observation Tool identity and evidence must not be empty.")
|
|
77
|
+
if self.error_code == "PLAN_SCOPE_DENIED":
|
|
78
|
+
if not (self.replan_reason or "").strip():
|
|
79
|
+
raise ValueError("PLAN_SCOPE_DENIED requires a material Replan reason.")
|
|
80
|
+
elif self.replan_reason is not None:
|
|
81
|
+
raise ValueError("Day 4 permits no additional semantic Replan trigger.")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True, slots=True)
|
|
85
|
+
class AgentDecisionRequest:
|
|
86
|
+
task: str
|
|
87
|
+
context: WorkingContext
|
|
88
|
+
plan: Plan
|
|
89
|
+
observations: tuple[Observation, ...]
|
|
90
|
+
validation_result: ValidationResult | None = None
|
|
91
|
+
repair_guidance: RepairGuidance | None = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _validate_uuid(value: str, field_name: str) -> None:
|
|
95
|
+
try:
|
|
96
|
+
parsed = UUID(value)
|
|
97
|
+
except (AttributeError, TypeError, ValueError) as exc:
|
|
98
|
+
raise ValueError(f"{field_name} must be a canonical UUID string.") from exc
|
|
99
|
+
if str(parsed) != value:
|
|
100
|
+
raise ValueError(f"{field_name} must be a canonical UUID string.")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _is_json_value(value: object) -> bool:
|
|
104
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
105
|
+
return True
|
|
106
|
+
if isinstance(value, list):
|
|
107
|
+
return all(_is_json_value(item) for item in value)
|
|
108
|
+
if isinstance(value, dict):
|
|
109
|
+
return all(
|
|
110
|
+
isinstance(key, str) and _is_json_value(item)
|
|
111
|
+
for key, item in value.items()
|
|
112
|
+
)
|
|
113
|
+
return False
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Nexus-owned graph state through Day 4."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from nexus.domain.agent_decision import Observation, ToolAction
|
|
6
|
+
from nexus.domain.approvals import ApprovalRequest
|
|
7
|
+
from nexus.domain.exploration import ExplorationResult, WorkingContext
|
|
8
|
+
from nexus.domain.model import ModelMessage, TokenUsageAggregate
|
|
9
|
+
from nexus.domain.planning import (
|
|
10
|
+
ApprovedPlanEvidence,
|
|
11
|
+
ChangedFile,
|
|
12
|
+
Plan,
|
|
13
|
+
RepairGuidance,
|
|
14
|
+
TerminalStatus,
|
|
15
|
+
)
|
|
16
|
+
from nexus.domain.runtime_events import RuntimeStatus
|
|
17
|
+
from nexus.domain.tooling import ToolResult
|
|
18
|
+
from nexus.domain.validation import ValidationResult
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class AgentState:
|
|
23
|
+
"""Nexus-owned state passed through the graph boundary."""
|
|
24
|
+
|
|
25
|
+
task: str
|
|
26
|
+
messages: list[ModelMessage]
|
|
27
|
+
run_id: str
|
|
28
|
+
session_id: str | None
|
|
29
|
+
status: RuntimeStatus
|
|
30
|
+
exploration: ExplorationResult | None = None
|
|
31
|
+
context: WorkingContext | None = None
|
|
32
|
+
plan: Plan | None = None
|
|
33
|
+
plan_history: tuple[Plan, ...] = ()
|
|
34
|
+
observations: tuple[Observation, ...] = ()
|
|
35
|
+
tool_results: tuple[ToolResult, ...] = ()
|
|
36
|
+
pending_tool_action: ToolAction | None = None
|
|
37
|
+
latest_tool_result: ToolResult | None = None
|
|
38
|
+
pending_plan_approval: ApprovalRequest | None = None
|
|
39
|
+
approved_plan: ApprovedPlanEvidence | None = None
|
|
40
|
+
repair_guidance: RepairGuidance | None = None
|
|
41
|
+
validation_result: ValidationResult | None = None
|
|
42
|
+
changed_files: tuple[ChangedFile, ...] = ()
|
|
43
|
+
step_count: int = 0
|
|
44
|
+
tool_call_count: int = 0
|
|
45
|
+
llm_call_count: int = 0
|
|
46
|
+
replan_count: int = 0
|
|
47
|
+
repair_count: int = 0
|
|
48
|
+
terminal_status: TerminalStatus | None = None
|
|
49
|
+
token_usage: TokenUsageAggregate = field(default_factory=TokenUsageAggregate)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Nexus-owned Day 3 approval lifecycle entity."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from nexus.domain.tooling import ApprovalDecision, RiskLevel
|
|
10
|
+
|
|
11
|
+
_ACTORS = {"runtime", "user", "auto_policy", "security_policy"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class ApprovalRequest:
|
|
16
|
+
approval_id: str
|
|
17
|
+
run_id: str
|
|
18
|
+
session_id: str
|
|
19
|
+
operation: str
|
|
20
|
+
risk_level: RiskLevel
|
|
21
|
+
resource_or_command_summary: str
|
|
22
|
+
decision: ApprovalDecision
|
|
23
|
+
actor: str
|
|
24
|
+
reason: str | None
|
|
25
|
+
created_at: datetime
|
|
26
|
+
decided_at: datetime | None
|
|
27
|
+
|
|
28
|
+
def __post_init__(self) -> None:
|
|
29
|
+
_validate_uuid(self.approval_id, "approval_id")
|
|
30
|
+
_validate_uuid(self.run_id, "run_id")
|
|
31
|
+
_validate_uuid(self.session_id, "session_id")
|
|
32
|
+
_validate_timestamp(self.created_at, "created_at")
|
|
33
|
+
if self.decided_at is not None:
|
|
34
|
+
_validate_timestamp(self.decided_at, "decided_at")
|
|
35
|
+
if not self.operation or not self.resource_or_command_summary:
|
|
36
|
+
raise ValueError("Approval operation, summary, and actor must not be empty.")
|
|
37
|
+
if self.actor not in _ACTORS:
|
|
38
|
+
raise ValueError("Approval actor is not a frozen Day 3 actor.")
|
|
39
|
+
if self.decision is ApprovalDecision.PENDING:
|
|
40
|
+
if self.decided_at is not None or self.actor != "runtime":
|
|
41
|
+
raise ValueError("A pending approval must have runtime actor and no decided_at.")
|
|
42
|
+
elif self.decided_at is None:
|
|
43
|
+
raise ValueError("A terminal approval must have decided_at.")
|
|
44
|
+
elif self.decision is ApprovalDecision.APPROVED and not (
|
|
45
|
+
self.actor == "user"
|
|
46
|
+
or self.actor == "auto_policy" and self.operation == "approve_plan"
|
|
47
|
+
):
|
|
48
|
+
raise ValueError("Only the user or scoped Plan auto policy may approve.")
|
|
49
|
+
elif self.decision is ApprovalDecision.DENIED and self.actor == "runtime":
|
|
50
|
+
raise ValueError("The runtime actor cannot own a terminal denial.")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _validate_uuid(value: str, field_name: str) -> None:
|
|
54
|
+
try:
|
|
55
|
+
parsed = UUID(value)
|
|
56
|
+
except ValueError as exc:
|
|
57
|
+
raise ValueError(f"{field_name} must be a UUID string.") from exc
|
|
58
|
+
if str(parsed) != value:
|
|
59
|
+
raise ValueError(f"{field_name} must use canonical UUID text.")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _validate_timestamp(value: datetime, field_name: str) -> None:
|
|
63
|
+
if value.tzinfo is None or value.utcoffset() is None:
|
|
64
|
+
raise ValueError(f"{field_name} must be timezone-aware.")
|
nexus/domain/context.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Frozen Day 5 retrieval, indexing and embedding values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from nexus.domain.exploration import ExplorationResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RetrievalSource(StrEnum):
|
|
14
|
+
LEXICAL = "LEXICAL"
|
|
15
|
+
SEMANTIC = "SEMANTIC"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class CodeChunk:
|
|
20
|
+
file_path: str
|
|
21
|
+
language: str
|
|
22
|
+
symbol: str | None
|
|
23
|
+
start_line: int
|
|
24
|
+
end_line: int
|
|
25
|
+
content: str
|
|
26
|
+
content_hash: str
|
|
27
|
+
file_hash: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class ContextCandidate:
|
|
32
|
+
chunk: CodeChunk
|
|
33
|
+
lexical_rank: int | None
|
|
34
|
+
semantic_rank: int | None
|
|
35
|
+
semantic_score: float | None
|
|
36
|
+
rrf_score: float
|
|
37
|
+
sources: tuple[RetrievalSource, ...]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class ContextBudget:
|
|
42
|
+
max_retrieved_chunks: int
|
|
43
|
+
max_exploration_seed_chunks: int
|
|
44
|
+
max_code_context_tokens: int
|
|
45
|
+
max_recent_observations: int
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class RetrievalQuery:
|
|
50
|
+
repository_id: str
|
|
51
|
+
workspace_path: str
|
|
52
|
+
text: str
|
|
53
|
+
limit: int
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class RetrievalResult:
|
|
58
|
+
candidates: tuple[ContextCandidate, ...]
|
|
59
|
+
semantic_used: bool
|
|
60
|
+
semantic_status: str | None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True, slots=True)
|
|
64
|
+
class ContextRequest:
|
|
65
|
+
task: str
|
|
66
|
+
repository_id: str
|
|
67
|
+
workspace_path: str
|
|
68
|
+
exploration: ExplorationResult
|
|
69
|
+
run_id: str
|
|
70
|
+
session_id: str
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True, slots=True)
|
|
74
|
+
class EmbeddingConfig:
|
|
75
|
+
provider: str
|
|
76
|
+
model: str
|
|
77
|
+
dimension: int
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True, slots=True)
|
|
81
|
+
class IndexRequest:
|
|
82
|
+
repository_id: str
|
|
83
|
+
workspace_path: str
|
|
84
|
+
rebuild: bool
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True, slots=True)
|
|
88
|
+
class IndexResult:
|
|
89
|
+
repository_id: str
|
|
90
|
+
scanned_files: int
|
|
91
|
+
indexed_files: int
|
|
92
|
+
unchanged_files: int
|
|
93
|
+
removed_files: int
|
|
94
|
+
skipped_files: int
|
|
95
|
+
chunk_count: int
|
|
96
|
+
rebuilt: bool
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class IndexCompatibilityStatus(StrEnum):
|
|
100
|
+
COMPATIBLE = "COMPATIBLE"
|
|
101
|
+
INDEX_NOT_FOUND = "INDEX_NOT_FOUND"
|
|
102
|
+
INDEX_INCOMPATIBLE = "INDEX_INCOMPATIBLE"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True, slots=True)
|
|
106
|
+
class IndexCompatibility:
|
|
107
|
+
status: IndexCompatibilityStatus
|
|
108
|
+
reason: str | None
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Day 4 selective exploration and bounded context values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from nexus.domain.context import ContextCandidate
|
|
9
|
+
from nexus.domain.persistence import SessionTurn
|
|
10
|
+
from nexus.domain.skills import SelectedSkill, SkillSelectionResult
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from nexus.domain.agent_decision import Observation
|
|
14
|
+
|
|
15
|
+
from nexus.domain.tooling import ToolResult
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class ExplorationRequest:
|
|
20
|
+
task: str
|
|
21
|
+
run_id: str
|
|
22
|
+
session_id: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class RepositoryInstruction:
|
|
27
|
+
path: str
|
|
28
|
+
scope_path: str
|
|
29
|
+
depth: int
|
|
30
|
+
content: str
|
|
31
|
+
truncated: bool
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class RepositoryFileEvidence:
|
|
36
|
+
path: str
|
|
37
|
+
category: str
|
|
38
|
+
discovery_reason: str
|
|
39
|
+
summary: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class ExplorationResult:
|
|
44
|
+
instructions: tuple[RepositoryInstruction, ...]
|
|
45
|
+
manifests: tuple[RepositoryFileEvidence, ...]
|
|
46
|
+
top_level_paths: tuple[str, ...]
|
|
47
|
+
relevant_files: tuple[RepositoryFileEvidence, ...]
|
|
48
|
+
initial_git_status: ToolResult
|
|
49
|
+
tool_results: tuple[ToolResult, ...]
|
|
50
|
+
truncated: bool
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class SelectedFileContext:
|
|
55
|
+
path: str
|
|
56
|
+
content: str
|
|
57
|
+
applicable_instruction_paths: tuple[str, ...]
|
|
58
|
+
discovery_reason: str
|
|
59
|
+
truncated: bool
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True, slots=True)
|
|
63
|
+
class WorkingContext:
|
|
64
|
+
task: str
|
|
65
|
+
repository_instructions: tuple[RepositoryInstruction, ...]
|
|
66
|
+
manifest_summaries: tuple[RepositoryFileEvidence, ...]
|
|
67
|
+
top_level_paths: tuple[str, ...]
|
|
68
|
+
selected_files: tuple[SelectedFileContext, ...]
|
|
69
|
+
truncated: bool
|
|
70
|
+
retrieved_candidates: tuple[ContextCandidate, ...] = ()
|
|
71
|
+
compacted_conversation: str | None = None
|
|
72
|
+
semantic_retrieval_used: bool = False
|
|
73
|
+
semantic_retrieval_status: str | None = None
|
|
74
|
+
recent_observations: tuple[Observation, ...] = ()
|
|
75
|
+
recent_conversation_turns: tuple[SessionTurn, ...] = ()
|
|
76
|
+
compacted_observations: str | None = None
|
|
77
|
+
selected_skills: tuple[SelectedSkill, ...] = ()
|
|
78
|
+
skill_selection_result: SkillSelectionResult | None = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True, slots=True)
|
|
82
|
+
class ContextBuildRequest:
|
|
83
|
+
task: str
|
|
84
|
+
exploration: ExplorationResult
|
|
85
|
+
run_id: str
|
|
86
|
+
session_id: str
|
nexus/domain/mcp.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Nexus-owned, SDK-neutral MCP metadata values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from nexus.domain.tooling import JsonObject
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class MCPToolDescriptor:
|
|
12
|
+
server_id: str
|
|
13
|
+
remote_name: str
|
|
14
|
+
registry_name: str
|
|
15
|
+
description: str | None
|
|
16
|
+
input_schema: JsonObject
|
|
17
|
+
|
|
18
|
+
def __post_init__(self) -> None:
|
|
19
|
+
if not self.server_id or not self.remote_name or not self.registry_name:
|
|
20
|
+
raise ValueError("MCP Tool identity must not be empty.")
|
|
21
|
+
if self.description is not None and not isinstance(self.description, str):
|
|
22
|
+
raise ValueError("MCP Tool description must be text or None.")
|
|
23
|
+
if not _is_json_value(self.input_schema):
|
|
24
|
+
raise ValueError("MCP input schema must be a JSON object.")
|
|
25
|
+
object.__setattr__(self, "input_schema", dict(self.input_schema))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class MCPConnectionInfo:
|
|
30
|
+
server_id: str
|
|
31
|
+
connected: bool
|
|
32
|
+
server_name: str | None
|
|
33
|
+
server_version: str | None
|
|
34
|
+
negotiated_protocol_version: str | None
|
|
35
|
+
|
|
36
|
+
def __post_init__(self) -> None:
|
|
37
|
+
if not self.server_id:
|
|
38
|
+
raise ValueError("MCP server_id must not be empty.")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _is_json_value(value: object) -> bool:
|
|
42
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
43
|
+
return True
|
|
44
|
+
if isinstance(value, list):
|
|
45
|
+
return all(_is_json_value(item) for item in value)
|
|
46
|
+
if isinstance(value, dict):
|
|
47
|
+
return all(
|
|
48
|
+
isinstance(key, str) and _is_json_value(item)
|
|
49
|
+
for key, item in value.items()
|
|
50
|
+
)
|
|
51
|
+
return False
|
nexus/domain/model.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Provider-neutral model request, response, and usage values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
ModelRole = Literal["system", "user", "assistant"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UsageAvailability(StrEnum):
|
|
13
|
+
REPORTED = "REPORTED"
|
|
14
|
+
PARTIAL = "PARTIAL"
|
|
15
|
+
UNAVAILABLE = "UNAVAILABLE"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ModelCallPhase(StrEnum):
|
|
19
|
+
DIRECT_RESPONSE = "DIRECT_RESPONSE"
|
|
20
|
+
PLAN = "PLAN"
|
|
21
|
+
REPLAN = "REPLAN"
|
|
22
|
+
REPAIR = "REPAIR"
|
|
23
|
+
AGENT_STEP = "AGENT_STEP"
|
|
24
|
+
SKILL_SELECTION = "SKILL_SELECTION"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class TokenUsage:
|
|
29
|
+
input_tokens: int | None
|
|
30
|
+
output_tokens: int | None
|
|
31
|
+
total_tokens: int | None
|
|
32
|
+
availability: UsageAvailability
|
|
33
|
+
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
values = (self.input_tokens, self.output_tokens, self.total_tokens)
|
|
36
|
+
if any(value is not None and value < 0 for value in values):
|
|
37
|
+
raise ValueError("Token usage values must not be negative.")
|
|
38
|
+
known = sum(value is not None for value in values)
|
|
39
|
+
if self.availability is UsageAvailability.UNAVAILABLE and known != 0:
|
|
40
|
+
raise ValueError("UNAVAILABLE token usage cannot contain reported values.")
|
|
41
|
+
if self.availability is UsageAvailability.REPORTED and known != 3:
|
|
42
|
+
raise ValueError("REPORTED token usage requires a complete usage triple.")
|
|
43
|
+
if self.availability is UsageAvailability.PARTIAL and known not in {1, 2}:
|
|
44
|
+
raise ValueError("PARTIAL token usage requires one or two reported values.")
|
|
45
|
+
if known == 3:
|
|
46
|
+
assert self.input_tokens is not None
|
|
47
|
+
assert self.output_tokens is not None
|
|
48
|
+
if self.total_tokens != self.input_tokens + self.output_tokens:
|
|
49
|
+
raise ValueError("Token total must equal input plus output tokens.")
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def unavailable(cls) -> TokenUsage:
|
|
53
|
+
return cls(None, None, None, UsageAvailability.UNAVAILABLE)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class TokenUsageAggregate:
|
|
58
|
+
input_tokens_reported: int = 0
|
|
59
|
+
output_tokens_reported: int = 0
|
|
60
|
+
total_tokens_reported: int = 0
|
|
61
|
+
calls_with_reported_usage: int = 0
|
|
62
|
+
calls_with_partial_usage: int = 0
|
|
63
|
+
calls_with_unavailable_usage: int = 0
|
|
64
|
+
usage_complete: bool = True
|
|
65
|
+
|
|
66
|
+
def __post_init__(self) -> None:
|
|
67
|
+
counters = (
|
|
68
|
+
self.input_tokens_reported,
|
|
69
|
+
self.output_tokens_reported,
|
|
70
|
+
self.total_tokens_reported,
|
|
71
|
+
self.calls_with_reported_usage,
|
|
72
|
+
self.calls_with_partial_usage,
|
|
73
|
+
self.calls_with_unavailable_usage,
|
|
74
|
+
)
|
|
75
|
+
if any(value < 0 for value in counters):
|
|
76
|
+
raise ValueError("Token usage aggregate values must not be negative.")
|
|
77
|
+
complete = (
|
|
78
|
+
self.calls_with_partial_usage == 0
|
|
79
|
+
and self.calls_with_unavailable_usage == 0
|
|
80
|
+
)
|
|
81
|
+
if self.usage_complete is not complete:
|
|
82
|
+
raise ValueError("Token usage aggregate completeness is inconsistent.")
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def call_count(self) -> int:
|
|
86
|
+
return (
|
|
87
|
+
self.calls_with_reported_usage
|
|
88
|
+
+ self.calls_with_partial_usage
|
|
89
|
+
+ self.calls_with_unavailable_usage
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def add(self, usage: TokenUsage) -> TokenUsageAggregate:
|
|
93
|
+
return TokenUsageAggregate(
|
|
94
|
+
input_tokens_reported=self.input_tokens_reported + (usage.input_tokens or 0),
|
|
95
|
+
output_tokens_reported=self.output_tokens_reported + (usage.output_tokens or 0),
|
|
96
|
+
total_tokens_reported=self.total_tokens_reported + (usage.total_tokens or 0),
|
|
97
|
+
calls_with_reported_usage=(
|
|
98
|
+
self.calls_with_reported_usage
|
|
99
|
+
+ int(usage.availability is UsageAvailability.REPORTED)
|
|
100
|
+
),
|
|
101
|
+
calls_with_partial_usage=(
|
|
102
|
+
self.calls_with_partial_usage
|
|
103
|
+
+ int(usage.availability is UsageAvailability.PARTIAL)
|
|
104
|
+
),
|
|
105
|
+
calls_with_unavailable_usage=(
|
|
106
|
+
self.calls_with_unavailable_usage
|
|
107
|
+
+ int(usage.availability is UsageAvailability.UNAVAILABLE)
|
|
108
|
+
),
|
|
109
|
+
usage_complete=(
|
|
110
|
+
self.usage_complete
|
|
111
|
+
and usage.availability is UsageAvailability.REPORTED
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def to_usage(self) -> TokenUsage:
|
|
116
|
+
if self.call_count == 0 or (
|
|
117
|
+
self.calls_with_reported_usage == 0
|
|
118
|
+
and self.calls_with_partial_usage == 0
|
|
119
|
+
):
|
|
120
|
+
return TokenUsage.unavailable()
|
|
121
|
+
availability = (
|
|
122
|
+
UsageAvailability.REPORTED
|
|
123
|
+
if self.usage_complete
|
|
124
|
+
else UsageAvailability.PARTIAL
|
|
125
|
+
)
|
|
126
|
+
if availability is UsageAvailability.PARTIAL:
|
|
127
|
+
values: tuple[int | None, int | None, int | None] = (
|
|
128
|
+
self.input_tokens_reported,
|
|
129
|
+
self.output_tokens_reported,
|
|
130
|
+
None,
|
|
131
|
+
)
|
|
132
|
+
else:
|
|
133
|
+
values = (
|
|
134
|
+
self.input_tokens_reported,
|
|
135
|
+
self.output_tokens_reported,
|
|
136
|
+
self.total_tokens_reported,
|
|
137
|
+
)
|
|
138
|
+
return TokenUsage(*values, availability)
|
|
139
|
+
|
|
140
|
+
def to_dict(self) -> dict[str, int | bool]:
|
|
141
|
+
return {
|
|
142
|
+
"input_tokens_reported": self.input_tokens_reported,
|
|
143
|
+
"output_tokens_reported": self.output_tokens_reported,
|
|
144
|
+
"total_tokens_reported": self.total_tokens_reported,
|
|
145
|
+
"calls_with_reported_usage": self.calls_with_reported_usage,
|
|
146
|
+
"calls_with_partial_usage": self.calls_with_partial_usage,
|
|
147
|
+
"calls_with_unavailable_usage": self.calls_with_unavailable_usage,
|
|
148
|
+
"usage_complete": self.usage_complete,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(frozen=True, slots=True)
|
|
153
|
+
class ModelMessage:
|
|
154
|
+
"""A minimal Nexus-owned chat message."""
|
|
155
|
+
|
|
156
|
+
role: ModelRole
|
|
157
|
+
content: str
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass(frozen=True, slots=True)
|
|
161
|
+
class ModelResponse:
|
|
162
|
+
"""A normalized non-streaming model response."""
|
|
163
|
+
|
|
164
|
+
content: str
|
|
165
|
+
usage: TokenUsage | None = None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass(frozen=True, slots=True)
|
|
169
|
+
class ModelChunk:
|
|
170
|
+
"""A normalized streaming model fragment."""
|
|
171
|
+
|
|
172
|
+
content: str
|
|
173
|
+
usage: TokenUsage | None = None
|