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.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
calc_terminal/metrics.py
ADDED
|
@@ -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)
|