mcp-efficiency-engine 0.1.0 → 0.1.2

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 (191) hide show
  1. package/.githooks/post-commit +20 -0
  2. package/.github/workflows/autodocs-sync.yml +2 -15
  3. package/.github/workflows/release-npm.yml +111 -0
  4. package/FINAL_USAGE_GUIDE.md +78 -0
  5. package/README.md +231 -0
  6. package/autodocs/README.md +6 -0
  7. package/bin/install-host.js +26 -1
  8. package/observability/metrics.md +33 -0
  9. package/observability/scoring-rubric.md +20 -0
  10. package/orchestrator/wiki/graph_consolidator.py +60 -1
  11. package/orchestrator/wiki/incremental_engine.py +23 -1
  12. package/orchestrator/wiki/plugin_manager.py +87 -22
  13. package/package.json +10 -2
  14. package/requirements.txt +2 -0
  15. package/scripts/intake/resolve-routing.py +124 -87
  16. package/scripts/intake/validate-database-routing.py +13 -4
  17. package/scripts/intake/validate-rag-routing.py +4 -3
  18. package/scripts/learning/learning-loop-report.py +28 -10
  19. package/scripts/ops/bye.ps1 +30 -0
  20. package/scripts/ops/hi.ps1 +13 -0
  21. package/scripts/ops/llm-token-probe.py +103 -39
  22. package/scripts/ops/post-commit-refresh.ps1 +110 -0
  23. package/scripts/ops/publish-langsmith-kpis.py +302 -0
  24. package/scripts/ops/telemetry-benchmark.py +184 -0
  25. package/scripts/setup/install-project-hooks.ps1 +33 -0
  26. package/scripts/setup/setup-prerequisites.ps1 +42 -15
  27. package/scripts/wiki/compiler_main.py +108 -76
  28. package/scripts/wiki/providers/repo_content_provider.py +51 -0
  29. package/scripts/wiki/validator.py +40 -15
  30. package/telemetry/__init__.py +4 -0
  31. package/telemetry/bootstrap.py +47 -0
  32. package/telemetry/collector/engine.py +261 -0
  33. package/telemetry/config.json +20 -0
  34. package/telemetry/config.py +131 -0
  35. package/telemetry/context/propagation.py +85 -0
  36. package/telemetry/events/catalog.py +24 -0
  37. package/telemetry/events/factory.py +42 -0
  38. package/telemetry/exporters/base.py +16 -0
  39. package/telemetry/exporters/console/exporter.py +30 -0
  40. package/telemetry/exporters/json/exporter.py +34 -0
  41. package/telemetry/exporters/langsmith/exporter.py +415 -0
  42. package/telemetry/metrics/aggregator.py +60 -0
  43. package/telemetry/scoring/efficiency.py +68 -0
  44. package/telemetry/tracing/span.py +43 -0
  45. package/observability/evals/chat-token-usage-report.json +0 -64
  46. package/observability/evals/chat-token-usage-report.md +0 -33
  47. package/observability/evals/iteration-value-report.json +0 -76
  48. package/observability/evals/iteration-value-report.md +0 -39
  49. package/observability/evals/learning-loop-report.json +0 -268
  50. package/observability/evals/learning-loop-report.md +0 -37
  51. package/observability/evals/repo-intake-eval-cases.md +0 -23
  52. package/observability/evals/routing-eval-cases.json +0 -68
  53. package/observability/evals/routing-eval-cases.md +0 -25
  54. package/observability/evals/routing-eval-report.json +0 -72
  55. package/observability/logs/copilot-usage-ingest-report.json +0 -13
  56. package/observability/logs/iteration-metrics.jsonl +0 -75
  57. package/observability/logs/learning-feedback.jsonl +0 -152
  58. package/observability/logs/routing-decisions.jsonl +0 -333
  59. package/observability/logs/session/agent-pipeline-preflight-20260701-004146.log +0 -8
  60. package/observability/logs/session/agent-pipeline-preflight-20260701-005554.log +0 -8
  61. package/observability/logs/session/agent-pipeline-preflight-20260701-010351.log +0 -8
  62. package/observability/logs/session/agent-pipeline-preflight-20260704-145829.log +0 -8
  63. package/observability/logs/session/agent-pipeline-preflight-20260704-150021.log +0 -8
  64. package/observability/logs/session/agent-pipeline-preflight-20260704-154328.log +0 -8
  65. package/observability/logs/session/bye-20260629-221513.json +0 -39
  66. package/observability/logs/session/bye-20260629-221545.json +0 -59
  67. package/observability/logs/session/bye-20260629-222007.json +0 -52
  68. package/observability/logs/session/bye-20260629-222133.json +0 -59
  69. package/observability/logs/session/bye-20260629-222412.json +0 -45
  70. package/observability/logs/session/bye-20260629-223224.json +0 -52
  71. package/observability/logs/session/bye-20260629-224050.json +0 -59
  72. package/observability/logs/session/bye-20260630-105732.json +0 -24
  73. package/observability/logs/session/bye-20260630-211536.json +0 -87
  74. package/observability/logs/session/bye-20260630-223423.json +0 -87
  75. package/observability/logs/session/bye-20260630-232534.json +0 -87
  76. package/observability/logs/session/bye-20260701-003638.json +0 -126
  77. package/observability/logs/session/bye-20260701-003849.json +0 -126
  78. package/observability/logs/session/bye-20260701-004315.json +0 -138
  79. package/observability/logs/session/bye-20260701-004559.json +0 -76
  80. package/observability/logs/session/bye-20260701-005913.json +0 -138
  81. package/observability/logs/session/bye-20260701-010753.json +0 -138
  82. package/observability/logs/session/bye-20260703-194106.json +0 -24
  83. package/observability/logs/session/bye-20260703-233443.json +0 -76
  84. package/observability/logs/session/bye-20260703-233653.json +0 -76
  85. package/observability/logs/session/bye-20260704-150317.json +0 -152
  86. package/observability/logs/session/bye-20260704-150929.json +0 -75
  87. package/observability/logs/session/bye-20260704-151050.json +0 -75
  88. package/observability/logs/session/bye-20260705-115458.json +0 -165
  89. package/observability/logs/session/codegraph-status-20260701-004228.log +0 -39
  90. package/observability/logs/session/codegraph-status-20260701-004310.log +0 -39
  91. package/observability/logs/session/codegraph-status-20260701-005637.log +0 -39
  92. package/observability/logs/session/codegraph-status-20260701-005908.log +0 -39
  93. package/observability/logs/session/codegraph-status-20260701-010444.log +0 -39
  94. package/observability/logs/session/codegraph-status-20260701-010747.log +0 -39
  95. package/observability/logs/session/codegraph-status-20260704-150137.log +0 -26
  96. package/observability/logs/session/codegraph-status-20260704-150313.log +0 -28
  97. package/observability/logs/session/codegraph-status-20260704-154445.log +0 -26
  98. package/observability/logs/session/codegraph-status-20260705-115454.log +0 -27
  99. package/observability/logs/session/codegraph-sync-20260701-004211.log +0 -88
  100. package/observability/logs/session/codegraph-sync-20260701-004241.log +0 -82
  101. package/observability/logs/session/codegraph-sync-20260701-005620.log +0 -89
  102. package/observability/logs/session/codegraph-sync-20260701-005840.log +0 -88
  103. package/observability/logs/session/codegraph-sync-20260701-010419.log +0 -93
  104. package/observability/logs/session/codegraph-sync-20260701-010609.log +0 -90
  105. package/observability/logs/session/codegraph-sync-20260704-145856.log +0 -70
  106. package/observability/logs/session/codegraph-sync-20260704-150047.log +0 -53
  107. package/observability/logs/session/codegraph-sync-20260704-150204.log +0 -44
  108. package/observability/logs/session/codegraph-sync-20260704-154353.log +0 -26
  109. package/observability/logs/session/codegraph-sync-20260705-115337.log +0 -32
  110. package/observability/logs/session/hi-20260629-221459.json +0 -38
  111. package/observability/logs/session/hi-20260629-221945.json +0 -59
  112. package/observability/logs/session/hi-20260629-222108.json +0 -66
  113. package/observability/logs/session/hi-20260629-222352.json +0 -45
  114. package/observability/logs/session/hi-20260629-225719.json +0 -59
  115. package/observability/logs/session/hi-20260629-233722.json +0 -52
  116. package/observability/logs/session/hi-20260629-233913.json +0 -198
  117. package/observability/logs/session/hi-20260630-110156.json +0 -177
  118. package/observability/logs/session/hi-20260630-191243.json +0 -184
  119. package/observability/logs/session/hi-20260630-230050.json +0 -234
  120. package/observability/logs/session/hi-20260630-230523.json +0 -227
  121. package/observability/logs/session/hi-20260630-230855.json +0 -227
  122. package/observability/logs/session/hi-20260630-231638.json +0 -227
  123. package/observability/logs/session/hi-20260630-231756.json +0 -219
  124. package/observability/logs/session/hi-20260701-002842.json +0 -234
  125. package/observability/logs/session/hi-20260701-003458.json +0 -282
  126. package/observability/logs/session/hi-20260701-004233.json +0 -290
  127. package/observability/logs/session/hi-20260701-004554.json +0 -236
  128. package/observability/logs/session/hi-20260701-005641.json +0 -290
  129. package/observability/logs/session/hi-20260701-010450.json +0 -290
  130. package/observability/logs/session/hi-20260703-233854.json +0 -312
  131. package/observability/logs/session/hi-20260703-234448.json +0 -318
  132. package/observability/logs/session/hi-20260704-145907.json +0 -353
  133. package/observability/logs/session/hi-20260704-150141.json +0 -392
  134. package/observability/logs/session/hi-20260704-154448.json +0 -392
  135. package/observability/logs/session/iteration-value-refresh-20260701-004309.log +0 -3
  136. package/observability/logs/session/iteration-value-refresh-20260701-005907.log +0 -3
  137. package/observability/logs/session/iteration-value-refresh-20260701-010747.log +0 -3
  138. package/observability/logs/session/iteration-value-refresh-20260704-150312.log +0 -3
  139. package/observability/logs/session/iteration-value-refresh-20260705-115453.log +0 -3
  140. package/observability/logs/session/learning-loop-refresh-20260701-004309.log +0 -3
  141. package/observability/logs/session/learning-loop-refresh-20260701-005906.log +0 -3
  142. package/observability/logs/session/learning-loop-refresh-20260701-010745.log +0 -3
  143. package/observability/logs/session/learning-loop-refresh-20260704-150309.log +0 -3
  144. package/observability/logs/session/learning-loop-refresh-20260705-115450.log +0 -3
  145. package/observability/logs/session/project-notes-refresh-20260701-004227.log +0 -1
  146. package/observability/logs/session/project-notes-refresh-20260701-004308.log +0 -1
  147. package/observability/logs/session/project-notes-refresh-20260701-005636.log +0 -1
  148. package/observability/logs/session/project-notes-refresh-20260701-005906.log +0 -1
  149. package/observability/logs/session/project-notes-refresh-20260701-010443.log +0 -1
  150. package/observability/logs/session/project-notes-refresh-20260701-010744.log +0 -1
  151. package/observability/logs/session/project-notes-refresh-20260704-150133.log +0 -1
  152. package/observability/logs/session/project-notes-refresh-20260704-150309.log +0 -1
  153. package/observability/logs/session/project-notes-refresh-20260704-154442.log +0 -1
  154. package/observability/logs/session/project-notes-refresh-20260705-115449.log +0 -1
  155. package/observability/logs/session/repo-intake-20260701-004218.log +0 -6
  156. package/observability/logs/session/repo-intake-20260701-005626.log +0 -6
  157. package/observability/logs/session/repo-intake-20260701-010433.log +0 -6
  158. package/observability/logs/session/repo-intake-20260704-150053.log +0 -6
  159. package/observability/logs/session/repo-intake-20260704-154404.log +0 -6
  160. package/observability/logs/session/repo-intake-refresh-20260701-004247.log +0 -6
  161. package/observability/logs/session/repo-intake-refresh-20260701-005847.log +0 -6
  162. package/observability/logs/session/repo-intake-refresh-20260701-010615.log +0 -6
  163. package/observability/logs/session/repo-intake-refresh-20260704-150209.log +0 -6
  164. package/observability/logs/session/repo-intake-refresh-20260705-115342.log +0 -6
  165. package/observability/logs/session/repo-registry-validation-20260701-004234.log +0 -2
  166. package/observability/logs/session/repo-registry-validation-20260701-004556.log +0 -2
  167. package/observability/logs/session/repo-registry-validation-20260701-005833.log +0 -2
  168. package/observability/logs/session/repo-registry-validation-20260701-010601.log +0 -2
  169. package/observability/logs/session/repo-registry-validation-20260704-150155.log +0 -2
  170. package/observability/logs/session/repo-registry-validation-20260705-115323.log +0 -2
  171. package/observability/logs/session/repomix-refresh-20260701-003823.log +0 -4236
  172. package/observability/logs/session/repomix-refresh-20260701-004251.log +0 -4275
  173. package/observability/logs/session/repomix-refresh-20260701-005850.log +0 -4248
  174. package/observability/logs/session/repomix-refresh-20260701-010619.log +0 -6531
  175. package/observability/logs/session/repomix-refresh-20260704-150239.log +0 -652
  176. package/observability/logs/session/repomix-refresh-20260705-115419.log +0 -178
  177. package/observability/logs/session/routing-evals-20260701-004221.log +0 -2
  178. package/observability/logs/session/routing-evals-20260701-004235.log +0 -2
  179. package/observability/logs/session/routing-evals-20260701-005629.log +0 -2
  180. package/observability/logs/session/routing-evals-20260701-005834.log +0 -2
  181. package/observability/logs/session/routing-evals-20260701-010437.log +0 -2
  182. package/observability/logs/session/routing-evals-20260701-010603.log +0 -2
  183. package/observability/logs/session/routing-evals-20260704-150125.log +0 -2
  184. package/observability/logs/session/routing-evals-20260704-150157.log +0 -2
  185. package/observability/logs/session/routing-evals-20260704-154434.log +0 -2
  186. package/observability/logs/session/routing-evals-20260705-115327.log +0 -2
  187. package/observability/logs/session/sibling-discovery-refresh-20260701-004250.log +0 -3
  188. package/observability/logs/session/sibling-discovery-refresh-20260701-005850.log +0 -3
  189. package/observability/logs/session/sibling-discovery-refresh-20260701-010618.log +0 -3
  190. package/observability/logs/session/sibling-discovery-refresh-20260704-150238.log +0 -3
  191. package/observability/logs/session/sibling-discovery-refresh-20260705-115418.log +0 -3
