Phase 5.5-5.9: Plugin-Marketplace, Agent Memory, GraphRAG, Subagents, External Agent API
Check Cross-Plugin Imports / check (push) Has been cancelled

5.5 Plugin-Marketplace:
- New plugin: marketplace/ (models, routes, services, schemas, config)
- MarketplaceListing model (global, no tenant_id)
- Ed25519 signature verification via PluginSignature
- Endpoints: list, detail, install, verify, categories
- Config: MARKETPLACE_SERVER_URL setting

5.6 Agent Memory (persistent):
- New plugin: agent_memory/ (models, routes, services, schemas)
- AgentMemory model with embedding vector(768) + HNSW index
- store_memory() with auto-embedding
- retrieve_relevant_memories() with pgvector cosine similarity
- Semantic search endpoint

5.7 GraphRAG:
- New plugin: graph_rag/ (models, routes, services, provider, schemas)
- EntityRelationship model (source/target type+id, relationship_type, metadata)
- BFS graph traversal (bidirectional, configurable depth)
- GraphRAGSearchProvider registered in unified_search

5.8 Subagents / Multi-Agent:
- AgentCoordinator class (create_subtask, wait_for_subtask, aggregate, cancel)
- AgentSubtask model + migration 0002_agent_subtasks.sql
- 6 new API endpoints for subtask management
- Tools registered in AI tool registry

5.9 External Agent API:
- external_api.py: POST /run, GET /status, POST /stream (SSE)
- Bearer API token authentication
- Rate limiting: 10 req/min per token
- ExternalAgentRequest/Response schemas

