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,396 @@
|
|
|
1
|
+
"""Strict allowlist conversion from business RuntimeEvents to TelemetryEvent v1.0."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
from nexus.application.event_publisher import PublishedObservation
|
|
11
|
+
from nexus.domain.model import TokenUsage
|
|
12
|
+
from nexus.domain.observability import (
|
|
13
|
+
EVENT_PHASES,
|
|
14
|
+
RedactionMetadata,
|
|
15
|
+
TelemetryEvent,
|
|
16
|
+
TelemetryEventType,
|
|
17
|
+
telemetry_payload_dict,
|
|
18
|
+
telemetry_severity,
|
|
19
|
+
)
|
|
20
|
+
from nexus.domain.runtime_events import (
|
|
21
|
+
AgentStepCompleted,
|
|
22
|
+
ApprovalRequested,
|
|
23
|
+
ApprovalResolved,
|
|
24
|
+
ChangedFileRecorded,
|
|
25
|
+
ContextBuilt,
|
|
26
|
+
ErrorOccurred,
|
|
27
|
+
FinalResult,
|
|
28
|
+
ModelCallFinished,
|
|
29
|
+
ModelCallStarted,
|
|
30
|
+
ObservabilityWarning,
|
|
31
|
+
PlanCreated,
|
|
32
|
+
RepairStarted,
|
|
33
|
+
ReplanOccurred,
|
|
34
|
+
RepositoryExplored,
|
|
35
|
+
RunInterrupted,
|
|
36
|
+
RuntimeEvent,
|
|
37
|
+
TaskStarted,
|
|
38
|
+
ToolFinished,
|
|
39
|
+
ToolStarted,
|
|
40
|
+
ValidationFinished,
|
|
41
|
+
ValidationStarted,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class EventEnricher:
|
|
46
|
+
"""Build only approved safe fields; arbitrary RuntimeEvent serialization is forbidden."""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
workspace: Path,
|
|
51
|
+
tool_sources: dict[str, str] | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
self._workspace = workspace.resolve()
|
|
54
|
+
self._tool_sources = dict(tool_sources or {})
|
|
55
|
+
self._validation_spans: dict[str, str] = {}
|
|
56
|
+
|
|
57
|
+
def enrich(
|
|
58
|
+
self,
|
|
59
|
+
event: RuntimeEvent,
|
|
60
|
+
observation: PublishedObservation,
|
|
61
|
+
*,
|
|
62
|
+
tool_call_count: int | None = None,
|
|
63
|
+
) -> TelemetryEvent:
|
|
64
|
+
event_type, payload, omitted = self._payload(
|
|
65
|
+
event, tool_call_count=tool_call_count
|
|
66
|
+
)
|
|
67
|
+
span_id, parent_span_id = self._spans(event, event_type, observation)
|
|
68
|
+
success = event.success if isinstance(event, ModelCallFinished) else None
|
|
69
|
+
return TelemetryEvent(
|
|
70
|
+
event_id=str(uuid4()),
|
|
71
|
+
event_type=event_type,
|
|
72
|
+
timestamp=event.timestamp,
|
|
73
|
+
sequence=observation.sequence,
|
|
74
|
+
trace_id=observation.context.trace_id,
|
|
75
|
+
execution_id=observation.context.execution_id,
|
|
76
|
+
run_id=event.run_id,
|
|
77
|
+
session_id=event.session_id,
|
|
78
|
+
span_id=span_id,
|
|
79
|
+
parent_span_id=parent_span_id,
|
|
80
|
+
phase=EVENT_PHASES[event_type],
|
|
81
|
+
severity=telemetry_severity(event_type, model_call_success=success),
|
|
82
|
+
payload=payload,
|
|
83
|
+
redaction=RedactionMetadata(
|
|
84
|
+
redacted=bool(omitted),
|
|
85
|
+
omitted_fields=omitted,
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def _payload(
|
|
90
|
+
self, event: RuntimeEvent, *, tool_call_count: int | None
|
|
91
|
+
) -> tuple[TelemetryEventType, dict[str, object], tuple[str, ...]]:
|
|
92
|
+
if isinstance(event, TaskStarted):
|
|
93
|
+
return (
|
|
94
|
+
TelemetryEventType.RUN_STARTED,
|
|
95
|
+
{"task_character_count": len(event.task)},
|
|
96
|
+
("task",),
|
|
97
|
+
)
|
|
98
|
+
if isinstance(event, RepositoryExplored):
|
|
99
|
+
return (
|
|
100
|
+
TelemetryEventType.REPOSITORY_EXPLORED,
|
|
101
|
+
{
|
|
102
|
+
"instruction_path_count": len(event.instruction_paths),
|
|
103
|
+
"manifest_path_count": len(event.manifest_paths),
|
|
104
|
+
"relevant_path_count": len(event.relevant_paths),
|
|
105
|
+
"exploration_tool_calls": event.exploration_tool_calls,
|
|
106
|
+
"truncated": event.truncated,
|
|
107
|
+
},
|
|
108
|
+
("repository_paths",),
|
|
109
|
+
)
|
|
110
|
+
if isinstance(event, ContextBuilt):
|
|
111
|
+
return (
|
|
112
|
+
TelemetryEventType.CONTEXT_BUILT,
|
|
113
|
+
{
|
|
114
|
+
"selected_path_count": len(event.selected_paths),
|
|
115
|
+
"retained_characters": event.retained_characters,
|
|
116
|
+
"truncated": event.truncated,
|
|
117
|
+
"selected_chunk_count": event.selected_chunk_count,
|
|
118
|
+
"semantic_retrieval_used": event.semantic_retrieval_used,
|
|
119
|
+
"semantic_retrieval_status": event.semantic_retrieval_status,
|
|
120
|
+
"selected_skill_ids": list(event.selected_skill_ids),
|
|
121
|
+
"selected_skill_count": len(event.selected_skill_ids),
|
|
122
|
+
"selection_reason_present": bool(event.skill_selection_reason_summary),
|
|
123
|
+
},
|
|
124
|
+
("repository_paths", "skill_selection_reason"),
|
|
125
|
+
)
|
|
126
|
+
if isinstance(event, PlanCreated):
|
|
127
|
+
return (
|
|
128
|
+
TelemetryEventType.PLAN_CREATED,
|
|
129
|
+
{
|
|
130
|
+
"plan_id": event.plan_id,
|
|
131
|
+
"plan_version": event.plan_version,
|
|
132
|
+
"plan_kind": event.plan_kind.value,
|
|
133
|
+
"step_count": len(event.step_summaries),
|
|
134
|
+
"replan_reason_present": bool(event.replan_reason),
|
|
135
|
+
},
|
|
136
|
+
("step_summaries", "replan_reason"),
|
|
137
|
+
)
|
|
138
|
+
if isinstance(event, ApprovalRequested):
|
|
139
|
+
return (
|
|
140
|
+
TelemetryEventType.APPROVAL_REQUESTED,
|
|
141
|
+
{
|
|
142
|
+
"approval_id": event.approval_id,
|
|
143
|
+
"subject": event.subject.value,
|
|
144
|
+
"invocation_id": event.invocation_id,
|
|
145
|
+
"plan_id": event.plan_id,
|
|
146
|
+
"plan_version": event.plan_version,
|
|
147
|
+
"operation": event.operation,
|
|
148
|
+
"risk_level": event.risk_level.value,
|
|
149
|
+
},
|
|
150
|
+
("resource_or_command_summary",),
|
|
151
|
+
)
|
|
152
|
+
if isinstance(event, ApprovalResolved):
|
|
153
|
+
return (
|
|
154
|
+
TelemetryEventType.APPROVAL_RESOLVED,
|
|
155
|
+
{
|
|
156
|
+
"approval_id": event.approval_id,
|
|
157
|
+
"subject": event.subject.value,
|
|
158
|
+
"invocation_id": event.invocation_id,
|
|
159
|
+
"plan_id": event.plan_id,
|
|
160
|
+
"plan_version": event.plan_version,
|
|
161
|
+
"decision": event.decision.value,
|
|
162
|
+
"actor_category": event.actor_category.value,
|
|
163
|
+
},
|
|
164
|
+
(),
|
|
165
|
+
)
|
|
166
|
+
if isinstance(event, AgentStepCompleted):
|
|
167
|
+
return (
|
|
168
|
+
TelemetryEventType.AGENT_STEP_COMPLETED,
|
|
169
|
+
{
|
|
170
|
+
"step_count": event.step_count,
|
|
171
|
+
"decision_kind": event.decision_kind.value,
|
|
172
|
+
"model_call_id": event.model_call_id,
|
|
173
|
+
},
|
|
174
|
+
(),
|
|
175
|
+
)
|
|
176
|
+
if isinstance(event, ModelCallStarted):
|
|
177
|
+
return (
|
|
178
|
+
TelemetryEventType.MODEL_CALL_STARTED,
|
|
179
|
+
{
|
|
180
|
+
"model_call_id": event.model_call_id,
|
|
181
|
+
"model_call_phase": event.phase.value,
|
|
182
|
+
"provider": event.provider,
|
|
183
|
+
"model": event.model,
|
|
184
|
+
},
|
|
185
|
+
(),
|
|
186
|
+
)
|
|
187
|
+
if isinstance(event, ModelCallFinished):
|
|
188
|
+
return (
|
|
189
|
+
TelemetryEventType.MODEL_CALL_FINISHED,
|
|
190
|
+
{
|
|
191
|
+
"model_call_id": event.model_call_id,
|
|
192
|
+
"model_call_phase": event.phase.value,
|
|
193
|
+
"success": event.success,
|
|
194
|
+
"duration_ms": event.duration_ms,
|
|
195
|
+
"usage": _usage_payload(event.usage),
|
|
196
|
+
"error_code": event.error_code,
|
|
197
|
+
},
|
|
198
|
+
(),
|
|
199
|
+
)
|
|
200
|
+
if isinstance(event, ToolStarted):
|
|
201
|
+
return (
|
|
202
|
+
TelemetryEventType.TOOL_STARTED,
|
|
203
|
+
{
|
|
204
|
+
"invocation_id": event.invocation_id,
|
|
205
|
+
"tool_name": event.tool_name,
|
|
206
|
+
"source": self._tool_sources.get(event.tool_name, "UNKNOWN"),
|
|
207
|
+
"proposed_risk": event.risk_level.value,
|
|
208
|
+
"tool_call_count": tool_call_count,
|
|
209
|
+
},
|
|
210
|
+
(),
|
|
211
|
+
)
|
|
212
|
+
if isinstance(event, ToolFinished):
|
|
213
|
+
return (
|
|
214
|
+
TelemetryEventType.TOOL_FINISHED,
|
|
215
|
+
{
|
|
216
|
+
"invocation_id": event.invocation_id,
|
|
217
|
+
"tool_name": event.tool_name,
|
|
218
|
+
"source": self._tool_sources.get(event.tool_name, "UNKNOWN"),
|
|
219
|
+
"success": event.success,
|
|
220
|
+
"final_risk": event.risk_level.value,
|
|
221
|
+
"policy_decision": event.policy_decision.value,
|
|
222
|
+
"approval_decision": (
|
|
223
|
+
None
|
|
224
|
+
if event.approval_decision is None
|
|
225
|
+
else event.approval_decision.value
|
|
226
|
+
),
|
|
227
|
+
"duration_ms": event.duration_ms,
|
|
228
|
+
"error_code": event.error_code,
|
|
229
|
+
},
|
|
230
|
+
(),
|
|
231
|
+
)
|
|
232
|
+
if isinstance(event, ReplanOccurred):
|
|
233
|
+
return (
|
|
234
|
+
TelemetryEventType.REPLAN_OCCURRED,
|
|
235
|
+
{
|
|
236
|
+
"plan_id": event.plan_id,
|
|
237
|
+
"previous_version": event.previous_version,
|
|
238
|
+
"new_version": event.new_version,
|
|
239
|
+
"replan_count": event.replan_count,
|
|
240
|
+
"reason_present": bool(event.reason),
|
|
241
|
+
},
|
|
242
|
+
("reason",),
|
|
243
|
+
)
|
|
244
|
+
if isinstance(event, ValidationStarted):
|
|
245
|
+
return (
|
|
246
|
+
TelemetryEventType.VALIDATION_STARTED,
|
|
247
|
+
{
|
|
248
|
+
"check_ids": list(event.check_ids),
|
|
249
|
+
"check_kinds": [value.value for value in event.check_kinds],
|
|
250
|
+
},
|
|
251
|
+
(),
|
|
252
|
+
)
|
|
253
|
+
if isinstance(event, ValidationFinished):
|
|
254
|
+
return (
|
|
255
|
+
TelemetryEventType.VALIDATION_FINISHED,
|
|
256
|
+
{
|
|
257
|
+
"validation_status": event.validation_status.value,
|
|
258
|
+
"confidence": event.confidence.value,
|
|
259
|
+
"executed_check_count": event.executed_check_count,
|
|
260
|
+
"repair_count": event.repair_count,
|
|
261
|
+
"duration_ms": event.duration_ms,
|
|
262
|
+
},
|
|
263
|
+
(),
|
|
264
|
+
)
|
|
265
|
+
if isinstance(event, RepairStarted):
|
|
266
|
+
return (
|
|
267
|
+
TelemetryEventType.REPAIR_STARTED,
|
|
268
|
+
{
|
|
269
|
+
"plan_id": event.plan_id,
|
|
270
|
+
"plan_version": event.plan_version,
|
|
271
|
+
"repair_count": event.repair_count,
|
|
272
|
+
"max_repair_attempts": event.max_repair_attempts,
|
|
273
|
+
"failure_summary_present": bool(event.failure_summary),
|
|
274
|
+
},
|
|
275
|
+
("failure_summary",),
|
|
276
|
+
)
|
|
277
|
+
if isinstance(event, ChangedFileRecorded):
|
|
278
|
+
self._validate_changed_path(event.relative_path)
|
|
279
|
+
return (
|
|
280
|
+
TelemetryEventType.CHANGED_FILE_RECORDED,
|
|
281
|
+
{
|
|
282
|
+
"invocation_id": event.invocation_id,
|
|
283
|
+
"relative_path": event.relative_path.replace("\\", "/"),
|
|
284
|
+
"change_kind": event.change_kind.value,
|
|
285
|
+
"changed_file_count": event.changed_file_count,
|
|
286
|
+
},
|
|
287
|
+
(),
|
|
288
|
+
)
|
|
289
|
+
if isinstance(event, RunInterrupted):
|
|
290
|
+
return TelemetryEventType.RUN_INTERRUPTED, {}, ()
|
|
291
|
+
if isinstance(event, FinalResult):
|
|
292
|
+
return (
|
|
293
|
+
TelemetryEventType.RUN_FINISHED,
|
|
294
|
+
{
|
|
295
|
+
"runtime_status": event.status.value,
|
|
296
|
+
"terminal_status": event.terminal_status.value,
|
|
297
|
+
"changed_file_count": len(event.changed_files),
|
|
298
|
+
"validation_status": (
|
|
299
|
+
None
|
|
300
|
+
if event.validation_result is None
|
|
301
|
+
else event.validation_result.status.value
|
|
302
|
+
),
|
|
303
|
+
},
|
|
304
|
+
("content", "diff", "validation_evidence"),
|
|
305
|
+
)
|
|
306
|
+
if isinstance(event, ErrorOccurred):
|
|
307
|
+
return (
|
|
308
|
+
TelemetryEventType.ERROR_OCCURRED,
|
|
309
|
+
{"error_code": event.code, "retryable": event.retryable},
|
|
310
|
+
("error_message",),
|
|
311
|
+
)
|
|
312
|
+
if isinstance(event, ObservabilityWarning):
|
|
313
|
+
return (
|
|
314
|
+
TelemetryEventType.OBSERVABILITY_WARNING,
|
|
315
|
+
{
|
|
316
|
+
"code": event.code,
|
|
317
|
+
"sink": event.sink.value,
|
|
318
|
+
"operation": event.operation.value,
|
|
319
|
+
"fallback": event.fallback.value,
|
|
320
|
+
},
|
|
321
|
+
(),
|
|
322
|
+
)
|
|
323
|
+
raise ValueError("RuntimeEvent type is not registered for Day 8 telemetry.")
|
|
324
|
+
|
|
325
|
+
def _spans(
|
|
326
|
+
self,
|
|
327
|
+
event: RuntimeEvent,
|
|
328
|
+
event_type: TelemetryEventType,
|
|
329
|
+
observation: PublishedObservation,
|
|
330
|
+
) -> tuple[str | None, str | None]:
|
|
331
|
+
root = observation.context.execution_id
|
|
332
|
+
if event_type in {
|
|
333
|
+
TelemetryEventType.RUN_STARTED,
|
|
334
|
+
TelemetryEventType.RUN_INTERRUPTED,
|
|
335
|
+
TelemetryEventType.RUN_FINISHED,
|
|
336
|
+
TelemetryEventType.ERROR_OCCURRED,
|
|
337
|
+
}:
|
|
338
|
+
return root, None
|
|
339
|
+
if isinstance(event, (ModelCallStarted, ModelCallFinished)):
|
|
340
|
+
return event.model_call_id, root
|
|
341
|
+
if isinstance(event, (ToolStarted, ToolFinished)):
|
|
342
|
+
parent = self._validation_spans.get(observation.context.execution_id, root)
|
|
343
|
+
return event.invocation_id, parent
|
|
344
|
+
if isinstance(event, ValidationStarted):
|
|
345
|
+
span_id = str(uuid4())
|
|
346
|
+
self._validation_spans[observation.context.execution_id] = span_id
|
|
347
|
+
return span_id, root
|
|
348
|
+
if isinstance(event, ValidationFinished):
|
|
349
|
+
return self._validation_spans.pop(observation.context.execution_id, root), root
|
|
350
|
+
return None, root
|
|
351
|
+
|
|
352
|
+
def _validate_changed_path(self, relative_path: str) -> None:
|
|
353
|
+
candidate = (self._workspace / relative_path).resolve()
|
|
354
|
+
try:
|
|
355
|
+
candidate.relative_to(self._workspace)
|
|
356
|
+
except ValueError as exc:
|
|
357
|
+
raise ValueError("Changed-file path escapes the workspace.") from exc
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _usage_payload(usage: TokenUsage) -> dict[str, object]:
|
|
361
|
+
return {
|
|
362
|
+
"input_tokens": usage.input_tokens,
|
|
363
|
+
"output_tokens": usage.output_tokens,
|
|
364
|
+
"total_tokens": usage.total_tokens,
|
|
365
|
+
"availability": usage.availability.value,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class TelemetryRedactor:
|
|
370
|
+
"""Defense-in-depth rejection after the primary event allowlist."""
|
|
371
|
+
|
|
372
|
+
_FORBIDDEN = tuple(
|
|
373
|
+
re.compile(pattern, re.IGNORECASE)
|
|
374
|
+
for pattern in (
|
|
375
|
+
r"\b(?:authorization|cookie)\s*[:=]",
|
|
376
|
+
r"\b(?:postgresql|https?)://[^\s/@:]+:[^\s/@]+@",
|
|
377
|
+
r"\bsk-[a-z0-9_-]{12,}",
|
|
378
|
+
r"(?:^|[\s\"'])/[a-z0-9_.-]+/",
|
|
379
|
+
r"[a-z]:\\users\\",
|
|
380
|
+
r"-----BEGIN [A-Z ]+PRIVATE KEY-----",
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def redact(self, event: TelemetryEvent) -> TelemetryEvent:
|
|
385
|
+
serialized = json.dumps(
|
|
386
|
+
{
|
|
387
|
+
"payload": telemetry_payload_dict(event.payload),
|
|
388
|
+
"omitted": event.redaction.omitted_fields,
|
|
389
|
+
"truncated": event.redaction.truncated_fields,
|
|
390
|
+
},
|
|
391
|
+
ensure_ascii=False,
|
|
392
|
+
separators=(",", ":"),
|
|
393
|
+
)
|
|
394
|
+
if any(pattern.search(serialized) for pattern in self._FORBIDDEN):
|
|
395
|
+
raise ValueError("Telemetry failed the defense-in-depth redaction boundary.")
|
|
396
|
+
return event
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Non-blocking telemetry ingress isolated from the required live event lane."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from nexus.application.event_publisher import (
|
|
10
|
+
PublishedObservation,
|
|
11
|
+
current_published_observation,
|
|
12
|
+
)
|
|
13
|
+
from nexus.application.execution_context import bind_execution_context
|
|
14
|
+
from nexus.application.execution_ledger import ToolExecutionLedger
|
|
15
|
+
from nexus.application.telemetry import EventEnricher, TelemetryRedactor
|
|
16
|
+
from nexus.application.tracing_dispatcher import SafeTracerDispatcher, WarningEmitter
|
|
17
|
+
from nexus.domain.observability import (
|
|
18
|
+
RunExecutionContext,
|
|
19
|
+
TraceRunFinish,
|
|
20
|
+
TraceRunStart,
|
|
21
|
+
)
|
|
22
|
+
from nexus.domain.runtime_events import (
|
|
23
|
+
RuntimeEvent,
|
|
24
|
+
TraceFallback,
|
|
25
|
+
TraceOperation,
|
|
26
|
+
TraceSink,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class _ObservedEvent:
|
|
32
|
+
event: RuntimeEvent
|
|
33
|
+
observation: PublishedObservation
|
|
34
|
+
tool_call_count: int | None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class _FinishExecution:
|
|
39
|
+
finish: TraceRunFinish
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_IngressItem = _ObservedEvent | _FinishExecution
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(slots=True)
|
|
46
|
+
class _IngressExecution:
|
|
47
|
+
context: RunExecutionContext
|
|
48
|
+
queue: asyncio.Queue[_IngressItem]
|
|
49
|
+
worker: asyncio.Task[None]
|
|
50
|
+
finish: TraceRunFinish | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TelemetrySubscriber:
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
enricher: EventEnricher,
|
|
57
|
+
dispatcher: SafeTracerDispatcher,
|
|
58
|
+
warning_emitter: WarningEmitter | None = None,
|
|
59
|
+
ledger: ToolExecutionLedger | None = None,
|
|
60
|
+
redactor: TelemetryRedactor | None = None,
|
|
61
|
+
flush_timeout_seconds: float = 2.0,
|
|
62
|
+
finish_observer: Callable[[TraceRunFinish], None] | None = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
if flush_timeout_seconds <= 0:
|
|
65
|
+
raise ValueError("Telemetry flush timeout must be positive.")
|
|
66
|
+
self._enricher = enricher
|
|
67
|
+
self._dispatcher = dispatcher
|
|
68
|
+
self._warning_emitter = warning_emitter
|
|
69
|
+
self._ledger = ledger
|
|
70
|
+
self._redactor = redactor or TelemetryRedactor()
|
|
71
|
+
self._flush_timeout_seconds = flush_timeout_seconds
|
|
72
|
+
self._finish_observer = finish_observer
|
|
73
|
+
self._executions: dict[str, _IngressExecution] = {}
|
|
74
|
+
self._warned_executions: set[str] = set()
|
|
75
|
+
|
|
76
|
+
def start_execution(self, start: TraceRunStart) -> None:
|
|
77
|
+
execution_id = start.context.execution_id
|
|
78
|
+
if execution_id in self._executions:
|
|
79
|
+
raise RuntimeError("Telemetry ingress execution already exists.")
|
|
80
|
+
self._dispatcher.start_execution(start)
|
|
81
|
+
queue: asyncio.Queue[_IngressItem] = asyncio.Queue()
|
|
82
|
+
worker = asyncio.create_task(self._work(execution_id, queue))
|
|
83
|
+
self._executions[execution_id] = _IngressExecution(
|
|
84
|
+
context=start.context,
|
|
85
|
+
queue=queue,
|
|
86
|
+
worker=worker,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
async def on_event(self, event: RuntimeEvent) -> None:
|
|
90
|
+
observation = current_published_observation()
|
|
91
|
+
state = self._executions.get(observation.context.execution_id)
|
|
92
|
+
if state is None:
|
|
93
|
+
return
|
|
94
|
+
state.queue.put_nowait(
|
|
95
|
+
_ObservedEvent(
|
|
96
|
+
event,
|
|
97
|
+
observation,
|
|
98
|
+
None if self._ledger is None else self._ledger.count(event.run_id),
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def finish_execution(self, finish: TraceRunFinish) -> None:
|
|
103
|
+
if self._finish_observer is not None:
|
|
104
|
+
try:
|
|
105
|
+
self._finish_observer(finish)
|
|
106
|
+
except Exception:
|
|
107
|
+
# Observability must remain fail-open. Evaluation detects a missing
|
|
108
|
+
# authoritative finish as its own infrastructure error.
|
|
109
|
+
pass
|
|
110
|
+
state = self._executions.get(finish.context.execution_id)
|
|
111
|
+
if state is None:
|
|
112
|
+
return
|
|
113
|
+
state.finish = finish
|
|
114
|
+
state.queue.put_nowait(_FinishExecution(finish))
|
|
115
|
+
|
|
116
|
+
async def drain_execution(self, execution_id: str) -> None:
|
|
117
|
+
state = self._executions.get(execution_id)
|
|
118
|
+
if state is None:
|
|
119
|
+
await self._dispatcher.drain_execution(execution_id)
|
|
120
|
+
return
|
|
121
|
+
try:
|
|
122
|
+
await asyncio.wait_for(
|
|
123
|
+
asyncio.shield(state.worker), timeout=self._flush_timeout_seconds
|
|
124
|
+
)
|
|
125
|
+
except TimeoutError:
|
|
126
|
+
state.worker.cancel()
|
|
127
|
+
await asyncio.gather(state.worker, return_exceptions=True)
|
|
128
|
+
await self._warn_once(
|
|
129
|
+
execution_id,
|
|
130
|
+
PublishedObservation(state.context, 1),
|
|
131
|
+
"TRACE_FLUSH_FAILED",
|
|
132
|
+
TraceOperation.FLUSH,
|
|
133
|
+
)
|
|
134
|
+
if state.finish is not None:
|
|
135
|
+
self._dispatcher.finish_execution(state.finish)
|
|
136
|
+
finally:
|
|
137
|
+
await self._dispatcher.drain_execution(execution_id)
|
|
138
|
+
if self._executions.get(execution_id) is state:
|
|
139
|
+
self._executions.pop(execution_id, None)
|
|
140
|
+
self._warned_executions.discard(execution_id)
|
|
141
|
+
|
|
142
|
+
async def close(self) -> None:
|
|
143
|
+
for execution_id in tuple(self._executions):
|
|
144
|
+
await self.drain_execution(execution_id)
|
|
145
|
+
await self._dispatcher.close()
|
|
146
|
+
|
|
147
|
+
async def _work(
|
|
148
|
+
self, execution_id: str, queue: asyncio.Queue[_IngressItem]
|
|
149
|
+
) -> None:
|
|
150
|
+
lifecycle_completed = False
|
|
151
|
+
try:
|
|
152
|
+
while True:
|
|
153
|
+
item = await queue.get()
|
|
154
|
+
if isinstance(item, _FinishExecution):
|
|
155
|
+
self._dispatcher.finish_execution(item.finish)
|
|
156
|
+
await self._dispatcher.drain_execution(execution_id)
|
|
157
|
+
lifecycle_completed = True
|
|
158
|
+
return
|
|
159
|
+
try:
|
|
160
|
+
telemetry = self._enricher.enrich(
|
|
161
|
+
item.event,
|
|
162
|
+
item.observation,
|
|
163
|
+
tool_call_count=item.tool_call_count,
|
|
164
|
+
)
|
|
165
|
+
except Exception:
|
|
166
|
+
await self._warn_once(
|
|
167
|
+
execution_id,
|
|
168
|
+
item.observation,
|
|
169
|
+
"TELEMETRY_EVENT_INVALID",
|
|
170
|
+
TraceOperation.ENRICH,
|
|
171
|
+
)
|
|
172
|
+
continue
|
|
173
|
+
try:
|
|
174
|
+
telemetry = self._redactor.redact(telemetry)
|
|
175
|
+
except Exception:
|
|
176
|
+
await self._warn_once(
|
|
177
|
+
execution_id,
|
|
178
|
+
item.observation,
|
|
179
|
+
"TRACE_REDACTION_FAILED",
|
|
180
|
+
TraceOperation.REDACT,
|
|
181
|
+
)
|
|
182
|
+
continue
|
|
183
|
+
self._dispatcher.record(telemetry)
|
|
184
|
+
finally:
|
|
185
|
+
state = self._executions.get(execution_id)
|
|
186
|
+
if (
|
|
187
|
+
lifecycle_completed
|
|
188
|
+
and state is not None
|
|
189
|
+
and state.worker is asyncio.current_task()
|
|
190
|
+
):
|
|
191
|
+
self._executions.pop(execution_id, None)
|
|
192
|
+
self._warned_executions.discard(execution_id)
|
|
193
|
+
|
|
194
|
+
async def _warn_once(
|
|
195
|
+
self,
|
|
196
|
+
execution_id: str,
|
|
197
|
+
observation: PublishedObservation,
|
|
198
|
+
code: str,
|
|
199
|
+
operation: TraceOperation,
|
|
200
|
+
) -> None:
|
|
201
|
+
if (
|
|
202
|
+
self._warning_emitter is None
|
|
203
|
+
or execution_id in self._warned_executions
|
|
204
|
+
):
|
|
205
|
+
return
|
|
206
|
+
self._warned_executions.add(execution_id)
|
|
207
|
+
try:
|
|
208
|
+
with bind_execution_context(observation.context):
|
|
209
|
+
await self._warning_emitter(
|
|
210
|
+
code,
|
|
211
|
+
TraceSink.TELEMETRY,
|
|
212
|
+
operation,
|
|
213
|
+
TraceFallback.NONE,
|
|
214
|
+
)
|
|
215
|
+
except Exception:
|
|
216
|
+
pass
|