mirror of
https://github.com/HKUDS/OpenSpace.git
synced 2026-08-28 05:15:00 +00:00
308 lines
11 KiB
Python
308 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
import re
|
|
import traceback
|
|
import uuid
|
|
from pathlib import Path
|
|
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__)
|
|
|
|
|
|
def _normal_path(value: Any) -> str | None:
|
|
if value is None:
|
|
return None
|
|
text = str(value).strip()
|
|
if not text:
|
|
return None
|
|
return str(Path(text).expanduser())
|
|
|
|
|
|
def _first_env_skill_dir() -> str | None:
|
|
raw = os.environ.get("OPENSPACE_HOST_SKILL_DIRS")
|
|
if not raw:
|
|
return None
|
|
for item in re.split(rf"[,{re.escape(os.pathsep)}]", raw):
|
|
path = _normal_path(item)
|
|
if path:
|
|
return path
|
|
return None
|
|
|
|
|
|
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)
|
|
|
|
def _initial_capture_skill_dir(self, request: ExecutionRequest) -> str | None:
|
|
for value in (
|
|
request.capture_skill_dir,
|
|
getattr(self.config, "capture_skill_dir", None),
|
|
os.environ.get("OPENSPACE_CAPTURE_SKILL_DIR"),
|
|
_first_env_skill_dir(),
|
|
):
|
|
path = _normal_path(value)
|
|
if path:
|
|
return path
|
|
return None
|
|
|
|
def _default_capture_skill_dir(self, workspace_dir: Any) -> str:
|
|
workspace = _normal_path(workspace_dir)
|
|
if workspace is None:
|
|
workspace = _normal_path(getattr(self.config, "workspace_dir", None))
|
|
if workspace is None:
|
|
workspace = os.getcwd()
|
|
return str(Path(workspace).expanduser() / ".openspace" / "skills")
|
|
|
|
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 = self._initial_capture_skill_dir(request)
|
|
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,
|
|
)
|
|
if capture_skill_dir is None:
|
|
capture_skill_dir = self._default_capture_skill_dir(workspace_dir)
|
|
execution_context["capture_skill_dir"] = capture_skill_dir
|
|
self.state.capture_skill_dir = capture_skill_dir
|
|
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)
|