@@ -0,0 +1,261 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from contextlib import AbstractContextManager
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timezone
7
+ from typing import Any
8
+
9
+ from telemetry.context.propagation import (
10
+ TelemetryContext,
11
+ child_context,
12
+ get_current_context,
13
+ new_execution_context,
14
+ reset_current_context,
15
+ set_current_context,
16
+ )
17
+ from telemetry.events.catalog import CANONICAL_EVENTS
18
+ from telemetry.events.factory import make_event
19
+ from telemetry.metrics.aggregator import MetricsAggregator
20
+ from telemetry.scoring.efficiency import compute_efficiency_score
21
+ from telemetry.tracing.span import SpanRecord
22
+
23
+
24
+ def utc_now() -> str:
25
+ return datetime.now(timezone.utc).isoformat()
26
+
27
+
28
+ @dataclass
29
+ class _ExecutionScope(AbstractContextManager[TelemetryContext]):
30
+ collector: "TelemetryCollector"
31
+ context: TelemetryContext
32
+ token: Any
33
+ operation: str
34
+ start_ts: str
35
+
36
+ def __enter__(self) -> TelemetryContext:
37
+ self.collector.record_event("ExecutionStarted", {"operation": self.operation, "started_at": self.start_ts})
38
+ return self.context
39
+
40
+ def __exit__(self, exc_type, exc, tb) -> bool:
41
+ if exc is not None:
42
+ self.collector.record_event("ExceptionThrown", {"operation": self.operation, "error": str(exc)}, level="ERROR")
43
+ self.collector.metrics.bump("error_count")
44
+ self.collector.metrics.snapshot.execution_time_ms = max(
45
+ 0.0,
46
+ (datetime.fromisoformat(utc_now()) - datetime.fromisoformat(self.start_ts)).total_seconds() * 1000.0,
47
+ )
48
+ self.collector.record_metric("efficiency_score", compute_efficiency_score(self.collector.metrics.to_dict()), unit="score")
49
+ self.collector.record_event("ExecutionFinished", {"operation": self.operation, "finished_at": utc_now()})
50
+ self.collector.flush()
51
+ reset_current_context(self.token)
52
+ return False
53
+
54
+
55
+ @dataclass
56
+ class _SpanScope(AbstractContextManager[SpanRecord]):
57
+ collector: "TelemetryCollector"
58
+ span: SpanRecord
59
+ token: Any
60
+
61
+ def __enter__(self) -> SpanRecord:
62
+ self.collector.record_event(
63
+ "SpanStarted",
64
+ {
65
+ "name": self.span.name,
66
+ "span_id": self.span.span_id,
67
+ "parent_span_id": self.span.parent_span_id,
68
+ "kind": self.span.kind,
69
+ },
70
+ )
71
+ return self.span
72
+
73
+ def __exit__(self, exc_type, exc, tb) -> bool:
74
+ if exc is not None:
75
+ self.span.end(status="ERROR", error=str(exc))
76
+ self.collector.metrics.bump("error_count")
77
+ self.collector.record_event("ExceptionThrown", {"span": self.span.name, "error": str(exc)}, level="ERROR")
78
+ else:
79
+ self.span.end(status="OK")
80
+
81
+ if self.span.duration_ms is not None:
82
+ self.collector.metrics.add_duration(self.span.duration_ms)
83
+
84
+ self.collector._emit(
85
+ make_event(
86
+ event_name="SpanFinished",
87
+ event_type="span",
88
+ context=get_current_context(),
89
+ payload={
90
+ "name": self.span.name,
91
+ "kind": self.span.kind,
92
+ "span_id": self.span.span_id,
93
+ "parent_span_id": self.span.parent_span_id,
94
+ "trace_id": self.span.trace_id,
95
+ "execution_id": self.span.execution_id,
96
+ "started_at": self.span.started_at,
97
+ "ended_at": self.span.ended_at,
98
+ "duration_ms": self.span.duration_ms,
99
+ "status": self.span.status,
100
+ "attributes": self.span.attributes,
101
+ "events": self.span.events,
102
+ "error": self.span.error,
103
+ },
104
+ )
105
+ )
106
+ reset_current_context(self.token)
107
+ return False
108
+
109
+
110
+ class TelemetryCollector:
111
+ def __init__(
112
+ self,
113
+ *,
114
+ exporters: list[Any],
115
+ enabled: bool = True,
116
+ batch_size: int = 25,
117
+ ) -> None:
118
+ self.enabled = bool(enabled)
119
+ self.exporters = exporters
120
+ self.batch_size = max(1, int(batch_size))
121
+ self.queue: list[dict[str, Any]] = []
122
+ self.metrics = MetricsAggregator()
123
+ self.warning_buffer: list[str] = []
124
+
125
+ def start_execution(
126
+ self,
127
+ *,
128
+ operation: str,
129
+ request_id: str | None = None,
130
+ session_id: str = "default-session",
131
+ correlation_id: str | None = None,
132
+ execution_id: str | None = None,
133
+ user: str | None = None,
134
+ provider: str | None = None,
135
+ model: str | None = None,
136
+ ) -> AbstractContextManager[TelemetryContext]:
137
+ request = request_id or str(uuid.uuid4())
138
+ correlation = correlation_id or request
139
+ execution = execution_id or str(uuid.uuid4())
140
+ context = new_execution_context(
141
+ request_id=request,
142
+ session_id=session_id,
143
+ correlation_id=correlation,
144
+ execution_id=execution,
145
+ user=user,
146
+ provider=provider,
147
+ model=model,
148
+ )
149
+ token = set_current_context(context)
150
+ return _ExecutionScope(self, context, token, operation, utc_now())
151
+
152
+ def start_span(
153
+ self,
154
+ *,
155
+ name: str,
156
+ kind: str = "INTERNAL",
157
+ attributes: dict[str, Any] | None = None,
158
+ ) -> AbstractContextManager[SpanRecord]:
159
+ current = get_current_context()
160
+ if current is None:
161
+ current = new_execution_context(
162
+ request_id=str(uuid.uuid4()),
163
+ session_id="implicit-session",
164
+ correlation_id=str(uuid.uuid4()),
165
+ execution_id=str(uuid.uuid4()),
166
+ )
167
+ token = set_current_context(current)
168
+ else:
169
+ child = child_context(current)
170
+ token = set_current_context(child)
171
+ current = child
172
+
173
+ span = SpanRecord(
174
+ name=name,
175
+ kind=kind,
176
+ span_id=current.span_id,
177
+ parent_span_id=current.parent_span_id,
178
+ trace_id=current.trace_id,
179
+ execution_id=current.execution_id,
180
+ started_at=utc_now(),
181
+ attributes=attributes or {},
182
+ )
183
+ return _SpanScope(self, span, token)
184
+
185
+ def record_event(self, event_name: str, payload: dict[str, Any] | None = None, *, level: str = "INFO") -> None:
186
+ canonical = event_name if event_name in CANONICAL_EVENTS else "WarningGenerated"
187
+ if canonical == "WarningGenerated" and event_name not in CANONICAL_EVENTS:
188
+ payload = {"original_event": event_name, **(payload or {})}
189
+ if level.upper() == "WARNING":
190
+ self.metrics.bump("warning_count")
191
+ self._emit(make_event(event_name=canonical, event_type="event", context=get_current_context(), payload=payload, level=level.upper()))
192
+
193
+ def record_metric(self, metric_name: str, value: float, *, unit: str = "count", tags: dict[str, Any] | None = None) -> None:
194
+ payload = {"name": metric_name, "value": value, "unit": unit, "tags": tags or {}}
195
+ if metric_name == "estimated_cost":
196
+ self.metrics.add_cost(float(value))
197
+ self._emit(make_event(event_name="MetricCalculated", event_type="metric", context=get_current_context(), payload=payload))
198
+
199
+ def record_usage(self, *, input_tokens: int, output_tokens: int, estimated_cost_usd: float) -> None:
200
+ self.metrics.add_tokens(input_tokens, output_tokens)
201
+ self.metrics.add_cost(estimated_cost_usd)
202
+ self.record_metric("tokens_input", float(input_tokens), unit="tokens")
203
+ self.record_metric("tokens_output", float(output_tokens), unit="tokens")
204
+ self.record_metric("total_tokens", float(input_tokens + output_tokens), unit="tokens")
205
+ self.record_metric("estimated_cost", float(estimated_cost_usd), unit="usd")
206
+
207
+ def _emit(self, record: dict[str, Any]) -> None:
208
+ if not self.enabled:
209
+ return
210
+ self.queue.append(record)
211
+ if len(self.queue) >= self.batch_size:
212
+ self.flush()
213
+
214
+ def flush(self) -> None:
215
+ if not self.enabled or not self.queue:
216
+ return
217
+
218
+ batch = self.queue[:]
219
+ self.queue = []
220
+ for exporter in self.exporters:
221
+ try:
222
+ exporter_started = make_event(
223
+ event_name="ExporterStarted",
224
+ event_type="event",
225
+ context=get_current_context(),
226
+ payload={"exporter": exporter.name},
227
+ )
228
+ batch_with_started = [exporter_started, *batch]
229
+ exporter.export(batch_with_started)
230
+ exporter.flush()
231
+ exporter_finished = make_event(
232
+ event_name="ExporterFinished",
233
+ event_type="event",
234
+ context=get_current_context(),
235
+ payload={"exporter": exporter.name, "success": True},
236
+ )
237
+ exporter.export([exporter_finished])
238
+ except Exception as exc:
239
+ self.warning_buffer.append(f"exporter_failed:{exporter.name}:{exc}")
240
+ warning = make_event(
241
+ event_name="WarningGenerated",
242
+ event_type="event",
243
+ context=get_current_context(),
244
+ payload={"warning": "exporter_failed", "exporter": exporter.name, "error": str(exc)},
245
+ level="WARNING",
246
+ )
247
+ for backup in self.exporters:
248
+ if backup.name == exporter.name:
249
+ continue
250
+ try:
251
+ backup.export([warning])
252
+ except Exception:
253
+ continue
254
+
255
+ def shutdown(self) -> None:
256
+ self.flush()
257
+ for exporter in self.exporters:
258
+ try:
259
+ exporter.shutdown()
260
+ except Exception:
261
+ continue
@@ -0,0 +1,20 @@
1
+ {
2
+ "telemetry": {
3
+ "enabled": true,
4
+ "batch_size": 100,
5
+ "telemetry_dir": ".telemetry",
6
+ "exporters": [
7
+ "console",
8
+ "json"
9
+ ]
10
+ },
11
+ "langsmith": {
12
+ "enabled": false,
13
+ "api_key": "",
14
+ "project": "",
15
+ "endpoint": "",
16
+ "high_signal_only": true,
17
+ "min_span_duration_ms": 100,
18
+ "emit_execution_summary": true
19
+ }
20
+ }
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ def _parse_bool(value: str | bool | None, default: bool) -> bool:
11
+ if isinstance(value, bool):
12
+ return value
13
+ if value is None:
14
+ return default
15
+ return str(value).strip().lower() in {"1", "true", "yes", "on"}
16
+
17
+
18
+ def _parse_float(value: str | float | int | None, default: float) -> float:
19
+ if isinstance(value, (int, float)):
20
+ return float(value)
21
+ if value is None:
22
+ return default
23
+ try:
24
+ return float(str(value).strip())
25
+ except Exception:
26
+ return default
27
+
28
+
29
+ @dataclass
30
+ class LangSmithConfig:
31
+ enabled: bool = False
32
+ api_key: str = ""
33
+ project: str = ""
34
+ endpoint: str = ""
35
+ workspace_id: str = ""
36
+ high_signal_only: bool = True
37
+ min_span_duration_ms: float = 100.0
38
+ emit_execution_summary: bool = True
39
+
40
+
41
+ @dataclass
42
+ class TelemetryConfig:
43
+ enabled: bool = True
44
+ exporters: list[str] = field(default_factory=lambda: ["console", "json"])
45
+ batch_size: int = 25
46
+ telemetry_dir: str = ".telemetry"
47
+ langsmith: LangSmithConfig = field(default_factory=LangSmithConfig)
48
+
49
+
50
+ def _load_json_file(path: Path) -> dict[str, Any]:
51
+ if not path.exists():
52
+ return {}
53
+ try:
54
+ payload = json.loads(path.read_text(encoding="utf-8"))
55
+ except Exception:
56
+ return {}
57
+ return payload if isinstance(payload, dict) else {}
58
+
59
+
60
+ def _load_dotenv_file(path: Path) -> dict[str, str]:
61
+ if not path.exists():
62
+ return {}
63
+
64
+ values: dict[str, str] = {}
65
+ for raw_line in path.read_text(encoding="utf-8").splitlines():
66
+ line = raw_line.strip()
67
+ if not line or line.startswith("#") or "=" not in line:
68
+ continue
69
+
70
+ key, value = line.split("=", 1)
71
+ env_key = key.strip()
72
+ if not env_key:
73
+ continue
74
+
75
+ env_value = value.strip()
76
+ if len(env_value) >= 2 and env_value[0] == env_value[-1] and env_value[0] in {'"', "'"}:
77
+ env_value = env_value[1:-1]
78
+ values[env_key] = env_value
79
+
80
+ return values
81
+
82
+
83
+ def load_config(repo_root: Path) -> TelemetryConfig:
84
+ raw = _load_json_file(repo_root / "telemetry" / "config.json")
85
+ dotenv = _load_dotenv_file(repo_root / ".env")
86
+ telemetry_raw = raw.get("telemetry", {}) if isinstance(raw.get("telemetry"), dict) else {}
87
+ langsmith_raw = raw.get("langsmith", {}) if isinstance(raw.get("langsmith"), dict) else {}
88
+
89
+ def _get_env(name: str, default: str | None = None) -> str | None:
90
+ if name in os.environ:
91
+ value = os.environ.get(name)
92
+ return None if value is None else str(value)
93
+ return dotenv.get(name, default)
94
+
95
+ exporters = telemetry_raw.get("exporters", ["console", "json"])
96
+ if not isinstance(exporters, list):
97
+ exporters = ["console", "json"]
98
+
99
+ env_exporters = (_get_env("TELEMETRY_EXPORTERS", "") or "").strip()
100
+ if env_exporters:
101
+ exporters = [item.strip().lower() for item in env_exporters.split(",") if item.strip()]
102
+
103
+ cfg = TelemetryConfig(
104
+ enabled=_parse_bool(_get_env("TELEMETRY_ENABLED"), _parse_bool(telemetry_raw.get("enabled"), True)),
105
+ exporters=[str(item).lower() for item in exporters],
106
+ batch_size=max(1, int((_get_env("TELEMETRY_BATCH_SIZE", str(telemetry_raw.get("batch_size", 25))) or str(telemetry_raw.get("batch_size", 25))))),
107
+ telemetry_dir=str((_get_env("TELEMETRY_DIR", str(telemetry_raw.get("telemetry_dir", ".telemetry"))) or str(telemetry_raw.get("telemetry_dir", ".telemetry")))),
108
+ langsmith=LangSmithConfig(
109
+ enabled=_parse_bool(
110
+ _get_env("LANGSMITH_ENABLED", _get_env("LANGSMITH_TRACING")),
111
+ _parse_bool(langsmith_raw.get("enabled"), False),
112
+ ),
113
+ api_key=str((_get_env("LANGSMITH_API_KEY", str(langsmith_raw.get("api_key", ""))) or str(langsmith_raw.get("api_key", "")))),
114
+ project=str((_get_env("LANGSMITH_PROJECT", str(langsmith_raw.get("project", ""))) or str(langsmith_raw.get("project", "")))),
115
+ endpoint=str((_get_env("LANGSMITH_ENDPOINT", str(langsmith_raw.get("endpoint", ""))) or str(langsmith_raw.get("endpoint", "")))),
116
+ workspace_id=str((_get_env("LANGSMITH_WORKSPACE_ID", str(langsmith_raw.get("workspace_id", ""))) or str(langsmith_raw.get("workspace_id", "")))),
117
+ high_signal_only=_parse_bool(
118
+ _get_env("LANGSMITH_HIGH_SIGNAL_ONLY"),
119
+ _parse_bool(langsmith_raw.get("high_signal_only"), True),
120
+ ),
121
+ min_span_duration_ms=_parse_float(
122
+ _get_env("LANGSMITH_MIN_SPAN_DURATION_MS"),
123
+ _parse_float(langsmith_raw.get("min_span_duration_ms"), 100.0),
124
+ ),
125
+ emit_execution_summary=_parse_bool(
126
+ _get_env("LANGSMITH_EMIT_EXECUTION_SUMMARY"),
127
+ _parse_bool(langsmith_raw.get("emit_execution_summary"), True),
128
+ ),
129
+ ),
130
+ )
131
+ return cfg
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ import contextvars
4
+ import secrets
5
+ from dataclasses import dataclass, replace
6
+ from datetime import datetime, timezone
7
+
8
+
9
+ def utc_now() -> str:
10
+ return datetime.now(timezone.utc).isoformat()
11
+
12
+
13
+ def new_trace_id() -> str:
14
+ # 16 bytes -> 32 lowercase hex chars, aligned with OTel expectations.
15
+ return secrets.token_hex(16)
16
+
17
+
18
+ def new_span_id() -> str:
19
+ # 8 bytes -> 16 lowercase hex chars, aligned with OTel expectations.
20
+ return secrets.token_hex(8)
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class TelemetryContext:
25
+ execution_id: str
26
+ trace_id: str
27
+ span_id: str
28
+ parent_span_id: str | None
29
+ request_id: str
30
+ session_id: str
31
+ correlation_id: str
32
+ timestamp: str
33
+ user: str | None = None
34
+ provider: str | None = None
35
+ model: str | None = None
36
+
37
+
38
+ _context_var: contextvars.ContextVar[TelemetryContext | None] = contextvars.ContextVar("telemetry_context", default=None)
39
+
40
+
41
+ def get_current_context() -> TelemetryContext | None:
42
+ return _context_var.get()
43
+
44
+
45
+ def set_current_context(context: TelemetryContext | None) -> contextvars.Token[TelemetryContext | None]:
46
+ return _context_var.set(context)
47
+
48
+
49
+ def reset_current_context(token: contextvars.Token[TelemetryContext | None]) -> None:
50
+ _context_var.reset(token)
51
+
52
+
53
+ def new_execution_context(
54
+ *,
55
+ request_id: str,
56
+ session_id: str,
57
+ correlation_id: str,
58
+ execution_id: str,
59
+ user: str | None = None,
60
+ provider: str | None = None,
61
+ model: str | None = None,
62
+ ) -> TelemetryContext:
63
+ root_span_id = new_span_id()
64
+ return TelemetryContext(
65
+ execution_id=execution_id,
66
+ trace_id=new_trace_id(),
67
+ span_id=root_span_id,
68
+ parent_span_id=None,
69
+ request_id=request_id,
70
+ session_id=session_id,
71
+ correlation_id=correlation_id,
72
+ timestamp=utc_now(),
73
+ user=user,
74
+ provider=provider,
75
+ model=model,
76
+ )
77
+
78
+
79
+ def child_context(parent: TelemetryContext) -> TelemetryContext:
80
+ return replace(
81
+ parent,
82
+ span_id=new_span_id(),
83
+ parent_span_id=parent.span_id,
84
+ timestamp=utc_now(),
85
+ )
@@ -0,0 +1,24 @@
1
+ CANONICAL_EVENTS = {
2
+ "ExecutionStarted",
3
+ "ExecutionFinished",
4
+ "ToolStarted",
5
+ "ToolFinished",
6
+ "PromptGenerated",
7
+ "PromptOptimized",
8
+ "ContextOptimized",
9
+ "CacheHit",
10
+ "CacheMiss",
11
+ "LLMRequest",
12
+ "LLMResponse",
13
+ "ProviderRequest",
14
+ "ProviderResponse",
15
+ "ExporterStarted",
16
+ "ExporterFinished",
17
+ "ExceptionThrown",
18
+ "WarningGenerated",
19
+ "MetricCalculated",
20
+ "KnowledgeGenerated",
21
+ "SpanStarted",
22
+ "SpanFinished",
23
+ "RoutingResolved",
24
+ }
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Any
5
+
6
+ from telemetry.context.propagation import TelemetryContext
7
+
8
+
9
+ def utc_now() -> str:
10
+ return datetime.now(timezone.utc).isoformat()
11
+
12
+
13
+ def make_event(
14
+ *,
15
+ event_name: str,
16
+ event_type: str,
17
+ context: TelemetryContext | None,
18
+ payload: dict[str, Any] | None = None,
19
+ level: str = "INFO",
20
+ ) -> dict[str, Any]:
21
+ data: dict[str, Any] = {
22
+ "event_schema_version": "1.0.0",
23
+ "timestamp": utc_now(),
24
+ "type": event_type,
25
+ "event_name": event_name,
26
+ "level": level,
27
+ "payload": payload or {},
28
+ }
29
+ if context is not None:
30
+ data["context"] = {
31
+ "execution_id": context.execution_id,
32
+ "trace_id": context.trace_id,
33
+ "span_id": context.span_id,
34
+ "parent_span_id": context.parent_span_id,
35
+ "request_id": context.request_id,
36
+ "session_id": context.session_id,
37
+ "correlation_id": context.correlation_id,
38
+ "user": context.user,
39
+ "provider": context.provider,
40
+ "model": context.model,
41
+ }
42
+ return data
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+
6
+ class ITelemetryExporter(Protocol):
7
+ name: str
8
+
9
+ def export(self, records: list[dict]) -> None:
10
+ ...
11
+
12
+ def flush(self) -> None:
13
+ ...
14
+
15
+ def shutdown(self) -> None:
16
+ ...
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+
6
+ class ConsoleExporter:
7
+ name = "console"
8
+
9
+ def export(self, records: list[dict]) -> None:
10
+ for record in records:
11
+ event_name = str(record.get("event_name", "unknown"))
12
+ event_type = str(record.get("type", "event"))
13
+ level = str(record.get("level", "INFO"))
14
+ context = record.get("context", {})
15
+ execution_id = context.get("execution_id", "n/a")
16
+ print(f"[telemetry][{level}][{event_type}] {event_name} execution={execution_id}")
17
+ if event_type == "span":
18
+ payload = record.get("payload", {})
19
+ summary = {
20
+ "name": payload.get("name"),
21
+ "duration_ms": payload.get("duration_ms"),
22
+ "status": payload.get("status"),
23
+ }
24
+ print(f"[telemetry][span] {json.dumps(summary, ensure_ascii=False)}")
25
+
26
+ def flush(self) -> None:
27
+ return
28
+
29
+ def shutdown(self) -> None:
30
+ return
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+
7
+ class JsonExporter:
8
+ name = "json"
9
+
10
+ def __init__(self, telemetry_dir: Path) -> None:
11
+ self.telemetry_dir = telemetry_dir
12
+ self.telemetry_dir.mkdir(parents=True, exist_ok=True)
13
+ self.traces_file = self.telemetry_dir / "traces.jsonl"
14
+ self.metrics_file = self.telemetry_dir / "metrics.jsonl"
15
+
16
+ def _append_lines(self, file_path: Path, entries: list[dict]) -> None:
17
+ if not entries:
18
+ return
19
+ with file_path.open("a", encoding="utf-8") as handle:
20
+ for entry in entries:
21
+ handle.write(json.dumps(entry, ensure_ascii=False, separators=(",", ":")))
22
+ handle.write("\n")
23
+
24
+ def export(self, records: list[dict]) -> None:
25
+ traces = [r for r in records if str(r.get("type", "")) in {"event", "span"}]
26
+ metrics = [r for r in records if str(r.get("type", "")) == "metric"]
27
+ self._append_lines(self.traces_file, traces)
28
+ self._append_lines(self.metrics_file, metrics)
29
+
30
+ def flush(self) -> None:
31
+ return
32
+
33
+ def shutdown(self) -> None:
34
+ return