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
@@ -11,12 +11,21 @@ SECTIONS = section_catalog()
11
11
 
12
12
 
13
13
  class GraphConsolidator:
14
- def __init__(self, output_graph_path: Path, markdown_output_dir: Path) -> None:
14
+ def __init__(self, output_graph_path: Path, markdown_output_dir: Path, telemetry_collector: Any | None = None) -> None:
15
15
  self.output_graph_path = output_graph_path
16
16
  self.markdown_output_dir = markdown_output_dir
17
17
  self.generated_output_dir = output_graph_path.parent
18
+ self._telemetry = telemetry_collector
18
19
 
19
20
  def consolidate(self, provider_contracts: List[Dict[str, Any]]) -> Dict[str, Any]:
21
+ if self._telemetry is not None:
22
+ self._telemetry.record_event(
23
+ "KnowledgeGenerated",
24
+ {
25
+ "operation": "graph_consolidate_start",
26
+ "provider_contracts": len(provider_contracts),
27
+ },
28
+ )
20
29
  raw_nodes: Dict[str, Dict[str, Any]] = {}
21
30
  wiki_nodes: Dict[str, Dict[str, Any]] = {}
22
31
  for contract in provider_contracts:
@@ -54,6 +63,17 @@ class GraphConsolidator:
54
63
  relations_index = self._build_relations_index(wiki_nodes)
55
64
  section_manifest = self._build_section_manifest(wiki_nodes)
56
65
 
