108 lines
3.6 KiB
Python
108 lines
3.6 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from sqlmodel import col, select
|
||
|
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||
|
|
|
||
|
|
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.models.boards import Board
|
||
|
|
from app.models.gateways import Gateway
|
||
|
|
from app.models.users import User
|
||
|
|
from app.services.agent_provisioning import DEFAULT_HEARTBEAT_CONFIG, provision_agent
|
||
|
|
|
||
|
|
|
||
|
|
def lead_session_key(board: Board) -> str:
|
||
|
|
return f"agent:lead-{board.id}:main"
|
||
|
|
|
||
|
|
|
||
|
|
def lead_agent_name(_: Board) -> str:
|
||
|
|
return "Lead Agent"
|
||
|
|
|
||
|
|
|
||
|
|
async def ensure_board_lead_agent(
|
||
|
|
session: AsyncSession,
|
||
|
|
*,
|
||
|
|
board: Board,
|
||
|
|
gateway: Gateway,
|
||
|
|
config: GatewayClientConfig,
|
||
|
|
user: User | None,
|
||
|
|
agent_name: str | None = None,
|
||
|
|
identity_profile: dict[str, str] | None = None,
|
||
|
|
action: str = "provision",
|
||
|
|
) -> tuple[Agent, bool]:
|
||
|
|
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 = agent_name or lead_agent_name(board)
|
||
|
|
changed = False
|
||
|
|
if existing.name != desired_name:
|
||
|
|
existing.name = desired_name
|
||
|
|
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 identity_profile:
|
||
|
|
merged_identity_profile.update(
|
||
|
|
{key: value.strip() for key, value in identity_profile.items() if value.strip()}
|
||
|
|
)
|
||
|
|
|
||
|
|
agent = Agent(
|
||
|
|
name=agent_name or lead_agent_name(board),
|
||
|
|
status="provisioning",
|
||
|
|
board_id=board.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=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, board, gateway, raw_token, user, action=action)
|
||
|
|
if agent.openclaw_session_id:
|
||
|
|
await ensure_session(agent.openclaw_session_id, config=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=config,
|
||
|
|
deliver=True,
|
||
|
|
)
|
||
|
|
except OpenClawGatewayError:
|
||
|
|
# Best-effort provisioning. The board/agent rows should still exist.
|
||
|
|
pass
|
||
|
|
|
||
|
|
return agent, True
|