mcp-efficiency-engine 0.1.0 → 0.1.1

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 (183) hide show
  1. package/.github/workflows/autodocs-sync.yml +2 -15
  2. package/.github/workflows/release-npm.yml +111 -0
  3. package/FINAL_USAGE_GUIDE.md +56 -0
  4. package/README.md +133 -0
  5. package/autodocs/README.md +6 -0
  6. package/observability/metrics.md +33 -0
  7. package/observability/scoring-rubric.md +20 -0
  8. package/orchestrator/wiki/graph_consolidator.py +60 -1
  9. package/orchestrator/wiki/incremental_engine.py +23 -1
  10. package/orchestrator/wiki/plugin_manager.py +87 -22
  11. package/package.json +8 -2
  12. package/requirements.txt +1 -0
  13. package/scripts/intake/resolve-routing.py +124 -87
  14. package/scripts/intake/validate-database-routing.py +13 -4
  15. package/scripts/intake/validate-rag-routing.py +4 -3
  16. package/scripts/learning/learning-loop-report.py +28 -10
  17. package/scripts/ops/llm-token-probe.py +103 -39
  18. package/scripts/ops/telemetry-benchmark.py +184 -0
  19. package/scripts/wiki/compiler_main.py +108 -76
  20. package/scripts/wiki/providers/repo_content_provider.py +51 -0
  21. package/scripts/wiki/validator.py +40 -15
  22. package/telemetry/__init__.py +4 -0
  23. package/telemetry/bootstrap.py +47 -0
  24. package/telemetry/collector/engine.py +261 -0
  25. package/telemetry/config.json +20 -0
  26. package/telemetry/config.py +131 -0
  27. package/telemetry/context/propagation.py +85 -0
  28. package/telemetry/events/catalog.py +24 -0
  29. package/telemetry/events/factory.py +42 -0
  30. package/telemetry/exporters/base.py +16 -0
  31. package/telemetry/exporters/console/exporter.py +30 -0
  32. package/telemetry/exporters/json/exporter.py +34 -0
  33. package/telemetry/exporters/langsmith/exporter.py +415 -0
  34. package/telemetry/metrics/aggregator.py +60 -0
  35. package/telemetry/scoring/efficiency.py +68 -0
  36. package/telemetry/tracing/span.py +43 -0
  37. package/observability/evals/chat-token-usage-report.json +0 -64
  38. package/observability/evals/chat-token-usage-report.md +0 -33
  39. package/observability/evals/iteration-value-report.json +0 -76
  40. package/observability/evals/iteration-value-report.md +0 -39
  41. package/observability/evals/learning-loop-report.json +0 -268
  42. package/observability/evals/learning-loop-report.md +0 -37
  43. package/observability/evals/repo-intake-eval-cases.md +0 -23
  44. package/observability/evals/routing-eval-cases.json +0 -68
  45. package/observability/evals/routing-eval-cases.md +0 -25
  46. package/observability/evals/routing-eval-report.json +0 -72
  47. package/observability/logs/copilot-usage-ingest-report.json +0 -13
  48. package/observability/logs/iteration-metrics.jsonl +0 -75
  49. package/observability/logs/learning-feedback.jsonl +0 -152
  50. package/observability/logs/routing-decisions.jsonl +0 -333
  51. package/observability/logs/session/agent-pipeline-preflight-20260701-004146.log +0 -8
  52. package/observability/logs/session/agent-pipeline-preflight-20260701-005554.log +0 -8
  53. package/observability/logs/session/agent-pipeline-preflight-20260701-010351.log +0 -8
  54. package/observability/logs/session/agent-pipeline-preflight-20260704-145829.log +0 -8
  55. package/observability/logs/session/agent-pipeline-preflight-20260704-150021.log +0 -8
  56. package/observability/logs/session/agent-pipeline-preflight-20260704-154328.log +0 -8
  57. package/observability/logs/session/bye-20260629-221513.json +0 -39
  58. package/observability/logs/session/bye-20260629-221545.json +0 -59
  59. package/observability/logs/session/bye-20260629-222007.json +0 -52
  60. package/observability/logs/session/bye-20260629-222133.json +0 -59
  61. package/observability/logs/session/bye-20260629-222412.json +0 -45
  62. package/observability/logs/session/bye-20260629-223224.json +0 -52
  63. package/observability/logs/session/bye-20260629-224050.json +0 -59
  64. package/observability/logs/session/bye-20260630-105732.json +0 -24
  65. package/observability/logs/session/bye-20260630-211536.json +0 -87
  66. package/observability/logs/session/bye-20260630-223423.json +0 -87
  67. package/observability/logs/session/bye-20260630-232534.json +0 -87
  68. package/observability/logs/session/bye-20260701-003638.json +0 -126
  69. package/observability/logs/session/bye-20260701-003849.json +0 -126
  70. package/observability/logs/session/bye-20260701-004315.json +0 -138
  71. package/observability/logs/session/bye-20260701-004559.json +0 -76
  72. package/observability/logs/session/bye-20260701-005913.json +0 -138
  73. package/observability/logs/session/bye-20260701-010753.json +0 -138
  74. package/observability/logs/session/bye-20260703-194106.json +0 -24
  75. package/observability/logs/session/bye-20260703-233443.json +0 -76
  76. package/observability/logs/session/bye-20260703-233653.json +0 -76
  77. package/observability/logs/session/bye-20260704-150317.json +0 -152
  78. package/observability/logs/session/bye-20260704-150929.json +0 -75
  79. package/observability/logs/session/bye-20260704-151050.json +0 -75
  80. package/observability/logs/session/bye-20260705-115458.json +0 -165
  81. package/observability/logs/session/codegraph-status-20260701-004228.log +0 -39
  82. package/observability/logs/session/codegraph-status-20260701-004310.log +0 -39
  83. package/observability/logs/session/codegraph-status-20260701-005637.log +0 -39
  84. package/observability/logs/session/codegraph-status-20260701-005908.log +0 -39
  85. package/observability/logs/session/codegraph-status-20260701-010444.log +0 -39
  86. package/observability/logs/session/codegraph-status-20260701-010747.log +0 -39
  87. package/observability/logs/session/codegraph-status-20260704-150137.log +0 -26
  88. package/observability/logs/session/codegraph-status-20260704-150313.log +0 -28
  89. package/observability/logs/session/codegraph-status-20260704-154445.log +0 -26
  90. package/observability/logs/session/codegraph-status-20260705-115454.log +0 -27
  91. package/observability/logs/session/codegraph-sync-20260701-004211.log +0 -88
  92. package/observability/logs/session/codegraph-sync-20260701-004241.log +0 -82
  93. package/observability/logs/session/codegraph-sync-20260701-005620.log +0 -89
  94. package/observability/logs/session/codegraph-sync-20260701-005840.log +0 -88
  95. package/observability/logs/session/codegraph-sync-20260701-010419.log +0 -93
  96. package/observability/logs/session/codegraph-sync-20260701-010609.log +0 -90
  97. package/observability/logs/session/codegraph-sync-20260704-145856.log +0 -70
  98. package/observability/logs/session/codegraph-sync-20260704-150047.log +0 -53
  99. package/observability/logs/session/codegraph-sync-20260704-150204.log +0 -44
  100. package/observability/logs/session/codegraph-sync-20260704-154353.log +0 -26
  101. package/observability/logs/session/codegraph-sync-20260705-115337.log +0 -32
  102. package/observability/logs/session/hi-20260629-221459.json +0 -38
  103. package/observability/logs/session/hi-20260629-221945.json +0 -59
  104. package/observability/logs/session/hi-20260629-222108.json +0 -66
  105. package/observability/logs/session/hi-20260629-222352.json +0 -45
  106. package/observability/logs/session/hi-20260629-225719.json +0 -59
  107. package/observability/logs/session/hi-20260629-233722.json +0 -52
  108. package/observability/logs/session/hi-20260629-233913.json +0 -198
  109. package/observability/logs/session/hi-20260630-110156.json +0 -177
  110. package/observability/logs/session/hi-20260630-191243.json +0 -184
  111. package/observability/logs/session/hi-20260630-230050.json +0 -234
  112. package/observability/logs/session/hi-20260630-230523.json +0 -227
  113. package/observability/logs/session/hi-20260630-230855.json +0 -227
  114. package/observability/logs/session/hi-20260630-231638.json +0 -227
  115. package/observability/logs/session/hi-20260630-231756.json +0 -219
  116. package/observability/logs/session/hi-20260701-002842.json +0 -234
  117. package/observability/logs/session/hi-20260701-003458.json +0 -282
  118. package/observability/logs/session/hi-20260701-004233.json +0 -290
  119. package/observability/logs/session/hi-20260701-004554.json +0 -236
  120. package/observability/logs/session/hi-20260701-005641.json +0 -290
  121. package/observability/logs/session/hi-20260701-010450.json +0 -290
  122. package/observability/logs/session/hi-20260703-233854.json +0 -312
  123. package/observability/logs/session/hi-20260703-234448.json +0 -318
  124. package/observability/logs/session/hi-20260704-145907.json +0 -353
  125. package/observability/logs/session/hi-20260704-150141.json +0 -392
  126. package/observability/logs/session/hi-20260704-154448.json +0 -392
  127. package/observability/logs/session/iteration-value-refresh-20260701-004309.log +0 -3
  128. package/observability/logs/session/iteration-value-refresh-20260701-005907.log +0 -3
  129. package/observability/logs/session/iteration-value-refresh-20260701-010747.log +0 -3
  130. package/observability/logs/session/iteration-value-refresh-20260704-150312.log +0 -3
  131. package/observability/logs/session/iteration-value-refresh-20260705-115453.log +0 -3
  132. package/observability/logs/session/learning-loop-refresh-20260701-004309.log +0 -3
  133. package/observability/logs/session/learning-loop-refresh-20260701-005906.log +0 -3
  134. package/observability/logs/session/learning-loop-refresh-20260701-010745.log +0 -3
  135. package/observability/logs/session/learning-loop-refresh-20260704-150309.log +0 -3
  136. package/observability/logs/session/learning-loop-refresh-20260705-115450.log +0 -3
  137. package/observability/logs/session/project-notes-refresh-20260701-004227.log +0 -1
  138. package/observability/logs/session/project-notes-refresh-20260701-004308.log +0 -1
  139. package/observability/logs/session/project-notes-refresh-20260701-005636.log +0 -1
  140. package/observability/logs/session/project-notes-refresh-20260701-005906.log +0 -1
  141. package/observability/logs/session/project-notes-refresh-20260701-010443.log +0 -1
  142. package/observability/logs/session/project-notes-refresh-20260701-010744.log +0 -1
  143. package/observability/logs/session/project-notes-refresh-20260704-150133.log +0 -1
  144. package/observability/logs/session/project-notes-refresh-20260704-150309.log +0 -1
  145. package/observability/logs/session/project-notes-refresh-20260704-154442.log +0 -1
  146. package/observability/logs/session/project-notes-refresh-20260705-115449.log +0 -1
  147. package/observability/logs/session/repo-intake-20260701-004218.log +0 -6
  148. package/observability/logs/session/repo-intake-20260701-005626.log +0 -6
  149. package/observability/logs/session/repo-intake-20260701-010433.log +0 -6
  150. package/observability/logs/session/repo-intake-20260704-150053.log +0 -6
  151. package/observability/logs/session/repo-intake-20260704-154404.log +0 -6
  152. package/observability/logs/session/repo-intake-refresh-20260701-004247.log +0 -6
  153. package/observability/logs/session/repo-intake-refresh-20260701-005847.log +0 -6
  154. package/observability/logs/session/repo-intake-refresh-20260701-010615.log +0 -6
  155. package/observability/logs/session/repo-intake-refresh-20260704-150209.log +0 -6
  156. package/observability/logs/session/repo-intake-refresh-20260705-115342.log +0 -6
  157. package/observability/logs/session/repo-registry-validation-20260701-004234.log +0 -2
  158. package/observability/logs/session/repo-registry-validation-20260701-004556.log +0 -2
  159. package/observability/logs/session/repo-registry-validation-20260701-005833.log +0 -2
  160. package/observability/logs/session/repo-registry-validation-20260701-010601.log +0 -2
  161. package/observability/logs/session/repo-registry-validation-20260704-150155.log +0 -2
  162. package/observability/logs/session/repo-registry-validation-20260705-115323.log +0 -2
  163. package/observability/logs/session/repomix-refresh-20260701-003823.log +0 -4236
  164. package/observability/logs/session/repomix-refresh-20260701-004251.log +0 -4275
  165. package/observability/logs/session/repomix-refresh-20260701-005850.log +0 -4248
  166. package/observability/logs/session/repomix-refresh-20260701-010619.log +0 -6531
  167. package/observability/logs/session/repomix-refresh-20260704-150239.log +0 -652
  168. package/observability/logs/session/repomix-refresh-20260705-115419.log +0 -178
  169. package/observability/logs/session/routing-evals-20260701-004221.log +0 -2
  170. package/observability/logs/session/routing-evals-20260701-004235.log +0 -2
  171. package/observability/logs/session/routing-evals-20260701-005629.log +0 -2
  172. package/observability/logs/session/routing-evals-20260701-005834.log +0 -2
  173. package/observability/logs/session/routing-evals-20260701-010437.log +0 -2
  174. package/observability/logs/session/routing-evals-20260701-010603.log +0 -2
  175. package/observability/logs/session/routing-evals-20260704-150125.log +0 -2
  176. package/observability/logs/session/routing-evals-20260704-150157.log +0 -2
  177. package/observability/logs/session/routing-evals-20260704-154434.log +0 -2
  178. package/observability/logs/session/routing-evals-20260705-115327.log +0 -2
  179. package/observability/logs/session/sibling-discovery-refresh-20260701-004250.log +0 -3
  180. package/observability/logs/session/sibling-discovery-refresh-20260701-005850.log +0 -3
  181. package/observability/logs/session/sibling-discovery-refresh-20260701-010618.log +0 -3
  182. package/observability/logs/session/sibling-discovery-refresh-20260704-150238.log +0 -3
  183. package/observability/logs/session/sibling-discovery-refresh-20260705-115418.log +0 -3
