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