feat: Add agent workspace system and runtime management
- Add agent core modules (agent_core, factory, registry, skill_loader) - Add runtime system for agent execution management - Add REST API for agents, workspaces, and runtime control - Add process supervisor for agent lifecycle management - Add workspace template system with agent profiles - Add frontend RuntimeView and runtime API integration - Add per-agent skill workspaces for smoke_fullstack run - Refactor skill system with active/installed separation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
13
backend/runtime/__init__.py
Normal file
13
backend/runtime/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from .agent_runtime import AgentRuntimeState
|
||||
from .context import TradingRunContext
|
||||
from .manager import TradingRuntimeManager
|
||||
from .registry import RuntimeRegistry
|
||||
from .session import TradingSessionKey
|
||||
|
||||
__all__ = [
|
||||
"AgentRuntimeState",
|
||||
"TradingRunContext",
|
||||
"TradingRuntimeManager",
|
||||
"RuntimeRegistry",
|
||||
"TradingSessionKey",
|
||||
]
|
||||
26
backend/runtime/agent_runtime.py
Normal file
26
backend/runtime/agent_runtime.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, UTC
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentRuntimeState:
|
||||
agent_id: str
|
||||
status: str = "idle"
|
||||
last_session: str | None = None
|
||||
last_updated: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
|
||||
def update(self, status: str, session_key: str | None = None) -> None:
|
||||
self.status = status
|
||||
self.last_session = session_key
|
||||
self.last_updated = datetime.now(UTC)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"agent_id": self.agent_id,
|
||||
"status": self.status,
|
||||
"last_session": self.last_session,
|
||||
"last_updated": self.last_updated.isoformat(),
|
||||
}
|
||||
15
backend/runtime/context.py
Normal file
15
backend/runtime/context.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TradingRunContext:
|
||||
config_name: str
|
||||
run_dir: Path
|
||||
bootstrap_values: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def describe(self) -> str:
|
||||
return f"Run {self.config_name} @ {self.run_dir}"
|
||||
134
backend/runtime/manager.py
Normal file
134
backend/runtime/manager.py
Normal file
@@ -0,0 +1,134 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, UTC
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from .agent_runtime import AgentRuntimeState
|
||||
from .context import TradingRunContext
|
||||
from .registry import RuntimeRegistry
|
||||
|
||||
_global_runtime_manager: Optional["TradingRuntimeManager"] = None
|
||||
|
||||
|
||||
def set_global_runtime_manager(manager: "TradingRuntimeManager") -> None:
|
||||
global _global_runtime_manager
|
||||
_global_runtime_manager = manager
|
||||
|
||||
|
||||
def clear_global_runtime_manager() -> None:
|
||||
global _global_runtime_manager
|
||||
_global_runtime_manager = None
|
||||
|
||||
|
||||
def get_global_runtime_manager() -> Optional["TradingRuntimeManager"]:
|
||||
return _global_runtime_manager
|
||||
|
||||
|
||||
class TradingRuntimeManager:
|
||||
def __init__(self, config_name: str, run_dir: Path, bootstrap: Optional[Dict[str, Any]] = None) -> None:
|
||||
self.config_name = config_name
|
||||
self.run_dir = run_dir
|
||||
self.bootstrap = bootstrap or {}
|
||||
self.context: Optional[TradingRunContext] = None
|
||||
self.registry = RuntimeRegistry()
|
||||
self.current_session_key: Optional[str] = None
|
||||
self.events: List[Dict[str, Any]] = []
|
||||
self.pending_approvals: Dict[str, Dict[str, Any]] = {}
|
||||
self.snapshot_path = self.run_dir / "state" / "runtime_state.json"
|
||||
|
||||
def prepare_run(self) -> TradingRunContext:
|
||||
self.run_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.context = TradingRunContext(
|
||||
config_name=self.config_name,
|
||||
run_dir=self.run_dir,
|
||||
bootstrap_values=self.bootstrap,
|
||||
)
|
||||
self._persist_snapshot()
|
||||
return self.context
|
||||
|
||||
def set_session_key(self, session_key: str) -> None:
|
||||
self.current_session_key = session_key
|
||||
self._persist_snapshot()
|
||||
|
||||
def log_event(self, event: str, details: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
entry = {
|
||||
"timestamp": datetime.now(UTC).isoformat(),
|
||||
"event": event,
|
||||
"details": details or {},
|
||||
"session": self.current_session_key,
|
||||
}
|
||||
self.events.append(entry)
|
||||
self._persist_snapshot()
|
||||
return entry
|
||||
|
||||
def register_agent(self, agent_id: str) -> AgentRuntimeState:
|
||||
state = AgentRuntimeState(agent_id=agent_id)
|
||||
self.registry.register(agent_id, state)
|
||||
self._persist_snapshot()
|
||||
return state
|
||||
|
||||
def register_pending_approval(self, approval_id: str, payload: Dict[str, Any]) -> None:
|
||||
payload.setdefault("status", "pending")
|
||||
payload.setdefault("created_at", datetime.now(UTC).isoformat())
|
||||
self.pending_approvals[approval_id] = payload
|
||||
self._persist_snapshot()
|
||||
|
||||
def update_agent_status(
|
||||
self,
|
||||
agent_id: str,
|
||||
status: str,
|
||||
session_key: Optional[str] = None,
|
||||
) -> AgentRuntimeState:
|
||||
state = self.registry.get(agent_id)
|
||||
if state is None:
|
||||
state = self.register_agent(agent_id)
|
||||
effective_session = session_key or self.current_session_key
|
||||
state.update(status, effective_session)
|
||||
self._persist_snapshot()
|
||||
return state
|
||||
|
||||
def get_agent_state(self, agent_id: str) -> Optional[AgentRuntimeState]:
|
||||
return self.registry.get(agent_id)
|
||||
|
||||
def list_agents(self) -> list[str]:
|
||||
return self.registry.list_agents()
|
||||
|
||||
def resolve_pending_approval(self, approval_id: str, resolved_by: str, status: str) -> None:
|
||||
entry = self.pending_approvals.get(approval_id)
|
||||
if not entry:
|
||||
return
|
||||
entry["status"] = status
|
||||
entry["resolved_at"] = datetime.now(UTC).isoformat()
|
||||
entry["resolved_by"] = resolved_by
|
||||
self._persist_snapshot()
|
||||
|
||||
def list_pending_approvals(self) -> List[Dict[str, Any]]:
|
||||
return list(self.pending_approvals.values())
|
||||
|
||||
def build_snapshot(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"context": {
|
||||
"config_name": self.context.config_name,
|
||||
"run_dir": str(self.context.run_dir),
|
||||
"bootstrap_values": self.context.bootstrap_values,
|
||||
}
|
||||
if self.context
|
||||
else None,
|
||||
"current_session_key": self.current_session_key,
|
||||
"agents": [
|
||||
state.to_dict()
|
||||
for agent_id in self.registry.list_agents()
|
||||
if (state := self.registry.get(agent_id)) is not None
|
||||
],
|
||||
"events": self.events,
|
||||
"pending_approvals": self.list_pending_approvals(),
|
||||
}
|
||||
|
||||
def _persist_snapshot(self) -> None:
|
||||
self.snapshot_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.snapshot_path.write_text(
|
||||
json.dumps(self.build_snapshot(), ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
20
backend/runtime/registry.py
Normal file
20
backend/runtime/registry.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
class RuntimeRegistry:
|
||||
def __init__(self) -> None:
|
||||
self._states: Dict[str, "AgentRuntimeState"] = {}
|
||||
|
||||
def register(self, agent_id: str, state: "AgentRuntimeState") -> None:
|
||||
self._states[agent_id] = state
|
||||
|
||||
def get(self, agent_id: str) -> Optional["AgentRuntimeState"]:
|
||||
return self._states.get(agent_id)
|
||||
|
||||
def list_agents(self) -> list[str]:
|
||||
return list(self._states.keys())
|
||||
|
||||
def clear(self) -> None:
|
||||
self._states.clear()
|
||||
14
backend/runtime/session.py
Normal file
14
backend/runtime/session.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TradingSessionKey:
|
||||
date: str
|
||||
ticker: str | None = None
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.date:
|
||||
raise ValueError("Session must have a date")
|
||||
|
||||
def key(self) -> str:
|
||||
return f"{self.date}:{self.ticker or 'all'}"
|
||||
Reference in New Issue
Block a user