"""Helpers for ensuring each board has a provisioned lead agent.""" from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any from sqlmodel import col, select from app.core.agent_tokens import generate_agent_token, hash_agent_token from app.core.time import utcnow from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message from app.models.agents import Agent from app.services.agent_provisioning import ( DEFAULT_HEARTBEAT_CONFIG, AgentProvisionRequest, ProvisionOptions, provision_agent, ) if TYPE_CHECKING: from sqlmodel.ext.asyncio.session import AsyncSession from app.models.boards import Board from app.models.gateways import Gateway from app.models.users import User def lead_session_key(board: Board) -> str: """Return the deterministic main session key for a board lead agent.""" return f"agent:lead-{board.id}:main" def lead_agent_name(_: Board) -> str: """Return the default display name for board lead agents.""" return "Lead Agent" @dataclass(frozen=True, slots=True) class LeadAgentOptions: """Optional overrides for board-lead provisioning behavior.""" agent_name: str | None = None identity_profile: dict[str, str] | None = None action: str = "provision" @dataclass(frozen=True, slots=True) class LeadAgentRequest: """Inputs required to ensure or provision a board lead agent.""" board: Board gateway: Gateway config: GatewayClientConfig user: User | None options: LeadAgentOptions = field(default_factory=LeadAgentOptions) async def ensure_board_lead_agent( session: AsyncSession, *, request: LeadAgentRequest, ) -> tuple[Agent, bool]: """Ensure a board has a lead agent; return `(agent, created)`.""" board = request.board config_options = request.options existing = ( await session.exec( select(Agent) .where(Agent.board_id == board.id) .where(col(Agent.is_board_lead).is_(True)), ) ).first() if existing: desired_name = config_options.agent_name or lead_agent_name(board) changed = False if existing.name != desired_name: existing.name = desired_name changed = True if existing.gateway_id != request.gateway.id: existing.gateway_id = request.gateway.id changed = True desired_session_key = lead_session_key(board) if not existing.openclaw_session_id: existing.openclaw_session_id = desired_session_key changed = True if changed: existing.updated_at = utcnow() session.add(existing) await session.commit() await session.refresh(existing) return existing, False merged_identity_profile: dict[str, Any] = { "role": "Board Lead", "communication_style": "direct, concise, practical", "emoji": ":gear:", } if config_options.identity_profile: merged_identity_profile.update( { key: value.strip() for key, value in config_options.identity_profile.items() if value.strip() }, ) agent = Agent( name=config_options.agent_name or lead_agent_name(board), status="provisioning", board_id=board.id, gateway_id=request.gateway.id, is_board_lead=True, heartbeat_config=DEFAULT_HEARTBEAT_CONFIG.copy(), identity_profile=merged_identity_profile, openclaw_session_id=lead_session_key(board), provision_requested_at=utcnow(), provision_action=config_options.action, ) raw_token = generate_agent_token() agent.agent_token_hash = hash_agent_token(raw_token) session.add(agent) await session.commit() await session.refresh(agent) try: await provision_agent( agent, AgentProvisionRequest( board=board, gateway=request.gateway, auth_token=raw_token, user=request.user, options=ProvisionOptions(action=config_options.action), ), ) if agent.openclaw_session_id: await ensure_session( agent.openclaw_session_id, config=request.config, label=agent.name, ) await send_message( ( f"Hello {agent.name}. Your workspace has been provisioned.\n\n" "Start the agent, run BOOT.md, and if BOOTSTRAP.md exists run " "it once " "then delete it. Begin heartbeats after startup." ), session_key=agent.openclaw_session_id, config=request.config, deliver=True, ) except OpenClawGatewayError: # Best-effort provisioning. The board/agent rows should still exist. pass return agent, True