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,326 @@
|
|
|
1
|
+
"""Nexus-owned Day 8 observability values, independent of concrete tracers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
from types import MappingProxyType
|
|
11
|
+
from uuid import UUID
|
|
12
|
+
|
|
13
|
+
from nexus.domain.model import TokenUsage
|
|
14
|
+
from nexus.domain.planning import TerminalStatus
|
|
15
|
+
from nexus.domain.runtime_events import RuntimeStatus
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ExecutionOutcome(StrEnum):
|
|
19
|
+
COMPLETED = "COMPLETED"
|
|
20
|
+
FAILED = "FAILED"
|
|
21
|
+
INTERRUPTED = "INTERRUPTED"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TraceValidationStatus(StrEnum):
|
|
25
|
+
PASS = "PASS"
|
|
26
|
+
FAIL = "FAIL"
|
|
27
|
+
UNKNOWN = "UNKNOWN"
|
|
28
|
+
NOT_RUN = "NOT_RUN"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TelemetrySeverity(StrEnum):
|
|
32
|
+
DEBUG = "DEBUG"
|
|
33
|
+
INFO = "INFO"
|
|
34
|
+
WARNING = "WARNING"
|
|
35
|
+
ERROR = "ERROR"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RuntimePhase(StrEnum):
|
|
39
|
+
RUN = "RUN"
|
|
40
|
+
REPOSITORY = "REPOSITORY"
|
|
41
|
+
CONTEXT = "CONTEXT"
|
|
42
|
+
PLAN = "PLAN"
|
|
43
|
+
APPROVAL = "APPROVAL"
|
|
44
|
+
AGENT = "AGENT"
|
|
45
|
+
MODEL = "MODEL"
|
|
46
|
+
TOOL = "TOOL"
|
|
47
|
+
REPLAN = "REPLAN"
|
|
48
|
+
VALIDATION = "VALIDATION"
|
|
49
|
+
REPAIR = "REPAIR"
|
|
50
|
+
CHANGE = "CHANGE"
|
|
51
|
+
ERROR = "ERROR"
|
|
52
|
+
OBSERVABILITY = "OBSERVABILITY"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TelemetryEventType(StrEnum):
|
|
56
|
+
RUN_STARTED = "run.started"
|
|
57
|
+
REPOSITORY_EXPLORED = "repository.explored"
|
|
58
|
+
CONTEXT_BUILT = "context.built"
|
|
59
|
+
PLAN_CREATED = "plan.created"
|
|
60
|
+
APPROVAL_REQUESTED = "approval.requested"
|
|
61
|
+
APPROVAL_RESOLVED = "approval.resolved"
|
|
62
|
+
AGENT_STEP_COMPLETED = "agent_step.completed"
|
|
63
|
+
MODEL_CALL_STARTED = "model_call.started"
|
|
64
|
+
MODEL_CALL_FINISHED = "model_call.finished"
|
|
65
|
+
TOOL_STARTED = "tool.started"
|
|
66
|
+
TOOL_FINISHED = "tool.finished"
|
|
67
|
+
REPLAN_OCCURRED = "replan.occurred"
|
|
68
|
+
VALIDATION_STARTED = "validation.started"
|
|
69
|
+
VALIDATION_FINISHED = "validation.finished"
|
|
70
|
+
REPAIR_STARTED = "repair.started"
|
|
71
|
+
CHANGED_FILE_RECORDED = "changed_file.recorded"
|
|
72
|
+
RUN_INTERRUPTED = "run.interrupted"
|
|
73
|
+
RUN_FINISHED = "run.finished"
|
|
74
|
+
ERROR_OCCURRED = "error.occurred"
|
|
75
|
+
OBSERVABILITY_WARNING = "observability.warning"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
EVENT_PHASES: Mapping[TelemetryEventType, RuntimePhase] = MappingProxyType(
|
|
79
|
+
{
|
|
80
|
+
TelemetryEventType.RUN_STARTED: RuntimePhase.RUN,
|
|
81
|
+
TelemetryEventType.RUN_INTERRUPTED: RuntimePhase.RUN,
|
|
82
|
+
TelemetryEventType.RUN_FINISHED: RuntimePhase.RUN,
|
|
83
|
+
TelemetryEventType.REPOSITORY_EXPLORED: RuntimePhase.REPOSITORY,
|
|
84
|
+
TelemetryEventType.CONTEXT_BUILT: RuntimePhase.CONTEXT,
|
|
85
|
+
TelemetryEventType.PLAN_CREATED: RuntimePhase.PLAN,
|
|
86
|
+
TelemetryEventType.APPROVAL_REQUESTED: RuntimePhase.APPROVAL,
|
|
87
|
+
TelemetryEventType.APPROVAL_RESOLVED: RuntimePhase.APPROVAL,
|
|
88
|
+
TelemetryEventType.AGENT_STEP_COMPLETED: RuntimePhase.AGENT,
|
|
89
|
+
TelemetryEventType.MODEL_CALL_STARTED: RuntimePhase.MODEL,
|
|
90
|
+
TelemetryEventType.MODEL_CALL_FINISHED: RuntimePhase.MODEL,
|
|
91
|
+
TelemetryEventType.TOOL_STARTED: RuntimePhase.TOOL,
|
|
92
|
+
TelemetryEventType.TOOL_FINISHED: RuntimePhase.TOOL,
|
|
93
|
+
TelemetryEventType.REPLAN_OCCURRED: RuntimePhase.REPLAN,
|
|
94
|
+
TelemetryEventType.VALIDATION_STARTED: RuntimePhase.VALIDATION,
|
|
95
|
+
TelemetryEventType.VALIDATION_FINISHED: RuntimePhase.VALIDATION,
|
|
96
|
+
TelemetryEventType.REPAIR_STARTED: RuntimePhase.REPAIR,
|
|
97
|
+
TelemetryEventType.CHANGED_FILE_RECORDED: RuntimePhase.CHANGE,
|
|
98
|
+
TelemetryEventType.ERROR_OCCURRED: RuntimePhase.ERROR,
|
|
99
|
+
TelemetryEventType.OBSERVABILITY_WARNING: RuntimePhase.OBSERVABILITY,
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
105
|
+
class RunExecutionContext:
|
|
106
|
+
trace_id: str
|
|
107
|
+
execution_id: str
|
|
108
|
+
run_id: str
|
|
109
|
+
session_id: str | None
|
|
110
|
+
is_resume: bool
|
|
111
|
+
started_at: datetime
|
|
112
|
+
|
|
113
|
+
def __post_init__(self) -> None:
|
|
114
|
+
_validate_uuid(self.trace_id, "trace_id")
|
|
115
|
+
_validate_uuid(self.execution_id, "execution_id")
|
|
116
|
+
_validate_uuid(self.run_id, "run_id")
|
|
117
|
+
if self.session_id is not None:
|
|
118
|
+
_validate_uuid(self.session_id, "session_id")
|
|
119
|
+
if self.trace_id != self.run_id:
|
|
120
|
+
raise ValueError("trace_id must equal run_id for Nexus V1.")
|
|
121
|
+
_validate_utc(self.started_at, "started_at")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
125
|
+
class RedactionMetadata:
|
|
126
|
+
policy_version: str = "1.0"
|
|
127
|
+
redacted: bool = False
|
|
128
|
+
omitted_fields: tuple[str, ...] = ()
|
|
129
|
+
truncated_fields: tuple[str, ...] = ()
|
|
130
|
+
|
|
131
|
+
def __post_init__(self) -> None:
|
|
132
|
+
if self.policy_version != "1.0":
|
|
133
|
+
raise ValueError("Unsupported redaction policy version.")
|
|
134
|
+
omitted = tuple(self.omitted_fields)
|
|
135
|
+
truncated = tuple(self.truncated_fields)
|
|
136
|
+
if any(not value or len(value) > 256 for value in (*omitted, *truncated)):
|
|
137
|
+
raise ValueError("Redaction field categories are invalid.")
|
|
138
|
+
object.__setattr__(self, "omitted_fields", omitted)
|
|
139
|
+
object.__setattr__(self, "truncated_fields", truncated)
|
|
140
|
+
if self.redacted != bool(omitted or truncated):
|
|
141
|
+
raise ValueError("Redaction metadata flag is inconsistent.")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
145
|
+
class TelemetryEvent:
|
|
146
|
+
event_id: str
|
|
147
|
+
event_type: TelemetryEventType
|
|
148
|
+
timestamp: datetime
|
|
149
|
+
sequence: int
|
|
150
|
+
trace_id: str
|
|
151
|
+
execution_id: str
|
|
152
|
+
run_id: str
|
|
153
|
+
session_id: str | None
|
|
154
|
+
span_id: str | None
|
|
155
|
+
parent_span_id: str | None
|
|
156
|
+
phase: RuntimePhase
|
|
157
|
+
severity: TelemetrySeverity
|
|
158
|
+
payload: Mapping[str, object]
|
|
159
|
+
redaction: RedactionMetadata
|
|
160
|
+
schema_version: str = field(default="1.0", init=False)
|
|
161
|
+
|
|
162
|
+
def __post_init__(self) -> None:
|
|
163
|
+
if self.schema_version != "1.0":
|
|
164
|
+
raise ValueError("Unsupported telemetry schema version.")
|
|
165
|
+
for name, value in (
|
|
166
|
+
("event_id", self.event_id),
|
|
167
|
+
("trace_id", self.trace_id),
|
|
168
|
+
("execution_id", self.execution_id),
|
|
169
|
+
("run_id", self.run_id),
|
|
170
|
+
):
|
|
171
|
+
_validate_uuid(value, name)
|
|
172
|
+
if self.session_id is not None:
|
|
173
|
+
_validate_uuid(self.session_id, "session_id")
|
|
174
|
+
if self.span_id is not None:
|
|
175
|
+
_validate_uuid(self.span_id, "span_id")
|
|
176
|
+
if self.parent_span_id is not None:
|
|
177
|
+
_validate_uuid(self.parent_span_id, "parent_span_id")
|
|
178
|
+
if self.trace_id != self.run_id:
|
|
179
|
+
raise ValueError("Telemetry trace_id must equal run_id.")
|
|
180
|
+
if self.sequence < 1:
|
|
181
|
+
raise ValueError("Telemetry sequence must be positive.")
|
|
182
|
+
_validate_utc(self.timestamp, "timestamp")
|
|
183
|
+
if EVENT_PHASES[self.event_type] is not self.phase:
|
|
184
|
+
raise ValueError("Telemetry RuntimePhase does not match event type.")
|
|
185
|
+
frozen = _freeze_json(dict(self.payload), depth=0)
|
|
186
|
+
success_value = self.payload.get("success")
|
|
187
|
+
model_call_success = success_value if isinstance(success_value, bool) else None
|
|
188
|
+
expected_severity = telemetry_severity(
|
|
189
|
+
self.event_type,
|
|
190
|
+
model_call_success=model_call_success,
|
|
191
|
+
)
|
|
192
|
+
if self.severity is not expected_severity:
|
|
193
|
+
raise ValueError("Telemetry severity does not match the Day 8 mapping.")
|
|
194
|
+
serialized = json.dumps(_thaw_json(frozen), separators=(",", ":"), ensure_ascii=False)
|
|
195
|
+
if len(serialized.encode("utf-8")) > 32 * 1024:
|
|
196
|
+
raise ValueError("Telemetry payload exceeds the 32 KiB limit.")
|
|
197
|
+
object.__setattr__(self, "payload", frozen)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
201
|
+
class TraceRunStart:
|
|
202
|
+
context: RunExecutionContext
|
|
203
|
+
task_character_count: int
|
|
204
|
+
model_provider: str | None
|
|
205
|
+
model_name: str | None
|
|
206
|
+
|
|
207
|
+
def __post_init__(self) -> None:
|
|
208
|
+
if self.task_character_count < 0:
|
|
209
|
+
raise ValueError("task_character_count must not be negative.")
|
|
210
|
+
_validate_safe_identifier(self.model_provider, "model_provider")
|
|
211
|
+
_validate_safe_identifier(self.model_name, "model_name")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
215
|
+
class TraceRunFinish:
|
|
216
|
+
context: RunExecutionContext
|
|
217
|
+
finished_at: datetime
|
|
218
|
+
execution_outcome: ExecutionOutcome
|
|
219
|
+
runtime_status: RuntimeStatus
|
|
220
|
+
terminal_status: TerminalStatus | None
|
|
221
|
+
duration_ms: int
|
|
222
|
+
step_count: int
|
|
223
|
+
llm_call_count: int
|
|
224
|
+
tool_call_count: int
|
|
225
|
+
replan_count: int
|
|
226
|
+
repair_count: int
|
|
227
|
+
token_usage: TokenUsage
|
|
228
|
+
changed_file_count: int
|
|
229
|
+
validation_status: TraceValidationStatus
|
|
230
|
+
error_code: str | None
|
|
231
|
+
|
|
232
|
+
def __post_init__(self) -> None:
|
|
233
|
+
_validate_utc(self.finished_at, "finished_at")
|
|
234
|
+
counters = (
|
|
235
|
+
self.duration_ms,
|
|
236
|
+
self.step_count,
|
|
237
|
+
self.llm_call_count,
|
|
238
|
+
self.tool_call_count,
|
|
239
|
+
self.replan_count,
|
|
240
|
+
self.repair_count,
|
|
241
|
+
self.changed_file_count,
|
|
242
|
+
)
|
|
243
|
+
if any(value < 0 for value in counters):
|
|
244
|
+
raise ValueError("Trace finish counters and duration must not be negative.")
|
|
245
|
+
if self.error_code is not None:
|
|
246
|
+
_validate_safe_identifier(self.error_code, "error_code")
|
|
247
|
+
if self.execution_outcome.value != self.runtime_status.value:
|
|
248
|
+
raise ValueError("Execution outcome must preserve the terminal RuntimeStatus.")
|
|
249
|
+
if self.runtime_status is RuntimeStatus.COMPLETED:
|
|
250
|
+
if self.terminal_status is not TerminalStatus.SUCCEEDED:
|
|
251
|
+
raise ValueError("Completed trace requires SUCCEEDED terminal status.")
|
|
252
|
+
elif self.runtime_status is RuntimeStatus.FAILED:
|
|
253
|
+
if self.terminal_status is TerminalStatus.SUCCEEDED:
|
|
254
|
+
raise ValueError("Failed trace cannot carry SUCCEEDED terminal status.")
|
|
255
|
+
elif self.runtime_status is RuntimeStatus.INTERRUPTED:
|
|
256
|
+
if self.terminal_status is not None:
|
|
257
|
+
raise ValueError("Interrupted trace cannot carry a terminal status.")
|
|
258
|
+
else:
|
|
259
|
+
raise ValueError("Trace finish requires a terminal RuntimeStatus.")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def telemetry_severity(
|
|
263
|
+
event_type: TelemetryEventType, *, model_call_success: bool | None = None
|
|
264
|
+
) -> TelemetrySeverity:
|
|
265
|
+
if event_type is TelemetryEventType.OBSERVABILITY_WARNING:
|
|
266
|
+
return TelemetrySeverity.WARNING
|
|
267
|
+
if event_type is TelemetryEventType.ERROR_OCCURRED:
|
|
268
|
+
return TelemetrySeverity.ERROR
|
|
269
|
+
if (
|
|
270
|
+
event_type is TelemetryEventType.MODEL_CALL_FINISHED
|
|
271
|
+
and model_call_success is False
|
|
272
|
+
):
|
|
273
|
+
return TelemetrySeverity.ERROR
|
|
274
|
+
return TelemetrySeverity.INFO
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def telemetry_payload_dict(value: Mapping[str, object]) -> dict[str, object]:
|
|
278
|
+
"""Return a JSON-compatible mutable copy for sink serialization."""
|
|
279
|
+
|
|
280
|
+
thawed = _thaw_json(value)
|
|
281
|
+
if not isinstance(thawed, dict):
|
|
282
|
+
raise ValueError("Telemetry payload root must be an object.")
|
|
283
|
+
return thawed
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _validate_uuid(value: str, field_name: str) -> None:
|
|
287
|
+
try:
|
|
288
|
+
parsed = UUID(value)
|
|
289
|
+
except (TypeError, ValueError) as exc:
|
|
290
|
+
raise ValueError(f"{field_name} must be a UUID string.") from exc
|
|
291
|
+
if str(parsed) != value:
|
|
292
|
+
raise ValueError(f"{field_name} must use canonical UUID text.")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _validate_utc(value: datetime, field_name: str) -> None:
|
|
296
|
+
if value.tzinfo is None or value.utcoffset() != timedelta(0):
|
|
297
|
+
raise ValueError(f"{field_name} must be timezone-aware UTC.")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _validate_safe_identifier(value: str | None, field_name: str) -> None:
|
|
301
|
+
if value is not None and (not value.strip() or len(value) > 256):
|
|
302
|
+
raise ValueError(f"{field_name} must be a bounded non-empty identifier.")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _freeze_json(value: object, *, depth: int) -> object:
|
|
306
|
+
if depth > 4:
|
|
307
|
+
raise ValueError("Telemetry payload exceeds maximum depth.")
|
|
308
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
309
|
+
return value
|
|
310
|
+
if isinstance(value, Mapping):
|
|
311
|
+
if any(not isinstance(key, str) for key in value):
|
|
312
|
+
raise ValueError("Telemetry payload keys must be strings.")
|
|
313
|
+
return MappingProxyType(
|
|
314
|
+
{key: _freeze_json(item, depth=depth + 1) for key, item in value.items()}
|
|
315
|
+
)
|
|
316
|
+
if isinstance(value, (list, tuple)):
|
|
317
|
+
return tuple(_freeze_json(item, depth=depth + 1) for item in value)
|
|
318
|
+
raise ValueError("Telemetry payload must contain only JSON-compatible values.")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _thaw_json(value: object) -> object:
|
|
322
|
+
if isinstance(value, Mapping):
|
|
323
|
+
return {key: _thaw_json(item) for key, item in value.items()}
|
|
324
|
+
if isinstance(value, tuple):
|
|
325
|
+
return [_thaw_json(item) for item in value]
|
|
326
|
+
return value
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Nexus-owned Day 2 business persistence models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
JsonObject = dict[str, object]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RunStatus(StrEnum):
|
|
14
|
+
"""The complete Day 2 run lifecycle."""
|
|
15
|
+
|
|
16
|
+
RUNNING = "RUNNING"
|
|
17
|
+
INTERRUPTED = "INTERRUPTED"
|
|
18
|
+
COMPLETED = "COMPLETED"
|
|
19
|
+
FAILED = "FAILED"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class Repository:
|
|
24
|
+
repository_id: str
|
|
25
|
+
canonical_path: str
|
|
26
|
+
metadata: JsonObject
|
|
27
|
+
configuration_reference: str | None
|
|
28
|
+
created_at: datetime
|
|
29
|
+
|
|
30
|
+
def __post_init__(self) -> None:
|
|
31
|
+
_validate_uuid(self.repository_id, "repository_id")
|
|
32
|
+
_validate_timestamp(self.created_at, "created_at")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True)
|
|
36
|
+
class NexusSession:
|
|
37
|
+
session_id: str
|
|
38
|
+
repository_id: str
|
|
39
|
+
created_at: datetime
|
|
40
|
+
last_active_at: datetime
|
|
41
|
+
configuration_reference: str | None
|
|
42
|
+
|
|
43
|
+
def __post_init__(self) -> None:
|
|
44
|
+
_validate_uuid(self.session_id, "session_id")
|
|
45
|
+
_validate_uuid(self.repository_id, "repository_id")
|
|
46
|
+
_validate_timestamp(self.created_at, "created_at")
|
|
47
|
+
_validate_timestamp(self.last_active_at, "last_active_at")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True, slots=True)
|
|
51
|
+
class Run:
|
|
52
|
+
run_id: str
|
|
53
|
+
session_id: str
|
|
54
|
+
task: str
|
|
55
|
+
status: RunStatus
|
|
56
|
+
model_metadata: JsonObject | None
|
|
57
|
+
started_at: datetime
|
|
58
|
+
finished_at: datetime | None
|
|
59
|
+
token_count: int
|
|
60
|
+
tool_call_count: int
|
|
61
|
+
changed_file_refs: list[str] | None
|
|
62
|
+
final_outcome: JsonObject | None
|
|
63
|
+
graph_thread_id: str
|
|
64
|
+
|
|
65
|
+
def __post_init__(self) -> None:
|
|
66
|
+
_validate_uuid(self.run_id, "run_id")
|
|
67
|
+
_validate_uuid(self.session_id, "session_id")
|
|
68
|
+
_validate_timestamp(self.started_at, "started_at")
|
|
69
|
+
if self.finished_at is not None:
|
|
70
|
+
_validate_timestamp(self.finished_at, "finished_at")
|
|
71
|
+
if not self.task:
|
|
72
|
+
raise ValueError("Run task must not be empty.")
|
|
73
|
+
if not self.graph_thread_id:
|
|
74
|
+
raise ValueError("Run graph_thread_id must not be empty.")
|
|
75
|
+
if self.token_count < 0 or self.tool_call_count < 0:
|
|
76
|
+
raise ValueError("Run counters must not be negative.")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class SessionTurn:
|
|
81
|
+
id: str
|
|
82
|
+
session_id: str
|
|
83
|
+
run_id: str | None
|
|
84
|
+
sequence: int
|
|
85
|
+
role: str
|
|
86
|
+
content: str
|
|
87
|
+
metadata: JsonObject | None
|
|
88
|
+
created_at: datetime
|
|
89
|
+
|
|
90
|
+
def __post_init__(self) -> None:
|
|
91
|
+
_validate_uuid(self.id, "id")
|
|
92
|
+
_validate_uuid(self.session_id, "session_id")
|
|
93
|
+
if self.run_id is not None:
|
|
94
|
+
_validate_uuid(self.run_id, "run_id")
|
|
95
|
+
_validate_timestamp(self.created_at, "created_at")
|
|
96
|
+
if self.sequence < 1:
|
|
97
|
+
raise ValueError("SessionTurn sequence must be positive.")
|
|
98
|
+
if not self.role:
|
|
99
|
+
raise ValueError("SessionTurn role must not be empty.")
|
|
100
|
+
if not self.content:
|
|
101
|
+
raise ValueError("SessionTurn content must not be empty.")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True, slots=True)
|
|
105
|
+
class SessionSummary:
|
|
106
|
+
session_id: str
|
|
107
|
+
last_active_at: datetime
|
|
108
|
+
resumable: bool
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _validate_uuid(value: str, field_name: str) -> None:
|
|
112
|
+
try:
|
|
113
|
+
parsed = UUID(value)
|
|
114
|
+
except ValueError as exc:
|
|
115
|
+
raise ValueError(f"{field_name} must be a UUID string.") from exc
|
|
116
|
+
if str(parsed) != value:
|
|
117
|
+
raise ValueError(f"{field_name} must use canonical UUID text.")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _validate_timestamp(value: datetime, field_name: str) -> None:
|
|
121
|
+
if value.tzinfo is None or value.utcoffset() is None:
|
|
122
|
+
raise ValueError(f"{field_name} must be timezone-aware.")
|