66
+ if self._telemetry is not None:
67
+ self._telemetry.record_metric("tool_invocations", float(len(provider_contracts)), unit="count")
68
+ self._telemetry.record_metric("cache_misses", 0.0, unit="count")
69
+ self._telemetry.record_event(
70
+ "KnowledgeGenerated",
71
+ {
72
+ "operation": "graph_consolidate_finish",
73
+ "nodes": len(wiki_nodes),
74
+ },
75
+ )
76
+
57
77
  return {
58
78
  "last_updated": self._deterministic_marker(wiki_nodes),
59
79
  "nodes": wiki_nodes,
@@ -71,11 +91,23 @@ class GraphConsolidator:
71
91
  return f"sha256:{digest}"
72
92
 
73
93
  def persist_graph(self, unified_graph: Dict[str, Any]) -> None:
94
+ if self._telemetry is not None:
95
+ self._telemetry.record_event("ToolStarted", {"tool": "persist_graph"})
74
96
  self.output_graph_path.parent.mkdir(parents=True, exist_ok=True)
75
97
  with self.output_graph_path.open("w", encoding="utf-8") as handle:
76
98
  json.dump(unified_graph, handle, indent=2, ensure_ascii=True)
99
+ if self._telemetry is not None:
100
+ self._telemetry.record_event(
101
+ "ToolFinished",
102
+ {
103
+ "tool": "persist_graph",
104
+ "path": str(self.output_graph_path),
105
+ },
106
+ )
77
107
 
78
108
  def persist_manifests(self, unified_graph: Dict[str, Any]) -> Dict[str, int]:
109
+ if self._telemetry is not None:
110
+ self._telemetry.record_event("ToolStarted", {"tool": "persist_manifests"})
79
111
  self.generated_output_dir.mkdir(parents=True, exist_ok=True)
80
112
  artifacts = {
81
113
  "search-index.json": unified_graph.get("search_index", []),
@@ -87,6 +119,14 @@ class GraphConsolidator:
87
119
  output_path = self.generated_output_dir / file_name
88
120
  with output_path.open("w", encoding="utf-8") as handle:
89
121
  json.dump(payload, handle, indent=2, ensure_ascii=True)
122
+ if self._telemetry is not None:
123
+ self._telemetry.record_event(
124
+ "ToolFinished",
125
+ {
126
+ "tool": "persist_manifests",
127
+ "manifests": len(artifacts),
128
+ },
129
+ )
90
130
  return {"manifests": len(artifacts)}
91
131
 
92
132
  def project_dirty_nodes(
@@ -96,6 +136,15 @@ class GraphConsolidator:
96
136
  all_nodes: Dict[str, Dict[str, Any]],
97
137
  cached_nodes: Dict[str, Dict[str, Any]],
98
138
  ) -> Dict[str, int]:
139
+ if self._telemetry is not None:
140
+ self._telemetry.record_event(
141
+ "ToolStarted",
142
+ {
143
+ "tool": "project_dirty_nodes",
144
+ "dirty_nodes": len(dirty_nodes),
145
+ "deleted_nodes": len(deleted_nodes),
146
+ },
147
+ )
99
148
  self.markdown_output_dir.mkdir(parents=True, exist_ok=True)
100
149
 
101
150
  rendered = 0
@@ -124,6 +173,16 @@ class GraphConsolidator:
124
173
  deleted += 1
125
174
 
126
175
  indexes = self._render_index_pages(all_nodes)
176
+ if self._telemetry is not None:
177
+ self._telemetry.record_event(
178
+ "ToolFinished",
179
+ {
180
+ "tool": "project_dirty_nodes",
181
+ "rendered": rendered,
182
+ "deleted": deleted,
183
+ "indexes": indexes,
184
+ },
185
+ )
127
186
  return {
128
187
  "rendered": rendered,
129
188
  "deleted": deleted,
@@ -4,8 +4,9 @@ from typing import Any, Dict
4
4
 
5
5
 
6
6
  class IncrementalEngine:
7
- def __init__(self, cache_path: Path) -> None:
7
+ def __init__(self, cache_path: Path, telemetry_collector: Any | None = None) -> None:
8
8
  self.cache_path = cache_path
9
+ self._telemetry = telemetry_collector
9
10
 
10
11
  def _extract_nodes(self, loaded: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
11
12
  preferred = loaded.get("wiki_nodes")
@@ -37,6 +38,14 @@ class IncrementalEngine:
37
38
  return {"last_updated": "", "nodes": {}}
38
39
 
39
40
  def diff(self, incoming_nodes: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
41
+ if self._telemetry is not None:
42
+ self._telemetry.record_event(
43
+ "ToolStarted",
44
+ {
45
+ "tool": "incremental_diff",
46
+ "incoming_nodes": len(incoming_nodes),
47
+ },
48
+ )
40
49
  cached = self._load_cached_graph()
41
50
  cached_nodes = cached.get("nodes", {})
42
51
 
@@ -50,6 +59,19 @@ class IncrementalEngine:
50
59
  dirty_nodes[node_key] = incoming
51
60
 
52
61
  deleted_nodes = [key for key in cached_nodes.keys() if key not in incoming_nodes]
62
+ if self._telemetry is not None:
63
+ cache_hits = max(0, len(incoming_nodes) - len(dirty_nodes))
64
+ cache_misses = len(dirty_nodes)
65
+ self._telemetry.record_event(
66
+ "ToolFinished",
67
+ {
68
+ "tool": "incremental_diff",
69
+ "dirty_nodes": len(dirty_nodes),
70
+ "deleted_nodes": len(deleted_nodes),
71
+ },
72
+ )
73
+ self._telemetry.record_metric("cache_hits", float(cache_hits), unit="count")
74
+ self._telemetry.record_metric("cache_misses", float(cache_misses), unit="count")
53
75
  return {
54
76
  "dirty_nodes": dirty_nodes,
55
77
  "deleted_nodes": deleted_nodes,
@@ -1,40 +1,105 @@
1
1
  from datetime import datetime, timezone
2
2
  from typing import Any, Dict, List
3
3
 
4
+ from telemetry import build_telemetry_collector
5
+
4
6
 
5
7
  class PluginManager:
6
- def __init__(self) -> None:
8
+ def __init__(self, telemetry_collector: Any | None = None) -> None:
7
9
  self._providers: List[Any] = []
10
+ self._telemetry = telemetry_collector
11
+
12
+ def _collector(self) -> Any:
13
+ if self._telemetry is None:
14
+ from pathlib import Path
15
+
16
+ repo_root = Path(__file__).resolve().parents[2]
17
+ self._telemetry = build_telemetry_collector(repo_root)
18
+ return self._telemetry
8
19
 
9
20
  def register_provider(self, provider: Any) -> None:
21
+ collector = self._collector()
22
+ with collector.start_span(name="plugin.register_provider", kind="INTERNAL"):
23
+ collector.record_event(
24
+ "ProviderRequest",
25
+ {
26
+ "provider_id": str(getattr(provider, "provider_id", "unknown")),
27
+ "operation": "register_provider",
28
+ },
29
+ )
10
30
  if not hasattr(provider, "provider_id"):
11
31
  raise ValueError("Provider must define provider_id")
12
32
  if not hasattr(provider, "gather_knowledge"):
13
33
  raise ValueError("Provider must implement gather_knowledge()")
14
34
  self._providers.append(provider)
35
+ collector.record_event(
36
+ "ProviderResponse",
37
+ {
38
+ "provider_id": str(getattr(provider, "provider_id", "unknown")),
39
+ "status": "registered",
40
+ },
41
+ )
15
42
 
16
43
  def gather_all(self) -> Dict[str, List[Dict[str, Any]]]:
44
+ collector = self._collector()
17
45
  contracts: List[Dict[str, Any]] = []
18
46
  errors: List[Dict[str, Any]] = []
19
- for provider in self._providers:
20
- try:
21
- contract = provider.gather_knowledge()
22
- if isinstance(contract, dict):
23
- contracts.append(contract)
24
- else:
25
- errors.append(
26
- {
27
- "timestamp": datetime.now(timezone.utc).isoformat(),
28
- "provider_id": str(getattr(provider, "provider_id", "unknown")),
29
- "error": "Provider returned non-dict contract",
30
- }
31
- )
32
- except Exception as exc:
33
- errors.append(
34
- {
35
- "timestamp": datetime.now(timezone.utc).isoformat(),
36
- "provider_id": str(getattr(provider, "provider_id", "unknown")),
37
- "error": str(exc),
38
- }
39
- )
47
+ with collector.start_execution(operation="plugin-manager-gather-all", session_id="wiki-orchestrator"):
48
+ for provider in self._providers:
49
+ provider_id = str(getattr(provider, "provider_id", "unknown"))
50
+ with collector.start_span(name="plugin.gather_knowledge", kind="INTERNAL", attributes={"provider_id": provider_id}):
51
+ try:
52
+ collector.record_event(
53
+ "ProviderRequest",
54
+ {
55
+ "provider_id": provider_id,
56
+ "operation": "gather_knowledge",
57
+ },
58
+ )
59
+ contract = provider.gather_knowledge()
60
+ if isinstance(contract, dict):
61
+ contracts.append(contract)
62
+ collector.record_event(
63
+ "ProviderResponse",
64
+ {
65
+ "provider_id": provider_id,
66
+ "status": "ok",
67
+ "contract_kind": str(contract.get("kind", "unknown")),
68
+ },
69
+ )
70
+ else:
71
+ errors.append(
72
+ {
73
+ "timestamp": datetime.now(timezone.utc).isoformat(),
74
+ "provider_id": provider_id,
75
+ "error": "Provider returned non-dict contract",
76
+ }
77
+ )
78
+ collector.record_event(
79
+ "WarningGenerated",
80
+ {
81
+ "provider_id": provider_id,
82
+ "warning": "Provider returned non-dict contract",
83
+ },
84
+ level="WARNING",
85
+ )
86
+ except Exception as exc:
87
+ errors.append(
88
+ {
89
+ "timestamp": datetime.now(timezone.utc).isoformat(),
90
+ "provider_id": provider_id,
91
+ "error": str(exc),
92
+ }
93
+ )
94
+ collector.record_event(
95
+ "ExceptionThrown",
96
+ {
97
+ "provider_id": provider_id,
98
+ "error": str(exc),
99
+ },
100
+ level="ERROR",
101
+ )
102
+
103
+ collector.record_metric("provider_calls", float(len(self._providers)), unit="count")
104
+ collector.record_metric("error_count", float(len(errors)), unit="count")
40
105
  return {"contracts": contracts, "errors": errors}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-efficiency-engine",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Motor de orquestacion para agentes MCP con routing por dominio y bootstrap portable.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -9,7 +9,9 @@
9
9
  "mcp-efficiency-engine": "./bin/mcpee.js"
10
10
  },
11
11
  "files": [
12
+ ".githooks/",
12
13
  "bin/",
14
+ "telemetry/",
13
15
  "autodocs/README.md",
14
16
  "autodocs/schema/",
15
17
  "autolearning/",
@@ -20,7 +22,12 @@
20
22
  "repo-registry/repos.schema.json",
21
23
  "repo-registry/repos.template.json",
22
24
  "orchestrator/",
23
- "observability/",
25
+ "observability/evaluation.md",
26
+ "observability/logs.schema.json",
27
+ "observability/logs.schema.md",
28
+ "observability/metrics.md",
29
+ "observability/routing-evaluator.md",
30
+ "observability/scoring-rubric.md",
24
31
  "optimization/",
25
32
  "policies/",
26
33
  "specs/",
@@ -44,6 +51,7 @@
44
51
  "hi": "node ./bin/mcpee.js hi",
45
52
  "bye": "node ./bin/mcpee.js bye",
46
53
  "intake": "node ./bin/mcpee.js intake",
54
+ "langsmith:kpis": "py -3 ./scripts/ops/publish-langsmith-kpis.py",
47
55
  "pack:check": "npm pack --dry-run"
48
56
  },
49
57
  "engines": {
package/requirements.txt CHANGED
@@ -1 +1,3 @@
1
1
  graphifyy[mcp]
2
+ langsmith
3
+ mcp
@@ -2,11 +2,18 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import json
5
+ import sys
5
6
  import uuid
6
7
  from datetime import datetime, timezone
7
8
  from pathlib import Path
8
9
  from typing import Any
9
10
 
11
+ _REPO_ROOT = Path(__file__).resolve().parents[2]
12
+ if str(_REPO_ROOT) not in sys.path:
13
+ sys.path.insert(0, str(_REPO_ROOT))
14
+
15
+ from telemetry import build_telemetry_collector
16
+
10
17
  try:
11
18
  import yaml # type: ignore
12
19
  except Exception:
@@ -477,6 +484,7 @@ def main() -> int:
477
484
  return 1
478
485
 
479
486
  repo_root = Path(__file__).resolve().parents[2]
487
+ collector = build_telemetry_collector(repo_root)
480
488
  registry = load_registry((repo_root / args.registry).resolve())
481
489
  capabilities = load_capability_index((repo_root / args.generated_root).resolve())
482
490
  manifests = load_manifest_index((repo_root / args.generated_root).resolve())
@@ -525,93 +533,122 @@ def main() -> int:
525
533
  fallback=fallback,
526
534
  notes=notes,
527
535
  )
528
- event = {
529
- "event_id": event_id,
530
- "timestamp": utc_now(),
531
- "input": args.input,
532
- "intent": args.intent,
533
- "domain": normalized_domain,
534
- "source_type": args.source_type,
535
- "agent": route["agent"],
536
- "engine": route["engine"],
537
- "optimization_profile": f"{token_saver_profile}+{caveman_profile}",
538
- "fallback": fallback,
539
- "grounded": grounded,
540
- "sources": sources,
541
- "notes": ";".join(notes) if notes else "",
542
- "prompt": {
543
- "selected": selected_prompt,
544
- "exists": prompt_exists,
545
- "selection_mode": "auto",
546
- },
547
- "skill": {
548
- "selected": selected_skill,
549
- "exists": skill_exists,
550
- "selection_mode": "auto",
551
- },
552
- "optimization": {
553
- "token_saver": "always_on",
554
- "token_saver_profile": token_saver_profile,
555
- "caveman": "always_on",
556
- "caveman_profile": caveman_profile,
557
- "sources_preserved": True,
558
- "context_reduction_strategy": context_strategy,
559
- },
560
- "memory": {
561
- "selected": selected_memories,
562
- "reason": memory_reason,
563
- },
564
- "requirements": runtime_requirements_for_route(args.source_type, route["engine"]),
565
- "learning": {
566
- "used_pattern": f"{normalized_domain}+{args.intent}",
567
- # Real success must be written post-execution by the feedback updater.
568
- "success": None,
569
- "outcome_status": "pending",
570
- "fallback": fallback,
571
- "confidence": 0.9 if not fallback else 0.7,
572
- },
573
- "hitl": hitl,
574
- }
575
-
576
- if has_input_tokens and has_output_tokens:
577
- total_tokens = args.input_tokens + args.output_tokens
578
- event["usage"] = {
579
- "input_tokens": args.input_tokens,
580
- "output_tokens": args.output_tokens,
581
- "total_tokens": total_tokens,
582
- "estimated_cost_usd": round(float(args.estimated_cost_usd), 6),
583
- }
584
-
585
- metrics_recorded = False
586
- metrics_output_path = ""
587
- if has_input_tokens and has_output_tokens:
588
- metric = build_iteration_metric(
589
- event_id=event_id,
590
- model=str(args.model),
591
- input_tokens=args.input_tokens,
592
- output_tokens=args.output_tokens,
593
- estimated_cost_usd=float(args.estimated_cost_usd),
594
- local_tools=args.local_tools,
595
- remote_tools=args.remote_tools,
596
- notes="auto-recorded by resolve-routing",
597
- )
598
- metrics_path = (repo_root / args.metrics_output).resolve()
599
- append_jsonl(metrics_path, metric)
600
- metrics_recorded = True
601
- metrics_output_path = args.metrics_output.replace("\\", "/")
602
-
603
- event["telemetry"] = {
604
- "iteration_metrics_recorded": metrics_recorded,
605
- "metrics_output": metrics_output_path,
606
- }
607
-
608
- output_path = (repo_root / args.output).resolve()
609
- append_jsonl(output_path, event)
610
-
611
- print(json.dumps(event, indent=2, ensure_ascii=False))
612
- print(f"Event appended to: {output_path}")
613
- if metrics_recorded:
614
- print(f"Metrics appended to: {metrics_output_path}")
536
+ with collector.start_execution(
537
+ operation="resolve-routing",
538
+ request_id=event_id,
539
+ session_id="intake-routing",
540
+ correlation_id=event_id,
541
+ execution_id=event_id,
542
+ model=str(args.model),
543
+ ):
544
+ with collector.start_span(name="routing-resolution", kind="INTERNAL", attributes={"intent": args.intent, "domain": normalized_domain}):
545
+ event = {
546
+ "event_id": event_id,
547
+ "timestamp": utc_now(),
548
+ "input": args.input,
549
+ "intent": args.intent,
550
+ "domain": normalized_domain,
551
+ "source_type": args.source_type,
552
+ "agent": route["agent"],
553
+ "engine": route["engine"],
554
+ "optimization_profile": f"{token_saver_profile}+{caveman_profile}",
555
+ "fallback": fallback,
556
+ "grounded": grounded,
557
+ "sources": sources,
558
+ "notes": ";".join(notes) if notes else "",
559
+ "prompt": {
560
+ "selected": selected_prompt,
561
+ "exists": prompt_exists,
562
+ "selection_mode": "auto",
563
+ },
564
+ "skill": {
565
+ "selected": selected_skill,
566
+ "exists": skill_exists,
567
+ "selection_mode": "auto",
568
+ },
569
+ "optimization": {
570
+ "token_saver": "always_on",
571
+ "token_saver_profile": token_saver_profile,
572
+ "caveman": "always_on",
573
+ "caveman_profile": caveman_profile,
574
+ "sources_preserved": True,
575
+ "context_reduction_strategy": context_strategy,
576
+ },
577
+ "memory": {
578
+ "selected": selected_memories,
579
+ "reason": memory_reason,
580
+ },
581
+ "requirements": runtime_requirements_for_route(args.source_type, route["engine"]),
582
+ "learning": {
583
+ "used_pattern": f"{normalized_domain}+{args.intent}",
584
+ # Real success must be written post-execution by the feedback updater.
585
+ "success": None,
586
+ "outcome_status": "pending",
587
+ "fallback": fallback,
588
+ "confidence": 0.9 if not fallback else 0.7,
589
+ },
590
+ "hitl": hitl,
591
+ }
592
+
593
+ if has_input_tokens and has_output_tokens:
594
+ total_tokens = args.input_tokens + args.output_tokens
595
+ event["usage"] = {
596
+ "input_tokens": args.input_tokens,
597
+ "output_tokens": args.output_tokens,
598
+ "total_tokens": total_tokens,
599
+ "estimated_cost_usd": round(float(args.estimated_cost_usd), 6),
600
+ }
601
+ collector.record_usage(
602
+ input_tokens=args.input_tokens,
603
+ output_tokens=args.output_tokens,
604
+ estimated_cost_usd=float(args.estimated_cost_usd),
605
+ )
606
+
607
+ metrics_recorded = False
608
+ metrics_output_path = ""
609
+ if has_input_tokens and has_output_tokens:
610
+ metric = build_iteration_metric(
611
+ event_id=event_id,
612
+ model=str(args.model),
613
+ input_tokens=args.input_tokens,
614
+ output_tokens=args.output_tokens,
615
+ estimated_cost_usd=float(args.estimated_cost_usd),
616
+ local_tools=args.local_tools,
617
+ remote_tools=args.remote_tools,
618
+ notes="auto-recorded by resolve-routing",
619
+ )
620
+ metrics_path = (repo_root / args.metrics_output).resolve()
621
+ append_jsonl(metrics_path, metric)
622
+ metrics_recorded = True
623
+ metrics_output_path = args.metrics_output.replace("\\", "/")
624
+
625
+ event["telemetry"] = {
626
+ "iteration_metrics_recorded": metrics_recorded,
627
+ "metrics_output": metrics_output_path,
628
+ }
629
+
630
+ collector.record_event(
631
+ "RoutingResolved",
632
+ {
633
+ "event_id": event_id,
634
+ "intent": args.intent,
635
+ "domain": normalized_domain,
636
+ "agent": route["agent"],
637
+ "engine": route["engine"],
638
+ "fallback": fallback,
639
+ "grounded": grounded,
640
+ },
641
+ )
642
+
643
+ output_path = (repo_root / args.output).resolve()
644
+ append_jsonl(output_path, event)
645
+
646
+ print(json.dumps(event, indent=2, ensure_ascii=False))
647
+ print(f"Event appended to: {output_path}")
648
+ if metrics_recorded:
649
+ print(f"Metrics appended to: {metrics_output_path}")
650
+
651
+ collector.shutdown()
615
652
  return 0
616
653
 
617
654
 
@@ -31,13 +31,22 @@ def run_routing(**kwargs: str) -> dict:
31
31
  encoding="utf-8",
32
32
  errors="replace"
33
33
  )
34
- # Output contains a JSON block followed by a trailing log line; extract JSON.
34
+ # Output can include telemetry lines after the main event JSON.
35
+ # Decode only the first valid JSON object from stdout.
35
36
  stdout = proc.stdout or ""
36
37
  brace = stdout.find("{")
37
- last_brace = stdout.rfind("}")
38
- if brace == -1 or last_brace == -1:
38
+ if brace == -1:
39
39
  raise ValueError(f"No JSON in output: {stdout[:200]}")
40
- return json.loads(stdout[brace : last_brace + 1])
40
+
41
+ decoder = json.JSONDecoder()
42
+ try:
43
+ payload, _ = decoder.raw_decode(stdout[brace:])
44
+ except Exception as exc:
45
+ raise ValueError(f"Invalid JSON payload in output: {stdout[:200]}") from exc
46
+
47
+ if not isinstance(payload, dict):
48
+ raise ValueError(f"Expected JSON object in output, got: {type(payload).__name__}")
49
+ return payload
41
50
 
42
51
 
43
52
  def main() -> int:
@@ -33,10 +33,11 @@ def run_routing(**kwargs: str) -> dict:
33
33
  )
34
34
  stdout = proc.stdout or ""
35
35
  brace = stdout.find("{")
36
- last_brace = stdout.rfind("}")
37
- if brace == -1 or last_brace == -1:
36
+ if brace == -1:
38
37
  raise ValueError(f"No JSON in output: {stdout[:200]}")
39
- return json.loads(stdout[brace : last_brace + 1])
38
+ decoder = json.JSONDecoder()
39
+ payload, _ = decoder.raw_decode(stdout[brace:])
40
+ return payload
40
41
 
41
42
 
42
43
  def main() -> int: