2026-02-09 15:49:50 +05:30
|
|
|
"""Agent-scoped API routes for board operations and gateway coordination."""
|
|
|
|
|
|
2026-02-05 19:06:32 +05:30
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
from enum import Enum
|
2026-02-09 17:43:42 +05:30
|
|
|
from typing import TYPE_CHECKING, Any
|
2026-02-13 02:08:30 +05:30
|
|
|
from typing import cast
|
2026-02-09 23:55:52 +05:30
|
|
|
from uuid import UUID
|
2026-02-05 19:06:32 +05:30
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
2026-02-12 18:04:35 +05:30
|
|
|
from sqlalchemy import func
|
2026-02-08 00:46:10 +05:30
|
|
|
from sqlmodel import SQLModel, col, select
|
2026-02-05 19:06:32 +05:30
|
|
|
|
|
|
|
|
from app.api import agents as agents_api
|
|
|
|
|
from app.api import approvals as approvals_api
|
|
|
|
|
from app.api import board_memory as board_memory_api
|
|
|
|
|
from app.api import board_onboarding as onboarding_api
|
|
|
|
|
from app.api import tasks as tasks_api
|
|
|
|
|
from app.api.deps import ActorContext, get_board_or_404, get_task_or_404
|
|
|
|
|
from app.core.agent_auth import AgentAuthContext, get_agent_auth_context
|
2026-02-06 19:11:11 +05:30
|
|
|
from app.db.pagination import paginate
|
2026-02-05 19:06:32 +05:30
|
|
|
from app.db.session import get_session
|
2026-02-05 22:27:50 +05:30
|
|
|
from app.models.agents import Agent
|
2026-02-05 19:06:32 +05:30
|
|
|
from app.models.boards import Board
|
2026-02-12 18:35:48 +05:30
|
|
|
from app.models.tags import Tag
|
2026-02-07 00:21:44 +05:30
|
|
|
from app.models.task_dependencies import TaskDependency
|
2026-02-06 02:43:08 +05:30
|
|
|
from app.models.tasks import Task
|
2026-02-07 00:21:44 +05:30
|
|
|
from app.schemas.agents import (
|
|
|
|
|
AgentCreate,
|
|
|
|
|
AgentHeartbeat,
|
|
|
|
|
AgentHeartbeatCreate,
|
|
|
|
|
AgentNudge,
|
|
|
|
|
AgentRead,
|
|
|
|
|
)
|
2026-02-06 16:12:04 +05:30
|
|
|
from app.schemas.approvals import ApprovalCreate, ApprovalRead, ApprovalStatus
|
2026-02-05 19:06:32 +05:30
|
|
|
from app.schemas.board_memory import BoardMemoryCreate, BoardMemoryRead
|
2026-02-06 16:12:04 +05:30
|
|
|
from app.schemas.board_onboarding import BoardOnboardingAgentUpdate, BoardOnboardingRead
|
2026-02-05 19:06:32 +05:30
|
|
|
from app.schemas.boards import BoardRead
|
2026-02-06 16:12:04 +05:30
|
|
|
from app.schemas.common import OkResponse
|
2026-02-07 15:20:36 +05:30
|
|
|
from app.schemas.gateway_coordination import (
|
|
|
|
|
GatewayLeadBroadcastRequest,
|
|
|
|
|
GatewayLeadBroadcastResponse,
|
|
|
|
|
GatewayLeadMessageRequest,
|
|
|
|
|
GatewayLeadMessageResponse,
|
2026-02-07 16:21:31 +05:30
|
|
|
GatewayMainAskUserRequest,
|
|
|
|
|
GatewayMainAskUserResponse,
|
2026-02-07 15:20:36 +05:30
|
|
|
)
|
2026-02-06 19:11:11 +05:30
|
|
|
from app.schemas.pagination import DefaultLimitOffsetPage
|
2026-02-12 18:35:48 +05:30
|
|
|
from app.schemas.tags import TagRef
|
2026-02-09 20:44:05 +05:30
|
|
|
from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate
|
2026-02-05 22:27:50 +05:30
|
|
|
from app.services.activity_log import record_activity
|
2026-02-10 15:08:14 +05:30
|
|
|
from app.services.openclaw.coordination_service import GatewayCoordinationService
|
2026-02-10 15:44:49 +05:30
|
|
|
from app.services.openclaw.policies import OpenClawAuthorizationPolicy
|
2026-02-11 00:00:19 +05:30
|
|
|
from app.services.openclaw.provisioning_db import AgentLifecycleService
|
2026-02-12 18:35:48 +05:30
|
|
|
from app.services.tags import replace_tags, validate_tag_ids
|
2026-02-07 00:21:44 +05:30
|
|
|
from app.services.task_dependencies import (
|
|
|
|
|
blocked_by_dependency_ids,
|
|
|
|
|
dependency_status_by_id,
|
|
|
|
|
validate_dependency_update,
|
|
|
|
|
)
|
2026-02-05 19:06:32 +05:30
|
|
|
|
2026-02-09 15:49:50 +05:30
|
|
|
if TYPE_CHECKING:
|
2026-02-09 17:43:42 +05:30
|
|
|
from collections.abc import Sequence
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
from app.models.activity_events import ActivityEvent
|
|
|
|
|
from app.models.board_memory import BoardMemory
|
|
|
|
|
from app.models.board_onboarding import BoardOnboardingSession
|
|
|
|
|
|
2026-02-05 19:06:32 +05:30
|
|
|
router = APIRouter(prefix="/agent", tags=["agent"])
|
2026-02-09 15:49:50 +05:30
|
|
|
SESSION_DEP = Depends(get_session)
|
|
|
|
|
AGENT_CTX_DEP = Depends(get_agent_auth_context)
|
|
|
|
|
BOARD_DEP = Depends(get_board_or_404)
|
|
|
|
|
TASK_DEP = Depends(get_task_or_404)
|
|
|
|
|
BOARD_ID_QUERY = Query(default=None)
|
|
|
|
|
TASK_STATUS_QUERY = Query(default=None, alias="status")
|
|
|
|
|
IS_CHAT_QUERY = Query(default=None)
|
|
|
|
|
APPROVAL_STATUS_QUERY = Query(default=None, alias="status")
|
2026-02-08 00:46:10 +05:30
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
AGENT_LEAD_TAGS = cast("list[str | Enum]", ["agent-lead"])
|
|
|
|
|
AGENT_MAIN_TAGS = cast("list[str | Enum]", ["agent-main"])
|
|
|
|
|
AGENT_BOARD_TAGS = cast("list[str | Enum]", ["agent-lead", "agent-worker"])
|
|
|
|
|
AGENT_ALL_ROLE_TAGS = cast("list[str | Enum]", ["agent-lead", "agent-worker", "agent-main"])
|
|
|
|
|
|
2026-02-08 00:46:10 +05:30
|
|
|
|
2026-02-09 17:43:42 +05:30
|
|
|
def _coerce_agent_items(items: Sequence[Any]) -> list[Agent]:
|
|
|
|
|
agents: list[Agent] = []
|
|
|
|
|
for item in items:
|
|
|
|
|
if not isinstance(item, Agent):
|
|
|
|
|
msg = "Expected Agent items from paginated query"
|
|
|
|
|
raise TypeError(msg)
|
|
|
|
|
agents.append(item)
|
|
|
|
|
return agents
|
|
|
|
|
|
|
|
|
|
|
2026-02-08 00:46:10 +05:30
|
|
|
class SoulUpdateRequest(SQLModel):
|
2026-02-09 15:49:50 +05:30
|
|
|
"""Payload for updating an agent SOUL document."""
|
|
|
|
|
|
2026-02-08 00:46:10 +05:30
|
|
|
content: str
|
|
|
|
|
source_url: str | None = None
|
|
|
|
|
reason: str | None = None
|
|
|
|
|
|
2026-02-05 19:06:32 +05:30
|
|
|
|
2026-02-09 17:24:21 +05:30
|
|
|
class AgentTaskListFilters(SQLModel):
|
|
|
|
|
"""Query filters for board task listing in agent routes."""
|
|
|
|
|
|
|
|
|
|
status_filter: str | None = None
|
|
|
|
|
assigned_agent_id: UUID | None = None
|
|
|
|
|
unassigned: bool | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _task_list_filters(
|
|
|
|
|
status_filter: str | None = TASK_STATUS_QUERY,
|
|
|
|
|
assigned_agent_id: UUID | None = None,
|
|
|
|
|
unassigned: bool | None = None,
|
|
|
|
|
) -> AgentTaskListFilters:
|
|
|
|
|
return AgentTaskListFilters(
|
|
|
|
|
status_filter=status_filter,
|
|
|
|
|
assigned_agent_id=assigned_agent_id,
|
|
|
|
|
unassigned=unassigned,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TASK_LIST_FILTERS_DEP = Depends(_task_list_filters)
|
|
|
|
|
|
|
|
|
|
|
2026-02-05 19:06:32 +05:30
|
|
|
def _actor(agent_ctx: AgentAuthContext) -> ActorContext:
|
|
|
|
|
return ActorContext(actor_type="agent", agent=agent_ctx.agent)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _guard_board_access(agent_ctx: AgentAuthContext, board: Board) -> None:
|
2026-02-10 15:44:49 +05:30
|
|
|
allowed = not (agent_ctx.agent.board_id and agent_ctx.agent.board_id != board.id)
|
|
|
|
|
OpenClawAuthorizationPolicy.require_board_write_access(allowed=allowed)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _require_board_lead(agent_ctx: AgentAuthContext) -> Agent:
|
|
|
|
|
return OpenClawAuthorizationPolicy.require_board_lead_actor(
|
|
|
|
|
actor_agent=agent_ctx.agent,
|
|
|
|
|
detail="Only board leads can perform this action",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _guard_task_access(agent_ctx: AgentAuthContext, task: Task) -> None:
|
|
|
|
|
allowed = not (
|
|
|
|
|
agent_ctx.agent.board_id and task.board_id and agent_ctx.agent.board_id != task.board_id
|
|
|
|
|
)
|
|
|
|
|
OpenClawAuthorizationPolicy.require_board_write_access(allowed=allowed)
|
2026-02-05 19:06:32 +05:30
|
|
|
|
|
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
@router.get(
|
|
|
|
|
"/boards",
|
|
|
|
|
response_model=DefaultLimitOffsetPage[BoardRead],
|
|
|
|
|
tags=AGENT_ALL_ROLE_TAGS,
|
|
|
|
|
)
|
2026-02-06 16:12:04 +05:30
|
|
|
async def list_boards(
|
2026-02-09 15:49:50 +05:30
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-09 20:40:17 +05:30
|
|
|
) -> LimitOffsetPage[BoardRead]:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""List boards visible to the authenticated agent.
|
|
|
|
|
|
|
|
|
|
Board-scoped agents typically see only their assigned board.
|
|
|
|
|
Main agents may see multiple boards when permitted by auth scope.
|
|
|
|
|
"""
|
2026-02-06 19:11:11 +05:30
|
|
|
statement = select(Board)
|
2026-02-05 19:06:32 +05:30
|
|
|
if agent_ctx.agent.board_id:
|
2026-02-06 19:11:11 +05:30
|
|
|
statement = statement.where(col(Board.id) == agent_ctx.agent.board_id)
|
|
|
|
|
statement = statement.order_by(col(Board.created_at).desc())
|
|
|
|
|
return await paginate(session, statement)
|
2026-02-05 19:06:32 +05:30
|
|
|
|
|
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
@router.get("/boards/{board_id}", response_model=BoardRead, tags=AGENT_ALL_ROLE_TAGS)
|
2026-02-05 19:06:32 +05:30
|
|
|
def get_board(
|
2026-02-09 15:49:50 +05:30
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-05 19:06:32 +05:30
|
|
|
) -> Board:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""Return one board if the authenticated agent can access it.
|
|
|
|
|
|
|
|
|
|
Use this when an agent needs board metadata (objective, status, target date)
|
|
|
|
|
before planning or posting updates.
|
|
|
|
|
"""
|
2026-02-05 19:06:32 +05:30
|
|
|
_guard_board_access(agent_ctx, board)
|
|
|
|
|
return board
|
|
|
|
|
|
|
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
@router.get(
|
|
|
|
|
"/agents",
|
|
|
|
|
response_model=DefaultLimitOffsetPage[AgentRead],
|
|
|
|
|
tags=AGENT_ALL_ROLE_TAGS,
|
|
|
|
|
)
|
2026-02-06 16:12:04 +05:30
|
|
|
async def list_agents(
|
2026-02-09 15:49:50 +05:30
|
|
|
board_id: UUID | None = BOARD_ID_QUERY,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-09 20:40:17 +05:30
|
|
|
) -> LimitOffsetPage[AgentRead]:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""List agents visible to the caller, optionally filtered by board.
|
|
|
|
|
|
|
|
|
|
Useful for lead delegation and workload balancing.
|
|
|
|
|
"""
|
2026-02-05 22:27:50 +05:30
|
|
|
statement = select(Agent)
|
|
|
|
|
if agent_ctx.agent.board_id:
|
2026-02-10 15:44:49 +05:30
|
|
|
if board_id:
|
|
|
|
|
OpenClawAuthorizationPolicy.require_board_write_access(
|
|
|
|
|
allowed=board_id == agent_ctx.agent.board_id,
|
|
|
|
|
)
|
2026-02-05 22:27:50 +05:30
|
|
|
statement = statement.where(Agent.board_id == agent_ctx.agent.board_id)
|
|
|
|
|
elif board_id:
|
|
|
|
|
statement = statement.where(Agent.board_id == board_id)
|
2026-02-06 19:11:11 +05:30
|
|
|
statement = statement.order_by(col(Agent.created_at).desc())
|
2026-02-05 22:27:50 +05:30
|
|
|
|
2026-02-06 19:11:11 +05:30
|
|
|
def _transform(items: Sequence[Any]) -> Sequence[Any]:
|
2026-02-09 17:43:42 +05:30
|
|
|
agents = _coerce_agent_items(items)
|
2026-02-06 19:11:11 +05:30
|
|
|
return [
|
2026-02-10 14:50:27 +05:30
|
|
|
AgentLifecycleService.to_agent_read(
|
|
|
|
|
AgentLifecycleService.with_computed_status(agent),
|
2026-02-09 17:24:21 +05:30
|
|
|
)
|
2026-02-06 19:11:11 +05:30
|
|
|
for agent in agents
|
|
|
|
|
]
|
2026-02-05 22:27:50 +05:30
|
|
|
|
2026-02-06 19:11:11 +05:30
|
|
|
return await paginate(session, statement, transformer=_transform)
|
|
|
|
|
|
|
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
@router.get(
|
|
|
|
|
"/boards/{board_id}/tasks",
|
|
|
|
|
response_model=DefaultLimitOffsetPage[TaskRead],
|
|
|
|
|
tags=AGENT_BOARD_TAGS,
|
|
|
|
|
)
|
2026-02-09 17:24:21 +05:30
|
|
|
async def list_tasks(
|
|
|
|
|
filters: AgentTaskListFilters = TASK_LIST_FILTERS_DEP,
|
2026-02-09 15:49:50 +05:30
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-09 20:40:17 +05:30
|
|
|
) -> LimitOffsetPage[TaskRead]:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""List tasks on a board with status/assignment filters.
|
|
|
|
|
|
|
|
|
|
Common patterns:
|
|
|
|
|
- worker: fetch assigned inbox/in-progress tasks
|
|
|
|
|
- lead: fetch unassigned inbox tasks for delegation
|
|
|
|
|
"""
|
2026-02-05 19:06:32 +05:30
|
|
|
_guard_board_access(agent_ctx, board)
|
2026-02-06 16:12:04 +05:30
|
|
|
return await tasks_api.list_tasks(
|
2026-02-09 17:24:21 +05:30
|
|
|
status_filter=filters.status_filter,
|
|
|
|
|
assigned_agent_id=filters.assigned_agent_id,
|
|
|
|
|
unassigned=filters.unassigned,
|
2026-02-05 19:06:32 +05:30
|
|
|
board=board,
|
|
|
|
|
session=session,
|
2026-02-09 17:43:42 +05:30
|
|
|
_actor=_actor(agent_ctx),
|
2026-02-05 19:06:32 +05:30
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
@router.get("/boards/{board_id}/tags", response_model=list[TagRef], tags=AGENT_BOARD_TAGS)
|
2026-02-12 18:35:48 +05:30
|
|
|
async def list_tags(
|
2026-02-12 18:04:35 +05:30
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-12 18:35:48 +05:30
|
|
|
) -> list[TagRef]:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""List available tags for the board's organization.
|
|
|
|
|
|
|
|
|
|
Use returned ids in task create/update payloads (`tag_ids`).
|
|
|
|
|
"""
|
2026-02-12 18:04:35 +05:30
|
|
|
_guard_board_access(agent_ctx, board)
|
|
|
|
|
tags = (
|
|
|
|
|
await session.exec(
|
2026-02-12 18:35:48 +05:30
|
|
|
select(Tag)
|
|
|
|
|
.where(col(Tag.organization_id) == board.organization_id)
|
|
|
|
|
.order_by(func.lower(col(Tag.name)).asc(), col(Tag.created_at).asc()),
|
2026-02-12 18:04:35 +05:30
|
|
|
)
|
|
|
|
|
).all()
|
|
|
|
|
return [
|
2026-02-12 18:35:48 +05:30
|
|
|
TagRef(
|
2026-02-12 18:04:35 +05:30
|
|
|
id=tag.id,
|
|
|
|
|
name=tag.name,
|
|
|
|
|
slug=tag.slug,
|
|
|
|
|
color=tag.color,
|
|
|
|
|
)
|
|
|
|
|
for tag in tags
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
@router.post("/boards/{board_id}/tasks", response_model=TaskRead, tags=AGENT_LEAD_TAGS)
|
2026-02-06 16:12:04 +05:30
|
|
|
async def create_task(
|
2026-02-06 00:44:03 +05:30
|
|
|
payload: TaskCreate,
|
2026-02-09 15:49:50 +05:30
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-07 00:21:44 +05:30
|
|
|
) -> TaskRead:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""Create a task as the board lead.
|
|
|
|
|
|
|
|
|
|
Lead-only endpoint. Supports dependency-aware creation via
|
|
|
|
|
`depends_on_task_ids` and optional `tag_ids`.
|
|
|
|
|
"""
|
2026-02-06 00:44:03 +05:30
|
|
|
_guard_board_access(agent_ctx, board)
|
2026-02-10 15:44:49 +05:30
|
|
|
_require_board_lead(agent_ctx)
|
2026-02-12 18:04:35 +05:30
|
|
|
data = payload.model_dump(exclude={"depends_on_task_ids", "tag_ids"})
|
2026-02-09 17:43:42 +05:30
|
|
|
depends_on_task_ids = list(payload.depends_on_task_ids)
|
2026-02-12 18:04:35 +05:30
|
|
|
tag_ids = list(payload.tag_ids)
|
2026-02-07 00:21:44 +05:30
|
|
|
|
|
|
|
|
task = Task.model_validate(data)
|
2026-02-06 00:44:03 +05:30
|
|
|
task.board_id = board.id
|
|
|
|
|
task.auto_created = True
|
|
|
|
|
task.auto_reason = f"lead_agent:{agent_ctx.agent.id}"
|
2026-02-07 00:21:44 +05:30
|
|
|
|
|
|
|
|
normalized_deps = await validate_dependency_update(
|
|
|
|
|
session,
|
|
|
|
|
board_id=board.id,
|
|
|
|
|
task_id=task.id,
|
|
|
|
|
depends_on_task_ids=depends_on_task_ids,
|
|
|
|
|
)
|
2026-02-12 18:35:48 +05:30
|
|
|
normalized_tag_ids = await validate_tag_ids(
|
2026-02-12 18:04:35 +05:30
|
|
|
session,
|
|
|
|
|
organization_id=board.organization_id,
|
|
|
|
|
tag_ids=tag_ids,
|
|
|
|
|
)
|
2026-02-07 00:21:44 +05:30
|
|
|
dep_status = await dependency_status_by_id(
|
|
|
|
|
session,
|
|
|
|
|
board_id=board.id,
|
|
|
|
|
dependency_ids=normalized_deps,
|
|
|
|
|
)
|
2026-02-09 15:49:50 +05:30
|
|
|
blocked_by = blocked_by_dependency_ids(
|
2026-02-09 20:44:05 +05:30
|
|
|
dependency_ids=normalized_deps,
|
|
|
|
|
status_by_id=dep_status,
|
2026-02-09 15:49:50 +05:30
|
|
|
)
|
2026-02-07 00:21:44 +05:30
|
|
|
|
|
|
|
|
if blocked_by and (task.assigned_agent_id is not None or task.status != "inbox"):
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
|
|
|
detail={
|
|
|
|
|
"message": "Task is blocked by incomplete dependencies.",
|
|
|
|
|
"blocked_by_task_ids": [str(value) for value in blocked_by],
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-02-06 00:44:03 +05:30
|
|
|
if task.assigned_agent_id:
|
2026-02-09 02:04:14 +05:30
|
|
|
agent = await Agent.objects.by_id(task.assigned_agent_id).first(session)
|
2026-02-06 00:44:03 +05:30
|
|
|
if agent is None:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
if agent.is_board_lead:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
|
|
|
detail="Board leads cannot assign tasks to themselves.",
|
|
|
|
|
)
|
|
|
|
|
if agent.board_id and agent.board_id != board.id:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
|
|
|
|
session.add(task)
|
2026-02-07 02:42:33 +05:30
|
|
|
# Ensure the task exists in the DB before inserting dependency rows.
|
|
|
|
|
await session.flush()
|
2026-02-07 00:21:44 +05:30
|
|
|
for dep_id in normalized_deps:
|
|
|
|
|
session.add(
|
|
|
|
|
TaskDependency(
|
|
|
|
|
board_id=board.id,
|
|
|
|
|
task_id=task.id,
|
|
|
|
|
depends_on_task_id=dep_id,
|
2026-02-09 15:49:50 +05:30
|
|
|
),
|
2026-02-07 00:21:44 +05:30
|
|
|
)
|
2026-02-12 18:35:48 +05:30
|
|
|
await replace_tags(
|
2026-02-12 18:04:35 +05:30
|
|
|
session,
|
|
|
|
|
task_id=task.id,
|
|
|
|
|
tag_ids=normalized_tag_ids,
|
|
|
|
|
)
|
2026-02-06 16:12:04 +05:30
|
|
|
await session.commit()
|
|
|
|
|
await session.refresh(task)
|
2026-02-06 00:44:03 +05:30
|
|
|
record_activity(
|
|
|
|
|
session,
|
|
|
|
|
event_type="task.created",
|
|
|
|
|
task_id=task.id,
|
|
|
|
|
message=f"Task created by lead: {task.title}.",
|
|
|
|
|
agent_id=agent_ctx.agent.id,
|
|
|
|
|
)
|
2026-02-06 16:12:04 +05:30
|
|
|
await session.commit()
|
2026-02-06 00:44:03 +05:30
|
|
|
if task.assigned_agent_id:
|
2026-02-09 15:49:50 +05:30
|
|
|
assigned_agent = await Agent.objects.by_id(task.assigned_agent_id).first(
|
|
|
|
|
session,
|
|
|
|
|
)
|
2026-02-06 00:44:03 +05:30
|
|
|
if assigned_agent:
|
2026-02-09 17:24:21 +05:30
|
|
|
await tasks_api.notify_agent_on_task_assign(
|
2026-02-06 00:44:03 +05:30
|
|
|
session=session,
|
|
|
|
|
board=board,
|
|
|
|
|
task=task,
|
|
|
|
|
agent=assigned_agent,
|
|
|
|
|
)
|
2026-02-12 18:04:35 +05:30
|
|
|
return await tasks_api._task_read_response(
|
|
|
|
|
session,
|
|
|
|
|
task=task,
|
|
|
|
|
board_id=board.id,
|
2026-02-07 00:21:44 +05:30
|
|
|
)
|
2026-02-06 00:44:03 +05:30
|
|
|
|
|
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
@router.patch(
|
|
|
|
|
"/boards/{board_id}/tasks/{task_id}",
|
|
|
|
|
response_model=TaskRead,
|
|
|
|
|
tags=AGENT_BOARD_TAGS,
|
|
|
|
|
)
|
2026-02-06 16:12:04 +05:30
|
|
|
async def update_task(
|
2026-02-05 19:06:32 +05:30
|
|
|
payload: TaskUpdate,
|
2026-02-09 15:49:50 +05:30
|
|
|
task: Task = TASK_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-07 00:21:44 +05:30
|
|
|
) -> TaskRead:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""Update a task after board-level authorization checks.
|
|
|
|
|
|
|
|
|
|
Supports status, assignment, dependencies, and optional inline comment.
|
|
|
|
|
"""
|
2026-02-10 15:44:49 +05:30
|
|
|
_guard_task_access(agent_ctx, task)
|
2026-02-06 16:12:04 +05:30
|
|
|
return await tasks_api.update_task(
|
2026-02-05 19:06:32 +05:30
|
|
|
payload=payload,
|
|
|
|
|
task=task,
|
|
|
|
|
session=session,
|
|
|
|
|
actor=_actor(agent_ctx),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-02-06 19:11:11 +05:30
|
|
|
@router.get(
|
|
|
|
|
"/boards/{board_id}/tasks/{task_id}/comments",
|
|
|
|
|
response_model=DefaultLimitOffsetPage[TaskCommentRead],
|
2026-02-13 02:08:30 +05:30
|
|
|
tags=AGENT_BOARD_TAGS,
|
2026-02-06 19:11:11 +05:30
|
|
|
)
|
2026-02-06 16:12:04 +05:30
|
|
|
async def list_task_comments(
|
2026-02-09 15:49:50 +05:30
|
|
|
task: Task = TASK_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-09 20:40:17 +05:30
|
|
|
) -> LimitOffsetPage[TaskCommentRead]:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""List task comments visible to the authenticated agent.
|
|
|
|
|
|
|
|
|
|
Read this before posting updates to avoid duplicate or low-value comments.
|
|
|
|
|
"""
|
2026-02-10 15:44:49 +05:30
|
|
|
_guard_task_access(agent_ctx, task)
|
2026-02-06 16:12:04 +05:30
|
|
|
return await tasks_api.list_task_comments(
|
2026-02-05 19:06:32 +05:30
|
|
|
task=task,
|
|
|
|
|
session=session,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-02-09 15:49:50 +05:30
|
|
|
@router.post(
|
2026-02-09 20:44:05 +05:30
|
|
|
"/boards/{board_id}/tasks/{task_id}/comments",
|
|
|
|
|
response_model=TaskCommentRead,
|
2026-02-13 02:08:30 +05:30
|
|
|
tags=AGENT_BOARD_TAGS,
|
2026-02-09 15:49:50 +05:30
|
|
|
)
|
2026-02-06 16:12:04 +05:30
|
|
|
async def create_task_comment(
|
2026-02-05 19:06:32 +05:30
|
|
|
payload: TaskCommentCreate,
|
2026-02-09 15:49:50 +05:30
|
|
|
task: Task = TASK_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-06 16:12:04 +05:30
|
|
|
) -> ActivityEvent:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""Create a task comment as the authenticated agent.
|
|
|
|
|
|
|
|
|
|
This is the primary collaboration/log surface for task progress.
|
|
|
|
|
"""
|
2026-02-10 15:44:49 +05:30
|
|
|
_guard_task_access(agent_ctx, task)
|
2026-02-06 16:12:04 +05:30
|
|
|
return await tasks_api.create_task_comment(
|
2026-02-05 19:06:32 +05:30
|
|
|
payload=payload,
|
|
|
|
|
task=task,
|
|
|
|
|
session=session,
|
|
|
|
|
actor=_actor(agent_ctx),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-02-09 15:49:50 +05:30
|
|
|
@router.get(
|
2026-02-09 20:44:05 +05:30
|
|
|
"/boards/{board_id}/memory",
|
|
|
|
|
response_model=DefaultLimitOffsetPage[BoardMemoryRead],
|
2026-02-13 02:08:30 +05:30
|
|
|
tags=AGENT_BOARD_TAGS,
|
2026-02-09 15:49:50 +05:30
|
|
|
)
|
2026-02-06 16:12:04 +05:30
|
|
|
async def list_board_memory(
|
2026-02-09 15:49:50 +05:30
|
|
|
is_chat: bool | None = IS_CHAT_QUERY,
|
|
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-09 20:40:17 +05:30
|
|
|
) -> LimitOffsetPage[BoardMemoryRead]:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""List board memory with optional chat filtering.
|
|
|
|
|
|
|
|
|
|
Use `is_chat=false` for durable context and `is_chat=true` for board chat.
|
|
|
|
|
"""
|
2026-02-05 19:06:32 +05:30
|
|
|
_guard_board_access(agent_ctx, board)
|
2026-02-06 16:12:04 +05:30
|
|
|
return await board_memory_api.list_board_memory(
|
2026-02-06 21:56:16 +05:30
|
|
|
is_chat=is_chat,
|
2026-02-05 19:06:32 +05:30
|
|
|
board=board,
|
|
|
|
|
session=session,
|
2026-02-09 17:43:42 +05:30
|
|
|
_actor=_actor(agent_ctx),
|
2026-02-05 19:06:32 +05:30
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
@router.post("/boards/{board_id}/memory", response_model=BoardMemoryRead, tags=AGENT_BOARD_TAGS)
|
2026-02-06 16:12:04 +05:30
|
|
|
async def create_board_memory(
|
2026-02-05 19:06:32 +05:30
|
|
|
payload: BoardMemoryCreate,
|
2026-02-09 15:49:50 +05:30
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-06 16:12:04 +05:30
|
|
|
) -> BoardMemory:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""Create a board memory entry.
|
|
|
|
|
|
|
|
|
|
Use tags to indicate purpose (e.g. `chat`, `decision`, `plan`, `handoff`).
|
|
|
|
|
"""
|
2026-02-05 19:06:32 +05:30
|
|
|
_guard_board_access(agent_ctx, board)
|
2026-02-06 16:12:04 +05:30
|
|
|
return await board_memory_api.create_board_memory(
|
2026-02-05 19:06:32 +05:30
|
|
|
payload=payload,
|
|
|
|
|
board=board,
|
|
|
|
|
session=session,
|
|
|
|
|
actor=_actor(agent_ctx),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-02-06 19:11:11 +05:30
|
|
|
@router.get(
|
|
|
|
|
"/boards/{board_id}/approvals",
|
|
|
|
|
response_model=DefaultLimitOffsetPage[ApprovalRead],
|
2026-02-13 02:08:30 +05:30
|
|
|
tags=AGENT_BOARD_TAGS,
|
2026-02-06 19:11:11 +05:30
|
|
|
)
|
2026-02-06 16:12:04 +05:30
|
|
|
async def list_approvals(
|
2026-02-09 15:49:50 +05:30
|
|
|
status_filter: ApprovalStatus | None = APPROVAL_STATUS_QUERY,
|
|
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-09 20:40:17 +05:30
|
|
|
) -> LimitOffsetPage[ApprovalRead]:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""List approvals for a board.
|
|
|
|
|
|
|
|
|
|
Use status filtering to process pending approvals efficiently.
|
|
|
|
|
"""
|
2026-02-05 19:06:32 +05:30
|
|
|
_guard_board_access(agent_ctx, board)
|
2026-02-06 16:12:04 +05:30
|
|
|
return await approvals_api.list_approvals(
|
2026-02-05 19:06:32 +05:30
|
|
|
status_filter=status_filter,
|
|
|
|
|
board=board,
|
|
|
|
|
session=session,
|
2026-02-09 17:43:42 +05:30
|
|
|
_actor=_actor(agent_ctx),
|
2026-02-05 19:06:32 +05:30
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
@router.post("/boards/{board_id}/approvals", response_model=ApprovalRead, tags=AGENT_BOARD_TAGS)
|
2026-02-06 16:12:04 +05:30
|
|
|
async def create_approval(
|
2026-02-05 19:06:32 +05:30
|
|
|
payload: ApprovalCreate,
|
2026-02-09 15:49:50 +05:30
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-11 20:30:14 +05:30
|
|
|
) -> ApprovalRead:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""Create an approval request for risky or low-confidence actions.
|
|
|
|
|
|
|
|
|
|
Include `task_id` or `task_ids` to scope the decision precisely.
|
|
|
|
|
"""
|
2026-02-05 19:06:32 +05:30
|
|
|
_guard_board_access(agent_ctx, board)
|
2026-02-06 16:12:04 +05:30
|
|
|
return await approvals_api.create_approval(
|
2026-02-05 19:06:32 +05:30
|
|
|
payload=payload,
|
|
|
|
|
board=board,
|
|
|
|
|
session=session,
|
2026-02-09 17:43:42 +05:30
|
|
|
_actor=_actor(agent_ctx),
|
2026-02-05 19:06:32 +05:30
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
@router.post(
|
|
|
|
|
"/boards/{board_id}/onboarding",
|
|
|
|
|
response_model=BoardOnboardingRead,
|
|
|
|
|
tags=AGENT_BOARD_TAGS,
|
|
|
|
|
)
|
2026-02-06 16:12:04 +05:30
|
|
|
async def update_onboarding(
|
|
|
|
|
payload: BoardOnboardingAgentUpdate,
|
2026-02-09 15:49:50 +05:30
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-06 16:12:04 +05:30
|
|
|
) -> BoardOnboardingSession:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""Apply board onboarding updates from an agent workflow.
|
|
|
|
|
|
|
|
|
|
Used during structured objective/success-metric intake loops.
|
|
|
|
|
"""
|
2026-02-05 19:06:32 +05:30
|
|
|
_guard_board_access(agent_ctx, board)
|
2026-02-06 16:12:04 +05:30
|
|
|
return await onboarding_api.agent_onboarding_update(
|
2026-02-05 19:06:32 +05:30
|
|
|
payload=payload,
|
|
|
|
|
board=board,
|
|
|
|
|
session=session,
|
|
|
|
|
actor=_actor(agent_ctx),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
@router.post("/agents", response_model=AgentRead, tags=AGENT_LEAD_TAGS)
|
2026-02-05 22:27:50 +05:30
|
|
|
async def create_agent(
|
2026-02-05 19:06:32 +05:30
|
|
|
payload: AgentCreate,
|
2026-02-09 15:49:50 +05:30
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-05 19:06:32 +05:30
|
|
|
) -> AgentRead:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""Create a new board agent as lead.
|
|
|
|
|
|
|
|
|
|
The new agent is always forced onto the caller's board (`board_id` override).
|
|
|
|
|
"""
|
2026-02-10 15:44:49 +05:30
|
|
|
lead = _require_board_lead(agent_ctx)
|
2026-02-09 15:49:50 +05:30
|
|
|
payload = AgentCreate(
|
2026-02-10 15:44:49 +05:30
|
|
|
**{**payload.model_dump(), "board_id": lead.board_id},
|
2026-02-09 15:49:50 +05:30
|
|
|
)
|
2026-02-05 22:27:50 +05:30
|
|
|
return await agents_api.create_agent(
|
2026-02-05 19:06:32 +05:30
|
|
|
payload=payload,
|
|
|
|
|
session=session,
|
|
|
|
|
actor=_actor(agent_ctx),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
@router.post(
|
|
|
|
|
"/boards/{board_id}/agents/{agent_id}/nudge",
|
|
|
|
|
response_model=OkResponse,
|
|
|
|
|
tags=AGENT_LEAD_TAGS,
|
|
|
|
|
)
|
2026-02-06 16:12:04 +05:30
|
|
|
async def nudge_agent(
|
2026-02-05 22:27:50 +05:30
|
|
|
payload: AgentNudge,
|
|
|
|
|
agent_id: str,
|
2026-02-09 15:49:50 +05:30
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-06 16:12:04 +05:30
|
|
|
) -> OkResponse:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""Send a direct nudge to one board agent.
|
|
|
|
|
|
|
|
|
|
Lead-only endpoint for stale or blocked in-progress work.
|
|
|
|
|
"""
|
2026-02-05 22:27:50 +05:30
|
|
|
_guard_board_access(agent_ctx, board)
|
2026-02-10 15:44:49 +05:30
|
|
|
_require_board_lead(agent_ctx)
|
2026-02-10 14:50:27 +05:30
|
|
|
coordination = GatewayCoordinationService(session)
|
|
|
|
|
await coordination.nudge_board_agent(
|
|
|
|
|
board=board,
|
|
|
|
|
actor_agent=agent_ctx.agent,
|
|
|
|
|
target_agent_id=agent_id,
|
|
|
|
|
message=payload.message,
|
|
|
|
|
correlation_id=f"nudge:{board.id}:{agent_id}",
|
2026-02-05 22:27:50 +05:30
|
|
|
)
|
2026-02-06 16:12:04 +05:30
|
|
|
return OkResponse()
|
2026-02-05 22:27:50 +05:30
|
|
|
|
|
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
@router.post("/heartbeat", response_model=AgentRead, tags=AGENT_ALL_ROLE_TAGS)
|
2026-02-05 19:06:32 +05:30
|
|
|
async def agent_heartbeat(
|
|
|
|
|
payload: AgentHeartbeatCreate,
|
2026-02-09 15:49:50 +05:30
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-05 19:06:32 +05:30
|
|
|
) -> AgentRead:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""Record heartbeat status for the authenticated agent.
|
|
|
|
|
|
|
|
|
|
Heartbeats are identity-bound to the token's agent id.
|
|
|
|
|
"""
|
2026-02-06 11:50:14 +05:30
|
|
|
# Heartbeats must apply to the authenticated agent; agent names are not unique.
|
2026-02-06 16:12:04 +05:30
|
|
|
return await agents_api.heartbeat_agent(
|
2026-02-06 11:50:14 +05:30
|
|
|
agent_id=str(agent_ctx.agent.id),
|
|
|
|
|
payload=AgentHeartbeat(status=payload.status),
|
2026-02-05 19:06:32 +05:30
|
|
|
session=session,
|
|
|
|
|
actor=_actor(agent_ctx),
|
|
|
|
|
)
|
2026-02-07 15:20:36 +05:30
|
|
|
|
|
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
@router.get(
|
|
|
|
|
"/boards/{board_id}/agents/{agent_id}/soul",
|
|
|
|
|
response_model=str,
|
|
|
|
|
tags=AGENT_BOARD_TAGS,
|
|
|
|
|
)
|
2026-02-08 00:46:10 +05:30
|
|
|
async def get_agent_soul(
|
|
|
|
|
agent_id: str,
|
2026-02-09 15:49:50 +05:30
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-08 00:46:10 +05:30
|
|
|
) -> str:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""Fetch an agent's SOUL.md content.
|
|
|
|
|
|
|
|
|
|
Allowed for board lead, or for an agent reading its own SOUL.
|
|
|
|
|
"""
|
2026-02-08 00:46:10 +05:30
|
|
|
_guard_board_access(agent_ctx, board)
|
2026-02-10 15:44:49 +05:30
|
|
|
OpenClawAuthorizationPolicy.require_board_lead_or_same_actor(
|
|
|
|
|
actor_agent=agent_ctx.agent,
|
|
|
|
|
target_agent_id=agent_id,
|
|
|
|
|
)
|
2026-02-10 14:50:27 +05:30
|
|
|
coordination = GatewayCoordinationService(session)
|
|
|
|
|
return await coordination.get_agent_soul(
|
|
|
|
|
board=board,
|
|
|
|
|
target_agent_id=agent_id,
|
|
|
|
|
correlation_id=f"soul.read:{board.id}:{agent_id}",
|
2026-02-09 15:49:50 +05:30
|
|
|
)
|
2026-02-08 00:46:10 +05:30
|
|
|
|
|
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
@router.put(
|
|
|
|
|
"/boards/{board_id}/agents/{agent_id}/soul",
|
|
|
|
|
response_model=OkResponse,
|
|
|
|
|
tags=AGENT_LEAD_TAGS,
|
|
|
|
|
)
|
2026-02-08 00:46:10 +05:30
|
|
|
async def update_agent_soul(
|
|
|
|
|
agent_id: str,
|
|
|
|
|
payload: SoulUpdateRequest,
|
2026-02-09 15:49:50 +05:30
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-08 00:46:10 +05:30
|
|
|
) -> OkResponse:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""Update an agent's SOUL.md template in DB and gateway.
|
|
|
|
|
|
|
|
|
|
Lead-only endpoint. Persists as `soul_template` for future reprovisioning.
|
|
|
|
|
"""
|
2026-02-08 00:46:10 +05:30
|
|
|
_guard_board_access(agent_ctx, board)
|
2026-02-10 15:44:49 +05:30
|
|
|
_require_board_lead(agent_ctx)
|
2026-02-10 14:50:27 +05:30
|
|
|
coordination = GatewayCoordinationService(session)
|
|
|
|
|
await coordination.update_agent_soul(
|
|
|
|
|
board=board,
|
|
|
|
|
target_agent_id=agent_id,
|
|
|
|
|
content=payload.content,
|
|
|
|
|
reason=payload.reason,
|
|
|
|
|
source_url=payload.source_url,
|
|
|
|
|
actor_agent_id=agent_ctx.agent.id,
|
|
|
|
|
correlation_id=f"soul.write:{board.id}:{agent_id}",
|
2026-02-08 00:46:10 +05:30
|
|
|
)
|
|
|
|
|
return OkResponse()
|
|
|
|
|
|
|
|
|
|
|
2026-02-13 02:08:30 +05:30
|
|
|
@router.delete(
|
|
|
|
|
"/boards/{board_id}/agents/{agent_id}",
|
|
|
|
|
response_model=OkResponse,
|
|
|
|
|
tags=AGENT_LEAD_TAGS,
|
|
|
|
|
)
|
2026-02-12 02:54:56 +05:30
|
|
|
async def delete_board_agent(
|
|
|
|
|
agent_id: str,
|
|
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> OkResponse:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""Delete a board agent as board lead.
|
|
|
|
|
|
|
|
|
|
Cleans up runtime/session state through lifecycle services.
|
|
|
|
|
"""
|
2026-02-12 02:54:56 +05:30
|
|
|
_guard_board_access(agent_ctx, board)
|
|
|
|
|
_require_board_lead(agent_ctx)
|
|
|
|
|
service = AgentLifecycleService(session)
|
|
|
|
|
return await service.delete_agent_as_lead(
|
|
|
|
|
agent_id=agent_id,
|
|
|
|
|
actor_agent=agent_ctx.agent,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-02-07 16:21:31 +05:30
|
|
|
@router.post(
|
|
|
|
|
"/boards/{board_id}/gateway/main/ask-user",
|
|
|
|
|
response_model=GatewayMainAskUserResponse,
|
2026-02-13 02:08:30 +05:30
|
|
|
tags=AGENT_LEAD_TAGS,
|
2026-02-07 16:21:31 +05:30
|
|
|
)
|
|
|
|
|
async def ask_user_via_gateway_main(
|
|
|
|
|
payload: GatewayMainAskUserRequest,
|
2026-02-09 15:49:50 +05:30
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-07 16:21:31 +05:30
|
|
|
) -> GatewayMainAskUserResponse:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""Ask the human via gateway-main external channels.
|
|
|
|
|
|
|
|
|
|
Lead-only endpoint for situations where board chat is not responsive.
|
|
|
|
|
"""
|
2026-02-07 16:21:31 +05:30
|
|
|
_guard_board_access(agent_ctx, board)
|
2026-02-10 15:44:49 +05:30
|
|
|
_require_board_lead(agent_ctx)
|
2026-02-10 14:50:27 +05:30
|
|
|
coordination = GatewayCoordinationService(session)
|
|
|
|
|
return await coordination.ask_user_via_gateway_main(
|
|
|
|
|
board=board,
|
|
|
|
|
payload=payload,
|
|
|
|
|
actor_agent=agent_ctx.agent,
|
2026-02-07 16:21:31 +05:30
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-02-07 15:20:36 +05:30
|
|
|
@router.post(
|
|
|
|
|
"/gateway/boards/{board_id}/lead/message",
|
|
|
|
|
response_model=GatewayLeadMessageResponse,
|
2026-02-13 02:08:30 +05:30
|
|
|
tags=AGENT_MAIN_TAGS,
|
2026-02-07 15:20:36 +05:30
|
|
|
)
|
|
|
|
|
async def message_gateway_board_lead(
|
|
|
|
|
board_id: UUID,
|
|
|
|
|
payload: GatewayLeadMessageRequest,
|
2026-02-09 15:49:50 +05:30
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-07 15:20:36 +05:30
|
|
|
) -> GatewayLeadMessageResponse:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""Send a gateway-main control message to one board lead."""
|
2026-02-10 14:50:27 +05:30
|
|
|
coordination = GatewayCoordinationService(session)
|
|
|
|
|
return await coordination.message_gateway_board_lead(
|
|
|
|
|
actor_agent=agent_ctx.agent,
|
|
|
|
|
board_id=board_id,
|
|
|
|
|
payload=payload,
|
2026-02-07 15:20:36 +05:30
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/gateway/leads/broadcast",
|
|
|
|
|
response_model=GatewayLeadBroadcastResponse,
|
2026-02-13 02:08:30 +05:30
|
|
|
tags=AGENT_MAIN_TAGS,
|
2026-02-07 15:20:36 +05:30
|
|
|
)
|
|
|
|
|
async def broadcast_gateway_lead_message(
|
|
|
|
|
payload: GatewayLeadBroadcastRequest,
|
2026-02-09 15:49:50 +05:30
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
2026-02-07 15:20:36 +05:30
|
|
|
) -> GatewayLeadBroadcastResponse:
|
2026-02-13 02:08:30 +05:30
|
|
|
"""Broadcast a gateway-main control message to multiple board leads."""
|
2026-02-10 14:50:27 +05:30
|
|
|
coordination = GatewayCoordinationService(session)
|
|
|
|
|
return await coordination.broadcast_gateway_lead_message(
|
|
|
|
|
actor_agent=agent_ctx.agent,
|
|
|
|
|
payload=payload,
|
2026-02-07 15:20:36 +05:30
|
|
|
)
|