Refine sub-mode runtime agent API surface
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
"""Primary agent API module for Heicode sub-mode runtime."""
|
||||
|
||||
from .router import router
|
||||
|
||||
__all__ = ["router"]
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Primary router for Heicode sub-mode runtime APIs."""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from api.agnet.auth import extract_headers
|
||||
from api.agnet.callbacks import router as callbacks_router, user_router as callback_user_router
|
||||
from api.agnet.deployments import router as deployments_router
|
||||
from api.agnet.models import HealthCheckResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/agent", tags=["agent"])
|
||||
sub_agile_router = APIRouter(prefix="/sub-agile", tags=["agent-sub-agile"])
|
||||
|
||||
sub_agile_router.include_router(deployments_router)
|
||||
router.include_router(sub_agile_router)
|
||||
router.include_router(callbacks_router)
|
||||
router.include_router(callback_user_router)
|
||||
|
||||
|
||||
@router.get("/health", response_model=HealthCheckResponse)
|
||||
async def health_check(request: Request):
|
||||
"""Health check endpoint for the primary sub-mode runtime surface."""
|
||||
headers = extract_headers(request)
|
||||
logger.info("Agent health check - correlation_id=%s", headers["correlation_id"])
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"status": "healthy",
|
||||
"service": "agent-manager-sub-mode-runtime",
|
||||
"version": "1.0.0",
|
||||
"phase": "sub-mode-runtime",
|
||||
},
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
"""Agnet API module for Heicode integration."""
|
||||
"""Legacy agnet compatibility module for Heicode sub-mode runtime APIs."""
|
||||
|
||||
+25
-14
@@ -1,4 +1,4 @@
|
||||
"""Runtime callback endpoints for Heicode sub-mode events."""
|
||||
"""Runtime callback and observability endpoints for Heicode sub-mode events."""
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import hmac
|
||||
@@ -22,11 +22,13 @@ from api.agnet.auth import verify_service_token
|
||||
from api.agnet.validators import validate_no_sensitive_fields
|
||||
from api.agnet.vault_client import vault_client
|
||||
from api.swarm.artifact_store import load_azblob_artifact, load_runtime_artifact, runtime_uri_parts
|
||||
from api.status_projection import RuntimeDisplayStatus
|
||||
from config.error_codes import ErrorCode
|
||||
from config.settings import settings
|
||||
|
||||
router = APIRouter(prefix="/callbacks", tags=["agnet-callbacks"])
|
||||
user_router = APIRouter(prefix="/user/deployments", tags=["agnet-user-observability"])
|
||||
router = APIRouter(prefix="/callbacks", tags=["agent-callbacks"])
|
||||
compat_router = APIRouter(prefix="/callbacks", tags=["agnet-callbacks"])
|
||||
user_router = APIRouter(prefix="/user/deployments", tags=["agent-user-observability"])
|
||||
|
||||
|
||||
PHASES = {
|
||||
@@ -211,6 +213,10 @@ def _update_projection_state(
|
||||
status_value = payload.get("status") or payload.get("to_status")
|
||||
if status_value in DBDeploymentStatus._value2member_map_:
|
||||
deployment.status = DBDeploymentStatus(status_value)
|
||||
elif event_type == "approval.requested":
|
||||
deployment.status = DBDeploymentStatus.RUNNING
|
||||
if not deployment.phase:
|
||||
deployment.phase = RuntimeDisplayStatus.WAITING_APPROVAL.value
|
||||
elif event_type in STATUS_BY_EVENT and STATUS_BY_EVENT[event_type] is not None:
|
||||
deployment.status = STATUS_BY_EVENT[event_type]
|
||||
|
||||
@@ -225,6 +231,8 @@ def _update_projection_state(
|
||||
agent.status = DBDeploymentStatus.STOPPED
|
||||
elif event_type in {"agent.crashed", "sk_tool.failed"}:
|
||||
agent.status = DBDeploymentStatus.FAILED
|
||||
elif event_type == "approval.requested":
|
||||
agent.status = DBDeploymentStatus.RUNNING
|
||||
|
||||
deployment.updated_at = datetime.utcnow()
|
||||
|
||||
@@ -248,9 +256,10 @@ def _audit_approval_request(db: Session, deployment: Deployment, event_id: str,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/swarm-events")
|
||||
async def receive_swarm_event(request: Request, db: Session = Depends(get_db)):
|
||||
"""Receive Runtime events using the HEICODE_API_INTEGRATION v2.1 contract."""
|
||||
@router.post("/runtime-events")
|
||||
@compat_router.post("/swarm-events")
|
||||
async def receive_runtime_event(request: Request, db: Session = Depends(get_db)):
|
||||
"""Receive runtime events using the HEICODE_API_INTEGRATION v2.1 contract."""
|
||||
raw_body = await request.body()
|
||||
try:
|
||||
body = json.loads(raw_body.decode("utf-8") or "{}")
|
||||
@@ -309,9 +318,10 @@ async def receive_swarm_event(request: Request, db: Session = Depends(get_db)):
|
||||
return {"success": True, "event_id": event_id, "deduplicated": False}
|
||||
|
||||
|
||||
@router.get("/swarm-events/schema")
|
||||
async def get_swarm_event_schema():
|
||||
"""Expose callback contract metadata for Agent Manager联调."""
|
||||
@router.get("/runtime-events/schema")
|
||||
@compat_router.get("/swarm-events/schema")
|
||||
async def get_runtime_event_schema():
|
||||
"""Expose callback contract metadata for sub-mode runtime integration."""
|
||||
event_types = {
|
||||
"deployment.status_changed": {
|
||||
"category": "status",
|
||||
@@ -376,7 +386,8 @@ async def get_swarm_event_schema():
|
||||
}
|
||||
return {
|
||||
"success": True,
|
||||
"endpoint": "/api/agnet/callbacks/swarm-events",
|
||||
"endpoint": "/api/agent/callbacks/runtime-events",
|
||||
"compatibility_endpoints": ["/api/agnet/callbacks/swarm-events"],
|
||||
"headers": {
|
||||
"X-Agnet-Event-Id": "required for idempotency",
|
||||
"X-Agnet-Timestamp": "required for HMAC, Unix milliseconds",
|
||||
@@ -407,7 +418,7 @@ async def list_deployment_artifacts(
|
||||
db: Session = Depends(get_db),
|
||||
token: str = Depends(verify_service_token),
|
||||
):
|
||||
"""Return artifacts projected from v2.1 artifact.created callbacks."""
|
||||
"""Return artifacts projected from runtime artifact.created callbacks."""
|
||||
_ensure_deployment(db, deployment_id)
|
||||
events = (
|
||||
db.query(Event)
|
||||
@@ -445,7 +456,7 @@ async def get_deployment_artifact_content(
|
||||
db: Session = Depends(get_db),
|
||||
token: str = Depends(verify_service_token),
|
||||
):
|
||||
"""Return full content for Runtime-local artifacts referenced by artifact.created events."""
|
||||
"""Return full content for runtime-local artifacts referenced by artifact.created events."""
|
||||
_ensure_deployment(db, deployment_id)
|
||||
events = (
|
||||
db.query(Event)
|
||||
@@ -508,7 +519,7 @@ async def list_deployment_timeline(
|
||||
db: Session = Depends(get_db),
|
||||
token: str = Depends(verify_service_token),
|
||||
):
|
||||
"""Return a merged timeline from callback events."""
|
||||
"""Return a merged timeline from runtime callback events."""
|
||||
_ensure_deployment(db, deployment_id)
|
||||
timeline_event_types = {
|
||||
"timeline.updated",
|
||||
@@ -560,7 +571,7 @@ async def list_deployment_sk_snapshots(
|
||||
db: Session = Depends(get_db),
|
||||
token: str = Depends(verify_service_token),
|
||||
):
|
||||
"""Return SK snapshots projected from Runtime callback payloads."""
|
||||
"""Return SK snapshots projected from runtime callback payloads."""
|
||||
_ensure_deployment(db, deployment_id)
|
||||
events = (
|
||||
db.query(Event)
|
||||
|
||||
+42
-19
@@ -1,4 +1,4 @@
|
||||
"""Deployment endpoints for Heicode integration."""
|
||||
"""Deployment endpoints for Heicode sub-mode runtime integration."""
|
||||
from fastapi import APIRouter, Depends, Request, HTTPException, BackgroundTasks
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
@@ -22,6 +22,7 @@ from api.agnet.idempotency import idempotency_cache
|
||||
from api.agnet.k8s_manager import k8s_manager
|
||||
from api.agnet.vault_client import vault_client
|
||||
from api.swarm.callback_client import CallbackDeliveryClient
|
||||
from api.status_projection import RuntimeDisplayStatus, project_deployment_status, project_runtime_run_status
|
||||
from config.error_codes import ErrorCode
|
||||
from config.settings import settings
|
||||
import logging
|
||||
@@ -33,6 +34,25 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _project_deployment_status_for_record(db: Session, deployment: Deployment) -> str:
|
||||
"""Project persisted deployment state into a Manager-facing runtime status."""
|
||||
events = (
|
||||
db.query(Event.event_type, Event.payload)
|
||||
.filter(Event.deployment_id == deployment.deployment_id)
|
||||
.order_by(Event.occurred_at.asc())
|
||||
.all()
|
||||
)
|
||||
normalized_events = [{"event_type": event_type, "payload": payload or {}} for event_type, payload in events]
|
||||
return project_deployment_status(deployment.status, phase=deployment.phase, events=normalized_events)
|
||||
|
||||
|
||||
def _project_agent_instance_status(deployment_status: str, agent_status: str) -> str:
|
||||
"""Keep agent rows aligned with the projected deployment status contract."""
|
||||
if deployment_status == RuntimeDisplayStatus.ACCEPTED.value and agent_status == DBDeploymentStatus.PENDING.value:
|
||||
return RuntimeDisplayStatus.ACCEPTED.value
|
||||
return agent_status
|
||||
|
||||
|
||||
def generate_deployment_id() -> str:
|
||||
"""Generate unique deployment ID."""
|
||||
return f"dep_{uuid.uuid4().hex[:12]}"
|
||||
@@ -268,16 +288,17 @@ def build_plan_summary(request: CreateDeploymentRequest) -> dict:
|
||||
|
||||
|
||||
def build_deployment_response_from_swarm(db: Session, swarm: Swarm) -> GetDeploymentResponse:
|
||||
"""Expose /api/swarms-created runs through deployment detail compatibility."""
|
||||
"""Expose compatibility runtime runs through the deployment-detail shape."""
|
||||
context = swarm.project_context or {}
|
||||
billing_context = context.get("billing_context") or {}
|
||||
budget = context.get("budget") or {}
|
||||
agents = db.query(SwarmAgent).filter(SwarmAgent.swarm_id == swarm.swarm_id).all()
|
||||
display_status = project_runtime_run_status(swarm.status)
|
||||
return GetDeploymentResponse(
|
||||
deployment_id=swarm.swarm_id,
|
||||
user_id=swarm.owner_id,
|
||||
binding_scope=context.get("binding_scope") or context.get("intent_id") or swarm.swarm_id,
|
||||
status=swarm.status.value,
|
||||
status=display_status,
|
||||
phase=swarm.phase,
|
||||
orchestration_plan=json.dumps(
|
||||
{
|
||||
@@ -304,7 +325,7 @@ def build_deployment_response_from_swarm(db: Session, swarm: Swarm) -> GetDeploy
|
||||
AgentInstanceResponse(
|
||||
agent_instance_id=agent.agent_id,
|
||||
role=agent.role,
|
||||
status=agent.status.value,
|
||||
status=_project_agent_instance_status(display_status, agent.status.value),
|
||||
phase=swarm.phase,
|
||||
)
|
||||
for agent in agents
|
||||
@@ -351,7 +372,7 @@ async def create_deployment(
|
||||
db: Session = Depends(get_db),
|
||||
token: str = Depends(verify_service_token)
|
||||
):
|
||||
"""Create a new deployment."""
|
||||
"""Create a new sub-agile runtime deployment."""
|
||||
headers = extract_headers(http_request)
|
||||
correlation_id = headers.get("correlation_id")
|
||||
user_id = headers.get("user_id")
|
||||
@@ -552,12 +573,12 @@ async def create_deployment(
|
||||
response = CreateDeploymentResponse(
|
||||
deployment_id=deployment_id,
|
||||
swarm_id=deployment_id,
|
||||
status="pending",
|
||||
status=RuntimeDisplayStatus.ACCEPTED.value,
|
||||
agent_instances=[
|
||||
AgentInstanceResponse(
|
||||
agent_instance_id=inst.agent_instance_id,
|
||||
role=inst.role,
|
||||
status="pending",
|
||||
status=RuntimeDisplayStatus.ACCEPTED.value,
|
||||
phase=None
|
||||
)
|
||||
for inst in agent_instances
|
||||
@@ -567,7 +588,7 @@ async def create_deployment(
|
||||
data={
|
||||
"deployment_id": deployment_id,
|
||||
"swarm_id": deployment_id,
|
||||
"status": "pending",
|
||||
"status": RuntimeDisplayStatus.ACCEPTED.value,
|
||||
"estimated_ready_at": (deployment.created_at + timedelta(minutes=2)).isoformat(),
|
||||
},
|
||||
)
|
||||
@@ -637,7 +658,7 @@ async def list_deployments(
|
||||
db: Session = Depends(get_db),
|
||||
token: str = Depends(verify_service_token)
|
||||
):
|
||||
"""List deployments with filtering and pagination."""
|
||||
"""List sub-mode runtime deployments with filtering and pagination."""
|
||||
query = db.query(Deployment)
|
||||
|
||||
# Apply filters
|
||||
@@ -666,10 +687,11 @@ async def list_deployments(
|
||||
instance_count = db.query(AgentInstance).filter(
|
||||
AgentInstance.deployment_id == dep.deployment_id
|
||||
).count()
|
||||
display_status = _project_deployment_status_for_record(db, dep)
|
||||
|
||||
deployment_summaries.append(DeploymentSummary(
|
||||
deployment_id=dep.deployment_id,
|
||||
status=dep.status.value,
|
||||
status=display_status,
|
||||
risk_level=dep.risk_level.value,
|
||||
budget=BudgetSummary(
|
||||
max_usd=dep.budget_max_usd or 0.0,
|
||||
@@ -695,7 +717,7 @@ async def get_deployment(
|
||||
db: Session = Depends(get_db),
|
||||
token: str = Depends(verify_service_token)
|
||||
):
|
||||
"""Get deployment details."""
|
||||
"""Get sub-mode runtime deployment details."""
|
||||
deployment = db.query(Deployment).filter(
|
||||
Deployment.deployment_id == deployment_id
|
||||
).first()
|
||||
@@ -719,12 +741,13 @@ async def get_deployment(
|
||||
instances = db.query(AgentInstance).filter(
|
||||
AgentInstance.deployment_id == deployment_id
|
||||
).all()
|
||||
display_status = _project_deployment_status_for_record(db, deployment)
|
||||
|
||||
return GetDeploymentResponse(
|
||||
deployment_id=deployment.deployment_id,
|
||||
user_id=deployment.user_id,
|
||||
binding_scope=deployment.binding_scope,
|
||||
status=deployment.status.value,
|
||||
status=display_status,
|
||||
phase=deployment.phase,
|
||||
orchestration_plan=deployment.orchestration_plan,
|
||||
risk_level=deployment.risk_level.value,
|
||||
@@ -742,7 +765,7 @@ async def get_deployment(
|
||||
AgentInstanceResponse(
|
||||
agent_instance_id=inst.agent_instance_id,
|
||||
role=inst.role,
|
||||
status=inst.status.value,
|
||||
status=_project_agent_instance_status(display_status, inst.status.value),
|
||||
phase=inst.phase
|
||||
)
|
||||
for inst in instances
|
||||
@@ -761,7 +784,7 @@ async def stop_deployment(
|
||||
db: Session = Depends(get_db),
|
||||
token: str = Depends(verify_service_token)
|
||||
):
|
||||
"""Stop a deployment."""
|
||||
"""Stop a sub-mode runtime deployment."""
|
||||
headers = extract_headers(http_request)
|
||||
correlation_id = headers.get("correlation_id")
|
||||
user_id = headers.get("user_id")
|
||||
@@ -784,7 +807,7 @@ async def stop_deployment(
|
||||
db.commit()
|
||||
return StopDeploymentResponse(
|
||||
deployment_id=deployment_id,
|
||||
status="stopped",
|
||||
status=RuntimeDisplayStatus.STOPPED.value,
|
||||
stopped_at=stopped_at,
|
||||
)
|
||||
raise HTTPException(
|
||||
@@ -905,7 +928,7 @@ async def receive_deployment_approval_decision(
|
||||
db: Session = Depends(get_db),
|
||||
token: str = Depends(verify_service_token),
|
||||
):
|
||||
"""Accept Manager approval decisions for ordinary sub-mode Runtime actions."""
|
||||
"""Accept Manager approval decisions for ordinary sub-mode runtime actions."""
|
||||
headers = extract_headers(http_request)
|
||||
deployment = db.query(Deployment).filter(
|
||||
Deployment.deployment_id == deployment_id
|
||||
@@ -979,7 +1002,7 @@ async def get_deployment_logs(
|
||||
db: Session = Depends(get_db),
|
||||
token: str = Depends(verify_service_token)
|
||||
):
|
||||
"""Get logs for a deployment or specific agent instance."""
|
||||
"""Get logs for a sub-mode runtime deployment or agent instance."""
|
||||
deployment = db.query(Deployment).filter(
|
||||
Deployment.deployment_id == deployment_id
|
||||
).first()
|
||||
@@ -1062,7 +1085,7 @@ async def get_deployment_events(
|
||||
db: Session = Depends(get_db),
|
||||
token: str = Depends(verify_service_token)
|
||||
):
|
||||
"""Get events for a deployment."""
|
||||
"""Get events for a sub-mode runtime deployment."""
|
||||
deployment = db.query(Deployment).filter(
|
||||
Deployment.deployment_id == deployment_id
|
||||
).first()
|
||||
@@ -1111,7 +1134,7 @@ async def get_deployment_metrics(
|
||||
db: Session = Depends(get_db),
|
||||
token: str = Depends(verify_service_token)
|
||||
):
|
||||
"""Get resource metrics for a deployment."""
|
||||
"""Get resource metrics for a sub-mode runtime deployment."""
|
||||
deployment = db.query(Deployment).filter(
|
||||
Deployment.deployment_id == deployment_id
|
||||
).first()
|
||||
|
||||
+29
-2
@@ -1,4 +1,4 @@
|
||||
"""Pydantic models for Heicode integration API."""
|
||||
"""Pydantic models for Heicode sub-mode runtime APIs."""
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
from typing import List, Optional, Dict, Any, Union
|
||||
from datetime import datetime
|
||||
@@ -19,13 +19,24 @@ class RiskLevel(str, Enum):
|
||||
|
||||
|
||||
class DeploymentStatus(str, Enum):
|
||||
"""Deployment status."""
|
||||
"""Legacy deployment persistence status."""
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
STOPPED = "stopped"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class RuntimeDisplayStatus(str, Enum):
|
||||
"""Projected Manager-facing runtime status."""
|
||||
|
||||
ACCEPTED = "accepted"
|
||||
RUNNING = "running"
|
||||
WAITING_APPROVAL = "waiting_approval"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
STOPPED = "stopped"
|
||||
|
||||
|
||||
DEFAULT_CALLBACK_EVENTS = [
|
||||
"deployment.status_changed",
|
||||
"phase.changed",
|
||||
@@ -436,3 +447,19 @@ class GetMetricsResponse(BaseModel):
|
||||
timestamp: datetime
|
||||
agent_metrics: List[AgentMetrics]
|
||||
total_resources: ResourceMetrics
|
||||
|
||||
|
||||
class SubAgileDeploymentCreateRequest(CreateDeploymentRequest):
|
||||
"""Primary request model for /api/agent/sub-agile/deployments."""
|
||||
|
||||
|
||||
class SubAgileDeploymentCreateResponse(CreateDeploymentResponse):
|
||||
"""Primary response model for /api/agent/sub-agile/deployments."""
|
||||
|
||||
|
||||
class SubAgileDeploymentDetailResponse(GetDeploymentResponse):
|
||||
"""Primary response model for /api/agent/sub-agile/deployments/{id}."""
|
||||
|
||||
|
||||
class SubAgileDeploymentStopResponse(StopDeploymentResponse):
|
||||
"""Primary response model for /api/agent/sub-agile/deployments/{id}/stop."""
|
||||
|
||||
+7
-7
@@ -1,19 +1,19 @@
|
||||
"""Main router for Heicode integration API."""
|
||||
"""Legacy compatibility router for Heicode sub-mode runtime APIs."""
|
||||
from fastapi import APIRouter, Request
|
||||
from api.agnet.auth import extract_headers
|
||||
from api.agnet.models import HealthCheckResponse
|
||||
from api.agnet.deployments import router as deployments_router
|
||||
from api.agnet.callbacks import router as callbacks_router, user_router as callback_user_router
|
||||
from api.agnet.callbacks import compat_router as callbacks_router, user_router as callback_user_router
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/agnet",
|
||||
tags=["agnet"],
|
||||
tags=["agent-compatibility"],
|
||||
)
|
||||
|
||||
# Include deployment endpoints
|
||||
# Legacy compatibility endpoints reuse the shared sub-mode runtime handlers.
|
||||
router.include_router(deployments_router)
|
||||
router.include_router(callbacks_router)
|
||||
router.include_router(callback_user_router)
|
||||
@@ -21,7 +21,7 @@ router.include_router(callback_user_router)
|
||||
|
||||
@router.get("/health", response_model=HealthCheckResponse)
|
||||
async def health_check(request: Request):
|
||||
"""Health check endpoint for Heicode integration.
|
||||
"""Health check endpoint for the legacy compatibility surface.
|
||||
|
||||
Returns service status and version information.
|
||||
"""
|
||||
@@ -32,8 +32,8 @@ async def health_check(request: Request):
|
||||
"success": True,
|
||||
"data": {
|
||||
"status": "healthy",
|
||||
"service": "agent-manager-agnet",
|
||||
"service": "agent-manager-sub-mode-runtime",
|
||||
"version": "1.0.0",
|
||||
"phase": "2-deployments"
|
||||
"phase": "sub-mode-runtime-compatibility"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
"""Shared status projection helpers for Heicode sub-mode runtime APIs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Iterable, Mapping, Any
|
||||
|
||||
|
||||
class RuntimeDisplayStatus(str, Enum):
|
||||
"""User-facing runtime status projected for Manager-facing APIs."""
|
||||
|
||||
ACCEPTED = "accepted"
|
||||
RUNNING = "running"
|
||||
WAITING_APPROVAL = "waiting_approval"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
STOPPED = "stopped"
|
||||
|
||||
|
||||
_COMPLETED_PHASES = {"done", "deliver", "delivered", "deployment", "completed"}
|
||||
_APPROVAL_EVENT_TYPES = {"approval.requested"}
|
||||
_APPROVAL_RESOLUTION_EVENT_TYPES = {"approval.decision"}
|
||||
_COMPLETED_EVENT_TYPES = {"task.completed"}
|
||||
_FAILED_EVENT_TYPES = {"task.failed", "task.blocked", "deployment.failed", "agent.crashed"}
|
||||
_STOPPED_EVENT_TYPES = {"deployment.stopped"}
|
||||
_RUNNING_EVENT_TYPES = {"deployment.started", "agent.started"}
|
||||
|
||||
|
||||
def _normalize_status(value: Any) -> str | None:
|
||||
"""Normalize Enum/string statuses to lowercase strings."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, Enum):
|
||||
value = value.value
|
||||
return str(value).strip().lower() or None
|
||||
|
||||
|
||||
def _event_payload_status(event: Mapping[str, Any]) -> str | None:
|
||||
"""Extract a comparable status from an event payload."""
|
||||
payload = event.get("payload")
|
||||
if not isinstance(payload, Mapping):
|
||||
return None
|
||||
return _normalize_status(payload.get("status") or payload.get("to_status"))
|
||||
|
||||
|
||||
def project_deployment_status(
|
||||
db_status: str | Enum | None,
|
||||
*,
|
||||
phase: str | None = None,
|
||||
events: Iterable[Mapping[str, Any]] | None = None,
|
||||
) -> str:
|
||||
"""Project deployment DB state plus callback facts into a Manager-facing status."""
|
||||
normalized_status = _normalize_status(db_status)
|
||||
normalized_phase = _normalize_status(phase)
|
||||
event_list = list(events or [])
|
||||
|
||||
last_approval_requested_idx = -1
|
||||
last_approval_resolved_idx = -1
|
||||
|
||||
for idx, event in enumerate(event_list):
|
||||
event_type = str(event.get("event_type") or "").strip()
|
||||
payload_status = _event_payload_status(event)
|
||||
|
||||
if event_type in _APPROVAL_EVENT_TYPES:
|
||||
last_approval_requested_idx = idx
|
||||
elif event_type in _APPROVAL_RESOLUTION_EVENT_TYPES:
|
||||
last_approval_resolved_idx = idx
|
||||
|
||||
if payload_status == RuntimeDisplayStatus.STOPPED.value or event_type in _STOPPED_EVENT_TYPES:
|
||||
return RuntimeDisplayStatus.STOPPED.value
|
||||
if payload_status == RuntimeDisplayStatus.FAILED.value or event_type in _FAILED_EVENT_TYPES:
|
||||
return RuntimeDisplayStatus.FAILED.value
|
||||
if payload_status == RuntimeDisplayStatus.COMPLETED.value or event_type in _COMPLETED_EVENT_TYPES:
|
||||
return RuntimeDisplayStatus.COMPLETED.value
|
||||
if payload_status == RuntimeDisplayStatus.RUNNING.value or event_type in _RUNNING_EVENT_TYPES:
|
||||
normalized_status = RuntimeDisplayStatus.RUNNING.value
|
||||
|
||||
if last_approval_requested_idx > last_approval_resolved_idx:
|
||||
return RuntimeDisplayStatus.WAITING_APPROVAL.value
|
||||
|
||||
if normalized_status == "stopped":
|
||||
return RuntimeDisplayStatus.STOPPED.value
|
||||
if normalized_status == "failed":
|
||||
return RuntimeDisplayStatus.FAILED.value
|
||||
if normalized_status == "running":
|
||||
if normalized_phase in _COMPLETED_PHASES:
|
||||
return RuntimeDisplayStatus.COMPLETED.value
|
||||
return RuntimeDisplayStatus.RUNNING.value
|
||||
if normalized_phase in _COMPLETED_PHASES:
|
||||
return RuntimeDisplayStatus.COMPLETED.value
|
||||
return RuntimeDisplayStatus.ACCEPTED.value
|
||||
|
||||
|
||||
def project_runtime_run_status(runtime_status: str | Enum | None) -> str:
|
||||
"""Project runtime-run statuses to the shared Manager-facing status set."""
|
||||
normalized_status = _normalize_status(runtime_status)
|
||||
if normalized_status == "completed":
|
||||
return RuntimeDisplayStatus.COMPLETED.value
|
||||
if normalized_status == "failed":
|
||||
return RuntimeDisplayStatus.FAILED.value
|
||||
if normalized_status == "stopped":
|
||||
return RuntimeDisplayStatus.STOPPED.value
|
||||
if normalized_status == "running":
|
||||
return RuntimeDisplayStatus.RUNNING.value
|
||||
return RuntimeDisplayStatus.ACCEPTED.value
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Sub-mode runtime compatibility module."""
|
||||
|
||||
"""Compatibility exports for the legacy /api/swarms runtime surface."""
|
||||
|
||||
from .router import swarms_router
|
||||
|
||||
__all__ = ["swarms_router"]
|
||||
|
||||
+6
-6
@@ -17,7 +17,7 @@ class AgentConfig(BaseModel):
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def normalize_sub_mode_agent(cls, data: Any) -> Any:
|
||||
"""Accept Manager sub-mode agent fields when /api/swarms is used."""
|
||||
"""Accept Manager sub-mode agent fields when the compatibility API is used."""
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
data = dict(data)
|
||||
@@ -59,7 +59,7 @@ class CallbackConfig(BaseModel):
|
||||
|
||||
|
||||
class SwarmCreateRequest(BaseModel):
|
||||
"""Request model for creating a sub-mode runtime run."""
|
||||
"""Compatibility request model for creating a sub-mode runtime run."""
|
||||
task_description: str = Field(..., description="Task description")
|
||||
project_context: Optional[ProjectContext] = Field(None, description="Project context")
|
||||
agents: List[AgentConfig] = Field(..., description="Agent configurations")
|
||||
@@ -80,7 +80,7 @@ class SwarmAgentInfo(BaseModel):
|
||||
|
||||
|
||||
class SwarmCreateResponse(BaseModel):
|
||||
"""Response model for sub-mode runtime creation."""
|
||||
"""Compatibility response model for sub-mode runtime creation."""
|
||||
deployment_id: Optional[str] = None
|
||||
swarm_id: str
|
||||
status: str
|
||||
@@ -97,7 +97,7 @@ class SwarmMetrics(BaseModel):
|
||||
|
||||
|
||||
class SwarmStatusResponse(BaseModel):
|
||||
"""Response model for sub-mode runtime status."""
|
||||
"""Compatibility response model for sub-mode runtime status."""
|
||||
deployment_id: Optional[str] = None
|
||||
swarm_id: str
|
||||
status: str
|
||||
@@ -112,13 +112,13 @@ class SwarmStatusResponse(BaseModel):
|
||||
|
||||
|
||||
class SwarmStopRequest(BaseModel):
|
||||
"""Request model for stopping a sub-mode runtime run."""
|
||||
"""Compatibility request model for stopping a sub-mode runtime run."""
|
||||
reason: Optional[str] = Field(None, description="Reason for stopping")
|
||||
cleanup: bool = Field(default=True, description="Whether to cleanup K8s resources")
|
||||
|
||||
|
||||
class SwarmStopResponse(BaseModel):
|
||||
"""Response model for stopping a sub-mode runtime run."""
|
||||
"""Compatibility response model for stopping a sub-mode runtime run."""
|
||||
deployment_id: Optional[str] = None
|
||||
swarm_id: str
|
||||
status: str
|
||||
|
||||
@@ -55,13 +55,13 @@ class SwarmOrchestrator:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Load swarm from database
|
||||
# Load the internal runtime-run record from the database.
|
||||
self.swarm = self.db.query(Swarm).filter(
|
||||
Swarm.swarm_id == self.swarm_id
|
||||
).first()
|
||||
|
||||
if not self.swarm:
|
||||
logger.error(f"Swarm {self.swarm_id} not found")
|
||||
logger.error(f"Runtime deployment {self.swarm_id} not found")
|
||||
return False
|
||||
|
||||
project_context = self.swarm.project_context or {}
|
||||
@@ -73,7 +73,7 @@ class SwarmOrchestrator:
|
||||
self.db.commit()
|
||||
await self._emit_status("initializing")
|
||||
|
||||
logger.info(f"Initializing swarm {self.swarm_id}")
|
||||
logger.info(f"Initializing sub-mode runtime deployment {self.swarm_id}")
|
||||
|
||||
# Get all agents for this swarm
|
||||
swarm_agents = self.db.query(SwarmAgent).filter(
|
||||
|
||||
+25
-23
@@ -1,4 +1,4 @@
|
||||
"""Sub-mode runtime compatibility router."""
|
||||
"""Sub-mode runtime compatibility router for legacy /api/swarms clients."""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
@@ -26,8 +26,9 @@ from .models import (
|
||||
ApprovalDecisionRequest
|
||||
)
|
||||
from .orchestrator import SwarmOrchestrator
|
||||
from api.status_projection import RuntimeDisplayStatus, project_runtime_run_status
|
||||
|
||||
swarms_router = APIRouter(prefix="/api/swarms", tags=["swarms"])
|
||||
swarms_router = APIRouter(prefix="/api/swarms", tags=["sub-mode-runtime-compatibility"])
|
||||
|
||||
|
||||
def generate_swarm_id() -> str:
|
||||
@@ -119,10 +120,11 @@ def _synthesized_artifacts_for_swarm(db: Session, swarm: Swarm) -> list[Dict[str
|
||||
def _build_swarm_status_response(db: Session, swarm: Swarm) -> SwarmStatusResponse:
|
||||
"""Return a standard status payload for the sub-mode runtime compatibility API."""
|
||||
elapsed_seconds = int((datetime.utcnow() - swarm.created_at).total_seconds())
|
||||
display_status = project_runtime_run_status(swarm.status)
|
||||
return SwarmStatusResponse(
|
||||
deployment_id=swarm.swarm_id,
|
||||
swarm_id=swarm.swarm_id,
|
||||
status=swarm.status.value,
|
||||
status=display_status,
|
||||
phase=swarm.phase,
|
||||
progress=swarm.progress,
|
||||
agents=_agent_infos_for_swarm(db, swarm.swarm_id),
|
||||
@@ -139,10 +141,10 @@ def _build_swarm_status_response(db: Session, swarm: Swarm) -> SwarmStatusRespon
|
||||
|
||||
|
||||
def _stop_swarm_record(db: Session, swarm_id: str, request: SwarmStopRequest) -> SwarmStopResponse:
|
||||
"""Idempotently stop a swarm database record."""
|
||||
"""Idempotently stop a compatibility runtime-run database record."""
|
||||
swarm = db.query(Swarm).filter(Swarm.swarm_id == swarm_id).first()
|
||||
if not swarm:
|
||||
raise HTTPException(status_code=404, detail="Swarm not found")
|
||||
raise HTTPException(status_code=404, detail="Runtime deployment not found")
|
||||
|
||||
stopped_at = datetime.utcnow()
|
||||
if swarm.status != SwarmStatus.STOPPED:
|
||||
@@ -157,7 +159,7 @@ def _stop_swarm_record(db: Session, swarm_id: str, request: SwarmStopRequest) ->
|
||||
return SwarmStopResponse(
|
||||
deployment_id=swarm_id,
|
||||
swarm_id=swarm_id,
|
||||
status=SwarmStatus.STOPPED.value,
|
||||
status=RuntimeDisplayStatus.STOPPED.value,
|
||||
stopped_at=stopped_at,
|
||||
)
|
||||
|
||||
@@ -184,7 +186,7 @@ async def initialize_and_execute_swarm(swarm_id: str, db_url: str):
|
||||
await orchestrator.execute()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in background swarm execution: {e}")
|
||||
print(f"Error in background sub-mode runtime execution: {e}")
|
||||
finally:
|
||||
await orchestrator.cleanup()
|
||||
db.close()
|
||||
@@ -261,7 +263,7 @@ async def create_swarm(
|
||||
return SwarmCreateResponse(
|
||||
deployment_id=swarm_id,
|
||||
swarm_id=swarm_id,
|
||||
status=swarm.status.value,
|
||||
status=project_runtime_run_status(swarm.status),
|
||||
agents=agent_infos,
|
||||
created_at=swarm.created_at,
|
||||
estimated_ready_at=swarm.created_at + timedelta(minutes=2)
|
||||
@@ -353,19 +355,19 @@ async def create_swarm_compat(
|
||||
|
||||
@swarms_router.get("/{swarm_id}", response_model=SwarmStatusResponse)
|
||||
async def get_swarm_detail_compat(swarm_id: str, db: Session = Depends(get_db)):
|
||||
"""Compatibility detail endpoint for Manager Runtime bridge."""
|
||||
"""Compatibility detail endpoint for legacy Manager runtime clients."""
|
||||
swarm = db.query(Swarm).filter(Swarm.swarm_id == swarm_id).first()
|
||||
if not swarm:
|
||||
raise HTTPException(status_code=404, detail="Swarm not found")
|
||||
raise HTTPException(status_code=404, detail="Runtime deployment not found")
|
||||
return _build_swarm_status_response(db, swarm)
|
||||
|
||||
|
||||
@swarms_router.get("/{swarm_id}/status", response_model=SwarmStatusResponse)
|
||||
async def get_swarm_status_compat(swarm_id: str, db: Session = Depends(get_db)):
|
||||
"""Compatibility status endpoint for Manager Runtime bridge."""
|
||||
"""Compatibility status endpoint for legacy Manager runtime clients."""
|
||||
swarm = db.query(Swarm).filter(Swarm.swarm_id == swarm_id).first()
|
||||
if not swarm:
|
||||
raise HTTPException(status_code=404, detail="Swarm not found")
|
||||
raise HTTPException(status_code=404, detail="Runtime deployment not found")
|
||||
return _build_swarm_status_response(db, swarm)
|
||||
|
||||
|
||||
@@ -383,7 +385,7 @@ async def get_swarm_logs(swarm_id: str, db: Session = Depends(get_db)):
|
||||
"""Get aggregated logs from all agents in a sub-mode runtime run."""
|
||||
swarm = db.query(Swarm).filter(Swarm.swarm_id == swarm_id).first()
|
||||
if not swarm:
|
||||
raise HTTPException(status_code=404, detail="Swarm not found")
|
||||
raise HTTPException(status_code=404, detail="Runtime deployment not found")
|
||||
|
||||
agents = db.query(SwarmAgent).filter(SwarmAgent.swarm_id == swarm_id).all()
|
||||
|
||||
@@ -462,10 +464,10 @@ async def get_swarm_logs_compat(swarm_id: str, db: Session = Depends(get_db)):
|
||||
|
||||
@swarms_router.get("/{swarm_id}/events")
|
||||
async def get_swarm_events_compat(swarm_id: str, db: Session = Depends(get_db)):
|
||||
"""Return swarm messages as Runtime events for Manager polling fallback."""
|
||||
"""Return runtime messages as compatibility events for polling fallback."""
|
||||
swarm = db.query(Swarm).filter(Swarm.swarm_id == swarm_id).first()
|
||||
if not swarm:
|
||||
raise HTTPException(status_code=404, detail="Swarm not found")
|
||||
raise HTTPException(status_code=404, detail="Runtime deployment not found")
|
||||
|
||||
messages = (
|
||||
db.query(SwarmMessage)
|
||||
@@ -476,7 +478,7 @@ async def get_swarm_events_compat(swarm_id: str, db: Session = Depends(get_db)):
|
||||
events = [
|
||||
{
|
||||
"event_id": message.message_id,
|
||||
"event_type": f"swarm.message.{message.message_type}",
|
||||
"event_type": f"runtime.message.{message.message_type}",
|
||||
"swarm_id": swarm_id,
|
||||
"agent_instance_id": message.from_agent_id or message.to_agent_id,
|
||||
"occurred_at": message.created_at,
|
||||
@@ -507,16 +509,16 @@ async def get_swarm_events_compat(swarm_id: str, db: Session = Depends(get_db)):
|
||||
|
||||
@swarms_router.get("/{swarm_id}/metrics")
|
||||
async def get_swarm_metrics_compat(swarm_id: str, db: Session = Depends(get_db)):
|
||||
"""Return basic Runtime metrics for Manager polling fallback."""
|
||||
"""Return basic runtime metrics for compatibility polling fallback."""
|
||||
swarm = db.query(Swarm).filter(Swarm.swarm_id == swarm_id).first()
|
||||
if not swarm:
|
||||
raise HTTPException(status_code=404, detail="Swarm not found")
|
||||
raise HTTPException(status_code=404, detail="Runtime deployment not found")
|
||||
|
||||
elapsed_seconds = int((datetime.utcnow() - swarm.created_at).total_seconds())
|
||||
return {
|
||||
"success": True,
|
||||
"swarm_id": swarm_id,
|
||||
"status": swarm.status.value,
|
||||
"status": project_runtime_run_status(swarm.status),
|
||||
"stage": swarm.phase,
|
||||
"checkpoint": "completed" if swarm.status == SwarmStatus.COMPLETED else "agent_running",
|
||||
"metrics": {
|
||||
@@ -534,10 +536,10 @@ async def get_swarm_artifact_content(
|
||||
artifact_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Return full persisted content for a Runtime artifact URI."""
|
||||
"""Return full persisted content for a sub-mode runtime artifact URI."""
|
||||
swarm = db.query(Swarm).filter(Swarm.swarm_id == swarm_id).first()
|
||||
if not swarm:
|
||||
raise HTTPException(status_code=404, detail="Swarm not found")
|
||||
raise HTTPException(status_code=404, detail="Runtime deployment not found")
|
||||
|
||||
artifacts_by_id = {
|
||||
artifact.get("artifact_id"): artifact
|
||||
@@ -574,10 +576,10 @@ async def receive_swarm_approval_decision(
|
||||
request: ApprovalDecisionRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Accept Manager approval decisions for paused high-risk swarm actions."""
|
||||
"""Accept Manager approval decisions for paused high-risk runtime actions."""
|
||||
swarm = db.query(Swarm).filter(Swarm.swarm_id == swarm_id).first()
|
||||
if not swarm:
|
||||
raise HTTPException(status_code=404, detail="Swarm not found")
|
||||
raise HTTPException(status_code=404, detail="Runtime deployment not found")
|
||||
if request.approval_id != approval_id:
|
||||
raise HTTPException(status_code=422, detail="approval_id path/body mismatch")
|
||||
if request.decision not in {"approved", "rejected"}:
|
||||
|
||||
@@ -38,11 +38,15 @@ app.include_router(tool_generator_router)
|
||||
# 注册外部工具 API Router(符合 MCP-Server 规范)
|
||||
app.include_router(external_tool_router)
|
||||
|
||||
# 注册 Heicode Agnet API Router
|
||||
# 注册 Heicode sub-mode Runtime 主 API Router
|
||||
from api.agent.router import router as agent_router
|
||||
app.include_router(agent_router)
|
||||
|
||||
# 注册 Heicode 兼容 API Router(旧 agnet 命名)
|
||||
from api.agnet.router import router as agnet_router
|
||||
app.include_router(agnet_router)
|
||||
|
||||
# 注册 Heicode sub-mode Runtime 兼容 Router
|
||||
# 注册 Heicode sub-mode Runtime 兼容 Router(旧 /api/swarms 入口)
|
||||
from api.swarm.router import swarms_router
|
||||
app.include_router(swarms_router)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from enum import Enum
|
||||
|
||||
|
||||
class ErrorCode(str, Enum):
|
||||
"""Standard error codes for /api/agnet/* endpoints."""
|
||||
"""Standard error codes for sub-mode runtime API endpoints."""
|
||||
|
||||
# Authentication
|
||||
UNAUTHORIZED = "UNAUTHORIZED"
|
||||
|
||||
+3
-2
@@ -35,6 +35,7 @@ echo ""
|
||||
echo "✅ Deployment complete!"
|
||||
echo ""
|
||||
echo "📝 Next steps:"
|
||||
echo " 1. Port forward: kubectl port-forward -n agent-manager svc/agent-manager 8000:8000"
|
||||
echo " 2. Test health: curl -H 'Authorization: Bearer heicode-prod-token-change-me' http://localhost:8000/api/agnet/health"
|
||||
echo " 1. Port forward: kubectl port-forward -n agent-manager svc/agent-manager 8000:80"
|
||||
echo " 2. Test health: curl http://localhost:8000/api/agent/health"
|
||||
echo " 3. Compatibility health: curl http://localhost:8000/api/agnet/health"
|
||||
echo ""
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
# Heicode Sub 模式 Runtime 对接文档
|
||||
|
||||
更新时间:2026-06-01
|
||||
|
||||
本文档描述当前仓库对外提供的 **Sub Agile / 普通 sub 模式 Runtime** 契约。
|
||||
|
||||
边界原则:
|
||||
|
||||
- 当前仓库只负责 `sub_agile` / 普通 sub 模式 Runtime
|
||||
- 真正的 Swarm 模式不在当前仓库实现
|
||||
- `/api/swarms` 仅保留为 sub-mode compatibility API
|
||||
- `agent` 是主命名,`agnet` 是兼容命名
|
||||
|
||||
## 1. 接口分层
|
||||
|
||||
### 主接口
|
||||
|
||||
```text
|
||||
GET /api/agent/health
|
||||
|
||||
POST /api/agent/sub-agile/deployments
|
||||
GET /api/agent/sub-agile/deployments
|
||||
GET /api/agent/sub-agile/deployments/{deployment_id}
|
||||
POST /api/agent/sub-agile/deployments/{deployment_id}/stop
|
||||
POST /api/agent/sub-agile/deployments/{deployment_id}/approvals/{approval_id}
|
||||
GET /api/agent/sub-agile/deployments/{deployment_id}/logs
|
||||
GET /api/agent/sub-agile/deployments/{deployment_id}/events
|
||||
GET /api/agent/sub-agile/deployments/{deployment_id}/metrics
|
||||
|
||||
POST /api/agent/callbacks/runtime-events
|
||||
GET /api/agent/callbacks/runtime-events/schema
|
||||
|
||||
GET /api/agent/user/deployments/{deployment_id}/artifacts
|
||||
GET /api/agent/user/deployments/{deployment_id}/artifacts/{artifact_id}/content
|
||||
GET /api/agent/user/deployments/{deployment_id}/timeline
|
||||
GET /api/agent/user/deployments/{deployment_id}/sk-snapshots
|
||||
```
|
||||
|
||||
### 兼容接口
|
||||
|
||||
```text
|
||||
GET /api/agnet/health
|
||||
POST /api/agnet/deployments
|
||||
GET /api/agnet/deployments
|
||||
GET /api/agnet/deployments/{deployment_id}
|
||||
POST /api/agnet/deployments/{deployment_id}/stop
|
||||
POST /api/agnet/deployments/{deployment_id}/approvals/{approval_id}
|
||||
GET /api/agnet/deployments/{deployment_id}/logs
|
||||
GET /api/agnet/deployments/{deployment_id}/events
|
||||
GET /api/agnet/deployments/{deployment_id}/metrics
|
||||
POST /api/agnet/callbacks/swarm-events
|
||||
GET /api/agnet/callbacks/swarm-events/schema
|
||||
GET /api/agnet/user/deployments/{deployment_id}/artifacts
|
||||
GET /api/agnet/user/deployments/{deployment_id}/artifacts/{artifact_id}/content
|
||||
GET /api/agnet/user/deployments/{deployment_id}/timeline
|
||||
GET /api/agnet/user/deployments/{deployment_id}/sk-snapshots
|
||||
```
|
||||
|
||||
### Legacy runtime compatibility API
|
||||
|
||||
```text
|
||||
POST /api/swarms
|
||||
GET /api/swarms/{swarm_id}
|
||||
GET /api/swarms/{swarm_id}/status
|
||||
POST /api/swarms/{swarm_id}/stop
|
||||
GET /api/swarms/{swarm_id}/logs
|
||||
GET /api/swarms/{swarm_id}/events
|
||||
GET /api/swarms/{swarm_id}/metrics
|
||||
GET /api/swarms/{swarm_id}/artifacts/{artifact_id}/content
|
||||
POST /api/swarms/{swarm_id}/approvals/{approval_id}
|
||||
```
|
||||
|
||||
## 2. 状态语义
|
||||
|
||||
当前对外统一投影为:
|
||||
|
||||
```text
|
||||
accepted
|
||||
running
|
||||
waiting_approval
|
||||
completed
|
||||
failed
|
||||
stopped
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 新创建 deployment 默认返回 `accepted`
|
||||
- `approval.requested` 会把展示状态投影为 `waiting_approval`
|
||||
- `/api/swarms` 内部仍复用旧 runtime 记录,但对外返回统一状态集合
|
||||
|
||||
## 3. 创建与回调
|
||||
|
||||
### 创建
|
||||
|
||||
Manager 可调用主接口:
|
||||
|
||||
```text
|
||||
POST /api/agent/sub-agile/deployments
|
||||
```
|
||||
|
||||
兼容期仍可调用:
|
||||
|
||||
```text
|
||||
POST /api/agnet/deployments
|
||||
POST /api/swarms
|
||||
```
|
||||
|
||||
### 回调
|
||||
|
||||
主回调入口:
|
||||
|
||||
```text
|
||||
POST /api/agent/callbacks/runtime-events
|
||||
```
|
||||
|
||||
兼容回调入口:
|
||||
|
||||
```text
|
||||
POST /api/agnet/callbacks/swarm-events
|
||||
```
|
||||
|
||||
当前支持的关键事件包括:
|
||||
|
||||
- `deployment.status_changed`
|
||||
- `phase.changed`
|
||||
- `timeline.updated`
|
||||
- `agent.started`
|
||||
- `agent.completed`
|
||||
- `agent.crashed`
|
||||
- `approval.requested`
|
||||
- `artifact.created`
|
||||
- `task.completed`
|
||||
- `task.failed`
|
||||
- `task.blocked`
|
||||
- `sk_tool.called`
|
||||
- `sk_tool.completed`
|
||||
- `sk_tool.failed`
|
||||
- `budget.alert`
|
||||
|
||||
## 4. 产物读取
|
||||
|
||||
推荐路径:
|
||||
|
||||
```text
|
||||
GET /api/agent/user/deployments/{deployment_id}/artifacts
|
||||
GET /api/agent/user/deployments/{deployment_id}/artifacts/{artifact_id}/content
|
||||
```
|
||||
|
||||
兼容路径:
|
||||
|
||||
```text
|
||||
GET /api/agnet/user/deployments/{deployment_id}/artifacts
|
||||
GET /api/agnet/user/deployments/{deployment_id}/artifacts/{artifact_id}/content
|
||||
GET /api/swarms/{swarm_id}/artifacts/{artifact_id}/content
|
||||
```
|
||||
|
||||
约定:
|
||||
|
||||
- 有真实 artifact 时优先返回真实 artifact
|
||||
- 无真实 artifact 的终态运行会生成 synthesized fallback artifact
|
||||
- synthesized artifact 会稳定标记 `metadata.synthesized = true`
|
||||
|
||||
## 5. 健康检查与部署
|
||||
|
||||
K8s 建议探活路径:
|
||||
|
||||
```text
|
||||
GET /api/agent/health
|
||||
```
|
||||
|
||||
兼容探活仍可使用:
|
||||
|
||||
```text
|
||||
GET /api/agnet/health
|
||||
```
|
||||
|
||||
对应部署文件:
|
||||
|
||||
- [k8s/agent-manager-deployment.yaml](/Users/mac/Projects/agent-manager/tools/agent-manager/k8s/agent-manager-deployment.yaml)
|
||||
- [k8s/deployment.yaml](/Users/mac/Projects/agent-manager/tools/agent-manager/k8s/deployment.yaml)
|
||||
- [k8s/deployment-with-kubeconfig.yaml](/Users/mac/Projects/agent-manager/tools/agent-manager/k8s/deployment-with-kubeconfig.yaml)
|
||||
|
||||
## 6. 验证建议
|
||||
|
||||
最小验证:
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8000/api/agent/health
|
||||
curl http://127.0.0.1:8000/api/agnet/health
|
||||
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python3 -m pytest tests/test_sub_mode_runtime_contract.py -q
|
||||
```
|
||||
|
||||
本次收敛改造关注点:
|
||||
|
||||
- 主接口切换到 `agent`
|
||||
- 兼容层继续保留
|
||||
- `/api/swarms` 明确降级为 compatibility API
|
||||
- 不在当前仓库内新增任何真正 swarm-only 接口
|
||||
+62
-378
@@ -1,414 +1,98 @@
|
||||
# Agent Manager Kubernetes部署文档
|
||||
# Agent Manager K8s 部署说明
|
||||
|
||||
## 概述
|
||||
本文档对应当前仓库的 **Heicode Sub 模式 Runtime** 部署方式。
|
||||
|
||||
本文档说明如何在Azure Kubernetes Service (AKS)上部署Agent Manager服务。
|
||||
当前接口边界:
|
||||
|
||||
## 前提条件
|
||||
- 主接口:`/api/agent/sub-agile/*`
|
||||
- 主回调:`/api/agent/callbacks/runtime-events`
|
||||
- 兼容接口:`/api/agnet/*`
|
||||
- 兼容 Runtime 入口:`/api/swarms/*`
|
||||
|
||||
1. **Azure资源**
|
||||
- Azure订阅
|
||||
- AKS集群
|
||||
- Azure Container Registry (ACR)
|
||||
- Azure DNS Zone(用于自动配置域名)
|
||||
## 目录与用途
|
||||
|
||||
2. **本地工具**
|
||||
- `kubectl` (Kubernetes命令行工具)
|
||||
- `az` (Azure CLI)
|
||||
- Docker (用于构建镜像)
|
||||
- `agent-manager-namespace.yaml`: 运行命名空间
|
||||
- `agent-manager-rbac.yaml`: ServiceAccount、ClusterRole、ClusterRoleBinding
|
||||
- `agent-manager-secret.yaml`: 运行时 Secret
|
||||
- `agent-manager-configmap.yaml`: 运行时配置
|
||||
- `agent-manager-deployment.yaml`: 主部署清单
|
||||
- `agent-manager-service.yaml`: LoadBalancer 服务
|
||||
- `deploy.sh`: 标准部署脚本
|
||||
- `deploy-with-kubeconfig.sh`: 使用 `kubeconfig-secret` 的部署脚本
|
||||
|
||||
3. **权限要求**
|
||||
- AKS集群的管理员权限
|
||||
- ACR的推送权限
|
||||
- DNS Zone的管理权限
|
||||
## 关键约定
|
||||
|
||||
## 部署步骤
|
||||
- 命名空间统一使用 `agent-manager`
|
||||
- K8s 探活统一检查 `GET /api/agent/health`
|
||||
- 运行时配置中的 `NAMESPACE_PREFIX` 已更新为 `agent`
|
||||
- `/api/swarms` 仅作为 sub-mode compatibility API 保留,不表示本仓库实现独立 Swarm 产品
|
||||
|
||||
### 1. 配置Azure凭据
|
||||
|
||||
#### 1.1 创建Service Principal(如果还没有)
|
||||
## 标准部署
|
||||
|
||||
```bash
|
||||
# 创建Service Principal
|
||||
az ad sp create-for-rbac \
|
||||
--name "agent-manager-sp" \
|
||||
--role contributor \
|
||||
--scopes /subscriptions/{subscription-id}
|
||||
|
||||
# 输出示例:
|
||||
# {
|
||||
# "appId": "c5ba26db-f180-425f-bac3-93708d853988",
|
||||
# "displayName": "agent-manager-sp",
|
||||
# "password": "ydt8Q~...",
|
||||
# "tenant": "263c3ff6-1be5-4141-8308-b188464fb297"
|
||||
# }
|
||||
```
|
||||
|
||||
#### 1.2 配置DNS权限(重要!⚠️)
|
||||
|
||||
**Agent Manager需要DNS Zone Contributor权限才能为创建的agent自动配置域名。**
|
||||
|
||||
使用提供的脚本配置DNS权限:
|
||||
|
||||
```bash
|
||||
# 方法1:使用自动化脚本(推荐)
|
||||
bash scripts/setup_dns_permissions.sh
|
||||
|
||||
# 方法2:手动配置
|
||||
AZURE_CLIENT_ID="your-service-principal-app-id"
|
||||
AZURE_SUBSCRIPTION_ID="your-subscription-id"
|
||||
AZURE_RESOURCE_GROUP="your-resource-group"
|
||||
AZURE_DNS_ZONE="your-dns-zone.com"
|
||||
|
||||
DNS_ZONE_ID="/subscriptions/$AZURE_SUBSCRIPTION_ID/resourceGroups/$AZURE_RESOURCE_GROUP/providers/Microsoft.Network/dnsZones/$AZURE_DNS_ZONE"
|
||||
|
||||
az role assignment create \
|
||||
--assignee $AZURE_CLIENT_ID \
|
||||
--role "DNS Zone Contributor" \
|
||||
--scope $DNS_ZONE_ID
|
||||
```
|
||||
|
||||
验证权限:
|
||||
```bash
|
||||
az role assignment list \
|
||||
--assignee $AZURE_CLIENT_ID \
|
||||
--scope $DNS_ZONE_ID \
|
||||
--output table
|
||||
```
|
||||
|
||||
#### 1.3 更新Kubernetes Secret
|
||||
|
||||
编辑 `k8s/agent-manager-secret.yaml`:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: agent-manager-secret
|
||||
namespace: agent-manager
|
||||
type: Opaque
|
||||
stringData:
|
||||
AZURE_TENANT_ID: "263c3ff6-1be5-4141-8308-b188464fb297"
|
||||
AZURE_CLIENT_ID: "c5ba26db-f180-425f-bac3-93708d853988"
|
||||
AZURE_CLIENT_SECRET: "your-client-secret"
|
||||
AZURE_SUBSCRIPTION_ID: "45d7a360-af09-40fc-9afc-56dc475245ec"
|
||||
AZURE_RESOURCE_GROUP: "taiji-ai-v0"
|
||||
AZURE_DNS_ZONE: "taijiagnet.com"
|
||||
```
|
||||
|
||||
### 2. 配置ACR访问
|
||||
|
||||
创建ACR secret:
|
||||
|
||||
```bash
|
||||
# 获取ACR登录服务器
|
||||
ACR_NAME="your-acr-name"
|
||||
ACR_LOGIN_SERVER="${ACR_NAME}.azurecr.io"
|
||||
|
||||
# 创建Docker registry secret
|
||||
kubectl create secret docker-registry acr-secret \
|
||||
--namespace agent-manager \
|
||||
--docker-server=$ACR_LOGIN_SERVER \
|
||||
--docker-username=$AZURE_CLIENT_ID \
|
||||
--docker-password=$AZURE_CLIENT_SECRET
|
||||
```
|
||||
|
||||
或使用脚本:
|
||||
```bash
|
||||
bash k8s/create-acr-secret.sh
|
||||
```
|
||||
|
||||
### 3. 构建和推送镜像
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t $ACR_LOGIN_SERVER/agent-manager:latest .
|
||||
|
||||
# 登录ACR
|
||||
az acr login --name $ACR_NAME
|
||||
|
||||
# 推送镜像
|
||||
docker push $ACR_LOGIN_SERVER/agent-manager:latest
|
||||
```
|
||||
|
||||
### 4. 部署到Kubernetes
|
||||
|
||||
```bash
|
||||
# 创建命名空间
|
||||
kubectl apply -f k8s/agent-manager-namespace.yaml
|
||||
|
||||
# 创建RBAC(ServiceAccount、Role、RoleBinding)
|
||||
kubectl apply -f k8s/agent-manager-rbac.yaml
|
||||
|
||||
# 创建Secret(Azure凭据)
|
||||
kubectl apply -f k8s/agent-manager-secret.yaml
|
||||
|
||||
# 创建ACR Secret
|
||||
kubectl apply -f k8s/acr-secret.yaml
|
||||
|
||||
# 创建ConfigMap(可选)
|
||||
kubectl apply -f k8s/agent-manager-configmap.yaml
|
||||
|
||||
# 创建Deployment
|
||||
kubectl apply -f k8s/acr-secret.yaml
|
||||
kubectl apply -f k8s/agent-manager-deployment.yaml
|
||||
|
||||
# 创建Service(LoadBalancer)
|
||||
kubectl apply -f k8s/agent-manager-service.yaml
|
||||
```
|
||||
|
||||
或使用一键部署脚本:
|
||||
或直接执行:
|
||||
|
||||
```bash
|
||||
bash k8s/deploy.sh
|
||||
```
|
||||
|
||||
### 5. 验证部署
|
||||
## 使用 kubeconfig Secret 的部署
|
||||
|
||||
先生成 Secret:
|
||||
|
||||
```bash
|
||||
bash k8s/create-kubeconfig-secret.sh
|
||||
```
|
||||
|
||||
再执行:
|
||||
|
||||
```bash
|
||||
bash k8s/deploy-with-kubeconfig.sh
|
||||
```
|
||||
|
||||
## 部署后检查
|
||||
|
||||
```bash
|
||||
# 检查Pod状态
|
||||
kubectl get pods -n agent-manager
|
||||
|
||||
# 检查Service和外网IP
|
||||
kubectl get svc -n agent-manager
|
||||
|
||||
# 查看日志
|
||||
kubectl logs -n agent-manager deployment/agent-manager
|
||||
|
||||
# 测试健康检查
|
||||
AGENT_MANAGER_IP=$(kubectl get svc agent-manager -n agent-manager -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||
curl http://$AGENT_MANAGER_IP/
|
||||
kubectl port-forward -n agent-manager svc/agent-manager 8000:80
|
||||
curl http://127.0.0.1:8000/api/agent/health
|
||||
curl http://127.0.0.1:8000/api/agnet/health
|
||||
```
|
||||
|
||||
### 6. 测试Agent创建
|
||||
## 运行时配置项
|
||||
|
||||
```bash
|
||||
# 创建测试agent
|
||||
curl -X POST http://$AGENT_MANAGER_IP/agents \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "test-agent",
|
||||
"template": "echo_agent",
|
||||
"framework": "API",
|
||||
"config": {
|
||||
"user_id": "test-user"
|
||||
}
|
||||
}'
|
||||
`agent-manager-configmap.yaml` 中与本次对接直接相关的键:
|
||||
|
||||
# 检查返回结果应包含:
|
||||
# - external_ip: 外网IP地址
|
||||
# - domain: 自动配置的域名 (test-agent.taijiagnet.com)
|
||||
# - domain_url: 域名访问地址
|
||||
# - recommended: 推荐访问地址(域名)
|
||||
- `NAMESPACE`
|
||||
- `REDIS_URL`
|
||||
- `HEICODE_NEWAPI_BASE_URL`
|
||||
- `LITELLM_BASE_URL`
|
||||
- `NAMESPACE_PREFIX`
|
||||
- `RUNTIME_ARTIFACT_*`
|
||||
|
||||
# 查看agent状态
|
||||
curl http://$AGENT_MANAGER_IP/agents/test-agent/status
|
||||
`agent-manager-secret.yaml` 中至少需要正确配置:
|
||||
|
||||
# 测试域名访问
|
||||
curl http://test-agent.taijiagnet.com/
|
||||
- `HEICODE_SERVICE_TOKEN`
|
||||
- `AZURE_*`
|
||||
- `AZURE_STORAGE_*`
|
||||
- `VAULT_TOKEN`(如果当前环境启用 Vault)
|
||||
|
||||
# 清理测试agent
|
||||
curl -X DELETE http://$AGENT_MANAGER_IP/agents/test-agent
|
||||
```
|
||||
## 发布建议
|
||||
|
||||
## 目录结构
|
||||
更新代码后,建议同步修改:
|
||||
|
||||
```
|
||||
k8s/
|
||||
├── README.md # 本文档
|
||||
├── agent-manager-namespace.yaml # Namespace定义
|
||||
├── agent-manager-rbac.yaml # RBAC配置(ServiceAccount、Role等)
|
||||
├── agent-manager-secret.yaml # Azure凭据Secret
|
||||
├── agent-manager-configmap.yaml # 配置文件ConfigMap
|
||||
├── agent-manager-deployment.yaml # Deployment定义
|
||||
├── agent-manager-service.yaml # LoadBalancer Service定义
|
||||
├── acr-secret.yaml # ACR访问Secret
|
||||
├── create-acr-secret.sh # 创建ACR Secret脚本
|
||||
└── deploy.sh # 一键部署脚本
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: Agent创建后没有返回域名信息
|
||||
|
||||
**症状:** 创建agent时返回外网IP但没有`domain`字段。
|
||||
|
||||
**原因:** Service Principal缺少DNS Zone的写权限。
|
||||
|
||||
**解决方案:**
|
||||
```bash
|
||||
# 运行DNS权限配置脚本
|
||||
bash scripts/setup_dns_permissions.sh
|
||||
|
||||
# 或手动分配权限(见上文"配置DNS权限"部分)
|
||||
```
|
||||
|
||||
详细信息见:`docs/DNS_ISSUE_FIX_REPORT.md`
|
||||
|
||||
### Q2: Pod启动失败,提示ImagePullBackOff
|
||||
|
||||
**原因:** 无法从ACR拉取镜像。
|
||||
|
||||
**解决方案:**
|
||||
1. 检查ACR secret是否正确创建
|
||||
2. 验证Service Principal有ACR的pull权限
|
||||
3. 确认镜像名称和标签正确
|
||||
|
||||
```bash
|
||||
# 检查ACR secret
|
||||
kubectl get secret acr-secret -n agent-manager
|
||||
|
||||
# 重新创建ACR secret
|
||||
bash k8s/create-acr-secret.sh
|
||||
```
|
||||
|
||||
### Q3: LoadBalancer IP一直处于Pending状态
|
||||
|
||||
**原因:** AKS集群配置或云提供商问题。
|
||||
|
||||
**解决方案:**
|
||||
1. 检查AKS集群是否支持LoadBalancer
|
||||
2. 查看Service事件:`kubectl describe svc agent-manager -n agent-manager`
|
||||
3. 确认Azure订阅有足够的配额
|
||||
|
||||
### Q4: 如何更新部署
|
||||
|
||||
```bash
|
||||
# 方法1:修改YAML文件后重新应用
|
||||
kubectl apply -f k8s/agent-manager-deployment.yaml
|
||||
|
||||
# 方法2:更新镜像
|
||||
kubectl set image deployment/agent-manager \
|
||||
agent-manager=your-acr.azurecr.io/agent-manager:new-tag \
|
||||
-n agent-manager
|
||||
|
||||
# 方法3:编辑Deployment
|
||||
kubectl edit deployment agent-manager -n agent-manager
|
||||
|
||||
# 查看滚动更新状态
|
||||
kubectl rollout status deployment/agent-manager -n agent-manager
|
||||
```
|
||||
|
||||
### Q5: 如何查看日志
|
||||
|
||||
```bash
|
||||
# 查看所有Pod日志
|
||||
kubectl logs -n agent-manager -l app=agent-manager
|
||||
|
||||
# 查看特定Pod日志
|
||||
kubectl logs -n agent-manager <pod-name>
|
||||
|
||||
# 实时跟踪日志
|
||||
kubectl logs -n agent-manager -l app=agent-manager -f
|
||||
|
||||
# 查看前一个容器的日志(如果Pod重启过)
|
||||
kubectl logs -n agent-manager <pod-name> --previous
|
||||
```
|
||||
|
||||
## 监控和维护
|
||||
|
||||
### 资源使用
|
||||
|
||||
```bash
|
||||
# 查看Pod资源使用
|
||||
kubectl top pods -n agent-manager
|
||||
|
||||
# 查看Node资源使用
|
||||
kubectl top nodes
|
||||
```
|
||||
|
||||
### 扩缩容
|
||||
|
||||
```bash
|
||||
# 手动扩容
|
||||
kubectl scale deployment agent-manager \
|
||||
--replicas=3 \
|
||||
-n agent-manager
|
||||
|
||||
# 自动扩缩容(HPA)
|
||||
kubectl autoscale deployment agent-manager \
|
||||
--cpu-percent=80 \
|
||||
--min=2 \
|
||||
--max=10 \
|
||||
-n agent-manager
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
|
||||
Agent Manager提供以下健康检查端点:
|
||||
|
||||
- `GET /` - 基本健康检查
|
||||
- `GET /templates` - 模板列表(验证数据库连接)
|
||||
- `GET /agents` - Agent列表(验证K8s连接)
|
||||
|
||||
## 安全最佳实践
|
||||
|
||||
1. **Secret管理**
|
||||
- 不要将Secret提交到版本控制
|
||||
- 使用Azure Key Vault或Kubernetes Secrets加密
|
||||
- 定期轮换凭据
|
||||
|
||||
2. **RBAC**
|
||||
- 使用最小权限原则
|
||||
- 为不同环境使用不同的Service Principal
|
||||
- 定期审计权限分配
|
||||
|
||||
3. **网络安全**
|
||||
- 考虑使用Private LoadBalancer
|
||||
- 配置Network Policy限制Pod间通信
|
||||
- 使用Ingress Controller配置TLS
|
||||
|
||||
4. **镜像安全**
|
||||
- 定期扫描镜像漏洞
|
||||
- 使用最新的基础镜像
|
||||
- 不要在镜像中包含敏感信息
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 诊断命令
|
||||
|
||||
```bash
|
||||
# 检查所有资源
|
||||
kubectl get all -n agent-manager
|
||||
|
||||
# 查看Pod详情
|
||||
kubectl describe pod <pod-name> -n agent-manager
|
||||
|
||||
# 查看事件
|
||||
kubectl get events -n agent-manager --sort-by='.lastTimestamp'
|
||||
|
||||
# 检查ServiceAccount
|
||||
kubectl get sa -n agent-manager
|
||||
kubectl describe sa agent-manager-sa -n agent-manager
|
||||
|
||||
# 检查RoleBinding
|
||||
kubectl get rolebinding -n agent-manager
|
||||
kubectl describe rolebinding agent-manager-role-binding -n agent-manager
|
||||
|
||||
# 进入Pod调试
|
||||
kubectl exec -it <pod-name> -n agent-manager -- /bin/bash
|
||||
```
|
||||
|
||||
### 日志级别
|
||||
|
||||
在Deployment中设置环境变量调整日志级别:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
- name: LOG_LEVEL
|
||||
value: "DEBUG" # DEBUG, INFO, WARNING, ERROR
|
||||
```
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Kubernetes官方文档](https://kubernetes.io/docs/)
|
||||
- [Azure Kubernetes Service文档](https://docs.microsoft.com/azure/aks/)
|
||||
- [Azure DNS文档](https://docs.microsoft.com/azure/dns/)
|
||||
- [Agent Manager API文档](../docs/API_DOCUMENTATION.md)
|
||||
- [DNS问题修复报告](../docs/DNS_ISSUE_FIX_REPORT.md)
|
||||
|
||||
## 联系支持
|
||||
|
||||
如有问题,请:
|
||||
1. 查看本文档的常见问题部分
|
||||
2. 查看`docs/DNS_ISSUE_FIX_REPORT.md`
|
||||
3. 查看agent-manager日志
|
||||
4. 联系开发团队
|
||||
1. 镜像 tag
|
||||
2. `agent-manager-deployment.yaml` 中的 `image`
|
||||
3. 对接文档中的版本记录
|
||||
4. 部署后健康检查与契约测试结果
|
||||
|
||||
+1
-1
@@ -4,5 +4,5 @@ data:
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: acr-secret
|
||||
namespace: default
|
||||
namespace: agent-manager
|
||||
type: kubernetes.io/dockerconfigjson
|
||||
|
||||
@@ -7,11 +7,11 @@ data:
|
||||
NAMESPACE: "agent-manager"
|
||||
DATABASE_URL: "postgresql://taiji:By%40123456.@taijipda.postgres.database.azure.com:5432/taijiagnet"
|
||||
|
||||
# Heicode Integration (NEW)
|
||||
# Heicode sub-mode runtime integration
|
||||
REDIS_URL: "redis://localhost:6379/0"
|
||||
HEICODE_NEWAPI_BASE_URL: "https://code.xinghanlab.com"
|
||||
LITELLM_BASE_URL: "http://litellm-service:8000"
|
||||
NAMESPACE_PREFIX: "agnet"
|
||||
NAMESPACE_PREFIX: "agent"
|
||||
MAX_CONCURRENT_DEPLOYMENTS_PER_USER: "10"
|
||||
MAX_CONCURRENT_DEPLOYMENTS_PER_SCOPE: "50"
|
||||
RUNTIME_ARTIFACT_BACKEND: "azblob"
|
||||
|
||||
@@ -102,7 +102,7 @@ spec:
|
||||
# 健康检查
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
path: /api/agent/health
|
||||
port: 8000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
@@ -111,7 +111,7 @@ spec:
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
path: /api/agent/health
|
||||
port: 8000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
|
||||
@@ -29,7 +29,7 @@ kubectl create secret docker-registry acr-secret \
|
||||
--docker-server=$REGISTRY_URL \
|
||||
--docker-username=$REGISTRY_USERNAME \
|
||||
--docker-password=$REGISTRY_PASSWORD \
|
||||
--namespace=default \
|
||||
--namespace=agent-manager \
|
||||
--dry-run=client -o yaml > k8s/acr-secret.yaml
|
||||
|
||||
echo ""
|
||||
|
||||
@@ -13,7 +13,7 @@ fi
|
||||
# 创建Secret
|
||||
kubectl create secret generic kubeconfig-secret \
|
||||
--from-file=config=$KUBECONFIG_FILE \
|
||||
--namespace=default \
|
||||
--namespace=agent-manager \
|
||||
--dry-run=client -o yaml > k8s/kubeconfig-secret.yaml
|
||||
|
||||
echo "✅ Secret配置已生成: k8s/kubeconfig-secret.yaml"
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
echo "开始部署Agent Manager (使用Kubeconfig Secret)..."
|
||||
|
||||
# 1. 创建命名空间
|
||||
echo "1. 创建ai-agents命名空间..."
|
||||
kubectl apply -f k8s/namespace.yaml
|
||||
echo "1. 创建agent-manager命名空间..."
|
||||
kubectl apply -f k8s/agent-manager-namespace.yaml
|
||||
|
||||
# 2. 创建ACR访问密钥
|
||||
echo "2. 创建ACR访问密钥..."
|
||||
@@ -23,26 +23,31 @@ if [ ! -f "k8s/kubeconfig-secret.yaml" ]; then
|
||||
fi
|
||||
kubectl apply -f k8s/kubeconfig-secret.yaml
|
||||
|
||||
# 4. 部署Agent Manager服务(使用kubeconfig)
|
||||
echo "4. 部署Agent Manager..."
|
||||
# 4. 创建运行时 Secret / ConfigMap
|
||||
echo "4. 配置Secret和ConfigMap..."
|
||||
kubectl apply -f k8s/agent-manager-secret.yaml
|
||||
kubectl apply -f k8s/agent-manager-configmap.yaml
|
||||
|
||||
# 5. 部署Agent Manager服务(使用kubeconfig)
|
||||
echo "5. 部署Agent Manager..."
|
||||
kubectl apply -f k8s/deployment-with-kubeconfig.yaml
|
||||
|
||||
# 5. 等待部署完成
|
||||
echo "5. 等待Pod就绪..."
|
||||
kubectl wait --for=condition=ready pod -l app=agent-manager -n default --timeout=120s
|
||||
# 6. 等待部署完成
|
||||
echo "6. 等待Pod就绪..."
|
||||
kubectl wait --for=condition=ready pod -l app=agent-manager -n agent-manager --timeout=120s
|
||||
|
||||
# 6. 显示服务状态
|
||||
# 7. 显示服务状态
|
||||
echo ""
|
||||
echo "✅ 部署完成!"
|
||||
echo ""
|
||||
echo "服务状态:"
|
||||
kubectl get pods -n default -l app=agent-manager
|
||||
kubectl get pods -n agent-manager -l app=agent-manager
|
||||
echo ""
|
||||
echo "服务信息:"
|
||||
kubectl get svc -n default -l app=agent-manager
|
||||
kubectl get svc -n agent-manager -l app=agent-manager
|
||||
echo ""
|
||||
echo "查看日志:"
|
||||
echo "kubectl logs -n default -l app=agent-manager -f"
|
||||
echo "kubectl logs -n agent-manager deployment/agent-manager -f"
|
||||
echo ""
|
||||
echo "访问服务:"
|
||||
echo "kubectl port-forward -n default svc/agent-manager 8000:8000"
|
||||
echo "kubectl port-forward -n agent-manager svc/agent-manager 8000:8000"
|
||||
|
||||
+20
-14
@@ -4,8 +4,8 @@
|
||||
echo "开始部署Agent Manager (使用ServiceAccount + RBAC)..."
|
||||
|
||||
# 1. 创建命名空间
|
||||
echo "1. 创建ai-agents命名空间..."
|
||||
kubectl apply -f k8s/namespace.yaml
|
||||
echo "1. 创建agent-manager命名空间..."
|
||||
kubectl apply -f k8s/agent-manager-namespace.yaml
|
||||
|
||||
# 2. 创建ACR访问密钥
|
||||
echo "2. 创建ACR访问密钥..."
|
||||
@@ -17,28 +17,34 @@ kubectl apply -f k8s/acr-secret.yaml
|
||||
|
||||
# 3. 配置RBAC权限
|
||||
echo "3. 配置RBAC权限..."
|
||||
kubectl apply -f k8s/rbac.yaml
|
||||
kubectl apply -f k8s/agent-manager-rbac.yaml
|
||||
|
||||
# 4. 部署Agent Manager服务
|
||||
echo "4. 部署Agent Manager..."
|
||||
kubectl apply -f k8s/deployment.yaml
|
||||
# 4. 创建运行时 Secret / ConfigMap
|
||||
echo "4. 配置Secret和ConfigMap..."
|
||||
kubectl apply -f k8s/agent-manager-secret.yaml
|
||||
kubectl apply -f k8s/agent-manager-configmap.yaml
|
||||
|
||||
# 5. 等待部署完成
|
||||
echo "5. 等待Pod就绪..."
|
||||
kubectl wait --for=condition=ready pod -l app=agent-manager -n default --timeout=120s
|
||||
# 5. 部署Agent Manager服务
|
||||
echo "5. 部署Agent Manager..."
|
||||
kubectl apply -f k8s/agent-manager-deployment.yaml
|
||||
kubectl apply -f k8s/agent-manager-service.yaml
|
||||
|
||||
# 6. 显示服务状态
|
||||
# 6. 等待部署完成
|
||||
echo "6. 等待Pod就绪..."
|
||||
kubectl wait --for=condition=ready pod -l app=agent-manager -n agent-manager --timeout=120s
|
||||
|
||||
# 7. 显示服务状态
|
||||
echo ""
|
||||
echo "✅ 部署完成!"
|
||||
echo ""
|
||||
echo "服务状态:"
|
||||
kubectl get pods -n default -l app=agent-manager
|
||||
kubectl get pods -n agent-manager -l app=agent-manager
|
||||
echo ""
|
||||
echo "服务信息:"
|
||||
kubectl get svc -n default -l app=agent-manager
|
||||
kubectl get svc -n agent-manager -l app=agent-manager
|
||||
echo ""
|
||||
echo "查看日志:"
|
||||
echo "kubectl logs -n default -l app=agent-manager -f"
|
||||
echo "kubectl logs -n agent-manager deployment/agent-manager -f"
|
||||
echo ""
|
||||
echo "访问服务:"
|
||||
echo "kubectl port-forward -n default svc/agent-manager 8000:8000"
|
||||
echo "kubectl port-forward -n agent-manager svc/agent-manager 8000:80"
|
||||
|
||||
@@ -3,7 +3,7 @@ apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: agent-manager
|
||||
namespace: default
|
||||
namespace: agent-manager
|
||||
labels:
|
||||
app: agent-manager
|
||||
spec:
|
||||
@@ -29,7 +29,7 @@ spec:
|
||||
name: http
|
||||
env:
|
||||
- name: NAMESPACE
|
||||
value: "ai-agents"
|
||||
value: "agent-manager"
|
||||
- name: SERVICE_PORT
|
||||
value: "8000"
|
||||
- name: SERVICE_HOST
|
||||
@@ -49,13 +49,13 @@ spec:
|
||||
readOnly: true
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
path: /api/agent/health
|
||||
port: 8000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
path: /api/agent/health
|
||||
port: 8000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
@@ -68,7 +68,7 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: agent-manager
|
||||
namespace: default
|
||||
namespace: agent-manager
|
||||
labels:
|
||||
app: agent-manager
|
||||
spec:
|
||||
|
||||
+2
-2
@@ -49,13 +49,13 @@ spec:
|
||||
# readOnly: true
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
path: /api/agent/health
|
||||
port: 8000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
path: /api/agent/health
|
||||
port: 8000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
|
||||
@@ -4,4 +4,4 @@ data:
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: kubeconfig-secret
|
||||
namespace: default
|
||||
namespace: agent-manager
|
||||
|
||||
@@ -2,7 +2,7 @@ apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: kubeconfig-secret
|
||||
namespace: default
|
||||
namespace: agent-manager
|
||||
type: Opaque
|
||||
data:
|
||||
config: |
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: agent-manager
|
||||
namespace: default
|
||||
namespace: agent-manager
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
@@ -33,7 +33,7 @@ metadata:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: agent-manager
|
||||
namespace: default
|
||||
namespace: agent-manager
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: agent-manager-role
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Contract tests for the sub-mode runtime API surfaces."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from api.status_projection import RuntimeDisplayStatus, project_deployment_status, project_runtime_run_status
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _read(relative_path: str) -> str:
|
||||
"""Load a repository file as text."""
|
||||
return (ROOT / relative_path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_primary_and_compat_runtime_routes_are_registered():
|
||||
"""The new agent surface and both compatibility surfaces must coexist."""
|
||||
app_source = _read("app.py")
|
||||
agent_router_source = _read("api/agent/router.py")
|
||||
callbacks_source = _read("api/agnet/callbacks.py")
|
||||
swarm_router_source = _read("api/swarm/router.py")
|
||||
|
||||
assert "from api.agent.router import router as agent_router" in app_source
|
||||
assert 'prefix="/api/agent"' in agent_router_source
|
||||
assert 'prefix="/sub-agile"' in agent_router_source
|
||||
assert '@router.post("/runtime-events")' in callbacks_source
|
||||
assert '@compat_router.post("/swarm-events")' in callbacks_source
|
||||
assert 'prefix="/api/swarms"' in swarm_router_source
|
||||
assert '@swarms_router.post("/{swarm_id}/approvals/{approval_id}")' in swarm_router_source
|
||||
|
||||
|
||||
def test_projected_statuses_cover_manager_facing_contract():
|
||||
"""Projected deployment statuses should match the Manager contract."""
|
||||
assert project_deployment_status("pending") == RuntimeDisplayStatus.ACCEPTED.value
|
||||
assert (
|
||||
project_deployment_status(
|
||||
"running",
|
||||
events=[{"event_type": "approval.requested", "payload": {"approval_id": "appr_1"}}],
|
||||
)
|
||||
== RuntimeDisplayStatus.WAITING_APPROVAL.value
|
||||
)
|
||||
assert (
|
||||
project_deployment_status(
|
||||
"running",
|
||||
phase="done",
|
||||
events=[{"event_type": "task.completed", "payload": {"status": "completed"}}],
|
||||
)
|
||||
== RuntimeDisplayStatus.COMPLETED.value
|
||||
)
|
||||
assert (
|
||||
project_deployment_status(
|
||||
"running",
|
||||
events=[{"event_type": "task.failed", "payload": {"status": "failed"}}],
|
||||
)
|
||||
== RuntimeDisplayStatus.FAILED.value
|
||||
)
|
||||
assert project_deployment_status("stopped") == RuntimeDisplayStatus.STOPPED.value
|
||||
|
||||
|
||||
def test_runtime_run_projection_remains_sub_mode_compatible():
|
||||
"""Legacy /api/swarms runs should expose the shared projected status set."""
|
||||
assert project_runtime_run_status("initializing") == RuntimeDisplayStatus.ACCEPTED.value
|
||||
assert project_runtime_run_status("running") == RuntimeDisplayStatus.RUNNING.value
|
||||
assert project_runtime_run_status("completed") == RuntimeDisplayStatus.COMPLETED.value
|
||||
assert project_runtime_run_status("failed") == RuntimeDisplayStatus.FAILED.value
|
||||
assert project_runtime_run_status("stopped") == RuntimeDisplayStatus.STOPPED.value
|
||||
Reference in New Issue
Block a user