nucleus-mcp 1.0.7 → 1.2.0
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.
- package/package.json +2 -4
- package/.github/workflows/ci.yml +0 -72
- package/.smithery/shttp/module.js +0 -3
- package/.smithery/shttp/module.js.map +0 -7
- package/.smithery/stdio/index.cjs +0 -74
- package/.smithery/stdio/index.cjs.map +0 -7
- package/CHANGELOG.md +0 -65
- package/CODE_OF_CONDUCT.md +0 -34
- package/COMPARISON.md +0 -20
- package/CONTRIBUTING.md +0 -137
- package/DEVELOPMENT.md +0 -109
- package/LICENSE +0 -21
- package/README.md +0 -319
- package/ROADMAP.md +0 -24
- package/SECURITY.md +0 -88
- package/docs/COMMUNITY_FAQ.md +0 -48
- package/docs/COMPETITIVE_ANALYSIS.md +0 -90
- package/docs/DEMO_SCRIPT.md +0 -86
- package/docs/ENTERPRISE.md +0 -169
- package/docs/FAQ.md +0 -228
- package/docs/QUICK_START.md +0 -155
- package/docs/README.md +0 -67
- package/docs/RECURSIVE_SNAP_DEMO.gif +0 -0
- package/docs/social-preview-hq.png +0 -0
- package/docs/social-preview.png +0 -0
- package/examples/basic_usage.py +0 -84
- package/package.json.real +0 -24
- package/pyproject.toml +0 -50
- package/registry-server.json +0 -20
- package/scripts/demo_governance.py +0 -109
- package/scripts/sse_bridge.py +0 -87
- package/scripts/verify_windows_paths.py +0 -48
- package/server.json +0 -20
- package/src/mcp_server_nucleus/__init__.py +0 -8583
- package/src/mcp_server_nucleus/__main__.py +0 -9
- package/src/mcp_server_nucleus/cli.py +0 -1225
- package/src/mcp_server_nucleus/commitment_core.py +0 -146
- package/src/mcp_server_nucleus/commitment_ledger.py +0 -841
- package/src/mcp_server_nucleus/core/__init__.py +0 -6
- package/src/mcp_server_nucleus/core/orchestrator.py +0 -9
- package/src/mcp_server_nucleus/core/tool_registration_impl.py +0 -99
- package/src/mcp_server_nucleus/daemon/safety/lock.py +0 -121
- package/src/mcp_server_nucleus/deployment/render_integration.py +0 -4
- package/src/mcp_server_nucleus/hypervisor/__init__.py +0 -0
- package/src/mcp_server_nucleus/hypervisor/injector.py +0 -83
- package/src/mcp_server_nucleus/hypervisor/locker.py +0 -130
- package/src/mcp_server_nucleus/hypervisor/watchdog.py +0 -71
- package/src/mcp_server_nucleus/runtime/__init__.py +0 -58
- package/src/mcp_server_nucleus/runtime/agent.py +0 -517
- package/src/mcp_server_nucleus/runtime/agent_pool.py +0 -797
- package/src/mcp_server_nucleus/runtime/agents/base.py +0 -34
- package/src/mcp_server_nucleus/runtime/agents/oracle/manifest.json +0 -28
- package/src/mcp_server_nucleus/runtime/auth.py +0 -98
- package/src/mcp_server_nucleus/runtime/autopilot.py +0 -1057
- package/src/mcp_server_nucleus/runtime/broker.py +0 -126
- package/src/mcp_server_nucleus/runtime/budget.py +0 -153
- package/src/mcp_server_nucleus/runtime/capabilities/base.py +0 -20
- package/src/mcp_server_nucleus/runtime/capabilities/brain_ops.py +0 -253
- package/src/mcp_server_nucleus/runtime/capabilities/code_ops.py +0 -192
- package/src/mcp_server_nucleus/runtime/capabilities/depth_tracker.py +0 -81
- package/src/mcp_server_nucleus/runtime/capabilities/feature_map.py +0 -274
- package/src/mcp_server_nucleus/runtime/capabilities/marketing_engine.py +0 -205
- package/src/mcp_server_nucleus/runtime/capabilities/memory_ops.py +0 -79
- package/src/mcp_server_nucleus/runtime/capabilities/proof_system.py +0 -127
- package/src/mcp_server_nucleus/runtime/capabilities/render_ops.py +0 -80
- package/src/mcp_server_nucleus/runtime/capabilities/render_poller_cap.py +0 -266
- package/src/mcp_server_nucleus/runtime/capabilities/self_healing.py +0 -110
- package/src/mcp_server_nucleus/runtime/capabilities/strategy.py +0 -138
- package/src/mcp_server_nucleus/runtime/capabilities/synthesizer.py +0 -134
- package/src/mcp_server_nucleus/runtime/capabilities/web_ops.py +0 -94
- package/src/mcp_server_nucleus/runtime/common.py +0 -134
- package/src/mcp_server_nucleus/runtime/context_manager.py +0 -342
- package/src/mcp_server_nucleus/runtime/context_rules.json +0 -86
- package/src/mcp_server_nucleus/runtime/crdt_task_store.py +0 -349
- package/src/mcp_server_nucleus/runtime/critic.py +0 -150
- package/src/mcp_server_nucleus/runtime/daemon.py +0 -142
- package/src/mcp_server_nucleus/runtime/dashboard.py +0 -1087
- package/src/mcp_server_nucleus/runtime/depth_ops.py +0 -339
- package/src/mcp_server_nucleus/runtime/event_ops.py +0 -103
- package/src/mcp_server_nucleus/runtime/event_stream.py +0 -213
- package/src/mcp_server_nucleus/runtime/export.py +0 -64
- package/src/mcp_server_nucleus/runtime/factory.py +0 -594
- package/src/mcp_server_nucleus/runtime/federation.py +0 -1040
- package/src/mcp_server_nucleus/runtime/fetcher.py +0 -92
- package/src/mcp_server_nucleus/runtime/file_monitor.py +0 -208
- package/src/mcp_server_nucleus/runtime/firestore_bridge.py +0 -91
- package/src/mcp_server_nucleus/runtime/gcloud_ops.py +0 -186
- package/src/mcp_server_nucleus/runtime/hooks.py +0 -246
- package/src/mcp_server_nucleus/runtime/identity/gatekeeper.py +0 -106
- package/src/mcp_server_nucleus/runtime/identity/keygen.py +0 -147
- package/src/mcp_server_nucleus/runtime/identity/manifest.py +0 -62
- package/src/mcp_server_nucleus/runtime/identity/trust.py +0 -27
- package/src/mcp_server_nucleus/runtime/inspector.py +0 -74
- package/src/mcp_server_nucleus/runtime/installer.py +0 -81
- package/src/mcp_server_nucleus/runtime/ipc_auth.py +0 -377
- package/src/mcp_server_nucleus/runtime/lifecycle.py +0 -129
- package/src/mcp_server_nucleus/runtime/llm_client.py +0 -408
- package/src/mcp_server_nucleus/runtime/llm_tiers.json +0 -88
- package/src/mcp_server_nucleus/runtime/locking.py +0 -228
- package/src/mcp_server_nucleus/runtime/loops/fixer.py +0 -110
- package/src/mcp_server_nucleus/runtime/memory.py +0 -158
- package/src/mcp_server_nucleus/runtime/mounter.py +0 -210
- package/src/mcp_server_nucleus/runtime/mounter_ops.py +0 -250
- package/src/mcp_server_nucleus/runtime/nuke_protocol.py +0 -147
- package/src/mcp_server_nucleus/runtime/orchestrator.py +0 -370
- package/src/mcp_server_nucleus/runtime/orchestrator_unified.py +0 -263
- package/src/mcp_server_nucleus/runtime/orchestrator_v3.py +0 -617
- package/src/mcp_server_nucleus/runtime/plugin_loader.py +0 -84
- package/src/mcp_server_nucleus/runtime/policy.py +0 -117
- package/src/mcp_server_nucleus/runtime/profiling.py +0 -219
- package/src/mcp_server_nucleus/runtime/prometheus.py +0 -286
- package/src/mcp_server_nucleus/runtime/proposals.py +0 -101
- package/src/mcp_server_nucleus/runtime/publisher.py +0 -109
- package/src/mcp_server_nucleus/runtime/pulse.py +0 -516
- package/src/mcp_server_nucleus/runtime/registry.py +0 -82
- package/src/mcp_server_nucleus/runtime/render_ops.py +0 -83
- package/src/mcp_server_nucleus/runtime/schema_gen.py +0 -140
- package/src/mcp_server_nucleus/runtime/session_ops.py +0 -224
- package/src/mcp_server_nucleus/runtime/stdio_server.py +0 -633
- package/src/mcp_server_nucleus/runtime/storage.py +0 -83
- package/src/mcp_server_nucleus/runtime/strategy.py +0 -91
- package/src/mcp_server_nucleus/runtime/swarm.py +0 -170
- package/src/mcp_server_nucleus/runtime/sync_ops.py +0 -878
- package/src/mcp_server_nucleus/runtime/task_ingestion.py +0 -1020
- package/src/mcp_server_nucleus/runtime/task_ops.py +0 -449
- package/src/mcp_server_nucleus/runtime/task_scheduler.py +0 -488
- package/src/mcp_server_nucleus/runtime/team.py +0 -59
- package/src/mcp_server_nucleus/runtime/triggers.py +0 -237
- package/src/mcp_server_nucleus/runtime/vector_store.py +0 -125
- package/src/mcp_server_nucleus/runtime/watcher.py +0 -112
- package/src/mcp_server_nucleus/tool_tiers.py +0 -243
- package/tests/__init__.py +0 -1
- package/tests/conftest.py +0 -42
- package/tests/test_core.py +0 -235
- package/tests/test_search.py +0 -69
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
PluginLoader v2: The Airlock.
|
|
3
|
-
Strictly controls how tools enter the Nucleus Runtime.
|
|
4
|
-
|
|
5
|
-
Strategic Role:
|
|
6
|
-
- Enforces "Air Gap": Only loads tools explicitly allowed by Manifest.
|
|
7
|
-
- Enforces "Governance": Wraps all tools in BudgetGuard.
|
|
8
|
-
- Isolates Agents: Scopes loading to specific Agent Directory.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import sys
|
|
12
|
-
import importlib.util
|
|
13
|
-
import logging
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import List
|
|
16
|
-
from .capabilities.base import Capability
|
|
17
|
-
from .budget import BudgetAuditor, BudgetGuard
|
|
18
|
-
|
|
19
|
-
logger = logging.getLogger("nucleus.plugins")
|
|
20
|
-
|
|
21
|
-
class PluginLoader:
|
|
22
|
-
def __init__(self, brain_path: Path, auditor: BudgetAuditor):
|
|
23
|
-
self.brain_path = brain_path
|
|
24
|
-
self.auditor = auditor
|
|
25
|
-
self.installed_tools_dir = brain_path / "tools" / "installed"
|
|
26
|
-
|
|
27
|
-
def load_agent_tools(self, agent_id: str, authorized_modules: List[str]) -> List[Capability]:
|
|
28
|
-
"""
|
|
29
|
-
Loads allowed tools for a specific agent.
|
|
30
|
-
Wraps them in BudgetGuard(default=$0.00).
|
|
31
|
-
"""
|
|
32
|
-
agent_dir = self.installed_tools_dir / agent_id
|
|
33
|
-
|
|
34
|
-
if not agent_dir.exists():
|
|
35
|
-
logger.warning(f"⚠️ Agent directory not found: {agent_dir}")
|
|
36
|
-
return []
|
|
37
|
-
|
|
38
|
-
logger.info(f"🔌 Accessing Agent Airlock: {agent_id}")
|
|
39
|
-
|
|
40
|
-
# Ensure the directory is in python path
|
|
41
|
-
sys.path.append(str(agent_dir))
|
|
42
|
-
|
|
43
|
-
loaded_plugins = []
|
|
44
|
-
|
|
45
|
-
for module_name in authorized_modules:
|
|
46
|
-
# Construct expected file path
|
|
47
|
-
tool_path = agent_dir / f"{module_name}.py"
|
|
48
|
-
|
|
49
|
-
if not tool_path.exists():
|
|
50
|
-
logger.error(f"❌ Missing authorized tool: {module_name} in {agent_dir}")
|
|
51
|
-
continue
|
|
52
|
-
|
|
53
|
-
try:
|
|
54
|
-
# Import Module
|
|
55
|
-
spec = importlib.util.spec_from_file_location(module_name, tool_path)
|
|
56
|
-
if spec and spec.loader:
|
|
57
|
-
module = importlib.util.module_from_spec(spec)
|
|
58
|
-
spec.loader.exec_module(module)
|
|
59
|
-
|
|
60
|
-
if hasattr(module, "get_capability"):
|
|
61
|
-
cap = module.get_capability()
|
|
62
|
-
if isinstance(cap, Capability):
|
|
63
|
-
|
|
64
|
-
# --- GOVERNANCE WRAPPER ---
|
|
65
|
-
# By default, tools have $0.00 budget until Policy grants more.
|
|
66
|
-
# We wrap immediately to ensure safety.
|
|
67
|
-
guarded_cap = BudgetGuard(
|
|
68
|
-
inner=cap,
|
|
69
|
-
auditor=self.auditor,
|
|
70
|
-
agent_id=agent_id,
|
|
71
|
-
max_budget_usd=0.0 # Default Deny
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
loaded_plugins.append(guarded_cap)
|
|
75
|
-
logger.info(f" ✅ Loaded & Secured: {cap.name}")
|
|
76
|
-
else:
|
|
77
|
-
logger.warning(f" ⚠️ Invalid Capability in {module_name}")
|
|
78
|
-
else:
|
|
79
|
-
logger.warning(f" ℹ️ Missing entry point in {module_name}")
|
|
80
|
-
|
|
81
|
-
except Exception as e:
|
|
82
|
-
logger.error(f" ❌ Failed to load {module_name}: {e}")
|
|
83
|
-
|
|
84
|
-
return loaded_plugins
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
"""
|
|
3
|
-
Policy Engine for Nucleus Daemon.
|
|
4
|
-
Handles 'Adaptive Policy' (Directives) and 'Bounded Autonomy' (Mission Parameters).
|
|
5
|
-
|
|
6
|
-
Strategic Role:
|
|
7
|
-
- Reads `directives.md` to align Daemon with user intent.
|
|
8
|
-
- Enforces `MissionParameters` (Budget, Time, Safety) to prevent "Strategic Drift".
|
|
9
|
-
- Provides the `SafetyLevel` context for "Unshackled" operations.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from enum import Enum
|
|
13
|
-
from dataclasses import dataclass, field
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import List, Dict
|
|
16
|
-
import json
|
|
17
|
-
import logging
|
|
18
|
-
|
|
19
|
-
logger = logging.getLogger(__name__)
|
|
20
|
-
|
|
21
|
-
class SafetyLevel(Enum):
|
|
22
|
-
SAFE = "safe" # Standard SaaS-like filters
|
|
23
|
-
MODERATE = "moderate" # Less restrictive, warns on high risk
|
|
24
|
-
RAW = "raw" # "Unshackled" - No middleware constraints (User liability)
|
|
25
|
-
|
|
26
|
-
class ExecutionMode(Enum):
|
|
27
|
-
PLANNING = "planning"
|
|
28
|
-
EXECUTION = "execution"
|
|
29
|
-
VERIFICATION = "verification"
|
|
30
|
-
|
|
31
|
-
@dataclass
|
|
32
|
-
class MissionParameters:
|
|
33
|
-
"""The Compact: Bounded Autonomy Agreement"""
|
|
34
|
-
max_budget_usd: float = 1.0
|
|
35
|
-
max_steps: int = 10
|
|
36
|
-
timeout_seconds: int = 600
|
|
37
|
-
allowed_tools: List[str] = field(default_factory=list)
|
|
38
|
-
blocked_tools: List[str] = field(default_factory=list)
|
|
39
|
-
safety_level: SafetyLevel = SafetyLevel.SAFE
|
|
40
|
-
# If true, requires explicit human ratification before "Write" actions
|
|
41
|
-
require_ratification: bool = False
|
|
42
|
-
|
|
43
|
-
class DirectivesLoader:
|
|
44
|
-
def __init__(self, brain_path: Path):
|
|
45
|
-
self.brain_path = brain_path
|
|
46
|
-
self.directives_path = brain_path / "ledger" / "directives.md"
|
|
47
|
-
# Support json config as alternative
|
|
48
|
-
self.config_path = brain_path / "ledger" / "policy.json"
|
|
49
|
-
|
|
50
|
-
def load_policy(self) -> Dict:
|
|
51
|
-
"""Load the current policy directives"""
|
|
52
|
-
policy = {
|
|
53
|
-
"mode": "default",
|
|
54
|
-
"priorities": [],
|
|
55
|
-
"constraints": [],
|
|
56
|
-
"safety_level": SafetyLevel.SAFE.value
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
# 1. Try Config JSON first (Structured)
|
|
60
|
-
if self.config_path.exists():
|
|
61
|
-
try:
|
|
62
|
-
with open(self.config_path, 'r') as f:
|
|
63
|
-
data = json.load(f)
|
|
64
|
-
if data:
|
|
65
|
-
policy.update(data)
|
|
66
|
-
except Exception as e:
|
|
67
|
-
logger.error(f"Failed to load policy.json: {e}")
|
|
68
|
-
|
|
69
|
-
# 2. Parse Markdown Directives (Context)
|
|
70
|
-
# For now, we just verify it exists to be available for LLM context injection
|
|
71
|
-
# In Phase 60, we might NLP parse strict rules from MD.
|
|
72
|
-
policy["directives_text"] = self._read_directives_text()
|
|
73
|
-
|
|
74
|
-
return policy
|
|
75
|
-
|
|
76
|
-
def _read_directives_text(self) -> str:
|
|
77
|
-
if self.directives_path.exists():
|
|
78
|
-
return self.directives_path.read_text()
|
|
79
|
-
return "No directives found. Operate in default safe mode."
|
|
80
|
-
|
|
81
|
-
def get_mission_parameters(self, task_type: str) -> MissionParameters:
|
|
82
|
-
"""Factory for Mission Parameters based on Task Type"""
|
|
83
|
-
policy = self.load_policy()
|
|
84
|
-
safety_val = policy.get("safety_level", "safe")
|
|
85
|
-
|
|
86
|
-
try:
|
|
87
|
-
safety = SafetyLevel(safety_val)
|
|
88
|
-
except ValueError:
|
|
89
|
-
safety = SafetyLevel.SAFE
|
|
90
|
-
|
|
91
|
-
# Default Profiles
|
|
92
|
-
if task_type == "quick_check":
|
|
93
|
-
return MissionParameters(
|
|
94
|
-
max_budget_usd=0.05,
|
|
95
|
-
max_steps=5,
|
|
96
|
-
timeout_seconds=60,
|
|
97
|
-
safety_level=safety
|
|
98
|
-
)
|
|
99
|
-
elif task_type == "deep_research":
|
|
100
|
-
return MissionParameters(
|
|
101
|
-
max_budget_usd=2.0,
|
|
102
|
-
max_steps=30,
|
|
103
|
-
timeout_seconds=1800,
|
|
104
|
-
safety_level=safety,
|
|
105
|
-
blocked_tools=["write_to_file", "run_command"] # Read only
|
|
106
|
-
)
|
|
107
|
-
elif task_type == "autonomous_fix":
|
|
108
|
-
return MissionParameters(
|
|
109
|
-
max_budget_usd=0.5,
|
|
110
|
-
max_steps=15,
|
|
111
|
-
timeout_seconds=300,
|
|
112
|
-
safety_level=safety,
|
|
113
|
-
require_ratification=True # Fixes usually need approval in V1
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
# Default
|
|
117
|
-
return MissionParameters(safety_level=safety)
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Nucleus Runtime - Performance Profiling (AG-014)
|
|
3
|
-
=================================================
|
|
4
|
-
Timing decorators and performance monitoring for identifying bottlenecks.
|
|
5
|
-
|
|
6
|
-
Usage:
|
|
7
|
-
from .profiling import timed, get_metrics, reset_metrics
|
|
8
|
-
|
|
9
|
-
@timed("task_list")
|
|
10
|
-
def _list_tasks(...):
|
|
11
|
-
...
|
|
12
|
-
|
|
13
|
-
# Get metrics
|
|
14
|
-
metrics = get_metrics()
|
|
15
|
-
print(metrics["task_list"]) # {"calls": 10, "total_ms": 150.5, "avg_ms": 15.05}
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
import time
|
|
19
|
-
import functools
|
|
20
|
-
import threading
|
|
21
|
-
from typing import Dict, Any, Callable, Optional
|
|
22
|
-
from datetime import datetime, timezone
|
|
23
|
-
import json
|
|
24
|
-
import os
|
|
25
|
-
from pathlib import Path
|
|
26
|
-
|
|
27
|
-
# Thread-safe metrics storage
|
|
28
|
-
_metrics_lock = threading.Lock()
|
|
29
|
-
_metrics: Dict[str, Dict[str, Any]] = {}
|
|
30
|
-
|
|
31
|
-
# Configuration
|
|
32
|
-
PROFILING_ENABLED = os.environ.get("NUCLEUS_PROFILING", "false").lower() == "true"
|
|
33
|
-
SLOW_THRESHOLD_MS = float(os.environ.get("NUCLEUS_SLOW_THRESHOLD_MS", "100"))
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def timed(operation_name: str, log_slow: bool = True):
|
|
37
|
-
"""
|
|
38
|
-
Decorator to time function execution and collect metrics.
|
|
39
|
-
|
|
40
|
-
Args:
|
|
41
|
-
operation_name: Name for this operation in metrics
|
|
42
|
-
log_slow: Whether to log warning for slow operations
|
|
43
|
-
|
|
44
|
-
Example:
|
|
45
|
-
@timed("brain_list_tasks")
|
|
46
|
-
def _list_tasks():
|
|
47
|
-
...
|
|
48
|
-
"""
|
|
49
|
-
def decorator(func: Callable) -> Callable:
|
|
50
|
-
@functools.wraps(func)
|
|
51
|
-
def wrapper(*args, **kwargs):
|
|
52
|
-
if not PROFILING_ENABLED:
|
|
53
|
-
return func(*args, **kwargs)
|
|
54
|
-
|
|
55
|
-
start = time.perf_counter()
|
|
56
|
-
try:
|
|
57
|
-
result = func(*args, **kwargs)
|
|
58
|
-
return result
|
|
59
|
-
finally:
|
|
60
|
-
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
61
|
-
_record_metric(operation_name, elapsed_ms, log_slow)
|
|
62
|
-
|
|
63
|
-
@functools.wraps(func)
|
|
64
|
-
async def async_wrapper(*args, **kwargs):
|
|
65
|
-
if not PROFILING_ENABLED:
|
|
66
|
-
return await func(*args, **kwargs)
|
|
67
|
-
|
|
68
|
-
start = time.perf_counter()
|
|
69
|
-
try:
|
|
70
|
-
result = await func(*args, **kwargs)
|
|
71
|
-
return result
|
|
72
|
-
finally:
|
|
73
|
-
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
74
|
-
_record_metric(operation_name, elapsed_ms, log_slow)
|
|
75
|
-
|
|
76
|
-
# Return appropriate wrapper based on function type
|
|
77
|
-
if asyncio_iscoroutinefunction(func):
|
|
78
|
-
return async_wrapper
|
|
79
|
-
return wrapper
|
|
80
|
-
return decorator
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def asyncio_iscoroutinefunction(func):
|
|
84
|
-
"""Check if function is async."""
|
|
85
|
-
import inspect
|
|
86
|
-
return inspect.iscoroutinefunction(func)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def _record_metric(operation_name: str, elapsed_ms: float, log_slow: bool):
|
|
90
|
-
"""Record a timing metric (thread-safe)."""
|
|
91
|
-
with _metrics_lock:
|
|
92
|
-
if operation_name not in _metrics:
|
|
93
|
-
_metrics[operation_name] = {
|
|
94
|
-
"calls": 0,
|
|
95
|
-
"total_ms": 0.0,
|
|
96
|
-
"min_ms": float('inf'),
|
|
97
|
-
"max_ms": 0.0,
|
|
98
|
-
"last_call": None,
|
|
99
|
-
"slow_count": 0
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
m = _metrics[operation_name]
|
|
103
|
-
m["calls"] += 1
|
|
104
|
-
m["total_ms"] += elapsed_ms
|
|
105
|
-
m["min_ms"] = min(m["min_ms"], elapsed_ms)
|
|
106
|
-
m["max_ms"] = max(m["max_ms"], elapsed_ms)
|
|
107
|
-
m["last_call"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
108
|
-
|
|
109
|
-
if elapsed_ms > SLOW_THRESHOLD_MS:
|
|
110
|
-
m["slow_count"] += 1
|
|
111
|
-
if log_slow:
|
|
112
|
-
# Use print for now, can integrate with structured logging
|
|
113
|
-
print(f"⚠️ SLOW: {operation_name} took {elapsed_ms:.2f}ms (threshold: {SLOW_THRESHOLD_MS}ms)")
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def get_metrics() -> Dict[str, Dict[str, Any]]:
|
|
117
|
-
"""
|
|
118
|
-
Get current performance metrics.
|
|
119
|
-
|
|
120
|
-
Returns:
|
|
121
|
-
Dict with metrics per operation:
|
|
122
|
-
{
|
|
123
|
-
"operation_name": {
|
|
124
|
-
"calls": int,
|
|
125
|
-
"total_ms": float,
|
|
126
|
-
"avg_ms": float,
|
|
127
|
-
"min_ms": float,
|
|
128
|
-
"max_ms": float,
|
|
129
|
-
"slow_count": int,
|
|
130
|
-
"last_call": str (ISO timestamp)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
"""
|
|
134
|
-
with _metrics_lock:
|
|
135
|
-
result = {}
|
|
136
|
-
for name, m in _metrics.items():
|
|
137
|
-
result[name] = {
|
|
138
|
-
**m,
|
|
139
|
-
"avg_ms": m["total_ms"] / m["calls"] if m["calls"] > 0 else 0
|
|
140
|
-
}
|
|
141
|
-
# Clean up inf for JSON serialization
|
|
142
|
-
if result[name]["min_ms"] == float('inf'):
|
|
143
|
-
result[name]["min_ms"] = 0
|
|
144
|
-
return result
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def reset_metrics():
|
|
148
|
-
"""Reset all metrics (useful for testing)."""
|
|
149
|
-
global _metrics
|
|
150
|
-
with _metrics_lock:
|
|
151
|
-
_metrics = {}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def get_metrics_summary() -> str:
|
|
155
|
-
"""Get a formatted summary of metrics."""
|
|
156
|
-
metrics = get_metrics()
|
|
157
|
-
if not metrics:
|
|
158
|
-
return "No metrics collected. Set NUCLEUS_PROFILING=true to enable."
|
|
159
|
-
|
|
160
|
-
lines = ["# Performance Metrics", ""]
|
|
161
|
-
lines.append("| Operation | Calls | Avg (ms) | Min | Max | Slow |")
|
|
162
|
-
lines.append("|-----------|-------|----------|-----|-----|------|")
|
|
163
|
-
|
|
164
|
-
for name, m in sorted(metrics.items(), key=lambda x: -x[1]["total_ms"]):
|
|
165
|
-
lines.append(
|
|
166
|
-
f"| {name} | {m['calls']} | {m['avg_ms']:.2f} | "
|
|
167
|
-
f"{m['min_ms']:.2f} | {m['max_ms']:.2f} | {m['slow_count']} |"
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
lines.append("")
|
|
171
|
-
lines.append(f"*Slow threshold: {SLOW_THRESHOLD_MS}ms*")
|
|
172
|
-
return "\n".join(lines)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def export_metrics_to_file(brain_path: Optional[Path] = None) -> str:
|
|
176
|
-
"""
|
|
177
|
-
Export metrics to a JSON file in the brain folder.
|
|
178
|
-
|
|
179
|
-
Args:
|
|
180
|
-
brain_path: Path to brain folder (uses env var if not provided)
|
|
181
|
-
|
|
182
|
-
Returns:
|
|
183
|
-
Path to exported file
|
|
184
|
-
"""
|
|
185
|
-
if brain_path is None:
|
|
186
|
-
brain_path = Path(os.environ.get("NUCLEAR_BRAIN_PATH", ".brain"))
|
|
187
|
-
|
|
188
|
-
metrics_dir = brain_path / "metrics"
|
|
189
|
-
metrics_dir.mkdir(parents=True, exist_ok=True)
|
|
190
|
-
|
|
191
|
-
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
|
192
|
-
filepath = metrics_dir / f"perf_{timestamp}.json"
|
|
193
|
-
|
|
194
|
-
data = {
|
|
195
|
-
"exported_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
196
|
-
"threshold_ms": SLOW_THRESHOLD_MS,
|
|
197
|
-
"metrics": get_metrics()
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
with open(filepath, 'w') as f:
|
|
201
|
-
json.dump(data, f, indent=2)
|
|
202
|
-
|
|
203
|
-
return str(filepath)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
# Convenience decorators for common operations
|
|
207
|
-
def timed_io(func):
|
|
208
|
-
"""Decorator for I/O-bound operations (file reads, etc.)."""
|
|
209
|
-
return timed(f"io.{func.__name__}", log_slow=True)(func)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def timed_compute(func):
|
|
213
|
-
"""Decorator for compute-bound operations."""
|
|
214
|
-
return timed(f"compute.{func.__name__}", log_slow=True)(func)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def timed_llm(func):
|
|
218
|
-
"""Decorator for LLM API calls (expected to be slow)."""
|
|
219
|
-
return timed(f"llm.{func.__name__}", log_slow=False)(func)
|