Persist active run event history

This commit is contained in:
武阳
2026-05-07 17:40:46 +08:00
parent 847b74ffb8
commit d57570e527
5 changed files with 316 additions and 24 deletions
+107 -16
View File
@@ -14,7 +14,7 @@ import shutil
import threading
import time
import venv
from dataclasses import dataclass, replace
from dataclasses import dataclass, field, replace
from pathlib import Path
from threading import Lock, RLock
from typing import Any
@@ -214,6 +214,7 @@ class RunRecord:
process_registry: RunProcessRegistry
current_stage: str = ''
error: str = ''
events: list[dict[str, Any]] = field(default_factory=list)
class RunManager:
@@ -252,6 +253,20 @@ class RunManager:
record.error = error
record.updated_at = time.time()
def record_event(self, run_id: str, event: dict[str, object]) -> None:
normalized = _normalize_run_event(event)
if normalized is None:
return
normalized['recorded_at'] = time.time()
with self._lock:
record = self._runs.get(run_id)
if record is None:
return
record.events.append(normalized)
if len(record.events) > 160:
del record.events[: len(record.events) - 160]
record.updated_at = time.time()
def finish(self, run_id: str, status: str) -> None:
self.update(run_id, status=status)
@@ -286,6 +301,7 @@ class RunManager:
'started_at': record.started_at,
'updated_at': record.updated_at,
'error': record.error,
'events': [dict(event) for event in record.events],
}
def _select_session_record(
@@ -835,6 +851,7 @@ def create_app(state: AgentState) -> FastAPI:
stage = _runtime_event_stage(event)
if stage:
state.run_manager.update(run_record.run_id, stage=stage)
state.run_manager.record_event(run_record.run_id, event)
_emit_runtime_event(event_sink, event)
if request.resume_session_id is None:
@@ -849,14 +866,16 @@ def create_app(state: AgentState) -> FastAPI:
requested_session_id,
)
if run_lock.locked():
queued_event = {
'type': 'run_queued',
'run_id': run_record.run_id,
'session_id': requested_session_id or '',
'account_id': request.account_id or '',
}
state.run_manager.record_event(run_record.run_id, queued_event)
_emit_runtime_event(
event_sink,
{
'type': 'run_queued',
'run_id': run_record.run_id,
'session_id': requested_session_id or '',
'account_id': request.account_id or '',
},
queued_event,
)
with run_lock:
if run_record.cancel_event.is_set():
@@ -872,15 +891,14 @@ def create_app(state: AgentState) -> FastAPI:
'stop_reason': 'cancelled',
}
state.run_manager.update(run_record.run_id, status='running', stage='后端开始执行')
_emit_runtime_event(
event_sink,
{
'type': 'run_started',
'run_id': run_record.run_id,
'session_id': requested_session_id or '',
'account_id': request.account_id or '',
},
)
started_event = {
'type': 'run_started',
'run_id': run_record.run_id,
'session_id': requested_session_id or '',
'account_id': request.account_id or '',
}
state.run_manager.record_event(run_record.run_id, started_event)
_emit_runtime_event(event_sink, started_event)
agent.tool_context = replace(
agent.tool_context,
cancel_event=run_record.cancel_event,
@@ -1289,6 +1307,79 @@ def _runtime_event_stage(event: dict[str, object]) -> str:
return ''
_RUN_EVENT_TYPES = {
'run_queued',
'run_started',
'tool_start',
'tool_result',
'final_text_start',
'final_text_end',
'user_review_required',
'continuation_request',
'prompt_length_check',
'prompt_length_recovery',
'auto_compact_summary',
'auto_compact_circuit_breaker',
'task_budget_exceeded',
}
def _normalize_run_event(event: dict[str, object]) -> dict[str, Any] | None:
event_type = event.get('type')
if not isinstance(event_type, str) or event_type not in _RUN_EVENT_TYPES:
return None
allowed = {
'type',
'run_id',
'session_id',
'account_id',
'tool_name',
'tool_call_id',
'arguments',
'assistant_content',
'message_id',
'ok',
'metadata',
'reason',
'continuation_index',
'turn_index',
'strategy',
'projected_input_tokens',
'soft_input_limit_tokens',
'hard_input_limit_tokens',
'exceeds_hard_limit',
'exceeds_soft_limit',
}
normalized = {
key: _json_safe_limited(value)
for key, value in event.items()
if key in allowed and value is not None
}
normalized['type'] = event_type
return normalized
def _json_safe_limited(value: Any, *, depth: int = 0) -> Any:
if depth > 4:
return str(value)[:500]
if isinstance(value, str):
return value[:2000]
if isinstance(value, (int, float, bool)) or value is None:
return value
if isinstance(value, dict):
return {
str(key)[:200]: _json_safe_limited(item, depth=depth + 1)
for key, item in list(value.items())[:80]
}
if isinstance(value, (list, tuple)):
return [_json_safe_limited(item, depth=depth + 1) for item in value[:80]]
try:
json.dumps(value)
return value
except TypeError:
return str(value)[:1000]
def _emit_runtime_event(
event_sink: Any | None,
event: dict[str, object],