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
@@ -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
@@ -0,0 +1,415 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from datetime import datetime
5
+ from datetime import timezone
6
+ from uuid import UUID
7
+ from uuid import uuid5
8
+ from uuid import NAMESPACE_URL
9
+ from typing import Any
10
+
11
+
12
+ class LangSmithExporter:
13
+ name = "langsmith"
14
+
15
+ def __init__(
16
+ self,
17
+ *,
18
+ api_key: str,
19
+ project: str,
20
+ endpoint: str | None = None,
21
+ workspace_id: str | None = None,
22
+ high_signal_only: bool = True,
23
+ min_span_duration_ms: float = 100.0,
24
+ emit_execution_summary: bool = True,
25
+ ) -> None:
26
+ self.api_key = api_key.strip()
27
+ self.project = project.strip()
28
+ self.endpoint = (endpoint or "").strip() or None
29
+ self.workspace_id = (workspace_id or "").strip() or None
30
+ self.high_signal_only = bool(high_signal_only)
31
+ self.min_span_duration_ms = max(0.0, float(min_span_duration_ms))
32
+ self.emit_execution_summary = bool(emit_execution_summary)
33
+ self._client: Any | None = None
34
+ self._usage_by_execution: dict[str, dict[str, float]] = defaultdict(dict)
35
+ self._execution_state: dict[str, dict[str, Any]] = defaultdict(dict)
36
+ if not self.api_key or not self.project:
37
+ raise ValueError("LangSmith exporter requires api_key and project")
38
+
39
+ def _update_execution_state(self, record: dict[str, Any], timestamp: datetime) -> None:
40
+ context = record.get("context", {})
41
+ payload = record.get("payload", {})
42
+ execution_id = str(context.get("execution_id") or "")
43
+ if not execution_id:
44
+ return
45
+
46
+ state = self._execution_state[execution_id]
47
+ state.setdefault("warnings", 0)
48
+ state.setdefault("errors", 0)
49
+ state.setdefault("operation", payload.get("operation"))
50
+ state.setdefault("provider", context.get("provider"))
51
+ state.setdefault("model", context.get("model"))
52
+
53
+ event_name = str(record.get("event_name", ""))
54
+ record_type = str(record.get("type", "event"))
55
+ level = str(record.get("level", "INFO")).upper()
56
+
57
+ if event_name == "ExecutionStarted":
58
+ started_at = payload.get("started_at") or record.get("timestamp")
59
+ state["started_at"] = self._parse_timestamp(started_at)
60
+ if payload.get("operation"):
61
+ state["operation"] = payload.get("operation")
62
+ elif event_name == "RoutingResolved":
63
+ if payload.get("agent"):
64
+ state["agent"] = payload.get("agent")
65
+ if payload.get("engine"):
66
+ state["engine"] = payload.get("engine")
67
+ elif event_name in {"WarningGenerated"} or level == "WARNING":
68
+ state["warnings"] = int(state.get("warnings", 0)) + 1
69
+ elif event_name in {"ExceptionThrown"} or level == "ERROR":
70
+ state["errors"] = int(state.get("errors", 0)) + 1
71
+ if payload.get("error"):
72
+ state["last_error"] = payload.get("error")
73
+
74
+ if record_type == "span":
75
+ status = str(payload.get("status", "OK")).upper()
76
+ if status != "OK":
77
+ state["errors"] = int(state.get("errors", 0)) + 1
78
+ state["last_span_duration_ms"] = payload.get("duration_ms")
79
+
80
+ def _build_execution_summary(self, execution_id: str, context: dict[str, Any], timestamp: datetime) -> dict[str, Any]:
81
+ state = self._execution_state.get(execution_id, {})
82
+ started_at = state.get("started_at")
83
+ if isinstance(started_at, datetime):
84
+ duration_ms = max(0.0, (timestamp - started_at).total_seconds() * 1000.0)
85
+ else:
86
+ duration_ms = 0.0
87
+
88
+ warnings = int(state.get("warnings", 0))
89
+ errors = int(state.get("errors", 0))
90
+ status = "error" if errors > 0 else "ok"
91
+
92
+ usage = self._usage_by_execution.get(execution_id, {})
93
+ usage_summary = {
94
+ "input_tokens": int(usage.get("tokens_input", 0.0)),
95
+ "output_tokens": int(usage.get("tokens_output", 0.0)),
96
+ "total_tokens": int(usage.get("total_tokens", 0.0)),
97
+ "total_cost": float(usage.get("estimated_cost", 0.0)),
98
+ }
99
+
100
+ summary = {
101
+ "name": "ExecutionSummary",
102
+ "run_type": "chain",
103
+ "inputs": {
104
+ "execution_id": execution_id,
105
+ "operation": state.get("operation"),
106
+ "agent": state.get("agent"),
107
+ "engine": state.get("engine"),
108
+ },
109
+ "outputs": {
110
+ "status": status,
111
+ "duration_ms": duration_ms,
112
+ "warning_count": warnings,
113
+ "error_count": errors,
114
+ "usage_metadata": usage_summary,
115
+ "provider": context.get("provider") or state.get("provider"),
116
+ "model": context.get("model") or state.get("model"),
117
+ },
118
+ "extra": {
119
+ "metadata": {
120
+ "execution_id": execution_id,
121
+ "telemetry_kind": "execution_summary",
122
+ "last_error": state.get("last_error"),
123
+ "usage_metadata": usage_summary,
124
+ "ls_provider": context.get("provider") or state.get("provider"),
125
+ "ls_model_name": context.get("model") or state.get("model"),
126
+ }
127
+ },
128
+ "id": self._to_uuid(f"{execution_id}:execution-summary", salt="run"),
129
+ "start_time": started_at if isinstance(started_at, datetime) else timestamp,
130
+ "end_time": timestamp,
131
+ "total_tokens": usage_summary["total_tokens"],
132
+ "total_cost": usage_summary["total_cost"],
133
+ "tags": ["telemetry", "execution", "summary"],
134
+ }
135
+ return summary
136
+
137
+ def _is_high_signal_record(self, record: dict[str, Any]) -> bool:
138
+ record_type = str(record.get("type", "event"))
139
+ event_name = str(record.get("event_name", "TelemetryEvent"))
140
+ payload = record.get("payload", {})
141
+ level = str(record.get("level", "INFO")).upper()
142
+
143
+ if level in {"ERROR", "WARNING"}:
144
+ return True
145
+
146
+ if record_type == "metric":
147
+ # Emit only the per-execution summary for usage/cost to avoid noise.
148
+ return False
149
+
150
+ if record_type == "span":
151
+ duration_ms = float(payload.get("duration_ms") or 0.0)
152
+ status = str(payload.get("status", "OK")).upper()
153
+ return status != "OK" or duration_ms >= self.min_span_duration_ms
154
+
155
+ return event_name in {
156
+ "ExecutionStarted",
157
+ "ExecutionFinished",
158
+ "RoutingResolved",
159
+ "ExceptionThrown",
160
+ "WarningGenerated",
161
+ }
162
+
163
+ def _to_uuid(self, value: Any, *, salt: str = "") -> str | None:
164
+ if value is None:
165
+ return None
166
+ raw = str(value).strip()
167
+ if not raw:
168
+ return None
169
+ try:
170
+ return str(UUID(raw))
171
+ except Exception:
172
+ try:
173
+ return str(UUID(hex=raw))
174
+ except Exception:
175
+ return str(uuid5(NAMESPACE_URL, f"mcpee:{salt}:{raw}"))
176
+
177
+ def _parse_timestamp(self, value: Any) -> datetime:
178
+ if isinstance(value, datetime):
179
+ return value.astimezone(timezone.utc)
180
+ if isinstance(value, str) and value:
181
+ try:
182
+ return datetime.fromisoformat(value).astimezone(timezone.utc)
183
+ except Exception:
184
+ pass
185
+ return datetime.now(timezone.utc)
186
+
187
+ def _build_usage_summary(self, execution_id: str, context: dict[str, Any], timestamp: datetime) -> dict[str, Any]:
188
+ usage = self._usage_by_execution.get(execution_id, {})
189
+ model = context.get("model")
190
+ provider = context.get("provider")
191
+ input_tokens = int(usage.get("tokens_input", 0.0))
192
+ output_tokens = int(usage.get("tokens_output", 0.0))
193
+ total_tokens = int(usage.get("total_tokens", 0.0))
194
+ total_cost = float(usage.get("estimated_cost", 0.0))
195
+ usage_metadata = {
196
+ "input_tokens": input_tokens,
197
+ "output_tokens": output_tokens,
198
+ "total_tokens": total_tokens,
199
+ "total_cost": total_cost,
200
+ }
201
+ return {
202
+ "name": "UsageSummary",
203
+ "run_type": "llm",
204
+ "inputs": {
205
+ "execution_id": execution_id,
206
+ "provider": provider,
207
+ "model": model,
208
+ },
209
+ "outputs": {
210
+ "model": model,
211
+ "provider": provider,
212
+ "usage_metadata": usage_metadata,
213
+ "status": "recorded",
214
+ },
215
+ "extra": {
216
+ "metadata": {
217
+ "execution_id": execution_id,
218
+ "trace_id": context.get("trace_id"),
219
+ "provider": provider,
220
+ "model": model,
221
+ "ls_provider": provider,
222
+ "ls_model_name": model,
223
+ "usage_metadata": usage_metadata,
224
+ "telemetry_kind": "usage_summary",
225
+ }
226
+ },
227
+ "id": self._to_uuid(f"{execution_id}:usage-summary", salt="run"),
228
+ "start_time": timestamp,
229
+ "end_time": timestamp,
230
+ "prompt_tokens": input_tokens,
231
+ "completion_tokens": output_tokens,
232
+ "total_tokens": total_tokens,
233
+ "total_cost": total_cost,
234
+ "tags": ["telemetry", "usage", "cost"],
235
+ }
236
+
237
+ def _run_payload(self, record: dict[str, Any], *, index: int) -> dict[str, Any]:
238
+ event_name = str(record.get("event_name", "TelemetryEvent"))
239
+ context = record.get("context", {})
240
+ payload = record.get("payload", {})
241
+ record_type = str(record.get("type", "event"))
242
+ level = str(record.get("level", "INFO"))
243
+ execution_id = str(context.get("execution_id") or "")
244
+ provider = context.get("provider")
245
+ model = context.get("model")
246
+ timestamp = self._parse_timestamp(record.get("timestamp"))
247
+
248
+ inputs: dict[str, Any] = {"event": event_name, "type": record_type}
249
+ outputs: dict[str, Any] = {"status": "recorded", "level": level}
250
+ run_type = "chain"
251
+ prompt_tokens: int | None = None
252
+ completion_tokens: int | None = None
253
+ total_tokens: int | None = None
254
+ total_cost: float | None = None
255
+
256
+ if record_type == "metric":
257
+ metric_name = str(payload.get("name", "unknown"))
258
+ metric_value = payload.get("value")
259
+ metric_unit = payload.get("unit")
260
+ inputs = {
261
+ "metric": metric_name,
262
+ "value": metric_value,
263
+ "unit": metric_unit,
264
+ "execution_id": execution_id,
265
+ }
266
+ outputs = {
267
+ "metric": metric_name,
268
+ "value": metric_value,
269
+ "unit": metric_unit,
270
+ "provider": provider,
271
+ "model": model,
272
+ "status": "recorded",
273
+ }
274
+ if metric_name in {"tokens_input", "tokens_output", "total_tokens", "estimated_cost"} and execution_id:
275
+ self._usage_by_execution[execution_id][metric_name] = float(metric_value or 0.0)
276
+ run_type = "llm"
277
+ if metric_name == "tokens_input":
278
+ prompt_tokens = int(float(metric_value or 0.0))
279
+ elif metric_name == "tokens_output":
280
+ completion_tokens = int(float(metric_value or 0.0))
281
+ elif metric_name == "total_tokens":
282
+ total_tokens = int(float(metric_value or 0.0))
283
+ elif metric_name == "estimated_cost":
284
+ total_cost = float(metric_value or 0.0)
285
+ elif record_type == "span":
286
+ run_type = "tool"
287
+ inputs = {
288
+ "span": payload.get("name"),
289
+ "kind": payload.get("kind"),
290
+ "execution_id": execution_id,
291
+ }
292
+ outputs = {
293
+ "status": payload.get("status", "recorded"),
294
+ "duration_ms": payload.get("duration_ms"),
295
+ "error": payload.get("error"),
296
+ "provider": provider,
297
+ "model": model,
298
+ }
299
+ else:
300
+ inputs = {
301
+ "event": event_name,
302
+ "operation": payload.get("operation"),
303
+ "execution_id": execution_id,
304
+ }
305
+ outputs = {
306
+ "status": payload.get("status", "recorded"),
307
+ "provider": provider,
308
+ "model": model,
309
+ }
310
+
311
+ metadata = {
312
+ "execution_id": context.get("execution_id"),
313
+ "trace_id": context.get("trace_id"),
314
+ "span_id": context.get("span_id"),
315
+ "parent_span_id": context.get("parent_span_id"),
316
+ "request_id": context.get("request_id"),
317
+ "session_id": context.get("session_id"),
318
+ "correlation_id": context.get("correlation_id"),
319
+ "user": context.get("user"),
320
+ "provider": provider,
321
+ "model": model,
322
+ "ls_provider": provider,
323
+ "ls_model_name": model,
324
+ "event_name": event_name,
325
+ "type": record_type,
326
+ "level": level,
327
+ "payload": payload,
328
+ }
329
+
330
+ if prompt_tokens is not None or completion_tokens is not None or total_tokens is not None or total_cost is not None:
331
+ metadata["usage_metadata"] = {
332
+ "input_tokens": prompt_tokens or 0,
333
+ "output_tokens": completion_tokens or 0,
334
+ "total_tokens": total_tokens or 0,
335
+ "total_cost": total_cost or 0.0,
336
+ }
337
+
338
+ run_id = self._to_uuid(
339
+ f"{execution_id or 'no-exec'}:{context.get('span_id') or 'no-span'}:{event_name}:{record_type}:{index}",
340
+ salt="run",
341
+ )
342
+
343
+ run_payload: dict[str, Any] = {
344
+ "name": event_name,
345
+ "run_type": run_type,
346
+ "inputs": inputs,
347
+ "outputs": outputs,
348
+ "project_name": self.project,
349
+ "extra": {"metadata": metadata},
350
+ "id": run_id,
351
+ "start_time": timestamp,
352
+ "end_time": timestamp,
353
+ "tags": ["telemetry", record_type],
354
+ "_usage_ready": record_type == "metric" and str(payload.get("name", "")) == "estimated_cost" and bool(execution_id),
355
+ "_execution_id": execution_id,
356
+ "_context": context,
357
+ }
358
+ if prompt_tokens is not None:
359
+ run_payload["prompt_tokens"] = prompt_tokens
360
+ if completion_tokens is not None:
361
+ run_payload["completion_tokens"] = completion_tokens
362
+ if total_tokens is not None:
363
+ run_payload["total_tokens"] = total_tokens
364
+ if total_cost is not None:
365
+ run_payload["total_cost"] = total_cost
366
+ return run_payload
367
+
368
+ def _get_client(self) -> Any:
369
+ if self._client is not None:
370
+ return self._client
371
+ try:
372
+ from langsmith import Client # type: ignore
373
+ except Exception as exc:
374
+ raise RuntimeError("langsmith SDK is not installed") from exc
375
+
376
+ kwargs: dict[str, Any] = {"api_key": self.api_key}
377
+ if self.endpoint:
378
+ kwargs["api_url"] = self.endpoint
379
+ if self.workspace_id:
380
+ kwargs["workspace_id"] = self.workspace_id
381
+ self._client = Client(**kwargs)
382
+ return self._client
383
+
384
+ def export(self, records: list[dict]) -> None:
385
+ client = self._get_client()
386
+ for index, record in enumerate(records):
387
+ run = self._run_payload(record, index=index)
388
+ usage_ready = bool(run.pop("_usage_ready", False))
389
+ execution_id = str(run.pop("_execution_id", "") or "")
390
+ context = run.pop("_context", {})
391
+ timestamp = run["end_time"]
392
+
393
+ self._update_execution_state(record, timestamp)
394
+
395
+ should_emit = True
396
+ if self.high_signal_only:
397
+ should_emit = self._is_high_signal_record(record)
398
+
399
+ if should_emit:
400
+ client.create_run(**run)
401
+
402
+ if usage_ready and execution_id:
403
+ summary = self._build_usage_summary(execution_id, context, run["end_time"])
404
+ client.create_run(project_name=self.project, **summary)
405
+
406
+ event_name = str(record.get("event_name", ""))
407
+ if self.emit_execution_summary and execution_id and event_name == "ExecutionFinished":
408
+ execution_summary = self._build_execution_summary(execution_id, context, timestamp)
409
+ client.create_run(project_name=self.project, **execution_summary)
410
+
411
+ def flush(self) -> None:
412
+ return
413
+
414
+ def shutdown(self) -> None:
415
+ return
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+
6
+ @dataclass
7
+ class MetricsSnapshot:
8
+ execution_time_ms: float = 0.0
9
+ tool_duration_ms: float = 0.0
10
+ average_duration_ms: float = 0.0
11
+ parallel_tasks: int = 0
12
+ sequential_tasks: int = 0
13
+ retry_count: int = 0
14
+ error_count: int = 0
15
+ warning_count: int = 0
16
+ llm_calls: int = 0
17
+ provider_calls: int = 0
18
+ mcp_calls: int = 0
19
+ tool_invocations: int = 0
20
+ tokens_input: int = 0
21
+ tokens_output: int = 0
22
+ total_tokens: int = 0
23
+ estimated_cost: float = 0.0
24
+ cache_hits: int = 0
25
+ cache_misses: int = 0
26
+ compression_ratio: float = 0.0
27
+ parallel_efficiency: float = 0.0
28
+ prompt_reduction: float = 0.0
29
+ context_reduction: float = 0.0
30
+
31
+
32
+ @dataclass
33
+ class MetricsAggregator:
34
+ durations: list[float] = field(default_factory=list)
35
+ snapshot: MetricsSnapshot = field(default_factory=MetricsSnapshot)
36
+
37
+ def add_duration(self, value_ms: float) -> None:
38
+ safe = max(0.0, float(value_ms))
39
+ self.durations.append(safe)
40
+ self.snapshot.tool_duration_ms += safe
41
+ if self.durations:
42
+ self.snapshot.average_duration_ms = self.snapshot.tool_duration_ms / len(self.durations)
43
+
44
+ def add_tokens(self, input_tokens: int, output_tokens: int) -> None:
45
+ self.snapshot.tokens_input += max(0, int(input_tokens))
46
+ self.snapshot.tokens_output += max(0, int(output_tokens))
47
+ self.snapshot.total_tokens = self.snapshot.tokens_input + self.snapshot.tokens_output
48
+
49
+ def add_cost(self, estimated_cost: float) -> None:
50
+ self.snapshot.estimated_cost += max(0.0, float(estimated_cost))
51
+
52
+ def bump(self, metric_name: str, value: int = 1) -> None:
53
+ if not hasattr(self.snapshot, metric_name):
54
+ return
55
+ current = getattr(self.snapshot, metric_name)
56
+ if isinstance(current, int):
57
+ setattr(self.snapshot, metric_name, max(0, current + int(value)))
58
+
59
+ def to_dict(self) -> dict[str, float | int]:
60
+ return self.snapshot.__dict__.copy()
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ DEFAULT_WEIGHTS = {
7
+ "time": 0.18,
8
+ "cost": 0.16,
9
+ "tokens": 0.14,
10
+ "cache": 0.12,
11
+ "parallelism": 0.10,
12
+ "reuse": 0.10,
13
+ "calls": 0.10,
14
+ "compression": 0.10,
15
+ }
16
+
17
+
18
+ def _clamp(value: float, lo: float = 0.0, hi: float = 1.0) -> float:
19
+ return max(lo, min(hi, value))
20
+
21
+
22
+ def _safe_ratio(numerator: float, denominator: float) -> float:
23
+ if denominator <= 0:
24
+ return 0.0
25
+ return numerator / denominator
26
+
27
+
28
+ def compute_efficiency_score(metrics: dict[str, Any], weights: dict[str, float] | None = None) -> float:
29
+ w = dict(DEFAULT_WEIGHTS)
30
+ if weights:
31
+ w.update(weights)
32
+
33
+ duration = float(metrics.get("execution_time_ms", 0.0))
34
+ cost = float(metrics.get("estimated_cost", 0.0))
35
+ total_tokens = float(metrics.get("total_tokens", 0.0))
36
+ cache_hits = float(metrics.get("cache_hits", 0.0))
37
+ cache_misses = float(metrics.get("cache_misses", 0.0))
38
+ parallel_eff = float(metrics.get("parallel_efficiency", 0.0))
39
+ prompt_reduction = float(metrics.get("prompt_reduction", 0.0))
40
+ context_reduction = float(metrics.get("context_reduction", 0.0))
41
+ calls = float(
42
+ metrics.get("tool_invocations", 0)
43
+ + metrics.get("provider_calls", 0)
44
+ + metrics.get("llm_calls", 0)
45
+ + metrics.get("mcp_calls", 0)
46
+ )
47
+ compression = float(metrics.get("compression_ratio", 0.0))
48
+
49
+ time_score = 1.0 - _clamp(duration / 120000.0)
50
+ cost_score = 1.0 - _clamp(cost / 2.0)
51
+ token_score = 1.0 - _clamp(total_tokens / 200000.0)
52
+ cache_score = _clamp(_safe_ratio(cache_hits, cache_hits + cache_misses))
53
+ parallel_score = _clamp(parallel_eff)
54
+ reuse_score = _clamp((prompt_reduction + context_reduction) / 2.0)
55
+ call_score = 1.0 - _clamp(calls / 200.0)
56
+ compression_score = _clamp(compression)
57
+
58
+ weighted = (
59
+ time_score * w["time"]
60
+ + cost_score * w["cost"]
61
+ + token_score * w["tokens"]
62
+ + cache_score * w["cache"]
63
+ + parallel_score * w["parallelism"]
64
+ + reuse_score * w["reuse"]
65
+ + call_score * w["calls"]
66
+ + compression_score * w["compression"]
67
+ )
68
+ return round(_clamp(weighted) * 100.0, 2)