Spaces:
Sleeping
Sleeping
Claude Code Claude Opus 4.6 commited on
Commit ·
ee42c89
1
Parent(s): 86389f0
feat: Add resource exchange system for multi-agent coordination
Browse files- Add SharedMemory class with resource tracking and transfer methods
- Add TRANSFER_RESOURCES, CHECK_RESOURCES, and GET_RESOURCE_HISTORY tools
- Add resource display UI with bars and transfer notifications
- Add API endpoints for resource management (/api/resources, /api/resources/transfer, /api/resources/history)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- .openclaw/agents/brain_minimal.py +160 -0
- .openclaw/core/shared_memory.py +149 -0
- app.py +93 -0
- frontend/agent-dashboard.html +238 -0
.openclaw/agents/brain_minimal.py
CHANGED
|
@@ -20,6 +20,7 @@ import signal
|
|
| 20 |
import sys
|
| 21 |
import traceback
|
| 22 |
import asyncio
|
|
|
|
| 23 |
|
| 24 |
logger = logging.getLogger(__name__)
|
| 25 |
|
|
@@ -141,6 +142,19 @@ except ImportError:
|
|
| 141 |
print("[Brain] Warning: Conversation persistence not available")
|
| 142 |
|
| 143 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
# State Machine for Agent State Management
|
| 145 |
class AgentState(Enum):
|
| 146 |
"""Agent operational states"""
|
|
@@ -690,6 +704,27 @@ class BrainMinimal:
|
|
| 690 |
"Ping another agent"
|
| 691 |
)
|
| 692 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
def _is_infrastructure_agent(self) -> bool:
|
| 694 |
"""Check if this is an infrastructure agent (Adam)"""
|
| 695 |
if self.legacy_mode:
|
|
@@ -1237,6 +1272,131 @@ class BrainMinimal:
|
|
| 1237 |
"timestamp": datetime.utcnow().isoformat()
|
| 1238 |
}
|
| 1239 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1240 |
# ========== Decision Making ==========
|
| 1241 |
|
| 1242 |
def think(self, context: Dict[str, Any]) -> str:
|
|
|
|
| 20 |
import sys
|
| 21 |
import traceback
|
| 22 |
import asyncio
|
| 23 |
+
import re
|
| 24 |
|
| 25 |
logger = logging.getLogger(__name__)
|
| 26 |
|
|
|
|
| 142 |
print("[Brain] Warning: Conversation persistence not available")
|
| 143 |
|
| 144 |
|
| 145 |
+
# Import Shared Memory for resource exchange
|
| 146 |
+
try:
|
| 147 |
+
from ..core.shared_memory import get_shared_memory, SharedMemory
|
| 148 |
+
SHARED_MEMORY_AVAILABLE = True
|
| 149 |
+
except ImportError:
|
| 150 |
+
try:
|
| 151 |
+
from core.shared_memory import get_shared_memory, SharedMemory
|
| 152 |
+
SHARED_MEMORY_AVAILABLE = True
|
| 153 |
+
except ImportError:
|
| 154 |
+
SHARED_MEMORY_AVAILABLE = False
|
| 155 |
+
print("[Brain] Warning: Shared memory not available")
|
| 156 |
+
|
| 157 |
+
|
| 158 |
# State Machine for Agent State Management
|
| 159 |
class AgentState(Enum):
|
| 160 |
"""Agent operational states"""
|
|
|
|
| 704 |
"Ping another agent"
|
| 705 |
)
|
| 706 |
|
| 707 |
+
# Resource management tools
|
| 708 |
+
if SHARED_MEMORY_AVAILABLE:
|
| 709 |
+
self.register_tool(
|
| 710 |
+
"transfer_resources",
|
| 711 |
+
self._transfer_resources,
|
| 712 |
+
ToolCategory.SHARED,
|
| 713 |
+
"Transfer resources to another agent"
|
| 714 |
+
)
|
| 715 |
+
self.register_tool(
|
| 716 |
+
"check_resources",
|
| 717 |
+
self._check_resources,
|
| 718 |
+
ToolCategory.SHARED,
|
| 719 |
+
"Check current resource holdings"
|
| 720 |
+
)
|
| 721 |
+
self.register_tool(
|
| 722 |
+
"get_resource_history",
|
| 723 |
+
self._get_resource_history,
|
| 724 |
+
ToolCategory.SHARED,
|
| 725 |
+
"Get recent resource transfer history"
|
| 726 |
+
)
|
| 727 |
+
|
| 728 |
def _is_infrastructure_agent(self) -> bool:
|
| 729 |
"""Check if this is an infrastructure agent (Adam)"""
|
| 730 |
if self.legacy_mode:
|
|
|
|
| 1272 |
"timestamp": datetime.utcnow().isoformat()
|
| 1273 |
}
|
| 1274 |
|
| 1275 |
+
# ========== Resource Management ==========
|
| 1276 |
+
|
| 1277 |
+
def _transfer_resources(self, to_agent: str, amount: int) -> Dict[str, Any]:
|
| 1278 |
+
"""
|
| 1279 |
+
Transfer resources to another agent.
|
| 1280 |
+
|
| 1281 |
+
Args:
|
| 1282 |
+
to_agent: Target agent to receive resources
|
| 1283 |
+
amount: Amount of resources to transfer
|
| 1284 |
+
|
| 1285 |
+
Returns:
|
| 1286 |
+
Result dict with transfer status
|
| 1287 |
+
"""
|
| 1288 |
+
if not SHARED_MEMORY_AVAILABLE:
|
| 1289 |
+
return {
|
| 1290 |
+
"success": False,
|
| 1291 |
+
"error": "Shared memory not available",
|
| 1292 |
+
"agent": self.agent_name
|
| 1293 |
+
}
|
| 1294 |
+
|
| 1295 |
+
try:
|
| 1296 |
+
shared_mem = get_shared_memory()
|
| 1297 |
+
success = shared_mem.transfer_resources(
|
| 1298 |
+
from_agent=self.agent_name,
|
| 1299 |
+
to_agent=to_agent,
|
| 1300 |
+
amount=amount
|
| 1301 |
+
)
|
| 1302 |
+
|
| 1303 |
+
if success:
|
| 1304 |
+
logger.info(f"[{self.agent_name}] Transferred {amount} resources to {to_agent}")
|
| 1305 |
+
return {
|
| 1306 |
+
"success": True,
|
| 1307 |
+
"from": self.agent_name,
|
| 1308 |
+
"to": to_agent,
|
| 1309 |
+
"amount": amount,
|
| 1310 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 1311 |
+
}
|
| 1312 |
+
else:
|
| 1313 |
+
return {
|
| 1314 |
+
"success": False,
|
| 1315 |
+
"error": "Insufficient resources",
|
| 1316 |
+
"from": self.agent_name,
|
| 1317 |
+
"to": to_agent,
|
| 1318 |
+
"amount": amount,
|
| 1319 |
+
"available": shared_mem.get_resources(self.agent_name).get(self.agent_name, 0)
|
| 1320 |
+
}
|
| 1321 |
+
except Exception as e:
|
| 1322 |
+
logger.error(f"[{self.agent_name}] Transfer failed: {e}")
|
| 1323 |
+
return {
|
| 1324 |
+
"success": False,
|
| 1325 |
+
"error": str(e),
|
| 1326 |
+
"agent": self.agent_name
|
| 1327 |
+
}
|
| 1328 |
+
|
| 1329 |
+
def _check_resources(self, agent: Optional[str] = None) -> Dict[str, Any]:
|
| 1330 |
+
"""
|
| 1331 |
+
Check current resource holdings.
|
| 1332 |
+
|
| 1333 |
+
Args:
|
| 1334 |
+
agent: Optional agent name to check. If None, returns all agents.
|
| 1335 |
+
|
| 1336 |
+
Returns:
|
| 1337 |
+
Result dict with resource information
|
| 1338 |
+
"""
|
| 1339 |
+
if not SHARED_MEMORY_AVAILABLE:
|
| 1340 |
+
return {
|
| 1341 |
+
"success": False,
|
| 1342 |
+
"error": "Shared memory not available",
|
| 1343 |
+
"agent": self.agent_name
|
| 1344 |
+
}
|
| 1345 |
+
|
| 1346 |
+
try:
|
| 1347 |
+
shared_mem = get_shared_memory()
|
| 1348 |
+
resources = shared_mem.get_resources(agent)
|
| 1349 |
+
|
| 1350 |
+
return {
|
| 1351 |
+
"success": True,
|
| 1352 |
+
"resources": resources,
|
| 1353 |
+
"agent": self.agent_name,
|
| 1354 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 1355 |
+
}
|
| 1356 |
+
except Exception as e:
|
| 1357 |
+
logger.error(f"[{self.agent_name}] Check resources failed: {e}")
|
| 1358 |
+
return {
|
| 1359 |
+
"success": False,
|
| 1360 |
+
"error": str(e),
|
| 1361 |
+
"agent": self.agent_name
|
| 1362 |
+
}
|
| 1363 |
+
|
| 1364 |
+
def _get_resource_history(self, limit: int = 10) -> Dict[str, Any]:
|
| 1365 |
+
"""
|
| 1366 |
+
Get recent resource transfer history.
|
| 1367 |
+
|
| 1368 |
+
Args:
|
| 1369 |
+
limit: Maximum number of transfers to return
|
| 1370 |
+
|
| 1371 |
+
Returns:
|
| 1372 |
+
Result dict with transfer history
|
| 1373 |
+
"""
|
| 1374 |
+
if not SHARED_MEMORY_AVAILABLE:
|
| 1375 |
+
return {
|
| 1376 |
+
"success": False,
|
| 1377 |
+
"error": "Shared memory not available",
|
| 1378 |
+
"agent": self.agent_name
|
| 1379 |
+
}
|
| 1380 |
+
|
| 1381 |
+
try:
|
| 1382 |
+
shared_mem = get_shared_memory()
|
| 1383 |
+
history = shared_mem.get_transfer_log(limit)
|
| 1384 |
+
|
| 1385 |
+
return {
|
| 1386 |
+
"success": True,
|
| 1387 |
+
"history": history,
|
| 1388 |
+
"count": len(history),
|
| 1389 |
+
"agent": self.agent_name,
|
| 1390 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 1391 |
+
}
|
| 1392 |
+
except Exception as e:
|
| 1393 |
+
logger.error(f"[{self.agent_name}] Get history failed: {e}")
|
| 1394 |
+
return {
|
| 1395 |
+
"success": False,
|
| 1396 |
+
"error": str(e),
|
| 1397 |
+
"agent": self.agent_name
|
| 1398 |
+
}
|
| 1399 |
+
|
| 1400 |
# ========== Decision Making ==========
|
| 1401 |
|
| 1402 |
def think(self, context: Dict[str, Any]) -> str:
|
.openclaw/core/shared_memory.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
Shared Memory Module for OpenClaw
|
| 3 |
==================================
|
| 4 |
Provides persistent conversation storage with atomic write operations.
|
|
|
|
| 5 |
"""
|
| 6 |
import json
|
| 7 |
import os
|
|
@@ -208,3 +209,151 @@ class ConversationStore:
|
|
| 208 |
if session_id:
|
| 209 |
return sum(1 for msg in messages if msg.session_id == session_id)
|
| 210 |
return len(messages)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
Shared Memory Module for OpenClaw
|
| 3 |
==================================
|
| 4 |
Provides persistent conversation storage with atomic write operations.
|
| 5 |
+
Also provides SharedMemory class for agent resource exchange.
|
| 6 |
"""
|
| 7 |
import json
|
| 8 |
import os
|
|
|
|
| 209 |
if session_id:
|
| 210 |
return sum(1 for msg in messages if msg.session_id == session_id)
|
| 211 |
return len(messages)
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
class SharedMemory:
|
| 215 |
+
"""
|
| 216 |
+
Thread-safe shared memory for agent resource exchange and coordination.
|
| 217 |
+
Provides resource tracking, transfer capabilities, and inter-agent communication.
|
| 218 |
+
"""
|
| 219 |
+
|
| 220 |
+
def __init__(self, storage_dir: Optional[Path] = None):
|
| 221 |
+
"""
|
| 222 |
+
Initialize shared memory with resource tracking.
|
| 223 |
+
|
| 224 |
+
Args:
|
| 225 |
+
storage_dir: Directory to store shared memory data.
|
| 226 |
+
Defaults to ~/.openclaw/memory/
|
| 227 |
+
"""
|
| 228 |
+
if storage_dir is None:
|
| 229 |
+
storage_dir = Path.home() / ".openclaw" / "memory"
|
| 230 |
+
|
| 231 |
+
self.storage_dir = Path(storage_dir)
|
| 232 |
+
self._lock = Lock()
|
| 233 |
+
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
| 234 |
+
|
| 235 |
+
# Initialize resources
|
| 236 |
+
self.resources = {"adam": 100, "eve": 100}
|
| 237 |
+
self._transfer_log = []
|
| 238 |
+
|
| 239 |
+
# Try to load existing state
|
| 240 |
+
self._load_state()
|
| 241 |
+
|
| 242 |
+
def _load_state(self) -> None:
|
| 243 |
+
"""Load shared memory state from disk."""
|
| 244 |
+
state_file = self.storage_dir / "shared_memory_state.json"
|
| 245 |
+
if state_file.exists():
|
| 246 |
+
try:
|
| 247 |
+
with open(state_file, 'r') as f:
|
| 248 |
+
state = json.load(f)
|
| 249 |
+
self.resources = state.get("resources", {"adam": 100, "eve": 100})
|
| 250 |
+
self._transfer_log = state.get("transfer_log", [])
|
| 251 |
+
except (json.JSONDecodeError, IOError):
|
| 252 |
+
pass
|
| 253 |
+
|
| 254 |
+
def _save_state(self) -> None:
|
| 255 |
+
"""Save shared memory state to disk."""
|
| 256 |
+
state_file = self.storage_dir / "shared_memory_state.json"
|
| 257 |
+
try:
|
| 258 |
+
with open(state_file, 'w') as f:
|
| 259 |
+
json.dump({
|
| 260 |
+
"resources": self.resources,
|
| 261 |
+
"transfer_log": self._transfer_log
|
| 262 |
+
}, f)
|
| 263 |
+
except IOError:
|
| 264 |
+
pass
|
| 265 |
+
|
| 266 |
+
def transfer_resources(self, from_agent: str, to_agent: str, amount: int) -> bool:
|
| 267 |
+
"""
|
| 268 |
+
Transfer resources between agents.
|
| 269 |
+
|
| 270 |
+
Args:
|
| 271 |
+
from_agent: Source agent name
|
| 272 |
+
to_agent: Target agent name
|
| 273 |
+
amount: Amount to transfer
|
| 274 |
+
|
| 275 |
+
Returns:
|
| 276 |
+
True if transfer succeeded, False otherwise
|
| 277 |
+
"""
|
| 278 |
+
with self._lock:
|
| 279 |
+
from_agent = from_agent.lower()
|
| 280 |
+
to_agent = to_agent.lower()
|
| 281 |
+
|
| 282 |
+
# Check if sender has enough resources
|
| 283 |
+
if self.resources.get(from_agent, 0) >= amount:
|
| 284 |
+
self.resources[from_agent] -= amount
|
| 285 |
+
self.resources[to_agent] = self.resources.get(to_agent, 0) + amount
|
| 286 |
+
|
| 287 |
+
# Log the transfer
|
| 288 |
+
self._transfer_log.append({
|
| 289 |
+
"from": from_agent,
|
| 290 |
+
"to": to_agent,
|
| 291 |
+
"amount": amount,
|
| 292 |
+
"timestamp": datetime.utcnow().isoformat() + 'Z'
|
| 293 |
+
})
|
| 294 |
+
|
| 295 |
+
self._save_state()
|
| 296 |
+
return True
|
| 297 |
+
return False
|
| 298 |
+
|
| 299 |
+
def get_resources(self, agent: Optional[str] = None) -> Dict[str, int]:
|
| 300 |
+
"""
|
| 301 |
+
Get current resource values.
|
| 302 |
+
|
| 303 |
+
Args:
|
| 304 |
+
agent: If provided, return only this agent's resources
|
| 305 |
+
|
| 306 |
+
Returns:
|
| 307 |
+
Dictionary of agent to resource amount
|
| 308 |
+
"""
|
| 309 |
+
with self._lock:
|
| 310 |
+
if agent:
|
| 311 |
+
return {agent.lower(): self.resources.get(agent.lower(), 0)}
|
| 312 |
+
return self.resources.copy()
|
| 313 |
+
|
| 314 |
+
def get_transfer_log(self, limit: int = 10) -> List[Dict[str, Any]]:
|
| 315 |
+
"""
|
| 316 |
+
Get recent transfer history.
|
| 317 |
+
|
| 318 |
+
Args:
|
| 319 |
+
limit: Maximum number of transfers to return
|
| 320 |
+
|
| 321 |
+
Returns:
|
| 322 |
+
List of transfer records
|
| 323 |
+
"""
|
| 324 |
+
with self._lock:
|
| 325 |
+
return self._transfer_log[-limit:]
|
| 326 |
+
|
| 327 |
+
def reset_resources(self, initial_values: Optional[Dict[str, int]] = None) -> None:
|
| 328 |
+
"""
|
| 329 |
+
Reset resources to initial values.
|
| 330 |
+
|
| 331 |
+
Args:
|
| 332 |
+
initial_values: Optional dict of agent to starting amount.
|
| 333 |
+
Defaults to {"adam": 100, "eve": 100}
|
| 334 |
+
"""
|
| 335 |
+
with self._lock:
|
| 336 |
+
if initial_values is None:
|
| 337 |
+
initial_values = {"adam": 100, "eve": 100}
|
| 338 |
+
self.resources = {k.lower(): v for k, v in initial_values.items()}
|
| 339 |
+
self._transfer_log = []
|
| 340 |
+
self._save_state()
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
# Global shared memory instance
|
| 344 |
+
_shared_memory_instance: Optional[SharedMemory] = None
|
| 345 |
+
_shared_memory_lock = Lock()
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def get_shared_memory() -> SharedMemory:
|
| 349 |
+
"""
|
| 350 |
+
Get the global shared memory instance (singleton pattern).
|
| 351 |
+
|
| 352 |
+
Returns:
|
| 353 |
+
The global SharedMemory instance
|
| 354 |
+
"""
|
| 355 |
+
global _shared_memory_instance
|
| 356 |
+
with _shared_memory_lock:
|
| 357 |
+
if _shared_memory_instance is None:
|
| 358 |
+
_shared_memory_instance = SharedMemory()
|
| 359 |
+
return _shared_memory_instance
|
app.py
CHANGED
|
@@ -2935,6 +2935,99 @@ def create_agent_office_with_ws():
|
|
| 2935 |
logger.error(f"[API_ERROR] /api/analytics/reset: {e}")
|
| 2936 |
return {"error": str(e)}
|
| 2937 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2938 |
# ========== Conversation Persistence API Endpoints ==========
|
| 2939 |
|
| 2940 |
if CONVERSATION_STORAGE_AVAILABLE:
|
|
|
|
| 2935 |
logger.error(f"[API_ERROR] /api/analytics/reset: {e}")
|
| 2936 |
return {"error": str(e)}
|
| 2937 |
|
| 2938 |
+
# ========== Resource Exchange API Endpoints ==========
|
| 2939 |
+
|
| 2940 |
+
# Import SharedMemory for resource management
|
| 2941 |
+
try:
|
| 2942 |
+
from .openclaw.core.shared_memory import get_shared_memory
|
| 2943 |
+
SHARED_MEMORY_AVAILABLE = True
|
| 2944 |
+
except ImportError:
|
| 2945 |
+
try:
|
| 2946 |
+
from openclaw.core.shared_memory import get_shared_memory
|
| 2947 |
+
SHARED_MEMORY_AVAILABLE = True
|
| 2948 |
+
except ImportError:
|
| 2949 |
+
SHARED_MEMORY_AVAILABLE = False
|
| 2950 |
+
|
| 2951 |
+
if SHARED_MEMORY_AVAILABLE:
|
| 2952 |
+
@fastapi_app.get("/api/resources")
|
| 2953 |
+
async def api_get_resources(agent: str = None):
|
| 2954 |
+
"""Get current resource holdings."""
|
| 2955 |
+
try:
|
| 2956 |
+
shared_mem = get_shared_memory()
|
| 2957 |
+
resources = shared_mem.get_resources(agent)
|
| 2958 |
+
return {
|
| 2959 |
+
"success": True,
|
| 2960 |
+
"resources": resources,
|
| 2961 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 2962 |
+
}
|
| 2963 |
+
except asyncio.CancelledError:
|
| 2964 |
+
logger.debug("[API_CANCEL] /api/resources was cancelled")
|
| 2965 |
+
from fastapi.responses import Response
|
| 2966 |
+
return Response(status_code=204, content=None)
|
| 2967 |
+
except Exception as e:
|
| 2968 |
+
logger.error(f"[API_ERROR] /api/resources: {e}")
|
| 2969 |
+
return {"success": False, "error": str(e)}
|
| 2970 |
+
|
| 2971 |
+
@fastapi_app.post("/api/resources/transfer")
|
| 2972 |
+
async def api_transfer_resources(from_agent: str, to_agent: str, amount: int):
|
| 2973 |
+
"""Transfer resources between agents."""
|
| 2974 |
+
try:
|
| 2975 |
+
shared_mem = get_shared_memory()
|
| 2976 |
+
success = shared_mem.transfer_resources(from_agent, to_agent, amount)
|
| 2977 |
+
return {
|
| 2978 |
+
"success": success,
|
| 2979 |
+
"from": from_agent,
|
| 2980 |
+
"to": to_agent,
|
| 2981 |
+
"amount": amount,
|
| 2982 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 2983 |
+
}
|
| 2984 |
+
except asyncio.CancelledError:
|
| 2985 |
+
logger.debug("[API_CANCEL] /api/resources/transfer was cancelled")
|
| 2986 |
+
from fastapi.responses import Response
|
| 2987 |
+
return Response(status_code=204, content=None)
|
| 2988 |
+
except Exception as e:
|
| 2989 |
+
logger.error(f"[API_ERROR] /api/resources/transfer: {e}")
|
| 2990 |
+
return {"success": False, "error": str(e)}
|
| 2991 |
+
|
| 2992 |
+
@fastapi_app.get("/api/resources/history")
|
| 2993 |
+
async def api_resource_history(limit: int = 10):
|
| 2994 |
+
"""Get recent resource transfer history."""
|
| 2995 |
+
try:
|
| 2996 |
+
shared_mem = get_shared_memory()
|
| 2997 |
+
history = shared_mem.get_transfer_log(limit)
|
| 2998 |
+
return {
|
| 2999 |
+
"success": True,
|
| 3000 |
+
"history": history,
|
| 3001 |
+
"count": len(history),
|
| 3002 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 3003 |
+
}
|
| 3004 |
+
except asyncio.CancelledError:
|
| 3005 |
+
logger.debug("[API_CANCEL] /api/resources/history was cancelled")
|
| 3006 |
+
from fastapi.responses import Response
|
| 3007 |
+
return Response(status_code=204, content=None)
|
| 3008 |
+
except Exception as e:
|
| 3009 |
+
logger.error(f"[API_ERROR] /api/resources/history: {e}")
|
| 3010 |
+
return {"success": False, "error": str(e)}
|
| 3011 |
+
|
| 3012 |
+
@fastapi_app.post("/api/resources/reset")
|
| 3013 |
+
async def api_reset_resources():
|
| 3014 |
+
"""Reset resources to initial values."""
|
| 3015 |
+
try:
|
| 3016 |
+
shared_mem = get_shared_memory()
|
| 3017 |
+
shared_mem.reset_resources()
|
| 3018 |
+
return {
|
| 3019 |
+
"success": True,
|
| 3020 |
+
"message": "Resources reset",
|
| 3021 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 3022 |
+
}
|
| 3023 |
+
except asyncio.CancelledError:
|
| 3024 |
+
logger.debug("[API_CANCEL] /api/resources/reset was cancelled")
|
| 3025 |
+
from fastapi.responses import Response
|
| 3026 |
+
return Response(status_code=204, content=None)
|
| 3027 |
+
except Exception as e:
|
| 3028 |
+
logger.error(f"[API_ERROR] /api/resources/reset: {e}")
|
| 3029 |
+
return {"success": False, "error": str(e)}
|
| 3030 |
+
|
| 3031 |
# ========== Conversation Persistence API Endpoints ==========
|
| 3032 |
|
| 3033 |
if CONVERSATION_STORAGE_AVAILABLE:
|
frontend/agent-dashboard.html
CHANGED
|
@@ -300,6 +300,115 @@
|
|
| 300 |
margin-bottom: 16px;
|
| 301 |
opacity: 0.5;
|
| 302 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
</style>
|
| 304 |
</head>
|
| 305 |
<body>
|
|
@@ -393,23 +502,62 @@
|
|
| 393 |
</div>
|
| 394 |
</div>
|
| 395 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
</div>
|
| 397 |
|
|
|
|
|
|
|
|
|
|
| 398 |
<script>
|
| 399 |
// Configuration
|
| 400 |
const API_BASE = window.location.origin;
|
| 401 |
const POLL_INTERVAL = 2000; // 2 seconds
|
|
|
|
| 402 |
|
| 403 |
// State
|
| 404 |
let lastThoughtCount = 0;
|
| 405 |
let startTime = Date.now();
|
| 406 |
let eventCount = 0;
|
|
|
|
| 407 |
|
| 408 |
// Initialize
|
| 409 |
document.addEventListener('DOMContentLoaded', () => {
|
| 410 |
connectToEventBus();
|
| 411 |
setInterval(fetchThoughts, POLL_INTERVAL);
|
| 412 |
setInterval(updateStats, 1000);
|
|
|
|
|
|
|
| 413 |
});
|
| 414 |
|
| 415 |
// Connect to event bus
|
|
@@ -588,6 +736,96 @@
|
|
| 588 |
|
| 589 |
// Expose simulation function for testing
|
| 590 |
window.simulateThought = simulateThought;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 591 |
</script>
|
| 592 |
</body>
|
| 593 |
</html>
|
|
|
|
| 300 |
margin-bottom: 16px;
|
| 301 |
opacity: 0.5;
|
| 302 |
}
|
| 303 |
+
|
| 304 |
+
/* Resource Display Styles */
|
| 305 |
+
.resource-panel {
|
| 306 |
+
background: white;
|
| 307 |
+
border-radius: 12px;
|
| 308 |
+
padding: 20px;
|
| 309 |
+
margin-top: 20px;
|
| 310 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
.resource-panel h2 {
|
| 314 |
+
color: #667eea;
|
| 315 |
+
font-size: 20px;
|
| 316 |
+
margin-bottom: 16px;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.resource-grid {
|
| 320 |
+
display: grid;
|
| 321 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 322 |
+
gap: 16px;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.resource-card {
|
| 326 |
+
background: #f8f9fa;
|
| 327 |
+
border-radius: 8px;
|
| 328 |
+
padding: 16px;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.resource-card h3 {
|
| 332 |
+
font-size: 16px;
|
| 333 |
+
margin-bottom: 8px;
|
| 334 |
+
color: #333;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.resource-bar-container {
|
| 338 |
+
background: #e0e0e0;
|
| 339 |
+
border-radius: 8px;
|
| 340 |
+
height: 24px;
|
| 341 |
+
overflow: hidden;
|
| 342 |
+
margin-bottom: 8px;
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
.resource-bar {
|
| 346 |
+
height: 100%;
|
| 347 |
+
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
| 348 |
+
transition: width 0.5s ease;
|
| 349 |
+
border-radius: 8px;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
.resource-bar.adam {
|
| 353 |
+
background: linear-gradient(90deg, #28a745 0%, #20c997 100%);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.resource-bar.eve {
|
| 357 |
+
background: linear-gradient(90deg, #fd7e14 0%, #ffc107 100%);
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
.resource-value {
|
| 361 |
+
font-size: 24px;
|
| 362 |
+
font-weight: bold;
|
| 363 |
+
color: #667eea;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
.resource-label {
|
| 367 |
+
font-size: 12px;
|
| 368 |
+
color: #666;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.transfer-notification {
|
| 372 |
+
position: fixed;
|
| 373 |
+
bottom: 20px;
|
| 374 |
+
right: 20px;
|
| 375 |
+
background: white;
|
| 376 |
+
border-radius: 8px;
|
| 377 |
+
padding: 16px;
|
| 378 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 379 |
+
animation: slideInRight 0.3s ease-out;
|
| 380 |
+
max-width: 300px;
|
| 381 |
+
z-index: 1000;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
@keyframes slideInRight {
|
| 385 |
+
from {
|
| 386 |
+
transform: translateX(100%);
|
| 387 |
+
opacity: 0;
|
| 388 |
+
}
|
| 389 |
+
to {
|
| 390 |
+
transform: translateX(0);
|
| 391 |
+
opacity: 1;
|
| 392 |
+
}
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.transfer-notification.success {
|
| 396 |
+
border-left: 4px solid #28a745;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
.transfer-notification.error {
|
| 400 |
+
border-left: 4px solid #dc3545;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
.transfer-notification .title {
|
| 404 |
+
font-weight: bold;
|
| 405 |
+
margin-bottom: 4px;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
.transfer-notification .message {
|
| 409 |
+
font-size: 14px;
|
| 410 |
+
color: #666;
|
| 411 |
+
}
|
| 412 |
</style>
|
| 413 |
</head>
|
| 414 |
<body>
|
|
|
|
| 502 |
</div>
|
| 503 |
</div>
|
| 504 |
</div>
|
| 505 |
+
|
| 506 |
+
<!-- Resource Exchange Panel -->
|
| 507 |
+
<div class="resource-panel">
|
| 508 |
+
<h2>
|
| 509 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle; margin-right: 8px;">
|
| 510 |
+
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
|
| 511 |
+
</svg>
|
| 512 |
+
Resource Exchange
|
| 513 |
+
</h2>
|
| 514 |
+
<div class="resource-grid" id="resourceGrid">
|
| 515 |
+
<div class="resource-card">
|
| 516 |
+
<h3>Adam</h3>
|
| 517 |
+
<div class="resource-bar-container">
|
| 518 |
+
<div class="resource-bar adam" id="adamBar" style="width: 50%"></div>
|
| 519 |
+
</div>
|
| 520 |
+
<div class="resource-value" id="adamValue">100</div>
|
| 521 |
+
<div class="resource-label">Resources</div>
|
| 522 |
+
</div>
|
| 523 |
+
<div class="resource-card">
|
| 524 |
+
<h3>Eve</h3>
|
| 525 |
+
<div class="resource-bar-container">
|
| 526 |
+
<div class="resource-bar eve" id="eveBar" style="width: 50%"></div>
|
| 527 |
+
</div>
|
| 528 |
+
<div class="resource-value" id="eveValue">100</div>
|
| 529 |
+
<div class="resource-label">Resources</div>
|
| 530 |
+
</div>
|
| 531 |
+
</div>
|
| 532 |
+
<div class="controls" style="margin-top: 16px;">
|
| 533 |
+
<button class="btn btn-primary" onclick="fetchResources()">Refresh Resources</button>
|
| 534 |
+
<button class="btn btn-secondary" onclick="showTransferHistory()">Transfer History</button>
|
| 535 |
+
</div>
|
| 536 |
+
</div>
|
| 537 |
</div>
|
| 538 |
|
| 539 |
+
<!-- Transfer notification container -->
|
| 540 |
+
<div id="notificationContainer"></div>
|
| 541 |
+
|
| 542 |
<script>
|
| 543 |
// Configuration
|
| 544 |
const API_BASE = window.location.origin;
|
| 545 |
const POLL_INTERVAL = 2000; // 2 seconds
|
| 546 |
+
const RESOURCE_INTERVAL = 5000; // 5 seconds
|
| 547 |
|
| 548 |
// State
|
| 549 |
let lastThoughtCount = 0;
|
| 550 |
let startTime = Date.now();
|
| 551 |
let eventCount = 0;
|
| 552 |
+
let lastResources = { adam: 100, eve: 100 };
|
| 553 |
|
| 554 |
// Initialize
|
| 555 |
document.addEventListener('DOMContentLoaded', () => {
|
| 556 |
connectToEventBus();
|
| 557 |
setInterval(fetchThoughts, POLL_INTERVAL);
|
| 558 |
setInterval(updateStats, 1000);
|
| 559 |
+
setInterval(fetchResources, RESOURCE_INTERVAL);
|
| 560 |
+
fetchResources(); // Initial fetch
|
| 561 |
});
|
| 562 |
|
| 563 |
// Connect to event bus
|
|
|
|
| 736 |
|
| 737 |
// Expose simulation function for testing
|
| 738 |
window.simulateThought = simulateThought;
|
| 739 |
+
|
| 740 |
+
// ========== Resource Management ==========
|
| 741 |
+
|
| 742 |
+
// Fetch resources from API
|
| 743 |
+
async function fetchResources() {
|
| 744 |
+
try {
|
| 745 |
+
const response = await fetch(`${API_BASE}/api/resources`);
|
| 746 |
+
if (response.ok) {
|
| 747 |
+
const data = await response.json();
|
| 748 |
+
updateResourceDisplay(data.resources || {});
|
| 749 |
+
}
|
| 750 |
+
} catch (error) {
|
| 751 |
+
console.error('Error fetching resources:', error);
|
| 752 |
+
}
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
// Update resource display
|
| 756 |
+
function updateResourceDisplay(resources) {
|
| 757 |
+
const maxResources = 200; // For bar scaling
|
| 758 |
+
|
| 759 |
+
for (const [agent, amount] of Object.entries(resources)) {
|
| 760 |
+
const valueEl = document.getElementById(`${agent}Value`);
|
| 761 |
+
const barEl = document.getElementById(`${agent}Bar`);
|
| 762 |
+
|
| 763 |
+
if (valueEl && barEl) {
|
| 764 |
+
valueEl.textContent = amount;
|
| 765 |
+
const percentage = Math.min((amount / maxResources) * 100, 100);
|
| 766 |
+
barEl.style.width = `${percentage}%`;
|
| 767 |
+
|
| 768 |
+
// Check for changes and show notification
|
| 769 |
+
if (lastResources[agent] !== undefined && lastResources[agent] !== amount) {
|
| 770 |
+
const diff = amount - lastResources[agent];
|
| 771 |
+
if (diff !== 0) {
|
| 772 |
+
showTransferNotification(agent, diff);
|
| 773 |
+
}
|
| 774 |
+
}
|
| 775 |
+
}
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
lastResources = { ...resources };
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
// Show transfer notification
|
| 782 |
+
function showTransferNotification(agent, diff) {
|
| 783 |
+
const container = document.getElementById('notificationContainer');
|
| 784 |
+
const notification = document.createElement('div');
|
| 785 |
+
|
| 786 |
+
const isSuccess = diff > 0;
|
| 787 |
+
notification.className = `transfer-notification ${isSuccess ? 'success' : 'error'}`;
|
| 788 |
+
|
| 789 |
+
notification.innerHTML = `
|
| 790 |
+
<div class="title">${isSuccess ? 'Resources Received!' : 'Resources Transferred'}</div>
|
| 791 |
+
<div class="message">
|
| 792 |
+
${agent.charAt(0).toUpperCase() + agent.slice(1)}: ${diff > 0 ? '+' : ''}${diff} resources
|
| 793 |
+
</div>
|
| 794 |
+
`;
|
| 795 |
+
|
| 796 |
+
container.appendChild(notification);
|
| 797 |
+
|
| 798 |
+
// Remove notification after 3 seconds
|
| 799 |
+
setTimeout(() => {
|
| 800 |
+
notification.style.animation = 'slideInRight 0.3s ease-out reverse';
|
| 801 |
+
setTimeout(() => notification.remove(), 300);
|
| 802 |
+
}, 3000);
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
// Show transfer history
|
| 806 |
+
async function showTransferHistory() {
|
| 807 |
+
try {
|
| 808 |
+
const response = await fetch(`${API_BASE}/api/resources/history?limit=10`);
|
| 809 |
+
if (response.ok) {
|
| 810 |
+
const data = await response.json();
|
| 811 |
+
displayTransferHistory(data.history || []);
|
| 812 |
+
}
|
| 813 |
+
} catch (error) {
|
| 814 |
+
console.error('Error fetching transfer history:', error);
|
| 815 |
+
}
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
// Display transfer history
|
| 819 |
+
function displayTransferHistory(history) {
|
| 820 |
+
if (history.length === 0) {
|
| 821 |
+
showTransferNotification('system', 0);
|
| 822 |
+
return;
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
// Show a summary notification
|
| 826 |
+
const latest = history[history.length - 1];
|
| 827 |
+
showTransferNotification(latest.to, latest.amount);
|
| 828 |
+
}
|
| 829 |
</script>
|
| 830 |
</body>
|
| 831 |
</html>
|