Przeglądaj źródła

解耦:通过 Host Resolver 加载旧项目运行环境

run_api 不再直接 import examples.{project_name}.run,改为依赖注入 ProjectEnvironmentResolver;api_server 在 Host 侧注册 LegacyExampleResolver 保持旧 project_name 行为。同步补齐反思与压缩接口使用的后台运行兼容包装,避免框架 API 内核依赖具体业务包。
SamLee 3 dni temu
rodzic
commit
287766d0b5

+ 52 - 0
agent/agent/trace/project_environment.py

@@ -0,0 +1,52 @@
+"""Host adapter boundary for optional project-specific execution environments."""
+
+from __future__ import annotations
+
+import importlib
+from dataclasses import dataclass
+from typing import Any, Dict, List, Optional, Protocol
+
+
+@dataclass(frozen=True)
+class ProjectEnvironment:
+    runner: Any
+    messages: List[Dict[str, Any]]
+    default_config: Any
+
+
+class ProjectEnvironmentResolver(Protocol):
+    async def resolve(
+        self,
+        project_name: str,
+        messages: Optional[List[Dict[str, Any]]] = None,
+    ) -> Optional[ProjectEnvironment]: ...
+
+
+class LegacyExampleResolver:
+    """Compatibility adapter retained by the host, outside the API kernel."""
+
+    async def resolve(
+        self,
+        project_name: str,
+        messages: Optional[List[Dict[str, Any]]] = None,
+    ) -> Optional[ProjectEnvironment]:
+        module_name = f"examples.{project_name}.run"
+        try:
+            module = importlib.import_module(module_name)
+        except ImportError as exc:
+            if getattr(exc, "name", None) == module_name:
+                return None
+            raise
+        initializer = getattr(module, "init_project_env", None)
+        if initializer is None:
+            return None
+        if messages is None:
+            runner, resolved_messages, config = await initializer()
+        else:
+            runner, resolved_messages, config = await initializer(messages)
+        return ProjectEnvironment(runner, resolved_messages, config)
+
+
+__all__ = [
+    "ProjectEnvironment", "ProjectEnvironmentResolver", "LegacyExampleResolver",
+]

+ 27 - 31
agent/agent/trace/run_api.py

@@ -15,10 +15,7 @@ Trace 控制 API — 新建 / 运行 / 停止 / 反思
 
 import asyncio
 import logging
-import re
-import uuid
 import os
-from datetime import datetime
 from typing import Any, Dict, List, Optional
 
 from fastapi import APIRouter, HTTPException
@@ -35,6 +32,7 @@ experiences_router = APIRouter(prefix="/api", tags=["experiences"])
 # ===== 全局 Runner(由 api_server.py 注入)=====
 
 _runner = None
+_project_environment_resolver = None
 
 
 def set_runner(runner):
@@ -43,6 +41,12 @@ def set_runner(runner):
     _runner = runner
 
 
