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
@@ -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.1",
4
4
  "description": "Motor de orquestacion para agentes MCP con routing por dominio y bootstrap portable.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "files": [
12
12
  "bin/",
13
+ "telemetry/",
13
14
  "autodocs/README.md",
14
15
  "autodocs/schema/",
15
16
  "autolearning/",
@@ -20,7 +21,12 @@
20
21
  "repo-registry/repos.schema.json",
21
22
  "repo-registry/repos.template.json",
22
23
  "orchestrator/",
23
- "observability/",
24
+ "observability/evaluation.md",
25
+ "observability/logs.schema.json",
26
+ "observability/logs.schema.md",
27
+ "observability/metrics.md",
28
+ "observability/routing-evaluator.md",
29
+ "observability/scoring-rubric.md",
24
30
  "optimization/",
25
31
  "policies/",
26
32
  "specs/",
package/requirements.txt CHANGED
@@ -1 +1,2 @@
1
1
  graphifyy[mcp]
2
+ langsmith
@@ -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:
@@ -2,11 +2,18 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import json
5
+ import sys
5
6
  from collections import defaultdict
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
 
11
18
  def utc_now() -> str:
12
19
  return datetime.now(timezone.utc).isoformat()
@@ -247,24 +254,35 @@ def main() -> int:
247
254
  args = parser.parse_args()
248
255
 
249
256
  repo_root = Path(__file__).resolve().parents[2]
257
+ collector = build_telemetry_collector(repo_root)
250
258
  in_path = (repo_root / args.input).resolve()
251
259
  feedback_path = (repo_root / args.feedback).resolve()
252
260
  out_json = (repo_root / args.out_json).resolve()
253
261
  out_md = (repo_root / args.out_md).resolve()
254
262
 
255
- events = parse_events(in_path)
256
- feedback = parse_feedback(feedback_path)
257
- report = build_report(events, str(in_path), feedback, str(feedback_path))
263
+ with collector.start_execution(operation="learning-loop-report", session_id="learning"):
264
+ with collector.start_span(name="learning.report.build", kind="INTERNAL"):
265
+ events = parse_events(in_path)
266
+ feedback = parse_feedback(feedback_path)
267
+ report = build_report(events, str(in_path), feedback, str(feedback_path))
268
+
269
+ out_json.parent.mkdir(parents=True, exist_ok=True)
270
+ out_md.parent.mkdir(parents=True, exist_ok=True)
271
+
272
+ out_json.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
273
+ out_md.write_text(report_to_markdown(report), encoding="utf-8")
258
274
 
259
- out_json.parent.mkdir(parents=True, exist_ok=True)
260
- out_md.parent.mkdir(parents=True, exist_ok=True)
275
+ kpis = report.get("kpis", {}) if isinstance(report.get("kpis", {}), dict) else {}
276
+ collector.record_metric("tool_invocations", float(report.get("total_events", 0)), unit="count")
277
+ collector.record_metric("fallback_rate", float(kpis.get("fallback_rate", 0.0)), unit="ratio")
278
+ collector.record_metric("grounded_rate", float(kpis.get("grounded_rate", 0.0)), unit="ratio")
279
+ collector.record_metric("confidence_avg", float(kpis.get("confidence_avg", 0.0)), unit="score")
261
280
 
262
- out_json.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
263
- out_md.write_text(report_to_markdown(report), encoding="utf-8")
281
+ print(f"Learning events processed: {len(events)}")
282
+ print(f"Report JSON: {out_json}")
283
+ print(f"Report MD: {out_md}")
264
284
 
265
- print(f"Learning events processed: {len(events)}")
266
- print(f"Report JSON: {out_json}")
267
- print(f"Report MD: {out_md}")
285
+ collector.shutdown()
268
286
  return 0
269
287
 
270
288