feat(agents): Phase 8.4-10.5 built-in plugins, bundled skills, coordinator
This commit is contained in:
@@ -6,6 +6,8 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.agents.registry import load_builtin_registry_indexes
|
||||
from app.agents.runtime_metrics import coerce_cost_thresholds, estimate_token_cost, is_cost_budget_warning
|
||||
from app.models.agent import Agent
|
||||
from app.models.conversation import Conversation
|
||||
from app.models.skill import Skill
|
||||
@@ -17,14 +19,21 @@ from app.schemas.agent import (
|
||||
AgentCreate,
|
||||
AgentOut,
|
||||
AgentStats,
|
||||
AgentVisibilityCostByAgentOut,
|
||||
AgentVisibilityCostOut,
|
||||
AgentVisibilityCostSummaryOut,
|
||||
AgentVisibilityEvidenceOut,
|
||||
AgentVisibilityEventsResponse,
|
||||
AgentVisibilityEventOut,
|
||||
AgentVisibilityIsolationOut,
|
||||
AgentVisibilityRuntimeSummaryOut,
|
||||
AgentVisibilityTaskSummaryOut,
|
||||
AgentVisibilityThreadMessageOut,
|
||||
AgentVisibilityThreadOut,
|
||||
AgentVisibilityTopologyNodeOut,
|
||||
AgentVisibilityTopologyOut,
|
||||
AgentVisibilityToolGovernanceItemOut,
|
||||
AgentVisibilityToolGovernanceOut,
|
||||
AgentVisibilityVerifierOut,
|
||||
)
|
||||
from app.services.agent_service import _extract_continuity_snapshot
|
||||
@@ -153,12 +162,13 @@ def _build_topology_nodes(
|
||||
|
||||
root_agent_id = str(state.get("root_agent_id") or state.get("agent_id") or "") or None
|
||||
current_agent = str(state.get("current_agent") or "") or None
|
||||
parent_agent_id = str(state.get("parent_agent_id") or "") or None
|
||||
nodes: dict[str, AgentVisibilityTopologyNodeOut] = {}
|
||||
if root_agent_id:
|
||||
nodes[root_agent_id] = AgentVisibilityTopologyNodeOut(
|
||||
agent_id=root_agent_id,
|
||||
role=root_agent_id.split("-")[0],
|
||||
parent_agent_id=None,
|
||||
parent_agent_id=parent_agent_id if root_agent_id != state.get("agent_id") else None,
|
||||
source="root",
|
||||
task_count=task_counts.get(root_agent_id, 0),
|
||||
completed_task_count=completed_counts.get(root_agent_id, 0),
|
||||
@@ -185,6 +195,153 @@ def _build_topology_nodes(
|
||||
return list(nodes.values())
|
||||
|
||||
|
||||
def _estimate_runtime_cost(input_tokens: int, output_tokens: int) -> float | None:
|
||||
return estimate_token_cost(input_tokens, output_tokens)
|
||||
|
||||
|
||||
def _build_cost_summary(
|
||||
state: dict[str, Any],
|
||||
*,
|
||||
conversation_id: str,
|
||||
) -> AgentVisibilityCostSummaryOut:
|
||||
input_tokens = int(state.get("input_tokens") or 0)
|
||||
output_tokens = int(state.get("output_tokens") or 0)
|
||||
estimated_cost = _estimate_runtime_cost(input_tokens, output_tokens)
|
||||
thresholds = coerce_cost_thresholds(state.get("cost_thresholds"))
|
||||
total_budget_warning = bool(state.get("budget_warning") or False) or is_cost_budget_warning(
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
estimated_cost,
|
||||
thresholds,
|
||||
)
|
||||
|
||||
by_agent_items: list[AgentVisibilityCostByAgentOut] = []
|
||||
for agent_id, payload in dict(state.get("cost_by_agent") or {}).items():
|
||||
payload_dict = dict(payload or {})
|
||||
agent_input_tokens = int(payload_dict.get("input_tokens") or 0)
|
||||
agent_output_tokens = int(payload_dict.get("output_tokens") or 0)
|
||||
agent_estimated_cost = payload_dict.get("estimated_cost")
|
||||
if agent_estimated_cost is None:
|
||||
agent_estimated_cost = _estimate_runtime_cost(agent_input_tokens, agent_output_tokens)
|
||||
by_agent_items.append(
|
||||
AgentVisibilityCostByAgentOut(
|
||||
agent_id=str(payload_dict.get("agent_id") or agent_id),
|
||||
input_tokens=agent_input_tokens,
|
||||
output_tokens=agent_output_tokens,
|
||||
total_tokens=int(payload_dict.get("total_tokens") or (agent_input_tokens + agent_output_tokens)),
|
||||
estimated_cost=agent_estimated_cost,
|
||||
budget_warning=bool(payload_dict.get("budget_warning") or False),
|
||||
)
|
||||
)
|
||||
by_agent_items.sort(key=lambda item: item.total_tokens, reverse=True)
|
||||
|
||||
return AgentVisibilityCostSummaryOut(
|
||||
conversation_id=conversation_id,
|
||||
total=AgentVisibilityCostOut(
|
||||
input_tokens=input_tokens,
|
||||
output_tokens=output_tokens,
|
||||
total_tokens=input_tokens + output_tokens,
|
||||
estimated_cost=estimated_cost,
|
||||
budget_warning=total_budget_warning,
|
||||
),
|
||||
thresholds=thresholds,
|
||||
by_agent=by_agent_items,
|
||||
)
|
||||
|
||||
|
||||
def _build_tool_governance(
|
||||
state: dict[str, Any],
|
||||
*,
|
||||
conversation_id: str,
|
||||
) -> AgentVisibilityToolGovernanceOut:
|
||||
indexes = load_builtin_registry_indexes()
|
||||
tool_outcomes = [dict(item) for item in state.get("tool_outcomes") or [] if isinstance(item, dict)]
|
||||
usage_count_by_tool: dict[str, int] = {}
|
||||
last_result_preview_by_tool: dict[str, str | None] = {}
|
||||
for item in tool_outcomes:
|
||||
tool_name = str(item.get("tool_name") or "")
|
||||
if tool_name == "search_web":
|
||||
tool_name = "web_search"
|
||||
if not tool_name:
|
||||
continue
|
||||
usage_count_by_tool[tool_name] = usage_count_by_tool.get(tool_name, 0) + 1
|
||||
preview = item.get("result_preview")
|
||||
if isinstance(preview, str) and preview:
|
||||
last_result_preview_by_tool[tool_name] = preview
|
||||
|
||||
items = [
|
||||
AgentVisibilityToolGovernanceItemOut(
|
||||
capability_id=capability.capability_id,
|
||||
tool_name=capability.tool_name,
|
||||
permission_class=capability.permission_class.value,
|
||||
side_effect_scope=capability.side_effect_scope.value,
|
||||
supports_retry=capability.supports_retry,
|
||||
idempotent=capability.idempotent,
|
||||
safe_for_parallel_use=capability.safe_for_parallel_use,
|
||||
requires_confirmation=capability.requires_confirmation,
|
||||
usage_count=usage_count_by_tool.get(capability.tool_name, 0),
|
||||
last_result_preview=last_result_preview_by_tool.get(capability.tool_name),
|
||||
)
|
||||
for capability in indexes.capability_by_id.values()
|
||||
]
|
||||
items.sort(key=lambda item: (-item.usage_count, item.tool_name))
|
||||
|
||||
return AgentVisibilityToolGovernanceOut(
|
||||
conversation_id=conversation_id,
|
||||
total_tools=len(items),
|
||||
used_tools=sum(1 for item in items if item.usage_count > 0),
|
||||
items=items,
|
||||
upgrade_candidates=[
|
||||
"worktree_manager",
|
||||
"cost_inspector",
|
||||
"runtime_event_drilldown",
|
||||
"tool_policy_explorer",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _build_runtime_summary(
|
||||
state: dict[str, Any],
|
||||
*,
|
||||
conversation_id: str,
|
||||
) -> AgentVisibilityRuntimeSummaryOut:
|
||||
tasks = [dict(item) for item in state.get("active_tasks") or []]
|
||||
task_results = [dict(item) for item in state.get("task_results") or []]
|
||||
topology_nodes = _build_topology_nodes(state, tasks, task_results)
|
||||
cost_summary = _build_cost_summary(state, conversation_id=conversation_id)
|
||||
input_tokens = cost_summary.total.input_tokens
|
||||
output_tokens = cost_summary.total.output_tokens
|
||||
recent_events_raw = [dict(item) for item in (state.get("event_trace") or [])[-10:]]
|
||||
isolation_mode = str(state.get("isolation_mode") or "none")
|
||||
|
||||
return AgentVisibilityRuntimeSummaryOut(
|
||||
conversation_id=conversation_id,
|
||||
execution_mode=state.get("execution_mode"),
|
||||
current_phase=state.get("current_phase"),
|
||||
current_checkpoint=state.get("current_checkpoint"),
|
||||
phase_history=list(state.get("phase_history") or []),
|
||||
checkpoint_history=list(state.get("checkpoint_history") or []),
|
||||
verifier=AgentVisibilityVerifierOut(
|
||||
conversation_id=conversation_id,
|
||||
status=state.get("verification_status"),
|
||||
summary=state.get("verification_summary"),
|
||||
evidence=list(state.get("verification_evidence") or []),
|
||||
),
|
||||
isolation=AgentVisibilityIsolationOut(
|
||||
mode=isolation_mode,
|
||||
isolation_id=state.get("isolation_id"),
|
||||
workspace_path=state.get("isolation_workspace_path"),
|
||||
parent_conversation_id=state.get("isolation_parent_conversation_id") or state.get("parent_conversation_id"),
|
||||
metadata=dict(state.get("isolation_metadata") or {}),
|
||||
),
|
||||
cost=cost_summary.total,
|
||||
topology_node_count=len(topology_nodes),
|
||||
active_task_count=len(tasks),
|
||||
completed_task_count=sum(1 for item in task_results if item.get("status") == "completed"),
|
||||
recent_events=[_coerce_event_payload(item) for item in recent_events_raw],
|
||||
)
|
||||
|
||||
|
||||
def record_agent_call(agent_id: str):
|
||||
_agent_call_counts[agent_id] = _agent_call_counts.get(agent_id, 0) + 1
|
||||
|
||||
@@ -475,6 +632,36 @@ async def get_visibility_verifier(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/visibility/runtime-summary", response_model=AgentVisibilityRuntimeSummaryOut)
|
||||
async def get_visibility_runtime_summary(
|
||||
conversation_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
state = await _get_visibility_state(conversation_id, current_user=current_user, db=db)
|
||||
return _build_runtime_summary(state, conversation_id=conversation_id)
|
||||
|
||||
|
||||
@router.get("/visibility/cost", response_model=AgentVisibilityCostSummaryOut)
|
||||
async def get_visibility_cost(
|
||||
conversation_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
state = await _get_visibility_state(conversation_id, current_user=current_user, db=db)
|
||||
return _build_cost_summary(state, conversation_id=conversation_id)
|
||||
|
||||
|
||||
@router.get("/visibility/tools", response_model=AgentVisibilityToolGovernanceOut)
|
||||
async def get_visibility_tools(
|
||||
conversation_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
state = await _get_visibility_state(conversation_id, current_user=current_user, db=db)
|
||||
return _build_tool_governance(state, conversation_id=conversation_id)
|
||||
|
||||
|
||||
@router.post("", response_model=AgentOut, status_code=201)
|
||||
async def create_agent(
|
||||
data: AgentCreate,
|
||||
|
||||
Reference in New Issue
Block a user