Expose richer sub-mode runtime workflow facts

This commit is contained in:
elipitc
2026-06-03 10:50:54 +08:00
parent aae209574a
commit 504d9a1ab0
10 changed files with 437 additions and 6 deletions
+18 -1
View File
@@ -341,6 +341,22 @@ async def get_runtime_event_schema():
"category": "artifact",
"required_payload_fields": ["artifact_id", "artifact_type", "title"],
},
"artifact.local_edit_applied": {
"category": "artifact",
"required_payload_fields": ["artifact_id", "project_revision", "manifest_uri", "archive_uri"],
},
"artifact.local_edit_reviewed": {
"category": "artifact",
"required_payload_fields": ["artifact_id", "project_revision"],
},
"artifact.local_edit_rejected": {
"category": "artifact",
"required_payload_fields": ["artifact_id", "project_revision"],
},
"artifact.local_edit_conflict": {
"category": "artifact",
"required_payload_fields": ["artifact_id", "project_revision", "base_project_revision"],
},
"approval.requested": {
"category": "approval",
"required_payload_fields": ["approval_id", "operation", "risk_level", "reason"],
@@ -399,7 +415,7 @@ async def get_runtime_event_schema():
"body_required_fields": ["event_id", "event_type", "deployment_id", "occurred_at", "payload"],
"event_types": event_types,
"stages": ["planning", "design", "development", "testing", "fixing", "deployment", "review", "done", "failed"],
"artifact_types": ["code_patch", "document", "test_report", "deployment_manifest", "log_bundle", "other"],
"artifact_types": ["project_folder", "code_patch", "document", "test_report", "deployment_manifest", "log_bundle", "other"],
}
@@ -445,6 +461,7 @@ async def list_deployment_artifacts(
"stage": artifact.get("stage"),
"checkpoint": artifact.get("checkpoint"),
"metadata": artifact.get("metadata") or {},
"source_agent_role": (artifact.get("metadata") or {}).get("source_agent_role") or (artifact.get("metadata") or {}).get("agent_role"),
"created_at": event.occurred_at,
})
+80 -1
View File
@@ -14,7 +14,7 @@ from api.agnet.models import (
ListDeploymentsResponse, GetDeploymentResponse, StopDeploymentRequest, StopDeploymentResponse,
DeploymentSummary, BudgetSummary, PaginationInfo,
GetLogsResponse, LogEntry, GetEventsResponse, EventEntry, GetMetricsResponse,
AgentMetrics, ResourceMetrics
AgentMetrics, ResourceMetrics, ArtifactEditRequest, ArtifactEditResponse
)
from api.agnet.auth import verify_service_token, extract_headers
from api.agnet.validators import validate_no_sensitive_fields, validate_vault_references
@@ -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.swarm.router import apply_runtime_artifact_edit, _phases_for_swarm, _emit_artifact_edit_event
from api.status_projection import RuntimeDisplayStatus, project_deployment_status, project_runtime_run_status
from config.error_codes import ErrorCode
from config.settings import settings
@@ -53,6 +54,26 @@ def _project_agent_instance_status(deployment_status: str, agent_status: str) ->
return agent_status
def _runtime_mode_from_orchestration_plan(orchestration_plan: str | dict | None) -> str:
"""Best-effort projection of runtime mode for list/detail surfaces."""
plan = orchestration_plan
if isinstance(orchestration_plan, str):
try:
plan = json.loads(orchestration_plan)
except Exception:
plan = {}
if not isinstance(plan, dict):
plan = {}
metadata = plan.get("metadata") if isinstance(plan.get("metadata"), dict) else {}
runtime_mode = metadata.get("runtime_mode") or plan.get("runtime_mode")
if runtime_mode == "swarm":
return "swarm"
sub_mode = plan.get("sub_mode")
if sub_mode in {"agile", "waterfall"}:
return "sub_agile"
return "sub_agile"
def generate_deployment_id() -> str:
"""Generate unique deployment ID."""
return f"dep_{uuid.uuid4().hex[:12]}"
@@ -294,11 +315,17 @@ def build_deployment_response_from_swarm(db: Session, swarm: Swarm) -> GetDeploy
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)
phases = [phase.model_dump() for phase in _phases_for_swarm(db, swarm)]
agent_phase_map = {}
for phase in phases:
for phase_agent in phase.get("agents", []):
agent_phase_map[phase_agent.get("agent_id")] = phase_agent
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=display_status,
mode="sub_agile",
phase=swarm.phase,
orchestration_plan=json.dumps(
{
@@ -327,9 +354,15 @@ def build_deployment_response_from_swarm(db: Session, swarm: Swarm) -> GetDeploy
role=agent.role,
status=_project_agent_instance_status(display_status, agent.status.value),
phase=swarm.phase,
tokens=(agent_phase_map.get(agent.agent_id) or {}).get("tokens", 0),
tools=(agent_phase_map.get(agent.agent_id) or {}).get("tools", 0),
elapsed_seconds=(agent_phase_map.get(agent.agent_id) or {}).get("elapsed_seconds", 0),
artifact_ids=(agent_phase_map.get(agent.agent_id) or {}).get("artifact_ids", []),
current_action=(agent_phase_map.get(agent.agent_id) or {}).get("current_action"),
)
for agent in agents
],
phases=phases,
resource_grants=context.get("resource_grants") or [],
created_at=swarm.created_at,
updated_at=swarm.updated_at,
@@ -574,6 +607,7 @@ async def create_deployment(
deployment_id=deployment_id,
swarm_id=deployment_id,
status=RuntimeDisplayStatus.ACCEPTED.value,
mode="sub_agile",
agent_instances=[
AgentInstanceResponse(
agent_instance_id=inst.agent_instance_id,
@@ -589,6 +623,7 @@ async def create_deployment(
"deployment_id": deployment_id,
"swarm_id": deployment_id,
"status": RuntimeDisplayStatus.ACCEPTED.value,
"mode": "sub_agile",
"estimated_ready_at": (deployment.created_at + timedelta(minutes=2)).isoformat(),
},
)
@@ -692,6 +727,7 @@ async def list_deployments(
deployment_summaries.append(DeploymentSummary(
deployment_id=dep.deployment_id,
status=display_status,
mode=_runtime_mode_from_orchestration_plan(dep.orchestration_plan),
risk_level=dep.risk_level.value,
budget=BudgetSummary(
max_usd=dep.budget_max_usd or 0.0,
@@ -748,6 +784,7 @@ async def get_deployment(
user_id=deployment.user_id,
binding_scope=deployment.binding_scope,
status=display_status,
mode=_runtime_mode_from_orchestration_plan(deployment.orchestration_plan),
phase=deployment.phase,
orchestration_plan=deployment.orchestration_plan,
risk_level=deployment.risk_level.value,
@@ -770,6 +807,7 @@ async def get_deployment(
)
for inst in instances
],
phases=[],
resource_grants=deployment.resource_grants or [],
created_at=deployment.created_at,
updated_at=deployment.updated_at
@@ -919,6 +957,47 @@ async def stop_deployment(
)
@router.post("/deployments/{deployment_id}/artifact-edits", response_model=ArtifactEditResponse)
async def receive_deployment_artifact_edit(
deployment_id: str,
request: ArtifactEditRequest,
db: Session = Depends(get_db),
token: str = Depends(verify_service_token),
):
"""Accept an accepted project revision for a runtime-backed deployment."""
deployment = db.query(Deployment).filter(
Deployment.deployment_id == deployment_id
).first()
if deployment:
raise HTTPException(
status_code=422,
detail={
"success": False,
"error": {
"code": ErrorCode.INVALID_REQUEST,
"message": "ARTIFACT_EDIT_UNSUPPORTED_FOR_LAYOUT",
},
},
)
swarm = db.query(Swarm).filter(Swarm.swarm_id == deployment_id).first()
if not swarm:
raise HTTPException(
status_code=404,
detail={
"success": False,
"error": {
"code": ErrorCode.DEPLOYMENT_NOT_FOUND,
"message": f"Deployment {deployment_id} not found",
},
},
)
response = apply_runtime_artifact_edit(db, deployment_id, request)
await _emit_artifact_edit_event(swarm, request, "artifact.local_edit_applied")
return response
@router.post("/deployments/{deployment_id}/approvals/{approval_id}")
async def receive_deployment_approval_decision(
deployment_id: str,
+31
View File
@@ -298,6 +298,11 @@ class AgentInstanceResponse(BaseModel):
role: str
status: str
phase: Optional[str] = None
tokens: int = 0
tools: int = 0
elapsed_seconds: int = 0
artifact_ids: List[str] = Field(default_factory=list)
current_action: Optional[str] = None
class CreateDeploymentResponse(BaseModel):
@@ -306,6 +311,7 @@ class CreateDeploymentResponse(BaseModel):
deployment_id: str
swarm_id: Optional[str] = None
status: str
mode: Optional[str] = None
agent_instances: List[AgentInstanceResponse]
created_at: datetime
estimated_ready_at: Optional[datetime] = None
@@ -323,6 +329,7 @@ class DeploymentSummary(BaseModel):
"""Deployment summary for list response."""
deployment_id: str
status: str
mode: Optional[str] = None
risk_level: str
budget: BudgetSummary
created_at: datetime
@@ -366,12 +373,14 @@ class GetDeploymentResponse(BaseModel):
user_id: str
binding_scope: str
status: str
mode: Optional[str] = None
phase: Optional[str]
orchestration_plan: str
risk_level: str
budget: BudgetSummary
billing_context: Dict[str, Any]
agent_instances: List[AgentInstanceResponse]
phases: List[Dict[str, Any]] = Field(default_factory=list)
resource_grants: List[Dict[str, Any]]
created_at: datetime
updated_at: datetime
@@ -463,3 +472,25 @@ class SubAgileDeploymentDetailResponse(GetDeploymentResponse):
class SubAgileDeploymentStopResponse(StopDeploymentResponse):
"""Primary response model for /api/agent/sub-agile/deployments/{id}/stop."""
class ArtifactEditRequest(BaseModel):
"""Manager-forwarded accepted revision metadata for a project artifact."""
artifact_id: str
project_revision: int
base_project_revision: Optional[int] = None
base_content_hash: Optional[str] = None
manifest_uri: str
archive_uri: str
source: str
class ArtifactEditResponse(BaseModel):
"""Runtime acknowledgement for artifact revision updates."""
success: bool = True
deployment_id: str
artifact_id: str
project_revision: int
status: str
+50
View File
@@ -77,6 +77,34 @@ class SwarmAgentInfo(BaseModel):
service_url: Optional[str] = None
current_task: Optional[str] = None
output: Optional[str] = None
tokens: int = 0
tools: int = 0
elapsed_seconds: int = 0
artifact_ids: List[str] = Field(default_factory=list)
current_action: Optional[str] = None
class SwarmPhaseAgentInfo(BaseModel):
"""Per-phase agent information for workflow/work displays."""
agent_id: str
role: str
status: str
tokens: int = 0
tools: int = 0
elapsed_seconds: int = 0
artifact_ids: List[str] = Field(default_factory=list)
current_action: Optional[str] = None
error: Optional[str] = None
summary: Optional[str] = None
class SwarmPhaseInfo(BaseModel):
"""Runtime phase summary with agent-level details."""
name: str
status: str
agents: List[SwarmPhaseAgentInfo] = Field(default_factory=list)
artifact_ids: List[str] = Field(default_factory=list)
summary: Optional[str] = None
class SwarmCreateResponse(BaseModel):
@@ -100,10 +128,12 @@ class SwarmStatusResponse(BaseModel):
"""Compatibility response model for sub-mode runtime status."""
deployment_id: Optional[str] = None
swarm_id: str
mode: Optional[str] = None
status: str
phase: Optional[str] = None
progress: int
agents: List[SwarmAgentInfo]
phases: List[SwarmPhaseInfo] = Field(default_factory=list)
metrics: SwarmMetrics
artifacts: List[Dict[str, Any]] = []
error_message: Optional[str] = None
@@ -139,3 +169,23 @@ class ApprovalDecisionRequest(BaseModel):
credential_ref: Optional[str] = None
lease_id: Optional[str] = None
lease_expires_at: Optional[int] = None
class ArtifactEditRequest(BaseModel):
"""Accepted project revision notification from Manager."""
artifact_id: str
project_revision: int
base_project_revision: Optional[int] = None
base_content_hash: Optional[str] = None
manifest_uri: str
archive_uri: str
source: str
class ArtifactEditResponse(BaseModel):
"""Runtime acknowledgement for an accepted artifact revision."""
success: bool = True
deployment_id: str
artifact_id: str
project_revision: int
status: str
+9
View File
@@ -1096,6 +1096,7 @@ class SwarmOrchestrator:
metadata = {
"redacted": True,
"agent_role": role,
"source_agent_role": role,
"runtime_deployment_id": self.swarm_id,
"summary_only": False,
"artifact_layout": "project_folder",
@@ -1105,6 +1106,7 @@ class SwarmOrchestrator:
"directory_count": stored_project.directory_count if stored_project else 1,
"manifest_uri": runtime_artifact_manifest_path(self.swarm_id, artifact_id),
"archive_uri": runtime_artifact_archive_path(self.swarm_id, artifact_id),
"project_revision": 1,
}
if stored_project:
metadata.update(
@@ -1112,6 +1114,11 @@ class SwarmOrchestrator:
"content_hash": stored_project.content_hash,
"download_path": runtime_artifact_archive_path(self.swarm_id, artifact_id),
"files_base_uri": runtime_artifact_file_path(self.swarm_id, artifact_id, ""),
"delivery_ref": {
"kind": "runtime_artifact",
"artifact_id": artifact_id,
"project_revision": 1,
},
}
)
return {
@@ -1132,6 +1139,7 @@ class SwarmOrchestrator:
metadata = {
"redacted": True,
"agent_role": role,
"source_agent_role": role,
"runtime_deployment_id": self.swarm_id,
"summary_only": False,
"artifact_layout": "single_file_content",
@@ -1224,6 +1232,7 @@ class SwarmOrchestrator:
"summary_only": True,
"artifact_layout": "single_file_content",
"primary_read_path": "content",
"project_revision": 0,
}
if stored:
metadata.update(
+224 -1
View File
@@ -29,10 +29,11 @@ from .artifact_store import (
from .models import (
SwarmCreateRequest, SwarmCreateResponse, SwarmStatusResponse,
SwarmStopRequest, SwarmStopResponse, SwarmAgentInfo, SwarmMetrics,
ApprovalDecisionRequest
ApprovalDecisionRequest, SwarmPhaseInfo, SwarmPhaseAgentInfo, ArtifactEditRequest, ArtifactEditResponse
)
from .orchestrator import SwarmOrchestrator
from api.status_projection import RuntimeDisplayStatus, project_runtime_run_status
from .callback_client import CallbackDeliveryClient
swarms_router = APIRouter(prefix="/api/swarms", tags=["sub-mode-runtime-compatibility"])
@@ -50,6 +51,41 @@ def generate_agent_id(role: str) -> str:
def _agent_infos_for_swarm(db: Session, swarm_id: str) -> list[SwarmAgentInfo]:
"""Build response agent summaries for a swarm."""
agents = db.query(SwarmAgent).filter(SwarmAgent.swarm_id == swarm_id).all()
messages = (
db.query(SwarmMessage)
.filter(SwarmMessage.swarm_id == swarm_id)
.order_by(SwarmMessage.created_at.asc())
.all()
)
artifacts_by_agent = {}
swarm = db.query(Swarm).filter(Swarm.swarm_id == swarm_id).first()
for artifact in (swarm.artifacts or []) if swarm else []:
agent_instance_id = artifact.get("agent_instance_id")
if agent_instance_id:
artifacts_by_agent.setdefault(agent_instance_id, []).append(artifact.get("artifact_id"))
def metrics_for_agent(agent_id: str):
agent_messages = [message for message in messages if agent_id in {message.from_agent_id, message.to_agent_id}]
tokens = 0
tools = 0
current_action = None
first_seen = None
last_seen = None
for message in agent_messages:
metadata = message.message_metadata or {}
usage = metadata.get("model_usage") or {}
tokens += int(usage.get("total_tokens") or 0)
if message.message_type in {"task", "task_retry", "response", "error"}:
tools += 1
if message.message_type in {"task", "task_retry"} and message.content:
current_action = message.content[:200]
if first_seen is None or message.created_at < first_seen:
first_seen = message.created_at
if last_seen is None or message.created_at > last_seen:
last_seen = message.created_at
elapsed = int((last_seen - first_seen).total_seconds()) if first_seen and last_seen else 0
return tokens, tools, elapsed, current_action
return [
SwarmAgentInfo(
agent_id=agent.agent_id,
@@ -59,11 +95,92 @@ def _agent_infos_for_swarm(db: Session, swarm_id: str) -> list[SwarmAgentInfo]:
service_url=agent.service_url,
current_task=agent.current_task,
output=agent.output,
tokens=metrics_for_agent(agent.agent_id)[0],
tools=metrics_for_agent(agent.agent_id)[1],
elapsed_seconds=metrics_for_agent(agent.agent_id)[2],
artifact_ids=artifacts_by_agent.get(agent.agent_id, []),
current_action=metrics_for_agent(agent.agent_id)[3] or agent.current_task,
)
for agent in agents
]
def _phases_for_swarm(db: Session, swarm: Swarm) -> list[SwarmPhaseInfo]:
"""Build workflow-style phase summaries from persisted runtime artifacts and messages."""
agents = db.query(SwarmAgent).filter(SwarmAgent.swarm_id == swarm.swarm_id).all()
messages = (
db.query(SwarmMessage)
.filter(SwarmMessage.swarm_id == swarm.swarm_id)
.order_by(SwarmMessage.created_at.asc())
.all()
)
artifacts = swarm.artifacts or []
artifacts_by_agent = {}
for artifact in artifacts:
agent_instance_id = artifact.get("agent_instance_id")
if agent_instance_id:
artifacts_by_agent.setdefault(agent_instance_id, []).append(artifact.get("artifact_id"))
def metrics_for_agent(agent_id: str):
agent_messages = [message for message in messages if agent_id in {message.from_agent_id, message.to_agent_id}]
tokens = 0
tools = 0
current_action = None
first_seen = None
last_seen = None
for message in agent_messages:
metadata = message.message_metadata or {}
usage = metadata.get("model_usage") or {}
tokens += int(usage.get("total_tokens") or 0)
if message.message_type in {"task", "task_retry", "response", "error"}:
tools += 1
if message.message_type in {"task", "task_retry"} and message.content:
current_action = message.content[:200]
if first_seen is None or message.created_at < first_seen:
first_seen = message.created_at
if last_seen is None or message.created_at > last_seen:
last_seen = message.created_at
elapsed = int((last_seen - first_seen).total_seconds()) if first_seen and last_seen else 0
return tokens, tools, elapsed, current_action
agent_phase_infos = []
artifact_ids = []
for agent in agents:
tokens, tools, elapsed, current_action = metrics_for_agent(agent.agent_id)
agent_artifact_ids = artifacts_by_agent.get(agent.agent_id, [])
artifact_ids.extend(agent_artifact_ids)
agent_phase_infos.append(
SwarmPhaseAgentInfo(
agent_id=agent.agent_id,
role=agent.role,
status=agent.status.value,
tokens=tokens,
tools=tools,
elapsed_seconds=elapsed,
artifact_ids=agent_artifact_ids,
current_action=current_action or agent.current_task,
error=agent.output if agent.status == SwarmAgentStatus.FAILED else None,
summary=agent.output if agent.status != SwarmAgentStatus.FAILED else None,
)
)
phase_status = "completed"
if any(agent.status == SwarmAgentStatus.FAILED for agent in agents):
phase_status = "failed"
elif any(agent.status in {SwarmAgentStatus.PENDING, SwarmAgentStatus.RUNNING} for agent in agents):
phase_status = "running"
return [
SwarmPhaseInfo(
name=swarm.phase or "development",
status=phase_status,
agents=agent_phase_infos,
artifact_ids=artifact_ids,
summary=f"Runtime phase {swarm.phase or 'development'}",
)
]
def _synthesized_artifacts_for_swarm(db: Session, swarm: Swarm) -> list[Dict[str, Any]]:
"""Return stored artifacts or a compatibility summary for old empty terminal runs."""
if swarm.artifacts:
@@ -133,10 +250,12 @@ def _build_swarm_status_response(db: Session, swarm: Swarm) -> SwarmStatusRespon
return SwarmStatusResponse(
deployment_id=swarm.swarm_id,
swarm_id=swarm.swarm_id,
mode="sub_agile",
status=display_status,
phase=swarm.phase,
progress=swarm.progress,
agents=_agent_infos_for_swarm(db, swarm.swarm_id),
phases=_phases_for_swarm(db, swarm),
metrics=SwarmMetrics(
total_messages=swarm.total_messages,
tokens_used=swarm.tokens_used,
@@ -653,6 +772,110 @@ async def get_swarm_artifact_file(
)
async def _emit_artifact_edit_event(swarm: Swarm, request: ArtifactEditRequest, event_type: str) -> None:
"""Emit a best-effort callback for project revision updates."""
project_context = swarm.project_context or {}
callback_config = project_context.get("_callback")
if not callback_config:
return
callback = CallbackDeliveryClient(callback_config)
if not callback.enabled:
return
payload = {
"artifact_id": request.artifact_id,
"project_revision": request.project_revision,
"base_project_revision": request.base_project_revision,
"base_content_hash": request.base_content_hash,
"manifest_uri": request.manifest_uri,
"archive_uri": request.archive_uri,
"source": request.source,
"runtime_deployment_id": swarm.swarm_id,
}
await callback.emit(
event_type,
project_context.get("manager_deployment_id")
or project_context.get("heicode_deployment_id")
or swarm.swarm_id,
swarm_id=swarm.swarm_id,
correlation_id=project_context.get("correlation_id"),
payload=payload,
)
def apply_runtime_artifact_edit(db: Session, swarm_id: str, request: ArtifactEditRequest) -> ArtifactEditResponse:
"""Persist an accepted project revision forwarded by Manager."""
swarm = db.query(Swarm).filter(Swarm.swarm_id == swarm_id).first()
if not swarm:
raise HTTPException(status_code=404, detail="Runtime deployment not found")
artifacts = list(swarm.artifacts or [])
artifact = next((artifact for artifact in artifacts if artifact.get("artifact_id") == request.artifact_id), None)
if not artifact:
raise HTTPException(status_code=404, detail="ARTIFACT_REVISION_NOT_FOUND")
metadata = artifact.get("metadata") or {}
if metadata.get("artifact_layout") != "project_folder":
raise HTTPException(status_code=422, detail="ARTIFACT_EDIT_UNSUPPORTED_FOR_LAYOUT")
current_revision = int(metadata.get("project_revision") or 1)
if request.base_project_revision is not None and request.base_project_revision != current_revision:
raise HTTPException(status_code=409, detail="ARTIFACT_REVISION_CONFLICT")
metadata.update(
{
"project_revision": request.project_revision,
"base_project_revision": request.base_project_revision,
"base_content_hash": request.base_content_hash,
"manifest_uri": request.manifest_uri,
"archive_uri": request.archive_uri,
"content_hash": request.base_content_hash or metadata.get("content_hash"),
"revision_source": request.source,
"delivery_ref": {
"kind": "runtime_artifact",
"artifact_id": request.artifact_id,
"project_revision": request.project_revision,
},
}
)
artifact["metadata"] = metadata
swarm.artifacts = artifacts
swarm.updated_at = datetime.utcnow()
db.commit()
message = SwarmMessage(
message_id=f"rev_{uuid.uuid4().hex[:12]}",
swarm_id=swarm_id,
from_agent_id=None,
to_agent_id=None,
message_type="artifact_edit",
content=f"artifact {request.artifact_id} revision {request.project_revision}",
message_metadata=request.model_dump(),
)
db.add(message)
db.commit()
return ArtifactEditResponse(
deployment_id=swarm_id,
artifact_id=request.artifact_id,
project_revision=request.project_revision,
status="accepted",
)
@swarms_router.post("/{swarm_id}/artifact-edits", response_model=ArtifactEditResponse)
async def receive_swarm_artifact_edit(
swarm_id: str,
request: ArtifactEditRequest,
db: Session = Depends(get_db),
):
"""Accept an accepted project revision forwarded by Manager."""
response = apply_runtime_artifact_edit(db, swarm_id, request)
swarm = db.query(Swarm).filter(Swarm.swarm_id == swarm_id).first()
if swarm:
await _emit_artifact_edit_event(swarm, request, "artifact.local_edit_applied")
return response
@swarms_router.post("/{swarm_id}/approvals/{approval_id}")
async def receive_swarm_approval_decision(
swarm_id: str,
+1 -1
View File
@@ -31,7 +31,7 @@ spec:
containers:
- name: agent-manager
image: agnettaiji.azurecr.io/ai-agents/agent-manager:heicode-v2-runtime-20260602171443-arm64
image: agnettaiji.azurecr.io/ai-agents/agent-manager:heicode-v2-runtime-20260603104215-arm64
imagePullPolicy: Always
ports:
+1 -1
View File
@@ -22,7 +22,7 @@ spec:
- name: acr-secret
containers:
- name: agent-manager
image: agnettaiji.azurecr.io/ai-agents/agent-manager:heicode-v2-runtime-20260602171443-arm64
image: agnettaiji.azurecr.io/ai-agents/agent-manager:heicode-v2-runtime-20260603104215-arm64
imagePullPolicy: Always
ports:
- containerPort: 8000
+1 -1
View File
@@ -20,7 +20,7 @@ spec:
- name: acr-secret
containers:
- name: agent-manager
image: agnettaiji.azurecr.io/ai-agents/agent-manager:heicode-v2-runtime-20260602171443-arm64
image: agnettaiji.azurecr.io/ai-agents/agent-manager:heicode-v2-runtime-20260603104215-arm64
imagePullPolicy: Always
ports:
- containerPort: 8000
+22
View File
@@ -25,6 +25,7 @@ def test_primary_and_compat_runtime_routes_are_registered():
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 '@router.post("/deployments/{deployment_id}/artifact-edits"' in _read("api/agnet/deployments.py")
assert 'prefix="/api/swarms"' in swarm_router_source
assert '@swarms_router.post("/{swarm_id}/approvals/{approval_id}")' in swarm_router_source
assert '@swarms_router.get("/{swarm_id}/artifacts/{artifact_id}/manifest")' in swarm_router_source
@@ -82,3 +83,24 @@ def test_project_folder_contract_is_documented_and_flagged():
assert '"artifact_layout": "project_folder"' in orchestrator_source
assert '"primary_read_path": "manifest"' in orchestrator_source
assert '"summary_only": True' in swarm_router_source
def test_runtime_rich_workflow_contract_is_present():
"""Workflow/work outputs should expose richer runtime fields."""
swarm_models_source = _read("api/swarm/models.py")
swarm_router_source = _read("api/swarm/router.py")
orchestrator_source = _read("api/swarm/orchestrator.py")
assert "tokens: int = 0" in swarm_models_source
assert "tools: int = 0" in swarm_models_source
assert "elapsed_seconds: int = 0" in swarm_models_source
assert "artifact_ids: List[str] = Field(default_factory=list)" in swarm_models_source
assert "class SwarmPhaseInfo" in swarm_models_source
assert "phases: List[SwarmPhaseInfo]" in swarm_models_source
assert "source_agent_role" in orchestrator_source
assert "project_revision" in orchestrator_source
assert "delivery_ref" in orchestrator_source
assert "@swarms_router.post(\"/{swarm_id}/artifact-edits\"" in swarm_router_source
callbacks_source = _read("api/agnet/callbacks.py")
assert "artifact.local_edit_applied" in callbacks_source
assert '"project_folder"' in callbacks_source