Refine sub-mode runtime agent API surface

This commit is contained in:
elipitc
2026-06-01 19:36:09 +08:00
parent 532dca13f4
commit 5e248a09bb
30 changed files with 679 additions and 502 deletions
+5
View File
@@ -0,0 +1,5 @@
"""Primary agent API module for Heicode sub-mode runtime."""
from .router import router
__all__ = ["router"]
+37
View File
@@ -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
View File
@@ -1 +1 @@
"""Agnet API module for Heicode integration."""
"""Legacy agnet compatibility module for Heicode sub-mode runtime APIs."""
+25 -14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
}
+105
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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
View File
@@ -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"}:
+6 -2
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -4,5 +4,5 @@ data:
kind: Secret
metadata:
name: acr-secret
namespace: default
namespace: agent-manager
type: kubernetes.io/dockerconfigjson
+2 -2
View File
@@ -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"
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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 ""
+1 -1
View File
@@ -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"
+17 -12
View File
@@ -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
View File
@@ -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"
+5 -5
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -4,4 +4,4 @@ data:
kind: Secret
metadata:
name: kubeconfig-secret
namespace: default
namespace: agent-manager
+1 -1
View File
@@ -2,7 +2,7 @@ apiVersion: v1
kind: Secret
metadata:
name: kubeconfig-secret
namespace: default
namespace: agent-manager
type: Opaque
data:
config: |
+2 -2
View File
@@ -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
+66
View File
@@ -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