Просмотр исходного кода

feat: initial Agent module MVP

Core features:
- Trace/Step models for execution tracing
- Experience/Skill models for memory
- AgentRunner with call() and run() modes
- @tool decorator for tool registration
- Memory-based storage implementation
- Protocol-based storage abstraction

Extracted from Resonote LLM module:
- ToolRegistry and @tool decorator
- SchemaGenerator for auto schema generation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Howard 1 месяц назад
Сommit
8ca19b58fe

+ 47 - 0
.gitignore

@@ -0,0 +1,47 @@
+# reson-agent
+
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Virtual environments
+venv/
+ENV/
+env/
+.venv/
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+
+# Testing
+.pytest_cache/
+.coverage
+htmlcov/
+.tox/
+.nox/
+
+# Misc
+.DS_Store
+Thumbs.db

+ 164 - 0
README.md

@@ -0,0 +1,164 @@
+# Reson Agent
+
+可扩展、可学习的 Agent 框架,支持执行追踪和持久记忆。
+
+## 核心特性
+
+- **执行追踪**:每次 LLM 调用、工具调用、结论都记录为 Step,形成可视化的执行图
+- **三层记忆**:Task State(任务状态)→ Experience(经验规则)→ Skill(归纳技能)
+- **存储可插拔**:通过 Protocol 定义存储接口,支持内存、PostgreSQL、Neo4j 等实现
+- **工具系统**:`@tool` 装饰器自动注册工具,支持 Schema 生成、确认机制、参数编辑
+
+## 安装
+
+```bash
+# 基础安装
+pip install reson-agent
+
+# 带 PostgreSQL 支持
+pip install reson-agent[postgres]
+
+# 完整安装
+pip install reson-agent[all]
+```
+
+## 快速开始
+
+### 1. 单次调用(不需要 Agent 能力)
+
+```python
+from reson_agent import AgentRunner
+from reson_agent.storage import MemoryTraceStore
+
+# 最简使用
+runner = AgentRunner(
+    trace_store=MemoryTraceStore()  # 内存存储,测试用
+)
+
+result = await runner.call(
+    messages=[{"role": "user", "content": "你好"}],
+    model="gpt-4o"
+)
+print(result.reply)
+```
+
+### 2. Agent 模式(带执行追踪)
+
+```python
+from reson_agent import AgentRunner
+from reson_agent.storage import MemoryTraceStore, MemoryMemoryStore
+from reson_agent.tools import tool
+
+# 注册工具
+@tool(
+    editable_params=["query"],
+    display={"zh": {"name": "搜索", "params": {"query": "关键词"}}}
+)
+async def search(query: str, uid: str = "") -> str:
+    """搜索相关内容"""
+    return f"搜索结果: {query}"
+
+# 创建 Runner
+runner = AgentRunner(
+    trace_store=MemoryTraceStore(),
+    memory_store=MemoryMemoryStore(),  # 可选,启用经验/技能
+)
+
+# 执行任务
+async for event in runner.run(
+    task="帮我分析最近的阅读兴趣",
+    agent_type="researcher",
+    max_iterations=5
+):
+    if event.type == "llm_delta":
+        print(event.data["delta"], end="")
+    elif event.type == "conclusion":
+        print(f"\n结论: {event.data['content']}")
+```
+
+### 3. 添加反馈
+
+```python
+# 对某个执行步骤给反馈
+exp_id = await runner.add_feedback(
+    trace_id="trace_abc",
+    target_step_id="step_123",
+    feedback_type="correction",
+    content="搜索时应该限制在最近30天内"
+)
+```
+
+## 核心概念
+
+### Trace(执行轨迹)
+
+一次完整的 LLM 交互,包含多个 Step。
+
+```python
+@dataclass
+class Trace:
+    trace_id: str
+    mode: Literal["call", "agent"]  # 单次调用 or Agent 模式
+    status: Literal["running", "completed", "failed"]
+    # ...
+```
+
+### Step(执行步骤)
+
+Trace 中的一个原子操作。
+
+```python
+@dataclass
+class Step:
+    step_id: str
+    trace_id: str
+    step_type: Literal["llm_call", "tool_call", "tool_result", "conclusion", "feedback"]
+    parent_ids: List[str]  # 形成 DAG
+    data: Dict[str, Any]
+```
+
+### Experience(经验)
+
+从执行或反馈中提取的规则。
+
+```python
+@dataclass
+class Experience:
+    condition: str   # 什么情况下适用
+    rule: str        # 应该怎么做
+    evidence: List[str]  # step_ids 作为证据
+```
+
+### Skill(技能)
+
+从经验归纳的高层知识。
+
+```python
+@dataclass
+class Skill:
+    name: str
+    guidelines: List[str]
+    derived_from: List[str]  # experience_ids
+```
+
+## 存储适配
+
+实现 `TraceStore` 和 `MemoryStore` Protocol 即可接入任何存储。
+
+```python
+from reson_agent.storage import TraceStore, MemoryStore
+
+class MyPostgresTraceStore(TraceStore):
+    async def create_trace(self, trace: Trace) -> str: ...
+    async def add_step(self, step: Step) -> str: ...
+    # ...
+```
+
+## 文档
+
+- [设计文档](docs/design.md) - 详细的架构设计和决策
+- [API 参考](docs/api.md) - 完整的 API 文档
+
+## License
+
+MIT

+ 316 - 0
docs/design.md