+def set_project_environment_resolver(resolver):
+    """Inject optional Host adapter; the framework never imports projects."""
+    global _project_environment_resolver
+    _project_environment_resolver = resolver
+
+
 def _get_runner():
     if _runner is None:
         raise HTTPException(
@@ -174,6 +178,11 @@ async def _run_in_background(trace_id: str, messages: List[Dict], config, runner
         _running_tasks.pop(trace_id, None)
 
 
+async def _run_trace_background(runner, messages: List[Dict], config):
+    """Compatibility wrapper used by reflect/compact background endpoints."""
+    await _run_in_background(config.trace_id, messages, config, runner_instance=runner)
+
+
 async def _run_with_trace_signal(
     messages: List[Dict], config, trace_id_future: asyncio.Future, runner_instance=None
 ):
@@ -207,7 +216,6 @@ async def create_and_run(req: CreateRequest):
     立即返回 trace_id,后台异步执行。
     通过 WebSocket /api/traces/{trace_id}/watch 监听实时更新。
     """
-    import importlib
     from agent.core.runner import RunConfig
 
     runner = None
@@ -216,13 +224,14 @@ async def create_and_run(req: CreateRequest):
 
     if req.project_name:
         try:
-            # 动态加载对应 example 的 run.py
-            module_name = f"examples.{req.project_name}.run"
-            example_module = importlib.import_module(module_name)
-            if hasattr(example_module, "init_project_env"):
+            environment = None
+            if _project_environment_resolver:
+                environment = await _project_environment_resolver.resolve(req.project_name, req.messages)
+            if environment:
                 # 获取该 example 专属的 runner, 带上下文 messages, 以及默认 config
-                runner, example_messages, default_config = await example_module.init_project_env(req.messages)
-                messages = example_messages
+                runner = environment.runner
+                default_config = environment.default_config
+                messages = environment.messages
                 
                 # 合并请求配置和 example 默认配置
                 config = RunConfig(
@@ -235,13 +244,7 @@ async def create_and_run(req: CreateRequest):
                     enable_research_flow=default_config.enable_research_flow,
                     context={"project_name": req.project_name}
                 )
-        except ImportError as e:
-            if getattr(e, "name", None) == module_name:
-                logger.warning(f"Project '{req.project_name}' has no custom run.py, falling back to default.")
-            else:
-                import traceback
-                logger.error(f"Error INSIDE {module_name}:\n{traceback.format_exc()}")
-        except Exception as e:
+        except Exception:
             import traceback
             logger.error(f"Unexpected error loading project environment for {req.project_name}:\n{traceback.format_exc()}")
             
@@ -387,7 +390,6 @@ async def run_trace(trace_id: str, req: TraceRunRequest):
     但缺少对应的 tool responses),框架会自动检测并调整插入位置,确保不会产生不一致的状态。
     """
     from agent.core.runner import RunConfig
-    import importlib
 
     runner = _get_runner()
 
@@ -437,19 +439,13 @@ async def run_trace(trace_id: str, req: TraceRunRequest):
         
         if project_name:
             try:
-                module_name = f"examples.{project_name}.run"
-                example_module = importlib.import_module(module_name)
-                if hasattr(example_module, "init_project_env"):
+                environment = None
+                if _project_environment_resolver:
+                    environment = await _project_environment_resolver.resolve(project_name)
+                if environment:
                     logger.info(f"Trace {trace_id} 绑定了项目 {project_name},动态加载执行环境...")
-                    project_runner, project_msgs, default_config = await example_module.init_project_env()
-                    runner = project_runner  # 发生替换
-            except ImportError as e:
-                if getattr(e, "name", None) == module_name:
-                    logger.warning(f"Project '{project_name}' has no custom run.py, keeping default runner.")
-                else:
-                    import traceback
-                    logger.error(f"Error INSIDE {module_name} during resume:\n{traceback.format_exc()}")
-            except Exception as e:
+                    runner = environment.runner
+            except Exception:
                 import traceback
                 logger.error(f"Unexpected error loading run.py environment for project {project_name} in trace {trace_id}:\n{traceback.format_exc()}")
 
@@ -682,7 +678,7 @@ async def compact_trace(trace_id: str):
 @router.get("/running", tags=["run"])
 async def list_running():
     """列出正在运行的 Trace(包含活跃状态判断)"""
-    from datetime import datetime, timedelta
+    from datetime import datetime
 
     runner = _get_runner()
     running = []

+ 8 - 1
agent/api_server.py

@@ -19,7 +19,13 @@ import uvicorn
 
 from agent.trace import FileSystemTraceStore
 from agent.trace.api import router as api_router, set_trace_store as set_api_trace_store
-from agent.trace.run_api import router as run_router, experiences_router, set_runner
+from agent.trace.run_api import (
+    router as run_router,
+    experiences_router,
+    set_runner,
+    set_project_environment_resolver,
+)
+from agent.trace.project_environment import LegacyExampleResolver
 from agent.trace.websocket import router as ws_router, set_trace_store as set_ws_trace_store
 from agent.trace.examples_api import router as examples_router
 from agent.trace.logs_websocket import router as logs_router, setup_websocket_logging
@@ -79,6 +85,7 @@ runner = AgentRunner(
     llm_call=create_openrouter_llm_call(model="anthropic/claude-sonnet-4.5"),
 )
 set_runner(runner)
+set_project_environment_resolver(LegacyExampleResolver())
 
 
 # ===== 注册路由 =====