|
|
|
|
@@ -2,7 +2,9 @@
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from enum import Enum
|
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
|
from typing import cast
|
|
|
|
|
from uuid import UUID
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
|
|
|
@@ -77,6 +79,11 @@ TASK_STATUS_QUERY = Query(default=None, alias="status")
|
|
|
|
|
IS_CHAT_QUERY = Query(default=None)
|
|
|
|
|
APPROVAL_STATUS_QUERY = Query(default=None, alias="status")
|
|
|
|
|
|
|
|
|
|
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"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _coerce_agent_items(items: Sequence[Any]) -> list[Agent]:
|
|
|
|
|
agents: list[Agent] = []
|
|
|
|
|
@@ -142,12 +149,20 @@ def _guard_task_access(agent_ctx: AgentAuthContext, task: Task) -> None:
|
|
|
|
|
OpenClawAuthorizationPolicy.require_board_write_access(allowed=allowed)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/boards", response_model=DefaultLimitOffsetPage[BoardRead])
|
|
|
|
|
@router.get(
|
|
|
|
|
"/boards",
|
|
|
|
|
response_model=DefaultLimitOffsetPage[BoardRead],
|
|
|
|
|
tags=AGENT_ALL_ROLE_TAGS,
|
|
|
|
|
)
|
|
|
|
|
async def list_boards(
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> LimitOffsetPage[BoardRead]:
|
|
|
|
|
"""List boards visible to the authenticated agent."""
|
|
|
|
|
"""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.
|
|
|
|
|
"""
|
|
|
|
|
statement = select(Board)
|
|
|
|
|
if agent_ctx.agent.board_id:
|
|
|
|
|
statement = statement.where(col(Board.id) == agent_ctx.agent.board_id)
|
|
|
|
|
@@ -155,23 +170,34 @@ async def list_boards(
|
|
|
|
|
return await paginate(session, statement)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/boards/{board_id}", response_model=BoardRead)
|
|
|
|
|
@router.get("/boards/{board_id}", response_model=BoardRead, tags=AGENT_ALL_ROLE_TAGS)
|
|
|
|
|
def get_board(
|
|
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> Board:
|
|
|
|
|
"""Return a board if the authenticated agent can access it."""
|
|
|
|
|
"""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.
|
|
|
|
|
"""
|
|
|
|
|
_guard_board_access(agent_ctx, board)
|
|
|
|
|
return board
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/agents", response_model=DefaultLimitOffsetPage[AgentRead])
|
|
|
|
|
@router.get(
|
|
|
|
|
"/agents",
|
|
|
|
|
response_model=DefaultLimitOffsetPage[AgentRead],
|
|
|
|
|
tags=AGENT_ALL_ROLE_TAGS,
|
|
|
|
|
)
|
|
|
|
|
async def list_agents(
|
|
|
|
|
board_id: UUID | None = BOARD_ID_QUERY,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> LimitOffsetPage[AgentRead]:
|
|
|
|
|
"""List agents, optionally filtered to a board."""
|
|
|
|
|
"""List agents visible to the caller, optionally filtered by board.
|
|
|
|
|
|
|
|
|
|
Useful for lead delegation and workload balancing.
|
|
|
|
|
"""
|
|
|
|
|
statement = select(Agent)
|
|
|
|
|
if agent_ctx.agent.board_id:
|
|
|
|
|
if board_id:
|
|
|
|
|
@@ -195,14 +221,23 @@ async def list_agents(
|
|
|
|
|
return await paginate(session, statement, transformer=_transform)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/boards/{board_id}/tasks", response_model=DefaultLimitOffsetPage[TaskRead])
|
|
|
|
|
@router.get(
|
|
|
|
|
"/boards/{board_id}/tasks",
|
|
|
|
|
response_model=DefaultLimitOffsetPage[TaskRead],
|
|
|
|
|
tags=AGENT_BOARD_TAGS,
|
|
|
|
|
)
|
|
|
|
|
async def list_tasks(
|
|
|
|
|
filters: AgentTaskListFilters = TASK_LIST_FILTERS_DEP,
|
|
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> LimitOffsetPage[TaskRead]:
|
|
|
|
|
"""List tasks on a board with optional status and assignment filters."""
|
|
|
|
|
"""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
|
|
|
|
|
"""
|
|
|
|
|
_guard_board_access(agent_ctx, board)
|
|
|
|
|
return await tasks_api.list_tasks(
|
|
|
|
|
status_filter=filters.status_filter,
|
|
|
|
|
@@ -214,13 +249,16 @@ async def list_tasks(
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/boards/{board_id}/tags", response_model=list[TagRef])
|
|
|
|
|
@router.get("/boards/{board_id}/tags", response_model=list[TagRef], tags=AGENT_BOARD_TAGS)
|
|
|
|
|
async def list_tags(
|
|
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> list[TagRef]:
|
|
|
|
|
"""List tags available to the board's organization."""
|
|
|
|
|
"""List available tags for the board's organization.
|
|
|
|
|
|
|
|
|
|
Use returned ids in task create/update payloads (`tag_ids`).
|
|
|
|
|
"""
|
|
|
|
|
_guard_board_access(agent_ctx, board)
|
|
|
|
|
tags = (
|
|
|
|
|
await session.exec(
|
|
|
|
|
@@ -240,14 +278,18 @@ async def list_tags(
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/boards/{board_id}/tasks", response_model=TaskRead)
|
|
|
|
|
@router.post("/boards/{board_id}/tasks", response_model=TaskRead, tags=AGENT_LEAD_TAGS)
|
|
|
|
|
async def create_task(
|
|
|
|
|
payload: TaskCreate,
|
|
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> TaskRead:
|
|
|
|
|
"""Create a task on the board as the lead agent."""
|
|
|
|
|
"""Create a task as the board lead.
|
|
|
|
|
|
|
|
|
|
Lead-only endpoint. Supports dependency-aware creation via
|
|
|
|
|
`depends_on_task_ids` and optional `tag_ids`.
|
|
|
|
|
"""
|
|
|
|
|
_guard_board_access(agent_ctx, board)
|
|
|
|
|
_require_board_lead(agent_ctx)
|
|
|
|
|
data = payload.model_dump(exclude={"depends_on_task_ids", "tag_ids"})
|
|
|
|
|
@@ -343,14 +385,21 @@ async def create_task(
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/boards/{board_id}/tasks/{task_id}", response_model=TaskRead)
|
|
|
|
|
@router.patch(
|
|
|
|
|
"/boards/{board_id}/tasks/{task_id}",
|
|
|
|
|
response_model=TaskRead,
|
|
|
|
|
tags=AGENT_BOARD_TAGS,
|
|
|
|
|
)
|
|
|
|
|
async def update_task(
|
|
|
|
|
payload: TaskUpdate,
|
|
|
|
|
task: Task = TASK_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> TaskRead:
|
|
|
|
|
"""Update a task after board-level access checks."""
|
|
|
|
|
"""Update a task after board-level authorization checks.
|
|
|
|
|
|
|
|
|
|
Supports status, assignment, dependencies, and optional inline comment.
|
|
|
|
|
"""
|
|
|
|
|
_guard_task_access(agent_ctx, task)
|
|
|
|
|
return await tasks_api.update_task(
|
|
|
|
|
payload=payload,
|
|
|
|
|
@@ -363,13 +412,17 @@ async def update_task(
|
|
|
|
|
@router.get(
|
|
|
|
|
"/boards/{board_id}/tasks/{task_id}/comments",
|
|
|
|
|
response_model=DefaultLimitOffsetPage[TaskCommentRead],
|
|
|
|
|
tags=AGENT_BOARD_TAGS,
|
|
|
|
|
)
|
|
|
|
|
async def list_task_comments(
|
|
|
|
|
task: Task = TASK_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> LimitOffsetPage[TaskCommentRead]:
|
|
|
|
|
"""List comments for a task visible to the authenticated agent."""
|
|
|
|
|
"""List task comments visible to the authenticated agent.
|
|
|
|
|
|
|
|
|
|
Read this before posting updates to avoid duplicate or low-value comments.
|
|
|
|
|
"""
|
|
|
|
|
_guard_task_access(agent_ctx, task)
|
|
|
|
|
return await tasks_api.list_task_comments(
|
|
|
|
|
task=task,
|
|
|
|
|
@@ -380,6 +433,7 @@ async def list_task_comments(
|
|
|
|
|
@router.post(
|
|
|
|
|
"/boards/{board_id}/tasks/{task_id}/comments",
|
|
|
|
|
response_model=TaskCommentRead,
|
|
|
|
|
tags=AGENT_BOARD_TAGS,
|
|
|
|
|
)
|
|
|
|
|
async def create_task_comment(
|
|
|
|
|
payload: TaskCommentCreate,
|
|
|
|
|
@@ -387,7 +441,10 @@ async def create_task_comment(
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> ActivityEvent:
|
|
|
|
|
"""Create a task comment on behalf of the authenticated agent."""
|
|
|
|
|
"""Create a task comment as the authenticated agent.
|
|
|
|
|
|
|
|
|
|
This is the primary collaboration/log surface for task progress.
|
|
|
|
|
"""
|
|
|
|
|
_guard_task_access(agent_ctx, task)
|
|
|
|
|
return await tasks_api.create_task_comment(
|
|
|
|
|
payload=payload,
|
|
|
|
|
@@ -400,6 +457,7 @@ async def create_task_comment(
|
|
|
|
|
@router.get(
|
|
|
|
|
"/boards/{board_id}/memory",
|
|
|
|
|
response_model=DefaultLimitOffsetPage[BoardMemoryRead],
|
|
|
|
|
tags=AGENT_BOARD_TAGS,
|
|
|
|
|
)
|
|
|
|
|
async def list_board_memory(
|
|
|
|
|
is_chat: bool | None = IS_CHAT_QUERY,
|
|
|
|
|
@@ -407,7 +465,10 @@ async def list_board_memory(
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> LimitOffsetPage[BoardMemoryRead]:
|
|
|
|
|
"""List board memory entries with optional chat filtering."""
|
|
|
|
|
"""List board memory with optional chat filtering.
|
|
|
|
|
|
|
|
|
|
Use `is_chat=false` for durable context and `is_chat=true` for board chat.
|
|
|
|
|
"""
|
|
|
|
|
_guard_board_access(agent_ctx, board)
|
|
|
|
|
return await board_memory_api.list_board_memory(
|
|
|
|
|
is_chat=is_chat,
|
|
|
|
|
@@ -417,14 +478,17 @@ async def list_board_memory(
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/boards/{board_id}/memory", response_model=BoardMemoryRead)
|
|
|
|
|
@router.post("/boards/{board_id}/memory", response_model=BoardMemoryRead, tags=AGENT_BOARD_TAGS)
|
|
|
|
|
async def create_board_memory(
|
|
|
|
|
payload: BoardMemoryCreate,
|
|
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> BoardMemory:
|
|
|
|
|
"""Create a board memory entry."""
|
|
|
|
|
"""Create a board memory entry.
|
|
|
|
|
|
|
|
|
|
Use tags to indicate purpose (e.g. `chat`, `decision`, `plan`, `handoff`).
|
|
|
|
|
"""
|
|
|
|
|
_guard_board_access(agent_ctx, board)
|
|
|
|
|
return await board_memory_api.create_board_memory(
|
|
|
|
|
payload=payload,
|
|
|
|
|
@@ -437,6 +501,7 @@ async def create_board_memory(
|
|
|
|
|
@router.get(
|
|
|
|
|
"/boards/{board_id}/approvals",
|
|
|
|
|
response_model=DefaultLimitOffsetPage[ApprovalRead],
|
|
|
|
|
tags=AGENT_BOARD_TAGS,
|
|
|
|
|
)
|
|
|
|
|
async def list_approvals(
|
|
|
|
|
status_filter: ApprovalStatus | None = APPROVAL_STATUS_QUERY,
|
|
|
|
|
@@ -444,7 +509,10 @@ async def list_approvals(
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> LimitOffsetPage[ApprovalRead]:
|
|
|
|
|
"""List approvals for a board."""
|
|
|
|
|
"""List approvals for a board.
|
|
|
|
|
|
|
|
|
|
Use status filtering to process pending approvals efficiently.
|
|
|
|
|
"""
|
|
|
|
|
_guard_board_access(agent_ctx, board)
|
|
|
|
|
return await approvals_api.list_approvals(
|
|
|
|
|
status_filter=status_filter,
|
|
|
|
|
@@ -454,14 +522,17 @@ async def list_approvals(
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/boards/{board_id}/approvals", response_model=ApprovalRead)
|
|
|
|
|
@router.post("/boards/{board_id}/approvals", response_model=ApprovalRead, tags=AGENT_BOARD_TAGS)
|
|
|
|
|
async def create_approval(
|
|
|
|
|
payload: ApprovalCreate,
|
|
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> ApprovalRead:
|
|
|
|
|
"""Create a board approval request."""
|
|
|
|
|
"""Create an approval request for risky or low-confidence actions.
|
|
|
|
|
|
|
|
|
|
Include `task_id` or `task_ids` to scope the decision precisely.
|
|
|
|
|
"""
|
|
|
|
|
_guard_board_access(agent_ctx, board)
|
|
|
|
|
return await approvals_api.create_approval(
|
|
|
|
|
payload=payload,
|
|
|
|
|
@@ -471,14 +542,21 @@ async def create_approval(
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/boards/{board_id}/onboarding", response_model=BoardOnboardingRead)
|
|
|
|
|
@router.post(
|
|
|
|
|
"/boards/{board_id}/onboarding",
|
|
|
|
|
response_model=BoardOnboardingRead,
|
|
|
|
|
tags=AGENT_BOARD_TAGS,
|
|
|
|
|
)
|
|
|
|
|
async def update_onboarding(
|
|
|
|
|
payload: BoardOnboardingAgentUpdate,
|
|
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> BoardOnboardingSession:
|
|
|
|
|
"""Apply onboarding updates for a board."""
|
|
|
|
|
"""Apply board onboarding updates from an agent workflow.
|
|
|
|
|
|
|
|
|
|
Used during structured objective/success-metric intake loops.
|
|
|
|
|
"""
|
|
|
|
|
_guard_board_access(agent_ctx, board)
|
|
|
|
|
return await onboarding_api.agent_onboarding_update(
|
|
|
|
|
payload=payload,
|
|
|
|
|
@@ -488,13 +566,16 @@ async def update_onboarding(
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/agents", response_model=AgentRead)
|
|
|
|
|
@router.post("/agents", response_model=AgentRead, tags=AGENT_LEAD_TAGS)
|
|
|
|
|
async def create_agent(
|
|
|
|
|
payload: AgentCreate,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> AgentRead:
|
|
|
|
|
"""Create an agent on the caller's board."""
|
|
|
|
|
"""Create a new board agent as lead.
|
|
|
|
|
|
|
|
|
|
The new agent is always forced onto the caller's board (`board_id` override).
|
|
|
|
|
"""
|
|
|
|
|
lead = _require_board_lead(agent_ctx)
|
|
|
|
|
payload = AgentCreate(
|
|
|
|
|
**{**payload.model_dump(), "board_id": lead.board_id},
|
|
|
|
|
@@ -506,7 +587,11 @@ async def create_agent(
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/boards/{board_id}/agents/{agent_id}/nudge", response_model=OkResponse)
|
|
|
|
|
@router.post(
|
|
|
|
|
"/boards/{board_id}/agents/{agent_id}/nudge",
|
|
|
|
|
response_model=OkResponse,
|
|
|
|
|
tags=AGENT_LEAD_TAGS,
|
|
|
|
|
)
|
|
|
|
|
async def nudge_agent(
|
|
|
|
|
payload: AgentNudge,
|
|
|
|
|
agent_id: str,
|
|
|
|
|
@@ -514,7 +599,10 @@ async def nudge_agent(
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> OkResponse:
|
|
|
|
|
"""Send a direct nudge message to a board agent."""
|
|
|
|
|
"""Send a direct nudge to one board agent.
|
|
|
|
|
|
|
|
|
|
Lead-only endpoint for stale or blocked in-progress work.
|
|
|
|
|
"""
|
|
|
|
|
_guard_board_access(agent_ctx, board)
|
|
|
|
|
_require_board_lead(agent_ctx)
|
|
|
|
|
coordination = GatewayCoordinationService(session)
|
|
|
|
|
@@ -528,13 +616,16 @@ async def nudge_agent(
|
|
|
|
|
return OkResponse()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/heartbeat", response_model=AgentRead)
|
|
|
|
|
@router.post("/heartbeat", response_model=AgentRead, tags=AGENT_ALL_ROLE_TAGS)
|
|
|
|
|
async def agent_heartbeat(
|
|
|
|
|
payload: AgentHeartbeatCreate,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> AgentRead:
|
|
|
|
|
"""Record heartbeat status for the authenticated agent."""
|
|
|
|
|
"""Record heartbeat status for the authenticated agent.
|
|
|
|
|
|
|
|
|
|
Heartbeats are identity-bound to the token's agent id.
|
|
|
|
|
"""
|
|
|
|
|
# Heartbeats must apply to the authenticated agent; agent names are not unique.
|
|
|
|
|
return await agents_api.heartbeat_agent(
|
|
|
|
|
agent_id=str(agent_ctx.agent.id),
|
|
|
|
|
@@ -544,14 +635,21 @@ async def agent_heartbeat(
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/boards/{board_id}/agents/{agent_id}/soul", response_model=str)
|
|
|
|
|
@router.get(
|
|
|
|
|
"/boards/{board_id}/agents/{agent_id}/soul",
|
|
|
|
|
response_model=str,
|
|
|
|
|
tags=AGENT_BOARD_TAGS,
|
|
|
|
|
)
|
|
|
|
|
async def get_agent_soul(
|
|
|
|
|
agent_id: str,
|
|
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> str:
|
|
|
|
|
"""Fetch the target agent's SOUL.md content from the gateway."""
|
|
|
|
|
"""Fetch an agent's SOUL.md content.
|
|
|
|
|
|
|
|
|
|
Allowed for board lead, or for an agent reading its own SOUL.
|
|
|
|
|
"""
|
|
|
|
|
_guard_board_access(agent_ctx, board)
|
|
|
|
|
OpenClawAuthorizationPolicy.require_board_lead_or_same_actor(
|
|
|
|
|
actor_agent=agent_ctx.agent,
|
|
|
|
|
@@ -565,7 +663,11 @@ async def get_agent_soul(
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/boards/{board_id}/agents/{agent_id}/soul", response_model=OkResponse)
|
|
|
|
|
@router.put(
|
|
|
|
|
"/boards/{board_id}/agents/{agent_id}/soul",
|
|
|
|
|
response_model=OkResponse,
|
|
|
|
|
tags=AGENT_LEAD_TAGS,
|
|
|
|
|
)
|
|
|
|
|
async def update_agent_soul(
|
|
|
|
|
agent_id: str,
|
|
|
|
|
payload: SoulUpdateRequest,
|
|
|
|
|
@@ -573,7 +675,10 @@ async def update_agent_soul(
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> OkResponse:
|
|
|
|
|
"""Update an agent's SOUL.md content in DB and gateway."""
|
|
|
|
|
"""Update an agent's SOUL.md template in DB and gateway.
|
|
|
|
|
|
|
|
|
|
Lead-only endpoint. Persists as `soul_template` for future reprovisioning.
|
|
|
|
|
"""
|
|
|
|
|
_guard_board_access(agent_ctx, board)
|
|
|
|
|
_require_board_lead(agent_ctx)
|
|
|
|
|
coordination = GatewayCoordinationService(session)
|
|
|
|
|
@@ -589,14 +694,21 @@ async def update_agent_soul(
|
|
|
|
|
return OkResponse()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/boards/{board_id}/agents/{agent_id}", response_model=OkResponse)
|
|
|
|
|
@router.delete(
|
|
|
|
|
"/boards/{board_id}/agents/{agent_id}",
|
|
|
|
|
response_model=OkResponse,
|
|
|
|
|
tags=AGENT_LEAD_TAGS,
|
|
|
|
|
)
|
|
|
|
|
async def delete_board_agent(
|
|
|
|
|
agent_id: str,
|
|
|
|
|
board: Board = BOARD_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> OkResponse:
|
|
|
|
|
"""Delete a board agent as the board lead."""
|
|
|
|
|
"""Delete a board agent as board lead.
|
|
|
|
|
|
|
|
|
|
Cleans up runtime/session state through lifecycle services.
|
|
|
|
|
"""
|
|
|
|
|
_guard_board_access(agent_ctx, board)
|
|
|
|
|
_require_board_lead(agent_ctx)
|
|
|
|
|
service = AgentLifecycleService(session)
|
|
|
|
|
@@ -609,6 +721,7 @@ async def delete_board_agent(
|
|
|
|
|
@router.post(
|
|
|
|
|
"/boards/{board_id}/gateway/main/ask-user",
|
|
|
|
|
response_model=GatewayMainAskUserResponse,
|
|
|
|
|
tags=AGENT_LEAD_TAGS,
|
|
|
|
|
)
|
|
|
|
|
async def ask_user_via_gateway_main(
|
|
|
|
|
payload: GatewayMainAskUserRequest,
|
|
|
|
|
@@ -616,7 +729,10 @@ async def ask_user_via_gateway_main(
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> GatewayMainAskUserResponse:
|
|
|
|
|
"""Route a lead's ask-user request through the dedicated gateway agent."""
|
|
|
|
|
"""Ask the human via gateway-main external channels.
|
|
|
|
|
|
|
|
|
|
Lead-only endpoint for situations where board chat is not responsive.
|
|
|
|
|
"""
|
|
|
|
|
_guard_board_access(agent_ctx, board)
|
|
|
|
|
_require_board_lead(agent_ctx)
|
|
|
|
|
coordination = GatewayCoordinationService(session)
|
|
|
|
|
@@ -630,6 +746,7 @@ async def ask_user_via_gateway_main(
|
|
|
|
|
@router.post(
|
|
|
|
|
"/gateway/boards/{board_id}/lead/message",
|
|
|
|
|
response_model=GatewayLeadMessageResponse,
|
|
|
|
|
tags=AGENT_MAIN_TAGS,
|
|
|
|
|
)
|
|
|
|
|
async def message_gateway_board_lead(
|
|
|
|
|
board_id: UUID,
|
|
|
|
|
@@ -637,7 +754,7 @@ async def message_gateway_board_lead(
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> GatewayLeadMessageResponse:
|
|
|
|
|
"""Send a gateway-main message to a single board lead agent."""
|
|
|
|
|
"""Send a gateway-main control message to one board lead."""
|
|
|
|
|
coordination = GatewayCoordinationService(session)
|
|
|
|
|
return await coordination.message_gateway_board_lead(
|
|
|
|
|
actor_agent=agent_ctx.agent,
|
|
|
|
|
@@ -649,13 +766,14 @@ async def message_gateway_board_lead(
|
|
|
|
|
@router.post(
|
|
|
|
|
"/gateway/leads/broadcast",
|
|
|
|
|
response_model=GatewayLeadBroadcastResponse,
|
|
|
|
|
tags=AGENT_MAIN_TAGS,
|
|
|
|
|
)
|
|
|
|
|
async def broadcast_gateway_lead_message(
|
|
|
|
|
payload: GatewayLeadBroadcastRequest,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
|
|
|
|
) -> GatewayLeadBroadcastResponse:
|
|
|
|
|
"""Broadcast a gateway-main message to multiple board leads."""
|
|
|
|
|
"""Broadcast a gateway-main control message to multiple board leads."""
|
|
|
|
|
coordination = GatewayCoordinationService(session)
|
|
|
|
|
return await coordination.broadcast_gateway_lead_message(
|
|
|
|
|
actor_agent=agent_ctx.agent,
|
|
|
|
|
|