@@ -0,0 +1,316 @@
+# Reson Agent 设计文档
+
+> **设计目标**:可扩展、可学习的 Agent 框架,支持执行追踪和持久记忆。
+
+---
+
+## 1. 核心洞察
+
+**单次调用是 Agent 的特例**:
+
+| 特性 | 单次调用 | Agent 模式 |
+|------|---------|-----------|
+| 循环次数 | 1 | N (可配置) |
+| 工具调用 | 可选 | 常用 |
+| 状态管理 | 无 | 有 (Task State) |
+| 记忆检索 | 无 | 有 (Experience/Skill) |
+| 执行图 | 1 个节点 | N 个节点的 DAG |
+
+**统一抽象**:用同一套数据结构描述两者。
+
+---
+
+## 2. 三层记忆模型
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Layer 3: Skills(技能库)                                     │
+│ - 从 Experience 归纳的高层策略                                │
+│ - 层次化组织,按领域/任务类型分类                              │
+│ - 执行前注入到 System Prompt                                 │
+└─────────────────────────────────────────────────────────────┘
+                              ▲
+                              │ 归纳
+┌─────────────────────────────────────────────────────────────┐
+│ Layer 2: Experience(经验库)                                 │
+│ - 条件 + 规则 + 证据                                         │
+│ - 来源:执行反馈、人工标注                                    │
+│ - 持久存储,跨任务复用                                        │
+└─────────────────────────────────────────────────────────────┘
+                              ▲
+                              │ 提取
+┌─────────────────────────────────────────────────────────────┐
+│ Layer 1: Task State(任务状态)                               │
+│ - 当前任务的工作记忆                                          │
+│ - 计划、进度、中间结论                                        │
+│ - 任务结束时重置                                              │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 3. 执行图(Execution Graph)
+
+每次任务执行产生一张 DAG:
+
+```
+TaskNode(任务根节点)
+    │
+    ├─▶ LLMCallNode(第1次推理)
+    │       │
+    │       ├─▶ ToolCallNode(搜索工具)
+    │       │       │
+    │       │       └─▶ ToolResultNode(搜索结果)
+    │       │
+    │       └─▶ ConclusionNode(中间结论)
+    │
+    ├─▶ LLMCallNode(第2次推理)
+    │       │
+    │       └─▶ ConclusionNode(最终结论)
+    │
+    └─▶ FeedbackNode(人工反馈)
+            │
+            └─▶ ExperienceNode(提取的经验)
+```
+
+**节点类型**:
+
+| 类型 | 说明 | 数据 |
+|------|------|------|
+| `llm_call` | LLM 调用 | messages, response, model, tokens, cost |
+| `tool_call` | 工具调用 | tool_name, arguments |
+| `tool_result` | 工具结果 | result, duration_ms |
+| `conclusion` | 中间/最终结论 | content, is_final |
+| `feedback` | 人工反馈 | feedback_type, content, target_step_id |
+| `memory_read` | 读取记忆 | skills, experiences |
+| `memory_write` | 写入记忆 | experience_id |
+
+---
+
+## 4. 数据模型
+
+### 4.1 Trace(执行轨迹)
+
+```python
+@dataclass
+class Trace:
+    trace_id: str
+    mode: Literal["call", "agent"]
+    prompt_name: Optional[str] = None
+
+    # Agent 模式特有
+    task: Optional[str] = None
+    agent_type: Optional[str] = None
+
+    # 状态
+    status: Literal["running", "completed", "failed"] = "running"
+
+    # 统计
+    total_steps: int = 0
+    total_tokens: int = 0
+    total_cost: float = 0.0
+
+    # 上下文
+    uid: Optional[str] = None
+    context: Dict[str, Any] = field(default_factory=dict)
+
+    # 时间
+    created_at: datetime
+    completed_at: Optional[datetime] = None
+```
+
+### 4.2 Step(执行步骤)
+
+```python
+@dataclass
+class Step:
+    step_id: str
+    trace_id: str
+    step_type: StepType
+    sequence: int
+
+    # DAG 结构
+    parent_ids: List[str] = field(default_factory=list)
+
+    # 类型相关数据
+    data: Dict[str, Any] = field(default_factory=dict)
+
+    created_at: datetime
+```
+
+### 4.3 Experience(经验)
+
+```python
+@dataclass
+class Experience:
+    exp_id: str
+    scope: str  # "agent:{type}" 或 "user:{uid}"
+
+    # 核心三元组
+    condition: str
+    rule: str
+    evidence: List[str]  # step_ids
+
+    # 元数据
+    source: Literal["execution", "feedback", "manual"]
+    confidence: float = 0.5
+    usage_count: int = 0
+    success_rate: float = 0.0
+
+    created_at: datetime
+    updated_at: datetime
+```
+
+### 4.4 Skill(技能)
+
+```python
+@dataclass
+class Skill:
+    skill_id: str
+    scope: str
+
+    name: str
+    description: str
+    category: str
+
+    # 层次结构
+    parent_id: Optional[str] = None
+
+    # 内容
+    guidelines: List[str]
+    derived_from: List[str]  # experience_ids
+
+    version: int = 1
+    created_at: datetime
+    updated_at: datetime
+```
+
+---
+
+## 5. 存储抽象
+
+### Protocol 定义
+
+```python
+class TraceStore(Protocol):
+    async def create_trace(self, trace: Trace) -> str: ...
+    async def get_trace(self, trace_id: str) -> Optional[Trace]: ...
+    async def update_trace(self, trace_id: str, **updates) -> None: ...
+    async def add_step(self, step: Step) -> str: ...
+    async def get_trace_steps(self, trace_id: str) -> List[Step]: ...
+
+class MemoryStore(Protocol):
+    async def add_experience(self, exp: Experience) -> str: ...
+    async def search_experiences(self, scope: str, context: str, limit: int) -> List[Experience]: ...
+    async def add_skill(self, skill: Skill) -> str: ...
+    async def search_skills(self, scope: str, context: str, limit: int) -> List[Skill]: ...
+```
+
+---
+
+## 6. 事件系统
+
+```python
+@dataclass
+class AgentEvent:
+    type: Literal[
+        "trace_started",
+        "memory_loaded",
+        "step_started",
+        "llm_delta",
+        "tool_executing",
+        "tool_result",
+        "conclusion",
+        "feedback_received",
+        "experience_extracted",
+        "trace_completed",
+        "trace_failed"
+    ]
+    data: Dict[str, Any]
+```
+
+---
+
+## 7. 与 LLM 提供商的集成
+
+Agent 模块**不绑定**特定 LLM 提供商。通过 `LLMProvider` Protocol 抽象:
+
+```python
+class LLMProvider(Protocol):
+    async def chat(
+        self,
+        messages: List[Dict],
+        model: str,
+        tools: Optional[List[Dict]] = None,
+        **kwargs
+    ) -> AsyncIterator[LLMEvent]: ...
+```
+
+宿主项目实现具体的 Provider(OpenAI、Anthropic、Azure 等)。
+
+---
+
+## 8. 模块结构
+
+```
+reson_agent/
+├── __init__.py
+├── runner.py              # AgentRunner
+├── events.py              # AgentEvent
+├── models/
+│   ├── __init__.py
+│   ├── trace.py           # Trace, Step
+│   └── memory.py          # Experience, Skill
+├── storage/
+│   ├── __init__.py
+│   ├── protocols.py       # TraceStore, MemoryStore
+│   └── memory_impl.py     # 内存实现
+└── tools/
+    ├── __init__.py
+    ├── registry.py        # ToolRegistry, @tool
+    └── schema.py          # SchemaGenerator
+```
+
+---
+
+## 9. 设计决策
+
+### 为什么 Trace/Step 而不是复用 llm_call_history?
+
+- `llm_call_history` 是扁平的调用日志
+- `Trace/Step` 是带因果关系的执行图
+- Step 支持多种类型(llm_call, tool_call, conclusion, feedback)
+- Step 之间可以形成 DAG(不只是链表)
+
+### 为什么存储可插拔?
+
+- 不同项目有不同的存储需求(PostgreSQL、Neo4j、MongoDB)
+- MVP 阶段用内存实现,快速验证
+- 生产环境再接入持久化存储
+
+### 为什么工具系统要独立?
+
+- 工具注册和 Schema 生成是通用能力
+- 可以在没有 Agent 的场景单独使用
+- 与 LLM Provider 解耦
+
+---
+
+## 10. 后续计划
+
+### Phase 1(当前):MVP
+- [x] 数据模型
+- [x] 存储接口 + 内存实现
+- [x] 工具系统
+- [ ] AgentRunner 基础循环
+- [ ] 测试
+
+### Phase 2:完善
+- [ ] Experience 提取
+- [ ] Skill 归纳
+- [ ] PostgreSQL 存储实现
+- [ ] 与 Resonote 集成
+
+### Phase 3:重构 LLM 模块
+- [ ] 将验证过的设计合并回 Resonote/llm
+- [ ] 统一 call/stream/run

+ 40 - 0
pyproject.toml

