2026-02-10 14:50:27 +05:30
|
|
|
"""Thin API wrappers for async agent lifecycle operations."""
|
2026-02-09 15:49:50 +05:30
|
|
|
|
2026-02-04 02:28:51 +05:30
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-02-09 17:24:21 +05:30
|
|
|
from dataclasses import dataclass
|
2026-02-10 14:50:27 +05:30
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
from uuid import UUID
|
2026-02-04 02:28:51 +05:30
|
|
|
|
2026-02-10 14:50:27 +05:30
|
|
|
from fastapi import APIRouter, Depends, Query, Request
|
2026-02-05 22:51:46 +05:30
|
|
|
from sse_starlette.sse import EventSourceResponse
|
2026-02-04 02:28:51 +05:30
|
|
|
|
2026-03-03 22:45:38 -07:00
|
|
|
from app.api.deps import ActorContext, require_org_admin, require_user_or_agent
|
2026-02-08 21:16:26 +05:30
|
|
|
from app.core.auth import AuthContext, get_auth_context
|
2026-02-10 14:50:27 +05:30
|
|
|
from app.db.session import get_session
|
2026-02-06 02:43:08 +05:30
|
|
|
from app.schemas.agents import (
|
|
|
|
|
AgentCreate,
|
|
|
|
|
AgentHeartbeat,
|
|
|
|
|
AgentHeartbeatCreate,
|
|
|
|
|
AgentRead,
|
|
|
|
|
AgentUpdate,
|
|
|
|
|
)
|
2026-02-07 00:21:44 +05:30
|
|
|
from app.schemas.common import OkResponse
|
2026-02-06 19:11:11 +05:30
|
|
|
from app.schemas.pagination import DefaultLimitOffsetPage
|
2026-02-11 00:00:19 +05:30
|
|
|
from app.services.openclaw.provisioning_db import AgentLifecycleService, AgentUpdateOptions
|
2026-02-10 14:50:27 +05:30
|
|
|
from app.services.organizations import OrganizationContext
|
2026-02-04 02:28:51 +05:30
|
|
|
|
2026-02-09 15:49:50 +05:30
|
|
|
if TYPE_CHECKING:
|
2026-02-09 20:40:17 +05:30
|
|
|
from fastapi_pagination.limit_offset import LimitOffsetPage
|
2026-02-09 15:49:50 +05:30
|
|
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
|
|
|
|
2026-02-04 02:28:51 +05:30
|
|
|
router = APIRouter(prefix="/agents", tags=["agents"])
|
|
|
|
|
|
2026-02-09 15:49:50 +05:30
|
|
|
BOARD_ID_QUERY = Query(default=None)
|
|
|
|
|
GATEWAY_ID_QUERY = Query(default=None)
|
|
|
|
|
SINCE_QUERY = Query(default=None)
|
|
|
|
|
SESSION_DEP = Depends(get_session)
|
|
|
|
|
ORG_ADMIN_DEP = Depends(require_org_admin)
|
2026-03-03 21:41:56 -07:00
|
|
|
ACTOR_DEP = Depends(require_user_or_agent)
|
2026-02-09 15:49:50 +05:30
|
|
|
AUTH_DEP = Depends(get_auth_context)
|
2026-02-04 02:28:51 +05:30
|
|
|
|
|
|
|
|
|
2026-02-09 17:24:21 +05:30
|
|
|
@dataclass(frozen=True, slots=True)
|
|
|
|
|
class _AgentUpdateParams:
|
|
|
|
|
force: bool
|
|
|
|
|
auth: AuthContext
|
|
|
|
|
ctx: OrganizationContext
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _agent_update_params(
|
|
|
|
|
*,
|
|
|
|
|
force: bool = False,
|
|
|
|
|
auth: AuthContext = AUTH_DEP,
|
|
|
|
|
ctx: OrganizationContext = ORG_ADMIN_DEP,
|
|
|
|
|
) -> _AgentUpdateParams:
|
|
|
|
|
return _AgentUpdateParams(force=force, auth=auth, ctx=ctx)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
AGENT_UPDATE_PARAMS_DEP = Depends(_agent_update_params)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("", response_model=DefaultLimitOffsetPage[AgentRead])
|
|
|
|
|
async def list_agents(
|
|
|
|
|
board_id: UUID | None = BOARD_ID_QUERY,
|
|
|
|
|
gateway_id: UUID | None = GATEWAY_ID_QUERY,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
ctx: OrganizationContext = ORG_ADMIN_DEP,
|
2026-02-09 20:40:17 +05:30
|
|
|
) -> LimitOffsetPage[AgentRead]:
|
2026-02-09 17:24:21 +05:30
|
|
|
"""List agents visible to the active organization admin."""
|
2026-02-10 14:50:27 +05:30
|
|
|
service = AgentLifecycleService(session)
|
|
|
|
|
return await service.list_agents(
|
|
|
|
|
board_id=board_id,
|
|
|
|
|
gateway_id=gateway_id,
|
|
|
|
|
ctx=ctx,
|
|
|
|
|
)
|
2026-02-09 17:24:21 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/stream")
|
|
|
|
|
async def stream_agents(
|
|
|
|
|
request: Request,
|
|
|
|
|
board_id: UUID | None = BOARD_ID_QUERY,
|
|
|
|
|
since: str | None = SINCE_QUERY,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
ctx: OrganizationContext = ORG_ADMIN_DEP,
|
|
|
|
|
) -> EventSourceResponse:
|
|
|
|
|
"""Stream agent updates as SSE events."""
|
2026-02-10 14:50:27 +05:30
|
|
|
service = AgentLifecycleService(session)
|
|
|
|
|
return await service.stream_agents(
|
|
|
|
|
request=request,
|
|
|
|
|
board_id=board_id,
|
|
|
|
|
since=since,
|
|
|
|
|
ctx=ctx,
|
|
|
|
|
)
|
2026-02-09 17:24:21 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("", response_model=AgentRead)
|
|
|
|
|
async def create_agent(
|
|
|
|
|
payload: AgentCreate,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
actor: ActorContext = ACTOR_DEP,
|
|
|
|
|
) -> AgentRead:
|
|
|
|
|
"""Create and provision an agent."""
|
2026-02-10 14:50:27 +05:30
|
|
|
service = AgentLifecycleService(session)
|
|
|
|
|
return await service.create_agent(payload=payload, actor=actor)
|
2026-02-09 17:24:21 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{agent_id}", response_model=AgentRead)
|
|
|
|
|
async def get_agent(
|
|
|
|
|
agent_id: str,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
ctx: OrganizationContext = ORG_ADMIN_DEP,
|
|
|
|
|
) -> AgentRead:
|
|
|
|
|
"""Get a single agent by id."""
|
2026-02-10 14:50:27 +05:30
|
|
|
service = AgentLifecycleService(session)
|
|
|
|
|
return await service.get_agent(agent_id=agent_id, ctx=ctx)
|
2026-02-09 17:24:21 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/{agent_id}", response_model=AgentRead)
|
|
|
|
|
async def update_agent(
|
|
|
|
|
agent_id: str,
|
|
|
|
|
payload: AgentUpdate,
|
|
|
|
|
params: _AgentUpdateParams = AGENT_UPDATE_PARAMS_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
) -> AgentRead:
|
|
|
|
|
"""Update agent metadata and optionally reprovision."""
|
2026-02-10 14:50:27 +05:30
|
|
|
service = AgentLifecycleService(session)
|
|
|
|
|
return await service.update_agent(
|
|
|
|
|
agent_id=agent_id,
|
|
|
|
|
payload=payload,
|
|
|
|
|
options=AgentUpdateOptions(
|
|
|
|
|
force=params.force,
|
|
|
|
|
user=params.auth.user,
|
|
|
|
|
context=params.ctx,
|
|
|
|
|
),
|
2026-02-09 17:24:21 +05:30
|
|
|
)
|
2026-02-04 02:28:51 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{agent_id}/heartbeat", response_model=AgentRead)
|
2026-02-06 16:12:04 +05:30
|
|
|
async def heartbeat_agent(
|
2026-02-04 02:28:51 +05:30
|
|
|
agent_id: str,
|
|
|
|
|
payload: AgentHeartbeat,
|
2026-02-09 15:49:50 +05:30
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
actor: ActorContext = ACTOR_DEP,
|
2026-02-06 11:50:14 +05:30
|
|
|
) -> AgentRead:
|
2026-02-09 15:49:50 +05:30
|
|
|
"""Record a heartbeat for a specific agent."""
|
2026-02-10 14:50:27 +05:30
|
|
|
service = AgentLifecycleService(session)
|
|
|
|
|
return await service.heartbeat_agent(agent_id=agent_id, payload=payload, actor=actor)
|
2026-02-04 02:28:51 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/heartbeat", response_model=AgentRead)
|
2026-02-09 17:24:21 +05:30
|
|
|
async def heartbeat_or_create_agent(
|
2026-02-04 02:28:51 +05:30
|
|
|
payload: AgentHeartbeatCreate,
|
2026-02-09 15:49:50 +05:30
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
actor: ActorContext = ACTOR_DEP,
|
2026-02-06 11:50:14 +05:30
|
|
|
) -> AgentRead:
|
2026-02-09 15:49:50 +05:30
|
|
|
"""Heartbeat an existing agent or create/provision one if needed."""
|
2026-02-10 14:50:27 +05:30
|
|
|
service = AgentLifecycleService(session)
|
|
|
|
|
return await service.heartbeat_or_create_agent(payload=payload, actor=actor)
|
2026-02-04 02:28:51 +05:30
|
|
|
|
|
|
|
|
|
2026-02-06 16:12:04 +05:30
|
|
|
@router.delete("/{agent_id}", response_model=OkResponse)
|
|
|
|
|
async def delete_agent(
|
2026-02-04 02:28:51 +05:30
|
|
|
agent_id: str,
|
2026-02-09 15:49:50 +05:30
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
ctx: OrganizationContext = ORG_ADMIN_DEP,
|
2026-02-06 16:12:04 +05:30
|
|
|
) -> OkResponse:
|
2026-02-09 15:49:50 +05:30
|
|
|
"""Delete an agent and clean related task state."""
|
2026-02-10 14:50:27 +05:30
|
|
|
service = AgentLifecycleService(session)
|
|
|
|
|
return await service.delete_agent(agent_id=agent_id, ctx=ctx)
|