Persist active run event history
This commit is contained in:
+107
-16
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user