@@ -0,0 +1,40 @@
+[project]
+name = "reson-agent"
+version = "0.1.0"
+description = "Extensible, learnable Agent framework with execution tracing and memory"
+readme = "README.md"
+requires-python = ">=3.10"
+license = {text = "MIT"}
+authors = [
+    {name = "Resonote Team"}
+]
+keywords = ["agent", "llm", "ai", "memory", "trace"]
+
+dependencies = [
+    "docstring-parser>=0.15",  # 用于解析函数 docstring 生成 schema
+]
+
+[project.optional-dependencies]
+postgres = ["asyncpg>=0.29"]
+redis = ["redis>=5.0"]
+openai = ["openai>=1.0"]
+dev = [
+    "pytest>=7.0",
+    "pytest-asyncio>=0.21",
+]
+all = [
+    "asyncpg>=0.29",
+    "redis>=5.0",
+    "openai>=1.0",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+packages = ["reson_agent"]
+
+[tool.pytest.ini_options]
+asyncio_mode = "auto"
+testpaths = ["tests"]

+ 41 - 0
reson_agent/__init__.py

@@ -0,0 +1,41 @@
+"""
+Reson Agent - 可扩展、可学习的 Agent 框架
+
+核心导出:
+- AgentRunner: Agent 执行引擎
+- AgentEvent: Agent 事件
+- Trace, Step: 执行追踪
+- Experience, Skill: 记忆模型
+- tool: 工具装饰器
+- TraceStore, MemoryStore: 存储接口
+"""
+
+from reson_agent.runner import AgentRunner
+from reson_agent.events import AgentEvent
+from reson_agent.models.trace import Trace, Step, StepType
+from reson_agent.models.memory import Experience, Skill
+from reson_agent.tools import tool, ToolRegistry, get_tool_registry
+from reson_agent.storage.protocols import TraceStore, MemoryStore, StateStore
+
+__version__ = "0.1.0"
+
+__all__ = [
+    # Runner
+    "AgentRunner",
+    # Events
+    "AgentEvent",
+    # Models
+    "Trace",
+    "Step",
+    "StepType",
+    "Experience",
+    "Skill",
+    # Tools
+    "tool",
+    "ToolRegistry",
+    "get_tool_registry",
+    # Storage
+    "TraceStore",
+    "MemoryStore",
+    "StateStore",
+]

+ 45 - 0
reson_agent/events.py

@@ -0,0 +1,45 @@
+"""
+Agent 事件定义
+"""
+
+from dataclasses import dataclass
+from typing import Dict, Any, Literal
+
+
+AgentEventType = Literal[
+    # Trace 生命周期
+    "trace_started",        # Trace 开始
+    "trace_completed",      # Trace 完成
+    "trace_failed",         # Trace 失败
+
+    # 记忆
+    "memory_loaded",        # 记忆加载完成(skills, experiences)
+    "experience_extracted", # 提取了经验
+
+    # 步骤
+    "step_started",         # 步骤开始
+    "llm_delta",            # LLM 输出增量
+    "llm_call_completed",   # LLM 调用完成
+    "tool_executing",       # 工具执行中
+    "tool_result",          # 工具结果
+    "conclusion",           # 结论(中间或最终)
+
+    # 反馈
+    "feedback_received",    # 收到人工反馈
+
+    # 等待用户
+    "awaiting_user_action", # 等待用户确认(工具调用)
+]
+
+
+@dataclass
+class AgentEvent:
+    """Agent 事件"""
+    type: AgentEventType
+    data: Dict[str, Any]
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {"type": self.type, "data": self.data}
+
+    def __repr__(self) -> str:
+        return f"AgentEvent(type={self.type!r}, data={self.data!r})"

+ 8 - 0
reson_agent/models/__init__.py

@@ -0,0 +1,8 @@
+"""
+Models 包
+"""
+
+from reson_agent.models.trace import Trace, Step, StepType
+from reson_agent.models.memory import Experience, Skill
+
+__all__ = ["Trace", "Step", "StepType", "Experience", "Skill"]

+ 164 - 0
reson_agent/models/memory.py

@@ -0,0 +1,164 @@
+"""
+Memory 数据模型
+
+Experience: 经验规则(条件 + 规则 + 证据)
+Skill: 技能(从经验归纳的高层知识)
+"""
+
+from dataclasses import dataclass, field
+from datetime import datetime
+from typing import Dict, Any, List, Optional, Literal
+import uuid
+
+
+@dataclass
+class Experience:
+    """
+    经验规则
+
+    从执行过程或人工反馈中提取的规则,格式:
+    - condition: 什么情况下适用
+    - rule: 应该怎么做
+    - evidence: 证据(step_ids)
+    """
+    exp_id: str
+    scope: str  # "agent:{type}" 或 "user:{uid}"
+
+    # 核心三元组
+    condition: str
+    rule: str
+    evidence: List[str] = field(default_factory=list)  # step_ids
+
+    # 元数据
+    source: Literal["execution", "feedback", "manual"] = "feedback"
+    confidence: float = 0.5
+    usage_count: int = 0
+    success_rate: float = 0.0
+
+    # 时间
+    created_at: datetime = field(default_factory=datetime.now)
+    updated_at: datetime = field(default_factory=datetime.now)
+
+    @classmethod
+    def create(
+        cls,
+        scope: str,
+        condition: str,
+        rule: str,
+        evidence: List[str] = None,
+        source: Literal["execution", "feedback", "manual"] = "feedback",
+        confidence: float = 0.5,
+    ) -> "Experience":
+        """创建新的 Experience"""
+        now = datetime.now()
+        return cls(
+            exp_id=str(uuid.uuid4()),
+            scope=scope,
+            condition=condition,
+            rule=rule,
+            evidence=evidence or [],
+            source=source,
+            confidence=confidence,
+            created_at=now,
+            updated_at=now,
+        )
+
+    def to_dict(self) -> Dict[str, Any]:
+        """转换为字典"""
+        return {
+            "exp_id": self.exp_id,
+            "scope": self.scope,
+            "condition": self.condition,
+            "rule": self.rule,
+            "evidence": self.evidence,
+            "source": self.source,
+            "confidence": self.confidence,
+            "usage_count": self.usage_count,
+            "success_rate": self.success_rate,
+            "created_at": self.created_at.isoformat() if self.created_at else None,
+            "updated_at": self.updated_at.isoformat() if self.updated_at else None,
+        }
+
+    def to_prompt_text(self) -> str:
+        """转换为可注入 Prompt 的文本"""
+        return f"当 {self.condition} 时,{self.rule}"
+
+
+@dataclass
+class Skill:
+    """
+    技能 - 从经验归纳的高层知识
+
+    技能可以形成层次结构(通过 parent_id)
+    """
+    skill_id: str
+    scope: str  # "agent:{type}" 或 "user:{uid}"
+
+    name: str
+    description: str
+    category: str  # 分类,如 "search", "reasoning", "writing"
+
+    # 层次结构
+    parent_id: Optional[str] = None
+
+    # 内容
+    guidelines: List[str] = field(default_factory=list)
+    derived_from: List[str] = field(default_factory=list)  # experience_ids
+
+    # 版本
+    version: int = 1
+
+    # 时间
+    created_at: datetime = field(default_factory=datetime.now)
+    updated_at: datetime = field(default_factory=datetime.now)
+
+    @classmethod
+    def create(
+        cls,
+        scope: str,
+        name: str,
+        description: str,
+        category: str = "general",
+        guidelines: List[str] = None,
+        derived_from: List[str] = None,
+        parent_id: Optional[str] = None,
+    ) -> "Skill":
+        """创建新的 Skill"""
+        now = datetime.now()
+        return cls(
+            skill_id=str(uuid.uuid4()),
+            scope=scope,
+            name=name,
+            description=description,
+            category=category,
+            parent_id=parent_id,
+            guidelines=guidelines or [],
+            derived_from=derived_from or [],
+            created_at=now,
+            updated_at=now,
+        )
+
+    def to_dict(self) -> Dict[str, Any]:
+        """转换为字典"""
+        return {
+            "skill_id": self.skill_id,
+            "scope": self.scope,
+            "name": self.name,
+            "description": self.description,
+            "category": self.category,
+            "parent_id": self.parent_id,
+            "guidelines": self.guidelines,
+            "derived_from": self.derived_from,
+            "version": self.version,
+            "created_at": self.created_at.isoformat() if self.created_at else None,
+            "updated_at": self.updated_at.isoformat() if self.updated_at else None,
+        }
+
+    def to_prompt_text(self) -> str:
+        """转换为可注入 Prompt 的文本"""
+        lines = [f"### {self.name}", self.description]
+        if self.guidelines:
+            lines.append("指导原则:")
+            for g in self.guidelines:
+                lines.append(f"- {g}")
+        return "\n".join(lines)

+ 197 - 0
reson_agent/models/trace.py

@@ -0,0 +1,197 @@
+"""
+Trace 和 Step 数据模型
+
+Trace: 一次完整的 LLM 交互(单次调用或 Agent 任务)
+Step: Trace 中的一个原子操作
+"""
+
+from dataclasses import dataclass, field
+from datetime import datetime
+from typing import Dict, Any, List, Optional, Literal
+import uuid
+
+
+StepType = Literal[
+    "llm_call",      # LLM 调用
+    "tool_call",     # 工具调用
+    "tool_result",   # 工具结果
+    "conclusion",    # 中间/最终结论
+    "feedback",      # 人工反馈
+    "memory_read",   # 读取记忆(经验/技能)
+    "memory_write",  # 写入记忆
+]
+
+
+@dataclass
+class Trace:
+    """
+    执行轨迹 - 一次完整的 LLM 交互
+
+    单次调用: mode="call", 只有 1 个 Step
+    Agent 模式: mode="agent", 多个 Steps 形成 DAG
+    """
+    trace_id: str
+    mode: Literal["call", "agent"]
+
+    # Prompt 标识(可选)
+    prompt_name: Optional[str] = None
+
+    # Agent 模式特有
+    task: Optional[str] = None
+    agent_type: Optional[str] = None
+
+    # 状态
+    status: Literal["running", "completed", "failed"] = "running"
+
+    # 统计
+    total_steps: int = 0
+    total_tokens: int = 0
+    total_cost: float = 0.0
+
+    # 上下文
+    uid: Optional[str] = None
+    context: Dict[str, Any] = field(default_factory=dict)
+
+    # 时间
+    created_at: datetime = field(default_factory=datetime.now)
+    completed_at: Optional[datetime] = None
+
+    @classmethod
+    def create(
+        cls,
+        mode: Literal["call", "agent"],
+        **kwargs
+    ) -> "Trace":
+        """创建新的 Trace"""
+        return cls(
+            trace_id=str(uuid.uuid4()),
+            mode=mode,
+            **kwargs
+        )
+
+    def to_dict(self) -> Dict[str, Any]:
+        """转换为字典"""
+        return {
+            "trace_id": self.trace_id,
+            "mode": self.mode,
+            "prompt_name": self.prompt_name,
+            "task": self.task,
+            "agent_type": self.agent_type,
+            "status": self.status,
+            "total_steps": self.total_steps,
+            "total_tokens": self.total_tokens,
+            "total_cost": self.total_cost,
+            "uid": self.uid,
+            "context": self.context,
+            "created_at": self.created_at.isoformat() if self.created_at else None,
+            "completed_at": self.completed_at.isoformat() if self.completed_at else None,
+        }
+
+
+@dataclass
+class Step:
+    """
+    执行步骤 - Trace 中的一个原子操作
+
+    Step 之间通过 parent_ids 形成 DAG 结构
+    """
+    step_id: str
+    trace_id: str
+    step_type: StepType
+    sequence: int  # 在 Trace 中的顺序
+
+    # DAG 结构(支持多父节点)
+    parent_ids: List[str] = field(default_factory=list)
+
+    # 类型相关数据
+    data: Dict[str, Any] = field(default_factory=dict)
+
+    # 时间
+    created_at: datetime = field(default_factory=datetime.now)
+
+    @classmethod
+    def create(
+        cls,
+        trace_id: str,
+        step_type: StepType,
+        sequence: int,
+        data: Dict[str, Any] = None,
+        parent_ids: List[str] = None,
+    ) -> "Step":
+        """创建新的 Step"""
+        return cls(
+            step_id=str(uuid.uuid4()),
+            trace_id=trace_id,
+            step_type=step_type,
+            sequence=sequence,
+            parent_ids=parent_ids or [],
+            data=data or {},
+        )
+
+    def to_dict(self) -> Dict[str, Any]:
+        """转换为字典"""
+        return {
+            "step_id": self.step_id,
+            "trace_id": self.trace_id,
+            "step_type": self.step_type,
+            "sequence": self.sequence,
+            "parent_ids": self.parent_ids,
+            "data": self.data,
+            "created_at": self.created_at.isoformat() if self.created_at else None,
+        }
+
+
+# Step.data 结构说明
+#
+# llm_call:
+#   {
+#       "messages": [...],
+#       "response": "...",
+#       "model": "gpt-4o",
+#       "prompt_tokens": 100,
+#       "completion_tokens": 50,
+#       "cost": 0.01,
+#       "tool_calls": [...]  # 如果有
+#   }
+#
+# tool_call:
+#   {
+#       "tool_name": "search_blocks",
+#       "arguments": {...},
+#       "llm_step_id": "..."  # 哪个 LLM 调用触发的
+#   }
+#
+# tool_result:
+#   {
+#       "tool_call_step_id": "...",
+#       "result": "...",
+#       "duration_ms": 123
+#   }
+#
+# conclusion:
+#   {
+#       "content": "...",
+#       "is_final": True/False
+#   }
+#
+# feedback:
+#   {
+#       "target_step_id": "...",
+#       "feedback_type": "positive" | "negative" | "correction",
+#       "content": "..."
+#   }
+#
+# memory_read:
+#   {
+#       "skills": [...],
+#       "experiences": [...],
+#       "skills_count": 3,
+#       "experiences_count": 5
+#   }
+#
+# memory_write:
+#   {
+#       "experience_id": "...",
+#       "condition": "...",
+#       "rule": "..."
+#   }

+ 571 - 0
reson_agent/runner.py

@@ -0,0 +1,571 @@
+"""
+Agent Runner - Agent 执行引擎
+
+核心职责:
+1. 执行 Agent 任务(循环调用 LLM + 工具)
+2. 记录执行图(Trace + Steps)
+3. 检索和注入记忆(Experience + Skill)
+4. 收集反馈,提取经验
+"""
+
+import logging
+from dataclasses import dataclass, field
+from datetime import datetime
+from typing import AsyncIterator, Optional, Dict, Any, List, Callable, Literal
+
+from reson_agent.events import AgentEvent
+from reson_agent.models.trace import Trace, Step
+from reson_agent.models.memory import Experience, Skill
+from reson_agent.storage.protocols import TraceStore, MemoryStore, StateStore
+from reson_agent.tools import ToolRegistry, get_tool_registry
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class AgentConfig:
+    """Agent 配置"""
+    agent_type: str = "default"
+    max_iterations: int = 10
+    enable_memory: bool = True
+    auto_execute_tools: bool = True
+
+
+@dataclass
+class CallResult:
+    """单次调用结果"""
+    reply: str
+    tool_calls: Optional[List[Dict]] = None
+    trace_id: Optional[str] = None
+    step_id: Optional[str] = None
+    tokens: Optional[Dict[str, int]] = None
+    cost: float = 0.0
+
+
+class AgentRunner:
+    """
+    Agent 执行引擎
+
+    支持两种模式:
+    1. call(): 单次 LLM 调用(简洁 API)
+    2. run(): Agent 模式(循环 + 记忆 + 追踪)
+    """
+
+    def __init__(
+        self,
+        trace_store: Optional[TraceStore] = None,
+        memory_store: Optional[MemoryStore] = None,
+        state_store: Optional[StateStore] = None,
+        tool_registry: Optional[ToolRegistry] = None,
+        llm_call: Optional[Callable] = None,
+        config: Optional[AgentConfig] = None,
+    ):
+        """
+        初始化 AgentRunner
+
+        Args:
+            trace_store: Trace 存储(可选,不提供则不记录)
+            memory_store: Memory 存储(可选,不提供则不使用记忆)
+            state_store: State 存储(可选,用于任务状态)
+            tool_registry: 工具注册表(可选,默认使用全局注册表)
+            llm_call: LLM 调用函数(必须提供,用于实际调用 LLM)
+            config: Agent 配置
+        """
+        self.trace_store = trace_store
+        self.memory_store = memory_store
+        self.state_store = state_store
+        self.tools = tool_registry or get_tool_registry()
+        self.llm_call = llm_call
+        self.config = config or AgentConfig()
+
+    def _generate_id(self) -> str:
+        """生成唯一 ID"""
+        import uuid
+        return str(uuid.uuid4())
+
+    # ===== 单次调用 =====
+
+    async def call(
+        self,
+        messages: List[Dict],
+        model: str = "gpt-4o",
+        tools: Optional[List[str]] = None,
+        uid: Optional[str] = None,
+        trace: bool = True,
+        **kwargs
+    ) -> CallResult:
+        """
+        单次 LLM 调用
+
+        Args:
+            messages: 消息列表
+            model: 模型名称
+            tools: 工具名称列表
+            uid: 用户 ID
+            trace: 是否记录 Trace
+            **kwargs: 其他参数传递给 LLM
+
+        Returns:
+            CallResult
+        """
+        if not self.llm_call:
+            raise ValueError("llm_call function not provided")
+
+        trace_id = None
+        step_id = None
+
+        # 创建 Trace
+        if trace and self.trace_store:
+            trace_obj = Trace.create(
+                mode="call",
+                uid=uid,
+                context={"model": model}
+            )
+            trace_id = await self.trace_store.create_trace(trace_obj)
+
+        # 准备工具 Schema
+        tool_schemas = None
+        if tools:
+            tool_schemas = self.tools.get_schemas(tools)
+
+        # 调用 LLM
+        result = await self.llm_call(
+            messages=messages,
+            model=model,
+            tools=tool_schemas,
+            **kwargs
+        )
+
+        # 记录 Step
+        if trace and self.trace_store and trace_id:
+            step = Step.create(
+                trace_id=trace_id,
+                step_type="llm_call",
+                sequence=0,
+                data={
+                    "messages": messages,
+                    "response": result.get("content", ""),
+                    "model": model,
+                    "tool_calls": result.get("tool_calls"),
+                    "prompt_tokens": result.get("prompt_tokens", 0),
+                    "completion_tokens": result.get("completion_tokens", 0),
+                    "cost": result.get("cost", 0),
+                }
+            )
+            step_id = await self.trace_store.add_step(step)
+
+            # 完成 Trace
+            await self.trace_store.update_trace(
+                trace_id,
+                status="completed",
+                completed_at=datetime.now(),
+                total_tokens=result.get("prompt_tokens", 0) + result.get("completion_tokens", 0),
+                total_cost=result.get("cost", 0)
+            )
+
+        return CallResult(
+            reply=result.get("content", ""),
+            tool_calls=result.get("tool_calls"),
+            trace_id=trace_id,
+            step_id=step_id,
+            tokens={
+                "prompt": result.get("prompt_tokens", 0),
+                "completion": result.get("completion_tokens", 0),
+            },
+            cost=result.get("cost", 0)
+        )
+
+    # ===== Agent 模式 =====
+
+    async def run(
+        self,
+        task: str,
+        messages: Optional[List[Dict]] = None,
+        system_prompt: Optional[str] = None,
+        model: str = "gpt-4o",
+        tools: Optional[List[str]] = None,
+        agent_type: Optional[str] = None,
+        uid: Optional[str] = None,
+        max_iterations: Optional[int] = None,
+        enable_memory: Optional[bool] = None,
+        auto_execute_tools: Optional[bool] = None,
+        **kwargs
+    ) -> AsyncIterator[AgentEvent]:
+        """
+        Agent 模式执行
+
+        Args:
+            task: 任务描述
+            messages: 初始消息(可选)
+            system_prompt: 系统提示(可选)
+            model: 模型名称
+            tools: 工具名称列表
+            agent_type: Agent 类型
+            uid: 用户 ID
+            max_iterations: 最大迭代次数
+            enable_memory: 是否启用记忆
+            auto_execute_tools: 是否自动执行工具
+            **kwargs: 其他参数
+
+        Yields:
+            AgentEvent
+        """
+        if not self.llm_call:
+            raise ValueError("llm_call function not provided")
+
+        # 使用配置默认值
+        agent_type = agent_type or self.config.agent_type
+        max_iterations = max_iterations or self.config.max_iterations
+        enable_memory = enable_memory if enable_memory is not None else self.config.enable_memory
+        auto_execute_tools = auto_execute_tools if auto_execute_tools is not None else self.config.auto_execute_tools
+
+        # 创建 Trace
+        trace_id = self._generate_id()
+        if self.trace_store:
+            trace_obj = Trace(
+                trace_id=trace_id,
+                mode="agent",
+                task=task,
+                agent_type=agent_type,
+                uid=uid,
+                context={"model": model, **kwargs}
+            )
+            await self.trace_store.create_trace(trace_obj)
+
+        yield AgentEvent("trace_started", {
+            "trace_id": trace_id,
+            "task": task,
+            "agent_type": agent_type
+        })
+
+        try:
+            # 加载记忆
+            skills_text = ""
+            experiences_text = ""
+
+            if enable_memory and self.memory_store:
+                scope = f"agent:{agent_type}"
+                skills = await self.memory_store.search_skills(scope, task)
+                experiences = await self.memory_store.search_experiences(scope, task)
+
+                skills_text = self._format_skills(skills)
+                experiences_text = self._format_experiences(experiences)
+
+                # 记录 memory_read Step
+                if self.trace_store:
+                    mem_step = Step.create(
+                        trace_id=trace_id,
+                        step_type="memory_read",
+                        sequence=0,
+                        data={
+                            "skills_count": len(skills),
+                            "experiences_count": len(experiences),
+                            "skills": [s.to_dict() for s in skills],
+                            "experiences": [e.to_dict() for e in experiences],
+                        }
+                    )
+                    await self.trace_store.add_step(mem_step)
+
+                yield AgentEvent("memory_loaded", {
+                    "skills_count": len(skills),
+                    "experiences_count": len(experiences)
+                })
+
+            # 构建初始消息
+            if messages is None:
+                messages = []
+
+            if system_prompt:
+                # 注入记忆到 system prompt
+                full_system = system_prompt
+                if skills_text:
+                    full_system += f"\n\n## 相关技能\n{skills_text}"
+                if experiences_text:
+                    full_system += f"\n\n## 相关经验\n{experiences_text}"
+
+                messages = [{"role": "system", "content": full_system}] + messages
+
+            # 添加任务描述
+            messages.append({"role": "user", "content": task})
+
+            # 准备工具
+            tool_schemas = None
+            if tools:
+                tool_schemas = self.tools.get_schemas(tools)
+
+            # 执行循环
+            parent_step_ids = []
+            sequence = 1
+            total_tokens = 0
+            total_cost = 0.0
+
+            for iteration in range(max_iterations):
+                yield AgentEvent("step_started", {
+                    "iteration": iteration,
+                    "step_type": "llm_call"
+                })
+
+                # 调用 LLM
+                result = await self.llm_call(
+                    messages=messages,
+                    model=model,
+                    tools=tool_schemas,
+                    **kwargs
+                )
+
+                response_content = result.get("content", "")
+                tool_calls = result.get("tool_calls")
+                tokens = result.get("prompt_tokens", 0) + result.get("completion_tokens", 0)
+                cost = result.get("cost", 0)
+
+                total_tokens += tokens
+                total_cost += cost
+
+                # 记录 LLM 调用 Step
+                llm_step_id = self._generate_id()
+                if self.trace_store:
+                    llm_step = Step(
+                        step_id=llm_step_id,
+                        trace_id=trace_id,
+                        step_type="llm_call",
+                        sequence=sequence,
+                        parent_ids=parent_step_ids,
+                        data={
+                            "messages": messages,
+                            "response": response_content,
+                            "model": model,
+                            "tool_calls": tool_calls,
+                            "prompt_tokens": result.get("prompt_tokens", 0),
+                            "completion_tokens": result.get("completion_tokens", 0),
+                            "cost": cost,
+                        }
+                    )
+                    await self.trace_store.add_step(llm_step)
+
+                sequence += 1
+                parent_step_ids = [llm_step_id]
+
+                yield AgentEvent("llm_call_completed", {
+                    "step_id": llm_step_id,
+                    "content": response_content,
+                    "tool_calls": tool_calls,
+                    "tokens": tokens,
+                    "cost": cost
+                })
+
+                # 处理工具调用
+                if tool_calls and auto_execute_tools:
+                    # 检查是否需要用户确认
+                    if self.tools.check_confirmation_required(tool_calls):
+                        yield AgentEvent("awaiting_user_action", {
+                            "tool_calls": tool_calls,
+                            "confirmation_flags": self.tools.get_confirmation_flags(tool_calls),
+                            "editable_params": self.tools.get_editable_params_map(tool_calls)
+                        })
+                        # TODO: 等待用户确认
+                        break
+
+                    # 执行工具
+                    messages.append({"role": "assistant", "content": response_content, "tool_calls": tool_calls})
+
+                    for tc in tool_calls:
+                        tool_name = tc["function"]["name"]
+                        tool_args = tc["function"]["arguments"]
+                        if isinstance(tool_args, str):
+                            import json
+                            tool_args = json.loads(tool_args)
+
+                        yield AgentEvent("tool_executing", {
+                            "tool_name": tool_name,
+                            "arguments": tool_args
+                        })
+
+                        # 执行工具
+                        tool_result = await self.tools.execute(
+                            tool_name,
+                            tool_args,
+                            uid=uid or ""
+                        )
+
+                        # 记录 tool_call Step
+                        tool_step_id = self._generate_id()
+                        if self.trace_store:
+                            tool_step = Step(
+                                step_id=tool_step_id,
+                                trace_id=trace_id,
+                                step_type="tool_call",
+                                sequence=sequence,
+                                parent_ids=[llm_step_id],
+                                data={
+                                    "tool_name": tool_name,
+                                    "arguments": tool_args,
+                                    "result": tool_result,
+                                }
+                            )
+                            await self.trace_store.add_step(tool_step)
+
+                        sequence += 1
+                        parent_step_ids.append(tool_step_id)
+
+                        yield AgentEvent("tool_result", {
+                            "step_id": tool_step_id,
+                            "tool_name": tool_name,
+                            "result": tool_result
+                        })
+
+                        # 添加到消息
+                        messages.append({
+                            "role": "tool",
+                            "tool_call_id": tc["id"],
+                            "content": tool_result
+                        })
+
+                    continue  # 继续循环
+
+                # 无工具调用,任务完成
+                # 记录 conclusion Step
+                conclusion_step_id = self._generate_id()
+                if self.trace_store:
+                    conclusion_step = Step(
+                        step_id=conclusion_step_id,
+                        trace_id=trace_id,
+                        step_type="conclusion",
+                        sequence=sequence,
+                        parent_ids=parent_step_ids,
+                        data={
+                            "content": response_content,
+                            "is_final": True
+                        }
+                    )
+                    await self.trace_store.add_step(conclusion_step)
+
+                yield AgentEvent("conclusion", {
+                    "step_id": conclusion_step_id,
+                    "content": response_content,
+                    "is_final": True
+                })
+                break
+
+            # 完成 Trace
+            if self.trace_store:
+                await self.trace_store.update_trace(
+                    trace_id,
+                    status="completed",
+                    completed_at=datetime.now(),
+                    total_tokens=total_tokens,
+                    total_cost=total_cost
+                )
+
+            yield AgentEvent("trace_completed", {
+                "trace_id": trace_id,
+                "total_tokens": total_tokens,
+                "total_cost": total_cost
+            })
+
+        except Exception as e:
+            logger.error(f"Agent run failed: {e}")
+
+            if self.trace_store:
+                await self.trace_store.update_trace(
+                    trace_id,
+                    status="failed",
+                    completed_at=datetime.now()
+                )
+
+            yield AgentEvent("trace_failed", {
+                "trace_id": trace_id,
+                "error": str(e)
+            })
+            raise
+
+    # ===== 反馈 =====
+
+    async def add_feedback(
+        self,
+        trace_id: str,
+        target_step_id: str,
+        feedback_type: Literal["positive", "negative", "correction"],
+        content: str,
+        extract_experience: bool = True
+    ) -> Optional[str]:
+        """
+        添加人工反馈
+
+        Args:
+            trace_id: Trace ID
+            target_step_id: 反馈针对的 Step ID
+            feedback_type: 反馈类型
+            content: 反馈内容
+            extract_experience: 是否自动提取经验
+
+        Returns:
+            experience_id: 如果提取了经验
+        """
+        if not self.trace_store:
+            return None
+
+        # 获取 Trace
+        trace = await self.trace_store.get_trace(trace_id)
+        if not trace:
+            logger.warning(f"Trace not found: {trace_id}")
+            return None
+
+        # 创建 feedback Step
+        steps = await self.trace_store.get_trace_steps(trace_id)
+        max_seq = max(s.sequence for s in steps) if steps else 0
+
+        feedback_step = Step.create(
+            trace_id=trace_id,
+            step_type="feedback",
+            sequence=max_seq + 1,
+            parent_ids=[target_step_id],
+            data={
+                "target_step_id": target_step_id,
+                "feedback_type": feedback_type,
+                "content": content
+            }
+        )
+        await self.trace_store.add_step(feedback_step)
+
+        # 提取经验
+        exp_id = None
+        if extract_experience and self.memory_store and feedback_type in ("positive", "correction"):
+            exp = Experience.create(
+                scope=f"agent:{trace.agent_type}" if trace.agent_type else "agent:default",
+                condition=f"执行类似 '{trace.task}' 任务时" if trace.task else "通用场景",
+                rule=content,
+                evidence=[target_step_id, feedback_step.step_id],
+                source="feedback",
+                confidence=0.8 if feedback_type == "positive" else 0.6
+            )
+            exp_id = await self.memory_store.add_experience(exp)
+
+            # 记录 memory_write Step
+            mem_step = Step.create(
+                trace_id=trace_id,
+                step_type="memory_write",
+                sequence=max_seq + 2,
+                parent_ids=[feedback_step.step_id],
+                data={
+                    "experience_id": exp_id,
+                    "condition": exp.condition,
+                    "rule": exp.rule
+                }
+            )
+            await self.trace_store.add_step(mem_step)
+
+        return exp_id
+
+    # ===== 辅助方法 =====
+
+    def _format_skills(self, skills: List[Skill]) -> str:
+        """格式化技能为 Prompt 文本"""
+        if not skills:
+            return ""
+        return "\n\n".join(s.to_prompt_text() for s in skills)
+
+    def _format_experiences(self, experiences: List[Experience]) -> str:
+        """格式化经验为 Prompt 文本"""
+        if not experiences:
+            return ""
+        return "\n".join(f"- {e.to_prompt_text()}" for e in experiences)

+ 15 - 0
reson_agent/storage/__init__.py

@@ -0,0 +1,15 @@
+"""
+Storage 包 - 存储接口和实现
+"""
+
+from reson_agent.storage.protocols import TraceStore, MemoryStore, StateStore
+from reson_agent.storage.memory_impl import MemoryTraceStore, MemoryMemoryStore, MemoryStateStore
+
+__all__ = [
+    "TraceStore",
+    "MemoryStore",
+    "StateStore",
+    "MemoryTraceStore",
+    "MemoryMemoryStore",
+    "MemoryStateStore",
+]

+ 184 - 0
reson_agent/storage/memory_impl.py

@@ -0,0 +1,184 @@
+"""
+Memory Implementation - 内存存储实现
+
+用于测试和简单场景,数据不持久化
+"""
+
+from typing import Dict, List, Optional, Any
+from datetime import datetime
+
+from reson_agent.models.trace import Trace, Step
+from reson_agent.models.memory import Experience, Skill
+
+
+class MemoryTraceStore:
+    """内存 Trace 存储"""
+
+    def __init__(self):
+        self._traces: Dict[str, Trace] = {}
+        self._steps: Dict[str, Step] = {}
+        self._trace_steps: Dict[str, List[str]] = {}  # trace_id -> [step_ids]
+
+    async def create_trace(self, trace: Trace) -> str:
+        self._traces[trace.trace_id] = trace
+        self._trace_steps[trace.trace_id] = []
+        return trace.trace_id
+
+    async def get_trace(self, trace_id: str) -> Optional[Trace]:
+        return self._traces.get(trace_id)
+
+    async def update_trace(self, trace_id: str, **updates) -> None:
+        trace = self._traces.get(trace_id)
+        if trace:
+            for key, value in updates.items():
+                if hasattr(trace, key):
+                    setattr(trace, key, value)
+
+    async def list_traces(
+        self,
+        mode: Optional[str] = None,
+        agent_type: Optional[str] = None,
+        uid: Optional[str] = None,
+        status: Optional[str] = None,
+        limit: int = 50
+    ) -> List[Trace]:
+        traces = list(self._traces.values())
+
+        # 过滤
+        if mode:
+            traces = [t for t in traces if t.mode == mode]
+        if agent_type:
+            traces = [t for t in traces if t.agent_type == agent_type]
+        if uid:
+            traces = [t for t in traces if t.uid == uid]
+        if status:
+            traces = [t for t in traces if t.status == status]
+
+        # 排序(最新的在前)
+        traces.sort(key=lambda t: t.created_at, reverse=True)
+
+        return traces[:limit]
+
+    async def add_step(self, step: Step) -> str:
+        self._steps[step.step_id] = step
+
+        # 添加到 trace 的 steps 列表
+        if step.trace_id in self._trace_steps:
+            self._trace_steps[step.trace_id].append(step.step_id)
+
+        # 更新 trace 的 total_steps
+        trace = self._traces.get(step.trace_id)
+        if trace:
+            trace.total_steps += 1
+
+        return step.step_id
+
+    async def get_step(self, step_id: str) -> Optional[Step]:
+        return self._steps.get(step_id)
+
+    async def get_trace_steps(self, trace_id: str) -> List[Step]:
+        step_ids = self._trace_steps.get(trace_id, [])
+        steps = [self._steps[sid] for sid in step_ids if sid in self._steps]
+        steps.sort(key=lambda s: s.sequence)
+        return steps
+
+    async def get_step_children(self, step_id: str) -> List[Step]:
+        children = []
+        for step in self._steps.values():
+            if step_id in step.parent_ids:
+                children.append(step)
+        children.sort(key=lambda s: s.sequence)
+        return children
+
+
+class MemoryMemoryStore:
+    """内存 Memory 存储(Experience + Skill)"""
+
+    def __init__(self):
+        self._experiences: Dict[str, Experience] = {}
+        self._skills: Dict[str, Skill] = {}
+
+    # ===== Experience =====
+
+    async def add_experience(self, exp: Experience) -> str:
+        self._experiences[exp.exp_id] = exp
+        return exp.exp_id
+
+    async def get_experience(self, exp_id: str) -> Optional[Experience]:
+        return self._experiences.get(exp_id)
+
+    async def search_experiences(
+        self,
+        scope: str,
+        context: str,
+        limit: int = 10
+    ) -> List[Experience]:
+        # 简单实现:按 scope 过滤,按 confidence 排序
+        experiences = [
+            e for e in self._experiences.values()
+            if e.scope == scope
+        ]
+        experiences.sort(key=lambda e: e.confidence, reverse=True)
+        return experiences[:limit]
+
+    async def update_experience_stats(
+        self,
+        exp_id: str,
+        success: bool
+    ) -> None:
+        exp = self._experiences.get(exp_id)
+        if exp:
+            exp.usage_count += 1
+            if success:
+                # 更新成功率
+                total_success = exp.success_rate * (exp.usage_count - 1) + (1 if success else 0)
+                exp.success_rate = total_success / exp.usage_count
+            exp.updated_at = datetime.now()
+
+    # ===== Skill =====
+
+    async def add_skill(self, skill: Skill) -> str:
+        self._skills[skill.skill_id] = skill
+        return skill.skill_id
+
+    async def get_skill(self, skill_id: str) -> Optional[Skill]:
+        return self._skills.get(skill_id)
+
+    async def get_skill_tree(self, scope: str) -> List[Skill]:
+        return [s for s in self._skills.values() if s.scope == scope]
+
+    async def search_skills(
+        self,
+        scope: str,
+        context: str,
+        limit: int = 5
+    ) -> List[Skill]:
+        # 简单实现:按 scope 过滤
+        skills = [s for s in self._skills.values() if s.scope == scope]
+        return skills[:limit]
+
+
+class MemoryStateStore:
+    """内存状态存储"""
+
+    def __init__(self):
+        self._state: Dict[str, Dict[str, Any]] = {}
+
+    async def get(self, key: str) -> Optional[Dict[str, Any]]:
+        return self._state.get(key)
+
+    async def set(
+        self,
+        key: str,
+        value: Dict[str, Any],
+        ttl: Optional[int] = None
+    ) -> None:
+        # 内存实现忽略 ttl
+        self._state[key] = value
+
+    async def update(self, key: str, **updates) -> None:
+        if key in self._state:
+            self._state[key].update(updates)
+
+    async def delete(self, key: str) -> None:
+        self._state.pop(key, None)

+ 175 - 0
reson_agent/storage/protocols.py

@@ -0,0 +1,175 @@
+"""
+Storage Protocols - 存储接口定义
+
+使用 Protocol 定义接口,允许不同的存储实现(内存、PostgreSQL、Neo4j 等)
+"""
+
+from typing import Protocol, List, Optional, Dict, Any, runtime_checkable
+
+from reson_agent.models.trace import Trace, Step
+from reson_agent.models.memory import Experience, Skill
+
+
+@runtime_checkable
+class TraceStore(Protocol):
+    """Trace + Step 存储接口"""
+
+    # ===== Trace 操作 =====
+
+    async def create_trace(self, trace: Trace) -> str:
+        """
+        创建新的 Trace
+
+        Args:
+            trace: Trace 对象
+
+        Returns:
+            trace_id
+        """
+        ...
+
+    async def get_trace(self, trace_id: str) -> Optional[Trace]:
+        """获取 Trace"""
+        ...
+
+    async def update_trace(self, trace_id: str, **updates) -> None:
+        """
+        更新 Trace
+
+        Args:
+            trace_id: Trace ID
+            **updates: 要更新的字段
+        """
+        ...
+
+    async def list_traces(
+        self,
+        mode: Optional[str] = None,
+        agent_type: Optional[str] = None,
+        uid: Optional[str] = None,
+        status: Optional[str] = None,
+        limit: int = 50
+    ) -> List[Trace]:
+        """列出 Traces"""
+        ...
+
+    # ===== Step 操作 =====
+
+    async def add_step(self, step: Step) -> str:
+        """
+        添加 Step
+
+        Args:
+            step: Step 对象
+
+        Returns:
+            step_id
+        """
+        ...
+
+    async def get_step(self, step_id: str) -> Optional[Step]:
+        """获取 Step"""
+        ...
+
+    async def get_trace_steps(self, trace_id: str) -> List[Step]:
+        """获取 Trace 的所有 Steps(按 sequence 排序)"""
+        ...
+
+    async def get_step_children(self, step_id: str) -> List[Step]:
+        """获取 Step 的子节点"""
+        ...
+
+
+@runtime_checkable
+class MemoryStore(Protocol):
+    """Experience + Skill 存储接口"""
+
+    # ===== Experience 操作 =====
+
+    async def add_experience(self, exp: Experience) -> str:
+        """添加 Experience"""
+        ...
+
+    async def get_experience(self, exp_id: str) -> Optional[Experience]:
+        """获取 Experience"""
+        ...
+
+    async def search_experiences(
+        self,
+        scope: str,
+        context: str,
+        limit: int = 10
+    ) -> List[Experience]:
+        """
+        搜索相关 Experience
+
+        Args:
+            scope: 范围(如 "agent:researcher")
+            context: 当前上下文,用于语义匹配
+            limit: 最大返回数量
+        """
+        ...
+
+    async def update_experience_stats(
+        self,
+        exp_id: str,
+        success: bool
+    ) -> None:
+        """更新 Experience 使用统计"""
+        ...
+
+    # ===== Skill 操作 =====
+
+    async def add_skill(self, skill: Skill) -> str:
+        """添加 Skill"""
+        ...
+
+    async def get_skill(self, skill_id: str) -> Optional[Skill]:
+        """获取 Skill"""
+        ...
+
+    async def get_skill_tree(self, scope: str) -> List[Skill]:
+        """获取技能树"""
+        ...
+
+    async def search_skills(
+        self,
+        scope: str,
+        context: str,
+        limit: int = 5
+    ) -> List[Skill]:
+        """搜索相关 Skills"""
+        ...
+
+
+@runtime_checkable
+class StateStore(Protocol):
+    """短期状态存储接口(用于 Task State,通常用 Redis)"""
+
+    async def get(self, key: str) -> Optional[Dict[str, Any]]:
+        """获取状态"""
+        ...
+
+    async def set(
+        self,
+        key: str,
+        value: Dict[str, Any],
+        ttl: Optional[int] = None
+    ) -> None:
+        """
+        设置状态
+
+        Args:
+            key: 键
+            value: 值
+            ttl: 过期时间(秒)
+        """
+        ...
+
+    async def update(self, key: str, **updates) -> None:
+        """部分更新"""
+        ...
+
+    async def delete(self, key: str) -> None:
+        """删除"""
+        ...

+ 8 - 0
reson_agent/tools/__init__.py

@@ -0,0 +1,8 @@
+"""
+Tools 包 - 工具注册和 Schema 生成
+"""
+
+from reson_agent.tools.registry import ToolRegistry, tool, get_tool_registry
+from reson_agent.tools.schema import SchemaGenerator
+
+__all__ = ["ToolRegistry", "tool", "get_tool_registry", "SchemaGenerator"]

+ 286 - 0
reson_agent/tools/registry.py

@@ -0,0 +1,286 @@
+"""
+Tool Registry - 工具注册表和装饰器
+
+职责:
+1. @tool 装饰器:自动注册工具并生成 Schema
+2. 管理所有工具的 Schema 和实现
+3. 路由工具调用到具体实现
+
+从 Resonote/llm/tools/registry.py 抽取
+"""
+
+import json
+import inspect
+import logging
+from typing import Any, Callable, Dict, List, Optional
+
+logger = logging.getLogger(__name__)
+
+
+class ToolRegistry:
+    """工具注册表"""
+
+    def __init__(self):
+        self._tools: Dict[str, Dict[str, Any]] = {}
+
+    def register(
+        self,
+        func: Callable,
+        schema: Optional[Dict] = None,
+        requires_confirmation: bool = False,
+        editable_params: Optional[List[str]] = None,
+        display: Optional[Dict[str, Dict[str, Any]]] = None
+    ):
+        """
+        注册工具
+
+        Args:
+            func: 工具函数
+            schema: 工具 Schema(如果为 None,自动生成)
+            requires_confirmation: 是否需要用户确认
+            editable_params: 允许用户编辑的参数列表
+            display: i18n 展示信息 {"zh": {"name": "xx", "params": {...}}, "en": {...}}
+        """
+        func_name = func.__name__
+
+        # 如果没有提供 Schema,自动生成
+        if schema is None:
+            try:
+                from reson_agent.tools.schema import SchemaGenerator
+                schema = SchemaGenerator.generate(func)
+            except Exception as e:
+                logger.error(f"Failed to generate schema for {func_name}: {e}")
+                raise
+
+        self._tools[func_name] = {
+            "func": func,
+            "schema": schema,
+            "ui_metadata": {
+                "requires_confirmation": requires_confirmation,
+                "editable_params": editable_params or [],
+                "display": display or {}
+            }
+        }
+
+        logger.debug(
+            f"[ToolRegistry] Registered: {func_name} "
+            f"(requires_confirmation={requires_confirmation}, "
+            f"editable_params={editable_params or []})"
+        )
+
+    def is_registered(self, tool_name: str) -> bool:
+        """检查工具是否已注册"""
+        return tool_name in self._tools
+
+    def get_schemas(self, tool_names: Optional[List[str]] = None) -> List[Dict]:
+        """
+        获取工具 Schema
+
+        Args:
+            tool_names: 工具名称列表(None = 所有工具)
+
+        Returns:
+            OpenAI Tool Schema 列表
+        """
+        if tool_names is None:
+            tool_names = list(self._tools.keys())
+
+        schemas = []
+        for name in tool_names:
+            if name in self._tools:
+                schemas.append(self._tools[name]["schema"])
+            else:
+                logger.warning(f"[ToolRegistry] Tool not found: {name}")
+
+        return schemas
+
+    def get_tool_names(self) -> List[str]:
+        """获取所有注册的工具名称"""
+        return list(self._tools.keys())
+
+    async def execute(
+        self,
+        name: str,
+        arguments: Dict[str, Any],
+        uid: str = "",
+        context: Optional[Dict[str, Any]] = None
+    ) -> str:
+        """
+        执行工具调用
+
+        Args:
+            name: 工具名称
+            arguments: 工具参数
+            uid: 用户ID(自动注入)
+            context: 额外上下文
+
+        Returns:
+            JSON 字符串格式的结果
+        """
+        if name not in self._tools:
+            error_msg = f"Unknown tool: {name}"
+            logger.error(f"[ToolRegistry] {error_msg}")
+            return json.dumps({"error": error_msg}, ensure_ascii=False)
+
+        try:
+            func = self._tools[name]["func"]
+
+            # 注入 uid
+            kwargs = {**arguments, "uid": uid}
+
+            # 注入 context(如果函数接受)
+            sig = inspect.signature(func)
+            if "context" in sig.parameters:
+                kwargs["context"] = context
+
+            # 执行函数
+            if inspect.iscoroutinefunction(func):
+                result = await func(**kwargs)
+            else:
+                result = func(**kwargs)
+
+            # 返回 JSON 字符串
+            if isinstance(result, str):
+                return result
+            return json.dumps(result, ensure_ascii=False, indent=2)
+
+        except Exception as e:
+            error_msg = f"Error executing tool '{name}': {str(e)}"
+            logger.error(f"[ToolRegistry] {error_msg}")
+            import traceback
+            logger.error(traceback.format_exc())
+            return json.dumps({"error": error_msg}, ensure_ascii=False)
+
+    def check_confirmation_required(self, tool_calls: List[Dict]) -> bool:
+        """检查是否有工具需要用户确认"""
+        for tc in tool_calls:
+            tool_name = tc.get("function", {}).get("name")
+            if tool_name and tool_name in self._tools:
+                if self._tools[tool_name]["ui_metadata"].get("requires_confirmation", False):
+                    return True
+        return False
+
+    def get_confirmation_flags(self, tool_calls: List[Dict]) -> List[bool]:
+        """返回每个工具是否需要确认"""
+        flags = []
+        for tc in tool_calls:
+            tool_name = tc.get("function", {}).get("name")
+            if tool_name and tool_name in self._tools:
+                flags.append(self._tools[tool_name]["ui_metadata"].get("requires_confirmation", False))
+            else:
+                flags.append(False)
+        return flags
+
+    def check_any_param_editable(self, tool_calls: List[Dict]) -> bool:
+        """检查是否有任何工具允许参数编辑"""
+        for tc in tool_calls:
+            tool_name = tc.get("function", {}).get("name")
+            if tool_name and tool_name in self._tools:
+                editable_params = self._tools[tool_name]["ui_metadata"].get("editable_params", [])
+                if editable_params:
+                    return True
+        return False
+
+    def get_editable_params_map(self, tool_calls: List[Dict]) -> Dict[str, List[str]]:
+        """返回每个工具调用的可编辑参数列表"""
+        params_map = {}
+        for tc in tool_calls:
+            tool_call_id = tc.get("id")
+            tool_name = tc.get("function", {}).get("name")
+
+            if tool_name and tool_name in self._tools:
+                editable_params = self._tools[tool_name]["ui_metadata"].get("editable_params", [])
+                params_map[tool_call_id] = editable_params
+            else:
+                params_map[tool_call_id] = []
+
+        return params_map
+
+    def get_ui_metadata(
+        self,
+        locale: str = "zh",
+        tool_names: Optional[List[str]] = None
+    ) -> Dict[str, Dict[str, Any]]:
+        """
+        获取工具的UI元数据(用于前端展示)
+
+        Returns:
+            {
+                "tool_name": {
+                    "display_name": "搜索笔记",
+                    "param_display_names": {"query": "搜索关键词"},
+                    "requires_confirmation": false,
+                    "editable_params": ["query"]
+                }
+            }
+        """
+        if tool_names is None:
+            tool_names = list(self._tools.keys())
+
+        metadata = {}
+        for name in tool_names:
+            if name not in self._tools:
+                continue
+
+            ui_meta = self._tools[name]["ui_metadata"]
+            display = ui_meta.get("display", {}).get(locale, {})
+
+            metadata[name] = {
+                "display_name": display.get("name", name),
+                "param_display_names": display.get("params", {}),
+                "requires_confirmation": ui_meta.get("requires_confirmation", False),
+                "editable_params": ui_meta.get("editable_params", [])
+            }
+
+        return metadata
+
+
+# 全局单例
+_global_registry = ToolRegistry()
+
+
+def tool(
+    description: Optional[str] = None,
+    param_descriptions: Optional[Dict[str, str]] = None,
+    requires_confirmation: bool = False,
+    editable_params: Optional[List[str]] = None,
+    display: Optional[Dict[str, Dict[str, Any]]] = None
+):
+    """
+    工具装饰器 - 自动注册工具并生成 Schema
+
+    Args:
+        description: 函数描述(可选,从 docstring 提取)
+        param_descriptions: 参数描述(可选,从 docstring 提取)
+        requires_confirmation: 是否需要用户确认(默认 False)
+        editable_params: 允许用户编辑的参数列表
+        display: i18n 展示信息
+
+    Example:
+        @tool(
+            editable_params=["query"],
+            display={
+                "zh": {"name": "搜索笔记", "params": {"query": "搜索关键词"}},
+                "en": {"name": "Search Notes", "params": {"query": "Query"}}
+            }
+        )
+        async def search_blocks(query: str, limit: int = 10, uid: str = "") -> str:
+            '''搜索用户的笔记块'''
+            ...
+    """
+    def decorator(func: Callable) -> Callable:
+        # 注册到全局 registry
+        _global_registry.register(
+            func,
+            requires_confirmation=requires_confirmation,
+            editable_params=editable_params,
+            display=display
+        )
+        return func
+
+    return decorator
+
+
+def get_tool_registry() -> ToolRegistry:
+    """获取全局工具注册表"""
+    return _global_registry

+ 176 - 0
reson_agent/tools/schema.py

@@ -0,0 +1,176 @@
+"""
+Schema Generator - 从函数签名自动生成 OpenAI Tool Schema
+
+职责:
+1. 解析函数签名(参数、类型注解、默认值)
+2. 解析 docstring(Google 风格)
+3. 生成 OpenAI Tool Calling 格式的 JSON Schema
+
+从 Resonote/llm/tools/schema.py 抽取
+"""
+
+import inspect
+import logging
+from typing import Any, Dict, List, Optional, get_args, get_origin
+
+logger = logging.getLogger(__name__)
+
+# 尝试导入 docstring_parser,如果不可用则提供降级方案
+try:
+    from docstring_parser import parse as parse_docstring
+    HAS_DOCSTRING_PARSER = True
+except ImportError:
+    HAS_DOCSTRING_PARSER = False
+    logger.warning("docstring_parser not installed, using fallback docstring parsing")
+
+
+def _simple_parse_docstring(docstring: str) -> tuple[str, Dict[str, str]]:
+    """简单的 docstring 解析(降级方案)"""
+    if not docstring:
+        return "", {}
+
+    lines = docstring.strip().split("\n")
+    description = lines[0] if lines else ""
+    param_descriptions = {}
+
+    # 简单解析 Args: 部分
+    in_args = False
+    for line in lines[1:]:
+        line = line.strip()
+        if line.lower().startswith("args:"):
+            in_args = True
+            continue
+        if line.lower().startswith(("returns:", "raises:", "example:")):
+            in_args = False
+            continue
+        if in_args and ":" in line:
+            parts = line.split(":", 1)
+            param_name = parts[0].strip()
+            param_desc = parts[1].strip() if len(parts) > 1 else ""
+            param_descriptions[param_name] = param_desc
+
+    return description, param_descriptions
+
+
+class SchemaGenerator:
+    """从函数生成 OpenAI Tool Schema"""
+
+    # Python 类型到 JSON Schema 类型的映射
+    TYPE_MAP = {
+        str: "string",
+        int: "integer",
+        float: "number",
+        bool: "boolean",
+        list: "array",
+        dict: "object",
+        List: "array",
+        Dict: "object",
+    }
+
+    @classmethod
+    def generate(cls, func: callable) -> Dict[str, Any]:
+        """
+        从函数生成 OpenAI Tool Schema
+
+        Args:
+            func: 要生成 Schema 的函数
+
+        Returns:
+            OpenAI Tool Schema(JSON 格式)
+        """
+        # 解析函数签名
+        sig = inspect.signature(func)
+        func_name = func.__name__
+
+        # 解析 docstring
+        if HAS_DOCSTRING_PARSER:
+            doc = parse_docstring(func.__doc__ or "")
+            func_description = doc.short_description or doc.long_description or f"Call {func_name}"
+            param_descriptions = {p.arg_name: p.description for p in doc.params if p.description}
+        else:
+            func_description, param_descriptions = _simple_parse_docstring(func.__doc__ or "")
+            if not func_description:
+                func_description = f"Call {func_name}"
+
+        # 生成参数 Schema
+        properties = {}
+        required = []
+
+        for param_name, param in sig.parameters.items():
+            # 跳过特殊参数
+            if param_name in ["self", "cls", "kwargs", "context"]:
+                continue
+
+            # 跳过 uid(由框架自动注入)
+            if param_name == "uid":
+                continue
+
+            # 获取类型注解
+            param_type = param.annotation if param.annotation != inspect.Parameter.empty else str
+
+            # 生成参数 Schema
+            param_schema = cls._type_to_schema(param_type)
+
+            # 添加描述
+            if param_name in param_descriptions:
+                param_schema["description"] = param_descriptions[param_name]
+
+            # 添加默认值
+            if param.default != inspect.Parameter.empty:
+                param_schema["default"] = param.default
+            else:
+                required.append(param_name)
+
+            properties[param_name] = param_schema
+
+        # 构建完整的 Schema
+        schema = {
+            "type": "function",
+            "function": {
+                "name": func_name,
+                "description": func_description,
+                "parameters": {
+                    "type": "object",
+                    "properties": properties,
+                    "required": required
+                }
+            }
+        }
+
+        return schema
+
+    @classmethod
+    def _type_to_schema(cls, python_type: Any) -> Dict[str, Any]:
+        """将 Python 类型转换为 JSON Schema"""
+        # 处理 Optional[T]
+        origin = get_origin(python_type)
+        args = get_args(python_type)
+
+        if origin is Optional.__class__ or (origin and str(origin) == "typing.Union"):
+            # Optional[T] = Union[T, None]
+            if len(args) == 2 and type(None) in args:
+                inner_type = args[0] if args[1] is type(None) else args[1]
+                schema = cls._type_to_schema(inner_type)
+                return schema
+
+        # 处理 List[T]
+        if origin is list or origin is List:
+            if args:
+                item_type = args[0]
+                return {
+                    "type": "array",
+                    "items": cls._type_to_schema(item_type)
+                }
+            return {"type": "array"}
+
+        # 处理 Dict[K, V]
+        if origin is dict or origin is Dict:
+            return {"type": "object"}
+
+        # 处理基础类型
+        if python_type in cls.TYPE_MAP:
+            return {"type": cls.TYPE_MAP[python_type]}
+
+        # 默认为 string
+        logger.warning(f"Unknown type {python_type}, defaulting to string")
+        return {"type": "string"}

+ 0 - 0
tests/__init__.py


+ 252 - 0
tests/test_runner.py

@@ -0,0 +1,252 @@
+"""
+AgentRunner 测试
+"""
+
+import pytest
+from reson_agent import (
+    AgentRunner,
+    AgentEvent,
+    Trace,
+    Step,
+    Experience,
+    Skill,
+    tool,
+    get_tool_registry,
+)
+from reson_agent.storage import MemoryTraceStore, MemoryMemoryStore
+
+
+# 测试工具
+@tool(
+    editable_params=["query"],
+    display={"zh": {"name": "测试搜索", "params": {"query": "关键词"}}}
+)
+async def test_search(query: str, limit: int = 10, uid: str = "") -> dict:
+    """测试搜索工具"""
+    return {"results": [f"结果: {query}"], "count": 1}
+
+
+# Mock LLM 调用
+async def mock_llm_call(
+    messages: list,
+    model: str = "gpt-4o",
+    tools: list = None,
+    **kwargs
+) -> dict:
+    """模拟 LLM 调用"""
+    # 简单模拟:如果有工具,第一次调用返回 tool_call,第二次返回结果
+    user_msg = messages[-1]["content"] if messages else ""
+
+    if "搜索" in user_msg and tools:
+        return {
+            "content": "",
+            "tool_calls": [{
+                "id": "call_123",
+                "function": {
+                    "name": "test_search",
+                    "arguments": '{"query": "测试查询"}'
+                }
+            }],
+            "prompt_tokens": 100,
+            "completion_tokens": 50,
+            "cost": 0.01
+        }
+
+    return {
+        "content": f"回复: {user_msg}",
+        "tool_calls": None,
+        "prompt_tokens": 100,
+        "completion_tokens": 50,
+        "cost": 0.01
+    }
+
+
+class TestTraceAndStep:
+    """测试 Trace 和 Step"""
+
+    def test_trace_create(self):
+        trace = Trace.create(mode="call", uid="user123")
+        assert trace.trace_id is not None
+        assert trace.mode == "call"
+        assert trace.uid == "user123"
+        assert trace.status == "running"
+
+    def test_step_create(self):
+        step = Step.create(
+            trace_id="trace_123",
+            step_type="llm_call",
+            sequence=0,
+            data={"response": "hello"}
+        )
+        assert step.step_id is not None
+        assert step.trace_id == "trace_123"
+        assert step.step_type == "llm_call"
+        assert step.data["response"] == "hello"
+
+
+class TestMemoryStore:
+    """测试内存存储"""
+
+    @pytest.mark.asyncio
+    async def test_trace_store(self):
+        store = MemoryTraceStore()
+
+        # 创建 Trace
+        trace = Trace.create(mode="agent", task="测试任务")
+        trace_id = await store.create_trace(trace)
+        assert trace_id == trace.trace_id
+
+        # 获取 Trace
+        retrieved = await store.get_trace(trace_id)
+        assert retrieved is not None
+        assert retrieved.task == "测试任务"
+
+        # 添加 Step
+        step = Step.create(
+            trace_id=trace_id,
+            step_type="llm_call",
+            sequence=0
+        )
+        await store.add_step(step)
+
+        # 获取 Steps
+        steps = await store.get_trace_steps(trace_id)
+        assert len(steps) == 1
+
+    @pytest.mark.asyncio
+    async def test_memory_store(self):
+        store = MemoryMemoryStore()
+
+        # 添加 Experience
+        exp = Experience.create(
+            scope="agent:test",
+            condition="测试条件",
+            rule="测试规则"
+        )
+        exp_id = await store.add_experience(exp)
+        assert exp_id == exp.exp_id
+
+        # 搜索
+        results = await store.search_experiences("agent:test", "")
+        assert len(results) == 1
+
+
+class TestToolRegistry:
+    """测试工具注册"""
+
+    def test_tool_registered(self):
+        registry = get_tool_registry()
+        assert registry.is_registered("test_search")
+
+    def test_get_schemas(self):
+        registry = get_tool_registry()
+        schemas = registry.get_schemas(["test_search"])
+        assert len(schemas) == 1
+        assert schemas[0]["function"]["name"] == "test_search"
+
+    @pytest.mark.asyncio
+    async def test_execute_tool(self):
+        registry = get_tool_registry()
+        result = await registry.execute("test_search", {"query": "hello"}, uid="test")
+        assert "结果" in result
+
+
+class TestAgentRunner:
+    """测试 AgentRunner"""
+
+    @pytest.mark.asyncio
+    async def test_call_simple(self):
+        """测试简单调用"""
+        runner = AgentRunner(
+            trace_store=MemoryTraceStore(),
+            llm_call=mock_llm_call
+        )
+
+        result = await runner.call(
+            messages=[{"role": "user", "content": "你好"}],
+            model="gpt-4o"
+        )
+
+        assert "你好" in result.reply
+        assert result.trace_id is not None
+
+    @pytest.mark.asyncio
+    async def test_run_simple(self):
+        """测试 Agent 运行"""
+        runner = AgentRunner(
+            trace_store=MemoryTraceStore(),
+            memory_store=MemoryMemoryStore(),
+            llm_call=mock_llm_call
+        )
+
+        events = []
+        async for event in runner.run(
+            task="简单任务",
+            agent_type="test"
+        ):
+            events.append(event)
+
+        # 检查事件序列
+        event_types = [e.type for e in events]
+        assert "trace_started" in event_types
+        assert "trace_completed" in event_types
+
+    @pytest.mark.asyncio
+    async def test_run_with_tools(self):
+        """测试带工具的 Agent 运行"""
+        runner = AgentRunner(
+            trace_store=MemoryTraceStore(),
+            llm_call=mock_llm_call
+        )
+
+        events = []
+        async for event in runner.run(
+            task="请搜索相关内容",
+            tools=["test_search"],
+            agent_type="test"
+        ):
+            events.append(event)
+
+        event_types = [e.type for e in events]
+        assert "tool_executing" in event_types
+        assert "tool_result" in event_types
+
+    @pytest.mark.asyncio
+    async def test_add_feedback(self):
+        """测试添加反馈"""
+        trace_store = MemoryTraceStore()
+        memory_store = MemoryMemoryStore()
+
+        runner = AgentRunner(
+            trace_store=trace_store,
+            memory_store=memory_store,
+            llm_call=mock_llm_call
+        )
+
+        # 先运行一个任务
+        trace_id = None
+        step_id = None
+        async for event in runner.run(task="测试任务", agent_type="test"):
+            if event.type == "trace_started":
+                trace_id = event.data["trace_id"]
+            if event.type == "llm_call_completed":
+                step_id = event.data["step_id"]
+
+        # 添加反馈
+        exp_id = await runner.add_feedback(
+            trace_id=trace_id,
+            target_step_id=step_id,
+            feedback_type="correction",
+            content="应该这样做"
+        )
+
+        assert exp_id is not None
+
+        # 验证经验被存储
+        exp = await memory_store.get_experience(exp_id)
+        assert exp is not None
+        assert exp.rule == "应该这样做"
+
+
+if __name__ == "__main__":
+    pytest.main([__file__, "-v"])