-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsystem.py
More file actions
132 lines (107 loc) · 4.96 KB
/
Copy pathsystem.py
File metadata and controls
132 lines (107 loc) · 4.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
"""System endpoints: status, health, and run-log listing."""
from __future__ import annotations
import asyncio
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, Response
from hive.server.deps import ServerContext, get_context
router = APIRouter(tags=["system"])
@router.get("/status")
async def status(ctx: ServerContext = Depends(get_context)) -> list[dict[str, Any]]:
"""Status of all agents (mirrors ``hive status``)."""
agents = await ctx.store.list_agents()
goals = await ctx.store.get_active_goals_map()
return [
{
"agent_id": a.agent_id,
"name": a.name,
"role": a.role,
"model": a.model,
"status": a.status.value,
"goal": goals.get(a.agent_id),
}
for a in agents
]
@router.get("/healthz")
async def healthz(response: Response, ctx: ServerContext = Depends(get_context)) -> dict[str, Any]:
"""Liveness + readiness (DB reachable).
Returns 503 when the database is unreachable so container/orchestrator probes
(e.g. the image's HEALTHCHECK) actually detect a degraded instance.
"""
try:
await ctx.store.list_agents()
db_ok = True
except Exception:
db_ok = False
response.status_code = 200 if db_ok else 503
return {"status": "ok" if db_ok else "degraded", "database": db_ok}
@router.get("/runs")
async def list_runs(
ctx: ServerContext = Depends(get_context),
limit: int | None = Query(None, ge=1, le=1000),
offset: int = Query(0, ge=0),
) -> list[dict[str, Any]]:
from hive.logging.reader import LogReader
reader = LogReader(ctx.root / "logs")
# LogReader is fully synchronous (iterdir + read_text per run); run it off the
# event loop so it can't stall the API (and the in-process daemon under
# `serve --with-daemon`).
runs = await asyncio.to_thread(reader.list_runs)
if limit is not None:
runs = runs[offset : offset + limit]
return [r.model_dump(mode="json") for r in runs]
@router.get("/runs/{run_id}")
async def get_run(run_id: str, ctx: ServerContext = Depends(get_context)) -> dict[str, Any]:
from hive.logging.reader import LogReader
reader = LogReader(ctx.root / "logs")
summary = await asyncio.to_thread(reader.get_summary, run_id)
if not summary:
raise HTTPException(status_code=404, detail=f"run not found: {run_id}")
return summary
@router.get("/runs/{run_id}/trace")
async def get_run_trace(
run_id: str, ctx: ServerContext = Depends(get_context)
) -> list[dict[str, Any]]:
"""Span tree (run -> agent -> goal -> decision/tool) derived from run logs."""
from hive.logging.trace import TraceBuilder
builder = TraceBuilder(ctx.root / "logs")
spans = await asyncio.to_thread(builder.build, run_id)
if not spans:
raise HTTPException(status_code=404, detail=f"run not found: {run_id}")
return [s.model_dump(mode="json") for s in spans]
@router.get("/metrics", response_class=Response)
async def metrics(ctx: ServerContext = Depends(get_context)) -> Response:
"""Prometheus text-format metrics: agent statuses plus latest-run counters.
Rendered by hand -- the exposition text format is trivial and not worth a
dependency. The per-run values are snapshots of the most recent run's log
summary, exposed as gauges (they reset when a new run starts, so they are
deliberately NOT counters -- don't apply rate()/increase() to them).
"""
from hive.logging.reader import LogReader
agents = await ctx.store.list_agents()
by_status: dict[str, int] = {}
for a in agents:
by_status[a.status.value] = by_status.get(a.status.value, 0) + 1
reader = LogReader(ctx.root / "logs")
runs = await asyncio.to_thread(reader.list_runs)
summary: dict[str, Any] = {}
if runs:
summary = await asyncio.to_thread(reader.get_summary, runs[0].run_id)
lines = [
"# HELP hive_agents Agents known to the store, by status.",
"# TYPE hive_agents gauge",
]
for status_value, count in sorted(by_status.items()):
lines.append(f'hive_agents{{status="{status_value}"}} {count}')
snapshot_help = {
"goals_generated": "Goals generated in the latest run (snapshot; resets each run).",
"goals_completed": "Goals completed in the latest run (snapshot; resets each run).",
"goals_abandoned": "Goals abandoned in the latest run (snapshot; resets each run).",
"tool_calls": "Tool calls in the latest run (snapshot; resets each run).",
"total_tokens": "Tokens consumed in the latest run (snapshot; resets each run).",
"total_cost_usd": "Estimated cost (USD) of the latest run (snapshot; resets each run).",
}
for key, help_text in snapshot_help.items():
lines.append(f"# HELP hive_{key} {help_text}")
lines.append(f"# TYPE hive_{key} gauge")
lines.append(f"hive_{key} {summary.get(key, 0)}")
return Response("\n".join(lines) + "\n", media_type="text/plain; version=0.0.4")