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:
@@ -19,6 +19,8 @@ from backend.utils.settlement import SettlementCoordinator
|
||||
from backend.utils.terminal_dashboard import get_dashboard
|
||||
from backend.core.state_sync import StateSync
|
||||
from backend.utils.trade_executor import PortfolioTradeExecutor
|
||||
from backend.runtime.manager import TradingRuntimeManager
|
||||
from backend.runtime.session import TradingSessionKey
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -46,6 +48,8 @@ class TradingPipeline:
|
||||
6. Reflection phase: broadcast closing P&L, agents record to long-term memory
|
||||
|
||||
Real-time updates via StateSync after each agent completes.
|
||||
|
||||
Supports both legacy agent lists and new workspace-based agent loading.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -56,6 +60,9 @@ class TradingPipeline:
|
||||
state_sync: Optional["StateSync"] = None,
|
||||
settlement_coordinator: Optional[SettlementCoordinator] = None,
|
||||
max_comm_cycles: Optional[int] = None,
|
||||
workspace_id: Optional[str] = None,
|
||||
agent_factory: Optional[Any] = None,
|
||||
runtime_manager: Optional[TradingRuntimeManager] = None,
|
||||
):
|
||||
self.analysts = analysts
|
||||
self.risk_manager = risk_manager
|
||||
@@ -66,6 +73,10 @@ class TradingPipeline:
|
||||
os.getenv("MAX_COMM_CYCLES", "2"),
|
||||
)
|
||||
self.conference_summary = None # Store latest conference summary
|
||||
self.workspace_id = workspace_id
|
||||
self.agent_factory = agent_factory
|
||||
self.runtime_manager = runtime_manager
|
||||
self._session_key: Optional[str] = None
|
||||
|
||||
async def run_cycle(
|
||||
self,
|
||||
@@ -80,6 +91,7 @@ class TradingPipeline:
|
||||
get_close_prices_fn: Optional[
|
||||
Callable[[], Awaitable[Dict[str, float]]]
|
||||
] = None,
|
||||
execute_decisions: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Run one complete trading cycle
|
||||
@@ -101,6 +113,12 @@ class TradingPipeline:
|
||||
Each agent's result is broadcast immediately via StateSync.
|
||||
"""
|
||||
_log(f"Starting cycle {date} - {len(tickers)} tickers")
|
||||
session_key = TradingSessionKey(date=date).key()
|
||||
self._session_key = session_key
|
||||
if self.runtime_manager:
|
||||
self.runtime_manager.set_session_key(session_key)
|
||||
self._runtime_log_event("cycle:start", {"tickers": tickers, "date": date})
|
||||
self._runtime_batch_status(self.analysts, "analysis_in_progress")
|
||||
|
||||
# Phase 0: Clear short-term memory to avoid cross-day context pollution
|
||||
_log("Phase 0: Clearing memory")
|
||||
@@ -123,6 +141,7 @@ class TradingPipeline:
|
||||
|
||||
# Phase 1.2: Risk Manager
|
||||
_log("Phase 1.2: Risk assessment")
|
||||
self._runtime_update_status(self.risk_manager, "risk_assessment")
|
||||
risk_assessment = await self._run_risk_manager_with_sync(
|
||||
tickers,
|
||||
date,
|
||||
@@ -161,6 +180,7 @@ class TradingPipeline:
|
||||
|
||||
# Phase 3: PM makes decisions
|
||||
_log("Phase 3.1: PM makes decisions")
|
||||
self._runtime_update_status(self.pm, "decision_phase")
|
||||
pm_result = await self._run_pm_with_sync(
|
||||
tickers,
|
||||
date,
|
||||
@@ -169,10 +189,17 @@ class TradingPipeline:
|
||||
risk_assessment,
|
||||
)
|
||||
|
||||
# Phase 4: Execute decisions
|
||||
_log("Phase 4: Executing trades")
|
||||
decisions = pm_result.get("decisions", {})
|
||||
execution_result = self._execute_decisions(decisions, prices, date)
|
||||
execution_result = {
|
||||
"executed_trades": [],
|
||||
"portfolio": self.pm.get_portfolio_state(),
|
||||
}
|
||||
if execute_decisions:
|
||||
_log("Phase 4: Executing trades")
|
||||
self._runtime_update_status(self.pm, "executing")
|
||||
execution_result = self._execute_decisions(decisions, prices, date)
|
||||
else:
|
||||
_log("Phase 4: Skipping trade execution")
|
||||
|
||||
# Live mode: wait for market close before settlement
|
||||
if get_close_prices_fn:
|
||||
@@ -184,6 +211,10 @@ class TradingPipeline:
|
||||
settlement_result = None
|
||||
if close_prices and self.settlement_coordinator:
|
||||
_log("Phase 5: Daily review and generate memories")
|
||||
self._runtime_batch_status(
|
||||
[self.risk_manager] + self.analysts + [self.pm],
|
||||
"settlement",
|
||||
)
|
||||
|
||||
agent_trajectories = await self._capture_agent_trajectories()
|
||||
|
||||
@@ -214,8 +245,17 @@ class TradingPipeline:
|
||||
settlement_result=settlement_result,
|
||||
conference_summary=self.conference_summary,
|
||||
)
|
||||
self._runtime_batch_status(
|
||||
[self.risk_manager] + self.analysts + [self.pm],
|
||||
"reflection",
|
||||
)
|
||||
|
||||
_log(f"Cycle complete: {date}")
|
||||
self._runtime_batch_status(
|
||||
self.analysts + [self.risk_manager, self.pm],
|
||||
"idle",
|
||||
)
|
||||
self._runtime_log_event("cycle:end", {"tickers": tickers, "date": date})
|
||||
|
||||
return {
|
||||
"analyst_results": analyst_results,
|
||||
@@ -1306,3 +1346,122 @@ class TradingPipeline:
|
||||
if decision_texts:
|
||||
return "Decisions: " + "; ".join(decision_texts)
|
||||
return "Portfolio analysis completed. No trades recommended."
|
||||
|
||||
def load_agents_from_workspace(
|
||||
self,
|
||||
workspace_id: str,
|
||||
agent_factory: Optional[Any] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Load agents from workspace using AgentFactory.
|
||||
|
||||
This method supports the new EvoAgent architecture by loading
|
||||
agents from a workspace instead of using hardcoded agents.
|
||||
|
||||
Args:
|
||||
workspace_id: Workspace identifier
|
||||
agent_factory: Optional AgentFactory instance (uses self.agent_factory if None)
|
||||
|
||||
Returns:
|
||||
Dictionary with loaded agents:
|
||||
{
|
||||
"analysts": List[EvoAgent],
|
||||
"risk_manager": EvoAgent,
|
||||
"portfolio_manager": EvoAgent,
|
||||
}
|
||||
|
||||
Raises:
|
||||
ValueError: If workspace doesn't exist or no agents found
|
||||
"""
|
||||
factory = agent_factory or self.agent_factory
|
||||
if factory is None:
|
||||
from backend.agents import AgentFactory
|
||||
factory = AgentFactory()
|
||||
|
||||
# Check workspace exists
|
||||
if not factory.workspaces_root.exists():
|
||||
raise ValueError(f"Workspaces root does not exist: {factory.workspaces_root}")
|
||||
|
||||
workspace_dir = factory.workspaces_root / workspace_id
|
||||
if not workspace_dir.exists():
|
||||
raise ValueError(f"Workspace '{workspace_id}' does not exist")
|
||||
|
||||
# Load agents from workspace
|
||||
agents_data = factory.list_agents(workspace_id=workspace_id)
|
||||
|
||||
if not agents_data:
|
||||
raise ValueError(f"No agents found in workspace '{workspace_id}'")
|
||||
|
||||
# Categorize agents by type
|
||||
analysts = []
|
||||
risk_manager = None
|
||||
portfolio_manager = None
|
||||
|
||||
for agent_data in agents_data:
|
||||
agent_type = agent_data.get("agent_type", "unknown")
|
||||
agent_id = agent_data.get("agent_id")
|
||||
|
||||
# Load full agent configuration
|
||||
config_path = Path(agent_data.get("config_path", ""))
|
||||
if config_path.exists():
|
||||
agent = factory.load_agent(agent_id, workspace_id)
|
||||
|
||||
if agent_type.endswith("_analyst"):
|
||||
analysts.append(agent)
|
||||
elif agent_type == "risk_manager":
|
||||
risk_manager = agent
|
||||
elif agent_type == "portfolio_manager":
|
||||
portfolio_manager = agent
|
||||
|
||||
if not analysts:
|
||||
raise ValueError(f"No analysts found in workspace '{workspace_id}'")
|
||||
if risk_manager is None:
|
||||
raise ValueError(f"No risk_manager found in workspace '{workspace_id}'")
|
||||
if portfolio_manager is None:
|
||||
raise ValueError(f"No portfolio_manager found in workspace '{workspace_id}'")
|
||||
|
||||
return {
|
||||
"analysts": analysts,
|
||||
"risk_manager": risk_manager,
|
||||
"portfolio_manager": portfolio_manager,
|
||||
}
|
||||
|
||||
def reload_agents_from_workspace(self, workspace_id: Optional[str] = None) -> None:
|
||||
"""
|
||||
Reload all agents from workspace.
|
||||
|
||||
This updates self.analysts, self.risk_manager, and self.pm
|
||||
with agents loaded from the specified workspace.
|
||||
|
||||
Args:
|
||||
workspace_id: Workspace ID (uses self.workspace_id if None)
|
||||
"""
|
||||
ws_id = workspace_id or self.workspace_id
|
||||
if not ws_id:
|
||||
raise ValueError("No workspace_id specified")
|
||||
|
||||
loaded = self.load_agents_from_workspace(ws_id)
|
||||
|
||||
self.analysts = loaded["analysts"]
|
||||
self.risk_manager = loaded["risk_manager"]
|
||||
self.pm = loaded["portfolio_manager"]
|
||||
self.workspace_id = ws_id
|
||||
|
||||
logger.info(f"Reloaded {len(self.analysts)} analysts from workspace '{ws_id}'")
|
||||
|
||||
def _runtime_update_status(self, agent: Any, status: str) -> None:
|
||||
if not self.runtime_manager:
|
||||
return
|
||||
agent_id = getattr(agent, "agent_id", None) or getattr(agent, "name", None)
|
||||
if not agent_id:
|
||||
return
|
||||
self.runtime_manager.update_agent_status(agent_id, status, self._session_key)
|
||||
|
||||
def _runtime_batch_status(self, agents: List[Any], status: str) -> None:
|
||||
for agent in agents:
|
||||
self._runtime_update_status(agent, status)
|
||||
|
||||
def _runtime_log_event(self, event: str, details: Optional[Dict[str, Any]] = None) -> None:
|
||||
if not self.runtime_manager:
|
||||
return
|
||||
self.runtime_manager.log_event(event, details)
|
||||
|
||||
Reference in New Issue
Block a user