@@ -4,10 +4,11 @@ from pathlib import Path
4
4
  from typing import Any, Dict, List
5
5
 
6
6
  from scripts.wiki.normalizer import section_catalog
7
+ from telemetry import build_telemetry_collector
7
8
 
8
9
 
9
10
  class WikiValidator:
10
- def __init__(self, schema_path: Path) -> None:
11
+ def __init__(self, schema_path: Path, telemetry_collector: Any | None = None) -> None:
11
12
  self.schema_path = schema_path
12
13
  self.allowed_sections = {section["id"] for section in section_catalog()}
13
14
  self.allowed_kinds = {
@@ -20,27 +21,51 @@ class WikiValidator:
20
21
  "report",
21
22
  "capability",
22
23
  }
24
+ self._telemetry = telemetry_collector
25
+
26
+ def _collector(self) -> Any:
27
+ if self._telemetry is None:
28
+ repo_root = Path(__file__).resolve().parents[2]
29
+ self._telemetry = build_telemetry_collector(repo_root)
30
+ return self._telemetry
23
31
 
24
32
  def validate(self, wiki_nodes: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
33
+ collector = self._collector()
25
34
  errors: List[Dict[str, Any]] = []
26
35
  warnings: List[Dict[str, Any]] = []
27
36
  info: List[Dict[str, Any]] = []
28
37
  slug_registry: Dict[str, str] = {}
29
38
 
30
- if not self.schema_path.exists():
31
- warnings.append(self._issue("schema", "schema_missing", "Schema file not found", "warning"))
32
-
33
- for node_key, node in wiki_nodes.items():
34
- self._validate_required(node_key, node, errors)
35
- self._validate_kind(node_key, node, errors)
36
- self._validate_section(node_key, node, errors)
37
- self._validate_slug(node_key, node, slug_registry, errors)
38
- self._validate_sources(node_key, node, warnings)
39
- self._validate_summary(node_key, node, warnings)
40
- self._validate_relations(node_key, node, wiki_nodes, errors)
41
-
42
- quality_score = self._quality_score(total=len(wiki_nodes), errors=len(errors), warnings=len(warnings))
43
- info.append(self._issue("summary", "quality_score", f"quality_score={quality_score}", "info"))
39
+ with collector.start_execution(operation="wiki-validator", session_id="autodocs"):
40
+ with collector.start_span(name="wiki.validate", kind="INTERNAL", attributes={"node_count": len(wiki_nodes)}):
41
+ if not self.schema_path.exists():
42
+ warnings.append(self._issue("schema", "schema_missing", "Schema file not found", "warning"))
43
+ collector.record_event("WarningGenerated", {"warning": "schema_missing", "schema_path": str(self.schema_path)}, level="WARNING")
44
+
45
+ for node_key, node in wiki_nodes.items():
46
+ self._validate_required(node_key, node, errors)
47
+ self._validate_kind(node_key, node, errors)
48
+ self._validate_section(node_key, node, errors)
49
+ self._validate_slug(node_key, node, slug_registry, errors)
50
+ self._validate_sources(node_key, node, warnings)
51
+ self._validate_summary(node_key, node, warnings)
52
+ self._validate_relations(node_key, node, wiki_nodes, errors)
53
+
54
+ quality_score = self._quality_score(total=len(wiki_nodes), errors=len(errors), warnings=len(warnings))
55
+ info.append(self._issue("summary", "quality_score", f"quality_score={quality_score}", "info"))
56
+
57
+ collector.record_metric("warning_count", float(len(warnings)), unit="count")
58
+ collector.record_metric("error_count", float(len(errors)), unit="count")
59
+ collector.record_metric("quality_score", float(quality_score), unit="score")
60
+ collector.record_event(
61
+ "MetricCalculated",
62
+ {
63
+ "node_count": len(wiki_nodes),
64
+ "error_count": len(errors),
65
+ "warning_count": len(warnings),
66
+ "quality_score": quality_score,
67
+ },
68
+ )
44
69
 
45
70
  signature = hashlib.sha256(
46
71
  json.dumps(wiki_nodes, sort_keys=True, ensure_ascii=True, separators=(",", ":")).encode("utf-8")
@@ -0,0 +1,4 @@
1
+ from .bootstrap import build_telemetry_collector
2
+ from .collector.engine import TelemetryCollector
3
+
4
+ __all__ = ["TelemetryCollector", "build_telemetry_collector"]
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from telemetry.collector.engine import TelemetryCollector
6
+ from telemetry.config import load_config
7
+ from telemetry.exporters.console.exporter import ConsoleExporter
8
+ from telemetry.exporters.json.exporter import JsonExporter
9
+ from telemetry.exporters.langsmith.exporter import LangSmithExporter
10
+
11
+
12
+ def build_telemetry_collector(repo_root: Path) -> TelemetryCollector:
13
+ cfg = load_config(repo_root)
14
+ exporters: list[object] = []
15
+
16
+ # Console exporter is always available in runtime.
17
+ if "console" in cfg.exporters:
18
+ exporters.append(ConsoleExporter())
19
+ elif not cfg.exporters:
20
+ exporters.append(ConsoleExporter())
21
+
22
+ if "json" in cfg.exporters:
23
+ exporters.append(JsonExporter((repo_root / cfg.telemetry_dir).resolve()))
24
+
25
+ if "langsmith" in cfg.exporters and cfg.langsmith.enabled:
26
+ has_langsmith_config = bool(cfg.langsmith.api_key.strip()) and bool(cfg.langsmith.project.strip())
27
+ if has_langsmith_config:
28
+ try:
29
+ exporters.append(
30
+ LangSmithExporter(
31
+ api_key=cfg.langsmith.api_key,
32
+ project=cfg.langsmith.project,
33
+ endpoint=cfg.langsmith.endpoint,
34
+ workspace_id=cfg.langsmith.workspace_id,
35
+ high_signal_only=cfg.langsmith.high_signal_only,
36
+ min_span_duration_ms=cfg.langsmith.min_span_duration_ms,
37
+ emit_execution_summary=cfg.langsmith.emit_execution_summary,
38
+ )
39
+ )
40
+ except Exception:
41
+ # LangSmith is optional and must never break startup.
42
+ pass
43
+
44
+ if not exporters:
45
+ exporters.append(ConsoleExporter())
46
+
47
+ return TelemetryCollector(exporters=exporters, enabled=cfg.enabled, batch_size=cfg.batch_size)
@@ -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
+ }