fix(api): /executions incluye también las ejecuciones HITL pendientes

`GET /executions` solo leía `executions.jsonl`, donde nunca se escribe una ejecución
en `awaiting_approval` (esas viven solo en el checkpointer). La página de Aprobaciones
del dashboard hace `[e for e in list_executions() if e.status=="awaiting_approval"]`,
así que nunca encontraba aprobaciones pendientes. Ahora `list_executions` reconstruye
las no-terminales desde `execution_index.json` + `orchestrator.snapshot()` y las
fusiona con las del JSONL. Añade `test_list_executions_incluye_la_awaiting`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Juan
2026-05-11 14:38:52 +02:00
co-authored by Claude Opus 4.7
parent c414e16357
commit c860984ae4
2 changed files with 53 additions and 2 deletions
+41 -2
View File
@@ -147,8 +147,47 @@ async def invoke_agent(
@router.get("", response_model=list[AgentExecutionSummary])
def list_executions(settings: SettingsDep) -> list[AgentExecutionSummary]:
return read_execution_summaries(settings.data_dir)
async def list_executions(
registry: RegistryDep,
policies: PolicyStoreDep,
orchestrator: OrchestratorDep,
settings: SettingsDep,
) -> list[AgentExecutionSummary]:
"""Resumen de ejecuciones: las terminales del JSONL más las pausadas en HITL.
Una ejecución en ``awaiting_approval`` no se escribe en el log append-only
(`executions.jsonl`); solo vive en el checkpointer. Para que la página de
aprobaciones del dashboard la encuentre, aquí se reconstruyen esas desde el
índice `execution_index.json` + el checkpointer.
"""
summaries = read_execution_summaries(settings.data_dir)
seen = {str(s.trace_id) for s in summaries}
for trace_id, meta in _load_index(settings.data_dir).items():
if trace_id in seen:
continue
try:
agent_def = registry.get_version(meta["agent_name"], meta["version"])
policy = policies.get_policy(agent_def.guardrails[0])
execution = await orchestrator.snapshot(
agent_def=agent_def, policy=policy, trace_id=UUID(trace_id)
)
except (FileNotFoundError, IndexError, KeyError, ValueError):
continue
if execution is None:
continue
summaries.append(
AgentExecutionSummary(
trace_id=execution.trace_id,
agent_name=execution.agent_name,
agent_version=execution.agent_version,
status=execution.status,
started_at=execution.started_at,
finished_at=execution.finished_at,
n_violations=len(execution.violations),
n_proposed_actions=len(execution.proposed_actions),
)
)
return summaries
@router.get("/{trace_id}", response_model=AgentExecution)
+12
View File
@@ -81,6 +81,18 @@ def test_list_executions_incluye_la_completada(client: TestClient) -> None:
assert rows[0]["n_proposed_actions"] == 1
def test_list_executions_incluye_la_awaiting(client: TestClient) -> None:
"""Una ejecución pausada en HITL no se escribe en el JSONL; aún así debe listarse
(la página de Aprobaciones del dashboard depende de ello)."""
trace_id = client.post(
"/agents/incident_analyzer/invoke", json={"input": "caída registros sip"}
).json()["trace_id"]
rows = client.get("/executions").json()
awaiting = [r for r in rows if r["trace_id"] == trace_id]
assert len(awaiting) == 1
assert awaiting[0]["status"] == "awaiting_approval"
def test_approve_resume_completa_ejecucion(client: TestClient) -> None:
invoked = client.post(
"/agents/incident_analyzer/invoke", json={"input": "caída registros sip"}