3 new plugins registered in main.py and __init__.py
All files py_compile clean
This commit is contained in:
Agent Zero
2026-08-04 15:06:23 +02:00
parent 597aea1c23
commit 000c969b13
36 changed files with 3118 additions and 2 deletions
+238
View File
@@ -624,3 +624,241 @@ async def restore_automation_version(
return _automation_to_response(automation)
# ─── Subtask Endpoints (Phase 5.8) ───
@router.get(
"/subtasks",
dependencies=[Depends(require_permission("agents:read"))],
response_model=SubtaskListResponse,
)
async def list_subtasks(
parent_agent_id: str | None = Query(None),
child_agent_id: str | None = Query(None),
status: str | None = Query(None),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
current_user: dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""List subtasks with optional filters."""
tenant_id = uuid.UUID(current_user["tenant_id"])
try:
parent_id = uuid.UUID(parent_agent_id) if parent_agent_id else None
child_id = uuid.UUID(child_agent_id) if child_agent_id else None
except (ValueError, TypeError):
raise HTTPException(status_code=400, detail="Invalid agent ID")
from app.plugins.builtins.automation.agent_coordinator import AgentCoordinator
items, total = await AgentCoordinator.list_subtasks(
db, tenant_id,
parent_agent_id=parent_id,
child_agent_id=child_id,
status=status,
limit=limit,
offset=offset,
)
return SubtaskListResponse(
items=[_subtask_to_response(s) for s in items],
total=total,
)
@router.post(
"/subtasks",
dependencies=[Depends(require_permission("agents:execute"))],
response_model=SubtaskRead,
status_code=201,
)
async def create_subtask(
data: SubtaskCreate,
current_user: dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Create a new subtask for multi-agent orchestration."""
tenant_id = uuid.UUID(current_user["tenant_id"])
try:
parent_id = uuid.UUID(data.parent_agent_id)
child_id = uuid.UUID(data.child_agent_id)
except (ValueError, TypeError):
raise HTTPException(status_code=400, detail="Invalid agent ID")
from app.plugins.builtins.automation.agent_coordinator import AgentCoordinator
subtask = await AgentCoordinator.create_subtask(
db=db,
tenant_id=tenant_id,
parent_agent_id=parent_id,
child_agent_id=child_id,
task_description=data.task_description,
)
await db.commit()
return _subtask_to_response(subtask)
@router.get(
"/subtasks/{subtask_id}",
dependencies=[Depends(require_permission("agents:read"))],
response_model=SubtaskRead,
)
async def get_subtask(
subtask_id: str,
current_user: dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get a single subtask by ID."""
tenant_id = uuid.UUID(current_user["tenant_id"])
try:
sid = uuid.UUID(subtask_id)
except (ValueError, TypeError):
raise HTTPException(status_code=400, detail="Invalid subtask ID")
from app.plugins.builtins.automation.models import AgentSubtask
from sqlalchemy import select
result = await db.execute(
select(AgentSubtask)
.where(AgentSubtask.id == sid)
.where(AgentSubtask.tenant_id == tenant_id)
.limit(1)
)
subtask = result.scalar_one_or_none()
if subtask is None:
raise HTTPException(status_code=404, detail="Subtask not found")
return _subtask_to_response(subtask)
@router.patch(
"/subtasks/{subtask_id}",
dependencies=[Depends(require_permission("agents:execute"))],
response_model=SubtaskRead,
)
async def update_subtask(
subtask_id: str,
data: SubtaskUpdate,
current_user: dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Update a subtask (status, result)."""
tenant_id = uuid.UUID(current_user["tenant_id"])
try:
sid = uuid.UUID(subtask_id)
except (ValueError, TypeError):
raise HTTPException(status_code=400, detail="Invalid subtask ID")
from app.plugins.builtins.automation.models import AgentSubtask
from sqlalchemy import select
result = await db.execute(
select(AgentSubtask)
.where(AgentSubtask.id == sid)
.where(AgentSubtask.tenant_id == tenant_id)
.limit(1)
)
subtask = result.scalar_one_or_none()
if subtask is None:
raise HTTPException(status_code=404, detail="Subtask not found")
update_data = data.model_dump(exclude_none=True)
if "status" in update_data:
subtask.status = update_data["status"]
if update_data["status"] in ("completed", "failed", "cancelled"):
subtask.completed_at = __import__("datetime").datetime.now(
__import__("datetime").timezone.utc
)
if "result" in update_data:
subtask.result = update_data["result"]
await db.commit()
await db.refresh(subtask)
return _subtask_to_response(subtask)
@router.post(
"/subtasks/{subtask_id}/cancel",
dependencies=[Depends(require_permission("agents:execute"))],
)
async def cancel_subtask(
subtask_id: str,
current_user: dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Cancel a pending or running subtask."""
tenant_id = uuid.UUID(current_user["tenant_id"])
try:
sid = uuid.UUID(subtask_id)
except (ValueError, TypeError):
raise HTTPException(status_code=400, detail="Invalid subtask ID")
from app.plugins.builtins.automation.agent_coordinator import AgentCoordinator
success = await AgentCoordinator.cancel_subtask(db, tenant_id, sid)
if not success:
raise HTTPException(
status_code=400,
detail="Subtask not found or already in terminal state",
)
await db.commit()
return {"status": "cancelled", "subtask_id": subtask_id}
@router.post(
"/subtasks/{subtask_id}/wait",
dependencies=[Depends(require_permission("agents:execute"))],
)
async def wait_for_subtask(
subtask_id: str,
current_user: dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Wait for a subtask to complete (polls until terminal state)."""
tenant_id = uuid.UUID(current_user["tenant_id"])
try:
sid = uuid.UUID(subtask_id)
except (ValueError, TypeError):
raise HTTPException(status_code=400, detail="Invalid subtask ID")
from app.plugins.builtins.automation.agent_coordinator import AgentCoordinator
result = await AgentCoordinator.wait_for_subtask(db, tenant_id, sid)
return result
@router.post(
"/subtasks/aggregate",
dependencies=[Depends(require_permission("agents:read"))],
)
async def aggregate_subtasks(
subtask_ids: list[str],
current_user: dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Aggregate results from multiple subtasks."""
tenant_id = uuid.UUID(current_user["tenant_id"])
try:
ids = [uuid.UUID(sid) for sid in subtask_ids]
except (ValueError, TypeError):
raise HTTPException(status_code=400, detail="Invalid subtask ID in list")
from app.plugins.builtins.automation.agent_coordinator import AgentCoordinator
result = await AgentCoordinator.aggregate_results(db, tenant_id, ids)
return result
def _subtask_to_response(s) -> SubtaskRead:
"""Convert AgentSubtask model to response schema."""
return SubtaskRead(
id=str(s.id),
parent_agent_id=str(s.parent_agent_id),
child_agent_id=str(s.child_agent_id),
task_description=s.task_description,
status=s.status,
result=s.result or {},
created_at=s.created_at.isoformat() if s.created_at else None,
completed_at=s.completed_at.isoformat() if s.completed_at else None,
updated_at=s.updated_at.isoformat() if s.updated_at else None,
)