OpenSpace/openspace/runtime/execution_lifecycle.py
2026-06-02 16:28:29 +08:00

261 lines
9.7 KiB
Python

from __future__ import annotations
import asyncio
import traceback
import uuid
from typing import Any
from openspace.utils.logging import Logger
from .execution_context import ExecutionContextManager
from .execution_events import ExecutionEventEmitter
from .execution_finalizer import ExecutionFinalizer
from .execution_request import ExecutionRequest, ExecutionResult
from .execution_scheduler import ExecutionScheduler
logger = Logger.get_logger(__name__)
class ExecutionLifecycle:
"""Owns the per-request execution lifecycle for OpenSpaceRuntime.
The runtime remains the public owner of state and services. This class keeps
the request orchestration readable and delegates context, scheduler, event,
and finalization details to focused collaborators.
"""
def __init__(self, runtime: Any) -> None:
self._runtime = runtime
self.context_manager = ExecutionContextManager(runtime)
self.scheduler = ExecutionScheduler(runtime)
self.events = ExecutionEventEmitter(runtime)
self.finalizer = ExecutionFinalizer(runtime)
def __getattr__(self, name: str) -> Any:
return getattr(self._runtime, name)
async def execute(self, request: ExecutionRequest) -> ExecutionResult:
task = request.prompt
if not self.state.initialized:
raise RuntimeError(
"OpenSpace not initialized. "
"Call await initialize() before execute() or use async with."
)
await self.context_manager.wait_until_idle()
logger.info("=" * 60)
logger.info(f"Task: {task[:100]}...")
logger.info("=" * 60)
self.context_manager.record_interaction()
self.state.running = True
self.state.task_done.clear()
loop = asyncio.get_event_loop()
start_time = loop.time()
task_id = request.task_id or f"task_{uuid.uuid4().hex[:12]}"
logger.info(f"Task ID: {task_id}")
result: dict[str, Any] = {}
evolved_skills: list[dict[str, Any]] = []
capture_skill_dir = request.capture_skill_dir
execution_context: dict[str, Any] = {}
execution_time = 0.0
cancelled_exc: asyncio.CancelledError | None = None
memory_drain_timeout = self.context_manager.memory_drain_timeout()
try:
execution_context = self.context_manager.build_initial_context(
request,
task_id,
)
low_latency_profiler = execution_context.get("low_latency_profiler")
had_input_session_id = bool(execution_context.get("session_id"))
session_id = await self.session_runtime.prepare(execution_context)
if not self.current_session_id:
self.current_session_id = session_id
execution_context["session_id"] = session_id
if "session_start_source" not in execution_context:
if request.session_id or request.resume:
execution_context["session_start_source"] = "resume"
elif not had_input_session_id:
execution_context["session_start_source"] = "startup"
self.context_manager.attach_runtime_context(execution_context)
if self.state.multi_agent is not None:
self.state.multi_agent.inject_context(execution_context)
if request.max_iterations is not None:
execution_context["max_iterations"] = request.max_iterations
if self.state.reasoning_effort:
execution_context["reasoning_effort"] = self.state.reasoning_effort
self.context_manager.apply_config_defaults(execution_context)
self.context_manager.apply_permission_mode(execution_context)
await self.context_manager.start_recording(task_id, task)
workspace_dir = await self.context_manager.resolve_workspace(
request=request,
task_id=task_id,
execution_context=execution_context,
)
self.scheduler.install_ensure(
execution_context=execution_context,
low_latency_profiler=low_latency_profiler,
workspace_dir=workspace_dir,
)
await self.scheduler.maybe_start(
task=task,
execution_context=execution_context,
low_latency_profiler=low_latency_profiler,
workspace_dir=workspace_dir,
)
self.remember_memory_cleanup_context(execution_context)
await self.context_manager.configure_workspace(workspace_dir)
max_iterations = self.context_manager.resolve_max_iterations(
request.max_iterations
)
await self.emit_runtime_event(
"task_started",
{
"task_id": task_id,
"parent_task_id": execution_context.get("parent_task_id"),
"session_id": session_id,
"agent_id": execution_context.get("agent_id") or "primary",
"instruction": request.prompt,
"workspace_dir": workspace_dir,
"max_iterations": max_iterations,
"permission_mode": execution_context.get("permission_mode"),
"session_start_source": execution_context.get(
"session_start_source"
),
"model": self.config.llm_model,
},
)
await self.events.emit_start(
task=task,
task_id=task_id,
session_id=session_id,
max_iterations=max_iterations,
)
result = await self.events.run_turns(
task=task,
task_id=task_id,
session_id=session_id,
max_iterations=max_iterations,
execution_context=execution_context,
)
memory_drain_timeout = self.context_manager.memory_drain_timeout()
await self.drain_memory_background_tasks(
timeout_s=memory_drain_timeout,
reason="agent_finished",
context=execution_context,
)
execution_time = loop.time() - start_time
self.context_manager.apply_final_permission_mode(
result,
execution_context,
)
await self.events.emit_complete(
task=task,
task_id=task_id,
session_id=session_id,
result=result,
execution_time=execution_time,
)
except asyncio.CancelledError as exc:
execution_time = loop.time() - start_time
logger.warning("Task execution cancelled")
await self.emit_runtime_event(
"background_session_update",
{
"session_id": self.current_session_id,
"title": task,
"status": "cancelled",
"active_agent_id": "primary",
"metadata": {
"task_id": task_id,
"execution_time": execution_time,
},
},
)
result = {
"status": "cancelled",
"error": "Task execution cancelled",
"response": "",
"execution_time": execution_time,
"task_id": task_id,
"iterations": 0,
"tool_executions": [],
}
cancelled_exc = exc
except Exception as exc:
execution_time = loop.time() - start_time
tb = traceback.format_exc(limit=10)
logger.error(f"Task execution failed: {exc}", exc_info=True)
await self.emit(
"task_error",
{
"task_id": task_id,
"error": str(exc)[:500],
"execution_time": execution_time,
},
)
await self.emit_runtime_event(
"background_session_update",
{
"session_id": self.current_session_id,
"title": task,
"status": "error",
"active_agent_id": "primary",
"metadata": {
"task_id": task_id,
"error": str(exc)[:500],
"execution_time": execution_time,
},
},
)
result = {
"status": "error",
"error": str(exc),
"traceback": tb,
"response": f"Task execution error: {str(exc)}",
"execution_time": execution_time,
"task_id": task_id,
"iterations": 0,
"tool_executions": [],
}
finally:
try:
final_result = await self.finalizer.finalize(
task_id=task_id,
start_time=start_time,
execution_time=execution_time,
result=result,
execution_context=execution_context,
memory_drain_timeout=memory_drain_timeout,
evolved_skills=evolved_skills,
capture_skill_dir=capture_skill_dir,
cancelled_exc=cancelled_exc,
)
finally:
self.state.running = False
self.state.task_done.set()
if cancelled_exc is not None:
raise cancelled_exc
return ExecutionResult.from_mapping(final_result)