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
|
@@ -0,0 +1,1334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT Activity System — centralized real-time Activity/Event bus.
|
|
3
|
+
|
|
4
|
+
Every real agent operation, tool call, command, file op, permission check,
|
|
5
|
+
question, or system event publishes a structured Activity here.
|
|
6
|
+
The Textual UI subscribes and renders live — never fake animations.
|
|
7
|
+
|
|
8
|
+
Spec section 1 event shape:
|
|
9
|
+
{
|
|
10
|
+
"id": "unique-activity-id",
|
|
11
|
+
"type": "tool | command | file | permission | question | status | system",
|
|
12
|
+
"action": "search | grep | glob | read | edit | write | create | delete | run | test | build | web | provider | etc.",
|
|
13
|
+
"status": "pending | running | completed | failed | cancelled | paused | waiting",
|
|
14
|
+
"title": "Human-readable activity title",
|
|
15
|
+
"details": "Optional additional information",
|
|
16
|
+
"file": "Optional file path",
|
|
17
|
+
"command": "Optional command",
|
|
18
|
+
"tool": "Optional tool name",
|
|
19
|
+
"result": "Optional summarized result",
|
|
20
|
+
"timestamp": "...",
|
|
21
|
+
"duration_ms": 0
|
|
22
|
+
}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
import platform
|
|
29
|
+
import shutil
|
|
30
|
+
import subprocess
|
|
31
|
+
import time
|
|
32
|
+
import uuid
|
|
33
|
+
import threading
|
|
34
|
+
import logging
|
|
35
|
+
from dataclasses import dataclass, field, asdict
|
|
36
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger("cct.activity")
|
|
39
|
+
|
|
40
|
+
# ── diagnostic event debug flag (spec section 31: cat --debug-events)
|
|
41
|
+
_DEBUG_EVENTS = False
|
|
42
|
+
|
|
43
|
+
def set_debug_events(enable: bool = True) -> None:
|
|
44
|
+
global _DEBUG_EVENTS
|
|
45
|
+
_DEBUG_EVENTS = bool(enable)
|
|
46
|
+
|
|
47
|
+
def is_debug_events() -> bool:
|
|
48
|
+
return _DEBUG_EVENTS
|
|
49
|
+
|
|
50
|
+
# ── helper duration formatter ──────────────────────────────────────────────
|
|
51
|
+
def _fmt_duration(ms: int) -> str:
|
|
52
|
+
if ms < 1000:
|
|
53
|
+
return f"{ms}ms"
|
|
54
|
+
s = ms / 1000.0
|
|
55
|
+
if s < 60:
|
|
56
|
+
return f"{s:.1f}s"
|
|
57
|
+
m = int(s // 60)
|
|
58
|
+
r = s % 60
|
|
59
|
+
return f"{m}m {r:.0f}s"
|
|
60
|
+
|
|
61
|
+
# ── thread-local current turn (so any nested tool call without explicit turn_id still binds to the active UI turn)
|
|
62
|
+
_thread_local = threading.local()
|
|
63
|
+
|
|
64
|
+
def set_current_turn(turn_id: str) -> None:
|
|
65
|
+
try:
|
|
66
|
+
_thread_local.current_turn = turn_id or ""
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
def get_current_turn() -> str:
|
|
71
|
+
try:
|
|
72
|
+
return getattr(_thread_local, "current_turn", "") or ""
|
|
73
|
+
except Exception:
|
|
74
|
+
return ""
|
|
75
|
+
|
|
76
|
+
def clear_current_turn() -> None:
|
|
77
|
+
try:
|
|
78
|
+
_thread_local.current_turn = ""
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
# ── canonical status values (spec section 3) ───────────────────────────────
|
|
83
|
+
STATUS_QUEUED = "queued"
|
|
84
|
+
STATUS_PENDING = "pending"
|
|
85
|
+
STATUS_STARTED = "started"
|
|
86
|
+
STATUS_RUNNING = "running"
|
|
87
|
+
STATUS_COMPLETED = "completed"
|
|
88
|
+
STATUS_FAILED = "failed"
|
|
89
|
+
STATUS_CANCELLED = "cancelled"
|
|
90
|
+
STATUS_WAITING_PERMISSION = "waiting_permission"
|
|
91
|
+
STATUS_PAUSED = "paused"
|
|
92
|
+
STATUS_RETRYING = "retrying"
|
|
93
|
+
STATUS_WAITING = "waiting"
|
|
94
|
+
STATUS_WARNING = "warning"
|
|
95
|
+
|
|
96
|
+
# Backwards compat aliases
|
|
97
|
+
QUEUED = STATUS_QUEUED
|
|
98
|
+
PENDING = STATUS_PENDING
|
|
99
|
+
STARTED = STATUS_STARTED
|
|
100
|
+
RUNNING = STATUS_RUNNING
|
|
101
|
+
COMPLETED = STATUS_COMPLETED
|
|
102
|
+
FAILED = STATUS_FAILED
|
|
103
|
+
CANCELLED = STATUS_CANCELLED
|
|
104
|
+
WAITING_PERMISSION = STATUS_WAITING_PERMISSION
|
|
105
|
+
PAUSED = STATUS_PAUSED
|
|
106
|
+
RETRYING = STATUS_RETRYING
|
|
107
|
+
WAITING = STATUS_WAITING
|
|
108
|
+
WARNING = STATUS_WARNING
|
|
109
|
+
|
|
110
|
+
STATUSES = frozenset({
|
|
111
|
+
STATUS_QUEUED, STATUS_PENDING, STATUS_STARTED, STATUS_RUNNING, STATUS_COMPLETED,
|
|
112
|
+
STATUS_FAILED, STATUS_CANCELLED, STATUS_WAITING_PERMISSION,
|
|
113
|
+
STATUS_PAUSED, STATUS_RETRYING, STATUS_WAITING, STATUS_WARNING
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
# ── canonical event types (spec section 3) ───────────────────────────────────
|
|
117
|
+
EVENT_TASK_STARTED = "task.started"
|
|
118
|
+
EVENT_TASK_COMPLETED = "task.completed"
|
|
119
|
+
EVENT_TASK_FAILED = "task.failed"
|
|
120
|
+
|
|
121
|
+
EVENT_PLAN_CREATED = "plan.created"
|
|
122
|
+
EVENT_PLAN_STEP_STARTED = "plan.step_started"
|
|
123
|
+
EVENT_PLAN_STEP_COMPLETED = "plan.step_completed"
|
|
124
|
+
|
|
125
|
+
EVENT_MODEL_REQUEST_STARTED = "model.request_started"
|
|
126
|
+
EVENT_MODEL_TOKEN_STREAMING = "model.token_streaming"
|
|
127
|
+
EVENT_MODEL_REQUEST_COMPLETED = "model.request_completed"
|
|
128
|
+
EVENT_MODEL_REQUEST_FAILED = "model.request_failed"
|
|
129
|
+
|
|
130
|
+
EVENT_TOOL_STARTED = "tool.started"
|
|
131
|
+
EVENT_TOOL_PROGRESS = "tool.progress"
|
|
132
|
+
EVENT_TOOL_COMPLETED = "tool.completed"
|
|
133
|
+
EVENT_TOOL_FAILED = "tool.failed"
|
|
134
|
+
|
|
135
|
+
EVENT_FILE_READ_STARTED = "file.read_started"
|
|
136
|
+
EVENT_FILE_READ_COMPLETED = "file.read_completed"
|
|
137
|
+
EVENT_FILE_WRITE_STARTED = "file.write_started"
|
|
138
|
+
EVENT_FILE_WRITE_COMPLETED = "file.write_completed"
|
|
139
|
+
EVENT_FILE_DELETE_STARTED = "file.delete_started"
|
|
140
|
+
EVENT_FILE_DELETE_COMPLETED = "file.delete_completed"
|
|
141
|
+
|
|
142
|
+
EVENT_TERMINAL_STARTED = "terminal.started"
|
|
143
|
+
EVENT_TERMINAL_OUTPUT = "terminal.output"
|
|
144
|
+
EVENT_TERMINAL_COMPLETED = "terminal.completed"
|
|
145
|
+
EVENT_TERMINAL_FAILED = "terminal.failed"
|
|
146
|
+
|
|
147
|
+
EVENT_BROWSER_STARTED = "browser.started"
|
|
148
|
+
EVENT_BROWSER_NAVIGATION = "browser.navigation"
|
|
149
|
+
EVENT_BROWSER_CLICK = "browser.click"
|
|
150
|
+
EVENT_BROWSER_INPUT = "browser.input"
|
|
151
|
+
EVENT_BROWSER_SCREENSHOT = "browser.screenshot"
|
|
152
|
+
EVENT_BROWSER_COMPLETED = "browser.completed"
|
|
153
|
+
|
|
154
|
+
EVENT_TEST_STARTED = "test.started"
|
|
155
|
+
EVENT_TEST_PROGRESS = "test.progress"
|
|
156
|
+
EVENT_TEST_PASSED = "test.passed"
|
|
157
|
+
EVENT_TEST_FAILED = "test.failed"
|
|
158
|
+
|
|
159
|
+
EVENT_MEMORY_READ = "memory.read"
|
|
160
|
+
EVENT_MEMORY_WRITE = "memory.write"
|
|
161
|
+
|
|
162
|
+
EVENT_PERMISSION_REQUESTED = "permission.requested"
|
|
163
|
+
EVENT_PERMISSION_GRANTED = "permission.granted"
|
|
164
|
+
EVENT_PERMISSION_DENIED = "permission.denied"
|
|
165
|
+
|
|
166
|
+
EVENT_AGENT_PAUSED = "agent.paused"
|
|
167
|
+
EVENT_AGENT_RESUMED = "agent.resumed"
|
|
168
|
+
EVENT_AGENT_RETRYING = "agent.retrying"
|
|
169
|
+
EVENT_AGENT_CANCELLED = "agent.cancelled"
|
|
170
|
+
|
|
171
|
+
EVENT_RESOURCE_SAMPLE = "resource.sample"
|
|
172
|
+
EVENT_SYSTEM_WARNING = "system.warning"
|
|
173
|
+
EVENT_SYSTEM_ERROR = "system.error"
|
|
174
|
+
|
|
175
|
+
# ── canonical categories (spec section 1) ───────────────────────────────────
|
|
176
|
+
CAT_SEARCH = "SEARCH"
|
|
177
|
+
CAT_FILE_READ = "FILE_READ"
|
|
178
|
+
CAT_EDIT = "EDIT"
|
|
179
|
+
CAT_TERMINAL = "TERMINAL"
|
|
180
|
+
CAT_TEST = "TEST"
|
|
181
|
+
CAT_BUILD = "BUILD"
|
|
182
|
+
CAT_ANALYSIS = "ANALYSIS"
|
|
183
|
+
CAT_GIT = "GIT"
|
|
184
|
+
CAT_PLANNING = "PLANNING"
|
|
185
|
+
CAT_SUBAGENT = "SUBAGENT"
|
|
186
|
+
CAT_SYSTEM = "SYSTEM"
|
|
187
|
+
CAT_MODEL = "MODEL"
|
|
188
|
+
|
|
189
|
+
CATEGORIES = frozenset({
|
|
190
|
+
CAT_SEARCH, CAT_FILE_READ, CAT_EDIT, CAT_TERMINAL, CAT_TEST,
|
|
191
|
+
CAT_BUILD, CAT_ANALYSIS, CAT_GIT, CAT_PLANNING, CAT_SUBAGENT,
|
|
192
|
+
CAT_SYSTEM, CAT_MODEL
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
# ── canonical phases ────────────────────────────────────────────────────────
|
|
196
|
+
PHASE_DISCOVERY = "DISCOVERY"
|
|
197
|
+
PHASE_ANALYSIS = "ANALYSIS"
|
|
198
|
+
PHASE_IMPLEMENTATION = "IMPLEMENTATION"
|
|
199
|
+
PHASE_VALIDATION = "VALIDATION"
|
|
200
|
+
PHASE_COMPLETE = "COMPLETE"
|
|
201
|
+
|
|
202
|
+
PHASE_ICONS = {
|
|
203
|
+
PHASE_DISCOVERY: "⌕",
|
|
204
|
+
PHASE_ANALYSIS: "🧠",
|
|
205
|
+
PHASE_IMPLEMENTATION: "✎",
|
|
206
|
+
PHASE_VALIDATION: "▶",
|
|
207
|
+
PHASE_COMPLETE: "✓",
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# ── canonical type values (spec section 1) ─────────────────────────────────
|
|
211
|
+
TYPE_TOOL = "tool"
|
|
212
|
+
TYPE_COMMAND = "command"
|
|
213
|
+
TYPE_FILE = "file"
|
|
214
|
+
TYPE_PERMISSION = "permission"
|
|
215
|
+
TYPE_QUESTION = "question"
|
|
216
|
+
TYPE_STATUS = "status"
|
|
217
|
+
TYPE_SYSTEM = "system"
|
|
218
|
+
TYPE_PROVIDER = "provider"
|
|
219
|
+
TYPE_SEARCH = "search"
|
|
220
|
+
TYPE_WORKSPACE = "workspace"
|
|
221
|
+
|
|
222
|
+
TYPES = frozenset({TYPE_TOOL, TYPE_COMMAND, TYPE_FILE, TYPE_PERMISSION, TYPE_QUESTION, TYPE_STATUS, TYPE_SYSTEM, TYPE_PROVIDER, TYPE_SEARCH, TYPE_WORKSPACE})
|
|
223
|
+
|
|
224
|
+
# status -> static icon (spec section 3, accessibility: icon+text not color alone)
|
|
225
|
+
STATUS_ICONS = {
|
|
226
|
+
STATUS_PENDING: "⏳",
|
|
227
|
+
STATUS_STARTED: "◐",
|
|
228
|
+
STATUS_RUNNING: "◐",
|
|
229
|
+
STATUS_COMPLETED: "✓",
|
|
230
|
+
STATUS_FAILED: "✕",
|
|
231
|
+
STATUS_CANCELLED: "⊘",
|
|
232
|
+
STATUS_WAITING_PERMISSION: "⚠",
|
|
233
|
+
STATUS_PAUSED: "⏸",
|
|
234
|
+
STATUS_WAITING: "?",
|
|
235
|
+
STATUS_WARNING: "⚠",
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# Backwards compat alias for older search of icons
|
|
239
|
+
STATE_ICONS = STATUS_ICONS
|
|
240
|
+
|
|
241
|
+
# throttle: if > N events per second for same turn, coalesce
|
|
242
|
+
_THROTTLE_WINDOW = 0.05 # 50ms
|
|
243
|
+
_MAX_PER_TURN = 120
|
|
244
|
+
_MAX_TOTAL = 500
|
|
245
|
+
_MAX_BUFFER = 500
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@dataclass
|
|
249
|
+
class ResourceSample:
|
|
250
|
+
timestamp: float
|
|
251
|
+
system_ram_used_gb: float
|
|
252
|
+
system_ram_total_gb: float
|
|
253
|
+
system_ram_percent: float
|
|
254
|
+
process_ram_mb: float
|
|
255
|
+
cpu_percent: float
|
|
256
|
+
disk_percent: float = 0.0
|
|
257
|
+
gpu_vram_used_gb: Optional[float] = None
|
|
258
|
+
gpu_vram_total_gb: Optional[float] = None
|
|
259
|
+
gpu_name: Optional[str] = None
|
|
260
|
+
warning_level: str = "normal" # normal | elevated | high | critical
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class ResourceMonitor:
|
|
264
|
+
"""
|
|
265
|
+
Real-time system resource monitor.
|
|
266
|
+
Non-blocking, configurable background daemon sampler.
|
|
267
|
+
Measures actual RAM, CPU, and process RSS without guessing.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
def __init__(self, interval_sec: float = 1.0, on_sample: Optional[Callable[[ResourceSample], None]] = None):
|
|
271
|
+
self.interval_sec = interval_sec
|
|
272
|
+
self._running = False
|
|
273
|
+
self._thread: Optional[threading.Thread] = None
|
|
274
|
+
self._lock = threading.Lock()
|
|
275
|
+
self._latest_sample: Optional[ResourceSample] = None
|
|
276
|
+
self._on_sample = on_sample
|
|
277
|
+
self._last_warning_time = 0.0
|
|
278
|
+
self._last_warning_level = "normal"
|
|
279
|
+
self._cooldown_sec = 30.0
|
|
280
|
+
|
|
281
|
+
def sample_now(self) -> ResourceSample:
|
|
282
|
+
sys_used_gb = 0.0
|
|
283
|
+
sys_total_gb = 0.0
|
|
284
|
+
sys_pct = 0.0
|
|
285
|
+
proc_mb = 0.0
|
|
286
|
+
cpu_pct = 0.0
|
|
287
|
+
disk_pct = 0.0
|
|
288
|
+
gpu_name = None
|
|
289
|
+
gpu_used_gb = None
|
|
290
|
+
gpu_total_gb = None
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
import psutil
|
|
294
|
+
vm = psutil.virtual_memory()
|
|
295
|
+
sys_total_gb = round(vm.total / (1024 ** 3), 2)
|
|
296
|
+
sys_used_gb = round(vm.used / (1024 ** 3), 2)
|
|
297
|
+
sys_pct = round(vm.percent, 1)
|
|
298
|
+
|
|
299
|
+
proc = psutil.Process(os.getpid())
|
|
300
|
+
proc_mb = round(proc.memory_info().rss / (1024 ** 2), 1)
|
|
301
|
+
|
|
302
|
+
cpu_pct = round(psutil.cpu_percent(interval=None), 1)
|
|
303
|
+
try:
|
|
304
|
+
disk = psutil.disk_usage(os.getcwd() if os.path.exists(os.getcwd()) else "/")
|
|
305
|
+
disk_pct = round(disk.percent, 1)
|
|
306
|
+
except Exception:
|
|
307
|
+
disk_pct = 0.0
|
|
308
|
+
except Exception:
|
|
309
|
+
try:
|
|
310
|
+
if platform.system() == "Windows":
|
|
311
|
+
import ctypes
|
|
312
|
+
class MEMORYSTATUSEX(ctypes.Structure):
|
|
313
|
+
_fields_ = [
|
|
314
|
+
("dwLength", ctypes.c_ulong), ("dwMemoryLoad", ctypes.c_ulong),
|
|
315
|
+
("ullTotalPhys", ctypes.c_ulonglong), ("ullAvailPhys", ctypes.c_ulonglong),
|
|
316
|
+
("ullTotalPageFile", ctypes.c_ulonglong), ("ullAvailPageFile", ctypes.c_ulonglong),
|
|
317
|
+
("ullTotalVirtual", ctypes.c_ulonglong), ("ullAvailVirtual", ctypes.c_ulonglong),
|
|
318
|
+
("ullAvailExtendedVirtual", ctypes.c_ulonglong)
|
|
319
|
+
]
|
|
320
|
+
stat = MEMORYSTATUSEX(dwLength=ctypes.sizeof(MEMORYSTATUSEX))
|
|
321
|
+
ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat))
|
|
322
|
+
sys_total_gb = round(int(stat.ullTotalPhys) / (1024 ** 3), 2)
|
|
323
|
+
avail_gb = round(int(stat.ullAvailPhys) / (1024 ** 3), 2)
|
|
324
|
+
sys_used_gb = round(sys_total_gb - avail_gb, 2)
|
|
325
|
+
sys_pct = round((sys_used_gb / sys_total_gb) * 100, 1) if sys_total_gb > 0 else 0.0
|
|
326
|
+
except Exception:
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
# Try real GPU query via nvidia-smi if available (never fake!)
|
|
330
|
+
try:
|
|
331
|
+
if shutil.which("nvidia-smi"):
|
|
332
|
+
out = subprocess.check_output(
|
|
333
|
+
["nvidia-smi", "--query-gpu=name,memory.used,memory.total", "--format=csv,noheader,nounits"],
|
|
334
|
+
timeout=1.5, text=True
|
|
335
|
+
)
|
|
336
|
+
lines = [l.strip() for l in out.splitlines() if l.strip()]
|
|
337
|
+
if lines:
|
|
338
|
+
parts = [p.strip() for p in lines[0].split(",")]
|
|
339
|
+
if len(parts) >= 3:
|
|
340
|
+
gpu_name = parts[0]
|
|
341
|
+
gpu_used_gb = round(float(parts[1]) / 1024.0, 2)
|
|
342
|
+
gpu_total_gb = round(float(parts[2]) / 1024.0, 2)
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
# Thresholds per spec:
|
|
347
|
+
# <70% normal, 70-85% elevated, 85-95% high, >95% critical
|
|
348
|
+
warn_level = "normal"
|
|
349
|
+
if sys_pct > 95.0:
|
|
350
|
+
warn_level = "critical"
|
|
351
|
+
elif sys_pct > 85.0:
|
|
352
|
+
warn_level = "high"
|
|
353
|
+
elif sys_pct > 70.0:
|
|
354
|
+
warn_level = "elevated"
|
|
355
|
+
|
|
356
|
+
sample = ResourceSample(
|
|
357
|
+
timestamp=time.time(),
|
|
358
|
+
system_ram_used_gb=sys_used_gb,
|
|
359
|
+
system_ram_total_gb=sys_total_gb,
|
|
360
|
+
system_ram_percent=sys_pct,
|
|
361
|
+
process_ram_mb=proc_mb,
|
|
362
|
+
cpu_percent=cpu_pct,
|
|
363
|
+
disk_percent=disk_pct,
|
|
364
|
+
gpu_vram_used_gb=gpu_used_gb,
|
|
365
|
+
gpu_vram_total_gb=gpu_total_gb,
|
|
366
|
+
gpu_name=gpu_name,
|
|
367
|
+
warning_level=warn_level,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
with self._lock:
|
|
371
|
+
self._latest_sample = sample
|
|
372
|
+
|
|
373
|
+
# Check threshold warnings with cooldown
|
|
374
|
+
now = time.time()
|
|
375
|
+
if warn_level != "normal":
|
|
376
|
+
if (now - self._last_warning_time > self._cooldown_sec) or (warn_level != self._last_warning_level and warn_level in ("high", "critical")):
|
|
377
|
+
self._last_warning_time = now
|
|
378
|
+
self._last_warning_level = warn_level
|
|
379
|
+
try:
|
|
380
|
+
mgr = globals().get("manager")
|
|
381
|
+
if mgr:
|
|
382
|
+
mgr.create(
|
|
383
|
+
type=TYPE_SYSTEM,
|
|
384
|
+
action="resource_warning",
|
|
385
|
+
title=f"⚠ System RAM {warn_level.upper()}: {sys_used_gb:.1f}/{sys_total_gb:.1f} GB ({sys_pct:.1f}%)",
|
|
386
|
+
status=STATUS_WARNING,
|
|
387
|
+
details=f"System RAM: {sys_used_gb:.1f}/{sys_total_gb:.1f} GB ({sys_pct:.1f}%), CAT Process: {proc_mb:.1f} MB, CPU: {cpu_pct:.1f}%",
|
|
388
|
+
event_type=EVENT_SYSTEM_WARNING,
|
|
389
|
+
category=CAT_SYSTEM,
|
|
390
|
+
phase=PHASE_IMPLEMENTATION,
|
|
391
|
+
)
|
|
392
|
+
except Exception:
|
|
393
|
+
pass
|
|
394
|
+
|
|
395
|
+
return sample
|
|
396
|
+
|
|
397
|
+
def start(self):
|
|
398
|
+
with self._lock:
|
|
399
|
+
if self._running:
|
|
400
|
+
return
|
|
401
|
+
self._running = True
|
|
402
|
+
self._thread = threading.Thread(target=self._run_loop, daemon=True, name="CCT-ResourceMonitor")
|
|
403
|
+
self._thread.start()
|
|
404
|
+
|
|
405
|
+
def stop(self):
|
|
406
|
+
with self._lock:
|
|
407
|
+
self._running = False
|
|
408
|
+
|
|
409
|
+
def _run_loop(self):
|
|
410
|
+
while self._running:
|
|
411
|
+
try:
|
|
412
|
+
sample = self.sample_now()
|
|
413
|
+
if self._on_sample:
|
|
414
|
+
self._on_sample(sample)
|
|
415
|
+
except Exception as e:
|
|
416
|
+
logger.debug("Resource sampler loop error: %s", e)
|
|
417
|
+
time.sleep(self.interval_sec)
|
|
418
|
+
|
|
419
|
+
def get_latest(self) -> ResourceSample:
|
|
420
|
+
with self._lock:
|
|
421
|
+
if self._latest_sample is not None:
|
|
422
|
+
return self._latest_sample
|
|
423
|
+
return self.sample_now()
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@dataclass
|
|
427
|
+
class Activity:
|
|
428
|
+
id: str
|
|
429
|
+
type: str # activity_type
|
|
430
|
+
action: str
|
|
431
|
+
status: str
|
|
432
|
+
title: str
|
|
433
|
+
details: str = ""
|
|
434
|
+
file: str = ""
|
|
435
|
+
command: str = ""
|
|
436
|
+
tool: str = ""
|
|
437
|
+
result: str = ""
|
|
438
|
+
timestamp: float = field(default_factory=time.time) # updated_time
|
|
439
|
+
duration_ms: int = 0
|
|
440
|
+
turn_id: str = ""
|
|
441
|
+
# internal helpers not in spec but needed for UI
|
|
442
|
+
start_time: float = field(default_factory=time.time)
|
|
443
|
+
output_lines: List[str] = field(default_factory=list)
|
|
444
|
+
expanded: bool = False
|
|
445
|
+
parent_id: str = ""
|
|
446
|
+
progress: Optional[float] = None # nullable per spec
|
|
447
|
+
# --- canonical category & phase ---
|
|
448
|
+
category: str = ""
|
|
449
|
+
phase: str = ""
|
|
450
|
+
# --- execution details ---
|
|
451
|
+
stdout: str = ""
|
|
452
|
+
stderr: str = ""
|
|
453
|
+
exit_code: Optional[int] = None
|
|
454
|
+
target_path: str = ""
|
|
455
|
+
line_count: Optional[int] = None
|
|
456
|
+
diff_summary: str = ""
|
|
457
|
+
query: str = ""
|
|
458
|
+
match_count: Optional[int] = None
|
|
459
|
+
permission_req: Optional[Dict[str, Any]] = None
|
|
460
|
+
# --- spec-required extended fields (all real, never fake) ---
|
|
461
|
+
description: str = "" # alias for details, human-readable
|
|
462
|
+
updated_time: float = field(default_factory=time.time)
|
|
463
|
+
end_time: Optional[float] = None
|
|
464
|
+
model: str = "" # real provider model, e.g. nvidia/nemotron-3-ultra-550b
|
|
465
|
+
provider: str = "" # real provider id, e.g. nvidia
|
|
466
|
+
workspace: str = "" # real workspace path
|
|
467
|
+
current_step: str = "" # real current step description
|
|
468
|
+
error: str = "" # real error message when FAILED
|
|
469
|
+
cancellable: bool = True
|
|
470
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
471
|
+
# --- 100% real event schema extensions ---
|
|
472
|
+
event_id: str = ""
|
|
473
|
+
task_id: str = ""
|
|
474
|
+
parent_event_id: str = ""
|
|
475
|
+
event_type: str = ""
|
|
476
|
+
source: str = ""
|
|
477
|
+
duration: float = 0.0
|
|
478
|
+
operation_id: str = ""
|
|
479
|
+
pid: Optional[int] = None
|
|
480
|
+
lines_added: int = 0
|
|
481
|
+
lines_removed: int = 0
|
|
482
|
+
|
|
483
|
+
def __post_init__(self):
|
|
484
|
+
# keep aliases in sync - description/details, timestamp/updated_time
|
|
485
|
+
if self.description and not self.details:
|
|
486
|
+
self.details = self.description
|
|
487
|
+
elif self.details and not self.description:
|
|
488
|
+
self.description = self.details
|
|
489
|
+
if not self.updated_time:
|
|
490
|
+
self.updated_time = self.timestamp
|
|
491
|
+
else:
|
|
492
|
+
self.timestamp = self.updated_time
|
|
493
|
+
if self.file and not self.target_path:
|
|
494
|
+
self.target_path = self.file
|
|
495
|
+
elif self.target_path and not self.file:
|
|
496
|
+
self.file = self.target_path
|
|
497
|
+
# keep event schema aliases in sync
|
|
498
|
+
if not self.event_id:
|
|
499
|
+
self.event_id = self.id
|
|
500
|
+
elif not self.id:
|
|
501
|
+
self.id = self.event_id
|
|
502
|
+
if not self.task_id:
|
|
503
|
+
self.task_id = self.turn_id
|
|
504
|
+
elif not self.turn_id:
|
|
505
|
+
self.turn_id = self.task_id
|
|
506
|
+
if not self.parent_event_id:
|
|
507
|
+
self.parent_event_id = self.parent_id
|
|
508
|
+
elif not self.parent_id:
|
|
509
|
+
self.parent_id = self.parent_event_id
|
|
510
|
+
if not self.event_type:
|
|
511
|
+
self.event_type = f"{self.type}.{self.action}" if self.type and self.action else (self.type or "task.operation")
|
|
512
|
+
if self.duration == 0 and self.duration_ms > 0:
|
|
513
|
+
self.duration = self.duration_ms / 1000.0
|
|
514
|
+
elif self.duration > 0 and self.duration_ms == 0:
|
|
515
|
+
self.duration_ms = int(self.duration * 1000)
|
|
516
|
+
# auto-infer category if not given
|
|
517
|
+
if not self.category:
|
|
518
|
+
if self.tool in ("search_workspace", "list_directory", "web_search", "deep_research") or self.type == TYPE_SEARCH:
|
|
519
|
+
self.category = CAT_SEARCH
|
|
520
|
+
elif self.tool in ("read_file", "read_attachment", "archive_list") or (self.type == TYPE_FILE and self.action in ("read", "list")):
|
|
521
|
+
self.category = CAT_FILE_READ
|
|
522
|
+
elif self.tool in ("write_file", "create_folder", "delete_file", "rename_file", "edit_file") or (self.type == TYPE_FILE and self.action in ("write", "edit", "create", "delete", "rename")):
|
|
523
|
+
self.category = CAT_EDIT
|
|
524
|
+
elif self.tool in ("run_terminal", "install_packages", "device_action") or self.type == TYPE_COMMAND:
|
|
525
|
+
self.category = CAT_TERMINAL
|
|
526
|
+
elif self.tool in ("run_tests",) or self.action == "test":
|
|
527
|
+
self.category = CAT_TEST
|
|
528
|
+
elif self.tool in ("run_build", "archive_validate") or self.action == "build":
|
|
529
|
+
self.category = CAT_BUILD
|
|
530
|
+
elif self.type == TYPE_PROVIDER:
|
|
531
|
+
self.category = CAT_MODEL
|
|
532
|
+
elif self.type == TYPE_PERMISSION:
|
|
533
|
+
self.category = CAT_SYSTEM
|
|
534
|
+
else:
|
|
535
|
+
self.category = CAT_ANALYSIS
|
|
536
|
+
# auto-infer phase if not given
|
|
537
|
+
if not self.phase:
|
|
538
|
+
if self.category in (CAT_SEARCH, CAT_FILE_READ):
|
|
539
|
+
self.phase = PHASE_DISCOVERY
|
|
540
|
+
elif self.category in (CAT_ANALYSIS, CAT_PLANNING, CAT_MODEL):
|
|
541
|
+
self.phase = PHASE_ANALYSIS
|
|
542
|
+
elif self.category in (CAT_EDIT, CAT_TERMINAL, CAT_GIT):
|
|
543
|
+
self.phase = PHASE_IMPLEMENTATION
|
|
544
|
+
elif self.category in (CAT_TEST, CAT_BUILD):
|
|
545
|
+
self.phase = PHASE_VALIDATION
|
|
546
|
+
else:
|
|
547
|
+
self.phase = PHASE_IMPLEMENTATION
|
|
548
|
+
|
|
549
|
+
@property
|
|
550
|
+
def activity_id(self) -> str:
|
|
551
|
+
return self.id
|
|
552
|
+
|
|
553
|
+
@property
|
|
554
|
+
def activity_type(self) -> str:
|
|
555
|
+
return self.type
|
|
556
|
+
|
|
557
|
+
@property
|
|
558
|
+
def elapsed_time(self) -> float:
|
|
559
|
+
return self.elapsed_ms / 1000.0
|
|
560
|
+
|
|
561
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
562
|
+
d = asdict(self)
|
|
563
|
+
# spec expects timestamp as iso or float; keep float for now
|
|
564
|
+
# add computed elapsed
|
|
565
|
+
d["elapsed_time"] = self.elapsed_time
|
|
566
|
+
d["activity_id"] = self.id
|
|
567
|
+
d["activity_type"] = self.type
|
|
568
|
+
return d
|
|
569
|
+
|
|
570
|
+
@property
|
|
571
|
+
def icon(self) -> str:
|
|
572
|
+
return STATUS_ICONS.get(self.status, "·")
|
|
573
|
+
|
|
574
|
+
@property
|
|
575
|
+
def elapsed_ms(self) -> int:
|
|
576
|
+
if self.status in (COMPLETED, FAILED, CANCELLED, WARNING):
|
|
577
|
+
return self.duration_ms
|
|
578
|
+
return int((time.time() - self.start_time) * 1000)
|
|
579
|
+
|
|
580
|
+
@property
|
|
581
|
+
def summary(self) -> str:
|
|
582
|
+
"""Human-readable one-liner for collapsed history."""
|
|
583
|
+
base = f"{self.icon} {self.title}"
|
|
584
|
+
if self.result:
|
|
585
|
+
base += f" — {self.result[:80]}"
|
|
586
|
+
elif self.details:
|
|
587
|
+
base += f" — {self.details[:80]}"
|
|
588
|
+
return base
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
class ActivityManager:
|
|
592
|
+
"""
|
|
593
|
+
Central activity bus. Thread-safe. Backend emits, UI subscribes.
|
|
594
|
+
Synchronous delivery (callbacks fire in emit thread) — UI callbacks
|
|
595
|
+
must use call_from_thread if they touch Textual widgets.
|
|
596
|
+
"""
|
|
597
|
+
|
|
598
|
+
def __init__(self):
|
|
599
|
+
self._activities: Dict[str, Activity] = {}
|
|
600
|
+
self._turn_order: Dict[str, List[str]] = {} # turn_id -> [activity_id] in creation order
|
|
601
|
+
self._global_order: List[str] = [] # all activity ids in order
|
|
602
|
+
self._subscribers: List[Callable[[Activity, str], None]] = []
|
|
603
|
+
self._lock = threading.RLock()
|
|
604
|
+
self._throttle_last: Dict[str, float] = {} # activity_id -> last notify time
|
|
605
|
+
self._pending_throttle: Dict[str, Activity] = {}
|
|
606
|
+
# deduplication & mapping
|
|
607
|
+
self._operation_map: Dict[str, str] = {} # operation_id -> activity_id
|
|
608
|
+
self._active_dedup_keys: Dict[str, str] = {} # dedup_key -> activity_id
|
|
609
|
+
# real-time resource monitor daemon
|
|
610
|
+
self.resource_monitor = ResourceMonitor(interval_sec=1.0)
|
|
611
|
+
try:
|
|
612
|
+
self.resource_monitor.start()
|
|
613
|
+
except Exception as e:
|
|
614
|
+
logger.debug("Failed to start resource monitor daemon: %s", e)
|
|
615
|
+
|
|
616
|
+
# ── subscription ───────────────────────────────────────────────────
|
|
617
|
+
def subscribe(self, callback: Callable[[Activity, str], None]) -> Callable:
|
|
618
|
+
with self._lock:
|
|
619
|
+
self._subscribers.append(callback)
|
|
620
|
+
return callback
|
|
621
|
+
|
|
622
|
+
def unsubscribe(self, callback: Callable) -> None:
|
|
623
|
+
with self._lock:
|
|
624
|
+
if callback in self._subscribers:
|
|
625
|
+
self._subscribers.remove(callback)
|
|
626
|
+
|
|
627
|
+
def _notify(self, activity: Activity, change: str) -> None:
|
|
628
|
+
# change: "created" | "updated" | "removed"
|
|
629
|
+
subs = list(self._subscribers)
|
|
630
|
+
for cb in subs:
|
|
631
|
+
try:
|
|
632
|
+
cb(activity, change)
|
|
633
|
+
except Exception as e:
|
|
634
|
+
logger.debug("Activity subscriber error: %s", e)
|
|
635
|
+
|
|
636
|
+
# ── creation ───────────────────────────────────────────────────────
|
|
637
|
+
def create(
|
|
638
|
+
self,
|
|
639
|
+
type: str,
|
|
640
|
+
action: str,
|
|
641
|
+
title: str,
|
|
642
|
+
status: str = RUNNING,
|
|
643
|
+
details: str = "",
|
|
644
|
+
file: str = "",
|
|
645
|
+
command: str = "",
|
|
646
|
+
tool: str = "",
|
|
647
|
+
result: str = "",
|
|
648
|
+
turn_id: str = "",
|
|
649
|
+
parent_id: str = "",
|
|
650
|
+
duration_ms: int = 0,
|
|
651
|
+
# spec extended fields - all from real runtime, never fake
|
|
652
|
+
category: str = "",
|
|
653
|
+
phase: str = "",
|
|
654
|
+
stdout: str = "",
|
|
655
|
+
stderr: str = "",
|
|
656
|
+
exit_code: Optional[int] = None,
|
|
657
|
+
target_path: str = "",
|
|
658
|
+
line_count: Optional[int] = None,
|
|
659
|
+
diff_summary: str = "",
|
|
660
|
+
query: str = "",
|
|
661
|
+
match_count: Optional[int] = None,
|
|
662
|
+
permission_req: Optional[Dict[str, Any]] = None,
|
|
663
|
+
description: str = "",
|
|
664
|
+
progress: Optional[float] = None,
|
|
665
|
+
model: str = "",
|
|
666
|
+
provider: str = "",
|
|
667
|
+
workspace: str = "",
|
|
668
|
+
current_step: str = "",
|
|
669
|
+
error: str = "",
|
|
670
|
+
cancellable: bool = True,
|
|
671
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
672
|
+
id: Optional[str] = "",
|
|
673
|
+
# 100% real event schema extensions
|
|
674
|
+
operation_id: str = "",
|
|
675
|
+
event_id: str = "",
|
|
676
|
+
task_id: str = "",
|
|
677
|
+
parent_event_id: str = "",
|
|
678
|
+
event_type: str = "",
|
|
679
|
+
source: str = "",
|
|
680
|
+
pid: Optional[int] = None,
|
|
681
|
+
lines_added: int = 0,
|
|
682
|
+
lines_removed: int = 0,
|
|
683
|
+
) -> Activity:
|
|
684
|
+
if not turn_id:
|
|
685
|
+
turn_id = task_id or get_current_turn()
|
|
686
|
+
if not parent_id and parent_event_id:
|
|
687
|
+
parent_id = parent_event_id
|
|
688
|
+
if not id and event_id:
|
|
689
|
+
id = event_id
|
|
690
|
+
if status not in STATUSES:
|
|
691
|
+
status = RUNNING
|
|
692
|
+
|
|
693
|
+
now = time.time()
|
|
694
|
+
# description aliases details
|
|
695
|
+
if description and not details:
|
|
696
|
+
details = description
|
|
697
|
+
elif details and not description:
|
|
698
|
+
description = details
|
|
699
|
+
|
|
700
|
+
# ── Deduplication Guard: Check existing operation or running signature ──
|
|
701
|
+
with self._lock:
|
|
702
|
+
if operation_id and operation_id in self._operation_map:
|
|
703
|
+
existing_id = self._operation_map[operation_id]
|
|
704
|
+
existing = self._activities.get(existing_id)
|
|
705
|
+
if existing:
|
|
706
|
+
# Update existing activity instead of creating duplicate card
|
|
707
|
+
updates: Dict[str, Any] = {"title": title, "status": status}
|
|
708
|
+
if details:
|
|
709
|
+
updates["details"] = details
|
|
710
|
+
if result:
|
|
711
|
+
updates["result"] = result
|
|
712
|
+
if duration_ms > 0:
|
|
713
|
+
updates["duration_ms"] = duration_ms
|
|
714
|
+
if exit_code is not None:
|
|
715
|
+
updates["exit_code"] = exit_code
|
|
716
|
+
if lines_added > 0:
|
|
717
|
+
updates["lines_added"] = lines_added
|
|
718
|
+
if lines_removed > 0:
|
|
719
|
+
updates["lines_removed"] = lines_removed
|
|
720
|
+
return self.update(existing_id, **updates) or existing
|
|
721
|
+
|
|
722
|
+
# Signature deduplication for active/running operations in the same turn
|
|
723
|
+
target_key = target_path or file or command or title
|
|
724
|
+
dedup_key = f"{turn_id}:{type}:{tool}:{target_key}"
|
|
725
|
+
if status in (STATUS_RUNNING, STATUS_PENDING, STATUS_STARTED, STATUS_QUEUED) and dedup_key in self._active_dedup_keys:
|
|
726
|
+
existing_id = self._active_dedup_keys[dedup_key]
|
|
727
|
+
existing = self._activities.get(existing_id)
|
|
728
|
+
if existing and existing.status in (STATUS_RUNNING, STATUS_PENDING, STATUS_STARTED, STATUS_QUEUED):
|
|
729
|
+
if details and details != existing.details:
|
|
730
|
+
existing.details = details
|
|
731
|
+
existing.description = details
|
|
732
|
+
if title and title != existing.title:
|
|
733
|
+
existing.title = title
|
|
734
|
+
if progress is not None:
|
|
735
|
+
existing.progress = progress
|
|
736
|
+
existing.updated_time = now
|
|
737
|
+
self._notify(existing, "updated")
|
|
738
|
+
return existing
|
|
739
|
+
|
|
740
|
+
# Generate canonical EVT-<hex> if not specified
|
|
741
|
+
act_id = id or f"EVT-{uuid.uuid4().hex[:8].upper()}"
|
|
742
|
+
|
|
743
|
+
# try to fill workspace/model/provider from real context if not provided
|
|
744
|
+
if not workspace:
|
|
745
|
+
try:
|
|
746
|
+
from . import workspace as _ws
|
|
747
|
+
workspace = _ws.root_dir() or ""
|
|
748
|
+
except Exception:
|
|
749
|
+
pass
|
|
750
|
+
if not provider or not model:
|
|
751
|
+
try:
|
|
752
|
+
from . import aicore as _ac
|
|
753
|
+
cfg = _ac.load_config()
|
|
754
|
+
if not provider:
|
|
755
|
+
provider = cfg.get("provider") or ""
|
|
756
|
+
if not model:
|
|
757
|
+
model = cfg.get("model") or ""
|
|
758
|
+
except Exception:
|
|
759
|
+
pass
|
|
760
|
+
|
|
761
|
+
act = Activity(
|
|
762
|
+
id=act_id,
|
|
763
|
+
type=type,
|
|
764
|
+
action=action,
|
|
765
|
+
status=status,
|
|
766
|
+
title=title,
|
|
767
|
+
details=details,
|
|
768
|
+
description=description,
|
|
769
|
+
file=file,
|
|
770
|
+
command=command,
|
|
771
|
+
tool=tool,
|
|
772
|
+
result=result,
|
|
773
|
+
timestamp=now,
|
|
774
|
+
updated_time=now,
|
|
775
|
+
start_time=now,
|
|
776
|
+
duration_ms=duration_ms,
|
|
777
|
+
turn_id=turn_id or "",
|
|
778
|
+
parent_id=parent_id or "",
|
|
779
|
+
progress=progress,
|
|
780
|
+
category=category,
|
|
781
|
+
phase=phase,
|
|
782
|
+
stdout=stdout,
|
|
783
|
+
stderr=stderr,
|
|
784
|
+
exit_code=exit_code,
|
|
785
|
+
target_path=target_path or file,
|
|
786
|
+
line_count=line_count,
|
|
787
|
+
diff_summary=diff_summary,
|
|
788
|
+
query=query,
|
|
789
|
+
match_count=match_count,
|
|
790
|
+
permission_req=permission_req,
|
|
791
|
+
model=model,
|
|
792
|
+
provider=provider,
|
|
793
|
+
workspace=workspace,
|
|
794
|
+
current_step=current_step,
|
|
795
|
+
error=error,
|
|
796
|
+
cancellable=cancellable,
|
|
797
|
+
metadata=metadata or {},
|
|
798
|
+
# 100% real event fields
|
|
799
|
+
event_id=act_id,
|
|
800
|
+
task_id=turn_id or "",
|
|
801
|
+
parent_event_id=parent_id or "",
|
|
802
|
+
event_type=event_type or f"{type}.{action}",
|
|
803
|
+
source=source or "cat.core",
|
|
804
|
+
duration=duration_ms / 1000.0 if duration_ms > 0 else 0.0,
|
|
805
|
+
operation_id=operation_id,
|
|
806
|
+
pid=pid,
|
|
807
|
+
lines_added=lines_added,
|
|
808
|
+
lines_removed=lines_removed,
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
with self._lock:
|
|
812
|
+
self._activities[act_id] = act
|
|
813
|
+
self._global_order.append(act_id)
|
|
814
|
+
if operation_id:
|
|
815
|
+
self._operation_map[operation_id] = act_id
|
|
816
|
+
if status in (STATUS_RUNNING, STATUS_PENDING, STATUS_STARTED, STATUS_QUEUED):
|
|
817
|
+
self._active_dedup_keys[dedup_key] = act_id
|
|
818
|
+
|
|
819
|
+
if turn_id:
|
|
820
|
+
self._turn_order.setdefault(turn_id, []).append(act_id)
|
|
821
|
+
# bound per-turn
|
|
822
|
+
if len(self._turn_order[turn_id]) > _MAX_PER_TURN:
|
|
823
|
+
oldest = self._turn_order[turn_id].pop(0)
|
|
824
|
+
self._activities.pop(oldest, None)
|
|
825
|
+
if oldest in self._global_order:
|
|
826
|
+
self._global_order.remove(oldest)
|
|
827
|
+
# bound total
|
|
828
|
+
if len(self._global_order) > _MAX_TOTAL:
|
|
829
|
+
oldest = self._global_order.pop(0)
|
|
830
|
+
self._activities.pop(oldest, None)
|
|
831
|
+
# also remove from turn index if there
|
|
832
|
+
for tid, lst in list(self._turn_order.items()):
|
|
833
|
+
if oldest in lst:
|
|
834
|
+
lst.remove(oldest)
|
|
835
|
+
if not lst:
|
|
836
|
+
self._turn_order.pop(tid, None)
|
|
837
|
+
break
|
|
838
|
+
|
|
839
|
+
self._notify(act, "created")
|
|
840
|
+
# also mirror to legacy event_stream for debugging/logging
|
|
841
|
+
try:
|
|
842
|
+
from . import event_stream as _es
|
|
843
|
+
_es.stream.emit("activity_created", source="activity", activity_id=act_id, title=title, status=status, turn_id=turn_id)
|
|
844
|
+
except Exception:
|
|
845
|
+
pass
|
|
846
|
+
return act
|
|
847
|
+
|
|
848
|
+
# ── update ─────────────────────────────────────────────────────────
|
|
849
|
+
def update(
|
|
850
|
+
self,
|
|
851
|
+
activity_id: str,
|
|
852
|
+
status: Optional[str] = None,
|
|
853
|
+
title: Optional[str] = None,
|
|
854
|
+
details: Optional[str] = None,
|
|
855
|
+
file: Optional[str] = None,
|
|
856
|
+
command: Optional[str] = None,
|
|
857
|
+
tool: Optional[str] = None,
|
|
858
|
+
result: Optional[str] = None,
|
|
859
|
+
duration_ms: Optional[int] = None,
|
|
860
|
+
expanded: Optional[bool] = None,
|
|
861
|
+
progress: Optional[float] = None,
|
|
862
|
+
# extended spec fields
|
|
863
|
+
category: Optional[str] = None,
|
|
864
|
+
phase: Optional[str] = None,
|
|
865
|
+
stdout: Optional[str] = None,
|
|
866
|
+
stderr: Optional[str] = None,
|
|
867
|
+
exit_code: Optional[int] = None,
|
|
868
|
+
target_path: Optional[str] = None,
|
|
869
|
+
line_count: Optional[int] = None,
|
|
870
|
+
diff_summary: Optional[str] = None,
|
|
871
|
+
query: Optional[str] = None,
|
|
872
|
+
match_count: Optional[int] = None,
|
|
873
|
+
permission_req: Optional[Dict[str, Any]] = None,
|
|
874
|
+
description: Optional[str] = None,
|
|
875
|
+
model: Optional[str] = None,
|
|
876
|
+
provider: Optional[str] = None,
|
|
877
|
+
workspace: Optional[str] = None,
|
|
878
|
+
current_step: Optional[str] = None,
|
|
879
|
+
error: Optional[str] = None,
|
|
880
|
+
cancellable: Optional[bool] = None,
|
|
881
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
882
|
+
# real event fields
|
|
883
|
+
event_type: Optional[str] = None,
|
|
884
|
+
source: Optional[str] = None,
|
|
885
|
+
duration: Optional[float] = None,
|
|
886
|
+
operation_id: Optional[str] = None,
|
|
887
|
+
pid: Optional[int] = None,
|
|
888
|
+
lines_added: Optional[int] = None,
|
|
889
|
+
lines_removed: Optional[int] = None,
|
|
890
|
+
) -> Optional[Activity]:
|
|
891
|
+
with self._lock:
|
|
892
|
+
act = self._activities.get(activity_id)
|
|
893
|
+
if act is None:
|
|
894
|
+
return None
|
|
895
|
+
if status is not None:
|
|
896
|
+
act.status = status
|
|
897
|
+
# compute duration on terminal states if not supplied
|
|
898
|
+
if status in (COMPLETED, FAILED, CANCELLED, WARNING, PAUSED, WAITING,
|
|
899
|
+
STATUS_COMPLETED, STATUS_FAILED, STATUS_CANCELLED, STATUS_WARNING):
|
|
900
|
+
if duration_ms is not None:
|
|
901
|
+
act.duration_ms = duration_ms
|
|
902
|
+
act.duration = duration_ms / 1000.0
|
|
903
|
+
elif duration is not None:
|
|
904
|
+
act.duration = duration
|
|
905
|
+
act.duration_ms = int(duration * 1000)
|
|
906
|
+
elif act.duration_ms == 0:
|
|
907
|
+
act.duration_ms = int((time.time() - act.start_time) * 1000)
|
|
908
|
+
act.duration = act.duration_ms / 1000.0
|
|
909
|
+
act.end_time = time.time()
|
|
910
|
+
if status in (FAILED, STATUS_FAILED) and error is None and act.error == "" and result:
|
|
911
|
+
# capture real error from result if not provided
|
|
912
|
+
act.error = result[:500]
|
|
913
|
+
|
|
914
|
+
# Release deduplication lock for terminal states
|
|
915
|
+
for k, aid in list(self._active_dedup_keys.items()):
|
|
916
|
+
if aid == activity_id:
|
|
917
|
+
self._active_dedup_keys.pop(k, None)
|
|
918
|
+
else:
|
|
919
|
+
act.end_time = None
|
|
920
|
+
if title is not None:
|
|
921
|
+
act.title = title
|
|
922
|
+
if details is not None:
|
|
923
|
+
act.details = details
|
|
924
|
+
act.description = details
|
|
925
|
+
if description is not None:
|
|
926
|
+
act.description = description
|
|
927
|
+
act.details = description
|
|
928
|
+
if file is not None:
|
|
929
|
+
act.file = file
|
|
930
|
+
if target_path is None:
|
|
931
|
+
act.target_path = file
|
|
932
|
+
if target_path is not None:
|
|
933
|
+
act.target_path = target_path
|
|
934
|
+
act.file = target_path
|
|
935
|
+
if command is not None:
|
|
936
|
+
act.command = command
|
|
937
|
+
if tool is not None:
|
|
938
|
+
act.tool = tool
|
|
939
|
+
if result is not None:
|
|
940
|
+
act.result = result
|
|
941
|
+
if category is not None:
|
|
942
|
+
act.category = category
|
|
943
|
+
if phase is not None:
|
|
944
|
+
act.phase = phase
|
|
945
|
+
if stdout is not None:
|
|
946
|
+
act.stdout = stdout
|
|
947
|
+
if stderr is not None:
|
|
948
|
+
act.stderr = stderr
|
|
949
|
+
if exit_code is not None:
|
|
950
|
+
act.exit_code = exit_code
|
|
951
|
+
if line_count is not None:
|
|
952
|
+
act.line_count = line_count
|
|
953
|
+
if diff_summary is not None:
|
|
954
|
+
act.diff_summary = diff_summary
|
|
955
|
+
if query is not None:
|
|
956
|
+
act.query = query
|
|
957
|
+
if match_count is not None:
|
|
958
|
+
act.match_count = match_count
|
|
959
|
+
if permission_req is not None:
|
|
960
|
+
act.permission_req = permission_req
|
|
961
|
+
if duration_ms is not None and status is None:
|
|
962
|
+
act.duration_ms = duration_ms
|
|
963
|
+
act.duration = duration_ms / 1000.0
|
|
964
|
+
if duration is not None and status is None:
|
|
965
|
+
act.duration = duration
|
|
966
|
+
act.duration_ms = int(duration * 1000)
|
|
967
|
+
if expanded is not None:
|
|
968
|
+
act.expanded = expanded
|
|
969
|
+
if progress is not None:
|
|
970
|
+
try:
|
|
971
|
+
v = float(progress)
|
|
972
|
+
act.progress = max(0.0, min(1.0, v))
|
|
973
|
+
except Exception:
|
|
974
|
+
pass
|
|
975
|
+
if model is not None:
|
|
976
|
+
act.model = model
|
|
977
|
+
if provider is not None:
|
|
978
|
+
act.provider = provider
|
|
979
|
+
if workspace is not None:
|
|
980
|
+
act.workspace = workspace
|
|
981
|
+
if current_step is not None:
|
|
982
|
+
act.current_step = current_step
|
|
983
|
+
if error is not None:
|
|
984
|
+
act.error = error
|
|
985
|
+
if cancellable is not None:
|
|
986
|
+
act.cancellable = cancellable
|
|
987
|
+
if metadata is not None:
|
|
988
|
+
act.metadata.update(metadata)
|
|
989
|
+
if event_type is not None:
|
|
990
|
+
act.event_type = event_type
|
|
991
|
+
if source is not None:
|
|
992
|
+
act.source = source
|
|
993
|
+
if operation_id is not None:
|
|
994
|
+
act.operation_id = operation_id
|
|
995
|
+
self._operation_map[operation_id] = activity_id
|
|
996
|
+
if pid is not None:
|
|
997
|
+
act.pid = pid
|
|
998
|
+
if lines_added is not None:
|
|
999
|
+
act.lines_added = lines_added
|
|
1000
|
+
if lines_removed is not None:
|
|
1001
|
+
act.lines_removed = lines_removed
|
|
1002
|
+
|
|
1003
|
+
act.timestamp = time.time()
|
|
1004
|
+
act.updated_time = act.timestamp
|
|
1005
|
+
|
|
1006
|
+
# throttling: if high-frequency updates for same id, coalesce within window
|
|
1007
|
+
now = time.time()
|
|
1008
|
+
last = self._throttle_last.get(activity_id, 0)
|
|
1009
|
+
if status in (COMPLETED, FAILED, CANCELLED, WARNING, WAITING, PAUSED,
|
|
1010
|
+
STATUS_COMPLETED, STATUS_FAILED, STATUS_CANCELLED, STATUS_WARNING) or (now - last) >= _THROTTLE_WINDOW:
|
|
1011
|
+
self._throttle_last[activity_id] = now
|
|
1012
|
+
self._pending_throttle.pop(activity_id, None)
|
|
1013
|
+
self._notify(act, "updated")
|
|
1014
|
+
else:
|
|
1015
|
+
# store pending and schedule a delayed flush after the throttle window
|
|
1016
|
+
self._pending_throttle[activity_id] = act
|
|
1017
|
+
if not getattr(self, '_flush_timer_active', False):
|
|
1018
|
+
self._flush_timer_active = True
|
|
1019
|
+
def _deferred_flush():
|
|
1020
|
+
self._flush_timer_active = False
|
|
1021
|
+
self.flush_throttled()
|
|
1022
|
+
timer = threading.Timer(_THROTTLE_WINDOW, _deferred_flush)
|
|
1023
|
+
timer.daemon = True
|
|
1024
|
+
timer.start()
|
|
1025
|
+
try:
|
|
1026
|
+
from . import event_stream as _es
|
|
1027
|
+
_es.stream.emit("activity_updated", source="activity", activity_id=activity_id, status=act.status, title=act.title)
|
|
1028
|
+
except Exception:
|
|
1029
|
+
pass
|
|
1030
|
+
return act
|
|
1031
|
+
|
|
1032
|
+
def check_stale_activities(self, stale_threshold_sec: float = 15.0) -> List[Activity]:
|
|
1033
|
+
"""
|
|
1034
|
+
Watchdog: detects running/pending activities with no heartbeat or update for
|
|
1035
|
+
longer than stale_threshold_sec. Marks them as WARNING to prevent stuck UI spinners.
|
|
1036
|
+
"""
|
|
1037
|
+
now = time.time()
|
|
1038
|
+
stale: List[Activity] = []
|
|
1039
|
+
with self._lock:
|
|
1040
|
+
for act in self._activities.values():
|
|
1041
|
+
if act.status in (STATUS_RUNNING, STATUS_PENDING, STATUS_STARTED, RUNNING, PENDING) and (now - act.updated_time) > stale_threshold_sec:
|
|
1042
|
+
stale.append(act)
|
|
1043
|
+
|
|
1044
|
+
for act in stale:
|
|
1045
|
+
self.update(
|
|
1046
|
+
act.id,
|
|
1047
|
+
status=STATUS_WARNING,
|
|
1048
|
+
result=act.result or f"Operation timed out ({stale_threshold_sec:.0f}s with no updates).",
|
|
1049
|
+
error=act.error or "Execution timed out / unresponsive",
|
|
1050
|
+
)
|
|
1051
|
+
return stale
|
|
1052
|
+
|
|
1053
|
+
def get_resource_sample(self) -> ResourceSample:
|
|
1054
|
+
"""Fetch latest actual system/process resource sample."""
|
|
1055
|
+
return self.resource_monitor.get_latest()
|
|
1056
|
+
|
|
1057
|
+
def get_session_summary(self, turn_id: str = "") -> Dict[str, Any]:
|
|
1058
|
+
"""
|
|
1059
|
+
Returns an authentic, non-simulated execution summary of all events for a turn or session.
|
|
1060
|
+
"""
|
|
1061
|
+
with self._lock:
|
|
1062
|
+
if turn_id:
|
|
1063
|
+
acts = self.get_for_turn(turn_id)
|
|
1064
|
+
else:
|
|
1065
|
+
acts = self.get_all()
|
|
1066
|
+
|
|
1067
|
+
sample = self.get_resource_sample()
|
|
1068
|
+
if not acts:
|
|
1069
|
+
return {
|
|
1070
|
+
"count": 0,
|
|
1071
|
+
"total_events": 0,
|
|
1072
|
+
"duration_ms": 0,
|
|
1073
|
+
"duration_str": "0s",
|
|
1074
|
+
"completed": 0,
|
|
1075
|
+
"failed": 0,
|
|
1076
|
+
"cancelled": 0,
|
|
1077
|
+
"warning": 0,
|
|
1078
|
+
"running": 0,
|
|
1079
|
+
"waiting": 0,
|
|
1080
|
+
"files_read": [],
|
|
1081
|
+
"files_modified": [],
|
|
1082
|
+
"commands_executed": [],
|
|
1083
|
+
"tools_used": [],
|
|
1084
|
+
"errors": [],
|
|
1085
|
+
"ram_used_gb": sample.system_ram_used_gb,
|
|
1086
|
+
"ram_total_gb": sample.system_ram_total_gb,
|
|
1087
|
+
"ram_percent": sample.system_ram_percent,
|
|
1088
|
+
"process_ram_mb": sample.process_ram_mb,
|
|
1089
|
+
"cpu_percent": sample.cpu_percent,
|
|
1090
|
+
"gpu_name": sample.gpu_name,
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
start_time = min(a.start_time for a in acts)
|
|
1094
|
+
end_time = max(a.updated_time for a in acts)
|
|
1095
|
+
total_ms = int((end_time - start_time) * 1000)
|
|
1096
|
+
|
|
1097
|
+
files_read = sorted(list({a.file or a.target_path for a in acts if a.category == CAT_FILE_READ and (a.file or a.target_path)}))
|
|
1098
|
+
files_mod = sorted(list({a.file or a.target_path for a in acts if a.category == CAT_EDIT and (a.file or a.target_path)}))
|
|
1099
|
+
commands = sorted(list({a.command for a in acts if a.command}))
|
|
1100
|
+
tools = sorted(list({a.tool for a in acts if a.tool}))
|
|
1101
|
+
errors = [a.error or a.result for a in acts if a.status in (STATUS_FAILED, STATUS_WARNING, FAILED, WARNING) and (a.error or a.result)]
|
|
1102
|
+
|
|
1103
|
+
return {
|
|
1104
|
+
"count": len(acts),
|
|
1105
|
+
"total_events": len(acts),
|
|
1106
|
+
"duration_ms": total_ms,
|
|
1107
|
+
"duration_str": _fmt_duration(total_ms),
|
|
1108
|
+
"start_time": start_time,
|
|
1109
|
+
"end_time": end_time,
|
|
1110
|
+
"completed": sum(1 for a in acts if a.status in (STATUS_COMPLETED, COMPLETED)),
|
|
1111
|
+
"failed": sum(1 for a in acts if a.status in (STATUS_FAILED, FAILED)),
|
|
1112
|
+
"cancelled": sum(1 for a in acts if a.status in (STATUS_CANCELLED, CANCELLED)),
|
|
1113
|
+
"warning": sum(1 for a in acts if a.status in (STATUS_WARNING, WARNING)),
|
|
1114
|
+
"running": sum(1 for a in acts if a.status in (STATUS_RUNNING, STATUS_PENDING, STATUS_STARTED, RUNNING, PENDING)),
|
|
1115
|
+
"waiting": sum(1 for a in acts if a.status in (STATUS_WAITING, STATUS_WAITING_PERMISSION, WAITING, "waiting")),
|
|
1116
|
+
"files_read": files_read,
|
|
1117
|
+
"files_modified": files_mod,
|
|
1118
|
+
"commands_executed": commands,
|
|
1119
|
+
"tools_used": tools,
|
|
1120
|
+
"errors": errors,
|
|
1121
|
+
"ram_used_gb": sample.system_ram_used_gb,
|
|
1122
|
+
"ram_total_gb": sample.system_ram_total_gb,
|
|
1123
|
+
"ram_percent": sample.system_ram_percent,
|
|
1124
|
+
"process_ram_mb": sample.process_ram_mb,
|
|
1125
|
+
"cpu_percent": sample.cpu_percent,
|
|
1126
|
+
"gpu_name": sample.gpu_name,
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
def append_output(self, activity_id: str, line: str, max_lines: int = 200) -> Optional[Activity]:
|
|
1130
|
+
with self._lock:
|
|
1131
|
+
act = self._activities.get(activity_id)
|
|
1132
|
+
if act is None:
|
|
1133
|
+
return None
|
|
1134
|
+
act.output_lines.append(line)
|
|
1135
|
+
if len(act.output_lines) > max_lines:
|
|
1136
|
+
# keep tail
|
|
1137
|
+
act.output_lines = act.output_lines[-max_lines:]
|
|
1138
|
+
act.timestamp = time.time()
|
|
1139
|
+
# throttled output: directly notify but UI should coalesce
|
|
1140
|
+
self._notify(act, "updated")
|
|
1141
|
+
return act
|
|
1142
|
+
|
|
1143
|
+
def get(self, activity_id: str) -> Optional[Activity]:
|
|
1144
|
+
with self._lock:
|
|
1145
|
+
return self._activities.get(activity_id)
|
|
1146
|
+
|
|
1147
|
+
def get_for_turn(self, turn_id: str, limit: Optional[int] = None) -> List[Activity]:
|
|
1148
|
+
with self._lock:
|
|
1149
|
+
ids = list(self._turn_order.get(turn_id, []))
|
|
1150
|
+
if limit is not None:
|
|
1151
|
+
ids = ids[-limit:]
|
|
1152
|
+
return [self._activities[i] for i in ids if i in self._activities]
|
|
1153
|
+
|
|
1154
|
+
def get_all(self, limit: Optional[int] = None) -> List[Activity]:
|
|
1155
|
+
with self._lock:
|
|
1156
|
+
ids = list(self._global_order)
|
|
1157
|
+
if limit is not None:
|
|
1158
|
+
ids = ids[-limit:]
|
|
1159
|
+
return [self._activities[i] for i in ids if i in self._activities]
|
|
1160
|
+
|
|
1161
|
+
def get_recent(self, limit: int = 50) -> List[Activity]:
|
|
1162
|
+
return self.get_all(limit=limit)
|
|
1163
|
+
|
|
1164
|
+
def turn_summary(self, turn_id: str) -> Dict[str, Any]:
|
|
1165
|
+
return self.get_session_summary(turn_id)
|
|
1166
|
+
|
|
1167
|
+
def cancel_turn(self, turn_id: str) -> int:
|
|
1168
|
+
"""Transition all running/pending activities for turn_id to CANCELLED."""
|
|
1169
|
+
count = 0
|
|
1170
|
+
with self._lock:
|
|
1171
|
+
ids = list(self._turn_order.get(turn_id, []))
|
|
1172
|
+
for aid in ids:
|
|
1173
|
+
act = self._activities.get(aid)
|
|
1174
|
+
if act and act.status in (PENDING, RUNNING, PAUSED, STATUS_RUNNING, STATUS_PENDING, STATUS_STARTED):
|
|
1175
|
+
act.status = CANCELLED
|
|
1176
|
+
act.result = act.result or "Cancelled"
|
|
1177
|
+
act.duration_ms = int((time.time() - act.start_time) * 1000)
|
|
1178
|
+
act.duration = act.duration_ms / 1000.0
|
|
1179
|
+
act.timestamp = time.time()
|
|
1180
|
+
count += 1
|
|
1181
|
+
if count:
|
|
1182
|
+
for aid in self._turn_order.get(turn_id, []):
|
|
1183
|
+
act = self._activities.get(aid)
|
|
1184
|
+
if act and act.status in (CANCELLED, STATUS_CANCELLED):
|
|
1185
|
+
self._notify(act, "updated")
|
|
1186
|
+
return count
|
|
1187
|
+
|
|
1188
|
+
def clear_turn(self, turn_id: str) -> int:
|
|
1189
|
+
with self._lock:
|
|
1190
|
+
ids = self._turn_order.pop(turn_id, [])
|
|
1191
|
+
for aid in ids:
|
|
1192
|
+
self._activities.pop(aid, None)
|
|
1193
|
+
if aid in self._global_order:
|
|
1194
|
+
self._global_order.remove(aid)
|
|
1195
|
+
return len(ids)
|
|
1196
|
+
|
|
1197
|
+
def clear_all(self) -> None:
|
|
1198
|
+
with self._lock:
|
|
1199
|
+
self._activities.clear()
|
|
1200
|
+
self._turn_order.clear()
|
|
1201
|
+
self._global_order.clear()
|
|
1202
|
+
self._throttle_last.clear()
|
|
1203
|
+
self._pending_throttle.clear()
|
|
1204
|
+
self._operation_map.clear()
|
|
1205
|
+
self._active_dedup_keys.clear()
|
|
1206
|
+
|
|
1207
|
+
def flush_throttled(self) -> None:
|
|
1208
|
+
pending = list(self._pending_throttle.values())
|
|
1209
|
+
self._pending_throttle.clear()
|
|
1210
|
+
for act in pending:
|
|
1211
|
+
self._notify(act, "updated")
|
|
1212
|
+
self._throttle_last[act.id] = time.time()
|
|
1213
|
+
|
|
1214
|
+
# ── global singleton ───────────────────────────────────────────────────
|
|
1215
|
+
manager = ActivityManager()
|
|
1216
|
+
|
|
1217
|
+
# ── convenience helpers ────────────────────────────────────────────────
|
|
1218
|
+
def create_activity(type: str, action: str, title: str, **kwargs) -> Activity:
|
|
1219
|
+
return manager.create(type=type, action=action, title=title, **kwargs)
|
|
1220
|
+
|
|
1221
|
+
def update_activity(activity_id: str, **kwargs) -> Optional[Activity]:
|
|
1222
|
+
return manager.update(activity_id, **kwargs)
|
|
1223
|
+
|
|
1224
|
+
def append_output(activity_id: str, line: str) -> Optional[Activity]:
|
|
1225
|
+
return manager.append_output(activity_id, line)
|
|
1226
|
+
|
|
1227
|
+
def get_for_turn(turn_id: str) -> List[Activity]:
|
|
1228
|
+
return manager.get_for_turn(turn_id)
|
|
1229
|
+
|
|
1230
|
+
# ── context manager for scoped activities ──────────────────────────────
|
|
1231
|
+
class ActivitySpan:
|
|
1232
|
+
"""
|
|
1233
|
+
Context manager that creates a RUNNING activity on entry and
|
|
1234
|
+
completes/fails it on exit. Use when the real operation is bracketed:
|
|
1235
|
+
|
|
1236
|
+
with ActivitySpan(type=TYPE_FILE, action="read", title="Reading x", turn_id=tid) as act:
|
|
1237
|
+
... do real IO ...
|
|
1238
|
+
act.details = "2,184 lines"
|
|
1239
|
+
On exception, marks FAILED automatically.
|
|
1240
|
+
"""
|
|
1241
|
+
|
|
1242
|
+
def __init__(self, type: str, action: str, title: str, turn_id: str = "", **kwargs):
|
|
1243
|
+
self.type = type
|
|
1244
|
+
self.action = action
|
|
1245
|
+
self.title = title
|
|
1246
|
+
self.turn_id = turn_id
|
|
1247
|
+
self.kwargs = kwargs
|
|
1248
|
+
self.activity: Optional[Activity] = None
|
|
1249
|
+
|
|
1250
|
+
def __enter__(self) -> Activity:
|
|
1251
|
+
self.activity = manager.create(
|
|
1252
|
+
type=self.type, action=self.action, title=self.title, status=RUNNING, turn_id=self.turn_id, **self.kwargs
|
|
1253
|
+
)
|
|
1254
|
+
return self.activity
|
|
1255
|
+
|
|
1256
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
1257
|
+
if self.activity is None:
|
|
1258
|
+
return False
|
|
1259
|
+
if exc_type is not None:
|
|
1260
|
+
manager.update(self.activity.id, status=FAILED, result=str(exc_val)[:400] if exc_val else "Failed")
|
|
1261
|
+
else:
|
|
1262
|
+
# only auto-complete if still running/pending
|
|
1263
|
+
if self.activity.status in (RUNNING, PENDING):
|
|
1264
|
+
dur = int((time.time() - self.activity.start_time) * 1000)
|
|
1265
|
+
manager.update(self.activity.id, status=COMPLETED, duration_ms=dur)
|
|
1266
|
+
return False
|
|
1267
|
+
|
|
1268
|
+
# ── human helpers: map tool name -> activity meta ──────────────────────
|
|
1269
|
+
class ToolMeta(tuple):
|
|
1270
|
+
def __new__(cls, category: str, action: str, title: str, phase: str = PHASE_IMPLEMENTATION, type: str = ""):
|
|
1271
|
+
t = type or category.lower()
|
|
1272
|
+
return super().__new__(cls, (t, action, title))
|
|
1273
|
+
|
|
1274
|
+
def __init__(self, category: str, action: str, title: str, phase: str = PHASE_IMPLEMENTATION, type: str = ""):
|
|
1275
|
+
self.category = category
|
|
1276
|
+
self.action = action
|
|
1277
|
+
self.title = title
|
|
1278
|
+
self.phase = phase
|
|
1279
|
+
self.type = type or category.lower()
|
|
1280
|
+
|
|
1281
|
+
TOOL_ACTIVITY_META: Dict[str, ToolMeta] = {
|
|
1282
|
+
# search / discovery
|
|
1283
|
+
"search_workspace": ToolMeta(CAT_SEARCH, "search", "Searching workspace", PHASE_DISCOVERY, TYPE_SEARCH),
|
|
1284
|
+
"inspect_project": ToolMeta(CAT_ANALYSIS, "inspect", "Inspecting project", PHASE_DISCOVERY, TYPE_WORKSPACE),
|
|
1285
|
+
"list_directory": ToolMeta(CAT_SEARCH, "list", "Listing directory", PHASE_DISCOVERY, TYPE_FILE),
|
|
1286
|
+
"read_file": ToolMeta(CAT_FILE_READ, "read", "Reading file", PHASE_DISCOVERY, TYPE_FILE),
|
|
1287
|
+
"read_attachment": ToolMeta(CAT_FILE_READ, "read", "Reading attachment", PHASE_DISCOVERY, TYPE_FILE),
|
|
1288
|
+
# file mutation
|
|
1289
|
+
"write_file": ToolMeta(CAT_EDIT, "write", "Writing file", PHASE_IMPLEMENTATION, TYPE_FILE),
|
|
1290
|
+
"create_folder": ToolMeta(CAT_EDIT, "create", "Creating folder", PHASE_IMPLEMENTATION, TYPE_FILE),
|
|
1291
|
+
"delete_file": ToolMeta(CAT_EDIT, "delete", "Deleting file", PHASE_IMPLEMENTATION, TYPE_FILE),
|
|
1292
|
+
"rename_file": ToolMeta(CAT_EDIT, "rename", "Renaming file", PHASE_IMPLEMENTATION, TYPE_FILE),
|
|
1293
|
+
"edit_file": ToolMeta(CAT_EDIT, "edit", "Editing file", PHASE_IMPLEMENTATION, TYPE_FILE),
|
|
1294
|
+
"archive_list": ToolMeta(CAT_FILE_READ, "archive", "Listing archive", PHASE_DISCOVERY, TYPE_FILE),
|
|
1295
|
+
"archive_delete_entries": ToolMeta(CAT_EDIT, "archive", "Updating archive", PHASE_IMPLEMENTATION, TYPE_FILE),
|
|
1296
|
+
"archive_extract": ToolMeta(CAT_EDIT, "archive", "Extracting archive", PHASE_IMPLEMENTATION, TYPE_FILE),
|
|
1297
|
+
"archive_add_entries": ToolMeta(CAT_EDIT, "archive", "Adding to archive", PHASE_IMPLEMENTATION, TYPE_FILE),
|
|
1298
|
+
"archive_repack": ToolMeta(CAT_EDIT, "archive", "Repacking archive", PHASE_IMPLEMENTATION, TYPE_FILE),
|
|
1299
|
+
"archive_validate": ToolMeta(CAT_BUILD, "archive", "Validating archive", PHASE_VALIDATION, TYPE_FILE),
|
|
1300
|
+
# execution
|
|
1301
|
+
"run_terminal": ToolMeta(CAT_TERMINAL, "run", "Running command", PHASE_IMPLEMENTATION, TYPE_COMMAND),
|
|
1302
|
+
"run_build": ToolMeta(CAT_BUILD, "build", "Running build", PHASE_VALIDATION, TYPE_COMMAND),
|
|
1303
|
+
"run_tests": ToolMeta(CAT_TEST, "test", "Running tests", PHASE_VALIDATION, TYPE_COMMAND),
|
|
1304
|
+
"install_packages": ToolMeta(CAT_TERMINAL, "install", "Installing packages", PHASE_IMPLEMENTATION, TYPE_COMMAND),
|
|
1305
|
+
"device_action": ToolMeta(CAT_TERMINAL, "device", "Device action", PHASE_IMPLEMENTATION, TYPE_COMMAND),
|
|
1306
|
+
# chemistry / compute
|
|
1307
|
+
"calculate": ToolMeta(CAT_ANALYSIS, "calculate", "Calculating", PHASE_ANALYSIS, TYPE_TOOL),
|
|
1308
|
+
"solve_formula": ToolMeta(CAT_ANALYSIS, "solve", "Solving formula", PHASE_ANALYSIS, TYPE_TOOL),
|
|
1309
|
+
"solve_custom": ToolMeta(CAT_ANALYSIS, "solve", "Solving custom formula", PHASE_ANALYSIS, TYPE_TOOL),
|
|
1310
|
+
"list_formulas": ToolMeta(CAT_ANALYSIS, "list", "Listing formulas", PHASE_ANALYSIS, TYPE_TOOL),
|
|
1311
|
+
"generate_numerical": ToolMeta(CAT_ANALYSIS, "generate", "Generating numerical", PHASE_ANALYSIS, TYPE_TOOL),
|
|
1312
|
+
"plot_preset": ToolMeta(CAT_ANALYSIS, "plot", "Plotting preset", PHASE_ANALYSIS, TYPE_TOOL),
|
|
1313
|
+
"plot_function": ToolMeta(CAT_ANALYSIS, "plot", "Plotting function", PHASE_ANALYSIS, TYPE_TOOL),
|
|
1314
|
+
"plot_surface": ToolMeta(CAT_ANALYSIS, "plot", "Exporting surface", PHASE_ANALYSIS, TYPE_TOOL),
|
|
1315
|
+
"simulate_atom_2d": ToolMeta(CAT_ANALYSIS, "simulate", "Simulating atom (2D)", PHASE_ANALYSIS, TYPE_TOOL),
|
|
1316
|
+
"simulate_atom_3d": ToolMeta(CAT_ANALYSIS, "simulate", "Simulating atom (3D)", PHASE_ANALYSIS, TYPE_TOOL),
|
|
1317
|
+
"simulate_orbital": ToolMeta(CAT_ANALYSIS, "simulate", "Simulating orbital", PHASE_ANALYSIS, TYPE_TOOL),
|
|
1318
|
+
"orbital_grid": ToolMeta(CAT_ANALYSIS, "simulate", "Exporting orbital grid", PHASE_ANALYSIS, TYPE_TOOL),
|
|
1319
|
+
"bonding_map": ToolMeta(CAT_ANALYSIS, "simulate", "Rendering bonding map", PHASE_ANALYSIS, TYPE_TOOL),
|
|
1320
|
+
"bloch_sphere": ToolMeta(CAT_ANALYSIS, "simulate", "Rendering Bloch sphere", PHASE_ANALYSIS, TYPE_TOOL),
|
|
1321
|
+
# web
|
|
1322
|
+
"web_search": ToolMeta(CAT_SEARCH, "web", "Searching web", PHASE_DISCOVERY, TYPE_TOOL),
|
|
1323
|
+
"deep_research": ToolMeta(CAT_SEARCH, "web", "Researching", PHASE_DISCOVERY, TYPE_TOOL),
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
def meta_for_tool(tool_name: str) -> ToolMeta:
|
|
1327
|
+
return TOOL_ACTIVITY_META.get(
|
|
1328
|
+
tool_name,
|
|
1329
|
+
ToolMeta(CAT_TERMINAL, tool_name, f"Running {tool_name}", PHASE_IMPLEMENTATION, TYPE_TOOL)
|
|
1330
|
+
)
|
|
1331
|
+
|
|
1332
|
+
# ── provider / model activity helper ───────────────────────────────────
|
|
1333
|
+
def emit_provider_activity(title: str, status: str = RUNNING, turn_id: str = "", details: str = "", result: str = "") -> Activity:
|
|
1334
|
+
return manager.create(type=TYPE_PROVIDER, action="provider", title=title, status=status, turn_id=turn_id, details=details, result=result)
|