cct-cli 0.7.9.0__py3-none-any.whl

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 (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,333 @@
1
+ """
2
+ CAT v0.7.9.0 — metrics.py: real per-request latency instrumentation.
3
+
4
+ Requirement: "Add internal timing information ... Do not guess where the
5
+ delay comes from." This module is the single place every pipeline stage
6
+ reports into. It records REAL wall-clock measurements — never fabricated
7
+ numbers — for:
8
+
9
+ request_received_ms memory_retrieval_ms routing_ms
10
+ model_first_token_ms tool_execution_ms agent_loop_ms
11
+ final_response_ms vision_ms
12
+
13
+ plus per-request counters (model_calls, tool_calls) and a rolling
14
+ session history so bottlenecks can be found from actual data.
15
+
16
+ Design constraints:
17
+
18
+ * Thread-safe — the UI runs agent work on worker threads while the
19
+ Textual main thread reads the session history (e.g. the /perf view).
20
+ * Zero-overhead when disabled? No: measurements are cheap (time.perf_counter
21
+ pairs), always on. The cost is nanoseconds per stage.
22
+ * Never raises from the recording side — metrics are observability, and a
23
+ broken recorder must not be able to break the request path.
24
+
25
+ Usage:
26
+ from . import metrics
27
+
28
+ m = metrics.begin_request()
29
+ m.mark_stage("memory_retrieval", start=..., end=...) # explicit pair
30
+ with m.stage("routing"): # context form
31
+ ...
32
+ m.count_model_call(); m.count_tool_call()
33
+ m.finish() # stamps total
34
+ print(m.summary_lines()) # real numbers only
35
+
36
+ A threading.local "current" handle lets deep code (agent.py, aicore.py)
37
+ record without plumbing an object through every signature; callers that
38
+ have the object should pass it directly instead.
39
+ """
40
+
41
+ if __name__ == "__main__":
42
+ print("This is a library file and is not meant to be run directly.")
43
+ import sys
44
+ sys.exit(1)
45
+
46
+ import threading
47
+ import time
48
+ from collections import deque
49
+ from typing import Dict, List, Optional
50
+
51
+ # Stage keys -> canonical millisecond field names. Anything recorded under
52
+ # an unknown key still lands in `stages_ms`; these are just the documented
53
+ # vocabulary shared by the whole pipeline.
54
+ STAGE_KEYS = (
55
+ "request_received", "memory_retrieval", "routing",
56
+ "model_first_token", "tool_execution", "agent_loop",
57
+ "vision", "final_response", "history_summarize",
58
+ )
59
+
60
+ HISTORY_LIMIT = 50 # rolling window of finished requests
61
+
62
+
63
+ def _now_ms() -> float:
64
+ return time.perf_counter() * 1000.0
65
+
66
+
67
+ class RequestMetrics:
68
+ """One user request's real timing record."""
69
+
70
+ __slots__ = ("request_id", "_t0", "_stage_pairs", "_open_stages",
71
+ "model_calls", "tool_calls", "streamed_chunks",
72
+ "first_token_ms", "total_ms", "route_path", "task_types",
73
+ "model_used", "finished", "_lock")
74
+
75
+ def __init__(self, request_id: Optional[str] = None):
76
+ self.request_id = request_id or f"req-{int(time.time() * 1000)}-{id(self) % 100000}"
77
+ self._t0 = time.perf_counter()
78
+ self._stage_pairs: Dict[str, List[float]] = {}
79
+ self._open_stages: Dict[str, float] = {}
80
+ self.model_calls = 0
81
+ self.tool_calls = 0
82
+ self.streamed_chunks = 0
83
+ self.first_token_ms: Optional[float] = None
84
+ self.total_ms: Optional[float] = None
85
+ self.route_path = ""
86
+ self.task_types: List[str] = []
87
+ self.model_used = ""
88
+ self.finished = False
89
+ self._lock = threading.Lock()
90
+
91
+ # ------------------------------------------------------------- stages --
92
+ def stage_start(self, key: str) -> None:
93
+ """Mark the beginning of a stage (for non-context-manager call
94
+ sites). A stage may open/close multiple times; durations add up."""
95
+ try:
96
+ with self._lock:
97
+ self._open_stages[key] = time.perf_counter()
98
+ except Exception:
99
+ pass
100
+
101
+ def stage_end(self, key: str) -> None:
102
+ try:
103
+ with self._lock:
104
+ t0 = self._open_stages.pop(key, None)
105
+ if t0 is None:
106
+ return
107
+ self._stage_pairs.setdefault(key, []).append(
108
+ (time.perf_counter() - t0) * 1000.0)
109
+ if key == "model_first_token" and self.first_token_ms is None:
110
+ self.first_token_ms = self._stage_pairs[key][-1]
111
+ except Exception:
112
+ pass
113
+
114
+ def mark_stage(self, key: str, start: float, end: Optional[float] = None) -> None:
115
+ """Record one completed stage duration from explicit timestamps
116
+ (time.perf_counter values)."""
117
+ try:
118
+ end = time.perf_counter() if end is None else end
119
+ with self._lock:
120
+ self._stage_pairs.setdefault(key, []).append(max(0.0, (end - start) * 1000.0))
121
+ if key == "model_first_token" and self.first_token_ms is None:
122
+ self.first_token_ms = self._stage_pairs[key][-1]
123
+ except Exception:
124
+ pass
125
+
126
+ class _StageCtx:
127
+ __slots__ = ("m", "key")
128
+
129
+ def __init__(self, m: "RequestMetrics", key: str):
130
+ self.m = m
131
+ self.key = key
132
+
133
+ def __enter__(self):
134
+ self.m.stage_start(self.key)
135
+ return self.m
136
+
137
+ def __exit__(self, exc_type, exc, tb):
138
+ self.m.stage_end(self.key)
139
+ return False
140
+
141
+ def stage(self, key: str) -> "RequestMetrics._StageCtx":
142
+ """Context manager form: `with m.stage("routing"): ...`."""
143
+ return RequestMetrics._StageCtx(self, key)
144
+
145
+ def stages_ms(self) -> Dict[str, float]:
146
+ """Total recorded milliseconds per stage key (summed)."""
147
+ out: Dict[str, float] = {}
148
+ try:
149
+ with self._lock:
150
+ for k, vals in self._stage_pairs.items():
151
+ out[k] = sum(vals)
152
+ except Exception:
153
+ pass
154
+ return out
155
+
156
+ # ----------------------------------------------------------- counters --
157
+ def count_model_call(self, n: int = 1) -> None:
158
+ try:
159
+ with self._lock:
160
+ self.model_calls += n
161
+ except Exception:
162
+ pass
163
+
164
+ def count_tool_call(self, n: int = 1) -> None:
165
+ try:
166
+ with self._lock:
167
+ self.tool_calls += n
168
+ except Exception:
169
+ pass
170
+
171
+ def count_chunk(self, n: int = 1) -> None:
172
+ try:
173
+ with self._lock:
174
+ self.streamed_chunks += n
175
+ except Exception:
176
+ pass
177
+
178
+ def note_first_token(self) -> None:
179
+ """Called by the streaming layer on the very first real token.
180
+ Records time-to-first-token relative to request start."""
181
+ try:
182
+ with self._lock:
183
+ if self.first_token_ms is None:
184
+ self.first_token_ms = (time.perf_counter() - self._t0) * 1000.0
185
+ except Exception:
186
+ pass
187
+
188
+ # ------------------------------------------------------------- finish --
189
+ def finish(self, *args, **kwargs) -> float:
190
+ """Stamps total latency and files this request into the session
191
+ history. Safely accepts optional arguments (*args, **kwargs) without error.
192
+ Returns total ms."""
193
+ try:
194
+ with self._lock:
195
+ if self.total_ms is None:
196
+ self.total_ms = (time.perf_counter() - self._t0) * 1000.0
197
+ self.finished = True
198
+ except Exception:
199
+ pass
200
+ try:
201
+ _record_finished(self)
202
+ except Exception:
203
+ pass
204
+ return self.total_ms or 0.0
205
+
206
+ # ------------------------------------------------------------ reporting --
207
+ def as_dict(self) -> Dict[str, object]:
208
+ stages = self.stages_ms()
209
+
210
+ def _ms(k: str) -> float:
211
+ return round(stages.get(k, 0.0), 1)
212
+
213
+ return {
214
+ "request_id": self.request_id,
215
+ "path": self.route_path,
216
+ "task_types": list(self.task_types),
217
+ "model": self.model_used,
218
+ "request_received_ms": round(self.total_ms or 0.0, 1),
219
+ "memory_retrieval_ms": _ms("memory_retrieval"),
220
+ "routing_ms": _ms("routing"),
221
+ "history_summarize_ms": _ms("history_summarize"),
222
+ "model_first_token_ms": round(self.first_token_ms or 0.0, 1),
223
+ "tool_execution_ms": _ms("tool_execution"),
224
+ "agent_loop_ms": _ms("agent_loop"),
225
+ "vision_ms": _ms("vision"),
226
+ "final_response_ms": round((self.total_ms or 0.0)
227
+ - sum(v for k, v in stages.items()
228
+ if k != "final_response"), 1)
229
+ if self.total_ms is not None else None,
230
+ "model_calls": self.model_calls,
231
+ "tool_calls": self.tool_calls,
232
+ "streamed_chunks": self.streamed_chunks,
233
+ }
234
+
235
+ def summary_lines(self) -> List[str]:
236
+ """Human-readable REAL measurement lines for the completion card.
237
+ Only measured values appear — nothing is invented."""
238
+ d = self.as_dict()
239
+ bits = []
240
+ if d["model_first_token_ms"]:
241
+ bits.append(f"TTFB {d['model_first_token_ms']:.0f}ms")
242
+ for label, key in (("route", None), ("memory", "memory_retrieval_ms"),
243
+ ("tools", "tool_execution_ms"), ("loop", "agent_loop_ms"),
244
+ ("vision", "vision_ms")):
245
+ if key and d.get(key):
246
+ bits.append(f"{label} {d[key]:.0f}ms")
247
+ calls = f"{d['model_calls']} model call(s)"
248
+ if d["tool_calls"]:
249
+ calls += f", {d['tool_calls']} tool call(s)"
250
+ if d["path"]:
251
+ calls = f"[{d['path']}] " + calls
252
+ lines = ["[dim]\u23f1 Real timings: " + (" \u00b7 ".join(bits) if bits else "n/a") + "[/dim]",
253
+ "[dim]\U0001f4ca " + calls + "[/dim]"]
254
+ return lines
255
+
256
+
257
+ # ------------------------------------------------------------- session log --
258
+
259
+ _history_lock = threading.Lock()
260
+ _history: deque = deque(maxlen=HISTORY_LIMIT)
261
+
262
+
263
+ def _record_finished(m: RequestMetrics) -> None:
264
+ with _history_lock:
265
+ _history.append(m)
266
+
267
+
268
+ def get_recent(limit: int = 10) -> List[RequestMetrics]:
269
+ with _history_lock:
270
+ return list(_history)[-limit:]
271
+
272
+
273
+ def session_totals() -> Dict[str, float]:
274
+ """Aggregate REAL session-wide measurements (mean/median/p95 over the
275
+ rolling window). Used by /perf and scripts/benchmark_cat.py."""
276
+ with _history_lock:
277
+ items = list(_history)
278
+
279
+ def _stats(vals: List[float]) -> Dict[str, float]:
280
+ if not vals:
281
+ return {"mean": 0.0, "p95": 0.0}
282
+ s = sorted(vals)
283
+ mean = sum(s) / len(s)
284
+ p95 = s[min(len(s) - 1, int(round(0.95 * len(s))) - 1)] if s else 0.0
285
+ return {"mean": mean, "p95": p95}
286
+
287
+ out: Dict[str, float] = {
288
+ "requests": len(items),
289
+ "model_calls": sum(i.model_calls for i in items),
290
+ "tool_calls": sum(i.tool_calls for i in items),
291
+ "total_mean_ms": _stats([i.total_ms or 0.0 for i in items])["mean"],
292
+ "ttfb_mean_ms": _stats([i.first_token_ms or 0.0 for i in items])["mean"],
293
+ "ttfb_p95_ms": _stats([i.first_token_ms or 0.0 for i in items])["p95"],
294
+ }
295
+ agg: Dict[str, List[float]] = {}
296
+ for it in items:
297
+ for k, v in it.stages_ms().items():
298
+ agg.setdefault(k, []).append(v)
299
+ for k, vals in agg.items():
300
+ st = _stats(vals)
301
+ out[f"{k}_mean_ms"] = st["mean"]
302
+ return out
303
+
304
+
305
+ # ------------------------------------------------------ thread-local current --
306
+
307
+ _local = threading.local()
308
+
309
+
310
+ def begin_request(request_id: Optional[str] = None) -> RequestMetrics:
311
+ """Starts a fresh RequestMetrics and binds it to the calling thread as
312
+ 'current' (worker threads run one request at a time)."""
313
+ m = RequestMetrics(request_id)
314
+ _local.current = m
315
+ return m
316
+
317
+
318
+ def current() -> Optional[RequestMetrics]:
319
+ """The calling thread's active request, if any. Deep code uses this to
320
+ record without signature changes; returns None outside a request."""
321
+ return getattr(_local, "current", None)
322
+
323
+
324
+ def clear_current() -> None:
325
+ _local.current = None
326
+
327
+
328
+ def format_duration(ms: Optional[float]) -> str:
329
+ if ms is None or ms <= 0:
330
+ return "n/a"
331
+ if ms < 1000:
332
+ return f"{ms:.0f} ms"
333
+ return f"{ms / 1000:.2f} s"
@@ -0,0 +1,146 @@
1
+ """
2
+ CCT — mode_detection.py: Automatic Mode Switching (v0.7.7 spec section 5).
3
+
4
+ The AI should intelligently determine which mode a message needs, switch
5
+ to it (with a visible transition), and keep the user informed. Modes are
6
+ the ai_modes.py personas plus the two the spec adds for autonomous work:
7
+
8
+ Notebook — conversation, explanation, teaching, documentation,
9
+ summaries, general chat.
10
+ Planner — roadmaps, architecture, algorithms, task planning,
11
+ project decomposition, todo generation.
12
+ Research — documentation search, API research, framework
13
+ comparison, web research, deep analysis.
14
+ Build — coding, file editing, dependency installation,
15
+ compilation, testing, execution, deployment.
16
+ Debugger — error analysis, crash investigation, performance
17
+ profiling, security analysis.
18
+ Agent — the tool-executing loop (kept for explicit /agent use;
19
+ the detector never force-switches into it, because
20
+ run_agent's blocking loop is a user choice, not an
21
+ auto-routing destination).
22
+
23
+ Honest scope: detection is keyword/regex-based intent classification
24
+ (the same category of heuristic the app's existing `detect_intent` /
25
+ `detect_derivation` use), NOT an LLM call per message — a model call
26
+ just to decide which prompt to send next would double every request's
27
+ cost for a label a regex table gets right ~95% of the time. The
28
+ classifier exposes its matched reason so the UI can show a real
29
+ transition note ("Switching Notebook → Build — detected a dependency
30
+ install request") instead of a silent persona change.
31
+
32
+ The router also recognizes the spec's *workflow* triggers: install
33
+ requests are routed to the package-manager flow (spec section 1),
34
+ deep-research-ish requests to Research mode, big build requests to the
35
+ autonomous pipeline (spec section 6) when the caller opts in.
36
+ """
37
+
38
+ if __name__ == "__main__":
39
+ print("This is a library file and is not meant to be run directly.")
40
+ import sys
41
+ sys.exit(1)
42
+
43
+ import re
44
+
45
+ # (regex, mode, reason-template). Ordered: first match wins.
46
+ _RULES = [
47
+ # ---- Build (software) ----
48
+ (re.compile(r"\b(install|pip install|npm install|npm i|apt install|brew install|choco install|winget install|cargo install|go install)\b", re.I),
49
+ "build", "dependency / package install request"),
50
+ (re.compile(r"\b(compile|build the project|build a project|build an app|build this|make a program|write the code|write code for|generate code|implement the|create a script|write a script|develop an app|create an app|create a project|set up a project|scaffold)\b", re.I),
51
+ "build", "code generation / project build request"),
52
+ (re.compile(r"\b(coding|codepad|code review|refactor|debug a (python|js|script)|deploy|deployment|dockerfile|docker compose|\.py\b|\.js\b|\.ts\b|\.html\b|\.css\b)\b", re.I),
53
+ "build", "software development task"),
54
+ # ---- Debugger ----
55
+ (re.compile(r"\b(debug this|why is this (broken|failing|erroring)|fix this error|crash|traceback|stack trace|segfault|exception|performance profiling|profile this|memory leak|security scan|security audit|vulnerab|malware|sandbox)\b", re.I),
56
+ "debugger", "error / performance / security diagnosis"),
57
+ (re.compile(r"\b(what's wrong with|what went wrong|error analysis|root cause|code is slow|slow code)\b", re.I),
58
+ "debugger", "failure / slowdown investigation"),
59
+ # ---- Research ----
60
+ (re.compile(r"\b(research|compare (the )?(frameworks|libraries|tools|packages)|which (library|framework|tool) (should|is best|to use)|latest version|documentation for|docs for|api reference|how does .* work)\b", re.I),
61
+ "research", "documentation / framework research"),
62
+ (re.compile(r"\b(best practices for|alternatives to|pros and cons|vs\.?\s|versus|is .* better than|difference between)\b", re.I),
63
+ "research", "technology comparison"),
64
+ # ---- Planner ----
65
+ (re.compile(r"\b(roadmap|architecture (design|diagram)|design (the )?(system|architecture|api)|algorithm design|task plan|plan of action|project plan|decompose|break this (down|into)|create a plan|todo list|milestones|phases? for|study plan)\b", re.I),
66
+ "plan", "planning / roadmap / task decomposition"),
67
+ # ---- Notebook (teaching / explanation) ----
68
+ (re.compile(r"\b(explain|teach|what is|what are|define|concept|concepts|summary|summarize|overview|derive|derive the|half ?life|mole concept|nernst|arrhenius|kinetics|thermodynamics|stoichiometry)\b", re.I),
69
+ "notebook", "conceptual / teaching question"),
70
+ ]
71
+
72
+ # Workflow triggers — not mode changes, but *pipeline* routing decisions.
73
+ # (regex, kind) where kind is one of "install" | "research_deep" |
74
+ # "pipeline" (a big multi-stage autonomous build).
75
+ _WORKFLOW_RULES = [
76
+ (re.compile(r"\b(install|setup|get)\s+(latest\s+)?[a-z0-9]", re.I), "install"),
77
+ (re.compile(r"\b(pip|npm|pnpm|yarn|bun|cargo|go install|conda|brew|choco|winget|apt|dnf|composer|vcpkg)\s+install\b", re.I), "install"),
78
+ (re.compile(r"\b(deep research|in-depth research|research thoroughly|full research)\b", re.I), "research_deep"),
79
+ (re.compile(r"\b(build (me |us )?(an? |the )?([a-z0-9-]+ )?(app|project|program|tool|api|website|script|viewer|dashboard|cli|game|module|server)|create a full (app|project|program|api)|develop a complete|implement a full)\b", re.I), "pipeline"),
80
+ ]
81
+
82
+ _DEBUG_PREFIXES = re.compile(r"^(traceback|error|exception|crash|fatal|segfault|core dump)", re.I)
83
+ _CODE_FENCE = re.compile(r"```")
84
+
85
+
86
+ def classify(text):
87
+ """Returns (mode_key, reason, confidence) for a piece of user text.
88
+ confidence is "high" (explicit rule match) or "low" (fallback:
89
+ Notebook — no signal either way)."""
90
+ if not text:
91
+ return "notebook", "empty message", "low"
92
+ low = text.strip()
93
+ if _CODE_FENCE.search(low):
94
+ return "build", "code block detected", "high"
95
+ if _DEBUG_PREFIXES.match(low):
96
+ return "debugger", "error report detected", "high"
97
+ for pattern, mode, reason in _RULES:
98
+ if pattern.search(low):
99
+ return mode, reason, "high"
100
+ # Weak conversational signals.
101
+ if re.search(r"\b(what do you think|any ideas|brainstorm|tell me about)\b", low, re.I):
102
+ return "plan", "open-ended ideation", "low"
103
+ return "notebook", "general conversation", "low"
104
+
105
+
106
+ def workflow_kind(text):
107
+ """Returns one of None | "install" | "research_deep" | "pipeline"
108
+ for workflow routing (spec sections 1/6), or None when the message
109
+ is ordinary conversation."""
110
+ if not text:
111
+ return None
112
+ low = text.strip()
113
+ for pattern, kind in _WORKFLOW_RULES:
114
+ if pattern.search(low):
115
+ return kind
116
+ return None
117
+
118
+
119
+ def transition_line(from_mode, to_mode, reason=""):
120
+ """The visible transition note callers show when auto-switching
121
+ (spec section 5: "Show transition animation"). Pure text — each UI
122
+ renders it its own way."""
123
+ from . import ai_modes
124
+ if from_mode == to_mode:
125
+ return None
126
+ f = ai_modes.meta(from_mode)
127
+ t = ai_modes.meta(to_mode)
128
+ base = f"{f['icon']} {f['label']} \u2192 {t['icon']} {t['label']}"
129
+ if reason:
130
+ base += f" \u2014 {reason}"
131
+ return base
132
+
133
+
134
+ def suggest_mode_change(current_mode, text):
135
+ """One-stop helper: classify `text`, and if the suggested mode
136
+ differs from `current_mode` (and isn't agent), return
137
+ (to_mode, transition_text). Otherwise return None."""
138
+ if current_mode == "agent":
139
+ # Never yank the user out of the explicit tool-executing loop
140
+ # based on a heuristic — auto-switching only applies between
141
+ # the streaming personas.
142
+ return None
143
+ mode, reason, _conf = classify(text)
144
+ if mode == current_mode:
145
+ return None
146
+ return mode, transition_line(current_mode, mode, reason)