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,617 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
NOP V3.1 Unified Orchestrator
|
|
3
|
-
|
|
4
|
-
Converges Track A (Pure Python Core) with Track B (MCP Integration):
|
|
5
|
-
- CRDTTaskStore: 15K+ writes/sec, LWW + vector clocks
|
|
6
|
-
- TaskScheduler: 423K tasks/sec, intelligent routing
|
|
7
|
-
- V3.1 Features: Reset cycles, checkpoints, context summaries
|
|
8
|
-
|
|
9
|
-
This is the CORE orchestration engine that MCP tools delegate to.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import json
|
|
13
|
-
import time
|
|
14
|
-
import os
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from typing import Dict, List, Optional, Any
|
|
17
|
-
from datetime import datetime
|
|
18
|
-
from dataclasses import dataclass, field
|
|
19
|
-
|
|
20
|
-
from .crdt_task_store import CRDTTaskStore
|
|
21
|
-
from .task_scheduler import TaskScheduler
|
|
22
|
-
|
|
23
|
-
# V3.1 Components - lazy loaded to avoid circular imports
|
|
24
|
-
_agent_pool = None
|
|
25
|
-
_ingestion_engine = None
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _get_agent_pool():
|
|
29
|
-
"""Lazy load AgentPool to avoid circular imports."""
|
|
30
|
-
global _agent_pool
|
|
31
|
-
if _agent_pool is None:
|
|
32
|
-
try:
|
|
33
|
-
from .agent_pool import AgentPool
|
|
34
|
-
_agent_pool = AgentPool()
|
|
35
|
-
except ImportError:
|
|
36
|
-
pass
|
|
37
|
-
return _agent_pool
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _get_ingestion_engine(brain_path=None):
|
|
41
|
-
"""Lazy load TaskIngestionEngine."""
|
|
42
|
-
global _ingestion_engine
|
|
43
|
-
if _ingestion_engine is None:
|
|
44
|
-
try:
|
|
45
|
-
from .task_ingestion import TaskIngestionEngine
|
|
46
|
-
_ingestion_engine = TaskIngestionEngine(brain_path=brain_path)
|
|
47
|
-
except ImportError:
|
|
48
|
-
pass
|
|
49
|
-
return _ingestion_engine
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def get_brain_path() -> Path:
|
|
53
|
-
"""Get brain path from environment or default."""
|
|
54
|
-
return Path(os.getenv("NUCLEAR_BRAIN_PATH", "/Users/lokeshgarg/ai-mvp-backend/.brain"))
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@dataclass
|
|
58
|
-
class ResetCycleInfo:
|
|
59
|
-
"""V3.1: Reset cycle configuration for model-specific limits."""
|
|
60
|
-
hours: Optional[int] = None # None = unlimited (Opus 4.5)
|
|
61
|
-
last_reset_at: Optional[str] = None
|
|
62
|
-
next_reset_at: Optional[str] = None
|
|
63
|
-
warning_threshold_minutes: int = 30
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@dataclass
|
|
67
|
-
class Checkpoint:
|
|
68
|
-
"""V3.1: Checkpoint for long-running tasks."""
|
|
69
|
-
enabled: bool = False
|
|
70
|
-
last_checkpoint_at: Optional[str] = None
|
|
71
|
-
data: Dict[str, Any] = field(default_factory=dict)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
@dataclass
|
|
75
|
-
class ContextSummary:
|
|
76
|
-
"""V3.1: Context summary for handoffs."""
|
|
77
|
-
generated_at: Optional[str] = None
|
|
78
|
-
summary: str = ""
|
|
79
|
-
key_decisions: List[str] = field(default_factory=list)
|
|
80
|
-
handoff_notes: str = ""
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
class NucleusOrchestratorV3:
|
|
84
|
-
"""
|
|
85
|
-
Unified orchestration engine for NOP V3.1.
|
|
86
|
-
|
|
87
|
-
Combines:
|
|
88
|
-
- CRDTTaskStore (conflict-free task storage)
|
|
89
|
-
- TaskScheduler (intelligent task assignment)
|
|
90
|
-
- V3.1 features (checkpoints, context summaries, reset cycles)
|
|
91
|
-
|
|
92
|
-
Provides:
|
|
93
|
-
- All internal _impl functions for MCP tools
|
|
94
|
-
- Backward compatibility with V3.0 tools
|
|
95
|
-
- High-performance operations from Track A
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
def __init__(self, brain_path: Optional[Path] = None):
|
|
99
|
-
self.brain_path = brain_path or get_brain_path()
|
|
100
|
-
self.replica_id = f"nucleus_{int(time.time())}"
|
|
101
|
-
|
|
102
|
-
# Initialize Track A components
|
|
103
|
-
self.task_store = CRDTTaskStore(replica_id=self.replica_id)
|
|
104
|
-
self.scheduler = TaskScheduler(max_agents=1000)
|
|
105
|
-
|
|
106
|
-
# Load existing state from JSON files (backward compat)
|
|
107
|
-
self._load_from_legacy_json()
|
|
108
|
-
|
|
109
|
-
def _load_from_legacy_json(self):
|
|
110
|
-
"""Load existing tasks from V3.0/V3.1 JSON files."""
|
|
111
|
-
tasks_path = self.brain_path / "ledger" / "tasks.json"
|
|
112
|
-
if tasks_path.exists():
|
|
113
|
-
try:
|
|
114
|
-
with open(tasks_path) as f:
|
|
115
|
-
data = json.load(f)
|
|
116
|
-
|
|
117
|
-
for task in data.get("tasks", []):
|
|
118
|
-
# Map to CRDT format
|
|
119
|
-
crdt_task = {
|
|
120
|
-
"id": task.get("id"),
|
|
121
|
-
"title": task.get("description", task.get("title", "")),
|
|
122
|
-
"status": task.get("status", "PENDING"),
|
|
123
|
-
"tier": task.get("required_tier", "T2_CODE"),
|
|
124
|
-
"priority": task.get("priority", 3),
|
|
125
|
-
"claimed_by": task.get("claimed_by"),
|
|
126
|
-
"blocked_by": task.get("blocked_by", []),
|
|
127
|
-
# V3.1 fields
|
|
128
|
-
"checkpoint": task.get("checkpoint"),
|
|
129
|
-
"context_summary": task.get("context_summary"),
|
|
130
|
-
"dependency_metadata": task.get("dependency_metadata", {})
|
|
131
|
-
}
|
|
132
|
-
self.task_store.add_task(crdt_task)
|
|
133
|
-
except Exception as e:
|
|
134
|
-
print(f"Warning: Failed to load legacy tasks: {e}")
|
|
135
|
-
|
|
136
|
-
def _save_to_legacy_json(self):
|
|
137
|
-
"""Save CRDT state back to legacy JSON format."""
|
|
138
|
-
tasks_path = self.brain_path / "ledger" / "tasks.json"
|
|
139
|
-
|
|
140
|
-
# Load existing to preserve metadata
|
|
141
|
-
existing = {"tasks": [], "metadata": {"version": "3.1"}}
|
|
142
|
-
if tasks_path.exists():
|
|
143
|
-
with open(tasks_path) as f:
|
|
144
|
-
existing = json.load(f)
|
|
145
|
-
|
|
146
|
-
# Convert CRDT tasks to legacy format
|
|
147
|
-
crdt_tasks = self.task_store.get_all_tasks()
|
|
148
|
-
legacy_tasks = []
|
|
149
|
-
|
|
150
|
-
for task in crdt_tasks:
|
|
151
|
-
legacy_task = {
|
|
152
|
-
"id": task["id"],
|
|
153
|
-
"description": task.get("title", ""),
|
|
154
|
-
"status": task.get("status", "PENDING"),
|
|
155
|
-
"priority": task.get("priority", 3),
|
|
156
|
-
"required_tier": task.get("tier", "standard"),
|
|
157
|
-
"claimed_by": task.get("claimed_by"),
|
|
158
|
-
"blocked_by": task.get("blocked_by", []),
|
|
159
|
-
"checkpoint": task.get("checkpoint"),
|
|
160
|
-
"context_summary": task.get("context_summary"),
|
|
161
|
-
"dependency_metadata": task.get("dependency_metadata", {})
|
|
162
|
-
}
|
|
163
|
-
legacy_tasks.append(legacy_task)
|
|
164
|
-
|
|
165
|
-
existing["tasks"] = legacy_tasks
|
|
166
|
-
existing["metadata"]["last_synced"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
|
|
167
|
-
|
|
168
|
-
with open(tasks_path, "w") as f:
|
|
169
|
-
json.dump(existing, f, indent=2)
|
|
170
|
-
|
|
171
|
-
# =========================================================================
|
|
172
|
-
# CORE OPERATIONS
|
|
173
|
-
# =========================================================================
|
|
174
|
-
|
|
175
|
-
def get_task(self, task_id: str) -> Optional[Dict]:
|
|
176
|
-
"""Get a single task by ID."""
|
|
177
|
-
return self.task_store.get_task(task_id)
|
|
178
|
-
|
|
179
|
-
def get_all_tasks(self, status: Optional[str] = None) -> List[Dict]:
|
|
180
|
-
"""Get all tasks, optionally filtered by status."""
|
|
181
|
-
tasks = self.task_store.get_all_tasks()
|
|
182
|
-
if status:
|
|
183
|
-
tasks = [t for t in tasks if t.get("status") == status]
|
|
184
|
-
return tasks
|
|
185
|
-
|
|
186
|
-
def claim_task(self, task_id: str, agent_id: str) -> Dict:
|
|
187
|
-
"""Atomic task claiming with conflict detection."""
|
|
188
|
-
task = self.task_store.get_task(task_id)
|
|
189
|
-
if not task:
|
|
190
|
-
return {"success": False, "error": f"Task {task_id} not found"}
|
|
191
|
-
|
|
192
|
-
if task.get("claimed_by") and task["claimed_by"] != agent_id:
|
|
193
|
-
return {
|
|
194
|
-
"success": False,
|
|
195
|
-
"error": f"Task already claimed by {task['claimed_by']}"
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
# Update task with claim
|
|
199
|
-
task["claimed_by"] = agent_id
|
|
200
|
-
task["status"] = "IN_PROGRESS"
|
|
201
|
-
task["claimed_at"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
|
|
202
|
-
|
|
203
|
-
self.task_store.update_task(task_id, task)
|
|
204
|
-
self._save_to_legacy_json()
|
|
205
|
-
|
|
206
|
-
return {"success": True, "task": task}
|
|
207
|
-
|
|
208
|
-
def complete_task(self, task_id: str, agent_id: str, outcome: str = "success") -> Dict:
|
|
209
|
-
"""Mark task as complete."""
|
|
210
|
-
task = self.task_store.get_task(task_id)
|
|
211
|
-
if not task:
|
|
212
|
-
return {"success": False, "error": f"Task {task_id} not found"}
|
|
213
|
-
|
|
214
|
-
task["status"] = "DONE" if outcome == "success" else "FAILED"
|
|
215
|
-
task["completed_at"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
|
|
216
|
-
task["completed_by"] = agent_id
|
|
217
|
-
|
|
218
|
-
self.task_store.update_task(task_id, task)
|
|
219
|
-
self._save_to_legacy_json()
|
|
220
|
-
|
|
221
|
-
return {"success": True, "task": task}
|
|
222
|
-
|
|
223
|
-
def add_task(self, description: str, priority: int = 3,
|
|
224
|
-
tier: str = "T2_CODE", blocked_by: List[str] = None,
|
|
225
|
-
source: str = "user", required_skills: List[str] = None) -> Dict:
|
|
226
|
-
"""Create new task via orchestrator."""
|
|
227
|
-
task_id = f"task_{int(time.time() * 1000)}"
|
|
228
|
-
task = {
|
|
229
|
-
"id": task_id,
|
|
230
|
-
"title": description,
|
|
231
|
-
"description": description,
|
|
232
|
-
"status": "PENDING",
|
|
233
|
-
"tier": tier,
|
|
234
|
-
"priority": priority,
|
|
235
|
-
"blocked_by": blocked_by or [],
|
|
236
|
-
"required_skills": required_skills or [],
|
|
237
|
-
"source": source,
|
|
238
|
-
"created_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
239
|
-
# V3.1 fields
|
|
240
|
-
"checkpoint": None,
|
|
241
|
-
"context_summary": None,
|
|
242
|
-
"dependency_metadata": {"depth": 0, "blocks": []}
|
|
243
|
-
}
|
|
244
|
-
self.task_store.add_task(task)
|
|
245
|
-
self._save_to_legacy_json()
|
|
246
|
-
return {"success": True, "task": task}
|
|
247
|
-
|
|
248
|
-
def update_task(self, task_id: str, updates: Dict) -> Dict:
|
|
249
|
-
"""Update task fields."""
|
|
250
|
-
task = self.task_store.get_task(task_id)
|
|
251
|
-
if not task:
|
|
252
|
-
return {"success": False, "error": f"Task {task_id} not found"}
|
|
253
|
-
|
|
254
|
-
# Apply updates (never update ID)
|
|
255
|
-
for key, value in updates.items():
|
|
256
|
-
if key != "id":
|
|
257
|
-
task[key] = value
|
|
258
|
-
task["updated_at"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
|
|
259
|
-
|
|
260
|
-
self.task_store.update_task(task_id, task)
|
|
261
|
-
self._save_to_legacy_json()
|
|
262
|
-
return {"success": True, "task": task}
|
|
263
|
-
|
|
264
|
-
def get_next_task(self, skills: List[str] = None,
|
|
265
|
-
agent_tier: str = None) -> Optional[Dict]:
|
|
266
|
-
"""Get highest priority unblocked task."""
|
|
267
|
-
tasks = self.task_store.get_all_tasks()
|
|
268
|
-
|
|
269
|
-
# Filter to pending/ready tasks
|
|
270
|
-
available = [t for t in tasks if t.get("status") in ["PENDING", "READY"]]
|
|
271
|
-
|
|
272
|
-
# Filter out blocked tasks
|
|
273
|
-
done_ids = {t["id"] for t in tasks if t.get("status") == "DONE"}
|
|
274
|
-
unblocked = []
|
|
275
|
-
for t in available:
|
|
276
|
-
deps = t.get("blocked_by", []) + t.get("depends_on", [])
|
|
277
|
-
if all(d in done_ids for d in deps):
|
|
278
|
-
unblocked.append(t)
|
|
279
|
-
|
|
280
|
-
if not unblocked:
|
|
281
|
-
return None
|
|
282
|
-
|
|
283
|
-
# Sort by priority (lower = higher priority)
|
|
284
|
-
unblocked.sort(key=lambda x: x.get("priority", 3))
|
|
285
|
-
return unblocked[0]
|
|
286
|
-
|
|
287
|
-
def resume_from_checkpoint(self, task_id: str) -> Dict:
|
|
288
|
-
"""Get checkpoint data for task resumption."""
|
|
289
|
-
task = self.task_store.get_task(task_id)
|
|
290
|
-
if not task:
|
|
291
|
-
return {"success": False, "error": f"Task {task_id} not found"}
|
|
292
|
-
|
|
293
|
-
checkpoint = task.get("checkpoint")
|
|
294
|
-
if not checkpoint:
|
|
295
|
-
return {"success": False, "error": "No checkpoint found for task"}
|
|
296
|
-
|
|
297
|
-
return {
|
|
298
|
-
"success": True,
|
|
299
|
-
"task_id": task_id,
|
|
300
|
-
"checkpoint": checkpoint,
|
|
301
|
-
"context_summary": task.get("context_summary"),
|
|
302
|
-
"resume_instructions": f"Resume from step {checkpoint.get('data', {}).get('step', 'unknown')}"
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
def escalate_task(self, task_id: str, reason: str) -> Dict:
|
|
306
|
-
"""Mark task as needing human intervention."""
|
|
307
|
-
task = self.task_store.get_task(task_id)
|
|
308
|
-
if not task:
|
|
309
|
-
return {"success": False, "error": f"Task {task_id} not found"}
|
|
310
|
-
|
|
311
|
-
task["status"] = "ESCALATED"
|
|
312
|
-
task["escalation"] = {
|
|
313
|
-
"reason": reason,
|
|
314
|
-
"escalated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
315
|
-
"resolved": False
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
self.task_store.update_task(task_id, task)
|
|
319
|
-
self._save_to_legacy_json()
|
|
320
|
-
return {"success": True, "task": task}
|
|
321
|
-
|
|
322
|
-
# =========================================================================
|
|
323
|
-
# V3.1 FEATURES
|
|
324
|
-
# =========================================================================
|
|
325
|
-
|
|
326
|
-
def checkpoint_task(self, task_id: str, checkpoint_data: Dict) -> Dict:
|
|
327
|
-
"""Save checkpoint for long-running task."""
|
|
328
|
-
task = self.task_store.get_task(task_id)
|
|
329
|
-
if not task:
|
|
330
|
-
return {"success": False, "error": f"Task {task_id} not found"}
|
|
331
|
-
|
|
332
|
-
task["checkpoint"] = {
|
|
333
|
-
"enabled": True,
|
|
334
|
-
"last_checkpoint_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
335
|
-
"data": checkpoint_data
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
self.task_store.update_task(task_id, task)
|
|
339
|
-
self._save_to_legacy_json()
|
|
340
|
-
|
|
341
|
-
return {"success": True, "checkpoint": task["checkpoint"]}
|
|
342
|
-
|
|
343
|
-
def generate_context_summary(self, task_id: str, summary: str,
|
|
344
|
-
key_decisions: List[str] = None,
|
|
345
|
-
handoff_notes: str = "") -> Dict:
|
|
346
|
-
"""Generate handoff context summary for a task."""
|
|
347
|
-
task = self.task_store.get_task(task_id)
|
|
348
|
-
if not task:
|
|
349
|
-
return {"success": False, "error": f"Task {task_id} not found"}
|
|
350
|
-
|
|
351
|
-
task["context_summary"] = {
|
|
352
|
-
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
353
|
-
"summary": summary,
|
|
354
|
-
"key_decisions": key_decisions or [],
|
|
355
|
-
"handoff_notes": handoff_notes
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
self.task_store.update_task(task_id, task)
|
|
359
|
-
self._save_to_legacy_json()
|
|
360
|
-
|
|
361
|
-
return {"success": True, "context_summary": task["context_summary"]}
|
|
362
|
-
|
|
363
|
-
def check_reset_warnings(self) -> List[Dict]:
|
|
364
|
-
"""Check all slots for approaching resets."""
|
|
365
|
-
warnings = []
|
|
366
|
-
registry_path = self.brain_path / "slots" / "registry.json"
|
|
367
|
-
|
|
368
|
-
if not registry_path.exists():
|
|
369
|
-
return warnings
|
|
370
|
-
|
|
371
|
-
with open(registry_path) as f:
|
|
372
|
-
registry = json.load(f)
|
|
373
|
-
|
|
374
|
-
now = datetime.now()
|
|
375
|
-
|
|
376
|
-
for slot_id, slot in registry.get("slots", {}).items():
|
|
377
|
-
reset_cycle = slot.get("reset_cycle")
|
|
378
|
-
if not reset_cycle or not reset_cycle.get("next_reset_at"):
|
|
379
|
-
continue
|
|
380
|
-
|
|
381
|
-
try:
|
|
382
|
-
next_reset = datetime.fromisoformat(reset_cycle["next_reset_at"].replace("Z", "+00:00"))
|
|
383
|
-
time_remaining = (next_reset - now).total_seconds() / 60
|
|
384
|
-
threshold = reset_cycle.get("warning_threshold_minutes", 30)
|
|
385
|
-
|
|
386
|
-
if time_remaining <= threshold and time_remaining > 0:
|
|
387
|
-
warnings.append({
|
|
388
|
-
"slot_id": slot_id,
|
|
389
|
-
"minutes_remaining": int(time_remaining),
|
|
390
|
-
"next_reset_at": reset_cycle["next_reset_at"],
|
|
391
|
-
"action": "PREPARE_HANDOFF"
|
|
392
|
-
})
|
|
393
|
-
except (ValueError, TypeError):
|
|
394
|
-
continue
|
|
395
|
-
|
|
396
|
-
return warnings
|
|
397
|
-
|
|
398
|
-
def record_exhaustion(self, slot_id: str, reason: str,
|
|
399
|
-
tasks_affected: List[str] = None) -> Dict:
|
|
400
|
-
"""Record slot exhaustion event."""
|
|
401
|
-
registry_path = self.brain_path / "slots" / "registry.json"
|
|
402
|
-
|
|
403
|
-
if not registry_path.exists():
|
|
404
|
-
return {"success": False, "error": "Registry not found"}
|
|
405
|
-
|
|
406
|
-
with open(registry_path) as f:
|
|
407
|
-
registry = json.load(f)
|
|
408
|
-
|
|
409
|
-
slot = registry.get("slots", {}).get(slot_id)
|
|
410
|
-
if not slot:
|
|
411
|
-
return {"success": False, "error": f"Slot {slot_id} not found"}
|
|
412
|
-
|
|
413
|
-
# Initialize exhaustion_history if needed
|
|
414
|
-
if "exhaustion_history" not in slot:
|
|
415
|
-
slot["exhaustion_history"] = []
|
|
416
|
-
|
|
417
|
-
# Add exhaustion event
|
|
418
|
-
event = {
|
|
419
|
-
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
420
|
-
"reason": reason,
|
|
421
|
-
"tasks_affected": tasks_affected or [],
|
|
422
|
-
"recovery_time_seconds": 300 if reason == "rate_limit_hit" else 18000
|
|
423
|
-
}
|
|
424
|
-
slot["exhaustion_history"].append(event)
|
|
425
|
-
|
|
426
|
-
# Keep only last 10 events
|
|
427
|
-
slot["exhaustion_history"] = slot["exhaustion_history"][-10:]
|
|
428
|
-
|
|
429
|
-
registry["slots"][slot_id] = slot
|
|
430
|
-
|
|
431
|
-
with open(registry_path, "w") as f:
|
|
432
|
-
json.dump(registry, f, indent=2)
|
|
433
|
-
|
|
434
|
-
return {"success": True, "event": event}
|
|
435
|
-
|
|
436
|
-
# =========================================================================
|
|
437
|
-
# AGENT POOL OPERATIONS (V3.1)
|
|
438
|
-
# =========================================================================
|
|
439
|
-
|
|
440
|
-
def get_agent_pool(self):
|
|
441
|
-
"""Get the AgentPool instance."""
|
|
442
|
-
return _get_agent_pool()
|
|
443
|
-
|
|
444
|
-
def spawn_agent(self, model: str, tier: str = "T2_CODE",
|
|
445
|
-
alias: str = None) -> Dict:
|
|
446
|
-
"""Spawn a new agent in the pool."""
|
|
447
|
-
pool = _get_agent_pool()
|
|
448
|
-
if pool is None:
|
|
449
|
-
return {"success": False, "error": "AgentPool not available"}
|
|
450
|
-
return pool.spawn_agent(model=model, tier=tier, alias=alias)
|
|
451
|
-
|
|
452
|
-
def get_agent(self, agent_id: str) -> Optional[Dict]:
|
|
453
|
-
"""Get agent by ID."""
|
|
454
|
-
pool = _get_agent_pool()
|
|
455
|
-
if pool is None:
|
|
456
|
-
return None
|
|
457
|
-
return pool.get_agent(agent_id)
|
|
458
|
-
|
|
459
|
-
def mark_agent_exhausted(self, agent_id: str, reason: str = "rate_limit") -> Dict:
|
|
460
|
-
"""Mark an agent as exhausted."""
|
|
461
|
-
pool = _get_agent_pool()
|
|
462
|
-
if pool is None:
|
|
463
|
-
return {"success": False, "error": "AgentPool not available"}
|
|
464
|
-
return pool.exhaust_agent(agent_id, reason=reason)
|
|
465
|
-
|
|
466
|
-
def respawn_agent(self, agent_id: str) -> Dict:
|
|
467
|
-
"""Respawn an exhausted agent."""
|
|
468
|
-
pool = _get_agent_pool()
|
|
469
|
-
if pool is None:
|
|
470
|
-
return {"success": False, "error": "AgentPool not available"}
|
|
471
|
-
return pool.respawn_agent(agent_id)
|
|
472
|
-
|
|
473
|
-
def get_available_agent(self, tier: str = None) -> Optional[str]:
|
|
474
|
-
"""Get an available agent for the given tier."""
|
|
475
|
-
pool = _get_agent_pool()
|
|
476
|
-
if pool is None:
|
|
477
|
-
return None
|
|
478
|
-
return pool.get_available_agent(tier=tier)
|
|
479
|
-
|
|
480
|
-
def assign_task_to_agent(self, task_id: str, agent_id: str = None,
|
|
481
|
-
tier: str = None) -> Dict:
|
|
482
|
-
"""Assign a task to an agent (or find best available)."""
|
|
483
|
-
pool = _get_agent_pool()
|
|
484
|
-
if pool is None:
|
|
485
|
-
return {"success": False, "error": "AgentPool not available"}
|
|
486
|
-
|
|
487
|
-
if agent_id is None and tier:
|
|
488
|
-
agent_id = pool.get_available_agent(tier=tier)
|
|
489
|
-
if agent_id is None:
|
|
490
|
-
return {"success": False, "error": f"No available agent for tier {tier}"}
|
|
491
|
-
|
|
492
|
-
return pool.assign_task(task_id, agent_id=agent_id)
|
|
493
|
-
|
|
494
|
-
# =========================================================================
|
|
495
|
-
# TASK INGESTION OPERATIONS (V3.1)
|
|
496
|
-
# =========================================================================
|
|
497
|
-
|
|
498
|
-
def get_ingestion_engine(self):
|
|
499
|
-
"""Get the TaskIngestionEngine instance."""
|
|
500
|
-
return _get_ingestion_engine(self.brain_path)
|
|
501
|
-
|
|
502
|
-
def ingest_tasks(self, source: str, source_type: str = "auto",
|
|
503
|
-
session_id: str = None, auto_assign: bool = False,
|
|
504
|
-
skip_dedup: bool = False, dry_run: bool = False) -> Dict:
|
|
505
|
-
"""Ingest tasks from various sources."""
|
|
506
|
-
engine = _get_ingestion_engine(self.brain_path)
|
|
507
|
-
if engine is None:
|
|
508
|
-
return {"success": False, "error": "TaskIngestionEngine not available"}
|
|
509
|
-
|
|
510
|
-
import os
|
|
511
|
-
if os.path.exists(source):
|
|
512
|
-
result = engine.ingest_from_file(
|
|
513
|
-
source, source_type=source_type, session_id=session_id,
|
|
514
|
-
auto_assign=auto_assign, skip_dedup=skip_dedup, dry_run=dry_run
|
|
515
|
-
)
|
|
516
|
-
else:
|
|
517
|
-
result = engine.ingest_from_text(
|
|
518
|
-
source, source_type=source_type if source_type != "auto" else "manual",
|
|
519
|
-
session_id=session_id, auto_assign=auto_assign,
|
|
520
|
-
skip_dedup=skip_dedup, dry_run=dry_run
|
|
521
|
-
)
|
|
522
|
-
|
|
523
|
-
return result.to_dict()
|
|
524
|
-
|
|
525
|
-
def rollback_ingestion(self, batch_id: str, reason: str = None) -> Dict:
|
|
526
|
-
"""Rollback an ingestion batch."""
|
|
527
|
-
engine = _get_ingestion_engine(self.brain_path)
|
|
528
|
-
if engine is None:
|
|
529
|
-
return {"success": False, "error": "TaskIngestionEngine not available"}
|
|
530
|
-
return engine.rollback(batch_id, reason)
|
|
531
|
-
|
|
532
|
-
def get_ingestion_stats(self) -> Dict:
|
|
533
|
-
"""Get ingestion statistics."""
|
|
534
|
-
engine = _get_ingestion_engine(self.brain_path)
|
|
535
|
-
if engine is None:
|
|
536
|
-
return {"error": "TaskIngestionEngine not available"}
|
|
537
|
-
return engine.get_ingestion_stats()
|
|
538
|
-
|
|
539
|
-
# =========================================================================
|
|
540
|
-
# METRICS & MONITORING
|
|
541
|
-
# =========================================================================
|
|
542
|
-
|
|
543
|
-
def get_pool_metrics(self) -> Dict:
|
|
544
|
-
"""Get pool-wide metrics."""
|
|
545
|
-
tasks = self.task_store.get_all_tasks()
|
|
546
|
-
|
|
547
|
-
return {
|
|
548
|
-
"total_tasks": len(tasks),
|
|
549
|
-
"pending": len([t for t in tasks if t.get("status") in ["PENDING", "READY"]]),
|
|
550
|
-
"in_progress": len([t for t in tasks if t.get("status") == "IN_PROGRESS"]),
|
|
551
|
-
"blocked": len([t for t in tasks if t.get("status") == "BLOCKED"]),
|
|
552
|
-
"done": len([t for t in tasks if t.get("status") == "DONE"]),
|
|
553
|
-
"failed": len([t for t in tasks if t.get("status") == "FAILED"]),
|
|
554
|
-
"with_checkpoints": len([t for t in tasks if t.get("checkpoint")]),
|
|
555
|
-
"with_summaries": len([t for t in tasks if t.get("context_summary")])
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
def get_dependency_graph(self) -> Dict:
|
|
559
|
-
"""Compute dependency graph for all tasks."""
|
|
560
|
-
tasks = self.task_store.get_all_tasks()
|
|
561
|
-
|
|
562
|
-
# Build forward dependencies (task -> what it depends on)
|
|
563
|
-
forward = {}
|
|
564
|
-
# Build reverse dependencies (task -> what depends on it)
|
|
565
|
-
reverse = {}
|
|
566
|
-
|
|
567
|
-
for task in tasks:
|
|
568
|
-
task_id = task["id"]
|
|
569
|
-
deps = task.get("blocked_by", []) + task.get("depends_on", [])
|
|
570
|
-
forward[task_id] = deps
|
|
571
|
-
|
|
572
|
-
for dep in deps:
|
|
573
|
-
if dep not in reverse:
|
|
574
|
-
reverse[dep] = []
|
|
575
|
-
reverse[dep].append(task_id)
|
|
576
|
-
|
|
577
|
-
# Compute depths
|
|
578
|
-
depths = {}
|
|
579
|
-
|
|
580
|
-
def compute_depth(task_id: str, visited: set) -> int:
|
|
581
|
-
if task_id in depths:
|
|
582
|
-
return depths[task_id]
|
|
583
|
-
if task_id in visited:
|
|
584
|
-
return -1 # Cycle detected
|
|
585
|
-
|
|
586
|
-
visited.add(task_id)
|
|
587
|
-
deps = forward.get(task_id, [])
|
|
588
|
-
|
|
589
|
-
if not deps:
|
|
590
|
-
depths[task_id] = 0
|
|
591
|
-
return 0
|
|
592
|
-
|
|
593
|
-
max_dep_depth = max(compute_depth(d, visited) for d in deps)
|
|
594
|
-
depths[task_id] = max_dep_depth + 1 if max_dep_depth >= 0 else -1
|
|
595
|
-
return depths[task_id]
|
|
596
|
-
|
|
597
|
-
for task in tasks:
|
|
598
|
-
compute_depth(task["id"], set())
|
|
599
|
-
|
|
600
|
-
return {
|
|
601
|
-
"forward_deps": forward,
|
|
602
|
-
"reverse_deps": reverse,
|
|
603
|
-
"depths": depths,
|
|
604
|
-
"computed_at": time.strftime("%Y-%m-%dT%H:%M:%S%z")
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
# Singleton instance
|
|
609
|
-
_orchestrator: Optional[NucleusOrchestratorV3] = None
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
def get_orchestrator() -> NucleusOrchestratorV3:
|
|
613
|
-
"""Get or create the singleton orchestrator instance."""
|
|
614
|
-
global _orchestrator
|
|
615
|
-
if _orchestrator is None:
|
|
616
|
-
_orchestrator = NucleusOrchestratorV3()
|
|
617
|
-
return _orchestrator
|