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,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)
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime, timezone
5
+ from typing import Any
6
+
7
+
8
+ def utc_now() -> str:
9
+ return datetime.now(timezone.utc).isoformat()
10
+
11
+
12
+ @dataclass
13
+ class SpanRecord:
14
+ name: str
15
+ kind: str
16
+ span_id: str
17
+ parent_span_id: str | None
18
+ trace_id: str
19
+ execution_id: str
20
+ started_at: str
21
+ ended_at: str | None = None
22
+ duration_ms: float | None = None
23
+ status: str = "UNSET"
24
+ attributes: dict[str, Any] = field(default_factory=dict)
25
+ events: list[dict[str, Any]] = field(default_factory=list)
26
+ error: str | None = None
27
+
28
+ def add_event(self, name: str, attributes: dict[str, Any] | None = None) -> None:
29
+ self.events.append(
30
+ {
31
+ "name": name,
32
+ "timestamp": utc_now(),
33
+ "attributes": attributes or {},
34
+ }
35
+ )
36
+
37
+ def end(self, *, status: str = "OK", error: str | None = None) -> None:
38
+ self.ended_at = utc_now()
39
+ self.status = status
40
+ self.error = error
41
+ start_dt = datetime.fromisoformat(self.started_at)
42
+ end_dt = datetime.fromisoformat(self.ended_at)
43
+ self.duration_ms = max(0.0, (end_dt - start_dt).total_seconds() * 1000.0)
@@ -1,64 +0,0 @@
1
- {
2
- "timestamp": "2026-07-05T09:54:53.886533+00:00",
3
- "source": "observability/logs/iteration-metrics.jsonl",
4
- "filters": {
5
- "source": "copilot-session",
6
- "from": "",
7
- "to": ""
8
- },
9
- "totals": {
10
- "events": 36,
11
- "input_tokens": 3893402,
12
- "output_tokens": 126892,
13
- "total_tokens": 4020294,
14
- "copilot_credits": 816.9227
15
- },
16
- "breakdown": {
17
- "model": {
18
- "copilot/auto": {
19
- "count": 36,
20
- "input_tokens": 3893402,
21
- "output_tokens": 126892,
22
- "total_tokens": 4020294,
23
- "copilot_credits": 816.9227
24
- }
25
- },
26
- "day": {
27
- "2026-07-03": {
28
- "count": 19,
29
- "input_tokens": 2219137,
30
- "output_tokens": 67846,
31
- "total_tokens": 2286983,
32
- "copilot_credits": 492.07088600000003
33
- },
34
- "2026-07-04": {
35
- "count": 9,
36
- "input_tokens": 876490,
37
- "output_tokens": 21560,
38
- "total_tokens": 898050,
39
- "copilot_credits": 160.161499
40
- },
41
- "2026-07-05": {
42
- "count": 8,
43
- "input_tokens": 797775,
44
- "output_tokens": 37486,
45
- "total_tokens": 835261,
46
- "copilot_credits": 164.690315
47
- }
48
- }
49
- },
50
- "license": {
51
- "plan": "business",
52
- "seats": 1,
53
- "included_credits_per_user_month": 19.0,
54
- "included_credits_total_month": 19.0,
55
- "official_plans_url": "https://github.com/features/copilot/plans?ref_cta=View+pricing+and+plans&ref_loc=hero&ref_page=%2Ffeatures_copilot_copilot_business&plans=business"
56
- },
57
- "budget": {
58
- "used_copilot_credits": 816.9227,
59
- "budget_utilization_rate": 42.995932,
60
- "overage_credits": 797.9227,
61
- "equivalent_seats_used": 42.995932,
62
- "percent_of_one_seat": 4299.59
63
- }
64
- }
@@ -1,33 +0,0 @@
1
- # Chat Token Usage Report
2
-
3
- - timestamp: 2026-07-05T09:54:53.886533+00:00
4
- - source: observability/logs/iteration-metrics.jsonl
5
- - filter.source: copilot-session
6
- - filter.from:
7
- - filter.to:
8
- - plan: business
9
- - seats: 1
10
- - official_plans_url: https://github.com/features/copilot/plans?ref_cta=View+pricing+and+plans&ref_loc=hero&ref_page=%2Ffeatures_copilot_copilot_business&plans=business
11
-
12
- ## Totals
13
-
14
- - events: 36
15
- - input_tokens: 3893402
16
- - output_tokens: 126892
17
- - total_tokens: 4020294
18
- - copilot_credits: 816.9227
19
- - included_credits_total_month: 19.0
20
- - budget_utilization_rate: 42.995932
21
- - overage_credits: 797.9227
22
- - equivalent_seats_used: 42.995932
23
- - percent_of_one_seat: 4299.59
24
-
25
- ## Breakdown by Model
26
-
27
- - copilot/auto: count=36, input=3893402, output=126892, total=4020294, credits=816.9227
28
-
29
- ## Breakdown by Day
30
-
31
- - 2026-07-03: count=19, input=2219137, output=67846, total=2286983, credits=492.070886
32
- - 2026-07-04: count=9, input=876490, output=21560, total=898050, credits=160.161499
33
- - 2026-07-05: count=8, input=797775, output=37486, total=835261, credits=164.690315